@revealui/harnesses 0.1.7 → 0.1.9
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/LICENSE +104 -17
- package/README.md +6 -6
- package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +0 -1
- package/dist/chunk-ANX4L2PF.js +651 -0
- package/dist/{chunk-JG6CAG4A.js → chunk-Y4FFO3TO.js} +29 -8
- package/dist/chunk-YYAYTCRM.js +3016 -0
- package/dist/{chunk-XXEKWC6F.js → chunk-ZNIQELKZ.js} +189 -345
- package/dist/cli.js +44 -9
- package/dist/content/index.d.ts +1 -1
- package/dist/content/index.js +2 -3
- package/dist/index-w5ashbfb.d.ts +266 -0
- package/dist/index.d.ts +770 -85
- package/dist/index.js +39 -10
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +9 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +0 -1
- package/dist/workboard/index.d.ts +26 -14
- package/dist/workboard/index.js +2 -3
- package/package.json +30 -7
- package/LICENSE.commercial +0 -111
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/chunk-JG6CAG4A.js.map +0 -1
- package/dist/chunk-XLIKSLM3.js +0 -1105
- package/dist/chunk-XLIKSLM3.js.map +0 -1
- package/dist/chunk-XXEKWC6F.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/content/index.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/workboard/index.js.map +0 -1
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
// src/storage/schema.ts
|
|
2
|
+
var SCHEMA_SQL = `
|
|
3
|
+
CREATE TABLE IF NOT EXISTS agent_sessions (
|
|
4
|
+
id TEXT PRIMARY KEY,
|
|
5
|
+
env TEXT NOT NULL DEFAULT '',
|
|
6
|
+
task TEXT NOT NULL DEFAULT '(starting)',
|
|
7
|
+
files TEXT NOT NULL DEFAULT '',
|
|
8
|
+
pid INTEGER,
|
|
9
|
+
started_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
10
|
+
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
11
|
+
ended_at TIMESTAMP,
|
|
12
|
+
exit_summary TEXT
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
CREATE TABLE IF NOT EXISTS agent_messages (
|
|
16
|
+
id SERIAL PRIMARY KEY,
|
|
17
|
+
from_agent TEXT NOT NULL,
|
|
18
|
+
to_agent TEXT NOT NULL,
|
|
19
|
+
subject TEXT NOT NULL DEFAULT '',
|
|
20
|
+
body TEXT NOT NULL DEFAULT '',
|
|
21
|
+
read BOOLEAN NOT NULL DEFAULT FALSE,
|
|
22
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_messages_to_unread
|
|
26
|
+
ON agent_messages (to_agent, read) WHERE read = FALSE;
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS file_reservations (
|
|
29
|
+
file_path TEXT PRIMARY KEY,
|
|
30
|
+
agent_id TEXT NOT NULL,
|
|
31
|
+
reserved_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
32
|
+
expires_at TIMESTAMP NOT NULL,
|
|
33
|
+
reason TEXT NOT NULL DEFAULT ''
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_reservations_agent
|
|
37
|
+
ON file_reservations (agent_id);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
description TEXT NOT NULL DEFAULT '',
|
|
42
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
43
|
+
owner TEXT,
|
|
44
|
+
claimed_at TIMESTAMP,
|
|
45
|
+
completed_at TIMESTAMP,
|
|
46
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status
|
|
50
|
+
ON tasks (status);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_owner
|
|
53
|
+
ON tasks (owner) WHERE owner IS NOT NULL;
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
56
|
+
id SERIAL PRIMARY KEY,
|
|
57
|
+
agent_id TEXT NOT NULL,
|
|
58
|
+
event_type TEXT NOT NULL,
|
|
59
|
+
payload JSONB NOT NULL DEFAULT '{}',
|
|
60
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_events_agent
|
|
64
|
+
ON events (agent_id, created_at DESC);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS worktrees (
|
|
67
|
+
agent_id TEXT PRIMARY KEY,
|
|
68
|
+
branch TEXT NOT NULL,
|
|
69
|
+
worktree_path TEXT NOT NULL,
|
|
70
|
+
base_branch TEXT NOT NULL DEFAULT 'test',
|
|
71
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
72
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE TABLE IF NOT EXISTS agent_memory (
|
|
76
|
+
id SERIAL PRIMARY KEY,
|
|
77
|
+
agent_id TEXT NOT NULL,
|
|
78
|
+
memory_type TEXT NOT NULL,
|
|
79
|
+
content TEXT NOT NULL,
|
|
80
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
81
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_memory_agent_type
|
|
85
|
+
ON agent_memory (agent_id, memory_type, created_at DESC);
|
|
86
|
+
|
|
87
|
+
CREATE TABLE IF NOT EXISTS merge_requests (
|
|
88
|
+
id TEXT PRIMARY KEY,
|
|
89
|
+
agent_id TEXT NOT NULL,
|
|
90
|
+
task_id TEXT,
|
|
91
|
+
source_branch TEXT NOT NULL,
|
|
92
|
+
base_branch TEXT NOT NULL DEFAULT 'test',
|
|
93
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
94
|
+
pr_number INTEGER,
|
|
95
|
+
pr_url TEXT,
|
|
96
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
97
|
+
error_message TEXT,
|
|
98
|
+
ci_output TEXT,
|
|
99
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
100
|
+
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_merge_requests_agent
|
|
104
|
+
ON merge_requests (agent_id, status);
|
|
105
|
+
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_merge_requests_branch
|
|
107
|
+
ON merge_requests (source_branch);
|
|
108
|
+
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_merge_requests_pr
|
|
110
|
+
ON merge_requests (pr_number) WHERE pr_number IS NOT NULL;
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
// src/storage/daemon-store.ts
|
|
114
|
+
var DaemonStore = class {
|
|
115
|
+
db = null;
|
|
116
|
+
dataDir;
|
|
117
|
+
constructor(config) {
|
|
118
|
+
this.dataDir = config.dataDir;
|
|
119
|
+
}
|
|
120
|
+
/** Initialize PGlite and create tables. */
|
|
121
|
+
async init() {
|
|
122
|
+
const { PGlite: PGliteClass } = await import("@electric-sql/pglite");
|
|
123
|
+
this.db = new PGliteClass(this.dataDir);
|
|
124
|
+
await this.db.exec(SCHEMA_SQL);
|
|
125
|
+
}
|
|
126
|
+
/** Shut down the database. */
|
|
127
|
+
async close() {
|
|
128
|
+
if (this.db) {
|
|
129
|
+
await this.db.close();
|
|
130
|
+
this.db = null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
getDb() {
|
|
134
|
+
if (!this.db) throw new Error("DaemonStore not initialized - call init() first");
|
|
135
|
+
return this.db;
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Sessions
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
/** Register or update an agent session. */
|
|
141
|
+
async registerSession(session) {
|
|
142
|
+
const db = this.getDb();
|
|
143
|
+
const result = await db.query(
|
|
144
|
+
`INSERT INTO agent_sessions (id, env, task, pid)
|
|
145
|
+
VALUES ($1, $2, $3, $4)
|
|
146
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
147
|
+
env = EXCLUDED.env,
|
|
148
|
+
task = COALESCE(EXCLUDED.task, agent_sessions.task),
|
|
149
|
+
pid = COALESCE(EXCLUDED.pid, agent_sessions.pid),
|
|
150
|
+
updated_at = NOW(),
|
|
151
|
+
ended_at = NULL
|
|
152
|
+
RETURNING *`,
|
|
153
|
+
[session.id, session.env, session.task ?? "(starting)", session.pid ?? null]
|
|
154
|
+
);
|
|
155
|
+
return result.rows[0];
|
|
156
|
+
}
|
|
157
|
+
/** Update a session's task and/or files. */
|
|
158
|
+
async updateSession(id, updates) {
|
|
159
|
+
const db = this.getDb();
|
|
160
|
+
const sets = ["updated_at = NOW()"];
|
|
161
|
+
const params = [];
|
|
162
|
+
let paramIdx = 1;
|
|
163
|
+
if (updates.task !== void 0) {
|
|
164
|
+
sets.push(`task = $${paramIdx++}`);
|
|
165
|
+
params.push(updates.task);
|
|
166
|
+
}
|
|
167
|
+
if (updates.files !== void 0) {
|
|
168
|
+
sets.push(`files = $${paramIdx++}`);
|
|
169
|
+
params.push(updates.files);
|
|
170
|
+
}
|
|
171
|
+
params.push(id);
|
|
172
|
+
const result = await db.query(
|
|
173
|
+
`UPDATE agent_sessions SET ${sets.join(", ")} WHERE id = $${paramIdx} RETURNING *`,
|
|
174
|
+
params
|
|
175
|
+
);
|
|
176
|
+
return result.rows[0] ?? null;
|
|
177
|
+
}
|
|
178
|
+
/** End a session (mark ended_at, record exit summary). */
|
|
179
|
+
async endSession(id, exitSummary) {
|
|
180
|
+
const db = this.getDb();
|
|
181
|
+
await db.query(
|
|
182
|
+
`UPDATE agent_sessions SET ended_at = NOW(), exit_summary = $2, updated_at = NOW()
|
|
183
|
+
WHERE id = $1`,
|
|
184
|
+
[id, exitSummary ?? null]
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
/** Get all active sessions (ended_at IS NULL). */
|
|
188
|
+
async getActiveSessions() {
|
|
189
|
+
const db = this.getDb();
|
|
190
|
+
const result = await db.query(
|
|
191
|
+
"SELECT * FROM agent_sessions WHERE ended_at IS NULL ORDER BY started_at"
|
|
192
|
+
);
|
|
193
|
+
return result.rows;
|
|
194
|
+
}
|
|
195
|
+
/** Get session history for an agent (most recent first). */
|
|
196
|
+
async getSessionHistory(agentId, limit) {
|
|
197
|
+
const db = this.getDb();
|
|
198
|
+
const result = await db.query(
|
|
199
|
+
"SELECT * FROM agent_sessions WHERE id = $1 ORDER BY started_at DESC LIMIT $2",
|
|
200
|
+
[agentId, limit]
|
|
201
|
+
);
|
|
202
|
+
return result.rows;
|
|
203
|
+
}
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Messages
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
/** Send a message from one agent to another. */
|
|
208
|
+
async sendMessage(msg) {
|
|
209
|
+
const db = this.getDb();
|
|
210
|
+
const result = await db.query(
|
|
211
|
+
`INSERT INTO agent_messages (from_agent, to_agent, subject, body)
|
|
212
|
+
VALUES ($1, $2, $3, $4)
|
|
213
|
+
RETURNING *`,
|
|
214
|
+
[msg.fromAgent, msg.toAgent, msg.subject, msg.body ?? ""]
|
|
215
|
+
);
|
|
216
|
+
return result.rows[0];
|
|
217
|
+
}
|
|
218
|
+
/** Broadcast a message to all active agents (except sender). */
|
|
219
|
+
async broadcastMessage(msg) {
|
|
220
|
+
const active = await this.getActiveSessions();
|
|
221
|
+
let sent = 0;
|
|
222
|
+
for (const session of active) {
|
|
223
|
+
if (session.id === msg.fromAgent) continue;
|
|
224
|
+
await this.sendMessage({
|
|
225
|
+
fromAgent: msg.fromAgent,
|
|
226
|
+
toAgent: session.id,
|
|
227
|
+
subject: msg.subject,
|
|
228
|
+
body: msg.body
|
|
229
|
+
});
|
|
230
|
+
sent++;
|
|
231
|
+
}
|
|
232
|
+
return sent;
|
|
233
|
+
}
|
|
234
|
+
/** Get unread messages for an agent. */
|
|
235
|
+
async getInbox(agentId, unreadOnly) {
|
|
236
|
+
const db = this.getDb();
|
|
237
|
+
const whereClause = unreadOnly ? "WHERE to_agent = $1 AND read = FALSE" : "WHERE to_agent = $1";
|
|
238
|
+
const result = await db.query(
|
|
239
|
+
`SELECT * FROM agent_messages ${whereClause} ORDER BY created_at DESC LIMIT 50`,
|
|
240
|
+
[agentId]
|
|
241
|
+
);
|
|
242
|
+
return result.rows;
|
|
243
|
+
}
|
|
244
|
+
/** Mark messages as read. */
|
|
245
|
+
async markRead(messageIds) {
|
|
246
|
+
if (messageIds.length === 0) return;
|
|
247
|
+
const db = this.getDb();
|
|
248
|
+
const placeholders = messageIds.map((_, i) => `$${i + 1}`).join(", ");
|
|
249
|
+
await db.query(
|
|
250
|
+
`UPDATE agent_messages SET read = TRUE WHERE id IN (${placeholders})`,
|
|
251
|
+
messageIds
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// File Reservations
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
/** Reserve a file for an agent (CAS: fails if already reserved by another). */
|
|
258
|
+
async reserveFile(reservation) {
|
|
259
|
+
const db = this.getDb();
|
|
260
|
+
await db.query("DELETE FROM file_reservations WHERE expires_at < NOW()");
|
|
261
|
+
const result = await db.query(
|
|
262
|
+
`INSERT INTO file_reservations (file_path, agent_id, expires_at, reason)
|
|
263
|
+
VALUES ($1, $2, NOW() + ($3 || ' seconds')::INTERVAL, $4)
|
|
264
|
+
ON CONFLICT (file_path) DO UPDATE SET
|
|
265
|
+
agent_id = EXCLUDED.agent_id,
|
|
266
|
+
reserved_at = NOW(),
|
|
267
|
+
expires_at = EXCLUDED.expires_at,
|
|
268
|
+
reason = EXCLUDED.reason
|
|
269
|
+
WHERE file_reservations.agent_id = $2 OR file_reservations.expires_at < NOW()
|
|
270
|
+
RETURNING *`,
|
|
271
|
+
[
|
|
272
|
+
reservation.filePath,
|
|
273
|
+
reservation.agentId,
|
|
274
|
+
String(reservation.ttlSeconds),
|
|
275
|
+
reservation.reason ?? ""
|
|
276
|
+
]
|
|
277
|
+
);
|
|
278
|
+
if (result.rows.length > 0) {
|
|
279
|
+
return { success: true };
|
|
280
|
+
}
|
|
281
|
+
const existing = await db.query(
|
|
282
|
+
"SELECT agent_id FROM file_reservations WHERE file_path = $1",
|
|
283
|
+
[reservation.filePath]
|
|
284
|
+
);
|
|
285
|
+
return { success: false, holder: existing.rows[0]?.agent_id };
|
|
286
|
+
}
|
|
287
|
+
/** Check who holds a file reservation. */
|
|
288
|
+
async checkReservation(filePath) {
|
|
289
|
+
const db = this.getDb();
|
|
290
|
+
await db.query("DELETE FROM file_reservations WHERE expires_at < NOW()");
|
|
291
|
+
const result = await db.query(
|
|
292
|
+
"SELECT * FROM file_reservations WHERE file_path = $1",
|
|
293
|
+
[filePath]
|
|
294
|
+
);
|
|
295
|
+
return result.rows[0] ?? null;
|
|
296
|
+
}
|
|
297
|
+
/** Release all reservations held by an agent. */
|
|
298
|
+
async releaseAllReservations(agentId) {
|
|
299
|
+
const db = this.getDb();
|
|
300
|
+
const result = await db.query("DELETE FROM file_reservations WHERE agent_id = $1", [agentId]);
|
|
301
|
+
return result.affectedRows ?? 0;
|
|
302
|
+
}
|
|
303
|
+
/** Get all reservations for an agent. */
|
|
304
|
+
async getReservations(agentId) {
|
|
305
|
+
const db = this.getDb();
|
|
306
|
+
await db.query("DELETE FROM file_reservations WHERE expires_at < NOW()");
|
|
307
|
+
const result = await db.query(
|
|
308
|
+
"SELECT * FROM file_reservations WHERE agent_id = $1 ORDER BY reserved_at",
|
|
309
|
+
[agentId]
|
|
310
|
+
);
|
|
311
|
+
return result.rows;
|
|
312
|
+
}
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Tasks
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
/** Create a new task. */
|
|
317
|
+
async createTask(task) {
|
|
318
|
+
const db = this.getDb();
|
|
319
|
+
const result = await db.query(
|
|
320
|
+
`INSERT INTO tasks (id, description) VALUES ($1, $2)
|
|
321
|
+
ON CONFLICT (id) DO UPDATE SET description = EXCLUDED.description
|
|
322
|
+
RETURNING *`,
|
|
323
|
+
[task.id, task.description]
|
|
324
|
+
);
|
|
325
|
+
return result.rows[0];
|
|
326
|
+
}
|
|
327
|
+
/** Claim a task atomically (CAS: fails if already claimed by another agent). */
|
|
328
|
+
async claimTask(taskId, agentId) {
|
|
329
|
+
const db = this.getDb();
|
|
330
|
+
const result = await db.query(
|
|
331
|
+
`UPDATE tasks SET status = 'claimed', owner = $2, claimed_at = NOW()
|
|
332
|
+
WHERE id = $1 AND (status = 'open' OR owner = $2)
|
|
333
|
+
RETURNING *`,
|
|
334
|
+
[taskId, agentId]
|
|
335
|
+
);
|
|
336
|
+
if (result.rows.length > 0) {
|
|
337
|
+
return { success: true };
|
|
338
|
+
}
|
|
339
|
+
const existing = await db.query("SELECT owner FROM tasks WHERE id = $1", [taskId]);
|
|
340
|
+
if (existing.rows.length === 0) {
|
|
341
|
+
return { success: false, owner: void 0 };
|
|
342
|
+
}
|
|
343
|
+
return { success: false, owner: existing.rows[0].owner ?? void 0 };
|
|
344
|
+
}
|
|
345
|
+
/** Complete a task (only the owner can complete it). */
|
|
346
|
+
async completeTask(taskId, agentId) {
|
|
347
|
+
const db = this.getDb();
|
|
348
|
+
const result = await db.query(
|
|
349
|
+
`UPDATE tasks SET status = 'completed', completed_at = NOW()
|
|
350
|
+
WHERE id = $1 AND owner = $2
|
|
351
|
+
RETURNING id`,
|
|
352
|
+
[taskId, agentId]
|
|
353
|
+
);
|
|
354
|
+
return (result.rows?.length ?? 0) > 0;
|
|
355
|
+
}
|
|
356
|
+
/** Release a claimed task back to open (only the owner can release). */
|
|
357
|
+
async releaseTask(taskId, agentId) {
|
|
358
|
+
const db = this.getDb();
|
|
359
|
+
const result = await db.query(
|
|
360
|
+
`UPDATE tasks SET status = 'open', owner = NULL, claimed_at = NULL
|
|
361
|
+
WHERE id = $1 AND owner = $2
|
|
362
|
+
RETURNING id`,
|
|
363
|
+
[taskId, agentId]
|
|
364
|
+
);
|
|
365
|
+
return (result.rows?.length ?? 0) > 0;
|
|
366
|
+
}
|
|
367
|
+
/** List tasks, optionally filtered by status and/or owner. */
|
|
368
|
+
async listTasks(filter) {
|
|
369
|
+
const db = this.getDb();
|
|
370
|
+
const conditions = [];
|
|
371
|
+
const params = [];
|
|
372
|
+
let paramIdx = 1;
|
|
373
|
+
if (filter?.status) {
|
|
374
|
+
conditions.push(`status = $${paramIdx++}`);
|
|
375
|
+
params.push(filter.status);
|
|
376
|
+
}
|
|
377
|
+
if (filter?.owner) {
|
|
378
|
+
conditions.push(`owner = $${paramIdx++}`);
|
|
379
|
+
params.push(filter.owner);
|
|
380
|
+
}
|
|
381
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
382
|
+
const result = await db.query(
|
|
383
|
+
`SELECT * FROM tasks ${where} ORDER BY created_at`,
|
|
384
|
+
params
|
|
385
|
+
);
|
|
386
|
+
return result.rows;
|
|
387
|
+
}
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Events
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
/** Append an event to the audit log. */
|
|
392
|
+
async logEvent(event) {
|
|
393
|
+
const db = this.getDb();
|
|
394
|
+
const result = await db.query(
|
|
395
|
+
`INSERT INTO events (agent_id, event_type, payload)
|
|
396
|
+
VALUES ($1, $2, $3)
|
|
397
|
+
RETURNING *`,
|
|
398
|
+
[event.agentId, event.eventType, JSON.stringify(event.payload ?? {})]
|
|
399
|
+
);
|
|
400
|
+
return result.rows[0];
|
|
401
|
+
}
|
|
402
|
+
/** Get recent events (newest first). */
|
|
403
|
+
async getRecentEvents(limit) {
|
|
404
|
+
const db = this.getDb();
|
|
405
|
+
const result = await db.query(
|
|
406
|
+
"SELECT * FROM events ORDER BY created_at DESC LIMIT $1",
|
|
407
|
+
[limit]
|
|
408
|
+
);
|
|
409
|
+
return result.rows;
|
|
410
|
+
}
|
|
411
|
+
/** Prune events older than a given number of days. */
|
|
412
|
+
async pruneEvents(olderThanDays) {
|
|
413
|
+
const db = this.getDb();
|
|
414
|
+
const result = await db.query(
|
|
415
|
+
`DELETE FROM events WHERE created_at < NOW() - ($1 || ' days')::INTERVAL`,
|
|
416
|
+
[String(olderThanDays)]
|
|
417
|
+
);
|
|
418
|
+
return result.affectedRows ?? 0;
|
|
419
|
+
}
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// Worktrees
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
/** Register a worktree for an agent. */
|
|
424
|
+
async registerWorktree(wt) {
|
|
425
|
+
const db = this.getDb();
|
|
426
|
+
const result = await db.query(
|
|
427
|
+
`INSERT INTO worktrees (agent_id, branch, worktree_path, base_branch)
|
|
428
|
+
VALUES ($1, $2, $3, $4)
|
|
429
|
+
ON CONFLICT (agent_id) DO UPDATE SET
|
|
430
|
+
branch = EXCLUDED.branch,
|
|
431
|
+
worktree_path = EXCLUDED.worktree_path,
|
|
432
|
+
base_branch = EXCLUDED.base_branch,
|
|
433
|
+
status = 'active'
|
|
434
|
+
RETURNING *`,
|
|
435
|
+
[wt.agentId, wt.branch, wt.worktreePath, wt.baseBranch ?? "test"]
|
|
436
|
+
);
|
|
437
|
+
return result.rows[0];
|
|
438
|
+
}
|
|
439
|
+
/** Get a worktree by agent ID. */
|
|
440
|
+
async getWorktree(agentId) {
|
|
441
|
+
const db = this.getDb();
|
|
442
|
+
const result = await db.query("SELECT * FROM worktrees WHERE agent_id = $1", [
|
|
443
|
+
agentId
|
|
444
|
+
]);
|
|
445
|
+
return result.rows[0] ?? null;
|
|
446
|
+
}
|
|
447
|
+
/** List all active worktrees. */
|
|
448
|
+
async getActiveWorktrees() {
|
|
449
|
+
const db = this.getDb();
|
|
450
|
+
const result = await db.query(
|
|
451
|
+
"SELECT * FROM worktrees WHERE status = 'active' ORDER BY created_at"
|
|
452
|
+
);
|
|
453
|
+
return result.rows;
|
|
454
|
+
}
|
|
455
|
+
/** Update worktree status (active → merged | abandoned). */
|
|
456
|
+
async updateWorktreeStatus(agentId, status) {
|
|
457
|
+
const db = this.getDb();
|
|
458
|
+
const result = await db.query(
|
|
459
|
+
"UPDATE worktrees SET status = $2 WHERE agent_id = $1 RETURNING agent_id",
|
|
460
|
+
[agentId, status]
|
|
461
|
+
);
|
|
462
|
+
return (result.rows?.length ?? 0) > 0;
|
|
463
|
+
}
|
|
464
|
+
/** Remove a worktree record. */
|
|
465
|
+
async removeWorktree(agentId) {
|
|
466
|
+
const db = this.getDb();
|
|
467
|
+
const result = await db.query("DELETE FROM worktrees WHERE agent_id = $1 RETURNING agent_id", [
|
|
468
|
+
agentId
|
|
469
|
+
]);
|
|
470
|
+
return (result.rows?.length ?? 0) > 0;
|
|
471
|
+
}
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
// Agent Memory
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
/** Store a memory entry. */
|
|
476
|
+
async storeMemory(entry) {
|
|
477
|
+
const db = this.getDb();
|
|
478
|
+
const result = await db.query(
|
|
479
|
+
`INSERT INTO agent_memory (agent_id, memory_type, content, metadata)
|
|
480
|
+
VALUES ($1, $2, $3, $4)
|
|
481
|
+
RETURNING *`,
|
|
482
|
+
[entry.agentId, entry.memoryType, entry.content, JSON.stringify(entry.metadata ?? {})]
|
|
483
|
+
);
|
|
484
|
+
return result.rows[0];
|
|
485
|
+
}
|
|
486
|
+
/** Recall memory entries by agent and type (newest first). */
|
|
487
|
+
async recallMemory(query) {
|
|
488
|
+
const db = this.getDb();
|
|
489
|
+
const conditions = [];
|
|
490
|
+
const params = [];
|
|
491
|
+
let paramIdx = 1;
|
|
492
|
+
if (query.agentId) {
|
|
493
|
+
conditions.push(`agent_id = $${paramIdx++}`);
|
|
494
|
+
params.push(query.agentId);
|
|
495
|
+
}
|
|
496
|
+
if (query.memoryType) {
|
|
497
|
+
conditions.push(`memory_type = $${paramIdx++}`);
|
|
498
|
+
params.push(query.memoryType);
|
|
499
|
+
}
|
|
500
|
+
if (query.keyword) {
|
|
501
|
+
conditions.push(`content ILIKE $${paramIdx++}`);
|
|
502
|
+
params.push(`%${query.keyword}%`);
|
|
503
|
+
}
|
|
504
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
505
|
+
const limit = query.limit ?? 20;
|
|
506
|
+
params.push(limit);
|
|
507
|
+
const result = await db.query(
|
|
508
|
+
`SELECT * FROM agent_memory ${where} ORDER BY created_at DESC LIMIT $${paramIdx}`,
|
|
509
|
+
params
|
|
510
|
+
);
|
|
511
|
+
return result.rows;
|
|
512
|
+
}
|
|
513
|
+
/** Get a summary of recent memory (last N per type for context injection). */
|
|
514
|
+
async summarizeMemory(agentId, perType) {
|
|
515
|
+
const db = this.getDb();
|
|
516
|
+
const result = await db.query(
|
|
517
|
+
`SELECT * FROM (
|
|
518
|
+
SELECT *, ROW_NUMBER() OVER (PARTITION BY memory_type ORDER BY created_at DESC) AS rn
|
|
519
|
+
FROM agent_memory WHERE agent_id = $1
|
|
520
|
+
) sub WHERE rn <= $2
|
|
521
|
+
ORDER BY memory_type, created_at DESC`,
|
|
522
|
+
[agentId, perType]
|
|
523
|
+
);
|
|
524
|
+
return result.rows;
|
|
525
|
+
}
|
|
526
|
+
/** Prune old memory entries (keep last N per agent). */
|
|
527
|
+
async pruneMemory(keepPerAgent) {
|
|
528
|
+
const db = this.getDb();
|
|
529
|
+
const result = await db.query(
|
|
530
|
+
`DELETE FROM agent_memory WHERE id IN (
|
|
531
|
+
SELECT id FROM (
|
|
532
|
+
SELECT id, ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY created_at DESC) AS rn
|
|
533
|
+
FROM agent_memory
|
|
534
|
+
) sub WHERE rn > $1
|
|
535
|
+
)`,
|
|
536
|
+
[keepPerAgent]
|
|
537
|
+
);
|
|
538
|
+
return result.affectedRows ?? 0;
|
|
539
|
+
}
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
// Merge Requests
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
/** Create a merge request for an agent's branch. */
|
|
544
|
+
async createMergeRequest(mr) {
|
|
545
|
+
const db = this.getDb();
|
|
546
|
+
const result = await db.query(
|
|
547
|
+
`INSERT INTO merge_requests (id, agent_id, task_id, source_branch, base_branch)
|
|
548
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
549
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
550
|
+
status = 'pending',
|
|
551
|
+
retry_count = merge_requests.retry_count,
|
|
552
|
+
updated_at = NOW()
|
|
553
|
+
RETURNING *`,
|
|
554
|
+
[mr.id, mr.agentId, mr.taskId ?? null, mr.sourceBranch, mr.baseBranch ?? "test"]
|
|
555
|
+
);
|
|
556
|
+
return result.rows[0];
|
|
557
|
+
}
|
|
558
|
+
/** Get a merge request by ID. */
|
|
559
|
+
async getMergeRequest(id) {
|
|
560
|
+
const db = this.getDb();
|
|
561
|
+
const result = await db.query("SELECT * FROM merge_requests WHERE id = $1", [id]);
|
|
562
|
+
return result.rows[0] ?? null;
|
|
563
|
+
}
|
|
564
|
+
/** Get a merge request by source branch. */
|
|
565
|
+
async getMergeRequestByBranch(branch) {
|
|
566
|
+
const db = this.getDb();
|
|
567
|
+
const result = await db.query(
|
|
568
|
+
"SELECT * FROM merge_requests WHERE source_branch = $1 AND status NOT IN ('merged', 'escalated') ORDER BY created_at DESC LIMIT 1",
|
|
569
|
+
[branch]
|
|
570
|
+
);
|
|
571
|
+
return result.rows[0] ?? null;
|
|
572
|
+
}
|
|
573
|
+
/** Get a merge request by PR number. */
|
|
574
|
+
async getMergeRequestByPr(prNumber) {
|
|
575
|
+
const db = this.getDb();
|
|
576
|
+
const result = await db.query(
|
|
577
|
+
"SELECT * FROM merge_requests WHERE pr_number = $1 ORDER BY created_at DESC LIMIT 1",
|
|
578
|
+
[prNumber]
|
|
579
|
+
);
|
|
580
|
+
return result.rows[0] ?? null;
|
|
581
|
+
}
|
|
582
|
+
/** Update a merge request's fields. */
|
|
583
|
+
async updateMergeRequest(id, updates) {
|
|
584
|
+
const db = this.getDb();
|
|
585
|
+
const sets = ["updated_at = NOW()"];
|
|
586
|
+
const params = [];
|
|
587
|
+
let paramIdx = 1;
|
|
588
|
+
if (updates.status !== void 0) {
|
|
589
|
+
sets.push(`status = $${paramIdx++}`);
|
|
590
|
+
params.push(updates.status);
|
|
591
|
+
}
|
|
592
|
+
if (updates.prNumber !== void 0) {
|
|
593
|
+
sets.push(`pr_number = $${paramIdx++}`);
|
|
594
|
+
params.push(updates.prNumber);
|
|
595
|
+
}
|
|
596
|
+
if (updates.prUrl !== void 0) {
|
|
597
|
+
sets.push(`pr_url = $${paramIdx++}`);
|
|
598
|
+
params.push(updates.prUrl);
|
|
599
|
+
}
|
|
600
|
+
if (updates.errorMessage !== void 0) {
|
|
601
|
+
sets.push(`error_message = $${paramIdx++}`);
|
|
602
|
+
params.push(updates.errorMessage);
|
|
603
|
+
}
|
|
604
|
+
if (updates.ciOutput !== void 0) {
|
|
605
|
+
sets.push(`ci_output = $${paramIdx++}`);
|
|
606
|
+
params.push(updates.ciOutput);
|
|
607
|
+
}
|
|
608
|
+
params.push(id);
|
|
609
|
+
const result = await db.query(
|
|
610
|
+
`UPDATE merge_requests SET ${sets.join(", ")} WHERE id = $${paramIdx} RETURNING *`,
|
|
611
|
+
params
|
|
612
|
+
);
|
|
613
|
+
return result.rows[0] ?? null;
|
|
614
|
+
}
|
|
615
|
+
/** Increment the retry count for a merge request. */
|
|
616
|
+
async incrementMergeRetry(id) {
|
|
617
|
+
const db = this.getDb();
|
|
618
|
+
const result = await db.query(
|
|
619
|
+
`UPDATE merge_requests SET retry_count = retry_count + 1, updated_at = NOW()
|
|
620
|
+
WHERE id = $1 RETURNING retry_count`,
|
|
621
|
+
[id]
|
|
622
|
+
);
|
|
623
|
+
return result.rows[0]?.retry_count ?? 0;
|
|
624
|
+
}
|
|
625
|
+
/** List merge requests, optionally filtered by status and/or agent. */
|
|
626
|
+
async listMergeRequests(filter) {
|
|
627
|
+
const db = this.getDb();
|
|
628
|
+
const conditions = [];
|
|
629
|
+
const params = [];
|
|
630
|
+
let paramIdx = 1;
|
|
631
|
+
if (filter?.status) {
|
|
632
|
+
conditions.push(`status = $${paramIdx++}`);
|
|
633
|
+
params.push(filter.status);
|
|
634
|
+
}
|
|
635
|
+
if (filter?.agentId) {
|
|
636
|
+
conditions.push(`agent_id = $${paramIdx++}`);
|
|
637
|
+
params.push(filter.agentId);
|
|
638
|
+
}
|
|
639
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
640
|
+
const result = await db.query(
|
|
641
|
+
`SELECT * FROM merge_requests ${where} ORDER BY created_at DESC`,
|
|
642
|
+
params
|
|
643
|
+
);
|
|
644
|
+
return result.rows;
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
export {
|
|
649
|
+
SCHEMA_SQL,
|
|
650
|
+
DaemonStore
|
|
651
|
+
};
|