fix(session-query): make persisted observation non-mutating

This commit is contained in:
Hypatia May
2026-07-23 21:33:28 +08:00
parent c1fa985c20
commit 9482affc48
22 changed files with 239 additions and 63 deletions

View File

@@ -438,11 +438,12 @@ export class SessionQuerySqlite extends SessionQueryService {
persisted = materializePersistenceSnapshots(before)
for (const entry of persisted.values()) {
if (canReuseIndexed && indexed.get(entry.header.id)?.revision === entry.revision) continue
// `load()` may durably repair an interrupted tail. Never invoke it
// for a session currently owned by the live store: a checkpointed
// open turn is active, not crash-interrupted.
// Skip work already shadowed by a live owner. `inspect()` is
// non-mutating, so an owner attaching after this check cannot cause
// crash-repair side effects; the live-membership retry below makes
// the returned observation live-preferred.
if (initiallyLive.has(entry.header.id) || this.ctx.sessions.get(entry.header.id) !== undefined) continue
const loaded = await waitWithAbort(persistence.load(entry.header.id), signal)
const loaded = await waitWithAbort(persistence.inspect(entry.header.id), signal)
assertSessionHeadersCompatible(entry.header, loaded.meta)
entry.loaded = observeSession(loaded.meta, loaded.events)
}

View File

@@ -60,8 +60,9 @@ export async function openSearchDatabase(path: string, journalMode: JournalMode)
if (applicationId === 0 && userTables.length > 0) {
throw new Error(`session-search database at "${actual}" is not an empty or recognized derived index`)
}
if (applicationId === SESSION_QUERY_SQLITE_APPLICATION_ID && version !== SESSION_QUERY_SQLITE_SCHEMA_VERSION) {
resetDerivedSchema(db, actual, userTables)
if (applicationId === SESSION_QUERY_SQLITE_APPLICATION_ID) {
assertDerivedUserTables(actual, userTables)
if (version !== SESSION_QUERY_SQLITE_SCHEMA_VERSION) resetDerivedSchema(db, userTables)
}
// Apply mutating pragmas only after refusing foreign or canonical files.
// journalMode is a validated closed union, not caller-controlled SQL.
@@ -82,13 +83,16 @@ function listUserTables(db: DatabaseSync): string[] {
return rows.map(row => row.name)
}
function resetDerivedSchema(db: DatabaseSync, path: string, userTables: readonly string[]): void {
function assertDerivedUserTables(path: string, userTables: readonly string[]): void {
const unknownTables = userTables.filter(name => !DERIVED_USER_TABLES.has(name))
if (unknownTables.length > 0) {
throw new Error(
`session-search database at "${path}" has unrecognized user tables: ${unknownTables.join(', ')}`,
)
}
}
function resetDerivedSchema(db: DatabaseSync, userTables: readonly string[]): void {
for (const name of userTables) {
db.exec(`DROP TABLE IF EXISTS ${quoteIdentifier(name)}`)
}