fix(session-persistence-jsonl): make loadLive cwd-scope-exact (Codex review)

Codex's converge pass on PR B found a cross-cwd adoption hole: the coordinator
calls loadLive(id, session.header.cwd) for HMR live-adoption, but JSONL's
loadLive delegated to findLog(id, cwd) which, for cwd === undefined, scanned
ALL cwd buckets. So a live NO-CWD session could adopt a same-id log from a real
cwd bucket, ending with a live cwd: undefined but a persisted meta.cwd: '/w'.

loadLive must treat `undefined` as the DEFINITE no-cwd bucket, not "unknown":
it now goes straight to logPath(cwd, id) (which maps undefined -> _no-cwd),
never the all-buckets scan. loadStored/deleteStored keep the any-cwd scan
(resume/removal identify by id alone), so findLog is now a pure scan-all and
loses its dead cwd-direct branch.

The coordinator's has() relied on loadLive(id, undefined) meaning "any scope"
for an untracked id — fixed to use loadStored for the untracked (unknown-cwd)
case and loadLive only for a tracked session's known cwd.

Adds a regression test: a no-cwd live session reusing an id persisted in a real
cwd bucket no longer cross-cwd-adopts — it falls through to createCore's
any-cwd collision probe and REJECTS, leaving the original log untouched. Also
fixes the README to say `tornMarker !== undefined` (a marker may be falsy, 0).
This commit is contained in:
Tianyi Cui
2026-06-20 04:13:37 +08:00
parent ab02e9acec
commit 3d67a98291
4 changed files with 73 additions and 28 deletions

View File

@@ -484,6 +484,41 @@ describe('SessionPersistenceJsonl: edge cases', () => {
await expect(backend.inits.get(b)).rejects.toThrow(/already bound to a different live session|already has a persisted log on disk/)
})
it('a NO-CWD live session does NOT cross-cwd-adopt a same-id log from a real cwd bucket (loadLive is scope-exact)', async () => {
// Backend 1: materialize a log under id "x" in the cwd "/w" bucket, then
// dispose the WHOLE backend (so backend 2 mounts with an EMPTY states map —
// the HMR/reload path where onCreated goes through loadLive, not a tracked
// collision).
await ctx.sessionPersistence.create(meta('x', '/w'))
await ctx.sessionPersistence.append(SessionId('x'), oneTurnLog())
await ctx.fiber.dispose()
// Backend 2 over the SAME root. A live no-cwd session reuses id "x". Because
// loadLive(id, undefined) is the DEFINITE no-cwd bucket (NOT an all-buckets
// scan), case-2 adoption does NOT match the "/w" log — so it would NOT
// silently graft the no-cwd events onto the "/w" log with a mismatched cwd
// (the bug a non-scope-exact loadLive caused). It falls through to the
// new-session path, where createCore's any-cwd collision probe (loadStored)
// catches the duplicate id and REJECTS — the id is taken in another bucket.
const ctx2 = new Context()
await ctx2.plugin(SessionStore)
await ctx2.plugin(SessionPersistenceJsonl, { root })
const backend = ctx2.sessionPersistence as unknown as { inits: Map<Session, Promise<void>> }
let b!: Session
await ctx2.plugin(Object.assign((inner: Context) => {
b = inner.sessions.create('x') // no cwd
}, { inject: ['sessions'] }))
await expect(backend.inits.get(b)).rejects.toThrow(/already has a persisted log on disk/)
// The "/w" log is untouched — no no-cwd events were grafted onto it, and no
// `_no-cwd` log for "x" was created.
const inW = scanLog(await readFile(logPath(root, '/w', SessionId('x'))))
expect(inW.meta.cwd).toBe('/w')
expect(inW.events).toHaveLength(6)
await expect(stat(logPath(root, undefined, SessionId('x')))).rejects.toThrow()
await ctx2.fiber.dispose()
})
it('a seed with matching seq/type/time but DIFFERENT data is rejected (deep prefix compare)', async () => {
// Materialize and load (ownerless, cursor = 6).
await ctx.sessionPersistence.create(meta('divergent', '/a'))
@@ -549,7 +584,7 @@ describe('SessionPersistenceJsonl: edge cases', () => {
// open() must surface, not be collapsed to "not found" (which would let a
// collision check proceed under a false absence assumption). A LAZY session
// (created, never appended) keeps its cwd in state, so has() reaches
// findLog(id, cwd) → exists(logPath). Make that cwd's bucket DIRECTORY a
// loadLive(id, cwd) → exists(logPath). Make that cwd's bucket DIRECTORY a
// regular file: open()ing `bucket/<id>.jsonl` under it then fails ENOTDIR.
const cwd = '/x'
const ctx2 = new Context()