mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix: reject legacy session events on every path
This commit is contained in:
@@ -251,11 +251,15 @@ export class PersistenceCoordinator<TornMarker = unknown> {
|
||||
if (batch === undefined) {
|
||||
throw new TypeError('session event batch is not losslessly JSON-serializable because it contains non-JSON-serializable data')
|
||||
}
|
||||
assertSupportedEvents(batch, id)
|
||||
return this.serialize(id, () => this.appendCore(id, batch))
|
||||
}
|
||||
|
||||
private async appendCore(id: SessionId, events: readonly SessionEvent[]): Promise<void> {
|
||||
// Every append route converges here: the public service, live write-behind
|
||||
// drains, and HMR seed/suffix adoption. Keep vocabulary rejection at that
|
||||
// shared boundary so a stale JavaScript plugin cannot persist an event that
|
||||
// this same backend will refuse to load.
|
||||
assertSupportedEvents(events, id)
|
||||
if (events.length === 0) return
|
||||
let state = this.states.get(id)
|
||||
if (state === undefined) state = await this.adopt(id) // calls loadCore, not load
|
||||
@@ -528,6 +532,7 @@ export class PersistenceCoordinator<TornMarker = unknown> {
|
||||
private async adoptLivePrefix(session: Session, seed: readonly SessionEvent[], stored: StoredPrefix<TornMarker>): Promise<void> {
|
||||
const { meta, events, tornMarker } = stored
|
||||
this.assertVersion(meta)
|
||||
assertSupportedEvents(events, session.header.id)
|
||||
if (!seedCoversPrefix(seed, events)) {
|
||||
throw new Error(`session "${session.header.id}" already has a persisted log on disk that does not match this live session (id collision)`)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,16 @@ import { runCoordinatorContract, type CoordinatorFixture } from './coordinator-c
|
||||
/** The durable store shape: materialized sessions only (no lazy entries). */
|
||||
type MemoryStore = Map<string, { meta: SessionHeader; events: SessionEvent[] }>
|
||||
|
||||
/** An obsolete event fixture that emulates an untyped pre-change producer. */
|
||||
function legacyHeaderDelta(seq = 0): SessionEvent {
|
||||
return {
|
||||
type: 'request/header-delta',
|
||||
seq,
|
||||
time: 1,
|
||||
data: { config: { model: 'legacy' } },
|
||||
} as unknown as SessionEvent
|
||||
}
|
||||
|
||||
/** Optional plugin config: an EXTERNAL store shared across backend instances. */
|
||||
interface MemoryConfig { store?: MemoryStore }
|
||||
|
||||
@@ -164,4 +174,37 @@ describe('SessionPersistence service registration', () => {
|
||||
.rejects.toThrow('session metadata must be losslessly JSON-serializable')
|
||||
await fiber.dispose()
|
||||
})
|
||||
|
||||
it('rejects a legacy header delta buffered by a pre-change live producer', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const fiber = await ctx.plugin(MemoryPersistence)
|
||||
const session = ctx.sessions.create(SessionId('legacy-live'), { meta: { cwd: '/legacy' } })
|
||||
// Model the runtime shape available to JavaScript or a hot-loaded plugin
|
||||
// compiled against the obsolete event vocabulary.
|
||||
const appendLegacy = session.append.bind(session) as (type: string, data: unknown) => SessionEvent
|
||||
appendLegacy('request/header-delta', { config: { model: 'legacy' } })
|
||||
|
||||
await expect(ctx.sessions.flush(session))
|
||||
.rejects.toThrow(/unsupported legacy request\/header-delta event at seq 0/)
|
||||
await fiber.dispose()
|
||||
})
|
||||
|
||||
it('rejects a legacy stored prefix during live HMR adoption', async () => {
|
||||
const id = SessionId('legacy-hmr')
|
||||
const m = meta(id, '/legacy')
|
||||
const legacy = legacyHeaderDelta()
|
||||
const store: MemoryStore = new Map([[id, { meta: m, events: [legacy] }]])
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
// A current live session cannot carry the obsolete event in its seed, but
|
||||
// HMR still has to identify the persisted prefix as unsupported rather than
|
||||
// treating it as an ordinary live-prefix collision.
|
||||
const session = ctx.sessions.create(id, { meta: { cwd: '/legacy' } })
|
||||
const fiber = await ctx.plugin(MemoryPersistence, { store })
|
||||
|
||||
await expect(ctx.sessions.flush(session))
|
||||
.rejects.toThrow(/unsupported legacy request\/header-delta event at seq 0/)
|
||||
await Promise.allSettled([fiber.dispose()])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
"workflow"
|
||||
]
|
||||
},
|
||||
"reason": "fallback"
|
||||
"reason": "change"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -916,22 +916,29 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "request/header-delta",
|
||||
"type": "request/header",
|
||||
"seq": 56,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"system": {
|
||||
"keepStart": 62,
|
||||
"keepEnd": 34,
|
||||
"insert": []
|
||||
"header": {
|
||||
"config": {
|
||||
"model": "smoke-model"
|
||||
},
|
||||
"system": "{{system}}",
|
||||
"tools": [
|
||||
"bash",
|
||||
"bash_kill",
|
||||
"bash_output",
|
||||
"cordis_inspect",
|
||||
"cordis_mount",
|
||||
"cordis_unmount",
|
||||
"run_code",
|
||||
"skill",
|
||||
"subagent",
|
||||
"workflow"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"added": [],
|
||||
"removed": [
|
||||
"snapshot_double"
|
||||
],
|
||||
"changed": []
|
||||
}
|
||||
"reason": "change"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1397,7 +1404,7 @@
|
||||
"workflow"
|
||||
]
|
||||
},
|
||||
"reason": "fallback"
|
||||
"reason": "change"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2360,22 +2367,29 @@
|
||||
"payload": {
|
||||
"sessionId": "{{parent}}",
|
||||
"event": {
|
||||
"type": "request/header-delta",
|
||||
"type": "request/header",
|
||||
"seq": 56,
|
||||
"time": 0,
|
||||
"data": {
|
||||
"system": {
|
||||
"keepStart": 62,
|
||||
"keepEnd": 34,
|
||||
"insert": []
|
||||
"header": {
|
||||
"config": {
|
||||
"model": "smoke-model"
|
||||
},
|
||||
"system": "{{system}}",
|
||||
"tools": [
|
||||
"bash",
|
||||
"bash_kill",
|
||||
"bash_output",
|
||||
"cordis_inspect",
|
||||
"cordis_mount",
|
||||
"cordis_unmount",
|
||||
"run_code",
|
||||
"skill",
|
||||
"subagent",
|
||||
"workflow"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"added": [],
|
||||
"removed": [
|
||||
"snapshot_double"
|
||||
],
|
||||
"changed": []
|
||||
}
|
||||
"reason": "change"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
{"type":"tool/result","seq":11,"time":0,"data":{"turn":1,"step":1,"callId":"advanced-mount","content":[{"type":"text","text":"mounted dyn-1 (plugin \"<anonymous>\", state: active)"}],"isError":false},"sourceEventSeqs":[10],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":12,"time":0,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":13,"time":0,"data":{"turn":1,"step":2}}
|
||||
{"type":"request/header","seq":14,"time":0,"data":{"header":{"config":{"model":"smoke-model"},"system":"{{system}}","tools":["bash","bash_kill","bash_output","cordis_inspect","cordis_mount","cordis_unmount","run_code","skill","snapshot_double","subagent","workflow"]},"reason":"fallback"}}
|
||||
{"type":"request/header","seq":14,"time":0,"data":{"header":{"config":{"model":"smoke-model"},"system":"{{system}}","tools":["bash","bash_kill","bash_output","cordis_inspect","cordis_mount","cordis_unmount","run_code","skill","snapshot_double","subagent","workflow"]},"reason":"change"}}
|
||||
{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"tool-call-delta","index":0,"id":"advanced-code","name":"run_code","argumentsDelta":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\"}"}}}
|
||||
{"type":"assistant/chunk","seq":17,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\": \"return await tools.snapshot_double({ value: 21 })\"}"}}}}
|
||||
@@ -55,7 +55,7 @@
|
||||
{"type":"tool/result","seq":53,"time":0,"data":{"turn":1,"step":5,"callId":"advanced-unmount","content":[{"type":"text","text":"unmounted dyn-1 (plugin \"<anonymous>\")"}],"isError":false},"sourceEventSeqs":[52],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":54,"time":0,"data":{"turn":1,"step":5}}
|
||||
{"type":"step/start","seq":55,"time":0,"data":{"turn":1,"step":6}}
|
||||
{"type":"request/header-delta","seq":56,"time":0,"data":{"system":{"keepStart":62,"keepEnd":34,"insert":[]},"tools":{"added":[],"removed":["snapshot_double"],"changed":[]}}}
|
||||
{"type":"request/header","seq":56,"time":0,"data":{"header":{"config":{"model":"smoke-model"},"system":"{{system}}","tools":["bash","bash_kill","bash_output","cordis_inspect","cordis_mount","cordis_unmount","run_code","skill","subagent","workflow"]},"reason":"change"}}
|
||||
{"type":"assistant/chunk","seq":57,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":58,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"text-delta","index":0,"text":"ADVANCED_EXECUTABLE_OK"}}}
|
||||
{"type":"assistant/chunk","seq":59,"time":0,"data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"ADVANCED_EXECUTABLE_OK"}}}}
|
||||
|
||||
Reference in New Issue
Block a user