Skip to content

feat(goal-loop): harness-owned persistence loop that works until the goal is complete - #1003

Merged
Juliusolsson05 merged 15 commits into
mainfrom
feat/agent-goal-loop
Sep 18, 2026
Merged

Juliusolsson05 merged 15 commits into
mainfrom
feat/agent-goal-loop

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Problem

Agents stop at the first natural pause, leaving multi-step goals half-finished until a human re-prompts (#1001). Provider Stop hooks cannot be the fix: Claude Code force-stops after 9 consecutive blocks (which kills every ralph/ultrawork-style mode — oh-my-claudecode #3138), and OpenCode has no block-capable Stop hook at all.

Implemented behavior

  • New built-in MCP domain goal_loop (off by default, Settings row like Goal): goal_loop_start(goal, loopPrompt, maxContinuations?) — the agent writes its own continuation prompt when the user asks for a loop — and goal_loop_complete(outcome: done | blocked, summary) — instructed to fire only when the goal is utterly satisfied, with blocked as the honest needs-the-user exit.
  • GoalLoopService (main process): subscribes to SessionManager semantic events, detects working→idle via reduceWorkingState (never continues while tools are pending or the agent is blocked on the user), and delivers the continuation prompt through deliverPromptToAgent with an iteration/budget header. Works identically for Claude, Codex and OpenCode — no provider hooks.
  • Safety: budget (default 25) pauses before exceeding, never hard-kills; failed deliveries self-retry with backoff (a failed delivery means no turn_completed will ever rescue the loop) and pause with error after three rounds; control actions racing an in-flight continuation hand off through a pending flag instead of being dropped; state is durable so a crash surfaces as paused(interrupted) with one-click resume; session removal pauses as interrupted.
  • Control surface: an always-on per-pane status strip (Goal loop · active · iteration 3/25 · <goal>) with Pause/Stop, a latched overlay (Cmd+Shift+Y) adding Resume/Raise cap, palette commands Goal Loop and Stop Goal Loop, and a controlGoalLoop IPC. Creating loops stays MCP-only; the renderer only steers.

Design decisions and tradeoffs

  • Harness-owned loop, single mechanism — research summary in the spec; the send interface and turn-boundary observation are ours, so the loop is provider-agnostic and unbounded by provider overrides.
  • Spec refinements (documented in the plan's self-review): loops keyed by sessionId (the actuator is deliverPromptToAgent(sessionId, …)), a latch + always-on strip instead of extending tldr's hold-peek PreviewKind (different data model; the loop surface's job is control), and domain teaching via MCP init instructions (workflows-domain pattern) rather than a managed product skill.
  • Keybinding is Cmd+Shift+Y: Cmd+Shift+G is owned by global-dispatch and check:keybindings rejects unapproved overlaps.

Tests and verification

  • New: 14 service/store/system tests (unit + real MCP-host system test driving start → idle → continuation → complete), prompt-builder and domain tests, 3 renderer pane/commands tests; catalog governance baselines pinned to 132 commands and the goal-loop feature reference documented.
  • npm run typecheck clean; npm run check:keybindings clean; full npm test: 4417 passed, 1 failed — imageAttachment.test.ts corpus invariant, verified failing identically on main (pre-existing, unrelated).

Known limitations

  • No stall detection: an agent that never returns a turn boundary event holds the loop in active (delivery failures are covered; silent stalls are not).
  • Provider switch/reload mid-loop pauses as interrupted; resume is manual (conservative v1).
  • complete(done) is the agent's claim, not a verified fact; the instructions put the verification burden on the model and the summary surfaces what it believes finished.

Fixes #1001

Juliusolsson05 and others added 15 commits September 18, 2026 15:22
Harness-owned persistence loop: agent starts a loop via MCP with a
self-written continuation prompt; Agent Code re-prompts on every idle
without complete. Provider stop hooks were researched and rejected as
the loop driver (Claude Code force-stops after 9 consecutive blocks;
OpenCode has no block-capable Stop hook), so the loop rides our own
deliverPromptToAgent + turn_completed infrastructure instead.
Eight tasks with complete code: shared contract, prompt builder, durable
store, loop service state machine, MCP tools + system test, main wiring
and IPC, renderer control surface, verification and PR. Refs #1001.
Main-process state machine over SessionManager semantic events: working→idle
via reduceWorkingState triggers deliverPromptToAgent with the agent-written
continuation prompt. Cap pauses before exceeding budget; failed deliveries
self-retry with backoff (a failed delivery means no turn_completed will ever
rescue the loop) and land in paused(error) after three rounds; control
actions racing an in-flight continuation hand off through pendingContinue
instead of being dropped.
…#1001

GoalLoopService constructed before the one-shot MCP setDependencies, IPC
mirrors the TLDR read/control split (renderer steers, never creates), and
the changed broadcast is a payload-free invalidation ping so panes re-read
instead of racing two copies of the same loop state.
… - Refs #1001

Always-on per-pane status strip while a loop exists, latched control
overlay (pause/resume/raise-cap/stop), palette commands and a Settings
row. Final keybinding is Cmd+Shift+Y: Cmd+Shift+G is owned by
global-dispatch and check:keybindings rejects unapproved overlaps.
…re owner - Refs #1001

132 governed commands (goal-loop-preview, goal-loop-stop), the goal-loop
feature reference, and a guard in GoalLoopPane so partially-stubbed
window.api surfaces render nothing instead of crashing pane tests.
…nuations

Review of #1003 found the state machine could double-deliver, prompt an
agent mid-turn, and stall with nothing left to observe.

The backoff retry honoured retrySafe only for the immediate retry, so an
acceptance timeout after Enter re-sent a continuation that was probably
already running. An uncertain delivery now pauses the loop instead.

Working state was reduced only while a loop was active and seeded blank
at goal_loop_start. Claude publishes turn_completed per assistant message
and those events reach main on a 200 ms poll, so the completion of the
very message that called goal_loop_start arrived after the blank seed and
triggered continuation #1 inside that turn. The same gap left a loop
paused mid-turn permanently "working", so Resume never delivered. State is
now tracked for every session and the seed only fills in a non-working one.

Every trigger now goes through one request path that re-checks the tracked
state, which closes pause->resume during an in-flight delivery (delivered
twice) and a backoff retry dropped by the re-entrancy guard on a slow disk.

persist() evicted the newest entries rather than the oldest and never
bounded paused loops, which every removed session and restart mints.
start() also overwrote the corrupt file the store claimed to preserve;
unreadable storage is now moved aside first.

Adds a dismiss control action so an ended loop can be cleared, one shared
action union for main, preload and the service, and a cross-session
isolation test over a shared service, which the per-session-service
system test could not observe.

Refs #1001

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An ended loop is persisted and its strip sits over the pane's top line,
but it rendered no controls at all, so it could never be cleared. It now
offers Dismiss.

Raise cap sent maxContinuations + 25 unclamped; past 175 that exceeded the
IPC schema maximum, main rejected the request, and the rejection surfaced
only as an unhandled promise. The value is clamped to the shared ceiling
and the button is hidden once there is nothing left to raise.

The latched overlay is opaque over the whole pane and had no exit other
than the chord, so it gains a Close button. Escape is deliberately not
bound: in an agent pane it interrupts the running turn.

Refs #1001

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…departures

Off-by-default and the missing parked-agent wake both differ from the
approved spec and were not in the plan's list of deliberate refinements.
Neither was changed in review; writing them down makes them decisions to
confirm rather than accidents to rediscover.

Refs #1001

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

feat(goal-loop): harness-owned persistence loop that works until the goal is complete

1 participant