@sema-agent/server 7.5.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 (94) 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 +37 -1
  5. package/dist/approval-reconciler.d.ts +1 -1
  6. package/dist/approval-reconciler.js +6 -5
  7. package/dist/boot/config-center.js +15 -2
  8. package/dist/boot/execution-env.js +1 -1
  9. package/dist/boot/parked-revive-gate.d.ts +78 -0
  10. package/dist/boot/parked-revive-gate.js +114 -0
  11. package/dist/boot/resolve-spec.d.ts +3 -20
  12. package/dist/boot/resolve-spec.js +25 -119
  13. package/dist/budget.d.ts +1 -1
  14. package/dist/budget.js +1 -1
  15. package/dist/capabilities/repo-tools.d.ts +1 -1
  16. package/dist/capabilities/repo-tools.js +8 -2
  17. package/dist/config-provider.d.ts +1 -0
  18. package/dist/config-provider.js +23 -3
  19. package/dist/config-types.d.ts +5 -3
  20. package/dist/config.js +5 -4
  21. package/dist/deployment-governance.d.ts +168 -0
  22. package/dist/deployment-governance.js +206 -0
  23. package/dist/fleet/fleet-bus.d.ts +11 -1
  24. package/dist/fleet/fleet-bus.js +43 -0
  25. package/dist/fleet/fleet-terminal-window.d.ts +98 -0
  26. package/dist/fleet/fleet-terminal-window.js +316 -0
  27. package/dist/http/routes/approvals-assistant.js +6 -7
  28. package/dist/http/routes/fleet.js +160 -14
  29. package/dist/http/routes/runs.js +5 -1
  30. package/dist/http/routes/trace-usage.js +3 -4
  31. package/dist/http/send.d.ts +23 -0
  32. package/dist/http/send.js +23 -0
  33. package/dist/http/server.d.ts +4 -0
  34. package/dist/http/server.js +5 -2
  35. package/dist/http/sse-log.js +3 -4
  36. package/dist/leader/diffout.d.ts +10 -0
  37. package/dist/leader/diffout.js +14 -2
  38. package/dist/leader/diffup.js +3 -2
  39. package/dist/leader/planner.js +7 -0
  40. package/dist/main.js +22 -26
  41. package/dist/observability/fail-open.d.ts +4 -0
  42. package/dist/observability/fail-open.js +4 -0
  43. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  44. package/dist/orchestration/workflow-notify-journal.js +14 -34
  45. package/dist/plugins/approval-ask-store-sql.d.ts +33 -0
  46. package/dist/plugins/approval-ask-store-sql.js +66 -33
  47. package/dist/plugins/background-agent-store-sql.js +16 -16
  48. package/dist/plugins/breaker-state-sql.js +2 -2
  49. package/dist/plugins/checkpoint-store-sql.d.ts +5 -2
  50. package/dist/plugins/checkpoint-store-sql.js +5 -2
  51. package/dist/plugins/image-bake-store-sql.d.ts +1 -1
  52. package/dist/plugins/image-bake-store-sql.js +27 -27
  53. package/dist/plugins/image-index-sql.js +15 -15
  54. package/dist/plugins/mailbox-store-sql.js +3 -3
  55. package/dist/plugins/memory-engine-pg.js +9 -9
  56. package/dist/plugins/memory-engine-tidb.js +7 -7
  57. package/dist/plugins/memory-sync-store-pg.js +13 -13
  58. package/dist/plugins/memory-sync-store-tidb.js +5 -5
  59. package/dist/plugins/outcome-ledger-sql.js +7 -7
  60. package/dist/plugins/pg-cost-quota.js +3 -3
  61. package/dist/plugins/pg-pool.js +84 -75
  62. package/dist/plugins/pg-rate-limiter.js +3 -3
  63. package/dist/plugins/pg-session-storage.d.ts +1 -1
  64. package/dist/plugins/pg-session-storage.js +12 -13
  65. package/dist/plugins/remote-env-host.js +3 -1
  66. package/dist/plugins/remote-env-local-docker.js +6 -3
  67. package/dist/plugins/remote-env-ssh.d.ts +13 -1
  68. package/dist/plugins/roster-store-sql.js +8 -8
  69. package/dist/plugins/store-contracts.d.ts +19 -0
  70. package/dist/plugins/store-contracts.js +42 -0
  71. package/dist/plugins/task-attachment-store.js +5 -5
  72. package/dist/plugins/task-list-store-sql.js +1 -1
  73. package/dist/plugins/tidb-cost-quota.js +1 -1
  74. package/dist/plugins/tidb-pool.js +83 -60
  75. package/dist/plugins/tidb-rate-limiter.js +1 -1
  76. package/dist/plugins/tidb-session-store.js +2 -5
  77. package/dist/plugins/tool-result-store-sql.js +2 -2
  78. package/dist/plugins/usage-window-store-sql.js +13 -13
  79. package/dist/plugins/write-behind-counter.d.ts +10 -2
  80. package/dist/plugins/write-behind-counter.js +13 -3
  81. package/dist/resource-suspend.d.ts +3 -1
  82. package/dist/resource-suspend.js +3 -1
  83. package/dist/run-local.d.ts +73 -1
  84. package/dist/run-local.js +146 -5
  85. package/dist/runs.d.ts +3 -1
  86. package/dist/runs.js +3 -1
  87. package/dist/security.d.ts +12 -0
  88. package/dist/security.js +12 -0
  89. package/dist/session-sync-kernel.d.ts +13 -0
  90. package/dist/session-sync-kernel.js +13 -0
  91. package/dist/trace/core-keyset-guard.d.ts +1 -1
  92. package/dist/trace/project.d.ts +10 -1
  93. package/dist/trace/project.js +31 -0
  94. package/package.json +3 -3
@@ -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,
@@ -163,7 +163,7 @@ export async function ensureTiDBBackgroundAgentSchema(pool) {
163
163
  handle VARBINARY(190) NOT NULL,
164
164
  scope VARCHAR(190) NOT NULL,
165
165
  owner VARCHAR(190) NOT NULL,
166
- session_scoped TINYINT NOT NULL,
166
+ session_scoped TINYINT(1) NOT NULL,
167
167
  session_id VARCHAR(190) NULL,
168
168
  parent_session_id VARCHAR(190) NULL,
169
169
  -- core 1.367 δ(additive):root 锚(listBySession 第二臂;NULL = 该记录无 root 锚,谓词天然拒)
@@ -198,29 +198,29 @@ export async function ensurePgBackgroundAgentSchema(q) {
198
198
  await q(`CREATE TABLE IF NOT EXISTS ${BACKGROUND_AGENT_TABLE} (
199
199
  scope_key VARCHAR(190) COLLATE "C" NOT NULL,
200
200
  handle VARCHAR(190) COLLATE "C" NOT NULL,
201
- scope VARCHAR(190) NOT NULL,
202
- owner VARCHAR(190) NOT NULL,
201
+ scope VARCHAR(190) COLLATE "C" NOT NULL,
202
+ owner VARCHAR(190) COLLATE "C" NOT NULL,
203
203
  session_scoped SMALLINT NOT NULL,
204
- session_id VARCHAR(190),
205
- parent_session_id VARCHAR(190),
204
+ session_id VARCHAR(190) COLLATE "C",
205
+ parent_session_id VARCHAR(190) COLLATE "C",
206
206
  -- core 1.367 δ(additive,TiDB 同案):root 锚(listBySession 第二臂)
207
- root_session_id VARCHAR(190),
208
- session_anchor VARCHAR(190),
209
- name VARCHAR(255),
210
- agent_type VARCHAR(190),
211
- status VARCHAR(16) NOT NULL,
207
+ root_session_id VARCHAR(190) COLLATE "C",
208
+ session_anchor VARCHAR(190) COLLATE "C",
209
+ name VARCHAR(255) COLLATE "C",
210
+ agent_type VARCHAR(190) COLLATE "C",
211
+ status VARCHAR(16) COLLATE "C" NOT NULL,
212
212
  spawned_at_ms BIGINT NOT NULL,
213
213
  updated_at_ms BIGINT NOT NULL,
214
214
  settled_at_ms BIGINT,
215
- usage_json TEXT,
215
+ usage_json TEXT COLLATE "C",
216
216
  rev BIGINT NOT NULL,
217
217
  -- core 1.383([1565] / design/153 件3a)ε(TiDB 同案注):updateIf 的 CAS 守卫专用列
218
- parked_checkpoint_token VARCHAR(190),
219
- park_claim_id VARCHAR(190),
218
+ parked_checkpoint_token VARCHAR(190) COLLATE "C",
219
+ park_claim_id VARCHAR(190) COLLATE "C",
220
220
  -- core 5.14.0 #22(TiDB 同案注:三态与「写侧投影」口径见 MySQL 孪生的行内注)。
221
- admitted_org_scopes TEXT,
222
- admitted_org_write_scope VARCHAR(190),
223
- record_json TEXT NOT NULL,
221
+ admitted_org_scopes TEXT COLLATE "C",
222
+ admitted_org_write_scope VARCHAR(190) COLLATE "C",
223
+ record_json TEXT COLLATE "C" NOT NULL,
224
224
  PRIMARY KEY (scope_key, handle)
225
225
  )`);
226
226
  await q(`CREATE INDEX IF NOT EXISTS idx_bga_anchor ON ${BACKGROUND_AGENT_TABLE} (scope_key, session_anchor, spawned_at_ms)`);
@@ -4,8 +4,8 @@ import { mysqlDriver, pgDriver } from "./sql-driver.js";
4
4
  * centrally in tidb-pool.ts (no PG-side twin needed there — production wires ONE ensureTidbSchema). */
5
5
  export const PG_BREAKER_STATE_SCHEMA = [
6
6
  `CREATE TABLE IF NOT EXISTS circuit_breaker (
7
- breaker_key VARCHAR(190) NOT NULL,
8
- phase VARCHAR(16) NOT NULL,
7
+ breaker_key VARCHAR(190) COLLATE "C" NOT NULL,
8
+ phase VARCHAR(16) COLLATE "C" NOT NULL,
9
9
  failures INTEGER NOT NULL DEFAULT 0,
10
10
  opened_at TIMESTAMPTZ(3),
11
11
  updated_at TIMESTAMPTZ(3) NOT NULL,
@@ -226,8 +226,11 @@ export declare class SqlCheckpointStore implements CheckpointStore {
226
226
  *
227
227
  * 🔴 从「一条无条件 UPDATE」变成「读-改-写」,中间那道窗必须自己有 CAS:两个并发 steer 读到同一份
228
228
  * 队列、后写者整个盖掉前者 —— 就是 #147 要消灭的静默丢失换了个位置复发。守卫是 `pending_steer_rev`
229
- * **专用整数列**(不是拿旧列值比:本表无显式 COLLATE,*_ci 排序规则会把只差大小写的两份信封判等 ⇒
230
- * CAS 假命中;也不是复用 resolve `rev`:bump 它会让并发 resume 的 `resolve(expect)` 白白落败)。
229
+ * **专用整数列**(不是拿旧列值比:整数按构造唯一,而长 JSON 文本的相等性取决于排序规则/字段序/空白与
230
+ * 转义形,任一处判等松一点就是一次 CAS 假命中 = 悄悄丢一条 steer。#192 A11 更正:本注原先的理由写的是
231
+ * "本表无显式 COLLATE,*_ci 会把只差大小写的两份信封判等",那半已不成立——`checkpoint` 现在钉了
232
+ * `COLLATE utf8mb4_bin`/PG 侧 `COLLATE "C"`;结论不变,理由回到上面那条更根本的;
233
+ * 也不是复用 resolve 的 `rev`:bump 它会让并发 resume 的 `resolve(expect)` 白白落败)。
231
234
  * CAS 输了就重读重算(队列只有 3 个位子,争用者天然极少);给足冗余仍全败 ⇒ **抛**,不返回 false ——
232
235
  * `false` 在调用方的语义是「没有 pending 行可转向」(HTTP 409「已不再挂起」),把争用报成那个是撒谎。
233
236
  *
@@ -454,8 +454,11 @@ export class SqlCheckpointStore {
454
454
  *
455
455
  * 🔴 从「一条无条件 UPDATE」变成「读-改-写」,中间那道窗必须自己有 CAS:两个并发 steer 读到同一份
456
456
  * 队列、后写者整个盖掉前者 —— 就是 #147 要消灭的静默丢失换了个位置复发。守卫是 `pending_steer_rev`
457
- * **专用整数列**(不是拿旧列值比:本表无显式 COLLATE,*_ci 排序规则会把只差大小写的两份信封判等 ⇒
458
- * CAS 假命中;也不是复用 resolve `rev`:bump 它会让并发 resume 的 `resolve(expect)` 白白落败)。
457
+ * **专用整数列**(不是拿旧列值比:整数按构造唯一,而长 JSON 文本的相等性取决于排序规则/字段序/空白与
458
+ * 转义形,任一处判等松一点就是一次 CAS 假命中 = 悄悄丢一条 steer。#192 A11 更正:本注原先的理由写的是
459
+ * "本表无显式 COLLATE,*_ci 会把只差大小写的两份信封判等",那半已不成立——`checkpoint` 现在钉了
460
+ * `COLLATE utf8mb4_bin`/PG 侧 `COLLATE "C"`;结论不变,理由回到上面那条更根本的;
461
+ * 也不是复用 resolve 的 `rev`:bump 它会让并发 resume 的 `resolve(expect)` 白白落败)。
459
462
  * CAS 输了就重读重算(队列只有 3 个位子,争用者天然极少);给足冗余仍全败 ⇒ **抛**,不返回 false ——
460
463
  * `false` 在调用方的语义是「没有 pending 行可转向」(HTTP 409「已不再挂起」),把争用报成那个是撒谎。
461
464
  *
@@ -7,7 +7,7 @@ import { type BakeStatus, type BakeState, type BakeRecord, type BakeEvent, type
7
7
  * TINYINT(1) 归一(clay 批 2026-07-26):布尔列的 PG 映射统一 SMALLINT 0/1(此前本家族用 BOOLEAN)——
8
8
  * 两方言驱动读回同为 number ⇒ 不再存在「忘了配的适配器」这个病族;写入参与 SQL 字面量一律 0/1,
9
9
  * 读侧 `!!`/`Boolean()` 保持,store 出口 JS 类型逐字节不变(真双库套件的严格值断言为证)。
10
- * The uq_line UNIQUE is given an explicit name so a 23505 can be attributed to it via err.constraint. */
10
+ * The line UNIQUE is given the explicit name uq_image_bake_event_line so a 23505 can be attributed to it via err.constraint. */
11
11
  export declare const PG_IMAGE_BAKE_SCHEMA: string[];
12
12
  /** Idempotent self-contained PG schema apply (for the integration test; central aggregation is done separately). */
13
13
  export declare function ensurePgImageBakeSchema(pool: PgPool | PoolClient, poolName?: string): Promise<void>;
@@ -26,7 +26,7 @@
26
26
  * - `?` placeholders vs `$n` - `CAST(? AS JSON)` vs `$n::jsonb` (argv equality)
27
27
  * - `INSERT IGNORE` vs `ON CONFLICT DO NOTHING` - affectedRows vs rowCount (via SqlDriver)
28
28
  * - dup-key `ER_DUP_ENTRY` vs SQLSTATE `23505` - which UNIQUE collided: mysql2 `sqlMessage` text
29
- * vs pg `err.constraint` (uq_line naming differs)
29
+ * vs pg `err.constraint` (#192 A5 归一后两条方言同名 uq_image_bake_event_line)
30
30
  * - `BEGIN PESSIMISTIC` (TiDB current read) vs `BEGIN` (PG READ COMMITTED under the sentinel lock)
31
31
  * - the ADMISSION statement itself differs (see createBakeIfIdle) — both are correct under the
32
32
  * sentinel lock, and both are preserved verbatim because the real-DB suites are the oracle
@@ -46,34 +46,34 @@ import { mapBakeRow as mapRow, BAKE_SELECT_COLS as SELECT_COLS, } from "./store-
46
46
  * TINYINT(1) 归一(clay 批 2026-07-26):布尔列的 PG 映射统一 SMALLINT 0/1(此前本家族用 BOOLEAN)——
47
47
  * 两方言驱动读回同为 number ⇒ 不再存在「忘了配的适配器」这个病族;写入参与 SQL 字面量一律 0/1,
48
48
  * 读侧 `!!`/`Boolean()` 保持,store 出口 JS 类型逐字节不变(真双库套件的严格值断言为证)。
49
- * The uq_line UNIQUE is given an explicit name so a 23505 can be attributed to it via err.constraint. */
49
+ * The line UNIQUE is given the explicit name uq_image_bake_event_line so a 23505 can be attributed to it via err.constraint. */
50
50
  export const PG_IMAGE_BAKE_SCHEMA = [
51
51
  `CREATE TABLE IF NOT EXISTS image_bake (
52
- bake_id VARCHAR(64) NOT NULL,
53
- profile VARCHAR(128) NOT NULL,
52
+ bake_id VARCHAR(64) COLLATE "C" NOT NULL,
53
+ profile VARCHAR(128) COLLATE "C" NOT NULL,
54
54
  bands JSONB NULL,
55
- base_ref VARCHAR(255) NULL,
55
+ base_ref VARCHAR(255) COLLATE "C" NULL,
56
56
  push SMALLINT NOT NULL DEFAULT 0,
57
57
  dry_run SMALLINT NOT NULL DEFAULT 0,
58
58
  logs SMALLINT NOT NULL DEFAULT 0,
59
59
  argv JSONB NOT NULL,
60
- status VARCHAR(16) NOT NULL,
61
- state VARCHAR(16) NULL,
62
- digest VARCHAR(255) NULL,
63
- repo VARCHAR(255) NULL,
64
- ref VARCHAR(320) NULL,
65
- index_id VARCHAR(64) NULL,
60
+ status VARCHAR(16) COLLATE "C" NOT NULL,
61
+ state VARCHAR(16) COLLATE "C" NULL,
62
+ digest VARCHAR(255) COLLATE "C" NULL,
63
+ repo VARCHAR(255) COLLATE "C" NULL,
64
+ ref VARCHAR(320) COLLATE "C" NULL,
65
+ index_id VARCHAR(64) COLLATE "C" NULL,
66
66
  exit_code INT NULL,
67
- error TEXT NULL,
68
- error_code VARCHAR(32) NULL,
69
- manifest_sha VARCHAR(64) NULL,
70
- tag VARCHAR(255) NULL,
71
- idem_key VARCHAR(255) NULL,
72
- ingest_secret VARCHAR(64) NULL,
73
- runner_id VARCHAR(64) NULL,
67
+ error TEXT COLLATE "C" NULL,
68
+ error_code VARCHAR(32) COLLATE "C" NULL,
69
+ manifest_sha VARCHAR(64) COLLATE "C" NULL,
70
+ tag VARCHAR(255) COLLATE "C" NULL,
71
+ idem_key VARCHAR(255) COLLATE "C" NULL,
72
+ ingest_secret VARCHAR(64) COLLATE "C" NULL,
73
+ runner_id VARCHAR(64) COLLATE "C" NULL,
74
74
  lease_until TIMESTAMPTZ(3) NULL,
75
75
  cancel_requested SMALLINT NOT NULL DEFAULT 0,
76
- requested_by VARCHAR(128) NULL,
76
+ requested_by VARCHAR(128) COLLATE "C" NULL,
77
77
  created_at TIMESTAMPTZ(3) NOT NULL,
78
78
  updated_at TIMESTAMPTZ(3) NOT NULL,
79
79
  PRIMARY KEY (bake_id),
@@ -82,19 +82,19 @@ export const PG_IMAGE_BAKE_SCHEMA = [
82
82
  `CREATE INDEX IF NOT EXISTS idx_image_bake_status ON image_bake (status)`,
83
83
  `CREATE INDEX IF NOT EXISTS idx_image_bake_created ON image_bake (created_at)`,
84
84
  `CREATE TABLE IF NOT EXISTS image_bake_event (
85
- bake_id VARCHAR(64) NOT NULL,
85
+ bake_id VARCHAR(64) COLLATE "C" NOT NULL,
86
86
  seq INT NOT NULL,
87
87
  line_ord INT NULL,
88
- kind VARCHAR(24) NOT NULL,
89
- level VARCHAR(8) NULL,
88
+ kind VARCHAR(24) COLLATE "C" NOT NULL,
89
+ level VARCHAR(8) COLLATE "C" NULL,
90
90
  data JSONB NULL,
91
91
  ts TIMESTAMPTZ(3) NOT NULL,
92
92
  PRIMARY KEY (bake_id, seq),
93
93
  CONSTRAINT uq_image_bake_event_line UNIQUE (bake_id, line_ord)
94
94
  )`,
95
95
  `CREATE TABLE IF NOT EXISTS image_bake_lease (
96
- pool VARCHAR(32) NOT NULL DEFAULT 'standard',
97
- bake_id VARCHAR(64) NOT NULL,
96
+ pool VARCHAR(32) COLLATE "C" NOT NULL DEFAULT 'standard',
97
+ bake_id VARCHAR(64) COLLATE "C" NOT NULL,
98
98
  acquired_at TIMESTAMPTZ(3) NOT NULL,
99
99
  expires_at TIMESTAMPTZ(3) NOT NULL,
100
100
  PRIMARY KEY (pool)
@@ -104,7 +104,7 @@ export const PG_IMAGE_BAKE_SCHEMA = [
104
104
  // NOT EXISTS that two concurrent READ COMMITTED txns both pass — see createBakeIfIdle). Dialect-uniform with
105
105
  // the TiDB twin so a future divergence FAILS LOUD in the shared real-DB suite.
106
106
  `CREATE TABLE IF NOT EXISTS image_bake_admit (
107
- pool VARCHAR(32) NOT NULL,
107
+ pool VARCHAR(32) COLLATE "C" NOT NULL,
108
108
  PRIMARY KEY (pool)
109
109
  )`,
110
110
  ];
@@ -121,7 +121,7 @@ function isDupKey(dialect, err) {
121
121
  return Boolean(err) && code === (dialect === "tidb" ? "ER_DUP_ENTRY" : "23505");
122
122
  }
123
123
  /** Which UNIQUE key did the dup collide on? The ATTRIBUTION CHANNEL differs by engine: mysql2 names the key in
124
- * `sqlMessage` ("Duplicate entry '…' for key 'image_bake_event.uq_line'" vs "…'image_bake_event.PRIMARY'");
124
+ * `sqlMessage` ("Duplicate entry '…' for key 'image_bake_event.uq_image_bake_event_line'" vs "…'image_bake_event.PRIMARY'");
125
125
  * PG names the violated constraint in `err.constraint` (the PK is auto-named `image_bake_event_pkey`, the line
126
126
  * UNIQUE is explicitly named `uq_image_bake_event_line` so it is attributable at all).
127
127
  * M2: a line-unique dup is a BENIGN at-least-once re-send (already stored → no-op); a `PRIMARY` (bake_id,seq)
@@ -129,7 +129,7 @@ function isDupKey(dialect, err) {
129
129
  function dupKeyName(dialect, err) {
130
130
  if (dialect === "tidb") {
131
131
  const msg = String(err?.sqlMessage ?? err?.message ?? "");
132
- if (/uq_line/i.test(msg))
132
+ if (/uq_image_bake_event_line/i.test(msg))
133
133
  return "uq_line";
134
134
  if (/PRIMARY/i.test(msg))
135
135
  return "primary";
@@ -52,33 +52,33 @@ import { mysqlDriver, pgDriver } from "./sql-driver.js";
52
52
  * 读侧 `!!` 保持,出口 JS 类型不变), inline UNIQUE KEY/KEY → separate CREATE UNIQUE INDEX / CREATE INDEX). */
53
53
  export const PG_IMAGE_INDEX_SCHEMA = [
54
54
  `CREATE TABLE IF NOT EXISTS sandbox_image_index (
55
- id VARCHAR(64) NOT NULL,
56
- profile VARCHAR(64) NOT NULL,
55
+ id VARCHAR(64) COLLATE "C" NOT NULL,
56
+ profile VARCHAR(64) COLLATE "C" NOT NULL,
57
57
  bands JSONB,
58
- repo VARCHAR(255) NOT NULL,
59
- tag VARCHAR(128) NOT NULL,
60
- digest VARCHAR(80) NOT NULL,
58
+ repo VARCHAR(255) COLLATE "C" NOT NULL,
59
+ tag VARCHAR(128) COLLATE "C" NOT NULL,
60
+ digest VARCHAR(80) COLLATE "C" NOT NULL,
61
61
  toolchain_versions JSONB,
62
62
  capabilities JSONB,
63
63
  pod_contract JSONB,
64
64
  size_bytes BIGINT,
65
- status VARCHAR(16) NOT NULL DEFAULT 'building',
66
- visibility VARCHAR(16) NOT NULL DEFAULT 'public',
67
- tenant_id VARCHAR(64),
68
- manifest_sha VARCHAR(64),
69
- recipe_git_sha VARCHAR(64),
70
- generator_version VARCHAR(32),
65
+ status VARCHAR(16) COLLATE "C" NOT NULL DEFAULT 'building',
66
+ visibility VARCHAR(16) COLLATE "C" NOT NULL DEFAULT 'public',
67
+ tenant_id VARCHAR(64) COLLATE "C",
68
+ manifest_sha VARCHAR(64) COLLATE "C",
69
+ recipe_git_sha VARCHAR(64) COLLATE "C",
70
+ generator_version VARCHAR(32) COLLATE "C",
71
71
  build_date TIMESTAMPTZ(3),
72
- supersedes VARCHAR(64),
72
+ supersedes VARCHAR(64) COLLATE "C",
73
73
  signed SMALLINT NOT NULL DEFAULT 0,
74
74
  created_at TIMESTAMPTZ(3) NOT NULL,
75
75
  updated_at TIMESTAMPTZ(3) NOT NULL,
76
76
  PRIMARY KEY (id)
77
77
  )`,
78
78
  `CREATE UNIQUE INDEX IF NOT EXISTS uq_repo_digest ON sandbox_image_index (repo, digest)`,
79
- `CREATE INDEX IF NOT EXISTS idx_profile_status ON sandbox_image_index (profile, status, visibility)`,
80
- `CREATE INDEX IF NOT EXISTS idx_digest ON sandbox_image_index (digest)`,
81
- `CREATE INDEX IF NOT EXISTS idx_tenant ON sandbox_image_index (tenant_id, status)`,
79
+ `CREATE INDEX IF NOT EXISTS idx_sandbox_image_index_profile_status ON sandbox_image_index (profile, status, visibility)`,
80
+ `CREATE INDEX IF NOT EXISTS idx_sandbox_image_index_digest ON sandbox_image_index (digest)`,
81
+ `CREATE INDEX IF NOT EXISTS idx_sandbox_image_index_tenant ON sandbox_image_index (tenant_id, status)`,
82
82
  ];
83
83
  /** Idempotent self-contained schema apply (for the integration test; central aggregation is done separately). */
84
84
  export async function ensureSchema(pool) {
@@ -111,7 +111,7 @@ export async function ensurePgMailboxSchema(q) {
111
111
  await q(`CREATE TABLE IF NOT EXISTS ${MAILBOX_TABLE} (
112
112
  scope_key VARCHAR(190) COLLATE "C" NOT NULL,
113
113
  handle VARCHAR(190) COLLATE "C" NOT NULL,
114
- scope VARCHAR(190) NOT NULL,
114
+ scope VARCHAR(190) COLLATE "C" NOT NULL,
115
115
  next_seq BIGINT NOT NULL,
116
116
  lease_owner VARCHAR(190) COLLATE "C",
117
117
  lease_expires_at_ms BIGINT,
@@ -123,9 +123,9 @@ export async function ensurePgMailboxSchema(q) {
123
123
  handle VARCHAR(190) COLLATE "C" NOT NULL,
124
124
  seq BIGINT NOT NULL,
125
125
  from_id VARCHAR(255) COLLATE "C",
126
- content TEXT NOT NULL,
126
+ content TEXT COLLATE "C" NOT NULL,
127
127
  sent_at_ms BIGINT NOT NULL,
128
- hop_chain TEXT, -- core 5.17.0 design/176(TiDB twin 同注:NULL = 缺席且有语义)
128
+ hop_chain TEXT COLLATE "C", -- core 5.17.0 design/176(TiDB twin 同注:NULL = 缺席且有语义)
129
129
  PRIMARY KEY (scope_key, handle, seq)
130
130
  )`);
131
131
  await q(`CREATE INDEX IF NOT EXISTS idx_mbm_sent ON ${MAILBOX_MSG_TABLE} (scope_key, handle, sent_at_ms)`);
@@ -42,18 +42,18 @@ export async function ensurePgMemoryEngineSchema(query, opts = {}) {
42
42
  await query("CREATE EXTENSION IF NOT EXISTS vector");
43
43
  const embeddingCol = vec?.pgvector ? `vector(${vec.dimensions})` : "jsonb";
44
44
  await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_ENGINE_TABLES.entries} (
45
- id text PRIMARY KEY,
46
- scope text NOT NULL,
47
- slug text NOT NULL,
45
+ id text COLLATE "C" PRIMARY KEY,
46
+ scope text COLLATE "C" NOT NULL,
47
+ slug text COLLATE "C" NOT NULL,
48
48
  frontmatter jsonb NOT NULL,
49
- body text NOT NULL,
50
- rev text NOT NULL,
49
+ body text COLLATE "C" NOT NULL,
50
+ rev text COLLATE "C" NOT NULL,
51
51
  mtime_ms bigint NOT NULL,
52
52
  size_bytes integer NOT NULL,
53
53
  embedding ${embeddingCol}
54
54
  )`);
55
55
  try {
56
- await query(`CREATE INDEX IF NOT EXISTS ${PG_MEMORY_ENGINE_TABLES.entries}_scope_idx ON ${PG_MEMORY_ENGINE_TABLES.entries} (scope)`);
56
+ await query(`CREATE INDEX IF NOT EXISTS idx_${PG_MEMORY_ENGINE_TABLES.entries}_scope ON ${PG_MEMORY_ENGINE_TABLES.entries} (scope)`);
57
57
  }
58
58
  catch {
59
59
  /* pg-mem: partial CREATE INDEX support — the index is a perf add-on, never correctness */
@@ -63,10 +63,10 @@ export async function ensurePgMemoryEngineSchema(query, opts = {}) {
63
63
  // insert, leaving two entries whose file projections overwrite each other at materialize. The DB
64
64
  // constraint is the only true arbiter; the add path catches the violation and retries the next
65
65
  // suffix. NOT wrapped in try/catch — if this constraint cannot be created, the backend is unsafe.
66
- await query(`CREATE UNIQUE INDEX IF NOT EXISTS ${PG_MEMORY_ENGINE_TABLES.entries}_scope_slug_uq ON ${PG_MEMORY_ENGINE_TABLES.entries} (scope, slug)`);
66
+ await query(`CREATE UNIQUE INDEX IF NOT EXISTS uq_${PG_MEMORY_ENGINE_TABLES.entries}_scope_slug ON ${PG_MEMORY_ENGINE_TABLES.entries} (scope, slug)`);
67
67
  await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_ENGINE_TABLES.cursors} (
68
- scope text PRIMARY KEY,
69
- cursor text NOT NULL
68
+ scope text COLLATE "C" PRIMARY KEY,
69
+ cursor text COLLATE "C" NOT NULL
70
70
  )`);
71
71
  }
72
72
  /** pgvector input literal — the same `[…]` string JSON.parse reads back from jsonb (one format, two columns). */
@@ -12,7 +12,7 @@
12
12
  // - add 路径 Pg 用 `INSERT … ON CONFLICT (id) DO UPDATE`(冲突靶=仅 PK)。MySQL 的 ON DUPLICATE KEY
13
13
  // UPDATE 会被 **任意** 唯一键触发——(scope,slug) 撞键时它会把竞争对手的行改写成本 entry 的 id(数据毁),
14
14
  // 不能用;拆成「不存在→INSERT / 已存在→UPDATE by id」,两条腿的 ER_DUP_ENTRY 都按键名分诊(PRIMARY=
15
- // 并发同 id add → 转 UPDATE 腿;uk_scope_slug=并发 slug 竞争 → 下一个 -n 后缀重试);
15
+ // 并发同 id add → 转 UPDATE 腿;uq_agent_memory_engine_entries_scope_slug=并发 slug 竞争 → 下一个 -n 后缀重试);
16
16
  // - unique violation 判定:ER_DUP_ENTRY(errno 1062),不是 Pg 的 23505;
17
17
  // - pg-mem 伪影 workaround(索引列 ANY 零行/重复占位符重复行)不抄——MySQL 本就走 IN-list,dedupe 保留
18
18
  // 是因为它同时是诚实语义(重复 key 问的是同一个 entry,契约锁死 once-per-entry);
@@ -45,9 +45,9 @@ export async function ensureTiDBMemoryEngineSchema(pool) {
45
45
  size_bytes INT NOT NULL,
46
46
  embedding JSON NULL,
47
47
  PRIMARY KEY (id),
48
- KEY idx_scope (scope),
49
- UNIQUE KEY uk_scope_slug (scope, slug)
50
- )`);
48
+ KEY idx_agent_memory_engine_entries_scope (scope),
49
+ UNIQUE KEY uq_agent_memory_engine_entries_scope_slug (scope, slug)
50
+ ) COLLATE utf8mb4_bin`);
51
51
  // The (scope, slug) UNIQUE key above is CORRECTNESS, not perf (Pg 版同注):two
52
52
  // instances racing an add of one scope+slug (different ids) can both probe "free" and both insert;
53
53
  // the DB constraint is the only true arbiter and the add path retries the next suffix on violation.
@@ -55,14 +55,14 @@ export async function ensureTiDBMemoryEngineSchema(pool) {
55
55
  scope VARCHAR(190) NOT NULL,
56
56
  \`cursor\` TEXT NOT NULL,
57
57
  PRIMARY KEY (scope)
58
- )`);
58
+ ) COLLATE utf8mb4_bin`);
59
59
  }
60
60
  function isDupEntry(err) {
61
61
  if (typeof err !== "object" || err === null)
62
62
  return false;
63
63
  return err.code === "ER_DUP_ENTRY" || err.errno === 1062;
64
64
  }
65
- /** ER_DUP_ENTRY messages name the violated key (`… for key 'tbl.PRIMARY'` / `… for key 'tbl.uk_scope_slug'`)
65
+ /** ER_DUP_ENTRY messages name the violated key (`… for key 'tbl.PRIMARY'` / `… for key 'tbl.uq_agent_memory_engine_entries_scope_slug'`)
66
66
  * — the add path uses this to tell "id already exists (→ overwrite leg)" from "slug race (→ -n retry)". */
67
67
  function isPrimaryKeyDup(err) {
68
68
  return isDupEntry(err) && /for key '[^']*PRIMARY'/i.test(String(err.message ?? ""));
@@ -263,7 +263,7 @@ export class TiDBMemoryEngineBackend {
263
263
  }
264
264
  }
265
265
  // Slug collision with a DIFFERENT entry → deterministic `-n` suffix (file-backend parity); the
266
- // probe is only an optimization — the write INSIDE the loop is arbitrated by uk_scope_slug, and
266
+ // probe is only an optimization — the write INSIDE the loop is arbitrated by uq_agent_memory_engine_entries_scope_slug, and
267
267
  // a concurrent loser retries the next suffix (Pg 版同注).
268
268
  let slug = entry.slug;
269
269
  for (let n = 2;; n++) {
@@ -8,23 +8,23 @@ export const PG_MEMORY_SYNC_TABLES = {
8
8
  /** Idempotent DDL for the sync plane (PG dialect). Call once at startup. */
9
9
  export async function ensurePgMemorySyncSchema(query) {
10
10
  await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_SYNC_TABLES.syncCursors} (
11
- scope varchar(190) NOT NULL,
12
- peer varchar(190) NOT NULL,
11
+ scope varchar(190) COLLATE "C" NOT NULL,
12
+ peer varchar(190) COLLATE "C" NOT NULL,
13
13
  base_revs jsonb NOT NULL,
14
14
  updated_at_ms bigint NOT NULL,
15
15
  PRIMARY KEY (scope, peer)
16
16
  )`);
17
17
  await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_SYNC_TABLES.pushQueue} (
18
- id char(36) PRIMARY KEY,
19
- scope varchar(190) NOT NULL,
20
- entry_id char(36) NOT NULL,
18
+ id char(36) COLLATE "C" PRIMARY KEY,
19
+ scope varchar(190) COLLATE "C" NOT NULL,
20
+ entry_id char(36) COLLATE "C" NOT NULL,
21
21
  payload jsonb NOT NULL,
22
22
  attempts integer NOT NULL DEFAULT 0,
23
23
  next_attempt_at bigint NOT NULL,
24
- last_error text NULL
24
+ last_error text COLLATE "C" NULL
25
25
  )`);
26
26
  // Scan face (§1.2): the reaper/consumer sweeps by due time. Perf add-on, never correctness.
27
- await query(`CREATE INDEX IF NOT EXISTS ${PG_MEMORY_SYNC_TABLES.pushQueue}_next_attempt_idx ON ${PG_MEMORY_SYNC_TABLES.pushQueue} (next_attempt_at)`);
27
+ await query(`CREATE INDEX IF NOT EXISTS idx_${PG_MEMORY_SYNC_TABLES.pushQueue}_next_attempt ON ${PG_MEMORY_SYNC_TABLES.pushQueue} (next_attempt_at)`);
28
28
  }
29
29
  /** jsonb defense: node-pg hands back parsed objects, but a text-typed round-trip (or a foreign
30
30
  * writer) may yield a string — same posture as parseJson in run-store-sql.ts. */
@@ -135,17 +135,17 @@ export async function ensurePgMemoryHistorySchema(query) {
135
135
  // (caller-injected epoch ms) instead of the draft's TIMESTAMP — the family's clock discipline
136
136
  // (sync_cursors.updated_at_ms precedent; DB clocks never judge anything).
137
137
  await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_SYNC_TABLES.history} (
138
- id char(36) NOT NULL,
139
- rev varchar(128) NOT NULL,
140
- prev_rev varchar(128) NULL,
141
- op varchar(16) NOT NULL,
142
- scope varchar(190) NOT NULL,
138
+ id char(36) COLLATE "C" NOT NULL,
139
+ rev varchar(128) COLLATE "C" NOT NULL,
140
+ prev_rev varchar(128) COLLATE "C" NULL,
141
+ op varchar(16) COLLATE "C" NOT NULL,
142
+ scope varchar(190) COLLATE "C" NOT NULL,
143
143
  snapshot jsonb NULL,
144
144
  written_at_ms bigint NOT NULL,
145
145
  PRIMARY KEY (id, rev)
146
146
  )`);
147
147
  // countByScope / future per-scope export-with-history scan face. Perf add-on, never correctness.
148
- await query(`CREATE INDEX IF NOT EXISTS ${PG_MEMORY_SYNC_TABLES.history}_scope_idx ON ${PG_MEMORY_SYNC_TABLES.history} (scope)`);
148
+ await query(`CREATE INDEX IF NOT EXISTS idx_${PG_MEMORY_SYNC_TABLES.history}_scope ON ${PG_MEMORY_SYNC_TABLES.history} (scope)`);
149
149
  }
150
150
  export class PgMemoryHistoryStore {
151
151
  query;
@@ -15,7 +15,7 @@ export async function ensureTiDBMemorySyncSchema(pool) {
15
15
  base_revs JSON NOT NULL,
16
16
  updated_at_ms BIGINT NOT NULL,
17
17
  PRIMARY KEY (scope, peer)
18
- )`);
18
+ ) COLLATE utf8mb4_bin`);
19
19
  await pool.query(`CREATE TABLE IF NOT EXISTS ${TIDB_MEMORY_SYNC_TABLES.pushQueue} (
20
20
  id CHAR(36) NOT NULL,
21
21
  scope VARCHAR(190) NOT NULL,
@@ -25,8 +25,8 @@ export async function ensureTiDBMemorySyncSchema(pool) {
25
25
  next_attempt_at BIGINT NOT NULL,
26
26
  last_error TEXT NULL,
27
27
  PRIMARY KEY (id),
28
- KEY idx_next_attempt (next_attempt_at)
29
- )`);
28
+ KEY idx_agent_memory_engine_push_queue_next_attempt (next_attempt_at)
29
+ ) COLLATE utf8mb4_bin`);
30
30
  }
31
31
  /** JSON-column defense: mysql2 normally hands back parsed objects for JSON columns, but string
32
32
  * shapes leak through (connection flags/proxies) — same posture as memory-engine-tidb.ts. */
@@ -138,8 +138,8 @@ export async function ensureTiDBMemoryHistorySchema(pool) {
138
138
  snapshot JSON NULL,
139
139
  written_at_ms BIGINT NOT NULL,
140
140
  PRIMARY KEY (id, rev),
141
- KEY idx_history_scope (scope)
142
- )`);
141
+ KEY idx_agent_memory_engine_history_scope (scope)
142
+ ) COLLATE utf8mb4_bin`);
143
143
  }
144
144
  export class TiDBMemoryHistoryStore {
145
145
  pool;