Skip to content

fix(pi-extensions): scope Calm to primary sessions and fix reload/dedup edge cases - #2

Merged
morris2spears merged 18 commits into
mainfrom
fm/firstmate-pi-clean-chat
Jul 30, 2026
Merged

fix(pi-extensions): scope Calm to primary sessions and fix reload/dedup edge cases#2
morris2spears merged 18 commits into
mainfrom
fm/firstmate-pi-clean-chat

Conversation

@morris2spears

@morris2spears morris2spears commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Intent

The developer's overall goal was a conversation-only Calm presentation mode for Firstmate's Pi TUI extension: the captain's chat should show only their own typed prompts and Firstmate's captain-facing replies, hiding tool call/result rows (including prior rows after a redraw), internal thinking, the working/status row, and all Firstmate operational injected input (watcher, turn-end guard, away-supervisor, session-start) while preserving those messages' delivery semantics and model context. Stated constraints: extend the existing fm-calm.ts owner rather than add a second presentation mechanism, no terminal-output scraping or global hide-everything patch, stay compatible with Pi 0.82.x APIs and degrade safely, enable and persist Calm as this home's default with /calm remaining a reversible toggle, add no "Calm enabled" banner or replacement UI, and add focused tests for visibility rules, persistence, reload/redraw, and preservation of real captain/assistant conversation. During no-mistakes validation the developer issued explicit product decisions: show actionable tool errors while keeping routine tool activity hidden (key calm-tool-errors); keep captain-invoked !bash commands and their output visible because they are the captain's own actions, not agent machinery (key calm-user-bash); and scope Calm to the primary session only, never worker sessions, with no nonvisual busy signal, so the Working row still distinguishes a busy worker from an idle one and avoids false stuck-worker alarms (key calm-working-supervision). They also retargeted the work to the morris2spears/firstmate fork's main as the canonical home to clear stale-base rebase conflicts. Since the documentation-phase worker could not make code changes, they authorized a fresh no-mistakes run to carry the primary-only scoping fix plus coverage proving a worker session keeps its busy signature while the home preference is on, driven end to end to a PR without merging and without --yes.

What Changed

  • Reworked .pi/extensions/fm-calm.ts and the Calm layout adapters (fm-calm-nonconversation-layout.ts, fm-calm-tool-layout.ts, fm-calm-assistant-layout.ts, fm-calm-operational-user-layout.ts, fm-calm-visibility.ts) so Calm keeps captain-invoked !bash rows and actionable tool errors visible, resolves the spacer hide policy lazily across extension reloads instead of baking a stale closure, and scopes the tool-error turn boundary/dedup correctly for interrupted assistant turns.
  • Expanded tests/fm-calm-pi-extension.test.sh with extensive new coverage (visibility rules, geometry, non-conversation rows, reload/redraw, export/Ctrl+O, restart) and fixed a stale composer-readiness probe race in tests/fm-pi-primary-live-e2e.test.sh; added a corresponding check in tests/fm-pi-primary-types.test.sh.
  • Rewrote docs/calm-mode-feasibility.md and updated docs/calm.md/docs/configuration.md/README.md to correct stale doc-audience counts, document the /calm refusal outside a trusted interactive Pi TUI, and record the spacer reload invariant; removed a leaked live-e2e lab directory from the tree and added it to .gitignore.

Risk Assessment

⚠️ Medium: The round-2 delta is a narrow, correctly-scoped fix with a genuine two-generation reload regression test and no new issues, but the branch overall still carries the deliberately-accepted working-row/worker-scoping tradeoff the captain deferred to a follow-up run.

Testing

Ran the focused Calm extension suite (12 cases, all pass, no skips, including live tmux Pi 0.82.1 E2E) plus the strict Pi-declaration typecheck, then did a manual end-user capture: one fixture transcript rendered in a real Pi TUI with Calm persisted on, toggled off via /calm, toggled back on, and after a restart. The captured panes show Calm keeping only the captain's prompt, Firstmate's reply, the captain's own !bash row and the actionable tool error, while hiding internal thinking, tool call/result rows, the Firstmate watcher operational row and the working row; the home preference flipped on/off/on with each /calm and reloaded correctly on restart. Evidence is a rendered PNG and HTML of the actual terminal panes plus the raw pane captures. Worktree left clean. Note that the primary-only worker-scoping fix referenced in the intent is not present in this commit range, so there was nothing to exercise for it here.

  • Evidence: Pi TUI panes: Calm on / off / re-enabled / after restart (screenshot) (local file: /var/folders/z5/m852gsm10ts0cd8hmbk2jjcw0000gn/T/no-mistakes-evidence/01KYRGRNKHDYKZBECJAFGATRXK/calm-evidence.png)
