@sema-agent/server 7.86.0 → 7.87.1

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.
Files changed (59) hide show
  1. package/USAGE.md +1 -1
  2. package/dist/approval-hmac.d.ts +62 -14
  3. package/dist/approval-hmac.js +15 -1
  4. package/dist/boot/stage-01-config.js +2 -2
  5. package/dist/brain.js +5 -3
  6. package/dist/capabilities/approvals-push.d.ts +47 -0
  7. package/dist/capabilities/approvals-push.js +4 -0
  8. package/dist/config-catalog.js +2 -1
  9. package/dist/config-invariants.d.ts +2 -2
  10. package/dist/config-invariants.js +8 -0
  11. package/dist/config.js +7 -0
  12. package/dist/fleet/fleet-bus.d.ts +23 -4
  13. package/dist/fleet/fleet-reconciler.d.ts +35 -0
  14. package/dist/fleet/fleet-reconciler.js +50 -7
  15. package/dist/http/approval-sweeps.js +1 -1
  16. package/dist/http/resume-legs.js +3 -2
  17. package/dist/http/routes/a2a-serve.js +1 -1
  18. package/dist/http/routes/approvals-assistant.d.ts +35 -23
  19. package/dist/http/routes/approvals-assistant.js +107 -30
  20. package/dist/http/routes/capabilities.js +2 -0
  21. package/dist/http/routes/fleet.js +1 -2
  22. package/dist/http/routes/runs.js +2 -2
  23. package/dist/http/routes/tasks.js +2 -2
  24. package/dist/http/routes/workflows.js +1 -1
  25. package/dist/http/server.d.ts +2 -2
  26. package/dist/http/server.js +3 -3
  27. package/dist/leader/diffout.js +1 -1
  28. package/dist/leader/endpoint.js +1 -1
  29. package/dist/leader/fanout.js +1 -1
  30. package/dist/leader/leader.js +1 -1
  31. package/dist/leader/merge.js +1 -1
  32. package/dist/leader/planner.js +1 -1
  33. package/dist/leader/repair-oracle.js +1 -1
  34. package/dist/leader/repair-wire.js +1 -1
  35. package/dist/leader/wire.js +1 -1
  36. package/dist/model-provider.d.ts +77 -0
  37. package/dist/model-provider.js +26 -0
  38. package/dist/observability/fail-open.d.ts +8 -0
  39. package/dist/observability/fail-open.js +8 -0
  40. package/dist/observability/metrics.d.ts +12 -0
  41. package/dist/observability/metrics.js +17 -0
  42. package/dist/observability/{run-terminal-log.d.ts → run-terminal.d.ts} +50 -10
  43. package/dist/observability/{run-terminal-log.js → run-terminal.js} +27 -8
  44. package/dist/plugins/file-run-store.js +2 -2
  45. package/dist/plugins/memory-run-store.js +2 -2
  46. package/dist/plugins/remote-scratchpad.js +1 -1
  47. package/dist/plugins/run-store-sql.js +8 -7
  48. package/dist/plugins/store-contracts.d.ts +2 -2
  49. package/dist/plugins/workflow-run-store-sql.js +1 -1
  50. package/dist/runs.js +1 -1
  51. package/dist/session-titler.js +9 -5
  52. package/dist/tool-approval.d.ts +22 -2
  53. package/dist/tool-approval.js +2 -0
  54. package/dist/trace/core-keyset-guard.d.ts +1 -1
  55. package/dist/trace/ledger-events.d.ts +1 -1
  56. package/dist/trace/project.d.ts +1 -1
  57. package/dist/trace/project.js +1 -1
  58. package/dist/trace/redact.d.ts +1 -1
  59. package/package.json +2 -2
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { QuestionAnswer } from "@sema-agent/core";
3
3
  import type { CheckpointStoreFull } from "../../plugins/store-backend.js";
4
+ import { type ToolApprovalCoordinator } from "../../tool-approval.js";
4
5
  import { type GovernancePosture } from "../active-run-conflict.js";
5
6
  import { type RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
6
7
  import { type KeyClosureIssue } from "../../request-key-closure.js";
@@ -49,35 +50,46 @@ export declare function planReviewBodyKeyIssue(raw: unknown): KeyClosureIssue |
49
50
  export declare function decisionReasonOf(body: {
50
51
  reason?: unknown;
51
52
  }): string | undefined;
52
- /** Structural check for an operator-supplied QuestionAnswer (durable ask resume, TC-5.4). Strict on the
53
- * load-bearing shape — `answers[]` non-empty, each `{header: string, selected: string[], note?: string}` —
54
- * so a typo'd payload fails the request instead of resuming the task with an answer the tool can't use. */
55
- export declare function isQuestionAnswer(v: unknown): v is QuestionAnswer;
53
+ /** {@link checkQuestionAnswer} 的判词。两种拒各配**一个既有的**错误码,不铸第三种。 */
54
+ export type QuestionAnswerCheck = {
55
+ readonly ok: true;
56
+ readonly value: QuestionAnswer;
57
+ }
58
+ /** 形不对(不是对象 / `answers` 空 / 某一位的值形不对)⇒ 400 `request.body_shape`,文案逐字不变。 */
59
+ | {
60
+ readonly ok: false;
61
+ readonly kind: "shape";
62
+ }
63
+ /** 词表外的键 ⇒ 400 `request.field_invalid`,**点名键路径**(`answer.answers[0].selection`)。 */
64
+ | {
65
+ readonly ok: false;
66
+ readonly kind: "unknown_key";
67
+ readonly keyPaths: readonly string[];
68
+ };
69
+ /**
70
+ * Structural check for an operator-supplied QuestionAnswer (durable ask resume, TC-5.4). Strict on the
71
+ * load-bearing shape — `answers[]` non-empty, each `{header: string, selected: string[], note?: string}` —
72
+ * so a typo'd payload fails the request instead of resuming the task with an answer the tool can't use.
73
+ * S-452 起**同时闭键**:词表外的键(拼错 / 未来键 / `__proto__`)拒不丢。
74
+ */
75
+ export declare function checkQuestionAnswer(v: unknown): QuestionAnswerCheck;
56
76
  /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
57
77
  * = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
58
78
  export declare function handleApprovalsAssistant(req: IncomingMessage, res: ServerResponse, match: RouteMatch<ApprovalsAssistantRouteId>, ctx: RouteCtx): Promise<void>;
79
+ export declare function streamApprovals(req: IncomingMessage, res: ServerResponse, cs: Pick<CheckpointStoreFull, "listPending">,
59
80
  /**
60
- * GET /v1/approvals/stream ([ref] native push): SSE — pushes pending-approval deltas so the portal
61
- * SUBSCRIBES ONCE instead of polling GET /v1/approvals every ~10s (better UX: near-real-time + no client poll
62
- * loop). Polls listPending(scope) and diffs against the prior snapshot: `pending` for a new suspend, `resolved`
63
- * for a decided/expired/gone one. Cross-replica BY CONSTRUCTION — the poll reads the SHARED checkpoint table,
64
- * so a suspend on ANY replica is seen by an operator streaming on a DIFFERENT replica. A transient DB blip
65
- * heartbeats + retries (never kills the stream); a 15-min cap + req-close end it (parity with streamTaskTrace).
66
- * Poll-granularity caveat (**narrowed** by [ref]②): the delta now re-emits a `pending` frame whenever a key's
67
- * WIRE PROJECTION changes (upsert), not only when the key enters — so a same-key re-suspend within one interval
68
- * IS visible as long as anything on the row differs. What still shows no delta is the strictly degenerate case:
69
- * a resolve + re-suspend on the same (session,toolCallId) whose projection is BYTE-IDENTICAL — indistinguishable
70
- * by construction, and harmless (the card the operator sees is the card that is pending). Pins: the two halves of
71
- * `test/wire-pairing-approvals-stream.test.ts` S3b (changed ⇒ re-emit with the NEW payload; unchanged ⇒ silence).
81
+ * 🔴 S-454:**第二条 diff 源** —— `GET /v1/approvals` 的 `livePending` 那**同一只**读函数
82
+ * (`Pick` 而不是另立接口:同源是结构事实,不是两处各写一份的约定)。
83
+ *
84
+ * 为什么必须同源:消费方(client-core `hitl/approvalsFeed`)对 stream 上任一非 heartbeat 事件的
85
+ * 反应是**重取权威 `list()`** —— 推与拉读两份数据时,一条只在推面存在的行会让 feed 空转,
86
+ * 一条只在拉面存在的行则永远等不到催人事件。
72
87
  *
73
- * [ref](2026-08-09 亲验定性=设计接受,非欠账):每连接自跑 pollMs 轮询打共享 checkpoint 表,
74
- * 之所以不建 fan-out/单轮询器基建——①消费方=portal 运营面,并发连接数量级为个位(不是用户面);
75
- * ②listPending 走 (scope,status) 索引,单查成本与一次 /v1/approvals 轮询同阶,而本路正是替代客户端
76
- * ~10s 轮询的(净负载持平或更低);③DB blip 已 fail-soft(心跳+下拍重试),15-min cap 兜顶。连接
77
- * 数假设若被打破(portal 多开成常态),届时把 poll 收敛为进程内单轮询器多路复用——那是量级触发的
78
- * 演化,不是现在的缺陷。
88
+ * `undefined` = 本部署无 live 审批面(`TOOL_APPROVAL_ENABLED=false` ⇒ 协调器未装配)⇒ 本源整个
89
+ * 缺席,流的字节与 S-454 之前逐字相同(能力缺席 = 零变化,不是空数组)。**位置在 `pollMs` 之前**
90
+ * 与 `governance` 同一条理由:`pollMs` 只有测试传,而本参数是每个生产调用点都必须表态的那一个。
79
91
  */
80
- export declare function streamApprovals(req: IncomingMessage, res: ServerResponse, cs: Pick<CheckpointStoreFull, "listPending">, scope: string | undefined,
92
+ live: Pick<ToolApprovalCoordinator, "listLivePending"> | undefined, scope: string | undefined,
81
93
  /** [ref]②:出身归因的部署侧一半(缺席 ⇒ 归不出治理出身 ⇒ 键缺席,与 best-effort 同方向)。**位置在
82
94
  * `pollMs` 之前**是刻意的:`pollMs` 只有测试传,而本参数是每个生产调用点都必须给的那一个。 */
83
95
  governance?: GovernancePosture, pollMs?: number): Promise<void>;
@@ -6,11 +6,11 @@ import { APPROVAL_GATE_KINDS } from "../../tool-approval.js";
6
6
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
7
7
  import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
8
8
  import { editedPlanBinding, isWellFormedUnicode, verifyDirectDoorProof } from "../../principal-jwt.js";
9
- import { MAX_APPROVAL_REASON_CHARS } from "../../approval-hmac.js";
9
+ import { MAX_APPROVAL_REASON_CHARS, projectQuestionAnswer, questionAnswerFromProjection } from "../../approval-hmac.js";
10
10
  import { PLAN_APPROVE_PERMISSION_MODES_AFTER, isPlanApprovePermissionModeAfter, storedBodyPermissionMode } from "../../task-settings.js";
11
11
  import { MAX_APPROVAL_NONCE_CHARS } from "../../plugins/approval-nonce-store.js";
12
12
  import { redactedPreview, redactSecrets } from "../../trace/redact.js";
13
- import { redactRiskDescriptor } from "../../observability/run-terminal-log.js";
13
+ import { redactRiskDescriptor } from "../../observability/run-terminal.js";
14
14
  import { fleetRunLabels } from "../../fleet/fleet-bus.js";
15
15
  import { sleep } from "../sse-log.js";
16
16
  import { bindSseLifecycle } from "../sse-lifecycle.js";
@@ -23,6 +23,8 @@ import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "../../request-ke
23
23
  const DECIDE_BODY_DECLARED_KEYS = [
24
24
  "answer", "boundCallId", "boundInputHash", "checkpointToken", "decision", "reason", "remember", "updatedInput",
25
25
  ];
26
+ const DECIDE_PROOF_COVERED_KEYS = ["answer", "boundCallId", "boundInputHash", "decision", "reason"];
27
+ const DECIDE_NOT_IN_PROOF_KEYS = ["checkpointToken", "remember", "updatedInput"];
26
28
  const ACCEPTED_DECIDE_BODY = new Set(DECIDE_BODY_DECLARED_KEYS);
27
29
  const UNSUPPORTED_DECIDE_BODY = new Set(["editedPlan", "sessionId", "taskId"]);
28
30
  export function decideBodyKeyIssue(raw) {
@@ -50,6 +52,23 @@ export function decideBodyKeyIssue(raw) {
50
52
  const PLAN_REVIEW_BODY_DECLARED_KEYS = [
51
53
  "boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason",
52
54
  ];
55
+ const PLAN_REVIEW_PROOF_COVERED_KEYS = ["boundCallId", "decision", "editedPlan", "permissionModeAfter", "reason"];
56
+ const PLAN_REVIEW_NOT_IN_PROOF_KEYS = [];
57
+ const PROOF_SLOT_OF = {
58
+ answer: "answer",
59
+ boundCallId: "boundCallId",
60
+ boundInputHash: "boundInputHash",
61
+ decision: "decision",
62
+ editedPlan: "boundInputHash",
63
+ permissionModeAfter: "permissionModeAfter",
64
+ reason: "reason",
65
+ };
66
+ function buildApprovalEnvelope(sessionId, covered, reading) {
67
+ const envelope = { sessionId, decision: reading.decision };
68
+ for (const key of covered)
69
+ Object.assign(envelope, { [PROOF_SLOT_OF[key]]: reading[key] });
70
+ return envelope;
71
+ }
53
72
  const ACCEPTED_PLAN_REVIEW_BODY = new Set(PLAN_REVIEW_BODY_DECLARED_KEYS);
54
73
  const UNSUPPORTED_PLAN_REVIEW_BODY = new Set([
55
74
  "answer", "boundInputHash", "checkpointToken", "permissionMode", "remember", "sessionId", "taskId", "updatedInput",
@@ -79,13 +98,27 @@ export function planReviewBodyKeyIssue(raw) {
79
98
  export function decisionReasonOf(body) {
80
99
  return typeof body.reason === "string" && body.reason !== "" ? body.reason : undefined;
81
100
  }
82
- export function isQuestionAnswer(v) {
101
+ const QUESTION_ANSWER_TOP_KEYS = ["answers"];
102
+ const QUESTION_ANSWER_ITEM_KEYS = ["header", "selected", "note"];
103
+ const ACCEPTED_QUESTION_ANSWER_TOP = new Set(QUESTION_ANSWER_TOP_KEYS);
104
+ const ACCEPTED_QUESTION_ANSWER_ITEM = new Set(QUESTION_ANSWER_ITEM_KEYS);
105
+ const NO_TOLERATED_KEYS = new Set();
106
+ export function checkQuestionAnswer(v) {
83
107
  if (!v || typeof v !== "object" || Array.isArray(v))
84
- return false;
108
+ return { ok: false, kind: "shape" };
85
109
  const a = v.answers;
86
110
  if (!Array.isArray(a) || a.length === 0)
87
- return false;
88
- return a.every((it) => {
111
+ return { ok: false, kind: "shape" };
112
+ const buckets = { unknown: [], unsupported: [] };
113
+ scanKeyClosure(v, "answer.", ACCEPTED_QUESTION_ANSWER_TOP, NO_TOLERATED_KEYS, buckets);
114
+ a.forEach((it, i) => {
115
+ if (!it || typeof it !== "object" || Array.isArray(it))
116
+ return;
117
+ scanKeyClosure(it, `answer.answers[${i}].`, ACCEPTED_QUESTION_ANSWER_ITEM, NO_TOLERATED_KEYS, buckets);
118
+ });
119
+ if (buckets.unknown.length > 0)
120
+ return { ok: false, kind: "unknown_key", keyPaths: buckets.unknown.sort().slice(0, MAX_KEY_ECHO) };
121
+ const shapeOk = a.every((it) => {
89
122
  if (!it || typeof it !== "object" || Array.isArray(it))
90
123
  return false;
91
124
  const { header, selected, note } = it;
@@ -95,6 +128,7 @@ export function isQuestionAnswer(v) {
95
128
  selected.every((s) => typeof s === "string") &&
96
129
  (note === undefined || typeof note === "string"));
97
130
  });
131
+ return shapeOk ? { ok: true, value: v } : { ok: false, kind: "shape" };
98
132
  }
99
133
  async function decideIdempotentReplay(cs, args) {
100
134
  const { sessionId, out, binding, decision, payload, deciderPrincipal, explicitOperator, warn } = args;
@@ -173,7 +207,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
173
207
  const scope = operator
174
208
  ? (new URLSearchParams(match.search).get("owner") ?? undefined)
175
209
  : (principal ?? "__none__");
176
- await streamApprovals(req, res, cs, scope, deps.config);
210
+ await streamApprovals(req, res, cs, deps.toolApproval, scope, deps.config);
177
211
  return;
178
212
  }
179
213
  if (match.decl.id === "approvals-list") {
@@ -464,7 +498,13 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
464
498
  }
465
499
  const storeNowMs = await store.nowMs();
466
500
  const hdr = (n) => { const h = req.headers[n]; return Array.isArray(h) ? h[0] : h; };
467
- const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) }, { sessionId: run.sessionId, boundCallId: planNonce ?? null, boundInputHash: decision === "edit" ? editedPlanBinding(body.editedPlan) : null, decision, reason: typeof body.reason === "string" ? body.reason : null, ...(isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? { permissionModeAfter: body.permissionModeAfter } : {}) }, deps.config, { actionBinding: false, nowSec: Math.floor(storeNowMs / 1000) });
501
+ const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) }, buildApprovalEnvelope(run.sessionId, PLAN_REVIEW_PROOF_COVERED_KEYS, {
502
+ boundCallId: planNonce ?? null,
503
+ decision,
504
+ editedPlan: decision === "edit" ? editedPlanBinding(body.editedPlan) : null,
505
+ permissionModeAfter: isPlanApprovePermissionModeAfter(body.permissionModeAfter) ? body.permissionModeAfter : undefined,
506
+ reason: typeof body.reason === "string" ? body.reason : null,
507
+ }), deps.config, { actionBinding: false, nowSec: Math.floor(storeNowMs / 1000) });
468
508
  if (!proof.ok) {
469
509
  sendError(res, proof.status, proof.errorCode, proof.error);
470
510
  return;
@@ -556,12 +596,19 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
556
596
  remember = true;
557
597
  }
558
598
  let answer;
599
+ let answerProjection;
559
600
  if (body.answer !== undefined) {
560
- if (!isQuestionAnswer(body.answer)) {
561
- sendError(res, 400, "request.body_shape", "malformed answer — expected { answers: [{ header, selected: string[], note? }] }");
601
+ const checked = checkQuestionAnswer(body.answer);
602
+ if (!checked.ok) {
603
+ if (checked.kind === "shape") {
604
+ sendError(res, 400, "request.body_shape", "malformed answer — expected { answers: [{ header, selected: string[], note? }] }");
605
+ return;
606
+ }
607
+ sendError(res, 400, "request.field_invalid", `answer: unrecognized key(s) — refused rather than dropped, because a dropped key reads as "in effect" to the caller (a misspelled selected key silently drops part of the operator's answer). ${keyClosureClause([...checked.keyPaths], "unknown (not a question-answer key — check the spelling)")}. Accepted: answers, answers[].header, answers[].selected, answers[].note`, { unknownKeys: [...checked.keyPaths] });
562
608
  return;
563
609
  }
564
- answer = body.answer;
610
+ answerProjection = projectQuestionAnswer(checked.value);
611
+ answer = questionAnswerFromProjection(answerProjection);
565
612
  }
566
613
  for (const f of ["checkpointToken", "boundCallId", "boundInputHash"]) {
567
614
  if (body[f] !== undefined && typeof body[f] !== "string") {
@@ -573,6 +620,10 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
573
620
  sendError(res, 400, "updated_input_not_in_proof", "updatedInput is not supported on a direct-door worker (not covered by the decision proof)");
574
621
  return;
575
622
  }
623
+ if (deps.config.directDoorActive && body.checkpointToken !== undefined) {
624
+ sendError(res, 400, "checkpoint_token_not_in_proof", "checkpointToken is not supported on a direct-door worker (not covered by the decision proof); bind the decision with boundCallId and boundInputHash instead");
625
+ return;
626
+ }
576
627
  const binding = {
577
628
  checkpointToken: body.checkpointToken,
578
629
  boundCallId: body.boundCallId,
@@ -585,7 +636,13 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
585
636
  const h = req.headers[n];
586
637
  return Array.isArray(h) ? h[0] : h;
587
638
  };
588
- const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) }, { sessionId, boundCallId: binding.boundCallId, boundInputHash: binding.boundInputHash, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config);
639
+ const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) }, buildApprovalEnvelope(sessionId, DECIDE_PROOF_COVERED_KEYS, {
640
+ answer: answerProjection,
641
+ boundCallId: binding.boundCallId,
642
+ boundInputHash: binding.boundInputHash,
643
+ decision,
644
+ reason: typeof body.reason === "string" ? body.reason : null,
645
+ }), deps.config);
589
646
  if (!proof.ok) {
590
647
  sendError(res, proof.status, proof.errorCode, proof.error);
591
648
  return;
@@ -664,7 +721,24 @@ function projectPendingForWire(row, governance) {
664
721
  const governanceForced = governanceOriginOf({ riskDescriptor: row.riskDescriptor ?? undefined }, governance);
665
722
  return governanceForced ? { ...redacted, governanceForced } : redacted;
666
723
  }
667
- export async function streamApprovals(req, res, cs, scope, governance, pollMs = APPROVALS_STREAM_POLL_MS) {
724
+ function createSseDeltaLane(res, spec) {
725
+ let prev = new Map();
726
+ return {
727
+ tick(rows) {
728
+ const cur = new Map(rows.map((r) => [spec.keyOf(r), { row: r, bytes: spec.bytesOf(r) }]));
729
+ for (const [k, v] of cur) {
730
+ if (prev.get(k)?.bytes !== v.bytes)
731
+ res.write(`event: ${spec.upsertEvent}\ndata: ${JSON.stringify(spec.upsertData(v.row, Date.now()))}\n\n`);
732
+ }
733
+ for (const [k, v] of prev) {
734
+ if (!cur.has(k))
735
+ res.write(`event: ${spec.exitEvent}\ndata: ${JSON.stringify(spec.exitData(v.row, Date.now()))}\n\n`);
736
+ }
737
+ prev = cur;
738
+ },
739
+ };
740
+ }
741
+ export async function streamApprovals(req, res, cs, live, scope, governance, pollMs = APPROVALS_STREAM_POLL_MS) {
668
742
  const keyOf = (p) => JSON.stringify([p.sessionId, p.toolCallId ?? null]);
669
743
  sseHeaders(res);
670
744
  res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", version: 1, mode: "approvals-delta", pollMs: APPROVALS_STREAM_POLL_MS })}\n\n`);
@@ -672,13 +746,29 @@ export async function streamApprovals(req, res, cs, scope, governance, pollMs =
672
746
  bindSseLifecycle(req, res, () => { closed = true; });
673
747
  const start = Date.now();
674
748
  let lastBeat = Date.now();
675
- let prev = new Map();
676
- let prevFrames = new Map();
749
+ const durableLane = createSseDeltaLane(res, {
750
+ upsertEvent: "pending",
751
+ exitEvent: "resolved",
752
+ keyOf,
753
+ bytesOf: (p) => JSON.stringify({ type: "pending", ...p }),
754
+ upsertData: (p, nowMs) => ({ type: "pending", ...p, serverNowMs: nowMs }),
755
+ exitData: (p, nowMs) => ({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId, serverNowMs: nowMs }),
756
+ });
757
+ const liveLane = createSseDeltaLane(res, {
758
+ upsertEvent: "live_pending",
759
+ exitEvent: "live_resolved",
760
+ keyOf: (r) => r.approvalId,
761
+ bytesOf: (r) => JSON.stringify({ type: "live_pending", ...r }),
762
+ upsertData: (r, nowMs) => ({ type: "live_pending", ...r, serverNowMs: nowMs }),
763
+ exitData: (r, nowMs) => ({ type: "live_resolved", approvalId: r.approvalId, serverNowMs: nowMs }),
764
+ });
677
765
  let first = true;
678
766
  while (!closed) {
679
767
  let pending;
768
+ let livePending;
680
769
  try {
681
770
  pending = (await cs.listPending(scope)).map((r) => projectPendingForWire(r, governance));
771
+ livePending = live ? live.listLivePending(scope) : [];
682
772
  }
683
773
  catch {
684
774
  if (!res.writableEnded)
@@ -686,25 +776,12 @@ export async function streamApprovals(req, res, cs, scope, governance, pollMs =
686
776
  await sleep(pollMs);
687
777
  continue;
688
778
  }
689
- const cur = new Map(pending.map((p) => [keyOf(p), p]));
690
- const curFrames = new Map([...cur].map(([k, p]) => [k, JSON.stringify({ type: "pending", ...p })]));
691
- const pendingFrame = (k) => JSON.stringify({ type: "pending", ...cur.get(k), serverNowMs: Date.now() });
779
+ durableLane.tick(pending);
780
+ liveLane.tick(livePending);
692
781
  if (first) {
693
- for (const k of curFrames.keys())
694
- res.write(`event: pending\ndata: ${pendingFrame(k)}\n\n`);
695
782
  res.write(`event: synced\ndata: ${JSON.stringify({ type: "synced", count: pending.length })}\n\n`);
696
783
  first = false;
697
784
  }
698
- else {
699
- for (const [k, frame] of curFrames)
700
- if (prevFrames.get(k) !== frame)
701
- res.write(`event: pending\ndata: ${pendingFrame(k)}\n\n`);
702
- for (const [k, p] of prev)
703
- if (!cur.has(k))
704
- res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId, serverNowMs: Date.now() })}\n\n`);
705
- }
706
- prev = cur;
707
- prevFrames = curFrames;
708
785
  if (Date.now() - start > SSE_MAX_STREAM_MS) {
709
786
  res.write(`event: error\ndata: ${JSON.stringify({ type: "error", errorCode: "STREAM_MAX_DURATION", message: "approvals stream reached its 15-minute cap — reconnect to continue" })}\n\n`);
710
787
  break;
@@ -14,6 +14,7 @@ import { projectWriteProtectionCapability } from "../../write-protection.js";
14
14
  import { projectSqlEngineCapability } from "../../sql-engine-posture.js";
15
15
  import { projectWebSearchCapability } from "../../plugins/web-search.js";
16
16
  import { projectExecutionLaneCapability } from "../../capabilities/execution-lane.js";
17
+ import { projectApprovalsStreamLiveCapability } from "../../capabilities/approvals-push.js";
17
18
  import { workflowModelAllowlistFor, selfOrchestrationDenial } from "../../task-workflow.js";
18
19
  import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
19
20
  import { judgeAutoModeArming } from "../../auto-mode-face.js";
@@ -138,6 +139,7 @@ export async function handleCapabilities(req, res, match, ctx) {
138
139
  mcpElicitation: Boolean(deps.elicitation),
139
140
  askUserQuestion: Boolean(deps.question),
140
141
  toolApproval: Boolean(deps.toolApproval),
142
+ approvalsStreamLive: projectApprovalsStreamLiveCapability(deps),
141
143
  streamApproval: resolveStreamApprovalGate({
142
144
  toolApprovalEnabled: Boolean(deps.toolApproval),
143
145
  streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
@@ -40,7 +40,7 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
40
40
  const visW = (r) => (callerScope === null || r.scope === callerScope) && sessionOk(r.sessionId);
41
41
  const SCOPED_TASK_KEY_DISPOSITION = {
42
42
  id: "keep", parentId: "keep", parentToolCallId: "keep", workflowRunId: "keep", status: "keep",
43
- startedAt: "keep", elapsedMs: "keep", tokens: "keep", queuedCount: "keep",
43
+ startedAt: "keep", elapsedMs: "keep", tokens: "keep",
44
44
  awaitingPlanApproval: "keep", toolUses: "keep", usage: "keep", resumable: "keep", sourceLane: "keep",
45
45
  cycleSeq: "keep", retiredBy: "keep",
46
46
  name: "rewrite", scope: "strip", sessionId: "strip",
@@ -61,7 +61,6 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
61
61
  ...(rest.startedAt !== undefined ? { startedAt: rest.startedAt } : {}),
62
62
  ...(rest.elapsedMs !== undefined ? { elapsedMs: rest.elapsedMs } : {}),
63
63
  ...(rest.tokens !== undefined ? { tokens: rest.tokens } : {}),
64
- ...(rest.queuedCount !== undefined ? { queuedCount: rest.queuedCount } : {}),
65
64
  ...(rest.awaitingPlanApproval !== undefined ? { awaitingPlanApproval: rest.awaitingPlanApproval } : {}),
66
65
  ...(rest.toolUses !== undefined ? { toolUses: rest.toolUses } : {}),
67
66
  ...(rest.usage !== undefined ? { usage: rest.usage } : {}),
@@ -1,5 +1,5 @@
1
1
  import { persistedPlaneOf } from "../../terminal.js";
2
- import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventData, redactTerminalResult } from "../../observability/run-terminal-log.js";
2
+ import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventData, redactTerminalResult } from "../../observability/run-terminal.js";
3
3
  import { isModelTextLedgerEventType } from "../../trace/ledger-events.js";
4
4
  import { z } from "zod";
5
5
  import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer } from "@sema-agent/core";
@@ -89,7 +89,7 @@ async function subagentReadGate(req, res, ctx, runId, search) {
89
89
  }
90
90
  function sendHandleMiss(res, run, thisInstanceId) {
91
91
  if (isTerminalRunStatus(run.status)) {
92
- sendError(res, 404, "not_found.task_owner_settled", "no such background task handle here. Routing hint about its OWNER (not about this handle): the run has already settled, and background handles live in the replica-local registry for the parent's lifetime, so none of its handles are readable any more. Read the run's own outcome via GET /v1/runs/:id");
92
+ sendError(res, 404, "not_found.task_owner_settled", "no such background task handle here. Routing hint about its OWNER (not about this handle): the run has already settled. Read the run's own outcome via GET /v1/runs/:id");
93
93
  return true;
94
94
  }
95
95
  if (run.status === "running" && runProvenElsewhere(run, thisInstanceId)) {
@@ -1,4 +1,4 @@
1
- import { redactErrorMessage, redactHead, redactTerminalResult } from "../../observability/run-terminal-log.js";
1
+ import { redactErrorMessage, redactHead, redactTerminalResult } from "../../observability/run-terminal.js";
2
2
  import { runWithVerification, runCascade, uuidv7, terminalProjection } from "@sema-agent/core";
3
3
  import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, resumeAtHttpStatus, HEARTBEAT_MS } from "../../runs.js";
4
4
  import { stripResumeToken } from "../../terminal.js";
@@ -414,7 +414,7 @@ export async function handleTasks(req, res, match, ctx) {
414
414
  finalizeTaskResult(ev.result, principal, prepared.spec.objective, prepared.spec.sessionId);
415
415
  noteRunOutcome(finalResult);
416
416
  putRewindAnchor();
417
- settleFleet(finalResult ? terminalProjection(finalResult.terminal).status : "completed", fleetRunResiduals(finalResult));
417
+ settleFleet(terminalProjection(finalResult.terminal).status, fleetRunResiduals(finalResult));
418
418
  if (ledgerSink && finalResult) {
419
419
  if (usageKey && deps.modelUsage && deps.runStore && durableTaskId) {
420
420
  const rs2 = deps.runStore;
@@ -5,7 +5,7 @@ import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from ".
5
5
  import { journalEntryOfRow, journalEntryPayload, journalRowPayloadOf } from "../../orchestration/workflow-journal-entry.js";
6
6
  import { redactSecrets } from "../../trace/redact.js";
7
7
  import { createHash } from "node:crypto";
8
- import { redactErrorMessage, redactHead } from "../../observability/run-terminal-log.js";
8
+ import { redactErrorMessage, redactHead } from "../../observability/run-terminal.js";
9
9
  const JOURNAL_PROJECTION_MEMO_CAP = 1024;
10
10
  const journalProjectionMemo = new Map();
11
11
  import { sendJson, sendError, sseHeaders } from "../send.js";
@@ -28,8 +28,8 @@ import type { ModelUsageTracker, PromptManifestTracker } from "../budget.js";
28
28
  import { scopedIdempotencyKey } from "./idempotency.js";
29
29
  export { scopedIdempotencyKey };
30
30
  import type { ConsolidationSeatLabel } from "../boot/memory-consolidation.js";
31
- import { streamApprovals, isQuestionAnswer } from "./routes/approvals-assistant.js";
32
- export { streamApprovals, isQuestionAnswer };
31
+ import { streamApprovals, checkQuestionAnswer } from "./routes/approvals-assistant.js";
32
+ export { streamApprovals, checkQuestionAnswer };
33
33
  import { cascadeConfig } from "./run-meta.js";
34
34
  export { cascadeConfig };
35
35
  import { coarseStatusForState, errorCodeForExit } from "./routes/images.js";
@@ -1,5 +1,5 @@
1
1
  import http from "node:http";
2
- import { redactHead } from "../observability/run-terminal-log.js";
2
+ import { redactHead } from "../observability/run-terminal.js";
3
3
  import { validatePendingSteer, terminalProjection } from "@sema-agent/core";
4
4
  import {} from "../config-center/facade.js";
5
5
  import { HttpError } from "../security.js";
@@ -12,8 +12,8 @@ import {} from "../tool-approval.js";
12
12
  import {} from "../observability/rate-limit.js";
13
13
  import { IdempotencyCache, scopedIdempotencyKey } from "./idempotency.js";
14
14
  export { scopedIdempotencyKey };
15
- import { streamApprovals, isQuestionAnswer } from "./routes/approvals-assistant.js";
16
- export { streamApprovals, isQuestionAnswer };
15
+ import { streamApprovals, checkQuestionAnswer } from "./routes/approvals-assistant.js";
16
+ export { streamApprovals, checkQuestionAnswer };
17
17
  import { cascadeConfig } from "./run-meta.js";
18
18
  export { cascadeConfig };
19
19
  import { coarseStatusForState, errorCodeForExit } from "./routes/images.js";
@@ -1,4 +1,4 @@
1
- import { redactHead } from "../observability/run-terminal-log.js";
1
+ import { redactHead } from "../observability/run-terminal.js";
2
2
  export const DEFAULT_MAX_DIFF_BYTES = 5 * 1024 * 1024;
3
3
  const DEFAULT_READ_TIMEOUT_MS = 30_000;
4
4
  const SAFE_REV = /^[A-Za-z0-9._/-]{1,200}$/;
@@ -1,5 +1,5 @@
1
1
  import { uuidv7 } from "@sema-agent/core";
2
- import { redactErrorMessage, redactHead, redactLeaderResult, redactLeaderResultWithFlag } from "../observability/run-terminal-log.js";
2
+ import { redactErrorMessage, redactHead, redactLeaderResult, redactLeaderResultWithFlag } from "../observability/run-terminal.js";
3
3
  import { withPrincipal } from "../observability/principal-context.js";
4
4
  import { leaderIdemKey } from "../plugins/leader-run-store-sql.js";
5
5
  function statusForLeaderResult(result) {
@@ -1,5 +1,5 @@
1
1
  import { terminalProjection } from "@sema-agent/core";
2
- import { redactErrorMessage, redactWorkerReportError } from "../observability/run-terminal-log.js";
2
+ import { redactErrorMessage, redactWorkerReportError } from "../observability/run-terminal.js";
3
3
  import { pausedCauseOf } from "../terminal.js";
4
4
  class Semaphore {
5
5
  max;
@@ -1,5 +1,5 @@
1
1
  import { fanOut } from "./fanout.js";
2
- import { redactErrorMessage, redactHead } from "../observability/run-terminal-log.js";
2
+ import { redactErrorMessage, redactHead } from "../observability/run-terminal.js";
3
3
  import { pullDiff } from "./diffout.js";
4
4
  import { mergeBranches } from "./merge.js";
5
5
  import { Runner, runWithVerification, resumeWithVerification, terminalProjection } from "@sema-agent/core";
@@ -1,5 +1,5 @@
1
1
  import { delimitUntrusted } from "@sema-agent/core";
2
- import { OVERSIZED_DIAG_PLACEHOLDER, redactArtifactText, redactErrorMessage, redactHead, redactTail } from "../observability/run-terminal-log.js";
2
+ import { OVERSIZED_DIAG_PLACEHOLDER, redactArtifactText, redactErrorMessage, redactHead, redactTail } from "../observability/run-terminal.js";
3
3
  import { applyAndGrade, changedFilesOutOfAllowlist } from "./grader-env-factory.js";
4
4
  import { shellQuote as Q } from "../plugins/remote-shell.js";
5
5
  const CONFLICT_FILES_MAX = 200;
@@ -1,4 +1,4 @@
1
- import { redactErrorMessage, redactHead, redactTail } from "../observability/run-terminal-log.js";
1
+ import { redactErrorMessage, redactHead, redactTail } from "../observability/run-terminal.js";
2
2
  export function economicGuard(objective, opts = {}) {
3
3
  if (opts.fanoutEnabled === false)
4
4
  return { single: true, reason: "fan-out disabled by deployment (A6)" };
@@ -1,7 +1,7 @@
1
1
  import { runExecGate, } from "@sema-agent/core";
2
2
  import { createHash } from "node:crypto";
3
3
  import { shellQuote as sq } from "../plugins/remote-shell.js";
4
- import { redactHead, redactTail } from "../observability/run-terminal-log.js";
4
+ import { redactHead, redactTail } from "../observability/run-terminal.js";
5
5
  const sha = (s) => createHash("sha256").update(s).digest("hex").slice(0, 16);
6
6
  function changedFilesOf(patch) {
7
7
  const out = new Set();
@@ -1,7 +1,7 @@
1
1
  import { runRepairLoop, delimitUntrusted, } from "@sema-agent/core";
2
2
  import { mkRepairOracle } from "./repair-oracle.js";
3
3
  import { terminalProjection } from "@sema-agent/core";
4
- import { redactHead } from "../observability/run-terminal-log.js";
4
+ import { redactHead } from "../observability/run-terminal.js";
5
5
  function statusForTerminal(t) {
6
6
  return t === "fixed" || t === "candidate_only" ? "completed" : "failed";
7
7
  }
@@ -16,7 +16,7 @@ import { attachRepairLoopDeps } from "./repair-wire.js";
16
16
  import { routePlanWithFallback, validateSubtasks } from "./planner.js";
17
17
  import { createEngineNoticeSeat } from "../boot/runner-deps.js";
18
18
  import { currentPrincipal } from "../observability/principal-context.js";
19
- import { redactHead } from "../observability/run-terminal-log.js";
19
+ import { redactHead } from "../observability/run-terminal.js";
20
20
  const REPAIR_LOOP_WIRED = true;
21
21
  const sh = (env) => async (cmd) => {
22
22
  const r = await env.exec(`bash -lc ${JSON.stringify(cmd)}`);
@@ -0,0 +1,77 @@
1
+ /**
2
+ * S-461 —— **模型 provider 词表的唯一属主**(闭集 + 路由腿的穷举判词 + 域外拒因)。
3
+ *
4
+ * ## 病
5
+ * `MODEL_PROVIDER` 此前收**任意字符串**(`config.ts` 里就是一句 `process.env.MODEL_PROVIDER`),而消费它的
6
+ * 只有一处「非 anthropic 即 gateway」的二分。于是最自然的误填 —— 把**厂商名**写成 provider,
7
+ * `MODEL_PROVIDER=deepseek` —— 起得来、静默走网关栈(实测改前读数:`model.provider="deepseek"`、
8
+ * `api="openai-completions"`)。运维以为自己点了一条专用路线,拿到的是默认腿,而读数上没有任何异常。
9
+ * 这与 [ref] 件⑤ / S-188 的三根模型线路旋钮是**同一条族律**([ref] A 档:坏值拒启,不静默折默认)。
10
+ *
11
+ * ## 词源:取不到型,就取那句自述 + 一格钉
12
+ * 词表的上游属主是 `@sema-agent/settings-schema` 的 `ModelEntry.provider`。它的 zod 形是
13
+ * `z.string().min(1).default("gateway")` —— **没有可取的型**(与 `RemoteExecProvider` / `WebSearchProvider`
14
+ * 那两张能取型的表不同),但它的 JSDoc 逐字写着三词与各自的腿:
15
+ * > Routes the brain in the service (createRoutingBrain by provider): "gateway"/"vllm" → OpenAI brain,
16
+ * > "anthropic" → Anthropic brain.
17
+ * 于是本仓建闭集常量,并由 `test/config-coverage-model-plane.test.ts` 的 S-461 ④ 格**钉住它与那句自述同集**
18
+ * (读的是装载的那份 dist,不手抄第二份词表)。上游哪天把词表写进 zod enum,那一格会告诉我们该改成取型。
19
+ *
20
+ * ## 三条腿各自的边界(刻意分开写)
21
+ * · {@link isModelProvider} / {@link modelProviderRefusal} —— **env 面**的闭集判据与拒因。它管的是
22
+ * `MODEL_PROVIDER`(以及缺省推断产出的那个词),执法点在 `config.ts` 的模型面装配 + `config-invariants.ts`
23
+ * 的 `model-provider-word` 条(热路径同判,不是第二份判据);
24
+ * · {@link modelBrainLane} —— **穷举 switch**([ref]:词表是闭集,加一词不补臂 = 编译红);
25
+ * · {@link catalogBrainLane} —— **目录词**那一侧。见它自己的头注:目录条目的 provider **不过** boot 闸,
26
+ * 所以那里必须有一条「认不出」的臂,而那条臂是**既有语义的书面化**,不是本批新开的兜底。
27
+ */
28
+ /**
29
+ * `MODEL_PROVIDER` 的闭集。**声明序 = 语义序**:两条 OpenAI 兼容腿在前,Anthropic 腿在后
30
+ * (与上游那句自述的行文同序,方便逐字对读)。
31
+ */
32
+ export declare const MODEL_PROVIDER_WORDS: readonly ["gateway", "vllm", "anthropic"];
33
+ /** 闭集成员型。加一词 ⇒ {@link modelBrainLane} 的穷举 switch 当场编译红。 */
34
+ export type ModelProvider = (typeof MODEL_PROVIDER_WORDS)[number];
35
+ /** 这个词在闭集里吗(`undefined` / 空串一律 false —— 「未设」不是一个 provider,由调用点自己折缺省)。 */
36
+ export declare function isModelProvider(word: string | undefined): word is ModelProvider;
37
+ /**
38
+ * 域外值的**拒因**(`undefined` = 这个词合法)。
39
+ *
40
+ * 🔴 **机读码 `config.model_provider_invalid` 逐字在句子里**(clay 令 2026-09-18):拒启原话是运维与壳
41
+ * 唯一拿得到的东西,一句纯散文只能靠人读。码的拼法随 `config.<键名蛇形>_invalid` 既有族
42
+ * (`config.retention_policy_invalid` / `config.execution_env_home_dir_invalid` / `config.placement_root_invalid`),
43
+ * 方括号形与本仓既有那一处(`plugins/remote-env-device.ts`)逐字同形。**出现位置**:boot 期 `loadConfig()`
44
+ * 抛出的 Error.message ⇒ 进程以非零退出,这句话落在 **stderr 首屏**(未捕获异常的栈首行)。
45
+ *
46
+ * 拒因三件齐:**码** + **违例值**(照原样带引号,前后空格/大小写一眼可见) + **闭集三词**。
47
+ */
48
+ export declare function modelProviderRefusal(word: string): string | undefined;
49
+ /** 一只 provider 词走哪条 brain 腿。闭集 ⇒ 两个值,没有第三种。 */
50
+ export type ModelBrainLane = "anthropic" | "gateway";
51
+ /**
52
+ * **穷举判词**([ref]:词表是闭集,加一词而不补臂是 COMPILE 错,不是运行期漏判)。
53
+ *
54
+ * `"vllm"` 与 `"gateway"` 同腿**不是**兜底,是上游那句自述的逐字转写(两者都是 OpenAI 兼容端点,
55
+ * 差别在部署形而不在协议);它们哪天要分家,分的是这张表,而不是某个调用点的 `if`。
56
+ */
57
+ export declare function modelBrainLane(provider: ModelProvider): ModelBrainLane;
58
+ /**
59
+ * **目录条目**(`config.models[*].provider`)那一侧的判词。
60
+ *
61
+ * 🔴 为什么它与 {@link modelBrainLane} 不是同一只函数:两者的**输入保证不同**。
62
+ * · `MODEL_PROVIDER` 走 boot 闸(域外拒启)⇒ 到达 {@link modelBrainLane} 的恒是闭集词;
63
+ * · **目录条目**来自 settings-schema 的 `z.string()`,可以从 config-center **热应用**进来 —— 那条路上
64
+ * 没有、也不该有「拒启」这回事(一次热发布不能让在跑的副本死),所以这里必然存在一条「认不出」的臂。
65
+ *
66
+ * 🔴 那条臂**就是今天的语义,本批一个字节都没改**([ref]「不可避免的运行期 miss 臂必须说出来」):
67
+ * core 的 `createRoutingBrain({anthropic}, {fallback: gatewayStack})` 本身就把一切非 `anthropic` 的词
68
+ * 路由到网关栈(`brain.ts` 顶注那句 "everything else → the local gateway stack (also the fallback for an
69
+ * unrecognized provider)" 是它的原话)。本函数做的只是把那句散文变成**一个有名字的判词**,于是
70
+ * 「目录里写了个没人认识的 provider」这件事在代码里有一处可指的坐标,而不是散在两个 `=== "anthropic"` 里。
71
+ *
72
+ * ⚠️ **未闭环**(S-461 收车档登记):目录词今天仍然**无声**地落进网关腿。要让它也响亮,需要的是一条
73
+ * **热路径上的**告警/拒收(config-center 应用期逐条校验 + 运维可读的拒因),那是另一件事、另一台车 ——
74
+ * 在这里加一个 throw 会让一次热发布打死在跑的副本,方向反了。
75
+ */
76
+ export declare function catalogBrainLane(provider: string | undefined): ModelBrainLane;
77
+ //# sourceMappingURL=model-provider.d.ts.map
@@ -0,0 +1,26 @@
1
+ export const MODEL_PROVIDER_WORDS = ["gateway", "vllm", "anthropic"];
2
+ export function isModelProvider(word) {
3
+ return word !== undefined && MODEL_PROVIDER_WORDS.includes(word);
4
+ }
5
+ export function modelProviderRefusal(word) {
6
+ if (isModelProvider(word))
7
+ return undefined;
8
+ return (`env MODEL_PROVIDER="${word}" is not a known model provider [config.model_provider_invalid] — ` +
9
+ `valid: ${MODEL_PROVIDER_WORDS.join(" | ")}. ` +
10
+ "An unknown word used to start FINE and silently route to the gateway stack (a vendor name like " +
11
+ '"deepseek" in this slot is the natural mis-fill), so the operator got the default leg while believing ' +
12
+ "they had pinned a dedicated one — nothing in the readings showed it (#210 A 档:坏值拒启,不静默折默认).");
13
+ }
14
+ export function modelBrainLane(provider) {
15
+ switch (provider) {
16
+ case "anthropic":
17
+ return "anthropic";
18
+ case "gateway":
19
+ case "vllm":
20
+ return "gateway";
21
+ }
22
+ }
23
+ export function catalogBrainLane(provider) {
24
+ return isModelProvider(provider) ? modelBrainLane(provider) : "gateway";
25
+ }
26
+ //# sourceMappingURL=model-provider.js.map