Repository navigation
feat(core): move long-running tools to the background after a wait - #60375
kitlangton wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
The auto-yield works as described. With the simulated model, a 45 s shell command blocked the turn for the full 45 s on the base. With this PR the call returned at 30 s with jobID and Output so far, and the completion notice started the session again by itself at 45 s. Core job, shell, execute, subagent and steer tests pass, as do the server session-jobs and client solid-data tests, and core typecheck:tsc is clean.
Public API (additive, but the default behaviour changes):
shell,executeandsubagenttake a new optionalyield_afterinput (defaults to 30000;0backgrounds right away). Their outputs gainjobID, andexecuteoutput gainsstatus: "running".- New Code Mode tools:
tools.opencode.job_list,job_output,job_waitandjob_stop. - New endpoint
GET /api/experimental/session/:sessionID/jobs, exposed asexperimental.session.jobsin the client and SDK. - New
job.updatedevent. - New
Session.jobsin the plugin API. /api/session/activenow also lists sessions whose only activity is a running job.Session.backgroundno longer adds the "moved to the background" message (from #60367).
Existing callers keep working. What changes is that tool calls longer than 30 s now return running instead of blocking, and the implicit 2-minute shell timeout no longer applies once a command yields.
Concerns:
- A subagent that starts a server never reports back to its parent (inline comment).
- Because sessions now count as busy while any job runs, and a yielded command keeps no implicit timeout, a session with a dev server or watch-mode command left running will look busy indefinitely (
esc interrupt,1 shellin the footer). Please confirm that's intended, and that Esc orjob_stopis an obvious way to clear it.
Fixes #50278
Fixes #48826
Fixes #56729
Related: #54221, #51810, #36423
| yield* sessions.resume(recovery.childSessionID) | ||
| while (true) { | ||
| yield* sessions.resume(recovery.childSessionID) | ||
| yield* jobs.awaitSessionBackground(recovery.childSessionID) |
There was a problem hiding this comment.
The subagent now only reports back once every background job it owns has settled. With the 30 s auto-yield, any server the subagent starts, for example uvicorn or bun dev in the foreground or with background: true, becomes one of those jobs. The subagent then never completes, and its parent stays busy waiting for a notice that never comes. Before, the foreground server was killed at 2 minutes, or the subagent finished with the job still running. Could long-lived jobs be excluded from this wait, given a time limit, or handed over to the parent when the subagent's turn ends?


Long-running shell commands, Code Mode scripts, and subagents now move to the background on their own after 30 seconds instead of blocking the turn or getting killed at 2 minutes.
Calls that finish within 30 seconds still return inline in a single step as before, and sessions stay busy while their background jobs run. Stacked on #60367 (
t901-steerbg).💡 Why: Previously a slow build, server, Code Mode script, or subagent blocked the whole turn unless the model guessed
background: trueup front,shellkilled foreground commands at 2 minutes, plugin reloads dropped completion fibers, and sessions flipped to idle while background jobs were still running.🆕 Now you can: Pass
yield_afterin milliseconds onshell,execute, andsubagent(0backgrounds right away; defaults to30000), inspect or control jobs withtools.opencode.job_list,job_output,job_wait, andjob_stop, and query running family jobs atGET /api/experimental/session/:sessionID/jobs.🟰 Behavior change (30 s default yield): Yes —
shell,execute, andsubagentcalls that run past 30 s now yieldstatus: "running"with ajobIDand the output captured so far instead of holding the step open. This is safe because fast calls (< 30 s) are unchanged, nothing is killed on yield (shellclears only its implicit 2-minute foreground timeout while keeping any explicittimeoutarmed), completion notices automatically resume the session fromJob's global scope, subagents wait for their own background jobs before reporting back, and agents can pass a largeryield_afteror calljob_waitwhen they want to wait longer.📊 Proof
yield_aftershellSIGTERMs at 2m)jobID+ live output so far; Implicit 2mshelltimeout cleared, explicittimeoutpreservedyield_afterat the boundaryJob's global scope (onSettle)job_list,job_output,job_wait,job_stop+ 25-entry completed history; stopping a subagent or deleting a session cancels descendant jobs/api/session/active,job.updated, and/api/experimental/session/:sessionID/jobsNew unit and integration tests in
packages/core/test/tool-auto-background.test.ts,packages/core/test/job.test.ts,packages/server/test/session-jobs.test.ts, andpackages/client/test/solid-data.test.tsfail without the change; 673 core, server, client, schema, protocol, and codemode tests pass.👀 Read:
packages/core/src/job.tsinblock,completeBackground, andcancelSession:blockracesdone,backgrounded, andwaitYieldthen resolves ownership under thestate.jobslock (matching #60367),completeBackgroundclears the durable notice marker while keeping finished jobs in the 25-entry history, andcancelSession+SubagentJobtear down descendant jobs before session removal or subagent stop. The rest is routine.Visual
🔍 Stacked on #60367 · independently reviewed and all 5 findings fixed with regression tests · no user reports · not measured
Checks run
bun run script/test.ts test/job.test.ts test/tool-auto-background.test.ts --rerun-each 10 --randomize(packages/core): 260 pass, 0 failbun run script/test.ts test/session-steer-background.test.ts test/session-owned.test.ts test/tool-subagent.test.ts test/tool-execute.test.ts test/tool-image-generate.test.ts test/location-layer.test.ts test/session-execution.test.ts test/session-runner.test.ts test/schedule.test.ts(packages/core): 398 pass, 0 failbun test test/session-jobs.test.ts(packages/server): 1 pass, 0 failbun test test/solid-data.test.ts test/effect.test.ts(packages/client): 45 pass, 0 failbun test test/contract-hygiene.test.ts test/event-manifest.test.ts(packages/schema): 22 pass, 0 failbun test test/experimental-identifiers.test.ts && bun run check:generated(packages/protocol): passbun test test/codemode.test.ts(packages/codemode): 54 pass, 0 failbun run typecheck && bun run typecheck:tsc(schema,codemode,core,plugin,protocol,server,client,sdk) &bun run typecheck(tui,app): 0 errorsAdded by the opencode bot (AI-generated).
Fixes #50278
Fixes #48826
Fixes #56729
Related: #54221, #51810, #36423
Public API: adds yield_after to shell/execute/subagent, job_list/job_output/job_wait/job_stop tools, GET /api/experimental/session/:sessionID/jobs, job.updated event and Session.jobs; session active now includes job-only sessions (additive; default 30s auto-yield changes tool behaviour)