@zhushanwen/pi-session-reader 0.8.1 → 0.9.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 +32 -9
- package/src/__tests__/entry-anchor.test.ts +21 -9
- package/src/__tests__/execution-tree.test.ts +38 -40
- package/src/__tests__/rebuild-manifest-dual-write.test.ts +10 -6
- package/src/__tests__/subagents.test.ts +36 -185
- package/src/__tests__/tool-handler.test.ts +49 -135
- package/src/__tests__/workflow.test.ts +69 -163
- package/src/__tests__/zcode-manifest.test.ts +4 -1
- package/src/__tests__/zcode-routing.test.ts +12 -15
- package/src/core/workflow.ts +1 -1
- package/src/discovery/entry-anchor.ts +10 -13
- package/src/discovery/workflows.ts +73 -221
- package/src/tool-handler.ts +1 -1
- package/src/zcode-anchor-classify.ts +4 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-session-reader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"taiji": {
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@zhushanwen/pi-extension-logger": "0.6.1",
|
|
20
20
|
"@zhushanwen/pi-ext-guards": "0.4.2",
|
|
21
|
-
"@zhushanwen/session-
|
|
22
|
-
"@zhushanwen/
|
|
21
|
+
"@zhushanwen/zcode-session-source": "0.2.4",
|
|
22
|
+
"@zhushanwen/session-core": "0.11.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@vitest/coverage-v8": "^4.1.9",
|
|
26
26
|
"vitest": "^4.1.8",
|
|
27
|
-
"@zhushanwen/subagent-core": "
|
|
27
|
+
"@zhushanwen/subagent-core": "1.2.1"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"index.ts",
|
|
@@ -9,6 +9,7 @@ import { handleSessionRead } from '../tool-handler.js'
|
|
|
9
9
|
import {
|
|
10
10
|
ModelConfigService,
|
|
11
11
|
RecordStore,
|
|
12
|
+
SUBAGENT_RECORD_CUSTOM_TYPE,
|
|
12
13
|
SubagentService,
|
|
13
14
|
type SubagentRecord,
|
|
14
15
|
} from '@zhushanwen/subagent-core'
|
|
@@ -135,7 +136,9 @@ describe('跨包集成:subagent-core 重建 manifest → session-reader result
|
|
|
135
136
|
fs.rmSync(agentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
136
137
|
})
|
|
137
138
|
|
|
138
|
-
/** 真实 RecordStore(写文件 pi:种子经真实
|
|
139
|
+
/** 真实 RecordStore(写文件 pi:种子经真实 register 通路 + v2 条目构造器
|
|
140
|
+
* (toRegisteredEntryData,[v1 兼容层删除] v1 快照写点 toSubagentRecordEntry 已删)
|
|
141
|
+
* 落主文件)。 */
|
|
139
142
|
function makeSeedStore(): RecordStore {
|
|
140
143
|
return new RecordStore(
|
|
141
144
|
getSubagentSessionDir(agentDir, agentDir),
|
|
@@ -199,12 +202,12 @@ describe('跨包集成:subagent-core 重建 manifest → session-reader result
|
|
|
199
202
|
data: { id: m.id, agent: '/agents/worker.md', mode: 'background', task: 'cross-package task', startedAt: 1000, rootSessionId: ROOT_SESSION, depth: 0 },
|
|
200
203
|
}),
|
|
201
204
|
JSON.stringify({
|
|
205
|
+
// 登记 §3.3:主/子文件条目面 = v2 注册条(身份域;运行态事实源在 record 事件文件)。
|
|
202
206
|
type: 'custom', id: `cid-${m.id}-2`, parentId: `cid-${m.id}-1`, timestamp: ts,
|
|
203
207
|
customType: 'subagent-record',
|
|
204
208
|
data: {
|
|
205
|
-
v:
|
|
206
|
-
|
|
207
|
-
depth: 0, turns: 1, totalTokens: 10, model: m.model, eventLog: [], displayItems: [],
|
|
209
|
+
v: 2, kind: 'registered', id: m.id, agent: '/agents/worker.md', task: 'cross-package task',
|
|
210
|
+
slug: 'cross', origin: 'tool', rootSessionId: ROOT_SESSION, depth: 0, startedAt: 1000,
|
|
208
211
|
},
|
|
209
212
|
}),
|
|
210
213
|
)
|
|
@@ -216,11 +219,31 @@ describe('跨包集成:subagent-core 重建 manifest → session-reader result
|
|
|
216
219
|
* 全文 + sessionFile)——[U4/D4] 翻边后成功成员崩溃时的真实末条形态(U5 前的
|
|
217
220
|
* running+resumable 桥接形态已随字段退役消亡)。 */
|
|
218
221
|
function seedRoundTerminalEntries(m: MemberSeed, childFile: string): void {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
// 登记 §3.3:v1 快照写点已删——经写入型 pi 落 v2 条目族(注册 + 终态)。
|
|
223
|
+
const pi = makeWritingPi(mainFile)
|
|
224
|
+
const seed = (record: SubagentRecord): void => {
|
|
225
|
+
for (const entry of v2Entries(record)) pi.appendEntry(SUBAGENT_RECORD_CUSTOM_TYPE, entry)
|
|
226
|
+
}
|
|
227
|
+
seed(memberRecord({ id: m.id, sessionFile: childFile, model: m.model }))
|
|
228
|
+
seed(memberRecord({ id: m.id, sessionFile: childFile, model: m.model, status: 'idle', result: m.resultText }))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** record → v2 条目族(注册恒在场;非 running 再补终态条)。 */
|
|
232
|
+
function v2Entries(record: SubagentRecord): Array<Record<string, unknown>> {
|
|
233
|
+
const registered = {
|
|
234
|
+
v: 2, kind: 'registered', id: record.id, agent: record.agent, task: record.task, slug: record.slug,
|
|
235
|
+
origin: record.origin ?? 'tool', rootSessionId: record.rootSessionId, depth: record.depth,
|
|
236
|
+
startedAt: record.startedAt,
|
|
237
|
+
}
|
|
238
|
+
if (record.status === 'running') return [registered]
|
|
239
|
+
return [registered, {
|
|
240
|
+
v: 2, kind: 'settled', id: record.id, status: 'idle',
|
|
241
|
+
stopReason: record.stopReason ?? 'end_turn', endedAt: record.endedAt ?? record.startedAt,
|
|
242
|
+
turns: record.turns, totalTokens: record.totalTokens, model: record.model,
|
|
243
|
+
thinkingLevel: record.thinkingLevel,
|
|
244
|
+
...(record.sessionFile !== undefined ? { sessionFile: record.sessionFile } : {}),
|
|
245
|
+
...(record.result !== undefined ? { result: record.result } : {}),
|
|
246
|
+
}]
|
|
224
247
|
}
|
|
225
248
|
|
|
226
249
|
/** 全链路驱动:种子 → 恢复 service(orphan 覆写)→ rebuildIndexes(现行 boot
|
|
@@ -32,14 +32,26 @@ function recordLine(
|
|
|
32
32
|
entryId: string,
|
|
33
33
|
saId: string,
|
|
34
34
|
sessionRef: Record<string, string>,
|
|
35
|
-
v =
|
|
35
|
+
v = 2,
|
|
36
36
|
): string {
|
|
37
|
+
// 登记 §3.3:v1 全量快照形态已删——锚源 = v2 终态条(settled 携带 engine/engineHandle)。
|
|
37
38
|
return JSON.stringify({
|
|
38
39
|
type: 'custom',
|
|
39
40
|
customType: 'subagent-record',
|
|
40
41
|
id: entryId,
|
|
41
42
|
parentId: null,
|
|
42
|
-
data: {
|
|
43
|
+
data: {
|
|
44
|
+
v,
|
|
45
|
+
kind: 'settled',
|
|
46
|
+
id: saId,
|
|
47
|
+
status: 'idle',
|
|
48
|
+
stopReason: 'completed',
|
|
49
|
+
endedAt: 2,
|
|
50
|
+
turns: 1,
|
|
51
|
+
totalTokens: 1,
|
|
52
|
+
engine: 'zcode',
|
|
53
|
+
engineHandle: { sessionRef, poolKey: 'shared' },
|
|
54
|
+
},
|
|
43
55
|
})
|
|
44
56
|
}
|
|
45
57
|
|
|
@@ -208,7 +220,7 @@ describe('形状校验窄而严:缺键条目不算命中,继续找更早条
|
|
|
208
220
|
expect(await findZcodeEntryAnchor([file], 'sa-1')).toBeUndefined()
|
|
209
221
|
})
|
|
210
222
|
|
|
211
|
-
it('data.v
|
|
223
|
+
it('data.v 非当前版本(future-v,如 3)不算命中,更早 v=2 条目仍可命中', async () => {
|
|
212
224
|
const file = writeMainSession('main.jsonl', [
|
|
213
225
|
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
214
226
|
recordLine('e1', 'sa-1', { sessionId: 'sess-A', dbPath: DB_PATH }),
|
|
@@ -251,22 +263,22 @@ describe('v2 终态条锚定:kind="settled" + engineHandle.sessionRef 双键',
|
|
|
251
263
|
expect(await findZcodeEntryAnchor([file], 'sa-1')).toBeUndefined()
|
|
252
264
|
})
|
|
253
265
|
|
|
254
|
-
it('
|
|
266
|
+
it('同 id 两条 v2 终态条(早/晚)→ 末条胜(末条锚定语义)', async () => {
|
|
255
267
|
const file = writeMainSession('main.jsonl', [
|
|
256
268
|
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
257
|
-
recordLine('e1', 'sa-1', { sessionId: 'sess-
|
|
269
|
+
recordLine('e1', 'sa-1', { sessionId: 'sess-OLD', dbPath: DB_PATH }),
|
|
258
270
|
v2SettledLine('e2', 'sa-1', {
|
|
259
271
|
engine: 'zcode',
|
|
260
|
-
sessionRef: { sessionId: 'sess-
|
|
272
|
+
sessionRef: { sessionId: 'sess-NEW', dbPath: DB_PATH },
|
|
261
273
|
}),
|
|
262
274
|
])
|
|
263
275
|
|
|
264
276
|
const anchor = await findZcodeEntryAnchor([file], 'sa-1')
|
|
265
|
-
expect(anchor?.sessionId).toBe('sess-
|
|
266
|
-
expect(anchor?.sessionId).not.toBe('sess-
|
|
277
|
+
expect(anchor?.sessionId).toBe('sess-NEW')
|
|
278
|
+
expect(anchor?.sessionId).not.toBe('sess-OLD')
|
|
267
279
|
})
|
|
268
280
|
|
|
269
|
-
it('v2 终态条锚残缺(缺 engineHandle)→
|
|
281
|
+
it('v2 终态条锚残缺(缺 engineHandle)→ 不命中,更早完整条目仍可命中(缺键不遮蔽)', async () => {
|
|
270
282
|
const file = writeMainSession('main.jsonl', [
|
|
271
283
|
JSON.stringify({ type: 'session', id: 'main-1' }),
|
|
272
284
|
recordLine('e1', 'sa-1', { sessionId: 'sess-EARLY', dbPath: DB_PATH }),
|
|
@@ -114,42 +114,45 @@ async function writeRecordManifest(
|
|
|
114
114
|
await writeFile(join(recordsDir, `${id}.json`), JSON.stringify({ id, ...fields }))
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
117
|
+
/** [ADR-0095] v2 单档 fixture:写 record 流(agent-settled 帧提 calls)并向 session 文件
|
|
118
|
+
* 追加 v2 注册条目——替代已删除的 wf-state 快照 + workflow-state-link 组合。 */
|
|
119
|
+
async function writeWfV2Run(
|
|
119
120
|
dir: string,
|
|
120
|
-
|
|
121
|
+
cwdSlug: string,
|
|
122
|
+
sessionPath: string,
|
|
123
|
+
sessionId: string,
|
|
121
124
|
runId: string,
|
|
122
125
|
callSessionFiles: string[],
|
|
123
126
|
): Promise<string> {
|
|
124
|
-
const wfDir = join(dir, 'sessions',
|
|
127
|
+
const wfDir = join(dir, 'sessions', cwdSlug, 'workflow-state')
|
|
125
128
|
await mkdir(wfDir, { recursive: true })
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
})
|
|
135
|
-
await writeFile(path, snap + '\n')
|
|
136
|
-
return path
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** 向 session 文件追加 workflow-state-link custom entry。 */
|
|
140
|
-
async function writeWfLink(
|
|
141
|
-
sessionPath: string,
|
|
142
|
-
sessionId: string,
|
|
143
|
-
link: { runId: string; path: string },
|
|
144
|
-
): Promise<void> {
|
|
129
|
+
const recordPath = join(wfDir, `${runId}.record.jsonl`)
|
|
130
|
+
const lines = [
|
|
131
|
+
JSON.stringify({ type: 'run-created', seq: 1, ts: 1758000000000, runId, workflowName: 'tree-flow', argsSummary: '{}' }),
|
|
132
|
+
...callSessionFiles.map((sf, i) =>
|
|
133
|
+
JSON.stringify({ type: 'agent-settled', seq: 2 + i, ts: 1758000001000 + i, taskIndex: i, attempt: 1, outcome: 'done', durationMs: 100, result: { content: 'ok', sessionFile: sf } }),
|
|
134
|
+
),
|
|
135
|
+
]
|
|
136
|
+
await writeFile(recordPath, lines.join('\n') + '\n')
|
|
145
137
|
const line = JSON.stringify({
|
|
146
138
|
type: 'custom',
|
|
147
|
-
id: `wf-
|
|
139
|
+
id: `wf-reg-${runId}`,
|
|
148
140
|
parentId: sessionId,
|
|
149
|
-
customType: 'workflow-
|
|
150
|
-
data: {
|
|
141
|
+
customType: 'workflow-record',
|
|
142
|
+
data: {
|
|
143
|
+
v: 2,
|
|
144
|
+
kind: 'registered',
|
|
145
|
+
runId,
|
|
146
|
+
workflowName: 'tree-flow',
|
|
147
|
+
scriptName: 'tree-flow',
|
|
148
|
+
slug: 'tree',
|
|
149
|
+
startedAt: 1758000000000,
|
|
150
|
+
recordPath,
|
|
151
|
+
},
|
|
152
|
+
timestamp: '2026-09-28T00:00:00Z',
|
|
151
153
|
})
|
|
152
154
|
await writeFile(sessionPath, line + '\n', { flag: 'a' })
|
|
155
|
+
return recordPath
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
/** 在节点树里按 type+sessionId 查找节点(DFS)。 */
|
|
@@ -207,7 +210,7 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
207
210
|
status: 'completed',
|
|
208
211
|
parentRecordId: recAId,
|
|
209
212
|
})
|
|
210
|
-
// workflow-call B:A 的 sessionFile 的
|
|
213
|
+
// workflow-call B:A 的 sessionFile 的 v2 注册条目 → record 流 → calls[].sessionFile = SB
|
|
211
214
|
const sbB = join(
|
|
212
215
|
dir,
|
|
213
216
|
'subagents',
|
|
@@ -218,8 +221,7 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
218
221
|
// B call session 文件需存在(resolveWorkflows 读 call sessionFile;这里只需路径,不读内容建树)
|
|
219
222
|
await mkdir(join(dir, 'subagents', '--main-cwd--', 'sessions'), { recursive: true })
|
|
220
223
|
await writeFile(sbB, JSON.stringify({ type: 'session', id: B_REAL, cwd: '/proj/wf' }) + '\n')
|
|
221
|
-
|
|
222
|
-
await writeWfLink(saA, A_REAL, { runId: RUN_A, path: wfPath })
|
|
224
|
+
await writeWfV2Run(dir, '--main-cwd--', saA, A_REAL, RUN_A, [sbB])
|
|
223
225
|
|
|
224
226
|
const tree = await buildExecutionTree(MAIN, dir)
|
|
225
227
|
|
|
@@ -311,10 +313,8 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
311
313
|
agentName: 'explorer',
|
|
312
314
|
sessionFile: saB,
|
|
313
315
|
})
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
await writeWfLink(saA, A_REAL, { runId: RUN_A, path: wfA })
|
|
317
|
-
await writeWfLink(saB, B_REAL, { runId: RUN_B, path: wfB })
|
|
316
|
+
await writeWfV2Run(dir, '--main-cwd--', saA, A_REAL, RUN_A, [saB])
|
|
317
|
+
await writeWfV2Run(dir, '--main-cwd--', saB, B_REAL, RUN_B, [saA])
|
|
318
318
|
|
|
319
319
|
const tree = await buildExecutionTree(MAIN, dir)
|
|
320
320
|
|
|
@@ -459,9 +459,8 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
459
459
|
status: 'completed',
|
|
460
460
|
parentRecordId: recBId,
|
|
461
461
|
})
|
|
462
|
-
// A 的
|
|
463
|
-
|
|
464
|
-
await writeWfLink(saA, A_REAL, { runId: RUN_A, path: wfPath })
|
|
462
|
+
// A 的 v2 注册条目 → record 流 → calls[0].sessionFile = sbB
|
|
463
|
+
await writeWfV2Run(dir, '--main-cwd--', saA, A_REAL, RUN_A, [sbB])
|
|
465
464
|
|
|
466
465
|
const tree = await buildExecutionTree(MAIN, dir)
|
|
467
466
|
|
|
@@ -564,7 +563,7 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
564
563
|
|
|
565
564
|
it('TC-m3b-mf1-main-root:main root 填 sessionFile 后 main 自身发起的 workflow run 入树(MF-1)', async () => {
|
|
566
565
|
const mainPath = await writeMainSession(dir, '--main-cwd--', MAIN)
|
|
567
|
-
// main 的
|
|
566
|
+
// main 的 v2 注册条目 → record 流 → call session B
|
|
568
567
|
const sbB = join(
|
|
569
568
|
dir,
|
|
570
569
|
'subagents',
|
|
@@ -574,12 +573,11 @@ describe('buildExecutionTree - fixture', () => {
|
|
|
574
573
|
)
|
|
575
574
|
await mkdir(join(dir, 'subagents', '--main-cwd--', 'sessions'), { recursive: true })
|
|
576
575
|
await writeFile(sbB, JSON.stringify({ type: 'session', id: B_REAL, cwd: '/proj/wf' }) + '\n')
|
|
577
|
-
|
|
578
|
-
await writeWfLink(mainPath, MAIN, { runId: RUN_A, path: wfPath })
|
|
576
|
+
await writeWfV2Run(dir, '--main-cwd--', mainPath, MAIN, RUN_A, [sbB])
|
|
579
577
|
|
|
580
578
|
const tree = await buildExecutionTree(MAIN, dir, mainPath)
|
|
581
579
|
|
|
582
|
-
// main root 携带 sessionFile,
|
|
580
|
+
// main root 携带 sessionFile,v2 注册条目被读取 → wf-call 子节点入树
|
|
583
581
|
expect(tree.root.type).toBe('main')
|
|
584
582
|
expect(tree.root.sessionFile).toBe(mainPath)
|
|
585
583
|
const nodeB = findNode(tree.root, 'workflow-call', B_REAL)
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
// 2. 重建窗口前后,session-reader identity 投影的输入字段集(rootSessionId/slug/
|
|
8
8
|
// task/agentName/model/status/sessionFile/parentRecordId)逐字段一致——双写过渡
|
|
9
9
|
// 字段(executionStatus)在场且不被本包消费(旧 status 三态投影继续生效);
|
|
10
|
-
// 3. 重建源 =
|
|
11
|
-
//
|
|
10
|
+
// 3. 重建源 = 事件流折叠 + identity entry([③ .state 退场] 后终态位消失)——
|
|
11
|
+
// 身份/富字段逐字段保真;legacy status 不再保真 closed(重建单规则恒 idle +
|
|
12
|
+
// 无 closedReason → derivedManifestRecord 如实投影 legacy "running",终态语义
|
|
13
|
+
// 由 record-settled 事件帧承载)。
|
|
12
14
|
//
|
|
13
15
|
// 形态同 cross-package-subagent-core.test.ts:subagent-core 真实 tmpdir 产出磁盘
|
|
14
16
|
// 状态,本包零 mock 读同一 tmpdir;不触碰真实数据目录(红线)。
|
|
@@ -143,13 +145,15 @@ describe('跨包集成:subagent-core rebuildIndexes 重建 manifest → sessio
|
|
|
143
145
|
// ── session-reader 视角:重建后 identity 投影不降级 ──
|
|
144
146
|
const after = (await listRecordManifests(agentDir)).find((m) => m.id === 'sa-dw')
|
|
145
147
|
expect(after).toBeDefined()
|
|
146
|
-
|
|
148
|
+
// 字段集保真(身份/富字段逐字段一致);仅 legacy status 按 ③ 后重建语义如实
|
|
149
|
+
// 投影 running(终态权威 = 事件流 record-settled 帧,磁盘重建无 closedReason 载体)。
|
|
150
|
+
expect(identityView(after!)).toEqual({ ...viewBefore, status: 'running' })
|
|
147
151
|
|
|
148
|
-
// 重建源 = `.state` 权威:closedReason 从终态位派生回 manifest(旧三态 closed)
|
|
149
152
|
const rawAfter = JSON.parse(fs.readFileSync(path.join(recordsDir, 'sa-dw.json'), 'utf-8')) as Record<string, unknown>
|
|
150
|
-
expect(rawAfter.status).toBe('
|
|
153
|
+
expect(rawAfter.status).toBe('running')
|
|
151
154
|
expect(rawAfter.executionStatus).toBe('idle')
|
|
152
|
-
|
|
155
|
+
// closedReason 无重建载体(`.state` 退场)→ 如实缺席。
|
|
156
|
+
expect(rawAfter.closedReason).toBeUndefined()
|
|
153
157
|
})
|
|
154
158
|
|
|
155
159
|
it('惰性通道:查询面(collectRecords)补建的 manifest 同样可被本包读取投影', async () => {
|
|
@@ -103,34 +103,6 @@ async function writeAliveSubagentNoIdentity(
|
|
|
103
103
|
return path
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/** 写 wf-state 文件(每行一个快照;字符串按原样写入,可注入坏行)。返回绝对路径。 */
|
|
107
|
-
async function writeWfState(dir: string, slug: string, lines: string[]): Promise<string> {
|
|
108
|
-
const wfDir = join(dir, 'sessions', slug, 'workflow-state')
|
|
109
|
-
await mkdir(wfDir, { recursive: true })
|
|
110
|
-
const path = join(wfDir, 'wf-test.jsonl')
|
|
111
|
-
await writeFile(path, lines.join('\n') + '\n')
|
|
112
|
-
return path
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** 向主 session 文件追加 workflow-state-link custom entry(resolveWorkflows 的输入)。 */
|
|
116
|
-
async function writeWfLink(
|
|
117
|
-
dir: string,
|
|
118
|
-
slug: string,
|
|
119
|
-
id: string,
|
|
120
|
-
link: { runId: string; path: string },
|
|
121
|
-
): Promise<void> {
|
|
122
|
-
const sessionPath = join(dir, 'sessions', slug, `${id}.jsonl`)
|
|
123
|
-
const line = JSON.stringify({
|
|
124
|
-
type: 'custom',
|
|
125
|
-
id: `wf-link-${link.runId}`,
|
|
126
|
-
parentId: id,
|
|
127
|
-
customType: 'workflow-state-link',
|
|
128
|
-
data: { runId: link.runId, path: link.path, updatedAt: '2026-08-07T16:48:24.933Z' },
|
|
129
|
-
timestamp: '2026-08-07T16:48:24.933Z',
|
|
130
|
-
})
|
|
131
|
-
await writeFile(sessionPath, line + '\n', { flag: 'a' })
|
|
132
|
-
}
|
|
133
|
-
|
|
134
106
|
/** 写 records manifest(孤儿源)。U4 扩展:fields 支持富字段 task/slug/model/status。 */
|
|
135
107
|
async function writeRecordManifest(
|
|
136
108
|
dir: string,
|
|
@@ -256,14 +228,14 @@ describe('buildFamilyFromFs - fixture', () => {
|
|
|
256
228
|
await expect(buildFamilyFromFs('nonexistent-session-id', dir)).rejects.toThrow(/not found/)
|
|
257
229
|
})
|
|
258
230
|
|
|
259
|
-
it('workflows:
|
|
231
|
+
it('workflows:v2 注册条目 record 流直读提 calls;命中 pathToRef 取完整 ref,GC\'d 路径回退最小 ref', async () => {
|
|
260
232
|
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
261
233
|
// 真实存在的 subagent(步骤 2 扫到 → pathToRef 命中 → 完整 SessionRef)
|
|
262
234
|
const subPath = await writeSubagentSession(dir, '--root-cwd--', SUB_REAL, {
|
|
263
235
|
rootSessionId: ROOT,
|
|
264
236
|
slug: 'wf-sub',
|
|
265
237
|
})
|
|
266
|
-
// GC
|
|
238
|
+
// GC'd 路径(文件不存在 → pathToRef 未命中 → sessionRefFromPath 最小 ref)
|
|
267
239
|
const gced = join(
|
|
268
240
|
dir,
|
|
269
241
|
'subagents',
|
|
@@ -271,27 +243,43 @@ describe('buildFamilyFromFs - fixture', () => {
|
|
|
271
243
|
'sessions',
|
|
272
244
|
'2026-08-07T16-49-48-393Z_019fdd21-8169-7a02-8f11-eef6c9ca11cc.jsonl',
|
|
273
245
|
)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
246
|
+
// record 流:agent-settled 帧携带 result.sessionFile(两 call:命中 + GC 各一)
|
|
247
|
+
const wfDir = join(dir, 'sessions', '--root-cwd--', 'workflow-state')
|
|
248
|
+
await mkdir(wfDir, { recursive: true })
|
|
249
|
+
const recordPath = join(wfDir, 'wf-v2-rich.record.jsonl')
|
|
250
|
+
await writeFile(
|
|
251
|
+
recordPath,
|
|
252
|
+
[
|
|
253
|
+
JSON.stringify({ type: 'run-created', seq: 1, ts: 1000, runId: 'wf-v2-rich', workflowName: 'rich', argsSummary: '{}' }),
|
|
254
|
+
JSON.stringify({ type: 'agent-settled', seq: 2, ts: 1100, taskIndex: 0, attempt: 1, outcome: 'done', durationMs: 1, result: { content: 'ok', sessionFile: subPath } }),
|
|
255
|
+
JSON.stringify({ type: 'agent-settled', seq: 3, ts: 1200, taskIndex: 1, attempt: 1, outcome: 'done', durationMs: 1, result: { content: 'ok', sessionFile: gced } }),
|
|
256
|
+
].join('\n') + '\n',
|
|
257
|
+
)
|
|
258
|
+
const line = JSON.stringify({
|
|
259
|
+
type: 'custom',
|
|
260
|
+
id: 'wf-record-wf-v2-rich',
|
|
261
|
+
parentId: ROOT,
|
|
262
|
+
customType: 'workflow-record',
|
|
263
|
+
data: {
|
|
264
|
+
v: 2,
|
|
265
|
+
kind: 'registered',
|
|
266
|
+
runId: 'wf-v2-rich',
|
|
267
|
+
workflowName: 'rich',
|
|
268
|
+
scriptName: 'rich',
|
|
269
|
+
slug: 'rich',
|
|
270
|
+
startedAt: 1,
|
|
271
|
+
recordPath,
|
|
272
|
+
},
|
|
273
|
+
timestamp: '2026-08-07T16:48:24.933Z',
|
|
274
|
+
})
|
|
275
|
+
await writeFile(join(dir, 'sessions', '--root-cwd--', `${ROOT}.jsonl`), line + '\n', { flag: 'a' })
|
|
288
276
|
|
|
289
277
|
const family = await buildFamilyFromFs(ROOT, dir)
|
|
290
278
|
|
|
291
279
|
expect(family.workflows).toHaveLength(1)
|
|
292
280
|
const wf = family.workflows[0]
|
|
293
|
-
expect(wf.runId).toBe('wf-
|
|
294
|
-
expect(wf.stateFile).toBe(
|
|
281
|
+
expect(wf.runId).toBe('wf-v2-rich')
|
|
282
|
+
expect(wf.stateFile).toBe(recordPath)
|
|
295
283
|
expect(wf.calls).toHaveLength(2)
|
|
296
284
|
// 命中 pathToRef:完整 ref(真实 id / mtime / size / cwd)
|
|
297
285
|
expect(wf.calls[0].fileName).toBe(subPath)
|
|
@@ -299,7 +287,7 @@ describe('buildFamilyFromFs - fixture', () => {
|
|
|
299
287
|
expect(wf.calls[0].mtime).toBeGreaterThan(0)
|
|
300
288
|
expect(wf.calls[0].sizeBytes).toBeGreaterThan(0)
|
|
301
289
|
expect(wf.calls[0].cwd).toBe('/proj/--root-cwd--')
|
|
302
|
-
// GC
|
|
290
|
+
// GC'd 未命中:fileName-only 最小 ref(sessionId 从文件名提取,mtime/size/cwd 占位)
|
|
303
291
|
expect(wf.calls[1].fileName).toBe(gced)
|
|
304
292
|
expect(wf.calls[1].sessionId).toBe('019fdd21-8169-7a02-8f11-eef6c9ca11cc')
|
|
305
293
|
expect(wf.calls[1].mtime).toBe(0)
|
|
@@ -307,83 +295,6 @@ describe('buildFamilyFromFs - fixture', () => {
|
|
|
307
295
|
expect(wf.calls[1].cwd).toBe('')
|
|
308
296
|
})
|
|
309
297
|
|
|
310
|
-
it('workflows:NEW 格式坏尾行回退上一快照;顶层 sessionFile 优先于 result.sessionFile', async () => {
|
|
311
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
312
|
-
const topLevel = join(
|
|
313
|
-
dir,
|
|
314
|
-
'subagents',
|
|
315
|
-
'--root-cwd--',
|
|
316
|
-
'sessions',
|
|
317
|
-
'2026-08-01T00-00-00-000Z_019fdd11-1111-1111-1111-111111111111.jsonl',
|
|
318
|
-
)
|
|
319
|
-
const inResult = join(
|
|
320
|
-
dir,
|
|
321
|
-
'subagents',
|
|
322
|
-
'--root-cwd--',
|
|
323
|
-
'sessions',
|
|
324
|
-
'2026-08-02T00-00-00-000Z_019fdd22-2222-2222-2222-222222222222.jsonl',
|
|
325
|
-
)
|
|
326
|
-
const snap = JSON.stringify({
|
|
327
|
-
v: 'wf-run-v1',
|
|
328
|
-
runId: 'wf-x',
|
|
329
|
-
state: { calls: [{ id: 0, sessionFile: topLevel, result: { sessionFile: inResult } }] },
|
|
330
|
-
})
|
|
331
|
-
// 尾行坏 JSON → readWorkflowCallSessionFiles 从尾向头回退到上一有效快照
|
|
332
|
-
const wfPath = await writeWfState(dir, '--root-cwd--', [snap, '{broken json'])
|
|
333
|
-
await writeWfLink(dir, '--root-cwd--', ROOT, { runId: 'wf-x', path: wfPath })
|
|
334
|
-
|
|
335
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
336
|
-
|
|
337
|
-
expect(family.workflows).toHaveLength(1)
|
|
338
|
-
expect(family.workflows[0].calls).toHaveLength(1)
|
|
339
|
-
// 顶层 sessionFile 优先(result.sessionFile 不覆盖)
|
|
340
|
-
expect(family.workflows[0].calls[0].fileName).toBe(topLevel)
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
it('workflows:OLD 格式(无 v)callCache [{key,value}] → value.sessionFile + value.result.sessionFile 回退;无 sessionFile 的 call 不产出', async () => {
|
|
344
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
345
|
-
const viaValue = join(
|
|
346
|
-
dir,
|
|
347
|
-
'subagents',
|
|
348
|
-
'--root-cwd--',
|
|
349
|
-
'sessions',
|
|
350
|
-
'2026-08-03T00-00-00-000Z_019fdd33-3333-3333-3333-333333333333.jsonl',
|
|
351
|
-
)
|
|
352
|
-
const viaResult = join(
|
|
353
|
-
dir,
|
|
354
|
-
'subagents',
|
|
355
|
-
'--root-cwd--',
|
|
356
|
-
'sessions',
|
|
357
|
-
'2026-08-04T00-00-00-000Z_019fdd44-4444-4444-4444-444444444444.jsonl',
|
|
358
|
-
)
|
|
359
|
-
const wfPath = await writeWfState(dir, '--root-cwd--', [
|
|
360
|
-
JSON.stringify({
|
|
361
|
-
runId: 'wf-old-1',
|
|
362
|
-
name: 'old-wf',
|
|
363
|
-
status: 'done',
|
|
364
|
-
callCache: [
|
|
365
|
-
// 真实 OLD 数据形态:value 无 sessionFile(旧 pi 不持久化)→ 不产出
|
|
366
|
-
{ key: 1, value: { content: 'PASS', durationMs: 100 } },
|
|
367
|
-
// value.sessionFile(源码注释 OLD 分支读取点)
|
|
368
|
-
{ key: 2, value: { sessionFile: viaValue, content: 'ok' } },
|
|
369
|
-
// value.result.sessionFile 回退
|
|
370
|
-
{ key: 3, value: { result: { sessionFile: viaResult, durationMs: 1 } } },
|
|
371
|
-
// value 非对象 → 整项兜底(无 sessionFile → 不产出)
|
|
372
|
-
{ key: 4, value: 'str' },
|
|
373
|
-
],
|
|
374
|
-
}),
|
|
375
|
-
])
|
|
376
|
-
await writeWfLink(dir, '--root-cwd--', ROOT, { runId: 'wf-old-1', path: wfPath })
|
|
377
|
-
|
|
378
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
379
|
-
|
|
380
|
-
expect(family.workflows).toHaveLength(1)
|
|
381
|
-
expect(family.workflows[0].calls.map((c) => c.fileName)).toEqual([viaValue, viaResult])
|
|
382
|
-
// 未命中 pathToRef → 最小 ref:sessionId 从文件名提取,mtime 占位 0
|
|
383
|
-
expect(family.workflows[0].calls[0].sessionId).toBe('019fdd33-3333-3333-3333-333333333333')
|
|
384
|
-
expect(family.workflows[0].calls[0].mtime).toBe(0)
|
|
385
|
-
})
|
|
386
|
-
|
|
387
298
|
it('MF-3 回归:alive 但无 identity 的 subagent(运行中)不被收编为 cleanedUp——U4 后 manifest 主路径建族', async () => {
|
|
388
299
|
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
389
300
|
// 运行中 subagent:有效 header、无 identity 尾行(identity 完成时才写入),manifest 已存在
|
|
@@ -416,66 +327,6 @@ describe('buildFamilyFromFs - fixture', () => {
|
|
|
416
327
|
})
|
|
417
328
|
})
|
|
418
329
|
|
|
419
|
-
// ============================================================
|
|
420
|
-
// family workflows 富 run(多 call)契约(fixture)
|
|
421
|
-
// ============================================================
|
|
422
|
-
|
|
423
|
-
describe('buildFamilyFromFs - workflow 富 run(多 call)', () => {
|
|
424
|
-
let dir: string
|
|
425
|
-
|
|
426
|
-
beforeEach(async () => {
|
|
427
|
-
dir = await makeAgentDir()
|
|
428
|
-
})
|
|
429
|
-
afterEach(async () => {
|
|
430
|
-
await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
it('workflows 非空,多 call(4 个)workflow 的 fileName/stateFile/runId 契约', async () => {
|
|
434
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
435
|
-
// 1 个存活 subagent(pathToRef 命中 → 完整 ref)+ 3 个 GC 路径(未命中 → 最小 ref,
|
|
436
|
-
// fileName 仍非空——calls 的 sessionFile 路径恒落入 fileName)
|
|
437
|
-
const subPath = await writeSubagentSession(dir, '--root-cwd--', SUB_REAL, {
|
|
438
|
-
rootSessionId: ROOT,
|
|
439
|
-
slug: 'wf-rich',
|
|
440
|
-
})
|
|
441
|
-
const ghost = (n: number): string =>
|
|
442
|
-
join(
|
|
443
|
-
dir,
|
|
444
|
-
'subagents',
|
|
445
|
-
'--root-cwd--',
|
|
446
|
-
'sessions',
|
|
447
|
-
`2026-08-0${n}T00-00-00-000Z_019fdd55-1111-1111-1111-11111111111${n}.jsonl`,
|
|
448
|
-
)
|
|
449
|
-
const wfPath = await writeWfState(dir, '--root-cwd--', [
|
|
450
|
-
JSON.stringify({
|
|
451
|
-
v: 'wf-run-v1',
|
|
452
|
-
runId: 'wf-rich-run-1',
|
|
453
|
-
state: {
|
|
454
|
-
status: 'done',
|
|
455
|
-
calls: [
|
|
456
|
-
{ id: 0, status: 'done', sessionFile: subPath, sessionId: SUB_REAL },
|
|
457
|
-
{ id: 1, status: 'done', sessionFile: ghost(1) },
|
|
458
|
-
{ id: 2, status: 'done', sessionFile: ghost(2) },
|
|
459
|
-
{ id: 3, status: 'done', sessionFile: ghost(3) },
|
|
460
|
-
],
|
|
461
|
-
},
|
|
462
|
-
}),
|
|
463
|
-
])
|
|
464
|
-
await writeWfLink(dir, '--root-cwd--', ROOT, { runId: 'wf-rich-run-1', path: wfPath })
|
|
465
|
-
|
|
466
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
467
|
-
|
|
468
|
-
expect(family.workflows.length).toBeGreaterThan(0)
|
|
469
|
-
const rich = family.workflows.find((w) => w.calls.length >= 4)
|
|
470
|
-
expect(rich).toBeDefined()
|
|
471
|
-
// calls 的 sessionFile 路径已落入 fileName(sessionRefFromPath)
|
|
472
|
-
expect(rich!.calls.every((c) => c.fileName.length > 0)).toBe(true)
|
|
473
|
-
// stateFile 是 wf-state 文件绝对路径
|
|
474
|
-
expect(rich!.stateFile.endsWith('.jsonl')).toBe(true)
|
|
475
|
-
expect(rich!.runId.startsWith('wf-')).toBe(true)
|
|
476
|
-
})
|
|
477
|
-
})
|
|
478
|
-
|
|
479
330
|
// ============================================================
|
|
480
331
|
// w2 TC1:listRecordManifests + RecordManifest 导出(IF2/DM2,行为零变更验证)
|
|
481
332
|
// ============================================================
|
|
@@ -662,7 +513,7 @@ describe('U4 buildFamilyFromFs 富化(manifest 主 / P-fallback)', () => {
|
|
|
662
513
|
// ① 结构性忽略——subagents 树扫描只收 .jsonl,.events 天然不进候选集(D3「无后缀
|
|
663
514
|
// 的结构性收益」:被忽略或被误读两类风险一次排空);
|
|
664
515
|
// ② 首行头行读者兼容——.events 首行是自描述头行 {"type":"record-journal","id":...}
|
|
665
|
-
// (写侧 record-events.ts
|
|
516
|
+
// (写侧 record-events.ts toRecordEventHeader 契约),session-reader 对未知文件
|
|
666
517
|
// 读首行判 header 时命中非 session header 即忽略(检查点④:零成本兼容——不改
|
|
667
518
|
// 扫描器即可与该文件族共存)。
|
|
668
519
|
|