fix(session-query): reject misplaced surface ops

This commit is contained in:
Hypatia May
2026-07-13 16:05:11 +08:00
parent 3d3789bf2d
commit 84e6f72ef5
12 changed files with 182 additions and 114 deletions

View File

@@ -14,7 +14,7 @@ This is trusted context-wide infrastructure. It performs no caller authorization
Persistence is optional and may mount or unmount dynamically. Cross-corpus listing and lineage tracing fail with `SESSION_QUERY_PERSISTENCE_FAILED` while mounted persistence is unreadable. An event read or trace targeting a known live session does not consult persistence, so durable backend health cannot make current in-memory history unreadable. Persisted event operations list before loading and reject a metadata mismatch rather than combining inconsistent observations.
`traceEvent()` validates the whole loaded log with `dsh-session`'s shared provenance checker before returning relationships: provenance arrays are nonempty and duplicate-free, references name known earlier events, only surface event types carry sources, and each positional replacement names every surface node it removed. Provenance violations fail with `SESSION_QUERY_INVALID_PROVENANCE`; positional fold failures remain `SESSION_QUERY_INVALID_SURFACE`. `listEvents()` only needs surface classification and deliberately does not enforce the trace-specific provenance contract.
`traceEvent()` validates the whole loaded log with `dsh-session`'s shared surface-metadata checker before returning relationships: surface markers obey event-type eligibility, provenance arrays are nonempty and duplicate-free, references name known earlier events, and each positional replacement names every surface node it removed. Surface-marker and positional-fold violations fail with `SESSION_QUERY_INVALID_SURFACE`; provenance violations use `SESSION_QUERY_INVALID_PROVENANCE`. `listEvents()` only needs surface classification and deliberately does not enforce the trace-specific provenance contract.
`SessionQueryError.code` is a closed union: `SESSION_QUERY_EVENT_NOT_FOUND`, `SESSION_QUERY_INVALID_CONFIG`, `SESSION_QUERY_INVALID_LINEAGE`, `SESSION_QUERY_INVALID_PROVENANCE`, `SESSION_QUERY_INVALID_SURFACE`, `SESSION_QUERY_INVALID_WINDOW`, `SESSION_QUERY_PERSISTENCE_FAILED`, `SESSION_QUERY_SESSION_NOT_FOUND`, and `SESSION_QUERY_SOURCE_CONFLICT`.

View File

@@ -1,6 +1,6 @@
/** One-shot session-lineage and event-relationship tracing helpers. */
import { foldSurface, validateSurfaceProvenance } from '@deepseek-ai/dsh-session'
import { foldSurface, validateSurfaceMetadata } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
import { SessionQueryError } from './config.ts'
import type {
@@ -53,14 +53,14 @@ export function traceEventLog(
const analysis = analyzeEventLog(sessionId, events)
const knownSeqs = new Set<number>()
for (const event of events) {
const violation = validateSurfaceProvenance(
const violation = validateSurfaceMetadata(
event,
knownSeqs,
analysis.replacedEventSeqs.get(event.seq),
)
if (violation !== undefined) {
throw new SessionQueryError(
`invalid session provenance: ${violation}`,
`invalid session provenance: ${violation.message}`,
'SESSION_QUERY_INVALID_PROVENANCE',
)
}

View File

@@ -390,6 +390,23 @@ describe('session event tracing', () => {
.rejects.toThrow(expectCode('SESSION_QUERY_INVALID_PROVENANCE'))
})
it('rejects surfaceOp on a non-surface event as an invalid surface', async () => {
const durable = header('invalid-non-surface-op')
const events = [{
type: 'turn/start',
seq: 0,
time: 1,
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
surfaceOp: 'append',
}] as unknown as SessionEvent[]
TracePersistence.reset([{ meta: durable, events }])
const ctx = await queryContext()
await ctx.plugin(TracePersistence)
await expect(ctx.sessionQuery.traceEvent({ sessionId: durable.id, seq: 0 }))
.rejects.toThrow(expectCode('SESSION_QUERY_INVALID_SURFACE'))
})
it('keeps listEvents tolerant of malformed provenance alone', async () => {
const durable = header('list-regression')
TracePersistence.reset([{ meta: durable, events: [appendEvent(0), appendEvent(1, [0, 0])] }])