Merge master into codex/session-title

This commit is contained in:
Tianyi Cui
2026-07-21 21:11:32 +08:00
650 changed files with 21729 additions and 3375 deletions

View File

@@ -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-title": "^0.0.1",
@@ -37,6 +43,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-title": "workspace:^",

View 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 */

View File

@@ -154,6 +154,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' } },
@@ -170,13 +172,14 @@ 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'])
})
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',
@@ -185,14 +188,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'))
@@ -214,6 +217,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' } },
@@ -223,7 +227,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.readEvent({ sessionId: durable.id, seq: 0 }))
@@ -242,6 +246,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' } },
@@ -251,8 +256,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'))
})
@@ -281,19 +286,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,

View File

@@ -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])

View File

@@ -28,6 +28,9 @@
},
{
"path": "../../session-persistence/session-persistence"
},
{
"path": "../../support/invariants"
}
]
}