Skip to content

fix(server): a delegated task waiting for an approval notifies its idle parent - #35433

Open
juliusmarminge wants to merge 2 commits into
mainfrom
factory/issue-22925-a20928
Open

juliusmarminge wants to merge 2 commits into
mainfrom
factory/issue-22925-a20928

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Cause: DelegatedTaskQuestionReactor only handled runtime requests of kind user_input, in its live path (runtime-request.updated), its run-end catch-up path and its subagent-settled cancel path. Separately, Orchestrator.isStaleQuestionWake only kept a queued delegated-task notice alive while a pending user_input request matched it, so even a reactor fix produced 0 notices. Fix: the reactor now also handles provider approval kinds (the ProviderRequestKind set, the same set OrchestratorMcpService.pendingTaskApproval treats as approvals). Only pending live approvals raise a notice, matching pendingTaskApproval and the catch-up path. An approval that cannot be answered live (for example the message-capability command request in the existing question test) is still skipped. For approvals, wakeParent queues an advisory notice that names the task, request kind, requestId and child threadId. It says the parent agent cannot approve it and should tell the user or task_cancel and relaunch the task. Its summary reads 'Delegated task "…" needs approval'. Approval notices reuse the message:delegated-task-question:: id, so the existing cancelQueuedWakes logic cancels them once the approval resolves or the task settles. isStaleQuestionWake now matches by request id and pending status, not by kind. No new regex or parser.

Fixes #22925

Reproduction on main

On origin/main f03db4f: started T3CODE_DEV_SEED=0 vp run dev (fresh database) and added a scratch git project. Ran node apps/server/src/bin.ts thread start --provider claudeAgent --model claude-haiku-5-5 with a prompt telling the parent to call delegate_task once (mode async, same provider instance, runtimeMode approval-required) for a child that runs the Bash command date > stamp.txt, then end its turn without polling. Observed in the dev database (read-only) and the web UI: the parent's only run completed at 17:20:02 after replying with the childThreadId. The subagent was app_owned, running, completionWake=always. The child raised a command runtime request at 17:20:03 that stayed pending. At 17:26:43 the parent still had only its two original messages and one run: no notification message and no queued or new run. The parent's UI showed the 'Subagent needs approval' banner with Approve/Decline while the parent agent stayed idle. A unit test added to Orchestrator.control-reads.test.ts (idle parent, async child with a pending live command request, expect one delegated_task notice) fails on main with 0 notices, and passes when the request kind is switched to user_input.

Evidence

Run the parent with one async MCP delegation, let its turn finish, then raise the child's command approval for date > stamp.txt. Leave approval unanswered. Real web client: Chromium, 1280×800, light theme, fresh synthetic data on both commits.

Before After
f03db4fbef d9a1ed3a06
Before: parent stays idle with approval pending After: parent receives an update and replies
Before recording After recording

Scenario driver. The approval remains unanswered in both captures. The base parent has one run and no approval notice; the fixed parent receives one notice and starts a second turn.

AI-assisted; verified with the tests and tools listed above.

Not shown:

  • Live-provider behavior and approval kinds other than command were not exercised.

Tests

Reproduced first: vp test run src/orchestration-v2/Orchestrator.control-reads.test.ts -t approval in apps/server failed on the repro branch (expected +0 to equal 1). After the fix: vp test run src/orchestration-v2/Orchestrator.control-reads.test.ts src/orchestration-v2/SteeringCompletion.integration.test.ts: 88/88 passed. vp run check:changed: format, lint, knip, graphql, typecheck (3 packages) and tests (4 files, 32 tests) all passed.

Not in this change

  • The reactor and its message/command ids are still named 'question' (DelegatedTaskQuestionReactor, delegated-task-question:*). They were not renamed, to keep ids stable and the diff minimal.
  • Orchestrator's task-completion cleanup (dismissing pending questions via thread.user-input.dismiss) only covers user_input requests. Pending approvals on a finished task are left alone, as before.
  • No new eval case was added for the new advisory notice text under evals/.

AI-assisted; verified with the tests and tools listed above.

Fails on main: DelegatedTaskQuestionReactor routes only user_input requests,
so a child's command approval never notifies the parent agent (#22925).

AI-assisted; verified with the tests and tools listed above.
…le parent

DelegatedTaskQuestionReactor routed only user_input requests, so an async
delegated child stuck on a command or other approval never told its parent
agent, and the Orchestrator also treated any such queued notice as stale.
Live approval requests now queue an advisory notice that names the child
thread and request and says the agent cannot approve it; the notice is
cancelled once the approval resolves or the task settles, as for questions.

