@zhushanwen/pi-session-reader 0.8.1 → 0.9.0

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.
@@ -3,7 +3,7 @@
3
3
  // ============================================================
4
4
  //
5
5
  // 本文件消费 discovery/workflows.ts 的 readRunSnapshot(v1/旧指针档——返 unknown
6
- // 原始快照对象,NEW/OLD 双格式分支)与 record 流文本([D16③] v2 档——journalPath
6
+ // 原始快照对象,NEW/OLD 双格式分支)与 record 流文本([D16③] v2 档——recordPath
7
7
  // 锚点语义重定义为 record 流路径后的新数据源),把 unknown/事件流行类型化为
8
8
  // WorkflowOverview,再渲染为人类可读文本。零 IO:parseRunSnapshot /
9
9
  // parseRunRecordStream / renderWorkflowOverview 喂 mock 即可单测(w5
@@ -46,12 +46,12 @@ const SUBAGENT_RECORD_ENTRY_V2 = 2
46
46
  * 从单条 entry 提取 zcode 锚。形状校验窄而严,任一不满足 = 该条不算命中(返回
47
47
  * undefined,调用方继续扫——缺键条目不遮蔽更早的完整条目):
48
48
  * - `type === 'custom'` ∧ `customType === 'subagent-record'`
49
- * - 版本/判别键二选一(schema 版本守卫,不认识的版本跳过而非猜测——写侧
50
- * record-entry.ts classifySubagentRecordEntryData 同口径):
51
- * - v1:`data.v === 1`(全量快照形态,兼容读面);
52
- * - v2:`data.v === 2` ∧ `data.kind === 'settled'`(W1 注册/终态两条小条目——
53
- * **锚只取自终态条**:registered 条目不携带 engineHandle,且设计 D1 裁决
54
- * 「sessionRef 双键取自终态条」;kind 非法 = unknown-kind 跳过);
49
+ * - 版本/判别键(schema 版本守卫,不认识的版本跳过而非猜测——写侧
50
+ * record-entry.ts classifySubagentRecordEntryData v2-only 同口径):
51
+ * `data.v === 2` ∧ `data.kind === 'settled'`(W1 注册/终态两条小条目——
52
+ * **锚只取自终态条**:registered 条目不携带 engineHandle,且设计 D1 裁决
53
+ * 「sessionRef 双键取自终态条」;kind 非法 = unknown-kind 跳过。v1 全量快照形态
54
+ * 已随兼容层删除,不再命中,2026-09-30 登记 §3.3);
55
55
  * - `data.id === saId`(record id 即 sa-id,record-access.ts `sa-${uuid}`)
56
56
  * - `engineHandle.sessionRef.sessionId` / `.dbPath` 均为非空 string(F8 双键齐判据的
57
57
  * 锚字段部分;pi record 的 sessionRef 无 dbPath → 天然不命中,无需 engine 判别)
@@ -66,14 +66,11 @@ function zcodeAnchorOfEntry(entry: Entry, saId: string): ZcodeAnchor | undefined
66
66
  return extractZcodeSessionRefAnchor(d.engineHandle)
67
67
  }
68
68
 
69
- /** [zcodeAnchorOfEntry 拆分] 条目形态判别:v1 全量快照 或 v2 终态条(registered
70
- * 条目不携带 engineHandle,且设计 D1 裁决「sessionRef 双键取自终态条」;kind
71
- * 非法 = unknown-kind 跳过)。 */
69
+ /** [zcodeAnchorOfEntry 拆分] 条目形态判别:当前版本终态条(registered 条目不携带
70
+ * engineHandle,且设计 D1 裁决「sessionRef 双键取自终态条」;kind 非法 = 跳过。
71
+ * v1 全量快照形态已随兼容层删除,不再命中)。 */
72
72
  function isAnchorEligibleRecordEntryVersion(d: Record<string, unknown>): boolean {
73
- if (d.v === SUBAGENT_RECORD_ENTRY_V2) {
74
- return d.kind === 'settled'
75
- }
76
- return d.v === 1
73
+ return d.v === SUBAGENT_RECORD_ENTRY_V2 && d.kind === 'settled'
77
74
  }
78
75
 
79
76
  /** [zcodeAnchorOfEntry 拆分] engineHandle.sessionRef 双键提取(sessionId/dbPath
@@ -5,109 +5,45 @@ import type { SessionRef, WorkflowRef } from '../core/family.js'
5
5
  import { extractSessionIdFromFilename } from './subagents.js'
6
6
 
7
7
  // ============================================================
8
- // workflow-state 发现链路(w5 从 subagents.ts 物理迁移,架构归位 SSOT §6.3)
8
+ // workflow-record 发现链路(单档:v2 注册条目 recordPath 主源)
9
9
  // ============================================================
10
10
  //
11
11
  // 本文件持有 workflow run 的发现与 sessionFile 提取逻辑(IO 适配层):
12
- // - resolveWorkflows:三档发现链(按 runId 合并,高档条目在即用高档)——
13
- // ① v2 journalPath 主源([D16③] 发现链重锚:journalPath 锚点语义 = record 流路径):
14
- // workflow-record v2 注册条目的 journalPath 指向 <runId>.record.jsonl([D1] record
15
- // 单源事件流),发现链读 record 流行自提 calls——sessionFile 取 agent-settled 帧
16
- // result 全文携带的 sessionFile([D1] 载荷表:result 全文入事件,AgentResult
17
- // 自带 sessionFile 字段——家族链数据源同 record 流,无第二承载位);旧后缀锚点
18
- // (.events.jsonl 形态)= 历史实体([D1] 历史数据处置:旧两件不读)→ calls=[]
19
- // 仅 run 存在性兜底;
20
- // ② v1 快照层:workflow-record v1 快照条目(W17~W1 期间创建的 run——快照是全量的,
21
- // 直接从 snapshot.calls 提 sessionFile,家族链数据完整);
22
- // ③ 旧指针 fallback:workflow-state-link 指针条目(W17 前的 run,照旧读 link 指向的
23
- // wf-state 文件)。
24
- // - readRunSnapshot:读 wf-state 文件尾向找首个可解析行,返回原始对象(unknown,格式收窄交 core 层)。
25
- // - extractCallSessionFiles:从快照对象提 calls 的 sessionFile 绝对路径数组(NEW/OLD 双格式)。
12
+ // - resolveWorkflows:workflow-record v2 注册条目(`v === 2` ∧ `kind === 'registered'`
13
+ // ∧ runId/recordPath 均非空 string)是唯一发现通道——recordPath 锚点语义 = record
14
+ // 流路径([D16③]),指向 <runId>.record.jsonl([D1] record 单源事件流),发现链读
15
+ // record 流行自提 calls——sessionFile 取 agent-settled 帧 result 全文携带的
16
+ // sessionFile([D1] 载荷表:result 全文入事件,AgentResult 自带 sessionFile 字段
17
+ // ——家族链数据源同 record 流,无第二承载位);旧后缀锚点(.events.jsonl 形态)=
18
+ // 历史实体([D1] 历史数据处置:旧两件不读)→ calls=[] 仅 run 存在性兜底。
19
+ // 历史格式条目(v1 全量快照 / workflow-state-link 指针)不识别、静默从发现链消失
20
+ // (不拒读、不报错)——run 侧 v1 读面已删除(ADR-0095,同 record 侧裁决:项目未
21
+ // 上线无历史数据,不迁移不兼容)。
22
+ // - readRunSnapshot:读 wf-state 文件尾向找首个可解析行,返回原始对象(unknown,格式
23
+ // 收窄交 core 层)。唯一消费方 = tool-handler workflow 概览的快照链分支。
26
24
  // - extractRecordStreamSessionFiles:[D16③] 从 record 事件流提 calls 的 sessionFile
27
25
  // (agent-settled 帧 result.sessionFile——活跃 run 与已收编 run 同源可用)。
28
- // - sessionRefFromPath:sessionFile 路径 → SessionRef(命中 pathToRef 取完整,否则文件名提取最小 ref)。
26
+ // - sessionRefFromPath:sessionFile 路径 → SessionRef(命中 pathToRef 取完整,否则
27
+ // 文件名提取最小 ref)。
29
28
  //
30
29
  // 分层约定(w5 TC-wf-core-pure-logic):IO 全在 discovery/,core/workflow.ts 的
31
- // parseRunSnapshot/renderWorkflowOverview 是纯逻辑零 IO(喂 mock 可单测)。readRunSnapshot 返
32
- // unknown 不收窄——NEW/OLD 双格式的类型化是 core 层 parseRunSnapshot 的职责(TC-wf-snapshot-version-union)。
30
+ // parseRunSnapshot/renderWorkflowOverview 是纯逻辑零 IO(喂 mock 可单测)。
31
+ // readRunSnapshot 返 unknown 不收窄——类型化是 core 层 parseRunSnapshot 的职责
32
+ // (TC-wf-snapshot-version-union)。
33
33
  //
34
- // extractSessionIdFromFilename 留在 subagents.ts(find.ts + 导出契约测试直接消费),此处反向 import。
35
-
36
- /**
37
- * 从 wf-state 快照对象提取 calls[].sessionFile(绝对路径数组)。
38
- *
39
- * 两种格式(探查确认,本机 371 个 wf 文件):
40
- * - NEW (v="wf-run-v1" 或 "wf-run-v2",读取面形状一致):state.calls[],每项顶层
41
- * .sessionFile(258 文件 / 1590 sessionFile)。v2 由 pi-subagent-workflow 8.x 一次性
42
- * 生命周期收敛引入(status 两态、无 pausedAt),calls[].sessionFile/result 保留
43
- * - OLD (无 v):callCache[]=[{key,value}],value.sessionFile(112 文件 / 0 sessionFile,旧 pi 不持久化)
44
- */
45
- export function extractCallSessionFiles(snap: unknown): string[] {
46
- const out: string[] = []
47
- if (typeof snap !== 'object' || snap === null) return out
48
- const s = snap as Record<string, unknown>
49
- const isNew = isNewSnapshotFormat(s)
50
- const callsRaw = extractCallsRaw(s, isNew)
51
- if (!Array.isArray(callsRaw)) return out
52
- for (const c of callsRaw) {
53
- if (typeof c !== 'object' || c === null) continue
54
- const item = resolveCallItem(c as Record<string, unknown>, isNew)
55
- const sf = extractSessionFileFromItem(item)
56
- if (sf !== undefined) out.push(sf)
57
- }
58
- return out
59
- }
60
-
61
- /** 快照是否为 NEW 格式(v="wf-run-v1"/"wf-run-v2",读取面形状一致) */
62
- function isNewSnapshotFormat(s: Record<string, unknown>): boolean {
63
- return s.v === 'wf-run-v1' || s.v === 'wf-run-v2'
64
- }
65
-
66
- /** 按格式取 calls 原始集合:NEW 读 state.calls(state 守卫后取),OLD 读顶层 callCache */
67
- function extractCallsRaw(s: Record<string, unknown>, isNew: boolean): unknown {
68
- if (isNew) {
69
- const state = s.state
70
- return typeof state === 'object' && state !== null
71
- ? (state as Record<string, unknown>).calls
72
- : undefined
73
- }
74
- return s.callCache
75
- }
76
-
77
- /**
78
- * 单条 call 归一为可取 sessionFile 的对象:
79
- * NEW: call 本身;OLD: {key, value},取 value(value 非对象时回退 call 本身,容错脏数据)
80
- */
81
- function resolveCallItem(co: Record<string, unknown>, isNew: boolean): Record<string, unknown> {
82
- if (isNew) return co
83
- const value = co.value
84
- return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : co
85
- }
86
-
87
- /** 从归一后的 call 对象提 sessionFile:顶层优先,回退 result.sessionFile;均缺 → undefined */
88
- function extractSessionFileFromItem(item: Record<string, unknown>): string | undefined {
89
- const sf = item.sessionFile
90
- if (typeof sf === 'string') return sf
91
- const result = item.result
92
- const sf2 =
93
- typeof result === 'object' && result !== null
94
- ? (result as Record<string, unknown>).sessionFile
95
- : undefined
96
- if (typeof sf2 === 'string') return sf2
97
- return undefined
98
- }
34
+ // extractSessionIdFromFilename 留在 subagents.ts(find.ts + 导出契约测试直接消费),
35
+ // 此处反向 import。
99
36
 
