@zhushanwen/pi-subagent-workflow 8.14.7 → 9.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 +6 -5
- package/package.json +11 -7
- package/relay/relay.mjs +45 -2
- package/skills/workflow-script-format/SKILL.md +2 -56
- package/src/host/__tests__/inflight-reporter.test.ts +26 -1
- package/src/host/__tests__/pi-host.test.ts +418 -4
- package/src/host/inflight-reporter.ts +13 -10
- package/src/host/pi-host.ts +205 -17
- package/src/index.ts +21 -72
- package/src/injectors/__tests__/engine-awareness.test.ts +236 -33
- package/src/injectors/__tests__/helpers/injector-test-mocks.ts +15 -7
- package/src/injectors/__tests__/workflow-list-injector.test.ts +447 -40
- package/src/injectors/resource-list-injector.ts +103 -32
- package/src/injectors/subagent-list-injector.ts +4 -28
- package/src/injectors/workflow-list-injector.ts +8 -4
- package/src/interface/__tests__/capture-tool.ts +56 -0
- package/src/interface/__tests__/commands-resume.test.ts +103 -0
- package/src/interface/__tests__/tool-prompt-contract.test.ts +361 -0
- package/src/interface/__tests__/tool-render.test.ts +105 -2
- package/src/interface/__tests__/tool-subagents.test.ts +24 -26
- package/src/interface/__tests__/tool-workflow-resume.test.ts +227 -0
- package/src/interface/__tests__/tool-workflow-script.test.ts +557 -0
- package/src/interface/__tests__/tool-workflow.test.ts +884 -0
- package/src/interface/bg-notify-render.ts +18 -9
- package/src/interface/command-actions.ts +22 -17
- package/src/interface/commands.ts +58 -12
- package/src/interface/format.ts +50 -16
- package/src/interface/id-preview.ts +4 -4
- package/src/interface/subagent-actions.ts +18 -50
- package/src/interface/subagent-tool.ts +26 -18
- package/src/interface/subagents.ts +5 -11
- package/src/interface/tool-render.ts +14 -30
- package/src/interface/tool-shared.ts +55 -12
- package/src/interface/tool-subagents.ts +21 -11
- package/src/interface/tool-workflow-script.ts +52 -72
- package/src/interface/tool-workflow.ts +345 -139
- package/src/interface/views/WorkflowsView.ts +10 -7
- package/src/interface/views/__tests__/{WorkflowsView-signature.test.ts → WorkflowsView.test.ts} +155 -11
- package/src/interface/views/detail-content.ts +3 -5
- package/src/jsonl-run-store.ts +656 -544
- package/src/model-events.ts +35 -0
- package/src/session-lifecycle.ts +376 -85
- package/src/subagents-events.ts +56 -0
- package/src/workflow-events.ts +285 -200
- package/src/workflow-notify.ts +456 -0
- package/src/injectors/__tests__/engine-section-stability.test.ts +0 -353
- package/src/injectors/__tests__/model-list-injector.test.ts +0 -136
- package/src/injectors/__tests__/subagent-list-injector.test.ts +0 -376
- package/src/interface/__tests__/detectors.test.ts +0 -161
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +0 -183
- package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +0 -219
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +0 -251
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +0 -215
- package/src/interface/__tests__/workflow-state-file-exposure.test.ts +0 -38
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +0 -129
- package/src/interface/helpers.ts +0 -272
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +0 -73
- package/src/interface/views/__tests__/record-progress.test.ts +0 -235
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Extension — workflow 域通知生产
|
|
3
|
+
*
|
|
4
|
+
* notifyDone(pi, runId, run, notified) — run 完成时发 completion notification
|
|
5
|
+
* ([u9 账本化] 经 core NotifyLedger 四步生命周期,C-ext-19;未 bind 降级直发)。
|
|
6
|
+
*
|
|
7
|
+
* [D7 载荷扩展] 终局必达通知的不变量面:成功含结果摘要与产物指针、失败含
|
|
8
|
+
* errorCode 与证据指针、取消亦通知——载荷 outcome/errorCode/resultSummary/
|
|
9
|
+
* artifactsDir/eventsJournalPath(details 层,content 追加指针段)。
|
|
10
|
+
*
|
|
11
|
+
* 层归属:workflow 域(与 workflow-events.ts 同层;唯一生产消费方 =
|
|
12
|
+
* workflow-events.ts。原名 interface/helpers.ts,2026-09-24 名实归位——内容自始
|
|
13
|
+
* 是通知生产而非 interface 注册面杂项)。
|
|
14
|
+
*
|
|
15
|
+
* 事件注册面:setupNotifyLedgerCompactionGuard(pi compaction 事件的 ledger
|
|
16
|
+
* 补写守卫——notify 账本的家内事务,随跨域 handler 迁出从 workflow-events.ts
|
|
17
|
+
* 装配 seam 原样搬入;注册时点仍由 setupWorkflowDomain 在原位调用)。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
ExtensionAPI,
|
|
24
|
+
ExtensionContext,
|
|
25
|
+
SessionCompactEvent,
|
|
26
|
+
} from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import { guardStaleCtx, toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
28
|
+
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
29
|
+
|
|
30
|
+
// bounded JSON pretty 序列化(IF13/#19,TC5/ES5)已下沉 core shared
|
|
31
|
+
// (u-core-atomic 逐字平移,输出与原本地实现字节一致;本地实现已删)。
|
|
32
|
+
// getBoundNotifyLedger:core 通知账本消费入口(bindNotifyLedgerHost 在
|
|
33
|
+
// session-lifecycle.ts session_start 装配;未 bind 时降级直发,见 notifyDone 注释)。
|
|
34
|
+
import {
|
|
35
|
+
boundedPrettySerialize,
|
|
36
|
+
getBoundNotifyLedger,
|
|
37
|
+
isTerminalDoneReason,
|
|
38
|
+
RUN_EVENT_JOURNAL_SUFFIX,
|
|
39
|
+
type RunOutcome,
|
|
40
|
+
} from "@zhushanwen/subagent-core";
|
|
41
|
+
import type { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
42
|
+
import { runSettledOutcomeToDoneReason, type RunSettlementRecord } from "./jsonl-run-store.ts";
|
|
43
|
+
|
|
44
|
+
// 模块级 logger(与 session-lifecycle.ts / index.ts 同 component 名)
|
|
45
|
+
const logger = getLogger("subagents");
|
|
46
|
+
|
|
47
|
+
import { WORKFLOW_RESULT_CUSTOM_TYPE } from "@zhushanwen/extension-protocol";
|
|
48
|
+
|
|
49
|
+
// ── 常量 ─────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const MAX_RESULT_LENGTH = 8000;
|
|
52
|
+
|
|
53
|
+
/** [D7] details.resultSummary 的截断上限(成功时的结果短摘要;全文摘要走 content
|
|
54
|
+
* 的 Script Result 段 bounded 8000,载荷字段是消费方可编程读取的短形态)。 */
|
|
55
|
+
const MAX_RESULT_SUMMARY_LENGTH = 500;
|
|
56
|
+
|
|
57
|
+
/** 写账失败 error 留痕的 content 摘要截断长度(诊断定位用,非消费契约)。 */
|
|
58
|
+
const RECORD_FAIL_CONTENT_PREVIEW_LENGTH = 200;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* [D7] 事件 journal 文件名(run store 旁,artifactsDir 内):`<runId>.events.jsonl`
|
|
62
|
+
* (runId 即 generateRunId 的 wf- 前缀产物,渲染名 = 设计 D5 的 wf-<id>.events.jsonl;
|
|
63
|
+
* 后缀经 core barrel 单源 RUN_EVENT_JOURNAL_SUFFIX,core 命名变更时编译期同步)。
|
|
64
|
+
*/
|
|
65
|
+
function runEventsJournalPath(artifactsDir: string, runId: string): string {
|
|
66
|
+
return join(artifactsDir, `${runId}${RUN_EVENT_JOURNAL_SUFFIX}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* notifyDone 账本幂等键前缀(u9 账本化,对齐 C-ext-19)。键形态
|
|
71
|
+
* `wf-done:<runId>`——一个 run 的收口通知只投递一次(写账 → courier 边沿投递 →
|
|
72
|
+
* 回执销账 → 断连/重启经账本重放,幂等键去重不双投递)。前缀惯例承接 collect
|
|
73
|
+
* 时代 `sync-batch:` 的「通道语义前缀 + 天然唯一标识」形态;导出供测试构造
|
|
74
|
+
* 回执 entry 与账本断言复用同键。
|
|
75
|
+
*/
|
|
76
|
+
export const WORKFLOW_DONE_NOTIFY_ID_PREFIX = "wf-done:";
|
|
77
|
+
|
|
78
|
+
// workflow 收口通知的送达 customType 经 extension-protocol 单源
|
|
79
|
+
// (WORKFLOW_RESULT_CUSTOM_TYPE,与 shared/runtime/core 消费侧同源):runtime
|
|
80
|
+
// event-interpreter 按该类型识别 run 完成并驱动 W18 workflow-record 失效信号
|
|
81
|
+
// (session.workflows 增量广播),taiji 完成通知 display 覆写 SSOT
|
|
82
|
+
// (COMPLETE_NOTIFY_CUSTOM_TYPES)亦按它收录——不可复用 subagent-bg-notify 通道。
|
|
83
|
+
// 等值/单源锁 = src/__tests__/contract.notify-custom-types.test.ts。
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* notifiedRunIds 去重窗口大小。
|
|
87
|
+
*
|
|
88
|
+
* 最近该数量的已通知 runId 可去重,更旧挤出后不再去重——runId 全局唯一
|
|
89
|
+
* (wf-<Date.now>-<rand>),旧 id 重现概率为零,语义无损。
|
|
90
|
+
*/
|
|
91
|
+
export const MAX_NOTIFIED_RUN_IDS = 1000;
|
|
92
|
+
|
|
93
|
+
// [D7] run 终局 outcome + DoneReason 派生:经 jsonl-run-store 的联合判别单点
|
|
94
|
+
//([W2/V1 D1 第 7 行] 载荷源换终局记录——outcome/errorCode 帧直取、DoneReason
|
|
95
|
+
// 联合派生;原 extractFailureErrorCode 重提取链废弃——它与 core 帧生产链
|
|
96
|
+
// finalRunErrorCodeOf 值域本就不同,换源后通知码与 journal 码同源同值)。
|
|
97
|
+
// 漂移信号 = journal run-settled 帧出现词表外值时 core 自有用例先红。
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* notifyDone 的 details 结构(通过 pi.sendMessage 透传给前端)。
|
|
101
|
+
*
|
|
102
|
+
* 抽取为显式接口替代裸 Record<string, unknown>。
|
|
103
|
+
* 模块私有(原 export 已删):无跨文件消费者,测试经 notifyDone 公共入口
|
|
104
|
+
* 观察并内联类型。
|
|
105
|
+
*/
|
|
106
|
+
interface WorkflowNotifyDetails {
|
|
107
|
+
runId: string;
|
|
108
|
+
name: string;
|
|
109
|
+
status: string;
|
|
110
|
+
reason: string | undefined;
|
|
111
|
+
traceLength: number;
|
|
112
|
+
/**
|
|
113
|
+
* [u9] 账本幂等键(`wf-done:<runId>`)——回执匹配键(ledger checkReceipts 扫
|
|
114
|
+
* 送达 custom_message entry 的 details.notifyId 判定销账)。携带在 details 不进
|
|
115
|
+
* 文案(G4 字节锁定不受影响)。
|
|
116
|
+
*/
|
|
117
|
+
notifyId: string;
|
|
118
|
+
/**
|
|
119
|
+
* [D7] 终局 outcome 三态(completed/failed/cancelled)——脚本层失败
|
|
120
|
+
* (review-failure = outcome:completed + 脚本返回失败结论)与 run 自身怎么死的
|
|
121
|
+
* (outcome:failed)在通知面可区分(D5 终态双维度语义)。
|
|
122
|
+
*/
|
|
123
|
+
outcome: RunOutcome;
|
|
124
|
+
/**
|
|
125
|
+
* [D7] 失败时的结构化码(最后失败 ask 的 failureKind;无失败帧的 run 级死法
|
|
126
|
+
* 缺省——见 extractFailureErrorCode)。completed/cancelled 恒缺省。
|
|
127
|
+
*/
|
|
128
|
+
errorCode?: string;
|
|
129
|
+
/**
|
|
130
|
+
* [D7] 成功时的结果短摘要(scriptResult bounded 截断;全文走 content 的
|
|
131
|
+
* Script Result 段)。
|
|
132
|
+
*/
|
|
133
|
+
resultSummary?: string;
|
|
134
|
+
/**
|
|
135
|
+
* [D7] 产物目录指针:run 持久化产物所在目录绝对路径
|
|
136
|
+
* (`<sessionDir>/workflow-state`——journal/manifest/.state 同目录;调用方经
|
|
137
|
+
* onRunDone 桥注入,旧调用缺省 undefined)。
|
|
138
|
+
*/
|
|
139
|
+
artifactsDir?: string;
|
|
140
|
+
/**
|
|
141
|
+
* [D7] 事件 journal 指针(`<artifactsDir>/wf-<runId>.events.jsonl`)——终局
|
|
142
|
+
* 证据的入口载荷(D5-3 诊断引用落账的消费面)。
|
|
143
|
+
*/
|
|
144
|
+
eventsJournalPath?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* workflow 到达 done 终态时发送完成通知。
|
|
149
|
+
*
|
|
150
|
+
* [u9 账本化] 结果语义通知接 core NotifyLedger 四步生命周期(C-ext-19:持久账本 +
|
|
151
|
+
* notifyId 幂等,替代裸 steer fire-once——relay 瞬断不再丢通知):
|
|
152
|
+
* ① 写账:ledger.record(`wf-done:<runId>`) 落盘(幂等键去重,同 run 跨重启
|
|
153
|
+
* 不双投递)→ ② courier 边沿投递(settled 边沿 + isIdle 二次复查 + 120s
|
|
154
|
+
* 看门狗,送达 customType 保持 "workflow-result")→ ③ 回执销账 → ④ 断连/
|
|
155
|
+
* 重启经 recoverFromSession at-least-once 重放(机制见 notify-ledger.ts)。
|
|
156
|
+
* 账本未装配时(旧宿主 / 无 ledger 测试)降级为 fire-once 直发(at-most-once,
|
|
157
|
+
* 对齐 notifier 内核降级路径;triggerTurn 单通道,deliverAs 已删——u9 偏差裁决,
|
|
158
|
+
* D7 账本化配套)。
|
|
159
|
+
*
|
|
160
|
+
* **内存去重**:notifiedRunIds Set 由调用方(factory/extension instance)持有,
|
|
161
|
+
* 同 runId 的重复收口回调拦截(跨 session_shutdown 等边界防重复);标记在写账成功
|
|
162
|
+
* (或账本幂等拒绝)后落下。**已知丢失面(如实登记)**:record 抛(reload 窗口
|
|
163
|
+
* appendEntry assertActive 等)= 账本 entry 未写,而 notifyDone 的唯一调用点
|
|
164
|
+
* onRunDone 每终态恰好一次、recoverFromSession 无源可重放——该终局通知**永久丢失**,
|
|
165
|
+
* 仅 error 日志留痕(含 notifyId/content 摘要,供事后手工补偿);根治需 pending-retry
|
|
166
|
+
* 结构(设计变更,本轮不建,见加固审查报告)。持久层幂等由账本 notifyId 承接
|
|
167
|
+
* (内存窗口挤出 / 重启后的重复仍被 record 拒绝)。
|
|
168
|
+
*
|
|
169
|
+
* @param pi ExtensionAPI(仅降级路径直发用)
|
|
170
|
+
* @param runId run 标识
|
|
171
|
+
* @param run WorkflowRun 聚合根(读 spec.scriptName + trace + scriptResult;status/reason/outcome 载荷源 = 终局记录 settlement——[W2/V1 D1 第 7 行] 换源,不读两态机字段)
|
|
172
|
+
* @param notifiedRunIds 去重 Set(调用方持有,scope 到 factory 实例)
|
|
173
|
+
* @param artifactsDir [D7] 产物目录指针(`<sessionDir>/workflow-state`,onRunDone
|
|
174
|
+
* 桥注入;缺省 undefined = 旧调用兼容,载荷与文案指针段双双省略)
|
|
175
|
+
*/
|
|
176
|
+
/**
|
|
177
|
+
* notifyDone 构建面(content + details 一次成型)。
|
|
178
|
+
*
|
|
179
|
+
* [W2/V1 D1 第 7 行] 载荷源换源:status 串 / reason / outcome / errorCode 全部
|
|
180
|
+
* 取终局记录(settlement——与 run-settled 帧同源),不再读两态机字段(I2 失效
|
|
181
|
+
* 窗口:活体写点删除后 state.reason 恒 undefined,原 I2 兜底载荷恒退 completed
|
|
182
|
+
* 假成功——显式换源后删除)。trace/scriptResult 仍读聚合(非两态机字段)。
|
|
183
|
+
*
|
|
184
|
+
* content 段顺序:标题行 →(终止性原因非正常完成时)防偷懒收尾指令 →
|
|
185
|
+
* (有 scriptResult 时)Script Result 段 → Agent Trace 段 →(artifactsDir 有时)
|
|
186
|
+
* Artifacts 指针段。
|
|
187
|
+
*/
|
|
188
|
+
function buildDoneNotifyContent(
|
|
189
|
+
run: WorkflowRun,
|
|
190
|
+
runId: string,
|
|
191
|
+
artifactsDir: string | undefined,
|
|
192
|
+
settlement: RunSettlementRecord,
|
|
193
|
+
): { content: string; eventsJournalPath: string | undefined } {
|
|
194
|
+
const traceNodes = run.state.trace.toArray();
|
|
195
|
+
const name = run.spec.scriptName;
|
|
196
|
+
const reason = runSettledOutcomeToDoneReason(settlement.outcome, settlement.errorCode);
|
|
197
|
+
const status = `done (${reason})`;
|
|
198
|
+
|
|
199
|
+
// 构建消息内容
|
|
200
|
+
const parts: string[] = [];
|
|
201
|
+
parts.push(`Workflow '${name}' done: ${status}`);
|
|
202
|
+
|
|
203
|
+
// 终止性原因(非正常完成)追加防偷懒收尾指令——budget/time 耗尽或 abort 不是任务完成,
|
|
204
|
+
// 模型可能把 "done" 当成功汇报(F3 偷懒完成)。收尾三步骤与 turn-limiter WRAP_UP_MESSAGE 对齐。
|
|
205
|
+
// 判定经 core isTerminalDoneReason 单源(穷举 switch,DoneReason 新增成员 tsc 强制归类),
|
|
206
|
+
// 输入 = 派生 DoneReason(载荷源换源后与原 state.reason 判据逐值等价——细分恢复使
|
|
207
|
+
// 预算/超时终局不误命中/漏命中)。
|
|
208
|
+
if (isTerminalDoneReason(reason)) {
|
|
209
|
+
parts.push("");
|
|
210
|
+
parts.push(
|
|
211
|
+
"This is NOT task completion. Summarize what was DONE and VERIFIED, list what remains " +
|
|
212
|
+
"NOT DONE, and give the user the single most important next step.",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (run.state.scriptResult !== undefined && run.state.scriptResult !== null) {
|
|
217
|
+
// M10: scriptResult 来自 worker 脚本返回值(用户可控),可能含循环引用导致 JSON.stringify 抛 TypeError
|
|
218
|
+
// IF13(#19):bounded 序列化(只生成会被保留的前缀;BigInt/循环引用整体回退
|
|
219
|
+
// String(x) 与旧实现整串 catch 同款)——≤8000 与旧全量 pretty 逐字节一致,
|
|
220
|
+
// >8000 与 .slice(0,8000)+标记 逐字节一致(等价测试锚定)
|
|
221
|
+
const truncated = boundedPrettySerialize(run.state.scriptResult, MAX_RESULT_LENGTH);
|
|
222
|
+
parts.push("");
|
|
223
|
+
parts.push("--- Script Result ---");
|
|
224
|
+
parts.push(truncated);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
parts.push("");
|
|
228
|
+
parts.push("--- Agent Trace ---");
|
|
229
|
+
for (const node of traceNodes) {
|
|
230
|
+
parts.push(`[${node.stepIndex}] ${node.agent}: ${node.status}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// [D7] 产物指针段(主 agent 可操作的下一步入口:结果/证据在哪)。artifactsDir
|
|
234
|
+
// 缺省(旧调用兼容)整段省略,content 字节与既有形态一致。
|
|
235
|
+
const eventsJournalPath =
|
|
236
|
+
artifactsDir !== undefined ? runEventsJournalPath(artifactsDir, runId) : undefined;
|
|
237
|
+
if (artifactsDir !== undefined && eventsJournalPath !== undefined) {
|
|
238
|
+
parts.push("");
|
|
239
|
+
parts.push("--- Artifacts ---");
|
|
240
|
+
parts.push(`Artifacts dir: ${artifactsDir}`);
|
|
241
|
+
parts.push(`Events journal: ${eventsJournalPath}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { content: parts.join("\n"), eventsJournalPath };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* notifyDone 的 details 构建面(baseDetails 成型)。
|
|
249
|
+
*
|
|
250
|
+
* 送达通道保持 "workflow-result"(runtime W18 失效信号 + taiji display 覆写 SSOT
|
|
251
|
+
* 按该类型识别;迁移不改变消息类型与文案字节,只改变投递可靠性机制)。
|
|
252
|
+
*/
|
|
253
|
+
function buildDoneNotifyDetails(
|
|
254
|
+
run: WorkflowRun,
|
|
255
|
+
runId: string,
|
|
256
|
+
artifactsDir: string | undefined,
|
|
257
|
+
eventsJournalPath: string | undefined,
|
|
258
|
+
settlement: RunSettlementRecord,
|
|
259
|
+
): WorkflowNotifyDetails {
|
|
260
|
+
const traceNodes = run.state.trace.toArray();
|
|
261
|
+
const name = run.spec.scriptName;
|
|
262
|
+
const reason = runSettledOutcomeToDoneReason(settlement.outcome, settlement.errorCode);
|
|
263
|
+
const baseDetails: WorkflowNotifyDetails = {
|
|
264
|
+
runId,
|
|
265
|
+
name,
|
|
266
|
+
status: "done",
|
|
267
|
+
reason,
|
|
268
|
+
traceLength: traceNodes.length,
|
|
269
|
+
notifyId: `${WORKFLOW_DONE_NOTIFY_ID_PREFIX}${runId}`,
|
|
270
|
+
// [W2/V1 D1 第 7 行] 终局必达载荷:outcome/errorCode 帧直取(与 run-settled
|
|
271
|
+
// 帧同源——漂移判据 = core run-events 注释的「通知 outcome ≡ 帧 outcome」;
|
|
272
|
+
// 换源后 engine 协议码族与 'unknown' 首次进入通知 errorCode 值域,消费方容忍
|
|
273
|
+
// 已核对:bg-notify-render 不消费 errorCode,真实消费方按扩张值域透传)。
|
|
274
|
+
outcome: settlement.outcome,
|
|
275
|
+
};
|
|
276
|
+
if (settlement.errorCode !== undefined) {
|
|
277
|
+
baseDetails.errorCode = settlement.errorCode;
|
|
278
|
+
}
|
|
279
|
+
if (settlement.outcome === "done" && run.state.scriptResult !== undefined && run.state.scriptResult !== null) {
|
|
280
|
+
baseDetails.resultSummary = boundedPrettySerialize(run.state.scriptResult, MAX_RESULT_SUMMARY_LENGTH);
|
|
281
|
+
}
|
|
282
|
+
if (artifactsDir !== undefined && eventsJournalPath !== undefined) {
|
|
283
|
+
baseDetails.artifactsDir = artifactsDir;
|
|
284
|
+
baseDetails.eventsJournalPath = eventsJournalPath;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return baseDetails;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* notifyDone 投递面:账本在 → 四步生命周期(写账 / courier 投递 / 销账 / 重放);
|
|
292
|
+
* 账本未装配 → fire-once 直发降级(stale ctx 静默)。见 notifyDone 主注释。
|
|
293
|
+
*/
|
|
294
|
+
function deliverDoneNotify(
|
|
295
|
+
pi: ExtensionAPI,
|
|
296
|
+
runId: string,
|
|
297
|
+
notifiedRunIds: Set<string>,
|
|
298
|
+
content: string,
|
|
299
|
+
details: WorkflowNotifyDetails,
|
|
300
|
+
): void {
|
|
301
|
+
const ledger = getBoundNotifyLedger();
|
|
302
|
+
if (ledger) {
|
|
303
|
+
// [u9 账本化] ledger 在 → ①写账(record false = 同幂等键已在账/已销账——内存
|
|
304
|
+
// 去重窗口挤出或重启恢复后的重复收口,跳过投递)→ ②attemptDeliver(courier
|
|
305
|
+
// 边沿 + isIdle 二次复查,③销账 ④重放在 ledger 内;送达通道经
|
|
306
|
+
// deliveryCustomType 保持 "workflow-result")。
|
|
307
|
+
// 内存去重标记在写账**成功后**才落下。**已知丢失面(如实登记)**:record 抛
|
|
308
|
+
// (reload 窗口 appendEntry 命中 assertActive 等)= 账本 entry 未写——onRunDone
|
|
309
|
+
// 每终态恰好一次(无重复收口可重试)、账本无 entry 则 recoverFromSession 无源
|
|
310
|
+
// 可重放,该终局通知**永久丢失**;不提前标记只是给进程内的假想重复收口留重试
|
|
311
|
+
// 通道(防御形态,不改变丢失事实)。留痕与根治面见下方 catch 注释。
|
|
312
|
+
// stale ctx 防御由装配层 sendDelivery 内置(session-lifecycle.ts
|
|
313
|
+
// bindLedgerHostAndRecover),此处无需重复包裹。
|
|
314
|
+
let recorded: boolean;
|
|
315
|
+
try {
|
|
316
|
+
recorded = ledger.record(details.notifyId, content, details, {
|
|
317
|
+
deliveryCustomType: WORKFLOW_RESULT_CUSTOM_TYPE,
|
|
318
|
+
});
|
|
319
|
+
} catch (err) {
|
|
320
|
+
// 写账抛错 = 该终局通知丢失(账面 entry 未写,无重放源)。error 级留痕含
|
|
321
|
+
// notifyId 与 content 摘要,供事后按 run 手工补偿(从 run journal/manifest
|
|
322
|
+
// 读取终态)。根治需 pending-retry 结构(设计变更,本轮不建,见加固审查报告);
|
|
323
|
+
// 原样上抛保持 finalizeRun 围栏的既有接住链路。
|
|
324
|
+
logger.error(
|
|
325
|
+
"workflow completion notice ledger record failed — this terminal notification is LOST (no ledger entry, no replay source); compensate manually from run artifacts",
|
|
326
|
+
{
|
|
327
|
+
runId,
|
|
328
|
+
notifyId: details.notifyId,
|
|
329
|
+
contentPreview: content.slice(0, RECORD_FAIL_CONTENT_PREVIEW_LENGTH),
|
|
330
|
+
reason: toErrorMessage(err),
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
throw err;
|
|
334
|
+
}
|
|
335
|
+
trackNotifiedRunId(notifiedRunIds, runId);
|
|
336
|
+
if (recorded) ledger.attemptDeliver();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 降级:ledger 未装配(旧宿主 / 无账本测试)→ fire-once 直发(at-most-once)。
|
|
341
|
+
// triggerTurn 单通道(deliverAs 已删——u9 偏差裁决,D7 账本化配套:busy 场景的
|
|
342
|
+
// 投递时机治理本就由账本路径承担,降级路径不再依赖 pi 内存队列的 steer 形态);stale ctx 防御
|
|
343
|
+
// (crash-resilience D1 / ext-guards 审计 §7 blockers#1 收口):session 替换窗口
|
|
344
|
+
// 触碰 stale pi 命中 assertActive(PS-30)即无人接 rejection 崩 pi(E1 同机制)。
|
|
345
|
+
// stale 静默降级(完成通知不投递,用户可从 session 历史 / 工具结果看到 workflow
|
|
346
|
+
// 结果,判定见 stale-ctx-audit.md §4),非 stale 错误原样上抛(守卫不吞真实 bug)。
|
|
347
|
+
// 去重标记在实际发送**之后**落下:非 stale 瞬态发送失败(同步抛错上抛)后进程内
|
|
348
|
+
// 仍可重试(去重不阻断);stale 分支由 onStale 补标记保持现语义——stale = 通知对
|
|
349
|
+
// 旧 session 已无意义,不重试。
|
|
350
|
+
guardStaleCtx(
|
|
351
|
+
() => {
|
|
352
|
+
const sent = pi.sendMessage(
|
|
353
|
+
{
|
|
354
|
+
customType: WORKFLOW_RESULT_CUSTOM_TYPE,
|
|
355
|
+
content,
|
|
356
|
+
display: true,
|
|
357
|
+
details,
|
|
358
|
+
},
|
|
359
|
+
{ triggerTurn: true },
|
|
360
|
+
);
|
|
361
|
+
trackNotifiedRunId(notifiedRunIds, runId);
|
|
362
|
+
return sent;
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
label: "subagent-workflow:notifyDone",
|
|
366
|
+
onStale: (error) => {
|
|
367
|
+
trackNotifiedRunId(notifiedRunIds, runId);
|
|
368
|
+
logger.warn("workflow completion notice delivery skipped (stale ctx)", {
|
|
369
|
+
runId,
|
|
370
|
+
error: toErrorMessage(error),
|
|
371
|
+
});
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function notifyDone(
|
|
378
|
+
pi: ExtensionAPI,
|
|
379
|
+
runId: string,
|
|
380
|
+
run: WorkflowRun,
|
|
381
|
+
notifiedRunIds: Set<string>,
|
|
382
|
+
artifactsDir: string | undefined,
|
|
383
|
+
settlement: RunSettlementRecord,
|
|
384
|
+
): void {
|
|
385
|
+
if (notifiedRunIds.has(runId)) return;
|
|
386
|
+
const { content, eventsJournalPath } = buildDoneNotifyContent(run, runId, artifactsDir, settlement);
|
|
387
|
+
const details = buildDoneNotifyDetails(run, runId, artifactsDir, eventsJournalPath, settlement);
|
|
388
|
+
deliverDoneNotify(pi, runId, notifiedRunIds, content, details);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* 把 runId 纳入 notifiedRunIds 去重窗口,超 cap 时删最旧(契约 W3C2)。
|
|
393
|
+
*
|
|
394
|
+
* 职责拆分:**去重判定**留在 notifyDone 的 has 读(本体零改动),
|
|
395
|
+
* 本函数只持**有界化**职责——返回 void,不引入双重去重判定语义。
|
|
396
|
+
*
|
|
397
|
+
* 语义:
|
|
398
|
+
* - 幂等 add:Set.add 对已存在元素不改变其迭代位置(重复 track 同一 id,
|
|
399
|
+
* 其「最旧」地位不变)。
|
|
400
|
+
* - FIFO 有界:Set 迭代序=插入序,超 cap 时删迭代器首元素=最旧。
|
|
401
|
+
* 被挤出窗口的旧 id 再经 notifyDone:内存层放行后由账本 notifyId 幂等兜底
|
|
402
|
+
* ([u9] record 同键拒绝——生产环境账本已 bind 时不会重发;无账本降级环境
|
|
403
|
+
* 才会重新直发,runId 全局唯一,旧 id 重现概率为零,该边界由 W3TC12 单测
|
|
404
|
+
* 在降级形态下钉死)。
|
|
405
|
+
*
|
|
406
|
+
* 调用点:notifyDone 内部(写账成功/false 后 + 降级直发受理后)+ workflow-events
|
|
407
|
+
* onRunDone 回调(notifyDone 之后,幂等二次添加)。notifyDone 抛出(reload 窗口
|
|
408
|
+
* record 抛 / 非 stale 发送失败等)时内外都不标记——进程内的重复收口回调不被去重
|
|
409
|
+
* 阻断(防御形态;账本路径抛错的已知丢失面登记见 notifyDone 注释)。
|
|
410
|
+
*
|
|
411
|
+
* @param notifiedRunIds 去重 Set(调用方持有,scope 到 factory 实例)
|
|
412
|
+
* @param runId run 标识
|
|
413
|
+
* @param cap 窗口大小(默认 MAX_NOTIFIED_RUN_IDS)
|
|
414
|
+
*/
|
|
415
|
+
export function trackNotifiedRunId(
|
|
416
|
+
notifiedRunIds: Set<string>,
|
|
417
|
+
runId: string,
|
|
418
|
+
cap: number = MAX_NOTIFIED_RUN_IDS,
|
|
419
|
+
): void {
|
|
420
|
+
notifiedRunIds.add(runId);
|
|
421
|
+
while (notifiedRunIds.size > cap) {
|
|
422
|
+
const oldest = notifiedRunIds.values().next().value;
|
|
423
|
+
if (oldest === undefined) break;
|
|
424
|
+
notifiedRunIds.delete(oldest);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ── notify 域事件注册(跨域 handler 迁入) ────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* compaction 后 notify ledger 补写守卫(注册 pi 的 session compaction 事件)。
|
|
432
|
+
*
|
|
433
|
+
* [U2 P-B4 降级] compaction 对 custom entry 保留行为实装未验证——检测 ledger/ack
|
|
434
|
+
* entry 被 compaction 清除时按内存态补写(notify-ledger compactionCheck;未清除则
|
|
435
|
+
* no-op)。内存态在 compaction 后仍活着,作为补写源;重启后的权威仍是两列 entry
|
|
436
|
+
* 差集(内存不承担销账职责)。
|
|
437
|
+
*
|
|
438
|
+
* 归 notify 域:守卫对象是 NotifyLedger 的账面完整性(getBoundNotifyLedger 的
|
|
439
|
+
* 家内事务),与 workflow 域事件族装配零数据耦合。由 setupWorkflowDomain 在
|
|
440
|
+
* 原注册位置调用(pi.on 注册顺序逐位不变,
|
|
441
|
+
* workflow-events.test.ts 锁定)。
|
|
442
|
+
*/
|
|
443
|
+
export function setupNotifyLedgerCompactionGuard(pi: ExtensionAPI): void {
|
|
444
|
+
pi.on("session_compact", (_event: SessionCompactEvent, _ctx: ExtensionContext) => {
|
|
445
|
+
try {
|
|
446
|
+
const rewritten = getBoundNotifyLedger()?.compactionCheck() ?? 0;
|
|
447
|
+
if (rewritten > 0) {
|
|
448
|
+
logger.warn(`[subagents] notify ledger entries lost to compaction; rewrote ${rewritten} from memory`);
|
|
449
|
+
}
|
|
450
|
+
} catch (err) {
|
|
451
|
+
logger.warn("[subagents] notify ledger compactionCheck failed", {
|
|
452
|
+
reason: toErrorMessage(err),
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|