Skip to content

feat: wave-B restart-resilience — reconcile handshake live, codex locator, recover-my-panes, fresh-agent verdicts (4 lanes, integrated)#537

Merged
danshapiro merged 91 commits into
mainfrom
integration/waveB-preflight
Jul 26, 2026
Merged

feat: wave-B restart-resilience — reconcile handshake live, codex locator, recover-my-panes, fresh-agent verdicts (4 lanes, integrated)#537
danshapiro merged 91 commits into
mainfrom
integration/waveB-preflight

Conversation

@danshapiro

Copy link
Copy Markdown
Owner

Wave B of the restart-resilience campaign — four lanes merged in load-bearing order (B1→B2→B3→B4) and verified as a combined tree, plus integration fixes and fast-follows. Lanes (preserved as merge parents):

  1. feat/reconcile-client-adoption — Phase 3 CLIENT ADOPTION: the reconciliation handshake is now live end-to-end (paneReconcileV1 negotiation, verdict folding, batched dead-session panel, warming banner); retry verdict deleted from the wire (bounded index-warming deferral + PROVIDER_UNAVAILABLE); per-sessionRef liveness-bound lease closes the two-clients-duplicate-respawn race (D8); legacy census path kept as capability-gated fallback for the TS server

  2. feat/codex-rollout-locator — server-side codex identity locator (Enter-anchored discovery, folder-match, contested-folder refusal); client candidate channel retired to accept-and-ignore (one writer per identity fact)

  3. feat/recover-my-panes — "recover my panes" offer for empty-layout browsers, fed by pane ledger + tabs snapshots, with server liveness guard; the operator-only restore machinery deleted (−4,469 lines). Resolves kata h9vt as Option A (store is now load-bearing)

  4. feat/freshagent-verdicts-resume — fresh-agent panes get ledger records incl. resume-invocation settings (model/sandbox/permissionMode/effort/cwd reapplied on every resume — no more silent settings reset); server-side kind:fresh-agent verdict arm; codex crash-respawn user-visible degradation frame; respawn cap with alive-clears-counter

Integration commits incl.: B2×B4 misbind fix (codex locator refuses freshagent-known thread ids), B3×B4 opencode recovered-panes-now-resume verification, PWA first-boot offer/reload race fix, inventory liveness join widened, REST send-keys all-blank-settings guard, single-burn respawn-counter pin.

Verification: cargo workspace 1907 tests green, clippy -D warnings green, coordinated JS suites green (4156/4548/350), e2e 36/36 (+2 sweeps, 1 investigated flake documented for wave C), contract wall green with remaining pins re-validated, port-contract drift unchanged vs main.

Known wave-C follow-ups: reload-path pre-verdict create race; fresh-agent per-sessionRef create lease; port-contract freeze vs T0 inventory reconciliation.

danshapiro and others added 30 commits July 26, 2026 02:24
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Lane B4: fresh-agent panes into the reconcile verdict system (server side),
resume-invocation record in the pane-identity ledger, settings-from-ledger
resume for codex/opencode/claude, and the codex crash-respawn degradation
frame. 15 TDD tasks, capability-gated behind paneReconcileFreshAgentV1.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Lane B3 (campaign P1.9 + kata h9vt resolution): user-facing browser-loss
recovery surface. Plan covers the GET /api/recovery/inventory read API
(snapshots union + pane-ledger join), the client offer panel with
boot-captured trigger eligibility, ledger-authority-chain recreation,
the browser-loss e2e, and deletion of the operator restore endpoint +
marker/fence machinery (h9vt Option A disposition).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ption validation

9 falsified assumptions fixed: reconcileEpoch re-fire mechanism for same-createRequestId
verdict folds; error-frame/reconnect fallback instead of assuming results always arrive;
lease code relocated to crates/freshell-terminal/src/registry.rs; Task 6 drops the
redundant ledger write (already on the create path); deferral awaits the index sweep
(warming banner expected on cold boots); TTL rebased as explicit env-tunable backstop;
fold ownership via existing ws.onMessage; client test paths corrected to
test/unit/client/; playwright rust-chromium spec registration added. Ledger:
.the-usual-logs/reconcile-client-adoption/load-bearing-ledger.md

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…dation

Validated 15 load-bearing assumptions (3 verified, 12 falsified) and fixed
the plan accordingly:
- liveness: inventory joins the terminal registry; live panes recreated
  without resume; new Task 2b adds a guard on the direct sessionRef rung
  (no-restart browser loss would otherwise silently duplicate live sessions)
- ledgerOnly matched across ALL device unions and excludes live rows
- 15-min staleness rule stops stale clientInstanceIds resurrecting closed tabs
- contentId re-keyed to sha256 over timestamp-free substance (DefaultHasher
  churned on 5-min heartbeats and is unstable across Rust releases)
- D1 boot-capture rationale corrected; storage-migration import-order pinned
- per-kind recovery adapter (editor content default, restoreError strip)
- freshopencode fresh-start known limitation documented (fenced B4 crate)
- Task 8 resume proof rebuilt from fake-CLI argv log (cited assertion did
  not exist); browser-pane + no-restart scenarios added
- Task 9: parse_selector/GET snapshot API kept, deploy-tab-diff script+spec
  handled, explicit freshell-ws screenshot broker audit table

Ledger: .worktrees/.the-usual-logs/recover-my-panes/load-bearing-ledger.md

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…tion

Validated against codex source @rust-v0.145.0, a 3,858-rollout corpus,
and the frozen client (evidence: .the-usual-logs ledger + reports).
Falsified assumptions fixed:
- A1: real codex materializes the rollout only at the first user prompt
  -> windows are Enter-anchored only; spawn window removed (was a pure
  foreign-capture window).
- A3: create->session_meta gap (git-info await, worst ~10s) -> unparsable
  new files become bind-blocking pending candidates with a 10s re-probe
  grace (PENDING_FIRST_LINE_GRACE_MS).
- A4 (critical): same-cwd foreign rollouts could misbind -> cwd is now
  required (no-cwd never binds), cross-tick contested-cwd refusal,
  already-bound thread-id refusal; freshagent same-cwd residual documented
  (fix fenced to Lane B4).
- A5: arm site is post-PTY-spawn -> safe because Enter-anchored; rationale
  made explicit; arm scan moved into spawn_blocking (A6 cold-cache
  insurance; measured perf numbers replace the stale 35-55ms claim).
Verified premises recorded in a new Validated Premises section; Task 11
gains a mechanical one-writer merge-time gate (A8).
Fresh-eyes review (iteration 1) found the pinned main-import-order.test.ts
asserted storage-migration is the FIRST import of src/main.tsx, but the
actual first import is React (storage-migration is 4th, after the
side-effect-free react/react-dom/react-redux library imports), so the
test could never pass while Task 3 forbids changing main.tsx.

- main-import-order.test.ts spec now asserts storage-migration is
  imported before the @/store/store and @/App imports (the real
  invariant: it runs before any module that could touch the layout keys)
- D1 prose, the file-structure table, Task 3's files list, the
  boot-state.ts comment block, and the pinned main.tsx comment text no
  longer claim storage-migration is the first import
…ng validation findings

Executed 10 parallel evidence-gathering validators over Lane B4 implementation plan
(fresh-agent verdicts + settings-from-ledger resume). Incorporated validation findings:

1. PaneIdentitySink writes now awaited (durable-before-answer, wave-A policy conformance)
2. G3 supersession linkage written at codex crash-respawn + verdict arm resolves session refs to the supersession-chain terminus
3. SETTINGS_RESET alarm gated to previously-recorded sessions only (no spam for historical/pre-ship sessions, no defaults-laundering refresh writes)
4. restart/resume topology corrected — post-reload the frozen client sends create{resumeSessionId}, so opencode handle_create now honors resume_session_id (the actual P1.13 pin-flip target) and e2e assertions are create-shaped
5. e2e seeding for codex/claude switched from REST (rejects agent≠opencode) to raw WS freshAgent.create
6. opencode fake audit assertions fixed to event==='prompt_async' + fake extended to audit the prompt body
7. codex fake crash staging redesigned to a single static behavior with a crash-once marker knob
8. B1 lane-coexistence hardening (DELETE-AT-MERGE annotation, ProviderUnavailable→Unknown mapping, B1-first merge order, snapshot-built-once)
9. respawn cap reworked to respawn-answers-only with reset-on-live
10. stale client citations fixed + alarm frame shape pinned (top-level sessionType/provider + message)

Plus codex zero-turn/compression caveats and Task 13 harness notes.

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
… error{index_warming}

Task 2 of the reconcile-client-adoption plan: `retry` is deleted from the
pane.reconcile wire.

- freshell-protocol: `ReconcileVerdict::Retry` -> `ReconcileVerdict::Error`
  (wire "error", terminal per-pane error state, reason "index_warming" |
  "provider_unavailable"); `PaneVerdict.retry_after_ms` removed.
- freshell-ws reconcile: the Unknown-existence arm now derives
  error{index_warming}; RETRY_AFTER_MS repurposed as
  RECONCILE_DEFERRAL_BUDGET_MS_DEFAULT (2000ms).
- freshell-ws terminal: handle_pane_reconcile performs at most ONE bounded
  deferral (WsState.reconcile_deferral_budget_ms, default 2000ms) when a
  derivation comes back warming, then re-derives exactly once - never loops.
  Plain bounded sleep: SessionIndex exposes no awaitable sweep-completion
  signal (peek()/is_fresh() are sync by design; a cold snapshot().await is
  unbounded), and the index-backed probe already kicks a detached refresh on
  every non-fresh read, so the sweep converges during the wait when it can.
- e2e synthetic client: reconcileUntilStable re-requests on
  error{index_warming} instead of the deleted retry verdict.

Honest expectation (pinned in the brief): a truly cold index scan exceeds
the budget, so error{index_warming} remains EXPECTED on cold boots; the
manual retry affordance (task 13) is the recovery path.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…t error verdict

A known provider whose session root does not exist on this machine is NOT
warming — it will never warm up. New SessionExistence::ProviderUnavailable:
IndexExistenceProbe checks the provider's session root (the same paths the
index sources use) before the cold-peek Unknown; the derivation answers
error{provider_unavailable} immediately, and the handler's single warming
deferral (index_warming only) never fires for it. NoIndexProbe unchanged.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Independent cross-model review (fresheyes iter 1) found 3 blocking majors;
all fixed against verified repository behavior:

1. Task 14 opencode e2e (test 2): the wire body carries model as a
   {providerID, modelID} OBJECT only for provider/model-shaped ids
   (split_opencode_model returns None for slashless) and effort as
   'variant' - never 'effort'/'reasoningEffort'. Switched the test model
   to 'fakeprov/big-model', the fake's audit fields to {model, variant},
   and the assertions to the actual wire shape.

2. Task 14 raw-WS templates: added the required 'provider' field to every
   freshAgent.create template (dispatch silently drops provider: None)
   and to the freshAgent.attach template (provider is a required field -
   deserialization fails without it); corrected the raw-attach donor
   citation to freshagent_claude_attach.rs:276.

3. Task 1: tests belong in the sibling file pane_ledger_tests.rs (pulled
   in via #[path] at pane_ledger.rs:797-799), constructed via the real
   helpers (temp_root(label) + PaneLedger::new(Some(root)) + manual
   remove_dir_all cleanup) instead of the nonexistent test_ledger();
   added pane_ledger_tests.rs to Task 1's git add so the TDD evidence
   is committed.
…ut-locator plan

Blocking issue 1 (Task 6 could not pass as written): the shared test
harness spawn helpers build WsState with no locators and never spawn a
sweep, so the planned integration test could never resolve identity.
Task 6 now adds a locator-wired spawn helper to tests/common/mod.rs
(codex_locator set + 150 ms sweep spawned before router), the File
Structure table and Task 4's compile-sweep instruction name the harness,
and the failure-triage note points at the harness first.

Blocking issue 2 (Premise 10 understated the misbind surface): added the
FIRST-submit known_files re-snapshot hardening - strictly safe by
Premise 7 - closing the arm-to-first-Enter foreign-capture gap. Encoded
in Task 1 (note_submit re-snapshot + new test), Task 2 (re-open keeps
the frozen snapshot pin), Task 4 (async note_possible_submit awaited
BEFORE the PTY write - ordering is load-bearing because codex
materializes the rollout in response to that very Enter), Task 5/6
(seed-after-submit orderings), and Task 8 (fixture writes the rollout
on first Enter, not first stdin data). Premises 9/10 and the plan
Self-Review updated to match.
…TY invariant alarm

Council rule 6 (reconcile side): a pane claiming a sessionRef that a LIVE
terminal already carries now gets attach{terminalId} regardless of
createRequestId, closing the D8 second-writer respawn shape. The lookup
consults BOTH identity stores, mirroring resolve_authoritative_ref's
dual-source read: the ws-owned identity registry (live entry, re-verified
against registry liveness) then the registry-row join
(mode + resume_session_id + Running) for REST-created resumes whose
identity never reached the identity registry.

Council rule 9: TerminalRegistry::alarm_if_duplicate_session_ref ERROR-logs
(target "invariant", duplicate_pty_for_session_ref) when >=2 live PTYs
carry one sessionRef, and returns whether the invariant is violated.
TerminalRegistry::live_terminal_for_session_ref exposes the row-join
(newest generation first). Tasks 5/6 consume both.

No existing test asserted respawn where the new branch fires — no D8-pinning
tests needed updating; the full freshell-terminal and freshell-ws suites
pass unchanged.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
- Task 4: correct buildQueryString call shape to the real tuple-entries
  signature (api.ts:273) in both the Interfaces block and the api.ts
  implementation snippet; getRecoveryInventory now typechecks and runs.
- Task 8: replace the copied codex-syntax hasResumePair (bare 'resume')
  with an in-spec hasClaudeResumePair matching the fake claude CLI's
  --resume flag, so Scenario 1's accept-resume poll can pass and
  Scenario 3's D7 negative assertion is non-vacuous.
- D2/D4 + Tasks 1/2/3/4/5: close the same-fresh-browser multi-window
  self-pollution hole with a bootAgoMs concurrent-client cutoff (A16):
  boot-state exports bootCapturedAtMs, the pending flag persists
  {contentId, bootAt}, the inventory request carries bootAgoMs, and
  select_foreign_recent_generation_ids drops clients born after the
  boot cutoff (new unit test) so junk fresh windows can never win
  primary-device selection over the genuinely lost device.
…nch via the uniform promotion rule

Review finding on Task 4 (ee9374d): the council-rule-6 attach branch in
verdict_for_pane fired only on a structured sessionRef claim, so a
legacy-claiming client (mode + resumeSessionId only) whose session was
carried by a live terminal under a DIFFERENT createRequestId fell through
to keyed resolution -> disk-Present -> respawn -- the D8 second-writer
shape rule 6 exists to close.

Fix: extract §5.2's ONE uniform promotion rule into promoted_legacy_claim
(reused by resolve_authoritative_ref step 4 -- no duplication) and compute
the branch's effective claim as sessionRef.or(promoted claim). Structured-
claim behavior and ordering unchanged; unsupported_kind arm untouched.

Test (RED first: derived Respawn, the literal D8 verdict):
reconcile::tests::legacy_resume_claim_attaches_to_live_terminal_across_create_request_ids

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ally fail

Fresh-eyes iteration 2 blocking fix: Tasks 1 and 4 created their new .rs
files in Step 1 but deferred the 'pub mod' registration to Step 3, so the
Step 2 'Expected: COMPILE ERROR' gates would instead exit green with 0
tests (cargo never compiles an unregistered file). Move the module
registration (pub mod codex_locator / pub mod codex_association) into
Step 1 of each task, drop the duplicate registration from each Step 3,
and spell out both expected RED error families for Task 4 Step 2
(missing controller functions + missing WsState codex_locator field).
- Task 2 Step 3: authoritative identity_sink.rs code block now includes the
  supersedes field on FreshAgentBindingUpsert, matching the Interfaces block,
  Task 2's own test, and all downstream constructions (Tasks 3/4/7/9/10)
- Task 11: paneReconcileFreshAgentV1 ready echo now names the required
  freshell-protocol ReadyCapabilities change (server_messages.rs:757-760,
  typed ready frame via build_handshake_with_capabilities - no raw-JSON path),
  adds it to the File Structure table, Task 11 Files list, and the Step 5
  git add so the scripted commit compiles
- Replace non-allowlisted model gpt-5.3-codex with allowlisted
  gpt-5.3-codex-spark throughout (normalize_freshcodex_model clamps unknown
  models to gpt-5.5, making Task 4's Rust assertion and Task 14 e2e test 1's
  resume assertion unpassable as written)
Two blocking defects from independent plan review:

1. Task 2 Step 3 reference snippet omitted the load-bearing D2/A16
   bootAgoMs plumbing: InventoryQuery now carries boot_ago_ms:
   Option<u64>, the handler computes boot_cutoff =
   now_ms().saturating_sub(bootAgoMs), and read_foreign_unions is
   called with its real 3-arg signature. Added a dedicated route test
   (route_bootagoms_drops_concurrent_post_boot_clients, real
   wall-clock fixtures) that forces the wiring to exist: a post-boot
   concurrent client is dropped with bootAgoMs=30000 and kept without
   the param. Prose tightened: missing => 0; non-numeric => 400 from
   the Query extractor. Self-Review Record sections 1 and 3 updated to
   match.

2. Task 8 Scenario 3 step 1 attempted UI pane creation underneath the
   plan's own modal recovery offer (fresh context D against a server
   with recoverable state). Step 1 now waits for
   recovery-offer-panel and declines it before any pane interaction,
   noting the dismissal is scoped to context D's localStorage and does
   not affect context E.
