fix(server): emit terminal task rows for subagents that survive a stop sweep - #7585

Closed
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks
Closed

fix(server): emit terminal task rows for subagents that survive a stop sweep#7585
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks

Conversation

@spiky02plateau

@spiky02plateauspiky02plateau commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Interrupting a turn can leave a spawned subagent's row in the agents panel showing "running" forever: blue dot, duration counting up against the wall clock for the life of the thread. Observed with a nested agent (spawned from inside another subagent), but any task whose stop confirmation goes missing gets stranded the same way.

The interrupt path stops live tasks best-effort: stopTask per task with a 3s timeout, and only an acknowledged stop got a synthesized terminal task.completed. Every failure mode (refusal, timeout, the id already gone) returned silently, and nothing downstream can recover: completeTurn and stopSessionInternal never sweep liveTaskIds, the client's coordinator cascade only reaches workflow members keyed by parentAgentId (which nested agents never carry), and the client's dead-session sweep only fires when the session disconnects, which a turn interrupt does not cause.

I checked whether the SDK offers a roster to reconcile against, since a comment in the adapter called the background_tasks control request "the reconciliation source". It is not: per the installed SDK's types, Query.backgroundTasks backgrounds in-flight tasks (the Ctrl+B equivalent) and returns a boolean; BackgroundTaskSummary[] only appears on stop-hook inputs. There is no on-demand liveness query.

So the fix makes T3's own bookkeeping honest. A shared settleLiveTasks drains liveTaskIds and emits one synthesized task.completed { status: "stopped" } with the task's linkage per remaining id. The interrupt path's stop loop collapses to a pure best-effort sweep (same 3s/10s bounds), then interrupt(), then settle: once the interrupt lands the turn is dead and T3 has no remaining liveness signal for anything still marked live. stopSessionInternal settles before completeTurn (rows keep their turnId) and before session.exited, where it is unambiguous since the session process is going away. A turn that ends normally still leaves backgrounded tasks live on purpose. The stale comment now says what background_tasks actually does.

Left alone, for scope: the client's uncapped 1s duration interval (any future row missing its terminal event still counts up), and the client cascades that cannot reach nested agents. With the server now settling, those are defense-in-depth concerns.

Verification

In apps/server:

  • vp test run src/provider/Layers/ClaudeAdapter.test.ts: 73 passed (73). Three new tests: a refused stopTask still yields a terminal stopped row after interrupt; a nested task keeps its owning-agent linkage on the settled row; stopSession emits terminal rows before session.exited.
  • tsgo --noEmit: no errors.
  • vp lint on both files: one pre-existing warning elsewhere in the file, nothing new. vp fmt --check: clean.

Change made by Claude Opus via Claude Code.


Note

Medium Risk
Changes interrupt and session teardown paths in the Claude adapter; incorrect settlement could misreport agent state, but scope is bounded to live-task bookkeeping and is covered by new tests.

Overview
Fixes agent panel rows that stay running forever after Stop when the Claude SDK never confirms subagent shutdown (refused stopTask, timeout, or missing task_notification).

settleLiveTasks drains liveTaskIds and emits synthesized task.completed with status: "stopped" and existing task linkage. interruptTurn still best-efforts stopTask (same timeouts), then interrupt(), then always settles remaining live tasks instead of only synthesizing completion when stopTask succeeded. stopSessionInternal settles live tasks before completeTurn and session.exited.

Comments on background_tasks_changed are corrected: the SDK control is not a roster query. Tests cover refused stopTask, nested agentId on settled rows, and shutdown event ordering.

Reviewed by Cursor Bugbot for commit 287aa51. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Emit terminal task rows for subagents that survive a stop sweep in interruptTurn and stopSession

  • Introduces settleLiveTasks in ClaudeAdapter.ts, which drains liveTaskIds and emits a synthesized task.completed event with status 'stopped' for each remaining live task.
  • interruptTurn now calls settleLiveTasks after attempting stopTask and invoking query.interrupt, guaranteeing terminal task rows even when individual stopTask calls reject or time out.
  • stopSessionInternal calls settleLiveTasks before completing the turn, ensuring task rows are emitted with the current turnId before session.exited.
  • Adds tests covering: stopTask rejection, nested task termination with agent linkage preserved, and correct ordering of task events before session.exited.

Macroscope summarized 287aa51.

…p sweep
Interrupting a turn left a spawned subagent's row stuck "running" forever.
The interrupt path stopped each live task, but on any failure mode - stopTask
rejecting, timing out, or the id already being gone - it returned without
emitting a terminal row, so only acknowledged stops ever produced one. Session
stop had the same hole: it emitted session.exited while leaving liveTaskIds
populated. Nothing else closes those rows out, so the panel counted up for the
life of the thread. Seen with a nested subagent, which no client-side cascade
covers either.
Both call sites now share settleLiveTasks, which drains liveTaskIds into
synthesized task.completed { status: "stopped" } rows carrying the usual
linkage. It runs after query.interrupt() resolves and before session.exited -
both points where nothing the session spawned can still be running. The SDK
offers no roster to reconcile against (Query.backgroundTasks backgrounds
tasks, it does not list them), so a task the CLI never confirmed is called
stopped rather than left spinning. The stale comment claiming the typed
background_tasks control request was the reconciliation source is corrected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 658869f1-a1c9-44a4-a078-104f2a465540

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 19, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix modifies runtime event emission behavior in the ClaudeAdapter's task cleanup paths. While the change is well-scoped and well-tested, it affects core session lifecycle logic in a file the author hasn't previously contributed to, warranting human review.

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

MarcL01 added a commit to MarcL01/t3code that referenced this pull request Aug 25, 2026
…p sweep
Interrupting a turn could leave a spawned subagent's row in the agents panel
showing "running" forever. The interrupt path synthesized a terminal
task.completed only when stopTask was acknowledged within its 3s timeout, so a
timed-out stop or an id the SDK had already dropped bailed out silently, and
nothing downstream swept liveTaskIds afterwards.
A shared settleLiveTasks now drains liveTaskIds and emits one synthesized
task.completed with status "stopped" per remaining id. interruptTurn settles
after interrupt(), where the turn is dead and no liveness signal remains;
stopSessionInternal settles before completeTurn so the rows keep their turnId,
and before session.exited.
Ported from pingdotgg#7585. Verified the defect was still present before
porting: the acknowledged-only guard was live in interruptTurn, no
settleLiveTasks existed, and neither completeTurn, handleStreamExit, nor
stopSessionInternal drained liveTaskIds. Merged without conflicts.
Co-authored-by: Tobi <155588579+spiky02plateau@users.noreply.github.com>
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Current main already contains the terminal-task-row behavior for subagents that survive a stop sweep. This branch has no distinct repair left to review.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@spiky02plateau@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(server): emit terminal task rows for subagents that survive a stop sweep - #7585

Closed
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks
Closed

fix(server): emit terminal task rows for subagents that survive a stop sweep#7585
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks

Conversation

@spiky02plateau

@spiky02plateauspiky02plateau commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Interrupting a turn can leave a spawned subagent's row in the agents panel showing "running" forever: blue dot, duration counting up against the wall clock for the life of the thread. Observed with a nested agent (spawned from inside another subagent), but any task whose stop confirmation goes missing gets stranded the same way.

The interrupt path stops live tasks best-effort: stopTask per task with a 3s timeout, and only an acknowledged stop got a synthesized terminal task.completed. Every failure mode (refusal, timeout, the id already gone) returned silently, and nothing downstream can recover: completeTurn and stopSessionInternal never sweep liveTaskIds, the client's coordinator cascade only reaches workflow members keyed by parentAgentId (which nested agents never carry), and the client's dead-session sweep only fires when the session disconnects, which a turn interrupt does not cause.

I checked whether the SDK offers a roster to reconcile against, since a comment in the adapter called the background_tasks control request "the reconciliation source". It is not: per the installed SDK's types, Query.backgroundTasks backgrounds in-flight tasks (the Ctrl+B equivalent) and returns a boolean; BackgroundTaskSummary[] only appears on stop-hook inputs. There is no on-demand liveness query.

So the fix makes T3's own bookkeeping honest. A shared settleLiveTasks drains liveTaskIds and emits one synthesized task.completed { status: "stopped" } with the task's linkage per remaining id. The interrupt path's stop loop collapses to a pure best-effort sweep (same 3s/10s bounds), then interrupt(), then settle: once the interrupt lands the turn is dead and T3 has no remaining liveness signal for anything still marked live. stopSessionInternal settles before completeTurn (rows keep their turnId) and before session.exited, where it is unambiguous since the session process is going away. A turn that ends normally still leaves backgrounded tasks live on purpose. The stale comment now says what background_tasks actually does.

Left alone, for scope: the client's uncapped 1s duration interval (any future row missing its terminal event still counts up), and the client cascades that cannot reach nested agents. With the server now settling, those are defense-in-depth concerns.

Verification

In apps/server:

  • vp test run src/provider/Layers/ClaudeAdapter.test.ts: 73 passed (73). Three new tests: a refused stopTask still yields a terminal stopped row after interrupt; a nested task keeps its owning-agent linkage on the settled row; stopSession emits terminal rows before session.exited.
  • tsgo --noEmit: no errors.
  • vp lint on both files: one pre-existing warning elsewhere in the file, nothing new. vp fmt --check: clean.

Change made by Claude Opus via Claude Code.


Note

Medium Risk
Changes interrupt and session teardown paths in the Claude adapter; incorrect settlement could misreport agent state, but scope is bounded to live-task bookkeeping and is covered by new tests.

Overview
Fixes agent panel rows that stay running forever after Stop when the Claude SDK never confirms subagent shutdown (refused stopTask, timeout, or missing task_notification).

settleLiveTasks drains liveTaskIds and emits synthesized task.completed with status: "stopped" and existing task linkage. interruptTurn still best-efforts stopTask (same timeouts), then interrupt(), then always settles remaining live tasks instead of only synthesizing completion when stopTask succeeded. stopSessionInternal settles live tasks before completeTurn and session.exited.

Comments on background_tasks_changed are corrected: the SDK control is not a roster query. Tests cover refused stopTask, nested agentId on settled rows, and shutdown event ordering.

Reviewed by Cursor Bugbot for commit 287aa51. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Emit terminal task rows for subagents that survive a stop sweep in interruptTurn and stopSession

  • Introduces settleLiveTasks in ClaudeAdapter.ts, which drains liveTaskIds and emits a synthesized task.completed event with status 'stopped' for each remaining live task.
  • interruptTurn now calls settleLiveTasks after attempting stopTask and invoking query.interrupt, guaranteeing terminal task rows even when individual stopTask calls reject or time out.
  • stopSessionInternal calls settleLiveTasks before completing the turn, ensuring task rows are emitted with the current turnId before session.exited.
  • Adds tests covering: stopTask rejection, nested task termination with agent linkage preserved, and correct ordering of task events before session.exited.

Macroscope summarized 287aa51.

…p sweep
Interrupting a turn left a spawned subagent's row stuck "running" forever.
The interrupt path stopped each live task, but on any failure mode - stopTask
rejecting, timing out, or the id already being gone - it returned without
emitting a terminal row, so only acknowledged stops ever produced one. Session
stop had the same hole: it emitted session.exited while leaving liveTaskIds
populated. Nothing else closes those rows out, so the panel counted up for the
life of the thread. Seen with a nested subagent, which no client-side cascade
covers either.
Both call sites now share settleLiveTasks, which drains liveTaskIds into
synthesized task.completed { status: "stopped" } rows carrying the usual
linkage. It runs after query.interrupt() resolves and before session.exited -
both points where nothing the session spawned can still be running. The SDK
offers no roster to reconcile against (Query.backgroundTasks backgrounds
tasks, it does not list them), so a task the CLI never confirmed is called
stopped rather than left spinning. The stale comment claiming the typed
background_tasks control request was the reconciliation source is corrected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 658869f1-a1c9-44a4-a078-104f2a465540

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 19, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix modifies runtime event emission behavior in the ClaudeAdapter's task cleanup paths. While the change is well-scoped and well-tested, it affects core session lifecycle logic in a file the author hasn't previously contributed to, warranting human review.

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

MarcL01 added a commit to MarcL01/t3code that referenced this pull request Aug 25, 2026
…p sweep
Interrupting a turn could leave a spawned subagent's row in the agents panel
showing "running" forever. The interrupt path synthesized a terminal
task.completed only when stopTask was acknowledged within its 3s timeout, so a
timed-out stop or an id the SDK had already dropped bailed out silently, and
nothing downstream swept liveTaskIds afterwards.
A shared settleLiveTasks now drains liveTaskIds and emits one synthesized
task.completed with status "stopped" per remaining id. interruptTurn settles
after interrupt(), where the turn is dead and no liveness signal remains;
stopSessionInternal settles before completeTurn so the rows keep their turnId,
and before session.exited.
Ported from pingdotgg#7585. Verified the defect was still present before
porting: the acknowledged-only guard was live in interruptTurn, no
settleLiveTasks existed, and neither completeTurn, handleStreamExit, nor
stopSessionInternal drained liveTaskIds. Merged without conflicts.
Co-authored-by: Tobi <155588579+spiky02plateau@users.noreply.github.com>
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Current main already contains the terminal-task-row behavior for subagents that survive a stop sweep. This branch has no distinct repair left to review.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@spiky02plateau@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): emit terminal task rows for subagents that survive a stop sweep - #7585

Closed
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks
Closed

fix(server): emit terminal task rows for subagents that survive a stop sweep#7585
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks

Conversation

@spiky02plateau

@spiky02plateauspiky02plateau commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Interrupting a turn can leave a spawned subagent's row in the agents panel showing "running" forever: blue dot, duration counting up against the wall clock for the life of the thread. Observed with a nested agent (spawned from inside another subagent), but any task whose stop confirmation goes missing gets stranded the same way.

The interrupt path stops live tasks best-effort: stopTask per task with a 3s timeout, and only an acknowledged stop got a synthesized terminal task.completed. Every failure mode (refusal, timeout, the id already gone) returned silently, and nothing downstream can recover: completeTurn and stopSessionInternal never sweep liveTaskIds, the client's coordinator cascade only reaches workflow members keyed by parentAgentId (which nested agents never carry), and the client's dead-session sweep only fires when the session disconnects, which a turn interrupt does not cause.

I checked whether the SDK offers a roster to reconcile against, since a comment in the adapter called the background_tasks control request "the reconciliation source". It is not: per the installed SDK's types, Query.backgroundTasks backgrounds in-flight tasks (the Ctrl+B equivalent) and returns a boolean; BackgroundTaskSummary[] only appears on stop-hook inputs. There is no on-demand liveness query.

So the fix makes T3's own bookkeeping honest. A shared settleLiveTasks drains liveTaskIds and emits one synthesized task.completed { status: "stopped" } with the task's linkage per remaining id. The interrupt path's stop loop collapses to a pure best-effort sweep (same 3s/10s bounds), then interrupt(), then settle: once the interrupt lands the turn is dead and T3 has no remaining liveness signal for anything still marked live. stopSessionInternal settles before completeTurn (rows keep their turnId) and before session.exited, where it is unambiguous since the session process is going away. A turn that ends normally still leaves backgrounded tasks live on purpose. The stale comment now says what background_tasks actually does.

Left alone, for scope: the client's uncapped 1s duration interval (any future row missing its terminal event still counts up), and the client cascades that cannot reach nested agents. With the server now settling, those are defense-in-depth concerns.

Verification

In apps/server:

  • vp test run src/provider/Layers/ClaudeAdapter.test.ts: 73 passed (73). Three new tests: a refused stopTask still yields a terminal stopped row after interrupt; a nested task keeps its owning-agent linkage on the settled row; stopSession emits terminal rows before session.exited.
  • tsgo --noEmit: no errors.
  • vp lint on both files: one pre-existing warning elsewhere in the file, nothing new. vp fmt --check: clean.

Change made by Claude Opus via Claude Code.


Note

Medium Risk
Changes interrupt and session teardown paths in the Claude adapter; incorrect settlement could misreport agent state, but scope is bounded to live-task bookkeeping and is covered by new tests.

Overview
Fixes agent panel rows that stay running forever after Stop when the Claude SDK never confirms subagent shutdown (refused stopTask, timeout, or missing task_notification).

settleLiveTasks drains liveTaskIds and emits synthesized task.completed with status: "stopped" and existing task linkage. interruptTurn still best-efforts stopTask (same timeouts), then interrupt(), then always settles remaining live tasks instead of only synthesizing completion when stopTask succeeded. stopSessionInternal settles live tasks before completeTurn and session.exited.

Comments on background_tasks_changed are corrected: the SDK control is not a roster query. Tests cover refused stopTask, nested agentId on settled rows, and shutdown event ordering.

Reviewed by Cursor Bugbot for commit 287aa51. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Emit terminal task rows for subagents that survive a stop sweep in interruptTurn and stopSession

  • Introduces settleLiveTasks in ClaudeAdapter.ts, which drains liveTaskIds and emits a synthesized task.completed event with status 'stopped' for each remaining live task.
  • interruptTurn now calls settleLiveTasks after attempting stopTask and invoking query.interrupt, guaranteeing terminal task rows even when individual stopTask calls reject or time out.
  • stopSessionInternal calls settleLiveTasks before completing the turn, ensuring task rows are emitted with the current turnId before session.exited.
  • Adds tests covering: stopTask rejection, nested task termination with agent linkage preserved, and correct ordering of task events before session.exited.

Macroscope summarized 287aa51.

…p sweep
Interrupting a turn left a spawned subagent's row stuck "running" forever.
The interrupt path stopped each live task, but on any failure mode - stopTask
rejecting, timing out, or the id already being gone - it returned without
emitting a terminal row, so only acknowledged stops ever produced one. Session
stop had the same hole: it emitted session.exited while leaving liveTaskIds
populated. Nothing else closes those rows out, so the panel counted up for the
life of the thread. Seen with a nested subagent, which no client-side cascade
covers either.
Both call sites now share settleLiveTasks, which drains liveTaskIds into
synthesized task.completed { status: "stopped" } rows carrying the usual
linkage. It runs after query.interrupt() resolves and before session.exited -
both points where nothing the session spawned can still be running. The SDK
offers no roster to reconcile against (Query.backgroundTasks backgrounds
tasks, it does not list them), so a task the CLI never confirmed is called
stopped rather than left spinning. The stale comment claiming the typed
background_tasks control request was the reconciliation source is corrected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 658869f1-a1c9-44a4-a078-104f2a465540

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 19, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix modifies runtime event emission behavior in the ClaudeAdapter's task cleanup paths. While the change is well-scoped and well-tested, it affects core session lifecycle logic in a file the author hasn't previously contributed to, warranting human review.

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

MarcL01 added a commit to MarcL01/t3code that referenced this pull request Aug 25, 2026
…p sweep
Interrupting a turn could leave a spawned subagent's row in the agents panel
showing "running" forever. The interrupt path synthesized a terminal
task.completed only when stopTask was acknowledged within its 3s timeout, so a
timed-out stop or an id the SDK had already dropped bailed out silently, and
nothing downstream swept liveTaskIds afterwards.
A shared settleLiveTasks now drains liveTaskIds and emits one synthesized
task.completed with status "stopped" per remaining id. interruptTurn settles
after interrupt(), where the turn is dead and no liveness signal remains;
stopSessionInternal settles before completeTurn so the rows keep their turnId,
and before session.exited.
Ported from pingdotgg#7585. Verified the defect was still present before
porting: the acknowledged-only guard was live in interruptTurn, no
settleLiveTasks existed, and neither completeTurn, handleStreamExit, nor
stopSessionInternal drained liveTaskIds. Merged without conflicts.
Co-authored-by: Tobi <155588579+spiky02plateau@users.noreply.github.com>
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Current main already contains the terminal-task-row behavior for subagents that survive a stop sweep. This branch has no distinct repair left to review.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@spiky02plateau@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): emit terminal task rows for subagents that survive a stop sweep - #7585