100
37
  /**
101
38
  * 读 wf-state 文件,从尾向头找首个 trim 非空且 JSON.parse 成功的行,返回解析后的原始对象。
102
39
  *
103
- * 拆解自原 readWorkflowCallSessionFiles 的读行职责(w5 TC-wf-core-pure-logic)——后者=读行+
104
- * 提 sessionFile 的胶水,迁移后由 readRunSnapshot(读行,返 unknown)+ extractCallSessionFiles
105
- * (提 sessionFile)组合替代。返回类型 unknown:IO 层不假设格式,类型收窄交 core/workflow.ts
106
- * 的 parseRunSnapshot(C-readrunsnapshot-unknown,TC-wf-snapshot-version-union)。
40
+ * 拆解自原 readWorkflowCallSessionFiles 的读行职责(w5 TC-wf-core-pure-logic)——
41
+ * 读行(本函数,返 unknown)+ 类型化(core/workflow.ts 的 parseRunSnapshot,
42
+ * C-readrunsnapshot-unknown)分层。
107
43
  *
108
44
  * 尾向回退策略(沿用原实现,ES-wf-snapshot-partial):wf 文件是 rewrite 覆盖模式,读撞 rewrite
109
45
  * 中点时末行是半截 JSON(parse 失败)→ 试上一完整行;单行文件半行 → 全失败 → undefined。
110
- * 文件不存在/读失败/全行不可解析 → undefined(不抛错,调用方 resolveWorkflows 据此 calls=[])。
46
+ * 文件不存在/读失败/全行不可解析 → undefined(不抛错,调用方据此跳过该 run)。
111
47
  */
