bikky 0.3.2 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -34
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -1
- package/dist/config.js.map +1 -1
- package/dist/config.test.d.ts +3 -2
- package/dist/config.test.d.ts.map +1 -1
- package/dist/config.test.js +12 -6
- package/dist/config.test.js.map +1 -1
- package/dist/daemon/capture-policy.d.ts +4 -4
- package/dist/daemon/capture-policy.d.ts.map +1 -1
- package/dist/daemon/capture-policy.js +8 -17
- package/dist/daemon/capture-policy.js.map +1 -1
- package/dist/daemon/capture-policy.test.js +2 -2
- package/dist/daemon/capture-policy.test.js.map +1 -1
- package/dist/daemon/entity-typing.d.ts +20 -0
- package/dist/daemon/entity-typing.d.ts.map +1 -0
- package/dist/daemon/entity-typing.js +166 -0
- package/dist/daemon/entity-typing.js.map +1 -0
- package/dist/daemon/episode-summary.d.ts +4 -6
- package/dist/daemon/episode-summary.d.ts.map +1 -1
- package/dist/daemon/episode-summary.js +24 -38
- package/dist/daemon/episode-summary.js.map +1 -1
- package/dist/daemon/episode-summary.test.js +5 -5
- package/dist/daemon/episode-summary.test.js.map +1 -1
- package/dist/daemon/extraction-quality.test.d.ts +2 -0
- package/dist/daemon/extraction-quality.test.d.ts.map +1 -0
- package/dist/daemon/extraction-quality.test.js +283 -0
- package/dist/daemon/extraction-quality.test.js.map +1 -0
- package/dist/daemon/extraction-rules.d.ts +131 -0
- package/dist/daemon/extraction-rules.d.ts.map +1 -0
- package/dist/daemon/extraction-rules.js +321 -0
- package/dist/daemon/extraction-rules.js.map +1 -0
- package/dist/daemon/extraction-rules.test.d.ts +2 -0
- package/dist/daemon/extraction-rules.test.d.ts.map +1 -0
- package/dist/daemon/extraction-rules.test.js +183 -0
- package/dist/daemon/extraction-rules.test.js.map +1 -0
- package/dist/daemon/extraction.d.ts +19 -1
- package/dist/daemon/extraction.d.ts.map +1 -1
- package/dist/daemon/extraction.js +169 -21
- package/dist/daemon/extraction.js.map +1 -1
- package/dist/daemon/extraction.test.js +96 -2
- package/dist/daemon/extraction.test.js.map +1 -1
- package/dist/daemon/loop.d.ts.map +1 -1
- package/dist/daemon/loop.js +14 -0
- package/dist/daemon/loop.js.map +1 -1
- package/dist/daemon/qdrant.d.ts +15 -1
- package/dist/daemon/qdrant.d.ts.map +1 -1
- package/dist/daemon/qdrant.js +45 -2
- package/dist/daemon/qdrant.js.map +1 -1
- package/dist/daemon/relations-vocab.d.ts +44 -0
- package/dist/daemon/relations-vocab.d.ts.map +1 -0
- package/dist/daemon/relations-vocab.js +168 -0
- package/dist/daemon/relations-vocab.js.map +1 -0
- package/dist/daemon/relations-vocab.test.d.ts +2 -0
- package/dist/daemon/relations-vocab.test.d.ts.map +1 -0
- package/dist/daemon/relations-vocab.test.js +69 -0
- package/dist/daemon/relations-vocab.test.js.map +1 -0
- package/dist/daemon/relations.d.ts +2 -0
- package/dist/daemon/relations.d.ts.map +1 -1
- package/dist/daemon/relations.js +15 -5
- package/dist/daemon/relations.js.map +1 -1
- package/dist/daemon/session-index.test.js +1 -1
- package/dist/daemon/session-index.test.js.map +1 -1
- package/dist/daemon/watcher-health.d.ts +20 -0
- package/dist/daemon/watcher-health.d.ts.map +1 -0
- package/dist/daemon/watcher-health.js +78 -0
- package/dist/daemon/watcher-health.js.map +1 -0
- package/dist/daemon/watcher-health.test.d.ts +5 -0
- package/dist/daemon/watcher-health.test.d.ts.map +1 -0
- package/dist/daemon/watcher-health.test.js +96 -0
- package/dist/daemon/watcher-health.test.js.map +1 -0
- package/dist/daemon/watcher.test.d.ts +3 -2
- package/dist/daemon/watcher.test.d.ts.map +1 -1
- package/dist/daemon/watcher.test.js +9 -19
- package/dist/daemon/watcher.test.js.map +1 -1
- package/dist/daemon/workstream-resolver.d.ts +76 -0
- package/dist/daemon/workstream-resolver.d.ts.map +1 -0
- package/dist/daemon/workstream-resolver.js +180 -0
- package/dist/daemon/workstream-resolver.js.map +1 -0
- package/dist/daemon/workstream-resolver.test.d.ts +2 -0
- package/dist/daemon/workstream-resolver.test.d.ts.map +1 -0
- package/dist/daemon/workstream-resolver.test.js +128 -0
- package/dist/daemon/workstream-resolver.test.js.map +1 -0
- package/dist/daemon/workstream-summary.d.ts +1 -8
- package/dist/daemon/workstream-summary.d.ts.map +1 -1
- package/dist/daemon/workstream-summary.js +4 -37
- package/dist/daemon/workstream-summary.js.map +1 -1
- package/dist/daemon/workstream-summary.test.js +4 -4
- package/dist/daemon/workstream-summary.test.js.map +1 -1
- package/dist/mcp/helpers.d.ts.map +1 -1
- package/dist/mcp/helpers.js +17 -2
- package/dist/mcp/helpers.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +8 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/taxonomy.d.ts +20 -18
- package/dist/mcp/taxonomy.d.ts.map +1 -1
- package/dist/mcp/taxonomy.js +75 -25
- package/dist/mcp/taxonomy.js.map +1 -1
- package/dist/mcp/taxonomy.test.js +10 -5
- package/dist/mcp/taxonomy.test.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +457 -93
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/tools.test.js +209 -0
- package/dist/mcp/tools.test.js.map +1 -1
- package/dist/prompts/distill.d.ts.map +1 -1
- package/dist/prompts/distill.js +36 -17
- package/dist/prompts/distill.js.map +1 -1
- package/dist/prompts/entity-typing.d.ts +18 -0
- package/dist/prompts/entity-typing.d.ts.map +1 -0
- package/dist/prompts/entity-typing.js +60 -0
- package/dist/prompts/entity-typing.js.map +1 -0
- package/dist/prompts/episode-summary.d.ts +15 -0
- package/dist/prompts/episode-summary.d.ts.map +1 -0
- package/dist/prompts/episode-summary.js +74 -0
- package/dist/prompts/episode-summary.js.map +1 -0
- package/dist/prompts/extraction.d.ts.map +1 -1
- package/dist/prompts/extraction.js +138 -6
- package/dist/prompts/extraction.js.map +1 -1
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +3 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/prompts.test.js +1 -1
- package/dist/prompts/prompts.test.js.map +1 -1
- package/dist/prompts/relations.d.ts.map +1 -1
- package/dist/prompts/relations.js +26 -4
- package/dist/prompts/relations.js.map +1 -1
- package/dist/prompts/workstream-summary.d.ts +17 -0
- package/dist/prompts/workstream-summary.d.ts.map +1 -0
- package/dist/prompts/workstream-summary.js +72 -0
- package/dist/prompts/workstream-summary.js.map +1 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +19 -1
- package/dist/render.js.map +1 -1
- package/dist/render.test.js +37 -5
- package/dist/render.test.js.map +1 -1
- package/docs/diagrams/architecture.svg +87 -0
- package/docs/diagrams/team-memory.svg +250 -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 +4 -2
package/dist/mcp/tools.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { STALENESS_DAYS, THRESHOLD_DUPLICATE, THRESHOLD_RELATED, QDRANT_INDEXES, categoryValues, domainValues, kindValues, memorySubtypeValues, sourceValues, DEFAULT_CATEGORY, DEFAULT_DOMAIN, DEFAULT_KIND, DEFAULT_SOURCE, categoryForMemorySubtype, layerForMemorySubtype, normalizeCategory, normalizeDomain, normalizeKind, validateMemorySubtype, } from "./taxonomy.js";
|
|
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, MEMORY_RECALL_EXCLUDED_KINDS, } from "./helpers.js";
|
|
8
8
|
import { ready, qdrantUrl, qdrantApiKey, setupError, setQdrantUrl, setQdrantApiKey, setReady, getCollection, log, embed, getEmbeddingConfig, qdrantReq, ensureCollection, qdrantUpsert, qdrantSearch, qdrantScroll, qdrantSetPayload, qdrantGetPoints, } from "./api.js";
|
|
9
|
-
import { saveConfig, loadConfig } from "../config.js";
|
|
9
|
+
import { saveConfig, loadConfig, EXTRACTION_HEALTH_PATH } from "../config.js";
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { inspectWatcherPaths, formatIssue } from "../daemon/watcher-health.js";
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
11
13
|
// Runtime state
|
|
12
14
|
// ---------------------------------------------------------------------------
|
|
@@ -166,7 +168,11 @@ async function graphTraversal(primaryResults, limit, scope) {
|
|
|
166
168
|
// ---------------------------------------------------------------------------
|
|
167
169
|
export function registerTools(mcp) {
|
|
168
170
|
// ── get_setup_status ────────────────────────────────────────────────────
|
|
169
|
-
mcp.tool("get_setup_status",
|
|
171
|
+
mcp.tool("get_setup_status", [
|
|
172
|
+
"Check whether the memory system is configured and reachable.",
|
|
173
|
+
"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.",
|
|
174
|
+
"Read-only — safe to call any time.",
|
|
175
|
+
].join(" "), {}, async () => {
|
|
170
176
|
const status = {
|
|
171
177
|
ready,
|
|
172
178
|
qdrant_url: !!qdrantUrl,
|
|
@@ -195,6 +201,44 @@ export function registerTools(mcp) {
|
|
|
195
201
|
status["embedding_connected"] = true;
|
|
196
202
|
}
|
|
197
203
|
catch { /* ignore */ }
|
|
204
|
+
// Watcher / extraction health (issue #58)
|
|
205
|
+
const warnings = [];
|
|
206
|
+
try {
|
|
207
|
+
const cfg = loadConfig();
|
|
208
|
+
status["watcher_path"] = cfg.watchers.copilot.path;
|
|
209
|
+
for (const issue of inspectWatcherPaths(cfg)) {
|
|
210
|
+
warnings.push(formatIssue(issue));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch { /* ignore */ }
|
|
214
|
+
try {
|
|
215
|
+
if (existsSync(EXTRACTION_HEALTH_PATH)) {
|
|
216
|
+
const health = JSON.parse(readFileSync(EXTRACTION_HEALTH_PATH, "utf-8"));
|
|
217
|
+
status["extraction_last_tick_at"] = health.last_tick_at ?? null;
|
|
218
|
+
status["extraction_last_active_session_at"] = health.last_active_session_at ?? null;
|
|
219
|
+
status["extraction_active_session_count"] = health.active_session_count ?? 0;
|
|
220
|
+
if (health.last_active_session_at) {
|
|
221
|
+
const hours = (Date.now() - Date.parse(health.last_active_session_at)) / 3_600_000;
|
|
222
|
+
status["extraction_hours_since_active_session"] = Math.round(hours * 10) / 10;
|
|
223
|
+
if (hours > 6) {
|
|
224
|
+
warnings.push(`Watcher has not seen any active Copilot sessions for ${Math.round(hours)}h — ` +
|
|
225
|
+
`check watcher_path (${health.watcher_path ?? "unknown"}) and that the daemon is running.`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
status["extraction_hours_since_active_session"] = null;
|
|
230
|
+
warnings.push("Daemon has never observed an active Copilot session — extraction may be stalled.");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
status["extraction_last_tick_at"] = null;
|
|
235
|
+
status["extraction_last_active_session_at"] = null;
|
|
236
|
+
status["extraction_hours_since_active_session"] = null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch { /* ignore */ }
|
|
240
|
+
if (warnings.length > 0)
|
|
241
|
+
status["warnings"] = warnings;
|
|
198
242
|
if (!status["ready"] && missing.length > 0) {
|
|
199
243
|
status["setup_instructions"] =
|
|
200
244
|
"Run `bikky setup` or guide the user. Pick one Qdrant option:\n" +
|
|
@@ -206,7 +250,10 @@ export function registerTools(mcp) {
|
|
|
206
250
|
return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
|
|
207
251
|
});
|
|
208
252
|
// ── configure_credentials ───────────────────────────────────────────────
|
|
209
|
-
mcp.tool("configure_credentials",
|
|
253
|
+
mcp.tool("configure_credentials", [
|
|
254
|
+
"Persist Qdrant and embedding credentials to ~/.bikky/config.json and bring the memory system online.",
|
|
255
|
+
"Call this only during onboarding (or when rotating credentials). After it succeeds, the collection is created if missing and embeddings are tested. For day-to-day use, prefer get_setup_status.",
|
|
256
|
+
].join(" "), {
|
|
210
257
|
qdrant_url: z.string().optional().describe("Qdrant REST URL — Qdrant Cloud (https://xxx.cloud.qdrant.io:6333), local Docker (http://localhost:6333), or self-hosted"),
|
|
211
258
|
qdrant_api_key: z.string().optional().describe("Qdrant API key — required for Qdrant Cloud; optional / leave blank for unauthenticated local or self-hosted instances"),
|
|
212
259
|
openai_api_key: z.string().optional().describe("OpenAI API key (for OpenAI embedding/LLM provider)"),
|
|
@@ -252,7 +299,11 @@ export function registerTools(mcp) {
|
|
|
252
299
|
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
253
300
|
});
|
|
254
301
|
// ── verify_connection ───────────────────────────────────────────────────
|
|
255
|
-
mcp.tool("verify_connection",
|
|
302
|
+
mcp.tool("verify_connection", [
|
|
303
|
+
"Confirm Qdrant is reachable, embeddings work, and the collection exists.",
|
|
304
|
+
"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.",
|
|
305
|
+
"Read-only.",
|
|
306
|
+
].join(" "), {}, async () => {
|
|
256
307
|
const results = { qdrant: false, embedding: false, collection: false };
|
|
257
308
|
if (qdrantUrl) {
|
|
258
309
|
try {
|
|
@@ -281,39 +332,36 @@ export function registerTools(mcp) {
|
|
|
281
332
|
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
282
333
|
});
|
|
283
334
|
// ── memory_store ────────────────────────────────────────────────────────
|
|
284
|
-
mcp.tool("memory_store",
|
|
285
|
-
"
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
episode_id: z.string().optional().describe("
|
|
299
|
-
workstream_key: z.string().optional().describe("
|
|
300
|
-
task_key: z.string().optional().describe("
|
|
301
|
-
repo: z.string().optional().describe("
|
|
302
|
-
branch: z.string().optional().describe("
|
|
303
|
-
review_status: z.enum(["candidate", "reviewed", "approved", "rejected"]).optional()
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
importance: z.number().min(0).max(1).optional().describe("How important (0.0-1.0). Omit to default to 0.5."),
|
|
309
|
-
supersedes: z.string().optional().describe("ID of a fact this one replaces"),
|
|
335
|
+
mcp.tool("memory_store", [
|
|
336
|
+
"Persist one atomic fact to long-term memory.",
|
|
337
|
+
"Call this whenever you learn something a future session would need: a service detail, a decision rationale, a workaround, a user preference, an ownership fact, a task-resume pointer. One fact per call — split compound observations into separate calls.",
|
|
338
|
+
"Dedup is automatic (content hash + vector similarity), so you do NOT need to recall first. The tool returns one of: inserted (new fact), reinforced (exact or near-duplicate found — counters bumped), or — if there are similar-but-different facts — a list of potential conflicts so you can decide whether to use 'supersedes'.",
|
|
339
|
+
"To create a typed edge between two entities at the same time, set the optional 'relation' field — no separate tool call needed.",
|
|
340
|
+
"Do NOT use for ephemeral state (current cursor, in-flight todo). Use the harness task folder instead.",
|
|
341
|
+
].join(" "), {
|
|
342
|
+
content: z.string().describe("The fact to store. Should be one atomic, self-contained statement (no compound 'A and B') that makes sense out of context."),
|
|
343
|
+
category: z.enum(categoryValues()).describe(categoryEnumDescription()),
|
|
344
|
+
entities: z.array(z.string()).describe("Lowercase entity names mentioned by this fact (e.g. ['qdrant', 'workspace_id']). Used for entity-scoped recall and graph traversal — keep them short and canonical."),
|
|
345
|
+
domain: z.enum(domainValues()).default(DEFAULT_DOMAIN).describe(domainEnumDescription()),
|
|
346
|
+
kind: z.enum(kindValues()).default(DEFAULT_KIND).describe(kindEnumDescription()),
|
|
347
|
+
memory_subtype: z.enum(memorySubtypeValues()).optional().describe(memorySubtypeEnumDescription()),
|
|
348
|
+
workspace_id: z.string().optional().describe("Workspace namespace for team-shared memory. Omit to use the default workspace from config."),
|
|
349
|
+
episode_id: z.string().optional().describe("Coherent activity-segment ID. Group facts captured during the same coherent task or transcript."),
|
|
350
|
+
workstream_key: z.string().optional().describe("Durable continuity key for a long-running objective (survives across sessions)."),
|
|
351
|
+
task_key: z.string().optional().describe("Task or issue key (e.g. GitHub issue number, JIRA key)."),
|
|
352
|
+
repo: z.string().optional().describe("Repository or project surface this fact relates to (e.g. 'bikky-dev/bikky')."),
|
|
353
|
+
branch: z.string().optional().describe("Branch or working surface (e.g. 'main', 'feat/x')."),
|
|
354
|
+
review_status: z.enum(["candidate", "reviewed", "approved", "rejected"]).optional().describe("Review lifecycle status. candidate=auto-extracted (daemon), reviewed=human-checked, approved=human-confirmed, rejected=incorrect. Agents normally leave this unset."),
|
|
355
|
+
source: z.enum(sourceValues()).default(DEFAULT_SOURCE).describe(sourceEnumDescription()),
|
|
356
|
+
confidence: z.number().min(0).max(1).default(0.9).describe("How certain you are this fact is correct (0.0-1.0). Default 0.9. Lower (~0.6) for inferred or unverified facts."),
|
|
357
|
+
importance: z.number().min(0).max(1).optional().describe("How important this fact is for future recall (0.0-1.0). Defaults to 0.5 if omitted. ≥0.8 surfaces in session briefings."),
|
|
358
|
+
supersedes: z.string().optional().describe("ID of an existing fact that this one replaces. The old fact is marked superseded and excluded from recall. Use this when a fact is updated; use memory_forget when a fact was simply wrong."),
|
|
310
359
|
relation: z.object({
|
|
311
|
-
from: z.string().describe("Source entity"),
|
|
312
|
-
type: z.string().describe("Relation type (owns, uses, decided, prefers, works-on
|
|
313
|
-
to: z.string().describe("Target entity"),
|
|
314
|
-
}).optional().describe("Optional typed
|
|
315
|
-
metadata: z.record(z.string(), z.string()).optional()
|
|
316
|
-
.describe("Optional key-value metadata. Stored with the fact and filterable via memory_recall."),
|
|
360
|
+
from: z.string().describe("Source entity (lowercase)."),
|
|
361
|
+
type: z.string().describe("Relation type (e.g. 'owns', 'uses', 'decided', 'prefers', 'works-on')."),
|
|
362
|
+
to: z.string().describe("Target entity (lowercase)."),
|
|
363
|
+
}).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."),
|
|
364
|
+
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)."),
|
|
317
365
|
}, async ({ content, category, entities, domain, kind, memory_subtype, workspace_id, episode_id, workstream_key, task_key, repo, branch, review_status, source, confidence, importance, supersedes, relation, metadata, }) => {
|
|
318
366
|
const guard = requireReady();
|
|
319
367
|
if (guard)
|
|
@@ -556,29 +604,34 @@ export function registerTools(mcp) {
|
|
|
556
604
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
557
605
|
});
|
|
558
606
|
// ── memory_recall ───────────────────────────────────────────────────────
|
|
559
|
-
mcp.tool("memory_recall",
|
|
560
|
-
"
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
607
|
+
mcp.tool("memory_recall", [
|
|
608
|
+
"Semantic + filtered search over memory. Returns facts ranked by relevance (vector similarity blended with recency, importance, and reinforcement).",
|
|
609
|
+
"Three main uses:",
|
|
610
|
+
" 1. Session-start briefing — broad query like 'session briefing: user preferences, active projects, recent decisions'.",
|
|
611
|
+
" 2. Per-prompt contextual recall — focused query derived from what the user just asked.",
|
|
612
|
+
" 3. Pre-store conflict check — recall similar facts before storing, so you can use 'supersedes' if the new fact replaces an older one.",
|
|
613
|
+
"Combine the natural-language query with structured filters (category, domain, entity, date range, metadata) for tighter results.",
|
|
614
|
+
"If you have a known entity name and want everything about it, prefer memory_entity. For 'what does X own/use?' style questions, prefer memory_relations.",
|
|
615
|
+
].join("\n"), {
|
|
616
|
+
query: z.string().describe("Natural-language description of what you're looking for. Embedded and matched semantically — full sentences work better than keyword lists."),
|
|
617
|
+
category: z.string().optional().describe("Filter by category (same vocabulary as memory_store.category). Optional."),
|
|
618
|
+
domain: z.string().optional().describe("Filter by domain activity profile (same vocabulary as memory_store.domain). Optional."),
|
|
619
|
+
kind: z.string().optional().describe("Filter by kind: fact, summary, distilled, relation. Optional. Telemetry is excluded by default."),
|
|
620
|
+
memory_subtype: z.string().optional().describe("Filter by memory subtype (must be valid for the chosen kind). Optional."),
|
|
621
|
+
workspace_id: z.string().optional().describe("Filter to facts in this workspace namespace. Omit to use the default workspace from config."),
|
|
622
|
+
include_legacy_workspace: z.boolean().optional().describe("Backwards-compatibility flag: also include legacy facts that have no workspace_id. Default false. Only set this if you suspect pre-migration data is missing from results."),
|
|
623
|
+
entity: z.string().optional().describe("Restrict to facts mentioning this entity (case-insensitive). For full entity context prefer memory_entity."),
|
|
624
|
+
episode_id: z.string().optional().describe("Filter by coherent episode ID."),
|
|
625
|
+
workstream_key: z.string().optional().describe("Filter by durable workstream key."),
|
|
626
|
+
task_key: z.string().optional().describe("Filter by task or issue key."),
|
|
627
|
+
repo: z.string().optional().describe("Filter by repository or project surface."),
|
|
628
|
+
branch: z.string().optional().describe("Filter by branch or working surface."),
|
|
629
|
+
review_status: z.string().optional().describe("Filter by review lifecycle status (candidate / reviewed / approved / rejected)."),
|
|
630
|
+
since: z.string().optional().describe("Only facts created on or after this ISO 8601 date or datetime."),
|
|
631
|
+
until: z.string().optional().describe("Only facts created on or before this ISO 8601 date or datetime."),
|
|
632
|
+
limit: z.number().optional().default(10).describe("Max results to return (default 10)."),
|
|
633
|
+
graph_depth: z.number().optional().default(0).describe("Entity-graph traversal depth. 0 = vector search only (fast, default). 1 = also surface 1-hop entity-related facts (slower; use when the user asks 'what's connected to X?')."),
|
|
634
|
+
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)."),
|
|
582
635
|
}, async ({ query, category, domain, kind, memory_subtype, workspace_id, include_legacy_workspace, entity, episode_id, workstream_key, task_key, repo, branch, review_status, since, until, limit, graph_depth, metadata_filter, }) => {
|
|
583
636
|
const guard = requireReady();
|
|
584
637
|
if (guard)
|
|
@@ -641,18 +694,37 @@ export function registerTools(mcp) {
|
|
|
641
694
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
642
695
|
});
|
|
643
696
|
// ── memory_entity ───────────────────────────────────────────────────────
|
|
644
|
-
mcp.tool("memory_entity",
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
697
|
+
mcp.tool("memory_entity", [
|
|
698
|
+
"Get everything bikky knows about a specific entity — facts mentioning it plus typed relations into and out of it.",
|
|
699
|
+
"Prefer this over memory_recall when the user asks 'tell me about X' or 'what do we know about X' and X is a known entity name (service, person, repo, concept). Faster and more complete than semantic search for entity-centric queries.",
|
|
700
|
+
"If you only have a fuzzy description, use memory_recall first to find the entity name.",
|
|
701
|
+
].join(" "), {
|
|
702
|
+
name: z.string().describe("Entity name (case-insensitive, e.g. 'qdrant', 'workspace_id'). Should match the lowercase canonical form used when facts were stored."),
|
|
703
|
+
limit: z.number().optional().default(20).describe("Max facts to return (default 20). Relations are always returned in full, capped at 50 each direction."),
|
|
704
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
705
|
+
include_legacy_workspace: z.boolean().optional().describe("Backwards-compatibility: also include legacy facts with no workspace_id. Default false."),
|
|
650
706
|
}, async ({ name, limit, workspace_id, include_legacy_workspace }) => {
|
|
651
707
|
const guard = requireReady();
|
|
652
708
|
if (guard)
|
|
653
709
|
return guard;
|
|
654
710
|
const entityName = name.toLowerCase();
|
|
655
711
|
const scope = resolveScope(workspace_id, include_legacy_workspace);
|
|
712
|
+
// Look up the daemon-classified entity type, if any.
|
|
713
|
+
let entityType = null;
|
|
714
|
+
try {
|
|
715
|
+
const typeFilter = scopedFilter(scope) ?? { must: [] };
|
|
716
|
+
typeFilter.must.push({ key: "kind", match: { value: "entity_type" } });
|
|
717
|
+
typeFilter.must.push({ key: "entity_name", match: { value: entityName } });
|
|
718
|
+
const typePoints = await qdrantScroll(typeFilter, 1);
|
|
719
|
+
const typePoint = typePoints.result?.points?.[0];
|
|
720
|
+
const payload = typePoint?.payload;
|
|
721
|
+
if (payload?.entity_type) {
|
|
722
|
+
entityType = String(payload.entity_type);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
// Type lookup is best-effort — never fails the request.
|
|
727
|
+
}
|
|
656
728
|
const factsFilter = scopedFilter(scope) ?? { must: [] };
|
|
657
729
|
factsFilter.must.push({ key: "entities", match: { value: entityName } });
|
|
658
730
|
const facts = await qdrantScroll(factsFilter, limit ?? 20);
|
|
@@ -665,13 +737,19 @@ export function registerTools(mcp) {
|
|
|
665
737
|
const output = [];
|
|
666
738
|
const factPoints = facts.result?.points ?? [];
|
|
667
739
|
if (factPoints.length > 0) {
|
|
668
|
-
|
|
740
|
+
const header = entityType
|
|
741
|
+
? `## Facts about ${name} [type: ${entityType}] (${factPoints.length})`
|
|
742
|
+
: `## Facts about ${name} (${factPoints.length})`;
|
|
743
|
+
output.push(header);
|
|
669
744
|
for (const p of factPoints) {
|
|
670
745
|
if (p.payload.category !== "relation") {
|
|
671
746
|
output.push(`- ${formatFact(p)}`);
|
|
672
747
|
}
|
|
673
748
|
}
|
|
674
749
|
}
|
|
750
|
+
else if (entityType) {
|
|
751
|
+
output.push(`## ${name} [type: ${entityType}]`);
|
|
752
|
+
}
|
|
675
753
|
const allRelations = [
|
|
676
754
|
...(relationsFrom.result?.points ?? []),
|
|
677
755
|
...(relationsTo.result?.points ?? []),
|
|
@@ -696,14 +774,16 @@ export function registerTools(mcp) {
|
|
|
696
774
|
return { content: [{ type: "text", text: output.join("\n") }] };
|
|
697
775
|
});
|
|
698
776
|
// ── memory_relations ────────────────────────────────────────────────────
|
|
699
|
-
mcp.tool("memory_relations",
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
777
|
+
mcp.tool("memory_relations", [
|
|
778
|
+
"Query typed edges between entities. Returns 'A --[type]--> B' triples that semantic search alone wouldn't surface.",
|
|
779
|
+
"Use for 'what does X own / use / depend on?' and 'who owns Y?' style questions. Optionally filter by direction (from / to / both) and relation type.",
|
|
780
|
+
"To create relations, use memory_store with the 'relation' field — there is no separate create-relation tool.",
|
|
781
|
+
].join(" "), {
|
|
782
|
+
entity: z.string().describe("Entity name to query (case-insensitive)."),
|
|
783
|
+
relation_type: z.string().optional().describe("Filter to a specific edge label (e.g. 'owns', 'uses', 'decided', 'prefers', 'works-on'). Optional."),
|
|
784
|
+
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)."),
|
|
785
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
786
|
+
include_legacy_workspace: z.boolean().optional().describe("Backwards-compatibility: also include legacy facts with no workspace_id. Default false."),
|
|
707
787
|
}, async ({ entity, relation_type, direction, workspace_id, include_legacy_workspace }) => {
|
|
708
788
|
const guard = requireReady();
|
|
709
789
|
if (guard)
|
|
@@ -746,10 +826,13 @@ export function registerTools(mcp) {
|
|
|
746
826
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
747
827
|
});
|
|
748
828
|
// ── memory_forget ───────────────────────────────────────────────────────
|
|
749
|
-
mcp.tool("memory_forget",
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
829
|
+
mcp.tool("memory_forget", [
|
|
830
|
+
"Mark a fact as superseded/wrong. The fact stays in storage (for audit) but is excluded from all recall results.",
|
|
831
|
+
"Use this when a fact was simply incorrect or no longer applies and there is no replacement. If you have a corrected version, use memory_store with 'supersedes: <fact_id>' instead — that way the new fact stays linked to the old one.",
|
|
832
|
+
].join(" "), {
|
|
833
|
+
fact_id: z.string().describe("ID of the fact to forget (returned by memory_store / memory_recall as 'id')."),
|
|
834
|
+
reason: z.string().describe("Short human-readable reason this fact is being retired (stored in 'superseded_by' for future audit)."),
|
|
835
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
753
836
|
}, async ({ fact_id, reason, workspace_id }) => {
|
|
754
837
|
const guard = requireReady();
|
|
755
838
|
if (guard)
|
|
@@ -766,6 +849,13 @@ export function registerTools(mcp) {
|
|
|
766
849
|
superseded_by: `forgotten:${redactedReason.text}`,
|
|
767
850
|
superseded_at: now,
|
|
768
851
|
updated_at: now,
|
|
852
|
+
// Mark this fact's vector as a bad-exemplar centroid: future
|
|
853
|
+
// candidates with high cosine similarity will be auto-flagged
|
|
854
|
+
// for review. Forgotten facts keep their original vector — the
|
|
855
|
+
// is_bad_exemplar payload flag opts them into the centroid set
|
|
856
|
+
// without requiring a new point.
|
|
857
|
+
is_bad_exemplar: true,
|
|
858
|
+
bad_exemplar_reason: redactedReason.text,
|
|
769
859
|
});
|
|
770
860
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
771
861
|
status: "forgotten",
|
|
@@ -779,9 +869,13 @@ export function registerTools(mcp) {
|
|
|
779
869
|
}
|
|
780
870
|
});
|
|
781
871
|
// ── memory_verify ───────────────────────────────────────────────────────
|
|
782
|
-
mcp.tool("memory_verify",
|
|
783
|
-
|
|
784
|
-
|
|
872
|
+
mcp.tool("memory_verify", [
|
|
873
|
+
"Confirm an existing fact is still accurate, without re-storing it. Resets the staleness clock and bumps a verification counter.",
|
|
874
|
+
"Use this when memory_heartbeat surfaces a stale fact ID and you can confirm it's still true (e.g. you just observed the system in that state). Lighter than memory_store(supersedes:) — same content, fresh timestamp.",
|
|
875
|
+
"If the fact is no longer true, use memory_forget or memory_store(supersedes:) instead.",
|
|
876
|
+
].join(" "), {
|
|
877
|
+
fact_id: z.string().describe("ID of the fact to verify (from memory_recall or memory_heartbeat)."),
|
|
878
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
785
879
|
}, async ({ fact_id, workspace_id }) => {
|
|
786
880
|
const guard = requireReady();
|
|
787
881
|
if (guard)
|
|
@@ -818,17 +912,284 @@ export function registerTools(mcp) {
|
|
|
818
912
|
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
819
913
|
}
|
|
820
914
|
});
|
|
915
|
+
// ── memory_mark_useful ──────────────────────────────────────────────────
|
|
916
|
+
mcp.tool("memory_mark_useful", [
|
|
917
|
+
"Report that a previously recalled fact actually helped you answer the user's question or complete a task.",
|
|
918
|
+
"Bumps a 'useful_count' counter on the fact and writes a telemetry feedback_event row that future ranking work can aggregate.",
|
|
919
|
+
"Call this AFTER you used a fact from memory_recall / memory_entity and confirmed it was helpful — not for every recalled fact. If the fact was wrong or misleading, use memory_report_outcome with outcome='wrong' or 'misleading' instead.",
|
|
920
|
+
].join(" "), {
|
|
921
|
+
fact_id: z.string().describe("ID of the fact that was useful (from memory_recall or memory_entity)."),
|
|
922
|
+
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."),
|
|
923
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
924
|
+
}, async ({ fact_id, note, workspace_id }) => {
|
|
925
|
+
const guard = requireReady();
|
|
926
|
+
if (guard)
|
|
927
|
+
return guard;
|
|
928
|
+
const now = nowISO();
|
|
929
|
+
try {
|
|
930
|
+
const scope = resolveScope(workspace_id);
|
|
931
|
+
const writable = await getPointForWorkspaceWrite(fact_id, scope);
|
|
932
|
+
if (writable.error) {
|
|
933
|
+
return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
|
|
934
|
+
}
|
|
935
|
+
const existingPt = writable.point;
|
|
936
|
+
const currentCount = existingPt?.payload.useful_count ?? 0;
|
|
937
|
+
const newCount = currentCount + 1;
|
|
938
|
+
await qdrantSetPayload([fact_id], {
|
|
939
|
+
useful_count: newCount,
|
|
940
|
+
last_useful_at: now,
|
|
941
|
+
updated_at: now,
|
|
942
|
+
});
|
|
943
|
+
// Write a telemetry feedback_event row so the signal is also visible
|
|
944
|
+
// to aggregations and review tooling.
|
|
945
|
+
const eventId = newId();
|
|
946
|
+
const eventContent = note
|
|
947
|
+
? `Fact ${fact_id} marked useful: ${note}`
|
|
948
|
+
: `Fact ${fact_id} marked useful.`;
|
|
949
|
+
const eventPayload = {
|
|
950
|
+
content: eventContent,
|
|
951
|
+
category: "observations",
|
|
952
|
+
domain: "software_engineering",
|
|
953
|
+
kind: "telemetry",
|
|
954
|
+
memory_subtype: "feedback_event",
|
|
955
|
+
layer: "memory_object",
|
|
956
|
+
entities: [],
|
|
957
|
+
source: "agent",
|
|
958
|
+
confidence: 1.0,
|
|
959
|
+
importance: 0.3,
|
|
960
|
+
content_hash: contentHash("feedback_event", `${fact_id}:useful:${now}`),
|
|
961
|
+
target_fact_id: fact_id,
|
|
962
|
+
feedback_kind: "useful",
|
|
963
|
+
created_at: now,
|
|
964
|
+
updated_at: now,
|
|
965
|
+
};
|
|
966
|
+
addWorkspacePayload(eventPayload, scope);
|
|
967
|
+
try {
|
|
968
|
+
const eventVector = await embed(eventContent);
|
|
969
|
+
await qdrantUpsert(eventId, eventVector, eventPayload);
|
|
970
|
+
}
|
|
971
|
+
catch (e) {
|
|
972
|
+
log("WARN", `Failed to record feedback_event: ${e instanceof Error ? e.message : String(e)}`);
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
976
|
+
status: "marked_useful",
|
|
977
|
+
fact_id,
|
|
978
|
+
useful_count: newCount,
|
|
979
|
+
event_id: eventId,
|
|
980
|
+
}) }],
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
catch (e) {
|
|
984
|
+
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
// ── memory_report_outcome ───────────────────────────────────────────────
|
|
988
|
+
mcp.tool("memory_report_outcome", [
|
|
989
|
+
"Report the downstream outcome of using a recalled fact — useful, misleading, irrelevant, or wrong.",
|
|
990
|
+
"Writes a telemetry outcome_event row that future ranking and review work can aggregate. Unlike memory_mark_useful (positive-only, bumps a counter), this records a richer signal including negative outcomes and optional notes.",
|
|
991
|
+
"Use this when you can confidently judge whether a fact actually helped: 'useful' = helped you complete the task; 'misleading' = pointed in a wrong direction; 'irrelevant' = matched semantically but didn't help; 'wrong' = factually incorrect (also consider memory_forget for clearly wrong facts).",
|
|
992
|
+
].join(" "), {
|
|
993
|
+
fact_id: z.string().describe("ID of the fact whose outcome you are reporting."),
|
|
994
|
+
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."),
|
|
995
|
+
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."),
|
|
996
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
997
|
+
}, async ({ fact_id, outcome, notes, workspace_id }) => {
|
|
998
|
+
const guard = requireReady();
|
|
999
|
+
if (guard)
|
|
1000
|
+
return guard;
|
|
1001
|
+
const now = nowISO();
|
|
1002
|
+
try {
|
|
1003
|
+
const scope = resolveScope(workspace_id);
|
|
1004
|
+
const target = await getPointForWorkspaceWrite(fact_id, scope);
|
|
1005
|
+
if (target.error) {
|
|
1006
|
+
return { content: [{ type: "text", text: JSON.stringify(target.error, null, 2) }], isError: true };
|
|
1007
|
+
}
|
|
1008
|
+
const eventId = newId();
|
|
1009
|
+
const eventContent = notes
|
|
1010
|
+
? `Fact ${fact_id} outcome=${outcome}: ${notes}`
|
|
1011
|
+
: `Fact ${fact_id} outcome=${outcome}.`;
|
|
1012
|
+
const eventPayload = {
|
|
1013
|
+
content: eventContent,
|
|
1014
|
+
category: "observations",
|
|
1015
|
+
domain: "software_engineering",
|
|
1016
|
+
kind: "telemetry",
|
|
1017
|
+
memory_subtype: "outcome_event",
|
|
1018
|
+
layer: "memory_object",
|
|
1019
|
+
entities: [],
|
|
1020
|
+
source: "agent",
|
|
1021
|
+
confidence: 1.0,
|
|
1022
|
+
importance: outcome === "wrong" || outcome === "misleading" ? 0.6 : 0.3,
|
|
1023
|
+
content_hash: contentHash("outcome_event", `${fact_id}:${outcome}:${now}`),
|
|
1024
|
+
target_fact_id: fact_id,
|
|
1025
|
+
outcome,
|
|
1026
|
+
created_at: now,
|
|
1027
|
+
updated_at: now,
|
|
1028
|
+
};
|
|
1029
|
+
addWorkspacePayload(eventPayload, scope);
|
|
1030
|
+
const eventVector = await embed(eventContent);
|
|
1031
|
+
await qdrantUpsert(eventId, eventVector, eventPayload);
|
|
1032
|
+
return {
|
|
1033
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
1034
|
+
status: "outcome_recorded",
|
|
1035
|
+
fact_id,
|
|
1036
|
+
outcome,
|
|
1037
|
+
event_id: eventId,
|
|
1038
|
+
}) }],
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
catch (e) {
|
|
1042
|
+
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
// ── memory_session_summary ──────────────────────────────────────────────
|
|
1046
|
+
mcp.tool("memory_session_summary", [
|
|
1047
|
+
"Persist a compact summary of the current session — what got done, what decisions were made, what's still open.",
|
|
1048
|
+
"Stored as kind='summary', memory_subtype='session_index', source='agent'. Keep it short (target 30-80 words). Future sessions retrieve these via memory_recall to bootstrap context faster than re-reading the original transcript.",
|
|
1049
|
+
"Call this near session close (or at major milestone boundaries) when the work is meaningful enough to want a future agent to inherit. Skip for trivial single-question sessions.",
|
|
1050
|
+
].join(" "), {
|
|
1051
|
+
content: z.string().describe("The summary text. Atomic, self-contained, 30-80 words ideally. Should answer: what was the goal, what did we do, what remains?"),
|
|
1052
|
+
entities: z.array(z.string()).optional().describe("Lowercase entity names mentioned by the summary (services, repos, people, concepts). Used for entity-scoped recall later."),
|
|
1053
|
+
episode_id: z.string().optional().describe("Coherent activity-segment ID for grouping with related captures."),
|
|
1054
|
+
workstream_key: z.string().optional().describe("Durable continuity key for a long-running objective (survives across sessions)."),
|
|
1055
|
+
task_key: z.string().optional().describe("Task or issue key (e.g. GitHub issue number, JIRA key)."),
|
|
1056
|
+
repo: z.string().optional().describe("Repository or project surface this summary relates to."),
|
|
1057
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
1058
|
+
}, async ({ content, entities, episode_id, workstream_key, task_key, repo, workspace_id }) => {
|
|
1059
|
+
const guard = requireReady();
|
|
1060
|
+
if (guard)
|
|
1061
|
+
return guard;
|
|
1062
|
+
lastStoreTime = Date.now();
|
|
1063
|
+
const now = nowISO();
|
|
1064
|
+
try {
|
|
1065
|
+
const scope = resolveScope(workspace_id);
|
|
1066
|
+
const normalizedEntities = (entities ?? []).map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
1067
|
+
const summaryId = newId();
|
|
1068
|
+
const vector = await embed(content);
|
|
1069
|
+
const payload = {
|
|
1070
|
+
content,
|
|
1071
|
+
category: categoryForMemorySubtype("session_index") ?? "projects",
|
|
1072
|
+
domain: "software_engineering",
|
|
1073
|
+
kind: "summary",
|
|
1074
|
+
memory_subtype: "session_index",
|
|
1075
|
+
layer: layerForMemorySubtype("session_index") ?? "episode",
|
|
1076
|
+
entities: normalizedEntities,
|
|
1077
|
+
source: "agent",
|
|
1078
|
+
confidence: 0.9,
|
|
1079
|
+
importance: 0.6,
|
|
1080
|
+
content_hash: contentHash("summary", content),
|
|
1081
|
+
reinforcement_count: 1,
|
|
1082
|
+
last_reinforced_at: now,
|
|
1083
|
+
superseded_by: null,
|
|
1084
|
+
superseded_at: null,
|
|
1085
|
+
created_at: now,
|
|
1086
|
+
updated_at: now,
|
|
1087
|
+
};
|
|
1088
|
+
if (episode_id)
|
|
1089
|
+
payload["episode_id"] = episode_id;
|
|
1090
|
+
if (workstream_key)
|
|
1091
|
+
payload["workstream_key"] = workstream_key;
|
|
1092
|
+
if (task_key)
|
|
1093
|
+
payload["task_key"] = task_key;
|
|
1094
|
+
if (repo)
|
|
1095
|
+
payload["repo"] = repo;
|
|
1096
|
+
addWorkspacePayload(payload, scope);
|
|
1097
|
+
await qdrantUpsert(summaryId, vector, payload);
|
|
1098
|
+
return {
|
|
1099
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
1100
|
+
status: "summary_stored",
|
|
1101
|
+
summary_id: summaryId,
|
|
1102
|
+
workspace_id: scope.workspaceId,
|
|
1103
|
+
}) }],
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
catch (e) {
|
|
1107
|
+
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
// ── memory_distill ──────────────────────────────────────────────────────
|
|
1111
|
+
mcp.tool("memory_distill", [
|
|
1112
|
+
"Persist a distilled convention — a reusable learning, pattern, or runbook synthesized from multiple prior memories.",
|
|
1113
|
+
"Stored as kind='distilled', memory_subtype='convention', source='agent'. Use this when you've noticed a pattern across several prior facts/sessions that's worth surfacing as its own atomic learning. The new memory will rank above raw facts in semantic recall because distilled patterns are higher-signal.",
|
|
1114
|
+
"Provide 'supersedes' if this distillation replaces an earlier convention. The original stays in storage but is excluded from recall.",
|
|
1115
|
+
].join(" "), {
|
|
1116
|
+
content: z.string().describe("One-sentence reusable convention or pattern. Should be self-contained and applicable beyond a single situation."),
|
|
1117
|
+
entities: z.array(z.string()).describe("Lowercase entity names this distillation applies to (services, tools, concepts)."),
|
|
1118
|
+
supersedes: z.string().optional().describe("ID of an earlier distilled fact that this one replaces. Old fact is marked superseded and excluded from recall."),
|
|
1119
|
+
task_key: z.string().optional().describe("Task or issue key associated with this learning, if relevant."),
|
|
1120
|
+
repo: z.string().optional().describe("Repository or project surface this learning applies to."),
|
|
1121
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
1122
|
+
}, async ({ content, entities, supersedes, task_key, repo, workspace_id }) => {
|
|
1123
|
+
const guard = requireReady();
|
|
1124
|
+
if (guard)
|
|
1125
|
+
return guard;
|
|
1126
|
+
lastStoreTime = Date.now();
|
|
1127
|
+
const now = nowISO();
|
|
1128
|
+
try {
|
|
1129
|
+
const scope = resolveScope(workspace_id);
|
|
1130
|
+
const normalizedEntities = entities.map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
1131
|
+
const distilledId = newId();
|
|
1132
|
+
const vector = await embed(content);
|
|
1133
|
+
if (supersedes) {
|
|
1134
|
+
const existing = await getPointForWorkspaceWrite(supersedes, scope);
|
|
1135
|
+
if (existing.error) {
|
|
1136
|
+
return { content: [{ type: "text", text: JSON.stringify(existing.error, null, 2) }], isError: true };
|
|
1137
|
+
}
|
|
1138
|
+
await qdrantSetPayload([supersedes], {
|
|
1139
|
+
superseded_by: distilledId,
|
|
1140
|
+
superseded_at: now,
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
const payload = {
|
|
1144
|
+
content,
|
|
1145
|
+
category: categoryForMemorySubtype("convention") ?? "observations",
|
|
1146
|
+
domain: "software_engineering",
|
|
1147
|
+
kind: "distilled",
|
|
1148
|
+
memory_subtype: "convention",
|
|
1149
|
+
layer: layerForMemorySubtype("convention") ?? "domain",
|
|
1150
|
+
entities: normalizedEntities,
|
|
1151
|
+
source: "agent",
|
|
1152
|
+
confidence: 0.9,
|
|
1153
|
+
importance: 0.7,
|
|
1154
|
+
content_hash: contentHash("distilled", content),
|
|
1155
|
+
reinforcement_count: 1,
|
|
1156
|
+
last_reinforced_at: now,
|
|
1157
|
+
superseded_by: null,
|
|
1158
|
+
superseded_at: null,
|
|
1159
|
+
created_at: now,
|
|
1160
|
+
updated_at: now,
|
|
1161
|
+
};
|
|
1162
|
+
if (task_key)
|
|
1163
|
+
payload["task_key"] = task_key;
|
|
1164
|
+
if (repo)
|
|
1165
|
+
payload["repo"] = repo;
|
|
1166
|
+
addWorkspacePayload(payload, scope);
|
|
1167
|
+
await qdrantUpsert(distilledId, vector, payload);
|
|
1168
|
+
return {
|
|
1169
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
1170
|
+
status: "distilled_stored",
|
|
1171
|
+
distilled_id: distilledId,
|
|
1172
|
+
supersedes: supersedes ?? null,
|
|
1173
|
+
workspace_id: scope.workspaceId,
|
|
1174
|
+
}) }],
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
catch (e) {
|
|
1178
|
+
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
821
1181
|
// ── memory_review ───────────────────────────────────────────────────────
|
|
822
|
-
mcp.tool("memory_review",
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1182
|
+
mcp.tool("memory_review", [
|
|
1183
|
+
"Triage facts that were extracted automatically by the bikky daemon (source='daemon').",
|
|
1184
|
+
"Only useful when the daemon is running and capturing memories from logs/transcripts; otherwise this returns an empty list. Supports four actions: list (default — show recent daemon facts), approve (mark verified), reject (mark superseded with reason), correct (replace with edited content as a new fact).",
|
|
1185
|
+
].join(" "), {
|
|
1186
|
+
limit: z.number().optional().default(10).describe("Max facts to return when action=list (default 10)."),
|
|
1187
|
+
action: z.enum(["list", "approve", "reject", "correct"]).optional().default("list").describe("What to do. list = show recent daemon-extracted facts (default). approve = confirm a fact is correct (bumps verification count). reject = mark a fact as wrong (requires 'reason'). correct = supersede with an edited version (requires 'corrected_content')."),
|
|
1188
|
+
fact_id: z.string().optional().describe("Fact ID to act on. Required for approve / reject / correct."),
|
|
1189
|
+
reason: z.string().optional().describe("Required for action=reject. Short reason the fact is wrong."),
|
|
1190
|
+
corrected_content: z.string().optional().describe("Required for action=correct. The fixed fact text. Stored as a new fact that supersedes the original."),
|
|
1191
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
1192
|
+
include_legacy_workspace: z.boolean().optional().describe("Backwards-compatibility: also include legacy facts with no workspace_id. Default false."),
|
|
832
1193
|
}, async ({ limit, action, fact_id, reason, corrected_content, workspace_id, include_legacy_workspace }) => {
|
|
833
1194
|
const guard = requireReady();
|
|
834
1195
|
if (guard)
|
|
@@ -947,7 +1308,10 @@ export function registerTools(mcp) {
|
|
|
947
1308
|
return { content: [{ type: "text", text: `Unknown action: ${String(action)}` }] };
|
|
948
1309
|
});
|
|
949
1310
|
// ── memory_heartbeat ────────────────────────────────────────────────────
|
|
950
|
-
mcp.tool("memory_heartbeat",
|
|
1311
|
+
mcp.tool("memory_heartbeat", [
|
|
1312
|
+
"Reflection check-in. Returns up to three things: a memory nudge if you haven't stored anything in 10+ minutes, stale-fact alerts every 3rd call (with IDs you can pass to memory_verify or memory_forget), and a reflection prompt asking whether the last few minutes of work produced anything worth storing.",
|
|
1313
|
+
"Call periodically during interactive sessions — roughly every 10 minutes or every 3rd user prompt. No arguments. Cheap and read-only.",
|
|
1314
|
+
].join(" "), {}, async () => {
|
|
951
1315
|
heartbeatCount++;
|
|
952
1316
|
const sections = [];
|
|
953
1317
|
const nudge = buildMemoryNudge();
|