Closed
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks
Closed

fix(server): emit terminal task rows for subagents that survive a stop sweep#7585
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks

Conversation

@spiky02plateau

@spiky02plateauspiky02plateau commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Interrupting a turn can leave a spawned subagent's row in the agents panel showing "running" forever: blue dot, duration counting up against the wall clock for the life of the thread. Observed with a nested agent (spawned from inside another subagent), but any task whose stop confirmation goes missing gets stranded the same way.

The interrupt path stops live tasks best-effort: stopTask per task with a 3s timeout, and only an acknowledged stop got a synthesized terminal task.completed. Every failure mode (refusal, timeout, the id already gone) returned silently, and nothing downstream can recover: completeTurn and stopSessionInternal never sweep liveTaskIds, the client's coordinator cascade only reaches workflow members keyed by parentAgentId (which nested agents never carry), and the client's dead-session sweep only fires when the session disconnects, which a turn interrupt does not cause.

I checked whether the SDK offers a roster to reconcile against, since a comment in the adapter called the background_tasks control request "the reconciliation source". It is not: per the installed SDK's types, Query.backgroundTasks backgrounds in-flight tasks (the Ctrl+B equivalent) and returns a boolean; BackgroundTaskSummary[] only appears on stop-hook inputs. There is no on-demand liveness query.

So the fix makes T3's own bookkeeping honest. A shared settleLiveTasks drains liveTaskIds and emits one synthesized task.completed { status: "stopped" } with the task's linkage per remaining id. The interrupt path's stop loop collapses to a pure best-effort sweep (same 3s/10s bounds), then interrupt(), then settle: once the interrupt lands the turn is dead and T3 has no remaining liveness signal for anything still marked live. stopSessionInternal settles before completeTurn (rows keep their turnId) and before session.exited, where it is unambiguous since the session process is going away. A turn that ends normally still leaves backgrounded tasks live on purpose. The stale comment now says what background_tasks actually does.

Left alone, for scope: the client's uncapped 1s duration interval (any future row missing its terminal event still counts up), and the client cascades that cannot reach nested agents. With the server now settling, those are defense-in-depth concerns.

Verification

In apps/server:

  • vp test run src/provider/Layers/ClaudeAdapter.test.ts: 73 passed (73). Three new tests: a refused stopTask still yields a terminal stopped row after interrupt; a nested task keeps its owning-agent linkage on the settled row; stopSession emits terminal rows before session.exited.
  • tsgo --noEmit: no errors.
  • vp lint on both files: one pre-existing warning elsewhere in the file, nothing new. vp fmt --check: clean.

Change made by Claude Opus via Claude Code.


Note

Medium Risk
Changes interrupt and session teardown paths in the Claude adapter; incorrect settlement could misreport agent state, but scope is bounded to live-task bookkeeping and is covered by new tests.

Overview
Fixes agent panel rows that stay running forever after Stop when the Claude SDK never confirms subagent shutdown (refused stopTask, timeout, or missing task_notification).

settleLiveTasks drains liveTaskIds and emits synthesized task.completed with status: "stopped" and existing task linkage. interruptTurn still best-efforts stopTask (same timeouts), then interrupt(), then always settles remaining live tasks instead of only synthesizing completion when stopTask succeeded. stopSessionInternal settles live tasks before completeTurn and session.exited.

Comments on background_tasks_changed are corrected: the SDK control is not a roster query. Tests cover refused stopTask, nested agentId on settled rows, and shutdown event ordering.

Reviewed by Cursor Bugbot for commit 287aa51. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Emit terminal task rows for subagents that survive a stop sweep in interruptTurn and stopSession

  • Introduces settleLiveTasks in ClaudeAdapter.ts, which drains liveTaskIds and emits a synthesized task.completed event with status 'stopped' for each remaining live task.
  • interruptTurn now calls settleLiveTasks after attempting stopTask and invoking query.interrupt, guaranteeing terminal task rows even when individual stopTask calls reject or time out.
  • stopSessionInternal calls settleLiveTasks before completing the turn, ensuring task rows are emitted with the current turnId before session.exited.
  • Adds tests covering: stopTask rejection, nested task termination with agent linkage preserved, and correct ordering of task events before session.exited.

Macroscope summarized 287aa51.

…p sweep
Interrupting a turn left a spawned subagent's row stuck "running" forever.
The interrupt path stopped each live task, but on any failure mode - stopTask
rejecting, timing out, or the id already being gone - it returned without
emitting a terminal row, so only acknowledged stops ever produced one. Session
stop had the same hole: it emitted session.exited while leaving liveTaskIds
populated. Nothing else closes those rows out, so the panel counted up for the
life of the thread. Seen with a nested subagent, which no client-side cascade
covers either.
Both call sites now share settleLiveTasks, which drains liveTaskIds into
synthesized task.completed { status: "stopped" } rows carrying the usual
linkage. It runs after query.interrupt() resolves and before session.exited -
both points where nothing the session spawned can still be running. The SDK
offers no roster to reconcile against (Query.backgroundTasks backgrounds
tasks, it does not list them), so a task the CLI never confirmed is called
stopped rather than left spinning. The stale comment claiming the typed
background_tasks control request was the reconciliation source is corrected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 658869f1-a1c9-44a4-a078-104f2a465540

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 19, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix modifies runtime event emission behavior in the ClaudeAdapter's task cleanup paths. While the change is well-scoped and well-tested, it affects core session lifecycle logic in a file the author hasn't previously contributed to, warranting human review.

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

MarcL01 added a commit to MarcL01/t3code that referenced this pull request Aug 25, 2026
…p sweep
Interrupting a turn could leave a spawned subagent's row in the agents panel
showing "running" forever. The interrupt path synthesized a terminal
task.completed only when stopTask was acknowledged within its 3s timeout, so a
timed-out stop or an id the SDK had already dropped bailed out silently, and
nothing downstream swept liveTaskIds afterwards.
A shared settleLiveTasks now drains liveTaskIds and emits one synthesized
task.completed with status "stopped" per remaining id. interruptTurn settles
after interrupt(), where the turn is dead and no liveness signal remains;
stopSessionInternal settles before completeTurn so the rows keep their turnId,
and before session.exited.
Ported from pingdotgg#7585. Verified the defect was still present before
porting: the acknowledged-only guard was live in interruptTurn, no
settleLiveTasks existed, and neither completeTurn, handleStreamExit, nor
stopSessionInternal drained liveTaskIds. Merged without conflicts.
Co-authored-by: Tobi <155588579+spiky02plateau@users.noreply.github.com>
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Current main already contains the terminal-task-row behavior for subagents that survive a stop sweep. This branch has no distinct repair left to review.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@spiky02plateau@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(server): emit terminal task rows for subagents that survive a stop sweep - #7585

