Merge remote-tracking branch 'origin/master' into xtr/agent-loop-message-machine

# Conflicts:
#	docs/cordis-catalog/services.md
This commit is contained in:
_Kerman
2026-07-24 21:41:02 +08:00
41 changed files with 922 additions and 144 deletions

View File

@@ -84,6 +84,9 @@ function isHeaderLine(value: unknown): value is HeaderLine {
&& typeof (value as { version?: unknown }).version === 'number'
&& typeof (value as { id?: unknown }).id === 'string'
&& typeof (value as { createdAt?: unknown }).createdAt === 'number'
&& Number.isSafeInteger((value as { createdAt: number }).createdAt)
&& (value as { createdAt: number }).createdAt >= 0
&& !Object.is((value as { createdAt: number }).createdAt, -0)
&& typeof (value as { delegationDepth?: unknown }).delegationDepth === 'number'
&& Number.isSafeInteger((value as { delegationDepth: number }).delegationDepth)
&& (value as { delegationDepth: number }).delegationDepth >= 0

View File

@@ -559,6 +559,26 @@ describe('SessionPersistenceJsonl: scanLog unit', () => {
expect(() => scanLog(Buffer.from('{"type":"event"}\n'))).toThrow(/session header/)
})
it.each([
['fractional', 1.5],
['negative', -1],
['unsafe', Number.MAX_SAFE_INTEGER + 1],
])('rejects a session header with a %s createdAt', (_label, createdAt) => {
const log = JSON.stringify({
type: 'session',
version: 0,
id: 'invalid-created-at',
createdAt,
delegationDepth: 0,
}) + '\n'
expect(() => scanLog(Buffer.from(log))).toThrow(/session header/)
})
it('rejects a session header with negative-zero createdAt', () => {
const log = '{"type":"session","version":0,"id":"invalid-created-at","createdAt":-0,"delegationDepth":0}\n'
expect(() => scanLog(Buffer.from(log))).toThrow(/session header/)
})
it.each([
['missing', undefined],
['a string', '1'],

View File

@@ -8,9 +8,9 @@ A SQLite durable session-persistence backend — a second `SessionPersistence` i
## Storage model
Each `SessionEvent` maps 1:1 onto a row in an `events` table `(session_id, seq, type, time, data, source_event_seqs, surface_op)``data` is the event payload as JSON text, so the row shape is the event verbatim (including `assistant/chunk`, keeping `seq` contiguous). The two `TEXT` columns `source_event_seqs` and `surface_op` are nullable; they store the event's optional surface-metadata fields (see [session surface](../../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md)). Out-of-log metadata (`SessionHeader`), a per-materialization incarnation id, and a monotonic per-log revision live in a `sessions` row; a singleton state row carries the immutable store id. A `sessions` row is written only by the first `append` — its existence is the lazy-materialization signal (`list` reports exactly the sessions that have a row).
Each `SessionEvent` maps 1:1 onto a row in an `events` table `(session_id, seq, type, time, data, source_event_seqs, surface_op)``data` is the event payload as JSON text, so the row shape is the event verbatim (including `assistant/chunk`, keeping `seq` contiguous). The two `TEXT` columns `source_event_seqs` and `surface_op` are nullable; they store the event's optional surface-metadata fields (see [session surface](../../../.agents/notes/implemented/architecture/2026-06-18-session-surface.md)). Out-of-log metadata (`SessionHeader`), a per-materialization incarnation id, and a monotonic per-log revision live in a `sessions` row; `createdAt` is a non-negative safe integer stored in a strict `INTEGER` column. A singleton state row carries the immutable store id. A `sessions` row is written only by the first `append` — its existence is the lazy-materialization signal (`list` reports exactly the sessions that have a row).
The repository's Node range supports unflagged `node:sqlite`. The database enables foreign keys and uses the configured journal mode (`wal` by default; use a rollback mode where WAL shared-memory files are unsuitable). `PRAGMA user_version` stores the table-layout version; databases with any other version are rejected because this unreleased format has no migrations.
The repository's Node range supports unflagged `node:sqlite`. The database enables foreign keys and uses the configured journal mode (`wal` by default; use a rollback mode where WAL shared-memory files are unsuitable). `PRAGMA application_id` identifies the canonical persistence database, and `PRAGMA user_version` stores its layout version. A fresh database must have no application identity or user-defined schema objects; initialization creates every table and stamps both pragmas in one transaction. Non-pristine unversioned databases, foreign application identities, and every non-current version reject before journal-mode mutation because this unreleased format has no migrations.
On filesystems with POSIX modes, the backend requests mode `0700` for missing directories and exclusively creates a missing database with mode `0600` before SQLite opens it; the process umask may further restrict both. New WAL, shared-memory, and persistent rollback-journal sidecars receive the database's resulting owner-only mode. Existing directories, database files, and sidecars keep their modes; filesystem setup errors other than an existing database fail initialization. These defaults prevent incidental exposure through a permissive process umask, but do not protect database confidentiality or integrity when another principal can replace the database entry in its parent directory.
@@ -55,5 +55,5 @@ SQLite storage does not mutate live request prefixes. A resumed loop can reuse p
- **`DatabaseSync` is synchronous** — every append transaction blocks the event loop for its duration; acceptable for local stores, a throughput ceiling for busy multi-session servers.
- **Write contention has no wait or retry policy** — the backend sets no busy timeout and retries no locked-database error, so another connection holding a write transaction makes the operation reject immediately.
- **Only the current `SCHEMA_VERSION` opens** — a database with any other schema version is rejected rather than migrated (unreleased software; no persisted user data to preserve).
- **Only a pristine new database or the current owned `SCHEMA_VERSION` opens** — unversioned schema objects, foreign application identities, and every other schema version are rejected rather than migrated (unreleased software; no persisted user data to preserve).
- **Nothing deletes stored sessions** — rows accumulate until removed externally (the seam has no deletion surface; `ON DELETE CASCADE` is wired for such out-of-band cleanup).

View File

@@ -17,7 +17,10 @@ import type { SessionEvent, SessionId, SessionHeader, SurfaceOp } from '@deepsee
* layout; orthogonal to a session's own `version` (which versions the EVENT
* vocabulary, stored per session in the `sessions` row).
*/
export const SCHEMA_VERSION = 8
export const SCHEMA_VERSION = 10
/** SQLite application id protecting unrelated databases from persistence writes. */
export const SESSION_PERSISTENCE_SQLITE_APPLICATION_ID = 0x44534850
/**
* A row of the `sessions` table — the out-of-log metadata ({@link SessionHeader}).
@@ -63,9 +66,10 @@ export interface EventRow {
export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
/**
* Open the database and apply its schema and pragmas. A zero `user_version` is
* stamped with {@link SCHEMA_VERSION}; every other non-current version rejects
* rather than being migrated in place.
* Open the database and apply its schema and pragmas. An empty database with a
* zero `user_version` is initialized at {@link SCHEMA_VERSION}; a nonempty
* unversioned database and every other non-current version reject rather than
* being migrated in place.
* @param path - the SQLite database file to open (created when absent).
* @param journalMode - validated journal pragma.
* @returns the open handle with pragmas applied and all three tables ensured.
@@ -83,51 +87,81 @@ export function openDatabase(path: string, journalMode: JournalMode): DatabaseSy
function configureDatabase(db: DatabaseSync, path: string, journalMode: JournalMode): void {
db.exec('PRAGMA foreign_keys = ON')
let began = false
try {
db.exec('BEGIN IMMEDIATE')
began = true
// Validate while holding the write lock so no other connection can change
// schema ownership between inspection and initialization.
const { user_version: onDisk } = db.prepare('PRAGMA user_version').get() as { user_version: number }
const { application_id: applicationId } = db.prepare('PRAGMA application_id').get() as { application_id: number }
const { count: userObjectCount } = db.prepare(
"SELECT COUNT(*) AS count FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*'",
).get() as { count: number }
if (onDisk === 0 && (applicationId !== 0 || userObjectCount > 0)) {
throw new Error(`session database at "${path}" has an unversioned schema or application identity`)
}
if (onDisk !== 0 && onDisk !== SCHEMA_VERSION) {
throw new Error(`session database at "${path}" has schema version ${onDisk}, incompatible with this build (${SCHEMA_VERSION})`)
}
if (onDisk === SCHEMA_VERSION && applicationId !== SESSION_PERSISTENCE_SQLITE_APPLICATION_ID) {
throw new Error(
`session database at "${path}" has application id ${applicationId}, expected ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`,
)
}
db.exec(`
CREATE TABLE IF NOT EXISTS persistence_state (
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
store_id TEXT NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
version INTEGER NOT NULL,
created_at INTEGER NOT NULL,
cwd TEXT,
parent_session TEXT,
seed_length INTEGER,
delegation_depth INTEGER,
incarnation TEXT NOT NULL,
revision INTEGER NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS events (
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
seq INTEGER NOT NULL,
type TEXT NOT NULL,
time INTEGER NOT NULL,
data TEXT NOT NULL,
source_event_seqs TEXT,
surface_op TEXT,
PRIMARY KEY (session_id, seq)
) STRICT
`)
db.prepare(
'INSERT OR IGNORE INTO persistence_state (singleton, store_id) VALUES (1, ?)',
).run(randomUUID())
if (onDisk === 0) {
db.exec(`PRAGMA application_id = ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`)
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`)
}
db.exec('COMMIT')
began = false
} catch (error: unknown) {
/* v8 ignore next -- a BEGIN failure leaves no transaction to roll back. */
if (began) {
/* v8 ignore next 5 -- preserve the original schema failure if SQLite also refuses rollback. */
try {
db.exec('ROLLBACK')
} catch {
// The original SQLite failure remains the actionable cause.
}
}
throw error
}
// The validated union is safe to interpolate into a non-bindable PRAGMA.
// Apply it only after ownership validation and initialization commit.
db.exec(`PRAGMA journal_mode = ${journalMode.toUpperCase()}`)
// `PRAGMA user_version` always returns exactly one row { user_version }.
const { user_version: onDisk } = db.prepare('PRAGMA user_version').get() as { user_version: number }
if (onDisk !== 0 && onDisk !== SCHEMA_VERSION) {
throw new Error(`session database at "${path}" has schema version ${onDisk}, incompatible with this build (${SCHEMA_VERSION})`)
}
if (onDisk === 0) {
// Stamp fresh or pre-versioning databases.
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`)
}
db.exec(`
CREATE TABLE IF NOT EXISTS persistence_state (
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
store_id TEXT NOT NULL
) STRICT
`)
db.prepare(
'INSERT OR IGNORE INTO persistence_state (singleton, store_id) VALUES (1, ?)',
).run(randomUUID())
db.exec(`
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
version INTEGER NOT NULL,
created_at INTEGER NOT NULL,
cwd TEXT,
parent_session TEXT,
seed_length INTEGER,
delegation_depth INTEGER,
incarnation TEXT NOT NULL,
revision INTEGER NOT NULL
) STRICT
`)
db.exec(`
CREATE TABLE IF NOT EXISTS events (
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
seq INTEGER NOT NULL,
type TEXT NOT NULL,
time INTEGER NOT NULL,
data TEXT NOT NULL,
source_event_seqs TEXT,
surface_op TEXT,
PRIMARY KEY (session_id, seq)
) STRICT
`)
}
/**
@@ -136,6 +170,9 @@ function configureDatabase(db: DatabaseSync, path: string, journalMode: JournalM
* @returns the header, `NULL` columns mapped to omitted optional fields.
*/
export function rowToMeta(row: SessionRow): SessionHeader {
if (!Number.isSafeInteger(row.created_at) || row.created_at < 0) {
throw new Error('stored session createdAt must be a non-negative safe integer')
}
return {
version: row.version,
id: row.id as SessionId,

View File

@@ -4,10 +4,18 @@ import { existsSync } from 'node:fs'
import { chmod, mkdtemp, rm, stat, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { DatabaseSync } from 'node:sqlite'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SurfaceEvent, SurfaceEventType } from '@deepseek-ai/dsh-session'
import SessionPersistenceSqlite, { SCHEMA_VERSION } from '@deepseek-ai/dsh-session-persistence-sqlite'
import { openDatabase, rowToEvent, scanRows, type EventRow } from '../src/schema.ts'
import {
openDatabase,
rowToEvent,
rowToMeta,
scanRows,
SESSION_PERSISTENCE_SQLITE_APPLICATION_ID,
type EventRow,
} from '../src/schema.ts'
import { runPersistenceContract, meta, oneTurnLog, appendLog } from '../../session-persistence/tests/contract.ts'
import { runCoordinatorContract, type CoordinatorFixture } from '../../session-persistence/tests/coordinator-contract.ts'
@@ -150,6 +158,22 @@ describe('scanRows', () => {
})
})
describe('rowToMeta', () => {
it('rejects fractional stored creation metadata', () => {
expect(() => rowToMeta({
id: 'fractional',
version: 0,
created_at: 1.5,
cwd: null,
parent_session: null,
seed_length: null,
incarnation: 'fractional',
revision: 1,
delegation_depth: null,
})).toThrow('stored session createdAt must be a non-negative safe integer')
})
})
describe('SessionPersistenceSqlite: durability and crash semantics', () => {
it('rejects a stored v0 log containing a legacy request/header-delta event', async () => {
const path = await freshDbPath()
@@ -304,6 +328,121 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
expect(() => openDatabase(olderPath, 'wal')).toThrow(/incompatible with this build/)
})
it('rejects a table-backed unversioned database before stamping or changing journal mode', async () => {
const path = await freshDbPath()
const legacy = new DatabaseSync(path)
legacy.exec('CREATE TABLE sessions (id TEXT PRIMARY KEY)')
legacy.close()
expect(() => openDatabase(path, 'wal')).toThrow(/unversioned schema or application identity/)
const unchanged = new DatabaseSync(path)
expect(unchanged.prepare('PRAGMA user_version').get()).toEqual({ user_version: 0 })
expect(unchanged.prepare('PRAGMA journal_mode').get()).toEqual({ journal_mode: 'delete' })
expect(unchanged.prepare(
"SELECT name FROM sqlite_schema WHERE type = 'table' AND name = 'sessions'",
).get()).toEqual({ name: 'sessions' })
unchanged.close()
})
it('counts a sqliteX table as user-owned instead of mistaking it for SQLite metadata', async () => {
const path = await freshDbPath()
const unrelated = new DatabaseSync(path)
unrelated.exec('CREATE TABLE sqliteX (value TEXT)')
unrelated.exec("INSERT INTO sqliteX VALUES ('safe')")
unrelated.close()
expect(() => openDatabase(path, 'wal')).toThrow(/unversioned schema or application identity/)
const unchanged = new DatabaseSync(path)
expect(unchanged.prepare('SELECT value FROM sqliteX').get()).toEqual({ value: 'safe' })
expect(unchanged.prepare('PRAGMA application_id').get()).toEqual({ application_id: 0 })
expect(unchanged.prepare('PRAGMA user_version').get()).toEqual({ user_version: 0 })
expect(unchanged.prepare('PRAGMA journal_mode').get()).toEqual({ journal_mode: 'delete' })
unchanged.close()
})
it('rejects view-only and foreign-application unversioned databases without mutation', async () => {
const viewPath = await freshDbPath()
const viewOnly = new DatabaseSync(viewPath)
viewOnly.exec('CREATE VIEW foreign_view AS SELECT 1 AS value')
viewOnly.close()
expect(() => openDatabase(viewPath, 'wal')).toThrow(/unversioned schema or application identity/)
const unchangedView = new DatabaseSync(viewPath)
expect(unchangedView.prepare('PRAGMA journal_mode').get()).toEqual({ journal_mode: 'delete' })
expect(unchangedView.prepare(
"SELECT type FROM sqlite_schema WHERE name = 'foreign_view'",
).get()).toEqual({ type: 'view' })
unchangedView.close()
const applicationPath = await freshDbPath()
const foreignApplication = new DatabaseSync(applicationPath)
foreignApplication.exec('PRAGMA application_id = 12345')
foreignApplication.close()
expect(() => openDatabase(applicationPath, 'wal')).toThrow(/unversioned schema or application identity/)
const unchangedApplication = new DatabaseSync(applicationPath)
expect(unchangedApplication.prepare('PRAGMA application_id').get()).toEqual({ application_id: 12345 })
expect(unchangedApplication.prepare('PRAGMA user_version').get()).toEqual({ user_version: 0 })
expect(unchangedApplication.prepare('PRAGMA journal_mode').get()).toEqual({ journal_mode: 'delete' })
unchangedApplication.close()
})
it('rejects a current-version database with a foreign application identity', async () => {
const path = await freshDbPath()
const foreign = new DatabaseSync(path)
foreign.exec('PRAGMA application_id = 12345')
foreign.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`)
foreign.close()
expect(() => openDatabase(path, 'wal')).toThrow(/has application id 12345/)
const unchanged = new DatabaseSync(path)
expect(unchanged.prepare('PRAGMA application_id').get()).toEqual({ application_id: 12345 })
expect(unchanged.prepare('PRAGMA user_version').get()).toEqual({ user_version: SCHEMA_VERSION })
expect(unchanged.prepare('PRAGMA journal_mode').get()).toEqual({ journal_mode: 'delete' })
unchanged.close()
})
it('rolls back schema objects and identity stamps when initialization fails', async () => {
const path = await freshDbPath()
const conflicting = new DatabaseSync(path)
conflicting.exec(`PRAGMA application_id = ${SESSION_PERSISTENCE_SQLITE_APPLICATION_ID}`)
conflicting.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`)
conflicting.exec("CREATE VIEW persistence_state AS SELECT 1 AS singleton, 'foreign' AS store_id")
conflicting.close()
expect(() => openDatabase(path, 'wal')).toThrow()
const unchanged = new DatabaseSync(path)
expect(unchanged.prepare(
"SELECT type FROM sqlite_schema WHERE name = 'persistence_state'",
).get()).toEqual({ type: 'view' })
expect(unchanged.prepare(
"SELECT type FROM sqlite_schema WHERE name = 'sessions'",
).get()).toBeUndefined()
expect(unchanged.prepare(
"SELECT type FROM sqlite_schema WHERE name = 'events'",
).get()).toBeUndefined()
expect(unchanged.prepare('PRAGMA application_id').get())
.toEqual({ application_id: SESSION_PERSISTENCE_SQLITE_APPLICATION_ID })
expect(unchanged.prepare('PRAGMA user_version').get()).toEqual({ user_version: SCHEMA_VERSION })
expect(unchanged.prepare('PRAGMA journal_mode').get()).toEqual({ journal_mode: 'delete' })
unchanged.close()
})
it('stamps the persistence application identity with the schema version', async () => {
const path = await freshDbPath()
openDatabase(path, 'wal').close()
const db = new DatabaseSync(path)
expect(db.prepare('PRAGMA application_id').get())
.toEqual({ application_id: SESSION_PERSISTENCE_SQLITE_APPLICATION_ID })
expect(db.prepare('PRAGMA user_version').get()).toEqual({ user_version: SCHEMA_VERSION })
db.close()
})
it('rejects a sibling v3 database (the merge-collided version) rather than opening it against missing columns', async () => {
// Version 3 identified two incompatible sibling layouts, so it is always rejected.
const path = await freshDbPath()
@@ -442,7 +581,7 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
})
it('exposes the schema version constant', () => {
expect(SCHEMA_VERSION).toBe(8)
expect(SCHEMA_VERSION).toBe(10)
})
it('keeps the revision stable for an empty repair hook', async () => {

View File

@@ -180,6 +180,9 @@ export class PersistenceCoordinator<TornMarker = unknown> {
if (snapshot === undefined) {
return Promise.reject(new TypeError('session metadata must be losslessly JSON-serializable'))
}
if (!Number.isSafeInteger(snapshot.createdAt) || snapshot.createdAt < 0) {
return Promise.reject(new TypeError('session metadata createdAt must be a non-negative safe integer'))
}
return this.serialize(snapshot.id, () => this.createCore(snapshot))
}

View File

@@ -84,6 +84,22 @@ export function runPersistenceContract(name: string, make: () => Promise<Contrac
}
})
it('rejects a fractional creation timestamp without reserving its session id', async () => {
const { persistence, dispose } = await make()
try {
const m = { ...meta('fractional-created-at'), createdAt: 1.5 }
await expect(persistence.create(m))
.rejects.toThrow('session metadata createdAt must be a non-negative safe integer')
const valid = meta('fractional-created-at')
await persistence.create(valid)
await persistence.append(valid.id, oneTurnLog())
expect((await persistence.load(valid.id)).meta.createdAt).toBe(valid.createdAt)
} finally {
await dispose()
}
})
it('crash recovery: load preserves an interrupted (unclosed) turn and closes it with turn/end {interrupted}', async () => {
const { persistence, dispose } = await make()
try {