Skip to content

feat: server-build mismatch auto-reload (ready.buildId) - #697

Merged
danshapiro merged 15 commits into
mainfrom
the-usual/server-version-reload
Aug 28, 2026
Merged

feat: server-build mismatch auto-reload (ready.buildId)#697
danshapiro merged 15 commits into
mainfrom
the-usual/server-version-reload

Conversation

@danshapiro

Copy link
Copy Markdown
Owner

Summary

Self-heals the stale-client class of failures (the 2026-08-26 "Fresh-agent snapshot response did not match the shared contract" incident): when a browser tab's client bundle was built from a different commit than the server it connects to, the client now detects the mismatch from the WS ready frame and reloads itself exactly once.

How it works

  • Artifact-time identity on all three producers — each stamp is the full git rev-parse HEAD (literal "unknown" fallback), baked when its artifact is produced:
    • Rust: new crates/freshell-ws/build.rs bakes FRESHELL_WS_BUILD_COMMIT; the handshake ready stamps build_id (serde-omitted when None → frozen transcripts stay byte-valid).
    • Node: build:server writes dist/server/build-id.json (scripts/bake-server-build-id.mjs); server/build-id.ts prefers the bake (a stale dist advertises its own build, never the checkout) and fails inert to "unknown" without a valid stamp; tsx-from-source dev mode probes runtime HEAD.
    • Client: Vite define bakes __FRESHELL_BUILD_ID__ from the repo HEAD.
  • Additive/optional wire contract (bootId doctrine): ReadyMessage.buildId?: string, regenerated ws-server-messages.schema.json (still additionalProperties: false), old clients/servers unaffected.
  • Client guard (src/lib/server-build-check.ts): reload iff both ids present, non-empty, neither "unknown", and different. The sessionStorage sentinel (freshell.server-build-reload) records the attempted server build id — the same server id never reloads twice, a different (corrected) deployment re-arms the guard, a matching ready clears it. Any sessionStorage failure = no reload (fail-safe, logged).
  • Known limits (documented in code + AGENTS.md): per-server-identity once-guard; a newer client vs older server costs one bounded futile reload; a mixed-build origin behind one address could oscillate (not hardened for the single-server self-hosted threat model).

Test evidence

  • Unit: compare-rule matrix incl. A→B→B→C re-arm sequence, throwing-storage + denying-getter fail-safes, at-call-time set-before-reload ordering (18+17 tests).
  • Server: build-id bake precedence/source-vs-compiled split; handshake buildId snapshot stability (both servers).
  • Port: frozen handshake transcript stays byte-valid; contract-freeze + mutation suites green; oracle freshness guard (nodeBuildStampIsCurrent) unit-tested 5/5.
  • E2E (local rust-chromium): exactly-one-reload → real match path clears the sentinel → convergence; persistence across a real navigation; repeat-mismatch suppression. Mandatory three-run RED-verification recorded (green → commented-out red → green).
  • Gate: npm run check + npm run test:oracle + npm run test:integration green at HEAD, excluding 5 pre-existing failures that reproduce at base 6baeb3e2a (update-flow ×3, visible-first-audit-gate ×2 — proxy-env stderr pollution; recorded in the run ledger).

Review provenance

"the-usual" workflow: load-bearing validation (2 falsified assumptions → plan changes), plan review 3 rounds (cap; all blockers fixed), delta review 4 rounds ending PASSED (round 3 fixed the loop-guard's per-server-identity gap). Full artifacts: .worktrees/.the-usual-logs/server-version-reload/.

Caveats

  • The new e2e spec is in CLOUD_SKIP_SPECS with justification: the Cloud Run image builds without git metadata, so both stamps are "unknown" there and the compare is inert by design. Coverage lane is local rust-chromium (global-setup fresh-builds both artifacts at HEAD).
  • Committing from this repo surfaced a broken user.name in the main checkout's .git/config (literal user.email); commits here carry an explicit correct author.

@danshapiro
danshapiro merged commit 6c541be into main Aug 28, 2026
3 checks passed
@danshapiro
danshapiro deleted the the-usual/server-version-reload branch August 28, 2026 00:15
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