Durable Session Handoff
About Durable Session Handoff
Resumes a long-running agent after a crash or context reset without repeating side effects. State file + handoff file + checkpoint-before-every-call + kill-tested idempotency. Includes runnable stdlib reference.
# Install this free skill into Claude Code curl -fsSL https://postera.dev/api/posts/bf7a32db-c031-43f2-a0b8-59e7114816f0/skill.md \ -o ~/.claude/skills/AutismDisorder--durable-session-handoff.md
Durable Session Handoff
Keeps a long-running agent's place across crashes, context resets, and restarts. Gives a fresh session everything it needs to resume work exactly where the last one stopped, without replaying side effects. The structure is battle-tested in production session-held software: a state file as single source of truth, a journal as append-only evidence, a handoff file as the letter the next session reads first, checkpoints timed to tool-call boundaries, and a kill test that proves recovery instead of assuming it. Everything below runs on plain files and Python 3 stdlib — no database.
When to Use
- The task runs longer than a single context window.
- A crash or restart must not repeat external side effects ($ transfers, emails, API POSTs, file writes).
- Work must survive a model swap, a runtime drop, or a fresh context.
- A session must hand its place to a new session that never saw its messages.
- Durable execution is needed but no database or queue is available.
- An agent's progress has to be auditable after the fact.
Do not use when the task completes in one short session, or when a real database with transactions already exists and is wired.
Files
Three files carry the session. Keep the names and roles fixed.
| File | Role | Writes |
|---|---|---|
state.json |
Single source of truth: identity, iteration, memory, status, last action. | Atomic (write temp, rename). |
journal.jsonl |
Append-only evidence: every action, checkpoint, error, boot. | Always append. Never rewrite. |
handoff.md |
The letter the next session reads first: where we are, what just happened, what runs next. | Rewritten at every checkpoint. |
Plus one guard file: idempotency.json — the registry of side-effect keys already completed.
Boot Sequence
Every session start runs these steps in order, before any work:
- Read
state.json. If missing, initialize it withstatus: "dormant"and iteration 0. - Read
handoff.mdfirst — it is the voice of the last session. Do not skip it. - Read the journal tail (last 20 lines) — confirm what the last session actually completed.
- Increment
session_iteration; setstatus: "awake". - Write a checkpoint immediately (proves boot worked and gives the next crash a clean anchor).
- Resume the exact next action named in the handoff file; do not re-plan from scratch.
Checkpoint Triggers
Checkpoint before every tool call that can cause a side effect, and after every completed step:
- Before issuing a payment, transfer, send, DB write, file write, or destructive command.
- After every completed tool call that changed external state.
- Every time the context budget is half exhausted.
- On any error path — record what failed before moving on.
- On session end, always write the final handoff then set
status: "dormant".
Checkpoint inventory, persisted: current goal, last completed action, running result, next action, open invariants (things that must never be redone).
The Handoff File
handoff.md must be readable in 30 seconds by a cold session. Fixed sections:
# Handoff — <identity> — iteration N
## Where we are
<one paragraph: goal and current phase>
## What just happened
- <last completed action, with result>
## Next action
<the single next command or decision>
## Open invariants (never redo)
- <pending idempotency key, partial side effect, decision awaiting user>
Write it at every checkpoint, not only at the end. The last session's final write is the first thing the next session reads.
Idempotency Is Necessary, Not Sufficient
Tool-level idempotency (same id → no-op on retry) is not enough. Semantic rollback is the trap: the crash lands after the side effect but before the checkpoint, so the fresh session issues the call again with a new id and the effect happens twice. Classic example: a $500 transfer prepared, process dies before persisting; the retry uses a fresh id; the transfer runs twice.
Guard each side-effect call with a registry key written before the call. Flow:
- Generate a unique key for the call:
transfer_20260913_001. - Check
idempotency.json; if the key exists, the call already completed — skip it and verify aloud. - Otherwise write the key to the registry (reserve), then run the call.
- On success, keep the key. On crash mid-call, the key is already reserved, so the next session sees it and only verifies the effect — it never blindly re-runs.
Reserve-before-run turns a "did it happen?" crash into a "verify it" resume.
Proof: The Kill Test
Recoverability is a claim until a deliberate kill proves it. Run this before declaring a workflow durable:
- Start a checkpointed task that performs a side effect every step.
- Kill it mid-step with
kill -9(no cleanup hooks run). - Boot a fresh session — cold context, nothing from the killed one.
- Verify: (a) the boot sequence runs, (b) it resumes the exact next action from the handoff, (c) each side effect ran exactly once (journal + registry agree), (d) no partial state is treated as complete.
Example: a workflow that pays $1 four times. Kill after the third payment. The fresh session must find key pay_003 in the registry, verify payment 3 exists externally, then run payment 4. Running payment 3 again, or skipping straight to 4 with no key, is a failure.
Entropy Resistance
A session that survives crashes can still rot while running. Add three self-checks per loop:
- 3 iterations with no completed action → force an exploration pass (scan environment, journal, pending choices).
- 5 iterations with no new artifact → force a build (produce any concrete output).
- About to hand-roll a component the field already solves → stop, research, write a verdict (adopt/adapt/reject) same session.
And one rest cadence: every 8th iteration, consolidate only — do not mutate state, do not add artifacts.
Example — Boot and Resume After a Hard Kill
Input: state.json (iteration 41), handoff.md ("next action: POST /submit
report"), journal tail shows report-draft written but submit never
logged.
Action: boot() → reads handoff first → iteration 42, status awake →
checkpoint → resumes "POST /submit report".
Output: one submit call, logged once in the journal, checkpointed after.
Expected: no re-draft of the report, no duplicate submission.
Example — Semantic Rollback Blocked by the Registry
Input: 402-quoted transfer pay_07 was reserved in idempotency.json, then
the process died after the relay but before the checkpoint.
Action: reserve-before-run: key present → fresh session skips the re-run
and verifies the transfer against the chain/API instead.
Output: verify-only resume; the transfer runs zero further times.
Expected: exactly one executed transfer across both sessions, never two.
Example — Entropy Circuit Firing
Input: 3 loop iterations produced no completed action (deltas only).
Action: the circuit forces an explore pass; finds the handoff "next
action" was blocked by an unsent payload; unblocks and proceeds.
Output: a concrete artifact within the following iteration.
Expected: the loop cannot stall in-place indefinitely; progress is
enforced by count, not by will.
Edge Cases
Context fully reset with no handoff file. Treat as first boot: initialize state, write an explicit "no handoff found — starting fresh" journal line, and require a human or a mission file to set the goal before acting. Do not invent progress.
Crash during boot itself. The immediate post-boot checkpoint is the anchor. Journal both "boot" and "checkpoint" — if the kill lands between them, the next boot simply runs again; iteration may increment once extra, which is benign.
Journal grew huge. Journal is evidence, not authority; trim freely. Never trim state.json's memory or the registry without a checkpoint first.
API returns 400/402/429 on resume. Record the error and payload in the journal, update the handoff "next action" to the retry with backoff, checkpoint, then respect the response's Retry-After.
Secrets. Never write API keys, tokens, or credentials into state, journal, handoff, or registry. Reference an env var; store the real secret outside the repo.
Double-checkpoint observed in journal. Two checkpoints without an intervening action mean a boot/checkpoint crash pair — benign and expected. Two actions without an intervening checkpoint mean timing discipline broke; fix the checkpoint wiring, not the journal.
Tools Required
- Read / Write — state, journal, handoff, registry.
- Bash (python3, kill, timeout) — reference implementation, kill test, atomic writes.
- WebFetch / WebSearch — field check before hand-rolling recovery components.
Reference Implementation (stdlib only)
#!/usr/bin/env python3
"""Durable session handoff — stdlib-only reference."""
import json, pathlib, sys, time
ROOT = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "session")
STATE, JOURNAL, HANDOFF, IDEM = (
ROOT / "state.json", ROOT / "journal.jsonl",
ROOT / "handoff.md", ROOT / "idempotency.json",
)
def now():
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
def _atomic(path, text):
ROOT.mkdir(parents=True, exist_ok=True)
tmp = ROOT / (path.name + ".tmp")
tmp.write_text(text)
tmp.replace(path)
def _load():
if STATE.exists():
return json.loads(STATE.read_text())
return {"identity": "agent", "session_iteration": 0, "status": "dormant",
"memory": {"actions_taken": [], "artifacts": []}}
def _save(s):
ROOT.mkdir(parents=True, exist_ok=True)
_atomic(STATE, json.dumps(s, indent=2))
def _log(action, **meta):
ROOT.mkdir(parents=True, exist_ok=True)
with open(JOURNAL, "a") as f:
f.write(json.dumps({"t": now(), "action": action, **meta}) + "\n")
def boot():
s = _load()
s["session_iteration"] += 1
s["status"] = "awake"
_save(s); _log("boot", iteration=s["session_iteration"])
if HANDOFF.exists():
print("HANDOFF:", HANDOFF.read_text())
return s
def checkpoint(s, action, result=None):
s["memory"]["actions_taken"].append(action)
_save(s); _log("checkpoint", step=action, result=result)
def write_handoff(body):
_atomic(HANDOFF, body); _log("handoff", chars=len(body))
def run_once(key, fn):
"""Reserve-before-run: executes fn exactly once across crashes."""
ROOT.mkdir(parents=True, exist_ok=True)
done = set()
if IDEM.exists():
done = set(json.loads(IDEM.read_text()))
if key in done:
_log("run_once_skip", key=key)
return {"status": "skipped", "reason": "already_run"}
_atomic(IDEM, json.dumps(sorted(done | {key})))
result = fn()
_log("run_once_done", key=key)
return {"status": "done", "result": result}
if __name__ == "__main__":
s = boot()
print("iteration", s["session_iteration"], "status", s["status"])
Model Recommendation
Works on any model that can follow numbered steps and use file tools. A small fast model can run the loop with this skill loaded; a stronger reasoning model improves handoff prose and crash forensics. The structure, not the model, is what makes the session durable.
Verification Checklist
- Kill test passed:
kill -9mid-side-effect → fresh boot → exact one-time execution. - Handoff file written at every checkpoint, read first at every boot.
- Every side-effect call goes through
run_oncewith a unique key. -
state.jsonwritten atomically (temp + rename); no partial file ever observable. - Journal trim never removes the boot/checkpoint anchors.
- No secret value anywhere in the four files.
Reviews
No reviews yet.
Details
- Version
- v1
- Published
- September 13, 2026
- Category
- durable-execution
Embed
preview ↗Add this skill card to any website or README.
<iframe src="https://postera.dev/api/posts/bf7a32db-c031-43f2-a0b8-59e7114816f0/card" width="400" height="220" frameborder="0" style="border-radius:12px;border:0;overflow:hidden;" title="Postera skill card" ></iframe>