@sema-agent/server 7.82.1 → 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.
- package/dist/bench/s1/arms.d.ts +4 -4
- package/dist/bench/s1/arms.js +1 -2
- package/dist/bench/s1/live-deps.js +0 -1
- package/dist/bench/s1/runner-ctx.d.ts +3 -3
- package/dist/bench/s1/tasks.d.ts +8 -4
- package/dist/config-catalog.js +0 -1
- package/dist/config.js +1 -0
- package/dist/leader/endpoint.js +2 -0
- package/dist/leader/leader.d.ts +33 -6
- package/dist/leader/leader.js +54 -45
- package/dist/leader/merge.d.ts +38 -29
- package/dist/leader/merge.js +45 -38
- package/dist/leader/wire.d.ts +2 -17
- package/dist/leader/wire.js +5 -58
- package/dist/observability/fail-open.d.ts +5 -5
- package/dist/observability/fail-open.js +5 -2
- package/dist/observability/run-terminal-log.d.ts +0 -7
- package/dist/observability/run-terminal-log.js +9 -20
- package/dist/plugins/adoption-log-sql.d.ts +3 -0
- package/dist/plugins/adoption-log-sql.js +5 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +3 -0
- package/dist/plugins/approval-ask-store-sql.js +9 -5
- package/dist/plugins/background-agent-store-sql.d.ts +3 -0
- package/dist/plugins/background-agent-store-sql.js +7 -2
- package/dist/plugins/device-store-sql.d.ts +3 -0
- package/dist/plugins/device-store-sql.js +9 -4
- package/dist/plugins/ensure-index.d.ts +42 -0
- package/dist/plugins/ensure-index.js +81 -0
- package/dist/plugins/ensure-schema-lock-policy.d.ts +29 -0
- package/dist/plugins/ensure-schema-lock-policy.js +31 -0
- package/dist/plugins/image-bake-store-sql.d.ts +3 -0
- package/dist/plugins/image-bake-store-sql.js +6 -2
- package/dist/plugins/image-index-sql.d.ts +3 -0
- package/dist/plugins/image-index-sql.js +8 -4
- package/dist/plugins/leader-run-store-sql.d.ts +3 -0
- package/dist/plugins/leader-run-store-sql.js +6 -2
- package/dist/plugins/mailbox-store-sql.d.ts +3 -0
- package/dist/plugins/mailbox-store-sql.js +6 -1
- package/dist/plugins/memory-engine-pg.d.ts +4 -0
- package/dist/plugins/memory-engine-pg.js +6 -6
- package/dist/plugins/memory-engine-tidb.js +5 -0
- package/dist/plugins/memory-sync-store-pg.d.ts +5 -0
- package/dist/plugins/memory-sync-store-pg.js +9 -2
- package/dist/plugins/memory-sync-store-tidb.js +6 -0
- package/dist/plugins/one-time-migrations.d.ts +1 -1
- package/dist/plugins/one-time-migrations.js +7 -14
- package/dist/plugins/outcome-ledger-sql.d.ts +3 -0
- package/dist/plugins/outcome-ledger-sql.js +7 -3
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +9 -5
- package/dist/plugins/pg-cost-quota.d.ts +3 -0
- package/dist/plugins/pg-cost-quota.js +5 -1
- package/dist/plugins/pg-pool.d.ts +23 -2
- package/dist/plugins/pg-pool.js +22 -37
- package/dist/plugins/pg-rate-limiter.d.ts +3 -0
- package/dist/plugins/pg-rate-limiter.js +5 -1
- package/dist/plugins/pg-session-storage.d.ts +3 -0
- package/dist/plugins/pg-session-storage.js +8 -4
- package/dist/plugins/retention-store-sql.d.ts +3 -0
- package/dist/plugins/retention-store-sql.js +7 -3
- package/dist/plugins/roster-store-sql.d.ts +3 -0
- package/dist/plugins/roster-store-sql.js +6 -1
- package/dist/plugins/schema-indexes.d.ts +21 -0
- package/dist/plugins/schema-indexes.js +25 -0
- package/dist/plugins/task-attachment-store.d.ts +3 -0
- package/dist/plugins/task-attachment-store.js +7 -2
- package/dist/plugins/tidb-pool.d.ts +15 -3
- package/dist/plugins/tidb-pool.js +38 -15
- package/dist/plugins/tool-result-store-sql.d.ts +3 -0
- package/dist/plugins/tool-result-store-sql.js +5 -1
- package/dist/trace/project.d.ts +2 -1
- package/dist/trace/redact.d.ts +6 -0
- package/package.json +1 -1
|
@@ -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) {
|
|
@@ -2,11 +2,14 @@ import type { Pool as MySqlPool } from "mysql2/promise";
|
|
|
2
2
|
import type { Pool as PgPool, PoolClient } from "pg";
|
|
3
3
|
import { type ImageStatus, type ImageIndexEntry, type ImageIndexUpsert, type ImageViewer, type ImageListFilter } from "./store-contracts.js";
|
|
4
4
|
import { type SqlDriver } from "./sql-driver.js";
|
|
5
|
+
import { type IndexSpec } from "./ensure-index.js";
|
|
5
6
|
export type { ImageNestedBuildMode, ImageCapabilities, ImagePodContract, ImageStatus, ImageVisibility, ImageIndexEntry, ImageIndexUpsert, ImageViewer, ImageListFilter, ImageRow, } from "./store-contracts.js";
|
|
6
7
|
/** PG translation of the `sandbox_image_index` DDL in tidb-pool.ts (JSON→JSONB, DATETIME(3)→TIMESTAMPTZ(3),
|
|
7
8
|
* TINYINT(1)→SMALLINT(布尔列 PG 映射归一,clay 批 2026-07-26 —— 两方言读回同为 number,写入 0/1,
|
|
8
9
|
* 读侧 `!!` 保持,出口 JS 类型不变), inline UNIQUE KEY/KEY → separate CREATE UNIQUE INDEX / CREATE INDEX). */
|
|
9
10
|
export declare const PG_IMAGE_INDEX_SCHEMA: string[];
|
|
11
|
+
/** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensureSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
|
|
12
|
+
export declare const IMAGE_INDEX_INDEXES: readonly IndexSpec[];
|
|
10
13
|
/** Idempotent self-contained schema apply (for the integration test; central aggregation is done separately). */
|
|
11
14
|
export declare function ensureSchema(pool: PgPool | PoolClient): Promise<void>;
|
|
12
15
|
/** Dual-dialect INDEX store. See the file header for the dialect-delta ledger. */
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { pgSafeJsonStringify, pgSanitizeText, pgHasUnstorable, PgUnstorableError } from "./pg-safe-json.js";
|
|
3
3
|
import { mapImageRow as mapRow, } from "./store-contracts.js";
|
|
4
4
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
5
|
+
import { ensureIndexes } from "./ensure-index.js";
|
|
5
6
|
export const PG_IMAGE_INDEX_SCHEMA = [
|
|
6
7
|
`CREATE TABLE IF NOT EXISTS sandbox_image_index (
|
|
7
8
|
id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
@@ -27,14 +28,17 @@ export const PG_IMAGE_INDEX_SCHEMA = [
|
|
|
27
28
|
updated_at TIMESTAMPTZ(3) NOT NULL,
|
|
28
29
|
PRIMARY KEY (id)
|
|
29
30
|
)`,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
];
|
|
32
|
+
export const IMAGE_INDEX_INDEXES = [
|
|
33
|
+
{ table: "sandbox_image_index", name: "uq_repo_digest", columns: ["repo", "digest"], unique: true },
|
|
34
|
+
{ table: "sandbox_image_index", name: "idx_sandbox_image_index_profile_status", columns: ["profile", "status", "visibility"] },
|
|
35
|
+
{ table: "sandbox_image_index", name: "idx_sandbox_image_index_digest", columns: ["digest"] },
|
|
36
|
+
{ table: "sandbox_image_index", name: "idx_sandbox_image_index_tenant", columns: ["tenant_id", "status"] },
|
|
34
37
|
];
|
|
35
38
|
export async function ensureSchema(pool) {
|
|
36
39
|
for (const stmt of PG_IMAGE_INDEX_SCHEMA)
|
|
37
40
|
await pool.query(stmt);
|
|
41
|
+
await ensureIndexes((t, p) => pool.query(t, p), "pg", IMAGE_INDEX_INDEXES);
|
|
38
42
|
}
|
|
39
43
|
const MAX_LIMIT = 200;
|
|
40
44
|
const DEFAULT_LIMIT = 50;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
3
|
import { type SqlDriver } from "./sql-driver.js";
|
|
4
|
+
import { type IndexSpec } from "./ensure-index.js";
|
|
4
5
|
/** 表名([ref] 单数纪律;`schema-naming-invariants` 的闭集词表执法)。 */
|
|
5
6
|
export declare const LEADER_RUN_TABLE = "leader_run";
|
|
6
7
|
/** leader run 的四态 —— 与 `leader/endpoint.ts` 的 `LeaderRun["status"]` 同词表(闭集)。 */
|
|
@@ -47,6 +48,8 @@ export declare function leaderIdemKey(raw: string, owner: string | null): string
|
|
|
47
48
|
export declare const TIDB_LEADER_RUN_STATEMENTS: readonly string[];
|
|
48
49
|
/** PG 方言的建表语句(MySQL 孪生的逐条翻译:JSON→JSONB,逐列 `COLLATE "C"`,内联 KEY→独立 CREATE INDEX)。 */
|
|
49
50
|
export declare const PG_LEADER_RUN_SCHEMA: readonly string[];
|
|
51
|
+
/** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgLeaderRunSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
|
|
52
|
+
export declare const LEADER_RUN_INDEXES: readonly IndexSpec[];
|
|
50
53
|
/** 幂等的 PG schema 应用(中央 `ensurePgSchema` 组合它;集成套件也直接调)。 */
|
|
51
54
|
export declare function ensurePgLeaderRunSchema(q: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
|
|
52
55
|
/** 清算腿写进 `error` 列的 WHY(消费方读到的就是这句;不是一个空的 failed)。 */
|
|
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { isDupKeyError } from "./sql-errors.js";
|
|
4
4
|
import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
|
|
5
5
|
import { parseJsonOr } from "./sql-row-helpers.js";
|
|
6
|
+
import { ensureIndexes } from "./ensure-index.js";
|
|
6
7
|
export const LEADER_RUN_TABLE = "leader_run";
|
|
7
8
|
const LEADER_RUN_STATUSES = new Set(["running", "completed", "failed", "needs_human"]);
|
|
8
9
|
export const LEADER_OBJECTIVE_PREVIEW_CHARS = 160;
|
|
@@ -46,12 +47,15 @@ export const PG_LEADER_RUN_SCHEMA = [
|
|
|
46
47
|
PRIMARY KEY (leader_run_id),
|
|
47
48
|
CONSTRAINT uq_leader_run_idem UNIQUE (idem_key)
|
|
48
49
|
)`,
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
];
|
|
51
|
+
export const LEADER_RUN_INDEXES = [
|
|
52
|
+
{ table: `${LEADER_RUN_TABLE}`, name: "idx_leader_run_owner", columns: ["owner"] },
|
|
53
|
+
{ table: `${LEADER_RUN_TABLE}`, name: "idx_leader_run_started", columns: ["started_at_ms"] },
|
|
51
54
|
];
|
|
52
55
|
export async function ensurePgLeaderRunSchema(q) {
|
|
53
56
|
for (const stmt of PG_LEADER_RUN_SCHEMA)
|
|
54
57
|
await q(stmt);
|
|
58
|
+
await ensureIndexes(q, "pg", LEADER_RUN_INDEXES);
|
|
55
59
|
}
|
|
56
60
|
export const STALE_SWEEP_REASON = "leader run went stale: no replica settled it within LEADER_RUN_STALE_MS (the replica driving it most likely died). " +
|
|
57
61
|
"The run was NOT taken over — re-submit with a NEW Idempotency-Key to run it again.";
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
46
46
|
import type { Pool as PgPool } from "pg";
|
|
47
47
|
import type { MailboxAppendMessage, MailboxLease, MailboxStore } from "@sema-agent/core";
|
|
48
|
+
import { type IndexSpec } from "./ensure-index.js";
|
|
48
49
|
export declare const MAILBOX_TABLE = "mailbox";
|
|
49
50
|
export declare const MAILBOX_MSG_TABLE = "mailbox_message";
|
|
50
51
|
/** 店侧日志座(可选;boot 注入 `Logger`)。7.58.0 合并重扫 [medium]:坏 `peer_meta` 行的 fail-closed 抛必须**响亮**——core 唯一的
|
|
@@ -54,6 +55,8 @@ export interface MailboxStoreLogger {
|
|
|
54
55
|
warn(event: string, fields: Record<string, unknown>): void;
|
|
55
56
|
}
|
|
56
57
|
export declare function ensureTiDBMailboxSchema(pool: MySqlPool): Promise<void>;
|
|
58
|
+
/** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgMailboxSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */
|
|
59
|
+
export declare const MAILBOX_INDEXES: readonly IndexSpec[];
|
|
57
60
|
export declare function ensurePgMailboxSchema(q: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
|
|
58
61
|
export declare class TiDBMailboxStore implements MailboxStore {
|
|
59
62
|
private readonly pool;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MAILBOX_INVALID_PEER_META_CODE, MailboxStoreError, readMailboxPeerMeta } from "@sema-agent/core";
|
|
2
2
|
import { pgHasUnstorable, pgSanitizeText, PgUnstorableError } from "./pg-safe-json.js";
|
|
3
|
+
import { ensureIndexes } from "./ensure-index.js";
|
|
3
4
|
export const MAILBOX_TABLE = "mailbox";
|
|
4
5
|
export const MAILBOX_MSG_TABLE = "mailbox_message";
|
|
5
6
|
function assertScope(scope) {
|
|
@@ -131,7 +132,11 @@ export async function ensureTiDBMailboxSchema(pool) {
|
|
|
131
132
|
PRIMARY KEY (scope_key, handle, seq),
|
|
132
133
|
KEY idx_mbm_sent (scope_key, handle, sent_at_ms)
|
|
133
134
|
) COLLATE utf8mb4_bin`);
|
|
135
|
+
await ensureIndexes((text, params) => pool.query(text, params), "mysql", MAILBOX_INDEXES);
|
|
134
136
|
}
|
|
137
|
+
export const MAILBOX_INDEXES = [
|
|
138
|
+
{ table: `${MAILBOX_MSG_TABLE}`, name: "idx_mbm_sent", columns: ["scope_key", "handle", "sent_at_ms"] },
|
|
139
|
+
];
|
|
135
140
|
export async function ensurePgMailboxSchema(q) {
|
|
136
141
|
await q(`CREATE TABLE IF NOT EXISTS ${MAILBOX_TABLE} (
|
|
137
142
|
scope_key VARCHAR(190) COLLATE "C" NOT NULL,
|
|
@@ -154,7 +159,7 @@ export async function ensurePgMailboxSchema(q) {
|
|
|
154
159
|
peer_meta TEXT COLLATE "C", -- S-90 core fc52b4d4 design/385 片2b(TiDB twin 同注:typed peer 记录 JSON;NULL = 缺席且有语义)
|
|
155
160
|
PRIMARY KEY (scope_key, handle, seq)
|
|
156
161
|
)`);
|
|
157
|
-
await q
|
|
162
|
+
await ensureIndexes(q, "pg", MAILBOX_INDEXES);
|
|
158
163
|
}
|
|
159
164
|
export class TiDBMailboxStore {
|
|
160
165
|
pool;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { MemoryHistoryAppendRow } from "./memory-sync-store-pg.js";
|
|
2
2
|
import type { PgEmbedder, PgQueryFn } from "./pg-query.js";
|
|
3
3
|
import type { MemoryBackend, MemoryEntry, MemoryEntryHeader, NotePatch, PatchReport, ScoredMemoryEntry } from "@sema-agent/core";
|
|
4
|
+
import { type IndexSpec } from "./ensure-index.js";
|
|
4
5
|
/** Table names (single source). Deliberately DISJOINT from the legacy `agent_memory*` tables —
|
|
5
6
|
* the retired MemoryStore plane and this entry plane must never cross-write. */
|
|
6
7
|
export declare const PG_MEMORY_ENGINE_TABLES: {
|
|
@@ -16,6 +17,9 @@ export interface PgMemoryEngineSchemaOptions {
|
|
|
16
17
|
pgvector?: boolean;
|
|
17
18
|
};
|
|
18
19
|
}
|
|
20
|
+
/** S-287:记忆引擎 PG 面的索引**声明**(MySQL 孪生是 `memory-engine-tidb.ts` 的内联 `KEY`/`UNIQUE KEY`,
|
|
21
|
+
* 由 `tidb-pool.ts` 的中央 `ensureSchema` 用同一份声明补存量库)。 */
|
|
22
|
+
export declare const MEMORY_ENGINE_INDEXES: readonly IndexSpec[];
|
|
19
23
|
/** Idempotent DDL for the entry plane. Call once at startup (or run through your migration tool). */
|
|
20
24
|
export declare function ensurePgMemoryEngineSchema(query: PgQueryFn, opts?: PgMemoryEngineSchemaOptions): Promise<void>;
|
|
21
25
|
/** Options for {@link PgMemoryEngineBackend}. */
|
|
@@ -4,10 +4,15 @@ import { computeEntryRev, serializeEntryFile } from "@sema-agent/core";
|
|
|
4
4
|
import { pgSafeJsonStringify, pgHasUnstorable } from "./pg-safe-json.js";
|
|
5
5
|
import { assertMemoryScopeWidth, memoryEntryKeyWidthRefusal } from "./memory-key-guards.js";
|
|
6
6
|
import { ambiguousOriginRefusal, createBatchCommittedBaseline, exposureBandOf, exposureCarriage, originMarkedSqlPredicate, originWhitewashRefusal, rememberBatchCommitted, distilledWhitewashRefusal, distilledCarriage, unknownPatchOpRefusal, } from "./memory-origin-law.js";
|
|
7
|
+
import { ensureIndexes } from "./ensure-index.js";
|
|
7
8
|
export const PG_MEMORY_ENGINE_TABLES = {
|
|
8
9
|
entry: "agent_memory_engine_entry",
|
|
9
10
|
cursor: "agent_memory_engine_cursor",
|
|
10
11
|
};
|
|
12
|
+
export const MEMORY_ENGINE_INDEXES = [
|
|
13
|
+
{ table: `${PG_MEMORY_ENGINE_TABLES.entry}`, name: `idx_${PG_MEMORY_ENGINE_TABLES.entry}_scope`, columns: ["scope"] },
|
|
14
|
+
{ table: `${PG_MEMORY_ENGINE_TABLES.entry}`, name: `uq_${PG_MEMORY_ENGINE_TABLES.entry}_scope_slug`, columns: ["scope", "slug"], unique: true },
|
|
15
|
+
];
|
|
11
16
|
export async function ensurePgMemoryEngineSchema(query, opts = {}) {
|
|
12
17
|
const vec = opts.memoryVector;
|
|
13
18
|
if (vec !== undefined && (!Number.isInteger(vec.dimensions) || vec.dimensions < 1)) {
|
|
@@ -27,16 +32,11 @@ export async function ensurePgMemoryEngineSchema(query, opts = {}) {
|
|
|
27
32
|
size_bytes integer NOT NULL,
|
|
28
33
|
embedding ${embeddingCol}
|
|
29
34
|
)`);
|
|
30
|
-
try {
|
|
31
|
-
await query(`CREATE INDEX IF NOT EXISTS idx_${PG_MEMORY_ENGINE_TABLES.entry}_scope ON ${PG_MEMORY_ENGINE_TABLES.entry} (scope)`);
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
}
|
|
35
|
-
await query(`CREATE UNIQUE INDEX IF NOT EXISTS uq_${PG_MEMORY_ENGINE_TABLES.entry}_scope_slug ON ${PG_MEMORY_ENGINE_TABLES.entry} (scope, slug)`);
|
|
36
35
|
await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_ENGINE_TABLES.cursor} (
|
|
37
36
|
scope varchar(190) COLLATE "C" PRIMARY KEY,
|
|
38
37
|
cursor text COLLATE "C" NOT NULL
|
|
39
38
|
)`);
|
|
39
|
+
await ensureIndexes(query, "pg", MEMORY_ENGINE_INDEXES);
|
|
40
40
|
}
|
|
41
41
|
function vectorLiteral(v) {
|
|
42
42
|
return `[${v.join(",")}]`;
|
|
@@ -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
|
|
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
|
|
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 `
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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*`)。
|