@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 +6 -3
- package/src/__tests__/cross-package-subagent-core.test.ts +42 -49
- package/src/__tests__/env.test.ts +54 -54
- package/src/__tests__/family.test.ts +0 -7
- package/src/__tests__/find.test.ts +6 -6
- package/src/__tests__/index.test.ts +1 -1
- package/src/__tests__/real-data.ts +23 -10
- package/src/__tests__/render.test.ts +36 -1
- package/src/__tests__/result.test.ts +20 -23
- package/src/__tests__/roots.test.ts +47 -81
- package/src/__tests__/session-command.test.ts +23 -0
- package/src/__tests__/subagents.test.ts +5 -7
- package/src/__tests__/tool-handler.test.ts +481 -216
- package/src/__tests__/workflow.test.ts +1 -7
- package/src/core/execution-tree.ts +1 -0
- package/src/core/family.ts +12 -9
- package/src/core/render.ts +41 -14
- package/src/core/toolcall.ts +1 -2
- package/src/core/units.ts +14 -0
- package/src/core/workflow.ts +7 -20
- package/src/discovery/env.ts +41 -41
- package/src/discovery/find.ts +43 -135
- package/src/discovery/roots.ts +18 -89
- package/src/discovery/session-header.ts +103 -0
- package/src/discovery/subagents.ts +23 -94
- package/src/doctor.ts +12 -143
- package/src/extract.ts +6 -19
- package/src/handler-utils.ts +28 -5
- package/src/index.ts +2 -2
- package/src/no-match.ts +3 -2
- package/src/result-action.ts +29 -34
- package/src/search-across.ts +6 -8
- package/src/tool-handler.ts +151 -176
- package/src/tui/hash-provider.ts +7 -3
- package/src/tui/session-command.ts +22 -10
- package/vitest.config.ts +2 -4
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-session-reader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
-
"
|
|
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.
|
|
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 跨包集成测试(
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
-
*
|
|
36
|
-
*
|
|
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
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
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
|
|
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)→ 轮终(
|
|
217
|
-
*
|
|
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,
|
|
222
|
+
memberRecord({ id: m.id, sessionFile: childFile, model: m.model, status: 'idle', result: m.resultText }),
|
|
223
223
|
)
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
/**
|
|
227
|
-
*
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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.
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
258
|
+
const childFiles = await driveManifestRebuild([m])
|
|
266
259
|
|
|
267
|
-
// manifest 内容断言(
|
|
268
|
-
// sessionFile 来自 W1 投影扩展。[U2/U3 两态桥接]
|
|
269
|
-
// idle
|
|
270
|
-
|
|
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
|
|
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
|
-
|
|
4
|
+
TAIJI_AGENT_EXT_LOG_ENV,
|
|
5
5
|
PI_CODING_AGENT_DIR_ENV,
|
|
6
|
-
|
|
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 →
|
|
25
|
+
it('双信号齐备 + B 后新布局 <dataDir>/agent → taiji(packaged)', () => {
|
|
26
26
|
const r = detectEnvironment(
|
|
27
27
|
signals({
|
|
28
28
|
env: {
|
|
29
|
-
[
|
|
30
|
-
[PI_CODING_AGENT_DIR_ENV]: '/Users/u/.
|
|
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('
|
|
36
|
+
expect(r.kind).toBe('taiji')
|
|
37
37
|
expect(r.distribution).toBe('packaged')
|
|
38
|
-
expect(r.dataDir).toBe('/Users/u/.
|
|
38
|
+
expect(r.dataDir).toBe('/Users/u/.taiji')
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
it('双信号齐备 + B 前旧布局 <dataDir>/pi/agent →
|
|
41
|
+
it('双信号齐备 + B 前旧布局 <dataDir>/pi/agent → taiji(迁移窗口期双形态兼容)', () => {
|
|
42
42
|
const r = detectEnvironment(
|
|
43
43
|
signals({
|
|
44
44
|
env: {
|
|
45
|
-
[
|
|
46
|
-
[PI_CODING_AGENT_DIR_ENV]: '/Users/u/.
|
|
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/
|
|
48
|
+
bundleUrl: 'file:///Users/u/Code/taiji/extensions/universal/session-reader/src/index.ts',
|
|
49
49
|
}),
|
|
50
50
|
)
|
|
51
|
-
expect(r.kind).toBe('
|
|
51
|
+
expect(r.kind).toBe('taiji')
|
|
52
52
|
expect(r.distribution).toBe('dev')
|
|
53
|
-
expect(r.dataDir).toBe('/Users/u/.
|
|
53
|
+
expect(r.dataDir).toBe('/Users/u/.taiji-dev')
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
it('透传污染反例 1:仅
|
|
56
|
+
it('透传污染反例 1:仅 TAIJI_AGENT_EXT_LOG=1(PI_CODING_AGENT_DIR 未设)→ standalone-pi,evidence 点名孤置信号', () => {
|
|
57
57
|
const r = detectEnvironment(
|
|
58
|
-
signals({ env: { [
|
|
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(`${
|
|
63
|
+
expect(joined).toContain(`${TAIJI_AGENT_EXT_LOG_ENV}='1' → 命中`)
|
|
64
64
|
expect(joined).toContain('单独成立不合取')
|
|
65
|
-
expect(joined).toContain(
|
|
65
|
+
expect(joined).toContain(TAIJI_AGENT_EXT_LOG_ENV)
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
-
it('透传污染反例 2:仅
|
|
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
|
-
[
|
|
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/.
|
|
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('
|
|
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
|
-
[
|
|
101
|
-
[PI_CODING_AGENT_DIR_ENV]: '/Users/u/.
|
|
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('形态防误报:无点前缀
|
|
110
|
-
for (const dir of ['/home/u/
|
|
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
|
-
[
|
|
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/
|
|
148
|
-
'file:///Users/u/Code/
|
|
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('托管:
|
|
163
|
+
it('托管:TAIJI_AGENT_DATA_DIR 优先于 PI_CODING_AGENT_DIR 剥层', () => {
|
|
164
164
|
const r = detectEnvironment(
|
|
165
165
|
signals({
|
|
166
166
|
env: {
|
|
167
|
-
[
|
|
168
|
-
[PI_CODING_AGENT_DIR_ENV]: '/data/.
|
|
169
|
-
[
|
|
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('
|
|
174
|
-
expect(r.dataDir).toBe('/data/.
|
|
175
|
-
expect(r.evidence.join('\n')).toContain(`来源:${
|
|
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
|
-
[
|
|
183
|
-
[PI_CODING_AGENT_DIR_ENV]: '/data/.
|
|
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/.
|
|
187
|
+
expect(newLayout.dataDir).toBe('/data/.taiji')
|
|
188
188
|
|
|
189
189
|
const oldLayout = detectEnvironment(
|
|
190
190
|
signals({
|
|
191
191
|
env: {
|
|
192
|
-
[
|
|
193
|
-
[PI_CODING_AGENT_DIR_ENV]: '/data/.
|
|
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/.
|
|
197
|
+
expect(oldLayout.dataDir).toBe('/data/.taiji')
|
|
198
198
|
})
|
|
199
199
|
|
|
200
|
-
it('standalone:即使
|
|
200
|
+
it('standalone:即使 TAIJI_AGENT_DATA_DIR 被设置也恒 undefined', () => {
|
|
201
201
|
const r = detectEnvironment(
|
|
202
202
|
signals({
|
|
203
203
|
env: {
|
|
204
|
-
[
|
|
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
|
-
[
|
|
219
|
-
[PI_CODING_AGENT_DIR_ENV]: '/data/.
|
|
220
|
-
[
|
|
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: { [
|
|
226
|
-
['agentDir 孤置', signals({ env: { [PI_CODING_AGENT_DIR_ENV]: '/data/.
|
|
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
|
-
[
|
|
242
|
-
[PI_CODING_AGENT_DIR_ENV]: '/data/.
|
|
241
|
+
[TAIJI_AGENT_EXT_LOG_ENV]: '0',
|
|
242
|
+
[PI_CODING_AGENT_DIR_ENV]: '/data/.taiji/pi/agent',
|
|
243
243
|
},
|
|
244
|
-
agentDir: '/data/.
|
|
244
|
+
agentDir: '/data/.taiji/pi/agent',
|
|
245
245
|
}),
|
|
246
246
|
)
|
|
247
247
|
const joined = r.evidence.join('\n')
|
|
248
248
|
// 未命中信号:原文 + 未命中注记
|
|
249
|
-
expect(joined).toContain(`${
|
|
249
|
+
expect(joined).toContain(`${TAIJI_AGENT_EXT_LOG_ENV}='0' → 未命中`)
|
|
250
250
|
// 命中信号:原文 + 匹配注记
|
|
251
|
-
expect(joined).toContain(`${PI_CODING_AGENT_DIR_ENV}='/data/.
|
|
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/.
|
|
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/ 结构,
|
|
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 根(
|
|
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 根(
|
|
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 = '
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 })
|