@jeffober/thatch 0.1.10 → 0.1.11
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/package.json +1 -1
- package/src/db.ts +48 -27
- package/src/prompts.ts +59 -2
- package/src/tool-defs.ts +13 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffober/thatch",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin), Claude Code (as an MCP server), and Cursor (as an MCP server).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/db.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface MemoryRow {
|
|
|
9
9
|
model: string | null;
|
|
10
10
|
branch: string | null;
|
|
11
11
|
confidence: number | null;
|
|
12
|
+
archived: boolean;
|
|
12
13
|
created_at: string;
|
|
13
14
|
updated_at: string;
|
|
14
15
|
recall_count: number;
|
|
@@ -69,6 +70,7 @@ export class ThatchDB {
|
|
|
69
70
|
model TEXT,
|
|
70
71
|
branch TEXT,
|
|
71
72
|
confidence INTEGER,
|
|
73
|
+
archived INTEGER NOT NULL DEFAULT 0,
|
|
72
74
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
|
73
75
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
|
74
76
|
recall_count INTEGER NOT NULL DEFAULT 0,
|
|
@@ -102,6 +104,7 @@ export class ThatchDB {
|
|
|
102
104
|
const wanted: [string, string][] = [
|
|
103
105
|
["recall_count", "INTEGER NOT NULL DEFAULT 0"],
|
|
104
106
|
["last_recalled_at", "TEXT"],
|
|
107
|
+
["archived", "INTEGER NOT NULL DEFAULT 0"],
|
|
105
108
|
];
|
|
106
109
|
for (const [col, decl] of wanted) {
|
|
107
110
|
if (!existing.has(col)) {
|
|
@@ -170,27 +173,23 @@ export class ThatchDB {
|
|
|
170
173
|
content: string,
|
|
171
174
|
embedding: Float32Array,
|
|
172
175
|
model: string,
|
|
173
|
-
opts?: { branch?: string; confidence?: number; overwrite?: boolean },
|
|
176
|
+
opts?: { branch?: string; confidence?: number; overwrite?: boolean; archived?: boolean },
|
|
174
177
|
): { ok: true } | { ok: false; error: string } {
|
|
175
178
|
const slug = this.slugify(label);
|
|
176
179
|
this.ensureStore(store);
|
|
177
180
|
|
|
178
|
-
// The embedding may be a view into a larger buffer (transformers.js tensor
|
|
179
|
-
// output); serialize exactly the view's bytes, not the whole backing buffer.
|
|
180
181
|
const blob = new Uint8Array(embedding.buffer, embedding.byteOffset, embedding.byteLength);
|
|
181
182
|
const branch = opts?.branch ?? null;
|
|
182
183
|
const confidence = opts?.confidence ?? null;
|
|
184
|
+
const archived = opts?.archived === true ? 1 : (opts?.archived === false ? 0 : null);
|
|
183
185
|
const now = new Date().toISOString();
|
|
184
|
-
const params = [slug, store, label, content, blob, model, branch, confidence, now, now];
|
|
185
186
|
|
|
186
187
|
if (!opts?.overwrite) {
|
|
187
|
-
// Plain INSERT so the PK constraint enforces no-clobber atomically —
|
|
188
|
-
// a check-then-upsert would race against concurrent sessions.
|
|
189
188
|
try {
|
|
190
189
|
this.#db.run(
|
|
191
|
-
`INSERT INTO entries (slug, store, label, content, embedding, model, branch, confidence, created_at, updated_at)
|
|
192
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
193
|
-
|
|
190
|
+
`INSERT INTO entries (slug, store, label, content, embedding, model, branch, confidence, archived, created_at, updated_at)
|
|
191
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
192
|
+
[slug, store, label, content, blob, model, branch, confidence, archived ?? 0, now, now] as any,
|
|
194
193
|
);
|
|
195
194
|
return { ok: true };
|
|
196
195
|
} catch (err: any) {
|
|
@@ -205,10 +204,25 @@ export class ThatchDB {
|
|
|
205
204
|
}
|
|
206
205
|
}
|
|
207
206
|
|
|
207
|
+
const existing = this.#db
|
|
208
|
+
.query("SELECT archived FROM entries WHERE slug = ? AND store = ?")
|
|
209
|
+
.get(slug, store) as { archived: number } | null;
|
|
210
|
+
|
|
211
|
+
if (existing && existing.archived && opts?.archived === undefined) {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
error: `"${label}" is archived. Pass archived: true to keep it archived, or archived: false to unarchive it.`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// archived column for upsert: the INSERT VALUES always needs a non-null
|
|
219
|
+
// value (NOT NULL constraint), so use archived ?? 0. To preserve the
|
|
220
|
+
// existing value when the caller doesn't specify archived, the UPDATE
|
|
221
|
+
// clause accepts a nullable extra param: NULL means "keep existing."
|
|
208
222
|
this.#db.run(
|
|
209
223
|
`
|
|
210
|
-
INSERT INTO entries (slug, store, label, content, embedding, model, branch, confidence, created_at, updated_at)
|
|
211
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
224
|
+
INSERT INTO entries (slug, store, label, content, embedding, model, branch, confidence, archived, created_at, updated_at)
|
|
225
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
212
226
|
ON CONFLICT(slug, store) DO UPDATE SET
|
|
213
227
|
label = excluded.label,
|
|
214
228
|
content = excluded.content,
|
|
@@ -216,13 +230,12 @@ export class ThatchDB {
|
|
|
216
230
|
model = excluded.model,
|
|
217
231
|
branch = COALESCE(excluded.branch, entries.branch),
|
|
218
232
|
confidence = COALESCE(excluded.confidence, entries.confidence),
|
|
233
|
+
archived = COALESCE(?, entries.archived),
|
|
219
234
|
updated_at = excluded.updated_at
|
|
220
235
|
`,
|
|
221
|
-
|
|
236
|
+
[slug, store, label, content, blob, model, branch, confidence, archived ?? 0, now, now, archived] as any,
|
|
222
237
|
);
|
|
223
238
|
|
|
224
|
-
// Content changed, so prior dedup verdicts involving this entry are stale;
|
|
225
|
-
// clear them so findDuplicates can re-flag the pair.
|
|
226
239
|
this.#db.run(
|
|
227
240
|
"DELETE FROM dedup_pairs WHERE store = ? AND (slug_a = ? OR slug_b = ?)",
|
|
228
241
|
[store, slug, slug],
|
|
@@ -242,22 +255,25 @@ export class ThatchDB {
|
|
|
242
255
|
search(
|
|
243
256
|
stores: string[],
|
|
244
257
|
queryEmbedding: Float32Array,
|
|
245
|
-
opts?: { branch?: string; limit?: number },
|
|
258
|
+
opts?: { branch?: string; limit?: number; includeArchived?: boolean },
|
|
246
259
|
): (MemoryRow & { _score: number })[] {
|
|
247
260
|
if (stores.length === 0) return [];
|
|
248
261
|
|
|
249
262
|
const limit = opts?.limit ?? 10;
|
|
250
263
|
const branch = opts?.branch;
|
|
264
|
+
const includeArchived = opts?.includeArchived ?? false;
|
|
251
265
|
|
|
252
266
|
const placeholders = stores.map(() => "?").join(", ");
|
|
253
267
|
|
|
254
268
|
interface SqlParams { sql: string; params: any[] }
|
|
255
269
|
|
|
256
270
|
const { sql, params }: SqlParams = (() => {
|
|
271
|
+
const clauses = ["store IN (" + placeholders + ")", "embedding IS NOT NULL"];
|
|
272
|
+
if (!includeArchived) clauses.push("archived = 0");
|
|
257
273
|
const base = `
|
|
258
|
-
SELECT slug, store, label, content, embedding, model, branch, confidence, created_at, updated_at, recall_count, last_recalled_at
|
|
274
|
+
SELECT slug, store, label, content, embedding, model, branch, confidence, archived, created_at, updated_at, recall_count, last_recalled_at
|
|
259
275
|
FROM entries
|
|
260
|
-
WHERE
|
|
276
|
+
WHERE ${clauses.join(" AND ")}
|
|
261
277
|
`;
|
|
262
278
|
|
|
263
279
|
if (branch) {
|
|
@@ -278,6 +294,7 @@ export class ThatchDB {
|
|
|
278
294
|
// rather than ranked. Dimension is the discriminator — model tags are
|
|
279
295
|
// informational only.
|
|
280
296
|
const scored = rows.flatMap((row) => {
|
|
297
|
+
row.archived = !!row.archived;
|
|
281
298
|
const emb = blobToVector(row.embedding!);
|
|
282
299
|
if (emb.length !== queryEmbedding.length) return [];
|
|
283
300
|
return [{ ...row, embedding: row.embedding, _score: cosineSimilarity(queryEmbedding, emb) }];
|
|
@@ -295,7 +312,7 @@ export class ThatchDB {
|
|
|
295
312
|
recall(
|
|
296
313
|
stores: string[],
|
|
297
314
|
queryEmbedding: Float32Array,
|
|
298
|
-
opts?: { branch?: string; limit?: number },
|
|
315
|
+
opts?: { branch?: string; limit?: number; includeArchived?: boolean },
|
|
299
316
|
): (MemoryRow & { _score: number })[] {
|
|
300
317
|
const top = this.search(stores, queryEmbedding, opts);
|
|
301
318
|
|
|
@@ -326,7 +343,7 @@ export class ThatchDB {
|
|
|
326
343
|
const limit = opts?.limit ?? 3;
|
|
327
344
|
|
|
328
345
|
const rows = this.#db
|
|
329
|
-
.query("SELECT slug, label, embedding FROM entries WHERE store = ? AND embedding IS NOT NULL")
|
|
346
|
+
.query("SELECT slug, label, embedding FROM entries WHERE store = ? AND archived = 0 AND embedding IS NOT NULL")
|
|
330
347
|
.all(store) as any[];
|
|
331
348
|
|
|
332
349
|
const hits: SimilarEntry[] = [];
|
|
@@ -345,10 +362,10 @@ export class ThatchDB {
|
|
|
345
362
|
}
|
|
346
363
|
|
|
347
364
|
/** Lists all entries in a store, returning metadata without content. */
|
|
348
|
-
listEntries(store: string): { slug: string; label: string; branch: string | null; confidence: number | null; updated_at: string }[] {
|
|
365
|
+
listEntries(store: string): { slug: string; label: string; branch: string | null; confidence: number | null; archived: boolean; updated_at: string }[] {
|
|
349
366
|
return this.#db
|
|
350
367
|
.query(
|
|
351
|
-
"SELECT slug, label, branch, confidence, updated_at FROM entries WHERE store = ? ORDER BY label",
|
|
368
|
+
"SELECT slug, label, branch, confidence, archived, updated_at FROM entries WHERE store = ? ORDER BY label",
|
|
352
369
|
)
|
|
353
370
|
.all(store)
|
|
354
371
|
.map((r: any) => ({
|
|
@@ -356,6 +373,7 @@ export class ThatchDB {
|
|
|
356
373
|
label: r.label,
|
|
357
374
|
branch: r.branch,
|
|
358
375
|
confidence: r.confidence,
|
|
376
|
+
archived: !!r.archived,
|
|
359
377
|
updated_at: r.updated_at,
|
|
360
378
|
}));
|
|
361
379
|
}
|
|
@@ -363,11 +381,14 @@ export class ThatchDB {
|
|
|
363
381
|
/** Full content of a single entry by label. */
|
|
364
382
|
showEntry(store: string, label: string): MemoryRow | null {
|
|
365
383
|
const slug = this.slugify(label);
|
|
366
|
-
|
|
384
|
+
const row = this.#db
|
|
367
385
|
.query(
|
|
368
|
-
"SELECT slug, store, label, content, embedding, model, branch, confidence, created_at, updated_at, recall_count, last_recalled_at FROM entries WHERE slug = ? AND store = ?",
|
|
386
|
+
"SELECT slug, store, label, content, embedding, model, branch, confidence, archived, created_at, updated_at, recall_count, last_recalled_at FROM entries WHERE slug = ? AND store = ?",
|
|
369
387
|
)
|
|
370
|
-
.get(slug, store) as
|
|
388
|
+
.get(slug, store) as Record<string, unknown> | null;
|
|
389
|
+
if (!row) return null;
|
|
390
|
+
row.archived = !!row.archived;
|
|
391
|
+
return row as unknown as MemoryRow;
|
|
371
392
|
}
|
|
372
393
|
|
|
373
394
|
forgetEntry(store: string, label: string): boolean {
|
|
@@ -386,7 +407,7 @@ export class ThatchDB {
|
|
|
386
407
|
findDuplicates(store: string, threshold = 0.85): DedupCandidate[] {
|
|
387
408
|
const rows = this.#db
|
|
388
409
|
.query(
|
|
389
|
-
"SELECT slug, label, content, embedding FROM entries WHERE store = ? AND embedding IS NOT NULL ORDER BY slug",
|
|
410
|
+
"SELECT slug, label, content, embedding FROM entries WHERE store = ? AND archived = 0 AND embedding IS NOT NULL ORDER BY slug",
|
|
390
411
|
)
|
|
391
412
|
.all(store) as any[];
|
|
392
413
|
|
|
@@ -446,7 +467,7 @@ export class ThatchDB {
|
|
|
446
467
|
const row = this.#db
|
|
447
468
|
.query(
|
|
448
469
|
`SELECT COUNT(*) AS n FROM entries
|
|
449
|
-
WHERE store = ? AND max(updated_at, COALESCE(last_recalled_at, updated_at)) < ?`,
|
|
470
|
+
WHERE store = ? AND archived = 0 AND max(updated_at, COALESCE(last_recalled_at, updated_at)) < ?`,
|
|
450
471
|
)
|
|
451
472
|
.get(store, cutoffIso) as any;
|
|
452
473
|
return row?.n ?? 0;
|
|
@@ -465,7 +486,7 @@ export class ThatchDB {
|
|
|
465
486
|
if (branches.length === 0) return 0;
|
|
466
487
|
const placeholders = branches.map(() => "?").join(", ");
|
|
467
488
|
const row = this.#db
|
|
468
|
-
.query(`SELECT COUNT(*) AS n FROM entries WHERE store = ? AND branch IN (${placeholders})`)
|
|
489
|
+
.query(`SELECT COUNT(*) AS n FROM entries WHERE store = ? AND archived = 0 AND branch IN (${placeholders})`)
|
|
469
490
|
.get(store, ...branches) as any;
|
|
470
491
|
return row?.n ?? 0;
|
|
471
492
|
}
|
package/src/prompts.ts
CHANGED
|
@@ -69,6 +69,25 @@ new project knowledge, conventions, architectural decisions, investigation resul
|
|
|
69
69
|
Session-specific context, incomplete/unverified info, anything already in
|
|
70
70
|
CLAUDE.md or OPENCODE.md, speculative conclusions.
|
|
71
71
|
|
|
72
|
+
## Archived Memories
|
|
73
|
+
|
|
74
|
+
Memories can be marked \`archived\` — a flag for stable, long-term historical
|
|
75
|
+
records that should not trigger hygiene nudges (stale, orphaned, duplicate).
|
|
76
|
+
Archived memories are excluded from search/recall results by default; pass
|
|
77
|
+
\`includeArchived: true\` to surface them for archaeological dives.
|
|
78
|
+
|
|
79
|
+
When a branch is merged or about to be deleted, consolidate its branch-scoped
|
|
80
|
+
memories into a single archived memory scoped to that same branch (preserving
|
|
81
|
+
provenance). Capture intent, design decisions, review back-and-forth, PR
|
|
82
|
+
number, unexpected pivots — the kind of git-archaeology context that explains
|
|
83
|
+
_why_ the code looks the way it does a year from now. Then memory_forget the
|
|
84
|
+
originals. The archived record outlives the branch. Future sessions searching
|
|
85
|
+
with \`includeArchived: true\` can pull it up when investigating ambiguous code.
|
|
86
|
+
|
|
87
|
+
Updating an archived memory requires passing \`archived\` explicitly — omit it
|
|
88
|
+
and the tool returns an error. Pass \`archived: true\` to keep it archived,
|
|
89
|
+
\`archived: false\` to unarchive.
|
|
90
|
+
|
|
72
91
|
## Explicit Requests
|
|
73
92
|
|
|
74
93
|
"Remember X" — save immediately.
|
|
@@ -94,7 +113,7 @@ Use thatch_store_list then thatch_memory_list to see what's available, then that
|
|
|
94
113
|
return `${base}
|
|
95
114
|
|
|
96
115
|
[thatch hygiene] ${hygiene}
|
|
97
|
-
When convenient this session (not before addressing the user's request), tend the store: run thatch_find_duplicates and consolidate clusters, review stale memories with thatch_memory_show and update or forget them, and
|
|
116
|
+
When convenient this session (not before addressing the user's request), tend the store: run thatch_find_duplicates and consolidate clusters, review stale memories with thatch_memory_show and update or forget them, and for memories scoped to deleted branches, consolidate them into an archived historical record before forgetting the originals.`;
|
|
98
117
|
}
|
|
99
118
|
|
|
100
119
|
/**
|
|
@@ -181,6 +200,25 @@ new project knowledge, conventions, architectural decisions, investigation resul
|
|
|
181
200
|
Session-specific context, incomplete/unverified info, anything already in
|
|
182
201
|
CLAUDE.md, speculative conclusions.
|
|
183
202
|
|
|
203
|
+
## Archived Memories
|
|
204
|
+
|
|
205
|
+
Memories can be marked \`archived\` — a flag for stable, long-term historical
|
|
206
|
+
records that should not trigger hygiene nudges (stale, orphaned, duplicate).
|
|
207
|
+
Archived memories are excluded from search/recall results by default; pass
|
|
208
|
+
\`includeArchived: true\` to surface them for archaeological dives.
|
|
209
|
+
|
|
210
|
+
When a branch is merged or about to be deleted, consolidate its branch-scoped
|
|
211
|
+
memories into a single archived memory scoped to that same branch (preserving
|
|
212
|
+
provenance). Capture intent, design decisions, review back-and-forth, PR
|
|
213
|
+
number, unexpected pivots — the kind of git-archaeology context that explains
|
|
214
|
+
_why_ the code looks the way it does a year from now. Then memory_forget the
|
|
215
|
+
originals. The archived record outlives the branch. Future sessions searching
|
|
216
|
+
with \`includeArchived: true\` can pull it up when investigating ambiguous code.
|
|
217
|
+
|
|
218
|
+
Updating an archived memory requires passing \`archived\` explicitly — omit it
|
|
219
|
+
and the tool returns an error. Pass \`archived: true\` to keep it archived,
|
|
220
|
+
\`archived: false\` to unarchive.
|
|
221
|
+
|
|
184
222
|
## Explicit Requests
|
|
185
223
|
|
|
186
224
|
"Remember X" — save immediately.
|
|
@@ -205,7 +243,7 @@ Use store_list then memory_list to see what's available, then memory_recall as n
|
|
|
205
243
|
return `${base}
|
|
206
244
|
|
|
207
245
|
[thatch hygiene] ${hygiene}
|
|
208
|
-
When convenient this session (not before addressing the user's request), tend the store: run find_duplicates and consolidate clusters, review stale memories with memory_show and update or forget them, and
|
|
246
|
+
When convenient this session (not before addressing the user's request), tend the store: run find_duplicates and consolidate clusters, review stale memories with memory_show and update or forget them, and for memories scoped to deleted branches, consolidate them into an archived historical record before forgetting the originals.`;
|
|
209
247
|
}
|
|
210
248
|
|
|
211
249
|
/**
|
|
@@ -309,6 +347,25 @@ new project knowledge, conventions, architectural decisions, investigation resul
|
|
|
309
347
|
Session-specific context, incomplete/unverified info, anything already in
|
|
310
348
|
AGENTS.md, speculative conclusions.
|
|
311
349
|
|
|
350
|
+
## Archived Memories
|
|
351
|
+
|
|
352
|
+
Memories can be marked \`archived\` — a flag for stable, long-term historical
|
|
353
|
+
records that should not trigger hygiene nudges (stale, orphaned, duplicate).
|
|
354
|
+
Archived memories are excluded from search/recall results by default; pass
|
|
355
|
+
\`includeArchived: true\` to surface them for archaeological dives.
|
|
356
|
+
|
|
357
|
+
When a branch is merged or about to be deleted, consolidate its branch-scoped
|
|
358
|
+
memories into a single archived memory scoped to that same branch (preserving
|
|
359
|
+
provenance). Capture intent, design decisions, review back-and-forth, PR
|
|
360
|
+
number, unexpected pivots — the kind of git-archaeology context that explains
|
|
361
|
+
_why_ the code looks the way it does a year from now. Then memory_forget the
|
|
362
|
+
originals. The archived record outlives the branch. Future sessions searching
|
|
363
|
+
with \`includeArchived: true\` can pull it up when investigating ambiguous code.
|
|
364
|
+
|
|
365
|
+
Updating an archived memory requires passing \`archived\` explicitly — omit it
|
|
366
|
+
and the tool returns an error. Pass \`archived: true\` to keep it archived,
|
|
367
|
+
\`archived: false\` to unarchive.
|
|
368
|
+
|
|
312
369
|
## Explicit Requests
|
|
313
370
|
|
|
314
371
|
"Remember X" — save immediately.
|
package/src/tool-defs.ts
CHANGED
|
@@ -40,6 +40,7 @@ function formatEntry(
|
|
|
40
40
|
let meta = `[${entry.store}]`;
|
|
41
41
|
if (entry.branch) meta += ` branch:${entry.branch}`;
|
|
42
42
|
if (entry.confidence) meta += ` confidence:${entry.confidence}`;
|
|
43
|
+
if (entry.archived) meta += " archived:true";
|
|
43
44
|
parts.push(meta, "", entry.content);
|
|
44
45
|
return parts.join("\n");
|
|
45
46
|
}
|
|
@@ -48,6 +49,7 @@ function formatRecallResult(entry: any): string {
|
|
|
48
49
|
let meta = `store:${entry.store}`;
|
|
49
50
|
if (entry.branch) meta += ` branch:${entry.branch}`;
|
|
50
51
|
if (entry.confidence) meta += ` confidence:${entry.confidence}`;
|
|
52
|
+
if (entry.archived) meta += " archived:true";
|
|
51
53
|
const score = entry._score.toFixed(3);
|
|
52
54
|
return `[${meta}] [score:${score}]\n${entry.content}`;
|
|
53
55
|
}
|
|
@@ -118,6 +120,11 @@ const rememberDef: ToolDef = {
|
|
|
118
120
|
overwrite: z.boolean().optional().describe(
|
|
119
121
|
"Set to true to replace an existing memory with the same label.",
|
|
120
122
|
),
|
|
123
|
+
archived: z.boolean().optional().describe(
|
|
124
|
+
"Mark this memory as a stable, long-term record that should not trigger hygiene nudges. " +
|
|
125
|
+
"When updating an already-archived memory, this param is REQUIRED — " +
|
|
126
|
+
"pass archived: true to keep it archived, or archived: false to unarchive.",
|
|
127
|
+
),
|
|
121
128
|
},
|
|
122
129
|
async execute(args, ctx) {
|
|
123
130
|
const store = (args.store as string) || ctx.defaultStore;
|
|
@@ -125,9 +132,6 @@ const rememberDef: ToolDef = {
|
|
|
125
132
|
const content = `# ${label}\n\n${args.content as string}`;
|
|
126
133
|
const embedding = await ctx.model.passageEmbed(content);
|
|
127
134
|
|
|
128
|
-
// Write-time collision check: the embedding is already computed, so
|
|
129
|
-
// scanning the store is nearly free. The save proceeds regardless —
|
|
130
|
-
// the agent decides whether and how to reconcile.
|
|
131
135
|
const similar = ctx.db.findSimilar(store, embedding, {
|
|
132
136
|
excludeSlug: ctx.db.slugify(label),
|
|
133
137
|
});
|
|
@@ -136,6 +140,7 @@ const rememberDef: ToolDef = {
|
|
|
136
140
|
branch: args.branch as string | undefined,
|
|
137
141
|
confidence: args.confidence as number | undefined,
|
|
138
142
|
overwrite: args.overwrite as boolean | undefined,
|
|
143
|
+
archived: args.archived as boolean | undefined,
|
|
139
144
|
});
|
|
140
145
|
|
|
141
146
|
if (!result.ok) return result.error;
|
|
@@ -172,6 +177,9 @@ const recallDef: ToolDef = {
|
|
|
172
177
|
limit: z.number().int().min(1).max(20).optional().describe(
|
|
173
178
|
"Maximum number of results. Default 10.",
|
|
174
179
|
),
|
|
180
|
+
includeArchived: z.boolean().optional().describe(
|
|
181
|
+
"Include archived memories in results. Default false — archived memories are excluded.",
|
|
182
|
+
),
|
|
175
183
|
},
|
|
176
184
|
async execute(args, ctx) {
|
|
177
185
|
const stores = args.store
|
|
@@ -183,6 +191,7 @@ const recallDef: ToolDef = {
|
|
|
183
191
|
const results = ctx.db.recall(stores, queryEmbedding, {
|
|
184
192
|
branch: args.branch as string | undefined,
|
|
185
193
|
limit,
|
|
194
|
+
includeArchived: args.includeArchived as boolean | undefined,
|
|
186
195
|
});
|
|
187
196
|
|
|
188
197
|
if (results.length === 0) return "No matching memories found.";
|
|
@@ -212,6 +221,7 @@ const listDef: ToolDef = {
|
|
|
212
221
|
let line = `[${store}] ${e.label}`;
|
|
213
222
|
if (e.branch) line += ` (branch:${e.branch})`;
|
|
214
223
|
if (e.confidence) line += ` (c:${e.confidence})`;
|
|
224
|
+
if (e.archived) line += " (archived)";
|
|
215
225
|
line += ` (${e.updated_at})`;
|
|
216
226
|
return line;
|
|
217
227
|
})
|