@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
|
@@ -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 流档概览(
|
|
1583
|
+
// ---- [D16③] v2 record 流档概览(recordPath 锚点 = record 流路径)----
|
|
1644
1584
|
|
|
1645
|
-
/** 向 main session 追加 workflow-record v2 注册条目行(
|
|
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,
|
|
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
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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
|
-
|
|
1766
|
-
|
|
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
|
-
|
|
1789
|
-
|
|
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
|
-
|
|
1818
|
-
|
|
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
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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-
|
|
1852
|
+
id: 'wf-reg-wf-sub-1',
|
|
1939
1853
|
parentId: SUB_WF_ROOT,
|
|
1940
|
-
customType: 'workflow-
|
|
1941
|
-
data: { runId: 'wf-sub-1',
|
|
1942
|
-
timestamp: '2026-
|
|
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
|
|
381
|
-
//
|
|
330
|
+
// resolveWorkflows 发现链([ADR-0095] v1 读面删除后的单档形态:v2 注册条目
|
|
331
|
+
// recordPath 主源;历史格式条目——v1 快照 / workflow-state-link 指针——静默消失)
|
|
382
332
|
// ============================================================
|
|
383
333
|
|
|
384
|
-
|
|
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 注册条目行(
|
|
442
|
-
function v2RegisteredLine(runId: 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
|
-
|
|
375
|
+
recordPath,
|
|
457
376
|
},
|
|
458
377
|
})
|
|
459
378
|
}
|
|
460
379
|
|
|
461
|
-
/** workflow-record
|
|
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
|
|
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 流路径(
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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-
|
|
586
|
-
expect(workflows[0].stateFile).toBe(
|
|
587
|
-
expect(workflows[0].calls.map((c) => c.fileName)).toEqual([
|
|
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('
|
|
591
|
-
dir = await mkdtemp(join(tmpdir(), 'wf-
|
|
592
|
-
const
|
|
593
|
-
JSON.stringify({ type: 'run-created', seq: 1, ts: 1000, runId: 'wf-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
601
|
-
|
|
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
|
|
608
|
-
|
|
609
|
-
expect(workflows[
|
|
610
|
-
expect(workflows[
|
|
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,无
|
|
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('
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
223
|
+
// 登记 §3.3:条目面 = v2 族——pi 形态 = 注册条(身份)+ 终态条(engineHandle 半键)。
|
|
224
|
+
v: 2,
|
|
225
|
+
kind: 'settled',
|
|
221
226
|
id: saId,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
})
|