@zhushanwen/pi-session-reader 0.10.4 → 0.12.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 (41) hide show
  1. package/package.json +7 -6
  2. package/src/__tests__/cross-package-subagent-core.test.ts +34 -25
  3. package/src/__tests__/entry-anchor.test.ts +3 -3
  4. package/src/__tests__/execution-tree.test.ts +85 -86
  5. package/src/__tests__/family-zcode.test.ts +24 -14
  6. package/src/__tests__/family.test.ts +4 -4
  7. package/src/__tests__/find.test.ts +50 -5
  8. package/src/__tests__/index.test.ts +14 -3
  9. package/src/__tests__/rebuild-manifest-dual-write.test.ts +29 -22
  10. package/src/__tests__/render.test.ts +2 -2
  11. package/src/__tests__/result.test.ts +20 -11
  12. package/src/__tests__/roots.test.ts +64 -26
  13. package/src/__tests__/subagents.test.ts +108 -52
  14. package/src/__tests__/summary.test.ts +223 -0
  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/entry-anchor.ts +5 -5
  25. package/src/discovery/find.ts +9 -1
  26. package/src/discovery/roots.ts +95 -7
  27. package/src/discovery/subagents.ts +24 -17
  28. package/src/discovery/whitelist.ts +4 -4
  29. package/src/discovery/workflows.ts +8 -8
  30. package/src/discovery/zcode-manifest.ts +22 -27
  31. package/src/extract.ts +1 -1
  32. package/src/index.ts +12 -4
  33. package/src/no-match.ts +1 -1
  34. package/src/result-action.ts +1 -1
  35. package/src/search-across.ts +3 -3
  36. package/src/summary.ts +178 -0
  37. package/src/tool-format.ts +12 -12
  38. package/src/tool-handler.ts +27 -19
  39. package/src/tui/session-command.ts +1 -1
  40. package/src/tui/tool-render.ts +48 -0
  41. package/src/zcode-anchor-classify.ts +2 -2
@@ -1,11 +1,11 @@
1
1
  import { readFile, readdir, open, stat } from 'node:fs/promises'
2
2
  import type { FileHandle } from 'node:fs/promises'
3
- import { join, basename } from 'node:path'
3
+ import { join } from 'node:path'
4
4
  import type { Entry } from '@zhushanwen/session-core'
5
5
  import type { Family, SessionRef } from '../core/family.js'
6
6
  import { buildFamilyIndex, resolveFamily } from '../core/family.js'
7
7
  import { listZcodeManifests } from './zcode-manifest.js'
8
- import { resolveSessionRoots, type SessionFileMeta } from './roots.js'
8
+ import { resolveSessionRoots, resolveSessionStoreRoot, resolveStoreSubagentRecordsDir, listSessionStorePartitions, type SessionFileMeta } from './roots.js'
9
9
  import { parseSessionHeader, readSessionHeaderFirstLine } from './session-header.js'
10
10
  import { resolveWorkflows } from './workflows.js'
11
11
 
@@ -186,7 +186,8 @@ async function collectSubagentIdentities(
186
186
  // U4 数据流:manifest 命中透全字段;未命中 P-fallback 读尾行 identity 取 task/slug/agent
187
187
  //(model/status 不可回退,留 undefined);无 manifest 无 identity(运行中/异常)跳过。
188
188
  // A3(ext-simplify-04):直调 resolveSessionRoots(原 listSubagentSessions 薄包装已删除)
189
- //——agentDir 信号包下 [subagent] 根 = <agentDir>/subagents(常量推导,恒唯一)
189
+ //——[subagent] 根 = session 存储树([session-store-layout-inversion] 单新根,常量
190
+ // 推导,恒唯一;扫描定向探各 sid 分区的 subagents/sessions/)。
190
191
  const subMetas =
191
192
  (await resolveSessionRoots({ agentDir })).find((r) => r.kind === 'subagent')?.files ?? []
192
193
  for (const meta of subMetas) {
@@ -418,31 +419,37 @@ async function tryReadManifest(path: string): Promise<RecordManifest | undefined
418
419
  }
419
420
 
420
421
  /**
421
- * 扫描 subagents/<cwdSlug>/records/*.json —— subagent 注册清单。
422
- * 每个 manifest 由终态 finalize 或 sync 批落标出口写入(创建时不写),持久存在即使
422
+ * 扫描 records/*.json —— subagent 注册清单(读侧单新根,[session-store-layout-inversion]
423
+ * D6 裁决:旧布局存量原位保留、不再读写——旧 record 从 agent 面 result/find 消失是该
424
+ * 裁决取舍清单明示项,不做旧根兼容扫描)。
425
+ * 每个 manifest 由终态 finalize 或 rebuildIndexes 投影写入(创建时不写),持久存在即使
423
426
  * .jsonl 被 GC。坏 manifest(缺必填字段/JSON 损坏)跳过,不中断扫描。
427
+ *
428
+ * 单新根:`<dataDir>/ext-sessions`(dataDir = dirname(agentDir),布局 B agentDir =
429
+ * `<dataDir>/agent`;树根段镜像 shared SESSION_STORE_DIR_SEGMENT,对拍机检锚点 =
430
+ * scripts/check-artifact-dir-formula-sync.mjs 域 9)。新布局 records 在
431
+ * `<root>/<sid>/subagents/records/`(分区键 = rootSessionId,嵌套派发全层同值——
432
+ * 嵌套 subagent 的 manifest 同落根会话分区,固定位置扫描完整覆盖)。扫描剪枝:
433
+ * `<root>/` 一级只进 isPiSessionId 形态目录(不进 logs/attachments 等大目录),每个
434
+ * sid 分区内只探 subagents/records/ 固定位置,不整树递归。根不存在时静默空读。
424
435
  */
425
436
  export async function listRecordManifests(agentDir: string): Promise<RecordManifest[]> {
426
- const root = join(agentDir, 'subagents')
437
+ const storeRoot = resolveSessionStoreRoot(agentDir)
427
438
  const out: RecordManifest[] = []
428
- async function walk(dir: string): Promise<void> {
439
+ for (const sid of await listSessionStorePartitions(storeRoot)) {
440
+ const recordsDir = resolveStoreSubagentRecordsDir(agentDir, sid)
429
441
  let entries
430
442
  try {
431
- entries = await readdir(dir, { withFileTypes: true })
443
+ entries = await readdir(recordsDir, { withFileTypes: true })
432
444
  } catch {
433
- return // 目录不存在/无权限 → 静默返回
445
+ continue // 该分区无 records/(无 subagent 的会话)→ 静默跳过
434
446
  }
435
447
  for (const e of entries) {
436
- const full = join(dir, e.name)
437
- if (e.isDirectory()) {
438
- await walk(full)
439
- } else if (e.isFile() && e.name.endsWith('.json') && basename(dir) === 'records') {
440
- const m = await tryReadManifest(full)
441
- if (m) out.push(m)
442
- }
448
+ if (!e.isFile() || !e.name.endsWith('.json')) continue
449
+ const m = await tryReadManifest(join(recordsDir, e.name))
450
+ if (m) out.push(m)
443
451
  }
444
452
  }
445
- await walk(root)
446
453
  return out
447
454
  }
448
455
 
@@ -16,8 +16,8 @@ import { zcodeImportDbAllowlist } from '@zhushanwen/zcode-session-source'
16
16
  *
17
17
  * **检查顺序铁律(§3.4 三段递进,实现与测试双锚定)**:存在性先于路径——
18
18
  * `existsSync` 为 false 报 `zcode_db_unreadable`(库文件不存在)而非
19
- * `zcode_db_path_forbidden`,避免把「只是还没落库」误报成安全事件(forbidden 是
20
- * 安全面错误,语义更重、更惊扰 agent)。存在才 realpath 后与派生允许集合精确比对。
19
+ * `zcode_db_path_forbidden`,避免把「只是还没写入仓库」误报成安全事件(forbidden 是
20
+ * 安全侧错误,语义更重、更惊扰 agent)。存在才 realpath 后与派生允许集合精确比对。
21
21
  * 开库(第 3 段)由调用方在本闸通过后进行。
22
22
  *
23
23
  * **TOCTOU 残留(设计 §3.4 显式登记,不在本梯队硬化)**:realpath 之后仍按原
@@ -72,10 +72,10 @@ export function formatZcodeDbPathForbidden(dbPath: string): string {
72
72
  * 白名单闸(第 1 段存在性 → 第 2 段路径;通过才允许开库)。
73
73
  *
74
74
  * @throws Error(消费侧映射为 zcode_db_unreadable / zcode_db_path_forbidden,
75
- * 消息即 §3.4 的 👉 指引文案——tool-handler 直接 throw 给 pi 错误面)
75
+ * 消息即 §3.4 的 👉 指引文案——tool-handler 直接 throw 给 pi 错误范围)
76
76
  */
77
77
  export function assertZcodeDbPathAllowed(dbPath: string, agentDir: string): void {
78
- // 第 1 段 存在性(先于路径——「还没落库」不是安全事件)
78
+ // 第 1 段 存在性(先于路径——「还没写入仓库」不是安全事件)
79
79
  if (!existsSync(dbPath)) {
80
80
  throw new Error(formatZcodeDbUnreadable(dirname(agentDir), `(库文件不存在:${dbPath})`))
81
81
  }
@@ -17,7 +17,7 @@ import { extractSessionIdFromFilename } from './subagents.js'
17
17
  // ——家族链数据源同 record 流,无第二承载位);旧后缀锚点(.events.jsonl 形态)=
18
18
  // 历史实体([D1] 历史数据处置:旧两件不读)→ calls=[] 仅 run 存在性兜底。
19
19
  // 历史格式条目(v1 全量快照 / workflow-state-link 指针)不识别、静默从发现链消失
20
- // (不拒读、不报错)——run 侧 v1 读面已删除(ADR-0095,同 record 侧裁决:项目未
20
+ // (不拒读、不报错)——run 侧 v1 读侧已删除(ADR-0095,同 record 侧裁决:项目未
21
21
  // 上线无历史数据,不迁移不兼容)。
22
22
  // - readRunSnapshot:读 wf-state 文件尾向找首个可解析行,返回原始对象(unknown,格式
23
23
  // 收窄交 core 层)。唯一消费方 = tool-handler workflow 概览的快照链分支。
@@ -82,16 +82,16 @@ function sessionRefFromPath(path: string, pathToRef: Map<string, SessionRef>): S
82
82
  }
83
83
 
84
84
  // ============================================================
85
- // workflow-record 条目契约(v2 注册条目判别——本地字面量 + 测试守卫漂移)
85
+ // workflow-record 条目契约(v2 注册条目判别——本地字面量 + 测试检查漂移)
86
86
  // ============================================================
87
87
 
88
88
  /**
89
89
  * `workflow-record` custom entry 的 customType 值。
90
90
  *
91
91
  * 磁盘 JSONL 协议字符串(跨侧契约):写侧单源 = subagent-core
92
- * workflow-record-entry.ts 的 WORKFLOW_RECORD_CUSTOM_TYPE。reader 生产依赖面不引
92
+ * workflow-record-entry.ts 的 WORKFLOW_RECORD_CUSTOM_TYPE。reader 生产依赖范围不引
93
93
  * subagent-core(同 entry-anchor.ts 的协议字面量模式),两侧漂移由 workflow.test.ts
94
- * 的跨包断言守卫。
94
+ * 的跨包断言检查。
95
95
  */
96
96
  const WORKFLOW_RECORD_CUSTOM_TYPE = 'workflow-record'
97
97
 
@@ -102,7 +102,7 @@ const WORKFLOW_RECORD_ENTRY_V2 = 2
102
102
  * run record 流文件名尾段(`<runId>.record.jsonl`)。写侧 subagent-core
103
103
  * RUN_EVENTS_SUFFIX 同值([D1] record 单源后缀)——v2 注册条目的
104
104
  * recordPath 锚点后缀判定用(新锚点直读 record 流;旧 `.events.jsonl` 锚点 =
105
- * 历史实体分流),本地持有 + 测试守卫漂移。
105
+ * 历史实体分流),本地持有 + 测试检查漂移。
106
106
  */
107
107
  export const RUN_RECORD_STREAM_SUFFIX = '.record.jsonl'
108
108
 
@@ -142,7 +142,7 @@ function collectRegisteredRecordPaths(entries: readonly Entry[]): RegisteredReco
142
142
  /**
143
143
  * [D16③] recordPath 后缀分流:`.record.jsonl` 锚点 = 新形态(record 流直读);
144
144
  * 其余(含旧 `.events.jsonl` 锚点)= 历史实体([D1] 历史数据处置——旧两件不读
145
- * 不写,历史 run 从发现链数据面退空即预期行为)。
145
+ * 不写,历史 run 从发现链的数据中退空即预期行为)。
146
146
  */
147
147
  function isNewRecordStreamAnchor(recordPath: string): boolean {
148
148
  return recordPath.endsWith(RUN_RECORD_STREAM_SUFFIX)
@@ -155,7 +155,7 @@ interface AgentSettledSessionFileView {
155
155
  result: { sessionFile: string }
156
156
  }
157
157
 
158
- /** [extractRecordStreamSessionFiles 守卫] 单帧是否为携带非空 result.sessionFile
158
+ /** [extractRecordStreamSessionFiles 检查] 单帧是否为携带非空 result.sessionFile
159
159
  * 的 agent-settled 帧:逐字段 typeof 校验(type → taskIndex → result →
160
160
  * sessionFile),不过关 = 宽容跳过(与 readRunSnapshot 同容忍度)。 */
161
161
  function isAgentSettledSessionFileFrame(v: unknown): v is AgentSettledSessionFileView {
@@ -194,7 +194,7 @@ export function extractRecordStreamSessionFiles(content: string): string[] {
194
194
  /**
195
195
  * 读目标 session 文件全文,从 workflow-record v2 注册条目构造 WorkflowRef[]
196
196
  * (单档发现链——v1 快照条目与 workflow-state-link 指针条目不再识别,历史 run
197
- * 从发现链数据面退空即预期行为,见 ADR-0095)。同一 runId 的多条目后写覆盖
197
+ * 从发现链的数据中退空即预期行为,见 ADR-0095)。同一 runId 的多条目后写覆盖
198
198
  * (条目顺序 = 时间顺序,取最新锚点)。
199
199
  *
200
200
  * **签名与返回值结构(WorkflowRef[]{runId,stateFile,calls:SessionRef[]})不变**
@@ -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/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
@@ -1,12 +1,14 @@
1
- import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent'
1
+ import type { ExtensionAPI, ExtensionContext, Theme } from '@earendil-works/pi-coding-agent'
2
2
  import { getAgentDir, SessionManager } from '@earendil-works/pi-coding-agent'
3
3
  import { StringEnum } from '@earendil-works/pi-ai'
4
4
  import { Type } from 'typebox'
5
5
  import { setPiHandle } from '@zhushanwen/pi-extension-logger'
6
6
  import type { SessionMetadataProvider } from './discovery/find.js'
7
+ import { withResultSummary } from './summary.js'
7
8
  import { handleSessionRead, type SessionReadParams, type SessionReadSignals } from './tool-handler.js'
8
9
  import { createHashAutocompleteProvider } from './tui/hash-provider.js'
9
10
  import { createSessionCommand } from './tui/session-command.js'
11
+ import { renderSessionReadResult } from './tui/tool-render.js'
10
12
 
11
13
  /**
12
14
  * pi-session-reader extension 入口(M3 工具适配层)。
@@ -154,7 +156,7 @@ const guidelines = [
154
156
  // 工具内指向本工具的交叉引用,非本清单的锚)。
155
157
  'Progressive reading: outline (~1500 tokens) → expand (one turn) → detail; defaults omit toolResult/thinking noise.',
156
158
  'find first (uuid fragment / name); TUI #references are full uuids. Never read raw .jsonl—use this tool.',
157
- // zcode 路由(U9)对 LLM 的唯一可见面:sa-id 输入自动路由,无需任何参数;首遇
159
+ // zcode 路由(U9)对 LLM 的唯一可见部分:sa-id 输入自动路由,无需任何参数;首遇
158
160
  // zcode_* 错误码前先有背景预告。
159
161
  'sa- ids route transparently to pi or zcode engine sessions; sess_ prefixed ids are not accepted—use the completion-notice sa- id.',
160
162
  'Errors carry a 👉 recovery hint—follow it to retry.',
@@ -232,7 +234,13 @@ export default function sessionReaderExtension(pi: ExtensionAPI): void {
232
234
  }
233
235
  // u11:标题元数据 provider 随信号包注入(provider 抛错由发现层单目录降级,
234
236
  // 不会变成工具错误——宿主 listAll 异常不得打断 find 的「能找到 session」底线)。
235
- return handleSessionRead(params, signals, signal, metadataProvider)
237
+ const result = await handleSessionRead(params, signals, signal, metadataProvider)
238
+ // 收起态一行摘要:details 追加 summary(现有字段保留),GUI 收起态与 TUI 折叠态共用
239
+ return withResultSummary(params, result)
240
+ },
241
+
242
+ renderResult(result: unknown, options: { expanded: boolean }, theme: Theme) {
243
+ return renderSessionReadResult(result, options, theme)
236
244
  },
237
245
  })
238
246
 
@@ -242,7 +250,7 @@ export default function sessionReaderExtension(pi: ExtensionAPI): void {
242
250
  //
243
251
  // addAutocompleteProvider 挂在 ctx.ui(非 ExtensionAPI),setup 入口无 ctx,
244
252
  // 只能在 event handler 里拿——session_start 是最早且每 session 触发的 event。
245
- // once-guard + ctx.mode 守卫 + typeof 运行时守卫三重防护。
253
+ // once-guard + ctx.mode 检查 + typeof 运行时检查三重防护。
246
254
  //
247
255
  // 2026-08-10 重构:数据源从全盘 findSessions(agentDir) 换为 SessionManager.listAll(ctx.sessionManager.getSessionDir())。
248
256
  // getSessionDir() 返回当前 session 的目录(encoded cwd),listAll 只扫该目录 →
package/src/no-match.ts CHANGED
@@ -147,7 +147,7 @@ function guidanceLines(): string[] {
147
147
  * ③「正确做法」四条:标题/keyword、绝对路径 outline、更短前缀、doctor。
148
148
  * ④ 最后一行封死 shell 绕行:find/ls/rg 搜 session 目录、cat/read 原始 .jsonl 均禁止。
149
149
  *
150
- * 无「👉 用 recent 看全量」误导指引(recent 候选对写错的 uuid 无自纠价值,§6.7 被否项)。
150
+ * 无「👉 用 recent 看全量」误导指引(recent 候选对写错的 uuid 无自纠价值,§6.7 不采用项)。
151
151
  */
152
152
  export function formatNoMatch(query: string, roots: SessionRoot[]): string {
153
153
  return [
@@ -31,7 +31,7 @@ const RESULT_DEFAULT_LIMIT = 8000
31
31
  /** result 批量条目分隔符(与 notifier 批通知 "\n\n---\n\n" join 同款)。 */
32
32
  const RESULT_BATCH_SEPARATOR = '\n\n---\n\n'
33
33
 
34
- /** result action 对 tool-handler 文件私有 helper 的注入面(入口分发处 per-call 构造)。 */
34
+ /** result action 对 tool-handler 文件私有 helper 的注入点(入口分发处 per-call 构造)。 */
35
35
  export interface ResultActionDeps {
36
36
  resolveSessionId(
37
37
  rawSession: string | undefined,
@@ -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
package/src/summary.ts ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * session_read 工具的收起态一行摘要(GUI details.summary + TUI renderResult 共用)。
3
+ *
4
+ * 纯函数:按 params.action 经分发表派发到各 action 的 summarizer 小函数,从
5
+ * result.details 容错取数(details 形态随 action 与历史 entry 版本漂移,逐字段
6
+ * typeof/Array.isArray 守卫,取不到的后缀省略)。文案与 content 正文风格一致
7
+ * (动作 + 数量/目标),一行英文,不带逐条详情。
8
+ * 每个 action 独立函数(圈复杂度门禁:单函数 ≤15;曾以单函数全内联 11-action
9
+ * switch 达 44 被门禁拦截后拆分)。
10
+ */
11
+
12
+ import type { SessionReadAction, SessionReadParams, ToolResult } from './tool-handler.js'
13
+ import { pad } from './handler-utils.js'
14
+
15
+ // ---- details 容错取数原语(details 是 unknown,逐字段守卫替代裸断言)----
16
+
17
+ function asRecord(v: unknown): Record<string, unknown> {
18
+ return typeof v === 'object' && v !== null ? (v as Record<string, unknown>) : {}
19
+ }
20
+
21
+ function numOf(v: unknown): number | undefined {
22
+ return typeof v === 'number' ? v : undefined
23
+ }
24
+
25
+ function strOf(v: unknown): string | undefined {
26
+ return typeof v === 'string' ? v : undefined
27
+ }
28
+
29
+ function arrOf(v: unknown): unknown[] | undefined {
30
+ return Array.isArray(v) ? v : undefined
31
+ }
32
+
33
+ function countOf(details: Record<string, unknown>, key: string): number {
34
+ return arrOf(details[key])?.length ?? 0
35
+ }
36
+
37
+ /** 截断标注(find/search/result 的 truncated 字段在场且为真时追加) */
38
+ function truncatedSuffix(details: Record<string, unknown>): string {
39
+ return details.truncated === true ? ' (truncated)' : ''
40
+ }
41
+
42
+ /** session 未解析出显式值时的占位(params.session 可选字段缺省 → 空串) */
43
+ function sessionLabel(params: SessionReadParams): string {
44
+ return params.session ?? ''
45
+ }
46
+
47
+ // ---- 各 action 的 summarizer(签名统一,经 ACTION_SUMMARIZERS 分发)----
48
+
49
+ function summarizeFind(params: SessionReadParams, details: Record<string, unknown>): string {
50
+ const query = params.query ?? ''
51
+ const matches = countOf(details, 'matches')
52
+ if (matches === 0) return `No sessions matching "${query}"`
53
+ return `Found ${matches} sessions matching "${query}"${truncatedSuffix(details)}`
54
+ }
55
+
56
+ function summarizeFamily(params: SessionReadParams, details: Record<string, unknown>): string {
57
+ // recursive=true 的 details 是 { tree };flat 的 details 是 Family(root/parents/forks/subagents/workflows)
58
+ if (details.tree !== undefined) return `Execution tree of session ${sessionLabel(params)}`
59
+ const relations =
60
+ countOf(details, 'parents') + countOf(details, 'forks') + countOf(details, 'subagents') + countOf(details, 'workflows')
61
+ return `Family of session ${sessionLabel(params)}: ${relations} relations`
62
+ }
63
+
64
+ function summarizeOutline(params: SessionReadParams, details: Record<string, unknown>): string {
65
+ const turns = numOf(asRecord(details.stats).totalTurns)
66
+ return turns === undefined
67
+ ? `Outline of session ${sessionLabel(params)}`
68
+ : `Outline of session ${sessionLabel(params)} · ${turns} turns`
69
+ }
70
+
71
+ function summarizeExpand(params: SessionReadParams, details: Record<string, unknown>): string {
72
+ const turn = numOf(asRecord(details.turn).index)
73
+ const entries = countOf(details, 'entries')
74
+ return `Expanded turn ${turn !== undefined ? `T${pad(turn)}` : (params.turn ?? '')} · ${entries} entries`
75
+ }
76
+
77
+ function summarizeDetail(params: SessionReadParams, details: Record<string, unknown>): string {
78
+ const label = strOf(details.turns) ?? params.turns ?? ''
79
+ return `Read turns ${label} · ${countOf(details, 'entries')} entries`
80
+ }
81
+
82
+ function summarizeSearch(params: SessionReadParams, details: Record<string, unknown>): string {
83
+ const pattern = params.pattern ?? ''
84
+ const hits = countOf(details, 'hits')
85
+ if (hits === 0) return `No hits for "${pattern}"`
86
+ return `Found ${hits} hits for "${pattern}"${truncatedSuffix(details)}`
87
+ }
88
+
89
+ function summarizeExport(params: SessionReadParams, details: Record<string, unknown>): string {
90
+ const format = params.format ?? 'outline'
91
+ const sizeBytes = numOf(details.sizeBytes)
92
+ return sizeBytes === undefined
93
+ ? `Exported ${format} view`
94
+ : `Exported ${format} view · ${sizeBytes} bytes`
95
+ }
96
+
97
+ function summarizeExtract(params: SessionReadParams, details: Record<string, unknown>): string {
98
+ const what = strOf(details.what) ?? params.what ?? ''
99
+ const count = numOf(details.count) ?? 0
100
+ if (count === 0) return `No ${what} found`
101
+ const shown = numOf(details.shown)
102
+ const clipped = details.truncated === true && shown !== undefined ? ` (showing ${shown})` : ''
103
+ return `Extracted ${count} ${what}${clipped}`
104
+ }
105
+
106
+ function summarizeWorkflow(_params: SessionReadParams, details: Record<string, unknown>): string {
107
+ const runs = countOf(details, 'runs')
108
+ if (runs === 0) return 'No workflow runs'
109
+ const skipped = countOf(details, 'skippedRuns')
110
+ return `Found ${runs} workflow runs${skipped > 0 ? ` · ${skipped} skipped` : ''}`
111
+ }
112
+
113
+ function summarizeResult(params: SessionReadParams, details: Record<string, unknown>): string {
114
+ // 批量 details = { count, items };单条 details = ResultItem 本体(session/totalChars/truncated)
115
+ const count = numOf(details.count)
116
+ if (count !== undefined && details.items !== undefined) {
117
+ return `Read ${count} final results`
118
+ }
119
+ const session = strOf(details.session) ?? sessionLabel(params)
120
+ const totalChars = numOf(details.totalChars)
121
+ return `Read final result of ${session} · ${totalChars !== undefined ? `${totalChars} chars` : 'done'}${truncatedSuffix(details)}`
122
+ }
123
+
124
+ function summarizeDoctor(_params: SessionReadParams, details: Record<string, unknown>): string {
125
+ const roots = arrOf(details.roots)
126
+ return roots !== undefined ? `Doctor: ${roots.length} roots checked` : 'Doctor check completed'
127
+ }
128
+
129
+ /** 消歧结果(任意 action 的 session 解析多匹配共用此 details 形态),优先于 action 分发 */
130
+ function summarizeAmbiguous(params: SessionReadParams, details: Record<string, unknown>): string {
131
+ const candidates = countOf(details, 'candidates')
132
+ return `Ambiguous session "${params.session ?? ''}": ${candidates} candidates`
133
+ }
134
+
135
+ /** 未知/缺省 action 的兜底(历史 entry 漂移面,展示面不抛错) */
136
+ function summarizeDone(): string {
137
+ return 'Done'
138
+ }
139
+
140
+ /** action → summarizer 分发表(Partial:运行时未知 action 落 summarizeDone 兜底) */
141
+ const ACTION_SUMMARIZERS: Partial<Record<SessionReadAction, (params: SessionReadParams, details: Record<string, unknown>) => string>> = {
142
+ find: summarizeFind,
143
+ family: summarizeFamily,
144
+ outline: summarizeOutline,
145
+ expand: summarizeExpand,
146
+ detail: summarizeDetail,
147
+ search: summarizeSearch,
148
+ export: summarizeExport,
149
+ extract: summarizeExtract,
150
+ workflow: summarizeWorkflow,
151
+ result: summarizeResult,
152
+ doctor: summarizeDoctor,
153
+ }
154
+
155
+ /**
156
+ * 按 action 产出一行摘要。action 与 details 不匹配(历史 entry / 异常形态)时
157
+ * 落到该 action 的兜底文案,不抛错——summary 是展示面,失败不追究。
158
+ */
159
+ export function buildSessionReadSummary(params: SessionReadParams, result: ToolResult): string {
160
+ const details = asRecord(result.details)
161
+ if (details.ambiguous === true) return summarizeAmbiguous(params, details)
162
+ const summarize = ACTION_SUMMARIZERS[params.action] ?? summarizeDone
163
+ return summarize(params, details)
164
+ }
165
+
166
+ /**
167
+ * 给 tool result 的 details 追加 summary(现有字段一律保留)。details 非对象
168
+ * (防御形态)时整体替换为 { summary }——生产路径 details 恒为对象,此分支只为
169
+ * 类型完备。
170
+ */
171
+ export function withResultSummary(params: SessionReadParams, result: ToolResult): ToolResult {
172
+ const summary = buildSessionReadSummary(params, result)
173
+ const details = result.details
174
+ if (details === null || typeof details !== 'object' || Array.isArray(details)) {
175
+ return { ...result, details: { summary } }
176
+ }
177
+ return { ...result, details: { ...details, summary } }
178
+ }
@@ -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 等公共类型留
@@ -84,7 +84,7 @@ export function formatSaIdNotFound(saId: string): string {
84
84
  }
85
85
 
86
86
  // ---------------------------------------------------------------------------
87
- // zcode 错误面(U9,design §3.4 七码 + 👉 逐行;错误码标识入 message 供机器识别,
87
+ // zcode 错误范围(U9,design §3.4 七码 + 👉 逐行;错误码标识入 message 供机器识别,
88
88
  // 形态对齐 §3.1 失败路径示例 `[zcode_db_path_forbidden] …`)
89
89
  // ---------------------------------------------------------------------------
90
90
 
@@ -131,7 +131,7 @@ function formatZcodeSchemaDrift(observed: string | undefined): string {
131
131
 
132
132
  /**
133
133
  * bun 驱动探测失败(§3.4 表第 8 行理论态)的 message:宿主既无 bun:sqlite 也无
134
- * node:sqlite——zcode action 整体降级为本错误面(fail-fast + 日志),pi 链路不受影响。
134
+ * node:sqlite——zcode action 整体降级为本错误范围(fail-fast + 日志),pi 链路不受影响。
135
135
  * 错误码字面量为实施期命名(设计该行未定码名,形态对齐七码 `[zcode_*]` 惯例)。
136
136
  */
137
137
  function formatZcodeHostUnsupported(detail: string): string {
@@ -145,8 +145,8 @@ function formatZcodeHostUnsupported(detail: string): string {
145
145
  /**
146
146
  * sqlite 驱动探测失败的消息特征(zcode-session-source sqlite-driver.ts 探测失败的
147
147
  * 错误消息契约;该包不导出专用错误子类,reader 侧按消息特征单列识别——跨包漂移
148
- * 由 zcode-routing.test.ts 的源文本契约测试守卫:sqlite-driver 源文不再含本子串即红)。
149
- * 导出面仅测试消费(契约锚 + 映射测试引用同值,禁测试内硬编码副本)。
148
+ * 由 zcode-routing.test.ts 的源文本契约测试检查:sqlite-driver 源文不再含本子串即红)。
149
+ * 导出范围仅测试消费(契约锚 + 映射测试引用同值,禁测试内硬编码副本)。
150
150
  */
151
151
  export const SQLITE_DRIVER_UNSUPPORTED_MARK = '不支持 node:sqlite'
152
152
 
@@ -154,17 +154,17 @@ export const SQLITE_DRIVER_UNSUPPORTED_MARK = '不支持 node:sqlite'
154
154
  * 库文件缺失的消息特征(zcode-session-source sqlite-access.ts `openZcodeSessionDb`
155
155
  * 开头 existsSync 失败抛普通 Error「db 文件不存在:<dbPath>」的消息契约锚点)。该包
156
156
  * 不导出专用错误子类,reader 侧按消息特征单列识别——落 §3.4 第 4 行
157
- * `zcode_db_unreadable`(不误标 schema_drift)。生产路径上白名单闸
157
+ * `zcode_db_unreadable`(不误标 schema_drift)。生产路径上白名单门禁
158
158
  * (assertZcodeDbPathAllowed 第 1 段)已先行拦掉绝大多数缺失,此处命中的是两段
159
- * existsSync 之间的 TOCTOU 理论窗口(闸通过后开库前文件被删)。跨包漂移由
160
- * zcode-routing.test.ts 的源文本契约测试守卫(SQLITE_DRIVER_UNSUPPORTED_MARK 同范式)。
161
- * 导出面仅测试消费(契约锚 + 映射测试引用同值,禁测试内硬编码副本)。
159
+ * existsSync 之间的 TOCTOU 理论窗口(门禁通过后开库前文件被删)。跨包漂移由
160
+ * zcode-routing.test.ts 的源文本契约测试检查(SQLITE_DRIVER_UNSUPPORTED_MARK 同范式)。
161
+ * 导出范围仅测试消费(契约锚 + 映射测试引用同值,禁测试内硬编码副本)。
162
162
  */
163
163
  export const ZCODE_DB_MISSING_MARK = 'db 文件不存在'
164
164
 
165
165
  /**
166
- * 开库/查询期错误 → §3.4 错误面映射(SqliteUnreadableError / schema drift / 驱动探测失败 / 库缺失 / 其余)。
167
- * 导出面仅测试消费(L4 包装形态的映射契约直测,zcode-routing.test.ts)。
166
+ * 开库/查询期错误 → §3.4 错误范围映射(SqliteUnreadableError / schema drift / 驱动探测失败 / 库缺失 / 其余)。
167
+ * 导出范围仅测试消费(L4 包装形态的映射契约直测,zcode-routing.test.ts)。
168
168
  */
169
169
  export function zcodeReadErrorMessage(e: unknown, agentDir: string): string {
170
170
  if (e instanceof SqliteUnreadableError) {