opencode-mem-agents 0.3.1 → 0.3.2
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/README.md +97 -0
- package/dist/contracts.d.ts +175 -0
- package/dist/contracts.js +170 -0
- package/dist/dashboard-html.d.ts +1 -1
- package/dist/dashboard-html.js +12 -12
- package/dist/index.js +273 -73
- package/dist/worker/config.d.ts +14 -0
- package/dist/worker/config.js +44 -0
- package/dist/worker/http.d.ts +23 -0
- package/dist/worker/http.js +130 -0
- package/dist/worker/repository.d.ts +61 -0
- package/dist/worker/repository.js +535 -0
- package/dist/worker/routes.d.ts +31 -0
- package/dist/worker/routes.js +251 -0
- package/dist/worker/server.d.ts +1 -0
- package/dist/worker/server.js +177 -0
- package/dist/worker/utils.d.ts +8 -0
- package/dist/worker/utils.js +98 -0
- package/dist/worker.d.ts +0 -16
- package/dist/worker.js +2 -647
- package/package.json +4 -2
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { prepareTracePayload, safeParseJson, safeSerialize, sanitizeFtsQuery, sanitizeText, toEpochMs, } from "./utils.js";
|
|
3
|
+
const SEARCH_ORDER_SQL = {
|
|
4
|
+
created_desc: "o.created_at_epoch DESC, o.id DESC",
|
|
5
|
+
created_asc: "o.created_at_epoch ASC, o.id ASC",
|
|
6
|
+
id_desc: "o.id DESC",
|
|
7
|
+
id_asc: "o.id ASC",
|
|
8
|
+
};
|
|
9
|
+
export class WorkerRepository {
|
|
10
|
+
db;
|
|
11
|
+
traceRetentionMs;
|
|
12
|
+
traceMaxPayloadChars;
|
|
13
|
+
insertObs;
|
|
14
|
+
insertObsIgnoreCapture;
|
|
15
|
+
getObsByCaptureKey;
|
|
16
|
+
getObsByIds;
|
|
17
|
+
getObsByIdInProject;
|
|
18
|
+
getLatestObsInProject;
|
|
19
|
+
findTimelineAnchorByQuery;
|
|
20
|
+
getTimelineBeforeRows;
|
|
21
|
+
getTimelineAfterRows;
|
|
22
|
+
insertIoEvent;
|
|
23
|
+
deleteStaleIoEvents;
|
|
24
|
+
getRecentTraceEvents;
|
|
25
|
+
getTraceEventsByTraceId;
|
|
26
|
+
traceWritesSincePrune = 0;
|
|
27
|
+
constructor(dbPath, traceRetentionDays = 7, traceMaxPayloadChars = 4000) {
|
|
28
|
+
this.db = new Database(dbPath);
|
|
29
|
+
this.db.pragma("journal_mode = WAL");
|
|
30
|
+
this.db.pragma("foreign_keys = ON");
|
|
31
|
+
this.traceRetentionMs = traceRetentionDays * 24 * 60 * 60 * 1000;
|
|
32
|
+
this.traceMaxPayloadChars = traceMaxPayloadChars;
|
|
33
|
+
this.initializeSchema();
|
|
34
|
+
this.insertObs = this.db.prepare(`
|
|
35
|
+
INSERT INTO observations
|
|
36
|
+
(session_id, project, type, title, text, facts, narrative, concepts,
|
|
37
|
+
files_read, files_modified, agent, workflow_id, task_id, phase, signal,
|
|
38
|
+
capture_key, source, created_at, created_at_epoch)
|
|
39
|
+
VALUES
|
|
40
|
+
(@session_id, @project, @type, @title, @text, @facts, @narrative, @concepts,
|
|
41
|
+
@files_read, @files_modified, @agent, @workflow_id, @task_id, @phase, @signal,
|
|
42
|
+
@capture_key, @source, @created_at, @created_at_epoch)
|
|
43
|
+
`);
|
|
44
|
+
this.insertObsIgnoreCapture = this.db.prepare(`
|
|
45
|
+
INSERT OR IGNORE INTO observations
|
|
46
|
+
(session_id, project, type, title, text, facts, narrative, concepts,
|
|
47
|
+
files_read, files_modified, agent, workflow_id, task_id, phase, signal,
|
|
48
|
+
capture_key, source, created_at, created_at_epoch)
|
|
49
|
+
VALUES
|
|
50
|
+
(@session_id, @project, @type, @title, @text, @facts, @narrative, @concepts,
|
|
51
|
+
@files_read, @files_modified, @agent, @workflow_id, @task_id, @phase, @signal,
|
|
52
|
+
@capture_key, @source, @created_at, @created_at_epoch)
|
|
53
|
+
`);
|
|
54
|
+
this.getObsByCaptureKey = this.db.prepare(`
|
|
55
|
+
SELECT id FROM observations WHERE capture_key = ? ORDER BY id DESC LIMIT 1
|
|
56
|
+
`);
|
|
57
|
+
this.getObsByIds = this.db.prepare(`
|
|
58
|
+
SELECT * FROM observations WHERE id IN (SELECT value FROM json_each(?))
|
|
59
|
+
ORDER BY created_at_epoch DESC
|
|
60
|
+
`);
|
|
61
|
+
this.getObsByIdInProject = this.db.prepare(`
|
|
62
|
+
SELECT *
|
|
63
|
+
FROM observations
|
|
64
|
+
WHERE (? = '' OR project = ?)
|
|
65
|
+
AND id = ?
|
|
66
|
+
LIMIT 1
|
|
67
|
+
`);
|
|
68
|
+
this.getLatestObsInProject = this.db.prepare(`
|
|
69
|
+
SELECT *
|
|
70
|
+
FROM observations
|
|
71
|
+
WHERE (? = '' OR project = ?)
|
|
72
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
73
|
+
LIMIT 1
|
|
74
|
+
`);
|
|
75
|
+
this.findTimelineAnchorByQuery = this.db.prepare(`
|
|
76
|
+
SELECT o.id
|
|
77
|
+
FROM observations o
|
|
78
|
+
JOIN observations_fts fts ON o.id = fts.rowid
|
|
79
|
+
WHERE observations_fts MATCH ?
|
|
80
|
+
AND (? = '' OR o.project = ?)
|
|
81
|
+
ORDER BY rank
|
|
82
|
+
LIMIT 1
|
|
83
|
+
`);
|
|
84
|
+
this.getTimelineBeforeRows = this.db.prepare(`
|
|
85
|
+
SELECT *
|
|
86
|
+
FROM observations
|
|
87
|
+
WHERE (? = '' OR project = ?)
|
|
88
|
+
AND (
|
|
89
|
+
created_at_epoch < ?
|
|
90
|
+
OR (created_at_epoch = ? AND id < ?)
|
|
91
|
+
)
|
|
92
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
93
|
+
LIMIT ?
|
|
94
|
+
`);
|
|
95
|
+
this.getTimelineAfterRows = this.db.prepare(`
|
|
96
|
+
SELECT *
|
|
97
|
+
FROM observations
|
|
98
|
+
WHERE (? = '' OR project = ?)
|
|
99
|
+
AND (
|
|
100
|
+
created_at_epoch > ?
|
|
101
|
+
OR (created_at_epoch = ? AND id > ?)
|
|
102
|
+
)
|
|
103
|
+
ORDER BY created_at_epoch ASC, id ASC
|
|
104
|
+
LIMIT ?
|
|
105
|
+
`);
|
|
106
|
+
this.insertIoEvent = this.db.prepare(`
|
|
107
|
+
INSERT INTO io_events
|
|
108
|
+
(trace_id, direction, method, path, status, payload_json, duration_ms, created_at_epoch)
|
|
109
|
+
VALUES
|
|
110
|
+
(?, ?, ?, ?, ?, ?, ?, ?)
|
|
111
|
+
`);
|
|
112
|
+
this.deleteStaleIoEvents = this.db.prepare(`
|
|
113
|
+
DELETE FROM io_events
|
|
114
|
+
WHERE created_at_epoch < ?
|
|
115
|
+
`);
|
|
116
|
+
this.getRecentTraceEvents = this.db.prepare(`
|
|
117
|
+
SELECT id, trace_id, direction, method, path, status, payload_json, duration_ms, created_at_epoch
|
|
118
|
+
FROM io_events
|
|
119
|
+
ORDER BY created_at_epoch DESC
|
|
120
|
+
LIMIT ?
|
|
121
|
+
`);
|
|
122
|
+
this.getTraceEventsByTraceId = this.db.prepare(`
|
|
123
|
+
SELECT id, trace_id, direction, method, path, status, payload_json, duration_ms, created_at_epoch
|
|
124
|
+
FROM io_events
|
|
125
|
+
WHERE trace_id = ?
|
|
126
|
+
ORDER BY created_at_epoch DESC
|
|
127
|
+
LIMIT ?
|
|
128
|
+
`);
|
|
129
|
+
this.pruneStaleTraces();
|
|
130
|
+
}
|
|
131
|
+
close() {
|
|
132
|
+
this.db.close();
|
|
133
|
+
}
|
|
134
|
+
logTrace(traceId, direction, method, path, status, payload, durationMs = 0) {
|
|
135
|
+
try {
|
|
136
|
+
this.insertIoEvent.run(traceId, direction, method, path, status, prepareTracePayload(payload, this.traceMaxPayloadChars), Math.max(0, Math.floor(durationMs)), Date.now());
|
|
137
|
+
this.traceWritesSincePrune += 1;
|
|
138
|
+
if (this.traceWritesSincePrune >= 100) {
|
|
139
|
+
this.traceWritesSincePrune = 0;
|
|
140
|
+
this.pruneStaleTraces();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// best effort only
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
search(input) {
|
|
148
|
+
const filters = [];
|
|
149
|
+
const values = [];
|
|
150
|
+
const sanitizedQuery = input.query ? sanitizeFtsQuery(input.query) : "";
|
|
151
|
+
const hasFtsQuery = Boolean(sanitizedQuery);
|
|
152
|
+
if (input.project) {
|
|
153
|
+
filters.push("o.project = ?");
|
|
154
|
+
values.push(input.project);
|
|
155
|
+
}
|
|
156
|
+
if (input.type) {
|
|
157
|
+
filters.push("o.type = ?");
|
|
158
|
+
values.push(input.type);
|
|
159
|
+
}
|
|
160
|
+
if (input.signal) {
|
|
161
|
+
filters.push("o.signal = ?");
|
|
162
|
+
values.push(input.signal);
|
|
163
|
+
}
|
|
164
|
+
if (input.agent) {
|
|
165
|
+
filters.push("o.agent = ?");
|
|
166
|
+
values.push(input.agent);
|
|
167
|
+
}
|
|
168
|
+
const dateStartEpoch = toEpochMs(input.dateStart);
|
|
169
|
+
const dateEndEpoch = toEpochMs(input.dateEnd);
|
|
170
|
+
if (dateStartEpoch !== undefined) {
|
|
171
|
+
filters.push("o.created_at_epoch >= ?");
|
|
172
|
+
values.push(dateStartEpoch);
|
|
173
|
+
}
|
|
174
|
+
if (dateEndEpoch !== undefined) {
|
|
175
|
+
filters.push("o.created_at_epoch <= ?");
|
|
176
|
+
values.push(dateEndEpoch);
|
|
177
|
+
}
|
|
178
|
+
if (input.since_id > 0) {
|
|
179
|
+
filters.push("o.id > ?");
|
|
180
|
+
values.push(input.since_id);
|
|
181
|
+
}
|
|
182
|
+
const whereClauses = [...filters];
|
|
183
|
+
let fromClause = "observations o";
|
|
184
|
+
if (hasFtsQuery) {
|
|
185
|
+
fromClause += " JOIN observations_fts fts ON o.id = fts.rowid";
|
|
186
|
+
whereClauses.unshift("observations_fts MATCH ?");
|
|
187
|
+
values.unshift(sanitizedQuery);
|
|
188
|
+
}
|
|
189
|
+
const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
|
190
|
+
const orderSql = SEARCH_ORDER_SQL[input.orderBy] ?? SEARCH_ORDER_SQL.created_desc;
|
|
191
|
+
const rowSql = `
|
|
192
|
+
SELECT o.*
|
|
193
|
+
FROM ${fromClause}
|
|
194
|
+
${whereSql}
|
|
195
|
+
ORDER BY ${orderSql}
|
|
196
|
+
LIMIT ?
|
|
197
|
+
OFFSET ?
|
|
198
|
+
`;
|
|
199
|
+
const countSql = `
|
|
200
|
+
SELECT COUNT(*) AS count
|
|
201
|
+
FROM ${fromClause}
|
|
202
|
+
${whereSql}
|
|
203
|
+
`;
|
|
204
|
+
const observations = this.db.prepare(rowSql).all(...values, input.limit, input.offset);
|
|
205
|
+
const count = this.db.prepare(countSql).get(...values);
|
|
206
|
+
return {
|
|
207
|
+
observations,
|
|
208
|
+
totalResults: count?.count ?? observations.length,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
timeline(input) {
|
|
212
|
+
const anchor = this.resolveTimelineAnchor(input);
|
|
213
|
+
if (!anchor) {
|
|
214
|
+
return {
|
|
215
|
+
anchorId: -1,
|
|
216
|
+
observations: [],
|
|
217
|
+
depthBefore: input.depth_before,
|
|
218
|
+
depthAfter: input.depth_after,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const project = input.project ?? "";
|
|
222
|
+
const before = this.getTimelineBeforeRows.all(project, project, anchor.created_at_epoch, anchor.created_at_epoch, anchor.id, input.depth_before);
|
|
223
|
+
const after = this.getTimelineAfterRows.all(project, project, anchor.created_at_epoch, anchor.created_at_epoch, anchor.id, input.depth_after);
|
|
224
|
+
return {
|
|
225
|
+
anchorId: anchor.id,
|
|
226
|
+
observations: [...before.reverse(), anchor, ...after],
|
|
227
|
+
depthBefore: input.depth_before,
|
|
228
|
+
depthAfter: input.depth_after,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
getByIds(ids, project) {
|
|
232
|
+
if (!project) {
|
|
233
|
+
return this.getObsByIds.all(JSON.stringify(ids));
|
|
234
|
+
}
|
|
235
|
+
const sql = `
|
|
236
|
+
SELECT *
|
|
237
|
+
FROM observations
|
|
238
|
+
WHERE id IN (SELECT value FROM json_each(?))
|
|
239
|
+
AND project = ?
|
|
240
|
+
ORDER BY created_at_epoch DESC
|
|
241
|
+
`;
|
|
242
|
+
return this.db.prepare(sql).all(JSON.stringify(ids), project);
|
|
243
|
+
}
|
|
244
|
+
saveMemory(input) {
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
const type = input.type ?? "discovery";
|
|
247
|
+
const agentName = input.agent ?? input.metadata?.agent?.agentName ?? "";
|
|
248
|
+
const workflowId = input.workflow_id ?? input.metadata?.agent?.workflowId ?? "";
|
|
249
|
+
const taskId = input.task_id ?? input.metadata?.agent?.taskId ?? "";
|
|
250
|
+
const phase = input.phase ?? input.metadata?.agent?.phase ?? "";
|
|
251
|
+
const signal = input.signal ?? input.metadata?.signal ?? "medium";
|
|
252
|
+
const result = this.insertObs.run({
|
|
253
|
+
session_id: input.sessionId ?? "",
|
|
254
|
+
project: input.project ?? "",
|
|
255
|
+
type,
|
|
256
|
+
title: sanitizeText(input.title ?? "", 500),
|
|
257
|
+
text: sanitizeText(input.text, 8192),
|
|
258
|
+
facts: JSON.stringify(input.facts),
|
|
259
|
+
narrative: sanitizeText(input.narrative ?? "", 4096),
|
|
260
|
+
concepts: JSON.stringify(input.concepts),
|
|
261
|
+
files_read: JSON.stringify(input.files_read),
|
|
262
|
+
files_modified: JSON.stringify(input.files_modified),
|
|
263
|
+
agent: agentName,
|
|
264
|
+
workflow_id: workflowId,
|
|
265
|
+
task_id: taskId,
|
|
266
|
+
phase,
|
|
267
|
+
signal,
|
|
268
|
+
capture_key: "",
|
|
269
|
+
source: "memory_save",
|
|
270
|
+
created_at: new Date(now).toISOString(),
|
|
271
|
+
created_at_epoch: now,
|
|
272
|
+
});
|
|
273
|
+
return {
|
|
274
|
+
id: Number(result.lastInsertRowid),
|
|
275
|
+
status: "saved",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
saveToolResult(input) {
|
|
279
|
+
const now = input.metadata?.timestamp ?? Date.now();
|
|
280
|
+
const toolName = sanitizeText(input.tool ?? "unknown", 100);
|
|
281
|
+
const argsString = typeof input.args === "string" ? input.args : safeSerialize(input.args ?? {}, 200);
|
|
282
|
+
const captureKey = input.sessionId && input.callId
|
|
283
|
+
? `${input.sessionId}:${input.callId}:${toolName}`
|
|
284
|
+
: "";
|
|
285
|
+
const result = this.insertObsIgnoreCapture.run({
|
|
286
|
+
session_id: input.sessionId ?? "",
|
|
287
|
+
project: input.project ?? "",
|
|
288
|
+
type: "change",
|
|
289
|
+
title: sanitizeText(`[tool] ${toolName}`, 500),
|
|
290
|
+
text: sanitizeText(input.output ?? "", 8192),
|
|
291
|
+
facts: "[]",
|
|
292
|
+
narrative: sanitizeText(`Tool ${toolName} called with: ${argsString}`, 4096),
|
|
293
|
+
concepts: "[]",
|
|
294
|
+
files_read: "[]",
|
|
295
|
+
files_modified: JSON.stringify(input.files_modified),
|
|
296
|
+
agent: input.metadata?.agent?.agentName ?? "",
|
|
297
|
+
workflow_id: input.metadata?.agent?.workflowId ?? "",
|
|
298
|
+
task_id: input.metadata?.agent?.taskId ?? "",
|
|
299
|
+
phase: input.metadata?.agent?.phase ?? "",
|
|
300
|
+
signal: input.metadata?.signal ?? "medium",
|
|
301
|
+
capture_key: captureKey,
|
|
302
|
+
source: "tool_result",
|
|
303
|
+
created_at: new Date(now).toISOString(),
|
|
304
|
+
created_at_epoch: now,
|
|
305
|
+
});
|
|
306
|
+
if (result.changes > 0) {
|
|
307
|
+
return {
|
|
308
|
+
id: Number(result.lastInsertRowid),
|
|
309
|
+
status: "captured",
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (captureKey) {
|
|
313
|
+
const existing = this.getObsByCaptureKey.get(captureKey);
|
|
314
|
+
if (existing?.id) {
|
|
315
|
+
return {
|
|
316
|
+
id: existing.id,
|
|
317
|
+
status: "duplicate",
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
id: 0,
|
|
323
|
+
status: "duplicate",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
getContextRows(input) {
|
|
327
|
+
const where = [];
|
|
328
|
+
const values = [];
|
|
329
|
+
if (input.project) {
|
|
330
|
+
where.push("project = ?");
|
|
331
|
+
values.push(input.project);
|
|
332
|
+
}
|
|
333
|
+
if (input.agent) {
|
|
334
|
+
where.push("agent = ?");
|
|
335
|
+
values.push(input.agent);
|
|
336
|
+
}
|
|
337
|
+
if (input.sessionId) {
|
|
338
|
+
where.push("session_id = ?");
|
|
339
|
+
values.push(input.sessionId);
|
|
340
|
+
}
|
|
341
|
+
const whereSql = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
342
|
+
const sql = `
|
|
343
|
+
SELECT *
|
|
344
|
+
FROM observations
|
|
345
|
+
${whereSql}
|
|
346
|
+
ORDER BY created_at_epoch DESC
|
|
347
|
+
LIMIT ?
|
|
348
|
+
`;
|
|
349
|
+
return this.db.prepare(sql).all(...values, input.limit);
|
|
350
|
+
}
|
|
351
|
+
getActivityRows(input) {
|
|
352
|
+
const where = ["id > ?"];
|
|
353
|
+
const values = [input.since_id];
|
|
354
|
+
if (input.project) {
|
|
355
|
+
where.push("project = ?");
|
|
356
|
+
values.push(input.project);
|
|
357
|
+
}
|
|
358
|
+
const sql = `
|
|
359
|
+
SELECT *
|
|
360
|
+
FROM observations
|
|
361
|
+
WHERE ${where.join(" AND ")}
|
|
362
|
+
ORDER BY created_at_epoch DESC
|
|
363
|
+
LIMIT ?
|
|
364
|
+
`;
|
|
365
|
+
return this.db.prepare(sql).all(...values, input.limit);
|
|
366
|
+
}
|
|
367
|
+
getTraces(input) {
|
|
368
|
+
const rows = input.trace_id
|
|
369
|
+
? this.getTraceEventsByTraceId.all(input.trace_id, input.limit)
|
|
370
|
+
: this.getRecentTraceEvents.all(input.limit);
|
|
371
|
+
return rows
|
|
372
|
+
.map((row) => ({
|
|
373
|
+
id: row.id,
|
|
374
|
+
trace_id: row.trace_id,
|
|
375
|
+
direction: row.direction,
|
|
376
|
+
method: row.method,
|
|
377
|
+
path: row.path,
|
|
378
|
+
status: row.status,
|
|
379
|
+
payload: safeParseJson(row.payload_json, row.payload_json),
|
|
380
|
+
duration_ms: row.duration_ms,
|
|
381
|
+
created_at_epoch: row.created_at_epoch,
|
|
382
|
+
}))
|
|
383
|
+
.reverse();
|
|
384
|
+
}
|
|
385
|
+
initializeSchema() {
|
|
386
|
+
this.db.exec(`
|
|
387
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
388
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
389
|
+
session_id TEXT DEFAULT '',
|
|
390
|
+
project TEXT NOT NULL DEFAULT '',
|
|
391
|
+
type TEXT NOT NULL DEFAULT 'discovery',
|
|
392
|
+
title TEXT DEFAULT '',
|
|
393
|
+
text TEXT DEFAULT '',
|
|
394
|
+
facts TEXT DEFAULT '[]',
|
|
395
|
+
narrative TEXT DEFAULT '',
|
|
396
|
+
concepts TEXT DEFAULT '[]',
|
|
397
|
+
files_read TEXT DEFAULT '[]',
|
|
398
|
+
files_modified TEXT DEFAULT '[]',
|
|
399
|
+
agent TEXT DEFAULT '',
|
|
400
|
+
workflow_id TEXT DEFAULT '',
|
|
401
|
+
task_id TEXT DEFAULT '',
|
|
402
|
+
phase TEXT DEFAULT '',
|
|
403
|
+
signal TEXT DEFAULT 'medium',
|
|
404
|
+
capture_key TEXT DEFAULT '',
|
|
405
|
+
source TEXT DEFAULT '',
|
|
406
|
+
created_at TEXT NOT NULL,
|
|
407
|
+
created_at_epoch INTEGER NOT NULL
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
CREATE INDEX IF NOT EXISTS idx_obs_project ON observations(project);
|
|
411
|
+
CREATE INDEX IF NOT EXISTS idx_obs_type ON observations(type);
|
|
412
|
+
CREATE INDEX IF NOT EXISTS idx_obs_created ON observations(created_at_epoch DESC);
|
|
413
|
+
CREATE INDEX IF NOT EXISTS idx_obs_session ON observations(session_id);
|
|
414
|
+
CREATE INDEX IF NOT EXISTS idx_obs_workflow ON observations(workflow_id);
|
|
415
|
+
CREATE INDEX IF NOT EXISTS idx_obs_agent ON observations(agent);
|
|
416
|
+
CREATE INDEX IF NOT EXISTS idx_obs_signal ON observations(signal);
|
|
417
|
+
CREATE INDEX IF NOT EXISTS idx_obs_since ON observations(id);
|
|
418
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_obs_capture_key ON observations(capture_key)
|
|
419
|
+
WHERE capture_key <> '';
|
|
420
|
+
|
|
421
|
+
CREATE TABLE IF NOT EXISTS io_events (
|
|
422
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
423
|
+
trace_id TEXT NOT NULL,
|
|
424
|
+
direction TEXT NOT NULL,
|
|
425
|
+
method TEXT NOT NULL DEFAULT '',
|
|
426
|
+
path TEXT NOT NULL DEFAULT '',
|
|
427
|
+
status INTEGER NOT NULL DEFAULT 0,
|
|
428
|
+
payload_json TEXT NOT NULL DEFAULT '{}',
|
|
429
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
430
|
+
created_at_epoch INTEGER NOT NULL
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
CREATE INDEX IF NOT EXISTS idx_io_created ON io_events(created_at_epoch DESC);
|
|
434
|
+
CREATE INDEX IF NOT EXISTS idx_io_trace ON io_events(trace_id, created_at_epoch DESC);
|
|
435
|
+
`);
|
|
436
|
+
this.ensureColumn("observations", "capture_key", "TEXT DEFAULT ''");
|
|
437
|
+
this.ensureColumn("observations", "source", "TEXT DEFAULT ''");
|
|
438
|
+
try {
|
|
439
|
+
this.db.exec(`
|
|
440
|
+
CREATE VIRTUAL TABLE observations_fts USING fts5(
|
|
441
|
+
title, text, narrative, facts, concepts,
|
|
442
|
+
content='observations',
|
|
443
|
+
content_rowid='id'
|
|
444
|
+
);
|
|
445
|
+
`);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
// already exists
|
|
449
|
+
}
|
|
450
|
+
const triggers = [
|
|
451
|
+
`CREATE TRIGGER IF NOT EXISTS obs_fts_ai AFTER INSERT ON observations BEGIN
|
|
452
|
+
INSERT INTO observations_fts(rowid, title, text, narrative, facts, concepts)
|
|
453
|
+
VALUES (new.id, new.title, new.text, new.narrative, new.facts, new.concepts);
|
|
454
|
+
END`,
|
|
455
|
+
`CREATE TRIGGER IF NOT EXISTS obs_fts_ad AFTER DELETE ON observations BEGIN
|
|
456
|
+
INSERT INTO observations_fts(observations_fts, rowid, title, text, narrative, facts, concepts)
|
|
457
|
+
VALUES('delete', old.id, old.title, old.text, old.narrative, old.facts, old.concepts);
|
|
458
|
+
END`,
|
|
459
|
+
`CREATE TRIGGER IF NOT EXISTS obs_fts_au AFTER UPDATE ON observations BEGIN
|
|
460
|
+
INSERT INTO observations_fts(observations_fts, rowid, title, text, narrative, facts, concepts)
|
|
461
|
+
VALUES('delete', old.id, old.title, old.text, old.narrative, old.facts, old.concepts);
|
|
462
|
+
INSERT INTO observations_fts(rowid, title, text, narrative, facts, concepts)
|
|
463
|
+
VALUES (new.id, new.title, new.text, new.narrative, new.facts, new.concepts);
|
|
464
|
+
END`,
|
|
465
|
+
];
|
|
466
|
+
for (const trigger of triggers) {
|
|
467
|
+
try {
|
|
468
|
+
this.db.exec(trigger);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// ignore existing trigger errors
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
this.rebuildFtsIfOutOfSync();
|
|
475
|
+
}
|
|
476
|
+
ensureColumn(tableName, columnName, columnSpec) {
|
|
477
|
+
const columns = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
478
|
+
const hasColumn = columns.some((column) => column.name === columnName);
|
|
479
|
+
if (!hasColumn) {
|
|
480
|
+
this.db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnSpec}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
resolveTimelineAnchor(input) {
|
|
484
|
+
const project = input.project ?? "";
|
|
485
|
+
if (input.anchor && input.anchor > 0) {
|
|
486
|
+
const anchorById = this.getObsByIdInProject.get(project, project, input.anchor);
|
|
487
|
+
if (anchorById)
|
|
488
|
+
return anchorById;
|
|
489
|
+
}
|
|
490
|
+
if (input.query) {
|
|
491
|
+
const sanitized = sanitizeFtsQuery(input.query);
|
|
492
|
+
if (sanitized) {
|
|
493
|
+
try {
|
|
494
|
+
const match = this.findTimelineAnchorByQuery.get(sanitized, project, project);
|
|
495
|
+
if (match?.id) {
|
|
496
|
+
const byQuery = this.getObsByIdInProject.get(project, project, match.id);
|
|
497
|
+
if (byQuery)
|
|
498
|
+
return byQuery;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
// ignore malformed FTS input
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const latest = this.getLatestObsInProject.get(project, project);
|
|
507
|
+
return latest ?? null;
|
|
508
|
+
}
|
|
509
|
+
pruneStaleTraces() {
|
|
510
|
+
const cutoff = Date.now() - this.traceRetentionMs;
|
|
511
|
+
try {
|
|
512
|
+
this.deleteStaleIoEvents.run(cutoff);
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// best effort only
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
rebuildFtsIfOutOfSync() {
|
|
519
|
+
try {
|
|
520
|
+
const counts = this.db.prepare(`
|
|
521
|
+
SELECT
|
|
522
|
+
(SELECT COUNT(*) FROM observations) AS obs_count,
|
|
523
|
+
(SELECT COUNT(*) FROM observations_fts) AS fts_count
|
|
524
|
+
`).get();
|
|
525
|
+
if (!counts)
|
|
526
|
+
return;
|
|
527
|
+
if ((counts.obs_count ?? 0) === (counts.fts_count ?? -1))
|
|
528
|
+
return;
|
|
529
|
+
this.db.exec(`INSERT INTO observations_fts(observations_fts) VALUES ('rebuild')`);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// if FTS is unavailable, fallback search behavior still works for non-query filters
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { WorkerConfig } from "./config.js";
|
|
2
|
+
import { WorkerRepository } from "./repository.js";
|
|
3
|
+
export type RouteResult = {
|
|
4
|
+
kind: "json";
|
|
5
|
+
status: number;
|
|
6
|
+
body: Record<string, unknown>;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "text";
|
|
9
|
+
status: number;
|
|
10
|
+
body: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: "html";
|
|
13
|
+
status: number;
|
|
14
|
+
body: string;
|
|
15
|
+
};
|
|
16
|
+
export interface RouteContext {
|
|
17
|
+
config: WorkerConfig;
|
|
18
|
+
startedAt: number;
|
|
19
|
+
version: string;
|
|
20
|
+
repo: WorkerRepository;
|
|
21
|
+
}
|
|
22
|
+
export declare function handleDashboard(): RouteResult;
|
|
23
|
+
export declare function handleHealth(ctx: RouteContext): RouteResult;
|
|
24
|
+
export declare function handleSearch(ctx: RouteContext, paramsRaw: Record<string, string>): RouteResult;
|
|
25
|
+
export declare function handleTimeline(ctx: RouteContext, paramsRaw: Record<string, string>): RouteResult;
|
|
26
|
+
export declare function handleObservationsBatch(ctx: RouteContext, bodyRaw: unknown): RouteResult;
|
|
27
|
+
export declare function handleMemorySave(ctx: RouteContext, bodyRaw: unknown): RouteResult;
|
|
28
|
+
export declare function handleToolResult(ctx: RouteContext, bodyRaw: unknown): RouteResult;
|
|
29
|
+
export declare function handleContextSession(ctx: RouteContext, paramsRaw: Record<string, string>): RouteResult;
|
|
30
|
+
export declare function handleActivity(ctx: RouteContext, paramsRaw: Record<string, string>): RouteResult;
|
|
31
|
+
export declare function handleTraces(ctx: RouteContext, paramsRaw: Record<string, string>): RouteResult;
|