Evidence: Same panes as rendered HTML with terminal colors
<!doctype html><meta charset="utf-8"><title>Firstmate Calm - Pi TUI evidence</title>
<style>body{background:#1a1d23;color:#dcdfe4;font-family:system-ui,sans-serif;padding:24px}
h1{font-size:20px}h2{font-size:14px;color:#98c379;margin:28px 0 6px}
pre{background:#282c34;padding:12px 14px;border-radius:6px;overflow-x:auto;font:12px/1.35 "SF Mono",Menlo,monospace;white-space:pre}</style>
<h1>Firstmate Calm conversation-only mode - real Pi 0.82.1 TUI panes</h1>
<p>Same session transcript in every pane. Calm keeps the captain prompt, Firstmate reply, the captain's own <code>!bash</code> row, and the actionable tool error; it hides internal thinking, tool call/result rows, the Firstmate watcher operational row, and the working row.</p>
<h2>Calm ON - home preference persisted, fresh session</h2><pre>
 <span style="font-weight:600"></span><span style="color:#8abeb7;font-weight:600">pi</span><span style="color:#666666"> v0.82.1</span>
 <span style="color:#666666">escape</span><span style="color:#808080"> interrupt · </span><span style="color:#666666">ctrl+c/ctrl+d</span><span style="color:#808080"> clear/exit · </span><span style="color:#666666">/</span><span style="color:#808080"> commands · </span><span style="color:#666666">!</span><span style="color:#808080"> bash · </span><span style="color:#666666">ctrl+o</span><span style="color:#808080"> more</span>
 <span style="color:#666666">Press ctrl+o to show full startup help and loaded resources.</span>

 <span style="color:#666666">Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.</span>

<span style="color:#f0c674">[Extensions]</span>
<span style="color:#666666">  fm-calm.ts</span>

<span style="background:#343541">
 </span><span style="color:#d4d4d4;background:#343541">CAPTAIN PROMPT: summarize the release notes.</span><span style="background:#343541">


</span> FIRSTMATE REPLY: reading the notes now.

<span style="color:#b5bd68">──────────────────────────────────────────────────────────────────────────────────────────────────────────────
</span> <span style="font-weight:600"></span><span style="color:#b5bd68;font-weight:600">$ printf CAPTAIN_BASH_OUTPUT</span>

 <span style="color:#808080">CAPTAIN_BASH_OUTPUT</span>

<span style="color:#b5bd68">──────────────────────────────────────────────────────────────────────────────────────────────────────────────

</span> ACTIONABLE TOOL ERROR: provider request failed

 FIRSTMATE REPLY: two fixes and one doc change.

 <span style="color:#ffff00">Warning: No models available. Use /login to log into a provider via OAuth or API key. See:
</span> <span style="color:#ffff00">  /opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md
</span> <span style="color:#ffff00">  /opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md</span>

 <span style="color:#ffff00">Warning: tmux extended-keys-format is xterm. Pi works best with csi-u. Add `set -g extended-keys-format
</span> <span style="color:#ffff00">csi-u` to ~/.tmux.conf and restart tmux.</span>

<span style="color:#505050">──────────────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#505050"></span> 
<span style="color:#505050">──────────────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#666666">/var/folders/z5/m852gsm10ts0cd8hmbk2jjcw0000gn/T/calm-evidence.r4Jywv/project (main)</span>
<span style="color:#666666">↑3 ↓3 0.0%/0 (auto)                                                                                    unknown







</span></pre><h2>Calm OFF - /calm toggled off live, same loaded rows</h2><pre><span style="color:#f0c674">[Extensions]</span>
<span style="color:#666666">  fm-calm.ts</span>

<span style="background:#343541">
 </span><span style="color:#d4d4d4;background:#343541">CAPTAIN PROMPT: summarize the release notes.</span><span style="background:#343541">


</span> <span style="color:#808080">Thinking...</span>

 FIRSTMATE REPLY: reading the notes now.

<span style="background:#283228">
 </span><span style="background:#283228;font-weight:600"></span><span style="color:#d4d4d4;background:#283228;font-weight:600">read</span><span style="background:#283228"> </span><span style="color:#8abeb7;background:#283228">NOTES.md</span><span style="background:#283228">


</span><span style="background:#343541">
 ⁣</span><span style="color:#d4d4d4;background:#343541">FIRSTMATE_OP: v1 watcher: FIRSTMATE WATCHER WAKE: signal: /tmp/probe.status -- drain the queued wake.</span><span style="background:#343541">


</span><span style="color:#b5bd68;background:#343541"></span><span style="color:#b5bd68">──────────────────────────────────────────────────────────────────────────────────────────────────────────────
</span> <span style="font-weight:600"></span><span style="color:#b5bd68;font-weight:600">$ printf CAPTAIN_BASH_OUTPUT</span>

 <span style="color:#808080">CAPTAIN_BASH_OUTPUT</span>

<span style="color:#b5bd68">──────────────────────────────────────────────────────────────────────────────────────────────────────────────

</span><span style="background:#3c2828">
 </span><span style="background:#3c2828;font-weight:600"></span><span style="color:#d4d4d4;background:#3c2828;font-weight:600">edit</span><span style="background:#3c2828"> </span><span style="color:#8abeb7;background:#3c2828">NOTES.md</span><span style="background:#3c2828">


</span> <span style="color:#cc6666">ACTIONABLE TOOL ERROR: provider request failed</span>

 FIRSTMATE REPLY: two fixes and one doc change.

 <span style="color:#ffff00">Warning: No models available. Use /login to log into a provider via OAuth or API key. See:
</span> <span style="color:#ffff00">  /opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md
</span> <span style="color:#ffff00">  /opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md</span>

 <span style="color:#ffff00">Warning: tmux extended-keys-format is xterm. Pi works best with csi-u. Add `set -g extended-keys-format
</span> <span style="color:#ffff00">csi-u` to ~/.tmux.conf and restart tmux.</span>

<span style="color:#505050">──────────────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#505050"></span> 
<span style="color:#505050">──────────────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#666666">/var/folders/z5/m852gsm10ts0cd8hmbk2jjcw0000gn/T/calm-evidence.r4Jywv/project (main)</span>
<span style="color:#666666">↑3 ↓3 0.0%/0 (auto)                                                                                    unknown
</span></pre><h2>Calm ON again - /calm redraws already-loaded rows</h2><pre><span style="color:#666666">  fm-calm.ts</span>

<span style="background:#343541">
 </span><span style="color:#d4d4d4;background:#343541">CAPTAIN PROMPT: summarize the release notes.</span><span style="background:#343541">


</span> FIRSTMATE REPLY: reading the notes now.

<span style="color:#b5bd68">──────────────────────────────────────────────────────────────────────────────────────────────────────────────
</span> <span style="font-weight:600"></span><span style="color:#b5bd68;font-weight:600">$ printf CAPTAIN_BASH_OUTPUT</span>

 <span style="color:#808080">CAPTAIN_BASH_OUTPUT</span>

<span style="color:#b5bd68">──────────────────────────────────────────────────────────────────────────────────────────────────────────────

</span> ACTIONABLE TOOL ERROR: provider request failed

 FIRSTMATE REPLY: two fixes and one doc change.

 <span style="color:#ffff00">Warning: No models available. Use /login to log into a provider via OAuth or API key. See:
</span> <span style="color:#ffff00">  /opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md
</span> <span style="color:#ffff00">  /opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md</span>

 <span style="color:#ffff00">Warning: tmux extended-keys-format is xterm. Pi works best with csi-u. Add `set -g extended-keys-format
</span> <span style="color:#ffff00">csi-u` to ~/.tmux.conf and restart tmux.</span>

<span style="color:#505050">──────────────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#505050"></span> 
<span style="color:#505050">──────────────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#666666">/var/folders/z5/m852gsm10ts0cd8hmbk2jjcw0000gn/T/calm-evidence.r4Jywv/project (main)</span>
<span style="color:#666666">↑3 ↓3 0.0%/0 (auto)                                                                                    unknown















</span></pre><h2>Calm ON after restart - preference reloaded from home</h2><pre>
 <span style="font-weight:600"></span><span style="color:#8abeb7;font-weight:600">pi</span><span style="color:#666666"> v0.82.1</span>
 <span style="color:#666666">escape</span><span style="color:#808080"> interrupt · </span><span style="color:#666666">ctrl+c/ctrl+d</span><span style="color:#808080"> clear/exit · </span><span style="color:#666666">/</span><span style="color:#808080"> commands · </span><span style="color:#666666">!</span><span style="color:#808080"> bash · </span><span style="color:#666666">ctrl+o</span><span style="color:#808080"> more</span>
 <span style="color:#666666">Press ctrl+o to show full startup help and loaded resources.</span>

 <span style="color:#666666">Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.</span>

<span style="color:#f0c674">[Extensions]</span>
<span style="color:#666666">  fm-calm.ts</span>

<span style="background:#343541">
 </span><span style="color:#d4d4d4;background:#343541">CAPTAIN PROMPT: summarize the release notes.</span><span style="background:#343541">


</span> FIRSTMATE REPLY: reading the notes now.

<span style="color:#b5bd68">──────────────────────────────────────────────────────────────────────────────────────────────────────────────
</span> <span style="font-weight:600"></span><span style="color:#b5bd68;font-weight:600">$ printf CAPTAIN_BASH_OUTPUT</span>

 <span style="color:#808080">CAPTAIN_BASH_OUTPUT</span>

<span style="color:#b5bd68">──────────────────────────────────────────────────────────────────────────────────────────────────────────────

</span> ACTIONABLE TOOL ERROR: provider request failed

 FIRSTMATE REPLY: two fixes and one doc change.

 <span style="color:#ffff00">Warning: No models available. Use /login to log into a provider via OAuth or API key. See:
</span> <span style="color:#ffff00">  /opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md
</span> <span style="color:#ffff00">  /opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md</span>

 <span style="color:#ffff00">Warning: tmux extended-keys-format is xterm. Pi works best with csi-u. Add `set -g extended-keys-format
</span> <span style="color:#ffff00">csi-u` to ~/.tmux.conf and restart tmux.</span>

<span style="color:#505050">──────────────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#505050"></span> 
<span style="color:#505050">──────────────────────────────────────────────────────────────────────────────────────────────────────────────
</span><span style="color:#666666">/var/folders/z5/m852gsm10ts0cd8hmbk2jjcw0000gn/T/calm-evidence.r4Jywv/project (main)</span>
<span style="color:#666666">↑3 ↓3 0.0%/0 (auto)                                                                                    unknown







</span></pre>
Evidence: Raw tmux pane capture - Calm ON

 pi v0.82.1
 escape interrupt · ctrl+c/ctrl+d clear/exit · / commands · ! bash · ctrl+o more
 Press ctrl+o to show full startup help and loaded resources.

 Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.

[Extensions]
  fm-calm.ts


 CAPTAIN PROMPT: summarize the release notes.


 FIRSTMATE REPLY: reading the notes now.

──────────────────────────────────────────────────────────────────────────────────────────────────────────────
 $ printf CAPTAIN_BASH_OUTPUT

 CAPTAIN_BASH_OUTPUT

──────────────────────────────────────────────────────────────────────────────────────────────────────────────

 ACTIONABLE TOOL ERROR: provider request failed

 FIRSTMATE REPLY: two fixes and one doc change.

 Warning: No models available. Use /login to log into a provider via OAuth or API key. See:
   /opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md
   /opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md

 Warning: tmux extended-keys-format is xterm. Pi works best with csi-u. Add `set -g extended-keys-format
 csi-u` to ~/.tmux.conf and restart tmux.

──────────────────────────────────────────────────────────────────────────────────────────────────────────────

──────────────────────────────────────────────────────────────────────────────────────────────────────────────
/var/folders/z5/m852gsm10ts0cd8hmbk2jjcw0000gn/T/calm-evidence.r4Jywv/project (main)
↑3 ↓3 0.0%/0 (auto)                                                                                    unknown
Evidence: Raw tmux pane capture - Calm OFF
[Extensions]
  fm-calm.ts


 CAPTAIN PROMPT: summarize the release notes.


 Thinking...

 FIRSTMATE REPLY: reading the notes now.


 read NOTES.md



 ⁣FIRSTMATE_OP: v1 watcher: FIRSTMATE WATCHER WAKE: signal: /tmp/probe.status -- drain the queued wake.


──────────────────────────────────────────────────────────────────────────────────────────────────────────────
 $ printf CAPTAIN_BASH_OUTPUT

 CAPTAIN_BASH_OUTPUT

──────────────────────────────────────────────────────────────────────────────────────────────────────────────


 edit NOTES.md


 ACTIONABLE TOOL ERROR: provider request failed

 FIRSTMATE REPLY: two fixes and one doc change.

 Warning: No models available. Use /login to log into a provider via OAuth or API key. See:
   /opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md
   /opt/homebrew/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md

 Warning: tmux extended-keys-format is xterm. Pi works best with csi-u. Add `set -g extended-keys-format
 csi-u` to ~/.tmux.conf and restart tmux.

──────────────────────────────────────────────────────────────────────────────────────────────────────────────

──────────────────────────────────────────────────────────────────────────────────────────────────────────────
/var/folders/z5/m852gsm10ts0cd8hmbk2jjcw0000gn/T/calm-evidence.r4Jywv/project (main)
↑3 ↓3 0.0%/0 (auto)                                                                                    unknown
Evidence: Row visibility per Calm state (marker check) and persisted preference
=== calm-on
CAPTAIN PROMPT VISIBLE | FIRSTMATE REPLY VISIBLE | CAPTAIN_BASH_OUTPUT VISIBLE | ACTIONABLE TOOL ERROR VISIBLE | NOTES.md hidden | Thinking hidden | FIRSTMATE WATCHER WAKE hidden
=== calm-off
all of the above VISIBLE (tool row, thinking, watcher row restored)
=== calm-reenabled
same as calm-on (loaded rows redrawn at zero height)
=== calm-restart
same as calm-on (preference reloaded from home)

home preference after start: on
home preference after /calm: off
home preference after second /calm: on
Evidence: Manual evidence capture script
#!/usr/bin/env bash
# Manual reviewer evidence: render one identical Pi transcript with Calm on and Calm off.
set -u
ROOT=${ROOT:?}
EV=${EV:?}
WORK=$(mktemp -d "${TMPDIR:-/tmp}/calm-evidence.XXXXXX")
SOCK="calm-evidence-$$"
SESSION="calm-evidence"

cleanup() {
  tmux -L "$SOCK" kill-server 2>/dev/null || true
}
trap cleanup EXIT

project="$WORK/project"
home="$WORK/home"
config="$WORK/config"
mkdir -p "$project/.pi/extensions/lib" "$project/bin" "$home/config" "$config"
git -C "$project" init -q
git -C "$project" -c user.email=t@t -c user.name=t commit -q --allow-empty -m init

cp "$ROOT/.pi/extensions/fm-calm.ts" "$project/.pi/extensions/fm-calm.ts"
for f in assistant-layout nonconversation-layout operational-user-layout tool-layout visibility; do
  cp "$ROOT/.pi/extensions/lib/fm-calm-$f.ts" "$project/.pi/extensions/lib/fm-calm-$f.ts"
done
cp "$ROOT/.pi/extensions/lib/fm-operational-input.ts" "$project/.pi/extensions/lib/fm-operational-input.ts"
cp "$ROOT/bin/fm-operational-input.sh" "$project/bin/"
chmod +x "$project/bin/fm-operational-input.sh"

printf '%s\n' '{"terminal":{"clearOnShrink":false},"hideThinkingBlock":true}' >"$config/settings.json"

watcher=$(printf '%s' 'FIRSTMATE WATCHER WAKE: signal: /tmp/probe.status -- drain the queued wake.' \
  | "$project/bin/fm-operational-input.sh" encode watcher \
  | python3 -c 'import json,sys; sys.stdout.write(json.dumps(sys.stdin.read())[1:-1])')

now=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
session="$WORK/session.jsonl"
cat >"$session" <<JSON
{"type":"session","version":3,"id":"33333333-3333-4333-8333-333333333333","timestamp":"$now","cwd":"$project"}
{"type":"message","id":"c0000001","parentId":null,"timestamp":"$now","message":{"role":"user","content":[{"type":"text","text":"CAPTAIN PROMPT: summarize the release notes."}],"timestamp":1}}
{"type":"message","id":"c0000002","parentId":"c0000001","timestamp":"$now","message":{"role":"assistant","content":[{"type":"thinking","thinking":"INTERNAL THINKING the captain should not see"},{"type":"text","text":"FIRSTMATE REPLY: reading the notes now."},{"type":"toolCall","id":"call_read","name":"read","arguments":{"path":"NOTES.md"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":1,"output":1,"cacheRead":0,"cacheWrite":0,"totalTokens":2,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":2}}
{"type":"message","id":"c0000003","parentId":"c0000002","timestamp":"$now","message":{"role":"toolResult","toolCallId":"call_read","toolName":"read","content":[{"type":"text","text":"ROUTINE TOOL RESULT ROW"}],"details":{},"isError":false,"timestamp":3}}
{"type":"custom","id":"c0000004","parentId":"c0000003","timestamp":"$now","customType":"firstmate-synthetic-input-presentation","data":{"content":"$watcher","kind":"watcher"}}
{"type":"message","id":"c0000005","parentId":"c0000004","timestamp":"$now","message":{"role":"user","content":[{"type":"text","text":"$watcher"}],"timestamp":5}}
{"type":"message","id":"c0000006","parentId":"c0000005","timestamp":"$now","message":{"role":"bashExecution","command":"printf CAPTAIN_BASH_OUTPUT","output":"CAPTAIN_BASH_OUTPUT\n","exitCode":0,"cancelled":false,"truncated":false,"timestamp":6}}
{"type":"message","id":"c0000007","parentId":"c0000006","timestamp":"$now","message":{"role":"assistant","content":[{"type":"toolCall","id":"call_edit","name":"edit","arguments":{"path":"NOTES.md","oldText":"a","newText":"b"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":1,"output":1,"cacheRead":0,"cacheWrite":0,"totalTokens":2,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"error","errorMessage":"ACTIONABLE TOOL ERROR: provider request failed","timestamp":7}}
{"type":"message","id":"c0000008","parentId":"c0000007","timestamp":"$now","message":{"role":"toolResult","toolCallId":"call_edit","toolName":"edit","content":[{"type":"text","text":"ACTIONABLE TOOL ERROR: provider request failed"}],"details":{},"isError":true,"timestamp":8}}
{"type":"message","id":"c0000009","parentId":"c0000008","timestamp":"$now","message":{"role":"assistant","content":[{"type":"text","text":"FIRSTMATE REPLY: two fixes and one doc change."}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":1,"output":1,"cacheRead":0,"cacheWrite":0,"totalTokens":2,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":9}}
JSON

launch() {
  tmux -L "$SOCK" new-session -d -s "$SESSION" -c "$project" -x 110 -y 46 \
    "env FM_HOME='$home' PI_CODING_AGENT_DIR='$config' PI_OFFLINE=1 pi --approve --no-context-files --no-skills --no-prompt-templates --no-extensions -e ./.pi/extensions/fm-calm.ts --session '$session'"
}

wait_for() { # file text
  local i=0
  while [ "$i" -lt 200 ]; do
    tmux -L "$SOCK" capture-pane -p -t "$SESSION" >"$1" 2>/dev/null
    grep -Fq "$2" "$1" && return 0
    sleep 0.05
    i=$((i + 1))
  done
  return 1
}

snap() { # basename
  tmux -L "$SOCK" capture-pane -p -t "$SESSION" >"$EV/$1.txt"
  tmux -L "$SOCK" capture-pane -p -e -t "$SESSION" >"$EV/$1.ansi"
}

# 1) Calm persisted ON for this home.
printf '%s\n' on >"$home/config/calm"
launch
wait_for "$WORK/probe.txt" "FIRSTMATE REPLY: two fixes" || { echo "FAIL: calm-on transcript never rendered"; exit 1; }
sleep 1
snap calm-on
echo "--- home preference after start: $(cat "$home/config/calm")"

# 2) /calm toggles it off live (same session, same loaded rows).
tmux -L "$SOCK" send-keys -t "$SESSION" -l '/calm'
tmux -L "$SOCK" send-keys -t "$SESSION" Enter
wait_for "$WORK/probe.txt" "FIRSTMATE WATCHER WAKE" || { echo "FAIL: /calm off did not restore hidden rows"; exit 1; }
sleep 1
snap calm-off
echo "--- home preference after /calm: $(cat "$home/config/calm")"

# 3) /calm back on redraws already-loaded rows.
tmux -L "$SOCK" send-keys -t "$SESSION" -l '/calm'
tmux -L "$SOCK" send-keys -t "$SESSION" Enter
i=0
while [ "$i" -lt 200 ]; do
  tmux -L "$SOCK" capture-pane -p -t "$SESSION" >"$WORK/probe.txt"
  grep -Fq "FIRSTMATE WATCHER WAKE" "$WORK/probe.txt" || break
  sleep 0.05
  i=$((i + 1))
done
sleep 1
snap calm-reenabled
echo "--- home preference after second /calm: $(cat "$home/config/calm")"

tmux -L "$SOCK" send-keys -t "$SESSION" -l '/quit'
tmux -L "$SOCK" send-keys -t "$SESSION" Enter
sleep 2
tmux -L "$SOCK" kill-server 2>/dev/null || true

# 4) Restart with the persisted preference still on.
printf '%s\n' on >"$home/config/calm"
launch
wait_for "$WORK/probe.txt" "FIRSTMATE REPLY: two fixes" || { echo "FAIL: restart transcript never rendered"; exit 1; }
sleep 1
snap calm-restart
tmux -L "$SOCK" send-keys -t "$SESSION" -l '/quit'
tmux -L "$SOCK" send-keys -t "$SESSION" Enter
sleep 1
echo "--- done"

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 4 issues found → auto-fixed ✅
  • 🚨 .pi/extensions/fm-calm.ts:163 - Calm now hides Pi's working row (ctx.ui.setWorkingVisible(!active)), and Working... is the ONLY busy signature Firstmate has for a pi pane (FM_TMUX_PI_BUSY_REGEX_DEFAULT=&#39;Working\.\.\.&#39;, bin/fm-tmux-lib.sh:83). Under tmux there is no native busy state - fm_backend_busy_state returns unknown (bin/fm-backend.sh:792) - so pane_is_busy falls back to the pane tail. Failing sequence: away mode on, captain's Pi (the supervisor target, i.e. firstmate's own pane) is mid-turn under Calm -> the busy guard at bin/fm-supervise-daemon.sh:1118 finds no busy line, and the composer reads empty because Pi's composer is empty while the agent works -> the away-supervisor injects an escalation into the captain's Pi mid-turn. The same loss affects fm_tmux_submit_enter_core's busy-queued-Enter fallback (bin/fm-tmux-lib.sh:411). The prior code deliberately kept this row visible (the removed live-e2e assertion failed if Calm hid it), so this is a behavior regression the presentation change did not compensate for. Either keep the row visible in the primary, or give the supervision path a Calm-aware busy signal before hiding it.
  • 🚨 .pi/extensions/fm-calm.ts:107 - The stated product decision was to scope Calm to the primary session only, never worker sessions, so the Working row keeps distinguishing a busy worker from an idle one. The only gate implemented is ctx.mode === &#34;tui&#34; &amp;&amp; ctx.isProjectTrusted(), which says nothing about primary vs worker: a crewmate/worker pi pane that is a trusted TUI and resolves the same effective home (tmux panes inherit an exported FM_HOME; fm-spawn only overrides FM_HOME for --secondmate, bin/fm-spawn.sh:1576) reads the same config/calm=on and hides its own Working... row. Downstream, crew_pane_is_busy (bin/fm-crew-state.sh:176, :609) and the daemon's stale_window_is_busy then classify a busy pi worker as idle -> exactly the false stuck-worker alarm the decision was meant to prevent. There is also no test asserting a worker session keeps its busy signature while the home preference is on (tests only cover rpc mode and untrusted). Needs either a real primary-session check or an explicit statement that worker homes can never resolve this preference.
  • ℹ️ .pi/extensions/lib/fm-calm-nonconversation-layout.ts:226 - installCalmLeadingSpacerLayout captures rowHides from the class registry at wrap time and bakes it into the wrapper, unlike every other adapter here, which reads patch.hides() per render. Pi re-evaluates this module on extension reload while the installed prototype/host wrappers survive, so the reload path replaces the registry entry's hides with the new module instance's closure but already-wrapped spacers keep calling the old instance's closure - whose calm flag is never updated again. Sequence: Calm on, transcript contains a compaction/branch/skill row, extension reload, /calm off -> the hidden row reappears while its leading spacer stays suppressed (or vice-versa) until the transcript is rebuilt. Resolve the hides function at render time (e.g. look it up by exportName) so it follows the live registry like the other adapters.
  • ℹ️ .pi/extensions/lib/fm-calm-tool-layout.ts:206 - The tool-error turn boundary treats every updateContent call as a turn signal, but Pi also calls it for presentation-only refreshes - invalidate(), setHiddenThinkingLabel(), setOutputPad() (assistant-message.js:30-51), and ui.invalidate() walks every transcript child on a theme change or terminal cell-dimension report. Each historical component then differs from scope.turn.component, so the scope is reassigned and the owners map is cleared. If that sweep lands between two sibling updateResult calls of one interrupted turn (e.g. the captain toggles /calm mid-turn, which itself calls setHiddenThinkingLabel), the same abort text is re-owned and printed on more than one tool row instead of once. Narrow and cosmetic, but it is a real hole in the documented "one message per interrupted turn" invariant.

🔧 Fix: resolve Calm spacer hide policy lazily across reloads
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-calm-pi-extension.test.sh (12 focused cases including live tmux Pi 0.82.1 E2E for visibility rules, geometry, non-conversation rows, operational follow-ups, export/Ctrl+O, restart)
  • npm exec --yes --package=typescript -- bash -c &#39;tests/fm-pi-primary-types.test.sh&#39; (strict no-emit typecheck of the Calm adapters against installed Pi declarations)
  • Manual TUI capture: launched real pi --session &lt;fixture&gt; in tmux with the home Calm preference on, captured the pane, sent /calm off, /calm on again, quit and relaunched; recorded pane snapshots and the config/calm file value at each step
  • Marker check across snapshots: captain prompt / assistant reply / !bash command+output / actionable tool error visible in all Calm states; read NOTES.md tool row, Thinking..., and the encoded FIRSTMATE watcher row hidden with Calm on and restored with Calm off
⚠️ **Document** - 1 info
  • ℹ️ docs/calm.md:24 - docs/calm.md and docs/calm-mode-feasibility.md correctly describe Calm as activating in any trusted interactive Pi TUI, which matches the code at this commit. The author's stated product decision to scope Calm to primary sessions only (never worker sessions) is not implemented here, so no doc claims it. When that scoping lands, calm.md (operator-facing scope) and the feasibility doc's 'Required presentation boundary' and taxonomy 'working-status' row are the two places that will need updating.
⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 127)
✅ **Push** - passed

✅ No issues found.

morris2spears pushed a commit that referenced this pull request Jul 30, 2026
* fix(bin): handle dash-leading harness process names (#2)

* fix: handle dash-leading harness process names

* no-mistakes(review): Make dash-leading harness regression hermetic

* fix: preserve secondmate reply routes across relative homes

Resolve relative home, data, and state inputs before durable charter generation, and fail when caller-relative directories cannot be resolved.

Use absolute paths at the related spawn, AFK daemon, and X-mode cross-process handoffs so later processes cannot reinterpret them from another working directory.

* no-mistakes(review): Preserve absolute overrides and normalize relative durable paths

* no-mistakes(review): Normalize relative home before deriving durable paths

* no-mistakes(document): Document relative durable-path normalization

* no-mistakes(review): Captain: Ignore inherited CDPATH during relative path normalization

* no-mistakes(lint): Fix empty CDPATH assignments for ShellCheck
@morris2spears
morris2spears force-pushed the fm/firstmate-pi-clean-chat branch from 13b71dd to 44cd085 Compare July 30, 2026 04:47
@morris2spears morris2spears changed the title feat(pi): make Calm mode conversation-only in the Pi TUI fix(pi-extensions): scope Calm to primary sessions and fix reload/dedup edge cases Jul 30, 2026
@morris2spears
morris2spears merged commit 41f33d0 into main Jul 30, 2026
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant