@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
@@ -11,7 +11,7 @@
11
11
  * 域模块拆分(max-lines 拆分轮机械提取,零行为变更,result-action.ts 先例同型):
12
12
  * result-action.ts(result)/ doctor.ts(doctor + SessionReadSignals)/
13
13
  * search-across.ts(search 管线 + u12 跨会话)/ extract.ts(extract 预设)/
14
- * tool-format.ts(各 action 输出文本渲染 + 错误面 message + F2 消歧与
14
+ * tool-format.ts(各 action 输出文本渲染 + 错误范围 message + F2 消歧与
15
15
  * find 零匹配包装)/ no-match.ts(F1 自检行)/ zcode-anchor-classify.ts(entry
16
16
  * 兜底归因的 entry 级纯判定)/ handler-utils.ts(pad/err/
17
17
  * stripHash/requireStr/SESSION_ID_PREFIX_LEN/turn 索引解析低层小工具)。
@@ -26,7 +26,7 @@
26
26
  */
27
27
  import { existsSync } from 'node:fs'
28
28
  import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
29
- import { join, isAbsolute } from 'node:path'
29
+ import { join, dirname, isAbsolute } from 'node:path'
30
30
  import { homedir } from 'node:os'
31
31
  import { toErrorMessage } from '@zhushanwen/pi-ext-guards'
32
32
  import { getLogger } from '@zhushanwen/pi-extension-logger'
@@ -102,7 +102,7 @@ import {
102
102
  extractUserMessages,
103
103
  type ExtractWhat,
104
104
  } from './extract.js'
105
- // 同轮拆分的渲染域模块(format* 纯函数簇,含 zcode 错误面 message 与
105
+ // 同轮拆分的渲染域模块(format* 纯函数簇,含 zcode 错误范围 message 与
106
106
  // zcodeReadErrorMessage 错误映射):运行时依赖方向本模块 → tool-format,
107
107
  // tool-format 对本模块仅 type import(ToolResult),无循环。
108
108
  import {
@@ -132,7 +132,7 @@ import { firstIncompleteAnchorReason } from './zcode-anchor-classify.js'
132
132
  export type { SessionReadSignals }
133
133
 
134
134
  // zcode 读链的结构化日志(「事后排查」通道:degradations 留痕 / L2-L3 恢复成功 /
135
- // anchor-missing 归因——§3.4 可观测性 + P2 降级隔离契约 F13,均不进 LLM 可见面)。
135
+ // anchor-missing 归因——§3.4 可观测性 + P2 降级隔离契约 F13,均不进 LLM 可见部分)。
136
136
  // pi handle 由 index.ts 初始化时 setPiHandle 注入(appendEntry 通道);未注入时
137
137
  // warn/error 降级文件日志,双开关缺省 no-op(纯 pi 独立用户零行为影响)。
138
138
  const logger = getLogger('session-reader')
@@ -267,7 +267,7 @@ async function resolveSessionId(
267
267
 
268
268
  // §3.4 第 0 段:sess_ 形态 id 无第一梯队直读入口(agent 从不持有 sess id,F14;
269
269
  // 且无 zcode 候选发现,D6)→ zcode_param_invalid。pi 的 session id 是 uuid(无
270
- // sess_ 前缀),此检测只改变错误输入的错误面文案。
270
+ // sess_ 前缀),此检测只改变错误输入的错误范围文案。
271
271
  if (session.startsWith('sess_')) {
272
272
  throw err(formatZcodeParamInvalid(session))
273
273
  }
@@ -287,7 +287,7 @@ async function resolveSessionId(
287
287
  * `resolveByRecordId`(engine 缺省/'pi' 形态,行为与今天逐字节一致)。
288
288
  * ③ pi 无命中(今天此处直接 ES2)→ entry 兜底(§3.5 第②层,仅 liveSessionDir 内,
289
289
  * 越界显式失败):命中 → zcode 读链;未命中 → `zcode_record_not_found` + 👉
290
- * (§3.4:sa-id 语境的统一错误面——F14 agent 唯一持有的 id 就是 sa-id,指引动作
290
+ * (§3.4:sa-id 语境的统一错误范围——F14 agent 唯一持有的 id 就是 sa-id,指引动作
291
291
  * 覆盖除 find 指针外的全部恢复路径(find 按 §3.4 刻意不保留)。
292
292
  */
293
293
  async function resolveSaIdRoute(
@@ -299,7 +299,7 @@ async function resolveSaIdRoute(
299
299
  ): Promise<ResolveResult> {
300
300
  const zm = await readZcodeManifest(agentDir, session)
301
301
  if (zm.kind === 'anchor-missing') {
302
- // 归因进结构化日志不进 LLM 可见面(§3.4):reason 逐键归因(missing-engineHandle /
302
+ // 归因进结构化日志不进 LLM 可见部分(§3.4):reason 逐键归因(missing-engineHandle /
303
303
  // missing-sessionRef / missing-sessionId / missing-dbPath,见 zcode-manifest.ts)
304
304
  logger.warn('zcode manifest anchor-missing', { saId: session, reason: zm.reason })
305
305
  throw err(formatZcodeAnchorMissing(session))
@@ -391,7 +391,7 @@ async function resolveZcodeRoute(
391
391
  const zcodeNodes = await listZcodeManifests(agentDir)
392
392
  const rootSessionId = zcodeNodes.find((n) => n.id === saId)?.rootSessionId
393
393
  // 枚举未含该 id(直读命中与枚举之间 manifest 被迁移的极端窗口)→ 退回锚 sessionId,
394
- // 后续 byId 查不到走既有 not-found 错误面(不静默伪造家族)
394
+ // 后续 byId 查不到走既有 not-found 错误范围(不静默伪造家族)
395
395
  if (rootSessionId === undefined) {
396
396
  return { kind: 'ok', sessionId: anchor.sessionId, fileName: '' }
397
397
  }
@@ -501,8 +501,8 @@ async function resolveByFragment(
501
501
  /**
502
502
  * zcode 锚 → ParseResult(既有 turns/render 管线的统一内容入口)。
503
503
  *
504
- * 检查顺序三段递进(§3.4,实现与测试双锚定):第 1 段存在性 → 第 2 段路径闸
505
- * (whitelist.ts)→ 第 3 段开库与查询(四级恢复阶梯 + schema 已知集闸门在
504
+ * 检查顺序三段递进(§3.4,实现与测试双锚定):第 1 段存在性 → 第 2 段路径门禁
505
+ * (whitelist.ts)→ 第 3 段开库与查询(四级恢复阶梯 + schema 已知集门禁在
506
506
  * openZcodeSessionDb 单点)。读库发生在 ext 进程内(§3.5 关键论断),不经 taiji。
507
507
  */
508
508
  async function loadZcodeParsed(anchor: ZcodeAnchor, agentDir: string): Promise<ParseResult> {
@@ -515,7 +515,7 @@ async function loadZcodeParsed(anchor: ZcodeAnchor, agentDir: string): Promise<P
515
515
  }
516
516
  try {
517
517
  // 恢复成功不再是静默事件(§3.4 硬要求):结构化日志(恢复方式 + db 路径),不进
518
- // LLM 可见面。用 warn 而非 debug——L2/L3 是恢复降级路径,语义表 warn=内部降级与
518
+ // LLM 可见部分。用 warn 而非 debug——L2/L3 是恢复降级路径,语义表 warn=内部降级与
519
519
  // 失败(appendEntry 持久化);debug 缺省 no-op 会让恢复成功重新变回静默事件。
520
520
  if (handle.via !== 'L1-direct') {
521
521
  logger.warn('zcode session db recovered via recovery ladder', {
@@ -545,7 +545,7 @@ async function loadZcodeParsed(anchor: ZcodeAnchor, agentDir: string): Promise<P
545
545
  // converter 保证),totalBytes/skippedLines 恒 0
546
546
  return { entries: normalized.entries, totalBytes: 0, skippedLines: 0, lastLinePartial: false }
547
547
  } catch (e) {
548
- if (e instanceof Error && e.message.startsWith('[zcode_')) throw e // 本函数产出的错误面原样传播
548
+ if (e instanceof Error && e.message.startsWith('[zcode_')) throw e // 本函数产出的错误范围原样传播
549
549
  throw err(zcodeReadErrorMessage(e, agentDir))
550
550
  } finally {
551
551
  handle.dispose()
@@ -912,7 +912,7 @@ async function doSearch(
912
912
  /** export full 模式的 entry 分隔线('=' 重复)宽度。 */
913
913
  const EXPORT_SEPARATOR_LEN = 40
914
914
 
915
- /** export:物化摘要到 <agentDir>/tmp/session-view-<id>.md(design §3.4 export,D-8)。 */
915
+ /** export:物化摘要到 <dataDir>/ext-sessions/<id>/session-view.md(session-store-layout-inversion 域 6;文件名去 sid——sid 已在路径)。 */
916
916
  async function doExport(
917
917
  params: SessionReadParams,
918
918
  agentDir: string,
@@ -972,8 +972,16 @@ async function doExport(
972
972
  label = 'outline'
973
973
  }
974
974
 
975
- const outDir = join(agentDir, 'tmp')
976
- const outPath = join(outDir, `session-view-${resolved.sessionId}.md`)
975
+ // [session-store-layout-inversion 域 6] 导出落 session 存储树(<root>/<sid>/session-view.md,
976
+ // 文件名去 sid——sid 已在路径):基址 = agentDir 剥一层(taiji dataDir,托管态
977
+ // PI_CODING_AGENT_DIR = <dataDir>/agent)。session-reader 零 shared 依赖(extension 包
978
+ // 边界),公式与 shared getSessionViewPath 同字面镜像;sessionId 过 isPiSessionId 同域
979
+ // 校验(解析器产出的 sid 正常路径恒合法,此处是路径穿越纵深防线)。
980
+ if (!/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(resolved.sessionId)) {
981
+ throw err(`导出拒绝:sessionId 形态非法(${resolved.sessionId})👉 检查 session 参数或用 find 重新定位`)
982
+ }
983
+ const outDir = join(dirname(agentDir), 'ext-sessions', resolved.sessionId)
984
+ const outPath = join(outDir, 'session-view.md')
977
985
  await mkdir(outDir, { recursive: true })
978
986
  await writeFile(outPath, text, 'utf8')
979
987
  const sizeBytes = Buffer.byteLength(text, 'utf8')
@@ -996,7 +1004,7 @@ async function doExport(
996
1004
  //(max-lines 拆分轮机械提取);本段保留 F7 what 校验与 doExtract 编排(定位依赖
997
1005
  // tool-handler 私有的 resolveSessionId/safeParse/segmentTurns)。纯提取,不调 LLM。
998
1006
 
999
- /** what 类型守卫(直接比较,避开不安全断言;schema 已校验,此处防御 + 可单测绕过)。 */
1007
+ /** what 类型检查(直接比较,避开不安全断言;schema 已校验,此处防御 + 可单测绕过)。 */
1000
1008
  function isExtractWhat(v: unknown): v is ExtractWhat {
1001
1009
  return (
1002
1010
  v === 'user-messages' ||
@@ -1109,7 +1117,7 @@ interface SkippedRun {
1109
1117
  * session 会抛「session not found in family index」)——resolveSessionId 已把 session 解析到
1110
1118
  * 真实文件(kind==='ok' 保证文件存在,三形态:绝对路径/sa-id 均 existsSync 校验,片段匹配
1111
1119
  * 来自实际 fs 扫描),直接用 resolved.fileName 构造单条目 sessionIdToPath 调 resolveWorkflows
1112
- *(与 buildFamilyFromFs 步骤 6 的 workflow 腿同源)。pathToRef 传空 Map(单条目链路无其他
1120
+ *(与 buildFamilyFromFs 步骤 6 的 workflow 路径同源)。pathToRef 传空 Map(单条目链路无其他
1113
1121
  * 文件可反查),call 引用 100% 走 sessionRefFromPath 文件名最小回退(sessionId+fileName,
1114
1122
  * 足够 LLM 跳 outline/detail 深读)。
1115
1123
  *
@@ -1131,7 +1139,7 @@ async function renderSingleWorkflowRun(
1131
1139
  ): Promise<void> {
1132
1140
  // [D16③] 概览链换源:v2 档的 stateFile = record 流路径(recordPath 锚点语义
1133
1141
  // 重定义)——直读流经 parseRunRecordStream 概览([D2] 三态 status;空流/读失败
1134
- // 投影 running,活跃 run 概览不退化为 skipped——红线)。record 后缀以外的
1142
+ // 映射 running,活跃 run 概览不退化为 skipped——红线)。record 后缀以外的
1135
1143
  // stateFile(v1 快照档空串 / 旧指针档 link path)走原快照链。
1136
1144
  if (wf.stateFile.endsWith('.record.jsonl')) {
1137
1145
  await renderRunFromRecordStream(wf, ctx)
@@ -1391,7 +1399,7 @@ export async function handleSessionRead(
1391
1399
  case 'workflow':
1392
1400
  return doWorkflow(params, agentDir, signals.liveSessionDir)
1393
1401
  case 'result': {
1394
- // per-call 构造注入面(仅剩 tool-handler 文件私有 helper,纯函数经 handler-utils
1402
+ // per-call 构造注入点(仅剩 tool-handler 文件私有 helper,纯函数经 handler-utils
1395
1403
  // 直接 import——ext-simplify-04 E5):resolveSessionId 包装把信号包中的
1396
1404
  // liveSessionDir 闭包进解析调用(ResultActionDeps 接口签名固定 5 参,包装保持
1397
1405
  // 同形、末位补传),result 的片段形态与 find/outline 消费同一 roots
@@ -77,7 +77,7 @@ export function createSessionCommand(
77
77
  const chosen = await ctx.ui.select('选择一个 session 插入 # 引用', labels)
78
78
  if (chosen === undefined) return
79
79
  // chosen 必属 labels(select 契约返回列表成员或 undefined,后者上一行已拦截),
80
- // indexOf 恒命中——不再保留 idx<0 死防御分支
80
+ // indexOf 恒命中——不再保留 idx<0 永不命中的防御机制分支
81
81
  const idx = labels.indexOf(chosen)
82
82
  // 选中后插入 # + 完整 uuid(剥 # 算片段,再补 # 插入编辑器)
83
83
  const frag = toCandidate(top[idx]).insertText.slice(1)
@@ -0,0 +1,48 @@
1
+ /**
2
+ * session_read 工具的 TUI renderResult(折叠态一行摘要 / 展开态 content 纯文本)。
3
+ *
4
+ * summary 来源 = details.summary(index.ts execute 经 withResultSummary 追加);
5
+ * 历史持久化 entry 无 summary 时折叠态回退 content 文本首行(旧形态可用性兜底)。
6
+ */
7
+
8
+ import type { Theme } from '@earendil-works/pi-coding-agent'
9
+ import { Text } from '@earendil-works/pi-tui'
10
+ import { firstContentText } from '@zhushanwen/extension-protocol'
11
+
12
+ /** content[0] 的 text 纯文本(content 缺失或非 text 类型时兜底空串)。 */
13
+ function contentText(result: unknown): string {
14
+ const content = fieldValue(result, 'content')
15
+ if (!Array.isArray(content)) return ''
16
+ return firstContentText({ content })
17
+ }
18
+
19
+ /** unknown 值按字段名安全取值(index-signature 收窄,替代全可选结构断言)。 */
20
+ function fieldValue(v: unknown, key: string): unknown {
21
+ if (typeof v !== 'object' || v === null) return undefined
22
+ return (v as Record<string, unknown>)[key]
23
+ }
24
+
25
+ /** 从 details 安全读取 summary(容错非对象形态与旧版本 entry)。 */
26
+ function readDetailsSummary(result: unknown): string | undefined {
27
+ const summary = fieldValue(fieldValue(result, 'details'), 'summary')
28
+ return typeof summary === 'string' && summary !== '' ? summary : undefined
29
+ }
30
+
31
+ /** 折叠态回退:content 文本首行(多行 outline/detail 输出只取首行,保持一行摘要形态)。 */
32
+ function firstLine(text: string): string {
33
+ const idx = text.indexOf('\n')
34
+ return idx === -1 ? text : text.slice(0, idx)
35
+ }
36
+
37
+ /**
38
+ * 折叠态(expanded=false)= details.summary 一行(dim);展开态 = content 纯文本。
39
+ */
40
+ export function renderSessionReadResult(
41
+ result: unknown,
42
+ options: { expanded: boolean },
43
+ theme: Theme,
44
+ ): Text {
45
+ if (options.expanded) return new Text(contentText(result), 0, 0)
46
+ const summary = readDetailsSummary(result) ?? firstLine(contentText(result))
47
+ return new Text(theme.fg('dim', summary), 0, 0)
48
+ }
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * 判定与 entry-anchor.ts 的 zcodeAnchorOfEntry 双键判据同构(v1 快照 / v2 终态条
7
7
  * 双版本门),协议字面量同值(写侧 subagent-core record-entry.ts
8
- * SUBAGENT_RECORD_CUSTOM_TYPE,漂移由 entry-anchor.test.ts 守卫)。
8
+ * SUBAGENT_RECORD_CUSTOM_TYPE,漂移由 entry-anchor.test.ts 检查)。
9
9
  */
10
10
  import type { Entry } from '@zhushanwen/session-core'
11
11
  import type { ZcodeAnchorMissingReason } from './discovery/zcode-manifest.js'
@@ -13,7 +13,7 @@ import type { ZcodeAnchorMissingReason } from './discovery/zcode-manifest.js'
13
13
  /**
14
14
  * v2 条目 data 的认识版本词表(W1 起 = 2)。与写侧 subagent-core
15
15
  * SUBAGENT_RECORD_ENTRY_VERSION 同值——本地持有 + entry-anchor.test.ts 跨包断言
16
- * 守卫漂移(生产依赖面不引 subagent-core,同 entry-anchor.ts 的协议字面量模式)。
16
+ * 检查漂移(生产依赖范围不引 subagent-core,同 entry-anchor.ts 的协议字面量模式)。
17
17
  */
18
18
  const SUBAGENT_RECORD_ENTRY_V2 = 2
19
19