Skip to content

Persistent agency seams: on-by-default steering, idle loop with backoff, journal fork/merge (headlong comparison) #1015

Description

@drewstone

Read-only survey of agent-runtime 0.174.0 against laude-institute/headlong's persistent-agency loop, driven by a measured failure in discovery-lab.

The measured problem

Across every director-v2 run with a ledger (discovery-lab, 2026-08-22..24), 8 of 8 steer-continuity edges died with runtime-has-no-inbox. Directors direct only at spawn; mid-run supervision has never delivered a message.

Finding 1: that is a config omission, not a runtime bug

Scope.send (src/runtime/supervise/scope.ts:1067-1073) delivers only if the child executor exposes deliver. The sandbox executor builds an inbox only under the opt-in branch (src/runtime/supervise/runtime.ts:1379-1400, if (seam.steering)); the fallthrough singleShotDriver has no deliver key. Same for provider. Nothing in src/ sets steering by default; the only sites are tests (tests/runtime/mid-flight-steering.test.ts:262-268).

Proposal: either default steering on for sandbox/provider workers with a documented box-held-across-turns cost, or make steer_agent refuse loudly at spawn time when the target has no inbox, instead of reporting delivery failure after the fact. Today a director can spend a whole run steering into a void and only the ledger notices.

Finding 2: no persistent idle loop exists

Every loop is run-until-settled: tool-loop.ts:203-235 returns the moment the model replies with no tool call; supervisor.ts:699-734 tears down all live children when root act() returns. No heartbeat/tick/backoff anywhere (the only idle read-models, progress.ts:80-83 and stop-rules no-progress/all-stalled, END runs on idleness).

Headlong's liveness is a timer with exponential backoff (5s doubling to a 300s cap, reset by any real event; measured $1-2/hour idle). Smallest transplant: an onIdle hook in runBrainLoop (tool-loop.ts:218) returning 'settle' | { resumeAfterMs }, implemented in coordination-driver.ts:1032 as drain inbox -> wait on the coordination bus or a backoff timer via existing Scope.wait('timer') (scope.ts:1091) -> push an operator turn and continue; journal each sleep as waiting/woken so restart resumes the schedule; plus a wait coordination verb so a brain can choose to sleep (today await_event burns an inference turn per 15s poll). Constraints: must live inside act() or the join barrier kills the tree; must not extend the pool deadline (scope.ts:1108-1110); idle turns should be excluded from the equal-compute unit (tool-loop.ts:154).

Finding 3: journal fork/merge is partial

Journals are append-only with parent pointers (durable/spawn-journal.ts, types.ts:1049-1270); sandbox box fork exists (sandbox-lineage.ts:158-215); no history fork/merge. Smallest: forkTree(srcRoot, upToSeq, newRoot) copying the seq <= upToSeq prefix plus a forked-from event; merge as a union of settlements keyed by assignmentId/key with a merged-from event. Blobs are content-addressed so nothing copies. Risks: re-seq for replaySpawnTree, bridge session re-attach via assignmentId (supervise.ts:206-216), budget double-count (supervisor.ts:769-790).

What NOT to import from headlong

Its "self-improvement by fork, test, merge" and goal generation are README text with no code path (the dedicated thinkers were built and removed in Aug 2026). agent-runtime already has what it lacks: promotionGate (paired bootstrap, minPairedTasks=6), approval-gated activation (improvement-cycle.ts:988-1007), and the invariant that the measured thing never writes its own grade (wave-types.ts:396-401). Keep promotion outside act().

Full seam survey with file:line evidence is in the discovery-lab session log; happy to attach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions