Skip to content

feat(rust-server): session resume-by-id parity with the hardened resolve contract (SYNC-06) - #592

Merged
danshapiro merged 52 commits into
mainfrom
feat/rust-resolve-parity
Jul 31, 2026
Merged

feat(rust-server): session resume-by-id parity with the hardened resolve contract (SYNC-06)#592
danshapiro merged 52 commits into
mainfrom
feat/rust-resolve-parity

Conversation

@danshapiro

Copy link
Copy Markdown
Owner

Summary

  • Closes the Rust half of the resume-by-id feature: POST /api/sessions/resolve on 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 the sessionResolve feature flag so the shared client shows the pinned sidebar Resume button on Rust builds.
  • Adopted a prior parity foundation (built pre-hardening), rebased onto the hardened contract, and closed the delta through a full plan / load-bearing / review pipeline:
    • Hardened wire contract: status:'degraded', providerErrors, unsearchedProviders, homeDir.
    • Node-mirror match ranking: per-token exact-index → exact-fallback → prefix; ses_ case-sensitive; subagent rules; saved-override projection incl. titleOverride + legacy deleted keys; provider-generated-title suppression.
    • Budgeted shape-gated exact-id fallbacks (claude transcript locator multi-root/subagents; opencode by-id direct row query) with fail-fast admission control + fallback-scoped 15s deadline.
    • Scan-failure/refresh health channels; express.json() Content-Type gating parity; HOMEUSERPROFILE fallback.
    • Cross-language anti-drift fixture table (test/fixtures/resume-input/parser-cases.json, 34 cases) consumed by BOTH the TS and Rust parser test suites.
    • Resume-button e2e promoted to MATRIX_SPECS (green on both legacy-chromium and rust-chromium).

Review rigor

  • Load-bearing validation: 24 verified / 1 falsified-fixed.
  • 3 plan review rounds (32 findings); per-task spec + quality reviews.
  • Whole-branch review: "Ready to merge: Yes".
  • 5 cross-model delta review rounds (24 majors, all fixed) + scoped re-review of the final fixes (verdict: LAND).

Test plan

  • cargo freshell-server/sessions/ws green at tip (477 + 162 + 359 lib + integration)
  • cargo fmt + clippy clean
  • TS shared suites 262/262
  • e2e resume-button 6/6 on both projects (legacy-chromium, rust-chromium)

Known follow-ups (non-blocking, documented)

Generated with Amplifier

danshapiro and others added 30 commits July 31, 2026 04:22
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.
…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.
…586 legacy contract (SYNC-06)

Adds the resolve route against the retired pre-#586 Node contract,
not the hardened in-tree implementation. The hardened #586 wire
contract (degraded status, providerErrors/unsearchedProviders/homeDir)
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>
danshapiro and others added 22 commits July 31, 2026 04:22
…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>
@danshapiro
danshapiro merged commit 5048e9f into main Jul 31, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant