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
+76
View File
@@ -1124,6 +1124,82 @@ for (const { name, fixture } of fixtures) {
});
}
if (shouldRunScenario('foreign') && fixture.runtime.foreignSessionScenario) {
it('clears another project\'s leftover browser session instead of resuming it', liveE2eTestOptions, async (t) => {
if (manualOnly || process.env.IMPECCABLE_E2E_MANUAL_SCENARIO) {
t.skip('manual scenario filter is active');
return;
}
// Repro of the cross-project leak: localStorage is per-ORIGIN, and two
// projects routinely reuse the same localhost port across time. A
// leftover cycling session from the other project used to be resumed
// ("Variants ready" with no user action), and its checkpoints
// materialized a ghost session in THIS project's durable store that
// kept reattaching after every discard.
const agent = createFakeAgent();
const session = await bootFixtureSession({
name,
fixture,
browser,
agent,
wrapTarget: wrapTargetFromPickedElement,
log: (m) => t.diagnostic(m),
});
const { page, appRoot, teardown } = session;
const pickSelector = fixture.runtime.pickSelector || 'h1.hero-title';
try {
await waitForHandshake(page);
const cases = [
// Stamped with another app's root: dropped at load time, before
// any server roundtrip.
{ id: 'f0e1d2c3', appRoot: '/somewhere/else/entirely' },
// Legacy shape without a stamp: dropped when the server refuses
// its first checkpoint as unknown_session.
{ id: 'deadf00d' },
];
for (const foreign of cases) {
t.diagnostic(`Seeding foreign session ${foreign.id}${foreign.appRoot ? ' (stamped)' : ' (legacy shape)'}`);
await page.evaluate((saved) => {
localStorage.setItem('impeccable-live-session', JSON.stringify(saved));
localStorage.removeItem('impeccable-live-session-handled');
}, {
id: foreign.id,
state: 'CYCLING',
expected: 3,
arrived: 3,
visible: 1,
sourceFile: 'src/App.jsx',
pageUrl: '/',
checkpointRevision: 5,
...(foreign.appRoot ? { appRoot: foreign.appRoot } : {}),
});
await page.reload({ waitUntil: 'domcontentloaded' });
await waitForHandshake(page);
const deadline = Date.now() + 20_000;
for (;;) {
const current = await readLiveSessionStorage(page);
if (!current || current.id !== foreign.id) break;
if (Date.now() > deadline) throw new Error(`foreign session ${foreign.id} was never cleared`);
await new Promise((resolve) => setTimeout(resolve, 250));
}
const sessionsDir = join(appRoot, '.impeccable/live/sessions');
assert.equal(
existsSync(join(sessionsDir, `${foreign.id}.jsonl`)),
false,
`no ghost journal for ${foreign.id} may materialize in this project's store`,
);
}
// The surface is genuinely back: a fresh pick must work.
await pickElement(page, pickSelector, { position: fixture.runtime.pickPosition });
} finally {
await teardownAndResetBrowser(teardown);
}
});
}
if (shouldRunScenario('manual') && Array.isArray(fixture.runtime.manualEditScenarios) && fixture.runtime.manualEditScenarios.length > 0) {
const manualScenarioFilter = process.env.IMPECCABLE_E2E_MANUAL_SCENARIO || '';
for (const scenario of fixture.runtime.manualEditScenarios) {