A thread waiting on its own background command shows as done unless the agent marks the wait; T3 could mark obvious waits itself

Open
#35,214 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
32/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Active
Tech stack
typescript
Domain
ai, backend

Research direction

Read packages/contracts/src/orchestrationV2.ts and packages/shared/src/orchestrationV2PendingBackgroundWork.ts to understand how awaited tasks hold completion, then trace task registration and text generation through ThreadManagementService and apps/server/src/textGeneration/TextGeneration.ts. The issue proposes classifying unlabelled background tasks from their command, description, and nearby agent text; done means inferred waits hold thread completion while unclear tasks and failures preserve current behavior. This spans orchestration, provider adapters, and model calls, so it needs substantial project familiarity.

Written by the indexing model from the issue text.

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:

  1. started a background command until grep -q DONE <evidence>/coord-status.txt; do sleep 60; done; cat … with the description "Wait for 184 gates",
  2. wrote "I'm waiting for the gates, then I decide whether to merge",
  3. 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 a command or monitor only when awaited === true.
  • awaited is set only when the agent calls t3_background_wait, which goes through ThreadManagementService.awaitBackgroundTasks to the provider adapter's optional awaitBackgroundTasks.

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 get unsupported. 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 taskId but the schema needs taskIds (#31889)
    • registration has a 15s timeout that busy sessions hit (#33646)

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.ts already 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 in textGenerationModelSelection. 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: wait or not_wait, using wait only 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 without awaitBackgroundTasks.
  • 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, and waiting: true marks one the model missed. The user still has Stop. Because awaited is currently Literal(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
  1. In any thread whose provider runs background commands (reproduced with Claude), ask: "Run until [ -f /tmp/t3-done ]; do sleep 10; done in the background, tell me you're waiting for it, and end your turn. Don't call t3_background_wait."
  2. Once the turn ends, the composer shows "Running 1 command".
  3. The sidebar row shows no Working or Waiting status, and the completion notification fires.
  4. 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:

  • #14872, #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.
  • #15413 and discussion #15433: "any live command holds the thread" was declined, and #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.
  • #26689: deadlines and lapse reasons for declared waits. That work is complementary and would apply to inferred waits the same way.
  • #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.
  • #17625 / #18062: auto-settle while a background command runs. An inferred wait holds auto-settle through the existing rule.
  • #31889, #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

Dominant language
TypeScript
Stars
24.8k
Forks
6.4k
Avg merge
4h 16m
Merged PRs (30d)
352

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from pingdotgg/t3code

All issues in pingdotgg/t3code

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.