@sema-agent/server 7.69.0 → 7.71.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 (107) hide show
  1. package/README.md +4 -1
  2. package/README.zh-CN.md +4 -1
  3. package/USAGE.md +109 -10
  4. package/dist/approval-ask-machine.d.ts +1 -1
  5. package/dist/approval-card.d.ts +3 -2
  6. package/dist/approval-content-kind.d.ts +13 -7
  7. package/dist/approval-content-kind.js +3 -1
  8. package/dist/approval-reconciler.d.ts +0 -54
  9. package/dist/approval-reconciler.js +2 -2
  10. package/dist/approval.js +3 -2
  11. package/dist/bench/s1/live-deps.js +2 -0
  12. package/dist/boot/execution-env.js +12 -2
  13. package/dist/boot/memory-consolidation.d.ts +2 -1
  14. package/dist/boot/parked-revive-gate.d.ts +2 -0
  15. package/dist/boot/parked-revive-gate.js +15 -10
  16. package/dist/boot/resolve-spec.js +8 -3
  17. package/dist/boot/runner-deps.d.ts +50 -46
  18. package/dist/boot/runner-deps.js +12 -10
  19. package/dist/brain.d.ts +3 -1
  20. package/dist/brain.js +2 -1
  21. package/dist/budget.js +3 -2
  22. package/dist/config-catalog.d.ts +31 -2
  23. package/dist/config-catalog.js +44 -24
  24. package/dist/config-types.d.ts +31 -1
  25. package/dist/config-types.js +1 -0
  26. package/dist/config.js +15 -3
  27. package/dist/deployment-governance.d.ts +6 -3
  28. package/dist/deployment-governance.js +2 -1
  29. package/dist/execution-lane-caps.d.ts +51 -37
  30. package/dist/execution-lane-caps.js +23 -0
  31. package/dist/fleet/fleet-bus.d.ts +22 -1
  32. package/dist/fleet/fleet-bus.js +2 -1
  33. package/dist/http/routes/approvals-assistant.js +3 -12
  34. package/dist/http/routes/diagnostics.js +4 -0
  35. package/dist/http/routes/runs.js +2 -3
  36. package/dist/http/routes/tasks.js +22 -18
  37. package/dist/http/server.js +21 -7
  38. package/dist/index.d.ts +1 -1
  39. package/dist/leader/wire.js +4 -3
  40. package/dist/main.js +6 -4
  41. package/dist/memory-operator-faces.d.ts +1 -1
  42. package/dist/memory-scope.d.ts +24 -0
  43. package/dist/memory-scope.js +12 -3
  44. package/dist/observability/metrics.d.ts +4 -6
  45. package/dist/observability/metrics.js +5 -1
  46. package/dist/observability/permission-rule-events.d.ts +20 -0
  47. package/dist/observability/permission-rule-events.js +28 -0
  48. package/dist/observability/run-terminal-log.d.ts +13 -22
  49. package/dist/observability/run-terminal-log.js +187 -47
  50. package/dist/observability/tool-trace.d.ts +12 -0
  51. package/dist/observability/tool-trace.js +5 -1
  52. package/dist/orchestration/workflow-agent-session-index.d.ts +85 -2
  53. package/dist/orchestration/workflow-agent-session-index.js +38 -5
  54. package/dist/parked-decide.d.ts +40 -1
  55. package/dist/parked-decide.js +19 -2
  56. package/dist/plugins/checkpoint-store-sql.js +5 -5
  57. package/dist/plugins/local-checkpoint-store.js +3 -3
  58. package/dist/plugins/remote-env-adb.d.ts +11 -0
  59. package/dist/plugins/remote-env-adb.js +2 -0
  60. package/dist/plugins/remote-env-device.d.ts +9 -0
  61. package/dist/plugins/remote-env-device.js +3 -0
  62. package/dist/plugins/remote-env-e2b.d.ts +11 -0
  63. package/dist/plugins/remote-env-e2b.js +2 -0
  64. package/dist/plugins/remote-env-host.d.ts +11 -0
  65. package/dist/plugins/remote-env-host.js +2 -0
  66. package/dist/plugins/remote-env-k8s.d.ts +11 -0
  67. package/dist/plugins/remote-env-k8s.js +2 -0
  68. package/dist/plugins/remote-env-local-docker.d.ts +11 -0
  69. package/dist/plugins/remote-env-local-docker.js +2 -0
  70. package/dist/plugins/remote-env-ssh.d.ts +11 -0
  71. package/dist/plugins/remote-env-ssh.js +2 -0
  72. package/dist/plugins/store-backend.d.ts +36 -13
  73. package/dist/plugins/store-backend.js +12 -3
  74. package/dist/plugins/store-contracts.d.ts +54 -18
  75. package/dist/plugins/store-contracts.js +17 -30
  76. package/dist/plugins/workflow-journal-store-sql.d.ts +1 -1
  77. package/dist/plugins/workflow-journal-store-sql.js +3 -10
  78. package/dist/plugins/workflow-run-store-sql.d.ts +31 -9
  79. package/dist/plugins/workflow-run-store-sql.js +28 -1
  80. package/dist/posture-source.d.ts +23 -3
  81. package/dist/posture-source.js +1 -1
  82. package/dist/read-face-posture.d.ts +8 -1
  83. package/dist/read-face-posture.js +1 -0
  84. package/dist/run-cancel-context.d.ts +50 -1
  85. package/dist/run-cancel-context.js +22 -0
  86. package/dist/run-local.d.ts +14 -6
  87. package/dist/run-local.js +4 -1
  88. package/dist/runs.d.ts +0 -51
  89. package/dist/runs.js +40 -25
  90. package/dist/task-a2a.js +2 -2
  91. package/dist/task-mcp.js +2 -2
  92. package/dist/task-settings.d.ts +21 -8
  93. package/dist/task-settings.js +2 -2
  94. package/dist/tool-approval.d.ts +14 -8
  95. package/dist/tool-approval.js +3 -3
  96. package/dist/trace/core-keyset-guard.d.ts +135 -11
  97. package/dist/trace/engine-notice-wire.d.ts +1 -1
  98. package/dist/trace/engine-notice-wire.js +1 -0
  99. package/dist/trace/ledger-events.d.ts +22 -2
  100. package/dist/trace/ledger-events.js +6 -1
  101. package/dist/trace/ledger-sink.d.ts +10 -8
  102. package/dist/trace/ledger-sink.js +10 -2
  103. package/dist/trace/project.d.ts +24 -6
  104. package/dist/trace/project.js +5 -3
  105. package/dist/trace/redact.d.ts +28 -0
  106. package/dist/trace/redact.js +58 -30
  107. package/package.json +3 -3
@@ -6,6 +6,7 @@ import { decodeCheckpointScope } from "../security.js";
6
6
  import { createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "../deployment-governance.js";
7
7
  import { applyRuntimeGovernance } from "../runtime-governance.js";
8
8
  import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
9
+ import { executionLaneHomeDir } from "../execution-lane-caps.js";
9
10
  function hostGateCwd(config, localRoot) {
10
11
  return join(config.localDataRoot ?? localRoot, "parked-revive-unrooted");
11
12
  }
@@ -14,6 +15,8 @@ export function createParkedReviveInheritedGate(deps) {
14
15
  const slots = deps.slots ?? sandboxPathEnvSlots;
15
16
  return async (row, identity) => {
16
17
  const metadata = await chainMetadataOf(deps, row, identity);
18
+ const laneHome = executionLaneHomeDir(config);
19
+ const homeBase = laneHome !== undefined ? { taskHome: laneHome } : {};
17
20
  const pathAdjudication = isSandboxPathAdjudicationLane(config.remoteExec?.provider)
18
21
  ? {
19
22
  env: new DeferredSandboxPathEnv({
@@ -21,8 +24,9 @@ export function createParkedReviveInheritedGate(deps) {
21
24
  slots,
22
25
  logger: { warn: (event, fields) => logger.warn(event, { ...(fields ?? {}), lane: config.remoteExec?.provider, leg: "parked_revive" }) },
23
26
  }),
27
+ ...homeBase,
24
28
  }
25
- : { env: createHostAdjudicationEnv(config, localRoot) };
29
+ : { env: createHostAdjudicationEnv(config, localRoot), ...homeBase };
26
30
  const rootSessionId = row.rootSessionId;
27
31
  const baseline = createApprovalBaselinePolicy(config, {
28
32
  question,
@@ -83,22 +87,23 @@ function autoModePostureOf(deps, row, caps) {
83
87
  }
84
88
  function createUnreachableAncestorClassifier(deps, row) {
85
89
  const inner = createAutoModeDecider({
86
- failureThreshold: 1,
87
90
  classify: async () => {
88
91
  throw new Error("the ancestor task's auto-mode classifier is a live closure over that task's own transcript/brain — it cannot be " +
89
- "reconstructed on a fresh replica, so this inherited layer has no classifier verdict to give (the call falls " +
90
- "through to the ancestor's frozen approver chain)");
92
+ "reconstructed on a fresh replica, so this inherited layer has no classifier verdict to give");
91
93
  },
92
- onBreakerOpen: () => deps.logger.warn("parked_revive_ancestor_classifier_unreachable", {
93
- handle: row.handle,
94
- sessionId: row.sessionId,
95
- note: "an inherited ancestor layer was auto-mode armed at park time; its classifier does not survive the process boundary — inherited asks resolve on the approver chain (durable park) instead of a classifier verdict",
96
- }),
97
94
  });
95
+ let warned = false;
98
96
  return {
99
- breakerOpen: () => inner.breakerOpen(),
100
97
  decide: async (input, signal) => {
101
98
  recordFailOpen("server.parked-revive.ancestor-classifier-unreachable", row.handle);
99
+ if (!warned) {
100
+ warned = true;
101
+ deps.logger.warn("parked_revive_ancestor_classifier_unreachable", {
102
+ handle: row.handle,
103
+ sessionId: row.sessionId,
104
+ note: "an inherited ancestor layer was auto-mode armed at park time; its classifier does not survive the process boundary — since core 7.12.0 (#661) the inherited station DENIES such calls outright (it no longer falls through to the frozen approver chain), so classifier-eligible inherited asks on this redeemed leg are auto-denied, not re-parked for a person",
105
+ });
106
+ }
102
107
  return inner.decide(input, signal);
103
108
  },
104
109
  };
@@ -1,6 +1,5 @@
1
1
  import { realpathSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { homedir } from "node:os";
4
3
  import { NodeExecutionEnv, QUESTION_AWAITS_RESUME, deriveAskEffective, expandTiers, resolveAskSeamForm, tightenTaskSpec } from "@sema-agent/core";
5
4
  import { cappedCeiling } from "../budget.js";
6
5
  import { centerPromptProvider, centerIdentityAssembled } from "../capabilities/center-prompts.js";
@@ -35,6 +34,7 @@ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSet
35
34
  import { enableForkFromBody, normalizeRetainSubagentSessions, selfOrchestrationFromBody } from "../task-workflow.js";
36
35
  import { redactSecrets } from "../trace/redact.js";
37
36
  import { DeferredSandboxPathEnv, isSandboxPathAdjudicationLane, sandboxPathEnvSlots } from "./deferred-sandbox-path-env.js";
37
+ import { executionLaneHomeDir } from "../execution-lane-caps.js";
38
38
  import { effectiveMemoryPersistenceCapable } from "./memory-boundary.js";
39
39
  export function createResolveSpec(ctx) {
40
40
  const { config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver, getCenterPrompts, getKeyResolver, taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner, hookLlm, hookAgent, fleetBus, hookWakeBus, noteSessionShellGate, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec, selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage, sessionEnvSelection, liveQuestionFace, approverSeat, lockedKeys, } = ctx;
@@ -409,17 +409,20 @@ export function createResolveSpec(ctx) {
409
409
  const foldGovernanceAndSettings = async (body, auth, spec, gated, effMode) => {
410
410
  const { appendLessPack, acceptedAppend, parsedSettings } = gated;
411
411
  const hostSemanticsLane = !isSandboxPathAdjudicationLane(config.remoteExec?.provider);
412
+ const laneHomeBase = () => {
413
+ const home = executionLaneHomeDir(config);
414
+ return home !== undefined ? { taskHome: home } : {};
415
+ };
412
416
  const pathAdjudication = hostSemanticsLane
413
417
  ? (() => {
414
418
  const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
415
419
  const realRoot = sessionCwd ?? (config.remoteExec === undefined ? process.cwd() : undefined);
416
420
  const gateCwd = realRoot ?? join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted");
417
- const realHome = config.remoteExec === undefined ? homedir() : undefined;
418
421
  return {
419
422
  env: new NodeExecutionEnv({ cwd: gateCwd }),
420
423
  cwd: gateCwd,
421
424
  ...(realRoot !== undefined ? { taskRoot: realRoot } : {}),
422
- ...(realHome !== undefined ? { taskHome: realHome } : {}),
425
+ ...laneHomeBase(),
423
426
  };
424
427
  })()
425
428
  : {
@@ -428,6 +431,7 @@ export function createResolveSpec(ctx) {
428
431
  slots: sandboxPathEnvSlots,
429
432
  logger: { warn: (event, fields) => logger.warn(event, { ...(fields ?? {}), lane: config.remoteExec?.provider }) },
430
433
  }),
434
+ ...laneHomeBase(),
431
435
  };
432
436
  const governedBase = applyRuntimeGovernance(spec, createDeploymentGovernanceInputs(config, pathAdjudication, { engineDataRoot: memoryEngine?.root }));
433
437
  let governed = governedBase;
@@ -476,6 +480,7 @@ export function createResolveSpec(ctx) {
476
480
  const sandboxScratchpadDir = isRemoteScratchpadLane(config.remoteExec?.provider) ? remoteScratchpadDirFor(auth?.sessionId) : undefined;
477
481
  return {
478
482
  env: pathAdjudication.env,
483
+ ...(pathAdjudication.taskHome !== undefined ? { taskHome: pathAdjudication.taskHome } : {}),
479
484
  ...gateCommon,
480
485
  ...(sandboxScratchpadDir ? { scratchpadDir: sandboxScratchpadDir } : {}),
481
486
  ...(gateCommon.isExempt
@@ -21,10 +21,10 @@ import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js"
21
21
  import { type WorkflowCompletionInbox } from "../orchestration/workflow-completion-inbox.js";
22
22
  import { type EngineNoticeRouter } from "../trace/engine-notice-wire.js";
23
23
  import { WorkflowNotifyGate, type WorkflowCompletionPayload } from "../orchestration/workflow-notify-journal.js";
24
- import type { ServiceWorkflowJournalStore, StoreBackend, ToolResultStoreFull } from "../plugins/store-backend.js";
24
+ import type { ServiceWorkflowJournalStore, SessionCaptureRecordSource, StoreBackend, ToolResultStoreFull } from "../plugins/store-backend.js";
25
25
  import type { QuestionCoordinator } from "../question.js";
26
26
  import type { ToolApprovalCoordinator } from "../tool-approval.js";
27
- import type { MemoryBackend, SessionCaptureRecordStore } from "@sema-agent/core";
27
+ import type { MemoryBackend } from "@sema-agent/core";
28
28
  import type { MemorySyncRunner } from "../memory-sync-client.js";
29
29
  /**
30
30
  * `RunnerDeps.onAsk` 的装配([ref] 件4:从内联闭包提为具名导出,**纯 testability 重构、零行为变化**)。
@@ -79,16 +79,51 @@ export interface RunnerDepsCtx {
79
79
  question: QuestionCoordinator | undefined;
80
80
  toolApproval: ToolApprovalCoordinator | undefined;
81
81
  sessionStore: ReturnType<StoreBackend["session"]>;
82
+ /**
83
+ * B-060(test [ref] 核心发现①)—— **一条部署腿上的每一只 Runner 看同一只检查点店**。
84
+ *
85
+ * 本键从「差异键」升进{@link createSharedRunnerDeps 共享基座}。修前它**只**挂在 subRunner 上,主
86
+ * runner 靠 per-task `spec.checkpointStore` 拿店 —— 而 core 的 `run_workflow` 是挂在**主** Runner
87
+ * (`prepare-caps-and-workflow.js:244` 的 `runner: runnerSelf`;core 7.12.0 亲核行号,B-060 票据引的
88
+ * :249 是 7.11.2 的坐标)上的,workflow 子代的 childSpec 不带
89
+ * `checkpointStore`(`orchestration/workflow-primitives.js:80-81` 只透传 `"disabled"` 这一种状态),
90
+ * 于是子代的 `resolveCheckpointStore(spec, deps)` 落到主 runner 这一格 = `undefined`
91
+ * ⇒ `parkLane {capable:false, reasons:["no_checkpoint_store"]}` ⇒ 受门的调用不是 park 而是
92
+ * `delegation.ask_unresolvable{parkLaneExisted:false}` 直接拒(过度拒绝,S-185 的 `/decide`
93
+ * 第三车道在这条路径上结构性不可达)。
94
+ *
95
+ * 🔴 host 腿的读数判据是**core 侧硬的**,不是本仓的但愿:`resolveCheckpointStore`
96
+ * (`checkpoint-store.js:367`)**spec 优先**——`spec.checkpointStore` 在场就用它、`"disabled"` 就是
97
+ * 关、只有缺席才落 deps。而本仓 host 腿恒经 `resolveSpec` 的 durableEnabled 分支盖上**同一个实例**,
98
+ * 且 `durableEnabled ⇔ checkpointStore !== undefined`(`boot/coordinators.ts:70` 与 main.ts 的构造
99
+ * 条件同源)⇒ 每一条经 `resolveSpec` 的腿看到的店逐字同一只。
100
+ *
101
+ * ⚠️ **如实登记一处不经解析器的直读**(codex 交叉复审提名,亲读 core dist 确认):
102
+ * `core/runner/run-terminal-adoption.js:74` 读的是 `runner.deps.checkpointStore` **本身**,不走
103
+ * `resolveCheckpointStore`。它是「宿主任务 park 时,把没排空的 steer / follow-up 迁移到 park token 上」
104
+ * 那一步 —— 修前主 runner 这一格是空的,于是**整段迁移静默不跑**(那些输入随本腿一起丢,只留
105
+ * `task.user_followup_undrained` 一类通告)。接上之后它开始真的跑。方向是修好一条本就该在的腿,
106
+ * 但它**不是**「host 腿字节不变」的一部分,所以写在这里而不是混进上面那句。
107
+ *
108
+ * 🔴 这一席是**部署腿级**的答案,不是「哪只 Runner」的属性:一条腿要么有赎回口(HTTP 面的
109
+ * `/v1/approvals/:id/decide` + resume 家族)、于是它的每只 Runner 都该有店;要么没有(`run-local`
110
+ * 的一次性 CLI)、于是**一只都不该有** —— core 的平台/资源停驻(`prepare-boundary-parks.js` 的
111
+ * `suspendForPlatformLimit`)只看「店在不在」,不看 approver 席,给一条没有赎回口的腿接店 = 用一张
112
+ * 没人能兑付的 checkpoint 换掉一次干净的失败。
113
+ */
114
+ checkpointStore: RunnerDeps["checkpointStore"];
82
115
  memoryEngine: {
83
116
  backend: MemoryBackend;
84
117
  root: string;
85
118
  } | undefined;
86
- /** [ref]②([ref] §2.1b,core 7.0.2 [ref] 件2):会话 capture opt-out 记录的 SQL 载体
87
- * (`backend.sessionCaptureRecords()`,main.ts 取**一次**、与四只 operator 面引擎同实例 —— 全站同批
88
- * 注入律,机器钉 = test/memory-capture-optout-lane.test.ts ②)。缺席(local 车道)⇒ core 用控制面
89
- * 文件三腿,单机字节不变;在场 ⇒ 远端车道 `capture:"off"` 声明从 409 `config.memory_capture_unsupported`
90
- * 稳态转为真持久(core prepare-memory 的 §2.1b 能力信号)。 */
91
- sessionCaptureRecordStore: SessionCaptureRecordStore | undefined;
119
+ /** [ref]②([ref] §2.1b,core 7.0.2 [ref] 件2)/ S-215:会话 capture opt-out 记录的**按平面取用源**
120
+ * (`backend.sessionCaptureRecords()`,main.ts 取**一次**、与四只 operator 面引擎同一只源 —— 全站同批
121
+ * 注入律,机器钉 = test/memory-capture-optout-lane.test.ts ②)。在场 ⇒ `capture:"off"` 声明与中途翻转
122
+ * verb 真持久;缺席 ⇒ core 读作「无店」,而 core 的 `captureCarrierUnsupported` 判据是**席位在不在**
123
+ * (不是「文件形能不能用」),叠上 `isRemoteExecutionEnv` 的接口形鸭子判 ⇒ 两条 ingress 都拒
124
+ * `config.memory_capture_unsupported`。S-215 起三条车道都供(SQL 双方言 twin / local 的控制面文件三腿),
125
+ * 所以缺席今天只剩一个成因:**记忆面暗**的 local 部署(那时 core 的 capture 腿本就不参与)。 */
126
+ sessionCaptureRecords: SessionCaptureRecordSource | undefined;
92
127
  memorySyncRunner: MemorySyncRunner | undefined;
93
128
  /** [ref] 件A([ref] 件3③):org 记忆准入两 seam 的装配产物(boot/org-memory.ts)。进**共享
94
129
  * 基座**——主/sub 两 Runner 必须同源:委托子代的 prepare 同样跑准入(委托冻结的重判腿),漏挂
@@ -154,7 +189,7 @@ export interface RunnerDepsCtx {
154
189
  }
155
190
  /** [ref] A10 留档发现②:main runner `RunnerDeps` 与 main.ts subRunner 字面量之间此前手工重复
156
191
  * 的 ~15 个键,类型标注见 {@link createSharedRunnerDeps} 头注。 */
157
- export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryCapturePolicy" | "delegationEntryCaps" | "crossSessionInbound" | "crossSessionDialogExpiry" | "writeProtectedPaths" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "hands" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore" | "memoryScopeAdmission" | "deploymentMemoryScopes" | "sharedMemoryStores" | "compliancePostureResolver" | "lockedConfig" | "retentionPolicy" | "onNotice" | "mcpRevocations">;
192
+ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryCapturePolicy" | "delegationEntryCaps" | "crossSessionInbound" | "crossSessionDialogExpiry" | "writeProtectedPaths" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "checkpointStore" | "hands" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore" | "memoryScopeAdmission" | "deploymentMemoryScopes" | "sharedMemoryStores" | "compliancePostureResolver" | "lockedConfig" | "retentionPolicy" | "onNotice" | "mcpRevocations">;
158
193
  /**
159
194
  * [ref] A10 留档发现②(review 2026-07-29,[ref]§三族A 同源修补的延续):main runner 的
160
195
  * `RunnerDeps` 字面量(下方 `createRunnerDeps`)与 `main.ts` 里 subRunner 的 `new Runner({...})`
@@ -176,50 +211,19 @@ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "readFace" | "readDeny
176
211
  * 差异键(为何不在本共享基座、逐一注明):
177
212
  * - `sessionStore`:main runner 用宿主 durable 会话店;subRunner 用私有短 TTL 的
178
213
  * `ForkRoutingSessionStore`(main.ts 现场构造,子代转录生命周期与主会话不同)。
179
- * - `checkpointStore`:main runner 走 `spec.checkpointStore` 分支(不进 `RunnerDeps`,本 ctx 根本
180
- * 没有这个字段);subRunner 单独携带(子代执行面的 park 设施,[ref])。
181
214
  * - `onBackgroundChildEvent` / `loadProjectMemory` / `probeInstructionSources` / `onError`:
182
215
  * [ref]§三族A 已用「直引 `runnerDeps.X`」修过(main.ts 两处引用同一个 `runnerDeps` 实例的同一
183
216
  * 属性,比再抽一层共享基座更强的同源保证),不重复收纳进这里。
184
217
  */
185
218
  /** 基座真实消费的窄面(Pick)——subRunner 调用点(main.ts)只需凑这 13 个字段,不必造全量 ctx。 */
186
- export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore" | "hands" | "usageWindowStore" | "orgMemoryAdmission" | "sharedMemoryStores" | "logger" | "governanceSeams" | "mcpRevocations">;
187
- /**
188
- * core `EngineNotice` → 结构化 `engine_notice` 行的**唯一**转发体([ref]②)。
189
- *
190
- * 🔴 为什么是具名导出而不是就地闭包(合并码重扫):这一席此前只落在共享基座里,于是 leader 车道
191
- * (`src/leader/wire.ts` 自拼 deps 的五处 `new Runner({...})`)一处都没有 —— core 的契约是「接线 ⇒ 走
192
- * sink,缺席 ⇒ 逐字打 `console.warn`」,所以缺席不是静默而是**同一部署两条通告通道**:主车道进结构化
193
- * JSON 行,leader 车道落裸 stderr,按 code 采集(`tool_result.offload_put_failed` 等)的运维面漏掉后者。
194
- * 两处各写一份转发体必漂(键名/字段集),故收成一个函数;入参取**结构形** logger(leader 的 cfg 只带
195
- * 可选 `warn`/`info` 两格,不是全形 `Logger`)。
196
- *
197
- * 🔴 `warn` 是**必填**(codex 复审第一轮,验真后修):可选链 `logger.warn?.(…)` 形在「只装了 `info` 的
198
- * logger」上会把通告**吞掉** —— 比不接线还坏(不接线时 core 至少还打它自己的 `console.warn`)。所以
199
- * 「有没有 warn」这个判断留在**装配点**:没有就别铸这一席,让 core 的响亮权留在原处。
200
- *
201
- * ## [ref]:双终点分流器(设计稿 = 黑板 [ref])
202
- * 本体自 [ref] 起是**分流器**,两个终点:
203
- * 1. **日志(全族,逐字保留)** —— 上面那段契约一个字节不变:任何 code、detail 原样、缺 detail 不铸键。
204
- * 运维观察面零回退是本腿的硬前提(wire 半场是**增量**,不是搬家)。
205
- * 2. **wire(白名单 + sessionId 路由)** —— `router.route(notice)`:码在
206
- * {@link import("../trace/engine-notice-wire.js").ENGINE_NOTICE_WIRE_CODES} 内**且**归属键在场时,
207
- * 投给那条会话的 run 腿(SSE + durable 账本);否则一帧不出。归属键 = 顶层 `EngineNotice.sessionId`
208
- * **优先**、缺席回落 `detail.sessionId`(单点 `engineNoticeSessionId`;此前这一行只写了 detail 那一半,
209
- * 顶层键 core 5.46+ 就到了)。判据整条住在
210
- * `src/trace/engine-notice-wire.ts`(白名单/归属/消毒/隔离四件同源),这里只负责**先日志后分流**的次序。
211
- *
212
- * 🔴 次序是承重的:日志在前 ⇒ 即便 wire 半场整条坏掉(路由抛、口抛),事实仍然完整落在运维面。
213
- * 反过来写就是「wire 故障吃掉运维日志」,那正是本仓 [ref] 纪律要根除的形。route 自身按 sink 隔离 +
214
- * F 类留痕(`server.engine-notice.wire-sink-threw`),不向 core 回抛。
215
- *
216
- * 🔴 `router` **有默认值**(进程单例)而不是必填:这一席有五个以上装配点(共享基座 / leader 车道四处 /
217
- * 未来任何新车道),必填会让"新车道忘了传 router"变成**静默半聋**(日志有、wire 无)——与本函数当初
218
- * 被抽成具名导出的理由逐字同一个。测试要隔离时显式传自己 new 的实例。
219
- */
219
+ export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "checkpointStore" | "sessionPolicyStore" | "hands" | "usageWindowStore" | "orgMemoryAdmission" | "sharedMemoryStores" | "logger" | "governanceSeams" | "mcpRevocations" | "metrics">;
220
+ /** 转发席只需要 `inc` —— 刻意窄到这一个动词,免得把整只 Metrics 拖进这条纯转发腿。 */
221
+ export interface EngineNoticeMetricsSeat {
222
+ inc: (name: string, labels?: Record<string, string>) => void;
223
+ }
220
224
  export declare function createEngineNoticeForwarder(logger: {
221
225
  warn: (msg: string, fields?: Record<string, unknown>) => void;
222
- }, router?: EngineNoticeRouter): NonNullable<RunnerDeps["onNotice"]>;
226
+ }, router?: EngineNoticeRouter, metrics?: EngineNoticeMetricsSeat): NonNullable<RunnerDeps["onNotice"]>;
223
227
  /**
224
228
  * `onNotice` **席**的装配判据本身 —— 「`warn` 真在场才铸席」这一条从此是**一个可直接调用的函数**,
225
229
  * 不是散在装配点、只能靠扫源码文本去猜的一行三元(codex 交叉复审 R1-[medium],验真后修)。
@@ -23,9 +23,15 @@ export const CONFIG_ADVISORY_CLASSIFICATIONS = new Set([
23
23
  "shared-memory-not-mounted",
24
24
  "shell-gate-off",
25
25
  ]);
26
- export function createEngineNoticeForwarder(logger, router = defaultEngineNoticeRouter) {
26
+ const CEILING_ORIGINS = new Set(["walltime", "turns"]);
27
+ export function createEngineNoticeForwarder(logger, router = defaultEngineNoticeRouter, metrics) {
27
28
  return (notice) => {
28
29
  logger.warn("engine_notice", { code: notice.code, message: notice.message, ...(notice.detail !== undefined ? { detail: notice.detail } : {}) });
30
+ if (metrics !== undefined && notice.code === "task.interrupt_unconsumed") {
31
+ const origin = notice.detail?.origin;
32
+ if (typeof origin === "string" && CEILING_ORIGINS.has(origin))
33
+ metrics.inc("limit_ceiling_after_answer_total", { origin });
34
+ }
29
35
  router.route(notice);
30
36
  };
31
37
  }
@@ -53,7 +59,7 @@ export function buildDeploymentPostureSeats(config) {
53
59
  export function createSharedRunnerDeps(ctx) {
54
60
  const shared = {
55
61
  brain: ctx.brain,
56
- onNotice: createEngineNoticeForwarder(ctx.logger),
62
+ onNotice: createEngineNoticeForwarder(ctx.logger, defaultEngineNoticeRouter, ctx.metrics),
57
63
  tiers: ctx.config.tiers,
58
64
  models: ctx.config.models,
59
65
  roles: ctx.config.roles,
@@ -69,6 +75,7 @@ export function createSharedRunnerDeps(ctx) {
69
75
  hooks: ctx.deploymentHooks,
70
76
  hands: ctx.hands,
71
77
  toolResultStore: ctx.toolResultStore,
78
+ checkpointStore: ctx.checkpointStore ? ctx.checkpointStore : undefined,
72
79
  sessionPolicyStore: ctx.sessionPolicyStore ? ctx.sessionPolicyStore : undefined,
73
80
  memoryScopeAdmission: ctx.orgMemoryAdmission.memoryScopeAdmission ? ctx.orgMemoryAdmission.memoryScopeAdmission : undefined,
74
81
  deploymentMemoryScopes: ctx.orgMemoryAdmission.deploymentMemoryScopes && ctx.orgMemoryAdmission.deploymentMemoryScopes.length > 0 ? ctx.orgMemoryAdmission.deploymentMemoryScopes : undefined,
@@ -83,7 +90,7 @@ export function createSharedRunnerDeps(ctx) {
83
90
  return shared;
84
91
  }
85
92
  export function createRunnerDeps(ctx) {
86
- const { config, logger, metrics, localRoot, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine, sessionCaptureRecordStore, memorySyncRunner, runtimeCapsResolver, fileHistoryStore, legacyRewindBoundaryProbe, permissionRuleStore, fleetBus, workflowRunStore, workflowJournalStore, workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, getRunStore, sessionShellGateExplicitlyOff, } = ctx;
93
+ const { config, logger, metrics, localRoot, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine, sessionCaptureRecords, memorySyncRunner, runtimeCapsResolver, fileHistoryStore, legacyRewindBoundaryProbe, permissionRuleStore, fleetBus, workflowRunStore, workflowJournalStore, workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, getRunStore, sessionShellGateExplicitlyOff, } = ctx;
87
94
  const sharedRunnerDeps = createSharedRunnerDeps(ctx);
88
95
  const workflowModelAllowlist = workflowModelAllowlistFor(config);
89
96
  const selfOrchestrationDeps = config.selfOrchestrationEnabled
@@ -147,16 +154,11 @@ export function createRunnerDeps(ctx) {
147
154
  onElicit: elicitation ? elicitation.elicit : undefined,
148
155
  onQuestion: question ? question.question : undefined,
149
156
  onAsk: createRunnerDepsOnAsk(toolApproval),
150
- autoMode: {
151
- onBreakerOpen: (info) => {
152
- metrics.inc("auto_mode_breaker_open_total");
153
- logger.warn("auto_mode_breaker_open", { consecutiveFailures: info.consecutiveFailures, lastCause: info.lastCause });
154
- },
155
- },
157
+ autoMode: {},
156
158
  sessionStore,
157
159
  memoryBackend: memoryEngine ? memoryEngine.backend : undefined,
158
160
  memoryEngineDir: memoryEngine ? memoryEngine.root : undefined,
159
- memoryCaptureRecordStore: sessionCaptureRecordStore !== undefined ? () => sessionCaptureRecordStore : undefined,
161
+ memoryCaptureRecordStore: sessionCaptureRecords !== undefined ? (plane) => sessionCaptureRecords(plane) : undefined,
160
162
  onMemoryHarvestReport: memoryEngine
161
163
  ? (report, info) => {
162
164
  metrics.inc("memory_harvest_total", { ok: String(report.ok), phase: info.phase, incident: report.incident?.kind ?? "none" });
package/dist/brain.d.ts CHANGED
@@ -15,7 +15,9 @@ export declare const FAST_FAIL_MAX_RETRIES = 2;
15
15
  * - **Tiered timeouts** (1.38, `connectTimeoutMs`/`firstTokenTimeoutMs`) are passed into each
16
16
  * openai-compatible brain at creation — they bound "gateway never accepts the connection" and
17
17
  * "stream opened but hangs without a token" as retryable `[network]` failures. Default ON
18
- * (30s/120s, [ref] fleet audit; env =0 disables).
18
+ * (connect 30s per the fleet-audit recommendation; first-token 600s since S-202 — a self-hosted backend's long
19
+ * prefill legitimately takes minutes to the first byte, and 120s turned that into a retry handing the
20
+ * same prompt back to the same queue. env =0 disables; env =120000 restores the old posture).
19
21
  * - **Circuit breaker** (`createCircuitBreakerBrain`) wraps the *primary* gateway route(s) when
20
22
  * `MODEL_CIRCUIT_BREAKER=true`: after N transient failures it opens and fast-fails, so failover
21
23
  * switches to the bare backup immediately instead of waiting out each timeout. The last gateway
package/dist/brain.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { adjudicateModelRoute, createAnthropicBrain, createCircuitBreakerBrain, createDegradingBrain, createFailoverBrain, createOpenAIBrain, createRoutingBrain, hasAuthCarrier, resolveRouteCredential, routeRefusalText, } from "@sema-agent/core";
2
2
  import { normalizeBaseUrl } from "@sema-agent/core";
3
+ import { MODEL_FIRST_TOKEN_TIMEOUT_MS_DEFAULT } from "./config-types.js";
3
4
  import { resolveModelApiKey } from "./key-resolver.js";
4
5
  function isOffDeploymentRoot(baseUrl, config) {
5
6
  if (!baseUrl)
@@ -16,7 +17,7 @@ function degradeDeclaredRoot(fallbackBaseUrl, config) {
16
17
  export const FAST_FAIL_MAX_RETRIES = 2;
17
18
  const DEFAULT_RESILIENCE = {
18
19
  connectTimeoutMs: 30_000,
19
- firstTokenTimeoutMs: 120_000,
20
+ firstTokenTimeoutMs: MODEL_FIRST_TOKEN_TIMEOUT_MS_DEFAULT,
20
21
  idleTimeoutMs: 300_000,
21
22
  circuitBreaker: false,
22
23
  failureThreshold: 5,
package/dist/budget.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { modelCostToPricing } from "@sema-agent/core";
2
2
  import { currentPrincipal } from "./observability/principal-context.js";
3
+ import { isPermissionRuleEventKind, permissionRuleEventWordOf } from "./observability/permission-rule-events.js";
3
4
  import { promptManifestRecordOf, configAssembledRecordOf } from "./observability/prompt-manifest.js";
4
5
  export function buildPricing(models) {
5
6
  const pricing = {};
@@ -119,8 +120,8 @@ export function createTracer(metrics, costQuota, modelUsage, fleetUsage, fleetLe
119
120
  else if (e.kind.startsWith("compaction.")) {
120
121
  metrics.inc("compaction_events_total", { outcome: e.kind.slice("compaction.".length), trigger: e.trigger ?? "none" });
121
122
  }
122
- else if (e.kind === "permission.persisted_rule_allowed" || e.kind === "permission.rule_store_unreadable" || e.kind === "permission.read_only_allowed") {
123
- metrics.inc("permission_rule_events_total", { event: e.kind.slice("permission.".length) });
123
+ else if (isPermissionRuleEventKind(e.kind)) {
124
+ metrics.inc("permission_rule_events_total", { event: permissionRuleEventWordOf(e.kind) });
124
125
  }
125
126
  };
126
127
  }
@@ -34,10 +34,30 @@ export interface ConfigCatalogSpec {
34
34
  readonly derivedDefaultNote?: string;
35
35
  readonly danger?: readonly CatalogDangerAxis[];
36
36
  readonly enumValues?: readonly string[];
37
+ /**
38
+ * S-100 —— **底层布尔 ↔ 闭集词**的行内映射,只给「实算腿回布尔、而行对外说词」的 enum 行。
39
+ *
40
+ * 为什么写在行上而不是建第二张全局极性表:`config.ts` 的极性表(`ConfigKnobRecord`)记的是
41
+ * **布尔旋钮**的来源与默认方向,它压根不带词 —— 拿它当 `true→enumValues[0]` 的位置约定用,等于
42
+ * 在两个文件之间约定一个不写下来的下标。行自己声明,是同一处声明它 `enumValues` 的地方。
43
+ *
44
+ * 某一极**没有词**(该极的真实投影是「键不在场」)⇒ 显式写 `null`,不是省略 —— 省略与「这一行忘了
45
+ * 声明映射」在形上同形,而后者必须落到响亮的 `effectiveValueInvalid` 臂。
46
+ */
47
+ readonly booleanEnum?: {
48
+ readonly true: string | null;
49
+ readonly false: string | null;
50
+ };
37
51
  /** `ServiceConfig` 的平铺键(实算兜底 + center managedNow 判据对表用;嵌套块不填,走 resolve)。 */
38
52
  readonly configKey?: string;
39
- /** 本进程实算(非 secret/opaque 行才可用;返回 undefined = 本行没有实算腿,落 env 原文/staticDefault)。 */
40
- readonly resolve?: (config: ServiceConfig) => CatalogValue | undefined;
53
+ /**
54
+ * 本进程实算(非 secret/opaque 行才可用;返回 undefined = 本行没有实算腿,落 env 原文/staticDefault)。
55
+ *
56
+ * 🔴 第二参是**部署 env**,不是第五条供值腿:有几根旋钮的属主不在 `ServiceConfig` 上(它在装配点按
57
+ * env 现算,如 WebSearch 后端),而目录的纪律是「回显属主算出来的值」—— 只能把同一条实算腿的入参放宽,
58
+ * 不能在目录里再抄一遍属主的读法(S-100 / codex r1 F3 的成案:抄一遍就是第二个写者,当场分歧)。
59
+ */
60
+ readonly resolve?: (config: ServiceConfig, env: NodeJS.ProcessEnv) => CatalogValue | undefined;
41
61
  readonly center?: CatalogCenterSeatSpec;
42
62
  /** 缺省 "deploy";retired/零消费键标 "inert"(设计稿 ⚪ 的 env 腿投影)。 */
43
63
  readonly envTiming?: "inert";
@@ -69,6 +89,15 @@ export interface ConfigCatalogRow {
69
89
  readonly staticDefault?: string;
70
90
  readonly derivedDefaultNote?: string;
71
91
  readonly effectiveValue: CatalogValue;
92
+ /**
93
+ * S-100(additive;**只在真非法时铸,恒不铸 `false`**)—— 本行的实算值**落在它自己宣告的
94
+ * `enumValues` 之外**,或它的实算腿回了一个本行没有声明映射的布尔。
95
+ *
96
+ * 在场时 `effectiveValue` 恒 `null`:回显一个集外值等于目录在教消费方写一个写回去就非法的词,
97
+ * 而**静默丢**会让「配错了」与「真的没设」在 wire 上同形([ref] 响亮臂)。
98
+ * 缺席 = 这一格没问题(不是「不知道」)。
99
+ */
100
+ readonly effectiveValueInvalid?: true;
72
101
  readonly valueClass: "plain" | "secret" | "opaque";
73
102
  readonly danger: readonly CatalogDangerAxis[];
74
103
  readonly center?: ConfigCatalogRowCenter;
@@ -1,5 +1,7 @@
1
1
  import { CROSS_SESSION_INBOUND_SETTINGS, CROSS_SESSION_DIALOG_EXPIRY_VALUES } from "@sema-agent/core";
2
2
  import { THINKING_FORMATS } from "./model-compat.js";
3
+ import { MODEL_FIRST_TOKEN_TIMEOUT_MS_DEFAULT } from "./config-types.js";
4
+ import { webSearchConfigFromEnv } from "./plugins/web-search.js";
3
5
  import { CENTER_READ_FACE_LIVE_KEYS } from "./config-center/read-face.js";
4
6
  export const CATALOG_VALUE_TYPES = ["boolean", "number", "string", "enum", "csv", "json", "path", "url", "secret", "opaque"];
5
7
  export const CATALOG_TIMINGS = ["hot", "restart", "deploy", "inert"];
@@ -83,7 +85,7 @@ export const CONFIG_CATALOG = [
83
85
  r("BG_AGENT_STALE_RUNNING_MS", "orchestration", "number", "running 后台 agent 行写者心跳断更超此值翻 failed(ms;下限 10min 硬钳)", { staticDefault: String(15 * 60_000), configKey: "backgroundAgentStaleRunningMs" }),
84
86
  r("BIND_HOST", "auth", "string", "监听绑址(显式值恒生效)", { danger: SEC, derivedDefaultNote: "缺席:无鉴权写面(allowUnauthedWrites ∧ 零凭证)自动收窄 127.0.0.1,其余全接口", resolve: (c) => c.bindHost ?? null }),
85
87
  r("COMPLIANCE_ADDITIONAL_DENIES", "auth", "csv", "合规档位之上额外拒绝的能力名(只能收紧;词表=core 闭集,拼错拒启)", { danger: SEC }),
86
- r("COMPLIANCE_PROFILE", "auth", "enum", "本部署合规档位(core compliancePostureResolver 供数;拼错拒启)", { danger: SEC, enumValues: ["hipaa", "zdr"] }),
88
+ r("COMPLIANCE_PROFILE", "auth", "enum", "本部署合规档位(core compliancePostureResolver 供数;拼错拒启)", { danger: SEC, enumValues: ["hipaa", "zdr"], resolve: (c) => c.compliancePosture?.profile ?? null }),
87
89
  r("CONFIG_BOOT_FETCH_BUDGET_MS", "integrations", "number", "boot 首拉配置的抢答窗(ms;过窗以 env/兜底起服,拉取转后台热应用)", { staticDefault: "1500", configKey: "configBootFetchBudgetMs" }),
88
90
  r("CONFIG_CATALOG_ENABLED", "auth", "boolean", "本目录端点 GET /v1/config/catalog 的旋钮(operator-only 面;合规部署可关 ⇒ 路径 404 + 能力位翻假)", { staticDefault: "true", danger: SEC, configKey: "configCatalogEnabled" }),
89
91
  r("CONFIG_LKG_DURABLE", "integrations", "boolean", "LKG 跨重启 handoff(严格 'true' 才开:重启先用 LKG 起服,tier 候选借此落地)", { staticDefault: "false" }),
@@ -200,7 +202,7 @@ export const CONFIG_CATALOG = [
200
202
  r("LOG_LEVEL", "observability", "string", "日志档位", { staticDefault: "info", configKey: "logLevel" }),
201
203
  r("LSP_ENABLED", "orchestration", "boolean", "LSP 沙箱腿 opt-in(需烤好的 LSP 模板;host 腿另有 LSP_HOST_ENABLED)", { staticDefault: "false", configKey: "lspEnabled" }),
202
204
  r("LSP_HOST_ENABLED", "orchestration", "boolean", "LSP host 腿开关(本机 language server,优雅降级)", { staticDefault: "true", configKey: "lspHostEnabled" }),
203
- r("MANUAL_MODE_SHELL_GATE", "approval", "enum", "手动模式 shell 门收紧腿(off=no-op 非放松 —— 本键只有收紧半场;拼错拒启)", { danger: SEC, enumValues: ["always", "classify", "off"] }),
205
+ r("MANUAL_MODE_SHELL_GATE", "approval", "enum", "手动模式 shell 门收紧腿(off=no-op 非放松 —— 本键只有收紧半场;拼错拒启)", { danger: SEC, enumValues: ["always", "classify", "off"], resolve: (c) => c.manualModeShellGate ?? null, derivedDefaultNote: "off/未设 ⇒ 本键不贡献任何东西(归一成键缺席)⇒ 回显 null;真姿态只有 always/classify 两个" }),
204
206
  r("MAX_PRINCIPAL_COST_USD", "limitsHttp", "number", "per-principal 窗内成本天花板(0=关)", { staticDefault: "0", danger: BILL, configKey: "maxPrincipalCostUsd", center: cLimits("maxPrincipalCostUsd") }),
205
207
  r("MAX_TASK_COST_USD", "limitsHttp", "number", "单任务成本天花板(0=关)", { staticDefault: "0", danger: BILL, configKey: "maxTaskCostUsd", center: cLimits("maxTaskCostUsd") }),
206
208
  r("MAX_TASK_TOKENS", "limitsHttp", "number", "单任务 token 天花板(per-slice 窗;0=关)", { staticDefault: "0", danger: BILL, configKey: "maxTaskTokens", center: cLimits("maxTaskTokens") }),
@@ -210,7 +212,7 @@ export const CONFIG_CATALOG = [
210
212
  r("MCP_ELICITATION_MIN_INTERVAL_MS", "approval", "number", "elicitation per-server 最小间隔(ms)", { derivedDefaultNote: "缺席 ⇒ core 出厂窗" }),
211
213
  r("MCP_ELICITATION_TTL_MS", "approval", "number", "elicitation 无人应答释放窗(ms)", { derivedDefaultNote: "缺席 ⇒ core 出厂窗" }),
212
214
  r("MEMORY_CAPTURE_POLICY", "memory", "enum", "记忆 capture 姿态(open=声明按面值;governed=按 per-principal verdict;capture-required=恒采)", { enumValues: ["open", "governed", "capture-required"], staticDefault: "open", danger: SEC, configKey: "memoryCapturePolicy" }),
213
- r("MEMORY_CONSOLIDATION_DRIVER", "memory", "enum", "记忆折叠阀门(off=零装配零探针,两 admin 口 404 族)", { enumValues: ["on", "off"], staticDefault: "off", danger: BILL }),
215
+ r("MEMORY_CONSOLIDATION_DRIVER", "memory", "enum", "记忆折叠阀门(off=零装配零探针,两 admin 口 404 族)", { enumValues: ["on", "off"], staticDefault: "off", danger: BILL, resolve: (c) => c.memoryConsolidationDriver.enabled, booleanEnum: { true: "on", false: "off" } }),
214
216
  r("MEMORY_CONSOLIDATION_INTERVAL_SEC", "memory", "number", "折叠周期腿节律(秒;本 build 任何正值拒启 —— 周期腿未落地,恒 0)", { staticDefault: "0" }),
215
217
  r("MEMORY_CONSOLIDATION_SCOPES", "memory", "csv", "折叠阀门 scope 表(恰一条时=admin 口缺省;不是授权边界)"),
216
218
  r("MEMORY_DELEGATION_EVIDENCE", "memory", "enum", "委派臂污染标记的证据标准", { enumValues: ["static-face", "attested-only"], staticDefault: "static-face", danger: SEC, configKey: "memoryDelegationEvidence" }),
@@ -219,7 +221,7 @@ export const CONFIG_CATALOG = [
219
221
  r("MEMORY_EMBEDDER_ENDPOINT", "memory", "url", "OpenAI 兼容 /embeddings 端点(pg 记忆后端的向量面)"),
220
222
  r("MEMORY_EMBEDDER_MODEL", "memory", "string", "embeddings 模型名"),
221
223
  r("MEMORY_EMBEDDER_TIMEOUT_MS", "memory", "number", "单次 embed 活性上限(ms)", { staticDefault: "30000" }),
222
- r("MEMORY_ENGINE", "memory", "enum", "记忆引擎总开关(注入式文件记忆面;双族词表,未知词拒启)", { enumValues: ["on", "off"], staticDefault: "on" }),
224
+ r("MEMORY_ENGINE", "memory", "enum", "记忆引擎总开关(注入式文件记忆面;双族词表,未知词拒启)", { enumValues: ["on", "off"], staticDefault: "on", configKey: "memoryEngineEnabled", booleanEnum: { true: "on", false: "off" } }),
223
225
  r("MEMORY_ENGINE_BACKEND", "memory", "enum", "记忆持久面方言(file=现状;pg/tidb=DB 真身,多租户点亮的唯一门)", { enumValues: ["file", "pg", "tidb"], staticDefault: "file", danger: DATA, configKey: "memoryEngineBackend" }),
224
226
  r("MEMORY_ENGINE_DIR", "memory", "path", "记忆引擎根覆写", { derivedDefaultNote: "缺席 ⇒ core 默认链(AGENT_DATA_DIR → ~/.ai-agent)", configKey: "memoryEngineDir" }),
225
227
  r("MEMORY_ENGINE_REMOTE_LANE", "memory", "enum", "远端执行车道上放行 file 记忆引擎的 plane-split 覆写", { enumValues: ["allow"] }),
@@ -227,7 +229,7 @@ export const CONFIG_CATALOG = [
227
229
  r("MEMORY_ORG_DIRECTORY_JSON", "memory", "opaque", "单机形 org 目录原文(principal→org:scope 授权表;解析期整段校验)", { danger: SEC }),
228
230
  r("MEMORY_ORG_GRANT_TTL_MS", "memory", "number", "org 目录判决缓存 TTL(ms;准入判决滞后上限)", { staticDefault: "60000", configKey: "memoryOrgGrantTtlMs" }),
229
231
  r("MEMORY_ORG_UNAVAILABLE_BACKOFF_MS", "memory", "number", "org 目录取数失败退避窗(ms)", { staticDefault: "10000", configKey: "memoryOrgUnavailableBackoffMs" }),
230
- r("MEMORY_PERSISTENCE_CAPABLE", "memory", "enum", "本部署能否把「记住 X」落到持久处的自我声明(三态;未知词拒启)", { enumValues: ["on", "off"], derivedDefaultNote: "缺席 ⇒ 键不上 TaskSpec,core 按 roster 推断" }),
232
+ r("MEMORY_PERSISTENCE_CAPABLE", "memory", "enum", "本部署能否把「记住 X」落到持久处的自我声明(三态;未知词拒启)", { enumValues: ["on", "off"], derivedDefaultNote: "缺席 ⇒ 键不上 TaskSpec,core 按 roster 推断", configKey: "memoryPersistenceCapable", booleanEnum: { true: "on", false: "off" } }),
231
233
  r("MEMORY_PROVENANCE", "memory", "enum", "已暴露会话的记忆写处置(carry=带 origin 标记照写;off=关)", { enumValues: ["off", "carry"], staticDefault: "carry", danger: SEC, configKey: "memoryProvenance" }),
232
234
  r("MEMORY_SCOPE", "memory", "string", "单用户部署的记忆 scope 名", { derivedDefaultNote: "缺席:引擎开 ∧ 单用户 ⇒ local;其余 ⇒ 缺席(记忆暗)", configKey: "memoryScope" }),
233
235
  r("MEMORY_SYNC_MAX_PULL_ENTRIES", "memory", "number", "TOC 同步单轮 pull 条数帽(设了必须 ≥1 整数)", { derivedDefaultNote: "缺席 ⇒ 键缺席,core 默认" }),
@@ -273,7 +275,7 @@ export const CONFIG_CATALOG = [
273
275
  r("MODEL_DEGRADE_TO", "modelPlane", "string", "降级目标模型(设了=开降级;外部网关名不必在目录)", { danger: BILL }),
274
276
  r("MODEL_DEGRADE_TO_VISION", "modelPlane", "boolean", "降级目标的 vision 声明(false ⇒ 带图任务丢降级不丢图)", { staticDefault: "false" }),
275
277
  r("MODEL_EXTRA_BODY", "modelPlane", "opaque", "生成参数直通 JSON(top_k/logit_bias 等;必须静态 —— 前缀缓存键序)", { danger: SEC }),
276
- r("MODEL_FIRST_TOKEN_TIMEOUT_MS", "modelPlane", "number", "首 token 超时(ms;0=关 —— 开流不吐字的唯一看门狗)", { staticDefault: "120000" }),
278
+ r("MODEL_FIRST_TOKEN_TIMEOUT_MS", "modelPlane", "number", "首 token 超时(ms;0=关 —— 开流不吐字的唯一看门狗;自托管长 prefill 后端按此档留足)", { staticDefault: String(MODEL_FIRST_TOKEN_TIMEOUT_MS_DEFAULT) }),
277
279
  r("MODEL_FREQUENCY_PENALTY", "modelPlane", "number", "frequency_penalty 直通(压过 EXTRA_BODY 同键)"),
278
280
  r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(默认 localhost 占位 —— 公开分发不烤内网坐标)", { staticDefault: "http://127.0.0.1:8000/v1", configKey: "gatewayBaseUrl", center: cModels("models[].baseUrl") }),
279
281
  r("MODEL_GATEWAY_FALLBACK_URLS", "modelPlane", "url", "网关故障转移地址表(CSV)", { configKey: "gatewayFallbackUrls" }),
@@ -344,6 +346,7 @@ export const CONFIG_CATALOG = [
344
346
  r("REAP_INTERVAL_SEC", "limitsHttp", "number", "reaper 周期(秒)", { staticDefault: "60", configKey: "reapIntervalSec" }),
345
347
  r("REAP_RUN_STALE_SEC", "limitsHttp", "number", "run 活性判陈旧阈(秒)", { staticDefault: "120", configKey: "runStaleSec", center: cLimits("runStaleSec") }),
346
348
  r("REMOTE_EXEC", "orchestration", "enum", "执行车道选择(点名而配不齐/认不出 ⇒ 拒启,绝不静默降级到 host/stub)", { enumValues: ["e2b", "k8s", "ssh", "adb", "host", "local-docker", "device"], danger: SEC, derivedDefaultNote: "缺席:CONFIG_PROVIDER=local ⇒ host;其余 ⇒ 进程内 stub", configKey: "remoteExec", resolve: (c) => c.remoteExec?.provider ?? null }),
349
+ r("REMOTE_EXEC_HOME_DIR", "orchestration", "path", "执行环境的 home 目录(`~/` 形权限规则的基;core ExecutionEnv.homeDir 座)", { danger: SEC, derivedDefaultNote: "缺席:host/in-process ⇒ os.homedir();e2b ⇒ /home/user;k8s/local-docker/ssh/adb/device ⇒ 无(`~/` 规则 422 响亮拒)", configKey: "remoteExecHomeDir", resolve: (c) => c.remoteExecHomeDir ?? null }),
347
350
  r("REQUIRE_PRINCIPAL", "auth", "boolean", "多租户开关(posture 判别位:true ⇒ CC 能力族默认收紧)", { staticDefault: "false", danger: SEC, configKey: "requirePrincipal" }),
348
351
  r("RESOURCE_SUSPEND", "approval", "boolean", "durable 路径 opt-in core resourceSuspend(预算/preempt ⇒ 可恢复挂起)", { staticDefault: "false", configKey: "resourceSuspend" }),
349
352
  r("RESOURCE_SUSPEND_MAX_SLICES", "approval", "number", "resume 链片数上限(0=不设;零进展循环兜底)", { staticDefault: "0", configKey: "resourceSuspendMaxSlices" }),
@@ -351,12 +354,12 @@ export const CONFIG_CATALOG = [
351
354
  r("RESOURCE_SUSPEND_TOTAL_TOKENS", "approval", "number", "跨片 token 总额(0=不设;与 MAX_TASK_TOKENS 的 per-slice 窗是两根轴)", { staticDefault: "0", danger: BILL, configKey: "resourceSuspendTotalTokens" }),
352
355
  r("RESOURCE_SUSPEND_TTL_SEC", "approval", "number", "resource/preempt checkpoint 的弃置 GC TTL(秒;0 ⇒ core 默认 30d)", { staticDefault: "0", danger: DATA, configKey: "resourceSuspendTtlSec" }),
353
356
  r("RETENTION_MAX_AGE_DAYS", "auth", "number", "托管留存期声明(core 启动期能力校验;真删除归 sweep lane)", { danger: DATA }),
354
- r("RETENTION_MODE", "auth", "enum", "留存 sweep 灰度档(拼错拒启 —— audit-only 谎成 enforce 两个方向都致命)", { enumValues: ["audit-only", "enforce"], staticDefault: "audit-only", danger: DATA }),
357
+ r("RETENTION_MODE", "auth", "enum", "留存 sweep 灰度档(拼错拒启 —— audit-only 谎成 enforce 两个方向都致命)", { enumValues: ["audit-only", "enforce"], staticDefault: "audit-only", danger: DATA, resolve: (c) => c.retentionSweep.mode }),
355
358
  r("RETENTION_SWEEP_INTERVAL_SEC", "auth", "number", "留存 sweep 节律(秒;0=lane 关)", { staticDefault: "0", danger: DATA }),
356
359
  r("REWIND_SNAPSHOT_MAX_MB", "store", "number", "已退役(S-15 整树快照纪元退役):设了只 boot warn 一次,零行为", { envTiming: "inert" }),
357
360
  r("ROSTER_RETENTION_MS", "orchestration", "number", "agent_roster 行 TTL(ms;SQL twins 扩展面)", { staticDefault: String(30 * 24 * 60 * 60 * 1000), danger: DATA, configKey: "rosterRetentionMs" }),
358
361
  r("ROUTER_ENABLED", "orchestration", "boolean", "A 价值路由(per-task 编排自动决策;ship-dark)", { staticDefault: "false", configKey: "routerEnabled" }),
359
- r("RUN_STORE_STRICT_HYDRATE", "store", "enum", "file run-store 列不出 runs/ 时拒启的 fail-closed 门(缺省带病起动+响亮披露)", { enumValues: ["on", "off"], staticDefault: "off", danger: DATA, configKey: "runStoreStrictHydrate" }),
362
+ r("RUN_STORE_STRICT_HYDRATE", "store", "enum", "file run-store 列不出 runs/ 时拒启的 fail-closed 门(缺省带病起动+响亮披露)", { enumValues: ["on", "off"], staticDefault: "off", danger: DATA, configKey: "runStoreStrictHydrate", booleanEnum: { true: "on", false: "off" } }),
360
363
  r("S3_ENDPOINT", "store", "url", "外接 S3 兼容位(单独在场即开公链签发,兼作公网面)", { danger: DATA }),
361
364
  r("S3_PUBLIC_BUCKET", "store", "string", "SendUserFile 公链桶", { derivedDefaultNote: "缺席 ⇒ sema-public" }),
362
365
  r("S3_PUBLIC_ENDPOINT", "store", "url", "SendUserFile 公网面 endpoint(跨仓契约名,禁改)"),
@@ -441,7 +444,7 @@ export const CONFIG_CATALOG = [
441
444
  r("WEB_SEARCH_ENDPOINT", "orchestration", "url", "web search 后端 endpoint 覆写(searxng 自建面)"),
442
445
  r("WEB_SEARCH_MAX_RESULTS", "orchestration", "number", "web search 单次结果数上限", { derivedDefaultNote: "缺席/非正 ⇒ backend 默认" }),
443
446
  r("WEB_SEARCH_PROBE_ON_BOOT", "orchestration", "boolean", "boot 期 web search 连通性探测(只认 true/1;含糊值当没开——猜错方向是凭空出网)", { staticDefault: "false" }),
444
- r("WEB_SEARCH_PROVIDER", "orchestration", "enum", "web search 工具后端(设合法词=装配 WebSearch;其它/缺席=不装配)", { enumValues: ["brave", "tavily", "searxng"], danger: SEC }),
447
+ r("WEB_SEARCH_PROVIDER", "orchestration", "enum", "web search 工具后端(设合法词=装配 WebSearch;其它/缺席=不装配)", { enumValues: ["brave", "tavily", "searxng"], danger: SEC, resolve: (_c, env) => webSearchConfigFromEnv(env)?.provider ?? null }),
445
448
  r("WEB_SEARCH_SEARXNG_PARAMS", "orchestration", "string", "searxng 额外查询参数(k=v 对;解析不出任何一对 ⇒ 键不铸)"),
446
449
  r("WEB_SEARCH_TIMEOUT_MS", "orchestration", "number", "web search 单次超时(ms)", { derivedDefaultNote: "缺席/非正 ⇒ backend 默认" }),
447
450
  r("WORKFLOW_AGENTS_READONLY", "orchestration", "boolean", "workflow 子 agent 只读钳(TOB 保守旋钮;默认同权 CC parity)", { staticDefault: "false", danger: SEC, configKey: "workflowAgentsReadOnly" }),
@@ -483,17 +486,36 @@ function stripUrlUserinfo(v) {
483
486
  u.hash = "";
484
487
  return u.href;
485
488
  }
486
- function sanitizeValue(type, v) {
489
+ function sanitizeEnumValue(spec, v) {
490
+ const words = spec.enumValues;
491
+ if (v === null)
492
+ return { value: null };
493
+ if (words === undefined)
494
+ return { value: null, invalid: true };
495
+ if (typeof v === "boolean") {
496
+ const word = spec.booleanEnum?.[v ? "true" : "false"];
497
+ if (word === null)
498
+ return { value: null };
499
+ return word !== undefined && words.includes(word) ? { value: word } : { value: null, invalid: true };
500
+ }
501
+ if (typeof v === "string")
502
+ return words.includes(v) ? { value: v } : { value: null, invalid: true };
503
+ return { value: null, invalid: true };
504
+ }
505
+ function sanitizeValue(spec, v) {
506
+ const type = spec.type;
487
507
  if (type === "url") {
488
508
  if (typeof v === "string")
489
- return v.includes(",") ? v.split(",").map((s) => stripUrlUserinfo(s.trim())).filter((s) => s.length > 0) : stripUrlUserinfo(v);
509
+ return { value: v.includes(",") ? v.split(",").map((s) => stripUrlUserinfo(s.trim())).filter((s) => s.length > 0) : stripUrlUserinfo(v) };
490
510
  if (Array.isArray(v))
491
- return v.map((s) => stripUrlUserinfo(s));
492
- return v;
511
+ return { value: v.map((s) => stripUrlUserinfo(s)) };
512
+ return { value: v };
493
513
  }
494
514
  if (type === "csv" && typeof v === "string")
495
- return v.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
496
- return v;
515
+ return { value: v.split(",").map((s) => s.trim()).filter((s) => s.length > 0) };
516
+ if (type === "enum")
517
+ return sanitizeEnumValue(spec, v);
518
+ return { value: v };
497
519
  }
498
520
  function valueClassOf(type) {
499
521
  if (type === "secret")
@@ -520,6 +542,7 @@ export function buildConfigCatalog(inputs) {
520
542
  const centerEffective = seat !== undefined && managedNow && centerTiming === "hot" && (seat.precedence === "center-wins" || !envHeld);
521
543
  const effectiveLane = centerEffective ? "center" : envHeld ? "env" : "default";
522
544
  let effectiveValue;
545
+ let effectiveValueInvalid;
523
546
  if (valueClass === "secret") {
524
547
  effectiveValue = { present: envHeld };
525
548
  }
@@ -527,15 +550,11 @@ export function buildConfigCatalog(inputs) {
527
550
  effectiveValue = envHeld ? { present: true, length: raw.length } : { present: false };
528
551
  }
529
552
  else {
530
- const resolved = spec.resolve !== undefined ? spec.resolve(config) : spec.configKey !== undefined ? primitiveConfigValue(config, spec.configKey) : undefined;
531
- effectiveValue =
532
- resolved !== undefined
533
- ? sanitizeValue(spec.type, resolved)
534
- : knob !== undefined
535
- ? knob.value
536
- : envHeld
537
- ? sanitizeValue(spec.type, raw)
538
- : spec.staticDefault ?? null;
553
+ const resolved = spec.resolve !== undefined ? spec.resolve(config, env) : spec.configKey !== undefined ? primitiveConfigValue(config, spec.configKey) : undefined;
554
+ const picked = resolved !== undefined ? resolved : knob !== undefined ? knob.value : envHeld ? raw : spec.staticDefault ?? null;
555
+ const sanitized = sanitizeValue(spec, picked);
556
+ effectiveValue = sanitized.value;
557
+ effectiveValueInvalid = sanitized.invalid;
539
558
  }
540
559
  return {
541
560
  name: spec.name,
@@ -549,6 +568,7 @@ export function buildConfigCatalog(inputs) {
549
568
  ...(spec.staticDefault !== undefined ? { staticDefault: spec.staticDefault } : {}),
550
569
  ...(spec.derivedDefaultNote !== undefined ? { derivedDefaultNote: spec.derivedDefaultNote } : {}),
551
570
  effectiveValue,
571
+ ...(effectiveValueInvalid === true ? { effectiveValueInvalid } : {}),
552
572
  valueClass,
553
573
  danger: spec.danger ?? [],
554
574
  ...(seat !== undefined && centerTiming !== undefined