mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor(session): separate validation from snapshots
This commit is contained in:
@@ -103,17 +103,16 @@ declare module 'cordis' {
|
||||
}
|
||||
}
|
||||
|
||||
/** Detach, validate, and freeze the creation metadata published by a session. */
|
||||
function snapshotSessionHeader(id: SessionId, source?: SessionHeader): SessionHeader {
|
||||
const input: unknown = source === undefined
|
||||
? { version: SESSION_FORMAT_VERSION, id, createdAt: Date.now() }
|
||||
: source
|
||||
const snapshot = snapshotJsonValue(input)
|
||||
if (snapshot === undefined) throw new Error('session header is not losslessly JSON-serializable')
|
||||
if (snapshot === null || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
||||
/** Validate and freeze one detached creation header in place. */
|
||||
function validateSessionHeader(id: SessionId, input: unknown): SessionHeader {
|
||||
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
||||
throw new Error('session header is not a plain JSON record')
|
||||
}
|
||||
const record = snapshot as Record<string, unknown>
|
||||
const prototype = Reflect.getPrototypeOf(input)
|
||||
if (prototype !== Object.prototype && prototype !== null) {
|
||||
throw new Error('session header is not a plain JSON record')
|
||||
}
|
||||
const record = input as Record<string, unknown>
|
||||
if (record.version !== SESSION_FORMAT_VERSION) {
|
||||
throw new Error(`session header version must be ${SESSION_FORMAT_VERSION}, got ${String(record.version)}`)
|
||||
}
|
||||
@@ -148,31 +147,52 @@ function snapshotSessionHeader(id: SessionId, source?: SessionHeader): SessionHe
|
||||
return deepFreeze(record as unknown as SessionHeader)
|
||||
}
|
||||
|
||||
/** Detach, validate, and freeze the creation metadata published by a session. */
|
||||
function snapshotSessionHeader(id: SessionId, source?: SessionHeader): SessionHeader {
|
||||
const input: unknown = source === undefined
|
||||
? { version: SESSION_FORMAT_VERSION, id, createdAt: Date.now() }
|
||||
: source
|
||||
const snapshot = snapshotJsonValue(input)
|
||||
if (snapshot === undefined) throw new Error('session header is not losslessly JSON-serializable')
|
||||
return validateSessionHeader(id, snapshot)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an exclusively owned event and deeply freeze its identified message
|
||||
* without copying the event. The caller transfers an object graph that no
|
||||
* producer retains and that shares no mutable children with another event.
|
||||
* Use {@link snapshotSessionEvent} when exclusive ownership is not guaranteed.
|
||||
* @param event - exclusively owned event imported across a trusted boundary.
|
||||
* @returns the same event object with a validated, deeply frozen message.
|
||||
*/
|
||||
export function adoptSessionEvent<T extends SessionEvent>(event: T): T {
|
||||
assertMessageEventShape(
|
||||
event,
|
||||
`session event at seq ${event.seq}`,
|
||||
)
|
||||
switch (event.type) {
|
||||
case 'user/message':
|
||||
deepFreeze(event.data)
|
||||
break
|
||||
case 'assistant/message':
|
||||
case 'tool/result':
|
||||
case 'steering/message':
|
||||
deepFreeze(event.data.message)
|
||||
break
|
||||
default:
|
||||
// SessionEventMap is merge-extensible; plugin-owned events carry no core message.
|
||||
break
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach one event while preserving deep immutability for its identified message.
|
||||
* @param event - event imported across a query or persistence boundary.
|
||||
* @returns a detached event snapshot with a validated, deeply frozen message.
|
||||
*/
|
||||
export function snapshotSessionEvent<T extends SessionEvent>(event: T): T {
|
||||
const snapshot = structuredClone(event)
|
||||
assertMessageEventShape(
|
||||
snapshot,
|
||||
`session event at seq ${snapshot.seq}`,
|
||||
)
|
||||
switch (snapshot.type) {
|
||||
case 'user/message':
|
||||
deepFreeze(snapshot.data)
|
||||
break
|
||||
case 'assistant/message':
|
||||
case 'tool/result':
|
||||
case 'steering/message':
|
||||
deepFreeze(snapshot.data.message)
|
||||
break
|
||||
default:
|
||||
// SessionEventMap is merge-extensible; plugin-owned events carry no core message.
|
||||
break
|
||||
}
|
||||
return snapshot
|
||||
return adoptSessionEvent(structuredClone(event))
|
||||
}
|
||||
|
||||
/** Validate the fixed event envelope after one-pass JSON materialization. */
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, expectTypeOf, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { createUserMessage, CallId, createMessage, createToolResultMessage, MessageId, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, {
|
||||
adoptSessionEvent,
|
||||
findLastMessageTurnEnd,
|
||||
SESSION_FORMAT_VERSION,
|
||||
Session,
|
||||
@@ -389,6 +390,45 @@ describe('Session', () => {
|
||||
.toEqual([{ type: 'plugin-block', value: 1 }])
|
||||
})
|
||||
|
||||
it('adopts exclusively owned messages in place and keeps snapshots detached', () => {
|
||||
const owned = {
|
||||
type: 'user/message',
|
||||
seq: 0,
|
||||
time: 1,
|
||||
surfaceOp: 'append',
|
||||
data: {
|
||||
id: 'owned-message',
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: 'owned' }],
|
||||
source: { kind: 'user' },
|
||||
},
|
||||
} as SessionEvent<'user/message'>
|
||||
expect(adoptSessionEvent(owned)).toBe(owned)
|
||||
expect(Object.isFrozen(owned.data)).toBe(true)
|
||||
expect(Object.isFrozen(owned.data.content)).toBe(true)
|
||||
|
||||
const source = structuredClone(owned)
|
||||
const snapshot = snapshotSessionEvent(source)
|
||||
expect(snapshot).not.toBe(source)
|
||||
expect(snapshot.data).not.toBe(source.data)
|
||||
expect(snapshot.data.content).not.toBe(source.data.content)
|
||||
})
|
||||
|
||||
it('validates message shape before adopting ownership', () => {
|
||||
const malformed = {
|
||||
type: 'user/message',
|
||||
seq: 0,
|
||||
time: 1,
|
||||
data: {
|
||||
id: 'wrong-role',
|
||||
role: 'assistant',
|
||||
content: [],
|
||||
source: { kind: 'user' },
|
||||
},
|
||||
} as unknown as SessionEvent
|
||||
expect(() => adoptSessionEvent(malformed)).toThrow('message must have role "user"')
|
||||
})
|
||||
|
||||
it('round-trips a non-empty reasoning effort and rejects invalid durable values', () => {
|
||||
const valid = {
|
||||
type: 'request/header',
|
||||
|
||||
Reference in New Issue
Block a user