bikky 0.3.13 → 0.4.0
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/CONTRIBUTING.md +206 -0
- package/README.md +64 -20
- package/dist/config.d.ts +49 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +125 -4
- package/dist/config.js.map +1 -1
- package/dist/daemon/loop.d.ts.map +1 -1
- package/dist/daemon/loop.js +15 -1
- package/dist/daemon/loop.js.map +1 -1
- package/dist/daemon/qdrant.d.ts.map +1 -1
- package/dist/daemon/qdrant.js +0 -1
- package/dist/daemon/qdrant.js.map +1 -1
- package/dist/lib/qdrant-pool.d.ts +57 -0
- package/dist/lib/qdrant-pool.d.ts.map +1 -0
- package/dist/lib/qdrant-pool.js +104 -0
- package/dist/lib/qdrant-pool.js.map +1 -0
- package/dist/mcp/api.d.ts +56 -19
- package/dist/mcp/api.d.ts.map +1 -1
- package/dist/mcp/api.js +133 -72
- package/dist/mcp/api.js.map +1 -1
- package/dist/mcp/helpers.d.ts +0 -1
- package/dist/mcp/helpers.d.ts.map +1 -1
- package/dist/mcp/helpers.js +2 -15
- package/dist/mcp/helpers.js.map +1 -1
- package/dist/mcp/helpers.test.js +3 -21
- package/dist/mcp/helpers.test.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +29 -14
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/tools.d.ts +0 -7
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +337 -219
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/types.d.ts +0 -3
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/routing.d.ts +53 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +129 -0
- package/dist/routing.js.map +1 -0
- package/dist/routing.test.d.ts +2 -0
- package/dist/routing.test.d.ts.map +1 -0
- package/dist/routing.test.js +79 -0
- package/dist/routing.test.js.map +1 -0
- package/docs/config/fully-hosted.md +57 -0
- package/docs/config/hosted-models.md +50 -0
- package/docs/config/hosted-qdrant-local-models.md +39 -0
- package/docs/config/local.md +34 -0
- package/docs/configuration.md +374 -0
- package/docs/screenshots/dashboard.png +0 -0
- package/docs/screenshots/graph.png +0 -0
- package/docs/screenshots/memory.png +0 -0
- package/package.json +6 -3
- package/dist/mcp/api.test.d.ts +0 -6
- package/dist/mcp/api.test.d.ts.map +0 -1
- package/dist/mcp/api.test.js +0 -130
- package/dist/mcp/api.test.js.map +0 -1
- package/dist/mcp/tools.integration.itest.d.ts +0 -23
- package/dist/mcp/tools.integration.itest.d.ts.map +0 -1
- package/dist/mcp/tools.integration.itest.js +0 -171
- package/dist/mcp/tools.integration.itest.js.map +0 -1
- package/dist/mcp/tools.test.d.ts +0 -16
- package/dist/mcp/tools.test.d.ts.map +0 -1
- package/dist/mcp/tools.test.js +0 -908
- package/dist/mcp/tools.test.js.map +0 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -5,7 +5,8 @@ import crypto from "node:crypto";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { STALENESS_DAYS, THRESHOLD_DUPLICATE, THRESHOLD_RELATED, QDRANT_INDEXES, categoryValues, categoryEnumDescription, domainValues, domainEnumDescription, kindValues, kindEnumDescription, memorySubtypeValues, memorySubtypeEnumDescription, sourceValues, sourceEnumDescription, DEFAULT_CATEGORY, DEFAULT_DOMAIN, DEFAULT_KIND, DEFAULT_SOURCE, categoryForMemorySubtype, layerForMemorySubtype, normalizeCategory, normalizeDomain, normalizeKind, validateMemorySubtype, } from "./taxonomy.js";
|
|
7
7
|
import { contentHash, daysSince, lastActivityDate, computeCombinedScore, buildFilter, formatFact, structuredFact, MEMORY_RECALL_EXCLUDED_KINDS, } from "./helpers.js";
|
|
8
|
-
import { ready,
|
|
8
|
+
import { ready, setupError, setReady, log, embed, getEmbeddingConfig, qdrantReq, ensureCollectionsAll, qdrantUpsert, qdrantSearch, qdrantScroll, qdrantSetPayload, qdrantGetPoints, rebuildPool, hasPool, listDestinations, resolveDest, findPointById, } from "./api.js";
|
|
9
|
+
import { DestinationNotFoundError } from "../routing.js";
|
|
9
10
|
import { saveConfig, loadConfig, EXTRACTION_HEALTH_PATH } from "../config.js";
|
|
10
11
|
import { existsSync, readFileSync } from "node:fs";
|
|
11
12
|
import { inspectWatcherPaths, formatIssue, repairSuspiciousWatcherPaths } from "../daemon/watcher-health.js";
|
|
@@ -28,32 +29,48 @@ function nowISO() {
|
|
|
28
29
|
function newId() {
|
|
29
30
|
return crypto.randomUUID();
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|| process.env.BIKKY_WORKSPACE?.trim()
|
|
34
|
-
|| loadConfig().default_workspace?.trim()
|
|
35
|
-
|| undefined;
|
|
36
|
-
// The literal "default" workspace also includes legacy facts that have no
|
|
37
|
-
// workspace_id payload (pre-migration data). Any other named workspace stays
|
|
38
|
-
// strict. An explicit includeLegacyWorkspace=true from the caller still wins.
|
|
39
|
-
const isDefault = resolved === "default";
|
|
32
|
+
// Build a RoutingInput from the standard memory-tool fields.
|
|
33
|
+
function routingInput(args) {
|
|
40
34
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
destination: args.destination,
|
|
36
|
+
cwd: process.cwd(),
|
|
37
|
+
content: args.content,
|
|
38
|
+
entities: args.entities,
|
|
39
|
+
metadata: args.metadata,
|
|
44
40
|
};
|
|
45
41
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
42
|
+
// Resolve a destination from a routing input, returning either the destination
|
|
43
|
+
// or an MCP error result if the override is invalid / no destinations exist.
|
|
44
|
+
function resolveDestOrError(input) {
|
|
45
|
+
try {
|
|
46
|
+
return { dest: resolveDest(input) };
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
50
|
+
if (e instanceof DestinationNotFoundError) {
|
|
51
|
+
return {
|
|
52
|
+
error: {
|
|
53
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
54
|
+
status: "destination_not_found",
|
|
55
|
+
message: msg,
|
|
56
|
+
available_destinations: listDestinations().map((d) => d.name),
|
|
57
|
+
}, null, 2) }],
|
|
58
|
+
isError: true,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
error: {
|
|
64
|
+
content: [{ type: "text", text: JSON.stringify({ status: "error", message: msg }, null, 2) }],
|
|
65
|
+
isError: true,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
52
69
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const actorId = actor?.actor_id ??
|
|
70
|
+
// Add actor identity payload fields. Workspace was removed in v0.4.0 — physical
|
|
71
|
+
// separation now happens via routing destinations (see routing.ts).
|
|
72
|
+
function addActorPayload(payload, actor, actorIdOverride) {
|
|
73
|
+
const actorId = actor?.actor_id ?? normalizeActorId(actorIdOverride);
|
|
57
74
|
if (actorId)
|
|
58
75
|
payload["actor_id"] = actorId;
|
|
59
76
|
if (actor?.actor_label) {
|
|
@@ -66,19 +83,34 @@ function addWorkspacePayload(payload, scope, actor) {
|
|
|
66
83
|
payload["metadata"] = metadata;
|
|
67
84
|
}
|
|
68
85
|
}
|
|
69
|
-
async function
|
|
70
|
-
const existing = await qdrantGetPoints([factId]);
|
|
86
|
+
async function getPointForWrite(dest, factId) {
|
|
87
|
+
const existing = await qdrantGetPoints(dest, [factId]);
|
|
71
88
|
const point = existing.result?.[0];
|
|
72
89
|
if (!point) {
|
|
73
90
|
return { error: { status: "not_found", fact_id: factId } };
|
|
74
91
|
}
|
|
75
92
|
return { point };
|
|
76
93
|
}
|
|
94
|
+
// Locate which destination owns a fact ID (fan-out across pool). Used by
|
|
95
|
+
// ID-based ops where the caller doesn't know upfront which destination holds
|
|
96
|
+
// the point (memory_forget, memory_verify, memory_report_outcome, etc.).
|
|
97
|
+
async function locatePoint(factId) {
|
|
98
|
+
const found = await findPointById(factId);
|
|
99
|
+
if (!found)
|
|
100
|
+
return null;
|
|
101
|
+
return { dest: found.destination, point: found.point };
|
|
102
|
+
}
|
|
103
|
+
function notFoundResult(factId) {
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: "text", text: JSON.stringify({ status: "not_found", fact_id: factId }, null, 2) }],
|
|
106
|
+
isError: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
77
109
|
function requireReady() {
|
|
78
110
|
if (!ready) {
|
|
79
111
|
const missing = [];
|
|
80
|
-
if (!
|
|
81
|
-
missing.push("
|
|
112
|
+
if (!hasPool())
|
|
113
|
+
missing.push("destinations");
|
|
82
114
|
return {
|
|
83
115
|
content: [{
|
|
84
116
|
type: "text",
|
|
@@ -86,9 +118,6 @@ function requireReady() {
|
|
|
86
118
|
status: "setup_required",
|
|
87
119
|
ready: false,
|
|
88
120
|
missing,
|
|
89
|
-
// Surface the underlying init failure (embedding / Qdrant) when
|
|
90
|
-
// present so users see an actionable reason instead of a generic
|
|
91
|
-
// "setup required" message.
|
|
92
121
|
...(setupError ? { setup_error: setupError } : {}),
|
|
93
122
|
setup_instructions: "Memory is not configured. Run `bikky setup` or call configure_credentials:\n" +
|
|
94
123
|
"1. Go to cloud.qdrant.io → sign up (free tier: 1GB, no credit card)\n" +
|
|
@@ -126,7 +155,7 @@ function clampRecallLimit(limit) {
|
|
|
126
155
|
/**
|
|
127
156
|
* Entity-graph traversal for memory_recall.
|
|
128
157
|
*/
|
|
129
|
-
async function graphTraversal(primaryResults, limit
|
|
158
|
+
async function graphTraversal(dest, primaryResults, limit) {
|
|
130
159
|
try {
|
|
131
160
|
const primaryEntities = new Set();
|
|
132
161
|
const primaryIds = new Set();
|
|
@@ -140,16 +169,16 @@ async function graphTraversal(primaryResults, limit, scope) {
|
|
|
140
169
|
return { points: [] };
|
|
141
170
|
const relatedEntities = new Set();
|
|
142
171
|
for (const entity of primaryEntities) {
|
|
143
|
-
const outgoingFilter =
|
|
172
|
+
const outgoingFilter = buildFilter({ excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS }) ?? { must: [] };
|
|
144
173
|
outgoingFilter.must.push({ key: "from_entity", match: { value: entity } });
|
|
145
|
-
const outgoing = await qdrantScroll(outgoingFilter, 10).catch(() => ({ result: { points: [] } }));
|
|
174
|
+
const outgoing = await qdrantScroll(dest, outgoingFilter, 10).catch(() => ({ result: { points: [] } }));
|
|
146
175
|
for (const pt of (outgoing.result?.points ?? [])) {
|
|
147
176
|
if (pt.payload.to_entity)
|
|
148
177
|
relatedEntities.add(pt.payload.to_entity);
|
|
149
178
|
}
|
|
150
|
-
const incomingFilter =
|
|
179
|
+
const incomingFilter = buildFilter({ excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS }) ?? { must: [] };
|
|
151
180
|
incomingFilter.must.push({ key: "to_entity", match: { value: entity } });
|
|
152
|
-
const incoming = await qdrantScroll(incomingFilter, 10).catch(() => ({ result: { points: [] } }));
|
|
181
|
+
const incoming = await qdrantScroll(dest, incomingFilter, 10).catch(() => ({ result: { points: [] } }));
|
|
153
182
|
for (const pt of (incoming.result?.points ?? [])) {
|
|
154
183
|
if (pt.payload.from_entity)
|
|
155
184
|
relatedEntities.add(pt.payload.from_entity);
|
|
@@ -162,9 +191,9 @@ async function graphTraversal(primaryResults, limit, scope) {
|
|
|
162
191
|
const relatedFacts = [];
|
|
163
192
|
const maxPerEntity = Math.max(2, Math.floor(limit / relatedEntities.size));
|
|
164
193
|
for (const entity of relatedEntities) {
|
|
165
|
-
const filter =
|
|
194
|
+
const filter = buildFilter({ excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS }) ?? { must: [] };
|
|
166
195
|
filter.must.push({ key: "entities", match: { value: entity } });
|
|
167
|
-
const result = await qdrantScroll(filter, maxPerEntity).catch(() => ({ result: { points: [] } }));
|
|
196
|
+
const result = await qdrantScroll(dest, filter, maxPerEntity).catch(() => ({ result: { points: [] } }));
|
|
168
197
|
for (const pt of (result.result?.points ?? [])) {
|
|
169
198
|
if (!primaryIds.has(pt.id)) {
|
|
170
199
|
relatedFacts.push(pt);
|
|
@@ -189,33 +218,51 @@ export function registerTools(mcp) {
|
|
|
189
218
|
"Use this when memory tools return a 'setup_required' error, or once at session start if you're not sure bikky is wired up. Reports which credentials are missing and includes onboarding instructions if anything is incomplete.",
|
|
190
219
|
"Read-only — safe to call any time.",
|
|
191
220
|
].join(" "), {}, async () => {
|
|
192
|
-
const
|
|
193
|
-
|| loadConfig().default_workspace?.trim()
|
|
194
|
-
|| null;
|
|
221
|
+
const dests = listDestinations();
|
|
195
222
|
const status = {
|
|
196
223
|
ready,
|
|
197
|
-
|
|
198
|
-
qdrant_api_key: !!qdrantApiKey,
|
|
224
|
+
destinations_configured: dests.length,
|
|
199
225
|
missing: [],
|
|
200
|
-
qdrant_connected: false,
|
|
201
226
|
embedding_connected: false,
|
|
202
227
|
embedding_provider: getEmbeddingConfig().provider,
|
|
203
228
|
embedding_model: getEmbeddingConfig().model,
|
|
204
229
|
embedding_dimensions: getEmbeddingConfig().dimensions,
|
|
205
|
-
...(activeWorkspace ? { active_workspace: activeWorkspace } : {}),
|
|
206
230
|
...(setupError ? { setup_error: setupError } : {}),
|
|
207
231
|
};
|
|
208
232
|
const missing = status["missing"];
|
|
209
|
-
if (
|
|
210
|
-
missing.push("
|
|
211
|
-
//
|
|
212
|
-
|
|
233
|
+
if (dests.length === 0)
|
|
234
|
+
missing.push("destinations");
|
|
235
|
+
// Per-destination health
|
|
236
|
+
const destStatus = [];
|
|
237
|
+
for (const d of dests) {
|
|
238
|
+
const block = {
|
|
239
|
+
name: d.name,
|
|
240
|
+
qdrant_url_host: (() => { try {
|
|
241
|
+
return new URL(d.qdrant_url).host;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return d.qdrant_url;
|
|
245
|
+
} })(),
|
|
246
|
+
collection: d.collection,
|
|
247
|
+
default: d.default ?? false,
|
|
248
|
+
connected: false,
|
|
249
|
+
collection_exists: false,
|
|
250
|
+
};
|
|
213
251
|
try {
|
|
214
|
-
await qdrantReq("GET", "/collections");
|
|
215
|
-
|
|
252
|
+
await qdrantReq(d, "GET", "/collections");
|
|
253
|
+
block["connected"] = true;
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
block["last_error"] = e instanceof Error ? e.message : String(e);
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
await qdrantReq(d, "GET", `/collections/${d.collection}`);
|
|
260
|
+
block["collection_exists"] = true;
|
|
216
261
|
}
|
|
217
262
|
catch { /* ignore */ }
|
|
263
|
+
destStatus.push(block);
|
|
218
264
|
}
|
|
265
|
+
status["destinations"] = destStatus;
|
|
219
266
|
try {
|
|
220
267
|
await embed("test");
|
|
221
268
|
status["embedding_connected"] = true;
|
|
@@ -283,12 +330,10 @@ export function registerTools(mcp) {
|
|
|
283
330
|
if (qdrant_url) {
|
|
284
331
|
const url = qdrant_url.replace(/\/+$/, "");
|
|
285
332
|
cfg.qdrant_url = url;
|
|
286
|
-
setQdrantUrl(url);
|
|
287
333
|
results["qdrant_url"] = "stored ✓";
|
|
288
334
|
}
|
|
289
335
|
if (qdrant_api_key) {
|
|
290
336
|
cfg.qdrant_api_key = qdrant_api_key;
|
|
291
|
-
setQdrantApiKey(qdrant_api_key);
|
|
292
337
|
results["qdrant_api_key"] = "stored ✓";
|
|
293
338
|
}
|
|
294
339
|
if (openai_api_key) {
|
|
@@ -301,14 +346,22 @@ export function registerTools(mcp) {
|
|
|
301
346
|
results["watcher_path_repairs"] = watcherRepairs;
|
|
302
347
|
}
|
|
303
348
|
saveConfig(cfg);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
349
|
+
// Rebuild the destination pool from the updated config so the
|
|
350
|
+
// synthesized default destination picks up the new url/key.
|
|
351
|
+
try {
|
|
352
|
+
rebuildPool();
|
|
353
|
+
}
|
|
354
|
+
catch (e) {
|
|
355
|
+
results["pool_rebuild"] = `error: ${e instanceof Error ? e.message : String(e)}`;
|
|
356
|
+
}
|
|
357
|
+
if (hasPool()) {
|
|
358
|
+
const ensured = await ensureCollectionsAll(QDRANT_INDEXES);
|
|
359
|
+
results["destinations"] = ensured.map((r) => ({
|
|
360
|
+
name: r.destination.name,
|
|
361
|
+
collection: r.destination.collection,
|
|
362
|
+
ok: r.ok,
|
|
363
|
+
...(r.error ? { error: r.error } : {}),
|
|
364
|
+
}));
|
|
312
365
|
}
|
|
313
366
|
try {
|
|
314
367
|
await embed("memory system test");
|
|
@@ -318,7 +371,7 @@ export function registerTools(mcp) {
|
|
|
318
371
|
catch (e) {
|
|
319
372
|
results["embedding"] = `error: ${e instanceof Error ? e.message : String(e)}`;
|
|
320
373
|
}
|
|
321
|
-
setReady(
|
|
374
|
+
setReady(hasPool());
|
|
322
375
|
results["ready"] = ready;
|
|
323
376
|
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
324
377
|
});
|
|
@@ -328,21 +381,31 @@ export function registerTools(mcp) {
|
|
|
328
381
|
"Use this to debug a sudden 'setup_required' or empty-recall after a network blip or credential change. Lighter than configure_credentials — does not write to disk.",
|
|
329
382
|
"Read-only.",
|
|
330
383
|
].join(" "), {}, async () => {
|
|
331
|
-
const results = {
|
|
332
|
-
|
|
384
|
+
const results = { embedding: false };
|
|
385
|
+
const dests = listDestinations();
|
|
386
|
+
const destResults = [];
|
|
387
|
+
for (const d of dests) {
|
|
388
|
+
const block = {
|
|
389
|
+
name: d.name,
|
|
390
|
+
collection: d.collection,
|
|
391
|
+
qdrant: false,
|
|
392
|
+
collection_exists: false,
|
|
393
|
+
};
|
|
333
394
|
try {
|
|
334
|
-
await qdrantReq("GET", "/collections");
|
|
335
|
-
|
|
395
|
+
await qdrantReq(d, "GET", "/collections");
|
|
396
|
+
block["qdrant"] = true;
|
|
336
397
|
}
|
|
337
398
|
catch (e) {
|
|
338
|
-
|
|
399
|
+
block["qdrant_error"] = e instanceof Error ? e.message : String(e);
|
|
339
400
|
}
|
|
340
401
|
try {
|
|
341
|
-
await qdrantReq("GET", `/collections/${
|
|
342
|
-
|
|
402
|
+
await qdrantReq(d, "GET", `/collections/${d.collection}`);
|
|
403
|
+
block["collection_exists"] = true;
|
|
343
404
|
}
|
|
344
405
|
catch { /* ignore */ }
|
|
406
|
+
destResults.push(block);
|
|
345
407
|
}
|
|
408
|
+
results["destinations"] = destResults;
|
|
346
409
|
try {
|
|
347
410
|
await embed("connection test");
|
|
348
411
|
results["embedding"] = true;
|
|
@@ -350,7 +413,8 @@ export function registerTools(mcp) {
|
|
|
350
413
|
catch (e) {
|
|
351
414
|
results["embedding_error"] = e instanceof Error ? e.message : String(e);
|
|
352
415
|
}
|
|
353
|
-
const allReady = results["
|
|
416
|
+
const allReady = results["embedding"] === true && destResults.length > 0
|
|
417
|
+
&& destResults.every((b) => b["qdrant"] === true && b["collection_exists"] === true);
|
|
354
418
|
results["ready"] = allReady;
|
|
355
419
|
setReady(allReady);
|
|
356
420
|
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
@@ -369,7 +433,8 @@ export function registerTools(mcp) {
|
|
|
369
433
|
domain: z.enum(domainValues()).default(DEFAULT_DOMAIN).describe(domainEnumDescription()),
|
|
370
434
|
kind: z.enum(kindValues()).default(DEFAULT_KIND).describe(kindEnumDescription()),
|
|
371
435
|
memory_subtype: z.enum(memorySubtypeValues()).optional().describe(memorySubtypeEnumDescription()),
|
|
372
|
-
workspace_id: z.string().optional().describe("
|
|
436
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op. Routing now uses destinations — see destination."),
|
|
437
|
+
destination: z.string().optional().describe("Optional destination override. When set, routes to that destination by name. Hard-errors if no such destination exists. Omit to let routing rules in ~/.bikky/config.json decide based on cwd/entities/content/metadata."),
|
|
373
438
|
actor_id: z.string().optional().describe("Stable actor/person/agent identity associated with this capture. Overrides identity config/env/Git-derived fallback for this write."),
|
|
374
439
|
episode_id: z.string().optional().describe("Coherent activity-segment ID. Group facts captured during the same coherent task or transcript."),
|
|
375
440
|
workstream_key: z.string().optional().describe("Durable continuity key for a long-running objective (survives across sessions)."),
|
|
@@ -387,13 +452,21 @@ export function registerTools(mcp) {
|
|
|
387
452
|
to: z.string().describe("Target entity (lowercase)."),
|
|
388
453
|
}).optional().describe("Optional typed edge between two entities — created in the same call. Use this whenever the fact also expresses a relationship; no separate tool call needed."),
|
|
389
454
|
metadata: z.record(z.string(), z.string()).optional().describe("Arbitrary key-value metadata. Stored with the fact and exact-match filterable via memory_recall.metadata_filter (all key/value pairs must match — AND logic)."),
|
|
390
|
-
}, async ({ content, category, entities, domain, kind, memory_subtype, workspace_id, actor_id, episode_id, workstream_key, task_key, repo, branch, review_status, source, confidence, importance, supersedes, relation, metadata, }) => {
|
|
455
|
+
}, async ({ content, category, entities, domain, kind, memory_subtype, workspace_id: _workspace_id, destination, actor_id, episode_id, workstream_key, task_key, repo, branch, review_status, source, confidence, importance, supersedes, relation, metadata, }) => {
|
|
391
456
|
const guard = requireReady();
|
|
392
457
|
if (guard)
|
|
393
458
|
return guard;
|
|
394
459
|
lastStoreTime = Date.now();
|
|
395
460
|
const now = nowISO();
|
|
396
|
-
const
|
|
461
|
+
const resolved = resolveDestOrError(routingInput({
|
|
462
|
+
destination,
|
|
463
|
+
content,
|
|
464
|
+
entities,
|
|
465
|
+
metadata,
|
|
466
|
+
}));
|
|
467
|
+
if (resolved.error)
|
|
468
|
+
return resolved.error;
|
|
469
|
+
const dest = resolved.dest;
|
|
397
470
|
const actor = resolveActorIdentity({ actorId: actor_id, config: loadConfig() });
|
|
398
471
|
const normalizedKind = normalizeKind(kind);
|
|
399
472
|
let normalizedSubtype = null;
|
|
@@ -439,14 +512,14 @@ export function registerTools(mcp) {
|
|
|
439
512
|
} : null;
|
|
440
513
|
// 1. Exact dedup via content hash
|
|
441
514
|
try {
|
|
442
|
-
const hashFilter =
|
|
515
|
+
const hashFilter = buildFilter({}) ?? { must: [] };
|
|
443
516
|
hashFilter.must.push({ key: "content_hash", match: { value: hash } });
|
|
444
|
-
const existing = await qdrantScroll(hashFilter, 1);
|
|
517
|
+
const existing = await qdrantScroll(dest, hashFilter, 1);
|
|
445
518
|
const existingPoint = existing.result?.points?.[0];
|
|
446
519
|
if (existingPoint) {
|
|
447
520
|
const point = existingPoint;
|
|
448
521
|
const count = (point.payload.reinforcement_count || 1) + 1;
|
|
449
|
-
await qdrantSetPayload([point.id], {
|
|
522
|
+
await qdrantSetPayload(dest, [point.id], {
|
|
450
523
|
reinforcement_count: count,
|
|
451
524
|
last_reinforced_at: now,
|
|
452
525
|
updated_at: now,
|
|
@@ -470,18 +543,18 @@ export function registerTools(mcp) {
|
|
|
470
543
|
let similarFacts = [];
|
|
471
544
|
let potentialConflicts = [];
|
|
472
545
|
try {
|
|
473
|
-
const filter =
|
|
546
|
+
const filter = buildFilter({}) ?? { must: [] };
|
|
474
547
|
if (normalizedEntities.length > 0) {
|
|
475
548
|
filter.must.push({ key: "entities", match: { any: normalizedEntities } });
|
|
476
549
|
}
|
|
477
|
-
const results = await qdrantSearch(vector, filter, 3);
|
|
550
|
+
const results = await qdrantSearch(dest, vector, filter, 3);
|
|
478
551
|
const firstResult = results.result?.[0];
|
|
479
552
|
if (results.result?.length > 0 && firstResult) {
|
|
480
553
|
const topScore = firstResult.score ?? 0;
|
|
481
554
|
if (topScore > THRESHOLD_DUPLICATE) {
|
|
482
555
|
const point = firstResult;
|
|
483
556
|
const count = (point.payload.reinforcement_count || 1) + 1;
|
|
484
|
-
await qdrantSetPayload([point.id], {
|
|
557
|
+
await qdrantSetPayload(dest, [point.id], {
|
|
485
558
|
reinforcement_count: count,
|
|
486
559
|
last_reinforced_at: now,
|
|
487
560
|
updated_at: now,
|
|
@@ -531,11 +604,11 @@ export function registerTools(mcp) {
|
|
|
531
604
|
// 5. Supersede old fact if requested
|
|
532
605
|
if (supersedes) {
|
|
533
606
|
try {
|
|
534
|
-
const existing = await
|
|
607
|
+
const existing = await getPointForWrite(dest, supersedes);
|
|
535
608
|
if (existing.error) {
|
|
536
609
|
return { content: [{ type: "text", text: JSON.stringify(existing.error, null, 2) }], isError: true };
|
|
537
610
|
}
|
|
538
|
-
await qdrantSetPayload([supersedes], {
|
|
611
|
+
await qdrantSetPayload(dest, [supersedes], {
|
|
539
612
|
superseded_by: factId,
|
|
540
613
|
superseded_at: now,
|
|
541
614
|
});
|
|
@@ -583,9 +656,9 @@ export function registerTools(mcp) {
|
|
|
583
656
|
}
|
|
584
657
|
if (review_status)
|
|
585
658
|
payload["review_status"] = review_status;
|
|
586
|
-
|
|
659
|
+
addActorPayload(payload, actor);
|
|
587
660
|
addRedactionPayload(payload, factRedactionSummary);
|
|
588
|
-
await qdrantUpsert(factId, vector, payload);
|
|
661
|
+
await qdrantUpsert(dest, factId, vector, payload);
|
|
589
662
|
// 7. Insert relation point if provided
|
|
590
663
|
let relationId = null;
|
|
591
664
|
if (sanitizedRelation) {
|
|
@@ -612,14 +685,14 @@ export function registerTools(mcp) {
|
|
|
612
685
|
relation_type: sanitizedRelation.type.toLowerCase(),
|
|
613
686
|
to_entity: sanitizedRelation.to.toLowerCase(),
|
|
614
687
|
};
|
|
615
|
-
|
|
688
|
+
addActorPayload(relPayload, actor);
|
|
616
689
|
addRedactionPayload(relPayload, relationRedactionSummary);
|
|
617
|
-
await qdrantUpsert(relationId, relVector, relPayload);
|
|
690
|
+
await qdrantUpsert(dest, relationId, relVector, relPayload);
|
|
618
691
|
}
|
|
619
692
|
const result = {
|
|
620
693
|
action: "inserted",
|
|
621
694
|
fact_id: factId,
|
|
622
|
-
|
|
695
|
+
destination: dest.name,
|
|
623
696
|
};
|
|
624
697
|
if (actor.actor_id)
|
|
625
698
|
result["actor_id"] = actor.actor_id;
|
|
@@ -653,9 +726,10 @@ export function registerTools(mcp) {
|
|
|
653
726
|
domain: z.string().optional().describe("Filter by domain activity profile (same vocabulary as memory_store.domain). Optional."),
|
|
654
727
|
kind: z.string().optional().describe("Filter by kind: fact, summary, distilled, relation. Optional. Telemetry is excluded by default."),
|
|
655
728
|
memory_subtype: z.string().optional().describe("Filter by memory subtype (must be valid for the chosen kind). Optional."),
|
|
656
|
-
workspace_id: z.string().optional().describe("
|
|
729
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
730
|
+
destination: z.string().optional().describe("Optional destination override. When set, queries that destination by name. Hard-errors if no such destination exists. Omit to let routing rules decide."),
|
|
657
731
|
actor_id: z.string().optional().describe("Filter to facts captured by or associated with this stable actor identity. Optional."),
|
|
658
|
-
include_legacy_workspace: z.boolean().optional().describe("
|
|
732
|
+
include_legacy_workspace: z.boolean().optional().describe("[Removed in v0.4.0] No-op."),
|
|
659
733
|
entity: z.string().optional().describe("Restrict to facts mentioning this entity (case-insensitive). For full entity context prefer memory_entity."),
|
|
660
734
|
episode_id: z.string().optional().describe("Filter by coherent episode ID."),
|
|
661
735
|
workstream_key: z.string().optional().describe("Filter by durable workstream key."),
|
|
@@ -669,14 +743,22 @@ export function registerTools(mcp) {
|
|
|
669
743
|
graph_depth: z.number().optional().default(0).describe("Entity-graph traversal depth. 0 = vector search only (fast, default). 1 = also surface up to ceil(limit / 2) extra 1-hop entity-related facts (slower; use when the user asks 'what's connected to X?'). In JSON output these are returned separately as related."),
|
|
670
744
|
output_format: z.enum(["text", "json"]).optional().default("text").describe("Response format. text = backward-compatible human-readable lines (default). json = parseable object with query, limit metadata, results, related, counts, and optional nudge."),
|
|
671
745
|
metadata_filter: z.record(z.string(), z.string()).optional().describe("Exact-match filter on the metadata map stored with each fact. All key/value pairs must match (AND logic)."),
|
|
672
|
-
}, async ({ query, category, domain, kind, memory_subtype, workspace_id, actor_id, include_legacy_workspace, entity, episode_id, workstream_key, task_key, repo, branch, review_status, since, until, limit, graph_depth, output_format, metadata_filter, }) => {
|
|
746
|
+
}, async ({ query, category, domain, kind, memory_subtype, workspace_id: _workspace_id, destination, actor_id, include_legacy_workspace: _include_legacy_workspace, entity, episode_id, workstream_key, task_key, repo, branch, review_status, since, until, limit, graph_depth, output_format, metadata_filter, }) => {
|
|
673
747
|
const guard = requireReady();
|
|
674
748
|
if (guard)
|
|
675
749
|
return guard;
|
|
676
750
|
const requestedLimit = limit ?? MEMORY_RECALL_DEFAULT_LIMIT;
|
|
677
751
|
const effectiveLimit = clampRecallLimit(limit);
|
|
678
752
|
const actorFilter = resolveActorIdentity({ actorId: actor_id, useGitFallback: false });
|
|
679
|
-
const
|
|
753
|
+
const resolved = resolveDestOrError(routingInput({
|
|
754
|
+
destination,
|
|
755
|
+
content: query,
|
|
756
|
+
entities: entity ? [entity] : [],
|
|
757
|
+
metadata: metadata_filter,
|
|
758
|
+
}));
|
|
759
|
+
if (resolved.error)
|
|
760
|
+
return resolved.error;
|
|
761
|
+
const dest = resolved.dest;
|
|
680
762
|
const redactedQuery = redactStorageText(query);
|
|
681
763
|
const vector = await embed(redactedQuery.text);
|
|
682
764
|
const normalizedKind = kind ? normalizeKind(kind) : undefined;
|
|
@@ -692,11 +774,12 @@ export function registerTools(mcp) {
|
|
|
692
774
|
};
|
|
693
775
|
}
|
|
694
776
|
}
|
|
695
|
-
const filter =
|
|
777
|
+
const filter = buildFilter({
|
|
696
778
|
category: category ? normalizeCategory(category) : undefined,
|
|
697
779
|
domain: domain ? normalizeDomain(domain) : undefined,
|
|
698
780
|
kind: normalizedKind,
|
|
699
781
|
memory_subtype: normalizedSubtype,
|
|
782
|
+
actor_id: actorFilter.actor_id,
|
|
700
783
|
entity,
|
|
701
784
|
episode_id,
|
|
702
785
|
workstream_key,
|
|
@@ -709,7 +792,7 @@ export function registerTools(mcp) {
|
|
|
709
792
|
metadata: metadata_filter,
|
|
710
793
|
excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS,
|
|
711
794
|
});
|
|
712
|
-
const results = await qdrantSearch(vector, filter, effectiveLimit * 2);
|
|
795
|
+
const results = await qdrantSearch(dest, vector, filter, effectiveLimit * 2);
|
|
713
796
|
if (!results.result?.length) {
|
|
714
797
|
const nudge = buildMemoryNudge();
|
|
715
798
|
if (output_format === "json") {
|
|
@@ -738,7 +821,7 @@ export function registerTools(mcp) {
|
|
|
738
821
|
const lines = ranked.map((r) => formatFact(r));
|
|
739
822
|
let related = { points: [] };
|
|
740
823
|
if ((graph_depth ?? 0) >= 1) {
|
|
741
|
-
related = await graphTraversal(ranked, effectiveLimit
|
|
824
|
+
related = await graphTraversal(dest, ranked, effectiveLimit);
|
|
742
825
|
if (related.points.length > 0) {
|
|
743
826
|
lines.push("", "── Related (1-hop) ──");
|
|
744
827
|
lines.push(...related.points.map((r) => formatFact(r)));
|
|
@@ -777,21 +860,25 @@ export function registerTools(mcp) {
|
|
|
777
860
|
].join(" "), {
|
|
778
861
|
name: z.string().describe("Entity name (case-insensitive, e.g. 'qdrant', 'workspace_id'). Should match the lowercase canonical form used when facts were stored."),
|
|
779
862
|
limit: z.number().optional().default(20).describe("Max facts to return (default 20). Relations are always returned in full, capped at 50 each direction."),
|
|
780
|
-
workspace_id: z.string().optional().describe("
|
|
781
|
-
|
|
782
|
-
|
|
863
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
864
|
+
destination: z.string().optional().describe("Optional destination override. Omit to let routing rules decide."),
|
|
865
|
+
include_legacy_workspace: z.boolean().optional().describe("[Removed in v0.4.0] No-op."),
|
|
866
|
+
}, async ({ name, limit, workspace_id: _workspace_id, destination, include_legacy_workspace: _include_legacy_workspace }) => {
|
|
783
867
|
const guard = requireReady();
|
|
784
868
|
if (guard)
|
|
785
869
|
return guard;
|
|
786
870
|
const entityName = name.toLowerCase();
|
|
787
|
-
const
|
|
871
|
+
const resolved = resolveDestOrError(routingInput({ destination, entities: [entityName] }));
|
|
872
|
+
if (resolved.error)
|
|
873
|
+
return resolved.error;
|
|
874
|
+
const dest = resolved.dest;
|
|
788
875
|
// Look up the daemon-classified entity type, if any.
|
|
789
876
|
let entityType = null;
|
|
790
877
|
try {
|
|
791
|
-
const typeFilter =
|
|
878
|
+
const typeFilter = buildFilter({}) ?? { must: [] };
|
|
792
879
|
typeFilter.must.push({ key: "kind", match: { value: "entity_type" } });
|
|
793
880
|
typeFilter.must.push({ key: "entity_name", match: { value: entityName } });
|
|
794
|
-
const typePoints = await qdrantScroll(typeFilter, 1);
|
|
881
|
+
const typePoints = await qdrantScroll(dest, typeFilter, 1);
|
|
795
882
|
const typePoint = typePoints.result?.points?.[0];
|
|
796
883
|
const payload = typePoint?.payload;
|
|
797
884
|
if (payload?.entity_type) {
|
|
@@ -801,15 +888,15 @@ export function registerTools(mcp) {
|
|
|
801
888
|
catch {
|
|
802
889
|
// Type lookup is best-effort — never fails the request.
|
|
803
890
|
}
|
|
804
|
-
const factsFilter =
|
|
891
|
+
const factsFilter = buildFilter({}) ?? { must: [] };
|
|
805
892
|
factsFilter.must.push({ key: "entities", match: { value: entityName } });
|
|
806
|
-
const facts = await qdrantScroll(factsFilter, limit ?? 20);
|
|
807
|
-
const fromFilter =
|
|
893
|
+
const facts = await qdrantScroll(dest, factsFilter, limit ?? 20);
|
|
894
|
+
const fromFilter = buildFilter({}) ?? { must: [] };
|
|
808
895
|
fromFilter.must.push({ key: "from_entity", match: { value: entityName } });
|
|
809
|
-
const relationsFrom = await qdrantScroll(fromFilter, 50);
|
|
810
|
-
const toFilter =
|
|
896
|
+
const relationsFrom = await qdrantScroll(dest, fromFilter, 50);
|
|
897
|
+
const toFilter = buildFilter({}) ?? { must: [] };
|
|
811
898
|
toFilter.must.push({ key: "to_entity", match: { value: entityName } });
|
|
812
|
-
const relationsTo = await qdrantScroll(toFilter, 50);
|
|
899
|
+
const relationsTo = await qdrantScroll(dest, toFilter, 50);
|
|
813
900
|
const output = [];
|
|
814
901
|
const factPoints = facts.result?.points ?? [];
|
|
815
902
|
if (factPoints.length > 0) {
|
|
@@ -858,31 +945,35 @@ export function registerTools(mcp) {
|
|
|
858
945
|
entity: z.string().describe("Entity name to query (case-insensitive)."),
|
|
859
946
|
relation_type: z.string().optional().describe("Filter to a specific edge label (e.g. 'owns', 'uses', 'decided', 'prefers', 'works-on'). Optional."),
|
|
860
947
|
direction: z.enum(["from", "to", "both"]).optional().default("both").describe("Which side of the edge the entity is on. 'from' = entity is the source (X --[?]--> ?). 'to' = entity is the target (? --[?]--> X). 'both' = either (default)."),
|
|
861
|
-
workspace_id: z.string().optional().describe("
|
|
862
|
-
|
|
863
|
-
|
|
948
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
949
|
+
destination: z.string().optional().describe("Optional destination override. Omit to let routing rules decide."),
|
|
950
|
+
include_legacy_workspace: z.boolean().optional().describe("[Removed in v0.4.0] No-op."),
|
|
951
|
+
}, async ({ entity, relation_type, direction, workspace_id: _workspace_id, destination, include_legacy_workspace: _include_legacy_workspace }) => {
|
|
864
952
|
const guard = requireReady();
|
|
865
953
|
if (guard)
|
|
866
954
|
return guard;
|
|
867
955
|
const entityName = entity.toLowerCase();
|
|
868
|
-
const
|
|
956
|
+
const resolved = resolveDestOrError(routingInput({ destination, entities: [entityName] }));
|
|
957
|
+
if (resolved.error)
|
|
958
|
+
return resolved.error;
|
|
959
|
+
const dest = resolved.dest;
|
|
869
960
|
const results = [];
|
|
870
961
|
if (direction === "from" || direction === "both") {
|
|
871
|
-
const filter =
|
|
962
|
+
const filter = buildFilter({}) ?? { must: [] };
|
|
872
963
|
filter.must.push({ key: "from_entity", match: { value: entityName } });
|
|
873
964
|
if (relation_type) {
|
|
874
965
|
filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
|
|
875
966
|
}
|
|
876
|
-
const r = await qdrantScroll(filter, 50);
|
|
967
|
+
const r = await qdrantScroll(dest, filter, 50);
|
|
877
968
|
results.push(...(r.result?.points ?? []));
|
|
878
969
|
}
|
|
879
970
|
if (direction === "to" || direction === "both") {
|
|
880
|
-
const filter =
|
|
971
|
+
const filter = buildFilter({}) ?? { must: [] };
|
|
881
972
|
filter.must.push({ key: "to_entity", match: { value: entityName } });
|
|
882
973
|
if (relation_type) {
|
|
883
974
|
filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
|
|
884
975
|
}
|
|
885
|
-
const r = await qdrantScroll(filter, 50);
|
|
976
|
+
const r = await qdrantScroll(dest, filter, 50);
|
|
886
977
|
results.push(...(r.result?.points ?? []));
|
|
887
978
|
}
|
|
888
979
|
const seen = new Set();
|
|
@@ -908,21 +999,19 @@ export function registerTools(mcp) {
|
|
|
908
999
|
].join(" "), {
|
|
909
1000
|
fact_id: z.string().describe("ID of the fact to forget (returned by memory_store / memory_recall as 'id')."),
|
|
910
1001
|
reason: z.string().describe("Short human-readable reason this fact is being retired (stored in 'superseded_by' for future audit)."),
|
|
911
|
-
workspace_id: z.string().optional().describe("
|
|
912
|
-
}, async ({ fact_id, reason, workspace_id }) => {
|
|
1002
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1003
|
+
}, async ({ fact_id, reason, workspace_id: _workspace_id }) => {
|
|
913
1004
|
const guard = requireReady();
|
|
914
1005
|
if (guard)
|
|
915
1006
|
return guard;
|
|
916
1007
|
const now = nowISO();
|
|
917
1008
|
try {
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
return { content: [{ type: "text", text: JSON.stringify(existing.error, null, 2) }], isError: true };
|
|
923
|
-
}
|
|
1009
|
+
const located = await locatePoint(fact_id);
|
|
1010
|
+
if (!located)
|
|
1011
|
+
return notFoundResult(fact_id);
|
|
1012
|
+
const { dest } = located;
|
|
924
1013
|
const redactedReason = redactStorageText(reason);
|
|
925
|
-
await qdrantSetPayload([fact_id], {
|
|
1014
|
+
await qdrantSetPayload(dest, [fact_id], {
|
|
926
1015
|
superseded_by: `forgotten:${redactedReason.text}`,
|
|
927
1016
|
superseded_at: now,
|
|
928
1017
|
updated_at: now,
|
|
@@ -937,6 +1026,7 @@ export function registerTools(mcp) {
|
|
|
937
1026
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
938
1027
|
status: "forgotten",
|
|
939
1028
|
fact_id,
|
|
1029
|
+
destination: dest.name,
|
|
940
1030
|
reason: redactedReason.text,
|
|
941
1031
|
...(redactedReason.redacted ? { redaction: redactedReason } : {}),
|
|
942
1032
|
}) }] };
|
|
@@ -952,26 +1042,20 @@ export function registerTools(mcp) {
|
|
|
952
1042
|
"If the fact is no longer true, use memory_forget or memory_store(supersedes:) instead.",
|
|
953
1043
|
].join(" "), {
|
|
954
1044
|
fact_id: z.string().describe("ID of the fact to verify (from memory_recall or memory_heartbeat)."),
|
|
955
|
-
workspace_id: z.string().optional().describe("
|
|
956
|
-
}, async ({ fact_id, workspace_id }) => {
|
|
1045
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1046
|
+
}, async ({ fact_id, workspace_id: _workspace_id }) => {
|
|
957
1047
|
const guard = requireReady();
|
|
958
1048
|
if (guard)
|
|
959
1049
|
return guard;
|
|
960
1050
|
const now = nowISO();
|
|
961
1051
|
try {
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
let currentCount = 0;
|
|
969
|
-
const existingPt = writable.point;
|
|
970
|
-
if (existingPt) {
|
|
971
|
-
currentCount = existingPt.payload.verification_count ?? 0;
|
|
972
|
-
}
|
|
1052
|
+
const located = await locatePoint(fact_id);
|
|
1053
|
+
if (!located)
|
|
1054
|
+
return notFoundResult(fact_id);
|
|
1055
|
+
const { dest, point } = located;
|
|
1056
|
+
const currentCount = point.payload.verification_count ?? 0;
|
|
973
1057
|
const newCount = currentCount + 1;
|
|
974
|
-
await qdrantSetPayload([fact_id], {
|
|
1058
|
+
await qdrantSetPayload(dest, [fact_id], {
|
|
975
1059
|
last_verified_at: now,
|
|
976
1060
|
last_reinforced_at: now,
|
|
977
1061
|
verification_count: newCount,
|
|
@@ -981,6 +1065,7 @@ export function registerTools(mcp) {
|
|
|
981
1065
|
content: [{ type: "text", text: JSON.stringify({
|
|
982
1066
|
status: "verified",
|
|
983
1067
|
fact_id,
|
|
1068
|
+
destination: dest.name,
|
|
984
1069
|
verification_count: newCount,
|
|
985
1070
|
message: "Fact confirmed as still accurate. Staleness clock reset.",
|
|
986
1071
|
}) }],
|
|
@@ -998,23 +1083,21 @@ export function registerTools(mcp) {
|
|
|
998
1083
|
].join(" "), {
|
|
999
1084
|
fact_id: z.string().describe("ID of the fact that was useful (from memory_recall or memory_entity)."),
|
|
1000
1085
|
note: z.string().optional().describe("Optional short note about how the fact was useful (e.g. 'unblocked auth debug'). Stored on the telemetry event for future analysis."),
|
|
1001
|
-
workspace_id: z.string().optional().describe("
|
|
1002
|
-
}, async ({ fact_id, note, workspace_id }) => {
|
|
1086
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1087
|
+
}, async ({ fact_id, note, workspace_id: _workspace_id }) => {
|
|
1003
1088
|
const guard = requireReady();
|
|
1004
1089
|
if (guard)
|
|
1005
1090
|
return guard;
|
|
1006
1091
|
const now = nowISO();
|
|
1007
1092
|
try {
|
|
1008
|
-
const
|
|
1093
|
+
const located = await locatePoint(fact_id);
|
|
1094
|
+
if (!located)
|
|
1095
|
+
return notFoundResult(fact_id);
|
|
1096
|
+
const { dest, point } = located;
|
|
1009
1097
|
const actor = resolveActorIdentity({ config: loadConfig() });
|
|
1010
|
-
const
|
|
1011
|
-
if (writable.error) {
|
|
1012
|
-
return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
|
|
1013
|
-
}
|
|
1014
|
-
const existingPt = writable.point;
|
|
1015
|
-
const currentCount = existingPt?.payload.useful_count ?? 0;
|
|
1098
|
+
const currentCount = point.payload.useful_count ?? 0;
|
|
1016
1099
|
const newCount = currentCount + 1;
|
|
1017
|
-
await qdrantSetPayload([fact_id], {
|
|
1100
|
+
await qdrantSetPayload(dest, [fact_id], {
|
|
1018
1101
|
useful_count: newCount,
|
|
1019
1102
|
last_useful_at: now,
|
|
1020
1103
|
updated_at: now,
|
|
@@ -1043,11 +1126,11 @@ export function registerTools(mcp) {
|
|
|
1043
1126
|
created_at: now,
|
|
1044
1127
|
updated_at: now,
|
|
1045
1128
|
};
|
|
1046
|
-
|
|
1129
|
+
addActorPayload(eventPayload, actor);
|
|
1047
1130
|
addRedactionPayload(eventPayload, redactedEvent);
|
|
1048
1131
|
try {
|
|
1049
1132
|
const eventVector = await embed(redactedEvent.text);
|
|
1050
|
-
await qdrantUpsert(eventId, eventVector, eventPayload);
|
|
1133
|
+
await qdrantUpsert(dest, eventId, eventVector, eventPayload);
|
|
1051
1134
|
}
|
|
1052
1135
|
catch (e) {
|
|
1053
1136
|
log("WARN", `Failed to record feedback_event: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -1056,6 +1139,7 @@ export function registerTools(mcp) {
|
|
|
1056
1139
|
content: [{ type: "text", text: JSON.stringify({
|
|
1057
1140
|
status: "marked_useful",
|
|
1058
1141
|
fact_id,
|
|
1142
|
+
destination: dest.name,
|
|
1059
1143
|
useful_count: newCount,
|
|
1060
1144
|
event_id: eventId,
|
|
1061
1145
|
}) }],
|
|
@@ -1074,19 +1158,18 @@ export function registerTools(mcp) {
|
|
|
1074
1158
|
fact_id: z.string().describe("ID of the fact whose outcome you are reporting."),
|
|
1075
1159
|
outcome: z.enum(["useful", "misleading", "irrelevant", "wrong"]).describe("How the fact actually played out. 'useful' = helped you finish the task; 'misleading' = sent you the wrong way; 'irrelevant' = semantically matched but didn't help; 'wrong' = factually incorrect."),
|
|
1076
1160
|
notes: z.string().optional().describe("Optional short context for the outcome (e.g. 'API moved in v2', 'wrong port number'). Stored on the telemetry event for future analysis."),
|
|
1077
|
-
workspace_id: z.string().optional().describe("
|
|
1078
|
-
}, async ({ fact_id, outcome, notes, workspace_id }) => {
|
|
1161
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1162
|
+
}, async ({ fact_id, outcome, notes, workspace_id: _workspace_id }) => {
|
|
1079
1163
|
const guard = requireReady();
|
|
1080
1164
|
if (guard)
|
|
1081
1165
|
return guard;
|
|
1082
1166
|
const now = nowISO();
|
|
1083
1167
|
try {
|
|
1084
|
-
const
|
|
1168
|
+
const located = await locatePoint(fact_id);
|
|
1169
|
+
if (!located)
|
|
1170
|
+
return notFoundResult(fact_id);
|
|
1171
|
+
const { dest } = located;
|
|
1085
1172
|
const actor = resolveActorIdentity({ config: loadConfig() });
|
|
1086
|
-
const target = await getPointForWorkspaceWrite(fact_id, scope);
|
|
1087
|
-
if (target.error) {
|
|
1088
|
-
return { content: [{ type: "text", text: JSON.stringify(target.error, null, 2) }], isError: true };
|
|
1089
|
-
}
|
|
1090
1173
|
const eventId = newId();
|
|
1091
1174
|
const eventContent = notes
|
|
1092
1175
|
? `Fact ${fact_id} outcome=${outcome}: ${notes}`
|
|
@@ -1109,14 +1192,15 @@ export function registerTools(mcp) {
|
|
|
1109
1192
|
created_at: now,
|
|
1110
1193
|
updated_at: now,
|
|
1111
1194
|
};
|
|
1112
|
-
|
|
1195
|
+
addActorPayload(eventPayload, actor);
|
|
1113
1196
|
addRedactionPayload(eventPayload, redactedEvent);
|
|
1114
1197
|
const eventVector = await embed(redactedEvent.text);
|
|
1115
|
-
await qdrantUpsert(eventId, eventVector, eventPayload);
|
|
1198
|
+
await qdrantUpsert(dest, eventId, eventVector, eventPayload);
|
|
1116
1199
|
return {
|
|
1117
1200
|
content: [{ type: "text", text: JSON.stringify({
|
|
1118
1201
|
status: "outcome_recorded",
|
|
1119
1202
|
fact_id,
|
|
1203
|
+
destination: dest.name,
|
|
1120
1204
|
outcome,
|
|
1121
1205
|
event_id: eventId,
|
|
1122
1206
|
}) }],
|
|
@@ -1138,16 +1222,24 @@ export function registerTools(mcp) {
|
|
|
1138
1222
|
workstream_key: z.string().optional().describe("Durable continuity key for a long-running objective (survives across sessions)."),
|
|
1139
1223
|
task_key: z.string().optional().describe("Task or issue key (e.g. GitHub issue number, JIRA key)."),
|
|
1140
1224
|
repo: z.string().optional().describe("Repository or project surface this summary relates to."),
|
|
1141
|
-
workspace_id: z.string().optional().describe("
|
|
1225
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1226
|
+
destination: z.string().optional().describe("Optional destination override. Omit to let routing rules decide."),
|
|
1142
1227
|
actor_id: z.string().optional().describe("Stable actor identity associated with this session summary. Overrides identity config/env/Git fallback."),
|
|
1143
|
-
}, async ({ content, entities, episode_id, workstream_key, task_key, repo, workspace_id, actor_id }) => {
|
|
1228
|
+
}, async ({ content, entities, episode_id, workstream_key, task_key, repo, workspace_id: _workspace_id, destination, actor_id }) => {
|
|
1144
1229
|
const guard = requireReady();
|
|
1145
1230
|
if (guard)
|
|
1146
1231
|
return guard;
|
|
1147
1232
|
lastStoreTime = Date.now();
|
|
1148
1233
|
const now = nowISO();
|
|
1149
1234
|
try {
|
|
1150
|
-
const
|
|
1235
|
+
const resolved = resolveDestOrError(routingInput({
|
|
1236
|
+
destination,
|
|
1237
|
+
content,
|
|
1238
|
+
entities: entities ?? [],
|
|
1239
|
+
}));
|
|
1240
|
+
if (resolved.error)
|
|
1241
|
+
return resolved.error;
|
|
1242
|
+
const dest = resolved.dest;
|
|
1151
1243
|
const actor = resolveActorIdentity({ actorId: actor_id, config: loadConfig() });
|
|
1152
1244
|
const normalizedEntities = (entities ?? []).map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
1153
1245
|
const summaryId = newId();
|
|
@@ -1180,14 +1272,14 @@ export function registerTools(mcp) {
|
|
|
1180
1272
|
payload["task_key"] = task_key;
|
|
1181
1273
|
if (repo)
|
|
1182
1274
|
payload["repo"] = repo;
|
|
1183
|
-
|
|
1275
|
+
addActorPayload(payload, actor);
|
|
1184
1276
|
addRedactionPayload(payload, redactedContent);
|
|
1185
|
-
await qdrantUpsert(summaryId, vector, payload);
|
|
1277
|
+
await qdrantUpsert(dest, summaryId, vector, payload);
|
|
1186
1278
|
return {
|
|
1187
1279
|
content: [{ type: "text", text: JSON.stringify({
|
|
1188
1280
|
status: "summary_stored",
|
|
1189
1281
|
summary_id: summaryId,
|
|
1190
|
-
|
|
1282
|
+
destination: dest.name,
|
|
1191
1283
|
actor_id: actor.actor_id,
|
|
1192
1284
|
}) }],
|
|
1193
1285
|
};
|
|
@@ -1207,27 +1299,35 @@ export function registerTools(mcp) {
|
|
|
1207
1299
|
supersedes: z.string().optional().describe("ID of an earlier distilled fact that this one replaces. Old fact is marked superseded and excluded from recall."),
|
|
1208
1300
|
task_key: z.string().optional().describe("Task or issue key associated with this learning, if relevant."),
|
|
1209
1301
|
repo: z.string().optional().describe("Repository or project surface this learning applies to."),
|
|
1210
|
-
workspace_id: z.string().optional().describe("
|
|
1302
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1303
|
+
destination: z.string().optional().describe("Optional destination override. Omit to let routing rules decide."),
|
|
1211
1304
|
actor_id: z.string().optional().describe("Stable actor identity associated with this distillation. Overrides identity config/env/Git fallback."),
|
|
1212
|
-
}, async ({ content, entities, supersedes, task_key, repo, workspace_id, actor_id }) => {
|
|
1305
|
+
}, async ({ content, entities, supersedes, task_key, repo, workspace_id: _workspace_id, destination, actor_id }) => {
|
|
1213
1306
|
const guard = requireReady();
|
|
1214
1307
|
if (guard)
|
|
1215
1308
|
return guard;
|
|
1216
1309
|
lastStoreTime = Date.now();
|
|
1217
1310
|
const now = nowISO();
|
|
1218
1311
|
try {
|
|
1219
|
-
const
|
|
1312
|
+
const resolved = resolveDestOrError(routingInput({
|
|
1313
|
+
destination,
|
|
1314
|
+
content,
|
|
1315
|
+
entities,
|
|
1316
|
+
}));
|
|
1317
|
+
if (resolved.error)
|
|
1318
|
+
return resolved.error;
|
|
1319
|
+
const dest = resolved.dest;
|
|
1220
1320
|
const actor = resolveActorIdentity({ actorId: actor_id, config: loadConfig() });
|
|
1221
1321
|
const normalizedEntities = entities.map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
1222
1322
|
const distilledId = newId();
|
|
1223
1323
|
const redactedContent = redactStorageText(content);
|
|
1224
1324
|
const vector = await embed(redactedContent.text);
|
|
1225
1325
|
if (supersedes) {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
await qdrantSetPayload([supersedes], {
|
|
1326
|
+
// Supersede may live in a different destination — locate it.
|
|
1327
|
+
const located = await locatePoint(supersedes);
|
|
1328
|
+
if (!located)
|
|
1329
|
+
return notFoundResult(supersedes);
|
|
1330
|
+
await qdrantSetPayload(located.dest, [supersedes], {
|
|
1231
1331
|
superseded_by: distilledId,
|
|
1232
1332
|
superseded_at: now,
|
|
1233
1333
|
});
|
|
@@ -1255,15 +1355,15 @@ export function registerTools(mcp) {
|
|
|
1255
1355
|
payload["task_key"] = task_key;
|
|
1256
1356
|
if (repo)
|
|
1257
1357
|
payload["repo"] = repo;
|
|
1258
|
-
|
|
1358
|
+
addActorPayload(payload, actor);
|
|
1259
1359
|
addRedactionPayload(payload, redactedContent);
|
|
1260
|
-
await qdrantUpsert(distilledId, vector, payload);
|
|
1360
|
+
await qdrantUpsert(dest, distilledId, vector, payload);
|
|
1261
1361
|
return {
|
|
1262
1362
|
content: [{ type: "text", text: JSON.stringify({
|
|
1263
1363
|
status: "distilled_stored",
|
|
1264
1364
|
distilled_id: distilledId,
|
|
1365
|
+
destination: dest.name,
|
|
1265
1366
|
supersedes: supersedes ?? null,
|
|
1266
|
-
workspace_id: scope.workspaceId,
|
|
1267
1367
|
actor_id: actor.actor_id,
|
|
1268
1368
|
}) }],
|
|
1269
1369
|
};
|
|
@@ -1282,26 +1382,37 @@ export function registerTools(mcp) {
|
|
|
1282
1382
|
fact_id: z.string().optional().describe("Fact ID to act on. Required for approve / reject / correct."),
|
|
1283
1383
|
reason: z.string().optional().describe("Required for action=reject. Short reason the fact is wrong."),
|
|
1284
1384
|
corrected_content: z.string().optional().describe("Required for action=correct. The fixed fact text. Stored as a new fact that supersedes the original."),
|
|
1285
|
-
workspace_id: z.string().optional().describe("
|
|
1286
|
-
include_legacy_workspace: z.boolean().optional().describe("
|
|
1287
|
-
}, async ({ limit, action, fact_id, reason, corrected_content, workspace_id, include_legacy_workspace }) => {
|
|
1385
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1386
|
+
include_legacy_workspace: z.boolean().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1387
|
+
}, async ({ limit, action, fact_id, reason, corrected_content, workspace_id: _workspace_id, include_legacy_workspace: _include_legacy_workspace }) => {
|
|
1288
1388
|
const guard = requireReady();
|
|
1289
1389
|
if (guard)
|
|
1290
1390
|
return guard;
|
|
1291
|
-
const scope = resolveScope(workspace_id, include_legacy_workspace);
|
|
1292
1391
|
if (action === "list") {
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1392
|
+
// List spans all destinations.
|
|
1393
|
+
const destinations = listDestinations();
|
|
1394
|
+
const allPoints = [];
|
|
1395
|
+
const filter = { must: [{ key: "source", match: { any: ["system", "daemon"] } }] };
|
|
1396
|
+
for (const dest of destinations) {
|
|
1397
|
+
try {
|
|
1398
|
+
const result = await qdrantScroll(dest, filter, (limit ?? 10) * 2);
|
|
1399
|
+
const points = result.result?.points ?? [];
|
|
1400
|
+
for (const pt of points)
|
|
1401
|
+
allPoints.push({ dest: dest.name, point: pt });
|
|
1402
|
+
}
|
|
1403
|
+
catch (e) {
|
|
1404
|
+
log("WARN", `memory_review list scroll failed on ${dest.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
const sorted = allPoints
|
|
1408
|
+
.sort((a, b) => (b.point.payload.created_at ?? "").localeCompare(a.point.payload.created_at ?? ""))
|
|
1298
1409
|
.slice(0, limit ?? 10);
|
|
1299
|
-
if (
|
|
1410
|
+
if (sorted.length === 0) {
|
|
1300
1411
|
return { content: [{ type: "text", text: "No system-captured facts found." }] };
|
|
1301
1412
|
}
|
|
1302
|
-
const lines =
|
|
1413
|
+
const lines = sorted.map(({ dest, point: pt }) => {
|
|
1303
1414
|
const p = pt.payload;
|
|
1304
|
-
return `[${p.category}] ${p.content}\n id: ${pt.id} | confidence: ${p.confidence} | importance: ${p.importance} | entities: ${(p.entities ?? []).join(", ")} | created: ${p.created_at}`;
|
|
1415
|
+
return `[${p.category}] ${p.content}\n id: ${pt.id} | dest: ${dest} | confidence: ${p.confidence} | importance: ${p.importance} | entities: ${(p.entities ?? []).join(", ")} | created: ${p.created_at}`;
|
|
1305
1416
|
});
|
|
1306
1417
|
return { content: [{ type: "text", text: lines.join("\n\n") }] };
|
|
1307
1418
|
}
|
|
@@ -1310,32 +1421,28 @@ export function registerTools(mcp) {
|
|
|
1310
1421
|
}
|
|
1311
1422
|
const now = nowISO();
|
|
1312
1423
|
if (action === "approve") {
|
|
1313
|
-
const
|
|
1314
|
-
if (
|
|
1315
|
-
return
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
if (approvePt) {
|
|
1320
|
-
currentCount = approvePt.payload.verification_count ?? 0;
|
|
1321
|
-
}
|
|
1322
|
-
await qdrantSetPayload([fact_id], {
|
|
1424
|
+
const located = await locatePoint(fact_id);
|
|
1425
|
+
if (!located)
|
|
1426
|
+
return notFoundResult(fact_id);
|
|
1427
|
+
const { dest, point } = located;
|
|
1428
|
+
const currentCount = point.payload.verification_count ?? 0;
|
|
1429
|
+
await qdrantSetPayload(dest, [fact_id], {
|
|
1323
1430
|
last_verified_at: now,
|
|
1324
1431
|
verification_count: currentCount + 1,
|
|
1325
1432
|
updated_at: now,
|
|
1326
1433
|
});
|
|
1327
|
-
return { content: [{ type: "text", text: JSON.stringify({ status: "approved", fact_id }) }] };
|
|
1434
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "approved", fact_id, destination: dest.name }) }] };
|
|
1328
1435
|
}
|
|
1329
1436
|
if (action === "reject") {
|
|
1330
1437
|
if (!reason) {
|
|
1331
1438
|
return { content: [{ type: "text", text: "Error: reason is required for reject action." }] };
|
|
1332
1439
|
}
|
|
1333
|
-
const
|
|
1334
|
-
if (
|
|
1335
|
-
return
|
|
1336
|
-
}
|
|
1440
|
+
const located = await locatePoint(fact_id);
|
|
1441
|
+
if (!located)
|
|
1442
|
+
return notFoundResult(fact_id);
|
|
1443
|
+
const { dest } = located;
|
|
1337
1444
|
const redactedReason = redactStorageText(reason);
|
|
1338
|
-
await qdrantSetPayload([fact_id], {
|
|
1445
|
+
await qdrantSetPayload(dest, [fact_id], {
|
|
1339
1446
|
superseded_by: `rejected:${redactedReason.text}`,
|
|
1340
1447
|
superseded_at: now,
|
|
1341
1448
|
updated_at: now,
|
|
@@ -1343,6 +1450,7 @@ export function registerTools(mcp) {
|
|
|
1343
1450
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
1344
1451
|
status: "rejected",
|
|
1345
1452
|
fact_id,
|
|
1453
|
+
destination: dest.name,
|
|
1346
1454
|
reason: redactedReason.text,
|
|
1347
1455
|
...(redactedReason.redacted ? { redaction: redactedReason } : {}),
|
|
1348
1456
|
}) }] };
|
|
@@ -1351,15 +1459,12 @@ export function registerTools(mcp) {
|
|
|
1351
1459
|
if (!corrected_content) {
|
|
1352
1460
|
return { content: [{ type: "text", text: "Error: corrected_content is required for correct action." }] };
|
|
1353
1461
|
}
|
|
1354
|
-
const
|
|
1355
|
-
if (
|
|
1356
|
-
return
|
|
1357
|
-
}
|
|
1358
|
-
const origPayload =
|
|
1462
|
+
const located = await locatePoint(fact_id);
|
|
1463
|
+
if (!located)
|
|
1464
|
+
return notFoundResult(fact_id);
|
|
1465
|
+
const { dest, point } = located;
|
|
1466
|
+
const origPayload = point.payload;
|
|
1359
1467
|
const redactedCorrected = redactStorageText(corrected_content);
|
|
1360
|
-
const correctionScope = origPayload?.workspace_id
|
|
1361
|
-
? resolveScope(origPayload.workspace_id, false)
|
|
1362
|
-
: scope;
|
|
1363
1468
|
const actor = resolveActorIdentity({ config: loadConfig() });
|
|
1364
1469
|
const vector = await embed(redactedCorrected.text);
|
|
1365
1470
|
const correctedId = crypto.randomUUID();
|
|
@@ -1390,15 +1495,15 @@ export function registerTools(mcp) {
|
|
|
1390
1495
|
updated_at: now,
|
|
1391
1496
|
metadata: { ...(origPayload?.metadata ?? {}), corrected_from: fact_id },
|
|
1392
1497
|
};
|
|
1393
|
-
|
|
1498
|
+
addActorPayload(correctedPayload, actor);
|
|
1394
1499
|
addRedactionPayload(correctedPayload, redactedCorrected);
|
|
1395
|
-
await qdrantUpsert(correctedId, vector, correctedPayload);
|
|
1396
|
-
await qdrantSetPayload([fact_id], {
|
|
1500
|
+
await qdrantUpsert(dest, correctedId, vector, correctedPayload);
|
|
1501
|
+
await qdrantSetPayload(dest, [fact_id], {
|
|
1397
1502
|
superseded_by: correctedId,
|
|
1398
1503
|
superseded_at: now,
|
|
1399
1504
|
updated_at: now,
|
|
1400
1505
|
});
|
|
1401
|
-
return { content: [{ type: "text", text: JSON.stringify({ status: "corrected", old_fact_id: fact_id, new_fact_id: correctedId }) }] };
|
|
1506
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "corrected", old_fact_id: fact_id, new_fact_id: correctedId, destination: dest.name }) }] };
|
|
1402
1507
|
}
|
|
1403
1508
|
return { content: [{ type: "text", text: `Unknown action: ${String(action)}` }] };
|
|
1404
1509
|
});
|
|
@@ -1415,8 +1520,7 @@ export function registerTools(mcp) {
|
|
|
1415
1520
|
if (heartbeatCount % 3 === 0 && ready) {
|
|
1416
1521
|
try {
|
|
1417
1522
|
const staleThreshold = new Date(Date.now() - STALENESS_DAYS * 86400000).toISOString();
|
|
1418
|
-
const
|
|
1419
|
-
const staleFilter = scopedFilter(scope) ?? { must: [] };
|
|
1523
|
+
const staleFilter = { must: [] };
|
|
1420
1524
|
staleFilter.must.push({ key: "category", match: { any: ["engineering", "product", "human", "system"] } });
|
|
1421
1525
|
staleFilter.should = [
|
|
1422
1526
|
{ key: "last_reinforced_at", range: { lte: staleThreshold } },
|
|
@@ -1425,10 +1529,24 @@ export function registerTools(mcp) {
|
|
|
1425
1529
|
staleFilter.must_not = [
|
|
1426
1530
|
{ key: "last_verified_at", range: { gte: staleThreshold } },
|
|
1427
1531
|
];
|
|
1428
|
-
|
|
1429
|
-
const staleFacts =
|
|
1430
|
-
|
|
1431
|
-
|
|
1532
|
+
// Aggregate stale facts across all destinations.
|
|
1533
|
+
const staleFacts = [];
|
|
1534
|
+
for (const dest of listDestinations()) {
|
|
1535
|
+
try {
|
|
1536
|
+
const r = await qdrantScroll(dest, staleFilter, 3);
|
|
1537
|
+
const pts = r.result?.points ?? [];
|
|
1538
|
+
for (const pt of pts)
|
|
1539
|
+
staleFacts.push(pt);
|
|
1540
|
+
if (staleFacts.length >= 3)
|
|
1541
|
+
break;
|
|
1542
|
+
}
|
|
1543
|
+
catch (e) {
|
|
1544
|
+
log("WARN", `Staleness check failed on ${dest.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
const trimmed = staleFacts.slice(0, 3);
|
|
1548
|
+
if (trimmed.length > 0) {
|
|
1549
|
+
const staleLines = trimmed.map((f) => {
|
|
1432
1550
|
const d = Math.round(daysSince(lastActivityDate(f.payload)));
|
|
1433
1551
|
return ` • [${f.payload.category}] ${f.payload.content} (${d}d old, id: ${f.id})`;
|
|
1434
1552
|
});
|