@sema-agent/server 7.103.2 → 7.105.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 (204) hide show
  1. package/MIGRATION.md +378 -2
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +5 -1
  4. package/USAGE.md +320 -16
  5. package/deploy/sema-up/README.md +57 -7
  6. package/deploy/sema-up/cluster-up.sh +18 -4
  7. package/deploy/sema-up/kube-up.sh +16 -0
  8. package/deploy/sema-up/lib/dataroot.sh +142 -0
  9. package/deploy/sema-up/lib/flagshape.sh +69 -0
  10. package/deploy/sema-up/lib/lanes.py +14 -4
  11. package/deploy/sema-up/lib/shell-access.sh +41 -0
  12. package/deploy/sema-up/preflight.sh +14 -1
  13. package/deploy/sema-up/sema-up.sh +134 -37
  14. package/deploy/sema-up/smoke.sh +5 -0
  15. package/dist/auto-mode-face.js +3 -4
  16. package/dist/boot/budget-tracing.js +1 -1
  17. package/dist/boot/config-center.d.ts +6 -2
  18. package/dist/boot/config-center.js +5 -59
  19. package/dist/boot/execution-env.d.ts +11 -0
  20. package/dist/boot/execution-env.js +14 -0
  21. package/dist/boot/execution-lane.d.ts +25 -0
  22. package/dist/boot/execution-lane.js +95 -0
  23. package/dist/boot/memory-consolidation.d.ts +185 -9
  24. package/dist/boot/memory-consolidation.js +192 -32
  25. package/dist/boot/resolve-spec.d.ts +2 -0
  26. package/dist/boot/resolve-spec.js +28 -24
  27. package/dist/boot/runner-deps.d.ts +6 -2
  28. package/dist/boot/runner-deps.js +6 -1
  29. package/dist/boot/session-faces.d.ts +5 -0
  30. package/dist/boot/session-faces.js +18 -2
  31. package/dist/boot/session-incarnation-seat.d.ts +41 -0
  32. package/dist/boot/session-incarnation-seat.js +69 -0
  33. package/dist/boot/stage-01-config.js +2 -2
  34. package/dist/boot/stage-02-registry-pull.js +3 -1
  35. package/dist/boot/stage-04-stores.d.ts +2 -0
  36. package/dist/boot/stage-04-stores.js +3 -1
  37. package/dist/boot/stage-05-execution-env.d.ts +4 -0
  38. package/dist/boot/stage-05-execution-env.js +2 -2
  39. package/dist/boot/stage-06-runners.d.ts +4 -1
  40. package/dist/boot/stage-06-runners.js +7 -14
  41. package/dist/boot/stage-07-capability-layer.d.ts +4 -1
  42. package/dist/boot/stage-08-reapers.d.ts +4 -1
  43. package/dist/boot/stage-09-leader.d.ts +6 -1
  44. package/dist/boot/stage-09-leader.js +2 -1
  45. package/dist/boot/stage-10-http-server.d.ts +6 -1
  46. package/dist/boot/stage-10-http-server.js +4 -1
  47. package/dist/brain.d.ts +1 -1
  48. package/dist/config-catalog.d.ts +16 -1
  49. package/dist/config-catalog.js +9 -9
  50. package/dist/config-center/restart-signal.d.ts +4 -4
  51. package/dist/config-invariants.d.ts +7 -1
  52. package/dist/config-invariants.js +37 -6
  53. package/dist/config-provider.d.ts +42 -10
  54. package/dist/config-provider.js +18 -16
  55. package/dist/config-types.d.ts +37 -16
  56. package/dist/config-types.js +1 -0
  57. package/dist/config.d.ts +28 -30
  58. package/dist/config.js +95 -77
  59. package/dist/env-facts.d.ts +13 -3
  60. package/dist/env-facts.js +15 -3
  61. package/dist/execution-lane-caps.d.ts +15 -0
  62. package/dist/execution-lane-caps.js +8 -0
  63. package/dist/file-history-capture.d.ts +42 -35
  64. package/dist/file-history-capture.js +15 -10
  65. package/dist/file-history-disclosure.d.ts +7 -31
  66. package/dist/file-history-disclosure.js +12 -0
  67. package/dist/fleet/fleet-bus.d.ts +5 -1
  68. package/dist/http/admission.d.ts +12 -5
  69. package/dist/http/admission.js +5 -1
  70. package/dist/http/dispatch.d.ts +18 -0
  71. package/dist/http/dispatch.js +19 -6
  72. package/dist/http/principal-gate.d.ts +22 -0
  73. package/dist/http/principal-gate.js +20 -0
  74. package/dist/http/resume-legs.d.ts +1 -1
  75. package/dist/http/resume-legs.js +9 -5
  76. package/dist/http/route-table.d.ts +15 -4
  77. package/dist/http/route-table.js +6 -5
  78. package/dist/http/routes/agents-roster.d.ts +7 -0
  79. package/dist/http/routes/agents-roster.js +6 -2
  80. package/dist/http/routes/approvals-assistant.d.ts +48 -19
  81. package/dist/http/routes/approvals-assistant.js +117 -50
  82. package/dist/http/routes/capabilities.js +3 -2
  83. package/dist/http/routes/config-catalog.js +1 -0
  84. package/dist/http/routes/fleet.d.ts +3 -2
  85. package/dist/http/routes/images.js +0 -1
  86. package/dist/http/routes/memory-consolidation.d.ts +3 -2
  87. package/dist/http/routes/memory-policy.js +3 -3
  88. package/dist/http/routes/rules.js +2 -2
  89. package/dist/http/routes/runs.js +29 -10
  90. package/dist/http/routes/session-sync.js +14 -15
  91. package/dist/http/routes/sessions-list.js +3 -2
  92. package/dist/http/routes/sessions.js +49 -25
  93. package/dist/http/routes/shared-memory.js +2 -2
  94. package/dist/http/routes/side-query.js +8 -1
  95. package/dist/http/routes/tasks.js +22 -19
  96. package/dist/http/routes/trace-usage.d.ts +2 -0
  97. package/dist/http/routes/trace-usage.js +8 -13
  98. package/dist/http/send.d.ts +12 -0
  99. package/dist/http/send.js +12 -1
  100. package/dist/http/server.d.ts +30 -8
  101. package/dist/http/server.js +11 -5
  102. package/dist/http/sse-lifecycle.d.ts +1 -1
  103. package/dist/http/sse-log.d.ts +12 -9
  104. package/dist/http/sse-log.js +1 -8
  105. package/dist/http/sync-submit-body.d.ts +43 -0
  106. package/dist/http/sync-submit-body.js +4 -0
  107. package/dist/http/wire-types.d.ts +23 -5
  108. package/dist/index.d.ts +1 -0
  109. package/dist/index.js +1 -0
  110. package/dist/leader/merge.d.ts +8 -3
  111. package/dist/memory-incident-sink.js +2 -1
  112. package/dist/model-select.d.ts +9 -0
  113. package/dist/model-select.js +5 -1
  114. package/dist/observability/corrupt-read-seat.d.ts +151 -16
  115. package/dist/observability/corrupt-read-seat.js +158 -15
  116. package/dist/observability/fail-open.d.ts +17 -1
  117. package/dist/observability/fail-open.js +17 -1
  118. package/dist/observability/metrics.js +1 -1
  119. package/dist/observability/run-terminal.d.ts +60 -10
  120. package/dist/observability/run-terminal.js +119 -9
  121. package/dist/parked-decide.d.ts +8 -3
  122. package/dist/parked-decide.js +56 -20
  123. package/dist/parked-revive-surface.d.ts +14 -7
  124. package/dist/parked-revive-surface.js +5 -1
  125. package/dist/plugins/approval-ask-store-file.d.ts +4 -1
  126. package/dist/plugins/approval-ask-store-file.js +9 -12
  127. package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
  128. package/dist/plugins/approval-ask-store-sql.js +4 -1
  129. package/dist/plugins/approval-exemption-store.d.ts +21 -1
  130. package/dist/plugins/approval-exemption-store.js +36 -7
  131. package/dist/plugins/approval-nonce-store.d.ts +16 -2
  132. package/dist/plugins/approval-nonce-store.js +17 -7
  133. package/dist/plugins/caching-session-store.d.ts +5 -0
  134. package/dist/plugins/caching-session-store.js +3 -0
  135. package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
  136. package/dist/plugins/file-resume-anchor-store.js +19 -5
  137. package/dist/plugins/file-run-store.d.ts +21 -8
  138. package/dist/plugins/file-run-store.js +60 -38
  139. package/dist/plugins/image-bake-store-sql.d.ts +0 -2
  140. package/dist/plugins/image-bake-store-sql.js +0 -5
  141. package/dist/plugins/local-session-store.d.ts +22 -0
  142. package/dist/plugins/local-session-store.js +74 -11
  143. package/dist/plugins/memory-run-store.d.ts +7 -5
  144. package/dist/plugins/memory-run-store.js +11 -4
  145. package/dist/plugins/one-time-migrations.d.ts +38 -1
  146. package/dist/plugins/one-time-migrations.js +143 -15
  147. package/dist/plugins/permission-rule-store-file.d.ts +9 -2
  148. package/dist/plugins/permission-rule-store-file.js +37 -51
  149. package/dist/plugins/pg-session-storage.d.ts +3 -0
  150. package/dist/plugins/pg-session-storage.js +19 -10
  151. package/dist/plugins/retention-store-sql.js +1 -0
  152. package/dist/plugins/run-store-sql.d.ts +15 -6
  153. package/dist/plugins/run-store-sql.js +15 -13
  154. package/dist/plugins/schema-indexes.js +1 -0
  155. package/dist/plugins/send-file-ledger.d.ts +5 -1
  156. package/dist/plugins/send-file-ledger.js +11 -6
  157. package/dist/plugins/session-event-payload.d.ts +14 -4
  158. package/dist/plugins/session-event-payload.js +5 -2
  159. package/dist/plugins/session-policy-store-sql.js +18 -5
  160. package/dist/plugins/store-backend.d.ts +13 -7
  161. package/dist/plugins/store-backend.js +10 -6
  162. package/dist/plugins/store-contracts.d.ts +7 -2
  163. package/dist/plugins/tidb-pool.js +12 -5
  164. package/dist/plugins/tidb-session-store.d.ts +6 -1
  165. package/dist/plugins/tidb-session-store.js +17 -10
  166. package/dist/project-identity.d.ts +4 -6
  167. package/dist/read-face-posture.d.ts +21 -0
  168. package/dist/read-face-posture.js +9 -0
  169. package/dist/request-key-closure.js +1 -1
  170. package/dist/request-value-closure.d.ts +72 -3
  171. package/dist/request-value-closure.js +12 -4
  172. package/dist/roster-bound-http-status.d.ts +6 -0
  173. package/dist/roster-bound-http-status.js +6 -0
  174. package/dist/rules-consent.js +0 -11
  175. package/dist/run-local.js +22 -27
  176. package/dist/runs.d.ts +22 -2
  177. package/dist/runs.js +29 -7
  178. package/dist/sealed-key.d.ts +31 -4
  179. package/dist/sealed-key.js +9 -0
  180. package/dist/security.d.ts +5 -1
  181. package/dist/session-children-retire.d.ts +92 -0
  182. package/dist/session-children-retire.js +85 -0
  183. package/dist/session-error-wire.d.ts +58 -0
  184. package/dist/session-error-wire.js +38 -0
  185. package/dist/session-incarnation.d.ts +61 -0
  186. package/dist/session-incarnation.js +43 -0
  187. package/dist/session-policy-wire.d.ts +98 -12
  188. package/dist/session-policy-wire.js +71 -8
  189. package/dist/session-records-skipped-notice.d.ts +20 -0
  190. package/dist/session-records-skipped-notice.js +27 -0
  191. package/dist/tool-approval.d.ts +65 -0
  192. package/dist/tool-approval.js +36 -9
  193. package/dist/trace/core-keyset-guard.d.ts +8 -7
  194. package/dist/trace/engine-notice-wire.d.ts +3 -1
  195. package/dist/trace/engine-notice-wire.js +2 -0
  196. package/dist/trace/project.d.ts +11 -6
  197. package/dist/trace/project.js +12 -3
  198. package/dist/trace/redact.d.ts +7 -0
  199. package/dist/trace/redact.js +153 -61
  200. package/dist/trace/wire-projection-faces.d.ts +10 -3
  201. package/dist/trace/wire-projection-faces.js +8 -2
  202. package/package.json +2 -2
  203. package/dist/plugins/memory-session-policy-store.d.ts +0 -46
  204. package/dist/plugins/memory-session-policy-store.js +0 -52
@@ -47,6 +47,13 @@ export declare const TASK_WRITE_FACE_ESCALATIONS: readonly ["allow", "refuse"];
47
47
  export type TaskWriteFaceEscalation = (typeof TASK_WRITE_FACE_ESCALATIONS)[number];
48
48
  export declare const RETENTION_MODES: readonly ["audit-only", "enforce"];
49
49
  export type RetentionMode = (typeof RETENTION_MODES)[number];
50
+ /**
51
+ * S-841 —— `CONFIG_PROVIDER` 的词表闭集(**属主在此**:`loadConfig` 的唯一读点、config-center 工厂、执行车道合成步、
52
+ * 配置目录的 enum 行共读这一份)。缺席 / 空串 = 未表态(配了 `SEMA_REGISTRY_URL` ⇒ 中心,否则纯 env);两词之外 ⇒ 拒启。
53
+ * 修前是开放词:认不出的词被配置源判官当 `local`、被隐式 host 臂当「不是 local」,一个旋钮在一个进程里有两种读法。
54
+ */
55
+ export declare const CONFIG_PROVIDER_WORDS: readonly ["local", "remote"];
56
+ export type ConfigProviderWord = (typeof CONFIG_PROVIDER_WORDS)[number];
50
57
  /** [ref] §3/§4 —— sweep lane 的节律与灰度档(语义与三问见 {@link ServiceConfigFlat.retentionSweep})。 */
51
58
  export interface RetentionSweepConfig {
52
59
  /** `audit-only` = 照跑照判、只记 `audit_only` 行,**不调任何破坏性方法**;`enforce` = 真删。默认前者。 */
@@ -193,9 +200,17 @@ export declare function buildDefaultResilience(): ServiceConfigFlat["resilience"
193
200
  * {@link ServiceConfigFlat.memoryConsolidationDriver})。纯数据。
194
201
  */
195
202
  export interface MemoryConsolidationDriverConfig {
196
- /** 阀门(env `MEMORY_CONSOLIDATION_DRIVER=on|off`,缺省 `off`)。`false` = 零装配零探针:
197
- * 座不解析、引擎不构造、两个 admin 口整个不挂(404 族,**不是** 501)、能力位翻假。 */
198
- enabled: boolean;
203
+ /**
204
+ * 阀门的**部署表态**(env `MEMORY_CONSOLIDATION_DRIVER=on|off`,原词)。**缺席 = 没表态,不是 off**。
205
+ *
206
+ * 🔴 S-824 / clay **[ref]**(取代 [ref] ① 的「出厂 off」实况):出厂缺省 = **开,但只在整理座不落主模型时生效**
207
+ * (`MODEL_CHEAP_ID` 在场,或目录给 consolidate / summarize / flash 档位绑了别的模型);否则不开并在起服时提示一行。
208
+ * 这件事要等**目录应用之后**、记忆引擎接线之后才判得出来,所以本字段只记表态,开不开由 boot 第 6 段的
209
+ * `resolveConsolidationValve`(`boot/memory-consolidation.ts`)一次判定 —— 那是阀门开关**唯一**的属主。
210
+ * 显式 `on` / `off` 恒赢;阀门关 = 零装配零探针:座不解析、引擎不构造、两个 admin 口整个不挂(404 族,
211
+ * **不是** 501)、能力位翻假。
212
+ */
213
+ valve?: "on" | "off";
199
214
  /**
200
215
  * 周期腿节律(秒;env `MEMORY_CONSOLIDATION_INTERVAL_SEC`,缺省 `0` = 不周期跑)。
201
216
  * 🔴 **本 build 里任何正值都拒启**:结构上没有周期腿(没有可用的真互斥基建 —— 全文与偏离登记见
@@ -225,7 +240,8 @@ export interface MemoryConsolidationDriverConfig {
225
240
  * · `true`(`=on`)= 武装。**武装是一条出口决定,不是性能旋钮**:每个建议任务之后会有一次整库整理跑,
226
241
  * 记忆内容自动外流到配置的整理模型 —— 设它之前请读 `boot/memory-consolidation.ts` 的 EGRESS 段。
227
242
  *
228
- * ⚠️ **前置 = `MEMORY_CONSOLIDATION_DRIVER=on`**(且记忆引擎接了线)。阀门开着 ⇒ 引擎那半场的整理两席
243
+ * ⚠️ **前置 = 整理阀门开着**(`MEMORY_CONSOLIDATION_DRIVER=on`,或未设而 [ref] 出厂缺省派生成开 —— 见 {@link valve};
244
+ * 且记忆引擎接了线)。阀门开着 ⇒ 引擎那半场的整理两席
229
245
  * (`RunnerDeps.memoryConsolidation` + `memoryConsolidationDriver`)由**同一只已解析座**接上
230
246
  * (S-399,`boot/runner-deps.ts` 的 `ctx.consolidationSeat` 那一段)⇒ 本旋钮真的生效;阀门关着 ⇒ 两席
231
247
  * 一个键都不写,而 core 对「武装但没接整理席」的处置是**每一次 prepare 都抛** `config.auto_consolidation`
@@ -761,12 +777,12 @@ export interface ServiceConfigFlat {
761
777
  /**
762
778
  * [ref](core 5.58 [ref] 片②)—— 记忆 **consolidation driver 阀门**的三根旋钮。
763
779
  * **恒在场**(不是可选块,与 {@link retentionSweep} 同姿态):三根都有文档化的缺省,而缺省本身
764
- * 是一条要能被读出来的姿态 —— `{enabled:false, intervalSec:0, scopes:[]}` = 「阀门关着」。
780
+ * 是一条要能被读出来的姿态 —— `{intervalSec:0, scopes:[]}`(`valve` 键缺席)= 「阀门没表态,出厂缺省在 boot 期派生」。
765
781
  *
766
782
  * 三问(设计稿 §消长账):谁需要 = 记忆库只涨不折的 TOB 部署(检索质量随库龄衰减);谁被伤 =
767
783
  * 误开 + 错 role 的部署(一轮 consolidation 读全库 ~1e5 prompt tokens,静默升级到最贵 default
768
- * 就是把一次维护跑成一笔意外账单);补偿 = 阀门默认关 + 座解析失败拒启 + D3 投影把每轮花了什么
769
- * 摆出来 + v1 只有**手动**阀门(不信任周期腿之前可人工控节奏)。
784
+ * 就是把一次维护跑成一笔意外账单);补偿 = 出厂缺省只在整理座**不落主模型**时开([ref];落主模型 ⇒ 不开 +
785
+ * 起服提示)+ 座解析失败拒启 + D3 投影把每轮花了什么摆出来 + v1 只有**手动**阀门(不信任周期腿之前可人工控节奏)。
770
786
  *
771
787
  * 语义与解析见 {@link MemoryConsolidationDriverConfig} 与 config.ts 的 `parseMemoryDomain`。
772
788
  */
@@ -1051,11 +1067,12 @@ export interface ServiceConfigFlat {
1051
1067
  fileHistory?: {
1052
1068
  retention?: FileHistoryRetentionPolicy;
1053
1069
  };
1054
- /** S-589:文件历史**捕获**总开关(`FILE_HISTORY_CAPTURE`,布尔词表,缺省 true,未知词拒启)。三条车道
1055
- * (local / mysql / pg)同一语义:`false` ⇒ 装配点不把历史店交给 core(`RunnerDeps.fileHistoryStore` 缺席 ⇒
1056
- * 首次触碰捕获结构上不发生),同时没有 rewind / restore / `rewindFilesTo` / 2c session-sync(那几位能力位与
1057
- * 能力词 `fileHistoryCapture:"off"` 同源翻关)。**只管写入,不管擦除**:已捕获的历史照旧随会话删除 / 留存
1058
- * 规则清理,翻旋钮本身不做批量删除。判定的唯一属主是 `file-history-capture.ts` 的 `assembleFileHistory`。
1070
+ /** S-589 / S-724:文件历史**捕获**总开关(`FILE_HISTORY_CAPTURE`,布尔词表,缺省 true,未知词拒启)。三条车道
1071
+ * (local / mysql / pg)同一语义:`false` ⇒ 装配点照接历史店,但给 core 的部署捕获位 `RunnerDeps.fileHistoryCapture: "off"`
1072
+ * (首次触碰备份 / 回合边界 / fork 携带结构上不发生;能力词 `fileHistoryCapture:"off"` 同一只判定派生)。**只关捕获**:
1073
+ * 已存历史照常服务 restore / rewind / 2c session-sync(S-589 时旋钮关 = 摘店,连已存历史也读不了;S-724 改落 core 的捕获位)。
1074
+ * **不管擦除**:已捕获的历史照旧随会话删除 / 留存规则清理,翻旋钮本身不做批量删除。没有店的部署形不下发捕获位
1075
+ * (core 对「off 但没店」拒 prepare,而没店本来就不捕获)。判定的唯一属主是 `file-history-capture.ts`。
1059
1076
  * 刻意**不**并进上面的 `fileHistory` 键:那一格是 core 店构造选项的同域键形,原样透传给 core。 */
1060
1077
  fileHistoryCapture: boolean;
1061
1078
  /** plugins 域 clone host 白名单(`PLUGINS_ALLOW_HOSTS`,缺省 github.com;契约 [ref]① 双保险面,
@@ -1526,8 +1543,11 @@ export interface ServiceConfigFlat {
1526
1543
  selectEnvironmentTool: boolean;
1527
1544
  /** RFC A1: inject structured sandbox envFacts into the task (`TaskSpec.envFacts` — core 1.240.0 whitelist
1528
1545
  * field; rendered as the `# Environment` block, sanitized+bounded core-side). Default ON —
1529
- * facts only ride when the deployment actually knows them (image binding/pkgSource/egress), absent = block
1530
- * unchanged. `SANDBOX_ENV_FACTS=false` opts out. */
1546
+ * facts only ride when the deployment actually knows them (image binding/pkgSource/egress/resume facts), absent =
1547
+ * block unchanged. `SANDBOX_ENV_FACTS=false` opts out of the SANDBOX facts only.
1548
+ * 🔴 S-842:the session scratchpad declaration (`envFacts.scratchpadDir`) is NOT under this knob — it is the write
1549
+ * surface's declaration (core opens the root-fence hole for it and teaches it by the same verdict), not a display
1550
+ * fact; gating it would close the scratchpad together with the display. */
1531
1551
  envFactsEnabled: boolean;
1532
1552
  /** ③ sensitive-path write DENY set (core 1.295 `createSensitivePathPolicy`). The PATTERN SET is core's call
1533
1553
  * (RECOMMENDED_SENSITIVE_PATTERNS — .env/.ssh/keys/.git hooks+config/cloud creds/histories, rationale
@@ -2054,8 +2074,9 @@ export interface ServiceConfigFlat {
2054
2074
  centerCredentialRenewLeadMs: number;
2055
2075
  /** Dual-mode config SOURCE (DUAL-MODE-DESIGN §4): "remote" (sema-registry HTTP) | "local" (.env + config.d
2056
2076
  * via @sema-agent/settings-schema FileConfigStore) | unset → remote if SEMA_REGISTRY_URL (legacy CONFIG_CENTER_URL now boot-rejects, no dual-read) is set, else pure-env.
2057
- * Env: CONFIG_PROVIDER. The same resolver/EffectiveConfig either way (can't drift). */
2058
- configProvider?: string;
2077
+ * Env: CONFIG_PROVIDER —— S-841 起闭集 {@link CONFIG_PROVIDER_WORDS}(认不出的词拒启;空串 = 未设)。
2078
+ * The same resolver/EffectiveConfig either way (can't drift). */
2079
+ configProvider?: ConfigProviderWord;
2059
2080
  /** Root dir holding `config.d/<domain>.json` for the local config source. Env: CONFIG_LOCAL_DIR. */
2060
2081
  configLocalDir?: string;
2061
2082
  }
@@ -1,6 +1,7 @@
1
1
  import { DRAIN_GRACE_FLOOR_MS } from "@sema-agent/settings-schema/fleet";
2
2
  export const TASK_WRITE_FACE_ESCALATIONS = ["allow", "refuse"];
3
3
  export const RETENTION_MODES = ["audit-only", "enforce"];
4
+ export const CONFIG_PROVIDER_WORDS = ["local", "remote"];
4
5
  export const MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC = 300;
5
6
  export const MAX_TIMER_DELAY_MS = 2_147_483_647;
6
7
  export const AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS = { minMs: 1_000, maxMs: MAX_TIMER_DELAY_MS };
package/dist/config.d.ts CHANGED
@@ -75,8 +75,9 @@ export declare function parseNumOrFailNonNegative(name: string, raw: string | un
75
75
  * 采用值也被报成夹取、而钥集条目**被丢弃**(没有任何采用值、也不该印 raw)却被报成「坏值回默认」。
76
76
  * 告警规则是按事件名分流的,报错的种类就是报错的事实本身;可选字段的在场性是实现细节,不该被当契约用。
77
77
  *
78
- * `invalid-default` 是**缺省种类**(不写 `kind` 即是它)—— 历史形 `{env,raw}` 的三个存量生产者
79
- * (`optFinitePositiveEnv` / `boolEnv` / `boolEnvWithLegacyNegated`)因此逐字不动,它们的断言也不破。
78
+ * `invalid-default` 是**缺省种类**(不写 `kind` 即是它)—— 历史形 `{env,raw}` 的存量生产者(`optFinitePositiveEnv`)
79
+ * 因此逐字不动,它的断言也不破。(布尔族 `boolEnv` 自 [ref]-rc2 起未知词在 `parseBoolWord` 里拒启,不再往这里 push;
80
+ * S-588 起旧负名走墓碑表 —— 两者都不是本种类的生产者。)
80
81
  */
81
82
  type ConfigWarningKind = "invalid-default" | "clamped" | "empty-preserved" | "entry-rejected" | "set-rejected" | "ineffective";
82
83
  interface ConfigWarning {
@@ -90,10 +91,23 @@ interface ConfigWarning {
90
91
  detail?: string;
91
92
  }
92
93
  export declare function drainConfigWarnings(): ConfigWarning[];
93
- export declare function drainConfigNotices(): Array<{
94
+ /** Boot NOTICES ([ref]④): non-error decisions loadConfig makes that the operator should see once (e.g. the
95
+ * MODEL_PROVIDER inference below). Same pre-logger collection pattern as CONFIG_WARNINGS, but each entry
96
+ * carries its own event name + fields (the warnings drain has one fixed "invalid, using default" message
97
+ * which would misdescribe an inference). main.ts drains + logs right after createLogger.
98
+ *
99
+ * S-176 —— 每条带一个**级别**(闭集两词,排空处穷举 switch)。此前这一格固定按 warn 排空,于是「每次 boot 都成立的
100
+ * 常态事实」(posture 派生的档位)在这里没有位置:进 warn 会让一台一个字节都没配错的单机每次启动报警,另开一个
101
+ * 收集器又是第五条日志族。级别写在**铸点**(不是按事件名在排空处猜):
102
+ * · `warn` —— 运维要看一眼的决定(推断出的 provider、被丢掉的缺省行、no-op 旋钮……):既有的每一条;
103
+ * · `info` —— 常态事实,每次这样配就每次成立,不是异常(与执行车道落点那一行同级)。 */
104
+ type ConfigNoticeLevel = "warn" | "info";
105
+ export interface ConfigNotice {
106
+ level: ConfigNoticeLevel;
94
107
  event: string;
95
108
  fields: Record<string, string>;
96
- }>;
109
+ }
110
+ export declare function drainConfigNotices(): ConfigNotice[];
97
111
  /** F10 (codex audit): drain + log BOTH pre-logger diagnostic collectors (S20 soft-knob warnings + [ref]④ boot
98
112
  * notices) in one place. EVERY entrypoint that calls loadConfig() then builds a logger must call this right
99
113
  * after (main.ts, run-local.ts) — a new entry that forgets it would silently drop operator-facing diagnostics
@@ -169,10 +183,10 @@ export declare function buildUnpricedCeilingNotice(models: Record<string, Model>
169
183
  * stated, so it was re-invented per line and the suffix stopped predicting anything.
170
184
  *
171
185
  * Semantics are the two dialects' semantics VERBATIM — an opt-in knob is `boolEnv(x, false)`, an opt-out knob is
172
- * `boolEnv(x, true)` — so this migration is value-preserving on every input. The one addition: a value that is
173
- * neither literal (`1`, `yes`, `TRUE`) still lands on the knob's default (as before) but now raises a config
174
- * warning instead of being swallowed, which is the whole failure mode operators kept hitting (`LSP_ENABLED=1`
175
- * reads as "on" to a human and as "off" to `=== "true"`).
186
+ * `boolEnv(x, true)` — for the two literals and for unset. Anything else goes through `parseBoolWord` ([ref]-rc2,
187
+ * [ref]): `1`/`0`/`yes`/`no`/`on`/`off` in any case are read as the operator meant them (`LSP_ENABLED=1` reads as
188
+ * "on" to a human, and now to the parser too), and an unknown word REFUSES TO BOOT — it no longer lands on the
189
+ * default with a warning (that was this helper's first form, retired with [ref]-rc2; S-588 corrected this note).
176
190
  *
177
191
  * Note the deliberate non-migrations: the posture family (`postureOn`) keeps its tri-state — its unset case is
178
192
  * neither `true` nor `false` but "single-user ∧ infra-ready" — and the sentinel-valued knobs (`MEMORY_ENGINE`,
@@ -183,7 +197,7 @@ export declare function buildUnpricedCeilingNotice(models: Record<string, Model>
183
197
  * 用",而实际设了就拒启——诊断面撒的谎比没有诊断更贵(它会让人先去试那个必然拒启的开关)。 */
184
198
  type ConfigKnobSource = "env" | "posture" | "default";
185
199
  export interface ConfigKnobRecord {
186
- /** The env name. 3.0.0 起只有这一个名字 —— 负名别名已是墓碑(见 `boolEnvWithLegacyNegated`)。 */
200
+ /** The env name. 3.0.0 起只有这一个名字 —— 负名别名已是墓碑(`RETIRED_ENVS` 的 replaced 行,S-588 起在场即拒)。 */
187
201
  name: string;
188
202
  /** `opt-in` = default OFF, `opt-out` = default ON, `posture` = default derived from REQUIRE_PRINCIPAL ∧ infra. */
189
203
  polarity: "opt-in" | "opt-out" | "posture";
@@ -192,28 +206,12 @@ export interface ConfigKnobRecord {
192
206
  }
193
207
  /** The registered boolean knobs, sorted by name — the data behind `logConfigDiagnostics`'s polarity table. */
194
208
  export declare function configKnobTable(): ConfigKnobRecord[];
195
- /** [ref] N8 (D-family) + server 3.0.0 墓碑 —— a knob whose CANONICAL name is positive (`X_ENABLED`,
196
- * default ON) and whose retired negative name (`X_DISABLED`) is now a **fail-loud tombstone**.
197
- *
198
- * Why the flip at all: `X_DISABLED !== "true"` is a double negative at the read site, so the four knobs that
199
- * used it (`PROJECT_MEMORY`, `CONFIG_LKG`, `HOST_BG`, `HOST_EXEC_SPOOL`) could not be read for their default
200
- * without inverting twice in your head, and `projectMemoryDisabled` was the ONE negative boolean on a
201
- * `ServiceConfig` that otherwise carries 16 positive `*Enabled`/`*Allowed` fields.
202
- *
203
- * 🔴 3.0.0([ref],clay 令「不做兼容」):旧名不再有兼容窗——**设了就拒启**。这个函数保留 legacy 形参
204
- * 不是为了继续读它,而正是为了**认出它在场并拒绝启动**(把墓碑放在唯一知道这对名字关系的地方)。
205
- *
206
- * Resolution order:
207
- * 1. `X_ENABLED` set to a literal ⇒ that.
208
- * 2. `X_ENABLED` set to a non-literal ⇒ default + warning (the tombstone is NOT consulted — a typo in the
209
- * new name must not turn into a boot refusal about an env the operator never set).
210
- * 3. `X_ENABLED` unset ∧ `X_DISABLED` set(**任何值**,含 `"false"`)⇒ throw,文案指路新名。选拒启不选
211
- * 静默忽略:静默会让升级部署的旧开关名义在、实际归默认——比拒启危险,且下游冒烟测试测不出来。
212
- * 4. neither ⇒ default. */
213
- export declare function boolEnvWithLegacyNegated(name: string, legacyNegatedName: string, def: boolean): boolean;
214
- /** LKG (last-known-good) persistence of the remote effective-config fetch. Read LAZILY at both main.ts sites
209
+ /** [ref] B4 ②:D 族四枚(`PROJECT_MEMORY` / `CONFIG_LKG` / `HOST_BG` / `HOST_EXEC_SPOOL`)的正名 —— 缺省开,读点只读正名。
210
+ * 3.0.0 之前它们叫 `X_DISABLED`(读点上的双重否定);那四个旧负名是墓碑,在 `RETIRED_ENVS` 里(S-588 起在场即拒启,不论正名在不在)。
211
+ *
212
+ * LKG (last-known-good) persistence of the remote effective-config fetch. Read LAZILY at both main.ts sites
215
213
  * rather than lifted onto `ServiceConfig`, so this stays a pure env knob; `loadConfig` pre-registers it (below)
216
- * so its polarity row and any deprecation notice appear at boot with everything else. */
214
+ * so its polarity row appears at boot with everything else. */
217
215
  export declare const configLkgEnabled: () => boolean;
218
216
  /** The `host` exec lane's background-shell capability kill switch (read per capability probe in remote-env-host.ts). */
219
217
  export declare const hostBackgroundShellEnabled: () => boolean;
package/dist/config.js CHANGED
@@ -10,10 +10,10 @@ import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
10
10
  import { isV2ScopeKey, memoryReplicationCoverage } from "./memory-scope.js";
11
11
  import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
12
12
  import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
13
- import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS } from "./config-types.js";
13
+ import { CONFIG_PROVIDER_WORDS, RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC, RESILIENCE_DEFAULTS, TASK_WRITE_FACE_ESCALATIONS, MAX_TIMER_DELAY_MS, AUTO_MODE_CLASSIFIER_TIMEOUT_BOUNDS, DRAIN_GRACE_FLOOR_MS, DRAIN_SECOND_SIGNAL_MIN_GAP_BOUNDS, DRAIN_SECOND_SIGNAL_MIN_GAP_DEFAULT_MS, FIRST_BYTE_TIMEOUT_BOUNDS } from "./config-types.js";
14
14
  import { buildWriteProtectionPosture } from "./write-protection.js";
15
15
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
16
- import { readFaceForPosture } from "./read-face-posture.js";
16
+ import { buildReadFacePostureNotice, readFaceForPosture } from "./read-face-posture.js";
17
17
  import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat, isThinkingTier } from "./model-compat.js";
18
18
  import {} from "./posture-source.js";
19
19
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
@@ -130,6 +130,10 @@ const RETIRED_ENVS = [
130
130
  "widened retry; 0 = no engine clock on that segment and the transport's header / body clocks off too — the TCP + TLS connect " +
131
131
  "keeps the transport's own clock)",
132
132
  ],
133
+ ["PROJECT_MEMORY_DISABLED", "PROJECT_MEMORY_ENABLED", "replaced", "negative-name alias retired in server 3.0.0 — unset it; the polarity is inverted: PROJECT_MEMORY_DISABLED=true is PROJECT_MEMORY_ENABLED=false (the positive name defaults on)"],
134
+ ["CONFIG_LKG_DISABLED", "CONFIG_LKG_ENABLED", "replaced", "negative-name alias retired in server 3.0.0 — unset it; the polarity is inverted: CONFIG_LKG_DISABLED=true is CONFIG_LKG_ENABLED=false (the positive name defaults on)"],
135
+ ["HOST_BG_DISABLED", "HOST_BG_ENABLED", "replaced", "negative-name alias retired in server 3.0.0 — unset it; the polarity is inverted: HOST_BG_DISABLED=true is HOST_BG_ENABLED=false (the positive name defaults on)"],
136
+ ["HOST_EXEC_SPOOL_DISABLED", "HOST_EXEC_SPOOL_ENABLED", "replaced", "negative-name alias retired in server 3.0.0 — unset it; the polarity is inverted: HOST_EXEC_SPOOL_DISABLED=true is HOST_EXEC_SPOOL_ENABLED=false (the positive name defaults on)"],
133
137
  ];
134
138
  const RETIRED_ENV_SENTENCE = {
135
139
  renamed: (hits) => `${hits.map(([old, to]) => `${old} → use ${to}`).join("; ")} — rename the env key(s); the value semantics are unchanged`,
@@ -147,7 +151,7 @@ function rejectRetiredEnvs() {
147
151
  if (hit.length === 0)
148
152
  return;
149
153
  const parts = Object.keys(RETIRED_ENV_SENTENCE).flatMap((kind) => retiredEnvSentence(kind, hit));
150
- throw new Error(`retired env name(s) set [${RETIRED_ENV_REFUSAL_CODE}] ([2354] 兼容面全清,5.0.0 起拒启): ${parts.join(" | ")}.`);
154
+ throw new Error(`retired env name(s) set [${RETIRED_ENV_REFUSAL_CODE}] — refused while present, whether or not the replacement is also set: ${parts.join(" | ")}.`);
151
155
  }
152
156
  export function numEnv(name, fallback) {
153
157
  return parseNumOrFail(name, env(name, fallback));
@@ -201,13 +205,24 @@ export function drainConfigNotices() {
201
205
  return CONFIG_NOTICES.splice(0, CONFIG_NOTICES.length);
202
206
  }
203
207
  let EXEC_LANE_NOTICE;
204
- function buildExecLaneNotice(remoteExec) {
208
+ function buildExecLaneNotice(remoteExec, configProvider) {
205
209
  const explicit = process.env.REMOTE_EXEC !== undefined && process.env.REMOTE_EXEC !== "";
206
210
  return {
207
211
  lane: executionLaneOf(remoteExec?.provider),
208
- source: explicit ? "explicit-env" : process.env.CONFIG_PROVIDER === "local" ? "default-local-provider" : "default-unset",
212
+ source: explicit ? "explicit-env" : configProvider === "local" ? "default-local-provider" : "default-unset",
209
213
  };
210
214
  }
215
+ function parseConfigProvider() {
216
+ const raw = process.env.CONFIG_PROVIDER;
217
+ if (raw === undefined || raw === "")
218
+ return undefined;
219
+ const word = CONFIG_PROVIDER_WORDS.find((w) => w === raw);
220
+ if (word === undefined) {
221
+ throw new Error(`env CONFIG_PROVIDER="${raw.slice(0, 120)}" must be one of: ${CONFIG_PROVIDER_WORDS.join(", ")} — or leave it unset (then SEMA_REGISTRY_URL, if set, selects the center as the config source; otherwise this worker is env-only). ` +
222
+ `The word is matched exactly (no trimming, no case folding). Refusing to boot rather than guessing: the config source, the execution-lane default and the local config root all key on this one word.`);
223
+ }
224
+ return word;
225
+ }
211
226
  export function logConfigDiagnostics(logger) {
212
227
  for (const w of drainConfigWarnings()) {
213
228
  const kind = w.kind ?? "invalid-default";
@@ -236,8 +251,20 @@ export function logConfigDiagnostics(logger) {
236
251
  }
237
252
  }
238
253
  }
239
- for (const n of drainConfigNotices())
240
- logger.warn(n.event, n.fields);
254
+ for (const n of drainConfigNotices()) {
255
+ switch (n.level) {
256
+ case "warn":
257
+ logger.warn(n.event, n.fields);
258
+ break;
259
+ case "info":
260
+ logger.info?.(n.event, n.fields);
261
+ break;
262
+ default: {
263
+ const unreachable = n.level;
264
+ throw new Error(`unhandled config notice level: ${String(unreachable)}`);
265
+ }
266
+ }
267
+ }
241
268
  const laneNotice = EXEC_LANE_NOTICE;
242
269
  EXEC_LANE_NOTICE = undefined;
243
270
  if (laneNotice && logger.info)
@@ -433,30 +460,9 @@ function boolEnv(name, def) {
433
460
  registerKnob({ name, polarity: def ? "opt-out" : "opt-in", value, source });
434
461
  return value;
435
462
  }
436
- export function boolEnvWithLegacyNegated(name, legacyNegatedName, def) {
437
- const raw = process.env[name];
438
- const legacy = process.env[legacyNegatedName];
439
- let value;
440
- let source;
441
- const parsedCanonical = parseBoolWord(name, raw);
442
- if (parsedCanonical !== undefined) {
443
- value = parsedCanonical;
444
- source = "env";
445
- }
446
- else if (legacy !== undefined && legacy !== "") {
447
- throw new Error(`${legacyNegatedName} was removed in server 3.0.0 — set ${name}=${legacy !== "true"} instead ` +
448
- `(positive-name knob, default ${def ? "on" : "off"}; migration table in USAGE.md)`);
449
- }
450
- else {
451
- value = def;
452
- source = "default";
453
- }
454
- registerKnob({ name, polarity: def ? "opt-out" : "opt-in", value, source });
455
- return value;
456
- }
457
- export const configLkgEnabled = () => boolEnvWithLegacyNegated("CONFIG_LKG_ENABLED", "CONFIG_LKG_DISABLED", true);
458
- export const hostBackgroundShellEnabled = () => boolEnvWithLegacyNegated("HOST_BG_ENABLED", "HOST_BG_DISABLED", true);
459
- export const hostExecSpoolEnabled = () => boolEnvWithLegacyNegated("HOST_EXEC_SPOOL_ENABLED", "HOST_EXEC_SPOOL_DISABLED", true);
463
+ export const configLkgEnabled = () => boolEnv("CONFIG_LKG_ENABLED", true);
464
+ export const hostBackgroundShellEnabled = () => boolEnv("HOST_BG_ENABLED", true);
465
+ export const hostExecSpoolEnabled = () => boolEnv("HOST_EXEC_SPOOL_ENABLED", true);
460
466
  export const REQUIRED_ENVS_BY_LANE = {
461
467
  e2b: ["E2B_API_KEY"],
462
468
  k8s: ["K8S_SANDBOX_IMAGE"],
@@ -537,6 +543,7 @@ export function parseEnvNameAllowlist(knob, singleUserTurnkey) {
537
543
  return [];
538
544
  case "honored":
539
545
  CONFIG_NOTICES.push({
546
+ level: "warn",
540
547
  event: "config_env_forwarded_multi_tenant",
541
548
  fields: {
542
549
  env: knob,
@@ -640,6 +647,7 @@ function parseWriteProtectedTableEnv(extraRaw, replaceRaw) {
640
647
  const dropped = posture.droppedDefaultRows ?? [];
641
648
  CONFIG_NOTICES.push(posture.source === "off"
642
649
  ? {
650
+ level: "warn",
643
651
  event: "write_protection_table_disabled",
644
652
  fields: {
645
653
  env: "WRITE_PROTECTED_TABLE_REPLACE",
@@ -650,6 +658,7 @@ function parseWriteProtectedTableEnv(extraRaw, replaceRaw) {
650
658
  },
651
659
  }
652
660
  : {
661
+ level: "warn",
653
662
  event: "write_protection_table_replaced",
654
663
  fields: {
655
664
  env: "WRITE_PROTECTED_TABLE_REPLACE",
@@ -801,6 +810,7 @@ function noticeAutoCompactAboveWindow(slot, envName, m) {
801
810
  if (act === undefined || window <= 0 || act <= window)
802
811
  return;
803
812
  CONFIG_NOTICES.push({
813
+ level: "warn",
804
814
  event: "auto_compact_tokens_above_context_window",
805
815
  fields: {
806
816
  slot,
@@ -822,6 +832,7 @@ function parseStoreDomain(ctx) {
822
832
  const dbBackend = enumEnv("DB_BACKEND", bareBoot ? (multiTenantBareBoot ? "memory" : "local") : "mysql", ["mysql", "pg", "local", "memory"]);
823
833
  if (multiTenantBareBoot) {
824
834
  CONFIG_NOTICES.push({
835
+ level: "warn",
825
836
  event: "db_backend_default_memory_multi_tenant",
826
837
  fields: {
827
838
  note: "REQUIRE_PRINCIPAL=true with no DB_BACKEND — the single-user default (local file store) is incompatible with multi-tenant (no tenant isolation in the file owner map), so this deployment runs IN-MEMORY (runs/sessions lost on restart, durable runs 501). Configure DB_BACKEND=mysql|pg for a durable multi-tenant deployment.",
@@ -928,6 +939,7 @@ function parseModelDomain() {
928
939
  return explicitProvider;
929
940
  if (inferredAnthropicBaseUrl && inferredAnthropicCred) {
930
941
  CONFIG_NOTICES.push({
942
+ level: "warn",
931
943
  event: "model_provider_inferred",
932
944
  fields: {
933
945
  provider: "anthropic",
@@ -973,11 +985,28 @@ function parseModelDomain() {
973
985
  const cheapId = process.env.MODEL_CHEAP_ID;
974
986
  if (cheapId && modelId === "") {
975
987
  CONFIG_NOTICES.push({
988
+ level: "warn",
976
989
  event: "model_cheap_slot_without_main",
977
990
  fields: { hint: "MODEL_CHEAP_ID is set but MODEL_ID is not — the env cheap slot derives from the env main model, so it is not minted; the catalog (config.d/models.json or the config center) serves the roles. Set MODEL_ID to use the env slots, or unset MODEL_CHEAP_ID to silence this line." },
978
991
  });
979
992
  }
980
- const cheapModel = cheapId && modelId !== ""
993
+ const cheapIsMain = cheapId !== undefined && cheapId !== "" && modelId !== "" && cheapId === modelId;
994
+ if (cheapIsMain) {
995
+ CONFIG_NOTICES.push({
996
+ level: "warn",
997
+ event: "config_env_entry_cheap_same_as_main",
998
+ fields: {
999
+ env: "MODEL_CHEAP_ID",
1000
+ model: modelId,
1001
+ hint: `MODEL_CHEAP_ID names the same model as MODEL_ID ("${modelId}") — the env cheap slot is the main gateway under a different id, ` +
1002
+ `so the same id IS the main model: no cheap slot is minted and subagent / summarize stay on the main model (roles -> default); ` +
1003
+ `any other MODEL_CHEAP_* key set here describes that slot and is not applied. Set MODEL_CHEAP_ID to a different (cheaper) model ` +
1004
+ `id to run those roles cheap, or declare two differently-named entries in config.d/models.json to give one upstream id a second ` +
1005
+ `shape (price / reasoning / window); unset MODEL_CHEAP_ID to silence this line.`,
1006
+ },
1007
+ });
1008
+ }
1009
+ const cheapModel = cheapId && modelId !== "" && !cheapIsMain
981
1010
  ? {
982
1011
  ...model,
983
1012
  id: cheapId,
@@ -998,6 +1027,7 @@ function parseModelDomain() {
998
1027
  const mainAct = optFinitePositiveEnv("MODEL_AUTO_COMPACT_TOKENS");
999
1028
  if (applyAutoCompactWindow(model, mainAct) === "removed-1m" && modelId !== "") {
1000
1029
  CONFIG_NOTICES.push({
1030
+ level: "warn",
1001
1031
  event: "auto_compact_window_not_derived_1m",
1002
1032
  fields: {
1003
1033
  slot: "main",
@@ -1013,6 +1043,7 @@ function parseModelDomain() {
1013
1043
  if (applyAutoCompactWindow(cheapModel, cheapAct) === "removed-1m") {
1014
1044
  const inherited = process.env.MODEL_CHEAP_CONTEXT_WINDOW === undefined || process.env.MODEL_CHEAP_CONTEXT_WINDOW === "";
1015
1045
  CONFIG_NOTICES.push({
1046
+ level: "warn",
1016
1047
  event: "auto_compact_window_not_derived_1m",
1017
1048
  fields: {
1018
1049
  slot: "cheap",
@@ -1029,6 +1060,7 @@ function parseModelDomain() {
1029
1060
  const gatewayApiKey = process.env.MODEL_API_KEY;
1030
1061
  if (gatewayBaseUrl === undefined && gatewayApiKey !== undefined && gatewayApiKey !== "") {
1031
1062
  CONFIG_NOTICES.push({
1063
+ level: "warn",
1032
1064
  event: "model_gateway_key_unpaired",
1033
1065
  fields: {
1034
1066
  hint: "MODEL_API_KEY is set but MODEL_GATEWAY_BASEURL is not — MODEL_API_KEY is the gateway's credential and pairs only with the gateway's own root, so with no gateway it is NOT loaded and is sent to no route (a model with its own baseUrl authenticates with its own per-model key). Set MODEL_GATEWAY_BASEURL to use it, or unset MODEL_API_KEY to silence this line.",
@@ -1308,6 +1340,7 @@ function parseApprovalDomain(ctx) {
1308
1340
  if (trimmed === "" || trimmed === "off") {
1309
1341
  if (trimmed === "off") {
1310
1342
  CONFIG_NOTICES.push({
1343
+ level: "warn",
1311
1344
  event: "manual_mode_shell_gate_off_is_noop",
1312
1345
  fields: {
1313
1346
  env: "MANUAL_MODE_SHELL_GATE",
@@ -1526,7 +1559,7 @@ function parseMemoryConsolidationDriver() {
1526
1559
  `who believes the valve is armed gets a 404 rather than a diagnosis). This knob is new in server 7.46 and has no legacy ` +
1527
1560
  `spellings to preserve, unlike MEMORY_ENGINE's two-family word table.`);
1528
1561
  }
1529
- const enabled = word === "on";
1562
+ const valve = word === "on" || word === "off" ? word : undefined;
1530
1563
  const autoWord = requireNonBlank("MEMORY_AUTO_CONSOLIDATION", process.env.MEMORY_AUTO_CONSOLIDATION);
1531
1564
  if (autoWord !== undefined && autoWord !== "on" && autoWord !== "off") {
1532
1565
  throw new Error(`MEMORY_AUTO_CONSOLIDATION must be exactly "on" or "off", got ${JSON.stringify(autoWord)} — this knob decides whether memory content ` +
@@ -1558,12 +1591,12 @@ function parseMemoryConsolidationDriver() {
1558
1591
  }
1559
1592
  const scopesRaw = requireNonBlank("MEMORY_CONSOLIDATION_SCOPES", process.env.MEMORY_CONSOLIDATION_SCOPES);
1560
1593
  const scopes = scopesRaw !== undefined ? csv("MEMORY_CONSOLIDATION_SCOPES") : [];
1561
- if (!enabled && (intervalSec > 0 || scopes.length > 0)) {
1562
- throw new Error(`MEMORY_CONSOLIDATION_INTERVAL_SEC / MEMORY_CONSOLIDATION_SCOPES are configured but MEMORY_CONSOLIDATION_DRIVER is not "on" — ` +
1563
- `the consolidation valve is OFF, so neither knob reaches anything (the two admin endpoints are not even mounted). ` +
1564
- `Set MEMORY_CONSOLIDATION_DRIVER=on, or unset the other two.`);
1565
- }
1566
- return { enabled, intervalSec, scopes, ...(autoRunOnRecommendation !== undefined ? { autoRunOnRecommendation } : {}) };
1594
+ return {
1595
+ ...(valve !== undefined ? { valve } : {}),
1596
+ intervalSec,
1597
+ scopes,
1598
+ ...(autoRunOnRecommendation !== undefined ? { autoRunOnRecommendation } : {}),
1599
+ };
1567
1600
  }
1568
1601
  function parseMemoryDomain(ctx) {
1569
1602
  const { requirePrincipal } = ctx;
@@ -1605,6 +1638,7 @@ function parseMemoryDomain(ctx) {
1605
1638
  const replication = memoryReplicationCoverage(memorySync !== undefined ? { memorySync } : {});
1606
1639
  if (replication.kind === "scopes" && memoryEngineEnabled && !requirePrincipal && memoryScope !== undefined) {
1607
1640
  CONFIG_NOTICES.push({
1641
+ level: "warn",
1608
1642
  event: "memory_project_scope_yields_to_sync",
1609
1643
  fields: {
1610
1644
  replicatedScopes: replication.scopes.join(","),
@@ -1615,7 +1649,7 @@ function parseMemoryDomain(ctx) {
1615
1649
  },
1616
1650
  });
1617
1651
  }
1618
- const projectMemoryEnabled = boolEnvWithLegacyNegated("PROJECT_MEMORY_ENABLED", "PROJECT_MEMORY_DISABLED", true);
1652
+ const projectMemoryEnabled = boolEnv("PROJECT_MEMORY_ENABLED", true);
1619
1653
  const memoryOrgAdmissionMode = enumEnv("MEMORY_ORG_ADMISSION_MODE", "enforce", ["audit", "enforce"]);
1620
1654
  const memoryOrgDirectoryJson = process.env.MEMORY_ORG_DIRECTORY_JSON || undefined;
1621
1655
  const memoryOrgGrantTtlMs = numEnvBounded("MEMORY_ORG_GRANT_TTL_MS", "60000", 1000, 3_600_000);
@@ -1787,11 +1821,15 @@ function parseOrchestrationDomain(ctx) {
1787
1821
  }
1788
1822
  }
1789
1823
  }
1824
+ const readFaceFacts = process.env.READ_FACE !== undefined
1825
+ ? { readFace: enumEnv("READ_FACE", "roots", ["open", "roots"]), readFaceSource: "env" }
1826
+ : readFaceForPosture(singleUserTurnkey);
1827
+ const readFaceNotice = buildReadFacePostureNotice(readFaceFacts);
1828
+ if (readFaceNotice !== undefined)
1829
+ CONFIG_NOTICES.push({ level: "info", ...readFaceNotice });
1790
1830
  return {
1791
1831
  requireExecutionLane: boolEnv("REQUIRE_EXECUTION_LANE", false),
1792
- ...(process.env.READ_FACE !== undefined
1793
- ? { readFace: enumEnv("READ_FACE", "roots", ["open", "roots"]), readFaceSource: "env" }
1794
- : readFaceForPosture(singleUserTurnkey)),
1832
+ ...readFaceFacts,
1795
1833
  ...optionalEnumKey("taskWriteFaceEscalation", "TASK_WRITE_FACE_ESCALATION", process.env.TASK_WRITE_FACE_ESCALATION, TASK_WRITE_FACE_ESCALATIONS),
1796
1834
  ...(process.env.READ_DENY_PATTERNS !== undefined ? { readDenyPatterns: parseReadDenyPatternsEnv(process.env.READ_DENY_PATTERNS) } : {}),
1797
1835
  ...(() => {
@@ -1910,7 +1948,7 @@ function parseOrchestrationDomain(ctx) {
1910
1948
  ...(process.env.DEVICE_SUPERSEDE ? { supersede: enumEnv("DEVICE_SUPERSEDE", "stale_only", ["stale_only", "deny"]) } : {}),
1911
1949
  }
1912
1950
  :
1913
- process.env.CONFIG_PROVIDER === "local"
1951
+ ctx.configProvider === "local"
1914
1952
  ? { provider: "host" }
1915
1953
  : undefined,
1916
1954
  worktreeIsolation: boolEnv("WORKTREE_ISOLATION_ENABLED", false) && process.env.WORKTREE_REPO_ROOT
@@ -2028,7 +2066,13 @@ function parseLimitsHttpDomain() {
2028
2066
  costQuotaWindowSec: numEnv("COST_QUOTA_WINDOW_SEC", "86400"),
2029
2067
  reapIntervalSec: numEnv("REAP_INTERVAL_SEC", "60"),
2030
2068
  runStaleSec: numEnv("REAP_RUN_STALE_SEC", "120"),
2031
- toolResultTtlSec: numEnv("TOOL_RESULT_TTL_SEC", "86400"),
2069
+ toolResultTtlSec: (() => {
2070
+ const ttl = numEnv("TOOL_RESULT_TTL_SEC", "86400");
2071
+ if (ttl <= 0) {
2072
+ throw new Error(`env TOOL_RESULT_TTL_SEC=${ttl} must be > 0 — it is the age (seconds) after which an offloaded tool result is reaped, so 0 or less puts the cutoff at or after now and every reaper tick would delete ALL of them; there is no "off": set a larger value to keep results longer (the config center refuses the same value)`);
2073
+ }
2074
+ return ttl;
2075
+ })(),
2032
2076
  };
2033
2077
  }
2034
2078
  function parseObservabilityDomain() {
@@ -2081,7 +2125,7 @@ function parseGitApiKind(raw, baseUrl) {
2081
2125
  return kind;
2082
2126
  }
2083
2127
  function parseIntegrationsDomain(ctx) {
2084
- const { requirePrincipal } = ctx;
2128
+ const { requirePrincipal, configProvider } = ctx;
2085
2129
  return {
2086
2130
  pluginsAllowHosts: (process.env.PLUGINS_ALLOW_HOSTS ?? "github.com").split(",").map((h) => h.trim()).filter(Boolean),
2087
2131
  configBootFetchBudgetMs: clampEnvWithWarn("CONFIG_BOOT_FETCH_BUDGET_MS", "1500", 0),
@@ -2092,7 +2136,7 @@ function parseIntegrationsDomain(ctx) {
2092
2136
  skillsDir: env("SKILLS_DIR", defaultSkillsDir()),
2093
2137
  centerCredentialRenewLeadMs: parseCenterCredentialRenewLeadMs(process.env.CENTER_CREDENTIAL_RENEW_LEAD_MS),
2094
2138
  configCenter: process.env.SEMA_REGISTRY_URL ? parseConfigCenter(env("SEMA_REGISTRY_URL"), requirePrincipal) : undefined,
2095
- configProvider: process.env.CONFIG_PROVIDER || undefined,
2139
+ ...(configProvider !== undefined ? { configProvider } : {}),
2096
2140
  configLocalDir: process.env.CONFIG_LOCAL_DIR || undefined,
2097
2141
  a2aServe: parseA2aServe(),
2098
2142
  };
@@ -2281,6 +2325,7 @@ export function loadConfig() {
2281
2325
  CONFIG_KNOBS.clear();
2282
2326
  const requirePrincipal = boolEnv("REQUIRE_PRINCIPAL", false);
2283
2327
  const singleUserTurnkey = !requirePrincipal;
2328
+ const configProvider = parseConfigProvider();
2284
2329
  const postureOn = (name, infraReady = true) => {
2285
2330
  const explicit = parseBoolWord(name, process.env[name]);
2286
2331
  const value = explicit ?? (singleUserTurnkey && infraReady);
@@ -2293,10 +2338,10 @@ export function loadConfig() {
2293
2338
  const approval = parseApprovalDomain({ postureOn, singleUserTurnkey, infraReady: durableStoreReady });
2294
2339
  const memory = parseMemoryDomain({ requirePrincipal });
2295
2340
  const auth = parseAuthDomain({ requirePrincipal });
2296
- const orchestration = parseOrchestrationDomain({ postureOn, singleUserTurnkey });
2341
+ const orchestration = parseOrchestrationDomain({ postureOn, singleUserTurnkey, configProvider });
2297
2342
  const limitsHttp = parseLimitsHttpDomain();
2298
2343
  const observability = parseObservabilityDomain();
2299
- const integrations = parseIntegrationsDomain({ requirePrincipal });
2344
+ const integrations = parseIntegrationsDomain({ requirePrincipal, configProvider });
2300
2345
  const { directApprovalDoor, durableApproval: dgDurable, approvalHmacKeys: dgHmacKeys } = approval;
2301
2346
  const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, authTokens } = auth;
2302
2347
  const { enabled: imageBakesEnabled, runnerPrincipal: bakeRunnerPrincipal } = orchestration.imageBakes;
@@ -2330,14 +2375,6 @@ export function loadConfig() {
2330
2375
  if (imageBakesEnabled && !Object.values(authTokens).includes(bakeRunnerPrincipal)) {
2331
2376
  throw new Error(`IMAGE_BAKES_ENABLED=true but no SERVICE_AUTH_TOKENS entry maps to the runner principal '${bakeRunnerPrincipal}' — the bake-runner authenticates by its credential (token-derived source), so a '<BAKE_RUNNER_TOKEN>=${bakeRunnerPrincipal}' SERVICE_AUTH_TOKENS entry is required or claim/ingest/heartbeat all 403 (IMAGE-API-DESIGN.md §P2.12 boot invariant)`);
2332
2377
  }
2333
- if (orchestration.leaderEnabled && orchestration.remoteExec?.provider === "host") {
2334
- throw new Error("LEADER_ENABLED=true + REMOTE_EXEC=host is refused: the leader wire pins ONE absolute workspace path " +
2335
- "(LEADER_WORKSPACE / the /home/user default) for every sub-worker, while the host adapter gives each env a " +
2336
- "DIFFERENT random directory — so N concurrent host workers would share one physical git worktree and " +
2337
- "corrupt each other's state SILENTLY (per-worker workspace is not wired on this lane yet). Run the leader " +
2338
- "on an isolated lane (REMOTE_EXEC=e2b or k8s), or turn the leader off (unset LEADER_ENABLED) " +
2339
- "(#193 车4 件2 / staleness ledger P1-10 boot invariant)");
2340
- }
2341
2378
  if (orchestration.leaderEnabled && store.dbBackend !== "mysql" && store.dbBackend !== "pg") {
2342
2379
  throw new Error(`LEADER_ENABLED=true but DB_BACKEND=${store.dbBackend} (no external SQL store) is refused: a leader run is an ` +
2343
2380
  "N-worker fan-out lasting up to 24h that ends in a REAL git push, and without a durable store its registry is " +
@@ -2345,25 +2382,6 @@ export function loadConfig() {
2345
2382
  "push. Point the service at a durable store (DB_BACKEND=mysql|pg), or turn the leader off (unset LEADER_ENABLED) " +
2346
2383
  "(#193 车4 件2 / staleness ledger P1-9+P1-10 boot invariant)");
2347
2384
  }
2348
- {
2349
- const deviceLane = orchestration.remoteExec?.provider === "device";
2350
- const deviceSqlStore = (store.dbBackend === "mysql" && store.tidb !== undefined) || (store.dbBackend === "pg" && store.pg !== undefined);
2351
- if (deviceLane && !deviceSqlStore) {
2352
- throw new Error(`REMOTE_EXEC=device with DB_BACKEND=${store.dbBackend} and no SQL connection coordinates built is refused ` +
2353
- "[device-durable-store]: the device registry, the session↔device binding and the device audit trail are all " +
2354
- "SQL-backed — without a real SQL store the binding lives in this process only, so a restart forgets WHICH device " +
2355
- "a parked run must resume onto and a second replica cannot see it at all. Note DB_BACKEND alone is not enough: " +
2356
- "SESSION_BACKEND=memory (or auto with no MYSQL_HOST/PG_HOST) builds no coordinates at all. Configure a durable " +
2357
- "store (DB_BACKEND=mysql|pg + its *_HOST/*_USER/*_DATABASE), or unset REMOTE_EXEC " +
2358
- "(device-executor-lane-v2 §4.7 boot invariant)");
2359
- }
2360
- if (deviceLane && !dgDurable) {
2361
- throw new Error("REMOTE_EXEC=device without DURABLE_APPROVAL=true is refused [device-durable-approval]: a personal device goes " +
2362
- "offline (lid closed, network drop) at arbitrary points, so an in-flight approval that outlives the window MUST " +
2363
- "have a park exit — without the durable approval store there is none and the run is stuck with nowhere to persist. " +
2364
- "Set DURABLE_APPROVAL=true, or unset REMOTE_EXEC (device-executor-lane-v2 §4.7 boot invariant)");
2365
- }
2366
- }
2367
2385
  if (orchestration.requireExecutionLane && orchestration.remoteExec === undefined) {
2368
2386
  throw new Error(`REQUIRE_EXECUTION_LANE=true but NO execution lane is named on this deployment — refusing to start. ` +
2369
2387
  `With no lane the engine falls back to its in-process stub environment, where at least the Bash tool is ` +
@@ -2387,7 +2405,7 @@ export function loadConfig() {
2387
2405
  throw new Error(refusal);
2388
2406
  }
2389
2407
  }
2390
- EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec);
2408
+ EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec, configProvider);
2391
2409
  return attachConfigGroups({
2392
2410
  ...store,
2393
2411
  ...modelPlane,