Skip to content

[Bug]: Actively running thread shows no "Working" status in the sidebar (stale/undelivered runtime shell) #25992

Closed

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

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

  1. 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 (worktreePath set, branch: null).
  2. Send a prompt so the run starts; the agent keeps working in the foreground (tool calls, commands) for a while.
  3. Open the desktop/web sidebar and look at that thread's row while the run is active.
  4. Compare against ground truth: t3_thread_read on the same thread reports status: "running", a non-null activeRunId and latestRunId, 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 — only status === "working" produces the Working (or Goal) label + spinner; every other branch yields Waiting / 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 === null

For 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); // :227

With 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)

  1. Cross-surface shell delivery. The thread is web-created but observed in the desktop sidebar. The run-start running transition may not be pushed into the desktop sidebar's shell subscription, so the row keeps the shell it hydrated at creation (preparing/idle ⇒ runtime null/idle ⇒ ready/Waiting ⇒ no Working). Same cross-surface family as [Bug]: Assistant message hover timestamp missing while a command is still running #25466.
  2. Hydration vs. perf trimming. Scratch/worktree threads hydrate at creation; a later running shell 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 to Working.
  3. 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 Working label, 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.status the 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 running runtime transition to the sidebar shell store (ensure run-start shell deltas reach every subscribed surface and are not trimmed), rather than touching resolveSidebarThreadStatus — the resolver is correct.
  • Add a regression test asserting that a thread with status: running, activeRunId/latestRunId set, and no held background tasks resolves to working end-to-end from the server shell projection.

Related

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

  1. juliusmarminge commented on Oct 27, 2026

    @juliusmarminge
    Member

    I tried to reproduce this on main at 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.

    • The tab that created the thread (Claude Haiku 5.5, full access) showed the row as Working with a running timer, inside the Working shelf.
    • A second client paired mid-run hydrated straight into Working.
    • The second client was already subscribed when a new run started on the same thread: Working within seconds.
    • A thread created in the second client (Codex), watched from the first: Working. The regenerated title also arrived live.

    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:

    1. A screenshot of the sidebar while it happens. Under which header did the row sit (Working, the main list, Snoozed, Settled, Pinned)? Did it show any label at all, such as "Waiting"?
    2. Whether the thread had been snoozed or settled earlier, by you, by an agent, or automatically.
    3. The desktop app version, and whether the Mac slept or the connection dropped while the run was going.
    4. Whether reloading the window (or opening the thread) made Working appear.
  2. MdSadiqMd commented on Oct 27, 2026

    @MdSadiqMd
    Author

    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 in pendingBackgroundTasks as kind: "command" with no awaited flag. By design (orchestrationV2PendingBackgroundWork.ts:97-107), an unmarked command never holds the thread, so the thread presents as completed. The sidebar row gets no Working, no Waiting, 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 sleep commands in the foreground inside a turn, which correctly shows Working. This case happens between turns.

    Evidence (two independent threads, both on v0.1.3)

    Thread A (remote SSH environment) Thread B (local environment)
    Latest run #165 completed 15:42:31Z #22 completed 16:04:51Z
    Provider thread status idle idle
    pendingBackgroundTasks [{ kind: "command", description: "Wait for the … training or a failure" }], no awaited [{ kind: "command", description: "Wait for next queue run to finish" }], no awaited
    Waiter process alive? yes, child of the agent CLI, running 28 min when checked yes, child of the agent CLI, running 9 min when checked
    Last turn armed the wait with run_in_background, then ended the turn ("needs roughly 7 more hours") armed /tmp/waitnext.sh, then ended the turn
    Previous runs #162–#165 each started by a <task-notification> from the previous wait same wake-up loop (#18–#22)
    snoozedAt / snoozedUntil / settledAt / settledOverride all null all null
    Archived / deleted no / no no / no
    Open runtime requests 0 0
    lastVisitedAt equal to the run's completedAt equal to the run's completedAt
    t3_background_wait called? no no

    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)

    1. packages/shared/src/orchestrationV2PendingBackgroundWork.ts:89-107 checks backgroundWorkHoldsCompletion. command and monitor hold the thread only when awaited === true, because they are "as often a dev server or its watchdog as a wait".
    2. packages/client-runtime/src/state/models.ts:216-227 sets parkAtIdle = false, so runtime.status = thread.status = "completed".
    3. apps/web/src/components/Sidebar.logic.ts:871-896: resolveSidebarThreadStatus returns "ready". The Waiting pill at Sidebar.logic.ts:1106-1108 is also gated on backgroundWorkHoldsCompletion, so it doesn't show either.
    4. SidebarThreadRow.tsx:699-745 sets topStatus = null. That's because the status is ready, not woke, and lastVisitedAt === completedAt means not unread, so there isn't even a Done badge. The row sits in the main list, not the Working shelf, with no label.
    5. The thread view shows the task anyway. useThreadLifecycle.tsx:372 passes the roster to backgroundWorkNowItem, and presentPendingBackgroundWork (threadExecution.ts:314) titles it Running: <description>. So the thread view says running while the sidebar row shows nothing.
    6. The only way such a task holds the thread is for the agent to call t3_background_wait. The Claude system prompt asks for it (ClaudeAdapterV2.ts:1402), and the adapter then marks the task awaited (ClaudeAdapterV2.ts:2784-2790). In both threads the agent skipped that call on every wake-up cycle.

    Answers to your questions

    1. Header / label: I don't have a screenshot. From the state above, the row resolves to the main list (not Working, Snoozed or Settled) with no label at all, not even Waiting or Done.
    2. Snoozed or settled earlier: no. All four snooze/settle fields are null on both threads, and neither was archived.
    3. Version / sleep / connection: desktop 0.1.3, and the remote server is also 0.1.3. Thread B ran entirely on the local server. Thread A's tunnel reconnected several times today, but the server-side state is the cause, not delivery.
    4. Reload: I didn't test a reload. I'd expect no change, because the server state itself is completed with 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:

    • Make the sidebar match the thread view. When the roster has non-holding tasks, show a muted Background / Running in background label on the row instead of nothing. The thread view already does this. The roster is already in the shell, so this is client-only.
    • Hold for wait-shaped commands. For Claude, any run_in_background Bash 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 (showing Waiting) would match what users expect. Dev servers would still need an opt-out, which is the trade-off the current design avoids.
    • Make the marker harder to skip. For example, if a turn ends with an unmarked running command, the adapter could ask the agent whether it's waiting on it, or the server could infer awaited when 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)".

  3. juliusmarminge commented on Oct 27, 2026

    @juliusmarminge
    Member

    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_background command, and the sidebar row shows no Working or Waiting status (or shows Done) until the agent wakes.

    Checked on main @ 36d4d71. backgroundWorkHoldsCompletion returns false for a command or monitor task unless the agent marks it awaited through t3_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 awaited when the next run starts from that task's notification) are useful evidence there.

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