Closed
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks
Closed

fix(server): emit terminal task rows for subagents that survive a stop sweep#7585
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks

Conversation

@spiky02plateau

@spiky02plateauspiky02plateau commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Interrupting a turn can leave a spawned subagent's row in the agents panel showing "running" forever: blue dot, duration counting up against the wall clock for the life of the thread. Observed with a nested agent (spawned from inside another subagent), but any task whose stop confirmation goes missing gets stranded the same way.

The interrupt path stops live tasks best-effort: stopTask per task with a 3s timeout, and only an acknowledged stop got a synthesized terminal task.completed. Every failure mode (refusal, timeout, the id already gone) returned silently, and nothing downstream can recover: completeTurn and stopSessionInternal never sweep liveTaskIds, the client's coordinator cascade only reaches workflow members keyed by parentAgentId (which nested agents never carry), and the client's dead-session sweep only fires when the session disconnects, which a turn interrupt does not cause.

I checked whether the SDK offers a roster to reconcile against, since a comment in the adapter called the background_tasks control request "the reconciliation source". It is not: per the installed SDK's types, Query.backgroundTasks backgrounds in-flight tasks (the Ctrl+B equivalent) and returns a boolean; BackgroundTaskSummary[] only appears on stop-hook inputs. There is no on-demand liveness query.

So the fix makes T3's own bookkeeping honest. A shared settleLiveTasks drains liveTaskIds and emits one synthesized task.completed { status: "stopped" } with the task's linkage per remaining id. The interrupt path's stop loop collapses to a pure best-effort sweep (same 3s/10s bounds), then interrupt(), then settle: once the interrupt lands the turn is dead and T3 has no remaining liveness signal for anything still marked live. stopSessionInternal settles before completeTurn (rows keep their turnId) and before session.exited, where it is unambiguous since the session process is going away. A turn that ends normally still leaves backgrounded tasks live on purpose. The stale comment now says what background_tasks actually does.

Left alone, for scope: the client's uncapped 1s duration interval (any future row missing its terminal event still counts up), and the client cascades that cannot reach nested agents. With the server now settling, those are defense-in-depth concerns.

Verification

In apps/server:

  • vp test run src/provider/Layers/ClaudeAdapter.test.ts: 73 passed (73). Three new tests: a refused stopTask still yields a terminal stopped row after interrupt; a nested task keeps its owning-agent linkage on the settled row; stopSession emits terminal rows before session.exited.
  • tsgo --noEmit: no errors.
  • vp lint on both files: one pre-existing warning elsewhere in the file, nothing new. vp fmt --check: clean.

Change made by Claude Opus via Claude Code.


Note

Medium Risk
Changes interrupt and session teardown paths in the Claude adapter; incorrect settlement could misreport agent state, but scope is bounded to live-task bookkeeping and is covered by new tests.

Overview
Fixes agent panel rows that stay running forever after Stop when the Claude SDK never confirms subagent shutdown (refused stopTask, timeout, or missing task_notification).

settleLiveTasks drains liveTaskIds and emits synthesized task.completed with status: "stopped" and existing task linkage. interruptTurn still best-efforts stopTask (same timeouts), then interrupt(), then always settles remaining live tasks instead of only synthesizing completion when stopTask succeeded. stopSessionInternal settles live tasks before completeTurn and session.exited.

Comments on background_tasks_changed are corrected: the SDK control is not a roster query. Tests cover refused stopTask, nested agentId on settled rows, and shutdown event ordering.

Reviewed by Cursor Bugbot for commit 287aa51. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Emit terminal task rows for subagents that survive a stop sweep in interruptTurn and stopSession

  • Introduces settleLiveTasks in ClaudeAdapter.ts, which drains liveTaskIds and emits a synthesized task.completed event with status 'stopped' for each remaining live task.
  • interruptTurn now calls settleLiveTasks after attempting stopTask and invoking query.interrupt, guaranteeing terminal task rows even when individual stopTask calls reject or time out.
  • stopSessionInternal calls settleLiveTasks before completing the turn, ensuring task rows are emitted with the current turnId before session.exited.
  • Adds tests covering: stopTask rejection, nested task termination with agent linkage preserved, and correct ordering of task events before session.exited.

Macroscope summarized 287aa51.

…p sweep
Interrupting a turn left a spawned subagent's row stuck "running" forever.
The interrupt path stopped each live task, but on any failure mode - stopTask
rejecting, timing out, or the id already being gone - it returned without
emitting a terminal row, so only acknowledged stops ever produced one. Session
stop had the same hole: it emitted session.exited while leaving liveTaskIds
populated. Nothing else closes those rows out, so the panel counted up for the
life of the thread. Seen with a nested subagent, which no client-side cascade
covers either.
Both call sites now share settleLiveTasks, which drains liveTaskIds into
synthesized task.completed { status: "stopped" } rows carrying the usual
linkage. It runs after query.interrupt() resolves and before session.exited -
both points where nothing the session spawned can still be running. The SDK
offers no roster to reconcile against (Query.backgroundTasks backgrounds
tasks, it does not list them), so a task the CLI never confirmed is called
stopped rather than left spinning. The stale comment claiming the typed
background_tasks control request was the reconciliation source is corrected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 658869f1-a1c9-44a4-a078-104f2a465540

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 19, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix modifies runtime event emission behavior in the ClaudeAdapter's task cleanup paths. While the change is well-scoped and well-tested, it affects core session lifecycle logic in a file the author hasn't previously contributed to, warranting human review.

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

MarcL01 added a commit to MarcL01/t3code that referenced this pull request Aug 25, 2026
…p sweep
Interrupting a turn could leave a spawned subagent's row in the agents panel
showing "running" forever. The interrupt path synthesized a terminal
task.completed only when stopTask was acknowledged within its 3s timeout, so a
timed-out stop or an id the SDK had already dropped bailed out silently, and
nothing downstream swept liveTaskIds afterwards.
A shared settleLiveTasks now drains liveTaskIds and emits one synthesized
task.completed with status "stopped" per remaining id. interruptTurn settles
after interrupt(), where the turn is dead and no liveness signal remains;
stopSessionInternal settles before completeTurn so the rows keep their turnId,
and before session.exited.
Ported from pingdotgg#7585. Verified the defect was still present before
porting: the acknowledged-only guard was live in interruptTurn, no
settleLiveTasks existed, and neither completeTurn, handleStreamExit, nor
stopSessionInternal drained liveTaskIds. Merged without conflicts.
Co-authored-by: Tobi <155588579+spiky02plateau@users.noreply.github.com>
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Current main already contains the terminal-task-row behavior for subagents that survive a stop sweep. This branch has no distinct repair left to review.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@spiky02plateau@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): emit terminal task rows for subagents that survive a stop sweep - #7585

Closed
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks
Closed

