@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
package/USAGE.md CHANGED
@@ -680,7 +680,8 @@ curl -N http://<host>:8090/v1/tasks/stream -H 'content-type: application/json' \
680
680
  | `GET /v1/agents/roster` | 后台 agent **名册**(7.29 起,需 durable background-agent store,否则 501 `capability.background_agent_store_required`):调用方自己 scope 的 `a…` 行,新→旧。行只带 `handle`/`name`/`agentType`/`status`/`spawnedAt`/`updatedAt`/`settledAt`(content-free;子代产物正文走 `GET /v1/runs/<id>/subagents/<handle>/output`)。查询:`?limit=`(1..100,默认 20)、`?before=`(上一页的 `nextBefore` 游标)、`?status=`(running\|parked\|completed\|failed\|killed)、`?session=`(按会话树窄化)、`?scope=`(**仅** service-token/单用户形;带 principal 的调用方钉死自己)。回体 `{agents,total,nextBefore?,truncated?}`;`truncated:true` = 租户行数超过名册窗(500),`total` 此时是窗内数。非属主的行**不出现也不计数**(无存在性 oracle) |
681
681
  | `GET /v1/capabilities/scenarios/<name>` | 单个场景的只读详情:工具面、提示词概览、**本部署现在跑不跑得动**(见下) |
682
682
  | `POST /v1/admin/drain` `{"reason":"…"}` | **停机因由显式声明**(7.35.0+;operator-only —— principal 须在 `OPERATOR_PRINCIPALS` 内,空名单 = 谁都不是 operator ⇒ 恒 403)。编排壳在发 SIGTERM **之前**调它;server 记进程内 `drainState.reason`,draining 期由 `/health` 的 `drainReason` 与 503 `draining` 体的 `reason` 透出(未声明 = 两面键缺席)。声明是**覆盖式**、无 TTL、随进程重启清零 ⇒ 每次停机流程内都要重新声明。`reason` 非空、trim 后 ≤256 字符,坏值 400。🔴 **写口是特权的,读口不是**:`/health` 免鉴权(见下一行)且缺省全网卡监听 —— 任何能连到这个端口的人都读得到你写的那句话。**别在 reason 里写工单号、内部主机名或任何内部标识**。 |
683
- | `GET /health` | 健康(无需鉴权) |
683
+ | `POST /v1/admin/config/refresh` | **配置手动刷新**(#322 批1;operator-only,与上一行同门:空 `OPERATOR_PRINCIPALS` ⇒ 恒 403)。改完中心配置 / `config.d` 之后不想等 60s 轮询就调它:触发**一次既有**的 refresh 拍(在飞则汇入那一拍,绝不并发双拍),返回时该拍已落地。200 体 `{triggered:true,targetVersion,appliedVersion}` —— 两个世代号与 `/health` 的 `configTargetVersion`/`configAppliedVersion` 同源。本部署没有配置管道(纯 env worker:既没配 `CONFIG_CENTER`、也不是 `CONFIG_PROVIDER=local`)⇒ 409 `config.refresh_unavailable` 指路,不会回一个「触发了但什么都没发生」的 200。 |
684
+ | `GET /health` | 健康(无需鉴权)。7.37.0+ 配置管道在场时带世代账键:`configTargetVersion`(本副本**最后见到**的配置版本,身份标)、`configAppliedVersion`(最后一次真落地的版本)、`configTargetOrdinal`/`configAppliedOrdinal`(7.38.0-rc.2 起,**收货序数**——`CONFIG_PROVIDER=local` 下 version 是内容哈希不承诺序,「改了没生效」看序数对不对齐)、`configApplyStaleMs`(target≠applied **持续**时长;收敛时键缺席)—— 编排器据此摘掉「配置持续落后」的副本 |
684
685
  | `GET /metrics` | Prometheus 指标(有 token 时需带) |
685
686
 
686
687
  > **场景可用性(7.7.0 起)**:场景详情里 `enabled` 与 `available` 是**两件事**。`enabled` = 这条场景
@@ -52,6 +52,22 @@ export const REBIND_LEGS = [
52
52
  residualKey: undefined, // PK(session_id) 不含 owner
53
53
  why: "会话归属主列(183 §4.3 形Ⅰ 首项;存量 `anon:shell-live` 哨兵行同走此腿——它只是一个 principal 字面量,不需要 ad hoc 分支)",
54
54
  },
55
+ {
56
+ // #292(core 5.48 placement 分区,身份轴普查门在合并树首跑即逮到——门的存在理由):core 契约明写
57
+ // placement.scope = "Tenant key, same source as the durable agent row's scope" ⇒ 形Ⅱ(scope ≡
58
+ // principal 分区面),与 background_agent.scope 同判。**活的可见性轴**:listPlaced 按它过滤
59
+ // (pg/tidb 店的 WHERE placement_kind+placement_scope),不迁则收编后子代转录对新身份枚举不到、
60
+ // 对旧身份被 owner 门拒 = 双盲。placement_scope 只在 placed 行非 NULL,谓词天然只碰 placement 分区。
61
+ leg: "session_meta#placement_scope",
62
+ table: "session_meta",
63
+ kind: "bulk-rebind",
64
+ action: "row-rewrite",
65
+ columns: ["placement_scope"],
66
+ matchColumn: "placement_scope",
67
+ encoding: "verbatim",
68
+ residualKey: undefined, // PK(session_id) 不含它;idx_session_meta_placement 非唯一
69
+ why: "placement 分区键(core 5.48 SessionPlacement.scope=tenant key,与 durable agent 行 scope 同源)——子代转录的枚举可见性轴,不迁=收编后 listPlaced 对新旧身份双盲",
70
+ },
55
71
  {
56
72
  leg: "task_run#owner",
57
73
  table: "task_run",
@@ -22,7 +22,7 @@ export declare function createBudgetAndTracing(ctx: BudgetTracingCtx): {
22
22
  prevStreak: number;
23
23
  error?: string;
24
24
  }) => void;
25
- costQuota: CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota | undefined;
25
+ costQuota: CostQuota | import("../plugins/pg-cost-quota.js").PgCostQuota | import("../plugins/tidb-cost-quota.js").TiDBCostQuota;
26
26
  modelUsageTracker: ModelUsageTracker;
27
27
  promptManifestTracker: PromptManifestTracker;
28
28
  fleetUsage: FleetUsageAccumulator | undefined;
@@ -13,6 +13,7 @@ import { ModelUsageTracker, PromptManifestTracker, buildPricing, createSideQuery
13
13
  import { FleetUsageAccumulator } from "../fleet-client.js";
14
14
  import { createFleetLeaseFromEnv } from "../fleet-lease.js";
15
15
  import { CostQuota } from "../observability/cost-quota.js";
16
+ import { buildQuotaBounds } from "./limit-sync.js"; // #322:USD/秒 → micro/ms 的单一换算点(装配与热同步共用)
16
17
  export function createBudgetAndTracing(ctx) {
17
18
  const { config, logger, metrics, backend, breakerState } = ctx;
18
19
  const brain = createBrain(config, { breakerState });
@@ -34,11 +35,17 @@ export function createBudgetAndTracing(ctx) {
34
35
  };
35
36
  // Per-principal cumulative cost quota: TiDB-backed (cross-replica, atomic write-behind) when a pool
36
37
  // exists, else in-memory per-replica (single-replica / no DB). Both satisfy QuotaTracker. (design/30 P0-1)
37
- const costQuota = config.maxPrincipalCostUsd > 0
38
- ? backend?.costQuota // local omits costQuota() in-memory per-process CostQuota (single-replica)
39
- ? backend.costQuota(Math.round(config.maxPrincipalCostUsd * 1e6), config.costQuotaWindowSec * 1000, counterDegradeHook).startRefresh()
40
- : new CostQuota(Math.round(config.maxPrincipalCostUsd * 1e6), config.costQuotaWindowSec * 1000)
41
- : undefined;
38
+ //
39
+ // 🔴 #322 批1(codex 设计审 F1,恒构造 + 关断哨兵):修前这里是 `maxPrincipalCostUsd > 0 ? … : undefined`,
40
+ // 而该键**默认 0** 默认部署根本没有实例。把配额改热之后,0→N 的一次发布会「记账上显示已生效、实际
41
+ // 没有任何 ceiling」—— 经济轴上的静默 fail-open。改为 **boot 恒构造**:0 由座位自己的关断哨兵表达
42
+ // (恒放行、不记账;SQL 腿连刷新环都不起 ⇒ off 态零 SQL),于是 0↔N 从「拓扑翻转」收敛成纯值变,
43
+ // 热同步(boot/limit-sync.ts)一句 setLimits 就够。`startRefresh()` 照常调:它现在记的是**运维意图**,
44
+ // 真起不起环由哨兵判(见 TiDBCostQuota.syncRefresh)。
45
+ const bounds = buildQuotaBounds(config);
46
+ const costQuota = backend?.costQuota // local omits costQuota() → in-memory per-process CostQuota (single-replica)
47
+ ? backend.costQuota(bounds.limitMicroUsd, bounds.windowMs, counterDegradeHook).startRefresh()
48
+ : new CostQuota(bounds.limitMicroUsd, bounds.windowMs);
42
49
  // E8 (shell-host contract): per-task × per-model usage accumulator, fed by the tracer and drained by the durable run
43
50
  // lifecycle into `model_usage` events (the `TaskStats.modelUsage` echo). One instance, shared tracer↔server.
44
51
  const modelUsageTracker = new ModelUsageTracker();
@@ -1,4 +1,4 @@
1
- import { CenterPromptSource } from "@sema-agent/core";
1
+ import { CenterPromptSource, type Model } from "@sema-agent/core";
2
2
  import { buildPricing } from "../budget.js";
3
3
  import { type PromptsDomainFaces } from "../prompts-domain-validate.js";
4
4
  import { type Scenario, type ScenarioDeps, type ScenarioDetail } from "../capabilities/scenarios.js";
@@ -8,6 +8,8 @@ import { type ModelKeyRef } from "../key-resolver.js";
8
8
  import type { Logger } from "../observability/logger.js";
9
9
  import type { Metrics } from "../observability/metrics.js";
10
10
  import { type RestartSignal } from "../config-center/facade.js";
11
+ import { type ConfigApplyLedgerView } from "../config-center/apply-ledger.js";
12
+ import type { LimitSync } from "./limit-sync.js";
11
13
  export interface ConfigCenterRuntimeCtx {
12
14
  config: ServiceConfig;
13
15
  logger: Logger;
@@ -37,10 +39,19 @@ export interface ConfigCenterRuntime {
37
39
  applyCenterScenarios(t: CenterScenarioOverlayTargets): void;
38
40
  /** per-model key resolver 首建(原位调用,见文件头注 5)。 */
39
41
  initKeyResolver(): void;
40
- /** 60s refresh cadence + boot-deferred 到货续接。 */
42
+ /** 60s refresh cadence + boot-deferred 到货续接。
43
+ * `limitSync`(#322 批1)= 恒构造的两只限额座的换代口:commit 之后**零 await 内**推一把,与 pricing /
44
+ * keyResolver 那两件派生重建同族(缺席 = 这条腿本部署没装配,例如 run-local)。
45
+ * `swapRunnerModels`(core 5.49,[4775]①)= 模型面永久热的换装口:plane 变更候选在 commit 前喂给
46
+ * 全部 boot Runner 原子换代;在场 ⇒ models-tiers defer/restart 臂退役,缺席 ⇒ 旧 defer 行为逐字保留。 */
41
47
  startRefreshLoop(a: {
42
48
  runnerTierFrozen: boolean;
43
49
  pricing: ReturnType<typeof buildPricing>;
50
+ limitSync?: LimitSync;
51
+ swapRunnerModels?: (plane: {
52
+ models: Record<string, Model>;
53
+ tiers: Record<string, string>;
54
+ }) => void;
44
55
  }): void;
45
56
  /** #131-2:停刷新环(hardShutdown 收尾链;幂等,未起环时 no-op)。 */
46
57
  stopRefreshLoop(): void;
@@ -61,6 +72,20 @@ export interface ConfigCenterRuntime {
61
72
  since: number;
62
73
  blocked?: string[];
63
74
  } | undefined;
75
+ /** #322 世代账:/health 的 target/applied/失败持续时长三键 + 诊断端点的 configApply 段(同一份读数)。
76
+ * `undefined` = 本部署没有配置管道(纯 env worker)⇒ 两个读面的形状**逐字不变**(additive 纪律)。 */
77
+ configApplyState(): ConfigApplyLedgerView | undefined;
78
+ /**
79
+ * #322 追加件:手动触发**一次既有** refresh 拍(`POST /v1/admin/config/refresh` 的实现面)。
80
+ *
81
+ * 一拍在飞 ⇒ **join** 它(并发调用共享同一拍,绝不并发双拍、绝不另铸拉取路径);返回时该拍已落地,
82
+ * 所以调用方紧接着读 `configApplyState()` 就是本次触发的结果。
83
+ * `"not-wired"` = 本部署没有配置管道(纯 env 形)或刷新环还没起 —— 端点据此诚实 4xx,不吞成 200。
84
+ * `"failed"` = 这一拍真的失败了(中心超时 / 盘读不了 / 应用抛错)—— 端点据此 5xx。定时拍照旧吞掉异常
85
+ * 保住循环,但**吞掉的事实必须传给按下刷新的那个人**(codex R2-F3:否则「中心超时」与「已刷新」在
86
+ * 回执上不可分辨,这个口的「诚实」二字就是假的)。
87
+ */
88
+ refreshNow(): Promise<"triggered" | "not-wired" | "failed">;
64
89
  }
65
90
  export declare function createConfigCenterRuntime(ctx: ConfigCenterRuntimeCtx): Promise<ConfigCenterRuntime>;
66
91
  //# sourceMappingURL=config-center.d.ts.map
@@ -48,6 +48,7 @@ import { createKeyResolver } from "../key-resolver.js";
48
48
  import { ensureSealedKeyStore, reportExecutionPublicKey } from "../sealed-key.js";
49
49
  import { applyEffective, applyCenterReadFace, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, resolveA2aPeers, restartReasons, modelPlaneChanged, planeHasActiveTiers, fetchPromptArtifact } from "../config-center/facade.js";
50
50
  import { SHA256_HEX_RE } from "../digest-form.js";
51
+ import { createConfigApplyLedger } from "../config-center/apply-ledger.js"; // #322 世代账
51
52
  export async function createConfigCenterRuntime(ctx) {
52
53
  const { config, logger, metrics, localRoot } = ctx;
53
54
  // Config-center: pull the effective config on boot and apply it OVER the env
@@ -68,6 +69,25 @@ export async function createConfigCenterRuntime(ctx) {
68
69
  // the prompts-domain validation (core S1 schema) hooks into the same gate.
69
70
  let latestEffective;
70
71
  let lastRejectedCandidate;
72
+ // #322 世代账(§2):主键=上游 version(不造本地 epoch)。boot/refresh/迟到 boot 三条腿共用这一本;
73
+ // 候选被整拒的拍只推进 target ⇒ /health 的 stale 面从结构上可见。
74
+ const applyLedger = createConfigApplyLedger();
75
+ // #325(test [4809],[4810] 阻断裁定):**收货序数**——CAS 与世代账的序轴。source version 在
76
+ // local 腿是内容哈希(registry-core file-store getVersion(),无序承诺),不能当序数比;这里按
77
+ // 收货顺序铸进程内单调 ordinal:version 变即 ++,等版本重放(etag 未推进的下一拍)沿用同
78
+ // ordinal=幂等语义保留。version 仍是跨副本对账的身份标(账里两者双报)。
79
+ let generationOrdinal = 0;
80
+ let lastGenerationVersion;
81
+ const generationOf = (version) => {
82
+ if (version !== lastGenerationVersion) {
83
+ lastGenerationVersion = version;
84
+ generationOrdinal += 1;
85
+ }
86
+ return generationOrdinal;
87
+ };
88
+ // #322 追加件:手动触发口的实现指针 —— `startRefreshLoop` 起环时装上(cadence 与手动口共用的那只
89
+ // join 形拍)。未起环(纯 env 部署 / 还没到装配那一步)⇒ 恒 undefined ⇒ 端点诚实 4xx。
90
+ let manualRefresh;
71
91
  // codex R10 (models-tiers 窗收口): the last EffectiveConfig whose MODEL PLANE (models/roles/tiers/default) was
72
92
  // actually applied to the live config — undefined = env plane (no center plane applied yet). When the Runner is
73
93
  // tier-frozen, a refresh whose plane differs from THIS is deferred to restart (applyEffective deferModelPlane),
@@ -369,7 +389,10 @@ export async function createConfigCenterRuntime(ctx) {
369
389
  return { effective: read.lkg.effective, ...(read.lkg.etag !== undefined ? { etag: read.lkg.etag } : {}) };
370
390
  };
371
391
  const bootApplyLkgInline = async (lkgR) => {
372
- applyEffective(config, lkgR.effective, logger, { sealedKeys });
392
+ const lkgGen = generationOf(lkgR.effective.version);
393
+ applyLedger.seenTarget(lkgGen, lkgR.effective.version);
394
+ applyEffective(config, lkgR.effective, logger, { sealedKeys, generation: lkgGen, report: (rep) => applyLedger.recordApply(rep) });
395
+ applyLedger.applied(lkgGen, lkgR.effective.version);
373
396
  await adoptCenterPrompts(lkgR.effective, "boot-lkg");
374
397
  markRosterLanded(lkgR.effective);
375
398
  effective = lkgR.effective;
@@ -424,16 +447,33 @@ export async function createConfigCenterRuntime(ctx) {
424
447
  const bootPromptsOk = bootPromptsRaw === undefined || validatePromptsDomain(bootPromptsRaw).ok;
425
448
  // 坏 prompts 面同 domainErrors 计入 not-clean(F6 boot 半场):etag 不进 → refresh 首拍同树整拒持续
426
449
  // warn;LKG 不指向带坏 prompts 的候选。adoptCenterPrompts 自身会拒采用+点名。
427
- const bootClean = (r.domainErrors ?? []).length === 0 && bootPromptsOk;
428
- if (bootClean)
429
- ccEtag = r.etag;
450
+ let bootClean = (r.domainErrors ?? []).length === 0 && bootPromptsOk;
430
451
  for (const de of r.domainErrors ?? [])
431
452
  logger.warn("config_domain_invalid", { domain: de.domain, error: de.error, note: "tolerated at BOOT (corrupt file → schema defaults; dangling ref → reported as-is); the refresh path rejects such a candidate whole and keeps the LKG (§9.5-5)" });
432
453
  if (dryRun) {
433
454
  logEffectiveDiff(config, r.effective, logger); // compare-only: log the would-be changes, keep env
455
+ if (bootClean)
456
+ ccEtag = r.etag;
434
457
  }
435
458
  else {
436
- applyEffective(config, r.effective, logger, { sealedKeys }); // sealedKeys: unseal sealedApiKey → in-memory key pipeline
459
+ const bootGen = generationOf(r.effective.version);
460
+ applyLedger.seenTarget(bootGen, r.effective.version);
461
+ let bootReport;
462
+ applyEffective(config, r.effective, logger, {
463
+ sealedKeys,
464
+ generation: bootGen,
465
+ report: (rep) => {
466
+ bootReport = rep;
467
+ applyLedger.recordApply(rep);
468
+ },
469
+ }); // sealedKeys: unseal sealedApiKey → in-memory key pipeline
470
+ applyLedger.applied(bootGen, r.effective.version);
471
+ // codex R2-F1(boot 半场):某组被拒 ⇒ 这棵树**不干净** —— 与 domainErrors/坏 prompts 同处置
472
+ // (etag 不进、不进 LKG 候选),否则一次带坏 limits 的 boot 会把它写成盘上的 LKG。
473
+ if (bootReport?.fullyApplied === false)
474
+ bootClean = false;
475
+ if (bootClean)
476
+ ccEtag = r.etag; // 干净才推进 etag(判定要在 apply 之后——某组被拒也算不干净)
437
477
  await adoptCenterPrompts(r.effective, "boot"); // [1057] additive prompts key(判形采用;缺席=内置)
438
478
  markRosterLanded(r.effective); // boot pull 即落 roster 的常态:门在启动窗内就打开
439
479
  effective = r.effective; // skills/mcp applied below, before buildScenarios (restart-to-apply)
@@ -525,6 +565,12 @@ export async function createConfigCenterRuntime(ctx) {
525
565
  modelReady: () => modelReadyState.ready,
526
566
  restartState: () => pendingRestart,
527
567
  planeDeferredState: () => planeDeferredNoHandoff,
568
+ configApplyState: () => (configProvider ? applyLedger.view() : undefined), // 纯 env 形没有可对账的世代 ⇒ 诚实缺席,不报一对恒 0
569
+ async refreshNow() {
570
+ if (!manualRefresh)
571
+ return "not-wired";
572
+ return (await manualRefresh()) === "failed" ? "failed" : "triggered";
573
+ },
528
574
  async applyLocalRemoteExec() {
529
575
  // DUAL-MODE §4/§5 (center #1): in LOCAL config mode, source remoteExec from the SHARED
530
576
  // `config.d/remote-exec.json` via the registry `loadRemoteExec` — the SAME file + zod validation that the
@@ -685,7 +731,7 @@ export async function createConfigCenterRuntime(ctx) {
685
731
  }
686
732
  },
687
733
  startRefreshLoop(a) {
688
- const { runnerTierFrozen, pricing } = a;
734
+ const { runnerTierFrozen, pricing, limitSync, swapRunnerModels } = a;
689
735
  // Config refresh: re-pull with the ETag; teams hot-reload via the registry. A models/roles
690
736
  // change updates config but the live Runner keeps its startup set — logged so ops restart to apply.
691
737
  // 提示词装配协议 §9.5-1/-3: the loop goes through the ConfigProvider seam (never the raw HTTP fetch —
@@ -700,9 +746,12 @@ export async function createConfigCenterRuntime(ctx) {
700
746
  // the stalled OLDER read then complete and roll the config (and ccEtag) back. Skip the tick while one
701
747
  // is in flight; the flag flips in `finally`, so a throw never wedges the loop shut.
702
748
  let refreshInFlight = false;
749
+ // #322 追加件(codex R2-F3):拍的**结果**要能被手动触发口看见 —— `"failed"` = 这一拍的拉取/应用
750
+ // 抛了(下面的 catch 吞掉异常保住循环,但吞掉的事实必须传出去:operator 按下刷新时,「中心超时」
751
+ // 与「已刷新」必须可分辨,否则这个口的「诚实」二字就是假的)。
703
752
  const refreshTick = async (prefetched) => {
704
753
  if (refreshInFlight)
705
- return;
754
+ return "ok";
706
755
  refreshInFlight = true;
707
756
  try {
708
757
  // codex-3/4 F1:欠账的 catalog 过渡在**每个真拍**开头无条件重试——与 fetch 成败/候选好坏解耦
@@ -761,6 +810,10 @@ export async function createConfigCenterRuntime(ctx) {
761
810
  }
762
811
  }
763
812
  if (r) {
813
+ // #322 世代账:**拉到即算 target**(候选是否被采纳是下面的事)。整拒的拍只推进 target ⇒
814
+ // target≠applied 的持续时长在 /health 上可见 —— 「持续卡在旧代」的副本从结构上可被摘除。
815
+ const tickGen = generationOf(r.effective.version); // #325:收货序数(本拍全链同一个)
816
+ applyLedger.seenTarget(tickGen, r.effective.version);
764
817
  // §9.5-5 candidate gate: ANY invalid domain rejects the WHOLE candidate — the live config
765
818
  // keeps serving the LKG (`latestEffective`) instead of that domain falling to schema-default/
766
819
  // empty. ccEtag is deliberately NOT advanced: the bad tree stays visible (re-judged each poll),
@@ -793,7 +846,7 @@ export async function createConfigCenterRuntime(ctx) {
793
846
  note: "candidate rejected whole (§9.5-5) — live config keeps the last-known-good; fix the named files to unblock",
794
847
  });
795
848
  }
796
- return;
849
+ return "ok"; // 拍本身跑完了(候选被判拒是**结论**,不是失败)
797
850
  }
798
851
  lastRejectedCandidate = undefined;
799
852
  // ccEtag advances at the END of each branch (codex lens-1 HIGH): advancing before apply meant
@@ -818,12 +871,28 @@ export async function createConfigCenterRuntime(ctx) {
818
871
  // re-runs expandTiers, so hot-applying the arriving tier table would let tier words pass the
819
872
  // expanded admission gate while core throws "Unknown model ref" until restart. Hot-apply is safe
820
873
  // only when BOTH generations are tier-less.
821
- const planeDeferred = (runnerTierFrozen || planeHasActiveTiers(r.effective)) && modelPlaneChanged(appliedPlaneEff, r.effective);
822
- const committed = applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys, ...(planeDeferred ? { deferModelPlane: true } : {}) });
874
+ // core 5.49 swapModels 接缝在场 defer 判据整条退役:plane 变更( tier-frozen /
875
+ // 候选激活 tiers 两旧 defer 子臂,也含从前的 tier-less 就地热臂)一律经 swapPlane
876
+ // commit 前换进全部 boot Runner——统一走 swap 让「Runner 私有目录 vs 共享表」两种姿势
877
+ // 收敛成一种(swap 后 Runner 恒在私有目录上,后续变更继续 swap,无姿势追踪状态)。
878
+ const planeChanged = modelPlaneChanged(appliedPlaneEff, r.effective);
879
+ const planeDeferred = swapRunnerModels === undefined && (runnerTierFrozen || planeHasActiveTiers(r.effective)) && planeChanged;
880
+ let applyReport;
881
+ const committed = applyEffective(config, r.effective, logger, {
882
+ teamsOnly: true,
883
+ sealedKeys,
884
+ generation: tickGen,
885
+ report: (rep) => {
886
+ applyReport = rep;
887
+ applyLedger.recordApply(rep);
888
+ },
889
+ ...(planeDeferred ? { deferModelPlane: true } : {}),
890
+ ...(swapRunnerModels !== undefined && planeChanged ? { swapPlane: swapRunnerModels } : {}),
891
+ });
823
892
  // [2283]③:CAS 拒绝(更旧世代)⇒ 本拍整体跳过——prompts 采用/pricing/keyResolver/restart
824
893
  // 对账/etag 推进/LKG 落盘都不得从旁路半应用同一个被拒世代(拒绝 warn 已在 applyEffective 留痕)。
825
894
  if (!committed)
826
- return;
895
+ return "ok"; // CAS 拒(更旧世代)——同上,拍成功地得出了「不采纳」
827
896
  // 🔴 #303 codex R1-F2(真 finding,红先钉在 test/apply-effective-stage-commit.test.ts):
828
897
  // commit 的**派生两件**必须紧贴 commit,零 await —— 修前它们在下面 `await adoptCenterPrompts`
829
898
  // 之后,而 commit 已经把 `config.models` 就地换代(三个消费面实时读)、把
@@ -833,6 +902,8 @@ export async function createConfigCenterRuntime(ctx) {
833
902
  // (plane 被 defer 时 commit 压根没动模型面 ⇒ 这里按同一张旧表重建,内容不变,与修前同。)
834
903
  mutateInPlace(pricing, buildPricing(config.models)); // hot: cost/model changes; Runner reads this.deps.pricing live
835
904
  keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys); // hot: per-model key add/remove/change (env-ref + sealed)
905
+ limitSync?.sync(); // #322 批1:限额座与 config 成对换代(同一条「派生件紧贴 commit」纪律)
906
+ applyLedger.applied(tickGen, r.effective.version);
836
907
  if (planeDeferred) {
837
908
  logger.warn("models_tiers_plane_deferred", { version: r.effective.version, note: "tier-frozen Runner: the changed model plane (models/roles/tiers/default) is NOT hot-applied — admission stays on the Runner's generation; restart applies the new plane (models-tiers restart signal rides /health)" });
838
909
  }
@@ -858,8 +929,33 @@ export async function createConfigCenterRuntime(ctx) {
858
929
  // fire-and-forget 的 rename 可能没完成 → 下一进程读旧 LKG → 同差异重现 = 本功能要消的环。
859
930
  // 顺序=先持久化(含 skill 正文预热,F1)再发信号;持久化失败=warn+照发(不发=配置永不生效;
860
931
  // 「盘坏+中心挂」双故障下环重现,接受并点名)。
932
+ // 🔴 codex R2-F1(真 finding,红先):**没有完整应用的候选不许成为 LKG,也不许推进 etag**。
933
+ // applyEffective 对「其余面提交了、某组被整拒」的候选返回 true;修前这里照旧落盘 + 推进
934
+ // latestEffective/ccEtag ⇒ N-1 的好 LKG 被 N 盖掉,重启且中心不可达时 N 的那一组再次被拒 ⇒
935
+ // 退到 env 基线 = **N-1 的限额保护丢了**。世代账已记下拒因(applied 不前进),这里只需早退:
936
+ // 盘上的 LKG 与 etag 都停在上一个**完整**世代,下一拍重新判同一棵树(修好即自愈)。
937
+ const fullyApplied = applyReport?.fullyApplied !== false;
938
+ if (!fullyApplied) {
939
+ logger.warn("config_candidate_partially_applied", {
940
+ version: r.effective.version,
941
+ rejected: applyReport?.limits.rejected?.keys ?? [],
942
+ note: "a group of this candidate was refused — the live config keeps serving the prior values for it; the LKG and the ETag stay on the last FULLY applied generation (this tree is re-judged next tick)",
943
+ });
944
+ return "ok"; // 同上:拍跑完了,结论是「这一代没能完整应用」(世代账 + 本行 warn 是它的读面)
945
+ }
861
946
  const lkgPersisted = await persistLkgDurable(r.effective, r.etag);
862
947
  const reasons = restartReasons(effective, r.effective, restartCtx());
948
+ // swapModels 接缝在场 ⇒ models-tiers 指纹的 boot 基线差异不再是重启理由:能走到这一行的
949
+ // 候选,其模型面要么本拍已 swap 进全部 Runner、要么与已应用面相同(swap 拒绝在 committed
950
+ // === false 早退了)——留着这个 reason 就是指纹注释里点名的「applied yet signalled」不一致,
951
+ // orchestrator 会为一个已生效的变更滚动重启整片 fleet。degrade-route 面**不**跟着退役:
952
+ // 它指纹的是 createBrain 在 boot 构造期捕获目标 Model 的降级壳(restart-signal.ts:180),
953
+ // swapModels 只换 Runner 的目录代际、不重组 server 侧 brain 壳,该臂重启理由仍然诚实。
954
+ if (swapRunnerModels !== undefined) {
955
+ const mi = reasons.indexOf("models-tiers");
956
+ if (mi >= 0)
957
+ reasons.splice(mi, 1);
958
+ }
863
959
  // codex R35: the sticky boot skill debt rides EVERY tick's reasons — change-detection vs boot is
864
960
  // blind to it (the stale table IS the boot baseline). Under a no-handoff deferral it folds into
865
961
  // blocked (promoted later); otherwise it keeps pendingRestart alive until the restart happens.
@@ -953,12 +1049,30 @@ export async function createConfigCenterRuntime(ctx) {
953
1049
  }
954
1050
  catch (err) {
955
1051
  logger.warn("sema_registry_refresh_failed", { err: String(err), note: "etag NOT advanced — the same candidate is re-fetched and re-applied next poll" });
1052
+ return "failed";
956
1053
  }
957
1054
  finally {
958
1055
  refreshInFlight = false;
959
1056
  }
1057
+ return "ok";
960
1058
  };
961
- ccTimer = setInterval(() => void refreshTick(), 60_000);
1059
+ // #322 追加件(clay 当场裁定):**手动触发口**(POST /v1/admin/config/refresh) cadence 走**同一只**
1060
+ // 拍。这里加的是 join 语义而不是第二条拉取路径:`refreshTick` 自己的 `refreshInFlight` 是「在飞就跳过」
1061
+ // (对定时器是对的:下一拍 60s 后就来),而对一次**人发起的**触发,跳过等于骗人(回执说 triggered、
1062
+ // 实际什么都没干)。所以 cadence 与手动口都经 `runTick`:在飞 ⇒ 返回**同一个 promise**(join),
1063
+ // 空闲 ⇒ 起一拍。内层守卫保留(纵深:任何忘了走 runTick 的新调用点仍不会并发双拍)。
1064
+ let inFlight;
1065
+ const runTick = (prefetched) => {
1066
+ if (inFlight)
1067
+ return inFlight; // join:全部 joiner 拿到**同一拍的同一个结果**(成功与失败都一样)
1068
+ const p = refreshTick(prefetched).finally(() => {
1069
+ inFlight = undefined;
1070
+ });
1071
+ inFlight = p;
1072
+ return p;
1073
+ };
1074
+ manualRefresh = runTick;
1075
+ ccTimer = setInterval(() => void runTick(), 60_000);
962
1076
  ccTimer.unref?.();
963
1077
  // Boot-deferred continuation(二轮复审 F5 改形):到货结果按「迟到的 boot」处理,而不是转普通 tick——
964
1078
  // 普通 tick 的 restartReasons(undefined, r) 会把 prompts/skills 面全判为差异 → restart → 中心持续慢时
@@ -986,6 +1100,8 @@ export async function createConfigCenterRuntime(ctx) {
986
1100
  }
987
1101
  };
988
1102
  const deferredBootApplyInner = async (r) => {
1103
+ const lateGen = generationOf(r.effective.version); // #325:收货序数(迟到 boot 拍全链同一个)
1104
+ applyLedger.seenTarget(lateGen, r.effective.version); // #322:迟到 boot 也是「见过这一代」
989
1105
  const promptsRaw = r.effective.prompts;
990
1106
  const promptsGate = promptsRaw !== undefined ? validatePromptsDomain(promptsRaw) : { ok: true };
991
1107
  if ((r.domainErrors ?? []).length > 0 || !promptsGate.ok) {
@@ -1004,14 +1120,32 @@ export async function createConfigCenterRuntime(ctx) {
1004
1120
  // codex R10 (deferred-boot twin of the refresh-lane guard): the Runner was built from the ENV plane before
1005
1121
  // this late arrival — if it froze a tier-expanded copy, the arriving center plane must not hot-apply
1006
1122
  // (admission/Runner split). Tier-less env boot (the common deferred-boot shape) keeps true hot-apply.
1007
- const planeDeferredLate = (runnerTierFrozen || planeHasActiveTiers(r.effective)) && modelPlaneChanged(appliedPlaneEff, r.effective);
1008
- const committedLate = applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys, ...(planeDeferredLate ? { deferModelPlane: true } : {}) });
1123
+ // core 5.49 swapModels 在场 ⇒ 该守卫与正常 tick 腿(:936)同判据退役:候选面经 swapPlane
1124
+ // commit 前原子换进全部 boot Runner,admission/Runner split 不复存在。合并窗重扫 high(V2
1125
+ // 同能力多腿修一漏一现行犯):换装批只接了 tick 腿,这条迟到 boot 腿漏接 ⇒ 生产恒接
1126
+ // swapRunnerModels 的部署在 env 起服+首拉过窗时,plane 仍被整面 defer + 误签 restart。
1127
+ const planeChangedLate = modelPlaneChanged(appliedPlaneEff, r.effective);
1128
+ const planeDeferredLate = swapRunnerModels === undefined && (runnerTierFrozen || planeHasActiveTiers(r.effective)) && planeChangedLate;
1129
+ let applyReportLate;
1130
+ const committedLate = applyEffective(config, r.effective, logger, {
1131
+ teamsOnly: true,
1132
+ sealedKeys,
1133
+ generation: lateGen,
1134
+ report: (rep) => {
1135
+ applyReportLate = rep;
1136
+ applyLedger.recordApply(rep);
1137
+ },
1138
+ ...(planeDeferredLate ? { deferModelPlane: true } : {}),
1139
+ ...(swapRunnerModels !== undefined && planeChangedLate ? { swapPlane: swapRunnerModels } : {}),
1140
+ });
1009
1141
  // [2283]③(refresh 腿同款):CAS 拒绝 ⇒ 迟到 boot 拍整体跳过,旁路消费与 etag/LKG 都不动。
1010
1142
  if (!committedLate)
1011
1143
  return;
1012
1144
  // #303 R1-F2 的 late-boot 双胞胎(同一形,只修一条=另一条继续漏):派生两件紧贴 commit,零 await。
1013
1145
  mutateInPlace(pricing, buildPricing(config.models));
1014
1146
  keyResolver = createKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1147
+ limitSync?.sync(); // #322 批1(late-boot 双胞胎:只修一条腿 = 另一条继续漏)
1148
+ applyLedger.applied(lateGen, r.effective.version);
1015
1149
  if (planeDeferredLate)
1016
1150
  logger.warn("models_tiers_plane_deferred", { version: r.effective.version, note: "tier-frozen Runner (env tiers): the late-boot center model plane is NOT hot-applied — restart applies it" });
1017
1151
  else {
@@ -1022,11 +1156,29 @@ export async function createConfigCenterRuntime(ctx) {
1022
1156
  if (!planeDeferredLate)
1023
1157
  markRosterLanded(r.effective); // codex R13: same guard as the refresh lane — never open readiness off an unapplied plane
1024
1158
  // (pricing/keyResolver 的重建已上提到 commit 紧邻处 —— 见上方 #303 R1-F2 注)
1159
+ // codex R2-F1 的 late-boot 双胞胎(只修一条腿 = 另一条继续漏)。
1160
+ const fullyAppliedLate = applyReportLate?.fullyApplied !== false;
1161
+ if (!fullyAppliedLate) {
1162
+ logger.warn("config_candidate_partially_applied", {
1163
+ version: r.effective.version,
1164
+ rejected: applyReportLate?.limits.rejected?.keys ?? [],
1165
+ note: "late-boot candidate had a refused group — the LKG/ETag stay on the last FULLY applied generation; the refresh cadence re-judges the same tree",
1166
+ });
1167
+ return;
1168
+ }
1025
1169
  effective = r.effective; // restart 比较基线=到货值(cadence 不再重复触发)
1026
1170
  latestEffective = r.effective;
1027
1171
  ccEtag = r.etag;
1028
1172
  const lkgPersistedLate = await persistLkgDurable(r.effective, r.etag); // F2 parity:信号可见前落盘+skill 正文预热
1029
1173
  const reasons = restartReasons(undefined, r.effective, restartCtx()); // (prompts 不在 restart slices——热采用已在上方 adopt)
1174
+ // swapModels 接缝在场 ⇒ models-tiers 摘除(:1007 正常 tick 腿同形同理由):能走到这一行的候选,
1175
+ // 模型面已本拍 swap 进全部 Runner(swap 拒绝在 committedLate === false 早退)——env 基线对候选的
1176
+ // 指纹差异不再是重启理由;degrade-route 不跟(brain 壳 boot 期捕获,swap 不重组)。
1177
+ if (swapRunnerModels !== undefined) {
1178
+ const miLate = reasons.indexOf("models-tiers");
1179
+ if (miLate >= 0)
1180
+ reasons.splice(miLate, 1);
1181
+ }
1030
1182
  // codex R15 (late-boot twin): tiered env boot + tier-less late candidate ⇒ deferral fires but BOTH slice
1031
1183
  // fingerprints reduce to null (undefined baseline, tier-less candidate) — no models-tiers reason, plane
1032
1184
  // deferred forever. Deferral is the ground truth; force the reason so the orchestrator restarts and the
@@ -1063,8 +1215,9 @@ export async function createConfigCenterRuntime(ctx) {
1063
1215
  };
1064
1216
  // LKG 起服的进程有完整 boot 基线 → 到货走普通 tick(候选门+hot+对 LKG 的 restartReasons,通常
1065
1217
  // 零差异=无信号无环);env 起服(无 LKG)才走「迟到 boot」路径。
1218
+ // 迟到 boot 的续接也走 runTick(同一只拍;手动口若正好在飞,这条会 join 它而不是并发跑第二拍)。
1066
1219
  if (bootConfigPending)
1067
- void bootConfigPending.then((r) => (lkgBooted ? refreshTick(r) : deferredBootApply(r)), () => { });
1220
+ void bootConfigPending.then((r) => void (lkgBooted ? runTick(r) : deferredBootApply(r)), () => { });
1068
1221
  }
1069
1222
  },
1070
1223
  stopRefreshLoop() {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * #322 批1 —— 限额座的**换代同步口**(design/322 §2 B簇1)。
3
+ *
4
+ * `applyEffective` 的 commit 段只做一件事:把新值写进 `config`。两只限额座(RateGate / QuotaTracker)持有的
5
+ * 是**比较参数**,不是 config 引用,所以它们要在 commit 之后被推一把 —— 与 `pricing` / `keyResolver` 那两件
6
+ * 派生重建同族、同纪律(commit 之后、**零 await 之内**;源锚在 test/config-hot/batch1-limits-hot.test.ts)。
7
+ *
8
+ * 为什么不是「座位每次 check 现读 config」:那样每个请求都要过一次配置对象取值,而且会把「限额座」这个
9
+ * 概念稀释成一堆散落的读点(六个实现类各读各的)。setter 形让 0↔N、N→N′ 全部收敛成一次显式换代,并且
10
+ * 关断哨兵可以顺手把 SQL 腿的刷新环真的停掉(off 态零 SQL 的承诺就落在这里)。
11
+ */
12
+ import type { ServiceConfig } from "../config-types.js";
13
+ import type { QuotaLimits, QuotaTracker } from "../observability/cost-quota.js";
14
+ import type { Logger } from "../observability/logger.js";
15
+ import type { RateGate } from "../observability/rate-limit.js";
16
+ /** USD/秒 → micro/ms 的**唯一**换算点(装配期构造与热同步共用;两处各算一次迟早会差一个 1e6)。 */
17
+ export declare function buildQuotaBounds(config: Pick<ServiceConfig, "maxPrincipalCostUsd" | "costQuotaWindowSec">): QuotaLimits;
18
+ export interface LimitSyncCtx {
19
+ /** 活配置(热应用就地改写的**同一个**对象)。 */
20
+ readonly config: Pick<ServiceConfig, "rateLimitPerMin" | "maxPrincipalCostUsd" | "costQuotaWindowSec">;
21
+ /** 恒构造的两只座;undefined 只出现在「这条腿本部署根本没装配」的夹具形。 */
22
+ readonly rateGate?: RateGate;
23
+ readonly quota?: QuotaTracker;
24
+ readonly logger?: Logger;
25
+ }
26
+ export interface LimitSync {
27
+ /** 把两只座推到 config 现值。幂等;**只在真变了的时候**留一行(refresh 每分钟一拍,恒打就是噪音)。 */
28
+ sync(): void;
29
+ }
30
+ /** `create*`:返回带行为的活对象(闭包持座 + 上一代读数)。 */
31
+ export declare function createLimitSync(ctx: LimitSyncCtx): LimitSync;
32
+ //# sourceMappingURL=limit-sync.d.ts.map
@@ -0,0 +1,35 @@
1
+ /** USD/秒 → micro/ms 的**唯一**换算点(装配期构造与热同步共用;两处各算一次迟早会差一个 1e6)。 */
2
+ export function buildQuotaBounds(config) {
3
+ return { limitMicroUsd: Math.round(config.maxPrincipalCostUsd * 1e6), windowMs: config.costQuotaWindowSec * 1000 };
4
+ }
5
+ /** `create*`:返回带行为的活对象(闭包持座 + 上一代读数)。 */
6
+ export function createLimitSync(ctx) {
7
+ const { config, rateGate, quota, logger } = ctx;
8
+ let lastRate;
9
+ let lastQuota;
10
+ return {
11
+ sync() {
12
+ const changed = {};
13
+ if (rateGate && config.rateLimitPerMin !== lastRate) {
14
+ rateGate.setLimits({ rateLimitPerMin: config.rateLimitPerMin });
15
+ if (lastRate !== undefined)
16
+ changed.rateLimitPerMin = config.rateLimitPerMin;
17
+ lastRate = config.rateLimitPerMin;
18
+ }
19
+ if (quota) {
20
+ const next = buildQuotaBounds(config);
21
+ if (next.limitMicroUsd !== lastQuota?.limitMicroUsd || next.windowMs !== lastQuota?.windowMs) {
22
+ quota.setLimits(next);
23
+ if (lastQuota !== undefined) {
24
+ changed.maxPrincipalCostUsd = config.maxPrincipalCostUsd;
25
+ changed.costQuotaWindowSec = config.costQuotaWindowSec;
26
+ }
27
+ lastQuota = next;
28
+ }
29
+ }
30
+ if (Object.keys(changed).length > 0)
31
+ logger?.info("limit_gates_reloaded", changed);
32
+ },
33
+ };
34
+ }
35
+ //# sourceMappingURL=limit-sync.js.map
@@ -98,6 +98,74 @@ export interface ReapersCtx {
98
98
  /** 晚绑(server 造出来才有)——见文件头「位置即契约」①。 */
99
99
  getRunDenySweep: () => ((now: number) => Promise<void>) | undefined;
100
100
  }
101
+ /** core 5.48 `reapDurableAgents` 的 `deps.sessions` 面(结构形,只列本仓要填的三格)。 */
102
+ export interface ReapSessionsFacade {
103
+ unpin(sessionId: string): unknown;
104
+ release(sessionId: string): Promise<void>;
105
+ listPlaced?(kind: "subagent", opts?: {
106
+ olderThanMs?: number;
107
+ scope?: string;
108
+ }): Promise<Array<{
109
+ sessionId: string;
110
+ placedAt: number;
111
+ } & ({
112
+ scope: string;
113
+ handle: string;
114
+ } | {
115
+ tupleIncomplete: true;
116
+ })>>;
117
+ }
118
+ /** 本仓两只会话面的窄读口(host = boot 造的真 durable 店;routed = subRunner 的路由店)。 */
119
+ interface ReapSessionsInputs {
120
+ host?: {
121
+ release(sessionId: string): Promise<void> | void;
122
+ listPlaced?(kind: "subagent", opts?: {
123
+ olderThanMs?: number;
124
+ scope?: string;
125
+ }): Promise<Array<{
126
+ sessionId: string;
127
+ placedAt: number;
128
+ } & ({
129
+ scope: string;
130
+ handle: string;
131
+ } | {
132
+ tupleIncomplete: true;
133
+ })>>;
134
+ placementOf?(sessionId: string): Promise<{
135
+ kind: string;
136
+ } | undefined>;
137
+ };
138
+ routed: {
139
+ unpin?(sessionId: string): unknown;
140
+ release(sessionId: string): Promise<void> | void;
141
+ };
142
+ }
143
+ /**
144
+ * 🔴 #292 C18(core 5.48.0 design/323,[4743] 提货单)——**分区腿武装**的判据本体。
145
+ *
146
+ * core 5.48 起 `reapDurableAgents` 的 `deps.sessions` 多一只可选 `listPlaced`:在场即启用**分区收割
147
+ * 腿** —— 一条 placed 转录的属主行已经不在(put 从未落地 / 上一轮条件删赢了但 release 失败)时,行驱动
148
+ * 的那条臂**结构上看不见**它(它从行出发枚举),只有从会话侧枚举才找得到。
149
+ *
150
+ * 两只面必须落在**host durable 店**上,而 release 这一格是承重的:
151
+ * · `listPlaced` = host 店的分区枚举(SQL 孪生实装;local/File 后端无此面 ⇒ 键**不设**,core 那条腿
152
+ * 如实不可用 —— 绝不给一个恒返回 `[]` 的假面,那会把「枚不了」读成「没有孤儿」);
153
+ * · `release` 对 **placed id** 必须走 host 店:路由店(`ForkRoutingSessionStore`)对一个重启后不认识
154
+ * 的 id 会回落 transient(进程内 no-op),那会让 core 数出一次**从未发生**的 release
155
+ * (`orphanPlacedReleased` 当场变成谎)而字节还在库里。故先按**持久化元组**判(一次 PK 探针),
156
+ * placed 走 host 店的真删除;其余 id 原样保留 [1522] 裁定的双店语义。
157
+ * · 🔴 探针本身失败 ⇒ **响亮抛**(codex 对抗复审 R1-[medium],验真后修:本函数的初版把探针错误
158
+ * 改写成 `undefined` 再落路由店 —— 而重启后路由店不认识 placed id ⇒ transient no-op,core 却因为
159
+ * release 正常 resolve 而 `orphanPlacedReleased++`。间歇性 DB 故障于是被**伪装成 GC 成功**:监控
160
+ * 报着「收掉了 N 条孤儿」,durable 字节一条没动。这正是本函数顶注要防的那个形,初版自己踩了。
161
+ * 抛的后果是安全的:core 两条臂的 release 都在 try 内、且计数在 release **之后**递增 ⇒ 抛 = 不计数
162
+ * + 下一 tick 重新枚举重试(失败经济学与 core 的成文一致:泄漏给下一轮,而不是谎报已清)。
163
+ *
164
+ * 抽成具名工厂而不是就地闭包的理由与 `createEngineNoticeSeat` 逐字同一条:判据在**值**上
165
+ * (哪只店被调),源码文本门看不出对错,只有行为测得出来。
166
+ */
167
+ export declare function createPlacementAwareReapSessions(deps: ReapSessionsInputs): ReapSessionsFacade;
101
168
  /** 起后台维护 tick,返回定时器 handle(收尾段 clearInterval 用)。 */
102
169
  export declare function startReapers(ctx: ReapersCtx): NodeJS.Timeout;
170
+ export {};
103
171
  //# sourceMappingURL=reapers.d.ts.map