Skip to content

[Epic] I0 P5: Session Record: How a Run Is Started, Written and Continued #533

Description

@m2ux

Summary

A session's state lives in a file the server rewrites on every call, with a sealed envelope beside it binding those exact bytes to the server's signing key. That arrangement is why resuming a run rebuilds it instead of continuing it, why a rotated key reads as tampering, and why continuing a run that already exists is hard enough that the first attempt at it was reverted.

This epic replaces the rewritten snapshot with a record that is appended to and folded. State becomes what you get by replaying it. Resuming is replay; continuing an existing run is free, because nothing is ever overwritten; and a key that cannot be established degrades the claim about who wrote an entry rather than making the content unreadable.

It sits after the program that walks the definition in #527, because that program is what produces a durable position for the record to carry. It is the smallest of the migration epics — three items — because two of its three problems are already fixed by the change of shape rather than by work aimed at them.

The three gaps

Continuing a run means rebuilding it. The first stage of the transition made a second dispatch into an occupied planning folder refuse rather than overwrite the run it finds. That converted silent loss into a visible error, which is all it was meant to do. What it did not do is let the run continue, which is what the caller actually wanted. Measured on a real work package before the refusal landed: a cursor and one completed activity before, an empty cursor and none after, five history events reduced to two, the same identifier handed back throughout.

Six faults sit behind a straightforward continuation, each found by reviewing a first attempt that was written and then taken back out, and each only visible once the previous was fixed. A session that cannot be read is not a session that is absent — the likeliest cause is a rotated key, which leaves the content perfectly intact. Whether a child has finished is not recorded where a continuation looks, so reading it resumes a completed workflow onto its close-out activity and runs it a second time, observed on the real corpus with completion recorded twice. A position can point somewhere that cannot be entered — a terminal marker, or an activity the workflow no longer declares. The identifier is recorded in two places and only one resolves it, which surfaces once the folder has moved. A run abandoned at an unanswered gate is the most likely thing to continue and the hardest, because every tool refuses until the gate is answered and every step that could answer one is gated on a worker's result. And order inside the folder is load-bearing, since an identifier is derived from the slot a child sits in.

Under an appended record, four of the six cannot occur: an unreadable record is distinguishable from an absent one, a child's completion is in its own record, the identifier is the record's identity, and slots stop deriving identifiers. Two remain genuine and are settled here.

A rotated key reads as tampering. The seal covers the whole file, so when the signing key changes, verification fails and the error surfaces as a mismatch — the signature usually read as the state having been edited outside the server. The content is perfectly intact and the folder is clean; nothing in the failure says so. The same conflation reaches further than it should: a refusal recorded as a history event pins a session to the server version that wrote it, because loading validates every event against a strict list, and an older server meeting an event it does not know fails that validation as a mismatch too.

A key that cannot be placed is explained for one reason out of several. Creating the key directory and writing the key file can go wrong in one way that matters to an operator — the server cannot establish a key there — but the guidance covers only the permission-denied case and stays silent for the rest, including a read-only filesystem and a path component that is really a file.

The work

W1 — The session becomes an appended record. Entries are appended and never rewritten; state is a fold over them. Each entry is chained to the one before, so tampering is visible without a seal over a mutable whole, and a key that changes degrades the claim about authorship rather than the readability of the content. The position the preceding epic produces — the current step with a frame per enclosing loop — lives here, so a fold reaches a place a run can be resumed from. The throwaway-session-then-promote machinery retires with the change, along with the process-local registry the code documents as fragile across restarts and which has already mis-promoted a session once.

W2 — Continuation replaces refusal. A second dispatch into an occupied folder continues the run it finds, with its position, completed activities and variables intact, and the response distinguishes a continued run from a first dispatch. A finished run is not resumed; a new one starts beside it, and every prior run keeps its place. The two faults the record does not dissolve are settled explicitly: a position naming an activity the workflow no longer declares does not strand the run, and a run abandoned at an unanswered gate is either continued or refused with the reason stated plainly. The arguments a caller passes on a continuation are honoured rather than silently ignored.

