Skip to content

feat(orchestrator): archive and delete owned subagent subtrees#3878

Closed
PixPMusic wants to merge 1 commit into
pingdotgg:t3code/codex-turn-mappingfrom
PixPMusic:pixpmusic/orchestrator-subagent-lifecycle
Closed

feat(orchestrator): archive and delete owned subagent subtrees#3878
PixPMusic wants to merge 1 commit into
pingdotgg:t3code/codex-turn-mappingfrom
PixPMusic:pixpmusic/orchestrator-subagent-lifecycle

Conversation

@PixPMusic

@PixPMusic PixPMusic commented Jul 11, 2026

Copy link
Copy Markdown

Applying archive, unarchive, or delete to a thread now cascades through the subagent threads it recursively owns, entirely server-side:

  • OwnedSubagentTree walks the recursively owned subtree (subagent lineage only — forks and context transfers are not ownership edges; malformed cycles are bounded).
  • The orchestrator intercepts thread.archive / thread.unarchive / thread.delete, applies the lifecycle transition to every subtree member with a single timestamp (so "archived together" is exact), and cancels active runs and pending attempts across the subtree.
  • Unarchiving a root restores only the members archived together with it; independently archived descendants stay archived.
  • OwnedSubagentLifecycleRepair repairs half-applied cascades on startup.
  • CheckpointCleanupService removes checkpoints orphaned by deleted threads.

Server-only: no client, web, or mobile changes. The sidebar (#3861) and mobile (#3871) UI work layer on top of this behavior and should land after it.

All 282 orchestration-v2 tests pass (276 passed, 6 pre-existing skips), including new coverage for the cascade, repair, cleanup, turn-start gating on archived/deleted threads, and foundation persistence.


Note

High Risk
Large orchestration-v2 changes to thread lifecycle, event commit guards, effect cancellation, and checkpoint deletion—areas where races or incomplete cascades could leave inconsistent state or drop in-flight provider work.

Overview
Thread archive, unarchive, and delete now drive a server-side cascade over recursively owned subagent threads (subagent lineage only—not forks), using a shared archive timestamp so “archived together” is exact and unarchive restores only that cohort. The orchestrator cancels active work across the subtree, detaches sessions, enqueues terminal/attachment cleanup, and on delete adds checkpoint.cleanup via new CheckpointCleanupService and deleteScopeRefs (including deterministic ordinals through interrupted baselines).

Concurrency and repair: dispatch is serialized under a root-thread graph lock; archive/delete cancel process-bound (and capture) effects across all affected thread IDs. OwnedSubagentLifecycleRepair plus getThreadLifecycleRecords reconcile inconsistent parent/child lifecycle on startup and when new subagent threads appear under archived/deleted parents.

Persistence races: writeIfRunCurrent rejects events for archived/deleted threads (including routed child-thread events), accepts multiple expected run statuses, and optionally admits terminal artifacts from superseded attempts. Provider turn start and run execution detach or gate when lifecycle wins; delegated-task and subagent finalization skip inactive parents.

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

Note

Archive and delete owned subagent subtrees with cascading lifecycle propagation

  • Thread archive, unarchive, and delete commands now propagate through the entire owned subagent subtree (via BFS traversal in OwnedSubagentTree.ts), cancelling runs, attempts, nodes, provider turns, and effects for all affected threads.
  • A new CheckpointCleanupService deletes checkpoint refs for each scope in a thread up to the latest known ordinal; it is enqueued as a checkpoint.cleanup effect during archive/delete.
  • writeIfRunCurrent in EventSink.ts now rejects writes if any referenced thread is archived/deleted, and can accept multiple expected run statuses and terminal artifacts for superseded attempts.
  • On startup and on new thread.created events, the orchestrator runs lifecycle repair via OwnedSubagentLifecycleRepair.ts to align child threads whose lifecycle disagrees with an archived/deleted parent.
  • All command dispatches and startNextQueuedRun are now serialized by a graph-level lock keyed on rootThreadId, preventing races across related threads.
  • Risk: archiving or deleting any thread now cascades to its entire owned subagent subtree and cancels all associated in-flight effects, which is a significant behavioral change for multi-thread agent graphs.
📊 Macroscope summarized 225ba2d. 13 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

Applying archive, unarchive, or delete to a thread now cascades
through the subagent threads it recursively owns: the orchestrator
walks the owned subtree (OwnedSubagentTree), applies the lifecycle
transition to every member with a single timestamp, and cancels their
active runs and pending attempts. Half-applied cascades are repaired
on startup (OwnedSubagentLifecycleRepair), and checkpoints orphaned by
deleted threads are cleaned up (CheckpointCleanupService).
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

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

Run ID: 0078d2e8-d8e2-4542-a264-f2bc45bd60e5

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 11, 2026
PixPMusic added a commit to PixPMusic/t3code that referenced this pull request Jul 11, 2026
Mobile UI for the owned-subtree lifecycle cascade in pingdotgg#3878: thread
list actions archive, unarchive, and delete a root together with its
recursively owned subagent threads using the shared subtree helpers
and confirmation copy from client-runtime, and thread lists flag
recovery roots whose subagent parent is no longer available. Depends
on the server-side cascade in pingdotgg#3878.
PixPMusic added a commit to PixPMusic/t3code that referenced this pull request Jul 11, 2026
The recursive lifecycle for owned subagent subtrees is enforced
server-side (pingdotgg#3878): one command per root, one timestamp per cascade,
run cancellation included. Archive, delete, and unarchive go back to a
single root dispatch, and "archived with it" matching returns to
exact timestamp equality, which the server-side cascade guarantees.
@PixPMusic

Copy link
Copy Markdown
Author

Folded into #3861 — the server-side cascade belongs with the first PR in the sequence (no mobile code there); mobile UI follows in #3871.

@PixPMusic PixPMusic closed this Jul 11, 2026
@PixPMusic PixPMusic deleted the pixpmusic/orchestrator-subagent-lifecycle branch July 11, 2026 02:32
(candidate) => candidate.id === run.providerThreadId,
);
if (projection.thread.archivedAt !== null || projection.thread.deletedAt !== null) {
if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium orchestration-v2/ProviderTurnStartService.ts:86

The early archived/deleted fast path detaches the provider session based on the initially-read projection, without revalidating before the call. If the thread is unarchived and a new turn reattaches the same providerSessionId before this outbox effect runs, providerSessions.detach removes the now-current attachment and may release the single-thread session, disrupting the in-flight turn. Consider rechecking the thread lifecycle and current attachment state transactionally before detaching, or conditioning detachment on a lifecycle generation so it only applies to the archived/deleted state it was scheduled for.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/ProviderTurnStartService.ts around line 86:

The early archived/deleted fast path detaches the provider session based on the initially-read projection, without revalidating before the call. If the thread is unarchived and a new turn reattaches the same `providerSessionId` before this outbox effect runs, `providerSessions.detach` removes the now-current attachment and may release the single-thread session, disrupting the in-flight turn. Consider rechecking the thread lifecycle and current attachment state transactionally before detaching, or conditioning detachment on a lifecycle generation so it only applies to the archived/deleted state it was scheduled for.

},
}
: {}),
writeIfRunCurrent: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium orchestration-v2/RunExecutionService.ts:656

