Skip to content

fix(workspace): Reload Agents re-checks ownership around its awaits (#1282) - #1326

Merged
Juliusolsson05 merged 7 commits into
mainfrom
fix/reload-all-ownership
Sep 27, 2026
Merged

Juliusolsson05 merged 7 commits into
mainfrom
fix/reload-all-ownership

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1282.

Plan: docs/plans/2026-09-26-reload-all-ownership.md (first commit; review rounds recorded there).

Why

reloadAgentSessions (the Dangerous Agents toggle) walked a snapshot, awaiting a kill and a spawn per agent, and re-checked nothing:

  • an agent closed during the reload came back under a new id, or as an invisible orphan if its project had closed;
  • an agent replaced during it (for example a provider switch) got two successors on one provider transcript; replaceSession has canCommit for this (bug(sessions): retire replacement when its source placement disappears #815), reload had nothing;
  • a draft typed meanwhile was lost, because the draft was copied from the pre-reload snapshot; draftImages and unreadSince/unreadKind were never copied.

What merges

  • Reloads run one at a time. Clicking the Dangerous Agents switch on and then off leaves the last click's mode running.

  • Each agent is committed as soon as its own spawn returns. The successor is the pane from that moment, so a close is an ordinary close of the new id. Before, successors ran unfiled until every later spawn returned.

  • Ownership is re-checked after every await:

    • before the kill;
    • after the kill (a close during it means no spawn);
    • after the spawn (a close during it orphans the successor, which is killed at once).

    "Owned" is canCommit's rule: same cwd, kind and provider runtime, and still in a project.

  • The successor is filed from the LIVE row. A project merge or rename during the reload is kept. Only spawn-owned fields come from the loop: identity, MCP domains, and the provider session it resumed. A fresh start keeps the durable providerSessionId spawn reports (runtime-start) and never carries a provisional one.

  • Orphan cleanup has an owner. A failed kill is retried once, then remembered in unstoppedReloadSuccessorsRef, and the next reload retries it first (before its no-live-agents return). Only a confirmed kill (true) clears it, because false can mean "not yours, still running". Application quit's killAll stops everything regardless. A confirmed kill also drops the orphan's renderer bookkeeping.

  • A failed respawn keeps its pane with fix(ipc): session:spawn launders provider exceptions before they cross IPC (#1267) #1324's curated sentence (never raw IPC text), unless the agent was closed meanwhile.

  • Unowned rows are pruned up front (so an all-failed reload prunes them too) and again in each commit (for rows that became unowned mid-reload).

  • The draft, draft images and unread marker come from the live predecessor runtime at commit.

Tests

reloadAgentsOwnership.renderer.test.tsx, 22 cases on the owner's recorded v3 workspace (1 window, 3 projects, 13 sessions, 2 lanes). Spawn and kill IPC are held open so each test acts inside the dangerous interval, not only on the final state:

  • close / replace / cwd change / project removal / provider-runtime change during a respawn;
  • a close during the pre-spawn kill;
  • a close before the loop reaches an agent;
  • an early successor filed (lane, pin and child relationships remapped) while a later spawn is still held, and closed through the real killSession;
  • overlapping on/off reloads;
  • a project merge mid-reload;
  • a rejected and a refused orphan kill, including when no live agent remains;
  • a failed respawn of a closed agent;
  • the durable and provisional provider-id cases;
  • unowned rows (all-failed, and mid-reload);
  • an orphan's early runtime entry;
  • the fix(ipc): session:spawn launders provider exceptions before they cross IPC (#1267) #1324 combined fail-and-close case;
  • drafts, images and unread markers.

Every case was shown red on the head before its fix, and each guard is mutation-checked (the remaps, pin, prune, retry order, true-only clearing, curated message). Workspace suites: 1356/1356. npx tsc -b is clean.

Final verification (verify3, A/C, at 9a8e0444): both MERGE-READY, no findings.

Residuals

  • A remembered orphan lives in renderer memory. A renderer reload forgets it, and application quit is then its owner.
  • A backend that was already gone when main answered false costs one IPC call per later reload.

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits September 26, 2026 16:49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1282)

The reload walked a snapshot and re-checked nothing while it killed and
respawned each agent: an agent closed meanwhile came back under a new id,
one replaced meanwhile got two successors on one transcript, and a draft
typed meanwhile was lost. It now skips an agent that is no longer the same
owned agent before its kill, re-checks every successor at commit (killing
the orphans, caller reload.orphaned-successor), and carries the live draft,
draft images and unread marker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 26, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ait (#1326 review)

Round-1 findings: overlapping on/off reloads, project merge mid-reload,
close during an awaited orphan kill, rejected orphan kill, failed respawn
of a closed agent, and two surviving mutations. Checks and commit are one
synchronous run; orphans are killed after it; successors file from the
live row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 and others added 4 commits September 26, 2026 17:44
…round 2)

An early successor ran unfiled while later spawns were pending, so a
close missed it; a close during the pre-spawn kill was still followed by
a spawn; a failed orphan kill was swallowed; and a fresh respawn's
durable provider id was dropped. Each agent now commits on its own,
ownership is rechecked after every await, orphan kills retry and are
retried by the next reload, and the reported provider id is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reload's per-agent loop keeps #1324's curated failure sentence: the spawn
catch computes it and markRespawnFailed carries it onto the failed pane.
Adds the combined fail-and-close integration case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…keep refused kills (#1326 verify)

A remembered orphan was never retried once every agent was closed, and a
kill main refused (false: possibly still running) cleared its record. An
all-failed reload also stopped pruning unowned rows. Orphans are retried
first, only a confirmed kill clears them, and pruning happens up front.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…phans locally (#1326 verify2)

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

Copy link
Copy Markdown
Owner Author

Review disposition (#1326): reviewers A, B, C; round 1, round 2 and two verification passes (A, C); final head 9a8e0444

Round Finding Verdict Disposition
1 A1: overlapping on/off reloads leave the dangerous successors running valid, Blocker Fixed: reloads are serialized.
1 A2: project merge mid-reload files the successor into a deleted project valid, Blocker Fixed: the successor is filed from the live row.
1 A3 / B1: a close during an awaited orphan kill was never re-checked valid, Major Fixed; later superseded by per-agent commits.
1 A4: a rejected orphan kill aborted the reload valid, Major Fixed: per-orphan try/catch.
1 A5: a failed respawn recreated a closed agent's runtime valid, Minor Fixed: failed ids are ownership-filtered.
1 A / B survivors: cwd clause, project-ownership clause valid Pinned.
2 C1: a close during the pre-spawn kill still spawned valid, Major Fixed: re-check after the kill.
2 C2: an early successor ran unfiled while a later spawn stalled valid, Major Fixed at the root: each agent commits as its spawn returns.
2 C3: a swallowed orphan-kill rejection valid, Major Fixed: retry, then remembered for the next reload.
2 A1: a fresh respawn's durable provider id was dropped valid, Major Fixed (runtime-start); the provisional id is never carried.
2 C4: plan and harness comments drifted valid, Minor Corrected.
2 B: two tests green on the round-1 head accepted Intentional pins of guards that survived round-1 mutations.
verify A / C + q56: a remembered orphan was not retried with no live agents left valid, Major Fixed: retried before the empty return.
verify C + q56: a refused (false) kill cleared the retry record valid, Major Fixed: only true clears it.
verify A: an all-failed reload no longer pruned unowned rows valid, Minor Fixed: prune up front.
verify C survivors: relationship and pin remaps valid Pinned at the dangerous interval.
verify2 A: rows unowned mid-reload not pruned valid, Minor Fixed: per-commit prune restored alongside the up-front one.
verify2 A / C: an orphan's early runtime entry left behind valid, Minor Fixed: forgetSessionLocally after a confirmed kill.
— #1324 integration (q42) done Merged; the curated sentence survives, with a combined fail-and-close test.

The #1325 integration (carry workflow runs only for committed pairs) is owned by W3 if #1325 merges first.

@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Final verification (verify3) at 9a8e0444: reviewers A and C both MERGE-READY, no findings. Disposition above stands; the body is current for this head.

@Juliusolsson05 Juliusolsson05 added type:bug Something works wrong class:C2-identity Identity/ownership across reload, replace, restore, wake sev:P2 Real bug with a workaround labels Sep 27, 2026
@Juliusolsson05
Juliusolsson05 merged commit 797ee4b 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/reload-all-ownership branch September 27, 2026 02:45
Juliusolsson05 added a commit that referenced this pull request Sep 27, 2026
…w-pane

Steering q42: #1326 commits each reloaded agent as its own spawn returns
and kills orphaned successors. The Reload Agents workflow carry moves next
to the per-agent goal-loop carry after commitSuccessor, so only committed
pairs are carried, never an orphan. Test added in
reloadAgentsOwnership.renderer.test.tsx.

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:P2 Real bug with a workaround type:bug Something works wrong

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reload-all can resurrect a closed agent or double a replaced one; it has no commit-time ownership check

1 participant