@zhushanwen/pi-session-reader 0.13.0 → 0.13.2
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 +5 -5
- package/src/__tests__/entry-anchor.test.ts +704 -324
- package/src/__tests__/family.test.ts +588 -312
- package/src/__tests__/index.test.ts +17 -1
- package/src/__tests__/{cross-package-subagent-core.test.ts → path-encoding.test.ts} +194 -30
- package/src/__tests__/roots-merged.test.ts +4919 -0
- package/src/__tests__/roots.test.ts +1986 -432
- package/src/__tests__/tool-handler.test.ts +872 -3216
- package/src/__tests__/tree.test.ts +738 -101
- package/src/tui/tool-render.ts +3 -28
- package/src/__tests__/archive-ledger.test.ts +0 -447
- package/src/__tests__/execution-tree.test.ts +0 -898
- package/src/__tests__/family-zcode.test.ts +0 -265
- package/src/__tests__/find.test.ts +0 -802
- package/src/__tests__/rebuild-manifest-dual-write.test.ts +0 -188
- package/src/__tests__/render.test.ts +0 -481
- package/src/__tests__/result.test.ts +0 -347
- package/src/__tests__/subagents.test.ts +0 -668
- package/src/__tests__/summary.test.ts +0 -223
- package/src/__tests__/turns.test.ts +0 -144
- package/src/__tests__/zcode-manifest.test.ts +0 -372
- package/src/__tests__/zcode-routing.test.ts +0 -772
|
@@ -1,668 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
-
import { tmpdir } from 'node:os'
|
|
3
|
-
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import {
|
|
6
|
-
buildFamilyFromFs,
|
|
7
|
-
listRecordManifests,
|
|
8
|
-
extractSessionIdFromFilename,
|
|
9
|
-
type RecordManifest,
|
|
10
|
-
} from '../discovery/subagents.js'
|
|
11
|
-
import {
|
|
12
|
-
resolveSessionRoots,
|
|
13
|
-
resolveStoreSubagentSessionsDir,
|
|
14
|
-
resolveStoreSubagentRecordsDir,
|
|
15
|
-
} from '../discovery/roots.js'
|
|
16
|
-
import { parseSessionHeader, readSessionHeaderFirstLine } from '../discovery/session-header.js'
|
|
17
|
-
|
|
18
|
-
// ---- fixture 常量(uuid 特征,满足 extractSessionIdFromFilename + 互不为子串)----
|
|
19
|
-
const ROOT = '0aaaaaaa-bbbb-7ccc-dddd-000000000001'
|
|
20
|
-
const FORK = '0aaaaaaa-bbbb-7ccc-dddd-000000000002'
|
|
21
|
-
const SUB_REAL = '0aaaaaaa-bbbb-7ccc-dddd-000000000003'
|
|
22
|
-
|
|
23
|
-
// ---- fixture helpers ----
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* 建 mkdtemp dataDir 根(生产拓扑复刻,[session-store-layout-inversion] 布局 B):
|
|
27
|
-
* agentDir = `<dataDir>/agent`(调用方经 agentDirOf 推导并传入被测函数),存储树 =
|
|
28
|
-
* `<dataDir>/ext-sessions`(fixture 经 roots.ts 生产同源推导函数落盘,禁手拼树根段字面量)。
|
|
29
|
-
*/
|
|
30
|
-
async function makeAgentDir(): Promise<string> {
|
|
31
|
-
return mkdtemp(join(tmpdir(), 'subagents-test-'))
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** agentDir 推导(生产拓扑:agentDir = <dataDir>/agent,session-reader 的 agentDir 语义)。 */
|
|
35
|
-
function agentDirOf(dataDir: string): string {
|
|
36
|
-
return join(dataDir, 'agent')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** 写主 session 文件(首行 header)。返回绝对路径(供 fork 的 parentSession 指向)。 */
|
|
40
|
-
async function writeMainSession(
|
|
41
|
-
dir: string,
|
|
42
|
-
slug: string,
|
|
43
|
-
id: string,
|
|
44
|
-
opts?: { cwd?: string; parentSession?: string },
|
|
45
|
-
): Promise<string> {
|
|
46
|
-
const sessionDir = join(dir, 'sessions', slug)
|
|
47
|
-
await mkdir(sessionDir, { recursive: true })
|
|
48
|
-
const path = join(sessionDir, `${id}.jsonl`)
|
|
49
|
-
const header: Record<string, unknown> = { type: 'session', id, cwd: opts?.cwd ?? `/proj/${slug}` }
|
|
50
|
-
if (opts?.parentSession) header.parentSession = opts.parentSession
|
|
51
|
-
await writeFile(path, JSON.stringify(header) + '\n')
|
|
52
|
-
return path
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 写 subagent session 文件(新布局存储树:`<root>/<rootSessionId>/subagents/sessions/`,
|
|
57
|
-
* 分区键 = identity.rootSessionId,生产同源 resolveStoreSubagentSessionsDir 推导):
|
|
58
|
-
* 首行 header(真实 id)+ 占位 message + 尾行 identity。
|
|
59
|
-
* identity 在尾行(实测 pi 行为;subagent-identity 由 session-runner 在 session 创建后写)。
|
|
60
|
-
* U4 扩展:identity.task/agent 可定制(P-fallback 测试用,默认 't'/'explorer')。
|
|
61
|
-
*/
|
|
62
|
-
async function writeSubagentSession(
|
|
63
|
-
dir: string,
|
|
64
|
-
realId: string,
|
|
65
|
-
identity: { rootSessionId: string; slug: string; dataId?: string; task?: string; agent?: string },
|
|
66
|
-
): Promise<string> {
|
|
67
|
-
const sessionDir = resolveStoreSubagentSessionsDir(dir, identity.rootSessionId)
|
|
68
|
-
await mkdir(sessionDir, { recursive: true })
|
|
69
|
-
const path = join(sessionDir, `${realId}.jsonl`)
|
|
70
|
-
const lines = [
|
|
71
|
-
JSON.stringify({ type: 'session', id: realId, cwd: `/proj/${identity.slug}` }),
|
|
72
|
-
JSON.stringify({
|
|
73
|
-
type: 'message',
|
|
74
|
-
id: 'm1',
|
|
75
|
-
parentId: realId,
|
|
76
|
-
message: { role: 'user', content: 'do work' },
|
|
77
|
-
}),
|
|
78
|
-
JSON.stringify({
|
|
79
|
-
type: 'custom',
|
|
80
|
-
customType: 'subagent-identity',
|
|
81
|
-
data: {
|
|
82
|
-
id: identity.dataId ?? `sa-${realId.slice(0, 8)}`,
|
|
83
|
-
rootSessionId: identity.rootSessionId,
|
|
84
|
-
slug: identity.slug,
|
|
85
|
-
agent: identity.agent ?? 'explorer',
|
|
86
|
-
mode: 'sync',
|
|
87
|
-
task: identity.task ?? 't',
|
|
88
|
-
startedAt: 1,
|
|
89
|
-
},
|
|
90
|
-
}),
|
|
91
|
-
]
|
|
92
|
-
await writeFile(path, lines.join('\n') + '\n')
|
|
93
|
-
return path
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 写 alive subagent 文件但**无 identity 尾行**(运行中/异常,尾行是 message)。
|
|
98
|
-
* TC-u4-pfallback-no-identity 场景:header 有效、无 manifest、无 identity → buildFamilyFromFs 跳过。
|
|
99
|
-
*/
|
|
100
|
-
async function writeAliveSubagentNoIdentity(
|
|
101
|
-
dir: string,
|
|
102
|
-
rootSessionId: string,
|
|
103
|
-
realId: string,
|
|
104
|
-
): Promise<string> {
|
|
105
|
-
const sessionDir = resolveStoreSubagentSessionsDir(dir, rootSessionId)
|
|
106
|
-
await mkdir(sessionDir, { recursive: true })
|
|
107
|
-
const path = join(sessionDir, `${realId}.jsonl`)
|
|
108
|
-
const lines = [
|
|
109
|
-
JSON.stringify({ type: 'session', id: realId, cwd: '/proj/root' }),
|
|
110
|
-
JSON.stringify({
|
|
111
|
-
type: 'message',
|
|
112
|
-
id: 'm1',
|
|
113
|
-
parentId: realId,
|
|
114
|
-
message: { role: 'user', content: 'still running' },
|
|
115
|
-
}),
|
|
116
|
-
]
|
|
117
|
-
await writeFile(path, lines.join('\n') + '\n')
|
|
118
|
-
return path
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* 写 records manifest(孤儿源;新布局 `<root>/<rootSessionId>/subagents/records/`,
|
|
123
|
-
* 分区键 = fields.rootSessionId,生产同源 resolveStoreSubagentRecordsDir 推导)。
|
|
124
|
-
* U4 扩展:fields 支持富字段 task/slug/model/status。
|
|
125
|
-
*/
|
|
126
|
-
async function writeRecordManifest(
|
|
127
|
-
dir: string,
|
|
128
|
-
id: string,
|
|
129
|
-
fields: {
|
|
130
|
-
rootSessionId: string
|
|
131
|
-
agentName?: string
|
|
132
|
-
sessionFile: string
|
|
133
|
-
task?: string
|
|
134
|
-
slug?: string
|
|
135
|
-
model?: string
|
|
136
|
-
status?: string
|
|
137
|
-
},
|
|
138
|
-
): Promise<void> {
|
|
139
|
-
const recordsDir = resolveStoreSubagentRecordsDir(dir, fields.rootSessionId)
|
|
140
|
-
await mkdir(recordsDir, { recursive: true })
|
|
141
|
-
await writeFile(join(recordsDir, `${id}.json`), JSON.stringify({ id, ...fields }))
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ============================================================
|
|
145
|
-
// fixture 测试
|
|
146
|
-
// ============================================================
|
|
147
|
-
|
|
148
|
-
describe('buildFamilyFromFs - fixture', () => {
|
|
149
|
-
let dataDir: string
|
|
150
|
-
let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻,见 makeAgentDir 注释)
|
|
151
|
-
|
|
152
|
-
beforeEach(async () => {
|
|
153
|
-
dataDir = await makeAgentDir()
|
|
154
|
-
dir = agentDirOf(dataDir)
|
|
155
|
-
})
|
|
156
|
-
afterEach(async () => {
|
|
157
|
-
await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it('基础 family:root + fork + 隔代 subagent,SubagentRef.sessionId 是真实 id(非 sa-xxx)', async () => {
|
|
161
|
-
const rootPath = await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
162
|
-
// fork 在不同 cwd slug(模拟跨 cwd fork)
|
|
163
|
-
await writeMainSession(dir, '--fork-cwd--', FORK, {
|
|
164
|
-
cwd: '/proj/fork',
|
|
165
|
-
parentSession: rootPath,
|
|
166
|
-
})
|
|
167
|
-
// subagent 挂在 fork 子代下(rootSessionId=FORK,非 ROOT)→ 隔代
|
|
168
|
-
await writeSubagentSession(dir, SUB_REAL, {
|
|
169
|
-
rootSessionId: FORK,
|
|
170
|
-
slug: 'test-sub',
|
|
171
|
-
dataId: 'sa-placeholder-1',
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
175
|
-
|
|
176
|
-
expect(family.root.sessionId).toBe(ROOT)
|
|
177
|
-
// fork:parentSession 含 ROOT id → childrenOf[ROOT] = [FORK]
|
|
178
|
-
expect(family.forks.some((f) => f.sessionId === FORK)).toBe(true)
|
|
179
|
-
// Q1 隔代:subagent rootSessionId=FORK(fork 子代),从 ROOT resolve 能关联
|
|
180
|
-
const sub = family.subagents.find((s) => s.sessionId === SUB_REAL)
|
|
181
|
-
expect(sub).toBeDefined()
|
|
182
|
-
// id 修正核心断言:sessionId 是 subagent 文件首行 header.id(真实),非 identity.data.id 的 sa-xxx
|
|
183
|
-
expect(sub!.sessionId).toBe(SUB_REAL)
|
|
184
|
-
expect(sub!.sessionId.startsWith('sa-')).toBe(false)
|
|
185
|
-
expect(sub!.rootSessionId).toBe(FORK)
|
|
186
|
-
expect(sub!.slug).toBe('test-sub')
|
|
187
|
-
expect(sub!.cleanedUp).toBe(false)
|
|
188
|
-
// enrichRefs 已删除(ext-simplify-04 U4/E2):family 路径 fileName/cwd 维持 core 占位空串
|
|
189
|
-
//(SessionRef 与 find 路径共享,find 侧有真实消费者;workflow calls 反查走 pathToRef 不受影响)
|
|
190
|
-
expect(sub!.fileName).toBe('')
|
|
191
|
-
expect(sub!.cwd).toBe('')
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it('从 fork 子代 resolve 也能关联到挂在其下的 subagent', async () => {
|
|
195
|
-
const rootPath = await writeMainSession(dir, '--root-cwd--', ROOT)
|
|
196
|
-
await writeMainSession(dir, '--fork-cwd--', FORK, { parentSession: rootPath })
|
|
197
|
-
await writeSubagentSession(dir, SUB_REAL, { rootSessionId: FORK, slug: 's' })
|
|
198
|
-
|
|
199
|
-
const family = await buildFamilyFromFs(FORK, dir)
|
|
200
|
-
expect(family.subagents.some((s) => s.sessionId === SUB_REAL)).toBe(true)
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
it('cleanedUp:manifest 孤儿(.jsonl 不存在)→ cleanedUp=true', async () => {
|
|
204
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
205
|
-
// 孤儿 manifest:rootSessionId=ROOT,sessionFile 指向不存在路径(模拟 .jsonl 被 GC)
|
|
206
|
-
await writeRecordManifest(dir, 'sa-ghost-id', {
|
|
207
|
-
rootSessionId: ROOT,
|
|
208
|
-
agentName: 'explorer',
|
|
209
|
-
sessionFile: '/nonexistent/ghost.jsonl',
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
213
|
-
|
|
214
|
-
const ghost = family.subagents.find((s) => s.sessionId === 'sa-ghost-id')
|
|
215
|
-
expect(ghost).toBeDefined()
|
|
216
|
-
expect(ghost!.cleanedUp).toBe(true)
|
|
217
|
-
expect(ghost!.rootSessionId).toBe(ROOT)
|
|
218
|
-
// 孤儿无文件 → mtime/size 占位 0
|
|
219
|
-
expect(ghost!.mtime).toBe(0)
|
|
220
|
-
expect(ghost!.sizeBytes).toBe(0)
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
it('alive subagent 同时有 manifest → 不重复计数(manifest 跳过 alive)', async () => {
|
|
224
|
-
const rootPath = await writeMainSession(dir, '--root-cwd--', ROOT)
|
|
225
|
-
await writeMainSession(dir, '--fork-cwd--', FORK, { parentSession: rootPath })
|
|
226
|
-
const subPath = await writeSubagentSession(dir, SUB_REAL, {
|
|
227
|
-
rootSessionId: FORK,
|
|
228
|
-
slug: 'dup-test',
|
|
229
|
-
})
|
|
230
|
-
// manifest 指向真实文件路径(alive)→ 应被跳过,不产生孤儿副本
|
|
231
|
-
await writeRecordManifest(dir, `sa-${SUB_REAL}`, {
|
|
232
|
-
rootSessionId: FORK,
|
|
233
|
-
agentName: 'explorer',
|
|
234
|
-
sessionFile: subPath,
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
238
|
-
// 只有一个 SUB_REAL(真实 id),无 sa- 副本
|
|
239
|
-
const realOnes = family.subagents.filter((s) => s.sessionId === SUB_REAL)
|
|
240
|
-
expect(realOnes).toHaveLength(1)
|
|
241
|
-
expect(realOnes[0].cleanedUp).toBe(false)
|
|
242
|
-
const orphans = family.subagents.filter((s) => s.sessionId.startsWith('sa-'))
|
|
243
|
-
expect(orphans).toHaveLength(0)
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
it('sessionId 不在任意 main header → 抛 Error', async () => {
|
|
247
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
248
|
-
await expect(buildFamilyFromFs('nonexistent-session-id', dir)).rejects.toThrow(/not found/)
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
it('workflows:v2 注册条目 record 流直读提 calls;命中 pathToRef 取完整 ref,GC\'d 路径回退最小 ref', async () => {
|
|
252
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
253
|
-
// 真实存在的 subagent(步骤 2 扫到 → pathToRef 命中 → 完整 SessionRef)
|
|
254
|
-
const subPath = await writeSubagentSession(dir, SUB_REAL, {
|
|
255
|
-
rootSessionId: ROOT,
|
|
256
|
-
slug: 'wf-sub',
|
|
257
|
-
})
|
|
258
|
-
// GC'd 路径(文件不存在 → pathToRef 未命中 → sessionRefFromPath 最小 ref);
|
|
259
|
-
// 新布局 subagent 会话路径形态(存储树分区,生产同源推导)
|
|
260
|
-
const gced = join(
|
|
261
|
-
resolveStoreSubagentSessionsDir(dir, ROOT),
|
|
262
|
-
'2026-08-07T16-49-48-393Z_019fdd21-8169-7a02-8f11-eef6c9ca11cc.jsonl',
|
|
263
|
-
)
|
|
264
|
-
// record 流:agent-settled 帧携带 result.sessionFile(两 call:命中 + GC 各一)
|
|
265
|
-
const wfDir = join(dir, 'sessions', '--root-cwd--', 'workflow-state')
|
|
266
|
-
await mkdir(wfDir, { recursive: true })
|
|
267
|
-
const recordPath = join(wfDir, 'wf-v2-rich.record.jsonl')
|
|
268
|
-
await writeFile(
|
|
269
|
-
recordPath,
|
|
270
|
-
[
|
|
271
|
-
JSON.stringify({ type: 'run-created', seq: 1, ts: 1000, runId: 'wf-v2-rich', workflowName: 'rich', argsSummary: '{}' }),
|
|
272
|
-
JSON.stringify({ type: 'agent-settled', seq: 2, ts: 1100, taskIndex: 0, attempt: 1, outcome: 'done', durationMs: 1, result: { content: 'ok', sessionFile: subPath } }),
|
|
273
|
-
JSON.stringify({ type: 'agent-settled', seq: 3, ts: 1200, taskIndex: 1, attempt: 1, outcome: 'done', durationMs: 1, result: { content: 'ok', sessionFile: gced } }),
|
|
274
|
-
].join('\n') + '\n',
|
|
275
|
-
)
|
|
276
|
-
const line = JSON.stringify({
|
|
277
|
-
type: 'custom',
|
|
278
|
-
id: 'wf-record-wf-v2-rich',
|
|
279
|
-
parentId: ROOT,
|
|
280
|
-
customType: 'workflow-record',
|
|
281
|
-
data: {
|
|
282
|
-
v: 2,
|
|
283
|
-
kind: 'registered',
|
|
284
|
-
runId: 'wf-v2-rich',
|
|
285
|
-
workflowName: 'rich',
|
|
286
|
-
scriptName: 'rich',
|
|
287
|
-
slug: 'rich',
|
|
288
|
-
startedAt: 1,
|
|
289
|
-
recordPath,
|
|
290
|
-
},
|
|
291
|
-
timestamp: '2026-08-07T16:48:24.933Z',
|
|
292
|
-
})
|
|
293
|
-
await writeFile(join(dir, 'sessions', '--root-cwd--', `${ROOT}.jsonl`), line + '\n', { flag: 'a' })
|
|
294
|
-
|
|
295
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
296
|
-
|
|
297
|
-
expect(family.workflows).toHaveLength(1)
|
|
298
|
-
const wf = family.workflows[0]
|
|
299
|
-
expect(wf.runId).toBe('wf-v2-rich')
|
|
300
|
-
expect(wf.stateFile).toBe(recordPath)
|
|
301
|
-
expect(wf.calls).toHaveLength(2)
|
|
302
|
-
// 命中 pathToRef:完整 ref(真实 id / mtime / size / cwd)
|
|
303
|
-
expect(wf.calls[0].fileName).toBe(subPath)
|
|
304
|
-
expect(wf.calls[0].sessionId).toBe(SUB_REAL)
|
|
305
|
-
expect(wf.calls[0].mtime).toBeGreaterThan(0)
|
|
306
|
-
expect(wf.calls[0].sizeBytes).toBeGreaterThan(0)
|
|
307
|
-
expect(wf.calls[0].cwd).toBe('/proj/wf-sub') // cwd 从 subagent 文件首行 header 透传(fixture header.cwd = /proj/<identity.slug>)
|
|
308
|
-
// GC'd 未命中:fileName-only 最小 ref(sessionId 从文件名提取,mtime/size/cwd 占位)
|
|
309
|
-
expect(wf.calls[1].fileName).toBe(gced)
|
|
310
|
-
expect(wf.calls[1].sessionId).toBe('019fdd21-8169-7a02-8f11-eef6c9ca11cc')
|
|
311
|
-
expect(wf.calls[1].mtime).toBe(0)
|
|
312
|
-
expect(wf.calls[1].sizeBytes).toBe(0)
|
|
313
|
-
expect(wf.calls[1].cwd).toBe('')
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
it('MF-3 回归:alive 但无 identity 的 subagent(运行中)不被收编为 cleanedUp——U4 后 manifest 主路径建族', async () => {
|
|
317
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
318
|
-
// 运行中 subagent:有效 header、无 identity 尾行(identity 完成时才写入),manifest 已存在
|
|
319
|
-
const subDir = resolveStoreSubagentSessionsDir(dir, ROOT)
|
|
320
|
-
await mkdir(subDir, { recursive: true })
|
|
321
|
-
const subPath = join(subDir, SUB_REAL + '.jsonl')
|
|
322
|
-
await writeFile(
|
|
323
|
-
subPath,
|
|
324
|
-
JSON.stringify({ type: 'session', id: SUB_REAL, cwd: '/proj/root' }) + '\n',
|
|
325
|
-
)
|
|
326
|
-
await writeRecordManifest(dir, `sa-${SUB_REAL}`, {
|
|
327
|
-
rootSessionId: ROOT,
|
|
328
|
-
agentName: 'explorer',
|
|
329
|
-
sessionFile: subPath,
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
333
|
-
// U4 语义变化(TC-manifest-source):m0 时 alive 无 identity → 完全丢弃(无数据源建族)。
|
|
334
|
-
// U4 manifest 主路径:manifest 命中即建族(manifest 有 rootSessionId),不再依赖 identity。
|
|
335
|
-
// MF-3 核心保护仍生效:不被收编为 cleanedUp(fileStats 有 realId → cleanedUp=false)。
|
|
336
|
-
const sub = family.subagents.find((s) => s.sessionId === SUB_REAL)
|
|
337
|
-
expect(sub).toBeDefined()
|
|
338
|
-
expect(sub!.cleanedUp).toBe(false) // 核心:运行中不被当孤儿
|
|
339
|
-
expect(sub!.rootSessionId).toBe(ROOT) // manifest 主:rootSessionId 从 manifest 透传
|
|
340
|
-
expect(sub!.agentName).toBe('explorer') // manifest.agentName → agentName
|
|
341
|
-
// 无 identity 尾行 → task/model/status 取决于 manifest(本 fixture manifest 未写这些 → undefined)
|
|
342
|
-
expect(sub!.task).toBeUndefined()
|
|
343
|
-
expect(sub!.model).toBeUndefined()
|
|
344
|
-
expect(family.subagents.every((s) => s.cleanedUp === false)).toBe(true)
|
|
345
|
-
})
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
// ============================================================
|
|
349
|
-
// w2 TC1:listRecordManifests + RecordManifest 导出(IF2/DM2,行为零变更验证)
|
|
350
|
-
// ============================================================
|
|
351
|
-
|
|
352
|
-
describe('listRecordManifests 导出(w2 TC1)', () => {
|
|
353
|
-
let dataDir: string
|
|
354
|
-
let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻,见 makeAgentDir 注释)
|
|
355
|
-
|
|
356
|
-
beforeEach(async () => {
|
|
357
|
-
dataDir = await makeAgentDir()
|
|
358
|
-
dir = agentDirOf(dataDir)
|
|
359
|
-
})
|
|
360
|
-
afterEach(async () => {
|
|
361
|
-
await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
it('导出生效:import + 调用返回 RecordManifest[],字段完整', async () => {
|
|
365
|
-
await writeRecordManifest(dir, 'sa-tc1', {
|
|
366
|
-
rootSessionId: 'root-1',
|
|
367
|
-
agentName: 'explorer',
|
|
368
|
-
sessionFile: '/tmp/sa-tc1.jsonl',
|
|
369
|
-
})
|
|
370
|
-
const manifests: RecordManifest[] = await listRecordManifests(dir)
|
|
371
|
-
expect(manifests).toHaveLength(1)
|
|
372
|
-
const m = manifests[0]
|
|
373
|
-
expect(m.id).toBe('sa-tc1')
|
|
374
|
-
expect(m.rootSessionId).toBe('root-1')
|
|
375
|
-
expect(m.agentName).toBe('explorer')
|
|
376
|
-
expect(m.sessionFile).toBe('/tmp/sa-tc1.jsonl')
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
it('D6 单新根锚定:旧布局 <agentDir>/subagents 造 record + 新根不造 → 不返回(存量不可见,防回归双根扫描)', async () => {
|
|
380
|
-
// 旧布局存量原位保留(D6 裁决:不迁移、不再读写)——旧根造一份合法 manifest
|
|
381
|
-
const legacyRecordsDir = join(dir, 'subagents', '--legacy-cwd--', 'records')
|
|
382
|
-
await mkdir(legacyRecordsDir, { recursive: true })
|
|
383
|
-
await writeFile(
|
|
384
|
-
join(legacyRecordsDir, 'sa-legacy.json'),
|
|
385
|
-
JSON.stringify({ id: 'sa-legacy', rootSessionId: 'root-legacy', sessionFile: '/nonexistent/legacy.jsonl' }),
|
|
386
|
-
)
|
|
387
|
-
// 单新根语义:旧根 manifest 不进结果集(result/find 面旧记录不可见是 D6 取舍清单明示项)
|
|
388
|
-
expect(await listRecordManifests(dir)).toHaveLength(0)
|
|
389
|
-
})
|
|
390
|
-
|
|
391
|
-
it('D6 单新根锚定:非会话分区目录(isPiSessionId 不过)剪枝跳过,合法分区正常读', async () => {
|
|
392
|
-
const { resolveSessionStoreRoot } = await import('../discovery/roots.js')
|
|
393
|
-
const storeRoot = resolveSessionStoreRoot(dir)
|
|
394
|
-
// 非法分区名(前导 - 违反 isPiSessionId 首字符规则)下的 records 不扫
|
|
395
|
-
const bogus = join(storeRoot, '-bogus-partition', 'subagents', 'records')
|
|
396
|
-
await mkdir(bogus, { recursive: true })
|
|
397
|
-
await writeFile(
|
|
398
|
-
join(bogus, 'sa-bogus.json'),
|
|
399
|
-
JSON.stringify({ id: 'sa-bogus', rootSessionId: '-bogus-partition', sessionFile: '/nonexistent/b.jsonl' }),
|
|
400
|
-
)
|
|
401
|
-
// 合法分区在场
|
|
402
|
-
await writeRecordManifest(dir, 'sa-ok', {
|
|
403
|
-
rootSessionId: 'root-ok',
|
|
404
|
-
agentName: 'explorer',
|
|
405
|
-
sessionFile: '/tmp/sa-ok.jsonl',
|
|
406
|
-
})
|
|
407
|
-
const manifests = await listRecordManifests(dir)
|
|
408
|
-
expect(manifests.map((m) => m.id)).toEqual(['sa-ok'])
|
|
409
|
-
})
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
// ============================================================
|
|
413
|
-
// U4 端到端:buildFamilyFromFs 富化(manifest 主 / P-fallback identity 回退 / orphan / compat)
|
|
414
|
-
// 验证数据流重组:manifest 索引命中透全字段,未命中读尾行 identity 回退,孤儿 manifest 富字段+cleanedUp
|
|
415
|
-
// ============================================================
|
|
416
|
-
|
|
417
|
-
describe('U4 buildFamilyFromFs 富化(manifest 主 / P-fallback)', () => {
|
|
418
|
-
let dataDir: string
|
|
419
|
-
let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻,见 makeAgentDir 注释)
|
|
420
|
-
|
|
421
|
-
beforeEach(async () => {
|
|
422
|
-
dataDir = await makeAgentDir()
|
|
423
|
-
dir = agentDirOf(dataDir)
|
|
424
|
-
})
|
|
425
|
-
afterEach(async () => {
|
|
426
|
-
await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
427
|
-
})
|
|
428
|
-
|
|
429
|
-
it('TC-u4-manifest-enrich: alive + manifest 全字段 → SubagentRef 富字段全透传', async () => {
|
|
430
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
431
|
-
const subPath = await writeSubagentSession(dir, SUB_REAL, {
|
|
432
|
-
rootSessionId: ROOT,
|
|
433
|
-
slug: 'identity-slug', // identity.slug,manifest 主时被 manifest.slug 覆盖
|
|
434
|
-
task: 'identity-task', // identity.task,manifest 主时被 manifest.task 覆盖
|
|
435
|
-
agent: 'worker', // identity.agent,manifest 主时被 manifest.agentName 覆盖
|
|
436
|
-
})
|
|
437
|
-
// manifest 全字段(命中 meta.path 索引 → 走 manifest 主路径,覆盖 identity 值)
|
|
438
|
-
await writeRecordManifest(dir, `sa-${SUB_REAL}`, {
|
|
439
|
-
rootSessionId: ROOT,
|
|
440
|
-
agentName: 'explorer',
|
|
441
|
-
sessionFile: subPath,
|
|
442
|
-
task: '调研 codex',
|
|
443
|
-
slug: 'codex-ask-user-research',
|
|
444
|
-
model: 'glm-5.2',
|
|
445
|
-
status: 'completed',
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
449
|
-
const sub = family.subagents.find((s) => s.sessionId === SUB_REAL)
|
|
450
|
-
|
|
451
|
-
expect(sub).toBeDefined()
|
|
452
|
-
// manifest 主:富字段从 manifest 透传(覆盖 identity 的值)
|
|
453
|
-
expect(sub!.task).toBe('调研 codex')
|
|
454
|
-
expect(sub!.slug).toBe('codex-ask-user-research')
|
|
455
|
-
expect(sub!.agentName).toBe('explorer') // manifest.agentName → agentName
|
|
456
|
-
expect(sub!.model).toBe('glm-5.2')
|
|
457
|
-
expect(sub!.status).toBe('completed')
|
|
458
|
-
expect(sub!.sessionFile).toBe(subPath)
|
|
459
|
-
expect(sub!.cleanedUp).toBe(false)
|
|
460
|
-
expect(sub!.rootSessionId).toBe(ROOT)
|
|
461
|
-
})
|
|
462
|
-
|
|
463
|
-
it('TC-u4-pfallback-identity: alive 无 manifest + identity 含 task/agent → 回退 identity,model/status undefined', async () => {
|
|
464
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
465
|
-
// 只写 alive session(含 identity 尾行),不写 manifest → P-fallback 路径
|
|
466
|
-
const subPath = await writeSubagentSession(dir, SUB_REAL, {
|
|
467
|
-
rootSessionId: ROOT,
|
|
468
|
-
slug: 'fix',
|
|
469
|
-
task: 'fix bug',
|
|
470
|
-
agent: 'worker',
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
474
|
-
const sub = family.subagents.find((s) => s.sessionId === SUB_REAL)
|
|
475
|
-
|
|
476
|
-
expect(sub).toBeDefined()
|
|
477
|
-
// P-fallback:task/agent/sessionFile 从 identity 回退
|
|
478
|
-
expect(sub!.task).toBe('fix bug')
|
|
479
|
-
expect(sub!.slug).toBe('fix')
|
|
480
|
-
expect(sub!.agentName).toBe('worker') // identity.data.agent → agentName
|
|
481
|
-
expect(sub!.sessionFile).toBe(subPath) // P-fallback sessionFile=alive meta.path
|
|
482
|
-
// P-fallback 核心断言:model/status 不可回退,必 undefined
|
|
483
|
-
expect(sub!.model).toBeUndefined()
|
|
484
|
-
expect(sub!.status).toBeUndefined()
|
|
485
|
-
expect(sub!.cleanedUp).toBe(false)
|
|
486
|
-
})
|
|
487
|
-
|
|
488
|
-
it('TC-u4-pfallback-no-identity: alive 无 manifest 无 identity(运行中)→ 不入 family.subagents,不抛错', async () => {
|
|
489
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
490
|
-
// alive 文件无 identity 尾行(运行中/异常),无 manifest
|
|
491
|
-
await writeAliveSubagentNoIdentity(dir, ROOT, SUB_REAL)
|
|
492
|
-
|
|
493
|
-
// 核心断言:buildFamilyFromFs 不抛错、不崩溃
|
|
494
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
495
|
-
|
|
496
|
-
// 无 manifest 无 identity → 无法确定 rootSessionId → 不入 family.subagents
|
|
497
|
-
const sub = family.subagents.find((s) => s.sessionId === SUB_REAL)
|
|
498
|
-
expect(sub).toBeUndefined()
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
it('TC-u4-orphan-manifest: manifest 全字段 + sessionFile 指向不存在路径 → 孤儿 cleanedUp=true,富字段透传', async () => {
|
|
502
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
503
|
-
const ghostPath = '/nonexistent/gc-ghost.jsonl' // .jsonl 已被 GC(不写该文件)
|
|
504
|
-
await writeRecordManifest(dir, 'sa-ghost-id', {
|
|
505
|
-
rootSessionId: ROOT,
|
|
506
|
-
agentName: 'worker',
|
|
507
|
-
sessionFile: ghostPath,
|
|
508
|
-
task: 'ghost task',
|
|
509
|
-
slug: 'ghost-slug',
|
|
510
|
-
model: 'gpt-4',
|
|
511
|
-
status: 'completed',
|
|
512
|
-
})
|
|
513
|
-
|
|
514
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
515
|
-
const ghost = family.subagents.find((s) => s.sessionId === 'sa-ghost-id')
|
|
516
|
-
|
|
517
|
-
expect(ghost).toBeDefined()
|
|
518
|
-
expect(ghost!.cleanedUp).toBe(true) // 文件 GC → cleanedUp
|
|
519
|
-
// 孤儿用 manifest 完整富字段
|
|
520
|
-
expect(ghost!.task).toBe('ghost task')
|
|
521
|
-
expect(ghost!.slug).toBe('ghost-slug')
|
|
522
|
-
expect(ghost!.agentName).toBe('worker')
|
|
523
|
-
expect(ghost!.model).toBe('gpt-4')
|
|
524
|
-
expect(ghost!.status).toBe('completed')
|
|
525
|
-
expect(ghost!.sessionFile).toBe(ghostPath) // GC 路径保留(不置空)
|
|
526
|
-
expect(ghost!.rootSessionId).toBe(ROOT)
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
it('TC-u4-recordmanifest-compat: 旧 manifest(仅 id/rootSessionId/sessionFile)→ 富字段 undefined,不抛错', async () => {
|
|
530
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
531
|
-
// 旧格式 manifest:无 task/slug/model/status/agentName
|
|
532
|
-
await writeRecordManifest(dir, 'sa-old', {
|
|
533
|
-
rootSessionId: ROOT,
|
|
534
|
-
sessionFile: '/nonexistent/old.jsonl',
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
// listRecordManifests 兼容:返回该 manifest,富字段 undefined
|
|
538
|
-
const manifests = await listRecordManifests(dir)
|
|
539
|
-
expect(manifests).toHaveLength(1)
|
|
540
|
-
const m = manifests[0]
|
|
541
|
-
expect(m.id).toBe('sa-old')
|
|
542
|
-
expect(m.rootSessionId).toBe(ROOT)
|
|
543
|
-
expect(m.task).toBeUndefined()
|
|
544
|
-
expect(m.slug).toBeUndefined()
|
|
545
|
-
expect(m.model).toBeUndefined()
|
|
546
|
-
expect(m.status).toBeUndefined()
|
|
547
|
-
expect(m.agentName).toBeUndefined()
|
|
548
|
-
|
|
549
|
-
// buildFamilyFromFs 消费旧 manifest(孤儿路径)不抛错,富字段 undefined
|
|
550
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
551
|
-
const ghost = family.subagents.find((s) => s.sessionId === 'sa-old')
|
|
552
|
-
expect(ghost).toBeDefined()
|
|
553
|
-
expect(ghost!.cleanedUp).toBe(true)
|
|
554
|
-
expect(ghost!.task).toBeUndefined()
|
|
555
|
-
expect(ghost!.model).toBeUndefined()
|
|
556
|
-
// 旧 manifest slug 缺失 → 回退 agentName(也缺)→ 空串兜底
|
|
557
|
-
expect(ghost!.slug).toBe('')
|
|
558
|
-
})
|
|
559
|
-
})
|
|
560
|
-
|
|
561
|
-
// ============================================================
|
|
562
|
-
// .events 文件族结构性忽略(W1 D3 落点裁决 + 设计检查点④)
|
|
563
|
-
// ============================================================
|
|
564
|
-
//
|
|
565
|
-
// record 事件文件族(<recordsDir>/<sa-id>.events,无 .jsonl 后缀)与 manifest 同目录
|
|
566
|
-
// 同主名。session-reader 的两层兼容形态在此断言:
|
|
567
|
-
// ① 结构性忽略——subagents 树扫描只收 .jsonl,.events 天然不进候选集(D3「无后缀
|
|
568
|
-
// 的结构性收益」:被忽略或被误读两类风险一次排空);
|
|
569
|
-
// ② 首行头行读者兼容——.events 首行是自描述头行 {"type":"record-journal","id":...}
|
|
570
|
-
// (写侧 record-events.ts toRecordEventHeader 契约),session-reader 对未知文件
|
|
571
|
-
// 读首行判 header 时命中非 session header 即忽略(检查点④:零成本兼容——不改
|
|
572
|
-
// 扫描器即可与该文件族共存)。
|
|
573
|
-
|
|
574
|
-
describe('.events 文件族结构性忽略(W1 D3 / 检查点④)', () => {
|
|
575
|
-
let dataDir: string
|
|
576
|
-
let dir: string // agentDir = <dataDir>/agent(生产拓扑复刻,见 makeAgentDir 注释)
|
|
577
|
-
|
|
578
|
-
beforeEach(async () => {
|
|
579
|
-
dataDir = await makeAgentDir()
|
|
580
|
-
dir = agentDirOf(dataDir)
|
|
581
|
-
})
|
|
582
|
-
afterEach(async () => {
|
|
583
|
-
await rm(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
|
|
584
|
-
})
|
|
585
|
-
|
|
586
|
-
/** 写 record 事件文件(首行头行 + 事件行集,形态对齐写侧 record-events.ts)。 */
|
|
587
|
-
async function writeEventsFile(saId: string): Promise<string> {
|
|
588
|
-
const recordsDir = resolveStoreSubagentRecordsDir(dir, ROOT)
|
|
589
|
-
await mkdir(recordsDir, { recursive: true })
|
|
590
|
-
const path = join(recordsDir, `${saId}.events`)
|
|
591
|
-
const lines = [
|
|
592
|
-
JSON.stringify({ type: 'record-journal', id: saId }),
|
|
593
|
-
JSON.stringify({ type: 'record-created', seq: 1, ts: 1, agent: 'dev', task: 't', slug: 's', origin: 'tool', rootSessionId: ROOT, depth: 0, mode: 'sync', startedAt: 1 }),
|
|
594
|
-
JSON.stringify({ type: 'record-settled', seq: 2, ts: 2, stopReason: 'end_turn', endedAt: 2, turns: 1, totalTokens: 5 }),
|
|
595
|
-
]
|
|
596
|
-
await writeFile(path, lines.join('\n') + '\n')
|
|
597
|
-
return path
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
it('subagent 树扫描不收 .events:resolveSessionRoots 的 subagent 根 files 含 .jsonl 不含 .events', async () => {
|
|
601
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
602
|
-
const subPath = await writeSubagentSession(dir, SUB_REAL, {
|
|
603
|
-
rootSessionId: ROOT,
|
|
604
|
-
slug: 'ev-sibling',
|
|
605
|
-
})
|
|
606
|
-
await writeEventsFile('sa-ev-1')
|
|
607
|
-
|
|
608
|
-
const roots = await resolveSessionRoots({ agentDir: dir })
|
|
609
|
-
const subRoot = roots.find((r) => r.kind === 'subagent')
|
|
610
|
-
|
|
611
|
-
expect(subRoot).toBeDefined()
|
|
612
|
-
expect(subRoot!.files.map((f) => f.path)).toContain(subPath)
|
|
613
|
-
expect(subRoot!.files.some((f) => f.path.endsWith('.events'))).toBe(false)
|
|
614
|
-
})
|
|
615
|
-
|
|
616
|
-
it('首行头行命中非 session header 即忽略:record-journal 头行 → parseSessionHeader 返回 null', async () => {
|
|
617
|
-
const eventsPath = await writeEventsFile('sa-ev-1')
|
|
618
|
-
// 检查点④形态:session-reader 对未知文件读首行判 header,非 session 行 → null
|
|
619
|
-
//(即使未来通配规则把 .events 收进候选集,该文件也不会被误认成 session)
|
|
620
|
-
const firstLine = await readSessionHeaderFirstLine(eventsPath)
|
|
621
|
-
expect(firstLine).toBeDefined()
|
|
622
|
-
expect(parseSessionHeader(firstLine)).toBeNull()
|
|
623
|
-
})
|
|
624
|
-
|
|
625
|
-
it('家族扫描:.events 与正常 subagent 同目录共存 → 家族正常、不产幻影 subagent 节点', async () => {
|
|
626
|
-
await writeMainSession(dir, '--root-cwd--', ROOT, { cwd: '/proj/root' })
|
|
627
|
-
await writeSubagentSession(dir, SUB_REAL, {
|
|
628
|
-
rootSessionId: ROOT,
|
|
629
|
-
slug: 'ev-coexist',
|
|
630
|
-
})
|
|
631
|
-
await writeEventsFile('sa-ev-1')
|
|
632
|
-
|
|
633
|
-
const family = await buildFamilyFromFs(ROOT, dir)
|
|
634
|
-
|
|
635
|
-
// 正常 subagent 在场
|
|
636
|
-
expect(family.subagents.some((s) => s.sessionId === SUB_REAL)).toBe(true)
|
|
637
|
-
// .events 的 record id 不进任何集合(无幻影)
|
|
638
|
-
expect(family.subagents.some((s) => s.sessionId === 'sa-ev-1')).toBe(false)
|
|
639
|
-
expect(family.subagents.some((s) => s.sessionId.startsWith('sa-ev'))).toBe(false)
|
|
640
|
-
})
|
|
641
|
-
})
|
|
642
|
-
|
|
643
|
-
// ============================================================
|
|
644
|
-
// U4 C4 契约:extractSessionIdFromFilename 导出(仅加 export,不改逻辑,供 w4 复用)
|
|
645
|
-
// ============================================================
|
|
646
|
-
|
|
647
|
-
describe('U4 extractSessionIdFromFilename 导出(C4 契约)', () => {
|
|
648
|
-
it('导出生效:从 <timestamp>_<sessionId>.jsonl 提取 sessionId', () => {
|
|
649
|
-
expect(
|
|
650
|
-
extractSessionIdFromFilename('2026-08-07T16-49-48-393Z_019fdd21-8169-7a02-8f11-eef6c9ca11cc.jsonl'),
|
|
651
|
-
).toBe('019fdd21-8169-7a02-8f11-eef6c9ca11cc')
|
|
652
|
-
})
|
|
653
|
-
|
|
654
|
-
it('非 uuid 特征文件名 → 空串(行为不变)', () => {
|
|
655
|
-
expect(extractSessionIdFromFilename('not-a-uuid.jsonl')).toBe('')
|
|
656
|
-
expect(extractSessionIdFromFilename('readme.txt')).toBe('')
|
|
657
|
-
})
|
|
658
|
-
|
|
659
|
-
it('无下划线的纯 uuid 文件名 → 返回 uuid(兼容简化场景)', () => {
|
|
660
|
-
expect(extractSessionIdFromFilename('019fdd21-8169-7a02-8f11-eef6c9ca11cc.jsonl')).toBe(
|
|
661
|
-
'019fdd21-8169-7a02-8f11-eef6c9ca11cc',
|
|
662
|
-
)
|
|
663
|
-
})
|
|
664
|
-
})
|
|
665
|
-
|
|
666
|
-
// U4 富字段透传契约由 fixture 侧逐条覆盖:manifest 主(TC-u4-manifest-enrich,task/slug/
|
|
667
|
-
// model/status 全字段 + sessionFile 非空)、P-fallback(TC-u4-pfallback-identity,status 必
|
|
668
|
-
// undefined)、孤儿(TC-u4-orphan-manifest)、旧 manifest 兼容(TC-u4-recordmanifest-compat)。
|