Skip to content

[Bug]: Subagent status stays red after a usage limit recovery, and never updates while running #7314

Description

@ChiChuRita

Before submitting

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

Area

apps/server, apps/web, packages/client-runtime

Summary

Subagent status in the Agents panel is unreliable in two ways:

  1. After a usage limit, a subagent goes red and stays red even after it recovers and finishes the work.
  2. While a subagent runs, its status never updates — the current step is set at spawn and not touched again until the run ends.

Both come from data the app already receives and then drops.

Note on existing issues

The session-level half of this is already known: #6513, with PRs #7165, #5077 and #5473 in flight (and #6639 closed). None of them touch subagent status — they all work at the thread/session layer, and none change subagentRuntime.ts or the Agents panel. Part 1 below is the subagent-level gap those fixes leave open. #7128 is the opposite direction (progress wrongly reviving an idle task), so please read part 2 alongside it.


Part 1 — a subagent that recovers from a usage limit stays red

Steps to reproduce

  1. Spawn several subagents on a Claude session.
  2. Let the account hit its 5-hour usage limit mid-run.
  3. Wait for the limit window to reset and the subagents to be re-dispatched.
  4. Watch the Agents panel.

Actual behavior

The subagents are marked failed, and they stay red for the rest of the session — including while they are working again and after they complete.

From my own logs (~/.t3/userdata/logs/provider/events.<threadId>.log), four subagents died on one limit and all four came back:

09:38:56.716  account.rate-limits.updated  {"status":"rejected","rateLimitType":"five_hour","resetsAt":1786969200}
09:38:56.718  task.updated  taskId=aa7ad7db11045589e  status=failed
              error="Agent terminated early due to an API error: You've hit your session limit · resets 2:20pm"
09:49:23.309  task.started  taskId=aa7ad7db11045589e   <- same taskId, working again
10:00:45.545  task.completed  status=completed         <- finished; row still red

Expected behavior

A usage limit is a pause, not a failure. The row should show something like "waiting · resets 2:20pm", and it must go back to normal once the agent resumes.

Why it happens

Three things line up:

  • The rate-limit signal is discarded. ClaudeAdapter.ts:3474 and CodexAdapter.ts:1401 both emit account.rate-limits.updated carrying the window type and resetsAt. Grepping the tree for that string returns only the two emitters and the contract — ProviderRuntimeIngestion.ts has no case arm, so it falls through. The UI can't know a limit happened. (PR fix(server): type the account rate-limit runtime payload #5473 makes the same observation.)

  • It collapses into failed. With no rate-limit check in the LLM path, the result hits the default return "failed" at ClaudeAdapter.ts:956. There's also no better value available: the status union at subagentRuntime.ts:22 has no throttled/waiting-for-quota member.

  • failed is frozen, and re-dispatch reuses the taskId. subagentRuntime.ts:515 blocks a progress tick from reopening a terminal agent:

    } else if (
      (payload.usageSnapshot !== true || !existed) &&
      !isTerminalSubagentStatus(agent.status) &&   // <- blocks recovery
      agent.status !== "idle"
    ) {
      applyStatus(agent, "running", at);
    }

    Only an explicit running/pending status event reopens the row, so the recovered agent inherits the old terminal state under the same id.

Blast radius: one stuck member turns the whole collapsed workflow dot red (AgentsPanel.tsx:482) and the chat CTA dot too (MessagesTimeline.tsx:2171).

For contrast, the codebase already models this properly for the GitHub CLI — packages/contracts/src/vcs.ts:76 has a first-class "rate-limited" kind. The LLM path never got it.


Part 2 — a running subagent gets no status updates

Actual behavior

Every canonical event for one subagent across a 7-minute run:

09:32:26  task.started
09:37:40  thread.token-usage.updated   (x4)
09:39:45  task.updated  status=failed

Status is set at spawn, then nothing for 7m19s. A later run in the same log has a 30m12s gap.

In that same 7-minute window the SDK sent 128 task_progress notifications, each carrying the current step:

{"task_id":"a76927b010c77a27d","description":"Running Inspect worktree layout",
 "last_tool_name":"Bash","usage":{"tool_uses":1,"duration_ms":3419}}

Across all my logs: 1309 progress notifications, 65 status updates. The progress events are projected into token usage only — description and last_tool_name are dropped.

Expected behavior

The panel reflects the agent's most recent real activity. The data needed for this is already arriving many times per minute.

Also contributing

  • Progress rows are collapse-on-write. ProviderRuntimeIngestion.ts:592 writes under a stable id task-progress:{threadId}:{taskId}, so one row exists per task ever. If an event is missed, the last value is pinned with nothing to notice it's old.
  • No liveness timer anywhere, client or server. So a finished agent reads "Working" with a happily ticking elapsed clock (AgentsPanel.tsx:101 updates textContent only and never re-folds).
  • Terminal events aren't synthesized on teardown. stopSessionInternal (ClaudeAdapter.ts:3595) doesn't drain liveTaskIds, so an agent that ends without a task_notification shows running forever.

Part 3 — small related bug: retryable Codex errors paint the red pill

CodexSessionRuntime.ts:1428:

return updateSession(sessionRef, {
  status: willRetry ? "running" : "error",
  ...(errorMessage ? { lastError: errorMessage } : {}),
});

The lastError spread sits outside the willRetry ternary, so a retryable error leaves the session running with lastError set. The sidebar red pill keys purely off lastError being non-null (Sidebar.tsx:337), and ProviderRuntimeIngestion.ts:1592 only clears lastError when status === "ready"turn.started maps to "running", so resuming never clears it.

Claude's equivalent path does the right thing: ClaudeAdapter.ts:3325 turns api_retry into session.state.changed{state:"running"} to keep the session visibly alive.


Suggestions

  1. Consume account.rate-limits.updated into a non-terminal throttled state at the subagent level, not just the thread banner the open PRs add.
  2. Let a live progress tick reopen a failed agent (subagentRuntime.ts:515) so red can clear itself. Please check this against [Bug]: Finished threads stay Working after delayed task progress #7128 — that issue wants progress to not revive an idle task, so the two need a shared rule.
  3. Project task_progress into status, not only token usage. The current step is already on the wire ~18x/minute.
  4. Add a lastActivityAt so a quiet agent looks different from a busy one.

Items 1 and 3 need no new plumbing — only reading events the app already emits.

Impact

Moderate: the Agents panel can't be trusted, so I wait on work that's already done, or kill agents that were fine.

Version or commit

0.0.34-nightly.20260814.1092

Environment

macOS (Darwin 25.5.0) desktop app, Claude and Codex providers.

How I traced this

The nightly ships sourcemaps with full sourcesContent, so all line numbers above refer to real source, not minified offsets. You may want to check whether that's intended for released builds.

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