…release semantics

Council rule 7 (D8 two-writers): one in-flight create per sessionRef,
enforced in TerminalRegistry via a lease map keyed "provider\0sessionId"
(mirrors keyed_create_inflight) plus a session_ref_bindings map recording
the winner's terminalId at spawn-complete.

- claim_session_ref: BoundElsewhere (live row-join or non-dead binding) >
  Held (unexpired) > ExpiredNeedsKill{pid} (TTL + recorded child; lease
  stays held — kill-before-release) > revoke + hold closed (TTL, pid-less).
- complete_session_ref_claim: bind + release + duplicate alarm; false if
  revoked while spawning.
- fail_session_ref_claim / release_session_ref_leases_for_conn /
  force_release_after_confirmed_kill: release on spawn fail and conn
  death; pid-carrying conn-death entries returned still-held for
  confirmed kill first.
- SESSION_REF_LEASE_TTL_MS = 20s explicit wall-clock backstop (not
  derived from the spawn-gate permit budget).

Unit level only; Task 6 wires the create path to this exact API.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…iteration 3

The located_session_bound_elsewhere_is_rejected pin had only negative
assertions, which pass identically whether the adoption-tail guard fired
or the locator silently never resolved (wrong cwd, bad seed timing) - and
the prescribed identity-inversion check cannot tell those worlds apart.

- Test delta now keeps a locator handle, seeds the rollout with
  payload.cwd = the pane's own cwd (mirroring test 1), and asserts the
  positive resolution signal locator.armed_count() == 0 BEFORE the
  negative identity/resume assertions.
- Step 2's inversion instruction for this test now targets the positive
  signal (invert to armed_count() == 1) and warns that identity-only
  inversions cannot prove non-vacuity.
…e_session_ref_claim atomic

Task 5 review fix round 1 (both findings Important):

1. Killed winner stranded the sessionRef: kill_internal removes the
   terminal row entirely, so claim_session_ref's "known dead" probe
   (registered-but-not-Running) could never fire for a killed winner --
   the unknown-id binding was honored as BoundElsewhere{dead-id} forever.
   Now kill_internal (the only terminals.remove site; natural exit
   retains the row) prunes any session_ref_bindings entry pointing at
   the removed terminal. The brief-mandated
   completed_claim_yields_bound_elsewhere test (never-registered id)
   still passes unchanged.

2. complete_session_ref_claim released the lease and inserted the
   binding under separate locks, opening a window where a racing
   claim_session_ref saw no row, no binding, no lease -> Acquired ->
   second spawn. The binding is now inserted while the leases lock is
   still held. Lock order leases -> bindings is acyclic:
   claim_session_ref's bindings block ends before it takes leases, and
   no path acquires leases while holding bindings.

New test: killed_winner_binding_is_pruned_so_next_claim_acquires
(claim -> complete -> real kill path -> next claim Acquired), verified
RED before the fix.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Task 7's REST send-keys materialization change to freshell-freshagent/src/lib.rs
was declared but never verified nor committed within the task. Now: Step 1
writes the failing rest_send_keys_materialization_records_binding test in
lib.rs as part of the RED phase; Steps 2/4 run a second cargo test filter
exercising it (FAIL then PASS); Step 3 item 4 references the named test;
Step 5 stages both opencode_ws.rs and lib.rs and the commit message names
the REST send-keys site.
…pane-ledger (B3/P1.9)

Task 1 of the recover-my-panes lane: the PURE server-side inventory
builder (no HTTP yet - Task 2 adds the route).

- DeviceUnion + build_inventory(device_unions, bindings,
  live_session_keys) -> the /api/recovery inventory JSON: primary
  device = newest union with records, otherDevices summaries,
  D4 effective sessionRefs via the ledger superseded chain
  (bound|closed|gc_expired|unknown), D7 live flags, cross-device
  ledgerOnly (A4), and a timestamp-free 16-hex contentId (D3) so
  heartbeat capturedAt churn never defeats dismissal.
- select_foreign_recent_generation_ids: A15 staleness (15-min
  cutoff vs the remaining clients' max) + A16 concurrent-fresh-window
  boot-cutoff filter, consumed by Task 2 when composing unions.
- 11 sibling-module tests written red-first against a null stub.
- sha2 added to freshell-server (tabs_persist digest convention).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ate, Enter-anchored resolve with pending-first-line grace
…-state read API (B3/P1.9)

Wires the Task 1 pure inventory builder into an authenticated axum route:
- RecoveryInventoryState + router() in recovery_inventory.rs; merged in
  main.rs beside the tabs_snapshots surface, sharing the SAME
  tabs-snapshots dir, pane ledger, and terminal registry.
- Snapshot reads run in spawn_blocking over the read-only tabs_persist
  selectors; per-device generations filtered through
  select_foreign_recent_generation_ids (own-client, A15 stale, A16
  post-boot concurrent). IO/Join errors -> 500 + tracing::error (fail-loud).
- D7 liveness join reads the shared TerminalRegistry directory():
  (mode, resume_session_id) for Running rows.
- bootAgoMs optional query param (missing => 0 => boot_cutoff = now).
- Removed Task 1's module-wide #![allow(dead_code)]; DeviceUnion.device_id
  is now read by build_inventory instead of re-derived from the union doc.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
danshapiro and others added 29 commits July 26, 2026 06:59
…VALID_TERMINAL_ID re-drive with auto-resolve exhaustion

Task 12 of the pane.reconcile client adoption:

- A1 fix: add terminalContent?.reconcileEpoch to the create-or-attach
  effect dependency array so verdict folds into an already-mounted pane
  (same createRequestId - never re-minted) re-fire the effect. terminalId
  stays intentionally out of the deps.
- Create-arg precedence: pendingReconcile 'respawn' sends the server-named
  sessionRef with restore:true (loud guard on provider mismatch);
  'fresh' omits resume identity entirely; both suppress fresh-recovery
  intent. pendingReconcile is cleared by the reducer on terminal.created.
- SESSION_RESERVED bounded re-drive: re-send the SAME terminal.create
  after max(retryAfterMs, 250ms), inside a 30s wall-clock window
  (> 20s lease TTL + margin). Window survives effect re-fires (no refunds).
- Exhaustion auto-resolve (council rule 8): single-pane
  pane.reconcile.request via the existing ws.onMessage broadcast
  subscription, folding ONLY the reconcileId this pane minted
  (fold-ownership rule) - attach silently or dead_session/fresh with a
  visible notice; never a wedge, never a duplicate.
- F9: launch-time INVALID_TERMINAL_ID for a still-creating pane with no
  matching launch attempt gets a bounded 5 x 500ms same-requestId pump
  before surfacing status 'error'.
- One-shot reconcileNotice rendered via writeLocalXtermNotice after
  attach / terminal.created, then cleared from the store.

Tests: test/unit/client/components/TerminalView.session-reserved.test.tsx
(8 tests, written RED-first; the two fold-on-mounted tests failed
specifically because reconcileEpoch was missing from the effect deps).
All 23 TerminalView unit suites pass (286 tests); typecheck:client clean.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…hoed via typed ReadyCapabilities; frozen client unaffected
…, mixed-kind, reload guard, decline, live no-restart (B3/P1.9)

Adds test/e2e-browser/specs/recover-my-panes-rust.spec.ts (registered in
RUST_ONLY_SPECS + the rust-chromium testMatch), three serial scenarios on
one owned RustServer:

- Scenario 1: lost browser + server restart -> fresh context is offered
  recovery; accept recreates the tab (claude `--resume <sessionId>` argv
  proof + scrollback marker, mixed-kind browser pane at example.com), and
  a same-browser reload never re-offers.
- Scenario 2: decline closes the panel and adds no recovered tabs.
- Scenario 3 (D7): browser lost WITHOUT restart -> live claude PTY stays
  running; offer shows the live-session note; accept recreates the pane
  with NO `--resume` for that session (non-vacuous: the spawn is observed
  in the argv log first, and scenario 1 proves the same matcher matches).

Contexts block service workers (perf-harness precedent): the production
client reloads on SW controllerchange, which raced App mount and aborted
the panel's one-shot inventory fetch (net::ERR_ABORTED), flaking fresh-
context offers. Red-verification performed: with <RecoveryOfferPanel />
unmounted the spec fails on the offer-visibility assertion; App.tsx
restored byte-identical. 2 consecutive green runs.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…g banner with manual retry

DeadSessionPanel: ONE role="dialog" (aria-label "Dead sessions") listing
ALL dead panes (council rule 1 / F11-human — never N modals). Per-row
"Start fresh here" dispatches resetPaneForReconcileCreate({intent:'fresh'})
preserving createRequestId (I7) + resolves the row; "Close pane" goes
through closePaneWithCleanup (the established component-level close path,
handles the last-pane→close-tab case); "Dismiss" clears the list but keeps
per-pane restoreError cards. Nothing is auto-closed (council rule 12).

ReconcileWarmingBanner: ONE role="status" banner "Waiting for session
index — N pane(s)" (council rule 5). Prominent "Retry now" re-sends a
reconcile request for exactly the warming panes and folds ONLY results
whose reconcileId its own Retry minted (fold-ownership rule, Task 9),
via the Task-12 subscribe→match→foldVerdicts→unsubscribe pattern —
subscription exists only while a retry is in flight. Cardinality
violations keep the banner up for another retry.

Both mounted in App.tsx beside AuthRequiredModal.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…s a single hard assertion (B3/P1.9 review fix)

openFreshContextWithOffer no longer retries with new contexts: with
service workers blocked, the known cause of transient offer suppression
is gone, and the loop would silently absorb exactly the flaky-offer
regression class this feature already exhibited. The offer must appear
in ONE fresh context within 15s or the spec fails loud (the brief's
contract). 2 consecutive green runs.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…head marker, and screenshot delivery fence

Kata h9vt disposition: Option A. The tabs-snapshots store is now load-bearing
via GET /api/recovery/inventory + the client recover-my-panes flow. The
server-push restore machinery (marker, one-browser gate, delivery fence,
scripts/restore-tabs.sh, parse_restore_selection) served only the blind
operator push and has no UI-flow consumer; the unreachable fence was flagged
for deletion in campaign P2.17. The GET snapshots read API, parse_selector,
and the broadcast screenshot broker survive (audit table in the plan);
deploy-tab-diff remediation now points at the UI recovery flow.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…lients one sessionRef now yields exactly 1 PTY

- New reconcile-client-adoption-rust.spec.ts (3 scenarios): mixed-pane
  restart recovery driven by verdicts (census never destroys), ONE
  batched dead-session adjudication panel with per-row decisions, and
  double-restart mid-reconcile convergence with no duplicate PTYs.
- Flip the P1.7 (D8) wall pin: the sessionRef single-flight lease +
  reconcile attach-to-winner landed (Tasks 2-13), so the two-clients
  test's exactly-1-PTY assertion now passes; the test.fail pin is
  deleted (an unexpected pass is a hard suite failure).
- Rewrite the double-restart guard's legacy observation: the adopted
  client sends pane.reconcile.request after every ready (asserted via
  the harness sent-message log) and the pane converges to exactly one
  running PTY (asserted via /api/terminals). Name+scenario unchanged.
- Wall claude-terminal fixture realism: real claude writes the session
  transcript to ~/.claude/projects at session start; the fake CLI does
  not. Under the adopted client the post-restart verdict derives from
  disk truth (claimed-but-missing = loud dead_session), so the test now
  mirrors what real claude persists before the SIGKILL. No assertions
  changed.
- Register the new spec in RUST_ONLY_SPECS AND the rust-chromium
  testMatch (collection verified: 3 tests).

Wall audit (A12): full wall run green (15 passed) - no other test.fail
pin unexpectedly passed; all remaining pins still hold.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…elper, wall-spec comment fix

Final whole-branch review fixes (Lane B4):

- claude.rs: the sdk.session.init recording arm now skips the binding
  write when the threaded settings snapshot is entirely blank (V7/A10
  parity with codex record_codex_binding) -- an all-blank row would make
  was_recorded true while load_settings returns None, arming a FALSE
  SETTINGS_RESET for a legitimately-default session on a later resume.
  New test session_init_with_all_blank_settings_records_no_binding
  (shown RED before the guard, GREEN after).

- pane_ledger.rs: extracted the duplicated supersession retire block
  from record_binding_locked and record_fresh_agent_binding into ONE
  private retire_and_link_locked helper (behavior identical, no public
  API change) -- removes the two-copies drift hazard.

- restore-contract-wall-rust.spec.ts: HISTORY note now cites the actual
  Rust test name (create_with_resume_session_id_rebinds_the_durable_session).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…agent call site

Commit b2ce5cb extracted retire_and_link_locked but only converted
record_binding_locked, leaving record_fresh_agent_binding with its
original inline retire-and-link block — and leaving the helper's
fresh-agent (terminal_id: None) log arm unreachable. Replace the inline
block with a helper call, preserving the old_id != session_id guard,
the silent no-op on a missing old row, the pinned new-row-first order,
and the exact fresh-agent log message. RetiredReason::Superseded now
appears in pane_ledger.rs only inside the helper.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…te failures

Task 15 gate-fix: the reconcile-adoption full run failed 7 specs; triage in
isolation reproduced 6 (harness-01-rust-server passed in isolation — parallel-
load flake, hardened below). No assertion was weakened or deleted; per-test
justification:

- fake-amplifier-cli.mjs, fake-amplifier-activity-cli.mjs (fixes
  amplifier-restore-rust, amplifier-lane-resilience-rust): FIXTURE REALISM —
  real amplifier persists metadata.json, the CANONICAL record the session
  index reads (amplifier.rs); the fakes wrote only events.jsonl, so the
  adopted client's disk-truth verdict was honestly
  dead_session{session_not_on_disk} (dialog captured in error-context)
  instead of respawn-with-resume. The fakes now mirror the real CLI's
  persistence. Assertions untouched.

- wavea-interactions-rust (A2xA3): FIXTURE REALISM — same Task-14 precedent
  as restore-contract-wall-rust's claude scenario: write the
  ~/.claude/projects/<proj>/<sessionId>.jsonl a real claude would have
  persisted before the abrupt kill. Assertions untouched.

- opencode-terminal-restore-rust, amplifier-restore-rust: stale-terminalId
  capture race — the adopted client is non-destructive on boot (pane keeps
  persisted terminalId+status until the verdict folds), so the old
  "status not error && terminalId truthy" gate matched the STALE pre-restart
  id and the resume-marker poll scraped a dead terminal (error-context showed
  the resume marker ALIVE in the new terminal). Now polls until the id
  provably changed (wall-spec pattern) — a STRICTER gate; the marker/argv
  assertions are untouched.

- multi-client (reconnecting second viewer): legacy-observation rewrite —
  toHaveLength(1) encoded the pre-reconcile client's single attach source.
  Under paneReconcileV1 a reconnect legitimately has two: TerminalView's own
  reconnect path + the attach-verdict fold's deliberate reconcileEpoch
  re-fire (the A1 design pin: "every fold bumps reconcileEpoch"). Contract
  guarded is unchanged and still asserted: not dropped (>=1), no retry storm
  (<=2), reconnect-shaped intent, PTY size stable, shared output intact.

- session-directory-matrix: latent time bomb, NOT branch-caused — amplifier
  lastActivityAt folds transcript.jsonl/events.jsonl mtimes
  (getActivityMtimeMs port); seed-time mtime "now" began dominating the
  seeded 2026-07-19T08:00:02Z metadata timestamp the moment the wall clock
  passed it. FIXTURE REALISM: fs.utimes the seeded files to the seeded
  activity instant. Exact-match assertion untouched.

- harness-01-rust-server: parallel-load flake — passed in isolation both
  runs; under full-project load (14 workers + cargo build-lock contention)
  the post-restart marker wait exceeded its 20s budget. Budget raised to
  60s; the assertion itself (marker MUST appear) is unchanged.

All six reproducing specs re-run green in isolation (7, 6, 1, 1, 3, 2
passed); harness-01 green in isolation (1 passed).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ears stale batched UI state

Finding 1 (registry.rs): claim_session_ref's lease phase now re-checks
session_ref_bindings WHILE HOLDING the leases lock before acquiring. A
loser that passed checks 1a/1b pre-registration and was preempted across
the winner's register->complete window previously saw no lease and
answered Acquired -> second spawn on one sessionRef (the D8
duplicate-writer shape). Lock order leases->bindings matches
complete_session_ref_claim; acyclic. Lease phase extracted so the
interleave is pinnable sequentially (step 1b intercepts any full claim
while a binding exists, so only direct lease-phase entry exercises the
re-check). New test: lease_phase_rechecks_bindings_under_leases_lock
(RED pre-fix: got Acquired).

Finding 2 (App.tsx): after a non-cardinality-violation fold, App's
all-pane fold site (authoritative) dispatches clearReconcileWarming()
when the round reports 0 warming and clearDeadSessionAdjudication()
when 0 dead - a clean round after a WS reconnect no longer leaves the
warming banner or dead-sessions dialog up forever. Deliberately NOT in
foldVerdicts: single-pane requesters must not clear other panes' state.
New test in App.reconcile-adoption.test.tsx (RED pre-fix: warming state
survived the clean round).

Finding 3 (TerminalView.tsx, comment-only): reserveWindowStart comment
now claims exactly the effect-re-fire protection; a full unmount/remount
DOES reset the window (the ref dies with the component).

Finding 4 (multi-client.spec.ts, comment-only): stale "exactly one
re-attach" comment updated to the bounded 1..2 contract (reconnect
attach + at most one designed reconcileEpoch fold re-fire).