Fixes #22925

AI-assisted; verified with the tests and tools listed above.
@github-actions github-actions Bot added vouch:trusted Author is trusted by repo permissions or the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Nov 12, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at d9a1ed3

Macroscope's review found this PR approvable — Delegated tasks waiting for live approval now notify their parent, with obsolete queued notices removed. The change preserves user-controlled approval and existing task lifecycle guards.

You can add or adjust custom eligibility rules. Learn more.

@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.5 KiB 21.5 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 1 1 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 1 1 0 (0.0%) 8 ✅

Other scenarios

All 64 scenario measurements match main.

All 64 scenario measurements
Provider Scenario Metric Main baseline This PR Impact PR ceiling
Codex short turn resume WebSocket decoded 10.5 KiB 10.5 KiB 0 B (0.0%) 12.7 KiB ✅
Codex short turn resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex long turn running, cold open HTTP decoded 281.7 KiB 281.7 KiB 0 B (0.0%) 302.7 KiB ✅
Codex long turn running, cold open rows 75 75 0 (0.0%) 75 ✅
Codex long turn running, history page HTTP decoded 92.4 KiB 92.4 KiB 0 B (0.0%) 107.4 KiB ✅
Codex long turn running, history page rows 75 75 0 (0.0%) 75 ✅
Codex long turn finished, cold open HTTP decoded 381.5 KiB 381.5 KiB 0 B (0.0%) 410.2 KiB ✅
Codex long turn finished, cold open rows 151 151 0 (0.0%) 151 ✅
Codex long turn finished, socket cold open WebSocket decoded 381.6 KiB 381.6 KiB 0 B (0.0%) 410.2 KiB ✅
Codex long turn finished, socket cold open WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex long turn finished, socket cold open rows 151 151 0 (0.0%) 151 ✅
Codex long turn finished, work page HTTP decoded 186.2 KiB 186.2 KiB 0 B (0.0%) 200.2 KiB ✅
Codex long turn finished, work page rows 150 150 0 (0.0%) 150 ✅
Codex idle thread resume WebSocket decoded 65 B 65 B 0 B (0.0%) 200 B ✅
Codex idle thread resume WebSocket messages 1 1 0 (0.0%) 1 ✅
Codex fleet shell snapshot HTTP decoded 41.6 KiB 41.6 KiB 0 B (0.0%) 53.7 KiB ✅
Codex fleet shell snapshot rows 27 27 0 (0.0%) 27 ✅
Codex fleet shell subscribe WebSocket decoded 65 B 65 B 0 B (0.0%) 200 B ✅
Codex fleet shell subscribe WebSocket messages 1 1 0 (0.0%) 1 ✅
Codex fleet shell resume check HTTP decoded 18 B 18 B 0 B (0.0%) 100 B ✅
Codex fleet shell resume check rows 0 0 0 0 ✅
Codex stale shell reload HTTP decoded 41.6 KiB 41.6 KiB 0 B (0.0%) 53.7 KiB ✅
Codex stale shell reload rows 27 27 0 (0.0%) 27 ✅
Codex stale shell resume WebSocket decoded 140 B 140 B 0 B (0.0%) 400 B ✅
Codex stale shell resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex stale todo list cold open HTTP decoded 72.4 KiB 72.4 KiB 0 B (0.0%) 83.0 KiB ✅
Codex stale todo list cold open rows 75 75 0 (0.0%) 75 ✅
Codex stale todo list cold open nodes 2 2 0 (0.0%) 4 ✅
Codex stale todo list cold open subagents 2 2 0 (0.0%) 4 ✅
Codex stale todo list cold open plans 1 1 0 (0.0%) 2 ✅
Codex stale todo list cold open providerTurns 1 1 0 (0.0%) 2 ✅
Codex stale todo list cold open checkpoints 1 1 0 (0.0%) 2 ✅
Claude short turn resume WebSocket decoded 10.6 KiB 10.6 KiB 0 B (0.0%) 12.7 KiB ✅
Claude short turn resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude long turn running, cold open HTTP decoded 283.3 KiB 283.3 KiB 0 B (0.0%) 302.7 KiB ✅
Claude long turn running, cold open rows 75 75 0 (0.0%) 75 ✅
Claude long turn running, history page HTTP decoded 93.4 KiB 93.4 KiB 0 B (0.0%) 107.4 KiB ✅
Claude long turn running, history page rows 75 75 0 (0.0%) 75 ✅
Claude long turn finished, cold open HTTP decoded 384.2 KiB 384.2 KiB 0 B (0.0%) 410.2 KiB ✅
Claude long turn finished, cold open rows 151 151 0 (0.0%) 151 ✅
Claude long turn finished, socket cold open WebSocket decoded 384.3 KiB 384.3 KiB 0 B (0.0%) 410.2 KiB ✅
Claude long turn finished, socket cold open WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude long turn finished, socket cold open rows 151 151 0 (0.0%) 151 ✅
Claude long turn finished, work page HTTP decoded 188.1 KiB 188.1 KiB 0 B (0.0%) 200.2 KiB ✅
Claude long turn finished, work page rows 150 150 0 (0.0%) 150 ✅
Claude idle thread resume WebSocket decoded 66 B 66 B 0 B (0.0%) 200 B ✅
Claude idle thread resume WebSocket messages 1 1 0 (0.0%) 1 ✅
Claude fleet shell snapshot HTTP decoded 42.2 KiB 42.2 KiB 0 B (0.0%) 53.7 KiB ✅
Claude fleet shell snapshot rows 27 27 0 (0.0%) 27 ✅
Claude fleet shell subscribe WebSocket decoded 66 B 66 B 0 B (0.0%) 200 B ✅
Claude fleet shell subscribe WebSocket messages 1 1 0 (0.0%) 1 ✅
Claude fleet shell resume check HTTP decoded 18 B 18 B 0 B (0.0%) 100 B ✅
Claude fleet shell resume check rows 0 0 0 0 ✅
Claude stale shell reload HTTP decoded 42.2 KiB 42.2 KiB 0 B (0.0%) 53.7 KiB ✅
Claude stale shell reload rows 27 27 0 (0.0%) 27 ✅
Claude stale shell resume WebSocket decoded 140 B 140 B 0 B (0.0%) 400 B ✅
Claude stale shell resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude stale todo list cold open HTTP decoded 73.4 KiB 73.4 KiB 0 B (0.0%) 83.0 KiB ✅
Claude stale todo list cold open rows 75 75 0 (0.0%) 75 ✅
Claude stale todo list cold open nodes 2 2 0 (0.0%) 4 ✅
Claude stale todo list cold open subagents 2 2 0 (0.0%) 4 ✅
Claude stale todo list cold open plans 1 1 0 (0.0%) 2 ✅
Claude stale todo list cold open providerTurns 1 1 0 (0.0%) 2 ✅
Claude stale todo list cold open checkpoints 1 1 0 (0.0%) 2 ✅

Baseline: 0cd6a73 · PR result: d9a1ed3 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@coderabbitai

coderabbitai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The delegated-task reactor now notifies parent runs about eligible child approval requests. The orchestrator also treats wakes matching pending requests of any kind as current.

Changes: Delegated approval notifications

Layer / File(s) Summary
Approval request notifications and cleanup
apps/server/src/orchestration-v2/DelegatedTaskQuestionReactor.ts
The reactor recognizes approval requests alongside user_input requests. It sends approval-specific notices and includes approvals in catch-up handling and settled-task cleanup.
Pending approval wake handling
apps/server/src/orchestration-v2/Orchestrator.ts, apps/server/src/orchestration-v2/Orchestrator.control-reads.test.ts
A matching pending request of any kind keeps a wake current. The test checks that a pending child approval produces one parent notification with the request ID, delegated task, and child thread.

Estimated code review effort: 2 (Simple) | ~15 minutes

Change: Bug fix · Severity of issue fixed: High

Sequence Diagram(s)

sequenceDiagram
  participant ChildRuntime
  participant DelegatedTaskQuestionReactor
  participant ParentRun
  ChildRuntime->>DelegatedTaskQuestionReactor: Submit pending live approval request
  DelegatedTaskQuestionReactor->>ParentRun: Send approval notice with child-thread link
Loading

Possibly related issues

  • pingdotgg/t3code#21935: This issue concerns approval requests that remain pending after a delegated task is interrupted or cancelled; this change includes approval requests in settled-task cleanup.

Possibly related PRs

  • pingdotgg/t3code#20928: This PR also routes delegated-child approval requests to the parent and adds approval-specific messages.
  • pingdotgg/t3code#25680: This PR changes delegated-task wake cleanup and stale-wake checks, which this change extends to approval requests.

Suggested labels: bugfix

Suggested reviewers: flamboh, t3dotgg

Merge Risk: Minimal · up to d9a1e

