fix(coding-agent): run extension slash commands immediately during active turns - #762
Merged
Merged
Conversation
The onSubmit and handleFollowUp comments claimed extension commands always execute immediately, which was false while AgentSession.prompt() serialized them behind the session-work barrier and during compaction. Describe the real flow instead: onSubmit short-circuits commands at its isExtensionCommand branch, and handleFollowUp (bound directly to app.message.followUp, never routed through onSubmit) dispatches them at its own compaction check. Comments only; no executable line changed.
…ive runs AgentSession.prompt() dispatched extension commands below the settled-session-work gate, so a bare prompt() first awaited _waitForSettledSessionWork(). A scheduled continuation (goal chain, queued follow-up) holds the SessionWorkBarrier for an entire run, so a slash command typed mid-turn (e.g. /todo) only executed once the turn ended; the same applied during compaction. Hoist the dispatch above the prompt-start bookkeeping and that gate, keeping a synchronous registry lookup so ordinary '/'-leading text gains no new await, and give it a catch that mirrors the existing preflightResult(false)+rethrow contract on post-handler cancellation. Covered by test/suite/regressions/extension-command-immediate-dispatch.test.ts (barrier-held run, blocked compaction, idle-with-held-barrier).
Immediate command dispatch newly exposes handlers to running turns and to active compaction. An audit of all 23 registered command handlers found /ir alone unsafe: it switches sessions, which aborts and disposes the live session, and during compaction the fire-and-forget abortCompaction() races dispose(). Guard the handler with ctx.isIdle()/ctx.isCompacting?.() - both clauses are needed because isIdle() is true during compaction without streaming - and warn instead of switching. Every other handler is safe or already reachable mid-turn via the Alt+Enter path.
The guard cited a plan path outside the package, which rots once the plan is gone. Point at extensions/builtin/changes.md instead and state why isCompacting is checked separately from isIdle.
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.
Problem
Typing an extension slash command (e.g.
/todo) while the agent is mid-turn showed its output only after the turn ended.AgentSession.prompt()dispatched extension commands below the settled-session-work gate:interactive-mode.ts:3566routes Enter-submitted commands tosession.prompt(text)with nostreamingBehavior.agent-session.ts:2580-2588then awaits_waitForSettledSessionWork()before reaching the command dispatch._scheduleContinuationAfterCurrentEvent()(agent-session.ts:4904) holds theSessionWorkBarrierfor an entire continued run, so in goal-armed sessions every turn deferred commands to turn end. The same happened during compaction.Two asymmetries showed this was unintended: the Alt+Enter path passes
streamingBehavior, hits thecanQueueWhileStreamingexemption, and already ran commands immediately; and builtin TUI commands (/model,/fork, …) were always immediate. The in-code comments claimed "extension commands execute immediately" — false for exactly these cases.Change
getCommand()lookup runs first, so ordinary/-leading text gains no new await; a dedicated catch mirrors the existingpreflightResult(false)+ rethrow contract. Fixes every caller at once — TUI Enter path, the compaction path, RPC, print mode./ir. Immediate dispatch newly exposes handlers to running turns and active compaction. An audit of all 23 registered command handlers found/iralone unsafe (it aborts + disposes the live session; during compaction the fire-and-forgetabortCompaction()racesdispose()). Bothctx.isIdle()andctx.isCompacting?.()are needed —isIdle()is true during compaction without streaming. Every other handler is safe or already reachable mid-turn via Alt+Enter.onSubmit/handleFollowUp. NotehandleFollowUpis bound directly toapp.message.followUpand never traversesonSubmit, so its command check is genuinely reachable.Verification
Regression tests (
test/suite/regressions/extension-command-immediate-dispatch.test.ts, TDD — RED captured before the fix, plus a mutation proof that reverting the hoist re-reds all three):session_before_compactcompaction · Case C idle with a held barrierpreflightResult(true)/promptDisposition("handled")fire.Pinned contracts stay green:
2023-queued-slash-command-followup,goal-continuation-streaming-prompt-trap,agent-session-prompt,agent-session-queue(queueing semantics untouched).Scoped run: 9 files, 67/67 passing.
npm run check: exit 0.Full suite: 11 files fail on this branch — a strict subset of the 12 that fail on the base commit
ec17cfc8a(rpc, stdout-cleanliness, session-*, mcp oauth-race, fswatch flakes). Zero new failures;footer-data-providerfails on base but passes here.Real-CLI QA (senpi-qa tool-serve mock + tmux driver, hermetic sandbox, zero tokens) — captured ordering from the live TUI:
/todo appendnotify/todomarkdown- [ ] Qa itemThe mid-turn screenshot shows the rendered todo widget while the status line still reads
● Working (16s • esc to interrupt). Pre-fix both outputs appeared only after index 77335. Teardown receipt recorded: tmux session killed, serve process dead, sandbox removed, realauth.jsonsha256 unchanged.Plan:
.omo/plans/immediate-extension-commands.mdSummary by cubic
Run extension slash commands immediately during active turns and compaction by hoisting dispatch in
AgentSession.prompt(). Also block/irwhile the agent is busy to prevent unsafe session switches, and update comments/docs to match.promptDisposition("handled")andpreflightResult(true)with no user message added./ir: warn and return when streaming or compacting to avoid replacing the live session mid-run./irhandler (now pointing toextensions/builtin/changes.md), and added aCHANGELOG.mdentry.Written for commit 846e074. Summary will update on new commits.