@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
|
@@ -63,6 +63,9 @@ const EXPONENTIAL_BACKOFF_BASE = 2;
|
|
|
63
63
|
/** errorLogs 最大保留条数(防止超长 session 中日志无界增长)。 */
|
|
64
64
|
const MAX_ERROR_LOGS = 500;
|
|
65
65
|
|
|
66
|
+
/** malformed agent-call 日志中 opts JSON 的预览截断长度(字符)。 */
|
|
67
|
+
const MALFORMED_MSG_LOG_PREVIEW_CHARS = 200;
|
|
68
|
+
|
|
66
69
|
// ── Worker 消息类型(与 infra/worker-script-builder.ts WorkerInMsg 对齐) ──
|
|
67
70
|
|
|
68
71
|
interface AgentCallMsg {
|
|
@@ -109,6 +112,25 @@ function isTerminal(run: WorkflowRun): boolean {
|
|
|
109
112
|
return run.state.status === "done";
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
/**
|
|
116
|
+
* 孤儿 call 判定:dispatch 时捕获的 call 实例是否已不是 calls Map 中该 callId
|
|
117
|
+
* 的当前条目。
|
|
118
|
+
*
|
|
119
|
+
* 为何需要实例级比对(而非只查 run 状态):rebuildRuntime 不改 status(全程
|
|
120
|
+
* running),既有的终态 stale 守卫拦不住旧 runtime 代际的迟到 completion。只有
|
|
121
|
+
* discardInFlightCalls(delete 条目)与新一代 dispatch(set 新实例)会改变
|
|
122
|
+
* 「callId → 实例」映射,故实例不等 ⟺ 本 completion 属于被丢弃/被替换的旧代际
|
|
123
|
+
* (S7-second 竞态:旧失败结果经 postAgentResult 投给新 worker 的同 callId
|
|
124
|
+
* pending,劫持重跑调用为假失败/空串假成功)。
|
|
125
|
+
*
|
|
126
|
+
* 运行期 calls Map 写点仅 discard 的 delete 与 dispatchAgentCall 的 set 两族
|
|
127
|
+
* (jsonl-run-store 的 set 在离线重水合路径,无在飞 promise),正常(非孤儿)
|
|
128
|
+
* 路径下实例恒等,无误判。
|
|
129
|
+
*/
|
|
130
|
+
function isOrphanedCall(run: WorkflowRun, callId: number, call: AgentCall): boolean {
|
|
131
|
+
return run.state.calls.get(callId) !== call;
|
|
132
|
+
}
|
|
133
|
+
|
|
112
134
|
/** 计算第 n 次重试前的退避时间(ms):1s, 2s, 4s 指数。 */
|
|
113
135
|
function backoffDelay(retryIndex: number): number {
|
|
114
136
|
return RETRY_BACKOFF_BASE_MS * Math.pow(EXPONENTIAL_BACKOFF_BASE, retryIndex - 1);
|
|
@@ -123,6 +145,29 @@ function delay(ms: number): Promise<void> {
|
|
|
123
145
|
|
|
124
146
|
// ── rebuildRuntime(G3-001 整重建) ─────────────────────────
|
|
125
147
|
|
|
148
|
+
/**
|
|
149
|
+
* 移除 run 中未真正完成的在飞 call(status !== "done")及其 trace 节点。
|
|
150
|
+
*
|
|
151
|
+
* 仅 rebuildRuntime 调用——清理被旧 runtime abort 的在飞 call,避免重跑时
|
|
152
|
+
* cached replay 把 abort 产生的 failed 结果当作已完成结果回放(原 MUST_FIX
|
|
153
|
+
* round-4 #1,自 pause 路径移入崩溃重建路径)。genuinely-done 的 call(成功或
|
|
154
|
+
* 失败均 "done")保留,重跑时按原语义 replay(不重复耗 token)。
|
|
155
|
+
*
|
|
156
|
+
* 返回被丢弃的 callId 数组(升序——Map 迭代按插入序,排序保证返回值与
|
|
157
|
+
* rebuildRuntime 的 L3 日志 payload 形态稳定),供调用方记日志。
|
|
158
|
+
*/
|
|
159
|
+
function discardInFlightCalls(run: WorkflowRun): number[] {
|
|
160
|
+
const inFlight: number[] = [];
|
|
161
|
+
for (const [callId, call] of run.state.calls) {
|
|
162
|
+
if (call.status !== "done") inFlight.push(callId);
|
|
163
|
+
}
|
|
164
|
+
for (const callId of inFlight) {
|
|
165
|
+
run.state.calls.delete(callId);
|
|
166
|
+
run.state.trace.removeByStepIndex(callId);
|
|
167
|
+
}
|
|
168
|
+
return inFlight.sort((a, b) => a - b);
|
|
169
|
+
}
|
|
170
|
+
|
|
126
171
|
/**
|
|
127
172
|
* 重建整个 RunRuntime:新 controller + 新 gate + 新 worker。
|
|
128
173
|
*
|
|
@@ -142,19 +187,56 @@ export function rebuildRuntime(
|
|
|
142
187
|
deps: LifecycleDeps,
|
|
143
188
|
handlers: WorkerHandlers,
|
|
144
189
|
): void {
|
|
190
|
+
// OB3(可观察性):rebuild 关键节点 debug 日志——此前函数体 0 处 deps.log,
|
|
191
|
+
// 崩溃自愈只能靠行为证据诊断(L1 入口 / L2 重排 / L3 discard / L4 完成)。
|
|
192
|
+
deps.log?.("debug", "workflow:error-recovery", "runtime rebuild start", {
|
|
193
|
+
runId: run.runId,
|
|
194
|
+
budgetTimeMs: run.spec.budgetTimeMs,
|
|
195
|
+
});
|
|
145
196
|
const controller = new AbortController();
|
|
146
197
|
const gate = new ConcurrencyGate({ maxConcurrency: DEFAULT_CONCURRENCY });
|
|
147
198
|
const worker = deps.workerHost.start(run.spec, run.spec.args, handlers);
|
|
148
199
|
// D-12 regression fix (round-2 #2):重新调度 run 级墙钟预算计时器。
|
|
149
200
|
// replaceRuntime 释放旧 runtime 时 clearTimeout 了旧计时器(run-runtime.release),
|
|
150
201
|
// 新 runtime 必须重排,否则带 budgetTimeMs 的 run 命中一次 worker/script 错误重试后
|
|
151
|
-
// 时间预算静默失效(直到
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
202
|
+
// 时间预算静默失效(直到 rebuildRuntime 才重排——本函数即唯一重排点)。
|
|
203
|
+
// deps.scheduleTimeBudget 由 Interface 层注入;未注入时(旧测试)跳过重排(兼容,
|
|
204
|
+
// 不影响无时间预算的 run)。
|
|
205
|
+
// 重排分支改为 if——语义与原三元一致(同一条件调 scheduleTimeBudget),仅为在
|
|
206
|
+
// 分支内记 L2 日志,控制流/异常语义零变化。
|
|
207
|
+
let timeBudgetTimer: ReturnType<typeof setTimeout> | undefined;
|
|
208
|
+
if (run.spec.budgetTimeMs && run.spec.budgetTimeMs > 0 && deps.scheduleTimeBudget) {
|
|
209
|
+
timeBudgetTimer = deps.scheduleTimeBudget(run.runId, run.spec.budgetTimeMs);
|
|
210
|
+
deps.log?.("debug", "workflow:error-recovery", "time budget rescheduled", {
|
|
211
|
+
runId: run.runId,
|
|
212
|
+
budgetTimeMs: run.spec.budgetTimeMs,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
157
215
|
run.replaceRuntime(new RunRuntime(worker, gate, controller, timeBudgetTimer));
|
|
216
|
+
// 清除被旧 runtime abort 的在飞 call——必须在 replaceRuntime 之后同步执行(无
|
|
217
|
+
// await 间隔):replaceRuntime 同步 abort 旧 controller + terminate 旧 worker,
|
|
218
|
+
// 在飞 executeAgentCall 的 finalize 发生在 `await runner.run` resolve 后的
|
|
219
|
+
// microtask,此刻在飞 call 仍为 "running"/"pending"(status !== "done")可精确
|
|
220
|
+
// 清理;genuinely-done 的 call 保留(重跑 replay)。放 delay 退避之前会误删退避
|
|
221
|
+
// 期间自然完成的真结果(重跑重复耗 token);放任何 await 之后,假失败已 finalize
|
|
222
|
+
// 为 "done" 挡不住——重跑 replay 会把 abort 错误当真结果回放,静默污染输出。
|
|
223
|
+
// 注意:discard 只清 Map/trace 条目,旧 executeAgentCall 的 promise 链仍会醒来
|
|
224
|
+
// finalize。markDone 在孤儿实例上无害,但后续投递并非 no-op——postAgentResult
|
|
225
|
+
// 会投给 run.runtime(已是新 worker)的同 callId pending,劫持重跑调用(实测
|
|
226
|
+
// S7-second 竞态:旧失败结果被 worker 侧 resolve 为空串 → 脚本假成功);
|
|
227
|
+
// finalizeCall 的 trace.update 在重跑已 append 同 stepIndex 新节点时命中新节点
|
|
228
|
+
// (瞬时污染,由重跑完成时的 update 覆盖)。该投递由 dispatchAgentCall 的
|
|
229
|
+
// 孤儿守卫(isOrphanedCall)拦截,trace.update 的瞬时污染由 executeAgentCall
|
|
230
|
+
// 的 isOrphaned 谓词(OB2)拦截,此处不重复设防。
|
|
231
|
+
const discardedCallIds = discardInFlightCalls(run);
|
|
232
|
+
deps.log?.("debug", "workflow:error-recovery", "in-flight calls discarded", {
|
|
233
|
+
runId: run.runId,
|
|
234
|
+
callIds: discardedCallIds,
|
|
235
|
+
count: discardedCallIds.length,
|
|
236
|
+
});
|
|
237
|
+
deps.log?.("debug", "workflow:error-recovery", "runtime rebuild complete", {
|
|
238
|
+
runId: run.runId,
|
|
239
|
+
});
|
|
158
240
|
}
|
|
159
241
|
|
|
160
242
|
// ── handleWorkerMessage(消息路由) ──────────────────────────
|
|
@@ -166,7 +248,7 @@ export function rebuildRuntime(
|
|
|
166
248
|
* return → transition done,completed(脚本正常返回)
|
|
167
249
|
* error → handleScriptError(脚本主动抛错)
|
|
168
250
|
*
|
|
169
|
-
*
|
|
251
|
+
* 终态(done)下的 stale 消息丢弃(P0-1)。
|
|
170
252
|
*/
|
|
171
253
|
export async function handleWorkerMessage(
|
|
172
254
|
run: WorkflowRun,
|
|
@@ -174,8 +256,8 @@ export async function handleWorkerMessage(
|
|
|
174
256
|
deps: LifecycleDeps,
|
|
175
257
|
handlers: WorkerHandlers,
|
|
176
258
|
): Promise<void> {
|
|
177
|
-
//
|
|
178
|
-
if (isTerminal(run)
|
|
259
|
+
// 终态(done)丢弃 stale 消息(P0-1)
|
|
260
|
+
if (isTerminal(run)) return;
|
|
179
261
|
|
|
180
262
|
// M7: 形状校验——防畸形 IPC 消息(worker 崩溃/发非对象)导致下游 TypeError
|
|
181
263
|
if (typeof raw !== "object" || raw === null) return;
|
|
@@ -215,8 +297,12 @@ export async function handleWorkerMessage(
|
|
|
215
297
|
* **C-2 修复**:call 完成后检查 `budget.isExceeded` → abortRun(budget_limited),
|
|
216
298
|
* 终止整个 run(避免烧光预算后继续 spawn 新 call)。
|
|
217
299
|
*
|
|
218
|
-
* **stale
|
|
219
|
-
*
|
|
300
|
+
* **stale 完成守卫(两层)**:completion 到达时——
|
|
301
|
+
* 1. `run.state.status === "running"` recheck:run 终止(abort/terminate)后到达的
|
|
302
|
+
* call 完成不写 run.state.calls / 不 postAgentResult(终态快照不被迟到结果污染);
|
|
303
|
+
* 2. 孤儿 call 实例比对(isOrphanedCall):rebuildRuntime 后旧代际 dispatch 的
|
|
304
|
+
* completion 不投递——rebuild 不改 status,第 1 层拦不住跨 runtime 代际的迟到
|
|
305
|
+
* 结果(S7-second 竞态:旧失败结果投给新 worker 劫持重跑 pending → 假成功)。
|
|
220
306
|
*/
|
|
221
307
|
function dispatchAgentCall(
|
|
222
308
|
run: WorkflowRun,
|
|
@@ -229,11 +315,11 @@ function dispatchAgentCall(
|
|
|
229
315
|
if (typeof msg.callId !== "number" || !Number.isFinite(msg.callId) ||
|
|
230
316
|
typeof msg.opts !== "object" || msg.opts === null ||
|
|
231
317
|
typeof msg.opts.prompt !== "string") {
|
|
232
|
-
logger.error(`[workflow] malformed agent-call message: callId=${JSON.stringify(msg.callId)}, opts=${JSON.stringify(msg.opts)?.slice(0,
|
|
318
|
+
logger.error(`[workflow] malformed agent-call message: callId=${JSON.stringify(msg.callId)}, opts=${JSON.stringify(msg.opts)?.slice(0, MALFORMED_MSG_LOG_PREVIEW_CHARS)}`);
|
|
233
319
|
return;
|
|
234
320
|
}
|
|
235
321
|
|
|
236
|
-
// 已缓存的调用直接 replay(跨
|
|
322
|
+
// 已缓存的调用直接 replay(跨 rebuild——崩溃重建后重跑脚本,已完成调用按 callId 命中缓存)
|
|
237
323
|
const cached = run.state.calls.get(msg.callId);
|
|
238
324
|
if (cached && cached.status === "done") {
|
|
239
325
|
postAgentResult(run, msg.callId, cached.result!, true);
|
|
@@ -313,7 +399,7 @@ function dispatchAgentCall(
|
|
|
313
399
|
// runner(runner.run)管 spawn pi 子进程。
|
|
314
400
|
// assignRuntime/replaceRuntime 保证 status==="running" ⟺ runtime defined,
|
|
315
401
|
// 故 run.runtime 在此必存在(dispatchAgentCall 仅从 handleWorkerMessage 调用,
|
|
316
|
-
// 后者已守
|
|
402
|
+
// 后者已守 terminal(isTerminal)早期 return)。fallback new AbortController 已移除。
|
|
317
403
|
const runtime = run.runtime!;
|
|
318
404
|
const signal = runtime.controller.signal;
|
|
319
405
|
// D-005: onEvent 签名升级——executeAndAwait 直接出 AgentEvent(强类型,
|
|
@@ -333,7 +419,10 @@ function dispatchAgentCall(
|
|
|
333
419
|
.withSlot(
|
|
334
420
|
async () => {
|
|
335
421
|
try {
|
|
336
|
-
|
|
422
|
+
// OB2(S7 残留):isOrphaned 谓词注入——旧代际 finalize 在 trace.update 前被
|
|
423
|
+
// 拦截(判定语义与下方 .then/.catch 守卫同一 isOrphanedCall,详见
|
|
424
|
+
// execute-agent-call.ts finalizeCall 文档注释)。
|
|
425
|
+
await executeAgentCall(call, deps.runner, run.state.budget, signal, run.state.trace, onEvent, stream, () => isOrphanedCall(run, msg.callId, call));
|
|
337
426
|
} finally {
|
|
338
427
|
stream?.dispose();
|
|
339
428
|
}
|
|
@@ -343,10 +432,20 @@ function dispatchAgentCall(
|
|
|
343
432
|
.then(() => {
|
|
344
433
|
// 清除 live record:终态已由 executeAgentCall → finalizeCall 写入 node.result,
|
|
345
434
|
// live 不再需要(且含可变状态,不保留)。无论 stale 与否都清,避免内存泄漏。
|
|
346
|
-
// M4: 必须在 stale guard
|
|
435
|
+
// M4: 必须在 stale guard 之前清,否则跨 rebuild 的迟到 completion 会累积 live record。
|
|
347
436
|
node.live = undefined;
|
|
348
|
-
//
|
|
437
|
+
// run 终止(终态)后到达的 stale completion 不写 state
|
|
349
438
|
if (run.state.status !== "running") return;
|
|
439
|
+
// 孤儿 call 守卫(S7-second 竞态):rebuild 的 discardInFlightCalls 已移除本
|
|
440
|
+
// call、或重跑 dispatch 已用新实例替换同 callId 条目时,本 completion 属于旧
|
|
441
|
+
// runtime 代际。postAgentResult 的投递目标是 run.runtime(已是新 worker),
|
|
442
|
+
// 迟到结果会劫持新 worker 内重跑 agent() 的 pending Promise——跳过投递 /
|
|
443
|
+
// budget 同步 / 持久化,仅留日志。executeAgentCall 内 finalizeCall 的
|
|
444
|
+
// trace.update 若已命中重跑新节点(瞬时污染),由重跑完成时的 update 覆盖。
|
|
445
|
+
if (isOrphanedCall(run, msg.callId, call)) {
|
|
446
|
+
deps.log?.("debug", "workflow:error-recovery", "orphan agent call completion dropped", { runId: run.runId, callId: msg.callId });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
350
449
|
if (call.result) postAgentResult(run, msg.callId, call.result, false);
|
|
351
450
|
// D-12 regression fix (round-2 #1):executeAgentCall 内 consume/incrementCallCount
|
|
352
451
|
// 后同步 worker $BUDGET(否则 $BUDGET.spent()/remaining() 恒为 0)
|
|
@@ -401,6 +500,15 @@ function dispatchAgentCall(
|
|
|
401
500
|
// Promise 永不 resolve → agent() 永久 await → worker 脚本挂死。构造 failed AgentResult
|
|
402
501
|
//(与 resolveAgentOpts 失败路径 L262-275 一致的模式)postAgentResult 回 worker,
|
|
403
502
|
// 让 pending Promise resolve(结果为 error),脚本可继续或失败退出。
|
|
503
|
+
// 孤儿 call 守卫(与 .then 对称,S7-second 竞态):rebuild 后本 call 已被 discard
|
|
504
|
+
// 移除/替换——markDone 虽在孤儿实例上无害,但 trace.update 会污染重跑新建的同
|
|
505
|
+
// stepIndex 节点、postAgentResult 会劫持新 worker 的同 callId pending。孤儿时只
|
|
506
|
+
// 留日志,全部跳过。node.live 无条件先清(旧节点已脱离 trace,防御性统一)。
|
|
507
|
+
node.live = undefined;
|
|
508
|
+
if (isOrphanedCall(run, msg.callId, call)) {
|
|
509
|
+
deps.log?.("debug", "workflow:error-recovery", "orphan agent call failure dropped", { runId: run.runId, callId: msg.callId });
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
404
512
|
const errorResult: AgentResult = { content: "", error: message };
|
|
405
513
|
// call 已 done(executeAgentCall 内 finalizeCall 已 markDone)时跳过,避免重复 markDone。
|
|
406
514
|
// status 理论上必为 running(executeAgentCall L130 markRunning 先于 reject),pending
|
|
@@ -411,9 +519,8 @@ function dispatchAgentCall(
|
|
|
411
519
|
}
|
|
412
520
|
// state 一致性三件套(与 resolveAgentOpts 失败 L268-276 / .then L319-325 对等):
|
|
413
521
|
// trace 标 failed + 清 live record(防泄漏)+ 持久化(catch 恰是最需留证的场景)。
|
|
414
|
-
// stale 终态(run 已
|
|
415
|
-
// optional chaining 跳过 worker 回发;trace/state
|
|
416
|
-
node.live = undefined;
|
|
522
|
+
// stale 终态(run 已 done)时 run.runtime 为 undefined,postAgentResult 用
|
|
523
|
+
// optional chaining 跳过 worker 回发;trace/state 写入仍执行(无害,终态快照已存)。
|
|
417
524
|
run.state.trace.update(msg.callId, {
|
|
418
525
|
status: "failed",
|
|
419
526
|
result: errorResult,
|
|
@@ -450,7 +557,7 @@ export function makeSerializeFailedResult(
|
|
|
450
557
|
* 异步 postMessage(workflow-result) 回 worker。
|
|
451
558
|
*
|
|
452
559
|
* onWorkflowCall 未注入时(向后兼容),返回 error result 让脚本 soft-fail。
|
|
453
|
-
* 与 dispatchAgentCall 对称:异步触发(不 await),stale
|
|
560
|
+
* 与 dispatchAgentCall 对称:异步触发(不 await),stale 完成守卫(终态不发)。
|
|
454
561
|
*/
|
|
455
562
|
function dispatchWorkflowCall(
|
|
456
563
|
run: WorkflowRun,
|
|
@@ -619,9 +726,9 @@ export async function handleWorkerError(
|
|
|
619
726
|
deps: LifecycleDeps,
|
|
620
727
|
handlers: WorkerHandlers,
|
|
621
728
|
): Promise<void> {
|
|
622
|
-
// 与 handleWorkerMessage
|
|
623
|
-
//
|
|
624
|
-
if (isTerminal(run)
|
|
729
|
+
// 与 handleWorkerMessage 对称——终态(done)丢弃 stale error。
|
|
730
|
+
// 否则终态后到达的 worker error 仍会 workerErrorCount++(污染跨 runtime 计数)。
|
|
731
|
+
if (isTerminal(run)) return;
|
|
625
732
|
|
|
626
733
|
const count = (run.meta.workerErrorCount ?? 0) + 1;
|
|
627
734
|
run.meta.workerErrorCount = count;
|
|
@@ -664,7 +771,7 @@ export async function handleWorkerExit(
|
|
|
664
771
|
): Promise<void> {
|
|
665
772
|
// G-025: stale exit 事件丢弃(handle 已不是当前 runtime 的 worker)
|
|
666
773
|
if (!handle.isCurrent) return;
|
|
667
|
-
if (isTerminal(run)
|
|
774
|
+
if (isTerminal(run)) return;
|
|
668
775
|
|
|
669
776
|
if (code === 0) return; // 正常退出,no-op
|
|
670
777
|
|
|
@@ -695,8 +802,8 @@ export async function handleScriptError(
|
|
|
695
802
|
deps: LifecycleDeps,
|
|
696
803
|
handlers: WorkerHandlers,
|
|
697
804
|
): Promise<void> {
|
|
698
|
-
// 与 handleWorkerMessage/handleWorkerError
|
|
699
|
-
if (isTerminal(run)
|
|
805
|
+
// 与 handleWorkerMessage/handleWorkerError 对称——终态守卫前置。
|
|
806
|
+
if (isTerminal(run)) return;
|
|
700
807
|
|
|
701
808
|
// P2-2: 捕获 worker 诊断日志
|
|
702
809
|
// L9: 追加而非覆盖
|
|
@@ -733,8 +840,8 @@ export async function handleScriptError(
|
|
|
733
840
|
/**
|
|
734
841
|
* 退避后重建 RunRuntime(G3-001 整重建)。
|
|
735
842
|
*
|
|
736
|
-
* 退避期间 run 可能被
|
|
737
|
-
*
|
|
843
|
+
* 退避期间 run 可能被 abort(转终态 done)——rebuildRuntime 前重检状态,终态时
|
|
844
|
+
* 跳过重建(避免给已终止的 run 启新 worker)。
|
|
738
845
|
*/
|
|
739
846
|
async function scheduleRebuild(
|
|
740
847
|
run: WorkflowRun,
|
|
@@ -749,7 +856,7 @@ async function scheduleRebuild(
|
|
|
749
856
|
await delay(backoffDelay(retryIndex));
|
|
750
857
|
|
|
751
858
|
// 退避期间状态可能变化——重检
|
|
752
|
-
if (isTerminal(run)
|
|
859
|
+
if (isTerminal(run)) return;
|
|
753
860
|
|
|
754
861
|
rebuildRuntime(run, deps, handlers);
|
|
755
862
|
}
|
|
@@ -41,14 +41,24 @@ const BACKOFF_EXPONENT_BASE = 2;
|
|
|
41
41
|
const MAX_ATTEMPTS = 3;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Stale context 检测模式(P1-5
|
|
44
|
+
* Stale context 检测模式(P1-5;W4b 对齐 pi 0.84.x 真实文案)。
|
|
45
45
|
*
|
|
46
46
|
* pi session context 被 compact/cancel 时报告的模式。这种情况下重试无意义——
|
|
47
47
|
* 同样的 call 会再次失败。直接 markDone failed 终止单次调用。
|
|
48
|
+
*
|
|
49
|
+
* W4b:原 "stale context"/"stalecontext" 与 pi 真实文案零匹配(真实文案为
|
|
50
|
+
* "This extension ctx is stale after session replacement or reload. ..."——
|
|
51
|
+
* runner.ts:544(dist runner.js:352),词序是 "ctx is stale" 而非 "stale context"),stale 分诊对
|
|
52
|
+
* 真实文案失效。现对齐:
|
|
53
|
+
* - "ctx is stale":真实文案核心子串(词序修正)
|
|
54
|
+
* - "stale after session replacement":scheduler 已验证 marker(runtime.ts
|
|
55
|
+
* STALE_CTX_MARKER,同文案锚定)
|
|
56
|
+
* - "context canceled"/"aborted":保留——abort 族错误同样不重试(signal.aborted
|
|
57
|
+
* 分支的先行分诊,防边界竞态漏网),删除会放宽重试语义。
|
|
48
58
|
*/
|
|
49
59
|
export const STALE_CONTEXT_PATTERNS = [
|
|
50
|
-
"stale
|
|
51
|
-
"
|
|
60
|
+
"ctx is stale",
|
|
61
|
+
"stale after session replacement",
|
|
52
62
|
"context canceled",
|
|
53
63
|
"aborted",
|
|
54
64
|
] as const;
|
|
@@ -78,13 +88,31 @@ function backoffDelay(retryIndex: number): number {
|
|
|
78
88
|
*
|
|
79
89
|
* 成功:status="completed";失败:status="failed"。
|
|
80
90
|
* traceNode.stepIndex === call.id(D-10 单源,调用方保证)。
|
|
91
|
+
*
|
|
92
|
+
* 孤儿守卫(OB2,S7 残留):isOrphaned 谓词为 true 时跳过 trace.update——
|
|
93
|
+
* rebuild 竞态窗口中,重跑 dispatch 已 append 同 stepIndex 新节点,旧代际
|
|
94
|
+
* finalize 的 update 会命中新节点,TUI/中间快照短暂可见错误终态。正确性论证:
|
|
95
|
+
* 运行期 calls Map 写点仅 discardInFlightCalls 的 delete 与 dispatchAgentCall 的
|
|
96
|
+
* set 两族(error-recovery.ts isOrphanedCall 文档注释既定),故实例不等 ⟺ 本
|
|
97
|
+
* finalize 属于被丢弃/被替换的旧代际——与 dispatch 层 .then/.catch 守卫
|
|
98
|
+
* (S7-second 修复,8353f6b60)同一判定语义,本守卫只是把它前移到 trace.update
|
|
99
|
+
* 之前。markDone 与 sessionId/sessionFile 同步保留(markDone 在孤儿实例上无害,
|
|
100
|
+
* dispatch 层 catch 路径依赖 call.status 语义)。跳过时不记日志——本文件是纯
|
|
101
|
+
* 函数层无日志通道,dispatch 层 .then 守卫的 orphan completion dropped 日志已
|
|
102
|
+
* 覆盖同一事件的可观察性。
|
|
81
103
|
*/
|
|
82
|
-
function finalizeCall(
|
|
104
|
+
function finalizeCall(
|
|
105
|
+
call: AgentCall,
|
|
106
|
+
result: AgentResult,
|
|
107
|
+
trace: Trace,
|
|
108
|
+
isOrphaned?: () => boolean,
|
|
109
|
+
): void {
|
|
83
110
|
call.markDone(result);
|
|
84
111
|
const status = result.error === undefined ? "completed" : "failed";
|
|
85
112
|
// 同步 AgentCall 的 sessionId/sessionFile(对齐 trace 节点,持久化 + reset 用)
|
|
86
113
|
if (result.sessionId !== undefined) call.setSessionId(result.sessionId);
|
|
87
114
|
if (result.sessionFile !== undefined) call.setSessionFile(result.sessionFile);
|
|
115
|
+
if (isOrphaned?.()) return;
|
|
88
116
|
trace.update(call.id, {
|
|
89
117
|
status,
|
|
90
118
|
result,
|
|
@@ -124,6 +152,11 @@ function delay(ms: number): Promise<void> {
|
|
|
124
152
|
* @param budget Budget 值对象(consume + isExceeded 检查)
|
|
125
153
|
* @param signal AbortSignal(runner.run 传播;abort 后不重试)
|
|
126
154
|
* @param trace Trace 值对象(finalizeCall 时 update)
|
|
155
|
+
* @param onEvent 实时事件回调(live record 更新用)
|
|
156
|
+
* @param stream streaming sink(透传 runner.run)
|
|
157
|
+
* @param isOrphaned 孤儿判定谓词(OB2,可选,默认恒 false):true 时 finalizeCall
|
|
158
|
+
* 跳过 trace.update(判定语义与正确性论证见 finalizeCall 文档注释)。递归重试
|
|
159
|
+
* 透传本谓词。
|
|
127
160
|
*/
|
|
128
161
|
export async function executeAgentCall(
|
|
129
162
|
call: AgentCall,
|
|
@@ -133,6 +166,7 @@ export async function executeAgentCall(
|
|
|
133
166
|
trace: Trace,
|
|
134
167
|
onEvent?: (event: AgentEvent) => void,
|
|
135
168
|
stream?: SubagentStream,
|
|
169
|
+
isOrphaned?: () => boolean,
|
|
136
170
|
): Promise<void> {
|
|
137
171
|
call.markRunning();
|
|
138
172
|
|
|
@@ -145,21 +179,21 @@ export async function executeAgentCall(
|
|
|
145
179
|
|
|
146
180
|
// stale-context:不重试(P1-5)
|
|
147
181
|
if (result.error !== undefined && isStaleContextErrorMsg(result.error)) {
|
|
148
|
-
finalizeCall(call, result, trace);
|
|
182
|
+
finalizeCall(call, result, trace, isOrphaned);
|
|
149
183
|
budget.incrementCallCount();
|
|
150
184
|
return;
|
|
151
185
|
}
|
|
152
186
|
|
|
153
187
|
// signal 已 abort:调用方终止,不重试(避免无意义的递归)
|
|
154
188
|
if (signal.aborted) {
|
|
155
|
-
finalizeCall(call, result, trace);
|
|
189
|
+
finalizeCall(call, result, trace, isOrphaned);
|
|
156
190
|
budget.incrementCallCount();
|
|
157
191
|
return;
|
|
158
192
|
}
|
|
159
193
|
|
|
160
194
|
// 预算超限:不重试(重试只会突破预算且无意义)
|
|
161
195
|
if (result.error !== undefined && budget.isExceeded()) {
|
|
162
|
-
finalizeCall(call, result, trace);
|
|
196
|
+
finalizeCall(call, result, trace, isOrphaned);
|
|
163
197
|
budget.incrementCallCount();
|
|
164
198
|
return;
|
|
165
199
|
}
|
|
@@ -169,15 +203,15 @@ export async function executeAgentCall(
|
|
|
169
203
|
await delay(backoffDelay(call.attempts));
|
|
170
204
|
// 退避期间 signal 可能 abort
|
|
171
205
|
if (signal.aborted) {
|
|
172
|
-
finalizeCall(call, result, trace);
|
|
206
|
+
finalizeCall(call, result, trace, isOrphaned);
|
|
173
207
|
budget.incrementCallCount();
|
|
174
208
|
return;
|
|
175
209
|
}
|
|
176
|
-
await executeAgentCall(call, runner, budget, signal, trace, onEvent, stream);
|
|
210
|
+
await executeAgentCall(call, runner, budget, signal, trace, onEvent, stream, isOrphaned);
|
|
177
211
|
return;
|
|
178
212
|
}
|
|
179
213
|
|
|
180
214
|
// 终态(成功或达到重试上限的失败)
|
|
181
|
-
finalizeCall(call, result, trace);
|
|
215
|
+
finalizeCall(call, result, trace, isOrphaned);
|
|
182
216
|
budget.incrementCallCount();
|
|
183
217
|
}
|