@zhushanwen/pi-subagent-workflow 0.1.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.
Files changed (143) hide show
  1. package/agents/context-builder.md +17 -0
  2. package/agents/general-purpose.md +16 -0
  3. package/agents/oracle.md +17 -0
  4. package/agents/planner.md +17 -0
  5. package/agents/researcher.md +17 -0
  6. package/agents/reviewer.md +17 -0
  7. package/agents/scout.md +17 -0
  8. package/agents/worker.md +16 -0
  9. package/examples/README.md +43 -0
  10. package/examples/chain.example.js +92 -0
  11. package/examples/map-reduce.example.js +99 -0
  12. package/examples/parallel.example.js +82 -0
  13. package/examples/scatter-gather.example.js +106 -0
  14. package/index.ts +1 -0
  15. package/package.json +66 -0
  16. package/skills/workflow-script-format/SKILL.md +328 -0
  17. package/src/execution/__tests__/agent-registry.test.ts +164 -0
  18. package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
  19. package/src/execution/__tests__/alive-store.test.ts +147 -0
  20. package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
  21. package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
  22. package/src/execution/__tests__/config.test.ts +110 -0
  23. package/src/execution/__tests__/crash-recovery.test.ts +311 -0
  24. package/src/execution/__tests__/execute-nesting.test.ts +359 -0
  25. package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
  26. package/src/execution/__tests__/execution-record.test.ts +959 -0
  27. package/src/execution/__tests__/finalized-marker.test.ts +82 -0
  28. package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
  29. package/src/execution/__tests__/format.test.ts +320 -0
  30. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  31. package/src/execution/__tests__/list-component.test.ts +347 -0
  32. package/src/execution/__tests__/model-resolver.test.ts +356 -0
  33. package/src/execution/__tests__/output-collector.test.ts +61 -0
  34. package/src/execution/__tests__/path-encoding.test.ts +75 -0
  35. package/src/execution/__tests__/pi-invocation.test.ts +73 -0
  36. package/src/execution/__tests__/record-store.test.ts +545 -0
  37. package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
  38. package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
  39. package/src/execution/__tests__/sdk-contract.test.ts +272 -0
  40. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  41. package/src/execution/__tests__/session-file-gc.test.ts +247 -0
  42. package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
  43. package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
  44. package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
  45. package/src/execution/__tests__/spawn-args.test.ts +244 -0
  46. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  47. package/src/execution/__tests__/subagent-service.test.ts +678 -0
  48. package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
  49. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  50. package/src/execution/__tests__/timeout-integration.test.ts +381 -0
  51. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  52. package/src/execution/__tests__/tool-action.test.ts +330 -0
  53. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  54. package/src/execution/__tests__/worktree-manager.test.ts +423 -0
  55. package/src/execution/__tests__/worktree-registry.test.ts +161 -0
  56. package/src/execution/agent-registry.ts +252 -0
  57. package/src/execution/agent-result-mapper.ts +84 -0
  58. package/src/execution/alive-store.ts +92 -0
  59. package/src/execution/best-effort.ts +30 -0
  60. package/src/execution/concurrency-pool.ts +84 -0
  61. package/src/execution/config.ts +73 -0
  62. package/src/execution/execute-options-mapper.ts +86 -0
  63. package/src/execution/execution-record.ts +778 -0
  64. package/src/execution/finalized-marker.ts +51 -0
  65. package/src/execution/model-config-service.ts +225 -0
  66. package/src/execution/model-resolver.ts +247 -0
  67. package/src/execution/notifier.ts +168 -0
  68. package/src/execution/output-collector.ts +88 -0
  69. package/src/execution/path-encoding.ts +34 -0
  70. package/src/execution/pi-invocation.ts +70 -0
  71. package/src/execution/record-store.ts +350 -0
  72. package/src/execution/session-context-resolver.ts +64 -0
  73. package/src/execution/session-file-gc.ts +98 -0
  74. package/src/execution/session-reconstructor.ts +450 -0
  75. package/src/execution/session-runner.ts +725 -0
  76. package/src/execution/spawn-event-adapter.ts +150 -0
  77. package/src/execution/subagent-service.ts +973 -0
  78. package/src/execution/subprocess-agent-runner.ts +108 -0
  79. package/src/execution/temp-prompt.ts +57 -0
  80. package/src/execution/tombstone-store.ts +72 -0
  81. package/src/execution/turn-limiter.ts +88 -0
  82. package/src/execution/types.ts +634 -0
  83. package/src/execution/worktree-manager.ts +285 -0
  84. package/src/execution/worktree-registry.ts +144 -0
  85. package/src/index.ts +454 -0
  86. package/src/interface/bg-notify-render.ts +286 -0
  87. package/src/interface/commands.ts +157 -0
  88. package/src/interface/format.ts +501 -0
  89. package/src/interface/gui-adapter.ts +136 -0
  90. package/src/interface/helpers.ts +110 -0
  91. package/src/interface/list-component.ts +643 -0
  92. package/src/interface/list-shared.ts +84 -0
  93. package/src/interface/list-view.ts +373 -0
  94. package/src/interface/reentry-guard.ts +30 -0
  95. package/src/interface/subagent-actions.ts +294 -0
  96. package/src/interface/subagent-tool.ts +294 -0
  97. package/src/interface/subagents.ts +30 -0
  98. package/src/interface/tool-render.ts +333 -0
  99. package/src/interface/tool-workflow-script.ts +351 -0
  100. package/src/interface/tool-workflow.ts +485 -0
  101. package/src/interface/views/WorkflowsView.ts +944 -0
  102. package/src/interface/views/detail-content.ts +298 -0
  103. package/src/interface/views/format.ts +320 -0
  104. package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
  105. package/src/orchestration/__tests__/config-loader.test.ts +381 -0
  106. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
  107. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  108. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
  109. package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
  110. package/src/orchestration/__tests__/script-lint.test.ts +347 -0
  111. package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
  112. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
  113. package/src/orchestration/agent-opts-resolver.ts +128 -0
  114. package/src/orchestration/concurrency-gate.ts +69 -0
  115. package/src/orchestration/config-loader.ts +313 -0
  116. package/src/orchestration/error-recovery.ts +578 -0
  117. package/src/orchestration/execute-agent-call.ts +174 -0
  118. package/src/orchestration/jsonl-run-store.ts +292 -0
  119. package/src/orchestration/launcher.ts +368 -0
  120. package/src/orchestration/lifecycle.ts +373 -0
  121. package/src/orchestration/models/__tests__/budget.test.ts +367 -0
  122. package/src/orchestration/models/agent-call.ts +76 -0
  123. package/src/orchestration/models/budget.ts +148 -0
  124. package/src/orchestration/models/ports.ts +165 -0
  125. package/src/orchestration/models/run-runtime.ts +91 -0
  126. package/src/orchestration/models/run-spec.ts +54 -0
  127. package/src/orchestration/models/run-state.ts +44 -0
  128. package/src/orchestration/models/trace.ts +102 -0
  129. package/src/orchestration/models/types.ts +242 -0
  130. package/src/orchestration/models/workflow-run.ts +275 -0
  131. package/src/orchestration/models/workflow-script-registry.ts +32 -0
  132. package/src/orchestration/models/workflow-script.ts +90 -0
  133. package/src/orchestration/node-ops.ts +192 -0
  134. package/src/orchestration/script-lint.ts +387 -0
  135. package/src/orchestration/skill-discovery.ts +60 -0
  136. package/src/orchestration/worker-handle.ts +115 -0
  137. package/src/orchestration/worker-host.ts +93 -0
  138. package/src/orchestration/worker-script-builder.ts +281 -0
  139. package/src/orchestration/workflow-files.ts +85 -0
  140. package/src/orchestration/workflow-script-registry-impl.ts +128 -0
  141. package/src/shared/__tests__/resource-discovery.test.ts +226 -0
  142. package/src/shared/agent-event.ts +13 -0
  143. package/src/shared/resource-discovery.ts +535 -0
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Workflow Extension — executeAgentCall(关键路径)
3
+ *
4
+ * 单次 agent 调用执行的 Engine free function(D-12)。显式 5 参数
5
+ * `(call, runner, budget, signal, trace)`,无依赖注入 bag(AC-2:消除散落的
6
+ * Context factory)。
7
+ *
8
+ * 职责:
9
+ * - 重试:3 次 + 指数退避(BACKOFF_MS = [1000, 2000, 4000])
10
+ * - 预算:超限不重试(直接 markDone failed)
11
+ * - stale-context:不重试(直接 markDone failed)
12
+ * - 成功:consume usage + incrementCallCount + markDone + trace.update(completed)
13
+ *
14
+ * 关键设计:
15
+ * - **usage 透传**:result.usage 直接交给 budget.consume,加权由 Budget 内部的权重常量
16
+ * 处理(见 budget.ts)。此函数不再做 usage 形状的改写。
17
+ * - **参数显式化**:runner 直接传入(而非 ctx.getRun(runId).pool),无 runId 查找 / pool 守卫。
18
+ * - **stale-state 检查**:signal.aborted 时早返回。WorkflowRun 状态由调用方 lifecycle
19
+ * 持有,executeAgentCall 只关心单次 call 生命周期。
20
+ *
21
+ * 层归属:Engine。零 infra 依赖(runner 是 AgentRunner port,budget/trace/call 是 Engine 模型)。
22
+ *
23
+ * 参考:domain-models.md §5 + §失败处理矩阵。
24
+ */
25
+
26
+ import type { AgentEvent } from "../shared/agent-event.ts";
27
+ import type { AgentCall } from "./models/agent-call.ts";
28
+ import type { Budget } from "./models/budget.ts";
29
+ import type { AgentRunner } from "./models/ports.ts";
30
+ import type { Trace } from "./models/trace.ts";
31
+ import type { AgentResult } from "./models/types.ts";
32
+
33
+ // ── 常量 ─────────────────────────────────────────────────────
34
+
35
+ /** 指数退避基数(ms):第 n 次重试等待 BASE^n。 */
36
+ const BACKOFF_BASE_MS = 1000;
37
+ const BACKOFF_EXPONENT_BASE = 2;
38
+
39
+ /** 最大尝试次数(含首次):initial + 2 retries = 3。 */
40
+ const MAX_ATTEMPTS = 3;
41
+
42
+ /**
43
+ * Stale context 检测模式(P1-5)。
44
+ *
45
+ * pi session context 被 compact/cancel 时报告的模式。这种情况下重试无意义——
46
+ * 同样的 call 会再次失败。直接 markDone failed 终止单次调用。
47
+ */
48
+ export const STALE_CONTEXT_PATTERNS = [
49
+ "stale context",
50
+ "stalecontext",
51
+ "context canceled",
52
+ "aborted",
53
+ ] as const;
54
+
55
+ /**
56
+ * 判断错误信息是否表示 stale/canceled pi session context。
57
+ * 命中时不重试——重试只会再次失败(P1-5)。
58
+ */
59
+ export function isStaleContextErrorMsg(msg: string | undefined): boolean {
60
+ if (!msg) return false;
61
+ const lower = msg.toLowerCase();
62
+ return STALE_CONTEXT_PATTERNS.some((p) => lower.includes(p));
63
+ }
64
+
65
+ // ── 内部 helper ──────────────────────────────────────────────
66
+
67
+ /**
68
+ * 计算第 n 次重试前的退避时间(ms)。
69
+ * 第 1 次重试 → 1000ms,第 2 次 → 2000ms,第 3 次 → 4000ms(指数退避)。
70
+ */
71
+ function backoffDelay(retryIndex: number): number {
72
+ return BACKOFF_BASE_MS * Math.pow(BACKOFF_EXPONENT_BASE, retryIndex - 1);
73
+ }
74
+
75
+ /**
76
+ * 终态化单次 call:markDone + trace.update。
77
+ *
78
+ * 成功:status="completed";失败:status="failed"。
79
+ * traceNode.stepIndex === call.id(D-10 单源,调用方保证)。
80
+ */
81
+ function finalizeCall(call: AgentCall, result: AgentResult, trace: Trace): void {
82
+ call.markDone(result);
83
+ const status = result.error === undefined ? "completed" : "failed";
84
+ trace.update(call.id, {
85
+ status,
86
+ result,
87
+ completedAt: new Date().toISOString(),
88
+ sessionId: result.sessionId,
89
+ });
90
+ }
91
+
92
+ /**
93
+ * 延迟工具(testable —— 测试可通过 fake timers 推进)。
94
+ */
95
+ function delay(ms: number): Promise<void> {
96
+ return new Promise((resolve) => setTimeout(resolve, ms));
97
+ }
98
+
99
+ // ── executeAgentCall ─────────────────────────────────────────
100
+
101
+ /**
102
+ * 执行单次 agent 调用,含重试 + 预算 + stale-context 检测。
103
+ *
104
+ * 流程:
105
+ * 1. markRunning(attempts++,含首次)
106
+ * 2. await runner.run(opts, signal)(AgentRunner port,infra 实现 spawn pi 子进程)
107
+ * 3. 若 result.usage 存在:consumeUsage(D.4 修复)
108
+ * 4. stale-context → finalizeCall failed,返回(不重试)
109
+ * 5. signal.aborted → 返回(调用方已终止,不重试)
110
+ * 6. budget.isExceeded → finalizeCall failed,返回(不重试)
111
+ * 7. 失败 && attempts < MAX → 退避后递归(下一次 markRunning)
112
+ * 8. 否则 finalizeCall(completed 或 failed)+ incrementCallCount
113
+ *
114
+ * @param call AgentCall 实体(markRunning/markDone 由本函数驱动)
115
+ * @param runner AgentRunner port(执行子进程)
116
+ * @param budget Budget 值对象(consume + isExceeded 检查)
117
+ * @param signal AbortSignal(runner.run 传播;abort 后不重试)
118
+ * @param trace Trace 值对象(finalizeCall 时 update)
119
+ */
120
+ export async function executeAgentCall(
121
+ call: AgentCall,
122
+ runner: AgentRunner,
123
+ budget: Budget,
124
+ signal: AbortSignal,
125
+ trace: Trace,
126
+ onEvent?: (event: AgentEvent) => void,
127
+ ): Promise<void> {
128
+ call.markRunning();
129
+
130
+ const result = await runner.run(call.opts, signal, onEvent);
131
+
132
+ // 累加 usage(加权由 budget.consume 内部按权重常量处理,见 budget.ts)
133
+ if (result.usage) {
134
+ budget.consume(result.usage);
135
+ }
136
+
137
+ // stale-context:不重试(P1-5)
138
+ if (result.error !== undefined && isStaleContextErrorMsg(result.error)) {
139
+ finalizeCall(call, result, trace);
140
+ budget.incrementCallCount();
141
+ return;
142
+ }
143
+
144
+ // signal 已 abort:调用方终止,不重试(避免无意义的递归)
145
+ if (signal.aborted) {
146
+ finalizeCall(call, result, trace);
147
+ budget.incrementCallCount();
148
+ return;
149
+ }
150
+
151
+ // 预算超限:不重试(重试只会突破预算且无意义)
152
+ if (result.error !== undefined && budget.isExceeded()) {
153
+ finalizeCall(call, result, trace);
154
+ budget.incrementCallCount();
155
+ return;
156
+ }
157
+
158
+ // 可重试失败:退避后递归
159
+ if (result.error !== undefined && call.attempts < MAX_ATTEMPTS) {
160
+ await delay(backoffDelay(call.attempts));
161
+ // 退避期间 signal 可能 abort
162
+ if (signal.aborted) {
163
+ finalizeCall(call, result, trace);
164
+ budget.incrementCallCount();
165
+ return;
166
+ }
167
+ await executeAgentCall(call, runner, budget, signal, trace, onEvent);
168
+ return;
169
+ }
170
+
171
+ // 终态(成功或达到重试上限的失败)
172
+ finalizeCall(call, result, trace);
173
+ budget.incrementCallCount();
174
+ }
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Workflow Extension — JSONL Run Store
3
+ *
4
+ * RunStore port 的 Infra 实现。
5
+ *
6
+ * 职责:持久化 WorkflowRun 聚合根到 JSONL 文件 + 跨 session 重水合。
7
+ *
8
+ * 层归属:Infra(D-12)。implements Engine 层的 RunStore port。
9
+ * 依赖 @mariozechner/pi-coding-agent 的 ExtensionAPI/ExtensionContext(Infra 允许 Pi SDK)。
10
+ *
11
+ * 设计:
12
+ * - JsonlRunStore implements RunStore(而非散落的 persist/reconstruct 自由函数)。
13
+ * - **D-5: 不向后兼容**——reconstruct 时检查 snapshotVersion,无版本号或版本不匹配
14
+ * 的 session 返回空数组(spec 决策:旧 run 历史价值低,不尝试兼容迁移)。
15
+ * - rewrite mode(writeFile 覆盖,文件始终是最新单行快照)。
16
+ * - workflow-state-link 指针条目机制保留(pi.appendEntry)。
17
+ *
18
+ * 序列化策略:
19
+ * - WorkflowRun 是带方法的 class 聚合根——序列化只取公共字段快照。
20
+ * - Budget/Trace/AgentCall 都有公共构造器或 fromArray 工厂,反序列化时重建实例。
21
+ * - Snapshot 形态用 SnapshotVersion 守护(D-5:格式识别)。
22
+ *
23
+ * 参考:domain-models.md §Ports(RunStore 定义)、clarification.md D-5。
24
+ */
25
+
26
+ import * as fs from "node:fs";
27
+ import * as path from "node:path";
28
+
29
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
30
+
31
+ import { AgentCall } from "./models/agent-call.ts";
32
+ import { Budget } from "./models/budget.ts";
33
+ import type { RunSpec } from "./models/run-spec.ts";
34
+ import type { RunState } from "./models/run-state.ts";
35
+ import { Trace } from "./models/trace.ts";
36
+ import type { DoneReason, RunStatus, WorkerLogEntry } from "./models/types.ts";
37
+ import type { AgentCallOpts, AgentResult, ExecutionTraceNode } from "./models/types.ts";
38
+ import type { WorkflowRunMeta } from "./models/workflow-run.ts";
39
+ import { WorkflowRun } from "./models/workflow-run.ts";
40
+
41
+ // ── Snapshot format (D-5 version guard) ──────────────────────
42
+
43
+ /**
44
+ * 快照格式版本。D-5:旧 session(无此字段或值不匹配)被 loadAll 忽略。
45
+ *
46
+ * 升级格式时 bump 此常量并在 deserializeRun 中适配——旧文件返回 null(被 loadAll 跳过)。
47
+ */
48
+ export const SNAPSHOT_VERSION = "wf-run-v1" as const;
49
+
50
+ /**
51
+ * 持久化快照形态——WorkflowRun 公共字段的 JSON 可序列化投影。
52
+ *
53
+ * calls 序列化为数组(Map 不能直接 JSON.stringify);反序列化时重建 Map。
54
+ * budget/trace 在 deserialize 时重建实例(带方法的 class)。
55
+ */
56
+ interface RunSnapshot {
57
+ v: typeof SNAPSHOT_VERSION;
58
+ runId: string;
59
+ spec: RunSpec;
60
+ state: {
61
+ status: RunStatus;
62
+ reason?: DoneReason;
63
+ budget: {
64
+ maxTokens?: number;
65
+ maxCost?: number;
66
+ maxTimeMs?: number;
67
+ usedTokens: number;
68
+ usedCost: number;
69
+ totalCallCount: number;
70
+ };
71
+ calls: Array<{
72
+ id: number;
73
+ opts: AgentCallOpts;
74
+ status: "pending" | "running" | "done";
75
+ attempts: number;
76
+ result?: AgentResult;
77
+ sessionId?: string;
78
+ traceNode: ExecutionTraceNode;
79
+ }>;
80
+ trace: ExecutionTraceNode[];
81
+ errorLogs: WorkerLogEntry[];
82
+ error?: string;
83
+ scriptResult?: unknown;
84
+ };
85
+ meta: {
86
+ startedAt: string;
87
+ completedAt?: string;
88
+ pausedAt?: string;
89
+ workerErrorCount?: number;
90
+ scriptErrorCount?: number;
91
+ };
92
+ }
93
+
94
+ // ── Serialization ────────────────────────────────────────────
95
+
96
+ function serializeRun(run: WorkflowRun): RunSnapshot {
97
+ return {
98
+ v: SNAPSHOT_VERSION,
99
+ runId: run.runId,
100
+ spec: run.spec,
101
+ state: {
102
+ status: run.state.status,
103
+ reason: run.state.reason,
104
+ budget: {
105
+ maxTokens: run.state.budget.maxTokens,
106
+ maxCost: run.state.budget.maxCost,
107
+ maxTimeMs: run.state.budget.maxTimeMs,
108
+ usedTokens: run.state.budget.usedTokens,
109
+ usedCost: run.state.budget.usedCost,
110
+ totalCallCount: run.state.budget.totalCallCount,
111
+ },
112
+ calls: Array.from(run.state.calls.values()).map((c) => {
113
+ // strip live(同 trace 序列化,不持久化运行期对象)
114
+ const { live: _live, ...traceNodeRest } = c.traceNode;
115
+ return {
116
+ id: c.id,
117
+ opts: c.opts,
118
+ status: c.status,
119
+ attempts: c.attempts,
120
+ result: c.result,
121
+ sessionId: c.sessionId,
122
+ traceNode: traceNodeRest,
123
+ };
124
+ }),
125
+ // trace 节点浅拷贝时 strip live 字段——ExecutionRecord 含可变 turns[]/controller,
126
+ // 不适合序列化;pause/resume 后 live 为 undefined(重跑时由 dispatchAgentCall 重建)。
127
+ trace: run.state.trace.toArray().map(({ live: _live, ...rest }) => rest),
128
+ errorLogs: run.state.errorLogs,
129
+ error: run.state.error,
130
+ scriptResult: run.state.scriptResult,
131
+ },
132
+ meta: run.meta,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * 反序列化快照为 WorkflowRun。D-5:版本不匹配返回 null(旧 session)。
138
+ */
139
+ function deserializeRun(snapshot: RunSnapshot): WorkflowRun | null {
140
+ // D-5 version guard
141
+ if (snapshot.v !== SNAPSHOT_VERSION) return null;
142
+
143
+ const budget = new Budget({
144
+ maxTokens: snapshot.state.budget.maxTokens,
145
+ maxCost: snapshot.state.budget.maxCost,
146
+ maxTimeMs: snapshot.state.budget.maxTimeMs,
147
+ usedTokens: snapshot.state.budget.usedTokens,
148
+ usedCost: snapshot.state.budget.usedCost,
149
+ });
150
+ budget.totalCallCount = snapshot.state.budget.totalCallCount;
151
+
152
+ const calls = new Map<number, AgentCall>();
153
+ for (const c of snapshot.state.calls) {
154
+ const call = new AgentCall(c.id, c.opts, c.traceNode);
155
+ call.status = c.status;
156
+ call.attempts = c.attempts;
157
+ // Restore result directly — bypasses markRunning/markDone state-machine guards
158
+ // because we're reconstructing a known-good persisted state, not transitioning.
159
+ if (c.result !== undefined) {
160
+ call.result = c.result;
161
+ }
162
+ if (c.sessionId !== undefined) {
163
+ call.setSessionId(c.sessionId);
164
+ }
165
+ calls.set(c.id, call);
166
+ }
167
+
168
+ const trace = Trace.fromArray(snapshot.state.trace);
169
+
170
+ const state: RunState = {
171
+ status: snapshot.state.status,
172
+ reason: snapshot.state.reason,
173
+ budget,
174
+ calls,
175
+ trace,
176
+ errorLogs: snapshot.state.errorLogs,
177
+ error: snapshot.state.error,
178
+ scriptResult: snapshot.state.scriptResult,
179
+ };
180
+
181
+ const meta: WorkflowRunMeta = {
182
+ startedAt: snapshot.meta.startedAt,
183
+ completedAt: snapshot.meta.completedAt,
184
+ pausedAt: snapshot.meta.pausedAt,
185
+ workerErrorCount: snapshot.meta.workerErrorCount,
186
+ scriptErrorCount: snapshot.meta.scriptErrorCount,
187
+ };
188
+
189
+ // WorkflowRun.reconstruct 跳过 I1 校验——持久化的 running 状态没有 worker
190
+ // (进程被杀后 worker 不可能还活着),违反 I1。D-4 kill-9 恢复在 session_start
191
+ // 时把残留 running 转 done,failed,恢复 I1(见 index.ts session_start handler)。
192
+ return WorkflowRun.reconstruct(snapshot.runId, snapshot.spec, state, meta);
193
+ }
194
+
195
+ // ── JsonlRunStore ────────────────────────────────────────────
196
+
197
+ export interface JsonlRunStoreOptions {
198
+ /** Session directory root (state files live under <sessionDir>/workflow-state/). */
199
+ sessionDir: string;
200
+ /** Pi ExtensionAPI for appendEntry pointer writes (optional for testing). */
201
+ pi?: ExtensionAPI;
202
+ /** Pi ExtensionContext for sessionManager.getEntries (optional for testing). */
203
+ ctx?: ExtensionContext;
204
+ }
205
+
206
+ export class JsonlRunStore {
207
+ private readonly sessionDir: string;
208
+ private readonly pi?: ExtensionAPI;
209
+ private readonly ctx?: ExtensionContext;
210
+
211
+ constructor(opts: JsonlRunStoreOptions) {
212
+ this.sessionDir = opts.sessionDir;
213
+ this.pi = opts.pi;
214
+ this.ctx = opts.ctx;
215
+ }
216
+
217
+ /** State directory: <sessionDir>/workflow-state/ */
218
+ private get stateDir(): string {
219
+ return path.join(this.sessionDir, "workflow-state");
220
+ }
221
+
222
+ /** State file path for a given runId. */
223
+ private filePathFor(runId: string): string {
224
+ return path.join(this.stateDir, `${runId}.jsonl`);
225
+ }
226
+
227
+ /**
228
+ * Persist a single run: rewrite mode (overwrite) — file always contains the
229
+ * latest complete snapshot on a single line. Appends a workflow-state-link
230
+ * pointer entry via pi.appendEntry so loadAll can locate files.
231
+ */
232
+ async save(run: WorkflowRun): Promise<void> {
233
+ const filePath = this.filePathFor(run.runId);
234
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
235
+ const snapshot = serializeRun(run);
236
+ await fs.promises.writeFile(filePath, JSON.stringify(snapshot) + "\n", "utf8");
237
+ if (this.pi) {
238
+ this.pi.appendEntry("workflow-state-link", {
239
+ runId: run.runId,
240
+ path: filePath,
241
+ updatedAt: new Date().toISOString(),
242
+ });
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Reconstruct all runs from session JSONL pointer entries.
248
+ *
249
+ * D-5:旧格式(无版本号 / 版本不匹配)返回空——loadAll 跳过这些条目,
250
+ * 不尝试向后兼容旧 session(spec 决策)。
251
+ *
252
+ * 需要 ctx(构造时注入)——无 ctx 时返回空(测试或非 Pi 环境下)。
253
+ */
254
+ async loadAll(): Promise<WorkflowRun[]> {
255
+ if (!this.ctx) return [];
256
+ const runs: WorkflowRun[] = [];
257
+ try {
258
+ const entries = this.ctx.sessionManager.getEntries();
259
+ const pointers = new Map<string, { path: string }>();
260
+
261
+ for (const entry of entries) {
262
+ if (entry.type !== "custom") continue;
263
+ if (entry.customType !== "workflow-state-link") continue;
264
+ const data = entry.data as { runId?: string; path?: string } | undefined;
265
+ if (data?.runId && data?.path) {
266
+ pointers.set(data.runId, { path: data.path });
267
+ }
268
+ }
269
+
270
+ for (const [, pointer] of pointers) {
271
+ try {
272
+ const content = await fs.promises.readFile(pointer.path, "utf8");
273
+ const lines = content.split("\n").filter((l) => l.trim());
274
+ const lastLine = lines[lines.length - 1];
275
+ if (!lastLine) continue;
276
+ const parsed = JSON.parse(lastLine) as RunSnapshot;
277
+ const run = deserializeRun(parsed);
278
+ // D-5: null = old format / version mismatch — skip silently
279
+ if (run) runs.push(run);
280
+ } catch (err) {
281
+ // Corrupt/unreadable state file — skip (don't crash loadAll).
282
+ // Single bad file must not abort reconstruction of the rest.
283
+ void err;
284
+ }
285
+ }
286
+ } catch (err) {
287
+ // getEntries failed — return what we have (empty).
288
+ void err;
289
+ }
290
+ return runs;
291
+ }
292
+ }