@sema-agent/server 1.284.0 → 1.285.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/README.md CHANGED
@@ -77,6 +77,16 @@ model gateways, and cloud agent execution behind an HTTP/SSE contract.
77
77
 
78
78
  Requirements: Node ≥ 20 (npm path) and an OpenAI-compatible model gateway.
79
79
 
80
+ > **A note on the `glob@11` deprecation warning at install time.** `npm install` prints a deprecation
81
+ > warning for `glob@11.1.0`, pulled in transitively by `e2b` (the E2B sandbox SDK). It is **install-time
82
+ > noise with no runtime exposure here**: `glob` has exactly one load site inside `e2b` — a `dynamicImport`
83
+ > in its *template-build* file-packing path — and this server only ever touches E2B's *sandbox runtime*
84
+ > API. Verified by execution, not by reading: importing `e2b`, constructing the adapter and driving a real
85
+ > `exec` never puts `glob` in the module cache. We cannot silence it for you — npm `overrides` only apply
86
+ > when the package.json being read *is the project npm was invoked on*, so ours is ignored when this
87
+ > package is installed as a dependency. If the warning bothers you, add `"overrides": { "glob": "^13" }`
88
+ > to **your own** project's package.json (that is the one npm reads); the real fix is upstream in `e2b`.
89
+
80
90
  ```bash
81
91
  # A) npm
82
92
  npm install @sema-agent/server
@@ -105,6 +105,7 @@ export async function ensureTiDBBackgroundAgentSchema(pool) {
105
105
  session_scoped TINYINT NOT NULL,
106
106
  session_id VARCHAR(190) NULL,
107
107
  parent_session_id VARCHAR(190) NULL,
108
+ -- core 1.367 δ(additive):root 锚(listBySession 第二臂;NULL = 该记录无 root 锚,谓词天然拒)
108
109
  root_session_id VARCHAR(190) NULL,
109
110
  session_anchor VARCHAR(190) NULL,
110
111
  name VARCHAR(255) NULL,
@@ -115,6 +116,8 @@ export async function ensureTiDBBackgroundAgentSchema(pool) {
115
116
  settled_at_ms BIGINT NULL,
116
117
  usage_json TEXT NULL,
117
118
  rev BIGINT NOT NULL,
119
+ -- core 1.383([1565] / design/153 件3a)ε:updateIf 的 CAS 守卫**专用列**——守卫必须在库侧对当前
120
+ -- 行状态原子求值,不能只靠 record_json blob(那要求先读后写,读写之间就是 CAS 本要堵的竞态窗)。
118
121
  parked_checkpoint_token VARCHAR(190) NULL,
119
122
  park_claim_id VARCHAR(190) NULL,
120
123
  record_json LONGTEXT NOT NULL,
@@ -122,18 +125,6 @@ export async function ensureTiDBBackgroundAgentSchema(pool) {
122
125
  KEY idx_bga_anchor (scope_key, session_anchor, spawned_at_ms),
123
126
  KEY idx_bga_status (scope_key, status, updated_at_ms)
124
127
  ) COLLATE utf8mb4_bin`);
125
- const [rootCol] = (await pool.query(`SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = 'root_session_id'`, [BACKGROUND_AGENT_TABLE]));
126
- if (rootCol.length === 0) {
127
- await pool.query(`ALTER TABLE ${BACKGROUND_AGENT_TABLE} ADD COLUMN root_session_id VARCHAR(190) NULL AFTER parent_session_id`);
128
- }
129
- const [parkCols] = (await pool.query(`SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME IN ('parked_checkpoint_token', 'park_claim_id')`, [BACKGROUND_AGENT_TABLE]));
130
- const existingParkCols = new Set(parkCols.map((r) => r.COLUMN_NAME));
131
- if (!existingParkCols.has("parked_checkpoint_token")) {
132
- await pool.query(`ALTER TABLE ${BACKGROUND_AGENT_TABLE} ADD COLUMN parked_checkpoint_token VARCHAR(190) NULL`);
133
- }
134
- if (!existingParkCols.has("park_claim_id")) {
135
- await pool.query(`ALTER TABLE ${BACKGROUND_AGENT_TABLE} ADD COLUMN park_claim_id VARCHAR(190) NULL`);
136
- }
137
128
  }
138
129
  export async function ensurePgBackgroundAgentSchema(q) {
139
130
  await q(`CREATE TABLE IF NOT EXISTS ${BACKGROUND_AGENT_TABLE} (
@@ -144,6 +135,7 @@ export async function ensurePgBackgroundAgentSchema(q) {
144
135
  session_scoped BOOLEAN NOT NULL,
145
136
  session_id VARCHAR(190),
146
137
  parent_session_id VARCHAR(190),
138
+ -- core 1.367 δ(additive,TiDB 同案):root 锚(listBySession 第二臂)
147
139
  root_session_id VARCHAR(190),
148
140
  session_anchor VARCHAR(190),
149
141
  name VARCHAR(255),
@@ -154,6 +146,7 @@ export async function ensurePgBackgroundAgentSchema(q) {
154
146
  settled_at_ms BIGINT,
155
147
  usage_json TEXT,
156
148
  rev BIGINT NOT NULL,
149
+ -- core 1.383([1565] / design/153 件3a)ε(TiDB 同案注):updateIf 的 CAS 守卫专用列
157
150
  parked_checkpoint_token VARCHAR(190),
158
151
  park_claim_id VARCHAR(190),
159
152
  record_json TEXT NOT NULL,
@@ -161,9 +154,6 @@ export async function ensurePgBackgroundAgentSchema(q) {
161
154
  )`);
162
155
  await q(`CREATE INDEX IF NOT EXISTS idx_bga_anchor ON ${BACKGROUND_AGENT_TABLE} (scope_key, session_anchor, spawned_at_ms)`);
163
156
  await q(`CREATE INDEX IF NOT EXISTS idx_bga_status ON ${BACKGROUND_AGENT_TABLE} (scope_key, status, updated_at_ms)`);
164
- await q(`ALTER TABLE ${BACKGROUND_AGENT_TABLE} ADD COLUMN IF NOT EXISTS root_session_id VARCHAR(190)`);
165
- await q(`ALTER TABLE ${BACKGROUND_AGENT_TABLE} ADD COLUMN IF NOT EXISTS parked_checkpoint_token VARCHAR(190)`);
166
- await q(`ALTER TABLE ${BACKGROUND_AGENT_TABLE} ADD COLUMN IF NOT EXISTS park_claim_id VARCHAR(190)`);
167
157
  }
168
158
  export class TiDBBackgroundAgentStore {
169
159
  pool;
@@ -6,6 +6,9 @@ export const PG_APPROVAL_SCHEMA = [
6
6
  task_id VARCHAR(64),
7
7
  session_id VARCHAR(64),
8
8
  owner VARCHAR(190),
9
+ -- Single-DB fleet scope guard (tenant isolation). NULLABLE on purpose: a NULL-scope row is an untenanted
10
+ -- run, and every read/decide path matches it with \`scope IS NOT DISTINCT FROM $n\` (the PG twin of TiDB
11
+ -- \`<=>\`) so NULL matches only NULL and one tenant's scope NEVER matches another's.
9
12
  scope VARCHAR(190),
10
13
  tool_name VARCHAR(190) NOT NULL,
11
14
  args JSONB,
@@ -16,7 +19,6 @@ export const PG_APPROVAL_SCHEMA = [
16
19
  decided_at TIMESTAMPTZ(3),
17
20
  PRIMARY KEY (id)
18
21
  )`,
19
- `ALTER TABLE approval ADD COLUMN IF NOT EXISTS scope VARCHAR(190)`,
20
22
  `CREATE INDEX IF NOT EXISTS idx_approval_owner_status ON approval (owner, status)`,
21
23
  `CREATE INDEX IF NOT EXISTS idx_approval_status ON approval (status)`,
22
24
  `CREATE INDEX IF NOT EXISTS idx_approval_scope_status ON approval (scope, status)`,
@@ -22,11 +22,13 @@ export const PG_OUTCOME_LEDGER_SCHEMA = [
22
22
  outcome VARCHAR(8),
23
23
  llm_assisted JSONB,
24
24
  signature_inputs JSONB,
25
+ -- design/73 §1 bridge: the VERBATIM core TaskOutcome fact (oracleHadRedRun / status / oracle blob) as it
26
+ -- came off core, kept alongside the derived mechanical columns. NULL when the caller recorded through the
27
+ -- non-core \`record()\` path (no core fact to bridge).
25
28
  core_outcome JSONB,
26
29
  created_at TIMESTAMPTZ(3) NOT NULL,
27
30
  PRIMARY KEY (id)
28
31
  )`,
29
- `ALTER TABLE outcome_ledger ADD COLUMN IF NOT EXISTS core_outcome JSONB`,
30
32
  `CREATE INDEX IF NOT EXISTS idx_outcome_ledger_sig_model ON outcome_ledger (task_signature, model, run_status)`,
31
33
  `CREATE INDEX IF NOT EXISTS idx_outcome_ledger_outcome ON outcome_ledger (outcome)`,
32
34
  `CREATE INDEX IF NOT EXISTS idx_outcome_ledger_created ON outcome_ledger (created_at)`,
@@ -111,6 +111,9 @@ export const PG_SCHEMA_STATEMENTS = [
111
111
  `CREATE TABLE IF NOT EXISTS session_policy (
112
112
  policy_key CHAR(64) NOT NULL,
113
113
  session_id VARCHAR(64) NOT NULL,
114
+ -- 2c DENORMALIZED principal, carried solely so listBySession can ENUMERATE: policy_key is a one-way
115
+ -- sha256, so the principal is not recoverable from it. Enumeration-only — it is NOT part of the key and
116
+ -- NOT part of the CAS / tighten-only path. NULL = a session-wide (principal-less) policy row.
114
117
  principal VARCHAR(190),
115
118
  rules JSONB NOT NULL,
116
119
  rev BIGINT NOT NULL,
@@ -118,7 +121,6 @@ export const PG_SCHEMA_STATEMENTS = [
118
121
  updated_at TIMESTAMPTZ(3) NOT NULL,
119
122
  PRIMARY KEY (policy_key)
120
123
  )`,
121
- `ALTER TABLE session_policy ADD COLUMN IF NOT EXISTS principal VARCHAR(190)`,
122
124
  `CREATE INDEX IF NOT EXISTS idx_session_policy_session ON session_policy (session_id)`,
123
125
  `CREATE TABLE IF NOT EXISTS snapshot_manifest (
124
126
  scope VARCHAR(64) NOT NULL,
@@ -131,21 +133,30 @@ export const PG_SCHEMA_STATEMENTS = [
131
133
  `CREATE TABLE IF NOT EXISTS snapshot_blob (
132
134
  blob_hash CHAR(64) PRIMARY KEY,
133
135
  byte_len BIGINT NOT NULL,
136
+ -- \`bytes\` is NULLABLE and that nullability is LOAD-BEARING, not laxness: a SQL-backend row carries the
137
+ -- bytes inline; a MinIO-backend row keeps ONLY the INDEX (blob_hash, byte_len, created_at) here with
138
+ -- bytes NULL — the bytes themselves live in MinIO. The orphan/grace reference-tracking (gcOrphanBlobs /
139
+ -- reap / deleteBySession / sweepOrphanBlobs) reads snapshot_blob, so it works IDENTICALLY for BOTH
140
+ -- backends (and an E21 purge can delete the MinIO object). See blob-backend.ts MinioBlobBackend.
141
+ -- ⇒ NEVER re-add NOT NULL here: it would make the MinIO-backend index-only row un-insertable.
134
142
  bytes BYTEA NULL,
135
143
  created_at TIMESTAMPTZ(3) NOT NULL
136
144
  )`,
137
145
  `CREATE INDEX IF NOT EXISTS idx_snapshot_blob_created ON snapshot_blob (created_at)`,
138
- `ALTER TABLE snapshot_blob ALTER COLUMN bytes DROP NOT NULL`,
139
146
  `CREATE TABLE IF NOT EXISTS workflow_journal (
140
147
  run_id VARCHAR(191) NOT NULL,
141
148
  ordinal INT NOT NULL,
149
+ -- SVC-2 / CORE-9 audit BLOCKER: cross-tenant resume isolation (the WorkflowJournalStore seam takes a scope
150
+ -- param). NOT NULL DEFAULT '' — a row written without a scope lands in the '' bucket, which is
151
+ -- CROSS-SCOPE-ORPHANED but harmless: a real resume always filters \`WHERE scope = <caller>\`, so '' rows are
152
+ -- never handed to a tenant. Keep the DEFAULT: it is what makes a scope-less write land in that dead bucket
153
+ -- instead of failing or leaking into a real tenant's scope.
142
154
  scope VARCHAR(190) NOT NULL DEFAULT '',
143
155
  call_key VARCHAR(255) NOT NULL,
144
156
  result TEXT NOT NULL,
145
157
  created_at BIGINT NOT NULL,
146
158
  PRIMARY KEY (run_id, ordinal)
147
159
  )`,
148
- `ALTER TABLE workflow_journal ADD COLUMN IF NOT EXISTS scope VARCHAR(190) NOT NULL DEFAULT ''`,
149
160
  `CREATE TABLE IF NOT EXISTS workflow_run (
150
161
  id VARCHAR(191) NOT NULL,
151
162
  scope VARCHAR(190) NOT NULL,
@@ -166,13 +177,21 @@ export const PG_SCHEMA_STATEMENTS = [
166
177
  status VARCHAR(16) NOT NULL,
167
178
  summary TEXT NOT NULL,
168
179
  enqueued_at BIGINT NOT NULL,
180
+ -- 1.109 delivery envelope. kind NULL = a LEGACY \`workflow_complete\` row (the shape that predates the
181
+ -- envelope); readers must keep treating NULL as that kind, it is not "unknown". payload = the kind's
182
+ -- body, NULL for the legacy shape.
169
183
  kind VARCHAR(24),
170
184
  payload TEXT,
171
- PRIMARY KEY (session_id, run_id)
185
+ PRIMARY KEY (session_id, run_id),
186
+ -- Twin-alignment with TiDB's \`UNIQUE KEY uq_wfinbox_seq (seq)\` (same constraint name on purpose, so the two
187
+ -- schemas can be diffed by name). This is NOT a correctness fix: seq is BIGSERIAL, so it is already unique in
188
+ -- practice, and the only seq-keyed write — the overflow trim \`DELETE … WHERE session_id = $1 AND seq = ANY(…)\`
189
+ -- — is fenced by its session_id predicate, so a hypothetical duplicate seq could not cross sessions anyway.
190
+ -- What it buys: the implicit "BIGSERIAL is unique" assumption behind \`ORDER BY seq\` (the inbox's total order)
191
+ -- becomes MACHINE-ENFORCED rather than merely conventional, in both engines.
192
+ CONSTRAINT uq_wfinbox_seq UNIQUE (seq)
172
193
  )`,
173
194
  `CREATE INDEX IF NOT EXISTS idx_wfinbox_session_seq ON workflow_completion_inbox (session_id, seq)`,
174
- `ALTER TABLE workflow_completion_inbox ADD COLUMN IF NOT EXISTS kind VARCHAR(24)`,
175
- `ALTER TABLE workflow_completion_inbox ADD COLUMN IF NOT EXISTS payload TEXT`,
176
195
  `CREATE TABLE IF NOT EXISTS workflow_inbox_fence (
177
196
  session_id VARCHAR(190) NOT NULL,
178
197
  kind VARCHAR(8) NOT NULL,
@@ -23,11 +23,12 @@ export const PG_SESSION_SCHEMA = [
23
23
  updated_at TIMESTAMPTZ(3) NOT NULL,
24
24
  leaf_id VARCHAR(64),
25
25
  leaf_seq BIGINT NOT NULL DEFAULT 0,
26
+ -- Write-once auto-title (setTitleIfNull / probeTitle): NULL = "untitled", i.e. still claimable; the
27
+ -- three-state probe distinguishes it from "no such session". PG twin of the TiDB COLUMN_MIGRATIONS leg.
26
28
  title VARCHAR(120),
27
29
  PRIMARY KEY (session_id)
28
30
  )`,
29
31
  `CREATE INDEX IF NOT EXISTS idx_session_meta_owner ON session_meta (owner)`,
30
- `ALTER TABLE session_meta ADD COLUMN IF NOT EXISTS title VARCHAR(120)`,
31
32
  `CREATE TABLE IF NOT EXISTS session_event (
32
33
  session_id VARCHAR(64) NOT NULL,
33
34
  seq BIGINT NOT NULL,
@@ -27,27 +27,19 @@ export async function ensureTiDBRosterSchema(pool) {
27
27
  agent_id VARCHAR(190) NOT NULL,
28
28
  session_id VARCHAR(190) NULL,
29
29
  tool_use_id VARCHAR(190) NULL,
30
+ -- owner/scope:core 1.365 BREAKING 双轴必填(default-deny 谓词的两根轴;顶注 ① 记了曾 nullable 的由来)
30
31
  owner VARCHAR(190) NOT NULL,
31
32
  scope VARCHAR(190) NOT NULL,
32
33
  session_scoped TINYINT NULL,
34
+ -- core 1.367 δ(additive):RosterEntry.rootSessionId verbatim 存(枚举/恢复读者;谓词暂无臂)
33
35
  root_session_id VARCHAR(190) NULL,
36
+ -- core 1.373 additive:spawn 记录的解析后模型 id(NULL = 该 spawn 未记录模型)
34
37
  model VARCHAR(190) NULL,
35
38
  created_at_ms BIGINT NOT NULL,
36
39
  recorded_at_ms BIGINT NOT NULL,
37
40
  PRIMARY KEY (name_key, owner_key, scope_key),
38
41
  KEY idx_roster_agent (agent_id)
39
42
  ) COLLATE utf8mb4_bin`);
40
- await pool.query(`DELETE FROM ${ROSTER_TABLE} WHERE owner IS NULL OR scope IS NULL`);
41
- await pool.query(`ALTER TABLE ${ROSTER_TABLE} MODIFY COLUMN owner VARCHAR(190) NOT NULL`);
42
- await pool.query(`ALTER TABLE ${ROSTER_TABLE} MODIFY COLUMN scope VARCHAR(190) NOT NULL`);
43
- const [rootCol] = (await pool.query(`SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = 'root_session_id'`, [ROSTER_TABLE]));
44
- if (rootCol.length === 0) {
45
- await pool.query(`ALTER TABLE ${ROSTER_TABLE} ADD COLUMN root_session_id VARCHAR(190) NULL AFTER session_scoped`);
46
- }
47
- const [modelCol] = (await pool.query(`SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = 'model'`, [ROSTER_TABLE]));
48
- if (modelCol.length === 0) {
49
- await pool.query(`ALTER TABLE ${ROSTER_TABLE} ADD COLUMN model VARCHAR(190) NULL AFTER root_session_id`);
50
- }
51
43
  }
52
44
  export async function ensurePgRosterSchema(q) {
53
45
  await q(`CREATE TABLE IF NOT EXISTS ${ROSTER_TABLE} (
@@ -58,21 +50,19 @@ export async function ensurePgRosterSchema(q) {
58
50
  agent_id VARCHAR(190) NOT NULL,
59
51
  session_id VARCHAR(190),
60
52
  tool_use_id VARCHAR(190),
53
+ -- owner/scope:core 1.365 BREAKING 双轴必填(TiDB 同案;曾 nullable 的由来见两 ensure 上方顶注 ①)
61
54
  owner VARCHAR(190) COLLATE "C" NOT NULL,
62
55
  scope VARCHAR(190) COLLATE "C" NOT NULL,
63
56
  session_scoped BOOLEAN,
57
+ -- core 1.367 δ(additive,TiDB 同案):RosterEntry.rootSessionId verbatim 存
64
58
  root_session_id VARCHAR(190),
59
+ -- core 1.373 additive(TiDB 同案):解析后模型 id(NULL = 该 spawn 未记录模型)
65
60
  model VARCHAR(190),
66
61
  created_at_ms BIGINT NOT NULL,
67
62
  recorded_at_ms BIGINT NOT NULL,
68
63
  PRIMARY KEY (name_key, owner_key, scope_key)
69
64
  )`);
70
65
  await q(`CREATE INDEX IF NOT EXISTS idx_roster_agent ON ${ROSTER_TABLE} (agent_id)`);
71
- await q(`DELETE FROM ${ROSTER_TABLE} WHERE owner IS NULL OR scope IS NULL`);
72
- await q(`ALTER TABLE ${ROSTER_TABLE} ALTER COLUMN owner SET NOT NULL`);
73
- await q(`ALTER TABLE ${ROSTER_TABLE} ALTER COLUMN scope SET NOT NULL`);
74
- await q(`ALTER TABLE ${ROSTER_TABLE} ADD COLUMN IF NOT EXISTS root_session_id VARCHAR(190)`);
75
- await q(`ALTER TABLE ${ROSTER_TABLE} ADD COLUMN IF NOT EXISTS model VARCHAR(190)`);
76
66
  }
77
67
  function upsertParams(entry, now) {
78
68
  return [
@@ -33,6 +33,8 @@ export const SCHEMA_STATEMENTS = [
33
33
  updated_at DATETIME(3) NOT NULL,
34
34
  leaf_id VARCHAR(64) NULL,
35
35
  leaf_seq BIGINT NOT NULL DEFAULT 0,
36
+ -- title: auto-generated session title (cheap-model one-liner at first submit; write-once via
37
+ -- setTitleIfNull — the user-facing RENAME layer lives BFF-side as a label overlay, not here).
36
38
  title VARCHAR(120) NULL,
37
39
  PRIMARY KEY (session_id),
38
40
  KEY idx_owner (owner)
@@ -55,21 +57,46 @@ export const SCHEMA_STATEMENTS = [
55
57
  status VARCHAR(16) NOT NULL,
56
58
  result JSON NULL,
57
59
  error TEXT NULL,
60
+ -- error_code: the structured failure code denormalized out of the result blob (tidb-run-store fail()).
61
+ -- ⚠️ NO DESIGN PROVENANCE ON RECORD: its ADD COLUMN seam carried no rationale comment and neither git
62
+ -- history nor any doc records who asked for it (verified 2026-07-26 while folding the seams in). The only
63
+ -- consumer today is the projection (getRun/listRuns → errorCode) — nothing FILTERS on it; see the
64
+ -- deliberately-absent idx_error_code note below.
58
65
  error_code VARCHAR(64) NULL,
59
66
  instance_id VARCHAR(64) NULL,
67
+ -- job_id (work-view correlation substrate): groups the runs of one logical task across BOTH client doors.
60
68
  job_id VARCHAR(64) NULL,
69
+ -- source (credential-derived system identity, clay 2026-06-12): which door submitted the run
70
+ -- (oa / cc-mcp / portal).
61
71
  source VARCHAR(64) NULL,
72
+ -- objective_preview: the list's "what is this task" glance — redacted + truncated at write.
62
73
  objective_preview VARCHAR(160) NULL,
74
+ -- cancel_requested: durable cross-replica CANCEL flag. The running instance's heartbeat tick honors it
75
+ -- (+ a local AbortController fast path when the cancel lands on the same replica).
76
+ -- Terminal = "failed" + errorCode:"cancelled".
63
77
  cancel_requested TINYINT(1) NOT NULL DEFAULT 0,
78
+ -- preempt_requested (design/80 seam #2): durable cross-replica PREEMPT flag — the SCHEDULER's graceful
79
+ -- "yield this task" (≠ cancel's kill). The owning instance's heartbeat tick honors it (+ a local
80
+ -- preemptSignal fast path when the preempt lands on the same replica) → the task durably SUSPENDS at the
81
+ -- next clean turn boundary (gate resource_limit, reason preempt) and is resumable. Reset to 0 on resume
82
+ -- (markResuming) so a resumed leg is not re-preempted by a stale flag (the durable twin of core's
83
+ -- pre-aborted-signal strip).
64
84
  preempt_requested TINYINT(1) NOT NULL DEFAULT 0,
65
85
  created_at DATETIME(3) NOT NULL,
66
86
  updated_at DATETIME(3) NOT NULL,
67
87
  PRIMARY KEY (task_id),
68
88
  KEY idx_session_status (session_id, status),
69
89
  KEY idx_owner (owner),
70
- KEY idx_error_code (error_code),
90
+ -- idx_job: WHERE job_id = ? (work-view grouping) would full-scan without it.
71
91
  KEY idx_job (job_id),
92
+ -- idx_created: listRuns ALWAYS ORDER BY created_at DESC — without this the trace list does a filesort on
93
+ -- every query (deepseek council #2). Covers the unfiltered newest-first list; filtered lists narrow via
94
+ -- idx_owner / idx_job first.
72
95
  KEY idx_created (created_at)
96
+ -- ⛔ DELIBERATELY NO KEY idx_error_code (error_code) (dropped 2026-07-26, clay 裁): a whole-repo sweep found
97
+ -- ZERO code paths that filter on error_code — every reference is a SET or a projection — so the index only
98
+ -- ever served an operator's ad-hoc WHERE error_code LIKE 'budget.%', while every task_run write paid its
99
+ -- maintenance cost. If you need that ad-hoc query, build the index temporarily and drop it after.
73
100
  )`,
74
101
  `CREATE TABLE IF NOT EXISTS circuit_breaker (
75
102
  breaker_key VARCHAR(190) NOT NULL,
@@ -122,6 +149,11 @@ export const SCHEMA_STATEMENTS = [
122
149
  task_id VARCHAR(64) NULL,
123
150
  session_id VARCHAR(64) NULL,
124
151
  owner VARCHAR(190) NULL,
152
+ -- scope — single-DB fleet scope guard (defense-in-depth): the run's tenant identity (owner-sourced), so the
153
+ -- approval READ paths (getStatus / get / listPending / decide) can null-safe-filter on it. A NULL scope row
154
+ -- (an untenanted run — and, on the pre-folding schema, any PRE-MIGRATION row) is matched by a NULL scope arg
155
+ -- via MySQL <=> / PG IS NOT DISTINCT FROM — see tidb-approval-store.ts / pg-approval-store.ts.
156
+ -- expireStale deliberately stays GLOBAL (fleet maintenance, not a tenant read).
125
157
  scope VARCHAR(190) NULL,
126
158
  tool_name VARCHAR(190) NOT NULL,
127
159
  args JSON NULL,
@@ -132,6 +164,7 @@ export const SCHEMA_STATEMENTS = [
132
164
  decided_at DATETIME(3) NULL,
133
165
  PRIMARY KEY (id),
134
166
  KEY idx_owner_status (owner, status),
167
+ -- idx_scope_status: the scope-filtered READ paths (listPending WHERE scope <=> ?) would full-scan without it.
135
168
  KEY idx_scope_status (scope, status),
136
169
  KEY idx_status (status)
137
170
  )`,
@@ -143,18 +176,48 @@ export const SCHEMA_STATEMENTS = [
143
176
  status VARCHAR(16) NOT NULL DEFAULT 'pending',
144
177
  tool_name VARCHAR(190) NULL,
145
178
  tool_call_id VARCHAR(190) NULL,
179
+ -- tool_input: the pending-approval tool args = the approval card's payload, surfaced in listPending so the
180
+ -- BFF need not N+1-fetch trace.turns per pending item. Redacted + bounded at put().
181
+ -- NULL on PRE-MIGRATION rows (suspended before the column existed) ⇒ the consumer falls back.
146
182
  tool_input JSON NULL,
147
183
  checkpoint JSON NOT NULL,
148
184
  outcome JSON NULL,
149
185
  deadline BIGINT NULL,
150
186
  created_at BIGINT NOT NULL,
151
187
  decided_at BIGINT NULL,
188
+ -- rev (design/80 D-1): monotonic optimistic-concurrency counter bumped on every resolve/reopen, so a
189
+ -- resolve(expect) requires the rev the resume observed to still be live → fail-closed on a concurrent
190
+ -- resolve-reopen cycle (core → checkpoint.reopened_concurrently).
191
+ -- PRE-MIGRATION rows ⇒ 0 (the LEGACY CONTRACT; core's .d.ts spells it "Absent ⇒ legacy 0").
152
192
  rev BIGINT NOT NULL DEFAULT 0,
193
+ -- reopen_reason — reopen-by-reason: env_failed (a re-resume MUST replay the persisted winner) vs
194
+ -- tool_unavailable (a fresh decision is allowed). Drives core's reopen-revote validation.
195
+ -- NULL = NEVER REOPENED (the first resume is unconstrained).
153
196
  reopen_reason VARCHAR(32) NULL,
197
+ -- terminal_at: crash-safe ABSOLUTE lifetime backstop (design/80 §3 inv#3), distinct from the per-approval
198
+ -- deadline, stamped at put(). NULL on PRE-MIGRATION rows — those keep their DEADLINE-BASED expiry (the old
199
+ -- path); the backstop only covers rows suspended after the column existed.
154
200
  terminal_at BIGINT NULL,
201
+ -- gate_kind (design/80 D-D, SLA-timer): the CheckpointGate.kind, stamped at put(), so the SLA sweep splits by
202
+ -- kind WITHOUT parsing the JSON blob per row — human/irreversible_ask past deadline are resolve-DENIED (the
203
+ -- model continues with a denial), resource_limit/needs_review are abandonment-TTL → expire().
204
+ -- NULL (a PRE-MIGRATION row, or a gate carrying no kind) ⇒ the LEGACY UNIFORM expire() path: the deny split
205
+ -- is NEW-ROWS-ONLY. See tidb-checkpoint-store.ts reapExpired / listExpiredApprovalGates.
155
206
  gate_kind VARCHAR(32) NULL,
207
+ -- bound_input_hash (design/80 D-1): the server-minted opaque boundInputHash, stamped at put() from
208
+ -- pendingAction.boundInputHash, surfaced via listPending so the operator/portal can ECHO it on /decide (the
209
+ -- D-1 TOCTOU binding guard is UNREACHABLE by the portal without it — it can then only do the unbound legacy
210
+ -- fallback). NULL on PRE-D-1 rows ⇒ that unbound fallback.
156
211
  bound_input_hash VARCHAR(190) NULL,
212
+ -- pending_steer (design/80 D-A, durable steering): the parked steer ({text,trusted} JSON) set by
213
+ -- setPendingSteer on a STILL-PENDING checkpoint (last-writer-wins, CAS on status='pending'), surfaced via
214
+ -- get() as state.pendingSteer so core injects it on resume. NULL when no steer is parked.
215
+ -- Never touches status/resolve (design/80 inv #4).
157
216
  pending_steer TEXT NULL,
217
+ -- risk_descriptor (design/80 riskDescriptor inbox): core's INERT CheckpointGate.riskDescriptor ({severity
218
+ -- 1-5, axes, toolName, redacted summary, touchedPaths}), stamped at put() so listPending can surface it +
219
+ -- triage-sort the supervisor inbox by severity DESC — WITHOUT parsing the checkpoint blob per row.
220
+ -- NULL on gates with no descriptor.
158
221
  risk_descriptor TEXT NULL,
159
222
  PRIMARY KEY (token),
160
223
  KEY idx_status_deadline (status, deadline),
@@ -200,6 +263,10 @@ export const SCHEMA_STATEMENTS = [
200
263
  `CREATE TABLE IF NOT EXISTS session_policy (
201
264
  policy_key CHAR(64) NOT NULL,
202
265
  session_id VARCHAR(64) NOT NULL,
266
+ -- principal (2c session-sync): DENORMALIZED so listBySession can ENUMERATE a session's per-principal rows
267
+ -- (the sha256 policy_key is one-way → the principal is not recoverable from it). Enumeration-ONLY: not a key,
268
+ -- not part of the CAS/tighten logic — putRules just writes it. NULL = the session-wide row (principal
269
+ -- undefined). PRE-MIGRATION rows (E6 was new when the column landed) ⇒ NULL, i.e. read as session-wide.
203
270
  principal VARCHAR(190) NULL,
204
271
  rules JSON NOT NULL,
205
272
  rev BIGINT NOT NULL,
@@ -219,6 +286,13 @@ export const SCHEMA_STATEMENTS = [
219
286
  `CREATE TABLE IF NOT EXISTS snapshot_blob (
220
287
  blob_hash CHAR(64) NOT NULL,
221
288
  byte_len BIGINT NOT NULL,
289
+ -- bytes is NULLABLE — and that nullability IS the two-backend reference-tracking contract (see the block
290
+ -- comment above; this column is where it is enforced). It was originally LONGBLOB NOT NULL back when the
291
+ -- SQL store was the ONLY blob backend; the MinIO offload then required an INDEX-ONLY row here (bytes NULL,
292
+ -- the bytes living in MinIO) so that gcOrphanBlobs / reap / deleteBySession (E21 purge) / sweepOrphanBlobs
293
+ -- keep computing the orphan/grace SET from snapshot_blob for BOTH backends identically — and so an E21 purge
294
+ -- can DELETE the MinIO object instead of leaking it. Re-adding NOT NULL here would silently break MinIO-mode
295
+ -- reference tracking. See blob-backend.ts MinioBlobBackend.
222
296
  bytes LONGBLOB NULL,
223
297
  created_at DATETIME(3) NOT NULL,
224
298
  PRIMARY KEY (blob_hash),
@@ -227,6 +301,12 @@ export const SCHEMA_STATEMENTS = [
227
301
  `CREATE TABLE IF NOT EXISTS workflow_journal (
228
302
  run_id VARCHAR(191) NOT NULL,
229
303
  ordinal INT NOT NULL,
304
+ -- scope (SVC-2 / CORE-9 audit BLOCKER): the table predates scope — it was added when the
305
+ -- WorkflowJournalStore seam grew a scope param for cross-tenant resume isolation. Every new append writes the
306
+ -- run's REAL scope. EXISTING (pre-migration) rows carry '' = CROSS-SCOPE ORPHANS, harmless by construction: a
307
+ -- real resume filters WHERE scope = <caller>, so the blank-scope residue never matches a tenant. The
308
+ -- NOT NULL DEFAULT '' is what let the original ALTER succeed on a table that already had rows — keep the
309
+ -- default so the "'' = orphan, never a tenant" reading stays true.
230
310
  scope VARCHAR(190) NOT NULL DEFAULT '',
231
311
  call_key VARCHAR(255) NOT NULL,
232
312
  result MEDIUMTEXT NOT NULL,
@@ -253,7 +333,14 @@ export const SCHEMA_STATEMENTS = [
253
333
  status VARCHAR(16) NOT NULL,
254
334
  summary TEXT NOT NULL,
255
335
  enqueued_at BIGINT NOT NULL,
336
+ -- kind (1.109): task_notification entries SHARE this inbox — kind discriminates the drain frame family.
337
+ -- 🔴 NULL = a legacy workflow_complete row, AND it stays a LIVE value: the workflow_complete enqueue path
338
+ -- never sets kind, so new rows are written NULL too. The read side has the matching compat branch —
339
+ -- tidb-workflow-run-store.ts projects kind only when non-null and workflow-completion-inbox.ts routes
340
+ -- kind === "task_notification" vs everything-else. Do NOT "tidy" this into NOT NULL DEFAULT.
256
341
  kind VARCHAR(24) NULL,
342
+ -- payload: bounded + redacted JSON extras (task_type / result / …) for the task_notification family.
343
+ -- NULL for legacy / plain workflow_complete rows.
257
344
  payload TEXT NULL,
258
345
  PRIMARY KEY (session_id, run_id),
259
346
  UNIQUE KEY uq_wfinbox_seq (seq),
@@ -299,6 +386,8 @@ export const SCHEMA_STATEMENTS = [
299
386
  outcome VARCHAR(8) NULL,
300
387
  llm_assisted JSON NULL,
301
388
  signature_inputs JSON NULL,
389
+ -- core_outcome (design/73 §1 bridge): the VERBATIM core TaskOutcome fact
390
+ -- (oracleHadRedRun / status / oracle blob). The PG twin adds it via ADD COLUMN IF NOT EXISTS.
302
391
  core_outcome JSON NULL,
303
392
  created_at DATETIME(3) NOT NULL,
304
393
  PRIMARY KEY (id),
@@ -390,37 +479,6 @@ export const SCHEMA_STATEMENTS = [
390
479
  PRIMARY KEY (pool)
391
480
  )`,
392
481
  ];
393
- const COLUMN_MIGRATIONS = [
394
- { table: "session_meta", column: "title", ddl: "ALTER TABLE session_meta ADD COLUMN title VARCHAR(120) NULL" },
395
- { table: "outcome_ledger", column: "core_outcome", ddl: "ALTER TABLE outcome_ledger ADD COLUMN core_outcome JSON NULL" },
396
- { table: "workflow_completion_inbox", column: "kind", ddl: "ALTER TABLE workflow_completion_inbox ADD COLUMN kind VARCHAR(24) NULL" },
397
- { table: "workflow_completion_inbox", column: "payload", ddl: "ALTER TABLE workflow_completion_inbox ADD COLUMN payload TEXT NULL" },
398
- { table: "task_run", column: "error_code", ddl: "ALTER TABLE task_run ADD COLUMN error_code VARCHAR(64) NULL" },
399
- { table: "task_run", column: "job_id", ddl: "ALTER TABLE task_run ADD COLUMN job_id VARCHAR(64) NULL" },
400
- { table: "task_run", column: "cancel_requested", ddl: "ALTER TABLE task_run ADD COLUMN cancel_requested TINYINT(1) NOT NULL DEFAULT 0" },
401
- { table: "task_run", column: "preempt_requested", ddl: "ALTER TABLE task_run ADD COLUMN preempt_requested TINYINT(1) NOT NULL DEFAULT 0" },
402
- { table: "task_run", column: "source", ddl: "ALTER TABLE task_run ADD COLUMN source VARCHAR(64) NULL" },
403
- { table: "task_run", column: "objective_preview", ddl: "ALTER TABLE task_run ADD COLUMN objective_preview VARCHAR(160) NULL" },
404
- { table: "checkpoint", column: "tool_input", ddl: "ALTER TABLE checkpoint ADD COLUMN tool_input JSON NULL" },
405
- { table: "checkpoint", column: "rev", ddl: "ALTER TABLE checkpoint ADD COLUMN rev BIGINT NOT NULL DEFAULT 0" },
406
- { table: "checkpoint", column: "reopen_reason", ddl: "ALTER TABLE checkpoint ADD COLUMN reopen_reason VARCHAR(32) NULL" },
407
- { table: "checkpoint", column: "terminal_at", ddl: "ALTER TABLE checkpoint ADD COLUMN terminal_at BIGINT NULL" },
408
- { table: "checkpoint", column: "gate_kind", ddl: "ALTER TABLE checkpoint ADD COLUMN gate_kind VARCHAR(32) NULL" },
409
- { table: "checkpoint", column: "bound_input_hash", ddl: "ALTER TABLE checkpoint ADD COLUMN bound_input_hash VARCHAR(190) NULL" },
410
- { table: "checkpoint", column: "pending_steer", ddl: "ALTER TABLE checkpoint ADD COLUMN pending_steer TEXT NULL" },
411
- { table: "checkpoint", column: "risk_descriptor", ddl: "ALTER TABLE checkpoint ADD COLUMN risk_descriptor TEXT NULL" },
412
- { table: "approval", column: "scope", ddl: "ALTER TABLE approval ADD COLUMN scope VARCHAR(190) NULL" },
413
- { table: "session_policy", column: "principal", ddl: "ALTER TABLE session_policy ADD COLUMN principal VARCHAR(190) NULL" },
414
- { table: "workflow_journal", column: "scope", ddl: "ALTER TABLE workflow_journal ADD COLUMN scope VARCHAR(190) NOT NULL DEFAULT ''" },
415
- ];
416
- const INDEX_MIGRATIONS = [
417
- { table: "task_run", index: "idx_job", ddl: "CREATE INDEX idx_job ON task_run (job_id)" },
418
- { table: "task_run", index: "idx_created", ddl: "CREATE INDEX idx_created ON task_run (created_at)" },
419
- { table: "approval", index: "idx_scope_status", ddl: "CREATE INDEX idx_scope_status ON approval (scope, status)" },
420
- ];
421
- const NULLABILITY_MIGRATIONS = [
422
- { table: "snapshot_blob", column: "bytes", ddl: "ALTER TABLE snapshot_blob MODIFY COLUMN bytes LONGBLOB NULL" },
423
- ];
424
482
  export const ENSURE_SCHEMA_LOCK = "sema_ensure_schema";
425
483
  export async function acquireEnsureSchemaLock(conn, tag = "ensureSchema") {
426
484
  await new Promise((r) => setTimeout(r, Math.floor(Math.random() * 400)));
@@ -454,42 +512,6 @@ export async function ensureSchema(pool) {
454
512
  await conn.query(stmt);
455
513
  }
456
514
  await conn.query("INSERT IGNORE INTO image_bake_admit (pool) VALUES ('standard')");
457
- for (const m of COLUMN_MIGRATIONS) {
458
- const [rows] = await conn.query("SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ? LIMIT 1", [m.table, m.column]);
459
- if (rows.length === 0) {
460
- try {
461
- await conn.query(m.ddl);
462
- }
463
- catch (err) {
464
- if (Number(err.errno) !== 1060)
465
- throw err;
466
- }
467
- }
468
- }
469
- for (const m of INDEX_MIGRATIONS) {
470
- const [rows] = await conn.query("SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ? LIMIT 1", [m.table, m.index]);
471
- if (rows.length === 0) {
472
- try {
473
- await conn.query(m.ddl);
474
- }
475
- catch (err) {
476
- if (Number(err.errno) !== 1061)
477
- throw err;
478
- }
479
- }
480
- }
481
- for (const m of NULLABILITY_MIGRATIONS) {
482
- const [rows] = await conn.query("SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ? AND is_nullable = 'YES' LIMIT 1", [m.table, m.column]);
483
- if (rows.length === 0) {
484
- try {
485
- await conn.query(m.ddl);
486
- }
487
- catch (err) {
488
- if (Number(err.errno) !== 1060)
489
- throw err;
490
- }
491
- }
492
- }
493
515
  }
494
516
  finally {
495
517
  if (locked)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "1.284.0",
3
+ "version": "1.285.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",