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.
- package/CHANGELOG.md +52 -0
- package/CONTRIBUTING.md +20 -0
- package/LICENSE +201 -0
- package/README.md +250 -0
- package/SECURITY.md +23 -0
- package/dist/audit.d.ts +25 -0
- package/dist/audit.js +203 -0
- package/dist/audit.js.map +1 -0
- package/dist/broker.d.ts +103 -0
- package/dist/broker.js +805 -0
- package/dist/broker.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +712 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +41 -0
- package/dist/config.js.map +1 -0
- package/dist/console/console.css +794 -0
- package/dist/console/console.js +802 -0
- package/dist/console/index.html +309 -0
- package/dist/credentials.d.ts +16 -0
- package/dist/credentials.js +72 -0
- package/dist/credentials.js.map +1 -0
- package/dist/database.d.ts +119 -0
- package/dist/database.js +1356 -0
- package/dist/database.js.map +1 -0
- package/dist/diagnostics.d.ts +44 -0
- package/dist/diagnostics.js +357 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +14 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +215 -0
- package/dist/mcp.js.map +1 -0
- package/dist/migrations.d.ts +13 -0
- package/dist/migrations.js +266 -0
- package/dist/migrations.js.map +1 -0
- package/dist/policy.d.ts +22 -0
- package/dist/policy.js +33 -0
- package/dist/policy.js.map +1 -0
- package/dist/server.d.ts +49 -0
- package/dist/server.js +529 -0
- package/dist/server.js.map +1 -0
- package/dist/stdio-adapter.d.ts +230 -0
- package/dist/stdio-adapter.js +406 -0
- package/dist/stdio-adapter.js.map +1 -0
- package/dist/tui.d.ts +26 -0
- package/dist/tui.js +291 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +355 -0
- package/dist/types.js +85 -0
- package/dist/types.js.map +1 -0
- package/docs/ARCHITECTURE.md +119 -0
- package/docs/README.md +37 -0
- package/docs/RELEASING.md +228 -0
- package/docs/ROADMAP.md +112 -0
- package/docs/cli-reference.md +133 -0
- package/docs/dogfooding-report.md +83 -0
- package/docs/getting-started.md +228 -0
- package/docs/guides/antigravity-setup.md +84 -0
- package/docs/guides/claude-cursor-setup.md +76 -0
- package/docs/guides/codex-setup.md +75 -0
- package/docs/guides/human-console.md +177 -0
- package/docs/mcp-tools-reference.md +235 -0
- package/docs/policy-guide.md +105 -0
- package/examples/org.toml +65 -0
- package/package.json +68 -0
package/dist/database.js
ADDED
|
@@ -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
|