mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 16:46:31 +03:00
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:
co-authored by
Claude Code
parent
69456364b2
commit
a83d767cf9
@@ -91,6 +91,12 @@ function resolveProjectContext() {
|
||||
}
|
||||
const DEFAULT_POLL_TIMEOUT = 600_000; // 10 min — agent re-polls on timeout anyway
|
||||
const SSE_HEARTBEAT_INTERVAL = 30_000; // keepalive ping every 30s
|
||||
|
||||
// The browser events allowed to mint a NEW session journal. `generate` starts
|
||||
// a variant session at Go; `steer` mints its own request id. Every other
|
||||
// id-carrying event must land on an existing session (see the unknown_session
|
||||
// gate in the /events handler).
|
||||
const SESSION_CREATING_EVENT_TYPES = new Set(['generate', 'steer']);
|
||||
// The browser checkpoints for several unrelated reasons (see checkpointPayload
|
||||
// in live-browser.js). Only these two report that variant availability changed,
|
||||
// and only they may drive variant_progress / the *_reviewable phases.
|
||||
@@ -730,6 +736,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
||||
port: state.port,
|
||||
vocabulary: LIVE_COMMANDS,
|
||||
commandPrefix: IMPECCABLE_COMMAND_PREFIX,
|
||||
appRoot: process.cwd(),
|
||||
parts,
|
||||
});
|
||||
res.writeHead(200, {
|
||||
@@ -1020,6 +1027,20 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
return;
|
||||
}
|
||||
// Only the events that START a session may create its journal.
|
||||
// Everything else (checkpoints, mount acks, accept/discard) must
|
||||
// reference a session THIS store already knows: appendEvent creates a
|
||||
// journal for any id it is handed, so without this gate a browser
|
||||
// resuming another project's session from per-origin storage (two
|
||||
// apps sharing a localhost port) materializes a ghost session here
|
||||
// that keeps reattaching after every discard.
|
||||
if (msg.id && state.sessionStore
|
||||
&& !SESSION_CREATING_EVENT_TYPES.has(msg.type)
|
||||
&& !state.sessionStore.has(msg.id)) {
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'unknown_session', id: msg.id }));
|
||||
return;
|
||||
}
|
||||
const missedCompletion = detectMissedGenerationCompletion(msg);
|
||||
if (state.sessionStore && msg.id) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user