Repository navigation
[Bug]: Actively running thread shows no "Working" status in the sidebar (stale/undelivered runtime shell) #25992
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/web (resolution is correct here; the real gap is upstream in packages/client-runtime shell hydration + apps/server shell delivery)
Summary
An actively running thread shows no Working status in the left sidebar. The row renders, but with no Working label/spinner, no placement in the Working shelf, and no running timer — as if the thread were idle/ready — even though a provider turn is in progress the entire time.
This is a false negative (running work shown as not-working). It is the mirror image of #25449 (a stale row that keeps claiming Working after the connection drops): both are cases where the sidebar's cached thread shell disagrees with the thread's real runtime, but in opposite directions.
Steps to reproduce
- From the web app, create a thread that runs on a server-backed provider instance (observed on
claudeAgent,runtimeMode: full-access) in a scratch workspace (worktreePathset,branch: null). - Send a prompt so the run starts; the agent keeps working in the foreground (tool calls, commands) for a while.
- Open the desktop/web sidebar and look at that thread's row while the run is active.
- Compare against ground truth:
t3_thread_readon the same thread reportsstatus: "running", a non-nullactiveRunIdandlatestRunId,pendingRequestCount: 0, and no held background tasks.
Expected behavior
While a provider turn is active, the sidebar row shows Working (label + spinner), the thread sorts into the Working shelf, and the running timer advances — consistent with t3_thread_read reporting status: running.
Actual behavior
The row shows no Working status and does not enter the Working shelf while the run is active. Nothing in the pure client resolver explains this — given the thread's real inputs, the resolver returns "working" (traced below). The sidebar is therefore acting on a stale/undelivered runtime shell for this thread, not on its live run status.
Live evidence (this exact thread)
t3_thread_read for the running thread, captured mid-run:
{
"status": "running",
"latestRunId": "run:thread:…:ordinal:1",
"activeRunId": "run:thread:…:ordinal:1",
"providerInstanceId": "claudeAgent",
"runtimeMode": "full-access",
"creationSource": "web",
"branch": null,
"worktreePath": "/Users/…/.t3/scratch/…", // scratch workspace, not a git worktree
"pendingRequestCount": 0,
"recentRuns": [{ "ordinal": 1, "status": "running", "completedAt": null }]
}No pending approvals/input, no guest messages, no held background tasks. These are exactly the inputs that must resolve to Working.
Root-cause analysis
Verified against main @ 36d4d711. The client status resolver is correct — so the bug is upstream of it.
1. The sidebar badge requires resolveSidebarThreadStatus(thread) === "working".
apps/web/src/components/sidebar/SidebarThreadRow.tsx:685—const status = resolveSidebarThreadStatus(thread);SidebarThreadRow.tsx:700-705— onlystatus === "working"produces theWorking(orGoal) label + spinner; every other branch yieldsWaiting/Done/ nothing.
2. resolveSidebarThreadStatus returns "working" for a live run. (apps/web/src/components/Sidebar.logic.ts:871-896)
if (thread.runtime?.status === "running" && thread.blockedOnThreadWait === true) return "waiting"; // :882
if (thread.runtime !== null &&
["preparing","queued","starting","running","waiting"].includes(thread.runtime.status)) return "working"; // :885-889
if (thread.runtime?.status === "idle") return "waiting"; // :890-892
…
return "ready"; // runtime === nullFor this thread runtime.status should be "running" and blockedOnThreadWait is false ⇒ "working".
3. thread.runtime is built by shellRuntime. (packages/client-runtime/src/state/models.ts:220-243)
const parkAtIdle =
backgroundWorkHoldsCompletion(thread.pendingBackgroundTasks ?? []) &&
thread.status !== "failed"; // :221-224 → false here (no bg tasks)
if (thread.latestRunId === null && thread.activeProviderThreadId === null && !parkAtIdle)
return null; // :224-226 → not taken (latestRunId set)
const status = parkAtIdle ? "idle" : (thread.activityRunStatus ?? thread.status); // :227With no held background work and latestRunId set, this returns { status: "running", … } (no activity run ⇒ activityRunStatus is null ⇒ falls through to thread.status === "running").
4. The server projects the same. (apps/server/src/orchestration-v2/ProjectionStore.ts)
:1676—status: latestRun?.status ?? "idle"⇒"running"for an active run.:1671—activityRunStatus: activityRun?.status ?? null.
Conclusion. Every layer that transforms a known-running thread maps it to Working. The row nonetheless shows no Working status, so the sidebar is rendering a thread shell whose runtime was never advanced to running for this thread — i.e. a shell hydration/propagation gap, not a resolver bug.
Decision table (what the chain should produce vs. what is shown)
| Layer | Expected for this thread | Produces Working? |
|---|---|---|
Server projection status (ProjectionStore.ts:1676) |
running |
✅ |
shellRuntime().status (models.ts:227) |
running |
✅ |
resolveSidebarThreadStatus (Sidebar.logic.ts:885-889) |
working |
✅ |
SidebarThreadRow badge (SidebarThreadRow.tsx:700-705) |
Working |
❌ (shown as no status) |
The break is between the server knowing running and the sidebar row's cached thread.runtime reflecting it.
Candidate mechanisms to investigate (not individually confirmed)
- Cross-surface shell delivery. The thread is web-created but observed in the desktop sidebar. The run-start
runningtransition may not be pushed into the desktop sidebar's shell subscription, so the row keeps the shell it hydrated at creation (preparing/idle⇒runtimenull/idle ⇒ready/Waiting⇒ noWorking). Same cross-surface family as [Bug]: Assistant message hover timestamp missing while a command is still running #25466. - Hydration vs. perf trimming. Scratch/worktree threads hydrate at creation; a later
runningshell update may be trimmed, batched, or deprioritized by shell-size optimizations (cf. "cold connects skip older settled threads, 93% of the shell JSON" perf(shell): cold connects skip older settled threads, 93% of the shell JSON #24723). If the initial shell lands as non-running and the delta is dropped, the row never flips toWorking. - Cached runtime on the row. The same cached-shell family as [Bug]: Cached thread rows keep claiming Working while their environment reconnects #25449, but stuck in the not-working state rather than the working state.
Impact
- Running work is invisible in the sidebar: no
Workinglabel, no Working-shelf placement, no live timer. Users can't tell an active thread from an idle one, and a genuinely busy thread looks stalled/finished. - Undermines the Working shelf's purpose (surfacing in-flight threads) precisely for background/remote runs, where the sidebar is the only signal the user has.
Suggested fix direction
- Confirm what
thread.runtime.statusthe sidebar actually holds for a running web-created/scratch thread on another surface (log it, or a status-resolver test seeded from the real shell). - If the shell is stale, fix the delivery/hydration of the
runningruntime transition to the sidebar shell store (ensure run-start shell deltas reach every subscribed surface and are not trimmed), rather than touchingresolveSidebarThreadStatus— the resolver is correct. - Add a regression test asserting that a thread with
status: running,activeRunId/latestRunIdset, and no held background tasks resolves toworkingend-to-end from the server shell projection.
Related
- [Bug]: Cached thread rows keep claiming Working while their environment reconnects #25449 — cached row keeps claiming
Workingwhile the environment reconnects (opposite direction; shared "sidebar runtime shell is stale vs. real runtime" root family). - [Bug]: Sidebar retains deleted worktree/branch while right status card updates correctly #24935 — sidebar retains deleted worktree/branch while the right status card updates correctly (another sidebar-shell-staleness case).
- [Bug]: Pending async questions lose their thread-list indicator while the agent keeps working #24914 — running thread stays
Workingbut loses its pending-question indicator (adjacent sidebar/runtime-status surface).
Contributions welcome
This is a good pickup for anyone familiar with the shell subscription / client-runtime state. Comment to claim it or ask questions on the issue. The verified resolver chain above narrows the search to shell delivery/hydration, and the decision table pinpoints where the state diverges.
Environment
macOS desktop app observing a web-created, server-backed thread. Code verified on main @ 36d4d711. Reported 2026-10-27.
Activity
juliusmarminge commentedon Oct 27, 2026
I tried to reproduce this on
mainat 36d4d71 (the commit named in the report). I used the web dev server on a fresh database with two independent signed-in browser clients. Each turn was a 1.5–3 minute run of sleeping shell commands.So a stale or undelivered shell did not reproduce. I found one path that gives exactly the symptom you describe. If a running thread is snoozed, it moves to the Snoozed shelf as a compact row showing only the snooze countdown: no Working label, no Working shelf, no run timer. The turn keeps running the whole time. A settled thread renders the same way, but the server refuses to settle a running thread.
To tell which case you hit, could you share:
MdSadiqMd commentedon Oct 27, 2026
Thanks for the careful repro. You were right that the shell is not stale. My original root-cause guess was wrong, and I've corrected it below. I went through the server's own projection DB, the provider session logs, and the live process tree for two threads that showed this today. Here is what is actually happening.
Summary
Neither thread was snoozed or settled, and neither is in a provider turn. In both, the agent ended its turn after starting a background shell command that waits on a long job, for example
until [ -f done-marker ]; do sleep 90; done. The agent resumes when that command exits. The server records the command inpendingBackgroundTasksaskind: "command"with noawaitedflag. By design (orchestrationV2PendingBackgroundWork.ts:97-107), an unmarked command never holds the thread, so the thread presents as completed. The sidebar row gets noWorking, noWaiting, no shelf and no timer. Meanwhile the thread view shows a "Running: …" background-work banner for the same task.That is why it looks "still running" to the user. The agent's work is live and the agent will wake itself up, but the sidebar shows the row as finished. Your repro ran
sleepcommands in the foreground inside a turn, which correctly showsWorking. This case happens between turns.Evidence (two independent threads, both on v0.1.3)
completed15:42:31Zcompleted16:04:51ZidleidlependingBackgroundTasks[{ kind: "command", description: "Wait for the … training or a failure" }], noawaited[{ kind: "command", description: "Wait for next queue run to finish" }], noawaitedrun_in_background, then ended the turn ("needs roughly 7 more hours")/tmp/waitnext.sh, then ended the turn<task-notification>from the previous waitsnoozedAt/snoozedUntil/settledAt/settledOverridenullnulllastVisitedAtcompletedAtcompletedAtt3_background_waitcalled?Thread B is on the local server, so connection loss is ruled out. For thread A the desktop's SSH tunnel did reconnect several times today, but that doesn't matter here: the server's own projection already says
completed, so the client renders that state faithfully.Why the row shows nothing (main @ 36d4d71, identical at v0.1.3)
packages/shared/src/orchestrationV2PendingBackgroundWork.ts:89-107checksbackgroundWorkHoldsCompletion.commandandmonitorhold the thread only whenawaited === true, because they are "as often a dev server or its watchdog as a wait".packages/client-runtime/src/state/models.ts:216-227setsparkAtIdle = false, soruntime.status = thread.status = "completed".apps/web/src/components/Sidebar.logic.ts:871-896:resolveSidebarThreadStatusreturns"ready". TheWaitingpill atSidebar.logic.ts:1106-1108is also gated onbackgroundWorkHoldsCompletion, so it doesn't show either.SidebarThreadRow.tsx:699-745setstopStatus = null. That's because the status isready, not woke, andlastVisitedAt === completedAtmeans not unread, so there isn't even aDonebadge. The row sits in the main list, not the Working shelf, with no label.useThreadLifecycle.tsx:372passes the roster tobackgroundWorkNowItem, andpresentPendingBackgroundWork(threadExecution.ts:314) titles itRunning: <description>. So the thread view says running while the sidebar row shows nothing.t3_background_wait. The Claude system prompt asks for it (ClaudeAdapterV2.ts:1402), and the adapter then marks the taskawaited(ClaudeAdapterV2.ts:2784-2790). In both threads the agent skipped that call on every wake-up cycle.Answers to your questions
WaitingorDone.nullon both threads, and neither was archived.completedwith a non-holding roster, so a fresh hydrate resolves to the same thing.What I think the bug is
The status model can't tell "a background command the agent is waiting on and will resume from" apart from "a dev server the agent left running". Everything then depends on the agent remembering to call
t3_background_wait. Agents running long poll loops routinely don't, which these two threads show across nine wake-up cycles. The result is that the most common long-running pattern (start a job, poll for it, wake up, re-arm the wait) shows as finished in the sidebar for hours.Possible directions, roughly from least to most invasive:
Background/Running in backgroundlabel on the row instead of nothing. The thread view already does this. The roster is already in the shell, so this is client-only.run_in_backgroundBash task notifies the agent when it exits, and the agent resumes. If the agent's last turn ended while such a task was still running, treating it as holding (showingWaiting) would match what users expect. Dev servers would still need an opt-out, which is the trade-off the current design avoids.awaitedwhen the next run is triggered by that task's notification. Both threads show that pattern repeatedly.I'll retitle the issue to match these findings if that helps triage, for example: "Thread waiting on an unmarked background command shows no status in the sidebar (neither Working nor Waiting)".
juliusmarminge commentedon Oct 27, 2026
Thanks for tracing this through the projection and the process tree. With that correction, this is the same symptom as https://git.cafe/pingdotgg/t3code/issues/25585: an agent ends its turn while waiting on its own
run_in_backgroundcommand, and the sidebar row shows no Working or Waiting status (or shows Done) until the agent wakes.Checked on main @ 36d4d71.
backgroundWorkHoldsCompletionreturnsfalsefor acommandormonitortask unless the agent marks itawaitedthrought3_background_wait. The existing tests assert exactly this (orchestrationV2PendingBackgroundWork.test.ts, "a dev server" → false;Sidebar.logic.test.ts, "presents a completed thread with a dev server roster" →ready/done), and both pass. That opt-in was the trade-off made in https://git.cafe/pingdotgg/t3code/pulls/24610 so that a dev server left running doesn't keep a thread in Working.Closing as a duplicate of https://git.cafe/pingdotgg/t3code/issues/25585 so the discussion stays in one place. Your two threads (nine wake-ups, none marked) and your suggested directions (a muted background label on the row; inferring
awaitedwhen the next run starts from that task's notification) are useful evidence there.