opencodekit 0.18.22 → 0.18.23
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/dist/index.js +1 -1
- package/dist/template/.opencode/memory.db +0 -0
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/memory.db.corrupt.1774156595899 +0 -0
- package/dist/template/.opencode/memory.db.corrupt.1774156595899-shm +0 -0
- package/dist/template/.opencode/memory.db.corrupt.1774156595899-wal +0 -0
- package/dist/template/.opencode/plugin/lib/db/schema.ts +148 -1
- package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +134 -119
- package/dist/template/.opencode/plugin/lib/memory-tools.ts +41 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
@@ -6,8 +6,27 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Database } from "bun:sqlite";
|
|
9
|
+
import { appendFileSync, existsSync, renameSync } from "node:fs";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Recovery Logger
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Log recovery messages to a file instead of stderr.
|
|
18
|
+
* Writing to stderr corrupts the TUI in OpenCode.
|
|
19
|
+
*/
|
|
20
|
+
function logRecovery(message: string): void {
|
|
21
|
+
try {
|
|
22
|
+
const logPath = path.join(process.cwd(), ".opencode/memory-recovery.log");
|
|
23
|
+
const timestamp = new Date().toISOString();
|
|
24
|
+
appendFileSync(logPath, `[${timestamp}] ${message}\n`);
|
|
25
|
+
} catch {
|
|
26
|
+
// If we can't even write a log file, silently continue
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
// ============================================================================
|
|
12
31
|
// Schema v2
|
|
13
32
|
// ============================================================================
|
|
@@ -238,7 +257,64 @@ export function getMemoryDB(): Database {
|
|
|
238
257
|
if (dbInstance) return dbInstance;
|
|
239
258
|
|
|
240
259
|
const dbPath = path.join(process.cwd(), ".opencode/memory.db");
|
|
241
|
-
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
dbInstance = new Database(dbPath, { create: true });
|
|
263
|
+
} catch (err) {
|
|
264
|
+
// Database file may be corrupted — attempt recovery
|
|
265
|
+
const recovered = attemptDBRecovery(dbPath, err);
|
|
266
|
+
if (!recovered) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Failed to open memory database: ${err instanceof Error ? err.message : String(err)}. ` +
|
|
269
|
+
`Recovery also failed. Try manually deleting ${dbPath} to start fresh.`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
dbInstance = recovered;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Verify database integrity
|
|
276
|
+
try {
|
|
277
|
+
const result = dbInstance.query("PRAGMA integrity_check").get() as {
|
|
278
|
+
integrity_check: string;
|
|
279
|
+
} | null;
|
|
280
|
+
if (result && result.integrity_check !== "ok") {
|
|
281
|
+
logRecovery(
|
|
282
|
+
`[memory-db] Integrity check failed: ${result.integrity_check}`,
|
|
283
|
+
);
|
|
284
|
+
// Close bad instance and attempt recovery
|
|
285
|
+
dbInstance.close();
|
|
286
|
+
dbInstance = null;
|
|
287
|
+
const recovered = attemptDBRecovery(dbPath, new Error("integrity check failed"));
|
|
288
|
+
if (!recovered) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`Memory database integrity check failed and recovery failed. ` +
|
|
291
|
+
`Try manually deleting ${dbPath} to start fresh.`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
dbInstance = recovered;
|
|
295
|
+
}
|
|
296
|
+
} catch (err) {
|
|
297
|
+
if (
|
|
298
|
+
err instanceof Error &&
|
|
299
|
+
err.message.includes("recovery failed")
|
|
300
|
+
) {
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
303
|
+
// integrity_check itself failed — try recovery
|
|
304
|
+
logRecovery(
|
|
305
|
+
`[memory-db] Integrity check query failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
306
|
+
);
|
|
307
|
+
dbInstance?.close();
|
|
308
|
+
dbInstance = null;
|
|
309
|
+
const recovered = attemptDBRecovery(dbPath, err);
|
|
310
|
+
if (!recovered) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Memory database is corrupted and recovery failed. ` +
|
|
313
|
+
`Try manually deleting ${dbPath} to start fresh.`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
dbInstance = recovered;
|
|
317
|
+
}
|
|
242
318
|
|
|
243
319
|
// Enable WAL mode for better concurrency
|
|
244
320
|
dbInstance.run("PRAGMA journal_mode = WAL");
|
|
@@ -250,6 +326,77 @@ export function getMemoryDB(): Database {
|
|
|
250
326
|
return dbInstance;
|
|
251
327
|
}
|
|
252
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Attempt to recover from a corrupted database.
|
|
331
|
+
* Strategy: WAL checkpoint first, then backup corrupt file + create fresh.
|
|
332
|
+
*/
|
|
333
|
+
function attemptDBRecovery(
|
|
334
|
+
dbPath: string,
|
|
335
|
+
originalError: unknown,
|
|
336
|
+
): Database | null {
|
|
337
|
+
logRecovery(
|
|
338
|
+
`[memory-db] Database recovery triggered: ${originalError instanceof Error ? originalError.message : String(originalError)}`,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// Step 1: Try WAL checkpoint recovery (if file exists and is openable)
|
|
342
|
+
try {
|
|
343
|
+
if (existsSync(dbPath)) {
|
|
344
|
+
let tempDB: Database | undefined;
|
|
345
|
+
try {
|
|
346
|
+
tempDB = new Database(dbPath);
|
|
347
|
+
tempDB.run("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
348
|
+
tempDB.close();
|
|
349
|
+
tempDB = undefined;
|
|
350
|
+
} catch {
|
|
351
|
+
tempDB?.close();
|
|
352
|
+
throw new Error("WAL checkpoint failed");
|
|
353
|
+
}
|
|
354
|
+
// Try reopening after WAL checkpoint
|
|
355
|
+
const db = new Database(dbPath, { create: true });
|
|
356
|
+
const check = db.query("PRAGMA integrity_check").get() as {
|
|
357
|
+
integrity_check: string;
|
|
358
|
+
} | null;
|
|
359
|
+
if (check?.integrity_check === "ok") {
|
|
360
|
+
logRecovery("[memory-db] WAL checkpoint recovery succeeded.");
|
|
361
|
+
return db;
|
|
362
|
+
}
|
|
363
|
+
db.close();
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
// WAL recovery failed, continue to backup + recreate
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Step 2: Backup corrupt file and create fresh database
|
|
370
|
+
try {
|
|
371
|
+
if (existsSync(dbPath)) {
|
|
372
|
+
const backupPath = `${dbPath}.corrupt.${Date.now()}`;
|
|
373
|
+
renameSync(dbPath, backupPath);
|
|
374
|
+
logRecovery(
|
|
375
|
+
`[memory-db] Corrupt database backed up to: ${backupPath}`,
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
// Also clean up WAL/SHM files
|
|
379
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
380
|
+
const walPath = dbPath + suffix;
|
|
381
|
+
if (existsSync(walPath)) {
|
|
382
|
+
renameSync(walPath, `${backupPath}${suffix}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const freshDB = new Database(dbPath, { create: true });
|
|
388
|
+
logRecovery(
|
|
389
|
+
"[memory-db] Fresh database created. Previous observations are in the backup file.",
|
|
390
|
+
);
|
|
391
|
+
return freshDB;
|
|
392
|
+
} catch (backupErr) {
|
|
393
|
+
logRecovery(
|
|
394
|
+
`[memory-db] Recovery failed: ${backupErr instanceof Error ? backupErr.message : String(backupErr)}`,
|
|
395
|
+
);
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
253
400
|
/**
|
|
254
401
|
* Close the database connection (for cleanup).
|
|
255
402
|
*/
|
|
@@ -54,133 +54,148 @@ export function createAdminTools(deps: AdminToolDeps) {
|
|
|
54
54
|
force: tool.schema.boolean().optional().describe("Force re-migration"),
|
|
55
55
|
},
|
|
56
56
|
execute: async (args, ctx) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
try {
|
|
58
|
+
const op = args.operation ?? "status";
|
|
59
|
+
const dryRun = args.dry_run ?? false;
|
|
60
|
+
const olderThanDays = args.older_than_days ?? 90;
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
case "archive": {
|
|
97
|
-
const c = archiveOldObservations({
|
|
98
|
-
olderThanDays,
|
|
99
|
-
includeSuperseded: true,
|
|
100
|
-
dryRun,
|
|
101
|
-
});
|
|
102
|
-
return dryRun
|
|
103
|
-
? `Would archive ${c} observations.`
|
|
104
|
-
: `Archived ${c} observations.`;
|
|
105
|
-
}
|
|
106
|
-
case "checkpoint": {
|
|
107
|
-
const r = checkpointWAL();
|
|
108
|
-
return r.checkpointed
|
|
109
|
-
? `WAL checkpointed (${r.walSize} pages).`
|
|
110
|
-
: "Checkpoint failed or busy.";
|
|
111
|
-
}
|
|
112
|
-
case "vacuum":
|
|
113
|
-
return vacuumDatabase() ? "Vacuumed." : "Vacuum failed.";
|
|
114
|
-
case "capture-stats":
|
|
115
|
-
return JSON.stringify(getCaptureStats(), null, 2);
|
|
116
|
-
case "distill-now": {
|
|
117
|
-
const sid = ctx?.sessionID;
|
|
118
|
-
if (!sid) return "Error: No session ID.";
|
|
119
|
-
const did = distillSession(sid);
|
|
120
|
-
return did
|
|
121
|
-
? `Distillation #${did} created.`
|
|
122
|
-
: "Not enough undistilled messages.";
|
|
123
|
-
}
|
|
124
|
-
case "curate-now": {
|
|
125
|
-
const r = curateFromDistillations();
|
|
126
|
-
return `Created ${r.created}, skipped ${r.skipped}. Patterns: ${JSON.stringify(r.patterns)}`;
|
|
127
|
-
}
|
|
128
|
-
case "migrate": {
|
|
129
|
-
const obsDir = path.join(
|
|
130
|
-
directory,
|
|
131
|
-
".opencode",
|
|
132
|
-
"memory",
|
|
133
|
-
"observations",
|
|
134
|
-
);
|
|
135
|
-
let mdFiles: string[] = [];
|
|
136
|
-
try {
|
|
137
|
-
mdFiles = (await readdir(obsDir)).filter((f) =>
|
|
138
|
-
f.endsWith(".md"),
|
|
139
|
-
);
|
|
140
|
-
} catch {
|
|
141
|
-
return "No observations directory found.";
|
|
62
|
+
switch (op) {
|
|
63
|
+
case "status": {
|
|
64
|
+
const sizes = getDatabaseSizes();
|
|
65
|
+
const stats = getObservationStats();
|
|
66
|
+
const archivable = archiveOldObservations({
|
|
67
|
+
olderThanDays,
|
|
68
|
+
dryRun: true,
|
|
69
|
+
});
|
|
70
|
+
const captureStats = getCaptureStats();
|
|
71
|
+
const distillStats = getDistillationStats();
|
|
72
|
+
return [
|
|
73
|
+
"## Memory System Status\n",
|
|
74
|
+
`**Database**: ${(sizes.total / 1024).toFixed(1)} KB`,
|
|
75
|
+
`**FTS5**: ${checkFTS5Available() ? "Available (porter stemming)" : "Unavailable"}`,
|
|
76
|
+
`**Schema**: v2 (4-tier storage)\n`,
|
|
77
|
+
"### Observations",
|
|
78
|
+
...Object.entries(stats).map(([k, v]) => ` ${k}: ${v}`),
|
|
79
|
+
` Archivable (>${olderThanDays}d): ${archivable}\n`,
|
|
80
|
+
"### Capture Pipeline",
|
|
81
|
+
` Messages: ${captureStats.total} (undistilled: ${captureStats.undistilled})`,
|
|
82
|
+
` Sessions: ${captureStats.sessions}\n`,
|
|
83
|
+
"### Distillations",
|
|
84
|
+
` Total: ${distillStats.total} (${distillStats.sessions} sessions)`,
|
|
85
|
+
` Avg compression: ${(distillStats.avgCompression * 100).toFixed(1)}%`,
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
88
|
+
case "full": {
|
|
89
|
+
if (dryRun)
|
|
90
|
+
return `Dry run: would archive, purge, optimize, checkpoint, vacuum.`;
|
|
91
|
+
const r = runFullMaintenance({
|
|
92
|
+
olderThanDays,
|
|
93
|
+
includeSuperseded: true,
|
|
94
|
+
});
|
|
95
|
+
return `Done: archived ${r.archived}, purged ${r.purgedMessages} msgs, freed ${(r.freedBytes / 1024).toFixed(1)} KB.`;
|
|
142
96
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
97
|
+
case "archive": {
|
|
98
|
+
const c = archiveOldObservations({
|
|
99
|
+
olderThanDays,
|
|
100
|
+
includeSuperseded: true,
|
|
101
|
+
dryRun,
|
|
102
|
+
});
|
|
103
|
+
return dryRun
|
|
104
|
+
? `Would archive ${c} observations.`
|
|
105
|
+
: `Archived ${c} observations.`;
|
|
106
|
+
}
|
|
107
|
+
case "checkpoint": {
|
|
108
|
+
const r = checkpointWAL();
|
|
109
|
+
return r.checkpointed
|
|
110
|
+
? `WAL checkpointed (${r.walSize} pages).`
|
|
111
|
+
: "Checkpoint failed or busy.";
|
|
112
|
+
}
|
|
113
|
+
case "vacuum":
|
|
114
|
+
return vacuumDatabase() ? "Vacuumed." : "Vacuum failed.";
|
|
115
|
+
case "capture-stats":
|
|
116
|
+
return JSON.stringify(getCaptureStats(), null, 2);
|
|
117
|
+
case "distill-now": {
|
|
118
|
+
const sid = ctx?.sessionID;
|
|
119
|
+
if (!sid) return "Error: No session ID.";
|
|
120
|
+
const did = distillSession(sid);
|
|
121
|
+
return did
|
|
122
|
+
? `Distillation #${did} created.`
|
|
123
|
+
: "Not enough undistilled messages.";
|
|
124
|
+
}
|
|
125
|
+
case "curate-now": {
|
|
126
|
+
const r = curateFromDistillations();
|
|
127
|
+
return `Created ${r.created}, skipped ${r.skipped}. Patterns: ${JSON.stringify(r.patterns)}`;
|
|
128
|
+
}
|
|
129
|
+
case "migrate": {
|
|
130
|
+
const obsDir = path.join(
|
|
131
|
+
directory,
|
|
132
|
+
".opencode",
|
|
133
|
+
"memory",
|
|
134
|
+
"observations",
|
|
135
|
+
);
|
|
136
|
+
let mdFiles: string[] = [];
|
|
152
137
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
"utf-8",
|
|
138
|
+
mdFiles = (await readdir(obsDir)).filter((f) =>
|
|
139
|
+
f.endsWith(".md"),
|
|
156
140
|
);
|
|
157
|
-
const fmMatch = content.match(
|
|
158
|
-
/^---\n([\s\S]*?)\n---\n([\s\S]*)$/,
|
|
159
|
-
);
|
|
160
|
-
const body = fmMatch ? fmMatch[2].trim() : content.trim();
|
|
161
|
-
const fm = fmMatch ? fmMatch[1] : "";
|
|
162
|
-
storeObservation({
|
|
163
|
-
type: (fm.match(/type:\s*(\w+)/)?.[1] ??
|
|
164
|
-
"discovery") as ObservationType,
|
|
165
|
-
title:
|
|
166
|
-
fm.match(/title:\s*(.+)/)?.[1]?.trim() ??
|
|
167
|
-
file.replace(/\.md$/, ""),
|
|
168
|
-
narrative: body,
|
|
169
|
-
confidence: (fm.match(/confidence:\s*(\w+)/)?.[1] ??
|
|
170
|
-
"medium") as ConfidenceLevel,
|
|
171
|
-
markdown_file: file,
|
|
172
|
-
source: "imported",
|
|
173
|
-
});
|
|
174
|
-
migrated++;
|
|
175
141
|
} catch {
|
|
176
|
-
|
|
142
|
+
return "No observations directory found.";
|
|
143
|
+
}
|
|
144
|
+
if (mdFiles.length === 0) return "No files to migrate.";
|
|
145
|
+
const existing = new Set(getMarkdownFilesInSqlite());
|
|
146
|
+
const toMigrate = args.force
|
|
147
|
+
? mdFiles
|
|
148
|
+
: mdFiles.filter((f) => !existing.has(f));
|
|
149
|
+
if (toMigrate.length === 0) return "All files already migrated.";
|
|
150
|
+
if (dryRun) return `Would migrate ${toMigrate.length} files.`;
|
|
151
|
+
let migrated = 0;
|
|
152
|
+
for (const file of toMigrate) {
|
|
153
|
+
try {
|
|
154
|
+
const content = await readFile(
|
|
155
|
+
path.join(obsDir, file),
|
|
156
|
+
"utf-8",
|
|
157
|
+
);
|
|
158
|
+
const fmMatch = content.match(
|
|
159
|
+
/^---\n([\s\S]*?)\n---\n([\s\S]*)$/,
|
|
160
|
+
);
|
|
161
|
+
const body = fmMatch ? fmMatch[2].trim() : content.trim();
|
|
162
|
+
const fm = fmMatch ? fmMatch[1] : "";
|
|
163
|
+
storeObservation({
|
|
164
|
+
type: (fm.match(/type:\s*(\w+)/)?.[1] ??
|
|
165
|
+
"discovery") as ObservationType,
|
|
166
|
+
title:
|
|
167
|
+
fm.match(/title:\s*(.+)/)?.[1]?.trim() ??
|
|
168
|
+
file.replace(/\.md$/, ""),
|
|
169
|
+
narrative: body,
|
|
170
|
+
confidence: (fm.match(/confidence:\s*(\w+)/)?.[1] ??
|
|
171
|
+
"medium") as ConfidenceLevel,
|
|
172
|
+
markdown_file: file,
|
|
173
|
+
source: "imported",
|
|
174
|
+
});
|
|
175
|
+
migrated++;
|
|
176
|
+
} catch {
|
|
177
|
+
/* Skip failed files */
|
|
178
|
+
}
|
|
177
179
|
}
|
|
180
|
+
if (migrated > 0) rebuildFTS5();
|
|
181
|
+
return `Migrated ${migrated}/${toMigrate.length} files.`;
|
|
178
182
|
}
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
default:
|
|
184
|
+
return `Unknown operation: "${op}".`;
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
188
|
+
if (
|
|
189
|
+
message.includes("database disk image is malformed") ||
|
|
190
|
+
message.includes("SQLITE_CORRUPT") ||
|
|
191
|
+
message.includes("integrity check failed")
|
|
192
|
+
) {
|
|
193
|
+
return (
|
|
194
|
+
`Error: Memory database is corrupted. ` +
|
|
195
|
+
`Automatic repair failed. Delete .opencode/memory.db to start fresh. Details: ${message}`
|
|
196
|
+
);
|
|
181
197
|
}
|
|
182
|
-
|
|
183
|
-
return `Unknown operation: "${op}".`;
|
|
198
|
+
return `Error: Admin operation failed: ${message}`;
|
|
184
199
|
}
|
|
185
200
|
},
|
|
186
201
|
}),
|
|
@@ -32,6 +32,35 @@ import {
|
|
|
32
32
|
VALID_TYPES,
|
|
33
33
|
} from "./memory-helpers.js";
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Wrap a memory tool execute function with DB error handling.
|
|
37
|
+
* Returns a user-friendly error message instead of raw SQLite crashes.
|
|
38
|
+
*/
|
|
39
|
+
function withDBErrorHandling<T extends Record<string, unknown>>(
|
|
40
|
+
fn: (args: T) => Promise<string>,
|
|
41
|
+
): (args: T) => Promise<string> {
|
|
42
|
+
return async (args: T) => {
|
|
43
|
+
try {
|
|
44
|
+
return await fn(args);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
const message =
|
|
47
|
+
err instanceof Error ? err.message : String(err);
|
|
48
|
+
if (
|
|
49
|
+
message.includes("database disk image is malformed") ||
|
|
50
|
+
message.includes("SQLITE_CORRUPT") ||
|
|
51
|
+
message.includes("integrity check failed")
|
|
52
|
+
) {
|
|
53
|
+
return (
|
|
54
|
+
`Error: Memory database is corrupted. ` +
|
|
55
|
+
`Run \`memory-admin({ operation: "full" })\` to attempt repair, ` +
|
|
56
|
+
`or delete .opencode/memory.db to start fresh. Details: ${message}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return `Error: Memory operation failed: ${message}`;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
35
64
|
interface CoreToolDeps {
|
|
36
65
|
handoffDir: string;
|
|
37
66
|
}
|
|
@@ -89,7 +118,7 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
89
118
|
.optional()
|
|
90
119
|
.describe("manual, curator, imported"),
|
|
91
120
|
},
|
|
92
|
-
execute: async (args) => {
|
|
121
|
+
execute: withDBErrorHandling(async (args) => {
|
|
93
122
|
const obsType = args.type as ObservationType;
|
|
94
123
|
if (!VALID_TYPES.includes(obsType)) {
|
|
95
124
|
return `Error: Invalid type "${args.type}". Valid: ${VALID_TYPES.join(", ")}`;
|
|
@@ -144,7 +173,7 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
144
173
|
});
|
|
145
174
|
|
|
146
175
|
return `${TYPE_ICONS[obsType] ?? "\uD83D\uDCCC"} Observation #${id} stored [${obsType}] "${args.title}" (confidence: ${confidence}, source: ${source})`;
|
|
147
|
-
},
|
|
176
|
+
}),
|
|
148
177
|
}),
|
|
149
178
|
|
|
150
179
|
"memory-search": tool({
|
|
@@ -160,7 +189,7 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
160
189
|
.optional()
|
|
161
190
|
.describe("Max results (default: 10)"),
|
|
162
191
|
},
|
|
163
|
-
execute: async (args) => {
|
|
192
|
+
execute: withDBErrorHandling(async (args) => {
|
|
164
193
|
const query = args.query.trim();
|
|
165
194
|
if (!query) return "Error: Empty search query";
|
|
166
195
|
const limit = args.limit ?? 10;
|
|
@@ -237,7 +266,7 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
237
266
|
}
|
|
238
267
|
|
|
239
268
|
return lines.length > 0 ? lines.join("\n") : "No results found.";
|
|
240
|
-
},
|
|
269
|
+
}),
|
|
241
270
|
}),
|
|
242
271
|
|
|
243
272
|
"memory-get": tool({
|
|
@@ -245,7 +274,7 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
245
274
|
args: {
|
|
246
275
|
ids: tool.schema.string().describe("Comma-separated observation IDs"),
|
|
247
276
|
},
|
|
248
|
-
execute: async (args) => {
|
|
277
|
+
execute: withDBErrorHandling(async (args) => {
|
|
249
278
|
const idList = args.ids
|
|
250
279
|
.split(",")
|
|
251
280
|
.map((s) => Number.parseInt(s.trim(), 10))
|
|
@@ -257,7 +286,7 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
257
286
|
return observations
|
|
258
287
|
.map((obs) => formatObservation(obs))
|
|
259
288
|
.join("\n\n---\n\n");
|
|
260
|
-
},
|
|
289
|
+
}),
|
|
261
290
|
}),
|
|
262
291
|
|
|
263
292
|
"memory-read": tool({
|
|
@@ -265,12 +294,12 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
265
294
|
args: {
|
|
266
295
|
file: tool.schema.string().optional().describe("Memory file path"),
|
|
267
296
|
},
|
|
268
|
-
execute: async (args) => {
|
|
297
|
+
execute: withDBErrorHandling(async (args) => {
|
|
269
298
|
const filePath = (args.file ?? "").replace(/\.md$/, "");
|
|
270
299
|
if (!filePath) return "Error: No file path provided";
|
|
271
300
|
const row = getMemoryFile(filePath);
|
|
272
301
|
return row ? row.content : `Memory file "${filePath}" not found.`;
|
|
273
|
-
},
|
|
302
|
+
}),
|
|
274
303
|
}),
|
|
275
304
|
|
|
276
305
|
"memory-update": tool({
|
|
@@ -283,7 +312,7 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
283
312
|
.optional()
|
|
284
313
|
.describe("replace (default) or append"),
|
|
285
314
|
},
|
|
286
|
-
execute: async (args) => {
|
|
315
|
+
execute: withDBErrorHandling(async (args) => {
|
|
287
316
|
const filePath = args.file.replace(/\.md$/, "");
|
|
288
317
|
const mode = (args.mode ?? "replace") as "replace" | "append";
|
|
289
318
|
let finalContent = args.content;
|
|
@@ -292,7 +321,7 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
292
321
|
}
|
|
293
322
|
upsertMemoryFile(filePath, finalContent, mode);
|
|
294
323
|
return `Memory file "${filePath}" updated (mode: ${mode}).`;
|
|
295
|
-
},
|
|
324
|
+
}),
|
|
296
325
|
}),
|
|
297
326
|
|
|
298
327
|
"memory-timeline": tool({
|
|
@@ -308,7 +337,7 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
308
337
|
.optional()
|
|
309
338
|
.describe("Later observations (default: 5)"),
|
|
310
339
|
},
|
|
311
|
-
execute: async (args) => {
|
|
340
|
+
execute: withDBErrorHandling(async (args) => {
|
|
312
341
|
const { anchor, before, after } = getTimelineAroundObservation(
|
|
313
342
|
args.anchor_id,
|
|
314
343
|
args.depth_before ?? 5,
|
|
@@ -335,7 +364,7 @@ export function createCoreTools(deps: CoreToolDeps) {
|
|
|
335
364
|
);
|
|
336
365
|
}
|
|
337
366
|
return lines.join("\n");
|
|
338
|
-
},
|
|
367
|
+
}),
|
|
339
368
|
}),
|
|
340
369
|
};
|
|
341
370
|
}
|