crash-resume-index (gen-18 Track 1 #3)
The data layer for resume-not-respawn (R-036). Maps each cmux surface/pane to
the Claude session_id running in it, durably — so when a lead pane crashes
(RAM reboot, OOM, cmux restart), /orc can repoGolem --resume <session-id> instead
of spawning a fresh lead and discarding the crashed session's context.
Why it matters: orchestrator/263b3559#1 spawned-over-a-resumable and threw
away ~386K tokens of accumulated lead context. The fix is to remember which
session was on which surface, across a reboot, and resume it.
Scope
Resume is via repoGolem --resume; it prevents duplicate-spawning a fresh lead.
The flow: capture-on-boot → persist → resume-lookup
- Capture on boot — when a pane boots a Claude lead/worker, call
record(idx, {surfaceId, sessionId, repo, role}). First sight stamps
captured_at; every re-record bumps last_active.
- Persist durably —
saveIndex(path, idx) writes (atomically: temp + rename)
to a durable json path, default ~/.golems/crash-resume-index.json. NEVER
/tmp — a reboot wipes /tmp, which is the exact failure this index survives
(and a tmp-block hook denies /tmp writes anyway).
- Resume-lookup after a crash — on reboot,
loadIndex(path) rehydrates from
disk. lookup(idx, surfaceId) → the entry; resumableFor(idx, surfaceId, {maxAgeMs})
→ the session_id to resume (or null if unknown / too stale).
resumeCommand(...) emits the registered launcher form
<repo>Claude --resume <session-id> (e.g. orcClaude --resume …,
golemsClaude --resume …) — the actual repoGolem launchers, not a global
repogolem binary.
Staleness is opt-in (capture-on-boot caveat)
maxAgeMs is opt-in, not a default: capture-on-boot only stamps last_active
at boot, so a lead that crashed hours after boot would look "stale" and be wrongly
denied resume if a maxAge were imposed by default (Cursor HIGH). So:
resumableFor/resumeCommand with no maxAgeMs accept any recorded session.
A caller that does want a liveness window should keep last_active fresh by
calling touch(idx, surfaceId) on observed liveness (Track 3's hook does this on a
screen read / inbox tick).
Concurrency (two panes booting at once)
record via the CLI / Track 3 goes through recordToFile(indexPath, entry) — a
locked read-modify-write (a <index>.lock directory is the atomic mutex; a
clearly-stale lock from a crashed holder is stolen). Without it, two simultaneous
boots each load the same snapshot and the later saveIndex drops the earlier
pane's entry (Cursor MEDIUM / Codex P2). Verified: 10 parallel CLI captures → all
10 entries persist.
How it FEEDS idle-dwell-gate's SPAWN_OVER_RESUMABLE green path
The merged idle-dwell-gate skill FLAGS SPAWN_OVER_RESUMABLE — a fresh
spawn_agent over a resumable crashed lead. That gate's GREEN path is "there
IS a resumable session, so resume instead of spawn." This module is the index that
answers which session: resumableFor(surfaceId) returns the session_id the
gate's green path resumes. The gate decides whether to resume; this module
supplies what to resume.
Convergence with Track 3 (coordinate, don't duplicate)
This module is the data layer + lookup (pure functions + durable persistence).
The live cmux capture-on-boot wiring — the MCP hook that actually calls
record() when a pane boots a session — is Track 3's crash-resume work. Track 3
writes through this module; do not re-implement the index there.
API (src/crash-resume-index.mjs, ESM)
| Function |
What |
record(idx, {surfaceId, sessionId, repo, role}, now?) |
capture-on-boot; append/update; returns a new index (pure) |
recordToFile(indexPath, entry, opts?) |
locked capture-on-boot RMW (reload→record→save under a lock) — concurrency-safe |
touch(idx, surfaceId, now?) |
bump last_active for liveness (keeps a long-running session resumable) |
lookup(idx, surfaceId) |
entry {surfaceId, sessionId, repo, role, captured_at, last_active} or null |
resumableFor(idx, surfaceId, {maxAgeMs?, now?}) |
the session_id to resume, or null if unknown/stale (maxAgeMs opt-in) |
launcherFor(repo) |
the registered per-repo launcher (golems→golemsClaude, orchestrator→orcClaude) |
resumeCommand(idx, surfaceId, opts?) |
<repo>Claude --resume <id> (or repoGolem --resume <id> if repo unknown), or null |
pruneStale(idx, maxAgeMs, now?) |
drop entries past maxAgeMs; returns a new index |
loadIndex(path?) / saveIndex(path?, idx?) |
durable persistence; corrupt/missing → empty index (boot never blocks) |
DEFAULT_INDEX_PATH |
~/.golems/crash-resume-index.json |
CLI (scripts/crash-resume-index-cli.mjs)
crash-resume-index-cli.mjs record <surfaceId> <sessionId> <repo> [role]
crash-resume-index-cli.mjs lookup <surfaceId>
crash-resume-index-cli.mjs resume-cmd <surfaceId> [maxAgeMs] # prints <repo>Claude --resume …; exit 4 = none
# override path: --path <file> or CRASH_RESUME_INDEX_PATH
Tests
evals/crash-resume-index.test.mjs (bun:test, also node --test): record → save →
load across a simulated reboot (fresh loadIndex from the file returns the
entry) → lookup → resumableFor returns the right session_id → pruneStale
drops old entries → resume-cmd emits the correct repoGolem command. Negatives:
unknown surface → null; a stale entry past maxAge is not offered as resumable.
bun test skills/golem-powers/crash-resume-index/evals/
1---2name: crash-resume-index3description: Durable surface→session_id index so /orc RESUMES a crashed lead. Triggers: crash resume, resume not respawn, lead crashed, R-036, SPAWN_OVER_RESUMABLE green path, session_id lookup.4---56# crash-resume-index (gen-18 Track 1 #3)78The **data layer** for resume-not-respawn (R-036). Maps each cmux surface/pane to9the Claude `session_id` running in it, **durably** — so when a lead pane crashes10(RAM reboot, OOM, cmux restart), /orc can `repoGolem --resume <session-id>` instead11of spawning a fresh lead and discarding the crashed session's context.1213> **Why it matters:** orchestrator/263b3559#1 spawned-over-a-resumable and threw14> away **~386K tokens** of accumulated lead context. The fix is to remember which15> session was on which surface, across a reboot, and resume it.1617## Scope1819Resume is via `repoGolem --resume`; it prevents duplicate-spawning a fresh lead.2021## The flow: capture-on-boot → persist → resume-lookup22231. **Capture on boot** — when a pane boots a Claude lead/worker, call24 `record(idx, {surfaceId, sessionId, repo, role})`. First sight stamps25 `captured_at`; every re-record bumps `last_active`.262. **Persist durably** — `saveIndex(path, idx)` writes (atomically: temp + rename)27 to a **durable** json path, default `~/.golems/crash-resume-index.json`. **NEVER28 `/tmp`** — a reboot wipes `/tmp`, which is the exact failure this index survives29 (and a tmp-block hook denies `/tmp` writes anyway).303. **Resume-lookup after a crash** — on reboot, `loadIndex(path)` rehydrates from31 disk. `lookup(idx, surfaceId)` → the entry; `resumableFor(idx, surfaceId, {maxAgeMs})`32 → the `session_id` to resume (or `null` if unknown / too stale).33 `resumeCommand(...)` emits the **registered launcher** form34 `<repo>Claude --resume <session-id>` (e.g. `orcClaude --resume …`,35 `golemsClaude --resume …`) — the actual repoGolem launchers, not a global36 `repogolem` binary.3738### Staleness is opt-in (capture-on-boot caveat)3940`maxAgeMs` is **opt-in**, not a default: capture-on-boot only stamps `last_active`41at boot, so a lead that crashed hours *after* boot would look "stale" and be wrongly42denied resume if a maxAge were imposed by default (Cursor HIGH). So:43`resumableFor`/`resumeCommand` with no `maxAgeMs` accept **any** recorded session.44A caller that *does* want a liveness window should keep `last_active` fresh by45calling `touch(idx, surfaceId)` on observed liveness (Track 3's hook does this on a46screen read / inbox tick).4748### Concurrency (two panes booting at once)4950`record` via the CLI / Track 3 goes through `recordToFile(indexPath, entry)` — a51**locked** read-modify-write (a `<index>.lock` directory is the atomic mutex; a52clearly-stale lock from a crashed holder is stolen). Without it, two simultaneous53boots each load the same snapshot and the later `saveIndex` drops the earlier54pane's entry (Cursor MEDIUM / Codex P2). Verified: 10 parallel CLI captures → all5510 entries persist.5657## How it FEEDS idle-dwell-gate's SPAWN_OVER_RESUMABLE green path5859The merged `idle-dwell-gate` skill **FLAGS** `SPAWN_OVER_RESUMABLE` — a fresh60`spawn_agent` over a resumable crashed lead. That gate's **GREEN path** is "there61IS a resumable session, so resume instead of spawn." This module is the index that62answers *which* session: `resumableFor(surfaceId)` returns the `session_id` the63gate's green path resumes. The gate decides **whether** to resume; this module64supplies **what** to resume.6566## Convergence with Track 3 (coordinate, don't duplicate)6768This module is the **data layer + lookup** (pure functions + durable persistence).69The **live cmux capture-on-boot wiring** — the MCP hook that actually calls70`record()` when a pane boots a session — is **Track 3's** crash-resume work. Track 371writes *through* this module; do not re-implement the index there.7273## API (`src/crash-resume-index.mjs`, ESM)7475| Function | What |76|---|---|77| `record(idx, {surfaceId, sessionId, repo, role}, now?)` | capture-on-boot; append/update; returns a new index (pure) |78| `recordToFile(indexPath, entry, opts?)` | **locked** capture-on-boot RMW (reload→record→save under a lock) — concurrency-safe |79| `touch(idx, surfaceId, now?)` | bump `last_active` for liveness (keeps a long-running session resumable) |80| `lookup(idx, surfaceId)` | entry `{surfaceId, sessionId, repo, role, captured_at, last_active}` or `null` |81| `resumableFor(idx, surfaceId, {maxAgeMs?, now?})` | the `session_id` to resume, or `null` if unknown/stale (`maxAgeMs` opt-in) |82| `launcherFor(repo)` | the registered per-repo launcher (`golems`→`golemsClaude`, `orchestrator`→`orcClaude`) |83| `resumeCommand(idx, surfaceId, opts?)` | `<repo>Claude --resume <id>` (or `repoGolem --resume <id>` if repo unknown), or `null` |84| `pruneStale(idx, maxAgeMs, now?)` | drop entries past `maxAgeMs`; returns a new index |85| `loadIndex(path?)` / `saveIndex(path?, idx?)` | durable persistence; corrupt/missing → empty index (boot never blocks) |86| `DEFAULT_INDEX_PATH` | `~/.golems/crash-resume-index.json` |8788## CLI (`scripts/crash-resume-index-cli.mjs`)8990```bash91crash-resume-index-cli.mjs record <surfaceId> <sessionId> <repo> [role]92crash-resume-index-cli.mjs lookup <surfaceId>93crash-resume-index-cli.mjs resume-cmd <surfaceId> [maxAgeMs] # prints <repo>Claude --resume …; exit 4 = none94# override path: --path <file> or CRASH_RESUME_INDEX_PATH95```9697## Tests9899`evals/crash-resume-index.test.mjs` (bun:test, also `node --test`): record → save →100load **across a simulated reboot** (fresh `loadIndex` from the file returns the101entry) → lookup → `resumableFor` returns the right `session_id` → `pruneStale`102drops old entries → `resume-cmd` emits the correct repoGolem command. Negatives:103unknown surface → `null`; a stale entry past `maxAge` is not offered as resumable.104105```bash106bun test skills/golem-powers/crash-resume-index/evals/107```