@sema-agent/server 7.121.0 → 7.123.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 (136) hide show
  1. package/MIGRATION.md +175 -0
  2. package/README.md +6 -1
  3. package/README.zh-CN.md +6 -1
  4. package/USAGE.md +57 -18
  5. package/dist/approval-reconciler.d.ts +8 -0
  6. package/dist/approval-reconciler.js +72 -52
  7. package/dist/audit.d.ts +1 -1
  8. package/dist/audit.js +52 -22
  9. package/dist/auto-mode-face.d.ts +16 -5
  10. package/dist/auto-mode-face.js +7 -4
  11. package/dist/bench/s1/live-deps.js +1 -0
  12. package/dist/boot/config-center.js +3 -0
  13. package/dist/boot/leader.js +1 -0
  14. package/dist/boot/reapers.d.ts +3 -0
  15. package/dist/boot/reapers.js +2 -1
  16. package/dist/boot/resolve-spec.js +1 -4
  17. package/dist/boot/shutdown.js +6 -1
  18. package/dist/boot/stage-08-reapers.js +2 -2
  19. package/dist/capabilities/memory-notice.d.ts +1 -1
  20. package/dist/config-catalog.js +4 -4
  21. package/dist/config-center/apply-effective.d.ts +1 -1
  22. package/dist/config-center/apply-effective.js +5 -3
  23. package/dist/config-center/center-request.d.ts +6 -9
  24. package/dist/config-center/center-request.js +11 -27
  25. package/dist/config-center/http-client.js +3 -3
  26. package/dist/config-center/read-warnings.d.ts +56 -35
  27. package/dist/config-center/read-warnings.js +67 -19
  28. package/dist/config-center/types.d.ts +7 -8
  29. package/dist/config-lkg.d.ts +12 -9
  30. package/dist/config-lkg.js +38 -39
  31. package/dist/config-provider.d.ts +3 -2
  32. package/dist/config-provider.js +2 -2
  33. package/dist/config-types.d.ts +20 -9
  34. package/dist/config-types.js +1 -1
  35. package/dist/config.d.ts +10 -10
  36. package/dist/config.js +92 -127
  37. package/dist/fleet/drain-entry.d.ts +28 -3
  38. package/dist/fleet/drain-entry.js +3 -0
  39. package/dist/fleet/engine-signal.d.ts +54 -23
  40. package/dist/fleet/engine-signal.js +29 -5
  41. package/dist/fleet/serving-lease.js +13 -15
  42. package/dist/fleet-client.d.ts +8 -6
  43. package/dist/fleet-client.js +4 -3
  44. package/dist/governance-overrides.d.ts +7 -2
  45. package/dist/http/admission.js +7 -5
  46. package/dist/http/approval-sweeps.js +2 -2
  47. package/dist/http/checkpoint-ctx.d.ts +60 -11
  48. package/dist/http/checkpoint-ctx.js +23 -9
  49. package/dist/http/dispatch.js +22 -19
  50. package/dist/http/health-self-report.d.ts +3 -0
  51. package/dist/http/resume-legs.js +8 -8
  52. package/dist/http/route-ctx.d.ts +19 -3
  53. package/dist/http/route-table.d.ts +3 -0
  54. package/dist/http/route-table.js +4 -0
  55. package/dist/http/routes/admin-drain.d.ts +5 -2
  56. package/dist/http/routes/admin-drain.js +14 -2
  57. package/dist/http/routes/approvals-assistant.d.ts +1 -0
  58. package/dist/http/routes/approvals-assistant.js +6 -6
  59. package/dist/http/routes/capabilities.js +7 -5
  60. package/dist/http/routes/fleet.d.ts +1 -0
  61. package/dist/http/routes/fleet.js +1 -1
  62. package/dist/http/routes/images.d.ts +1 -0
  63. package/dist/http/routes/images.js +1 -1
  64. package/dist/http/routes/leader.js +6 -0
  65. package/dist/http/routes/runs.d.ts +2 -0
  66. package/dist/http/routes/runs.js +5 -4
  67. package/dist/http/routes/sessions.d.ts +1 -0
  68. package/dist/http/routes/sessions.js +2 -5
  69. package/dist/http/routes/trace-usage.d.ts +1 -0
  70. package/dist/http/routes/trace-usage.js +1 -1
  71. package/dist/http/routes/workflows.d.ts +1 -0
  72. package/dist/http/routes/workflows.js +1 -1
  73. package/dist/http/send.d.ts +13 -0
  74. package/dist/http/send.js +9 -0
  75. package/dist/http/server.d.ts +3 -0
  76. package/dist/http/stored-body-replay.d.ts +28 -0
  77. package/dist/http/stored-body-replay.js +22 -0
  78. package/dist/index.d.ts +2 -0
  79. package/dist/index.js +1 -0
  80. package/dist/leader/leader.d.ts +23 -1
  81. package/dist/leader/leader.js +6 -1
  82. package/dist/leader/repair-wire.d.ts +1 -1
  83. package/dist/leader/repair-wire.js +2 -2
  84. package/dist/leader/run-entry.d.ts +27 -0
  85. package/dist/leader/run-entry.js +29 -0
  86. package/dist/leader/wire.d.ts +10 -0
  87. package/dist/leader/wire.js +15 -10
  88. package/dist/model-entry-refusal.d.ts +18 -9
  89. package/dist/model-entry-refusal.js +19 -14
  90. package/dist/model-route-endpoint.d.ts +21 -1
  91. package/dist/model-route-endpoint.js +9 -3
  92. package/dist/model-route-gap.d.ts +103 -0
  93. package/dist/model-route-gap.js +80 -0
  94. package/dist/model-route-url.d.ts +13 -20
  95. package/dist/model-route-url.js +28 -24
  96. package/dist/namespaced-rule-name.d.ts +16 -30
  97. package/dist/namespaced-rule-name.js +2 -20
  98. package/dist/observability/fail-open.d.ts +23 -10
  99. package/dist/observability/fail-open.js +8 -8
  100. package/dist/plugins/approval-ask-store-file.d.ts +1 -0
  101. package/dist/plugins/approval-ask-store-file.js +3 -0
  102. package/dist/plugins/approval-ask-store-memory.d.ts +1 -0
  103. package/dist/plugins/approval-ask-store-memory.js +6 -0
  104. package/dist/plugins/approval-ask-store-sql.d.ts +12 -0
  105. package/dist/plugins/approval-ask-store-sql.js +4 -0
  106. package/dist/plugins/one-time-migrations.js +40 -0
  107. package/dist/plugins/park-commit-watch.d.ts +19 -5
  108. package/dist/plugins/park-commit-watch.js +24 -17
  109. package/dist/plugins/session-policy-store-sql.js +1 -2
  110. package/dist/plugins/workflow-run-store-sql.d.ts +4 -2
  111. package/dist/router/route-orchestration.d.ts +4 -2
  112. package/dist/router/route-orchestration.js +2 -1
  113. package/dist/rule-entry-list.d.ts +3 -13
  114. package/dist/rule-entry-list.js +1 -12
  115. package/dist/run-local.js +4 -2
  116. package/dist/runtime-governance.d.ts +67 -32
  117. package/dist/runtime-governance.js +28 -9
  118. package/dist/session-policy-wire.d.ts +47 -49
  119. package/dist/session-policy-wire.js +41 -36
  120. package/dist/session-sync.d.ts +5 -1
  121. package/dist/session-sync.js +2 -1
  122. package/dist/sighup-idle.d.ts +7 -1
  123. package/dist/sighup-idle.js +4 -4
  124. package/dist/spec-fields.d.ts +4 -4
  125. package/dist/spec-fields.js +1 -4
  126. package/dist/task-settings.d.ts +36 -14
  127. package/dist/task-settings.js +20 -23
  128. package/dist/tool-approval.d.ts +6 -0
  129. package/dist/tool-approval.js +13 -1
  130. package/dist/trace/redact.d.ts +3 -3
  131. package/dist/unattended-permission-mode.d.ts +17 -13
  132. package/dist/unattended-permission-mode.js +3 -1
  133. package/dist/url-shape.d.ts +31 -0
  134. package/dist/url-shape.js +41 -0
  135. package/package.json +3 -3
  136. package/scripts/drain-check-local.mjs +1 -1
@@ -0,0 +1,80 @@
1
+ import { DEFAULT_MODEL_REF } from "./model-select.js";
2
+ import { judgeModelRouteEndpoint } from "./model-route-endpoint.js";
3
+ export const MODEL_ROUTE_ENV_KEYS = {
4
+ gatewayBaseUrl: "MODEL_GATEWAY_BASEURL",
5
+ gatewayFallbackUrls: "MODEL_GATEWAY_FALLBACK_URLS",
6
+ provider: "MODEL_PROVIDER",
7
+ mainId: "MODEL_ID",
8
+ cheapId: "MODEL_CHEAP_ID",
9
+ anthropicApiKey: "ANTHROPIC_API_KEY",
10
+ anthropicAuthToken: "ANTHROPIC_AUTH_TOKEN",
11
+ anthropicBaseUrl: "ANTHROPIC_BASE_URL",
12
+ };
13
+ export function mintedCheapId(slot) {
14
+ switch (slot.kind) {
15
+ case "minted":
16
+ return slot.id;
17
+ case "absent":
18
+ case "without_main":
19
+ case "same_as_main":
20
+ return undefined;
21
+ }
22
+ }
23
+ const declaredValue = (v) => (v !== undefined && v !== "" ? v : undefined);
24
+ export const csvOf = (raw) => (raw ?? "")
25
+ .split(",")
26
+ .map((s) => s.trim())
27
+ .filter(Boolean);
28
+ export function readModelRouteEnv(env) {
29
+ const K = MODEL_ROUTE_ENV_KEYS;
30
+ const anthropicApiKey = declaredValue(env[K.anthropicApiKey]);
31
+ const anthropicAuthToken = declaredValue(env[K.anthropicAuthToken]);
32
+ const anthropicBaseUrl = declaredValue(env[K.anthropicBaseUrl]);
33
+ const anthropicRouteMounted = anthropicApiKey !== undefined || anthropicAuthToken !== undefined;
34
+ const explicitProvider = declaredValue(env[K.provider]);
35
+ const inferAnthropic = anthropicBaseUrl !== undefined && anthropicRouteMounted;
36
+ const mainId = env[K.mainId] ?? "";
37
+ const rawCheapId = env[K.cheapId];
38
+ return {
39
+ gatewayBaseUrl: declaredValue(env[K.gatewayBaseUrl]),
40
+ gatewayFallbackUrls: csvOf(env[K.gatewayFallbackUrls]),
41
+ anthropicApiKey,
42
+ anthropicAuthToken,
43
+ anthropicBaseUrl,
44
+ anthropicRouteMounted,
45
+ provider: explicitProvider ?? (inferAnthropic ? "anthropic" : "gateway"),
46
+ providerSource: explicitProvider !== undefined ? "explicit" : inferAnthropic ? "inferred" : "default",
47
+ mainId,
48
+ cheapSlot: !rawCheapId
49
+ ? { kind: "absent" }
50
+ : mainId === ""
51
+ ? { kind: "without_main", id: rawCheapId }
52
+ : rawCheapId === mainId
53
+ ? { kind: "same_as_main", id: rawCheapId }
54
+ : { kind: "minted", id: rawCheapId },
55
+ };
56
+ }
57
+ export function envLaneTable(mainId, main, cheap) {
58
+ if (mainId === "")
59
+ return {};
60
+ const table = { [DEFAULT_MODEL_REF]: main, [main.id]: main };
61
+ if (cheap !== undefined)
62
+ table[cheap.id] = cheap;
63
+ return table;
64
+ }
65
+ export function judgeModelRouteGap(env, opts = {}) {
66
+ const r = readModelRouteEnv(env);
67
+ const slot = (id) => ({ id, baseUrl: "", provider: r.provider });
68
+ const cheapId = mintedCheapId(r.cheapSlot);
69
+ const models = opts.envLaneDispatchable === false ? {} : envLaneTable(r.mainId, slot(r.mainId), cheapId !== undefined ? slot(cheapId) : undefined);
70
+ const verdict = judgeModelRouteEndpoint({
71
+ gatewayBaseUrl: r.gatewayBaseUrl,
72
+ gatewayFallbackUrls: r.gatewayFallbackUrls,
73
+ anthropic: r.anthropicRouteMounted ? {} : undefined,
74
+ models,
75
+ });
76
+ if (verdict === undefined)
77
+ return { gap: "none" };
78
+ return { gap: verdict.gap, code: verdict.code, reason: verdict.reason };
79
+ }
80
+ //# sourceMappingURL=model-route-gap.js.map
@@ -1,7 +1,7 @@
1
1
  /** 机读码(拼法随 `config.<键名蛇形>_*` 既有族:`config.model_provider_invalid` / `config.retention_policy_invalid`)。 */
2
2
  export declare const GATEWAY_URL_USERINFO_CODE = "config.gateway_url_userinfo";
3
- /** 同族第二码(S-483 座⑨):这一位根本不是一个层级式绝对 URL。三根键共用一码,与上面那只同纪律
4
- * (码按**规则**命名,不按键名 —— 键名已经逐字在拒因文案里点过了)。 */
3
+ /** 同族第二码(S-483 座⑨):这一位不是一个层级式 http(s) URL(不透明 / 解析不动 / 非 http(s) —— S-1269 起后者同码)。三根键共用一码,
4
+ * 与上面那只同纪律(码按**规则**命名,不按键名 —— 键名已经逐字在拒因文案里点过了)。 */
5
5
  export declare const MODEL_ROUTE_URL_INVALID_CODE = "config.model_route_url_invalid";
6
6
  /**
7
7
  * 🔴 **第三条臂的机读码(S-514;core 7.24.2 / [ref] 提货)——请求根带 `#fragment`**。
@@ -12,26 +12,19 @@ export declare const MODEL_ROUTE_URL_INVALID_CODE = "config.model_route_url_inva
12
12
  */
13
13
  export declare const MODEL_ROUTE_URL_FRAGMENT_CODE = "config.model_route_url_fragment";
14
14
  /**
15
- * 模型路由 base URL 的拒因(`undefined` = 这一位合法)—— **这三根键的唯一判官**。
16
- *
17
- * 两臂,顺序即语义,**两臂都经 display-safe 铸点**:
18
- * ① **不是一个层级式绝对 URL** ⇒ `config.model_route_url_invalid`;
19
- * ② 是,但带 userinfo ⇒ `config.gateway_url_userinfo`。
20
- *
21
- * 🔴 ① 为什么必须在这里、而不是「由别的门管」(S-483 座⑨,与 ME/A2A 两座**同病镜像**):本函数此前先问
22
- * 凭据位、并把「解析不动」读成**没有**凭据(`urlCarriesUserinfo` 的 `return false`),旁注写着「不是 URL 由
23
- * 别的门管」—— 而 config 层对这三根**没有**那道门(`config.ts` 与 `config-invariants.ts` 只调本函数)。
24
- * 于是半写 userinfo `http://user:pass@`(`URL.canParse` 假)与 `alice:pw@host:8000`(不透明 URL,
25
- * `username`/`password` 恒空)两形被当「无凭据」放行,一路到 brain 装配期才由 core 拒;core 那一臂
26
- * 今天不回显原值 —— 但**「恰好不漏」不是防线**,而且拒启点越晚,运维越读不出是哪根旋钮配错了。
27
- * ⇒ 判据改成与铸点**同一只** {@link isHierarchicalUrl}:两处再也不可能各判各的。
28
- *
29
- * 🚧 **未闭环(不在本车射程)**:层级式但**非 http(s)** 的形(`ftp://h/v1`,不带 userinfo)今天仍放行 ——
30
- * `fetch` 对它同样必然失败,但那是第三条臂、是一次新的行为面拒启,按三问另开车(S-483 收车档已登记)。
31
- *
32
- * 拒因四件齐:**码** + **哪个 env 键** + **洗过的 URL**(host/port/path 逐字,userinfo 与凭据参数成记号 ——
15
+ * 模型路由 base URL 的拒因(`undefined` = 这一位合法)—— 判由 `url-shape.ts` 那一只,本函数按**首个**拒因选码与出路句:
16
+ * ① 不是层级式 http(s) URL ⇒ `config.model_route_url_invalid`;
17
+ * ② 带 userinfo ⇒ `config.gateway_url_userinfo`;
18
+ * ③ 带 `#fragment` ⇒ `config.model_route_url_fragment`。
19
+ * 拒因四件齐:**码** + **哪个 env 键** + **值的样子**(判官的 `buildRefusalShownUrl`:层级式只到 `scheme://host[:port]` + `/…`,不透明形整只成记号 ——
33
20
  * 运维要靠 host 认出是哪一条路由,而密码一个字节都不许进启动错误)+ **出路一句**(点名真实旋钮)。
34
21
  *
22
+ * ① 为什么在**声明关口**而不是「由别的门管」(S-483 座⑨):半写 userinfo `http://user:pass@`(解析不动)与 `alice:pw@host:8000`
23
+ * (不透明 URL,`username`/`password` 恒空)两形若被当「无凭据」放行,一路到 brain 装配期才由 core 拒,拒启点越晚运维越读不出
24
+ * 是哪根旋钮配错了;层级式非 http(s)(`ftp://h/v1`)对 `fetch` 同样必然失败(S-1269 起同判,修前放行)。
25
+ * ③ 判在声明关口而不是留给 core 的 brain 构造期(S-514):core 的拒因只过位置律、pathname 逐字,`…/v1/sk-…#note` 那一形的
26
+ * core 拒因会把 path 上的令牌原文打进启动日志 —— 值在到达 core 之前就被这里拒掉,那条文本根本不由 core 铸。
27
+ *
35
28
  * @param envKey 出问题的那根 env 旋钮的名字(CSV 键上还带第几项)。
36
29
  * @param credentialKnob 这条路由把凭据放哪儿 —— 出路一句点名的旋钮名。
37
30
  */
@@ -1,33 +1,37 @@
1
- import { buildDisplaySafeUrl, isHierarchicalUrl } from "./trace/redact.js";
1
+ import { buildHttpUrlRefusal, httpUrlFlaws } from "./url-shape.js";
2
2
  export const GATEWAY_URL_USERINFO_CODE = "config.gateway_url_userinfo";
3
3
  export const MODEL_ROUTE_URL_INVALID_CODE = "config.model_route_url_invalid";
4
4
  export const MODEL_ROUTE_URL_FRAGMENT_CODE = "config.model_route_url_fragment";
5
+ const MODEL_ROUTE_FORBIDS = ["userinfo", "fragment"];
5
6
  export function modelRouteUrlRefusal(envKey, raw, credentialKnob) {
6
7
  if (raw === "")
7
8
  return undefined;
8
- if (!isHierarchicalUrl(raw)) {
9
- return (`env ${envKey} is not a parseable absolute URL (${buildDisplaySafeUrl(raw)}) [${MODEL_ROUTE_URL_INVALID_CODE}] — ` +
10
- "a model-route base URL is consumed by fetch, which requires an absolute hierarchical URL " +
11
- "(scheme://host[:port][/path]); half-written forms like \"http://user:pass@\" and opaque forms like " +
12
- "\"host:8000\" are not one, and they also hide any credentials they carry from every credential-position check. " +
13
- `Set ${envKey} to a full scheme://host URL (and put the credential in ${credentialKnob}, never in the URL).`);
14
- }
15
- const u = new URL(raw);
16
- if (u.username !== "" || u.password !== "")
17
- return (`env ${envKey} carries credentials in the URL userinfo (${buildDisplaySafeUrl(raw)}) [${GATEWAY_URL_USERINFO_CODE}] — ` +
18
- "WHATWG forbids constructing a fetch Request from a URL with credentials, so EVERY model call on this route " +
19
- "fails deterministically at request-construction time (it used to look like a slow network flake: the error was " +
20
- "retried for ~3 minutes before surfacing). " +
21
- `Put the credential in ${credentialKnob} (or the matching header knob) instead of the URL, and keep the URL to ` +
22
- "scheme/host/port/path.");
23
- if (u.hash !== "") {
24
- return (`env ${envKey} carries a URL fragment (${buildDisplaySafeUrl(raw)}#…) [${MODEL_ROUTE_URL_FRAGMENT_CODE}] — ` +
25
- "a fragment is never sent on the wire, so this route would silently target the root's own path and every " +
26
- "endpoint suffix (/chat/completions, /v1/messages) would land inside the discarded part: the request goes out, " +
27
- "may even return 200, and points somewhere else without saying so. " +
28
- `Drop everything from the "#" on in ${envKey}. A per-deployment QUERY is fully supported and rides every ` +
29
- "request (a gateway's `?api-version=…`); a fragment cannot be.");
9
+ const flaws = httpUrlFlaws(raw, MODEL_ROUTE_FORBIDS);
10
+ const first = flaws[0];
11
+ if (first === undefined)
12
+ return undefined;
13
+ const head = buildHttpUrlRefusal(`env ${envKey}`, raw, flaws);
14
+ switch (first) {
15
+ case "not-http-url":
16
+ return (`${head} [${MODEL_ROUTE_URL_INVALID_CODE}] — ` +
17
+ "a model-route base URL is consumed by fetch, which only sends to an absolute hierarchical http(s) URL; " +
18
+ "half-written forms like \"http://user:pass@\", opaque forms like \"host:8000\" and other schemes are not one, and the first two also hide " +
19
+ "any credentials they carry from every credential-position check. " +
20
+ `Set ${envKey} to a full http(s)://host URL (and put the credential in ${credentialKnob}, never in the URL).`);
21
+ case "userinfo":
22
+ return (`${head} [${GATEWAY_URL_USERINFO_CODE}] — ` +
23
+ "WHATWG forbids constructing a fetch Request from a URL with credentials, so EVERY model call on this route " +
24
+ "fails deterministically at request-construction time (it used to look like a slow network flake: the error was " +
25
+ "retried for ~3 minutes before surfacing). " +
26
+ `Put the credential in ${credentialKnob} (or the matching header knob) instead of the URL, and keep the URL to ` +
27
+ "scheme/host/port/path.");
28
+ case "fragment":
29
+ return (`${head} [${MODEL_ROUTE_URL_FRAGMENT_CODE}] — ` +
30
+ "a fragment is never sent on the wire, so this route would silently target the root's own path and every " +
31
+ "endpoint suffix (/chat/completions, /v1/messages) would land inside the discarded part: the request goes out, " +
32
+ "may even return 200, and points somewhere else without saying so. " +
33
+ `Drop everything from the "#" on in ${envKey}. A per-deployment QUERY is fully supported and rides every ` +
34
+ "request (a gateway's `?api-version=…`); a fragment cannot be.");
30
35
  }
31
- return undefined;
32
36
  }
33
37
  //# sourceMappingURL=model-route-url.js.map
@@ -1,32 +1,28 @@
1
1
  /**
2
- * 规则里的工具名**够不够得着**一只挂载工具 —— 本仓唯一的一问(判官 + 句子各一只),其余全是引擎判官 `judgeRuleToolName` 的判词。
2
+ * 规则里的工具名**够不够得着**一只挂载工具 —— 判官只有一个:引擎的 `judgeRuleToolName`。本文件只剩「规则文本里工具名那一段怎么取」与「判词怎么说」。
3
3
  *
4
- * 一个名字永远匹配不到工具的成因,判官是谁:
5
- * · 退役名 / 无协议前缀的 `__` 短名 / **协议名的对端段引擎铸不出**(`mcp__github.com__*`、`mcp__*`、`mcp____x`、`mcp__`)—— 引擎判官
6
- * (`retired` / `unprefixed_namespace` / `unmintable_segment`,带 `guidance` 与 1:1 `currentName`)。本仓不再拿 `mintNamespacePrefix` 自比对端段:
7
- * 那是同一问的第二个读者(core 7.43.0 #1355 (b) 起引擎自己给这个判词)。
8
- * · **工具段为空的协议名**(`mcp__srv__`)—— 引擎判官答 `current`、规则文法收下、allow 条目读成一条精确名行,而它一只工具都够不着
9
- * (引擎自己的两种拆法 —— 精确名 `ns.parse`、覆盖形 `isNamespacedCoveringRuleName` —— 都不认它)。这是引擎读不到的一形,本仓在下面点名的那些面上
10
- * 拒它({@link emptyToolSegmentOf}),句子在这里({@link describeEmptyToolSegment})。翻转条件:引擎判官给这一形一个判词 ⇒ 本文件整只退役。
4
+ * 一个名字永远匹配不到工具的成因,全是引擎判官的判词(`retired` / `unprefixed_namespace` / `unmintable_segment`,带 `guidance` 与 1:1 `currentName`):
5
+ * 退役名、无协议前缀的 `__` 短名、协议名的对端段引擎铸不出(`mcp__github.com__*`、`mcp__*`、`mcp____x`、`mcp__`),以及(core 7.48.0 #1400 起)
6
+ * **工具段为空**(`mcp__srv__` / `a2a__peer__`)。最后这一形 7.116.0–7.122.x 是本仓自答的(引擎判官当时答 `current`);引擎给了判词 ⇒ 本仓那一问
7
+ * (本文件那只判与它在会话规则写口、SQL 店写口、四张条目表上的调用)退役,零双读。
11
8
  *
12
- * **调它的面**(点名,不是「每个面」;判官一问与本仓这一问同调):
13
- * · 运维三张名表(`APPROVAL_REQUIRE` / `DENY` / `NEVER_AUTO`)与中心下发的同名表、请求 `settings.permissions.{ask,deny}`(经 `config.ts` 的 `unmatchableToolNamesRefusal`);
14
- * · 会话规则写口(`session-policy-wire.ts`)与 SQL 会话规则店的写口(`assertSessionRuleNamesReachable`);
15
- * · 引擎 allow 层的条目表:请求的 `settings.permissions.allow`、运维的 `APPROVAL_GRANT`、技能的两只清单 `allowedTools` / `disallowedTools`(顶层 `skills[]` 与子代
16
- * `agents[].skills[]` 两处受理口)—— 经 `rule-entry-list.ts` 的 `emptyToolSegmentEntryOf`(本仓这一问;退役名 / 铸不出的判词那一半由引擎 `compileAllowList` 给)。
9
+ * **调它的面**(点名,不是「每个面」):运维三张名表(`APPROVAL_REQUIRE` / `DENY` / `NEVER_AUTO`)与中心下发的同名表、请求
10
+ * `settings.permissions.{ask,deny}`(经 `config.ts` 的 `unmatchableToolNamesRefusal`)。会话规则写口直接调 core 的 `assertSessionRuleNamesCurrent`
11
+ * (同一只判官的店形);引擎 allow 层的条目表(请求 `settings.permissions.allow`、运维 `APPROVAL_GRANT`、技能两只清单)交引擎 `compileAllowList`
12
+ * (名册 = {@link NO_DECLARED_TOOL_NAMES};判官判非现役的条目读成 `tool_name`)。
17
13
  * **不调它的面**:`/v1/rules` 的持久规则写口与 `cc-import` 导入口(`http/routes/rules.ts`、`rules-consent.ts` 与两只持久规则店零判官调用)—— 如实未接,
18
- * 在那里写铸不出的服务器段、工具段为空的协议名仍被收下、永不命中;把它接上判官 = 另一件事(不在本文件里加特例;`unmatchableRuleNameOf` 就是现成的那一只)。
14
+ * 在那里写铸不出的服务器段仍被收下、永不命中;把它接上判官 = 另一件事(`unmatchableRuleNameOf` 就是现成的那一只)。
19
15
  * 子代的工具名表 `agents[].allowTools` / `denyTools` 不是本文件的面:core 7.45.0 #1377 / #1385 起名字归引擎在开跑前一只判(三来源同判,`spec-fields.ts`
20
16
  * 受理口只判形与条数),本仓不对它另做对照。
21
17
  * **精确名的工具段归一不在这里判**:包根没有导出工具段的铸法,不手抄 —— 精确名的工具段按字面收,本仓每张表上都不判它。
22
18
  *
23
- * 规则文本里「工具名那一段」怎么取也只有一种读法:{@link ruleEntryToolName}(配置表与技能清单同读)。
19
+ * 规则文本里「工具名那一段」怎么取也只有一种读法:{@link ruleEntryToolName}(配置表读它)。
24
20
  */
25
- import { type ProtocolNamespace, type RuleToolNameVerdict } from "@sema-agent/core";
21
+ import { type RuleToolNameVerdict } from "@sema-agent/core";
26
22
  /**
27
23
  * 一条**规则文本**里的工具名那一段(判官与旧名判据的主语)= core 包根 `parsePermissionRule` 的 `toolName`,原样 —— 引擎的技能编译器
28
24
  * (`compileSkillGrant`)与规则编译器读条目用的就是这一只:`Bash(npm:*)` ⇒ `Bash`;`mcp__srv(*)` / `mcp__srv()` / 带首尾空格的 `mcp__srv ` ⇒ 光杆名 `mcp__srv`。
29
- * 配置表的规则编译器消费者与技能的两只清单读同一只。不在这里另写解析、不回落整串:修前用 `parseRuleText`(只认 `工具(内容)` 形,光杆名解析不出就回落整串),
25
+ * 配置表的规则编译器消费者读它(技能两只清单的名字层 7.118.0 起交引擎的条目读法)。不在这里另写解析、不回落整串:修前用 `parseRuleText`(只认 `工具(内容)` 形,光杆名解析不出就回落整串),
30
26
  * 整串带着 `(*)` / 尾空格进判官 ⇒ 合法条目被判「铸不出」(修复验证轮 gpt-6 腿)。条目若是引擎文法本身就拒的形(配置表上 MCP 规则带括号:
31
27
  * `unsupported.mcp_paren`),拒由引擎的文法那一问说(配置表的判据链里它排在本判官之前),本判官不抢答。
32
28
  */
@@ -36,14 +32,11 @@ export declare function ruleEntryToolName(entry: string): string;
36
32
  * ⇒ 恒为空集。引擎判官、`compileAllowList` 的第三参、会话规则店同读这一只。
37
33
  */
38
34
  export declare const NO_DECLARED_TOOL_NAMES: ReadonlySet<string>;
39
- /** 一个规则名永远匹配不到工具的判词(闭集):引擎判官的三种非现行判词 ∪ 引擎读不到的那一形。 */
35
+ /** 一个规则名永远匹配不到工具的判词(闭集)= 引擎判官的非现行判词。 */
40
36
  export type UnmatchableRuleName = Exclude<RuleToolNameVerdict, {
41
37
  kind: "current";
42
- }> | {
43
- readonly kind: "empty_tool_segment";
44
- readonly ns: ProtocolNamespace;
45
- };
46
- /** 判:引擎判官先答(非 `current` ⇒ 原样交回),判官放行的再问引擎读不到的那一形;都过 ⇒ `undefined`。 */
38
+ }>;
39
+ /** 判:引擎判官非 `current` ⇒ 原样交回;`current` ⇒ `undefined`。 */
47
40
  export declare function unmatchableRuleNameOf(name: string): UnmatchableRuleName | undefined;
48
41
  /**
49
42
  * 判词 → 「为什么永远匹配不到 + 怎么写」(不复述条目本身 —— 调用方的句子已点名它)。引擎判词的 `guidance` 原样带出,是改法指引的**唯一**一句
@@ -51,11 +44,4 @@ export declare function unmatchableRuleNameOf(name: string): UnmatchableRuleName
51
44
  * 无前缀短名的出路从 core 的 `PROTOCOL_TABLE` 派生。闭集穷尽:判词加一种 ⇒ 编译红。
52
45
  */
53
46
  export declare function describeUnmatchableRuleName(v: UnmatchableRuleName, name: string): string;
54
- /**
55
- * 工具段为空的协议名(`<ns>__<peer>__`)⇒ 交回它的协议;别的名字 ⇒ `undefined`。判据只读引擎:判官答 `current`(对端段为空 / 铸不出的那些 ——
56
- * `mcp__`、`mcp____x` —— 是判官自己的判词,不在这里),而引擎自己的两种拆法(精确名 `ns.parse`、覆盖形 `isNamespacedCoveringRuleName`)都不认它。纯字符串运算。
57
- */
58
- export declare function emptyToolSegmentOf(name: string): ProtocolNamespace | undefined;
59
- /** {@link emptyToolSegmentOf} 的句子(「为什么永远匹配不到 + 怎么写」;不复述条目本身 —— 调用方的句子已点名它)。 */
60
- export declare function describeEmptyToolSegment(ns: ProtocolNamespace): string;
61
47
  //# sourceMappingURL=namespaced-rule-name.d.ts.map
@@ -1,4 +1,4 @@
1
- import { PROTOCOL_TABLE, isNamespacedCoveringRuleName, judgeRuleToolName, parsePermissionRule, protocolOf } from "@sema-agent/core";
1
+ import { PROTOCOL_TABLE, judgeRuleToolName, parsePermissionRule } from "@sema-agent/core";
2
2
  import { displayListEntry } from "./list-entry-display.js";
3
3
  export function ruleEntryToolName(entry) {
4
4
  return parsePermissionRule(entry).toolName;
@@ -6,10 +6,7 @@ export function ruleEntryToolName(entry) {
6
6
  export const NO_DECLARED_TOOL_NAMES = new Set();
7
7
  export function unmatchableRuleNameOf(name) {
8
8
  const verdict = judgeRuleToolName(name, NO_DECLARED_TOOL_NAMES);
9
- if (verdict.kind !== "current")
10
- return verdict;
11
- const ns = emptyToolSegmentOf(name);
12
- return ns === undefined ? undefined : { kind: "empty_tool_segment", ns };
9
+ return verdict.kind === "current" ? undefined : verdict;
13
10
  }
14
11
  export function describeUnmatchableRuleName(v, name) {
15
12
  switch (v.kind) {
@@ -19,25 +16,10 @@ export function describeUnmatchableRuleName(v, name) {
19
16
  return v.guidance;
20
17
  case "unprefixed_namespace":
21
18
  return `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => displayListEntry(`${e.prefix}${name}`)).join(" or ")}`;
22
- case "empty_tool_segment":
23
- return describeEmptyToolSegment(v.ns);
24
19
  default: {
25
20
  const unhandled = v;
26
21
  throw new Error(`unhandled rule name verdict: ${JSON.stringify(unhandled)}`);
27
22
  }
28
23
  }
29
24
  }
30
- export function emptyToolSegmentOf(name) {
31
- const ns = protocolOf(name);
32
- if (ns === undefined)
33
- return undefined;
34
- if (judgeRuleToolName(name, NO_DECLARED_TOOL_NAMES).kind !== "current")
35
- return undefined;
36
- if (ns.parse(name) !== undefined || isNamespacedCoveringRuleName(name))
37
- return undefined;
38
- return ns;
39
- }
40
- export function describeEmptyToolSegment(ns) {
41
- return `incomplete ${ns.id} name — the live form is ${ns.prefix}<server>__<tool>, or ${ns.prefix}<server> / ${ns.prefix}<server>__* to cover every tool on that server`;
42
- }
43
25
  //# sourceMappingURL=namespaced-rule-name.js.map
@@ -2,9 +2,22 @@ import type { Logger } from "./logger.js";
2
2
  import type { Metrics } from "./metrics.js";
3
3
  /**
4
4
  * 闭集 tag 词表。形=`<repo>.<domain>.<site>`(跨仓同形,便于三仓遥测并表)。
5
- * 每项 `{ cls, note }`:`cls` 是判据表的分类(见模块头——只有这两类允许走本封装):`F`=纯体验/缓存回退,放行最坏后果只是"难看/不够好";
6
- * `P-DEBT`=保护型欠账,方向明知不对、显式记债。`note` 说清这条兜底放行了什么、最坏后果是什么(写给未来读遥测的人,不写来历)。
5
+ * 每项 `{ cls, note }`:`cls` 是判据表的分类(见模块头——只有这三类允许走本封装):`F`=纯体验/缓存回退,放行最坏后果只是"难看/不够好";
6
+ * `P-DEBT`=保护型欠账,方向明知不对、显式记债;`R`=裁定定界的放行(另带 `ruling` / `bound`)。`note` 说清这条兜底放行了什么、最坏后果是什么
7
+ * (写给未来读遥测的人,不写来历)。
7
8
  */
9
+ export type FailOpenEntry = {
10
+ readonly cls: "F";
11
+ readonly note: string;
12
+ } | {
13
+ readonly cls: "P-DEBT";
14
+ readonly note: string;
15
+ } | {
16
+ readonly cls: "R";
17
+ readonly ruling: string;
18
+ readonly bound: string;
19
+ readonly note: string;
20
+ };
8
21
  export declare const FAIL_OPEN_TAGS: {
9
22
  readonly "server.approval-ask-journal.compaction-failed": {
10
23
  readonly cls: "F";
@@ -394,17 +407,17 @@ export declare const FAIL_OPEN_TAGS: {
394
407
  readonly cls: "F";
395
408
  readonly note: "S-1195(7.119.0;`plugins/snapshot-bucket-presence.ts` 的 `ensureSnapshotBucket`,boot 期核一次快照桶 `SESSION_SNAPSHOT_BUCKET`;detail = `bucket=… endpoint=<buildDisplaySafeUrl 的显示形> <HTTP 状态 | request failed: 错类>`):presigned 列举对 `MINIO_ENDPOINT` **连不上 / 403 / 5xx / 没有 NoSuchBucket 的 404**,或探针到窗尾一次都没答过(detail 写 `probe still pending when the window ended`)⇒ **证明不了桶缺**,不拒启、照常起服(只有 404 + `NoSuchBucket` 才是「缺」,那一形窗内重试、到窗拒启,不走本 tag)。三问:①错误落到哪 —— 本 tag 的计数 / 探针行 / 记录器的一次性 warn,detail 带端点显示形与状态或错类;②谁受害 —— 桶其实不存在、或凭据不能列举的部署:之后每一次快照 offload 写与 SendUserFile 私有 presigned 请求会答 404 / 403,孤儿 sweep 每分钟一条失败,运维读到的是一串失败请求而不是「桶没建」(与 7.118.x 的既有形状同形,不是新损失);③缺席倒向哪边 —— 倒向**放行起服**:没有证据既不拒启、也不伪造「桶在场」或「桶缺」,不是门 / 权限 / 凭证。最坏后果 = 缺桶的部署晚一步才暴露;不放行的代价是对象存储一次抖动 / 一次 403 就让整只服务起不来。计数非零 = 这台部署启动时没能确认快照桶,按 detail 的端点与状态去核对网络 / 凭据 / 桶。";
396
409
  };
397
- readonly "server.config-lkg.landed-path-unresolved": {
398
- readonly cls: "P-DEBT";
399
- readonly note: "7.120.0(`config-lkg.ts` 的 `landedPathOf`,`lkgReadDenyRows` 每次铸部署姿态座席时调;detail = `segment=<那一段的路径>`):`CONFIG_LKG_PATH` 覆写路径上有一段**目录在、却解不开**(悬空符号链接 / 链接环 / 读不了的目录:`realpath` 失败而 `lstat` 不说「不存在」)⇒ 这一段之下 LKG 真正会落在哪说不出来,读拒行只剩声明的拼写那一行(照旧在),真实落点那一行缺席。段**不存在**(`ENOENT` / `ENOTDIR`:还没建出来)是常态,不进这一臂;末段(文件本身)是悬空链接也不进(落盘的 rename 换掉的是那条目录项,文件就落在声明的拼写上)。归 P 的理由:这一行守的是 LKG 文件(含凭据),链接之后通了、LKG 写到链接目标下,模型按目标路径读得到它,直到重启重铸座席。归 DEBT 的理由:引擎没有「锚定单文件」的读拒行、座席只在 Runner 构造时铸一次,本服务说不出将来的落点;拿文件名在任意深度拒会把用户仓里同名文件一并挡掉。计数非零 = 去看 detail 点名的那一段(链接指向哪、卷挂了没有),修好后重启。";
400
- };
401
410
  readonly "server.fleet.serving-lease-unconfirmed": {
402
- readonly cls: "P-DEBT";
403
- readonly note: "7.121.0 修复轮(`fleet/serving-lease.ts` 的 `renewOnce`,租约生效时每一次续租;detail = `worker=<w> generation=<g> why=<不可达句 | verdict=unknown>`):中心这一次**没有确认**本实例该不该服务 —— 不可达 / 超时 / 非 2xx / 答复读不成判决,或答 `unknown`(此刻没有本实例的生命周期对象)。按设计**什么都不改**(规则 2 第三臂):到期之前照常 `granted`,过期后 `expired` 照常接单,直到过期满 `FLEET_LEASE_OUTAGE_MAX_MS` 才拒新工作。放行的最坏后果 = 一只其实已被取代的实例在中心失联期间多服务至多「租约剩余 + `T_max`」(中心的代际下限持久,恢复后第一次续租即拒);不放行的代价是中心抖一下整队停服。归 P 类:围栏轴,最坏后果是作业完整性(被取代的实例多服务),不是难看;归 DEBT:容忍规则本身(`T_max` 的取值与去抖)待裁定,收口条件 = 那次裁定落地。计数持续非零 = 中心长时间没确认本实例,去看 detail 的 why(连不上 / 中心不认识这只实例)。";
411
+ readonly cls: "R";
412
+ readonly ruling: "C-R296";
413
+ readonly bound: "FLEET_LEASE_OUTAGE_MAX_MS";
414
+ readonly note: "`fleet/serving-lease.ts` 的 `renewOnce`(租约生效时每一次续租;detail = `worker=<w> generation=<g> why=<不可达句 | verdict=unknown>`):中心这一次**没有确认**本实例该不该服务 —— 不可达 / 超时 / 非 2xx / 答复读不成判决,或答 `unknown`(此刻没有本实例的生命周期对象)。按设计**什么都不改**(规则 2 第三臂):到期之前照常 `granted`,过期后 `expired` 照常接单,直到过期满 `FLEET_LEASE_OUTAGE_MAX_MS`(`T_max`)才拒新工作(503 `state.lease_expired`,在飞不动、永不自停)。归 R:方向与上界由裁定写定 —— 实例只在中心权威终止(`denied`)或运维覆盖时被替换,`T_max` 只是引擎自己「失联后最多再接单多久」的准入上界,不是替换依据;没确认不是被拒。放行的最坏后果 = 一只其实已被取代的实例在中心失联期间多服务至多「租约剩余 + `T_max`」(中心的代际下限持久,恢复后第一次续租即拒)。计数长期保留:持续非零 = 中心长时间没确认本实例,去看 detail 的 why(连不上 / 中心不认识这只实例)。";
404
415
  };
405
416
  readonly "server.fleet.serving-lease-capability-absent": {
406
- readonly cls: "P-DEBT";
407
- readonly note: "7.121.0 修复轮(`fleet/serving-lease.ts` 的 `noteCenterCapabilities`;fleet 成员每一次中心能力读数;detail = `worker=<w> generation=<g> was=<enabled|none>`):中心答复体读得出、却**没有宣告** `servingLease`(老中心 `{ ok: true }` / 降级中心 / 位撤掉)⇒ 规则 1:租约不生效,`leaseState: none`,零续租、零 503 —— 这只 fleet 成员此刻**不受围栏**。答复体读不出不进这一臂(那是「没有读数」,上一次的位与钟原样,`fleet-client.ts` `capabilityReadingOf`);已被拒的实例不进(拒是黏的,位消失不复活)。放行的最坏后果 = 中心不执行围栏期间,一只被取代的实例照常服务;不放行的代价是老中心 / 降级中心下所有成员在第一次续租就失败。归 P 类:围栏轴,成员此刻不受围栏;归 DEBT:能力门控是契约规则(中心不宣告 = 不执行),收口条件 = 租约容忍规则的那次裁定(含「位从有到无再到有」的去抖)落地。计数非零 = 这台中心没宣告租约能力。";
417
+ readonly cls: "R";
418
+ readonly ruling: "C-R296";
419
+ readonly bound: "FLEET_LEASE_OUTAGE_MAX_MS";
420
+ readonly note: "`fleet/serving-lease.ts` 的 `noteCenterCapabilities`(fleet 成员每一次中心能力读数;detail = `worker=<w> generation=<g> was=<enabled|none>`):中心答复体读得出、却**没有宣告** `servingLease`(老中心 `{ ok: true }` / 降级中心 / 位撤掉)⇒ 规则 1:租约不生效,`leaseState: none`,零续租、零 503 —— 这只 fleet 成员此刻**不受围栏**。答复体读不出不进这一臂(那是「没有读数」,上一次的位与钟原样,`fleet-client.ts` `capabilityReadingOf`);已被拒的实例不进(拒是黏的,位消失不复活)。归 R:能力门控是契约规则(中心不宣告 = 中心不执行围栏),裁定写定替换只来自中心权威终止或运维覆盖;位只开关围栏、不碰钟 —— 位缺席期间 `T_max` 的钟照走(最后一次授予的到期 / 本进程第一次见到位的时刻),位一回来即按它判,所以位的抖动绕不过 `FLEET_LEASE_OUTAGE_MAX_MS`。放行的最坏后果 = 中心不执行围栏期间,一只被取代的实例照常服务(这一段由中心的选择定界,不由引擎)。计数长期保留:非零 = 这台中心没宣告租约能力。";
408
421
  };
409
422
  readonly "server.fleet.announce-reply-unreadable": {
410
423
  readonly cls: "P-DEBT";
@@ -367,17 +367,17 @@ export const FAIL_OPEN_TAGS = {
367
367
  cls: "F",
368
368
  note: "S-1195(7.119.0;`plugins/snapshot-bucket-presence.ts` 的 `ensureSnapshotBucket`,boot 期核一次快照桶 `SESSION_SNAPSHOT_BUCKET`;detail = `bucket=… endpoint=<buildDisplaySafeUrl 的显示形> <HTTP 状态 | request failed: 错类>`):presigned 列举对 `MINIO_ENDPOINT` **连不上 / 403 / 5xx / 没有 NoSuchBucket 的 404**,或探针到窗尾一次都没答过(detail 写 `probe still pending when the window ended`)⇒ **证明不了桶缺**,不拒启、照常起服(只有 404 + `NoSuchBucket` 才是「缺」,那一形窗内重试、到窗拒启,不走本 tag)。三问:①错误落到哪 —— 本 tag 的计数 / 探针行 / 记录器的一次性 warn,detail 带端点显示形与状态或错类;②谁受害 —— 桶其实不存在、或凭据不能列举的部署:之后每一次快照 offload 写与 SendUserFile 私有 presigned 请求会答 404 / 403,孤儿 sweep 每分钟一条失败,运维读到的是一串失败请求而不是「桶没建」(与 7.118.x 的既有形状同形,不是新损失);③缺席倒向哪边 —— 倒向**放行起服**:没有证据既不拒启、也不伪造「桶在场」或「桶缺」,不是门 / 权限 / 凭证。最坏后果 = 缺桶的部署晚一步才暴露;不放行的代价是对象存储一次抖动 / 一次 403 就让整只服务起不来。计数非零 = 这台部署启动时没能确认快照桶,按 detail 的端点与状态去核对网络 / 凭据 / 桶。",
369
369
  },
370
- "server.config-lkg.landed-path-unresolved": {
371
- cls: "P-DEBT",
372
- note: "7.120.0(`config-lkg.ts` 的 `landedPathOf`,`lkgReadDenyRows` 每次铸部署姿态座席时调;detail = `segment=<那一段的路径>`):`CONFIG_LKG_PATH` 覆写路径上有一段**目录在、却解不开**(悬空符号链接 / 链接环 / 读不了的目录:`realpath` 失败而 `lstat` 不说「不存在」)⇒ 这一段之下 LKG 真正会落在哪说不出来,读拒行只剩声明的拼写那一行(照旧在),真实落点那一行缺席。段**不存在**(`ENOENT` / `ENOTDIR`:还没建出来)是常态,不进这一臂;末段(文件本身)是悬空链接也不进(落盘的 rename 换掉的是那条目录项,文件就落在声明的拼写上)。归 P 的理由:这一行守的是 LKG 文件(含凭据),链接之后通了、LKG 写到链接目标下,模型按目标路径读得到它,直到重启重铸座席。归 DEBT 的理由:引擎没有「锚定单文件」的读拒行、座席只在 Runner 构造时铸一次,本服务说不出将来的落点;拿文件名在任意深度拒会把用户仓里同名文件一并挡掉。计数非零 = 去看 detail 点名的那一段(链接指向哪、卷挂了没有),修好后重启。",
373
- },
374
370
  "server.fleet.serving-lease-unconfirmed": {
375
- cls: "P-DEBT",
376
- note: "7.121.0 修复轮(`fleet/serving-lease.ts` 的 `renewOnce`,租约生效时每一次续租;detail = `worker=<w> generation=<g> why=<不可达句 | verdict=unknown>`):中心这一次**没有确认**本实例该不该服务 —— 不可达 / 超时 / 非 2xx / 答复读不成判决,或答 `unknown`(此刻没有本实例的生命周期对象)。按设计**什么都不改**(规则 2 第三臂):到期之前照常 `granted`,过期后 `expired` 照常接单,直到过期满 `FLEET_LEASE_OUTAGE_MAX_MS` 才拒新工作。放行的最坏后果 = 一只其实已被取代的实例在中心失联期间多服务至多「租约剩余 + `T_max`」(中心的代际下限持久,恢复后第一次续租即拒);不放行的代价是中心抖一下整队停服。归 P 类:围栏轴,最坏后果是作业完整性(被取代的实例多服务),不是难看;归 DEBT:容忍规则本身(`T_max` 的取值与去抖)待裁定,收口条件 = 那次裁定落地。计数持续非零 = 中心长时间没确认本实例,去看 detail 的 why(连不上 / 中心不认识这只实例)。",
371
+ cls: "R",
372
+ ruling: "C-R296",
373
+ bound: "FLEET_LEASE_OUTAGE_MAX_MS",
374
+ note: "`fleet/serving-lease.ts` 的 `renewOnce`(租约生效时每一次续租;detail = `worker=<w> generation=<g> why=<不可达句 | verdict=unknown>`):中心这一次**没有确认**本实例该不该服务 —— 不可达 / 超时 / 非 2xx / 答复读不成判决,或答 `unknown`(此刻没有本实例的生命周期对象)。按设计**什么都不改**(规则 2 第三臂):到期之前照常 `granted`,过期后 `expired` 照常接单,直到过期满 `FLEET_LEASE_OUTAGE_MAX_MS`(`T_max`)才拒新工作(503 `state.lease_expired`,在飞不动、永不自停)。归 R:方向与上界由裁定写定 —— 实例只在中心权威终止(`denied`)或运维覆盖时被替换,`T_max` 只是引擎自己「失联后最多再接单多久」的准入上界,不是替换依据;没确认不是被拒。放行的最坏后果 = 一只其实已被取代的实例在中心失联期间多服务至多「租约剩余 + `T_max`」(中心的代际下限持久,恢复后第一次续租即拒)。计数长期保留:持续非零 = 中心长时间没确认本实例,去看 detail 的 why(连不上 / 中心不认识这只实例)。",
377
375
  },
378
376
  "server.fleet.serving-lease-capability-absent": {
379
- cls: "P-DEBT",
380
- note: "7.121.0 修复轮(`fleet/serving-lease.ts` 的 `noteCenterCapabilities`;fleet 成员每一次中心能力读数;detail = `worker=<w> generation=<g> was=<enabled|none>`):中心答复体读得出、却**没有宣告** `servingLease`(老中心 `{ ok: true }` / 降级中心 / 位撤掉)⇒ 规则 1:租约不生效,`leaseState: none`,零续租、零 503 —— 这只 fleet 成员此刻**不受围栏**。答复体读不出不进这一臂(那是「没有读数」,上一次的位与钟原样,`fleet-client.ts` `capabilityReadingOf`);已被拒的实例不进(拒是黏的,位消失不复活)。放行的最坏后果 = 中心不执行围栏期间,一只被取代的实例照常服务;不放行的代价是老中心 / 降级中心下所有成员在第一次续租就失败。归 P 类:围栏轴,成员此刻不受围栏;归 DEBT:能力门控是契约规则(中心不宣告 = 不执行),收口条件 = 租约容忍规则的那次裁定(含「位从有到无再到有」的去抖)落地。计数非零 = 这台中心没宣告租约能力。",
377
+ cls: "R",
378
+ ruling: "C-R296",
379
+ bound: "FLEET_LEASE_OUTAGE_MAX_MS",
380
+ note: "`fleet/serving-lease.ts` 的 `noteCenterCapabilities`(fleet 成员每一次中心能力读数;detail = `worker=<w> generation=<g> was=<enabled|none>`):中心答复体读得出、却**没有宣告** `servingLease`(老中心 `{ ok: true }` / 降级中心 / 位撤掉)⇒ 规则 1:租约不生效,`leaseState: none`,零续租、零 503 —— 这只 fleet 成员此刻**不受围栏**。答复体读不出不进这一臂(那是「没有读数」,上一次的位与钟原样,`fleet-client.ts` `capabilityReadingOf`);已被拒的实例不进(拒是黏的,位消失不复活)。归 R:能力门控是契约规则(中心不宣告 = 中心不执行围栏),裁定写定替换只来自中心权威终止或运维覆盖;位只开关围栏、不碰钟 —— 位缺席期间 `T_max` 的钟照走(最后一次授予的到期 / 本进程第一次见到位的时刻),位一回来即按它判,所以位的抖动绕不过 `FLEET_LEASE_OUTAGE_MAX_MS`。放行的最坏后果 = 中心不执行围栏期间,一只被取代的实例照常服务(这一段由中心的选择定界,不由引擎)。计数长期保留:非零 = 这台中心没宣告租约能力。",
381
381
  },
382
382
  "server.fleet.announce-reply-unreadable": {
383
383
  cls: "P-DEBT",
@@ -173,6 +173,7 @@ export declare class FileApprovalAskStore implements ApprovalAskStore {
173
173
  listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
174
174
  listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
175
175
  listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
176
+ listParkingBySession(sessionId: string): Promise<AskRow[]>;
176
177
  getAsk(askId: string): Promise<AskRow | null>;
177
178
  getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
178
179
  getBatch(batchId: string): Promise<BatchRow | null>;
@@ -256,6 +256,9 @@ export class FileApprovalAskStore {
256
256
  listByCall(call) {
257
257
  return this.core.listByCall(call);
258
258
  }
259
+ listParkingBySession(sessionId) {
260
+ return this.core.listParkingBySession(sessionId);
261
+ }
259
262
  getAsk(askId) {
260
263
  return this.core.getAsk(askId);
261
264
  }
@@ -99,6 +99,7 @@ export declare class InMemoryApprovalAskStore implements ApprovalAskStore {
99
99
  listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
100
100
  listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
101
101
  listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
102
+ listParkingBySession(sessionId: string): Promise<AskRow[]>;
102
103
  getAsk(askId: string): Promise<AskRow | null>;
103
104
  getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
104
105
  resolveProvisional(askId: string, from: AskState, to: AskState, patch: AskTransitionPatch): Promise<boolean>;
@@ -310,6 +310,12 @@ export class InMemoryApprovalAskStore {
310
310
  .sort((a, b) => a.createdAtMs - b.createdAtMs || (a.askId < b.askId ? -1 : a.askId > b.askId ? 1 : 0))
311
311
  .map((r) => ({ ...r }));
312
312
  }
313
+ async listParkingBySession(sessionId) {
314
+ return [...this.asks.values()]
315
+ .filter((r) => r.sessionId === sessionId && r.state === "PARKING")
316
+ .sort((a, b) => a.createdAtMs - b.createdAtMs || (a.askId < b.askId ? -1 : a.askId > b.askId ? 1 : 0))
317
+ .map((r) => ({ ...r }));
318
+ }
313
319
  async getAsk(askId) {
314
320
  const r = this.asks.get(askId);
315
321
  return r ? { ...r } : null;
@@ -478,6 +478,17 @@ export interface ApprovalAskStore {
478
478
  * `(task_id, state)` 的 `task_id` 前缀(一条 run 的行数 = 真出过卡的问数;grant 代答的问不落行),不另加索引。
479
479
  */
480
480
  listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
481
+ /**
482
+ * 一个会话里**仍在 `PARKING`** 的全部行(序 = `created_at_ms ASC, ask_id ASC`,确定性)。
483
+ *
484
+ * 唯一消费者 = 对账收敛器的 park 提交那一拍(`approval-reconciler.ts` 的 `bindCommittedPark`):一只审批停车行提交了,收敛器要对**这次调用**
485
+ * 的 PARKING 行即时跑判据 1。停车行只陈述会话与调用 id(没有 taskId / legKey ⇒ `listByCall` 用不上);`listByState` 带 LIMIT 且按队列序,
486
+ * 新行排在队尾会被截掉。按会话读是**判据 1 能命中的行的超集**:判据 1 的候选读口按 `ask.sessionId` 查停车行,命中要求停车行的会话就是这一列。
487
+ *
488
+ * 不带租户门(内部读口,不上 wire):租户门在判据 1 的候选读口上(scope = 行的 owner)。索引走既有 `idx_ask_session_state (session_id, state)`。
489
+ * 三 twin 同答:SQL twin 读后按会话逐字再滤一遍(MySQL 协议 VARCHAR 等值的 PAD SPACE,与 `listByCall` 同一条出口对齐)。
490
+ */
491
+ listParkingBySession(sessionId: string): Promise<AskRow[]>;
481
492
  getAsk(askId: string): Promise<AskRow | null>;
482
493
  /** 回决幂等回放的读口。🔴 `taskId` **必填**且是键的第一维——UNIQUE 已改 `(task_id, idempotency_key)`
483
494
  * (车4 §12-E),裸按 key 全局查在多租户库上既可能回**别人 task** 的行(存在性外泄),也可能在同 key
@@ -636,6 +647,7 @@ export declare class SqlApprovalAskStore implements ApprovalAskStore {
636
647
  listPendingByTask(taskId: string, signal?: AbortSignal): Promise<AskRow[]>;
637
648
  listPendingBySession(sessionId: string, owner: string | null, signal?: AbortSignal): Promise<AskRow[]>;
638
649
  listByCall(call: AskCallCoordinate): Promise<AskRow[]>;
650
+ listParkingBySession(sessionId: string): Promise<AskRow[]>;
639
651
  getAsk(askId: string): Promise<AskRow | null>;
640
652
  getByIdempotencyKey(taskId: string, key: string): Promise<AskRow | null>;
641
653
  deferReconcile(askId: string, expectedState: AskState, expectedRev: number, nowMs: number): Promise<boolean>;
@@ -679,6 +679,10 @@ export class SqlApprovalAskStore {
679
679
  const { rows } = await this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND leg_key = ? AND tool_call_id = ? AND source_task_id = ? ORDER BY created_at_ms ASC, ask_id ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND leg_key = $2 AND tool_call_id = $3 AND source_task_id = $4 ORDER BY created_at_ms ASC, ask_id ASC`), [call.taskId, call.legKey, call.toolCallId, call.sourceTaskId]);
680
680
  return rows.map(mapAskRow).filter((r) => askRowOnCall(r, call));
681
681
  }
682
+ async listParkingBySession(sessionId) {
683
+ const { rows } = await this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = ? AND state = 'PARKING' ORDER BY created_at_ms ASC, ask_id ASC`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = $1 AND state = 'PARKING' ORDER BY created_at_ms ASC, ask_id ASC`), [sessionId]);
684
+ return rows.map(mapAskRow).filter((r) => r.sessionId === sessionId);
685
+ }
682
686
  async getAsk(askId) {
683
687
  const { rows } = await this.db.query(this.q(`SELECT ${askCols("tidb")} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = ?`, `SELECT ${askCols("pg")} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = $1`), [
684
688
  askId,
@@ -149,6 +149,46 @@ export const ONE_TIME_MIGRATIONS = [
149
149
  "出路 = 运维在库里把那只桶的那一行删掉(整桶 OCC 回写,指纹按 `buildRuleBucketChecksum` 重算)",
150
150
  reportKeys: ["undurableScopeRuleRows", "undurableScopeRuleBucketsDeferred", "undurableScopeRuleBucketsUnreadable"],
151
151
  },
152
+ {
153
+ id: "M12",
154
+ introducedIn: "7.122.0",
155
+ releasedOn: "2026-10-09",
156
+ removableAfter: "2026-11-08",
157
+ removal: "删模式词读法记号的判据与拒臂:`http/stored-body-replay.ts` 的 `permissionModeReadingUnstated`;`http/checkpoint-ctx.ts` 的 `permissionModeReadingStated` / `permissionModeReadingRefusal` / `PERMISSION_MODE_READINGS`、" +
158
+ "`StoredBodyReplayRefusal` 的 `permission_mode_reading` 臂及其日志字段与答复臂、`buildCheckpointCtx` 与 `withPermissionModeRewritten` 两处记号写点" +
159
+ "(后者删后 `resume-legs.ts` `drivePlanReview` 里对它的那一处调用回到只换体的 `{ ...ctx, body }`)、`CheckpointCtx.permissionModeReading`、" +
160
+ "`http/stored-body-replay.ts` `storedBodyReplayRefusal` 的 `autonomy` 形参(只服务这一问)与四个问点(`resume-legs.ts` 两处、`approval-sweeps.ts`、`routes/runs.ts`)传 `deps.config.autonomy` 的那一句",
161
+ afterRemoval: "没有响亮臂:删码之后仍停驻、在 7.121.x 及以前写下的行(把 `APPROVAL_TERMINAL_BACKSTOP_MS` 调长的部署,或跳过了带本条的版本直接升的库)在设了 " +
162
+ "`AUTONOMY=plan` 的部署上按体里的原词重放(运维的 plan 不再被按段重钳)—— 升级前把这些停驻的 run 决掉 / 取消,或升级窗内设 `AUTONOMY=read-only`",
163
+ reportKeys: [],
164
+ },
165
+ {
166
+ id: "M13",
167
+ introducedIn: "7.123.0",
168
+ releasedOn: "2026-10-10",
169
+ removableAfter: "2026-11-09",
170
+ removal: "删 `config-lkg.ts` 的 formatVersion 2 读回迁移:`migrateV2Lkg` / `migrateDroppedIdentityRow` / `RETIRED_CATALOG_COORDINATE` 与其上的窗注;`loadLkg` 的 2 臂" +
171
+ "(版本判据里的 `2` 与 `formatVersion === 2 ? migrateV2Lkg(…)` 那一支 —— 2 回到「unknown formatVersion」拒)、返回型的 `migrated` 位与 JSDoc 里说它的那一行、" +
172
+ "文件头注「2 读回时一次性迁移」那一句;`boot/config-center.ts` 读 `read.migrated` 打 `config_lkg_migrated` 的那一行(formatVersion 1 迁移的同名事件那一行留);" +
173
+ "`migrateV1Lkg` 里不带 v1 的 etag 那一句旁注对 `migrateV2Lkg` 的引用(不带 etag 的规则本身留 —— v1 载荷同样没有完整的被丢条目记录);契约 §A.1d " +
174
+ "`droppedEntries` 一节「升级窗(如实)」那一条与 §A.1d `[config.model_refused_name_minted]` 那一条里「LKG 载体 `droppedIdentities` 读回时一次性迁移」那半句;手写 formatVersion 2 信封的测试夹具" +
175
+ "(`test/config-lkg.test.ts`、`test/config-hot/batch1-version-ordinal.test.ts` 改写成 3;`test/models-dropped-entries-s1295.test.ts` 的旧 LKG 迁移格改钉 2 读回不可用)",
176
+ afterRemoval: "既有响亮臂 `config_lkg_unusable`(`loadLkg` 对 2 答「unknown formatVersion 2」):2 读回 ⇒ 不可用,中心可达照拉(整拉,读腿铸出完整载体,新 LKG 按 3 覆盖)、" +
177
+ "断着则 env 兜底,下一份干净候选重写 —— 与回滚到 7.122.0 读 3 同一种处置",
178
+ reportKeys: [],
179
+ },
180
+ {
181
+ id: "M14",
182
+ introducedIn: "7.99.0",
183
+ releasedOn: "2026-09-24",
184
+ removableAfter: "2026-10-24",
185
+ removal: "删 `config-lkg.ts` 的 formatVersion 1 迁移:`legacyV1LkgPath` / `migrateV1Lkg` 与其上的窗注(`loadLkg` 的 1 臂今天已是拒句,不动);" +
186
+ "`boot/config-center.ts` `bootFromLkg` 里调用 `migrateV1Lkg` 的那一段(含它打 `config_lkg_migrated` / 推不出身份时响亮拒的那两行);" +
187
+ "`migrateV1Lkg` 的测试格(`test/config-lkg.test.ts` v1 迁移组)",
188
+ afterRemoval: "既有响亮臂 `config_lkg_unusable`(`loadLkg` 对 1 答「pre-identity LKG (formatVersion 1) — not read back …」):旧默认路径上的 v1 文件不再被迁进箱、" +
189
+ "原样留在盘上;中心可达照拉(整拉,新 LKG 按现行箱名落盘)、断着则 env 兜底",
190
+ reportKeys: [],
191
+ },
152
192
  ];
153
193
  export function migrationWindowOpen(window, today) {
154
194
  return window.removableAfter === null || today <= window.removableAfter;
@@ -7,7 +7,7 @@
7
7
  * 「park 落盘」,有序关停若把前者当后者,就会在 `put` 之下关掉连接池(修前病:park 行没提交,审计账本却已记「结清」)。
8
8
  *
9
9
  * 本模块给店加**一层**装饰(两店同用,不读 core 私有键 —— [ref]):
10
- * ① **计在飞 `put`**:`quiesce(deadlineMs)` 等到零或到期(绝对时刻,`Date.now()` 域),如实返回残余数 —— 收尾链在
10
+ * ① **计在飞 `put`**(连同订阅者在这一拍的工作,见下):`quiesce(deadlineMs)` 等到零或到期(绝对时刻,`Date.now()` 域),如实返回残余数 —— 收尾链在
11
11
  * `backend.close()` 之前等它(`boot/shutdown.ts`),审批协调器的关停收尾也等它(`ToolApprovalCoordinator.drainForShutdown`);
12
12
  * ② **报 park 提交** —— 「这只店里有了这一行」的两种观察,同一件事实:一次 `put` 落定(resolve);或一次 `put` 抛错之后,同一 token 的
13
13
  * `get` 读到了行(写已落、回执丢了 —— core 的挂起序正是用这一次回读确认提交的,本层观察的就是那一次读,不另读;车内 codex r1)。
@@ -15,8 +15,14 @@
15
15
  * branch on `kind` first」)⇒ 把**停车行自己陈述的**调用身份交给订阅者({@link CommittedParkCalls})。抛错且回读不到 = 没提交,不报。
16
16
  * 身份取自**读到的那一行**(不是本进程想写的那一只):哪怕 token 撞了别人的行,报出去的也只是那一行真握着的调用。
17
17
  *
18
- * 订阅者契约:**不得抛**。订阅者跑在 `put` 已提交之后、返回给 core 之前 —— 一只抛错的订阅者会把一次**已提交**的 park 报成失败。
19
- * 唯一的订阅者是审批协调器(写审计 settle 行,那只 sink 自吞失败并按 [ref] 留痕),它在自己那一侧兜住(同 tag 扩射程,见协调器)。
18
+ * 订阅者契约:**不得抛、不得拒**。订阅者跑在 `put` 已提交之后、返回给 core 之前,`put` **等它跑完**才返回 —— 一只抛错 / 拒绝的订阅者会把一次
19
+ * **已提交**的 park 报成失败;一只不收尾的订阅者会把 core 的停驻挂住(订阅者自己给自己的工作上墙钟上限)。
20
+ * 两个订阅者,各管一件「这只停车行提交了」之后的事:
21
+ * · 审批协调器 —— 有序关停下写候着的审计 settle 行(sink 自吞失败并按 [ref] 留痕,同 tag 扩射程,见协调器);
22
+ * · 对账收敛器 —— S-1280:这次调用的 `PARKING` 行在这一拍收口成 `PARKED`(判据 1 那一只判官的即时调用,`approval-reconciler.ts`
23
+ * `bindCommittedPark`;它自带墙钟预算、失败只留痕,周期扫兜底)。`put` 等它(至多到它的预算)⇒ 健康时 core 的停驻返回之时行已收口;
24
+ * 预算到点它就交还 `put`、不再发新的店调用,已发出的那一次经 {@link ParkCommitWatch.track} 留在在飞里 ⇒ 收尾链的 `quiesce`
25
+ * 在 `backend.close()` 之前等到它落定。
20
26
  */
21
27
  import type { CheckpointStore } from "@sema-agent/core";
22
28
  /**
@@ -33,9 +39,17 @@ export interface CommittedParkCalls {
33
39
  readonly toolCallIds: readonly string[];
34
40
  }
35
41
  /** 装饰层的观察面(`create*` 产出的活对象的一半;另一半是装饰后的店本身)。 */
42
+ /** park 提交的订阅者(见模块头「订阅者契约」:不得抛、不得拒;异步的那一只 `put` 等它跑完)。 */
43
+ export type ParkCommittedListener = (calls: CommittedParkCalls) => void | Promise<void>;
36
44
  export interface ParkCommitWatch {
37
- /** 订阅 park 提交(见模块头「订阅者契约」:不得抛)。 */
38
- onParkCommitted(listener: (calls: CommittedParkCalls) => void): void;
45
+ /** 订阅 park 提交(见模块头「订阅者契约」)。 */
46
+ onParkCommitted(listener: ParkCommittedListener): void;
47
+ /**
48
+ * 把订阅者在 `put` 交还之后**仍可能在跑**的那段工作记进在飞:`quiesce` 等它落定(成功或失败都算落定),返回同一个 promise。
49
+ * 用途(S-1280):订阅者给自己的工作上了墙钟预算,到点就把 `put` 交还 core —— 而此刻已经发出的那次店调用还没回来;不记进来,
50
+ * 收尾链会读到「静默」并在它底下关池。
51
+ */
52
+ track<T>(work: Promise<T>): Promise<T>;
39
53
  /** 等在飞 `put` 归零或到 `deadlineMs`(绝对时刻,`Date.now()` 域);返回那一刻的残余在飞数(0 = 静默了)。从不抛。 */
40
54
  quiesce(deadlineMs: number): Promise<{
41
55
  inFlight: number;