@zhushanwen/pi-subagent-workflow 7.4.0 → 8.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.
- package/README.md +15 -0
- package/package.json +7 -4
- package/scripts/rfl.mjs +308 -0
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
- package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
- package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
- package/src/execution/__tests__/crash-recovery.test.ts +8 -2
- package/src/execution/__tests__/delivery-methods.test.ts +385 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -72
- package/src/execution/__tests__/execution-record.test.ts +199 -0
- package/src/execution/__tests__/finalize-record.test.ts +197 -15
- package/src/execution/__tests__/format.test.ts +131 -7
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +37 -10
- package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
- package/src/execution/__tests__/index-session-start.test.ts +257 -5
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/list-component.test.ts +59 -5
- package/src/execution/__tests__/list-fields.test.ts +109 -0
- package/src/execution/__tests__/model-resolver.test.ts +38 -1
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/notifier-flush.test.ts +209 -7
- package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
- package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
- package/src/execution/__tests__/record-store.test.ts +442 -54
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
- package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
- package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
- package/src/execution/__tests__/session-pending.test.ts +61 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
- package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
- package/src/execution/__tests__/spawn-args.test.ts +127 -49
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/status-refactor.test.ts +345 -0
- package/src/execution/__tests__/stdin-writer.test.ts +97 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +49 -11
- package/src/execution/__tests__/timeout-integration.test.ts +27 -13
- package/src/execution/__tests__/tool-action.test.ts +12 -10
- package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/worktree-manager.test.ts +300 -90
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +17 -13
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
- package/src/execution/__tests__/worktree-registry.test.ts +72 -34
- package/src/execution/agent-result-mapper.ts +4 -1
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/channel-registry-access.ts +3 -1
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +99 -13
- package/src/execution/idle-gc.ts +47 -0
- package/src/execution/lifecycle-manager.ts +491 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +61 -16
- package/src/execution/model-resolver.ts +26 -5
- package/src/execution/notifier.ts +69 -12
- package/src/execution/pi-invocation.ts +21 -1
- package/src/execution/record-entry.ts +118 -0
- package/src/execution/record-store.ts +844 -108
- package/src/execution/session-pending.ts +121 -49
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +713 -316
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/stdin-writer.ts +93 -7
- package/src/execution/stream-sink.ts +20 -3
- package/src/execution/subagent-service.ts +917 -138
- package/src/execution/temp-prompt.ts +8 -3
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +218 -19
- package/src/execution/worktree-manager.ts +449 -59
- package/src/execution/worktree-registry.ts +97 -29
- package/src/index.ts +318 -20
- package/src/injectors/subagent-list-injector.ts +26 -8
- package/src/injectors/workflow-list-injector.ts +25 -8
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
- package/src/interface/__tests__/tool-render.test.ts +10 -8
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +103 -26
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +179 -0
- package/src/interface/bg-notify-render.ts +32 -8
- package/src/interface/command-actions.ts +26 -7
- package/src/interface/commands.ts +21 -22
- package/src/interface/format.ts +53 -20
- package/src/interface/gui-mappers.ts +6 -8
- package/src/interface/helpers.ts +170 -10
- package/src/interface/list-component.ts +53 -14
- package/src/interface/subagent-actions.ts +235 -17
- package/src/interface/subagent-tool.ts +81 -16
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +21 -27
- package/src/interface/tool-workflow-script.ts +29 -33
- package/src/interface/tool-workflow.ts +67 -100
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/detail-content.ts +1 -1
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +333 -0
- package/src/orchestration/__tests__/args-validator.test.ts +1 -1
- package/src/orchestration/__tests__/config-loader.test.ts +38 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +144 -1
- package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +171 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +820 -19
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +1108 -19
- package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
- package/src/orchestration/__tests__/test-mocks.ts +197 -0
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +110 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/__tests__/workflows-e2e.test.ts +38 -40
- package/src/orchestration/agent-opts-resolver.ts +4 -1
- package/src/orchestration/args-validator.ts +2 -2
- package/src/orchestration/config-loader.ts +30 -1
- package/src/orchestration/error-recovery.ts +137 -30
- package/src/orchestration/execute-agent-call.ts +44 -10
- package/src/orchestration/jsonl-run-store.ts +397 -71
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +145 -133
- package/src/orchestration/models/__tests__/trace.test.ts +408 -0
- package/src/orchestration/models/budget.ts +1 -1
- package/src/orchestration/models/run-runtime.ts +15 -17
- package/src/orchestration/models/run-spec.ts +2 -2
- package/src/orchestration/models/run-state.ts +3 -3
- package/src/orchestration/models/trace.ts +95 -15
- package/src/orchestration/models/types.ts +8 -9
- package/src/orchestration/models/workflow-run.ts +50 -71
- package/src/orchestration/models/workflow-script.ts +32 -1
- package/src/orchestration/skill-discovery.ts +30 -0
- package/src/orchestration/worker-handle.ts +1 -1
- package/src/orchestration/worker-host.ts +1 -1
- package/src/orchestration/worker-script-builder.ts +38 -11
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
- package/src/shared/__tests__/resource-discovery.test.ts +79 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +22 -1
- package/src/shared/resource-discovery.ts +162 -59
- package/src/shared/schema-jsonify.ts +56 -0
- package/workflows/review-fix-loop-utils.cjs +542 -32
- package/workflows/review-fix-loop.js +462 -109
|
@@ -11,9 +11,29 @@
|
|
|
11
11
|
* 设计:
|
|
12
12
|
* - JsonlRunStore implements RunStore(而非散落的 persist/reconstruct 自由函数)。
|
|
13
13
|
* - **D-5: 不向后兼容**——reconstruct 时检查 snapshotVersion,无版本号或版本不匹配
|
|
14
|
-
*
|
|
14
|
+
* 的 session 返回空数组(spec 决策:旧 run 历史价值低,不尝试兼容迁移)。
|
|
15
15
|
* - rewrite mode(writeFile 覆盖,文件始终是最新单行快照)。
|
|
16
|
-
* - workflow-
|
|
16
|
+
* - **W17 [D4] workflow-record 自描述 entry**:每次成功 flush 同步 append 一条完整
|
|
17
|
+
* 快照 entry(pi.appendEntry)——pi 文件(session JSONL)是 workflow 数据持久化
|
|
18
|
+
* 权威,state 文件降级为纯性能缓存(读序 = entry > state 文件 > 空,写路径保留)。
|
|
19
|
+
* 旧 `workflow-state-link` 指针 entry 退役(loadAll 保留兼容读,存量 run 不丢)。
|
|
20
|
+
*
|
|
21
|
+
* save 去抖语义(cw swf-perf wave2):
|
|
22
|
+
* - **热路径**(running 中间态,本实例已写过):per-runId pending 批合并——窗口内
|
|
23
|
+
* N 次 save 只落盘 1 次(serialize-at-flush:写 flush 时刻最新聚合状态)。
|
|
24
|
+
* 固定窗口不重置 timer(批创建时定时一次),保证 flush 延迟有界 ≤saveDebounceMs;
|
|
25
|
+
* agent-call 间隔秒级下 trailing 重置无合并增益反可无限推迟。
|
|
26
|
+
* - **冷路径**(本实例对该 runId 首写,或 status !== "running" 即 done):
|
|
27
|
+
* 同步挂链 flush 绕过 timer——首写立即可见(跨 session 重启后 loadAll 从 entry
|
|
28
|
+
* 发现 run)、done 立即落盘(终态优先持久化:transition("done") 后的 save
|
|
29
|
+
* 不进去抖批,去抖窗口内的崩溃不吞终态)。
|
|
30
|
+
* - workflow-record entry 每次成功 flush 都 append(含热路径中间态 flush)——
|
|
31
|
+
* entry 流 = 落盘历史(最后一条 = 最后一次成功 flush,崩溃丢失边界语义与
|
|
32
|
+
* state 文件路径一致);去抖已把 flush 频率控制在与 agent-call 周期同量级。
|
|
33
|
+
* - per-runId 串行 flush 链:同 runId 的 flush 排队顺序执行(不跳过、永不并发
|
|
34
|
+
* writeFile),链尾吞错防断链——错误只经各 save() Promise 的 settlers 传播。
|
|
35
|
+
* - dispose():幂等(缓存自身 Promise);刷全部 pending 批 + await 全部 in-flight
|
|
36
|
+
* 链后返回。dispose 后 save 静默 no-op + debug 日志(session_shutdown 编排收尾)。
|
|
17
37
|
*
|
|
18
38
|
* 序列化策略:
|
|
19
39
|
* - WorkflowRun 是带方法的 class 聚合根——序列化只取公共字段快照。
|
|
@@ -26,7 +46,8 @@
|
|
|
26
46
|
import * as fs from "node:fs";
|
|
27
47
|
import * as path from "node:path";
|
|
28
48
|
|
|
29
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
49
|
+
import type { CustomEntry, ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
50
|
+
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
30
51
|
|
|
31
52
|
import { AgentCall } from "./models/agent-call.ts";
|
|
32
53
|
import { Budget } from "./models/budget.ts";
|
|
@@ -43,9 +64,15 @@ import { WorkflowRun } from "./models/workflow-run.ts";
|
|
|
43
64
|
/**
|
|
44
65
|
* 快照格式版本。D-5:旧 session(无此字段或值不匹配)被 loadAll 忽略。
|
|
45
66
|
*
|
|
67
|
+
* 版本历史:
|
|
68
|
+
* - wf-run-v1:status 三态(含 paused)、meta 含 pausedAt。
|
|
69
|
+
* - wf-run-v2(当前):status 两态(running/done)、meta 无 pausedAt(随一次性
|
|
70
|
+
* 生命周期收窄,F6/F8)。v1 文件 loadAll 静默跳过——含 v1 running 残留跳过 =
|
|
71
|
+
* 静默消失不显示,接受(父文档 D-5 边界声明:旧 run 历史价值低,不做兼容迁移)。
|
|
72
|
+
*
|
|
46
73
|
* 升级格式时 bump 此常量并在 deserializeRun 中适配——旧文件返回 null(被 loadAll 跳过)。
|
|
47
74
|
*/
|
|
48
|
-
export const SNAPSHOT_VERSION = "wf-run-
|
|
75
|
+
export const SNAPSHOT_VERSION = "wf-run-v2" as const;
|
|
49
76
|
|
|
50
77
|
/**
|
|
51
78
|
* 持久化快照形态——WorkflowRun 公共字段的 JSON 可序列化投影。
|
|
@@ -86,12 +113,44 @@ interface RunSnapshot {
|
|
|
86
113
|
meta: {
|
|
87
114
|
startedAt: string;
|
|
88
115
|
completedAt?: string;
|
|
89
|
-
pausedAt?: string;
|
|
90
116
|
workerErrorCount?: number;
|
|
91
117
|
scriptErrorCount?: number;
|
|
92
118
|
};
|
|
93
119
|
}
|
|
94
120
|
|
|
121
|
+
// ── Workflow-record self-describing entry (W17, D4) ─────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 自描述 workflow record entry 的 customType(W17 [D4])。命名对齐 W16 的
|
|
125
|
+
* `subagent-record`(连字符风格)。写点字面量与本常量的等值由
|
|
126
|
+
* __tests__/jsonl-run-store-session-file.test.ts 断言钉住(消费方引用本常量,勿用裸字符串)。
|
|
127
|
+
*/
|
|
128
|
+
export const WORKFLOW_RECORD_CUSTOM_TYPE = "workflow-record";
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `workflow-record` entry 的 data schema(v1)。
|
|
132
|
+
*
|
|
133
|
+
* = 完整 RunSnapshot 快照(runId/status/calls/trace 等全部重建需要的字段)+ 版本号。
|
|
134
|
+
* 读取方无需逆向解析 state 文件或指针(D4 自描述原则);snapshot 内部自带 D-5
|
|
135
|
+
* snapshotVersion guard(deserializeRun 检查),entry 层 v 与 snapshot 层 v 是两级
|
|
136
|
+
* 独立版本(entry schema 演化 vs 快照格式演化)。
|
|
137
|
+
*/
|
|
138
|
+
// 模块内类型(不导出:无外部消费方,fallow unused_types/private_type_leaks 双轨判定;
|
|
139
|
+
// 运行侧 workflow-extractor 的同形结构独立定义,见其注释)。
|
|
140
|
+
interface WorkflowRecordEntryData {
|
|
141
|
+
/** schema 版本(W17 起 v1)。消费方按 v 判别解析,不认识的版本跳过而非猜测。 */
|
|
142
|
+
v: 1;
|
|
143
|
+
/** 完整 RunSnapshot(与同次 flush 写入 state 文件的内容是同一份,不二次序列化)。 */
|
|
144
|
+
snapshot: RunSnapshot;
|
|
145
|
+
/** append 时刻 ISO 时间(诊断用;重建不依赖)。 */
|
|
146
|
+
updatedAt: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 已序列化快照 → 自描述 entry data(doFlush 消费同一 snapshot,保证 entry 与 state 文件一致)。 */
|
|
150
|
+
function toWorkflowRecordEntryData(snapshot: RunSnapshot): WorkflowRecordEntryData {
|
|
151
|
+
return { v: 1, snapshot, updatedAt: new Date().toISOString() };
|
|
152
|
+
}
|
|
153
|
+
|
|
95
154
|
// ── Serialization ────────────────────────────────────────────
|
|
96
155
|
|
|
97
156
|
function serializeRun(run: WorkflowRun): RunSnapshot {
|
|
@@ -125,7 +184,7 @@ function serializeRun(run: WorkflowRun): RunSnapshot {
|
|
|
125
184
|
};
|
|
126
185
|
}),
|
|
127
186
|
// trace 节点浅拷贝时 strip live 字段——ExecutionRecord 含可变 turns[]/controller,
|
|
128
|
-
// 不适合序列化;
|
|
187
|
+
// 不适合序列化;live 是运行期对象(done 时由 dispatchAgentCall 清除,重跑时重建)。
|
|
129
188
|
trace: run.state.trace.toArray().map(({ live: _live, ...rest }) => rest),
|
|
130
189
|
errorLogs: run.state.errorLogs,
|
|
131
190
|
error: run.state.error,
|
|
@@ -186,7 +245,6 @@ function deserializeRun(snapshot: RunSnapshot): WorkflowRun | null {
|
|
|
186
245
|
const meta: WorkflowRunMeta = {
|
|
187
246
|
startedAt: snapshot.meta.startedAt,
|
|
188
247
|
completedAt: snapshot.meta.completedAt,
|
|
189
|
-
pausedAt: snapshot.meta.pausedAt,
|
|
190
248
|
workerErrorCount: snapshot.meta.workerErrorCount,
|
|
191
249
|
scriptErrorCount: snapshot.meta.scriptErrorCount,
|
|
192
250
|
};
|
|
@@ -197,86 +255,370 @@ function deserializeRun(snapshot: RunSnapshot): WorkflowRun | null {
|
|
|
197
255
|
return WorkflowRun.reconstruct(snapshot.runId, snapshot.spec, state, meta);
|
|
198
256
|
}
|
|
199
257
|
|
|
258
|
+
/** workflow-record entry → 重建 run 写入 recordRuns(v1 entry guard + D-5 版本不匹配
|
|
259
|
+
* 跳过;同 runId 后写覆盖 = 最后一条 entry 胜出)。返回 entry 是否命中该类型。 */
|
|
260
|
+
function collectRecordRun(entry: CustomEntry, recordRuns: Map<string, WorkflowRun>): boolean {
|
|
261
|
+
if (entry.customType !== WORKFLOW_RECORD_CUSTOM_TYPE) return false;
|
|
262
|
+
// v1 entry guard:schema 版本不认识 → 跳过(不猜测解析)
|
|
263
|
+
const data = entry.data as WorkflowRecordEntryData | undefined;
|
|
264
|
+
if (data?.v !== 1 || !data.snapshot) return true;
|
|
265
|
+
const run = deserializeRun(data.snapshot);
|
|
266
|
+
// D-5: null = old snapshot format / version mismatch — skip silently
|
|
267
|
+
if (run) recordRuns.set(run.runId, run); // 后写覆盖 = 最后一条 entry 胜出
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** 旧 workflow-state-link 指针 entry → 写入 pointers(仅 state 文件发现通道,W17 前形态)。
|
|
272
|
+
* 返回 entry 是否命中该类型。 */
|
|
273
|
+
function collectStateLinkPointer(entry: CustomEntry, pointers: Map<string, { path: string }>): boolean {
|
|
274
|
+
if (entry.customType !== "workflow-state-link") return false;
|
|
275
|
+
const data = entry.data as { runId?: string; path?: string } | undefined;
|
|
276
|
+
if (data?.runId && data?.path) {
|
|
277
|
+
pointers.set(data.runId, { path: data.path });
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** loadAll 的 entry 扫描:主 session entries → 自描述 record 快照(每 runId 末条胜出)
|
|
283
|
+
* + 旧 workflow-state-link 指针(仅 state 文件发现通道)。 */
|
|
284
|
+
function collectEntrySources(entries: SessionEntry[]): {
|
|
285
|
+
recordRuns: Map<string, WorkflowRun>;
|
|
286
|
+
pointers: Map<string, { path: string }>;
|
|
287
|
+
} {
|
|
288
|
+
const recordRuns = new Map<string, WorkflowRun>();
|
|
289
|
+
const pointers = new Map<string, { path: string }>();
|
|
290
|
+
for (const entry of entries) {
|
|
291
|
+
if (entry.type !== "custom") continue;
|
|
292
|
+
if (collectRecordRun(entry, recordRuns)) continue;
|
|
293
|
+
collectStateLinkPointer(entry, pointers);
|
|
294
|
+
}
|
|
295
|
+
return { recordRuns, pointers };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** 旧 link 指针指向的 state 文件读取:末行 JSON 解析重建。损坏/不可读返回 null
|
|
299
|
+
* (单文件失败不阻断其余 run 重建)。 */
|
|
300
|
+
async function loadRunFromStateFile(filePath: string): Promise<WorkflowRun | null> {
|
|
301
|
+
try {
|
|
302
|
+
const content = await fs.promises.readFile(filePath, "utf8");
|
|
303
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
304
|
+
const lastLine = lines[lines.length - 1];
|
|
305
|
+
if (!lastLine) return null;
|
|
306
|
+
const parsed = JSON.parse(lastLine) as RunSnapshot;
|
|
307
|
+
// D-5: null = old format / version mismatch — skip silently
|
|
308
|
+
return deserializeRun(parsed);
|
|
309
|
+
} catch {
|
|
310
|
+
// Corrupt/unreadable state file — skip (don't crash loadAll).
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
200
315
|
// ── JsonlRunStore ────────────────────────────────────────────
|
|
201
316
|
|
|
317
|
+
/** Node fs 错误 code 判定(ENOENT = 路径不存在,并发删除场景)。 */
|
|
318
|
+
function isEnoentError(err: unknown): boolean {
|
|
319
|
+
return (
|
|
320
|
+
typeof err === "object" &&
|
|
321
|
+
err !== null &&
|
|
322
|
+
"code" in err &&
|
|
323
|
+
(err as { code: unknown }).code === "ENOENT"
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const logger = getLogger("subagents");
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* save 去抖窗口默认值(ms)。区间 100-250 内取值——agent-call 间隔秒级,
|
|
331
|
+
* 200ms 足以合并同一 call 周期内的多次状态 mutation,又不至于让崩溃窗口
|
|
332
|
+
* (未 flush 的 running 尾部丢失,等价崩溃链由 kill-9 恢复收编)明显放大。
|
|
333
|
+
* export 供测试边界构造(对齐 TRACE_RESULT_MAX_CHARS export 先例)。
|
|
334
|
+
*/
|
|
335
|
+
export const DEFAULT_SAVE_DEBOUNCE_MS = 200;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* per-runId 去抖批。窗口内 N 次 save 合并:latestRun 保留最新聚合引用
|
|
339
|
+
* (serialize-at-flush),settlers 收集批内全部 save() 调用方的 settle 回调。
|
|
340
|
+
*/
|
|
341
|
+
interface PendingSaveBatch {
|
|
342
|
+
latestRun: WorkflowRun;
|
|
343
|
+
/**
|
|
344
|
+
* 批的去抖 timer(构造时即确定——经 {@link JsonlRunStore.armPendingBatch} 工厂
|
|
345
|
+
* 内联组装,timer 与批对象在同一同步段成型,类型上不存在「先构造后赋值」的
|
|
346
|
+
* 可选窗口)。
|
|
347
|
+
*/
|
|
348
|
+
timer: NodeJS.Timeout;
|
|
349
|
+
settlers: Array<{ resolve: () => void; reject: (e: unknown) => void }>;
|
|
350
|
+
}
|
|
351
|
+
|
|
202
352
|
export interface JsonlRunStoreOptions {
|
|
203
|
-
|
|
353
|
+
/** Session directory root (state files live under <sessionDir>/workflow-state/). */
|
|
204
354
|
sessionDir: string;
|
|
205
|
-
|
|
355
|
+
/** Pi ExtensionAPI for workflow-record appendEntry writes (optional for testing). */
|
|
206
356
|
pi?: ExtensionAPI;
|
|
207
|
-
|
|
357
|
+
/** Pi ExtensionContext for sessionManager.getEntries (optional for testing). */
|
|
208
358
|
ctx?: ExtensionContext;
|
|
359
|
+
/** save 去抖窗口(ms),默认 {@link DEFAULT_SAVE_DEBOUNCE_MS}。 */
|
|
360
|
+
saveDebounceMs?: number;
|
|
209
361
|
}
|
|
210
362
|
|
|
211
363
|
export class JsonlRunStore {
|
|
212
364
|
private readonly sessionDir: string;
|
|
213
365
|
private readonly pi?: ExtensionAPI;
|
|
214
366
|
private readonly ctx?: ExtensionContext;
|
|
367
|
+
private readonly saveDebounceMs: number;
|
|
368
|
+
/** per-runId 去抖批(热路径)。 */
|
|
369
|
+
private readonly pending = new Map<string, PendingSaveBatch>();
|
|
370
|
+
/** 本实例已至少成功发起过一次 flush 的 runId(冷/热路径判据)。 */
|
|
371
|
+
private readonly writtenOnce = new Set<string>();
|
|
372
|
+
/**
|
|
373
|
+
* per-runId 串行 flush 链。同 runId 的 flush 排队顺序执行(排队不跳过——
|
|
374
|
+
* 跳过会丢最新状态且打破后写覆盖前写的单调性),不同 runId 互不阻塞。
|
|
375
|
+
* 链条目 settle 后不清理:runId 数量有界、生命周期短于 store,惰性清理
|
|
376
|
+
* 与排队写入存在竞态——取舍为每 runId 残留一个 settled Promise 引用,可忽略。
|
|
377
|
+
*/
|
|
378
|
+
private readonly chains = new Map<string, Promise<void>>();
|
|
379
|
+
private disposed = false;
|
|
380
|
+
private disposePromise: Promise<void> | undefined;
|
|
215
381
|
|
|
216
382
|
constructor(opts: JsonlRunStoreOptions) {
|
|
217
383
|
this.sessionDir = opts.sessionDir;
|
|
218
384
|
this.pi = opts.pi;
|
|
219
385
|
this.ctx = opts.ctx;
|
|
386
|
+
this.saveDebounceMs = opts.saveDebounceMs ?? DEFAULT_SAVE_DEBOUNCE_MS;
|
|
220
387
|
}
|
|
221
388
|
|
|
222
|
-
|
|
389
|
+
/** State directory: <sessionDir>/workflow-state/ */
|
|
223
390
|
private get stateDir(): string {
|
|
224
391
|
return path.join(this.sessionDir, "workflow-state");
|
|
225
392
|
}
|
|
226
393
|
|
|
227
|
-
|
|
394
|
+
/** State file path for a given runId. */
|
|
228
395
|
private filePathFor(runId: string): string {
|
|
229
396
|
return path.join(this.stateDir, `${runId}.jsonl`);
|
|
230
397
|
}
|
|
231
398
|
|
|
232
|
-
|
|
399
|
+
/** Public accessor: run 状态快照文件绝对路径(RunStore port 实现)。 */
|
|
233
400
|
stateFilePath(runId: string): string {
|
|
234
401
|
return this.filePathFor(runId);
|
|
235
402
|
}
|
|
236
403
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
404
|
+
/**
|
|
405
|
+
* Persist a single run: rewrite mode (overwrite) — file always contains the
|
|
406
|
+
* latest complete snapshot on a single line.
|
|
407
|
+
*
|
|
408
|
+
* 去抖路由:
|
|
409
|
+
* - 冷路径(本实例首写,或 status !== "running")→ 立即挂链 flush(绕过 timer),
|
|
410
|
+
* 回滚资格 = 首写(flush 失败时 doFlush 回滚 writtenOnce,下次 save 重走冷路径);
|
|
411
|
+
* - 热路径(running 且已写过)→ 并入 per-runId 去抖批(固定窗口不重置 timer)。
|
|
412
|
+
*
|
|
413
|
+
* Promise 语义:本批实际落盘后 resolve(同批多次调用共享 settle);IO 错误
|
|
414
|
+
* (非 ENOENT)reject 本批全部调用方;ENOENT 静默 resolve(工作目录已被清理,
|
|
415
|
+
* 持久化无意义也无法完成——见 doFlush)。
|
|
416
|
+
*/
|
|
242
417
|
async save(run: WorkflowRun): Promise<void> {
|
|
243
|
-
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
418
|
+
// R5 处置:dispose 后(session_shutdown 收尾后 in-flight 链的迟到 save)静默
|
|
419
|
+
// no-op + debug 留痕。不复活同步 flush——单向闸门状态机简单;此时 run 是
|
|
420
|
+
// running 落盘无增益(kill-9 恢复同样转 done,failed),终态 reason 保真损失极窄。
|
|
421
|
+
if (this.disposed) {
|
|
422
|
+
logger.debug(
|
|
423
|
+
`[subagent-workflow] jsonl-run-store save after dispose: silently dropped (runId=${run.runId})`,
|
|
424
|
+
);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const runId = run.runId;
|
|
429
|
+
const isFirstWrite = !this.writtenOnce.has(runId);
|
|
430
|
+
const isCold = isFirstWrite || run.state.status !== "running";
|
|
431
|
+
if (isCold) {
|
|
432
|
+
// 判定即记录:原子防并发双冷(两次并发首写都判 true 会各 flush 一次 entry)。
|
|
433
|
+
// ENOENT 边界:首写 flush 遇 ENOENT 时 entry 未写但 writtenOnce 已记——
|
|
434
|
+
// sessionDir 已删场景持久化无意义,接受(非 ENOENT 失败由 doFlush 回滚,重走冷路径)。
|
|
435
|
+
this.writtenOnce.add(runId);
|
|
436
|
+
// 原子取走 pending 批(终态与最后一个 agent-call 的 debounced save 交错时,
|
|
437
|
+
// pending 批 settlers 并入本次同步 flush 的批合并 settle,timer 取消防二次写)。
|
|
438
|
+
const batch = this.pending.get(runId);
|
|
439
|
+
if (batch) {
|
|
440
|
+
clearTimeout(batch.timer);
|
|
441
|
+
this.pending.delete(runId);
|
|
442
|
+
}
|
|
443
|
+
return this.enqueueFlush(runId, run, batch ? batch.settlers : [], isFirstWrite);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 热路径:running 中间态,并入去抖批
|
|
447
|
+
const existing = this.pending.get(runId);
|
|
448
|
+
if (existing) {
|
|
449
|
+
// latestRun 更新(固定窗口不重置 timer——flush 延迟有界 ≤saveDebounceMs)
|
|
450
|
+
existing.latestRun = run;
|
|
451
|
+
return new Promise<void>((resolve, reject) => {
|
|
452
|
+
existing.settlers.push({ resolve, reject });
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
const settlers: PendingSaveBatch["settlers"] = [];
|
|
456
|
+
const promise = new Promise<void>((resolve, reject) => {
|
|
457
|
+
settlers.push({ resolve, reject });
|
|
458
|
+
});
|
|
459
|
+
this.pending.set(runId, this.armPendingBatch(runId, run, settlers));
|
|
460
|
+
return promise;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* 构造去抖批([review 修复] 工厂内联组装:timer 与批对象在同一同步段成型,
|
|
465
|
+
* PendingSaveBatch.timer 保持非可选——消除「批先构造、timer 后赋值」靠注释维持
|
|
466
|
+
* 的可选窗口)。timer 回调闭包经局部 batch 变量持批引用做身份守卫(ES3)。
|
|
467
|
+
*/
|
|
468
|
+
private armPendingBatch(
|
|
469
|
+
runId: string,
|
|
470
|
+
run: WorkflowRun,
|
|
471
|
+
settlers: PendingSaveBatch["settlers"],
|
|
472
|
+
): PendingSaveBatch {
|
|
473
|
+
// timer 回调闭包经下方 const batch 持批引用做身份守卫——前向引用在运行时安全:
|
|
474
|
+
// 回调最早 saveDebounceMs 后才执行,届时 batch 已在本同步段尾部初始化完毕。
|
|
475
|
+
const timer = setTimeout(() => {
|
|
476
|
+
// ES3 幂等守卫(批身份比较):回调闭包持自身批引用,与 pending Map 现值做
|
|
477
|
+
// 身份比较而非仅按键存在性判断。除「批已被冷路径/flushPendingSaves/dispose
|
|
478
|
+
// 原子取走(clearTimeout 与回调触发在 fake timers 下可能交错)」的交接语义外,
|
|
479
|
+
// 还防「旧 timer 撞新批」交错:本批被取走后同 runId 的新批已入 Map 时,若只看
|
|
480
|
+
// 键存在性,旧 timer 会误取走新批提前 flush(缩短新批去抖窗口)。身份不匹配
|
|
481
|
+
// 直接 return,批由取走方负责 flush。
|
|
482
|
+
if (this.pending.get(runId) !== batch) return;
|
|
483
|
+
this.pending.delete(runId);
|
|
484
|
+
// 孤儿 Promise(无调用方持有):错误只经 settlers 传播给 save() 调用方,
|
|
485
|
+
// 此处 catch 防止 unhandled rejection。
|
|
486
|
+
this.enqueueFlush(runId, batch.latestRun, batch.settlers, false).catch(() => {});
|
|
487
|
+
}, this.saveDebounceMs);
|
|
488
|
+
// DS5:timer 必须 unref——不 unref 会钉住空转的 extension 进程不退出。
|
|
489
|
+
timer.unref();
|
|
490
|
+
const batch: PendingSaveBatch = { latestRun: run, timer, settlers };
|
|
491
|
+
return batch;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* 把一次 flush 排到 runId 的串行链尾。调用方 Promise(本函数返回值)与传入
|
|
496
|
+
* settlers 由同一次 doFlush 独占 settle 一次。
|
|
497
|
+
*/
|
|
498
|
+
private enqueueFlush(
|
|
499
|
+
runId: string,
|
|
500
|
+
run: WorkflowRun,
|
|
501
|
+
settlers: PendingSaveBatch["settlers"],
|
|
502
|
+
rollbackFirstWrite: boolean,
|
|
503
|
+
): Promise<void> {
|
|
504
|
+
const promise = new Promise<void>((resolve, reject) => {
|
|
505
|
+
settlers.push({ resolve, reject });
|
|
506
|
+
});
|
|
507
|
+
// 排队不跳过:前一 flush in-flight 时本次挂链尾顺序执行,同 runId 永不并发
|
|
508
|
+
// writeFile(整文件覆盖写并发会互相截断)。链尾吞错防断链——错误只经 settlers 传播。
|
|
509
|
+
const next = (this.chains.get(runId) ?? Promise.resolve())
|
|
510
|
+
.then(() => this.doFlush(runId, run, settlers, rollbackFirstWrite))
|
|
511
|
+
.catch(() => {});
|
|
512
|
+
this.chains.set(runId, next);
|
|
513
|
+
return promise;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* 实际落盘(在 runId 串行链上执行)。settlers 由本函数独占 settle 一次:
|
|
518
|
+
* 成功或 ENOENT 全 resolve,其他错误全 reject。
|
|
519
|
+
*/
|
|
520
|
+
private async doFlush(
|
|
521
|
+
runId: string,
|
|
522
|
+
run: WorkflowRun,
|
|
523
|
+
settlers: PendingSaveBatch["settlers"],
|
|
524
|
+
rollbackFirstWrite: boolean,
|
|
525
|
+
): Promise<void> {
|
|
526
|
+
const filePath = this.filePathFor(runId);
|
|
251
527
|
try {
|
|
252
|
-
|
|
528
|
+
// 兜底容错:run 工作目录(sessionDir)已被清理时,mkdir 抛 ENOENT,放弃持久化。
|
|
529
|
+
// 竞态场景(review-fix-loop-e2e 等 runAndWait 测试):handleReturn 内
|
|
530
|
+
// run.transition("done") 同步改 status 后,runAndWait 轮询发现 done 并 resolve,
|
|
531
|
+
// 测试 afterEach 随即 rmSync 删除 sessionDir;此时 in-flight 的 mkdir
|
|
532
|
+
// 遇到目录链已删除 → ENOENT({recursive:true} 在并发 rmSync 下仍可抛 ENOENT)。
|
|
533
|
+
// run 既已终态(状态不再变化),持久化无意义也无法完成 → settle resolve。
|
|
534
|
+
// 仅容错 ENOENT,其他错误(EACCES/ENOSPC 等真实磁盘问题)reject 不掩盖。
|
|
535
|
+
try {
|
|
536
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
537
|
+
} catch (err) {
|
|
538
|
+
if (isEnoentError(err)) {
|
|
539
|
+
for (const s of settlers) s.resolve();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
throw err;
|
|
543
|
+
}
|
|
544
|
+
// serialize-at-flush:写 flush 时刻的最新聚合状态(latestRun 语义)
|
|
545
|
+
const snapshot = serializeRun(run);
|
|
546
|
+
await fs.promises.writeFile(filePath, JSON.stringify(snapshot) + "\n", "utf8");
|
|
547
|
+
// W17 [D4]:每次成功 flush 同步 append 自描述 workflow-record entry(同一份
|
|
548
|
+
// snapshot,entry 与 state 文件内容一致)。pi 文件是 workflow 数据持久化权威
|
|
549
|
+
//(loadAll 优先从 entry 重建),state 文件降级纯性能缓存。pi 未注入(测试)时跳过。
|
|
550
|
+
this.pi?.appendEntry(
|
|
551
|
+
WORKFLOW_RECORD_CUSTOM_TYPE,
|
|
552
|
+
toWorkflowRecordEntryData(snapshot),
|
|
553
|
+
);
|
|
554
|
+
for (const s of settlers) s.resolve();
|
|
253
555
|
} catch (err) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
) {
|
|
260
|
-
|
|
556
|
+
// ES9 失败回滚(热路径 flush 也会写 entry,回滚的意义收敛为「下次 save 重走
|
|
557
|
+
// 冷路径立即重试」——不再有旧指针形态下「热路径永不写 entry → run 对重启
|
|
558
|
+
// 不可见」的窗口)。堵住首写失败后还得等去抖窗的恢复延迟。
|
|
559
|
+
// 残余窗口(已知接受):回滚后若再无任何 save(随即崩溃/退出),entry 与
|
|
560
|
+
// state 文件双双缺失——等价崩溃丢失,由 kill-9 恢复兜底。
|
|
561
|
+
if (rollbackFirstWrite) {
|
|
562
|
+
this.writtenOnce.delete(runId);
|
|
261
563
|
}
|
|
262
|
-
|
|
564
|
+
for (const s of settlers) s.reject(err);
|
|
263
565
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* 立即刷全部 pending 去抖批(测试与排查的备用手段)。自身恒 resolve——IO 错误
|
|
570
|
+
* 已由各 save() Promise 的 settlers 传播给调用方。store 保持可用:不动 disposed
|
|
571
|
+
* 标志,后续 save 正常进入新去抖批。
|
|
572
|
+
*/
|
|
573
|
+
async flushPendingSaves(): Promise<void> {
|
|
574
|
+
const flushes: Promise<void>[] = [];
|
|
575
|
+
for (const [runId, batch] of Array.from(this.pending.entries())) {
|
|
576
|
+
clearTimeout(batch.timer);
|
|
577
|
+
this.pending.delete(runId);
|
|
578
|
+
flushes.push(this.enqueueFlush(runId, batch.latestRun, batch.settlers, false));
|
|
272
579
|
}
|
|
580
|
+
await Promise.allSettled(flushes);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* 收尾:刷全部 pending 批 + 停 timer + await 全部 in-flight 链(in-flight flush
|
|
585
|
+
* 完成后才返回),此后 save 静默 no-op。
|
|
586
|
+
*
|
|
587
|
+
* 幂等:dispose 缓存自身 Promise——首次未完成时并发交叠进入的后续调用返回
|
|
588
|
+
* 同一 Promise(「dispose 返回 = 全部 flush 已落盘」对每个调用方都成立,
|
|
589
|
+
* 无第二次拿到立即 resolve 的空 Promise 瑕疵)。故本方法不能是 async 函数
|
|
590
|
+
* (async 总是创建新 Promise 破坏同一引用保证)。
|
|
591
|
+
*/
|
|
592
|
+
dispose(): Promise<void> {
|
|
593
|
+
if (this.disposePromise) return this.disposePromise;
|
|
594
|
+
this.disposePromise = this.doDispose();
|
|
595
|
+
return this.disposePromise;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private async doDispose(): Promise<void> {
|
|
599
|
+
// 同步置位:阻断新 save 进入去抖/冷路径(R5 no-op 分支接住 shutdown 后
|
|
600
|
+
// in-flight 链的迟到 save)。doDispose 被调用后同步执行到第一个 await 前。
|
|
601
|
+
this.disposed = true;
|
|
602
|
+
const flushes: Promise<void>[] = [];
|
|
603
|
+
for (const [runId, batch] of Array.from(this.pending.entries())) {
|
|
604
|
+
clearTimeout(batch.timer);
|
|
605
|
+
this.pending.delete(runId);
|
|
606
|
+
flushes.push(this.enqueueFlush(runId, batch.latestRun, batch.settlers, false));
|
|
607
|
+
}
|
|
608
|
+
await Promise.allSettled(flushes);
|
|
609
|
+
// await 全部 in-flight 链(ES4:flush 全部落定后才返回)
|
|
610
|
+
await Promise.allSettled(Array.from(this.chains.values()));
|
|
273
611
|
}
|
|
274
612
|
|
|
275
613
|
/**
|
|
276
|
-
* Reconstruct all runs
|
|
614
|
+
* Reconstruct all runs(W17 [D4] 读序 = workflow-record entry > state 文件 > 空)。
|
|
277
615
|
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
616
|
+
* 1. 优先扫描自描述 `workflow-record` entry(重建源——同一 runId 多条时最后一条
|
|
617
|
+
* 胜出,等价「最后一次成功 flush」)。entry 层 v1 guard:不认识的版本跳过而非
|
|
618
|
+
* 猜测;snapshot 层 D-5 snapshotVersion guard 保持(版本不匹配 → deserializeRun
|
|
619
|
+
* 返回 null → 跳过,不做兼容迁移)。
|
|
620
|
+
* 2. 旧 `workflow-state-link` 指针 entry 兼容读取(优先级低——存量 run 不静默
|
|
621
|
+
* 丢失,父文档 #9 踩坑):entry 未覆盖的 runId 经指针读 state 文件最后行。
|
|
280
622
|
*
|
|
281
623
|
* 需要 ctx(构造时注入)——无 ctx 时返回空(测试或非 Pi 环境下)。
|
|
282
624
|
*/
|
|
@@ -285,32 +627,16 @@ export class JsonlRunStore {
|
|
|
285
627
|
const runs: WorkflowRun[] = [];
|
|
286
628
|
try {
|
|
287
629
|
const entries = this.ctx.sessionManager.getEntries();
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
for (const entry of entries) {
|
|
291
|
-
if (entry.type !== "custom") continue;
|
|
292
|
-
if (entry.customType !== "workflow-state-link") continue;
|
|
293
|
-
const data = entry.data as { runId?: string; path?: string } | undefined;
|
|
294
|
-
if (data?.runId && data?.path) {
|
|
295
|
-
pointers.set(data.runId, { path: data.path });
|
|
296
|
-
}
|
|
297
|
-
}
|
|
630
|
+
const { recordRuns, pointers } = collectEntrySources(entries);
|
|
298
631
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// D-5: null = old format / version mismatch — skip silently
|
|
308
|
-
if (run) runs.push(run);
|
|
309
|
-
} catch (err) {
|
|
310
|
-
// Corrupt/unreadable state file — skip (don't crash loadAll).
|
|
311
|
-
// Single bad file must not abort reconstruction of the rest.
|
|
312
|
-
void err;
|
|
313
|
-
}
|
|
632
|
+
// 1) 自描述 entry 重建(优先——pi 文件是持久化权威)
|
|
633
|
+
runs.push(...recordRuns.values());
|
|
634
|
+
|
|
635
|
+
// 2) 旧 link 指针 → state 文件兼容(entry 已覆盖的 runId 跳过——link 优先级低)
|
|
636
|
+
for (const [runId, pointer] of pointers) {
|
|
637
|
+
if (recordRuns.has(runId)) continue;
|
|
638
|
+
const run = await loadRunFromStateFile(pointer.path);
|
|
639
|
+
if (run) runs.push(run);
|
|
314
640
|
}
|
|
315
641
|
} catch (err) {
|
|
316
642
|
// getEntries failed — return what we have (empty).
|
|
@@ -77,7 +77,13 @@ export interface LauncherDeps extends LifecycleDeps {
|
|
|
77
77
|
|
|
78
78
|
/** 轮询间隔 Promise。 */
|
|
79
79
|
function pollInterval(): Promise<void> {
|
|
80
|
-
|
|
80
|
+
// IF10(#16):unref 使轮询等待 tick 不钉住事件循环(对齐 subagent-service
|
|
81
|
+
// gcTimer.unref?.() 先例的防御 duck-type 写法)。resolve 语义不变——unref
|
|
82
|
+
// 只影响进程退出判定,已注册 timer 仍按 500ms 触发。
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const timer = setTimeout(resolve, STATUS_POLL_INTERVAL_MS);
|
|
85
|
+
timer.unref?.();
|
|
86
|
+
});
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
/**
|