@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,5 +1,7 @@
1
1
  import { StoredSession, SessionError, uuidv7, validateEntriesForImport, leafIdAfterEntry } from "@sema-agent/core";
2
2
  import { TiDBSessionStorage } from "./tidb-session-storage.js";
3
+ import { decodeSessionEventPayload } from "./session-event-payload.js";
4
+ import { jsonColumnText } from "./sql-json-column.js";
3
5
  import { escapeLike } from "./sql-escape.js";
4
6
  import { MANAGED_RETENTION } from "./retention-store-sql.js";
5
7
  import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
@@ -186,7 +188,7 @@ export class TiDBSessionStore {
186
188
  if (!metaRows[0])
187
189
  return null;
188
190
  const leafId = metaRows[0].leaf_id ?? null;
189
- const [eventRows] = await this.pool.query("SELECT entry_id, parent_id, type, payload, ts FROM session_event WHERE session_id = ? ORDER BY seq ASC", [sourceId]);
191
+ const [eventRows] = await this.pool.query(`SELECT entry_id, parent_id, type, ${jsonColumnText("tidb", "payload")}, ts FROM session_event WHERE session_id = ? ORDER BY seq ASC`, [sourceId]);
190
192
  const newId = uuidv7();
191
193
  const now = new Date();
192
194
  const conn = await this.pool.getConnection();
@@ -194,7 +196,7 @@ export class TiDBSessionStore {
194
196
  await conn.beginTransaction();
195
197
  for (let seq = 0; seq < eventRows.length; seq++) {
196
198
  const r = eventRows[seq];
197
- const payload = typeof r.payload === "string" ? r.payload : JSON.stringify(r.payload);
199
+ const payload = String(r.payload);
198
200
  await conn.query("INSERT INTO session_event (session_id, seq, entry_id, parent_id, type, payload, ts) VALUES (?,?,?,?,?,?,?)", [newId, seq, r.entry_id, r.parent_id ?? null, r.type, payload, r.ts]);
199
201
  }
200
202
  await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES (?,?,?,?,?,?)", [newId, owner, now, now, leafId, eventRows.length]);
@@ -217,8 +219,8 @@ export class TiDBSessionStore {
217
219
  const [metaRows] = await this.pool.query("SELECT 1 FROM session_meta WHERE session_id = ?", [sessionId]);
218
220
  if (!metaRows[0])
219
221
  return null;
220
- const [eventRows] = await this.pool.query("SELECT entry_id, parent_id, type, payload, ts FROM session_event WHERE session_id = ? ORDER BY seq ASC", [sessionId]);
221
- return eventRows.map((r) => (typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload));
222
+ const [eventRows] = await this.pool.query(`SELECT seq, entry_id, parent_id, type, ${jsonColumnText("tidb", "payload")}, ts FROM session_event WHERE session_id = ? ORDER BY seq ASC`, [sessionId]);
223
+ return eventRows.map((r) => decodeSessionEventPayload(r.payload, sessionId, r.seq));
222
224
  }
223
225
  async listEntryIds(sessionId) {
224
226
  const [metaRows] = await this.pool.query("SELECT 1 FROM session_meta WHERE session_id = ?", [sessionId]);
@@ -236,11 +238,9 @@ export class TiDBSessionStore {
236
238
  async function* gen() {
237
239
  let cursor = opts?.afterSeq ?? -1;
238
240
  for (;;) {
239
- const [rows] = await pool.query("SELECT seq, payload FROM session_event WHERE session_id = ? AND seq > ? ORDER BY seq ASC LIMIT ?", [sessionId, cursor, batchSize]);
240
- for (const r of rows) {
241
- const p = r.payload;
242
- yield (typeof p === "string" ? JSON.parse(p) : p);
243
- }
241
+ const [rows] = await pool.query(`SELECT seq, ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? AND seq > ? ORDER BY seq ASC LIMIT ?`, [sessionId, cursor, batchSize]);
242
+ for (const r of rows)
243
+ yield decodeSessionEventPayload(r.payload, sessionId, r.seq);
244
244
  if (rows.length < batchSize)
245
245
  return;
246
246
  cursor = Number(rows[rows.length - 1].seq);
@@ -321,8 +321,8 @@ export class TiDBSessionStore {
321
321
  }
322
322
  }
323
323
  async readStagedEntries(stagingId) {
324
- const [rows] = await this.pool.query("SELECT payload FROM session_event WHERE session_id = ? ORDER BY seq ASC", [stagingId]);
325
- return rows.map((r) => (typeof r.payload === "string" ? JSON.parse(r.payload) : r.payload));
324
+ const [rows] = await this.pool.query(`SELECT seq, ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? ORDER BY seq ASC`, [stagingId]);
325
+ return rows.map((r) => decodeSessionEventPayload(r.payload, stagingId, r.seq));
326
326
  }
327
327
  async sweepStagingSessions() {
328
328
  const cutoff = new Date(Date.now() - STAGING_GC_GRACE_MS);
@@ -416,12 +416,13 @@ class TiDBStagingHandle {
416
416
  throw new SyncConflictError(this.realSessionId, rel);
417
417
  }
418
418
  if (rel.relation === "fast_forward" && opts?.resolution !== "overwrite-dst" && dstIds !== null && dstIds.length > 0) {
419
- const [dstFull] = await conn.query("SELECT payload FROM session_event WHERE session_id = ? ORDER BY seq ASC", [this.realSessionId]);
420
- const [stagedFull] = await conn.query("SELECT entry_id, payload FROM session_event WHERE session_id = ? ORDER BY seq ASC", [this.stagingId]);
421
- const parseEntry = (p) => (typeof p === "string" ? JSON.parse(p) : p);
419
+ const [dstFull] = await conn.query(`SELECT seq, ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? ORDER BY seq ASC`, [this.realSessionId]);
420
+ const [stagedFull] = await conn.query(`SELECT entry_id, ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? ORDER BY seq ASC`, [this.stagingId]);
422
421
  const dstSet = new Set(dstIds);
423
- const dstEntries = dstFull.map((r) => parseEntry(r.payload));
424
- const stagedShared = stagedFull.filter((r) => dstSet.has(String(r.entry_id))).map((r) => parseEntry(r.payload));
422
+ const dstEntries = dstFull.map((r) => decodeSessionEventPayload(r.payload, this.realSessionId, r.seq));
423
+ const stagedShared = stagedFull
424
+ .filter((r) => dstSet.has(String(r.entry_id)))
425
+ .map((r) => decodeSessionEventPayload(r.payload, this.stagingId, r.entry_id));
425
426
  const diverged = fastForwardSharedContentDiverged(stagedShared, dstEntries);
426
427
  if (diverged) {
427
428
  await conn.rollback();
@@ -430,13 +431,10 @@ class TiDBStagingHandle {
430
431
  }
431
432
  await conn.query("DELETE FROM session_event WHERE session_id = ?", [this.realSessionId]);
432
433
  await conn.query("UPDATE session_event SET session_id = ? WHERE session_id = ?", [this.realSessionId, this.stagingId]);
433
- const [lastRow] = await conn.query("SELECT payload FROM session_event WHERE session_id = ? ORDER BY seq DESC LIMIT 1", [this.realSessionId]);
434
+ const [lastRow] = await conn.query(`SELECT ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? ORDER BY seq DESC LIMIT 1`, [this.realSessionId]);
434
435
  let leafId = null;
435
- if (lastRow[0]) {
436
- const p = lastRow[0].payload;
437
- const last = (typeof p === "string" ? JSON.parse(p) : p);
438
- leafId = leafIdAfterEntry(last);
439
- }
436
+ if (lastRow[0])
437
+ leafId = leafIdAfterEntry(decodeSessionEventPayload(lastRow[0].payload, this.realSessionId, "last staged row"));
440
438
  const now = new Date();
441
439
  await conn.query("INSERT INTO session_meta (session_id, owner, created_at, updated_at, leaf_id, leaf_seq) VALUES (?,?,?,?,?,?) " +
442
440
  "ON DUPLICATE KEY UPDATE owner = VALUES(owner), updated_at = VALUES(updated_at), leaf_id = VALUES(leaf_id), leaf_seq = VALUES(leaf_seq)", [this.realSessionId, owner, now, now, leafId, srcIds.length]);
@@ -25,8 +25,16 @@
25
25
  */
26
26
  import type { Pool as MySqlPool } from "mysql2/promise";
27
27
  import type { Pool as PgPool } from "pg";
28
- import type { UsageWindow, UsageWindowReading, UsageWindowStore } from "@sema-agent/core";
28
+ import type { UsageWindow, UsageWindowReading, UsageWindowRecord, UsageWindowStore } from "@sema-agent/core";
29
+ import { type JsonColumnShape } from "./sql-json-column.js";
29
30
  export declare const USAGE_WINDOW_TABLE = "usage_window";
31
+ /**
32
+ * `usage_window.record` 的**形读者**(S-550 车③a:本店既有的那一份深度判据逐字搬进来,一个字没改判)。
33
+ *
34
+ * 判据深到内层元素:非数值 tokens 参与累加会得到 `"0oops"` ⇒ `exhausted:false` ⇒ **超额账户被判未耗尽**
35
+ * (与 core `FileUsageWindowStore` 的 validateSlots/validateBuckets 同轴)。
36
+ */
37
+ export declare function readUsageWindowRecordBody(body: unknown): JsonColumnShape<UsageWindowRecord>;
30
38
  export declare function ensureTiDBUsageWindowSchema(pool: MySqlPool): Promise<void>;
31
39
  export declare function ensurePgUsageWindowSchema(q: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
32
40
  export declare class TiDBUsageWindowStore implements UsageWindowStore {
@@ -1,18 +1,32 @@
1
1
  import { EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord } from "@sema-agent/core";
2
+ import { decodeJsonColumn, foldJsonColumn, jsonColumnText } from "./sql-json-column.js";
2
3
  export const USAGE_WINDOW_TABLE = "usage_window";
4
+ class UsageRecordShapeProblem extends Error {
5
+ }
6
+ export function readUsageWindowRecordBody(body) {
7
+ try {
8
+ return { value: validateUsageWindowRecord(body) };
9
+ }
10
+ catch (e) {
11
+ if (e instanceof UsageRecordShapeProblem)
12
+ return { problem: e.message };
13
+ throw e;
14
+ }
15
+ }
3
16
  function parseRecord(raw, key) {
4
- let v;
5
17
  try {
6
- v = JSON.parse(String(raw));
18
+ return decodeJsonColumn(raw, readUsageWindowRecordBody, `${USAGE_WINDOW_TABLE}.record for key "${key}"`);
7
19
  }
8
20
  catch (e) {
9
- throw new Error(`usage_window record for key "${key}" is not valid JSON (${String(e)}) — refusing to mis-count a governance window`);
21
+ throw foldJsonColumn(e, (m) => new Error(`${m} — refusing to mis-count a governance window`));
10
22
  }
23
+ }
24
+ function validateUsageWindowRecord(v) {
11
25
  const r = v;
12
26
  if (typeof v !== "object" || v === null || !Array.isArray(r.slots) || !Array.isArray(r.buckets)) {
13
- throw new Error(`usage_window record for key "${key}" has an invalid shape (expected {slots[],buckets[]}) — refusing to mis-count a governance window`);
27
+ throw new UsageRecordShapeProblem("expected {slots[],buckets[]}");
14
28
  }
15
- const badShape = (detail) => new Error(`usage_window record for key "${key}" has an invalid shape (${detail}) — refusing to mis-count a governance window`);
29
+ const badShape = (detail) => new UsageRecordShapeProblem(detail);
16
30
  const finite = (x) => typeof x === "number" && Number.isFinite(x);
17
31
  const costKeys = (e) => {
18
32
  if (e.costMicroUsd !== undefined && (typeof e.costMicroUsd !== "number" || !Number.isSafeInteger(e.costMicroUsd) || e.costMicroUsd < 0)) {
@@ -70,7 +84,7 @@ export class TiDBUsageWindowStore {
70
84
  try {
71
85
  await c.beginTransaction();
72
86
  await c.query(`INSERT INTO ${USAGE_WINDOW_TABLE} (usage_key, record, updated_at_ms) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE usage_key = usage_key`, [key, JSON.stringify(EMPTY_USAGE_WINDOW_RECORD), at]);
73
- const [rows] = (await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ? FOR UPDATE`, [key]));
87
+ const [rows] = (await c.query(`SELECT ${jsonColumnText("tidb", "record")} FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ? FOR UPDATE`, [key]));
74
88
  if (rows.length === 0) {
75
89
  throw new Error(`usage_window row for key "${key}" vanished between ensure and lock — refusing to drop a governance charge`);
76
90
  }
@@ -88,7 +102,7 @@ export class TiDBUsageWindowStore {
88
102
  }
89
103
  }
90
104
  async read(key, windows, now) {
91
- const [rows] = (await this.pool.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ?`, [key]));
105
+ const [rows] = (await this.pool.query(`SELECT ${jsonColumnText("tidb", "record")} FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ?`, [key]));
92
106
  const record = rows.length === 0 ? EMPTY_USAGE_WINDOW_RECORD : parseRecord(rows[0].record, key);
93
107
  return readUsageRecord(record, windows, now);
94
108
  }
@@ -107,7 +121,7 @@ export class PgUsageWindowStore {
107
121
  JSON.stringify(EMPTY_USAGE_WINDOW_RECORD),
108
122
  at,
109
123
  ]);
110
- const { rows } = await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = $1 FOR UPDATE`, [key]);
124
+ const { rows } = await c.query(`SELECT ${jsonColumnText("pg", "record")} FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = $1 FOR UPDATE`, [key]);
111
125
  if (rows.length === 0) {
112
126
  throw new Error(`usage_window row for key "${key}" vanished between ensure and lock — refusing to drop a governance charge`);
113
127
  }
@@ -125,7 +139,7 @@ export class PgUsageWindowStore {
125
139
  }
126
140
  }
127
141
  async read(key, windows, now) {
128
- const { rows } = await this.pool.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = $1`, [key]);
142
+ const { rows } = await this.pool.query(`SELECT ${jsonColumnText("pg", "record")} FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = $1`, [key]);
129
143
  const record = rows.length === 0 ? EMPTY_USAGE_WINDOW_RECORD : parseRecord(rows[0].record, key);
130
144
  return readUsageRecord(record, windows, now);
131
145
  }
@@ -1,8 +1,17 @@
1
1
  import { callKeyOrdinal } from "@sema-agent/core";
2
2
  import { assertJournalEntryFits } from "@sema-agent/core";
3
- import { journalEntryOfRow, journalRowPayloadOf } from "../orchestration/workflow-journal-entry.js";
3
+ import { journalEntryOfBody, journalRowPayloadOf, readJournalRowBody } from "../orchestration/workflow-journal-entry.js";
4
4
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
5
+ import { decodeJsonColumn, foldJsonColumn, jsonColumnText, jsonColumnTextExpr } from "./sql-json-column.js";
5
6
  const DEFAULT_RESUME_CLAIM_TTL_MS = 60 * 60 * 1000;
7
+ function decodeJournalResult(cell, where) {
8
+ try {
9
+ return decodeJsonColumn(cell, readJournalRowBody, where);
10
+ }
11
+ catch (e) {
12
+ throw foldJsonColumn(e, (m) => new Error(`workflow journal: ${m} — refusing rather than replay a journal entry nobody can read`));
13
+ }
14
+ }
6
15
  export class SqlWorkflowJournalStore {
7
16
  db;
8
17
  resumeClaimTtlMs;
@@ -21,8 +30,11 @@ export class SqlWorkflowJournalStore {
21
30
  "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()]);
22
31
  }
23
32
  async load(runId, scope) {
24
- const { rows } = await this.db.query(this.q("SELECT call_key, result FROM workflow_journal WHERE run_id = ? AND scope = ? ORDER BY ordinal ASC", "SELECT call_key, result FROM workflow_journal WHERE run_id = $1 AND scope = $2 ORDER BY ordinal ASC"), [runId, scope]);
25
- return rows.map((r) => journalEntryOfRow(String(r.call_key), String(r.result)));
33
+ const { rows } = await this.db.query(this.q(`SELECT call_key, ${jsonColumnText("tidb", "result")} FROM workflow_journal WHERE run_id = ? AND scope = ? ORDER BY ordinal ASC`, `SELECT call_key, ${jsonColumnText("pg", "result")} FROM workflow_journal WHERE run_id = $1 AND scope = $2 ORDER BY ordinal ASC`), [runId, scope]);
34
+ return rows.map((r) => {
35
+ const callKey = String(r.call_key);
36
+ return journalEntryOfBody(callKey, decodeJournalResult(r.result, `workflow_journal.result for run ${runId} entry ${callKey}`));
37
+ });
26
38
  }
27
39
  locator(runId, _scope) {
28
40
  return `/v1/workflows/${runId}/journal`;
@@ -31,7 +43,7 @@ export class SqlWorkflowJournalStore {
31
43
  const params = this.db.dialect === "tidb"
32
44
  ? [opts.maxResultBytes, runId, scope, opts.limit, opts.offset]
33
45
  : [runId, scope, opts.maxResultBytes, opts.limit, opts.offset];
34
- const { rows } = await this.db.query(this.q("SELECT call_key, CASE WHEN LENGTH(result) > ? THEN NULL ELSE result END AS result_json, LENGTH(result) AS result_bytes FROM workflow_journal WHERE run_id = ? AND scope = ? ORDER BY ordinal ASC LIMIT ? OFFSET ?", "SELECT call_key, CASE WHEN OCTET_LENGTH(result) > $3 THEN NULL ELSE result END AS result_json, OCTET_LENGTH(result) AS result_bytes FROM workflow_journal WHERE run_id = $1 AND scope = $2 ORDER BY ordinal ASC LIMIT $4 OFFSET $5"), params);
46
+ const { rows } = await this.db.query(this.q(`SELECT call_key, CASE WHEN LENGTH(result) > ? THEN NULL ELSE ${jsonColumnTextExpr("tidb", "result")} END AS result_json, LENGTH(result) AS result_bytes FROM workflow_journal WHERE run_id = ? AND scope = ? ORDER BY ordinal ASC LIMIT ? OFFSET ?`, `SELECT call_key, CASE WHEN OCTET_LENGTH(result) > $3 THEN NULL ELSE ${jsonColumnTextExpr("pg", "result")} END AS result_json, OCTET_LENGTH(result) AS result_bytes FROM workflow_journal WHERE run_id = $1 AND scope = $2 ORDER BY ordinal ASC LIMIT $4 OFFSET $5`), params);
35
47
  return rows.map((r) => ({ callKey: String(r.call_key), resultJson: r.result_json === null ? null : String(r.result_json), resultBytes: Number(r.result_bytes) }));
36
48
  }
37
49
  async deleteByRun(runId) {
package/dist/run-local.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { DEFAULT_MODEL_REF } from "./model-select.js";
3
+ import { buildDurableAbsenceNote, createCorruptReadSeat } from "./observability/corrupt-read-seat.js";
3
4
  import { outwardPlane } from "./terminal.js";
4
5
  import { homedir } from "node:os";
5
6
  import { join } from "node:path";
@@ -17,6 +18,7 @@ import { taskWallClockSec } from "./task-workflow.js";
17
18
  import { applyCatalogToSource } from "./capabilities/center-prompts.js";
18
19
  import { validatePromptsDomain, CORE_ENGINE_VERSION } from "./prompts-domain-validate.js";
19
20
  import { buildUnpricedCeilingNotice, loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./config.js";
21
+ import { clearHostedEvidenceEnv } from "./hosted-posture.js";
20
22
  import { buildStoreOpenRefusalTail } from "./plugins/store-open-refusal.js";
21
23
  import { acquireLocalBackendWithWait } from "./plugins/store-lock-wait.js";
22
24
  import { createConfigProvider } from "./config-provider.js";
@@ -175,6 +177,7 @@ export async function runLocal(argv, deps = {}) {
175
177
  process.env.CONFIG_PROVIDER = "local";
176
178
  process.env.SESSION_BACKEND = "memory";
177
179
  process.env.REQUIRE_PRINCIPAL = "false";
180
+ clearHostedEvidenceEnv();
178
181
  const root = args.root ?? process.env.CONFIG_LOCAL_DIR ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
179
182
  if (args.root)
180
183
  process.env.CONFIG_LOCAL_DIR = args.root;
@@ -260,16 +263,7 @@ export async function runLocal(argv, deps = {}) {
260
263
  fileBackend = acquireLocalBackendWithWait(() => new FileStorageBackend({
261
264
  root,
262
265
  ...(config.fileHistory !== undefined ? { fileHistory: config.fileHistory } : {}),
263
- onCorruptRead: (info) => logger.warn("file_store_corrupt_read", {
264
- path: info.path,
265
- reason: info.reason,
266
- ...(info.sessionId !== undefined ? { sessionId: info.sessionId } : {}),
267
- ...(info.principal !== undefined ? { principal: info.principal } : {}),
268
- note: "a durable read was treated as ABSENT because the bytes were unreadable (never a plain ENOENT). " +
269
- "Consequence depends on which store read it: session-policy = this run lost its SUBTRACTIVE session-rule " +
270
- "layer (deployment policy / hooks / shell gate still applied); session repo = a listing silently omitted " +
271
- "rows; file-snapshot = a snapshot or scope read as missing. Inspect the named path before deciding.",
272
- }),
266
+ onCorruptRead: createCorruptReadSeat(logger.warn.bind(logger), "file_store_corrupt_read", buildDurableAbsenceNote("file-snapshot = a snapshot or scope read as missing")),
273
267
  }), config.storeLockWaitMs, (fields) => logger.warn("store_dir_lock_wait", { ...fields }));
274
268
  }
275
269
  catch (e) {
@@ -93,7 +93,20 @@ export type UsageDimension = "principal" | "model";
93
93
  export declare function usageBreakdown(rows: UsageRow[], dimension: UsageDimension): Array<{
94
94
  key: string;
95
95
  } & UsageTotals>;
96
- /** store 行投影 helper:result JSON(已 parse 或原始字符串)→ UsageRow.stats。所有后端复用(方言只在 SQL)。 */
96
+ /**
97
+ * store 行投影 helper:**已解码的** result 体 → UsageRow.stats。三个后端复用(方言只在 SQL)。
98
+ *
99
+ * 🔴 **本函数不解码**(S-550 车③b 删掉了它的 `typeof result === "string" ? JSON.parse(result) : result`
100
+ * 那一跳):那正是设计稿点名的歧义臂 ——「体是串就再解一次」在一列真 `JSON`/`JSONB` 上等于**解第二次**,
101
+ * 于是一个双重编码的 `task_run.result`(解一次得到的是串)会被解回好体、当成真的用量端出去。三个调用点
102
+ * 逐行亲核过,今天**一处都不递串**:
103
+ * · `plugins/run-store-sql.ts#usageScan` → `usageResultBody(r.result)`(属主按文本投影 + `readJsonObjectColumn`
104
+ * 判形,交出来的是对象或 `undefined`;串在那只读者上就是坏体);
105
+ * · `plugins/file-run-store.ts#usageScan` / `plugins/memory-run-store.ts#usageScan` → 行上的
106
+ * `result: PersistedTaskResult | null`(File 形由 core 的 `readJsonlRecords` 解,内存形本来就是对象)。
107
+ * ⇒ 解码的属主是**店**(列 / 文件 / 内存三种承载各自一个),不是这只聚合投影。递进来一个串不会再被
108
+ * 「宽容地」解开,它会在下面那条「`stats` 不是对象 ⇒ `undefined`」的臂上落成诚实的降级。
109
+ */
97
110
  export declare function projectUsageStats(result: unknown): UsageRow["stats"];
98
111
  /** usageScan 的统一行数上限(触顶=结果 truncated:true;试点规模远在限内)。 */
99
112
  export declare const USAGE_SCAN_LIMIT = 50000;
@@ -87,24 +87,18 @@ export function usageBreakdown(rows, dimension) {
87
87
  .map(([key, t]) => ({ key, ...finish(t) }));
88
88
  }
89
89
  export function projectUsageStats(result) {
90
- try {
91
- const parsed = typeof result === "string" ? JSON.parse(result) : result;
92
- const stats = parsed?.stats;
93
- if (!stats || typeof stats !== "object")
94
- return undefined;
95
- const out = {};
96
- if (typeof stats.tokens === "number")
97
- out.tokens = stats.tokens;
98
- const mu = stats.modelUsage;
99
- if (mu && typeof mu === "object" && !Array.isArray(mu))
100
- out.modelUsage = mu;
101
- if (stats.usageMissing === true)
102
- out.usageMissing = true;
103
- return out;
104
- }
105
- catch {
90
+ const stats = result?.stats;
91
+ if (!stats || typeof stats !== "object")
106
92
  return undefined;
107
- }
93
+ const out = {};
94
+ if (typeof stats.tokens === "number")
95
+ out.tokens = stats.tokens;
96
+ const mu = stats.modelUsage;
97
+ if (mu && typeof mu === "object" && !Array.isArray(mu))
98
+ out.modelUsage = mu;
99
+ if (stats.usageMissing === true)
100
+ out.usageMissing = true;
101
+ return out;
108
102
  }
109
103
  export const USAGE_SCAN_LIMIT = 50_000;
110
104
  //# sourceMappingURL=usage-analytics.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.93.7",
3
+ "version": "7.95.0",
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",
@@ -59,7 +59,7 @@
59
59
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
60
60
  },
61
61
  "dependencies": {
62
- "@sema-agent/core": "7.26.2",
62
+ "@sema-agent/core": "7.28.1",
63
63
  "@sema-agent/settings-schema": "3.0.0",
64
64
  "e2b": "^2.28.0",
65
65
  "libsodium-wrappers": "^0.8.4",