Repository navigation
Description
What happened
An agent thread is still working: the agent started a background command to wait until its validation gates finish, and it will act when that command exits. The composer says "Running 2 commands · Wait for 184 gates (long), Wait for 184 gates to complete". The sidebar shows neither Working nor Waiting, so the thread looks done. T3 knows something is running in the background but ignores it when deciding the thread's status.
Diagnosis
The thread's state, read through the orchestrator MCP: status: "completed", activeRunId: null, pendingRequestCount: 0. The last run ended right after the agent:
- started a background command
until grep -q DONE <evidence>/coord-status.txt; do sleep 60; done; cat …with the description "Wait for 184 gates", - wrote "I'm waiting for the gates, then I decide whether to merge",
- ended its turn without calling
t3_background_wait.
An earlier run did the same with another wait command.
How T3 decides today
- Each pending background task has a provider-neutral shape,
{ kind, command, description, awaited }(packages/contracts/src/orchestrationV2.ts:1025-1038). backgroundWorkHoldsCompletion(packages/shared/src/orchestrationV2PendingBackgroundWork.ts:90-108) holds the thread for acommandormonitoronly whenawaited === true.awaitedis set only when the agent callst3_background_wait, which goes throughThreadManagementService.awaitBackgroundTasksto the provider adapter's optionalawaitBackgroundTasks.
The comment on that field states the assumption behind all this: "Only the agent knows whether a command is a wait or a dev server it left running."
Why that's fragile, independent of provider
- The model must remember one specific call after starting the command. When it forgets (as here), the thread looks done even though the agent will wake up.
- Not every provider can mark a wait. In the source I read, only the Claude adapter implements
awaitBackgroundTasks; other providers getunsupported. Codex also wakes its agent when a background command exits (CodexAdapterV2.ts, background command continuation), but has no way to mark that wait. - The call itself can fail:
- the guidance names
taskIdbut the schema needstaskIds([Bug]: Claude agents call t3_background_wait withtaskIdand fail, because the instructions say "its task ID" but the schema needstaskIds#31889) - registration has a 15s timeout that busy sessions hit ([Bug]: t3_background_wait times out after 15 s on a busy Claude session and reports it as "Failed to update the awaited background work." #33646)
- the guidance names
Where the assumption is too strong: the agent is the only one who decides whether a command is a wait, but it usually says so in plain words, in the command's description ("Wait for 184 gates") and in the text around it ("I'm waiting for the gates" vs "the server is on localhost:3000"). T3 already has that text. It just doesn't read it.
Suggested fix
Keep the decision from #15433 / #24610 as it is: an awaited command holds the thread, and a dev server left running does not. Change only who can set awaited. When a background task starts and the agent hasn't labelled it, T3 asks its existing text-generation model whether this is a wait, and if so marks it awaited itself.
- It reuses an existing service.
apps/server/src/textGeneration/TextGeneration.tsalready runs short model jobs for T3 itself (generateThreadTitle,generateBranchName,generateCommitMessage,generatePrContent,generateReviewGuide,generateTranscriptionPostProcessing). It has drivers for several providers and uses the model the user picked intextGenerationModelSelection. This would be one more operation, e.g.classifyBackgroundTask. No new provider coupling, and nothing reaches a model that the user hasn't already sent titles, diffs and transcripts to. - Input: the command, its description and the agent's text just before and after starting it. Output:
waitornot_wait, usingwaitonly when the agent clearly intends to act on the result. Fixed rules wouldn't work: custom scripts (./scripts/qa.sh,bun run e2e:full) and the agent's own words ("tell me when to stop it") decide the answer, and rules can't read either reliably. - When: as soon as the task starts. A background command returns right away and the turn usually goes on, so the answer is normally there before the turn ends. If it isn't, the completion notification waits a moment. Either way the thread doesn't flash Done, and there's no race with auto-settle.
- Provider-neutral: it runs over the shared
{ command, description }task shape where pending background tasks are recorded or projected. So every provider that reports background commands benefits, including those withoutawaitBackgroundTasks. - A failure keeps today's behaviour. If the call fails, times out or text generation is off, the task stays un-awaited, exactly as now. The classifier can only add correct "Waiting" states, never break existing ones.
- The agent's word wins. Tasks the agent already labelled skip the call. An explicit
t3_background_wait({ taskIds, waiting: false })un-marks an inferred wait, andwaiting: truemarks one the model missed. The user still has Stop. Becauseawaitedis currentlyLiteral(true), keeping an explicit "not a wait" needs that state stored somewhere (e.g. a per-task override next to the inferred value). - Cost: one short call per unlabelled background task, about the size of naming a thread.
- No new kinds, UI or notification paths. The sidebar Waiting status, notifications, auto-settle and the composer strip already read
awaited.
This thread's commands are easy cases: an until … sleep loop described as "Wait for 184 gates", started right after "I'm waiting for the gates". T3 already stores every background task's command and description, so the classifier can be checked against existing threads before merging.
Steps to reproduce
- In any thread whose provider runs background commands (reproduced with Claude), ask: "Run
until [ -f /tmp/t3-done ]; do sleep 10; donein the background, tell me you're waiting for it, and end your turn. Don't call t3_background_wait." - Once the turn ends, the composer shows "Running 1 command".
- The sidebar row shows no Working or Waiting status, and the completion notification fires.
- Run
touch /tmp/t3-done. The agent wakes and keeps working in a thread that already reported itself as done.
Version
0.1.10-nightly.20261112.4564 (the server that ran the thread)
Environment
Server on Linux x64 (cloud). The thread ran on the Claude provider (claude-sonnet-5-5), viewed through the web client. The code paths above are provider-neutral, except awaitBackgroundTasks, which is implemented per adapter.
Evidence
# t3_thread_read (activity), redacted
thread.status=completed activeRunId=null pendingRequestCount=0 completedAt=2026-11-12T10:25:27Z
09:44:10 notification Command "Wait for 184 gates" was stopped
09:44:21 command E=~/<evidence>; sleep 30; until grep -q DONE $E/coord-status.txt 2>/dev/null; do sleep 60; done; cat $E/coord-status.txt
-> started in background (task bpouz203c)
09:44:35 assistant "...gates restarted. They're waiting for the shared lock..." (turn ends, no t3_background_wait)
10:25:22 command until grep -q DONE $E1/coord-status.txt 2>/dev/null; do sleep 60; done; echo 184; cat $E1/coord-status.txt
-> started in background (task b22im3ntx)
10:25:27 assistant "...I merge once the gates are green. When the gate queue reaches..." (turn ends, no t3_background_wait)Related issues
This builds on earlier discussion rather than reopening it:
- [Bug]: Sidebar hides a finished thread's unseen completion while a background shell it left running is still alive #14872, fix(clients): a dev server left running no longer says the thread is waiting #15114: commands stopped holding completion because dev servers left as hand-offs pulsed "Waiting" for hours. This proposal keeps that: servers and anything unclear stay un-awaited.
- fix(client-runtime): background commands keep threads in Working #15413 and discussion Should background commands keep a thread in the Working section? #15433: "any live command holds the thread" was declined, and fix(threads): Working holds only background work the agent waits on #24610 (
t3_background_wait) was chosen so the agent declares its waits. This proposal keeps that mechanism and fills the gap when the agent doesn't declare one, on any provider. - Proposal: make "waiting on other work" an explicit, bounded thread state #26689: deadlines and lapse reasons for declared waits. That work is complementary and would apply to inferred waits the same way.
- [Bug]: Thread shows as done at every intermediate stop of a multi-stage background job (sidebar Done badge, desktop "Thread completed", phone "Agent finished"), not once at the end #25585: Done shown at every intermediate stop of multi-stage background jobs. It has the same symptom; this issue adds the root cause and a specific fix, so it's filed separately rather than as a comment there.
- [Bug]: Thread auto-settles while the agent is still waiting on a background command #17625 / server, docs: hold auto-settle while a background command runs #18062: auto-settle while a background command runs. An inferred wait holds auto-settle through the existing rule.
- [Bug]: Claude agents call t3_background_wait with
taskIdand fail, because the instructions say "its task ID" but the schema needstaskIds#31889, [Bug]: t3_background_wait times out after 15 s on a busy Claude session and reports it as "Failed to update the awaited background work." #33646: ways the agent-side call fails today.
Fix applied or workaround
None applied. Workaround: tell the agent to call t3_background_wait with the task IDs of its wait commands.
Filed by
claude (claude-opus-5-5) via T3 Code, following the t3 triage playbook
Activity