fix(pi): join overlapping primary prompts instead of dropping them - #276
Merged
Merged
Conversation
added 8 commits
September 14, 2026 06:54
Pi 0.85.1 decides between queueing a prompt and starting a new turn before its preflight, so a watcher wake, turn-end nudge, or branch processing request that overlaps a captain prompt inside one preflight window was rejected with "Agent is already processing a prompt" and dropped, together with a spurious settle of the still-running turn. One owner, .pi/extensions/lib/fm-pi-prompt-delivery.ts, installed by the watch extension, now joins such a prompt to the running turn through Pi's own steer and follow-up queues, and starts a join that missed the turn's last queue check once that turn settles. A wake is consumed only when a model turn accepts it (its user message_start), never at before_agent_start. Loaded-generation markers are now written only from a started session run by the lock holder itself, so a pi --list-models probe from the primary's shell can no longer make stale extension code read as current. The session-start diagnostic, Pi harness reference, and updater guidance now state that /reload or a full restart activates changed extension code, while /new, /resume, and /fork keep the cached factory.
…mpt overlap guard in a named Herdr lab
…p arming and settle-time Calm repaint
…branch. (1) Behavior portable serial 2: tests/fm-live-gate.test.sh runs every live guard with FM_LIVE=0 and expects the shared refusal line. tests/fm-pi-prompt-collision-live-e2e.test.sh used its own env check, so it printed a different message. It now opens with `fm_live_gate opt-in FM_PI_PROMPT_COLLISION_LIVE_E2E pi` like tests/fm-pi-primary-live-e2e.test.sh, still needs its own variable to run, and keeps its tmux or herdr+jq checks when it does run. The sweep is unchanged: the hung-delivery guard is already listed there as keeping its own opt-in, and the primary guard already uses the shared gate. (2) Stock macOS Bash 3.2: tests/fm-pi-loaded-marker.test.sh had its Node script as a heredoc with apostrophes inside $(...), which Bash 3.2 can't parse. The script is now written to a file with a heredoc outside the command substitution, and $(...) only runs node on that file. The script and assertions are unchanged. Verified: fm-live-gate.test.sh passes (exit 0); fm-pi-loaded-marker.test.sh passes, including the real pi --list-models probe; the collision guard prints the shared refusal under FM_LIVE=0 and the opt-in message by default; ShellCheck via bin/fm-lint.sh is clean on both files. Bash 3.2 was not available locally. The macOS check reported only the marker test as failing to parse, and that file no longer has a heredoc inside $(...). Changes are not committed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
The captain reported that the active Firstmate Pi session repeatedly displays
Extension "<runtime>" error: Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion.After receiving the completed live diagnosis and the proposed bounded repair, the captain said: "I authorize that repair you have suggested."Ship that authorized repair. Prevent overlapping Firstmate extension messages and captain messages from being dropped by serializing or safely retrying Firstmate's primary Pi message delivery. Prevent child model-list probes from falsely recording that the primary loaded a new extension generation. Make the supported activation rule accurate: after a Pi extension update,
/newis not activation;/reloador a full Pi restart is required. Preserve the merged supervision continuity fix and its healthy successor behavior.What Changed
.pi/extensions/lib/fm-pi-prompt-delivery.ts, which the watch extension installs. It wraps Pi'sAgentSession._runAgentPromptonce per process, and later reloads reuse the same wrap. When a second prompt arrives while a turn is running, it joins that turn instead of failing withAgent is already processing a prompt: captain and other non-operational prompts are queued as steers, and Firstmate operational prompts and custom messages as follow-ups. Messages still left in the queue when the turn settles are started as a new turn, and a failure there is reported through the extension runner. A Pi build without this hook shows aprompt delivery unprotectedwarning at session start. Wake consumption now counts only the usermessage_start, no longerbefore_agent_start..pi/extensions/lib/fm-pi-loaded-marker.ts, shared by the watch and turn-end guard extensions. It only lets the process that holds the lock write the loaded-generation markers, or any process when no live process holds the lock. Both extensions now write markers only fromsession_start(or, in the watch extension, when it arms), never while the extension is being loaded. So a childpi --list-modelsprobe can no longer record a newer build under its own pid./new,/resume, and/forkkeep the old code, and only/reloador a full Pi restart loads the new code. This is updated in thefm-session-start.shnot-loaded message, the Pi harness reference, and theupdatefirstmateskill. Also added unit tests for prompt delivery and the marker rule, a live test that sends overlapping prompts, a reworked Herdr hung-delivery test, and verification notes. The new suites are registered infm-test-run.sh, and several existing suites got small test-setup changes, including a fixed umask for the quota suite.🤖 Generated with Claude Code
Risk Assessment
Testing
I drove real Pi 0.85.1 on this machine. On the base commit, the prompt-overlap TUI test reproduces the captain's exact banner and a dropped wake; on the fix, all four overlap orders (before and after /reload) deliver both messages in one turn with no banner and exactly one linked monitoring cycle. The rendered pane screenshots show both results. A base-vs-fix
pi --list-modelsprobe shows the base overwriting the holder's loaded markers while the fix leaves them untouched, and the loaded-marker and prompt-delivery test scripts pass against real Pi. A lifecycle probe shows /new reuses the same extension module while /reload loads a new one. The healthy-successor Herdr control passes. Temporary copies, the base extraction and the kept lab directory were removed, and the worktree is clean.Extension "<runtime>" error: Agent is already processing a promptand drops the wakebash tests/fm-pi-prompt-delivery.test.sh-> prompt-delivery.logpi --list-modelsunder the lock holder: loaded-generation markers stay exactly as the holder recorded them (base overwrites them with a new hash and the probe's pid)Evidence: Base collision run log (fails: wake never reaches a model turn)
Evidence: Fixed collision live test transcript
Evidence: Fixed pane captures per overlap
Evidence: pi --list-models probe marker contents, base vs fix
Evidence: Real Pi /new vs /reload extension module load IDs
factory gen=32f87v session_start reason=startup gen=32f87v factory gen=32f87v session_start reason=new gen=32f87v factory gen=pwb1vc session_start reason=reload gen=pwb1vcEvidence: Healthy-successor Herdr lab control
Evidence: Loaded-marker test with real pi probe
Evidence: Prompt delivery test against real Pi AgentSession
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
🔧 **Rebase** - 1 issue found → auto-fixed ✅
.agents/skills/harness-adapters/references/harness/pi.md- merge conflict rebasing onto origin/main🔧 Fix applied.
✅ Re-checked - no issues remain.
🔧 **Review** - 3 issues found → auto-fixed ✅
.pi/extensions/lib/fm-pi-prompt-delivery.ts:143- A joined prompt is queued withagent.steer/agent.followUpdirectly, which skips Pi's own session queue bookkeeping (AgentSession._queueSteer/_queueFollowUpadd the text to_steeringMessages/_followUpMessagesand call_emitQueueUpdate). This causes two captain-visible problems in the real TUI. (1) A captain message that loses the preflight race leaves the editor but never appears in the pending-messages display, which readssession.getSteeringMessages(), so until the running tool batch ends it looks like the message vanished. (2) If the captain presses Escape during that turn, interactive-moderestoreQueuedMessagesToEditor({abort:true})callssession.clearQueue(), which runsagent.clearAllQueues()(deleting the joined message) but returns only the session-tracked texts. The joined captain message is dropped silently and never put back in the editor, even though the stated goal is that captain messages are not dropped. Fix: when a joined message has roleuser, mirror Pi's queue bookkeeping (push its text into the matching session list and emit the queue update, as_queueSteer/_queueFollowUpdo). Pi's message_start handler already removes those entries by exact text..pi/extensions/lib/fm-pi-prompt-delivery.ts:155- The stranded-message restart is fire-and-forget (void wrapped.call(this, stranded)) with no rejection handler, and the Pi 0.85.1 CLI bundle installs nounhandledRejectionlistener. If that restarted_runAgentPromptthrows (for exampleAgent.promptsees anactiveRunfrom a continuation or retry started elsewhere, or an agent_settled handler throws out of_emitAgentSettled), Node's default unhandled-rejection mode ends the whole primary Pi process. Previously this was at worst a banner. Attach a catch that reports the failure through the same visible path rather than letting it escape as an unhandled rejection..pi/extensions/lib/fm-pi-prompt-delivery.ts:132- The install check is keyed on a process-global symbol, so after/reloadthe wrap installed by the first load stays in place and keeps closures over the old module's code, including the oldclassifyFirstmateCurrentOperationalText. That conflicts with the activation rule this change documents in pi.md:56 ("Changed extension code activates only through/reload... or a full process restart"). A later update to fm-pi-prompt-delivery.ts, or to the operational-input encoding it classifies with, does not take effect on/reload; only a restart applies it. Example: the encoding changes,/reloadloads the new encoder into the watch extension, and the stale wrap classifies new wakes as non-operational, so they are joined as steers instead of follow-ups. Fix: store the unwrapped original per prototype in the global registry and re-wrap it with the current module's function on each install, so reload replaces the wrap without ever double-wrapping.🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
Extension "<runtime>" error: Agent is already processing a promptand drops the wakebash tests/fm-pi-prompt-delivery.test.sh-> prompt-delivery.logpi --list-modelsunder the lock holder: loaded-generation markers stay exactly as the holder recorded them (base overwrites them with a new hash and the probe's pid)FM_PI_PROMPT_COLLISION_LIVE_E2E=1 bash tests/fm-pi-prompt-collision-live-e2e.test.sh(real Pi TUI in tmux, fixed branch)Same collision test script run against agit archive ea6b934aextraction of the base code, as a check that it fails before the fix, with pane captureCollision test copy that also saves the pane after each overlap on the fixed branch (temp copy, deleted afterwards), pane captures rendered to PNG with headless Chromebash tests/fm-pi-loaded-marker.test.sh(includes a realpi --list-modelsprobe under the lock holder)Manual base-vs-fixpi --list-modelsprobe with stale holder markers, before/after marker contents recordedbash tests/fm-pi-prompt-delivery.test.sh(real Pi AgentSession overlap with and without the delivery owner)Manual real Pi TUI lifecycle probe: startup, then /new, then /reload, recording the extension module load ID each timeFM_PI_HUNG_DELIVERY_HERDR_E2E=1 FM_PI_SETTLEMENT_CONTROLS=healthy bash tests/fm-pi-hung-delivery-herdr-e2e.test.sh(named Herdr lab)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.