fix(server): emit terminal task rows for subagents that survive a stop sweep#7585
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks

Conversation

@spiky02plateau

@spiky02plateauspiky02plateau commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Interrupting a turn can leave a spawned subagent's row in the agents panel showing "running" forever: blue dot, duration counting up against the wall clock for the life of the thread. Observed with a nested agent (spawned from inside another subagent), but any task whose stop confirmation goes missing gets stranded the same way.

The interrupt path stops live tasks best-effort: stopTask per task with a 3s timeout, and only an acknowledged stop got a synthesized terminal task.completed. Every failure mode (refusal, timeout, the id already gone) returned silently, and nothing downstream can recover: completeTurn and stopSessionInternal never sweep liveTaskIds, the client's coordinator cascade only reaches workflow members keyed by parentAgentId (which nested agents never carry), and the client's dead-session sweep only fires when the session disconnects, which a turn interrupt does not cause.

I checked whether the SDK offers a roster to reconcile against, since a comment in the adapter called the background_tasks control request "the reconciliation source". It is not: per the installed SDK's types, Query.backgroundTasks backgrounds in-flight tasks (the Ctrl+B equivalent) and returns a boolean; BackgroundTaskSummary[] only appears on stop-hook inputs. There is no on-demand liveness query.

So the fix makes T3's own bookkeeping honest. A shared settleLiveTasks drains liveTaskIds and emits one synthesized task.completed { status: "stopped" } with the task's linkage per remaining id. The interrupt path's stop loop collapses to a pure best-effort sweep (same 3s/10s bounds), then interrupt(), then settle: once the interrupt lands the turn is dead and T3 has no remaining liveness signal for anything still marked live. stopSessionInternal settles before completeTurn (rows keep their turnId) and before session.exited, where it is unambiguous since the session process is going away. A turn that ends normally still leaves backgrounded tasks live on purpose. The stale comment now says what background_tasks actually does.

Left alone, for scope: the client's uncapped 1s duration interval (any future row missing its terminal event still counts up), and the client cascades that cannot reach nested agents. With the server now settling, those are defense-in-depth concerns.

Verification

In apps/server:

  • vp test run src/provider/Layers/ClaudeAdapter.test.ts: 73 passed (73). Three new tests: a refused stopTask still yields a terminal stopped row after interrupt; a nested task keeps its owning-agent linkage on the settled row; stopSession emits terminal rows before session.exited.
  • tsgo --noEmit: no errors.
  • vp lint on both files: one pre-existing warning elsewhere in the file, nothing new. vp fmt --check: clean.

Change made by Claude Opus via Claude Code.


Note

Medium Risk
Changes interrupt and session teardown paths in the Claude adapter; incorrect settlement could misreport agent state, but scope is bounded to live-task bookkeeping and is covered by new tests.

Overview
Fixes agent panel rows that stay running forever after Stop when the Claude SDK never confirms subagent shutdown (refused stopTask, timeout, or missing task_notification).

settleLiveTasks drains liveTaskIds and emits synthesized task.completed with status: "stopped" and existing task linkage. interruptTurn still best-efforts stopTask (same timeouts), then interrupt(), then always settles remaining live tasks instead of only synthesizing completion when stopTask succeeded. stopSessionInternal settles live tasks before completeTurn and session.exited.

Comments on background_tasks_changed are corrected: the SDK control is not a roster query. Tests cover refused stopTask, nested agentId on settled rows, and shutdown event ordering.

Reviewed by Cursor Bugbot for commit 287aa51. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Emit terminal task rows for subagents that survive a stop sweep in interruptTurn and stopSession

  • Introduces settleLiveTasks in ClaudeAdapter.ts, which drains liveTaskIds and emits a synthesized task.completed event with status 'stopped' for each remaining live task.
  • interruptTurn now calls settleLiveTasks after attempting stopTask and invoking query.interrupt, guaranteeing terminal task rows even when individual stopTask calls reject or time out.
  • stopSessionInternal calls settleLiveTasks before completing the turn, ensuring task rows are emitted with the current turnId before session.exited.
  • Adds tests covering: stopTask rejection, nested task termination with agent linkage preserved, and correct ordering of task events before session.exited.

Macroscope summarized 287aa51.

…p sweep
Interrupting a turn left a spawned subagent's row stuck "running" forever.
The interrupt path stopped each live task, but on any failure mode - stopTask
rejecting, timing out, or the id already being gone - it returned without
emitting a terminal row, so only acknowledged stops ever produced one. Session
stop had the same hole: it emitted session.exited while leaving liveTaskIds
populated. Nothing else closes those rows out, so the panel counted up for the
life of the thread. Seen with a nested subagent, which no client-side cascade
covers either.
Both call sites now share settleLiveTasks, which drains liveTaskIds into
synthesized task.completed { status: "stopped" } rows carrying the usual
linkage. It runs after query.interrupt() resolves and before session.exited -
both points where nothing the session spawned can still be running. The SDK
offers no roster to reconcile against (Query.backgroundTasks backgrounds
tasks, it does not list them), so a task the CLI never confirmed is called
stopped rather than left spinning. The stale comment claiming the typed
background_tasks control request was the reconciliation source is corrected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 658869f1-a1c9-44a4-a078-104f2a465540

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 19, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix modifies runtime event emission behavior in the ClaudeAdapter's task cleanup paths. While the change is well-scoped and well-tested, it affects core session lifecycle logic in a file the author hasn't previously contributed to, warranting human review.

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

MarcL01 added a commit to MarcL01/t3code that referenced this pull request Aug 25, 2026
…p sweep
Interrupting a turn could leave a spawned subagent's row in the agents panel
showing "running" forever. The interrupt path synthesized a terminal
task.completed only when stopTask was acknowledged within its 3s timeout, so a
timed-out stop or an id the SDK had already dropped bailed out silently, and
nothing downstream swept liveTaskIds afterwards.
A shared settleLiveTasks now drains liveTaskIds and emits one synthesized
task.completed with status "stopped" per remaining id. interruptTurn settles
after interrupt(), where the turn is dead and no liveness signal remains;
stopSessionInternal settles before completeTurn so the rows keep their turnId,
and before session.exited.
Ported from pingdotgg#7585. Verified the defect was still present before
porting: the acknowledged-only guard was live in interruptTurn, no
settleLiveTasks existed, and neither completeTurn, handleStreamExit, nor
stopSessionInternal drained liveTaskIds. Merged without conflicts.
Co-authored-by: Tobi <155588579+spiky02plateau@users.noreply.github.com>
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Current main already contains the terminal-task-row behavior for subagents that survive a stop sweep. This branch has no distinct repair left to review.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@spiky02plateau@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): emit terminal task rows for subagents that survive a stop sweep - #7585

Closed
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks
Closed

fix(server): emit terminal task rows for subagents that survive a stop sweep#7585
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks

Conversation

@spiky02plateau

@spiky02plateauspiky02plateau commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Interrupting a turn can leave a spawned subagent's row in the agents panel showing "running" forever: blue dot, duration counting up against the wall clock for the life of the thread. Observed with a nested agent (spawned from inside another subagent), but any task whose stop confirmation goes missing gets stranded the same way.

The interrupt path stops live tasks best-effort: stopTask per task with a 3s timeout, and only an acknowledged stop got a synthesized terminal task.completed. Every failure mode (refusal, timeout, the id already gone) returned silently, and nothing downstream can recover: completeTurn and stopSessionInternal never sweep liveTaskIds, the client's coordinator cascade only reaches workflow members keyed by parentAgentId (which nested agents never carry), and the client's dead-session sweep only fires when the session disconnects, which a turn interrupt does not cause.

I checked whether the SDK offers a roster to reconcile against, since a comment in the adapter called the background_tasks control request "the reconciliation source". It is not: per the installed SDK's types, Query.backgroundTasks backgrounds in-flight tasks (the Ctrl+B equivalent) and returns a boolean; BackgroundTaskSummary[] only appears on stop-hook inputs. There is no on-demand liveness query.

So the fix makes T3's own bookkeeping honest. A shared settleLiveTasks drains liveTaskIds and emits one synthesized task.completed { status: "stopped" } with the task's linkage per remaining id. The interrupt path's stop loop collapses to a pure best-effort sweep (same 3s/10s bounds), then interrupt(), then settle: once the interrupt lands the turn is dead and T3 has no remaining liveness signal for anything still marked live. stopSessionInternal settles before completeTurn (rows keep their turnId) and before session.exited, where it is unambiguous since the session process is going away. A turn that ends normally still leaves backgrounded tasks live on purpose. The stale comment now says what background_tasks actually does.

Left alone, for scope: the client's uncapped 1s duration interval (any future row missing its terminal event still counts up), and the client cascades that cannot reach nested agents. With the server now settling, those are defense-in-depth concerns.

Verification

In apps/server:

  • vp test run src/provider/Layers/ClaudeAdapter.test.ts: 73 passed (73). Three new tests: a refused stopTask still yields a terminal stopped row after interrupt; a nested task keeps its owning-agent linkage on the settled row; stopSession emits terminal rows before session.exited.
  • tsgo --noEmit: no errors.
  • vp lint on both files: one pre-existing warning elsewhere in the file, nothing new. vp fmt --check: clean.

Change made by Claude Opus via Claude Code.


Note

Medium Risk
Changes interrupt and session teardown paths in the Claude adapter; incorrect settlement could misreport agent state, but scope is bounded to live-task bookkeeping and is covered by new tests.

Overview
Fixes agent panel rows that stay running forever after Stop when the Claude SDK never confirms subagent shutdown (refused stopTask, timeout, or missing task_notification).

settleLiveTasks drains liveTaskIds and emits synthesized task.completed with status: "stopped" and existing task linkage. interruptTurn still best-efforts stopTask (same timeouts), then interrupt(), then always settles remaining live tasks instead of only synthesizing completion when stopTask succeeded. stopSessionInternal settles live tasks before completeTurn and session.exited.

Comments on background_tasks_changed are corrected: the SDK control is not a roster query. Tests cover refused stopTask, nested agentId on settled rows, and shutdown event ordering.

Reviewed by Cursor Bugbot for commit 287aa51. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Emit terminal task rows for subagents that survive a stop sweep in interruptTurn and stopSession

  • Introduces settleLiveTasks in ClaudeAdapter.ts, which drains liveTaskIds and emits a synthesized task.completed event with status 'stopped' for each remaining live task.
  • interruptTurn now calls settleLiveTasks after attempting stopTask and invoking query.interrupt, guaranteeing terminal task rows even when individual stopTask calls reject or time out.
  • stopSessionInternal calls settleLiveTasks before completing the turn, ensuring task rows are emitted with the current turnId before session.exited.
  • Adds tests covering: stopTask rejection, nested task termination with agent linkage preserved, and correct ordering of task events before session.exited.

Macroscope summarized 287aa51.

…p sweep
Interrupting a turn left a spawned subagent's row stuck "running" forever.
The interrupt path stopped each live task, but on any failure mode - stopTask
rejecting, timing out, or the id already being gone - it returned without
emitting a terminal row, so only acknowledged stops ever produced one. Session
stop had the same hole: it emitted session.exited while leaving liveTaskIds
populated. Nothing else closes those rows out, so the panel counted up for the
life of the thread. Seen with a nested subagent, which no client-side cascade
covers either.
Both call sites now share settleLiveTasks, which drains liveTaskIds into
synthesized task.completed { status: "stopped" } rows carrying the usual
linkage. It runs after query.interrupt() resolves and before session.exited -
both points where nothing the session spawned can still be running. The SDK
offers no roster to reconcile against (Query.backgroundTasks backgrounds
tasks, it does not list them), so a task the CLI never confirmed is called
stopped rather than left spinning. The stale comment claiming the typed
background_tasks control request was the reconciliation source is corrected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 658869f1-a1c9-44a4-a078-104f2a465540

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 19, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix modifies runtime event emission behavior in the ClaudeAdapter's task cleanup paths. While the change is well-scoped and well-tested, it affects core session lifecycle logic in a file the author hasn't previously contributed to, warranting human review.

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

MarcL01 added a commit to MarcL01/t3code that referenced this pull request Aug 25, 2026
…p sweep
Interrupting a turn could leave a spawned subagent's row in the agents panel
showing "running" forever. The interrupt path synthesized a terminal
task.completed only when stopTask was acknowledged within its 3s timeout, so a
timed-out stop or an id the SDK had already dropped bailed out silently, and
nothing downstream swept liveTaskIds afterwards.
A shared settleLiveTasks now drains liveTaskIds and emits one synthesized
task.completed with status "stopped" per remaining id. interruptTurn settles
after interrupt(), where the turn is dead and no liveness signal remains;
stopSessionInternal settles before completeTurn so the rows keep their turnId,
and before session.exited.
Ported from pingdotgg#7585. Verified the defect was still present before
porting: the acknowledged-only guard was live in interruptTurn, no
settleLiveTasks existed, and neither completeTurn, handleStreamExit, nor
stopSessionInternal drained liveTaskIds. Merged without conflicts.
Co-authored-by: Tobi <155588579+spiky02plateau@users.noreply.github.com>
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Current main already contains the terminal-task-row behavior for subagents that survive a stop sweep. This branch has no distinct repair left to review.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@spiky02plateau@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(server): emit terminal task rows for subagents that survive a stop sweep - #7585

Closed
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks
Closed

fix(server): emit terminal task rows for subagents that survive a stop sweep#7585
spiky02plateau wants to merge 1 commit into
pingdotgg:mainfrom
spiky02plateau:fix/reconcile-interrupted-tasks

Conversation

@spiky02plateau

@spiky02plateauspiky02plateau commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Interrupting a turn can leave a spawned subagent's row in the agents panel showing "running" forever: blue dot, duration counting up against the wall clock for the life of the thread. Observed with a nested agent (spawned from inside another subagent), but any task whose stop confirmation goes missing gets stranded the same way.

The interrupt path stops live tasks best-effort: stopTask per task with a 3s timeout, and only an acknowledged stop got a synthesized terminal task.completed. Every failure mode (refusal, timeout, the id already gone) returned silently, and nothing downstream can recover: completeTurn and stopSessionInternal never sweep liveTaskIds, the client's coordinator cascade only reaches workflow members keyed by parentAgentId (which nested agents never carry), and the client's dead-session sweep only fires when the session disconnects, which a turn interrupt does not cause.

I checked whether the SDK offers a roster to reconcile against, since a comment in the adapter called the background_tasks control request "the reconciliation source". It is not: per the installed SDK's types, Query.backgroundTasks backgrounds in-flight tasks (the Ctrl+B equivalent) and returns a boolean; BackgroundTaskSummary[] only appears on stop-hook inputs. There is no on-demand liveness query.

So the fix makes T3's own bookkeeping honest. A shared settleLiveTasks drains liveTaskIds and emits one synthesized task.completed { status: "stopped" } with the task's linkage per remaining id. The interrupt path's stop loop collapses to a pure best-effort sweep (same 3s/10s bounds), then interrupt(), then settle: once the interrupt lands the turn is dead and T3 has no remaining liveness signal for anything still marked live. stopSessionInternal settles before completeTurn (rows keep their turnId) and before session.exited, where it is unambiguous since the session process is going away. A turn that ends normally still leaves backgrounded tasks live on purpose. The stale comment now says what background_tasks actually does.

Left alone, for scope: the client's uncapped 1s duration interval (any future row missing its terminal event still counts up), and the client cascades that cannot reach nested agents. With the server now settling, those are defense-in-depth concerns.

Verification

In apps/server:

  • vp test run src/provider/Layers/ClaudeAdapter.test.ts: 73 passed (73). Three new tests: a refused stopTask still yields a terminal stopped row after interrupt; a nested task keeps its owning-agent linkage on the settled row; stopSession emits terminal rows before session.exited.
  • tsgo --noEmit: no errors.
  • vp lint on both files: one pre-existing warning elsewhere in the file, nothing new. vp fmt --check: clean.

Change made by Claude Opus via Claude Code.


Note

Medium Risk
Changes interrupt and session teardown paths in the Claude adapter; incorrect settlement could misreport agent state, but scope is bounded to live-task bookkeeping and is covered by new tests.

Overview
Fixes agent panel rows that stay running forever after Stop when the Claude SDK never confirms subagent shutdown (refused stopTask, timeout, or missing task_notification).

settleLiveTasks drains liveTaskIds and emits synthesized task.completed with status: "stopped" and existing task linkage. interruptTurn still best-efforts stopTask (same timeouts), then interrupt(), then always settles remaining live tasks instead of only synthesizing completion when stopTask succeeded. stopSessionInternal settles live tasks before completeTurn and session.exited.

Comments on background_tasks_changed are corrected: the SDK control is not a roster query. Tests cover refused stopTask, nested agentId on settled rows, and shutdown event ordering.

Reviewed by Cursor Bugbot for commit 287aa51. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Emit terminal task rows for subagents that survive a stop sweep in interruptTurn and stopSession

  • Introduces settleLiveTasks in ClaudeAdapter.ts, which drains liveTaskIds and emits a synthesized task.completed event with status 'stopped' for each remaining live task.
  • interruptTurn now calls settleLiveTasks after attempting stopTask and invoking query.interrupt, guaranteeing terminal task rows even when individual stopTask calls reject or time out.
  • stopSessionInternal calls settleLiveTasks before completing the turn, ensuring task rows are emitted with the current turnId before session.exited.
  • Adds tests covering: stopTask rejection, nested task termination with agent linkage preserved, and correct ordering of task events before session.exited.

Macroscope summarized 287aa51.

…p sweep
Interrupting a turn left a spawned subagent's row stuck "running" forever.
The interrupt path stopped each live task, but on any failure mode - stopTask
rejecting, timing out, or the id already being gone - it returned without
emitting a terminal row, so only acknowledged stops ever produced one. Session
stop had the same hole: it emitted session.exited while leaving liveTaskIds
populated. Nothing else closes those rows out, so the panel counted up for the
life of the thread. Seen with a nested subagent, which no client-side cascade
covers either.
Both call sites now share settleLiveTasks, which drains liveTaskIds into
synthesized task.completed { status: "stopped" } rows carrying the usual
linkage. It runs after query.interrupt() resolves and before session.exited -
both points where nothing the session spawned can still be running. The SDK
offers no roster to reconcile against (Query.backgroundTasks backgrounds
tasks, it does not list them), so a task the CLI never confirmed is called
stopped rather than left spinning. The stale comment claiming the typed
background_tasks control request was the reconciliation source is corrected.
@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 658869f1-a1c9-44a4-a078-104f2a465540

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 19, 2026
@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This bug fix modifies runtime event emission behavior in the ClaudeAdapter's task cleanup paths. While the change is well-scoped and well-tested, it affects core session lifecycle logic in a file the author hasn't previously contributed to, warranting human review.

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

MarcL01 added a commit to MarcL01/t3code that referenced this pull request Aug 25, 2026
…p sweep
Interrupting a turn could leave a spawned subagent's row in the agents panel
showing "running" forever. The interrupt path synthesized a terminal
task.completed only when stopTask was acknowledged within its 3s timeout, so a
timed-out stop or an id the SDK had already dropped bailed out silently, and
nothing downstream swept liveTaskIds afterwards.
A shared settleLiveTasks now drains liveTaskIds and emits one synthesized
task.completed with status "stopped" per remaining id. interruptTurn settles
after interrupt(), where the turn is dead and no liveness signal remains;
stopSessionInternal settles before completeTurn so the rows keep their turnId,
and before session.exited.
Ported from pingdotgg#7585. Verified the defect was still present before
porting: the acknowledged-only guard was live in interruptTurn, no
settleLiveTasks existed, and neither completeTurn, handleStreamExit, nor
stopSessionInternal drained liveTaskIds. Merged without conflicts.
Co-authored-by: Tobi <155588579+spiky02plateau@users.noreply.github.com>
@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

Current main already contains the terminal-task-row behavior for subagents that survive a stop sweep. This branch has no distinct repair left to review.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@spiky02plateau@t3dotgg