agentcorp-broker 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/CONTRIBUTING.md +20 -0
  3. package/LICENSE +201 -0
  4. package/README.md +250 -0
  5. package/SECURITY.md +23 -0
  6. package/dist/audit.d.ts +25 -0
  7. package/dist/audit.js +203 -0
  8. package/dist/audit.js.map +1 -0
  9. package/dist/broker.d.ts +103 -0
  10. package/dist/broker.js +805 -0
  11. package/dist/broker.js.map +1 -0
  12. package/dist/cli.d.ts +2 -0
  13. package/dist/cli.js +712 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/config.d.ts +3 -0
  16. package/dist/config.js +41 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/console/console.css +794 -0
  19. package/dist/console/console.js +802 -0
  20. package/dist/console/index.html +309 -0
  21. package/dist/credentials.d.ts +16 -0
  22. package/dist/credentials.js +72 -0
  23. package/dist/credentials.js.map +1 -0
  24. package/dist/database.d.ts +119 -0
  25. package/dist/database.js +1356 -0
  26. package/dist/database.js.map +1 -0
  27. package/dist/diagnostics.d.ts +44 -0
  28. package/dist/diagnostics.js +357 -0
  29. package/dist/diagnostics.js.map +1 -0
  30. package/dist/errors.d.ts +5 -0
  31. package/dist/errors.js +14 -0
  32. package/dist/errors.js.map +1 -0
  33. package/dist/index.d.ts +23 -0
  34. package/dist/index.js +15 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/mcp.d.ts +3 -0
  37. package/dist/mcp.js +215 -0
  38. package/dist/mcp.js.map +1 -0
  39. package/dist/migrations.d.ts +13 -0
  40. package/dist/migrations.js +266 -0
  41. package/dist/migrations.js.map +1 -0
  42. package/dist/policy.d.ts +22 -0
  43. package/dist/policy.js +33 -0
  44. package/dist/policy.js.map +1 -0
  45. package/dist/server.d.ts +49 -0
  46. package/dist/server.js +529 -0
  47. package/dist/server.js.map +1 -0
  48. package/dist/stdio-adapter.d.ts +230 -0
  49. package/dist/stdio-adapter.js +406 -0
  50. package/dist/stdio-adapter.js.map +1 -0
  51. package/dist/tui.d.ts +26 -0
  52. package/dist/tui.js +291 -0
  53. package/dist/tui.js.map +1 -0
  54. package/dist/types.d.ts +355 -0
  55. package/dist/types.js +85 -0
  56. package/dist/types.js.map +1 -0
  57. package/docs/ARCHITECTURE.md +119 -0
  58. package/docs/README.md +37 -0
  59. package/docs/RELEASING.md +228 -0
  60. package/docs/ROADMAP.md +112 -0
  61. package/docs/cli-reference.md +133 -0
  62. package/docs/dogfooding-report.md +83 -0
  63. package/docs/getting-started.md +228 -0
  64. package/docs/guides/antigravity-setup.md +84 -0
  65. package/docs/guides/claude-cursor-setup.md +76 -0
  66. package/docs/guides/codex-setup.md +75 -0
  67. package/docs/guides/human-console.md +177 -0
  68. package/docs/mcp-tools-reference.md +235 -0
  69. package/docs/policy-guide.md +105 -0
  70. package/examples/org.toml +65 -0
  71. package/package.json +68 -0
