@sema-agent/server 7.82.0 → 7.83.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 (89) hide show
  1. package/USAGE.md +17 -14
  2. package/dist/bench/s1/arms.d.ts +4 -4
  3. package/dist/bench/s1/arms.js +1 -2
  4. package/dist/bench/s1/live-deps.js +0 -1
  5. package/dist/bench/s1/runner-ctx.d.ts +3 -3
  6. package/dist/bench/s1/tasks.d.ts +8 -4
  7. package/dist/boot/stage-07-capability-layer.d.ts +1 -0
  8. package/dist/boot/stage-07-capability-layer.js +1 -1
  9. package/dist/boot/stage-08-reapers.d.ts +1 -0
  10. package/dist/boot/stage-09-leader.d.ts +1 -0
  11. package/dist/boot/stage-10-http-server.d.ts +1 -0
  12. package/dist/boot/stage-10-http-server.js +2 -1
  13. package/dist/config-catalog.js +2 -3
  14. package/dist/config.js +1 -0
  15. package/dist/http/routes/capabilities.js +2 -0
  16. package/dist/http/server.d.ts +8 -0
  17. package/dist/leader/endpoint.js +2 -0
  18. package/dist/leader/leader.d.ts +33 -6
  19. package/dist/leader/leader.js +54 -45
  20. package/dist/leader/merge.d.ts +38 -29
  21. package/dist/leader/merge.js +45 -38
  22. package/dist/leader/wire.d.ts +2 -17
  23. package/dist/leader/wire.js +5 -58
  24. package/dist/observability/fail-open.d.ts +5 -5
  25. package/dist/observability/fail-open.js +5 -2
  26. package/dist/observability/run-terminal-log.d.ts +0 -7
  27. package/dist/observability/run-terminal-log.js +9 -20
  28. package/dist/plugins/adoption-log-sql.d.ts +3 -0
  29. package/dist/plugins/adoption-log-sql.js +5 -1
  30. package/dist/plugins/approval-ask-store-sql.d.ts +3 -0
  31. package/dist/plugins/approval-ask-store-sql.js +9 -5
  32. package/dist/plugins/background-agent-store-sql.d.ts +3 -0
  33. package/dist/plugins/background-agent-store-sql.js +7 -2
  34. package/dist/plugins/device-store-sql.d.ts +3 -0
  35. package/dist/plugins/device-store-sql.js +9 -4
  36. package/dist/plugins/ensure-index.d.ts +42 -0
  37. package/dist/plugins/ensure-index.js +81 -0
  38. package/dist/plugins/ensure-schema-lock-policy.d.ts +29 -0
  39. package/dist/plugins/ensure-schema-lock-policy.js +31 -0
  40. package/dist/plugins/image-bake-store-sql.d.ts +3 -0
  41. package/dist/plugins/image-bake-store-sql.js +6 -2
  42. package/dist/plugins/image-index-sql.d.ts +3 -0
  43. package/dist/plugins/image-index-sql.js +8 -4
  44. package/dist/plugins/leader-run-store-sql.d.ts +3 -0
  45. package/dist/plugins/leader-run-store-sql.js +6 -2
  46. package/dist/plugins/mailbox-store-sql.d.ts +3 -0
  47. package/dist/plugins/mailbox-store-sql.js +6 -1
  48. package/dist/plugins/memory-engine-pg.d.ts +4 -0
  49. package/dist/plugins/memory-engine-pg.js +6 -6
  50. package/dist/plugins/memory-engine-tidb.js +5 -0
  51. package/dist/plugins/memory-sync-store-pg.d.ts +5 -0
  52. package/dist/plugins/memory-sync-store-pg.js +9 -2
  53. package/dist/plugins/memory-sync-store-tidb.js +6 -0
  54. package/dist/plugins/one-time-migrations.d.ts +1 -1
  55. package/dist/plugins/one-time-migrations.js +7 -14
  56. package/dist/plugins/outcome-ledger-sql.d.ts +3 -0
  57. package/dist/plugins/outcome-ledger-sql.js +7 -3
  58. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  59. package/dist/plugins/permission-rule-store-sql.js +9 -5
  60. package/dist/plugins/pg-cost-quota.d.ts +3 -0
  61. package/dist/plugins/pg-cost-quota.js +5 -1
  62. package/dist/plugins/pg-pool.d.ts +23 -2
  63. package/dist/plugins/pg-pool.js +22 -37
  64. package/dist/plugins/pg-rate-limiter.d.ts +3 -0
  65. package/dist/plugins/pg-rate-limiter.js +5 -1
  66. package/dist/plugins/pg-session-storage.d.ts +3 -0
  67. package/dist/plugins/pg-session-storage.js +8 -4
  68. package/dist/plugins/remote-env-adb.d.ts +62 -3
  69. package/dist/plugins/remote-env-adb.js +27 -15
  70. package/dist/plugins/remote-env-host.js +1 -1
  71. package/dist/plugins/remote-shell.d.ts +7 -11
  72. package/dist/plugins/remote-shell.js +3 -3
  73. package/dist/plugins/retention-store-sql.d.ts +3 -0
  74. package/dist/plugins/retention-store-sql.js +7 -3
  75. package/dist/plugins/roster-store-sql.d.ts +3 -0
  76. package/dist/plugins/roster-store-sql.js +6 -1
  77. package/dist/plugins/schema-indexes.d.ts +21 -0
  78. package/dist/plugins/schema-indexes.js +25 -0
  79. package/dist/plugins/task-attachment-store.d.ts +3 -0
  80. package/dist/plugins/task-attachment-store.js +7 -2
  81. package/dist/plugins/tidb-pool.d.ts +15 -3
  82. package/dist/plugins/tidb-pool.js +38 -15
  83. package/dist/plugins/tool-result-store-sql.d.ts +3 -0
  84. package/dist/plugins/tool-result-store-sql.js +5 -1
  85. package/dist/plugins/web-search.d.ts +50 -2
  86. package/dist/plugins/web-search.js +9 -2
  87. package/dist/trace/project.d.ts +2 -1
  88. package/dist/trace/redact.d.ts +6 -0
  89. package/package.json +2 -2
@@ -4,10 +4,14 @@ import { pgHasUnstorable } from "./pg-safe-json.js";
4
4
  import { assertMemoryScopeWidth, memoryEntryKeyWidthRefusal } from "./memory-key-guards.js";
5
5
  import { ambiguousOriginRefusal, createBatchCommittedBaseline, exposureBandOf, exposureCarriage, originWhitewashRefusal, rememberBatchCommitted, distilledWhitewashRefusal, distilledCarriage, unknownPatchOpRefusal, } from "./memory-origin-law.js";
6
6
  import { isMysqlDupKeyError } from "./sql-errors.js";
7
+ import { ensureIndexes } from "./ensure-index.js";
8
+ import { MEMORY_ENGINE_INDEXES, PG_MEMORY_ENGINE_TABLES } from "./memory-engine-pg.js";
7
9
  export const TIDB_MEMORY_ENGINE_TABLES = {
8
10
  entry: "agent_memory_engine_entry",
9
11
  cursor: "agent_memory_engine_cursor",
10
12
  };
13
+ TIDB_MEMORY_ENGINE_TABLES;
14
+ PG_MEMORY_ENGINE_TABLES;
11
15
  export async function ensureTiDBMemoryEngineSchema(pool) {
12
16
  await pool.query(`CREATE TABLE IF NOT EXISTS ${TIDB_MEMORY_ENGINE_TABLES.entry} (
13
17
  id VARCHAR(64) NOT NULL,
@@ -28,6 +32,7 @@ export async function ensureTiDBMemoryEngineSchema(pool) {
28
32
  \`cursor\` TEXT NOT NULL,
29
33
  PRIMARY KEY (scope)
30
34
  ) COLLATE utf8mb4_bin`);
35
+ await ensureIndexes((text, params) => pool.query(text, params), "mysql", MEMORY_ENGINE_INDEXES);
31
36
  }
32
37
  const isDupEntry = isMysqlDupKeyError;
33
38
  function isPrimaryKeyDup(err) {
@@ -1,5 +1,6 @@
1
1
  import type { MemorySyncCursor } from "@sema-agent/core";
2
2
  import type { PgQueryFn } from "./pg-query.js";
3
+ import { type IndexSpec } from "./ensure-index.js";
3
4
  /** Table names (single source). SAME names as the TiDB twin (the two dialects never share one DB). */
4
5
  export declare const PG_MEMORY_SYNC_TABLES: {
5
6
  readonly syncCursor: "agent_memory_engine_sync_cursor";
@@ -45,6 +46,8 @@ export interface MemorySyncStore {
45
46
  /** Transient failure: attempts+1 (SQL-atomic), record the error, schedule the next attempt. */
46
47
  fail(id: string, error: string, nextAttemptAtMs: number): Promise<void>;
47
48
  }
49
+ /** S-287:push queue 的索引**声明**(MySQL 孪生 = `memory-sync-store-tidb.ts` 的内联 `KEY`)。 */
50
+ export declare const MEMORY_SYNC_INDEXES: readonly IndexSpec[];
48
51
  /** Idempotent DDL for the sync plane (PG dialect). Call once at startup. */
49
52
  export declare function ensurePgMemorySyncSchema(query: PgQueryFn): Promise<void>;
50
53
  /** Options for {@link PgMemorySyncStore}. */
@@ -106,6 +109,8 @@ export interface MemoryHistoryStore {
106
109
  /** Ops read face (§1.3 保留策略: v1 无界 + 运维计数 — quota/pruning waits for a demand signal). */
107
110
  countByScope(scope: string): Promise<number>;
108
111
  }
112
+ /** S-287:history 表的索引**声明**(MySQL 孪生 = `memory-sync-store-tidb.ts` 的内联 `KEY`)。 */
113
+ export declare const MEMORY_HISTORY_INDEXES: readonly IndexSpec[];
109
114
  /** Idempotent DDL for the history table (PG dialect). Separate from the sync-plane ensure so S5.1
110
115
  * can wire the write point independently of the S4 sync wiring. */
111
116
  export declare function ensurePgMemoryHistorySchema(query: PgQueryFn): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { ensureIndexes } from "./ensure-index.js";
1
2
  import { pgSafeJsonStringify, pgProtocolJsonStringify } from "./pg-safe-json.js";
2
3
  import { assertMemoryScopeWidth } from "./memory-key-guards.js";
3
4
  export const PG_MEMORY_SYNC_TABLES = {
@@ -5,6 +6,9 @@ export const PG_MEMORY_SYNC_TABLES = {
5
6
  pushQueue: "agent_memory_engine_push_queue",
6
7
  history: "agent_memory_engine_history",
7
8
  };
9
+ export const MEMORY_SYNC_INDEXES = [
10
+ { table: `${PG_MEMORY_SYNC_TABLES.pushQueue}`, name: `idx_${PG_MEMORY_SYNC_TABLES.pushQueue}_next_attempt_ms`, columns: ["next_attempt_at_ms"] },
11
+ ];
8
12
  export async function ensurePgMemorySyncSchema(query) {
9
13
  await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_SYNC_TABLES.syncCursor} (
10
14
  scope varchar(190) COLLATE "C" NOT NULL,
@@ -22,7 +26,7 @@ export async function ensurePgMemorySyncSchema(query) {
22
26
  next_attempt_at_ms bigint NOT NULL,
23
27
  last_error text COLLATE "C" NULL
24
28
  )`);
25
- await query(`CREATE INDEX IF NOT EXISTS idx_${PG_MEMORY_SYNC_TABLES.pushQueue}_next_attempt_ms ON ${PG_MEMORY_SYNC_TABLES.pushQueue} (next_attempt_at_ms)`);
29
+ await ensureIndexes(query, "pg", MEMORY_SYNC_INDEXES);
26
30
  }
27
31
  function fromJsonb(v) {
28
32
  if (typeof v === "string")
@@ -99,6 +103,9 @@ export class PgMemorySyncStore {
99
103
  WHERE id = $1`, [id, error, nextAttemptAtMs]);
100
104
  }
101
105
  }
106
+ export const MEMORY_HISTORY_INDEXES = [
107
+ { table: `${PG_MEMORY_SYNC_TABLES.history}`, name: `idx_${PG_MEMORY_SYNC_TABLES.history}_scope`, columns: ["scope"] },
108
+ ];
102
109
  export async function ensurePgMemoryHistorySchema(query) {
103
110
  await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_SYNC_TABLES.history} (
104
111
  id char(36) COLLATE "C" NOT NULL,
@@ -110,7 +117,7 @@ export async function ensurePgMemoryHistorySchema(query) {
110
117
  written_at_ms bigint NOT NULL,
111
118
  PRIMARY KEY (id, rev)
112
119
  )`);
113
- await query(`CREATE INDEX IF NOT EXISTS idx_${PG_MEMORY_SYNC_TABLES.history}_scope ON ${PG_MEMORY_SYNC_TABLES.history} (scope)`);
120
+ await ensureIndexes(query, "pg", MEMORY_HISTORY_INDEXES);
114
121
  }
115
122
  export class PgMemoryHistoryStore {
116
123
  query;
@@ -1,10 +1,14 @@
1
1
  import { historyRowFrom } from "./memory-sync-store-pg.js";
2
+ import { ensureIndexes } from "./ensure-index.js";
3
+ import { MEMORY_SYNC_INDEXES, MEMORY_HISTORY_INDEXES, PG_MEMORY_SYNC_TABLES } from "./memory-sync-store-pg.js";
2
4
  import { assertMemoryScopeWidth } from "./memory-key-guards.js";
3
5
  export const TIDB_MEMORY_SYNC_TABLES = {
4
6
  syncCursor: "agent_memory_engine_sync_cursor",
5
7
  pushQueue: "agent_memory_engine_push_queue",
6
8
  history: "agent_memory_engine_history",
7
9
  };
10
+ TIDB_MEMORY_SYNC_TABLES;
11
+ PG_MEMORY_SYNC_TABLES;
8
12
  export async function ensureTiDBMemorySyncSchema(pool) {
9
13
  await pool.query(`CREATE TABLE IF NOT EXISTS ${TIDB_MEMORY_SYNC_TABLES.syncCursor} (
10
14
  scope VARCHAR(190) NOT NULL,
@@ -24,6 +28,7 @@ export async function ensureTiDBMemorySyncSchema(pool) {
24
28
  PRIMARY KEY (id),
25
29
  KEY idx_agent_memory_engine_push_queue_next_attempt_ms (next_attempt_at_ms)
26
30
  ) COLLATE utf8mb4_bin`);
31
+ await ensureIndexes((text, params) => pool.query(text, params), "mysql", MEMORY_SYNC_INDEXES);
27
32
  }
28
33
  function fromJson(v) {
29
34
  if (typeof v === "string")
@@ -116,6 +121,7 @@ export async function ensureTiDBMemoryHistorySchema(pool) {
116
121
  PRIMARY KEY (id, rev),
117
122
  KEY idx_agent_memory_engine_history_scope (scope)
118
123
  ) COLLATE utf8mb4_bin`);
124
+ await ensureIndexes((text, params) => pool.query(text, params), "mysql", MEMORY_HISTORY_INDEXES);
119
125
  }
120
126
  export class TiDBMemoryHistoryStore {
121
127
  pool;
@@ -39,7 +39,7 @@ export declare class RetiredSteerSeatError extends Error {
39
39
  *
40
40
  * ## 并发(codex r1 [medium],亲核复现后修)
41
41
  * 一次滚动升级会让 N 个副本同时跑到这里。整段因此跑在**一条连接**上、握着**与 `ensureSchema` 同一把**
42
- * advisory 锁(MySQL 具名锁 / PG `pg_advisory_lock`):本段是「启动期改 schema 的活」,与 ensure 属同一类,
42
+ * advisory 锁(MySQL 具名锁 / PG `pg_try_advisory_lock`,两方言都是非阻塞轮询形):本段是「启动期改 schema 的活」,与 ensure 属同一类,
43
43
  * 复用同一把锁 ⇒ 本仓这一类工作只有**一个**锁身份,而且本段也不会与另一个副本的 ensure 交错。
44
44
  * 锁拿不到时按既有先例**降级**(不阻塞启动),但降级不等于放任:每条破坏性 DDL 的成功判据是它的
45
45
  * **后置条件**({@link execUntil}),所以「另一个副本抢先删掉了同一列」不会把滚动升级变成启动失败循环。
@@ -1,5 +1,5 @@
1
1
  import { acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js";
2
- import { ENSURE_SCHEMA_LOCK_KEY } from "./pg-pool.js";
2
+ import { acquirePgEnsureSchemaLock, releasePgEnsureSchemaLock } from "./pg-pool.js";
3
3
  export const NO_SQL_MIGRATIONS = {
4
4
  retiredSnapshotTable: "absent",
5
5
  retiredSnapshotRows: 0,
@@ -82,7 +82,8 @@ async function withMysqlLock(pool, run) {
82
82
  const conn = await pool.getConnection();
83
83
  let locked = false;
84
84
  try {
85
- locked = await acquireEnsureSchemaLock(conn, "oneTimeMigrations");
85
+ await acquireEnsureSchemaLock(conn, "oneTimeMigrations");
86
+ locked = true;
86
87
  return await run(mysqlDb(conn));
87
88
  }
88
89
  finally {
@@ -95,21 +96,13 @@ async function withPgLock(pool, run) {
95
96
  const client = await pool.connect();
96
97
  let locked = false;
97
98
  try {
98
- try {
99
- await client.query("SELECT pg_advisory_lock($1)", [ENSURE_SCHEMA_LOCK_KEY]);
100
- locked = true;
101
- }
102
- catch (err) {
103
- console.warn(`[oneTimeMigrations] pg_advisory_lock(${ENSURE_SCHEMA_LOCK_KEY}) failed; running the migration unserialized:`, err);
104
- }
99
+ await acquirePgEnsureSchemaLock(client, "oneTimeMigrations");
100
+ locked = true;
105
101
  return await run(pgDb(client));
106
102
  }
107
103
  finally {
108
- if (locked) {
109
- await client
110
- .query("SELECT pg_advisory_unlock($1)", [ENSURE_SCHEMA_LOCK_KEY])
111
- .catch((err) => void console.warn(`[oneTimeMigrations] pg_advisory_unlock(${ENSURE_SCHEMA_LOCK_KEY}) failed (auto-freed on connection close):`, err));
112
- }
104
+ if (locked)
105
+ await releasePgEnsureSchemaLock(client, "oneTimeMigrations");
113
106
  client.release();
114
107
  }
115
108
  }
@@ -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 { TaskOutcome as CoreTaskOutcome } from "@sema-agent/core";
4
4
  import { type SqlDriver } from "./sql-driver.js";
5
+ import { type IndexSpec } from "./ensure-index.js";
5
6
  /** Mechanical signals — the ONLY tier a future self-scheduler may read (council blocker 1). */
6
7
  export interface MechanicalOutcome {
7
8
  route?: "single" | "fanout";
@@ -88,6 +89,8 @@ export declare function coreOutcomeToLedgerRow(o: CoreTaskOutcome): TaskOutcome;
88
89
  * drop-and-recreate, and NO new `ALTER TABLE` seams are added here; fold into the CREATE instead.
89
90
  */
90
91
  export declare const PG_OUTCOME_LEDGER_SCHEMA: string[];
92
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgOutcomeLedgerSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
93
+ export declare const OUTCOME_LEDGER_INDEXES: readonly IndexSpec[];
91
94
  /** Idempotent self-contained PG schema apply (for the integration test; central aggregation via pg-pool.ts). */
92
95
  export declare function ensurePgOutcomeLedgerSchema(pool: PgPool | PoolClient): Promise<void>;
93
96
  /** Dual-dialect outcome ledger. See the file header for the dialect-delta ledger. */
@@ -1,5 +1,6 @@
1
1
  import { randomUUID, createHash } from "node:crypto";
2
2
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
3
+ import { ensureIndexes } from "./ensure-index.js";
3
4
  export function taskSignature(s) {
4
5
  if (s.tag)
5
6
  return `tag:${s.tag}`;
@@ -70,13 +71,16 @@ export const PG_OUTCOME_LEDGER_SCHEMA = [
70
71
  created_at TIMESTAMPTZ(3) NOT NULL,
71
72
  PRIMARY KEY (id)
72
73
  )`,
73
- `CREATE INDEX IF NOT EXISTS idx_outcome_ledger_sig_model ON outcome_ledger (task_signature, model, run_status)`,
74
- `CREATE INDEX IF NOT EXISTS idx_outcome_ledger_outcome ON outcome_ledger (outcome)`,
75
- `CREATE INDEX IF NOT EXISTS idx_outcome_ledger_created ON outcome_ledger (created_at)`,
74
+ ];
75
+ export const OUTCOME_LEDGER_INDEXES = [
76
+ { table: "outcome_ledger", name: "idx_outcome_ledger_sig_model", columns: ["task_signature", "model", "run_status"] },
77
+ { table: "outcome_ledger", name: "idx_outcome_ledger_outcome", columns: ["outcome"] },
78
+ { table: "outcome_ledger", name: "idx_outcome_ledger_created", columns: ["created_at"] },
76
79
  ];
77
80
  export async function ensurePgOutcomeLedgerSchema(pool) {
78
81
  for (const stmt of PG_OUTCOME_LEDGER_SCHEMA)
79
82
  await pool.query(stmt);
83
+ await ensureIndexes((t, p) => pool.query(t, p), "pg", OUTCOME_LEDGER_INDEXES);
80
84
  }
81
85
  export class SqlOutcomeLedger {
82
86
  db;
@@ -3,6 +3,7 @@ import { type PermissionRuleWriter, type DurableRulePartition, type DurableRuleP
3
3
  import type { Pool as MySqlPool } from "mysql2/promise";
4
4
  import type { PgQueryFn } from "./pg-query.js";
5
5
  import { type SqlDriver } from "./sql-driver.js";
6
+ import { type IndexSpec } from "./ensure-index.js";
6
7
  /**
7
8
  * 一只 durable 分区的写面,或 `undefined`(该 backend 从引擎侧看是只读的)。
8
9
  *
@@ -105,6 +106,8 @@ export declare function assertPermissionRuleApprovalSchema(query: (sql: string)
105
106
  export declare function assertPermissionRuleTicketSchema(query: (sql: string) => Promise<{
106
107
  rows: Record<string, unknown>[];
107
108
  }>, dialect: "tidb" | "pg"): Promise<void>;
109
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgPermissionRuleSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
110
+ export declare const PERMISSION_RULE_INDEXES: readonly IndexSpec[];
108
111
  export declare function ensurePgPermissionRuleSchema(q: PgQueryFn): Promise<void>;
109
112
  /**
110
113
  * 一只桶的**存储键**(纯数据 ⇒ `build*`)。
@@ -5,6 +5,7 @@ import { dialectProtocolJsonEncoder, sqlDbNowMs, SQL_DB_NOW_MS } from "./sql-dri
5
5
  import { isMissingColumnError } from "./sql-errors.js";
6
6
  import { canonicalJson } from "../canonical-json.js";
7
7
  import { EDITED_RULE_BREADTH_WARNING_CODES, PERSISTED_RULE_MATCHES, isPersistedRuleTool, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
8
+ import { ensureIndexes } from "./ensure-index.js";
8
9
  export function writerOfSqlRuleStore(store) {
9
10
  const w = Reflect.get(store, PERMISSION_RULE_WRITER);
10
11
  if (w === null || typeof w !== "object")
@@ -247,6 +248,13 @@ export async function assertPermissionRuleTicketSchema(query, dialect) {
247
248
  `Underlying probe error: ${err instanceof Error ? err.message : String(err)}`);
248
249
  }
249
250
  }
251
+ export const PERMISSION_RULE_INDEXES = [
252
+ { table: `${PERMISSION_RULE_APPROVAL_TABLE}`, name: "idx_permission_rule_approval_owner", columns: ["owner_key"] },
253
+ { table: `${PERMISSION_RULE_APPROVAL_TABLE}`, name: "idx_permission_rule_approval_sweep", columns: ["state", "created_at_ms"] },
254
+ { table: `${PERMISSION_RULE_TICKET_TABLE}`, name: "idx_permission_rule_ticket_owner", columns: ["owner_key"] },
255
+ { table: `${PERMISSION_RULE_TICKET_TABLE}`, name: "idx_permission_rule_ticket_approval", columns: ["approval_id"] },
256
+ { table: `${PERMISSION_RULE_TICKET_TABLE}`, name: "idx_permission_rule_ticket_expires", columns: ["expires_at_ms"] },
257
+ ];
250
258
  export async function ensurePgPermissionRuleSchema(q) {
251
259
  await q(`CREATE TABLE IF NOT EXISTS ${PERMISSION_RULE_TABLE} (
252
260
  owner_key VARCHAR(190) COLLATE "C" NOT NULL,
@@ -293,8 +301,6 @@ export async function ensurePgPermissionRuleSchema(q) {
293
301
  updated_at_ms BIGINT NOT NULL,
294
302
  PRIMARY KEY (record_id)
295
303
  )`);
296
- await q(`CREATE INDEX IF NOT EXISTS idx_permission_rule_approval_owner ON ${PERMISSION_RULE_APPROVAL_TABLE} (owner_key)`);
297
- await q(`CREATE INDEX IF NOT EXISTS idx_permission_rule_approval_sweep ON ${PERMISSION_RULE_APPROVAL_TABLE} (state, created_at_ms)`);
298
304
  await q(`CREATE TABLE IF NOT EXISTS ${PERMISSION_RULE_TICKET_TABLE} (
299
305
  ticket_id VARCHAR(190) COLLATE "C" NOT NULL,
300
306
  owner_key VARCHAR(190) COLLATE "C" NOT NULL,
@@ -315,9 +321,7 @@ export async function ensurePgPermissionRuleSchema(q) {
315
321
  created_at_ms BIGINT NOT NULL,
316
322
  PRIMARY KEY (ticket_id)
317
323
  )`);
318
- await q(`CREATE INDEX IF NOT EXISTS idx_permission_rule_ticket_owner ON ${PERMISSION_RULE_TICKET_TABLE} (owner_key)`);
319
- await q(`CREATE INDEX IF NOT EXISTS idx_permission_rule_ticket_approval ON ${PERMISSION_RULE_TICKET_TABLE} (approval_id)`);
320
- await q(`CREATE INDEX IF NOT EXISTS idx_permission_rule_ticket_expires ON ${PERMISSION_RULE_TICKET_TABLE} (expires_at_ms)`);
324
+ await ensureIndexes(q, "pg", PERMISSION_RULE_INDEXES);
321
325
  }
322
326
  export function buildRuleOwnerKey(owner) {
323
327
  const material = owner.kind === "principal" ? `principal:${owner.principal}` : "local-owner";
@@ -14,6 +14,7 @@
14
14
  import type { Pool, PoolClient } from "pg";
15
15
  import type { QuotaDecision, QuotaLimits, QuotaTracker } from "../observability/cost-quota.js";
16
16
  import { type CounterDegradeHook } from "./write-behind-counter.js";
17
+ import { type IndexSpec } from "./ensure-index.js";
17
18
  export declare class PgCostQuota implements QuotaTracker {
18
19
  private readonly counter;
19
20
  /** [ref]:与 TiDB 孪生逐字同语义(限额=比较参数;关断哨兵零 SQL;窗长换代=换桶)。 */
@@ -35,6 +36,8 @@ export declare class PgCostQuota implements QuotaTracker {
35
36
  * DATETIME(3)→TIMESTAMPTZ(3), inline `KEY idx_window`→separate CREATE INDEX). DISJOINT from other stores'
36
37
  * tables, so a self-contained ensureSchema is safe (the central pg-pool aggregation is done separately). */
37
38
  export declare const PG_COST_QUOTA_SCHEMA: string[];
39
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensureSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
40
+ export declare const COST_QUOTA_INDEXES: readonly IndexSpec[];
38
41
  /** Idempotent schema apply for the cost_quota table (the PG twin of tidb-pool.ts ensureSchema). */
39
42
  export declare function ensureSchema(pool: Pool | PoolClient): Promise<void>;
40
43
  //# sourceMappingURL=pg-cost-quota.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { PgWriteBehindCounter } from "./write-behind-counter.js";
2
+ import { ensureIndexes } from "./ensure-index.js";
2
3
  const COST_QUOTA_TABLE = { table: "cost_quota", keyCol: "scope_key", valCol: "micro_usd" };
3
4
  export class PgCostQuota {
4
5
  counter;
@@ -62,10 +63,13 @@ export const PG_COST_QUOTA_SCHEMA = [
62
63
  updated_at TIMESTAMPTZ(3) NOT NULL,
63
64
  PRIMARY KEY (scope_key, window_bucket)
64
65
  )`,
65
- `CREATE INDEX IF NOT EXISTS idx_cost_quota_window ON cost_quota (window_bucket)`,
66
+ ];
67
+ export const COST_QUOTA_INDEXES = [
68
+ { table: "cost_quota", name: "idx_cost_quota_window", columns: ["window_bucket"] },
66
69
  ];
67
70
  export async function ensureSchema(pool) {
68
71
  for (const stmt of PG_COST_QUOTA_SCHEMA)
69
72
  await pool.query(stmt);
73
+ await ensureIndexes((t, p) => pool.query(t, p), "pg", COST_QUOTA_INDEXES);
70
74
  }
71
75
  //# sourceMappingURL=pg-cost-quota.js.map
@@ -75,14 +75,35 @@ export declare function createPgPool(opts: string | pg.PoolConfig, onIdleClientE
75
75
  * one lock identity for the whole class (the one-time migration段 is the current user; it runs after ensure and
76
76
  * must not interleave with another replica's ensure). */
77
77
  export declare const ENSURE_SCHEMA_LOCK_KEY = 4906;
78
+ /**
79
+ * {@link ENSURE_SCHEMA_LOCK_KEY} 的**非阻塞**获取(轮询 + 抖动退避)—— MySQL 孪生
80
+ * `tidb-pool.ts acquireEnsureSchemaLock` 的逐条同形(循环共用 `ensure-schema-lock-policy.ts`)。轮到拿到为止;坏连接连续抛上抛
81
+ * (每条 DDL 本来就各自幂等),而不是把启动挂死。
82
+ *
83
+ * 🔴 **为什么不能再用阻塞的 `pg_advisory_lock()`**(S-287 复审 H1,对 7.82.1 的回归):本仓自 S-287 起在
84
+ * 锁窗内发 `CREATE INDEX CONCURRENTLY`,而 CIC 的末相要**等掉同库里所有 xmin 比它老的运行中后端**
85
+ * (`WaitForOlderSnapshots`)。一条卡在 `SELECT pg_advisory_lock($1)` 里的副本**正是**一条持着快照的运行中
86
+ * 查询 —— 它等锁、锁等 CIC、CIC 等它:三方互等。命中形是「全新 PG 库 + 多副本同时起」(表刚建完 ⇒ 索引
87
+ * 必缺 ⇒ 必走 CIC)。非阻塞轮询把等待挪到**库外**:每次尝试都是一条立刻返回的独立语句,轮询间隙进程
88
+ * 不持任何快照,CIC 因此永远等得到头。旧的普通 `CREATE INDEX` 不做 WaitForOlderSnapshots,所以这条病
89
+ * 是随 CIC 一起进来的,修也必须和它同批。
90
+ *
91
+ * ⚠️ 锁是 **session(连接)级**:调用方必须把锁与其后**全部**被串行化的语句跑在**同一条** client 上
92
+ * (走 `pool.query` 可能落到别的池连接,那把锁就静默作废了)。
93
+ */
94
+ export declare function acquirePgEnsureSchemaLock(client: pg.PoolClient, tag: string): Promise<void>;
95
+ /** {@link acquirePgEnsureSchemaLock} 的释放(尽力而为 —— 锁随连接归还自动释放)。 */
96
+ export declare function releasePgEnsureSchemaLock(client: pg.PoolClient, tag: string): Promise<void>;
78
97
  /** Idempotent central schema apply (the PG twin of tidb-pool.ts ensureSchema): the run/checkpoint tables here +
79
98
  * every PG store's own (disjoint) tables. Order is irrelevant (CREATE IF NOT EXISTS, no cross-table FKs).
80
99
  *
81
100
  * Concurrency: callers run this concurrently — tests fan ALL integration suites out in parallel (each beforeAll
82
101
  * calls ensurePgSchema), and in production every replica calls it at boot. Concurrent DDL on the SAME table makes
83
102
  * PG raise `tuple concurrently updated` in its catalog. We serialize the entire run behind a session-level
84
- * `pg_advisory_lock`: the first caller does the real CREATE/ALTER, the rest block until it unlocks then find every
85
- * statement already satisfied (IF NOT EXISTS ⇒ no-op), so there is no catalog race. The DDL semantics are
103
+ * advisory lock, acquired NON-BLOCKINGLY with polling ({@link acquirePgEnsureSchemaLock} — 阻塞形会与本函数
104
+ * 自 S-287 起发的 `CREATE INDEX CONCURRENTLY` 三方互等,理由写在那只函数的头注里): the first caller does
105
+ * the real CREATE/index work, the rest poll from OUTSIDE the database then find every statement already
106
+ * satisfied (IF NOT EXISTS ⇒ no-op), so there is no catalog race. The DDL semantics are
86
107
  * unchanged — only its timing is serialized. The advisory lock is SESSION (connection)-level, so the lock and
87
108
  * ALL the DDL MUST run on the one client we hold; routing DDL through `pool.query` could land on a different
88
109
  * pooled connection and silently void the lock. */
@@ -1,5 +1,6 @@
1
1
  import pg from "pg";
2
2
  import { recordFailOpen } from "../observability/fail-open.js";
3
+ import { pollUntilLocked } from "./ensure-schema-lock-policy.js";
3
4
  import { ensureSchema as ensureSessionSchema } from "./pg-session-storage.js";
4
5
  import { ensureSchema as ensureToolResultSchema } from "./tool-result-store-sql.js";
5
6
  import { ensureSchema as ensureCostQuotaSchema } from "./pg-cost-quota.js";
@@ -15,6 +16,8 @@ import { ensurePgAdoptionLogSchema } from "./adoption-log-sql.js";
15
16
  import { ensurePgPermissionRuleSchema } from "./permission-rule-store-sql.js";
16
17
  import { ensurePgLeaderRunSchema } from "./leader-run-store-sql.js";
17
18
  import { ensurePgRetentionSchema } from "./retention-store-sql.js";
19
+ import { ensureIndexes } from "./ensure-index.js";
20
+ import { CENTRAL_SCHEMA_INDEXES } from "./schema-indexes.js";
18
21
  export const PG_SCHEMA_STATEMENTS = [
19
22
  `CREATE TABLE IF NOT EXISTS task_run (
20
23
  task_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -43,10 +46,6 @@ export const PG_SCHEMA_STATEMENTS = [
43
46
  updated_at TIMESTAMPTZ(3) NOT NULL,
44
47
  PRIMARY KEY (task_id)
45
48
  )`,
46
- `CREATE INDEX IF NOT EXISTS idx_task_run_session_status ON task_run (session_id, status)`,
47
- `CREATE INDEX IF NOT EXISTS idx_task_run_owner ON task_run (owner)`,
48
- `CREATE INDEX IF NOT EXISTS idx_task_run_created ON task_run (created_at)`,
49
- `CREATE INDEX IF NOT EXISTS idx_task_run_job ON task_run (job_id)`,
50
49
  `CREATE TABLE IF NOT EXISTS task_event (
51
50
  task_id VARCHAR(64) COLLATE "C" NOT NULL,
52
51
  seq BIGINT NOT NULL,
@@ -61,7 +60,6 @@ export const PG_SCHEMA_STATEMENTS = [
61
60
  since TIMESTAMPTZ(3) NOT NULL,
62
61
  PRIMARY KEY (session_id)
63
62
  )`,
64
- `CREATE INDEX IF NOT EXISTS idx_task_active_task ON task_active (task_id)`,
65
63
  `CREATE TABLE IF NOT EXISTS checkpoint (
66
64
  token VARCHAR(120) COLLATE "C" NOT NULL,
67
65
  scope VARCHAR(190) COLLATE "C" NOT NULL,
@@ -101,8 +99,6 @@ export const PG_SCHEMA_STATEMENTS = [
101
99
  execution_at_ms BIGINT,
102
100
  PRIMARY KEY (token)
103
101
  )`,
104
- `CREATE INDEX IF NOT EXISTS idx_checkpoint_status_deadline ON checkpoint (status, deadline)`,
105
- `CREATE INDEX IF NOT EXISTS idx_checkpoint_session_status ON checkpoint (session_id, status)`,
106
102
  `CREATE TABLE IF NOT EXISTS checkpoint_ctx (
107
103
  session_id VARCHAR(190) COLLATE "C" NOT NULL,
108
104
  ctx JSONB NOT NULL,
@@ -117,7 +113,6 @@ export const PG_SCHEMA_STATEMENTS = [
117
113
  created_at TIMESTAMPTZ(3) NOT NULL,
118
114
  PRIMARY KEY (session_id, event_id)
119
115
  )`,
120
- `CREATE INDEX IF NOT EXISTS idx_resume_anchor_session ON resume_anchor (session_id)`,
121
116
  `CREATE TABLE IF NOT EXISTS approval_exemption (
122
117
  session_id VARCHAR(64) COLLATE "C" NOT NULL,
123
118
  tool_name VARCHAR(190) COLLATE "C" NOT NULL,
@@ -132,7 +127,6 @@ export const PG_SCHEMA_STATEMENTS = [
132
127
  expires_at_ms BIGINT NOT NULL,
133
128
  PRIMARY KEY (session_id, nonce)
134
129
  )`,
135
- `CREATE INDEX IF NOT EXISTS idx_approval_nonce_expires ON approval_nonce (expires_at_ms)`,
136
130
  `CREATE TABLE IF NOT EXISTS sendfile_link (
137
131
  id VARCHAR(64) COLLATE "C" NOT NULL,
138
132
  scope VARCHAR(190) COLLATE "C" NOT NULL,
@@ -147,7 +141,6 @@ export const PG_SCHEMA_STATEMENTS = [
147
141
  created_at TIMESTAMPTZ(3) NOT NULL,
148
142
  PRIMARY KEY (id)
149
143
  )`,
150
- `CREATE INDEX IF NOT EXISTS idx_sendfile_link_scope ON sendfile_link (scope, id)`,
151
144
  `CREATE TABLE IF NOT EXISTS session_policy (
152
145
  policy_key CHAR(64) COLLATE "C" NOT NULL,
153
146
  session_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -161,7 +154,6 @@ export const PG_SCHEMA_STATEMENTS = [
161
154
  updated_at TIMESTAMPTZ(3) NOT NULL,
162
155
  PRIMARY KEY (policy_key)
163
156
  )`,
164
- `CREATE INDEX IF NOT EXISTS idx_session_policy_session ON session_policy (session_id)`,
165
157
  `CREATE TABLE IF NOT EXISTS snapshot_blob (
166
158
  blob_hash CHAR(64) COLLATE "C" PRIMARY KEY,
167
159
  byte_len BIGINT NOT NULL,
@@ -174,7 +166,6 @@ export const PG_SCHEMA_STATEMENTS = [
174
166
  bytes BYTEA NULL,
175
167
  created_at TIMESTAMPTZ(3) NOT NULL
176
168
  )`,
177
- `CREATE INDEX IF NOT EXISTS idx_snapshot_blob_created ON snapshot_blob (created_at)`,
178
169
  `CREATE TABLE IF NOT EXISTS file_history_scope (
179
170
  scope VARCHAR(64) COLLATE "C" NOT NULL,
180
171
  created_at_ms BIGINT NOT NULL,
@@ -201,7 +192,6 @@ export const PG_SCHEMA_STATEMENTS = [
201
192
  backup_time VARCHAR(40) COLLATE "C" NOT NULL,
202
193
  PRIMARY KEY (scope, key_hash, version)
203
194
  )`,
204
- `CREATE INDEX IF NOT EXISTS idx_file_history_version_blob ON file_history_version (blob_hash)`,
205
195
  `CREATE TABLE IF NOT EXISTS file_history_boundary (
206
196
  scope VARCHAR(64) COLLATE "C" NOT NULL,
207
197
  entry_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -217,7 +207,6 @@ export const PG_SCHEMA_STATEMENTS = [
217
207
  version INT NOT NULL,
218
208
  PRIMARY KEY (scope, entry_id, key_hash)
219
209
  )`,
220
- `CREATE INDEX IF NOT EXISTS idx_file_history_bf_key ON file_history_boundary_file (scope, key_hash)`,
221
210
  `CREATE TABLE IF NOT EXISTS file_history_reaped_entry (
222
211
  scope VARCHAR(64) COLLATE "C" NOT NULL,
223
212
  entry_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -231,7 +220,6 @@ export const PG_SCHEMA_STATEMENTS = [
231
220
  bytes BYTEA NULL,
232
221
  created_at TIMESTAMPTZ(3) NOT NULL
233
222
  )`,
234
- `CREATE INDEX IF NOT EXISTS idx_file_history_blob_created ON file_history_blob (created_at)`,
235
223
  `CREATE TABLE IF NOT EXISTS workflow_journal (
236
224
  run_id VARCHAR(191) COLLATE "C" NOT NULL,
237
225
  ordinal INT NOT NULL,
@@ -263,8 +251,6 @@ export const PG_SCHEMA_STATEMENTS = [
263
251
  ended_at_ms BIGINT,
264
252
  PRIMARY KEY (id)
265
253
  )`,
266
- `CREATE INDEX IF NOT EXISTS idx_wfrun_scope_created ON workflow_run (scope, created_at_ms)`,
267
- `CREATE INDEX IF NOT EXISTS idx_wfrun_scope_status ON workflow_run (scope, status)`,
268
254
  `CREATE TABLE IF NOT EXISTS workflow_agent_session (
269
255
  session_id VARCHAR(190) COLLATE "C" NOT NULL,
270
256
  run_id VARCHAR(191) COLLATE "C" NOT NULL,
@@ -272,7 +258,6 @@ export const PG_SCHEMA_STATEMENTS = [
272
258
  scope VARCHAR(190) COLLATE "C" NOT NULL,
273
259
  PRIMARY KEY (session_id)
274
260
  )`,
275
- `CREATE INDEX IF NOT EXISTS idx_wfagentsess_run ON workflow_agent_session (run_id)`,
276
261
  `CREATE TABLE IF NOT EXISTS workflow_completion_inbox (
277
262
  seq BIGSERIAL,
278
263
  session_id VARCHAR(190) COLLATE "C" NOT NULL,
@@ -295,7 +280,6 @@ export const PG_SCHEMA_STATEMENTS = [
295
280
  -- becomes MACHINE-ENFORCED rather than merely conventional, in both engines.
296
281
  CONSTRAINT uq_wfinbox_seq UNIQUE (seq)
297
282
  )`,
298
- `CREATE INDEX IF NOT EXISTS idx_wfinbox_session_seq ON workflow_completion_inbox (session_id, seq)`,
299
283
  `CREATE TABLE IF NOT EXISTS workflow_inbox_fence (
300
284
  session_id VARCHAR(190) COLLATE "C" NOT NULL,
301
285
  kind VARCHAR(8) COLLATE "C" NOT NULL,
@@ -305,7 +289,6 @@ export const PG_SCHEMA_STATEMENTS = [
305
289
  until_ms BIGINT NOT NULL,
306
290
  PRIMARY KEY (session_id, kind, run_id)
307
291
  )`,
308
- `CREATE INDEX IF NOT EXISTS idx_wffence_until ON workflow_inbox_fence (until_ms)`,
309
292
  `CREATE TABLE IF NOT EXISTS workflow_notify_journal (
310
293
  run_id VARCHAR(191) COLLATE "C" NOT NULL,
311
294
  scope VARCHAR(190) COLLATE "C" NOT NULL,
@@ -316,7 +299,6 @@ export const PG_SCHEMA_STATEMENTS = [
316
299
  acked_at_ms BIGINT,
317
300
  PRIMARY KEY (run_id)
318
301
  )`,
319
- `CREATE INDEX IF NOT EXISTS idx_wfnotify_acked ON workflow_notify_journal (acked)`,
320
302
  `CREATE TABLE IF NOT EXISTS shared_memory_scope (
321
303
  scope_key VARCHAR(190) COLLATE "C" NOT NULL,
322
304
  state VARCHAR(16) COLLATE "C" NOT NULL,
@@ -332,7 +314,6 @@ export const PG_SCHEMA_STATEMENTS = [
332
314
  updated_at_ms BIGINT NOT NULL,
333
315
  PRIMARY KEY (store_id)
334
316
  )`,
335
- `CREATE INDEX IF NOT EXISTS idx_shared_memory_store_scope ON shared_memory_store (scope_key, store_id)`,
336
317
  `CREATE TABLE IF NOT EXISTS shared_memory_entry (
337
318
  store_id VARCHAR(64) COLLATE "C" NOT NULL,
338
319
  path VARCHAR(512) COLLATE "C" NOT NULL,
@@ -400,17 +381,26 @@ export function createPgPool(opts, onIdleClientError) {
400
381
  return pool;
401
382
  }
402
383
  export const ENSURE_SCHEMA_LOCK_KEY = 4906;
384
+ export async function acquirePgEnsureSchemaLock(client, tag) {
385
+ await pollUntilLocked(async () => {
386
+ const res = await client.query("SELECT pg_try_advisory_lock($1) AS got", [ENSURE_SCHEMA_LOCK_KEY]);
387
+ return res.rows[0]?.got === true;
388
+ }, { tag, label: `pg advisory lock ${ENSURE_SCHEMA_LOCK_KEY}` });
389
+ }
390
+ export async function releasePgEnsureSchemaLock(client, tag) {
391
+ try {
392
+ await client.query("SELECT pg_advisory_unlock($1)", [ENSURE_SCHEMA_LOCK_KEY]);
393
+ }
394
+ catch (err) {
395
+ console.warn(`[${tag}] pg_advisory_unlock(${ENSURE_SCHEMA_LOCK_KEY}) failed (lock auto-frees on connection close):`, err);
396
+ }
397
+ }
403
398
  export async function ensurePgSchema(pool) {
404
399
  const client = await pool.connect();
405
400
  let locked = false;
406
401
  try {
407
- try {
408
- await client.query("SELECT pg_advisory_lock($1)", [ENSURE_SCHEMA_LOCK_KEY]);
409
- locked = true;
410
- }
411
- catch (err) {
412
- console.warn(`[ensurePgSchema] pg_advisory_lock(${ENSURE_SCHEMA_LOCK_KEY}) failed; running DDL unserialized:`, err);
413
- }
402
+ await acquirePgEnsureSchemaLock(client, "ensurePgSchema");
403
+ locked = true;
414
404
  for (const stmt of PG_SCHEMA_STATEMENTS)
415
405
  await client.query(stmt);
416
406
  await ensureSessionSchema(client);
@@ -428,16 +418,11 @@ export async function ensurePgSchema(pool) {
428
418
  await ensurePgPermissionRuleSchema((text, params) => client.query(text, params));
429
419
  await ensurePgLeaderRunSchema((text, params) => client.query(text, params));
430
420
  await ensurePgRetentionSchema((text, params) => client.query(text, params));
421
+ await ensureIndexes((text, params) => client.query(text, params), "pg", CENTRAL_SCHEMA_INDEXES);
431
422
  }
432
423
  finally {
433
- if (locked) {
434
- try {
435
- await client.query("SELECT pg_advisory_unlock($1)", [ENSURE_SCHEMA_LOCK_KEY]);
436
- }
437
- catch (err) {
438
- console.warn(`[ensurePgSchema] pg_advisory_unlock(${ENSURE_SCHEMA_LOCK_KEY}) failed (lock auto-frees on connection close):`, err);
439
- }
440
- }
424
+ if (locked)
425
+ await releasePgEnsureSchemaLock(client, "ensurePgSchema");
441
426
  client.release();
442
427
  }
443
428
  }
@@ -16,6 +16,7 @@
16
16
  import type { Pool, PoolClient } from "pg";
17
17
  import type { RateDecision, RateGate, RateLimits } from "../observability/rate-limit.js";
18
18
  import { type CounterDegradeHook } from "./write-behind-counter.js";
19
+ import { type IndexSpec } from "./ensure-index.js";
19
20
  export declare class PgRateLimiter implements RateGate {
20
21
  private readonly counter;
21
22
  /** [ref]:与 TiDB 孪生逐字同语义(限额=比较参数;关断哨兵停/起刷新环)。 */
@@ -39,6 +40,8 @@ export declare class PgRateLimiter implements RateGate {
39
40
  * atomic ON CONFLICT increment.
40
41
  */
41
42
  export declare const PG_RATE_LIMITER_SCHEMA: string[];
43
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensureSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
44
+ export declare const RATE_LIMIT_INDEXES: readonly IndexSpec[];
42
45
  /** Self-contained schema bootstrap for tests (the rate_limit table is disjoint from other stores). */
43
46
  export declare function ensureSchema(pool: Pool | PoolClient): Promise<void>;
44
47
  //# sourceMappingURL=pg-rate-limiter.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { PgWriteBehindCounter } from "./write-behind-counter.js";
2
+ import { ensureIndexes } from "./ensure-index.js";
2
3
  const RATE_LIMIT_TABLE = { table: "rate_limit", keyCol: "limit_key", valCol: "hits" };
3
4
  export class PgRateLimiter {
4
5
  counter;
@@ -57,10 +58,13 @@ export const PG_RATE_LIMITER_SCHEMA = [
57
58
  updated_at TIMESTAMPTZ(3) NOT NULL,
58
59
  PRIMARY KEY (limit_key, window_bucket)
59
60
  )`,
60
- `CREATE INDEX IF NOT EXISTS idx_rate_limit_window ON rate_limit (window_bucket)`,
61
+ ];
62
+ export const RATE_LIMIT_INDEXES = [
63
+ { table: "rate_limit", name: "idx_rate_limit_window", columns: ["window_bucket"] },
61
64
  ];
62
65
  export async function ensureSchema(pool) {
63
66
  for (const stmt of PG_RATE_LIMITER_SCHEMA)
64
67
  await pool.query(stmt);
68
+ await ensureIndexes((t, p) => pool.query(t, p), "pg", RATE_LIMIT_INDEXES);
65
69
  }
66
70
  //# sourceMappingURL=pg-rate-limiter.js.map
@@ -3,6 +3,7 @@ import type { Pool, PoolClient } from "pg";
3
3
  import type { SessionSummary } from "./store-contracts.js";
4
4
  import { type PlacementAcquireOpts } from "./session-placement.js";
5
5
  import { type StagingHandle } from "../session-sync-kernel.js";
6
+ import { type IndexSpec } from "./ensure-index.js";
6
7
  /**
7
8
  * PG-dialect DDL for the L1 session log, translated from tidb-pool.ts SCHEMA_STATEMENTS:
8
9
  * JSON→JSONB, DATETIME(3)→TIMESTAMPTZ(3), inline `KEY`/`UNIQUE KEY`→separate CREATE INDEX (UNIQUE for uq_session_event_entry).
@@ -11,6 +12,8 @@ import { type StagingHandle } from "../session-sync-kernel.js";
11
12
  * drop-and-recreate, and NO new `ALTER TABLE` seams are added here; fold into the CREATE instead.
12
13
  */
13
14
  export declare const PG_SESSION_SCHEMA: string[];
15
+ /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensureSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
16
+ export declare const SESSION_INDEXES: readonly IndexSpec[];
14
17
  /** Idempotent schema apply for this store's tables (the test calls this; production aggregation is separate). */
15
18
  export declare function ensureSchema(pool: Pool | PoolClient): Promise<void>;
16
19
  /**