expectedStatus: ["running", "waiting"] is applied to every routed provider event, not just child-drain events. When a child keeps ingestion open after the root turn.terminal, a late mutable root event arriving after the run has moved to waiting is still committed and can overwrite terminal root provider state — for example, reverting the provider turn back to running. The waiting admission should be restricted to post-terminal child artifacts only, not applied unconditionally to all events.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/RunExecutionService.ts around line 656:

`expectedStatus: ["running", "waiting"]` is applied to every routed provider event, not just child-drain events. When a child keeps ingestion open after the root `turn.terminal`, a late mutable root event arriving after the run has moved to `waiting` is still committed and can overwrite terminal root provider state — for example, reverting the provider turn back to `running`. The `waiting` admission should be restricted to post-terminal child artifacts only, not applied unconditionally to all events.

Comment on lines +43 to +47
const throughOrdinal = Math.max(
0,
...projection.runs.map((run) => run.ordinal),
...projection.checkpoints.map((checkpoint) => checkpoint.ordinalWithinScope),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium orchestration-v2/CheckpointCleanupService.ts:43

cleanup computes throughOrdinal by spreading every run and checkpoint ordinal into Math.max, so a thread with enough runs and checkpoints exceeds the JavaScript engine's function-argument limit and throws RangeError before any scope refs are removed. Compute the maximum iteratively instead of spreading the arrays into Math.max.

Suggested change
const throughOrdinal = Math.max(
0,
...projection.runs.map((run) => run.ordinal),
...projection.checkpoints.map((checkpoint) => checkpoint.ordinalWithinScope),
);
const throughOrdinal = [
...projection.runs.map((run) => run.ordinal),
...projection.checkpoints.map((checkpoint) => checkpoint.ordinalWithinScope),
0,
].reduce((a, b) => Math.max(a, b), 0);
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration-v2/CheckpointCleanupService.ts around lines 43-47:

`cleanup` computes `throughOrdinal` by spreading every run and checkpoint ordinal into `Math.max`, so a thread with enough runs and checkpoints exceeds the JavaScript engine's function-argument limit and throws `RangeError` before any scope refs are removed. Compute the maximum iteratively instead of spreading the arrays into `Math.max`.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 225ba2d. Configure here.

const descendants = ownedSubagentDescendants(command.threadId, [
...shell.threads,
...shell.archivedThreads,
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete cascade misses shell gaps

Medium Severity

dispatchOwnedThreadLifecycle builds the owned subtree only from getShellSnapshot() threads, which omit deleted rows. If an intermediate owned subagent was already deleted, the walk cannot reach deeper descendants, so a later thread.delete on an ancestor may leave active grandchild threads undeleted and off the affectedThreadIds list used to cancel process-bound effects. planOwnedSubagentLifecycleRepairs runs only at orchestrator startup, so those orphans can keep running until restart.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 225ba2d. Configure here.

@macroscopeapp

macroscopeapp Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

3 blocking correctness issues found. This PR introduces a significant new feature for cascading archive/delete operations across owned subagent subtrees. The changes involve substantial new orchestration logic, new services, and modifications to core event handling. Four unresolved medium-severity review comments identify potential correctness issues including race conditions and orphaned thread scenarios.

You can customize Macroscope's approvability policy. Learn more.

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

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant