Merge remote-tracking branch 'origin/master' into xtr/identified-immutable-messages
# Conflicts: # .agents/notes/implemented/architecture/2026-07-24-separate-context-injection-from-turn-execution.i18n.yaml # docs/cordis-catalog/services.md # docs/core-data-structures/core.i18n.yaml # docs/core-data-structures/session.i18n.yaml # docs/event-producer-consumer.md # docs/persistence-catalog.md # packages/core/session/README.i18n.yaml # packages/session-title/session-title/tests/persistence.spec.ts
This commit is contained in:
@@ -11,9 +11,10 @@ export const name = 'plan-mode-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/** Validate one `plan/mode` payload before it reaches the durable log. */
|
||||
function validateEvent(event: SessionEvent, fail: InvariantFailure): void {
|
||||
/** Validate one `plan/mode` event before it reaches the durable log. */
|
||||
function validateEvent(openTurn: number | null, event: SessionEvent, fail: InvariantFailure): void {
|
||||
if (event.type !== 'plan/mode') return
|
||||
if (openTurn === null) fail('plan/mode appended outside any open turn')
|
||||
const active = (event.data as { active?: unknown }).active
|
||||
if (typeof active !== 'boolean') {
|
||||
fail(`plan/mode carries invalid active state ${JSON.stringify(active)}; expected a boolean`)
|
||||
@@ -23,13 +24,30 @@ function validateEvent(event: SessionEvent, fail: InvariantFailure): void {
|
||||
/* jscpd:ignore-start -- package companions share replay and dispatch plumbing */
|
||||
/** Install validation for loaded and newly appended plan-mode state. */
|
||||
const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
|
||||
for (const session of ctx.sessions.list()) {
|
||||
for (const event of session.events) validateEvent(event, fail)
|
||||
const traces = new WeakMap<Session, number | null>()
|
||||
const seed = (session: Session): number | null => {
|
||||
let openTurn: number | null = null
|
||||
traces.set(session, openTurn)
|
||||
for (const event of session.events) {
|
||||
if (event.type === 'turn/start') openTurn = event.data.turn
|
||||
else if (event.type === 'turn/end') openTurn = null
|
||||
validateEvent(openTurn, event, fail)
|
||||
traces.set(session, openTurn)
|
||||
}
|
||||
return openTurn
|
||||
}
|
||||
const traceFor = (session: Session): number | null => 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) => {
|
||||
if (event.type === 'turn/start') traces.set(session, event.data.turn)
|
||||
else if (event.type === 'turn/end') traces.set(session, null)
|
||||
}, { global: true })
|
||||
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
||||
if (eventName !== 'session/event') return
|
||||
const event = (args as [Session, SessionEvent])[1]
|
||||
validateEvent(event, fail)
|
||||
const [session, event] = args as [Session, SessionEvent]
|
||||
validateEvent(traceFor(session), event, fail)
|
||||
}, { global: true })
|
||||
}, { inject: ['sessions'] })
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import SessionStore, { type Session, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import SessionStore, { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import * as PlanModeInvariant from '@deepseek-ai/dsh-plan-mode/invariant'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
@@ -16,24 +16,46 @@ function event(active: unknown): SessionEvent {
|
||||
return { type: 'plan/mode', seq: 0, time: 0, data: { active } } as SessionEvent
|
||||
}
|
||||
|
||||
function emitTurnStart(ctx: Context, session: Session): void {
|
||||
ctx.emit('session/event', session, {
|
||||
type: 'turn/start', seq: 0, time: 0,
|
||||
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
|
||||
})
|
||||
}
|
||||
|
||||
describe('plan-mode stream invariants', () => {
|
||||
it('accepts either boolean state', async () => {
|
||||
const ctx = await setup()
|
||||
expect(() => { ctx.emit('session/event', {} as Session, event(true)) }).not.toThrow()
|
||||
expect(() => { ctx.emit('session/event', {} as Session, event(false)) }).not.toThrow()
|
||||
const session = new Session(SessionId('plan-state'))
|
||||
emitTurnStart(ctx, session)
|
||||
expect(() => { ctx.emit('session/event', session, event(true)) }).not.toThrow()
|
||||
expect(() => { ctx.emit('session/event', session, event(false)) }).not.toThrow()
|
||||
ctx.emit('session/event', session, {
|
||||
type: 'turn/end', seq: 3, time: 3,
|
||||
data: { turn: 1, reason: { kind: 'completed' } },
|
||||
})
|
||||
})
|
||||
|
||||
it.each([42, 'plan', undefined])('rejects invalid durable plan state %j', async (active) => {
|
||||
const ctx = await setup()
|
||||
expect(() => { ctx.emit('session/event', {} as Session, event(active)) })
|
||||
const session = new Session(SessionId(`invalid-${String(active)}`))
|
||||
emitTurnStart(ctx, session)
|
||||
expect(() => { ctx.emit('session/event', session, event(active)) })
|
||||
.toThrow(/expected a boolean/)
|
||||
})
|
||||
|
||||
it('rejects plan state outside any open turn', async () => {
|
||||
const ctx = await setup()
|
||||
expect(() => ctx.sessions.create().append('plan/mode', { active: true }))
|
||||
.toThrow(/outside any open turn/)
|
||||
})
|
||||
|
||||
it('ignores unrelated dispatches and session events', async () => {
|
||||
const ctx = await setup()
|
||||
const session = new Session(SessionId('unrelated'))
|
||||
expect(() => {
|
||||
ctx.emit('tools/change')
|
||||
ctx.emit('session/event', {} as Session, {
|
||||
ctx.emit('session/event', session, {
|
||||
type: 'turn/start', seq: 0, time: 0, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
|
||||
})
|
||||
}).not.toThrow()
|
||||
@@ -42,9 +64,33 @@ describe('plan-mode stream invariants', () => {
|
||||
it('rejects invalid existing state on late registration', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
ctx.sessions.create().append('plan/mode', { active: 'plan' as unknown as boolean })
|
||||
const session = ctx.sessions.create()
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('plan/mode', { active: 'plan' as unknown as boolean })
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
await ctx.plugin(InvariantService, { enabled: true })
|
||||
|
||||
await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).rejects.toThrow(/expected a boolean/)
|
||||
})
|
||||
|
||||
it('replays enclosed existing plan state through its closing boundary', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const session = ctx.sessions.create()
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('plan/mode', { active: true })
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
await ctx.plugin(InvariantService, { enabled: true })
|
||||
|
||||
await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects unenclosed existing plan state on late registration', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
ctx.sessions.create().append('plan/mode', { active: true })
|
||||
await ctx.plugin(InvariantService, { enabled: true })
|
||||
|
||||
await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).rejects.toThrow(/outside any open turn/)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user