Skip to the content.

Perpetual sessions (the relay)

Status: concept note plus GitHub epic spine. Nothing here is shipped yet. The staged sketch below decomposes into the child issues listed under The rungs; the doctrine is written to be enforced the fak way — closed vocabulary, evidence over self-report, data over code, disjointness before fan-out, advisory before enforcement, fail-closed.

The problem

Some goals do not fit in a context window and never will: an overnight backlog drain, a migration across a thousand call sites, a benchmark harness that runs for days, a “keep this green” watch. The work is unbounded in the transcript even when it is perfectly bounded in the artifacts it produces (commits, ledger rows, issue closures).

The industry’s default answer is compaction: when the window fills, summarize the transcript in place and continue. Anthropic ships this server-side, and Claude Code fires it automatically near the limit. Compaction is the right tool for a conversation a human is steering. It is the wrong tool for a perpetual machine goal, for four reasons:

So for the perpetual use case we do not want a better compaction. We want no compaction. The thesis of this note:

A load-bearing fact is either a durable record outside the context (and is queried on demand), or it does not survive at all. The transcript is disposable. When a leg of the goal fills its window, it does not shrink — it ends cleanly and a fresh leg takes over, seeded only by a small typed baton and a query interface into the durable store.

We call this a relay: one goal, run as a sequence of bounded legs, each handing a baton to the next. The invariant we are buying is:

Flat context (the “O(1)” the operator asked about): peak resident tokens in a relay are bounded by the per-leg ceiling, independent of goal duration or total work done. A relay that runs for a week peaks no higher than one that runs for an hour.

Where the state of the art is

The pattern is not exotic; it is the hardened, witnessed form of things people already do by hand.

The one-line contrast: compaction and self-paging both ask a pressured model to decide what to remember. The relay never asks that question — it asks what did this leg durably ship, and that has a witnessed answer.

What fak already has (we are extending, not starting)

This is the crucial part: the substrate is largely built under the managed-context epic #1570. The relay is a mode over it, plus a few genuinely new gates.

Existing surface What it already gives the relay
internal/ctxplan (Plan, Faithful, ObjectivePin) Treats the turn as an O(1) view over a lossless store and already refuses lossy compaction (Faithful fails any plan that elides a span with no recovery handle). This is the relay’s philosophy already in code.
internal/sessionreset (BuildSeed, Contributor, Seed) Deterministic, de-LLM-ified carryover via a contributor registry (durabilityFacts, taskDistill, warmPrefix, verbatimTail). The relay is a stricter contributor policy over this seam.
session.Recontinue / Generation / ContinuationID / ParentTrace The re-arm verb and lineage: mint a fresh live session under a child trace, link to parent, Generation++, leave parent Stopped. A relay leg is a generation.
session.ResetTransaction The audit ledger of a reset: old/new trace, seed digest, contributors, omitted spans with reasons, budget re-arm. The baton’s provenance record already exists.
internal/sessionimage (fak.session.v1) Portable, integrity-checked offload/restore across host and model. The relay’s durable checkpoint format.
session.Descriptor + FileStore The small durable index of a session, outside the transcript.
session.TimeBudget / Envelope / Budget / DecideVerdict Multi-axis budget with closed stop-reason tokens (BUDGET_CONTEXT_EXHAUSTED, DRAINING, …) and wall-clock accounting that survives process restart. The relay’s rotation trigger reuses these axes.
Handoff-to-close chain (#1462, taskmgr.Handoff, dos commit-audit) An end-to-end, hermetically-tested path from a handoff through routing, commit, and witness. The relay’s close arm binds here.
internal/corelocks, guard, witness, safecommit Where “is it safe to stop right now” is already adjudicated.

The gap the relay closes is not persistence, lineage, or carryover — those exist. It is three things that do not yet exist:

  1. a pointer-only carryover policy (a baton that is O(1) and re-verified, not a distilled recap that grows with the work);
  2. a safe-stop-at-a-work-boundary predicate and an externalize gate that fails closed if any load-bearing fact lives only in the transcript;
  3. perpetual-goal semantics — a done-check, anti-thrash hysteresis, and a no-progress escape — so a relay terminates on the goal, not on the window.

Vocabulary

A closed, small vocabulary (DOS lesson #1). Recommended names, with alternatives called out in Open questions.

The baton schema

The baton is data, not prose (DOS lesson #3), and it is the least trusted signal in the system, so its progress half is never a claim — it is a cursor the next leg re-verifies (DOS lesson #2). A deliberately closed schema:

The field-level fak.relay.baton.v1 contract lives in RELAY-BATON-SCHEMA-2026-07-01.md; the JSON below is the short spine sketch.

{
  "schema": "fak.relay.baton.v1",
  "relay_id": "RLY-...",          // stable across all legs of the goal
  "leg": 7,                        // monotonic; == session.Generation
  "parent_trace": "…",             // lineage link (session.ParentTrace)

  "objective": {                   // == ctxplan.ObjectivePin, carried verbatim
    "pin_id": "…", "text": "…", "digest": "…"
  },
  "done_when": "…",                // how a fresh leg checks 'am I already done?'

  "progress_cursor": {             // RE-VERIFIED, never trusted. No 'claimed' field.
    "start_sha": "…",              // ground-truth anchor for dos_status/verify
    "ledger_ref": "…",             // intent-ledger id to re-read verified progress
    "held_region": ["glob", ]     // lease region to re-acquire (disjointness)
  },

  "next_action": "…",              // the single next step, one line
  "open_questions": ["…"],         // unresolved decisions (pointers, not essays)

  "artifacts": [                   // pointers into the durable store, that's all
    {"kind": "commit",  "ref": "…"},
    {"kind": "issue",   "ref": "#1234"},
    {"kind": "memory",  "ref": "slug"},
    {"kind": "ledger",  "ref": "…"},
    {"kind": "file",    "ref": "path/glob"}
  ],
  "do_not_rederive": ["#dead-end-1", "memory:tried-X"],  // pointers to closed paths

  "tombstone": {                   // the closing leg's typed death note
    "reason": "RELAY_ROTATED",     // closed token (see below)
    "at_sha": "…",
    "note": "…"                    // honest, short, witness-backed
  }
}

Two properties are load-bearing and mirror the fail-closed A2A digest shape of dos_status:

The relay lifecycle

One leg’s loop, with the new gates in bold:

  1. Reload + re-verify. Start a fresh window seeded only by the baton + a query tool. Re-verify the baton against git/ledger (dos_recall discipline). On mismatch, mark RELAY_BATON_STALE and rebuild the cursor from ground truth.
  2. Done-check. Evaluate done_when against the durable store first. If the goal is already satisfied, end the relay with RELAY_GOAL_DONE and write no new leg. (Idempotent restart: a relay that is done stays done.)
  3. Work. Do the next action and beyond, committing/ledgering/filing as normal. Every result lands in the durable store through the ordinary witnessed path — this is what makes the externalize gate cheap.
  4. Arm at threshold. When a rotation trigger crosses its soft mark (context %, turns, wall-clock, spend — reused Envelope axes), set RELAY_ARMED. Arming does not stop anything; it just says “rotate at the next safe point.”
  5. Reach a safe point. Continue only to the next quiescence boundary. Do not rotate mid-action.
  6. Externalize gate (fail-closed). Before rotating, confirm nothing load-bearing lives only in the transcript. If uncommitted/unfiled state exists, refuse with RELAY_NOT_EXTERNALIZED — commit/file it, or park.
  7. Write baton + tombstone, rotate. Project the pointer-only baton, stamp the tombstone reason, Recontinue into leg N+1. The old leg is Stopped; its transcript is discarded, not summarized.
  8. Hard ceiling. If the window ceiling is hit before a safe point ever arrives, fail closed: write a RELAY_PARKED_UNSAFE tombstone and stop. Never blow the window to keep going; a parked relay is resumable by a careful next leg or an operator, an overrun one is not.

Closed reason vocabulary (data-only until a floor consumes it)

Proposed tokens, in the DOS style — each emittable, verifiable, and refusable. Data-only until a named check opts into enforcing them (DOS lesson #6):

Authoritative summary + fix rows live in RELAY-REASON-VOCABULARY-2026-07-01.md; the table below is the short spine view.

Token Category Meaning
RELAY_ARMED advisory soft threshold crossed; rotate at next safe point
RELAY_ROTATED TRUE_DRAIN clean rotation at a safe boundary (normal leg end)
RELAY_GOAL_DONE TRUE_DRAIN done_when satisfied against the durable store; relay ends
RELAY_NOT_EXTERNALIZED STALE_CLAIM refuse to rotate: load-bearing state lives only in the transcript
RELAY_PARKED_UNSAFE OPERATOR_GATE hit the hard ceiling before a safe point; parked, needs careful resume
RELAY_BATON_STALE STALE_CLAIM reload re-verification found the baton no longer matches ground truth
RELAY_NO_PROGRESS OPERATOR_GATE N consecutive legs made no verified progress; stop and escalate

Thresholds and triggers

Rotation is two-phase (arm, then fire) so it never lands mid-action:

Anti-thrash / hysteresis (a failure mode in its own right, below): a leg must make some minimum verified progress before it is allowed to arm again, a relay caps rotations per wall-clock hour, and RELAY_NO_PROGRESS fires if K consecutive legs close with no forward progress_cursor movement — the relay stops and escalates rather than spinning fresh windows forever.

Safe-stop-point detection

A safe point reuses existing adjudication rather than inventing a new one. It is the conjunction:

The relay does not get a bespoke stop mechanism; it composes Draining + safecommit + a one-line-next-action predicate. The RELAY_ARMED → safe-point → externalize sequence is the only new control flow.

Lessons applied from DOS

The relay is deliberately built to the same doctrine as core-locks:

  1. Closed vocabulary. Rotation and tombstone reasons are a fixed set; a relay never refuses with prose.
  2. Evidence, not claims. The baton carries no claimed progress; the successor re-verifies the cursor from git/ledger before trusting it.
  3. Data, not code. Rotation policy (soft marks, caps, done-check hooks) lives in dos.toml/Envelope, not scattered constants.
  4. Disjointness before fan-out. A relay holds a lane across legs; the baton carries held_region so leg N+1 re-acquires the same lease and does not collide with peers on the shared tree.
  5. Advisory before enforcement. Ship as shadow first: emit “would rotate here” and score baton fidelity for weeks before any auto-fire. (This reuses the PreCompact shadow-hook posture already in the tree.)
  6. No spontaneous refusal from vocabulary alone. Adding the reason tokens blocks nothing until a named gate (the externalize gate) consumes them.
  7. Both lenses. The relay is also an optimization: flat context means a stable, cacheable steady-state prefix (system + O(1) baton), lower token spend than a growing-then-compacting window, and no context-rot accuracy decay.

Failure modes and anti-patterns

Failure Mitigation
Tombstone rot / stale handoff — baton says progress that git no longer reflects Re-verify at read (dos_recall discipline); RELAY_BATON_STALE forces re-derivation from the durable store.
Thrash — rotating so often no leg makes progress Hysteresis: min verified progress before re-arm; per-hour rotation cap; RELAY_NO_PROGRESS escape after K empty legs.
Hidden-state loss — a load-bearing fact lived only in the transcript The externalize gate fails closed (RELAY_NOT_EXTERNALIZED); rotation is impossible until it is durable.
Mid-action rotation — window hit during a tool call or half-commit Two-phase arm/fire; safe-point predicate; hard-ceiling parks rather than cuts.
Goal drift — successor pursues a subtly different goal Objective pin carried verbatim + content-digested (ctxplan.ObjectivePin, ReconcileObjective); surfaces a typed outcome on mismatch, never a silent rewrite.
Infinite relay — a goal with no real end condition done_when re-checked each leg; a relay is bounded by the goal or an operator max-legs/max-spend envelope.
Cache thrash — every leg pays a cold prefix Steady-state prefix is small and stable (system + baton), so it caches; contrast compaction, which rewrites the middle every time anyway.
Poison carryover — dead ends re-inherited Only pointers cross the boundary; do_not_rederive is a pointer index, so the fresh window sheds the confusion instead of blurring it forward.

Implementation sketch (phases)

Stageable without destabilizing the tree, mirroring the core-locks rollout:

Dogfooding and QA

A relay is only real if a long goal survives many rotations with its progress intact and its cost bounded. The QA bar:

The rungs

GitHub epic #1860. The 50 child leaves, by track — each a worker-ready-issue with lane, path hints, dependencies, a done condition, and a witness:

The rollup manifest with per-rung lane, paths, and dependencies is docs/milestones/perpetual-sessions-epic-tickets-2026-07-01.json.

Success criteria

The relay is working only if all hold:

Open questions

The doctrine

Compaction shrinks a pressured window by asking a degraded model what to remember. The relay never asks that question. It ends a leg cleanly at a safe boundary, makes the machine prove every load-bearing fact is already durable, hands the next leg a small typed baton of pointers it must re-verify, and throws the transcript away. The goal runs forever; the window stays flat; and nothing survives a rotation that could not survive a witness.