Skip to content

[Bug]: Delegated Codex child completes, but its subagent node stays Running and the parent is never woken #17512

Description

@feechkablum6

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

Summary

A Claude parent starts a Codex child with delegate_task (async). The child finishes normally: its run and its provider turn are completed and its final answer is visible in the child thread. But the app-owned subagent node for that task stays running forever. The parent's Lineage card shows the child as Running with the timer still counting, the parent is never woken with the result, and the card can't be stopped. The only way out is to tell the parent by hand that the child is done so it reads the child thread.

This has happened 3–4 times for me. It's different from #15888: there the child run itself never reaches completed. Here the child run and turn do reach completed; only the subagent node and the completion delivery are lost.

Environment

  • T3 Code (Nightly) 0.0.46-nightly.20261009.2861, macOS 27.0, local server
  • Parent: Claude (claude-opus-5-5)
  • Child: delegate_task async → codex / gpt-6.1-sol, reasoningEffort: ultra, serviceTier: priority, full-access

Steps to reproduce

No reliable single-run repro. In a long Claude thread that delegates many Codex reviews:

  1. The parent calls delegate_task (async) with a Codex target and ends its turn to wait.
  2. The Codex child works for ~5 min and finishes with a final assistant message.
  3. About 1 in 30 times, the child stays Running in the parent and the parent is never woken.

Expected behavior

When the child run completes, the subagent node becomes completed, task_status returns the result, and the parent gets a completion wake, as for the other children.

Actual behavior

Live case from today (still stuck while writing this). Times in UTC:

What Value
Child thread thread:delegated-task:command%3Amcp%3A0a10d597-…%3Adelegate-task%3Asources-stage3a-code-r1
Child run ordinal:1 completed, requested 13:49:00.312, completed 13:54:01.723
Child provider turn completed, 13:49:01.109 → 13:54:01.000
orchestration_v2_projection_subagents row for this node origin=app_owned, status=running, completed_at=NULL, updated_at=13:49:00.307 (never updated after creation)
Parent 81332a00-…, owning run ordinal:30 (13:40:41 → 13:53:08)
Parent UI at 14:18 card "Running 28m 58s"; parent's last message: "waiting for the two Codex reviews — notifications will arrive on their own"

The server trace for the child shows the final EventSink.write / EventSink.writeIfRunCurrent at 13:54:01 and 13:54:14, all Success, with no error after that. The only failures for this child are two ProjectionStoreThreadNotFoundError at creation (13:49:00), but the same error also appears for children that completed normally (…sources-stage2-3a-code-r2), so it's probably unrelated.

In the same thread, every other delegated child's node got completed_at within milliseconds of a new parent run starting (the completion wake). For example, …sources-stage2-3a-code-r2 completed at 14:18:08.631, and parent run ordinal:38 was requested at 14:18:08.701. For stage3a-code-r1 there is no such pair.

Across my whole statev2.sqlite: 67 app-owned Codex children. 64 have node completed + run completed, 1 is interrupted/interrupted, and 2 have node running while the child run is completed:

  • the case above;
  • …delegate-task%3Akdl-docs-stage2-review-r1 (parent mcp:d69ea091-…, owner run ordinal:46): child run completed 12:29:31.409, node still running.

One thing I noticed: when stage3a-code-r1 was launched, the parent had overlapping runs. The owning run 30 was still open, run 31 was being cancelled (finished 13:49:04.990), and run 32 was running. The second case didn't have that overlap, so I'm not sure it matters.

Nothing in the UI recovers this. Stop doesn't clear the card, and the parent keeps waiting until I message it.

Workaround

Send the parent a message saying the child finished, so it reads the child thread with t3_thread_read.

