@zhushanwen/pi-subagent-workflow 7.3.4 → 8.0.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 +39 -12
- package/agents/analyst.md +61 -0
- package/agents/coder.md +70 -0
- package/agents/debugger.md +67 -0
- package/agents/doc-reviewer.md +3 -3
- package/agents/explorer.md +50 -18
- package/agents/general-purpose.md +19 -8
- package/agents/orchestrator.md +37 -32
- package/agents/planner.md +45 -11
- package/agents/researcher.md +53 -11
- package/agents/reviewer.md +74 -0
- package/package.json +1 -1
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/agent-registry.test.ts +13 -11
- 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 +170 -14
- 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 +25 -7
- 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 +158 -52
- 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 +598 -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 +292 -89
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +13 -12
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +90 -13
- package/src/execution/host-mode.ts +1 -1
- package/src/execution/lifecycle-manager.ts +484 -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-store.ts +554 -107
- package/src/execution/session-pending.ts +116 -45
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +290 -75
- 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 +867 -138
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +204 -22
- package/src/execution/worktree-manager.ts +128 -49
- package/src/execution/worktree-registry.ts +13 -2
- package/src/index.ts +277 -19
- 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 +18 -5
- package/src/interface/__tests__/tool-render.test.ts +15 -13
- 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 +44 -17
- 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 +82 -17
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +11 -24
- package/src/interface/tool-workflow.ts +20 -35
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +1 -1
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +325 -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 +95 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +657 -18
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/skill-discovery.test.ts +157 -0
- package/src/orchestration/__tests__/test-mocks.ts +191 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +98 -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 +1 -1
- 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 +133 -29
- package/src/orchestration/execute-agent-call.ts +31 -7
- package/src/orchestration/jsonl-run-store.ts +287 -40
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +135 -132
- 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 +29 -10
- 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 +55 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +16 -0
- package/src/shared/resource-discovery.ts +147 -58
- package/src/shared/schema-jsonify.ts +53 -0
- package/workflows/README.md +4 -4
- package/agents/code-reviewer.md +0 -47
- package/agents/context-builder.md +0 -21
- package/agents/oracle.md +0 -34
- package/agents/worker.md +0 -20
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import type { Component } from "@earendil-works/pi-tui";
|
|
23
23
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
24
24
|
|
|
25
|
+
import { displayAgentName } from "../shared/agent-ref.ts";
|
|
25
26
|
import {
|
|
26
27
|
firstLine,
|
|
27
28
|
padToVisible,
|
|
@@ -52,11 +53,16 @@ const MIN_BORDER_WIDTH = BORDER_CHARS + INNER_PAD_TOTAL + 1;
|
|
|
52
53
|
*/
|
|
53
54
|
interface BgNotifyRecord {
|
|
54
55
|
id: string;
|
|
55
|
-
|
|
56
|
+
/** v4 B-1: closed(终态,含 cancelled)或 running(对话模式轮次完成,旧 idle)。 */
|
|
57
|
+
status: "running" | "closed";
|
|
58
|
+
/** L2 关闭原因子枚举(仅 status="closed" 时有意义)。 */
|
|
59
|
+
closedReason?: string;
|
|
56
60
|
agent: string;
|
|
57
61
|
model?: string;
|
|
58
62
|
result?: string;
|
|
59
63
|
error?: string;
|
|
64
|
+
/** 对话轮次计数(仅 idle 有意义)。 */
|
|
65
|
+
round?: number;
|
|
60
66
|
/** [MF#1] worktree background 完成通知携带的 patch 文件路径。 */
|
|
61
67
|
patchFile?: string;
|
|
62
68
|
}
|
|
@@ -207,17 +213,34 @@ function sanitizeAnsiForBg(text: string): string {
|
|
|
207
213
|
function renderRecordLines(record: BgNotifyRecord, t: ThemeLike): string[] {
|
|
208
214
|
const glyph = statusGlyph(record.status);
|
|
209
215
|
const icon = glyph.icon ?? "•";
|
|
210
|
-
const agent = truncLine(record.agent, AGENT_MAX_WIDTH);
|
|
216
|
+
const agent = truncLine(displayAgentName(record.agent), AGENT_MAX_WIDTH);
|
|
211
217
|
// model 段:agent 后、状态描述前,accent 色。空则省略(向后兼容旧 record)。
|
|
212
218
|
const modelPart = record.model
|
|
213
219
|
? ` ${t.fg("dim", "·")} ${t.fg("accent", truncLine(record.model, MODEL_MAX_WIDTH))}`
|
|
214
220
|
: "";
|
|
215
|
-
|
|
221
|
+
// v4 B-1: closed 统一终态(含 cancelled)。按 closedReason 派生文案。
|
|
222
|
+
const reason = record.closedReason ?? "gc";
|
|
223
|
+
let verb: string;
|
|
224
|
+
if (reason === "cancelled") {
|
|
225
|
+
verb = "cancelled";
|
|
226
|
+
} else if (reason === "gc" && record.error) {
|
|
227
|
+
verb = "failed";
|
|
228
|
+
} else {
|
|
229
|
+
verb = "finished";
|
|
230
|
+
}
|
|
216
231
|
const idShort = shortId(record.id);
|
|
217
232
|
const head = `${t.fg(glyph.color, icon)} ${t.bold(agent)}${modelPart}${t.fg("dim", ` — background subagent ${verb} - ${idShort}`)}`;
|
|
218
233
|
|
|
219
234
|
switch (record.status) {
|
|
220
|
-
case "
|
|
235
|
+
case "closed": {
|
|
236
|
+
// v4 B-1: closed 统一终态(含 cancelled)。cancelled 无正文;失败显示错误;否则结果/patch。
|
|
237
|
+
const r = record.closedReason ?? "gc";
|
|
238
|
+
if (r === "cancelled") {
|
|
239
|
+
return [head];
|
|
240
|
+
}
|
|
241
|
+
if (r === "gc" && record.error) {
|
|
242
|
+
return [head, t.fg("dim", truncLine(`Error: ${firstLineSanitized(record.error)}`, BODY_MAX_WIDTH))];
|
|
243
|
+
}
|
|
221
244
|
if (!record.result && !record.patchFile) return [head];
|
|
222
245
|
const lines: string[] = [];
|
|
223
246
|
if (record.result) {
|
|
@@ -229,10 +252,9 @@ function renderRecordLines(record: BgNotifyRecord, t: ThemeLike): string[] {
|
|
|
229
252
|
}
|
|
230
253
|
return [head, ...lines];
|
|
231
254
|
}
|
|
232
|
-
case "
|
|
233
|
-
return [head, t.fg("dim", truncLine(`Error: ${record.error ? firstLineSanitized(record.error) : "(unknown)"}`, BODY_MAX_WIDTH))];
|
|
234
|
-
case "cancelled":
|
|
255
|
+
case "running":
|
|
235
256
|
default:
|
|
257
|
+
// v4 B-1: 对话模式轮次完成(旧 idle 折入 running)——仅标题行。
|
|
236
258
|
return [head];
|
|
237
259
|
}
|
|
238
260
|
}
|
|
@@ -263,7 +285,7 @@ function extractBgNotifyRecord(details: unknown): BgNotifyRecord | undefined {
|
|
|
263
285
|
const status = d.status;
|
|
264
286
|
const agent = d.agent;
|
|
265
287
|
if (
|
|
266
|
-
(status !== "
|
|
288
|
+
(status !== "closed" && status !== "running") ||
|
|
267
289
|
typeof agent !== "string"
|
|
268
290
|
) {
|
|
269
291
|
return undefined;
|
|
@@ -275,6 +297,8 @@ function extractBgNotifyRecord(details: unknown): BgNotifyRecord | undefined {
|
|
|
275
297
|
model: typeof d.model === "string" ? d.model : undefined,
|
|
276
298
|
result: typeof d.result === "string" ? d.result : undefined,
|
|
277
299
|
error: typeof d.error === "string" ? d.error : undefined,
|
|
300
|
+
closedReason: typeof d.closedReason === "string" ? d.closedReason : undefined,
|
|
301
|
+
round: typeof d.round === "number" ? d.round : undefined,
|
|
278
302
|
// [MF#1] 提取 patchFile(worktree background 完成通知携带)。
|
|
279
303
|
patchFile: typeof d.patchFile === "string" ? d.patchFile : undefined,
|
|
280
304
|
};
|
|
@@ -16,23 +16,36 @@ export type SubagentRpcAction =
|
|
|
16
16
|
|
|
17
17
|
/** /workflows RPC action 判别联合。 */
|
|
18
18
|
export type WorkflowRpcAction =
|
|
19
|
-
| { action: "pause"; runId: string }
|
|
20
|
-
| { action: "resume"; runId: string }
|
|
21
19
|
| { action: "abort"; runId: string }
|
|
22
|
-
| { action: "lifecycle-missing-id"; verb:
|
|
20
|
+
| { action: "lifecycle-missing-id"; verb: LifecycleVerb }
|
|
21
|
+
| { action: "lifecycle-removed"; verb: "pause" | "resume" }
|
|
23
22
|
| { action: "noop" };
|
|
24
23
|
|
|
25
24
|
/** workflow lifecycle verb 类型。 */
|
|
26
|
-
type LifecycleVerb = "
|
|
25
|
+
type LifecycleVerb = "abort";
|
|
27
26
|
|
|
28
27
|
/** workflow lifecycle verb 集合。 */
|
|
29
|
-
const LIFECYCLE_VERBS: ReadonlySet<LifecycleVerb> = new Set(["
|
|
28
|
+
const LIFECYCLE_VERBS: ReadonlySet<LifecycleVerb> = new Set(["abort"]);
|
|
30
29
|
|
|
31
30
|
/** verb 是否为 lifecycle action(类型守卫,收窄到 LifecycleVerb)。 */
|
|
32
31
|
function isLifecycleVerb(verb: string): verb is LifecycleVerb {
|
|
33
32
|
return LIFECYCLE_VERBS.has(verb as LifecycleVerb);
|
|
34
33
|
}
|
|
35
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 已移除的 lifecycle verb 集合(pause/resume——run 一次性生命周期化后删除,
|
|
37
|
+
* 解析为 lifecycle-removed 提示,而非 unknown noop)。
|
|
38
|
+
*/
|
|
39
|
+
const REMOVED_LIFECYCLE_VERBS: ReadonlySet<"pause" | "resume"> = new Set([
|
|
40
|
+
"pause",
|
|
41
|
+
"resume",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/** verb 是否为已移除的 lifecycle action(类型守卫,收窄到 "pause" | "resume")。 */
|
|
45
|
+
function isRemovedLifecycleVerb(verb: string): verb is "pause" | "resume" {
|
|
46
|
+
return REMOVED_LIFECYCLE_VERBS.has(verb as "pause" | "resume");
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
/**
|
|
37
50
|
* 解析 /subagents RPC 命令字符串。
|
|
38
51
|
*
|
|
@@ -60,8 +73,11 @@ export function parseSubagentRpcCommand(argsStr: string): SubagentRpcAction {
|
|
|
60
73
|
* 解析 /workflows RPC 命令字符串。
|
|
61
74
|
*
|
|
62
75
|
* 支持格式:
|
|
63
|
-
* - `
|
|
64
|
-
* - `
|
|
76
|
+
* - `abort <runId>` → { action: "abort", runId }
|
|
77
|
+
* - `abort`(无 runId)→ { action: "lifecycle-missing-id", verb: "abort" }
|
|
78
|
+
* - `pause|resume ...`(已移除的 lifecycle verb,带或不带 runId 均同)→
|
|
79
|
+
* { action: "lifecycle-removed", verb }——removed verb 优先于 missing-id 判定
|
|
80
|
+
* (提示语义优先:用户应得知能力已删除并获 abort 指引,而非被引导补 runId)
|
|
65
81
|
* - 其他(空 / 未知 action / 无参)→ { action: "noop" }
|
|
66
82
|
*/
|
|
67
83
|
export function parseWorkflowRpcCommand(argsStr: string): WorkflowRpcAction {
|
|
@@ -69,6 +85,9 @@ export function parseWorkflowRpcCommand(argsStr: string): WorkflowRpcAction {
|
|
|
69
85
|
if (args.length === 0) return { action: "noop" };
|
|
70
86
|
|
|
71
87
|
const [verb, runId] = args;
|
|
88
|
+
if (isRemovedLifecycleVerb(verb)) {
|
|
89
|
+
return { action: "lifecycle-removed", verb };
|
|
90
|
+
}
|
|
72
91
|
if (isLifecycleVerb(verb)) {
|
|
73
92
|
if (!runId) return { action: "lifecycle-missing-id", verb };
|
|
74
93
|
return { action: verb, runId };
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - /workflow save <name> → 用 workflow-script tool { action: "save" }
|
|
12
12
|
* - /workflow delete <name> → 用 workflow-script tool { action: "delete" }
|
|
13
13
|
*
|
|
14
|
-
* 层归属:Interface。依赖 Pi SDK + Engine lifecycle(
|
|
14
|
+
* 层归属:Interface。依赖 Pi SDK + Engine lifecycle(abort,注入 ViewActions)
|
|
15
15
|
* + WorkflowsView(读 WorkflowRun 聚合根)。
|
|
16
16
|
*
|
|
17
17
|
* 参考:
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
23
23
|
|
|
24
24
|
import type { LauncherDeps } from "../orchestration/launcher.ts";
|
|
25
|
-
import { abortRun
|
|
25
|
+
import { abortRun } from "../orchestration/lifecycle.ts";
|
|
26
26
|
import type { WorkflowRun } from "../orchestration/models/workflow-run.ts";
|
|
27
27
|
import { parseWorkflowRpcCommand } from "./command-actions.ts";
|
|
28
28
|
import { createWorkflowsView, type ViewActions } from "./views/WorkflowsView.ts";
|
|
@@ -30,10 +30,9 @@ import { createWorkflowsView, type ViewActions } from "./views/WorkflowsView.ts"
|
|
|
30
30
|
/** runId 截断长度(显示用)。 */
|
|
31
31
|
const RUNID_SHORT = 8;
|
|
32
32
|
|
|
33
|
-
/** status 显示顺序:running
|
|
33
|
+
/** status 显示顺序:running 优先(活跃态在前),再 startedAt 倒序。 */
|
|
34
34
|
const STATUS_ORDER: Record<string, number> = {
|
|
35
35
|
running: 0,
|
|
36
|
-
paused: 1,
|
|
37
36
|
done: 2,
|
|
38
37
|
};
|
|
39
38
|
/** 未知 status 的默认排序权重(排在已知 status 之后)。 */
|
|
@@ -52,12 +51,12 @@ const UNKNOWN_STATUS_WEIGHT = 9;
|
|
|
52
51
|
* · 1 run → 直接打开
|
|
53
52
|
* · 多 runs → select 选 → 打开选中 run 的 view
|
|
54
53
|
*
|
|
55
|
-
* ViewActions(
|
|
54
|
+
* ViewActions(abort)由本 command 注入——view 本身不持 lifecycle 依赖,
|
|
56
55
|
* 只通过 actions 回调与 engine 交互(解耦,便于 view 单测)。
|
|
57
56
|
*
|
|
58
57
|
* @param api ExtensionAPI
|
|
59
58
|
* @param getRuns 获取当前 session 的 runs(Map<runId, WorkflowRun>)
|
|
60
|
-
* @param deps LauncherDeps(lifecycle
|
|
59
|
+
* @param deps LauncherDeps(lifecycle abort 用)
|
|
61
60
|
*/
|
|
62
61
|
export function registerWorkflowsCommand(
|
|
63
62
|
api: ExtensionAPI,
|
|
@@ -65,7 +64,7 @@ export function registerWorkflowsCommand(
|
|
|
65
64
|
deps: LauncherDeps,
|
|
66
65
|
): void {
|
|
67
66
|
api.registerCommand("workflows", {
|
|
68
|
-
description: "Open workflow panel. /workflows [runId] | /workflows
|
|
67
|
+
description: "Open workflow panel. /workflows [runId] | /workflows abort <runId>",
|
|
69
68
|
getArgumentCompletions(prefix: string) {
|
|
70
69
|
const trimmed = prefix.trimStart();
|
|
71
70
|
const parts = trimmed.split(/\s+/).filter(Boolean);
|
|
@@ -73,14 +72,12 @@ export function registerWorkflowsCommand(
|
|
|
73
72
|
// 第一级:lifecycle 动词(带尾随空格,选中后继续补 runId)
|
|
74
73
|
if (parts.length <= 1) {
|
|
75
74
|
return [
|
|
76
|
-
{ label: "pause", value: "pause ", description: "Pause a workflow run" },
|
|
77
|
-
{ label: "resume", value: "resume ", description: "Resume a paused workflow run" },
|
|
78
75
|
{ label: "abort", value: "abort ", description: "Abort a workflow run" },
|
|
79
76
|
].filter((opt) => opt.label.startsWith(trimmed.toLowerCase()));
|
|
80
77
|
}
|
|
81
78
|
|
|
82
79
|
// 第二级:lifecycle 动词后补全当前 session 的 runId
|
|
83
|
-
if (parts[0] === "
|
|
80
|
+
if (parts[0] === "abort") {
|
|
84
81
|
try {
|
|
85
82
|
const runs = sortedRuns(getRuns());
|
|
86
83
|
if (runs.length === 0) return null;
|
|
@@ -102,21 +99,25 @@ export function registerWorkflowsCommand(
|
|
|
102
99
|
if (ctx.mode === "rpc") {
|
|
103
100
|
const parsed = parseWorkflowRpcCommand(args);
|
|
104
101
|
switch (parsed.action) {
|
|
105
|
-
case "pause":
|
|
106
|
-
case "resume":
|
|
107
102
|
case "abort": {
|
|
108
103
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const pastTense = parsed.action === "abort" ? "aborted" : `${parsed.action}d`;
|
|
113
|
-
ctx.ui.notify(`Workflow ${parsed.runId}: ${pastTense}`, "info");
|
|
104
|
+
await abortRun(parsed.runId, deps);
|
|
105
|
+
// abort 单态后 pastTense 固定(原三态拼接随 pause/resume 删除)
|
|
106
|
+
ctx.ui.notify(`Workflow ${parsed.runId}: aborted`, "info");
|
|
114
107
|
} catch (err) {
|
|
115
108
|
const msg = err instanceof Error ? err.message : String(err);
|
|
116
|
-
ctx.ui.notify(`Failed to
|
|
109
|
+
ctx.ui.notify(`Failed to abort workflow ${parsed.runId}: ${msg}`, "warning");
|
|
117
110
|
}
|
|
118
111
|
return;
|
|
119
112
|
}
|
|
113
|
+
case "lifecycle-removed":
|
|
114
|
+
// 已移除的 lifecycle verb(pause/resume):run 一次性生命周期后不可挂起,
|
|
115
|
+
// 给定制指引而非 Usage(F3 定稿——提示语义优先于 missing-id)
|
|
116
|
+
ctx.ui.notify(
|
|
117
|
+
`Workflow ${parsed.verb} has been removed — runs are one-shot. To stop a run early: /workflows abort <runId>`,
|
|
118
|
+
"warning",
|
|
119
|
+
);
|
|
120
|
+
return;
|
|
120
121
|
case "lifecycle-missing-id":
|
|
121
122
|
ctx.ui.notify(`Usage: /workflows ${parsed.verb} <runId>`, "warning");
|
|
122
123
|
return;
|
|
@@ -187,7 +188,7 @@ export function registerWorkflowsCommand(
|
|
|
187
188
|
// ── Helpers ──────────────────────────────────────────────────
|
|
188
189
|
|
|
189
190
|
/**
|
|
190
|
-
* 取 runs 按 status(running
|
|
191
|
+
* 取 runs 按 status(running 优先)+ startedAt 倒序排序。
|
|
191
192
|
* 复用 main 的 UX 顺序(活跃态在前,新的在前)。
|
|
192
193
|
*/
|
|
193
194
|
function sortedRuns(runs: Map<string, WorkflowRun>): WorkflowRun[] {
|
|
@@ -205,7 +206,7 @@ function sortedRuns(runs: Map<string, WorkflowRun>): WorkflowRun[] {
|
|
|
205
206
|
/**
|
|
206
207
|
* 打开 WorkflowsView(三级导航 TUI),注入 lifecycle ViewActions。
|
|
207
208
|
*
|
|
208
|
-
* ViewActions 通过 deps 调 lifecycle(
|
|
209
|
+
* ViewActions 通过 deps 调 lifecycle(abort),与 view 解耦——
|
|
209
210
|
* view 单测可注入 mock actions(见 workflows-view.test.ts)。
|
|
210
211
|
*/
|
|
211
212
|
async function openView(
|
|
@@ -215,8 +216,6 @@ async function openView(
|
|
|
215
216
|
deps: LauncherDeps,
|
|
216
217
|
): Promise<void> {
|
|
217
218
|
const actions: ViewActions = {
|
|
218
|
-
pause: (runId: string) => pauseRun(runId, deps),
|
|
219
|
-
resume: (runId: string) => resumeRun(runId, deps),
|
|
220
219
|
abort: (runId: string) => abortRun(runId, deps),
|
|
221
220
|
};
|
|
222
221
|
await createWorkflowsView(run, theme, ctx, actions, deps.store.stateFilePath(run.runId));
|
package/src/interface/format.ts
CHANGED
|
@@ -174,17 +174,9 @@ export function statusGlyph(status: ExecutionStatus): { icon: string | undefined
|
|
|
174
174
|
switch (status) {
|
|
175
175
|
case "running":
|
|
176
176
|
return { icon: undefined, color: "accent" };
|
|
177
|
-
case "
|
|
177
|
+
case "closed":
|
|
178
|
+
// v4 B-1: closed 统一终态(含 cancelled)。默认 ✓ success 色。
|
|
178
179
|
return { icon: "✓", color: "success" };
|
|
179
|
-
case "failed":
|
|
180
|
-
return { icon: "✗", color: "error" };
|
|
181
|
-
case "cancelled":
|
|
182
|
-
return { icon: "■", color: "muted" };
|
|
183
|
-
case "crashed":
|
|
184
|
-
return { icon: "✝", color: "error" };
|
|
185
|
-
default:
|
|
186
|
-
// 防御:运行时 status 可能是意外值(SDK 投影异常/未来新增状态),兜底为 running 语义
|
|
187
|
-
return { icon: undefined, color: "accent" };
|
|
188
180
|
}
|
|
189
181
|
}
|
|
190
182
|
|
|
@@ -235,7 +227,7 @@ export function firstLine(text?: string): string {
|
|
|
235
227
|
/**
|
|
236
228
|
* 从 renderCall/execute 的 unknown args 安全提取 agent 名.
|
|
237
229
|
* 类型守卫窄化(替代 `as { agent?: string }` 全可选断言).
|
|
238
|
-
* 无 agent
|
|
230
|
+
* 无 agent 字段或非空字符串时兜底 DEFAULT_AGENT_NAME(与 service 层 resolveIdentity 一致,
|
|
239
231
|
* 保证 block 标题显示的名与实际加载的 agent.md 相符).
|
|
240
232
|
*/
|
|
241
233
|
export function extractAgentName(args: unknown): string {
|
|
@@ -390,6 +382,20 @@ export function formatDisplayItem(item: DisplayItem, theme: ThemeLike): string {
|
|
|
390
382
|
// ANSI 安全截断
|
|
391
383
|
// ============================================================
|
|
392
384
|
|
|
385
|
+
/**
|
|
386
|
+
* SGR 序列 sticky regex(truncLine 专用)。
|
|
387
|
+
*
|
|
388
|
+
* lastIndex 置位后 exec/test 只在该位置匹配(等价 `flat.slice(pos).match(/^\x1b\[[0-9;]*m/)`,
|
|
389
|
+
* 但零子串分配)。同步代码内「设 lastIndex → 立即用」,无跨调用残留。
|
|
390
|
+
*/
|
|
391
|
+
const SGR_STICKY_RE = /\x1b\[[0-9;]*m/y;
|
|
392
|
+
|
|
393
|
+
/** 判断 s 在 pos 处是否开始一个 SGR 序列(非 SGR ESC——OSC/非 SGR CSI/裸 ESC——返回 false)。 */
|
|
394
|
+
function isSgrStart(s: string, pos: number): boolean {
|
|
395
|
+
SGR_STICKY_RE.lastIndex = pos;
|
|
396
|
+
return SGR_STICKY_RE.test(s);
|
|
397
|
+
}
|
|
398
|
+
|
|
393
399
|
/**
|
|
394
400
|
* 截断文本到 maxWidth 可见宽度(带省略号 `…`,ANSI 安全).
|
|
395
401
|
*
|
|
@@ -400,6 +406,16 @@ export function formatDisplayItem(item: DisplayItem, theme: ThemeLike): string {
|
|
|
400
406
|
* 截断时 `result + activeStyles.join("") + "…"`——重应用 active 样式,背景不断裂.
|
|
401
407
|
* 用 Intl.Segmenter grapheme 切分,正确处理 emoji/CJK/组合字符.
|
|
402
408
|
*
|
|
409
|
+
* 性能(IF12/#18):旧实现逐字符 `flat.slice(end).match(...)` 是 O(n²)(每字符
|
|
410
|
+
* 一次子串分配 + 正则扫描);现改为 `indexOf("\x1b", ...)` 跳到下一 ESC,仅在 ESC
|
|
411
|
+
* 位置做一次 sticky 判定——纯文本段零分配零正则。
|
|
412
|
+
*
|
|
413
|
+
* **非 SGR ESC 行为(等价关键,逐字节等价的定义点)**:OSC(\x1b]0;..\x07)、
|
|
414
|
+
* 非 SGR CSI(\x1b[K)、裸 \x1b 后跟非 [ 字符——均不构成文本段边界,作为纯文本
|
|
415
|
+
* 并入 textPortion 被 segmenter 消费(占 currentWidth、进 result),与旧实现
|
|
416
|
+
* 「while 停点集合 = SGR 序列起点」完全一致。输出由 __fixtures__/truncline.snapshot.json
|
|
417
|
+
* (改造前实现生成)逐字节锚定。
|
|
418
|
+
*
|
|
403
419
|
* 移植自 pi-subagents render.ts:44-89.
|
|
404
420
|
*/
|
|
405
421
|
export function truncLine(text: string, maxWidth: number): string {
|
|
@@ -417,8 +433,9 @@ export function truncLine(text: string, maxWidth: number): string {
|
|
|
417
433
|
let i = 0;
|
|
418
434
|
|
|
419
435
|
while (i < flat.length) {
|
|
420
|
-
// 捕获 ANSI SGR
|
|
421
|
-
|
|
436
|
+
// 捕获 ANSI SGR 序列(sticky:只在 i 处匹配,等价旧 slice(i).match(/^.../m))
|
|
437
|
+
SGR_STICKY_RE.lastIndex = i;
|
|
438
|
+
const ansiMatch = SGR_STICKY_RE.exec(flat);
|
|
422
439
|
if (ansiMatch) {
|
|
423
440
|
const code = ansiMatch[0];
|
|
424
441
|
result += code;
|
|
@@ -432,10 +449,20 @@ export function truncLine(text: string, maxWidth: number): string {
|
|
|
432
449
|
continue;
|
|
433
450
|
}
|
|
434
451
|
|
|
435
|
-
// 找到下一段纯文本(非 ANSI)
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
452
|
+
// 找到下一段纯文本(非 ANSI)的边界:下一处 SGR 序列起点(或串尾)。
|
|
453
|
+
// indexOf 跳到 ESC 候选位,仅对 ESC 位置做一次 sticky 判定;非 SGR ESC
|
|
454
|
+
// (OSC/\x1b[K/裸 \x1b)不构成边界、并入文本段(end 推进到该 ESC 之后继续找),
|
|
455
|
+
// 与旧逐字符 while 的停点集合逐字节一致。
|
|
456
|
+
let end = flat.length;
|
|
457
|
+
for (
|
|
458
|
+
let escPos = flat.indexOf("\x1b", i);
|
|
459
|
+
escPos !== -1;
|
|
460
|
+
escPos = flat.indexOf("\x1b", escPos + 1)
|
|
461
|
+
) {
|
|
462
|
+
if (isSgrStart(flat, escPos)) {
|
|
463
|
+
end = escPos;
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
439
466
|
}
|
|
440
467
|
|
|
441
468
|
// 按 grapheme 迭代这段文本,累加到 targetWidth
|
|
@@ -31,17 +31,17 @@ type TreeStatus = NonNullable<TreeItem["status"]>;
|
|
|
31
31
|
/**
|
|
32
32
|
* 把 workflow/subagent 状态字符串映射到 list-tree 的三态 status。
|
|
33
33
|
*
|
|
34
|
-
* 输入可能是纯 RunStatus(running/
|
|
34
|
+
* 输入可能是纯 RunStatus(running/done)、RunStatus+reason 组合
|
|
35
35
|
* (如 "done (failed)"),或 subagent status(running/done/failed/cancelled/crashed)。
|
|
36
36
|
*
|
|
37
37
|
* 映射规则:
|
|
38
|
-
* - running
|
|
38
|
+
* - running → running
|
|
39
39
|
* - failed / aborted / error / crashed / cancelled / budget_limited / time_limited → failed
|
|
40
40
|
* - 其他(done / completed / success / pending)→ done
|
|
41
41
|
*/
|
|
42
42
|
export function mapRunStatus(status: string): TreeStatus {
|
|
43
43
|
const s = status.toLowerCase();
|
|
44
|
-
if (s.includes("running")
|
|
44
|
+
if (s.includes("running")) return "running";
|
|
45
45
|
if (
|
|
46
46
|
s.includes("failed") ||
|
|
47
47
|
s.includes("abort") ||
|
|
@@ -59,14 +59,12 @@ export function mapRunStatus(status: string): TreeStatus {
|
|
|
59
59
|
/**
|
|
60
60
|
* 把状态字符串映射到 TreeItem.icon。
|
|
61
61
|
*
|
|
62
|
-
* running
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* 其他(done) → check
|
|
62
|
+
* running → circle(进行中)
|
|
63
|
+
* failed/abort/cancel/crash → cross
|
|
64
|
+
* 其他(done) → check
|
|
66
65
|
*/
|
|
67
66
|
export function mapRunIcon(status: string): TreeItemIcon {
|
|
68
67
|
const s = status.toLowerCase();
|
|
69
|
-
if (s.includes("paused")) return "pause";
|
|
70
68
|
if (s.includes("running")) return "circle";
|
|
71
69
|
if (
|
|
72
70
|
s.includes("failed") ||
|
package/src/interface/helpers.ts
CHANGED
|
@@ -25,6 +25,139 @@ import { mapRunIcon, mapRunStatus } from "./gui-mappers.ts";
|
|
|
25
25
|
const JSON_INDENT = 2;
|
|
26
26
|
const MAX_RESULT_LENGTH = 8000;
|
|
27
27
|
|
|
28
|
+
// ── bounded pretty 序列化(IF13/#19,TC5/ES5,helpers 私有)─────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* bounded JSON pretty 序列化:只生成会被保留的前缀(≤8000 输出与
|
|
32
|
+
* JSON.stringify(value, null, JSON_INDENT) 逐字节一致;>8000 输出与
|
|
33
|
+
* 全量序列化后 .slice(0, budget) + "\n... (truncated)" 逐字节一致——
|
|
34
|
+
* 等价测试锚定,见 __tests__/helpers-bounded-serialize.test.ts)。
|
|
35
|
+
*
|
|
36
|
+
* 现状成本:全量 stringify 产生数 MB 中间串再丢弃 99%;本函数在输出字符流
|
|
37
|
+
* 越过 budget 后停止一切生成。保真策略:原语(string/number/boolean/null/bigint)
|
|
38
|
+
* 逐值复用 JSON.stringify(转义/Unicode/数字格式原生一致,零重实现),仅结构
|
|
39
|
+
* 拼装(2 空格缩进/逗号/括号)自实现。
|
|
40
|
+
*
|
|
41
|
+
* 特殊值语义(TC5 a-d,与原生 JSON.stringify 全值域对齐):
|
|
42
|
+
* (a) 含 toJSON 的对象 → 该子树整体走一次 JSON.stringify(subtree)(原生会先调
|
|
43
|
+
* toJSON,如 Date 输出带引号序列化串;bounded 拼装展开会与原生不同)。注意
|
|
44
|
+
* 边界:toJSON 返回对象时原生 pretty 会按缩进展开,本实现按原语串接其
|
|
45
|
+
* compact 形态——设计 TC5 锚定面为 Date 型(返回字符串)toJSON。
|
|
46
|
+
* (b) 任何 stringify 抛出(BigInt → TypeError)→ 整体回退 String(value)(对齐
|
|
47
|
+
* 旧实现整体 try/catch 的整串回退;禁止逐节点回退——输出形态与整串回退不同)。
|
|
48
|
+
* (c) 对象属性值为 undefined/function/symbol → 拼装时跳过(原生省略)。
|
|
49
|
+
* (d) 数组元素为 undefined/function/symbol → 序列化为 "null"(原生行为)。
|
|
50
|
+
*
|
|
51
|
+
* 截断边界(输出字符流层级,与对完整串 slice 逐字节等价):逐段 append 时本段
|
|
52
|
+
* 会使总长越过 budget → 只 append 本段前 (budget - 已累积) 字符(恰好 budget,
|
|
53
|
+
* 可切在转义序列/括号中间,不补任何结构闭合);越过(exceeded)才追加
|
|
54
|
+
* "\n... (truncated)" 标记——恰好 ===budget 不加(> 判定)。
|
|
55
|
+
*
|
|
56
|
+
* 祖先 Set 循环引用守卫:命中 → 整体回退 String(value)(同 (b) 整串回退语义)。
|
|
57
|
+
*/
|
|
58
|
+
function boundedPrettySerialize(value: unknown, budget: number): string {
|
|
59
|
+
const ancestors = new Set<object>();
|
|
60
|
+
let out = "";
|
|
61
|
+
let exceeded = false;
|
|
62
|
+
|
|
63
|
+
/** 逐段追加:越过 budget 时截到恰好 budget 并置 exceeded(后续生成全部剪枝)。 */
|
|
64
|
+
function append(s: string): void {
|
|
65
|
+
if (exceeded) return;
|
|
66
|
+
if (out.length + s.length <= budget) {
|
|
67
|
+
out += s;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
out += s.slice(0, budget - out.length);
|
|
71
|
+
exceeded = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function serialize(v: unknown, depth: number): void {
|
|
75
|
+
if (exceeded) return;
|
|
76
|
+
const t = typeof v;
|
|
77
|
+
// 原语逐值复用原生序列化;bigint 的 JSON.stringify 抛 TypeError → 顶层 catch
|
|
78
|
+
// 整体回退 (b);NaN/Infinity → "null"(原生行为)
|
|
79
|
+
if (v === null || t === "string" || t === "number" || t === "boolean" || t === "bigint") {
|
|
80
|
+
append(JSON.stringify(v));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (t === "object" && v !== null) {
|
|
84
|
+
const obj = v as object;
|
|
85
|
+
if (ancestors.has(obj)) {
|
|
86
|
+
// 循环引用 → 整体回退 String(value)(旧实现同款整串回退)
|
|
87
|
+
throw new TypeError("circular reference");
|
|
88
|
+
}
|
|
89
|
+
// (a) toJSON 子树整体 stringify(Reflect.get 取属性,避免不安全断言)
|
|
90
|
+
if (typeof Reflect.get(obj, "toJSON") === "function") {
|
|
91
|
+
append(JSON.stringify(obj));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
ancestors.add(obj);
|
|
95
|
+
const childIndent = " ".repeat(JSON_INDENT * (depth + 1));
|
|
96
|
+
const closeIndent = " ".repeat(JSON_INDENT * depth);
|
|
97
|
+
if (Array.isArray(obj)) {
|
|
98
|
+
const arr = obj as unknown[];
|
|
99
|
+
if (arr.length === 0) {
|
|
100
|
+
append("[]");
|
|
101
|
+
} else {
|
|
102
|
+
append("[\n");
|
|
103
|
+
for (let i = 0; i < arr.length && !exceeded; i++) {
|
|
104
|
+
if (i > 0) append(",\n");
|
|
105
|
+
append(childIndent);
|
|
106
|
+
const el = arr[i];
|
|
107
|
+
// (d) 数组元素 undefined/function/symbol → "null"
|
|
108
|
+
if (el === undefined || typeof el === "function" || typeof el === "symbol") {
|
|
109
|
+
append("null");
|
|
110
|
+
} else {
|
|
111
|
+
serialize(el, depth + 1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
append("\n" + closeIndent + "]");
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
// (c) 对象属性 undefined/function/symbol 跳过(Object.entries 同原生:
|
|
118
|
+
// 只取 own enumerable,symbol 键天然不出现;getter 求值语义与原生一致)
|
|
119
|
+
const entries = Object.entries(obj).filter(
|
|
120
|
+
([, val]) => val !== undefined && typeof val !== "function" && typeof val !== "symbol",
|
|
121
|
+
);
|
|
122
|
+
if (entries.length === 0) {
|
|
123
|
+
append("{}");
|
|
124
|
+
} else {
|
|
125
|
+
append("{\n");
|
|
126
|
+
for (let i = 0; i < entries.length && !exceeded; i++) {
|
|
127
|
+
if (i > 0) append(",\n");
|
|
128
|
+
append(childIndent + JSON.stringify(entries[i][0]) + ": ");
|
|
129
|
+
serialize(entries[i][1], depth + 1);
|
|
130
|
+
}
|
|
131
|
+
append("\n" + closeIndent + "}");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
ancestors.delete(obj);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// 顶层 undefined/function/symbol(调用方守卫后不可达;防御兜底对齐原生 "null")
|
|
138
|
+
append(JSON.stringify(v) ?? "null");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
serialize(value, 0);
|
|
143
|
+
return exceeded ? out + "\n... (truncated)" : out;
|
|
144
|
+
} catch {
|
|
145
|
+
// (b) 整体回退:String(value)(超 budget 仍截断 + 标记,与旧实现截断路径一致)
|
|
146
|
+
const fallback = String(value);
|
|
147
|
+
return fallback.length > budget
|
|
148
|
+
? fallback.slice(0, budget) + "\n... (truncated)"
|
|
149
|
+
: fallback;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* notifiedRunIds 去重窗口大小。
|
|
155
|
+
*
|
|
156
|
+
* 最近该数量的已通知 runId 可去重,更旧挤出后不再去重——runId 全局唯一
|
|
157
|
+
* (wf-<Date.now>-<rand>),旧 id 重现概率为零,语义无损。
|
|
158
|
+
*/
|
|
159
|
+
export const MAX_NOTIFIED_RUN_IDS = 1000;
|
|
160
|
+
|
|
28
161
|
/** runId 前 8 字符用于显示(与 buildWorkflowGui 的 label 格式一致)。 */
|
|
29
162
|
const RUN_ID_DISPLAY_LENGTH = 8;
|
|
30
163
|
|
|
@@ -88,16 +221,10 @@ export function notifyDone(
|
|
|
88
221
|
|
|
89
222
|
if (run.state.scriptResult !== undefined && run.state.scriptResult !== null) {
|
|
90
223
|
// M10: scriptResult 来自 worker 脚本返回值(用户可控),可能含循环引用导致 JSON.stringify 抛 TypeError
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
serialized = String(run.state.scriptResult);
|
|
96
|
-
}
|
|
97
|
-
const truncated =
|
|
98
|
-
serialized.length > MAX_RESULT_LENGTH
|
|
99
|
-
? serialized.slice(0, MAX_RESULT_LENGTH) + "\n... (truncated)"
|
|
100
|
-
: serialized;
|
|
224
|
+
// IF13(#19):bounded 序列化(只生成会被保留的前缀;BigInt/循环引用整体回退
|
|
225
|
+
// String(x) 与旧实现整串 catch 同款)——≤8000 与旧全量 pretty 逐字节一致,
|
|
226
|
+
// >8000 与 .slice(0,8000)+标记 逐字节一致(等价测试锚定)
|
|
227
|
+
const truncated = boundedPrettySerialize(run.state.scriptResult, MAX_RESULT_LENGTH);
|
|
101
228
|
parts.push("");
|
|
102
229
|
parts.push("--- Script Result ---");
|
|
103
230
|
parts.push(truncated);
|
|
@@ -151,3 +278,36 @@ export function notifyDone(
|
|
|
151
278
|
{ triggerTurn: true, deliverAs: "steer" },
|
|
152
279
|
);
|
|
153
280
|
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 把 runId 纳入 notifiedRunIds 去重窗口,超 cap 时删最旧(契约 W3C2)。
|
|
284
|
+
*
|
|
285
|
+
* 职责拆分:**去重判定**留在 notifyDone 的 has 读(本体零改动),
|
|
286
|
+
* 本函数只持**有界化**职责——返回 void,不引入双重去重判定语义。
|
|
287
|
+
*
|
|
288
|
+
* 语义:
|
|
289
|
+
* - 幂等 add:Set.add 对已存在元素不改变其迭代位置(重复 track 同一 id,
|
|
290
|
+
* 其「最旧」地位不变)。
|
|
291
|
+
* - FIFO 有界:Set 迭代序=插入序,超 cap 时删迭代器首元素=最旧。
|
|
292
|
+
* 被挤出窗口的旧 id 再经 notifyDone 会重新发送(runId 全局唯一,旧 id
|
|
293
|
+
* 重现概率为零,该边界由 W3TC12 单测钉死)。
|
|
294
|
+
*
|
|
295
|
+
* 调用点:index.ts onRunDone 回调内、notifyDone 之后(notifyDone 内部已 add,
|
|
296
|
+
* 此处 track 的 add 是幂等二次添加)。
|
|
297
|
+
*
|
|
298
|
+
* @param notifiedRunIds 去重 Set(调用方持有,scope 到 factory 实例)
|
|
299
|
+
* @param runId run 标识
|
|
300
|
+
* @param cap 窗口大小(默认 MAX_NOTIFIED_RUN_IDS)
|
|
301
|
+
*/
|
|
302
|
+
export function trackNotifiedRunId(
|
|
303
|
+
notifiedRunIds: Set<string>,
|
|
304
|
+
runId: string,
|
|
305
|
+
cap: number = MAX_NOTIFIED_RUN_IDS,
|
|
306
|
+
): void {
|
|
307
|
+
notifiedRunIds.add(runId);
|
|
308
|
+
while (notifiedRunIds.size > cap) {
|
|
309
|
+
const oldest = notifiedRunIds.values().next().value;
|
|
310
|
+
if (oldest === undefined) break;
|
|
311
|
+
notifiedRunIds.delete(oldest);
|
|
312
|
+
}
|
|
313
|
+
}
|