@zhushanwen/pi-session-reader 0.11.0 → 0.13.0

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.
Files changed (42) hide show
  1. package/package.json +5 -5
  2. package/src/__tests__/archive-ledger.test.ts +447 -0
  3. package/src/__tests__/cross-package-subagent-core.test.ts +34 -25
  4. package/src/__tests__/entry-anchor.test.ts +3 -3
  5. package/src/__tests__/execution-tree.test.ts +85 -86
  6. package/src/__tests__/family-zcode.test.ts +24 -14
  7. package/src/__tests__/family.test.ts +4 -4
  8. package/src/__tests__/find.test.ts +50 -5
  9. package/src/__tests__/index.test.ts +3 -3
  10. package/src/__tests__/rebuild-manifest-dual-write.test.ts +29 -22
  11. package/src/__tests__/render.test.ts +2 -2
  12. package/src/__tests__/result.test.ts +20 -11
  13. package/src/__tests__/roots.test.ts +64 -26
  14. package/src/__tests__/subagents.test.ts +108 -52
  15. package/src/__tests__/tool-handler.test.ts +133 -83
  16. package/src/__tests__/workflow.test.ts +5 -5
  17. package/src/__tests__/zcode-manifest.test.ts +18 -10
  18. package/src/__tests__/zcode-routing.test.ts +21 -19
  19. package/src/__tests__/zcode-whitelist.test.ts +1 -1
  20. package/src/core/family.ts +6 -6
  21. package/src/core/render.ts +2 -2
  22. package/src/core/toolcall.ts +1 -1
  23. package/src/core/workflow.ts +9 -9
  24. package/src/discovery/archive-ledger.ts +221 -0
  25. package/src/discovery/archive-match.ts +84 -0
  26. package/src/discovery/entry-anchor.ts +5 -5
  27. package/src/discovery/find.ts +12 -1
  28. package/src/discovery/roots.ts +149 -14
  29. package/src/discovery/subagents.ts +24 -17
  30. package/src/discovery/whitelist.ts +4 -4
  31. package/src/discovery/workflows.ts +8 -8
  32. package/src/discovery/zcode-manifest.ts +22 -27
  33. package/src/doctor.ts +3 -0
  34. package/src/extract.ts +1 -1
  35. package/src/index.ts +5 -2
  36. package/src/no-match.ts +29 -4
  37. package/src/result-action.ts +7 -1
  38. package/src/search-across.ts +3 -3
  39. package/src/tool-format.ts +24 -13
  40. package/src/tool-handler.ts +68 -29
  41. package/src/tui/session-command.ts +1 -1
  42. package/src/zcode-anchor-classify.ts +2 -2
@@ -2,6 +2,7 @@ import { readFile, readdir } from 'node:fs/promises'
2
2
  import { statSync } from 'node:fs'
3
3
  import { join } from 'node:path'
4
4
 
5
+ import { resolveSessionStoreRoot, listSessionStorePartitions } from './roots.js'
5
6
  import type { ZcodeAnchor } from './entry-anchor.js'
6
7
  import type { ZcodeFamilyNode } from '../core/family.js'
7
8
 
@@ -9,9 +10,9 @@ import type { ZcodeFamilyNode } from '../core/family.js'
9
10
  * zcode manifest 直读与枚举(design session-reader-shared-core §3.1 第①步 / D5 / D5-1)。
10
11
  *
11
12
  * 背景(F21):pi 路径的 `isRecordManifest` 三必填含 `sessionFile`,而 zcode manifest
12
- * 落盘时该键被 JSON.stringify 丢弃(subagent-core record-store-rebuild.ts:665/725 投影
13
+ * 落盘时该键被 JSON.stringify 丢弃(subagent-core record-store-rebuild.ts:665/725 派生视图
13
14
  * 照写 `sessionFile: undefined`)→ `listRecordManifests` 对 zcode manifest 恒返回 []。
14
- * 本单元另开一条**不经过该守卫**的独立路径:
15
+ * 本单元另开一条**不经过该检查**的独立路径:
15
16
  * - **定点直读**(`readZcodeManifest`):manifest 文件名由 sa-id 完全确定(写侧
16
17
  * `path.join(recordsDir, \`${record.id}.json\`)`,manifest-store.ts:141/153),按确定性
17
18
  * 文件名 `<sa-id>.json` 在 `<agentDir>/subagents/<cwd>/records/` 下**窄 walk(命中即止,
@@ -24,7 +25,7 @@ import type { ZcodeFamilyNode } from '../core/family.js'
24
25
  * `listRecordManifests`(subagents.ts)一行不改——pi manifest 仍走原扫描路径,本单元
25
26
  * 的谓词**不走** isRecordManifest。判别规则与 runtime `transcriptAnchorOf` 同构
26
27
  * (F8:`engine === 'zcode'` 第一判据 + sessionRef 双键齐)。用 `engine` 判别而非
27
- * 「engineHandle 存在性」的理由见 D5 被否项 ②(pi record 提升后 engineHandle 仍持久化,
28
+ * 「engineHandle 存在性」的理由见 D5 不采用项 ②(pi record 提升后 engineHandle 仍持久化,
28
29
  * 存在性判别会误路由)。
29
30
  */
30
31
 
@@ -43,7 +44,7 @@ export interface ZcodeManifestRecord extends ZcodeFamilyNode {
43
44
  /**
44
45
  * anchor-missing 的缺失键归因(§3.4:缺失键名进结构化日志——engineHandle 整体缺席 /
45
46
  * 缺 sessionRef / 缺 sessionId / 缺 dbPath 归因不同:整体缺席多为旧版本产物,缺
46
- * sessionId 多为引擎未回调 onHandleReady,缺 dbPath 多为写入面截断)。entry 兜底侧
47
+ * sessionId 多为引擎未回调 onHandleReady,缺 dbPath 多为写侧截断)。entry 兜底侧
47
48
  * 的同构分类(tool-handler classifyIncompleteEntryAnchor)复用本类型单源。
48
49
  */
49
50
  export type ZcodeAnchorMissingReason =
@@ -67,7 +68,7 @@ export type ZcodeManifestLookup =
67
68
  | { kind: 'not-zcode' }
68
69
  | { kind: 'anchor-missing'; reason: ZcodeAnchorMissingReason }
69
70
 
70
- /** manifest 顶层 JSON 的读取视图(守卫输入) */
71
+ /** manifest 顶层 JSON 的读取视图(检查输入) */
71
72
  type ManifestJson = Record<string, unknown>
72
73
 
73
74
  function asObject(v: unknown): ManifestJson | undefined {
@@ -125,7 +126,7 @@ function richFieldsOf(m: ManifestJson): ManifestRichFields {
125
126
  return out
126
127
  }
127
128
 
128
- /** stat 存在性(任意路径无安全面——D5-1 只查「文件在不在」,不开库) */
129
+ /** stat 存在性(任意路径无安全侧——D5-1 只查「文件在不在」,不开库) */
129
130
  function fileExists(p: string): boolean {
130
131
  try {
131
132
  statSync(p)
@@ -161,7 +162,13 @@ function toZcodeManifestRecord(m: ManifestJson): ZcodeManifestRecord | undefined
161
162
  }
162
163
  }
163
164
 
164
- /** `<agentDir>/subagents` 布局常量(与写侧 path-encoding/manifest-store 布局对齐) */
165
+ /**
166
+ * [session-store-layout-inversion D6/单新根] zcode manifest 读侧 = session 存储树分区
167
+ * 固定位置 `<root>/<rootSessionId>/subagents/records/`(根推导与分区枚举复用 roots.ts
168
+ * 的 resolveSessionStoreRoot / listSessionStorePartitions——同包单源防漂移)。旧布局
169
+ * `<agentDir>/subagents/<cwdSlug>/records/` 不再读写(D6 存量不迁移不读取——旧 zcode
170
+ * record 从 agent 面消失是 D6 取舍清单明示项)。
171
+ */
165
172
  const SUBAGENTS_DIR = 'subagents'
166
173
  const RECORDS_DIR = 'records'
167
174
 
@@ -196,22 +203,16 @@ export async function readZcodeManifest(
196
203
  agentDir: string,
197
204
  saId: string,
198
205
  ): Promise<ZcodeManifestLookup> {
199
- const root = join(agentDir, SUBAGENTS_DIR)
200
- let slugs: DirentLike[]
201
- try {
202
- slugs = await readdir(root, { withFileTypes: true })
203
- } catch {
204
- return { kind: 'not-zcode' } // subagents 根不存在/不可读 → 无 manifest
205
- }
206
- for (const e of slugs) {
207
- if (!e.isDirectory()) continue
208
- const file = join(root, e.name, RECORDS_DIR, `${saId}.json`)
206
+ const root = resolveSessionStoreRoot(agentDir)
207
+ const partitions = await listSessionStorePartitions(root)
208
+ for (const part of partitions) {
209
+ const file = join(root, part, SUBAGENTS_DIR, RECORDS_DIR, `${saId}.json`)
209
210
  let raw: string
210
211
  try {
211
212
  // 命中即止:只读这一个文件(文件名由 sa-id 确定),坏内容同样终止(见 parseManifestLookup)
212
213
  raw = await readFile(file, 'utf8')
213
214
  } catch {
214
- continue // 该 cwd 目录下无此 manifest → 下一候选目录
215
+ continue // 该分区无此 manifest → 下一候选分区
215
216
  }
216
217
  return parseManifestLookup(raw)
217
218
  }
@@ -236,17 +237,11 @@ interface DirentLike {
236
237
  * family 是列表视图,单节点不可达不炸整屏(D5-1 GC 语义)。
237
238
  */
238
239
  export async function listZcodeManifests(agentDir: string): Promise<ZcodeManifestRecord[]> {
239
- const root = join(agentDir, SUBAGENTS_DIR)
240
+ const root = resolveSessionStoreRoot(agentDir)
240
241
  const out: ZcodeManifestRecord[] = []
241
- let slugs: DirentLike[]
242
- try {
243
- slugs = await readdir(root, { withFileTypes: true })
244
- } catch {
245
- return out // 根不存在/不可读 → 空(无 zcode 节点,不报错)
246
- }
242
+ const slugs = await listSessionStorePartitions(root)
247
243
  for (const slug of slugs) {
248
- if (!slug.isDirectory()) continue
249
- const recordsDir = join(root, slug.name, RECORDS_DIR)
244
+ const recordsDir = join(root, slug, SUBAGENTS_DIR, RECORDS_DIR)
250
245
  let files: DirentLike[]
251
246
  try {
252
247
  files = await readdir(recordsDir, { withFileTypes: true })
package/src/doctor.ts CHANGED
@@ -93,6 +93,9 @@ function renderDoctor(
93
93
  // 仅 subagents:'stat' 的 subagent 根(doctor 默认形态)——其余根恒有计数(实扫);
94
94
  // 被去重根已在上方提前返回
95
95
  facts.push('未扫描(subagent 根默认不扫,includeSubagents:true 开启)')
96
+ } else if (r.kind === 'archive') {
97
+ // [session-archive D7] 归档根计数 = 簿记数(非 .jsonl 文件数,措辞按实名)
98
+ facts.push(`${r.fileCount} 归档单元`, `扫 ${Math.round(r.scanMs ?? 0)}ms`)
96
99
  } else {
97
100
  facts.push(`${r.fileCount} 文件`, `扫 ${Math.round(r.scanMs ?? 0)}ms`)
98
101
  }
package/src/extract.ts CHANGED
@@ -347,7 +347,7 @@ function collectCommitCandidates(
347
347
  return { high, low }
348
348
  }
349
349
 
350
- /** 收口阶段:去重,高置信优先,同 hash 保留首次。 */
350
+ /** 收尾阶段:去重,高置信优先,同 hash 保留首次。 */
351
351
  function dedupeCommits(high: CommitItem[], low: CommitItem[]): CommitItem[] {
352
352
  const seen = new Set<string>()
353
353
  const items: CommitItem[] = []
package/src/index.ts CHANGED
@@ -156,9 +156,12 @@ const guidelines = [
156
156
  // 工具内指向本工具的交叉引用,非本清单的锚)。
157
157
  'Progressive reading: outline (~1500 tokens) → expand (one turn) → detail; defaults omit toolResult/thinking noise.',
158
158
  'find first (uuid fragment / name); TUI #references are full uuids. Never read raw .jsonl—use this tool.',
159
- // zcode 路由(U9)对 LLM 的唯一可见面:sa-id 输入自动路由,无需任何参数;首遇
159
+ // zcode 路由(U9)对 LLM 的唯一可见部分:sa-id 输入自动路由,无需任何参数;首遇
160
160
  // zcode_* 错误码前先有背景预告。
161
161
  'sa- ids route transparently to pi or zcode engine sessions; sess_ prefixed ids are not accepted—use the completion-notice sa- id.',
162
+ // [session-archive D7] 归档态硬契约的 LLM 侧预告:引用已归档 session 只得摘要
163
+ //(返回头部显式标注「已归档」),恢复完整历史是用户操作。
164
+ 'Archived sessions return summary-only views (marked 已归档/archived)—for full history, ask the user to restore the session.',
162
165
  'Errors carry a 👉 recovery hint—follow it to retry.',
163
166
  'Unsure about host environment or session roots? Run action:"doctor".',
164
167
  ]
@@ -250,7 +253,7 @@ export default function sessionReaderExtension(pi: ExtensionAPI): void {
250
253
  //
251
254
  // addAutocompleteProvider 挂在 ctx.ui(非 ExtensionAPI),setup 入口无 ctx,
252
255
  // 只能在 event handler 里拿——session_start 是最早且每 session 触发的 event。
253
- // once-guard + ctx.mode 守卫 + typeof 运行时守卫三重防护。
256
+ // once-guard + ctx.mode 检查 + typeof 运行时检查三重防护。
254
257
  //
255
258
  // 2026-08-10 重构:数据源从全盘 findSessions(agentDir) 换为 SessionManager.listAll(ctx.sessionManager.getSessionDir())。
256
259
  // getSessionDir() 返回当前 session 的目录(encoded cwd),listAll 只扫该目录 →
package/src/no-match.ts CHANGED
@@ -62,6 +62,13 @@ function buildSelfCheckLines(roots: SessionRoot[]): string[] {
62
62
  continue
63
63
  }
64
64
  const count = r.fileCount ?? r.files.length
65
+ // [session-archive D2 U2] 归档根不计入 find 候选集合计(find 只迭代在场 .jsonl,
66
+ // 归档根 files 恒空、fileCount = 簿记数——计入则「候选集非空」陈述失真);
67
+ // 措辞对齐 doctor 的「N 归档单元」实名(区别于 .jsonl 文件计数)。
68
+ if (r.kind === 'archive') {
69
+ lines.push(` [${r.kind}]`.padEnd(F1_ROOT_LABEL_PAD) + `${r.path}:${count} 归档单元`)
70
+ continue
71
+ }
65
72
  totalFiles += count
66
73
  lines.push(` [${r.kind}]`.padEnd(F1_ROOT_LABEL_PAD) + `${r.path}:${count} 文件`)
67
74
  }
@@ -117,8 +124,15 @@ function formatSuggestionLines(suggestions: NoMatchSuggestion[], query: string):
117
124
  return lines
118
125
  }
119
126
 
120
- /** ③ 正确做法四条 + ④ 封死 shell 绕行(纯静态文案)。 */
121
- function guidanceLines(): string[] {
127
+ /**
128
+ * ③ 正确做法四条 + ④ 封死 shell 绕行(纯静态文案)。
129
+ *
130
+ * [session-archive D7] archiveHint:归档根存在且有簿记时由 formatNoMatch 追加第
131
+ * 五条(目标可能已归档的引用指引)——find 结果不含归档条目,归档引用通道是深读
132
+ * action 的 session 参数(resolveByFragment 的归档回退层)。无归档目录 → 空数组,
133
+ * 文案零变化。
134
+ */
135
+ function guidanceLines(archiveHint: string[] = []): string[] {
122
136
  return [
123
137
  '',
124
138
  '正确做法:',
@@ -126,6 +140,7 @@ function guidanceLines(): string[] {
126
140
  ` - 已知文件路径时直接传绝对路径:session_read { action:"outline", session:"<绝对路径>.jsonl" }`,
127
141
  ` - 截断/过期 id 用更短前缀:session_read { action:"find", query:"<更短前缀>" }`,
128
142
  ` - 想先看环境与根目录状态:session_read { action:"doctor" }`,
143
+ ...archiveHint,
129
144
  '',
130
145
  '不要用 shell find/ls/rg 搜 session 目录,不要 cat/read 原始 .jsonl —— session_read 是唯一入口。',
131
146
  ]
@@ -147,15 +162,25 @@ function guidanceLines(): string[] {
147
162
  * ③「正确做法」四条:标题/keyword、绝对路径 outline、更短前缀、doctor。
148
163
  * ④ 最后一行封死 shell 绕行:find/ls/rg 搜 session 目录、cat/read 原始 .jsonl 均禁止。
149
164
  *
150
- * 无「👉 用 recent 看全量」误导指引(recent 候选对写错的 uuid 无自纠价值,§6.7 被否项)。
165
+ * 无「👉 用 recent 看全量」误导指引(recent 候选对写错的 uuid 无自纠价值,§6.7 不采用项)。
151
166
  */
152
167
  export function formatNoMatch(query: string, roots: SessionRoot[]): string {
168
+ // [session-archive D7] 归档指引(条件行):归档根存在且簿记非空才出现——自检行
169
+ // 已列 [archive] 根计数,此处补「怎么引用」的动作指引,find→深读链路对归档
170
+ // session 不断链。无归档目录的 roots 不含 archive 根,guidanceLines 零变化。
171
+ const archiveRoot = roots.find((r) => r.kind === 'archive' && r.dedupedInto === undefined)
172
+ const archiveHint =
173
+ archiveRoot !== undefined && (archiveRoot.fileCount ?? 0) > 0
174
+ ? [
175
+ ` - 目标可能已归档:直接用 session 参数引用(返回归档摘要),如 session_read { action:"outline", session:"<名称片段或id>" }`,
176
+ ]
177
+ : []
153
178
  return [
154
179
  `无匹配 session:"${query}"`,
155
180
  '',
156
181
  ...buildSelfCheckLines(roots),
157
182
  '',
158
183
  ...formatSuggestionLines(collectSuggestions(query, roots), query),
159
- ...guidanceLines(),
184
+ ...guidanceLines(archiveHint),
160
185
  ].join('\n')
161
186
  }
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import type { Entry, ParseResult } from '@zhushanwen/session-core'
19
19
  import { textBlockParts } from './core/render.js'
20
+ import { archivedResult } from './discovery/archive-match.js'
20
21
  import type { MatchedSession } from './discovery/find.js'
21
22
  import { listRecordManifests, type RecordManifest } from './discovery/subagents.js'
22
23
  import { err, requireStr, SESSION_ID_PREFIX_LEN, stripHash } from './handler-utils.js'
@@ -31,7 +32,7 @@ const RESULT_DEFAULT_LIMIT = 8000
31
32
  /** result 批量条目分隔符(与 notifier 批通知 "\n\n---\n\n" join 同款)。 */
32
33
  const RESULT_BATCH_SEPARATOR = '\n\n---\n\n'
33
34
 
34
- /** result action 对 tool-handler 文件私有 helper 的注入面(入口分发处 per-call 构造)。 */
35
+ /** result action 对 tool-handler 文件私有 helper 的注入点(入口分发处 per-call 构造)。 */
35
36
  export interface ResultActionDeps {
36
37
  resolveSessionId(
37
38
  rawSession: string | undefined,
@@ -208,6 +209,11 @@ export async function doResult(
208
209
  for (const id of ids) {
209
210
  const resolved = await deps.resolveSessionId(id, 'result', agentDir, params.source, prefetchedManifests)
210
211
  if (resolved.kind === 'multi') return deps.disambiguate(resolved.query, resolved.candidates)
212
+ // [session-archive D7] 片段命中归档 session:完整正文不可读,统一返回归档态摘要
213
+ //(构造与渲染收敛在 archive-match 的 archivedResult,与 tool-handler 同一出口)
214
+ if (resolved.kind === 'archived' || resolved.kind === 'archived-multi') {
215
+ return archivedResult(resolved)
216
+ }
211
217
  const { entries } = await deps.safeParse(resolved.fileName)
212
218
  const text = extractFinalAssistantText(entries)
213
219
  if (text.length === 0) {
@@ -137,7 +137,7 @@ export function collectSearchHits(
137
137
  return hits
138
138
  }
139
139
 
140
- /** search 收口阶段:结果文本组装(header 标注降级/scope/截断 + 逐行命中列表)。 */
140
+ /** search 收尾阶段:结果文本组装(header 标注降级/scope/截断 + 逐行命中列表)。 */
141
141
  export function formatSearchText(
142
142
  pattern: string,
143
143
  degraded: boolean,
@@ -196,7 +196,7 @@ interface CrossSearchSkipped {
196
196
  sizeBytes?: number
197
197
  }
198
198
 
199
- /** 跨会话检索 details(程序化消费/测试断言面)。 */
199
+ /** 跨会话检索 details(程序化消费/测试断言范围)。 */
200
200
  export interface CrossSearchDetails {
201
201
  pattern: string
202
202
  scope: NonNullable<SessionReadParams['scope']>
@@ -355,7 +355,7 @@ interface CrossSearchOptions {
355
355
  byteBudget?: number
356
356
  }
357
357
 
358
- /** 缺省值收口后的检索参数(各缺省值与旧内联 `opts?.** ?? 常量` 逐一对应)。 */
358
+ /** 缺省值收尾后的检索参数(各缺省值与旧内联 `opts?.** ?? 常量` 逐一对应)。 */
359
359
  interface CrossSearchResolvedOptions {
360
360
  scope: NonNullable<SessionReadParams['scope']>
361
361
  limit: number
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * session_read 各 action 的输出文本渲染(max-lines 拆分轮从 tool-handler.ts 机械
3
- * 提取,零行为变更):错误面 message(ES1/ES2 + zcode 七码 + 开库/查询期错误映射)+
3
+ * 提取,零行为变更):错误范围 message(ES1/ES2 + zcode 七码 + 开库/查询期错误映射)+
4
4
  * content 文本(find/outline/expand/detail/family)+ entry 可读文本 + F2 消歧与
5
5
  * find 零匹配的 ToolResult 包装。纯字符串/纯渲染函数,不触 fs(findNoMatch 的
6
6
  * roots 由调用方传入)。
7
7
  *
8
- * 导出面:SQLITE_DRIVER_UNSUPPORTED_MARK / ZCODE_DB_MISSING_MARK /
8
+ * 导出清单:SQLITE_DRIVER_UNSUPPORTED_MARK / ZCODE_DB_MISSING_MARK /
9
9
  * zcodeReadErrorMessage(测试契约锚消费方直接 import 本模块)。logger 用同名
10
10
  * getLogger('session-reader')——按名缓存单例,与 tool-handler 侧同一实例,
11
11
  * warn/error 落同一 appendEntry/文件日志通道。ToolResult 等公共类型留
@@ -33,7 +33,8 @@ import {
33
33
  import type { Family } from './core/family.js'
34
34
  import { SESSION_ID_PREFIX_LEN, rangeLabel } from './handler-utils.js'
35
35
  import { formatNoMatch } from './no-match.js'
36
- import type { ToolResult } from './tool-handler.js'
36
+ import { archivedResult } from './discovery/archive-match.js'
37
+ import type { ResolveResult, ToolResult } from './tool-handler.js'
37
38
 
38
39
  // 结构化日志通道(zcodeReadErrorMessage 的降级/TOCTOU warn):getLogger 按名缓存
39
40
  // 单例,与 tool-handler 侧调用取同一实例,appendEntry/文件日志行为不变。
@@ -84,7 +85,7 @@ export function formatSaIdNotFound(saId: string): string {
84
85
  }
85
86
 
86
87
  // ---------------------------------------------------------------------------
87
- // zcode 错误面(U9,design §3.4 七码 + 👉 逐行;错误码标识入 message 供机器识别,
88
+ // zcode 错误范围(U9,design §3.4 七码 + 👉 逐行;错误码标识入 message 供机器识别,
88
89
  // 形态对齐 §3.1 失败路径示例 `[zcode_db_path_forbidden] …`)
89
90
  // ---------------------------------------------------------------------------
90
91
 
@@ -131,7 +132,7 @@ function formatZcodeSchemaDrift(observed: string | undefined): string {
131
132
 
132
133
  /**
133
134
  * bun 驱动探测失败(§3.4 表第 8 行理论态)的 message:宿主既无 bun:sqlite 也无
134
- * node:sqlite——zcode action 整体降级为本错误面(fail-fast + 日志),pi 链路不受影响。
135
+ * node:sqlite——zcode action 整体降级为本错误范围(fail-fast + 日志),pi 链路不受影响。
135
136
  * 错误码字面量为实施期命名(设计该行未定码名,形态对齐七码 `[zcode_*]` 惯例)。
136
137
  */
137
138
  function formatZcodeHostUnsupported(detail: string): string {
@@ -145,8 +146,8 @@ function formatZcodeHostUnsupported(detail: string): string {
145
146
  /**
146
147
  * sqlite 驱动探测失败的消息特征(zcode-session-source sqlite-driver.ts 探测失败的
147
148
  * 错误消息契约;该包不导出专用错误子类,reader 侧按消息特征单列识别——跨包漂移
148
- * 由 zcode-routing.test.ts 的源文本契约测试守卫:sqlite-driver 源文不再含本子串即红)。
149
- * 导出面仅测试消费(契约锚 + 映射测试引用同值,禁测试内硬编码副本)。
149
+ * 由 zcode-routing.test.ts 的源文本契约测试检查:sqlite-driver 源文不再含本子串即红)。
150
+ * 导出范围仅测试消费(契约锚 + 映射测试引用同值,禁测试内硬编码副本)。
150
151
  */
151
152
  export const SQLITE_DRIVER_UNSUPPORTED_MARK = '不支持 node:sqlite'
152
153
 
@@ -154,17 +155,17 @@ export const SQLITE_DRIVER_UNSUPPORTED_MARK = '不支持 node:sqlite'
154
155
  * 库文件缺失的消息特征(zcode-session-source sqlite-access.ts `openZcodeSessionDb`
155
156
  * 开头 existsSync 失败抛普通 Error「db 文件不存在:<dbPath>」的消息契约锚点)。该包
156
157
  * 不导出专用错误子类,reader 侧按消息特征单列识别——落 §3.4 第 4 行
157
- * `zcode_db_unreadable`(不误标 schema_drift)。生产路径上白名单闸
158
+ * `zcode_db_unreadable`(不误标 schema_drift)。生产路径上白名单门禁
158
159
  * (assertZcodeDbPathAllowed 第 1 段)已先行拦掉绝大多数缺失,此处命中的是两段
159
- * existsSync 之间的 TOCTOU 理论窗口(闸通过后开库前文件被删)。跨包漂移由
160
- * zcode-routing.test.ts 的源文本契约测试守卫(SQLITE_DRIVER_UNSUPPORTED_MARK 同范式)。
161
- * 导出面仅测试消费(契约锚 + 映射测试引用同值,禁测试内硬编码副本)。
160
+ * existsSync 之间的 TOCTOU 理论窗口(门禁通过后开库前文件被删)。跨包漂移由
161
+ * zcode-routing.test.ts 的源文本契约测试检查(SQLITE_DRIVER_UNSUPPORTED_MARK 同范式)。
162
+ * 导出范围仅测试消费(契约锚 + 映射测试引用同值,禁测试内硬编码副本)。
162
163
  */
163
164
  export const ZCODE_DB_MISSING_MARK = 'db 文件不存在'
164
165
 
165
166
  /**
166
- * 开库/查询期错误 → §3.4 错误面映射(SqliteUnreadableError / schema drift / 驱动探测失败 / 库缺失 / 其余)。
167
- * 导出面仅测试消费(L4 包装形态的映射契约直测,zcode-routing.test.ts)。
167
+ * 开库/查询期错误 → §3.4 错误范围映射(SqliteUnreadableError / schema drift / 驱动探测失败 / 库缺失 / 其余)。
168
+ * 导出范围仅测试消费(L4 包装形态的映射契约直测,zcode-routing.test.ts)。
168
169
  */
169
170
  export function zcodeReadErrorMessage(e: unknown, agentDir: string): string {
170
171
  if (e instanceof SqliteUnreadableError) {
@@ -229,6 +230,16 @@ export function disambiguate(query: string, candidates: MatchedSession[]): ToolR
229
230
  return { content: [{ type: 'text', text }], details: { ambiguous: true, candidates } }
230
231
  }
231
232
 
233
+ /**
234
+ * ResolveResult 非 ok 支的统一分发([session-archive D7] 归并点):multi → 既有
235
+ * F2 消歧;archived / archived-multi → 归档态返回(archive-match 两支渲染,不读
236
+ * 压缩体)。8 个 do* 的「multi 消歧 + 归档短路」重复分支收敛到本函数单一出口。
237
+ */
238
+ export function resolveNonOkResult(resolved: Exclude<ResolveResult, { kind: 'ok' }>): ToolResult {
239
+ if (resolved.kind === 'multi') return disambiguate(resolved.query, resolved.candidates)
240
+ return archivedResult(resolved)
241
+ }
242
+
232
243
  // ---------------------------------------------------------------------------
233
244
  // 文本渲染(content)
234
245
  // ---------------------------------------------------------------------------