fix review findings: skip collided SCHEMA_VERSION 3; reject marker-less surface events

P1: both merge parents shipped SCHEMA_VERSION=3 for different layouts (surface
columns vs seed_length), so an on-disk 3 was ambiguous and wrongly accepted.
Bump to 4 (merged layout) so the version check rejects both sibling v3s.

P2: a surface-eligible event with no surfaceOp lands in the log but vanishes
from deriveMessages() (surface is the sole derivation path). The typed append
overload enforces the marker only when the type arg is a literal; it collapses
to optional when widened to the union (a caller iterating raw events). Guard at
runtime in both append() and the seed constructor — no backward-compat for
surface-less logs. Shared seed fixtures carry surfaceOp explicitly and the
appendLog helper forwards it verbatim (no synthesized default). Exports
isSurfaceEligibleType. Regression tests for all three, each verified to fail
on the unfixed code.

Gates: typecheck, test (1115), snapshot (14), doc-sync, lint, build, hygiene green.
This commit is contained in:
Hypatia May
2026-06-24 17:45:48 +08:00
parent 9cc8dc371e
commit 828c3f85c9
14 changed files with 166 additions and 36 deletions
@@ -10,7 +10,7 @@
import { describe, expect, it } from 'vitest'
import { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SessionHeader, SurfaceEventType, SurfaceIntent } from '@deepseek-ai/dsh-session'
import { CallId } from '@deepseek-ai/dsh-llm'
import type { SessionPersistence } from '../src/index.ts'
@@ -34,14 +34,40 @@ export function meta(id: string, cwd?: string): SessionHeader {
export function oneTurnLog(): SessionEvent[] {
return [
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } },
{ type: 'user/message', seq: 1, time: 2, data: { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } } },
{ type: 'user/message', seq: 1, time: 2, data: { content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }, surfaceOp: 'append' },
{ type: 'step/start', seq: 2, time: 3, data: { turn: 1, step: 1 } },
{ type: 'assistant/message', seq: 3, time: 4, data: { turn: 1, step: 1, content: [{ type: 'text', text: 'hello' }] } },
{ type: 'assistant/message', seq: 3, time: 4, data: { turn: 1, step: 1, content: [{ type: 'text', text: 'hello' }] }, surfaceOp: 'append' },
{ type: 'step/end', seq: 4, time: 5, data: { turn: 1, step: 1 } },
{ type: 'turn/end', seq: 5, time: 6, data: { turn: 1, reason: { kind: 'completed' } } },
]
}
/**
* Append a whole event log to a LIVE session, event by event, forwarding the
* surface metadata each event already carries. A bare `append(e.type, e.data)`
* over a `SessionEvent[]` widens the type argument to the union, where the
* typed overload's mandatory-marker rule collapses to optional — and `append`'s
* runtime guard then rejects a surface-eligible event with no marker. This
* helper forwards the `surfaceOp`/`sourceEventSeqs` VERBATIM from the source
* event (it does not synthesize a default), so a well-formed recorded log
* round-trips through a live session intact and a fixture that forgot a marker
* still trips the guard.
*/
export function appendLog(session: Session, events: readonly SessionEvent[]): void {
for (const e of events) {
const se = e as SessionEvent<SurfaceEventType>
if (se.surfaceOp !== undefined) {
const intent: SurfaceIntent = {
surfaceOp: se.surfaceOp,
...se.sourceEventSeqs !== undefined ? { sourceEventSeqs: se.sourceEventSeqs } : {},
}
session.append(e.type, e.data, intent)
} else {
session.append(e.type, e.data)
}
}
}
/**
* Run the backend-agnostic contract suite. `make()` MUST return a fresh, empty
* backend each call.
@@ -31,7 +31,7 @@ import { Context, type Fiber } from 'cordis'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import type { SessionPersistence } from '../src/index.ts'
import { meta, oneTurnLog } from './contract.ts'
import { meta, oneTurnLog, appendLog } from './contract.ts'
/**
* The backend-specific capabilities the orchestration suite needs beyond the
@@ -76,7 +76,7 @@ function inits(persistence: SessionPersistence): Map<Session, Promise<void>> {
/** Append a whole event log to a live session, event by event (drives session/event). */
function send(session: Session, events: readonly SessionEvent[]): void {
for (const e of events) session.append(e.type, e.data)
appendLog(session, events)
}
/** A live session created inside its OWN fiber, so it survives a backend reload. */