Gates: cargo fmt/clippy clean; cargo test -p freshell-terminal 131
passed; vitest App.reconcile-adoption + pane-reconcile 27 passed;
typecheck:client clean. No existing assertions weakened.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

# Conflicts:
#	test/e2e-browser/playwright.config.ts
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

# Conflicts:
#	test/e2e-browser/playwright.config.ts
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

# Conflicts:
#	crates/freshell-server/src/main.rs
#	crates/freshell-ws/src/amplifier_association.rs
#	crates/freshell-ws/src/lib.rs
#	crates/freshell-ws/src/opencode_association.rs
#	crates/freshell-ws/src/terminal.rs
#	crates/freshell-ws/tests/codex_managed_launch_e2e.rs
#	crates/freshell-ws/tests/codex_session_ref_resume.rs
#	crates/freshell-ws/tests/common/mod.rs
#	crates/freshell-ws/tests/diag01_lifecycle_events.rs
#	crates/freshell-ws/tests/freshagent_claude_attach.rs
#	crates/freshell-ws/tests/freshagent_claude_kill_interrupt.rs
#	crates/freshell-ws/tests/hello_timeout.rs
#	crates/freshell-ws/tests/keepalive.rs
#	crates/freshell-ws/tests/max_payload.rs
#	crates/freshell-ws/tests/origin_policy.rs
#	crates/freshell-ws/tests/pane_reconcile.rs
#	crates/freshell-ws/tests/safe08_restore_diagnostics.rs
#	crates/freshell-ws/tests/term09_output_queue.rs
#	test/e2e-browser/playwright.config.ts
- reconcile_freshagent.rs: honor B4's DELETE-AT-MERGE annotation (drop
  retry_after_ms from the single PaneVerdict construction point) and add
  the pre-decided ProviderUnavailable => Unknown presence arm (V9/A12:
  conservative respawn-with-cap, never dead_session).
- terminal.rs: fold B4's fresh_agent snapshot borrow into B1's rebuilt-deps
  derive closure (snapshot built ONCE per request - rebuilding would
  double-burn the respawn counter; V9 3.6); drop the now-dead outer deps.
- codex_association.rs / common/mod.rs / pane_reconcile_freshagent.rs:
  test fixtures gain the other lanes' WsState fields
  (reconcile_deferral_budget_ms, fresh_agent_respawn_counts, codex_locator).
- recovery_inventory_tests.rs: BindingRow fixtures gain B4's five optional
  resume-invocation fields.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…nventory

B3's RecoveryOfferPanel calls getRecoveryInventory on App mount; B1's
App.reconcile-adoption.test.tsx mocks @/lib/api completely and predates
that export. Mirror the stub B3's own App tests use (recoverable: false).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…minal

Under B1's adopted client, the page.reload restore path can assign the
pane a transient pre-verdict terminal that the pane.reconcile respawn
fold then replaces (reconcileEpoch bump). The spec sampled the FIRST
truthy terminalId and polled that stale terminal's buffer forever. Poll
the pane's current terminalId each iteration instead - the assertion now
targets the converged state the verdict produces. Evidence: argv log
shows the resume spawn ('resume <id>' adjacent pair) and the converged
buffer carries 'codex: resumed session <id>'.

The transient pre-verdict create itself (fresh CLI spawn racing the
verdict on reload) is documented as a wave-C follow-up - not silently
fixed here.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
B4 (freshagent-verdicts-resume) deliberately writes fresh-agent ledger
rows (paneKind: 'fresh-agent', pane_ledger.rs write_fresh_agent path),
obsoleting the wave-A pin 'fresh-agent UUID must never appear in the
pane ledger'. The invariant survives narrowed: the fresh-agent UUID may
appear ONLY as a kind:fresh-agent row (never a terminal row, and never
owning a liveTerminalId).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…viderUnavailable respawn mapping

Two integration-branch pins for the designed B1xB4 coexistence:
- warming_deferral_rederivation_burns_the_respawn_counter_once: a mixed
  terminal+fresh-agent request whose terminal pane triggers B1's bounded
  index-warming deferral re-derives ONCE reusing the fresh-agent snapshot
  (V9 3.6) - respawn counter burns exactly once (red-checked by mutating
  the burn to +2: test fails).
- provider_unavailable_existence_maps_to_respawn_with_cap: B1's new
  SessionExistence::ProviderUnavailable maps to B4's pre-decided arm
  (V9/A12): presence Unknown => respawn-with-cap, never dead_session and
  never the terminal arm's error{provider_unavailable}.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…known thread ids

Closes B2 plan item 10 (residual A4 risk, fix assigned to B4's
territory): the freshagent codex sidecar writes rollouts into the SAME
$HOME/.codex/sessions root the locator walks, so a foreign same-cwd
rollout appearing after a pane's first Enter could misbind as a sole
candidate. B4's kind:fresh-agent ledger rows (durable-before-answer at
thread start) + the live fresh_codex session map are the exclusion
signals: adopt_codex_identity now refuses a thread id that is live in
fresh_codex or recorded as a fresh-agent ledger row.

TDD: located_freshagent_known_thread_is_rejected written first (red -
adoption succeeded), guard added (green - 7/7 codex_association tests).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…aveB integration

B4 rewrote opencode handle_create to rebind durable ses_* ids from
resumeSessionId/sessionRef (handle_create_resume; pinned by
create_with_resume_session_id_rebinds_the_durable_session). B3's kept
sessionRef passthrough (build-recovery-plan) means recovered
freshopencode panes now RESUME, exactly as the forward-compat note
anticipated. No test pinned the fresh-start behavior (the pin was this
doc's known-limitation note).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ite (B4 fast-follow)

The REST send-keys materialization site now gets the SAME all-blank
settings guard as record_codex_binding and claude's consumer arm
(V7/A10): a pane with no model/effort/cwd never persists a blank
snapshot - it would make was_recorded true while load_settings returns
None, arming a FALSE SETTINGS_RESET for a legitimately-default create.

TDD: rest_send_keys_materialization_skips_all_blank_settings written
first (red - blank binding was recorded), guard added (green; the
populated-settings sibling still passes).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…/reload race

sw.js does install -> skipWaiting -> clients.claim, so on FIRST boot the
controllerchange fires on a page that was never controlled. The
unconditional stale-client reload raced B3's recovery offer: by the time
the reload landed, the auto shell tab had persisted a layout,
hadPersistedLayoutAtBoot flipped true on the reloaded boot, and the
offer was permanently lost. Only a genuine update swap (controller
already existed at registration) reloads now.

TDD: first-claim-no-reload test written first (red - reload fired);
guard added (green, 6/6); the update-swap reload test sharpened to seed
an existing controller.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…reate-rung guard (B3 fast-follow)

The D7 server guard on the direct wire-sessionRef rung checks BOTH
stores (identity-registry owner probed Running + the registry-row scan);
the inventory's live_session_keys checked only registry rows with
resume_session_id. A locator-adopted terminal (codex/opencode/amplifier)
holds its session in the identity registry while the row's resume id
stays unset, so the inventory offered it for resume and the accept died
on the guard. live_session_keys now joins the identity registry too
(live entries whose owning terminal probes Running), so the offer and
the guard agree.

TDD: live_session_keys_includes_identity_registry_bound_sessions +
retired/dead negative pin written first (red - signature/behavior),
join added (green; freshell-server 323/323).

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
@danshapiro
danshapiro merged commit bf6242a into main Jul 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant