Repository navigation
steer gate: read the session busy flag, not the agent's - #58
Merged
Merged
Conversation
pi 1.2.0 made a session count as busy from the first line of its run-start sequence, which awaits the compaction check and the before_agent_start handlers. agent.state.isStreaming only becomes true inside the later agent.prompt() call, so gating steering on it left a window where an arriving message was submitted as a plain prompt into a run that was already starting. pi rejects that with "Agent is already processing", which surfaced as a channel error and dropped the message. Before 1.2.0 the two flags flipped one statement apart. Read session.isStreaming instead (pi's _isAgentRunActive) at all three sites, and read it once per message so the steer option and the branch agree.
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.
Fixes a steering bug that the pi 1.3.0 bump widened. Base merged, so this is now a clean single-concern PR on
main— the diff is the fix commit plus its test, nothing else.Main has moved since this branch was cut (#59, "presence: show the model name only"), which touches
createAgentPool.tsin the same file. Merged rather than rebased, per the repo's convention; the merge was clean and #59's change is intact.The bug
pi has two busy flags and they flip at different points:
session.isStreaming— pi's_isAgentRunActivesession.agent.state.isStreaming— the agent loop's flag, which guardsactiveRuncreateAgentPool.handle()gated steering on the agent flag. But pi 1.2.0 moved the session flag to the first line of its run-start sequence, which then awaits the compaction check and thebefore_agent_starthandlers; the agent flag only becomes true inside the lateragent.prompt()call.So during that window a message arriving from Discord found the agent flag
false, the harness concluded nothing was in flight, and it submitted a plainprompt(text)with nostreamingBehavior. pi checks its flag, sees busy, and throws:handle()catches that and posts it viasender.sendError— so the user gets an error line and their message is never processed. On 0.84.4 the two flags flipped one statement apart, so this was effectively unreachable; on 1.3.0 the window spans the whole run-preparation, which is exactly when someone fires off a quick correction.evict()read the same flag for its busy check. Benign there, but wrong for the same reason — a just-started session could be evicted.Reproducing it
The window is normally sub-second, so I widened it deterministically by registering a ~2s
before_agent_starthandler in a throwaway agent home, then probing both flags while a run was starting:Both flags read and both prompt paths exercised in the same window, so this is the bug and the fix landing on the same measurement, not two separate runs.
The fix
Read
session.isStreamingat all three sites, via a smallisSessionBusy()helper that carries the explanation — this is a trap someone will otherwise walk back into.handle()also now reads the flag once and uses that value for both the option it builds and the branch it takes; those must agree or the prompt carries the wrong disposition.Why this can't regress anything: the session flag is cleared in
_emitAgentSettled(), which runs afteragent.prompt()and the post-run loop finish — so it is strictly wider than the agent flag, never narrower. Every case that previously steered still steers, and the only newly-steered case is the window that previously errored.What I deliberately didn't do
new Error()with different messages, so it would be a string match against prose. That is brittle and would mask genuine "already processing" bugs. Reading the authoritative flag makes the error unreachable on this path instead.wakeUp()keeps a plainprompt(). Its three callers (thread.tson a brand-new thread,recoverActiveon a fresh process,handleClearafterdropEntry) all target a session that is fresh or just dropped, so it can't be busy. If a caller is ever added that can hit a warm busy session, it needs the same gate — noted in the code.Tests
Added
src/agent/buildPromptOptions.test.ts(4 cases) pinning theisBusy→streamingBehavior: "steer"pairing, which is the contract this bug violated.buildPromptOptionswas the one piece of this change with a testable seam — the pool itself needs a live discord.js client and a full pi session to construct.bun run typecheckclean,bun test106/106.