@zhushanwen/pi-session-reader 0.6.0 → 0.7.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 (48) hide show
  1. package/README.md +100 -0
  2. package/package.json +7 -4
  3. package/src/__tests__/cross-package-subagent-core.test.ts +36 -44
  4. package/src/__tests__/entry-anchor.test.ts +226 -0
  5. package/src/__tests__/env.test.ts +54 -54
  6. package/src/__tests__/execution-tree.test.ts +6 -0
  7. package/src/__tests__/family-zcode.test.ts +255 -0
  8. package/src/__tests__/family.test.ts +1 -1
  9. package/src/__tests__/find.test.ts +24 -1
  10. package/src/__tests__/index.test.ts +1 -1
  11. package/src/__tests__/parser.test.ts +1 -2
  12. package/src/__tests__/real-data.ts +22 -9
  13. package/src/__tests__/render.test.ts +1 -2
  14. package/src/__tests__/result.test.ts +2 -2
  15. package/src/__tests__/roots.test.ts +1 -1
  16. package/src/__tests__/subagents.test.ts +1 -4
  17. package/src/__tests__/tool-handler.test.ts +76 -85
  18. package/src/__tests__/toolcall.test.ts +1 -1
  19. package/src/__tests__/tree.test.ts +1 -1
  20. package/src/__tests__/turns.test.ts +1 -2
  21. package/src/__tests__/zcode-manifest.test.ts +361 -0
  22. package/src/__tests__/zcode-routing.test.ts +751 -0
  23. package/src/__tests__/zcode-whitelist.test.ts +119 -0
  24. package/src/core/execution-tree.ts +3 -1
  25. package/src/core/family.ts +80 -1
  26. package/src/core/render.ts +2 -3
  27. package/src/core/toolcall.ts +3 -4
  28. package/src/core/tree.ts +1 -1
  29. package/src/core/turns.ts +1 -1
  30. package/src/core/units.ts +14 -0
  31. package/src/discovery/entry-anchor.ts +108 -0
  32. package/src/discovery/env.ts +41 -41
  33. package/src/discovery/find.ts +1 -1
  34. package/src/discovery/roots.ts +2 -2
  35. package/src/discovery/session-header.ts +30 -69
  36. package/src/discovery/subagents.ts +9 -2
  37. package/src/discovery/whitelist.ts +87 -0
  38. package/src/discovery/workflows.ts +1 -1
  39. package/src/discovery/zcode-manifest.ts +277 -0
  40. package/src/doctor.ts +5 -70
  41. package/src/index.ts +9 -2
  42. package/src/result-action.ts +15 -6
  43. package/src/search-across.ts +2 -5
  44. package/src/tool-format.ts +455 -0
  45. package/src/tool-handler.ts +321 -325
  46. package/src/zcode-anchor-classify.ts +60 -0
  47. package/vitest.config.ts +2 -4
  48. package/src/core/parser.ts +0 -167
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import { tmpdir } from 'node:os'
3
+ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
4
+ import { homedir } from 'node:os'
5
+ import { join } from 'node:path'
6
+
7
+ import {
8
+ assertZcodeDbPathAllowed,
9
+ zcodeDbAllowlistFor,
10
+ } from '../discovery/whitelist.js'
11
+
12
+ // ============================================================
13
+ // P-whitelist(design session-reader-shared-core §3.3 D5 / §3.4 检查顺序 / 附录 A):
14
+ // 派生白名单在 taiji dataDir 布局下精确命中隔离库。
15
+ //
16
+ // 布局(mkdtemp 自建自删):root/agent = agentDir(dirname = dataDir root),
17
+ // root/engines/zcode/session-db/db.sqlite = 隔离库。测试内手拼路径段是**测试对
18
+ // 派生正确性的唯一验证手段**(生产 src 禁手拼,段单源 = SDK ZCODE_ISOLATED_DB_
19
+ // SEGMENTS 经 zcode-session-source zcodeImportDbAllowlist 引用)。
20
+ //
21
+ // 检查顺序铁律断言:存在性(不存在 → unreadable)先于路径(集合外 → forbidden)——
22
+ // 集合外且不存在的路径必须报 unreadable 而非 forbidden(场景 5 ④ 顺序断言的单元锚)。
23
+ // ============================================================
24
+
25
+ const ISOLATED_DB_SEGMENTS = ['engines', 'zcode', 'session-db', 'db.sqlite']
26
+
27
+ let root: string
28
+ let agentDir: string
29
+ let isolatedDbPath: string
30
+
31
+ beforeEach(() => {
32
+ root = mkdtempSync(join(tmpdir(), 'zcode-whitelist-test-'))
33
+ agentDir = join(root, 'agent')
34
+ mkdirSync(agentDir, { recursive: true })
35
+ isolatedDbPath = join(root, ...ISOLATED_DB_SEGMENTS)
36
+ })
37
+ afterEach(() => {
38
+ rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
39
+ })
40
+
41
+ describe('zcodeDbAllowlistFor:tmp dataDir 布局精确命中隔离库(P-whitelist)', () => {
42
+ it('第一项 = zcodeIsolatedDbPath(dirname(agentDir)),集合共两项(隔离库 + 宿主库)', () => {
43
+ const allow = zcodeDbAllowlistFor(agentDir)
44
+ expect(allow).toHaveLength(2)
45
+ // 精确命中:手拼段与派生结果逐字节相等(同源常量 + 同一 join 派生式的行为锚)
46
+ expect(allow[0]).toBe(isolatedDbPath)
47
+ })
48
+
49
+ it('realpath 对齐:agentDir 经 symlink 到达(macOS /var → /private/var 形态)仍命中', () => {
50
+ // tmpdir 本身在 macOS 即 symlink 链(/var → /private/var):mkdtemp 产物 realpath
51
+ // 后 ≠ 字面串。允许集合与 dbPath 同经 realpath 归一(whitelist.ts realOf),
52
+ // 两侧一致归一后比对 = 命中。isolatedDbPath 真实存在才有 realpath 归一输入。
53
+ mkdirSync(join(root, ...ISOLATED_DB_SEGMENTS.slice(0, -1)), { recursive: true })
54
+ writeFileSync(isolatedDbPath, '', 'utf8')
55
+ expect(() => assertZcodeDbPathAllowed(isolatedDbPath, agentDir)).not.toThrow()
56
+ })
57
+
58
+ it('纯 pi 宿主边界(D5):agentDir 无对应 dataDir 布局时集合外路径照常拒绝', () => {
59
+ const outside = join(root, 'somewhere-else', 'db.sqlite')
60
+ mkdirSync(join(root, 'somewhere-else'), { recursive: true })
61
+ writeFileSync(outside, '', 'utf8')
62
+ expect(() => assertZcodeDbPathAllowed(outside, agentDir)).toThrow('zcode_db_path_forbidden')
63
+ })
64
+ })
65
+
66
+ describe('assertZcodeDbPathAllowed:检查顺序三段递进(存在性先于路径)', () => {
67
+ it('第 1 段存在性:白名单内路径但文件不存在 → zcode_db_unreadable(不误报 forbidden)', () => {
68
+ expect(() => assertZcodeDbPathAllowed(isolatedDbPath, agentDir)).toThrow(
69
+ 'zcode_db_unreadable',
70
+ )
71
+ })
72
+
73
+ it('第 1 段先于第 2 段:集合外且文件不存在的路径 → 仍是 unreadable(不是 forbidden)', () => {
74
+ const outsideMissing = join(root, 'nowhere', 'evil.sqlite')
75
+ expect(() => assertZcodeDbPathAllowed(outsideMissing, agentDir)).toThrow(
76
+ 'zcode_db_unreadable',
77
+ )
78
+ })
79
+
80
+ it('第 2 段路径:存在但集合外 → zcode_db_path_forbidden(安全面错误语义更重)', () => {
81
+ const outside = join(root, 'evil.sqlite')
82
+ writeFileSync(outside, '', 'utf8')
83
+ try {
84
+ assertZcodeDbPathAllowed(outside, agentDir)
85
+ expect.unreachable('should have thrown')
86
+ } catch (e) {
87
+ const msg = (e as Error).message
88
+ expect(msg).toContain('zcode_db_path_forbidden')
89
+ expect(msg).toContain(outside)
90
+ expect(msg).toContain('👉')
91
+ }
92
+ })
93
+
94
+ it('通过闸(集合内且存在)→ 不抛错(开库由调用方进行)', () => {
95
+ mkdirSync(join(root, ...ISOLATED_DB_SEGMENTS.slice(0, -1)), { recursive: true })
96
+ writeFileSync(isolatedDbPath, '', 'utf8')
97
+ expect(() => assertZcodeDbPathAllowed(isolatedDbPath, agentDir)).not.toThrow()
98
+ })
99
+
100
+ it('unreadable 指引含动态推导的隔离库路径与 -wal 告诫(§3.4 文案)', () => {
101
+ try {
102
+ assertZcodeDbPathAllowed(isolatedDbPath, agentDir)
103
+ expect.unreachable('should have thrown')
104
+ } catch (e) {
105
+ const msg = (e as Error).message
106
+ expect(msg).toContain(isolatedDbPath)
107
+ expect(msg).toContain('-wal')
108
+ expect(msg).toContain('👉')
109
+ }
110
+ })
111
+
112
+ it('宿主库第二项 = join(homedir(), SDK 宿主段)(存量兼容锚点,不在本梯队触碰)', () => {
113
+ const allow = zcodeDbAllowlistFor(agentDir)
114
+ // 第二项是 homedir 派生的宿主库(真实 HOME 下的 .zcode/cli/db/db.sqlite 形态)——
115
+ // 测试不触碰真实文件,只断言其位于集合且 ≠ 隔离库项
116
+ expect(allow[1]).not.toBe(allow[0])
117
+ expect(allow[1].startsWith(homedir())).toBe(true)
118
+ })
119
+ })
@@ -437,7 +437,9 @@ export async function buildExecutionTree(
437
437
  sessionId: rootSessionId,
438
438
  // MF-1:main root 携带 sessionFile(调用方传入),否则 attachWorkflowChildren 对无
439
439
  // sessionFile 节点直接 return,main 自身发起的 workflow run 永远进不了执行树。
440
- sessionFile: mainSessionFile,
440
+ // falsy 归一为 undefined:调用方的「路径不可得」哨兵是空串(zcode family 路由
441
+ // 反查未命中),root 节点 details 不携带 sessionFile:"" 这类非路径占位值。
442
+ sessionFile: mainSessionFile || undefined,
441
443
  depth: 0,
442
444
  rootSessionId,
443
445
  children: [],
@@ -1,4 +1,4 @@
1
- import type { Entry } from './parser.js'
1
+ import type { Entry } from '@zhushanwen/session-core'
2
2
 
3
3
  /**
4
4
  * 家族索引与解析(design §3.3 D-7)。
@@ -78,6 +78,74 @@ export interface FamilyIndex {
78
78
  fileStats: Map<string, { mtime: number; size: number }>
79
79
  }
80
80
 
81
+ // ---- zcode 节点(design session-reader-shared-core §3.3 D5-1)----
82
+
83
+ /**
84
+ * zcode subagent 节点的判据输入视图(discovery 层 zcode-manifest.ts 枚举产出,
85
+ * core 层零 IO 消费)。扁平化:dbPath 取自 manifest engineHandle.sessionRef.dbPath
86
+ * (zcode 锚),dbFileExists 由 discovery 枚举时 statSync 预计算——stat 只查存在性
87
+ * **不开库**(D5-1:静息态 CANTOPEN 会把活会话误判已 GC,且列表视图每节点开库的
88
+ * 成本不该付;库内 session 已被 zcode GC 的情形由 read 路径 zcode_session_not_found
89
+ * 权威承接,列表视图保持廉价是本判据的设计核心)。
90
+ */
91
+ export interface ZcodeFamilyNode {
92
+ /** record id(= sa-id,manifest 文件名 stem)。作 SubagentRef.sessionId(pi 孤儿链同款:entry.id = manifest.id) */
93
+ id: string
94
+ /** subagent 的发起 session id(subagentsByRoot 挂载键,与 pi identity.data.rootSessionId 同语义) */
95
+ rootSessionId: string
96
+ /** slug 标签(discovery 组装时已兜底 m.slug ?? m.agentName ?? '',与 pi 孤儿链同款) */
97
+ slug: string
98
+ /** agent 类型名 */
99
+ agentName?: string
100
+ task?: string
101
+ model?: string
102
+ status?: string
103
+ /** zcode 锚的库路径(engineHandle.sessionRef.dbPath;枚举谓词已保证非空 string) */
104
+ dbPath: string
105
+ /** 库文件存在性(discovery 枚举时 statSync 预计算;D5-1 第二判据的唯一输入) */
106
+ dbFileExists: boolean
107
+ }
108
+
109
+ /**
110
+ * zcode 节点 → subagentsByRoot(D5-1 判据落地)。
111
+ *
112
+ * 判据(两项、不开库):**锚可解析 ∧ 库文件存在 → cleanedUp=false;缺一 → true**。
113
+ * 「锚可解析」在枚举层已构造性成立(listZcodeManifests 谓词 = engine==='zcode' ∧
114
+ * sessionRef 双键齐,未通过的记录不产出)——core 层只需消费 dbFileExists。
115
+ *
116
+ * 与 pi 节点**刻意不同构**(D5-1 显式声明):pi 判据 = fileStats JSONL 文件存在性
117
+ * (buildSubagentsByRoot 的 !fileStats.has(ident.id));zcode 节点住在 SQLite 库里、
118
+ * 没有 JSONL 文件,「文件在不在」对它是无效判据。二者不互相回归。
119
+ */
120
+ export function buildZcodeSubagentsByRoot(
121
+ nodes: readonly ZcodeFamilyNode[],
122
+ ): Map<string, SubagentRef[]> {
123
+ const byRoot = new Map<string, SubagentRef[]>()
124
+ for (const n of nodes) {
125
+ const ref: SubagentRef = {
126
+ // zcode 节点无 subagent session 文件,sessionId 用 record id(pi 孤儿链同款占位)
127
+ sessionId: n.id,
128
+ rootSessionId: n.rootSessionId,
129
+ slug: n.slug,
130
+ fileName: '', // 占位空串(family 路径不回填,与 pi 一致)
131
+ mtime: 0, // 无 JSONL 文件可 stat,恒 0(占位约定,见文件头注释)
132
+ sizeBytes: 0,
133
+ cwd: '',
134
+ // D5-1:锚可解析(枚举谓词已过)∧ 库文件存在 → false;缺一 → true(GC 语义)
135
+ cleanedUp: !n.dbFileExists,
136
+ task: n.task,
137
+ agentName: n.agentName,
138
+ model: n.model,
139
+ status: n.status,
140
+ // sessionFile 不设:zcode 无 session.jsonl(pi 孤儿保留 GC 路径,zcode 无路径可保留)
141
+ }
142
+ const list = byRoot.get(n.rootSessionId)
143
+ if (list) list.push(ref)
144
+ else byRoot.set(n.rootSessionId, [ref])
145
+ }
146
+ return byRoot
147
+ }
148
+
81
149
  // ---- 类型守卫:从 unknown 的 entry.data 提取 subagent identity 字段 ----
82
150
 
83
151
  /**
@@ -225,6 +293,8 @@ function buildSubagentsByRoot(
225
293
  * - headers(type=session)→ byId + childrenOf(parentSession 文件路径反查父 sessionId)
226
294
  * - subagentIdentities(type=custom, customType=subagent-identity)→ subagentsByRoot
227
295
  * - fileStats 原样存入 index,供 cleanedUp 判断
296
+ * - zcodeNodes(可选,U6)→ zcode subagent 节点并入 subagentsByRoot(D5-1 判据,
297
+ * 与 pi 判据不同构;不传 = pi 行为逐字节不变——零涟漪)
228
298
  *
229
299
  * 坏数据容错:identity 缺 rootSessionId/slug 跳过;parentSession 反查不到父(父文件
230
300
  * 未被扫描到)该 entry 不进 childrenOf——均不报错,符合 pi 坏 session 容错(design §2)。
@@ -233,10 +303,19 @@ export function buildFamilyIndex(
233
303
  headers: Entry[],
234
304
  subagentIdentities: Entry[],
235
305
  fileStats: Map<string, { mtime: number; size: number }>,
306
+ zcodeNodes?: readonly ZcodeFamilyNode[],
236
307
  ): FamilyIndex {
237
308
  const byId = buildById(headers, fileStats)
238
309
  const childrenOf = buildChildrenOf(headers, byId)
239
310
  const subagentsByRoot = buildSubagentsByRoot(subagentIdentities, fileStats)
311
+ if (zcodeNodes !== undefined && zcodeNodes.length > 0) {
312
+ const zcodeByRoot = buildZcodeSubagentsByRoot(zcodeNodes)
313
+ for (const [rootId, refs] of zcodeByRoot) {
314
+ const existing = subagentsByRoot.get(rootId)
315
+ if (existing) existing.push(...refs)
316
+ else subagentsByRoot.set(rootId, refs)
317
+ }
318
+ }
240
319
  return { byId, childrenOf, subagentsByRoot, fileStats }
241
320
  }
242
321
 
@@ -1,4 +1,5 @@
1
- import type { Entry } from './parser.js'
1
+ import type { Entry } from '@zhushanwen/session-core'
2
+ import { BYTES_PER_KB } from './units.js'
2
3
  import { extractToolCalls, formatToolCallSummary, type ToolCallInfo } from './toolcall.js'
3
4
  import type { Turn } from './turns.js'
4
5
  import type { TreeView } from './tree.js'
@@ -7,8 +8,6 @@ import type { TreeView } from './tree.js'
7
8
  // 模块常量(渲染口径:截断宽度 / 换算基数 / 降级档位)
8
9
  // ---------------------------------------------------------------------------
9
10
 
10
- /** bytes→KB 换算基数(omitted 字节 / read 结果规模的 KB 显示)。 */
11
- const BYTES_PER_KB = 1024
12
11
  /** userBrief 截断字符数(TurnBrief.userBrief 文档口径「截 60 字符」)。 */
13
12
  const USER_BRIEF_MAX_CHARS = 60
14
13
  /** assistantBrief 截断字符数(TurnBrief.assistantBrief 文档口径「截 80 字符」)。 */
@@ -6,9 +6,10 @@
6
6
  * message.toolCalls 顶层字段从未存在——v1 render.ts 读 toolCalls 恒返 [] 是 O1 要修的 bug。
7
7
  * - arguments 始终是 object(10 工具 100%);string 形态做 JSON.parse 兜底(失败返 {}),防御历史/异类实现。
8
8
  * - toolResult.message 自带 toolName + toolCallId(515/515,全部匹配 toolCall.id),
9
- * parser.ts 已 additive 透出这两个字段,O2/O3 据此精确关联取参数。
9
+ * Entry 接口(session-core 基座)已 additive 透出这两个字段,O2/O3 据此精确关联取参数。
10
10
  */
11
- import type { Entry } from './parser.js'
11
+ import type { Entry } from '@zhushanwen/session-core'
12
+ import { BYTES_PER_KB } from './units.js'
12
13
 
13
14
  // ---------------------------------------------------------------------------
14
15
  // 模块常量(参数摘要的截断宽度 / 换算基数)
@@ -20,8 +21,6 @@ const BASH_CMD_MAX_CHARS = 60
20
21
  const SUBAGENT_TASK_MAX_CHARS = 40
21
22
  /** 未知工具 arguments JSON 摘要截断字符数。 */
22
23
  const ARGS_JSON_MAX_CHARS = 50
23
- /** bytes→KB 换算基数(write content 的 KB 显示)。 */
24
- const BYTES_PER_KB = 1024
25
24
 
26
25
  /** 单次工具调用信息(从 assistant content 的 toolCall block 提取)。 */
27
26
  export interface ToolCallInfo {
package/src/core/tree.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Entry } from './parser.js'
1
+ import type { Entry } from '@zhushanwen/session-core'
2
2
 
3
3
  export interface TreeView {
4
4
  /** root → leaf 的 id 序列(pi 重开视角的当前对话线,design D-2) */
package/src/core/turns.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Entry } from './parser.js'
1
+ import type { Entry } from '@zhushanwen/session-core'
2
2
 
3
3
  /**
4
4
  * 一轮对话(design §3.5 算法 3 的分段产物,冻结接口)。
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 字节量纲换算基数(本包内 KB/MB 换算的唯一来源)。
3
+ *
4
+ * 本包是独立 npm 发布的 pi extension(仅依赖 pi-ext-guards),不 import taiji 内部
5
+ * workspace 包(@taiji/shared 的 BYTES_PER_MB 单点不含本包)——量纲基数本地定义,
6
+ * 数值对齐由注释锚定(1MB = 1024×1024)。三个消费域(render 的 omitted 标记 /
7
+ * toolcall 的 write 摘要 / search-across 的预算与人性化格式化)共用本单点,
8
+ * 避免同值常量在本包内多份分裂。
9
+ */
10
+
11
+ /** bytes→KB 换算基数(omitted 字节 / read 结果规模 / write content 的 KB 显示)。 */
12
+ export const BYTES_PER_KB = 1024
13
+ /** bytes→MB 换算基数(跨会话检索预算与人性化格式化)。 */
14
+ export const BYTES_PER_MB = BYTES_PER_KB * BYTES_PER_KB
@@ -0,0 +1,108 @@
1
+ import { readFile } from 'node:fs/promises'
2
+
3
+ import { parseSessionContent, type Entry } from '@zhushanwen/session-core'
4
+
5
+ /**
6
+ * zcode 锚的 entry 兜底定位(设计 session-reader-shared-core §3.1 第②步)。
7
+ *
8
+ * 定位链第②层:zcode manifest 直读(第①层,zcode-manifest.ts)缺位/残缺时,
9
+ * 在候选主 session 文件里按 sa-id 扫 `subagent-record` custom entry,取其
10
+ * `engineHandle.sessionRef`(zcode 形态 = `{sessionId, dbPath}`)。
11
+ *
12
+ * 边界承载(设计四要素登记):本函数只认「候选文件列表」,越界语义(不做根内
13
+ * 全扫、liveSessionDir 优先排序)由调用方构造候选集时决定——第一梯队只传
14
+ * liveSessionDir 内的主 session 文件,越界查询返回 not-found 信号(undefined,
15
+ * 调用方映射 `zcode_record_not_found` 错误码,§3.4)。
16
+ */
17
+
18
+ /**
19
+ * zcode 锚(zcode 引擎自描述定位符的 zcode 形态)。与 manifest 直读产出的锚同形;
20
+ * `sessionRef` 写侧整体透传不枚举内部键(subagent-core record-entry.ts
21
+ * engineHandle.sessionRef 注释),本单元按 zcode 形态校验并窄化为双键。
22
+ */
23
+ export interface ZcodeAnchor {
24
+ sessionId: string
25
+ dbPath: string
26
+ }
27
+
28
+ /**
29
+ * `subagent-record` custom entry 的 customType 值。
30
+ *
31
+ * 磁盘 JSONL 协议字符串(跨侧契约):写侧单源 = subagent-core record-entry.ts 的
32
+ * SUBAGENT_RECORD_CUSTOM_TYPE。reader 生产依赖面不引 subagent-core(该包是
33
+ * devDependency + vitest alias 形态),故按协议字符串本地持有;两侧漂移由
34
+ * entry-anchor.test.ts 的跨包断言守卫。
35
+ */
36
+ const SUBAGENT_RECORD_CUSTOM_TYPE = 'subagent-record'
37
+
38
+ /**
39
+ * 从单条 entry 提取 zcode 锚。形状校验窄而严,任一不满足 = 该条不算命中(返回
40
+ * undefined,调用方继续扫——缺键条目不遮蔽更早的完整条目):
41
+ * - `type === 'custom'` ∧ `customType === 'subagent-record'`
42
+ * - `data.v === 1`(schema 版本守卫,不认识的版本跳过而非猜测——写侧 record-entry.ts v 注释同口径)
43
+ * - `data.id === saId`(record id 即 sa-id,record-access.ts `sa-${uuid}`)
44
+ * - `engineHandle.sessionRef.sessionId` / `.dbPath` 均为非空 string(F8 双键齐判据的
45
+ * 锚字段部分;pi record 的 sessionRef 无 dbPath → 天然不命中,无需 engine 判别)
46
+ */
47
+ function zcodeAnchorOfEntry(entry: Entry, saId: string): ZcodeAnchor | undefined {
48
+ if (entry.type !== 'custom' || entry.customType !== SUBAGENT_RECORD_CUSTOM_TYPE) return undefined
49
+ const data: unknown = entry.data
50
+ if (typeof data !== 'object' || data === null) return undefined
51
+ const d = data as Record<string, unknown>
52
+ if (d.v !== 1 || d.id !== saId) return undefined
53
+ const handle: unknown = d.engineHandle
54
+ if (typeof handle !== 'object' || handle === null) return undefined
55
+ const ref: unknown = (handle as Record<string, unknown>).sessionRef
56
+ if (typeof ref !== 'object' || ref === null) return undefined
57
+ const r = ref as Record<string, unknown>
58
+ const { sessionId, dbPath } = r
59
+ if (typeof sessionId !== 'string' || sessionId === '') return undefined
60
+ if (typeof dbPath !== 'string' || dbPath === '') return undefined
61
+ return { sessionId, dbPath }
62
+ }
63
+
64
+ /**
65
+ * 在候选主 session 文件里按 sa-id 定位 zcode 锚。
66
+ *
67
+ * 取条语义(设计 §2.5 裁决,subagent-core collectLastRecordEntries 逐字同构——
68
+ * 「主 session 全文 → 每 id 末条 record data」,record-store-rebuild.ts:212-227):
69
+ * 同一 sa-id 每次状态迁移写一条全量快照 entry 且 sessionRef 每轮覆写(F16),故
70
+ * 同文件内后条覆盖前条 = 文件顺序末条。**禁止另创取首条/合并语义**(附录 B-6)。
71
+ *
72
+ * 多候选文件:按候选列表顺序逐文件扫,文件内有命中锚(含末条裁决后)即止——
73
+ * 候选顺序 = 调用方声明的优先级(liveSessionDir 优先),不跨文件合并。
74
+ *
75
+ * 行级容错:逐行解析由基座 parseSessionContent 承担(坏行跳过继续、末行半行记
76
+ * lastLinePartial 不中断)——best-effort,不因一行坏 JSON 失败整文件。
77
+ *
78
+ * 候选文件读失败(不存在/不可读)= 该候选无命中,跳过继续下一候选;本函数契约
79
+ * 是「锚或 not-found」,IO 错误同样收敛到 not-found 信号(调用方映射错误码),
80
+ * 不向上抛。
81
+ *
82
+ * 读取用 node:fs/promises 的 async readFile——本函数跑在 pi 扩展进程内,同步
83
+ * readFileSync 全文读入会阻塞事件循环(同函数族 classifyIncompleteEntryAnchor
84
+ * 同形态;对照 readSessionHeaderIdSync 只在首行 4KB 有界读时才允许同步)。
85
+ *
86
+ * @returns 命中的锚;所有候选均未命中(或全部候选不可读)→ undefined(not-found 信号)
87
+ */
88
+ export async function findZcodeEntryAnchor(
89
+ candidateFiles: readonly string[],
90
+ saId: string,
91
+ ): Promise<ZcodeAnchor | undefined> {
92
+ for (const file of candidateFiles) {
93
+ let content: string
94
+ try {
95
+ content = await readFile(file, 'utf8')
96
+ } catch {
97
+ continue
98
+ }
99
+ const { entries } = parseSessionContent(content)
100
+ let anchor: ZcodeAnchor | undefined
101
+ for (const entry of entries) {
102
+ const hit = zcodeAnchorOfEntry(entry, saId)
103
+ if (hit !== undefined) anchor = hit
104
+ }
105
+ if (anchor !== undefined) return anchor
106
+ }
107
+ return undefined
108
+ }
@@ -12,35 +12,35 @@ import { dirname } from 'node:path'
12
12
  * 齐备),不 import roots.ts 的信号类型,避免并行单元耦合;接线层分别组装两种信号包。
13
13
  *
14
14
  * 信号可靠性依据(design §4.4 活体实测表,非推断):
15
- * - XYZ_AGENT_EXT_LOG 单独使用可靠性「中」——runtime 无条件注入,但
16
- * ENV_WHITELIST_PREFIXES 含裸 `XYZ_` 前缀,用户 shell 里的同名变量会透传进任何进程;
15
+ * - TAIJI_AGENT_EXT_LOG 单独使用可靠性「中」——runtime 无条件注入,但
16
+ * ENV_WHITELIST_PREFIXES 含裸 `TAIJI_` 前缀,用户 shell 里的同名变量会透传进任何进程;
17
17
  * - PI_CODING_AGENT_DIR 单独使用可靠性「中」——pi 原生变量,裸 pi 用户也可能自设,
18
- * 但其值形态是 xyz-agent 专属约定:`<*>/.xyz-agent*` 目录后接 `/agent`;
19
- * - XYZ_AGENT_PACKAGED **不可用**——在 SPAWN_ENV_OUTBOUND_DENY_LIST 被剥除
18
+ * 但其值形态是 taiji 专属约定:`<*>/.taiji*` 目录后接 `/agent`;
19
+ * - TAIJI_AGENT_PACKAGED **不可用**——在 SPAWN_ENV_OUTBOUND_DENY_LIST 被剥除
20
20
  * (§4.4 在案结论),故不引入该信号,防后人重蹈。
21
21
  * 因此托管判定必须是双信号合取:任一信号单独成立只判 standalone-pi,
22
22
  * 透传/自设污染提示写入 evidence。
23
23
  */
24
24
 
25
- /** xyz-agent 托管信号 1:runtime 无条件注入的 extension 日志开关(恒 '1') */
26
- export const XYZ_AGENT_EXT_LOG_ENV = 'XYZ_AGENT_EXT_LOG'
27
- /** xyz-agent 托管信号 2:pi agentDir 注入变量;其**值形态**是 xyz-agent 专属约定 */
25
+ /** taiji 托管信号 1:runtime 无条件注入的 extension 日志开关(恒 '1') */
26
+ export const TAIJI_AGENT_EXT_LOG_ENV = 'TAIJI_AGENT_EXT_LOG'
27
+ /** taiji 托管信号 2:pi agentDir 注入变量;其**值形态**是 taiji 专属约定 */
28
28
  export const PI_CODING_AGENT_DIR_ENV = 'PI_CODING_AGENT_DIR'
29
29
  /** 数据目录推导首选:runtime getDataDir() 的权威注入值 */
30
- export const XYZ_AGENT_DATA_DIR_ENV = 'XYZ_AGENT_DATA_DIR'
30
+ export const TAIJI_AGENT_DATA_DIR_ENV = 'TAIJI_AGENT_DATA_DIR'
31
31
 
32
32
  /** 托管信号 1 的唯一合法值(注释「托管语义恒为 '1'」) */
33
33
  const EXT_LOG_MANAGED_VALUE = '1'
34
34
 
35
- // xyz-agent agentDir 值形态(§6.2 双形态兼容——迁移窗口期 B 前后两种布局并存):
36
- // - B 后新布局:`<*>/.xyz-agent*` 目录后接 `/agent`(一层)
37
- // - B 前旧布局:`<*>/.xyz-agent*` 目录后接 `/pi/agent`(两层)
38
- // `.xyz-agent*` = 以 `.xyz-agent` 开头的目录段(prod `.xyz-agent` / dev `.xyz-agent-dev`)。
39
- // 三条正则都要求匹配到路径末尾(`$`),调用前输入先经 normalizeXyzPath 剥尾斜杠。
35
+ // taiji agentDir 值形态(§6.2 双形态兼容——迁移窗口期 B 前后两种布局并存):
36
+ // - B 后新布局:`<*>/.taiji*` 目录后接 `/agent`(一层)
37
+ // - B 前旧布局:`<*>/.taiji*` 目录后接 `/pi/agent`(两层)
38
+ // `.taiji*` = 以 `.taiji` 开头的目录段(prod `.taiji` / dev `.taiji-dev`)。
39
+ // 三条正则都要求匹配到路径末尾(`$`),调用前输入先经 normalizeTaijiPath 剥尾斜杠。
40
40
  // (用行注释:形态文本含星号斜杠序列,写进块注释会提前终止注释。)
41
- const AGENT_DIR_SHAPE_ANY = /(^|\/)\.xyz-agent[^/]*\/(pi\/)?agent$/
42
- const AGENT_DIR_SHAPE_OLD = /(^|\/)\.xyz-agent[^/]*\/pi\/agent$/
43
- const AGENT_DIR_SHAPE_NEW = /(^|\/)\.xyz-agent[^/]*\/agent$/
41
+ const AGENT_DIR_SHAPE_ANY = /(^|\/)\.taiji[^/]*\/(pi\/)?agent$/
42
+ const AGENT_DIR_SHAPE_OLD = /(^|\/)\.taiji[^/]*\/pi\/agent$/
43
+ const AGENT_DIR_SHAPE_NEW = /(^|\/)\.taiji[^/]*\/agent$/
44
44
 
45
45
  /**
46
46
  * 打包形态判据(§6.2:bundle 路径判发行形态;无法判定 → null 不猜)。
@@ -56,7 +56,7 @@ const MACOS_PACKAGED_PATTERN = /\.app\/Contents\/Resources\/extensions\//
56
56
  const REPO_STAGED_PATTERN = /(^|\/)apps\/electron\/resources\/extensions\//
57
57
  const WIN_LINUX_PACKAGED_PATTERN = /(^|\/)resources\/extensions\//
58
58
 
59
- export type EnvironmentKind = 'xyz-agent' | 'standalone-pi'
59
+ export type EnvironmentKind = 'taiji' | 'standalone-pi'
60
60
  export type Distribution = 'packaged' | 'dev' | null
61
61
 
62
62
  export interface EnvironmentSignals {
@@ -71,19 +71,19 @@ export interface EnvironmentSignals {
71
71
  export interface DetectedEnvironment {
72
72
  kind: EnvironmentKind
73
73
  distribution: Distribution
74
- /** 托管时的 xyz-agent 数据目录;standalone-pi 恒 undefined */
74
+ /** 托管时的 taiji 数据目录;standalone-pi 恒 undefined */
75
75
  dataDir?: string
