@sema-agent/server 7.7.1 → 7.8.1
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/approval-card.d.ts +2 -2
- package/dist/approval-card.js +2 -2
- package/dist/approval-deny-reasons.d.ts +1 -1
- package/dist/approval-deny-reasons.js +1 -1
- package/dist/approval-reconciler.js +2 -2
- package/dist/config-types.d.ts +1 -1
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/server.js +6 -1
- package/dist/plugins/approval-ask-store-memory.d.ts +1 -1
- package/dist/plugins/approval-ask-store-memory.js +17 -17
- package/dist/plugins/approval-ask-store-sql.d.ts +12 -12
- package/dist/plugins/approval-ask-store-sql.js +58 -58
- package/dist/plugins/background-agent-store-sql.d.ts +1 -1
- package/dist/plugins/background-agent-store-sql.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
- package/dist/plugins/checkpoint-store-sql.js +6 -6
- package/dist/plugins/mailbox-store-sql.d.ts +2 -2
- package/dist/plugins/mailbox-store-sql.js +2 -2
- package/dist/plugins/memory-engine-pg.d.ts +2 -2
- package/dist/plugins/memory-engine-pg.js +17 -17
- package/dist/plugins/memory-engine-tidb.d.ts +2 -2
- package/dist/plugins/memory-engine-tidb.js +16 -16
- package/dist/plugins/memory-sync-store-pg.d.ts +2 -2
- package/dist/plugins/memory-sync-store-pg.js +19 -19
- package/dist/plugins/memory-sync-store-tidb.d.ts +1 -1
- package/dist/plugins/memory-sync-store-tidb.js +21 -21
- package/dist/plugins/pg-pool.js +26 -26
- package/dist/plugins/task-list-store-sql.d.ts +1 -1
- package/dist/plugins/task-list-store-sql.js +11 -11
- package/dist/plugins/tidb-pool.js +26 -26
- package/dist/plugins/workflow-journal-store-sql.d.ts +1 -1
- package/dist/plugins/workflow-journal-store-sql.js +4 -4
- package/dist/plugins/workflow-run-store-sql.d.ts +1 -1
- package/dist/plugins/workflow-run-store-sql.js +9 -9
- package/dist/security.js +9 -1
- package/package.json +2 -2
|
@@ -26,8 +26,8 @@ import { pgSafeJsonStringify, pgHasUnstorable } from "./pg-safe-json.js";
|
|
|
26
26
|
/** Table names (single source). Deliberately DISJOINT from the legacy `agent_memory*` tables —
|
|
27
27
|
* the retired MemoryStore plane and this entry plane must never cross-write. */
|
|
28
28
|
export const PG_MEMORY_ENGINE_TABLES = {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
entry: "agent_memory_engine_entry",
|
|
30
|
+
cursor: "agent_memory_engine_cursor",
|
|
31
31
|
};
|
|
32
32
|
/** Idempotent DDL for the entry plane. Call once at startup (or run through your migration tool). */
|
|
33
33
|
export async function ensurePgMemoryEngineSchema(query, opts = {}) {
|
|
@@ -41,7 +41,7 @@ export async function ensurePgMemoryEngineSchema(query, opts = {}) {
|
|
|
41
41
|
if (vec?.pgvector)
|
|
42
42
|
await query("CREATE EXTENSION IF NOT EXISTS vector");
|
|
43
43
|
const embeddingCol = vec?.pgvector ? `vector(${vec.dimensions})` : "jsonb";
|
|
44
|
-
await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_ENGINE_TABLES.
|
|
44
|
+
await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_ENGINE_TABLES.entry} (
|
|
45
45
|
id text COLLATE "C" PRIMARY KEY,
|
|
46
46
|
scope text COLLATE "C" NOT NULL,
|
|
47
47
|
slug text COLLATE "C" NOT NULL,
|
|
@@ -53,7 +53,7 @@ export async function ensurePgMemoryEngineSchema(query, opts = {}) {
|
|
|
53
53
|
embedding ${embeddingCol}
|
|
54
54
|
)`);
|
|
55
55
|
try {
|
|
56
|
-
await query(`CREATE INDEX IF NOT EXISTS idx_${PG_MEMORY_ENGINE_TABLES.
|
|
56
|
+
await query(`CREATE INDEX IF NOT EXISTS idx_${PG_MEMORY_ENGINE_TABLES.entry}_scope ON ${PG_MEMORY_ENGINE_TABLES.entry} (scope)`);
|
|
57
57
|
}
|
|
58
58
|
catch {
|
|
59
59
|
/* pg-mem: partial CREATE INDEX support — the index is a perf add-on, never correctness */
|
|
@@ -63,8 +63,8 @@ export async function ensurePgMemoryEngineSchema(query, opts = {}) {
|
|
|
63
63
|
// insert, leaving two entries whose file projections overwrite each other at materialize. The DB
|
|
64
64
|
// constraint is the only true arbiter; the add path catches the violation and retries the next
|
|
65
65
|
// suffix. NOT wrapped in try/catch — if this constraint cannot be created, the backend is unsafe.
|
|
66
|
-
await query(`CREATE UNIQUE INDEX IF NOT EXISTS uq_${PG_MEMORY_ENGINE_TABLES.
|
|
67
|
-
await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_ENGINE_TABLES.
|
|
66
|
+
await query(`CREATE UNIQUE INDEX IF NOT EXISTS uq_${PG_MEMORY_ENGINE_TABLES.entry}_scope_slug ON ${PG_MEMORY_ENGINE_TABLES.entry} (scope, slug)`);
|
|
67
|
+
await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_ENGINE_TABLES.cursor} (
|
|
68
68
|
scope text COLLATE "C" PRIMARY KEY,
|
|
69
69
|
cursor text COLLATE "C" NOT NULL
|
|
70
70
|
)`);
|
|
@@ -149,14 +149,14 @@ export class PgMemoryEngineBackend {
|
|
|
149
149
|
const uniq = [...new Set(scopes)];
|
|
150
150
|
if (uniq.length === 0)
|
|
151
151
|
return [];
|
|
152
|
-
const res = await this.query(`SELECT id, scope, slug, frontmatter, rev, mtime_ms, size_bytes FROM ${PG_MEMORY_ENGINE_TABLES.
|
|
152
|
+
const res = await this.query(`SELECT id, scope, slug, frontmatter, rev, mtime_ms, size_bytes FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE scope IN (${inPlaceholders(1, uniq.length)}) ORDER BY id`, uniq);
|
|
153
153
|
return res.rows.map((r) => this.headerFromRow(r));
|
|
154
154
|
}
|
|
155
155
|
async getByIds(ids) {
|
|
156
156
|
const uniq = [...new Set(ids)];
|
|
157
157
|
if (uniq.length === 0)
|
|
158
158
|
return [];
|
|
159
|
-
const res = await this.query(`SELECT id, scope, slug, frontmatter, body, rev, mtime_ms, size_bytes FROM ${PG_MEMORY_ENGINE_TABLES.
|
|
159
|
+
const res = await this.query(`SELECT id, scope, slug, frontmatter, body, rev, mtime_ms, size_bytes FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE id IN (${inPlaceholders(1, uniq.length)}) ORDER BY id`, uniq);
|
|
160
160
|
return res.rows.map((r) => this.entryFromRow(r)); // unknown ids: silently absent
|
|
161
161
|
}
|
|
162
162
|
async search(query, scopes, opts) {
|
|
@@ -180,7 +180,7 @@ export class PgMemoryEngineBackend {
|
|
|
180
180
|
if (Array.isArray(qv) && qv.length === this.embedder.dimensions) {
|
|
181
181
|
const vecParam = scopes.length + 1;
|
|
182
182
|
const res = await this.query(`SELECT id, scope, slug, frontmatter, rev, mtime_ms, size_bytes, embedding <=> $${vecParam}::vector AS dist
|
|
183
|
-
FROM ${PG_MEMORY_ENGINE_TABLES.
|
|
183
|
+
FROM ${PG_MEMORY_ENGINE_TABLES.entry}
|
|
184
184
|
WHERE scope IN (${inPlaceholders(1, scopes.length)}) AND embedding IS NOT NULL
|
|
185
185
|
ORDER BY embedding <=> $${vecParam}::vector ASC
|
|
186
186
|
LIMIT ${Math.min(limit, 100_000)}`, [...scopes, vectorLiteral(qv)]);
|
|
@@ -188,7 +188,7 @@ export class PgMemoryEngineBackend {
|
|
|
188
188
|
}
|
|
189
189
|
// Wrong-dimension query vector (transient embedder fault) — fall open to the in-process path.
|
|
190
190
|
}
|
|
191
|
-
const res = await this.query(`SELECT id, scope, slug, frontmatter, body, rev, mtime_ms, size_bytes, embedding FROM ${PG_MEMORY_ENGINE_TABLES.
|
|
191
|
+
const res = await this.query(`SELECT id, scope, slug, frontmatter, body, rev, mtime_ms, size_bytes, embedding FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE scope IN (${inPlaceholders(1, scopes.length)})`, [...scopes]);
|
|
192
192
|
const queryVec = this.embedder ? await this.embedder.embed(query).catch(() => null) : null;
|
|
193
193
|
const dims = this.embedder?.dimensions;
|
|
194
194
|
const scored = [];
|
|
@@ -256,7 +256,7 @@ export class PgMemoryEngineBackend {
|
|
|
256
256
|
return report;
|
|
257
257
|
}
|
|
258
258
|
async applyOne(patch, report, plannedSlugs, historyRows) {
|
|
259
|
-
const T = PG_MEMORY_ENGINE_TABLES.
|
|
259
|
+
const T = PG_MEMORY_ENGINE_TABLES.entry;
|
|
260
260
|
// A patch whose entry carries a DIFFERENT id than the patch addresses is
|
|
261
261
|
// malformed — the two backends would otherwise diverge (File writes the entry's id, Pg would
|
|
262
262
|
// key the SELECT on patch.id but the write on entry.id). Refused up front, both backends alike.
|
|
@@ -354,7 +354,7 @@ export class PgMemoryEngineBackend {
|
|
|
354
354
|
}
|
|
355
355
|
// update/delete address an EXISTING entry by immutable id. (scope in the projection = the
|
|
356
356
|
// history row's scope for delete — a tombstone can't be joined back to the deleted row.)
|
|
357
|
-
const cur = await this.query(`SELECT slug, rev, scope, frontmatter FROM ${PG_MEMORY_ENGINE_TABLES.
|
|
357
|
+
const cur = await this.query(`SELECT slug, rev, scope, frontmatter FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE id = $1`, [patch.id]);
|
|
358
358
|
if (cur.rows.length === 0) {
|
|
359
359
|
report.conflicts.push({ op: patch.op, id: patch.id, reason: "unknown id (entry not found in backend)" });
|
|
360
360
|
return;
|
|
@@ -395,7 +395,7 @@ export class PgMemoryEngineBackend {
|
|
|
395
395
|
if (patch.op === "delete") {
|
|
396
396
|
// Conditional on the rev we just judged — a write that lands between the SELECT and this DELETE
|
|
397
397
|
// re-reports as the same CAS conflict instead of silently deleting the newer committed state.
|
|
398
|
-
const res = await this.query(`DELETE FROM ${PG_MEMORY_ENGINE_TABLES.
|
|
398
|
+
const res = await this.query(`DELETE FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE id = $1 AND rev = $2 RETURNING id`, [patch.id, currentRev]);
|
|
399
399
|
if (res.rows.length === 0) {
|
|
400
400
|
report.conflicts.push({ op: "delete", id: patch.id, reason: "rev mismatch (concurrent change)", baseRev: patch.baseRev, currentRev: await this.freshRev(patch.id, currentRev) });
|
|
401
401
|
return;
|
|
@@ -421,7 +421,7 @@ export class PgMemoryEngineBackend {
|
|
|
421
421
|
const cEntry = entry; // R3 终形:拒绝式后恒干净,rev 对拍原文
|
|
422
422
|
const rev = computeEntryRev(cEntry);
|
|
423
423
|
const embedding = await this.embeddingParam(this.haystackOf(cEntry.slug, cEntry.frontmatter, cEntry.body));
|
|
424
|
-
const res = await this.query(`UPDATE ${PG_MEMORY_ENGINE_TABLES.
|
|
424
|
+
const res = await this.query(`UPDATE ${PG_MEMORY_ENGINE_TABLES.entry}
|
|
425
425
|
SET scope = $2, slug = $3, frontmatter = $4, body = $5, rev = $6, mtime_ms = $7, size_bytes = $8, embedding = $9
|
|
426
426
|
WHERE id = $1 AND rev = $10
|
|
427
427
|
RETURNING id`, [cEntry.id, cEntry.scope, cEntry.slug, pgSafeJsonStringify(cEntry.frontmatter), cEntry.body, rev, this.clock(), Buffer.byteLength(serializeEntryFile(cEntry), "utf8"), embedding, currentRev]);
|
|
@@ -438,15 +438,15 @@ export class PgMemoryEngineBackend {
|
|
|
438
438
|
* SELECT→write window), the report's `currentRev` must be the rev NOW — re-reporting the pre-race
|
|
439
439
|
* SELECT value could claim `baseRev === currentRev` alongside a "rev mismatch" reason. */
|
|
440
440
|
async freshRev(id, fallback) {
|
|
441
|
-
const res = await this.query(`SELECT rev FROM ${PG_MEMORY_ENGINE_TABLES.
|
|
441
|
+
const res = await this.query(`SELECT rev FROM ${PG_MEMORY_ENGINE_TABLES.entry} WHERE id = $1`, [id]);
|
|
442
442
|
return res.rows.length > 0 ? String(res.rows[0].rev) : fallback;
|
|
443
443
|
}
|
|
444
444
|
async getConsolidationCursor(scope) {
|
|
445
|
-
const res = await this.query(`SELECT cursor FROM ${PG_MEMORY_ENGINE_TABLES.
|
|
445
|
+
const res = await this.query(`SELECT cursor FROM ${PG_MEMORY_ENGINE_TABLES.cursor} WHERE scope = $1`, [scope]);
|
|
446
446
|
return res.rows.length > 0 ? String(res.rows[0].cursor) : undefined;
|
|
447
447
|
}
|
|
448
448
|
async setConsolidationCursor(scope, cursor) {
|
|
449
|
-
await this.query(`INSERT INTO ${PG_MEMORY_ENGINE_TABLES.
|
|
449
|
+
await this.query(`INSERT INTO ${PG_MEMORY_ENGINE_TABLES.cursor} (scope, cursor) VALUES ($1, $2)
|
|
450
450
|
ON CONFLICT (scope) DO UPDATE SET cursor = $2`, [scope, cursor]);
|
|
451
451
|
}
|
|
452
452
|
}
|
|
@@ -5,8 +5,8 @@ import type { Pool } from "mysql2/promise";
|
|
|
5
5
|
/** Table names (single source) — SAME names as PG_MEMORY_ENGINE_TABLES (the two dialects never share
|
|
6
6
|
* one database), deliberately DISJOINT from the legacy `agent_memory*` MemoryStore plane. */
|
|
7
7
|
export declare const TIDB_MEMORY_ENGINE_TABLES: {
|
|
8
|
-
readonly
|
|
9
|
-
readonly
|
|
8
|
+
readonly entry: "agent_memory_engine_entry";
|
|
9
|
+
readonly cursor: "agent_memory_engine_cursor";
|
|
10
10
|
};
|
|
11
11
|
/** Idempotent DDL for the entry plane (TiDB dialect). Call once at startup.
|
|
12
12
|
* Key lengths: utf8mb4 = 4 bytes/char and the default index-key cap is 3072 bytes, so the
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// - add 路径 Pg 用 `INSERT … ON CONFLICT (id) DO UPDATE`(冲突靶=仅 PK)。MySQL 的 ON DUPLICATE KEY
|
|
13
13
|
// UPDATE 会被 **任意** 唯一键触发——(scope,slug) 撞键时它会把竞争对手的行改写成本 entry 的 id(数据毁),
|
|
14
14
|
// 不能用;拆成「不存在→INSERT / 已存在→UPDATE by id」,两条腿的 ER_DUP_ENTRY 都按键名分诊(PRIMARY=
|
|
15
|
-
// 并发同 id add → 转 UPDATE 腿;
|
|
15
|
+
// 并发同 id add → 转 UPDATE 腿;uq_agent_memory_engine_entry_scope_slug=并发 slug 竞争 → 下一个 -n 后缀重试);
|
|
16
16
|
// - unique violation 判定:ER_DUP_ENTRY(errno 1062),不是 Pg 的 23505;
|
|
17
17
|
// - pg-mem 伪影 workaround(索引列 ANY 零行/重复占位符重复行)不抄——MySQL 本就走 IN-list,dedupe 保留
|
|
18
18
|
// 是因为它同时是诚实语义(重复 key 问的是同一个 entry,契约锁死 once-per-entry);
|
|
@@ -27,14 +27,14 @@ import { pgHasUnstorable } from "./pg-safe-json.js";
|
|
|
27
27
|
/** Table names (single source) — SAME names as PG_MEMORY_ENGINE_TABLES (the two dialects never share
|
|
28
28
|
* one database), deliberately DISJOINT from the legacy `agent_memory*` MemoryStore plane. */
|
|
29
29
|
export const TIDB_MEMORY_ENGINE_TABLES = {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
entry: "agent_memory_engine_entry",
|
|
31
|
+
cursor: "agent_memory_engine_cursor",
|
|
32
32
|
};
|
|
33
33
|
/** Idempotent DDL for the entry plane (TiDB dialect). Call once at startup.
|
|
34
34
|
* Key lengths: utf8mb4 = 4 bytes/char and the default index-key cap is 3072 bytes, so the
|
|
35
35
|
* (scope, slug) unique key budget is scope 190 + slug 512 = 702 chars ⇒ 2808 bytes ≤ 3072. */
|
|
36
36
|
export async function ensureTiDBMemoryEngineSchema(pool) {
|
|
37
|
-
await pool.query(`CREATE TABLE IF NOT EXISTS ${TIDB_MEMORY_ENGINE_TABLES.
|
|
37
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS ${TIDB_MEMORY_ENGINE_TABLES.entry} (
|
|
38
38
|
id VARCHAR(64) NOT NULL,
|
|
39
39
|
scope VARCHAR(190) NOT NULL,
|
|
40
40
|
slug VARCHAR(512) NOT NULL,
|
|
@@ -45,13 +45,13 @@ export async function ensureTiDBMemoryEngineSchema(pool) {
|
|
|
45
45
|
size_bytes INT NOT NULL,
|
|
46
46
|
embedding JSON NULL,
|
|
47
47
|
PRIMARY KEY (id),
|
|
48
|
-
KEY
|
|
49
|
-
UNIQUE KEY
|
|
48
|
+
KEY idx_agent_memory_engine_entry_scope (scope),
|
|
49
|
+
UNIQUE KEY uq_agent_memory_engine_entry_scope_slug (scope, slug)
|
|
50
50
|
) COLLATE utf8mb4_bin`);
|
|
51
51
|
// The (scope, slug) UNIQUE key above is CORRECTNESS, not perf (Pg 版同注):two
|
|
52
52
|
// instances racing an add of one scope+slug (different ids) can both probe "free" and both insert;
|
|
53
53
|
// the DB constraint is the only true arbiter and the add path retries the next suffix on violation.
|
|
54
|
-
await pool.query(`CREATE TABLE IF NOT EXISTS ${TIDB_MEMORY_ENGINE_TABLES.
|
|
54
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS ${TIDB_MEMORY_ENGINE_TABLES.cursor} (
|
|
55
55
|
scope VARCHAR(190) NOT NULL,
|
|
56
56
|
\`cursor\` TEXT NOT NULL,
|
|
57
57
|
PRIMARY KEY (scope)
|
|
@@ -62,7 +62,7 @@ function isDupEntry(err) {
|
|
|
62
62
|
return false;
|
|
63
63
|
return err.code === "ER_DUP_ENTRY" || err.errno === 1062;
|
|
64
64
|
}
|
|
65
|
-
/** ER_DUP_ENTRY messages name the violated key (`… for key 'tbl.PRIMARY'` / `… for key 'tbl.
|
|
65
|
+
/** ER_DUP_ENTRY messages name the violated key (`… for key 'tbl.PRIMARY'` / `… for key 'tbl.uq_agent_memory_engine_entry_scope_slug'`)
|
|
66
66
|
* — the add path uses this to tell "id already exists (→ overwrite leg)" from "slug race (→ -n retry)". */
|
|
67
67
|
function isPrimaryKeyDup(err) {
|
|
68
68
|
return isDupEntry(err) && /for key '[^']*PRIMARY'/i.test(String(err.message ?? ""));
|
|
@@ -134,14 +134,14 @@ export class TiDBMemoryEngineBackend {
|
|
|
134
134
|
const uniq = [...new Set(scopes)];
|
|
135
135
|
if (uniq.length === 0)
|
|
136
136
|
return [];
|
|
137
|
-
const rows = await this.selectRows(`SELECT id, scope, slug, frontmatter, rev, mtime_ms, size_bytes FROM ${TIDB_MEMORY_ENGINE_TABLES.
|
|
137
|
+
const rows = await this.selectRows(`SELECT id, scope, slug, frontmatter, rev, mtime_ms, size_bytes FROM ${TIDB_MEMORY_ENGINE_TABLES.entry} WHERE scope IN (${inPlaceholders(uniq.length)}) ORDER BY id`, uniq);
|
|
138
138
|
return rows.map((r) => this.headerFromRow(r));
|
|
139
139
|
}
|
|
140
140
|
async getByIds(ids) {
|
|
141
141
|
const uniq = [...new Set(ids)];
|
|
142
142
|
if (uniq.length === 0)
|
|
143
143
|
return [];
|
|
144
|
-
const rows = await this.selectRows(`SELECT id, scope, slug, frontmatter, body, rev, mtime_ms, size_bytes FROM ${TIDB_MEMORY_ENGINE_TABLES.
|
|
144
|
+
const rows = await this.selectRows(`SELECT id, scope, slug, frontmatter, body, rev, mtime_ms, size_bytes FROM ${TIDB_MEMORY_ENGINE_TABLES.entry} WHERE id IN (${inPlaceholders(uniq.length)}) ORDER BY id`, uniq);
|
|
145
145
|
return rows.map((r) => this.entryFromRow(r)); // unknown ids: silently absent
|
|
146
146
|
}
|
|
147
147
|
async search(query, scopes, opts) {
|
|
@@ -155,7 +155,7 @@ export class TiDBMemoryEngineBackend {
|
|
|
155
155
|
if (q.size === 0)
|
|
156
156
|
return [];
|
|
157
157
|
const uniq = [...new Set(scopes)];
|
|
158
|
-
const rows = await this.selectRows(`SELECT id, scope, slug, frontmatter, body, rev, mtime_ms, size_bytes FROM ${TIDB_MEMORY_ENGINE_TABLES.
|
|
158
|
+
const rows = await this.selectRows(`SELECT id, scope, slug, frontmatter, body, rev, mtime_ms, size_bytes FROM ${TIDB_MEMORY_ENGINE_TABLES.entry} WHERE scope IN (${inPlaceholders(uniq.length)})`, uniq);
|
|
159
159
|
const scored = [];
|
|
160
160
|
for (const raw of rows) {
|
|
161
161
|
const r = raw;
|
|
@@ -212,7 +212,7 @@ export class TiDBMemoryEngineBackend {
|
|
|
212
212
|
return report;
|
|
213
213
|
}
|
|
214
214
|
async applyOne(patch, report, plannedSlugs, historyRows) {
|
|
215
|
-
const T = TIDB_MEMORY_ENGINE_TABLES.
|
|
215
|
+
const T = TIDB_MEMORY_ENGINE_TABLES.entry;
|
|
216
216
|
// A patch whose entry carries a DIFFERENT id than the patch addresses is malformed — refused up
|
|
217
217
|
// front, all backends alike (Pg 版同注).
|
|
218
218
|
if (patch.op !== "delete" && patch.entry !== undefined && patch.entry.id !== patch.id) {
|
|
@@ -263,7 +263,7 @@ export class TiDBMemoryEngineBackend {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
// Slug collision with a DIFFERENT entry → deterministic `-n` suffix (file-backend parity); the
|
|
266
|
-
// probe is only an optimization — the write INSIDE the loop is arbitrated by
|
|
266
|
+
// probe is only an optimization — the write INSIDE the loop is arbitrated by uq_agent_memory_engine_entry_scope_slug, and
|
|
267
267
|
// a concurrent loser retries the next suffix (Pg 版同注).
|
|
268
268
|
let slug = entry.slug;
|
|
269
269
|
for (let n = 2;; n++) {
|
|
@@ -419,7 +419,7 @@ export class TiDBMemoryEngineBackend {
|
|
|
419
419
|
/** The row's rev NOW, or undefined if the row is gone. The existence distinction is the whole point
|
|
420
420
|
* (codex lens-3): a concurrent delete must not be mistaken for an idempotent already-at-target write. */
|
|
421
421
|
async freshRevOrMissing(id) {
|
|
422
|
-
const rows = await this.selectRows(`SELECT rev FROM ${TIDB_MEMORY_ENGINE_TABLES.
|
|
422
|
+
const rows = await this.selectRows(`SELECT rev FROM ${TIDB_MEMORY_ENGINE_TABLES.entry} WHERE id = ?`, [id]);
|
|
423
423
|
return rows.length > 0 ? String(rows[0].rev) : undefined;
|
|
424
424
|
}
|
|
425
425
|
/** When the conditional write reports zero rows (a write landed inside the SELECT→write window), the
|
|
@@ -429,13 +429,13 @@ export class TiDBMemoryEngineBackend {
|
|
|
429
429
|
return (await this.freshRevOrMissing(id)) ?? fallback;
|
|
430
430
|
}
|
|
431
431
|
async getConsolidationCursor(scope) {
|
|
432
|
-
const rows = await this.selectRows(`SELECT \`cursor\` FROM ${TIDB_MEMORY_ENGINE_TABLES.
|
|
432
|
+
const rows = await this.selectRows(`SELECT \`cursor\` FROM ${TIDB_MEMORY_ENGINE_TABLES.cursor} WHERE scope = ?`, [scope]);
|
|
433
433
|
return rows.length > 0 ? String(rows[0].cursor) : undefined;
|
|
434
434
|
}
|
|
435
435
|
async setConsolidationCursor(scope, cursor) {
|
|
436
436
|
// Single unique key (the PK) on this table — ON DUPLICATE KEY UPDATE is unambiguous here,
|
|
437
437
|
// unlike the two-unique-key entries table (see the file-head dialect note).
|
|
438
|
-
await this.write(`INSERT INTO ${TIDB_MEMORY_ENGINE_TABLES.
|
|
438
|
+
await this.write(`INSERT INTO ${TIDB_MEMORY_ENGINE_TABLES.cursor} (scope, \`cursor\`) VALUES (?, ?) ON DUPLICATE KEY UPDATE \`cursor\` = VALUES(\`cursor\`)`, [scope, cursor]);
|
|
439
439
|
}
|
|
440
440
|
}
|
|
441
441
|
//# sourceMappingURL=memory-engine-tidb.js.map
|
|
@@ -2,7 +2,7 @@ import type { MemorySyncCursor } from "@sema-agent/core";
|
|
|
2
2
|
import type { PgQueryFn } from "./pg-query.js";
|
|
3
3
|
/** Table names (single source). SAME names as the TiDB twin (the two dialects never share one DB). */
|
|
4
4
|
export declare const PG_MEMORY_SYNC_TABLES: {
|
|
5
|
-
readonly
|
|
5
|
+
readonly syncCursor: "agent_memory_engine_sync_cursor";
|
|
6
6
|
readonly pushQueue: "agent_memory_engine_push_queue";
|
|
7
7
|
readonly history: "agent_memory_engine_history";
|
|
8
8
|
};
|
|
@@ -37,7 +37,7 @@ export interface MemorySyncStore {
|
|
|
37
37
|
/** Whole-row replace (round semantics — baseRevs is swapped as one map, never merged). */
|
|
38
38
|
putCursor(cursor: MemorySyncCursor): Promise<void>;
|
|
39
39
|
enqueue(item: PushQueueItem): Promise<void>;
|
|
40
|
-
/** Lease-claim up to `limit` due rows (
|
|
40
|
+
/** Lease-claim up to `limit` due rows (next_attempt_at_ms <= nowMs), oldest-due first. Claimed rows
|
|
41
41
|
* are invisible to other claimers until `complete`/`fail` or lease expiry (crash self-heal). */
|
|
42
42
|
claimDue(nowMs: number, limit: number): Promise<ClaimedPushQueueItem[]>;
|
|
43
43
|
/** Success = delete the row (outbox posture: the queue only holds outstanding work). */
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { pgSafeJsonStringify, pgProtocolJsonStringify } from "./pg-safe-json.js";
|
|
2
2
|
/** Table names (single source). SAME names as the TiDB twin (the two dialects never share one DB). */
|
|
3
3
|
export const PG_MEMORY_SYNC_TABLES = {
|
|
4
|
-
|
|
4
|
+
syncCursor: "agent_memory_engine_sync_cursor",
|
|
5
5
|
pushQueue: "agent_memory_engine_push_queue",
|
|
6
6
|
history: "agent_memory_engine_history",
|
|
7
7
|
};
|
|
8
8
|
/** Idempotent DDL for the sync plane (PG dialect). Call once at startup. */
|
|
9
9
|
export async function ensurePgMemorySyncSchema(query) {
|
|
10
|
-
await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_SYNC_TABLES.
|
|
10
|
+
await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_SYNC_TABLES.syncCursor} (
|
|
11
11
|
scope varchar(190) COLLATE "C" NOT NULL,
|
|
12
12
|
peer varchar(190) COLLATE "C" NOT NULL,
|
|
13
13
|
base_revs jsonb NOT NULL,
|
|
@@ -15,16 +15,16 @@ export async function ensurePgMemorySyncSchema(query) {
|
|
|
15
15
|
PRIMARY KEY (scope, peer)
|
|
16
16
|
)`);
|
|
17
17
|
await query(`CREATE TABLE IF NOT EXISTS ${PG_MEMORY_SYNC_TABLES.pushQueue} (
|
|
18
|
-
id
|
|
19
|
-
scope
|
|
20
|
-
entry_id
|
|
21
|
-
payload
|
|
22
|
-
attempts
|
|
23
|
-
|
|
24
|
-
last_error
|
|
18
|
+
id char(36) COLLATE "C" PRIMARY KEY,
|
|
19
|
+
scope varchar(190) COLLATE "C" NOT NULL,
|
|
20
|
+
entry_id char(36) COLLATE "C" NOT NULL,
|
|
21
|
+
payload jsonb NOT NULL,
|
|
22
|
+
attempts integer NOT NULL DEFAULT 0,
|
|
23
|
+
next_attempt_at_ms bigint NOT NULL,
|
|
24
|
+
last_error text COLLATE "C" NULL
|
|
25
25
|
)`);
|
|
26
26
|
// Scan face (§1.2): the reaper/consumer sweeps by due time. Perf add-on, never correctness.
|
|
27
|
-
await query(`CREATE INDEX IF NOT EXISTS idx_${PG_MEMORY_SYNC_TABLES.pushQueue}
|
|
27
|
+
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)`);
|
|
28
28
|
}
|
|
29
29
|
/** jsonb defense: node-pg hands back parsed objects, but a text-typed round-trip (or a foreign
|
|
30
30
|
* writer) may yield a string — same posture as parseJson in run-store-sql.ts. */
|
|
@@ -51,7 +51,7 @@ export class PgMemorySyncStore {
|
|
|
51
51
|
}
|
|
52
52
|
// ── cursors ────────────────────────────────────────────────────────────────
|
|
53
53
|
async getCursor(scope, peer) {
|
|
54
|
-
const res = await this.query(`SELECT base_revs, updated_at_ms FROM ${PG_MEMORY_SYNC_TABLES.
|
|
54
|
+
const res = await this.query(`SELECT base_revs, updated_at_ms FROM ${PG_MEMORY_SYNC_TABLES.syncCursor} WHERE scope = $1 AND peer = $2`, [scope, peer]);
|
|
55
55
|
const row = res.rows[0];
|
|
56
56
|
if (!row)
|
|
57
57
|
return undefined;
|
|
@@ -65,7 +65,7 @@ export class PgMemorySyncStore {
|
|
|
65
65
|
async putCursor(cursor) {
|
|
66
66
|
// PK (scope,peer) is the table's ONLY unique key ⇒ ON CONFLICT has exactly one target, no
|
|
67
67
|
// hijack face (纪律:the entries-table ban on blind upserts does NOT apply here).
|
|
68
|
-
await this.query(`INSERT INTO ${PG_MEMORY_SYNC_TABLES.
|
|
68
|
+
await this.query(`INSERT INTO ${PG_MEMORY_SYNC_TABLES.syncCursor} (scope, peer, base_revs, updated_at_ms)
|
|
69
69
|
VALUES ($1, $2, $3::jsonb, $4)
|
|
70
70
|
ON CONFLICT (scope, peer) DO UPDATE SET base_revs = EXCLUDED.base_revs, updated_at_ms = EXCLUDED.updated_at_ms`,
|
|
71
71
|
// codex R11:baseRevs=对账基线(entry id→rev,协议数据)——有损改写=peer 收到的与库里存的
|
|
@@ -76,7 +76,7 @@ export class PgMemorySyncStore {
|
|
|
76
76
|
async enqueue(item) {
|
|
77
77
|
// Plain INSERT, fail-loud on a duplicate id: the caller mints uuidv7 per enqueue, so a dup is a
|
|
78
78
|
// caller bug (or a replay the caller must decide about) — never silently absorbed here.
|
|
79
|
-
await this.query(`INSERT INTO ${PG_MEMORY_SYNC_TABLES.pushQueue} (id, scope, entry_id, payload, attempts,
|
|
79
|
+
await this.query(`INSERT INTO ${PG_MEMORY_SYNC_TABLES.pushQueue} (id, scope, entry_id, payload, attempts, next_attempt_at_ms, last_error)
|
|
80
80
|
VALUES ($1, $2, $3, $4::jsonb, 0, $5, NULL)`,
|
|
81
81
|
// codex R10:payload=协议数据(文档明言 verbatim;rev=内容哈希)——jsonb 列存不了脏值,但有损
|
|
82
82
|
// 改写=payload 与 rev 内部矛盾(消费端永拒/应用脏内容)。协议纪律:拒绝式(与 memory add/update
|
|
@@ -88,9 +88,9 @@ export class PgMemorySyncStore {
|
|
|
88
88
|
return [];
|
|
89
89
|
const leasedUntilMs = nowMs + this.claimLeaseMs;
|
|
90
90
|
// ONE statement = one implicit transaction: SKIP LOCKED excludes claimers overlapping in time,
|
|
91
|
-
// the
|
|
91
|
+
// the next_attempt_at_ms bump (lease) excludes everyone arriving after we commit. RETURNING gives
|
|
92
92
|
// the post-update row, so attempts/last_error are the pre-claim values (fail() is the only
|
|
93
|
-
// incrementer) and
|
|
93
|
+
// incrementer) and next_attempt_at_ms is the lease we just wrote.
|
|
94
94
|
// ⚠️ MATERIALIZED CTE, NOT `WHERE id IN (SELECT … LIMIT n FOR UPDATE SKIP LOCKED)`: the IN-subquery
|
|
95
95
|
// form lets the planner RESCAN the locking subquery (e.g. as a rescanned semi-join inner), and each
|
|
96
96
|
// rescan skips the rows the previous pass locked and picks the NEXT due ones — an over-claim beyond
|
|
@@ -99,12 +99,12 @@ export class PgMemorySyncStore {
|
|
|
99
99
|
// (PG12+) pins one evaluation; UPDATE … FROM joins the pinned id set.
|
|
100
100
|
const res = await this.query(`WITH due AS MATERIALIZED (
|
|
101
101
|
SELECT id FROM ${PG_MEMORY_SYNC_TABLES.pushQueue}
|
|
102
|
-
WHERE
|
|
103
|
-
ORDER BY
|
|
102
|
+
WHERE next_attempt_at_ms <= $1
|
|
103
|
+
ORDER BY next_attempt_at_ms, id
|
|
104
104
|
LIMIT $2
|
|
105
105
|
FOR UPDATE SKIP LOCKED
|
|
106
106
|
)
|
|
107
|
-
UPDATE ${PG_MEMORY_SYNC_TABLES.pushQueue} q SET
|
|
107
|
+
UPDATE ${PG_MEMORY_SYNC_TABLES.pushQueue} q SET next_attempt_at_ms = $3
|
|
108
108
|
FROM due WHERE q.id = due.id
|
|
109
109
|
RETURNING q.id, q.scope, q.entry_id, q.payload, q.attempts, q.last_error`, [nowMs, limit, leasedUntilMs]);
|
|
110
110
|
return res.rows.map((r) => ({
|
|
@@ -124,7 +124,7 @@ export class PgMemorySyncStore {
|
|
|
124
124
|
// attempts = attempts + 1 is SQL-atomic — a read-modify-write in JS would race a concurrent
|
|
125
125
|
// failer and lose increments (the attempt cap is the fail-loud stop, it must count honestly).
|
|
126
126
|
await this.query(`UPDATE ${PG_MEMORY_SYNC_TABLES.pushQueue}
|
|
127
|
-
SET attempts = attempts + 1, last_error = $2,
|
|
127
|
+
SET attempts = attempts + 1, last_error = $2, next_attempt_at_ms = $3
|
|
128
128
|
WHERE id = $1`, [id, error, nextAttemptAtMs]);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
@@ -4,7 +4,7 @@ import type { Pool } from "mysql2/promise";
|
|
|
4
4
|
/** Table names (single source) — SAME names as PG_MEMORY_SYNC_TABLES (the two dialects never share
|
|
5
5
|
* one database). */
|
|
6
6
|
export declare const TIDB_MEMORY_SYNC_TABLES: {
|
|
7
|
-
readonly
|
|
7
|
+
readonly syncCursor: "agent_memory_engine_sync_cursor";
|
|
8
8
|
readonly pushQueue: "agent_memory_engine_push_queue";
|
|
9
9
|
readonly history: "agent_memory_engine_history";
|
|
10
10
|
};
|
|
@@ -2,14 +2,14 @@ import { historyRowFrom } from "./memory-sync-store-pg.js";
|
|
|
2
2
|
/** Table names (single source) — SAME names as PG_MEMORY_SYNC_TABLES (the two dialects never share
|
|
3
3
|
* one database). */
|
|
4
4
|
export const TIDB_MEMORY_SYNC_TABLES = {
|
|
5
|
-
|
|
5
|
+
syncCursor: "agent_memory_engine_sync_cursor",
|
|
6
6
|
pushQueue: "agent_memory_engine_push_queue",
|
|
7
7
|
history: "agent_memory_engine_history",
|
|
8
8
|
};
|
|
9
9
|
/** Idempotent DDL for the sync plane (TiDB dialect). Call once at startup.
|
|
10
10
|
* Key budget: utf8mb4 = 4 bytes/char, index cap 3072 bytes ⇒ PK(scope,peer) = 380 chars = 1520 B ✓. */
|
|
11
11
|
export async function ensureTiDBMemorySyncSchema(pool) {
|
|
12
|
-
await pool.query(`CREATE TABLE IF NOT EXISTS ${TIDB_MEMORY_SYNC_TABLES.
|
|
12
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS ${TIDB_MEMORY_SYNC_TABLES.syncCursor} (
|
|
13
13
|
scope VARCHAR(190) NOT NULL,
|
|
14
14
|
peer VARCHAR(190) NOT NULL,
|
|
15
15
|
base_revs JSON NOT NULL,
|
|
@@ -17,15 +17,15 @@ export async function ensureTiDBMemorySyncSchema(pool) {
|
|
|
17
17
|
PRIMARY KEY (scope, peer)
|
|
18
18
|
) COLLATE utf8mb4_bin`);
|
|
19
19
|
await pool.query(`CREATE TABLE IF NOT EXISTS ${TIDB_MEMORY_SYNC_TABLES.pushQueue} (
|
|
20
|
-
id
|
|
21
|
-
scope
|
|
22
|
-
entry_id
|
|
23
|
-
payload
|
|
24
|
-
attempts
|
|
25
|
-
|
|
26
|
-
last_error
|
|
20
|
+
id CHAR(36) NOT NULL,
|
|
21
|
+
scope VARCHAR(190) NOT NULL,
|
|
22
|
+
entry_id CHAR(36) NOT NULL,
|
|
23
|
+
payload JSON NOT NULL,
|
|
24
|
+
attempts INT NOT NULL DEFAULT 0,
|
|
25
|
+
next_attempt_at_ms BIGINT NOT NULL,
|
|
26
|
+
last_error TEXT NULL,
|
|
27
27
|
PRIMARY KEY (id),
|
|
28
|
-
KEY
|
|
28
|
+
KEY idx_agent_memory_engine_push_queue_next_attempt_ms (next_attempt_at_ms)
|
|
29
29
|
) COLLATE utf8mb4_bin`);
|
|
30
30
|
}
|
|
31
31
|
/** JSON-column defense: mysql2 normally hands back parsed objects for JSON columns, but string
|
|
@@ -52,7 +52,7 @@ export class TiDBMemorySyncStore {
|
|
|
52
52
|
}
|
|
53
53
|
// ── cursors ────────────────────────────────────────────────────────────────
|
|
54
54
|
async getCursor(scope, peer) {
|
|
55
|
-
const [rows] = await this.pool.query(`SELECT base_revs, updated_at_ms FROM ${TIDB_MEMORY_SYNC_TABLES.
|
|
55
|
+
const [rows] = await this.pool.query(`SELECT base_revs, updated_at_ms FROM ${TIDB_MEMORY_SYNC_TABLES.syncCursor} WHERE scope = ? AND peer = ?`, [scope, peer]);
|
|
56
56
|
const row = rows[0];
|
|
57
57
|
if (!row)
|
|
58
58
|
return undefined;
|
|
@@ -66,7 +66,7 @@ export class TiDBMemorySyncStore {
|
|
|
66
66
|
async putCursor(cursor) {
|
|
67
67
|
// The table's ONLY unique key is PK(scope,peer) ⇒ ON DUPLICATE KEY UPDATE has exactly one
|
|
68
68
|
// possible trigger — no hijack face (the entries-table ban does NOT apply; see header).
|
|
69
|
-
await this.pool.query(`INSERT INTO ${TIDB_MEMORY_SYNC_TABLES.
|
|
69
|
+
await this.pool.query(`INSERT INTO ${TIDB_MEMORY_SYNC_TABLES.syncCursor} (scope, peer, base_revs, updated_at_ms)
|
|
70
70
|
VALUES (?, ?, ?, ?)
|
|
71
71
|
ON DUPLICATE KEY UPDATE base_revs = VALUES(base_revs), updated_at_ms = VALUES(updated_at_ms)`, [cursor.scope, cursor.peer, JSON.stringify(cursor.baseRevs), cursor.updatedAtMs]);
|
|
72
72
|
}
|
|
@@ -74,7 +74,7 @@ export class TiDBMemorySyncStore {
|
|
|
74
74
|
async enqueue(item) {
|
|
75
75
|
// Plain INSERT, fail-loud on a duplicate id (ER_DUP_ENTRY 1062) — caller mints uuidv7 per
|
|
76
76
|
// enqueue, a dup is a caller bug, never silently absorbed. Same posture as the PG twin.
|
|
77
|
-
await this.pool.query(`INSERT INTO ${TIDB_MEMORY_SYNC_TABLES.pushQueue} (id, scope, entry_id, payload, attempts,
|
|
77
|
+
await this.pool.query(`INSERT INTO ${TIDB_MEMORY_SYNC_TABLES.pushQueue} (id, scope, entry_id, payload, attempts, next_attempt_at_ms, last_error)
|
|
78
78
|
VALUES (?, ?, ?, ?, 0, ?, NULL)`, [item.id, item.scope, item.entryId, JSON.stringify(item.payload), item.nextAttemptAtMs]);
|
|
79
79
|
}
|
|
80
80
|
async claimDue(nowMs, limit) {
|
|
@@ -82,21 +82,21 @@ export class TiDBMemorySyncStore {
|
|
|
82
82
|
return [];
|
|
83
83
|
const leasedUntilMs = nowMs + this.claimLeaseMs;
|
|
84
84
|
// Optimistic CAS claim (header rationale): read the due candidates, then win each row with a
|
|
85
|
-
// conditional UPDATE on the exact
|
|
85
|
+
// conditional UPDATE on the exact next_attempt_at_ms we read. Losers (another worker claimed, or
|
|
86
86
|
// fail()/a re-schedule moved the row) see affectedRows=0 and are skipped — the SKIP LOCKED
|
|
87
87
|
// equivalent, without pessimistic locks or a held connection.
|
|
88
|
-
const [candidates] = await this.pool.query(`SELECT id, scope, entry_id, payload, attempts,
|
|
88
|
+
const [candidates] = await this.pool.query(`SELECT id, scope, entry_id, payload, attempts, next_attempt_at_ms, last_error
|
|
89
89
|
FROM ${TIDB_MEMORY_SYNC_TABLES.pushQueue}
|
|
90
|
-
WHERE
|
|
91
|
-
ORDER BY
|
|
90
|
+
WHERE next_attempt_at_ms <= ?
|
|
91
|
+
ORDER BY next_attempt_at_ms, id
|
|
92
92
|
LIMIT ?`, [nowMs, limit]);
|
|
93
93
|
const claimed = [];
|
|
94
94
|
for (const row of candidates) {
|
|
95
|
-
// affectedRows=CHANGED 语义坑防御:lease = nowMs + claimLeaseMs > nowMs ≥ 读到的
|
|
95
|
+
// affectedRows=CHANGED 语义坑防御:lease = nowMs + claimLeaseMs > nowMs ≥ 读到的 next_attempt_at_ms
|
|
96
96
|
// (WHERE 已保证),新值严格大于旧值 ⇒ 匹配必 change ⇒ affectedRows=1 恒等于「真的赢了」。
|
|
97
97
|
const [res] = await this.pool.query(`UPDATE ${TIDB_MEMORY_SYNC_TABLES.pushQueue}
|
|
98
|
-
SET
|
|
99
|
-
WHERE id = ? AND
|
|
98
|
+
SET next_attempt_at_ms = ?
|
|
99
|
+
WHERE id = ? AND next_attempt_at_ms = ?`, [leasedUntilMs, row.id, toMs(row.next_attempt_at_ms)]);
|
|
100
100
|
if (res.affectedRows !== 1)
|
|
101
101
|
continue; // lost the race — skip, never block
|
|
102
102
|
claimed.push({
|
|
@@ -119,7 +119,7 @@ export class TiDBMemorySyncStore {
|
|
|
119
119
|
// guarantees the row CHANGES — the claim verdict is honest under BOTH affectedRows semantics
|
|
120
120
|
// (mysql2 default = FOUND_ROWS/matched; changed-only would also work here).
|
|
121
121
|
await this.pool.query(`UPDATE ${TIDB_MEMORY_SYNC_TABLES.pushQueue}
|
|
122
|
-
SET attempts = attempts + 1, last_error = ?,
|
|
122
|
+
SET attempts = attempts + 1, last_error = ?, next_attempt_at_ms = ?
|
|
123
123
|
WHERE id = ?`, [error, nextAttemptAtMs, id]);
|
|
124
124
|
}
|
|
125
125
|
}
|
package/dist/plugins/pg-pool.js
CHANGED
|
@@ -116,9 +116,9 @@ export const PG_SCHEMA_STATEMENTS = [
|
|
|
116
116
|
`CREATE INDEX IF NOT EXISTS idx_checkpoint_status_deadline ON checkpoint (status, deadline)`,
|
|
117
117
|
`CREATE INDEX IF NOT EXISTS idx_checkpoint_session_status ON checkpoint (session_id, status)`,
|
|
118
118
|
`CREATE TABLE IF NOT EXISTS checkpoint_ctx (
|
|
119
|
-
session_id
|
|
120
|
-
ctx
|
|
121
|
-
|
|
119
|
+
session_id VARCHAR(190) COLLATE "C" NOT NULL,
|
|
120
|
+
ctx JSONB NOT NULL,
|
|
121
|
+
updated_at_ms BIGINT NOT NULL,
|
|
122
122
|
PRIMARY KEY (session_id)
|
|
123
123
|
)`,
|
|
124
124
|
// E18 resume-at anchor map (the PG twin of the TiDB resume_anchor table — eventId→entryId; see tidb-pool.ts).
|
|
@@ -203,17 +203,17 @@ export const PG_SCHEMA_STATEMENTS = [
|
|
|
203
203
|
// is TEXT, NOT JSONB — the byte-match contract is "string in, string out" (the store JSON.parses on load); JSONB would
|
|
204
204
|
// make `pg` auto-parse the row back to an object and break the twin's String()→JSON.parse path. See workflow-journal-store-sql.ts.
|
|
205
205
|
`CREATE TABLE IF NOT EXISTS workflow_journal (
|
|
206
|
-
run_id
|
|
207
|
-
ordinal
|
|
206
|
+
run_id VARCHAR(191) COLLATE "C" NOT NULL,
|
|
207
|
+
ordinal INT NOT NULL,
|
|
208
208
|
-- SVC-2 / CORE-9 audit BLOCKER: cross-tenant resume isolation (the WorkflowJournalStore seam takes a scope
|
|
209
209
|
-- param). NOT NULL DEFAULT '' — a row written without a scope lands in the '' bucket, which is
|
|
210
210
|
-- CROSS-SCOPE-ORPHANED but harmless: a real resume always filters \`WHERE scope = <caller>\`, so '' rows are
|
|
211
211
|
-- never handed to a tenant. Keep the DEFAULT: it is what makes a scope-less write land in that dead bucket
|
|
212
212
|
-- instead of failing or leaking into a real tenant's scope.
|
|
213
|
-
scope
|
|
214
|
-
call_key
|
|
215
|
-
result
|
|
216
|
-
|
|
213
|
+
scope VARCHAR(190) COLLATE "C" NOT NULL DEFAULT '',
|
|
214
|
+
call_key VARCHAR(255) COLLATE "C" NOT NULL,
|
|
215
|
+
result TEXT COLLATE "C" NOT NULL,
|
|
216
|
+
created_at_ms BIGINT NOT NULL,
|
|
217
217
|
PRIMARY KEY (run_id, ordinal)
|
|
218
218
|
)`,
|
|
219
219
|
// RB-242 / WF2 ([1981] core 拍板 a 形):cross-replica workflow resume admission lease — the PG twin of
|
|
@@ -228,30 +228,30 @@ export const PG_SCHEMA_STATEMENTS = [
|
|
|
228
228
|
// P1 (fleet failover): WorkflowRunStore + completion-inbox PG twins (design doc in workflow-run-store-sql.ts).
|
|
229
229
|
// `run` is TEXT not jsonb (node-pg auto-parse would break the shared String()→JSON.parse path).
|
|
230
230
|
`CREATE TABLE IF NOT EXISTS workflow_run (
|
|
231
|
-
id
|
|
232
|
-
scope
|
|
233
|
-
status
|
|
234
|
-
run
|
|
235
|
-
rev
|
|
236
|
-
|
|
237
|
-
ended_at
|
|
231
|
+
id VARCHAR(191) COLLATE "C" NOT NULL,
|
|
232
|
+
scope VARCHAR(190) COLLATE "C" NOT NULL,
|
|
233
|
+
status VARCHAR(16) COLLATE "C" NOT NULL,
|
|
234
|
+
run TEXT COLLATE "C" NOT NULL,
|
|
235
|
+
rev INTEGER NOT NULL DEFAULT 0,
|
|
236
|
+
created_at_ms BIGINT NOT NULL,
|
|
237
|
+
ended_at BIGINT,
|
|
238
238
|
PRIMARY KEY (id)
|
|
239
239
|
)`,
|
|
240
|
-
`CREATE INDEX IF NOT EXISTS idx_wfrun_scope_created ON workflow_run (scope,
|
|
240
|
+
`CREATE INDEX IF NOT EXISTS idx_wfrun_scope_created ON workflow_run (scope, created_at_ms)`,
|
|
241
241
|
`CREATE INDEX IF NOT EXISTS idx_wfrun_scope_status ON workflow_run (scope, status)`,
|
|
242
242
|
`CREATE TABLE IF NOT EXISTS workflow_completion_inbox (
|
|
243
|
-
seq
|
|
244
|
-
session_id
|
|
245
|
-
run_id
|
|
246
|
-
owner
|
|
247
|
-
status
|
|
248
|
-
summary
|
|
249
|
-
|
|
243
|
+
seq BIGSERIAL,
|
|
244
|
+
session_id VARCHAR(190) COLLATE "C" NOT NULL,
|
|
245
|
+
run_id VARCHAR(191) COLLATE "C" NOT NULL,
|
|
246
|
+
owner VARCHAR(190) COLLATE "C",
|
|
247
|
+
status VARCHAR(16) COLLATE "C" NOT NULL,
|
|
248
|
+
summary TEXT COLLATE "C" NOT NULL,
|
|
249
|
+
enqueued_at_ms BIGINT NOT NULL,
|
|
250
250
|
-- 1.109 delivery envelope. kind NULL = a LEGACY \`workflow_complete\` row (the shape that predates the
|
|
251
251
|
-- envelope); readers must keep treating NULL as that kind, it is not "unknown". payload = the kind's
|
|
252
252
|
-- body, NULL for the legacy shape.
|
|
253
|
-
kind
|
|
254
|
-
payload
|
|
253
|
+
kind VARCHAR(24) COLLATE "C",
|
|
254
|
+
payload TEXT COLLATE "C",
|
|
255
255
|
PRIMARY KEY (session_id, run_id),
|
|
256
256
|
-- Twin-alignment with TiDB's \`UNIQUE KEY uq_wfinbox_seq (seq)\` (same constraint name on purpose, so the two
|
|
257
257
|
-- schemas can be diffed by name). This is NOT a correctness fix: seq is BIGSERIAL, so it is already unique in
|
|
@@ -27,7 +27,7 @@ import type { Pool as MySqlPool } from "mysql2/promise";
|
|
|
27
27
|
import type { Pool as PgPool } from "pg";
|
|
28
28
|
import { type TaskListStore } from "@sema-agent/core";
|
|
29
29
|
export declare const TASK_LIST_META_TABLE = "task_list_meta";
|
|
30
|
-
export declare const
|
|
30
|
+
export declare const TASK_LIST_ITEM_TABLE = "task_list_item";
|
|
31
31
|
export declare function ensureTiDBTaskListSchema(pool: MySqlPool): Promise<void>;
|
|
32
32
|
export declare function ensurePgTaskListSchema(q: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
|
|
33
33
|
export declare function createTiDBTaskListStore(pool: MySqlPool, listKey: string): TaskListStore;
|