@sema-agent/server 1.314.0 → 1.316.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/dist/approval-hmac.d.ts +17 -0
- package/dist/approval-hmac.js +27 -0
- package/dist/config-center/apply-effective.d.ts +22 -0
- package/dist/config-center/apply-effective.js +283 -0
- package/dist/config-center/http-client.d.ts +23 -0
- package/dist/config-center/http-client.js +109 -0
- package/dist/config-center/restart-signal.d.ts +12 -0
- package/dist/config-center/restart-signal.js +70 -0
- package/dist/config-center/skills-mcp.d.ts +13 -0
- package/dist/config-center/skills-mcp.js +113 -0
- package/dist/config-center/types.d.ts +143 -0
- package/dist/config-center/types.js +2 -0
- package/dist/fleet/fleet-bus.js +92 -83
- package/dist/hooks/hook-runner.js +18 -9
- package/dist/http/idempotency.d.ts +1 -0
- package/dist/http/idempotency.js +3 -0
- package/dist/http/principal-gate.d.ts +12 -0
- package/dist/http/principal-gate.js +52 -0
- package/dist/http/route-ctx.d.ts +67 -0
- package/dist/http/route-ctx.js +2 -0
- package/dist/http/routes/attachments.d.ts +4 -0
- package/dist/http/routes/attachments.js +106 -0
- package/dist/http/routes/capabilities.d.ts +4 -0
- package/dist/http/routes/capabilities.js +111 -0
- package/dist/http/routes/images.d.ts +20 -0
- package/dist/http/routes/images.js +572 -0
- package/dist/http/routes/memory-policy.d.ts +4 -0
- package/dist/http/routes/memory-policy.js +105 -0
- package/dist/http/routes/observability.d.ts +4 -0
- package/dist/http/routes/observability.js +63 -0
- package/dist/http/routes/session-sync.d.ts +36 -0
- package/dist/http/routes/session-sync.js +513 -0
- package/dist/http/routes/sessions-list.d.ts +4 -0
- package/dist/http/routes/sessions-list.js +117 -0
- package/dist/http/routes/sessions.d.ts +17 -0
- package/dist/http/routes/sessions.js +746 -0
- package/dist/http/send.d.ts +6 -0
- package/dist/http/send.js +19 -0
- package/dist/http/server.d.ts +101 -76
- package/dist/http/server.js +129 -2410
- package/dist/http/sse-log.d.ts +25 -0
- package/dist/http/sse-log.js +59 -0
- package/dist/http/workspace-content.d.ts +3 -0
- package/dist/http/workspace-content.js +20 -0
- package/dist/index.d.ts +1 -1
- package/dist/leader/leader.js +5 -2
- package/dist/leader/wire.js +169 -165
- package/dist/main.js +462 -451
- package/dist/plugins/approval-store-sql.d.ts +52 -0
- package/dist/plugins/approval-store-sql.js +119 -0
- package/dist/plugins/breaker-state-sql.d.ts +29 -0
- package/dist/plugins/breaker-state-sql.js +104 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
- package/dist/plugins/checkpoint-store-sql.js +224 -0
- package/dist/plugins/file-snapshot-store-sql.d.ts +38 -0
- package/dist/plugins/file-snapshot-store-sql.js +210 -0
- package/dist/plugins/image-bake-store-sql.d.ts +53 -0
- package/dist/plugins/image-bake-store-sql.js +463 -0
- package/dist/plugins/image-index-sql.d.ts +33 -0
- package/dist/plugins/image-index-sql.js +328 -0
- package/dist/plugins/k8s-bg-scripts.d.ts +19 -0
- package/dist/plugins/k8s-bg-scripts.js +129 -0
- package/dist/plugins/k8s-exec-protocol.d.ts +20 -0
- package/dist/plugins/k8s-exec-protocol.js +87 -0
- package/dist/plugins/outcome-ledger-sql.d.ts +70 -0
- package/dist/plugins/outcome-ledger-sql.js +158 -0
- package/dist/plugins/pg-approval-store.d.ts +1 -27
- package/dist/plugins/pg-approval-store.js +1 -83
- package/dist/plugins/pg-breaker-state.d.ts +1 -19
- package/dist/plugins/pg-breaker-state.js +1 -88
- package/dist/plugins/pg-checkpoint-store.d.ts +1 -33
- package/dist/plugins/pg-checkpoint-store.js +1 -189
- package/dist/plugins/pg-cost-quota.js +3 -143
- package/dist/plugins/pg-file-snapshot-store.d.ts +1 -27
- package/dist/plugins/pg-file-snapshot-store.js +1 -190
- package/dist/plugins/pg-image-bake.d.ts +1 -40
- package/dist/plugins/pg-image-bake.js +1 -420
- package/dist/plugins/pg-image-index.d.ts +1 -21
- package/dist/plugins/pg-image-index.js +1 -217
- package/dist/plugins/pg-outcome-ledger.d.ts +2 -22
- package/dist/plugins/pg-outcome-ledger.js +1 -104
- package/dist/plugins/pg-rate-limiter.d.ts +2 -32
- package/dist/plugins/pg-rate-limiter.js +4 -147
- package/dist/plugins/pg-resume-anchor-store.d.ts +1 -13
- package/dist/plugins/pg-resume-anchor-store.js +1 -22
- package/dist/plugins/pg-run-store.d.ts +1 -75
- package/dist/plugins/pg-run-store.js +1 -287
- package/dist/plugins/pg-session-policy-store.d.ts +1 -11
- package/dist/plugins/pg-session-policy-store.js +1 -90
- package/dist/plugins/pg-tool-result-store.d.ts +1 -15
- package/dist/plugins/pg-tool-result-store.js +1 -52
- package/dist/plugins/pg-workflow-journal-store.d.ts +1 -20
- package/dist/plugins/pg-workflow-journal-store.js +1 -34
- package/dist/plugins/pg-workflow-run-store.d.ts +1 -55
- package/dist/plugins/pg-workflow-run-store.js +1 -220
- package/dist/plugins/remote-env-k8s.d.ts +5 -38
- package/dist/plugins/remote-env-k8s.js +7 -213
- package/dist/plugins/resume-anchor-store-sql.d.ts +23 -0
- package/dist/plugins/resume-anchor-store-sql.js +38 -0
- package/dist/plugins/run-store-sql.d.ts +89 -0
- package/dist/plugins/run-store-sql.js +397 -0
- package/dist/plugins/session-policy-store-sql.d.ts +22 -0
- package/dist/plugins/session-policy-store-sql.js +109 -0
- package/dist/plugins/sql-driver.d.ts +25 -0
- package/dist/plugins/sql-driver.js +59 -0
- package/dist/plugins/tidb-approval-store.d.ts +1 -39
- package/dist/plugins/tidb-approval-store.js +1 -71
- package/dist/plugins/tidb-breaker-state.d.ts +1 -17
- package/dist/plugins/tidb-breaker-state.js +1 -74
- package/dist/plugins/tidb-checkpoint-store.d.ts +1 -49
- package/dist/plugins/tidb-checkpoint-store.js +1 -191
- package/dist/plugins/tidb-file-snapshot-store.d.ts +1 -27
- package/dist/plugins/tidb-file-snapshot-store.js +1 -192
- package/dist/plugins/tidb-image-bake.d.ts +1 -38
- package/dist/plugins/tidb-image-bake.js +1 -348
- package/dist/plugins/tidb-image-index.d.ts +1 -19
- package/dist/plugins/tidb-image-index.js +1 -149
- package/dist/plugins/tidb-outcome-ledger.d.ts +1 -57
- package/dist/plugins/tidb-outcome-ledger.js +1 -97
- package/dist/plugins/tidb-resume-anchor-store.d.ts +1 -13
- package/dist/plugins/tidb-resume-anchor-store.js +1 -22
- package/dist/plugins/tidb-run-store.d.ts +1 -75
- package/dist/plugins/tidb-run-store.js +1 -306
- package/dist/plugins/tidb-session-policy-store.d.ts +1 -11
- package/dist/plugins/tidb-session-policy-store.js +1 -86
- package/dist/plugins/tidb-tool-result-store.d.ts +1 -13
- package/dist/plugins/tidb-tool-result-store.js +1 -38
- package/dist/plugins/tidb-workflow-journal-store.d.ts +1 -20
- package/dist/plugins/tidb-workflow-journal-store.js +1 -34
- package/dist/plugins/tidb-workflow-run-store.d.ts +1 -61
- package/dist/plugins/tidb-workflow-run-store.js +1 -231
- package/dist/plugins/tool-result-store-sql.d.ts +25 -0
- package/dist/plugins/tool-result-store-sql.js +69 -0
- package/dist/plugins/workflow-journal-store-sql.d.ts +30 -0
- package/dist/plugins/workflow-journal-store-sql.js +53 -0
- package/dist/plugins/workflow-run-store-sql.d.ts +85 -0
- package/dist/plugins/workflow-run-store-sql.js +299 -0
- package/dist/plugins/write-behind-counter.d.ts +14 -3
- package/dist/plugins/write-behind-counter.js +39 -12
- package/dist/principal-jwt.d.ts +44 -0
- package/dist/principal-jwt.js +95 -0
- package/dist/security.d.ts +2 -58
- package/dist/security.js +3 -118
- package/dist/sema-registry.d.ts +5 -200
- package/dist/sema-registry.js +4 -567
- package/package.json +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
|
+
import type { Pool as PgPool, PoolClient } from "pg";
|
|
3
|
+
import { type SqlDriver } from "./sql-driver.js";
|
|
4
|
+
export type ApprovalStatus = "pending" | "approved" | "denied" | "expired";
|
|
5
|
+
export interface ApprovalRow {
|
|
6
|
+
id: string;
|
|
7
|
+
taskId: string | null;
|
|
8
|
+
sessionId: string | null;
|
|
9
|
+
owner: string | null;
|
|
10
|
+
scope: string | null;
|
|
11
|
+
toolName: string;
|
|
12
|
+
args: unknown;
|
|
13
|
+
status: ApprovalStatus;
|
|
14
|
+
reason: string | null;
|
|
15
|
+
decidedBy: string | null;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
decidedAt: string | null;
|
|
18
|
+
}
|
|
19
|
+
export declare const PG_APPROVAL_SCHEMA: string[];
|
|
20
|
+
export declare function ensureSchema(pool: PgPool | PoolClient): Promise<void>;
|
|
21
|
+
export declare class SqlApprovalStore {
|
|
22
|
+
protected readonly db: SqlDriver;
|
|
23
|
+
constructor(db: SqlDriver);
|
|
24
|
+
private q;
|
|
25
|
+
private json;
|
|
26
|
+
createPending(id: string, scope: string | null, ctx: {
|
|
27
|
+
taskId?: string | null;
|
|
28
|
+
sessionId?: string | null;
|
|
29
|
+
owner?: string | null;
|
|
30
|
+
toolName: string;
|
|
31
|
+
args: unknown;
|
|
32
|
+
}): Promise<void>;
|
|
33
|
+
getStatus(id: string, scope: string | null): Promise<{
|
|
34
|
+
status: ApprovalStatus;
|
|
35
|
+
reason: string | null;
|
|
36
|
+
decidedBy: string | null;
|
|
37
|
+
} | undefined>;
|
|
38
|
+
decide(id: string, scope: string | null, decision: "approved" | "denied" | "expired", reason: string | null, decidedBy: string | null): Promise<boolean>;
|
|
39
|
+
listPending(scope: string | null, owner?: string): Promise<ApprovalRow[]>;
|
|
40
|
+
get(id: string, scope: string | null): Promise<ApprovalRow | undefined>;
|
|
41
|
+
getById(id: string): Promise<ApprovalRow | undefined>;
|
|
42
|
+
listPendingAll(): Promise<ApprovalRow[]>;
|
|
43
|
+
expireStale(olderThanMs: number): Promise<number>;
|
|
44
|
+
}
|
|
45
|
+
export declare function mapRow(r: Record<string, unknown>): ApprovalRow;
|
|
46
|
+
export declare class TiDBApprovalStore extends SqlApprovalStore {
|
|
47
|
+
constructor(pool: MySqlPool);
|
|
48
|
+
}
|
|
49
|
+
export declare class PgApprovalStore extends SqlApprovalStore {
|
|
50
|
+
constructor(pool: PgPool);
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=approval-store-sql.d.ts.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { parseJsonStrict as parseJson, toIsoOrNull as iso } from "./sql-row-helpers.js";
|
|
2
|
+
import { pgProtocolJsonStringify } from "./pg-safe-json.js";
|
|
3
|
+
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
4
|
+
export const PG_APPROVAL_SCHEMA = [
|
|
5
|
+
`CREATE TABLE IF NOT EXISTS approval (
|
|
6
|
+
id VARCHAR(64) NOT NULL,
|
|
7
|
+
task_id VARCHAR(64),
|
|
8
|
+
session_id VARCHAR(64),
|
|
9
|
+
owner VARCHAR(190),
|
|
10
|
+
-- Single-DB fleet scope guard (tenant isolation). NULLABLE on purpose: a NULL-scope row is an untenanted
|
|
11
|
+
-- run, and every read/decide path matches it with \`scope IS NOT DISTINCT FROM $n\` (the PG twin of TiDB
|
|
12
|
+
-- \`<=>\`) so NULL matches only NULL and one tenant's scope NEVER matches another's.
|
|
13
|
+
scope VARCHAR(190),
|
|
14
|
+
tool_name VARCHAR(190) NOT NULL,
|
|
15
|
+
args JSONB,
|
|
16
|
+
status VARCHAR(16) NOT NULL,
|
|
17
|
+
reason TEXT,
|
|
18
|
+
decided_by VARCHAR(190),
|
|
19
|
+
created_at TIMESTAMPTZ(3) NOT NULL,
|
|
20
|
+
decided_at TIMESTAMPTZ(3),
|
|
21
|
+
PRIMARY KEY (id)
|
|
22
|
+
)`,
|
|
23
|
+
`CREATE INDEX IF NOT EXISTS idx_approval_owner_status ON approval (owner, status)`,
|
|
24
|
+
`CREATE INDEX IF NOT EXISTS idx_approval_status ON approval (status)`,
|
|
25
|
+
`CREATE INDEX IF NOT EXISTS idx_approval_scope_status ON approval (scope, status)`,
|
|
26
|
+
];
|
|
27
|
+
export async function ensureSchema(pool) {
|
|
28
|
+
for (const stmt of PG_APPROVAL_SCHEMA)
|
|
29
|
+
await pool.query(stmt);
|
|
30
|
+
}
|
|
31
|
+
export class SqlApprovalStore {
|
|
32
|
+
db;
|
|
33
|
+
constructor(db) {
|
|
34
|
+
this.db = db;
|
|
35
|
+
}
|
|
36
|
+
q(tidb, pg) {
|
|
37
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
38
|
+
}
|
|
39
|
+
json(value, label) {
|
|
40
|
+
return this.db.dialect === "tidb" ? JSON.stringify(value) : pgProtocolJsonStringify(value, label);
|
|
41
|
+
}
|
|
42
|
+
async createPending(id, scope, ctx) {
|
|
43
|
+
await this.db.query(this.q("INSERT INTO approval (id, task_id, session_id, owner, scope, tool_name, args, status, created_at) " +
|
|
44
|
+
"VALUES (?,?,?,?,?,?,?,'pending',?)", "INSERT INTO approval (id, task_id, session_id, owner, scope, tool_name, args, status, created_at) " +
|
|
45
|
+
"VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,'pending',$8)"), [
|
|
46
|
+
id,
|
|
47
|
+
ctx.taskId ?? null,
|
|
48
|
+
ctx.sessionId ?? null,
|
|
49
|
+
ctx.owner ?? null,
|
|
50
|
+
scope,
|
|
51
|
+
ctx.toolName,
|
|
52
|
+
ctx.args == null ? null : this.json(ctx.args, "approval args"),
|
|
53
|
+
new Date(),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
async getStatus(id, scope) {
|
|
57
|
+
const { rows } = await this.db.query(this.q("SELECT status, reason, decided_by FROM approval WHERE id = ? AND scope <=> ?", "SELECT status, reason, decided_by FROM approval WHERE id = $1 AND scope IS NOT DISTINCT FROM $2"), [id, scope]);
|
|
58
|
+
const r = rows[0];
|
|
59
|
+
if (!r)
|
|
60
|
+
return undefined;
|
|
61
|
+
return { status: r.status, reason: r.reason ?? null, decidedBy: r.decided_by ?? null };
|
|
62
|
+
}
|
|
63
|
+
async decide(id, scope, decision, reason, decidedBy) {
|
|
64
|
+
const res = await this.db.query(this.q("UPDATE approval SET status = ?, reason = ?, decided_by = ?, decided_at = ? WHERE id = ? AND scope <=> ? AND status = 'pending'", "UPDATE approval SET status = $1, reason = $2, decided_by = $3, decided_at = $4 WHERE id = $5 AND scope IS NOT DISTINCT FROM $6 AND status = 'pending'"), [decision, reason, decidedBy, new Date(), id, scope]);
|
|
65
|
+
return res.affected === 1;
|
|
66
|
+
}
|
|
67
|
+
async listPending(scope, owner) {
|
|
68
|
+
const { rows } = owner
|
|
69
|
+
? await this.db.query(this.q("SELECT * FROM approval WHERE status = 'pending' AND scope <=> ? AND owner = ? ORDER BY created_at ASC", "SELECT * FROM approval WHERE status = 'pending' AND scope IS NOT DISTINCT FROM $1 AND owner = $2 ORDER BY created_at ASC"), [scope, owner])
|
|
70
|
+
: await this.db.query(this.q("SELECT * FROM approval WHERE status = 'pending' AND scope <=> ? ORDER BY created_at ASC", "SELECT * FROM approval WHERE status = 'pending' AND scope IS NOT DISTINCT FROM $1 ORDER BY created_at ASC"), [scope]);
|
|
71
|
+
return rows.map(mapRow);
|
|
72
|
+
}
|
|
73
|
+
async get(id, scope) {
|
|
74
|
+
const { rows } = await this.db.query(this.q("SELECT * FROM approval WHERE id = ? AND scope <=> ?", "SELECT * FROM approval WHERE id = $1 AND scope IS NOT DISTINCT FROM $2"), [id, scope]);
|
|
75
|
+
return rows[0] ? mapRow(rows[0]) : undefined;
|
|
76
|
+
}
|
|
77
|
+
async getById(id) {
|
|
78
|
+
const { rows } = await this.db.query(this.q("SELECT * FROM approval WHERE id = ?", "SELECT * FROM approval WHERE id = $1"), [id]);
|
|
79
|
+
return rows[0] ? mapRow(rows[0]) : undefined;
|
|
80
|
+
}
|
|
81
|
+
async listPendingAll() {
|
|
82
|
+
const { rows } = await this.db.query("SELECT * FROM approval WHERE status = 'pending' ORDER BY created_at ASC");
|
|
83
|
+
return rows.map(mapRow);
|
|
84
|
+
}
|
|
85
|
+
async expireStale(olderThanMs) {
|
|
86
|
+
const cutoff = new Date(Date.now() - olderThanMs);
|
|
87
|
+
const res = await this.db.query(this.q("UPDATE approval SET status = 'expired', reason = 'timed out before decision', decided_at = ? " +
|
|
88
|
+
"WHERE status = 'pending' AND created_at < ?", "UPDATE approval SET status = 'expired', reason = 'timed out before decision', decided_at = $1 " +
|
|
89
|
+
"WHERE status = 'pending' AND created_at < $2"), [new Date(), cutoff]);
|
|
90
|
+
return res.affected;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function mapRow(r) {
|
|
94
|
+
return {
|
|
95
|
+
id: String(r.id),
|
|
96
|
+
taskId: r.task_id ?? null,
|
|
97
|
+
sessionId: r.session_id ?? null,
|
|
98
|
+
owner: r.owner ?? null,
|
|
99
|
+
scope: r.scope ?? null,
|
|
100
|
+
toolName: String(r.tool_name),
|
|
101
|
+
args: parseJson(r.args),
|
|
102
|
+
status: r.status,
|
|
103
|
+
reason: r.reason ?? null,
|
|
104
|
+
decidedBy: r.decided_by ?? null,
|
|
105
|
+
createdAt: iso(r.created_at),
|
|
106
|
+
decidedAt: iso(r.decided_at),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export class TiDBApprovalStore extends SqlApprovalStore {
|
|
110
|
+
constructor(pool) {
|
|
111
|
+
super(mysqlDriver(pool));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export class PgApprovalStore extends SqlApprovalStore {
|
|
115
|
+
constructor(pool) {
|
|
116
|
+
super(pgDriver(pool));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=approval-store-sql.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
|
+
import type { Pool as PgPool, PoolClient } from "pg";
|
|
3
|
+
import type { BreakerSnapshot, BreakerState } from "@sema-agent/core";
|
|
4
|
+
import { type SqlDriver } from "./sql-driver.js";
|
|
5
|
+
export declare const PG_BREAKER_STATE_SCHEMA: string[];
|
|
6
|
+
export declare function ensureSchema(pool: PgPool | PoolClient): Promise<void>;
|
|
7
|
+
export declare class SqlBreakerState implements BreakerState {
|
|
8
|
+
protected readonly db: SqlDriver;
|
|
9
|
+
private readonly onWriteFail?;
|
|
10
|
+
private cache;
|
|
11
|
+
private timer?;
|
|
12
|
+
private readonly pending;
|
|
13
|
+
private writeFailStreak;
|
|
14
|
+
constructor(db: SqlDriver, onWriteFail?: ((streak: number) => void) | undefined);
|
|
15
|
+
private q;
|
|
16
|
+
get(key: string): BreakerSnapshot | undefined;
|
|
17
|
+
set(key: string, snap: BreakerSnapshot): void;
|
|
18
|
+
private writeThrough;
|
|
19
|
+
refresh(): Promise<void>;
|
|
20
|
+
startRefresh(intervalMs?: number): this;
|
|
21
|
+
stop(): void;
|
|
22
|
+
}
|
|
23
|
+
export declare class TiDBBreakerState extends SqlBreakerState {
|
|
24
|
+
constructor(pool: MySqlPool, onWriteFail?: (streak: number) => void);
|
|
25
|
+
}
|
|
26
|
+
export declare class PgBreakerState extends SqlBreakerState {
|
|
27
|
+
constructor(pool: PgPool, onWriteFail?: (streak: number) => void);
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=breaker-state-sql.d.ts.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
2
|
+
export const PG_BREAKER_STATE_SCHEMA = [
|
|
3
|
+
`CREATE TABLE IF NOT EXISTS circuit_breaker (
|
|
4
|
+
breaker_key VARCHAR(190) NOT NULL,
|
|
5
|
+
phase VARCHAR(16) NOT NULL,
|
|
6
|
+
failures INTEGER NOT NULL DEFAULT 0,
|
|
7
|
+
opened_at TIMESTAMPTZ(3),
|
|
8
|
+
updated_at TIMESTAMPTZ(3) NOT NULL,
|
|
9
|
+
PRIMARY KEY (breaker_key)
|
|
10
|
+
)`,
|
|
11
|
+
];
|
|
12
|
+
export async function ensureSchema(pool) {
|
|
13
|
+
for (const stmt of PG_BREAKER_STATE_SCHEMA)
|
|
14
|
+
await pool.query(stmt);
|
|
15
|
+
}
|
|
16
|
+
export class SqlBreakerState {
|
|
17
|
+
db;
|
|
18
|
+
onWriteFail;
|
|
19
|
+
cache = new Map();
|
|
20
|
+
timer;
|
|
21
|
+
pending = new Set();
|
|
22
|
+
writeFailStreak = 0;
|
|
23
|
+
constructor(db, onWriteFail) {
|
|
24
|
+
this.db = db;
|
|
25
|
+
this.onWriteFail = onWriteFail;
|
|
26
|
+
}
|
|
27
|
+
q(tidb, pg) {
|
|
28
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
29
|
+
}
|
|
30
|
+
get(key) {
|
|
31
|
+
return this.cache.get(key);
|
|
32
|
+
}
|
|
33
|
+
set(key, snap) {
|
|
34
|
+
this.cache.set(key, snap);
|
|
35
|
+
this.pending.add(key);
|
|
36
|
+
void this.writeThrough(key, snap);
|
|
37
|
+
}
|
|
38
|
+
async writeThrough(key, snap) {
|
|
39
|
+
try {
|
|
40
|
+
await this.db.query(this.q("INSERT INTO circuit_breaker (breaker_key, phase, failures, opened_at, updated_at) VALUES (?,?,?,?,?) " +
|
|
41
|
+
"ON DUPLICATE KEY UPDATE phase=VALUES(phase), failures=VALUES(failures), opened_at=VALUES(opened_at), updated_at=VALUES(updated_at)", "INSERT INTO circuit_breaker (breaker_key, phase, failures, opened_at, updated_at) VALUES ($1,$2,$3,$4,$5) " +
|
|
42
|
+
"ON CONFLICT (breaker_key) DO UPDATE SET phase=EXCLUDED.phase, failures=EXCLUDED.failures, opened_at=EXCLUDED.opened_at, updated_at=EXCLUDED.updated_at"), [key, snap.phase, snap.failures, snap.openedAt != null ? new Date(snap.openedAt) : null, new Date()]);
|
|
43
|
+
this.pending.delete(key);
|
|
44
|
+
if (this.writeFailStreak > 0) {
|
|
45
|
+
this.writeFailStreak = 0;
|
|
46
|
+
try {
|
|
47
|
+
this.onWriteFail?.(0);
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
try {
|
|
54
|
+
this.onWriteFail?.(++this.writeFailStreak);
|
|
55
|
+
}
|
|
56
|
+
catch { }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async refresh() {
|
|
60
|
+
try {
|
|
61
|
+
const pendingAtStart = new Set(this.pending);
|
|
62
|
+
const guarded = (key) => this.pending.has(key) || pendingAtStart.has(key);
|
|
63
|
+
const { rows } = await this.db.query("SELECT breaker_key, phase, failures, opened_at FROM circuit_breaker");
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
for (const r of rows) {
|
|
66
|
+
const key = String(r.breaker_key);
|
|
67
|
+
seen.add(key);
|
|
68
|
+
if (guarded(key))
|
|
69
|
+
continue;
|
|
70
|
+
this.cache.set(key, {
|
|
71
|
+
phase: r.phase,
|
|
72
|
+
failures: Number(r.failures),
|
|
73
|
+
openedAt: r.opened_at != null ? new Date(r.opened_at).getTime() : undefined,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
for (const key of this.cache.keys())
|
|
77
|
+
if (!seen.has(key) && !guarded(key))
|
|
78
|
+
this.cache.delete(key);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
startRefresh(intervalMs = 5_000) {
|
|
84
|
+
this.timer ??= setInterval(() => void this.refresh(), intervalMs);
|
|
85
|
+
this.timer.unref?.();
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
stop() {
|
|
89
|
+
if (this.timer)
|
|
90
|
+
clearInterval(this.timer);
|
|
91
|
+
this.timer = undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export class TiDBBreakerState extends SqlBreakerState {
|
|
95
|
+
constructor(pool, onWriteFail) {
|
|
96
|
+
super(mysqlDriver(pool), onWriteFail);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export class PgBreakerState extends SqlBreakerState {
|
|
100
|
+
constructor(pool, onWriteFail) {
|
|
101
|
+
super(pgDriver(pool), onWriteFail);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=breaker-state-sql.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
2
|
+
import type { Pool as PgPool } from "pg";
|
|
3
|
+
import { type Checkpoint, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor } from "@sema-agent/core";
|
|
4
|
+
import { type SqlDriver } from "./sql-driver.js";
|
|
5
|
+
export declare const TERMINAL_BACKSTOP_MS: number;
|
|
6
|
+
export declare const TERMINAL_GRACE_MS = 3600000;
|
|
7
|
+
export declare function tokenFingerprint(token: string): string;
|
|
8
|
+
export interface PendingCheckpoint {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
scope: string;
|
|
11
|
+
toolName: string | null;
|
|
12
|
+
toolCallId: string | null;
|
|
13
|
+
boundCallId: string | null;
|
|
14
|
+
boundInputHash: string | null;
|
|
15
|
+
taskId: string | null;
|
|
16
|
+
input: unknown;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
deadline: number | null;
|
|
19
|
+
riskDescriptor: RiskDescriptor | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function boundedToolInput(args: unknown): unknown;
|
|
22
|
+
export declare class SqlCheckpointStore implements CheckpointStore {
|
|
23
|
+
protected readonly db: SqlDriver;
|
|
24
|
+
protected readonly logger?: {
|
|
25
|
+
info?(msg: string, meta?: unknown): void;
|
|
26
|
+
} | undefined;
|
|
27
|
+
constructor(db: SqlDriver, logger?: {
|
|
28
|
+
info?(msg: string, meta?: unknown): void;
|
|
29
|
+
} | undefined);
|
|
30
|
+
private q;
|
|
31
|
+
private json;
|
|
32
|
+
private isDupKey;
|
|
33
|
+
put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
|
|
34
|
+
get(token: CheckpointToken): Promise<Checkpoint | null>;
|
|
35
|
+
resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>;
|
|
36
|
+
reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
|
|
37
|
+
setPendingSteer(token: CheckpointToken, scope: string, steer: {
|
|
38
|
+
text: string;
|
|
39
|
+
trusted: boolean;
|
|
40
|
+
}): Promise<boolean>;
|
|
41
|
+
expire(token: CheckpointToken, scope: string): Promise<boolean>;
|
|
42
|
+
reap(scope: string, cutoff: number): Promise<number>;
|
|
43
|
+
reapExpired(cutoff: number): Promise<number>;
|
|
44
|
+
listExpiredApprovalGates(cutoff: number, limit?: number): Promise<Array<{
|
|
45
|
+
sessionId: string;
|
|
46
|
+
scope: string;
|
|
47
|
+
}>>;
|
|
48
|
+
listPending(scope?: string): Promise<PendingCheckpoint[]>;
|
|
49
|
+
listByScope(scope: string): Promise<CheckpointSummary[]>;
|
|
50
|
+
findPendingTokenBySession(sessionId: string, scope?: string): Promise<CheckpointToken | null>;
|
|
51
|
+
peekPendingScope(sessionId: string): Promise<string | null | undefined>;
|
|
52
|
+
putCtx(sessionId: string, ctx: unknown): Promise<void>;
|
|
53
|
+
getCtx<T = unknown>(sessionId: string): Promise<T | null>;
|
|
54
|
+
reapCtx(cutoff: number): Promise<number>;
|
|
55
|
+
deleteBySession(sessionId: string, owner: string | null): Promise<number>;
|
|
56
|
+
}
|
|
57
|
+
export declare class TiDBCheckpointStore extends SqlCheckpointStore {
|
|
58
|
+
constructor(pool: MySqlPool, logger?: {
|
|
59
|
+
info?(msg: string, meta?: unknown): void;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export declare class PgCheckpointStore extends SqlCheckpointStore {
|
|
63
|
+
constructor(pool: PgPool, logger?: {
|
|
64
|
+
info?(msg: string, meta?: unknown): void;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=checkpoint-store-sql.d.ts.map
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { CheckpointError, validatePendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, } from "@sema-agent/core";
|
|
3
|
+
import { redactDeep } from "../trace/redact.js";
|
|
4
|
+
import { pgProtocolJsonStringify } from "./pg-safe-json.js";
|
|
5
|
+
import { parseJsonStrict as parseJson } from "./sql-row-helpers.js";
|
|
6
|
+
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
7
|
+
const MAX_TOOL_INPUT_CHARS = 8192;
|
|
8
|
+
export const TERMINAL_BACKSTOP_MS = Math.max(60_000, Number(process.env.APPROVAL_TERMINAL_BACKSTOP_MS) || 30 * 86_400_000);
|
|
9
|
+
export const TERMINAL_GRACE_MS = 3_600_000;
|
|
10
|
+
export function tokenFingerprint(token) {
|
|
11
|
+
return "sha256:" + createHash("sha256").update(token).digest("hex").slice(0, 12);
|
|
12
|
+
}
|
|
13
|
+
export function boundedToolInput(args) {
|
|
14
|
+
if (args === undefined)
|
|
15
|
+
return null;
|
|
16
|
+
const redacted = redactDeep(args);
|
|
17
|
+
const json = JSON.stringify(redacted);
|
|
18
|
+
if (json === undefined)
|
|
19
|
+
return null;
|
|
20
|
+
if (json.length > MAX_TOOL_INPUT_CHARS)
|
|
21
|
+
return { truncated: true, bytes: json.length };
|
|
22
|
+
return redacted;
|
|
23
|
+
}
|
|
24
|
+
const DUP_ENTRY = 1062;
|
|
25
|
+
const PG_UNIQUE_VIOLATION = "23505";
|
|
26
|
+
export class SqlCheckpointStore {
|
|
27
|
+
db;
|
|
28
|
+
logger;
|
|
29
|
+
constructor(db, logger) {
|
|
30
|
+
this.db = db;
|
|
31
|
+
this.logger = logger;
|
|
32
|
+
}
|
|
33
|
+
q(tidb, pg) {
|
|
34
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
35
|
+
}
|
|
36
|
+
json(value, label) {
|
|
37
|
+
return this.db.dialect === "tidb" ? JSON.stringify(value) : pgProtocolJsonStringify(value, label);
|
|
38
|
+
}
|
|
39
|
+
isDupKey(e) {
|
|
40
|
+
return this.db.dialect === "tidb"
|
|
41
|
+
? e?.errno === DUP_ENTRY
|
|
42
|
+
: e?.code === PG_UNIQUE_VIOLATION;
|
|
43
|
+
}
|
|
44
|
+
async put(token, cp) {
|
|
45
|
+
const pa = cp.pendingAction;
|
|
46
|
+
const version = checkpointVersionOf(cp);
|
|
47
|
+
const toolInput = boundedToolInput(pa?.args);
|
|
48
|
+
try {
|
|
49
|
+
await this.db.query(this.q("INSERT INTO checkpoint (token, scope, session_id, version, status, tool_name, tool_call_id, tool_input, checkpoint, deadline, created_at, terminal_at, gate_kind, bound_input_hash, risk_descriptor) " +
|
|
50
|
+
"VALUES (?,?,?,?,'pending',?,?,?,?,?,?,?,?,?,?)", "INSERT INTO checkpoint (token, scope, session_id, version, status, tool_name, tool_call_id, tool_input, checkpoint, deadline, created_at, terminal_at, gate_kind, bound_input_hash, risk_descriptor) " +
|
|
51
|
+
"VALUES ($1,$2,$3,$4,'pending',$5,$6,$7::jsonb,$8::jsonb,$9,$10,$11,$12,$13,$14)"), [
|
|
52
|
+
token,
|
|
53
|
+
cp.scope,
|
|
54
|
+
cp.sessionId,
|
|
55
|
+
version,
|
|
56
|
+
pa?.toolName ?? null,
|
|
57
|
+
pa?.toolCallId ?? null,
|
|
58
|
+
toolInput === null ? null : this.json(toolInput, "approval tool_input"),
|
|
59
|
+
this.json(cp, "checkpoint"),
|
|
60
|
+
cp.deadline ?? null,
|
|
61
|
+
cp.createdAt,
|
|
62
|
+
Math.max(cp.createdAt + TERMINAL_BACKSTOP_MS, (cp.deadline ?? 0) + TERMINAL_GRACE_MS),
|
|
63
|
+
cp.gate?.kind ?? null,
|
|
64
|
+
pa?.boundInputHash ?? null,
|
|
65
|
+
((g) => (g?.riskDescriptor ? this.json(g.riskDescriptor, "risk descriptor") : null))(cp.gate),
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
if (this.isDupKey(e)) {
|
|
70
|
+
throw new CheckpointError("checkpoint.already_exists", `checkpoint token already exists`);
|
|
71
|
+
}
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async get(token) {
|
|
76
|
+
const { rows } = await this.db.query(this.q("SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer FROM checkpoint WHERE token = ?", "SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer FROM checkpoint WHERE token = $1"), [token]);
|
|
77
|
+
const r = rows[0];
|
|
78
|
+
if (!r)
|
|
79
|
+
return null;
|
|
80
|
+
const version = Number(r.version);
|
|
81
|
+
if (version > MAX_SUPPORTED_CHECKPOINT_VERSION) {
|
|
82
|
+
throw new Error(`checkpoint format version ${version} is newer than this build supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION}) — upgrade the service to resume it`);
|
|
83
|
+
}
|
|
84
|
+
const cp = parseJson(r.checkpoint);
|
|
85
|
+
cp.status = r.status;
|
|
86
|
+
const outcomeRaw = parseJson(r.outcome);
|
|
87
|
+
if (outcomeRaw)
|
|
88
|
+
cp.resolvedOutcome = winnerFromOutcome(outcomeRaw);
|
|
89
|
+
cp.reopenReason = r.reopen_reason ?? undefined;
|
|
90
|
+
cp.rev = r.rev == null ? 0 : Number(r.rev);
|
|
91
|
+
const steerRaw = parseJson(r.pending_steer);
|
|
92
|
+
if (steerRaw && cp.state)
|
|
93
|
+
cp.state.pendingSteer = steerRaw;
|
|
94
|
+
this.logger?.info?.("checkpoint_get", {
|
|
95
|
+
tokenFp: tokenFingerprint(token),
|
|
96
|
+
status: cp.status,
|
|
97
|
+
version,
|
|
98
|
+
hasWorkspaceHandle: !!cp.state?.workspaceHandle,
|
|
99
|
+
snapshotId: cp.state?.workspaceHandle?.snapshotId,
|
|
100
|
+
});
|
|
101
|
+
return cp;
|
|
102
|
+
}
|
|
103
|
+
async resolve(token, scope, outcome, expect) {
|
|
104
|
+
const params = [this.json(outcome, "checkpoint outcome"), Date.now(), token, scope];
|
|
105
|
+
if (expect)
|
|
106
|
+
params.push(expect.rev);
|
|
107
|
+
const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'resolved', outcome = ?, decided_at = ?, rev = rev + 1, reopen_reason = NULL WHERE token = ? AND scope = ? AND status = 'pending'" +
|
|
108
|
+
(expect ? " AND rev = ?" : ""), "UPDATE checkpoint SET status = 'resolved', outcome = $1::jsonb, decided_at = $2, rev = rev + 1, reopen_reason = NULL WHERE token = $3 AND scope = $4 AND status = 'pending'" +
|
|
109
|
+
(expect ? " AND rev = $5" : "")), params);
|
|
110
|
+
return res.affected === 1;
|
|
111
|
+
}
|
|
112
|
+
async reopen(token, scope, reason) {
|
|
113
|
+
const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'pending', reopen_reason = ?, rev = rev + 1 WHERE token = ? AND scope = ? AND status = 'resolved'", "UPDATE checkpoint SET status = 'pending', reopen_reason = $1, rev = rev + 1 WHERE token = $2 AND scope = $3 AND status = 'resolved'"), [reason, token, scope]);
|
|
114
|
+
return res.affected === 1;
|
|
115
|
+
}
|
|
116
|
+
async setPendingSteer(token, scope, steer) {
|
|
117
|
+
const clean = validatePendingSteer(steer);
|
|
118
|
+
const res = await this.db.query(this.q("UPDATE checkpoint SET pending_steer = ? WHERE token = ? AND scope = ? AND status = 'pending'", "UPDATE checkpoint SET pending_steer = $1 WHERE token = $2 AND scope = $3 AND status = 'pending'"), [this.json(clean, "pending steer"), token, scope]);
|
|
119
|
+
return res.affected > 0;
|
|
120
|
+
}
|
|
121
|
+
async expire(token, scope) {
|
|
122
|
+
const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'expired', decided_at = ? WHERE token = ? AND scope = ? AND status = 'pending'", "UPDATE checkpoint SET status = 'expired', decided_at = $1 WHERE token = $2 AND scope = $3 AND status = 'pending'"), [Date.now(), token, scope]);
|
|
123
|
+
return res.affected === 1;
|
|
124
|
+
}
|
|
125
|
+
async reap(scope, cutoff) {
|
|
126
|
+
const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'expired', decided_at = ? WHERE scope = ? AND status = 'pending' AND deadline IS NOT NULL AND deadline <= ?", "UPDATE checkpoint SET status = 'expired', decided_at = $1 WHERE scope = $2 AND status = 'pending' AND deadline IS NOT NULL AND deadline <= $3"), [Date.now(), scope, cutoff]);
|
|
127
|
+
return res.affected;
|
|
128
|
+
}
|
|
129
|
+
async reapExpired(cutoff) {
|
|
130
|
+
const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'expired', decided_at = ? " +
|
|
131
|
+
"WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= ? AND (gate_kind IS NULL OR gate_kind NOT IN ('human','irreversible_ask') OR COALESCE(tool_name,'') = 'AskUserQuestion')) OR (terminal_at IS NOT NULL AND terminal_at <= ?))", "UPDATE checkpoint SET status = 'expired', decided_at = $1 " +
|
|
132
|
+
"WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= $2 AND (gate_kind IS NULL OR gate_kind NOT IN ('human','irreversible_ask') OR COALESCE(tool_name,'') = 'AskUserQuestion')) OR (terminal_at IS NOT NULL AND terminal_at <= $3))"), [Date.now(), cutoff, cutoff]);
|
|
133
|
+
return res.affected;
|
|
134
|
+
}
|
|
135
|
+
async listExpiredApprovalGates(cutoff, limit = 100) {
|
|
136
|
+
const { rows } = await this.db.query(this.q("SELECT session_id, scope FROM checkpoint WHERE status = 'pending' AND gate_kind IN ('human','irreversible_ask') " +
|
|
137
|
+
"AND COALESCE(tool_name,'') <> 'AskUserQuestion' AND deadline IS NOT NULL AND deadline <= ? ORDER BY deadline ASC LIMIT ?", "SELECT session_id, scope FROM checkpoint WHERE status = 'pending' AND gate_kind IN ('human','irreversible_ask') " +
|
|
138
|
+
"AND COALESCE(tool_name,'') <> 'AskUserQuestion' AND deadline IS NOT NULL AND deadline <= $1 ORDER BY deadline ASC LIMIT $2"), [cutoff, limit]);
|
|
139
|
+
return rows.map((r) => ({ sessionId: String(r.session_id), scope: String(r.scope) }));
|
|
140
|
+
}
|
|
141
|
+
async listPending(scope) {
|
|
142
|
+
const base = "SELECT c.session_id, c.scope, c.tool_name, c.tool_call_id, c.tool_input, c.bound_input_hash, c.risk_descriptor, c.created_at, c.deadline, ta.task_id " +
|
|
143
|
+
"FROM checkpoint c LEFT JOIN task_active ta ON ta.session_id = c.session_id WHERE c.status='pending'";
|
|
144
|
+
const { rows } = scope
|
|
145
|
+
? await this.db.query(`${base}${this.q(" AND c.scope=?", " AND c.scope=$1")} ORDER BY c.created_at ASC`, [scope])
|
|
146
|
+
: await this.db.query(`${base} ORDER BY c.created_at ASC`);
|
|
147
|
+
const out = rows.map((r) => {
|
|
148
|
+
const toolCallId = r.tool_call_id ?? null;
|
|
149
|
+
return {
|
|
150
|
+
sessionId: String(r.session_id),
|
|
151
|
+
scope: String(r.scope),
|
|
152
|
+
toolName: r.tool_name ?? null,
|
|
153
|
+
toolCallId,
|
|
154
|
+
boundCallId: toolCallId,
|
|
155
|
+
boundInputHash: r.bound_input_hash ?? null,
|
|
156
|
+
taskId: r.task_id ?? null,
|
|
157
|
+
input: r.tool_input ?? null,
|
|
158
|
+
createdAt: Number(r.created_at),
|
|
159
|
+
deadline: r.deadline == null ? null : Number(r.deadline),
|
|
160
|
+
riskDescriptor: parseJson(r.risk_descriptor),
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
return out.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
|
|
164
|
+
}
|
|
165
|
+
async listByScope(scope) {
|
|
166
|
+
const { rows } = await this.db.query(this.q("SELECT checkpoint, status FROM checkpoint WHERE status = 'pending' AND scope = ? ORDER BY created_at ASC LIMIT 500", "SELECT checkpoint, status FROM checkpoint WHERE status = 'pending' AND scope = $1 ORDER BY created_at ASC LIMIT 500"), [scope]);
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const r of rows) {
|
|
169
|
+
const cp = parseJson(r.checkpoint);
|
|
170
|
+
if (!cp)
|
|
171
|
+
continue;
|
|
172
|
+
cp.status = r.status;
|
|
173
|
+
out.push(summarizeCheckpoint(cp));
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
async findPendingTokenBySession(sessionId, scope) {
|
|
178
|
+
const { rows } = scope
|
|
179
|
+
? await this.db.query(this.q("SELECT token FROM checkpoint WHERE session_id=? AND scope=? AND status='pending' LIMIT 1", "SELECT token FROM checkpoint WHERE session_id=$1 AND scope=$2 AND status='pending' LIMIT 1"), [sessionId, scope])
|
|
180
|
+
: await this.db.query(this.q("SELECT token FROM checkpoint WHERE session_id=? AND status='pending' LIMIT 1", "SELECT token FROM checkpoint WHERE session_id=$1 AND status='pending' LIMIT 1"), [sessionId]);
|
|
181
|
+
return rows[0] ? String(rows[0].token) : null;
|
|
182
|
+
}
|
|
183
|
+
async peekPendingScope(sessionId) {
|
|
184
|
+
const { rows } = await this.db.query(this.q("SELECT scope FROM checkpoint WHERE session_id=? AND status='pending' LIMIT 1", "SELECT scope FROM checkpoint WHERE session_id=$1 AND status='pending' LIMIT 1"), [sessionId]);
|
|
185
|
+
if (!rows[0])
|
|
186
|
+
return undefined;
|
|
187
|
+
return rows[0].scope ?? null;
|
|
188
|
+
}
|
|
189
|
+
async putCtx(sessionId, ctx) {
|
|
190
|
+
await this.db.query(this.q("INSERT INTO checkpoint_ctx (session_id, ctx, updated_at) VALUES (?,?,?) " +
|
|
191
|
+
"ON DUPLICATE KEY UPDATE ctx = VALUES(ctx), updated_at = VALUES(updated_at)", "INSERT INTO checkpoint_ctx (session_id, ctx, updated_at) VALUES ($1,$2::jsonb,$3) " +
|
|
192
|
+
"ON CONFLICT (session_id) DO UPDATE SET ctx = EXCLUDED.ctx, updated_at = EXCLUDED.updated_at"), [sessionId, this.json(ctx, "checkpoint ctx"), Date.now()]);
|
|
193
|
+
}
|
|
194
|
+
async getCtx(sessionId) {
|
|
195
|
+
const { rows } = await this.db.query(this.q("SELECT ctx FROM checkpoint_ctx WHERE session_id=?", "SELECT ctx FROM checkpoint_ctx WHERE session_id=$1"), [sessionId]);
|
|
196
|
+
return rows[0] ? parseJson(rows[0].ctx) : null;
|
|
197
|
+
}
|
|
198
|
+
async reapCtx(cutoff) {
|
|
199
|
+
const res = await this.db.query(this.q("DELETE FROM checkpoint_ctx WHERE updated_at < ? " +
|
|
200
|
+
"AND session_id NOT IN (SELECT session_id FROM checkpoint WHERE status='pending') " +
|
|
201
|
+
"AND session_id NOT IN (SELECT session_id FROM task_active)", "DELETE FROM checkpoint_ctx WHERE updated_at < $1 " +
|
|
202
|
+
"AND session_id NOT IN (SELECT session_id FROM checkpoint WHERE status='pending') " +
|
|
203
|
+
"AND session_id NOT IN (SELECT session_id FROM task_active)"), [cutoff]);
|
|
204
|
+
return res.affected;
|
|
205
|
+
}
|
|
206
|
+
async deleteBySession(sessionId, owner) {
|
|
207
|
+
const ownerGuard = this.q("EXISTS (SELECT 1 FROM session_meta sm WHERE sm.session_id = ? AND sm.owner <=> ?)", "EXISTS (SELECT 1 FROM session_meta sm WHERE sm.session_id = $1 AND sm.owner IS NOT DISTINCT FROM $2)");
|
|
208
|
+
const params = this.db.dialect === "tidb" ? [sessionId, sessionId, owner] : [sessionId, owner];
|
|
209
|
+
const res = await this.db.query(this.q(`DELETE FROM checkpoint WHERE session_id = ? AND ${ownerGuard}`, `DELETE FROM checkpoint WHERE session_id = $1 AND ${ownerGuard}`), params);
|
|
210
|
+
await this.db.query(this.q(`DELETE FROM checkpoint_ctx WHERE session_id = ? AND ${ownerGuard}`, `DELETE FROM checkpoint_ctx WHERE session_id = $1 AND ${ownerGuard}`), params);
|
|
211
|
+
return res.affected;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export class TiDBCheckpointStore extends SqlCheckpointStore {
|
|
215
|
+
constructor(pool, logger) {
|
|
216
|
+
super(mysqlDriver(pool), logger);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export class PgCheckpointStore extends SqlCheckpointStore {
|
|
220
|
+
constructor(pool, logger) {
|
|
221
|
+
super(pgDriver(pool), logger);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=checkpoint-store-sql.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type FileSnapshotBounds, type FileSnapshotStore, type FileSnapshotResult, type ExecutionEnv } from "@sema-agent/core";
|
|
2
|
+
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
3
|
+
import type { Pool as PgPool } from "pg";
|
|
4
|
+
import { type BlobBackend } from "./blob-backend.js";
|
|
5
|
+
import { type SqlDriver } from "./sql-driver.js";
|
|
6
|
+
export declare class SqlFileSnapshotStore implements FileSnapshotStore {
|
|
7
|
+
protected readonly db: SqlDriver;
|
|
8
|
+
protected readonly pool: MySqlPool | PgPool;
|
|
9
|
+
protected readonly bounds: FileSnapshotBounds;
|
|
10
|
+
private readonly inFlight;
|
|
11
|
+
private readonly blobs;
|
|
12
|
+
onSkippedVanished: ((count: number) => void | Promise<void>) | undefined;
|
|
13
|
+
constructor(db: SqlDriver, pool: MySqlPool | PgPool, blobBackend?: BlobBackend, bounds?: FileSnapshotBounds);
|
|
14
|
+
private q;
|
|
15
|
+
private putBlobRow;
|
|
16
|
+
putBlob(hash: string, bytes: Uint8Array): Promise<FileSnapshotResult>;
|
|
17
|
+
snapshot(scope: string, key: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileSnapshotResult>;
|
|
18
|
+
importManifest(scope: string, key: string, manifest: Map<string, string>, srcGetBlob: (hash: string) => Promise<Uint8Array | undefined>): Promise<FileSnapshotResult>;
|
|
19
|
+
restore(scope: string, key: string, env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<FileSnapshotResult>;
|
|
20
|
+
has(scope: string, key: string): Promise<boolean>;
|
|
21
|
+
listKeys(scope: string): Promise<string[]>;
|
|
22
|
+
exportManifest(scope: string, key: string): Promise<Map<string, string> | null>;
|
|
23
|
+
getBlob(hash: string): Promise<Uint8Array | undefined>;
|
|
24
|
+
hasBlobs(hashes: string[]): Promise<Set<string>>;
|
|
25
|
+
blobSizes(hashes: string[]): Promise<Map<string, number>>;
|
|
26
|
+
reap(scope: string, keepKeys: string[]): Promise<number>;
|
|
27
|
+
deleteBySession(scope: string): Promise<number>;
|
|
28
|
+
private gcOrphanBlobs;
|
|
29
|
+
sweepOrphanBlobs(): Promise<number>;
|
|
30
|
+
private tx;
|
|
31
|
+
}
|
|
32
|
+
export declare class TiDBFileSnapshotStore extends SqlFileSnapshotStore {
|
|
33
|
+
constructor(pool: MySqlPool, blobBackend?: BlobBackend, bounds?: FileSnapshotBounds);
|
|
34
|
+
}
|
|
35
|
+
export declare class PgFileSnapshotStore extends SqlFileSnapshotStore {
|
|
36
|
+
constructor(pool: PgPool, blobBackend?: BlobBackend, bounds?: FileSnapshotBounds);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=file-snapshot-store-sql.d.ts.map
|