@sema-agent/server 7.4.0 → 7.6.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 (201) 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 +80 -1
  5. package/dist/approval-card.d.ts +15 -3
  6. package/dist/approval-card.js +41 -7
  7. package/dist/approval-reconciler.d.ts +109 -12
  8. package/dist/approval-reconciler.js +152 -24
  9. package/dist/boot/config-center.js +15 -2
  10. package/dist/boot/coordinators.js +10 -2
  11. package/dist/boot/execution-env.js +1 -1
  12. package/dist/boot/org-memory.d.ts +6 -0
  13. package/dist/boot/org-memory.js +1 -1
  14. package/dist/boot/parked-revive-gate.d.ts +78 -0
  15. package/dist/boot/parked-revive-gate.js +114 -0
  16. package/dist/boot/reapers.d.ts +2 -0
  17. package/dist/boot/reapers.js +11 -4
  18. package/dist/boot/resolve-spec.d.ts +3 -19
  19. package/dist/boot/resolve-spec.js +73 -67
  20. package/dist/boot/runner-deps.d.ts +23 -1
  21. package/dist/boot/runner-deps.js +8 -11
  22. package/dist/boot/workflow-orchestration.d.ts +8 -3
  23. package/dist/boot/workflow-orchestration.js +23 -1
  24. package/dist/budget.d.ts +1 -1
  25. package/dist/budget.js +1 -1
  26. package/dist/capabilities/repo-tools.d.ts +1 -1
  27. package/dist/capabilities/repo-tools.js +8 -2
  28. package/dist/config-center/apply-effective.js +33 -10
  29. package/dist/config-provider.d.ts +1 -0
  30. package/dist/config-provider.js +23 -3
  31. package/dist/config-types.d.ts +27 -9
  32. package/dist/config.d.ts +6 -1
  33. package/dist/config.js +61 -15
  34. package/dist/deployment-governance.d.ts +168 -0
  35. package/dist/deployment-governance.js +206 -0
  36. package/dist/env-facts.d.ts +3 -1
  37. package/dist/env-facts.js +3 -1
  38. package/dist/fleet/fleet-bus.d.ts +17 -2
  39. package/dist/fleet/fleet-bus.js +68 -3
  40. package/dist/fleet/fleet-terminal-window.d.ts +98 -0
  41. package/dist/fleet/fleet-terminal-window.js +316 -0
  42. package/dist/governance-ask-marks.d.ts +31 -0
  43. package/dist/governance-ask-marks.js +122 -0
  44. package/dist/hooks/hook-runner.d.ts +28 -0
  45. package/dist/hooks/hook-runner.js +149 -25
  46. package/dist/http/routes/approvals-assistant.js +6 -7
  47. package/dist/http/routes/diagnostics.js +10 -5
  48. package/dist/http/routes/fleet.js +160 -14
  49. package/dist/http/routes/memory-policy.d.ts +2 -1
  50. package/dist/http/routes/memory-policy.js +77 -13
  51. package/dist/http/routes/runs.js +6 -2
  52. package/dist/http/routes/tasks.js +59 -22
  53. package/dist/http/routes/trace-usage.js +3 -4
  54. package/dist/http/send.d.ts +23 -0
  55. package/dist/http/send.js +23 -0
  56. package/dist/http/server.d.ts +9 -0
  57. package/dist/http/server.js +28 -14
  58. package/dist/http/sse-log.js +3 -4
  59. package/dist/http/wire-types.d.ts +7 -2
  60. package/dist/leader/diffout.d.ts +10 -0
  61. package/dist/leader/diffout.js +14 -2
  62. package/dist/leader/diffup.js +3 -2
  63. package/dist/leader/planner.js +7 -0
  64. package/dist/main.js +39 -31
  65. package/dist/observability/fail-open.d.ts +17 -2
  66. package/dist/observability/fail-open.js +19 -4
  67. package/dist/observability/prompt-manifest.d.ts +5 -1
  68. package/dist/orchestration/workflow-notify-journal.d.ts +58 -2
  69. package/dist/orchestration/workflow-notify-journal.js +130 -45
  70. package/dist/parked-decide.js +9 -4
  71. package/dist/plugins/approval-ask-store-memory.d.ts +2 -2
  72. package/dist/plugins/approval-ask-store-memory.js +3 -2
  73. package/dist/plugins/approval-ask-store-sql.d.ts +60 -5
  74. package/dist/plugins/approval-ask-store-sql.js +75 -35
  75. package/dist/plugins/background-agent-store-sql.js +16 -16
  76. package/dist/plugins/background-shell-support.d.ts +1 -1
  77. package/dist/plugins/background-shell-support.js +2 -2
  78. package/dist/plugins/breaker-state-sql.js +2 -2
  79. package/dist/plugins/checkpoint-store-sql.d.ts +67 -8
  80. package/dist/plugins/checkpoint-store-sql.js +76 -13
  81. package/dist/plugins/image-bake-store-sql.d.ts +1 -1
  82. package/dist/plugins/image-bake-store-sql.js +27 -27
  83. package/dist/plugins/image-index-sql.js +15 -15
  84. package/dist/plugins/local-checkpoint-store.d.ts +20 -1
  85. package/dist/plugins/local-checkpoint-store.js +19 -0
  86. package/dist/plugins/mailbox-store-sql.d.ts +4 -10
  87. package/dist/plugins/mailbox-store-sql.js +59 -6
  88. package/dist/plugins/memory-engine-pg.js +9 -9
  89. package/dist/plugins/memory-engine-tidb.js +7 -7
  90. package/dist/plugins/memory-sync-store-pg.js +13 -13
  91. package/dist/plugins/memory-sync-store-tidb.js +5 -5
  92. package/dist/plugins/outcome-ledger-sql.js +7 -7
  93. package/dist/plugins/pg-cost-quota.js +3 -3
  94. package/dist/plugins/pg-pool.js +84 -75
  95. package/dist/plugins/pg-rate-limiter.js +3 -3
  96. package/dist/plugins/pg-session-storage.d.ts +1 -1
  97. package/dist/plugins/pg-session-storage.js +12 -13
  98. package/dist/plugins/remote-env-host.js +3 -1
  99. package/dist/plugins/remote-env-local-docker.js +6 -3
  100. package/dist/plugins/remote-env-ssh.d.ts +13 -1
  101. package/dist/plugins/roster-store-sql.js +8 -8
  102. package/dist/plugins/store-contracts.d.ts +19 -0
  103. package/dist/plugins/store-contracts.js +42 -0
  104. package/dist/plugins/task-attachment-store.js +5 -5
  105. package/dist/plugins/task-list-store-sql.js +1 -1
  106. package/dist/plugins/tidb-cost-quota.js +1 -1
  107. package/dist/plugins/tidb-pool.js +83 -60
  108. package/dist/plugins/tidb-rate-limiter.js +1 -1
  109. package/dist/plugins/tidb-session-store.js +2 -5
  110. package/dist/plugins/tool-result-store-sql.js +2 -2
  111. package/dist/plugins/usage-window-store-sql.js +13 -13
  112. package/dist/plugins/write-behind-counter.d.ts +10 -2
  113. package/dist/plugins/write-behind-counter.js +13 -3
  114. package/dist/resource-suspend.d.ts +3 -1
  115. package/dist/resource-suspend.js +3 -1
  116. package/dist/run-local.d.ts +73 -1
  117. package/dist/run-local.js +146 -5
  118. package/dist/runs.d.ts +11 -1
  119. package/dist/runs.js +18 -3
  120. package/dist/runtime-governance.d.ts +18 -0
  121. package/dist/runtime-governance.js +90 -3
  122. package/dist/security.d.ts +12 -0
  123. package/dist/security.js +12 -0
  124. package/dist/session-sync-kernel.d.ts +13 -0
  125. package/dist/session-sync-kernel.js +13 -0
  126. package/dist/task-settings.d.ts +3 -9
  127. package/dist/task-settings.js +16 -13
  128. package/dist/tool-approval.d.ts +33 -6
  129. package/dist/tool-approval.js +80 -23
  130. package/dist/trace/core-keyset-guard.d.ts +18 -4
  131. package/dist/trace/project.d.ts +10 -1
  132. package/dist/trace/project.js +31 -0
  133. package/package.json +3 -3
  134. package/dist/boot/lexical-path-env.d.ts +0 -10
  135. package/dist/boot/lexical-path-env.js +0 -88
  136. package/dist/capabilities/oa-tools.d.ts +0 -15
  137. package/dist/capabilities/oa-tools.js +0 -54
  138. package/dist/finance/cost-taxonomy.d.ts +0 -34
  139. package/dist/finance/cost-taxonomy.js +0 -26
  140. package/dist/plugins/approval-store-sql.d.ts +0 -116
  141. package/dist/plugins/approval-store-sql.js +0 -151
  142. package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
  143. package/dist/plugins/file-workflow-journal-store.js +0 -12
  144. package/dist/plugins/pg-approval-store.d.ts +0 -9
  145. package/dist/plugins/pg-approval-store.js +0 -9
  146. package/dist/plugins/pg-breaker-state.d.ts +0 -8
  147. package/dist/plugins/pg-breaker-state.js +0 -8
  148. package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
  149. package/dist/plugins/pg-checkpoint-store.js +0 -10
  150. package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
  151. package/dist/plugins/pg-file-snapshot-store.js +0 -8
  152. package/dist/plugins/pg-image-bake.d.ts +0 -12
  153. package/dist/plugins/pg-image-bake.js +0 -11
  154. package/dist/plugins/pg-image-index.d.ts +0 -12
  155. package/dist/plugins/pg-image-index.js +0 -11
  156. package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
  157. package/dist/plugins/pg-outcome-ledger.js +0 -11
  158. package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
  159. package/dist/plugins/pg-resume-anchor-store.js +0 -7
  160. package/dist/plugins/pg-run-store.d.ts +0 -9
  161. package/dist/plugins/pg-run-store.js +0 -9
  162. package/dist/plugins/pg-session-policy-store.d.ts +0 -7
  163. package/dist/plugins/pg-session-policy-store.js +0 -7
  164. package/dist/plugins/pg-session-store.d.ts +0 -12
  165. package/dist/plugins/pg-session-store.js +0 -12
  166. package/dist/plugins/pg-tool-result-store.d.ts +0 -9
  167. package/dist/plugins/pg-tool-result-store.js +0 -9
  168. package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
  169. package/dist/plugins/pg-workflow-journal-store.js +0 -9
  170. package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
  171. package/dist/plugins/pg-workflow-run-store.js +0 -9
  172. package/dist/plugins/tidb-approval-store.d.ts +0 -8
  173. package/dist/plugins/tidb-approval-store.js +0 -8
  174. package/dist/plugins/tidb-breaker-state.d.ts +0 -7
  175. package/dist/plugins/tidb-breaker-state.js +0 -7
  176. package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
  177. package/dist/plugins/tidb-checkpoint-store.js +0 -9
  178. package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
  179. package/dist/plugins/tidb-file-snapshot-store.js +0 -8
  180. package/dist/plugins/tidb-image-bake.d.ts +0 -12
  181. package/dist/plugins/tidb-image-bake.js +0 -11
  182. package/dist/plugins/tidb-image-index.d.ts +0 -12
  183. package/dist/plugins/tidb-image-index.js +0 -11
  184. package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
  185. package/dist/plugins/tidb-outcome-ledger.js +0 -12
  186. package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
  187. package/dist/plugins/tidb-resume-anchor-store.js +0 -7
  188. package/dist/plugins/tidb-run-store.d.ts +0 -10
  189. package/dist/plugins/tidb-run-store.js +0 -9
  190. package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
  191. package/dist/plugins/tidb-session-policy-store.js +0 -7
  192. package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
  193. package/dist/plugins/tidb-tool-result-store.js +0 -10
  194. package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
  195. package/dist/plugins/tidb-workflow-journal-store.js +0 -9
  196. package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
  197. package/dist/plugins/tidb-workflow-run-store.js +0 -10
  198. package/dist/plugins/workflow-journal-limits.d.ts +0 -12
  199. package/dist/plugins/workflow-journal-limits.js +0 -12
  200. package/dist/sema-registry.d.ts +0 -41
  201. package/dist/sema-registry.js +0 -40
@@ -74,8 +74,8 @@ export const SCHEMA_STATEMENTS = [
74
74
  -- setTitleIfNull — the user-facing RENAME layer lives BFF-side as a label overlay, not here).
75
75
  title VARCHAR(120) NULL,
76
76
  PRIMARY KEY (session_id),
77
- KEY idx_owner (owner)
78
- )`,
77
+ KEY idx_session_meta_owner (owner)
78
+ ) COLLATE utf8mb4_bin`,
79
79
  `CREATE TABLE IF NOT EXISTS session_event (
80
80
  session_id VARCHAR(64) NOT NULL,
81
81
  seq BIGINT NOT NULL,
@@ -85,11 +85,25 @@ export const SCHEMA_STATEMENTS = [
85
85
  payload JSON NOT NULL,
86
86
  ts DATETIME(3) NOT NULL,
87
87
  PRIMARY KEY (session_id, seq),
88
- UNIQUE KEY uk_entry (session_id, entry_id)
89
- )`,
88
+ UNIQUE KEY uq_session_event_entry (session_id, entry_id)
89
+ ) COLLATE utf8mb4_bin`,
90
90
  // Async run registry (S1) + replayable event log (S2). Any instance can serve a run's status /
91
91
  // event replay because both live here, not in a single instance's memory.
92
92
  `CREATE TABLE IF NOT EXISTS task_run (
93
+ -- ⚠️ 表级 COLLATE 的**非键列**影响(#192 A11,codex 复审 2026-08-08 round2 点名——先前"只有
94
+ -- outcome_ledger 有聚合语义变化"的说法过窄,如实更正):
95
+ -- · \`status\`:sourceSummary() 的 GROUP BY 键之一,是**闭集词表**(running/failed/…)由服务端铸字面量
96
+ -- 写入 ⇒ 字节分桶恒等于词表分桶,此变更对它无实际影响。
97
+ -- · \`source\`:同为 GROUP BY 键、且是 \`?source=\` 过滤列。🔴 **它不是闭集**(codex 复审 round3 纠正了
98
+ -- 本注的前一版:那版写它限于 oa/cc-mcp/portal,是错的)——真实来源是运维在 \`SERVICE_AUTH_TOKENS\`
99
+ -- 里写的 \`<token>=<system>\`,\`system\` 只过"非空"这一道(config.ts),大小写与重音全由运维自己定。
100
+ -- ⇒ 若某部署此前跑在 *_ci/_ai 库上、且配了只差大小写/重音的两个 system 名,升级后 sourceSummary 的
101
+ -- 分桶会**分裂**、\`?source=\` 也从宽松匹配变成字节精确。零存量用户下无人受影响;要恢复"宽松"语义
102
+ -- 应当去入口把 system 名规范化,而不是把库退回 *_ci(那会连隔离键一起放宽)。
103
+ -- · \`objective_preview\`:\`?q=\` 搜索谓词的被搜列。搜索侧用的是 \`LOWER(col) LIKE LOWER(?)\`
104
+ -- (run-store-sql.ts / pg-session-storage.ts 双方言同形,注里写明"collation-proof ci")⇒ 大小写面
105
+ -- 本就不依赖排序规则,不受影响。**残留**:若某部署此前跑在 *_ai(accent-insensitive)库上,重音
106
+ -- 折叠那一档会随之消失(LOWER() 不去重音)——零存量用户下无人受影响,但它是行为差,记在这里。
93
107
  task_id VARCHAR(64) NOT NULL,
94
108
  session_id VARCHAR(64) NOT NULL,
95
109
  owner VARCHAR(190) NULL,
@@ -124,19 +138,19 @@ export const SCHEMA_STATEMENTS = [
124
138
  created_at DATETIME(3) NOT NULL,
125
139
  updated_at DATETIME(3) NOT NULL,
126
140
  PRIMARY KEY (task_id),
127
- KEY idx_session_status (session_id, status),
128
- KEY idx_owner (owner),
141
+ KEY idx_task_run_session_status (session_id, status),
142
+ KEY idx_task_run_owner (owner),
129
143
  -- idx_job: WHERE job_id = ? (work-view grouping) would full-scan without it.
130
- KEY idx_job (job_id),
144
+ KEY idx_task_run_job (job_id),
131
145
  -- idx_created: listRuns ALWAYS ORDER BY created_at DESC — without this the trace list does a filesort on
132
146
  -- every query (deepseek council #2). Covers the unfiltered newest-first list; filtered lists narrow via
133
147
  -- idx_owner / idx_job first.
134
- KEY idx_created (created_at)
148
+ KEY idx_task_run_created (created_at)
135
149
  -- ⛔ DELIBERATELY NO KEY idx_error_code (error_code) (dropped 2026-07-26, clay 裁): a whole-repo sweep found
136
150
  -- ZERO code paths that filter on error_code — every reference is a SET or a projection — so the index only
137
151
  -- ever served an operator's ad-hoc WHERE error_code LIKE 'budget.%', while every task_run write paid its
138
152
  -- maintenance cost. If you need that ad-hoc query, build the index temporarily and drop it after.
139
- )`,
153
+ ) COLLATE utf8mb4_bin`,
140
154
  // Cross-replica circuit-breaker state (1.38). A row per breaker key ("<provider>:<id>"); each
141
155
  // replica write-through-caches its trips here and periodically refreshes so a provider outage is
142
156
  // learned fleet-wide instead of every replica re-discovering it. Advisory / eventually consistent
@@ -148,28 +162,28 @@ export const SCHEMA_STATEMENTS = [
148
162
  opened_at DATETIME(3) NULL,
149
163
  updated_at DATETIME(3) NOT NULL,
150
164
  PRIMARY KEY (breaker_key)
151
- )`,
165
+ ) COLLATE utf8mb4_bin`,
152
166
  // Cross-replica per-key CUMULATIVE cost quota (design/30 P0-1). One row per (scope_key, aligned window
153
167
  // bucket); replicas atomically `micro = micro + delta` (write-behind aggregation), so spend is summed
154
168
  // fleet-wide without losing increments. scope_key is opaque (principal today; team/feature later).
155
169
  `CREATE TABLE IF NOT EXISTS cost_quota (
156
170
  scope_key VARCHAR(190) NOT NULL,
157
171
  window_bucket BIGINT NOT NULL,
158
- micro BIGINT NOT NULL DEFAULT 0,
172
+ micro_usd BIGINT NOT NULL DEFAULT 0,
159
173
  updated_at DATETIME(3) NOT NULL,
160
174
  PRIMARY KEY (scope_key, window_bucket),
161
- KEY idx_window (window_bucket)
162
- )`,
175
+ KEY idx_cost_quota_window (window_bucket)
176
+ ) COLLATE utf8mb4_bin`,
163
177
  // Cross-replica per-key request rate limit (design/30 P0-2) — same write-behind-counter shape as
164
178
  // cost_quota (one row per key+aligned-window, atomic n = n + delta). Soft fleet-wide limiting.
165
179
  `CREATE TABLE IF NOT EXISTS rate_limit (
166
180
  limit_key VARCHAR(190) NOT NULL,
167
181
  window_bucket BIGINT NOT NULL,
168
- n INT NOT NULL DEFAULT 0,
182
+ hits INT NOT NULL DEFAULT 0,
169
183
  updated_at DATETIME(3) NOT NULL,
170
184
  PRIMARY KEY (limit_key, window_bucket),
171
- KEY idx_window (window_bucket)
172
- )`,
185
+ KEY idx_rate_limit_window (window_bucket)
186
+ ) COLLATE utf8mb4_bin`,
173
187
  // Durable backing for offloaded large tool results (core 1.47/1.49). One row per offload ref
174
188
  // (`tr_<sessionId>_<toolCallId>`, globally unique → PK); content is the full tool output moved out of
175
189
  // context. Write-once (INSERT IGNORE); TTL-reaped by created_at (idx_created). LONGTEXT holds large
@@ -179,8 +193,8 @@ export const SCHEMA_STATEMENTS = [
179
193
  content LONGTEXT NOT NULL,
180
194
  created_at DATETIME(3) NOT NULL,
181
195
  PRIMARY KEY (ref),
182
- KEY idx_created (created_at)
183
- )`,
196
+ KEY idx_tool_result_created (created_at)
197
+ ) COLLATE utf8mb4_bin`,
184
198
  `CREATE TABLE IF NOT EXISTS task_event (
185
199
  task_id VARCHAR(64) NOT NULL,
186
200
  seq BIGINT NOT NULL,
@@ -188,7 +202,7 @@ export const SCHEMA_STATEMENTS = [
188
202
  data JSON NULL,
189
203
  ts DATETIME(3) NOT NULL,
190
204
  PRIMARY KEY (task_id, seq)
191
- )`,
205
+ ) COLLATE utf8mb4_bin`,
192
206
  // Single-active-run-per-session claim. PRIMARY KEY(session_id) makes the claim a unique-key
193
207
  // insert: TiDB enforces it even with no prior row (it lacks InnoDB gap locks, so a
194
208
  // SELECT…FOR UPDATE on an empty match would NOT serialize two concurrent zero-prior createRuns).
@@ -197,8 +211,8 @@ export const SCHEMA_STATEMENTS = [
197
211
  task_id VARCHAR(64) NOT NULL,
198
212
  since DATETIME(3) NOT NULL,
199
213
  PRIMARY KEY (session_id),
200
- KEY idx_task (task_id)
201
- )`,
214
+ KEY idx_task_active_task (task_id)
215
+ ) COLLATE utf8mb4_bin`,
202
216
  // ([2354]/[2356] A-5,5.0.0:F4 轮询车道的 `approval` 表 DDL 已随车道退役——存量库里的旧表留置不动,
203
217
  // 代码零读写;durable 门只用下面的 checkpoint 表。)
204
218
  // Durable-checkpoint (core 1.67 / design/45): a policy `ask` persisted as a suspended task so an operator
@@ -263,8 +277,11 @@ export const SCHEMA_STATEMENTS = [
263
277
  -- pending_steer_rev:pending_steer_queue 列**自己**的乐观锁计数(与 resolve/reopen 的 rev 是两个轴,
264
278
  -- 刻意不复用)。队列化之后 setPendingSteer 从「一条无条件 UPDATE」变成「读-改-写」,中间那道窗必须
265
279
  -- 有 CAS 守着,否则两个并发 steer 各读到同一份队列、后写者把前者的条目整个盖掉。
266
- -- 🔴 为什么不拿队列列的旧值当守卫:本表没有显式 COLLATE,库默认多为 *_ci —— 字符串 <=> 在大小写
267
- -- 不敏感的排序规则下会把两份只差字母大小写的 JSON 判等,CAS 假命中 = 丢一条指令。整数列没有这个面。
280
+ -- 🔴 为什么不拿队列列的旧值当守卫:整数计数是**按构造**唯一的,长 JSON 文本的相等性则取决于列的排序
281
+ -- 规则、字段序、空白与转义形 —— 任何一处判等松一点,CAS 就假命中一次 = 悄悄丢一条 steer 指令。
282
+ -- (#192 A11 更正:本注原先的理由写的是"本表没有显式 COLLATE,库默认多为 *_ci",那半**已不成立** ——
283
+ -- 本表现在钉了 COLLATE utf8mb4_bin,大小写敏感面已由 schema 保证。但结论不变,只是理由回到更根本的
284
+ -- 那条:文本相等性有太多与业务无关的自由度,整数没有。)
268
285
  -- **绝不**用它做 resolve 的 OCC:bump 它不影响 core 的 rev 语义。
269
286
  pending_steer_rev BIGINT NOT NULL DEFAULT 0,
270
287
  -- risk_descriptor (design/80 riskDescriptor inbox): core's INERT CheckpointGate.riskDescriptor ({severity
@@ -273,9 +290,9 @@ export const SCHEMA_STATEMENTS = [
273
290
  -- NULL on gates with no descriptor.
274
291
  risk_descriptor TEXT NULL,
275
292
  PRIMARY KEY (token),
276
- KEY idx_status_deadline (status, deadline),
277
- KEY idx_session_status (session_id, status)
278
- )`,
293
+ KEY idx_checkpoint_status_deadline (status, deadline),
294
+ KEY idx_checkpoint_session_status (session_id, status)
295
+ ) COLLATE utf8mb4_bin`,
279
296
  // Service-side resume rebuild inputs (DURABLE-CHECKPOINT-PLAN §4.4a): sessionId-keyed because core's
280
297
  // put(token,cp) can't carry service scenario_ctx. GC'd by the reaper once its checkpoint is gone.
281
298
  `CREATE TABLE IF NOT EXISTS checkpoint_ctx (
@@ -283,7 +300,7 @@ export const SCHEMA_STATEMENTS = [
283
300
  ctx JSON NOT NULL,
284
301
  updated_at BIGINT NOT NULL,
285
302
  PRIMARY KEY (session_id)
286
- )`,
303
+ ) COLLATE utf8mb4_bin`,
287
304
  // E18 resume-at anchor map: the shell holds live E2 eventIds (never persisted by core); TaskSpec.resumeAt takes a
288
305
  // persisted SessionTreeEntry.id. This is the service-owned eventId→entryId translation. Keyed (session_id,event_id)
289
306
  // so resolve is a PK point-lookup; entry_id is the resolve target. owner = single-DB-fleet defense-in-depth (the
@@ -296,7 +313,7 @@ export const SCHEMA_STATEMENTS = [
296
313
  created_at DATETIME(3) NOT NULL,
297
314
  PRIMARY KEY (session_id, event_id),
298
315
  KEY idx_resume_anchor_session (session_id)
299
- )`,
316
+ ) COLLATE utf8mb4_bin`,
300
317
  // Per-session per-toolName approval exemption ("本会话不再询问") — an APPROVAL MEMORY the
301
318
  // F4 ask-gate consults AFTER deny/neverAuto (both always win). tool_name is CANONICAL space. GC'd with
302
319
  // the session (E21 delete). See sema-internal server/docs/design/APPROVAL-SESSION-EXEMPTION.md.
@@ -306,7 +323,7 @@ export const SCHEMA_STATEMENTS = [
306
323
  granted_by VARCHAR(190) NULL,
307
324
  created_at DATETIME(3) NOT NULL,
308
325
  PRIMARY KEY (session_id, tool_name)
309
- )`,
326
+ ) COLLATE utf8mb4_bin`,
310
327
  // SendUserFile server-side ledger (multi-tenant governance, 1.190.0 [768]②a second half): one row per issued
311
328
  // link — the scope↔object mapping the HASHED key segment deliberately hides from URLs (per-tenant list/revoke
312
329
  // handle). id = uuidv7 (time-ordered PK = the keyset cursor); scope = VERIFIED principal verbatim ("_" =
@@ -327,7 +344,7 @@ export const SCHEMA_STATEMENTS = [
327
344
  created_at DATETIME(3) NOT NULL,
328
345
  PRIMARY KEY (id),
329
346
  KEY idx_sendfile_link_scope (scope, id)
330
- )`,
347
+ ) COLLATE utf8mb4_bin`,
331
348
  // E6 durable SessionPolicyStore — operator-tightened per-(session,principal) tool-permission rules (CAS-versioned
332
349
  // tighten-only; core reads them at prepare-time, subtract-only). policy_key = sha256(JSON.stringify([sessionId,
333
350
  // principal ?? null])) — the SHA of core's exact composite key: a FIXED 64-char PK (an arbitrary-length / JSON-
@@ -352,7 +369,7 @@ export const SCHEMA_STATEMENTS = [
352
369
  updated_at DATETIME(3) NOT NULL,
353
370
  PRIMARY KEY (policy_key),
354
371
  KEY idx_session_policy_session (session_id)
355
- )`,
372
+ ) COLLATE utf8mb4_bin`,
356
373
  // E19 rewind-files — durable working-tree snapshots keyed by (scope=sessionId, snap_key=SessionTreeEntry.id; same
357
374
  // entryId anchor as E18). A per-(scope,key) manifest of relPath→blob_hash + content-addressed blobs (deduped across
358
375
  // all snapshots). core's captureManifest/applyManifest do the env walk/restore; this is just the durable persistence.
@@ -364,7 +381,7 @@ export const SCHEMA_STATEMENTS = [
364
381
  blob_hash CHAR(64) NOT NULL,
365
382
  KEY idx_snapshot_manifest_sk (scope, snap_key),
366
383
  KEY idx_snapshot_manifest_blob (blob_hash)
367
- )`,
384
+ ) COLLATE utf8mb4_bin`,
368
385
  // `bytes` is NULLABLE: a SQL-backend row carries the bytes inline; a MinIO-backend row keeps ONLY the INDEX
369
386
  // (blob_hash, byte_len, created_at) here with bytes NULL — the bytes live in MinIO. This INDEX row is what
370
387
  // gcOrphanBlobs / reap / deleteBySession (E21 purge) / sweepOrphanBlobs read to compute the orphan/grace SET, so
@@ -384,7 +401,7 @@ export const SCHEMA_STATEMENTS = [
384
401
  created_at DATETIME(3) NOT NULL,
385
402
  PRIMARY KEY (blob_hash),
386
403
  KEY idx_snapshot_blob_created (created_at)
387
- )`,
404
+ ) COLLATE utf8mb4_bin`,
388
405
  // SVC-2 (core CORE-7) durable WorkflowJournalStore — the cross-replica resume journal for an LLM-authored workflow.
389
406
  // One row per (run_id, ordinal): `ordinal` = core's callKeyOrdinal(callKey) (the positional replay index), `result`
390
407
  // = JSON.stringify(TaskResult). The (run_id, ordinal) PK makes append IDEMPOTENT (ON DUPLICATE KEY = OVERWRITE /
@@ -405,7 +422,7 @@ export const SCHEMA_STATEMENTS = [
405
422
  result MEDIUMTEXT NOT NULL,
406
423
  created_at BIGINT NOT NULL,
407
424
  PRIMARY KEY (run_id, ordinal)
408
- )`,
425
+ ) COLLATE utf8mb4_bin`,
409
426
  // RB-242 / WF2 ([1981] core 拍板 a 形):cross-replica workflow resume admission lease. PK (source_run_id, scope)
410
427
  // = the bake-store idem_key precedent — the INSERT either wins the claim or collides with the current holder.
411
428
  // TTL is the CRASH backstop only (engine releases on terminal via releaseResumeClaim); see workflow-journal-store-sql.ts.
@@ -415,7 +432,7 @@ export const SCHEMA_STATEMENTS = [
415
432
  new_run_id VARCHAR(191) NOT NULL,
416
433
  claimed_at BIGINT NOT NULL,
417
434
  PRIMARY KEY (source_run_id, scope)
418
- )`,
435
+ ) COLLATE utf8mb4_bin`,
419
436
  // P1 (fleet failover, 2026-07-05): durable cross-replica WorkflowRunStore twin — one row per run, the full
420
437
  // WorkflowRun as a JSON blob + extracted index columns; `rev` column = the OCC authority (blob rev overlaid
421
438
  // on read). MEDIUMTEXT (16MB) >> the 4MB write guard in the store.
@@ -430,7 +447,7 @@ export const SCHEMA_STATEMENTS = [
430
447
  PRIMARY KEY (id),
431
448
  KEY idx_wfrun_scope_created (scope, created_at),
432
449
  KEY idx_wfrun_scope_status (scope, status)
433
- )`,
450
+ ) COLLATE utf8mb4_bin`,
434
451
  // P1: cross-replica workflow completion inbox (the push half). seq = insertion order (re-enqueue
435
452
  // moves to tail via DELETE+INSERT). summary is bounded upstream (core notifier ≤4000).
436
453
  `CREATE TABLE IF NOT EXISTS workflow_completion_inbox (
@@ -453,7 +470,7 @@ export const SCHEMA_STATEMENTS = [
453
470
  PRIMARY KEY (session_id, run_id),
454
471
  UNIQUE KEY uq_wfinbox_seq (seq),
455
472
  KEY idx_wfinbox_session_seq (session_id, seq)
456
- )`,
473
+ ) COLLATE utf8mb4_bin`,
457
474
  // P1: inbox fences AS ROWS — the actual point of the SQL port: a poll served on replica A must fence the
458
475
  // late notify landing on replica B (cross-replica). kind = served (per-runId) | purge (run_id='',
459
476
  // session-wide; owner_known=0 → conservative fence-all). Swept opportunistically on every write verb.
@@ -466,7 +483,7 @@ export const SCHEMA_STATEMENTS = [
466
483
  until_ms BIGINT NOT NULL,
467
484
  PRIMARY KEY (session_id, kind, run_id),
468
485
  KEY idx_wffence_until (until_ms)
469
- )`,
486
+ ) COLLATE utf8mb4_bin`,
470
487
  // 1.108 review fix (lens③ HIGH): the notify JOURNAL twin — under WORKFLOW_RUN_STORE=auto the run store +
471
488
  // inbox were SQL but the notify journal stayed a replica-LOCAL File ledger, so a replica that died with an
472
489
  // un-acked notify stranded it forever (at-least-once silently degraded to at-most-once across replica death).
@@ -481,7 +498,7 @@ export const SCHEMA_STATEMENTS = [
481
498
  acked_at BIGINT NULL,
482
499
  PRIMARY KEY (run_id),
483
500
  KEY idx_wfnotify_acked (acked)
484
- )`,
501
+ ) COLLATE utf8mb4_bin`,
485
502
  // Outcome ledger (design/73 §7) — one row per graded task run, keyed by
486
503
  // (task_signature, model). v1 is READ-ONLY (dashboard + offline-backtest source); it drives NO
487
504
  // automatic policy change — that is flag-gated and must first pass an offline-backtest gate
@@ -491,6 +508,12 @@ export const SCHEMA_STATEMENTS = [
491
508
  // `outcome` is DERIVED from mechanical fields only (council blocker 1).
492
509
  `CREATE TABLE IF NOT EXISTS outcome_ledger (
493
510
  id VARCHAR(64) NOT NULL,
511
+ -- ⚠️ 表级 COLLATE 对本表**不只是钉住隔离键**(#192 A11,codex 复审 2026-08-08 点名):下面两列是
512
+ -- \`summary()\` 的 GROUP BY / ORDER BY 键(outcome-ledger-sql.ts),所以排序规则直接决定**分桶**。
513
+ -- utf8mb4_bin ⇒ 大小写/重音不同的 model id 各成一桶。这是**有意的**:model id 与 task signature 是
514
+ -- 机器铸的标识,不是自然语言;把 "GPT-x" 与 "gpt-x" 合成一桶是在替调用方猜,猜错了就是把两次不同
515
+ -- 实验的分数搅在一起。代价如实记:若某个部署此前跑在 *_ci 库上,升级后历史桶会**分裂**、顺序也会变
516
+ -- (字节序而非语言序)——零存量用户下无人受影响,但它是行为变化,不是纯粹的钉。
494
517
  task_signature VARCHAR(190) NOT NULL,
495
518
  model VARCHAR(190) NOT NULL,
496
519
  task_id VARCHAR(64) NULL,
@@ -513,10 +536,10 @@ export const SCHEMA_STATEMENTS = [
513
536
  core_outcome JSON NULL,
514
537
  created_at DATETIME(3) NOT NULL,
515
538
  PRIMARY KEY (id),
516
- KEY idx_sig_model (task_signature, model, run_status),
517
- KEY idx_outcome (outcome),
518
- KEY idx_created (created_at)
519
- )`,
539
+ KEY idx_outcome_ledger_sig_model (task_signature, model, run_status),
540
+ KEY idx_outcome_ledger_outcome (outcome),
541
+ KEY idx_outcome_ledger_created (created_at)
542
+ ) COLLATE utf8mb4_bin`,
520
543
  // sandbox-image-pool control API (IMAGE-API-DESIGN.md §1 / pool design §8). One row per image DIGEST (the
521
544
  // immutable runtime ref); a queryable projection of build.sh's replay-sufficient manifest + the publish lifecycle.
522
545
  `CREATE TABLE IF NOT EXISTS sandbox_image_index (
@@ -543,10 +566,10 @@ export const SCHEMA_STATEMENTS = [
543
566
  updated_at DATETIME(3) NOT NULL,
544
567
  PRIMARY KEY (id),
545
568
  UNIQUE KEY uq_repo_digest (repo, digest),
546
- KEY idx_profile_status (profile, status, visibility),
547
- KEY idx_digest (digest),
548
- KEY idx_tenant (tenant_id, status)
549
- )`,
569
+ KEY idx_sandbox_image_index_profile_status (profile, status, visibility),
570
+ KEY idx_sandbox_image_index_digest (digest),
571
+ KEY idx_sandbox_image_index_tenant (tenant_id, status)
572
+ ) COLLATE utf8mb4_bin`,
550
573
  // sandbox-image-pool BAKE job (IMAGE-API-DESIGN.md §P2.6). A dedicated row table — NOT a reuse of task_run
551
574
  // (FK-coupled to task_active + the run reaper + /v1/runs list; co-mingling bakes would leak non-task rows into
552
575
  // reapStale/listRuns). The reader polls the COARSE 4-value `status` (queued|running|done|failed) for SSE
@@ -558,9 +581,9 @@ export const SCHEMA_STATEMENTS = [
558
581
  profile VARCHAR(128) NOT NULL,
559
582
  bands JSON NULL,
560
583
  base_ref VARCHAR(255) NULL,
561
- push TINYINT NOT NULL DEFAULT 0,
562
- dry_run TINYINT NOT NULL DEFAULT 0,
563
- logs TINYINT NOT NULL DEFAULT 0,
584
+ push TINYINT(1) NOT NULL DEFAULT 0,
585
+ dry_run TINYINT(1) NOT NULL DEFAULT 0,
586
+ logs TINYINT(1) NOT NULL DEFAULT 0,
564
587
  argv JSON NOT NULL,
565
588
  status VARCHAR(16) NOT NULL,
566
589
  state VARCHAR(16) NULL,
@@ -577,19 +600,19 @@ export const SCHEMA_STATEMENTS = [
577
600
  ingest_secret VARCHAR(64) NULL,
578
601
  runner_id VARCHAR(64) NULL,
579
602
  lease_until DATETIME(3) NULL,
580
- cancel_requested TINYINT NOT NULL DEFAULT 0,
603
+ cancel_requested TINYINT(1) NOT NULL DEFAULT 0,
581
604
  requested_by VARCHAR(128) NULL,
582
605
  created_at DATETIME(3) NOT NULL,
583
606
  updated_at DATETIME(3) NOT NULL,
584
607
  PRIMARY KEY (bake_id),
585
- UNIQUE KEY uq_idem (idem_key),
586
- KEY idx_status (status),
587
- KEY idx_created (created_at)
588
- )`,
608
+ UNIQUE KEY uq_image_bake_idem (idem_key),
609
+ KEY idx_image_bake_status (status),
610
+ KEY idx_image_bake_created (created_at)
611
+ ) COLLATE utf8mb4_bin`,
589
612
  // Per-bake replayable event log (P2.6) — run-trace event-log mechanics ((id,seq) PK, ++seq, getEvents(after),
590
613
  // maxSeq, retainedFrom→416, heartbeat-not-persisted) WITHOUT the task_event FK coupling. image-api is the SOLE
591
614
  // ++seq writer (seq monotonic from 1). `line_ord` is the runner's per-line ordinal — the at-least-once ingest
592
- // dedupe key (uq_line): a re-sent (bakeId,line_ord) is a no-op (P2.10). `level='raw'` rows are the opt-in
615
+ // dedupe key (uq_image_bake_event_line): a re-sent (bakeId,line_ord) is a no-op (P2.10). `level='raw'` rows are the opt-in
593
616
  // build.log tail (?logs=1), pruned on a tighter TTL + evicted first (P2.5/P2.9).
594
617
  `CREATE TABLE IF NOT EXISTS image_bake_event (
595
618
  bake_id VARCHAR(64) NOT NULL,
@@ -600,8 +623,8 @@ export const SCHEMA_STATEMENTS = [
600
623
  data JSON NULL,
601
624
  ts DATETIME(3) NOT NULL,
602
625
  PRIMARY KEY (bake_id, seq),
603
- UNIQUE KEY uq_line (bake_id, line_ord)
604
- )`,
626
+ UNIQUE KEY uq_image_bake_event_line (bake_id, line_ord)
627
+ ) COLLATE utf8mb4_bin`,
605
628
  // Single-flight build lease (P2.7, §9). ONE row per builder pool — image-api owns the authoritative lease so the
606
629
  // one docker daemon never has two concurrent builds (a prune-behind-lock could evict layers an in-flight build
607
630
  // needs). `expires_at` makes a crashed-runner lease stealable (CAS on expiry).
@@ -611,7 +634,7 @@ export const SCHEMA_STATEMENTS = [
611
634
  acquired_at DATETIME(3) NOT NULL,
612
635
  expires_at DATETIME(3) NOT NULL,
613
636
  PRIMARY KEY (pool)
614
- )`,
637
+ ) COLLATE utf8mb4_bin`,
615
638
  // Admission sentinel: ONE always-present row per builder pool, `SELECT … FOR UPDATE`-locked to SERIALIZE the
616
639
  // createBakeIfIdle busy-check+insert. A bare conditional INSERT…SELECT WHERE NOT EXISTS does NOT serialize two
617
640
  // concurrent admissions under TiDB's REPEATABLE READ (nor PG's READ COMMITTED), so without this lock two keyless
@@ -619,7 +642,7 @@ export const SCHEMA_STATEMENTS = [
619
642
  `CREATE TABLE IF NOT EXISTS image_bake_admit (
620
643
  pool VARCHAR(32) NOT NULL,
621
644
  PRIMARY KEY (pool)
622
- )`,
645
+ ) COLLATE utf8mb4_bin`,
623
646
  // #151(design/172)流内审批协议持久层的两张表。真源 = approval-ask-store-sql.ts 的
624
647
  // `TIDB_APPROVAL_ASK_STATEMENTS`(那里与 PG twin 并排,方言差异一眼可对);展开进这个数组是为了让它们
625
648
  // 跟着中央 `ensureSchema` 在 **named-lock 的那条 conn** 上建 —— 把 pool 级 ensure 函数塞进执行循环
@@ -4,7 +4,7 @@ export class TiDBRateLimiter {
4
4
  counter;
5
5
  constructor(pool, limit, windowMs = 60_000, now, onDegraded, queryTimeoutMs) {
6
6
  this.limit = limit;
7
- this.counter = new WriteBehindCounter(pool, { table: "rate_limit", keyCol: "limit_key", valCol: "n" }, windowMs, now, onDegraded, queryTimeoutMs);
7
+ this.counter = new WriteBehindCounter(pool, { table: "rate_limit", keyCol: "limit_key", valCol: "hits" }, windowMs, now, onDegraded, queryTimeoutMs);
8
8
  }
9
9
  check(key) {
10
10
  if (this.limit <= 0)
@@ -15,11 +15,8 @@ import { StoredSession, SessionError, uuidv7, validateEntriesForImport, leafIdAf
15
15
  import { TiDBSessionStorage } from "./tidb-session-storage.js";
16
16
  import { escapeLike } from "./sql-escape.js";
17
17
  import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
18
- import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER, } from "../session-sync-kernel.js";
19
- /** Staging-row reaper grace window — a staging session's rows are GC'able only if it has NO session_meta (never
20
- * committed) AND its OLDEST row is older than this (an in-flight stream keeps appending fresh rows → safe). Mirrors
21
- * the file-snapshot store's BLOB_GC_GRACE_MS discipline (an in-flight import must never be reaped mid-stream). */
22
- const STAGING_GC_GRACE_MS = 3_600_000;
18
+ import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, // 两孪生店同源(理由见 kernel 处顶注:同值双抄单边改值 ⇒ 两后端 GC 判定分家)
19
+ } from "../session-sync-kernel.js";
23
20
  export class TiDBSessionStore {
24
21
  pool;
25
22
  /** In-flight acquisitions keyed by id, so concurrent acquire(sameId) in one process share one. */
@@ -44,8 +44,8 @@ import { mysqlDriver, pgDriver } from "./sql-driver.js";
44
44
  * tables, so a self-contained ensureSchema is safe (central pg-pool aggregation is done separately). */
45
45
  export const PG_TOOL_RESULT_SCHEMA = [
46
46
  `CREATE TABLE IF NOT EXISTS tool_result (
47
- ref VARCHAR(190) NOT NULL,
48
- content TEXT NOT NULL,
47
+ ref VARCHAR(190) COLLATE "C" NOT NULL,
48
+ content TEXT COLLATE "C" NOT NULL,
49
49
  created_at TIMESTAMPTZ(3) NOT NULL,
50
50
  PRIMARY KEY (ref)
51
51
  )`,
@@ -39,18 +39,18 @@ function parseRecord(raw, key) {
39
39
  }
40
40
  export async function ensureTiDBUsageWindowSchema(pool) {
41
41
  await pool.query(`CREATE TABLE IF NOT EXISTS ${USAGE_WINDOW_TABLE} (
42
- k VARCHAR(190) NOT NULL,
42
+ usage_key VARCHAR(190) NOT NULL,
43
43
  record LONGTEXT NOT NULL,
44
44
  updated_at_ms BIGINT NOT NULL,
45
- PRIMARY KEY (k)
45
+ PRIMARY KEY (usage_key)
46
46
  ) COLLATE utf8mb4_bin`);
47
47
  }
48
48
  export async function ensurePgUsageWindowSchema(q) {
49
49
  await q(`CREATE TABLE IF NOT EXISTS ${USAGE_WINDOW_TABLE} (
50
- k VARCHAR(190) NOT NULL,
51
- record TEXT NOT NULL,
50
+ usage_key VARCHAR(190) COLLATE "C" NOT NULL,
51
+ record TEXT COLLATE "C" NOT NULL,
52
52
  updated_at_ms BIGINT NOT NULL,
53
- PRIMARY KEY (k)
53
+ PRIMARY KEY (usage_key)
54
54
  )`);
55
55
  }
56
56
  export class TiDBUsageWindowStore {
@@ -63,19 +63,19 @@ export class TiDBUsageWindowStore {
63
63
  try {
64
64
  await c.beginTransaction();
65
65
  // ensure-first:幂等建空行(EMPTY record)⇒ 下一步 FOR UPDATE 行恒在(锁语义干净,无 gap-lock 死锁形)。
66
- await c.query(`INSERT IGNORE INTO ${USAGE_WINDOW_TABLE} (k, record, updated_at_ms) VALUES (?, ?, ?)`, [
66
+ await c.query(`INSERT IGNORE INTO ${USAGE_WINDOW_TABLE} (usage_key, record, updated_at_ms) VALUES (?, ?, ?)`, [
67
67
  key,
68
68
  JSON.stringify(EMPTY_USAGE_WINDOW_RECORD),
69
69
  at,
70
70
  ]);
71
- const [rows] = (await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE k = ? FOR UPDATE`, [key]));
71
+ const [rows] = (await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ? FOR UPDATE`, [key]));
72
72
  if (rows.length === 0) {
73
73
  // 本表无删除臂(不同于 mailbox 的 drop/reap),ensure 后空 = 真障碍 fail-loud。
74
74
  throw new Error(`usage_window row for key "${key}" vanished between ensure and lock — refusing to drop a governance charge`);
75
75
  }
76
76
  const record = parseRecord(rows[0].record, key);
77
77
  const next = chargeUsageRecord(record, tokens, at, windows);
78
- await c.query(`UPDATE ${USAGE_WINDOW_TABLE} SET record = ?, updated_at_ms = ? WHERE k = ?`, [JSON.stringify(next), at, key]);
78
+ await c.query(`UPDATE ${USAGE_WINDOW_TABLE} SET record = ?, updated_at_ms = ? WHERE usage_key = ?`, [JSON.stringify(next), at, key]);
79
79
  await c.commit();
80
80
  }
81
81
  catch (e) {
@@ -87,7 +87,7 @@ export class TiDBUsageWindowStore {
87
87
  }
88
88
  }
89
89
  async read(key, windows, now) {
90
- const [rows] = (await this.pool.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE k = ?`, [key]));
90
+ const [rows] = (await this.pool.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ?`, [key]));
91
91
  const record = rows.length === 0 ? EMPTY_USAGE_WINDOW_RECORD : parseRecord(rows[0].record, key);
92
92
  return readUsageRecord(record, windows, now);
93
93
  }
@@ -103,18 +103,18 @@ export class PgUsageWindowStore {
103
103
  await c.query("BEGIN");
104
104
  // ensure-first(TiDB 同注):ON CONFLICT DO NOTHING 幂等建空行,再锁——插入不带锁,锁只从
105
105
  // FOR UPDATE 来(复审靶1 的 PG 交错:并发首充双双 ensure,后到方 DO NOTHING 后必须真锁行)。
106
- await c.query(`INSERT INTO ${USAGE_WINDOW_TABLE} (k, record, updated_at_ms) VALUES ($1, $2, $3) ON CONFLICT (k) DO NOTHING`, [
106
+ await c.query(`INSERT INTO ${USAGE_WINDOW_TABLE} (usage_key, record, updated_at_ms) VALUES ($1, $2, $3) ON CONFLICT (usage_key) DO NOTHING`, [
107
107
  key,
108
108
  JSON.stringify(EMPTY_USAGE_WINDOW_RECORD),
109
109
  at,
110
110
  ]);
111
- const { rows } = await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE k = $1 FOR UPDATE`, [key]);
111
+ const { rows } = await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = $1 FOR UPDATE`, [key]);
112
112
  if (rows.length === 0) {
113
113
  throw new Error(`usage_window row for key "${key}" vanished between ensure and lock — refusing to drop a governance charge`);
114
114
  }
115
115
  const record = parseRecord(rows[0].record, key);
116
116
  const next = chargeUsageRecord(record, tokens, at, windows);
117
- await c.query(`UPDATE ${USAGE_WINDOW_TABLE} SET record = $1, updated_at_ms = $2 WHERE k = $3`, [JSON.stringify(next), at, key]);
117
+ await c.query(`UPDATE ${USAGE_WINDOW_TABLE} SET record = $1, updated_at_ms = $2 WHERE usage_key = $3`, [JSON.stringify(next), at, key]);
118
118
  await c.query("COMMIT");
119
119
  }
120
120
  catch (e) {
@@ -126,7 +126,7 @@ export class PgUsageWindowStore {
126
126
  }
127
127
  }
128
128
  async read(key, windows, now) {
129
- const { rows } = await this.pool.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE k = $1`, [key]);
129
+ const { rows } = await this.pool.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = $1`, [key]);
130
130
  const record = rows.length === 0 ? EMPTY_USAGE_WINDOW_RECORD : parseRecord(rows[0].record, key);
131
131
  return readUsageRecord(record, windows, now);
132
132
  }
@@ -42,7 +42,7 @@ export interface CounterTable {
42
42
  * kind (0 = recovered); `prevStreak` lets the consumer detect the recovery edge. Shared by both dialects. */
43
43
  export interface CounterDegradeInfo {
44
44
  table: string;
45
- kind: "flush" | "refresh" | "stalled";
45
+ kind: "flush" | "refresh" | "stalled" | "reap";
46
46
  streak: number;
47
47
  prevStreak: number;
48
48
  error?: string;
@@ -91,7 +91,15 @@ export declare class SqlWriteBehindCounter {
91
91
  private flushOnce;
92
92
  /** Load the current window's fleet totals into the cache (pendingDelta is separate → never clobbered). */
93
93
  refresh(): Promise<void>;
94
- /** Delete expired window buckets to bound the table. */
94
+ /**
95
+ * Delete expired window buckets to bound the table.
96
+ *
97
+ * 🔴 失败**必须留痕**(#192 codex 复审 round2 修):这里原先是裸 `catch { return 0 }` —— 一个只影响
98
+ * DELETE 的故障(权限被收回、引擎单独拒 DELETE)会被吞成"删了 0 行",而 flush/refresh 两条腿照常成功
99
+ * ⇒ 健康遥测全绿,同时 rate_limit / cost_quota 的历史桶无界增长。那正是本仓禁止的静默 fail-open。
100
+ * 处置是**留痕不改失败方向**:清理是 best-effort,失败仍返回 0(把一次运维层面的清理失败升级成业务
101
+ * 失败是错的方向),但走与 flush/refresh 同一套 `bump()` ⇒ 有连败计数、有降级钩子、有恢复边沿。
102
+ */
95
103
  reap(): Promise<number>;
96
104
  startRefresh(intervalMs?: number): this;
97
105
  stop(): void;
@@ -171,16 +171,26 @@ export class SqlWriteBehindCounter {
171
171
  this.bump("refresh", true, err); // S9
172
172
  }
173
173
  }
174
- /** Delete expired window buckets to bound the table. */
174
+ /**
175
+ * Delete expired window buckets to bound the table.
176
+ *
177
+ * 🔴 失败**必须留痕**(#192 codex 复审 round2 修):这里原先是裸 `catch { return 0 }` —— 一个只影响
178
+ * DELETE 的故障(权限被收回、引擎单独拒 DELETE)会被吞成"删了 0 行",而 flush/refresh 两条腿照常成功
179
+ * ⇒ 健康遥测全绿,同时 rate_limit / cost_quota 的历史桶无界增长。那正是本仓禁止的静默 fail-open。
180
+ * 处置是**留痕不改失败方向**:清理是 best-effort,失败仍返回 0(把一次运维层面的清理失败升级成业务
181
+ * 失败是错的方向),但走与 flush/refresh 同一套 `bump()` ⇒ 有连败计数、有降级钩子、有恢复边沿。
182
+ */
175
183
  async reap() {
176
184
  try {
177
185
  const { affected } = await this.exec(this.dialect === "tidb"
178
186
  ? `DELETE FROM ${this.t.table} WHERE window_bucket < ?`
179
187
  : `DELETE FROM ${this.t.table} WHERE window_bucket < $1`, [this.bucket()]);
188
+ this.bump("reap", false); // 恢复边沿(健康时是 no-op)
180
189
  return affected;
181
190
  }
182
- catch {
183
- return 0;
191
+ catch (err) {
192
+ this.bump("reap", true, err);
193
+ return 0; // best-effort:留痕之后照旧不抛
184
194
  }
185
195
  }
186
196
  startRefresh(intervalMs = 5_000) {
@@ -6,7 +6,9 @@
6
6
  * `POST /v1/assistant/tasks/:id/resume`) instead of FAILING; absent ⇒ a limit still FAILS.
7
7
  *
8
8
  * 🔴 EXCLUDE verify/cascade tasks. Core runs their implementation via `runWithVerification`/`runCascade`, which
9
- * map an inner durable suspend to a FAILED result (`errorCode: "suspended.awaiting_approval"`,core 5.9.0 W1 改名). The service would then
9
+ * map an inner durable suspend to a FAILED result (`errorCode: "unexpected.suspended"`,内层 status
10
+ * "needs_review" 时铸 `"unexpected.needs_review"` —— 真源是 core `agents/suspend-guard.js` 的
11
+ * `mapNestedSuspend`;`suspended.*` 三码是 bg 子代/委派报告面的词,不落在这条 TaskResult 腿上). The service would then
10
12
  * `setTerminal(failed)` while core had already minted a pending `resource_limit` checkpoint + suspended the
11
13
  * remote VM — ORPHANING both (the run row is terminal, so nothing resumes them). So a resource-limit hit on a
12
14
  * verify/cascade task must FAIL CLEANLY (no suspend), exactly as before the flag. This exclusion is the SAME
@@ -6,7 +6,9 @@
6
6
  * `POST /v1/assistant/tasks/:id/resume`) instead of FAILING; absent ⇒ a limit still FAILS.
7
7
  *
8
8
  * 🔴 EXCLUDE verify/cascade tasks. Core runs their implementation via `runWithVerification`/`runCascade`, which
9
- * map an inner durable suspend to a FAILED result (`errorCode: "suspended.awaiting_approval"`,core 5.9.0 W1 改名). The service would then
9
+ * map an inner durable suspend to a FAILED result (`errorCode: "unexpected.suspended"`,内层 status
10
+ * "needs_review" 时铸 `"unexpected.needs_review"` —— 真源是 core `agents/suspend-guard.js` 的
11
+ * `mapNestedSuspend`;`suspended.*` 三码是 bg 子代/委派报告面的词,不落在这条 TaskResult 腿上). The service would then
10
12
  * `setTerminal(failed)` while core had already minted a pending `resource_limit` checkpoint + suspended the
11
13
  * remote VM — ORPHANING both (the run row is terminal, so nothing resumes them). So a resource-limit hit on a
12
14
  * verify/cascade task must FAIL CLEANLY (no suspend), exactly as before the flag. This exclusion is the SAME