Delegated child approval requests now notify an idle parent, and no concrete merge-blocking issue remains in the reviewed changes.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the main change: a delegated task that waits for an approval now notifies its idle parent. It uses the conventional commit form, and it matches the raw summary, the PR objectives, and …
Linked Issues check Passed Issue #22925 requires that a parent receive a notice when an async delegated child waits on an approval after the parent's turn ends. The notice must link the child thread, and the parent must be able…
Out of Scope Changes check Passed All changes in the whole-PR diff (merge base f03db4f to head d9a1ed3) serve the approval-notice objective. DelegatedTaskQuestionReactor.ts adds approval handling in its three paths: live, catch-up a…
Description check Passed The description covers the required template sections in substance. Problem: the reactor handled only user_input requests, and isStaleQuestionWake kept notices alive only for pending user_input reques…
Full details: Title check

Explanation

The title names the main change: a delegated task that waits for an approval now notifies its idle parent. It uses the conventional commit form, and it matches the raw summary, the PR objectives, and linked issue #22925. The title is 78 characters, which is within a normal length for a conventional commit subject.

Full details: Linked Issues check

Explanation

Issue #22925 requires that a parent receive a notice when an async delegated child waits on an approval after the parent's turn ends. The notice must link the child thread, and the parent must be able to tell the user or stop and relaunch the child. The diff covers this in apps/server/src/orchestration-v2/DelegatedTaskQuestionReactor.ts. notifiesParent() now admits ProviderRequestKind approval kinds as well as user_input. The live path (runtime-request.updated) returns early only for non-live pending approvals. For approvals, wakeParent builds advisory text that names the task, request kind, requestId and childThreadId. The text states that the parent cannot approve, and it directs the parent to tell the user or use task_cancel and relaunch. The summary reads 'Delegated task "..." needs approval'. The catch-up and settled-cancel paths also use notifiesParent(), so pending approvals are handled there too. In apps/server/src/orchestration-v2/Orchestrator.ts, isStaleQuestionWake no longer requires kind === user_input. It matches on pending status and the message id, so a queued approval notice stays current while its request is pending. The PR states that a regression test in Orchestrator.control-reads.test.ts reproduces the 0-notice failure on main and passes after the fix. I did not run the tests. The result rests on the reviewed diff and the PR's reported test results. The PR's real-app evidence section is unfinished, but that does not affect the coding requirement. Issue #21935 (stale approvals after interrupt) is context only and is not required here.

Full details: Out of Scope Changes check

Explanation

All changes in the whole-PR diff (merge base f03db4f to head d9a1ed3) serve the approval-notice objective. DelegatedTaskQuestionReactor.ts adds approval handling in its three paths: live, catch-up and settled cancel. It also adds the advisory text and the approval summary. Orchestrator.ts removes the user_input-only restriction in isStaleQuestionWake, which is needed so approval notices remain valid. The regression test in Orchestrator.control-reads.test.ts supports the change. The changes to comments and the summary are small and tied to the same behavior. The PR's 'Not in this change' items (renaming question ids, finished-task dismissal of approvals, and the eval case) are explicit deferrals. They are not unrelated changes. No change was found that is unconnected to issue #22925.

Full details: Description check

Explanation

The description covers the required template sections in substance. Problem: the reactor handled only user_input requests, and isStaleQuestionWake kept notices alive only for pending user_input requests, so approval waits produced zero parent notices. Change: the reactor now handles provider approval kinds, queues an advisory notice for pending live approvals, and isStaleQuestionWake matches by request id and pending status. Scope: it links issue #22925 as the bug report. Verification: it gives a reproduction on main, a failing-then-passing focused test, the exact test commands with results, and before/after screenshots and recordings. It also states what was not shown, namely live-provider behavior and non-command approval kinds. The title is a conventional commit title in the expected form, although the template's example is a plain-language title. Minor gaps: the template asks for a conventional commit title, and the description does not state the title; the PR title in the objectives reads as conventional. The 'Not in this change' section is extra but useful. The model and harness line for the AI-assisted work is present as 'AI-assisted' but does not name the model and harness, which the template requests. The description is complete enough to pass.

Finishing Touches

Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 0cd6a73 and d9a1ed3.

Files selected for processing: 3

  • apps/server/src/orchestration-v2/DelegatedTaskQuestionReactor.ts
  • apps/server/src/orchestration-v2/Orchestrator.control-reads.test.ts
  • apps/server/src/orchestration-v2/Orchestrator.ts
Files with no reviewable changes: 1

  • apps/server/src/orchestration-v2/Orchestrator.ts

Limit details: You’ve used all 10 included reviews currently available.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:trusted Author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Parent agent is not notified when a delegated child waits for an approval

1 participant