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

未关闭
#35,214 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
5/5
预计耗时
一周以上
新手友好度
32/100
Issue 类型
功能
描述清晰度
描述清楚
活跃度
活跃
技术栈
typescript
领域
ai, backend

调研方向

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.

由索引模型根据 Issue 内容生成。

描述

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

主要语言
TypeScript
星标
24.8k
派生
6.4k
平均合并
4 小时 16 分钟
30 天内合并 PR
352

环境准备

在 Codespaces 中打开

在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

pingdotgg/t3code 的其他 Issue

查看 pingdotgg/t3code 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。