@sema-agent/server 7.99.0 → 7.100.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 (115) hide show
  1. package/MIGRATION.md +13 -0
  2. package/USAGE.md +75 -52
  3. package/dist/approval-ask-machine.d.ts +10 -0
  4. package/dist/approval-ask-machine.js +3 -0
  5. package/dist/approval-card.d.ts +74 -1
  6. package/dist/approval-card.js +41 -8
  7. package/dist/approval.d.ts +44 -20
  8. package/dist/approval.js +14 -6
  9. package/dist/boot/config-center.d.ts +16 -8
  10. package/dist/boot/config-center.js +39 -81
  11. package/dist/boot/execution-env.d.ts +1 -2
  12. package/dist/boot/execution-env.js +2 -2
  13. package/dist/boot/leader.js +12 -9
  14. package/dist/boot/memory-consolidation.d.ts +38 -0
  15. package/dist/boot/memory-consolidation.js +35 -0
  16. package/dist/boot/resolve-spec.d.ts +0 -1
  17. package/dist/boot/resolve-spec.js +4 -3
  18. package/dist/boot/runner-deps.d.ts +2 -2
  19. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  20. package/dist/boot/stage-06-runners.d.ts +2 -2
  21. package/dist/boot/stage-06-runners.js +3 -3
  22. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  23. package/dist/boot/stage-07-capability-layer.js +6 -6
  24. package/dist/boot/stage-08-reapers.d.ts +3 -4
  25. package/dist/boot/stage-08-reapers.js +2 -2
  26. package/dist/boot/stage-09-leader.d.ts +2 -3
  27. package/dist/boot/stage-10-http-server.d.ts +2 -3
  28. package/dist/boot/stage-10-http-server.js +2 -2
  29. package/dist/capabilities/center-plugins.d.ts +14 -2
  30. package/dist/capabilities/center-plugins.js +13 -5
  31. package/dist/capabilities/hands-lane.js +2 -1
  32. package/dist/capabilities/scenarios.js +18 -5
  33. package/dist/capabilities/tool-defer.d.ts +2 -2
  34. package/dist/config-catalog.d.ts +2 -2
  35. package/dist/config-catalog.js +12 -13
  36. package/dist/config-center/apply-effective.d.ts +34 -39
  37. package/dist/config-center/apply-effective.js +100 -64
  38. package/dist/config-center/apply-ledger.d.ts +35 -16
  39. package/dist/config-center/apply-ledger.js +25 -18
  40. package/dist/config-center/facade.d.ts +11 -5
  41. package/dist/config-center/facade.js +1 -1
  42. package/dist/config-center/hot-keys-registry.d.ts +11 -4
  43. package/dist/config-center/hot-keys-registry.js +11 -7
  44. package/dist/config-center/restart-signal.d.ts +21 -14
  45. package/dist/config-center/restart-signal.js +16 -22
  46. package/dist/config-center/types.d.ts +3 -3
  47. package/dist/config-types.d.ts +22 -7
  48. package/dist/cross-session-settings.js +1 -1
  49. package/dist/host-lsp-manager.d.ts +29 -0
  50. package/dist/host-lsp-manager.js +14 -0
  51. package/dist/http/active-run-conflict.d.ts +2 -12
  52. package/dist/http/active-run-conflict.js +1 -6
  53. package/dist/http/admission.js +8 -0
  54. package/dist/http/resume-legs.d.ts +1 -1
  55. package/dist/http/resume-legs.js +14 -10
  56. package/dist/http/route-ctx.d.ts +2 -2
  57. package/dist/http/routes/approvals-assistant.d.ts +1 -1
  58. package/dist/http/routes/approvals-assistant.js +2 -2
  59. package/dist/http/routes/memory-compliance.js +5 -0
  60. package/dist/http/routes/memory-origin.js +5 -0
  61. package/dist/http/routes/sessions.js +6 -0
  62. package/dist/http/routes/tasks.js +7 -6
  63. package/dist/http/server.d.ts +6 -5
  64. package/dist/http/wire-types.d.ts +6 -3
  65. package/dist/leader/wire.d.ts +20 -12
  66. package/dist/leader/wire.js +7 -6
  67. package/dist/memory-layer-preflight.d.ts +46 -0
  68. package/dist/memory-layer-preflight.js +121 -0
  69. package/dist/memory-operator-faces.js +17 -1
  70. package/dist/observability/corrupt-read-seat.d.ts +21 -4
  71. package/dist/observability/corrupt-read-seat.js +31 -0
  72. package/dist/observability/fail-open.d.ts +8 -4
  73. package/dist/observability/fail-open.js +8 -4
  74. package/dist/observability/run-terminal.js +1 -0
  75. package/dist/observability/secret-env-scrub.d.ts +1 -0
  76. package/dist/observability/secret-env-scrub.js +1 -0
  77. package/dist/orchestration/workflow-completion-inbox.d.ts +31 -23
  78. package/dist/orchestration/workflow-completion-inbox.js +8 -3
  79. package/dist/plugins/approval-ask-store-file.d.ts +20 -2
  80. package/dist/plugins/approval-ask-store-file.js +21 -3
  81. package/dist/plugins/approval-ask-store-memory.d.ts +2 -0
  82. package/dist/plugins/approval-ask-store-memory.js +39 -13
  83. package/dist/plugins/approval-ask-store-sql.d.ts +24 -5
  84. package/dist/plugins/approval-ask-store-sql.js +27 -3
  85. package/dist/plugins/checkpoint-store-sql.d.ts +9 -3
  86. package/dist/plugins/checkpoint-store-sql.js +19 -2
  87. package/dist/plugins/local-checkpoint-store.js +2 -0
  88. package/dist/plugins/mailbox-store-sql.d.ts +7 -5
  89. package/dist/plugins/mailbox-store-sql.js +6 -4
  90. package/dist/plugins/permission-rule-store-file.js +3 -2
  91. package/dist/plugins/sql-json-column.d.ts +16 -3
  92. package/dist/plugins/sql-json-column.js +15 -4
  93. package/dist/plugins/web-search.d.ts +42 -22
  94. package/dist/plugins/web-search.js +141 -49
  95. package/dist/run-local.js +6 -5
  96. package/dist/runs.js +6 -5
  97. package/dist/runtime-governance.d.ts +50 -3
  98. package/dist/runtime-governance.js +5 -0
  99. package/dist/server-secret-env.d.ts +4 -2
  100. package/dist/task-settings.d.ts +1 -1
  101. package/dist/tool-approval.d.ts +107 -19
  102. package/dist/tool-approval.js +64 -15
  103. package/dist/trace/core-keyset-guard.d.ts +5 -5
  104. package/dist/trace/ledger-sink.js +2 -2
  105. package/dist/trace/project.d.ts +13 -0
  106. package/dist/trace/project.js +22 -5
  107. package/dist/trace/projection-drop.d.ts +2 -0
  108. package/dist/trace/projection-drop.js +6 -0
  109. package/dist/trace/sema-provenance.d.ts +4 -1
  110. package/dist/trace/sema-provenance.js +1 -1
  111. package/dist/trace/task-notification-facets.d.ts +65 -0
  112. package/dist/trace/task-notification-facets.js +31 -0
  113. package/dist/trace/wire-projection-faces.d.ts +3 -3
  114. package/dist/trace/wire-projection-faces.js +3 -3
  115. package/package.json +3 -3
@@ -1,7 +1,8 @@
1
1
  import { resolveActiveTiers } from "@sema-agent/settings-schema";
2
+ import { DEFAULT_PLUGIN_ALLOW_HOSTS, materializablePlugins } from "../capabilities/center-plugins.js";
2
3
  import { resolveDefaultModelName } from "./apply-effective.js";
3
4
  import { CENTER_READ_FACE_LIVE_KEYS } from "./read-face.js";
4
- const RESTART_SLICES = ["skills", "mcp", "a2a", "scenarios", "runtime-gates", "models-tiers", "degrade-route", "read-face"];
5
+ const RESTART_SLICES = ["skills", "mcp", "a2a", "scenarios", "degrade-route", "read-face", "memory-consolidation"];
5
6
  export function stableStringify(v) {
6
7
  if (v === null || typeof v !== "object")
7
8
  return JSON.stringify(v) ?? "null";
@@ -26,31 +27,29 @@ function planeEntriesFingerprint(rows) {
26
27
  const enabled = enabledOnly(rows);
27
28
  return enabled ? enabled.map((m) => catalogEntryFingerprint(m)) : null;
28
29
  }
30
+ function restartSlicePair(boot, current, slice, ctx) {
31
+ if (slice === "memory-consolidation") {
32
+ const seat = ctx?.consolidationSeat;
33
+ return seat === undefined ? [null, null] : [seat.baked, seat.current];
34
+ }
35
+ return [restartSliceValue(boot, slice, ctx), restartSliceValue(current, slice, ctx)];
36
+ }
29
37
  function restartSliceValue(eff, slice, ctx) {
30
38
  if (!eff)
31
39
  return null;
32
40
  switch (slice) {
33
- case "skills":
34
- return enabledOnly(eff.skills?.skills);
41
+ case "skills": {
42
+ const skills = enabledOnly(eff.skills?.skills);
43
+ const materialized = materializablePlugins(eff, ctx?.pluginsAllowHosts ?? DEFAULT_PLUGIN_ALLOW_HOSTS);
44
+ const plugins = materialized.length > 0 ? materialized : null;
45
+ return skills === null && plugins === null ? null : { skills, plugins };
46
+ }
35
47
  case "mcp":
36
48
  return enabledOnly(eff.mcp?.servers);
37
49
  case "a2a":
38
50
  return enabledOnly(eff.a2a?.peers);
39
51
  case "scenarios":
40
52
  return enabledOnly(eff.scenarios?.scenarios);
41
- case "runtime-gates": {
42
- const ar = eff.governance?.approvalRequire;
43
- return Array.isArray(ar) ? { approvalRequire: ar } : null;
44
- }
45
- case "models-tiers": {
46
- const active = eff.models ? resolveActiveTiers(eff.models) : undefined;
47
- if (!active || Object.keys(active).length === 0)
48
- return null;
49
- const enabled = enabledOnly(eff.models?.models);
50
- const names = new Set((enabled ?? []).map((m) => m.name));
51
- const def = resolveDefaultModelName(eff, (n) => names.has(n), enabled?.[0]?.name ?? "");
52
- return { models: planeEntriesFingerprint(eff.models?.models), tiers: active, default: def.name };
53
- }
54
53
  case "read-face": {
55
54
  const d = eff.readFace;
56
55
  if (!d)
@@ -70,8 +69,7 @@ function restartSliceValue(eff, slice, ctx) {
70
69
  }
71
70
  export function restartReasons(boot, current, ctx) {
72
71
  return RESTART_SLICES.filter((s) => {
73
- const b = restartSliceValue(boot, s, ctx);
74
- const c = restartSliceValue(current, s, ctx);
72
+ const [b, c] = restartSlicePair(boot, current, s, ctx);
75
73
  if (stableStringify(b) === stableStringify(c))
76
74
  return false;
77
75
  if (s === "mcp" && ctx?.mcpRevocationLive === true && Array.isArray(b) && Array.isArray(c) && c.length < b.length) {
@@ -89,10 +87,6 @@ export function restartReasons(boot, current, ctx) {
89
87
  return true;
90
88
  });
91
89
  }
92
- export function planeHasActiveTiers(eff) {
93
- const active = eff.models ? resolveActiveTiers(eff.models) : undefined;
94
- return active !== undefined && Object.keys(active).length > 0;
95
- }
96
90
  export function modelPlaneChanged(prev, next) {
97
91
  if (!prev)
98
92
  return true;
@@ -303,7 +303,7 @@ export interface EffectiveConfig {
303
303
  * settings-schema 0.6 起住 `governance` 域,0.7.1 撤了 runtime 位的双写镜像;此后 `RuntimeConfig` 是 strip
304
304
  * 的 zod object,两条腿(远端 `readEffectiveWire` / 本地 FileConfigStore→`buildEffective`)都把旧位 strip
305
305
  * 掉、由 `legacyGovernanceFromRuntime` 抬进 governance ⇒ 本仓在 runtime 位上读它们的每一行都是死码。
306
- * 旧类型面上留着这三键,让 `runtime-gates` 重启片一直读着一个生产上恒缺席的位(审批闸收紧静默不生效)。 */
306
+ * 旧类型面上留着这三键,让(已于 S-668 波 2 退役的)`runtime-gates` 重启片一直读着一个生产上恒缺席的位(审批闸收紧静默不生效)。 */
307
307
  runtime?: {
308
308
  rateLimitPerMin?: number;
309
309
  maxTaskCostUsd?: number;
@@ -342,8 +342,8 @@ export interface EffectiveConfig {
342
342
  /** settings-schema 0.6 起治理三件(autonomy/commandPolicy/approvalRequire)的**唯一**承运域(0.7.1 撤
343
343
  * runtime 位双写镜像;顺序契约当年已走完:消费端先切、center 再撤)。键集与 schema `GovernanceConfig.shape`
344
344
  * 逐字相等([ref] 件3 对账门钉)。消费:autonomy/commandPolicy 逐请求热(`stageRuntimeHot`,boot+refresh);
345
- * approvalRequire boot-baked(`stageApprovalGate`,refresh 不写)⇒ 变更由 `runtime-gates` 重启片通告。
346
- * 同一 presence 规则(undefined = 不管 → 保 env / 回基线)。 */
345
+ * approvalRequire 自 S-668 波 2 起同样热(`stageApprovalGate`,boot+refresh 双腿,有自己的拒绝臂)。
346
+ * 同一 presence 规则(undefined = 不管 → 回 env 基线,非粘)。 */
347
347
  governance?: {
348
348
  autonomy?: Autonomy;
349
349
  commandPolicy?: CommandRule[];
@@ -472,8 +472,9 @@ export interface ServiceConfigFlat {
472
472
  /** registry-core 0.9.0 档位组: the resolved active tier table (`tier word → catalog model name`,
473
473
  * e.g. `{pro: "ds-pro"}`) fed to `RunnerDeps.tiers` (core 1.265 expandTiers — tier words + CC aliases become
474
474
  * catalog keys). NO env source — populated ONLY by applyEffective from `models.tierGroups`/`activeTierGroup`
475
- * via registry-core `resolveActiveTiers` (single semantic source). Empty = INERT (core contract). Consumed at
476
- * Runner CONSTRUCTION (restart-to-apply, same tier as models/roles); mutateInPlace keeps the reference. */
475
+ * via registry-core `resolveActiveTiers` (single semantic source). Empty = INERT (core contract). Expanded at
476
+ * Runner construction and re-expanded on every refresh-time plane swap (`Runner.swapDeps`) — HOT, same as
477
+ * models/roles; mutateInPlace keeps the reference. */
477
478
  tiers: Record<string, string>;
478
479
  /** 142-S4(registry-core 0.10.0 projects 域):center 项目登记簿的 service 消费面 —
479
480
  * 键=projectId(PROJECT_ID_REGEX,generic lowercase UUID)。消费点:①`memoryScopeFor` 第三参数
@@ -1738,6 +1739,12 @@ export interface ServiceConfigFlat {
1738
1739
  * 位:与「查无活流」同一件事实的两个时刻,详见 `askBroadcast` 的该臂注))。`"timeout"` 宿主自报只留在 (b)(c) ——
1739
1740
  * 结束等待的确实是本仓的窗;其余三臂结束等待的是容量/连接/装配,自报恒缺席。
1740
1741
  *
1742
+ * 🔴 **`deny` 交给引擎的形(S-689)**:部署政策拒对象臂 `{allow:false, settledBy:"policy", reason}` ⇒ 引擎结算
1743
+ * `policy_refused`(`who.party:"none"`)、模型读策略句(「部署政策拒了、此处没有交互审批,别重试、做不需审批的
1744
+ * 部分」)、run 不以「用户拒绝」收束而是继续。修前是裸 `false` —— 引擎契约里那就是「一个人拒了」
1745
+ * (`human_refused` / `haltedOnUserRejection`),与本键「没有人会来」的自声明正好相反。(b)(c) 两臂照旧带
1746
+ * `"timeout"` 自报(`approval_window_expired`)。
1747
+ *
1741
1748
  * 🔴 **射程边界(成文,不是遗漏)**:
1742
1749
  * · 唯一让本旋钮**无处施加**的键是 `TOOL_APPROVAL_ENABLED=false`(协调器整体不构造,core 走它自己的
1743
1750
  * headless 姿态)。**不造推断腿**:旋钮只在协调器在场时有意义,这一句是成文而不是代码分支。
@@ -1747,16 +1754,24 @@ export interface ServiceConfigFlat {
1747
1754
  * 而不是 server 的 ask 行,「无 askStore 但有 durable 设施」正是 R-13 点名的**主受益部署**。
1748
1755
  * 要回滚 R-13 的行为面,键是 `UNATTENDED_APPROVAL_POLICY=deny`,**不是**协议开关。
1749
1756
  * 射程钉:`test/unattended-approval-policy.test.ts` 的 codex R2-F2 两格。
1750
- * · APPROVAL_REQUIRE 名单工具走 `createApprovalBaselinePolicy` 的 durable gate 类,**不经流内臂**,
1751
- * 结构上不受本旋钮影响(裁定「非名单工具」的限定因此无需码内名单分支)。
1757
+ * · APPROVAL_REQUIRE 名单工具:**要不要 ask** 由 `createApprovalBaselinePolicy` 的 durable gate 类裁决,那一层
1758
+ * 结构上读不到本旋钮;但那只 ask 的**答法**在协调器在场时与治理 ask 同走流内臂、同政策(引擎先问活席
1759
+ * `onAsk`,答 `"unavailable"` 才 park)⇒ `deny` 部署对名单工具同样当场拒。旧注「不经流内臂、不受本旋钮
1760
+ * 影响」失实(test 黑盒证伪;S-689 的真 HTTP 格 `test/unattended-deny-settlement-e2e.test.ts` 用的正是
1761
+ * 名单工具)。
1752
1762
  * · **持久回放**(窗到期后向真源收敛、`ensureAsk` 幂等重入撞上已终结的行)读出 PARKING/PARKED 时
1753
1763
  * **同样按旋钮改判** —— 它们与 (a) 是同一件事(赢家可能是别的副本或上一次调用),不统一的话同一只
1754
1764
  * ask 的终局会取决于「谁赢了那把 CAS / 重试没重试」。人的真决议回放不受影响。
1755
1765
  * · 唯一**不受**改判的是 `ensureAsk` 身份未定 / 坏行两臂(「身份不确定」的处置,不是「没有人」的
1756
1766
  * 处置),于是 `deny` 部署上仍可能落极少数 park(店抖动期),已知且有意的残余。
1757
- * · `deny` 部署上 ask **行**仍会经 PARKING 记账(旋钮改的是交给引擎的终局,不是店语义)。收敛器只把
1758
- * PARKING 行绑到**已经存在的** checkpoint 上,而 deny 部署里引擎从不 park ⇒ 绑不上,行按 run 终局
1759
- * 自行收敛成 DENIED/VOID,不会凭空生出 park;残余 = 那类行的归因记成 `routing_failure`。
1767
+ * · 🔴 `deny` 部署上政策结算**当场落行**(NP-2,7.100.0):窗到期 / emit 全灭两臂的店终局 claim 是 `refuse` 意图
1768
+ * —— 这一只 ask 行 CAS 成 DECIDED(deny) + `settled_by="policy"`(按 ask 作用域,不碰批、不连坐兄弟),与交给
1769
+ * 引擎的那一只同一个来源词 ⇒ 重入 / 收敛后重入回放同一个政策拒,不退化成裸 `false`(人拒)。修前行停 PARKING,
1770
+ * run 完成后收敛器转 DENIED 且不带来源。仍按 PARKING 记账的只剩别的写者推进的行(park 政策副本 / 切换政策之前)
1771
+ * 与崩溃后收敛器孤儿腿代打 expire 的行(后者 run 终局后成 DENIED、归因 `routing_failure`,已知残余)。
1772
+ * · 🔴 `deny` 部署上**批是单 ask 批**(S-689 codex R1-F1;`deriveAskScopedBatchId`):批是 park 路由的单位,
1773
+ * 整腿一批的前提是「批一关腿就随 park 结束」;deny 下引擎拿到部署政策拒后腿继续跑,整腿一批会让首只到期
1774
+ * 把批关掉(同腿后续的卡人批不动)、把兄弟连坐成 VOID(裸 `false` = 人拒)。
1760
1775
  */
1761
1776
  unattendedApprovalPolicy: UnattendedApprovalPolicy;
1762
1777
  /** [ref]① (clay A 案 = workflow 权限全面 CC parity): workflow 子 agent 的默认权限基线与主 LLM 同权(base `{}`)。
@@ -1,7 +1,7 @@
1
1
  export const CROSS_SESSION_INBOUND_LAYER_NAMES = ["managed", "user", "repo"];
2
2
  export const CROSS_SESSION_INBOUND_LAYER_SOURCES = {
3
3
  managed: {
4
- source: "CROSS_SESSION_INBOUND —— 部署/组织层旋钮(词表 accept|hold|refuse,未设=本层不设)",
4
+ source: "CROSS_SESSION_INBOUND —— 部署/组织层旋钮(词表取自 core CROSS_SESSION_INBOUND_SETTINGS:7.30.0 起 wake|next-turn|hold|refuse,旧词 accept 拒启;未设=本层不设)",
5
5
  wired: true,
6
6
  },
7
7
  user: {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * S-616(core 7.30.1 #1073 提货)—— **宿主车道本地语言服务器**的唯一构造点。
3
+ *
4
+ * ## 病(DEBTS S-684 同轴的第二条,7.99.0 修复验证 opus 腿普查带出)
5
+ * core 的 `NodeLspManager` 默认 spawn(`defaultLspSpawn`)在 7.30.1 之前**不带 env** ⇒ 语言服务器整份继承本进程的
6
+ * `process.env`,脑钥 / 仓库令牌 / 本仓自己的凭据键全在里面,而语言服务器会执行仓里的代码(构建脚本、插件、
7
+ * 工作区配置)。7.30.1 起 core 默认改成 `scrubSecretEnv(process.env)`(按键名**形状**剥),并开了
8
+ * `NodeLspManagerOptions.env` 一格让宿主自己交 env(core 帖 [ref] ④;core 确认本仓的正确读法 = 交 host 车道**同一只**
9
+ * `scrubServerSecrets` 的结果)。
10
+ *
11
+ * ## 规则(一句)
12
+ * 语言服务器子进程的 env = {@link scrubServerSecrets}(process.env) —— 与模型可驱动的 host shell(`inheritEnv:"scrub"`)
13
+ * 和 project-memory 的 git 子进程**同一只函数**:目录 `type:"secret"` 键按名剥 ∘ core 形状规则按形剥。只交 core 的默认
14
+ * 会漏掉形状规则认不出的本仓凭据键(今天是 `OTEL_EXPORTER_OTLP_HEADERS`),那正是 S-684 当初的病形。
15
+ * 剥掉了什么经既有观测面留痕(site = `server.lsp.spawn-env`,每 site×key 一次披露 + 逐次计数)—— core 那侧
16
+ * 刻意不报(无收集器,KL),本仓这一格补上。
17
+ *
18
+ * ## 时刻
19
+ * env 在**构造时**取一次快照(boot / run-local 起腿时),而不是 core 默认那样每次 spawn 现取:`NodeLspManagerOptions.env`
20
+ * 是一份静态记录。本仓的 `process.env` 是 boot 期输入(热配置走配置中心,不改 `process.env`),两者在本仓部署上同值。
21
+ *
22
+ * ## 两个调用点
23
+ * `boot/execution-env.ts`(服务的 host 车道)与 `run-local.ts`(纯本地 CLI)此前各写一遍 `new NodeLspManager({ log })`;
24
+ * 现在都经本函数 —— 构造形只有一份,剥除规则也就只能有一份(源码对账钉在 `test/secret-env-scrub-disclosure.test.ts`)。
25
+ */
26
+ import { NodeLspManager } from "@sema-agent/core";
27
+ /** 构造宿主车道的本地语言服务器管理器(`create*`:返回活对象)。`log` = 管理器的观测钩(open/heal/degrade/evict 事件)。 */
28
+ export declare function createHostLspManager(log: (event: string, fields: Record<string, unknown>) => void): NodeLspManager;
29
+ //# sourceMappingURL=host-lsp-manager.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { NodeLspManager } from "@sema-agent/core";
2
+ import { recordSecretEnvScrub } from "./observability/secret-env-scrub.js";
3
+ import { scrubServerSecrets } from "./server-secret-env.js";
4
+ export function createHostLspManager(log) {
5
+ const findings = [];
6
+ const scrubbed = scrubServerSecrets(process.env, findings);
7
+ recordSecretEnvScrub("server.lsp.spawn-env", findings);
8
+ const env = {};
9
+ for (const [k, v] of Object.entries(scrubbed))
10
+ if (v !== undefined)
11
+ env[k] = v;
12
+ return new NodeLspManager({ log, env });
13
+ }
14
+ //# sourceMappingURL=host-lsp-manager.js.map
@@ -13,15 +13,10 @@
13
13
  * (`gate_not_tool_approval`/`gate_not_resumable`/`wake.gate_pending` 的指路句)。三处文案与本表若漂移,
14
14
  * test/session-active-conflict-materials.test.ts 的分门用例会红。认不出的 kind ⇒ null(诚实缺席,不铸假门)。
15
15
  */
16
- import { type Autonomy } from "../runtime-governance.js";
16
+ import { type GovernancePosture } from "../runtime-governance.js";
17
17
  import type { ShellGateDoctrine } from "@sema-agent/core";
18
18
  import { type RunRecord } from "../plugins/store-contracts.js";
19
19
  import { type PendingRowBinding } from "../plugins/checkpoint-store-sql.js";
20
- /** [ref] 归因的部署侧入参(两个治理旋钮;判据属主见 `governanceMandatesShellGateAlways`)。 */
21
- export interface GovernancePosture {
22
- autonomy?: Autonomy;
23
- manualModeShellGate?: "always" | "classify";
24
- }
25
20
  /** 与 runs.ts/tasks.ts 三个 409 位共享的旧文案(byte-frozen:api-error-text-freeze 门认这句)。 */
26
21
  export declare const ACTIVE_RUN_CONFLICT_BASE_TEXT = "session already has an active run \u2014 POST /v1/runs/{activeTaskId}/cancel stops it (same-instance interactive runs abort immediately)";
27
22
  /**
@@ -123,11 +118,6 @@ export declare function resumeEntryForGate(kind: string, ids: {
123
118
  sessionId: string;
124
119
  taskId: string;
125
120
  }): string | null;
126
- export declare function governanceOriginOf(gate: {
127
- riskDescriptor?: {
128
- shellGateDoctrine?: ShellGateDoctrine;
129
- };
130
- } | undefined, governance: GovernancePosture | undefined): true | undefined;
131
121
  /** token 泛型:真身是 branded CheckpointToken(秘密能力,只在本函数内部流转,绝不进响应体)。 */
132
122
  /**
133
123
  * SSE 车道的 done 帧 result(拒绝形)。与 409 body **同一铸体处** —— 此前 tasks.ts 在 res.write 里
@@ -177,7 +167,7 @@ interface ConflictProbeDeps<TToken> {
177
167
  findPendingTokenBySession?: (sessionId: string, scope?: string, binding?: PendingRowBinding) => Promise<TToken | null | undefined>;
178
168
  /** 结构形(不 import core 的 `Checkpoint`):只声明本模块**真读**的几格。`riskDescriptor` 是
179
169
  * core 在 mint 时挂到升级门上的判读元数据(display/triage only),`shellGateDoctrine` 是 2026-08-05
180
- * 裁定加的取证格 —— 见下方 `governanceOriginOf` 的推导论证。`checkpointId`([ref],core 5.42.0)
170
+ * 裁定加的取证格 —— 见 `runtime-governance.ts` 的 `governanceOriginOf` 推导论证。`checkpointId`([ref],core 5.42.0)
181
171
  * 是行的非秘密稳定身份,可选 —— legacy 行没有它(见 `PendingGateMaterial.checkpointId`)。 */
182
172
  get?: (token: TToken) => Promise<{
183
173
  checkpointId?: string;
@@ -1,4 +1,4 @@
1
- import { governanceMandatesShellGateAlways } from "../runtime-governance.js";
1
+ import { governanceOriginOf } from "../runtime-governance.js";
2
2
  import { isParkedRunStatus } from "../plugins/store-contracts.js";
3
3
  import { LIVE_RUN_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
4
4
  import { recordFailOpen } from "../observability/fail-open.js";
@@ -27,11 +27,6 @@ function safeCheckpointIdOf(raw, token) {
27
27
  return undefined;
28
28
  return raw;
29
29
  }
30
- export function governanceOriginOf(gate, governance) {
31
- if (gate?.riskDescriptor?.shellGateDoctrine !== "always")
32
- return undefined;
33
- return governanceMandatesShellGateAlways(governance ?? {}) ? true : undefined;
34
- }
35
30
  export function toDoneFrameResult(body) {
36
31
  return {
37
32
  status: "failed",
@@ -7,6 +7,7 @@ import { singleMemberOffFieldIssue } from "../request-value-closure.js";
7
7
  import { selfOrchestrationDenial } from "../task-workflow.js";
8
8
  import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
9
9
  import { parseHooksConfig } from "../hooks/hook-runner.js";
10
+ import { judgeRequestWebSearch } from "../plugins/web-search.js";
10
11
  import { normalizeApproachNotice, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
11
12
  import { isValidCwd, MAX_ADDITIONAL_DIRS, MAX_CWD_CHARS } from "../task-cwd.js";
12
13
  import { verifyRoundsFromBody } from "./verify-rounds.js";
@@ -193,6 +194,13 @@ export function createAdmission(ctx) {
193
194
  return null;
194
195
  }
195
196
  }
197
+ {
198
+ const webSearchVerdict = judgeRequestWebSearch(st.webSearch);
199
+ if (webSearchVerdict.kind === "malformed") {
200
+ sendError(res, 400, "request.field_invalid", webSearchVerdict.issue);
201
+ return null;
202
+ }
203
+ }
196
204
  }
197
205
  const skillsErr = validateUserSkills(body.skills);
198
206
  if (skillsErr) {
@@ -29,7 +29,7 @@ export declare function createResumeLegs(ctx: {
29
29
  routeCtxBase: RouteCtxBase;
30
30
  resumeCheckpoint: (sessionId: string, decision: "approve" | "deny", reason: string | undefined, hostDecision: HostDecision, req?: IncomingMessage, answer?: QuestionAnswer, binding?: DecideBinding, onResumeCommitted?: (grant: {
31
31
  overrideSessionId?: string;
32
- validatedToolName: string | null;
32
+ rememberableToolName: string | null;
33
33
  }) => Promise<void>, acceptEarly?: boolean, decider?: {
34
34
  principal?: string;
35
35
  explicitOperator: boolean;
@@ -1,6 +1,6 @@
1
1
  import { redactErrorMessage, redactHead } from "../observability/run-terminal.js";
2
2
  import { createHash } from "node:crypto";
3
- import { uuidv7, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, GLOBAL_USAGE_KEY, PAUSE_REGISTRY } from "@sema-agent/core";
3
+ import { uuidv7, resumeWithVerification, CheckpointError, defaultTaskRegistry, taskNotificationDedupKey, validatePendingSteer, GLOBAL_USAGE_KEY, PAUSE_REGISTRY } from "@sema-agent/core";
4
4
  import { decideParkedAgent, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
5
5
  import { HttpError, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE } from "../security.js";
6
6
  import { evictIfConflict, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
@@ -9,10 +9,10 @@ import { mintCancelledResult } from "../run-cancel-context.js";
9
9
  import {} from "../host-decision.js";
10
10
  import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
11
11
  import { recordResumeResourceWindow } from "../resource-window.js";
12
- import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
12
+ import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
13
13
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
14
14
  import { defaultSubagentTailBus } from "../fleet/subagent-tail-bus.js";
15
- import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, resolveApprovalLeg, resolveStreamApprovalGate } from "../tool-approval.js";
15
+ import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, parkRowStandingAnswerFacts, resolveApprovalLeg, resolveStreamApprovalGate, standingAnswerMayRetire } from "../tool-approval.js";
16
16
  import { buildCheckpointAnchorHash } from "../approval-ask-audit-store.js";
17
17
  import { TERMINAL_PARK_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
18
18
  import { withPrincipal } from "../observability/principal-context.js";
@@ -198,11 +198,14 @@ export function createResumeLegs(ctx) {
198
198
  };
199
199
  }
200
200
  const paForGrant = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction : undefined;
201
- const loadedGrantTool = paForGrant !== undefined && paForGrant.toolName && paForGrant.toolName !== ASK_USER_QUESTION_TOOL_NAME
201
+ const rememberableTool = paForGrant !== undefined &&
202
+ paForGrant.toolName &&
203
+ paForGrant.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
204
+ standingAnswerMayRetire(parkRowStandingAnswerFacts(paForGrant, cp.gate, deps.config))
202
205
  ? paForGrant.toolName
203
206
  : null;
204
207
  const boundGrant = onResumeCommitted
205
- ? (overrideSessionId) => onResumeCommitted({ ...(overrideSessionId !== undefined ? { overrideSessionId } : {}), validatedToolName: loadedGrantTool })
208
+ ? (overrideSessionId) => onResumeCommitted({ ...(overrideSessionId !== undefined ? { overrideSessionId } : {}), rememberableToolName: rememberableTool })
206
209
  : undefined;
207
210
  const lateSettleSink = deps.approvalLateSettle;
208
211
  const lateSettleAnchor = lateSettleSink !== undefined && req !== undefined && token
@@ -496,8 +499,9 @@ export function createResumeLegs(ctx) {
496
499
  const { type, ...rest } = frame;
497
500
  const f = frame;
498
501
  const write = rs0.appendEvent(taskId, drainSeq++, type, rest);
499
- if (f.type === "task_notification" && f.task_id)
500
- resumeNotifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), write.then(() => true, () => false));
502
+ const fKey = f.type === "task_notification" ? taskNotificationStreamKeyOf(f) : undefined;
503
+ if (fKey !== undefined)
504
+ resumeNotifiedKeys.set(fKey, write.then(() => true, () => false));
501
505
  return write;
502
506
  }, { route: "resume-open", connection: "durable-append", log: (m, x) => deps.logger?.info?.(m, x) });
503
507
  }
@@ -544,7 +548,7 @@ export function createResumeLegs(ctx) {
544
548
  void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(sessionId, principal ?? null, n, Date.now(), taskId, opts?.priority)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "resume-verify", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
545
549
  }
546
550
  else if (resumeLegLive) {
547
- const key = taskNotificationStreamKey(n);
551
+ const key = taskNotificationDedupKey(n);
548
552
  if (resumeNotifiedKeys.get(key) === undefined) {
549
553
  resumeNotifiedKeys.set(key, vAppend("task_notification", taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) })).then(() => true, () => false));
550
554
  }
@@ -671,7 +675,7 @@ export function createResumeLegs(ctx) {
671
675
  void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(sessionId, principal ?? null, n, Date.now(), taskId, opts?.priority)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "resume", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
672
676
  }
673
677
  else if (resumeLegLive) {
674
- const key = taskNotificationStreamKey(n);
678
+ const key = taskNotificationDedupKey(n);
675
679
  if (resumeNotifiedKeys.get(key) === undefined) {
676
680
  resumeNotifiedKeys.set(key, append("task_notification", taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) })).then(() => true, () => false));
677
681
  }
@@ -780,7 +784,7 @@ export function createResumeLegs(ctx) {
780
784
  case "task_notification": {
781
785
  await flush();
782
786
  const tnr = ev.notification;
783
- const tnrKey = tnr?.task_id ? taskNotificationStreamKey({ task_type: tnr.task_type, task_id: tnr.task_id, status: String(tnr.status), seq: tnr.seq }) : undefined;
787
+ const tnrKey = taskNotificationStreamKeyOf(tnr);
784
788
  const tnrPrior = tnrKey !== undefined ? resumeNotifiedKeys.get(tnrKey) : undefined;
785
789
  if (tnrPrior === undefined || !(await tnrPrior)) {
786
790
  const write = append("task_notification", taskNotificationEventData(ev));
@@ -357,11 +357,11 @@ export interface RouteLegs {
357
357
  resumeCheckpoint(sessionId: string, decision: "approve" | "deny", reason: string | undefined, hostDecision: HostDecision, req?: IncomingMessage, answer?: QuestionAnswer, binding?: DecideBinding,
358
358
  /** [ref] codex R4(验真后修):remember 豁免的 grant 挂点。**工具名不再由路由自己读**(那是与本函数
359
359
  * 载入行分歧的第三次 scope-less 读,三读分歧下会给他人工具落豁免=提权)——由本函数把**已属主校验的
360
- * 载入 cp** 的工具名(`validatedToolName`,非 tool_approval / AskUserQuestion / legacy null ⇒ `null`
360
+ * 载入 cp** 的可记工具名(`rememberableToolName`,非 tool_approval / AskUserQuestion / legacy null / S-663 强制卡 ⇒ `null`
361
361
  * = 无可记)一并回传,路由只对这个逐字落店。`overrideSessionId` = parked 腿的 root/host 会话 grant 锚。 */
362
362
  onResumeCommitted?: (grant: {
363
363
  overrideSessionId?: string;
364
- validatedToolName: string | null;
364
+ rememberableToolName: string | null;
365
365
  }) => Promise<void>,
366
366
  /** [ref]:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/decide` 腿传 `true`;
367
367
  * 内部 D-D SLA deny-sweep 不传 —— 它没有在等响应的人,提前受理对它零收益而少一条同步失败面。 */
@@ -2,7 +2,7 @@ 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
4
  import { type ToolApprovalCoordinator } from "../../tool-approval.js";
5
- import { type GovernancePosture } from "../active-run-conflict.js";
5
+ import { type GovernancePosture } from "../../runtime-governance.js";
6
6
  import { type RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
7
7
  import { type KeyClosureIssue } from "../../request-key-closure.js";
8
8
  /**
@@ -16,7 +16,7 @@ import { sleep } from "../sse-log.js";
16
16
  import { bindSseLifecycle } from "../sse-lifecycle.js";
17
17
  import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
18
18
  import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "../principal-gate.js";
19
- import { governanceOriginOf } from "../active-run-conflict.js";
19
+ import { governanceOriginOf } from "../../runtime-governance.js";
20
20
  import { sendResumeOutcome } from "../route-ctx.js";
21
21
  import { assertNever } from "../route-ctx.js";
22
22
  import { readResumeCtx, resumeCtxCorruptAnswer } from "../resume-ctx-read.js";
@@ -689,7 +689,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
689
689
  let pendingGrant;
690
690
  const grantOnCommit = remember && deps.approvalExemptionStore
691
691
  ? async (grant) => {
692
- const canonical = grant.validatedToolName;
692
+ const canonical = grant.rememberableToolName;
693
693
  if (!canonical)
694
694
  return;
695
695
  pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { ControlPlaneCorruptError } from "@sema-agent/core";
3
3
  import { erasureInputSmugglesPrototypeKey } from "../../memory-operator-faces.js";
4
+ import { MemoryLayerUnwritableError } from "../../memory-layer-preflight.js";
4
5
  import { sendJson, sendError } from "../send.js";
5
6
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
7
  import { assertNever } from "../route-ctx.js";
@@ -35,6 +36,10 @@ function codeOf(err) {
35
36
  return typeof code === "string" ? code : undefined;
36
37
  }
37
38
  function sentClassifiedFailure(res, ctx, face, err) {
39
+ if (face === "erase" && err instanceof MemoryLayerUnwritableError) {
40
+ sendError(res, 409, "memory.layer_unwritable", `memory layer directories are not writable on disk: ${err.dirList} — refused before the engine was called: nothing was written and no journal was left. Restore the owner write bit with \`chmod u+w ${err.chmodTargets}\` and retry. This is a known issue of the memory engine's mount: it leaves a layer that a session reads but does not write read-only on disk (directories 0555, entries 0444) and does not restore it, so a later project session can lock it again`);
41
+ return true;
42
+ }
38
43
  const coreCode = codeOf(err);
39
44
  if (coreCode !== undefined && sentCoreCode(res, coreCode, err instanceof Error ? err.message : String(err)))
40
45
  return true;
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { ControlPlaneCorruptError } from "@sema-agent/core";
3
3
  import { sendJson, sendError } from "../send.js";
4
4
  import { buildOriginFence, readOriginAware } from "../../memory-origin-fence.js";
5
+ import { MemoryLayerUnwritableError } from "../../memory-layer-preflight.js";
5
6
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
7
  import { assertNever } from "../route-ctx.js";
7
8
  export const MEMORY_ORIGIN_EXTERNAL_PATH = "/v1/memory/origin/external";
@@ -47,6 +48,10 @@ function codeOf(err) {
47
48
  return typeof code === "string" ? code : undefined;
48
49
  }
49
50
  function sentClassifiedFailure(res, ctx, face, err) {
51
+ if (face === "clear" && err instanceof MemoryLayerUnwritableError) {
52
+ sendError(res, 409, "memory.layer_unwritable", `memory layer directories are not writable on disk: ${err.dirList} — refused before the engine was called: nothing was written and no journal was left. Restore the owner write bit with \`chmod u+w ${err.chmodTargets}\` and retry. This is a known issue of the memory engine's mount: it leaves a layer that a session reads but does not write read-only on disk (directories 0555, entries 0444) and does not restore it, so a later project session can lock it again`);
53
+ return true;
54
+ }
50
55
  if (face === "clear") {
51
56
  const coreCode = codeOf(err);
52
57
  if (coreCode !== undefined && sentClearCode(res, coreCode, err instanceof Error ? err.message : String(err)))
@@ -9,6 +9,7 @@ import { readSessionPermissionRules, describeSessionPermissionRulesProblem } fro
9
9
  import { sendJson, sendError, sendSessionPolicyError, sseHeaders } from "../send.js";
10
10
  import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "../principal-gate.js";
11
11
  import { assertNever } from "../route-ctx.js";
12
+ import { MemoryLayerUnwritableError } from "../../memory-layer-preflight.js";
12
13
  const MCP_STATUS_TTL_MS = 5_000;
13
14
  const MCP_MATERIALIZE_TIMEOUT_MS = 10_000;
14
15
  const A2A_STATUS_TTL_MS = 5_000;
@@ -499,6 +500,11 @@ export async function handleSessions(req, res, match, ctx) {
499
500
  sendJson(res, 200, await faces.erase({ requestId: raw.requestId, select: { sessionId }, ...(raw.allowUnevidenced !== undefined ? { allowUnevidenced: raw.allowUnevidenced } : {}) }));
500
501
  }
501
502
  catch (err) {
503
+ if (err instanceof MemoryLayerUnwritableError) {
504
+ deps.logger?.warn?.("session_memory_erase_layer_unwritable", { sessionId, dirs: err.dirs });
505
+ sendError(res, 409, "memory.layer_unwritable", "memory erasure refused: a memory layer directory on this server is not writable on disk, so the erase was stopped before anything was written (nothing was deleted, no journal was left). Ask whoever operates this deployment to restore the owner write bit on the memory layer directories (a known issue of the memory engine's mount), then retry with the same requestId");
506
+ return;
507
+ }
502
508
  const code = err.code;
503
509
  if (code === "config.memory_erasure_request") {
504
510
  sendError(res, 400, "config.memory_erasure_request", err instanceof Error ? err.message : "invalid memory erasure request");
@@ -1,12 +1,12 @@
1
1
  import { redactErrorMessage, redactHead, redactTerminalResult } from "../../observability/run-terminal.js";
2
- import { runWithVerification, runCascade, uuidv7 } from "@sema-agent/core";
2
+ import { runWithVerification, runCascade, taskNotificationDedupKey, uuidv7 } from "@sema-agent/core";
3
3
  import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, resumeAtHttpStatus, HEARTBEAT_MS } from "../../runs.js";
4
4
  import { sealResultForExit, outwardPlane } from "../../terminal.js";
5
5
  import { withPrincipal } from "../../observability/principal-context.js";
6
6
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../../fleet/fleet-bus.js";
7
7
  import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
8
8
  import { createReasoningSegments, reasoningStreamOfTask } from "../../trace/model-segment.js";
9
- import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
9
+ import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
10
10
  import { createLedgerSink } from "../../trace/ledger-sink.js";
11
11
  import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
12
12
  import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../../turn-activity.js";
@@ -393,7 +393,7 @@ export async function handleTasks(req, res, match, ctx) {
393
393
  void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(prepared.spec.sessionId, askOwner, n, Date.now(), durableTaskId, opts?.priority)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "sync-stream", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
394
394
  }
395
395
  else if (deliverable) {
396
- const key = taskNotificationStreamKey(n);
396
+ const key = taskNotificationDedupKey(n);
397
397
  if (syncNotifiedKeys.get(key) === undefined) {
398
398
  syncNotifiedKeys.set(key, Promise.resolve(true));
399
399
  sseData(res, { type: "task_notification", ...taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) }) });
@@ -465,7 +465,7 @@ export async function handleTasks(req, res, match, ctx) {
465
465
  }
466
466
  else if (ev.type === "task_notification") {
467
467
  const tnv = ev.notification;
468
- const tnvKey = tnv?.task_id ? taskNotificationStreamKey({ task_type: tnv.task_type, task_id: tnv.task_id, status: String(tnv.status), seq: tnv.seq }) : undefined;
468
+ const tnvKey = taskNotificationStreamKeyOf(tnv);
469
469
  const tnvPrior = tnvKey !== undefined ? syncNotifiedKeys.get(tnvKey) : undefined;
470
470
  if (tnvPrior === undefined || !(await tnvPrior)) {
471
471
  if (tnvKey !== undefined)
@@ -553,8 +553,9 @@ export async function handleTasks(req, res, match, ctx) {
553
553
  if (closed || res.writableEnded || res.destroyed)
554
554
  throw new Error("stream ended before the completion frame was written");
555
555
  const f = frame;
556
- if (f.type === "task_notification" && f.task_id)
557
- syncNotifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), Promise.resolve(true));
556
+ const fKey = f.type === "task_notification" ? taskNotificationStreamKeyOf(f) : undefined;
557
+ if (fKey !== undefined)
558
+ syncNotifiedKeys.set(fKey, Promise.resolve(true));
558
559
  sseData(res, frame);
559
560
  }, { route: "sync-stream-open", connection: "live-sse", log: (m, x) => deps.logger?.info?.(m, x) });
560
561
  const withElicit = deps.elicitation
@@ -659,7 +659,7 @@ export interface ServiceDeploymentDeps {
659
659
  * registry 部署),现有部署零影响。main.ts 在 applyEffective 落模型后翻 true(one-way latch)。 */
660
660
  modelReady?: () => boolean;
661
661
  /** Sema-registry restart signal: the refresh loop sets this when a pull carries restart-to-apply
662
- * changes (skills/mcp/scenarios/runtime-gates baked at boot) that DIFFER from this process's boot config.
662
+ * changes (skills/mcp/a2a/scenarios/… baked at boot — the `RestartSlice` closed set) that DIFFER from this process's boot config.
663
663
  * Surfaced at GET /health as `restartRequired` + `restart{reasons,version,since}` so the orchestrator can
664
664
  * auto rolling-restart the worker. Change-detected (not presence) — absent when nothing baked-at-boot
665
665
  * changed, so polling it never causes a restart loop. */
@@ -686,10 +686,11 @@ export interface ServiceDeploymentDeps {
686
686
  mcpRevocationState?: () => {
687
687
  revoked: string[];
688
688
  } | undefined;
689
- /** codex R18 (models-tiers deferral): the persistent "deferred WITHOUT a next-boot handoff" state — set when a
690
- * changed model plane could not hot-apply (tier-frozen Runner / tiered candidate) AND no restart-surviving LKG
691
- * exists to justify forcing a restart. Surfaced on /health (`modelPlaneDeferred`) so the stuck-degraded state
692
- * outlives the one-shot warn (the etag advances → later ticks 304 and never re-enter the defer branch). */
689
+ /** codex R18: the persistent "retained WITHOUT a next-boot handoff" state — a candidate whose restart signal is
690
+ * withheld because no restart-surviving handoff exists yet (durable-declared LKG publication incomplete, or a
691
+ * boot skills debt not yet proven repairable). Surfaced on /health (`modelPlaneDeferred` — the key keeps its birth
692
+ * name: it was born for the model-plane deferral, which retired with the `models-tiers` slice in S-668 波 0) so
693
+ * the stuck state outlives the one-shot warn (the etag advances → later ticks 304). */
693
694
  planeDeferredState?: () => {
694
695
  version: number;
695
696
  since: number;
@@ -450,9 +450,12 @@ export interface TaskRequestBody {
450
450
  /** The caller's per-request settings stamp (the client-side `SemaSettings` shape). The KEY SET is deliberately
451
451
  * NOT mirrored here — a hand-written second shape on this interface would be the classic mirror-drift form.
452
452
  * Per-key ownership is split, not single: `parseTaskSettings` (src/task-settings.ts) defensively owns the
453
- * shared settings keys on every path, while `settings.webSearch` has its OWN validator on the single-user
454
- * scenario lane (capabilities/scenarios.ts `webSearchConfigFromSettings` — the multi-tenant lane ignores it
455
- * by design). The declared type is the INTENDED caller form (sibling convention: `oneShot` declares boolean,
453
+ * shared settings keys on every path, while `settings.webSearch` has its OWN judge (plugins/web-search.ts
454
+ * `judgeRequestWebSearch`, S-199): a malformed block — including a missing or unknown `provider` — 400s on submit
455
+ * on every lane, and only the single-user
456
+ * scenario lane honors a well-formed one (capabilities/scenarios.ts — the multi-tenant lane ignores it by design;
457
+ * a malformed block replayed on resume mounts no WebSearch for that call — never a substitute destination — with a
458
+ * fail-open count). The declared type is the INTENDED caller form (sibling convention: `oneShot` declares boolean,
456
459
  * runtime drops garbage): a non-array object bag, or omit the key. A `null` reads as absence on both the
457
460
  * fresh-submit wall (prepareSpec) and replay (parseTaskSettings), and malformed forms 400 on submit /
458
461
  * no-op on replay. ([ref] 件1 leftover key, declared 2026-08-31; ownership wording per codex adversarial round.)