@@ -0,0 +1,1356 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdirSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { getCurrentSchemaVersion, runMigrations } from "./migrations.js";
6
+ export const DEFAULT_PAGE_LIMIT = 50;
7
+ export const MAX_PAGE_LIMIT = 200;
8
+ export const DEFAULT_AUDIT_LIMIT = 500;
9
+ export const MAX_AUDIT_LIMIT = 5000;
10
+ export const MAX_MAINTENANCE_LOG_ENTRIES = 1000;
11
+ export function encodeCursor(timestamp, id) {
12
+ return Buffer.from(JSON.stringify({ t: timestamp, id })).toString("base64url");
13
+ }
14
+ export function decodeCursor(cursor) {
15
+ if (!cursor)
16
+ return null;
17
+ try {
18
+ const raw = Buffer.from(cursor, cursor.includes("-") || cursor.includes("_") ? "base64url" : "base64").toString("utf8");
19
+ const parsed = JSON.parse(raw);
20
+ if (parsed && (parsed.t || parsed.id)) {
21
+ return { timestamp: parsed.t, id: parsed.id };
22
+ }
23
+ }
24
+ catch { }
25
+ if (cursor.includes("T") && cursor.includes("Z")) {
26
+ return { timestamp: cursor };
27
+ }
28
+ return { id: cursor };
29
+ }
30
+ export function parseLimit(limit, defaultLimit = DEFAULT_PAGE_LIMIT, maxLimit = MAX_PAGE_LIMIT) {
31
+ if (limit === undefined || limit === null || Number.isNaN(limit))
32
+ return defaultLimit;
33
+ return Math.min(Math.max(1, Math.floor(limit)), maxLimit);
34
+ }
35
+ const AUDIT_TRUNCATION_MARKER = "... [truncated]";
36
+ const MAX_AUDIT_PREVIEW_BYTES = 256;
37
+ function auditPreviewPrefixBytes(totalBudget) {
38
+ return Math.max(0, totalBudget - Buffer.byteLength(AUDIT_TRUNCATION_MARKER, "utf8"));
39
+ }
40
+ function decodeUtf8Prefix(value, maxBytes) {
41
+ if (maxBytes <= 0 || value === null || value === undefined)
42
+ return "";
43
+ const bytes = value instanceof Uint8Array
44
+ ? Buffer.from(value.buffer, value.byteOffset, value.byteLength)
45
+ : Buffer.from(String(value), "utf8");
46
+ let end = Math.min(maxBytes, bytes.byteLength);
47
+ const decoder = new TextDecoder("utf-8", { fatal: true });
48
+ while (end > 0) {
49
+ try {
50
+ return decoder.decode(bytes.subarray(0, end));
51
+ }
52
+ catch {
53
+ end--;
54
+ }
55
+ }
56
+ return "";
57
+ }
58
+ function formatAuditPreview(value, totalBudget) {
59
+ if (totalBudget <= 0)
60
+ return "";
61
+ const markerBytes = Buffer.from(AUDIT_TRUNCATION_MARKER, "utf8");
62
+ if (totalBudget <= markerBytes.byteLength) {
63
+ return markerBytes.subarray(0, totalBudget).toString("utf8");
64
+ }
65
+ return decodeUtf8Prefix(value, auditPreviewPrefixBytes(totalBudget)) + AUDIT_TRUNCATION_MARKER;
66
+ }
67
+ function json(value) {
68
+ return JSON.parse(String(value));
69
+ }
70
+ function mapMessage(row) {
71
+ return {
72
+ messageId: String(row.message_id),
73
+ taskId: row.task_id === null ? null : String(row.task_id),
74
+ fromRole: String(row.from_role),
75
+ toRole: String(row.to_role),
76
+ type: String(row.type),
77
+ payload: json(row.payload),
78
+ references: json(row.references ?? row.references_json ?? "[]"),
79
+ inReplyTo: row.in_reply_to === null ? null : String(row.in_reply_to),
80
+ status: String(row.status),
81
+ riskTags: json(row.risk_tags ?? "[]"),
82
+ createdAt: String(row.created_at),
83
+ resolvedAt: row.resolved_at === null ? null : String(row.resolved_at),
84
+ };
85
+ }
86
+ function mapTask(row) {
87
+ return {
88
+ taskId: String(row.task_id),
89
+ title: String(row.title),
90
+ description: row.description === null ? null : String(row.description),
91
+ createdBy: String(row.created_by),
92
+ assignedTo: row.assigned_to === null ? null : String(row.assigned_to),
93
+ status: String(row.status),
94
+ createdAt: String(row.created_at),
95
+ updatedAt: String(row.updated_at),
96
+ };
97
+ }
98
+ function mapArtifact(row, includeContent) {
99
+ const record = {
100
+ artifactId: String(row.artifact_id),
101
+ type: String(row.type),
102
+ name: String(row.name),
103
+ producedBy: String(row.produced_by),
104
+ contentHash: String(row.content_hash),
105
+ visibleToRoles: row.visible_to_roles === "all" ? "all" : json(row.visible_to_roles),
106
+ relatedTaskId: row.related_task_id === null ? null : String(row.related_task_id),
107
+ createdAt: String(row.created_at),
108
+ };
109
+ if (includeContent && row.content !== null)
110
+ record.content = String(row.content);
111
+ if (includeContent && row.content_uri !== null)
112
+ record.contentUri = String(row.content_uri);
113
+ return record;
114
+ }
115
+ function mapPolicy(row) {
116
+ const optional = (key) => row[key] === null || row[key] === undefined ? undefined : String(row[key]);
117
+ return {
118
+ id: String(row.policy_id),
119
+ subject: String(row.subject),
120
+ priority: Number(row.priority),
121
+ action: String(row.action),
122
+ enabled: Boolean(row.enabled),
123
+ createdAt: String(row.created_at),
124
+ updatedAt: String(row.updated_at),
125
+ ...(optional("from_role") ? { from_role: optional("from_role") } : {}),
126
+ ...(optional("to_role") ? { to_role: optional("to_role") } : {}),
127
+ ...(optional("message_type")
128
+ ? { message_type: optional("message_type") }
129
+ : {}),
130
+ ...(row.risk_tags === null ? {} : { risk_tags: json(row.risk_tags) }),
131
+ ...(optional("from_status")
132
+ ? { from_status: optional("from_status") }
133
+ : {}),
134
+ ...(optional("to_status")
135
+ ? { to_status: optional("to_status") }
136
+ : {}),
137
+ ...(optional("delegate_role")
138
+ ? { delegate_role: optional("delegate_role") }
139
+ : {}),
140
+ };
141
+ }
142
+ function mapApproval(row) {
143
+ return {
144
+ approvalId: String(row.approval_id),
145
+ subject: String(row.subject),
146
+ subjectId: String(row.subject_id),
147
+ requestedBy: String(row.requested_by),
148
+ status: String(row.status),
149
+ context: json(row.context),
150
+ createdAt: String(row.created_at),
151
+ decidedAt: row.decided_at === null ? null : String(row.decided_at),
152
+ decisionNote: row.decision_note === null ? null : String(row.decision_note),
153
+ };
154
+ }
155
+ export class AgentCorpDatabase {
156
+ db;
157
+ defaultPageSize;
158
+ maxPageSize;
159
+ constructor(path = ".agentcorp/agentcorp.db", options = {}) {
160
+ if (path !== ":memory:")
161
+ mkdirSync(dirname(resolve(path)), { recursive: true });
162
+ this.defaultPageSize = options.defaultPageSize ?? DEFAULT_PAGE_LIMIT;
163
+ this.maxPageSize = options.maxPageSize ?? MAX_PAGE_LIMIT;
164
+ this.db = new DatabaseSync(path);
165
+ try {
166
+ // Install the busy handler before journal-mode negotiation or migrations;
167
+ // either operation can require a lock when another process opens the same DB.
168
+ this.db.exec("PRAGMA busy_timeout = 5000;");
169
+ this.db.exec("PRAGMA foreign_keys = ON;");
170
+ this.db.exec("PRAGMA journal_mode = WAL;");
171
+ this.migrate();
172
+ }
173
+ catch (error) {
174
+ this.db.close();
175
+ throw error;
176
+ }
177
+ }
178
+ resolveLimit(limit) {
179
+ return parseLimit(limit, this.defaultPageSize, this.maxPageSize);
180
+ }
181
+ close() {
182
+ this.db.close();
183
+ }
184
+ transactionDepth = 0;
185
+ transaction(operation) {
186
+ if (this.transactionDepth > 0) {
187
+ this.transactionDepth++;
188
+ try {
189
+ return operation();
190
+ }
191
+ finally {
192
+ this.transactionDepth--;
193
+ }
194
+ }
195
+ this.db.exec("BEGIN IMMEDIATE");
196
+ this.transactionDepth = 1;
197
+ try {
198
+ const result = operation();
199
+ this.db.exec("COMMIT");
200
+ return result;
201
+ }
202
+ catch (error) {
203
+ try {
204
+ this.db.exec("ROLLBACK");
205
+ }
206
+ catch {
207
+ // Suppress rollback error if already rolled back
208
+ }
209
+ throw error;
210
+ }
211
+ finally {
212
+ this.transactionDepth = 0;
213
+ }
214
+ }
215
+ getSchemaVersion() {
216
+ return getCurrentSchemaVersion(this.db);
217
+ }
218
+ migrate() {
219
+ runMigrations(this.db);
220
+ }
221
+ countPolicies() {
222
+ return Number(this.db.prepare("SELECT COUNT(*) AS count FROM policies").get().count);
223
+ }
224
+ insertPolicy(rule) {
225
+ this.db.prepare(`
226
+ INSERT INTO policies (
227
+ policy_id, subject, priority, from_role, to_role, message_type, risk_tags,
228
+ from_status, to_status, action, delegate_role, enabled, created_at, updated_at
229
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
230
+ `).run(rule.id, rule.subject, rule.priority, rule.from_role ?? null, rule.to_role ?? null, rule.message_type ?? null, rule.risk_tags ? JSON.stringify(rule.risk_tags) : null, rule.from_status ?? null, rule.to_status ?? null, rule.action, rule.delegate_role ?? null, rule.enabled ? 1 : 0, rule.createdAt, rule.updatedAt);
231
+ }
232
+ upsertPolicy(rule) {
233
+ this.db.prepare(`
234
+ INSERT INTO policies (
235
+ policy_id, subject, priority, from_role, to_role, message_type, risk_tags,
236
+ from_status, to_status, action, delegate_role, enabled, created_at, updated_at
237
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
238
+ ON CONFLICT(policy_id) DO UPDATE SET
239
+ subject = excluded.subject,
240
+ priority = excluded.priority,
241
+ from_role = excluded.from_role,
242
+ to_role = excluded.to_role,
243
+ message_type = excluded.message_type,
244
+ risk_tags = excluded.risk_tags,
245
+ from_status = excluded.from_status,
246
+ to_status = excluded.to_status,
247
+ action = excluded.action,
248
+ delegate_role = excluded.delegate_role,
249
+ enabled = excluded.enabled,
250
+ updated_at = excluded.updated_at
251
+ `).run(rule.id, rule.subject, rule.priority, rule.from_role ?? null, rule.to_role ?? null, rule.message_type ?? null, rule.risk_tags ? JSON.stringify(rule.risk_tags) : null, rule.from_status ?? null, rule.to_status ?? null, rule.action, rule.delegate_role ?? null, rule.enabled ? 1 : 0, rule.createdAt, rule.updatedAt);
252
+ }
253
+ getPolicy(policyId) {
254
+ const row = this.db.prepare("SELECT * FROM policies WHERE policy_id = ?").get(policyId);
255
+ return row ? mapPolicy(row) : undefined;
256
+ }
257
+ setPolicyEnabled(policyId, enabled, updatedAt) {
258
+ this.db.prepare("UPDATE policies SET enabled = ?, updated_at = ? WHERE policy_id = ?")
259
+ .run(enabled ? 1 : 0, updatedAt, policyId);
260
+ }
261
+ listPolicies() {
262
+ return this.db.prepare("SELECT * FROM policies ORDER BY priority DESC, created_at ASC").all()
263
+ .map(mapPolicy);
264
+ }
265
+ bindRole(roleId, agentId, capabilities, connectedAt) {
266
+ const cols = this.db.prepare("PRAGMA table_info(role_bindings)").all().map((c) => c.name);
267
+ const hasLastSeen = cols.includes("last_seen_at");
268
+ if (hasLastSeen) {
269
+ this.db.prepare(`
270
+ INSERT INTO role_bindings (role_id, agent_id, capabilities, connected_at, last_seen_at)
271
+ VALUES (?, ?, ?, ?, ?)
272
+ ON CONFLICT(role_id) DO UPDATE SET
273
+ agent_id = excluded.agent_id,
274
+ capabilities = excluded.capabilities,
275
+ connected_at = excluded.connected_at,
276
+ last_seen_at = excluded.last_seen_at
277
+ `).run(roleId, agentId, JSON.stringify(capabilities), connectedAt, connectedAt);
278
+ }
279
+ else {
280
+ this.db.prepare(`
281
+ INSERT INTO role_bindings (role_id, agent_id, capabilities, connected_at)
282
+ VALUES (?, ?, ?, ?)
283
+ ON CONFLICT(role_id) DO UPDATE SET
284
+ agent_id = excluded.agent_id,
285
+ capabilities = excluded.capabilities,
286
+ connected_at = excluded.connected_at
287
+ `).run(roleId, agentId, JSON.stringify(capabilities), connectedAt);
288
+ }
289
+ }
290
+ touchRolePresence(roleId, timestamp = new Date().toISOString()) {
291
+ const cols = this.db.prepare("PRAGMA table_info(role_bindings)").all().map((c) => c.name);
292
+ if (!cols.includes("last_seen_at"))
293
+ return;
294
+ const existing = this.db.prepare("SELECT role_id FROM role_bindings WHERE role_id = ?").get(roleId);
295
+ if (existing) {
296
+ this.db.prepare(`
297
+ UPDATE role_bindings
298
+ SET last_seen_at = ?
299
+ WHERE role_id = ?
300
+ `).run(timestamp, roleId);
301
+ }
302
+ else {
303
+ this.db.prepare(`
304
+ INSERT INTO role_bindings (role_id, agent_id, capabilities, connected_at, last_seen_at)
305
+ VALUES (?, ?, ?, ?, ?)
306
+ `).run(roleId, `${roleId}-session`, "[]", timestamp, timestamp);
307
+ }
308
+ }
309
+ listRolePresence() {
310
+ const cols = this.db.prepare("PRAGMA table_info(role_bindings)").all().map((c) => c.name);
311
+ const hasLastSeen = cols.includes("last_seen_at");
312
+ const query = hasLastSeen
313
+ ? "SELECT role_id, agent_id, connected_at, last_seen_at FROM role_bindings ORDER BY role_id ASC"
314
+ : "SELECT role_id, agent_id, connected_at FROM role_bindings ORDER BY role_id ASC";
315
+ const rows = this.db.prepare(query).all();
316
+ const nowMs = Date.now();
317
+ return rows.map((r) => {
318
+ const lastSeenStr = r.last_seen_at ?? r.connected_at;
319
+ const lastSeenMs = lastSeenStr ? new Date(lastSeenStr).getTime() : nowMs;
320
+ const diffMs = nowMs - lastSeenMs;
321
+ let status = "online";
322
+ let activityFreshness = "fresh";
323
+ if (diffMs > 300000) { // > 5 minutes
324
+ status = "offline";
325
+ activityFreshness = "stale";
326
+ }
327
+ else if (diffMs > 60000) { // > 1 minute
328
+ status = "idle";
329
+ activityFreshness = "idle";
330
+ }
331
+ return {
332
+ roleId: r.role_id,
333
+ agentId: r.agent_id,
334
+ connectedAt: r.connected_at,
335
+ lastSeenAt: lastSeenStr,
336
+ lastActiveAt: lastSeenStr,
337
+ status,
338
+ activityFreshness,
339
+ };
340
+ });
341
+ }
342
+ saveIdempotency(record) {
343
+ this.db.prepare(`
344
+ INSERT INTO idempotency_keys (role_id, key, operation, request_hash, response_json, created_at)
345
+ VALUES (?, ?, ?, ?, ?, ?)
346
+ `).run(record.roleId, record.key, record.operation, record.requestHash ?? null, record.responseJson, record.createdAt);
347
+ }
348
+ getIdempotency(key, roleId) {
349
+ const row = this.db.prepare(`
350
+ SELECT role_id, key, operation, request_hash, response_json, created_at
351
+ FROM idempotency_keys
352
+ WHERE role_id = ? AND key = ?
353
+ `).get(roleId, key);
354
+ if (!row)
355
+ return undefined;
356
+ return {
357
+ key: row.key,
358
+ roleId: row.role_id,
359
+ operation: row.operation,
360
+ requestHash: row.request_hash ?? undefined,
361
+ responseJson: row.response_json,
362
+ createdAt: row.created_at,
363
+ };
364
+ }
365
+ insertTask(task) {
366
+ const now = new Date().toISOString();
367
+ this.db.prepare(`
368
+ INSERT INTO tasks (task_id, title, description, created_by, assigned_to, status, created_at, updated_at)
369
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
370
+ `).run(task.taskId, task.title, task.description ?? null, task.createdBy, task.assignedTo ?? null, task.status, task.createdAt ?? now, task.updatedAt ?? now);
371
+ }
372
+ getTask(taskId) {
373
+ const row = this.db.prepare("SELECT * FROM tasks WHERE task_id = ?").get(taskId);
374
+ return row ? mapTask(row) : undefined;
375
+ }
376
+ listTasksForRolePaginated(roleId, options) {
377
+ const limit = this.resolveLimit(options?.limit);
378
+ let cursorTime = null;
379
+ let cursorId = null;
380
+ const decoded = decodeCursor(options?.cursor);
381
+ if (decoded) {
382
+ if (decoded.timestamp) {
383
+ cursorTime = decoded.timestamp;
384
+ cursorId = decoded.id ?? "";
385
+ }
386
+ else if (decoded.id) {
387
+ const row = this.db.prepare("SELECT updated_at FROM tasks WHERE task_id = ?").get(decoded.id);
388
+ if (row) {
389
+ cursorTime = String(row.updated_at);
390
+ cursorId = decoded.id;
391
+ }
392
+ }
393
+ }
394
+ const rows = (cursorTime !== null && cursorId !== null)
395
+ ? this.db.prepare(`
396
+ SELECT DISTINCT t.* FROM tasks t
397
+ LEFT JOIN messages m ON m.task_id = t.task_id
398
+ WHERE (t.created_by = ?
399
+ OR (t.assigned_to = ? AND t.status <> 'proposed')
400
+ OR m.from_role = ?
401
+ OR (m.to_role = ? AND m.status IN ('approved', 'delivered', 'acknowledged')))
402
+ AND (t.updated_at < ? OR (t.updated_at = ? AND t.task_id < ?))
403
+ ORDER BY t.updated_at DESC, t.task_id DESC
404
+ LIMIT ?
405
+ `).all(roleId, roleId, roleId, roleId, cursorTime, cursorTime, cursorId, limit + 1)
406
+ : this.db.prepare(`
407
+ SELECT DISTINCT t.* FROM tasks t
408
+ LEFT JOIN messages m ON m.task_id = t.task_id
409
+ WHERE t.created_by = ?
410
+ OR (t.assigned_to = ? AND t.status <> 'proposed')
411
+ OR m.from_role = ?
412
+ OR (m.to_role = ? AND m.status IN ('approved', 'delivered', 'acknowledged'))
413
+ ORDER BY t.updated_at DESC, t.task_id DESC
414
+ LIMIT ?
415
+ `).all(roleId, roleId, roleId, roleId, limit + 1);
416
+ const hasMore = rows.length > limit;
417
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
418
+ const items = pageRows.map(mapTask);
419
+ const last = items[items.length - 1];
420
+ const nextCursor = (hasMore && last) ? encodeCursor(last.updatedAt, last.taskId) : null;
421
+ return { items, nextCursor };
422
+ }
423
+ listTasksForRole(roleId, options) {
424
+ return this.listTasksForRolePaginated(roleId, options).items;
425
+ }
426
+ updateTaskStatus(taskId, status, updatedAt) {
427
+ this.db.prepare("UPDATE tasks SET status = ?, updated_at = ? WHERE task_id = ?")
428
+ .run(status, updatedAt, taskId);
429
+ }
430
+ insertTransition(transition) {
431
+ this.db.prepare(`
432
+ INSERT INTO task_transitions (
433
+ transition_id, task_id, from_status, to_status, requested_by, status, created_at
434
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
435
+ `).run(transition.id, transition.taskId, transition.fromStatus, transition.toStatus, transition.requestedBy, transition.status, transition.createdAt);
436
+ }
437
+ getTransition(id) {
438
+ return this.db.prepare("SELECT * FROM task_transitions WHERE transition_id = ?").get(id);
439
+ }
440
+ resolveTransition(id, status, resolvedAt) {
441
+ this.db.prepare("UPDATE task_transitions SET status = ?, resolved_at = ? WHERE transition_id = ?")
442
+ .run(status, resolvedAt, id);
443
+ }
444
+ hasPendingTransition(taskId, toStatus, requestedBy) {
445
+ const row = this.db.prepare(`
446
+ SELECT 1 AS found
447
+ FROM task_transitions
448
+ WHERE task_id = ? AND to_status = ? AND requested_by = ? AND status = 'pending'
449
+ LIMIT 1
450
+ `).get(taskId, toStatus, requestedBy);
451
+ return row !== undefined;
452
+ }
453
+ insertMessage(message) {
454
+ this.db.prepare(`
455
+ INSERT INTO messages (
456
+ message_id, task_id, from_role, to_role, type, payload, references_json,
457
+ in_reply_to, status, risk_tags, created_at, resolved_at
458
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
459
+ `).run(message.messageId, message.taskId, message.fromRole, message.toRole, message.type, JSON.stringify(message.payload), JSON.stringify(message.references), message.inReplyTo, message.status, JSON.stringify(message.riskTags), message.createdAt, message.resolvedAt);
460
+ }
461
+ getMessage(messageId) {
462
+ const row = this.db.prepare(`
463
+ SELECT message_id, task_id, from_role, to_role, type, payload,
464
+ references_json AS "references", in_reply_to, status, risk_tags, created_at, resolved_at
465
+ FROM messages WHERE message_id = ?
466
+ `).get(messageId);
467
+ return row ? mapMessage(row) : undefined;
468
+ }
469
+ listInboxPaginated(roleId, options) {
470
+ const limit = this.resolveLimit(options?.limit);
471
+ let cursorTime = null;
472
+ let cursorId = null;
473
+ const decoded = decodeCursor(options?.cursor);
474
+ if (decoded) {
475
+ if (decoded.timestamp) {
476
+ cursorTime = decoded.timestamp;
477
+ cursorId = decoded.id ?? "";
478
+ }
479
+ else if (decoded.id) {
480
+ const row = this.db.prepare("SELECT created_at FROM messages WHERE message_id = ?").get(decoded.id);
481
+ if (row) {
482
+ cursorTime = String(row.created_at);
483
+ cursorId = decoded.id;
484
+ }
485
+ }
486
+ }
487
+ const rows = (cursorTime !== null && cursorId !== null)
488
+ ? this.db.prepare(`
489
+ SELECT message_id, task_id, from_role, to_role, type, payload,
490
+ references_json AS "references", in_reply_to, status, risk_tags, created_at, resolved_at
491
+ FROM messages
492
+ WHERE to_role = ? AND status IN ('approved', 'delivered')
493
+ AND (created_at > ? OR (created_at = ? AND message_id > ?))
494
+ ORDER BY created_at ASC, message_id ASC
495
+ LIMIT ?
496
+ `).all(roleId, cursorTime, cursorTime, cursorId, limit + 1)
497
+ : this.db.prepare(`
498
+ SELECT message_id, task_id, from_role, to_role, type, payload,
499
+ references_json AS "references", in_reply_to, status, risk_tags, created_at, resolved_at
500
+ FROM messages
501
+ WHERE to_role = ? AND status IN ('approved', 'delivered')
502
+ ORDER BY created_at ASC, message_id ASC
503
+ LIMIT ?
504
+ `).all(roleId, limit + 1);
505
+ const hasMore = rows.length > limit;
506
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
507
+ const items = pageRows.map(mapMessage);
508
+ const last = items[items.length - 1];
509
+ const nextCursor = (hasMore && last) ? encodeCursor(last.createdAt, last.messageId) : null;
510
+ return { items, nextCursor };
511
+ }
512
+ listInbox(roleId, options) {
513
+ return this.listInboxPaginated(roleId, options).items;
514
+ }
515
+ listThreadPaginated(taskId, roleId, options) {
516
+ const limit = this.resolveLimit(options?.limit);
517
+ let cursorTime = null;
518
+ let cursorId = null;
519
+ const decoded = decodeCursor(options?.cursor);
520
+ if (decoded) {
521
+ if (decoded.timestamp) {
522
+ cursorTime = decoded.timestamp;
523
+ cursorId = decoded.id ?? "";
524
+ }
525
+ else if (decoded.id) {
526
+ const row = this.db.prepare("SELECT created_at FROM messages WHERE message_id = ?").get(decoded.id);
527
+ if (row) {
528
+ cursorTime = String(row.created_at);
529
+ cursorId = decoded.id;
530
+ }
531
+ }
532
+ }
533
+ const rows = (cursorTime !== null && cursorId !== null)
534
+ ? this.db.prepare(`
535
+ SELECT message_id, task_id, from_role, to_role, type, payload,
536
+ references_json AS "references", in_reply_to, status, risk_tags, created_at, resolved_at
537
+ FROM messages
538
+ WHERE task_id = ? AND (from_role = ? OR to_role = ?)
539
+ AND (from_role = ? OR status IN ('approved', 'delivered', 'acknowledged'))
540
+ AND (created_at > ? OR (created_at = ? AND message_id > ?))
541
+ ORDER BY created_at ASC, message_id ASC
542
+ LIMIT ?
543
+ `).all(taskId, roleId, roleId, roleId, cursorTime, cursorTime, cursorId, limit + 1)
544
+ : this.db.prepare(`
545
+ SELECT message_id, task_id, from_role, to_role, type, payload,
546
+ references_json AS "references", in_reply_to, status, risk_tags, created_at, resolved_at
547
+ FROM messages
548
+ WHERE task_id = ? AND (from_role = ? OR to_role = ?)
549
+ AND (from_role = ? OR status IN ('approved', 'delivered', 'acknowledged'))
550
+ ORDER BY created_at ASC, message_id ASC
551
+ LIMIT ?
552
+ `).all(taskId, roleId, roleId, roleId, limit + 1);
553
+ const hasMore = rows.length > limit;
554
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
555
+ const items = pageRows.map(mapMessage);
556
+ const last = items[items.length - 1];
557
+ const nextCursor = (hasMore && last) ? encodeCursor(last.createdAt, last.messageId) : null;
558
+ return { items, nextCursor };
559
+ }
560
+ listThread(taskId, roleId, options) {
561
+ return this.listThreadPaginated(taskId, roleId, options).items;
562
+ }
563
+ updateMessage(messageId, status, resolvedAt, payload) {
564
+ if (payload === undefined) {
565
+ this.db.prepare("UPDATE messages SET status = ?, resolved_at = ? WHERE message_id = ?")
566
+ .run(status, resolvedAt, messageId);
567
+ }
568
+ else {
569
+ this.db.prepare("UPDATE messages SET status = ?, resolved_at = ?, payload = ? WHERE message_id = ?")
570
+ .run(status, resolvedAt, JSON.stringify(payload), messageId);
571
+ }
572
+ }
573
+ insertMessageEvent(event) {
574
+ this.db.prepare(`
575
+ INSERT INTO message_events (event_id, message_id, status, actor, note, created_at)
576
+ VALUES (?, ?, ?, ?, ?, ?)
577
+ `).run(event.id, event.messageId, event.status, event.actor, event.note, event.createdAt);
578
+ }
579
+ insertArtifact(artifact) {
580
+ this.db.prepare(`
581
+ INSERT INTO artifacts (
582
+ artifact_id, type, name, produced_by, content, content_uri, content_hash,
583
+ visible_to_roles, related_task_id, created_at
584
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
585
+ `).run(artifact.artifactId, artifact.type, artifact.name, artifact.producedBy, artifact.content ?? null, artifact.contentUri ?? null, artifact.contentHash, artifact.visibleToRoles === "all" ? "all" : JSON.stringify(artifact.visibleToRoles), artifact.relatedTaskId, artifact.createdAt);
586
+ }
587
+ getArtifact(artifactId, includeContent = true) {
588
+ const row = this.db.prepare("SELECT * FROM artifacts WHERE artifact_id = ?").get(artifactId);
589
+ return row ? mapArtifact(row, includeContent) : undefined;
590
+ }
591
+ listArtifactsPaginated(taskId, options) {
592
+ const limit = this.resolveLimit(options?.limit);
593
+ let cursorTime = null;
594
+ let cursorId = null;
595
+ const decoded = decodeCursor(options?.cursor);
596
+ if (decoded) {
597
+ if (decoded.timestamp) {
598
+ cursorTime = decoded.timestamp;
599
+ cursorId = decoded.id ?? "";
600
+ }
601
+ else if (decoded.id) {
602
+ const row = this.db.prepare("SELECT created_at FROM artifacts WHERE artifact_id = ?").get(decoded.id);
603
+ if (row) {
604
+ cursorTime = String(row.created_at);
605
+ cursorId = decoded.id;
606
+ }
607
+ }
608
+ }
609
+ let rows;
610
+ if (taskId) {
611
+ rows = (cursorTime !== null && cursorId !== null)
612
+ ? this.db.prepare(`
613
+ SELECT * FROM artifacts
614
+ WHERE related_task_id = ?
615
+ AND (created_at > ? OR (created_at = ? AND artifact_id > ?))
616
+ ORDER BY created_at ASC, artifact_id ASC
617
+ LIMIT ?
618
+ `).all(taskId, cursorTime, cursorTime, cursorId, limit + 1)
619
+ : this.db.prepare(`
620
+ SELECT * FROM artifacts
621
+ WHERE related_task_id = ?
622
+ ORDER BY created_at ASC, artifact_id ASC
623
+ LIMIT ?
624
+ `).all(taskId, limit + 1);
625
+ }
626
+ else {
627
+ rows = (cursorTime !== null && cursorId !== null)
628
+ ? this.db.prepare(`
629
+ SELECT * FROM artifacts
630
+ WHERE (created_at > ? OR (created_at = ? AND artifact_id > ?))
631
+ ORDER BY created_at ASC, artifact_id ASC
632
+ LIMIT ?
633
+ `).all(cursorTime, cursorTime, cursorId, limit + 1)
634
+ : this.db.prepare(`
635
+ SELECT * FROM artifacts
636
+ ORDER BY created_at ASC, artifact_id ASC
637
+ LIMIT ?
638
+ `).all(limit + 1);
639
+ }
640
+ const hasMore = rows.length > limit;
641
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
642
+ const items = pageRows.map((row) => mapArtifact(row, false));
643
+ const last = items[items.length - 1];
644
+ const nextCursor = (hasMore && last) ? encodeCursor(last.createdAt, last.artifactId) : null;
645
+ return { items, nextCursor };
646
+ }
647
+ listArtifacts(taskId, options) {
648
+ return this.listArtifactsPaginated(taskId, options).items;
649
+ }
650
+ insertApproval(approval) {
651
+ this.db.prepare(`
652
+ INSERT INTO approvals (
653
+ approval_id, subject, subject_id, requested_by, status, context,
654
+ created_at, decided_at, decision_note
655
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
656
+ `).run(approval.approvalId, approval.subject, approval.subjectId, approval.requestedBy, approval.status, JSON.stringify(approval.context), approval.createdAt, approval.decidedAt, approval.decisionNote);
657
+ }
658
+ getApproval(approvalId) {
659
+ const row = this.db.prepare("SELECT * FROM approvals WHERE approval_id = ?").get(approvalId);
660
+ return row ? mapApproval(row) : undefined;
661
+ }
662
+ listPendingApprovalsPaginated(options) {
663
+ const limit = this.resolveLimit(options?.limit);
664
+ let cursorTime = null;
665
+ let cursorId = null;
666
+ const decoded = decodeCursor(options?.cursor);
667
+ if (decoded) {
668
+ if (decoded.timestamp) {
669
+ cursorTime = decoded.timestamp;
670
+ cursorId = decoded.id ?? "";
671
+ }
672
+ else if (decoded.id) {
673
+ const row = this.db.prepare("SELECT created_at FROM approvals WHERE approval_id = ?").get(decoded.id);
674
+ if (row) {
675
+ cursorTime = String(row.created_at);
676
+ cursorId = decoded.id;
677
+ }
678
+ }
679
+ }
680
+ const rows = (cursorTime !== null && cursorId !== null)
681
+ ? this.db.prepare(`
682
+ SELECT * FROM approvals
683
+ WHERE status = 'pending'
684
+ AND (created_at > ? OR (created_at = ? AND approval_id > ?))
685
+ ORDER BY created_at ASC, approval_id ASC
686
+ LIMIT ?
687
+ `).all(cursorTime, cursorTime, cursorId, limit + 1)
688
+ : this.db.prepare(`
689
+ SELECT * FROM approvals
690
+ WHERE status = 'pending'
691
+ ORDER BY created_at ASC, approval_id ASC
692
+ LIMIT ?
693
+ `).all(limit + 1);
694
+ const hasMore = rows.length > limit;
695
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
696
+ const items = pageRows.map(mapApproval);
697
+ const last = items[items.length - 1];
698
+ const nextCursor = (hasMore && last) ? encodeCursor(last.createdAt, last.approvalId) : null;
699
+ return { items, nextCursor };
700
+ }
701
+ listPendingApprovals(options) {
702
+ return this.listPendingApprovalsPaginated(options).items;
703
+ }
704
+ resolveApproval(approvalId, status, decidedAt, note, editedPayload) {
705
+ this.db.prepare(`
706
+ UPDATE approvals
707
+ SET status = ?, decided_at = ?, decision_note = ?, edited_payload = ?
708
+ WHERE approval_id = ?
709
+ `).run(status, decidedAt, note, editedPayload === undefined ? null : JSON.stringify(editedPayload), approvalId);
710
+ }
711
+ listAllTasksPaginated(options) {
712
+ const limit = this.resolveLimit(options?.limit);
713
+ let cursorTime = null;
714
+ let cursorId = null;
715
+ const decoded = decodeCursor(options?.cursor);
716
+ if (decoded) {
717
+ if (decoded.timestamp) {
718
+ cursorTime = decoded.timestamp;
719
+ cursorId = decoded.id ?? "";
720
+ }
721
+ else if (decoded.id) {
722
+ const row = this.db.prepare("SELECT created_at FROM tasks WHERE task_id = ?").get(decoded.id);
723
+ if (row) {
724
+ cursorTime = String(row.created_at);
725
+ cursorId = decoded.id;
726
+ }
727
+ }
728
+ }
729
+ const rows = (cursorTime !== null && cursorId !== null)
730
+ ? this.db.prepare(`
731
+ SELECT * FROM tasks
732
+ WHERE (created_at > ? OR (created_at = ? AND task_id > ?))
733
+ ORDER BY created_at ASC, task_id ASC
734
+ LIMIT ?
735
+ `).all(cursorTime, cursorTime, cursorId, limit + 1)
736
+ : this.db.prepare(`
737
+ SELECT * FROM tasks
738
+ ORDER BY created_at ASC, task_id ASC
739
+ LIMIT ?
740
+ `).all(limit + 1);
741
+ const hasMore = rows.length > limit;
742
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
743
+ const items = pageRows.map(mapTask);
744
+ const last = items[items.length - 1];
745
+ const nextCursor = (hasMore && last) ? encodeCursor(last.createdAt, last.taskId) : null;
746
+ return { items, nextCursor };
747
+ }
748
+ listAllTasks(options) {
749
+ return this.listAllTasksPaginated(options).items;
750
+ }
751
+ listAllMessagesPaginated(options) {
752
+ const limit = this.resolveLimit(options?.limit);
753
+ let cursorTime = null;
754
+ let cursorId = null;
755
+ const decoded = decodeCursor(options?.cursor);
756
+ if (decoded) {
757
+ if (decoded.timestamp) {
758
+ cursorTime = decoded.timestamp;
759
+ cursorId = decoded.id ?? "";
760
+ }
761
+ else if (decoded.id) {
762
+ const row = this.db.prepare("SELECT created_at FROM messages WHERE message_id = ?").get(decoded.id);
763
+ if (row) {
764
+ cursorTime = String(row.created_at);
765
+ cursorId = decoded.id;
766
+ }
767
+ }
768
+ }
769
+ const rows = (cursorTime !== null && cursorId !== null)
770
+ ? this.db.prepare(`
771
+ SELECT message_id, task_id, from_role, to_role, type, payload,
772
+ references_json AS "references", in_reply_to, status, risk_tags, created_at, resolved_at
773
+ FROM messages
774
+ WHERE (created_at > ? OR (created_at = ? AND message_id > ?))
775
+ ORDER BY created_at ASC, message_id ASC
776
+ LIMIT ?
777
+ `).all(cursorTime, cursorTime, cursorId, limit + 1)
778
+ : this.db.prepare(`
779
+ SELECT message_id, task_id, from_role, to_role, type, payload,
780
+ references_json AS "references", in_reply_to, status, risk_tags, created_at, resolved_at
781
+ FROM messages
782
+ ORDER BY created_at ASC, message_id ASC
783
+ LIMIT ?
784
+ `).all(limit + 1);
785
+ const hasMore = rows.length > limit;
786
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
787
+ const items = pageRows.map(mapMessage);
788
+ const last = items[items.length - 1];
789
+ const nextCursor = (hasMore && last) ? encodeCursor(last.createdAt, last.messageId) : null;
790
+ return { items, nextCursor };
791
+ }
792
+ listAllMessages(options) {
793
+ return this.listAllMessagesPaginated(options).items;
794
+ }
795
+ listAllApprovalsPaginated(options) {
796
+ const limit = this.resolveLimit(options?.limit);
797
+ let cursorTime = null;
798
+ let cursorId = null;
799
+ const decoded = decodeCursor(options?.cursor);
800
+ if (decoded) {
801
+ if (decoded.timestamp) {
802
+ cursorTime = decoded.timestamp;
803
+ cursorId = decoded.id ?? "";
804
+ }
805
+ else if (decoded.id) {
806
+ const row = this.db.prepare("SELECT created_at FROM approvals WHERE approval_id = ?").get(decoded.id);
807
+ if (row) {
808
+ cursorTime = String(row.created_at);
809
+ cursorId = decoded.id;
810
+ }
811
+ }
812
+ }
813
+ const rows = (cursorTime !== null && cursorId !== null)
814
+ ? this.db.prepare(`
815
+ SELECT * FROM approvals
816
+ WHERE (created_at > ? OR (created_at = ? AND approval_id > ?))
817
+ ORDER BY created_at ASC, approval_id ASC
818
+ LIMIT ?
819
+ `).all(cursorTime, cursorTime, cursorId, limit + 1)
820
+ : this.db.prepare(`
821
+ SELECT * FROM approvals
822
+ ORDER BY created_at ASC, approval_id ASC
823
+ LIMIT ?
824
+ `).all(limit + 1);
825
+ const hasMore = rows.length > limit;
826
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
827
+ const items = pageRows.map(mapApproval);
828
+ const last = items[items.length - 1];
829
+ const nextCursor = (hasMore && last) ? encodeCursor(last.createdAt, last.approvalId) : null;
830
+ return { items, nextCursor };
831
+ }
832
+ listAllApprovals(options) {
833
+ return this.listAllApprovalsPaginated(options).items;
834
+ }
835
+ listAllRoleBindings() {
836
+ return this.db.prepare("SELECT * FROM role_bindings ORDER BY connected_at ASC").all().map((row) => ({
837
+ roleId: String(row.role_id),
838
+ agentId: String(row.agent_id),
839
+ capabilities: json(row.capabilities),
840
+ connectedAt: String(row.connected_at),
841
+ }));
842
+ }
843
+ pruneHistory(options) {
844
+ const olderThanDays = Math.max(0, options.olderThanDays);
845
+ const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000).toISOString();
846
+ const dryRun = options.dryRun ?? false;
847
+ const deleteArtifacts = options.deleteArtifacts ?? false;
848
+ // Count eligible tasks
849
+ const eligibleTasksRow = this.db.prepare(`
850
+ SELECT COUNT(*) as count FROM tasks
851
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
852
+ `).get(cutoff);
853
+ const tasksCount = Number(eligibleTasksRow.count);
854
+ // Count artifacts associated with eligible tasks
855
+ const eligibleArtifactsRow = this.db.prepare(`
856
+ SELECT COUNT(*) as count FROM artifacts
857
+ WHERE related_task_id IN (
858
+ SELECT task_id FROM tasks
859
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
860
+ )
861
+ `).get(cutoff);
862
+ const artifactsCount = Number(eligibleArtifactsRow.count);
863
+ const artifactsDeletedCount = deleteArtifacts ? artifactsCount : 0;
864
+ const artifactsDetachedCount = deleteArtifacts ? 0 : artifactsCount;
865
+ // Count messages belonging to eligible tasks OR resolved standalone messages older than cutoff
866
+ const eligibleMessagesRow = this.db.prepare(`
867
+ SELECT COUNT(*) as count FROM messages
868
+ WHERE task_id IN (
869
+ SELECT task_id FROM tasks
870
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
871
+ ) OR (task_id IS NULL AND resolved_at IS NOT NULL AND resolved_at < ?)
872
+ `).get(cutoff, cutoff);
873
+ const messagesCount = Number(eligibleMessagesRow.count);
874
+ // Count message events
875
+ const eligibleEventsRow = this.db.prepare(`
876
+ SELECT COUNT(*) as count FROM message_events
877
+ WHERE message_id IN (
878
+ SELECT message_id FROM messages
879
+ WHERE task_id IN (
880
+ SELECT task_id FROM tasks
881
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
882
+ ) OR (task_id IS NULL AND resolved_at IS NOT NULL AND resolved_at < ?)
883
+ )
884
+ `).get(cutoff, cutoff);
885
+ const messageEventsCount = Number(eligibleEventsRow.count);
886
+ // Count task transitions
887
+ const eligibleTransitionsRow = this.db.prepare(`
888
+ SELECT COUNT(*) as count FROM task_transitions
889
+ WHERE task_id IN (
890
+ SELECT task_id FROM tasks
891
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
892
+ )
893
+ `).get(cutoff);
894
+ const taskTransitionsCount = Number(eligibleTransitionsRow.count);
895
+ // Count approvals
896
+ const eligibleApprovalsRow = this.db.prepare(`
897
+ SELECT COUNT(*) as count FROM approvals
898
+ WHERE (status IN ('approved', 'rejected') AND decided_at IS NOT NULL AND decided_at < ?)
899
+ OR (subject = 'task' AND subject_id IN (
900
+ SELECT task_id FROM tasks
901
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
902
+ ))
903
+ `).get(cutoff, cutoff);
904
+ const approvalsCount = Number(eligibleApprovalsRow.count);
905
+ const result = {
906
+ dryRun,
907
+ cutoffDate: cutoff,
908
+ tasksCount,
909
+ messagesCount,
910
+ messageEventsCount,
911
+ taskTransitionsCount,
912
+ approvalsCount,
913
+ artifactsDetachedCount,
914
+ artifactsDeletedCount,
915
+ };
916
+ if (!dryRun) {
917
+ this.transaction(() => {
918
+ // 1. Handle artifacts: either delete or detach to avoid foreign key failure
919
+ if (deleteArtifacts) {
920
+ this.db.prepare(`
921
+ DELETE FROM artifacts
922
+ WHERE related_task_id IN (
923
+ SELECT task_id FROM tasks
924
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
925
+ )
926
+ `).run(cutoff);
927
+ }
928
+ else {
929
+ this.db.prepare(`
930
+ UPDATE artifacts
931
+ SET related_task_id = NULL
932
+ WHERE related_task_id IN (
933
+ SELECT task_id FROM tasks
934
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
935
+ )
936
+ `).run(cutoff);
937
+ }
938
+ // 2. Detach in_reply_to references on any message that replies to a message about to be deleted
939
+ this.db.prepare(`
940
+ UPDATE messages
941
+ SET in_reply_to = NULL
942
+ WHERE in_reply_to IN (
943
+ SELECT message_id FROM messages
944
+ WHERE task_id IN (
945
+ SELECT task_id FROM tasks
946
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
947
+ ) OR (task_id IS NULL AND resolved_at IS NOT NULL AND resolved_at < ?)
948
+ )
949
+ `).run(cutoff, cutoff);
950
+ // 3. Delete message events
951
+ this.db.prepare(`
952
+ DELETE FROM message_events
953
+ WHERE message_id IN (
954
+ SELECT message_id FROM messages
955
+ WHERE task_id IN (
956
+ SELECT task_id FROM tasks
957
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
958
+ ) OR (task_id IS NULL AND resolved_at IS NOT NULL AND resolved_at < ?)
959
+ )
960
+ `).run(cutoff, cutoff);
961
+ // 4. Delete messages
962
+ this.db.prepare(`
963
+ DELETE FROM messages
964
+ WHERE task_id IN (
965
+ SELECT task_id FROM tasks
966
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
967
+ ) OR (task_id IS NULL AND resolved_at IS NOT NULL AND resolved_at < ?)
968
+ `).run(cutoff, cutoff);
969
+ // 5. Delete task transitions
970
+ this.db.prepare(`
971
+ DELETE FROM task_transitions
972
+ WHERE task_id IN (
973
+ SELECT task_id FROM tasks
974
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
975
+ )
976
+ `).run(cutoff);
977
+ // 6. Delete approvals
978
+ this.db.prepare(`
979
+ DELETE FROM approvals
980
+ WHERE (status IN ('approved', 'rejected') AND decided_at IS NOT NULL AND decided_at < ?)
981
+ OR (subject = 'task' AND subject_id IN (
982
+ SELECT task_id FROM tasks
983
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
984
+ ))
985
+ `).run(cutoff, cutoff);
986
+ // 7. Delete tasks
987
+ this.db.prepare(`
988
+ DELETE FROM tasks
989
+ WHERE status IN ('completed', 'failed', 'cancelled') AND updated_at < ?
990
+ `).run(cutoff);
991
+ // 8. Bounded maintenance log retention
992
+ this.db.prepare(`
993
+ DELETE FROM maintenance_log
994
+ WHERE created_at < ?
995
+ `).run(cutoff);
996
+ // 8b. Clean up expired idempotency keys
997
+ const tables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((t) => t.name);
998
+ if (tables.includes("idempotency_keys")) {
999
+ this.db.prepare(`
1000
+ DELETE FROM idempotency_keys
1001
+ WHERE created_at < ?
1002
+ `).run(cutoff);
1003
+ }
1004
+ // 9. Persist prune execution log inside transaction
1005
+ this.insertMaintenanceLog({
1006
+ id: `maint_${randomUUID()}`,
1007
+ action: "prune_execution",
1008
+ details: result,
1009
+ createdAt: new Date().toISOString(),
1010
+ });
1011
+ });
1012
+ }
1013
+ else {
1014
+ this.insertMaintenanceLog({
1015
+ id: `maint_${randomUUID()}`,
1016
+ action: "prune_simulation",
1017
+ details: result,
1018
+ createdAt: new Date().toISOString(),
1019
+ });
1020
+ }
1021
+ return result;
1022
+ }
1023
+ checkpointAndCompact() {
1024
+ const row = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
1025
+ const busy = (row?.busy ?? 0) !== 0;
1026
+ const logPages = row?.log ?? 0;
1027
+ const checkpointedPages = row?.checkpointed ?? 0;
1028
+ this.db.exec("VACUUM;");
1029
+ const result = {
1030
+ checkpoint: "TRUNCATE",
1031
+ busy,
1032
+ logPages,
1033
+ checkpointedPages,
1034
+ vacuumed: true,
1035
+ };
1036
+ this.insertMaintenanceLog({
1037
+ id: `maint_${randomUUID()}`,
1038
+ action: "compact",
1039
+ details: result,
1040
+ createdAt: new Date().toISOString(),
1041
+ });
1042
+ return result;
1043
+ }
1044
+ checkIntegrity() {
1045
+ const row = this.db.prepare("PRAGMA integrity_check").get();
1046
+ return row?.integrity_check === "ok";
1047
+ }
1048
+ insertMaintenanceLog(record, maxEntries = MAX_MAINTENANCE_LOG_ENTRIES) {
1049
+ this.db.prepare(`
1050
+ INSERT INTO maintenance_log (id, action, details_json, created_at)
1051
+ VALUES (?, ?, ?, ?)
1052
+ `).run(record.id, record.action, JSON.stringify(record.details), record.createdAt);
1053
+ // Enforce row ceiling across all maintenance paths (prune execution, simulation, compact)
1054
+ this.db.prepare(`
1055
+ DELETE FROM maintenance_log
1056
+ WHERE id NOT IN (
1057
+ SELECT id FROM maintenance_log ORDER BY created_at DESC, id DESC LIMIT ?
1058
+ )
1059
+ `).run(maxEntries);
1060
+ }
1061
+ listMaintenanceLogs(limit = 50) {
1062
+ const rows = this.db.prepare(`
1063
+ SELECT id, action, details_json, created_at
1064
+ FROM maintenance_log
1065
+ ORDER BY created_at DESC
1066
+ LIMIT ?
1067
+ `).all(limit);
1068
+ return rows.map((r) => ({
1069
+ id: String(r.id),
1070
+ action: r.action,
1071
+ details: json(r.details_json),
1072
+ createdAt: String(r.created_at),
1073
+ }));
1074
+ }
1075
+ countTasks() {
1076
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM tasks").get();
1077
+ return Number(row.count);
1078
+ }
1079
+ countMessages() {
1080
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM messages").get();
1081
+ return Number(row.count);
1082
+ }
1083
+ countApprovals() {
1084
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM approvals").get();
1085
+ return Number(row.count);
1086
+ }
1087
+ countArtifacts() {
1088
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM artifacts").get();
1089
+ return Number(row.count);
1090
+ }
1091
+ countAuditTasks(options) {
1092
+ let query = "SELECT COUNT(*) as count FROM tasks";
1093
+ const params = [];
1094
+ if (options?.since) {
1095
+ query += " WHERE (created_at >= ? OR updated_at >= ?)";
1096
+ params.push(options.since, options.since);
1097
+ }
1098
+ const row = this.db.prepare(query).get(...params);
1099
+ return Number(row.count);
1100
+ }
1101
+ countAuditMessages(options) {
1102
+ let query = "SELECT COUNT(*) as count FROM messages";
1103
+ const params = [];
1104
+ if (options?.since) {
1105
+ query += " WHERE created_at >= ?";
1106
+ params.push(options.since);
1107
+ }
1108
+ const row = this.db.prepare(query).get(...params);
1109
+ return Number(row.count);
1110
+ }
1111
+ countAuditApprovals(options) {
1112
+ let query = "SELECT COUNT(*) as count FROM approvals";
1113
+ const params = [];
1114
+ if (options?.since) {
1115
+ query += " WHERE created_at >= ?";
1116
+ params.push(options.since);
1117
+ }
1118
+ const row = this.db.prepare(query).get(...params);
1119
+ return Number(row.count);
1120
+ }
1121
+ countAuditArtifacts(options) {
1122
+ let query = "SELECT COUNT(*) as count FROM artifacts";
1123
+ const params = [];
1124
+ if (options?.since) {
1125
+ query += " WHERE created_at >= ?";
1126
+ params.push(options.since);
1127
+ }
1128
+ const row = this.db.prepare(query).get(...params);
1129
+ return Number(row.count);
1130
+ }
1131
+ getAuditTasks(options) {
1132
+ const limit = parseLimit(options?.limit, DEFAULT_AUDIT_LIMIT, MAX_AUDIT_LIMIT);
1133
+ const maxBytes = options?.maxPayloadBytes;
1134
+ const params = [];
1135
+ let query;
1136
+ if (maxBytes !== undefined && maxBytes > 0) {
1137
+ query = `SELECT
1138
+ task_id, title,
1139
+ CASE
1140
+ WHEN description IS NOT NULL AND OCTET_LENGTH(description) > ? THEN NULL
1141
+ ELSE description
1142
+ END AS description,
1143
+ CASE
1144
+ WHEN description IS NOT NULL AND OCTET_LENGTH(description) > ? THEN SUBSTR(CAST(description AS BLOB), 1, ?)
1145
+ ELSE NULL
1146
+ END AS description_preview,
1147
+ CASE WHEN description IS NOT NULL THEN OCTET_LENGTH(description) ELSE NULL END AS description_full_length,
1148
+ created_by, assigned_to, status, created_at, updated_at
1149
+ FROM tasks`;
1150
+ params.push(maxBytes, maxBytes, auditPreviewPrefixBytes(maxBytes));
1151
+ }
1152
+ else {
1153
+ query = "SELECT *, CASE WHEN description IS NOT NULL THEN OCTET_LENGTH(description) ELSE NULL END AS description_full_length FROM tasks";
1154
+ }
1155
+ if (options?.since) {
1156
+ query += " WHERE (created_at >= ? OR updated_at >= ?)";
1157
+ params.push(options.since, options.since);
1158
+ }
1159
+ query += " ORDER BY updated_at DESC, task_id DESC LIMIT ?";
1160
+ params.push(limit);
1161
+ const rows = this.db.prepare(query).all(...params);
1162
+ const items = rows.map((r) => {
1163
+ const fullLen = typeof r.description_full_length === "number" ? r.description_full_length : 0;
1164
+ const base = mapTask(r);
1165
+ if (maxBytes !== undefined && maxBytes > 0 && fullLen > maxBytes) {
1166
+ return {
1167
+ ...base,
1168
+ description: formatAuditPreview(r.description_preview, maxBytes),
1169
+ descriptionClipped: true,
1170
+ descriptionByteLength: fullLen,
1171
+ };
1172
+ }
1173
+ return base;
1174
+ });
1175
+ items.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.taskId.localeCompare(b.taskId));
1176
+ return items;
1177
+ }
1178
+ getAuditMessages(options) {
1179
+ const limit = parseLimit(options?.limit, DEFAULT_AUDIT_LIMIT, MAX_AUDIT_LIMIT);
1180
+ const maxBytes = options?.maxPayloadBytes;
1181
+ const params = [];
1182
+ let query;
1183
+ if (maxBytes !== undefined && maxBytes > 0) {
1184
+ const previewBudget = Math.min(MAX_AUDIT_PREVIEW_BYTES, maxBytes);
1185
+ query = `SELECT
1186
+ message_id, task_id, from_role, to_role, type,
1187
+ CASE
1188
+ WHEN OCTET_LENGTH(payload) > ? THEN NULL
1189
+ ELSE payload
1190
+ END AS payload,
1191
+ CASE
1192
+ WHEN OCTET_LENGTH(payload) > ? THEN SUBSTR(CAST(payload AS BLOB), 1, ?)
1193
+ ELSE NULL
1194
+ END AS payload_preview,
1195
+ OCTET_LENGTH(payload) AS payload_full_length,
1196
+ references_json, in_reply_to, status, risk_tags, created_at, resolved_at
1197
+ FROM messages`;
1198
+ params.push(maxBytes, maxBytes, auditPreviewPrefixBytes(previewBudget));
1199
+ }
1200
+ else {
1201
+ query = "SELECT *, OCTET_LENGTH(payload) AS payload_full_length FROM messages";
1202
+ }
1203
+ if (options?.since) {
1204
+ query += " WHERE created_at >= ?";
1205
+ params.push(options.since);
1206
+ }
1207
+ query += " ORDER BY created_at DESC, message_id DESC LIMIT ?";
1208
+ params.push(limit);
1209
+ const rows = this.db.prepare(query).all(...params);
1210
+ const items = rows.map((r) => {
1211
+ const fullLen = typeof r.payload_full_length === "number" ? r.payload_full_length : 0;
1212
+ if (maxBytes !== undefined && maxBytes > 0 && fullLen > maxBytes) {
1213
+ return {
1214
+ messageId: r.message_id,
1215
+ taskId: r.task_id ?? null,
1216
+ fromRole: r.from_role,
1217
+ toRole: r.to_role,
1218
+ type: r.type,
1219
+ payload: {
1220
+ _truncated: true,
1221
+ byteLength: fullLen,
1222
+ preview: formatAuditPreview(r.payload_preview, Math.min(MAX_AUDIT_PREVIEW_BYTES, maxBytes)),
1223
+ },
1224
+ references: JSON.parse(r.references_json ?? "[]"),
1225
+ inReplyTo: r.in_reply_to ?? null,
1226
+ status: r.status,
1227
+ riskTags: JSON.parse(r.risk_tags ?? "[]"),
1228
+ createdAt: r.created_at,
1229
+ resolvedAt: r.resolved_at ?? null,
1230
+ };
1231
+ }
1232
+ return mapMessage(r);
1233
+ });
1234
+ items.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.messageId.localeCompare(b.messageId));
1235
+ return items;
1236
+ }
1237
+ getAuditApprovals(options) {
1238
+ const limit = parseLimit(options?.limit, DEFAULT_AUDIT_LIMIT, MAX_AUDIT_LIMIT);
1239
+ const maxBytes = options?.maxPayloadBytes;
1240
+ const params = [];
1241
+ let query;
1242
+ if (maxBytes !== undefined && maxBytes > 0) {
1243
+ const previewBudget = Math.min(MAX_AUDIT_PREVIEW_BYTES, maxBytes);
1244
+ query = `SELECT
1245
+ approval_id, subject, subject_id, requested_by, status,
1246
+ CASE
1247
+ WHEN context IS NOT NULL AND OCTET_LENGTH(context) > ? THEN NULL
1248
+ ELSE context
1249
+ END AS context,
1250
+ CASE
1251
+ WHEN context IS NOT NULL AND OCTET_LENGTH(context) > ? THEN SUBSTR(CAST(context AS BLOB), 1, ?)
1252
+ ELSE NULL
1253
+ END AS context_preview,
1254
+ CASE WHEN context IS NOT NULL THEN OCTET_LENGTH(context) ELSE NULL END AS context_full_length,
1255
+ created_at, decided_at, decision_note
1256
+ FROM approvals`;
1257
+ params.push(maxBytes, maxBytes, auditPreviewPrefixBytes(previewBudget));
1258
+ }
1259
+ else {
1260
+ query = `SELECT
1261
+ approval_id, subject, subject_id, requested_by, status,
1262
+ context,
1263
+ CASE WHEN context IS NOT NULL THEN OCTET_LENGTH(context) ELSE NULL END AS context_full_length,
1264
+ created_at, decided_at, decision_note
1265
+ FROM approvals`;
1266
+ }
1267
+ if (options?.since) {
1268
+ query += " WHERE created_at >= ?";
1269
+ params.push(options.since);
1270
+ }
1271
+ query += " ORDER BY created_at DESC, approval_id DESC LIMIT ?";
1272
+ params.push(limit);
1273
+ const rows = this.db.prepare(query).all(...params);
1274
+ const items = rows.map((r) => {
1275
+ const fullLen = typeof r.context_full_length === "number" ? r.context_full_length : 0;
1276
+ if (maxBytes !== undefined && maxBytes > 0 && fullLen > maxBytes) {
1277
+ return {
1278
+ approvalId: String(r.approval_id),
1279
+ subject: String(r.subject),
1280
+ subjectId: String(r.subject_id),
1281
+ requestedBy: String(r.requested_by),
1282
+ status: String(r.status),
1283
+ context: {
1284
+ _truncated: true,
1285
+ byteLength: fullLen,
1286
+ preview: formatAuditPreview(r.context_preview, Math.min(MAX_AUDIT_PREVIEW_BYTES, maxBytes)),
1287
+ },
1288
+ createdAt: String(r.created_at),
1289
+ decidedAt: r.decided_at ? String(r.decided_at) : null,
1290
+ decisionNote: r.decision_note ? String(r.decision_note) : null,
1291
+ };
1292
+ }
1293
+ return mapApproval(r);
1294
+ });
1295
+ items.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.approvalId.localeCompare(b.approvalId));
1296
+ return items;
1297
+ }
1298
+ getAuditArtifacts(options) {
1299
+ const limit = parseLimit(options?.limit, DEFAULT_AUDIT_LIMIT, MAX_AUDIT_LIMIT);
1300
+ const maxBytes = options?.maxPayloadBytes;
1301
+ const params = [];
1302
+ let query;
1303
+ if (maxBytes !== undefined && maxBytes > 0) {
1304
+ const previewBudget = Math.min(MAX_AUDIT_PREVIEW_BYTES, maxBytes);
1305
+ query = `SELECT
1306
+ artifact_id, type, name, produced_by, content_hash,
1307
+ CASE
1308
+ WHEN content IS NOT NULL AND OCTET_LENGTH(content) > ? THEN NULL
1309
+ ELSE content
1310
+ END AS content,
1311
+ CASE
1312
+ WHEN content IS NOT NULL AND OCTET_LENGTH(content) > ? THEN SUBSTR(CAST(content AS BLOB), 1, ?)
1313
+ ELSE NULL
1314
+ END AS content_preview,
1315
+ CASE WHEN content IS NOT NULL THEN OCTET_LENGTH(content) ELSE NULL END AS content_full_length,
1316
+ content_uri, visible_to_roles, related_task_id, created_at
1317
+ FROM artifacts`;
1318
+ params.push(maxBytes, maxBytes, auditPreviewPrefixBytes(previewBudget));
1319
+ }
1320
+ else {
1321
+ query = "SELECT *, CASE WHEN content IS NOT NULL THEN OCTET_LENGTH(content) ELSE NULL END AS content_full_length FROM artifacts";
1322
+ }
1323
+ if (options?.since) {
1324
+ query += " WHERE created_at >= ?";
1325
+ params.push(options.since);
1326
+ }
1327
+ query += " ORDER BY created_at DESC, artifact_id DESC LIMIT ?";
1328
+ params.push(limit);
1329
+ const rows = this.db.prepare(query).all(...params);
1330
+ const items = rows.map((r) => {
1331
+ const fullLen = typeof r.content_full_length === "number" ? r.content_full_length : 0;
1332
+ if (maxBytes !== undefined && maxBytes > 0 && fullLen > maxBytes) {
1333
+ const item = mapArtifact(r, false);
1334
+ return {
1335
+ ...item,
1336
+ content: formatAuditPreview(r.content_preview, Math.min(MAX_AUDIT_PREVIEW_BYTES, maxBytes)),
1337
+ contentClipped: true,
1338
+ contentByteLength: fullLen,
1339
+ };
1340
+ }
1341
+ return mapArtifact(r, false);
1342
+ });
1343
+ items.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.artifactId.localeCompare(b.artifactId));
1344
+ return items;
1345
+ }
1346
+ }
1347
+ export function policyFromConfig(rule, id, timestamp) {
1348
+ return {
1349
+ ...rule,
1350
+ id,
1351
+ enabled: true,
1352
+ createdAt: timestamp,
1353
+ updatedAt: timestamp,
1354
+ };
1355
+ }
1356
+ //# sourceMappingURL=database.js.map