Merge commit 'refs/codex-unblock/20260723/pr570-master-initial' into worktree/pr570-merge-master-20260723

# Conflicts:
#	.agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md
#	docs/core-data-structures/persistence.md
#	packages/session-persistence/session-persistence/README.md
This commit is contained in:
Tianyi Cui
2026-07-23 23:21:08 +08:00
366 changed files with 11617 additions and 1027 deletions

View File

@@ -96,11 +96,25 @@ export function runPersistenceContract(name: string, make: () => Promise<Contrac
{ type: 'turn/start', seq: 6, time: 7, data: { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } } },
{ type: 'step/start', seq: 7, time: 8, data: { turn: 2, step: 1 } },
])
const beforeRepair = (await persistence.listSnapshots())
.find(snapshot => snapshot.header.id === m.id)?.revision
const inspected = await persistence.inspect(m.id)
const afterInspect = (await persistence.listSnapshots())
.find(snapshot => snapshot.header.id === m.id)?.revision
expect(afterInspect).toBe(beforeRepair)
expect(inspected.events.map(e => e.type)).toEqual([
'turn/start', 'user/message', 'step/start', 'assistant/message', 'step/end', 'turn/end',
'turn/start', 'step/start',
])
// load PRESERVES the interrupted turn's events (a turn can be huge — they
// must not be truncated) and closes the orphaned turn with synthetic
// boundary events: step/end (the step was open) then turn/end {interrupted}.
const loaded = await persistence.load(m.id)
const afterRepair = (await persistence.listSnapshots())
.find(snapshot => snapshot.header.id === m.id)?.revision
expect(afterRepair).not.toBe(beforeRepair)
expect(loaded.events.map(e => e.type)).toEqual([
'turn/start', 'user/message', 'step/start', 'assistant/message', 'step/end', 'turn/end', // turn 1
'turn/start', 'step/start', 'step/end', 'turn/end', // turn 2: real events + synthetic closers
@@ -201,18 +215,33 @@ export function runPersistenceContract(name: string, make: () => Promise<Contrac
try {
await persistence.create(meta('empty'))
expect((await persistence.list()).map(m => m.id)).not.toContain(SessionId('empty'))
expect((await persistence.listSnapshots()).map(snapshot => snapshot.header.id))
.not.toContain(SessionId('empty'))
} finally {
await dispose()
}
})
it('list() includes a session once it has events', async () => {
it('lists stable lightweight revisions that change after an append', async () => {
const { persistence, dispose } = await make()
try {
const m = meta('s2')
await persistence.create(m)
await persistence.append(m.id, oneTurnLog())
expect((await persistence.list()).map(x => x.id)).toContain(m.id)
const first = (await persistence.listSnapshots()).find(snapshot => snapshot.header.id === m.id)
const repeated = (await persistence.listSnapshots()).find(snapshot => snapshot.header.id === m.id)
expect(first).toBeDefined()
expect(repeated?.revision).toBe(first?.revision)
await persistence.append(m.id, [{
type: 'turn/start',
seq: 6,
time: 7,
data: { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } },
}])
const changed = (await persistence.listSnapshots()).find(snapshot => snapshot.header.id === m.id)
expect(changed?.revision).not.toBe(first?.revision)
} finally {
await dispose()
}

View File

@@ -740,11 +740,12 @@ export function runCoordinatorContract(name: string, makeFixture: () => Promise<
}
})
it('load rejects a missing session', async () => {
it('load and inspect reject a missing session', async () => {
const fix = await makeFixture()
const { ctx, fiber } = await freshCtx(fix)
try {
await expect(ctx.sessionPersistence.load(SessionId('nope'))).rejects.toThrow(/not found/)
await expect(ctx.sessionPersistence.inspect(SessionId('nope'))).rejects.toThrow(/not found/)
} finally {
await fiber.dispose()
await fix.cleanup()

View File

@@ -3,8 +3,8 @@ import { Context } from 'cordis'
import SessionStore, { SessionId, isJsonValue } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import {
SessionPersistence, PersistenceCoordinator,
type PersistenceBackend, type StoredPrefix,
SessionPersistence, SessionPersistenceRevision, PersistenceCoordinator,
type PersistenceBackend, type SessionPersistenceSnapshot, type StoredPrefix,
} from '../src/index.ts'
import { runPersistenceContract, meta, oneTurnLog } from './contract.ts'
import { runCoordinatorContract, type CoordinatorFixture } from './coordinator-contract.ts'
@@ -94,6 +94,10 @@ class MemoryPersistence extends SessionPersistence implements PersistenceBackend
return this.coordinator.load(id)
}
inspect(id: SessionId): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
return this.coordinator.inspect(id)
}
// --- PersistenceBackend hooks (the Map storage primitives) ---
// A Map-backed store has no torn tails, so `tornMarker` is never set.
@@ -131,6 +135,13 @@ class MemoryPersistence extends SessionPersistence implements PersistenceBackend
async list(): Promise<SessionHeader[]> {
return [...this.store.values()].map(e => structuredClone(e.meta))
}
async listSnapshots(): Promise<SessionPersistenceSnapshot[]> {
return [...this.store.values()].map(entry => ({
header: structuredClone(entry.meta),
revision: SessionPersistenceRevision(`events:${entry.events.length}`),
}))
}
}
/** Controllable storage primitive for serialization and retirement failure tests. */