0080: Active-loop registry v2 + per-session Claude/Codex lanes
0080: Active-loop registry v2 + per-session Claude/Codex lanes
- Status: accepted
- Date: 2026-05-27
- Deciders: User, Claude Code as implementer
- Related: #1588, active-agent-loop ops doc, ADR 0007, ADR 0066, ADR 0079, ADR 0005
Context
agent_loop.py active-loop already ships two session topologies — the default
four-role and the optional expanded-eight — plus the Conservative Gate that
only lets --execute call the ship runner when the required reviewer/auditor
sessions report a pass-class heartbeat. What it does not have is a layer that lets
Claude and Codex actually collaborate inside those sessions.
The operating model the user wants is: 8 durable role sessions, each able to run a
Claude lane and a Codex lane (up to 16 lanes, not 16 simultaneous workers); only
ship-blocking core roles enforce the gate; Planner/Experiment are idle/on-demand
and non-blocking; only the Implementer owns the default write lease; and the
Claude:Codex split is balanced by Work Unit (WU) over a rolling window, not by
session count. Crucially, dual-agent is meant to be a lane policy, not a third
topology — adding a four-role-dual-agent enum would fork the topology table and
the gate logic for no benefit.
The registry that backs all of this (reports/agent_loop/active/session_registry.json)
is schema_version: 1: a list of single-agent session dicts with no place to
record which agent ran, how WU is accumulating, or which session may write. The
contract has to grow without breaking the merged four-role behavior or its tests.
Decision
Introduce registry schema_version: 2 that layers Claude/Codex lanes onto the
existing topologies. Dual-agent is expressed purely as a lane policy; the
--topology enum stays four-role | expanded-eight.
Per-session additions (sessions stays a JSON list for back-compat):
lanes:{claude: {...}, codex: {...}}, each lane carryingagent,status,current_turn,wu_spent_rolling.write_lease_owner:trueonly for theImplementersession.ship_gate: the session’s Conservative-Gate class —lease-owner,blocking,non-blocking, orcontrol-plane.
Top-level additions:
gate_policy: "conservative".agent_mix:{target, unit: "work_unit", window: {type: "rolling_tasks", size}, max_allowed_skew_wu}, the WU-balance policy. The CLI accepts--agent-mix claude=5,codex=5, and the rolling ledger lives inreports/agent_loop/active/agent_mix.json.
Compatibility and surface rules:
- A v1 registry is lifted to v2 on read (lanes/
write_lease_owner/ship_gatesynthesized,gate_policy/agent_mixdefaulted).four-roleoutput is unchanged in role set, lease ownership, and gate behavior. session-heartbeat --agent claude|codexupdates a single lane; omitting--agentkeeps the prior session-level heartbeat.- The implementer write lease gains
lease_type: "write"andactive_agent(null until a lane borrows it in a later phase; Claude and Codex never hold it at once). scripts/agent_loop.pyis not promoted toLOAD_BEARING_PATHSin this phase. At Phase 1-2 the loop only writes an advisory dry-run ledger and never touches the retrieval/verifier/answer/eval runtime, so §5b real-data delta would be a perpetual vacuous attestation. The contract is instead pinned by this ADR plus thetests/test_agent_loop.pysuite. Promotion is deferred to the Phase 5 Orchestrator-only ship-executor, where a bug carries real ship blast radius.
This ADR fixes the v2 registry/lease/agent_mix contract as a new reviewer-facing coordination surface. Read-only lane execution adapters and WU accounting land in Phase 2; patch-proposal, mutating-writer, and ship-executor isolation follow in Phases 3-5.
Consequences
- Reviewers and downstream tooling can rely on a stable v2 shape: lanes,
ship_gate,write_lease_owner,gate_policy, andagent_mixare contractual. four-rolecallers and their tests keep working becausesessionsstays a list and v1 is lifted, not rejected.- WU becomes the unit of Claude:Codex balance, so later phases measure mix skew
against
agent_mix.jsonrather than counting sessions. - The active ledger (
session_registry.json,leases.json,agent_mix.json,events.jsonl) remains gitignored and must stay privacy-safe per ADR 0005: no raw private question/answer text,doc_id,chunk_id, filenames, prompt bodies, or absolute private paths. - Because agent_loop.py stays out of LOAD_BEARING_PATHS, this PR needs no §5b; the load-bearing decision is revisited when the loop can actually ship (Phase 5).
Alternatives considered
- Make
sessionsa map keyed by session id. Rejected: it would rewrite ~10 merged tests and the_write_active_registryhelper for no behavioral gain. A list plus per-itemlanespreserves back-compat and the “four-role unchanged” requirement. - Add a
four-role-dual-agenttopology enum. Rejected: dual-agent is orthogonal to topology. Forking the enum would duplicate the topology table, gate map, and next-command logic. A lane policy layered on both topologies is simpler and keeps the gate single-sourced. - Introduce a parallel
LOAD_BEARING_GLOBSlist for agent_loop.py. Rejected: CLAUDE.md mandates_governance.LOAD_BEARING_PATHSas the single machine-readable source. A second list would drift. (And we defer adding agent_loop.py to that one list until Phase 5.) - Promote agent_loop.py to load-bearing now. Rejected for this phase: at dry-run/read-only the loop has no model-path blast radius, so §5b would carry no signal. Deferred to the ship-executor phase.