Skip to content

The session subsystem has a reading of itself — every open episode of the store and not only the one the pointer follows, the episodes that can no longer close themselves, the commit the serving process is answering about, and what each provider's adapter declares

Reading the lifecycle back is two different questions with two different answers, and until this existed only one of them had a surface. A worker asks *what am I resuming from?* and continuity.state answers it: one episode — the one .ai/local/state/session-current.yaml resolves to — one handover, one checkpoint, the blockers. An operator asks *is this subsystem working?* and that question is structurally unanswerable from the first, because the pointer is a symlink that the most recent start event re-aims. lifecycle.episodes, lifecycle.recovery, lifecycle.runtime, lifecycle.providers and lifecycle.closed read the store rather than the pointer and answer the second question, over MCP, over GET /api/v1/lifecycle/<name>, and on the Cockpit's Continuity page, which holds no session model of its own and is a projection of exactly these five and continuity.state.

declared statusdeclared status: guaranteed Declared deterministic and blocking, implemented, and naming the behavioural test that is to prove it. Whether a recorded run supports it is the evidence verdict, shown beside it.

recorded evidencerecorded evidence: not run Declared guaranteed, and not supported by a recorded run: the test is named, and no passing run of the current inputs is recorded. how evidence is recorded

continuity.state answers the worker's question about one episode, which is right for a briefing and blind to the other windows open on the same worktree, because state/session-current.yaml is a symlink the most recent start event re-aims. The five lifecycle capabilities read the store, the ledger and the index instead, decide every standing from what the repository can observe, and carry a remedy with every finding. They never write, they judge no thresholds — age is session.freshness and continuity.state's to report — and they declare no command line, because they read .ai/local/ and are served rather than shipped. The Cockpit's Continuity page is their projection and holds no session model of its own.

What it means

Reading the lifecycle back is two different questions with two different answers, and until this existed only one of them had a surface. A worker asks what am I resuming from? and continuity.state answers it: one episode — the one .ai/local/state/session-current.yaml resolves to — one handover, one checkpoint, the blockers. An operator asks is this subsystem working? and that question is structurally unanswerable from the first, because the pointer is a symlink that the most recent start event re-aims. lifecycle.episodes, lifecycle.recovery, lifecycle.runtime, lifecycle.providers and lifecycle.closed read the store rather than the pointer and answer the second question, over MCP, over GET /api/v1/lifecycle/<name>, and on the Cockpit's Continuity page, which holds no session model of its own and is a projection of exactly these five and continuity.state.

How it works

lifecycle.episodes lists .ai/local/state/sessions-open/*.yaml — one file per provider session — and gives each a standing decided from what the repository can observe: current when the pointer resolves to it, open when it names this worktree and the pointer names another episode, foreign when it names another worktree, stranded when the worktree it opened in is gone from disk and nothing can compose its record any more. Every episode carries a note saying which and why, and the ledger is joined to it by the episode id each line is stamped with — never by a time range, because one ledger holds several workers and no timestamp separates them. The same join gives the task relation, and it runs from the episode outwards: an episode names the tasks its lines touched, and no task decides whether the episode's own records are written.

lifecycle.recovery reports what nobody can fix by running the tool normally: an open record whose worktree has vanished or which the ledger already recorded as closed, with the command that clears it; the .tmp. files a close killed between writing and renaming leaves in the tracked sessions section, where they are untracked, invisible to every reader of the section and offered to the next git add .; whether session-current.yaml is a symlink into the per-provider store or the regular file of the layout that kept one episode per checkout; and the arithmetic ADR 0052 asks for — session.started against session.closed against the open records, where a positive difference is episodes whose end event arrived and produced nothing.

lifecycle.runtime compares the commit the served index was built at with the commit git reports on the call. Both are read; neither is assumed. A server that built its index once and held it answers every question about a commit from whenever it started, identically from the HTTP API, from MCP and from the Cockpit, with nothing in any answer saying the picture is old, and there is no way to notice that from inside one of those answers.

lifecycle.providers reports, per provider, the lifecycle events its adapter declares in the provider's own vocabulary, whether it can archive prompts, and which of this repository's enforcement entries name its hook in wired_by. The declaration is share/providers.yaml; test/cases/170 refuses it drifting from the adapter table the shell actually drives. The two obvious alternatives are both wrong: reading the provider's hook directory reports an installation as a capability, and a list written into a page is a second source of truth that goes stale the day an adapter changes.

lifecycle.closed reads the tracked records through the object index — kind session — and reports how many exist, how many closed on this branch, and the newest twenty with a link to each object page. It is the only half of the subsystem that survives a clone.

None of the five declares a command line. They read .ai/local/, so they are served to the worker in front of the checkout and never shipped: a command line is how a value reaches a script, a log and eventually a commit.

How to see it

majordomus serve                              # then open /cockpit/continuity
curl -s localhost:$PORT/api/v1/lifecycle/episodes  | jq '[.episodes[] | {session_id, standing, provider, last_event}]'
curl -s localhost:$PORT/api/v1/lifecycle/recovery  | jq '{balance, stranded: [.stranded[].session_id], orphans: [.orphans[].path]}'
curl -s localhost:$PORT/api/v1/lifecycle/runtime   | jq '{served_head, repository_head, agree}'
curl -s localhost:$PORT/api/v1/lifecycle/providers | jq '[.providers[] | {id, lifecycle, prompt_capture, wired}]'
curl -s localhost:$PORT/api/v1/lifecycle/closed    | jq '{total, on_this_branch}'

What it does not cover

It never writes. The lifecycle has one writer, the shell tool, and a second account of events the ledger already holds is what that design refuses. Nothing here closes a stranded episode or deletes an orphan; each finding carries the command a person runs.

It decides no thresholds. Nothing in lifecycle.* judges a record to be old. Age is session.freshness in the policy and continuity.state's to report; a second engine for it here would be exactly the duplicated numbers that make two surfaces disagree.

It does not know whether a provider is attached. Whether the client that opened an episode is still in its conversation is not a fact of this repository: the episode file records no process, the peer board records no episode id, and a client that exits without firing its end event leaves a file identical to one a live worker is using. stranded is reserved for the one case the repository can establish — the worktree is gone — and everything else is reported as where the file is and when the ledger last saw it.

It shows no prompt. The prompt archive is the one part of the local half that is a conversation, and no capability here and no card on the Continuity page reads it.

The page does not follow a live channel. The executions pages follow a WebSocket because an execution emits events; the session store does not, and a browser asking every few seconds whether an episode is still open would spend the day being told yes. A reload is the refresh.

Why it exists

On 2026-09-11 this repository held five open episodes in one checkout and every surface the tool has could name exactly one of them — continuity.state follows the pointer, and the pointer is aimed at whichever episode opened most recently. The four others were invisible, and an episode nobody can see is an episode nobody closes: the ledger's own arithmetic showed twenty-one starts against fifteen closes at the same moment. The same week, a forensic audit found the subsystem had written no checkpoint and no handover for six days while doctor reported health throughout — ADR 0052, "The session lifecycle is the episode's, not the task's" — and a long-lived server was found serving a six-hour-old HEAD from three surfaces with nothing saying so. Three failures, one shape: the subsystem had no reading of itself, so each of them was discovered by a person noticing something else.

Detail rendered from docs/claims/session-observability.md.

From the statement to the proof

  1. 1 · the public statement

    The session subsystem has a reading of itself — every open episode of the store and not only the one the pointer follows, the episodes that can no longer close themselves, the commit the serving process is answering about, and what each provider's adapter declares

    Made on State.

  2. 2 · the canonical claim

    session-observabilitydeclared status: guaranteed in docs/CLAIMS.yaml

    Defined in read it on this site · docs/CONTINUITY.md

  3. 3 · the implementation

    apps/majordomus-cli/src/capability/builtin/lifecycle.rs

  4. 4 · the test and the scenarios

    Test: test/cases/171_session_observability.sh

  5. 5 · the recorded runs

    recorded evidence: not run how evidence is recorded

Verify it yourself

The test runs in a disposable temporary repository and asserts the behaviour, not a string in the source.

from a clone of the repository
bash test/run.sh 171_session_observability

Where this claim is used

responsibilities it covers

Searches the titles and descriptions of every page this site publishes.