Skip to content

Make session bootstrap and recovery atomic across reloads and restarts #545

Closed

Description

@Juliusolsson05

Problem

Renderer reload and full application restart currently share a fragmented session bootstrap path. Rehydrate can bypass main-owned adoption, allocate new local ids, duplicate provider processes and scoped MCP credentials, drop panes after startup failure, and mark input ready before provider replay actually quiesces.

Required shape

Keep SessionManager. Add one atomic main-owned recoverSession operation keyed by the persisted local SessionId.

  • Renderer reload adopts an existing backend without new provider, proxy, or MCP side effects.
  • Full restart spawns exactly once under the persisted local id and forwards durable provider resume hints.
  • Concurrent recovery joins one synchronous main-owned claim.
  • Kill during startup cannot leak or resurrect a backend.
  • Failed panes and drafts remain visible and retryable.
  • Autosave resumes after every pane has a resolved outcome, including retained failures.
  • Composer readiness is provider-attested, level-triggered, versioned, and transported through SessionFeed.
  • Add deterministic fake-provider, fake-MCP, renderer, and cross-layer restart tests for Claude, Codex, opencode, and terminal sessions.

Scope guard

Do not rename SessionManager, add a state-machine framework, or combine the correctness change with unrelated file moves. Provider-specific durable identity acceptance can follow after atomic ownership and readiness are green.

Activity

  1. Juliusolsson05 commented on Jul 16, 2026

    @Juliusolsson05
    OwnerAuthor

    Implementation is now underway in draft PR #548. The reviewed plan is included in that PR, and the first atomic SessionManager recovery slice is green.

  2. Juliusolsson05 commented on Jul 28, 2026

    @Juliusolsson05
    OwnerAuthor

    Stages 1 and 2 shipped in #627 (merged 2026-07-28).

    Plan: docs/superpowers/plans/2026-07-28-session-lifecycle-observability.md
    Decomposition: docs/decomposition/agent-boot-readiness.md

    What this issue is now blocked on — and it is not engineering

    Stage 3 of the decomposition is collecting a corpus: using the app normally until the failure shapes repeat. No code can be written for Stages 4–6 until it exists, because the whole premise of the decomposition is that ~30 previous fixes failed by being authored from source reading against a failure nobody had recorded.

    Recording is always on. No flag, no setup. It writes to ~/.config/agent-code/incidents/runs/<runId>/events.jsonl, capped at 50 MiB per run, alongside the existing crash/heap breadcrumbs.

    npm run lifecycle:summarize              # newest run, one phase ladder per session
    npm run lifecycle:summarize -- --stalled  # only sessions that tripped a diagnosis
    npm run lifecycle:summarize -- --all      # every retained run

    What #627 deliberately did NOT do

    It does not fix why backends go missing. That is still open and is the substance of this issue. What changed is that the next occurrence is classified rather than guessed at: delivery.reject now separates

    • never-owned — the renderer invented or resurrected a local id → a persistence/ownership defect
    • entry-lost-after-owned — main owned it and lost it → a teardown the renderer never observed

    Those are two different defects in two different files, and they were byte-identical to the user until now.

    It also does not consolidate the wake call sites. Making caller a required parameter of ensureSessionLive turned the compiler into a census and found 13, not the nine a grep had suggested. They are instrumented in place; which of them actually behave differently is a question for the corpus, not a guess — guessing is what produced #596 and #598.

    Ready-made diagnoses the summarizer already flags

    STRANDED CLAIM (recovery began, never resolved) · TRANSCRIPT LOAD NEVER ENDED (the #283 shape) · NEVER BECAME READY · LONG GATE STALL with its reason · DELIVERY REJECTED with the classification above · SUBMIT UNWOUND · WAKE STORM (many wakes collapsing onto few recoveries — the #596 remount shape).

    Next step

    Use it for ~a week, then run npm run lifecycle:summarize -- --stalled and build the Stage 4 catalog from what actually repeats.

    Known gaps carried forward

    • The pane shows the coarse readiness reason. Claude's detailed verdict (replay-pending / composer-unpainted / human-draft) is collapsed to provider-not-ready before it leaves main; surfacing it means widening the SessionInputReadiness contract, which is a Stage 4 decision to be made from evidence. It is in gate.eval meanwhile.
    • Codex and opencode declare prompt-gate but never emit it — they latch a coarse boolean and genuinely cannot say why they are not ready. That silence is left visible rather than faked.
  3. Juliusolsson05 commented on Aug 31, 2026

    @Juliusolsson05
    OwnerAuthor

    Captured restart failure again on 2026-08-30. The pre-restart Codex rollout is valid and discoverable on disk, but workspace persistence held providerSessionId: null; restart therefore emitted recover.request kind:codex hasResumeId:false and spawned a fresh backend. The running parent appeared current but compiled stale packages/codex-headless commit 4e10c167, so it did not contain the exact proxy header to rollout identity attachment merged via #703. The repair will prevent stale-submodule builds and verify that a fresh Codex run persists session_meta.payload.id before restart recovery.

  4. Juliusolsson05 commented on Sep 20, 2026

    @Juliusolsson05
    OwnerAuthor

    Fixed on main and never closed. PR #548, merge f0a7792b.

    Every bullet of "Required shape" landed:

    • Atomic main-owned recoverSession keyed by the persisted local SessionId, with a synchronous claim (src/main/sessionManager.ts, src/preload/api/session.ts, src/main/ipc/session.ts).
    • Adoption without side effects, join-one-claim, kill-cancels-without-leak, MCP exactly-once — each pinned individually in sessionManager.recover.test.ts: adopts without constructing a second provider (:162), joins concurrent calls under one claim (:262), registers MCP once on cold spawn and never during adoption (:478), kill cancels a blocked recovery leaving no backend (:584), a failed claim releases for retry (:389).
    • Provider-attested, level-triggered, versioned readiness through SessionFeed, with per-provider attestation tests for Claude, Codex and OpenCode, and monotonic revisions pinned at sessionManager.recover.test.ts:233.
    • The cross-layer restart test the issue asked for: sessionRecovery.integration.test.ts — cold spawn → renderer adoption → identity + readiness (:193), retained failure + retry (:291), close-during-start cancellation (:363).
    • 3d525b9f later hardened autosave-after-resolved-outcome and removed the orphan-leaf latch that pinned autosave off.

    One item is deferred, by this issue's own scope guard. It says "provider-specific durable identity acceptance can follow", and PR #548 deferred exactly that. The last comment here (2026-08-30) reports a restart failure in that bucket — Codex providerSessionId: null, attributed to a stale packages/codex-headless build rather than to the recovery ownership code. If that still needs tracking it belongs in #716–#718, not here.

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions