@zhushanwen/pi-session-reader 0.5.1 → 0.6.1

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.
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-session-reader",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
- "xyz-agent": {
6
+ "taiji": {
7
7
  "role": "universal"
8
8
  },
9
9
  "pi": {
@@ -15,10 +15,13 @@
15
15
  "keywords": [
16
16
  "pi-package"
17
17
  ],
18
+ "dependencies": {
19
+ "@zhushanwen/pi-ext-guards": "0.4.1"
20
+ },
18
21
  "devDependencies": {
19
22
  "@vitest/coverage-v8": "^4.1.9",
20
23
  "vitest": "^4.1.8",
21
- "@zhushanwen/subagent-core": "0.10.0"
24
+ "@zhushanwen/subagent-core": "0.10.5"
22
25
  },
23
26
  "files": [
24
27
  "index.ts",
@@ -20,20 +20,21 @@ import {
20
20
  } from '@zhushanwen/subagent-core/execution/assembly/path-encoding.ts'
21
21
 
22
22
  /**
23
- * W4 跨包集成测试(subagent-sync-collect v2 impl-plan W4 验收条款①,设计 §5 W4:
24
- * 「subagent-core 真实 tmpdir 产出磁盘状态 → session-reader result action sa- id 反查,
25
- * 不 mock manifest 写入」)。
23
+ * W4 跨包集成测试([collect 退役] 改造保留:原「E1 落标出口产出 manifest」驱动面
24
+ * 随 sync 批机制删除,现行真实投影出口 = store 重建通道 rebuildIndexes——manifest
25
+ * 是可丢缓存,boot 全量腿按磁盘子文件锚幂等补缺)。「subagent-core 真实 tmpdir
26
+ * 产出磁盘状态 → session-reader result action sa- id 反查,不 mock manifest 写入」
27
+ * 的跨包契约保持不变。
26
28
  *
27
- * 与 result.test.ts 的分工:result.test.ts 的 fixture **手工预写** manifest(U6 时期
28
- * W3 投影代码尚不存在);本文件 manifest 内容必须来自 v2 W3 的投影代码路径——
29
- * subagent-core 侧真实驱动 E1 崩溃恢复(kill -9 同构链路:真实 RecordStore 落盘
30
- * register/轮终 entry → initSession 内 orphan 覆写真实跑 → recoverSyncCollectBatch
31
- * 落标出口 appendBatchFinalizedEntry fire-and-forget 真写 records/<sa-id>.json),
32
- * session-reader 侧零 mock 读同一 tmpdir 断言反查闭环。
29
+ * 与 result.test.ts 的分工:result.test.ts 的 fixture **手工预写** manifest;本文件
30
+ * manifest 内容必须来自投影代码路径——subagent-core 侧真实驱动(真实 RecordStore
31
+ * 落盘 register/轮终 entry → initSession 内 orphan 覆写真实跑 → rebuildIndexes
32
+ * derivedManifestRecord 投影真写 records/<sa-id>.json),session-reader 侧零 mock
33
+ * 读同一 tmpdir 断言反查闭环。
33
34
  *
34
35
  * 构造形态取自 subagent-core sync-collect-recovery.test.ts 的 kill -9 同构主用例
35
- * (W1 领地,形态同构是 v1 教训「种子绕过真实链路 = 假绿」的执行):子 session 文件
36
- * 在其基础上补 message entries(result action 的正文提取源)。
36
+ * (形态同构是 v1 教训「种子绕过真实链路 = 假绿」的执行):子 session 文件在其
37
+ * 基础上补 message entries(result action 的正文提取源)。
37
38
  *
38
39
  * mock 面最小:仅构造 SubagentService 所需的最小 pi 形状(appendEntry 真写主文件,
39
40
  * 其余 no-op stub)——零 vi.mock、notifier/store/config 全真实实现,agentDir 全部
@@ -48,9 +49,9 @@ const ENV_KEYS_TO_STRIP = [
48
49
  'PI_SUBAGENT_DEPTH',
49
50
  'PI_SUBAGENT_ROOT_CWD',
50
51
  'PI_SUBAGENT_FORK_DEPTH',
51
- 'XYZ_SUBAGENT_RELAY_SOCKET',
52
- 'XYZ_SUBAGENT_RELAY_NODE',
53
- 'XYZ_SUBAGENT_RELAY_SCRIPT',
52
+ 'TAIJI_SUBAGENT_RELAY_SOCKET',
53
+ 'TAIJI_SUBAGENT_RELAY_NODE',
54
+ 'TAIJI_SUBAGENT_RELAY_SCRIPT',
54
55
  ] as const
55
56
 
56
57
  const ROOT_SESSION = 'root-cross-pkg'
@@ -102,8 +103,6 @@ function memberRecord(overrides: Partial<SubagentRecord> & { id: string }): Suba
102
103
  result: undefined,
103
104
  error: undefined,
104
105
  sessionFile: undefined,
105
- chatMode: false,
106
- collectMode: 'sync',
107
106
  ...overrides,
108
107
  }
109
108
  }
@@ -118,7 +117,7 @@ interface MemberSeed {
118
117
  model: string
119
118
  }
120
119
 
121
- describe('跨包集成:subagent-core E1 落标 manifest → session-reader result 反查(W4)', () => {
120
+ describe('跨包集成:subagent-core 重建 manifest → session-reader result 反查(W4)', () => {
122
121
  let agentDir: string
123
122
  let mainFile: string
124
123
  let services: SubagentService[]
@@ -213,31 +212,22 @@ describe('跨包集成:subagent-core E1 落标 manifest → session-reader res
213
212
  return childFile
214
213
  }
215
214
 
216
- /** 种「崩溃前主文件末条序列」:register(running+sync)→ 轮终(running+resumable+
217
- * result 全文 + sessionFile)——成功成员崩溃时的真实末条形态。 */
215
+ /** 种「崩溃前主文件末条序列」:register(running+sync)→ 轮终(idle + result
216
+ * 全文 + sessionFile)——[U4/D4] 翻边后成功成员崩溃时的真实末条形态(U5 前的
217
+ * running+resumable 桥接形态已随字段退役消亡)。 */
218
218
  function seedRoundTerminalEntries(m: MemberSeed, childFile: string): void {
219
219
  const store = makeSeedStore()
220
220
  store.reportSubagentRecord(memberRecord({ id: m.id, sessionFile: childFile, model: m.model }))
221
221
  store.reportSubagentRecord(
222
- memberRecord({ id: m.id, sessionFile: childFile, model: m.model, resumable: true, result: m.resultText }),
222
+ memberRecord({ id: m.id, sessionFile: childFile, model: m.model, status: 'idle', result: m.resultText }),
223
223
  )
224
224
  }
225
225
 
226
- /** 手写短轮询(真实 timers,vitest 4.1.8 vi.waitFor 对 falsy callback 不轮询——
227
- * subagent-core 同款纪律):manifest 为 fire-and-forget 异步写,需轮询等落盘。 */
228
- async function until(cond: () => boolean, timeoutMs = 3000): Promise<void> {
229
- const start = Date.now()
230
- while (!cond()) {
231
- if (Date.now() - start > timeoutMs) {
232
- throw new Error(`cross-pkg: condition not met within ${timeoutMs}ms`)
233
- }
234
- await new Promise((resolve) => setTimeout(resolve, 10))
235
- }
236
- }
237
-
238
- /** 全链路驱动:种子 → 恢复 service(orphan 覆写)→ E1(落标出口真写 manifest)。
239
- * 返回子 session 文件路径表。 */
240
- async function driveSyncBatchRecovery(members: MemberSeed[]): Promise<Map<string, string>> {
226
+ /** 全链路驱动:种子 → 恢复 service(orphan 覆写)→ rebuildIndexes(现行 boot
227
+ * 全量重建通道——derivedManifestRecord 投影真写 manifest)。返回子 session 文件
228
+ * 路径表。[collect 退役] 原 E1 落标出口(recoverSyncCollectBatch →
229
+ * appendBatchFinalizedEntry)随 sync 批机制删除。 */
230
+ async function driveManifestRebuild(members: MemberSeed[]): Promise<Map<string, string>> {
241
231
  const childFiles = new Map<string, string>()
242
232
  for (const m of members) {
243
233
  const childFile = writeChildSessionFile(m)
@@ -245,12 +235,15 @@ describe('跨包集成:subagent-core E1 落标 manifest → session-reader res
245
235
  seedRoundTerminalEntries(m, childFile)
246
236
  }
247
237
  const service = makeRecoveryService()
248
- service.recoverSyncCollectBatch()
249
- // E1 落标出口 appendBatchFinalizedEntry 的 manifest 是 fire-and-forget best-effort
250
- // 写——轮询等全部成员 manifest 真实落盘(W3 投影代码路径的产物)。
251
- await until(() =>
252
- members.every((m) => fs.existsSync(path.join(getSubagentRecordsDir(agentDir, agentDir), `${m.id}.json`))),
253
- )
238
+ service.rebuildIndexes()
239
+ // derivedManifestRecord 投影为同步落盘——全成员 manifest 即刻可见(W3 投影
240
+ // 代码路径的产物,非手工预写)。
241
+ for (const m of members) {
242
+ expect(
243
+ fs.existsSync(path.join(getSubagentRecordsDir(agentDir, agentDir), `${m.id}.json`)),
244
+ `manifest of ${m.id}`,
245
+ ).toBe(true)
246
+ }
254
247
  return childFiles
255
248
  }
256
249
 
@@ -262,12 +255,12 @@ describe('跨包集成:subagent-core E1 落标 manifest → session-reader res
262
255
  assistantParts: ['part one', 'part two'],
263
256
  model: 'prov/round-m',
264
257
  }
265
- const childFiles = await driveSyncBatchRecovery([m])
258
+ const childFiles = await driveManifestRebuild([m])
266
259
 
267
- // manifest 内容断言(W3 appendBatchFinalizedEntry 投影路径的产物,非手工预写):
268
- // sessionFile 来自 W1 投影扩展。[U2/U3 两态桥接] E1 落标成员经恢复链覆写后处于
269
- // idle(重建单规则)/running 形态、无旧终态遗留位 → legacy status 如实投影
270
- // "running"(batchManifestRecord 单点派生,session-reader 视角的活跃成员)。
260
+ // manifest 内容断言(rebuildIndexes → derivedManifestRecord 投影路径的产物,
261
+ // 非手工预写):sessionFile 来自 W1 投影扩展。[U2/U3 两态桥接] 恢复链覆写后成员
262
+ // 处于 idle(重建单规则)、无旧终态遗留位 → legacy status 如实投影 "running"
263
+ //(legacyManifestStatusFields 单点派生,session-reader 视角的活跃成员)。
271
264
  const manifest = JSON.parse(
272
265
  fs.readFileSync(path.join(getSubagentRecordsDir(agentDir, agentDir), 'sa-cross-1.json'), 'utf-8'),
273
266
  ) as Record<string, unknown>
@@ -280,7 +273,7 @@ describe('跨包集成:subagent-core E1 落标 manifest → session-reader res
280
273
  })
281
274
 
282
275
  // session-reader 侧:sa- id 反查(v1 此处报「无匹配 record」——manifest 惰性不落盘)
283
- const r = await handleSessionRead({ action: 'result', session: 'sa-cross-1' }, agentDir)
276
+ const r = await handleSessionRead({ action: 'result', session: 'sa-cross-1' }, { agentDir })
284
277
  // 同源语义:跨 assistant message "\n\n" join、thinking 块排除——与 record.result 逐字节一致
285
278
  expect(r.content[0]?.text).toBe(resultText)
286
279
  const d = r.details as {
@@ -302,9 +295,9 @@ describe('跨包集成:subagent-core E1 落标 manifest → session-reader res
302
295
  { id: 'sa-cross-a', resultText: 'alpha final answer', assistantParts: ['alpha final answer'], model: 'prov/ma' },
303
296
  { id: 'sa-cross-b', resultText: 'beta final answer', assistantParts: ['beta final answer'], model: 'prov/mb' },
304
297
  ]
305
- await driveSyncBatchRecovery(members)
298
+ await driveManifestRebuild(members)
306
299
 
307
- const r = await handleSessionRead({ action: 'result', session: 'sa-cross-a,sa-cross-b' }, agentDir)
300
+ const r = await handleSessionRead({ action: 'result', session: 'sa-cross-a,sa-cross-b' }, { agentDir })
308
301
  const text = r.content[0]?.text ?? ''
309
302
  expect(text).toContain('[1/2] sa-cross-a')
310
303
  expect(text).toContain('[2/2] sa-cross-b')
@@ -1,9 +1,9 @@
1
1
  import { describe, it, expect } from 'vitest'
2
2
  import {
3
3
  detectEnvironment,
4
- XYZ_AGENT_EXT_LOG_ENV,
4
+ TAIJI_AGENT_EXT_LOG_ENV,
5
5
  PI_CODING_AGENT_DIR_ENV,
6
- XYZ_AGENT_DATA_DIR_ENV,
6
+ TAIJI_AGENT_DATA_DIR_ENV,
7
7
  type EnvironmentSignals,
8
8
  } from '../discovery/env.js'
9
9
 
@@ -22,54 +22,54 @@ function signals(overrides: Partial<EnvironmentSignals> = {}): EnvironmentSignal
22
22
  }
23
23
 
24
24
  describe('detectEnvironment 托管合取判定', () => {
25
- it('双信号齐备 + B 后新布局 <dataDir>/agent → xyz-agent(packaged)', () => {
25
+ it('双信号齐备 + B 后新布局 <dataDir>/agent → taiji(packaged)', () => {
26
26
  const r = detectEnvironment(
27
27
  signals({
28
28
  env: {
29
- [XYZ_AGENT_EXT_LOG_ENV]: '1',
30
- [PI_CODING_AGENT_DIR_ENV]: '/Users/u/.xyz-agent/agent',
29
+ [TAIJI_AGENT_EXT_LOG_ENV]: '1',
30
+ [PI_CODING_AGENT_DIR_ENV]: '/Users/u/.taiji/agent',
31
31
  },
32
32
  bundleUrl:
33
33
  'file:///Applications/TaiJi.app/Contents/Resources/extensions/@zhushanwen/pi-session-reader/index.mjs',
34
34
  }),
35
35
  )
36
- expect(r.kind).toBe('xyz-agent')
36
+ expect(r.kind).toBe('taiji')
37
37
  expect(r.distribution).toBe('packaged')
38
- expect(r.dataDir).toBe('/Users/u/.xyz-agent')
38
+ expect(r.dataDir).toBe('/Users/u/.taiji')
39
39
  })
40
40
 
41
- it('双信号齐备 + B 前旧布局 <dataDir>/pi/agent → xyz-agent(迁移窗口期双形态兼容)', () => {
41
+ it('双信号齐备 + B 前旧布局 <dataDir>/pi/agent → taiji(迁移窗口期双形态兼容)', () => {
42
42
  const r = detectEnvironment(
43
43
  signals({
44
44
  env: {
45
- [XYZ_AGENT_EXT_LOG_ENV]: '1',
46
- [PI_CODING_AGENT_DIR_ENV]: '/Users/u/.xyz-agent-dev/pi/agent',
45
+ [TAIJI_AGENT_EXT_LOG_ENV]: '1',
46
+ [PI_CODING_AGENT_DIR_ENV]: '/Users/u/.taiji-dev/pi/agent',
47
47
  },
48
- bundleUrl: 'file:///Users/u/Code/xyz-agent/extensions/universal/session-reader/src/index.ts',
48
+ bundleUrl: 'file:///Users/u/Code/taiji/extensions/universal/session-reader/src/index.ts',
49
49
  }),
50
50
  )
51
- expect(r.kind).toBe('xyz-agent')
51
+ expect(r.kind).toBe('taiji')
52
52
  expect(r.distribution).toBe('dev')
53
- expect(r.dataDir).toBe('/Users/u/.xyz-agent-dev')
53
+ expect(r.dataDir).toBe('/Users/u/.taiji-dev')
54
54
  })
55
55
 
56
- it('透传污染反例 1:仅 XYZ_AGENT_EXT_LOG=1(PI_CODING_AGENT_DIR 未设)→ standalone-pi,evidence 点名孤置信号', () => {
56
+ it('透传污染反例 1:仅 TAIJI_AGENT_EXT_LOG=1(PI_CODING_AGENT_DIR 未设)→ standalone-pi,evidence 点名孤置信号', () => {
57
57
  const r = detectEnvironment(
58
- signals({ env: { [XYZ_AGENT_EXT_LOG_ENV]: '1' } }),
58
+ signals({ env: { [TAIJI_AGENT_EXT_LOG_ENV]: '1' } }),
59
59
  )
60
60
  expect(r.kind).toBe('standalone-pi')
61
61
  expect(r.dataDir).toBeUndefined()
62
62
  const joined = r.evidence.join('\n')
63
- expect(joined).toContain(`${XYZ_AGENT_EXT_LOG_ENV}='1' → 命中`)
63
+ expect(joined).toContain(`${TAIJI_AGENT_EXT_LOG_ENV}='1' → 命中`)
64
64
  expect(joined).toContain('单独成立不合取')
65
- expect(joined).toContain(XYZ_AGENT_EXT_LOG_ENV)
65
+ expect(joined).toContain(TAIJI_AGENT_EXT_LOG_ENV)
66
66
  })
67
67
 
68
- it('透传污染反例 2:仅 XYZ_AGENT_EXT_LOG=1 且 PI_CODING_AGENT_DIR 为裸 pi 形态 → standalone-pi', () => {
68
+ it('透传污染反例 2:仅 TAIJI_AGENT_EXT_LOG=1 且 PI_CODING_AGENT_DIR 为裸 pi 形态 → standalone-pi', () => {
69
69
  const r = detectEnvironment(
70
70
  signals({
71
71
  env: {
72
- [XYZ_AGENT_EXT_LOG_ENV]: '1',
72
+ [TAIJI_AGENT_EXT_LOG_ENV]: '1',
73
73
  [PI_CODING_AGENT_DIR_ENV]: '/home/u/.pi/agent',
74
74
  },
75
75
  }),
@@ -81,7 +81,7 @@ describe('detectEnvironment 托管合取判定', () => {
81
81
  it('透传污染反例 3:仅 PI_CODING_AGENT_DIR 形态匹配(EXT_LOG 未设)→ standalone-pi,evidence 点名孤置信号', () => {
82
82
  const r = detectEnvironment(
83
83
  signals({
84
- env: { [PI_CODING_AGENT_DIR_ENV]: '/Users/u/.xyz-agent/agent' },
84
+ env: { [PI_CODING_AGENT_DIR_ENV]: '/Users/u/.taiji/agent' },
85
85
  }),
86
86
  )
87
87
  expect(r.kind).toBe('standalone-pi')
@@ -92,13 +92,13 @@ describe('detectEnvironment 托管合取判定', () => {
92
92
  expect(joined).toContain(PI_CODING_AGENT_DIR_ENV)
93
93
  })
94
94
 
95
- it('XYZ_AGENT_EXT_LOG 非 1 值(如 0 / true)不构成命中', () => {
95
+ it('TAIJI_AGENT_EXT_LOG 非 1 值(如 0 / true)不构成命中', () => {
96
96
  for (const value of ['0', 'true', '']) {
97
97
  const r = detectEnvironment(
98
98
  signals({
99
99
  env: {
100
- [XYZ_AGENT_EXT_LOG_ENV]: value,
101
- [PI_CODING_AGENT_DIR_ENV]: '/Users/u/.xyz-agent/agent',
100
+ [TAIJI_AGENT_EXT_LOG_ENV]: value,
101
+ [PI_CODING_AGENT_DIR_ENV]: '/Users/u/.taiji/agent',
102
102
  },
103
103
  }),
104
104
  )
@@ -106,12 +106,12 @@ describe('detectEnvironment 托管合取判定', () => {
106
106
  }
107
107
  })
108
108
 
109
- it('形态防误报:无点前缀 xyz-agent、嵌套浅段不匹配', () => {
110
- for (const dir of ['/home/u/xyz-agent/agent', '/home/u/.xyz/agent', '/.pi/agent']) {
109
+ it('形态防误报:无点前缀 taiji、嵌套浅段不匹配', () => {
110
+ for (const dir of ['/home/u/taiji/agent', '/home/u/.foo/agent', '/.pi/agent']) {
111
111
  const r = detectEnvironment(
112
112
  signals({
113
113
  env: {
114
- [XYZ_AGENT_EXT_LOG_ENV]: '1',
114
+ [TAIJI_AGENT_EXT_LOG_ENV]: '1',
115
115
  [PI_CODING_AGENT_DIR_ENV]: dir,
116
116
  },
117
117
  }),
@@ -144,8 +144,8 @@ describe('detectEnvironment distribution(bundle 路径三态)', () => {
144
144
 
145
145
  it('仓库路径(源码与 dev staged 资源根两种)→ dev,staged 根不被误判 packaged', () => {
146
146
  for (const url of [
147
- 'file:///Users/u/Code/xyz-agent/extensions/universal/session-reader/src/index.ts',
148
- 'file:///Users/u/Code/xyz-agent/apps/electron/resources/extensions/@zhushanwen/pi-session-reader/index.mjs',
147
+ 'file:///Users/u/Code/taiji/extensions/universal/session-reader/src/index.ts',
148
+ 'file:///Users/u/Code/taiji/apps/electron/resources/extensions/@zhushanwen/pi-session-reader/index.mjs',
149
149
  ]) {
150
150
  const r = detectEnvironment(signals({ bundleUrl: url }))
151
151
  expect(r.distribution, `${url} 应判 dev`).toBe('dev')
@@ -160,48 +160,48 @@ describe('detectEnvironment distribution(bundle 路径三态)', () => {
160
160
  })
161
161
 
162
162
  describe('detectEnvironment dataDir 推导', () => {
163
- it('托管:XYZ_AGENT_DATA_DIR 优先于 PI_CODING_AGENT_DIR 剥层', () => {
163
+ it('托管:TAIJI_AGENT_DATA_DIR 优先于 PI_CODING_AGENT_DIR 剥层', () => {
164
164
  const r = detectEnvironment(
165
165
  signals({
166
166
  env: {
167
- [XYZ_AGENT_EXT_LOG_ENV]: '1',
168
- [PI_CODING_AGENT_DIR_ENV]: '/data/.xyz-agent/pi/agent',
169
- [XYZ_AGENT_DATA_DIR_ENV]: '/data/.xyz-agent',
167
+ [TAIJI_AGENT_EXT_LOG_ENV]: '1',
168
+ [PI_CODING_AGENT_DIR_ENV]: '/data/.taiji/pi/agent',
169
+ [TAIJI_AGENT_DATA_DIR_ENV]: '/data/.taiji',
170
170
  },
171
171
  }),
172
172
  )
173
- expect(r.kind).toBe('xyz-agent')
174
- expect(r.dataDir).toBe('/data/.xyz-agent')
175
- expect(r.evidence.join('\n')).toContain(`来源:${XYZ_AGENT_DATA_DIR_ENV}`)
173
+ expect(r.kind).toBe('taiji')
174
+ expect(r.dataDir).toBe('/data/.taiji')
175
+ expect(r.evidence.join('\n')).toContain(`来源:${TAIJI_AGENT_DATA_DIR_ENV}`)
176
176
  })
177
177
 
178
178
  it('托管:无 DATA_DIR 时按布局剥层——新布局剥一层、旧布局剥两层', () => {
179
179
  const newLayout = detectEnvironment(
180
180
  signals({
181
181
  env: {
182
- [XYZ_AGENT_EXT_LOG_ENV]: '1',
183
- [PI_CODING_AGENT_DIR_ENV]: '/data/.xyz-agent/agent',
182
+ [TAIJI_AGENT_EXT_LOG_ENV]: '1',
183
+ [PI_CODING_AGENT_DIR_ENV]: '/data/.taiji/agent',
184
184
  },
185
185
  }),
186
186
  )
187
- expect(newLayout.dataDir).toBe('/data/.xyz-agent')
187
+ expect(newLayout.dataDir).toBe('/data/.taiji')
188
188
 
189
189
  const oldLayout = detectEnvironment(
190
190
  signals({
191
191
  env: {
192
- [XYZ_AGENT_EXT_LOG_ENV]: '1',
193
- [PI_CODING_AGENT_DIR_ENV]: '/data/.xyz-agent/pi/agent',
192
+ [TAIJI_AGENT_EXT_LOG_ENV]: '1',
193
+ [PI_CODING_AGENT_DIR_ENV]: '/data/.taiji/pi/agent',
194
194
  },
195
195
  }),
196
196
  )
197
- expect(oldLayout.dataDir).toBe('/data/.xyz-agent')
197
+ expect(oldLayout.dataDir).toBe('/data/.taiji')
198
198
  })
199
199
 
200
- it('standalone:即使 XYZ_AGENT_DATA_DIR 被设置也恒 undefined', () => {
200
+ it('standalone:即使 TAIJI_AGENT_DATA_DIR 被设置也恒 undefined', () => {
201
201
  const r = detectEnvironment(
202
202
  signals({
203
203
  env: {
204
- [XYZ_AGENT_DATA_DIR_ENV]: '/data/.xyz-agent',
204
+ [TAIJI_AGENT_DATA_DIR_ENV]: '/data/.taiji',
205
205
  },
206
206
  }),
207
207
  )
@@ -215,15 +215,15 @@ describe('detectEnvironment evidence', () => {
215
215
  ['全信号缺失', signals({})],
216
216
  ['托管全信号', signals({
217
217
  env: {
218
- [XYZ_AGENT_EXT_LOG_ENV]: '1',
219
- [PI_CODING_AGENT_DIR_ENV]: '/data/.xyz-agent/agent',
220
- [XYZ_AGENT_DATA_DIR_ENV]: '/data/.xyz-agent',
218
+ [TAIJI_AGENT_EXT_LOG_ENV]: '1',
219
+ [PI_CODING_AGENT_DIR_ENV]: '/data/.taiji/agent',
220
+ [TAIJI_AGENT_DATA_DIR_ENV]: '/data/.taiji',
221
221
  },
222
222
  bundleUrl:
223
223
  'file:///Applications/TaiJi.app/Contents/Resources/extensions/@zhushanwen/pi-session-reader/index.mjs',
224
224
  })],
225
- ['EXT_LOG 孤置', signals({ env: { [XYZ_AGENT_EXT_LOG_ENV]: '1' } })],
226
- ['agentDir 孤置', signals({ env: { [PI_CODING_AGENT_DIR_ENV]: '/data/.xyz-agent/agent' } })],
225
+ ['EXT_LOG 孤置', signals({ env: { [TAIJI_AGENT_EXT_LOG_ENV]: '1' } })],
226
+ ['agentDir 孤置', signals({ env: { [PI_CODING_AGENT_DIR_ENV]: '/data/.taiji/agent' } })],
227
227
  ['bundleUrl 缺失', signals({})],
228
228
  ]
229
229
 
@@ -238,20 +238,20 @@ describe('detectEnvironment evidence', () => {
238
238
  const r = detectEnvironment(
239
239
  signals({
240
240
  env: {
241
- [XYZ_AGENT_EXT_LOG_ENV]: '0',
242
- [PI_CODING_AGENT_DIR_ENV]: '/data/.xyz-agent/pi/agent',
241
+ [TAIJI_AGENT_EXT_LOG_ENV]: '0',
242
+ [PI_CODING_AGENT_DIR_ENV]: '/data/.taiji/pi/agent',
243
243
  },
244
- agentDir: '/data/.xyz-agent/pi/agent',
244
+ agentDir: '/data/.taiji/pi/agent',
245
245
  }),
246
246
  )
247
247
  const joined = r.evidence.join('\n')
248
248
  // 未命中信号:原文 + 未命中注记
249
- expect(joined).toContain(`${XYZ_AGENT_EXT_LOG_ENV}='0' → 未命中`)
249
+ expect(joined).toContain(`${TAIJI_AGENT_EXT_LOG_ENV}='0' → 未命中`)
250
250
  // 命中信号:原文 + 匹配注记
251
- expect(joined).toContain(`${PI_CODING_AGENT_DIR_ENV}='/data/.xyz-agent/pi/agent'`)
251
+ expect(joined).toContain(`${PI_CODING_AGENT_DIR_ENV}='/data/.taiji/pi/agent'`)
252
252
  expect(joined).toContain('形态匹配')
253
253
  // 第三条输入:agentDir 透传记录
254
- expect(joined).toContain(`agentDir='/data/.xyz-agent/pi/agent'`)
254
+ expect(joined).toContain(`agentDir='/data/.taiji/pi/agent'`)
255
255
  // 判定结论行
256
256
  expect(joined).toContain('standalone-pi')
257
257
  })
@@ -107,13 +107,6 @@ describe('buildFamilyIndex', () => {
107
107
  expect(index.childrenOf.get('aaa')?.[0].sessionId).toBe('bbb')
108
108
  })
109
109
 
110
- it('parentSession 直接是 sessionId 也兼容(简化 fixture)', () => {
111
- const headers = [header('aaa'), header('bbb', { parentSession: 'aaa' })]
112
- const index = buildFamilyIndex(headers, [], new Map())
113
-
114
- expect(index.childrenOf.get('aaa')?.[0].sessionId).toBe('bbb')
115
- })
116
-
117
110
  it('parentSession 反查不到父 → 该 entry 不进 childrenOf(不报错)', () => {
118
111
  const headers = [header('bbb', { parentSession: '/missing.jsonl' })]
119
112
  const index = buildFamilyIndex(headers, [], new Map())
@@ -133,7 +133,7 @@ async function makeSubagentSession(
133
133
  describe('findSessions', () => {
134
134
  let agentDir: string
135
135
  let slugDir: string
136
- /** subagent fixture 目录(模拟 subagents/<cwd编码>/sessions/ 结构,roots.listSubagentSessions 扫描路径) */
136
+ /** subagent fixture 目录(模拟 subagents/<cwd编码>/sessions/ 结构,resolveSessionRoots [subagent] 根扫描路径) */
137
137
  let saDir: string
138
138
 
139
139
  let recordsDir: string
@@ -578,7 +578,7 @@ describe('findSessions', () => {
578
578
  describe('u11 标题元数据(metadataProvider 注入,§6.6)', () => {
579
579
  let root: string
580
580
  let agentDir: string
581
- /** 平铺 main 根(xyz-agent 形态:session 直在 sessions/ 下,[default] 根无子目录) */
581
+ /** 平铺 main 根(taiji 形态:session 直在 sessions/ 下,[default] 根无子目录) */
582
582
  let flatMain: string
583
583
  /** legacy 根(dirname(agentDir)/sessions) */
584
584
  let legacyDir: string
@@ -705,8 +705,8 @@ describe('u11 标题元数据(metadataProvider 注入,§6.6)', () => {
705
705
  await mkdir(legacyDir, { recursive: true })
706
706
  const provider = vi.fn(async (dir: string) =>
707
707
  dir === flatMain
708
- ? [{ path: pathA, id: 'dup-id-0001', cwd: '/demo', name: '旧标题', modified: 1000 }]
709
- : [{ path: pathA, id: 'dup-id-0001', cwd: '/demo', name: '新标题', modified: 2000 }],
708
+ ? [{ path: pathA, id: 'dup-id-0001', cwd: '/demo', name: '旧标题', modified: new Date(1000) }]
709
+ : [{ path: pathA, id: 'dup-id-0001', cwd: '/demo', name: '新标题', modified: new Date(2000) }],
710
710
  )
711
711
  const { matches } = await findSessions('新标题', agentDir, { metadataProvider: provider })
712
712
  expect(provider).toHaveBeenCalledTimes(2) // 两个平铺存在的根各一次
@@ -753,8 +753,8 @@ describe('u11 标题元数据(metadataProvider 注入,§6.6)', () => {
753
753
  const provider = vi.fn(async (dir: string) =>
754
754
  dir === flatMain
755
755
  ? [
756
- { path: pNew1, id: 'recent-new-1', cwd: '/demo', name: '最新标题一', modified: (base + 300) * 1000 },
757
- { path: pNew2, id: 'recent-new-2', cwd: '/demo', name: '最新标题二', modified: (base + 200) * 1000 },
756
+ { path: pNew1, id: 'recent-new-1', cwd: '/demo', name: '最新标题一', modified: new Date((base + 300) * 1000) },
757
+ { path: pNew2, id: 'recent-new-2', cwd: '/demo', name: '最新标题二', modified: new Date((base + 200) * 1000) },
758
758
  ]
759
759
  : [],
760
760
  )
@@ -169,7 +169,7 @@ describe('sessionReaderExtension - execute 信号包采集(U3:ctx 全形态
169
169
  })
170
170
 
171
171
  it('u11:execute 构造 metadataProvider 接 SessionManager.listAll——keyword 标题命中端到端', async () => {
172
- // fixture:平铺 main 根(xyz-agent 形态),session 首消息不含 query,标题经 mock listAll 注入
172
+ // fixture:平铺 main 根(taiji 形态),session 首消息不含 query,标题经 mock listAll 注入
173
173
  const tmp = await mkdtemp(join(tmpdir(), 'index-u11-'))
174
174
  try {
175
175
  const agentDir = join(tmp, 'agent')
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { execSync } from 'node:child_process'
3
+ import { homedir } from 'node:os'
3
4
  import { join } from 'node:path'
4
5
 
5
6
  /**
@@ -13,8 +14,8 @@ import { join } from 'node:path'
13
14
  * 本文件无 .test.ts 后缀,vitest include 只收集 `src/__tests__/` 下以 .test.ts 结尾的文件,不收集本文件。
14
15
  */
15
16
 
16
- /** 真实 pi agent 目录(本机),用于集成测试。 */
17
- export const REAL_AGENT_DIR = '/Users/zhushanwen/.pi/agent'
17
+ /** 真实 pi agent 目录(pi 官方家目录布局,随宿主 homedir 推导),用于集成测试。 */
18
+ export const REAL_AGENT_DIR = join(homedir(), '.pi', 'agent')
18
19
 
19
20
  /**
20
21
  * 全量扫描类真实数据用例的统一超时标定(90s)。
@@ -22,7 +23,7 @@ export const REAL_AGENT_DIR = '/Users/zhushanwen/.pi/agent'
22
23
  * 这类用例遍历本机 ~/.pi/agent 全量会话语料(实测 4600+ 文件),空载单用例已需 6-10s;
23
24
  * 38 包并行全量测试时 CPU/IO 争抢可降速 3-4 倍,30s 标定必假红。90s ≈ 空载 9 倍余量,
24
25
  * 仍能兜住真正的挂死。仅用于遍历全量语料的用例(findSessions / buildFamilyFromFs /
25
- * listMainSessions 等);读已知单文件的用例不需要放宽。
26
+ * resolveSessionRoots 等);读已知单文件的用例不需要放宽。
26
27
  */
27
28
  export const REAL_DATA_TIMEOUT_MS = 90_000
28
29
 
@@ -31,13 +32,25 @@ export const E6 = '019e6c96-0a0c-74b8-a73f-d1854d88e2a7'
31
32
  /** 真实 fork 家族根(fork 子代 019fe632,隔代 subagent 019fe635 挂在 019fe632 下)。 */
32
33
  export const FAM = '019fe620-8ae1-78a7-b76a-43a1ba4cc3c7'
33
34
 
34
- /** E6 的完整文件路径(parser/turns/render 直接 parseSessionFile 用)。 */
35
- export const REAL_SESSION = join(
36
- REAL_AGENT_DIR,
37
- 'sessions',
38
- '--Users-zhushanwen-Code-xyz-agent-workspace-feat-plugin-arch-3--',
39
- `2026-05-28T03-17-12-844Z_${E6}.jsonl`,
40
- )
35
+ /**
36
+ * E6 的完整文件路径(parser/turns/render 直接 parseSessionFile 用)。
37
+ * 按 sid 运行时定位而非硬编码宿主 workspace 目录名:真实文件挂在哪个 slug 目录随采集机器而异,
38
+ * 硬编码会绑死本机路径形态;定位不到时回落相对布局,由下方 HAS_REAL_SESSION 守卫 skip。
39
+ */
40
+ export const REAL_SESSION = (() => {
41
+ const fallback = join(REAL_AGENT_DIR, 'sessions', `2026-05-28T03-17-12-844Z_${E6}.jsonl`)
42
+ if (!existsSync(REAL_AGENT_DIR)) return fallback
43
+ try {
44
+ return (
45
+ execSync(
46
+ `find ${REAL_AGENT_DIR}/sessions -name '*${E6}*' -name '*.jsonl' ! -name '*.finalized' 2>/dev/null | head -1`,
47
+ { encoding: 'utf8' },
48
+ ).trim() || fallback
49
+ )
50
+ } catch {
51
+ return fallback
52
+ }
53
+ })()
41
54
 
42
55
  /** 同步探测真实 session 文件是否存在(不存在则 skip,避免在无该数据的机器上硬失败)。 */
43
56
  export function hasRealSession(sid: string): boolean {
@@ -90,6 +90,8 @@ describe('renderOutline', () => {
90
90
  // thinking 'secret'(6B) + toolResult 'output'(6B)
91
91
  expect(b.omittedBytes).toBe(12)
92
92
  expect(result.truncated).toBeUndefined()
93
+ // E7 行渲染统一:lines 随结果返回,与 turns 一一对应(预算度量 = 展示行同一份)
94
+ expect(result.lines).toEqual(['T000 · do something · bash×2,read×2 · → sure · [12B omitted]'])
93
95
  })
94
96
 
95
97
  it('2a. 降级:单行 toolSummary 过长超 perTurnBudget → 砍 toolSummary,保 userBrief 骨架', () => {
@@ -108,6 +110,25 @@ describe('renderOutline', () => {
108
110
  expect(b.toolSummary).toBe('') // 被砍(降级)
109
111
  expect(b.userBrief).toBe('u'.repeat(10)) // 骨架保留
110
112
  }
113
+ // E7:降级后的渲染行(骨架形态)与 briefs 字段同步——行内无 → 结论段、无工具聚合段
114
+ expect(result.lines).toHaveLength(3)
115
+ expect(result.lines).toEqual(['T000 · uuuuuuuuuu', 'T001 · uuuuuuuuuu', 'T002 · uuuuuuuuuu'])
116
+ expect(result.truncated).toBeUndefined()
117
+ })
118
+
119
+ it('2b. 降级(砍 assistantBrief 档):行内 → 结论段消失、toolSummary 保留,与改前语义一致', () => {
120
+ // budget=20 → perTurnCharBudget=80;level 0 行 = T000 · u*10 · bash×2 · → a*80… ≈112 > 80
121
+ // → 砍 assistantBrief 后 ≈26 ≤ 80 停(S3:该档形态与改前 handler 侧渲染降级后 briefs 等价)
122
+ const t = turn(
123
+ 0,
124
+ [uEntry('U', 'u'.repeat(10)), aEntry('A', 'a'.repeat(100), [{ name: 'bash' }, { name: 'bash' }])],
125
+ { userEntry: uEntry('U', 'u'.repeat(10)) },
126
+ )
127
+ const result = renderOutline([t], emptyTree(), { budget: 20 })
128
+
129
+ expect(result.turns[0].assistantBrief).toBe('') // 被砍
130
+ expect(result.turns[0].toolSummary).toBe('bash×2') // 保留
131
+ expect(result.lines).toEqual(['T000 · uuuuuuuuuu · bash×2'])
111
132
  expect(result.truncated).toBeUndefined()
112
133
  })
113
134
 
@@ -122,6 +143,8 @@ describe('renderOutline', () => {
122
143
 
123
144
  expect(result.truncated).toBe(3)
124
145
  expect(result.turns).toHaveLength(2)
146
+ // E7:lines 与 turns 同步截短(truncateToTotalBudget 双数组一致)
147
+ expect(result.lines).toHaveLength(2)
125
148
  })
126
149
 
127
150
  it('4. toolSummary 聚合:同名计数 ×N,不同名逗号分隔', () => {
@@ -168,6 +191,19 @@ describe('renderOutline', () => {
168
191
  expect(withoutBranches.turns[0].branch).toBeUndefined()
169
192
  })
170
193
 
194
+ it('7b. E7 旁支标记统一:渲染行 `[旁支 N entries]`(N = 该 forkPoint 下旁支子树 entry 数)', () => {
195
+ // branches: U → 3 entries 旁支子树(E7/D3:outline 与 export 两条路径共用此形态)
196
+ const tree: TreeView = { leafPath: ['U'], branches: new Map([['U', 3]]), orphans: [] }
197
+ const userEntry = uEntry('U', 'hi')
198
+ const turns = [turn(0, [userEntry], { userEntry })]
199
+
200
+ const result = renderOutline(turns, tree, { allBranches: true })
201
+ expect(result.lines).toEqual(['T000 · hi · [旁支 3 entries]'])
202
+ // branchSize 缺失(allBranches:false)不标旁支
203
+ const noBranch = renderOutline(turns, tree, { allBranches: false })
204
+ expect(noBranch.lines).toEqual(['T000 · hi'])
205
+ })
206
+
171
207
  it.skipIf(!HAS_REAL_SESSION)('8. 真实 019e6c96:outline tokenEstimate <= 1500 + assistantBrief/toolSummary 非空(v2 O1)', async () => {
172
208
  // v2 O1:加 assistantBrief + 修 toolSummary bug 后 outline 变长,阈值 600→1500(design §3.3 D4)
173
209
  const parsed = await parseSessionFile(REAL_SESSION)
@@ -310,7 +346,6 @@ describe('renderDetail', () => {
310
346
  expect(summary.summary).toMatch(/^read: f\.ts \(\d+KB\)$/)
311
347
  expect(summary.totalLines).toBe(4)
312
348
  expect(summary.headLines).toContain('line1')
313
- expect(summary.fullEntry.message?.role).toBe('toolResult')
314
349
 
315
350
  // includeToolResult 全文:返回原 Entry
316
351
  const full = renderDetail([t], { includeToolResult: true })