Skip to content

[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

Open

Description

@MdSadiqMd

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

packages/contracts or packages/shared

Summary

For long jobs the agent can't finish in one turn, such as ML training, evaluation, video renders or data backfills, it works in a loop:

plan → start a job in the background → end the turn → (job finishes) → agent is woken → read results, decide the next step → start the next job → end the turn → … → final answer

Every time the agent ends a turn to wait for its own background job, T3 Code reports the thread as done on every surface, even though the work isn't finished and is still being monitored:

  • 📱 Phone: an "Agent finished" push.
  • 🖥️ Desktop sidebar: the thread row shows the green Done badge, or no status at all, but never Working or Waiting.
  • 🖥️ Desktop notification: a "Thread completed" OS notification and sound (with "Notifications with sound").

In a 4-stage pipeline the user gets 4 "finished" signals on each surface, and only the last one is true. "Done" stops meaning done, so the user has to open the thread each time to check. This has been happening for a long time.

Related: #15099 (closed as fixed by #24610), #14831, #13625, #19926. This report covers what is still broken after those:

  1. Stable is still affected. The fix in fix(threads): Working holds only background work the agent waits on #24610 (t3_background_wait) is not in any stable release. v0.1.2 (the current "Latest") does not have the tool. I reproduced the false completions live on it, on the phone path and the desktop sidebar and notification path. Details below.
  2. On main, the hold is opt-in at every stage. A thread is held only if the agent calls a deferred MCP tool (which it first has to load through ToolSearch) with the right task ID before every turn end. If it misses one call in a 4-stage loop, the user sees a false Done on the sidebar, the desktop notification and the phone.
  3. Only Claude can opt in. awaitBackgroundTasks is optional on ProviderAdapter, and only ClaudeAdapterV2 implements it. Every other provider gets "unsupported", so background commands never hold.
  4. Monitors regressed on main. In v0.1.2 a pending monitor held completion. Since fix(threads): Working holds only background work the agent waits on #24610 it holds only when marked. A common pattern is a background render or training command plus a Monitor that watches it. That pattern was held on v0.1.2 and now shows Done at every stop unless the agent marks it.

Example (what the user experiences)

User: "Fine-tune the model, evaluate it, and if it beats the baseline, run the ablations and write up the results."

# Agent Background Desktop sidebar Desktop notification Phone
1 Plans, starts training (run_in_background), ends turn: "Training is running; I'll check the loss when it finishes." training running (2 h) Done ❌ Thread completed 🔔 ❌ Agent finished 🔔 ❌
2 Woken when training ends. Reads the curves, starts evaluation, ends turn. eval running (30 min) Done ❌ Thread completed 🔔 ❌ Agent finished 🔔 ❌
3 Woken when eval ends. Beats the baseline, so it starts ablations and ends the turn. ablations running (1 h) Done ❌ Thread completed 🔔 ❌ Agent finished 🔔 ❌
4 Woken when ablations end. Writes the results. none Done ✅ Thread completed ✅ Agent finished ✅

Expected: while the agent waits on its own background job, the sidebar shows Working or Waiting · , and no completion notification or push fires. One Done, one desktop notification and one push happen at step 4 (or, for steps 1–3, a clearly non-final signal such as "Waiting on: Run evaluation").
Actual: Done, "Thread completed" and "Agent finished" at steps 1, 2, 3 and 4.

Steps to reproduce (deterministic, ~3 min, stable v0.1.2)

  1. Turn on desktop notifications ("Notifications with sound"), and mobile push for the environment (T3 Connect).

  2. Create a Claude thread (any model; I used Claude Haiku 5.5, low effort), open it, then switch to another thread or app.

  3. Send it this prompt:

    This is a test of a two-stage mock ML pipeline. Keep replies to one short sentence.
    Stage 1: use Bash with run_in_background=true to run:
      sleep 75 && echo "train_loss=0.42" > ./stage1.txt
    Then end your turn with: "Training is running in the background; I'll check results when it finishes." Do not poll, sleep, or use Monitor.
    When notified that stage 1 finished, read ./stage1.txt, then start Stage 2 the same way:
      sleep 75 && echo "eval_acc=0.91" > ./stage2.txt
    Then end your turn with: "Evaluation is running in the background; I'll report when it finishes."
    When notified that stage 2 finished, read ./stage2.txt and reply: "Pipeline done: train_loss=0.42, eval_acc=0.91."
    
  4. Watch the thread row in the sidebar, the desktop notifications, and the phone. Each one reports "done" three times for one pipeline.

Evidence 1: desktop sidebar and desktop notification, live on stable v0.1.2

Setup: the v0.1.2 web client served by the same local server as the desktop app, with notificationMode: "notifications-and-sound" (the same as the desktop app's own client settings). The page was instrumented to emulate the app being in the background (document.hasFocus() → false). window.Notification was stubbed to record each notification the client tried to show. The sidebar state was read from each thread row's accessible label ("<title>, <status>, <project>").

Stop Run completed (UTC) Still running (pendingBackgroundTasks) Sidebar row label Desktop notification recorded
1 17:46:32.990 [{"taskId":"bp99jnhf1","kind":"command"}] (training) "Repro 2: …, No project": no Working or Waiting, so it looks idle* "Thread completed" at 17:46:33.044 ❌
2 17:47:52.389 [{"taskId":"bbgfft7ss","kind":"command"}] (evaluation) "Repro 2: …, Done, No project" ❌ "Thread completed" at 17:47:52.442 ❌
3 17:49:10.715 [] "Repro 2: …, Done, No project" ✅ "Thread completed" at 17:49:10.769 ✅

Runs 2 and 3 were started automatically by wake messages with notification.source.kind: "background_command". Each false "Thread completed" fired about 50 ms after the run ended, while the job's sleep process was still alive.

* At stop 1 the Done badge was hidden only because a client had the thread open at that moment: a thread.visit moved lastVisitedAt to exactly the completion time. The row still showed neither Working nor Waiting. At stop 2 nobody was viewing the thread, and the row showed Done while the evaluation job was running.

The completion sound uses the same trigger as the desktop notification (playNotificationSound(kind, …) runs right before it in ThreadNotificationCoordinator). My stub didn't capture the audio path, so the sound comes from reading the code, not from observation.

Evidence 2: phone push path, live on stable v0.1.2

Same pipeline in a separate thread (60 s stages). Server state captured with read-only SQLite reads of statev2.sqlite and t3_thread_read at each stop:

Run Triggered by Run completed (UTC) pendingBackgroundTasks at completion Thread status Projected relay phase (v0.1.2 rule) Agent's last message
1 user 17:33:42.335 [{"taskId":"b4ktlbi02","kind":"command"}] (the sleep 60 process was alive, checked with pgrep) completed, activeRunId: null completed → "Agent finished" "Training is running in the background; I'll check results when it finishes."
2 wake background_command, requested 17:34:40.942 17:34:50.229 [{"taskId":"bx0xyludo","kind":"command"}] completed completed → "Agent finished" "Evaluation is running in the background; I'll report when it finishes."
3 wake background_command, requested 17:35:49.036 17:35:54.932 [] completed completed → "Agent finished" ✅ "Pipeline done: train_loss=0.42, eval_acc=0.91."

None of the pending tasks carry awaited, and on v0.1.2 the agent has no way to set it. The server's relay publish spans (AgentAwarenessRelay.publishThread) do fire at each stop. They carry no thread or phase attributes, so I could not tie individual APNs deliveries to them from the logs. The phase decision is deterministic from the state above.

Evidence 3: how often this happens in real use (one machine)

Scan of the raw Claude SDK streams in ~/.t3/userdata/logs/provider/events.*.log*. For each result (turn end), I recorded the background_tasks_changed roster that was live at that moment:

  • Since the v0.1.2 release (2026-10-22 20:44 UTC): 134 Claude turn ends. 22 of them (16%) ended with only an unmarked background command still running, so each showed as done on v0.1.2.

  • In a data-processing thread, one user request produced three completed runs: "the download is running in the background" → wake → "the upload is running in the background" → wake → actually done. The user then sent "see how much is it completed" three times, because the thread kept looking finished.

  • Multi-stage render thread (2026-10-21). One request led to three turn ends, and each roster had a background render plus a Monitor watching it:

    • 17:42:36 "Waiting for the preview render to finish.": roster [local_bash: Render all chapters low-q…, Monitor: low-q render…]
    • 17:47:53 "The final render is running … I'll verify the output when it finishes.": roster [local_bash: Final 1080p60 render…, Monitor: final 1080p60 render]
    • 18:03:06 the actual result: roster []

    On v0.1.2 the Monitor holds these stops. On main they show as done unless the agent calls t3_background_wait (point 4 above).

Root cause

All three surfaces use the same rule, backgroundWorkHoldsCompletion. When a run completes and the remaining background work doesn't "hold completion", the thread counts as finished.

The rule. On v0.1.2 (packages/shared/src/orchestrationV2PendingBackgroundWork.ts @ v0.1.2), a command never holds, so a background Bash job always reads as finished:

case "command":
  return false;
case "subagent":
case "monitor":
case "background_task":
  return true;

On main @ e762f09e (orchestrationV2PendingBackgroundWork.ts:89-108, after #24610), commands and monitors hold only when the agent marks them awaited:

// Commands and monitors are as often a dev server or its watchdog as a wait, so they hold
// only when the agent marks them awaited.
case "command":
case "monitor":
  return false;

The only way to set awaited is the agent calling t3_background_wait (apps/server/src/mcp/toolkits/orchestrator/tools.ts:120, handler handlers.ts:60-80). The Claude system prompt asks it to (ClaudeAdapterV2.ts:1396-1403). Other providers get "unsupported" (ThreadManagementService.ts:1142, optional awaitBackgroundTasks at packages/provider-core/src/server/ProviderAdapter.ts:595).

Desktop sidebar: Done badge, no Working or Waiting. shellRuntime (packages/client-runtime/src/state/models.ts:216-223 @ v0.1.2, :220-227 on main) parks the thread at idle (shown as Waiting) only when the rule holds:

const parkAtIdle =
  backgroundWorkHoldsCompletion(thread.pendingBackgroundTasks ?? []) &&
  thread.status !== "failed";
...
const status = parkAtIdle ? "idle" : (thread.activityRunStatus ?? thread.status);

For an unmarked background command, the runtime status stays completed. resolveSidebarThreadStatus (apps/web/src/components/Sidebar.logic.ts:894 @ v0.1.2, :868 on main) then returns ready. hasUnseenCompletion (:716 @ v0.1.2, :689 on main) is true because the run's completedAt is newer than lastVisitedAt. So SidebarThreadRow.tsx renders the Done badge (:636/:696 @ v0.1.2, :684/:750 on main).

Desktop notification and sound: "Thread completed". ThreadNotificationCoordinator.tsx (v0.1.2 :139-174, :222) uses resolveSidebarThreadStatus too. status === "ready" && latestRun.status === "completed" with a newer completedAt counts as a new completion, which plays the completion sound and shows the toast or OS notification. On main (:187-210) this goes through createThreadNotificationGate, which waits only WORKING_EXIT_GRACE_MS = 2_000 (packages/client-runtime/src/state/threadInbox.ts:115). That covers the run-to-continuation handoff race from #13625, not a job that runs for minutes or hours.

Phone push: "Agent finished". projectThreadAwarenessV2 (packages/shared/src/agentAwareness.ts:97-135) maps a completed run to completed unless the rule holds:

case "completed":
  return backgroundWorkHoldsCompletion(thread.pendingBackgroundTasks ?? [])
    ? "running"
    : "completed";

The relay then alerts on every move into completed (apps/server/src/relay/AgentAwarenessRelay.ts:557-563, comment: "the relay alerts on every move into completed"). Its 5 s confirmation hold (nowMs + 5_000) has the same limit as the desktop's 2 s grace: it only covers the gap between a run ending and its continuation starting. Here the continuation starts when the job finishes, so every intermediate stop is a fresh completion (infra/relay/src/agentActivity/agentActivityAlerts.ts:149-161).

In short: whether the sidebar, the desktop notification and the phone all say "done" at a background-job stop depends entirely on the agent volunteering a tool call. In a multi-stage loop it has to do that at every stage. On stable it can't do it at all.

Suggested fix direction

The opt-in rule from #24610 suits auto-settle and the inbox, where a dev server must not keep a thread "working" forever. But a Done badge or notification while the agent's own job is still running is misleading, and a notification or push can't be taken back. Options:

  1. Show a distinct state while background work from the finished run is still pending (marked or not). In the sidebar, use Waiting · , not Done or blank. The roster already has description (this was the proposal in Show a Waiting status while a thread has running background tasks #19926). Keep Done for an empty roster. A dev server left running then reads "Waiting · dev server" instead of being stuck in Working, which is still accurate.
  2. Defer completion notifications (desktop and push) while that work is pending. Fire them when one of these happens:
    • (a) the roster drains and no wake run starts within the existing grace or confirmation window;
    • (b) the wake run completes with an empty roster;
    • (c) a cap passes (for example 10–15 min), so threads that leave a dev server running still get notified, just later.
  3. Or send a non-final signal at a background-wait stop, such as "Waiting on: Run stage 2 evaluation". Keep "Thread completed" and "Agent finished" for an empty roster.
  4. Ship fix(threads): Working holds only background work the agent waits on #24610 in a stable release. v0.1.2 users currently have no way to avoid this other than turning off notifications.
  5. Consider whether a Monitor that watches a pending background command from the same run should hold by default (the render and training pattern), since it held on v0.1.2.

Regression test idea: a completed run with an unmarked pending command, followed by a background_command wake run that completes with an empty roster, should produce exactly one Done transition, one desktop completion notification and one relay completion alert.

Impact

Major degradation or frequent failure

Version or commit

Reproduced on stable v0.1.2 (desktop app, web client and server 0.1.2). Code references are on v0.1.2 and main @ e762f09e.

Environment

macOS (arm64), T3 Code desktop 0.1.2 with "Notifications with sound", Claude provider (Claude Code SDK; Opus 5.5 in real use, Haiku 5.5 for the repro), mobile push via T3 Connect.

Workaround

None, other than turning off notifications and ignoring the sidebar Done badge. On nightly builds that include #24610, telling the agent to call t3_background_wait before ending each turn should help, but it depends on the model complying every time.


Contributions welcome

This is open for anyone who wants to pick it up. The root cause, a deterministic repro, and fix options are laid out above. If you'd like to take it, leave a comment so we don't double up, and feel free to ask questions on the issue.

Activity

  1. changed the title [-][Bug]: Phone gets an "Agent finished" push at every intermediate stop of a multi-stage background job (train → eval → …), not once at the end[/-] [+][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[/+] on Oct 26, 2026
  2. EtanHey commented on Oct 31, 2026

    @EtanHey
    Contributor

    Additional UX evidence on Nightly 0.1.6-nightly.20261030.3940: three Claude threads had completed turns and background inbox watchers. One appeared in Working → Waiting; two appeared idle. Read-only state and history showed only the first had awaited: true from t3_background_wait.

    The grouping matches #24610, but the UI gives users no explanation for the difference. A watcher can remain after the actual work finishes; the distinction needs to be understandable without inspecting internal flags.

    GPT-6.1 Sol / Codex in T3 Code triage.

  3. vycdev commented on Oct 31, 2026

    @vycdev

    Note

    🤖 Claude Opus 5.5 responding on behalf of vycdev

    This still happens on 0.1.5 (Windows, Claude threads), which ships t3_background_wait, so having the tool hasn't fixed it in practice. In the threads below the agents didn't call it, even though T3's Claude system prompt tells them to. vycdev finds this very annoying and considers it an important fix, because the Done badge can't be trusted while it depends on the agent remembering.

    Verified on one machine on 2026-10-31, using read-only statev2.sqlite queries and the process list (times in UTC):

    • Test suite: the agent's last message was "I'll commit as soon as the suite passes." The run completed at 09:00:58 with one pending command and no awaited. The suite finished at 09:06:05 and woke the agent, so the thread sat completed for 5 minutes while the tests ran.
    • Long measurement script: vycdev asked "are you still working?" at 09:00:50. The agent correctly said yes, and that run also completed at 09:00:58 with the command pending and no awaited. The process was still alive at 09:07:31.
    • Local history (about 14 hours): there is one t3_background_wait call on record, followed by 7 background command completions with no call for any of them. This is a small sample, and I only read the two threads above closely, so some of the 7 may not have been real waits.

    This bears on the workaround in the issue, which notes that it depends on the model complying every time. Here it mostly didn't.

    My view: options 1 and 2 are the ones to prioritize, meaning a distinct state for any pending background work (marked or not) and deferred completion notifications. Neither depends on the agent. Option 4 is done as of 0.1.5 and doesn't solve this on its own.

  4. MatEE404 commented on Nov 2, 2026

    @MatEE404

    Note

    🤖 Claude Opus 5.5 posting on behalf of MatEE404, after a read-only look at the thread and its provider log.

    Another case on a recent nightly. This time the agent skipped t3_background_wait even after the user explicitly told it to keep working until done. The thread view and the sidebar also disagreed on the same screen.

    Version: T3 Code (Nightly) 0.1.5-nightly.20261029.3877, macOS 26.5.1 arm64, Claude Code 2.1.312, claude-opus-5-5 (effort high).

    What happened (times UTC, 2026-11-02)

    The agent was running long local test gates through a machine-wide test queue before a push.

    Time Event
    14:20:49 Run 44 ends with "Waiting for all the gates to finish." A background gate command (b612kskjw) is still running. The agent's own reasoning just before said "I'll wait for the gates notification before ending the turn". It did not call t3_background_wait.
    14:22:49 The user, seeing the thread out of Working, asks: "Is this finished yet? How much is left? Keep working until it's done if it isn't."
    14:23:12 Run 45 replaces the gate script with a parallel version, started with run_in_background (b5moqc15w, "Run all gates in parallel through test queue"). The provider log shows background_tasks_changed with that task and task_started with is_backgrounded: true.
    14:23:47 Run 45 ends (result/success) with "Waiting for the gates." There was no t3_background_wait call in the run, and b5moqc15w was still on the roster.
    ~14:24 The same window shows both states. The thread view shows the bottom bar "Running Run all gates in parallel through test queue · Stop" and a spinner next to the send button. The sidebar row sits outside Working, at the top of the list with a "1m" timestamp, so it reads as finished.
    14:25:26 The user thinks the agent quit and writes: "Why did you stop working? Keep working until you finish everything, don't stop at all."
    14:25:38 Only now does run 46 call t3_background_wait(["b5moqc15w"]), and the thread goes back to running. The gate processes were alive the whole time (checked with pgrep).

    What this adds to the issue

    1. An explicit user instruction is not enough. The user said "keep working until it's done" in the message that started run 45, and the agent still ended the turn without marking the task. It called the tool only after a second message. This adds to vycdev's data: model compliance is not a reliable gate for Done.
    2. The client already knows the work is running. The thread view rendered the running background command with a Stop button while the sidebar classified the same thread as not working. The sidebar status and the thread view's command bar seem to use different rules (backgroundWorkHoldsCompletion vs the raw pendingBackgroundTasks roster). A user who sees both reasonably calls it a bug, not a missing feature.
    3. Real cost: the false Done made the user interrupt a healthy thread twice. A steer during a long wait can derail the agent's plan (here it restarted the gate script).

    +1 to options 1 and 2 above. A Waiting · <task description> state for any pending background work from the finished run, marked or not, would have shown exactly what the thread view already showed: "Waiting · Run all gates in parallel through test queue".

  5. Gooder24 commented on Nov 3, 2026

    @Gooder24

    Note

    🤖 Claude Opus 5.5 posting on behalf of Gooder24

    Same symptom here on nightly 0.1.7-nightly.20261103.4106 (Linux, Claude threads): a thread kept working on its own run_in_background command but sat outside Working, because the agent never called t3_background_wait.

    Since #24610 depends on the agent remembering the call, we added a client-side workaround: a Claude Code PostToolUse hook on Bash|Monitor that fires only inside T3 threads (T3CODE_THREAD_ID is set) and only for the root agent. When a tool result carries a background task ID (backgroundTaskId for Bash, also when a command times out into the background, and taskId for Monitor), it adds this context:

    Background task <id> is running. If you will act on its result when it ends (tests, build, benchmark, CI, deploy, render, a command that timed out into the background), call mcp__t3-code__t3_background_wait with taskIds ["<id>"] now, before ending your turn. […] Skip it only for work you leave running on purpose (dev server, watcher).

    In a live test, the reminder arrived right after the background command started, and the following t3_background_wait call matched the task. Script (about 40 lines, Python standard library): https://github.com/Gooder24/Karchi-skills.md/blob/main/hooks/background-wait.py

    This keeps the explicit-wait contract from #15433 (dev servers still don't hold Working) but puts the reminder at the moment the task ID exists, instead of only in the system prompt. T3's Claude adapter could inject the same reminder itself through the SDK's hooks, so every user gets it without setting anything up. It is still a nudge, not a guarantee: the agent can ignore it.

  6. BlackPhoenixSlo commented on Nov 5, 2026

    @BlackPhoenixSlo

    Note

    🤖 Claude Opus 5.5 posting on behalf of BlackPhoenixSlo

    +1, same on T3 Code (Beta) 0.1.2, macOS. Threads whose agent is still driving background work (subagents, background commands) show Done, so every "done" thread has to be opened to check whether it is really finished. Done has stopped meaning done.

    Ask: make the states visibly distinct so it is clear without opening the thread, e.g. Done only when nothing is pending, and a separate Waiting · / Background running badge (no completion notification) while the thread still has pending background tasks or subagents — ideally derived from pendingBackgroundTasks rather than depending on the agent calling t3_background_wait.

  7. vertpierre commented on Nov 6, 2026

    @vertpierre

    Note

    🤖 Claude Opus 5.5 posting on behalf of vertpierre, after a test on this machine.

    Another angle on this, from the parent agent's side, on 0.1.7 stable (Linux AppImage, Claude parent, Claude child, claude-opus-5-5).

    A delegated child runs a background command, calls t3_background_wait, ends its turn, and repeats once when woken. The parent received three notices, in this order, each marked completed:

    [completed] Stage one has started.
    [completed] Stage two has started.
    [completed] FINAL Both stages ended.
    

    The final report does arrive, so the lost final report we saw on 0.1.5 and 0.1.6 no longer reproduces on this path. But a parent agent can't tell an interim sentence from a final report, because they share one status. We work around it by having children start their last message with FINAL.

    A separate status for "waiting on its own background job", as asked above, would fix this for parent agents too, not only for the Done badge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions