Repository navigation
feat(core): move blocking work to the background when steering - #60367
kitlangton wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
The server-side move looks right. The new core integration tests fail on the base and pass with this change; core and app tests and tsc pass. I couldn't get a useful end-to-end steer check in the TUI with the simulated model, so I'm marking this not verified.
Public API: no schema, route or SDK changes. Two behaviour changes for SDK/HTTP callers:
POST /session/:id/backgroundno longer appends the"User requested that active blocking work be moved to the background…"synthetic message.- A
delivery: "steer"prompt, or an inbox update tosteer, now backgrounds the session's blocking shells and subagents on the server.
Both are backwards compatible for callers. Clients that called session.background after a steer can drop that call; calling it again does nothing harmful.
Concerns:
- Job depends on session code (inline): the generic job registry now imports
DatabaseandSessionInboxand queries the database on everyblock. - Lost guidance for the model (inline): without the synthetic message, a backgrounded shell's result no longer tells the model not to wait or poll.
- Test size:
session-steer-background.test.tsis 610 lines of live tests with real shell processes, file-polling loops and 10s timeouts. Please cut it down to the core cases: steer moves a running shell, a shell that starts after a steer is admitted is moved too, and a non-backgroundable tool is unchanged. Thejob.test.tscases already cover the lock race more cheaply.
| ) | ||
| return yield* Effect.gen(function* () { | ||
| // A steer that landed while tool-call JSON was streaming or the job was starting yields on arrival here. | ||
| if (Option.isSome(database) && (yield* SessionInbox.hasUserSteer(database.value.db, input.sessionID))) |
There was a problem hiding this comment.
This makes the generic job registry depend on Database and SessionInbox, and run an inbox query on every block call. It also uses Effect.serviceOption, so the check silently does nothing in any layer without a database. Admission and promotion in session/session.ts already call backgroundAll, so this entry check only covers the race where a job starts blocking after the steer was admitted. Could the session or tool side own that check instead, e.g. a predicate passed to block or a check in the shell/subagent plugins before they block? That would keep Job free of session and storage dependencies.
| ].join("\n"), | ||
| }) | ||
| .pipe(Effect.catchTag("Session.SyntheticConflictError", Effect.die)) | ||
| yield* jobs.backgroundAll({ sessionID }) |
There was a problem hiding this comment.
Removing the synthetic message also removes the guidance the model got after a manual (ctrl+b) or steer-driven move: "still unfinished… Do not wait, sleep, poll, or report the backgrounded work as complete until a later completion notification". The subagent's background result carries equivalent text. The shell's backgroundResult only says Command moved to the background … Output is streaming to: <file>, which invites polling the file. Consider adding the "you will be notified, do not poll" line to the shell's backgrounded result so the guidance isn't lost.
393101f to
fcd73cb
Compare
There was a problem hiding this comment.
The new commit is a rebase. The steer logic is unchanged and the core steer and job tests plus tsc still pass. The Database fallback in Job is gone because Job now uses the base's database service, but job.ts still imports SessionInbox and queries the inbox on every block. The shell's backgrounded result still has no "you will be notified, do not poll" guidance. Both earlier threads stay open.
The public API is unchanged since my last review: POST /session/:id/background no longer appends a synthetic message, and steer prompts now move blocking work to the background on the server. Both are backwards compatible.
The earlier note on session-steer-background.test.ts still applies: 610 lines of live tests with real shell processes and file polling.
fcd73cb to
794b716
Compare
There was a problem hiding this comment.
The new commit rebases cleanly and the steer logic is unchanged. Core and app tests and tsc pass. The lost "do not poll" guidance for backgrounded shells is now covered: the base appends the shell's background instruction to running results, and the steer test now checks that text reaches the next turn. The Job → SessionInbox dependency (earlier thread) is unchanged. The public API is unchanged since my last review.
Sending a steer message while an agent is waiting on a shell command or subagent now moves that work to the background on the server across every client.
The agent reads the new message on the next step while the command or subagent keeps running and posts its output when finished. Tools that cannot run in the background and prompts queued for the next turn still wait as before.
💡 Why: Previously only the web and desktop app moved blocking work on steer, using a second client request after prompt admission that missed TUI and SDK callers, missed tools that started while tool-call JSON was still streaming, and appended a duplicate synthetic notice beside the tool result.
🆕 Now you can: Steer a busy session from TUI, web, desktop, iOS, or the SDK (or promote a queued prompt to a steer) and have its blocking shells and subagents move to the background right away, with one completion notice when the background work finishes.
📊 Proof
blockentry"User requested..."synthetic messagedelivery: "queue"promptNew unit and integration tests in
packages/core/test/session-steer-background.test.tsandpackages/core/test/job.test.tsfail without the change; 94 core tests and 34 app composer tests pass.👀 Read:
packages/core/src/job.tsinsideblock: foreground versus background ownership is decided under thestate.jobslock afterraceFirstwakes so a job that settles right afterisBackgroundedcommits still takes the background observer path and clears its recovery marker. The rest is routine.Visual
🔍 Background-jobs split; independently reviewed and both findings fixed · no user reports · not measured
Checks run
bun run script/test.ts test/job.test.ts test/session-steer-background.test.ts test/session-owned.test.ts test/session-prompt.test.ts(packages/core): 94 pass, 0 failbun run script/test.ts test/session-steer-background.test.ts --rerun-each 10 --randomize(packages/core): 60 pass, 0 failbun run script/test.ts test/job.test.ts --rerun-each 10 --randomize(packages/core): 170 pass, 0 failbun run typecheck && bun run typecheck:tsc(packages/core): passbun test --conditions=solid --preload ../ui/test/solid-jsx.ts --preload ./happydom.ts src/composer/submit.test.ts src/session/composer/queue.test.ts(packages/app): 34 pass, 0 failbun run typecheck && bun run typecheck:tsc(packages/app): passbun run lint:changed(root): 0 problems