@sema-agent/server 7.37.0 → 7.38.0-rc.2

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 (83) hide show
  1. package/USAGE.md +2 -1
  2. package/dist/adoption/plan.js +16 -0
  3. package/dist/boot/budget-tracing.d.ts +1 -1
  4. package/dist/boot/budget-tracing.js +12 -5
  5. package/dist/boot/config-center.d.ts +27 -2
  6. package/dist/boot/config-center.js +168 -15
  7. package/dist/boot/limit-sync.d.ts +32 -0
  8. package/dist/boot/limit-sync.js +35 -0
  9. package/dist/boot/reapers.d.ts +68 -0
  10. package/dist/boot/reapers.js +63 -4
  11. package/dist/boot/resolve-spec.js +2 -0
  12. package/dist/boot/runner-deps.d.ts +1 -1
  13. package/dist/boot/runner-deps.js +4 -0
  14. package/dist/boot/stores.js +13 -0
  15. package/dist/config-center/apply-effective.d.ts +41 -3
  16. package/dist/config-center/apply-effective.js +87 -10
  17. package/dist/config-center/apply-ledger.d.ts +82 -0
  18. package/dist/config-center/apply-ledger.js +104 -0
  19. package/dist/config-center/facade.d.ts +1 -1
  20. package/dist/config-center/hot-keys-registry.d.ts +62 -0
  21. package/dist/config-center/hot-keys-registry.js +54 -0
  22. package/dist/config-center/http-client.js +12 -2
  23. package/dist/config-center/restart-signal.js +5 -2
  24. package/dist/config-center/stage-limits.d.ts +59 -0
  25. package/dist/config-center/stage-limits.js +188 -0
  26. package/dist/config-center/types.d.ts +60 -0
  27. package/dist/config-invariants.d.ts +65 -0
  28. package/dist/config-invariants.js +139 -0
  29. package/dist/config-provider.js +11 -0
  30. package/dist/config-types.d.ts +10 -1
  31. package/dist/config.d.ts +11 -0
  32. package/dist/config.js +37 -5
  33. package/dist/http/routes/admin-config-refresh.d.ts +20 -0
  34. package/dist/http/routes/admin-config-refresh.js +43 -0
  35. package/dist/http/routes/diagnostics.js +4 -0
  36. package/dist/http/routes/runs.js +10 -6
  37. package/dist/http/server.d.ts +8 -0
  38. package/dist/http/server.js +26 -0
  39. package/dist/main.js +49 -6
  40. package/dist/memory-sync.js +50 -1
  41. package/dist/observability/cost-quota.d.ts +23 -3
  42. package/dist/observability/cost-quota.js +27 -4
  43. package/dist/observability/fail-open.d.ts +8 -0
  44. package/dist/observability/fail-open.js +8 -0
  45. package/dist/observability/rate-limit.d.ts +11 -1
  46. package/dist/observability/rate-limit.js +8 -2
  47. package/dist/plan-cache-probe.d.ts +5 -7
  48. package/dist/plan-cache-probe.js +27 -5
  49. package/dist/plugins/caching-session-store.d.ts +20 -4
  50. package/dist/plugins/caching-session-store.js +43 -2
  51. package/dist/plugins/fork-routing-session-store.d.ts +26 -4
  52. package/dist/plugins/fork-routing-session-store.js +134 -9
  53. package/dist/plugins/memory-engine-pg.d.ts +1 -0
  54. package/dist/plugins/memory-engine-pg.js +34 -4
  55. package/dist/plugins/memory-engine-tidb.d.ts +1 -0
  56. package/dist/plugins/memory-engine-tidb.js +17 -2
  57. package/dist/plugins/memory-origin-law.d.ts +69 -0
  58. package/dist/plugins/memory-origin-law.js +118 -1
  59. package/dist/plugins/pg-cost-quota.d.ts +7 -2
  60. package/dist/plugins/pg-cost-quota.js +25 -2
  61. package/dist/plugins/pg-rate-limiter.d.ts +7 -2
  62. package/dist/plugins/pg-rate-limiter.js +20 -2
  63. package/dist/plugins/pg-session-storage.d.ts +30 -7
  64. package/dist/plugins/pg-session-storage.js +145 -23
  65. package/dist/plugins/retention-store-sql.js +10 -1
  66. package/dist/plugins/session-placement.d.ts +113 -0
  67. package/dist/plugins/session-placement.js +153 -0
  68. package/dist/plugins/session-store.js +7 -1
  69. package/dist/plugins/tidb-cost-quota.d.ts +7 -2
  70. package/dist/plugins/tidb-cost-quota.js +26 -2
  71. package/dist/plugins/tidb-pool.js +15 -1
  72. package/dist/plugins/tidb-rate-limiter.d.ts +9 -2
  73. package/dist/plugins/tidb-rate-limiter.js +22 -2
  74. package/dist/plugins/tidb-session-storage.js +10 -2
  75. package/dist/plugins/tidb-session-store.d.ts +55 -7
  76. package/dist/plugins/tidb-session-store.js +174 -24
  77. package/dist/plugins/write-behind-counter.d.ts +17 -2
  78. package/dist/plugins/write-behind-counter.js +19 -1
  79. package/dist/runs.d.ts +3 -1
  80. package/dist/runs.js +5 -2
  81. package/dist/trace/engine-notice-wire.d.ts +1 -1
  82. package/dist/trace/engine-notice-wire.js +24 -6
  83. package/package.json +3 -3
@@ -0,0 +1,139 @@
1
+ import { assertGuardPatternsUsable } from "./deployment-governance.js";
2
+ /**
3
+ * 活体心跳周期(ms)——`runs.ts` 的 `HEARTBEAT_MS` 就是它(那边 re-export,单源在此)。
4
+ *
5
+ * 为什么常量住在不变量文件里:它唯一的**判据用途**是「陈旧窗必须大于心跳」这条不变量,而该不变量现在
6
+ * 有 boot 与 stage 两个消费面。放在这里,两面读的是同一个数,`runs.ts` 的心跳环也读同一个数。
7
+ */
8
+ export const LIVENESS_HEARTBEAT_MS = 30_000;
9
+ const anyServiceAuth = (v) => Boolean(v.authToken) || Object.keys(v.authTokens ?? {}).length > 0;
10
+ /** 与 `resolveBindHost` 逐字同判:无鉴权写面 + 零凭据 ⇒ boot 期把监听地址收窄到 loopback。 */
11
+ const mustNarrowBind = (v) => Boolean(v.allowUnauthedWrites) && !anyServiceAuth(v);
12
+ /** 直连门的合取项(config.ts:2788 的同一组);任一项翻转 = 门姿态变更。 */
13
+ const directDoorConjuncts = (v) => Boolean(v.directApprovalDoor) && Boolean(v.durableApproval) && v.approvalHmacKeys.length > 0 && v.principalJwtPubkeys.length > 0 && Boolean(v.principalJwtIss) && Boolean(v.principalJwtAud);
14
+ export const CONFIG_CROSS_INVARIANTS = [
15
+ {
16
+ id: "run-stale-heartbeat",
17
+ keys: ["runStaleSec"],
18
+ restartReason: "run-stale-heartbeat",
19
+ bootAnchor: { file: "src/main.ts", marker: "CROSS-INVARIANT:run-stale-heartbeat" },
20
+ check: ({ next }) => next.runStaleSec * 1000 <= LIVENESS_HEARTBEAT_MS
21
+ ? `runStaleSec (${next.runStaleSec}s) must exceed the ${LIVENESS_HEARTBEAT_MS / 1000}s liveness heartbeat — the reaper would race a run that is still heartbeating normally and reap live work`
22
+ : undefined,
23
+ },
24
+ {
25
+ // 转换型:boot 期算一次的绑址收窄(resolveBindHost → server.listen)在运行期无法重算,所以任何
26
+ // **让收窄条件从不成立变成成立**的热切都是谎:写门确实收紧了,监听面却还在原来的接口上。
27
+ // 反向(窄→宽,例如加了凭据)不拒:那是收紧姿态,已绑的 loopback 只会更严。
28
+ id: "bind-posture",
29
+ keys: ["authToken", "authTokens", "allowUnauthedWrites"],
30
+ restartReason: "bind-posture",
31
+ check: ({ live, next }) => !mustNarrowBind(live) && mustNarrowBind(next)
32
+ ? "this candidate would make the unauthenticated-write posture require narrowing the listener to 127.0.0.1, but the listen address is bound once at boot — refusing to hot-apply (restart to re-bind)"
33
+ : undefined,
34
+ },
35
+ {
36
+ id: "bake-runner-credential",
37
+ keys: ["imageBakes", "authTokens"],
38
+ restartReason: "bake-runner-credential",
39
+ bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:bake-runner-credential" },
40
+ check: ({ next }) => next.imageBakes.enabled && !Object.values(next.authTokens ?? {}).includes(next.imageBakes.runnerPrincipal)
41
+ ? `IMAGE_BAKES_ENABLED is on but no SERVICE_AUTH_TOKENS entry maps to the runner principal '${next.imageBakes.runnerPrincipal}' — claim/ingest/heartbeat would all 403`
42
+ : undefined,
43
+ },
44
+ {
45
+ // 状态 + 转换两面都判:半开的直连门(boot 会拒启的那些组合)绝不许经热路径造出来;门**姿态**
46
+ // (激活/未激活)的翻转同样只许重启 —— 消费面(/health directDoorActive、鉴权腿)在 boot 期定形。
47
+ id: "direct-door-anchors",
48
+ keys: ["directApprovalDoor", "durableApproval", "approvalHmacKeys", "principalJwtPubkeys", "principalJwtIss", "principalJwtAud", "requirePrincipal", "operatorPrincipals"],
49
+ restartReason: "direct-door",
50
+ bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:direct-door-anchors" },
51
+ check: ({ live, next }) => {
52
+ if (next.directApprovalDoor) {
53
+ const missing = [];
54
+ if (!next.durableApproval)
55
+ missing.push("DURABLE_APPROVAL");
56
+ if (next.approvalHmacKeys.length === 0)
57
+ missing.push("APPROVAL_HMAC_KEYS");
58
+ if (next.principalJwtPubkeys.length === 0)
59
+ missing.push("PRINCIPAL_JWT_PUBKEYS");
60
+ if (!next.principalJwtIss)
61
+ missing.push("PRINCIPAL_JWT_ISS");
62
+ if (!next.principalJwtAud)
63
+ missing.push("PRINCIPAL_JWT_AUD");
64
+ if (next.operatorPrincipals.length === 0)
65
+ missing.push("OPERATOR_PRINCIPALS");
66
+ if (!next.requirePrincipal)
67
+ missing.push("REQUIRE_PRINCIPAL");
68
+ if (missing.length > 0)
69
+ return `a half-open direct approval door is refused: missing ${missing.join(", ")}`;
70
+ }
71
+ return directDoorConjuncts(live) !== directDoorConjuncts(next)
72
+ ? "the direct-door posture (its conjunction of approval/JWT anchors) is decided at boot — refusing to hot-flip it (restart to change the door)"
73
+ : undefined;
74
+ },
75
+ },
76
+ {
77
+ id: "operator-principals-required",
78
+ keys: ["durableApproval", "requirePrincipal", "operatorPrincipals"],
79
+ restartReason: "operator-principals",
80
+ bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:operator-principals-required" },
81
+ check: ({ next }) => next.durableApproval === true && next.requirePrincipal && next.operatorPrincipals.length === 0
82
+ ? "DURABLE_APPROVAL + REQUIRE_PRINCIPAL with an EMPTY OPERATOR_PRINCIPALS makes isOperator() true-for-all — every authenticated tenant could read every other tenant's pending-approval queue"
83
+ : undefined,
84
+ },
85
+ {
86
+ // bake 门是 build-host-RCE 面,空 operator 名单让 isOperator 变 true-for-all = world-writable RCE。
87
+ // 与上一条同族但**判据不同**(那条判多租户 durable 审批的读面),故各自登记、各自锚。
88
+ id: "bake-operator-principals",
89
+ keys: ["imageBakes", "operatorPrincipals"],
90
+ restartReason: "operator-principals",
91
+ bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:bake-operator-principals" },
92
+ check: ({ next }) => next.imageBakes.enabled && next.operatorPrincipals.length === 0
93
+ ? "IMAGE_BAKES_ENABLED with an EMPTY OPERATOR_PRINCIPALS is a world-writable bake door (the runner runs build.sh on the privileged build host)"
94
+ : undefined,
95
+ },
96
+ {
97
+ id: "sensitive-write-patterns",
98
+ keys: ["sensitiveWritePatterns"],
99
+ restartReason: "sensitive-write-patterns",
100
+ bootAnchor: { file: "src/boot/resolve-spec.ts", marker: "CROSS-INVARIANT:sensitive-write-patterns" },
101
+ check: ({ next }) => {
102
+ try {
103
+ assertGuardPatternsUsable({ sensitiveWritePatterns: next.sensitiveWritePatterns });
104
+ return undefined;
105
+ }
106
+ catch (e) {
107
+ return e instanceof Error ? e.message : String(e);
108
+ }
109
+ },
110
+ },
111
+ {
112
+ id: "leader-host-lane",
113
+ keys: ["leaderEnabled", "remoteExec"],
114
+ restartReason: "leader-lane",
115
+ bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:leader-host-lane" },
116
+ check: ({ next }) => next.leaderEnabled && next.remoteExec?.provider === "host"
117
+ ? "LEADER_ENABLED with REMOTE_EXEC=host shares ONE absolute workspace across concurrent sub-workers (silent git-state corruption)"
118
+ : undefined,
119
+ },
120
+ {
121
+ id: "leader-durable-store",
122
+ keys: ["leaderEnabled", "dbBackend"],
123
+ restartReason: "leader-lane",
124
+ bootAnchor: { file: "src/config.ts", marker: "CROSS-INVARIANT:leader-durable-store" },
125
+ check: ({ next }) => next.leaderEnabled && next.dbBackend !== "mysql" && next.dbBackend !== "pg"
126
+ ? `LEADER_ENABLED with DB_BACKEND=${next.dbBackend} (no external SQL store) degrades the leader-run registry to a per-process Map`
127
+ : undefined,
128
+ },
129
+ ];
130
+ /** 全表校验(**第一条不过即拒**:候选是整批的,报第一个成因足够行动,继续跑其余条目只会淹掉它)。 */
131
+ export function checkCrossInvariants(input) {
132
+ for (const inv of CONFIG_CROSS_INVARIANTS) {
133
+ const reason = inv.check(input);
134
+ if (reason !== undefined)
135
+ return { ok: false, id: inv.id, reason, restartReason: inv.restartReason };
136
+ }
137
+ return { ok: true };
138
+ }
139
+ //# sourceMappingURL=config-invariants.js.map
@@ -161,6 +161,14 @@ export class LocalConfigProvider {
161
161
  onWarning: (w) => {
162
162
  if (w.kind === "domain-defaulted")
163
163
  parseWarnings.push({ domain: w.domain, error: w.error instanceof Error ? w.error.message : String(w.error) });
164
+ // registry-core 0.19.0 响亮化三 kind(LANDING-7.38 接线件③):dropped/unread=operator 写了但没生效
165
+ // (与 defaulted 同级),carried=承运但键表方(stage-limits)才认——都入诊断面,禁静默丢。
166
+ else if (w.kind === "unknown-keys-dropped")
167
+ parseWarnings.push({ domain: w.domain, error: `unknown keys dropped by domain schema: ${w.keys.join(", ")}` });
168
+ else if (w.kind === "unknown-keys-carried")
169
+ parseWarnings.push({ domain: w.domain, error: `unknown keys carried to consumer (spelling?): ${w.keys.join(", ")}` });
170
+ else if (w.kind === "unread-config-file")
171
+ parseWarnings.push({ domain: w.domain ?? "config.d", error: `unread config file ${w.file} (${w.why})` });
164
172
  },
165
173
  });
166
174
  const { effective: eff, domainErrors } = await store.getEffective({ tolerant: true });
@@ -333,6 +341,9 @@ export function mapToServiceEffective(eff, version) {
333
341
  ...(eff.runtime.maxPrincipalCostUsd !== undefined ? { maxPrincipalCostUsd: eff.runtime.maxPrincipalCostUsd } : {}),
334
342
  ...(eff.runtime.costQuotaWindowSec !== undefined ? { costQuotaWindowSec: eff.runtime.costQuotaWindowSec } : {}),
335
343
  },
344
+ // #322 批1(registry-core 0.19.0 第 17 域):`limits` 整包透传——本地腿的最后一米在此收口;漏了
345
+ // 这行,14 键在 local lane 整域消失、继续吃 env 基线(LANDING-7.38 接线件②的事故形)。
346
+ ...(eff.limits !== undefined ? { limits: eff.limits } : {}),
336
347
  // Governance trio (autonomy/commandPolicy/approvalRequire): moved OUT of runtime into the dedicated
337
348
  // governance domain (registry-core 0.6, runtime double-write mirror removed in 0.7.1 — buildEffective
338
349
  // still READ-lifts legacy raw runtime values into governance, so old on-disk config keeps working).
@@ -449,6 +449,15 @@ export interface ServiceConfigFlat {
449
449
  * ⚠️ **不冻进 checkpoint**:resume 的那条腿跟**当前**部署配置走(core 成文)。
450
450
  * 运维读面 = `GET /v1/diagnostics/wiring` 的 `memoryPosture.provenance`(`null`=本部署未设)。 */
451
451
  memoryProvenance?: "off" | "carry";
452
+ /** debt #322-B(core 5.48 C18 提货件):委派入口 caps 的部署旋钮 —— env `DELEGATION_MAX_CONCURRENT` /
453
+ * `DELEGATION_MAX_PER_SESSION`(整数,numEnvBounded 响亮拒坏值)。**缺席 = 不铸键**(core 自缺省
454
+ * CC parity 20/200;`memoryDelegationEvidence` 同姿态);两键各自独立可设,pair 合法性
455
+ * (maxConcurrent ≤ maxCumulativePerSession)由 core prepare 的 `config.delegation_entry_caps`
456
+ * 响亮拒承担,本层不复刻第二判官。部署席 ONLY(TaskSpec 无同名键)。 */
457
+ delegationEntryCaps?: {
458
+ maxConcurrent?: number;
459
+ maxCumulativePerSession?: number;
460
+ };
452
461
  /** 件A 单机形目录:env `MEMORY_ORG_DIRECTORY_JSON` 原文(`Record<principal, Record<org:scope, {write?}>>`)。
453
462
  * config 解析期即整段校验(parseOrgDirectoryStatic,fail-loud);此处存**原文**(config=纯数据,
454
463
  * Map 在装配点重建)。与 config-center 目录腿互斥的裁决在装配点(两者都在=center 腿胜,env 表忽略
@@ -1392,7 +1401,7 @@ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled"
1392
1401
  * config 这一层它只是一句声明。 */
1393
1402
  export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "compliancePosture" | "lockedConfigKeys" | "retentionPolicy" | "retentionSweep" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
1394
1403
  /** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
1395
- export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes" | "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude">;
1404
+ export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes" | "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude">;
1396
1405
  /** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
1397
1406
  export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "sighupIdleGraceMs" | "parentPid" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
1398
1407
  /** 组:observability(可观测)。 */
package/dist/config.d.ts CHANGED
@@ -200,6 +200,17 @@ export declare function formatUnmatchableToolNames(source: string, bad: readonly
200
200
  /** #270 车2 —— sweep 节律的上界(秒)。`setInterval` 的 delay 是 32 位有符号毫秒(2^31-1);越界会被
201
201
  * Node **重置成 1ms**,理由与拒启判据见调用点。导出供判据与文档单点引用。 */
202
202
  export declare const RETENTION_SWEEP_INTERVAL_MAX_SEC: number;
203
+ /** 域:limits+http(限额与 HTTP 面)—— 端口、附件/工作区体积与 TTL、成本与速率天花板、排空窗、回收周期。 */
204
+ /**
205
+ * #322:limitsHttp 组里**有下限**的三个数值旋钮的下限值 —— env 腿(夹取+点名)与 center 热腿(整批拒)
206
+ * 共用同一组阈值。判据同源、**处置刻意不同**:env 值是既有部署的存量,夹取不打断服务;center 发布是一次
207
+ * 显式且可重发的动作,拒绝才是有意义的反馈(#123 响亮拒)。
208
+ */
209
+ export declare const LIMITS_HTTP_FLOORS: {
210
+ readonly attachmentMaxBytes: 1024;
211
+ readonly attachmentUnboundTtlMs: 60000;
212
+ readonly drainGraceMs: 10000;
213
+ };
203
214
  /** 组名 → 该组取景的平铺键(introspection 面:测试用它钉「每个平铺键恰好被一组取景」)。 */
204
215
  export declare const CONFIG_GROUP_KEYS: {
205
216
  readonly [G in keyof ServiceConfigGroups]: readonly (keyof ServiceConfigFlat)[];
package/dist/config.js CHANGED
@@ -1639,6 +1639,17 @@ function parseMemoryDomain(ctx) {
1639
1639
  ...(process.env.MEMORY_PROVENANCE !== undefined
1640
1640
  ? { memoryProvenance: enumEnv("MEMORY_PROVENANCE", "carry", ["off", "carry"]) }
1641
1641
  : {}),
1642
+ // debt #322-B(core 5.48 C18):委派入口 caps。两 env 各自独立可设;设了=numEnvBounded 启动期
1643
+ // 响亮拒坏值(#210),缺席=键不铸(core 自缺省 CC parity 20/200,不复制上游缺省)。pair 合法性
1644
+ // (concurrent ≤ cumulative)归 core prepare 的 `config.delegation_entry_caps` 门,单判官不复刻。
1645
+ ...(process.env.DELEGATION_MAX_CONCURRENT !== undefined || process.env.DELEGATION_MAX_PER_SESSION !== undefined
1646
+ ? {
1647
+ delegationEntryCaps: {
1648
+ ...(process.env.DELEGATION_MAX_CONCURRENT !== undefined ? { maxConcurrent: numEnvBounded("DELEGATION_MAX_CONCURRENT", "20", 1, 10_000) } : {}),
1649
+ ...(process.env.DELEGATION_MAX_PER_SESSION !== undefined ? { maxCumulativePerSession: numEnvBounded("DELEGATION_MAX_PER_SESSION", "200", 1, 100_000) } : {}),
1650
+ },
1651
+ }
1652
+ : {}),
1642
1653
  ...(memoryOrgDirectoryJson !== undefined ? { memoryOrgDirectoryJson } : {}),
1643
1654
  memoryOrgGrantTtlMs,
1644
1655
  memoryOrgUnavailableBackoffMs,
@@ -2090,6 +2101,16 @@ function parseOrchestrationDomain(ctx) {
2090
2101
  };
2091
2102
  }
2092
2103
  /** 域:limits+http(限额与 HTTP 面)—— 端口、附件/工作区体积与 TTL、成本与速率天花板、排空窗、回收周期。 */
2104
+ /**
2105
+ * #322:limitsHttp 组里**有下限**的三个数值旋钮的下限值 —— env 腿(夹取+点名)与 center 热腿(整批拒)
2106
+ * 共用同一组阈值。判据同源、**处置刻意不同**:env 值是既有部署的存量,夹取不打断服务;center 发布是一次
2107
+ * 显式且可重发的动作,拒绝才是有意义的反馈(#123 响亮拒)。
2108
+ */
2109
+ export const LIMITS_HTTP_FLOORS = {
2110
+ attachmentMaxBytes: 1024,
2111
+ attachmentUnboundTtlMs: 60_000,
2112
+ drainGraceMs: 10_000,
2113
+ };
2093
2114
  function parseLimitsHttpDomain() {
2094
2115
  return {
2095
2116
  // #210 A 档:裸 Number ⇒ NaN 直进 `server.listen(NaN)`(Node 当成 0 = 随机端口),于是服务起来了、
@@ -2115,11 +2136,11 @@ function parseLimitsHttpDomain() {
2115
2136
  })(process.env.ATTACHMENT_ORPHAN_GRACE_MS),
2116
2137
  // workspace 浏览面(#3,[1894]①)单文件读上限。
2117
2138
  workspaceFileMaxBytes: optFinitePositiveEnv("WORKSPACE_FILE_MAX_BYTES") ?? 8 * 1024 * 1024,
2118
- attachmentMaxBytes: clampEnvWithWarn("ATTACHMENT_MAX_BYTES", String(32 * 1024 * 1024), 1024), // #210 B 档
2139
+ attachmentMaxBytes: clampEnvWithWarn("ATTACHMENT_MAX_BYTES", String(32 * 1024 * 1024), LIMITS_HTTP_FLOORS.attachmentMaxBytes), // #210 B 档
2119
2140
  ...(process.env.ATTACHMENT_MIME_ALLOWLIST
2120
2141
  ? { attachmentMimeAllowlist: process.env.ATTACHMENT_MIME_ALLOWLIST.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean) }
2121
2142
  : {}),
2122
- attachmentUnboundTtlMs: clampEnvWithWarn("ATTACHMENT_UNBOUND_TTL_MS", String(24 * 3600 * 1000), 60_000), // #210 B 档
2143
+ attachmentUnboundTtlMs: clampEnvWithWarn("ATTACHMENT_UNBOUND_TTL_MS", String(24 * 3600 * 1000), LIMITS_HTTP_FLOORS.attachmentUnboundTtlMs), // #210 B 档
2123
2144
  infraCostRates: {
2124
2145
  // #210 B 档:负单价夹到 0 是对的(负成本会让账面倒扣),但夹取必须点名——不然 operator 只看到
2125
2146
  // 基建成本恒 0,查不到是自己那个负号被吃掉了。
@@ -2135,7 +2156,7 @@ function parseLimitsHttpDomain() {
2135
2156
  return v;
2136
2157
  })(),
2137
2158
  },
2138
- drainGraceMs: clampEnvWithWarn("DRAIN_GRACE_MS", String(10 * 60 * 1000), 10_000), // SIGTERM drain window, default 10min, floor 10s(#210 B 档:把 600 当成"秒"填进来的手滑最常见,夹取后必须点名)
2159
+ drainGraceMs: clampEnvWithWarn("DRAIN_GRACE_MS", String(10 * 60 * 1000), LIMITS_HTTP_FLOORS.drainGraceMs), // SIGTERM drain window, default 10min, floor 10s(#210 B 档:把 600 当成"秒"填进来的手滑最常见,夹取后必须点名)
2139
2160
  sighupIdleGraceMs: clampEnvWithWarn("SIGHUP_IDLE_GRACE_MS", String(120_000), 5_000), // B3: SIGHUP idle-shutdown window, default 2min, floor 5s
2140
2161
  ...parentPidEnv(), // #219:壳 pid 存活监视,缺席 ⇒ 键缺席 ⇒ 整件不装配
2141
2162
  // BL-9: numEnv (not bare Number) — a non-numeric typo FAILS at startup instead of becoming NaN, which would
@@ -2375,7 +2396,7 @@ const ORCHESTRATION_GROUP_KEYS = [
2375
2396
  "schedulerEnabled", "schedulerSessionWakeup", "schedulerSessionLifetime", "schedulerStorePath", "planModeEnabled", "workflowRunStoreBackend",
2376
2397
  "workflowOrphanGraceMs", "workflowJournalRetentionMs", "workflowRunRetentionMs", "usageWindows", "workflowAgentsReadOnly",
2377
2398
  "workflowSizeGuideline", "backgroundAgentRetentionMs", "backgroundAgentStaleRunningMs",
2378
- "backgroundAgentParkClaimStaleMs", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
2399
+ "backgroundAgentParkClaimStaleMs", "delegationEntryCaps", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
2379
2400
  "sessionAutoTitle", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled",
2380
2401
  "imageBakes", "readFace", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
2381
2402
  ];
@@ -2482,8 +2503,12 @@ export function loadConfig() {
2482
2503
  const limitsHttp = parseLimitsHttpDomain();
2483
2504
  const observability = parseObservabilityDomain();
2484
2505
  const integrations = parseIntegrationsDomain();
2485
- // ── 跨域耦合与跨域不变量:两条门(D-G 直连门、bake 门)的判据各自横跨 approval/auth/orchestration 三域,
2506
+ // ── CROSS-INVARIANT-REGION:begin ────────────────────────────────────────────────────────────────
2507
+ // 跨域耦合与跨域不变量:两条门(D-G 直连门、bake 门)的判据各自横跨 approval/auth/orchestration 三域,
2486
2508
  // 任何单域都不足以裁定,故留在装配层(旧名保留,断言与消息逐字不变)。
2509
+ // 🔴 #322:本区内的每一条拒启 throw 必须挂 `CROSS-INVARIANT:<id>` 锚,且 id 在
2510
+ // `src/config-invariants.ts` 的 CONFIG_CROSS_INVARIANTS 表内 —— 配置改热之后,一条**只活在 boot**
2511
+ // 的不变量就是热路径上的静默漏检。机械门:test/config-hot/batch1-cross-invariants.test.ts(双向)。
2487
2512
  const { directApprovalDoor, durableApproval: dgDurable, approvalHmacKeys: dgHmacKeys } = approval;
2488
2513
  const { principalJwtPubkeys, principalJwtIss, principalJwtAud, operatorPrincipals: dgOperators, authTokens } = auth;
2489
2514
  const { enabled: imageBakesEnabled, runnerPrincipal: bakeRunnerPrincipal } = orchestration.imageBakes;
@@ -2513,6 +2538,7 @@ export function loadConfig() {
2513
2538
  if (!requirePrincipal)
2514
2539
  missing.push("REQUIRE_PRINCIPAL=true (a direct door is multi-tenant; without it an absent principal is anonymous and the spoofable header governs owner/quota checks)");
2515
2540
  if (missing.length) {
2541
+ // CROSS-INVARIANT:direct-door-anchors
2516
2542
  throw new Error(`DIRECT_APPROVAL_DOOR=true but D-G anchors are missing: ${missing.join(", ")} — refusing to start a half-open direct door (design/80 D-G boot invariant)`);
2517
2543
  }
2518
2544
  }
@@ -2526,12 +2552,14 @@ export function loadConfig() {
2526
2552
  // single-tenant legacy semantic (REQUIRE_PRINCIPAL unset — the shared service token IS the operator boundary)
2527
2553
  // is deliberately preserved: only the multi-tenant form is a hole. Refuse to start it.
2528
2554
  if (dgDurable && requirePrincipal && dgOperators.length === 0) {
2555
+ // CROSS-INVARIANT:operator-principals-required
2529
2556
  throw new Error("DURABLE_APPROVAL=true + REQUIRE_PRINCIPAL=true but OPERATOR_PRINCIPALS is empty — on a multi-tenant worker the empty operator set makes isOperator() true-for-all, so every authenticated tenant could read every other tenant's pending-approval queue (GET /v1/approvals & siblings). Set OPERATOR_PRINCIPALS, or unset REQUIRE_PRINCIPAL if this is a single-tenant deployment (#157-① boot invariant)");
2530
2557
  }
2531
2558
  // 🔴 boot invariant (IMAGE-API-DESIGN.md §P2.4a): the bake door is build-host-RCE-capable, and isOperator([],p)
2532
2559
  // is true-for-all — so enabling bakes with an empty OPERATOR_PRINCIPALS = a WORLD-WRITABLE RCE door. Refuse to
2533
2560
  // start (never a half-open bake door). The explicit-operator gate in server.ts is the runtime twin of this.
2534
2561
  if (imageBakesEnabled && dgOperators.length === 0) {
2562
+ // CROSS-INVARIANT:bake-operator-principals
2535
2563
  throw new Error("IMAGE_BAKES_ENABLED=true but OPERATOR_PRINCIPALS is empty — the bake door runs build.sh on the privileged build host; an empty operator set makes isOperator() true-for-all (world-writable RCE). Set OPERATOR_PRINCIPALS or disable bakes (IMAGE-API-DESIGN.md §P2.4a boot invariant)");
2536
2564
  }
2537
2565
  // 🔴 boot invariant (IMAGE-API-DESIGN.md §P2.12 / H1): the bake-runner identity is DERIVED from its bearer
@@ -2539,6 +2567,7 @@ export function loadConfig() {
2539
2567
  // system == BAKE_RUNNER_PRINCIPAL MUST exist, else the runner can never satisfy isRunner and the entire
2540
2568
  // claim/ingest/heartbeat pipeline is silently dead. Refuse to start a bake door with no runner credential.
2541
2569
  if (imageBakesEnabled && !Object.values(authTokens).includes(bakeRunnerPrincipal)) {
2570
+ // CROSS-INVARIANT:bake-runner-credential
2542
2571
  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)`);
2543
2572
  }
2544
2573
  // 🔴 boot invariant #193 车4 件2(台账 P1-10 / docs/DESIGN-193-CAR4-leader-failclosed.md 件2):leader
@@ -2552,6 +2581,7 @@ export function loadConfig() {
2552
2581
  // 竞态写 git 状态:不是报错,是**静默数据损坏**。真修(provisionWorker 给每个 worker 传唯一 workspace)
2553
2582
  // 不在本车;门先行,消灭「启用即损坏」的窗口。
2554
2583
  if (orchestration.leaderEnabled && orchestration.remoteExec?.provider === "host") {
2584
+ // CROSS-INVARIANT:leader-host-lane
2555
2585
  throw new Error("LEADER_ENABLED=true + REMOTE_EXEC=host is refused: the leader wire pins ONE absolute workspace path " +
2556
2586
  "(LEADER_WORKSPACE / the /home/user default) for every sub-worker, while the host adapter gives each env a " +
2557
2587
  "DIFFERENT random directory — so N concurrent host workers would share one physical git worktree and " +
@@ -2564,12 +2594,14 @@ export function loadConfig() {
2564
2594
  // 「多副本」这件事在 config 层判不了(副本数不是本进程的知识),可判的**代理**就是 durable 前提本身:
2565
2595
  // 一个没有外部 SQL 店的部署,与 leader run(N worker、最长 24h、真 push)的语义结构性不相容。
2566
2596
  if (orchestration.leaderEnabled && store.dbBackend !== "mysql" && store.dbBackend !== "pg") {
2597
+ // CROSS-INVARIANT:leader-durable-store
2567
2598
  throw new Error(`LEADER_ENABLED=true but DB_BACKEND=${store.dbBackend} (no external SQL store) is refused: a leader run is an ` +
2568
2599
  "N-worker fan-out lasting up to 24h that ends in a REAL git push, and without a durable store its registry is " +
2569
2600
  "a per-process Map — a cross-replica GET 404s, a restart forgets the run, and a retried POST starts a SECOND " +
2570
2601
  "push. Point the service at a durable store (DB_BACKEND=mysql|pg), or turn the leader off (unset LEADER_ENABLED) " +
2571
2602
  "(#193 车4 件2 / staleness ledger P1-9+P1-10 boot invariant)");
2572
2603
  }
2604
+ // ── CROSS-INVARIANT-REGION:end ──────────────────────────────────────────────────────────────────
2573
2605
  // #157 F 类观测:执行车道落点记账放在这里 —— 九域解析与上面全部跨域不变量都过了之后,即「这一轮
2574
2606
  // 确实启得起来」才成立的位置(写在 REMOTE_EXEC 解析途中的话,后置拒启会留下一条没启起来的落点)。
2575
2607
  EXEC_LANE_NOTICE = buildExecLaneNotice(orchestration.remoteExec);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * #322 批1 追加件(clay 当场裁定,design/322 批1 行):`POST /v1/admin/config/refresh` —— operator 改完
3
+ * 配置**不等 60s tick**,手动触发一次立即拉取。
4
+ *
5
+ * 族属 = `POST /v1/admin/drain` 逐字同族(operator lane:同一条 `explicitOperatorOk` 门、billable=false、
6
+ * 刻意不吃 drain / roster-pending 两道 503 —— 运维在停机窗里也要打得通自己的口)。门序照那边:
7
+ * 身份 → 授权 →(本口无 body,无验型步)。
8
+ *
9
+ * 🔴 语义 = **触发一次既有的 refresh 拍**,不是「再实现一遍拉取」:实现面是
10
+ * `configCenter.refreshNow()`,它与 60s cadence 共用同一只 join 形拍(在飞则 join,绝不并发双拍)。
11
+ * 回执里的两个世代号取自**同一本**世代账(`/health` 的三键同源)—— 两面各算一次迟早会各说各话。
12
+ *
13
+ * 🔴 没有配置管道(纯 env 形)⇒ 诚实 409 指路,**不吞成 200**:一个「triggered:true 但什么都没发生」的
14
+ * 回执会让运维以为配置已经拉过了,那比拒绝糟得多。
15
+ */
16
+ import type { IncomingMessage, ServerResponse } from "node:http";
17
+ import type { RouteCtx } from "../route-ctx.js";
18
+ export declare const ADMIN_CONFIG_REFRESH_PATH = "/v1/admin/config/refresh";
19
+ export declare function handleAdminConfigRefresh(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
20
+ //# sourceMappingURL=admin-config-refresh.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { sendJson, sendError } from "../send.js";
2
+ import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
3
+ export const ADMIN_CONFIG_REFRESH_PATH = "/v1/admin/config/refresh";
4
+ const OPERATOR_ONLY_MESSAGE = "config refresh is operator-only: the caller principal is not in OPERATOR_PRINCIPALS (an empty list means NO one is an operator — configure it to enable this endpoint)";
5
+ export async function handleAdminConfigRefresh(req, res, url, ctx) {
6
+ const miss = { fell: false };
7
+ await handleAdminConfigRefreshBody(req, res, url, ctx, miss);
8
+ return !miss.fell;
9
+ }
10
+ async function handleAdminConfigRefreshBody(req, res, url, ctx, miss) {
11
+ const { deps } = ctx;
12
+ const path = url.split("?")[0] ?? url;
13
+ if (req.method !== "POST" || path !== ADMIN_CONFIG_REFRESH_PATH) {
14
+ miss.fell = true;
15
+ return;
16
+ }
17
+ const principal = gatedPrincipal(req, deps.config);
18
+ if (deps.config.requirePrincipal && !principal) {
19
+ sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
20
+ return;
21
+ }
22
+ if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
23
+ sendError(res, 403, "auth.operator_only", OPERATOR_ONLY_MESSAGE);
24
+ return;
25
+ }
26
+ const trigger = deps.configRefreshNow;
27
+ const outcome = trigger ? await trigger() : "not-wired";
28
+ if (outcome === "failed") {
29
+ // 🔴 codex R2-F3:定时拍吞掉异常是为了保住循环,但**按下刷新的那个人**必须知道这一拍真的失败了
30
+ // ——「中心超时」与「已刷新」在回执上不可分辨,正是这个口存在的反面。502:失败在依赖侧(配置源)。
31
+ sendError(res, 502, "internal.config_refresh_failed", "the config refresh tick failed (source unreachable / unreadable / apply error) — the live config is unchanged; see the worker's sema_registry_refresh_failed log line for the cause");
32
+ return;
33
+ }
34
+ if (outcome === "not-wired") {
35
+ sendError(res, 409, "config.refresh_unavailable", "this deployment has no config pipeline to refresh — set CONFIG_CENTER (remote control plane) or CONFIG_PROVIDER=local (config.d on disk); an env-only worker has nothing to pull");
36
+ return;
37
+ }
38
+ // 世代号取自世代账(与 /health 的 configTargetVersion/configAppliedVersion 同一份读数)。账缺席只发生在
39
+ // 「触发口在场但世代账没接线」的夹具形 —— 诚实报 0(不编一个看起来像真答案的数)。
40
+ const gen = deps.configApplyState?.();
41
+ sendJson(res, 200, { triggered: true, targetVersion: gen?.configTargetVersion ?? 0, appliedVersion: gen?.configAppliedVersion ?? 0 });
42
+ }
43
+ //# sourceMappingURL=admin-config-refresh.js.map
@@ -162,6 +162,10 @@ async function handleDiagnosticsBody(req, res, url, ctx, miss) {
162
162
  // 同受众(operator-only,门在上方,本段不自带更松的门)。
163
163
  // 缺席 ⇒ 显式 `null`(「这个进程说不出来」),不铸一个看起来像真答案的空壳对象。
164
164
  memoryPosture: deps.memoryPosture ?? null,
165
+ // #322 世代账的 operator 面:按组的 appliedVersion / deferredKeys+铸因 / lastRejected(消毒形——
166
+ // 只有键名与判据文案,值从不进读面)。/health 那三键是同一份读数的窄投影,两面不可能各说各话。
167
+ // 缺席 ⇒ 显式 `null`(与 memoryPosture 同姿势:「这个进程说不出来」,不铸空壳)。
168
+ configApply: deps.configApplyState?.() ?? null,
165
169
  serverGates: buildServerWiringGates({
166
170
  toolApprovalEnabled: deps.toolApproval !== undefined,
167
171
  streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
@@ -1396,19 +1396,23 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1396
1396
  // [1596]/[1600] core 1.397 三分:仲裁器**抛错**(店不可达)此前被误吞成 park_resume_won 的假话——
1397
1397
  // 「输给了并发 resume」与「根本问不到仲裁结果」是两种不同的行动建议(前者重查状态,后者原样重试
1398
1398
  // stop),新码 `park_arbiter_unreachable` 让二者不再共用一个 errorCode。
1399
- const parkArbiterUnreachable = details?.error === "park_arbiter_unreachable";
1400
- const parkResumeWon = details?.error === "park_resume_won";
1401
- const stillParked = details?.error === "parked_pending_approval" || details?.status === "parked";
1399
+ // [4797](core 5.50)details.code 孪生对表:stop 族座位今天仍只铸 `error`(族扫未及,已上板点名),
1400
+ // core 契约方向是「code wins when both present」且未承诺新失败座位必带 error 判类读
1401
+ // code-优先-error-回落、在场判据双键取并,未来只铸 code 的失败座位不会漏判成 200 成功。
1402
+ const stopCode = typeof details?.code === "string" ? details.code : details?.error;
1403
+ const parkArbiterUnreachable = stopCode === "park_arbiter_unreachable";
1404
+ const parkResumeWon = stopCode === "park_resume_won";
1405
+ const stillParked = stopCode === "parked_pending_approval" || details?.status === "parked";
1402
1406
  // v3.1 批2(统检第二波 high):判别方向翻转——旧形是 (status,error) 的**失败白名单**(status==="running"
1403
1407
  // || 三个具名码),core 新增失败码且行状态不在 {running,parked} 时整个分支被跳过落 200,与上面注释自陈
1404
1408
  // 的不变量(kill 没落地绝不读成功)正相悖(B5 联合非穷尽的 default-放行形;not_local/park 族/arbiter
1405
1409
  // 三次历史新增全靠人追认)。新形:stop 动词的 error **在场即失败**(not_found 已在上方早退),具名臂
1406
1410
  // 保留各自 errorCode,未知码落 stop.not_landed 兜底 409——新失败码默认可见,不默认成功。
1407
- if (stopVerb && details?.error !== undefined) {
1411
+ if (stopVerb && (details?.error !== undefined || details?.code !== undefined)) {
1408
1412
  // 1.250:durable 回落臂(core stopTask agentStore 分支)对他实例 running 行应答 error="not_local"
1409
1413
  // (没有 kill 被尝试)——与「kill 尝试了没落地」(stop.not_landed)是不同的失败形,各给各的
1410
1414
  // errorCode(同 409 家族,additive)。
1411
- const notLocal = details.error === "not_local";
1415
+ const notLocal = stopCode === "not_local";
1412
1416
  sendError(res, 409, notLocal
1413
1417
  ? "stop.not_local"
1414
1418
  : parkArbiterUnreachable
@@ -1425,7 +1429,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
1425
1429
  ? "the pending tool-approval was resolved (resumed) concurrently with this stop attempt — the resume won the race, so nothing was stopped; re-check the task's current status"
1426
1430
  : stillParked
1427
1431
  ? "this task is durably parked awaiting a tool-approval decision, not actively running — there is no live process to interrupt; resolve (or let expire) the pending approval instead"
1428
- : `stop did not land (${details.error}) — the process may still be running`, { taskId: runId, target, content: out.content, output: out.details });
1432
+ : `stop did not land (${stopCode}) — the process may still be running`, { taskId: runId, target, content: out.content, output: out.details });
1429
1433
  return;
1430
1434
  }
1431
1435
  // Registry projection passed through verbatim (status/retrieval_status/cursor flags); `content` is
@@ -455,6 +455,14 @@ export interface ServiceDeploymentDeps {
455
455
  * auto rolling-restart the worker. Change-detected (not presence) — absent when nothing baked-at-boot
456
456
  * changed, so polling it never causes a restart loop. */
457
457
  restartState?: () => RestartSignal | undefined;
458
+ /** #322 世代账(design/322 §2):本副本的配置收敛状态 —— `/health` 取三键
459
+ * (configTargetVersion / configAppliedVersion / configApplyStaleMs),`GET /v1/diagnostics/wiring` 取整份
460
+ * (按组的 appliedVersion / deferredKeys+铸因 / lastRejected 消毒形)。缺席 = 这个进程没有配置管道
461
+ * (纯 env 部署 / 测试夹具)⇒ 两个面的形状逐字不变(additive)。 */
462
+ configApplyState?: () => import("../config-center/apply-ledger.js").ConfigApplyLedgerView | undefined;
463
+ /** #322 追加件:`POST /v1/admin/config/refresh` 的实现面 —— 触发**一次既有**的 refresh 拍(在飞则 join)。
464
+ * 缺席 / 回 `"not-wired"` = 本部署没有配置管道 ⇒ 该口诚实 4xx(绝不吞成 200)。 */
465
+ configRefreshNow?: () => Promise<"triggered" | "not-wired" | "failed">;
458
466
  /** codex R18 (models-tiers deferral): the persistent "deferred WITHOUT a next-boot handoff" state — set when a
459
467
  * changed model plane could not hot-apply (tier-frozen Runner / tiered candidate) AND no restart-surviving LKG
460
468
  * exists to justify forcing a restart. Surfaced on /health (`modelPlaneDeferred`) so the stuck-degraded state
@@ -44,6 +44,7 @@ import { handleDiagnostics } from "./routes/diagnostics.js";
44
44
  import { handleAdoption } from "./routes/adoption.js";
45
45
  import { handleRetention } from "./routes/retention-ops.js";
46
46
  import { handleAdminDrain } from "./routes/admin-drain.js";
47
+ import { handleAdminConfigRefresh } from "./routes/admin-config-refresh.js"; // #322 批1 追加件:配置手动刷新口(operator lane)
47
48
  import { handleMemoryPolicy } from "./routes/memory-policy.js";
48
49
  import { handleMemoryBundle } from "./routes/memory-bundle.js";
49
50
  import { handleSharedMemory } from "./routes/shared-memory.js";
@@ -218,6 +219,7 @@ const ROUTE_DOMAINS = [
218
219
  handleAdoption,
219
220
  handleRetention,
220
221
  handleAdminDrain,
222
+ handleAdminConfigRefresh,
221
223
  handleMemoryPolicy,
222
224
  handleMemoryBundle,
223
225
  handleRules,
@@ -494,6 +496,23 @@ export function createHttpServer(rawDeps) {
494
496
  const stuck = deps.planeDeferredState?.();
495
497
  return stuck ? { modelPlaneDeferred: { version: stuck.version, since: stuck.since, noHandoff: true, ...(stuck.blocked ? { blockedReasons: stuck.blocked } : {}) } } : {};
496
498
  })(),
499
+ // #322 世代账三键(additive;未接线 ⇒ 键全缺席,/health 形状逐字不变)。
500
+ // 语义:target = 本副本**最后**见到的源版本(身份标);applied = 最后一次真的 commit 的版本;
501
+ // staleMs = 两者**持续**不等的时长(收敛即缺席)。#325 起 additive 补两 ordinal 键(收货序数,
502
+ // 进程内单调)——local 腿 version 是内容哈希无序承诺,「见过多少代/停在哪一代」的推进判断
503
+ // 用 ordinal 键;跨副本「同不同代」的对账仍用 version 身份标。
504
+ ...(() => {
505
+ const gen = deps.configApplyState?.();
506
+ if (!gen)
507
+ return {};
508
+ return {
509
+ configTargetVersion: gen.configTargetVersion,
510
+ configAppliedVersion: gen.configAppliedVersion,
511
+ configTargetOrdinal: gen.configTargetOrdinal,
512
+ configAppliedOrdinal: gen.configAppliedOrdinal,
513
+ ...(gen.configApplyStaleMs !== undefined ? { configApplyStaleMs: gen.configApplyStaleMs } : {}),
514
+ };
515
+ })(),
497
516
  // B1 活体 store 探测(座注见 storeLiveState 声明;钉 test/health-store-live.test.ts)
498
517
  ...(() => {
499
518
  const probe = deps.storeLiveState?.();
@@ -717,6 +736,10 @@ export function createHttpServer(rawDeps) {
717
736
  // 同族(operator 门、billable=false、不吃 drain 503——它正是 drain 前要调的口),位置跟着 retention。
718
737
  if (await handleAdminDrain(req, res, url, ctx))
719
738
  return;
739
+ // #322 批1 追加件:配置手动刷新口(routes/admin-config-refresh.ts,operator lane;POST
740
+ // /v1/admin/config/refresh)。与 drain 同族同门同 billable 姿势,位置紧跟它。
741
+ if (await handleAdminConfigRefresh(req, res, url, ctx))
742
+ return;
720
743
  // design/158 A9:memory 导出/同步 + /v1/policy 只读面(routes/memory-policy.ts)。
721
744
  if (await handleMemoryPolicy(req, res, url, ctx))
722
745
  return;
@@ -3496,6 +3519,9 @@ const ROUTE_LABEL_LITERALS = new Set([
3496
3519
  "/v1/adoption",
3497
3520
  // #291:停机因由声明口(operator-only,drain 前一次性动作)。同 adoption 判:低频但只在停机现场被打。
3498
3521
  "/v1/admin/drain",
3522
+ // #322 批1 追加件:配置手动刷新口(operator-only)。同判:低频,但它的 duration 与失败率正是「这次
3523
+ // 配置到底拉没拉下来」的第一手信号,落进 other 桶就看不见了。同族纪律:写**字面量**不引常量。
3524
+ "/v1/admin/config/refresh",
3499
3525
  // #154 车二:CC settings 导入两口。字面量(无 id 段)⇒ 进 LITERALS 表而不是模式表。
3500
3526
  // 🔴 写**字面量**而不是引 `RULES_CC_IMPORT_*_PATH` 常量:名册门与 billable 申明门都是**扫源码文本**
3501
3527
  // 的(它们正则抓本表里的双引号词),引常量会让两条新路由对两道门隐形 —— 那不是「过了门」,是绕过门。