112
48
  export async function readRunSnapshot(wfPath: string): Promise<unknown | undefined> {
113
49
  let content: string
@@ -146,7 +82,7 @@ function sessionRefFromPath(path: string, pathToRef: Map<string, SessionRef>): S
146
82
  }
147
83
 
148
84
  // ============================================================
149
- // workflow-record 条目契约(三档发现链的条目判别——本地字面量 + 测试守卫漂移)
85
+ // workflow-record 条目契约(v2 注册条目判别——本地字面量 + 测试守卫漂移)
150
86
  // ============================================================
151
87
 
152
88
  /**
@@ -159,111 +95,57 @@ function sessionRefFromPath(path: string, pathToRef: Map<string, SessionRef>): S
159
95
  */
160
96
  const WORKFLOW_RECORD_CUSTOM_TYPE = 'workflow-record'
161
97
 
162
- /** v2 条目 data 的认识版本词表(W1 起 = 2;写侧 WORKFLOW_RECORD_ENTRY_VERSION 同值)。 */
98
+ /** v2 条目 data 的认识版本(写侧 WORKFLOW_RECORD_ENTRY_VERSION 同值,W1 起 = 2)。 */
163
99
  const WORKFLOW_RECORD_ENTRY_V2 = 2
164
100
 
165
101
  /**
166
102
  * run record 流文件名尾段(`<runId>.record.jsonl`)。写侧 subagent-core
167
- * RUN_EVENT_JOURNAL_SUFFIX 同值([D1] record 单源后缀)——v2 注册条目的
168
- * journalPath 锚点后缀判定用(新锚点直读 record 流;旧 `.events.jsonl` 锚点 =
103
+ * RUN_EVENTS_SUFFIX 同值([D1] record 单源后缀)——v2 注册条目的
104
+ * recordPath 锚点后缀判定用(新锚点直读 record 流;旧 `.events.jsonl` 锚点 =
169
105
  * 历史实体分流),本地持有 + 测试守卫漂移。
170
106
  */
