@sema-agent/server 1.315.0 → 1.317.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 (133) hide show
  1. package/README.md +9 -0
  2. package/README.zh-CN.md +7 -0
  3. package/dist/config-types.d.ts +1 -1
  4. package/dist/config.d.ts +14 -0
  5. package/dist/config.js +148 -45
  6. package/dist/elicitation.js +2 -2
  7. package/dist/http/idempotency.d.ts +1 -0
  8. package/dist/http/idempotency.js +3 -0
  9. package/dist/http/principal-gate.d.ts +12 -0
  10. package/dist/http/principal-gate.js +52 -0
  11. package/dist/http/route-ctx.d.ts +116 -0
  12. package/dist/http/route-ctx.js +2 -0
  13. package/dist/http/routes/approvals-assistant.d.ts +11 -0
  14. package/dist/http/routes/approvals-assistant.js +530 -0
  15. package/dist/http/routes/attachments.d.ts +4 -0
  16. package/dist/http/routes/attachments.js +106 -0
  17. package/dist/http/routes/capabilities.d.ts +4 -0
  18. package/dist/http/routes/capabilities.js +111 -0
  19. package/dist/http/routes/fleet.d.ts +4 -0
  20. package/dist/http/routes/fleet.js +147 -0
  21. package/dist/http/routes/images.d.ts +20 -0
  22. package/dist/http/routes/images.js +572 -0
  23. package/dist/http/routes/leader.d.ts +4 -0
  24. package/dist/http/routes/leader.js +48 -0
  25. package/dist/http/routes/memory-policy.d.ts +4 -0
  26. package/dist/http/routes/memory-policy.js +105 -0
  27. package/dist/http/routes/notify-wake.d.ts +4 -0
  28. package/dist/http/routes/notify-wake.js +133 -0
  29. package/dist/http/routes/observability.d.ts +4 -0
  30. package/dist/http/routes/observability.js +63 -0
  31. package/dist/http/routes/runs.d.ts +19 -0
  32. package/dist/http/routes/runs.js +967 -0
  33. package/dist/http/routes/session-sync.d.ts +36 -0
  34. package/dist/http/routes/session-sync.js +513 -0
  35. package/dist/http/routes/sessions-list.d.ts +4 -0
  36. package/dist/http/routes/sessions-list.js +117 -0
  37. package/dist/http/routes/sessions.d.ts +17 -0
  38. package/dist/http/routes/sessions.js +746 -0
  39. package/dist/http/routes/side-query.d.ts +4 -0
  40. package/dist/http/routes/side-query.js +88 -0
  41. package/dist/http/routes/tasks.d.ts +4 -0
  42. package/dist/http/routes/tasks.js +632 -0
  43. package/dist/http/routes/trace-usage.d.ts +4 -0
  44. package/dist/http/routes/trace-usage.js +239 -0
  45. package/dist/http/routes/workflows.d.ts +5 -0
  46. package/dist/http/routes/workflows.js +337 -0
  47. package/dist/http/run-meta.d.ts +11 -0
  48. package/dist/http/run-meta.js +16 -0
  49. package/dist/http/send.d.ts +7 -0
  50. package/dist/http/send.js +34 -0
  51. package/dist/http/server.d.ts +12 -12
  52. package/dist/http/server.js +289 -5502
  53. package/dist/http/sse-log.d.ts +25 -0
  54. package/dist/http/sse-log.js +59 -0
  55. package/dist/http/wire-types.d.ts +6 -0
  56. package/dist/http/workspace-content.d.ts +3 -0
  57. package/dist/http/workspace-content.js +20 -0
  58. package/dist/leader/endpoint.js +4 -4
  59. package/dist/main.js +5 -5
  60. package/dist/plugins/approval-store-sql.d.ts +52 -0
  61. package/dist/plugins/approval-store-sql.js +119 -0
  62. package/dist/plugins/breaker-state-sql.d.ts +29 -0
  63. package/dist/plugins/breaker-state-sql.js +104 -0
  64. package/dist/plugins/file-snapshot-store-sql.d.ts +38 -0
  65. package/dist/plugins/file-snapshot-store-sql.js +210 -0
  66. package/dist/plugins/image-index-sql.d.ts +33 -0
  67. package/dist/plugins/image-index-sql.js +328 -0
  68. package/dist/plugins/outcome-ledger-sql.d.ts +70 -0
  69. package/dist/plugins/outcome-ledger-sql.js +158 -0
  70. package/dist/plugins/pg-approval-store.d.ts +1 -27
  71. package/dist/plugins/pg-approval-store.js +1 -83
  72. package/dist/plugins/pg-breaker-state.d.ts +1 -19
  73. package/dist/plugins/pg-breaker-state.js +1 -88
  74. package/dist/plugins/pg-file-snapshot-store.d.ts +1 -27
  75. package/dist/plugins/pg-file-snapshot-store.js +1 -190
  76. package/dist/plugins/pg-image-index.d.ts +1 -21
  77. package/dist/plugins/pg-image-index.js +1 -217
  78. package/dist/plugins/pg-outcome-ledger.d.ts +2 -22
  79. package/dist/plugins/pg-outcome-ledger.js +1 -104
  80. package/dist/plugins/pg-resume-anchor-store.d.ts +1 -13
  81. package/dist/plugins/pg-resume-anchor-store.js +1 -22
  82. package/dist/plugins/pg-run-store.d.ts +1 -75
  83. package/dist/plugins/pg-run-store.js +1 -287
  84. package/dist/plugins/pg-session-policy-store.d.ts +1 -11
  85. package/dist/plugins/pg-session-policy-store.js +1 -90
  86. package/dist/plugins/pg-tool-result-store.d.ts +1 -15
  87. package/dist/plugins/pg-tool-result-store.js +1 -52
  88. package/dist/plugins/pg-workflow-journal-store.d.ts +1 -20
  89. package/dist/plugins/pg-workflow-journal-store.js +1 -34
  90. package/dist/plugins/pg-workflow-run-store.d.ts +1 -55
  91. package/dist/plugins/pg-workflow-run-store.js +1 -220
  92. package/dist/plugins/remote-env-host.js +4 -3
  93. package/dist/plugins/resume-anchor-store-sql.d.ts +23 -0
  94. package/dist/plugins/resume-anchor-store-sql.js +38 -0
  95. package/dist/plugins/run-store-sql.d.ts +89 -0
  96. package/dist/plugins/run-store-sql.js +397 -0
  97. package/dist/plugins/session-policy-store-sql.d.ts +22 -0
  98. package/dist/plugins/session-policy-store-sql.js +109 -0
  99. package/dist/plugins/tidb-approval-store.d.ts +1 -39
  100. package/dist/plugins/tidb-approval-store.js +1 -71
  101. package/dist/plugins/tidb-breaker-state.d.ts +1 -17
  102. package/dist/plugins/tidb-breaker-state.js +1 -74
  103. package/dist/plugins/tidb-file-snapshot-store.d.ts +1 -27
  104. package/dist/plugins/tidb-file-snapshot-store.js +1 -192
  105. package/dist/plugins/tidb-image-index.d.ts +1 -19
  106. package/dist/plugins/tidb-image-index.js +1 -149
  107. package/dist/plugins/tidb-outcome-ledger.d.ts +1 -57
  108. package/dist/plugins/tidb-outcome-ledger.js +1 -97
  109. package/dist/plugins/tidb-resume-anchor-store.d.ts +1 -13
  110. package/dist/plugins/tidb-resume-anchor-store.js +1 -22
  111. package/dist/plugins/tidb-run-store.d.ts +1 -75
  112. package/dist/plugins/tidb-run-store.js +1 -306
  113. package/dist/plugins/tidb-session-policy-store.d.ts +1 -11
  114. package/dist/plugins/tidb-session-policy-store.js +1 -86
  115. package/dist/plugins/tidb-tool-result-store.d.ts +1 -13
  116. package/dist/plugins/tidb-tool-result-store.js +1 -38
  117. package/dist/plugins/tidb-workflow-journal-store.d.ts +1 -20
  118. package/dist/plugins/tidb-workflow-journal-store.js +1 -34
  119. package/dist/plugins/tidb-workflow-run-store.d.ts +1 -61
  120. package/dist/plugins/tidb-workflow-run-store.js +1 -231
  121. package/dist/plugins/tool-result-store-sql.d.ts +25 -0
  122. package/dist/plugins/tool-result-store-sql.js +69 -0
  123. package/dist/plugins/workflow-journal-store-sql.d.ts +30 -0
  124. package/dist/plugins/workflow-journal-store-sql.js +53 -0
  125. package/dist/plugins/workflow-run-store-sql.d.ts +85 -0
  126. package/dist/plugins/workflow-run-store-sql.js +299 -0
  127. package/dist/question.js +2 -2
  128. package/dist/run-local.js +1 -1
  129. package/dist/tool-approval.js +2 -2
  130. package/dist/trace/ledger-sink.js +1 -1
  131. package/dist/trace/project.d.ts +1 -0
  132. package/dist/trace/project.js +3 -0
  133. package/package.json +1 -1
@@ -0,0 +1,397 @@
1
+ import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
2
+ import { escapeLike } from "./sql-escape.js";
3
+ import { pgSafeJsonStringify, pgSanitizeText } from "./pg-safe-json.js";
4
+ import { parseJsonLenient as parseJson, toIso as iso } from "./sql-row-helpers.js";
5
+ import { mysqlDriver, pgDriver } from "./sql-driver.js";
6
+ export class SqlRunStore {
7
+ db;
8
+ constructor(db) {
9
+ this.db = db;
10
+ }
11
+ q(tidb, pg) {
12
+ return this.db.dialect === "tidb" ? tidb : pg;
13
+ }
14
+ isDupKey(err) {
15
+ const code = err?.code;
16
+ return Boolean(err) && (this.db.dialect === "tidb" ? code === "ER_DUP_ENTRY" : code === "23505");
17
+ }
18
+ json(value) {
19
+ return this.db.dialect === "tidb" ? JSON.stringify(value) : pgSafeJsonStringify(value);
20
+ }
21
+ async tx(fn) {
22
+ const conn = await this.db.connect();
23
+ try {
24
+ await conn.begin();
25
+ await fn(conn);
26
+ await conn.commit();
27
+ }
28
+ catch (err) {
29
+ try {
30
+ await conn.rollback();
31
+ }
32
+ catch {
33
+ }
34
+ throw err;
35
+ }
36
+ finally {
37
+ conn.release();
38
+ }
39
+ }
40
+ async createRun(taskId, sessionId, owner, instanceId, meta = {}) {
41
+ const conn = await this.db.connect();
42
+ try {
43
+ await conn.begin();
44
+ const now = new Date();
45
+ try {
46
+ await conn.query(this.q("INSERT INTO task_active (session_id, task_id, since) VALUES (?,?,?)", "INSERT INTO task_active (session_id, task_id, since) VALUES ($1,$2,$3)"), [sessionId, taskId, now]);
47
+ }
48
+ catch (err) {
49
+ if (this.isDupKey(err)) {
50
+ await conn.rollback();
51
+ const { rows: active } = await conn.query(this.q("SELECT task_id FROM task_active WHERE session_id = ?", "SELECT task_id FROM task_active WHERE session_id = $1"), [sessionId]);
52
+ return { ok: false, activeTaskId: String(active[0]?.task_id ?? "") };
53
+ }
54
+ throw err;
55
+ }
56
+ await conn.query(this.q("INSERT INTO task_run (task_id, session_id, owner, status, instance_id, job_id, source, objective_preview, created_at, updated_at) " +
57
+ "VALUES (?,?,?,'running',?,?,?,?,?,?)", "INSERT INTO task_run (task_id, session_id, owner, status, instance_id, job_id, source, objective_preview, created_at, updated_at) " +
58
+ "VALUES ($1,$2,$3,'running',$4,$5,$6,$7,$8,$9)"), [
59
+ taskId,
60
+ sessionId,
61
+ owner,
62
+ instanceId,
63
+ meta.jobId ?? null,
64
+ meta.source ?? null,
65
+ this.db.dialect === "tidb"
66
+ ? meta.objectivePreview ?? null
67
+ : meta.objectivePreview == null
68
+ ? null
69
+ : pgSanitizeText(meta.objectivePreview),
70
+ now,
71
+ now,
72
+ ]);
73
+ await conn.commit();
74
+ return { ok: true };
75
+ }
76
+ catch (err) {
77
+ try {
78
+ await conn.rollback();
79
+ }
80
+ catch {
81
+ }
82
+ throw err;
83
+ }
84
+ finally {
85
+ conn.release();
86
+ }
87
+ }
88
+ async requestCancel(taskId, owner) {
89
+ const { affected } = await this.db.query(this.q("UPDATE task_run SET cancel_requested = 1 WHERE task_id = ? AND owner <=> ? AND status = 'running'", "UPDATE task_run SET cancel_requested = 1 WHERE task_id = $1 AND owner IS NOT DISTINCT FROM $2 AND status = 'running'"), [taskId, owner]);
90
+ return affected > 0;
91
+ }
92
+ async isCancelRequested(taskId, owner) {
93
+ const { rows } = await this.db.query(this.q("SELECT cancel_requested FROM task_run WHERE task_id = ? AND owner <=> ?", "SELECT cancel_requested FROM task_run WHERE task_id = $1 AND owner IS NOT DISTINCT FROM $2"), [taskId, owner]);
94
+ return Boolean(rows[0]?.cancel_requested);
95
+ }
96
+ async requestPreempt(taskId, owner) {
97
+ const { affected } = await this.db.query(this.q("UPDATE task_run SET preempt_requested = 1 WHERE task_id = ? AND owner <=> ? AND status = 'running'", "UPDATE task_run SET preempt_requested = 1 WHERE task_id = $1 AND owner IS NOT DISTINCT FROM $2 AND status = 'running'"), [taskId, owner]);
98
+ return affected > 0;
99
+ }
100
+ async isPreemptRequested(taskId, owner) {
101
+ const { rows } = await this.db.query(this.q("SELECT preempt_requested FROM task_run WHERE task_id = ? AND owner <=> ?", "SELECT preempt_requested FROM task_run WHERE task_id = $1 AND owner IS NOT DISTINCT FROM $2"), [taskId, owner]);
102
+ return Boolean(rows[0]?.preempt_requested);
103
+ }
104
+ async heartbeat(taskId, owner) {
105
+ await this.db.query(this.q("UPDATE task_run SET updated_at = ? WHERE task_id = ? AND owner <=> ? AND status = 'running'", "UPDATE task_run SET updated_at = $1 WHERE task_id = $2 AND owner IS NOT DISTINCT FROM $3 AND status = 'running'"), [new Date(), taskId, owner]);
106
+ }
107
+ async appendEvent(taskId, seq, type, data) {
108
+ await this.db.query(this.q("INSERT INTO task_event (task_id, seq, type, data, ts) VALUES (?,?,?,?,?)", "INSERT INTO task_event (task_id, seq, type, data, ts) VALUES ($1,$2,$3,$4::jsonb,$5)"), [taskId, seq, type, data == null ? null : this.json(data), new Date()]);
109
+ }
110
+ async maxSeq(taskId) {
111
+ const { rows } = await this.db.query(this.q("SELECT MAX(seq) AS m FROM task_event WHERE task_id = ?", "SELECT MAX(seq) AS m FROM task_event WHERE task_id = $1"), [taskId]);
112
+ const m = rows[0]?.m;
113
+ return m == null ? 0 : Number(m);
114
+ }
115
+ async setTerminal(taskId, status, result, error) {
116
+ const errorCode = result?.errorCode ?? null;
117
+ await this.tx(async (conn) => {
118
+ await conn.query(this.q("UPDATE task_run SET status = ?, result = ?, error = ?, error_code = ?, updated_at = ? WHERE task_id = ? AND status IN ('running','suspended','needs_review')", "UPDATE task_run SET status = $1, result = $2::jsonb, error = $3, error_code = $4, updated_at = $5 WHERE task_id = $6 AND status IN ('running','suspended','needs_review')"), [
119
+ status,
120
+ result == null ? null : this.json(result),
121
+ this.db.dialect === "tidb" ? error : error == null ? null : pgSanitizeText(error),
122
+ this.db.dialect === "tidb" ? errorCode : errorCode == null ? null : pgSanitizeText(errorCode),
123
+ new Date(),
124
+ taskId,
125
+ ]);
126
+ await conn.query(this.q("DELETE FROM task_active WHERE task_id = ?", "DELETE FROM task_active WHERE task_id = $1"), [taskId]);
127
+ });
128
+ }
129
+ async setSuspended(taskId) {
130
+ await this.db.query(this.q("UPDATE task_run SET status = 'suspended', updated_at = ? WHERE task_id = ? AND status IN ('running','suspended')", "UPDATE task_run SET status = 'suspended', updated_at = $1 WHERE task_id = $2 AND status IN ('running','suspended')"), [new Date(), taskId]);
131
+ }
132
+ async setNeedsReview(taskId) {
133
+ await this.db.query(this.q("UPDATE task_run SET status = 'needs_review', updated_at = ? WHERE task_id = ? AND status IN ('running','needs_review')", "UPDATE task_run SET status = 'needs_review', updated_at = $1 WHERE task_id = $2 AND status IN ('running','needs_review')"), [new Date(), taskId]);
134
+ }
135
+ async markResuming(taskId) {
136
+ const { affected } = await this.db.query(this.q("UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0, updated_at = ? WHERE task_id = ? AND status IN ('suspended','needs_review')", "UPDATE task_run SET status = 'running', cancel_requested = 0, preempt_requested = 0, updated_at = $1 WHERE task_id = $2 AND status IN ('suspended','needs_review')"), [new Date(), taskId]);
137
+ return affected === 1;
138
+ }
139
+ async getActiveTaskId(sessionId) {
140
+ const { rows } = await this.db.query(this.q("SELECT task_id FROM task_active WHERE session_id = ?", "SELECT task_id FROM task_active WHERE session_id = $1"), [sessionId]);
141
+ return rows[0] ? String(rows[0].task_id) : undefined;
142
+ }
143
+ rowToRun(r) {
144
+ return {
145
+ taskId: String(r.task_id),
146
+ sessionId: String(r.session_id),
147
+ owner: r.owner ?? null,
148
+ status: r.status,
149
+ result: parseJson(r.result),
150
+ error: r.error ?? null,
151
+ errorCode: r.error_code ?? null,
152
+ jobId: r.job_id ?? null,
153
+ source: r.source ?? null,
154
+ instanceId: r.instance_id ?? null,
155
+ objectivePreview: r.objective_preview ?? null,
156
+ createdAt: iso(r.created_at),
157
+ updatedAt: iso(r.updated_at),
158
+ };
159
+ }
160
+ async getRun(taskId) {
161
+ const { rows } = await this.db.query(this.q("SELECT task_id, session_id, owner, status, result, error, error_code, job_id, source, instance_id, objective_preview, created_at, updated_at FROM task_run WHERE task_id = ?", "SELECT task_id, session_id, owner, status, result, error, error_code, job_id, source, instance_id, objective_preview, created_at, updated_at FROM task_run WHERE task_id = $1"), [taskId]);
162
+ return rows[0] ? this.rowToRun(rows[0]) : undefined;
163
+ }
164
+ async listRuns(opts) {
165
+ if (opts.cursor && Number.isNaN(Date.parse(opts.cursor.createdAt)))
166
+ return [];
167
+ if (this.db.dialect === "tidb") {
168
+ const where = [];
169
+ const params = [];
170
+ if (opts.status) {
171
+ where.push("status = ?");
172
+ params.push(opts.status);
173
+ }
174
+ if (opts.jobId) {
175
+ where.push("job_id = ?");
176
+ params.push(opts.jobId);
177
+ }
178
+ if (opts.source) {
179
+ where.push("source = ?");
180
+ params.push(opts.source);
181
+ }
182
+ if (opts.owner) {
183
+ where.push("owner = ?");
184
+ params.push(opts.owner);
185
+ }
186
+ if (opts.cursor) {
187
+ where.push("(created_at < ? OR (created_at = ? AND task_id < ?))");
188
+ params.push(new Date(opts.cursor.createdAt), new Date(opts.cursor.createdAt), opts.cursor.taskId);
189
+ }
190
+ const sql = "SELECT task_id, session_id, owner, status, result, error, error_code, job_id, source, objective_preview, created_at, updated_at FROM task_run" +
191
+ (where.length ? " WHERE " + where.join(" AND ") : "") +
192
+ " ORDER BY created_at DESC, task_id DESC LIMIT ?";
193
+ params.push(opts.limit);
194
+ const { rows } = await this.db.query(sql, params);
195
+ return rows.map((r) => this.rowToRun(r));
196
+ }
197
+ const params = [];
198
+ const p = (v) => {
199
+ params.push(v);
200
+ return `$${params.length}`;
201
+ };
202
+ const where = [];
203
+ if (opts.status)
204
+ where.push(`status = ${p(opts.status)}`);
205
+ if (opts.jobId)
206
+ where.push(`job_id = ${p(opts.jobId)}`);
207
+ if (opts.source)
208
+ where.push(`source = ${p(opts.source)}`);
209
+ if (opts.owner)
210
+ where.push(`owner = ${p(opts.owner)}`);
211
+ if (opts.cursor) {
212
+ const a = p(new Date(opts.cursor.createdAt));
213
+ const b = p(new Date(opts.cursor.createdAt));
214
+ const t = p(opts.cursor.taskId);
215
+ where.push(`(created_at < ${a} OR (created_at = ${b} AND task_id < ${t}))`);
216
+ }
217
+ const sql = "SELECT task_id, session_id, owner, status, result, error, error_code, job_id, source, objective_preview, created_at, updated_at FROM task_run" +
218
+ (where.length ? " WHERE " + where.join(" AND ") : "") +
219
+ ` ORDER BY created_at DESC, task_id DESC LIMIT ${p(opts.limit)}`;
220
+ const { rows } = await this.db.query(sql, params);
221
+ return rows.map((r) => this.rowToRun(r));
222
+ }
223
+ async listSessions(opts) {
224
+ if (this.db.dialect === "tidb") {
225
+ const params = [];
226
+ let innerWhere = "";
227
+ if (opts.owner) {
228
+ innerWhere = " WHERE owner = ?";
229
+ params.push(opts.owner);
230
+ }
231
+ const outer = ["rn = 1"];
232
+ if (opts.q) {
233
+ outer.push("EXISTS (SELECT 1 FROM task_run tr WHERE tr.session_id = t.session_id AND LOWER(tr.objective_preview) LIKE LOWER(?) ESCAPE '\\\\')");
234
+ params.push(`%${escapeLike(opts.q)}%`);
235
+ }
236
+ if (opts.cursor) {
237
+ outer.push("(last_activity < ? OR (last_activity = ? AND session_id < ?))");
238
+ params.push(new Date(opts.cursor.lastActivityAt), new Date(opts.cursor.lastActivityAt), opts.cursor.sessionId);
239
+ }
240
+ const sql = "SELECT session_id, owner, task_id, last_activity, first_activity, run_count, objective_preview, status FROM (" +
241
+ "SELECT session_id, owner, task_id, objective_preview, status," +
242
+ " ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY created_at DESC, task_id DESC) AS rn," +
243
+ " MAX(created_at) OVER (PARTITION BY session_id) AS last_activity," +
244
+ " MIN(created_at) OVER (PARTITION BY session_id) AS first_activity," +
245
+ " COUNT(*) OVER (PARTITION BY session_id) AS run_count" +
246
+ " FROM task_run" + innerWhere + ") t WHERE " + outer.join(" AND ") +
247
+ " ORDER BY last_activity DESC, session_id DESC LIMIT ?";
248
+ params.push(opts.limit);
249
+ const { rows } = await this.db.query(sql, params);
250
+ return rows.map((r) => this.rowToSession(r));
251
+ }
252
+ const params = [];
253
+ const p = (v) => {
254
+ params.push(v);
255
+ return `$${params.length}`;
256
+ };
257
+ const innerWhere = opts.owner ? ` WHERE owner = ${p(opts.owner)}` : "";
258
+ const outer = ["rn = 1"];
259
+ if (opts.q)
260
+ outer.push(`EXISTS (SELECT 1 FROM task_run tr WHERE tr.session_id = t.session_id AND LOWER(tr.objective_preview) LIKE LOWER(${p(`%${escapeLike(opts.q)}%`)}) ESCAPE '\\')`);
261
+ if (opts.cursor) {
262
+ const a = p(new Date(opts.cursor.lastActivityAt));
263
+ const b = p(new Date(opts.cursor.lastActivityAt));
264
+ const sid = p(opts.cursor.sessionId);
265
+ outer.push(`(last_activity < ${a} OR (last_activity = ${b} AND session_id < ${sid}))`);
266
+ }
267
+ const sql = "SELECT session_id, owner, task_id, last_activity, first_activity, run_count, objective_preview, status FROM (" +
268
+ "SELECT session_id, owner, task_id, objective_preview, status," +
269
+ " ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY created_at DESC, task_id DESC) AS rn," +
270
+ " MAX(created_at) OVER (PARTITION BY session_id) AS last_activity," +
271
+ " MIN(created_at) OVER (PARTITION BY session_id) AS first_activity," +
272
+ " COUNT(*) OVER (PARTITION BY session_id) AS run_count" +
273
+ " FROM task_run" + innerWhere + ") t WHERE " + outer.join(" AND ") +
274
+ ` ORDER BY last_activity DESC, session_id DESC LIMIT ${p(opts.limit)}`;
275
+ const { rows } = await this.db.query(sql, params);
276
+ return rows.map((r) => this.rowToSession(r));
277
+ }
278
+ rowToSession(r) {
279
+ return {
280
+ sessionId: String(r.session_id),
281
+ owner: r.owner ?? null,
282
+ lastActivityAt: iso(r.last_activity),
283
+ firstActivityAt: iso(r.first_activity),
284
+ runCount: Number(r.run_count),
285
+ objectivePreview: r.objective_preview ?? null,
286
+ lastStatus: String(r.status),
287
+ lastRunId: r.task_id ?? null,
288
+ title: null,
289
+ };
290
+ }
291
+ async deleteBySession(sessionId, owner) {
292
+ let result = { removed: 0 };
293
+ await this.tx(async (conn) => {
294
+ const { rows: active } = await conn.query(this.q("SELECT task_id FROM task_active WHERE session_id = ? FOR UPDATE", "SELECT task_id FROM task_active WHERE session_id = $1 FOR UPDATE"), [sessionId]);
295
+ if (active[0]) {
296
+ result = { active: String(active[0].task_id) };
297
+ return;
298
+ }
299
+ await conn.query(this.q("DELETE te FROM task_event te JOIN task_run tr ON te.task_id = tr.task_id WHERE tr.session_id = ? AND tr.owner <=> ?", "DELETE FROM task_event te USING task_run tr WHERE te.task_id = tr.task_id AND tr.session_id = $1 AND tr.owner IS NOT DISTINCT FROM $2"), [sessionId, owner]);
300
+ await conn.query(this.q("DELETE ta FROM task_active ta JOIN task_run tr ON ta.task_id = tr.task_id WHERE tr.session_id = ? AND tr.owner <=> ?", "DELETE FROM task_active ta USING task_run tr WHERE ta.task_id = tr.task_id AND tr.session_id = $1 AND tr.owner IS NOT DISTINCT FROM $2"), [sessionId, owner]);
301
+ const { affected } = await conn.query(this.q("DELETE FROM task_run WHERE session_id = ? AND owner <=> ?", "DELETE FROM task_run WHERE session_id = $1 AND owner IS NOT DISTINCT FROM $2"), [sessionId, owner]);
302
+ result = { removed: affected };
303
+ });
304
+ return result;
305
+ }
306
+ async sourceSummary(sinceMs) {
307
+ const { rows } = await this.db.query(this.q("SELECT source, status, COUNT(*) AS c FROM task_run WHERE created_at >= ? GROUP BY source, status", "SELECT source, status, COUNT(*) AS c FROM task_run WHERE created_at >= $1 GROUP BY source, status"), [new Date(sinceMs)]);
308
+ return rows.map((r) => ({ source: r.source ?? null, status: String(r.status), count: Number(r.c) }));
309
+ }
310
+ async usageScan(fromMs, toMs, opts) {
311
+ const limit = Math.min(opts?.limit ?? USAGE_SCAN_LIMIT, USAGE_SCAN_LIMIT);
312
+ const where = [this.q("created_at >= ?", "created_at >= $1"), this.q("created_at < ?", "created_at < $2")];
313
+ const params = [new Date(fromMs), new Date(toMs)];
314
+ if (opts?.owner !== undefined) {
315
+ where.push(this.q("owner = ?", "owner = $3"));
316
+ params.push(opts.owner);
317
+ }
318
+ const { rows } = await this.db.query(`SELECT owner, created_at, status, result FROM task_run WHERE ${where.join(" AND ")} ORDER BY created_at LIMIT ${limit + 1}`, params);
319
+ const truncated = rows.length > limit;
320
+ const page = truncated ? rows.slice(0, limit) : rows;
321
+ return {
322
+ truncated,
323
+ rows: page.map((r) => ({
324
+ owner: r.owner ?? null,
325
+ createdAtMs: new Date(r.created_at).getTime(),
326
+ status: String(r.status),
327
+ ...(r.result != null ? { stats: projectUsageStats(r.result) } : {}),
328
+ })),
329
+ };
330
+ }
331
+ async retainedFrom(taskId) {
332
+ const { rows } = await this.db.query(this.q("SELECT MIN(seq) AS m FROM task_event WHERE task_id = ?", "SELECT MIN(seq) AS m FROM task_event WHERE task_id = $1"), [taskId]);
333
+ const m = rows[0]?.m;
334
+ return m == null ? 0 : Number(m);
335
+ }
336
+ async getEvents(taskId, afterSeq) {
337
+ const { rows } = await this.db.query(this.q("SELECT seq, type, data, ts FROM task_event WHERE task_id = ? AND seq > ? ORDER BY seq ASC", "SELECT seq, type, data, ts FROM task_event WHERE task_id = $1 AND seq > $2 ORDER BY seq ASC"), [taskId, afterSeq]);
338
+ return rows.map((r) => ({
339
+ seq: Number(r.seq),
340
+ type: String(r.type),
341
+ data: parseJson(r.data),
342
+ ts: iso(r.ts),
343
+ }));
344
+ }
345
+ async reapStale(olderThanMs) {
346
+ const cutoff = new Date(Date.now() - olderThanMs);
347
+ let reaped = 0;
348
+ await this.tx(async (conn) => {
349
+ const { affected } = await conn.query(this.q("UPDATE task_run SET status = 'failed', error = 'run stalled (instance lost?)', updated_at = ? " +
350
+ "WHERE status = 'running' AND updated_at < ?", "UPDATE task_run SET status = 'failed', error = 'run stalled (instance lost?)', updated_at = $1 " +
351
+ "WHERE status = 'running' AND updated_at < $2"), [new Date(), cutoff]);
352
+ reaped = affected;
353
+ await conn.query(this.q("DELETE ta FROM task_active ta JOIN task_run tr ON ta.task_id = tr.task_id WHERE tr.status NOT IN ('running','suspended','needs_review')", "DELETE FROM task_active ta USING task_run tr WHERE ta.task_id = tr.task_id AND tr.status NOT IN ('running','suspended','needs_review')"));
354
+ });
355
+ return reaped;
356
+ }
357
+ async reapSuspended(olderThanMs) {
358
+ const cutoff = new Date(Date.now() - olderThanMs);
359
+ let reaped = 0;
360
+ await this.tx(async (conn) => {
361
+ const { affected } = await conn.query(this.q("UPDATE task_run tr SET tr.status = 'failed', tr.error = 'approval expired before decision', tr.error_code = 'approval.expired', tr.updated_at = ? " +
362
+ "WHERE tr.status = 'suspended' AND tr.updated_at < ? " +
363
+ "AND NOT EXISTS (SELECT 1 FROM checkpoint c WHERE c.session_id = tr.session_id AND c.status = 'pending')", "UPDATE task_run AS tr SET status = 'failed', error = 'approval expired before decision', error_code = 'approval.expired', updated_at = $1 " +
364
+ "WHERE tr.status = 'suspended' AND tr.updated_at < $2 " +
365
+ "AND NOT EXISTS (SELECT 1 FROM checkpoint c WHERE c.session_id = tr.session_id AND c.status = 'pending')"), [new Date(), cutoff]);
366
+ reaped = affected;
367
+ await conn.query(this.q("DELETE ta FROM task_active ta JOIN task_run tr ON ta.task_id = tr.task_id WHERE tr.status NOT IN ('running','suspended','needs_review')", "DELETE FROM task_active ta USING task_run tr WHERE ta.task_id = tr.task_id AND tr.status NOT IN ('running','suspended','needs_review')"));
368
+ });
369
+ return reaped;
370
+ }
371
+ async failSuspendedWithExpiredCheckpoint() {
372
+ let reaped = 0;
373
+ await this.tx(async (conn) => {
374
+ const { affected } = await conn.query(this.q("UPDATE task_run tr SET tr.status = 'failed', tr.error = 'approval expired before decision', tr.error_code = 'approval.expired', tr.updated_at = ? " +
375
+ "WHERE tr.status IN ('suspended','needs_review') " +
376
+ "AND EXISTS (SELECT 1 FROM checkpoint ce WHERE ce.session_id = tr.session_id AND ce.status = 'expired') " +
377
+ "AND NOT EXISTS (SELECT 1 FROM checkpoint cp WHERE cp.session_id = tr.session_id AND cp.status = 'pending')", "UPDATE task_run AS tr SET status = 'failed', error = 'approval expired before decision', error_code = 'approval.expired', updated_at = $1 " +
378
+ "WHERE tr.status IN ('suspended','needs_review') " +
379
+ "AND EXISTS (SELECT 1 FROM checkpoint ce WHERE ce.session_id = tr.session_id AND ce.status = 'expired') " +
380
+ "AND NOT EXISTS (SELECT 1 FROM checkpoint cp WHERE cp.session_id = tr.session_id AND cp.status = 'pending')"), [new Date()]);
381
+ reaped = affected;
382
+ await conn.query(this.q("DELETE ta FROM task_active ta JOIN task_run tr ON ta.task_id = tr.task_id WHERE tr.status NOT IN ('running','suspended','needs_review')", "DELETE FROM task_active ta USING task_run tr WHERE ta.task_id = tr.task_id AND tr.status NOT IN ('running','suspended','needs_review')"));
383
+ });
384
+ return reaped;
385
+ }
386
+ }
387
+ export class TiDBRunStore extends SqlRunStore {
388
+ constructor(pool) {
389
+ super(mysqlDriver(pool));
390
+ }
391
+ }
392
+ export class PgRunStore extends SqlRunStore {
393
+ constructor(pool) {
394
+ super(pgDriver(pool));
395
+ }
396
+ }
397
+ //# sourceMappingURL=run-store-sql.js.map
@@ -0,0 +1,22 @@
1
+ import { type SessionPolicyStore, type SessionPermissionRules, type StoredSessionRules, type SessionRulesRecord, type PutRulesOptions } from "@sema-agent/core";
2
+ import type { Pool as MySqlPool } from "mysql2/promise";
3
+ import type { Pool as PgPool } from "pg";
4
+ import { type SqlDriver } from "./sql-driver.js";
5
+ export declare class SqlSessionPolicyStore implements SessionPolicyStore {
6
+ private readonly db;
7
+ constructor(db: SqlDriver);
8
+ private q;
9
+ private isDupKey;
10
+ getRules(sessionId: string, principal?: string): Promise<StoredSessionRules | null>;
11
+ listBySession(sessionId: string): Promise<SessionRulesRecord[]>;
12
+ putRules(sessionId: string, principal: string | undefined, rules: SessionPermissionRules, opts?: PutRulesOptions): Promise<StoredSessionRules>;
13
+ deleteBySession(sessionId: string): Promise<void>;
14
+ private tx;
15
+ }
16
+ export declare class TiDBSessionPolicyStore extends SqlSessionPolicyStore {
17
+ constructor(pool: MySqlPool);
18
+ }
19
+ export declare class PgSessionPolicyStore extends SqlSessionPolicyStore {
20
+ constructor(pool: PgPool);
21
+ }
22
+ //# sourceMappingURL=session-policy-store-sql.d.ts.map
@@ -0,0 +1,109 @@
1
+ import { createHash } from "node:crypto";
2
+ import { loosenReasons, SessionPolicyError, normalizeRules, stripRev } from "@sema-agent/core";
3
+ import { pgHasUnstorable } from "./pg-safe-json.js";
4
+ import { mysqlDriver, pgDriver } from "./sql-driver.js";
5
+ const policyKey = (sessionId, principal) => createHash("sha256").update(JSON.stringify([sessionId, principal ?? null])).digest("hex");
6
+ const parseRules = (v) => (typeof v === "string" ? JSON.parse(v) : v);
7
+ export class SqlSessionPolicyStore {
8
+ db;
9
+ constructor(db) {
10
+ this.db = db;
11
+ }
12
+ q(tidb, pg) {
13
+ return this.db.dialect === "tidb" ? tidb : pg;
14
+ }
15
+ isDupKey(e) {
16
+ return this.db.dialect === "tidb"
17
+ ? e?.code === "ER_DUP_ENTRY" || e?.errno === 1062
18
+ : e?.code === "23505";
19
+ }
20
+ async getRules(sessionId, principal) {
21
+ const { rows } = await this.db.query(this.q("SELECT rules, rev FROM session_policy WHERE policy_key = ?", "SELECT rules, rev FROM session_policy WHERE policy_key = $1"), [policyKey(sessionId, principal)]);
22
+ if (!rows[0])
23
+ return null;
24
+ return { ...parseRules(rows[0].rules), rev: Number(rows[0].rev) };
25
+ }
26
+ async listBySession(sessionId) {
27
+ const { rows } = await this.db.query(this.q("SELECT principal, rules, rev FROM session_policy WHERE session_id = ?", "SELECT principal, rules, rev FROM session_policy WHERE session_id = $1"), [sessionId]);
28
+ return rows.map((r) => ({
29
+ principal: r.principal == null ? undefined : String(r.principal),
30
+ rules: { ...parseRules(r.rules), rev: Number(r.rev) },
31
+ }));
32
+ }
33
+ async putRules(sessionId, principal, rules, opts) {
34
+ const key = policyKey(sessionId, principal);
35
+ for (let attempt = 0;; attempt++) {
36
+ try {
37
+ return await this.tx(async (conn) => {
38
+ const { rows } = await conn.query(this.q("SELECT rules, rev FROM session_policy WHERE policy_key = ? FOR UPDATE", "SELECT rules, rev FROM session_policy WHERE policy_key = $1 FOR UPDATE"), [key]);
39
+ const prior = rows[0] ? { rules: parseRules(rows[0].rules), rev: Number(rows[0].rev) } : undefined;
40
+ const priorRev = prior?.rev ?? 0;
41
+ if (opts?.expectedRev !== undefined && priorRev !== opts.expectedRev) {
42
+ throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts.expectedRev}, have ${priorRev})`);
43
+ }
44
+ const clean = normalizeRules(stripRev(rules));
45
+ if (prior && opts?.operator !== true) {
46
+ const loosens = loosenReasons(prior.rules, clean);
47
+ if (loosens.length > 0) {
48
+ throw new SessionPolicyError("loosen_forbidden", `a non-operator session-rule write may only tighten; refused: ${loosens.join("; ")}`);
49
+ }
50
+ }
51
+ if (this.db.dialect === "pg" && pgHasUnstorable(clean)) {
52
+ throw new SessionPolicyError("loosen_forbidden", "rules contain unstorable bytes (NUL / lone surrogate) — refused; strip them at the source (authorization rules are never rewritten by the store)");
53
+ }
54
+ const nextRev = priorRev + 1;
55
+ const json = JSON.stringify(clean);
56
+ const now = new Date();
57
+ if (prior) {
58
+ const upd = await conn.query(this.q("UPDATE session_policy SET principal = ?, rules = ?, rev = ?, updated_at = ? WHERE policy_key = ? AND rev = ?", "UPDATE session_policy SET principal = $1, rules = $2::jsonb, rev = $3, updated_at = $4 WHERE policy_key = $5 AND rev = $6"), [principal ?? null, json, nextRev, now, key, priorRev]);
59
+ if (upd.affected !== 1)
60
+ throw new SessionPolicyError("conflict", `session rules rev mismatch (expected ${opts?.expectedRev ?? priorRev}, have ${priorRev})`);
61
+ }
62
+ else {
63
+ await conn.query(this.q("INSERT INTO session_policy (policy_key, session_id, principal, rules, rev, created_at, updated_at) VALUES (?,?,?,?,?,?,?)", "INSERT INTO session_policy (policy_key, session_id, principal, rules, rev, created_at, updated_at) VALUES ($1,$2,$3,$4::jsonb,$5,$6,$7)"), [key, sessionId, principal ?? null, json, nextRev, now, now]);
64
+ }
65
+ return structuredClone({ ...clean, rev: nextRev });
66
+ });
67
+ }
68
+ catch (e) {
69
+ if (this.isDupKey(e) && attempt < 4)
70
+ continue;
71
+ throw e;
72
+ }
73
+ }
74
+ }
75
+ async deleteBySession(sessionId) {
76
+ await this.db.query(this.q("DELETE FROM session_policy WHERE session_id = ?", "DELETE FROM session_policy WHERE session_id = $1"), [sessionId]);
77
+ }
78
+ async tx(fn) {
79
+ const conn = await this.db.connect();
80
+ try {
81
+ await conn.begin();
82
+ const out = await fn(conn);
83
+ await conn.commit();
84
+ return out;
85
+ }
86
+ catch (err) {
87
+ try {
88
+ await conn.rollback();
89
+ }
90
+ catch {
91
+ }
92
+ throw err;
93
+ }
94
+ finally {
95
+ conn.release();
96
+ }
97
+ }
98
+ }
99
+ export class TiDBSessionPolicyStore extends SqlSessionPolicyStore {
100
+ constructor(pool) {
101
+ super(mysqlDriver(pool));
102
+ }
103
+ }
104
+ export class PgSessionPolicyStore extends SqlSessionPolicyStore {
105
+ constructor(pool) {
106
+ super(pgDriver(pool));
107
+ }
108
+ }
109
+ //# sourceMappingURL=session-policy-store-sql.js.map
@@ -1,40 +1,2 @@
1
- import type { Pool } from "mysql2/promise";
2
- export type ApprovalStatus = "pending" | "approved" | "denied" | "expired";
3
- export interface ApprovalRow {
4
- id: string;
5
- taskId: string | null;
6
- sessionId: string | null;
7
- owner: string | null;
8
- scope: string | null;
9
- toolName: string;
10
- args: unknown;
11
- status: ApprovalStatus;
12
- reason: string | null;
13
- decidedBy: string | null;
14
- createdAt: string;
15
- decidedAt: string | null;
16
- }
17
- export declare class TiDBApprovalStore {
18
- private readonly pool;
19
- constructor(pool: Pool);
20
- createPending(id: string, scope: string | null, ctx: {
21
- taskId?: string | null;
22
- sessionId?: string | null;
23
- owner?: string | null;
24
- toolName: string;
25
- args: unknown;
26
- }): Promise<void>;
27
- getStatus(id: string, scope: string | null): Promise<{
28
- status: ApprovalStatus;
29
- reason: string | null;
30
- decidedBy: string | null;
31
- } | undefined>;
32
- decide(id: string, scope: string | null, decision: "approved" | "denied" | "expired", reason: string | null, decidedBy: string | null): Promise<boolean>;
33
- listPending(scope: string | null, owner?: string): Promise<ApprovalRow[]>;
34
- get(id: string, scope: string | null): Promise<ApprovalRow | undefined>;
35
- getById(id: string): Promise<ApprovalRow | undefined>;
36
- listPendingAll(): Promise<ApprovalRow[]>;
37
- expireStale(olderThanMs: number): Promise<number>;
38
- }
39
- export declare function mapRow(r: Record<string, unknown>): ApprovalRow;
1
+ export { TiDBApprovalStore, mapRow, type ApprovalRow, type ApprovalStatus, } from "./approval-store-sql.js";
40
2
  //# sourceMappingURL=tidb-approval-store.d.ts.map
@@ -1,72 +1,2 @@
1
- import { parseJsonStrict as parseJson, toIsoOrNull as iso } from "./sql-row-helpers.js";
2
- export class TiDBApprovalStore {
3
- pool;
4
- constructor(pool) {
5
- this.pool = pool;
6
- }
7
- async createPending(id, scope, ctx) {
8
- await this.pool.query("INSERT INTO approval (id, task_id, session_id, owner, scope, tool_name, args, status, created_at) " +
9
- "VALUES (?,?,?,?,?,?,?,'pending',?)", [
10
- id,
11
- ctx.taskId ?? null,
12
- ctx.sessionId ?? null,
13
- ctx.owner ?? null,
14
- scope,
15
- ctx.toolName,
16
- ctx.args == null ? null : JSON.stringify(ctx.args),
17
- new Date(),
18
- ]);
19
- }
20
- async getStatus(id, scope) {
21
- const [rows] = await this.pool.query("SELECT status, reason, decided_by FROM approval WHERE id = ? AND scope <=> ?", [id, scope]);
22
- const r = rows[0];
23
- if (!r)
24
- return undefined;
25
- return { status: r.status, reason: r.reason ?? null, decidedBy: r.decided_by ?? null };
26
- }
27
- async decide(id, scope, decision, reason, decidedBy) {
28
- const [res] = await this.pool.query("UPDATE approval SET status = ?, reason = ?, decided_by = ?, decided_at = ? WHERE id = ? AND scope <=> ? AND status = 'pending'", [decision, reason, decidedBy, new Date(), id, scope]);
29
- return res.affectedRows === 1;
30
- }
31
- async listPending(scope, owner) {
32
- const [rows] = owner
33
- ? await this.pool.query("SELECT * FROM approval WHERE status = 'pending' AND scope <=> ? AND owner = ? ORDER BY created_at ASC", [scope, owner])
34
- : await this.pool.query("SELECT * FROM approval WHERE status = 'pending' AND scope <=> ? ORDER BY created_at ASC", [scope]);
35
- return rows.map(mapRow);
36
- }
37
- async get(id, scope) {
38
- const [rows] = await this.pool.query("SELECT * FROM approval WHERE id = ? AND scope <=> ?", [id, scope]);
39
- return rows[0] ? mapRow(rows[0]) : undefined;
40
- }
41
- async getById(id) {
42
- const [rows] = await this.pool.query("SELECT * FROM approval WHERE id = ?", [id]);
43
- return rows[0] ? mapRow(rows[0]) : undefined;
44
- }
45
- async listPendingAll() {
46
- const [rows] = await this.pool.query("SELECT * FROM approval WHERE status = 'pending' ORDER BY created_at ASC");
47
- return rows.map(mapRow);
48
- }
49
- async expireStale(olderThanMs) {
50
- const cutoff = new Date(Date.now() - olderThanMs);
51
- const [res] = await this.pool.query("UPDATE approval SET status = 'expired', reason = 'timed out before decision', decided_at = ? " +
52
- "WHERE status = 'pending' AND created_at < ?", [new Date(), cutoff]);
53
- return res.affectedRows;
54
- }
55
- }
56
- export function mapRow(r) {
57
- return {
58
- id: String(r.id),
59
- taskId: r.task_id ?? null,
60
- sessionId: r.session_id ?? null,
61
- owner: r.owner ?? null,
62
- scope: r.scope ?? null,
63
- toolName: String(r.tool_name),
64
- args: parseJson(r.args),
65
- status: r.status,
66
- reason: r.reason ?? null,
67
- decidedBy: r.decided_by ?? null,
68
- createdAt: iso(r.created_at),
69
- decidedAt: iso(r.decided_at),
70
- };
71
- }
1
+ export { TiDBApprovalStore, mapRow, } from "./approval-store-sql.js";
72
2
  //# sourceMappingURL=tidb-approval-store.js.map