Repository navigation
Let a person's message to chopin stop a background job instead of being refused - #886
Merged
Merged
Conversation
… of being refused While chopin kept the document's title, or ran another background job, a message to it (an @chopin message, or any message from someone alone in the document) was refused with "Stop it to ask now", because a job's isolated turn takes no steering. Every member message asks for a title job, so this followed almost every reply. A message to the Planner, or an invoke_planner message, now preempts a background job. It is saved and acknowledged at once, the job's turn is stopped, and the person's turn starts as soon as the job's has wound down, ahead of anything waiting; a job being handed the Chat waits behind it instead. A write the job's own tool has under way finishes first and stands as its result, and no new write starts, so a title or a decision write-up is never half written or written twice. Without a write, the job reports the new outcome preempted and goes back to pending with its attempts unspent; the end of the person's turn wakes the queue and it runs again. A newer request for the same thing that is already waiting replaces it. One job gives way at most three times in a row; the next run is let finish with the person's turn right behind it, so it still completes. Turns a person started keep taking steering, and are never preempted. A job's turn is quiet (Turn.quiet): Chat draws no working row and offers no Stop for it, and since a message during it is no longer refused, Stop never appears for it now. The person's turn that takes its place is an ordinary watched turn; the job's run after it is quiet again. Measured with the unit fake harness (a 3 s title job, 10 ms saves), the time from send to the Planner receiving the person's turn went from 3,023 ms (refused until the job ended) to 20.4 ms; in the browser against the real server with a remote machine it was 163 ms. Assistant-model: Claude Opus 5.5 Assistant-workflow: inline (subagent) Assistant-verification: bun run types Assistant-verification: bun run ci Assistant-verification: bun test apps/server/src/chat apps/server/src/conversation-plan apps/server/src/agent apps/server/src/mcp (2568 pass, 0 fail) Assistant-verification: playwright steering, composer-send-stop, conversation-initiative, harness, harness-children, harness-comments, harness-decisions-deck, harness-investigation (64 passed) Assistant-verification: new unit tests and browser spec fail on origin/main 4b1e6c6 Assistant-verification: after rebasing onto 74772ee: bun run types; bun run ci; bun test apps/server/src/chat apps/server/src/conversation-plan apps/server/src/agent (2446 pass); playwright steering, composer-send-stop, chat-after-answer, harness, harness-children, harness-comments, harness-decisions-deck, harness-investigation (57 passed) User-preference: a person's message to chopin must never be refused because a background job is running Co-authored-by: Alex Lavaee <lavaman131@github.com>
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.
A person's message to chopin goes ahead of a background job
What the maintainer saw
While chopin was keeping the document's title, or doing another background job, a message to chopin (an
@chopinmessage, or any message from someone alone in the document) was refused: "chopin can't take messages while it works on this machine. Stop it to ask now." The draft came back to the composer. Every member message asks for a title job, so this happened right after almost every reply chopin gave.Both shots come from the same browser spec, taken 1.5 s after the second message while the title job was running.
Cause
A background job's turn is isolated and takes no steering, so
processSendrefused any message that arrived while one ran (planner-busy).invoke_plannermessages were refused the same way. The job held the Chat until it finished.The fix
A background job now gives way to a person (
preemptibleandpreemptinapps/server/src/chat/service.ts).draft_heading,write_decision_prose,write_decision_scope,refine_decision), under the document lock. A write already under way when the person asks is allowed to finish (chat.jobWriting), so it lands whole and counts as the job's result (done, not run again). Once a job is giving way, no new write may start.preempted.Jobs.settleputs it back topendingwith its attempts unspent. The queue picks it up when the person's turn ends (that end already wakes owner-bound work throughownerAvailable). If a newer request for the same thing is already waiting (a newer title request, or a newer refine of the same card), the one that gave way is skipped with "A newer request replaced it." This keeps the stored queue valid, because it allows at most one pending title job.invoke_plannermessages preempt in the same way.as: "system"follow-ups still need a turn of their own and are unchanged.AGENTS.md,
docs/hosted-agent.md(Messages while the Planner works),docs/local-agent-mcp.md(planner-busy) and the composer's Stop paragraph inapps/web/DESIGN.mdsay so.With quiet turns (#after-answer)
A background job's turn is sent as
quiet. Chat draws no working row for it and offers Stop only after a message was refused because of it. That is the turn a person's message now preempts.quiet), with its working row and Stop.The "Stop after a refusal" path still applies to a reply's quiet after-answer check on a harness that takes no input mid-run. The unit tests check
quieton the job's turn, the person's turn and the job's re-run. The browser spec checks that there is no Stop and no "Getting oriented" row while the title job runs and after it runs again.chat-after-answer.e2e.tsstill passes: its last test scripts a refusal during a quiet job turn on the client side. That case still exercises the client rule, though this server no longer refuses a message because of a job.Measurement: time from send to the Planner receiving the person's turn while a title job runs
Unit fake harness. The title job takes 3,000 ms unless stopped and each save takes 10 ms; 5 rounds. On main the send is refused, so the script sends again every 50 ms, which is the best a person could do.
Browser, real server.
HARNESS=remote, PostgreSQL, and a scripted machine whose title job keeps working until it is stopped. From pressing Enter to the machine receiving the person's turn took 163 ms on this branch. On main the spec times out after 10 s with no turn started; in a real session the person would have to wait for the title job to finish and send again.Tests
New, and failing on origin/main (all 9 unit tests fail there; the browser spec times out):
apps/server/src/chat/preempt.test.ts(7 tests). The person's turn starts at once and the job reportspreempted. A job whose tool already wrote isdoneand not run again. A write already under way finishes whole and is the job's result, and no new write starts. Three preemptions, then the job finishes with the person's turn right behind it, and the count resets. A job being handed the Chat waits behind the person's turn. The Copilot SDK harness preempts as well. Aninvoke_plannermessage preempts and settles when its turn ends.apps/server/src/conversation-plan/planner-jobs-preemption.test.ts(2 tests). A preempted job goes back to pending with attempts unspent and completes when woken. A preempted title job that a newer one replaces is skipped, and the stored queue still restores.e2e/steering.e2e.ts: "a message sent while Chopin writes the title starts its turn at once: the title waits and is written after". It records the timing above and checks that the title job ran again after the person's turn.Updated, because they asserted the old refusal:
steering.test.ts("a background job's turn takes no message: the person's own turn starts in its place") andjob-queue.test.ts(the person's turn after preemption has no job scope).Checks run
bun run types: passedbun run ci: passedbun test apps/server/src/chat apps/server/src/conversation-plan apps/server/src/agent apps/server/src/mcp: 2,568 passed, 0 failed (1 skipped) across 332 files--workers=2): steering, composer-send-stop, conversation-initiative, harness, harness-children, harness-comments, harness-decisions-deck, harness-investigation: 64 passedbun run typesandbun run cipassed;bun test apps/server/src/chat apps/server/src/conversation-plan apps/server/src/agentpassed (2,446 tests, 0 failed); Playwright steering, composer-send-stop, chat-after-answer and the five harness specs passed (57 tests; send to the person's turn took 112 ms). The only conflict was indocs/hosted-agent.md, where both sentences were kept.bun run typesandbun run cipassed,bun test apps/server/src/chatpassed (1,015 passed, 0 failed), and Playwright steering plus composer-send-stop passed (19 passed; send to the person's turn took 139 ms)Judgement calls
preemptedis a new outcome rather than reusingdeferred.deferredmeans "wait for an owner" and is limited to decision follow-ups. Mixing the two would weaken that check.as: "system"follow-up are still refused while any turn runs, a job's included. They are not messages, and changing them would widen the scope. They could use the samepreemptpath later.