Merge remote-tracking branch 'origin/master' into worktree/session-reference

# Conflicts:
#	docs/capability-seams.md
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	docs/module-graph.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/ui/acp/README.md
#	packages/ui/acp/package.json
#	packages/ui/acp/tsconfig.json
#	packages/ui/tui/package.json
#	packages/ui/tui/src/index.ts
#	packages/ui/tui/tests/tui.spec.ts
#	packages/ui/tui/tsconfig.json
#	pnpm-lock.yaml
#	python/sdk-runtime/package.json
#	scripts/gen-doc-graphs.ts
#	scripts/type-equiv.manifest.json
This commit is contained in:
Yichen Jiang
2026-07-22 10:21:17 +08:00
284 changed files with 11792 additions and 6300 deletions

View File

@@ -1,9 +1,9 @@
# session-query/ — session retrieval capability family
Trusted exact reads and relationship traces over live and durable session logs. The family contains one interface package that owns `ctx.sessionQuery`, logical-corpus precedence, surface classification, bounded event reads, lineage, and direct event relationships.
Trusted exact reads and relationship traces over live and durable session logs. The family contains one interface package that owns `ctx.sessionQuery`, logical-corpus precedence, title folding, surface classification, bounded event reads, lineage, and direct event relationships.
| Package | Role | ctx key |
|---|---|---|
| [`session-query/`](session-query/README.md) | Logical-corpus exact-read and relationship-tracing service | `ctx.sessionQuery` |
| [`session-query/`](session-query/README.md) | Logical-corpus title, event, lineage, and relationship reads | `ctx.sessionQuery` |
The family is independent of compaction: it reads canonical lineage, surface operations, and logged provenance but does not participate in compaction policy or execution. Full-text search remains a proposed SQLite package rather than a speculative provider seam in this interface package.

View File

@@ -5,13 +5,14 @@ Exact session-history retrieval and relationship tracing through `ctx.sessionQue
## Reads
- `listSessions()` reads current persistence metadata, merges live records with live precedence, and returns cloned records in deterministic newest-first order.
- `readTitle(sessionId)` loads one live-preferred or persisted log and folds its latest `session/title` event into a `SessionTitleSnapshot`; it returns `undefined` when the known session has no title.
- `listEvents(sessionId)` loads the live-preferred raw log and classifies each event as `current`, `shadowed`, or `log-only` with the shared `dsh-session` surface fold.
- `readSurface(sessionId)` returns one cloned header, raw-log capture boundary, and the complete folded current surface in model-history order. A live session wins over persistence; compaction is observed before or after its replacement append, never as a synthetic mixture.
- `readEvent(request)` returns a cloned header, the full target event, and a bounded raw-seq window. `before` and `after` default to zero and may not exceed `readWindowMax`.
- `traceSession(sessionId)` reads the corpus once and returns immediate-to-outward ancestors plus deterministic recursive descendant trees. `complete: false` identifies the first missing parent; a target-connected cycle fails with `SESSION_QUERY_INVALID_LINEAGE`.
- `traceEvent(request)` loads the logical log once and returns direct positional replacements and direct logged provenance. `replacementChain` follows positional replacers to the final replacement; provenance links remain non-transitive.
Persistence is optional and may mount or unmount dynamically. Cross-corpus listing and lineage tracing fail with `SESSION_QUERY_PERSISTENCE_FAILED` while mounted persistence is unreadable. An event read or trace targeting a known live session does not consult persistence, so durable backend health cannot make current in-memory history unreadable. Persisted event operations list before loading and reject a metadata mismatch rather than combining inconsistent observations.
Persistence is optional and may mount or unmount dynamically. Cross-corpus listing and lineage tracing fail with `SESSION_QUERY_PERSISTENCE_FAILED` while mounted persistence is unreadable. A title, event read, or trace targeting a known live session does not consult persistence, so durable backend health cannot make current in-memory state unreadable. Persisted title and event operations list before loading and reject a metadata mismatch rather than combining inconsistent observations. `listSessions()` remains lightweight and does not load logs or index titles.
`listEvents()`, `readSurface()`, and `traceEvent()` run the same one-pass `dsh-session` surface fold. A loaded log is valid only when event seqs are zero-based and contiguous, surface markers obey event-type eligibility, provenance arrays are nonempty and duplicate-free, references name earlier events, and each positional replacement names and cites every surface node it removes; every violation fails with `SESSION_QUERY_INVALID_SURFACE`.

View File

@@ -30,6 +30,7 @@
"@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",
"@deepseek-ai/dsh-session-persistence": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
@@ -45,6 +46,7 @@
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-title": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"cordis": "^4.0.0-rc.7"
}

View File

@@ -7,6 +7,8 @@
import { Context, Service } from 'cordis'
import z from 'schemastery'
import type { SessionId } from '@deepseek-ai/dsh-session'
import { foldSessionTitle } from '@deepseek-ai/dsh-session-title'
import type { SessionTitleSnapshot } from '@deepseek-ai/dsh-session-title'
import type {
SessionEventReadRequest,
SessionEventRecord,
@@ -65,6 +67,16 @@ export class SessionQueryService extends Service {
return this._corpus.listSessions()
}
/**
* Fold the latest log-backed title from one live-preferred logical session.
* @param sessionId - live or persisted session id to read.
* @returns latest title snapshot, or `undefined` when the log has no title event.
*/
async readTitle(sessionId: SessionId): Promise<SessionTitleSnapshot | undefined> {
const loaded = await this._corpus.load(sessionId)
return foldSessionTitle(loaded.events)
}
/**
* List lightweight raw-log event records for one logical session.
* @param sessionId - live-preferred session id to read.

View File

@@ -6,6 +6,7 @@ import SessionPersistence from '@deepseek-ai/dsh-session-persistence'
import SessionQueryService, {
type SessionQueryErrorCode,
} from '@deepseek-ai/dsh-session-query'
import { SessionTitleProviderId } from '@deepseek-ai/dsh-session-title'
function header(id: string, createdAt = 1, extra: Partial<SessionHeader> = {}): SessionHeader {
return { version: SESSION_FORMAT_VERSION, id: SessionId(id), createdAt, ...extra }
@@ -85,6 +86,58 @@ function rejectUnknown<T>(reason: unknown): Promise<T> {
}
describe('session-query exact reads', () => {
it('reads the latest title from one live-preferred or persisted log without widening listSessions', async () => {
const persistedHeader = header('persisted-title', 2)
const sharedHeader = header('shared-title', 3)
TestPersistence.reset([
{
meta: persistedHeader,
events: [{
type: 'session/title',
seq: 0,
time: 20,
data: {
title: 'Persisted title',
messageSeqs: [4],
source: { kind: 'fallback' },
},
}],
},
{
meta: sharedHeader,
events: [{
type: 'session/title',
seq: 0,
time: 30,
data: {
title: 'Stale durable title',
messageSeqs: [1],
source: { kind: 'fallback' },
},
}],
},
])
const ctx = await liveContext()
const shared = ctx.sessions.create(sharedHeader.id, { meta: { createdAt: 3 } })
shared.append('session/title', {
title: 'Live title',
messageSeqs: [7],
source: {
kind: 'provider',
provider: SessionTitleProviderId('query-test'),
},
})
await ctx.plugin(TestPersistence)
await expect(ctx.sessionQuery.readTitle(persistedHeader.id)).resolves.toMatchObject({
title: 'Persisted title', eventSeq: 0, updatedAt: 20,
})
await expect(ctx.sessionQuery.readTitle(shared.id)).resolves.toMatchObject({
title: 'Live title', eventSeq: 0,
})
expect(Object.keys((await ctx.sessionQuery.listSessions())[0]!)).toEqual(['header', 'live', 'persisted'])
})
it('lists live sessions deterministically and returns detached headers', async () => {
const ctx = await liveContext()
const older = ctx.sessions.create(SessionId('older'), { meta: { createdAt: 1 } })

View File

@@ -23,6 +23,9 @@
{
"path": "../../core/session"
},
{
"path": "../../session-title/session-title"
},
{
"path": "../../session-persistence/session-persistence"
},