@sema-agent/server 7.67.0 → 7.69.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.
Files changed (71) hide show
  1. package/USAGE.md +19 -2
  2. package/dist/adoption/plan.js +11 -0
  3. package/dist/approval-card.d.ts +26 -0
  4. package/dist/approval-card.js +11 -0
  5. package/dist/approval.js +5 -4
  6. package/dist/boot/config-center.d.ts +6 -7
  7. package/dist/boot/config-center.js +31 -8
  8. package/dist/boot/leader.js +1 -1
  9. package/dist/boot/resolve-spec.js +1 -1
  10. package/dist/boot/workflow-orchestration.d.ts +2 -0
  11. package/dist/boot/workflow-orchestration.js +10 -2
  12. package/dist/budget.js +1 -1
  13. package/dist/config-catalog.js +20 -1
  14. package/dist/config-center/apply-effective.d.ts +8 -1
  15. package/dist/config-center/apply-effective.js +23 -9
  16. package/dist/config-center/facade.d.ts +1 -1
  17. package/dist/config-center/facade.js +1 -1
  18. package/dist/config-center/read-face.d.ts +30 -1
  19. package/dist/config-center/read-face.js +33 -15
  20. package/dist/config-center/restart-signal.js +2 -1
  21. package/dist/config-center/types.d.ts +20 -5
  22. package/dist/config-provider.js +1 -0
  23. package/dist/config-types.d.ts +17 -2
  24. package/dist/config.d.ts +0 -21
  25. package/dist/config.js +19 -6
  26. package/dist/http/route-ctx.d.ts +44 -2
  27. package/dist/http/routes/approvals-assistant.js +1 -0
  28. package/dist/http/routes/capabilities.js +1 -0
  29. package/dist/http/routes/runs.js +1 -1
  30. package/dist/http/routes/tasks.js +1 -0
  31. package/dist/http/routes/workflows.js +15 -7
  32. package/dist/http/server.d.ts +15 -2
  33. package/dist/http/server.js +74 -45
  34. package/dist/leader/wire.d.ts +9 -2
  35. package/dist/leader/wire.js +6 -6
  36. package/dist/main.js +6 -5
  37. package/dist/mcp-tool-faces.js +11 -2
  38. package/dist/model-compat.d.ts +81 -0
  39. package/dist/model-compat.js +52 -0
  40. package/dist/observability/fail-open.d.ts +4 -0
  41. package/dist/observability/fail-open.js +4 -0
  42. package/dist/operator-ask.d.ts +46 -0
  43. package/dist/operator-ask.js +4 -0
  44. package/dist/orchestration/workflow-agent-session-index.d.ts +96 -0
  45. package/dist/orchestration/workflow-agent-session-index.js +146 -0
  46. package/dist/orchestration/workflow-journal-entry.d.ts +78 -0
  47. package/dist/orchestration/workflow-journal-entry.js +20 -0
  48. package/dist/orchestration/workflow-notify-journal.d.ts +43 -1
  49. package/dist/orchestration/workflow-notify-journal.js +43 -3
  50. package/dist/parked-decide.d.ts +115 -5
  51. package/dist/parked-decide.js +94 -26
  52. package/dist/plugins/pg-pool.js +8 -0
  53. package/dist/plugins/store-backend.d.ts +5 -0
  54. package/dist/plugins/store-backend.js +3 -1
  55. package/dist/plugins/tidb-pool.js +8 -0
  56. package/dist/plugins/workflow-journal-store-sql.js +10 -3
  57. package/dist/plugins/workflow-run-store-sql.d.ts +30 -0
  58. package/dist/plugins/workflow-run-store-sql.js +40 -0
  59. package/dist/router/route-orchestration.js +2 -1
  60. package/dist/run-local.js +1 -1
  61. package/dist/runs.d.ts +10 -1
  62. package/dist/runs.js +4 -1
  63. package/dist/runtime-governance.js +5 -4
  64. package/dist/task-mcp.d.ts +50 -6
  65. package/dist/task-mcp.js +71 -35
  66. package/dist/tool-approval.d.ts +20 -0
  67. package/dist/tool-approval.js +2 -1
  68. package/dist/trace/core-keyset-guard.d.ts +3 -3
  69. package/dist/trace/engine-notice-wire.d.ts +17 -1
  70. package/dist/trace/engine-notice-wire.js +7 -1
  71. package/package.json +4 -4
@@ -1,10 +1,11 @@
1
- import { compileReadDeny, resolveReadDenyBuiltins } from "@sema-agent/core";
1
+ import { compileReadDeny, isSwappableDepSeat, resolveReadDenyBuiltins } from "@sema-agent/core";
2
2
  const KEY_MAP = [
3
3
  { centerKey: "face", cfgKey: "readFace", envHolds: (c) => c.readFaceSource === "env" },
4
4
  { centerKey: "denyPatterns", cfgKey: "readDenyPatterns", envHolds: (c) => c.readDenyPatterns !== undefined },
5
5
  { centerKey: "denyBuiltinTiers", cfgKey: "readDenyBuiltinTiers", envHolds: (c) => c.readDenyBuiltinTiers !== undefined },
6
6
  { centerKey: "denyBuiltinExclude", cfgKey: "readDenyBuiltinExclude", envHolds: (c) => c.readDenyBuiltinExclude !== undefined },
7
7
  ];
8
+ export const CENTER_READ_FACE_LIVE_KEYS = new Set(KEY_MAP.filter((r) => isSwappableDepSeat(r.cfgKey)).map((r) => r.centerKey));
8
9
  class ReadFaceDomainError extends Error {
9
10
  }
10
11
  function normalizeDenyPatterns(raw) {
@@ -42,17 +43,27 @@ function normalizeNameList(field, raw) {
42
43
  return n.trim();
43
44
  });
44
45
  }
45
- export function applyCenterReadFace(config, eff, logger) {
46
+ export function applyCenterReadFace(config, eff, logger, apply = { phase: "boot" }) {
46
47
  const envHeldKeys = KEY_MAP.filter((r) => r.envHolds(config)).map((r) => r.centerKey);
48
+ const faceBefore = config.readFace;
49
+ const report = () => ({ envHeld: envHeldKeys, faceChanged: config.readFace !== faceBefore });
50
+ const revertFaceToBaseline = () => {
51
+ if (apply.phase !== "live")
52
+ return;
53
+ config.readFace = apply.baseline.readFace;
54
+ config.readFaceSource = apply.baseline.readFaceSource;
55
+ };
47
56
  const domain = eff.readFace;
48
- if (domain === undefined || domain === null)
49
- return envHeldKeys;
57
+ if (domain === undefined || domain === null) {
58
+ revertFaceToBaseline();
59
+ return report();
60
+ }
50
61
  if (typeof domain !== "object" || Array.isArray(domain)) {
51
62
  logger?.warn("sema_registry_read_face_invalid", {
52
63
  detail: `published readFace domain must be an object with optional {face,denyPatterns,denyBuiltinTiers,denyBuiltinExclude} — got ${Array.isArray(domain) ? "array" : typeof domain}`,
53
64
  kept: "the deployment's env READ-face posture is unchanged (refusing a malformed domain WHOLE — a half-applied read-containment posture is worse than none)",
54
65
  });
55
- return envHeldKeys;
66
+ return report();
56
67
  }
57
68
  const d = domain;
58
69
  const envHeld = KEY_MAP.filter((r) => d[r.centerKey] !== undefined && r.envHolds(config)).map((r) => `${r.cfgKey} (readFace.${r.centerKey})`);
@@ -83,18 +94,23 @@ export function applyCenterReadFace(config, eff, logger) {
83
94
  detail: e instanceof Error ? e.message : String(e),
84
95
  kept: "the deployment's env READ-face posture is unchanged (refusing the readFace domain WHOLE — applying half of a read-containment posture would read as 'policy landed' while part of it silently did not)",
85
96
  });
86
- return envHeldKeys;
97
+ return report();
87
98
  }
88
99
  if (staged.readFace !== undefined) {
89
100
  config.readFace = staged.readFace;
90
101
  config.readFaceSource = "center";
91
102
  }
92
- if (staged.readDenyPatterns !== undefined)
93
- config.readDenyPatterns = staged.readDenyPatterns;
94
- if (staged.readDenyBuiltinTiers !== undefined)
95
- config.readDenyBuiltinTiers = staged.readDenyBuiltinTiers;
96
- if (staged.readDenyBuiltinExclude !== undefined)
97
- config.readDenyBuiltinExclude = staged.readDenyBuiltinExclude;
103
+ else {
104
+ revertFaceToBaseline();
105
+ }
106
+ if (apply.phase === "boot") {
107
+ if (staged.readDenyPatterns !== undefined)
108
+ config.readDenyPatterns = staged.readDenyPatterns;
109
+ if (staged.readDenyBuiltinTiers !== undefined)
110
+ config.readDenyBuiltinTiers = staged.readDenyBuiltinTiers;
111
+ if (staged.readDenyBuiltinExclude !== undefined)
112
+ config.readDenyBuiltinExclude = staged.readDenyBuiltinExclude;
113
+ }
98
114
  if (envHeld.length > 0) {
99
115
  logger?.warn("sema_registry_read_face_env_wins", {
100
116
  keys: envHeld,
@@ -103,11 +119,13 @@ export function applyCenterReadFace(config, eff, logger) {
103
119
  }
104
120
  if (Object.keys(staged).length > 0) {
105
121
  logger?.info("sema_registry_read_face", {
106
- applied: Object.keys(staged),
122
+ applied: Object.keys(staged).filter((k) => apply.phase === "boot" || KEY_MAP.some((r) => r.cfgKey === k && CENTER_READ_FACE_LIVE_KEYS.has(r.centerKey))),
107
123
  ...(staged.readFace !== undefined ? { face: staged.readFace } : {}),
108
- note: "restart-to-apply — these keys are captured when the runner deps are assembled at boot",
124
+ note: apply.phase === "boot"
125
+ ? "boot phase — every seat still bakes into the runner deps that are about to be assembled"
126
+ : "live phase — the read FACE is hot-swapped into every boot Runner (core swapDeps seat); the deny seats stay boot-baked and keep asking for the `read-face` restart slice",
109
127
  });
110
128
  }
111
- return envHeldKeys;
129
+ return report();
112
130
  }
113
131
  //# sourceMappingURL=read-face.js.map
@@ -1,5 +1,6 @@
1
1
  import { resolveActiveTiers } from "@sema-agent/settings-schema";
2
2
  import { resolveDefaultModelName } from "./apply-effective.js";
3
+ import { CENTER_READ_FACE_LIVE_KEYS } from "./read-face.js";
3
4
  const RESTART_SLICES = ["skills", "mcp", "a2a", "scenarios", "runtime-gates", "models-tiers", "degrade-route", "read-face"];
4
5
  export function stableStringify(v) {
5
6
  if (v === null || typeof v !== "object")
@@ -55,7 +56,7 @@ function restartSliceValue(eff, slice, ctx) {
55
56
  if (!d)
56
57
  return null;
57
58
  const held = new Set(ctx?.readFaceEnvHeld ?? []);
58
- const view = Object.entries(d).filter(([k, v]) => !held.has(k) && v !== undefined);
59
+ const view = Object.entries(d).filter(([k, v]) => !held.has(k) && !CENTER_READ_FACE_LIVE_KEYS.has(k) && v !== undefined);
59
60
  return view.length > 0 ? Object.fromEntries(view) : null;
60
61
  }
61
62
  case "degrade-route": {
@@ -47,11 +47,26 @@ export interface CenterModel {
47
47
  * 记账时固化进 UsageEntry.{weightedTokens,quotaWeightAtUse} 与 lease weightedTokens 轴;改倍率只影响
48
48
  * 此后记账,不回算窗口内历史(registry types.d.ts 同句契约)。 */
49
49
  quotaWeight?: number;
50
- /** [ref] L2②: per-model declarative thinking config (center roster). defaultThinking = this model's DEFAULT
51
- * tier (resolution chain spec.thinking ?? role.thinking ?? model.defaultThinking); reasoningEffortLevels (openai
52
- * endpoints) = tiers the endpoint accepts. Validated against core's ThinkingLevel in toModel; "off" is dropped. */
53
- defaultThinking?: string;
54
- reasoningEffortLevels?: string[];
50
+ /**
51
+ * S-188(settings-schema 1.10.0 `ModelEntry.compat`)—— 这条目录条目对 **openai-completions 线路**的
52
+ * 兼容声明,上 core `Model.compat`(`OpenAICompletionsCompat` 五键)。真需求 = vLLM / Qwen 类**缺省开
53
+ * 思考**的网关:关思考只能靠 `thinkingFormat: "qwen-chat-template"`(`chat_template_kwargs.
54
+ * enable_thinking=false`),而 `extraBody` 穿不过引擎的 OpenAI 保留键表。
55
+ *
56
+ * 🔴 类型是 `unknown` 而不是那个结构,**刻意的**(与 {@link CenterMcpServer.toolFaces} 逐字同姿):本类型
57
+ * 是两条腿的**承载形**(本地 config.d 经 settings-schema 的 `z.unknown()` 解析透明地过来、远端 /effective
58
+ * 是直读 JSON),判据与坏形处置的单一属主是 `model-compat.ts` 的 `readModelCompat`(`toModel` 在铸 `Model`
59
+ * 那一刻调它)。在承载形上先写一个结构,等于把同一张判据表抄成两份(而其中一份没有执行力)。
60
+ *
61
+ * ⚠️ 前身两键 `defaultThinking` / `reasoningEffortLevels` 已随本件删除:它们从来没有声明面 ——
62
+ * settings-schema 的 `ModelEntry` 里没有这两个键,**两条腿**(本地 `parseDomainLoud` / 远端
63
+ * `readEffectiveWire`)都把它们当未知键剥掉(1.10.0 夹具实测:`unknown-keys-dropped
64
+ * {keys:["models.0.defaultThinking","models.0.reasoningEffortLevels"]}`),`mapToServiceEffective` 的
65
+ * CLOSED 投影也从未列过它们。留着 = 一份读不到任何东西的类型承诺 + `compat.reasoningEffortLevels`
66
+ * 之外的第二个写者([ref]:同一语义面禁两个写者)。roster 级 `defaultThinking` 若要真做,归 schema 补键
67
+ * 另立单据(见 CHANGELOG 未闭环)。
68
+ */
69
+ compat?: unknown;
55
70
  /** Generation-tuning passthrough, mirrors core `Model.extraBody` (1.60) — e.g. frequency/presence
56
71
  * penalties that *prevent* degenerate looping. Same category as `cost`/`tier`: logical model metadata,
57
72
  * no secrets. Carried through `toModel()` verbatim. When a center roster replaces the env model this is
@@ -114,6 +114,7 @@ export function mapToServiceEffective(eff, version) {
114
114
  ...(typeof m.charsPerToken === "number" ? { charsPerToken: m.charsPerToken } : {}),
115
115
  ...(m.cost !== undefined ? { cost: m.cost } : {}),
116
116
  ...(m.extraBody !== undefined ? { extraBody: m.extraBody } : {}),
117
+ ...(m.compat !== undefined ? { compat: m.compat } : {}),
117
118
  ...(m.promptGuidance !== undefined ? { promptGuidance: m.promptGuidance } : {}),
118
119
  enabled: m.enabled,
119
120
  })),
@@ -330,8 +330,23 @@ export interface ServiceConfigFlat {
330
330
  /** How long the breaker stays open before a half-open probe (ms). */
331
331
  cooldownMs: number;
332
332
  };
333
- /** Default model the service runs tasks on. */
333
+ /** Default model the service runs tasks on. **活的** —— center 热应用会整只换掉它
334
+ * (`config.model = config.models.default!`,config-center/apply-effective.ts 的 commit 段)。 */
334
335
  model: Model;
336
+ /**
337
+ * S-188 —— **boot 期 env 派生的那只模型**,`loadConfig()` 铸一次,此后**永不换**(与 `model` 在 boot
338
+ * 那一刻是同一个对象引用;center 热应用只动 `model`/`models`,不碰这里)。
339
+ *
340
+ * 用途只有一个:**BL-8 继承源** —— 一条 center 目录条目省掉 `maxTokens` / `contextWindow` /
341
+ * `reasoning` / `charsPerToken` / `defaultThinking` / `compat` 时,继承的是**运维在 env 里配的值**。
342
+ * 读活的 `model` 当继承源是错的(codex 对抗复审 r1 [high] 真复现):首次 commit 之后它已经是**上一代
343
+ * center 的默认模型**,于是 ①一条条目上的值会成为下一代所有条目的缺省(跨条目污染)②把那个值删掉
344
+ * 也撤不回来(残留到进程重启)—— 两条都与这几个键成文的语义(「inherits the operator's env values」)相反。
345
+ *
346
+ * 🔴 **必填、无可选形**:一个 `?` 会立刻招来一条「缺席就回落 `config.model`」的臂,而那条臂正是本键
347
+ * 要消灭的东西([ref]:同一语义面禁两个写者)。测试夹具与 `loadConfig` 一样,与 `model` 同源铸。
348
+ */
349
+ envModel: Model;
335
350
  /** Model catalog by name (1.24): always `default` + the main model's id, plus an optional cheap model.
336
351
  * Used to resolve roles and to allow-list `@-model` mentions. */
337
352
  models: Record<string, Model>;
@@ -1692,7 +1707,7 @@ export interface ServiceConfigFlat {
1692
1707
  /** 组:store(持久化)—— DB 引擎三态、session 后端、SQL coords、快照 BLOB / SendUserFile 对象存储。 */
1693
1708
  export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "sessionCacheTtlSec" | "storeProbeIntervalMs" | "dbBackend" | "dbBackendExplicit" | "localDataRoot" | "runStoreStrictHydrate" | "tidb" | "pg" | "dbQueryTimeoutMs" | "snapshotBlobStore" | "snapshotBlobSqlMaxBytes" | "snapshotBlobAllowSql" | "fileHistory" | "sendUserFile">;
1694
1709
  /** 组:modelPlane(模型面)—— 网关坐标、Anthropic 路线、韧性旋钮、主/廉价 model entry、role 表、降级梯。 */
1695
- export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "atModelAllowlist" | "cascadeLadder" | "degrade">;
1710
+ export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "gatewayMaxRetries" | "anthropic" | "resilience" | "model" | "envModel" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "atModelAllowlist" | "cascadeLadder" | "degrade">;
1696
1711
  /** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
1697
1712
  * 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
1698
1713
  export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "durableApprovalSource" | "streamAskWindowMsSource" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "resourceSuspendTotalTokens" | "resourceSuspendTotalBudgetUsd" | "resourceSuspendMaxSlices" | "askQuestionEnabled" | "questionThrottle" | "toolApprovalEnabled" | "permissionRulesEnabled" | "permissionRulesEnabledExplicit" | "streamAskWindowMarginMs" | "streamApproval" | "unattendedApprovalPolicy" | "mcpElicitation" | "sensitiveWritePatterns" | "writeProtectedPaths" | "manualModeShellGate" | "permissionsDisableAutoMode" | "crossSessionInbound" | "crossSessionDialogExpiry">;
package/dist/config.d.ts CHANGED
@@ -169,27 +169,6 @@ export declare const hostExecSpoolEnabled: () => boolean;
169
169
  * a loud CONFIG_NOTICES line ("removed-1m"); the center lane's roster culture is explicit declaration.
170
170
  * Shared by the env lane (main + cheap models) and the center lane (sema-registry toModel). */
171
171
  export declare function applyAutoCompactWindow(m: Model, explicit?: number): "explicit" | "sonnet5" | "removed-1m" | "removed";
172
- /**
173
- * [ref] 件⑤ —— 思考档两键的**闭集词表**与拒启判据(`MODEL_DEFAULT_THINKING` / `MODEL_REASONING_EFFORT_LEVELS`)。
174
- *
175
- * 改前两键都走 `isThinkingLevel(...)` **过滤**:拼错的词被**静默丢掉**,而两根旋钮的失败方向恰好都是
176
- * 「把运维的意图反向执行」——
177
- * · `MODEL_DEFAULT_THINKING=hight` ⇒ `defaultThinking` 不铸键 ⇒ 全部任务回落到「无模型级默认档」,
178
- * 运维以为整台机器抬到了 high;
179
- * · `MODEL_REASONING_EFFORT_LEVELS=med,high` ⇒ 只剩 `["high"]`;写成 `med` 一个词则整键落空、
180
- * `compat` 不铸 ⇒ 回落 core 的保守梯(`minimal|low|medium|high`),`xhigh`/`max` 被 clamp 下来。
181
- * 运维配这根旋钮的**全部用途**就是告诉引擎「本端点吃得下更高档」,静默落空 = 高档永远上不去。
182
- * 两条都是 `MODEL_DEGRADE_ON` / `TASK_TIMEOUT_SEC` 那条族律的同形([ref] A 档:坏值拒启,不静默折默认),
183
- * 所以改成 boot 响亮拒 —— 与 `MODEL_DEGRADE_ON` 的 `parseDegradeOn` 逐字同款(点名坏词 + 列合法词)。
184
- *
185
- * 词表用 `Record<ThinkingLevel, true>` 铸 = 与 llm-core 锁步:上游加一档而这里不补 ⇒ **编译红**
186
- * (`MODEL_DEGRADE_ON` 的 `Set<DegradeReason>` 同款纪律);目录行的 `enumValues` 由本表派生,
187
- * 「文档写五档、代码收六档」那种分叉在类型层就不可能发生。
188
- */
189
- type ModelThinkingLevel = NonNullable<Model["defaultThinking"]>;
190
- /** 六档闭集(有序:低→高,取自上表的书写序),目录行 `enumValues` 与两条拒因文案的单一真源。
191
- * (`CATALOG_RESTART_SLICES = Object.keys(RESTART_SLICE_SET)` 同款派生式。) */
192
- export declare const THINKING_LEVELS: readonly ModelThinkingLevel[];
193
172
  /**
194
173
  * 一条工具名**永不匹配任何现役工具**的三种拼法,及其修正指引。审批/权限名单是 server 侧 ToolPolicy 的
195
174
  * RAW 逐字比对(canonicalToolName 折叠面已随 core 5.0.0 [ref] 退役),所以写错的名字不会报错、只会静默
package/dist/config.js CHANGED
@@ -13,6 +13,7 @@ import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config
13
13
  import { buildWriteProtectionPosture } from "./write-protection.js";
14
14
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
15
15
  import { readFaceForPosture } from "./read-face-posture.js";
16
+ import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat, isThinkingTier } from "./model-compat.js";
16
17
  import {} from "./posture-source.js";
17
18
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
18
19
  export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
@@ -739,7 +740,11 @@ function parseModelDomain() {
739
740
  })();
740
741
  const extraBody = buildExtraBody();
741
742
  const thinkingDefault = parseDefaultThinking(process.env.MODEL_DEFAULT_THINKING);
742
- const effortLevels = parseReasoningEffortLevels(csv("MODEL_REASONING_EFFORT_LEVELS"));
743
+ const envCompatDeclared = buildModelCompat({
744
+ reasoningEffortLevels: parseReasoningEffortLevels(csv("MODEL_REASONING_EFFORT_LEVELS")),
745
+ thinkingFormat: parseThinkingFormat(process.env.MODEL_THINKING_FORMAT),
746
+ });
747
+ const envCompat = provider === "anthropic" ? undefined : envCompatDeclared;
743
748
  const model = {
744
749
  id: modelId,
745
750
  name: modelId,
@@ -761,7 +766,7 @@ function parseModelDomain() {
761
766
  ...(optFinitePositiveEnv("MODEL_CHARS_PER_TOKEN") !== undefined ? { charsPerToken: optFinitePositiveEnv("MODEL_CHARS_PER_TOKEN") } : {}),
762
767
  params: { promptCacheFamily: provider === "anthropic" ? "input-excludes-cached" : "input-includes-cached" },
763
768
  ...(thinkingDefault ? { defaultThinking: thinkingDefault } : {}),
764
- ...(effortLevels.length > 0 && provider !== "anthropic" ? { compat: { reasoningEffortLevels: effortLevels } } : {}),
769
+ ...(envCompat ? { compat: envCompat } : {}),
765
770
  ...(extraBody ? { extraBody } : {}),
766
771
  };
767
772
  const cheapId = process.env.MODEL_CHEAP_ID;
@@ -858,6 +863,7 @@ function parseModelDomain() {
858
863
  cooldownMs: numEnv("MODEL_CB_COOLDOWN_MS", "30000"),
859
864
  },
860
865
  model,
866
+ envModel: model,
861
867
  models,
862
868
  modelApiKeyEnv: {},
863
869
  modelApiKeys: {},
@@ -878,9 +884,6 @@ function parseModelDomain() {
878
884
  : undefined,
879
885
  };
880
886
  }
881
- const THINKING_LEVEL_TABLE = { minimal: true, low: true, medium: true, high: true, xhigh: true, max: true };
882
- export const THINKING_LEVELS = Object.keys(THINKING_LEVEL_TABLE);
883
- const isThinkingTier = (w) => THINKING_LEVELS.includes(w);
884
887
  function parseDefaultThinking(raw) {
885
888
  const v = raw?.trim();
886
889
  if (v === undefined || v === "")
@@ -904,6 +907,16 @@ function parseReasoningEffortLevels(words) {
904
907
  }
905
908
  return words.filter(isThinkingTier);
906
909
  }
910
+ function parseThinkingFormat(raw) {
911
+ const v = raw?.trim();
912
+ if (v === undefined || v === "")
913
+ return undefined;
914
+ if (!isThinkingFormat(v)) {
915
+ throw new Error(`env MODEL_THINKING_FORMAT="${v}" is not a wire thinking format — valid: ${THINKING_FORMATS.join(" | ")}. ` +
916
+ `A typo dropped SILENTLY would leave a think-by-default gateway thinking on every turn, with nothing in the readings to show it (#210 A 档).`);
917
+ }
918
+ return v;
919
+ }
907
920
  const DEGRADE_ON_WORDS = new Set(["breaker_open", "rate_limit", "budget", "server_error", "last_resort"]);
908
921
  function parseDegradeOn(words) {
909
922
  if (words.length === 0)
@@ -1907,7 +1920,7 @@ const STORE_GROUP_KEYS = [
1907
1920
  "tidb", "pg", "dbQueryTimeoutMs", "snapshotBlobStore", "snapshotBlobSqlMaxBytes", "snapshotBlobAllowSql", "fileHistory", "sendUserFile",
1908
1921
  ];
1909
1922
  const MODEL_PLANE_GROUP_KEYS = [
1910
- "gatewayBaseUrl", "gatewayApiKey", "gatewayFallbackUrls", "gatewayMaxRetries", "anthropic", "resilience", "model", "models",
1923
+ "gatewayBaseUrl", "gatewayApiKey", "gatewayFallbackUrls", "gatewayMaxRetries", "anthropic", "resilience", "model", "envModel", "models",
1911
1924
  "modelApiKeyEnv", "modelApiKeys", "modelQuotaWeights", "tiers", "projects", "roles", "atModelAllowlist", "cascadeLadder", "degrade",
1912
1925
  ];
1913
1926
  const APPROVAL_GROUP_KEYS = [
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
18
18
  import type { HostDecision } from "../host-decision.js";
19
- import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner } from "@sema-agent/core";
19
+ import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner, EngineNotice } from "@sema-agent/core";
20
20
  import type { FlatServiceDeps, RequestAuth } from "./server.js";
21
21
  import type { TaskRequestBody, DecideBinding } from "./wire-types.js";
22
22
  import type { IdempotencyCache } from "./idempotency.js";
@@ -122,6 +122,10 @@ export interface PreparedTaskSubmission {
122
122
  cascade?: boolean;
123
123
  jobId?: string;
124
124
  body: TaskRequestBody;
125
+ /** L-167:**装配期**铸下的、这条会话的用户面 `EngineNotice`(今天只有请求腿 MCP 注入被丢弃一族)。
126
+ * 装配跑在这条腿的通告口开张之前,所以铸点交不出去 —— 由腿在 `registerEngineNoticeLeg` 那一拍
127
+ * 经 `pending` 一起投(投递单点仍是那一只注册助手,不新开第二条机制)。缺席 = 本次装配没铸下任何通告。 */
128
+ engineNotices?: readonly EngineNotice[];
125
129
  }
126
130
  /** B4①(命名化):`driveResumeIntoRunLog` 的入参形(resume 家族共用)——原为 9 成员内联匿名对象。 */
127
131
  export interface DriveResumeArgs {
@@ -136,6 +140,19 @@ export interface DriveResumeArgs {
136
140
  resumeObjective: string;
137
141
  outcome: ResumeOutcome;
138
142
  verifyRounds: VerifyRoundsSpec | undefined;
143
+ /**
144
+ * **R-1**(车CJ 收车档 §10 R-1;S-185 批落地)—— 本条 resume 腿**装配期**铸下的用户面通告
145
+ * (`PreparedTaskSubmission.engineNotices` 的 resume 孪生;今天唯一一族:`mcp.injection_dropped`)。
146
+ *
147
+ * 为什么 resume 腿也要投:一条 park 任务的 body 是**存量**的,它自己那几条丢弃在首跑那一拍确实已经
148
+ * 披露过一次 —— 但**部署侧在 park 期间新占了一个名字**(center 加了同名 MCP server)这一形,只有
149
+ * 续跑这一拍才第一次发生,而它此前只有运维日志、用户面一个字都没有。⇒ 不投的代价是「我批准之后
150
+ * 我的 MCP 又少了一台,而没有任何人告诉我」;投的代价是存量丢弃在每次续跑各重说一遍(每腿每名一条,
151
+ * 由铸点的 per-leg 去重与 32 名上限天然兜住)。两害相权取后者:重复是噪声,缺席是失明。
152
+ *
153
+ * 缺席/空 ⇒ 与修前逐字相同(腿开口时 `pending` 不带任何东西)。
154
+ */
155
+ engineNotices?: readonly EngineNotice[];
139
156
  /** codex M2: side-effects that must land AFTER the markResuming CAS is WON (decide accepted, sibling races
140
157
  * lost — never fires on the 409/404 paths) and BEFORE the model leg drives (so the resumed leg's own next
141
158
  * ask sees them — the decide leg's exemption grant). Contract: must not throw (callers swallow internally);
@@ -150,6 +167,26 @@ export interface DriveResumeArgs {
150
167
  mintFreshRun?: {
151
168
  source: string;
152
169
  };
170
+ /**
171
+ * S-185 车CM —— **workflow 出身 park 的决定**,随本次续跑走 core 的可信 run 通道
172
+ * (`RunInternals.workflowParkedResume`)。
173
+ *
174
+ * 为什么是这里、为什么是这个形:core 给部署方留的**唯一**通道就是它(d.ts 逐字:「A host that decided a
175
+ * workflow child's parked checkpoint **launches the run that re-invokes** `Workflow({resumeFromRunId})`
176
+ * with the decision here」)。宿主这一跑的模型再调一次 `Workflow({resumeFromRunId})` 时,工具的
177
+ * `parkedResume` dep 只挑出 `runId` 命中这次 resume 源的条目,交给 `RunWorkflowOptions.parkedResume`,
178
+ * 于是那个 parked ordinal 带着人的决定继续,而不是再 park 一次。
179
+ *
180
+ * 🔴 **TRUSTED**:它绝不是 `TaskSpec` 字段、也绝不是工具参数(模型不能决定一次审批)。唯一的铸点是
181
+ * `/decide` 的第三条车道(判据属主 = `parked-decide.ts` 的 `planWorkflowParkedDecide` 顶注)。
182
+ * 缺席 ⇒ 本位对这条腿是完整 no-op,与本批之前逐字相同。
183
+ */
184
+ workflowParkedResume?: ReadonlyArray<{
185
+ runId: string;
186
+ token: string;
187
+ outcome: ResumeOutcome;
188
+ inheritedGate?: unknown;
189
+ }>;
153
190
  /** [ref]([ref] Inkglow 案 → [ref]① 定谳):**200 受理语义**——调用方声明「我的消费方按 poll/SSE 跟
154
191
  * 终态,别让 HTTP 等模型往返」。在场为 `true` 时本腿在**受理点**(`resumeStream` 解析之后)立刻把
155
192
  * `200 {taskId, status:"resuming", bindingEnforced:true}` 交回调用方,模型腿(流迭代 + 行驱动)继续
@@ -293,7 +330,12 @@ export interface RouteLegs {
293
330
  resumeWake(sessionId: string, message: string | undefined, caller: string | undefined, req: IncomingMessage | undefined,
294
331
  /** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/wake` 腿传 `true`
295
332
  * —— 本腿走 `mintFreshRun`,受理体里的 `taskId` 因此是**新铸的**那个(壳的 poll 锚)。 */
296
- acceptEarly?: boolean): Promise<{
333
+ acceptEarly?: boolean,
334
+ /** S-185 车CM:透传给 {@link DriveResumeArgs.workflowParkedResume}(语义与 TRUSTED 纪律的属主在那条
335
+ * 顶注)。HTTP `/wake` 腿**不设**此位 —— 它是 `/decide` 第三条车道的专用席,而两条腿共用这一条驱动
336
+ * 路径正是 `notify-wake.ts` 那条「wake 会烧模型,所以复用 resume 家族的同一条腿、不自建第二条驱动
337
+ * 路径」的成文纪律。 */
338
+ workflowParkedResume?: DriveResumeArgs["workflowParkedResume"]): Promise<{
297
339
  status: number;
298
340
  body: object;
299
341
  }>;
@@ -199,6 +199,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
199
199
  ...(s.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
200
200
  ...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
201
201
  ...(s.origin !== undefined ? { origin: s.origin } : {}),
202
+ ...(s.classifierUnavailable !== undefined ? { classifierUnavailable: s.classifierUnavailable } : {}),
202
203
  objective: ctx?.body?.objective ?? null,
203
204
  input,
204
205
  };
@@ -195,6 +195,7 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
195
195
  ...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
196
196
  ...(m.maxTokens ? { maxOutputTokens: m.maxTokens } : {}),
197
197
  ...(m.reasoning ? { supportedEffortLevels: [...DEFAULT_EFFORT_LEVELS] } : {}),
198
+ ...(m.compat !== undefined ? { compat: m.compat } : {}),
198
199
  ...(atAllowlist.length > 0 ? { atMentionable: isModelAllowlisted(name, atCatalog, atAllowlist) } : {}),
199
200
  routePairing: routePairingOf(deps, name, m),
200
201
  }));
@@ -1609,7 +1609,7 @@ export async function createDurableRun(req, res, ctx, prepared, idemKey) {
1609
1609
  windowMs: deps.config.streamApproval?.windowMs,
1610
1610
  windowMarginMs: deps.config.streamAskWindowMarginMs,
1611
1611
  }
1612
- : undefined, deps.checkpointStore);
1612
+ : undefined, deps.checkpointStore, prepared.engineNotices);
1613
1613
  return { status: 202, body: { taskId, sessionId, status: "running" } };
1614
1614
  }, (r) => r.status === 202);
1615
1615
  return resp;
@@ -299,6 +299,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
299
299
  const noticeLedger = ledgerSink;
300
300
  const unregisterEngineNotice = registerEngineNoticeLeg({
301
301
  sessionId: prepared.spec.sessionId,
302
+ ...(prepared.engineNotices !== undefined ? { pending: prepared.engineNotices } : {}),
302
303
  live: (row) => {
303
304
  if (res.writableEnded || res.destroyed) {
304
305
  recordFailOpen(failOpenTagForDroppedFrame("engine_notice"), "frame=engine_notice");
@@ -2,6 +2,7 @@ import { persistedPlaneOf } from "../../terminal.js";
2
2
  import { z } from "zod";
3
3
  import { getWorkflowRun, subscribeWorkflow, deriveAgentDisplayStatus, callKeyOrdinal } from "@sema-agent/core";
4
4
  import { redactSteerIn, STEER_IN_MAX_CHARS, STEER_IN_MAX_REQUEST_CHARS } from "../../orchestration/workflow-agent-steer.js";
5
+ import { journalEntryOfRow, journalEntryPayload, journalRowPayloadOf } from "../../orchestration/workflow-journal-entry.js";
5
6
  import { redactSecrets } from "../../trace/redact.js";
6
7
  import { createHash } from "node:crypto";
7
8
  import { redactErrorMessage, redactHead } from "../../observability/run-terminal-log.js";
@@ -75,6 +76,13 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
75
76
  const MAX_ROW_BYTES = 64 * 1024;
76
77
  const firstLine = (t) => (t ? redactHead(t.split("\n")[0], 300) : undefined);
77
78
  const MAX_RESULT_CHARS = 2000;
79
+ const projectEntry = (callKey, r, parked) => parked ? { ...projectResult(callKey, r), parked: true } : projectResult(callKey, r);
80
+ const oversizeStub = (callKey, resultBytes) => ({
81
+ callKey,
82
+ ordinal: callKeyOrdinal(callKey),
83
+ truncated: true,
84
+ resultBytes,
85
+ });
78
86
  const projectResult = (callKey, r) => {
79
87
  const memoKey = `${wfId}\u0000${callKey}\u0000${createHash("sha256").update(JSON.stringify(r)).digest("hex")}`;
80
88
  const hit = journalProjectionMemo.get(memoKey);
@@ -109,13 +117,14 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
109
117
  const page = await js.loadPage(wfId, scope, { offset: jOffset, limit: jLimit, maxResultBytes: MAX_ROW_BYTES });
110
118
  projected = page.map((row) => {
111
119
  if (row.resultJson === null) {
112
- return { callKey: row.callKey, ordinal: callKeyOrdinal(row.callKey), truncated: true, resultBytes: row.resultBytes };
120
+ return oversizeStub(row.callKey, row.resultBytes);
113
121
  }
114
122
  try {
115
- return projectResult(row.callKey, JSON.parse(row.resultJson));
123
+ const { payload, parked } = journalEntryPayload(journalEntryOfRow(row.callKey, row.resultJson));
124
+ return projectEntry(row.callKey, payload, parked);
116
125
  }
117
126
  catch {
118
- return { callKey: row.callKey, ordinal: callKeyOrdinal(row.callKey), truncated: true, resultBytes: row.resultBytes };
127
+ return oversizeStub(row.callKey, row.resultBytes);
119
128
  }
120
129
  });
121
130
  }
@@ -125,10 +134,9 @@ async function handleWorkflowsReadBody(req, res, url, ctx, miss) {
125
134
  .sort((a, b) => callKeyOrdinal(a.callKey) - callKeyOrdinal(b.callKey))
126
135
  .slice(jOffset, jOffset + jLimit)
127
136
  .map((en) => {
128
- const bytes = Buffer.byteLength(JSON.stringify(en.result));
129
- return bytes > MAX_ROW_BYTES
130
- ? { callKey: en.callKey, ordinal: callKeyOrdinal(en.callKey), truncated: true, resultBytes: bytes }
131
- : projectResult(en.callKey, en.result);
137
+ const { payload, parked } = journalEntryPayload(en);
138
+ const bytes = Buffer.byteLength(journalRowPayloadOf(en));
139
+ return bytes > MAX_ROW_BYTES ? oversizeStub(en.callKey, bytes) : projectEntry(en.callKey, payload, parked);
132
140
  });
133
141
  }
134
142
  sendJson(res, 200, { runId: wfId, entries: projected, ...(projected.length === jLimit ? { nextOffset: jOffset + jLimit } : {}) });
@@ -1,6 +1,6 @@
1
1
  import http from "node:http";
2
2
  import type { IncomingMessage } from "node:http";
3
- import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport, type EntryProvenanceAccount, type MemoryErasureAttestation, type MemoryEntryOrigin, type OriginClearanceRow, type SessionMemoryStatus, type ConsolidationRunReceipt, type ConsolidationDriverRunRow, type SideQuerySpec, type SideQueryResult, type Brain } from "@sema-agent/core";
3
+ import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport, type EntryProvenanceAccount, type MemoryErasureAttestation, type MemoryEntryOrigin, type OriginClearanceRow, type SessionMemoryStatus, type ConsolidationRunReceipt, type ConsolidationDriverRunRow, type SideQuerySpec, type SideQueryResult, type Brain, type EngineNotice } from "@sema-agent/core";
4
4
  import type { TaskRequestBody } from "./wire-types.js";
5
5
  import type { ServiceConfig } from "../config-types.js";
6
6
  import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js";
@@ -92,9 +92,17 @@ export interface ServiceCoreDeps {
92
92
  * `boot/resolve-spec.ts` 的 `declaresInteractivePosture`)。缺席/false ⇒ 无人腿,不声明 posture ——
93
93
  * bg(`POST /v1/runs`)、A2A、resume、cron/wake 一律走这一侧,行为字节不变。
94
94
  * ⚠️ 它是**腿身份**不是能力表态:绝不从请求体读,只由路由装配点按 URL 定(客户端说了不算)。 */
95
+ /** `opts.onNotice` (L-167, core 7.9.1):装配期铸下的、**这条会话**的用户面通告(今天只有一族:请求腿
96
+ * MCP 注入被丢弃)。装配跑在通告腿开口**之前**(`prepareSpec` → 路由注册 `registerEngineNoticeLeg`),
97
+ * 所以它不能在铸点直接 `route()` ——收下来交给调用腿,由腿在开口那一拍连同 `pending` 一起投。
98
+ * 缺席(桩)⇒ 只走结构化日志终点,行为逐字不变。
99
+ * ⚠️ 7.69.0(R-1)起 **resume 家族也递这一位** —— 四条 resume 腿收敛到 `resolveResumeSpec` 一只 resolver,
100
+ * 通告口接一次、四腿同得(见 http/server.ts 的 `resolveResumeSpec` 顶注);此处旧注写作「resume 家族缺席」
101
+ * 已过期,按新行为改。 */
95
102
  resolveSpec: (body: TaskRequestBody, req: IncomingMessage | undefined, auth?: RequestAuth, opts?: {
96
103
  leg?: "fresh" | "resume";
97
104
  liveStream?: boolean;
105
+ onNotice?: (notice: EngineNotice) => void;
98
106
  }) => TaskSpec | Promise<TaskSpec>;
99
107
  /**
100
108
  * [ref] 场景 hands lane 选路:按 `resolveSpec` 刚产出的 TaskSpec 取本任务的执行 Runner。
@@ -151,6 +159,11 @@ export interface ServiceStoreDeps {
151
159
  * (subscribeWorkflow) and works whenever self-orchestration is on, store or not. Runs are owner-gated by
152
160
  * `scope` (= the creating principal). Also drives `capabilities.workflowsList` (= "can durably LIST runs"). */
153
161
  workflowRunStore?: WorkflowRunStore;
162
+ /** S-185 车CM:workflow 出身 park 的**有界 join** 索引 —— 与 `workflowRunStore` **成对**才构成第三条
163
+ * 车道(索引只回答「去哪条 run 找」,行本体与赎回键仍以 run 的 blob 为准)。与 RunnerDeps 侧同实例
164
+ * (boot/workflow-orchestration.ts);任一缺席 = 那条车道不存在(workflow 出身的 park 落 legacy 腿,
165
+ * 与本批之前逐字同形)。契约与单写者纪律的属主 = orchestration/workflow-agent-session-index.ts。 */
166
+ workflowAgentSessionIndex?: import("../orchestration/workflow-agent-session-index.js").WorkflowAgentSessionIndex;
154
167
  /** [ref] journal 读面(PAIR-REVIEW 修车批):GET /v1/workflows/:runId/journal 的数据源——与 runner 侧
155
168
  * workflowJournalStore 同一实例(main.ts backend.workflowJournal());缺席=501(与 workflowRunStore 同门)。 */
156
169
  workflowJournalStore?: import("@sema-agent/core").WorkflowJournalStore;
@@ -676,7 +689,7 @@ export interface ServiceDeploymentDeps {
676
689
  * `workflowModelAllowlistFor(config)` 算一次(与 createRunnerDeps 喂给 core `workflowGovernanceBaseline.
677
690
  * workflowModelAllowlist` 的同一只函数、同一 config 代际),多租户 fail-closed 的 undefined 在装配点
678
691
  * 折 `[]` ⇒ 生产装配下本席**恒在场**。为什么不让能力面每请求活派生:core 5.49 起 models plane 经
679
- * `Runner.swapModels` 永久热,而 governance allowlist 是 Runner 构造期快照 —— 活派生会在热加目录键后
692
+ * `Runner.swapDeps` 永久热,而 governance allowlist 是 Runner 构造期快照 —— 活派生会在热加目录键后
680
693
  * 长期广告一个 core 必拒的词(教学面超前执法面,直到重启)。广告**引擎真快照**让「能力面教的」与
681
694
  * 「core 收到的 allowlist」结构性恒等。缺席(遗留 stub 装配)⇒ 能力面回落活 config 派生(测试便利,
682
695
  * 生产路径到不了)。 */