@sema-agent/server 7.2.0 → 7.4.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 +2 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +26 -1
- package/dist/approval-ask-machine.d.ts +39 -0
- package/dist/approval-ask-machine.js +101 -0
- package/dist/approval-card.d.ts +244 -0
- package/dist/approval-card.js +237 -0
- package/dist/approval-deny-reasons.d.ts +56 -0
- package/dist/approval-deny-reasons.js +54 -0
- package/dist/approval-reconciler.d.ts +174 -0
- package/dist/approval-reconciler.js +307 -0
- package/dist/boot/coordinators.d.ts +1 -0
- package/dist/boot/coordinators.js +39 -4
- package/dist/boot/deferred-sandbox-path-env.d.ts +99 -0
- package/dist/boot/deferred-sandbox-path-env.js +279 -0
- package/dist/boot/execution-env.js +11 -1
- package/dist/boot/lexical-path-env.d.ts +10 -0
- package/dist/boot/lexical-path-env.js +88 -0
- package/dist/boot/reapers.d.ts +34 -0
- package/dist/boot/reapers.js +198 -23
- package/dist/boot/resolve-spec.js +97 -33
- package/dist/capabilities/center-prompts.js +4 -1
- package/dist/capabilities/oa-tools.d.ts +15 -0
- package/dist/capabilities/oa-tools.js +54 -0
- package/dist/config-types.d.ts +68 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +138 -2
- package/dist/elicitation.d.ts +4 -0
- package/dist/elicitation.js +7 -3
- package/dist/finance/cost-taxonomy.d.ts +34 -0
- package/dist/finance/cost-taxonomy.js +26 -0
- package/dist/hooks/hook-runner.js +32 -0
- package/dist/http/routes/capabilities.js +14 -0
- package/dist/http/routes/diagnostics.d.ts +84 -0
- package/dist/http/routes/diagnostics.js +140 -0
- package/dist/http/routes/runs.d.ts +1 -0
- package/dist/http/routes/runs.js +548 -16
- package/dist/http/routes/tasks.js +175 -12
- package/dist/http/server.d.ts +6 -1
- package/dist/http/server.js +120 -4
- package/dist/http/sse-log.d.ts +51 -0
- package/dist/http/sse-log.js +64 -0
- package/dist/http/wire-types.d.ts +20 -5
- package/dist/main.js +35 -4
- package/dist/observability/fail-open.d.ts +98 -0
- package/dist/observability/fail-open.js +216 -0
- package/dist/observability/prompt-manifest.d.ts +13 -0
- package/dist/observability/prompt-manifest.js +8 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +38 -0
- package/dist/plugins/approval-ask-store-memory.js +299 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +341 -0
- package/dist/plugins/approval-ask-store-sql.js +705 -0
- package/dist/plugins/approval-store-sql.d.ts +116 -0
- package/dist/plugins/approval-store-sql.js +151 -0
- package/dist/plugins/background-agent-store-sql.js +20 -1
- package/dist/plugins/checkpoint-store-sql.d.ts +84 -9
- package/dist/plugins/checkpoint-store-sql.js +297 -16
- package/dist/plugins/file-workflow-journal-store.d.ts +12 -0
- package/dist/plugins/file-workflow-journal-store.js +12 -0
- package/dist/plugins/local-checkpoint-store.d.ts +6 -5
- package/dist/plugins/local-checkpoint-store.js +4 -0
- package/dist/plugins/pg-approval-store.d.ts +9 -0
- package/dist/plugins/pg-approval-store.js +9 -0
- package/dist/plugins/pg-breaker-state.d.ts +8 -0
- package/dist/plugins/pg-breaker-state.js +8 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +10 -0
- package/dist/plugins/pg-checkpoint-store.js +10 -0
- package/dist/plugins/pg-file-snapshot-store.d.ts +8 -0
- package/dist/plugins/pg-file-snapshot-store.js +8 -0
- package/dist/plugins/pg-image-bake.d.ts +12 -0
- package/dist/plugins/pg-image-bake.js +11 -0
- package/dist/plugins/pg-image-index.d.ts +12 -0
- package/dist/plugins/pg-image-index.js +11 -0
- package/dist/plugins/pg-outcome-ledger.d.ts +12 -0
- package/dist/plugins/pg-outcome-ledger.js +11 -0
- package/dist/plugins/pg-pool.js +11 -0
- package/dist/plugins/pg-resume-anchor-store.d.ts +7 -0
- package/dist/plugins/pg-resume-anchor-store.js +7 -0
- package/dist/plugins/pg-run-store.d.ts +9 -0
- package/dist/plugins/pg-run-store.js +9 -0
- package/dist/plugins/pg-session-policy-store.d.ts +7 -0
- package/dist/plugins/pg-session-policy-store.js +7 -0
- package/dist/plugins/pg-session-store.d.ts +12 -0
- package/dist/plugins/pg-session-store.js +12 -0
- package/dist/plugins/pg-tool-result-store.d.ts +9 -0
- package/dist/plugins/pg-tool-result-store.js +9 -0
- package/dist/plugins/pg-workflow-journal-store.d.ts +9 -0
- package/dist/plugins/pg-workflow-journal-store.js +9 -0
- package/dist/plugins/pg-workflow-run-store.d.ts +9 -0
- package/dist/plugins/pg-workflow-run-store.js +9 -0
- package/dist/plugins/store-backend.d.ts +18 -0
- package/dist/plugins/store-backend.js +10 -0
- package/dist/plugins/tidb-approval-store.d.ts +8 -0
- package/dist/plugins/tidb-approval-store.js +8 -0
- package/dist/plugins/tidb-breaker-state.d.ts +7 -0
- package/dist/plugins/tidb-breaker-state.js +7 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +9 -0
- package/dist/plugins/tidb-checkpoint-store.js +9 -0
- package/dist/plugins/tidb-file-snapshot-store.d.ts +8 -0
- package/dist/plugins/tidb-file-snapshot-store.js +8 -0
- package/dist/plugins/tidb-image-bake.d.ts +12 -0
- package/dist/plugins/tidb-image-bake.js +11 -0
- package/dist/plugins/tidb-image-index.d.ts +12 -0
- package/dist/plugins/tidb-image-index.js +11 -0
- package/dist/plugins/tidb-outcome-ledger.d.ts +12 -0
- package/dist/plugins/tidb-outcome-ledger.js +12 -0
- package/dist/plugins/tidb-pool.js +27 -4
- package/dist/plugins/tidb-resume-anchor-store.d.ts +7 -0
- package/dist/plugins/tidb-resume-anchor-store.js +7 -0
- package/dist/plugins/tidb-run-store.d.ts +10 -0
- package/dist/plugins/tidb-run-store.js +9 -0
- package/dist/plugins/tidb-session-policy-store.d.ts +7 -0
- package/dist/plugins/tidb-session-policy-store.js +7 -0
- package/dist/plugins/tidb-tool-result-store.d.ts +8 -0
- package/dist/plugins/tidb-tool-result-store.js +10 -0
- package/dist/plugins/tidb-workflow-journal-store.d.ts +9 -0
- package/dist/plugins/tidb-workflow-journal-store.js +9 -0
- package/dist/plugins/tidb-workflow-run-store.d.ts +10 -0
- package/dist/plugins/tidb-workflow-run-store.js +10 -0
- package/dist/plugins/workflow-journal-limits.d.ts +12 -0
- package/dist/plugins/workflow-journal-limits.js +12 -0
- package/dist/question.d.ts +21 -14
- package/dist/question.js +83 -34
- package/dist/runs.d.ts +16 -1
- package/dist/runs.js +61 -3
- package/dist/runtime-caps-resolver.d.ts +7 -1
- package/dist/runtime-caps-resolver.js +65 -3
- package/dist/sema-registry.d.ts +41 -0
- package/dist/sema-registry.js +40 -0
- package/dist/spec-fields.d.ts +4 -0
- package/dist/spec-fields.js +6 -0
- package/dist/task-settings.d.ts +36 -15
- package/dist/task-settings.js +19 -5
- package/dist/tool-approval.d.ts +296 -3
- package/dist/tool-approval.js +1074 -50
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/ledger-sink.js +14 -1
- package/dist/trace/project.d.ts +90 -0
- package/dist/trace/project.js +188 -0
- package/package.json +5 -4
|
@@ -6,12 +6,16 @@ import { defaultSubagentTailBus, projectTailFrame } from "../../fleet/subagent-t
|
|
|
6
6
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
|
|
7
7
|
import { createLedgerSink } from "../../trace/ledger-sink.js";
|
|
8
8
|
import { redactSecrets } from "../../trace/redact.js";
|
|
9
|
-
import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, appendModelUsageDelta, attachModelUsage } from "../../trace/project.js";
|
|
9
|
+
import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, wiringManifestOperatorEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage } from "../../trace/project.js";
|
|
10
10
|
import { cascadeConfig, runMeta } from "../run-meta.js";
|
|
11
11
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
12
12
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
13
|
+
import { collectSsePreamble } from "../sse-log.js";
|
|
14
|
+
import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
|
|
15
|
+
import { createApprovalCardEmitter, resolveApprovalLeg, resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
13
16
|
import { buildActiveRunConflict, toDoneFrameResult } from "../active-run-conflict.js";
|
|
14
|
-
import { headerStr, gatedPrincipal } from "../principal-gate.js";
|
|
17
|
+
import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
18
|
+
import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/fail-open.js";
|
|
15
19
|
/** #135([C95]/[C96]):live 腿 SSE 数据帧的**唯一**出口 —— 载荷带账本可锚 `eventId`(非空 string)的帧
|
|
16
20
|
* 前缀 `id: <eventId>` 行(SSE 游标=账本 eventId,壳/SDK 拿它锚进账本或断线换道 events tail 续读);
|
|
17
21
|
* 无 eventId 的帧**不发行**,EventSource 沿用上一游标 —— 绝不合成占位 id(那会把不可锚的位置伪装成可锚)。
|
|
@@ -196,6 +200,54 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
196
200
|
if (!res.writableEnded)
|
|
197
201
|
onDisconnect();
|
|
198
202
|
});
|
|
203
|
+
// #154 件一:本连接的 `wiring_manifest` 帧投哪一形。判据 = `explicitOperatorOk`(**不是** `isOperator`:
|
|
204
|
+
// 空 `OPERATOR_PRINCIPALS` 必须是「没有人是 operator」;把它读成「所有人都是」会让未配名单的部署把
|
|
205
|
+
// 治理面发给每一个订阅者)。**每连接求一次**、不逐帧求:身份在一条连接内不会变,而逐帧调用会让
|
|
206
|
+
// 每帧都重跑一遍凭证解析 —— 更重要的是,身份只在这一处判,帧循环里没有第二个可漂的判据。
|
|
207
|
+
const wiringOperatorFace = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
|
|
208
|
+
// #151 车3 §5.1:sync 腿的**开流重放**——`sseHeaders` + meta 帧之后、`streamBody` 之前,把同一
|
|
209
|
+
// (owner, session) 的未决审批卡投成 `approval_request` preamble 帧。
|
|
210
|
+
//
|
|
211
|
+
// 🔴 位置:必须排在**上面那套断连接线之后**(codex 复审 2026-08-06 的 high)。Node 的 `'close'`
|
|
212
|
+
// 只发一次 —— preamble 的 ≤2s 等待窗若插在接线之前,窗内断连的 close 事件在监听器注册前就烧掉了,
|
|
213
|
+
// `closed` 恒 false、`ac` 从不 abort ⇒ 模型腿在一条已断的连接上照常起跑并烧 token,「断连即杀 run」
|
|
214
|
+
// 这条既有契约在窗口内失效。排在接线之后 = 窗内断连照常 abort,腿拿到的是已 abort 的 signal
|
|
215
|
+
// (与「断连发生在起跑之后」同一条既有路径,不新增早退分支)。
|
|
216
|
+
//
|
|
217
|
+
// 🔴 为什么按 **session** 读而不是按 taskId:sync 腿每次开流都是**新 taskId**;它要接的未决卡来自
|
|
218
|
+
// 「同一 (owner, session) 的上一条腿 / 长命 bg 子代」——正是 `boundAsk` broker 的
|
|
219
|
+
// `["session", owner, sessionId]` 键域。按 taskId 读永远读不到它们。
|
|
220
|
+
//
|
|
221
|
+
// `sessionId` 缺席的 adhoc 流**不重放**:那条腿的行上 task_id/session_id 都是客户端永远看不到的
|
|
222
|
+
// 一次性 uuid,没有「重连回同一条腿」的概念(§5.1 表:这不是缺陷,是该腿形本来的语义)。
|
|
223
|
+
//
|
|
224
|
+
// owner = **verified** principal(与下方 `askOwner`、与 respond 的 owner gate 同一姿势)——重放面是
|
|
225
|
+
// 跨腿投递,租户门必须带上,不能让 A 租户的开流看到 B 租户的卡。
|
|
226
|
+
// 有界(2s)+ fail-open:store 抛或挂起都只记一次 warn、零卡继续开流(§5.3)。
|
|
227
|
+
// 🔴 开关谓词与 runs.ts 同源同理由(codex 复审第二轮 high:回滚下表里可能有残留 STREAM_PENDING
|
|
228
|
+
// 行,无谓词会破「默认关 = 逐字零变化」;顺带省掉关闭态每次开流的一次 store 往返)。钉:场景9。
|
|
229
|
+
const replaySessionId = deps.config.streamApproval.enabled ? prepared.spec.sessionId : undefined;
|
|
230
|
+
if (replaySessionId) {
|
|
231
|
+
const replayOwner = gatedPrincipal(req, deps.config) ?? null;
|
|
232
|
+
const preambleFrames = await collectSsePreamble(async (signal) => {
|
|
233
|
+
const store = deps.backend?.approvalAsk();
|
|
234
|
+
if (!store)
|
|
235
|
+
return [];
|
|
236
|
+
const rows = await store.listPendingBySession(replaySessionId, replayOwner, signal);
|
|
237
|
+
const { frames, skipped, dropped } = buildApprovalPreamble(rows, Date.now(), deps.config.streamApproval.replayMax);
|
|
238
|
+
if (skipped > 0)
|
|
239
|
+
deps.logger?.warn?.("approval_preamble_row_skipped", { sessionId: replaySessionId, skipped, face: "tasks.stream" });
|
|
240
|
+
if (dropped > 0)
|
|
241
|
+
deps.logger?.warn?.("approval_preamble_capped", { sessionId: replaySessionId, dropped, replayMax: deps.config.streamApproval.replayMax, face: "tasks.stream" });
|
|
242
|
+
return buildApprovalPreambleSseFrames(frames);
|
|
243
|
+
}, (reason, err) => deps.logger?.warn?.("approval_preamble_failed", { sessionId: replaySessionId, reason, face: "tasks.stream", error: err instanceof Error ? err.message : String(err ?? "") }));
|
|
244
|
+
// 具名事件、**无 `id:` 行**:preamble 不是账本行,不参与 live 腿的 eventId 游标(sseData 的契约)。
|
|
245
|
+
// 等待窗里断连了就别再写(与心跳的 `!res.writableEnded && !res.destroyed` 同一判据)。
|
|
246
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
247
|
+
for (const f of preambleFrames)
|
|
248
|
+
res.write(`event: ${f.event}\ndata: ${JSON.stringify(f.data)}\n\n`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
199
251
|
// Heartbeat so a long no-token gap (e.g. a debate phase) doesn't trip a mesh/ingress idle timeout on
|
|
200
252
|
// the relay (center). SSE comment lines are ignored by EventSource but keep the socket alive.
|
|
201
253
|
const hb = setInterval(() => {
|
|
@@ -704,6 +756,25 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
704
756
|
// 与两条 durable 腿同源(它曾是唯一不走 builder 的内容帧=三方普查共同盲区)。
|
|
705
757
|
sseData(res, { type: "compaction_outcome", ...compactionOutcomeEventData(ev) });
|
|
706
758
|
}
|
|
759
|
+
else if (ev.type === "wiring_manifest") {
|
|
760
|
+
// 🔴 core 5.14.0 design/173 新臂。`manifest.governance` 盖着 `audience:"operator"`,core 把
|
|
761
|
+
// 失败方向写死成「no projection ⇒ do not disclose」——所以默认(租户)形整段剥除,含**对该段
|
|
762
|
+
// 取的哈希** `configFingerprint`(无盐 + 4 布尔 = 16 组合谕示器)。
|
|
763
|
+
// #154 件一:分叉只在这条 live 腿(**唯一知道调用方身份的腿**),两条 durable 腿恒存剥后形
|
|
764
|
+
// ——账本写时不知道未来读者是谁,存全量 = 把 operator-only 面写进租户可读介质。
|
|
765
|
+
sseData(res, {
|
|
766
|
+
type: "wiring_manifest",
|
|
767
|
+
...(wiringOperatorFace
|
|
768
|
+
? wiringManifestOperatorEventData(ev)
|
|
769
|
+
: wiringManifestEventData(ev)),
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
else if (ev.type === "human_input") {
|
|
773
|
+
// core 5.14.0 design/171 新臂:谁把什么喂进了这条 run(本帧不带正文,只带载体/署名)。
|
|
774
|
+
// 同上——离开裸 catch-all:`issuer`/`actor.*` 是宿主派生的身份串(自由文本形)⇒ 构造器
|
|
775
|
+
// 内过 redactSecrets;`principal` 刻意不投影(帧本就走 owner-scoped 流)。
|
|
776
|
+
sseData(res, { type: "human_input", ...humanInputEventData(ev) });
|
|
777
|
+
}
|
|
707
778
|
else if (ev.type === "text_delta" || ev.type === "turn_end" || ev.type === "message_committed" || ev.type === "context_usage") {
|
|
708
779
|
// 裸 catch-all 白名单化收官(probe-wire-1 表征钉的根治半场):最后四个已知臂离开
|
|
709
780
|
// `JSON.stringify(ev)`。**零字节变化**白名单——已知键(含 E2 四 identity 键:live 现状
|
|
@@ -743,8 +814,18 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
743
814
|
const askTaskId = durableTaskId ?? uuidv7();
|
|
744
815
|
const askOwner = gatedPrincipal(req, deps.config) ?? null;
|
|
745
816
|
const emitAsk = (frame) => {
|
|
746
|
-
|
|
747
|
-
|
|
817
|
+
// #157(codex R2 复审 HIGH):流已断时这里是**静默 no-op** —— 上游 coordinator 的 try/catch
|
|
818
|
+
// 因此拿不到任何信号,一次断连就被记成"投递成功",问题挂到 TTL 才按无人应答结算。方向本批
|
|
819
|
+
// 不动(改成 throw 是既有的刻意决定,见下面 drain 包装处的注释),但**必须留痕**:否则
|
|
820
|
+
// `open-frame-undelivered` 只覆盖"持久化抛错",覆盖不到真正常见的那条路。
|
|
821
|
+
// `destroyed` 与 `writableEnded` 一起判——emitApproval 用的就是这对谓词。
|
|
822
|
+
// 分类按帧型走(`failOpenTagForDroppedFrame`):这条闭包同时驮着 open 帧、完成面包屑与
|
|
823
|
+
// 其它运行流帧,一刀切会让**一次**断连把保护型计数记两次,真 open 失败率被面包屑盖住。
|
|
824
|
+
if (res.writableEnded || res.destroyed) {
|
|
825
|
+
recordFailOpen(failOpenTagForDroppedFrame(frame.type), `frame=${frame.type}`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
res.write(`event: ${frame.type}\ndata: ${JSON.stringify(frame)}\n\n`);
|
|
748
829
|
};
|
|
749
830
|
// P1 ①②: this new turn is a stream-open for the session — push any async-workflow completions that
|
|
750
831
|
// finished since (frames ride the SAME connection, before the run's own events; the shell dedups by
|
|
@@ -800,22 +881,104 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
800
881
|
throw new Error("live stream ended — approval card undeliverable");
|
|
801
882
|
emitAsk(frame);
|
|
802
883
|
};
|
|
884
|
+
// ── #151 车3 刀 3b:sync 腿的流内审批协议装配 ────────────────────────────────────────────
|
|
885
|
+
// 上场判据走**单一谓词**(与协调器注入 / `/v1/capabilities` / 回决端点 501 同一个符号)。
|
|
886
|
+
// 关着 ⇒ 下面三样(呈卡口 / 腿轴真值 / windowZero 分支)全部不接,本腿字节逐字不变(A-1)。
|
|
887
|
+
const streamApprovalOn = resolveStreamApprovalGate({
|
|
888
|
+
toolApprovalEnabled: Boolean(deps.toolApproval),
|
|
889
|
+
streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
|
|
890
|
+
backend: deps.backend,
|
|
891
|
+
parkFacility: deps.checkpointStore !== undefined,
|
|
892
|
+
}).active;
|
|
893
|
+
const legWalltimeMs = typeof prepared.spec.limits?.maxWalltimeMs === "number" ? prepared.spec.limits.maxWalltimeMs : undefined;
|
|
894
|
+
// §7.3 + §8.3:本腿的「上不上场 / 窗是不是 0 / leg deadline 真值」走**三腿共用**的那一份判据
|
|
895
|
+
// (`resolveApprovalLeg` 顶注记着为什么必须是一个函数:第一版只写在本腿上,bg/resume 两腿因此
|
|
896
|
+
// 在开关关时行为变了、窗=0 也不生效)。取值点 = **本腿开始执行的时刻**(这里就是 `streamBody`
|
|
897
|
+
// 进入前的最后一站),误差方向偏短=安全侧。
|
|
898
|
+
const approvalLeg = resolveApprovalLeg({
|
|
899
|
+
streamApprovalOn,
|
|
900
|
+
windowMs: deps.config.streamApproval?.windowMs ?? 0,
|
|
901
|
+
windowMarginMs: deps.config.streamAskWindowMarginMs,
|
|
902
|
+
...(legWalltimeMs !== undefined ? { legWalltimeMs } : {}),
|
|
903
|
+
nowMonotonicMs: performance.now(),
|
|
904
|
+
});
|
|
905
|
+
const legDeadlineMonotonic = approvalLeg.legDeadlineMonotonic;
|
|
906
|
+
// 🔴 ①`forceDurableGate` principal 这一源**本装配点读不到**(`runtimeCapsResolver` 是 core 在
|
|
907
|
+
// prepare 内部按 principal 异步解析的 deps 级 seam)。按 §14 §12-5 亲裁:不可读则该源不做 ——
|
|
908
|
+
// core 自己兜得住(`prepare-task.js:3330` 的 `runtimeCaps?.forceDurableGate !== true` 让
|
|
909
|
+
// suspendAsk 不退位 ⇒ 结构性 park)。代价只是这类 principal 可能先收到一张必被 park 的卡,
|
|
910
|
+
// 已写进 wire 契约文档。
|
|
911
|
+
const windowZero = approvalLeg.windowZero;
|
|
912
|
+
// §4.3(a)+(c):呈卡帧的投递口 —— live SSE **与** detach 车道的 durable 账本**双写**。
|
|
913
|
+
// 🔴 codex round2 R2-1 修:两个 sink 的成败判定收进**唯一属主** `createApprovalCardEmitter`
|
|
914
|
+
// (顶注写着为什么「两路都没接下就必须抛」——静默成功会压住 park 路由,把一只没人能回答的 ask
|
|
915
|
+
// 挂到窗到期)。本处只负责把两个 sink 交出去;`writeLive` 在流已结束时**缺席**(不是 no-op),
|
|
916
|
+
// 于是「socket 已死 ∧ 账本写失败」如实成为一次投递失败。
|
|
917
|
+
const emitCard = createApprovalCardEmitter({
|
|
918
|
+
...(ledgerSink
|
|
919
|
+
? {
|
|
920
|
+
appendDurable: async (frame) => {
|
|
921
|
+
const { type, ...rest } = frame;
|
|
922
|
+
await ledgerSink.append(type, rest);
|
|
923
|
+
},
|
|
924
|
+
}
|
|
925
|
+
: {}),
|
|
926
|
+
writeLive: (frame) => {
|
|
927
|
+
if (res.writableEnded || res.destroyed)
|
|
928
|
+
throw new Error("live stream ended");
|
|
929
|
+
emitAsk(frame);
|
|
930
|
+
},
|
|
931
|
+
});
|
|
932
|
+
// 车6 撤卡帧:**live only**(不进 durable tail —— 收敛器/reaper 那类调用点没有 seq 可分配,
|
|
933
|
+
// 硬塞进账本会铸一条序号伪造的行;丢帧的结构补偿恒是重连 preamble 的全量对账基准)。
|
|
934
|
+
const emitRevokeLive = (frame) => {
|
|
935
|
+
if (!res.writableEnded && !res.destroyed)
|
|
936
|
+
emitAsk(frame);
|
|
937
|
+
};
|
|
803
938
|
// [1535] server 半场([1539] 定谳断点①的修):同一 ctx 双装——ALS 包裹(宿主自身 ask 的
|
|
804
939
|
// 既有路)+ **spec.onAsk = boundAsk(ctx)**(core 1.294 继承链 parentConstraints 冻结
|
|
805
940
|
// `spec.onAsk ?? deps.onAsk` 逐 ancestor 传给每个委派子代:bg 子代 lane 无 ALS ctx 时不再
|
|
806
941
|
// 「unavailable/零帧 fail-closed deny」,权限卡带 sourceTaskId 浮到宿主本流)。suspendAsk
|
|
807
942
|
// 退位条件的「onAsk 在场」真值不变(deps.onAsk 恒 wire),durable 部署行为零漂移。
|
|
808
|
-
|
|
809
|
-
|
|
943
|
+
// #151 车3 刀 3b:`windowZero` ⇒ **不包 ALS**(ALS 腿一并让位,`withApproval` 退化成
|
|
944
|
+
// `withQuestion`),且下面的 `spec.onAsk` 换成 immediate-unavailable 闭包 —— 两件必须同时做,
|
|
945
|
+
// 只做一件的话另一条路仍会呈卡(ALS 腿是 `deps.onAsk` 的到达路)。
|
|
946
|
+
const approvalCtx = deps.toolApproval && !windowZero
|
|
947
|
+
? {
|
|
948
|
+
taskId: askTaskId,
|
|
949
|
+
owner: askOwner,
|
|
950
|
+
emit: emitApproval,
|
|
951
|
+
abortSignal: ac.signal,
|
|
952
|
+
...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}),
|
|
953
|
+
// 协议上场才接新协议族的两个口与两条腿轴;关着时逐字保持既有四键形。
|
|
954
|
+
...(streamApprovalOn
|
|
955
|
+
? {
|
|
956
|
+
emitCard,
|
|
957
|
+
emitRevoke: emitRevokeLive,
|
|
958
|
+
// sync 腿恒是**首腿**:它不经 checkpoint token 复活(那条是 resume 腿的形),
|
|
959
|
+
// 空串是首腿的**真值**而不是「未知」(见 ToolApprovalRunContext.legKey 顶注)。
|
|
960
|
+
legKey: "",
|
|
961
|
+
...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}),
|
|
962
|
+
}
|
|
963
|
+
: {}),
|
|
964
|
+
}
|
|
810
965
|
: undefined;
|
|
811
|
-
if (
|
|
966
|
+
if (deps.toolApproval) {
|
|
812
967
|
// [1546] HIGH-1:闭包只携身份——emit 经 broker 取「当前」活跃流(宿主重连的新 SSE 在
|
|
813
968
|
// runWithContext 注册时覆盖指针),internalsSnapshot 冻结的闭包不再携死 response/死 signal。
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
969
|
+
// #151 车3 刀 3b(§8.3 ⑦):`windowZero` ⇒ 换成**立即** `"unavailable"` 的闭包。为什么不是
|
|
970
|
+
// 「不装 onAsk」:`deps.onAsk` 恒 wire(boot/runner-deps.ts)且 core 取 `spec.onAsk ?? deps.onAsk`,
|
|
971
|
+
// per-task 的「缺席」结构上不可达;而 core 的 `approverUnavailable === true` 分支
|
|
972
|
+
// (prepare-task.js:3328-3335)**直接绕过** onAsk 在场判定去铸 checkpoint —— 与「onAsk 缺席」
|
|
973
|
+
// 在 park 结局上逐字等价,且可 per-task 表达。
|
|
974
|
+
prepared.spec.onAsk = windowZero
|
|
975
|
+
? () => Promise.resolve("unavailable")
|
|
976
|
+
: deps.toolApproval.boundAsk({
|
|
977
|
+
owner: askOwner,
|
|
978
|
+
taskId: askTaskId,
|
|
979
|
+
...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}),
|
|
980
|
+
...(streamApprovalOn ? { legKey: "", ...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}) } : {}),
|
|
981
|
+
});
|
|
819
982
|
}
|
|
820
983
|
const withApproval = () => approvalCtx && deps.toolApproval ? deps.toolApproval.runWithContext(approvalCtx, withQuestion) : withQuestion();
|
|
821
984
|
// SendUserFile:sync 腿 file_link 走 live out-of-band 帧面(emitAsk)。detach 车道([1840]§五
|
package/dist/http/server.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { FleetLeaseManager } from "../fleet-lease.js";
|
|
|
16
16
|
import { type FleetEventBus } from "../fleet/fleet-bus.js";
|
|
17
17
|
import type { ElicitationCoordinator } from "../elicitation.js";
|
|
18
18
|
import type { QuestionCoordinator } from "../question.js";
|
|
19
|
-
import type
|
|
19
|
+
import { type ToolApprovalCoordinator } from "../tool-approval.js";
|
|
20
20
|
import type { PlanCacheProbe } from "../plan-cache-probe.js";
|
|
21
21
|
import type { Logger } from "../observability/logger.js";
|
|
22
22
|
import type { Metrics } from "../observability/metrics.js";
|
|
@@ -413,6 +413,11 @@ export interface ServiceDeploymentDeps {
|
|
|
413
413
|
ageMs: number;
|
|
414
414
|
error?: string;
|
|
415
415
|
} | undefined;
|
|
416
|
+
/** #154 件三:core `describeStaticWiring(deps, specTemplate)` 的 boot 产物 —— `GET /v1/diagnostics/wiring`
|
|
417
|
+
* 的 `static` 半场,**与启动期拒启自检读的是同一份**(诊断页与拒启判据结构性同源)。
|
|
418
|
+
* operator-only 面,故整份原样下发(含 `governance` 段:本面就是那个段的受众)。
|
|
419
|
+
* 缺席 = 这个进程不是由 composition root 装起来的(测试夹具形)⇒ 该路由诚实地不存在(404)。 */
|
|
420
|
+
staticWiring?: import("@sema-agent/core").WiringManifest;
|
|
416
421
|
/** ① core ruling — SPLIT: `capabilities.workflows` = the ENGINE-CAN axis, boot-computed from core's own
|
|
417
422
|
* `workflowsCapability(deps)` (hardened script runner ∧ governance), NOT the `Boolean(workflowRunStore)` store
|
|
418
423
|
* proxy. Orthogonal to `workflowsList` (the durable-list axis = `workflowRunStore`). Falls back to the store
|
package/dist/http/server.js
CHANGED
|
@@ -18,9 +18,10 @@ import {} from "../orchestration/workflow-agent-steer.js";
|
|
|
18
18
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
|
|
19
19
|
import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
|
|
20
20
|
import { defaultSubagentTailBus, projectTailFrame } from "../fleet/subagent-tail-bus.js";
|
|
21
|
+
import { createApprovalCardEmitter, resolveApprovalLeg, resolveStreamApprovalGate } from "../tool-approval.js";
|
|
21
22
|
import {} from "../observability/rate-limit.js";
|
|
22
23
|
import { withPrincipal } from "../observability/principal-context.js";
|
|
23
|
-
import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
|
|
24
|
+
import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, humanInputEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
|
|
24
25
|
import { redactSecrets } from "../trace/redact.js";
|
|
25
26
|
import { IdempotencyCache, scopedIdempotencyKey } from "./idempotency.js";
|
|
26
27
|
export { scopedIdempotencyKey };
|
|
@@ -28,6 +29,7 @@ import { streamSseLog } from "./sse-log.js";
|
|
|
28
29
|
import { clampVerifyRounds, verifyRoundsFromBody } from "./verify-rounds.js"; // design/97 R9: the shape's neutral leaf (route-ctx.ts type-imports VerifyRoundsSpec from here, not from this file)
|
|
29
30
|
import { handleCapabilities } from "./routes/capabilities.js";
|
|
30
31
|
import { handleObservability } from "./routes/observability.js";
|
|
32
|
+
import { handleDiagnostics } from "./routes/diagnostics.js";
|
|
31
33
|
import { handleMemoryPolicy } from "./routes/memory-policy.js";
|
|
32
34
|
import { handleSessionsList } from "./routes/sessions-list.js";
|
|
33
35
|
import { handleSessions, createSessionsLocal } from "./routes/sessions.js";
|
|
@@ -270,7 +272,10 @@ export function createHttpServer(rawDeps) {
|
|
|
270
272
|
return await cs.setPendingSteer(token, scope, { text, trusted: false });
|
|
271
273
|
}
|
|
272
274
|
catch {
|
|
273
|
-
|
|
275
|
+
// park 面任何失败=诚实 false(调用方按 dropped 记账),绝不 throw 回 hook 收尾路径。
|
|
276
|
+
// core 5.14.0(#147)新增的 `steering.queue_full` 也落这里:队列满 ⇒ 这条 hook 输出确实没投出去,
|
|
277
|
+
// `false` 就是它的真实结局。**不给它 wire 码**——本通道是进程内 hook 收尾,没有 HTTP 应答面。
|
|
278
|
+
return false;
|
|
274
279
|
}
|
|
275
280
|
};
|
|
276
281
|
}
|
|
@@ -587,6 +592,9 @@ export function createHttpServer(rawDeps) {
|
|
|
587
592
|
// design/158 A9:观测只读面(routes/observability.ts):/v1/outcomes + /v1/usage。
|
|
588
593
|
if (await handleObservability(req, res, url, ctx))
|
|
589
594
|
return;
|
|
595
|
+
// #154 件三:装配自证只读面(routes/diagnostics.ts):GET /v1/diagnostics/wiring(operator-only)。
|
|
596
|
+
if (await handleDiagnostics(req, res, url, ctx))
|
|
597
|
+
return;
|
|
590
598
|
// design/158 A9:memory 导出/同步 + /v1/policy 只读面(routes/memory-policy.ts)。
|
|
591
599
|
if (await handleMemoryPolicy(req, res, url, ctx))
|
|
592
600
|
return;
|
|
@@ -733,6 +741,13 @@ export function createHttpServer(rawDeps) {
|
|
|
733
741
|
return null;
|
|
734
742
|
}
|
|
735
743
|
const perms = st.permissions;
|
|
744
|
+
// F1(#157 复审车B 留裁,裁修 2026-08-05):出现但形错(数组/标量)⇒ 400,与 settings 自身门(上方
|
|
745
|
+
// :1097)同姿势——此前数组形被本门与 #157-③ 枚举门的 `!Array.isArray` 双双跳过、parse 层再静默
|
|
746
|
+
// drop = 保护型配置(deny 规则)端到端无效且无声。null 保持「容器缺席」不咬(既有钉)。
|
|
747
|
+
if (perms !== undefined && perms !== null && (typeof perms !== "object" || Array.isArray(perms))) {
|
|
748
|
+
sendError(res, 400, "request.field_invalid", "settings.permissions must be an object");
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
736
751
|
if (perms !== null && typeof perms === "object" && !Array.isArray(perms)) {
|
|
737
752
|
for (const k of ["allow", "deny", "ask"]) {
|
|
738
753
|
const arr = perms[k];
|
|
@@ -1065,6 +1080,44 @@ export function createHttpServer(rawDeps) {
|
|
|
1065
1080
|
return null;
|
|
1066
1081
|
}
|
|
1067
1082
|
}
|
|
1083
|
+
// [2856]② toolMaterializeStrategy:枚举 fail-loud(邻居 promptProfile 同款,同一条理由——静默折缺省
|
|
1084
|
+
// 会让调用方以为切了策略却没切,而这个键的整个存在理由就是 #181 那条「缺省下无界循环」)。
|
|
1085
|
+
{
|
|
1086
|
+
const v = body.toolMaterializeStrategy;
|
|
1087
|
+
if (v !== undefined && v !== "static" && v !== "swap") {
|
|
1088
|
+
sendError(res, 400, "request.field_invalid", 'toolMaterializeStrategy must be "static" or "swap"');
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
// #157-② permissionMode:枚举 fail-loud(邻居 promptProfile 同款)。此前开放 string + coercePermissionMode
|
|
1093
|
+
// 未知词静默折 "default"(最爱问档)——方向对(收紧)但静默:拼错(大小写 "Plan")的调用方以为进了 plan
|
|
1094
|
+
// 模式,实拿恒询问,silent-drop 病灶同族([2759] cli settings.json 同源病)。RESUME 重放持久化 body 不过
|
|
1095
|
+
// 此门(coercePermissionMode 宽容层护存量,双层与 [854]/outputStyle 同姿势);bundle 的
|
|
1096
|
+
// settings.permissions.defaultMode drop 臂本批不动(候 [2759] 跨仓对表统一裁,task-settings.ts:216 注)。
|
|
1097
|
+
{
|
|
1098
|
+
const v = body.permissionMode;
|
|
1099
|
+
if (v !== undefined && v !== "default" && v !== "acceptEdits" && v !== "plan" && v !== "bypassPermissions" && v !== "auto") {
|
|
1100
|
+
sendError(res, 400, "request.field_invalid", 'permissionMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
// #157-③([2766] cli 对表裁决):bundle 的 settings.permissions.defaultMode 与顶层同形 400 —— 经壳的
|
|
1105
|
+
// 流量上 wire 前已过闭集校验(壳侧对人手编辑面走「降级+响亮」b979f74),server 收到未知词只可能来自
|
|
1106
|
+
// 旧版壳或第三方直连=程序化入口,严格拒合理。parseTaskSettings 的 drop 臂保留护 RESUME 存量
|
|
1107
|
+
// (task-settings.ts:216 双层,同 permissionMode/outputStyle 姿势)。
|
|
1108
|
+
{
|
|
1109
|
+
const s = body.settings;
|
|
1110
|
+
if (s !== null && typeof s === "object" && !Array.isArray(s)) {
|
|
1111
|
+
const p = s.permissions;
|
|
1112
|
+
if (p !== null && typeof p === "object" && !Array.isArray(p)) {
|
|
1113
|
+
const m = p.defaultMode;
|
|
1114
|
+
if (m !== undefined && m !== "default" && m !== "acceptEdits" && m !== "plan" && m !== "bypassPermissions" && m !== "auto") {
|
|
1115
|
+
sendError(res, 400, "request.field_invalid", 'settings.permissions.defaultMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1068
1121
|
// [865]② fresh-submit 未知模型 400 fail-loud(clay 生产实锤:未知 body.model 静默落 default = 用户不知情
|
|
1069
1122
|
// 换模型)。🔴 门目录必须与 resolveSpec 同源 = expandTiers 增广视图(codex H1:裸 config.models 会把合法
|
|
1070
1123
|
// 档位词 "pro"/CC alias "sonnet" 当未知 400 误杀——SDK 契约明确允许它们;增广后 matchCatalogModel 对
|
|
@@ -1467,6 +1520,32 @@ export function createHttpServer(rawDeps) {
|
|
|
1467
1520
|
*/
|
|
1468
1521
|
async function driveResumeIntoRunLog(args) {
|
|
1469
1522
|
const { token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds } = args;
|
|
1523
|
+
// ── #151 车3 刀 3b:resume 腿的 `legKey` 真值(设计稿 §3.2′)──────────────────────────────────
|
|
1524
|
+
// 一次 park→resume 的 checkpoint token **就是**这条腿的天然身份:同一 token 重投 = 同一条腿(幂等,
|
|
1525
|
+
// askId 不变);新 park ⇒ 新 token ⇒ 新腿 ⇒ 新 askId ⇒ **重新征询**(§3.3「resume 只重放未完成兄弟」)。
|
|
1526
|
+
// 🔴 存**摘要**不存原文:token 是能力凭据(本仓有 `stripCheckpointToken` 专门把它从可重放账本/结果里
|
|
1527
|
+
// 剥掉),而 `askId` 是 wire 可见值 —— 摘要单向,原始 token 不进任何行、不进任何帧。
|
|
1528
|
+
// 入口算一次(这里就是唯一持有 `args.token` 的地方),下面随 ctx 走。
|
|
1529
|
+
const legKey = createHash("sha256").update(String(token)).digest("hex");
|
|
1530
|
+
// §7.3:本 leg 的 walltime deadline —— 取值点 = 本腿开始执行之前(`resumeStream` 调用在下方)。
|
|
1531
|
+
// 与 sync/bg 两腿同一条判据:只在协议上场时供值,误差方向偏短(安全侧),理由见 routes/tasks.ts 同处注。
|
|
1532
|
+
const streamApprovalOn = resolveStreamApprovalGate({
|
|
1533
|
+
toolApprovalEnabled: Boolean(deps.toolApproval),
|
|
1534
|
+
streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
|
|
1535
|
+
backend: deps.backend,
|
|
1536
|
+
parkFacility: deps.checkpointStore !== undefined,
|
|
1537
|
+
}).active;
|
|
1538
|
+
// 🔴 codex 交叉复审 F4(2026-08-06 真 finding)的修:本腿此前**无条件**包 approval ALS(只把新协议的
|
|
1539
|
+
// 口与轴挂在 `streamApprovalOn` 后面)⇒ ①协议关时本腿凭空多了一条活卡投递路(此前恒 park,A-1 破)、
|
|
1540
|
+
// ②窗=0 在本腿上不生效。裁定走**三腿共用**的 `resolveApprovalLeg`,一份判据三处消费。
|
|
1541
|
+
const approvalLeg = resolveApprovalLeg({
|
|
1542
|
+
streamApprovalOn,
|
|
1543
|
+
windowMs: deps.config.streamApproval?.windowMs ?? 0,
|
|
1544
|
+
windowMarginMs: deps.config.streamAskWindowMarginMs,
|
|
1545
|
+
...(typeof taskConfig.limits?.maxWalltimeMs === "number" ? { legWalltimeMs: taskConfig.limits.maxWalltimeMs } : {}),
|
|
1546
|
+
nowMonotonicMs: performance.now(),
|
|
1547
|
+
});
|
|
1548
|
+
const legDeadlineMonotonic = approvalLeg.legDeadlineMonotonic;
|
|
1470
1549
|
// 🔴 复审 C2:lease admission for EVERY resume family (/decide, /answer, /plan_review, preempt-resume)
|
|
1471
1550
|
// funnels through here — keyed on `principal` = the CHECKPOINT OWNER, the SAME identity the resumed model spend
|
|
1472
1551
|
// is billed to via `withPrincipal(principal)` below. The route guards deliberately do NOT lease-gate (they'd
|
|
@@ -1870,6 +1949,14 @@ export function createHttpServer(rawDeps) {
|
|
|
1870
1949
|
await flush();
|
|
1871
1950
|
await append("workspace_changed", workspaceChangedEventData(ev));
|
|
1872
1951
|
break; // [1559]二: cwd move (bash cd/EnterWorktree) — shared whitelist builder, §E1 redact — resume twin of the sync + ledger-sink legs
|
|
1952
|
+
case "wiring_manifest":
|
|
1953
|
+
await flush();
|
|
1954
|
+
await append("wiring_manifest", wiringManifestEventData(ev));
|
|
1955
|
+
break; // 🔴 core 5.14.0 design/173:resume 腿自证清单(每条腿发自己那份)。**同一个**构造器剥 `governance` 段(audience:"operator",no projection ⇒ do not disclose)——三腿同源,分头挑键即下一次泄露
|
|
1956
|
+
case "human_input":
|
|
1957
|
+
await flush();
|
|
1958
|
+
await append("human_input", humanInputEventData(ev));
|
|
1959
|
+
break; // core 5.14.0 design/171:人类输入生命周期账本(parked-steer 续跑帧正是在这条腿上出现);身份串脱敏在共享构造器里
|
|
1873
1960
|
case "message_committed":
|
|
1874
1961
|
await anchor.onMessageCommitted(ev.role, ev.entryId);
|
|
1875
1962
|
break; // R8: USER-message rewind anchor (CC "rewind to the prompt")
|
|
@@ -1952,9 +2039,33 @@ export function createHttpServer(rawDeps) {
|
|
|
1952
2039
|
}
|
|
1953
2040
|
return safe;
|
|
1954
2041
|
};
|
|
2042
|
+
// #151 车3 刀 3b(design/172 §4.3(b)):resume 腿的**审批 ALS** —— 此前这条腿只有 elicitation ctx,
|
|
2043
|
+
// 审批 ask 恒 `"unavailable"` ⇒ 恒 park,一张卡都不产。投递面 = 本腿的 durable events tail
|
|
2044
|
+
// (与 bg 腿同形:`append(type, rest)`,`emitCard` 同口;撤卡帧 live-only 故不接)。
|
|
2045
|
+
// `legKey` = 上面算好的 token 摘要 —— 这是 resume 腿与 sync/bg 首腿的唯一区别,也正是「新 park ⇒
|
|
2046
|
+
// 新腿 ⇒ 重新征询」这条安全语义的承载轴。
|
|
2047
|
+
const approvalEmit = (frame) => {
|
|
2048
|
+
const { type, ...rest } = frame;
|
|
2049
|
+
return append(type, rest);
|
|
2050
|
+
};
|
|
2051
|
+
// 包 ALS ⟺ 协议在本腿上场且窗不为 0(F4 修;`windowZero` 在没有 per-task `spec.onAsk` 装配点的
|
|
2052
|
+
// 腿上,「不包 ALS」就是 immediate-unavailable 闭包的等价形,见 resolveApprovalLeg 顶注)。
|
|
2053
|
+
const withApproval = () => deps.toolApproval && taskId && approvalLeg.active && !approvalLeg.windowZero
|
|
2054
|
+
? deps.toolApproval.runWithContext({
|
|
2055
|
+
taskId,
|
|
2056
|
+
owner: principal ?? null,
|
|
2057
|
+
emit: approvalEmit,
|
|
2058
|
+
abortSignal: cancelCtrl.signal,
|
|
2059
|
+
sessionId,
|
|
2060
|
+
// 同 bg 腿:唯一 sink 是本腿的 durable tail,append 失败如实算未送达(R2-1)。
|
|
2061
|
+
emitCard: createApprovalCardEmitter({ appendDurable: (f) => approvalEmit(f) }),
|
|
2062
|
+
legKey,
|
|
2063
|
+
...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}),
|
|
2064
|
+
}, driveResume)
|
|
2065
|
+
: driveResume();
|
|
1955
2066
|
return await (deps.elicitation && taskId
|
|
1956
|
-
? deps.elicitation.runWithContext({ taskId, owner: principal ?? null, emit: (frame) => { const { type, ...rest } = frame; return append(type, rest); }, abortSignal: cancelCtrl.signal },
|
|
1957
|
-
:
|
|
2067
|
+
? deps.elicitation.runWithContext({ taskId, owner: principal ?? null, emit: (frame) => { const { type, ...rest } = frame; return append(type, rest); }, abortSignal: cancelCtrl.signal }, withApproval)
|
|
2068
|
+
: withApproval());
|
|
1958
2069
|
});
|
|
1959
2070
|
}
|
|
1960
2071
|
catch (e) {
|
|
@@ -2624,6 +2735,9 @@ const ROUTE_LABEL_PATTERNS = [
|
|
|
2624
2735
|
[/^\/v1\/workflows\/[^/]+\/agents\/[^/]+\/steer$/, "/v1/workflows/:id/agents/:aid/steer"],
|
|
2625
2736
|
[/^\/v1\/workflows\/[^/]+\/(stream|journal)$/, "/v1/workflows/:id/$sub"],
|
|
2626
2737
|
[/^\/v1\/workflows\/[^/]+$/, "/v1/workflows/:id"],
|
|
2738
|
+
// #151 车4:durable 回决口。必须排在 `/v1/tasks/:id/$sub` **之前**?—— 不必(那条只认三个字面动词),
|
|
2739
|
+
// 但仍按「具体先于泛化」的表序纪律放在它前面,免得日后 $sub 放宽成 `[^/]+` 时静默吞掉本条。
|
|
2740
|
+
[/^\/v1\/tasks\/[^/]+\/asks\/[^/]+\/decision$/, "/v1/tasks/:id/asks/:askId/decision"],
|
|
2627
2741
|
[/^\/v1\/tasks\/[^/]+\/(turns|stream|artifacts)$/, "/v1/tasks/:id/$sub"],
|
|
2628
2742
|
[/^\/v1\/leader\/[^/]+$/, "/v1/leader/:id"],
|
|
2629
2743
|
[/^\/v1\/attachments\/[^/]+$/, "/v1/attachments/:id"],
|
|
@@ -2643,6 +2757,8 @@ const ROUTE_LABEL_LITERALS = new Set([
|
|
|
2643
2757
|
// 名册门的枚举器当时只认 `url === …`,整条路由对门隐形(枚举器已一并补上)。
|
|
2644
2758
|
"/v1/fleet/stream",
|
|
2645
2759
|
"/v1/memory/export", "/v1/sendfile-links",
|
|
2760
|
+
// #154:装配自证读面(operator-only)。低频但**每次都在排障现场被打**,落进 `other` 桶等于排障时看不见。
|
|
2761
|
+
"/v1/diagnostics/wiring",
|
|
2646
2762
|
]);
|
|
2647
2763
|
/** Stable, low-cardinality route label for metrics/logs (ids collapsed to `:id`).
|
|
2648
2764
|
* [#104] 字面量**先于**模式:此前模式先查,五条精确路由被形状桶吞掉(`/v1/approvals/stream`
|
package/dist/http/sse-log.d.ts
CHANGED
|
@@ -30,7 +30,58 @@ export interface SseLogProvider<E extends {
|
|
|
30
30
|
event: string;
|
|
31
31
|
data: unknown;
|
|
32
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* #151 车3 §5:**开流 preamble** —— 在 416 判定与 `sseHeaders` **之后**、日志 tail **之前**写的
|
|
35
|
+
* 一批**非账本**帧(今天唯一的用户 = 审批未决卡的全量对账基准)。
|
|
36
|
+
*
|
|
37
|
+
* 🔴 为什么必须是这里的内部 hook,而不是调用方在 `streamSseLog` **之前**自己写:本 helper 的判序是
|
|
38
|
+
* ①读 `last-event-id`/`?from=` → ②保留期比对、超界直接 `sendError(416)` → ③`sseHeaders` → ④tail。
|
|
39
|
+
* 在 ① 之前写任何字节 = 提前提交 200 头 ⇒ ② 的 416 分支**结构上不可能再触发**(或触发
|
|
40
|
+
* headers-already-sent),而调用方自己先调 `sseHeaders` 又会被本 helper 重复写头。
|
|
41
|
+
*
|
|
42
|
+
* 返回帧**不带 `id:`** —— 它不是账本行,不参与 `Last-Event-ID` 游标(壳的规则:preamble 给的是
|
|
43
|
+
* 权威全量卡集,账本重放里的同 id 帧只用于时间线渲染)。
|
|
44
|
+
*
|
|
45
|
+
* 缺席 = 零影响(其余 provider 不实现)。失败/挂起的处置见 {@link collectSsePreamble}。
|
|
46
|
+
*
|
|
47
|
+
* `signal`(#151 车3 刀 3b,§14.2 遗留记账):有界窗到点时**被 abort** —— 实现方应把它传进自己的
|
|
48
|
+
* store 读口,让那次读当场以 abort 拒绝,而不是留一个「结果已被丢弃、调用链却还在飞」的悬挂读。
|
|
49
|
+
* 可达到的语义与残留边界见 `ApprovalAskStore.listPendingBySession` 的 `signal` 注(驱动无取消 seam)。
|
|
50
|
+
*/
|
|
51
|
+
preamble?(signal: AbortSignal): Promise<Array<{
|
|
52
|
+
event: string;
|
|
53
|
+
data: unknown;
|
|
54
|
+
}>>;
|
|
55
|
+
/**
|
|
56
|
+
* `preamble` 被有界超时掐断("timeout")或抛出("error")时的**观测**钩(fail-open,已经决定不挡
|
|
57
|
+
* 开流了,这里只让域模块用自己的 logger 记一条 warn —— 本叶子模块没有、也不该有 logger)。
|
|
58
|
+
*/
|
|
59
|
+
onPreambleFailure?(reason: "timeout" | "error", err?: unknown): void;
|
|
33
60
|
}
|
|
61
|
+
/** #151 车3 §5.3:preamble 的**有界**等待上限。挂起的 store 不许拖住开流——开流是壳的主路径,
|
|
62
|
+
* 而 preamble 只是一个锦上添花的对账基准。超时即按「无卡集基准」继续(与今天等同)。 */
|
|
63
|
+
export declare const SSE_PREAMBLE_TIMEOUT_MS = 2000;
|
|
64
|
+
/**
|
|
65
|
+
* 跑一次 preamble,**有界 + fail-open**(#151 车3 §5.3)。
|
|
66
|
+
*
|
|
67
|
+
* 🔴 只 catch throw 是不够的:store **挂起**(不抛)会把整条开流一起拖住。所以这里是 `Promise.race`
|
|
68
|
+
* 而不是 try/catch —— 两种失败形(抛 / 不返回)各自有一条出路,都落到「零帧 + 一次 warn + 继续开流」。
|
|
69
|
+
*
|
|
70
|
+
* 🔴 #151 车3 刀 3b(§14.2 遗留记账兑现):超时**同时 abort** 一个交给 `preamble` 的 `AbortSignal`。
|
|
71
|
+
* 此前只是「丢结果」,原 promise 仍在飞 —— DB 挂死时的重连风暴会一轮轮往连接池里堆等待者。现在读口
|
|
72
|
+
* 拿到 abort 会当场拒绝,调用链不再累积;库那侧的那条查询仍会跑完(驱动无取消 seam,残留边界见
|
|
73
|
+
* `ApprovalAskStore.listPendingBySession` 的注)。丢结果这条语义不变:preamble 只读,丢弃零副作用。
|
|
74
|
+
*
|
|
75
|
+
* 导出给 `routes/tasks.ts` 的 sync 腿直接用 —— 那条腿不走 `streamSseLog`(它不是账本 tail),但
|
|
76
|
+
* 「有界 + fail-open」这条判据必须是**同一份实现**,不能各腿各写一遍。
|
|
77
|
+
*/
|
|
78
|
+
export declare function collectSsePreamble(preamble: (signal: AbortSignal) => Promise<Array<{
|
|
79
|
+
event: string;
|
|
80
|
+
data: unknown;
|
|
81
|
+
}>>, onFailure?: (reason: "timeout" | "error", err?: unknown) => void, timeoutMs?: number): Promise<Array<{
|
|
82
|
+
event: string;
|
|
83
|
+
data: unknown;
|
|
84
|
+
}>>;
|
|
34
85
|
/** The shared resumable SSE pump (P2.8) — provider supplies the log-specific differences; the liveness/
|
|
35
86
|
* heartbeat/416/terminal-re-fetch/15-min-cap logic is identical for task_run + image_bake. */
|
|
36
87
|
export declare function streamSseLog<E extends {
|
package/dist/http/sse-log.js
CHANGED
|
@@ -1,4 +1,51 @@
|
|
|
1
1
|
import { sendJson, sendError, sseHeaders } from "./send.js";
|
|
2
|
+
/** #151 车3 §5.3:preamble 的**有界**等待上限。挂起的 store 不许拖住开流——开流是壳的主路径,
|
|
3
|
+
* 而 preamble 只是一个锦上添花的对账基准。超时即按「无卡集基准」继续(与今天等同)。 */
|
|
4
|
+
export const SSE_PREAMBLE_TIMEOUT_MS = 2_000;
|
|
5
|
+
/**
|
|
6
|
+
* 跑一次 preamble,**有界 + fail-open**(#151 车3 §5.3)。
|
|
7
|
+
*
|
|
8
|
+
* 🔴 只 catch throw 是不够的:store **挂起**(不抛)会把整条开流一起拖住。所以这里是 `Promise.race`
|
|
9
|
+
* 而不是 try/catch —— 两种失败形(抛 / 不返回)各自有一条出路,都落到「零帧 + 一次 warn + 继续开流」。
|
|
10
|
+
*
|
|
11
|
+
* 🔴 #151 车3 刀 3b(§14.2 遗留记账兑现):超时**同时 abort** 一个交给 `preamble` 的 `AbortSignal`。
|
|
12
|
+
* 此前只是「丢结果」,原 promise 仍在飞 —— DB 挂死时的重连风暴会一轮轮往连接池里堆等待者。现在读口
|
|
13
|
+
* 拿到 abort 会当场拒绝,调用链不再累积;库那侧的那条查询仍会跑完(驱动无取消 seam,残留边界见
|
|
14
|
+
* `ApprovalAskStore.listPendingBySession` 的注)。丢结果这条语义不变:preamble 只读,丢弃零副作用。
|
|
15
|
+
*
|
|
16
|
+
* 导出给 `routes/tasks.ts` 的 sync 腿直接用 —— 那条腿不走 `streamSseLog`(它不是账本 tail),但
|
|
17
|
+
* 「有界 + fail-open」这条判据必须是**同一份实现**,不能各腿各写一遍。
|
|
18
|
+
*/
|
|
19
|
+
export async function collectSsePreamble(preamble, onFailure, timeoutMs = SSE_PREAMBLE_TIMEOUT_MS) {
|
|
20
|
+
const timedOut = Symbol("preamble-timeout");
|
|
21
|
+
let timer;
|
|
22
|
+
const ac = new AbortController();
|
|
23
|
+
try {
|
|
24
|
+
const raced = await Promise.race([
|
|
25
|
+
preamble(ac.signal),
|
|
26
|
+
new Promise((resolve) => {
|
|
27
|
+
timer = setTimeout(() => {
|
|
28
|
+
ac.abort(); // 先掐读,再回空集——两件事的顺序无关紧要,但都要发生
|
|
29
|
+
resolve(timedOut);
|
|
30
|
+
}, timeoutMs);
|
|
31
|
+
timer.unref?.(); // 别让这只表把进程钉住(测试进程里尤其明显)
|
|
32
|
+
}),
|
|
33
|
+
]);
|
|
34
|
+
if (raced === timedOut) {
|
|
35
|
+
onFailure?.("timeout");
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return raced;
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
onFailure?.("error", err);
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
if (timer)
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
2
49
|
/** The shared resumable SSE pump (P2.8) — provider supplies the log-specific differences; the liveness/
|
|
3
50
|
* heartbeat/416/terminal-re-fetch/15-min-cap logic is identical for task_run + image_bake. */
|
|
4
51
|
export async function streamSseLog(req, res, provider, id, staleMs) {
|
|
@@ -14,10 +61,27 @@ export async function streamSseLog(req, res, provider, id, staleMs) {
|
|
|
14
61
|
}
|
|
15
62
|
}
|
|
16
63
|
sseHeaders(res);
|
|
64
|
+
// 🔴 断连接线必须**先于** preamble 的等待窗接上(codex 复审 2026-08-06 的 medium):Node 的 `'close'`
|
|
65
|
+
// 只发一次,在等待窗里断连的客户端其 close 事件会在监听器注册之前烧掉 ⇒ `closed` 恒 false ⇒ 泵照常
|
|
66
|
+
// 进轮询循环,对着已死的 res 反复 getEvents/statusOf 直到终态/stale/15 分钟帽。本行原本紧跟
|
|
67
|
+
// sseHeaders,是 preamble 插进来才多出这个窗 —— 所以搬接线、不搬 preamble。
|
|
17
68
|
let closed = false;
|
|
18
69
|
req.on("close", () => {
|
|
19
70
|
closed = true;
|
|
20
71
|
});
|
|
72
|
+
// #151 车3 §5.1:preamble 排在 416 判定与 sseHeaders **之后**、日志 tail 之前。有界 + fail-open,
|
|
73
|
+
// 见 collectSsePreamble 顶注;`id:` 行故意不写(非账本行,不推进 Last-Event-ID 游标)。
|
|
74
|
+
const preamble = provider.preamble?.bind(provider); // bind:接口按方法签名声明,实现方允许用 `this`
|
|
75
|
+
if (preamble) {
|
|
76
|
+
const frames = await collectSsePreamble(preamble, (reason, err) => provider.onPreambleFailure?.(reason, err));
|
|
77
|
+
// 等待窗里断连了就别再写(`closed` 由上面的 close 监听器置;`writableEnded`/`destroyed` 是
|
|
78
|
+
// 第二道 —— codex 复审第二轮 medium 指出 res 侧的 close 面本 helper 没听,那半条见终报的存疑单,
|
|
79
|
+
// 这里先把「往已死的 res 写」这半堵掉,判据与 routes/tasks.ts 心跳处逐字同源)。
|
|
80
|
+
if (!closed && !res.writableEnded && !res.destroyed) {
|
|
81
|
+
for (const f of frames)
|
|
82
|
+
res.write(`event: ${f.event}\ndata: ${JSON.stringify(f.data)}\n\n`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
21
85
|
const start = Date.now();
|
|
22
86
|
const MAX_MS = 15 * 60 * 1000;
|
|
23
87
|
let lastBeat = Date.now();
|