@sema-agent/server 7.4.0 → 7.5.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/USAGE.md +43 -0
- package/dist/approval-card.d.ts +15 -3
- package/dist/approval-card.js +41 -7
- package/dist/approval-reconciler.d.ts +108 -11
- package/dist/approval-reconciler.js +146 -19
- package/dist/boot/coordinators.js +10 -2
- package/dist/boot/org-memory.d.ts +6 -0
- package/dist/boot/org-memory.js +1 -1
- package/dist/boot/reapers.d.ts +2 -0
- package/dist/boot/reapers.js +11 -4
- package/dist/boot/resolve-spec.d.ts +3 -2
- package/dist/boot/resolve-spec.js +132 -32
- package/dist/boot/runner-deps.d.ts +23 -1
- package/dist/boot/runner-deps.js +8 -11
- package/dist/boot/workflow-orchestration.d.ts +8 -3
- package/dist/boot/workflow-orchestration.js +23 -1
- package/dist/config-center/apply-effective.js +33 -10
- package/dist/config-types.d.ts +24 -8
- package/dist/config.d.ts +6 -1
- package/dist/config.js +56 -11
- package/dist/env-facts.d.ts +3 -1
- package/dist/env-facts.js +3 -1
- package/dist/fleet/fleet-bus.d.ts +6 -1
- package/dist/fleet/fleet-bus.js +25 -3
- package/dist/governance-ask-marks.d.ts +31 -0
- package/dist/governance-ask-marks.js +122 -0
- package/dist/hooks/hook-runner.d.ts +28 -0
- package/dist/hooks/hook-runner.js +149 -25
- package/dist/http/routes/diagnostics.js +10 -5
- package/dist/http/routes/memory-policy.d.ts +2 -1
- package/dist/http/routes/memory-policy.js +77 -13
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/routes/tasks.js +59 -22
- package/dist/http/server.d.ts +5 -0
- package/dist/http/server.js +23 -12
- package/dist/http/wire-types.d.ts +7 -2
- package/dist/main.js +17 -5
- package/dist/observability/fail-open.d.ts +13 -2
- package/dist/observability/fail-open.js +15 -4
- package/dist/observability/prompt-manifest.d.ts +5 -1
- package/dist/orchestration/workflow-notify-journal.d.ts +57 -1
- package/dist/orchestration/workflow-notify-journal.js +137 -32
- package/dist/parked-decide.js +9 -4
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
- package/dist/plugins/approval-ask-store-memory.js +3 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +27 -5
- package/dist/plugins/approval-ask-store-sql.js +9 -2
- package/dist/plugins/background-shell-support.d.ts +1 -1
- package/dist/plugins/background-shell-support.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +62 -6
- package/dist/plugins/checkpoint-store-sql.js +71 -11
- package/dist/plugins/local-checkpoint-store.d.ts +20 -1
- package/dist/plugins/local-checkpoint-store.js +19 -0
- package/dist/plugins/mailbox-store-sql.d.ts +4 -10
- package/dist/plugins/mailbox-store-sql.js +57 -4
- package/dist/runs.d.ts +8 -0
- package/dist/runs.js +15 -2
- package/dist/runtime-governance.d.ts +18 -0
- package/dist/runtime-governance.js +90 -3
- package/dist/task-settings.d.ts +3 -9
- package/dist/task-settings.js +16 -13
- package/dist/tool-approval.d.ts +33 -6
- package/dist/tool-approval.js +80 -23
- package/dist/trace/core-keyset-guard.d.ts +17 -3
- package/package.json +3 -3
- package/dist/boot/lexical-path-env.d.ts +0 -10
- package/dist/boot/lexical-path-env.js +0 -88
- package/dist/capabilities/oa-tools.d.ts +0 -15
- package/dist/capabilities/oa-tools.js +0 -54
- package/dist/finance/cost-taxonomy.d.ts +0 -34
- package/dist/finance/cost-taxonomy.js +0 -26
- package/dist/plugins/approval-store-sql.d.ts +0 -116
- package/dist/plugins/approval-store-sql.js +0 -151
- package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
- package/dist/plugins/file-workflow-journal-store.js +0 -12
- package/dist/plugins/pg-approval-store.d.ts +0 -9
- package/dist/plugins/pg-approval-store.js +0 -9
- package/dist/plugins/pg-breaker-state.d.ts +0 -8
- package/dist/plugins/pg-breaker-state.js +0 -8
- package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
- package/dist/plugins/pg-checkpoint-store.js +0 -10
- package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/pg-file-snapshot-store.js +0 -8
- package/dist/plugins/pg-image-bake.d.ts +0 -12
- package/dist/plugins/pg-image-bake.js +0 -11
- package/dist/plugins/pg-image-index.d.ts +0 -12
- package/dist/plugins/pg-image-index.js +0 -11
- package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
- package/dist/plugins/pg-outcome-ledger.js +0 -11
- package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/pg-resume-anchor-store.js +0 -7
- package/dist/plugins/pg-run-store.d.ts +0 -9
- package/dist/plugins/pg-run-store.js +0 -9
- package/dist/plugins/pg-session-policy-store.d.ts +0 -7
- package/dist/plugins/pg-session-policy-store.js +0 -7
- package/dist/plugins/pg-session-store.d.ts +0 -12
- package/dist/plugins/pg-session-store.js +0 -12
- package/dist/plugins/pg-tool-result-store.d.ts +0 -9
- package/dist/plugins/pg-tool-result-store.js +0 -9
- package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-journal-store.js +0 -9
- package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-run-store.js +0 -9
- package/dist/plugins/tidb-approval-store.d.ts +0 -8
- package/dist/plugins/tidb-approval-store.js +0 -8
- package/dist/plugins/tidb-breaker-state.d.ts +0 -7
- package/dist/plugins/tidb-breaker-state.js +0 -7
- package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
- package/dist/plugins/tidb-checkpoint-store.js +0 -9
- package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/tidb-file-snapshot-store.js +0 -8
- package/dist/plugins/tidb-image-bake.d.ts +0 -12
- package/dist/plugins/tidb-image-bake.js +0 -11
- package/dist/plugins/tidb-image-index.d.ts +0 -12
- package/dist/plugins/tidb-image-index.js +0 -11
- package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
- package/dist/plugins/tidb-outcome-ledger.js +0 -12
- package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/tidb-resume-anchor-store.js +0 -7
- package/dist/plugins/tidb-run-store.d.ts +0 -10
- package/dist/plugins/tidb-run-store.js +0 -9
- package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
- package/dist/plugins/tidb-session-policy-store.js +0 -7
- package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
- package/dist/plugins/tidb-tool-result-store.js +0 -10
- package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/tidb-workflow-journal-store.js +0 -9
- package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
- package/dist/plugins/tidb-workflow-run-store.js +0 -10
- package/dist/plugins/workflow-journal-limits.d.ts +0 -12
- package/dist/plugins/workflow-journal-limits.js +0 -12
- package/dist/sema-registry.d.ts +0 -41
- package/dist/sema-registry.js +0 -40
|
@@ -158,14 +158,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
158
158
|
// relay keeps burning billable tokens to completion (council). Mirrors the closed-flag pattern of the
|
|
159
159
|
// poll-based SSE endpoints, and additionally cancels the generator via spec.signal.
|
|
160
160
|
const ac = new AbortController();
|
|
161
|
-
//
|
|
162
|
-
// Ctrl+C on the shell drops the SSE and this abort kills the run, but the mark was only wired on the
|
|
163
|
-
// cancel-verb/resume legs, so background children reaped by THIS abort read "parent"/"system" instead
|
|
164
|
-
// of "user". Same registration discipline as runs.ts (FIRST on the signal, before core's teardown
|
|
165
|
-
// listeners). Owner = sessionId (the sync leg's core-canonical taskId — resolveSpec sets no spec.taskId,
|
|
166
|
-
// same key as the resume leg); skipSessionScoped inside the helper keeps CC Backgrounded children unmarked.
|
|
167
|
-
if (prepared.spec.sessionId)
|
|
168
|
-
markChildrenStoppedByUserOnAbort(ac.signal, prepared.spec.sessionId, prepared.auth?.principal);
|
|
161
|
+
// 🔴 stoppedBy 归因的注册点在**下面**(createRun 认领成功之后),不在这里 —— #168 件3,见那里的注。
|
|
169
162
|
let closed = false;
|
|
170
163
|
let detachLogged = false; // [854]①b:detach 断连 info 只打一条(req/res 两个 close listener 都可能进来)
|
|
171
164
|
const onDisconnect = () => {
|
|
@@ -225,7 +218,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
225
218
|
// 跨腿投递,租户门必须带上,不能让 A 租户的开流看到 B 租户的卡。
|
|
226
219
|
// 有界(2s)+ fail-open:store 抛或挂起都只记一次 warn、零卡继续开流(§5.3)。
|
|
227
220
|
// 🔴 开关谓词与 runs.ts 同源同理由(codex 复审第二轮 high:回滚下表里可能有残留 STREAM_PENDING
|
|
228
|
-
//
|
|
221
|
+
// 行,无谓词会破「开关关 = 逐字零变化」;顺带省掉关闭态每次开流的一次 store 往返)。钉:场景9。
|
|
229
222
|
const replaySessionId = deps.config.streamApproval.enabled ? prepared.spec.sessionId : undefined;
|
|
230
223
|
if (replaySessionId) {
|
|
231
224
|
const replayOwner = gatedPrincipal(req, deps.config) ?? null;
|
|
@@ -323,6 +316,23 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
323
316
|
}
|
|
324
317
|
durableTaskId = tid;
|
|
325
318
|
}
|
|
319
|
+
// ── stoppedBy 归因(#168 件3:注册点必须晚于 run 认领)────────────────────────────────────
|
|
320
|
+
// probe-stoppedby: a client DISCONNECT on the sync stream lane is a USER stop too — Ctrl+C on the
|
|
321
|
+
// shell drops the SSE and this abort kills the run, but the mark was only wired on the
|
|
322
|
+
// cancel-verb/resume legs, so background children reaped by THIS abort read "parent"/"system"
|
|
323
|
+
// instead of "user". Same registration discipline as runs.ts (FIRST on the signal, before core's
|
|
324
|
+
// teardown listeners). Owner = sessionId (the sync leg's core-canonical taskId — resolveSpec sets no
|
|
325
|
+
// spec.taskId, same key as the resume leg); skipSessionScoped inside the helper keeps CC Backgrounded
|
|
326
|
+
// children unmarked.
|
|
327
|
+
//
|
|
328
|
+
// 🔴 **位置即正确性**(#168 件3,3c codex R3 定性的先存病):这条规则说的是「本请求断连 ⇒ 把
|
|
329
|
+
// owner=sessionId 的 task-scoped 子代记成人为停止」,而**只有抢到了这条 session 的请求**才有资格
|
|
330
|
+
// 那么说。注册早于 `createRun` 时,一次 pre-start 断连会把**另一条活 run** 的子代标成 user-stopped
|
|
331
|
+
// (认领随后 409,本请求根本没代表过这条 session)。窗 = SSE 头/首帧 + 开流 preamble(有界 2s)+
|
|
332
|
+
// 一次 DB 往返。认领失败那条路在上面就 return 了 ⇒ 一次都不注册。
|
|
333
|
+
// 无 durable 账本(无 runStore / 无 sessionId)的部署没有认领这回事,注册点与从前等价。
|
|
334
|
+
if (prepared.spec.sessionId)
|
|
335
|
+
markChildrenStoppedByUserOnAbort(ac.signal, prepared.spec.sessionId, prepared.auth?.principal);
|
|
326
336
|
// [900]①/[906]③ detach 车道逐事件落账:sync 腿历史上只 createRun+setTerminal(events 账本恒空,
|
|
327
337
|
// detach 后壳只能轮询终局)。opt-in 车道把 for-await 的每个事件喂给与 bg 腿(runs.ts)共享的
|
|
328
338
|
// LedgerSink(src/trace/ledger-sink.ts —— 单一 switch,落账 type/data 形状与 GET /v1/runs/:id/events
|
|
@@ -815,9 +825,13 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
815
825
|
const askOwner = gatedPrincipal(req, deps.config) ?? null;
|
|
816
826
|
const emitAsk = (frame) => {
|
|
817
827
|
// #157(codex R2 复审 HIGH):流已断时这里是**静默 no-op** —— 上游 coordinator 的 try/catch
|
|
818
|
-
// 因此拿不到任何信号,一次断连就被记成"投递成功"
|
|
819
|
-
//
|
|
820
|
-
//
|
|
828
|
+
// 因此拿不到任何信号,一次断连就被记成"投递成功",该 ask 挂到 TTL 才按无人应答结算。当时
|
|
829
|
+
// 方向不动,但**必须留痕**:否则 `open-frame-undelivered` 只覆盖"持久化抛错",覆盖不到真正
|
|
830
|
+
// 常见的那条路。
|
|
831
|
+
// ⚠️ **question 半场已在 #173 换了方向**(a318fa1):question / question_complete 两型帧改走
|
|
832
|
+
// 下方 `emitQuestion` 的断流 THROW,于是它们**到不了**本 no-op 臂(两道守卫同步相邻、其间无
|
|
833
|
+
// await);实测 park 墙钟 8002ms → <160ms。今天真正还落在这条静默臂上的只有 elicitation 族
|
|
834
|
+
// 与其它运行流帧。别再据本段把 question 的 TTL 悬挂当作现行行为。
|
|
821
835
|
// `destroyed` 与 `writableEnded` 一起判——emitApproval 用的就是这对谓词。
|
|
822
836
|
// 分类按帧型走(`failOpenTagForDroppedFrame`):这条闭包同时驮着 open 帧、完成面包屑与
|
|
823
837
|
// 其它运行流帧,一刀切会让**一次**断连把保护型计数记两次,真 open 失败率被面包屑盖住。
|
|
@@ -832,13 +846,18 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
832
846
|
// runId). `askOwner` = the VERIFIED principal → only this caller's own completions are emitted.
|
|
833
847
|
// 🔒 THROW on a dead/ended stream instead of emitAsk's silent no-op (1.80 review: a disconnect
|
|
834
848
|
// between request-send and this drain made the two-phase ack a FALSE success — the entry was
|
|
835
|
-
// removed with no frame ever written; throwing keeps it pending for the next leg).
|
|
836
|
-
//
|
|
849
|
+
// removed with no frame ever written; throwing keeps it pending for the next leg). This wrapper was
|
|
850
|
+
// once the ONLY one that threw; since #173 (a318fa1) emitQuestion, emitApproval and writeLive all
|
|
851
|
+
// throw on a dead stream too. Of the HITL family only ELICIT still rides emitAsk's no-op — but emitAsk
|
|
852
|
+
// is NOT elicit-only: SendUserFile's `file_link` (below) and other runtime frames also write straight
|
|
853
|
+
// through it, so its dead-stream drop surface is wider than the HITL lanes (file_link is covered by its
|
|
854
|
+
// own durable twin, not by a throw).
|
|
837
855
|
// 🔴 drain 案: write these as ANONYMOUS `data:` frames, NOT emitAsk's NAMED
|
|
838
856
|
// `event: <type>` form — the run's own live-leg task_notification frames on this SAME connection
|
|
839
857
|
// are anonymous (L1392/L1474), so a shell parsing the default message channel silently never saw
|
|
840
|
-
// a drained frame (the Monitor "park✓ drain✗" fingerprint).
|
|
841
|
-
//
|
|
858
|
+
// a drained frame (the Monitor "park✓ drain✗" fingerprint). The HITL frames keep their own
|
|
859
|
+
// established NAMED-event contract — elicit still writes straight through emitAsk, question rides
|
|
860
|
+
// emitQuestion (same named-event write, one extra dead-stream guard in front, #173).
|
|
842
861
|
await emitPendingWorkflowCompletions(deps.workflowCompletionInbox, prepared.spec.sessionId, askOwner, (frame) => {
|
|
843
862
|
if (closed || res.writableEnded || res.destroyed)
|
|
844
863
|
throw new Error("stream ended before the completion frame was written");
|
|
@@ -860,11 +879,26 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
860
879
|
: streamBody;
|
|
861
880
|
// #152 复审 A1:`deliverable` 是 durable 部署上 park-vs-live 判决的投递面谓词(见 question.ts 该键注)。
|
|
862
881
|
// 本腿的投递面就是这条 SSE 连接,而 ①b detach 车道下客户端断连**不 abort、不结束这条腿**——run 继续在
|
|
863
|
-
// 本 runWithContext
|
|
864
|
-
//
|
|
865
|
-
//
|
|
882
|
+
// 本 runWithContext 作用域里跑。谓词让判决面此时如实读作「无活流」⇒ durable park(运维可经 /decide
|
|
883
|
+
// 补答),而不是 allow 后挂满 ttl。
|
|
884
|
+
// #173([2937] test 墙钟反证定谳):question 开帧的 emit 不再共享 emitAsk 的静默 no-op——死流上
|
|
885
|
+
// no-op = 开帧「假投递成功」,coordinator 等满 TTL 才以 unavailable 结算(实测 park 墙钟 ≈TTL,
|
|
886
|
+
// 断连→ask 间隔拉到 15s 也一样:判决在 gate 时刻,而 gate 早已判过的窗口里 ask 只剩 emit 一道门)。
|
|
887
|
+
// 收紧与 emitApproval 同形:断流 THROW ⇒ #166 的 delivery==="failed" 臂**立即** settle
|
|
888
|
+
// unavailable,不再等满 TTL。
|
|
889
|
+
// ⚠️ 收紧买到的是**结算速度**,不是可恢复性(2026-08-07 亲读 core 5.16 定):本闭包只在工具已
|
|
890
|
+
// 执行时被调用,即 `createDurableQuestionGate` 已判 allow、core 已过 `suspendAsk` 挂起点 ⇒ 这一
|
|
891
|
+
// 形落 `declined_unavailable` 自答续跑卡(赎回既有审批的腿返 isError),**不铸 checkpoint**。
|
|
892
|
+
// 真正 park 的是判决时投递面就不可达的腿(`deliverable`/`hasLiveContext` 读 false ⇒ 门判 ask),
|
|
893
|
+
// 它们根本走不到这里。别把两形写成一句。
|
|
894
|
+
// elicitation 保持 no-op(其 decline 语义 fail-closed,#166 车登记另议)。
|
|
895
|
+
const emitQuestion = (frame) => {
|
|
896
|
+
if (res.writableEnded || res.destroyed)
|
|
897
|
+
throw new Error("live stream ended — question frame undeliverable");
|
|
898
|
+
emitAsk(frame);
|
|
899
|
+
};
|
|
866
900
|
const withQuestion = () => deps.question
|
|
867
|
-
? deps.question.runWithContext({ taskId: askTaskId, owner: askOwner, emit:
|
|
901
|
+
? deps.question.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitQuestion, abortSignal: ac.signal, deliverable: () => !res.writableEnded && !res.destroyed }, withElicit)
|
|
868
902
|
: withElicit();
|
|
869
903
|
// [816]/[820]②: the live tool-approval context — SAME id/owner/emit/signal as question/elicit
|
|
870
904
|
// (nested ALS, each its own store). sessionId keys the "allow all edits this session" state.
|
|
@@ -874,8 +908,11 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
874
908
|
// 🔒 approval 帧不用共享 emitAsk 的静默 no-op 语义:res 已结束时 no-op = 卡「假投递成功」,
|
|
875
909
|
// coordinator 等满 TTL 落 deny —— G1 "unavailable"→park 腿在真 wiring 里就不可达(cross-review
|
|
876
910
|
// 抓获)。这里 THROW,coordinator 的 emit-catch 按「无活人可达」返 "unavailable"(durable 部署
|
|
877
|
-
// park;非 durable 由 core fail-closed deny,行为不劣于旧 no-op)
|
|
878
|
-
//
|
|
911
|
+
// park;非 durable 由 core fail-closed deny,行为不劣于旧 no-op)。
|
|
912
|
+
// 当年本注写的「仅 approval 帧收紧」已被 #173 推进一格:question 帧此后与本臂**同形**收紧(见上面
|
|
913
|
+
// `emitQuestion`)。**HITL 三族里**仍留在共享 emitAsk 静默 no-op 上的只有 elicitation(其 decline
|
|
914
|
+
// 结算本身就是 fail-closed,收紧收益小,另议);emitAsk 本身还驮着 SendUserFile 的 `file_link` 等
|
|
915
|
+
// 非 HITL 帧,那些不在本条收紧的辖域内(file_link 的补偿是它自己的 durable 双写,不是 throw)。
|
|
879
916
|
const emitApproval = (frame) => {
|
|
880
917
|
if (res.writableEnded || res.destroyed)
|
|
881
918
|
throw new Error("live stream ended — approval card undeliverable");
|
package/dist/http/server.d.ts
CHANGED
|
@@ -266,6 +266,11 @@ export interface ServiceSeamDeps {
|
|
|
266
266
|
* (MEMORY_ENGINE_BACKEND=pg|tidb); absent / file posture ⇒ 501 honestly (a single-user file plane
|
|
267
267
|
* IS the TOC side of a sync — it has no central-authority half to serve). */
|
|
268
268
|
memorySync?: (scope: string, syncReq: MemorySyncRequest) => Promise<MemorySyncResponse>;
|
|
269
|
+
/** design/170 件A §7 —— org 记忆授权目录,`boot/org-memory.ts` 装配的**同一个实例**(准入 seam 与
|
|
270
|
+
* 本面共享它的 TTL 缓存/退避窗/gen 高水位)。memory-policy 面的 `org:` 属主门用它回答成员性:
|
|
271
|
+
* 读面要该 scope 在授权表里、写面另要条目 `write === true`。缺席=本部署没有目录源 ⇒ `org:` 键
|
|
272
|
+
* 逐字保持 operator-only(收编前行为),绝不因目录不接线而放宽。 */
|
|
273
|
+
orgMemoryDirectory?: import("../org-memory-admission.js").OrgMemoryDirectory;
|
|
269
274
|
/** WEB-SETUP-RECON 附B — org sessionMirror 治理态的 SERVICE 半场 = **观测面(audit),非执法面**。
|
|
270
275
|
* 裁定 (c):「该镜像而未镜像」发生在数据产生端(壳)——本 server 是镜像**目标**,只看得见到达的 push;
|
|
271
276
|
* 没到达的会话对 server 不存在(无客户端会话清单、无心跳契约),required 锁的真执法只能在壳(CC
|
package/dist/http/server.js
CHANGED
|
@@ -1456,9 +1456,14 @@ export function createHttpServer(rawDeps) {
|
|
|
1456
1456
|
// "approve a content-ask with no answer" (runtask.js: `taskConfig.onQuestion ?? deps.onQuestion` is
|
|
1457
1457
|
// `undefined` or QUESTION_AWAITS_RESUME), but on an ASK_QUESTION_ENABLED deployment the spec no longer
|
|
1458
1458
|
// stamps the sentinel and `deps.onQuestion` is the coordinator, so core's condition is never met and it
|
|
1459
|
-
// does NOT refuse.
|
|
1460
|
-
//
|
|
1461
|
-
//
|
|
1459
|
+
// does NOT refuse.
|
|
1460
|
+
// ⚠️ The ORIGINAL rationale — "the redeemed leg would take the coordinator's empty 'no human' default" —
|
|
1461
|
+
// is DISPROVED and must not be repeated: since #166 QuestionCoordinator never synthesizes an empty answer
|
|
1462
|
+
// set (every no-human arm returns `{kind:"unavailable"}`), and core 5.16 answers a redeemed AskUserQuestion
|
|
1463
|
+
// with an isError ("will not silently self-answer") rather than a fabricated verdict. The guard STAYS on a
|
|
1464
|
+
// cheaper rationale: the checkpoint is consumed by the CAS BEFORE that revive can report unavailable, so
|
|
1465
|
+
// the operator loses the parked question for a revive that could only fail — refusing pre-CAS costs nothing
|
|
1466
|
+
// and gives a precise error. Do not delete it (parked twin: src/parked-decide.ts, same rationale).
|
|
1462
1467
|
// - an answer is only meaningful on an approve of a question gate; on a deny or a non-question gate it
|
|
1463
1468
|
// signals the operator is deciding a different pending action than the one parked — reject, don't guess.
|
|
1464
1469
|
if (decision === "approve" && pendingTool === "AskUserQuestion" && !answer) {
|
|
@@ -1627,14 +1632,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1627
1632
|
// un-cancellable (cancel 202'd + flagged the row, but nothing honored it). markResuming reset the flag,
|
|
1628
1633
|
// so only a cancel issued DURING this leg aborts it.
|
|
1629
1634
|
const cancelCtrl = new AbortController();
|
|
1630
|
-
// stoppedBy
|
|
1631
|
-
// leg core's canonical taskId == sessionId (the rebuilt taskConfig carries no spec.taskId), so a
|
|
1632
|
-
// task-scoped child's owner KEY VALUE coincides with a session-scoped child's — the old `list()` path
|
|
1633
|
-
// couldn't tell them apart (no sessionScoped flag exposed) and took an honest degrade. The seam filters
|
|
1634
|
-
// by the EXPLICIT flag (`skipSessionScoped:true` inside the helper), so only task-scoped children take
|
|
1635
|
-
// the "user" marker; session-scoped ones a cancelled parent deliberately does NOT stop stay unmarked
|
|
1636
|
-
// (CC Backgrounded semantics). Owner = sessionId (this leg's canonical key), NOT the run-row taskId.
|
|
1637
|
-
markChildrenStoppedByUserOnAbort(cancelCtrl.signal, sessionId, principal);
|
|
1635
|
+
// 🔴 stoppedBy 归因的注册点在**下面**(markResuming 认领成功之后),不在这里 —— #168 件3,见那里的注。
|
|
1638
1636
|
// design/80 seam #2: a resumed leg is itself preempt-able. Register a FRESH preempt controller (markResuming
|
|
1639
1637
|
// also reset preempt_requested), thread a LIVE preemptSignal into the resume taskConfig below, and poll the
|
|
1640
1638
|
// durable preempt flag in the heartbeat — so the scheduler can preempt the RESUMED work once it makes
|
|
@@ -1648,6 +1646,19 @@ export function createHttpServer(rawDeps) {
|
|
|
1648
1646
|
claimedRow = await deps.runStore.markResuming(taskId);
|
|
1649
1647
|
if (!claimedRow)
|
|
1650
1648
|
return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
|
|
1649
|
+
// stoppedBy — resume leg NOW marked (core 1.256 delivered the mark-by-owner seam): on this
|
|
1650
|
+
// leg core's canonical taskId == sessionId (the rebuilt taskConfig carries no spec.taskId), so a
|
|
1651
|
+
// task-scoped child's owner KEY VALUE coincides with a session-scoped child's — the old `list()` path
|
|
1652
|
+
// couldn't tell them apart (no sessionScoped flag exposed) and took an honest degrade. The seam filters
|
|
1653
|
+
// by the EXPLICIT flag (`skipSessionScoped:true` inside the helper), so only task-scoped children take
|
|
1654
|
+
// the "user" marker; session-scoped ones a cancelled parent deliberately does NOT stop stay unmarked
|
|
1655
|
+
// (CC Backgrounded semantics). Owner = sessionId (this leg's canonical key), NOT the run-row taskId.
|
|
1656
|
+
//
|
|
1657
|
+
// 🔴 位置(#168 件3,与 /v1/tasks/stream 同族):归因规则只有**抢到这条 run 的腿**才有资格挂 ——
|
|
1658
|
+
// `markResuming` 输掉的那一支(别的副本已经在 resume,或行已被 reap)在上面就 409 走人,从此一次
|
|
1659
|
+
// 都不注册。本腿此前把它挂在 CAS 之前,只是因为 `cancelCtrl` 的唯一 abort 源(inflightRuns / 心跳
|
|
1660
|
+
// 轮询)也都在 CAS 之后注册才没出事 —— 那是巧合不是保证。
|
|
1661
|
+
markChildrenStoppedByUserOnAbort(cancelCtrl.signal, sessionId, principal);
|
|
1651
1662
|
// MF-Fleet (#7): the row is now `running` again → re-publish it LIVE to the fleet (re-appears as the same row
|
|
1652
1663
|
// that was sitting "waiting" while parked). After onStart only — never if markResuming lost the CAS (a sibling
|
|
1653
1664
|
// owns the row). onTerminal at every settle below keeps the row's lifecycle in lock-step with the run-store row.
|
|
@@ -1952,7 +1963,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1952
1963
|
case "wiring_manifest":
|
|
1953
1964
|
await flush();
|
|
1954
1965
|
await append("wiring_manifest", wiringManifestEventData(ev));
|
|
1955
|
-
break; // 🔴 core 5.14.0 design/173:resume 腿自证清单(每条腿发自己那份)
|
|
1966
|
+
break; // 🔴 core 5.14.0 design/173:resume 腿自证清单(每条腿发自己那份)。本腿与 durable 账本腿共用**同一个**租户面构造器,剥 `governance` 段(audience:"operator",no projection ⇒ do not disclose)——账本写时不知道未来读者是谁,存全量 = 把 operator-only 面写进租户可读介质。⚠️ 「三腿同源」自 #154 件一起不再成立:live sync 腿(routes/tasks.ts)按连接身份分叉,operator 连接走 `wiringManifestOperatorEventData`。所以判据是「**durable 两腿恒剥、live 腿按身份分叉**」,不是「分头挑键即泄露」——别照旧注把已落地的 operator 面当泄露删掉,也别给第四条腿抄错构造器:先问这条腿知不知道读者是谁
|
|
1956
1967
|
case "human_input":
|
|
1957
1968
|
await flush();
|
|
1958
1969
|
await append("human_input", humanInputEventData(ev));
|
|
@@ -235,8 +235,13 @@ export interface TaskRequestBody {
|
|
|
235
235
|
* 后下一轮把占位符换成真 schema。
|
|
236
236
|
* 为什么要上 wire:cli 的 #181 取证表明,忠实跟随广告 schema 解码的 provider(openai-completions 车道)
|
|
237
237
|
* 在 `static` 下会**无界循环** —— 每轮发 `{}` → invalidArgumentsRejection → 下一轮广告仍空,纠错回路
|
|
238
|
-
* 结构上无效。core
|
|
239
|
-
*
|
|
238
|
+
* 结构上无效。core 的缺省链是 `spec.toolMaterializeStrategy ?? env ?? <core 缺省>`,而**进程级 env 说不了
|
|
239
|
+
* 「每个模型」的话**(模型目录车道 per-model api 可混),所以必须有一个**按任务**的位子——这条理由与
|
|
240
|
+
* core 缺省是哪个词无关,故不随其变。
|
|
241
|
+
* ⚠️ core 缺省本身已翻:5.15.0 BREAKING 起 `<core 缺省>` = **`"swap"`**(5.14 世代是 `"static"`;
|
|
242
|
+
* core 把 `"static"` 重新定性为「a strict explicit opt-in」)。本注上一版还写着 `?? "static"`,照它推
|
|
243
|
+
* 「不挂键 ⇒ 落 static」会反。缺省词的属主是 core,本仓不复述第二份——要确认取值去读 core 的
|
|
244
|
+
* `prepare-task` 与其 CHANGELOG。
|
|
240
245
|
* 缺省不挂键(交给 core 的 env/缺省链);未知词 400 fail-loud(与 promptProfile/permissionMode 同姿势 ——
|
|
241
246
|
* 静默折缺省会让调用方以为切了策略却没切)。 */
|
|
242
247
|
toolMaterializeStrategy?: "static" | "swap";
|
package/dist/main.js
CHANGED
|
@@ -184,7 +184,7 @@ async function main() {
|
|
|
184
184
|
// baseline, so a task-hooks fold (resolveSpec) shadows neither.
|
|
185
185
|
const deploymentHooks = toolTracer ? composeHooks(createPermissionDeniedMeter(metrics), toolTracer) : createPermissionDeniedMeter(metrics);
|
|
186
186
|
// design/158 A10:workflow 编排面搬到 src/boot/workflow-orchestration.ts(逐字;构造次序契约见该文件头注)。
|
|
187
|
-
const { sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion, workflowNotifyGate, fleetBus, workflowRunStore, workflowJournalStore, outcomeSink, workflowAgentRegistry, subagentSteerRegistry, } = createWorkflowOrchestration({ config, logger, metrics, localRoot, backend, getRunStore: () => runStore });
|
|
187
|
+
const { sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion, workflowNotifyGate, fleetBus, workflowRunStore, workflowJournalStore, outcomeSink, workflowRecoverOpts, workflowAgentRegistry, subagentSteerRegistry, } = createWorkflowOrchestration({ config, logger, metrics, localRoot, backend, getRunStore: () => runStore });
|
|
188
188
|
// design/158 A10:活体协调器 + SendUserFile 工具面搬到 src/boot/coordinators.ts(逐字;durableEnabled 的
|
|
189
189
|
// 「必须早于 runnerDeps 求值」次序契约见该文件头注)。
|
|
190
190
|
const { elicitation, question, toolApproval, durableEnabled, streamApprovalGate, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({ config, logger, backend, sendUserFileTaskEnvs });
|
|
@@ -406,8 +406,15 @@ async function main() {
|
|
|
406
406
|
// Finding #2 (edge case): the single-user auto-accept tool-policy BASELINE applies ONLY when the operator
|
|
407
407
|
// expressed NO gate intent. If they SET approval flags (APPROVAL_REQUIRE/APPROVAL_DENY/DURABLE_APPROVAL) but no
|
|
408
408
|
// backing store is wired (e.g. DB_BACKEND=local → no checkpointStore ⇒ durableEnabled
|
|
409
|
-
// false), that is a MISCONFIG — do NOT mask it with allow-all; leave toolPolicy undefined
|
|
410
|
-
//
|
|
409
|
+
// false), that is a MISCONFIG — do NOT mask it with allow-all; leave toolPolicy undefined. So the baseline needs
|
|
410
|
+
// single-user turnkey ∧ zero expressed gate intent.
|
|
411
|
+
// 🔴 #177 后本段的**信号面**已换人(原文「so core's UNGATED warning still surfaces it」已过期):守卫集
|
|
412
|
+
// (SENSITIVE_WRITE_PATTERNS,缺省非空)现在从 governance 拍无条件铸一条 DENY 基线 ⇒ 缺省部署的
|
|
413
|
+
// `spec.toolPolicy` 恒在场 ⇒ core 的 `hasEffectAwareGate`(prepare-task,判据是 policyLayers.length>0)
|
|
414
|
+
// 恒为真,那条 UNGATED onError 不再触发。补偿件在 boot/resolve-spec.ts:同一形(durable 门关 ∧ 单用户
|
|
415
|
+
// 基线不适用 ∧ 守卫集开)在 boot 期发一次 `tool_policy_only_sensitive_baseline` warn,把「守卫段之外
|
|
416
|
+
// 无裁决」这件事按我们自己的话说出来。本行留 `undefined` 仍是对的(不拿 allow-all 掩盖 misconfig),
|
|
417
|
+
// 只是它不再是那条信号的产地。
|
|
411
418
|
// 单源在 approval.ts(与 inv#2 同文件)—— 这里原是内联的三格枚举,漏了 approvalNeverAuto,详见该函数顶注。
|
|
412
419
|
const operatorGateIntent = hasOperatorGateIntent(config);
|
|
413
420
|
// D3-F1(全窗复审,HIGH):门意图已表达但唯一门形(durable)不可服务 ⇒ boot 拒启带双出路指引。
|
|
@@ -663,7 +670,7 @@ async function main() {
|
|
|
663
670
|
const reaper = startReapers({
|
|
664
671
|
config, logger, metrics, localRoot, backend, subRunner, runStore, checkpointStore,
|
|
665
672
|
rateLimiter, costQuota, toolResultStore, fileSnapshotStore, taskAttachmentStore, imageBakes, worktreeReap,
|
|
666
|
-
workflowNotifyGate, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore,
|
|
673
|
+
workflowNotifyGate, workflowRecoverOpts, workflowJournalStore, sqlWorkflowRunStore, workflowNotifyJournal, rosterStore,
|
|
667
674
|
backgroundAgentStore, mailboxStore, toolApproval,
|
|
668
675
|
getRunDenySweep: () => runDenySweep,
|
|
669
676
|
});
|
|
@@ -810,6 +817,10 @@ async function main() {
|
|
|
810
817
|
memorySync: memoryExportBackend && memorySyncCursors
|
|
811
818
|
? (scope, syncReq) => performMemorySync(memoryExportBackend, memorySyncCursors, scope, syncReq)
|
|
812
819
|
: undefined,
|
|
820
|
+
// design/170 件A §7 收编:memory-policy 面的 `org:` 属主门与 core 准入 seam 读**同一个**目录实例
|
|
821
|
+
// (`createOrgMemoryAdmissionWiring` 的产物),两面因此共享 TTL 缓存/退避窗/gen 高水位 —— 一个进程
|
|
822
|
+
// 对「谁属于 org:acme」只有一个答案。缺席(无 center 且无 env 表)⇒ 策略面 `org:` 键仍 operator-only。
|
|
823
|
+
orgMemoryDirectory: orgMemoryAdmission.directory,
|
|
813
824
|
// sessionMirror 观测面(server 非执法端,论证在 ServiceDeps.sessionMirrorRuling):
|
|
814
825
|
// 与 executionRuling 同车同缓存(零额外 center RTT);无 center/dry-run ⇒ 不接线,观测面暗、零行为差。
|
|
815
826
|
sessionMirrorRuling: principalCaps
|
|
@@ -1028,8 +1039,9 @@ async function main() {
|
|
|
1028
1039
|
// blocked on it (recovery only re-fires completion notifications — independent of serving new requests). A
|
|
1029
1040
|
// sweep throw is logged, never fatal.
|
|
1030
1041
|
if (workflowNotifyGate) {
|
|
1042
|
+
// [2995]/[2999] opts 单点合成在 workflow-orchestration(可见性回灌 + replica-local 形 boot 孤儿判死)。
|
|
1031
1043
|
void workflowNotifyGate
|
|
1032
|
-
.recover(
|
|
1044
|
+
.recover(workflowRecoverOpts)
|
|
1033
1045
|
.then((tally) => logger.info("workflow_notify_recovered", tally))
|
|
1034
1046
|
.catch((err) => logger.warn("workflow_notify_recover_failed", { err: String(err) }));
|
|
1035
1047
|
}
|
|
@@ -11,7 +11,7 @@ export interface FailOpenTagEntry {
|
|
|
11
11
|
export declare const FAIL_OPEN_TAGS: {
|
|
12
12
|
readonly "server.hitl.frame-undelivered-stream-closed": {
|
|
13
13
|
readonly cls: "P-DEBT";
|
|
14
|
-
readonly note: "HITL 的 **open**
|
|
14
|
+
readonly note: "HITL 的 **open** 帧写向一条已断/已关的 SSE 连接 ⇒ 静默丢弃,上游据此把投递记成成功,该 ask 挂到 TTL 才按无人应答结算。⚠️ #173 后 question 开帧改走断流 THROW(立即结算 unavailable),因此本 tag 在产的只剩 **elicitation** 一族——读计数时勿把它当作 question 的人在环缺口。no-op 而非 throw 对 elicitation 仍是既有的刻意决定,本条只保证它不再无声。";
|
|
15
15
|
};
|
|
16
16
|
readonly "server.stream.frame-dropped-stream-closed": {
|
|
17
17
|
readonly cls: "F";
|
|
@@ -19,7 +19,7 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
19
19
|
};
|
|
20
20
|
readonly "server.question.open-frame-undelivered": {
|
|
21
21
|
readonly cls: "P-DEBT";
|
|
22
|
-
readonly note: "AskUserQuestion 的 open 帧投递失败 ⇒
|
|
22
|
+
readonly note: "AskUserQuestion 的 open 帧投递失败 ⇒ 该问按 unavailable 结算(#166 前是合成空答)。⚠️ 本臂**恒在门判 allow 之后**(工具已在执行),core 已过挂起点 ⇒ 落点**不是 park**:普通腿发 declined_unavailable 自答续跑卡,赎回既有审批的腿返 isError(engine 拒绝自答)。也就是说这一形**不铸 checkpoint、不可恢复**,人在环这道门被一次投递故障真的跳过了(门判 allow 之前就不可达的那些腿走的是 durable park,不经本臂)。「绝不把 run 挂死」是刻意的产品姿态,但债要在遥测里显形——记债,不当合法兜底。";
|
|
23
23
|
};
|
|
24
24
|
readonly "server.elicitation.open-frame-undelivered": {
|
|
25
25
|
readonly cls: "F";
|
|
@@ -37,6 +37,10 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
37
37
|
readonly cls: "F";
|
|
38
38
|
readonly note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。";
|
|
39
39
|
};
|
|
40
|
+
readonly "server.fleet.subscriber-callback-threw": {
|
|
41
|
+
readonly cls: "F";
|
|
42
|
+
readonly note: "fleet bus 某订阅回调抛错 ⇒ 该回调本帧作废,其余订阅方与发布方不受影响。隔离是承重的:扇出同步,修前异常会传回发布方 put/update 投影点,core 持久化 catch{} 且不推进 storeRev ⇒ durable 行冻在 running 而 notify 已 ack(#183 复审 R3 HIGH)。丢的只是一个消费方的一帧渲染,故 F 类;但必须留痕——静默吞掉等于订阅方病灶永不显形。";
|
|
43
|
+
};
|
|
40
44
|
};
|
|
41
45
|
/** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
|
|
42
46
|
export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
|
|
@@ -46,6 +50,13 @@ export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
|
|
|
46
50
|
* 一刀切记同一个 tag 是错的:承载 HITL 帧的那条闭包同时驮着完成面包屑与其它运行流帧,于是**一次**
|
|
47
51
|
* 断连会让保护型计数涨两次(open 一次、complete 又一次),真正的 open 失败率被面包屑的假阳性盖住。
|
|
48
52
|
* 只有 open 帧是"人在环这道门被跳过"的证据,其余都是体验损失。
|
|
53
|
+
*
|
|
54
|
+
* ⚠️ **两条 question 臂在生产上已不可达**(#173 之后,2026-08-07):本函数唯一的生产调用点是
|
|
55
|
+
* `routes/tasks.ts` 的 `emitAsk` 断流臂,而 question 与 question_complete 两型帧都先过同文件
|
|
56
|
+
* `emitQuestion` 的断流守卫(两道守卫同步相邻、其间无 await)并在断流时 throw,永不抵达这里;另一条
|
|
57
|
+
* question 路(`runs.ts` 的 durable append)根本不经 emitAsk。在产的只剩 elicitation 族与 default。
|
|
58
|
+
* 两臂**保留不删**:它们是本映射的语义定义(单测直打的纯函数),elicit 侧若哪天照 question 收紧、或
|
|
59
|
+
* 新增第四条 emit 腿忘了守卫,留着的臂是正确落点而不是死码。读遥测时按上面这段判在产分布。
|
|
49
60
|
*/
|
|
50
61
|
export declare function failOpenTagForDroppedFrame(frameType: string): FailOpenTag;
|
|
51
62
|
/** {@link createFailOpenRecorder} 返回的活对象(有行为、有状态 ⇒ `create*` 而非 `build*`)。 */
|
|
@@ -34,7 +34,7 @@ import { appendFileSync } from "node:fs";
|
|
|
34
34
|
export const FAIL_OPEN_TAGS = {
|
|
35
35
|
"server.hitl.frame-undelivered-stream-closed": {
|
|
36
36
|
cls: "P-DEBT",
|
|
37
|
-
note: "HITL 的 **open**
|
|
37
|
+
note: "HITL 的 **open** 帧写向一条已断/已关的 SSE 连接 ⇒ 静默丢弃,上游据此把投递记成成功,该 ask 挂到 TTL 才按无人应答结算。⚠️ #173 后 question 开帧改走断流 THROW(立即结算 unavailable),因此本 tag 在产的只剩 **elicitation** 一族——读计数时勿把它当作 question 的人在环缺口。no-op 而非 throw 对 elicitation 仍是既有的刻意决定,本条只保证它不再无声。",
|
|
38
38
|
},
|
|
39
39
|
"server.stream.frame-dropped-stream-closed": {
|
|
40
40
|
cls: "F",
|
|
@@ -42,7 +42,7 @@ export const FAIL_OPEN_TAGS = {
|
|
|
42
42
|
},
|
|
43
43
|
"server.question.open-frame-undelivered": {
|
|
44
44
|
cls: "P-DEBT",
|
|
45
|
-
note: "AskUserQuestion 的 open 帧投递失败 ⇒
|
|
45
|
+
note: "AskUserQuestion 的 open 帧投递失败 ⇒ 该问按 unavailable 结算(#166 前是合成空答)。⚠️ 本臂**恒在门判 allow 之后**(工具已在执行),core 已过挂起点 ⇒ 落点**不是 park**:普通腿发 declined_unavailable 自答续跑卡,赎回既有审批的腿返 isError(engine 拒绝自答)。也就是说这一形**不铸 checkpoint、不可恢复**,人在环这道门被一次投递故障真的跳过了(门判 allow 之前就不可达的那些腿走的是 durable park,不经本臂)。「绝不把 run 挂死」是刻意的产品姿态,但债要在遥测里显形——记债,不当合法兜底。",
|
|
46
46
|
},
|
|
47
47
|
"server.elicitation.open-frame-undelivered": {
|
|
48
48
|
cls: "F",
|
|
@@ -60,6 +60,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
60
60
|
cls: "F",
|
|
61
61
|
note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。",
|
|
62
62
|
},
|
|
63
|
+
"server.fleet.subscriber-callback-threw": {
|
|
64
|
+
cls: "F",
|
|
65
|
+
note: "fleet bus 某订阅回调抛错 ⇒ 该回调本帧作废,其余订阅方与发布方不受影响。隔离是承重的:扇出同步,修前异常会传回发布方 put/update 投影点,core 持久化 catch{} 且不推进 storeRev ⇒ durable 行冻在 running 而 notify 已 ack(#183 复审 R3 HIGH)。丢的只是一个消费方的一帧渲染,故 F 类;但必须留痕——静默吞掉等于订阅方病灶永不显形。",
|
|
66
|
+
},
|
|
63
67
|
};
|
|
64
68
|
/**
|
|
65
69
|
* 断流丢帧该记哪个 tag —— **按帧型分类**,纯函数(与写流的那条闭包解耦,才单测得动)。
|
|
@@ -67,13 +71,20 @@ export const FAIL_OPEN_TAGS = {
|
|
|
67
71
|
* 一刀切记同一个 tag 是错的:承载 HITL 帧的那条闭包同时驮着完成面包屑与其它运行流帧,于是**一次**
|
|
68
72
|
* 断连会让保护型计数涨两次(open 一次、complete 又一次),真正的 open 失败率被面包屑的假阳性盖住。
|
|
69
73
|
* 只有 open 帧是"人在环这道门被跳过"的证据,其余都是体验损失。
|
|
74
|
+
*
|
|
75
|
+
* ⚠️ **两条 question 臂在生产上已不可达**(#173 之后,2026-08-07):本函数唯一的生产调用点是
|
|
76
|
+
* `routes/tasks.ts` 的 `emitAsk` 断流臂,而 question 与 question_complete 两型帧都先过同文件
|
|
77
|
+
* `emitQuestion` 的断流守卫(两道守卫同步相邻、其间无 await)并在断流时 throw,永不抵达这里;另一条
|
|
78
|
+
* question 路(`runs.ts` 的 durable append)根本不经 emitAsk。在产的只剩 elicitation 族与 default。
|
|
79
|
+
* 两臂**保留不删**:它们是本映射的语义定义(单测直打的纯函数),elicit 侧若哪天照 question 收紧、或
|
|
80
|
+
* 新增第四条 emit 腿忘了守卫,留着的臂是正确落点而不是死码。读遥测时按上面这段判在产分布。
|
|
70
81
|
*/
|
|
71
82
|
export function failOpenTagForDroppedFrame(frameType) {
|
|
72
83
|
switch (frameType) {
|
|
73
|
-
case "question":
|
|
84
|
+
case "question": // 见头注:#173 后生产不可达(emitQuestion 先 throw),保留为映射定义
|
|
74
85
|
case "elicitation":
|
|
75
86
|
return "server.hitl.frame-undelivered-stream-closed";
|
|
76
|
-
case "question_complete":
|
|
87
|
+
case "question_complete": // 同上,生产不可达
|
|
77
88
|
return "server.question.complete-breadcrumb-dropped";
|
|
78
89
|
case "elicitation_complete":
|
|
79
90
|
return "server.elicitation.complete-breadcrumb-dropped";
|
|
@@ -68,7 +68,11 @@ export declare function promptManifestRecordOf(e: {
|
|
|
68
68
|
wireSchemaDigest: string;
|
|
69
69
|
}>;
|
|
70
70
|
/** core 5.15.0(#181,[2872] 提货②):deferred 工具的披露策略自证——{deferredTools(数), strategy
|
|
71
|
-
* ("static"|"swap"), source(
|
|
71
|
+
* ("static"|"swap"), source(四值:`spec` / `env` / `default` / `degraded_no_direct_lane`)}。
|
|
72
|
+
* 诊断三键(数 + 两闭集词,零内容)。词表属主是 core(`core/trace.d.ts` 的 `source` 联合),本仓宽收
|
|
73
|
+
* ⇒ 词错不会红,所以照抄时务必对表:`degraded_no_direct_lane` = 直呼车道联锁把 `static` 请求收窄成
|
|
74
|
+
* `swap` 的那一格,正是运维最需要认出来的一格(本注上一版把它写成了不存在的 `api-default`,下游看板
|
|
75
|
+
* 照抄就会把真实的 degraded 读成未知词)。 */
|
|
72
76
|
toolDisclosure?: {
|
|
73
77
|
deferredTools: number;
|
|
74
78
|
strategy: string;
|
|
@@ -87,7 +87,21 @@ export declare class WorkflowNotifyGate {
|
|
|
87
87
|
* recovery-sweep error. */
|
|
88
88
|
onError?: (stage: "record" | "deliver" | "recover", runId: string, err: unknown) => void;
|
|
89
89
|
});
|
|
90
|
+
/**
|
|
91
|
+
* The runIds whose journal entry THIS process recorded — the boot-orphan judgment's authoritative anchor
|
|
92
|
+
* (round-1 review, MEDIUM). A wall-clock cutoff alone is not sound: after a BACKWARD clock step the entries
|
|
93
|
+
* this process records land BELOW the boot cutoff, and the sweep would finalize runs whose in-process
|
|
94
|
+
* executor is alive and running (a false `failed` notify for a workflow that then keeps going — worse than
|
|
95
|
+
* the absent row it was fixing). Membership here is a fact about THIS incarnation, unforgeable by any clock.
|
|
96
|
+
* Retired on ack AND the moment a sweep observes the run terminal (the anchor only ever gates the `running`
|
|
97
|
+
* arm), so a delivery/ack outage cannot pile up entries for runs that are no longer executing.
|
|
98
|
+
*/
|
|
99
|
+
private readonly recordedThisIncarnation;
|
|
90
100
|
private now;
|
|
101
|
+
/** Record a journal entry AND remember that this incarnation is the one that recorded it (see the field). */
|
|
102
|
+
private recordPending;
|
|
103
|
+
/** Ack an entry + drop its incarnation mark (acked ⇒ never scanned again, so the mark has no further use). */
|
|
104
|
+
private ackDelivered;
|
|
91
105
|
/**
|
|
92
106
|
* Journal a STARTED run as pending-notify. Call this with the synchronous `runId` from `startWorkflow` /
|
|
93
107
|
* `run_workflow` — BEFORE the workflow can reach terminal — so a crash mid-run still leaves a recoverable
|
|
@@ -129,14 +143,50 @@ export declare class WorkflowNotifyGate {
|
|
|
129
143
|
* workflow died) + ack, instead of leaking the entry forever. This closes the exact crash topology SVC-1
|
|
130
144
|
* exists for (a replica SIGKILLed mid-run).
|
|
131
145
|
* - `running` and FRESH (within the grace window): genuinely in flight → leave pending (the owning process
|
|
132
|
-
* delivers its terminal notify, or the next sweep catches it once it goes terminal or stale).
|
|
146
|
+
* delivers its terminal notify, or the next sweep catches it once it goes terminal or stale). The sweep
|
|
147
|
+
* publishes NOTHING here — see the single-writer invariant below.
|
|
133
148
|
* - MISSING (reaped / never persisted): ack-as-abandoned so the journal doesn't chase a ghost forever.
|
|
134
149
|
* `orphanGraceMs` MUST exceed the max expected workflow runtime (the run store has no cross-replica liveness
|
|
135
150
|
* signal, so age is the only orphan proxy). Returns a tally. A per-entry throw is isolated so one bad entry
|
|
136
151
|
* can't abort the sweep.
|
|
152
|
+
*
|
|
153
|
+
* BOTH abandoned arms (boot-orphan + stale-past-grace) flip the DURABLE row to `failed` FIRST (CAS on rev),
|
|
154
|
+
* so the run store, the fleet panel and the delivered notify give ONE answer — previously the notify said
|
|
155
|
+
* `failed` while `/workflows` kept saying `running` forever ([2999]: republishing without finalizing would
|
|
156
|
+
* have turned "panel empty" into "panel shows a row that never moves"). A lost CAS means the run moved under
|
|
157
|
+
* the sweep (e.g. its real terminal landed concurrently) — skip this pass; the entry stays pending and the
|
|
158
|
+
* next sweep handles the NEW state.
|
|
159
|
+
*
|
|
160
|
+
* 🔴 SINGLE-WRITER INVARIANT for the fleet row (round-1 review, two HIGH findings): the sweep only ever
|
|
161
|
+
* publishes a **TERMINAL** fleet frame (the flip above, via `publishTerminalFleetRow` → final frame + remove).
|
|
162
|
+
* It NEVER publishes a `running` row. Two reasons, both "a row we mint here can become one nobody can retire":
|
|
163
|
+
* 1. cross-replica (SQL journal): a pending `running` entry may belong to ANOTHER replica. Its terminal
|
|
164
|
+
* update lands on that replica's own (replica-local) fleet bus, and its ack removes the entry from the
|
|
165
|
+
* shared journal — so this replica would never see the run again and its minted row would sit `running`
|
|
166
|
+
* forever. (The terminal-redelivery arm deliberately doesn't publish either — it is a notify path.)
|
|
167
|
+
* 2. same-replica: `runStore.get` is a READ-TIME SNAPSHOT. If the live run commits its terminal (and the
|
|
168
|
+
* wrapper removes the row) between that read and the publish, a `running` republish resurrects a row
|
|
169
|
+
* that will never be removed again.
|
|
170
|
+
* A live row's ONE writer is this replica's `put`/`update` observation point ({@link JournalingWorkflowRunStore}),
|
|
171
|
+
* which by construction sees every transition including the terminal one.
|
|
137
172
|
*/
|
|
138
173
|
recover(opts?: {
|
|
139
174
|
orphanGraceMs?: number;
|
|
175
|
+
/** Replica-local (File/in-memory) journal ONLY: a pending entry recorded BEFORE this timestamp belongs to
|
|
176
|
+
* a previous incarnation of THIS process — workflow executors are in-process, so they died with it and a
|
|
177
|
+
* still-`running` run is a boot-orphan: finalize it NOW instead of leaving the user's "Waiting for
|
|
178
|
+
* workflow" pointed at a run that can never finish for the whole grace window ([2995] 顺带①/[3000]§二).
|
|
179
|
+
* NEVER pass this for a cross-replica (SQL) journal — there a pending running run may be genuinely alive
|
|
180
|
+
* on another replica, and age (`orphanGraceMs`) is the only sound orphan proxy.
|
|
181
|
+
* ⚠️ This wall-clock cutoff is a NECESSARY, not sufficient, condition — {@link recordedThisIncarnation}
|
|
182
|
+
* is the authoritative one (a clock rollback after boot would otherwise stamp THIS process's own new
|
|
183
|
+
* entries below the cutoff and finalize live runs). */
|
|
184
|
+
finalizeStartedBeforeMs?: number;
|
|
185
|
+
/** Retire a fleet row for a run this sweep just finalized ([2995]/[2999]): called ONLY with a TERMINAL run,
|
|
186
|
+
* so the {@link JournalingWorkflowRunStore} projection publishes the final frame and then removes the row —
|
|
187
|
+
* the store, the panel and the notify end up saying the same thing. Never called with a `running` run (see
|
|
188
|
+
* the single-writer invariant above). Late-bound because the wrapper is constructed after this gate. */
|
|
189
|
+
publishTerminalFleetRow?: (id: string, run: WorkflowRun) => void;
|
|
140
190
|
}): Promise<{
|
|
141
191
|
scanned: number;
|
|
142
192
|
redelivered: number;
|
|
@@ -234,6 +284,12 @@ export declare class JournalingWorkflowRunStore implements WorkflowRunStore {
|
|
|
234
284
|
/** Derive + publish the MF-Fleet workflow row from a WorkflowRun (doneCount/totalCount from agents, tokens from
|
|
235
285
|
* stats); a terminal workflow LEAVES the fleet (publish terminal then remove), a running one stays/updates. */
|
|
236
286
|
private publishFleet;
|
|
287
|
+
/** [2995] recovery projection seam: the same fleet projection as the put/update observation points, exposed so
|
|
288
|
+
* the recovery sweep can RETIRE a row for a run it just finalized (terminal input ⇒ final frame, then remove).
|
|
289
|
+
* Wire it as `recover`'s `publishTerminalFleetRow` hook — and only ever hand it a TERMINAL run: a `running`
|
|
290
|
+
* row published from outside the wrapper's own write path has no guaranteed retirement (see the invariant on
|
|
291
|
+
* {@link WorkflowNotifyGate.recover}). */
|
|
292
|
+
republishFleet(id: string, run: WorkflowRun): void;
|
|
237
293
|
put(id: string, run: WorkflowRun): Promise<void>;
|
|
238
294
|
update(id: string, scope: string, run: WorkflowRun, expect?: {
|
|
239
295
|
rev: number;
|