Merge branch 'master' into xtr/trajectory-inspection-ui

This commit is contained in:
_Kerman
2026-07-28 15:41:45 +08:00
committed by GitHub
103 changed files with 992 additions and 881 deletions

View File

@@ -17,6 +17,11 @@ interface CompactionTrace {
summarized: boolean
}
interface SessionTrace {
openTurn: number | null
compaction: CompactionTrace | undefined
}
type CompactionTransition =
| { kind: 'start'; turn: number }
| { kind: 'summary'; turn: number }
@@ -24,16 +29,27 @@ type CompactionTransition =
/** Validate one compaction event without advancing committed trace state. */
function validateCompactionEvent(
open: CompactionTrace | undefined,
trace: SessionTrace,
event: SessionEvent,
fail: InvariantFailure,
): CompactionTransition | undefined {
if (event.type !== 'compact/start' && event.type !== 'compact/summary' && event.type !== 'compact/end') {
return undefined
}
if (trace.openTurn === null) fail(`${event.type} appended outside any open turn`)
const open = trace.compaction
if (event.type === 'compact/start') {
if (open !== undefined) fail(`compact/start for turn ${event.data.turn} while turn ${open.turn} is still compacting`)
if (event.data.turn !== trace.openTurn) {
fail(`compact/start names turn ${event.data.turn} but open turn is ${trace.openTurn}`)
}
return { kind: 'start', turn: event.data.turn }
}
if (event.type === 'compact/summary') {
if (open === undefined) fail('compact/summary has no matching compact/start')
if (open.turn !== trace.openTurn) {
fail(`compact/summary belongs to turn ${open.turn} but open turn is ${trace.openTurn}`)
}
if (open.summarized) fail('compact/summary repeated within one compaction')
const seqs = event.data.shadowedSeqs
if (seqs.length === 0) fail('compact/summary shadowedSeqs must be non-empty')
@@ -45,11 +61,13 @@ function validateCompactionEvent(
}
return { kind: 'summary', turn: open.turn }
}
if (event.type !== 'compact/end') return undefined
if (open === undefined) fail('compact/end has no matching compact/start')
if (event.data.turn !== open.turn) {
fail(`compact/end turn ${event.data.turn} does not match compact/start turn ${open.turn}`)
}
if (event.data.turn !== trace.openTurn) {
fail(`compact/end names turn ${event.data.turn} but open turn is ${trace.openTurn}`)
}
if (event.data.error === undefined && !open.summarized) {
fail('successful compact/end requires one compact/summary')
}
@@ -69,29 +87,39 @@ function applyCompactionTransition(
// Event owners keep precommit staging local so their vocabularies never move into a central helper.
/* jscpd:ignore-start */
const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
const traces = new WeakMap<Session, CompactionTrace>()
const traces = new WeakMap<Session, SessionTrace>()
const staged = new WeakMap<SessionEvent, { session: Session; transition: CompactionTransition }>()
const seed = (session: Session): void => {
let open: CompactionTrace | undefined
const seed = (session: Session): SessionTrace => {
const trace: SessionTrace = { openTurn: null, compaction: undefined }
traces.set(session, trace)
for (const event of session.events) {
const transition = validateCompactionEvent(open, event, fail)
if (transition !== undefined) open = applyCompactionTransition(transition)
if (event.type === 'turn/start') trace.openTurn = event.data.turn
else if (event.type === 'turn/end') trace.openTurn = null
const transition = validateCompactionEvent(trace, event, fail)
if (transition !== undefined) trace.compaction = applyCompactionTransition(transition)
}
if (open !== undefined) traces.set(session, open)
return trace
}
const traceFor = (session: Session): CompactionTrace | undefined => traces.get(session)
const traceFor = (session: Session): SessionTrace => traces.get(session) ?? seed(session)
for (const session of ctx.sessions.list()) seed(session)
ctx.on('session/created', (session) => { seed(session) }, { global: true })
ctx.on('session/event', (session, event) => {
const trace = traceFor(session)
if (event.type === 'turn/start') {
trace.openTurn = event.data.turn
return
}
if (event.type === 'turn/end') {
trace.openTurn = null
return
}
if (event.type !== 'compact/start' && event.type !== 'compact/summary' && event.type !== 'compact/end') return
const candidate = staged.get(event)
/* v8 ignore next -- internal/dispatch stages every compaction event */
if (candidate === undefined || candidate.session !== session) return fail('compaction event published without pre-commit validation')
staged.delete(event)
const next = applyCompactionTransition(candidate.transition)
if (next === undefined) traces.delete(session)
else traces.set(session, next)
trace.compaction = applyCompactionTransition(candidate.transition)
}, { global: true })
ctx.on('internal/dispatch', (_mode, eventName, args) => {
if (eventName !== 'session/event') return

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import SessionStore from '@deepseek-ai/dsh-session'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
import * as CompactInvariant from '@deepseek-ai/dsh-compact/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
@@ -22,15 +22,21 @@ const summary = (overrides: Record<string, unknown> = {}) => ({
...overrides,
})
function startTurn(session: ReturnType<Context['sessions']['create']>, turn = 1): void {
session.append('turn/start', { turn, trigger: { kind: 'message', source: { kind: 'user' } } })
}
describe('compaction invariants', () => {
it('accepts successful and failed compaction lifecycles', async () => {
const ctx = await setup()
const success = ctx.sessions.create()
startTurn(success)
success.append('compact/start', { turn: 1 })
success.append('compact/summary', summary())
success.append('compact/end', { turn: 1 })
const failed = ctx.sessions.create()
startTurn(failed, 2)
failed.append('compact/start', { turn: 2 })
failed.append('compact/end', { turn: 2, error: 'provider failed' })
})
@@ -40,13 +46,68 @@ describe('compaction invariants', () => {
await ctx.plugin(SessionStore)
const session = ctx.sessions.create()
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('compact/start', { turn: 3 })
session.append('compact/start', { turn: 1 })
await ctx.plugin(InvariantService)
await ctx.plugin(CompactInvariant)
expect(() => session.append('compact/end', { turn: 3, error: 'resume failed' })).not.toThrow()
expect(() => session.append('compact/end', { turn: 1, error: 'resume failed' })).not.toThrow()
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
})
it('adopts a bare session and ignores unrelated committed events', async () => {
const ctx = await setup()
const session = new Session(SessionId('bare-compaction-session'))
expect(() => {
ctx.emit('session/event', session, {
type: 'turn/start', seq: 0, time: 0,
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
})
ctx.emit('session/event', session, {
type: 'step/start', seq: 1, time: 1, data: { turn: 1, step: 1 },
})
ctx.emit('session/event', session, {
type: 'compact/start', seq: 2, time: 2, data: { turn: 1 },
})
}).not.toThrow()
})
it('rejects compaction outside or for a different open turn', async () => {
const ctx = await setup()
const session = ctx.sessions.create()
expect(() => session.append('compact/start', { turn: 1 })).toThrow(/outside any open turn/)
startTurn(session)
expect(() => session.append('compact/start', { turn: 2 })).toThrow(/but open turn is 1/)
})
it('rejects an unenclosed compaction event when replaying an existing session', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const session = ctx.sessions.create()
startTurn(session)
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
session.append('compact/start', { turn: 1 })
await ctx.plugin(InvariantService)
await expect(ctx.plugin(CompactInvariant).then(() => undefined)).rejects.toThrow(/outside any open turn/)
})
it('rejects an open compaction that crosses into another turn', async () => {
const ctx = await setup()
const summarySession = ctx.sessions.create()
startTurn(summarySession)
summarySession.append('compact/start', { turn: 1 })
summarySession.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
startTurn(summarySession, 2)
expect(() => summarySession.append('compact/summary', summary()))
.toThrow(/belongs to turn 1 but open turn is 2/)
const endSession = ctx.sessions.create()
startTurn(endSession)
endSession.append('compact/start', { turn: 1 })
endSession.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
startTurn(endSession, 2)
expect(() => endSession.append('compact/end', { turn: 1, error: 'late' }))
.toThrow(/names turn 1 but open turn is 2/)
})
it.each([
['summary without start', (session: ReturnType<Context['sessions']['create']>) => {
session.append('compact/summary', summary())
@@ -85,6 +146,8 @@ describe('compaction invariants', () => {
}, /requires one compact\/summary/],
])('rejects %s', async (_name, action, message) => {
const ctx = await setup()
expect(() => { action(ctx.sessions.create()) }).toThrow(message)
const session = ctx.sessions.create()
startTurn(session)
expect(() => { action(session) }).toThrow(message)
})
})