Skip to content

fix(goal-loop): a loop follows its pane when the agent is replaced (#1279) - #1287

Merged
Juliusolsson05 merged 9 commits into
mainfrom
fix/goal-loop-follows-replace
Sep 25, 2026
Merged

Juliusolsson05 merged 9 commits into
mainfrom
fix/goal-loop-follows-replace

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1279.

Found by the Stage 3 C2 hunt. Plan: docs/plans/2026-09-25-goal-loop-follows-replace.md (first commit).

Why

GoalLoopService keys loops by session id. Reload, provider switch, resume, rewind, the MCP toggle and Reload Agents all give the pane a new id. The loop stayed filed under the dead id, so it vanished from the pane: Resume and Stop couldn't reach it, and goal_loop_complete from the new process failed.

What merges

  • GoalLoopService.carry(from, to) moves a loop to the successor.
    • It first runs the same interrupt() a removal would. That drops the dead id's trackers, so an active loop arrives paused/interrupted. This is the restart rule: the new process hasn't proved its hooks. The user resumes the loop from the pane, which now finds it.
    • It persists the move itself.
    • It refuses when there is no source loop, or when the successor already has a non-ended loop.
  • A new IPC, goal-loop:carry, has the same trust gate as goal-loop:control.
  • The renderer acts at both commit points (replaceSession and Reload Agents), after the commit, using the same old→new map it applies to pins, lanes and relationships. For each replaced pane:
    • Carry only when the successor continues the same conversation (not newConversation, e.g. the Conversations picker) and its resolved MCP domains include goal_loop.
    • Stop otherwise. The loop is ended with controlGoalLoop({ action: 'stop' }), because no pane could ever reach it again, and a successor without Goal Loop tools could not complete it. This is the rule AgentMcpServersModal already applies to its own toggle.
    • A replacement that fails or never commits keeps its loop.
  • Both calls are fire-and-forget. A failed carry leaves the behaviour from before this PR.

Tests

  • Service (GoalLoopService.test.ts, 4 new tests):
    • the loop moves, paused, and is persisted under the new id before anything else writes;
    • a late removal of the old id changes nothing;
    • carry never overwrites and ignores an unknown source;
    • nothing is delivered before resume;
    • a user-paused loop stays paused.
  • Renderer (replacementOwnership.renderer.test.tsx, 5 new tests):
    • a committed replacement carries;
    • a different conversation is not carried and is stopped;
    • a successor without Goal Loop tools is not carried and is stopped;
    • Reload Agents carries to each capable successor;
    • a mixed-capability Reload Agents carries only to the capable one and stops the other.

npx tsc -b is clean.

Residual

🤖 Generated with Claude Code

Juliusolsson05 and others added 3 commits September 25, 2026 06:40
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1279)

Loops are keyed by session id, and every replacement path gives the pane a
new one. The loop stayed under the dead id, so the pane, Resume/Stop and the
new process's goal_loop_complete all missed it. The renderer, which commits
the old -> new swap, now tells main (goal-loop:carry). Main moves the loop,
paused as interrupted (the restart rule: never blind-continue into a process
that has not proved its hooks), and never overwrites a successor's own loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 added a commit that referenced this pull request Sep 25, 2026
/#1297/#1298 opened

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Juliusolsson05 and others added 6 commits September 25, 2026 07:12
…ation with Goal Loop tools

- A picker swap (newConversation) carried the old conversation's loop into
  an unrelated one, whose Resume then injected the wrong goal.
- A reload that resolved the successor's domains without goal_loop still
  carried the loop into a process that can never call goal_loop_complete.
Both now leave the loop where it was (paused under the old id, the
pre-#1279 state). Tests cover both, the reload-all path and a user-paused
loop (surviving mutants in review A).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; pin reload-all and the carry's own persist

- A2: a loop skipped by the carry (a different conversation, or a successor
  without goal_loop) stayed paused under an id no pane has; it is now
  stopped after the commit, like AgentMcpServersModal's own reload.
- A1/B1: a mixed-capability Reload Agents test pins that only the successor
  keeping goal_loop gets the loop (deleting the reload-all carry or its
  filter now fails).
- B2: the persistence check runs right after the carry, before complete()
  could rewrite the file, so a missing persist in carry fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it and says what happens now

The JSDoc sat above stopGoalLoops and still said a loop left behind stays
paused; since review A2 such loops are stopped.

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

Copy link
Copy Markdown
Owner Author

Final disposition (pre-merge), head cef5365. Three independent reviewers:

  • A (correctness, ownership) and B (tests, UX): two rounds each.
  • C (third reviewer): one round, on the final head.
Reviewer Round 1 Round 2
A (correctness, ownership) FIX-BEFORE-MERGE FIX-BEFORE-MERGE (at 90c9e09)
B (tests, UX) FIX-BEFORE-MERGE MERGE-READY (at 19357dc)
C (third reviewer) MERGE-READY (at cef5365) —

Findings and dispositions:

Round 1

  • A1, major: resuming an unrelated conversation into the pane inherited the previous conversation's loop. Fixed in 90c9e09: there is no carry for newConversation. Since 19357dc that loop is stopped instead.
  • A2, major: a reload driven by a Settings change carried a loop to a successor without Goal Loop tools. Fixed in 90c9e09: the carry happens only when the successor's resolved domains include goal_loop. Since 19357dc that loop is stopped instead.
  • A3, minor: no tests for paused loops or reload-all. Fixed in 90c9e09.
  • B1, major: deleting the reload-all carry left every test green. Fixed in 90c9e09 and tightened in 19357dc. B confirmed that the deletion now fails both Reload Agents tests.
  • B2, major: the "persisted under the new id" check was masked by a later complete(). Fixed in 19357dc: the check now runs right after carry. B confirmed that deleting carry's persist fails it.

Round 2

  • A1, major: reload-all's capability filter was untested. Fixed in 19357dc by "Reload Agents carries only to successors that keep Goal Loop tools". B's round 2 confirmed it.
  • A2, minor: a loop that was not carried stayed paused under a dead id. Fixed in 19357dc: stopGoalLoops ends it after the commit, in both paths. B confirmed that dropping the stop fails three tests.
  • B, cosmetic: the carryGoalLoops doc comment was stale and sat above the wrong function. Fixed in 991378e: the comment moved onto carryGoalLoops and says that uncarried loops are stopped.

Round 1, reviewer C (final head; all minor, none fixed)

  • C1, minor: no test pins "carry and stop run only after the commit". Moving both calls before the commit survives, because the uncommitted-path tests install neither method. Not fixed. The code order is correct at session.ts, after if (!committed) return.
  • C2, minor: whether an ended loop follows the pane is not pinned. A mutant that refuses ended loops survives. Not fixed. Either behaviour is defensible.
  • C3, minor: the same-id guard in carry is not pinned. Not fixed. The input can't happen, because every successor id is freshly minted.
  • C4, minor (scope): undo-close respawns restored panes under new ids and carries no loop, the same symptom as Goal loop is lost from the pane when the agent is replaced (reload, provider switch, resume, MCP toggle) #1279. Not fixed. Out of the issue's listed paths, and unchanged from before; worth its own issue.

Residuals:

  • The C1–C3 test gaps. C1 is the one worth a quick follow-up before anyone refactors replaceSession.
  • Undo Close does not carry a loop (C4). No issue has been filed yet.
  • A visible behaviour change: replacing a pane with a different conversation, or with a successor that lacks Goal Loop tools, now ends its loop (the rule AgentMcpServersModal already uses).
  • Carried loops arrive paused/interrupted, and the user presses Resume.
  • Nobody checked the pane in the running app.

Merges through batch PR #1318 once every member passes its gate.

🤖 Generated with Claude Code

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Goal loop is lost from the pane when the agent is replaced (reload, provider switch, resume, MCP toggle)

1 participant