@sema-agent/server 7.5.0 → 7.7.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 (104) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +18 -3
  3. package/README.zh-CN.md +14 -3
  4. package/USAGE.md +100 -2
  5. package/dist/approval-reconciler.d.ts +1 -1
  6. package/dist/approval-reconciler.js +6 -5
  7. package/dist/boot/budget-tracing.js +1 -1
  8. package/dist/boot/config-center.js +17 -3
  9. package/dist/boot/execution-env.js +1 -1
  10. package/dist/boot/parked-revive-gate.d.ts +78 -0
  11. package/dist/boot/parked-revive-gate.js +114 -0
  12. package/dist/boot/resolve-spec.d.ts +8 -20
  13. package/dist/boot/resolve-spec.js +40 -125
  14. package/dist/budget.d.ts +1 -1
  15. package/dist/budget.js +1 -1
  16. package/dist/capabilities/hands-lane.d.ts +91 -0
  17. package/dist/capabilities/hands-lane.js +104 -0
  18. package/dist/capabilities/repo-tools.d.ts +1 -1
  19. package/dist/capabilities/repo-tools.js +8 -2
  20. package/dist/capabilities/scenarios.d.ts +49 -2
  21. package/dist/capabilities/scenarios.js +84 -16
  22. package/dist/config-provider.d.ts +1 -0
  23. package/dist/config-provider.js +23 -3
  24. package/dist/config-types.d.ts +5 -3
  25. package/dist/config.js +5 -4
  26. package/dist/deployment-governance.d.ts +168 -0
  27. package/dist/deployment-governance.js +206 -0
  28. package/dist/fleet/fleet-bus.d.ts +11 -1
  29. package/dist/fleet/fleet-bus.js +43 -0
  30. package/dist/fleet/fleet-terminal-window.d.ts +98 -0
  31. package/dist/fleet/fleet-terminal-window.js +319 -0
  32. package/dist/hooks/hook-runner.js +11 -0
  33. package/dist/http/route-ctx.d.ts +7 -1
  34. package/dist/http/routes/approvals-assistant.js +6 -7
  35. package/dist/http/routes/fleet.js +164 -14
  36. package/dist/http/routes/runs.js +7 -2
  37. package/dist/http/routes/tasks.js +5 -4
  38. package/dist/http/routes/trace-usage.js +3 -4
  39. package/dist/http/send.d.ts +23 -0
  40. package/dist/http/send.js +23 -0
  41. package/dist/http/server.d.ts +10 -0
  42. package/dist/http/server.js +28 -12
  43. package/dist/http/sse-log.js +3 -4
  44. package/dist/leader/diffout.d.ts +10 -0
  45. package/dist/leader/diffout.js +14 -2
  46. package/dist/leader/diffup.js +3 -2
  47. package/dist/leader/planner.js +7 -0
  48. package/dist/main.js +79 -32
  49. package/dist/observability/fail-open.d.ts +4 -0
  50. package/dist/observability/fail-open.js +4 -0
  51. package/dist/observability/tool-trace.d.ts +13 -0
  52. package/dist/observability/tool-trace.js +14 -0
  53. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  54. package/dist/orchestration/workflow-notify-journal.js +14 -34
  55. package/dist/plugins/approval-ask-store-sql.d.ts +33 -0
  56. package/dist/plugins/approval-ask-store-sql.js +66 -33
  57. package/dist/plugins/background-agent-store-sql.js +16 -16
  58. package/dist/plugins/breaker-state-sql.js +2 -2
  59. package/dist/plugins/checkpoint-store-sql.d.ts +5 -2
  60. package/dist/plugins/checkpoint-store-sql.js +5 -2
  61. package/dist/plugins/image-bake-store-sql.d.ts +1 -1
  62. package/dist/plugins/image-bake-store-sql.js +27 -27
  63. package/dist/plugins/image-index-sql.js +15 -15
  64. package/dist/plugins/mailbox-store-sql.js +3 -3
  65. package/dist/plugins/memory-engine-pg.js +9 -9
  66. package/dist/plugins/memory-engine-tidb.js +7 -7
  67. package/dist/plugins/memory-sync-store-pg.js +13 -13
  68. package/dist/plugins/memory-sync-store-tidb.js +5 -5
  69. package/dist/plugins/outcome-ledger-sql.js +7 -7
  70. package/dist/plugins/pg-cost-quota.js +3 -3
  71. package/dist/plugins/pg-pool.js +84 -75
  72. package/dist/plugins/pg-rate-limiter.js +3 -3
  73. package/dist/plugins/pg-session-storage.d.ts +1 -1
  74. package/dist/plugins/pg-session-storage.js +12 -13
  75. package/dist/plugins/remote-env-host.js +3 -1
  76. package/dist/plugins/remote-env-local-docker.js +6 -3
  77. package/dist/plugins/remote-env-ssh.d.ts +13 -1
  78. package/dist/plugins/roster-store-sql.js +8 -8
  79. package/dist/plugins/store-contracts.d.ts +19 -0
  80. package/dist/plugins/store-contracts.js +42 -0
  81. package/dist/plugins/task-attachment-store.js +5 -5
  82. package/dist/plugins/task-list-store-sql.js +1 -1
  83. package/dist/plugins/tidb-cost-quota.js +1 -1
  84. package/dist/plugins/tidb-pool.js +83 -60
  85. package/dist/plugins/tidb-rate-limiter.js +1 -1
  86. package/dist/plugins/tidb-session-store.js +2 -5
  87. package/dist/plugins/tool-result-store-sql.js +2 -2
  88. package/dist/plugins/usage-window-store-sql.js +13 -13
  89. package/dist/plugins/write-behind-counter.d.ts +10 -2
  90. package/dist/plugins/write-behind-counter.js +13 -3
  91. package/dist/resource-suspend.d.ts +3 -1
  92. package/dist/resource-suspend.js +3 -1
  93. package/dist/run-local.d.ts +73 -1
  94. package/dist/run-local.js +160 -9
  95. package/dist/runs.d.ts +3 -1
  96. package/dist/runs.js +3 -1
  97. package/dist/security.d.ts +12 -0
  98. package/dist/security.js +12 -0
  99. package/dist/session-sync-kernel.d.ts +13 -0
  100. package/dist/session-sync-kernel.js +13 -0
  101. package/dist/trace/core-keyset-guard.d.ts +1 -1
  102. package/dist/trace/project.d.ts +10 -1
  103. package/dist/trace/project.js +31 -0
  104. package/package.json +3 -3
@@ -1,6 +1,18 @@
1
- const DEFAULT_MAX_DIFF_BYTES = 5 * 1024 * 1024;
1
+ /**
2
+ * 一次 diff 的字节帽(pull 腿与 push/上行腿**共用同一个数**)。
3
+ *
4
+ * 🔴 exported 是判据的一部分(2026-08-08,#176 二轮扫描 finding):`diffup.ts` 原先另抄了一份同值常量,
5
+ * 而它**本就已经** `import { safeRev } from "./diffout.js"` —— 依赖边在,共享零成本。两份手抄的后果不是
6
+ * 「多写一行」,是**单边改值后上行/pull 两腿对同一个 diff 的过/拒判定分家**:同一份补丁在一条腿上过、
7
+ * 另一条腿上被截断,而两处都「按自己的常量」正确工作,没有任何一处会报错。
8
+ * (同族先例:`checkpoint-store-sql.ts` 的 `export const TERMINAL_GRACE_MS`。)
9
+ */
10
+ export const DEFAULT_MAX_DIFF_BYTES = 5 * 1024 * 1024;
2
11
  const DEFAULT_READ_TIMEOUT_MS = 30_000;
3
- /** A safe git rev token: sha, branch/tag name, or `A..B`. No shell metacharacters, no leading dash. */
12
+ /** A safe git rev token: sha, branch/tag name, or `A..B`. No shell metacharacters, no leading dash.
13
+ * 🔗 近亲判据:`leader/planner.ts` 的 branch 校验用同一套字符类 + 同一个 200 上限 + 同样拒前导 `-`,
14
+ * 只在 `..` 上有意分家(这里要放行 `A..B` 区间形,那边全拒)。差异与候裁说明写在 planner 那处
15
+ * (#176 二轮扫描 finding)——单边加字符类成员时,请两处一起看。 */
4
16
  const SAFE_REV = /^[A-Za-z0-9._/-]{1,200}$/;
5
17
  export function safeRev(rev) {
6
18
  return SAFE_REV.test(rev) && !rev.startsWith("-") && !rev.includes("..--");
@@ -14,9 +14,10 @@
14
14
  * A worker that skips the upload surfaces as `{ok:false}` → merge quarantines it (honest failure, no hang).
15
15
  * pullDiff stays as the compat path for static (leader-owned) envs — M2 降兼容.
16
16
  */
17
- import { safeRev } from "./diffout.js";
17
+ // 字节帽与 `safeRev` 同源取自 pull 腿(理由见 diffout.ts 的 DEFAULT_MAX_DIFF_BYTES 顶注:同值双抄一旦
18
+ // 单边改值,上行/pull 两腿对同一个 diff 的过/拒判定就分家,且两边都「按自己的常量」静默正确)。
19
+ import { safeRev, DEFAULT_MAX_DIFF_BYTES } from "./diffout.js";
18
20
  import { OBJECT_STORE_IO_TIMEOUT_MS } from "../plugins/blob-backend.js";
19
- const DEFAULT_MAX_DIFF_BYTES = 5 * 1024 * 1024;
20
21
  /** The staged upload script. `repoDir`/`workDir` are deployment config; `baseSha` is interpolated into a
21
22
  * shell line, so it is re-validated HERE (fail-closed) — callers should already have applied diffout's
22
23
  * safeRev rules, but this function is the last line before the shell. The patch is written OUTSIDE the repo
@@ -91,6 +91,13 @@ export function validateSubtasks(arr, opts = {}) {
91
91
  const branch = typeof e.branch === "string" && e.branch.trim() ? e.branch.trim() : `worker/${workerId}`;
92
92
  // Reject a leading "-" (would be parsed as an option by `git checkout -b <branch>`), ".." (ref escape), and
93
93
  // an over-long ref (git rejects it anyway; cap keeps the value tidy). The char set is shell-safe.
94
+ //
95
+ // 🔗 近亲判据(2026-08-08,#176 二轮扫描 finding):`leader/diffout.ts` 的 `safeRev` 用**同一套**字符
96
+ // 类 `[A-Za-z0-9._/-]`、**同一个** 200 长度上限、**同样**拒前导 `-`。唯一的真差异在 `..`:safeRev 只拒
97
+ // `..--`(rev 要支持 `A..B` 区间形),这里全拒 `..`(branch **名**里 `..` 无合法用途,git 自己也拒)
98
+ // —— 是**有意收紧**,不是抄漏。两处此前零互指、无共享基元、无跨处钉;单边加一个字符类成员时另一边
99
+ // 不会有任何提示。是否抽一个共享基元(比如 `refToken(s, { allowRange })`)属结构面决定,候裁;本批
100
+ // 只把「近亲关系 + 差异所在」两侧成文。
94
101
  if (branch.length > 200 || !/^[A-Za-z0-9._/-]+$/.test(branch) || branch.startsWith("-") || branch.includes("..")) {
95
102
  throw new Error(`sub-task ${workerId}: invalid branch`);
96
103
  }
package/dist/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { Runner, TtlSessionStore, uuidv7, defaultTaskRegistry, combinePolicies, createAllowDenyPolicy, workflowsCapability, createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel, probeSearchBackend, describeStaticWiring } from "@sema-agent/core";
4
+ import { Runner, InMemoryToolResultStore, TtlSessionStore, uuidv7, defaultTaskRegistry, createAllowDenyPolicy, workflowsCapability, createWebFetchSummarizer, resolveTaskModel as coreResolveTaskModel, probeSearchBackend, describeStaticWiring } from "@sema-agent/core";
5
5
  import { createSessionTitler } from "./session-titler.js";
6
6
  import { posIntEnv } from "./session-watch.js";
7
7
  import { selectEnvironmentTool } from "./capabilities/select-environment-tool.js";
@@ -14,10 +14,11 @@ import { ensureChildSessionDurableWithPromotion } from "./plugins/session-store.
14
14
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
15
15
  import { webSearchConfigFromEnv, createWebSearchBackend, setWebSearchBadPayloadObserver, shouldProbeWebSearchOnBoot } from "./plugins/web-search.js";
16
16
  import { createAuthorizer, encodeCheckpointScope } from "./security.js";
17
- import { assertGateIntentServiceable, createDurableAskPolicy, hasOperatorGateIntent } from "./approval.js";
17
+ import { assertGateIntentServiceable, hasOperatorGateIntent } from "./approval.js";
18
18
  import { loadSkills } from "./capabilities/skills.js";
19
19
  import { GiteaClient } from "./capabilities/repo-tools.js";
20
20
  import { buildScenarios, builtinScenarioDetails } from "./capabilities/scenarios.js";
21
+ import { createHandsLaneRegistry, pickHandsRunner, withoutExecutionEnv } from "./capabilities/hands-lane.js";
21
22
  import { createLogger } from "./observability/logger.js";
22
23
  import { createToolTracer, createPermissionDeniedMeter } from "./observability/tool-trace.js";
23
24
  import { createRegistryJwtVerifier } from "./auth-bridge.js";
@@ -38,7 +39,8 @@ import { createHookLlm } from "./hooks/hook-llm.js";
38
39
  import { startFleetClientFromEnv } from "./fleet-client.js";
39
40
  import { reclaimOrphansAtBoot } from "./boot-reclaim.js";
40
41
  import { createConfigCenterRuntime } from "./boot/config-center.js";
41
- import { createDurableQuestionGate, createResolveSpec } from "./boot/resolve-spec.js";
42
+ import { createResolveSpec } from "./boot/resolve-spec.js";
43
+ import { createParkedReviveInheritedGate } from "./boot/parked-revive-gate.js";
42
44
  import { startReapers } from "./boot/reapers.js";
43
45
  import { openStores } from "./boot/stores.js";
44
46
  import { createBudgetAndTracing } from "./boot/budget-tracing.js";
@@ -192,16 +194,54 @@ async function main() {
192
194
  const { principalCaps, centerRuntimeCapsResolver, runtimeCapsResolver } = createRuntimeCaps({ config, logger });
193
195
  // design/170 件A(#148 件3③):org 记忆准入装配(目录源三态选择+C12 能力探测,坏配置在此拒启动)。
194
196
  const orgMemoryAdmission = createOrgMemoryAdmissionWiring({ config, logger, metrics });
197
+ // 🔴 codex 复审(#196 finding-2b):`toolResultStore` 在**无 backend** 形下是 undefined,而 core 的
198
+ // Runner 构造函数会在缺席时**每只各自私建**一份 `RunnerSharedToolResultStore`(runtask.js
199
+ // `if (!this.deps.toolResultStore)`)。本部署有多只 Runner(主 / subRunner / hookAgent / #196 的两只无手
200
+ // 孪生),私建 ⇒ 每只一个独立店 ⇒ A 车道 offload 出去的 `ReadToolResult` 引用在 B 车道解引用不到
201
+ // (fork 子任务跨 runner 读父的 offload 是既有可达形,#196 的车道分家又多了一条)。这里显式铸**一份**
202
+ // 共享兜底(参数与 core 私建那只逐字相同:`RunnerSharedToolResultStore` 是 `InMemoryToolResultStore`
203
+ // 的空子类,cap 64M chars),只喂 Runner 侧。
204
+ // ⚠️ 刻意**不动** `toolResultStore` 本身:那个键的「present ⇔ durable 后端在场」语义还管着 E21 purge、
205
+ // reaper 清扫、leader 装配与 /health 的接线自述(改它会让无 backend 的部署自述成 "shared(sql)" = 谎)。
206
+ const runnerOffloadStore = toolResultStore ?? new InMemoryToolResultStore({ maxTotalChars: 64_000_000 });
195
207
  // design/158 A10:RunnerDeps 装配段搬到 src/boot/runner-deps.ts(逐字;runStore 晚绑改取值,见该文件头注)。
196
208
  const runnerDeps = createRunnerDeps({
197
209
  config, logger, metrics, localRoot, promptSource: configCenter.promptSource, rosterStore, backgroundAgentStore, mailboxStore, usageWindowStore, brain,
198
210
  pricing, tracer, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine,
199
- memorySyncRunner, toolResultStore, sessionPolicyStore, runtimeCapsResolver, fileSnapshotStore,
211
+ memorySyncRunner, toolResultStore: runnerOffloadStore, sessionPolicyStore, runtimeCapsResolver, fileSnapshotStore,
200
212
  executionEnvFactory, lspManager, fleetBus, deploymentHooks, workflowRunStore, workflowJournalStore,
201
213
  workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, orgMemoryAdmission,
202
214
  getRunStore: () => runStore,
203
215
  });
204
216
  const runner = new Runner(runnerDeps);
217
+ // #196:`runner` 的无手孪生 —— 同 deps 同实例(sessionStore/toolResultStore/checkpointStore… 全共享,
218
+ // `Runner.sessions` 就是传进去的那只 store,两只 Runner 不会各持一份会话缓存),唯独不挂
219
+ // `executionEnvFactory`。core 的 `handsEnabled = ownedEnv || deps.executionEnv` 是 **per-Runner** 合同,
220
+ // 单 runner 服务全场景时结构上无法逐场景兑现 —— 这只孪生就是兑现它的那一半(设计小票 §2a)。
221
+ // 声明 hands:"none" 的场景(scan / code-review 两腿 / team / center overlay)在 HTTP 执行点被路由到这里。
222
+ //
223
+ // 🔴 两条**已知边界**(codex 复审 2026-08-09 两轮提出,逐条亲验 core dist 后如实记账;两条的部署条件
224
+ // 互不相同 —— 不要把它们并成一句)。
225
+ //
226
+ // ① 会话锁是 **per-Runner** 的(core runtask.js 的 `sessionLocks` Map,每只 Runner 一份)。
227
+ // **条件:无 store backend 的部署形**。有 runStore 时同会话并发提交被服务端的 run 认领挡成 409
228
+ // (routes/tasks.ts:317 / routes/runs.ts:335 的 `createRun` 认领),两车道并发结构上不可达;无
229
+ // backend 的部署没有那道认领,同一 sessionId 上「一条 full + 一条 none」并发就失去了修前由单
230
+ // Runner 提供的串行化。同类缺口本就存在(subRunner 经 ForkRoutingSessionStore 也会碰宿主会话),
231
+ // 本件让它多一条可达路径。补偿:配任一 backend(含 `DB_BACKEND=local`)即恢复认领。
232
+ //
233
+ // ② 升级窗的旧 checkpoint。**条件恰恰相反:必须有 checkpointStore**(= 有 backend + DURABLE_APPROVAL),
234
+ // 且部署在 REMOTE_EXEC 车道上 —— 修前建立的 hands=none 场景 durable checkpoint 会带 workspaceHandle
235
+ // (可挂起 env 的快照形,以及**非**可挂起远端 env 的 `restoreMode:"park_only"` 形,两者都算;审批 park
236
+ // 与 resource suspend 都会写),续跑时被路由到这只无手 Runner,core 在 **CAS 之前**抛
237
+ // `CheckpointError("checkpoint.unsupported_version", "checkpoint has a remote workspaceHandle but no
238
+ // RunnerDeps.executionEnvFactory is wired to rebuild the env")`(runtask.js:3704)。
239
+ // 本仓的分类表把该码归 **TERMINAL**(http/server.ts 的 CheckpointError 分支):run 行被 `setTerminal`
240
+ // 成 failed 并**释放** task_active,响应 409 携带该 errorCode —— 会话**不会**被占住到 TTL,运维重提该
241
+ // 任务即可。故这是**响亮的、有界的**升级窗代价,不是静默损坏;但它是行为面的升级注记,发车说明必须写。
242
+ // ⚠️ 未覆盖登记:本形需要「真 durable 店 + 带 workspaceHandle 的旧 checkpoint」才能驱动,本批未建格
243
+ // (属真双库/迁移测试面)—— 移交主会话裁定是否要做 checkpoint 感知的续跑选路或启动期预检。
244
+ const handslessRunner = new Runner(withoutExecutionEnv(runnerDeps));
205
245
  // codex R10: TRUE ⇒ the Runner just froze a PRIVATE tier-expanded catalog copy (core runtask.js constructor,
206
246
  // dist-read) — in-place model-plane mutation no longer reaches it, so refresh-time plane changes must be
207
247
  // DEFERRED to restart (see appliedPlaneEff / applyEffective deferModelPlane). Captured HERE, at the same
@@ -320,7 +360,7 @@ async function main() {
320
360
  // (子会话住私有 TTL 店,park 前整树拷进 host durable 店——只查不迁的 1.263 形在拆店生产形下恒
321
361
  // 否决 park,cli 真机 14ms~2s expired 即此)。
322
362
  const subRunnerSessions = new ForkRoutingSessionStore(sessionStore, new TtlSessionStore({ defaultTtlDays: 1 / 24 }));
323
- const subRunner = new Runner({
363
+ const subRunnerDeps = {
324
364
  // [1543]§三族A 结构性根治(design/158 冲刺尾件):与主 runnerDeps 重复的 ~15 键统一走
325
365
  // createSharedRunnerDeps 展开——「双点挂载」从人工纪律变结构性保证(新共享键漏配其一=不可能,
326
366
  // 基座只有一份)。差异键在展开后显式列出,每个都有为何不同的理由(见 boot/runner-deps.ts 头注)。
@@ -336,7 +376,7 @@ async function main() {
336
376
  mailboxStore,
337
377
  rosterStore,
338
378
  deploymentHooks,
339
- toolResultStore,
379
+ toolResultStore: runnerOffloadStore,
340
380
  sessionPolicyStore,
341
381
  usageWindowStore,
342
382
  orgMemoryAdmission,
@@ -358,7 +398,13 @@ async function main() {
358
398
  // TaskStop on parked 走 expire CAS 仲裁(1.389 F-1);/decide 可赎回。行为钉 =
359
399
  // test/subagent-park-wiring.test.ts(SOURCE PIN + park 全链 + 旧形反向控制)。
360
400
  checkpointStore: checkpointStore ? checkpointStore : undefined,
361
- });
401
+ };
402
+ const subRunner = new Runner(subRunnerDeps);
403
+ // #196:`subRunner` 的无手孪生(同一份 deps 字面量摘掉 executionEnvFactory —— 差异面只有这一处,
404
+ // 结构上不可能漂移)。council 的 lens/arbiter、team 的成员/synthesizer 走它:那些子任务的人格声明
405
+ // 里一件工具都没有,却在修前照拿全量可写手(设计小票 §1 里最意外的一支)。
406
+ // ⚠️ 与主 subRunner 共享 `subRunnerSessions` 与 `checkpointStore` 同实例 —— park/fork 的行是同一批。
407
+ const handslessSubRunner = new Runner(withoutExecutionEnv(subRunnerDeps));
362
408
  // S6: resolve identity + owned session from the authenticated channel (never the body).
363
409
  const authorize = createAuthorizer(config, sessionStore);
364
410
  // S1/S2: durable async run registry + replayable event log (TiDB-backed).
@@ -597,7 +643,7 @@ async function main() {
597
643
  // design/158 S4:标注 `: ScenarioDeps` —— 无标注的字面量连自己的键名都不校验(打错/多写一个键
598
644
  // 只是多一个没人读的属性,编译期无声),这是 1.304 落错家那一族的另一半土壤。
599
645
  const scenarioDeps = {
600
- runner, subRunner, model: "default", skills, repoClient, requirePrincipal: config.requirePrincipal,
646
+ runner, subRunner, handslessSubRunner, model: "default", skills, repoClient, requirePrincipal: config.requirePrincipal,
601
647
  webSearch: webSearch ? webSearch : undefined, metrics, logger, webFetchSummarize,
602
648
  // core 1.364 写半场:与 RunnerDeps.backgroundAgentStore 同实例(组装区注释;半接=静默死特性)。
603
649
  backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
@@ -629,29 +675,12 @@ async function main() {
629
675
  // checkpointStore 同实例天然成立;per-request 项如 webSearch 对 revive 无关——objective 在 core
630
676
  // driveParkedResume 里被丢弃)。checkpointStore/agentStore 任一缺席 = park 本就不可达,分支不接线。
631
677
  const parkedReviveTool = checkpointStore && backgroundAgentStore ? scenarios.default?.({})?.tools.find((t) => t.name === "Agent") : undefined;
632
- // [1596]/[1597] 跨进程父约束重供席工厂:host 任务的 toolPolicy 解析槽按部署配置**同参重建**
633
- // (resolveSpec durableEnabled 分支的同款折叠:durableQuestion + durableAsk 四旋钮),单层链
634
- // count=1,durableMandate=true(host durableApproval durable 车道无 live onAsk 的恒形)。
635
- // exempt 探针锚 = row.rootSessionId——与 parked decide 的 remember grant 锚同键([1591] 候裁②),
636
- // 重建链上的豁免语义与原链一致。重建不出(无 root 锚)= 不供席 = core pre-CAS 门诚实拒(现状)。
637
- // #152 复审 A2:「同参重建」是本工厂自述的不变式,所以 question 门必须调 resolve-spec 的**同一个**
638
- // 工厂并传同一个活体面,不能自折 core 原形——分腿改动只落一侧就是两份拷贝开始漂移的那一刻。
639
- // 赎回腿今天无 question ALS ctx(/decide 路由触发,core driveParkedResume 内驱动)⇒ 判 ask,与原形同判。
678
+ // [1596]/[1597] 跨副本父约束重供席工厂:host 任务的 toolPolicy 解析槽按**当前**部署配置重建
679
+ // 「部署 操作员」两层完整链(design/181 件二收编;实现与全部理由在 boot/parked-revive-gate.ts,
680
+ // 提出去的唯一理由是 main.ts 顶层 `void main()` 让那条腿的运行期语义在原地一格都钉不住)。
681
+ // 构造条件逐字保持:裸 Agent 工具在场 部署真开了 durable 审批。
640
682
  const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
641
- ? (row) => ({
642
- parentConstraints: [{
643
- policy: combinePolicies(createDurableQuestionGate(question), createDurableAskPolicy({
644
- requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,
645
- ...(approvalExemptionStore && row.rootSessionId
646
- ? {
647
- exempt: (toolName) => approvalExemptionStore.has(row.rootSessionId, toolName),
648
- onExempted: (toolName, rawToolName) => logger.info("approval_exempted", { sessionId: row.rootSessionId, toolName, rawToolName, leg: "parked_revive" }),
649
- }
650
- : {}),
651
- })),
652
- durableMandate: true,
653
- }],
654
- })
683
+ ? createParkedReviveInheritedGate({ config, question, approvalExemptionStore, logger, localRoot })
655
684
  : undefined;
656
685
  // 场景详情:内建 details 必须在 overlay 合并【前】构建(探针要打纯内建工厂,不是被 center 顶掉的);
657
686
  // center details 随 overlay 同判定源盖同名——source 语义与 selectScenario 实际取用永一致(约定①)。
@@ -779,6 +808,10 @@ async function main() {
779
808
  // Undefined-safe: the env-only/no-backend worker 501s the routes.
780
809
  backend: backend ? backend : undefined,
781
810
  };
811
+ // [3193] 病理1:fleet 快照的 recovery 屏障(deferred 形,声明先于 deps 装配、落定在下方 recover 块——
812
+ // 对装配顺序鲁棒;无 notify gate 的部署在 recover 块 else 臂立即落)。
813
+ let resolveFleetRecovery;
814
+ const fleetRecoveryDone = new Promise((r) => { resolveFleetRecovery = r; });
782
815
  /** 进程内活体协调面(本副本 only,重启即空:HITL 协调器/可 steer 句柄注册表/总线/收件箱) */
783
816
  const coordinators = {
784
817
  elicitation: elicitation ? elicitation : undefined, // E23: live-only inbound-elicitation coordinator (respond route + ALS wraps)
@@ -788,6 +821,7 @@ async function main() {
788
821
  subagentSteerRegistry, // C2 (core 1.219): steerable Task-subagent handle registry (POST /v1/runs/:id/subagents/:target/steer)
789
822
  workflowCompletionInbox: workflowCompletionInbox ? workflowCompletionInbox : undefined, // P1 ①②: drain → workflow_complete frame on stream-open
790
823
  fleetBus,
824
+ fleetRecoveryDone, // [3193] 病理1:快照组装的有界等待信号(recover 块落定)
791
825
  // [1196] session 级 SSE 订阅:watch registry(N 订阅共享一条探针,自适应拍距,无订阅即停)。谓词与
792
826
  // /head 探针同门(getLeafId+ownerOf);缺 store 面不建=路由 501+capabilities.sessionEvents=false。
793
827
  sessionWatch: sessionWatchRegistry ? sessionWatchRegistry : undefined, // [1196] 建于 purgeSession 之前(delete 即 dropSession 的同副本围栏)
@@ -909,8 +943,12 @@ async function main() {
909
943
  // 帽只在 watch 面在场时才有意义(缺席时路由 501,读不到它)——保持与原条件段同门,不单独接线。
910
944
  sessionEventsMaxConnections: sessionWatchRegistry ? posIntEnv(process.env.SESSION_EVENTS_MAX_CONNS, 256, 100_000) : undefined,
911
945
  };
946
+ // #196:hands lane 登记簿 —— resolveSpec 登记本请求场景的表态,HTTP 执行点凭 spec 取回选 Runner。
947
+ // 两处**同一实例**(交给 createResolveSpec 与 runnerFor 闭包),配对由这一行结构性保证。
948
+ const handsLanes = createHandsLaneRegistry();
912
949
  // design/158 A10:resolveSpec 段搬到 src/boot/resolve-spec.ts(逐字;两处活引用改取值,见该文件头注)。
913
950
  const resolveSpec = createResolveSpec({
951
+ handsLanes,
914
952
  config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver,
915
953
  getCenterPrompts: () => configCenter.getCenterPrompts(),
916
954
  getKeyResolver: () => configCenter.getKeyResolver(),
@@ -921,7 +959,9 @@ async function main() {
921
959
  sessionEnvSelection,
922
960
  liveQuestionFace: question, // #152:活体问答面在场 ⇒ durable question 门按活流分腿(见 ResolveSpecCtx 注)
923
961
  });
924
- const server = createHttpServer({ runner, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
962
+ // #196:执行点选路。resolveSpec 已按场景判别位登记 lane;`full` 恒回主 runner(修前行为逐字不变)
963
+ const runnerFor = (spec) => pickHandsRunner(handsLanes.laneOf(spec), { full: runner, handsless: handslessRunner });
964
+ const server = createHttpServer({ runner, runnerFor, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
925
965
  // D-D SLA-timer: wire the server's deny-sweep into the reaper holder declared above (the reaper is defined
926
966
  // before the server, so it calls through this late-bound reference).
927
967
  runDenySweep = server.denyExpiredApprovals;
@@ -1040,10 +1080,17 @@ async function main() {
1040
1080
  // sweep throw is logged, never fatal.
1041
1081
  if (workflowNotifyGate) {
1042
1082
  // [2995]/[2999] opts 单点合成在 workflow-orchestration(可见性回灌 + replica-local 形 boot 孤儿判死)。
1083
+ // [3193] 病理1:recover 收官(成败皆算)即落 fleet 快照屏障——「serving 不等 recovery」对**提交请求**
1084
+ // 仍成立,但 fleet 快照组装现在对它做有界等待(routes/fleet.ts,3s cap):SIGKILL 后壳重连不再
1085
+ // 与 boot 判死赛跑(cli [3193] 插桩实证的竞态)。
1043
1086
  void workflowNotifyGate
1044
1087
  .recover(workflowRecoverOpts)
1045
1088
  .then((tally) => logger.info("workflow_notify_recovered", tally))
1046
- .catch((err) => logger.warn("workflow_notify_recover_failed", { err: String(err) }));
1089
+ .catch((err) => logger.warn("workflow_notify_recover_failed", { err: String(err) }))
1090
+ .finally(() => resolveFleetRecovery());
1091
+ }
1092
+ else {
1093
+ resolveFleetRecovery();
1047
1094
  }
1048
1095
  // design/158 A10:收尾段搬到 src/boot/shutdown.ts(逐字)。⚠️ 调用点必须留在 listen 之后 —— 位置即契约,
1049
1096
  // 理由(信号注册时点/三信号相对次序/clearInterval 先于 server.close)见该文件头注。
@@ -37,6 +37,10 @@ export declare const FAIL_OPEN_TAGS: {
37
37
  readonly cls: "F";
38
38
  readonly note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。";
39
39
  };
40
+ readonly "server.fleet.snapshot-terminal-window-unavailable": {
41
+ readonly cls: "F";
42
+ readonly note: "`/v1/fleet/stream` 连接时快照的**近期终态行窗**(#189:引擎重启后 boot 判死的 workflow 行,pull 自 durable store)读失败或超预算(2s)⇒ 本次快照少这几行历史。放行的最坏后果=面板首屏看不到刚结束的 workflow(与本修之前的行为等同,不是新损失);真源不受影响(`GET /v1/workflows` 照常)。方向刻意 fail-open:一次慢查询不该把整条 SSE 的握手拖住——少几行是难看,连不上流是坏掉。";
43
+ };
40
44
  readonly "server.fleet.subscriber-callback-threw": {
41
45
  readonly cls: "F";
42
46
  readonly note: "fleet bus 某订阅回调抛错 ⇒ 该回调本帧作废,其余订阅方与发布方不受影响。隔离是承重的:扇出同步,修前异常会传回发布方 put/update 投影点,core 持久化 catch{} 且不推进 storeRev ⇒ durable 行冻在 running 而 notify 已 ack(#183 复审 R3 HIGH)。丢的只是一个消费方的一帧渲染,故 F 类;但必须留痕——静默吞掉等于订阅方病灶永不显形。";
@@ -60,6 +60,10 @@ export const FAIL_OPEN_TAGS = {
60
60
  cls: "F",
61
61
  note: "已验签的提示词工件回填本地缓存失败。工件本身已返回给调用方;代价是下次同 digest 还要再取一次网络。",
62
62
  },
63
+ "server.fleet.snapshot-terminal-window-unavailable": {
64
+ cls: "F",
65
+ note: "`/v1/fleet/stream` 连接时快照的**近期终态行窗**(#189:引擎重启后 boot 判死的 workflow 行,pull 自 durable store)读失败或超预算(2s)⇒ 本次快照少这几行历史。放行的最坏后果=面板首屏看不到刚结束的 workflow(与本修之前的行为等同,不是新损失);真源不受影响(`GET /v1/workflows` 照常)。方向刻意 fail-open:一次慢查询不该把整条 SSE 的握手拖住——少几行是难看,连不上流是坏掉。",
66
+ },
63
67
  "server.fleet.subscriber-callback-threw": {
64
68
  cls: "F",
65
69
  note: "fleet bus 某订阅回调抛错 ⇒ 该回调本帧作废,其余订阅方与发布方不受影响。隔离是承重的:扇出同步,修前异常会传回发布方 put/update 投影点,core 持久化 catch{} 且不推进 storeRev ⇒ durable 行冻在 running 而 notify 已 ack(#183 复审 R3 HIGH)。丢的只是一个消费方的一帧渲染,故 F 类;但必须留痕——静默吞掉等于订阅方病灶永不显形。",
@@ -25,6 +25,19 @@ export declare function createPermissionDeniedMeter(metrics: Pick<Metrics, "inc"
25
25
  *
26
26
  * Pure observation: every hook returns `undefined`, so it never rewrites args, never gates, never alters
27
27
  * the result (and per core's hooks invariant a hook's `allow` can't bypass the policy gate anyway).
28
+ *
29
+ * 🔴 core 5.20.0 起这条「纯观察」性质必须**声明**出来(`preToolUseObservational`),不能只写在注释里:
30
+ * core 5.19.0 的委派折叠按「PreToolUse 席位在不在」把父的 screening 面折进子代的祖先约束链——它分不出
31
+ * 诊断 tracer 与真裁决者。后果是**这个默认关的诊断旋钮**一开,该部署所有 hook-wired 父的 parked 子代
32
+ * checkpoint 就记 `parentConstraintCount=2`,而跨副本赎回腿(`boot/parked-revive-gate.ts`)恒供 1 ⇒
33
+ * core pre-CAS 响亮拒、行永久 pending(层数 park 时写死,事后关旋钮/重批/回滚都不改)。
34
+ * 声明之后该面不铸链条目,count 回到 1,病路消灭。
35
+ * ⛔ 打这个标的前提是**真的一条裁决都不出**:core 对声明了却返回判词的面是「拒绝采纳 + onError 告警」
36
+ * (classification `observational-hook-verdict-ignored`),即判词被静默丢弃——把标打到会裁决的面上比原
37
+ * 缺陷更坏。本文件三个 pre/post 回调恒返 `undefined`,`permissionDenied` 本就是 void 观察槽。
38
+ * 📎 连带的**可观测**变化(不是回归):core 的「可写工具面无 effect-aware 门」启动告警此前把本 tracer
39
+ * 算作一道门而被抑制;声明之后它不再算,于是「没配任何 policy 层 + 开着 TOOL_TRACE」的部署升级后会
40
+ * 多出那行告警。方向正确——tracer 从来没有门住任何东西,原先的抑制本身就是一次静默 fail-open。
28
41
  */
29
42
  export declare function createToolTracer(logger: Logger): Hooks;
30
43
  //# sourceMappingURL=tool-trace.d.ts.map
@@ -33,9 +33,23 @@ export function createPermissionDeniedMeter(metrics) {
33
33
  *
34
34
  * Pure observation: every hook returns `undefined`, so it never rewrites args, never gates, never alters
35
35
  * the result (and per core's hooks invariant a hook's `allow` can't bypass the policy gate anyway).
36
+ *
37
+ * 🔴 core 5.20.0 起这条「纯观察」性质必须**声明**出来(`preToolUseObservational`),不能只写在注释里:
38
+ * core 5.19.0 的委派折叠按「PreToolUse 席位在不在」把父的 screening 面折进子代的祖先约束链——它分不出
39
+ * 诊断 tracer 与真裁决者。后果是**这个默认关的诊断旋钮**一开,该部署所有 hook-wired 父的 parked 子代
40
+ * checkpoint 就记 `parentConstraintCount=2`,而跨副本赎回腿(`boot/parked-revive-gate.ts`)恒供 1 ⇒
41
+ * core pre-CAS 响亮拒、行永久 pending(层数 park 时写死,事后关旋钮/重批/回滚都不改)。
42
+ * 声明之后该面不铸链条目,count 回到 1,病路消灭。
43
+ * ⛔ 打这个标的前提是**真的一条裁决都不出**:core 对声明了却返回判词的面是「拒绝采纳 + onError 告警」
44
+ * (classification `observational-hook-verdict-ignored`),即判词被静默丢弃——把标打到会裁决的面上比原
45
+ * 缺陷更坏。本文件三个 pre/post 回调恒返 `undefined`,`permissionDenied` 本就是 void 观察槽。
46
+ * 📎 连带的**可观测**变化(不是回归):core 的「可写工具面无 effect-aware 门」启动告警此前把本 tracer
47
+ * 算作一道门而被抑制;声明之后它不再算,于是「没配任何 policy 层 + 开着 TOOL_TRACE」的部署升级后会
48
+ * 多出那行告警。方向正确——tracer 从来没有门住任何东西,原先的抑制本身就是一次静默 fail-open。
36
49
  */
37
50
  export function createToolTracer(logger) {
38
51
  return {
52
+ preToolUseObservational: true,
39
53
  preToolUse(toolName, input, ctx) {
40
54
  logger.info("tool_trace", { phase: "pre", id: ctx.toolCallId, tool: toolName, arg: summarize(input) });
41
55
  return undefined;
@@ -1,5 +1,5 @@
1
1
  import type { WorkflowRun, WorkflowRunStore, WorkflowRunStatus, WorkflowRunSummary } from "@sema-agent/core";
2
- import type { FleetEventBus } from "../fleet/fleet-bus.js";
2
+ import { type FleetEventBus } from "../fleet/fleet-bus.js";
3
3
  /** A workflow completion notification's terminal payload — the bounded, redacted shape core's notifier seam
4
4
  * carries, re-derivable from a durable {@link import("@sema-agent/core").WorkflowRun} on the recovery path. */
5
5
  export interface WorkflowCompletionPayload {
@@ -41,8 +41,8 @@
41
41
  */
42
42
  import { mkdirSync, readFileSync, existsSync, openSync, writeSync, fsyncSync, closeSync } from "node:fs";
43
43
  import { join } from "node:path";
44
- import { deriveAgentDisplayStatus } from "@sema-agent/core";
45
- import { redactSecrets } from "../trace/redact.js";
44
+ import { buildFleetWorkflowRow } from "../fleet/fleet-bus.js";
45
+ import { isTerminalWorkflowRowStatus, seedTerminalWorkflowRow } from "../fleet/fleet-terminal-window.js";
46
46
  /** Build a bounded + redacted completion summary from a durable run's terminal state (the recovery path — the
47
47
  * live path uses core's own `boundedSummary`). We never have the raw result here (the run store keeps the run,
48
48
  * not the originator's payload), so we synthesize a conservative one: status + the run's already-bounded error.
@@ -500,43 +500,23 @@ export class JournalingWorkflowRunStore {
500
500
  publishFleet(id, run) {
501
501
  if (!this.fleetBus)
502
502
  return;
503
- const agents = run.agents ?? [];
504
- // [2336] doneCount failedCount **不相交**:done 只数 completed。契约以此为前提(fleet-bus.ts
505
- // startedCount 注的回退式 done+failed started);把 failed 也计进 done 会让全失败 workflow 的
506
- // 终帧渲成 "N done"(cli 4.1.3 实测 done=2 failed=2 started=2)
507
- const done = agents.filter((a) => a.status === "completed").length;
508
- const failed = agents.filter((a) => a.status === "failed").length;
509
- // cli [1726] 二①:CC 规模告警的分母是 **started**(已启动),而 `totalCount`(= agents.length)是**计划总数**
510
- // (含排队中)。判别口径走 core 导出的 `deriveAgentDisplayStatus` —— 一个 agent 已在 `run.agents` 里但
511
- // `startedAt` 未置就是排队中。**不自己写 `startedAt !== undefined`**:那是 core 的显示契约,两端各写一遍
512
- // 必然漂移(本仓刚在 coarse 门镜像上吃过同款),用它的函数则 core 一改、两端同时跟随。
513
- const started = agents.filter((a) => deriveAgentDisplayStatus(a, run.status) !== "queued").length;
514
- const terminal = run.status === "completed" || run.status === "failed";
503
+ // A-002.3(#194 病族 F1 同源性断裂):终态判据走 fleet-terminal-window 的共享谓词(其穷举表对
504
+ // core WorkflowRunStatus 编译期闭合)——此前手写两值比较是第二份独立定义,core 加终态词时穷举表
505
+ // 红、这里恒 false 行既不 seed 进终态窗也不 removeWorkflow,面板留幽灵 running 行。
506
+ const terminal = isTerminalWorkflowRowStatus(run.status);
515
507
  // 撤行**不挂在发帧成功上**(复审第 2 轮 MEDIUM):「终态行必须离场」是不变量、发终帧是尽力而为。
516
508
  // try/finally ⇒ 发布本体抛错照样撤行(异常仍向上抛,不吞:吞了就是一条未登记的 fail-open 臂)。
517
509
  // 订阅方抛错已在 fleet bus 的 subscribe 隔离层被吞并记 fail-open——本 finally 守的是发布机器本身
518
510
  // 的抛错(理论缝级,fresh 复审盘点后判近死防御但保留:防未来发布本体改动回退)。两腿一处收口。
511
+ // 行投影本体 = {@link buildFleetWorkflowRow}(#189:快照的终态行窗与本写路径共用同一份投影,
512
+ // 单源不漂;本函数的发布/撤行语义逐字不变)。
513
+ // [3156] 轮2 修:终态行**写路径顺手 seed** 进程内终态窗缓存——重启-重连剧本里(boot recover 判死),
514
+ // fleetWide 连接的 durable pull 没有正确的查询键可用(caller principal ≠ 引擎铸行 scope),seed 是
515
+ // 唯一同源可达径。放在 publish 之前:seed 是纯内存 push,不会抛;终态行入窗与发帧同拍。
516
+ if (terminal)
517
+ seedTerminalWorkflowRow(buildFleetWorkflowRow(id, run));
519
518
  try {
520
- this.fleetBus.publishWorkflow({
521
- id,
522
- // [WF2-A parity] redact the workflow label surfaces for parity with the run + subagent-child names (fleet-bus.ts):
523
- // a tool-launched (LLM-authored) workflow's meta.name/description is task-controlled and could carry a secret shape.
524
- name: redactSecrets(run.name ?? "Dynamic workflow"),
525
- ...(run.description ? { description: redactSecrets(run.description) } : {}),
526
- scope: run.scope,
527
- // codex-6 F2:sessionId 必须随行——streamFleet 对无 sessionId 的行按「同 principal 全会话可见」
528
- // 兜底,漏发=A 会话的 workflow 名/进度/token 泄进 B 会话的 ?session= 过滤流。
529
- ...(run.originatingSessionId ? { sessionId: run.originatingSessionId } : {}),
530
- status: run.status,
531
- doneCount: done,
532
- totalCount: agents.length,
533
- failedCount: failed,
534
- startedCount: started,
535
- tokens: (run.stats?.tokens ?? 0) + (run.stats?.nested?.tokens ?? 0),
536
- // [1294]:跑动中也带时长(1.232 只在 endedAt 后带——clay 验收轮实锚面板恒显 0s)。终态用
537
- // endedAt 定格,活跑用 now-startedAt(每次 put/update 观察点刷新,壳侧读帧即当前时长)。
538
- elapsedMs: (run.endedAt ?? Date.now()) - run.startedAt,
539
- });
519
+ this.fleetBus.publishWorkflow(buildFleetWorkflowRow(id, run));
540
520
  }
541
521
  finally {
542
522
  if (terminal)
@@ -297,6 +297,39 @@ export declare const APPROVAL_BATCHES_TABLE = "approval_batches";
297
297
  *
298
298
  * SCHEMA POLICY 同 `tidb-pool.ts` 头注:纯 CREATE,禁 ALTER 增量 seam,改列直接改这里 + 重建库
299
299
  * (`docs/schema/baseline-mysql.sql` 是由这些文本机器录制出的**产物**,永不手改)。
300
+ *
301
+ * ── 列宽的**测量依据**(#192 A10;U2 逐列取证在档)──────────────────────────────────────────────────
302
+ * 本表家族原先一律 VARCHAR(255),是"先建表后想"的产物。按"每列的值到底由谁铸、有没有入口上限"重新裁:
303
+ *
304
+ * 收窄(有硬上限背书,证据即上限本身):
305
+ * · `ask_id` / `batch_id` / `bound_ask_id` → 64:`deriveAskId`/`deriveBatchId` 恒是 sha256 hex 取前
306
+ * 64 字符(`approval-ask-machine.ts` deterministicId),按构造不可能更长。
307
+ * · `task_id` → 64:入口 `UUIDV7_RE`(`security.ts:134`)只放行 36 字符的规范 uuidv7;与兄弟列
308
+ * `task_run.task_id VARCHAR(64)` 同宽(此前 255 是本家族独有的偏离,无依据)。
309
+ * · `session_id` → 64:提交入口硬拒 >64(`http/server.ts` "sessionId must be at most 64 characters"),
310
+ * 与 `session_meta`/`task_run` 同宽。
311
+ * · `owner` → 190:`assertPrincipalShape` 的 `PRINCIPAL_MAX_LENGTH = 190`(`security.ts:350`)硬拒更长者;
312
+ * 全仓每一根 owner/scope 轴都是 190,此前 255 同样是无依据偏离。
313
+ * · `gate_token` → 120:它抄的是 checkpoint 的 token(core `mintCheckpointToken` = 16 字节 hex = 32 字符),
314
+ * 取与**被抄那一列** `checkpoint.token VARCHAR(120)` 同宽 —— 同一个值在两张表上宽度必须一致,
315
+ * 否则抄的那一步就是一道静默截断。512 是原先的 16 倍冗余。
316
+ *
317
+ * 🔑 上面四条收窄之所以**不引入任何新暴露面**,靠的不是入口断言而是一条更硬的事实:同一个 session_id /
318
+ * task_id / owner **早就**同时躺在 `session_meta.session_id VARCHAR(64)` / `task_run.task_id VARCHAR(64)` /
319
+ * `task_run.owner VARCHAR(190)` 里。任何长到能撑爆本表新宽度的值,在写到那几张**更中心**的表时就已经
320
+ * 先炸了 —— 本表此前的 255 从来不是一道额外的安全余量,只是一处与全仓不一致的偏离。
321
+ * ⇒ 收窄的风险上界 = 0;真正要担心的是反过来:留着 255 会让人误以为这里可以存更长的 id。
322
+ *
323
+ * 🔴 **刻意不收窄**(测量结果不支持,记在这里免得下一个人以为是漏了):
324
+ * · `source_task_id` / `tool_call_id` / `parent_tool_call_id` / `gate_bound_call_id` —— 这四列的值是
325
+ * **模型/引擎铸的原始 id**,服务端在写行这一步**没有任何长度断言**(`approval-card.ts` 的
326
+ * `MAX_IDENT` 只裁了给人看的那份 card 投影,不是本列)。没有入口上限就收窄 = 把"存不下"这件事
327
+ * 推迟到 INSERT 才炸(MySQL 更糟:静默截断),换来的只是几十字节。要收窄,先补入口断言
328
+ * (照 `assertPrincipalShape`/`assertIdempotencyKeyShape` 的姿势),那是另一件事。
329
+ * · `bound_input_hash` / `gate_bound_input_hash` —— 按约定是 sha256 hex(64),但 `readBoundInputHash`
330
+ * 只校验"非空字符串",没校长度/字形 ⇒ 同上,先有断言再谈收窄。
331
+ * · `idempotency_key` —— 255 **正是**入口断言的上限本身(zod `.max(255)` + 店内 `assertIdempotencyKeyShape`
332
+ * 双执法),宽度与上限同源,恰好正确,动它反而制造截断面。
300
333
  */
301
334
  export declare const TIDB_APPROVAL_ASK_STATEMENTS: readonly string[];
302
335
  /** {@link TIDB_APPROVAL_ASK_STATEMENTS} 的遍历壳——生产路径走 `tidb-pool.ts` 的中央 `ensureSchema`
@@ -71,13 +71,46 @@ export const APPROVAL_BATCHES_TABLE = "approval_batches";
71
71
  *
72
72
  * SCHEMA POLICY 同 `tidb-pool.ts` 头注:纯 CREATE,禁 ALTER 增量 seam,改列直接改这里 + 重建库
73
73
  * (`docs/schema/baseline-mysql.sql` 是由这些文本机器录制出的**产物**,永不手改)。
74
+ *
75
+ * ── 列宽的**测量依据**(#192 A10;U2 逐列取证在档)──────────────────────────────────────────────────
76
+ * 本表家族原先一律 VARCHAR(255),是"先建表后想"的产物。按"每列的值到底由谁铸、有没有入口上限"重新裁:
77
+ *
78
+ * 收窄(有硬上限背书,证据即上限本身):
79
+ * · `ask_id` / `batch_id` / `bound_ask_id` → 64:`deriveAskId`/`deriveBatchId` 恒是 sha256 hex 取前
80
+ * 64 字符(`approval-ask-machine.ts` deterministicId),按构造不可能更长。
81
+ * · `task_id` → 64:入口 `UUIDV7_RE`(`security.ts:134`)只放行 36 字符的规范 uuidv7;与兄弟列
82
+ * `task_run.task_id VARCHAR(64)` 同宽(此前 255 是本家族独有的偏离,无依据)。
83
+ * · `session_id` → 64:提交入口硬拒 >64(`http/server.ts` "sessionId must be at most 64 characters"),
84
+ * 与 `session_meta`/`task_run` 同宽。
85
+ * · `owner` → 190:`assertPrincipalShape` 的 `PRINCIPAL_MAX_LENGTH = 190`(`security.ts:350`)硬拒更长者;
86
+ * 全仓每一根 owner/scope 轴都是 190,此前 255 同样是无依据偏离。
87
+ * · `gate_token` → 120:它抄的是 checkpoint 的 token(core `mintCheckpointToken` = 16 字节 hex = 32 字符),
88
+ * 取与**被抄那一列** `checkpoint.token VARCHAR(120)` 同宽 —— 同一个值在两张表上宽度必须一致,
89
+ * 否则抄的那一步就是一道静默截断。512 是原先的 16 倍冗余。
90
+ *
91
+ * 🔑 上面四条收窄之所以**不引入任何新暴露面**,靠的不是入口断言而是一条更硬的事实:同一个 session_id /
92
+ * task_id / owner **早就**同时躺在 `session_meta.session_id VARCHAR(64)` / `task_run.task_id VARCHAR(64)` /
93
+ * `task_run.owner VARCHAR(190)` 里。任何长到能撑爆本表新宽度的值,在写到那几张**更中心**的表时就已经
94
+ * 先炸了 —— 本表此前的 255 从来不是一道额外的安全余量,只是一处与全仓不一致的偏离。
95
+ * ⇒ 收窄的风险上界 = 0;真正要担心的是反过来:留着 255 会让人误以为这里可以存更长的 id。
96
+ *
97
+ * 🔴 **刻意不收窄**(测量结果不支持,记在这里免得下一个人以为是漏了):
98
+ * · `source_task_id` / `tool_call_id` / `parent_tool_call_id` / `gate_bound_call_id` —— 这四列的值是
99
+ * **模型/引擎铸的原始 id**,服务端在写行这一步**没有任何长度断言**(`approval-card.ts` 的
100
+ * `MAX_IDENT` 只裁了给人看的那份 card 投影,不是本列)。没有入口上限就收窄 = 把"存不下"这件事
101
+ * 推迟到 INSERT 才炸(MySQL 更糟:静默截断),换来的只是几十字节。要收窄,先补入口断言
102
+ * (照 `assertPrincipalShape`/`assertIdempotencyKeyShape` 的姿势),那是另一件事。
103
+ * · `bound_input_hash` / `gate_bound_input_hash` —— 按约定是 sha256 hex(64),但 `readBoundInputHash`
104
+ * 只校验"非空字符串",没校长度/字形 ⇒ 同上,先有断言再谈收窄。
105
+ * · `idempotency_key` —— 255 **正是**入口断言的上限本身(zod `.max(255)` + 店内 `assertIdempotencyKeyShape`
106
+ * 双执法),宽度与上限同源,恰好正确,动它反而制造截断面。
74
107
  */
75
108
  export const TIDB_APPROVAL_ASK_STATEMENTS = [
76
109
  `CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCHES_TABLE} (
77
- batch_id VARCHAR(255) NOT NULL,
78
- task_id VARCHAR(255) NOT NULL,
110
+ batch_id VARCHAR(64) NOT NULL,
111
+ task_id VARCHAR(64) NOT NULL,
79
112
  state VARCHAR(32) NOT NULL,
80
- bound_ask_id VARCHAR(255) NULL,
113
+ bound_ask_id VARCHAR(64) NULL,
81
114
  version INT NOT NULL DEFAULT 0,
82
115
  created_at_ms BIGINT NOT NULL,
83
116
  updated_at_ms BIGINT NOT NULL,
@@ -85,12 +118,12 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
85
118
  KEY idx_batch_task (task_id)
86
119
  ) COLLATE utf8mb4_bin`,
87
120
  `CREATE TABLE IF NOT EXISTS ${APPROVAL_ASKS_TABLE} (
88
- ask_id VARCHAR(255) NOT NULL,
89
- task_id VARCHAR(255) NOT NULL,
121
+ ask_id VARCHAR(64) NOT NULL,
122
+ task_id VARCHAR(64) NOT NULL,
90
123
  source_task_id VARCHAR(255) NOT NULL,
91
- session_id VARCHAR(255) NOT NULL,
92
- owner VARCHAR(255) NULL,
93
- batch_id VARCHAR(255) NOT NULL,
124
+ session_id VARCHAR(64) NOT NULL,
125
+ owner VARCHAR(190) NULL,
126
+ batch_id VARCHAR(64) NOT NULL,
94
127
  tool_call_id VARCHAR(255) NOT NULL,
95
128
  -- leg_key: 车3 §3.2′ 身份轴——sha256(resume checkpoint token) 的 hex(64 字符),首腿存空串。
96
129
  -- 取代原来的数值 leg 列:一腿一凭据,零 DDL 依赖 task_run、零 markResuming 改动。NOT NULL(空串
@@ -110,7 +143,7 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
110
143
  decision_note TEXT NULL,
111
144
  decided_at_ms BIGINT NULL,
112
145
  denied_reason VARCHAR(64) NULL,
113
- gate_token VARCHAR(512) NULL,
146
+ gate_token VARCHAR(120) NULL,
114
147
  gate_bound_call_id VARCHAR(255) NULL,
115
148
  gate_bound_input_hash VARCHAR(255) NULL,
116
149
  idempotency_key VARCHAR(255) NULL,
@@ -141,10 +174,10 @@ export async function ensureTiDBApprovalAskSchema(pool) {
141
174
  }
142
175
  export async function ensurePgApprovalAskSchema(q) {
143
176
  await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCHES_TABLE} (
144
- batch_id VARCHAR(255) NOT NULL,
145
- task_id VARCHAR(255) NOT NULL,
146
- state VARCHAR(32) NOT NULL,
147
- bound_ask_id VARCHAR(255),
177
+ batch_id VARCHAR(64) COLLATE "C" NOT NULL,
178
+ task_id VARCHAR(64) COLLATE "C" NOT NULL,
179
+ state VARCHAR(32) COLLATE "C" NOT NULL,
180
+ bound_ask_id VARCHAR(64) COLLATE "C",
148
181
  version INT NOT NULL DEFAULT 0,
149
182
  created_at_ms BIGINT NOT NULL,
150
183
  updated_at_ms BIGINT NOT NULL,
@@ -152,33 +185,33 @@ export async function ensurePgApprovalAskSchema(q) {
152
185
  )`);
153
186
  await q(`CREATE INDEX IF NOT EXISTS idx_batch_task ON ${APPROVAL_BATCHES_TABLE} (task_id)`);
154
187
  await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_ASKS_TABLE} (
155
- ask_id VARCHAR(255) NOT NULL,
156
- task_id VARCHAR(255) NOT NULL,
157
- source_task_id VARCHAR(255) NOT NULL,
158
- session_id VARCHAR(255) NOT NULL,
159
- owner VARCHAR(255),
160
- batch_id VARCHAR(255) NOT NULL,
161
- tool_call_id VARCHAR(255) NOT NULL,
188
+ ask_id VARCHAR(64) COLLATE "C" NOT NULL,
189
+ task_id VARCHAR(64) COLLATE "C" NOT NULL,
190
+ source_task_id VARCHAR(255) COLLATE "C" NOT NULL,
191
+ session_id VARCHAR(64) COLLATE "C" NOT NULL,
192
+ owner VARCHAR(190) COLLATE "C",
193
+ batch_id VARCHAR(64) COLLATE "C" NOT NULL,
194
+ tool_call_id VARCHAR(255) COLLATE "C" NOT NULL,
162
195
  -- leg_key: 车3 §3.2′ 身份轴——sha256(resume checkpoint token) 的 hex(64 字符),首腿存空串
163
196
  -- (空串是首腿的真值,不是缺席,故 NOT NULL)。MySQL 侧 twin 逐字同形。
164
- leg_key VARCHAR(64) NOT NULL,
165
- parent_tool_call_id VARCHAR(255),
197
+ leg_key VARCHAR(64) COLLATE "C" NOT NULL,
198
+ parent_tool_call_id VARCHAR(255) COLLATE "C",
166
199
  -- bound_input_hash(车5 §9 C2)——MySQL twin 逐字同形(宽度/语义见那侧注)。
167
- bound_input_hash VARCHAR(255),
168
- state VARCHAR(32) NOT NULL,
200
+ bound_input_hash VARCHAR(255) COLLATE "C",
201
+ state VARCHAR(32) COLLATE "C" NOT NULL,
169
202
  -- clay 批 TINYINT(1) 归一先例:PG 侧统一 SMALLINT 0/1(此前本家族用过 BOOLEAN,已裁弃).
170
203
  provisional SMALLINT NOT NULL DEFAULT 0,
171
204
  version INT NOT NULL DEFAULT 0,
172
- decision VARCHAR(16),
173
- decision_actor TEXT,
174
- decision_note TEXT,
205
+ decision VARCHAR(16) COLLATE "C",
206
+ decision_actor TEXT COLLATE "C",
207
+ decision_note TEXT COLLATE "C",
175
208
  decided_at_ms BIGINT,
176
- denied_reason VARCHAR(64),
177
- gate_token VARCHAR(512),
178
- gate_bound_call_id VARCHAR(255),
179
- gate_bound_input_hash VARCHAR(255),
180
- idempotency_key VARCHAR(255),
181
- card_json TEXT NOT NULL,
209
+ denied_reason VARCHAR(64) COLLATE "C",
210
+ gate_token VARCHAR(120) COLLATE "C",
211
+ gate_bound_call_id VARCHAR(255) COLLATE "C",
212
+ gate_bound_input_hash VARCHAR(255) COLLATE "C",
213
+ idempotency_key VARCHAR(255) COLLATE "C",
214
+ card_json TEXT COLLATE "C" NOT NULL,
182
215
  schema_version INT NOT NULL,
183
216
  expires_at_ms BIGINT NOT NULL,
184
217
  created_at_ms BIGINT NOT NULL,