@sema-agent/server 7.93.6 → 7.94.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.
- package/USAGE.md +8 -0
- package/dist/boot/stores.js +6 -3
- package/dist/boot/workflow-orchestration.js +5 -1
- package/dist/config-catalog.js +1 -1
- package/dist/http/resume-ctx-read.d.ts +40 -0
- package/dist/http/resume-ctx-read.js +25 -0
- package/dist/http/resume-legs.js +24 -14
- package/dist/http/routes/images.js +12 -0
- package/dist/http/routes/runs.js +18 -4
- package/dist/http/routes/workflows.js +3 -2
- package/dist/observability/corrupt-read-seat.d.ts +56 -0
- package/dist/observability/corrupt-read-seat.js +16 -0
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/orchestration/workflow-journal-entry.d.ts +24 -6
- package/dist/orchestration/workflow-journal-entry.js +17 -10
- package/dist/plugins/approval-ask-store-sql.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +16 -3
- package/dist/plugins/checkpoint-store-sql.js +7 -9
- package/dist/plugins/image-bake-store-sql.js +85 -13
- package/dist/plugins/image-index-sql.js +75 -12
- package/dist/plugins/local-checkpoint-store.d.ts +23 -2
- package/dist/plugins/local-checkpoint-store.js +40 -33
- package/dist/plugins/local-session-store.d.ts +14 -0
- package/dist/plugins/local-session-store.js +12 -7
- package/dist/plugins/local-sidecar-quarantine.d.ts +10 -0
- package/dist/plugins/local-sidecar-quarantine.js +15 -0
- package/dist/plugins/local-task-attachment-store.d.ts +1 -1
- package/dist/plugins/local-task-attachment-store.js +5 -2
- package/dist/plugins/mailbox-store-sql.d.ts +6 -3
- package/dist/plugins/mailbox-store-sql.js +45 -43
- package/dist/plugins/memory-embedder-fingerprint.js +8 -12
- package/dist/plugins/memory-engine-pg.js +34 -16
- package/dist/plugins/memory-engine-tidb.js +19 -12
- package/dist/plugins/memory-engine-vector-util.d.ts +26 -0
- package/dist/plugins/memory-engine-vector-util.js +3 -0
- package/dist/plugins/memory-sync-store-pg.d.ts +6 -0
- package/dist/plugins/memory-sync-store-pg.js +14 -10
- package/dist/plugins/memory-sync-store-tidb.js +7 -11
- package/dist/plugins/pg-session-storage.js +26 -28
- package/dist/plugins/session-event-payload.d.ts +37 -0
- package/dist/plugins/session-event-payload.js +20 -0
- package/dist/plugins/sql-json-column.d.ts +124 -22
- package/dist/plugins/sql-json-column.js +22 -1
- package/dist/plugins/sql-row-helpers.d.ts +10 -16
- package/dist/plugins/sql-row-helpers.js +0 -29
- package/dist/plugins/store-backend.js +10 -12
- package/dist/plugins/store-contracts.d.ts +4 -6
- package/dist/plugins/store-contracts.js +0 -60
- package/dist/plugins/task-attachment-store.d.ts +21 -12
- package/dist/plugins/task-attachment-store.js +12 -0
- package/dist/plugins/tidb-session-storage.d.ts +2 -2
- package/dist/plugins/tidb-session-storage.js +12 -11
- package/dist/plugins/tidb-session-store.d.ts +2 -2
- package/dist/plugins/tidb-session-store.js +20 -22
- package/dist/plugins/usage-window-store-sql.d.ts +9 -1
- package/dist/plugins/usage-window-store-sql.js +23 -9
- package/dist/plugins/workflow-journal-store-sql.js +16 -4
- package/dist/run-local.js +2 -10
- package/dist/usage-analytics.d.ts +14 -1
- package/dist/usage-analytics.js +11 -17
- package/package.json +2 -2
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { parseJsonOr, toIso as iso, normalizeIsoOrNull as isoOrNull } from "./sql-row-helpers.js";
|
|
2
1
|
export class StoreBootRefusalError extends Error {
|
|
3
2
|
}
|
|
4
3
|
export const RUN_STATUS_CLASS = {
|
|
@@ -36,63 +35,4 @@ export function sessionTempoOfRunStatus(lastStatus) {
|
|
|
36
35
|
export function runProvenElsewhere(run, thisInstanceId) {
|
|
37
36
|
return typeof run.instanceId === "string" && run.instanceId.length > 0 && thisInstanceId !== undefined && run.instanceId !== thisInstanceId;
|
|
38
37
|
}
|
|
39
|
-
export function mapBakeRow(r) {
|
|
40
|
-
return {
|
|
41
|
-
bakeId: r.bake_id,
|
|
42
|
-
profile: r.profile,
|
|
43
|
-
bands: r.bands == null ? null : parseJsonOr(r.bands, []),
|
|
44
|
-
baseRef: r.base_ref,
|
|
45
|
-
push: !!r.push,
|
|
46
|
-
dryRun: !!r.dry_run,
|
|
47
|
-
logs: !!r.logs,
|
|
48
|
-
argv: parseJsonOr(r.argv, []),
|
|
49
|
-
status: r.status,
|
|
50
|
-
state: r.state ?? null,
|
|
51
|
-
digest: r.digest,
|
|
52
|
-
repo: r.repo,
|
|
53
|
-
ref: r.ref,
|
|
54
|
-
indexId: r.index_id,
|
|
55
|
-
exitCode: r.exit_code === null ? null : Number(r.exit_code),
|
|
56
|
-
error: r.error,
|
|
57
|
-
errorCode: r.error_code ?? null,
|
|
58
|
-
manifestSha: r.manifest_sha,
|
|
59
|
-
tag: r.tag,
|
|
60
|
-
idemKey: r.idem_key,
|
|
61
|
-
ingestSecret: r.ingest_secret,
|
|
62
|
-
runnerId: r.runner_id,
|
|
63
|
-
leaseUntil: isoOrNull(r.lease_until),
|
|
64
|
-
cancelRequested: !!r.cancel_requested,
|
|
65
|
-
requestedBy: r.requested_by,
|
|
66
|
-
createdAt: iso(r.created_at),
|
|
67
|
-
updatedAt: iso(r.updated_at),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
export const BAKE_SELECT_COLS = "bake_id, profile, bands, base_ref, push, dry_run, logs, argv, status, state, digest, repo, ref, index_id, " +
|
|
71
|
-
"exit_code, error, error_code, manifest_sha, tag, idem_key, ingest_secret, runner_id, lease_until, " +
|
|
72
|
-
"cancel_requested, requested_by, created_at, updated_at";
|
|
73
|
-
export function mapImageRow(r) {
|
|
74
|
-
return {
|
|
75
|
-
id: r.id,
|
|
76
|
-
profile: r.profile,
|
|
77
|
-
bands: parseJsonOr(r.bands, []),
|
|
78
|
-
repo: r.repo,
|
|
79
|
-
tag: r.tag,
|
|
80
|
-
digest: r.digest,
|
|
81
|
-
toolchainVersions: parseJsonOr(r.toolchain_versions, {}),
|
|
82
|
-
capabilities: parseJsonOr(r.capabilities, {}),
|
|
83
|
-
podContract: parseJsonOr(r.pod_contract, {}),
|
|
84
|
-
sizeBytes: r.size_bytes === null ? null : Number(r.size_bytes),
|
|
85
|
-
status: r.status,
|
|
86
|
-
visibility: r.visibility,
|
|
87
|
-
tenantId: r.tenant_id,
|
|
88
|
-
manifestSha: r.manifest_sha,
|
|
89
|
-
recipeGitSha: r.recipe_git_sha,
|
|
90
|
-
generatorVersion: r.generator_version,
|
|
91
|
-
buildDate: isoOrNull(r.build_date),
|
|
92
|
-
supersedes: r.supersedes,
|
|
93
|
-
signed: !!r.signed,
|
|
94
|
-
createdAt: isoOrNull(r.created_at) ?? new Date(0).toISOString(),
|
|
95
|
-
updatedAt: isoOrNull(r.updated_at) ?? new Date(0).toISOString(),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
38
|
//# sourceMappingURL=store-contracts.js.map
|
|
@@ -24,21 +24,30 @@
|
|
|
24
24
|
* - 绑定:`bind` 把行钉到 sessionId(重复绑定=改绑,同 owner 内 last-wins;同 session 多 task 共享
|
|
25
25
|
* 自然成立——listBySession 是物化输入)。未绑定行 = 上传后从未被任何 task 引用 ⇒ reapUnbound(TTL)。
|
|
26
26
|
*/
|
|
27
|
+
import { z } from "zod";
|
|
27
28
|
import type { Pool } from "mysql2/promise";
|
|
28
29
|
import type { PgQueryFn } from "./pg-query.js";
|
|
29
30
|
import { type IndexSpec } from "./ensure-index.js";
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
/**
|
|
32
|
+
* 一条附件 meta 的**形属主**(整仓唯一那份键集)。
|
|
33
|
+
*
|
|
34
|
+
* 🔴 为什么是一只 schema 而不只是一个 interface(S-550 车③b):SQL 车道把这些字段拆成**标量列**,
|
|
35
|
+
* 形由 DDL 管;local 车道把整条记录**序列化成一个 JSON 档**(`attachments/<id>.json`),于是它需要一个
|
|
36
|
+
* 运行期的形判据 —— 而那个判据必须与这里的键集是**同一份**(第二份键集分叉的那天 = 「写得进、读不出」)。
|
|
37
|
+
* 所以类型从 schema 推导({@link TaskAttachmentRecord}),两者按构造不可能漂。
|
|
38
|
+
*/
|
|
39
|
+
export declare const TaskAttachmentRecordSchema: z.ZodObject<{
|
|
40
|
+
id: z.ZodString;
|
|
41
|
+
owner: z.ZodString;
|
|
42
|
+
name: z.ZodString;
|
|
43
|
+
mime: z.ZodString;
|
|
44
|
+
sha256: z.ZodString;
|
|
45
|
+
sizeBytes: z.ZodNumber;
|
|
46
|
+
createdAtMs: z.ZodNumber;
|
|
47
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
48
|
+
boundAtMs: z.ZodOptional<z.ZodNumber>;
|
|
49
|
+
}, z.core.$strip>;
|
|
50
|
+
export type TaskAttachmentRecord = z.infer<typeof TaskAttachmentRecordSchema>;
|
|
42
51
|
export interface TaskAttachmentStore {
|
|
43
52
|
put(rec: TaskAttachmentRecord & {
|
|
44
53
|
content: Uint8Array;
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { ensureIndexes } from "./ensure-index.js";
|
|
3
|
+
export const TaskAttachmentRecordSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
owner: z.string(),
|
|
6
|
+
name: z.string(),
|
|
7
|
+
mime: z.string(),
|
|
8
|
+
sha256: z.string(),
|
|
9
|
+
sizeBytes: z.number(),
|
|
10
|
+
createdAtMs: z.number(),
|
|
11
|
+
sessionId: z.string().optional(),
|
|
12
|
+
boundAtMs: z.number().optional(),
|
|
13
|
+
});
|
|
2
14
|
export const TASK_ATTACHMENT_TABLE = "task_attachment";
|
|
3
15
|
export function materializedRelPaths(atts) {
|
|
4
16
|
const used = new Set();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { BaseSessionStorage, type PromptEpochDescriptor, type SessionMetadata, type SessionTreeEntry, type SessionWriteOptions } from "@sema-agent/core";
|
|
2
2
|
import type { Pool } from "mysql2/promise";
|
|
3
3
|
export declare const SESSION_EVENT_LATEST_BY_TYPE_SQL: {
|
|
4
|
-
readonly floor:
|
|
5
|
-
readonly epochAnchor:
|
|
4
|
+
readonly floor: `SELECT /*+ USE_INDEX(session_event, idx_session_event_type) */ ${string} FROM session_event WHERE session_id = ? AND type = 'compaction' ORDER BY seq DESC LIMIT 1`;
|
|
5
|
+
readonly epochAnchor: `SELECT /*+ USE_INDEX(session_event, idx_session_event_type) */ seq, ${string} FROM session_event WHERE session_id = ? AND seq < ? AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100`;
|
|
6
6
|
};
|
|
7
7
|
/**
|
|
8
8
|
* TiDB-backed {@link BaseSessionStorage} — the durable L1 event log for one session.
|
|
@@ -3,10 +3,12 @@ import { BaseSessionStorage, SessionError, leafIdAfterEntry, normalizePromptEpoc
|
|
|
3
3
|
import { toIso } from "./sql-row-helpers.js";
|
|
4
4
|
import { isMysqlDupKeyError } from "./sql-errors.js";
|
|
5
5
|
import { PLACEMENT_SELECT_COLUMNS, placementFromRow } from "./session-placement.js";
|
|
6
|
+
import { decodeSessionEventPayload } from "./session-event-payload.js";
|
|
7
|
+
import { jsonColumnText } from "./sql-json-column.js";
|
|
6
8
|
const SESSION_EVENT_BY_TYPE_HINT = "/*+ USE_INDEX(session_event, idx_session_event_type) */";
|
|
7
9
|
export const SESSION_EVENT_LATEST_BY_TYPE_SQL = {
|
|
8
|
-
floor: `SELECT ${SESSION_EVENT_BY_TYPE_HINT} payload FROM session_event WHERE session_id = ? AND type = 'compaction' ORDER BY seq DESC LIMIT 1`,
|
|
9
|
-
epochAnchor: `SELECT ${SESSION_EVENT_BY_TYPE_HINT} seq, payload FROM session_event WHERE session_id = ? AND seq < ? AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100`,
|
|
10
|
+
floor: `SELECT ${SESSION_EVENT_BY_TYPE_HINT} ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? AND type = 'compaction' ORDER BY seq DESC LIMIT 1`,
|
|
11
|
+
epochAnchor: `SELECT ${SESSION_EVENT_BY_TYPE_HINT} seq, ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? AND seq < ? AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100`,
|
|
10
12
|
};
|
|
11
13
|
const isDupKey = isMysqlDupKeyError;
|
|
12
14
|
export class TiDBSessionStorage extends BaseSessionStorage {
|
|
@@ -28,7 +30,7 @@ export class TiDBSessionStorage extends BaseSessionStorage {
|
|
|
28
30
|
if (rows.length === 0)
|
|
29
31
|
return undefined;
|
|
30
32
|
for (const r of rows) {
|
|
31
|
-
const e = (
|
|
33
|
+
const e = decodeSessionEventPayload(r.payload, this.sessionId, r.seq);
|
|
32
34
|
const d = e.type === "prompt_epoch" ? normalizePromptEpoch(e) : normalizePromptEpoch(e.details?.promptEpoch);
|
|
33
35
|
if (d)
|
|
34
36
|
return d;
|
|
@@ -42,8 +44,8 @@ export class TiDBSessionStorage extends BaseSessionStorage {
|
|
|
42
44
|
if (!meta)
|
|
43
45
|
return null;
|
|
44
46
|
const { floorEntryId, floorSeq } = await TiDBSessionStorage.computeFloor(pool, sessionId);
|
|
45
|
-
const [eventRows] = await pool.query(
|
|
46
|
-
const entries = eventRows.map((r) =>
|
|
47
|
+
const [eventRows] = await pool.query(`SELECT seq, ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? AND seq >= ? ORDER BY seq ASC`, [sessionId, floorSeq]);
|
|
48
|
+
const entries = eventRows.map((r) => decodeSessionEventPayload(r.payload, sessionId, r.seq));
|
|
47
49
|
const placement = placementFromRow(meta);
|
|
48
50
|
const metadata = { id: sessionId, createdAt: toIso(meta.created_at), ...(placement !== undefined ? { placement } : {}) };
|
|
49
51
|
return new TiDBSessionStorage(pool, sessionId, metadata, entries, meta.leaf_id ?? null, floorEntryId, floorSeq);
|
|
@@ -52,8 +54,8 @@ export class TiDBSessionStorage extends BaseSessionStorage {
|
|
|
52
54
|
const [compRows] = await pool.query(SESSION_EVENT_LATEST_BY_TYPE_SQL.floor, [sessionId]);
|
|
53
55
|
if (!compRows[0])
|
|
54
56
|
return { floorEntryId: null, floorSeq: 0 };
|
|
55
|
-
const comp = (
|
|
56
|
-
const fk = comp.firstKeptEntryId;
|
|
57
|
+
const comp = decodeSessionEventPayload(compRows[0].payload, sessionId, "latest compaction");
|
|
58
|
+
const fk = comp.type === "compaction" ? comp.firstKeptEntryId : undefined;
|
|
57
59
|
if (!fk)
|
|
58
60
|
return { floorEntryId: null, floorSeq: 0 };
|
|
59
61
|
const [seqRows] = await pool.query("SELECT seq FROM session_event WHERE session_id = ? AND entry_id = ?", [sessionId, fk]);
|
|
@@ -65,11 +67,10 @@ export class TiDBSessionStorage extends BaseSessionStorage {
|
|
|
65
67
|
const inMem = await super.getEntry(id);
|
|
66
68
|
if (inMem)
|
|
67
69
|
return inMem;
|
|
68
|
-
const [rows] = await this.pool.query(
|
|
69
|
-
|
|
70
|
-
if (p == null)
|
|
70
|
+
const [rows] = await this.pool.query(`SELECT ${jsonColumnText("tidb", "payload")} FROM session_event WHERE session_id = ? AND entry_id = ?`, [this.sessionId, id]);
|
|
71
|
+
if (rows[0] === undefined)
|
|
71
72
|
return undefined;
|
|
72
|
-
return (
|
|
73
|
+
return decodeSessionEventPayload(rows[0].payload, this.sessionId, id);
|
|
73
74
|
}
|
|
74
75
|
async appendEntry(entry, opts) {
|
|
75
76
|
await this.persist(entry, opts);
|
|
@@ -155,8 +155,8 @@ export declare class TiDBSessionStore implements SessionStore {
|
|
|
155
155
|
*
|
|
156
156
|
* Resolves to `null` (NOT an empty async-iterable) when the session does not exist — checked via `session_meta`
|
|
157
157
|
* EAGERLY here (before returning the iterable), exactly like {@link exportEntries}, so a missing session is distinct
|
|
158
|
-
* from a real-but-empty one (an empty stream). payload is
|
|
159
|
-
*
|
|
158
|
+
* from a real-but-empty one (an empty stream). payload is decoded like {@link exportEntries}
|
|
159
|
+
* (按文本投影 + `decodeSessionEventPayload` 恰好解一次码,坏体当场拒). The check is eager (the method is async) because a generator body is lazy — it can't decide
|
|
160
160
|
* `null` vs. iterable on its own first pull.
|
|
161
161
|
*/
|
|
162
162
|
exportEntriesStream(sessionId: string, opts?: {
|
|
@@ -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(
|
|
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 =
|
|
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(
|
|
221
|
-
return eventRows.map((r) => (
|
|
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(
|
|
240
|
-
for (const r of rows)
|
|
241
|
-
|
|
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(
|
|
325
|
-
return rows.map((r) => (
|
|
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(
|
|
420
|
-
const [stagedFull] = await conn.query(
|
|
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) =>
|
|
424
|
-
const stagedShared = stagedFull
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
return decodeJsonColumn(raw, readUsageWindowRecordBody, `${USAGE_WINDOW_TABLE}.record for key "${key}"`);
|
|
7
19
|
}
|
|
8
20
|
catch (e) {
|
|
9
|
-
throw
|
|
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
|
|
27
|
+
throw new UsageRecordShapeProblem("expected {slots[],buckets[]}");
|
|
14
28
|
}
|
|
15
|
-
const badShape = (detail) => new
|
|
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 {
|
|
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(
|
|
25
|
-
return rows.map((r) =>
|
|
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(
|
|
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";
|
|
@@ -260,16 +261,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
260
261
|
fileBackend = acquireLocalBackendWithWait(() => new FileStorageBackend({
|
|
261
262
|
root,
|
|
262
263
|
...(config.fileHistory !== undefined ? { fileHistory: config.fileHistory } : {}),
|
|
263
|
-
onCorruptRead: (
|
|
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
|
-
}),
|
|
264
|
+
onCorruptRead: createCorruptReadSeat(logger.warn.bind(logger), "file_store_corrupt_read", buildDurableAbsenceNote("file-snapshot = a snapshot or scope read as missing")),
|
|
273
265
|
}), config.storeLockWaitMs, (fields) => logger.warn("store_dir_lock_wait", { ...fields }));
|
|
274
266
|
}
|
|
275
267
|
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
|
-
/**
|
|
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;
|
package/dist/usage-analytics.js
CHANGED
|
@@ -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
|
-
|
|
91
|
-
|
|
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.
|
|
3
|
+
"version": "7.94.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.
|
|
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",
|