@zhushanwen/pi-session-reader 0.11.0 → 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 (38) hide show
  1. package/package.json +5 -5
  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 +3 -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__/tool-handler.test.ts +133 -83
  15. package/src/__tests__/workflow.test.ts +5 -5
  16. package/src/__tests__/zcode-manifest.test.ts +18 -10
  17. package/src/__tests__/zcode-routing.test.ts +21 -19
  18. package/src/__tests__/zcode-whitelist.test.ts +1 -1
  19. package/src/core/family.ts +6 -6
  20. package/src/core/render.ts +2 -2
  21. package/src/core/toolcall.ts +1 -1
  22. package/src/core/workflow.ts +9 -9
  23. package/src/discovery/entry-anchor.ts +5 -5
  24. package/src/discovery/find.ts +9 -1
  25. package/src/discovery/roots.ts +95 -7
  26. package/src/discovery/subagents.ts +24 -17
  27. package/src/discovery/whitelist.ts +4 -4
  28. package/src/discovery/workflows.ts +8 -8
  29. package/src/discovery/zcode-manifest.ts +22 -27
  30. package/src/extract.ts +1 -1
  31. package/src/index.ts +2 -2
  32. package/src/no-match.ts +1 -1
  33. package/src/result-action.ts +1 -1
  34. package/src/search-across.ts +3 -3
  35. package/src/tool-format.ts +12 -12
  36. package/src/tool-handler.ts +27 -19
  37. package/src/tui/session-command.ts +1 -1
  38. package/src/zcode-anchor-classify.ts +2 -2
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
4
- import { join } from 'node:path'
4
+ import { join, dirname } from 'node:path'
5
5
 
6
6
  import { readZcodeManifest, listZcodeManifests } from '../discovery/zcode-manifest.js'
7
7
  import { findZcodeEntryAnchor } from '../discovery/entry-anchor.js'
@@ -10,10 +10,10 @@ import { findZcodeEntryAnchor } from '../discovery/entry-anchor.js'
10
10
  // 变体 M(设计 §4 场景 1a):zcode manifest 直读(§3.1 第①步主路径)+ 枚举(family 用)。
11
11
  //
12
12
  // fixture 对齐 F21 磁盘真实形态:zcode manifest **无 sessionFile 键**
13
- // (投影函数照写 sessionFile: undefined,JSON.stringify 丢键)、engine:'zcode'、
13
+ // (派生视图函数照写 sessionFile: undefined,JSON.stringify 丢键)、engine:'zcode'、
14
14
  // engineHandle.sessionRef 双键齐。
15
15
  //
16
- // 单元边界:只测「认出 zcode manifest + 提取锚」;白名单闸 / 错误码映射(U9)、
16
+ // 单元边界:只测「认出 zcode manifest + 提取锚」;白名单门禁 / 错误码映射(U9)、
17
17
  // entry 兜底定位语义(U7 已交付)不在本文件展开。
18
18
  // ============================================================
19
19
 
@@ -21,9 +21,13 @@ const DB_PATH = '/data/agent/engines/zcode/session-db/db.sqlite'
21
21
 
22
22
  let agentDir: string
23
23
 
24
- /** 在 <agentDir>/subagents/<slug>/records/ 下写一个 manifest 文件(手写 JSON 字符串,忠实控制键) */
25
- function writeManifest(slug: string, name: string, content: string): string {
26
- const recordsDir = join(agentDir, 'subagents', slug, 'records')
24
+ /**
25
+ * 在存储树分区 <root>/<sid>/subagents/records/ 下写一个 manifest 文件(手写 JSON 字符串,
26
+ * 忠实控制键)。[session-store-layout-inversion D6/单新根] 新布局无 cwd-slug 层,
27
+ * slug 参数消亡(保留签名兼容既有调用点,弃用改名 _slug);分区名固定合法 sid。
28
+ */
29
+ function writeManifest(_slug: string, name: string, content: string): string {
30
+ const recordsDir = join(dirname(agentDir), 'ext-sessions', 'root-session-test', 'subagents', 'records')
27
31
  mkdirSync(recordsDir, { recursive: true })
28
32
  const file = join(recordsDir, name)
29
33
  writeFileSync(file, content, 'utf8')
@@ -73,7 +77,10 @@ function recordLine(saId: string, sessionRef: Record<string, string>): string {
73
77
  }
74
78
 
75
79
  beforeEach(() => {
76
- agentDir = mkdtempSync(join(tmpdir(), 'zcode-manifest-test-'))
80
+ // agentDir 两层化(<mkdtemp>/agent):存储树推导 = dirname(agentDir)/ext-sessions,
81
+ // 单层直接当 agentDir 会把扫描根泄到 tmpdir 公共层(跨测试污染——先例 reaper/family-zcode 同修)
82
+ agentDir = join(mkdtempSync(join(tmpdir(), 'zcode-manifest-test-')), 'agent')
83
+ mkdirSync(agentDir, { recursive: true })
77
84
  })
78
85
  afterEach(() => {
79
86
  rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
@@ -353,12 +360,13 @@ describe('listZcodeManifests 枚举(family 节点列表来源)', () => {
353
360
  expect(manifests.find((m) => m.id === 'sa-s2')?.slug).toBe('worker') // agentName 兜底
354
361
  })
355
362
 
356
- it('subagents 根不存在 → 空数组(不报错;单节点不可达不炸整屏)', async () => {
357
- const bareDir = mkdtempSync(join(tmpdir(), 'zcode-manifest-bare-'))
363
+ it('存储树根不存在 → 空数组(不报错;单节点不可达不炸整屏)', async () => {
364
+ // agentDir 两层化:扫描根 = dirname(agentDir)/ext-sessions(mkdtemp 私有层内,防公共层污染)
365
+ const bareDir = join(mkdtempSync(join(tmpdir(), 'zcode-manifest-bare-')), 'agent')
358
366
  try {
359
367
  expect(await listZcodeManifests(bareDir)).toEqual([])
360
368
  } finally {
361
- rmSync(bareDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
369
+ rmSync(dirname(bareDir), { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
362
370
  }
363
371
  })
364
372
  })
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { mkdtempSync, writeFileSync, mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs'
4
- import { join } from 'node:path'
4
+ import { join, dirname } from 'node:path'
5
5
  import { DatabaseSync } from 'node:sqlite'
6
6
  import { Check } from 'typebox/value'
7
7
 
@@ -16,7 +16,7 @@ import sessionReaderExtension from '../index.js'
16
16
  import { setPiHandle } from '@zhushanwen/pi-extension-logger'
17
17
 
18
18
  // ============================================================
19
- // U9 路由 + 白名单闸 + 错误面集成测试(design session-reader-shared-core
19
+ // U9 路由 + 白名单闸 + 错误范围集成测试(design session-reader-shared-core
20
20
  // §3.1 三层定位链 / §3.4 七码 + 检查顺序三段递进 / §4 场景 1a 变体 M/E +
21
21
  // 场景 2 family + 场景 5 八类失败 + 场景 3 pi 零回归 + D4 schema 零变化)。
22
22
  //
@@ -53,8 +53,8 @@ beforeEach(() => {
53
53
  mkdirSync(agentDir, { recursive: true })
54
54
  mkdirSync(liveDir, { recursive: true })
55
55
  dbPath = join(root, ...ISOLATED_DB_SEGMENTS)
56
- // 结构化日志捕获:logger.warn 走 appendEntry 通道(不进 LLM 可见面——本文件同时
57
- // 是「降级留痕不进 LLM 可见面」契约的断言面)。测试后清注入防跨文件泄漏。
56
+ // 结构化日志捕获:logger.warn 走 appendEntry 通道(不进 LLM 可见部分——本文件同时
57
+ // 是「降级留痕不进 LLM 可见部分」契约的断言范围)。测试后清注入防跨文件泄漏。
58
58
  appendEntries = vi.fn()
59
59
  setPiHandle({ appendEntry: appendEntries } as unknown as Parameters<typeof setPiHandle>[0])
60
60
  })
@@ -135,7 +135,7 @@ function assistantMsg(
135
135
 
136
136
  /**
137
137
  * 建真实 fixture 隔离库(WAL + session/message/part/schema_migration 四表,行形态 =
138
- * zcode-session-source helpers 同款宿主 schema 0.16.5 消费面子集)。
138
+ * zcode-session-source helpers 同款宿主 schema 0.16.5 消费方子集)。
139
139
  * 测试进程跑 node(node:sqlite 可用);生产读链的驱动选择在 source 包内。
140
140
  */
141
141
  function createZcodeDb(path: string, schemaVersion = '0.16.5', sessions: SessionRows[] = []): void {
@@ -175,7 +175,9 @@ function writeZcodeManifest(
175
175
  saId: string,
176
176
  opts: { sessionRef?: Record<string, string>; omitEngineHandle?: boolean; engine?: string | true } = {},
177
177
  ): void {
178
- const recordsDir = join(agentDir, 'subagents', 'proj-slug', 'records')
178
+ // [session-store-layout-inversion D6/单新根] records 读侧 = 存储树分区固定位置
179
+ // <root>/<rootSid>/subagents/records/(旧根 <agentDir>/subagents 不再读写)。
180
+ const recordsDir = join(dirname(agentDir), 'ext-sessions', ROOT_SESSION, 'subagents', 'records')
179
181
  mkdirSync(recordsDir, { recursive: true })
180
182
  const m: Record<string, unknown> = {
181
183
  id: saId,
@@ -210,8 +212,8 @@ function recordLine(entryId: string, saId: string, sessionRef: Record<string, st
210
212
 
211
213
  /**
212
214
  * pi 形态的 subagent-record custom entry 行(回归 A1 instrument):engine 键缺省 +
213
- * sessionRef 无 dbPath——subagent-core record-entry.ts 的 pi 投影形态(engineHandle
214
- * 可选、sessionRef 整体透传不枚举内部键;最小可信子集,跨包漂移由 entry-anchor.test 守卫)。
215
+ * sessionRef 无 dbPath——subagent-core record-entry.ts 的 pi 派生视图形态(engineHandle
216
+ * 可选、sessionRef 整体透传不枚举内部键;最小可信子集,跨包漂移由 entry-anchor.test 检查)。
215
217
  */
216
218
  function piRecordLine(entryId: string, saId: string): string {
217
219
  return JSON.stringify({
@@ -220,7 +222,7 @@ function piRecordLine(entryId: string, saId: string): string {
220
222
  id: entryId,
221
223
  parentId: null,
222
224
  data: {
223
- // 登记 §3.3:条目面 = v2 族——pi 形态 = 注册条(身份)+ 终态条(engineHandle 半键)。
225
+ // 登记 §3.3:条目范围 = v2 族——pi 形态 = 注册条(身份)+ 终态条(engineHandle 半键)。
224
226
  v: 2,
225
227
  kind: 'settled',
226
228
  id: saId,
@@ -260,7 +262,7 @@ function writeRootSessionFile(): void {
260
262
 
261
263
  /** pi manifest + pi subagent session 文件(pi 穿插断言用;engine 缺省 = pi 存量形态)。 */
262
264
  function writePiFixture(): void {
263
- const subDir = join(agentDir, 'subagents', 'pi-slug', 'sessions')
265
+ const subDir = join(dirname(agentDir), 'ext-sessions', ROOT_SESSION, 'subagents', 'sessions')
264
266
  mkdirSync(subDir, { recursive: true })
265
267
  const piSession = join(subDir, '019e6c96-pi-sub-agent-00000000004a4.jsonl')
266
268
  writeFileSync(
@@ -272,7 +274,7 @@ function writePiFixture(): void {
272
274
  ].join('\n') + '\n',
273
275
  'utf8',
274
276
  )
275
- const recordsDir = join(agentDir, 'subagents', 'pi-slug', 'records')
277
+ const recordsDir = join(dirname(agentDir), 'ext-sessions', ROOT_SESSION, 'subagents', 'records')
276
278
  mkdirSync(recordsDir, { recursive: true })
277
279
  writeFileSync(
278
280
  join(recordsDir, 'sa-pi-1.json'),
@@ -433,7 +435,7 @@ describe('场景 5 八类失败(§3.4 表逐行 + 检查顺序三段递进)'
433
435
  await expect(handleSessionRead({ action: 'result', session: SA_M }, signals())).rejects.toThrow(
434
436
  'zcode_anchor_missing',
435
437
  )
436
- // 归因进结构化日志不进 LLM 可见面(§3.4):缺失键名可区分(missing-dbPath)
438
+ // 归因进结构化日志不进 LLM 可见部分(§3.4):缺失键名可区分(missing-dbPath)
437
439
  expect(logCallsWithReason('missing-dbPath')).toBeGreaterThanOrEqual(1)
438
440
  })
439
441
 
@@ -521,14 +523,14 @@ describe('场景 5 八类失败(§3.4 表逐行 + 检查顺序三段递进)'
521
523
  })
522
524
 
523
525
  // ---------------------------------------------------------------------------
524
- // 驱动探测失败的错误面契约(跨包漂移守卫 + L4 包装形态映射)
526
+ // 驱动探测失败的错误契约(跨包漂移检查 + L4 包装形态映射)
525
527
  // ---------------------------------------------------------------------------
526
528
 
527
- describe('驱动探测失败错误面:跨包契约 + L4 包装形态映射', () => {
529
+ describe('驱动探测失败错误范围:跨包契约 + L4 包装形态映射', () => {
528
530
  it('跨包契约:SQLITE_DRIVER_UNSUPPORTED_MARK 子串在 zcode-session-source sqlite-driver 源文内(漂移即红)', () => {
529
531
  // 驱动探测错误无专用错误类型(该包不导出子类),reader 按消息特征识别——mark 与
530
532
  // sqlite-driver 源内错误字面量的绑定由本测试机器锚定(entry-anchor.test.ts 协议
531
- // 字面量跨包漂移守卫同范式):源文措辞变更不再含 mark → host_unsupported 面死亡,红。
533
+ // 字面量跨包漂移检查同范式):源文措辞变更不再含 mark → host_unsupported 契约死亡,红。
532
534
  const source = readFileSync(
533
535
  new URL('../../../../../packages/zcode-session-source/src/sqlite-driver.ts', import.meta.url),
534
536
  'utf8',
@@ -689,7 +691,7 @@ describe('pi 穿插不受影响:同 fixture 内 pi 会话读取与 zcode 调
689
691
  /** `session_read` 输入 schema 的 JSON 序列化快照(防无意漂移的逐字节锚)。
690
692
  *
691
693
  * 快照锚的意图 = 无意漂移拦截(参数增删 / 文案改动即红,迫使有意确认);
692
- * 有意的声明面压缩须同批重新生成快照。当前快照 = tool-declaration-slimming
694
+ * 有意的声明侧压缩须同批重新生成快照。当前快照 = tool-declaration-slimming
693
695
  * U-A1 压缩终态(D1 三层分工:action 功能语义唯一承载点 = 枚举 description;
694
696
  * 工具 description 只留定位 + 边界;guidelines 只留纪律)。重新生成:
695
697
  * tsx 起 extension 捕获 registerTool 的 parameters 后 JSON.stringify。
@@ -748,12 +750,12 @@ describe('session_read schema 快照(D4:不加任何工具参数,逐字节
748
750
 
749
751
  describe('session_read 锚词保留清单(D2:guidelines 纪律逐词锚定)', () => {
750
752
  // SCHEMA_SNAPSHOT 只逐字节锚 parameters schema,不覆盖 guidelines——本块是
751
- // guidelines 锚词的机器防线:弱模型回归(P7/PB-weak)后对本工具声明面的
753
+ // guidelines 锚词的机器防线:弱模型回归(P7/PB-weak)后对本工具声明侧的
752
754
  // 再压缩 / 措辞改动,逐词红灯迫使有意确认。清单与 index.ts 注释登记一致:
753
755
  // 1. "Progressive reading" —— 阅读纪律 outline → expand → detail
754
756
  // 2. "Never read raw .jsonl" —— 禁直读原始 session 文件
755
- // 3. "sa- ids route" —— zcode 路由对 LLM 的唯一可见面(U9)
756
- // 4. "👉 recovery hint" —— 错误面恢复指针契约
757
+ // 3. "sa- ids route" —— zcode 路由对 LLM 的唯一可见部分(U9)
758
+ // 4. "👉 recovery hint" —— 错误范围恢复指针契约
757
759
  // 5. "doctor" —— 环境自检入口(另被上方快照 action 枚举间接锚定)
758
760
  it('guidelines 5 锚词逐个在位(缺一词即红)', () => {
759
761
  const registerTool = vi.fn()
@@ -77,7 +77,7 @@ describe('assertZcodeDbPathAllowed:检查顺序三段递进(存在性先于
77
77
  )
78
78
  })
79
79
 
80
- it('第 2 段路径:存在但集合外 → zcode_db_path_forbidden(安全面错误语义更重)', () => {
80
+ it('第 2 段路径:存在但集合外 → zcode_db_path_forbidden(安全侧错误语义更重)', () => {
81
81
  const outside = join(root, 'evil.sqlite')
82
82
  writeFileSync(outside, '', 'utf8')
83
83
  try {
@@ -38,7 +38,7 @@ export interface SubagentRef extends SessionRef {
38
38
  task?: string
39
39
  /** agent 类型名(manifest.agentName,P-fallback 时 identity.data.agent;同语义异名) */
40
40
  agentName?: string
41
- /** 模型 id(仅 manifest 有;P-fallback 时 undefined)。仅 details 可见,不进文本渲染(D2②:展示面只取 status/agentName/task) */
41
+ /** 模型 id(仅 manifest 有;P-fallback 时 undefined)。仅 details 可见,不进文本渲染(D2②:展示侧只取 status/agentName/task) */
42
42
  model?: string
43
43
  /** 终态 completed/failed/running(仅 manifest 有;P-fallback 时 undefined) */
44
44
  status?: string
@@ -60,7 +60,7 @@ export interface Family {
60
60
  forks: SessionRef[]
61
61
  /** 含隔代(design §3.3 D-7 Q1:对 fork 链每个节点查 subagentsByRoot 合并) */
62
62
  subagents: SubagentRef[]
63
- /** M1 恒返回 [],workflow 腿需读 workflow-state 文件(IO)归 M2 */
63
+ /** M1 恒返回 [],workflow 路径需读 workflow-state 文件(IO)归 M2 */
64
64
  workflows: WorkflowRef[]
65
65
  }
66
66
 
@@ -146,12 +146,12 @@ export function buildZcodeSubagentsByRoot(
146
146
  return byRoot
147
147
  }
148
148
 
149
- // ---- 类型守卫:从 unknown 的 entry.data 提取 subagent identity 字段 ----
149
+ // ---- 类型检查:从 unknown 的 entry.data 提取 subagent identity 字段 ----
150
150
 
151
151
  /**
152
152
  * identity 尾行 data 结构(读取视图,DM-IdentityData)。
153
153
  *
154
- * 守卫只强制 rootSessionId+slug(m0 契约),其余富字段全 optional——由 buildFamilyFromFs
154
+ * 检查只强制 rootSessionId+slug(m0 契约),其余富字段全 optional——由 buildFamilyFromFs
155
155
  * 按 manifest 主/P-fallback 回退路径组装时决定是否填入(manifest 主填全,P-fallback
156
156
  * 只填 task/agent/sessionFile,model/status 缺)。
157
157
  */
@@ -178,7 +178,7 @@ interface SubagentIdentityData {
178
178
  function isSubagentIdentityData(v: unknown): v is SubagentIdentityData {
179
179
  if (typeof v !== 'object' || v === null) return false
180
180
  const obj = v as Record<string, unknown>
181
- // 守卫放宽:只校验 rootSessionId+slug 必填(m0 契约),task/agent/model/status/sessionFile optional
181
+ // 检查放宽:只校验 rootSessionId+slug 必填(m0 契约),task/agent/model/status/sessionFile optional
182
182
  return typeof obj.rootSessionId === 'string' && typeof obj.slug === 'string'
183
183
  }
184
184
 
@@ -380,7 +380,7 @@ export function resolveFamily(sessionId: string, index: FamilyIndex): Family {
380
380
  }
381
381
  }
382
382
 
383
- // 5. workflows:M1 恒 [],workflow 腿需读 workflow-state 文件(IO)归 M2
383
+ // 5. workflows:M1 恒 [],workflow 路径需读 workflow-state 文件(IO)归 M2
384
384
  const workflows: WorkflowRef[] = []
385
385
 
386
386
  return { root, parents, forks, subagents, workflows }
@@ -79,7 +79,7 @@ export interface OutlineOptions {
79
79
  }
80
80
 
81
81
  // ---------------------------------------------------------------------------
82
- // content 类型守卫与提取(message.content 是 unknown,design §3.4)
82
+ // content 类型检查与提取(message.content 是 unknown,design §3.4)
83
83
  // ---------------------------------------------------------------------------
84
84
 
85
85
  interface ContentBlock {
@@ -454,7 +454,7 @@ export function renderOutline(
454
454
  const allBranches = options?.allBranches ?? false
455
455
  const granularity = options?.granularity ?? 'turn'
456
456
 
457
- // 字节/条目统计保持全量口径(折叠是展示层行为,不丢数据面);行与轮次计数走对话集
457
+ // 字节/条目统计保持全量口径(折叠是展示层行为,不丢数据);行与轮次计数走对话集
458
458
  const parsedBytes = leafParsedBytes(turns)
459
459
  const leafEntryCount = turns.reduce((s, t) => s + t.entries.length, 0)
460
460
  // totalEntries = leaf + 旁支子树 + 孤儿;totalBytes 无原始文件字节数(签名不含 ParseResult),
@@ -69,7 +69,7 @@ function coerceArgs(raw: unknown): Record<string, unknown> {
69
69
  /**
70
70
  * 从 entry.message.content 的 `{type:"toolCall"}` block 提取工具调用列表。
71
71
  *
72
- * content 是 unknown 做类型守卫;非数组或无 toolCall block 返 []。
72
+ * content 是 unknown 做类型检查;非数组或无 toolCall block 返 []。
73
73
  * 仅纳入 id + name 均为 string 的 block(O2 靠 id 关联 toolResult,O1 靠 name 聚合;
74
74
  * 缺 id 的块无法关联,缺 name 无法摘要,跳过——实测 0 缺失)。
75
75
  */
@@ -15,7 +15,7 @@
15
15
 
16
16
  import { isRecord } from '@zhushanwen/pi-ext-guards'
17
17
 
18
- // ---- 类型守卫 helpers ----
18
+ // ---- 类型检查 helpers ----
19
19
 
20
20
  /** unknown → string 收窄(非 string → undefined)。快照可选字符串字段的统一入口。 */
21
21
  function strOr(v: unknown): string | undefined {
@@ -74,9 +74,9 @@ export interface WorkflowOverview {
74
74
  runId: string
75
75
  /** WorkflowRef.stateFile 透传 */
76
76
  stateFile: string
77
- /** NEW state.status / OLD 顶层 status / record 流投影([D2] 三态:running | interrupted | done) */
77
+ /** NEW state.status / OLD 顶层 status / record 流派生视图([D2] 三态:running | interrupted | done) */
78
78
  status: string
79
- /** details 格式标记(随 details.runs 透传给程序化消费方,文本渲染不读)。v2 读取面形状与 v1 一致(pi-subagent-workflow 8.x 一次性生命周期) */
79
+ /** details 格式标记(随 details.runs 透传给程序化消费方,文本渲染不读)。v2 读侧形状与 v1 一致(pi-subagent-workflow 8.x 一次性生命周期) */
80
80
  version: 'wf-run-v1' | 'wf-run-v2' | 'legacy'
81
81
  /** NEW spec.scriptName / spec.name / OLD name */
82
82
  script?: string
@@ -222,12 +222,12 @@ function parseLegacyRunSnapshot(
222
222
  *
223
223
  * 分支(C-parserunsnapshot-dualformat,TC-wf-snapshot-version-union):
224
224
  * - 非对象 → null(调用方跳过,ES-wf-snapshot-unparseable)
225
- * - NEW (snapshot.v === 'wf-run-v1' 或 'wf-run-v2',读取面形状一致):state.* / meta.* / spec.*
225
+ * - NEW (snapshot.v === 'wf-run-v1' 或 'wf-run-v2',读侧形状一致):state.* / meta.* / spec.*
226
226
  * - OLD (无 v,有 callCache 数组或顶层 status):顶层 status/budget/startedAt + callCache
227
227
  * - 既非 NEW 也非 OLD → null(未来版本(如 wf-run-v3)/ 异构内容)
228
228
  *
229
229
  * runId/stateFile 透传参数(不读 snapshot.runId,保证与 family.workflows 一致,避免 OLD 顶层
230
- * runId 可信度低的不一致)。零 any(全程 typeof/Array.isArray/isRecord 守卫收窄)。
230
+ * runId 可信度低的不一致)。零 any(全程 typeof/Array.isArray/isRecord 检查收窄)。
231
231
  */
232
232
  export function parseRunSnapshot(
233
233
  snapshot: unknown,
@@ -236,7 +236,7 @@ export function parseRunSnapshot(
236
236
  ): WorkflowOverview | null {
237
237
  if (!isRecord(snapshot)) return null
238
238
 
239
- // NEW 格式(v === 'wf-run-v1' || 'wf-run-v2',v2 读取面形状兼容 v1)
239
+ // NEW 格式(v === 'wf-run-v1' || 'wf-run-v2',v2 读侧形状兼容 v1)
240
240
  const v = snapshot.v
241
241
  if (v === 'wf-run-v1' || v === 'wf-run-v2') {
242
242
  return parseNewRunSnapshot(snapshot, v, runId, stateFile)
@@ -331,7 +331,7 @@ interface RecordStreamLine {
331
331
  [key: string]: unknown
332
332
  }
333
333
 
334
- /** 流解析产物:run 装配面(runId 透传 + stateFile=record 流路径)。 */
334
+ /** 流解析产物:run 装配侧(runId 透传 + stateFile=record 流路径)。 */
335
335
  interface RecordStreamFold {
336
336
  workflowName?: string
337
337
  startedAt?: number
@@ -371,7 +371,7 @@ function parseRecordStreamLine(line: string): RecordStreamLine | undefined {
371
371
  * sessionId——家族链跳转入口);
372
372
  * - run-interrupted:[D2] 中断转移 → status 'interrupted'(非终局);
373
373
  * - run-resumed:[D2] 复活转移 → 清中断标记(status 回 running——lifecycle fold
374
- * 投影语义:run-resumed 是 interrupted → running 的转移证据,跳过会使 resume
374
+ * 派生视图语义:run-resumed 是 interrupted → running 的转移证据,跳过会使 resume
375
375
  * 续跑期间的概览停在 interrupted 直至终局);
376
376
  * - run-settled:终局帧 → status 'done' + outcome(done/failed/cancelled/time_limited);
377
377
  * - 其余(phase-started / phase-settled):跳过(状态机转移不进概览文本——phase 分组展示
@@ -445,7 +445,7 @@ function applyAgentSettledToFold(
445
445
  })
446
446
  }
447
447
 
448
- /** [parseRunRecordStream 拆分] status 三态投影([D2]):终局帧 > 中断转移 > running(空流也 running——活跃兜底)。 */
448
+ /** [parseRunRecordStream 拆分] status 三态派生视图([D2]):终局帧 > 中断转移 > running(空流也 running——活跃兜底)。 */
449
449
  function projectRecordStreamStatus(fold: RecordStreamFold): {
450
450
  status: string
451
451
  reason: string | undefined
@@ -29,16 +29,16 @@ export interface ZcodeAnchor {
29
29
  * `subagent-record` custom entry 的 customType 值。
30
30
  *
31
31
  * 磁盘 JSONL 协议字符串(跨侧契约):写侧单源 = subagent-core record-entry.ts 的
32
- * SUBAGENT_RECORD_CUSTOM_TYPE。reader 生产依赖面不引 subagent-core(该包是
32
+ * SUBAGENT_RECORD_CUSTOM_TYPE。reader 生产依赖范围不引 subagent-core(该包是
33
33
  * devDependency + vitest alias 形态),故按协议字符串本地持有;两侧漂移由
34
- * entry-anchor.test.ts 的跨包断言守卫。
34
+ * entry-anchor.test.ts 的跨包断言检查。
35
35
  */
36
36
  const SUBAGENT_RECORD_CUSTOM_TYPE = 'subagent-record'
37
37
 
38
38
  /**
39
39
  * v2 条目 data 的认识版本词表(W1 起 = 2)。与写侧 subagent-core
40
- * SUBAGENT_RECORD_ENTRY_VERSION 同值、同「本地持有 + 测试守卫漂移」模式——写侧
41
- * 再升 v3 时本守卫先红,提醒评估新版本的锚读取面。
40
+ * SUBAGENT_RECORD_ENTRY_VERSION 同值、同「本地持有 + 测试检查漂移」模式——写侧
41
+ * 再升 v3 时本检查先红,提醒评估新版本的锚读取范围。
42
42
  */
43
43
  const SUBAGENT_RECORD_ENTRY_V2 = 2
44
44
 
@@ -46,7 +46,7 @@ const SUBAGENT_RECORD_ENTRY_V2 = 2
46
46
  * 从单条 entry 提取 zcode 锚。形状校验窄而严,任一不满足 = 该条不算命中(返回
47
47
  * undefined,调用方继续扫——缺键条目不遮蔽更早的完整条目):
48
48
  * - `type === 'custom'` ∧ `customType === 'subagent-record'`
49
- * - 版本/判别键(schema 版本守卫,不认识的版本跳过而非猜测——写侧
49
+ * - 版本/判别键(schema 版本检查,不认识的版本跳过而非猜测——写侧
50
50
  * record-entry.ts classifySubagentRecordEntryData v2-only 同口径):
51
51
  * `data.v === 2` ∧ `data.kind === 'settled'`(W1 注册/终态两条小条目——
52
52
  * **锚只取自终态条**:registered 条目不携带 engineHandle,且设计 D1 裁决
@@ -433,6 +433,14 @@ async function matchCandidates(
433
433
  }
434
434
  // 第一级 uuid 片段匹配(sessionId 或文件路径含 query)——cheap,已有 header。
435
435
  // 零命中才进入第二级(互斥回退,非合并排序,§6.7 子决策 1)。
436
+ // [session-store-layout-inversion] 完整 id 精确等值短路:存储树分区目录名 =
437
+ // rootSessionId(`<root>/<rootSessionId>/subagents/sessions/<sid>.jsonl`),根会话 id
438
+ // 会出现在其全部 subagent 候选的路径里——不短路则完整 id 直查根会话被伪 multi 消歧。
439
+ // 等值命中是 includes 命中的子集,短路只消伪 multi 不减真召回。
440
+ const exactIdHits = candidates.filter((c) => c.ref.sessionId === query)
441
+ if (exactIdHits.length > 0) {
442
+ return exactIdHits.map((c) => ({ ...c }))
443
+ }
436
444
  const exactHits = candidates.filter(
437
445
  (c) => c.ref.sessionId.includes(query) || c.meta.path.includes(query),
438
446
  )
@@ -626,7 +634,7 @@ export interface SessionFileRef {
626
634
  *
627
635
  * id 取自首行 header(复用 collectCandidates 的首行扫描——与 find 候选、resolveSessionId
628
636
  * 同一 id 语义,agent 从 find 输出复制的完整 id 恒能命中;不按文件名反推,避免「文件改名
629
- * 后与 header id 失配」的误报面)。workflow-state 跳过、坏 header 跳过、被去重根不产候选
637
+ * 后与 header id 失配」的误报风险)。workflow-state 跳过、坏 header 跳过、被去重根不产候选
630
638
  * 均随 collectCandidates 继承。同 id 多根出现([live] 与 [default] 不去重,V2 语义)时取
631
639
  * mtime 新者(与 loadTitleIndex 合并语义一致)。
632
640
  *
@@ -64,6 +64,68 @@ export interface SessionRoot {
64
64
  const SESSIONS_DIRNAME = 'sessions'
65
65
  const SUBAGENTS_DIRNAME = 'subagents'
66
66
 
67
+ /**
68
+ * taiji session 存储树根段(`<dataDir>/ext-sessions/`)。
69
+ *
70
+ * session-reader 是 extension 不 import @taiji/shared——本常量是 shared
71
+ * `SESSION_STORE_DIR_SEGMENT` 的同字面镜像,dataDir 推导先例 = `dirname(agentDir)`
72
+ * 基址(布局 B `agentDir = <dataDir>/agent`;同 tool-handler doExport 导出落点)。
73
+ * 字面量一致性由对拍机检锚点 `scripts/check-artifact-dir-formula-sync.mjs`(域 9
74
+ * session-reader discovery 断言位)锁定,树根段改名时 shared 与此处同批改。
75
+ */
76
+ const SESSION_STORE_DIR_SEGMENT = 'ext-sessions'
77
+
78
+ /**
79
+ * 存储树分区目录名合法性正则(镜像 shared `PI_SESSION_ID_PATTERN`,同域规则:首尾
80
+ * 字母数字,中间允许 [A-Za-z0-9._-];禁 `:`——btw 虚拟 id 必拒)。对拍机检锚点同上。
81
+ *
82
+ * [session-store-layout-inversion] 存储树扫描(subagent 根 / records manifest)以
83
+ * 此过滤 `<root>/` 一级目录——既是剪枝也是路径注入防线(目录名即 sessionId)。
84
+ */
85
+ const PI_SESSION_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/
86
+
87
+ /**
88
+ * session 存储树根目录(`<dataDir>/ext-sessions/`,dataDir = dirname(agentDir))。
89
+ *
90
+ * 新布局全部 session 维度衍生数据的统一落点(布局权威 = shared getSessionStoreDir,
91
+ * 本函数是 extension 侧镜像推导);旧布局 `<agentDir>/subagents` 不再读写(设计裁决
92
+ * D6:旧布局存量原位保留、不可见)。
93
+ */
94
+ export function resolveSessionStoreRoot(agentDir: string): string {
95
+ return join(dirname(agentDir), SESSION_STORE_DIR_SEGMENT)
96
+ }
97
+
98
+ /** 存储树某根会话分区的 subagent 域目录(`<root>/<rootSessionId>/subagents/`)。 */
99
+ export function resolveStoreSubagentsDir(agentDir: string, rootSessionId: string): string {
100
+ return join(resolveSessionStoreRoot(agentDir), rootSessionId, SUBAGENTS_DIRNAME)
101
+ }
102
+
103
+ /** subagent 会话 jsonl 目录(`<root>/<rootSessionId>/subagents/sessions/`)。 */
104
+ export function resolveStoreSubagentSessionsDir(agentDir: string, rootSessionId: string): string {
105
+ return join(resolveStoreSubagentsDir(agentDir, rootSessionId), 'sessions')
106
+ }
107
+
108
+ /** ExecutionRecord manifest 目录(`<root>/<rootSessionId>/subagents/records/`)。 */
109
+ export function resolveStoreSubagentRecordsDir(agentDir: string, rootSessionId: string): string {
110
+ return join(resolveStoreSubagentsDir(agentDir, rootSessionId), 'records')
111
+ }
112
+
113
+ /**
114
+ * 枚举存储树 `<root>/` 一级合法会话分区目录名(isPiSessionId 过滤,见
115
+ * PI_SESSION_ID_PATTERN 注释)。根不存在/无权限 → 空数组,不抛错。
116
+ */
117
+ export async function listSessionStorePartitions(storeRoot: string): Promise<string[]> {
118
+ let entries: Dirent[]
119
+ try {
120
+ entries = await readdir(storeRoot, { withFileTypes: true })
121
+ } catch {
122
+ return []
123
+ }
124
+ return entries
125
+ .filter((e) => e.isDirectory() && PI_SESSION_ID_PATTERN.test(e.name))
126
+ .map((e) => e.name)
127
+ }
128
+
67
129
  /**
68
130
  * `[live]` 规范化(§6.1/§7B 要点 3):`getSessionDir()` 在纯 pi 返回按 cwd 编码的子目录
69
131
  *(`<agentDir>/sessions/--Users-x--`),在 taiji 覆盖态返回根本身。判据:basename
@@ -90,7 +152,10 @@ function sessionRootSpecs(signals: SessionRootSignals): SessionRoot[] {
90
152
  }
91
153
  specs.push(spec('default', 'main', join(signals.agentDir, SESSIONS_DIRNAME)))
92
154
  specs.push(spec('legacy', 'main', join(dirname(signals.agentDir), SESSIONS_DIRNAME)))
93
- specs.push(spec('subagent', 'subagent', join(signals.agentDir, SUBAGENTS_DIRNAME)))
155
+ // [session-store-layout-inversion] subagent 根 = 存储树根(`<dataDir>/ext-sessions`),
156
+ // 扫描在 sid 分区内定向探 subagents/sessions/(scanStoreSubagentJsonl);旧布局
157
+ // `<agentDir>/subagents` 不再读(设计裁决 D6:存量原位保留、不可见)。
158
+ specs.push(spec('subagent', 'subagent', resolveSessionStoreRoot(signals.agentDir)))
94
159
  return specs
95
160
  }
96
161
 
@@ -120,7 +185,7 @@ const SKIP_DIRS_NONE = new Set<string>()
120
185
 
121
186
  // ============================================================
122
187
  // 扫描 options(u8 追加:doctor「同一数据源两处渲染,不重复实现探测」§6.3——
123
- // doctor 经 options 驱动 subagent 根不扫。原 u8 的进程内缓存注入面已删除
188
+ // doctor 经 options 驱动 subagent 根不扫。原 u8 的进程内缓存注入点已删除
124
189
  // (ext-simplify-04 U3):每次调用实扫,本模块不再持有任何缓存语义)
125
190
  // ============================================================
126
191
 
@@ -139,11 +204,12 @@ export interface SessionRootScanOptions {
139
204
  *
140
205
  * 优先级 live > default > legacy > subagent;按 realpath 去重:同路径多信号只扫一次,
141
206
  * 保留最高优先级 kind 标签,被去重的根以 `dedupedInto` 标注(不产生 fileCount/scanMs)。
142
- * 逐根复用 scanJsonlRecursive(main 源跳 workflow-state;subagent 源不跳)。单根失败
207
+ * 逐根扫描:main 源整树递归(跳 workflow-state);subagent 源 = 存储树定向扫描
208
+ *([session-store-layout-inversion] 单新根,见 scanStoreSubagentJsonl)。单根失败
143
209
  * (不存在/无权限)→ 空结果继续,不抛错(沿用 listXxxSessions 契约)。
144
210
  *
145
211
  * options(u8 追加,全部缺省安全):`subagents:'stat'` 让 subagent 根只做存在性检查。
146
- * 无缓存——每次调用实扫(原 u8 cache 注入面已删除,ext-simplify-04 U3;find/F1
212
+ * 无缓存——每次调用实扫(原 u8 cache 注入点已删除,ext-simplify-04 U3;find/F1
147
213
  * 自检行计数与 doctor 表计数同取本次实扫,§7B 要点 8)。
148
214
  */
149
215
  export async function resolveSessionRoots(
@@ -166,9 +232,13 @@ export async function resolveSessionRoots(
166
232
  out.push({ ...candidate, exists: await pathExists(candidate.path) })
167
233
  continue
168
234
  }
169
- const skipDirs = candidate.source === 'main' ? SKIP_DIRS_MAIN : SKIP_DIRS_NONE
235
+ // subagent 源 = 存储树定向扫描(sid 分区剪枝,见 scanStoreSubagentJsonl);
236
+ // main 源维持整树递归(跳 workflow-state)
170
237
  const t0 = performance.now()
171
- const files = await scanJsonlRecursive(candidate.path, skipDirs)
238
+ const files =
239
+ candidate.source === 'subagent'
240
+ ? await scanStoreSubagentJsonl(candidate.path)
241
+ : await scanJsonlRecursive(candidate.path, SKIP_DIRS_MAIN)
172
242
  const scanMs = performance.now() - t0
173
243
  const exists = await pathExists(candidate.path)
174
244
  const root: SessionRoot = {
@@ -194,7 +264,7 @@ export interface SessionFileMeta {
194
264
  /**
195
265
  * main sessions 扫描时整体跳过的子目录名。
196
266
  * `workflow-state` 目录存放 workflow 运行状态文件(wf-*.jsonl,首行 `{"v":"wf-run-v1"|"wf-run-v2"...}`,
197
- * 版本随 subagent-workflow 快照格式演进,读取侧 v1/v2 兼容),非 session 文件——属 family 腿
267
+ * 版本随 subagent-workflow 快照格式演进,读取侧 v1/v2 兼容),非 session 文件——属 family 路径
198
268
  * 独立处理(design §3.3 D-7),扫描 main sessions 时排除,
199
269
  * 否则会把 wf 文件误收为 session(且 find.ts 读其首行 header 时会因 type≠session 被丢弃,
200
270
  * 在此排除可避免这批无效首行扫描)。
@@ -210,6 +280,24 @@ function isSessionJsonl(name: string): boolean {
210
280
  return name.endsWith('.jsonl')
211
281
  }
212
282
 
283
+ /**
284
+ * 存储树 subagent 根定向扫描([session-store-layout-inversion] 读侧单新根):
285
+ * 只探 `<root>/<rootSessionId>/subagents/sessions/` 固定位置。`<root>/` 一级只进
286
+ * isPiSessionId 形态目录(剪枝:不进 logs/attachments 等大目录,也挡非分区名目录);
287
+ * 每 sid 分区内 records/ model-switch/ 不扫(会话文件域独立)。sessions/ 内部维持
288
+ * 递归 + stat(复用 scanJsonlRecursive,`.finalized` 天然排除)。存储树根或分区目录
289
+ * 不存在/无权限 → 空结果,不抛错(listXxxSessions 契约)。
290
+ */
291
+ async function scanStoreSubagentJsonl(storeRoot: string): Promise<SessionFileMeta[]> {
292
+ const results: SessionFileMeta[] = []
293
+ for (const sid of await listSessionStorePartitions(storeRoot)) {
294
+ results.push(
295
+ ...(await scanJsonlRecursive(join(storeRoot, sid, SUBAGENTS_DIRNAME, 'sessions'), SKIP_DIRS_NONE)),
296
+ )
297
+ }
298
+ return results
299
+ }
300
+
213
301
  /**
214
302
  * 递归扫描 rootDir 下所有 .jsonl 文件(排除 .finalized),返回绝对路径 + mtime + size。
215
303
  * `skipDirs` 命名的目录整体跳过。目录不存在/无权限 → 返回空数组,不抛错(design §2 坏路径容错)。