@sema-agent/server 7.82.1 → 7.84.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 (84) hide show
  1. package/dist/bench/s1/arms.d.ts +4 -4
  2. package/dist/bench/s1/arms.js +1 -2
  3. package/dist/bench/s1/live-deps.js +0 -1
  4. package/dist/bench/s1/runner-ctx.d.ts +3 -3
  5. package/dist/bench/s1/tasks.d.ts +8 -4
  6. package/dist/boot/resolve-spec.js +9 -5
  7. package/dist/capabilities/memory-notice.d.ts +6 -4
  8. package/dist/capabilities/memory-notice.js +1 -1
  9. package/dist/config-catalog.js +0 -1
  10. package/dist/config.js +1 -0
  11. package/dist/http/admission.js +7 -3
  12. package/dist/http/wire-types.d.ts +22 -0
  13. package/dist/leader/endpoint.js +2 -0
  14. package/dist/leader/leader.d.ts +33 -6
  15. package/dist/leader/leader.js +54 -45
  16. package/dist/leader/merge.d.ts +38 -29
  17. package/dist/leader/merge.js +45 -38
  18. package/dist/leader/wire.d.ts +2 -17
  19. package/dist/leader/wire.js +5 -58
  20. package/dist/memory-scope.d.ts +33 -5
  21. package/dist/memory-scope.js +10 -4
  22. package/dist/observability/fail-open.d.ts +5 -5
  23. package/dist/observability/fail-open.js +5 -2
  24. package/dist/observability/run-terminal-log.d.ts +2 -7
  25. package/dist/observability/run-terminal-log.js +119 -21
  26. package/dist/plugins/adoption-log-sql.d.ts +3 -0
  27. package/dist/plugins/adoption-log-sql.js +5 -1
  28. package/dist/plugins/approval-ask-store-sql.d.ts +3 -0
  29. package/dist/plugins/approval-ask-store-sql.js +9 -5
  30. package/dist/plugins/background-agent-store-sql.d.ts +3 -0
  31. package/dist/plugins/background-agent-store-sql.js +7 -2
  32. package/dist/plugins/device-store-sql.d.ts +3 -0
  33. package/dist/plugins/device-store-sql.js +9 -4
  34. package/dist/plugins/ensure-index.d.ts +42 -0
  35. package/dist/plugins/ensure-index.js +81 -0
  36. package/dist/plugins/ensure-schema-lock-policy.d.ts +29 -0
  37. package/dist/plugins/ensure-schema-lock-policy.js +31 -0
  38. package/dist/plugins/image-bake-store-sql.d.ts +3 -0
  39. package/dist/plugins/image-bake-store-sql.js +6 -2
  40. package/dist/plugins/image-index-sql.d.ts +3 -0
  41. package/dist/plugins/image-index-sql.js +8 -4
  42. package/dist/plugins/leader-run-store-sql.d.ts +3 -0
  43. package/dist/plugins/leader-run-store-sql.js +6 -2
  44. package/dist/plugins/mailbox-store-sql.d.ts +3 -0
  45. package/dist/plugins/mailbox-store-sql.js +6 -1
  46. package/dist/plugins/memory-engine-pg.d.ts +4 -0
  47. package/dist/plugins/memory-engine-pg.js +6 -6
  48. package/dist/plugins/memory-engine-tidb.js +5 -0
  49. package/dist/plugins/memory-sync-store-pg.d.ts +5 -0
  50. package/dist/plugins/memory-sync-store-pg.js +9 -2
  51. package/dist/plugins/memory-sync-store-tidb.js +6 -0
  52. package/dist/plugins/one-time-migrations.d.ts +1 -1
  53. package/dist/plugins/one-time-migrations.js +7 -14
  54. package/dist/plugins/outcome-ledger-sql.d.ts +3 -0
  55. package/dist/plugins/outcome-ledger-sql.js +7 -3
  56. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  57. package/dist/plugins/permission-rule-store-sql.js +9 -5
  58. package/dist/plugins/pg-cost-quota.d.ts +3 -0
  59. package/dist/plugins/pg-cost-quota.js +5 -1
  60. package/dist/plugins/pg-pool.d.ts +23 -2
  61. package/dist/plugins/pg-pool.js +22 -37
  62. package/dist/plugins/pg-rate-limiter.d.ts +3 -0
  63. package/dist/plugins/pg-rate-limiter.js +5 -1
  64. package/dist/plugins/pg-session-storage.d.ts +3 -0
  65. package/dist/plugins/pg-session-storage.js +8 -4
  66. package/dist/plugins/retention-store-sql.d.ts +3 -0
  67. package/dist/plugins/retention-store-sql.js +7 -3
  68. package/dist/plugins/roster-store-sql.d.ts +3 -0
  69. package/dist/plugins/roster-store-sql.js +6 -1
  70. package/dist/plugins/schema-indexes.d.ts +21 -0
  71. package/dist/plugins/schema-indexes.js +25 -0
  72. package/dist/plugins/task-attachment-store.d.ts +3 -0
  73. package/dist/plugins/task-attachment-store.js +7 -2
  74. package/dist/plugins/tidb-pool.d.ts +15 -3
  75. package/dist/plugins/tidb-pool.js +38 -15
  76. package/dist/plugins/tool-result-store-sql.d.ts +3 -0
  77. package/dist/plugins/tool-result-store-sql.js +5 -1
  78. package/dist/request-key-closure.d.ts +7 -0
  79. package/dist/request-key-closure.js +1 -1
  80. package/dist/request-value-closure.d.ts +31 -0
  81. package/dist/request-value-closure.js +18 -0
  82. package/dist/trace/project.d.ts +2 -1
  83. package/dist/trace/redact.d.ts +6 -0
  84. package/package.json +1 -1
@@ -104,7 +104,7 @@ const RESULT_KEY_TREATMENT = {
104
104
  effectiveWriteFace: "verbatim",
105
105
  effectiveReadDenyPatterns: "verbatim",
106
106
  effectiveMemoryScopes: "verbatim",
107
- stats: "verbatim",
107
+ stats: "redacted",
108
108
  };
109
109
  const LEGACY_RESULT_LEAVES = ["errorMessage", "blockedReason", "checkpointGate"];
110
110
  export const REDACTABLE_RESULT_LEAVES = [
@@ -207,6 +207,13 @@ export function redactTerminalResult(result) {
207
207
  : r.rewindNotes;
208
208
  changed = true;
209
209
  }
210
+ if (r.stats !== undefined) {
211
+ const next = redactStats(r.stats);
212
+ if (next !== r.stats) {
213
+ out.stats = next;
214
+ changed = true;
215
+ }
216
+ }
210
217
  return changed ? out : result;
211
218
  }
212
219
  export function redactLedgerEventData(type, data) {
@@ -317,6 +324,108 @@ export function redactRiskDescriptor(rd) {
317
324
  }
318
325
  return changed ? out : rd;
319
326
  }
327
+ const STATS_KEY_TREATMENT = {
328
+ turns: "verbatim",
329
+ tokens: "verbatim",
330
+ toolCalls: "verbatim",
331
+ usageMissing: "verbatim",
332
+ promptTokens: "verbatim",
333
+ cachedTokens: "verbatim",
334
+ cacheWriteTokens: "verbatim",
335
+ cacheHitRate: "verbatim",
336
+ totalInputTokens: "verbatim",
337
+ outputTokens: "verbatim",
338
+ cacheWriteTokensLong: "verbatim",
339
+ costMicroUsd: "verbatim",
340
+ nested: "verbatim",
341
+ memory: "verbatim",
342
+ suggestions: "verbatim",
343
+ costBreakdown: "verbatim",
344
+ mechanisms: "mechanisms",
345
+ humanReview: "human-review",
346
+ };
347
+ const STATS_MECHANISMS_TREATMENT = {
348
+ finalVerifyInjected: "verbatim",
349
+ finalVerifyInjections: "verbatim",
350
+ attachmentsInjected: "verbatim",
351
+ approachNoticesSent: "verbatim",
352
+ repetitionCuts: "verbatim",
353
+ repetitionSpared: "verbatim",
354
+ repetitionEvents: "repetition-rows",
355
+ reminderDisclosures: "verbatim",
356
+ };
357
+ const STATS_HUMAN_REVIEW_TREATMENT = {
358
+ count: "verbatim",
359
+ totalWaitMs: "verbatim",
360
+ gates: "gate-rows",
361
+ };
362
+ const STATS_GATE_ROW_TREATMENT = {
363
+ kind: "verbatim",
364
+ waitMs: "verbatim",
365
+ decision: "verbatim",
366
+ toolName: "verbatim",
367
+ toolArg: "model-text",
368
+ };
369
+ const STATS_REPETITION_EVENT_TREATMENT = {
370
+ turn: "verbatim",
371
+ action: "verbatim",
372
+ rule: "verbatim",
373
+ period: "verbatim",
374
+ reps: "verbatim",
375
+ segment: "model-text",
376
+ };
377
+ function redactStatsNode(node, table) {
378
+ if (node === null || typeof node !== "object" || Array.isArray(node))
379
+ return node;
380
+ const n = node;
381
+ let changed = false;
382
+ const out = { ...n };
383
+ const into = (v, t) => redactStatsNode(v, t);
384
+ const rows = (v, t) => {
385
+ if (!Array.isArray(v))
386
+ return v;
387
+ const next = v.map((row) => redactStatsNode(row, t));
388
+ return next.every((x, i) => x === v[i]) ? v : next;
389
+ };
390
+ for (const [key, treatment] of Object.entries(table)) {
391
+ const v = n[key];
392
+ if (v === undefined)
393
+ continue;
394
+ let next = v;
395
+ switch (treatment) {
396
+ case "verbatim":
397
+ break;
398
+ case "model-text":
399
+ if (typeof v === "string")
400
+ next = redactModelText(v);
401
+ break;
402
+ case "mechanisms":
403
+ next = into(v, STATS_MECHANISMS_TREATMENT);
404
+ break;
405
+ case "human-review":
406
+ next = into(v, STATS_HUMAN_REVIEW_TREATMENT);
407
+ break;
408
+ case "gate-rows":
409
+ next = rows(v, STATS_GATE_ROW_TREATMENT);
410
+ break;
411
+ case "repetition-rows":
412
+ next = rows(v, STATS_REPETITION_EVENT_TREATMENT);
413
+ break;
414
+ default: {
415
+ const unhandled = treatment;
416
+ throw new Error(`unhandled stats treatment: ${String(unhandled)}`);
417
+ }
418
+ }
419
+ if (next !== v) {
420
+ out[key] = next;
421
+ changed = true;
422
+ }
423
+ }
424
+ return changed ? out : node;
425
+ }
426
+ function redactStats(stats) {
427
+ return redactStatsNode(stats, STATS_KEY_TREATMENT);
428
+ }
320
429
  const CHECKPOINT_GATE_TREATMENT = {
321
430
  kind: "verbatim",
322
431
  reason: "text",
@@ -383,7 +492,6 @@ export function redactDiagnosticText(text) {
383
492
  export function redactHead(text, keep) {
384
493
  return redactDiagnosticText(text).slice(0, keep);
385
494
  }
386
- export const OVERSIZED_PATCH_PLACEHOLDER = "«redacted:oversized-patch-not-inlined»";
387
495
  function diffScanRefusal(patch, direct) {
388
496
  if (/^diff --(?:cc|combined) |^@{3,} /m.test(patch))
389
497
  return "combined-diff";
@@ -415,25 +523,6 @@ function diffScanRefusal(patch, direct) {
415
523
  }
416
524
  return undefined;
417
525
  }
418
- export function redactPatchHead(patch, keep) {
419
- if (patch.length > DIAG_INPUT_CAP) {
420
- recordFailOpen("server.run-terminal.patch-head-cut", `not-inlined len=${patch.length} keep=${keep}`);
421
- return `${OVERSIZED_PATCH_PLACEHOLDER} [len=${patch.length}]`;
422
- }
423
- try {
424
- const direct = redactSecrets(patch);
425
- const refusal = diffScanRefusal(patch, direct);
426
- if (refusal !== undefined) {
427
- recordFailOpen("server.run-terminal.patch-head-cut", `${refusal} len=${patch.length} keep=${keep}`);
428
- return `${OVERSIZED_PATCH_PLACEHOLDER} [len=${patch.length}]`;
429
- }
430
- return direct.slice(0, keep);
431
- }
432
- catch (err) {
433
- recordFailOpen("server.run-terminal.redact-threw", `patch-head len=${patch.length} err=${err instanceof Error ? err.name : String(err)}`);
434
- return OVERSIZED_REDACTION_PLACEHOLDER;
435
- }
436
- }
437
526
  export function redactTail(text, keep) {
438
527
  return redactDiagnosticText(text).slice(-keep);
439
528
  }
@@ -506,6 +595,15 @@ export function redactLeaderResultWithFlag(result) {
506
595
  m.measure = { ...measure, reason: str(measure.reason) };
507
596
  out.merge = m;
508
597
  }
598
+ const conflict = obj(r.conflict);
599
+ if (conflict) {
600
+ const c = { ...conflict };
601
+ if (typeof conflict.rejHead === "string")
602
+ c.rejHead = art(conflict.rejHead);
603
+ if (Array.isArray(conflict.files))
604
+ c.files = conflict.files.map((f) => (typeof f === "string" ? str(f) : f));
605
+ out.conflict = c;
606
+ }
509
607
  const cp = obj(r.candidatePatch);
510
608
  if (cp)
511
609
  out.candidatePatch = { ...cp, ...(typeof cp.reason === "string" ? { reason: str(cp.reason) } : {}), ...(typeof cp.patch === "string" ? { patch: art(cp.patch) } : {}) };
@@ -24,6 +24,7 @@ import type { Pool as MySqlPool } from "mysql2/promise";
24
24
  import type { Pool as PgPool } from "pg";
25
25
  import { type SqlDriver, type SqlExec, type SqlTxConn } from "./sql-driver.js";
26
26
  import type { AdoptionRejectCode } from "../adoption/wire.js";
27
+ import { type IndexSpec } from "./ensure-index.js";
27
28
  export declare const ADOPTION_LOG_TABLE = "adoption_log";
28
29
  /** 弧的阶段(183 §3.2 根级状态机的 form b 投影)。**单调**,只增不减。 */
29
30
  export declare const ADOPTION_PHASE: {
@@ -106,6 +107,8 @@ export interface AdoptionLogStore {
106
107
  /** MySQL 协议方言的建表语句(展开进 tidb-pool 的中央 `SCHEMA_STATEMENTS`,与 approval-ask 同姿势:
107
108
  * 跟着中央 ensureSchema 在 named-lock 的那条 conn 上建,不绕开 DDL 串行化)。 */
108
109
  export declare const TIDB_ADOPTION_LOG_STATEMENTS: readonly string[];
110
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgAdoptionLogSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
111
+ export declare const ADOPTION_LOG_INDEXES: readonly IndexSpec[];
109
112
  /** PostgreSQL 孪生(由 `ensurePgSchema` 在 advisory lock 的那条 client 上调用)。 */
110
113
  export declare function ensurePgAdoptionLogSchema(q: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
111
114
  /**
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
3
+ import { ensureIndexes } from "./ensure-index.js";
3
4
  export const ADOPTION_LOG_TABLE = "adoption_log";
4
5
  export const ADOPTION_PHASE = {
5
6
  INTENT: 2,
@@ -30,6 +31,9 @@ export const TIDB_ADOPTION_LOG_STATEMENTS = [
30
31
  KEY idx_adoption_log_state (state)
31
32
  ) COLLATE utf8mb4_bin`,
32
33
  ];
34
+ export const ADOPTION_LOG_INDEXES = [
35
+ { table: `${ADOPTION_LOG_TABLE}`, name: "idx_adoption_log_state", columns: ["state"] },
36
+ ];
33
37
  export async function ensurePgAdoptionLogSchema(q) {
34
38
  await q(`CREATE TABLE IF NOT EXISTS ${ADOPTION_LOG_TABLE} (
35
39
  adoption_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -48,7 +52,7 @@ export async function ensurePgAdoptionLogSchema(q) {
48
52
  PRIMARY KEY (adoption_id),
49
53
  CONSTRAINT uq_adoption_log_from UNIQUE (from_principal)
50
54
  )`);
51
- await q(`CREATE INDEX IF NOT EXISTS idx_adoption_log_state ON ${ADOPTION_LOG_TABLE} (state)`);
55
+ await ensureIndexes(q, "pg", ADOPTION_LOG_INDEXES);
52
56
  }
53
57
  const COLS = "adoption_id, from_principal, to_principal, phase, state, legs, configs, report, reject_code, reject_detail, created_at_ms, updated_at_ms";
54
58
  function toRow(r) {
@@ -41,6 +41,7 @@ import type { Pool as PgPool } from "pg";
41
41
  import type { PgQueryFn } from "./pg-query.js";
42
42
  import { type SqlDriver, type SqlDialect } from "./sql-driver.js";
43
43
  import { type AskState, type BatchState } from "../approval-ask-machine.js";
44
+ import { type IndexSpec } from "./ensure-index.js";
44
45
  export type AskDecision = "approve" | "deny";
45
46
  export interface AskRow {
46
47
  askId: string;
@@ -449,6 +450,8 @@ export declare const TIDB_APPROVAL_ASK_STATEMENTS: readonly string[];
449
450
  /** {@link TIDB_APPROVAL_ASK_STATEMENTS} 的遍历壳——生产路径走 `tidb-pool.ts` 的中央 `ensureSchema`
450
451
  * (那里展开了同一份数组),本函数留给只需要这两张表的集成测试。 */
451
452
  export declare function ensureTiDBApprovalAskSchema(pool: MySqlPool): Promise<void>;
453
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgApprovalAskSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
454
+ export declare const APPROVAL_ASK_INDEXES: readonly IndexSpec[];
452
455
  export declare function ensurePgApprovalAskSchema(q: PgQueryFn): Promise<void>;
453
456
  /**
454
457
  * [ref] 升级前置断言 —— **拒启**,不是 warn(形照 [ref] 的 `assertPermissionRuleApprovalSchema` /
@@ -1,6 +1,7 @@
1
1
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
2
2
  import { canAskTransition } from "../approval-ask-machine.js";
3
3
  import { isDupKeyError, isMissingColumnError } from "./sql-errors.js";
4
+ import { ensureIndexes } from "./ensure-index.js";
4
5
  export function assertIntegralEpochMs(field, value) {
5
6
  if (!Number.isSafeInteger(value)) {
6
7
  throw new TypeError(`approval-ask store: ${field} must be an integer epoch-ms (BIGINT column), got ${String(value)}`);
@@ -126,6 +127,13 @@ export async function ensureTiDBApprovalAskSchema(pool) {
126
127
  for (const stmt of TIDB_APPROVAL_ASK_STATEMENTS)
127
128
  await pool.query(stmt);
128
129
  }
130
+ export const APPROVAL_ASK_INDEXES = [
131
+ { table: `${APPROVAL_BATCH_TABLE}`, name: "idx_batch_task", columns: ["task_id"] },
132
+ { table: `${APPROVAL_ASK_TABLE}`, name: "idx_ask_task_state", columns: ["task_id", "state"] },
133
+ { table: `${APPROVAL_ASK_TABLE}`, name: "idx_ask_session_state", columns: ["session_id", "state"] },
134
+ { table: `${APPROVAL_ASK_TABLE}`, name: "idx_ask_batch", columns: ["batch_id"] },
135
+ { table: `${APPROVAL_ASK_TABLE}`, name: "idx_ask_state", columns: ["state"] },
136
+ ];
129
137
  export async function ensurePgApprovalAskSchema(q) {
130
138
  await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCH_TABLE} (
131
139
  batch_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -137,7 +145,6 @@ export async function ensurePgApprovalAskSchema(q) {
137
145
  updated_at_ms BIGINT NOT NULL,
138
146
  PRIMARY KEY (batch_id)
139
147
  )`);
140
- await q(`CREATE INDEX IF NOT EXISTS idx_batch_task ON ${APPROVAL_BATCH_TABLE} (task_id)`);
141
148
  await q(`CREATE TABLE IF NOT EXISTS ${APPROVAL_ASK_TABLE} (
142
149
  ask_id VARCHAR(64) COLLATE "C" NOT NULL,
143
150
  task_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -179,10 +186,7 @@ export async function ensurePgApprovalAskSchema(q) {
179
186
  -- (task_id, idempotency_key) 复合唯一——MySQL twin 逐字同形(改动理由见那侧注)。
180
187
  CONSTRAINT uq_approval_ask_idem UNIQUE (task_id, idempotency_key)
181
188
  )`);
182
- await q(`CREATE INDEX IF NOT EXISTS idx_ask_task_state ON ${APPROVAL_ASK_TABLE} (task_id, state)`);
183
- await q(`CREATE INDEX IF NOT EXISTS idx_ask_session_state ON ${APPROVAL_ASK_TABLE} (session_id, state)`);
184
- await q(`CREATE INDEX IF NOT EXISTS idx_ask_batch ON ${APPROVAL_ASK_TABLE} (batch_id)`);
185
- await q(`CREATE INDEX IF NOT EXISTS idx_ask_state ON ${APPROVAL_ASK_TABLE} (state)`);
189
+ await ensureIndexes(q, "pg", APPROVAL_ASK_INDEXES);
186
190
  }
187
191
  export async function assertApprovalAskRuleMaterialSchema(query, dialect) {
188
192
  try {
@@ -32,6 +32,7 @@
32
32
  import type { Pool } from "mysql2/promise";
33
33
  import { type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentStore } from "@sema-agent/core";
34
34
  import type { PgQueryFn } from "./pg-query.js";
35
+ import { type IndexSpec } from "./ensure-index.js";
35
36
  export declare const BACKGROUND_AGENT_TABLE = "background_agent";
36
37
  type ReapOpts = {
37
38
  maxAgeMs?: number;
@@ -43,6 +44,8 @@ type ListOpts = {
43
44
  limit?: number;
44
45
  };
45
46
  export declare function ensureTiDBBackgroundAgentSchema(pool: Pool): Promise<void>;
47
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgBackgroundAgentSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
48
+ export declare const BACKGROUND_AGENT_INDEXES: readonly IndexSpec[];
46
49
  export declare function ensurePgBackgroundAgentSchema(q: PgQueryFn): Promise<void>;
47
50
  export declare class TiDBBackgroundAgentStore implements BackgroundAgentStore {
48
51
  private readonly pool;
@@ -1,6 +1,7 @@
1
1
  import { BackgroundAgentStoreError, STALE_RUNNING_REAP_ATTRIBUTION, } from "@sema-agent/core";
2
2
  import { pgSafeJsonStringify, pgSanitizeText } from "./pg-safe-json.js";
3
3
  import { isMysqlDupKeyError } from "./sql-errors.js";
4
+ import { ensureIndexes } from "./ensure-index.js";
4
5
  export const BACKGROUND_AGENT_TABLE = "background_agent";
5
6
  const SUMMARY_COLS = "handle, owner, session_scoped, session_id, parent_session_id, root_session_id, name, agent_type, status, spawned_at_ms, updated_at_ms, settled_at_ms, usage_json";
6
7
  const PROJ_COLS = "scope, owner, session_scoped, session_id, parent_session_id, root_session_id, session_anchor, name, agent_type, status, spawned_at_ms, updated_at_ms, settled_at_ms, usage_json, rev, parked_checkpoint_token, park_claim_id, admitted_org_scopes, admitted_org_write_scope, record_json";
@@ -135,7 +136,12 @@ export async function ensureTiDBBackgroundAgentSchema(pool) {
135
136
  KEY idx_bga_anchor (scope_key, session_anchor, spawned_at_ms),
136
137
  KEY idx_bga_status (scope_key, status, updated_at_ms)
137
138
  ) COLLATE utf8mb4_bin`);
139
+ await ensureIndexes((text, params) => pool.query(text, params), "mysql", BACKGROUND_AGENT_INDEXES);
138
140
  }
141
+ export const BACKGROUND_AGENT_INDEXES = [
142
+ { table: `${BACKGROUND_AGENT_TABLE}`, name: "idx_bga_anchor", columns: ["scope_key", "session_anchor", "spawned_at_ms"] },
143
+ { table: `${BACKGROUND_AGENT_TABLE}`, name: "idx_bga_status", columns: ["scope_key", "status", "updated_at_ms"] },
144
+ ];
139
145
  export async function ensurePgBackgroundAgentSchema(q) {
140
146
  await q(`CREATE TABLE IF NOT EXISTS ${BACKGROUND_AGENT_TABLE} (
141
147
  scope_key VARCHAR(190) COLLATE "C" NOT NULL,
@@ -165,8 +171,7 @@ export async function ensurePgBackgroundAgentSchema(q) {
165
171
  record_json TEXT COLLATE "C" NOT NULL,
166
172
  PRIMARY KEY (scope_key, handle)
167
173
  )`);
168
- await q(`CREATE INDEX IF NOT EXISTS idx_bga_anchor ON ${BACKGROUND_AGENT_TABLE} (scope_key, session_anchor, spawned_at_ms)`);
169
- await q(`CREATE INDEX IF NOT EXISTS idx_bga_status ON ${BACKGROUND_AGENT_TABLE} (scope_key, status, updated_at_ms)`);
174
+ await ensureIndexes(q, "pg", BACKGROUND_AGENT_INDEXES);
170
175
  }
171
176
  export class TiDBBackgroundAgentStore {
172
177
  pool;
@@ -47,6 +47,7 @@ import type { Pool as MySqlPool } from "mysql2/promise";
47
47
  import type { Pool as PgPool } from "pg";
48
48
  import { type SqlDriver } from "./sql-driver.js";
49
49
  import { type AcquireConnectionInput, type AcquireConnectionResult, type AppendAuditInput, type BindSessionInput, type BindSessionResult, type ConsumeEnrollTokenInput, type ConsumeEnrollTokenResult, type DeviceAdmissionView, type DeviceAuditRow, type DeviceEnrollTokenRow, type DeviceOwner, type DeviceRow, type DeviceSessionRow, type DeviceStore, type HeartbeatConnectionInput, type IssueEnrollTokenInput, type ListAuditFilter, type RebindSessionInput, type RebindSessionResult, type RevokeDeviceInput, type RevokeDeviceResult } from "../device-store.js";
50
+ import { type IndexSpec } from "./ensure-index.js";
50
51
  /**
51
52
  * ── 命名(**落库一律单数**,与设计稿的复数形不同)──────────────────────────────────────────────────
52
53
  * design v2 §6 的 DDL 写的是 `devices` / `device_sessions` / `device_enrollment_tokens`(复数),但本仓有一条
@@ -63,6 +64,8 @@ export declare const TIDB_DEVICE_STATEMENTS: readonly string[];
63
64
  /** {@link TIDB_DEVICE_STATEMENTS} 的遍历壳(集成测试用;生产装配由中央 ensureSchema 展开同一份数组)。 */
64
65
  export declare function ensureTiDBDeviceSchema(pool: MySqlPool): Promise<void>;
65
66
  type PgQuery = (sql: string, params?: unknown[]) => Promise<unknown>;
67
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgDeviceSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
68
+ export declare const DEVICE_INDEXES: readonly IndexSpec[];
66
69
  export declare function ensurePgDeviceSchema(q: PgQuery): Promise<void>;
67
70
  /**
68
71
  * O4 升级前置探针 —— **拒启,不是 warn**([ref]/[ref]/[ref] `assert*Schema` 族先例):`device_audit` 的
@@ -1,6 +1,7 @@
1
1
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
2
2
  import { isDupKeyError, isCheckViolationError } from "./sql-errors.js";
3
3
  import { DEVICE_AUDIT_EVENTS, assertAcquireShape, assertDeviceIdShape, assertDeviceOwnerShape, assertEnrollTokenHashShape, assertIssueTokenShape, assertNewDeviceShape, assertRebindShape, assertRevokeShape, assertRootSessionIdShape, assertWholeSeconds, parseDeviceAuditEvent, parseDevicePlatformOs, parseDeviceStatus, } from "../device-store.js";
4
+ import { ensureIndexes } from "./ensure-index.js";
4
5
  export const DEVICES_TABLE = "device";
5
6
  export const DEVICE_ENROLL_TOKENS_TABLE = "device_enroll_token";
6
7
  export const DEVICE_SESSIONS_TABLE = "device_session";
@@ -84,7 +85,14 @@ export const TIDB_DEVICE_STATEMENTS = [
84
85
  export async function ensureTiDBDeviceSchema(pool) {
85
86
  for (const stmt of TIDB_DEVICE_STATEMENTS)
86
87
  await pool.query(stmt);
88
+ await ensureIndexes((text, params) => pool.query(text, params), "mysql", DEVICE_INDEXES);
87
89
  }
90
+ export const DEVICE_INDEXES = [
91
+ { table: `${DEVICES_TABLE}`, name: "idx_device_owner", columns: ["owner_tenant", "owner_subject"] },
92
+ { table: `${DEVICE_SESSIONS_TABLE}`, name: "idx_device_session_device", columns: ["device_id"] },
93
+ { table: `${DEVICE_AUDIT_TABLE}`, name: "idx_device_audit_device", columns: ["device_id", "at"] },
94
+ { table: `${DEVICE_AUDIT_TABLE}`, name: "idx_device_audit_event", columns: ["event", "at"] },
95
+ ];
88
96
  export async function ensurePgDeviceSchema(q) {
89
97
  await q(`CREATE TABLE IF NOT EXISTS ${DEVICES_TABLE} (
90
98
  device_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -112,7 +120,6 @@ export async function ensurePgDeviceSchema(q) {
112
120
  PRIMARY KEY (device_id),
113
121
  CONSTRAINT ${UQ_DEVICES_PUBKEY} UNIQUE (owner_tenant, pubkey)
114
122
  )`);
115
- await q(`CREATE INDEX IF NOT EXISTS idx_device_owner ON ${DEVICES_TABLE} (owner_tenant, owner_subject)`);
116
123
  await q(`CREATE TABLE IF NOT EXISTS ${DEVICE_ENROLL_TOKENS_TABLE} (
117
124
  token_hash VARCHAR(128) COLLATE "C" NOT NULL,
118
125
  target_tenant VARCHAR(190) COLLATE "C" NOT NULL,
@@ -135,7 +142,6 @@ export async function ensurePgDeviceSchema(q) {
135
142
  rev BIGINT NOT NULL DEFAULT 0,
136
143
  PRIMARY KEY (root_session_id)
137
144
  )`);
138
- await q(`CREATE INDEX IF NOT EXISTS idx_device_session_device ON ${DEVICE_SESSIONS_TABLE} (device_id)`);
139
145
  await q(`CREATE TABLE IF NOT EXISTS ${DEVICE_AUDIT_TABLE} (
140
146
  id BIGSERIAL PRIMARY KEY,
141
147
  at TIMESTAMPTZ(3) NOT NULL DEFAULT now(),
@@ -146,8 +152,7 @@ export async function ensurePgDeviceSchema(q) {
146
152
  detail JSONB,
147
153
  CONSTRAINT ck_device_audit_event CHECK (event IN (${AUDIT_EVENT_CHECK}))
148
154
  )`);
149
- await q(`CREATE INDEX IF NOT EXISTS idx_device_audit_device ON ${DEVICE_AUDIT_TABLE} (device_id, at)`);
150
- await q(`CREATE INDEX IF NOT EXISTS idx_device_audit_event ON ${DEVICE_AUDIT_TABLE} (event, at)`);
155
+ await ensureIndexes(q, "pg", DEVICE_INDEXES);
151
156
  }
152
157
  export async function assertDeviceAuditRebindEventSchema(db) {
153
158
  const conn = await db.connect();
@@ -0,0 +1,42 @@
1
+ /** 本原语认得的两种方言(闭集;新方言进来是编译期的事,不是运行期的 `default` 臂)。 */
2
+ export type SqlDialect = "mysql" | "pg";
3
+ /**
4
+ * 一条索引的**声明**(纯数据)。两方言共用同一份声明 —— 这就是"一处属主"的兑现物:
5
+ * per-store 文件各导出自己那份 `readonly IndexSpec[]`,PG 侧由该 store 的 `ensurePg*Schema` 应用,
6
+ * MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 统一应用(与 `SCHEMA_STATEMENTS` 的组合姿势同形)。
7
+ */
8
+ export interface IndexSpec {
9
+ /** 表名(与两方言的 `CREATE TABLE` 同名——本仓两方言表名逐字相同)。 */
10
+ readonly table: string;
11
+ /** 索引名(两方言同名;MySQL 侧它同时是 `CREATE TABLE` 里那条内联 `KEY` 的名字)。 */
12
+ readonly name: string;
13
+ /** 列序(等值前缀在前、排序列在后;与 MySQL 内联 `KEY` 的列序**必须**逐字一致)。 */
14
+ readonly columns: readonly string[];
15
+ /** UNIQUE 索引(MySQL 内联 `UNIQUE KEY` 的孪生)。缺省 = 普通索引。 */
16
+ readonly unique?: boolean;
17
+ }
18
+ /**
19
+ * 两方言的查询面。mysql2 的 `conn.query` 答 `[rows, fields]`,node-pg 答 `{ rows }` —— 两种形的读法
20
+ * 都关在本文件里(调用点只管把自己手上那只 `query` 传进来)。
21
+ *
22
+ * 🔴 **本原语一条绑定参数都不发**(探针与 DDL 全是字面量)。两条理由:
23
+ * ① 它的输入是**声明**不是数据 —— `IndexSpec` 的表名/索引名都是编译期常量,没有用户输入可注入,
24
+ * 字面量与占位符在安全性上等价;
25
+ * ② PG 那半的 `q` 是**调用方递进来的** `PgQueryFn`(十来个 `ensurePg*Schema` 都收它),而那些 ensure
26
+ * 腿在本批之前**从不带参**,于是仓里真有丢掉 `params` 只转发 sql 的包装(如
27
+ * `test/permission-rule-store-db-integration.test.ts` 的 `(sql) => db.query(sql, [])`)。一条带 `$1`
28
+ * 的探针会在那种包装上报 `there is no parameter $1` —— 而那是**调用方**的形,不是本原语能管的。
29
+ * 不带参 ⇒ 这一整类接缝脆弱性结构上不存在。两方言同律,不留一边带参一边不带的不对称。
30
+ */
31
+ export type IndexQueryFn = (sql: string, params?: unknown[]) => Promise<unknown>;
32
+ /**
33
+ * 幂等地保证一条索引在场。**这是本仓唯一允许发加索引 DDL 的地方**(SCHEMA POLICY 窄放的全部内容)。
34
+ *
35
+ * @param q 方言对应的查询面(mysql2 conn/pool 的 `query`,或裸 `PgQueryFn`)
36
+ * @param dialect 方言闭集
37
+ * @param spec 索引声明(两方言共用同一份)
38
+ */
39
+ export declare function ensureIndex(q: IndexQueryFn, dialect: SqlDialect, spec: IndexSpec): Promise<void>;
40
+ /** {@link ensureIndex} 的**逐条串行**遍历(DDL 并发打同一张表会踩 catalog 竞争,故不并行)。 */
41
+ export declare function ensureIndexes(q: IndexQueryFn, dialect: SqlDialect, specs: readonly IndexSpec[]): Promise<void>;
42
+ //# sourceMappingURL=ensure-index.d.ts.map
@@ -0,0 +1,81 @@
1
+ import { recordFailOpen } from "../observability/fail-open.js";
2
+ function rowsOf(res) {
3
+ if (Array.isArray(res)) {
4
+ const first = res[0];
5
+ return Array.isArray(first) ? first : [];
6
+ }
7
+ if (res && typeof res === "object" && Array.isArray(res.rows)) {
8
+ return res.rows;
9
+ }
10
+ return [];
11
+ }
12
+ const quoteId = (dialect, id) => dialect === "mysql" ? `\`${id.replace(/`/g, "``")}\`` : `"${id.replace(/"/g, '""')}"`;
13
+ const quoteLiteral = (s) => `'${s.replace(/'/g, "''")}'`;
14
+ const columnList = (dialect, spec) => spec.columns.map((c) => quoteId(dialect, c)).join(", ");
15
+ function lacksConcurrentlySupport(err) {
16
+ const code = err?.code;
17
+ if (code === "0A000" || code === "42601")
18
+ return true;
19
+ return typeof code !== "string";
20
+ }
21
+ async function pgIndexValid(q, name) {
22
+ const rows = rowsOf(await q(`SELECT i.indisvalid FROM pg_index i WHERE i.indexrelid = to_regclass(${quoteLiteral(name)})`));
23
+ if (rows.length === 0)
24
+ return undefined;
25
+ return rows[0].indisvalid !== false;
26
+ }
27
+ const refusal = (spec, unique, what) => `ensureIndex: PostgreSQL 索引 ${spec.name}(表 ${spec.table})${what} —— 拒绝带病启动。` +
28
+ "(一个 INVALID 索引会让此后每一次 CREATE INDEX … IF NOT EXISTS 静默跳过它,而规划器永不使用它。)" +
29
+ ` 运维配方:DROP INDEX CONCURRENTLY IF EXISTS ${quoteId("pg", spec.name)}; 然后在维护窗口手工 ` +
30
+ `CREATE ${unique}INDEX CONCURRENTLY ${quoteId("pg", spec.name)} ON ${quoteId("pg", spec.table)} (${columnList("pg", spec)});` +
31
+ " 常见根因 = 池级 statement_timeout(DB_QUERY_TIMEOUT_MS)把构建切断(见 cause),或 UNIQUE 索引撞上存量重复行。";
32
+ export async function ensureIndex(q, dialect, spec) {
33
+ switch (dialect) {
34
+ case "mysql": {
35
+ const present = rowsOf(await q("SELECT 1 AS present FROM information_schema.statistics WHERE table_schema = DATABASE() " +
36
+ `AND table_name = ${quoteLiteral(spec.table)} AND index_name = ${quoteLiteral(spec.name)} LIMIT 1`));
37
+ if (present.length > 0)
38
+ return;
39
+ await q(`ALTER TABLE ${quoteId("mysql", spec.table)} ADD ${spec.unique ? "UNIQUE " : ""}INDEX ` +
40
+ `${quoteId("mysql", spec.name)} (${columnList("mysql", spec)})`);
41
+ return;
42
+ }
43
+ case "pg": {
44
+ const unique = spec.unique ? "UNIQUE " : "";
45
+ const name = quoteId("pg", spec.name);
46
+ const tail = `${unique}INDEX CONCURRENTLY IF NOT EXISTS ${name} ON ${quoteId("pg", spec.table)} (${columnList("pg", spec)})`;
47
+ try {
48
+ await q(`CREATE ${tail}`);
49
+ }
50
+ catch (err) {
51
+ if (!lacksConcurrentlySupport(err))
52
+ throw err;
53
+ recordFailOpen("server.ensure-index.concurrently-unsupported", `${spec.table}.${spec.name}`);
54
+ await q(`CREATE ${unique}INDEX IF NOT EXISTS ${name} ON ${quoteId("pg", spec.table)} (${columnList("pg", spec)})`);
55
+ return;
56
+ }
57
+ const valid = await pgIndexValid(q, spec.name);
58
+ if (valid !== false)
59
+ return;
60
+ await q(`DROP INDEX CONCURRENTLY IF EXISTS ${name}`);
61
+ try {
62
+ await q(`CREATE ${tail}`);
63
+ }
64
+ catch (err) {
65
+ throw new Error(refusal(spec, unique, "重建时失败"), { cause: err });
66
+ }
67
+ if ((await pgIndexValid(q, spec.name)) === false)
68
+ throw new Error(refusal(spec, unique, "重建后仍是 INVALID"));
69
+ return;
70
+ }
71
+ default: {
72
+ const _never = dialect;
73
+ throw new Error(`ensureIndex: 未知方言 ${String(_never)}`);
74
+ }
75
+ }
76
+ }
77
+ export async function ensureIndexes(q, dialect, specs) {
78
+ for (const spec of specs)
79
+ await ensureIndex(q, dialect, spec);
80
+ }
81
+ //# sourceMappingURL=ensure-index.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 两方言启动锁(`ensureSchema` / `oneTimeMigrations` 的 MySQL `GET_LOCK(?,0)` 与 PG `pg_try_advisory_lock`)的
3
+ * **轮询策略唯一属主**(7.83.0 修复验证 车HC HC-1/HC-2 后收口:此前两只孪生各抄一份循环,一份改了「永不放弃」
4
+ * 另一份还是「40 次放弃后无锁跑」,而 S-287 之后无锁跑的那条腿发的是不带 `IF NOT EXISTS` 的 `ADD INDEX`,两副本
5
+ * 双发 ⇒ 输的那只拒启)。方言只提供「试一次」的探针,循环、抖动、退避、进展 warn、坏连接上抛全在这里。
6
+ *
7
+ * 三条律:
8
+ * · **按进展轮询,不按次数放弃** —— 持锁方还活着(两方言的锁都是连接级,随连接消失,不会遗留)就一直等,
9
+ * 每 {@link LOCK_PROBE_WARN_EVERY} 次 warn 一行说明在等谁;**永不**无锁跑(「响亮地等」严格优于「静默地竞争 DDL」)。
10
+ * · **坏连接不是争用**(HC-2,对 7.82.1 的回归):探针自己**连续**抛 {@link MAX_CONSECUTIVE_LOCK_PROBE_ERRORS} 次 =
11
+ * 这条连接已不可用(`_queryable=false` / 半开 TCP),与「谁持锁」无关 ⇒ 上抛让启动响亮失败,不许无界轮。
12
+ * 偶发一次抛(TiDB 悲观锁抖动)照旧继续轮,计数在一次成功探针后归零。
13
+ * · 轮询间隙进程不持任何快照(每次探针是亚毫秒的独立语句)⇒ 与 PG `CREATE INDEX CONCURRENTLY` 的
14
+ * `WaitForOlderSnapshots` 永不互等(理由全文见 `pg-pool.ts` `acquirePgEnsureSchemaLock` 头注)。
15
+ */
16
+ export declare const LOCK_PROBE_INITIAL_JITTER_MS = 400;
17
+ export declare const LOCK_PROBE_WARN_EVERY = 40;
18
+ export declare const MAX_CONSECUTIVE_LOCK_PROBE_ERRORS = 5;
19
+ /** 抖动退避(~0.1s..0.4s + 至多 0.15s 抖动)—— 两方言同一条曲线。 */
20
+ export declare function lockProbeBackoffMs(attempt: number): number;
21
+ /**
22
+ * 轮到拿到为止。`tryOnce` = 方言自己的非阻塞探针:`true` 拿到、`false` 别人持着(继续轮)、抛 = 探针本身失败。
23
+ * 初始抖动把 ~20 个在 t=0 同时开火的调用方(集成套扇出 / 同步的舰队启动)铺开。
24
+ */
25
+ export declare function pollUntilLocked(tryOnce: () => Promise<boolean>, opts: {
26
+ tag: string;
27
+ label: string;
28
+ }): Promise<void>;
29
+ //# sourceMappingURL=ensure-schema-lock-policy.d.ts.map
@@ -0,0 +1,31 @@
1
+ export const LOCK_PROBE_INITIAL_JITTER_MS = 400;
2
+ export const LOCK_PROBE_WARN_EVERY = 40;
3
+ export const MAX_CONSECUTIVE_LOCK_PROBE_ERRORS = 5;
4
+ export function lockProbeBackoffMs(attempt) {
5
+ return 100 + Math.min(attempt, 3) * 100 + Math.floor(Math.random() * 150);
6
+ }
7
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
8
+ export async function pollUntilLocked(tryOnce, opts) {
9
+ await sleep(Math.floor(Math.random() * LOCK_PROBE_INITIAL_JITTER_MS));
10
+ let consecutiveErrors = 0;
11
+ for (let attempt = 0;; attempt++) {
12
+ try {
13
+ if (await tryOnce())
14
+ return;
15
+ consecutiveErrors = 0;
16
+ }
17
+ catch (err) {
18
+ consecutiveErrors += 1;
19
+ const msg = err?.message ?? String(err);
20
+ console.warn(`[${opts.tag}] ${opts.label} probe attempt ${attempt + 1} threw (${consecutiveErrors}/${MAX_CONSECUTIVE_LOCK_PROBE_ERRORS}):`, msg);
21
+ if (consecutiveErrors >= MAX_CONSECUTIVE_LOCK_PROBE_ERRORS) {
22
+ throw new Error(`[${opts.tag}] ${opts.label}: lock probe threw ${MAX_CONSECUTIVE_LOCK_PROBE_ERRORS} times in a row — the connection itself is unusable, refusing to poll forever (last: ${msg})`, { cause: err });
23
+ }
24
+ }
25
+ if (attempt > 0 && attempt % LOCK_PROBE_WARN_EVERY === 0) {
26
+ console.warn(`[${opts.tag}] still waiting for ${opts.label} (another replica is running the serialized DDL; attempt ${attempt})`);
27
+ }
28
+ await sleep(lockProbeBackoffMs(attempt));
29
+ }
30
+ }
31
+ //# sourceMappingURL=ensure-schema-lock-policy.js.map
@@ -2,6 +2,7 @@ import type { Pool as MySqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool, PoolClient } from "pg";
3
3
  import { type SqlDriver } from "./sql-driver.js";
4
4
  import { type BakeStatus, type BakeState, type BakeRecord, type BakeEvent, type CreateBakeInput, type BakeTerminal } from "./store-contracts.js";
5
+ import { type IndexSpec } from "./ensure-index.js";
5
6
  /** PG translation of the image_bake / image_bake_event / image_bake_lease DDL in tidb-pool.ts SCHEMA_STATEMENTS
6
7
  * (JSON→JSONB, TINYINT→SMALLINT, DATETIME(3)→TIMESTAMPTZ(3), inline UNIQUE/KEY→named CONSTRAINT + CREATE INDEX).
7
8
  * TINYINT(1) 归一(clay 批 2026-07-26):布尔列的 PG 映射统一 SMALLINT 0/1(此前本家族用 BOOLEAN)——
@@ -9,6 +10,8 @@ import { type BakeStatus, type BakeState, type BakeRecord, type BakeEvent, type
9
10
  * 读侧 `!!`/`Boolean()` 保持,store 出口 JS 类型逐字节不变(真双库套件的严格值断言为证)。
10
11
  * 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
12
  export declare const PG_IMAGE_BAKE_SCHEMA: string[];
13
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgImageBakeSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
14
+ export declare const IMAGE_BAKE_INDEXES: readonly IndexSpec[];
12
15
  /** Idempotent self-contained PG schema apply (for the integration test; central aggregation is done separately). */
13
16
  export declare function ensurePgImageBakeSchema(pool: PgPool | PoolClient, poolName?: string): Promise<void>;
14
17
  /** Dual-dialect BAKE store. See the file header for the dialect-delta ledger. */
@@ -5,6 +5,7 @@ import { parseJsonOr as parseJson, toIso as iso } from "./sql-row-helpers.js";
5
5
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
6
6
  import { mapBakeRow as mapRow, BAKE_SELECT_COLS as SELECT_COLS, } from "./store-contracts.js";
7
7
  import { isDupKeyError } from "./sql-errors.js";
8
+ import { ensureIndexes } from "./ensure-index.js";
8
9
  export const PG_IMAGE_BAKE_SCHEMA = [
9
10
  `CREATE TABLE IF NOT EXISTS image_bake (
10
11
  bake_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -37,8 +38,6 @@ export const PG_IMAGE_BAKE_SCHEMA = [
37
38
  PRIMARY KEY (bake_id),
38
39
  CONSTRAINT uq_image_bake_idem UNIQUE (idem_key)
39
40
  )`,
40
- `CREATE INDEX IF NOT EXISTS idx_image_bake_status ON image_bake (status)`,
41
- `CREATE INDEX IF NOT EXISTS idx_image_bake_created ON image_bake (created_at)`,
42
41
  `CREATE TABLE IF NOT EXISTS image_bake_event (
43
42
  bake_id VARCHAR(64) COLLATE "C" NOT NULL,
44
43
  seq INT NOT NULL,
@@ -62,10 +61,15 @@ export const PG_IMAGE_BAKE_SCHEMA = [
62
61
  PRIMARY KEY (pool)
63
62
  )`,
64
63
  ];
64
+ export const IMAGE_BAKE_INDEXES = [
65
+ { table: "image_bake", name: "idx_image_bake_status", columns: ["status"] },
66
+ { table: "image_bake", name: "idx_image_bake_created", columns: ["created_at"] },
67
+ ];
65
68
  export async function ensurePgImageBakeSchema(pool, poolName = "standard") {
66
69
  for (const stmt of PG_IMAGE_BAKE_SCHEMA)
67
70
  await pool.query(stmt);
68
71
  await pool.query("INSERT INTO image_bake_admit (pool) VALUES ($1) ON CONFLICT (pool) DO NOTHING", [poolName]);
72
+ await ensureIndexes((t, p) => pool.query(t, p), "pg", IMAGE_BAKE_INDEXES);
69
73
  }
70
74
  const isDupKey = isDupKeyError;
71
75
  function dupKeyName(dialect, err) {