Skip to content

Tuval's running-subagent list is empty for every background Agent spawn #9506

Description

@usirin

Tuval's running-subagent list draws nothing while a background Agent worker runs. The slot is opened at the tool_use, then finished one frame later by the async-launch tool_result — so runningSubagents, which keeps only status === "running", has nothing left to show.

Where it breaks

apps/tuval/src/claude/history/map.ts — the tool-result arm. After foldSlots({...mapping, toolCalls}, settled, framedParentId, 0) a loop walks every settled row and flips its slot to finished, under the comment "A settling call is the one thing that ends its worker's slot". That premise held when every Agent call was foreground. A background spawn answers its own tool_result within a second (Async agent launched successfully. (This tool result is internal metadata …)), so the loop ends a worker that has not started work.

apps/tuval/src/shell/chat/subagents.ts — runningSubagents filters on status === "running", so the empty list is the downstream symptom, not a second bug.

apps/tuval/src/claude/history/map.ts — taskNoticeEvents is where the real end arrives: the task_notification frame for that tool_use_id is raised only when the worker settles, and taskOutcomeOf already reads its completed | failed | stopped status.

Triage note (read at origin/main, not the filer's checkout): the gap is live, and the original body overstates one thing. taskNoticeEvents looks the slot up by callId for the name only — it emits a system item and returns mapping unchanged. It does not resolve the slot. So the fix is two halves, not one: stop finishing on the async-launch result, and make the notice arm actually flip the slot.

Shape of the fix (non-binding)

Read the async-launch answer off the settled Agent row and skip it in the finish loop; foreground spawns keep today's behaviour, which the existing subagent-turn.json / two-subagent-turn.json fixtures cover. Then have taskNoticeEvents return a subagents map with that slot finished, plus its ended event, so the slot still cannot survive the turn (the #8401 invariant: a slot left running is a state no checkpoint can be taken on).

The kernelSpawnOf loop below the finish loop is the existing precedent for a spawn whose slot must not be ended by its own settling call — worth reading before changing the ordering.

Pointers

Why now

Background is the harness default for Agent, so every fabrika driver spawn — builder, reviewer, shipper, operator — lands in this path. The list exists to keep the live-vs-finished picture honest, and today it is empty exactly when a lane is running.

Why it is standalone

Searched the board on this surface. #8795 is the mirror-image bug (a kernel-child slot that never leaves running) and #9504 is child-process lifetime, not slot state; every other subagent-list ticket (#8384, #8401, #8405, #8475, #8664) is closed. Nothing open owns this surface, so it is minted on its own.

Acceptance criteria

  • A fixture of a background spawn — the Agent tool_use, its async-launch tool_result, and the later task_notification for the same tool_use_id — lives under apps/tuval/src/claude/history/fixtures/ with a row in PROVENANCE.md.
  • Mapping that fixture leaves the slot running after the tool_result frame and finished only after the task_notification frame, asserted in a unit test.
  • runningSubagents returns that worker's row for every frame between the two, asserted in a unit test.
  • A foreground Agent spawn still finishes on its own tool_result, asserted against the existing subagent-turn.json and two-subagent-turn.json fixtures.
  • taskNoticeEvents emits the slot's ended event alongside its notice item, so no slot outlives the turn (TuvalAiAgent port + core: a per-subagent slot on the agent state, coalesced into the checkpoint #8401).
  • A task_notification carrying failed or stopped ends the slot too, not just completed.
  • The desk shows the live worker for the whole run of a real background Agent spawn. [evidence: hand-verification section on the PR, naming the spawn and what the list showed]

Original report (verbatim)

Summary

The running-subagent list at the top of the Tuval agent window shows nothing while a background Agent worker runs. The spawning call's tool result is what ends a slot, and a background spawn answers that result within a second of launching, so the slot is marked finished while the worker keeps going for minutes.

What I was doing

Driving PR #8063 to merge from the tuval-fe desk session. I spawned a fabrika:reviewer subagent (opus, isolation: worktree, background by default). The harness's own agent list showed it running; the desk's subagent list was empty. The founder asked why.

What I observed

In apps/tuval/src/claude/history/map.ts, the tool-result arm runs foldSlots and then walks the settled calls: any slot whose spawning call just settled is set to status: "finished" (the loop right after foldSlots({...mapping, toolCalls}, settled, framedParentId, 0), with the comment "A settling call is the one thing that ends its worker's slot").

A background Agent call returns its tool_result immediately. This session's main transcript carries, for the spawning tool_use_id, the text Async agent launched successfully. (This tool result is internal metadata …) as the result, seconds after the call. So the slot opened by spawnTypeOf at the tool_use was finished on the next frame. runningSubagents filters to status === "running", so the list draws nothing.

The worker is real and readable: its sidechain lives under the CLI project store at <session>/subagents/agent-<id>.jsonl with a meta.json naming agentType, spawnedWithWorktree and the spawning toolUseId. The task_notification frame for that tool_use_id arrives only when the worker actually ends; taskNoticeEvents already receives it and already resolves the slot by callId, but by then the slot has been finished for the whole run.

Feature flag is not the cause: featuresDefault.subagentList is true.

Why it matters

Background is now the harness default for Agent, and every fabrika driver spawn (reviewer, shipper, builder, operator) goes that way. The list the founder relies on to see what is live is empty exactly when it matters, and a worker's nested rows have no live slot to fold into. The founder's standing ask is to keep the live-vs-finished picture accurate because a wrong list costs him attention.

Pointers

Suggested next step (non-binding)

Do not finish a slot on an Agent tool result that is the async-launch answer; end it on the task_notification frame for that tool_use_id instead, which is the real end of a background worker. Foreground spawns keep today's behaviour. Add a fixture from this session's frames (the tool_use, the async-launch tool_result, and the later task_notification).


Filed by an agent · session 2af7c8e9-8304-4933-9a86-72bfc3293915 · 2026-09-20T19:29:05Z

Activity

  1. usirin commented on Sep 20, 2026

    @usirin
    MemberAuthor
    No description provided.
  2. added
    p1Medium priority
    ready-for:agentAn execution engine may pick this up.
    status:triagedTriage signed off; ready for write-code to pick
    type:bugBehavior diverges from intent
    and removed on Sep 20, 2026
  3. usirin commented on Sep 20, 2026

    @usirin
    MemberAuthor

    PR #9524.

    Look first at apps/tuval/src/claude/history/map.ts: two small arms. userEvents skips the finish
    loop when the frame's tool_use_result says the Agent call answered a launch; taskNoticeEvents
    now finishes the slot and emits its event.

    The read is frame-level, not block-level — SDKUserMessage.tool_use_result is one structured output
    per frame and names no call of its own. Every capture in the corpus answers exactly one call per
    user frame, and a new case in boundary.unit.test.ts holds it to that, so the read is exact for
    every shape captured. A frame settling two Agent calls at once, one foreground and one background,
    is the shape it could not tell apart; none is captured and the harness backgrounds by default.

    The fixture is an excerpt from a real background spawn, not a query() capture — PROVENANCE.md's
    new section says what is verbatim and what was re-keyed, including why the notification's envelope
    comes off two-subagent-turn.json rather than off the type declaration.

    Criterion 7's evidence is in the PR body under "Hand verification", and the Deviations entry says
    what it substituted: no browser in this lane, so the rendered check runs through the shipped
    ChatWindow in jsdom over the real spawn's own frames.

  4. usirin commented on Sep 20, 2026

    @usirin
    MemberAuthor

    Lane transcript — lane 9506, terminal complete

    The merge queue landed PR #9524 at 2026-09-20 15:02 PDT (22:02:26Z), merge commit eb977e996f6116b58e73fe7b65adedb8e27ef18f, which closed this issue. This pass re-read the queue with ship reconcile 9524 --polls 1 (answer: landed) and recorded ISSUE.DONE (token LANDED). The fold now reads complete.

    lane print
    {
      "phases": [
        {
          "name": "pipeline",
          "tasks": [
            "issue"
          ]
        }
      ],
      "terminals": {
        "complete": "complete",
        "tripped": "tripped"
      },
      "tasks": {
        "issue": {
          "initial": "queued",
          "maxRetries": 3,
          "maxWaits": 3,
          "maxLaps": 16,
          "states": {
            "queued": [
              "WIP",
              "BLOCKED",
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "build": [
              "DONE",
              "BLOCKED",
              "LAP",
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "build:ui": [
              "DONE",
              "BLOCKED",
              "LAP",
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "review": [
              "PASS",
              "BLOCKED",
              "LAP",
              "FAIL",
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "review:ui": [
              "PASS",
              "BLOCKED",
              "LAP",
              "FAIL",
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "ship": [
              "LAP",
              "DONE",
              "WIP",
              "BLOCKED",
              "FAIL",
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "ship:queued": [
              "LAP",
              "DONE",
              "BLOCKED",
              "WIP",
              "FAIL",
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "blocked": [
              "UNBLOCKED",
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "human:cp-approval": [
              "UNBLOCKED",
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "human:queue-stall": [
              "UNBLOCKED",
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "human:machinery-stall": [
              "UNBLOCKED",
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "human:budget-spent": [
              "UNBLOCKED",
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "shipped": [
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "diagnosed": [
              "CLEARED",
              "CANCELLED",
              "LANDED"
            ],
            "board:cancelled": [
              "CLEARED"
            ],
            "board:landed": [
              "CLEARED"
            ]
          }
        }
      }
    }
    lane history
    [
      {
        "task": "issue",
        "event": "ISSUE.WIP",
        "at": "2026-09-20T20:03:46.941Z"
      },
      {
        "task": "issue",
        "event": "ISSUE.DONE",
        "at": "2026-09-20T20:24:00.861Z",
        "pr": "https://github.com/kamp-us/phoenix/pull/9524"
      },
      {
        "task": "issue",
        "event": "ISSUE.FAIL",
        "at": "2026-09-20T20:36:24.494Z",
        "pr": "https://github.com/kamp-us/phoenix/pull/9524",
        "classes": [
          "code",
          "doc",
          "ui"
        ]
      },
      {
        "task": "issue",
        "event": "ISSUE.DONE",
        "at": "2026-09-20T20:44:56.158Z",
        "pr": "https://github.com/kamp-us/phoenix/pull/9524"
      },
      {
        "task": "issue",
        "event": "ISSUE.FAIL",
        "at": "2026-09-20T20:59:42.666Z",
        "pr": "https://github.com/kamp-us/phoenix/pull/9524",
        "classes": [
          "code",
          "doc",
          "ui"
        ]
      },
      {
        "task": "issue",
        "event": "ISSUE.DONE",
        "at": "2026-09-20T21:10:21.870Z",
        "pr": "https://github.com/kamp-us/phoenix/pull/9524"
      },
      {
        "task": "issue",
        "event": "ISSUE.PASS",
        "at": "2026-09-20T21:32:38.068Z",
        "pr": "https://github.com/kamp-us/phoenix/pull/9524",
        "classes": [
          "code",
          "doc",
          "ui"
        ],
        "deferred": [
          "review-ui"
        ]
      },
      {
        "task": "issue",
        "event": "ISSUE.PASS",
        "at": "2026-09-20T21:38:16.756Z",
        "pr": "https://github.com/kamp-us/phoenix/pull/9524",
        "routed": [
          "review-ui"
        ]
      },
      {
        "task": "issue",
        "event": "ISSUE.WIP",
        "at": "2026-09-20T21:48:56.762Z",
        "pr": "https://github.com/kamp-us/phoenix/pull/9524"
      },
      {
        "task": "issue",
        "event": "ISSUE.WIP",
        "at": "2026-09-20T22:02:14.304Z"
      },
      {
        "task": "issue",
        "event": "ISSUE.DONE",
        "at": "2026-09-20T22:14:29.189Z",
        "pr": "https://github.com/kamp-us/phoenix/pull/9524",
        "partial": false,
        "landed": [
          9524
        ]
      }
    ]
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

    p1Medium priorityready-for:agentAn execution engine may pick this up.status:triagedTriage signed off; ready for write-code to picktype:bugBehavior diverges from intent

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions