@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.
Files changed (36) hide show
  1. package/dist/approval-card.d.ts +2 -2
  2. package/dist/approval-card.js +2 -2
  3. package/dist/approval-deny-reasons.d.ts +1 -1
  4. package/dist/approval-deny-reasons.js +1 -1
  5. package/dist/approval-reconciler.js +2 -2
  6. package/dist/config-types.d.ts +1 -1
  7. package/dist/http/routes/runs.js +1 -1
  8. package/dist/http/server.js +6 -1
  9. package/dist/plugins/approval-ask-store-memory.d.ts +1 -1
  10. package/dist/plugins/approval-ask-store-memory.js +17 -17
  11. package/dist/plugins/approval-ask-store-sql.d.ts +12 -12
  12. package/dist/plugins/approval-ask-store-sql.js +58 -58
  13. package/dist/plugins/background-agent-store-sql.d.ts +1 -1
  14. package/dist/plugins/background-agent-store-sql.js +2 -2
  15. package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
  16. package/dist/plugins/checkpoint-store-sql.js +6 -6
  17. package/dist/plugins/mailbox-store-sql.d.ts +2 -2
  18. package/dist/plugins/mailbox-store-sql.js +2 -2
  19. package/dist/plugins/memory-engine-pg.d.ts +2 -2
  20. package/dist/plugins/memory-engine-pg.js +17 -17
  21. package/dist/plugins/memory-engine-tidb.d.ts +2 -2
  22. package/dist/plugins/memory-engine-tidb.js +16 -16
  23. package/dist/plugins/memory-sync-store-pg.d.ts +2 -2
  24. package/dist/plugins/memory-sync-store-pg.js +19 -19
  25. package/dist/plugins/memory-sync-store-tidb.d.ts +1 -1
  26. package/dist/plugins/memory-sync-store-tidb.js +21 -21
  27. package/dist/plugins/pg-pool.js +26 -26
  28. package/dist/plugins/task-list-store-sql.d.ts +1 -1
  29. package/dist/plugins/task-list-store-sql.js +11 -11
  30. package/dist/plugins/tidb-pool.js +26 -26
  31. package/dist/plugins/workflow-journal-store-sql.d.ts +1 -1
  32. package/dist/plugins/workflow-journal-store-sql.js +4 -4
  33. package/dist/plugins/workflow-run-store-sql.d.ts +1 -1
  34. package/dist/plugins/workflow-run-store-sql.js +9 -9
  35. package/dist/security.js +9 -1
  36. package/package.json +2 -2
@@ -2,7 +2,7 @@
2
2
  import { assertJsonMetadata } from "@sema-agent/core";
3
3
  import { pgHasUnstorable, pgSafeJsonStringify, PgUnstorableError } from "./pg-safe-json.js";
4
4
  export const TASK_LIST_META_TABLE = "task_list_meta";
5
- export const TASK_LIST_ITEMS_TABLE = "task_list_items";
5
+ export const TASK_LIST_ITEM_TABLE = "task_list_item";
6
6
  /** JS 对象 integer-like(array-index)键判別——file 参照的遍历序里数字升序的那一族:canonical
7
7
  * 十进制、0 ≤ n < 2^32-1。"01"/"1e3"/负数/超界 = 普通串键(插入序殿后)。 */
8
8
  function arrayIndexOf(id) {
@@ -30,7 +30,7 @@ export async function ensureTiDBTaskListSchema(pool) {
30
30
  next_sort BIGINT NOT NULL,
31
31
  PRIMARY KEY (list_key)
32
32
  ) COLLATE utf8mb4_bin`);
33
- await pool.query(`CREATE TABLE IF NOT EXISTS ${TASK_LIST_ITEMS_TABLE} (
33
+ await pool.query(`CREATE TABLE IF NOT EXISTS ${TASK_LIST_ITEM_TABLE} (
34
34
  list_key VARBINARY(190) NOT NULL,
35
35
  id VARBINARY(190) NOT NULL,
36
36
  id_num BIGINT NULL,
@@ -46,7 +46,7 @@ export async function ensurePgTaskListSchema(q) {
46
46
  next_sort BIGINT NOT NULL,
47
47
  PRIMARY KEY (list_key)
48
48
  )`);
49
- await q(`CREATE TABLE IF NOT EXISTS ${TASK_LIST_ITEMS_TABLE} (
49
+ await q(`CREATE TABLE IF NOT EXISTS ${TASK_LIST_ITEM_TABLE} (
50
50
  list_key VARCHAR(190) COLLATE "C" NOT NULL,
51
51
  id VARCHAR(190) COLLATE "C" NOT NULL,
52
52
  id_num BIGINT,
@@ -83,9 +83,9 @@ async function setOn(exec, dialect, listKey, id, itemJson) {
83
83
  : `SELECT next_sort FROM ${TASK_LIST_META_TABLE} WHERE list_key = $1 FOR UPDATE`, [listKey]);
84
84
  const sort = Number(rows[0].next_sort);
85
85
  await exec(dialect === "tidb"
86
- ? `INSERT INTO ${TASK_LIST_ITEMS_TABLE} (list_key, id, id_num, sort_seq, item_json) VALUES (?, ?, ?, ?, ?)
86
+ ? `INSERT INTO ${TASK_LIST_ITEM_TABLE} (list_key, id, id_num, sort_seq, item_json) VALUES (?, ?, ?, ?, ?)
87
87
  ON DUPLICATE KEY UPDATE item_json = VALUES(item_json)`
88
- : `INSERT INTO ${TASK_LIST_ITEMS_TABLE} (list_key, id, id_num, sort_seq, item_json) VALUES ($1, $2, $3, $4, $5)
88
+ : `INSERT INTO ${TASK_LIST_ITEM_TABLE} (list_key, id, id_num, sort_seq, item_json) VALUES ($1, $2, $3, $4, $5)
89
89
  ON CONFLICT (list_key, id) DO UPDATE SET item_json = EXCLUDED.item_json`, [listKey, id, arrayIndexOf(id), sort, itemJson]);
90
90
  await exec(dialect === "tidb"
91
91
  ? `UPDATE ${TASK_LIST_META_TABLE} SET next_sort = ? WHERE list_key = ?`
@@ -118,7 +118,7 @@ export function createTiDBTaskListStore(pool, listKey) {
118
118
  // 套件抓获:8 并发 increment 终值 1)。MySQL 的快照点(首次快照读)不同但同修;顶层单语句无此臂。
119
119
  const build = (q, inTx) => ({
120
120
  get: async (id) => {
121
- const [rows] = (await q.query(`SELECT item_json FROM ${TASK_LIST_ITEMS_TABLE} WHERE list_key = ? AND id = ?${inTx ? " FOR UPDATE" : ""}`, [listKey, id]));
121
+ const [rows] = (await q.query(`SELECT item_json FROM ${TASK_LIST_ITEM_TABLE} WHERE list_key = ? AND id = ?${inTx ? " FOR UPDATE" : ""}`, [listKey, id]));
122
122
  return rows.length > 0 ? parseItem(rows[0].item_json) : undefined;
123
123
  },
124
124
  set: async (id, item) => {
@@ -128,11 +128,11 @@ export function createTiDBTaskListStore(pool, listKey) {
128
128
  await (inTx ? setOn(execOn(q), "tidb", listKey, id, json) : tx((c) => setOn(execOn(c), "tidb", listKey, id, json)));
129
129
  },
130
130
  delete: async (id) => {
131
- const [res] = (await q.query(`DELETE FROM ${TASK_LIST_ITEMS_TABLE} WHERE list_key = ? AND id = ?`, [listKey, id]));
131
+ const [res] = (await q.query(`DELETE FROM ${TASK_LIST_ITEM_TABLE} WHERE list_key = ? AND id = ?`, [listKey, id]));
132
132
  return res.affectedRows === 1;
133
133
  },
134
134
  list: async () => {
135
- const [rows] = (await q.query(`SELECT item_json FROM ${TASK_LIST_ITEMS_TABLE} WHERE list_key = ? ORDER BY id_num IS NULL, id_num, sort_seq${inTx ? " FOR UPDATE" : ""}`, [listKey]));
135
+ const [rows] = (await q.query(`SELECT item_json FROM ${TASK_LIST_ITEM_TABLE} WHERE list_key = ? ORDER BY id_num IS NULL, id_num, sort_seq${inTx ? " FOR UPDATE" : ""}`, [listKey]));
136
136
  return rows.map((r) => parseItem(r.item_json));
137
137
  },
138
138
  allocateId: async () => (inTx ? allocateOn(execOn(q), "tidb", listKey) : tx((c) => allocateOn(execOn(c), "tidb", listKey))),
@@ -177,7 +177,7 @@ export function createPgTaskListStore(pool, listKey) {
177
177
  // 双方言姿势,顺带把行锁真拿住)。
178
178
  const build = (q, inTx) => ({
179
179
  get: async (id) => {
180
- const { rows } = await q.query(`SELECT item_json FROM ${TASK_LIST_ITEMS_TABLE} WHERE list_key = $1 AND id = $2${inTx ? " FOR UPDATE" : ""}`, [listKey, id]);
180
+ const { rows } = await q.query(`SELECT item_json FROM ${TASK_LIST_ITEM_TABLE} WHERE list_key = $1 AND id = $2${inTx ? " FOR UPDATE" : ""}`, [listKey, id]);
181
181
  return rows.length > 0 ? parseItem(rows[0].item_json) : undefined;
182
182
  },
183
183
  set: async (id, item) => {
@@ -197,11 +197,11 @@ export function createPgTaskListStore(pool, listKey) {
197
197
  await (inTx ? setOn(execOn(q), "pg", listKey, id, json) : tx((c) => setOn(execOn(c), "pg", listKey, id, json)));
198
198
  },
199
199
  delete: async (id) => {
200
- const res = await q.query(`DELETE FROM ${TASK_LIST_ITEMS_TABLE} WHERE list_key = $1 AND id = $2`, [listKey, id]);
200
+ const res = await q.query(`DELETE FROM ${TASK_LIST_ITEM_TABLE} WHERE list_key = $1 AND id = $2`, [listKey, id]);
201
201
  return (res.rowCount ?? 0) === 1;
202
202
  },
203
203
  list: async () => {
204
- const { rows } = await q.query(`SELECT item_json FROM ${TASK_LIST_ITEMS_TABLE} WHERE list_key = $1 ORDER BY (id_num IS NULL), id_num, sort_seq${inTx ? " FOR UPDATE" : ""}`, [listKey]);
204
+ const { rows } = await q.query(`SELECT item_json FROM ${TASK_LIST_ITEM_TABLE} WHERE list_key = $1 ORDER BY (id_num IS NULL), id_num, sort_seq${inTx ? " FOR UPDATE" : ""}`, [listKey]);
205
205
  return rows.map((r) => parseItem(r.item_json));
206
206
  },
207
207
  allocateId: async () => (inTx ? allocateOn(execOn(q), "pg", listKey) : tx((c) => allocateOn(execOn(c), "pg", listKey))),
@@ -296,9 +296,9 @@ export const SCHEMA_STATEMENTS = [
296
296
  // Service-side resume rebuild inputs (DURABLE-CHECKPOINT-PLAN §4.4a): sessionId-keyed because core's
297
297
  // put(token,cp) can't carry service scenario_ctx. GC'd by the reaper once its checkpoint is gone.
298
298
  `CREATE TABLE IF NOT EXISTS checkpoint_ctx (
299
- session_id VARCHAR(190) NOT NULL,
300
- ctx JSON NOT NULL,
301
- updated_at BIGINT NOT NULL,
299
+ session_id VARCHAR(190) NOT NULL,
300
+ ctx JSON NOT NULL,
301
+ updated_at_ms BIGINT NOT NULL,
302
302
  PRIMARY KEY (session_id)
303
303
  ) COLLATE utf8mb4_bin`,
304
304
  // E18 resume-at anchor map: the shell holds live E2 eventIds (never persisted by core); TaskSpec.resumeAt takes a
@@ -409,18 +409,18 @@ export const SCHEMA_STATEMENTS = [
409
409
  // core's InMemoryWorkflowJournalStore (byOrdinal.set). LOAD-BEARING (a resume replays it), unlike the best-effort
410
410
  // task_event observation log. GC'd by the run reaper (deleteByRun). See workflow-journal-store-sql.ts.
411
411
  `CREATE TABLE IF NOT EXISTS workflow_journal (
412
- run_id VARCHAR(191) NOT NULL,
413
- ordinal INT NOT NULL,
412
+ run_id VARCHAR(191) NOT NULL,
413
+ ordinal INT NOT NULL,
414
414
  -- scope (SVC-2 / CORE-9 audit BLOCKER): the table predates scope — it was added when the
415
415
  -- WorkflowJournalStore seam grew a scope param for cross-tenant resume isolation. Every new append writes the
416
416
  -- run's REAL scope. EXISTING (pre-migration) rows carry '' = CROSS-SCOPE ORPHANS, harmless by construction: a
417
417
  -- real resume filters WHERE scope = <caller>, so the blank-scope residue never matches a tenant. The
418
418
  -- NOT NULL DEFAULT '' is what let the original ALTER succeed on a table that already had rows — keep the
419
419
  -- default so the "'' = orphan, never a tenant" reading stays true.
420
- scope VARCHAR(190) NOT NULL DEFAULT '',
421
- call_key VARCHAR(255) NOT NULL,
422
- result MEDIUMTEXT NOT NULL,
423
- created_at BIGINT NOT NULL,
420
+ scope VARCHAR(190) NOT NULL DEFAULT '',
421
+ call_key VARCHAR(255) NOT NULL,
422
+ result MEDIUMTEXT NOT NULL,
423
+ created_at_ms BIGINT NOT NULL,
424
424
  PRIMARY KEY (run_id, ordinal)
425
425
  ) COLLATE utf8mb4_bin`,
426
426
  // RB-242 / WF2 ([1981] core 拍板 a 形):cross-replica workflow resume admission lease. PK (source_run_id, scope)
@@ -437,36 +437,36 @@ export const SCHEMA_STATEMENTS = [
437
437
  // WorkflowRun as a JSON blob + extracted index columns; `rev` column = the OCC authority (blob rev overlaid
438
438
  // on read). MEDIUMTEXT (16MB) >> the 4MB write guard in the store.
439
439
  `CREATE TABLE IF NOT EXISTS workflow_run (
440
- id VARCHAR(191) NOT NULL,
441
- scope VARCHAR(190) NOT NULL,
442
- status VARCHAR(16) NOT NULL,
443
- run MEDIUMTEXT NOT NULL,
444
- rev INT NOT NULL DEFAULT 0,
445
- created_at BIGINT NOT NULL,
446
- ended_at BIGINT NULL,
440
+ id VARCHAR(191) NOT NULL,
441
+ scope VARCHAR(190) NOT NULL,
442
+ status VARCHAR(16) NOT NULL,
443
+ run MEDIUMTEXT NOT NULL,
444
+ rev INT NOT NULL DEFAULT 0,
445
+ created_at_ms BIGINT NOT NULL,
446
+ ended_at BIGINT NULL,
447
447
  PRIMARY KEY (id),
448
- KEY idx_wfrun_scope_created (scope, created_at),
448
+ KEY idx_wfrun_scope_created (scope, created_at_ms),
449
449
  KEY idx_wfrun_scope_status (scope, status)
450
450
  ) COLLATE utf8mb4_bin`,
451
451
  // P1: cross-replica workflow completion inbox (the push half). seq = insertion order (re-enqueue
452
452
  // moves to tail via DELETE+INSERT). summary is bounded upstream (core notifier ≤4000).
453
453
  `CREATE TABLE IF NOT EXISTS workflow_completion_inbox (
454
- seq BIGINT NOT NULL AUTO_INCREMENT,
455
- session_id VARCHAR(190) NOT NULL,
456
- run_id VARCHAR(191) NOT NULL,
457
- owner VARCHAR(190) NULL,
458
- status VARCHAR(16) NOT NULL,
459
- summary TEXT NOT NULL,
460
- enqueued_at BIGINT NOT NULL,
454
+ seq BIGINT NOT NULL AUTO_INCREMENT,
455
+ session_id VARCHAR(190) NOT NULL,
456
+ run_id VARCHAR(191) NOT NULL,
457
+ owner VARCHAR(190) NULL,
458
+ status VARCHAR(16) NOT NULL,
459
+ summary TEXT NOT NULL,
460
+ enqueued_at_ms BIGINT NOT NULL,
461
461
  -- kind (1.109): task_notification entries SHARE this inbox — kind discriminates the drain frame family.
462
462
  -- 🔴 NULL = a legacy workflow_complete row, AND it stays a LIVE value: the workflow_complete enqueue path
463
463
  -- never sets kind, so new rows are written NULL too. The read side has the matching compat branch —
464
464
  -- tidb-workflow-run-store.ts projects kind only when non-null and workflow-completion-inbox.ts routes
465
465
  -- kind === "task_notification" vs everything-else. Do NOT "tidy" this into NOT NULL DEFAULT.
466
- kind VARCHAR(24) NULL,
466
+ kind VARCHAR(24) NULL,
467
467
  -- payload: bounded + redacted JSON extras (task_type / result / …) for the task_notification family.
468
468
  -- NULL for legacy / plain workflow_complete rows.
469
- payload TEXT NULL,
469
+ payload TEXT NULL,
470
470
  PRIMARY KEY (session_id, run_id),
471
471
  UNIQUE KEY uq_wfinbox_seq (seq),
472
472
  KEY idx_wfinbox_session_seq (session_id, seq)
@@ -107,7 +107,7 @@ export declare class SqlWorkflowJournalStore implements WorkflowJournalStore {
107
107
  newRunId: string;
108
108
  scope: string;
109
109
  }): Promise<void>;
110
- /** GC — time-based sweep: purge journal entries older than `maxAgeMs` (created_at < now - maxAgeMs). The
110
+ /** GC — time-based sweep: purge journal entries older than `maxAgeMs` (created_at_ms < now - maxAgeMs). The
111
111
  * per-run deleteByRun has no production caller (a run-store reap gives no per-run hook), so this bounded sweep
112
112
  * — wired into the service reaper — is what keeps the heaviest table (TaskResult-bearing) from growing without
113
113
  * bound. A resume of a journal older than the retention window simply re-runs live (resume is an optimization).
@@ -29,11 +29,11 @@ export class SqlWorkflowJournalStore {
29
29
  // → its ordinal isn't cached, so a resume re-runs that one agent live (resume is an optimization).
30
30
  if (oversizeJournalResult(serialized))
31
31
  return;
32
- await this.db.query(this.q("INSERT INTO workflow_journal (run_id, ordinal, scope, call_key, result, created_at) VALUES (?,?,?,?,?,?) " +
32
+ await this.db.query(this.q("INSERT INTO workflow_journal (run_id, ordinal, scope, call_key, result, created_at_ms) VALUES (?,?,?,?,?,?) " +
33
33
  // scope NOT in the UPDATE: a re-append keeps the FIRST writer's scope (fail-closed — a re-append must never
34
34
  // reassign tenancy). The owner is the sole writer + runId is unique per scope, so this never fires
35
35
  // cross-scope in practice; the secure choice over matching InMemory's last-write-wins on scope.
36
- "ON DUPLICATE KEY UPDATE call_key = VALUES(call_key), result = VALUES(result)", "INSERT INTO workflow_journal (run_id, ordinal, scope, call_key, result, created_at) VALUES ($1,$2,$3,$4,$5,$6) " +
36
+ "ON DUPLICATE KEY UPDATE call_key = VALUES(call_key), result = VALUES(result)", "INSERT INTO workflow_journal (run_id, ordinal, scope, call_key, result, created_at_ms) VALUES ($1,$2,$3,$4,$5,$6) " +
37
37
  "ON CONFLICT (run_id, ordinal) DO UPDATE SET call_key = EXCLUDED.call_key, result = EXCLUDED.result"), [runId, callKeyOrdinal(entry.callKey), scope, entry.callKey, serialized, Date.now()]);
38
38
  }
39
39
  /** Entries for `runId` IF its recorded scope === `scope`, ASCENDING by ordinal; otherwise EMPTY. CORE-9 audit
@@ -108,13 +108,13 @@ export class SqlWorkflowJournalStore {
108
108
  async releaseResumeClaim(input) {
109
109
  await this.db.query(this.q("DELETE FROM workflow_resume_claim WHERE source_run_id = ? AND scope = ? AND new_run_id = ?", "DELETE FROM workflow_resume_claim WHERE source_run_id = $1 AND scope = $2 AND new_run_id = $3"), [input.sourceRunId, input.scope, input.newRunId]);
110
110
  }
111
- /** GC — time-based sweep: purge journal entries older than `maxAgeMs` (created_at < now - maxAgeMs). The
111
+ /** GC — time-based sweep: purge journal entries older than `maxAgeMs` (created_at_ms < now - maxAgeMs). The
112
112
  * per-run deleteByRun has no production caller (a run-store reap gives no per-run hook), so this bounded sweep
113
113
  * — wired into the service reaper — is what keeps the heaviest table (TaskResult-bearing) from growing without
114
114
  * bound. A resume of a journal older than the retention window simply re-runs live (resume is an optimization).
115
115
  * Idempotent; returns the rows purged. */
116
116
  async reapExpired(now, maxAgeMs) {
117
- const res = await this.db.query(this.q("DELETE FROM workflow_journal WHERE created_at < ?", "DELETE FROM workflow_journal WHERE created_at < $1"), [now - maxAgeMs]);
117
+ const res = await this.db.query(this.q("DELETE FROM workflow_journal WHERE created_at_ms < ?", "DELETE FROM workflow_journal WHERE created_at_ms < $1"), [now - maxAgeMs]);
118
118
  return res.affected;
119
119
  }
120
120
  }
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * ## WorkflowRunStore twin
14
14
  * One row per run: the full `WorkflowRun` as a JSON blob + EXTRACTED columns for everything SQL needs to
15
- * index/filter (scope, status, created_at, ended_at) + an authoritative `rev` column (the OCC key — the
15
+ * index/filter (scope, status, created_at_ms, ended_at) + an authoritative `rev` column (the OCC key — the
16
16
  * blob's own `rev` is OVERLAID from the column on every read, so writes never have to know the bumped value
17
17
  * up front). `update` is a single-statement CAS (`WHERE id AND scope [AND rev]`, `SET rev = rev + 1`);
18
18
  * `listByScope` projects через core's SHARED `summarizeWorkflowRun` (anti-drift — identical to InMemory/File).
@@ -46,7 +46,7 @@ export class SqlWorkflowRunStore {
46
46
  async put(id, run) {
47
47
  const stored = { ...run, id, rev: run.rev ?? 0 }; // key authoritative + observed-rev base (InMemory parity)
48
48
  try {
49
- await this.db.query(this.q("INSERT INTO workflow_run (id, scope, status, run, rev, created_at, ended_at) VALUES (?,?,?,?,?,?,?)", "INSERT INTO workflow_run (id, scope, status, run, rev, created_at, ended_at) VALUES ($1,$2,$3,$4,$5,$6,$7)"), [id, run.scope, run.status, JSON.stringify(stored), stored.rev, run.createdAt, run.endedAt ?? null]);
49
+ await this.db.query(this.q("INSERT INTO workflow_run (id, scope, status, run, rev, created_at_ms, ended_at) VALUES (?,?,?,?,?,?,?)", "INSERT INTO workflow_run (id, scope, status, run, rev, created_at_ms, ended_at) VALUES ($1,$2,$3,$4,$5,$6,$7)"), [id, run.scope, run.status, JSON.stringify(stored), stored.rev, run.createdAt, run.endedAt ?? null]);
50
50
  }
51
51
  catch (e) {
52
52
  // create-once error every backend throws (contract) — dup-key classification is dialect-specific:
@@ -132,7 +132,7 @@ export class SqlWorkflowRunStore {
132
132
  }
133
133
  }
134
134
  // Sort in SQL (newest first), summarize via core's SHARED projection (anti-drift).
135
- const { rows } = await this.db.query(`SELECT run, rev FROM workflow_run WHERE ${where} ORDER BY created_at DESC, id DESC${limit}`, params);
135
+ const { rows } = await this.db.query(`SELECT run, rev FROM workflow_run WHERE ${where} ORDER BY created_at_ms DESC, id DESC${limit}`, params);
136
136
  return rows.map((r) => {
137
137
  const run = JSON.parse(String(r.run));
138
138
  run.rev = Number(r.rev);
@@ -156,12 +156,12 @@ export class SqlWorkflowRunStore {
156
156
  return 0; // retention is always explicit
157
157
  // Cheap columns only; terminal-set + keep-N semantics computed in JS to match InMemory EXACTLY
158
158
  // (isTerminalWorkflowStatus is core's — no status list duplicated into SQL).
159
- const { rows } = await this.db.query(this.q("SELECT id, status, created_at, ended_at FROM workflow_run WHERE scope = ? ORDER BY created_at DESC, id DESC", "SELECT id, status, created_at, ended_at FROM workflow_run WHERE scope = $1 ORDER BY created_at DESC, id DESC"), [scope]);
159
+ const { rows } = await this.db.query(this.q("SELECT id, status, created_at_ms, ended_at FROM workflow_run WHERE scope = ? ORDER BY created_at_ms DESC, id DESC", "SELECT id, status, created_at_ms, ended_at FROM workflow_run WHERE scope = $1 ORDER BY created_at_ms DESC, id DESC"), [scope]);
160
160
  const terminal = rows.filter((r) => isTerminalWorkflowStatus(String(r.status)));
161
161
  const doomed = [];
162
162
  for (let i = 0; i < terminal.length; i++) {
163
163
  const r = terminal[i];
164
- const anchor = r.ended_at != null ? Number(r.ended_at) : Number(r.created_at);
164
+ const anchor = r.ended_at != null ? Number(r.ended_at) : Number(r.created_at_ms);
165
165
  const tooOld = opts.maxAgeMs !== undefined && anchor < now - opts.maxAgeMs;
166
166
  const overKeep = opts.keep !== undefined && i >= opts.keep;
167
167
  if (tooOld || overKeep)
@@ -214,9 +214,9 @@ export class SqlWorkflowCompletionInbox {
214
214
  // TiDB `REPLACE` = delete-conflicting-row + insert atomically, and omitting `seq` mints a fresh
215
215
  // AUTO_INCREMENT = tail. PG has no REPLACE, so the twin is an upsert that explicitly re-mints `seq` off the
216
216
  // BIGSERIAL's own sequence — same outcome (idempotent-on-runId + move-to-tail in ONE statement).
217
- await this.db.query(this.q("REPLACE INTO workflow_completion_inbox (session_id, run_id, owner, status, summary, enqueued_at, kind, payload) VALUES (?,?,?,?,?,?,?,?)", "INSERT INTO workflow_completion_inbox (session_id, run_id, owner, status, summary, enqueued_at, kind, payload) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) " +
217
+ await this.db.query(this.q("REPLACE INTO workflow_completion_inbox (session_id, run_id, owner, status, summary, enqueued_at_ms, kind, payload) VALUES (?,?,?,?,?,?,?,?)", "INSERT INTO workflow_completion_inbox (session_id, run_id, owner, status, summary, enqueued_at_ms, kind, payload) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) " +
218
218
  "ON CONFLICT (session_id, run_id) DO UPDATE SET seq = nextval(pg_get_serial_sequence('workflow_completion_inbox','seq')), " +
219
- "owner = EXCLUDED.owner, status = EXCLUDED.status, summary = EXCLUDED.summary, enqueued_at = EXCLUDED.enqueued_at, kind = EXCLUDED.kind, payload = EXCLUDED.payload"), [entry.sessionId, entry.runId, entry.owner, entry.status, entry.summary, entry.enqueuedAt, entry.kind ?? null, entry.payload ?? null]);
219
+ "owner = EXCLUDED.owner, status = EXCLUDED.status, summary = EXCLUDED.summary, enqueued_at_ms = EXCLUDED.enqueued_at_ms, kind = EXCLUDED.kind, payload = EXCLUDED.payload"), [entry.sessionId, entry.runId, entry.owner, entry.status, entry.summary, entry.enqueuedAt, entry.kind ?? null, entry.payload ?? null]);
220
220
  // 1.108 review (lens① MAJOR ×2): the File impl's check-then-set is atomic on one event loop; here the
221
221
  // fence check and the INSERT are two network statements, so a fence written IN BETWEEN (a poll served on
222
222
  // another replica, or a racing purge) would leave fence + pending row coexisting → the next drain re-pushes
@@ -254,9 +254,9 @@ export class SqlWorkflowCompletionInbox {
254
254
  // inbox does — markTerminalServed's fence-INSERT and ack-DELETE are two statements, and a drain on
255
255
  // another replica between them re-emitted an already-served completion (the in-memory pending()
256
256
  // filters by servedAt; this brings the SQL twin to the same semantics).
257
- const { rows } = await this.db.query(this.q("SELECT i.run_id, i.owner, i.status, i.summary, i.enqueued_at, i.kind, i.payload FROM workflow_completion_inbox i " +
257
+ const { rows } = await this.db.query(this.q("SELECT i.run_id, i.owner, i.status, i.summary, i.enqueued_at_ms, i.kind, i.payload FROM workflow_completion_inbox i " +
258
258
  "WHERE i.session_id = ? AND NOT EXISTS (SELECT 1 FROM workflow_inbox_fence f WHERE f.session_id = i.session_id AND f.kind = ? AND f.run_id IN (i.run_id, REGEXP_REPLACE(i.run_id, '#[0-9]+$', '')) AND f.until_ms >= ?) " +
259
- "ORDER BY i.seq ASC", "SELECT i.run_id, i.owner, i.status, i.summary, i.enqueued_at, i.kind, i.payload FROM workflow_completion_inbox i " +
259
+ "ORDER BY i.seq ASC", "SELECT i.run_id, i.owner, i.status, i.summary, i.enqueued_at_ms, i.kind, i.payload FROM workflow_completion_inbox i " +
260
260
  "WHERE i.session_id = $1 AND NOT EXISTS (SELECT 1 FROM workflow_inbox_fence f WHERE f.session_id = i.session_id AND f.kind = $2 AND f.run_id IN (i.run_id, regexp_replace(i.run_id, '#[0-9]+$', '')) AND f.until_ms >= $3) " +
261
261
  "ORDER BY i.seq ASC"), [sessionId, KIND_SERVED, Date.now()]);
262
262
  return rows.map((r) => ({
@@ -265,7 +265,7 @@ export class SqlWorkflowCompletionInbox {
265
265
  owner: r.owner === null ? null : String(r.owner),
266
266
  status: String(r.status),
267
267
  summary: String(r.summary),
268
- enqueuedAt: Number(r.enqueued_at),
268
+ enqueuedAt: Number(r.enqueued_at_ms),
269
269
  ...(r.kind != null ? { kind: String(r.kind) } : {}),
270
270
  ...(r.payload != null ? { payload: String(r.payload) } : {}),
271
271
  }));
package/dist/security.js CHANGED
@@ -175,7 +175,15 @@ export function createAuthorizer(config, sessionStore) {
175
175
  // omitted the header under requirePrincipal=false could attach to ANY owned session by id.
176
176
  // Anonymous callers may still attach to anonymous (owner=null) sessions, so single-tenant dev
177
177
  // (no principals anywhere) is unaffected; flipping the requirePrincipal default is NOT needed.
178
- if (owner != null && owner !== principal) {
178
+ //
179
+ // [3262] 存量哨兵窄互认([3260] cli live 取证→本仓裁 (a)):cli ≤1.0.70 对本地单用户自发
180
+ // `anon:shell-live` header,存量 session 的 owner 列全是这一个哨兵;cli 停发(表示法归一为
181
+ // 「缺席」,[3239] 复裁)后若仍逐字比对,缺席请求 attach 存量会话恒 401 —— 用户全部既存
182
+ // resume 即断。互认=同一主体(本地单用户)两个表示法的过渡承接,不是兼容臂:三条件缺一
183
+ // 不可(单机形 ∧ owner 恰为哨兵字面量 ∧ 请求 principal 缺席),带任何具名 principal 的请求
184
+ // 对哨兵会话仍走下方 403;多租形 header 必到,本臂不可达。存量随会话生命周期消亡后本臂可整删。
185
+ const legacyShellOwner = !config.requirePrincipal && owner === "anon:shell-live" && principal === undefined;
186
+ if (!legacyShellOwner && owner != null && owner !== principal) {
179
187
  throw principal
180
188
  ? new HttpError(403, "session does not belong to this principal")
181
189
  : new HttpError(401, `session is principal-owned; missing principal header '${config.principalHeader}'`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.7.1",
3
+ "version": "7.8.1",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -69,7 +69,7 @@
69
69
  "sharp": "^0.35.3"
70
70
  },
71
71
  "devDependencies": {
72
- "@sema-agent/sdk": "^6.10.0",
72
+ "@sema-agent/sdk": "^6.11.0",
73
73
  "@types/libsodium-wrappers": "^0.7.14",
74
74
  "@types/node": "22.10.2",
75
75
  "@types/pg": "^8.20.0",