171
107
  export const RUN_RECORD_STREAM_SUFFIX = '.record.jsonl'
172
108
 
173
- /** 单遍扫描 entries 的三档收集结果(档内同 runId 后写覆盖前写,跨档优先级见 resolveWorkflows)。 */
174
- interface WorkflowEntryTiers {
175
- /** ① v2 注册条目:runId → journalPath 锚点(W1+ 的 run)。 */
176
- v2ByRunId: Map<string, string>
177
- /** ② v1 快照条目:runId → 全量快照对象(W17~W1 期间创建的 run)。 */
178
- v1ByRunId: Map<string, unknown>
179
- /** ③ 旧指针条目:runId → link path(W17 前的 run)。 */
180
- linkByRunId: Map<string, { runId: string; path: string }>
181
- /** runId 首见序(输出稳定序——条目在 session 文件中的出现顺序)。 */
182
- runIdOrder: string[]
109
+ /** v2 注册条目收集结果(同 runId 后写覆盖前写;order = 条目在 session 文件中的首见序)。 */
110
+ interface RegisteredRecordPaths {
111
+ byRunId: Map<string, string>
112
+ order: string[]
183
113
  }
184
114
 
185
115
  /**
186
- * 单遍扫描 entries 按三档形态分桶(纯收集,无 IO):
187
- * - workflow-record v2 注册条目:`v === 2` ∧ `kind === 'registered'` ∧ runId/journalPath
188
- * 均非空 string(终态条目不携带 journalPath,非发现链数据源,不收集);
189
- * - workflow-record v1 快照条目:`v === 1` ∧ snapshot 为对象 ∧ `snapshot.runId` 为
190
- * string(runId 在快照内——entry data 顶层无 runId 字段);
191
- * - workflow-state-link 指针条目:现状判别。
116
+ * 单遍扫描 entries 收集 workflow-record v2 注册条目(纯收集,无 IO):
117
+ * `v === 2` ∧ `kind === 'registered'` ∧ runId/recordPath 均非空 string。终态条目
118
+ * (kind === 'settled')不携带 recordPath、不参与发现链,不收集。
192
119
  */
193
- function collectWorkflowEntryTiers(entries: readonly Entry[]): WorkflowEntryTiers {
194
- const tiers: WorkflowEntryTiers = {
195
- v2ByRunId: new Map(),
196
- v1ByRunId: new Map(),
197
- linkByRunId: new Map(),
198
- runIdOrder: [],
199
- }
120
+ function collectRegisteredRecordPaths(entries: readonly Entry[]): RegisteredRecordPaths {
121
+ const byRunId = new Map<string, string>()
122
+ const order: string[] = []
200
123
  const seen = new Set<string>()
201
- const noteRunId = (runId: string): void => {
202
- if (!seen.has(runId)) {
203
- seen.add(runId)
204
- tiers.runIdOrder.push(runId)
205
- }
206
- }
207
124
  for (const e of entries) {
208
125
  const data = e.data as Record<string, unknown> | undefined
209
- if (e.customType === WORKFLOW_RECORD_CUSTOM_TYPE && data !== undefined) {
210
- collectWorkflowRecordEntryTier(tiers, noteRunId, data)
211
- continue
212
- }
213
- // legacy 指针档字面量:常量 SSOT 在 subagent-core(WORKFLOW_STATE_LINK_CUSTOM_TYPE,
214
- // orchestration/workflow-record-entry.ts)。reader 生产依赖面不引 subagent-core
215
- // (包架构裁决,同 entry-anchor.ts 的协议字面量模式),保持本地镜像,值等价由
216
- // __tests__/workflow.test.ts 跨包契约守卫断言锚定。
217
- if (e.customType === 'workflow-state-link') {
218
- collectWorkflowStateLinkTier(tiers, noteRunId, data)
219
- }
220
- }
221
- return tiers
222
- }
223
-
224
- /** [collectWorkflowEntryTiers 拆分] workflow-record 条目两代分流(v2 注册 / v1 快照)。 */
225
- function collectWorkflowRecordEntryTier(
226
- tiers: WorkflowEntryTiers,
227
- noteRunId: (runId: string) => void,
228
- data: Record<string, unknown>,
229
- ): void {
230
- if (data.v === WORKFLOW_RECORD_ENTRY_V2 && data.kind === 'registered') {
126
+ if (e.customType !== WORKFLOW_RECORD_CUSTOM_TYPE || data === undefined) continue
127
+ if (data.v !== WORKFLOW_RECORD_ENTRY_V2 || data.kind !== 'registered') continue
231
128
  const runId = data.runId
232
- const journalPath = data.journalPath
233
- if (typeof runId === 'string' && runId !== '' && typeof journalPath === 'string' && journalPath !== '') {
234
- tiers.v2ByRunId.set(runId, journalPath)
235
- noteRunId(runId)
129
+ const recordPath = data.recordPath
130
+ if (typeof runId !== 'string' || runId === '' || typeof recordPath !== 'string' || recordPath === '') {
131
+ continue
236
132
  }
237
- } else if (data.v === 1 && typeof data.snapshot === 'object' && data.snapshot !== null) {
238
- const snapshotRunId = (data.snapshot as Record<string, unknown>).runId
239
- if (typeof snapshotRunId === 'string' && snapshotRunId !== '') {
240
- tiers.v1ByRunId.set(snapshotRunId, data.snapshot)
241
- noteRunId(snapshotRunId)
133
+ if (!seen.has(runId)) {
134
+ seen.add(runId)
135
+ order.push(runId)
242
136
  }
137
+ byRunId.set(runId, recordPath) // 后写覆盖前写(取最新锚点)
243
138
  }
244
- }
245
-
246
- /** [collectWorkflowEntryTiers 拆分] workflow-state-link 指针条目(后写覆盖前写——取最新 link)。 */
247
- function collectWorkflowStateLinkTier(
248
- tiers: WorkflowEntryTiers,
249
- noteRunId: (runId: string) => void,
250
- data: Record<string, unknown> | undefined,
251
- ): void {
252
- const runId = data?.runId
253
- const path = data?.path
254
- if (typeof runId === 'string' && typeof path === 'string') {
255
- tiers.linkByRunId.set(runId, { runId, path }) // 后写覆盖前写(取最新 link)
256
- noteRunId(runId)
257
- }
139
+ return { byRunId, order }
258
140
  }
259
141
 
260
142
  /**
261
- * [D16③] journalPath 后缀分流:`.record.jsonl` 锚点 = 新形态(record 流直读);
143
+ * [D16③] recordPath 后缀分流:`.record.jsonl` 锚点 = 新形态(record 流直读);
262
144
  * 其余(含旧 `.events.jsonl` 锚点)= 历史实体([D1] 历史数据处置——旧两件不读
263
145
  * 不写,历史 run 从发现链数据面退空即预期行为)。
264
146
  */
265
- function isNewRecordStreamAnchor(journalPath: string): boolean {
266
- return journalPath.endsWith(RUN_RECORD_STREAM_SUFFIX)
147
+ function isNewRecordStreamAnchor(recordPath: string): boolean {
148
+ return recordPath.endsWith(RUN_RECORD_STREAM_SUFFIX)
267
149
  }
268
150
 
269
151
  /** agent-settled 帧的最小消费视图(record 流宽容解析只消费 taskIndex 与
@@ -310,20 +192,16 @@ export function extractRecordStreamSessionFiles(content: string): string[] {
310
192
  }
311
193
 
312
194
  /**
313
- * 读目标 session 文件全文,按三档发现链构造 WorkflowRef[](D10 断链修复:W17 前/
314
- * W17~W1/W1+ 三类 run 全部有发现通道)。同一 runId 的多条目按 runId 去重取档内
315
- * 最新(后写覆盖);跨档优先级 = v2 注册条目 > v1 快照条目 > 旧指针——一个 run 只
316
- * 以一种形态写条目(创建时点决定),跨档同 runId 是坏数据防御,高档在即用高档。
195
+ * 读目标 session 文件全文,从 workflow-record v2 注册条目构造 WorkflowRef[]
196
+ * (单档发现链——v1 快照条目与 workflow-state-link 指针条目不再识别,历史 run
197
+ * 从发现链数据面退空即预期行为,见 ADR-0095)。同一 runId 的多条目后写覆盖
198
+ * (条目顺序 = 时间顺序,取最新锚点)。
317
199
  *
318
- * **签名与返回值结构(WorkflowRef[]{runId,stateFile,calls:SessionRef[]})完全不变**
319
- * (C-resolveworkflows-signature,保 m1 已冻结交付的消费者)。三档的 stateFile 语义:
320
- * - v2 档:stateFile = journalPath 本身(record 流路径——[D16③] 锚点语义重定义,
321
- * D1 后 v2 条目携带的锚点即 record 事件流文件);record 流直读提 calls
322
- * (agent-settled.result.sessionFile),流被清理/不可读 → calls=[](run 存在性
323
- * 兜底);
324
- * - v1 档:空串(v1 快照条目不携带 state 路径,快照数据直接从条目提 calls;概览
325
- * 消费面 readRunSnapshot('') 自然落「快照不可读」跳过,家族链 calls 不受影响);
326
- * - 旧指针档:link 的 path(现状)。
200
+ * **签名与返回值结构(WorkflowRef[]{runId,stateFile,calls:SessionRef[]})不变**
201
+ * (C-resolveworkflows-signature,保 m1 已冻结交付的消费者)。stateFile = recordPath
202
+ * 本身(record 流路径——[D16③] 锚点语义,D1 后 v2 条目携带的锚点即 record 事件流
203
+ * 文件);record 流直读提 calls(agent-settled.result.sessionFile),流被清理/
204
+ * 不可读 → calls=[](run 存在性兜底)。
327
205
  */
328
206
  export async function resolveWorkflows(
329
207
  sessionId: string,
@@ -339,49 +217,23 @@ export async function resolveWorkflows(
339
217
  return []
340
218
  }
341
219
  const { entries } = parseSessionContent(content)
342
- const tiers = collectWorkflowEntryTiers(entries)
220
+ const recordPaths = collectRegisteredRecordPaths(entries)
343
221
  const workflows: WorkflowRef[] = []
344
- for (const runId of tiers.runIdOrder) {
345
- const journalPath = tiers.v2ByRunId.get(runId)
346
- if (journalPath !== undefined) {
347
- // ① v2 档([D16③] 重锚):journalPath = record 流路径——新后缀锚点直读流提
348
- // calls(agent-settled.result.sessionFile);旧后缀锚点 = 历史实体([D1]
349
- // 不读旧两件)→ calls=[] 仅 run 存在性兜底。
350
- let sessionFiles: string[] = []
351
- if (isNewRecordStreamAnchor(journalPath)) {
352
- let content: string | undefined
353
- try {
354
- content = await readFile(journalPath, 'utf8')
355
- } catch {
356
- content = undefined // 流被保留期清理/不可读 → calls=[](run 存在性兜底)
357
- }
358
- sessionFiles = content === undefined ? [] : extractRecordStreamSessionFiles(content)
222
+ for (const runId of recordPaths.order) {
223
+ const recordPath = recordPaths.byRunId.get(runId)
224
+ if (recordPath === undefined) continue
225
+ let streamContent: string | undefined
226
+ if (isNewRecordStreamAnchor(recordPath)) {
227
+ try {
228
+ streamContent = await readFile(recordPath, 'utf8')
229
+ } catch {
230
+ streamContent = undefined // 流被保留期清理/不可读 → calls=[](run 存在性兜底)
359
231
  }
360
- workflows.push({
361
- runId,
362
- stateFile: journalPath,
363
- calls: sessionFiles.map((sf) => sessionRefFromPath(sf, pathToRef)),
364
- })
365
- continue
366
- }
367
- const snapshot = tiers.v1ByRunId.get(runId)
368
- if (snapshot !== undefined) {
369
- // ② v1 快照档:快照全量在场,直接提 calls(W17~W1 中间档恢复)
370
- workflows.push({
371
- runId,
372
- stateFile: '',
373
- calls: extractCallSessionFiles(snapshot).map((sf) => sessionRefFromPath(sf, pathToRef)),
374
- })
375
- continue
376
232
  }
377
- // ③ 旧指针档:现状(link path → wf-state 文件)
378
- const link = tiers.linkByRunId.get(runId)
379
- if (link === undefined) continue
380
- const snap = await readRunSnapshot(link.path)
381
- const sessionFiles = snap === undefined ? [] : extractCallSessionFiles(snap)
233
+ const sessionFiles = streamContent === undefined ? [] : extractRecordStreamSessionFiles(streamContent)
382
234
  workflows.push({
383
- runId: link.runId,
384
- stateFile: link.path,
235
+ runId,
236
+ stateFile: recordPath,
385
237
  calls: sessionFiles.map((sf) => sessionRefFromPath(sf, pathToRef)),
386
238
  })
387
239
  }
@@ -1129,7 +1129,7 @@ async function renderSingleWorkflowRun(
1129
1129
  contentParts: string[]
1130
1130
  },
1131
1131
  ): Promise<void> {
1132
- // [D16③] 概览链换源:v2 档的 stateFile = record 流路径(journalPath 锚点语义
1132
+ // [D16③] 概览链换源:v2 档的 stateFile = record 流路径(recordPath 锚点语义
1133
1133
  // 重定义)——直读流经 parseRunRecordStream 概览([D2] 三态 status;空流/读失败
1134
1134
  // 投影 running,活跃 run 概览不退化为 skipped——红线)。record 后缀以外的
1135
1135
  // stateFile(v1 快照档空串 / 旧指针档 link path)走原快照链。
@@ -19,9 +19,9 @@ const SUBAGENT_RECORD_ENTRY_V2 = 2
19
19
 
20
20
  /**
21
21
  * 目标 sa-id 的 zcode 形态 subagent-record data:type/customType/版本关/id/engine
22
- * 全过才返回。版本关(W1 v2 学习,entry-anchor.ts zcodeAnchorOfEntry 同口径):
23
- * v1 全量快照(`d.v === 1`,兼容读面)或 v2 终态条(`d.v === 2` ∧
24
- * `d.kind === 'settled'`——engineHandle 只在终态条,registered 不携带不归因)。
22
+ * 全过才返回。版本关(与 entry-anchor.ts zcodeAnchorOfEntry 同口径):
23
+ * v2 终态条(`d.v === 2` ∧ `d.kind === 'settled'`——engineHandle 只在终态条,
24
+ * registered 不携带不归因;v1 全量快照形态已随兼容层删除,2026-09-30 登记 §3.3)。
25
25
  * engine 判别(D5)在此——`d.engine !== 'zcode'` 的记录(pi 形态:
26
26
  * engine 缺省、sessionRef 无 dbPath)不是「zcode 锚不完整」,返回 undefined 跳过
27
27
  * 不归因:误归因 missing-dbPath 会把「pi record 先于 manifest settle 落盘」的正常
@@ -34,11 +34,7 @@ function zcodeRecordDataOf(entry: Entry, saId: string): Record<string, unknown>
34
34
  if (typeof data !== 'object' || data === null) return undefined
35
35
  const d = data as Record<string, unknown>
36
36
  if (d.id !== saId) return undefined
37
- if (d.v === SUBAGENT_RECORD_ENTRY_V2) {
38
- if (d.kind !== 'settled') return undefined
39
- } else if (d.v !== 1) {
40
- return undefined
41
- }
37
+ if (d.v !== SUBAGENT_RECORD_ENTRY_V2 || d.kind !== 'settled') return undefined
42
38
  if (d.engine !== 'zcode') return undefined
43
39
  return d
44
40
  }