Merge remote-tracking branch 'origin/master' into session-query-search

# Conflicts:
#	.agents/notes/implemented/feature/2026-07-10-session-query-service.md
#	.agents/notes/implemented/feature/2026-07-10-sqlite-session-query-provider.md
#	.agents/notes/proposed/feature/2026-07-10-sqlite-session-query-provider.md
#	docs/architecture.md
#	docs/capability-seams.md
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.md
#	docs/core-data-structures/persistence.md
#	docs/core-data-structures/session-query.md
#	docs/module-graph.md
#	docs/rfc/INDEX.md
#	packages/README.md
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/hooks/hooks-claude/tests/coverage.spec.ts
#	packages/session-persistence/session-persistence-jsonl/src/index.ts
#	packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts
#	packages/session-persistence/session-persistence-sqlite/README.md
#	packages/session-persistence/session-persistence-sqlite/src/index.ts
#	packages/session-persistence/session-persistence-sqlite/src/schema.ts
#	packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts
#	packages/session-persistence/session-persistence/README.md
#	packages/session-persistence/session-persistence/package.json
#	packages/session-query/README.md
#	packages/session-query/session-query/README.md
#	packages/session-query/session-query/package.json
#	packages/session-query/session-query/src/config.ts
#	packages/session-query/session-query/src/index.ts
#	packages/session-query/session-query/src/types.ts
#	pnpm-lock.yaml
#	scripts/gen-doc-graphs.ts
#	scripts/type-equiv.manifest.json
#	tsconfig.host.json
#	tsconfig.json
This commit is contained in:
Hypatia May
2026-07-23 13:56:56 +08:00
2630 files changed
+198302 -30288

No files matched your search

@@ -1,7 +1,8 @@
/**
* SQLite durable session-persistence backend. It maps each session header and
* event to rows, and delegates write-path orchestration to
* {@link PersistenceCoordinator}.
* {@link PersistenceCoordinator}. It has no independent per-session artifact,
* so its locator returns `undefined`.
* @module @deepseek-ai/dsh-session-persistence-sqlite
*/
@@ -10,11 +11,12 @@ import z from 'schemastery'
import { randomUUID } from 'node:crypto'
import { statSync } from 'node:fs'
import { DatabaseSync } from 'node:sqlite'
import { mkdir } from 'node:fs/promises'
import { mkdir, open } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import {
SessionPersistence, SessionPersistenceRevision, PersistenceCoordinator,
type PersistenceBackend, type SessionPersistenceSnapshot, type StoredPrefix,
type PersistenceBackend, type SessionLocation, type SessionPersistenceSnapshot,
type StoredPrefix,
} from '@deepseek-ai/dsh-session-persistence'
import type { SessionEvent, SurfaceEventType, SessionId, SessionHeader } from '@deepseek-ai/dsh-session'
import {
@@ -36,12 +38,32 @@ function surfaceBindings(event: SessionEvent): [string | null, string | null] {
]
}
/**
* Exclusively create a missing database file with owner-only permissions.
* Existing files retain their modes, and errors other than `EEXIST` propagate.
* `DatabaseSync` reopens by path, so this does not protect confidentiality or
* integrity when another principal can replace the database entry in its parent
* directory.
*/
async function createDatabaseFile(path: string): Promise<void> {
try {
const handle = await open(path, 'wx', 0o600)
await handle.close()
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
}
}
/** Plugin configuration. */
export interface Config {
/**
* Filesystem path to the SQLite database file. The special value `:memory:`
* opens an in-process database (tests); a file path is created (with parent
* dirs) on construction.
* opens an in-process database (tests). On filesystems with POSIX modes,
* missing directories and databases are created owner-only; existing path
* modes are preserved. Filesystem setup errors other than an existing database
* fail initialization. The backend does not protect confidentiality or
* integrity when another principal can replace the database entry in its
* parent directory.
*/
path: string
/**
@@ -88,7 +110,10 @@ export class SessionPersistenceSqlite extends SessionPersistence implements Pers
private async openDb(path: string, journalMode: JournalMode): Promise<void> {
const actual = path === ':memory:' ? path : resolve(path)
if (actual !== ':memory:') await mkdir(dirname(actual), { recursive: true, mode: 0o700 })
if (actual !== ':memory:') {
await mkdir(dirname(actual), { recursive: true, mode: 0o700 })
await createDatabaseFile(actual)
}
this.db = openDatabase(actual, journalMode)
try {
const row = this.db.prepare(
@@ -115,6 +140,11 @@ export class SessionPersistenceSqlite extends SessionPersistence implements Pers
// --- SessionPersistence service surface (delegated to the coordinator) ---
/** SQLite has one database, not an independent local artifact per session. */
locate(_meta: SessionHeader): SessionLocation | undefined {
return undefined
}
create(meta: SessionHeader): Promise<void> {
return this.coordinator.create(meta)
}
@@ -263,14 +293,15 @@ export class SessionPersistenceSqlite extends SessionPersistence implements Pers
private writeRow(meta: SessionHeader): void {
this.db.prepare(`
INSERT INTO sessions
(id, version, created_at, cwd, parent_session, seed_length, incarnation, revision)
VALUES (?, ?, ?, ?, ?, ?, ?, 0)
(id, version, created_at, cwd, parent_session, seed_length, delegation_depth, incarnation, revision)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
ON CONFLICT(id) DO UPDATE SET
version = excluded.version,
created_at = excluded.created_at,
cwd = excluded.cwd,
parent_session = excluded.parent_session,
seed_length = excluded.seed_length
seed_length = excluded.seed_length,
delegation_depth = excluded.delegation_depth
`).run(
meta.id,
meta.version,
@@ -278,6 +309,7 @@ export class SessionPersistenceSqlite extends SessionPersistence implements Pers
meta.cwd ?? null,
meta.parentSession ?? null,
meta.seedLength ?? null,
meta.delegationDepth ?? null,
randomUUID(),
)
}
@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-session-persistence-sqlite`.
* @module @deepseek-ai/dsh-session-persistence-sqlite/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-session-persistence-sqlite'
/** Cordis companion plugin name. */
export const name = 'session-persistence-sqlite-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: persistence correctness requires backend round-trip and crash-tail tests;
* this package exposes no continuously observable in-process relation.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */
@@ -17,7 +17,7 @@ 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 = 7
export const SCHEMA_VERSION = 8
/**
* A row of the `sessions` table — the out-of-log metadata ({@link SessionHeader}).
@@ -37,6 +37,7 @@ export interface SessionRow {
incarnation: string
/** Monotonic log-change token incremented in each mutating transaction. */
revision: number
delegation_depth: number | null
}
/** An `events` table row: one `SessionEvent` mapped 1:1 (`data` is JSON text). */
@@ -107,11 +108,12 @@ function configureDatabase(db: DatabaseSync, path: string, journalMode: JournalM
id TEXT PRIMARY KEY,
version INTEGER NOT NULL,
created_at INTEGER NOT NULL,
cwd TEXT,
parent_session TEXT,
seed_length INTEGER,
incarnation TEXT NOT NULL,
revision 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(`
@@ -141,6 +143,7 @@ export function rowToMeta(row: SessionRow): SessionHeader {
...row.cwd !== null ? { cwd: row.cwd } : {},
...row.parent_session !== null ? { parentSession: row.parent_session as SessionId } : {},
...row.seed_length !== null ? { seedLength: row.seed_length } : {},
...row.delegation_depth !== null ? { delegationDepth: row.delegation_depth } : {},
}
}
@@ -188,8 +191,8 @@ export function scanRows(rows: readonly EventRow[]): { preserved: SessionEvent[]
}
})
// The last index that is a valid `turn/end` — the last fully-committed
// boundary (the loop flushes only at turn/end).
// The last index that is a valid `turn/end` — holes through a closed turn
// are always committed corruption.
let lastTurnEnd = -1
for (let i = parsed.length - 1; i >= 0; i--) {
if (parsed[i]?.ok && rows[i]?.type === 'turn/end') { lastTurnEnd = i; break }