You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Marcus is an open-source system that takes a plain-English project description, decomposes it into tasks on a shared kanban board, and coordinates independent AI coding agents that pull those tasks, build them, and report back — communicating only through the board (decisions, artifacts, progress), never with each other.
This epic tracks the session-model migration: replacing how agents come to exist and how Marcus tracks them, plus the stabilization work sequenced around it.
Why (the user-visible motivation)
Today Marcus spawns one short-lived agent process per task (a claude CLI in a tmux pane, in its own git worktree, killed after one task). Analysis of all 22 open build-blockers (epic #701) showed 4 of the 6 run-killing bugs live partly or wholly in that spawning/babysitting layer — worktree explosions (#628), watchdogs killing healthy runs (#703), infinite respawn loops (#667), merge-recovery wedges (#700). Meanwhile agent platforms now provide process management natively.
The change: Marcus stops spawning anything. Long-lived agent sessions (started by a thin launcher outside Marcus's core) register once, then loop: pull a task → work → report → pull the next. Marcus tracks agents at exactly one level — the lease (task_id, expires_at, last_progress_at) — and stays deliberately blind to the OS process. Several of the worst bugs then get deleted instead of fixed.
PRE-3 gates (must complete before any Phase-3 code):
Keep/delete call-site inventory (advisory-panel review, Codex seat): grep-verified list, checked into this epic as a comment, of every durable-core touchpoint on (a) worktrees/<agent_id> path construction (e.g. task.py:3242 merge-recovery), (b) direct active_leases mutation outside the lease manager, (c) run_id as a schema/join key in cost_tracking ("core goes runless" deletes the ledger's join key — worker_ingester.py requires it on every token_events row), (d) experiment-scoped registration coupling in agent.py, (e) BOTH spawn_agents.py copies (experiments/ vs dev-tools/experiments/runners/) — each site mapped to a numbered migration change or explicitly marked "survives unchanged + why"
Implement ADR D11 fencing (PR docs: ADR 0012 amendment — D11 false-recovery fencing (the two-robots-one-chore rule) #710): lease_epoch on claim, epoch carried on reports, stale-epoch → preserve + reconciliation card (never discard), timeout ≥ 2× checkpoint cadence as config invariant. Verification scenario: falsely recover a live session; both report; exactly one completion, zero discarded work, one reconciliation card
Pull-loop worker prompt (delete "EXACTLY ONE task then exit"; honor retry_after; add an explicit project-drained stop signal)
Thin launcher/pool outside core (operator- or board-depth-controlled session count; replaces the per-task spawn loop)
Registration lifecycle: idempotent re-register + persist/rehydrate agent_status / agent_project_map (gap 1 — today a Marcus restart orphans every session)
Lease retune: durations for session-seam tasks, raise/remove max_renewals(10) / stuck_task_threshold_renewals(5) (gap 4), recovery cadence for silent deep work, session-aware stale-completion guard (gap 3 — a falsely-recovered live session must not have its work discarded)
Re-home cost/audit binding (gap 2 — attribution currently keyed off the spawn-registry file + per-agent cwd, both being deleted): record session→agent/run/project at register/claim time
Session context-management obligation (gap 5 — unbounded context growth across tasks in one process)
Remove merge EXECUTION from report_task_progress per the ADR decision; keep merge EVIDENCE
Phase 5 — Decompose at the session seam (waits on ADR seam definition)
Implement the seam test — cut only where the interface between two chunks can be written as a contract smaller than either chunk: retune should_decompose thresholds, remove the enterprise force-decompose rule, gate the second-level subtask split, reshape contract-first to genuine seams
Snake/one-card apps retired as coordination benchmarks (kept as regression tests)
Then measurement (board vs native orchestration) begins — not before.
Interim rule: no large experiments until Phase 3 lands — the shed bugs still bite live runs.
The 5 review-found gaps (must not be lost)
An adversarial code-review pass over the migration plan found five failure modes that only appear AFTER going session-based:
Registration doesn't survive restart — agent_status/agent_project_map init empty and are never reloaded (src/marcus_mcp/server.py:200-201; leases ARE reloaded, agent state is not — src/marcus_mcp/tools/task.py:3874-3876); with register-once sessions, a Marcus restart orphans every session (_scope_tasks_to_project raises at task.py:5150-5155).
Cost attribution loses its input — the resolver reads the spawn-registry file written by spawn_agents.py and disambiguates agents by cwd == per-agent worktree (src/cost_tracking/worker_ingester.py:26, 312-316); both are being deleted.
Stale-completion guard discards live sessions — the guard (task.py:3865-3960) assumes agents die on lease recovery; a long-silent session that trips recovery then reports 100% gets its entire coarse task thrown away.
Renewal ceiling trips healthy long tasks — max_renewals=10, stuck_task_threshold_renewals=5 (src/core/assignment_lease.py:211,219) flag a perfectly-heartbeating long task as stuck.
Unbounded context growth — no code path manages context accumulation across many tasks in one process (never needed under 1-process-per-task).
Where to look in the code first
File
Why
dev-tools/experiments/runners/spawn_agents.py
The spawn-per-task runner being replaced (worker prompt 854-972, spawn loop 1849-2061, worktrees 1374-1447, pane tracking 1598-1634)
dev-tools/experiments/runners/spawn_controller.py
compute_spawn_count, StallWatchdog, SpawnThrashDetector — retired in Phase 3
Phases 1: each fix lands with a regression test; pytest -m unit green; no new hangs in a small live run.
Phase 3: start Marcus + launcher with 3 sessions; kill and restart Marcus mid-run → sessions keep working (gap 1); kill a session mid-task → lease expires → another session claims (single-level liveness); token_events rows attribute to the right agent (gap 2).
Phase 6 gate: 5 consecutive unattended Shape-B runs, each ending with the behavioral gate passing and zero manual rescues.
Glossary
Term
Meaning
session
a long-lived agent CLI process (claude/codex/gemini/human) that registers once and loops pulling tasks
launcher / pool
thin ops layer OUTSIDE Marcus core that starts N sessions; Marcus spawns nothing
lease
Marcus's only tracking of an agent: (task_id, expires_at, last_progress_at); heartbeat = liveness
seam
a boundary where the interface between two chunks can be written as a contract smaller than either chunk — the only place decomposition cuts
Shape B benchmark
the smallest workload exercising every between-card mechanic: contract → 4 components → integrate+verify
shed / keep / split
verdicts from the layer triage of epic #701: delete with the runner / durable-core fix / half each
What is this system, briefly
Marcus is an open-source system that takes a plain-English project description, decomposes it into tasks on a shared kanban board, and coordinates independent AI coding agents that pull those tasks, build them, and report back — communicating only through the board (decisions, artifacts, progress), never with each other.
This epic tracks the session-model migration: replacing how agents come to exist and how Marcus tracks them, plus the stabilization work sequenced around it.
Why (the user-visible motivation)
Today Marcus spawns one short-lived agent process per task (a
claudeCLI in a tmux pane, in its own git worktree, killed after one task). Analysis of all 22 open build-blockers (epic #701) showed 4 of the 6 run-killing bugs live partly or wholly in that spawning/babysitting layer — worktree explosions (#628), watchdogs killing healthy runs (#703), infinite respawn loops (#667), merge-recovery wedges (#700). Meanwhile agent platforms now provide process management natively.The change: Marcus stops spawning anything. Long-lived agent sessions (started by a thin launcher outside Marcus's core) register once, then loop: pull a task → work → report → pull the next. Marcus tracks agents at exactly one level — the lease (task_id, expires_at, last_progress_at) — and stays deliberately blind to the OS process. Several of the worst bugs then get deleted instead of fixed.
The plan (six phases, strict order)
Target calendar (set 2026-07-06): Phase 1 remainder → Jul 19 · Phase 3 landed (launcher + pull-loop + D11) → Aug 2 · Phase 4 → Aug 30 · Phase 5 → Sep 13 · Phase 6 gate (5 unattended Shape-B passes) → Sep 27. Dates are targets, reviewed weekly against actual weekend output.
Phase 1 — Board-protocol run-killers (re-architecture-safe; start immediately)
request_task_redoMCP tool (recovery enabler)get_attachmentscontract (same shape as merged fix(context): Codex P1 + P2 on #622 release PR #623)Phase 2 — The boundary ADR (gates Phases 3 and 5 only)
log_decisiongate) · session-seam definition · pool-sizing authority · session↔project cardinality · recovery of long partial work · internal-subagent accounting · fate of "a run"Phase 3 — Session MVP: launcher + pull-loop (deletes bugs instead of fixing them)
PRE-3 gates (must complete before any Phase-3 code):
worktrees/<agent_id>path construction (e.g.task.py:3242merge-recovery), (b) directactive_leasesmutation outside the lease manager, (c)run_idas a schema/join key incost_tracking("core goes runless" deletes the ledger's join key —worker_ingester.pyrequires it on everytoken_eventsrow), (d) experiment-scoped registration coupling inagent.py, (e) BOTHspawn_agents.pycopies (experiments/vsdev-tools/experiments/runners/) — each site mapped to a numbered migration change or explicitly marked "survives unchanged + why"lease_epochon claim, epoch carried on reports, stale-epoch → preserve + reconciliation card (never discard), timeout ≥ 2× checkpoint cadence as config invariant. Verification scenario: falsely recover a live session; both report; exactly one completion, zero discarded work, one reconciliation cardretry_after; add an explicit project-drained stop signal)agent_status/agent_project_map(gap 1 — today a Marcus restart orphans every session)max_renewals(10) /stuck_task_threshold_renewals(5) (gap 4), recovery cadence for silent deep work, session-aware stale-completion guard (gap 3 — a falsely-recovered live session must not have its work discarded)report_task_progressper the ADR decision; keep merge EVIDENCEPhase 4 — Verification moat (survives everything; the product)
Phase 5 — Decompose at the session seam (waits on ADR seam definition)
should_decomposethresholds, remove the enterprise force-decompose rule, gate the second-level subtask split, reshape contract-first to genuine seamsPhase 6 — The gate
Interim rule: no large experiments until Phase 3 lands — the shed bugs still bite live runs.
The 5 review-found gaps (must not be lost)
An adversarial code-review pass over the migration plan found five failure modes that only appear AFTER going session-based:
agent_status/agent_project_mapinit empty and are never reloaded (src/marcus_mcp/server.py:200-201; leases ARE reloaded, agent state is not —src/marcus_mcp/tools/task.py:3874-3876); with register-once sessions, a Marcus restart orphans every session (_scope_tasks_to_projectraises attask.py:5150-5155).spawn_agents.pyand disambiguates agents by cwd == per-agent worktree (src/cost_tracking/worker_ingester.py:26, 312-316); both are being deleted.task.py:3865-3960) assumes agents die on lease recovery; a long-silent session that trips recovery then reports 100% gets its entire coarse task thrown away.max_renewals=10,stuck_task_threshold_renewals=5(src/core/assignment_lease.py:211,219) flag a perfectly-heartbeating long task as stuck.Where to look in the code first
dev-tools/experiments/runners/spawn_agents.pydev-tools/experiments/runners/spawn_controller.pycompute_spawn_count, StallWatchdog, SpawnThrashDetector — retired in Phase 3src/marcus_mcp/tools/task.pyrequest_next_taskno-task branch (2745-2793), stale-completion guard (3865-3960), merge execution (3395-3571, 4447)src/core/assignment_lease.pysrc/marcus_mcp/tools/agent.pyregister_agentephemerality assumptions (16-171)src/marcus_mcp/coordinator/decomposer.pyshould_decomposethresholds (18-215) — Phase 5src/cost_tracking/worker_ingester.pyHow to verify it works
pytest -m unitgreen; no new hangs in a small live run.token_eventsrows attribute to the right agent (gap 2).Glossary
Related
9479b614(migration change set),2f062a90(drop product-wedge; realign to core),fc0737eb(original Build-blocker fix plan (2-week triage): what stops Marcus from building projects #701 plan)