Skip to content

fix(orchestration): closed children follow their parent's replacement - #1369

Merged
Juliusolsson05 merged 12 commits into
mainfrom
fix/orchestration-tombstone-parent-carry
Sep 27, 2026
Merged

Juliusolsson05 merged 12 commits into
mainfrom
fix/orchestration-tombstone-parent-carry

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Refs #1283. This fixes item 1, orchestration tombstones. Item 2 (the Codex handoff window lease) is #1338, and item 3 (PTY attach counts across a renderer reload) is still open.

Plan: docs/plans/2026-09-27-orchestration-tombstone-parent-carry.md (first commit).

Problem

Main keeps a small in-memory tombstone for every child closed through the orchestration MCP. This is how a parent still sees "closed" as a state after the renderer removes the pane. A tombstone matches its parent by the frozen orchestrationParentId / orchestrationRootId.

When parent P is replaced by P′ (replace, provider switch, resume, rewind, Reload Agents), the renderer remaps every LIVE child to P′, but main was never told. So for P′:

  • list_agents and read_run_outputs omitted every closed child;
  • read_agent on one of them said not-found;
  • a child's prompt boundary invalidated P's status cache instead of P′'s.

What merges

  • OrchestrationBridge.carryParent(from, to) (main):
    • rewrites stored tombstones and child→parent hints to to, and invalidates both status caches;
    • keeps a bounded alias chain (same 24 h TTL and 500 cap as the tombstones). closeAgent reads a child before the renderer closes it, so a swap landing in between still files the tombstone under the successor. The chain follows P→P′→P″.
    • Acyclic by construction: a carry to to drops to's own outgoing edge, since to is live again. A hop cap backstops this.
  • Children created across the swap:
    • main's create answer and late adoption file the child's hint under the successor, and drop both parents' caches;
    • the renderer's createOrchestrationAgent resolves parent and root through a per-window successor map. The map is recorded by the same committed-swap helper, and it is bounded and acyclic.
  • A bootstrap mark from a replaced parent reaches the successor. It is resolved when the queued request is DISPATCHED, because the bridge serves one request at a time.
  • orchestration:carry-parent IPC, and the renderer's carryOrchestrationParents(idMap) in successorCarry.ts, called:
    • wherever live children are remapped: replace, including a newConversation swap, and Reload Agents (committed successors only, never an orphan);
    • on Undo Close, for each restored successor.
  • Why not workflows:carry-session: workflow runs follow the conversation and are skipped for newConversation; orchestration pointers follow the pane. One channel cannot honour both rules.
  • Rehydrate needs no carry: local ids are stable across a renderer reload, and a full restart starts main with no tombstones.
  • Trust: the same as workflows:carry-session / goal-loop:carry. Malformed input is a no-op.

Merge order

Merge before #1374. #1374 makes a restored parent's live children visible, and this PR's Undo Close carry rewrites main's child→parent hints, which #1374's review names as its only residual.

Tests

  • src/main/ipc/orchestration.test.ts: the real preload method goes through the real IPC handler into the real bridge, and malformed input is covered.
  • orchestrationRuntime.renderer.test.tsx: a create whose parent and root are replaced during its spawn is filed, answered and listed under the successors.
  • OrchestrationBridge.parentCarry.test.ts (the real close flow: pre-read, then close, then tombstone):
    • after the carry, P′ lists, reads and collects the closed child, and P no longer owns it;
    • a close straddling the carry is filed under P′;
    • a two-hop lineage and a revived pane (no loop, a new child stays with it);
    • P′'s status cache is invalidated by a child of P.
  • Renderer: replacementOwnership (newConversation still carries), reloadAgentsOwnership (the committed successor only, not an orphan) and undoCloseSuccessorCarry (the restored pane, not a bailed restore).
  • Red checks:
    • the new main tests fail with carryParent as a no-op;
    • removing any one of these fails a test: the tombstone rewrite, parent resolution, root resolution, the delete(to) cycle guard, the hint remap;
    • removing the three renderer calls fails three tests.
  • Gates: on 2abe9d74 (main merged after fix(sessions): a committed Codex handoff releases the predecessor's window lease #1338; the code is unchanged since 81314044), npx tsc -b is clean and the focused suites (src/main/orchestration, src/main/ipc, workspace actions, orchestration runtime) pass: 55 files, 385 tests.

🤖 Generated with Claude Code

Juliusolsson05 and others added 4 commits September 26, 2026 23:13
…cement (#1283 item 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in main

Tombstones of MCP-closed children named the parent's old session id, so
the parent's successor could not list, read or collect them. carryParent
rewrites stored tombstones and child->parent hints, and keeps a bounded,
acyclic alias chain for a close whose read predates the swap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t's successor

orchestration:carry-parent, called wherever live children are remapped
(replace incl. newConversation, Reload Agents) and on Undo Close, only
for committed successors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05 Juliusolsson05 added type:bug Something works wrong class:C2-identity Identity/ownership across reload, replace, restore, wake sev:P3 Minor labels Sep 27, 2026
Juliusolsson05 and others added 2 commits September 26, 2026 23:43
…s filed under the successor

#1369 review a: createAgent's answer and adoptLateResponse stored the
requested (replaced) parent as the child's hint and invalidated only its
cache, so the successor served a stale empty list. Both now resolve through
the alias chain. Review b: the preload->IPC->bridge boundary, the
same-conversation replace carry and alias pruning are now pinned; every
single mutation is red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erer helper's containment

#1369 review c: dropping either status-cache invalidation in carryParent,
the renderer's identity-pair skip or its .catch left every test green.
Also corrects the delete(to) comment: no wired caller revives an id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 and others added 3 commits September 27, 2026 00:05
…t swap under the successor

#1369 verification a and b: createOrchestrationAgent captured the parent
id, awaited the spawn, and filed the child under it; a replacement
committed meanwhile had only remapped existing children, so the successor
could not list, read or close the new child. The committed-swap helper now
records a bounded, acyclic successor map per window, and filing resolves
parent and root through it. Red without the resolution; every piece pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts successor

q85: the mark is the last step of a delivery that can land long after the
create began (a prompt waiting for the composer, or #1370's late
adoption). It was addressed with the parent id the tool call captured;
after a replacement that id has no window and owns no children, so the
mark was lost. Resolved through the alias chain; the combined late-create
+ replacement path was reproduced against #1375 and passes with this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…spatch

#1369 verification a, round 3: the bridge serves one renderer request at a
time, so a mark resolved when queued could still dispatch to a parent
replaced while it waited. Only the mark is rewritten; other requests act
for the parent that asked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Review disposition (head 81314044). Round 1: a and b FIX-BEFORE-MERGE; c (pi) MERGE-READY with minors. Verification: b FIX-BEFORE-MERGE in round 2, then MERGE-READY in round 3. a was FIX-BEFORE-MERGE in rounds 2 and 3, each time with a new real race, then MERGE-READY in round 4. Steering q85 and q86 are covered below.

Fixed

  • a, r1: a create answered after the parent swap, or adopted late, stored the retired parent as the child's hint and invalidated only its cache. The successor then served a stale empty list, and wait_agents could report done. Both paths now go through noteCreatedChild, which resolves through the alias chain.
  • b, r1:
    • the preload→IPC→bridge boundary was untested: one electron mock now routes the real preload method into the real handler and bridge, and malformed input is covered;
    • the same-conversation replace carry is now asserted;
    • alias TTL and the 500 cap are now tested.
  • c, r1 minors: both status-cache invalidations in carryParent, the renderer helper's identity skip and its .catch are now pinned. The delete(to) comment is corrected: no wired caller revives an id.
  • a and b, r2: the renderer filed a child created across a parent swap under the retired id. It captures the parent, awaits the spawn, and only existing children were remapped. The committed-swap helper now keeps a bounded, acyclic, per-window successor map, and createOrchestrationAgent resolves parent and root through it. A real useWorkspace test holds the spawn across both swaps.
  • q85: a bootstrap mark addressed with a replaced parent's id had no window and no ownership, so it was lost. It is now resolved. The combined late-create plus replacement path was reproduced on fix(orchestration): closed children follow their parent's replacement #1369 + fix(orchestration): deliver the bootstrap prompt to a child created after the deadline (#1370) #1375 and passes.
  • a, r3 and q86: a mark QUEUED behind another request (the bridge serves one at a time) still dispatched to a parent replaced while it waited. It now resolves at dispatch. Only the mark is rewritten; other requests act for the parent that asked. The test is red with enqueue-time resolution only, and a confirmed this in round 4.

Every fix was red before it landed. Each guard was also removed on its own, and each removal fails a test.

Filed as a follow-up, not in this PR

Handed to W4, not pushed here

🤖 Generated with Claude Code

@Juliusolsson05
Juliusolsson05 merged commit b9d9ee8 into main Sep 27, 2026
2 checks passed
Juliusolsson05 added a commit that referenced this pull request Sep 27, 2026
@Juliusolsson05
Juliusolsson05 deleted the fix/orchestration-tombstone-parent-carry branch September 27, 2026 19:07
Juliusolsson05 added a commit that referenced this pull request Sep 27, 2026
…esolve the parent lease through its alias chain

W2's patch (temp/q85): orchestration.bootstrap_mark_failed on the late path
only, and the combined late-create + parent-replacement case. isParentAttached
now resolves the parent through #1369's currentParentId, so a parent replaced
during the late window keeps its brief flowing to the successor; only a
parent with no live successor is gone. Removing the alias resolution fails
the replaced-parent test. Orchestration suites green; tsc 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 27, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

class:C2-identity Identity/ownership across reload, replace, restore, wake sev:P3 Minor type:bug Something works wrong

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant