refactor(session): drop the dead mutable SessionSummary
SessionSummary (updatedAt/title/firstPrompt) and SessionPersistence.update() were dead state: zero production callers of update(), no production reader of updatedAt/firstPrompt, and ACP's title comes from a tool-call presenter, not storage. The live Session.header was already typed SessionHeader, so the summary only ever existed in the persistence layer, written and read by nothing but its own contract test. Delete it entirely (no SessionMeta alias — SessionMeta collapses to SessionHeader everywhere). This removes the JSONL .summary.json sidecar machinery, the SQLite title/first_prompt/updated_at columns and per-append updated_at bump, and the update() method from the abstract service and both backends. SQLite SCHEMA_VERSION goes 1->2 and openDatabase now rejects any non-current user_version (older or newer) — no migration, unreleased software. Net -400 lines, and it erases the JSONL-sidecar-vs-SQLite-column durability divergence that the upcoming write coordinator would otherwise have to model. Records the decision in docs/rfc/implemented/2026-06-19-drop-mutable-session-summary.md and migrates the 2026-06-14 session-persistence RFC's facts to current truth. Adds a standalone AGENTS.md section "Tests document behavior, not golden truth" (a passing test pins current behavior, not necessarily correct behavior) with the summary-drop as its worked example, and reinforces the no-migration pre-release stance.
This commit is contained in:
@@ -6,9 +6,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)` — `data` is the event payload as JSON text, so the row shape is the event verbatim (including `assistant/chunk`, keeping `seq` contiguous). Out-of-log metadata (`SessionMeta`) lives in a `sessions` row, including the mutable `SessionSummary` fields (`updatedAt`, `title`, `firstPrompt`) that `update()` rewrites without touching the event log. A `sessions` row is written only by the first `append` — its existence is the lazy-materialization signal (`has`/`list` report exactly the sessions that have a row), so no separate column is needed.
|
||||
Each `SessionEvent` maps 1:1 onto a row in an `events` table `(session_id, seq, type, time, data)` — `data` is the event payload as JSON text, so the row shape is the event verbatim (including `assistant/chunk`, keeping `seq` contiguous). Out-of-log metadata (`SessionHeader`) lives in a `sessions` row. A `sessions` row is written only by the first `append` — its existence is the lazy-materialization signal (`has`/`list` report exactly the sessions that have a row), so no separate column is needed.
|
||||
|
||||
The repo targets Node ≥ 24 (the root `engines` field), which includes the stable `node:sqlite` module. The database opens with `foreign_keys = ON` (so `ON DELETE CASCADE` drops a session's events with its row) and `journal_mode = WAL`. The table-layout version is stored in `PRAGMA user_version` and checked on open: a fresh database is stamped with the current `SCHEMA_VERSION`; a database written by a newer, incompatible build (higher `user_version`) is rejected rather than opened against an unknown layout.
|
||||
The repo targets Node ≥ 24 (the root `engines` field), which includes the stable `node:sqlite` module. The database opens with `foreign_keys = ON` (so `ON DELETE CASCADE` drops a session's events with its row) and `journal_mode = WAL`. The table-layout version is stored in `PRAGMA user_version` and checked on open: a fresh database is stamped with the current `SCHEMA_VERSION`; a database written by any other, incompatible build (a non-current `user_version`, older or newer) is rejected rather than opened against an unknown layout — there is no migration (unreleased software).
|
||||
|
||||
## Contract semantics over rows
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
* / interrupted-turn-close-on-load semantics the JSONL backend expresses over
|
||||
* file bytes, expressed here over `node:sqlite` rows. Each `SessionEvent` maps
|
||||
* 1:1 onto a row `(session_id, seq, type, time, data)`; `append` is an INSERT
|
||||
* inside a transaction that asserts the contiguous-seq contract; the mutable
|
||||
* `SessionSummary` lives in the `sessions` metadata row.
|
||||
* inside a transaction that asserts the contiguous-seq contract.
|
||||
*
|
||||
* Like the JSONL backend it is also the write-path plugin: it installs the
|
||||
* `session/event` → buffer → `session/flush` drain, persists a fork's seed once
|
||||
@@ -28,7 +27,7 @@ import {
|
||||
SessionPersistence, assertSerializable, seedCoversPrefix,
|
||||
} from '@deepseek-ai/dsh-session-persistence'
|
||||
import { interruptedTurnClosers } from '@deepseek-ai/dsh-session'
|
||||
import type { Session, SessionEvent, SessionId, SessionMeta, SessionSummary } from '@deepseek-ai/dsh-session'
|
||||
import type { Session, SessionEvent, SessionId, SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
openDatabase, rowToMeta, scanRows, type EventRow, type SessionRow,
|
||||
} from './schema.ts'
|
||||
@@ -47,7 +46,7 @@ export interface Config {
|
||||
|
||||
/** Backend bookkeeping for a session id (NOT the live Session object). */
|
||||
interface SessionState {
|
||||
meta: SessionMeta
|
||||
meta: SessionHeader
|
||||
/** Next seq to write — equals the number of committed events. */
|
||||
cursor: number
|
||||
/** Whether the session has at least one persisted event (materialized). */
|
||||
@@ -108,12 +107,12 @@ export class SessionPersistenceSqlite extends SessionPersistence {
|
||||
|
||||
// --- SessionPersistence backend surface (all serialized per session id) ---
|
||||
|
||||
create(meta: SessionMeta): Promise<void> {
|
||||
const snapshot: SessionMeta = { ...meta }
|
||||
create(meta: SessionHeader): Promise<void> {
|
||||
const snapshot: SessionHeader = { ...meta }
|
||||
return this.serialize(snapshot.id, () => this.createCore(snapshot))
|
||||
}
|
||||
|
||||
private async createCore(meta: SessionMeta): Promise<void> {
|
||||
private async createCore(meta: SessionHeader): Promise<void> {
|
||||
await this.ready
|
||||
if (this.states.has(meta.id)) {
|
||||
throw new Error(`session "${meta.id}" already exists in this backend`)
|
||||
@@ -173,11 +172,7 @@ export class SessionPersistenceSqlite extends SessionPersistence {
|
||||
for (const event of events) {
|
||||
insertEvent.run(id, event.seq, event.type, event.time, JSON.stringify(event.data))
|
||||
}
|
||||
// Bump updatedAt on every append (the mutable summary lives in the row).
|
||||
const updatedAt = Date.now()
|
||||
this.db.prepare('UPDATE sessions SET updated_at = ? WHERE id = ?').run(updatedAt, id)
|
||||
this.db.exec('COMMIT')
|
||||
state.meta = { ...state.meta, updatedAt }
|
||||
} catch (error) {
|
||||
this.db.exec('ROLLBACK')
|
||||
throw error
|
||||
@@ -186,11 +181,11 @@ export class SessionPersistenceSqlite extends SessionPersistence {
|
||||
state.cursor += events.length
|
||||
}
|
||||
|
||||
load(id: SessionId): Promise<{ meta: SessionMeta; events: SessionEvent[] }> {
|
||||
load(id: SessionId): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
|
||||
return this.serialize(id, () => this.loadCore(id))
|
||||
}
|
||||
|
||||
private async loadCore(id: SessionId): Promise<{ meta: SessionMeta; events: SessionEvent[] }> {
|
||||
private async loadCore(id: SessionId): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
|
||||
await this.ready
|
||||
const row = this.rowFor(id)
|
||||
if (row === undefined) throw new Error(`session "${id}" not found`)
|
||||
@@ -290,7 +285,7 @@ export class SessionPersistenceSqlite extends SessionPersistence {
|
||||
if (suffix.length > 0) await this.appendCore(session.header.id, suffix)
|
||||
}
|
||||
|
||||
async list(): Promise<SessionMeta[]> {
|
||||
async list(): Promise<SessionHeader[]> {
|
||||
await this.ready
|
||||
// Every metadata row is a materialized session: the row is written only by
|
||||
// the first append (a created-but-never-appended session has no row), so
|
||||
@@ -320,23 +315,6 @@ export class SessionPersistenceSqlite extends SessionPersistence {
|
||||
this.states.delete(id)
|
||||
}
|
||||
|
||||
update(id: SessionId, summary: Partial<SessionSummary>): Promise<void> {
|
||||
return this.serialize(id, () => this.updateCore(id, summary))
|
||||
}
|
||||
|
||||
private async updateCore(id: SessionId, summary: Partial<SessionSummary>): Promise<void> {
|
||||
await this.ready
|
||||
let state = this.states.get(id)
|
||||
if (state === undefined) state = await this.adopt(id)
|
||||
const nextMeta: SessionMeta = { ...state.meta, ...summary, updatedAt: summary.updatedAt ?? Date.now() }
|
||||
// update's only durable effect is the summary fields; the event log is
|
||||
// untouched. If the row is not materialized yet (a lazy session updated
|
||||
// before its first append) there is nothing to write — keep the pending
|
||||
// summary in memory so the materializing append carries it.
|
||||
if (state.materialized) this.writeRow(nextMeta)
|
||||
state.meta = nextMeta
|
||||
}
|
||||
|
||||
// --- row helpers ---
|
||||
|
||||
/** Fetch a session's row, or undefined if absent. */
|
||||
@@ -346,32 +324,26 @@ export class SessionPersistenceSqlite extends SessionPersistence {
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert-or-replace a session's metadata row. The only callers are the first
|
||||
* materializing `append` and a post-materialization `update`, so writing the
|
||||
* row IS the materialization (its existence is the signal `has`/`list` read);
|
||||
* a never-appended session has no row at all.
|
||||
* Insert-or-replace a session's metadata row. The only caller is the first
|
||||
* materializing `append`, so writing the row IS the materialization (its
|
||||
* existence is the signal `has`/`list` read); a never-appended session has no
|
||||
* row at all.
|
||||
*/
|
||||
private writeRow(meta: SessionMeta): void {
|
||||
private writeRow(meta: SessionHeader): void {
|
||||
this.db.prepare(`
|
||||
INSERT INTO sessions (id, version, created_at, cwd, parent_session, updated_at, title, first_prompt)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO sessions (id, version, created_at, cwd, parent_session)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
version = excluded.version,
|
||||
created_at = excluded.created_at,
|
||||
cwd = excluded.cwd,
|
||||
parent_session = excluded.parent_session,
|
||||
updated_at = excluded.updated_at,
|
||||
title = excluded.title,
|
||||
first_prompt = excluded.first_prompt
|
||||
parent_session = excluded.parent_session
|
||||
`).run(
|
||||
meta.id,
|
||||
meta.version,
|
||||
meta.createdAt,
|
||||
meta.cwd ?? null,
|
||||
meta.parentSession ?? null,
|
||||
meta.updatedAt,
|
||||
meta.title ?? null,
|
||||
meta.firstPrompt ?? null,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -384,7 +356,7 @@ export class SessionPersistenceSqlite extends SessionPersistence {
|
||||
return state
|
||||
}
|
||||
|
||||
private assertVersion(meta: SessionMeta): void {
|
||||
private assertVersion(meta: SessionHeader): void {
|
||||
if (meta.version !== 1) {
|
||||
throw new Error(`unsupported session format version ${meta.version} for "${meta.id}" (only v1 is supported)`)
|
||||
}
|
||||
@@ -513,7 +485,7 @@ export class SessionPersistenceSqlite extends SessionPersistence {
|
||||
}
|
||||
|
||||
// case 4: a genuinely new session.
|
||||
const meta: SessionMeta = { ...session.header, updatedAt: Date.now() }
|
||||
const meta: SessionHeader = { ...session.header }
|
||||
await this.create(meta)
|
||||
const created = this.states.get(id)
|
||||
/* v8 ignore next -- create() always sets the state for the id */
|
||||
|
||||
@@ -8,18 +8,18 @@
|
||||
*/
|
||||
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import type { SessionEvent, SessionId, SessionMeta } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent, SessionId, SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/**
|
||||
* The on-disk schema version. Bumped only on a breaking change to the table
|
||||
* layout; orthogonal to a session's own `version` (which versions the EVENT
|
||||
* vocabulary, stored per session in the `sessions` row).
|
||||
*/
|
||||
export const SCHEMA_VERSION = 1
|
||||
export const SCHEMA_VERSION = 2
|
||||
|
||||
/**
|
||||
* A row of the `sessions` table — the out-of-log metadata (`SessionMeta`). The
|
||||
* row's EXISTENCE is the materialization signal: it is written only by the
|
||||
* A row of the `sessions` table — the out-of-log metadata ({@link SessionHeader}).
|
||||
* The row's EXISTENCE is the materialization signal: it is written only by the
|
||||
* first `append` (lazy materialization), so a created-but-never-appended
|
||||
* session has no row and is absent from `has`/`list`, mirroring the JSONL
|
||||
* backend's "no file until first append".
|
||||
@@ -30,9 +30,6 @@ export interface SessionRow {
|
||||
created_at: number
|
||||
cwd: string | null
|
||||
parent_session: string | null
|
||||
updated_at: number
|
||||
title: string | null
|
||||
first_prompt: string | null
|
||||
}
|
||||
|
||||
/** An `events` table row: one `SessionEvent` mapped 1:1 (`data` is JSON text). */
|
||||
@@ -51,10 +48,11 @@ export interface EventRow {
|
||||
*
|
||||
* The table-layout version is persisted in SQLite's `PRAGMA user_version` and
|
||||
* checked on open: a fresh database (user_version 0) is stamped with the
|
||||
* current {@link SCHEMA_VERSION}; an existing database with a NEWER version
|
||||
* (written by a future, incompatible build) is rejected rather than opened
|
||||
* against a layout this build does not understand. (An older-but-compatible
|
||||
* version would be migrated here when migrations exist; v1 has none.)
|
||||
* current {@link SCHEMA_VERSION}; an existing database whose version is NOT the
|
||||
* current one (written by a different, incompatible build — older or newer) is
|
||||
* REJECTED rather than opened against a layout this build does not understand.
|
||||
* There are no migrations: v1 had a different `sessions` layout and is not
|
||||
* upgraded in place.
|
||||
*/
|
||||
export function openDatabase(path: string): DatabaseSync {
|
||||
const db = new DatabaseSync(path)
|
||||
@@ -62,9 +60,9 @@ export function openDatabase(path: string): DatabaseSync {
|
||||
db.exec('PRAGMA journal_mode = WAL')
|
||||
// `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 > SCHEMA_VERSION) {
|
||||
if (onDisk !== 0 && onDisk !== SCHEMA_VERSION) {
|
||||
db.close()
|
||||
throw new Error(`session database at "${path}" has schema version ${onDisk}, newer than this build supports (${SCHEMA_VERSION})`)
|
||||
throw new Error(`session database at "${path}" has schema version ${onDisk}, incompatible with this build (${SCHEMA_VERSION})`)
|
||||
}
|
||||
if (onDisk === 0) {
|
||||
// Fresh (or pre-versioning) database: stamp the current layout version.
|
||||
@@ -78,10 +76,7 @@ export function openDatabase(path: string): DatabaseSync {
|
||||
version INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
cwd TEXT,
|
||||
parent_session TEXT,
|
||||
updated_at INTEGER NOT NULL,
|
||||
title TEXT,
|
||||
first_prompt TEXT
|
||||
parent_session TEXT
|
||||
) STRICT
|
||||
`)
|
||||
db.exec(`
|
||||
@@ -97,17 +92,14 @@ export function openDatabase(path: string): DatabaseSync {
|
||||
return db
|
||||
}
|
||||
|
||||
/** Reconstruct the full {@link SessionMeta} from a `sessions` row. */
|
||||
export function rowToMeta(row: SessionRow): SessionMeta {
|
||||
/** Reconstruct the {@link SessionHeader} from a `sessions` row. */
|
||||
export function rowToMeta(row: SessionRow): SessionHeader {
|
||||
return {
|
||||
version: row.version,
|
||||
id: row.id as SessionId,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
...row.cwd !== null ? { cwd: row.cwd } : {},
|
||||
...row.parent_session !== null ? { parentSession: row.parent_session as SessionId } : {},
|
||||
...row.title !== null ? { title: row.title } : {},
|
||||
...row.first_prompt !== null ? { firstPrompt: row.first_prompt } : {},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { Session, SessionEvent, SessionMeta } from '@deepseek-ai/dsh-session'
|
||||
import type { Session, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
import SessionPersistenceSqlite, { SCHEMA_VERSION } from '@deepseek-ai/dsh-session-persistence-sqlite'
|
||||
import { openDatabase, scanRows, type EventRow } from '../src/schema.ts'
|
||||
import { runPersistenceContract, meta, oneTurnLog } from '../../session-persistence/tests/contract.ts'
|
||||
@@ -232,14 +232,23 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
|
||||
await b2.dispose()
|
||||
})
|
||||
|
||||
it('rejects opening a database whose schema version is newer than this build', async () => {
|
||||
it('rejects opening a database whose schema version is not the current build (newer OR older)', async () => {
|
||||
const path = await freshDbPath()
|
||||
openDatabase(path).close() // stamp user_version = SCHEMA_VERSION
|
||||
// Bump user_version past what this build supports.
|
||||
const db = openDatabase(path)
|
||||
db.exec(`PRAGMA user_version = ${SCHEMA_VERSION + 1}`)
|
||||
db.close()
|
||||
expect(() => openDatabase(path)).toThrow(/newer than this build/)
|
||||
const dbNewer = openDatabase(path)
|
||||
dbNewer.exec(`PRAGMA user_version = ${SCHEMA_VERSION + 1}`)
|
||||
dbNewer.close()
|
||||
expect(() => openDatabase(path)).toThrow(/incompatible with this build/)
|
||||
|
||||
// A stale OLDER version (e.g. a pre-summary-drop v1 DB) is also rejected —
|
||||
// we do not migrate (unreleased software, no backward-compat).
|
||||
const olderPath = await freshDbPath()
|
||||
openDatabase(olderPath).close()
|
||||
const dbOlder = openDatabase(olderPath)
|
||||
dbOlder.exec('PRAGMA user_version = 1')
|
||||
dbOlder.close()
|
||||
expect(() => openDatabase(olderPath)).toThrow(/incompatible with this build/)
|
||||
})
|
||||
|
||||
it('append snapshots the batch: mutating an event after the call does not corrupt the persisted copy', async () => {
|
||||
@@ -323,7 +332,6 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
|
||||
const fiber1 = await ctx1.plugin(SessionPersistenceSqlite, { path })
|
||||
await ctx1.sessionPersistence.create(m)
|
||||
await ctx1.sessionPersistence.append(m.id, oneTurnLog())
|
||||
await ctx1.sessionPersistence.update(m.id, { title: 'T', firstPrompt: 'hi' })
|
||||
await fiber1.dispose()
|
||||
|
||||
const ctx2 = new Context()
|
||||
@@ -331,7 +339,7 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
|
||||
const fiber2 = await ctx2.plugin(SessionPersistenceSqlite, { path })
|
||||
expect((await ctx2.sessionPersistence.list()).map(x => x.id)).toContain(m.id)
|
||||
const loaded = await ctx2.sessionPersistence.load(m.id)
|
||||
expect(loaded.meta).toMatchObject({ id: m.id, cwd: '/proj', title: 'T', firstPrompt: 'hi' })
|
||||
expect(loaded.meta).toMatchObject({ id: m.id, cwd: '/proj' })
|
||||
expect(loaded.events).toEqual(oneTurnLog())
|
||||
await fiber2.dispose()
|
||||
})
|
||||
@@ -340,8 +348,8 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
|
||||
const path = await freshDbPath()
|
||||
// Materialize a row with version 2 directly via the real schema.
|
||||
const db = openDatabase(path)
|
||||
db.prepare('INSERT INTO sessions (id, version, created_at, updated_at) VALUES (?, ?, ?, ?)')
|
||||
.run('v2', 2, 1, 1)
|
||||
db.prepare('INSERT INTO sessions (id, version, created_at) VALUES (?, ?, ?)')
|
||||
.run('v2', 2, 1)
|
||||
db.close()
|
||||
|
||||
const ctx = new Context()
|
||||
@@ -372,7 +380,7 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => {
|
||||
})
|
||||
|
||||
it('exposes the schema version constant', () => {
|
||||
expect(SCHEMA_VERSION).toBe(1)
|
||||
expect(SCHEMA_VERSION).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -468,22 +476,6 @@ describe('SessionPersistenceSqlite: write path (session/event → flush)', () =>
|
||||
await expect(ctx2.parallel('session/flush', s2)).rejects.toThrow(/id collision/)
|
||||
await fiber2.dispose()
|
||||
})
|
||||
|
||||
it('update before the first append keeps the summary in memory and the session lazy', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const fiber = await ctx.plugin(SessionPersistenceSqlite, { path: ':memory:' })
|
||||
const m = meta('lazy-update')
|
||||
await ctx.sessionPersistence.create(m)
|
||||
await ctx.sessionPersistence.update(m.id, { title: 'pending' })
|
||||
// Still lazy: no materialized row yet.
|
||||
expect(await ctx.sessionPersistence.has(m.id)).toBe(false)
|
||||
// The first append materializes and carries the pending title.
|
||||
await ctx.sessionPersistence.append(m.id, oneTurnLog())
|
||||
const loaded = await ctx.sessionPersistence.load(m.id)
|
||||
expect(loaded.meta.title).toBe('pending')
|
||||
await fiber.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
describe('SessionPersistenceSqlite: edge cases', () => {
|
||||
@@ -529,21 +521,6 @@ describe('SessionPersistenceSqlite: edge cases', () => {
|
||||
await b2.dispose()
|
||||
})
|
||||
|
||||
it('update adopts a session that exists only in the DB (fresh instance)', async () => {
|
||||
const path = await freshDbPath()
|
||||
const m = meta('adopt-update')
|
||||
const b1 = await backend(path)
|
||||
await b1.ctx.sessionPersistence.create(m)
|
||||
await b1.ctx.sessionPersistence.append(m.id, oneTurnLog())
|
||||
await b1.dispose()
|
||||
|
||||
const b2 = await backend(path)
|
||||
await b2.ctx.sessionPersistence.update(m.id, { title: 'after restart' })
|
||||
const loaded = await b2.ctx.sessionPersistence.load(m.id)
|
||||
expect(loaded.meta.title).toBe('after restart')
|
||||
await b2.dispose()
|
||||
})
|
||||
|
||||
it('append rolls back and rethrows when an event INSERT fails inside the transaction', async () => {
|
||||
const path = await freshDbPath()
|
||||
const m = meta('rollback-insert')
|
||||
@@ -574,7 +551,7 @@ describe('SessionPersistenceSqlite: edge cases', () => {
|
||||
|
||||
it('round-trips a header with parentSession (fork lineage)', async () => {
|
||||
const { ctx, dispose } = await backend()
|
||||
const m: SessionMeta = { ...meta('child'), parentSession: SessionId('parent') }
|
||||
const m: SessionHeader = { ...meta('child'), parentSession: SessionId('parent') }
|
||||
await ctx.sessionPersistence.create(m)
|
||||
await ctx.sessionPersistence.append(m.id, oneTurnLog())
|
||||
const loaded = await ctx.sessionPersistence.load(m.id)
|
||||
|
||||
Reference in New Issue
Block a user