@sema-agent/server 7.93.7 → 7.95.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 (74) hide show
  1. package/USAGE.md +52 -0
  2. package/dist/boot/resolve-spec.js +14 -1
  3. package/dist/boot/stage-09-leader.js +1 -1
  4. package/dist/boot/stores.js +6 -3
  5. package/dist/boot/workflow-orchestration.js +5 -1
  6. package/dist/config-catalog.js +5 -3
  7. package/dist/config-center/types.d.ts +1 -1
  8. package/dist/config-invariants.d.ts +2 -2
  9. package/dist/config-invariants.js +17 -0
  10. package/dist/config-types.d.ts +44 -3
  11. package/dist/config-types.js +1 -0
  12. package/dist/config.js +24 -4
  13. package/dist/hosted-posture.d.ts +176 -0
  14. package/dist/hosted-posture.js +76 -0
  15. package/dist/http/resume-ctx-read.d.ts +40 -0
  16. package/dist/http/resume-ctx-read.js +25 -0
  17. package/dist/http/resume-legs.js +62 -42
  18. package/dist/http/routes/capabilities.js +3 -0
  19. package/dist/http/routes/images.js +12 -0
  20. package/dist/http/routes/runs.js +18 -4
  21. package/dist/http/routes/workflows.js +3 -2
  22. package/dist/http/wire-types.d.ts +13 -4
  23. package/dist/observability/corrupt-read-seat.d.ts +56 -0
  24. package/dist/observability/corrupt-read-seat.js +16 -0
  25. package/dist/observability/fail-open.d.ts +4 -0
  26. package/dist/observability/fail-open.js +4 -0
  27. package/dist/orchestration/workflow-journal-entry.d.ts +24 -6
  28. package/dist/orchestration/workflow-journal-entry.js +17 -10
  29. package/dist/plugins/approval-ask-store-sql.js +2 -2
  30. package/dist/plugins/checkpoint-store-sql.d.ts +16 -3
  31. package/dist/plugins/checkpoint-store-sql.js +7 -9
  32. package/dist/plugins/image-bake-store-sql.js +85 -13
  33. package/dist/plugins/image-index-sql.js +75 -12
  34. package/dist/plugins/local-checkpoint-store.d.ts +23 -2
  35. package/dist/plugins/local-checkpoint-store.js +40 -33
  36. package/dist/plugins/local-session-store.d.ts +14 -0
  37. package/dist/plugins/local-session-store.js +12 -7
  38. package/dist/plugins/local-sidecar-quarantine.d.ts +10 -0
  39. package/dist/plugins/local-sidecar-quarantine.js +15 -0
  40. package/dist/plugins/local-task-attachment-store.d.ts +1 -1
  41. package/dist/plugins/local-task-attachment-store.js +5 -2
  42. package/dist/plugins/mailbox-store-sql.d.ts +6 -3
  43. package/dist/plugins/mailbox-store-sql.js +45 -43
  44. package/dist/plugins/memory-embedder-fingerprint.js +8 -12
  45. package/dist/plugins/memory-engine-pg.js +34 -16
  46. package/dist/plugins/memory-engine-tidb.js +19 -12
  47. package/dist/plugins/memory-engine-vector-util.d.ts +26 -0
  48. package/dist/plugins/memory-engine-vector-util.js +3 -0
  49. package/dist/plugins/memory-sync-store-pg.d.ts +6 -0
  50. package/dist/plugins/memory-sync-store-pg.js +14 -10
  51. package/dist/plugins/memory-sync-store-tidb.js +7 -11
  52. package/dist/plugins/pg-session-storage.js +26 -28
  53. package/dist/plugins/session-event-payload.d.ts +37 -0
  54. package/dist/plugins/session-event-payload.js +20 -0
  55. package/dist/plugins/sql-json-column.d.ts +124 -22
  56. package/dist/plugins/sql-json-column.js +22 -1
  57. package/dist/plugins/sql-row-helpers.d.ts +10 -16
  58. package/dist/plugins/sql-row-helpers.js +0 -29
  59. package/dist/plugins/store-backend.js +10 -12
  60. package/dist/plugins/store-contracts.d.ts +4 -6
  61. package/dist/plugins/store-contracts.js +0 -60
  62. package/dist/plugins/task-attachment-store.d.ts +21 -12
  63. package/dist/plugins/task-attachment-store.js +12 -0
  64. package/dist/plugins/tidb-session-storage.d.ts +2 -2
  65. package/dist/plugins/tidb-session-storage.js +12 -11
  66. package/dist/plugins/tidb-session-store.d.ts +2 -2
  67. package/dist/plugins/tidb-session-store.js +20 -22
  68. package/dist/plugins/usage-window-store-sql.d.ts +9 -1
  69. package/dist/plugins/usage-window-store-sql.js +23 -9
  70. package/dist/plugins/workflow-journal-store-sql.js +16 -4
  71. package/dist/run-local.js +4 -10
  72. package/dist/usage-analytics.d.ts +14 -1
  73. package/dist/usage-analytics.js +11 -17
  74. package/package.json +2 -2
@@ -1,4 +1,6 @@
1
1
  import { jaccardDistance, termSet } from "@sema-agent/core";
2
+ import { readFrontmatterColumn } from "./memory-engine-vector-util.js";
3
+ import { decodeJsonColumn, foldJsonColumn, jsonColumnText } from "./sql-json-column.js";
2
4
  import { computeEntryRev, serializeEntryFile } from "@sema-agent/core";
3
5
  import { pgHasUnstorable } from "./pg-safe-json.js";
4
6
  import { assertMemoryScopeWidth, memoryEntryKeyWidthRefusal } from "./memory-key-guards.js";
@@ -41,8 +43,13 @@ function isPrimaryKeyDup(err) {
41
43
  function inPlaceholders(count) {
42
44
  return Array.from({ length: count }, () => "?").join(", ");
43
45
  }
44
- function fromJson(v) {
45
- return (typeof v === "string" ? JSON.parse(v) : v);
46
+ function readFrontmatter(cell, id) {
47
+ try {
48
+ return decodeJsonColumn(cell, readFrontmatterColumn, `${TIDB_MEMORY_ENGINE_TABLES.entry}.frontmatter for entry ${String(id)}`);
49
+ }
50
+ catch (e) {
51
+ throw foldJsonColumn(e, (m) => new Error(`memory-engine: ${m} — refusing rather than serve a note whose frontmatter (the origin/exposure law carrier) nobody can read`));
52
+ }
46
53
  }
47
54
  export class TiDBMemoryEngineBackend {
48
55
  pool;
@@ -72,7 +79,7 @@ export class TiDBMemoryEngineBackend {
72
79
  return `${fm.name ?? slug} ${fm.description ?? ""} ${body}`;
73
80
  }
74
81
  headerFromRow(r) {
75
- const fm = fromJson(r.frontmatter);
82
+ const fm = readFrontmatter(r.frontmatter, r.id);
76
83
  return {
77
84
  id: r.id,
78
85
  slug: r.slug,
@@ -92,7 +99,7 @@ export class TiDBMemoryEngineBackend {
92
99
  id: r.id,
93
100
  slug: r.slug,
94
101
  scope: r.scope,
95
- frontmatter: fromJson(r.frontmatter),
102
+ frontmatter: readFrontmatter(r.frontmatter, r.id),
96
103
  body: r.body,
97
104
  rev: r.rev,
98
105
  };
@@ -101,14 +108,14 @@ export class TiDBMemoryEngineBackend {
101
108
  const uniq = [...new Set(scopes)];
102
109
  if (uniq.length === 0)
103
110
  return [];
104
- 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);
111
+ const rows = await this.selectRows(`SELECT id, scope, slug, ${jsonColumnText("tidb", "frontmatter")}, rev, mtime_ms, size_bytes FROM ${TIDB_MEMORY_ENGINE_TABLES.entry} WHERE scope IN (${inPlaceholders(uniq.length)}) ORDER BY id`, uniq);
105
112
  return rows.map((r) => this.headerFromRow(r));
106
113
  }
107
114
  async getByIds(ids) {
108
115
  const uniq = [...new Set(ids)];
109
116
  if (uniq.length === 0)
110
117
  return [];
111
- 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);
118
+ const rows = await this.selectRows(`SELECT id, scope, slug, ${jsonColumnText("tidb", "frontmatter")}, body, rev, mtime_ms, size_bytes FROM ${TIDB_MEMORY_ENGINE_TABLES.entry} WHERE id IN (${inPlaceholders(uniq.length)}) ORDER BY id`, uniq);
112
119
  return rows.map((r) => this.entryFromRow(r));
113
120
  }
114
121
  async search(query, scopes, opts) {
@@ -121,11 +128,11 @@ export class TiDBMemoryEngineBackend {
121
128
  if (q.size === 0)
122
129
  return [];
123
130
  const uniq = [...new Set(scopes)];
124
- 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);
131
+ const rows = await this.selectRows(`SELECT id, scope, slug, ${jsonColumnText("tidb", "frontmatter")}, body, rev, mtime_ms, size_bytes FROM ${TIDB_MEMORY_ENGINE_TABLES.entry} WHERE scope IN (${inPlaceholders(uniq.length)})`, uniq);
125
132
  const scored = [];
126
133
  for (const raw of rows) {
127
134
  const r = raw;
128
- const fm = fromJson(r.frontmatter);
135
+ const fm = readFrontmatter(r.frontmatter, r.id);
129
136
  const d = jaccardDistance(q, this.haystackOf(r.slug, fm, r.body));
130
137
  if (d === null)
131
138
  continue;
@@ -203,11 +210,11 @@ export class TiDBMemoryEngineBackend {
203
210
  report.conflicts.push({ op: "add", id: entry.id, reason: "unstorable_bytes (utf8mb4 cannot store NUL/lone surrogates without mangling; strip them at the source — the store never rewrites content)" });
204
211
  return;
205
212
  }
206
- const prior = await this.selectRows(`SELECT scope, slug, rev, frontmatter FROM ${T} WHERE id = ?`, [entry.id]);
213
+ const prior = await this.selectRows(`SELECT scope, slug, rev, ${jsonColumnText("tidb", "frontmatter")} FROM ${T} WHERE id = ?`, [entry.id]);
207
214
  const priorRow = prior.length > 0 ? prior[0] : undefined;
208
215
  let existsById = priorRow !== undefined;
209
216
  {
210
- const committedBaseFm = rememberBatchCommitted(originBaseline, entry.id, priorRow !== undefined ? fromJson(priorRow.frontmatter) : undefined);
217
+ const committedBaseFm = rememberBatchCommitted(originBaseline, entry.id, priorRow !== undefined ? readFrontmatter(priorRow.frontmatter, entry.id) : undefined);
211
218
  const refusal = originWhitewashRefusal("add", committedBaseFm, entry.frontmatter);
212
219
  if (refusal !== undefined) {
213
220
  report.conflicts.push({ op: "add", id: entry.id, reason: refusal });
@@ -288,7 +295,7 @@ export class TiDBMemoryEngineBackend {
288
295
  slug = `${entry.slug}-${n}`;
289
296
  }
290
297
  }
291
- const cur = await this.selectRows(`SELECT slug, rev, scope, frontmatter FROM ${T} WHERE id = ?`, [patch.id]);
298
+ const cur = await this.selectRows(`SELECT slug, rev, scope, ${jsonColumnText("tidb", "frontmatter")} FROM ${T} WHERE id = ?`, [patch.id]);
292
299
  if (cur.length === 0) {
293
300
  report.conflicts.push({ op: patch.op, id: patch.id, reason: "unknown id (entry not found in backend)" });
294
301
  return;
@@ -296,7 +303,7 @@ export class TiDBMemoryEngineBackend {
296
303
  const currentRev = String(cur[0].rev);
297
304
  const currentSlug = String(cur[0].slug);
298
305
  const currentScope = String(cur[0].scope);
299
- const committedFm = fromJson(cur[0].frontmatter);
306
+ const committedFm = readFrontmatter(cur[0].frontmatter, patch.id);
300
307
  const committedBaseFm = rememberBatchCommitted(originBaseline, patch.id, committedFm);
301
308
  if (patch.op === "update" && patch.entry !== undefined) {
302
309
  if (committedFm.provenance?.kind === "repo_file") {
@@ -1,2 +1,28 @@
1
+ /** memory-engine 方言实现共用的纯工具函数。三个距离/词集函数(termSet/jaccardDistance/cosineDistance)
2
+ * 在 core(1.268.0 起公开导出,单一语义源)——消费者直引 core([ref] 兼容面全清:此处转口已删)。
3
+ * `isUniqueViolation` 重新内联:它当年随 core 的 pg **示例适配器**导出面走,core 1.424 按 clay 包卫生令
4
+ * 把整个 examples 面退出 npm 包([ref] BREAKING)⇒ re-export 断源。原为逐字抄自 core 1.423
5
+ * `examples/adapters/pg-adapter.js`(纯谓词,无 byte-equal 契约约束);[ref] P1-① 后**码面判据**
6
+ * 改走属主 `sql-errors.ts`,只留 message 补充臂(理由在函数体内)。 */
7
+ import type { MemoryEntryFrontmatter } from "@sema-agent/core";
8
+ import { type JsonColumnReader } from "./sql-json-column.js";
1
9
  export declare function isUniqueViolation(err: unknown): boolean;
10
+ /**
11
+ * `agent_memory_engine_entry.frontmatter` 的**形读者**。
12
+ *
13
+ * 🔴 判据只到「一只 JSON 对象」为止:键表属主在 core(`MemoryEntryFrontmatter`),而 core 没有导出
14
+ * 验证器 —— 在这里手写一份等价 zod 就是设计稿明令禁止的第二份键集。这一条最小判据已经把坏体四形全关在
15
+ * 门外,而此前这里是**零判据**(裸 `JSON.parse` + `as MemoryEntryFrontmatter`)。
16
+ *
17
+ * 🔴 失败方向 = **拒**,而且这一列不是普通载荷:`frontmatter` 是 **origin/exposure 不可变律**的载体
18
+ * (`committedOriginOf` / `exposureBandOf` / `distilledCarriage` 全读它)。一行读不出形的 frontmatter 被
19
+ * 当成 `{}` 端出去 = 那条笔记的 external-origin 标记与曝光带**静默消失** —— 洗白检查(`originWhitewashRefusal`)
20
+ * 拿到的「已提交基线」于是是空的,一次真的洗白会被放过。
21
+ */
22
+ export declare const readFrontmatterColumn: JsonColumnReader<MemoryEntryFrontmatter>;
23
+ /**
24
+ * `agent_memory_engine_entry.embedding` 的**形读者** = 本店既有的那一条判据(`Array.isArray`)。
25
+ * 维数判据仍在调用点(它要跟 embedder 的 `dimensions` 比,那是运行期事实,不是列的形)。
26
+ */
27
+ export declare const readEmbeddingColumn: JsonColumnReader<unknown[]>;
2
28
  //# sourceMappingURL=memory-engine-vector-util.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { isPgUniqueViolation } from "./sql-errors.js";
2
+ import { createObjectColumnReader, readJsonArrayColumn } from "./sql-json-column.js";
2
3
  export function isUniqueViolation(err) {
3
4
  if (isPgUniqueViolation(err))
4
5
  return true;
@@ -6,4 +7,6 @@ export function isUniqueViolation(err) {
6
7
  return false;
7
8
  return /duplicate key|unique constraint|already exists/i.test(String(err.message ?? ""));
8
9
  }
10
+ export const readFrontmatterColumn = createObjectColumnReader();
11
+ export const readEmbeddingColumn = readJsonArrayColumn;
9
12
  //# sourceMappingURL=memory-engine-vector-util.js.map
@@ -50,6 +50,12 @@ export interface MemorySyncStore {
50
50
  export declare const MEMORY_SYNC_INDEXES: readonly IndexSpec[];
51
51
  /** Idempotent DDL for the sync plane (PG dialect). Call once at startup. */
52
52
  export declare function ensurePgMemorySyncSchema(query: PgQueryFn): Promise<void>;
53
+ /**
54
+ * 本店三只 JSON 承载列的**折码点**(唯一)—— 本店没有枚举式 corrupt 码,既有的「读不出来」口径就是
55
+ * 响亮抛(`claimLeaseMs` 守卫 / dup id 一律 fail-loud)⇒ 折成响亮裸 `Error`,点名 `<表>.<列>` + 行标识
56
+ * (S-550 车①② 裁定,沿用;不新铸码)。两个孪生共用本函数(MySQL 腿 import 它 —— 一条折码规则一个写者)。
57
+ */
58
+ export declare function refusingMemorySyncRow<T>(read: () => T): T;
53
59
  /** Options for {@link PgMemorySyncStore}. */
54
60
  export interface PgMemorySyncStoreOptions {
55
61
  /** How long a `claimDue` lease holds a row invisible before a crashed worker's claim self-heals.
@@ -1,6 +1,7 @@
1
1
  import { ensureIndexes } from "./ensure-index.js";
2
2
  import { pgSafeJsonStringify, pgProtocolJsonStringify } from "./pg-safe-json.js";
3
3
  import { assertMemoryScopeWidth } from "./memory-key-guards.js";
4
+ import { decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, jsonColumnText, readNonNullJsonValueColumn, readOpaqueJsonValueColumn, readStringRecordColumn, } from "./sql-json-column.js";
4
5
  export const PG_MEMORY_SYNC_TABLES = {
5
6
  syncCursor: "agent_memory_engine_sync_cursor",
6
7
  pushQueue: "agent_memory_engine_push_queue",
@@ -28,10 +29,13 @@ export async function ensurePgMemorySyncSchema(query) {
28
29
  )`);
29
30
  await ensureIndexes(query, "pg", MEMORY_SYNC_INDEXES);
30
31
  }
31
- function fromJsonb(v) {
32
- if (typeof v === "string")
33
- return JSON.parse(v);
34
- return v;
32
+ export function refusingMemorySyncRow(read) {
33
+ try {
34
+ return read();
35
+ }
36
+ catch (e) {
37
+ throw foldJsonColumn(e, (m) => new Error(`memory-sync store: ${m} — refusing rather than serve a sync row nobody can read`));
38
+ }
35
39
  }
36
40
  function toMs(v) {
37
41
  return typeof v === "number" ? v : Number(v);
@@ -48,14 +52,14 @@ export class PgMemorySyncStore {
48
52
  this.claimLeaseMs = lease;
49
53
  }
50
54
  async getCursor(scope, peer) {
51
- 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
+ const res = await this.query(`SELECT ${jsonColumnText("pg", "base_revs")}, updated_at_ms FROM ${PG_MEMORY_SYNC_TABLES.syncCursor} WHERE scope = $1 AND peer = $2`, [scope, peer]);
52
56
  const row = res.rows[0];
53
57
  if (!row)
54
58
  return undefined;
55
59
  return {
56
60
  scope,
57
61
  peer,
58
- baseRevs: fromJsonb(row.base_revs),
62
+ baseRevs: refusingMemorySyncRow(() => decodeJsonColumn(row.base_revs, readStringRecordColumn, `${PG_MEMORY_SYNC_TABLES.syncCursor}.base_revs for scope ${scope} peer ${peer}`)),
59
63
  updatedAtMs: toMs(row.updated_at_ms),
60
64
  };
61
65
  }
@@ -83,12 +87,12 @@ export class PgMemorySyncStore {
83
87
  )
84
88
  UPDATE ${PG_MEMORY_SYNC_TABLES.pushQueue} q SET next_attempt_at_ms = $3
85
89
  FROM due WHERE q.id = due.id
86
- RETURNING q.id, q.scope, q.entry_id, q.payload, q.attempts, q.last_error`, [nowMs, limit, leasedUntilMs]);
90
+ RETURNING q.id, q.scope, q.entry_id, ${jsonColumnText("pg", "payload")}, q.attempts, q.last_error`, [nowMs, limit, leasedUntilMs]);
87
91
  return res.rows.map((r) => ({
88
92
  id: String(r.id),
89
93
  scope: String(r.scope),
90
94
  entryId: String(r.entry_id),
91
- payload: fromJsonb(r.payload),
95
+ payload: refusingMemorySyncRow(() => decodeJsonColumn(r.payload, readOpaqueJsonValueColumn, `${PG_MEMORY_SYNC_TABLES.pushQueue}.payload for item ${String(r.id)}`)),
92
96
  attempts: toMs(r.attempts),
93
97
  lastError: r.last_error == null ? null : String(r.last_error),
94
98
  leasedUntilMs,
@@ -138,7 +142,7 @@ export class PgMemoryHistoryStore {
138
142
  ON CONFLICT (id, rev) DO NOTHING`, params);
139
143
  }
140
144
  async historyOf(id) {
141
- const res = await this.query(`SELECT id, rev, prev_rev, op, scope, snapshot, written_at_ms
145
+ const res = await this.query(`SELECT id, rev, prev_rev, op, scope, ${jsonColumnText("pg", "snapshot")}, written_at_ms
142
146
  FROM ${PG_MEMORY_SYNC_TABLES.history}
143
147
  WHERE id = $1
144
148
  ORDER BY written_at_ms ASC, rev ASC`, [id]);
@@ -156,7 +160,7 @@ export function historyRowFrom(r) {
156
160
  prevRev: r.prev_rev == null ? null : String(r.prev_rev),
157
161
  op: String(r.op),
158
162
  scope: String(r.scope),
159
- snapshot: r.snapshot == null ? null : fromJsonb(r.snapshot),
163
+ snapshot: refusingMemorySyncRow(() => decodeNullableJsonColumn(r.snapshot, readNonNullJsonValueColumn, `${PG_MEMORY_SYNC_TABLES.history}.snapshot for entry ${String(r.id)} rev ${String(r.rev)}`)) ?? null,
160
164
  writtenAtMs: toMs(r.written_at_ms),
161
165
  };
162
166
  }
@@ -1,4 +1,5 @@
1
- import { historyRowFrom } from "./memory-sync-store-pg.js";
1
+ import { historyRowFrom, refusingMemorySyncRow } from "./memory-sync-store-pg.js";
2
+ import { decodeJsonColumn, jsonColumnText, readOpaqueJsonValueColumn, readStringRecordColumn } from "./sql-json-column.js";
2
3
  import { ensureIndexes } from "./ensure-index.js";
3
4
  import { MEMORY_SYNC_INDEXES, MEMORY_HISTORY_INDEXES, PG_MEMORY_SYNC_TABLES } from "./memory-sync-store-pg.js";
4
5
  import { assertMemoryScopeWidth } from "./memory-key-guards.js";
@@ -30,11 +31,6 @@ export async function ensureTiDBMemorySyncSchema(pool) {
30
31
  ) COLLATE utf8mb4_bin`);
31
32
  await ensureIndexes((text, params) => pool.query(text, params), "mysql", MEMORY_SYNC_INDEXES);
32
33
  }
33
- function fromJson(v) {
34
- if (typeof v === "string")
35
- return JSON.parse(v);
36
- return v;
37
- }
38
34
  function toMs(v) {
39
35
  return typeof v === "number" ? v : Number(v);
40
36
  }
@@ -50,14 +46,14 @@ export class TiDBMemorySyncStore {
50
46
  this.claimLeaseMs = lease;
51
47
  }
52
48
  async getCursor(scope, peer) {
53
- const [rows] = await this.pool.query(`SELECT base_revs, updated_at_ms FROM ${TIDB_MEMORY_SYNC_TABLES.syncCursor} WHERE scope = ? AND peer = ?`, [scope, peer]);
49
+ const [rows] = await this.pool.query(`SELECT ${jsonColumnText("tidb", "base_revs")}, updated_at_ms FROM ${TIDB_MEMORY_SYNC_TABLES.syncCursor} WHERE scope = ? AND peer = ?`, [scope, peer]);
54
50
  const row = rows[0];
55
51
  if (!row)
56
52
  return undefined;
57
53
  return {
58
54
  scope,
59
55
  peer,
60
- baseRevs: fromJson(row.base_revs),
56
+ baseRevs: refusingMemorySyncRow(() => decodeJsonColumn(row.base_revs, readStringRecordColumn, `${TIDB_MEMORY_SYNC_TABLES.syncCursor}.base_revs for scope ${scope} peer ${peer}`)),
61
57
  updatedAtMs: toMs(row.updated_at_ms),
62
58
  };
63
59
  }
@@ -76,7 +72,7 @@ export class TiDBMemorySyncStore {
76
72
  if (limit <= 0)
77
73
  return [];
78
74
  const leasedUntilMs = nowMs + this.claimLeaseMs;
79
- const [candidates] = await this.pool.query(`SELECT id, scope, entry_id, payload, attempts, next_attempt_at_ms, last_error
75
+ const [candidates] = await this.pool.query(`SELECT id, scope, entry_id, ${jsonColumnText("tidb", "payload")}, attempts, next_attempt_at_ms, last_error
80
76
  FROM ${TIDB_MEMORY_SYNC_TABLES.pushQueue}
81
77
  WHERE next_attempt_at_ms <= ?
82
78
  ORDER BY next_attempt_at_ms, id
@@ -92,7 +88,7 @@ export class TiDBMemorySyncStore {
92
88
  id: row.id,
93
89
  scope: row.scope,
94
90
  entryId: row.entry_id,
95
- payload: fromJson(row.payload),
91
+ payload: refusingMemorySyncRow(() => decodeJsonColumn(row.payload, readOpaqueJsonValueColumn, `${TIDB_MEMORY_SYNC_TABLES.pushQueue}.payload for item ${row.id}`)),
96
92
  attempts: toMs(row.attempts),
97
93
  lastError: row.last_error == null ? null : String(row.last_error),
98
94
  leasedUntilMs,
@@ -141,7 +137,7 @@ export class TiDBMemoryHistoryStore {
141
137
  ON DUPLICATE KEY UPDATE id = id`, params);
142
138
  }
143
139
  async historyOf(id) {
144
- const [rows] = await this.pool.query(`SELECT id, rev, prev_rev, op, scope, snapshot, written_at_ms
140
+ const [rows] = await this.pool.query(`SELECT id, rev, prev_rev, op, scope, ${jsonColumnText("tidb", "snapshot")}, written_at_ms
145
141
  FROM ${TIDB_MEMORY_SYNC_TABLES.history}
146
142
  WHERE id = ?
147
143
  ORDER BY written_at_ms ASC, rev ASC`, [id]);
@@ -1,6 +1,8 @@
1
1
  import { emitLeafAdvance } from "../session-leaf-bus.js";
2
2
  import { BaseSessionStorage, StoredSession, SessionError, uuidv7, validateEntriesForImport, leafIdAfterEntry, normalizePromptEpoch, } from "@sema-agent/core";
3
3
  import { escapeLike } from "./sql-escape.js";
4
+ import { decodeSessionEventPayload } from "./session-event-payload.js";
5
+ import { jsonColumnText } from "./sql-json-column.js";
4
6
  import { pgSafeJsonStringify } from "./pg-safe-json.js";
5
7
  import { toIso } from "./sql-row-helpers.js";
6
8
  import { assertPlacementAdmission, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS, placedRowFromRow, placementFromRow, placementInsertValues, SQL_SESSION_PLACEMENTS, } from "./session-placement.js";
@@ -10,9 +12,6 @@ import { isPgUniqueViolation } from "./sql-errors.js";
10
12
  import { MANAGED_RETENTION } from "./retention-store-sql.js";
11
13
  import { ensureIndexes } from "./ensure-index.js";
12
14
  const isDupKey = isPgUniqueViolation;
13
- function parseEntry(p) {
14
- return (typeof p === "string" ? JSON.parse(p) : p);
15
- }
16
15
  export const PG_SESSION_SCHEMA = [
17
16
  `CREATE TABLE IF NOT EXISTS session_meta (
18
17
  session_id VARCHAR(64) COLLATE "C" NOT NULL,
@@ -74,11 +73,11 @@ export class PgSessionStorage extends BaseSessionStorage {
74
73
  return undefined;
75
74
  let cursor = this.floorSeq;
76
75
  for (;;) {
77
- const res = await this.pool.query("SELECT seq, payload FROM session_event WHERE session_id = $1 AND seq < $2 AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100", [this.sessionId, cursor]);
76
+ const res = await this.pool.query(`SELECT seq, ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 AND seq < $2 AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100`, [this.sessionId, cursor]);
78
77
  if (res.rows.length === 0)
79
78
  return undefined;
80
79
  for (const r of res.rows) {
81
- const e = parseEntry(r.payload);
80
+ const e = decodeSessionEventPayload(r.payload, this.sessionId, r.seq);
82
81
  const d = e.type === "prompt_epoch" ? normalizePromptEpoch(e) : normalizePromptEpoch(e.details?.promptEpoch);
83
82
  if (d)
84
83
  return d;
@@ -92,18 +91,18 @@ export class PgSessionStorage extends BaseSessionStorage {
92
91
  if (!meta)
93
92
  return null;
94
93
  const { floorEntryId, floorSeq } = await PgSessionStorage.computeFloor(pool, sessionId);
95
- const eventRes = await pool.query("SELECT payload FROM session_event WHERE session_id = $1 AND seq >= $2 ORDER BY seq ASC", [sessionId, floorSeq]);
96
- const entries = eventRes.rows.map((r) => parseEntry(r.payload));
94
+ const eventRes = await pool.query(`SELECT seq, ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 AND seq >= $2 ORDER BY seq ASC`, [sessionId, floorSeq]);
95
+ const entries = eventRes.rows.map((r) => decodeSessionEventPayload(r.payload, sessionId, r.seq));
97
96
  const placement = placementFromRow(meta);
98
97
  const metadata = { id: sessionId, createdAt: toIso(meta.created_at), ...(placement !== undefined ? { placement } : {}) };
99
98
  return new PgSessionStorage(pool, sessionId, metadata, entries, meta.leaf_id ?? null, floorEntryId, floorSeq);
100
99
  }
101
100
  static async computeFloor(pool, sessionId) {
102
- const compRes = await pool.query("SELECT payload FROM session_event WHERE session_id = $1 AND type = 'compaction' ORDER BY seq DESC LIMIT 1", [sessionId]);
101
+ const compRes = await pool.query(`SELECT ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 AND type = 'compaction' ORDER BY seq DESC LIMIT 1`, [sessionId]);
103
102
  if (!compRes.rows[0])
104
103
  return { floorEntryId: null, floorSeq: 0 };
105
- const comp = parseEntry(compRes.rows[0].payload);
106
- const fk = comp.firstKeptEntryId;
104
+ const comp = decodeSessionEventPayload(compRes.rows[0].payload, sessionId, "latest compaction");
105
+ const fk = comp.type === "compaction" ? comp.firstKeptEntryId : undefined;
107
106
  if (!fk)
108
107
  return { floorEntryId: null, floorSeq: 0 };
109
108
  const seqRes = await pool.query("SELECT seq FROM session_event WHERE session_id = $1 AND entry_id = $2", [sessionId, fk]);
@@ -115,11 +114,10 @@ export class PgSessionStorage extends BaseSessionStorage {
115
114
  const inMem = await super.getEntry(id);
116
115
  if (inMem)
117
116
  return inMem;
118
- const res = await this.pool.query("SELECT payload FROM session_event WHERE session_id = $1 AND entry_id = $2", [this.sessionId, id]);
119
- const p = res.rows[0]?.payload;
120
- if (p == null)
117
+ const res = await this.pool.query(`SELECT ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 AND entry_id = $2`, [this.sessionId, id]);
118
+ if (res.rows[0] === undefined)
121
119
  return undefined;
122
- return parseEntry(p);
120
+ return decodeSessionEventPayload(res.rows[0].payload, this.sessionId, id);
123
121
  }
124
122
  async appendEntry(entry, opts) {
125
123
  await this.persist(entry, opts);
@@ -355,7 +353,7 @@ export class PgSessionStore {
355
353
  if (!metaRes.rows[0])
356
354
  return null;
357
355
  const leafId = metaRes.rows[0].leaf_id ?? null;
358
- const eventRes = await this.pool.query("SELECT entry_id, parent_id, type, payload, ts FROM session_event WHERE session_id = $1 ORDER BY seq ASC", [sourceId]);
356
+ const eventRes = await this.pool.query(`SELECT entry_id, parent_id, type, ${jsonColumnText("pg", "payload")}, ts FROM session_event WHERE session_id = $1 ORDER BY seq ASC`, [sourceId]);
359
357
  const rows = eventRes.rows;
360
358
  const newId = uuidv7();
361
359
  const now = new Date();
@@ -364,7 +362,7 @@ export class PgSessionStore {
364
362
  await conn.query("BEGIN");
365
363
  for (let seq = 0; seq < rows.length; seq++) {
366
364
  const r = rows[seq];
367
- const payload = typeof r.payload === "string" ? r.payload : pgSafeJsonStringify(r.payload);
365
+ const payload = String(r.payload);
368
366
  await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7)", [newId, seq, r.entry_id, r.parent_id ?? null, r.type, payload, r.ts]);
369
367
  }
370
368
  await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES ($1,$2,$3,$4,$5,$6)", [newId, owner, now, now, leafId, rows.length]);
@@ -387,8 +385,8 @@ export class PgSessionStore {
387
385
  const metaRes = await this.pool.query("SELECT 1 FROM session_meta WHERE session_id = $1", [sessionId]);
388
386
  if (metaRes.rows.length === 0)
389
387
  return null;
390
- const eventRes = await this.pool.query("SELECT payload FROM session_event WHERE session_id = $1 ORDER BY seq ASC", [sessionId]);
391
- return eventRes.rows.map((r) => parseEntry(r.payload));
388
+ const eventRes = await this.pool.query(`SELECT seq, ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 ORDER BY seq ASC`, [sessionId]);
389
+ return eventRes.rows.map((r) => decodeSessionEventPayload(r.payload, sessionId, r.seq));
392
390
  }
393
391
  async listEntryIds(sessionId) {
394
392
  const metaRes = await this.pool.query("SELECT 1 FROM session_meta WHERE session_id = $1", [sessionId]);
@@ -406,9 +404,9 @@ export class PgSessionStore {
406
404
  async function* gen() {
407
405
  let cursor = opts?.afterSeq ?? -1;
408
406
  for (;;) {
409
- const res = await pool.query("SELECT seq, payload FROM session_event WHERE session_id = $1 AND seq > $2 ORDER BY seq ASC LIMIT $3", [sessionId, cursor, batchSize]);
407
+ const res = await pool.query(`SELECT seq, ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 AND seq > $2 ORDER BY seq ASC LIMIT $3`, [sessionId, cursor, batchSize]);
410
408
  for (const r of res.rows)
411
- yield parseEntry(r.payload);
409
+ yield decodeSessionEventPayload(r.payload, sessionId, r.seq);
412
410
  if (res.rows.length < batchSize)
413
411
  return;
414
412
  cursor = Number(res.rows[res.rows.length - 1].seq);
@@ -483,8 +481,8 @@ export class PgSessionStore {
483
481
  return new PgStagingHandle(this.pool, realSessionId, stagingIdFor(realSessionId, token));
484
482
  }
485
483
  async readStagedEntries(stagingId) {
486
- const res = await this.pool.query("SELECT payload FROM session_event WHERE session_id = $1 ORDER BY seq ASC", [stagingId]);
487
- return res.rows.map((r) => parseEntry(r.payload));
484
+ const res = await this.pool.query(`SELECT seq, ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 ORDER BY seq ASC`, [stagingId]);
485
+ return res.rows.map((r) => decodeSessionEventPayload(r.payload, stagingId, r.seq));
488
486
  }
489
487
  async sweepStagingSessions() {
490
488
  const cutoff = new Date(Date.now() - STAGING_GC_GRACE_MS);
@@ -579,11 +577,11 @@ class PgStagingHandle {
579
577
  throw new SyncConflictError(this.realSessionId, rel);
580
578
  }
581
579
  if (rel.relation === "fast_forward" && opts?.resolution !== "overwrite-dst" && dstIds !== null && dstIds.length > 0) {
582
- const dstFull = await conn.query("SELECT payload FROM session_event WHERE session_id = $1 ORDER BY seq ASC", [this.realSessionId]);
583
- const stagedFull = await conn.query("SELECT entry_id, payload FROM session_event WHERE session_id = $1 ORDER BY seq ASC", [this.stagingId]);
580
+ const dstFull = await conn.query(`SELECT seq, ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 ORDER BY seq ASC`, [this.realSessionId]);
581
+ const stagedFull = await conn.query(`SELECT entry_id, ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 ORDER BY seq ASC`, [this.stagingId]);
584
582
  const dstSet = new Set(dstIds);
585
- const dstEntries = dstFull.rows.map((r) => parseEntry(r.payload));
586
- const stagedShared = stagedFull.rows.filter((r) => dstSet.has(String(r.entry_id))).map((r) => parseEntry(r.payload));
583
+ const dstEntries = dstFull.rows.map((r) => decodeSessionEventPayload(r.payload, this.realSessionId, r.seq));
584
+ const stagedShared = stagedFull.rows.filter((r) => dstSet.has(String(r.entry_id))).map((r) => decodeSessionEventPayload(r.payload, this.stagingId, r.entry_id));
587
585
  const diverged = fastForwardSharedContentDiverged(stagedShared, dstEntries);
588
586
  if (diverged) {
589
587
  await conn.query("ROLLBACK");
@@ -592,10 +590,10 @@ class PgStagingHandle {
592
590
  }
593
591
  await conn.query("DELETE FROM session_event WHERE session_id = $1", [this.realSessionId]);
594
592
  await conn.query("UPDATE session_event SET session_id = $1 WHERE session_id = $2", [this.realSessionId, this.stagingId]);
595
- const lastRes = await conn.query("SELECT payload FROM session_event WHERE session_id = $1 ORDER BY seq DESC LIMIT 1", [this.realSessionId]);
593
+ const lastRes = await conn.query(`SELECT ${jsonColumnText("pg", "payload")} FROM session_event WHERE session_id = $1 ORDER BY seq DESC LIMIT 1`, [this.realSessionId]);
596
594
  let leafId = null;
597
595
  if (lastRes.rows[0])
598
- leafId = leafIdAfterEntry(parseEntry(lastRes.rows[0].payload));
596
+ leafId = leafIdAfterEntry(decodeSessionEventPayload(lastRes.rows[0].payload, this.realSessionId, "last staged row"));
599
597
  const now = new Date();
600
598
  await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES ($1,$2,$3,$4,$5,$6) " +
601
599
  "ON CONFLICT (session_id) DO UPDATE SET owner = EXCLUDED.owner, updated_at = EXCLUDED.updated_at, leaf_id = EXCLUDED.leaf_id, leaf_seq = EXCLUDED.leaf_seq", [this.realSessionId, owner, now, now, leafId, srcIds.length]);
@@ -0,0 +1,37 @@
1
+ /**
2
+ * **`session_event.payload` 这一列的单一属主**(S-550 车③a)—— 投影铸点、解码点、形读者各一份,三个
3
+ * 会话存储腿(`tidb-session-storage.ts` / `pg-session-storage.ts` / `tidb-session-store.ts`)共读本文件。
4
+ *
5
+ * ## 为什么必须收成一处
6
+ * 这一列此前有 **10 个** 各自写一遍的读法(`typeof p === "string" ? JSON.parse(p) : p as SessionTreeEntry`),
7
+ * 分布在三个文件的 wake / getEntry / getEpochAnchor / computeFloor / export / fork / import / 收尾 leaf
8
+ * 计算上。那个三元式正是 S-550 要消灭的歧义本身:**从值本身分不出驱动解过没解过**,而这一列在 MySQL 侧是
9
+ * `JSON`、PG 侧是 `JSONB` —— 一台 MariaDB 部署上它以文本到达(驱动不自动解码),一条被迁移脚本多
10
+ * `JSON.stringify` 过一次的行则在真 JSON 列上「串就再解一次」地被读成好体。十份拷贝里只要有一份将来
11
+ * 忘了跟,那一条 wake 就会静默读出半棵树。
12
+ *
13
+ * ## 失败方向 = 拒
14
+ * 会话事件日志是 **wake 的全部依据**:一行读不出形的 payload 被当成 `null` / 空对象端出去 ⇒ 这只会话的
15
+ * 上下文树里多一个没有 `id`、没有 `type` 的节点,`leafIdAfterEntry` 与 `buildContext` 在它上面的行为未定;
16
+ * 静默跳过它则是**一段对话从历史里消失**(而模型会照着一棵少了一截的树继续说话)。两者都比一次响亮的
17
+ * wake 失败坏。本店没有枚举式 corrupt 码(`SessionError` 的词表 —— `conflict` 等 —— 是并发契约词,
18
+ * 不是读侧 corrupt 词)⇒ 折成响亮裸 `Error`,点名 `<表>.<列>` + 行标识(S-550 车①② 裁定,沿用)。
19
+ */
20
+ import type { SessionTreeEntry } from "@sema-agent/core";
21
+ import { type JsonColumnReader } from "./sql-json-column.js";
22
+ /**
23
+ * 形读者:core 自己声明的 `SessionTreeEntryBase` 里那两把**不可省**的串键。
24
+ *
25
+ * 🚧 **如实声明判据的射程**:core 的 base 形是 `{type, id, parentId, timestamp}` 四键全必填,但 `parentId`
26
+ * 可为 `null`、而 `JSON.stringify` 会把一个 `undefined` 的键直接丢掉 —— 拿它当必填有把存量行判坏的风险。
27
+ * 所以判据取 `type` + `id` 两把 —— 它们是本仓读腿**自己**要用的那两把:`e.type === "prompt_epoch"`
28
+ * (epoch 锚点腿)与 core 的 `leafIdAfterEntry`(亲读 7.26.2 的 dist:
29
+ * `entry.type === "leaf" ? entry.targetId : entry.id`,两把都在里面)。而且它们进得了列就必定是串 ——
30
+ * 写腿把这两把各自**另外**落进一列 `NOT NULL`(`session_event.entry_id` / `.type`)。
31
+ * 这两条已经把坏体四形全关在门外;**不复刻 core 的变体键表**(那是设计稿明令禁止的第二份键集,
32
+ * core 也没有导出 `SessionTreeEntry` 的验证器 —— `validateEntriesForImport` 吃的是已经成形的对象)。
33
+ */
34
+ export declare const readSessionEventPayload: JsonColumnReader<SessionTreeEntry>;
35
+ /** 一格 `session_event.payload`(已按文本投影)→ 一条事件。坏体 ⇒ 响亮拒(见文件头注)。 */
36
+ export declare function decodeSessionEventPayload(cell: unknown, sessionId: string, rowId: unknown): SessionTreeEntry;
37
+ //# sourceMappingURL=session-event-payload.d.ts.map
@@ -0,0 +1,20 @@
1
+ import { decodeJsonColumn, foldJsonColumn } from "./sql-json-column.js";
2
+ export const readSessionEventPayload = (body) => {
3
+ if (typeof body !== "object" || body === null || Array.isArray(body))
4
+ return { problem: "expected a JSON object (a SessionTreeEntry)" };
5
+ const row = body;
6
+ if (typeof row.id !== "string")
7
+ return { problem: "the entry carries no string `id`" };
8
+ if (typeof row.type !== "string")
9
+ return { problem: "the entry carries no string `type`" };
10
+ return { value: body };
11
+ };
12
+ export function decodeSessionEventPayload(cell, sessionId, rowId) {
13
+ try {
14
+ return decodeJsonColumn(cell, readSessionEventPayload, `session_event.payload for session ${sessionId} row ${String(rowId)}`);
15
+ }
16
+ catch (e) {
17
+ throw foldJsonColumn(e, (m) => new Error(`session storage: ${m} — refusing rather than wake a session from a tree with a node nobody can read`));
18
+ }
19
+ }
20
+ //# sourceMappingURL=session-event-payload.js.map