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

@@ -4,7 +4,7 @@
## Public API
- `listCandidates(agent, query?, limit?)` lists sessions other than `agent.id`, filters case-insensitively by id or cwd, and ranks same-cwd, cwd-less, then other-cwd records while preserving `listSessions()` creation order within each group. It searches no title or message body.
- `listCandidates(agent, query?, limit?)` lists sessions other than `agent.id`, filters case-insensitively by id or cwd, and ranks same-cwd, cwd-less, then other-cwd records while preserving `listSessions()` creation order within each group. Each selected candidate uses its latest log-backed title as the mention label and falls back to the session id; titles and message bodies are not searched.
- `prepare(agent, content, references, signal?)` preserves first-mention order, deduplicates ids, rejects self-reference and more than the configured distinct-source limit, reads every source in parallel, and returns detached content plus zero or one aggregated `HookContext`. Any invalid reference, failed read, cancellation, or budget failure rejects before the host calls `send()` or `steer()`.
- `encodeSessionReferenceUri()` and `decodeSessionReferenceUri()` implement `dsh-session:<base64url(JSON.stringify(sessionId))>` so every JavaScript string id round-trips exactly. `formatSessionReferenceMention()` emits `@[label](uri)`, and `parseSessionReferenceText()` replaces Markdown mentions or bare canonical URIs with readable `@label` text while returning structured references. Explicit Markdown mentions reject every malformed URI; bare text is considered a reference only when a non-empty base64url-shaped payload follows the scheme, and a matching noncanonical candidate still fails. Empty or punctuation-only scheme mentions remain ordinary discussion text.
@@ -42,7 +42,7 @@ Snapshot context is append-only at the target message boundary and preserves ear
## Known Limitations and Deferred Work
- **No full-text discovery** — candidates use session id and cwd only. SQLite FTS or title metadata may replace discovery later without changing URI, snapshot, or persistence contracts.
- **No title or full-text discovery** — candidates filter by session id and cwd only, although selected rows display the latest title. SQLite FTS may replace discovery later without changing URI, snapshot, or persistence contracts.
- **Trusted caller boundary** — the service assumes its host is authorized to read every session exposed by `ctx.sessionQuery`; it is not a model-facing search tool.
- **Text projection only** — non-text user and assistant blocks are not propagated across sessions.
- **No live link** — references are snapshots, not forks, resumes, subscriptions, or source-session mutations.

View File

@@ -100,12 +100,12 @@ export class SessionReferenceService extends Service {
}
/**
* List metadata-only reference candidates, ranked by working-directory affinity.
* List reference candidates, ranked by working-directory affinity.
* @param agent - target agent; self is excluded and its cwd drives ranking.
* @param query - optional case-insensitive session-id/cwd substring.
* @param limit - optional positive result cap.
* @param signal - optional cancellation boundary for host autocomplete teardown.
* @returns candidate records in stable source creation order within each rank.
* @returns candidates labeled by latest title or, when absent, session id.
*/
async listCandidates(
agent: Agent,
@@ -130,9 +130,13 @@ export class SessionReferenceService extends Service {
.sort((a, b) => candidateRank(a.record.header.cwd, targetCwd) - candidateRank(b.record.header.cwd, targetCwd)
|| a.index - b.index)
.slice(0, limit)
return records.map(({ record }) => ({
const titles = await settleWithCancellation(
Promise.all(records.map(({ record }) => this.ctx.sessionQuery.readTitle(record.header.id))),
signal,
)
return records.map(({ record }, index) => ({
sessionId: record.header.id,
label: record.header.id,
label: titles[index]?.title ?? record.header.id,
...record.header.cwd === undefined ? {} : { cwd: record.header.cwd },
createdAt: record.header.createdAt,
}))

View File

@@ -16,7 +16,7 @@ export interface SessionReferenceInput {
export interface SessionReferenceCandidate {
/** Opaque source session identity. */
sessionId: SessionId
/** Default display label. */
/** Latest log-backed title, falling back to the opaque session id. */
label: string
/** Source session working directory, when recorded. */
cwd?: string

View File

@@ -177,10 +177,15 @@ describe('session reference discovery and preparation', () => {
ctx.sessions.create(SessionId('other'), { meta: { cwd: '/else', createdAt: 40 } })
ctx.sessions.create(SessionId('none'), { meta: { createdAt: 30 } })
ctx.sessions.create(SessionId('same'), { meta: { cwd: '/same', createdAt: 20 } })
ctx.sessions.create(SessionId('same-later'), { meta: { cwd: '/same', createdAt: 25 } })
const sameLater = ctx.sessions.create(SessionId('same-later'), { meta: { cwd: '/same', createdAt: 25 } })
sameLater.append('session/title', {
title: 'Latest title',
messageSeqs: [],
source: { kind: 'fallback' },
})
await expect(ctx.sessionReferences.listCandidates(fakeAgent(target))).resolves.toEqual([
{ sessionId: SessionId('same-later'), label: 'same-later', cwd: '/same', createdAt: 25 },
{ sessionId: SessionId('same-later'), label: 'Latest title', cwd: '/same', createdAt: 25 },
{ sessionId: SessionId('same'), label: 'same', cwd: '/same', createdAt: 20 },
{ sessionId: SessionId('none'), label: 'none', createdAt: 30 },
{ sessionId: SessionId('other'), label: 'other', cwd: '/else', createdAt: 40 },