W3 — A failed key write says what actually happened. The two paths that create the key directory and write the key file report whatever the filesystem returned, rather than first asking whether it was permission denied — nothing is lost for diagnosis, because the message already carries the underlying code and text. Reading the key file stays as it is: a missing file means the key has not been minted yet and the server should carry on and create it. The two failures a test process cannot easily arrange from disk are covered by injecting them rather than provoking them. And with the chained record in place, the message distinguishes a key that cannot be established from a record that does not verify, which are the two things the current error conflates.

Why now is cheap

Four of the six known faults are dissolved by the change of shape rather than fixed. The design starts from what has to be true rather than from a blank page, and the two that remain were paid for once already by the reverted attempt whose review rounds are on the record.

The position already exists by this point. The preceding epic produces it; without that, an appended record would have nothing to fold to that a run could resume from, which is why this epic sits where it does rather than earlier.

The refusal is already in place, so nothing is losing work while this is built. The first stage of the transition bought exactly that.

Acceptance criteria

  • The session is an appended, chained record and state is a fold over it; no entry is rewritten.
  • A rotated or lost signing key is reported as a failure of the authorship claim, with the content still readable, and is distinguishable from a record that does not verify.
  • The durable position, with a frame per enclosing loop, lives in the record, and a fold reaches a resumable place.
  • The throwaway-session-then-promote machinery and the process-local registry are gone.
  • A second dispatch into a folder holding a running child continues that child, with position, completed activities and variables intact; a finished child is not resumed, a new one starts beside it, and every prior child keeps its place.
  • A continued run is visible as such in the response and in the folder's history, distinguishable from a first dispatch.
  • A position naming an activity the workflow no longer declares does not strand the run, and a run abandoned at an unanswered gate is continued — or refused with the reason stated.
  • The identifier handed back resolves, including after the folder has moved, and arguments passed on a continuation are honoured rather than ignored.
  • A failed key write reports the underlying failure whatever its cause, with the two hard-to-provoke cases covered by injection.

Non-goals

  • Changing what a session records. The shape of the record changes; the facts it holds are the ones it holds today, plus the position.
  • Resuming through a persistent parent. That path appends a second child rather than continuing the first, so it overwrites nothing and loses no work.
  • The refusal shipped in the first stage. It stays until W2 replaces it, and W2 is not a prerequisite for anything else.
  • Dependency pinning and the known-bad denylist. The other half of deployment hardening is unaffected by the record's shape and stays where it is.
  • Migrating existing sessions. Records written under the current shape are read as they are; the fold handles both. No rewrite of anything on disk.

Tracking

Each work item is delivered as its own pull request when picked up.

Work item Agent time Gate
[ ] W1 — the session becomes an appended record 10–16 h Mechanical-execution W1, for the durable position
[ ] W2 — continuation replaces refusal 6–9 h W1
[ ] W3 — a failed key write says what happened 1–2 h W1, for the two-cause distinction
Epic total 3–5 days

Item hours and epic effort-days are defined in #527.

Three items, the smallest of the migration epics. That is a consequence of the record's shape rather than of the problem being small: four of the six documented continuation faults stop being reachable, so what remains is the record itself, the two genuine questions, and one error message. The hours per item are high for the same reason — each of the three is substantial, and there are few review cycles to add on top.

Carries #401 W3 in full — the reduced refusal having shipped in the safe-ground epic — #526 W4, and #437 W2. #401 and #526 are closed and captured in the planning folder; #437 stays open for its dependency-pinning item.

Investigation detail

The measured damage, each of the six constraints with the evidence behind it, and the corpus-side gate semantics verified against the evaluator:
2026-08-04-session-reattach

The reverted first attempt is on the batched-dispatch branch, and its revert commits state what each fault was and why the direction was wrong. The twenty-seven decisions settled over four code-level deep dives: deep-dive decision record. The running order and item-by-item disposition: 2026-08-31-typed-execution-redesign.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or requestpriority: mediumDo after the high-priority groundwork lands

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions