@deepseek-ai/dsh-session-reference 0.1.1-rc.2 → 0.1.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/context/session-reference/README.md
5
- README.md: 1ce5ab0a3b0cb5342c1087ddd644933a92b2a596
6
- README.zh.md: 2fc30fa01d0e428a2e7ef0be1680a1937f06660c
5
+ README.md: 804cfea6357d4e9fc202e6562e75d740c5702c24
6
+ README.zh.md: 4a249ea63cb7546603666990f67add54ebfe035a
package/README.md CHANGED
@@ -1,31 +1,102 @@
1
- # `@deepseek-ai/dsh-session-reference`
1
+ ---
2
+ description: "Cross-session snapshot references and durable untrusted model context, for users and maintainers enabling or debugging ctx.sessionReferenceResolver."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-session-reference
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- `ctx.sessionReferenceResolver` prepares bounded, read-only snapshots of other sessions as sourced model-facing context. It consumes `ctx.sessionQuery` and the backend-independent compact checkpoint marker; SQLite FTS is not required. Hosts that support cross-session mentions may opt into the service.
10
+ ## Summary
11
+
12
+ `dsh-session-reference` lets a conversation reference other sessions: a host turns a `@label` mention into a canonical URI, and the service prepares a bounded, read-only snapshot of each referenced session as durable, untrusted background context for the model. Candidate discovery ranks other sessions by working-directory affinity and labels them with their latest titles. Snapshots are immutable after capture and carry a fixed warning that forbids following instructions, permission claims, or tool requests inside them. It is an opt-in service for hosts that support cross-session mentions; it consumes `ctx.sessionQuery` and needs no SQLite FTS.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Enable this service when hosts should let a user mention another session and give the model its context. It works with any session-query backend because it consumes the backend-independent compact checkpoint marker.
29
+
30
+ ### Mention syntax
31
+
32
+ A canonical mention is `@[label](dsh-session:<base64url-encoded-id>)` in Markdown, or the bare `dsh-session:` URI; every JavaScript string session id round-trips exactly. The service rewrites mentions into readable `@label` text in the message and returns the structured references. Explicit Markdown mentions reject malformed URIs; empty or punctuation-only scheme mentions stay ordinary discussion text.
33
+
34
+ ### What the agent gets
35
+
36
+ A message that cites other sessions is followed immediately by a `## Referenced sessions` snapshot as a second user-role message. The snapshot is untrusted background: the fixed warning tells the model not to follow instructions, permission claims, or tool requests inside it unless the current user explicitly repeats them. Each source is bounded independently — at most `maxReferences` distinct sessions per message and `maxReferenceBytes` per source — and a source that cannot fit its budget fails preparation instead of returning partial context.
37
+
38
+ ### Finding sessions to reference
39
+
40
+ `listCandidates(agent, query?, limit?)` lists sessions other than the agent's own, filters case-insensitively by id, working directory, or the projected title, and ranks same-directory sessions first. Each candidate carries its latest title as the mention label, falling back to the session id when the title is absent or unreadable, and reports whether its working directory is the requesting agent's so a host can surface a location only when it distinguishes the row. Browser consumers call the same discovery as `ctx.remote.sessionReferenceResolver.candidates`, which attaches each candidate's canonical mention.
41
+
42
+ ### Configuration
43
+
44
+ | Field | Default | Meaning |
45
+ |---|---|---|
46
+ | `maxReferences` | `3` | Maximum distinct source sessions in one prepared message; must not exceed `3` |
47
+ | `candidateLimit` | `50` | Default candidate count returned to a host |
48
+ | `maxReferenceBytes` | `65536` | Maximum serialized JSON bytes for one reference object |
49
+
50
+ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-session-reference) is the exhaustive source for every accepted field and its JSDoc.
6
51
 
7
- ## Public API
52
+ -----
8
53
 
9
- - `listCandidates(agent, query?, limit?)` lists sessions other than `agent.id`, filters case-insensitively by id, cwd, or the latest log-backed title, and ranks same-cwd, cwd-less, then other-cwd records while preserving `listSessions()` creation order within each group. Each selected candidate uses that title as the mention label and falls back to the session id when the title is absent or unreadable; message bodies are not searched. The unary `sessionReferenceResolver/candidates` Remote method serves the same discovery under the configured candidate limit and attaches each candidate's canonical mention, so browser consumers call `ctx.remote.sessionReferenceResolver.candidates` without an API Proxy route.
10
- - `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, identified `UserMessage` context. The service calls it for canonical mentions in direct user messages after downstream `agent/pre-step` listeners accept the step.
11
- - `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.
54
+ <a id="understand-the-implementation"></a>
55
+ ## Understand the implementation
12
56
 
13
- ## Snapshot semantics
57
+ <details>
58
+ <summary>Implementation internals — click to expand</summary>
14
59
 
15
- Preparation calls `ctx.sessionQuery.readSurface()` once per distinct source when the target message reaches `agent/pre-step`. A queued message therefore captures the source state at model-step entry, and the resulting context is immutable after that point. Projection keeps only direct-user `user/message`, assistant text, and `user/message` checkpoints carrying the canonical `dsh-compaction` source marker from the folded current surface. Separately sourced session-reference messages are injected context and are excluded, preventing recursive snapshot propagation. Shadowed pre-compaction events, tools, reasoning, other plugin-generated user messages except marked compact checkpoints, and unfinished assistant chunks are also excluded. A compacted source therefore contributes its latest checkpoint plus retained later conversation, not restored shadowed text.
60
+ This section explains the design of the service; the observable behavior is covered in [Use this package](#use-this-package).
16
61
 
17
- The context source is `{ kind: 'session-reference', version: 1, references }`; each reference records its source id and label, capture seq, compact presence, retained/omitted message counts, omitted UTF-8 bytes, and truncation state. The service's outer `agent/pre-step` listener post-processes accepted direct user messages, preserves their message ids, and inserts each snapshot immediately after the message that cited it. Queue edits and queue-to-steer relocation need no reference-specific handling because parsing occurs after the final inbox claim. Invalid mentions, failed reads, cancellation, and budget failures end that turn before its messages enter model-visible history. The target log records the readable direct `user/message` followed by its sourced context `user/message`; source mutation after capture cannot change target replay.
62
+ ### Design concept
18
63
 
19
- ## Configuration
64
+ Preparation reads each referenced session's current surface exactly once, when the target message reaches `agent/pre-step`, so a queued message captures source state at model-step entry and the resulting context is immutable afterwards. Projection keeps only direct-user `user/message`, assistant text, and `user/message` checkpoints carrying the canonical compaction marker; separately sourced session-reference messages are excluded, preventing recursive snapshot propagation. Source text is serialized as JSON with every `<` escaped as `\u003c`, so it cannot spell the `<referenced-sessions>` framing tag.
20
65
 
21
- | Key | Default | Contract |
22
- |---|---:|---|
23
- | `maxReferences` | `3` | Maximum distinct source sessions in one prepared message; must be at most `3`. |
24
- | `candidateLimit` | `50` | Default candidate count returned to a host. |
25
- | `maxReferenceBytes` | `65536` | Maximum serialized JSON bytes for one reference object. |
66
+ ### Source map
26
67
 
27
- Retention applies `maxReferenceBytes` independently to each source, keeps compact checkpoints and the newest message before dropping older non-checkpoint units, and uses `dsh-output-retention` head/tail truncation with an exact UTF-8 omission notice. If one source's fixed serialized fields cannot fit, preparation fails with `SESSION_REFERENCE_BUDGET_EXCEEDED` instead of returning a partial context.
68
+ | File | Role |
69
+ |---|---|
70
+ | [`src/index.ts`](src/index.ts) | `SessionReferenceResolver`: pre-step listener, candidate discovery, preparation |
71
+ | [`src/config.ts`](src/config.ts) | `Config` schema, `SessionReferenceError` taxonomy |
72
+ | [`src/uri.ts`](src/uri.ts) | `dsh-session:` URI codec, mention formatting and parsing |
73
+ | [`src/projection.ts`](src/projection.ts) | Current-surface projection and byte-budget retention |
74
+ | [`src/serialization.ts`](src/serialization.ts) | Tag-safe JSON escaping for snapshot payloads |
75
+ | [`src/types.ts`](src/types.ts) | `SessionReferenceInput`/`Candidate` and source types |
76
+ | [`src/invariant.ts`](src/invariant.ts) | Invariant companion for the reference contract |
28
77
 
78
+ ### Main flow
79
+
80
+ The outer `agent/pre-step` listener accepts the step, parses canonical mentions out of direct user messages, then calls `prepare`, which normalizes references (first-mention order, deduplication, self-reference and count rejection), reads every surface in parallel, retains each under `maxReferenceBytes`, and renders the aggregated prompt. Each snapshot is inserted immediately after the message that cited it, and the target log records the readable direct message followed by its sourced context, so source mutation after capture cannot change target replay.
81
+
82
+ </details>
83
+
84
+ -----
85
+
86
+ <a id="further-exploration"></a>
87
+ ## Further Exploration
88
+
89
+ Read these pages when the package-level contract is not enough. They move from the shared reference surface to the design decision and the read service behind it.
90
+
91
+ - [Session-reference subsystem](../../../docs/subsystems/session-reference.md) — canonical URIs, projection rules, and the stable error taxonomy.
92
+ - [Cross-session references decision record](../../../.agents/notes/implemented/feature/2026-07-21-cross-session-references.md) — design rationale for the reference contract.
93
+ - [Session-query subsystem](../../../docs/subsystems/session-query.md) — the read service that supplies session surfaces.
94
+ - [Context group map](../README.md) — sibling request-context packages.
95
+ - [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-session-reference) — every accepted config field and its source declaration.
96
+
97
+ -----
98
+
99
+ <a id="model-experience"></a>
29
100
  ## Model Experience
30
101
 
31
102
  ### Referenced session background
@@ -44,7 +115,23 @@ The request and snapshot are consecutive append-only target messages and preserv
44
115
 
45
116
  ## Known Limitations and Deferred Work
46
117
 
47
- - **No body discovery** — candidate queries inspect folded titles but do not search message bodies. A non-empty query may inspect every visible persisted session log through the session-query service's bounded, cancellable batch; a dedicated title index may replace that discovery path without changing URI, snapshot, or persistence contracts.
118
+ <a id="known-limitations-and-deferred-work"></a>
119
+
120
+
121
+ These limits define when cross-session references are a poor fit. They are current package constraints.
122
+
123
+ - **No body discovery** — candidate queries inspect titles but do not search message bodies.
124
+ - **Labels come from projections alone** — an attached session is labeled from its live projection cut, a cold one from its durable checkpoint, and a session neither answers for is labeled by its id and cannot be found by its title. Discovery never reads a log: folding one title costs a whole log, and this runs under every completion keystroke. A session persisted before the projection cache was composed regains its title the first time it is opened, which checkpoints it.
48
125
  - **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.
49
126
  - **Text projection only** — non-text user and assistant blocks are not propagated across sessions.
50
127
  - **No live link** — references are snapshots, not forks, resumes, subscriptions, or source-session mutations.
128
+
129
+ <a id="dev-note"></a>
130
+ ### Dev Note
131
+
132
+ <details>
133
+ <summary>Working context for maintainers — click to expand</summary>
134
+
135
+ None.
136
+
137
+ </details>
package/README.zh.md CHANGED
@@ -1,31 +1,102 @@
1
- # `@deepseek-ai/dsh-session-reference`
1
+ ---
2
+ description: "跨会话快照引用与持久的不受信任模型上下文,供启用或排查 ctx.sessionReferenceResolver 的用户与维护者阅读。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-session-reference
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- `ctx.sessionReferenceResolver` 会把其他会话准备为有界、只读快照,作为带来源信息、面向模型的上下文。它消费 `ctx.sessionQuery` 与后端无关的 compact 检查点标记;不需要 SQLite FTS。支持跨会话 mention 的宿主可以主动启用该服务。
10
+ ## 概述
11
+
12
+ `dsh-session-reference` 让一次对话可以引用其他会话:宿主把 `@label` mention 转换为规范 URI,服务则为模型准备每个被引用会话的有界、只读快照,作为持久、不受信任的背景上下文。候选发现按工作目录亲和度对其他会话排序,并用其最新标题作标签。快照在捕获后不可变,并带有固定警告,禁止遵循其中的指令、权限声明或工具请求。它是面向支持跨会话 mention 的宿主的可选服务;它消费 `ctx.sessionQuery`,不需要 SQLite FTS。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 当宿主应允许用户提及另一个会话并把其上下文交给模型时,启用此服务。由于它消费后端无关的 compact 检查点标记,任何 session-query 后端都可配合使用。
29
+
30
+ ### mention 语法
31
+
32
+ 规范 mention 是 Markdown 形式的 `@[label](dsh-session:<base64url 编码的 id>)`,或裸 `dsh-session:` URI;每个 JavaScript 字符串会话 id 都能精确往返。服务会把 mention 改写为消息中可读的 `@label` 文本,并返回结构化引用。显式 Markdown mention 会拒绝格式错误的 URI;空或只含标点符号的 scheme mention 仍是普通讨论文本。
33
+
34
+ ### 模型能得到什么
35
+
36
+ 引用其他会话的消息会紧随其后收到一条 `## Referenced sessions` 快照,作为第二条 user 角色消息。快照是不受信任的背景:固定警告告诉模型,除非当前用户明确重复,否则不得遵循其中的指令、权限声明或工具请求。每个来源都独立有界——每条消息至多 `maxReferences` 个不同会话、每个来源至多 `maxReferenceBytes` 字节——无法塞入预算的来源会直接使准备失败,而不是返回部分上下文。
37
+
38
+ ### 查找可引用的会话
39
+
40
+ `listCandidates(agent, query?, limit?)` 列出除 agent 自身外的会话,按 id、工作目录或投影标题做不区分大小写的过滤,并把同目录会话排在前面。每个候选以其最新标题作为 mention 标签;标题缺失或不可读时回退到会话 id,并报告其工作目录是否就是发起方 agent 的工作目录,宿主因此可以只在位置能区分该行时才显示它。浏览器消费方通过 `ctx.remote.sessionReferenceResolver.candidates` 调用同一发现能力,该方法会为每个候选附上规范 mention。
41
+
42
+ ### 配置
43
+
44
+ | 字段 | 默认值 | 含义 |
45
+ |---|---|---|
46
+ | `maxReferences` | `3` | 一条已准备消息中不同源会话的最大数量;不得超过 `3` |
47
+ | `candidateLimit` | `50` | 返回给宿主的默认候选数量 |
48
+ | `maxReferenceBytes` | `65536` | 一个引用对象的最大序列化 JSON 字节数 |
49
+
50
+ 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-reference)是每个受支持字段及其 JSDoc 的穷尽式真源。
6
51
 
7
- ## 公开 API
52
+ -----
8
53
 
9
- - `listCandidates(agent, query?, limit?)` 会列出 `agent.id` 之外的会话,按 id、cwd 或以日志为依据的最新标题进行不区分大小写的筛选,再按同 cwd、无 cwd、其他 cwd 记录排序,同时保持每组内的 `listSessions()` 创建顺序。每个已选候选会话都使用该标题作为 mention label;标题不存在或无法读取时回退到会话 id。不搜索消息主体。一元 `sessionReferenceResolver/candidates` Remote 方法在配置的候选上限内提供同一发现能力,并为每个候选附上规范 mention,浏览器消费方直接调用 `ctx.remote.sessionReferenceResolver.candidates`,无需 API Proxy 路由。
10
- - `prepare(agent, content, references, signal?)` 会保留首次 mention 顺序、对 id 去重,并拒绝自引用或超过已配置不同源上限的情况。它会并行读取所有源,返回与输入脱离的内容,外加零个或一个聚合且带标识的 `UserMessage` 上下文。下游 `agent/pre-step` 监听器接受步骤后,该服务会针对直接用户消息中的规范 mention 调用此方法。
11
- - `encodeSessionReferenceUri()` 与 `decodeSessionReferenceUri()` 实现 `dsh-session:<base64url(JSON.stringify(sessionId))>`,因此每个 JavaScript 字符串 id 都能精确往返。`formatSessionReferenceMention()` 发出 `@[label](uri)`,`parseSessionReferenceText()` 将 Markdown mention 或裸规范 URI 替换为可读的 `@label` 文本,并返回结构化引用。解析器会拒绝显式 Markdown mention 中任何格式错误的 URI;只当 scheme 后跟非空、符合 base64url 形状的 payload 时,裸文本才被视为引用,匹配但非规范的候选项仍会失败。空 scheme mention 或只含标点符号的 scheme mention 仍是普通讨论文本。
54
+ <a id="understand-the-implementation"></a>
55
+ ## 理解实现
12
56
 
13
- ## 快照语义
57
+ <details>
58
+ <summary>实现细节——点击展开</summary>
14
59
 
15
- 目标消息到达 `agent/pre-step` 时,准备阶段会对每个不同源调用一次 `ctx.sessionQuery.readSurface()`。因此,queued 消息在进入模型步骤时捕获源状态,此后生成的上下文保持不变。它仅投影折叠后当前表层中的用户直接发出的 `user/message`、assistant 文本,以及 `user/message` 检查点;这类检查点携带规范 `dsh-compaction` 源标记。带独立来源的 session-reference 消息属于注入上下文,会被排除以防止快照递归传播。已遮蔽的压缩(compaction)前事件、工具、推理(reasoning)、除已标记 compact 检查点外的其他插件生成 user 消息,以及未完成的 assistant 分片也都会被排除。因此,已压缩源只会提供最新检查点及其后保留的会话内容,不会还原已遮蔽的文本。
60
+ 本节解释服务的设计;可观察行为见[使用本包](#use-this-package)。
16
61
 
17
- 上下文源为 `{ kind: 'session-reference', version: 1, references }`;每条引用会记录其源 id 与 label、捕获 seq、是否存在 compact、已保留/已省略消息数、已省略 UTF-8 字节数与截断状态。该服务的外层 `agent/pre-step` 监听器会处理已接受的直接用户消息,保留其消息 id,并把每份快照插入到引用它的消息紧后。解析发生在最终领取收件箱消息之后,因此队列编辑和从 queue 移动到 steer 不需要引用专用处理。无效 mention、读取失败、取消和预算失败会在消息进入面向模型的历史之前结束该轮次。目标日志会先记录可读的直接 `user/message`,再记录其带来源信息的上下文 `user/message`;捕获后的源变更无法改变目标回放。
62
+ ### 设计理念
18
63
 
19
- ## 配置
64
+ 准备阶段在目标消息到达 `agent/pre-step` 时,对每个被引用会话的当前表层各精确读取一次,因此 queued 消息在进入模型步骤时捕获源状态,此后生成的上下文不可变。投影只保留用户直接发出的 `user/message`、assistant 文本,以及携带规范压缩标记的 `user/message` 检查点;带独立来源的 session-reference 消息会被排除,防止快照递归传播。源文本以 JSON 序列化,每个 `<` 都转义为 `\u003c`,因此无法拼出 `<referenced-sessions>` 定界标签。
20
65
 
21
- | Key | 默认值 | 约定 |
22
- |---|---:|---|
23
- | `maxReferences` | `3` | 一条已准备消息中不同源会话的最大数量;必须不大于 `3`。 |
24
- | `candidateLimit` | `50` | 返回给宿主的默认候选数量。 |
25
- | `maxReferenceBytes` | `65536` | 一个引用对象的最大序列化 JSON 字节数。 |
66
+ ### 源码地图
26
67
 
27
- 保留会对每个源独立应用 `maxReferenceBytes`,保留 compact 检查点与最新消息,再丢弃较旧的非检查点单元,并使用 `dsh-output-retention` 头部/尾部截断和精确 UTF-8 省略通知。如果某个源的固定序列化字段本身就超出限额,准备会以 `SESSION_REFERENCE_BUDGET_EXCEEDED` 失败,而不返回部分上下文。
68
+ | 文件 | 职责 |
69
+ |---|---|
70
+ | [`src/index.ts`](src/index.ts) | `SessionReferenceResolver`:pre-step 监听器、候选发现、准备 |
71
+ | [`src/config.ts`](src/config.ts) | `Config` schema、`SessionReferenceError` 错误分类 |
72
+ | [`src/uri.ts`](src/uri.ts) | `dsh-session:` URI 编解码、mention 格式化与解析 |
73
+ | [`src/projection.ts`](src/projection.ts) | 当前表层投影与字节预算保留 |
74
+ | [`src/serialization.ts`](src/serialization.ts) | 快照载荷的标签安全 JSON 转义 |
75
+ | [`src/types.ts`](src/types.ts) | `SessionReferenceInput`/`Candidate` 与来源类型 |
76
+ | [`src/invariant.ts`](src/invariant.ts) | 引用约定的不变式伴生插件 |
28
77
 
78
+ ### 主要流程
79
+
80
+ 外层 `agent/pre-step` 监听器接受步骤,从直接用户消息中解析规范 mention,再调用 `prepare`:规范化引用(保持首次 mention 顺序、去重、拒绝自引用与超限数量),并行读取每个表层,在 `maxReferenceBytes` 下逐源保留,并渲染聚合提示词。每份快照都插入到引用它的消息紧后,目标日志先记录可读的直接消息、再记录其带来源上下文,因此捕获后的源变更无法改变目标回放。
81
+
82
+ </details>
83
+
84
+ -----
85
+
86
+ <a id="further-exploration"></a>
87
+ ## 进一步探索
88
+
89
+ 包级约定不够用时阅读以下页面。它们从共享引用表面进入设计决策与其背后的读取服务。
90
+
91
+ - [会话引用子系统](../../../docs/subsystems/session-reference.zh.md)——规范 URI、投影规则与稳定的错误分类。
92
+ - [跨会话引用决策记录](../../../.agents/notes/implemented/feature/2026-07-21-cross-session-references.zh.md)——引用约定的设计理由。
93
+ - [会话查询子系统](../../../docs/subsystems/session-query.zh.md)——提供会话表层的读取服务。
94
+ - [context 组地图](../README.zh.md)——相邻的请求上下文包。
95
+ - [生成的配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-session-reference)——每个受支持配置字段及其源声明。
96
+
97
+ -----
98
+
99
+ <a id="model-experience"></a>
29
100
  ## 模型体验
30
101
 
31
102
  ### 引用会话背景
@@ -42,9 +113,25 @@
42
113
 
43
114
  请求与快照是两条连续、仅追加的目标消息,并保留较早的可缓存历史。不同引用或源捕获内容只改变新后缀;后续目标压缩可能使从替换边界起的复用失效。
44
115
 
45
- ## 已知限制与暂缓事项
116
+ ## 已知限制与延期工作
46
117
 
47
- - **不支持消息正文检索**:候选查询会检查折叠后的标题,但不搜索消息主体。非空查询可能通过 session-query 服务有界、可取消的批处理检查每个可见的持久化会话日志;专用标题索引未来可以替换这条发现路径,而不改变 URI、快照或持久化约定。
118
+ <a id="known-limitations-and-deferred-work"></a>
119
+
120
+
121
+ 这些限制说明跨会话引用何时不合适。它们是当前包约束。
122
+
123
+ - **不支持消息正文检索**:候选查询会检查标题,但不搜索消息主体。
124
+ - **标签只来自投影**:已挂载的会话由实时投影切面标注,冷会话由持久化 checkpoint 标注,两者都答不上来的会话用 id 作标签且无法按标题搜到。发现路径绝不读日志:折叠一个标题的代价是整份日志,而这段代码位于补全的每一次击键之下。早于投影缓存组合存在的会话,只要被打开一次(销毁时即写 checkpoint)就会恢复标题。
48
125
  - **受信任调用方边界**:该服务假设宿主有权读取 `ctx.sessionQuery` 公开的每个会话;它不是面向模型的搜索工具。
49
126
  - **只投影文本**:不会在会话间传播非文本 user 与 assistant 块。
50
127
  - **没有实时链接**:引用是快照,不是 fork、恢复、订阅或源会话变更。
128
+
129
+ <a id="dev-note"></a>
130
+ ### 开发备注
131
+
132
+ <details>
133
+ <summary>维护者的工作上下文——点击展开</summary>
134
+
135
+ 无。
136
+
137
+ </details>
package/lib/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
3
- import { assertNever, createUserMessage, freezeMessage } from "@deepseek-ai/dsh-llm";
3
+ import { createUserMessage, freezeMessage } from "@deepseek-ai/dsh-llm";
4
4
  import { isCompactCheckpointSource } from "@deepseek-ai/dsh-compaction";
5
5
  import { TextRetainer } from "@deepseek-ai/dsh-output-retention";
6
- import { SessionId } from "@deepseek-ai/dsh-session";
6
+ import { assertNever } from "@deepseek-ai/dsh-util-values";
7
+ import { brandString } from "@deepseek-ai/dsh-brand";
7
8
  //#region lib/types/config.js
8
9
  /** Configuration and stable diagnostics for session references. */
9
10
  /** Hard maximum references accepted by one message. */
@@ -207,7 +208,7 @@ function decodeSessionReferenceUri(uri) {
207
208
  try {
208
209
  const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
209
210
  if (typeof parsed !== "string") throw new TypeError("decoded session id is not a string");
210
- const sessionId = SessionId(parsed);
211
+ const sessionId = brandString(parsed);
211
212
  if (encodeSessionReferenceUri(sessionId) !== uri) throw new TypeError("URI is not canonical");
212
213
  return sessionId;
213
214
  } catch (error) {
@@ -360,7 +361,7 @@ let SessionReferenceResolver = (() => {
360
361
  const decision = await next();
361
362
  if (decision.kind === "reject") return decision;
362
363
  return {
363
- kind: "enter",
364
+ ...decision,
364
365
  messages: await this.prepareDirectMessages(agent, decision.messages, signal)
365
366
  };
366
367
  }, { prepend: true });
@@ -399,6 +400,10 @@ let SessionReferenceResolver = (() => {
399
400
  }
400
401
  /**
401
402
  * List reference candidates, ranked by working-directory affinity.
403
+ *
404
+ * Discovery runs at keystroke rate, so a title only ever comes from a
405
+ * projection read: see {@link SessionReferenceResolver.projectedTitle} for
406
+ * which sessions can answer one and which fall back to their id.
402
407
  * @param agent - target agent; self is excluded and its cwd drives ranking.
403
408
  * @param query - optional case-insensitive session-id/cwd/title substring.
404
409
  * @param limit - optional positive result cap.
@@ -410,30 +415,53 @@ let SessionReferenceResolver = (() => {
410
415
  const needle = query.toLocaleLowerCase();
411
416
  const targetCwd = agent.session.header.cwd;
412
417
  assertNotCancelled(signal);
413
- const records = (await settleWithCancellation(this.ctx.sessionQuery.listSessions(signal), signal)).filter((record) => record.header.id !== agent.id).map((record, index) => ({
418
+ return (await settleWithCancellation(this.ctx.sessionQuery.listSessions(signal), signal)).filter((record) => record.header.id !== agent.id).map((record, index) => ({
414
419
  record,
415
420
  index
416
- }));
417
- const inspected = needle === "" ? records.sort((a, b) => candidateRank(a.record.header.cwd, targetCwd) - candidateRank(b.record.header.cwd, targetCwd) || a.index - b.index).slice(0, limit) : records;
418
- const observations = await settleWithCancellation(this.ctx.sessionQuery.readTitleSnapshots(inspected.map(({ record }) => record.header.id), signal), signal);
419
- return inspected.map(({ record, index }, observationIndex) => {
420
- const observation = observations[observationIndex];
421
- return {
422
- record,
423
- index,
424
- label: observation.status === "fulfilled" ? observation.value.title?.title ?? record.header.id : record.header.id
425
- };
426
- }).filter(({ record, label }) => {
421
+ })).map(({ record, index }) => ({
422
+ record,
423
+ index,
424
+ label: this.projectedTitle(record) ?? record.header.id
425
+ })).filter(({ record, label }) => {
427
426
  if (needle === "") return true;
428
427
  return record.header.id.toLocaleLowerCase().includes(needle) || record.header.cwd?.toLocaleLowerCase().includes(needle) === true || label.toLocaleLowerCase().includes(needle);
429
428
  }).sort((a, b) => candidateRank(a.record.header.cwd, targetCwd) - candidateRank(b.record.header.cwd, targetCwd) || a.index - b.index).slice(0, limit).map(({ record, label }) => ({
430
429
  sessionId: record.header.id,
431
430
  label,
432
431
  ...record.header.cwd === void 0 ? {} : { cwd: record.header.cwd },
432
+ sameWorkspace: record.header.cwd !== void 0 && record.header.cwd === targetCwd,
433
433
  createdAt: record.header.createdAt
434
434
  }));
435
435
  }
436
436
  /**
437
+ * The title a session's projections can answer without reading its log.
438
+ *
439
+ * Attachment is decided by the store at read time, not by the listing:
440
+ * a session that attached in between would otherwise be answered from a
441
+ * checkpoint its live log has already moved past.
442
+ *
443
+ * An attached session answers from its live registry cut, which advances
444
+ * with every committed event, so a rename or a just-generated title is
445
+ * visible immediately; its events are already in memory, so the lazy fold
446
+ * costs no I/O. A cold session answers from the durable checkpoint the
447
+ * projection cache wrote when it went cold.
448
+ *
449
+ * Nothing else is attempted. Folding a title from a log costs the whole
450
+ * log, and this call sits under every keystroke of `@` completion. A
451
+ * session that no projection can answer for — one persisted before the
452
+ * cache was composed, or seeded straight to disk — is labeled by its id
453
+ * and cannot be found by its title until it is opened once, which
454
+ * checkpoints it.
455
+ * @param record - the listed session, live or cold.
456
+ * @returns the projected title, or undefined when no projection holds one.
457
+ */
458
+ projectedTitle(record) {
459
+ const attached = this.ctx.get("sessions")?.get(record.header.id);
460
+ const projections = this.ctx.get("sessionProjections");
461
+ if (attached !== void 0 && projections !== void 0) return titleOf(projections.snapshot(attached, ["title"]));
462
+ return titleOf(this.ctx.get("sessionProjectionCache")?.cachedSnapshot(record.header, ["title"]));
463
+ }
464
+ /**
437
465
  * Remote face of {@link listCandidates}: the configured candidate limit
438
466
  * applies, and every candidate carries the canonical mention a host inserts
439
467
  * into the prompt draft.
@@ -531,6 +559,11 @@ function normalizeReferences(targetId, references, maxReferences) {
531
559
  function renderPrompt(data) {
532
560
  return `${PROMPT_PREFIX}${stringifyTagSafeJson(data)}${PROMPT_SUFFIX}`;
533
561
  }
562
+ /** The title in one projection snapshot; undefined when the unit is absent or still untitled. */
563
+ function titleOf(snapshot) {
564
+ const title = snapshot?.values.title;
565
+ return title === void 0 || title === null ? void 0 : title;
566
+ }
534
567
  function candidateRank(candidateCwd, targetCwd) {
535
568
  if (candidateCwd !== void 0 && targetCwd !== void 0 && candidateCwd === targetCwd) return 0;
536
569
  if (candidateCwd === void 0) return 1;
@@ -8,6 +8,7 @@ const _deepseek_ai_dsh_session_reference_sessionReferenceResolver_candidates_res
8
8
  'sessionId': z.intersection(z.string(), z.unknown()),
9
9
  'label': z.string(),
10
10
  'cwd': z.string().optional(),
11
+ 'sameWorkspace': z.boolean(),
11
12
  'createdAt': z.number(),
12
13
  }))
13
14
 
@@ -57,7 +58,7 @@ export const TYPERT = {
57
58
  typeSymbol: '@deepseek-ai/dsh-session-reference#sessionReferenceResolver/candidates:result',
58
59
  schema: _deepseek_ai_dsh_session_reference_sessionReferenceResolver_candidates_result$schema,
59
60
  },
60
- sourceLocation: {"file":"packages/context/session-reference/src/index.ts","line":218,"column":9},
61
+ sourceLocation: {"file":"packages/context/session-reference/src/index.ts","line":245,"column":9},
61
62
  },
62
63
  ],
63
64
  model: {
@@ -75,7 +76,7 @@ export const TYPERT = {
75
76
  "name": "listCandidates",
76
77
  "signature": "async listCandidates( agent: Agent, query: string = '', limit: number = this.config.candidateLimit, signal?: AbortSignal, ): Promise<SessionReferenceCandidate[]>",
77
78
  "summary": "List reference candidates, ranked by working-directory affinity.",
78
- "jsDoc": "/**\n * List reference candidates, ranked by working-directory affinity.\n * @param agent - target agent; self is excluded and its cwd drives ranking.\n * @param query - optional case-insensitive session-id/cwd/title substring.\n * @param limit - optional positive result cap.\n * @param signal - optional cancellation boundary for host autocomplete teardown.\n * @returns candidates labeled by latest title or, when absent, session id.\n */"
79
+ "jsDoc": "/**\n * List reference candidates, ranked by working-directory affinity.\n *\n * Discovery runs at keystroke rate, so a title only ever comes from a\n * projection read: see {@link SessionReferenceResolver.projectedTitle} for\n * which sessions can answer one and which fall back to their id.\n * @param agent - target agent; self is excluded and its cwd drives ranking.\n * @param query - optional case-insensitive session-id/cwd/title substring.\n * @param limit - optional positive result cap.\n * @param signal - optional cancellation boundary for host autocomplete teardown.\n * @returns candidates labeled by latest title or, when absent, session id.\n */"
79
80
  },
80
81
  {
81
82
  "kind": "method",
@@ -103,7 +104,7 @@ export const TYPERT = {
103
104
  },
104
105
  {
105
106
  "name": "AgentOptions",
106
- "declaration": "export interface AgentOptions {\n provider?: string;\n model?: string;\n maxTokens?: number;\n}"
107
+ "declaration": "export interface AgentOptions {\n provider?: string;\n model?: string;\n reasoningEffort?: ReasoningEffortId;\n maxTokens?: number;\n subagentDepth?: number;\n}"
107
108
  },
108
109
  {
109
110
  "name": "AgentStatus",
@@ -137,22 +138,10 @@ export const TYPERT = {
137
138
  "name": "Branded",
138
139
  "declaration": "export type Branded<B extends string> = string & { readonly [BRAND]: B; };"
139
140
  },
140
- {
141
- "name": "CallId",
142
- "declaration": "export type CallId = Branded<'CallId'>;"
143
- },
144
141
  {
145
142
  "name": "CancelOptions",
146
143
  "declaration": "export interface CancelOptions {\n keepInbox?: boolean | undefined;\n}"
147
144
  },
148
- {
149
- "name": "CodeDispatchEventData",
150
- "declaration": "export interface CodeDispatchEventData extends CodeDispatchStartEventData {\n isError: boolean;\n content: ContentBlock[];\n}"
151
- },
152
- {
153
- "name": "CodeDispatchStartEventData",
154
- "declaration": "export interface CodeDispatchStartEventData {\n rootCallId: CallId;\n parentCallId: CallId;\n subCallId: CallId;\n name: string;\n arguments: unknown;\n}"
155
- },
156
145
  {
157
146
  "name": "CommandId",
158
147
  "declaration": "export type CommandId = Branded<'CommandId'>;"
@@ -189,6 +178,14 @@ export const TYPERT = {
189
178
  "name": "ContextSnapshotSection",
190
179
  "declaration": "export interface ContextSnapshotSection {\n readonly name: string;\n readonly text: string;\n}"
191
180
  },
181
+ {
182
+ "name": "ContinuableSubagentDescriptorData",
183
+ "declaration": "export interface ContinuableSubagentDescriptorData extends SubagentDescriptorBase {\n readonly mode: 'continuable';\n readonly label: string;\n readonly agentProvider?: string;\n readonly agentModel?: string;\n readonly agentReasoningEffort?: ReasoningEffortId;\n readonly persona?: string;\n readonly toolFilter?: ToolRestriction;\n}"
184
+ },
185
+ {
186
+ "name": "CoordinatorMessageSource",
187
+ "declaration": "export interface CoordinatorMessageSource {\n readonly kind: 'coordinator';\n readonly form: 'relay';\n readonly senderSessionId: SessionId;\n}"
188
+ },
192
189
  {
193
190
  "name": "EpochHeader",
194
191
  "declaration": "export interface EpochHeader {\n config: LlmCallConfig;\n adapterDefaults?: LlmCallConfigAdapterDefaults;\n system?: string;\n tools?: ToolSchema[];\n}"
@@ -291,12 +288,20 @@ export const TYPERT = {
291
288
  },
292
289
  {
293
290
  "name": "MessageSourceMap",
294
- "declaration": "export interface MessageSourceMap {\n user: { kind: 'user'; };\n plugin: { kind: 'plugin'; plugin: string; } & ContextFormed;\n model: ModelMessageSource;\n tool: ToolMessageSource;\n goal: GoalMessageSource;\n 'session-reference': SessionReferenceSource;\n}"
291
+ "declaration": "export interface MessageSourceMap {\n user: { kind: 'user'; };\n plugin: { kind: 'plugin'; plugin: string; } & ContextFormed;\n model: ModelMessageSource;\n tool: ToolMessageSource;\n 'user-rpc': { kind: 'user'; rpcId: SessionRequestId; clientTimeZone?: string; };\n coordinator: CoordinatorMessageSource;\n 'subagent-report': SubagentReportMessageSource;\n 'subagent-settled': SubagentSettledMessageSource;\n 'skill-invocation': SkillInvocationSource;\n 'team-message': TeamMessageSource;\n goal: GoalMessageSource;\n 'session-reference': SessionReferenceSource;\n}"
295
292
  },
296
293
  {
297
294
  "name": "ModelMessageSource",
298
295
  "declaration": "export interface ModelMessageSource extends AssistantProvenance {\n kind: 'model';\n}"
299
296
  },
297
+ {
298
+ "name": "ModelSelection",
299
+ "declaration": "export interface ModelSelection {\n readonly provider: string;\n readonly model: string;\n readonly reasoningEffort?: string;\n}"
300
+ },
301
+ {
302
+ "name": "OneShotSubagentDescriptorData",
303
+ "declaration": "export interface OneShotSubagentDescriptorData extends SubagentDescriptorBase {\n readonly mode: 'one-shot';\n readonly label?: string;\n}"
304
+ },
300
305
  {
301
306
  "name": "PreparedReferencedMessage",
302
307
  "declaration": "export interface PreparedReferencedMessage {\n content: ContentBlock[];\n additionalContext?: UserMessage;\n}"
@@ -305,6 +310,14 @@ export const TYPERT = {
305
310
  "name": "ProviderRequestId",
306
311
  "declaration": "export type ProviderRequestId = Branded<'ProviderRequestId'>;"
307
312
  },
313
+ {
314
+ "name": "PtcDispatchEventData",
315
+ "declaration": "export interface PtcDispatchEventData extends PtcDispatchStartEventData {\n isError: boolean;\n content: ContentBlock[];\n}"
316
+ },
317
+ {
318
+ "name": "PtcDispatchStartEventData",
319
+ "declaration": "export interface PtcDispatchStartEventData {\n rootCallId: ToolCallId;\n parentCallId: ToolCallId;\n subCallId: ToolCallId;\n name: string;\n arguments: unknown;\n}"
320
+ },
308
321
  {
309
322
  "name": "ReasoningBlock",
310
323
  "declaration": "export interface ReasoningBlock {\n type: 'reasoning';\n text: string;\n}"
@@ -323,7 +336,11 @@ export const TYPERT = {
323
336
  },
324
337
  {
325
338
  "name": "RequestHeaderReason",
326
- "declaration": "export type RequestHeaderReason = 'initial' | 'resume' | 'change';"
339
+ "declaration": "export type RequestHeaderReason = 'initial' | 'resume' | 'change' | 'series';"
340
+ },
341
+ {
342
+ "name": "SandboxMode",
343
+ "declaration": "export type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';"
327
344
  },
328
345
  {
329
346
  "name": "Session",
@@ -335,7 +352,7 @@ export const TYPERT = {
335
352
  },
336
353
  {
337
354
  "name": "SessionEventMap",
338
- "declaration": "export interface SessionEventMap {\n 'turn/start': { turn: number; };\n 'turn/end': { turn: number; reason: TurnEndReason; };\n 'step/start': { turn: number; step: number; };\n 'step/end': { turn: number; step: number; };\n 'user/message': UserMessage;\n 'assistant/chunk': { turn: number; step: number; chunk: StreamChunk; };\n 'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true; };\n 'tool/call': { turn: number; step: number; callId: CallId; name: string; arguments: string; };\n 'tool/result': { turn: number; step: number; message: ToolResultMessage; error?: { name: string; code: string; }; meta?: JsonValue; };\n 'todo/write': { todos: TodoItem[]; };\n 'request/header': { header: EpochHeader; reason: RequestHeaderReason; };\n 'request/context': RequestContext;\n 'session/end-seed': Record<string, never>;\n 'agent/inbox/spliced': { target: InboxTarget; start: number; removedCount?: number; inserted: UserMessage[]; outcome?: 'canceled'; };\n 'command/run': { commandId: CommandId; name: string; args?: string; source: CommandSource; };\n 'command/done': { commandId: CommandId; kind: 'success' | 'error'; text?: string; sourceEventSeq?: number; };\n 'approval/asked': { id: ApprovalRequestId; toolName: string; callId?: CallId; reason?: string; };\n 'approval/decided': { id: ApprovalRequestId; outcome: ApprovalOutcome; };\n 'approval/policy': { policy: ApprovalPolicy; source?: 'delegation'; };\n 'tool/code-dispatch-start': CodeDispatchStartEventData;\n 'tool/code-dispatch': CodeDispatchEventData;\n 'goal/change': GoalChangeMeta;\n 'session/title': SessionTitleEventData;\n 'compaction/start': { compactionId: CompactionId; sourceCommandId?: CommandId; turn: number | null; };\n 'compaction/summary': { compactionId: CompactionId; sourceCommandId?: CommandId; summary: ContentBlock[]; shadowedRange: { start: number; end: number; }; shadowedSeqs: number[]; shadowedTokenCount: number; provider: string; model: string; maxTokens?: number; usage?: TokenUsage; } & ({ rawOutput: ContentBlock[]; llmStreamCall: true; } | { rawOutput?: ContentBlock[]; llmStreamCall?: never; });\n 'compaction/end': { compactionId: CompactionId; sourceCommandId?: CommandId; turn: number | null; error?: string; };\n 'compaction/prune': { shadowedRange: { start: number; end: number; }; shadowedSeqs: number[]; shadowedTokenCount: number; };\n}"
355
+ "declaration": "export interface SessionEventMap {\n 'turn/start': { turn: number; };\n 'turn/end': { turn: number; reason: TurnEndReason; };\n 'step/start': { turn: number; step: number; };\n 'step/end': { turn: number; step: number; };\n 'user/message': UserMessage;\n 'assistant/chunk': { turn: number; step: number; chunk: StreamChunk; };\n 'assistant/message': { turn: number; step: number; message: AssistantMessage; usage?: TokenUsage; interrupted?: true; };\n 'tool/call': { turn: number; step: number; callId: ToolCallId; name: string; arguments: string; };\n 'tool/result': { turn: number; step: number; message: ToolResultMessage; error?: { name: string; code: string; }; meta?: JsonValue; };\n 'request/header': { header: EpochHeader; reason: RequestHeaderReason; startsSeries?: true; };\n 'request/context': RequestContext;\n 'session/end-seed': Record<string, never>;\n 'agent/inbox/spliced': { target: InboxTarget; start: number; removedCount?: number; inserted: UserMessage[]; outcome?: 'canceled'; };\n 'approval/asked': { id: ApprovalRequestId; toolName: string; callId?: ToolCallId; reason?: string; };\n 'approval/decided': { id: ApprovalRequestId; outcome: ApprovalOutcome; };\n 'approval/policy': { policy: ApprovalPolicy; source?: 'delegation'; };\n 'tool/code-dispatch-start': PtcDispatchStartEventData;\n 'tool/code-dispatch': PtcDispatchEventData;\n 'agent-preset/selected': { agentPreset: string; };\n 'session/title': SessionTitleEventData;\n 'todo/write': { todos: TodoItem[]; };\n 'model/selection': ModelSelection;\n 'subagent/descriptor': SubagentDescriptorData;\n 'sandbox/mode': { mode: SandboxMode; source?: 'delegation'; };\n 'command/run': { commandId: CommandId; name: string; args?: string; source: CommandSource; };\n 'command/done': { commandId: CommandId; kind: 'success' | 'error'; text?: string; sourceEventSeq?: number; };\n 'team/member': { version: 1; teamId: TeamId; member: TeamMemberSnapshot; };\n 'team/task': { version: 1; teamId: TeamId; task: TeamTaskSnapshot; };\n 'team/message/queued': { version: 1; teamId: TeamId; message: TeamMessageSnapshot; };\n 'team/message/delivered': { version: 1; teamId: TeamId; messageId: TeamMessageId; targetId: SessionId; };\n 'goal/change': GoalChangeMeta;\n 'compaction/start': { compactionId: CompactionId; sourceCommandId?: CommandId; turn: number | null; };\n 'compaction/summary': { compactionId: CompactionId; sourceCommandId?: CommandId; summary: ContentBlock[]; shadowedRange: { start: number; end: number; }; shadowedSeqs: number[]; shadowedTokenCount: number; provider: string; model: string; maxTokens?: number; usage?: TokenUsage; } & ({ rawOutput: ContentBlock[]; llmStreamCall: true; } | { rawOutput?: ContentBlock[]; llmStreamCall?: never; });\n 'compaction/end': { compactionId: CompactionId; sourceCommandId?: CommandId; turn: number | null; error?: string; };\n 'compaction/prune': { shadowedRange: { start: number; end: number; }; shadowedSeqs: number[]; shadowedTokenCount: number; };\n}"
339
356
  },
340
357
  {
341
358
  "name": "SessionEventType",
@@ -351,7 +368,7 @@ export const TYPERT = {
351
368
  },
352
369
  {
353
370
  "name": "SessionReferenceCandidate",
354
- "declaration": "export interface SessionReferenceCandidate {\n sessionId: SessionId;\n label: string;\n cwd?: string;\n createdAt: number;\n}"
371
+ "declaration": "export interface SessionReferenceCandidate {\n sessionId: SessionId;\n label: string;\n cwd?: string;\n sameWorkspace: boolean;\n createdAt: number;\n}"
355
372
  },
356
373
  {
357
374
  "name": "SessionReferenceInput",
@@ -365,6 +382,10 @@ export const TYPERT = {
365
382
  "name": "SessionReferenceSource",
366
383
  "declaration": "export interface SessionReferenceSource {\n kind: 'session-reference';\n form: 'recall';\n version: 1;\n references: { sessionId: string; label: string; capturedThroughSeq: number | null; compacted: boolean; originalMessages: number; retainedMessages: number; omittedMessages: number; omittedBytes: number; truncated: boolean; inputIndex: number; }[];\n}"
367
384
  },
385
+ {
386
+ "name": "SessionRequestId",
387
+ "declaration": "export type SessionRequestId = Branded<'session-request-id'>;"
388
+ },
368
389
  {
369
390
  "name": "SessionSurface",
370
391
  "declaration": "export interface SessionSurface {\n readonly nodes: readonly number[];\n readonly replaceGeneration: number;\n}"
@@ -385,9 +406,29 @@ export const TYPERT = {
385
406
  "name": "SessionTitleSource",
386
407
  "declaration": "export type SessionTitleSource = { readonly kind: 'fallback'; } | { readonly kind: 'provider'; readonly provider: SessionTitleProviderId; readonly model?: SessionTitleModelProvenance; } | { readonly kind: 'user'; };"
387
408
  },
409
+ {
410
+ "name": "SkillInvocationSource",
411
+ "declaration": "export interface SkillInvocationSource {\n readonly kind: 'skill-invocation';\n readonly name: string;\n readonly form: 'instructions';\n}"
412
+ },
388
413
  {
389
414
  "name": "StreamChunk",
390
- "declaration": "export type StreamChunk = { type: 'block-start'; index: number; blockType: ContentBlockType; } | { type: 'text-delta'; index: number; text: string; } | { type: 'reasoning-delta'; index: number; text: string; } | { type: 'tool-call-delta'; index: number; id: CallId; name?: string; argumentsDelta: string; } | { type: 'block-end'; index: number; block: ContentBlock; } | { type: 'usage'; usage: TokenUsage; } | { type: 'finish'; reason: FinishReason; replayState?: ReplayEnvelope; };"
415
+ "declaration": "export type StreamChunk = { type: 'block-start'; index: number; blockType: ContentBlockType; } | { type: 'text-delta'; index: number; text: string; } | { type: 'reasoning-delta'; index: number; text: string; } | { type: 'tool-call-delta'; index: number; id: ToolCallId; name?: string; argumentsDelta: string; } | { type: 'block-end'; index: number; block: ContentBlock; } | { type: 'usage'; usage: TokenUsage; } | { type: 'finish'; reason: FinishReason; replayState?: ReplayEnvelope; };"
416
+ },
417
+ {
418
+ "name": "SubagentDescriptorBase",
419
+ "declaration": "export interface SubagentDescriptorBase {\n readonly version: number;\n readonly mode: 'one-shot' | 'continuable';\n readonly provider: string;\n}"
420
+ },
421
+ {
422
+ "name": "SubagentDescriptorData",
423
+ "declaration": "export type SubagentDescriptorData = OneShotSubagentDescriptorData | ContinuableSubagentDescriptorData;"
424
+ },
425
+ {
426
+ "name": "SubagentReportMessageSource",
427
+ "declaration": "export interface SubagentReportMessageSource {\n readonly kind: 'subagent-report';\n readonly form: 'relay';\n readonly senderSessionId: SessionId;\n}"
428
+ },
429
+ {
430
+ "name": "SubagentSettledMessageSource",
431
+ "declaration": "export interface SubagentSettledMessageSource {\n readonly kind: 'subagent-settled';\n readonly form: 'notice';\n readonly summary: string;\n readonly senderSessionId: SessionId;\n}"
391
432
  },
392
433
  {
393
434
  "name": "SurfaceEventType",
@@ -401,6 +442,42 @@ export const TYPERT = {
401
442
  "name": "SurfaceOp",
402
443
  "declaration": "export type SurfaceOp = 'append' | { op: 'replace'; start: number; end: number; };"
403
444
  },
445
+ {
446
+ "name": "TeamId",
447
+ "declaration": "export type TeamId = Branded<'TeamId'>;"
448
+ },
449
+ {
450
+ "name": "TeamMemberPhase",
451
+ "declaration": "export type TeamMemberPhase = 'provisioning' | 'active' | 'failed';"
452
+ },
453
+ {
454
+ "name": "TeamMemberSnapshot",
455
+ "declaration": "export interface TeamMemberSnapshot {\n readonly id: SessionId;\n readonly name: string;\n readonly description: string;\n readonly provider: string;\n readonly context: 'fresh' | 'fork';\n readonly phase: TeamMemberPhase;\n readonly error?: string;\n}"
456
+ },
457
+ {
458
+ "name": "TeamMessageId",
459
+ "declaration": "export type TeamMessageId = Branded<'TeamMessageId'>;"
460
+ },
461
+ {
462
+ "name": "TeamMessageSnapshot",
463
+ "declaration": "export interface TeamMessageSnapshot {\n readonly id: TeamMessageId;\n readonly senderId: SessionId;\n readonly senderName: string;\n readonly targetId: SessionId;\n readonly delivery: 'quiet' | 'wakeup';\n readonly content: ContentBlock[];\n}"
464
+ },
465
+ {
466
+ "name": "TeamMessageSource",
467
+ "declaration": "export interface TeamMessageSource {\n readonly kind: 'team-message';\n readonly teamId: TeamId;\n readonly messageId: TeamMessageId;\n readonly senderId: SessionId;\n readonly senderName: string;\n}"
468
+ },
469
+ {
470
+ "name": "TeamTaskId",
471
+ "declaration": "export type TeamTaskId = Branded<'TeamTaskId'>;"
472
+ },
473
+ {
474
+ "name": "TeamTaskSnapshot",
475
+ "declaration": "export interface TeamTaskSnapshot {\n readonly id: TeamTaskId;\n readonly revision: number;\n readonly subject: string;\n readonly description: string;\n readonly status: TeamTaskStatus;\n readonly ownerId?: SessionId;\n readonly blockedBy: TeamTaskId[];\n readonly writeScopes: string[];\n}"
476
+ },
477
+ {
478
+ "name": "TeamTaskStatus",
479
+ "declaration": "export type TeamTaskStatus = 'pending' | 'in_progress' | 'completed' | 'deleted';"
480
+ },
404
481
  {
405
482
  "name": "TextBlock",
406
483
  "declaration": "export interface TextBlock {\n type: 'text';\n text: string;\n}"
@@ -411,19 +488,27 @@ export const TYPERT = {
411
488
  },
412
489
  {
413
490
  "name": "TokenUsage",
414
- "declaration": "export interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n reasoningTokens?: number;\n}"
491
+ "declaration": "export interface TokenUsage {\n inputTokens: number;\n outputTokens: number;\n totalTokens?: number;\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n reasoningTokens?: number;\n}"
415
492
  },
416
493
  {
417
494
  "name": "ToolCallBlock",
418
- "declaration": "export interface ToolCallBlock {\n type: 'tool-call';\n id: CallId;\n name: string;\n arguments: string;\n}"
495
+ "declaration": "export interface ToolCallBlock {\n type: 'tool-call';\n id: ToolCallId;\n name: string;\n arguments: string;\n}"
496
+ },
497
+ {
498
+ "name": "ToolCallId",
499
+ "declaration": "export type ToolCallId = Branded<'ToolCallId'>;"
419
500
  },
420
501
  {
421
502
  "name": "ToolMessageSource",
422
- "declaration": "export interface ToolMessageSource {\n kind: 'tool';\n callId: CallId;\n}"
503
+ "declaration": "export interface ToolMessageSource {\n kind: 'tool';\n callId: ToolCallId;\n}"
504
+ },
505
+ {
506
+ "name": "ToolRestriction",
507
+ "declaration": "export interface ToolRestriction {\n readonly allow?: readonly string[];\n readonly deny?: readonly string[];\n}"
423
508
  },
424
509
  {
425
510
  "name": "ToolResultBlock",
426
- "declaration": "export interface ToolResultBlock {\n type: 'tool-result';\n toolCallId: CallId;\n content: ContentBlock[];\n isError?: boolean;\n}"
511
+ "declaration": "export interface ToolResultBlock {\n type: 'tool-result';\n toolCallId: ToolCallId;\n content: ContentBlock[];\n isError?: boolean;\n}"
427
512
  },
428
513
  {
429
514
  "name": "ToolResultMessage",
@@ -8,6 +8,7 @@ const _deepseek_ai_dsh_session_reference_sessionReferenceResolver_candidates_res
8
8
  'sessionId': z.intersection(z.string(), z.unknown()),
9
9
  'label': z.string(),
10
10
  'cwd': z.string().optional(),
11
+ 'sameWorkspace': z.boolean(),
11
12
  'createdAt': z.number(),
12
13
  }))
13
14
 
@@ -54,7 +55,7 @@ export const TYPERT_REMOTE = {
54
55
  typeSymbol: '@deepseek-ai/dsh-session-reference#sessionReferenceResolver/candidates:result',
55
56
  schema: _deepseek_ai_dsh_session_reference_sessionReferenceResolver_candidates_result$schema,
56
57
  },
57
- sourceLocation: {"file":"packages/context/session-reference/src/index.ts","line":218,"column":9},
58
+ sourceLocation: {"file":"packages/context/session-reference/src/index.ts","line":245,"column":9},
58
59
  },
59
60
  ],
60
61
  }
@@ -37,6 +37,10 @@ export declare class SessionReferenceResolver extends TypertRemoteService {
37
37
  private prepareDirectMessages;
38
38
  /**
39
39
  * List reference candidates, ranked by working-directory affinity.
40
+ *
41
+ * Discovery runs at keystroke rate, so a title only ever comes from a
42
+ * projection read: see {@link SessionReferenceResolver.projectedTitle} for
43
+ * which sessions can answer one and which fall back to their id.
40
44
  * @param agent - target agent; self is excluded and its cwd drives ranking.
41
45
  * @param query - optional case-insensitive session-id/cwd/title substring.
42
46
  * @param limit - optional positive result cap.
@@ -44,6 +48,29 @@ export declare class SessionReferenceResolver extends TypertRemoteService {
44
48
  * @returns candidates labeled by latest title or, when absent, session id.
45
49
  */
46
50
  listCandidates(agent: Agent, query?: string, limit?: number, signal?: AbortSignal): Promise<SessionReferenceCandidate[]>;
51
+ /**
52
+ * The title a session's projections can answer without reading its log.
53
+ *
54
+ * Attachment is decided by the store at read time, not by the listing:
55
+ * a session that attached in between would otherwise be answered from a
56
+ * checkpoint its live log has already moved past.
57
+ *
58
+ * An attached session answers from its live registry cut, which advances
59
+ * with every committed event, so a rename or a just-generated title is
60
+ * visible immediately; its events are already in memory, so the lazy fold
61
+ * costs no I/O. A cold session answers from the durable checkpoint the
62
+ * projection cache wrote when it went cold.
63
+ *
64
+ * Nothing else is attempted. Folding a title from a log costs the whole
65
+ * log, and this call sits under every keystroke of `@` completion. A
66
+ * session that no projection can answer for — one persisted before the
67
+ * cache was composed, or seeded straight to disk — is labeled by its id
68
+ * and cannot be found by its title until it is opened once, which
69
+ * checkpoints it.
70
+ * @param record - the listed session, live or cold.
71
+ * @returns the projected title, or undefined when no projection holds one.
72
+ */
73
+ private projectedTitle;
47
74
  /**
48
75
  * Remote face of {@link listCandidates}: the configured candidate limit
49
76
  * applies, and every candidate carries the canonical mention a host inserts
@@ -96,7 +96,7 @@ let SessionReferenceResolver = (() => {
96
96
  if (decision.kind === 'reject')
97
97
  return decision;
98
98
  return {
99
- kind: 'enter',
99
+ ...decision,
100
100
  messages: await this.prepareDirectMessages(agent, decision.messages, signal),
101
101
  };
102
102
  }, { prepend: true });
@@ -135,6 +135,10 @@ let SessionReferenceResolver = (() => {
135
135
  }
136
136
  /**
137
137
  * List reference candidates, ranked by working-directory affinity.
138
+ *
139
+ * Discovery runs at keystroke rate, so a title only ever comes from a
140
+ * projection read: see {@link SessionReferenceResolver.projectedTitle} for
141
+ * which sessions can answer one and which fall back to their id.
138
142
  * @param agent - target agent; self is excluded and its cwd drives ranking.
139
143
  * @param query - optional case-insensitive session-id/cwd/title substring.
140
144
  * @param limit - optional positive result cap.
@@ -151,23 +155,12 @@ let SessionReferenceResolver = (() => {
151
155
  const records = (await settleWithCancellation(this.ctx.sessionQuery.listSessions(signal), signal))
152
156
  .filter(record => record.header.id !== agent.id)
153
157
  .map((record, index) => ({ record, index }));
154
- const inspected = needle === ''
155
- ? records
156
- .sort((a, b) => candidateRank(a.record.header.cwd, targetCwd) - candidateRank(b.record.header.cwd, targetCwd)
157
- || a.index - b.index)
158
- .slice(0, limit)
159
- : records;
160
- const observations = await settleWithCancellation(this.ctx.sessionQuery.readTitleSnapshots(inspected.map(({ record }) => record.header.id), signal), signal);
161
- return inspected.map(({ record, index }, observationIndex) => {
162
- const observation = observations[observationIndex];
163
- return {
164
- record,
165
- index,
166
- label: observation.status === 'fulfilled'
167
- ? observation.value.title?.title ?? record.header.id
168
- : record.header.id,
169
- };
170
- }).filter(({ record, label }) => {
158
+ const labelled = records.map(({ record, index }) => ({
159
+ record,
160
+ index,
161
+ label: this.projectedTitle(record) ?? record.header.id,
162
+ }));
163
+ return labelled.filter(({ record, label }) => {
171
164
  if (needle === '')
172
165
  return true;
173
166
  return record.header.id.toLocaleLowerCase().includes(needle)
@@ -180,9 +173,40 @@ let SessionReferenceResolver = (() => {
180
173
  sessionId: record.header.id,
181
174
  label,
182
175
  ...record.header.cwd === undefined ? {} : { cwd: record.header.cwd },
176
+ sameWorkspace: record.header.cwd !== undefined && record.header.cwd === targetCwd,
183
177
  createdAt: record.header.createdAt,
184
178
  }));
185
179
  }
180
+ /**
181
+ * The title a session's projections can answer without reading its log.
182
+ *
183
+ * Attachment is decided by the store at read time, not by the listing:
184
+ * a session that attached in between would otherwise be answered from a
185
+ * checkpoint its live log has already moved past.
186
+ *
187
+ * An attached session answers from its live registry cut, which advances
188
+ * with every committed event, so a rename or a just-generated title is
189
+ * visible immediately; its events are already in memory, so the lazy fold
190
+ * costs no I/O. A cold session answers from the durable checkpoint the
191
+ * projection cache wrote when it went cold.
192
+ *
193
+ * Nothing else is attempted. Folding a title from a log costs the whole
194
+ * log, and this call sits under every keystroke of `@` completion. A
195
+ * session that no projection can answer for — one persisted before the
196
+ * cache was composed, or seeded straight to disk — is labeled by its id
197
+ * and cannot be found by its title until it is opened once, which
198
+ * checkpoints it.
199
+ * @param record - the listed session, live or cold.
200
+ * @returns the projected title, or undefined when no projection holds one.
201
+ */
202
+ projectedTitle(record) {
203
+ const attached = this.ctx.get('sessions')?.get(record.header.id);
204
+ const projections = this.ctx.get('sessionProjections');
205
+ if (attached !== undefined && projections !== undefined) {
206
+ return titleOf(projections.snapshot(attached, ['title']));
207
+ }
208
+ return titleOf(this.ctx.get('sessionProjectionCache')?.cachedSnapshot(record.header, ['title']));
209
+ }
186
210
  /**
187
211
  * Remote face of {@link listCandidates}: the configured candidate limit
188
212
  * applies, and every candidate carries the canonical mention a host inserts
@@ -287,6 +311,11 @@ function normalizeReferences(targetId, references, maxReferences) {
287
311
  function renderPrompt(data) {
288
312
  return `${PROMPT_PREFIX}${stringifyTagSafeJson(data)}${PROMPT_SUFFIX}`;
289
313
  }
314
+ /** The title in one projection snapshot; undefined when the unit is absent or still untitled. */
315
+ function titleOf(snapshot) {
316
+ const title = snapshot?.values.title;
317
+ return title === undefined || title === null ? undefined : title;
318
+ }
290
319
  function candidateRank(candidateCwd, targetCwd) {
291
320
  if (candidateCwd !== undefined && targetCwd !== undefined && candidateCwd === targetCwd)
292
321
  return 0;
@@ -1,7 +1,7 @@
1
1
  /** Current-surface projection and byte-bounded rendering. */
2
2
  import { isCompactCheckpointSource } from '@deepseek-ai/dsh-compaction';
3
- import { assertNever } from '@deepseek-ai/dsh-llm';
4
3
  import { TextRetainer } from '@deepseek-ai/dsh-output-retention';
4
+ import { assertNever } from '@deepseek-ai/dsh-util-values';
5
5
  import { stringifyTagSafeJson } from "./serialization.js";
6
6
  /** Project current user/assistant conversation while excluding tools, reasoning, and injected context. */
7
7
  function projectSessionConversation(snapshot) {
@@ -46,6 +46,12 @@ export interface SessionReferenceCandidate {
46
46
  label: string;
47
47
  /** Source session working directory, when recorded. */
48
48
  cwd?: string;
49
+ /**
50
+ * True when {@link SessionReferenceCandidate.cwd} is recorded and equals the
51
+ * requesting agent's. Hosts that only surface a distinguishing location
52
+ * read this instead of comparing paths they never received.
53
+ */
54
+ sameWorkspace: boolean;
49
55
  /** Source session creation time in Unix epoch milliseconds. */
50
56
  createdAt: number;
51
57
  }
@@ -1,5 +1,5 @@
1
1
  /** Canonical session URI and inline mention encoding. */
2
- import { type SessionId as SessionIdType } from '@deepseek-ai/dsh-session';
2
+ import type { SessionId as SessionIdType } from '@deepseek-ai/dsh-session';
3
3
  import type { SessionReferenceInput } from './types.ts';
4
4
  /** URI scheme reserved for DeepSeek Harness session snapshots. */
5
5
  export declare const SESSION_REFERENCE_SCHEME = "dsh-session:";
package/lib/types/uri.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Canonical session URI and inline mention encoding. */
2
- import { SessionId } from '@deepseek-ai/dsh-session';
2
+ import { brandString } from '@deepseek-ai/dsh-brand';
3
3
  import { SessionReferenceError } from "./config.js";
4
4
  /** URI scheme reserved for DeepSeek Harness session snapshots. */
5
5
  export const SESSION_REFERENCE_SCHEME = 'dsh-session:';
@@ -28,7 +28,7 @@ export function decodeSessionReferenceUri(uri) {
28
28
  const parsed = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
29
29
  if (typeof parsed !== 'string')
30
30
  throw new TypeError('decoded session id is not a string');
31
- const sessionId = SessionId(parsed);
31
+ const sessionId = brandString(parsed);
32
32
  if (encodeSessionReferenceUri(sessionId) !== uri)
33
33
  throw new TypeError('URI is not canonical');
34
34
  return sessionId;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-session-reference",
3
3
  "description": "Cross-session snapshot references and durable untrusted model context (ctx.sessionReferenceResolver)",
4
- "version": "0.1.1-rc.2",
4
+ "version": "0.1.2-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -50,28 +50,41 @@
50
50
  "license": "MIT",
51
51
  "dependencies": {
52
52
  "zod": "^4.4.3",
53
- "@deepseek-ai/schemastery": "^3.18.1"
53
+ "@deepseek-ai/dsh-brand": "^0.1.2-alpha.2",
54
+ "@deepseek-ai/dsh-util-values": "^0.1.2-alpha.2",
55
+ "@deepseek-ai/schemastery": "^3.18.2"
54
56
  },
55
57
  "peerDependencies": {
56
- "@deepseek-ai/dsh-compaction": "^0.1.1-rc.2",
57
- "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
58
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
59
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
60
- "@deepseek-ai/dsh-output-retention": "^0.1.1-rc.2",
61
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
62
- "@deepseek-ai/dsh-session-query": "^0.1.1-rc.2",
63
- "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2",
64
- "@deepseek-ai/cordis": "^4.0.1"
58
+ "@deepseek-ai/cordis": "^4.0.2",
59
+ "@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
60
+ "@deepseek-ai/dsh-compaction": "^0.1.2-alpha.2",
61
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
62
+ "@deepseek-ai/dsh-output-retention": "^0.1.2-alpha.2",
63
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
64
+ "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
65
+ "@deepseek-ai/dsh-session-projection-cache": "^0.1.2-alpha.2",
66
+ "@deepseek-ai/dsh-session-query": "^0.1.2-alpha.2",
67
+ "@deepseek-ai/dsh-session-title": "^0.1.2-alpha.2",
68
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.2",
69
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "@deepseek-ai/dsh-session-projection-cache": {
73
+ "optional": true
74
+ }
65
75
  },
66
76
  "devDependencies": {
67
- "@deepseek-ai/dsh-compaction": "^0.1.1-rc.2",
68
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
69
- "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
70
- "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
71
- "@deepseek-ai/dsh-output-retention": "^0.1.1-rc.2",
72
- "@deepseek-ai/dsh-session-query": "^0.1.1-rc.2",
73
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
74
- "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.2",
75
- "@deepseek-ai/cordis": "^4.0.1"
77
+ "@deepseek-ai/cordis": "^4.0.2",
78
+ "@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
79
+ "@deepseek-ai/dsh-compaction": "^0.1.2-alpha.2",
80
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
81
+ "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
82
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
83
+ "@deepseek-ai/dsh-session-projection-cache": "^0.1.2-alpha.2",
84
+ "@deepseek-ai/dsh-session-query": "^0.1.2-alpha.2",
85
+ "@deepseek-ai/dsh-session-title": "^0.1.2-alpha.2",
86
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.2",
87
+ "@deepseek-ai/dsh-output-retention": "^0.1.2-alpha.2",
88
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2"
76
89
  }
77
90
  }