fix: stop cross-project live session leakage and stale-adapter 401s

Field session on a nested SvelteKit app surfaced a self-reinforcing leak:
localStorage is per-origin, two projects reused 127.0.0.1:5174, and a
React project's leftover cycling session was resumed inside the Svelte
project. Its checkpoints then materialized a ghost session in the new
project's durable store that kept reattaching after every discard, and a
stale adapter module 401'd on live.js, hiding the picker.

Four fixes:

- Server: only session-creating events (generate, steer) may mint a
  journal. Progress events (checkpoints, mount acks, accept/discard) for
  unknown ids are refused with 404 unknown_session and never enqueued, so
  foreign browser state cannot create ghost sessions. Browser sends are
  gated so progress never overtakes its own creating POST (the Go-time
  checkpoint and generate are concurrent fetches; the first sweep caught
  the out-of-order arrival breaking every SvelteKit flow). Steer
  checkpoints now follow the steer event for the same reason.
- Browser: saved sessions carry the server's appRoot; a session stamped
  by another project is dropped at load time. Unstamped legacy state is
  caught by the unknown_session refusal, which clears local state and
  re-arms the picker with an explanatory toast.
- SvelteKit adapter: the layout import carries a token-derived revision
  query so a helper restart changes the module specifier and no Vite
  client/SSR cache can serve an adapter with a rotated-out token;
  live-inject --port reads the running helper's token from server.json
  instead of writing an unauthenticated live.js URL; script load failures
  log an actionable console error; and adapter removal is byte-exact
  (the old regex swallowed the next line's indentation).
- live.mjs resolves surface briefs from appRoot, then contextRoot, then
  repoRoot, matching context.mjs in nested-app repos.

Tests: server unknown-session rejection units, adapter revision/
byte-exact-removal units, and a foreign-session e2e scenario that seeds
another project's localStorage state and asserts it is cleared, no ghost
journal materializes, and picking still works.

AI-assisted (Claude Code).

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-28 17:38:46 -07:00
co-authored by Claude Code
parent 69456364b2
commit a83d767cf9
11 changed files with 383 additions and 22 deletions
+10 -2
View File
@@ -169,12 +169,20 @@ The agent should then:
let surfaceBrief = null;
let surfaceBriefPath = null;
try {
const resolvedBrief = resolveSurfaceBrief(roots.appRoot, liveTarget.absoluteTargetPath || null);
if (resolvedBrief?.brief) {
// Briefs live under .impeccable/surfaces, which in a nested-app repo sits
// at the CONTEXT or repo root, not the app root; context.mjs already finds
// them there, and live must not report "no brief" for the same project.
const briefRoots = [roots.appRoot, roots.contextRoot, roots.repoRoot]
.filter(Boolean)
.filter((dir, i, arr) => arr.findIndex((other) => path.resolve(other) === path.resolve(dir)) === i);
for (const briefRoot of briefRoots) {
const resolvedBrief = resolveSurfaceBrief(briefRoot, liveTarget.absoluteTargetPath || null);
if (!resolvedBrief?.brief) continue;
surfaceBrief = resolvedBrief.brief.text ?? safeRead(resolvedBrief.brief.path);
surfaceBriefPath = resolvedBrief.brief.path
? path.relative(liveTarget.originalCwd, resolvedBrief.brief.path)
: null;
break;
}
} catch { /* briefs are optional context */ }
console.log(JSON.stringify({