76
76
  /** 每条命中/未命中信号的原文 + 判定依据,恒非空(§6.2:判定可被证据推翻,非黑盒断言) */
77
77
  evidence: string[]
78
78
  }
79
79
 
80
80
  /** 形态匹配前的归一化:Windows 分隔符 → `/`,剥尾部斜杠 */
81
- function normalizeXyzPath(p: string): string {
81
+ function normalizeTaijiPath(p: string): string {
82
82
  return p.replace(/\\/g, '/').replace(/\/+$/, '')
83
83
  }
84
84
 
85
- function matchesXyzAgentAgentDir(value: string): boolean {
86
- return AGENT_DIR_SHAPE_ANY.test(normalizeXyzPath(value))
85
+ function matchesTaijiAgentDir(value: string): boolean {
86
+ return AGENT_DIR_SHAPE_ANY.test(normalizeTaijiPath(value))
87
87
  }
88
88
 
89
89
  /** distribution 判定,判定行写入 evidence */
@@ -110,19 +110,19 @@ function detectDistribution(bundleUrl: string | undefined, evidence: string[]):
110
110
  }
111
111
 
112
112
  /**
113
- * 托管时的数据目录推导:XYZ_AGENT_DATA_DIR 优先(runtime 注入的权威值,与
113
+ * 托管时的数据目录推导:TAIJI_AGENT_DATA_DIR 优先(runtime 注入的权威值,与
114
114
  * getDataDir() 同源),缺失时按布局形态从 PI_CODING_AGENT_DIR 剥层。来源写入 evidence。
115
115
  */
116
116
  function deriveDataDir(env: Record<string, string | undefined>, evidence: string[]): string | undefined {
117
- const dataDirEnv = env[XYZ_AGENT_DATA_DIR_ENV]
117
+ const dataDirEnv = env[TAIJI_AGENT_DATA_DIR_ENV]
118
118
  if (dataDirEnv) {
119
- evidence.push(`数据目录:'${dataDirEnv}'(来源:${XYZ_AGENT_DATA_DIR_ENV})`)
119
+ evidence.push(`数据目录:'${dataDirEnv}'(来源:${TAIJI_AGENT_DATA_DIR_ENV})`)
120
120
  return dataDirEnv
121
121
  }
122
122
  const agentDirEnv = env[PI_CODING_AGENT_DIR_ENV]
123
123
  if (agentDirEnv) {
124
- const normalized = normalizeXyzPath(agentDirEnv)
125
- // 旧形态更长,先判:`.xyz-agent*/pi/agent` → 剥 pi/agent 两层
124
+ const normalized = normalizeTaijiPath(agentDirEnv)
125
+ // 旧形态更长,先判:`.taiji*/pi/agent` → 剥 pi/agent 两层
126
126
  if (AGENT_DIR_SHAPE_OLD.test(normalized)) {
127
127
  const dataDir = dirname(dirname(normalized))
128
128
  evidence.push(`数据目录:'${dataDir}'(来源:${PI_CODING_AGENT_DIR_ENV} 剥 pi/agent 两层,B 前旧布局)`)
@@ -134,7 +134,7 @@ function deriveDataDir(env: Record<string, string | undefined>, evidence: string
134
134
  return dataDir
135
135
  }
136
136
  }
137
- evidence.push(`数据目录:无法推导(${XYZ_AGENT_DATA_DIR_ENV} 未设置且 ${PI_CODING_AGENT_DIR_ENV} 形态不可剥层)`)
137
+ evidence.push(`数据目录:无法推导(${TAIJI_AGENT_DATA_DIR_ENV} 未设置且 ${PI_CODING_AGENT_DIR_ENV} 形态不可剥层)`)
138
138
  return undefined
139
139
  }
140
140
 
@@ -146,16 +146,16 @@ function pushExtLogEvidence(
146
146
  ): void {
147
147
  evidence.push(
148
148
  extLogHit
149
- ? `${XYZ_AGENT_EXT_LOG_ENV}='${extLog}' → 命中`
150
- : `${XYZ_AGENT_EXT_LOG_ENV}=${extLog === undefined ? '<未设置>' : `'${extLog}'`} → 未命中(要求值为 '1')`,
149
+ ? `${TAIJI_AGENT_EXT_LOG_ENV}='${extLog}' → 命中`
150
+ : `${TAIJI_AGENT_EXT_LOG_ENV}=${extLog === undefined ? '<未设置>' : `'${extLog}'`} → 未命中(要求值为 '1')`,
151
151
  )
152
152
  }
153
153
 
154
- /** 信号 2 的形态标注(§6.2 双形态兼容:B 前旧布局 / B 后新布局 / 非 xyz-agent 形态)。 */
154
+ /** 信号 2 的形态标注(§6.2 双形态兼容:B 前旧布局 / B 后新布局 / 非 taiji 形态)。 */
155
155
  function describeAgentDirShape(normalized: string): string {
156
156
  if (AGENT_DIR_SHAPE_OLD.test(normalized)) return 'B 前旧布局 pi/agent'
157
157
  if (AGENT_DIR_SHAPE_NEW.test(normalized)) return 'B 后新布局 agent'
158
- return '非 xyz-agent 形态'
158
+ return '非 taiji 形态'
159
159
  }
160
160
 
161
161
  /** 信号 2 evidence 行(未设置 → 未命中;有值 → 形态匹配与否 + 形态标注)。 */
@@ -168,19 +168,19 @@ function pushAgentDirEvidence(
168
168
  evidence.push(`${PI_CODING_AGENT_DIR_ENV}=<未设置> → 未命中`)
169
169
  return
170
170
  }
171
- const shape = describeAgentDirShape(normalizeXyzPath(agentDirEnv))
171
+ const shape = describeAgentDirShape(normalizeTaijiPath(agentDirEnv))
172
172
  evidence.push(
173
- `${PI_CODING_AGENT_DIR_ENV}='${agentDirEnv}' → ${agentDirHit ? `形态匹配(${shape})` : `形态不匹配(${shape},要求 <*>/.xyz-agent*/agent)`}`,
173
+ `${PI_CODING_AGENT_DIR_ENV}='${agentDirEnv}' → ${agentDirHit ? `形态匹配(${shape})` : `形态不匹配(${shape},要求 <*>/.taiji*/agent)`}`,
174
174
  )
175
175
  }
176
176
 
177
177
  /** 判定行:托管(双信号合取成立)/ standalone-pi(孤置信号点名,提示透传/自设污染)。 */
178
178
  function judgedEvidenceLine(extLogHit: boolean, agentDirHit: boolean): string {
179
179
  if (extLogHit && agentDirHit) {
180
- return `判定:xyz-agent 托管(${XYZ_AGENT_EXT_LOG_ENV}='1' 且 ${PI_CODING_AGENT_DIR_ENV} 形态匹配,双信号合取成立)`
180
+ return `判定:taiji 托管(${TAIJI_AGENT_EXT_LOG_ENV}='1' 且 ${PI_CODING_AGENT_DIR_ENV} 形态匹配,双信号合取成立)`
181
181
  }
182
182
  const orphaned: string[] = []
183
- if (extLogHit) orphaned.push(XYZ_AGENT_EXT_LOG_ENV)
183
+ if (extLogHit) orphaned.push(TAIJI_AGENT_EXT_LOG_ENV)
184
184
  if (agentDirHit) orphaned.push(PI_CODING_AGENT_DIR_ENV)
185
185
  const orphanNote =
186
186
  orphaned.length > 0
@@ -192,8 +192,8 @@ function judgedEvidenceLine(extLogHit: boolean, agentDirHit: boolean): string {
192
192
  /**
193
193
  * 判定当前宿主环境(design §6.2)。
194
194
  *
195
- * 托管(xyz-agent)= 合取:`XYZ_AGENT_EXT_LOG === '1'` **且** `PI_CODING_AGENT_DIR`
196
- * 值匹配 xyz-agent 形态(`<*>/.xyz-agent*` 目录后接 `/agent`;B 前旧布局接
195
+ * 托管(taiji)= 合取:`TAIJI_AGENT_EXT_LOG === '1'` **且** `PI_CODING_AGENT_DIR`
196
+ * 值匹配 taiji 形态(`<*>/.taiji*` 目录后接 `/agent`;B 前旧布局接
197
197
  * `/pi/agent` 同样接受)。任一信号单独成立 → standalone-pi,evidence 点名孤置
198
198
  * 信号(透传污染提示)。
199
199
  * evidence 恒非空:每条命中/未命中信号的原文都在列,让 agent 能核对并推翻判定。
@@ -205,19 +205,19 @@ export function detectEnvironment(signals: EnvironmentSignals): DetectedEnvironm
205
205
  const evidence: string[] = []
206
206
  const { env } = signals
207
207
 
208
- // 信号 1:XYZ_AGENT_EXT_LOG === '1'
209
- const extLog = env[XYZ_AGENT_EXT_LOG_ENV]
208
+ // 信号 1:TAIJI_AGENT_EXT_LOG === '1'
209
+ const extLog = env[TAIJI_AGENT_EXT_LOG_ENV]
210
210
  const extLogHit = extLog === EXT_LOG_MANAGED_VALUE
211
211
  pushExtLogEvidence(evidence, extLog, extLogHit)
212
212
 
213
213
  // 信号 2:PI_CODING_AGENT_DIR 值形态
214
214
  const agentDirEnv = env[PI_CODING_AGENT_DIR_ENV]
215
- const agentDirHit = agentDirEnv !== undefined && matchesXyzAgentAgentDir(agentDirEnv)
215
+ const agentDirHit = agentDirEnv !== undefined && matchesTaijiAgentDir(agentDirEnv)
216
216
  pushAgentDirEvidence(evidence, agentDirEnv, agentDirHit)
217
217
  evidence.push(`agentDir='${signals.agentDir}' → pi getAgentDir() 透传记录`)
218
218
 
219
219
  const managed = extLogHit && agentDirHit
220
- const kind: EnvironmentKind = managed ? 'xyz-agent' : 'standalone-pi'
220
+ const kind: EnvironmentKind = managed ? 'taiji' : 'standalone-pi'
221
221
  evidence.push(judgedEvidenceLine(extLogHit, agentDirHit))
222
222
 
223
223
  const distribution = detectDistribution(signals.bundleUrl, evidence)
@@ -327,7 +327,7 @@ function isFlatRoot(root: SessionRoot): boolean {
327
327
  * 加载标题元数据索引(u11)。仅 keyword 路径调用(惰性触发:uuid 精确/归一化两级零命中
328
328
  * 且 query 非纯 hex 才到达此处,§6.6 调用策略 ①)。
329
329
  *
330
- * 窄化目标(策略 ②,按序):未剥层 liveSessionDir 本身(xyz-agent 下即平铺主根;纯 pi 下
330
+ * 窄化目标(策略 ②,按序):未剥层 liveSessionDir 本身(taiji 下即平铺主根;纯 pi 下
331
331
  * 即当前 cwd 目录,小)+ 实扫结果无子目录的候选根。两条 guard:
332
332
  * - 仅对存在的根/目录调用(root.exists / stat liveSessionDir),且永传非空串——pi 的
333
333
  * listAll 对空串 falsy 走默认全盘分支(数千项 / 秒级,hash-provider.ts 空参灾难先例);
@@ -24,7 +24,7 @@ import { performance } from 'node:perf_hooks'
24
24
 
25
25
  /** 发现层信号包(index.ts 采集,全部可降级;本模块只消费不采集) */
26
26
  export interface SessionRootSignals {
27
- /** pi agent 数据目录(两宿主均成立;B 后 xyz-agent = `<dataDir>/agent`) */
27
+ /** pi agent 数据目录(两宿主均成立;B 后 taiji = `<dataDir>/agent`) */
28
28
  agentDir: string
29
29
  /**
30
30
  * 宿主已解析的 live session 目录(`ctx.sessionManager.getSessionDir()`,§6.1 信号 1)。
@@ -66,7 +66,7 @@ const SUBAGENTS_DIRNAME = 'subagents'
66
66
 
67
67
  /**
68
68
  * `[live]` 规范化(§6.1/§7B 要点 3):`getSessionDir()` 在纯 pi 返回按 cwd 编码的子目录
69
- *(`<agentDir>/sessions/--Users-x--`),在 xyz-agent 覆盖态返回根本身。判据:basename
69
+ *(`<agentDir>/sessions/--Users-x--`),在 taiji 覆盖态返回根本身。判据:basename
70
70
  * 匹配 encodeCwd 形态(`--` 开头 `--` 结尾,实证见 `~/.pi/agent/sessions/` 全部子目录
71
71
  * 与 real-data.ts)则取 dirname,否则取自身。
72
72
  */