@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.
@@ -1570,66 +1570,6 @@ async function wfStateFile(
1570
1570
  return path
1571
1571
  }
1572
1572
 
1573
- /** 向 main session 追加 workflow-state-link custom entry(resolveWorkflows 的输入)。 */
1574
- async function wfLink(
1575
- dir: string,
1576
- slug: string,
1577
- id: string,
1578
- link: { runId: string; path: string },
1579
- ): Promise<void> {
1580
- const sessionPath = join(dir, 'sessions', slug, `${id}.jsonl`)
1581
- const line = JSON.stringify({
1582
- type: 'custom',
1583
- id: `wf-link-${link.runId}`,
1584
- parentId: id,
1585
- customType: 'workflow-state-link',
1586
- data: { runId: link.runId, path: link.path, updatedAt: '2026-08-12T00:00:00Z' },
1587
- timestamp: '2026-08-12T00:00:00Z',
1588
- })
1589
- await writeFile(sessionPath, line + '\n', { flag: 'a' })
1590
- }
1591
-
1592
- /** 构造 NEW 格式 wf-state 快照 JSON 行(calls 含 sessionId/sessionFile,parseRunSnapshot 透传)。 */
1593
- function wfSnapshotNew(
1594
- runId: string,
1595
- calls: Array<{ sessionId: string; sessionFile: string; description?: string }>,
1596
- ): string {
1597
- return JSON.stringify({
1598
- v: 'wf-run-v1',
1599
- runId,
1600
- spec: { scriptName: 'test-wf', name: 'Test' },
1601
- state: {
1602
- status: 'done',
1603
- reason: 'completed',
1604
- budget: {
1605
- usedTokens: 100,
1606
- usedCost: 0,
1607
- totalCallCount: calls.length,
1608
- maxTokens: 10000,
1609
- },
1610
- calls: calls.map((c, i) => ({
1611
- id: i,
1612
- opts: {
1613
- prompt: 'do work',
1614
- model: 'test-model',
1615
- description: c.description ?? `step-${i}`,
1616
- },
1617
- status: 'done',
1618
- attempts: 1,
1619
- result: {
1620
- content: 'ok',
1621
- durationMs: 100,
1622
- sessionId: c.sessionId,
1623
- sessionFile: c.sessionFile,
1624
- },
1625
- sessionId: c.sessionId,
1626
- sessionFile: c.sessionFile,
1627
- })),
1628
- },
1629
- meta: { startedAt: '2026-01-01T00:00:00Z', completedAt: '2026-01-01T00:01:00Z' },
1630
- })
1631
- }
1632
-
1633
1573
  describe('doWorkflow(w6,fixture)', () => {
1634
1574
  let dir: string
1635
1575
 
@@ -1640,9 +1580,9 @@ describe('doWorkflow(w6,fixture)', () => {
1640
1580
  await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 })
1641
1581
  })
1642
1582
 
1643
- // ---- [D16③] v2 record 流档概览(journalPath 锚点 = record 流路径)----
1583
+ // ---- [D16③] v2 record 流档概览(recordPath 锚点 = record 流路径)----
1644
1584
 
1645
- /** 向 main session 追加 workflow-record v2 注册条目行(journalPath 锚点指向 record 流)。 */
1585
+ /** 向 main session 追加 workflow-record v2 注册条目行(recordPath 锚点指向 record 流)。 */
1646
1586
  async function wfV2Registered(
1647
1587
  dir: string,
1648
1588
  slug: string,
@@ -1656,12 +1596,36 @@ describe('doWorkflow(w6,fixture)', () => {
1656
1596
  id: `wf-reg-${runId}`,
1657
1597
  parentId: id,
1658
1598
  customType: 'workflow-record',
1659
- data: { v: 2, kind: 'registered', runId, workflowName: 'rec-flow', scriptName: 'rec-flow', slug: 'rec-flow', startedAt: 1758000000000, journalPath: recordPath },
1599
+ data: { v: 2, kind: 'registered', runId, workflowName: 'rec-flow', scriptName: 'rec-flow', slug: 'rec-flow', startedAt: 1758000000000, recordPath: recordPath },
1660
1600
  timestamp: '2026-09-28T00:00:00Z',
1661
1601
  })
1662
1602
  await writeFile(sessionPath, line + '\n', { flag: 'a' })
1663
1603
  }
1664
1604
 
1605
+ /** 构造 v2 record 流文件(run-created + agent-settled[+终局] 帧提 calls)并写注册条目——
1606
+ * [ADR-0095] v1 读面删除后的单档 fixture 形态(替代 wf-state 快照 + link 指针)。 */
1607
+ async function wfV2Run(
1608
+ dir: string,
1609
+ slug: string,
1610
+ id: string,
1611
+ runId: string,
1612
+ calls: Array<{ sessionId: string; sessionFile: string }>,
1613
+ opts: { settled?: boolean } = {},
1614
+ ): Promise<string> {
1615
+ const lines = [
1616
+ JSON.stringify({ type: 'run-created', seq: 1, ts: 1758000000000, runId, workflowName: 'test-wf', argsSummary: '{}' }),
1617
+ ...calls.map((c, i) =>
1618
+ JSON.stringify({ type: 'agent-settled', seq: 2 + i, ts: 1758000001000 + i, taskIndex: i, attempt: 1, outcome: 'done', durationMs: 100, result: { content: 'ok', sessionId: c.sessionId, sessionFile: c.sessionFile } }),
1619
+ ),
1620
+ ]
1621
+ if (opts.settled) {
1622
+ lines.push(JSON.stringify({ type: 'run-settled', seq: 2 + calls.length, ts: 1758000002000, outcome: 'done' }))
1623
+ }
1624
+ const recordPath = await wfStateFile(dir, slug, `${runId}.record.jsonl`, lines)
1625
+ await wfV2Registered(dir, slug, id, runId, recordPath)
1626
+ return recordPath
1627
+ }
1628
+
1665
1629
  it('[D16③] v2 档 record 流概览:注册条目锚点直读流——steps 提 sessionFile、status 三态、不进 skippedRuns(活跃 run 红线)', async () => {
1666
1630
  const slug = '--wf-record--'
1667
1631
  await wfMainSession(dir, slug, WF_ROOT)
@@ -1726,12 +1690,9 @@ describe('doWorkflow(w6,fixture)', () => {
1726
1690
  const slug = '--wf-single--'
1727
1691
  await wfMainSession(dir, slug, WF_ROOT)
1728
1692
  const callSession = join(dir, 'sessions', slug, `${WF_CALL}.jsonl`)
1729
- const wfPath = await wfStateFile(dir, slug, 'wf-single.jsonl', [
1730
- wfSnapshotNew('wf-single-1', [
1731
- { sessionId: WF_CALL, sessionFile: callSession, description: 'probe-step' },
1732
- ]),
1733
- ])
1734
- await wfLink(dir, slug, WF_ROOT, { runId: 'wf-single-1', path: wfPath })
1693
+ await wfV2Run(dir, slug, WF_ROOT, 'wf-single-1', [
1694
+ { sessionId: WF_CALL, sessionFile: callSession },
1695
+ ], { settled: true })
1735
1696
 
1736
1697
  const r = await handleSessionRead({ action: 'workflow', session: WF_ROOT }, { agentDir: dir })
1737
1698
  const d = r.details as {
@@ -1762,14 +1723,8 @@ describe('doWorkflow(w6,fixture)', () => {
1762
1723
  it('TC-w6-multi-run:多 run 拼接,content 含两段 overview,runs.length===2', async () => {
1763
1724
  const slug = '--wf-multi--'
1764
1725
  await wfMainSession(dir, slug, WF_ROOT)
1765
- const wf1 = await wfStateFile(dir, slug, 'wf-1.jsonl', [
1766
- wfSnapshotNew('wf-multi-1', [{ sessionId: WF_CALL, sessionFile: '/abs/a.jsonl' }]),
1767
- ])
1768
- const wf2 = await wfStateFile(dir, slug, 'wf-2.jsonl', [
1769
- wfSnapshotNew('wf-multi-2', [{ sessionId: WF_CALL, sessionFile: '/abs/b.jsonl' }]),
1770
- ])
1771
- await wfLink(dir, slug, WF_ROOT, { runId: 'wf-multi-1', path: wf1 })
1772
- await wfLink(dir, slug, WF_ROOT, { runId: 'wf-multi-2', path: wf2 })
1726
+ await wfV2Run(dir, slug, WF_ROOT, 'wf-multi-1', [{ sessionId: WF_CALL, sessionFile: '/abs/a.jsonl' }], { settled: true })
1727
+ await wfV2Run(dir, slug, WF_ROOT, 'wf-multi-2', [{ sessionId: WF_CALL, sessionFile: '/abs/b.jsonl' }], { settled: true })
1773
1728
 
1774
1729
  const r = await handleSessionRead({ action: 'workflow', session: WF_ROOT }, { agentDir: dir })
1775
1730
  const d = r.details as { runs: Array<{ runId: string }>; runIds: string[] }
@@ -1785,14 +1740,8 @@ describe('doWorkflow(w6,fixture)', () => {
1785
1740
  it('TC-w6-runid-filter:runId 过滤命中单 run', async () => {
1786
1741
  const slug = '--wf-filter--'
1787
1742
  await wfMainSession(dir, slug, WF_ROOT)
1788
- const wf1 = await wfStateFile(dir, slug, 'wf-1.jsonl', [
1789
- wfSnapshotNew('wf-filter-1', [{ sessionId: WF_CALL, sessionFile: '/abs/a.jsonl' }]),
1790
- ])
1791
- const wf2 = await wfStateFile(dir, slug, 'wf-2.jsonl', [
1792
- wfSnapshotNew('wf-filter-2', [{ sessionId: WF_CALL, sessionFile: '/abs/b.jsonl' }]),
1793
- ])
1794
- await wfLink(dir, slug, WF_ROOT, { runId: 'wf-filter-1', path: wf1 })
1795
- await wfLink(dir, slug, WF_ROOT, { runId: 'wf-filter-2', path: wf2 })
1743
+ await wfV2Run(dir, slug, WF_ROOT, 'wf-filter-1', [{ sessionId: WF_CALL, sessionFile: '/abs/a.jsonl' }], { settled: true })
1744
+ await wfV2Run(dir, slug, WF_ROOT, 'wf-filter-2', [{ sessionId: WF_CALL, sessionFile: '/abs/b.jsonl' }], { settled: true })
1796
1745
 
1797
1746
  const r = await handleSessionRead(
1798
1747
  { action: 'workflow', session: WF_ROOT, runId: 'wf-filter-1' },
@@ -1814,14 +1763,8 @@ describe('doWorkflow(w6,fixture)', () => {
1814
1763
  it('TC-w6-runid-not-found:runId 无匹配→ES-wf-runid-not-found(列候选+👉,不抛错)', async () => {
1815
1764
  const slug = '--wf-notfound--'
1816
1765
  await wfMainSession(dir, slug, WF_ROOT)
1817
- const wf1 = await wfStateFile(dir, slug, 'wf-1.jsonl', [
1818
- wfSnapshotNew('wf-nf-1', [{ sessionId: WF_CALL, sessionFile: '/abs/a.jsonl' }]),
1819
- ])
1820
- const wf2 = await wfStateFile(dir, slug, 'wf-2.jsonl', [
1821
- wfSnapshotNew('wf-nf-2', [{ sessionId: WF_CALL, sessionFile: '/abs/b.jsonl' }]),
1822
- ])
1823
- await wfLink(dir, slug, WF_ROOT, { runId: 'wf-nf-1', path: wf1 })
1824
- await wfLink(dir, slug, WF_ROOT, { runId: 'wf-nf-2', path: wf2 })
1766
+ await wfV2Run(dir, slug, WF_ROOT, 'wf-nf-1', [{ sessionId: WF_CALL, sessionFile: '/abs/a.jsonl' }], { settled: true })
1767
+ await wfV2Run(dir, slug, WF_ROOT, 'wf-nf-2', [{ sessionId: WF_CALL, sessionFile: '/abs/b.jsonl' }], { settled: true })
1825
1768
 
1826
1769
  const r = await handleSessionRead(
1827
1770
  { action: 'workflow', session: WF_ROOT, runId: 'wf-nonexist' },
@@ -1855,45 +1798,16 @@ describe('doWorkflow(w6,fixture)', () => {
1855
1798
  expect(text).toContain('family')
1856
1799
  })
1857
1800
 
1858
- it('TC-w6-snapshot-skip:run1 wf-state 不存在→跳过,run2 正常(ES-wf-snapshot-read-fail)', async () => {
1859
- const slug = '--wf-skip--'
1860
- await wfMainSession(dir, slug, WF_ROOT)
1861
- // run1 的 wf-state 文件不存在(link 指向不存在路径,模拟 GC)
1862
- const ghostPath = join(dir, 'sessions', slug, 'workflow-state', 'wf-ghost.jsonl')
1863
- const wf2 = await wfStateFile(dir, slug, 'wf-2.jsonl', [
1864
- wfSnapshotNew('wf-skip-2', [{ sessionId: WF_CALL, sessionFile: '/abs/b.jsonl' }]),
1865
- ])
1866
- await wfLink(dir, slug, WF_ROOT, { runId: 'wf-skip-1', path: ghostPath })
1867
- await wfLink(dir, slug, WF_ROOT, { runId: 'wf-skip-2', path: wf2 })
1868
-
1869
- const r = await handleSessionRead({ action: 'workflow', session: WF_ROOT }, { agentDir: dir })
1870
- const d = r.details as {
1871
- runs: Array<{ runId: string }>
1872
- runIds: string[]
1873
- skippedRuns?: Array<{ runId: string; stateFile: string; reason: string }>
1874
- }
1875
- expect(d.runs).toHaveLength(1)
1876
- expect(d.runs[0].runId).toBe('wf-skip-2')
1877
- expect(d.runIds).toEqual(['wf-skip-2'])
1878
- expect(d.skippedRuns).toBeDefined()
1879
- expect(d.skippedRuns).toHaveLength(1)
1880
- expect(d.skippedRuns![0].runId).toBe('wf-skip-1')
1881
- expect(d.skippedRuns![0].reason).toBe('snapshot-unreadable')
1882
- const text = r.content[0].text
1883
- expect(text).toContain('wf-skip-1')
1884
- expect(text).toContain('已跳过')
1885
- expect(text).toContain('run: wf-skip-2')
1886
- })
1801
+ // [ADR-0095] TC-w6-snapshot-skip(快照不可读 → skippedRuns)随 v1 读面删除:
1802
+ // 发现链收敛 v2 后 run 一律 record 流直读(读失败 → running 兜底不 skipped),
1803
+ // skippedRuns 快照链对 v2 档不可达。
1887
1804
 
1888
1805
  it('TC-w6-call-jump:workflow 概览 call sessionId 可被 resolveSessionId 深读(outline 跳转,§7 场景 2)', async () => {
1889
1806
  const slug = '--wf-jump--'
1890
1807
  await wfMainSession(dir, slug, WF_ROOT)
1891
1808
  // 真实存在的 call session(main session 形态,findSessions 可匹配)
1892
1809
  const callPath = await wfMainSession(dir, slug, WF_CALL, { cwd: '/proj/call' })
1893
- const wfPath = await wfStateFile(dir, slug, 'wf-jump.jsonl', [
1894
- wfSnapshotNew('wf-jump-1', [{ sessionId: WF_CALL, sessionFile: callPath }]),
1895
- ])
1896
- await wfLink(dir, slug, WF_ROOT, { runId: 'wf-jump-1', path: wfPath })
1810
+ await wfV2Run(dir, slug, WF_ROOT, 'wf-jump-1', [{ sessionId: WF_CALL, sessionFile: callPath }], { settled: true })
1897
1811
 
1898
1812
  // 第一次:workflow 概览,拿 call sessionId
1899
1813
  const rWf = await handleSessionRead({ action: 'workflow', session: WF_ROOT }, { agentDir: dir })
@@ -1925,21 +1839,21 @@ describe('doWorkflow(w6,fixture)', () => {
1925
1839
  JSON.stringify({ type: 'session', id: SUB_WF_ROOT, cwd: `/proj/${slug}` }) + '\n',
1926
1840
  )
1927
1841
  const callSession = join(dir, 'sessions', slug, `${WF_CALL}.jsonl`)
1928
- const wfPath = await wfStateFile(dir, slug, 'wf-sub.jsonl', [
1929
- wfSnapshotNew('wf-sub-1', [
1930
- { sessionId: WF_CALL, sessionFile: callSession, description: 'sub-step' },
1931
- ]),
1842
+ // subagent session 的 v2 注册条目([ADR-0095] v1 link 形态删除;record 流直读同源)
1843
+ const recordPath = await wfStateFile(dir, slug, 'wf-sub-1.record.jsonl', [
1844
+ JSON.stringify({ type: 'run-created', seq: 1, ts: 1758000000000, runId: 'wf-sub-1', workflowName: 'test-wf', argsSummary: '{}' }),
1845
+ JSON.stringify({ type: 'agent-settled', seq: 2, ts: 1758000001000, taskIndex: 0, attempt: 1, outcome: 'done', durationMs: 100, result: { content: 'ok', sessionId: WF_CALL, sessionFile: callSession } }),
1846
+ JSON.stringify({ type: 'run-settled', seq: 3, ts: 1758000002000, outcome: 'done' }),
1932
1847
  ])
1933
- // subagent session 的 workflow-state-link(wfLink helper 只写 sessions/,此处直接追加)
1934
1848
  await writeFile(
1935
1849
  subPath,
1936
1850
  JSON.stringify({
1937
1851
  type: 'custom',
1938
- id: 'wf-link-wf-sub-1',
1852
+ id: 'wf-reg-wf-sub-1',
1939
1853
  parentId: SUB_WF_ROOT,
1940
- customType: 'workflow-state-link',
1941
- data: { runId: 'wf-sub-1', path: wfPath, updatedAt: '2026-08-12T00:00:00Z' },
1942
- timestamp: '2026-08-12T00:00:00Z',
1854
+ customType: 'workflow-record',
1855
+ data: { v: 2, kind: 'registered', runId: 'wf-sub-1', workflowName: 'test-wf', scriptName: 'test-wf', slug: 'wf-sub', startedAt: 1758000000000, recordPath },
1856
+ timestamp: '2026-09-28T00:00:00Z',
1943
1857
  }) + '\n',
1944
1858
  { flag: 'a' },
1945
1859
  )
@@ -6,12 +6,10 @@ import { join } from 'node:path'
6
6
  import {
7
7
  WORKFLOW_RECORD_CUSTOM_TYPE,
8
8
  WORKFLOW_RECORD_ENTRY_VERSION,
9
- WORKFLOW_STATE_LINK_CUSTOM_TYPE,
10
9
  } from '@zhushanwen/subagent-core'
11
10
 
12
11
  import { parseRunSnapshot, renderWorkflowOverview } from '../core/workflow.js'
13
12
  import {
14
- extractCallSessionFiles,
15
13
  extractRecordStreamSessionFiles,
16
14
  readRunSnapshot,
17
15
  resolveWorkflows,
@@ -215,54 +213,6 @@ describe('parseRunSnapshot', () => {
215
213
  })
216
214
  })
217
215
 
218
- // ============================================================
219
- // extractCallSessionFiles(纯逻辑,family/workflows 腿的 sessionFile 提取入口)
220
- // ============================================================
221
-
222
- describe('extractCallSessionFiles', () => {
223
- it('v2 快照(wf-run-v2)state.calls 的 sessionFile 能被读出,不因版本字面量挡读', () => {
224
- const files = extractCallSessionFiles(V2_SNAPSHOT_FIXTURE)
225
- // 顶层 sessionFile + result.sessionFile 回退,两个 call 都读出
226
- expect(files).toEqual(['/abs/v2-call0.jsonl', '/abs/v2-call1.jsonl'])
227
- })
228
-
229
- it('v1 快照(wf-run-v1)行为不变:state.calls 提取', () => {
230
- const files = extractCallSessionFiles(NEW_SNAPSHOT_FIXTURE)
231
- expect(files).toEqual(['/abs/session.jsonl'])
232
- })
233
-
234
- it('OLD 快照(无 v,callCache)从 value 顶层与 value.result 提取 sessionFile', () => {
235
- const files = extractCallSessionFiles({
236
- callCache: [
237
- { key: 0, value: { sessionFile: '/abs/old-call0.jsonl' } },
238
- { key: 1, value: { result: { sessionFile: '/abs/old-call1.jsonl' } } },
239
- ],
240
- })
241
- expect(files).toEqual(['/abs/old-call0.jsonl', '/abs/old-call1.jsonl'])
242
- })
243
-
244
- it('OLD 快照 value 无 sessionFile / value 非对象 → 跳过该 call,不产出路径', () => {
245
- // 对齐 OLD_SNAPSHOT_FIXTURE 形状(旧 pi 不持久化 sessionFile)+ value 非对象脏数据回退 call 本身
246
- const files = extractCallSessionFiles({
247
- callCache: [
248
- { key: 7, value: { content: '', usage: { input: 0 } } },
249
- { key: 8, value: 42 },
250
- { key: 9, sessionFile: '/abs/bare-call.jsonl' },
251
- ],
252
- })
253
- expect(files).toEqual(['/abs/bare-call.jsonl'])
254
- })
255
-
256
- it('快照非对象 / calls 容器缺失 / call 元素非对象 → 空数组', () => {
257
- expect(extractCallSessionFiles(null)).toEqual([])
258
- expect(extractCallSessionFiles('wf-run-v1')).toEqual([])
259
- expect(extractCallSessionFiles({ v: 'wf-run-v1', state: null })).toEqual([])
260
- expect(extractCallSessionFiles({ v: 'wf-run-v1', state: {} })).toEqual([])
261
- expect(extractCallSessionFiles({ callCache: 'not-an-array' })).toEqual([])
262
- expect(extractCallSessionFiles({ callCache: [null, 7, 'x'] })).toEqual([])
263
- })
264
- })
265
-
266
216
  // ============================================================
267
217
  // renderWorkflowOverview(纯逻辑,TC-w5-render-new/old)
268
218
  // ============================================================
@@ -377,33 +327,11 @@ describe('readRunSnapshot', () => {
377
327
  })
378
328
 
379
329
  // ============================================================
380
- // resolveWorkflows 三档发现链(W1 D10 断链修复:v2 journalPath 主源 / v1 快照层 /
381
- // 旧指针 fallback——W17 前 / W17~W1 / W1+ 三类 run 全部有发现通道)
330
+ // resolveWorkflows 发现链([ADR-0095] v1 读面删除后的单档形态:v2 注册条目
331
+ // recordPath 主源;历史格式条目——v1 快照 / workflow-state-link 指针——静默消失)
382
332
  // ============================================================
383
333
 
384
- /** v1 快照对象(RunSnapshot 形态:v/runId/state.calls——v1 workflow-record 条目的 snapshot 载荷)。 */
385
- function v1Snapshot(runId: string, sessionFiles: string[]): Record<string, unknown> {
386
- return {
387
- v: 'wf-run-v2',
388
- runId,
389
- spec: { scriptName: 'mid-window', name: 'Mid' },
390
- state: {
391
- status: 'done',
392
- reason: 'completed',
393
- budget: { usedTokens: 10, usedCost: 0, totalCallCount: sessionFiles.length, maxTokens: 1000 },
394
- calls: sessionFiles.map((sf, i) => ({
395
- id: i,
396
- opts: { prompt: 'work', model: 'm', description: `step-${i}` },
397
- status: 'done',
398
- attempts: 1,
399
- result: { content: 'ok', durationMs: 1, sessionId: `019v1${i}`, sessionFile: sf },
400
- })),
401
- },
402
- meta: { startedAt: '2026-09-20T00:00:00.000Z', completedAt: '2026-09-20T00:01:00.000Z' },
403
- }
404
- }
405
-
406
- describe('resolveWorkflows 三档发现链(W1 D10)', () => {
334
+ describe('resolveWorkflows(v2 注册条目单档发现链)', () => {
407
335
  let dir: string
408
336
  afterEach(async () => {
409
337
  if (dir) await rm(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 }).catch(() => {})
@@ -420,15 +348,6 @@ describe('resolveWorkflows 三档发现链(W1 D10)', () => {
420
348
  return { path, sessionIdToPath: new Map([[sessionId, path]]) }
421
349
  }
422
350
 
423
- /** 写 wf-state 快照文件(尾行最新快照)。返回绝对路径。 */
424
- async function writeStateFile(runId: string, snapshot: Record<string, unknown>): Promise<string> {
425
- const wfDir = join(dir, 'workflow-state')
426
- await mkdir(wfDir, { recursive: true })
427
- const path = join(wfDir, `${runId}.jsonl`)
428
- await writeFile(path, JSON.stringify(snapshot) + '\n')
429
- return path
430
- }
431
-
432
351
  /** 写 record 流文件([D16③] v2 档数据源——目录自建)。 */
433
352
  async function writeRecordStream(runId: string, lines: string[]): Promise<string> {
434
353
  const wfDir = join(dir, 'workflow-state')
@@ -438,8 +357,8 @@ describe('resolveWorkflows 三档发现链(W1 D10)', () => {
438
357
  return p
439
358
  }
440
359
 
441
- /** workflow-record v2 注册条目行(journalPath 锚点)。 */
442
- function v2RegisteredLine(runId: string, journalPath: string): string {
360
+ /** workflow-record v2 注册条目行(recordPath 锚点)。 */
361
+ function v2RegisteredLine(runId: string, recordPath: string): string {
443
362
  return JSON.stringify({
444
363
  type: 'custom',
445
364
  customType: 'workflow-record',
@@ -453,23 +372,12 @@ describe('resolveWorkflows 三档发现链(W1 D10)', () => {
453
372
  scriptName: 'probe-flow',
454
373
  slug: 'probe',
455
374
  startedAt: 1,
456
- journalPath,
375
+ recordPath,
457
376
  },
458
377
  })
459
378
  }
460
379
 
461
- /** workflow-record v1 快照条目行(W17~W1 中间档形态:{v:1, snapshot, updatedAt})。 */
462
- function v1SnapshotLine(runId: string, sessionFiles: string[]): string {
463
- return JSON.stringify({
464
- type: 'custom',
465
- customType: 'workflow-record',
466
- id: `e-${runId}`,
467
- parentId: null,
468
- data: { v: 1, snapshot: v1Snapshot(runId, sessionFiles), updatedAt: 2 },
469
- })
470
- }
471
-
472
- /** workflow-record v2 终态条目行(不携带 journalPath——非发现链数据源)。 */
380
+ /** workflow-record v2 终态条目行(不携带 recordPath——非发现链数据源)。 */
473
381
  function v2SettledLine(runId: string): string {
474
382
  return JSON.stringify({
475
383
  type: 'custom',
@@ -504,7 +412,7 @@ describe('resolveWorkflows 三档发现链(W1 D10)', () => {
504
412
  const CALL_A = '/abs/subagents/--proj--/sessions/call-a.jsonl'
505
413
  const CALL_B = '/abs/subagents/--proj--/sessions/call-b.jsonl'
506
414
 
507
- it('v2 journalPath 主源([D16③] 重锚):注册条目 → record 流直读提 calls,stateFile = 流路径', async () => {
415
+ it('v2 recordPath 主源([D16③] 重锚):注册条目 → record 流直读提 calls,stateFile = 流路径', async () => {
508
416
  dir = await mkdtemp(join(tmpdir(), 'wf-v2-tier-'))
509
417
  // record 流内容:run-created + agent-started + agent-settled(result 携带 sessionFile)
510
418
  const recordPath = await writeRecordStream('wf-v2-1', [
@@ -518,7 +426,7 @@ describe('resolveWorkflows 三档发现链(W1 D10)', () => {
518
426
 
519
427
  expect(workflows).toHaveLength(1)
520
428
  expect(workflows[0].runId).toBe('wf-v2-1')
521
- // stateFile = record 流路径(journalPath 锚点语义重定义——[D16③])
429
+ // stateFile = record 流路径(recordPath 锚点语义重定义——[D16③])
522
430
  expect(workflows[0].stateFile).toBe(recordPath)
523
431
  expect(workflows[0].calls).toHaveLength(1)
524
432
  expect(workflows[0].calls[0].fileName).toBe(CALL_A)
@@ -560,58 +468,79 @@ describe('resolveWorkflows 三档发现链(W1 D10)', () => {
560
468
  expect(odd[0].calls).toEqual([])
561
469
  })
562
470
 
563
- it('v1 快照层(W17~W1 中间档恢复):v1 快照条目直接提 snapshot.calls,无指针条目也可见', async () => {
564
- // 该档现状发现链恒空(只认指针)——本用例锚定中间档恢复:升级窗口期的旧格式会话
565
- dir = await mkdtemp(join(tmpdir(), 'wf-v1-tier-'))
566
- const { sessionIdToPath } = await writeMain([v1SnapshotLine('wf-mid-1', [CALL_A, CALL_B])])
567
-
568
- const workflows = await resolveWorkflows('main-session-1', sessionIdToPath, new Map())
569
-
570
- expect(workflows).toHaveLength(1)
571
- expect(workflows[0].runId).toBe('wf-mid-1')
572
- // v1 条目不携带 state 路径 → stateFile 空串(概览消费面自然降级,家族链 calls 完整)
573
- expect(workflows[0].stateFile).toBe('')
574
- expect(workflows[0].calls.map((c) => c.fileName)).toEqual([CALL_A, CALL_B])
575
- })
576
-
577
- it('旧指针 fallback:workflow-state-link 指针条目照旧读 link 指向的快照文件', async () => {
578
- dir = await mkdtemp(join(tmpdir(), 'wf-link-tier-'))
579
- const stateFile = await writeStateFile('wf-old-1', v1Snapshot('wf-old-1', [CALL_B]))
580
- const { sessionIdToPath } = await writeMain([linkLine('wf-old-1', stateFile)])
471
+ // [ADR-0095] 删除回归:历史格式条目(v1 全量快照 / workflow-state-link 指针)
472
+ // 不再识别——静默从发现链消失(不拒读、不报错),历史 run 退空即预期行为。
473
+ it('历史格式条目(v1 快照 / state-link 指针)静默消失——不产 WorkflowRef、不影响 v2 条目', async () => {
474
+ dir = await mkdtemp(join(tmpdir(), 'wf-legacy-silent-'))
475
+ const recordPath = await writeRecordStream('wf-v2-live', [
476
+ JSON.stringify({ type: 'run-created', seq: 1, ts: 1000, runId: 'wf-v2-live', workflowName: 'v2-flow', argsSummary: '{}' }),
477
+ JSON.stringify({ type: 'agent-settled', seq: 2, ts: 1500, taskIndex: 0, attempt: 1, outcome: 'done', durationMs: 400, result: { content: 'ok', sessionFile: CALL_A } }),
478
+ ])
479
+ // 首见序:v1 快照条目(早)→ link 指针条目(中)→ v2 注册条目(晚)
480
+ const { sessionIdToPath } = await writeMain([
481
+ JSON.stringify({
482
+ type: 'custom',
483
+ customType: 'workflow-record',
484
+ id: 'e-v1-mid',
485
+ parentId: null,
486
+ data: {
487
+ v: 1,
488
+ snapshot: {
489
+ v: 'wf-run-v2',
490
+ runId: 'wf-mid-1',
491
+ spec: { scriptName: 'mid-window' },
492
+ state: {
493
+ status: 'done',
494
+ budget: { usedTokens: 10, usedCost: 0, totalCallCount: 1, maxTokens: 1000 },
495
+ calls: [{ id: 0, status: 'done', result: { sessionFile: CALL_B } }],
496
+ },
497
+ meta: { startedAt: '2026-09-20T00:00:00.000Z' },
498
+ },
499
+ updatedAt: 2,
500
+ },
501
+ }),
502
+ JSON.stringify({
503
+ type: 'custom',
504
+ customType: 'workflow-state-link',
505
+ id: 'e-link-old',
506
+ parentId: null,
507
+ data: { runId: 'wf-old-1', path: '/abs/wf-old-1.jsonl', updatedAt: '2026-08-07T16:48:24.933Z' },
508
+ }),
509
+ v2RegisteredLine('wf-v2-live', recordPath),
510
+ ])
581
511
 
582
512
  const workflows = await resolveWorkflows('main-session-1', sessionIdToPath, new Map())
583
513
 
514
+ // 只剩 v2 档一条;v1/link 条目不产 runId 幻影
584
515
  expect(workflows).toHaveLength(1)
585
- expect(workflows[0].runId).toBe('wf-old-1')
586
- expect(workflows[0].stateFile).toBe(stateFile)
587
- expect(workflows[0].calls.map((c) => c.fileName)).toEqual([CALL_B])
516
+ expect(workflows[0].runId).toBe('wf-v2-live')
517
+ expect(workflows[0].stateFile).toBe(recordPath)
518
+ expect(workflows[0].calls.map((c) => c.fileName)).toEqual([CALL_A])
588
519
  })
589
520
 
590
- it('三档混合会话:三个 run 各处一档 → 各自正确,按条目首见序输出([D16③] v2 档 = record 流直读)', async () => {
591
- dir = await mkdtemp(join(tmpdir(), 'wf-mixed-'))
592
- const recordPath = await writeRecordStream('wf-v2-1', [
593
- JSON.stringify({ type: 'run-created', seq: 1, ts: 1000, runId: 'wf-v2-1', workflowName: 'v2-flow', argsSummary: '{}' }),
594
- JSON.stringify({ type: 'agent-started', seq: 2, ts: 1100, taskIndex: 0, agentName: 's0', attempt: 1 }),
595
- JSON.stringify({ type: 'agent-settled', seq: 3, ts: 1500, taskIndex: 0, attempt: 1, outcome: 'done', durationMs: 400, result: { content: 'ok', sessionFile: CALL_A } }),
521
+ it('同 runId 多条 v2 注册条目:后写覆盖(取最新锚点),首见序输出', async () => {
522
+ dir = await mkdtemp(join(tmpdir(), 'wf-rewrite-'))
523
+ const firstPath = await writeRecordStream('wf-rewrite-1', [
524
+ JSON.stringify({ type: 'run-created', seq: 1, ts: 1000, runId: 'wf-rewrite-1', workflowName: 'v1-flow', argsSummary: '{}' }),
525
+ ])
526
+ const secondPath = await writeRecordStream('wf-rewrite-1-retry', [
527
+ JSON.stringify({ type: 'run-created', seq: 1, ts: 2000, runId: 'wf-rewrite-1', workflowName: 'v2-flow', argsSummary: '{}' }),
528
+ JSON.stringify({ type: 'agent-settled', seq: 2, ts: 2500, taskIndex: 0, attempt: 1, outcome: 'done', durationMs: 300, result: { content: 'ok', sessionFile: CALL_A } }),
596
529
  ])
597
- const oldState = await writeStateFile('wf-old-1', v1Snapshot('wf-old-1', [CALL_B]))
598
- // 首见序:指针(早)→ v1 快照(中)→ v2 注册(晚)
599
530
  const { sessionIdToPath } = await writeMain([
600
- linkLine('wf-old-1', oldState),
601
- v1SnapshotLine('wf-mid-1', [CALL_A]),
602
- v2RegisteredLine('wf-v2-1', recordPath),
531
+ v2RegisteredLine('wf-rewrite-1', firstPath),
532
+ v2RegisteredLine('wf-rewrite-1', secondPath),
603
533
  ])
604
534
 
605
535
  const workflows = await resolveWorkflows('main-session-1', sessionIdToPath, new Map())
606
536
 
607
- expect(workflows.map((w) => w.runId)).toEqual(['wf-old-1', 'wf-mid-1', 'wf-v2-1'])
608
- expect(workflows[0].stateFile).toBe(oldState)
609
- expect(workflows[1].stateFile).toBe('')
610
- expect(workflows[2].stateFile).toBe(recordPath)
611
- expect(workflows[2].calls[0].fileName).toBe(CALL_A)
537
+ expect(workflows).toHaveLength(1)
538
+ // 后写覆盖:stateFile 取最新锚点,calls 来自新流
539
+ expect(workflows[0].stateFile).toBe(secondPath)
540
+ expect(workflows[0].calls.map((c) => c.fileName)).toEqual([CALL_A])
612
541
  })
613
542
 
614
- it('v2 终态条目(settled,无 journalPath)不参与发现链——不产 runId 幻影', async () => {
543
+ it('v2 终态条目(settled,无 recordPath)不参与发现链——不产 runId 幻影', async () => {
615
544
  dir = await mkdtemp(join(tmpdir(), 'wf-settled-'))
616
545
  const { sessionIdToPath } = await writeMain([v2SettledLine('wf-settled-1')])
617
546
 
@@ -620,34 +549,11 @@ describe('resolveWorkflows 三档发现链(W1 D10)', () => {
620
549
  expect(workflows).toEqual([])
621
550
  })
622
551
 
623
- it('同 runId 跨档坏数据防御:v2 注册条目在 → 用 v2 档(高档在即用高档;[D16③] v2 档 = record 流)', async () => {
624
- dir = await mkdtemp(join(tmpdir(), 'wf-tier-prio-'))
625
- const recordPath = await writeRecordStream('wf-both-1', [
626
- JSON.stringify({ type: 'run-created', seq: 1, ts: 1000, runId: 'wf-both-1', workflowName: 'both', argsSummary: '{}' }),
627
- JSON.stringify({ type: 'agent-started', seq: 2, ts: 1100, taskIndex: 0, agentName: 's0', attempt: 1 }),
628
- JSON.stringify({ type: 'agent-settled', seq: 3, ts: 1500, taskIndex: 0, attempt: 1, outcome: 'done', durationMs: 400, result: { content: 'ok', sessionFile: CALL_A } }),
629
- ])
630
- const oldState = await writeStateFile('wf-both-1-old', v1Snapshot('wf-both-1', [CALL_B]))
631
- const { sessionIdToPath } = await writeMain([
632
- linkLine('wf-both-1', oldState),
633
- v2RegisteredLine('wf-both-1', recordPath),
634
- ])
635
-
636
- const workflows = await resolveWorkflows('main-session-1', sessionIdToPath, new Map())
637
-
638
- expect(workflows).toHaveLength(1)
639
- // v2 档胜出:stateFile 是 record 流路径(非 link 的 oldState),calls 来自流
640
- expect(workflows[0].stateFile).toBe(recordPath)
641
- expect(workflows[0].calls.map((c) => c.fileName)).toEqual([CALL_A])
642
- })
643
-
644
- it('跨包契约守卫:三档判别的本地字面量 === subagent-core 写侧单源([D1] record 后缀)', () => {
552
+ it('跨包契约守卫:发现链判别的本地字面量 === subagent-core 写侧单源([D1] record 后缀)', () => {
645
553
  // 源码本地持有磁盘协议字符串与版本/尾段(生产依赖面不引 subagent-core),锚定两侧同值
646
554
  expect(WORKFLOW_RECORD_CUSTOM_TYPE).toBe('workflow-record')
647
555
  expect(WORKFLOW_RECORD_ENTRY_VERSION).toBe(2)
648
556
  expect(RUN_RECORD_STREAM_SUFFIX).toBe('.record.jsonl')
649
- // legacy 指针档:collectWorkflowEntryTiers 本地字面量与 core 常量同值
650
- expect(WORKFLOW_STATE_LINK_CUSTOM_TYPE).toBe('workflow-state-link')
651
557
  })
652
558
  })
653
559
 
@@ -65,7 +65,10 @@ function recordLine(saId: string, sessionRef: Record<string, string>): string {
65
65
  customType: 'subagent-record',
66
66
  id: `e-${saId}`,
67
67
  parentId: null,
68
- data: { v: 1, id: saId, engine: 'zcode', engineHandle: { sessionRef, poolKey: 'shared' } },
68
+ data: {
69
+ v: 2, kind: 'settled', id: saId, status: 'idle', stopReason: 'completed', endedAt: 2,
70
+ turns: 1, totalTokens: 10, engine: 'zcode', engineHandle: { sessionRef, poolKey: 'shared' },
71
+ },
69
72
  })
70
73
  }
71
74
 
@@ -201,7 +201,10 @@ function recordLine(entryId: string, saId: string, sessionRef: Record<string, st
201
201
  customType: 'subagent-record',
202
202
  id: entryId,
203
203
  parentId: null,
204
- data: { v: 1, id: saId, engine: 'zcode', engineHandle: { sessionRef, poolKey: 'shared' } },
204
+ data: {
205
+ v: 2, kind: 'settled', id: saId, status: 'idle', stopReason: 'completed', endedAt: BASE_MS + 1,
206
+ turns: 1, totalTokens: 10, engine: 'zcode', engineHandle: { sessionRef, poolKey: 'shared' },
207
+ },
205
208
  })
206
209
  }
207
210
 
@@ -217,21 +220,15 @@ function piRecordLine(entryId: string, saId: string): string {
217
220
  id: entryId,
218
221
  parentId: null,
219
222
  data: {
220
- v: 1,
223
+ // 登记 §3.3:条目面 = v2 族——pi 形态 = 注册条(身份)+ 终态条(engineHandle 半键)。
224
+ v: 2,
225
+ kind: 'settled',
221
226
  id: saId,
222
- agent: 'pi-agent',
223
- task: 'pi task',
224
- slug: 'pi-sub',
225
- status: 'running',
226
- mode: 'subagent',
227
- startedAt: BASE_MS,
228
- rootSessionId: ROOT_SESSION,
229
- parentRecordId: null,
230
- depth: 0,
231
- turns: 0,
232
- totalTokens: 0,
233
- eventLog: [],
234
- displayItems: [],
227
+ status: 'idle',
228
+ stopReason: 'completed',
229
+ endedAt: BASE_MS + 1,
230
+ turns: 1,
231
+ totalTokens: 10,
235
232
  engineHandle: { sessionRef: { sessionId: SESS_A }, poolKey: 'shared' },
236
233
  },
237
234
  })