@zhushanwen/pi-session-reader 0.6.0 → 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 +4 -4
- package/src/__tests__/cross-package-subagent-core.test.ts +36 -44
- package/src/__tests__/env.test.ts +54 -54
- package/src/__tests__/find.test.ts +1 -1
- package/src/__tests__/index.test.ts +1 -1
- package/src/__tests__/real-data.ts +22 -9
- package/src/__tests__/roots.test.ts +1 -1
- package/src/__tests__/subagents.test.ts +1 -4
- package/src/__tests__/tool-handler.test.ts +64 -77
- package/src/core/render.ts +1 -2
- package/src/core/toolcall.ts +1 -2
- package/src/core/units.ts +14 -0
- package/src/discovery/env.ts +41 -41
- package/src/discovery/find.ts +1 -1
- package/src/discovery/roots.ts +2 -2
- package/src/doctor.ts +5 -70
- package/src/index.ts +2 -2
- package/src/result-action.ts +1 -1
- package/src/search-across.ts +1 -4
- 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.6.
|
|
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": {
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
"pi-package"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@zhushanwen/pi-ext-guards": "0.4.
|
|
19
|
+
"@zhushanwen/pi-ext-guards": "0.4.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@vitest/coverage-v8": "^4.1.9",
|
|
23
23
|
"vitest": "^4.1.8",
|
|
24
|
-
"@zhushanwen/subagent-core": "0.10.
|
|
24
|
+
"@zhushanwen/subagent-core": "0.10.5"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
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[]
|
|
@@ -224,21 +223,11 @@ describe('跨包集成:subagent-core E1 落标 manifest → session-reader res
|
|
|
224
223
|
)
|
|
225
224
|
}
|
|
226
225
|
|
|
227
|
-
/**
|
|
228
|
-
*
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (Date.now() - start > timeoutMs) {
|
|
233
|
-
throw new Error(`cross-pkg: condition not met within ${timeoutMs}ms`)
|
|
234
|
-
}
|
|
235
|
-
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** 全链路驱动:种子 → 恢复 service(orphan 覆写)→ E1(落标出口真写 manifest)。
|
|
240
|
-
* 返回子 session 文件路径表。 */
|
|
241
|
-
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>> {
|
|
242
231
|
const childFiles = new Map<string, string>()
|
|
243
232
|
for (const m of members) {
|
|
244
233
|
const childFile = writeChildSessionFile(m)
|
|
@@ -246,12 +235,15 @@ describe('跨包集成:subagent-core E1 落标 manifest → session-reader res
|
|
|
246
235
|
seedRoundTerminalEntries(m, childFile)
|
|
247
236
|
}
|
|
248
237
|
const service = makeRecoveryService()
|
|
249
|
-
service.
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
}
|
|
255
247
|
return childFiles
|
|
256
248
|
}
|
|
257
249
|
|
|
@@ -263,12 +255,12 @@ describe('跨包集成:subagent-core E1 落标 manifest → session-reader res
|
|
|
263
255
|
assistantParts: ['part one', 'part two'],
|
|
264
256
|
model: 'prov/round-m',
|
|
265
257
|
}
|
|
266
|
-
const childFiles = await
|
|
258
|
+
const childFiles = await driveManifestRebuild([m])
|
|
267
259
|
|
|
268
|
-
// manifest 内容断言(
|
|
269
|
-
// sessionFile 来自 W1 投影扩展。[U2/U3 两态桥接]
|
|
270
|
-
// idle
|
|
271
|
-
|
|
260
|
+
// manifest 内容断言(rebuildIndexes → derivedManifestRecord 投影路径的产物,
|
|
261
|
+
// 非手工预写):sessionFile 来自 W1 投影扩展。[U2/U3 两态桥接] 恢复链覆写后成员
|
|
262
|
+
// 处于 idle(重建单规则)、无旧终态遗留位 → legacy status 如实投影 "running"
|
|
263
|
+
//(legacyManifestStatusFields 单点派生,session-reader 视角的活跃成员)。
|
|
272
264
|
const manifest = JSON.parse(
|
|
273
265
|
fs.readFileSync(path.join(getSubagentRecordsDir(agentDir, agentDir), 'sa-cross-1.json'), 'utf-8'),
|
|
274
266
|
) as Record<string, unknown>
|
|
@@ -303,7 +295,7 @@ describe('跨包集成:subagent-core E1 落标 manifest → session-reader res
|
|
|
303
295
|
{ id: 'sa-cross-a', resultText: 'alpha final answer', assistantParts: ['alpha final answer'], model: 'prov/ma' },
|
|
304
296
|
{ id: 'sa-cross-b', resultText: 'beta final answer', assistantParts: ['beta final answer'], model: 'prov/mb' },
|
|
305
297
|
]
|
|
306
|
-
await
|
|
298
|
+
await driveManifestRebuild(members)
|
|
307
299
|
|
|
308
300
|
const r = await handleSessionRead({ action: 'result', session: 'sa-cross-a,sa-cross-b' }, { agentDir })
|
|
309
301
|
const text = r.content[0]?.text ?? ''
|
|
@@ -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
|
})
|
|
@@ -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
|
|
@@ -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)。
|
|
@@ -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 {
|
|
@@ -214,7 +214,7 @@ const HOST_CASES: HostCase[] = [
|
|
|
214
214
|
},
|
|
215
215
|
},
|
|
216
216
|
{
|
|
217
|
-
name: '
|
|
217
|
+
name: 'taiji:live=根本身(RPC 覆盖态),default 去重,legacy 残留与 subagent 独立列出',
|
|
218
218
|
async build(tmp) {
|
|
219
219
|
const dataDir = join(tmp, 'data')
|
|
220
220
|
const agentDir = join(dataDir, 'agent')
|
|
@@ -10,16 +10,13 @@ import {
|
|
|
10
10
|
extractSessionIdFromFilename,
|
|
11
11
|
type RecordManifest,
|
|
12
12
|
} from '../discovery/subagents.js'
|
|
13
|
-
import { REAL_DATA_TIMEOUT_MS } from './real-data.js'
|
|
13
|
+
import { REAL_AGENT_DIR, REAL_DATA_TIMEOUT_MS } from './real-data.js'
|
|
14
14
|
|
|
15
15
|
// ---- fixture 常量(uuid 特征,满足 extractSessionIdFromFilename + 互不为子串)----
|
|
16
16
|
const ROOT = '0aaaaaaa-bbbb-7ccc-dddd-000000000001'
|
|
17
17
|
const FORK = '0aaaaaaa-bbbb-7ccc-dddd-000000000002'
|
|
18
18
|
const SUB_REAL = '0aaaaaaa-bbbb-7ccc-dddd-000000000003'
|
|
19
19
|
|
|
20
|
-
/** 真实 pi agent 目录(本机),用于集成测试。 */
|
|
21
|
-
const REAL_AGENT_DIR = '/Users/zhushanwen/.pi/agent'
|
|
22
|
-
|
|
23
20
|
// 同步探测真实 session 是否存在(不存在则 skip,避免在无该数据的机器上硬失败)
|
|
24
21
|
function hasRealSession(sid: string): boolean {
|
|
25
22
|
try {
|
|
@@ -30,11 +30,20 @@ import {
|
|
|
30
30
|
hasRealSession,
|
|
31
31
|
} from './real-data.js'
|
|
32
32
|
|
|
33
|
+
// fs-guard:TC6 的「~ 前缀展开」语义需要 homedir,但真实 homedir 根在白名单外(禁止向用户
|
|
34
|
+
// 主目录散落文件)。vi.mock 把 homedir 指到用例内 mkdtemp 的 tmp;fakeHome 未注入时回落真实值,
|
|
35
|
+
// real-data.ts 模块初始化时推导 REAL_AGENT_DIR 不受影响(模块初始化先于任何用例执行)。
|
|
36
|
+
const homeState = vi.hoisted(() => ({ fakeHome: undefined as string | undefined }))
|
|
37
|
+
vi.mock('node:os', async (importOriginal) => {
|
|
38
|
+
const actual = await importOriginal<typeof import('node:os')>()
|
|
39
|
+
return { ...actual, homedir: () => homeState.fakeHome ?? actual.homedir() }
|
|
40
|
+
})
|
|
41
|
+
|
|
33
42
|
/**
|
|
34
43
|
* M3 tool-handler 集成测试。
|
|
35
44
|
*
|
|
36
45
|
* 测试框架 vitest(禁止 node:test/tsx)。直接调 handleSessionRead(纯逻辑,agentDir 注入),
|
|
37
|
-
* 传真实
|
|
46
|
+
* 传真实 `~/.pi/agent` 作 agentDir——用本机真实历史 session 数据,无需 mock。
|
|
38
47
|
*
|
|
39
48
|
* 覆盖全 11 action 主路径 + F1(find 零匹配)/F4(turn 越界)/F5(缺参)/resolveSessionId 片段等价,
|
|
40
49
|
* 以及 u8 doctor / u9 归一化 / u10 find 分组 / u11 metadataProvider 缓存 / u12 跨会话检索(各见对应 describe)。
|
|
@@ -95,14 +104,21 @@ describe.skipIf(!HAS_REAL)('handleSessionRead', () => {
|
|
|
95
104
|
})
|
|
96
105
|
|
|
97
106
|
it('6. export outline materializes a .md file', async () => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
// fs-guard:export 写目标 = <agentDir>/tmp/,agentDir 传 REAL 会 mkdir 真实 ~/.pi/agent/tmp
|
|
108
|
+
// (白名单外)。读侧经 liveSessionDir=[live] 根仍扫真实数据,写侧落 mkdtemp tmp 自建自删。
|
|
109
|
+
const dir = await mkdtemp(join(tmpdir(), 'tool-handler-export-'))
|
|
110
|
+
try {
|
|
111
|
+
const r = await handleSessionRead(
|
|
112
|
+
{ action: 'export', session: E6, format: 'outline' },
|
|
113
|
+
{ agentDir: dir, liveSessionDir: join(REAL, 'sessions') },
|
|
114
|
+
)
|
|
115
|
+
const d = r.details as { path: string; sizeBytes: number }
|
|
116
|
+
expect(d.path).toMatch(/\.md$/)
|
|
117
|
+
expect(existsSync(d.path)).toBe(true)
|
|
118
|
+
expect(d.sizeBytes).toBeGreaterThan(0)
|
|
119
|
+
} finally {
|
|
120
|
+
await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
121
|
+
}
|
|
106
122
|
})
|
|
107
123
|
|
|
108
124
|
// w1 合并 subagent 候选后,本机 subagent task 文本(含本用例 query 字符串本身,因当前
|
|
@@ -643,7 +659,7 @@ describe('extract commits 双路径(fixture:git-cmd 主路径 + commit-conte
|
|
|
643
659
|
message: {
|
|
644
660
|
role: 'assistant',
|
|
645
661
|
content: [
|
|
646
|
-
{ type: 'toolCall', id: 'tc-read-1', name: 'read', arguments: { path: '
|
|
662
|
+
{ type: 'toolCall', id: 'tc-read-1', name: 'read', arguments: { path: join(tmpdir(), 'log.txt') } },
|
|
647
663
|
],
|
|
648
664
|
},
|
|
649
665
|
}),
|
|
@@ -667,7 +683,7 @@ describe('extract commits 双路径(fixture:git-cmd 主路径 + commit-conte
|
|
|
667
683
|
message: {
|
|
668
684
|
role: 'assistant',
|
|
669
685
|
content: [
|
|
670
|
-
{ type: 'toolCall', id: 'tc-read-2', name: 'read', arguments: { path: '
|
|
686
|
+
{ type: 'toolCall', id: 'tc-read-2', name: 'read', arguments: { path: join(tmpdir(), 'out.txt') } },
|
|
671
687
|
],
|
|
672
688
|
},
|
|
673
689
|
}),
|
|
@@ -724,7 +740,7 @@ describe('extract commits 双路径(fixture:git-cmd 主路径 + commit-conte
|
|
|
724
740
|
message: {
|
|
725
741
|
role: 'assistant',
|
|
726
742
|
content: [
|
|
727
|
-
{ type: 'toolCall', id: 'tc-read-1', name: 'read', arguments: { path: '
|
|
743
|
+
{ type: 'toolCall', id: 'tc-read-1', name: 'read', arguments: { path: join(tmpdir(), 'deploy.txt') } },
|
|
728
744
|
],
|
|
729
745
|
},
|
|
730
746
|
}),
|
|
@@ -933,24 +949,33 @@ describe('resolveSessionId ① 绝对路径形态(w2 TC2-TC6)', () => {
|
|
|
933
949
|
})
|
|
934
950
|
|
|
935
951
|
it('TC6: ~ 前缀展开到 homedir(文件实际在 homedir 下)', async () => {
|
|
936
|
-
|
|
937
|
-
|
|
952
|
+
// fs-guard:真实 homedir 根在白名单外(禁止在用户主目录根散落 .sr-w2-test-*)——
|
|
953
|
+
// homedir 经文件头 vi.mock 指到 mkdtemp tmp(白名单内),「~ 前缀展开」断言语义不变
|
|
954
|
+
const fakeHome = await mkdtemp(join(tmpdir(), 'tool-handler-tc6-home-'))
|
|
955
|
+
homeState.fakeHome = fakeHome
|
|
938
956
|
try {
|
|
939
|
-
const
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
957
|
+
const home = homedir()
|
|
958
|
+
const tmpUnderHome = await mkdtemp(join(home, '.sr-w2-test-'))
|
|
959
|
+
try {
|
|
960
|
+
const fileId = '019e6c96-dddd-eeee-ffff-0000000006c1'
|
|
961
|
+
const sessionFile = join(tmpUnderHome, 's.jsonl')
|
|
962
|
+
await writeFile(
|
|
963
|
+
sessionFile,
|
|
964
|
+
JSON.stringify({ type: 'session', id: fileId, cwd: '/demo' }) + '\n',
|
|
965
|
+
)
|
|
966
|
+
// ~/开头的相对 homedir 路径
|
|
967
|
+
const tildePath = '~/' + sessionFile.slice(home.length + 1)
|
|
968
|
+
const r = await handleSessionRead(
|
|
969
|
+
{ action: 'export', session: tildePath, format: 'outline' },
|
|
970
|
+
{ agentDir: dir },
|
|
971
|
+
)
|
|
972
|
+
expect((r.details as { path: string }).path).toContain(fileId)
|
|
973
|
+
} finally {
|
|
974
|
+
await rm(tmpUnderHome, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
975
|
+
}
|
|
952
976
|
} finally {
|
|
953
|
-
|
|
977
|
+
homeState.fakeHome = undefined
|
|
978
|
+
await rm(fakeHome, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
954
979
|
}
|
|
955
980
|
})
|
|
956
981
|
})
|
|
@@ -1801,8 +1826,8 @@ describe('doctor action(u8:环境判定 + 根表 + 告警 + 残留 glob)',
|
|
|
1801
1826
|
}
|
|
1802
1827
|
|
|
1803
1828
|
/**
|
|
1804
|
-
*
|
|
1805
|
-
* PI_CODING_AGENT_DIR 形态判据 `<*>/.
|
|
1829
|
+
* taiji 形态四根 fixture:dataDir 目录名带 .taiji 前缀(detectEnvironment 的
|
|
1830
|
+
* PI_CODING_AGENT_DIR 形态判据 `<*>/.taiji*\/agent` 可命中,托管判定走正态)。
|
|
1806
1831
|
* live=agent/sessions 与 default 同字面路径(default 去重)、legacy 独立非空、subagent 独立。
|
|
1807
1832
|
*/
|
|
1808
1833
|
async function buildFourRootFixture(): Promise<{
|
|
@@ -1810,7 +1835,7 @@ describe('doctor action(u8:环境判定 + 根表 + 告警 + 残留 glob)',
|
|
|
1810
1835
|
agentDir: string
|
|
1811
1836
|
signals: SessionReadSignals
|
|
1812
1837
|
}> {
|
|
1813
|
-
const dataDir = join(tmp, '.
|
|
1838
|
+
const dataDir = join(tmp, '.taiji-fixture')
|
|
1814
1839
|
const agentDir = join(dataDir, 'agent')
|
|
1815
1840
|
await writeJsonl(join(agentDir, 'sessions', SLUG, 'a.jsonl'), 'aaaaaaaa')
|
|
1816
1841
|
await writeJsonl(join(agentDir, 'sessions', SLUG, 'b.jsonl'), 'bbbbbbbb')
|
|
@@ -1823,9 +1848,9 @@ describe('doctor action(u8:环境判定 + 根表 + 告警 + 残留 glob)',
|
|
|
1823
1848
|
agentDir,
|
|
1824
1849
|
liveSessionDir: join(agentDir, 'sessions'),
|
|
1825
1850
|
env: {
|
|
1826
|
-
|
|
1851
|
+
TAIJI_AGENT_EXT_LOG: '1',
|
|
1827
1852
|
PI_CODING_AGENT_DIR: agentDir,
|
|
1828
|
-
|
|
1853
|
+
TAIJI_AGENT_DATA_DIR: dataDir,
|
|
1829
1854
|
},
|
|
1830
1855
|
},
|
|
1831
1856
|
}
|
|
@@ -1843,8 +1868,8 @@ describe('doctor action(u8:环境判定 + 根表 + 告警 + 残留 glob)',
|
|
|
1843
1868
|
const r = await handleSessionRead({ action: 'doctor' }, signals)
|
|
1844
1869
|
const text = r.content[0].text
|
|
1845
1870
|
|
|
1846
|
-
// 环境判定行:双信号合取 →
|
|
1847
|
-
expect(text).toContain('环境判定:
|
|
1871
|
+
// 环境判定行:双信号合取 → taiji(托管)+ 数据目录 + evidence 正态
|
|
1872
|
+
expect(text).toContain('环境判定:taiji(托管)')
|
|
1848
1873
|
expect(text).toContain(`数据目录:${dataDir}`)
|
|
1849
1874
|
expect(text).toContain('依据:')
|
|
1850
1875
|
expect(text).toContain(`PI_CODING_AGENT_DIR='${agentDir}' → 形态匹配`)
|
|
@@ -1887,7 +1912,7 @@ describe('doctor action(u8:环境判定 + 根表 + 告警 + 残留 glob)',
|
|
|
1887
1912
|
const { signals } = await buildFourRootFixture()
|
|
1888
1913
|
const positive = await handleSessionRead({ action: 'doctor' }, signals)
|
|
1889
1914
|
expect(positive.content[0].text).toContain(
|
|
1890
|
-
`告警:[legacy] 根 ${join(tmp, '.
|
|
1915
|
+
`告警:[legacy] 根 ${join(tmp, '.taiji-fixture', 'sessions')} 非空(1 文件)`,
|
|
1891
1916
|
)
|
|
1892
1917
|
|
|
1893
1918
|
// 反态:legacy 目录不存在
|
|
@@ -1908,43 +1933,6 @@ describe('doctor action(u8:环境判定 + 根表 + 告警 + 残留 glob)',
|
|
|
1908
1933
|
expect(deduped.content[0].text).toContain('已去重')
|
|
1909
1934
|
})
|
|
1910
1935
|
|
|
1911
|
-
it('独立 glob 残留探测:pi/ 含 agent|sessions 形态列出 + 迁移指引;pi.backup-v2-* 备份列出;基点 = dirname(agentDir)', async () => {
|
|
1912
|
-
const dataDir = join(tmp, 'data')
|
|
1913
|
-
const agentDir = join(dataDir, 'agent')
|
|
1914
|
-
await writeJsonl(join(agentDir, 'sessions', SLUG, 'a.jsonl'))
|
|
1915
|
-
// 未迁移旧布局 pi/(含 agent/ 形态)
|
|
1916
|
-
await mkdir(join(dataDir, 'pi', 'agent'), { recursive: true })
|
|
1917
|
-
await writeJsonl(join(dataDir, 'pi', 'agent', 'sessions', 'old.jsonl'), 'old-id')
|
|
1918
|
-
// 迁移备份(含 sessions/ 形态——备份即原 pi/ 改名)
|
|
1919
|
-
const backup = join(dataDir, 'pi.backup-v2-1725900000000')
|
|
1920
|
-
await mkdir(join(backup, 'sessions'), { recursive: true })
|
|
1921
|
-
await writeJsonl(join(backup, 'sessions', 'bak.jsonl'), 'bak-id')
|
|
1922
|
-
|
|
1923
|
-
const r = await handleSessionRead({ action: 'doctor' }, { agentDir })
|
|
1924
|
-
const text = r.content[0].text
|
|
1925
|
-
expect(text).toContain(`旧布局残留探测(基点 ${dataDir})`)
|
|
1926
|
-
expect(text).toContain(` - ${join(dataDir, 'pi')}(未迁移旧布局 pi/)`)
|
|
1927
|
-
expect(text).toContain(` - ${backup}(迁移备份 pi.backup-v2-*)`)
|
|
1928
|
-
// 同一迁移指引(§6.11:doctor 与 u14b 启动探测同指引)
|
|
1929
|
-
expect(text).toContain('👉 关闭应用后运行 scripts/migrate-pi-layout-v2.mjs 完成迁移。')
|
|
1930
|
-
|
|
1931
|
-
const d = r.details as DoctorDetails
|
|
1932
|
-
expect(d.globBase).toBe(dataDir)
|
|
1933
|
-
expect(d.leftovers.map((l) => l.kind).sort()).toEqual(['backup', 'unmigrated'])
|
|
1934
|
-
})
|
|
1935
|
-
|
|
1936
|
-
it('残留探测反态:pi/ 空壳(无 agent|sessions 子目录)不报——防纯 pi 宿主 ~/.pi/pi/ 误报(§6.11 v9.1)', async () => {
|
|
1937
|
-
const dataDir = join(tmp, 'data')
|
|
1938
|
-
const agentDir = join(dataDir, 'agent')
|
|
1939
|
-
await writeJsonl(join(agentDir, 'sessions', SLUG, 'a.jsonl'))
|
|
1940
|
-
// 纯 pi 宿主下任意来源的 pi/ 目录:只有无关子目录,无 agent/ 与 sessions/ 形态
|
|
1941
|
-
await mkdir(join(dataDir, 'pi', 'extensions'), { recursive: true })
|
|
1942
|
-
|
|
1943
|
-
const r = await handleSessionRead({ action: 'doctor' }, { agentDir })
|
|
1944
|
-
expect(r.content[0].text).not.toContain('旧布局残留')
|
|
1945
|
-
expect((r.details as DoctorDetails).leftovers).toEqual([])
|
|
1946
|
-
})
|
|
1947
|
-
|
|
1948
1936
|
it('subagent 根默认不扫(只列路径与可扫性);includeSubagents:true 才扫出文件数(§6.3)', async () => {
|
|
1949
1937
|
const { agentDir, signals } = await buildFourRootFixture()
|
|
1950
1938
|
|
|
@@ -1995,7 +1983,7 @@ describe('doctor action(u8:环境判定 + 根表 + 告警 + 残留 glob)',
|
|
|
1995
1983
|
const r = await handleSessionRead({ action: 'doctor' }, { agentDir })
|
|
1996
1984
|
const text = r.content[0].text
|
|
1997
1985
|
expect(text).toContain('环境判定:standalone-pi · 发行形态:未知(不猜)')
|
|
1998
|
-
expect(text).toContain('
|
|
1986
|
+
expect(text).toContain('TAIJI_AGENT_EXT_LOG=<未设置> → 未命中')
|
|
1999
1987
|
expect(text).toContain('会话根(按优先级)')
|
|
2000
1988
|
expect(text).toContain('[default]')
|
|
2001
1989
|
expect(text).toContain(
|
|
@@ -2003,13 +1991,12 @@ describe('doctor action(u8:环境判定 + 根表 + 告警 + 残留 glob)',
|
|
|
2003
1991
|
)
|
|
2004
1992
|
})
|
|
2005
1993
|
|
|
2006
|
-
it('空 agentDir
|
|
1994
|
+
it('空 agentDir 防御:无根表、诊断「无候选根」(不扫 cwd 相对路径)', async () => {
|
|
2007
1995
|
const r = await handleSessionRead({ action: 'doctor' }, { agentDir: '' })
|
|
2008
1996
|
const text = r.content[0].text
|
|
2009
1997
|
expect(text).toContain('诊断:无候选根')
|
|
2010
1998
|
expect(text).not.toContain('[default]')
|
|
2011
1999
|
expect((r.details as DoctorDetails).roots).toEqual([])
|
|
2012
|
-
expect((r.details as DoctorDetails).leftovers).toEqual([])
|
|
2013
2000
|
})
|
|
2014
2001
|
})
|
|
2015
2002
|
|
|
@@ -2641,7 +2628,7 @@ describe('E1 find 单次根解析(探针 P1:resolveSessionRoots 恰 1 次)
|
|
|
2641
2628
|
// ============================================================
|
|
2642
2629
|
describe('u11 metadataProvider 注入 + TTL 缓存(fixture,§6.6)', () => {
|
|
2643
2630
|
let tmp: string
|
|
2644
|
-
/** 平铺 main 根(
|
|
2631
|
+
/** 平铺 main 根(taiji 形态),同时作 liveSessionDir(与 [default] 根同字面路径) */
|
|
2645
2632
|
let flatMain: string
|
|
2646
2633
|
|
|
2647
2634
|
async function writeSession(
|
package/src/core/render.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Entry } from './parser.js'
|
|
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 字符」)。 */
|
package/src/core/toolcall.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* parser.ts 已 additive 透出这两个字段,O2/O3 据此精确关联取参数。
|
|
10
10
|
*/
|
|
11
11
|
import type { Entry } from './parser.js'
|
|
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 {
|
|
@@ -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
|
package/src/discovery/env.ts
CHANGED
|
@@ -12,35 +12,35 @@ import { dirname } from 'node:path'
|
|
|
12
12
|
* 齐备),不 import roots.ts 的信号类型,避免并行单元耦合;接线层分别组装两种信号包。
|
|
13
13
|
*
|
|
14
14
|
* 信号可靠性依据(design §4.4 活体实测表,非推断):
|
|
15
|
-
* -
|
|
16
|
-
* ENV_WHITELIST_PREFIXES 含裸 `
|
|
15
|
+
* - TAIJI_AGENT_EXT_LOG 单独使用可靠性「中」——runtime 无条件注入,但
|
|
16
|
+
* ENV_WHITELIST_PREFIXES 含裸 `TAIJI_` 前缀,用户 shell 里的同名变量会透传进任何进程;
|
|
17
17
|
* - PI_CODING_AGENT_DIR 单独使用可靠性「中」——pi 原生变量,裸 pi 用户也可能自设,
|
|
18
|
-
* 但其值形态是
|
|
19
|
-
* -
|
|
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
|
-
/**
|
|
26
|
-
export const
|
|
27
|
-
/**
|
|
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
|
|
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
|
-
//
|
|
36
|
-
// - B 后新布局:`<*>/.
|
|
37
|
-
// - B 前旧布局:`<*>/.
|
|
38
|
-
// `.
|
|
39
|
-
// 三条正则都要求匹配到路径末尾(`$`),调用前输入先经
|
|
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 = /(^|\/)\.
|
|
42
|
-
const AGENT_DIR_SHAPE_OLD = /(^|\/)\.
|
|
43
|
-
const AGENT_DIR_SHAPE_NEW = /(^|\/)\.
|
|
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 = '
|
|
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
|
-
/** 托管时的
|
|
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
|
|
81
|
+
function normalizeTaijiPath(p: string): string {
|
|
82
82
|
return p.replace(/\\/g, '/').replace(/\/+$/, '')
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
function
|
|
86
|
-
return AGENT_DIR_SHAPE_ANY.test(
|
|
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
|
-
* 托管时的数据目录推导:
|
|
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[
|
|
117
|
+
const dataDirEnv = env[TAIJI_AGENT_DATA_DIR_ENV]
|
|
118
118
|
if (dataDirEnv) {
|
|
119
|
-
evidence.push(`数据目录:'${dataDirEnv}'(来源:${
|
|
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 =
|
|
125
|
-
// 旧形态更长,先判:`.
|
|
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(`数据目录:无法推导(${
|
|
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
|
-
? `${
|
|
150
|
-
: `${
|
|
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 后新布局 / 非
|
|
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 '非
|
|
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(
|
|
171
|
+
const shape = describeAgentDirShape(normalizeTaijiPath(agentDirEnv))
|
|
172
172
|
evidence.push(
|
|
173
|
-
`${PI_CODING_AGENT_DIR_ENV}='${agentDirEnv}' → ${agentDirHit ? `形态匹配(${shape})` : `形态不匹配(${shape},要求 <*>/.
|
|
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 `判定:
|
|
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(
|
|
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
|
-
* 托管(
|
|
196
|
-
* 值匹配
|
|
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:
|
|
209
|
-
const extLog = 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 &&
|
|
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 ? '
|
|
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)
|
package/src/discovery/find.ts
CHANGED
|
@@ -327,7 +327,7 @@ function isFlatRoot(root: SessionRoot): boolean {
|
|
|
327
327
|
* 加载标题元数据索引(u11)。仅 keyword 路径调用(惰性触发:uuid 精确/归一化两级零命中
|
|
328
328
|
* 且 query 非纯 hex 才到达此处,§6.6 调用策略 ①)。
|
|
329
329
|
*
|
|
330
|
-
* 窄化目标(策略 ②,按序):未剥层 liveSessionDir 本身(
|
|
330
|
+
* 窄化目标(策略 ②,按序):未剥层 liveSessionDir 本身(taiji 下即平铺主根;纯 pi 下
|
|
331
331
|
* 即当前 cwd 目录,小)+ 实扫结果无子目录的候选根。两条 guard:
|
|
332
332
|
* - 仅对存在的根/目录调用(root.exists / stat liveSessionDir),且永传非空串——pi 的
|
|
333
333
|
* listAll 对空串 falsy 走默认全盘分支(数千项 / 秒级,hash-provider.ts 空参灾难先例);
|
package/src/discovery/roots.ts
CHANGED
|
@@ -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 后
|
|
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--`),在
|
|
69
|
+
*(`<agentDir>/sessions/--Users-x--`),在 taiji 覆盖态返回根本身。判据:basename
|
|
70
70
|
* 匹配 encodeCwd 形态(`--` 开头 `--` 结尾,实证见 `~/.pi/agent/sessions/` 全部子目录
|
|
71
71
|
* 与 real-data.ts)则取 dirname,否则取自身。
|
|
72
72
|
*/
|
package/src/doctor.ts
CHANGED
|
@@ -2,13 +2,10 @@
|
|
|
2
2
|
* doctor action(u8:design 2026-09-10 §6.2/§6.3/§6.4/§7B 要点 2/4/5/8 + §6.11 U14b 段)。
|
|
3
3
|
*
|
|
4
4
|
* 从 tool-handler.ts 机械提取(max-lines 拆分轮,零行为变更):环境判定 + 根表渲染 +
|
|
5
|
-
*
|
|
5
|
+
* SessionReadSignals(u3 信号包超集)在域内定义,tool-handler
|
|
6
6
|
* re-export 供 index.ts 生产消费。原 u8 的扫描缓存机已删除(ext-simplify-04 U3):
|
|
7
7
|
* 每次调用实扫,无缓存命中语义;TTL/mtime 判定随 metadata 缓存留在 tool-handler 侧。
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync } from 'node:fs'
|
|
10
|
-
import { readdir } from 'node:fs/promises'
|
|
11
|
-
import { join, dirname } from 'node:path'
|
|
12
9
|
import { detectEnvironment, type DetectedEnvironment } from './discovery/env.js'
|
|
13
10
|
import { resolveSessionRoots, type SessionRoot, type SessionRootSignals } from './discovery/roots.js'
|
|
14
11
|
import type { SessionReadParams, ToolResult } from './tool-handler.js'
|
|
@@ -29,52 +26,8 @@ export interface SessionReadSignals extends SessionRootSignals {
|
|
|
29
26
|
/** doctor 根表来源标签列宽(最长 `[subagent]` 10 字符 + 1 对齐间距,§5.1 表格形态)。 */
|
|
30
27
|
const DOCTOR_ROOT_LABEL_PAD = 11
|
|
31
28
|
|
|
32
|
-
/** 旧布局残留条目(§6.11 U14b doctor 侧独立 glob 探测)。 */
|
|
33
|
-
/** 旧布局残留探测单条命中(doctor 与 u14b 启动探测同源判据,§6.11)。 */
|
|
34
|
-
export interface PiLayoutLeftover {
|
|
35
|
-
path: string
|
|
36
|
-
kind: 'backup' | 'unmigrated'
|
|
37
|
-
}
|
|
38
|
-
|
|
39
29
|
/**
|
|
40
|
-
*
|
|
41
|
-
* `<dataDir>`,纯 pi 下 = `~/.pi`);`pi/`(未迁移)与 `pi.backup-v2-` 前缀目录(迁移备份)
|
|
42
|
-
* 都不在任何候选根推导式内([legacy] 够不到带时间戳的备份名与 pi/ 层),故须独立探测。
|
|
43
|
-
*
|
|
44
|
-
* 形态判据(与 u14b 启动探测 / u14a 脚本 0a 同源):目录下含 `agent/` 或 `sessions/`
|
|
45
|
-
* 子目录才算残留——防纯 pi 宿主下任意来源的 `~/.pi/pi/` 目录误报。读探测,每次现查
|
|
46
|
-
* 不入缓存(两次 readdir 成本可忽略,缓存只服务扫盘贵的根)。
|
|
47
|
-
*
|
|
48
|
-
* 迁移期支持(A4 登记):本探测服务于 v2 布局迁移窗口,全量迁移确认后可整段删除。
|
|
49
|
-
*/
|
|
50
|
-
async function detectPiLayoutLeftovers(
|
|
51
|
-
agentDir: string,
|
|
52
|
-
): Promise<{ base: string; hits: PiLayoutLeftover[] }> {
|
|
53
|
-
const base = dirname(agentDir)
|
|
54
|
-
if (agentDir.length === 0) return { base, hits: [] }
|
|
55
|
-
let names: string[]
|
|
56
|
-
try {
|
|
57
|
-
names = (await readdir(base, { withFileTypes: true }))
|
|
58
|
-
.filter((e) => e.isDirectory())
|
|
59
|
-
.map((e) => e.name)
|
|
60
|
-
} catch {
|
|
61
|
-
return { base, hits: [] }
|
|
62
|
-
}
|
|
63
|
-
const hits: PiLayoutLeftover[] = []
|
|
64
|
-
for (const name of names) {
|
|
65
|
-
const isUnmigrated = name === 'pi'
|
|
66
|
-
const isBackup = name.startsWith('pi.backup-v2-')
|
|
67
|
-
if (!isUnmigrated && !isBackup) continue
|
|
68
|
-
const dir = join(base, name)
|
|
69
|
-
// 形态判据:含 agent/ 或 sessions/ 子目录(迁移备份必含其一——备份即原 pi/ 改名)
|
|
70
|
-
if (!existsSync(join(dir, 'agent')) && !existsSync(join(dir, 'sessions'))) continue
|
|
71
|
-
hits.push({ path: dir, kind: isBackup ? 'backup' : 'unmigrated' })
|
|
72
|
-
}
|
|
73
|
-
return { base, hits }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* doctor:环境判定 + 根表 + 事实型诊断 + legacy 非空告警 + 旧布局残留探测。
|
|
30
|
+
* doctor:环境判定 + 根表 + 事实型诊断 + legacy 非空告警。
|
|
78
31
|
*
|
|
79
32
|
* 诊断结论只陈述事实(「最高优先级 main 根 N 文件」),禁止「真的没有 session」类
|
|
80
33
|
* 归因断言(§7B 要点 5 / §3.3 教训);env/bundleUrl 信号缺失时环境判定按降级输出,
|
|
@@ -94,8 +47,7 @@ export async function doDoctor(
|
|
|
94
47
|
// 才扫。无缓存——每次实扫(原缓存机已删,ext-simplify-04 U3,§7B 要点 8 新态)。
|
|
95
48
|
subagents: params.includeSubagents === true ? 'scan' : 'stat',
|
|
96
49
|
})
|
|
97
|
-
|
|
98
|
-
return renderDoctor(roots, environment, leftovers)
|
|
50
|
+
return renderDoctor(roots, environment)
|
|
99
51
|
}
|
|
100
52
|
|
|
101
53
|
/**
|
|
@@ -105,24 +57,21 @@ export async function doDoctor(
|
|
|
105
57
|
export interface DoctorDetails {
|
|
106
58
|
environment: DetectedEnvironment
|
|
107
59
|
roots: SessionRoot[]
|
|
108
|
-
leftovers: PiLayoutLeftover[]
|
|
109
|
-
globBase: string
|
|
110
60
|
}
|
|
111
61
|
|
|
112
62
|
/**
|
|
113
63
|
* 渲染 doctor 输出(§5.1 形态):环境判定 + evidence + 会话根表(来源标签/路径/存在/
|
|
114
|
-
* 文件数/扫描耗时/去重注记)+ 事实型诊断 + legacy
|
|
64
|
+
* 文件数/扫描耗时/去重注记)+ 事实型诊断 + legacy 非空告警。
|
|
115
65
|
*/
|
|
116
66
|
function renderDoctor(
|
|
117
67
|
roots: SessionRoot[],
|
|
118
68
|
environment: DetectedEnvironment,
|
|
119
|
-
leftovers: { base: string; hits: PiLayoutLeftover[] },
|
|
120
69
|
): ToolResult {
|
|
121
70
|
const lines: string[] = []
|
|
122
71
|
|
|
123
72
|
// 环境判定行 + evidence(§6.2:判定可被证据推翻,非黑盒断言;evidence 恒非空)
|
|
124
73
|
lines.push(
|
|
125
|
-
`环境判定:${environment.kind === '
|
|
74
|
+
`环境判定:${environment.kind === 'taiji' ? 'taiji(托管)' : 'standalone-pi'}` +
|
|
126
75
|
` · 发行形态:${environment.distribution ?? '未知(不猜)'}`,
|
|
127
76
|
)
|
|
128
77
|
if (environment.dataDir !== undefined) lines.push(`数据目录:${environment.dataDir}`)
|
|
@@ -172,25 +121,11 @@ function renderDoctor(
|
|
|
172
121
|
)
|
|
173
122
|
}
|
|
174
123
|
|
|
175
|
-
// 旧布局残留(§6.11:命中才列,附同一迁移指引)
|
|
176
|
-
if (leftovers.hits.length > 0) {
|
|
177
|
-
lines.push('')
|
|
178
|
-
lines.push(`旧布局残留探测(基点 ${leftovers.base}):`)
|
|
179
|
-
for (const h of leftovers.hits) {
|
|
180
|
-
lines.push(
|
|
181
|
-
` - ${h.path}(${h.kind === 'backup' ? '迁移备份 pi.backup-v2-*' : '未迁移旧布局 pi/'})`,
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
lines.push(' 👉 关闭应用后运行 scripts/migrate-pi-layout-v2.mjs 完成迁移。')
|
|
185
|
-
}
|
|
186
|
-
|
|
187
124
|
return {
|
|
188
125
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
189
126
|
details: {
|
|
190
127
|
environment,
|
|
191
128
|
roots,
|
|
192
|
-
leftovers: leftovers.hits,
|
|
193
|
-
globBase: leftovers.base,
|
|
194
129
|
} satisfies DoctorDetails,
|
|
195
130
|
}
|
|
196
131
|
}
|
package/src/index.ts
CHANGED
|
@@ -157,7 +157,7 @@ const guidelines = [
|
|
|
157
157
|
"workflow action to see workflow run overviews (status/budget/steps). Each step's call sessionId can jump to outline/detail for deep reading.",
|
|
158
158
|
"result action to fetch a subagent's final result text (same content as its completion notice): session takes a single sa-id/uuid/path or a comma-separated batch of at most 10; optional limit caps chars per item (default 8000, truncated items carry a pointer to the full file).",
|
|
159
159
|
'Errors carry a 👉 recovery hint—follow it to retry in one step.',
|
|
160
|
-
'Unsure about the host environment (pure pi vs
|
|
160
|
+
'Unsure about the host environment (pure pi vs taiji) or where session roots live? Run action:"doctor" first—it prints the detected environment and every candidate session root.',
|
|
161
161
|
]
|
|
162
162
|
|
|
163
163
|
// ---- 工具 description(design §3.4,照搬措辞)----
|
|
@@ -231,7 +231,7 @@ export default function sessionReaderExtension(pi: ExtensionAPI): void {
|
|
|
231
231
|
|
|
232
232
|
// ── M4 TUI 层(design §1 + §3.3 D-3/D-4 + 附录 P-hash-trigger)──────────
|
|
233
233
|
// # 引用补全 provider + /session-pick 命令(命令名避开 pi 内置 /session 冲突)。仅 ctx.mode === 'tui' 注册:RPC 模式
|
|
234
|
-
// (
|
|
234
|
+
// (taiji 子进程)不用 pi TUI editor / slash 命令,加载即跳过。
|
|
235
235
|
//
|
|
236
236
|
// addAutocompleteProvider 挂在 ctx.ui(非 ExtensionAPI),setup 入口无 ctx,
|
|
237
237
|
// 只能在 event handler 里拿——session_start 是最早且每 session 触发的 event。
|
package/src/result-action.ts
CHANGED
package/src/search-across.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { parseSessionFile } from './core/parser.js'
|
|
17
17
|
import { segmentTurns, type Turn } from './core/turns.js'
|
|
18
18
|
import { buildTreeView } from './core/tree.js'
|
|
19
|
+
import { BYTES_PER_KB, BYTES_PER_MB } from './core/units.js'
|
|
19
20
|
import { err, pad } from './handler-utils.js'
|
|
20
21
|
import type { SessionReadSignals } from './doctor.js'
|
|
21
22
|
import type { SessionReadParams, ToolResult } from './tool-handler.js'
|
|
@@ -163,10 +164,6 @@ export function formatSearchText(
|
|
|
163
164
|
*/
|
|
164
165
|
export const MULTI_SEARCH_MAX_SESSIONS = 10
|
|
165
166
|
|
|
166
|
-
/** 字节展示/预算换算基数(人话格式化与预算常量共用同一量纲)。 */
|
|
167
|
-
const BYTES_PER_KB = 1024
|
|
168
|
-
const BYTES_PER_MB = BYTES_PER_KB * BYTES_PER_KB
|
|
169
|
-
|
|
170
167
|
/** 跨会话单次检索的预算 MB 数(§8.2 V8 字节上限的量纲;换算见 SEARCH_SCAN_BYTE_BUDGET)。 */
|
|
171
168
|
const SEARCH_SCAN_BUDGET_MB = 64
|
|
172
169
|
|
package/vitest.config.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { taijiTestConfig } from '../../../test-guard/factory.ts'
|
|
4
4
|
|
|
5
|
-
export default
|
|
5
|
+
export default taijiTestConfig({
|
|
6
6
|
test: {
|
|
7
|
-
reporters: ['default', 'junit'],
|
|
8
|
-
outputFile: { junit: './test-results/vitest-junit.xml' },
|
|
9
7
|
include: ['src/__tests__/**/*.test.ts'],
|
|
10
8
|
},
|
|
11
9
|
resolve: {
|