feat(rust-server): session resume-by-id parity with the hardened resolve contract (SYNC-06) - #592
Merged
Merged
Conversation
Falsified and fixed: zod 400 details literals rewritten to probed zod 4.3.6 wire output (A2/V2); opencode by-id fallback replaced with a bug-for-bug port of Node's parent-chain walk incl. legacy-schema and truthy-directory quirks (A3/V3); resolve snapshot now filtered through deleted session overrides matching Node and the Rust sidebar (A4/A14/ V4/V7). Recorded accepted deviations (transport/405, locator deltas, index membership edges), added e2e npm-ci precondition, aligned the checklist step with the SYNC-05 PARTIAL convention.
…y plan - Task 1 fixture: restore the existing suite's bare-v7-uuid id-shape hint case (the only bare-id coverage of the version==='7' -> codex deriveHint branch); fixture is now 31 cases, all >=30 guards and expected pass counts (TS 32, Task 9 evidence/gates) bumped to match. - Task 6 Step 1: un-gate ONLY SessionMetadataStore::get_all; keep get behind #[cfg(test)] (no production caller in a binary crate, so un-gating it would trip dead_code and fail the clippy -D warnings gates in Task 6 Step 6 / Task 9 Step 1). Replace get_all's stale 'Test-only today' doc line with the SYNC-06 production-read comment.
…ity plan - Task 1: add Step 0 node-deps guard (test -d node_modules || npm ci) so Task 1 Step 3's vitest run (tsx is an uninstalled devDependency), Task 6 Step 5's cargo test (safe11 fixture imports 'ws'), and Task 7 Step 5's typecheck/vitest can pass on the fresh worktree; idempotent guards repeated inline at Task 6 Step 5 and Task 7 Step 5. - Task 2: replace hex_tokens.sort_by(|a, b| b.len().cmp(&a.len())) with sort_by_key(Reverse(len)) — the former trips clippy's warn-by-default unnecessary_sort_by under the plan's -D warnings gates; still a stable sort, behavior identical. - Task 6: replace snapshot.as_ref().map(|s| s.as_slice()) with snapshot.as_deref() — the former trips clippy's warn-by-default option_as_ref_deref under -D warnings.
…pt_cwd export commit (SYNC-06)
…legacy contract (SYNC-06) Ports the resume-resolve matching core against the retired pre-#586 Node resolve contract, not the hardened in-tree implementation. Hardened #586 behavior (per-token exact->fallback->prefix ranking, provider-health/degraded status, budgeted shape-gated fallbacks) landed in later commits on this branch.
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…count to 23 tests Load-bearing pass over the hardened-parity plan: 24 assumptions verified with run/inspect evidence (worktree rebase state, hardened parser regex + cap-8 + fixture-probe run proving the plan's 5 new cases byte-exact and exactly one stale case failing, router merge semantics + wire field order, RESOLVE_MATCH_CAP=20, budget-2 shape-gated fallbacks, ses_ case sensitivity, by-id row query, Rust drift + API surface for Tasks 3-6, cargo baseline green at the rebased HEAD). One falsification: the Node core suite has 23 it() tests, not 25 — Task 3 Step 1 corrected. Ledger: .worktrees/.the-usual-logs/rust-resolve-parity-hardened/load-bearing-ledger.md 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ion 1) Reviewer (independent cross-model, gpt): FAILED with 16 major issues. Fixes, all verified against the worktree: - Task 1: spec doc is already committed — verify (clean tree), no re-commit - Spec doc: contract bullet updated to hardened #586 response shape (degraded/providerErrors/unsearchedProviders/homeDir, never-silent) - set -o pipefail on every piped cargo gate (Tasks 1/6/7); workspace passed-count now summed from per-binary 'test result:' lines - Task 3: explicit 23-test mapping to the Node core suite (every it() title -> verbatim/carryover/new Rust test) - Task 4: real locked-DB test (second connection holds BEGIN EXCLUSIVE, busy_timeout 500ms -> Err) - Task 5: coding_cli_enabled_providers is async (tokio RwLock, mirrors get(); session_overrides' std Mutex is not the pattern); scan-recovery test polls via wait_until (stale-while-revalidate refresh is detached) - Task 6: settings fixtures corrected to .freshell/config.json wrapped {version,settings}; JoinError (panicked resolver) -> explicit 500, recorded deviation (Node: unhandled rejection), never ready-empty; checked claude locator uses Node's authoritative subagents layout + transcript_cwd_checked propagating non-absence errors; fallbacks ALWAYS wired (Node does not settings-gate fallbacks); homeDir from OS user home (HOME/USERPROFILE), never resolve_home()/FRESHELL_HOME - Task 7: e2e claims scoped to what resume-button.spec.ts proves; explicit --project=legacy-chromium --project=rust-chromium filters 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ion 2) - Test 1 of the 23-mirror: assert the id-shape claude hint (Some), not hint==None — a v4 UUID always derives the hint on both parsers - Scan-failure tracking extended to file-backed sources (claude/codex/ amplifier) via SessionSource::discover_checked; false 'recorded deviation' removed; file-backed failure test added - Enabled-provider set read BEFORE core resolution and used to FILTER the snapshot (Node index-gating parity); fallbacks stay ungated; disabled-indexed-miss + disabled-fallback-hit endpoint tests added - Rust default enabledProviders aligned to Node's four-provider DEFAULT_ENABLED_CLI_PROVIDERS (adds amplifier) instead of pinning the current three-provider default; settings.rs added to Task 5 - Checked claude locator treats NotFound OR NotADirectory as expected absence (ENOENT||ENOTDIR parity); ENOTDIR test added - transcript_cwd_checked bounded to the first 64 KiB (CWD_SCAN_BYTES parity); beyond-64KiB test added - OpencodeByIdError preserves rusqlite SQLITE_* codes via sqlite_error_code(); production closure forwards them; missing/ corrupt/locked tests assert codes — degraded wire test now production-true - degraded request_refresh convergence endpoint test added - Permission test #[cfg(unix)]-gated with a root/CAP_DAC_OVERRIDE skip probe - Task 7 Step 2 now RUNS the shared contract test and the client suite (ResumeSessionDialog degraded/retry/homeDir coverage) as evidence 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…586) Fresh-eyes review found the Rust port targeted the retired pre-#586 resolve-session.ts rather than the hardened in-tree implementation, and corrects the false parity claims made in commits 9a59749 and 7ac205d. - resume_input.rs: add MAX_RESUME_CANDIDATES=8 work-budget cap (truncate before hint derivation, mirroring TS) and replace the generic [a-z]{2,10}_ prefixed-id regex with the hardened 9-family pattern (ses|sess|session|thread|thr|run|msg|task|amp)_[0-9A-Za-z]{8,64} - resume_resolve.rs: per-token exact -> exact-id-fallback -> prefix ordering (prefix never outranks an exact fallback hit); case-insensitive comparison only for uuid/hex-like tokens (ses_ base62 now case-SENSITIVE); exclude subagent sessions from prefix discovery; always emit session_type (defaults to provider) - module docs rewritten honestly: the hardened response surface (degraded/providerErrors/homeDir, warming default) is NOT yet ported, tracked in docs/plans/2026-07-30-rust-resolve-parity-hardened.md Tasks 3/5/6; stale resolve-session.ts line refs removed - parser-cases.json: fix abc_12345678 case (no candidates under hardened semantics), add 10-uuid -> first-8 cap case (32 cases total) - resume-input-parser.test.ts: wire the fixture into the TS unit suite so the cross-language anti-drift claim is actually true (fixture now consumed by BOTH the TS test and the Rust parity test) - resume_resolve.rs tests: honest module doc (matching-semantics subset, not a 1:1 mirror), rewritten ordering/case tests, new tests for fallback-beats-prefix, subagent exclusion, ses_ case sensitivity cargo test -p freshell-sessions: 207 passed; -p freshell-server: 430 passed; clippy -D warnings clean; vitest resume-input-parser: 70 passed
…e e2e until hardened port lands Fresh-eyes review flagged shipping the resolve endpoint under sessionResolve: true while its response surface still lacks the hardened degraded/providerErrors/homeDir fields the shared dialog consumes. - main.rs: declare sessionResolve: false (held until the hardened response surface from docs/plans/2026-07-30-rust-resolve-parity-hardened.md Tasks 3/5/6 is ported); flag-pinning tests updated - playwright.config.ts: remove resume-button.spec.ts from MATRIX_SPECS (chromium-only again) so CI does not gate on the Rust matrix run until the flag is genuinely earned
Fresh-eyes review found the SYNC-06 entry asserted evidence that is false at HEAD: dual fixture consumption (the TS test did not consume the fixture until now), a '14 passed, unchanged' Node suite count (the in-tree hardened suite runs 25 tests), and evidence commit c38422a0 which is not an ancestor of HEAD. Entry rewritten truthfully: PARTIAL/REOPENED — hardened matching semantics (ordering, case gating, subagent exclusion, work budget, sessionType emission) landed; hardened response surface deferred to the hardened plan Tasks 3/5/6; sessionResolve rolled back to false and resume-button.spec.ts de-matrixed until it lands
…on 3)
Cross-model reviewer (openai, zero-context) found 6 blocking plan defects;
all fixed, facts verified against the worktree (reports/iter3-facts.md):
1. OpenCode wire parity: Node's production worker boundary strips .code
(worker serializes {name,message} only; runner rebuilds Error without
code), so the wire is message-only. Production closure now maps
OpencodeByIdError to ProviderFailure{code: None}, logs the internal
SQLITE_* code via tracing; core + endpoint tests assert code-ABSENT
wire shape; Task 4 doc + Self-Review 1b reworded (internal code =
logging/message fidelity, not wire).
2. Claude locator roots: locate_transcript_checked now takes
projects_roots as a parameter (Node's locator takes projectsDir);
caller resolves CLAUDE_HOME||$HOME/.claude + /projects (Node
getSessionRoots parity; claude_home_candidates explicitly forbidden
- CLAUDE_CONFIG_DIR root excluded); global two-pass
direct-then-subagent ordering with precedence test (f).
3. errno_code preserves the raw OS errno symbolically via libc
(EPERM stays EPERM; EIO/EMFILE/ENOTDIR/etc. covered; unknown => None;
non-unix kind-based fallback) with unit tests incl. EPERM!=EACCES.
4. 64 KiB cwd scan parses EVERY segment including the final one (Node
split('\n') has no discard-tail rule); new tests (g) complete final
line without trailing newline parses, (h) boundary-straddling
fragment skipped without error.
5. settings_store.rs legacy migration DEFAULTS widens to four providers
(adds amplifier, mirroring Node DEFAULT_ENABLED_CLI_PROVIDERS);
legacy [claude,codex] migration test added.
6. Checked-locator tests hermetic by construction (injected temp roots,
zero env mutation; CLAUDE_ENV_LOCK + EnvVarsRestore pattern cited if
ever needed).
Reviewer's minor (unstructured scan-failure logging) and nit (---###
separator) intentionally not fixed per step policy (blocking-only).
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
The '---### Task 6:' run-on line broke markdown heading parsing and the execute stage's task-brief extraction (Task 5 briefs swallowed Task 6; Task 6 briefs came back empty). Formatting only, no content change. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…bounded cwd read
The previous port's fallback gates diverged from Node's resolve-fallbacks.ts
in two wire-observable ways:
- The opencode fallback gate was only `kind == PrefixedId &&
starts_with("ses_")` (any 8-64 base62 chars), while Node's
FALLBACK_ID_SHAPES gate is the FULL id `^ses_[0-9a-zA-Z]{26}$`. On a
legacy-schema opencode DB (universal-HIT by-id lookup), any wrong-length
`ses_*` token produced a FALSE exact hit where Node answers a miss with
zero work.
- There was NO per-request fallback budget: up to MAX_RESUME_CANDIDATES = 8
fallback invocations per request (each claude one a full projects-dir
scan), while Node's withRequestBudget caps each fallback at
FALLBACK_BUDGET_PER_REQUEST = 2 REAL invocations per request. A third
fallback-requiring token resolved on Rust but is not-found on Node.
Mirror Node exactly (resolve-fallbacks.ts withRequestBudget semantics):
- Full-id shape gates: `^[0-9a-fA-F]{8}-…-[0-9a-fA-F]{12}$` for the claude
fallback (replacing the equivalent-but-implicit `kind == Uuid` check with
Node's explicit shape), `^ses_[0-9a-zA-Z]{26}$` for opencode.
- FALLBACK_BUDGET_PER_REQUEST = 2, ONE counter PER FALLBACK (Node's
budgeted() closes over a separate `used` per key), fresh per request,
checked shape-FIRST budget-SECOND (Node: "order is load-bearing" — a
wrong-shape token must not consume budget), consumed by the real
invocation itself, hit or miss.
- The inline comment that falsely claimed the old gates matched "Node's
budgeted fallbacks" now describes the actual mirrored semantics.
Also bound the claude fallback's transcript cwd read (main.rs wiring) to
Node's CWD_SCAN_BYTES = 64 KiB: new transcript_cwd_bounded() reads at most
the first 64 KiB, splits on newline, and parses every segment INCLUDING the
final unterminated one (Node's head.split('\n') loop has no
discard-the-tail rule) — the hardened plan (Task 3a/3b,
docs/plans/2026-07-30-rust-resolve-parity-hardened.md:1807) explicitly
forbids mirroring the unbounded BufRead::lines() transcript_cwd for
resolve, where one request could scan a multi-GB transcript.
transcript_cwd itself is untouched for its other consumers; the
error-PROPAGATING checked variant stays deferred to plan Task 3.
Tests (crates/freshell-sessions/tests/resume_resolve.rs +
claude_snapshot.rs unit tests):
- wrong-length/wrong-charset ses_* tokens never reach the opencode
fallback (panicking lookup) and yield ready-empty,
- the claude gate accepts a full UUID in any hex case and rejects bare
hex-prefix tokens,
- a third fallback-requiring ses_ token in one request is budget-gated
(exactly 2 real lookups, not-found even though the third would hit),
- shape-gated tokens do not consume budget (a valid id after two
wrong-shape ones still resolves),
- budgets are tracked per provider (two opencode misses do not exhaust
the claude budget),
- transcript_cwd_bounded never scans past the 64 KiB prefix and still
parses a complete unterminated final line.
…fs and SYNC-06 overstatement The KNOWN DIVERGENCE / NOT YET PORTED disclosures in the two resolve module docs claimed the only remaining delta was the response surface (degraded/providerErrors/unsearchedProviders/homeDir/readiness merge) while claiming to port 'the HARDENED matching semantics'. Undisclosed MATCHING-SEMANTICS divergences remained; the in-code record is what a follow-up implementer relies on, so both module docs (freshell-sessions/src/resume_resolve.rs, freshell-server/src/resolve.rs) now enumerate every remaining divergence: - opencode by-id fallback runs the RETIRED resolveOpencodeSessionRoots parent-walk, not Node's hardened direct row query (providers/opencode-by-id-query.ts): orphaned/cyclic child rows are a Rust MISS where Node HITs; a legacy-schema DB is a Rust universal HIT for any full-shape ses_* id where Node hits only real rows; Rust hits omit Node's title/lastActivityAt. - fallback hits hardcode sessionType "claude"/"opencode" instead of consulting the session-metadata overlay (Node's sessionTypeFor) — a freshclaude/freshopencode session resolved via fallback would resume under the wrong runtime. - the claude fallback's locate_transcript probes <project>/<subdir>/<id>.jsonl and never Node's <project>/<parent>/subagents/<id>.jsonl layout, so subagent child transcripts miss; read errors are swallowed as misses (no provider-error channel). The cwd read IS now bounded to Node's 64 KiB (previous commit); the checked locator + error propagation stay deferred to the hardened plan's Task 3. - response surface: degraded/providerErrors/unsearchedProviders/homeDir and the scan-failure/warming merge (plan Tasks 3, 5, 6). Stale doc refs fixed in the same blocks: - resume_resolve.rs referenced nonexistent 'resolveOpencodeSessionIds' (real name: resolveOpencodeSessionRoots). - parse/opencode.rs by-id doc said the walk is 'consumed by resolve-session.ts:59-85' — stale: hardened Node consumes resolve-session.ts -> resolve-fallbacks.ts -> the by-id worker (a direct row query); the walk remains only the Rust fallback's interim lookup, recorded as a divergence. Checklist (SYNC-06, 2026-07-14-rust-tauri-parity-completion-checklist.md): the entry claimed the Rust port 'matches the HARDENED in-tree matching semantics' with a DEFERRED list naming only the response surface, while budgeted/shape-gated fallbacks (now genuinely ported, previous commit) and the by-id lookup / sessionType-overlay / subagent-locator gaps are matching behavior. Rewritten truthfully: AT PARITY = parser (fixture anti-drift), matching core incl. Node's FALLBACK_ID_SHAPES gates + FALLBACK_BUDGET_PER_REQUEST=2 per-fallback budget + 64 KiB bounded cwd read, route shell; DEFERRED = the matching-semantics divergences above plus the response surface, flag re-enable, and e2e matrix.
…x bound in the shared fixture Residual fixture delta atop 13a3428's already-landed Task 2 work: the known-family prefix regex and MAX_RESUME_CANDIDATES=8 cap are already committed on both sides, but the anti-drift table had no positive thread_/task_ family case and no suffix longer than 40 chars (the old Rust regex capped at 40, so a regression there would go uncaught). Adds two cases pinning the thread_ family and a 46-char task_ suffix within {8,64} cross-language; both parsers pass unchanged. (SYNC-06) 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…rules, provider-health channel, budgeted fallbacks Per-token exact→fallback→prefix order (a prefix match never outranks an exact resolution); ses_ ids case-SENSITIVE, uuid/hex case-folded; subagents excluded from prefix discovery; sessionType defaults to the provider name; fallbacks are shape-gated + budgeted (2/request/provider) and their failures surface as degraded + providerErrors — never a silent empty not-found. Mirrors test/unit/server/coding-cli/resolve-session.test.ts. Refactors atop the matching semantics already landed in 13a3428 and 7109a6e (ordering, case gating, subagent rules, shape gates, budgets): adds the Degraded status, ProviderFailure/ResumeResolveProviderError channel (fallback closures now Result — provider failure ≠ miss), OpencodeByIdHit with title/lastActivityAt + the sessionType overlay on fallback hits, and ResumeResolveOutcome replacing ResumeResolveResponse (the wire response moves to resolve.rs; LegacyWire keeps today's {status,matches,hint} shape until Task 6). Module divergence docs trimmed to what genuinely remains (Tasks 4/5/6). (SYNC-06) 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…query (archived+child included, errors propagate) Ports opencode-by-id-query.ts, replacing the #583 parent-walk. Full row (title/timestamps) feeds the resolve match; a missing/locked/corrupt DB is Err — provider unavailable ≠ not found. (SYNC-06) 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ssionIndex; enabled-providers reader + Node-parity default on SettingsStore getScanFailures()/requestRefresh() parity plumbing for the hardened resolve route (SYNC-06). All four real sources participate in failure tracking: direct-listed (opencode) and file-backed (claude/codex/ amplifier) root-listing failures are recorded, never silently treated as an empty listing. Default enabledProviders now includes amplifier (DEFAULT_ENABLED_CLI_PROVIDERS parity). Consequential parity updates: the captured handshake fixture (port/oracle/fixtures/handshake-transcript.json) and the protocol roundtrip assertion pinned the pre-amplifier three-provider default; both now carry Node HEAD's four-provider set (the fixture test is designed to fail loudly when the original's default tree shifts — it shifted at shared/coding-cli-defaults.ts:3). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ng the mutex across the sweep perform_refresh locked the scan_failures std mutex at the top of the spawn_blocking sweep closure and held it for the entire discovery+parse sweep, so the sync accessor scan_failures() (called by the resolve route from async context right after a stale-while-revalidate snapshot()) blocked a tokio runtime thread for the full sweep duration — exactly when degraded responses are being served. The sweep now clones the failure set in a short lock, mutates the LOCAL copy (refresh_snapshot's record/clear semantics unchanged for both the direct-listed and file-backed branches), and swaps it back into the shared mutex in a short lock at publish time, alongside the snapshot publish. The JoinError (panic) path leaves the shared set untouched, mirroring its preserve-the-published-snapshot stance. Lost updates are impossible: refresh_lock guarantees at most one sweep at a time. Regression test: scan_failures_is_readable_while_a_sweep_is_in_flight blocks a sweep inside discover_checked() and asserts a mid-sweep scan_failures() read completes within 500ms (fails by timeout on the old code). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ers/homeDir, scan-failure merge, degraded fire-and-forget refresh POST /api/sessions/resolve now emits the full #586 contract; production fallbacks report failures (checked claude locator, propagating opencode by-id) instead of swallowing them; degraded-path proven on the wire (broken store -> degraded + providerErrors, never silent not-found). Async hygiene verified: all blocking fallback IO runs inside the endpoint's spawn_blocking; work bounded by cap-8 candidates x budget-2 fallbacks. (SYNC-06) Recorded deviation: a JoinError (panicked resolver) now answers an explicit 500 instead of a fabricated ready-empty — Node has no defined behavior there (unhandled rejection, no response), and presenting an unsearchable state as a healthy not-found is the incident class this plan closes. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…e2e — hardened port landed Reverses the f50da2e rollback now that the hardened resolve response surface (degraded/providerErrors/unsearchedProviders/homeDir, warming default) is fully ported (hardened plan Tasks 2-6): - main.rs: declare sessionResolve: true (flag-pinning tests flipped first, RED, then the flag, GREEN) - playwright.config.ts: restore resume-button.spec.ts to MATRIX_SPECS so it runs on both legacy-chromium and rust-chromium 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…checklist 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…n exact cap-boundary assert The module-doc divergence ledgers in resolve.rs and resume_resolve.rs still claimed the sessionResolve capability flag was held false pending plan Task 7; 48b67f8 flipped it to true and ran the resume-button e2e matrix green. Rewrite both ledgers to the truthful state: flag declared true in build_platform_payload, no known unported divergences beyond the RECORDED DEVIATIONS (explicit 500 on resolver panic; homeDir omitted when no resolvable home). Also tighten the Node-mirror test #7 tail assertion from >= Some(5) to exactly Some(5) (25 sessions, activity 24..0, cap 20). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ot+failures atomically Three blocking findings from the independent cross-model review of the resolve-parity port, fixed at the system level: 1. OpenCode health gated on the mtime token (directory_index.rs): a db that becomes locked/unreadable WITHOUT an mtime change (chmod moves ctime, not mtime; locks move nothing) kept answering ready from stale evidence. The sweep now consults a per-sweep direct_health_check() whenever the change token is unchanged (Node parity: refreshDirectProvider() runs on every full scan) — the token still gates only the expensive re-query of session DATA. OpencodeProvider gains health_check() (read-only open + one sqlite_master page read, the same open path direct_list uses). The masking test's artificial always-changing token was replaced with a CONSTANT token, plus a real-OpencodeSource chmod-000 test proving unchanged-mtime outages are recorded and cached sessions preserved. 2. TOCTOU in file-backed discovery (directory_index.rs, amplifier.rs): the disposable ensure_root_listable() preflight followed by a second, error-swallowing read_dir meant a failure between the two calls — or a per-entry iterator error — returned Ok(empty), CLEARING the provider's scan failure and pruning its cache from a sweep that never listed anything. Claude/codex/amplifier discover_checked() are now SINGLE-PASS: the one read_dir handle opened (open_root_dir) is the one traversed, and root per-entry errors propagate with `?`. Missing roots stay a genuine empty; nested errors stay tolerant (Node parity). New tests exercise the REAL sources (chmod-000 roots), not injected discover_checked doubles. 3. Torn snapshot/failure reads (directory_index.rs, resolve.rs): the snapshot and scan_failures published under separate locks at separate times, so a request could pair a failed-scan empty snapshot with a subsequently cleared failure set (healthy-looking ready+matches:[]) or a recovered snapshot with stale failures. scan_failures now lives INSIDE CachedSnapshot — one lock write publishes the whole generation — and the route takes ONE coherent read via the new snapshot_with_failures() instead of two temporally separated reads. Short-lock discipline preserved (nothing held across a sweep or await). New atomic-generation test pins that no incoherent pair is ever observable. Verified: cargo test -p freshell-sessions (151 lib + integration, 0 fail), cargo test -p freshell-server (438+, 0 fail), cargo fmt --check, cargo clippy --workspace --all-targets -D warnings. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…on and bound the whole resolver in a 15s deadline
Two blocking findings from the cross-model review of the hardened
resolve port:
1. provider_home() (session_directory.rs) read only HOME, but production
Tauri inherits the desktop environment WITHOUT setting HOME, so a
native-Windows USERPROFILE-only environment built no session index
(session_index: None) and /api/sessions/resolve answered warming
forever while the sessionResolve capability stayed advertised. Now
HOME-then-USERPROFILE - an APPROXIMATION of Node's os.homedir(), not
its actual platform contract (Windows os.homedir() reads USERPROFILE
and never consults HOME; POSIX falls back to the passwd entry when
HOME is unset) - aligned with the then-existing homeDir wire-field
resolution. Exact os.homedir() platform semantics landed in a later
commit on this branch ("fix(server,sessions): resolve user home via
Node os.homedir() platform semantics"). Pinned by
provider_home_falls_back_to_userprofile_when_home_unset and
provider_home_prefers_home_over_userprofile; the none-case test now
clears USERPROFILE too.
2. The resolve route awaited the whole spawn_blocking resolver with no
deadline: a filesystem/SQLite op stalled outside SQLite's 500ms busy
handling held the request and a blocking-pool worker forever. The
route now bounds the ENTIRE resolver task with a 15s
RESOLVE_OUTER_DEADLINE. The 15s value is borrowed from Node's by-id
runner timeout (opencode-by-id-runner.ts DEFAULT_TIMEOUT_MS), but the
SCOPE is not Node's: Node applies that timeout per individual worker,
not around the whole resolver - a later commit on this branch
rescoped the deadline to each fallback dispatch to match. On elapse
the task is abandoned (recorded deviation: blocking tasks cannot be
cancelled, Node terminates its worker) and the request answers a
degraded 200 with message-only providerErrors (every enabled
provider reported unsearchable, never a healthy-looking not-found),
hint preserved, refresh fire-and-forgotten. Deadline injectable via
ResolveState.resolve_deadline; pinned by
a_stalled_resolver_answers_degraded_at_the_outer_deadline_never_hangs.
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…nces; reconcile spec divergences; add commit-message errata Fixes three blocking documentation findings from independent review: 1. 2026-07-30-rust-resolve-parity-hardened.md: the 10 Task-4 and 5 Task-6 test blocks had comment-only bodies that would pass vacuously if pasted literally. Each body now keeps its behavioral spec as comments and ends in unimplemented!() (a literal paste fails loudly), with post-execution notes pointing at the real verifiers that landed under the same names in crates/freshell-sessions/tests/opencode_row_by_id.rs and the resolve.rs test module. The self-review placeholder-scan line is corrected to admit the fifteen placeholders it previously denied. Trailing whitespace at the flagged lines removed. 2. 2026-07-29-rust-resolve-parity-spec.md: Requirement 1's blanket "identical validation and error shapes" claim is corrected to name the three deliberate, ledgered divergences recorded in resolve.rs's "Accepted deviations" (malformed/scalar JSON body 400 shape, 2 MB vs 1 MB body limit, non-POST 405 routing); the parser reference now describes all nine prefixed-id families with 8-64-char suffixes instead of only ses_. 3. 2026-07-14-rust-tauri-parity-completion-checklist.md: SYNC-06 gains an ERRATA entry recording that commits 9a59749 and 7ac205d overclaimed Node parity (retired pre-#586 contract), that 13a3428/f50da2e05 documented and disabled it, and that 08cc7df/48b67f838 delivered the hardened parity. History is pushed and not rewritten; the note is the accurate record. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…cap resolver tasks with permit-scoped admission + cooperative cancel Finding 1 (native Windows/Tauri claude fallback): the exact-id claude locator read only CLAUDE_HOME || HOME/.claude, so the documented USERPROFILE-only Tauri environment answered a healthy-looking ready-empty where Node (os.homedir()) finds the transcript. Extract the locator body to resolve_claude_exact_id_fallback() and route its home — plus the homeDir wire field (which also accepted an EMPTY HOME verbatim) and the opencode data-home helper in freshell-sessions — through the shared HOME||USERPROFILE, empty-as-unset resolution (provider_home() / a pure home_dir_from()). Tested at the fallback itself: a USERPROFILE-only env with a .claude transcript fixture now resolves it. Finding 2 (blocking-pool exhaustion): the 15s outer deadline bounded only the HTTP wait — an abandoned spawn_blocking resolver ran on (and repeated requests could accumulate stalled tasks until Tokio's blocking pool exhausted). Now: (a) an 8-permit semaphore whose permit MOVES INTO the blocking task caps concurrent (abandoned included) resolver tasks; a permit-starved request degrades within the same deadline with the same timeout-shaped 200 instead of queueing; (b) a cancel flag checked before every fallback invocation stops an abandoned resolver at its next fallback boundary. Precisely: task COUNT is bounded and provider work stops at fallback boundaries — an individual stalled FS/SQLite op inside a fallback still cannot be killed and holds its permit until it returns (recorded deviation from Node's worker.terminate()). Node's 15s degraded response shape is unchanged. Tests: fallback-level USERPROFILE-only resolution; empty-HOME fallthrough for the homeDir helper, provider_home(), and the opencode home helper; permit saturation degrades request N+1 without invoking an (N+1)th resolver body and the abandoned task returns its permit when its op completes; a cancelled resolver skips the second fallback invocation (with a non-cancelled control proving two would otherwise run). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…fallback; archive superseded plan; fix SYNC-06 checkbox contradiction Three blocking documentation findings from independent cross-model review: 1. 2026-07-30-rust-resolve-parity-hardened.md: the Task 6 Step 3b wiring instructed a CLAUDE_HOME -> HOME-only fallback for the exact-id claude locator (and a raw HOME||USERPROFILE homeDir chain that accepted an empty HOME verbatim), which cannot achieve native-Windows parity where Tauri leaves HOME unset. The wiring snippets now describe the LANDED behavior — resolve_claude_exact_id_fallback() and resolve_wire_home_dir() in main.rs, both resolving homes via session_directory::provider_home() (HOME then USERPROFILE, empty treated as unset) — and a new verifier list names the landed tests, including the USERPROFILE-only exact-id fallback test claude_exact_id_fallback_finds_transcript_in_a_userprofile_only_environment. 2. 2026-07-29-rust-resolve-parity.md: added an unmissable ARCHIVED / SUPERSEDED / DO NOT EXECUTE banner at the top. The plan implements the retired pre-#586 OpenCode parent-chain resolver contract, its parity reference (lines ~62-72) describes the obsolete pre-#586 behavior, and its expected test counts (~line 3103) are stale; re-executing it would reintroduce defects resolved by the hardened plan. Body unchanged — the banner is the fix. 3. 2026-07-29-rust-resolve-parity-spec.md: Requirement 8 said "Mark SYNC-06 done" while the same spec declares the required PW-TAURI-WIN validation out of scope and the implementation plan requires the checkbox stay unchecked. Resolved in favor of the plan/checklist: the spec now instructs recording the evidence as a PARTIAL bullet with the checkbox left UNCHECKED, and the Goal is reworded to "deliver the Rust-server half" so no instruction contradicts the out-of-scope declaration. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…false Node-parity commit messages The two SYNC-06 commits that claimed "Node-parity semantics"/"Node-parity behavior" were reworded in place (message-only rewrite, identical trees) to state the retired pre-#586 legacy contract they actually implemented. Every descendant SHA changed; update the checklist's commit references to the new history and record the correction in the errata, including that origin/feat/rust-resolve-parity still holds the pre-correction history. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…m semantics Correct the interim HOME->USERPROFILE approximation (commits c6f3220b0 and bb357a598 applied HOME-then-USERPROFILE on ALL platforms) to Node os.homedir() / libuv uv_os_homedir platform rules: Windows reads USERPROFILE and never consults HOME; POSIX reads HOME when set and non-empty, else the effective user's passwd-entry home (getpwuid_r) -- so POSIX now resolves a home (and main.rs builds a real session index, no permanent warming) even with HOME unset, and a Windows process with both variables set indexes USERPROFILE, never HOME. - session_directory::provider_home() and parse/opencode's home_dir() now delegate to std::env::home_dir() (un-deprecated since 1.87; MSRV 1.96), which implements exactly these platform semantics. The homeDir wire helper and the claude exact-id fallback in main.rs route through provider_home() and inherit the fix. Provider-specific overrides (CLAUDE_HOME/CODEX_HOME/FRESHELL_AMPLIFIER_HOME) keep their precedence. - Tests: the cross-platform HOME||USERPROFILE pins are replaced with platform-conditional ones -- cfg(unix) tests assert the passwd-entry fallback (via a getpwuid_r oracle) and that USERPROFILE is NEVER consulted; cfg(windows) tests pin USERPROFILE-wins-over-HOME. The opencode pure home_dir_from tests become env-mutating tests serialized on a new crate-wide HOME_ENV_TEST_LOCK (directory_index's persist-path test now takes the same lock). - docs: SYNC-06 ERRATA line recording the interim approximation and this correction.
…h (interim: a shared cancel flag still skipped and blamed later providers)
Corrects the earlier whole-resolver deadline/admission scoping (an
interim approximation introduced by "fix(server): resolve claude
fallback + homeDir via provider_home and cap resolver tasks with
permit-scoped admission + cooperative cancel"): the 8-permit semaphore
and the 15 s deadline previously wrapped the ENTIRE resolver - permit
wait, input parsing, in-memory index matching, warming and no-candidate
paths - so eight stalled fallbacks made every later request (exact
index hits, garbage input, warming) wait for admission and then return
a fabricated degraded result blaming every enabled provider.
Node scopes its 15 s timeout to the individual by-id worker
(opencode-by-id-runner.ts DEFAULT_TIMEOUT_MS); its cheap paths never
wait on fallback-worker availability. This commit moves the deadline
toward that scoping but does NOT fully reach Node's per-worker
behavior:
- each blocking fallback invocation now runs on its OWN spawn_blocking
task via bounded_fallback(): permit wait + fallback bounded by the
(renamed) RESOLVE_FALLBACK_DEADLINE, permit MOVED INTO the task so a
stalled syscall holds it until it returns (accumulation still capped
at RESOLVE_MAX_CONCURRENCY = 8), and a panicking fallback still
resumes into the resolver task so the explicit 500 contract holds
- parsing/validation, warming checks, index-only resolution, and
no-candidate responses run WITHOUT a permit and WITHOUT the deadline
and now succeed under full saturation
- REMAINING DIVERGENCE from Node: one cooperative cancellation flag is
SHARED across all of a request's fallback dispatches, so the first
timeout skips every subsequent fallback - including other providers'
never-dispatched work - and records fabricated timeout providerErrors
for them. A timeout can therefore blame providers whose fallbacks
were never dispatched, unlike Node, where a worker timeout affects
only that worker's provider. Corrected in a later commit on this
branch ("fix(resolve): bound each fallback dispatch independently -
a timeout never skips or blames other providers").
Tests (RED first against the old scoping): exact index hit / garbage
no-candidate input / warming all succeed with a zero-permit semaphore;
the existing stall/starvation/cancel tests reshaped to pin the new
scoping (request C exact hit succeeds while the only permit is still
held). Checklist ERRATA line added recording the correction.
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…mpletion record The plan was fully implemented on feat/rust-resolve-parity but still read as an active, unchecked executable task sequence (blocking review finding). Add a prominent EXECUTED / COMPLETION RECORD / DO NOT EXECUTE banner and annotate the four staleness defects in place: - Task 1 Step 4 drift findings marked as the pre-execution baseline (all five items have landed at HEAD) - Task 3 Step 2 and Task 6 Step 1 'expected compile FAILURE' runs marked as historical RED gates satisfied during execution (commits 5a3332be3, 1480e2a71), no longer reproducible at HEAD - Task 7 Step 4's superseded pre-reword c38422a0 reference annotated to the reworded history (checklist remap 97ab164a0, evidence bullet citing 22022a848) - Task 7 Step 5's plain first-time push annotated: origin holds divergent pre-reword history; publishing requires a deliberate git push --force-with-lease (safety tag pre-reword-backup) Checkboxes are preserved in their historical unchecked working form and declared as such under the banner. Annotation over deletion throughout; no code changes (cargo check clean).
…itleOverride and deleted-override key shapes
The resolve route previously filtered the index snapshot with only a
composite-key ('provider:sessionId') deleted check, so a renamed session
could resolve with its stale parsed title and a session deleted under a
bare-session-id or legacy Claude transcript-basename override key stayed
visible to prefix matching.
Port Node's override lookup chain and its deleted/titleOverride
application (session-indexer.ts:1173-1187 lookup chain, applyOverride
at 204-220, consumed by resolve-session.ts:85):
- lookup_session_override: composite key, then bare session id, then
(claude only, when the transcript basename differs from the parsed
session id) legacy 'claude:{basename}' then bare '{basename}' -- first
present entry wins, matching Node's || chain.
- project_session_through_overrides: 'deleted' hides the session;
a non-empty 'titleOverride' replaces the title before matching.
Other override fields (summary/createdAt/archived) never reach the
resolve wire shape and are not projected.
NOT ported here: Node's provider-generated titleSource suppression
branch (applyOverride, session-indexer.ts:210-211 - a dir/first-message
sourced titleOverride never clobbers a current provider-generated
title). The Rust index did not yet track titleSource, so this
projection unconditionally substitutes a non-empty titleOverride; the
suppression branch was implemented in a later commit on this branch
("fix(resolve): port Node's provider-generated-title suppression into
the override projection").
Tests (TDD, each watched fail RED first):
- title_override_projects_onto_resolve_matches
- bare_session_id_deleted_override_hides_prefix_matches
- legacy_claude_composite_basename_deleted_override_hides_the_session
- legacy_claude_bare_basename_deleted_override_hides_the_session
The existing composite-key deletion test is unchanged.
…never skips or blames other providers ffa4aac1a's interim cooperative cancel flag was SHARED across both providers' fallback dispatches: once one dispatch timed out, every later fallback in the same request returned a fabricated timeout without invoking its provider. For input with an opencode ses_ id followed by a claude UUID, an opencode stall skipped the healthy claude lookup, omitted the claude match, and blamed claude for a timeout it never had — contradicting Node, which bounds each by-id worker individually, records the rejection for that ONE provider, and continues through later candidates/providers (resolve-session.ts:133-156, opencode-by-id-runner.ts per-worker timeout). Remove the shared flag: each dispatch keeps its own 15 s deadline and 8-permit admission (ffa4aac1a's scoping otherwise intact); a timeout blames only the provider whose dispatch elapsed, and later dispatches — same or other provider — run with their own bounds. Abandoned-task accumulation stays bounded WITHOUT the flag: the per-provider budget (2) caps dispatches within a request, the semaphore caps tasks across requests. Tests: new cross-provider pin (opencode stalls, claude healthy — claude match returned, only opencode blamed); the shared-flag skip test is reshaped to pin the corrected contract (second candidate's dispatch still runs; first-error-wins keeps one wire entry). Checklist ERRATA records the interim flaw.
…xists
Through 37be35b9a each fallback dispatch awaited acquire_owned() inside
the outer resolver spawn_blocking worker (via Handle::block_on), so
under full permit saturation a dispatch parked that unbounded outer
blocking-pool worker for its entire 15 s deadline; with the per-provider
budget (2) across two providers, one crafted request could pin an outer
worker for ~60 s, and enough authenticated requests could exhaust
Tokio's blocking pool - the exact failure RESOLVE_MAX_CONCURRENCY
claims to prevent (iteration-5 reviewer finding).
Corrected: admission is now a synchronous try_acquire_owned() BEFORE
any fallback task exists. A permit-starved dispatch fails fast into the
existing degraded provider-error shape ('resolve concurrency limit
reached', message-only like Node's worker-timeout rejection) instead of
queueing; the per-dispatch deadline now bounds only the admitted
fallback task; per-provider blame and the cheap-path bypass (parsing,
index-only, warming, no-candidate never touch admission) are unchanged.
Node needs no analogue of this state: worker.terminate() reclaims its
stalled worker at the timeout, so it never caps admission
(opencode-by-id-runner.ts; resolve-session.ts:133-156 records the
rejection per provider and continues).
Tests: new permit_starvation_fails_fast_without_pinning_an_outer_worker
_for_the_deadline proves a starved dispatch answers degraded well under
a generous 5 s deadline with the fallback body never invoked (RED on
the old code: it queued the full 5 s and answered the timeout shape).
Strengthened saturated_permits_degrade_the_next_request_without_
spawning_another_resolver: request B now runs with a generous 10 s
deadline and must answer within 2 s, so an implementation that queues
for the deadline fails (the old 5 s allowance against a 100 ms deadline
let a full-deadline queuer pass).
Docs: ERRATA notes added to the hardened plan completion record and the
parity checklist recording that the interim design admitted inside the
outer worker.
…he override projection
The iter4 projection (75636c7ef) omitted one branch of Node's
applyOverride (session-indexer.ts:210-211): a stored titleOverride whose
recorded titleSource is 'dir' or 'first-message' must NOT replace a
CURRENT provider-generated title — Node preserves the provider's own
title; the Rust projection always substituted the stale override,
changing the title metadata resolve returns.
The Rust index carried no parse-layer title provenance, so this plumbs
Node's ParsedSessionTitleSource (types.ts:100 — sole variant
'provider-generated', hence a bool) faithfully through the snapshot:
- ParsedSessionMeta/IndexedSession gain title_provider_generated
(serde(default) keeps pre-existing persisted parse-caches loadable);
- claude parse sets it iff a custom-title/agent-name record produced the
title (providers/claude.ts:505; Node's third input, the
generated-summary-title extractor, has no ported extraction here so no
Rust-parsed title can originate from it);
- amplifier sets it iff the metadata 'name' is present
(providers/amplifier.ts:93); codex/opencode never set it, like Node;
- project_session_through_overrides now suppresses the override exactly
per Node's negated conjunction — every other override source ('user',
'ai', 'legacy', absent) still substitutes.
TDD: provider_generated_title_survives_dir_sourced_override and
provider_generated_title_survives_first_message_sourced_override were
RED first (title came back 'stale placeholder');
user_rename_still_overrides_a_provider_generated_title pins the
suppression's scoping. Existing rename/deleted-override tests unchanged
and green.
Docs: ERRATA line in the parity completion checklist recording the
omission and this correction.
Verified: cargo test -p freshell-server -p freshell-sessions (all
green), cargo fmt --all --check, cargo clippy --workspace --all-targets
-D warnings.
Node's global express.json() (server/index.ts:185, body-parser 1.20.4
default type 'application/json') only parses bodies whose media type is
exactly application/json (parameters stripped, case-insensitive; no
application/*+json); a skipped body leaves req.body = {} and the route
400s with the missing-input zod issue (sessions-router.ts:259-264). The
Rust route parsed the bytes as JSON regardless of Content-Type, so a
valid object under text/plain resolved on Rust while Node rejects it —
an unrecorded divergence.
Now the route parses JSON only when Content-Type matches the same
matcher; otherwise the body is treated as {} and answers Node's exact
400. New route tests pin text/plain and application/vnd.api+json
rejection, charset/case-variant acceptance, and the missing-header
case; spec ledger gains an ERRATA note that the divergence existed
unrecorded until this commit.
…tated commit messages An independent review found three commit messages materially overstated their commits (HOME||USERPROFILE claimed as os.homedir() parity + a whole-resolver deadline framed as Node's per-worker timeout; a shared cancel flag claimed to blame only actually-dispatched providers; an override projection claimed "full"/"exactly" while omitting the provider-generated-title suppression branch). Those three messages were reworded IN-HISTORY (message-only, trees byte-identical, safety tag pre-reword-backup-iter5), changing the SHAs of the three commits and every descendant. This commit remaps the stale SHA references in the completion-checklist ERRATA lines and the hardened-plan completion record to the post-reword SHAs, and adds a new checklist ERRATA entry recording the full old->new SHA map, the in-source resolve.rs comments that still cite pre-reword SHAs (source deliberately untouched), and that origin still holds the pre-correction history (publishing requires the user's deliberate git push --force-with-lease). 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…re-rebase Semantic rebase conflict: efe4ddf removed fail_json_code_retry_after as an unused helper, but origin/main (#589 graceful restore/resume) now calls it from the spawn-gate QueueFull path in terminal_tabs.rs. Restore the helper verbatim from origin/main. 🤖 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.
Summary
POST /api/sessions/resolveon the Rust server with full behavior parity to the hardened Node implementation (fix(resume): harden session-resolve — off-thread lookups, correct match ranking, provider-health channel, dialog guards #586), plus thesessionResolvefeature flag so the shared client shows the pinned sidebar Resume button on Rust builds.status:'degraded',providerErrors,unsearchedProviders,homeDir.ses_case-sensitive; subagent rules; saved-override projection incl.titleOverride+ legacy deleted keys; provider-generated-title suppression.express.json()Content-Type gating parity;HOME→USERPROFILEfallback.test/fixtures/resume-input/parser-cases.json, 34 cases) consumed by BOTH the TS and Rust parser test suites.MATRIX_SPECS(green on bothlegacy-chromiumandrust-chromium).Review rigor
Test plan
cargofreshell-server/sessions/ws green at tip (477 + 162 + 359 lib + integration)cargo fmt+clippycleanKnown follow-ups (non-blocking, documented)
session_directory.rsappliestitleOverridewithout the suppression rule (pre-existing divergence, now noted).pane_ledgerflock flake from Resume validation: disk-existence gate before resuming coding-CLI sessions #591 (reproduced at clean base).Generated with Amplifier