feat: wave-B restart-resilience — reconcile handshake live, codex locator, recover-my-panes, fresh-agent verdicts (4 lanes, integrated)#537
Merged
Conversation
🤖 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>
…e::Never branch is actually pinned
…ore brief's verbatim format! line)
…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>
…h dedupe + respawn cap
…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>
… paneReconcileFreshAgentV1 (P1.13 §4.3)
…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>
…-loss banner (P1.13)
…t resume record live
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):
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
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)
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)
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.