Merge remote-tracking branch 'origin/master' into worktree/session-reference
# Conflicts: # .agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/event-producer-consumer.md # docs/module-graph.md # packages/compact/compact-basic/src/region.ts # packages/compact/compact/README.md # packages/compact/compact/tests/compact.spec.ts # packages/examples/acp-demo/package.json # packages/ui/tui/README.md # packages/ui/tui/package.json # packages/ui/tui/src/index.ts # packages/ui/tui/tests/harness.ts # packages/ui/tui/tests/tui.spec.ts
This commit is contained in:
@@ -11,17 +11,23 @@
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./invariant": {
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"lib/types/**/*.d.ts.map",
|
||||
"src"
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session-persistence": "^0.0.1",
|
||||
@@ -36,6 +42,7 @@
|
||||
"schemastery": "^3.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-session-persistence": "workspace:^",
|
||||
|
||||
30
packages/session-query/session-query/src/invariant.ts
Normal file
30
packages/session-query/session-query/src/invariant.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-session-query`.
|
||||
* @module @deepseek-ai/dsh-session-query/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-session-query'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'session-query-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: query results are immutable per-call projections whose lineage and event
|
||||
* relations are validated while they are built; the service retains no observable result state.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
@@ -101,6 +101,8 @@ describe('session-query exact reads', () => {
|
||||
it('classifies current, shadowed, and raw-log-only events through foldSurface', async () => {
|
||||
const ctx = await liveContext()
|
||||
const session = ctx.sessions.create(SessionId('surface'))
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
const first = session.append(
|
||||
'user/message',
|
||||
{ content: [{ type: 'text', text: 'first' }], source: { kind: 'user' } },
|
||||
@@ -117,7 +119,7 @@ describe('session-query exact reads', () => {
|
||||
{ surfaceOp: { op: 'replace', start: first.seq, end: first.seq }, sourceEventSeqs: [first.seq] },
|
||||
)
|
||||
|
||||
expect((await ctx.sessionQuery.listEvents(session.id)).map(record => record.surface))
|
||||
expect((await ctx.sessionQuery.listEvents(session.id)).slice(2).map(record => record.surface))
|
||||
.toEqual(['shadowed', 'log-only', 'current'])
|
||||
})
|
||||
|
||||
@@ -182,6 +184,7 @@ describe('session-query exact reads', () => {
|
||||
it('returns a bounded detached raw-event window and validates the request', async () => {
|
||||
const ctx = await liveContext({ readWindowMax: 1 })
|
||||
const session = ctx.sessions.create(SessionId('window'), { meta: { cwd: '/work' } })
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
for (const text of ['one', 'two', 'three']) {
|
||||
session.append(
|
||||
'user/message',
|
||||
@@ -190,14 +193,14 @@ describe('session-query exact reads', () => {
|
||||
)
|
||||
}
|
||||
|
||||
const result = await ctx.sessionQuery.readEvent({ sessionId: session.id, seq: 1, before: 1, after: 1 })
|
||||
expect([result.startSeq, result.endSeq, result.target.seq]).toEqual([0, 2, 1])
|
||||
const result = await ctx.sessionQuery.readEvent({ sessionId: session.id, seq: 2, before: 1, after: 1 })
|
||||
expect([result.startSeq, result.endSeq, result.target.seq]).toEqual([1, 3, 2])
|
||||
expect(result.session).toEqual(session.header)
|
||||
Object.assign(result.session, { createdAt: -1 })
|
||||
if (result.events[0]?.type !== 'user/message') throw new Error('expected user message')
|
||||
result.events[0].data.content = []
|
||||
expect(session.header.createdAt).not.toBe(-1)
|
||||
expect(session.events[0]?.type === 'user/message' && session.events[0].data.content).toHaveLength(1)
|
||||
expect(session.events[1]?.type === 'user/message' && session.events[1].data.content).toHaveLength(1)
|
||||
|
||||
await expect(ctx.sessionQuery.readEvent({ sessionId: session.id, seq: 9 }))
|
||||
.rejects.toThrow(expectCode('SESSION_QUERY_EVENT_NOT_FOUND'))
|
||||
@@ -219,6 +222,7 @@ describe('session-query exact reads', () => {
|
||||
])
|
||||
const ctx = await liveContext()
|
||||
const live = ctx.sessions.create(shared.id, { meta: { createdAt: 3, cwd: '/same' } })
|
||||
live.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
live.append(
|
||||
'user/message',
|
||||
{ content: [{ type: 'text', text: 'live' }], source: { kind: 'user' } },
|
||||
@@ -228,7 +232,7 @@ describe('session-query exact reads', () => {
|
||||
|
||||
expect((await ctx.sessionQuery.listSessions()).map(record => [record.header.id, record.live, record.persisted]))
|
||||
.toEqual([[shared.id, true, true], [durable.id, false, true]])
|
||||
const liveRead = await ctx.sessionQuery.readEvent({ sessionId: shared.id, seq: 0 })
|
||||
const liveRead = await ctx.sessionQuery.readEvent({ sessionId: shared.id, seq: 1 })
|
||||
expect(liveRead.target.type === 'user/message' && liveRead.target.data.content[0])
|
||||
.toMatchObject({ text: 'live' })
|
||||
await expect(ctx.sessionQuery.readSurface(shared.id)).resolves.toMatchObject({
|
||||
@@ -254,6 +258,7 @@ describe('session-query exact reads', () => {
|
||||
TestPersistence.reset()
|
||||
const ctx = await liveContext()
|
||||
const live = ctx.sessions.create(SessionId('live'))
|
||||
live.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
live.append(
|
||||
'user/message',
|
||||
{ content: [{ type: 'text', text: 'available' }], source: { kind: 'user' } },
|
||||
@@ -263,8 +268,8 @@ describe('session-query exact reads', () => {
|
||||
TestPersistence.listFailure = new Error('list unavailable')
|
||||
TestPersistence.loadFailure = new Error('load unavailable')
|
||||
|
||||
await expect(ctx.sessionQuery.listEvents(live.id)).resolves.toHaveLength(1)
|
||||
await expect(ctx.sessionQuery.readEvent({ sessionId: live.id, seq: 0 })).resolves.toMatchObject({ target: { seq: 0 } })
|
||||
await expect(ctx.sessionQuery.listEvents(live.id)).resolves.toHaveLength(2)
|
||||
await expect(ctx.sessionQuery.readEvent({ sessionId: live.id, seq: 1 })).resolves.toMatchObject({ target: { seq: 1 } })
|
||||
await expect(ctx.sessionQuery.listSessions()).rejects.toThrow(expectCode('SESSION_QUERY_PERSISTENCE_FAILED'))
|
||||
await expect(ctx.sessionQuery.listEvents(SessionId('durable'))).rejects.toThrow(expectCode('SESSION_QUERY_PERSISTENCE_FAILED'))
|
||||
})
|
||||
@@ -293,19 +298,8 @@ describe('session-query exact reads', () => {
|
||||
.rejects.toThrow(expectCode('SESSION_QUERY_SOURCE_CONFLICT'))
|
||||
})
|
||||
|
||||
it('turns malformed surfaces and direct invalid config into typed errors', async () => {
|
||||
it('turns persisted malformed surfaces and direct invalid config into typed errors', async () => {
|
||||
const ctx = await liveContext()
|
||||
const session = ctx.sessions.create(SessionId('bad-surface'))
|
||||
;(session as unknown as { log: SessionEvent[] }).log.push({
|
||||
type: 'assistant/message',
|
||||
seq: 0,
|
||||
time: 1,
|
||||
data: { turn: 1, step: 1, content: [], provenance: { provider: 'mock', model: 'mock' } },
|
||||
surfaceOp: { op: 'replace', start: 9, end: 9 },
|
||||
})
|
||||
await expect(ctx.sessionQuery.listEvents(session.id))
|
||||
.rejects.toThrow(expectCode('SESSION_QUERY_INVALID_SURFACE'))
|
||||
|
||||
const persisted = header('bad-persisted-surface')
|
||||
TestPersistence.reset([{
|
||||
meta: persisted,
|
||||
|
||||
@@ -89,6 +89,8 @@ function expectCode(code: SessionQueryErrorCode): Error {
|
||||
}
|
||||
|
||||
function appendTraceEvents(session: Session): void {
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
session.append('assistant/chunk', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
@@ -97,22 +99,24 @@ function appendTraceEvents(session: Session): void {
|
||||
session.append(
|
||||
'user/message',
|
||||
{ content: [{ type: 'text', text: 'original' }], source: { kind: 'user' } },
|
||||
{ surfaceOp: 'append', sourceEventSeqs: [0] },
|
||||
{ surfaceOp: 'append', sourceEventSeqs: [2] },
|
||||
)
|
||||
session.append(
|
||||
'assistant/message',
|
||||
{ provenance: { provider: 'mock', model: 'mock' }, turn: 1, step: 1, content: [{ type: 'text', text: 'summary one' }] },
|
||||
{ surfaceOp: { op: 'replace', start: 1, end: 1 }, sourceEventSeqs: [1, 0] },
|
||||
{ surfaceOp: { op: 'replace', start: 3, end: 3 }, sourceEventSeqs: [3, 2] },
|
||||
)
|
||||
session.append(
|
||||
'context/message',
|
||||
{ content: [{ type: 'text', text: 'context' }], source: { kind: 'plugin', plugin: 'test' } },
|
||||
{ surfaceOp: 'append' },
|
||||
)
|
||||
session.append('step/end', { turn: 1, step: 1 })
|
||||
session.append('step/start', { turn: 1, step: 2 })
|
||||
session.append(
|
||||
'assistant/message',
|
||||
{ provenance: { provider: 'mock', model: 'mock' }, turn: 1, step: 2, content: [{ type: 'text', text: 'summary two' }] },
|
||||
{ surfaceOp: { op: 'replace', start: 2, end: 2 }, sourceEventSeqs: [0, 2] },
|
||||
{ surfaceOp: { op: 'replace', start: 4, end: 4 }, sourceEventSeqs: [2, 4] },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -235,40 +239,40 @@ describe('session event tracing', () => {
|
||||
const session = ctx.sessions.create(SessionId('trace'))
|
||||
appendTraceEvents(session)
|
||||
|
||||
const original = await ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 1 })
|
||||
const original = await ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 3 })
|
||||
expect(original.target).toMatchObject({
|
||||
sessionId: session.id,
|
||||
seq: 1,
|
||||
seq: 3,
|
||||
type: 'user/message',
|
||||
surface: 'shadowed',
|
||||
})
|
||||
expect(original).toMatchObject({
|
||||
replacedBy: 2,
|
||||
replacementChain: [2, 4],
|
||||
replacedBy: 4,
|
||||
replacementChain: [4, 8],
|
||||
replacedEventSeqs: [],
|
||||
sourceEventSeqs: [0],
|
||||
derivedEventSeqs: [2],
|
||||
sourceEventSeqs: [2],
|
||||
derivedEventSeqs: [4],
|
||||
})
|
||||
await expect(ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 2 }))
|
||||
await expect(ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 4 }))
|
||||
.resolves.toMatchObject({
|
||||
replacedBy: 4,
|
||||
replacementChain: [4],
|
||||
replacedEventSeqs: [1],
|
||||
sourceEventSeqs: [1, 0],
|
||||
derivedEventSeqs: [4],
|
||||
replacedBy: 8,
|
||||
replacementChain: [8],
|
||||
replacedEventSeqs: [3],
|
||||
sourceEventSeqs: [3, 2],
|
||||
derivedEventSeqs: [8],
|
||||
})
|
||||
await expect(ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 0 }))
|
||||
await expect(ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 2 }))
|
||||
.resolves.toMatchObject({
|
||||
target: { surface: 'log-only' },
|
||||
replacementChain: [],
|
||||
sourceEventSeqs: [],
|
||||
derivedEventSeqs: [1, 2, 4],
|
||||
derivedEventSeqs: [3, 4, 8],
|
||||
})
|
||||
await expect(ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 4 }))
|
||||
await expect(ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 8 }))
|
||||
.resolves.toMatchObject({
|
||||
replacementChain: [],
|
||||
replacedEventSeqs: [2],
|
||||
sourceEventSeqs: [0, 2],
|
||||
replacedEventSeqs: [4],
|
||||
sourceEventSeqs: [2, 4],
|
||||
derivedEventSeqs: [],
|
||||
})
|
||||
})
|
||||
@@ -278,18 +282,18 @@ describe('session event tracing', () => {
|
||||
const session = ctx.sessions.create(SessionId('detached'))
|
||||
appendTraceEvents(session)
|
||||
|
||||
const first = await ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 2 })
|
||||
const first = await ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 4 })
|
||||
first.target.time = -1
|
||||
first.replacementChain.push(99)
|
||||
first.replacedEventSeqs.push(99)
|
||||
first.sourceEventSeqs.push(99)
|
||||
first.derivedEventSeqs.push(99)
|
||||
const repeated = await ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 2 })
|
||||
const repeated = await ctx.sessionQuery.traceEvent({ sessionId: session.id, seq: 4 })
|
||||
expect(repeated.target.time).not.toBe(-1)
|
||||
expect(repeated.replacementChain).toEqual([4])
|
||||
expect(repeated.replacedEventSeqs).toEqual([1])
|
||||
expect(repeated.sourceEventSeqs).toEqual([1, 0])
|
||||
expect(repeated.derivedEventSeqs).toEqual([4])
|
||||
expect(repeated.replacementChain).toEqual([8])
|
||||
expect(repeated.replacedEventSeqs).toEqual([3])
|
||||
expect(repeated.sourceEventSeqs).toEqual([3, 2])
|
||||
expect(repeated.derivedEventSeqs).toEqual([8])
|
||||
})
|
||||
|
||||
it('loads persisted logs once, prefers live logs, and preserves failures and conflicts', async () => {
|
||||
@@ -303,6 +307,7 @@ describe('session event tracing', () => {
|
||||
expect([TracePersistence.listCalls, TracePersistence.loadCalls]).toEqual([1, 1])
|
||||
|
||||
const live = ctx.sessions.create(durable.id, { meta: { createdAt: 1, cwd: '/same' } })
|
||||
live.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
live.append(
|
||||
'context/message',
|
||||
{ content: [{ type: 'text', text: 'live' }], source: { kind: 'plugin', plugin: 'test' } },
|
||||
@@ -310,7 +315,7 @@ describe('session event tracing', () => {
|
||||
)
|
||||
TracePersistence.listFailure = new Error('list unavailable')
|
||||
TracePersistence.loadFailure = new Error('load unavailable')
|
||||
await expect(ctx.sessionQuery.traceEvent({ sessionId: durable.id, seq: 0 }))
|
||||
await expect(ctx.sessionQuery.traceEvent({ sessionId: durable.id, seq: 1 }))
|
||||
.resolves.toMatchObject({ target: { type: 'context/message' } })
|
||||
expect([TracePersistence.listCalls, TracePersistence.loadCalls]).toEqual([1, 1])
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../session-persistence/session-persistence"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user