Activity

  1. juliusmarminge commented on Oct 9, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Thanks for the detailed report and the database counts. I read the code on main (ec80933). I didn't reproduce this, so treat the following as a likely lead rather than a diagnosis.

    How the row should move to completed

    • The orchestration_v2_projection_subagents row is an upsert keyed on subagent_id (the parent's task node), driven by subagent.updated on the parent thread: ProjectionStore.ts#L2030-L2096.
    • For app-owned children, the only live writer of the terminal row is finalizeAppOwnedSubagent, which runs from the terminal run.updated listener on the child thread: Orchestrator.ts#L10630-L10697. It writes the terminal subagent.updated, the result transfer, and the wake together: Orchestrator.ts#L9766-L9871.

    Where it can return without writing anything
    finalizeAppOwnedSubagent has several early returns before that write: Orchestrator.ts#L9586-L9665. Your row (updated_at equal to creation time, no completed_at) fits one of them firing. The most plausible one seems to be L9600-L9601: it only proceeds when delegatedTaskProgress reports result_available. That function reports waiting_for_children if the child thread has any subagent row still active, a child task whose completion delivery is pending/claimed, or a provider thread with pendingBackgroundTasks: subagentProjection.ts#L237-L256. On a completed run, leftover run-owned subagents are not cascaded to a terminal status (the cascade covers only interrupted/failed/cancelled): RunExecutionService.ts#L162-L165.

    If that check is what fired, nothing re-runs finalize later while the server stays up. The listener reacts only to terminal run.updated events, so a nested item that settles a few seconds after the run (your trace shows a write at 13:54:14, after the run completed at 13:54:01) does not trigger it again. The catch-up pass recoverDelegatedTasks runs only at startup: Orchestrator.ts#L10704-L10731, ProjectionStore.ts#L3569-L3593. Any error inside finalize is also only logged as a warning ("Failed to react to terminal V2 run"), with no retry.

    The overlapping parent runs probably don't explain the stuck row. Finalize looks up the parent run by task.runId and writes the terminal status whatever that run's state is. The parent run's state only affects whether a wake is offered: Orchestrator.ts#L9403-L9436.

    Why Stop doesn't clear it: stopping the parent run marks the task's completionDelivery as disposed but copies its status unchanged: Orchestrator.ts#L2115-L2139. The child has no live run left to interrupt, so no new terminal run.updated arrives to re-run finalize.

    Related: #15888 (there the Grok child run itself stays non-terminal), #16355 and #17154 (nested subagents left running, task stuck in waiting_for_children), which may share this root cause if the Codex child had a nested item open. Open PR #15201 changes task_status reads, startup recovery, and pending-background-work derivation. From its diff it doesn't seem to add a live re-check of finalizeAppOwnedSubagent, so it may not cover this case on its own.

    Useful next data, if you still have the stuck case:

    1. What task_status returns for that task, in particular workState.
    2. In the child thread: any orchestration_v2_projection_subagents rows still running, and pendingBackgroundTasks on its provider thread. What wrote the 13:54:14 event?
    3. Any Failed to react to terminal V2 run warning around 13:54:01.
    4. Whether restarting the server settles the node and wakes the parent. The startup recovery pass would run finalize again.
  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 9, 2026
  3. Neonsy commented on Oct 9, 2026

    @Neonsy

    Another case on the same nightly (0.0.46-nightly.20261009.2861), this time with a Claude child. It includes the data asked for above

    Setup

    • A Claude parent runs delegate_task (async) to a Claude child
    • The child starts native background work and ends its turn
    • A background-task notification queues a wake run 25 ms before run 1 is recorded complete
    • The wake run finishes in about 0.15 s with no reply
    • The parent then calls task_cancel

    Expected

    The Lineage row shows the child as finished, and Stop is not offered

    Actual

    • The header reads "Lineage · 4 running" and counts this child
    • Its timer counts from the task's original start (1.3h and rising)
    • Stop subagent does nothing

    Timeline (UTC)

    Time Event
    16:05:05.954 Run 2 (wake) requested
    16:05:05.979 Run 1 completed
    16:05:09.090 Run 2 completed
    16:05:20 First task_cancel returns cancel_requested with no run active. It records run_interrupt_request on the already completed run 2
    16:22 task_status returns the values below
    16:22 Second task_cancel returns completed. t3_thread_interrupt returns no_active_run

    The child thread itself reports completed with no active run

    task_status at 16:22

    status: completed
    workState: result_available
    hasPendingChildRuns: false
    resultContextTransferId: null
    

    How it fits the lead above

    1. At run 1's end, run 2 was already queued, so finalize skipped
    2. At run 2's end, finalize skipped again. The task was still waiting on background work, inferred from step 3
    3. The first task_cancel returned cancel_requested with no active run, which only happens in waiting_for_children
    4. The cancel cleared that work but produced no new terminal run.updated, so finalize never re-ran
    5. The app-owned row stayed running. The Lineage edge falls back to that row when the child has no live run, so the count, the timer and the Stop button all follow it

    Also observed

    • Settling the child thread does not clear the row. It still showed Running 1h 31m about 12 minutes after settling, while the server still reported the thread as completed and settled

    Not tried

    • Whether a server restart's recovery pass settles it
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

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions