bikky 0.3.3 → 0.3.6
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 +26 -6
- package/dist/cli.js +26 -6
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +255 -3
- 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 +95 -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/consolidation.d.ts +4 -1
- package/dist/daemon/consolidation.d.ts.map +1 -1
- package/dist/daemon/consolidation.js +18 -4
- package/dist/daemon/consolidation.js.map +1 -1
- package/dist/daemon/entity-typing.d.ts +42 -0
- package/dist/daemon/entity-typing.d.ts.map +1 -0
- package/dist/daemon/entity-typing.js +295 -0
- package/dist/daemon/entity-typing.js.map +1 -0
- package/dist/daemon/entity-typing.test.d.ts +2 -0
- package/dist/daemon/entity-typing.test.d.ts.map +1 -0
- package/dist/daemon/entity-typing.test.js +50 -0
- package/dist/daemon/entity-typing.test.js.map +1 -0
- package/dist/daemon/episode-summary.d.ts +4 -8
- package/dist/daemon/episode-summary.d.ts.map +1 -1
- package/dist/daemon/episode-summary.js +52 -18
- package/dist/daemon/episode-summary.js.map +1 -1
- package/dist/daemon/episode-summary.test.js +10 -7
- 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 +183 -26
- 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 +22 -0
- package/dist/daemon/loop.js.map +1 -1
- package/dist/daemon/loop.test.d.ts +2 -0
- package/dist/daemon/loop.test.d.ts.map +1 -0
- package/dist/daemon/loop.test.js +85 -0
- package/dist/daemon/loop.test.js.map +1 -0
- package/dist/daemon/maintenance-state.d.ts +36 -0
- package/dist/daemon/maintenance-state.d.ts.map +1 -0
- package/dist/daemon/maintenance-state.js +95 -0
- package/dist/daemon/maintenance-state.js.map +1 -0
- package/dist/daemon/maintenance-state.test.d.ts +2 -0
- package/dist/daemon/maintenance-state.test.d.ts.map +1 -0
- package/dist/daemon/maintenance-state.test.js +56 -0
- package/dist/daemon/maintenance-state.test.js.map +1 -0
- package/dist/daemon/qdrant.d.ts +37 -1
- package/dist/daemon/qdrant.d.ts.map +1 -1
- package/dist/daemon/qdrant.js +107 -12
- package/dist/daemon/qdrant.js.map +1 -1
- package/dist/daemon/qdrant.test.js +57 -1
- package/dist/daemon/qdrant.test.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 +49 -34
- package/dist/daemon/relations.d.ts.map +1 -1
- package/dist/daemon/relations.js +249 -153
- package/dist/daemon/relations.js.map +1 -1
- package/dist/daemon/relations.test.d.ts +2 -0
- package/dist/daemon/relations.test.d.ts.map +1 -0
- package/dist/daemon/relations.test.js +36 -0
- package/dist/daemon/relations.test.js.map +1 -0
- package/dist/daemon/session-index.d.ts +1 -8
- package/dist/daemon/session-index.d.ts.map +1 -1
- package/dist/daemon/session-index.js +8 -10
- package/dist/daemon/session-index.js.map +1 -1
- package/dist/daemon/session-index.test.js +15 -9
- package/dist/daemon/session-index.test.js.map +1 -1
- package/dist/daemon/session-summary.d.ts +1 -8
- package/dist/daemon/session-summary.d.ts.map +1 -1
- package/dist/daemon/session-summary.js +17 -12
- package/dist/daemon/session-summary.js.map +1 -1
- package/dist/daemon/session-summary.test.js +5 -3
- package/dist/daemon/session-summary.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 +22 -14
- package/dist/daemon/workstream-summary.js.map +1 -1
- package/dist/daemon/workstream-summary.test.js +9 -6
- package/dist/daemon/workstream-summary.test.js.map +1 -1
- package/dist/lib/qdrant-client.d.ts +34 -0
- package/dist/lib/qdrant-client.d.ts.map +1 -1
- package/dist/lib/qdrant-client.js +54 -0
- package/dist/lib/qdrant-client.js.map +1 -1
- package/dist/lib/qdrant-client.test.js +49 -1
- package/dist/lib/qdrant-client.test.js.map +1 -1
- package/dist/llm/inference/index.d.ts +2 -1
- package/dist/llm/inference/index.d.ts.map +1 -1
- package/dist/llm/inference/index.js +37 -2
- package/dist/llm/inference/index.js.map +1 -1
- package/dist/llm/inference/index.test.js +44 -3
- package/dist/llm/inference/index.test.js.map +1 -1
- package/dist/llm/inference/providers/bedrock.d.ts +23 -0
- package/dist/llm/inference/providers/bedrock.d.ts.map +1 -1
- package/dist/llm/inference/providers/bedrock.js +10 -1
- package/dist/llm/inference/providers/bedrock.js.map +1 -1
- package/dist/llm/inference/providers/bedrock.test.js +49 -2
- package/dist/llm/inference/providers/bedrock.test.js.map +1 -1
- package/dist/llm/inference/providers/ollama.d.ts.map +1 -1
- package/dist/llm/inference/providers/ollama.js +7 -1
- package/dist/llm/inference/providers/ollama.js.map +1 -1
- package/dist/llm/inference/providers/openai.d.ts.map +1 -1
- package/dist/llm/inference/providers/openai.js +7 -1
- package/dist/llm/inference/providers/openai.js.map +1 -1
- package/dist/llm/inference/providers/openai.test.js +38 -2
- package/dist/llm/inference/providers/openai.test.js.map +1 -1
- package/dist/llm/inference/providers/portkey.d.ts.map +1 -1
- package/dist/llm/inference/providers/portkey.js +7 -1
- package/dist/llm/inference/providers/portkey.js.map +1 -1
- package/dist/llm/inference/types.d.ts +15 -0
- package/dist/llm/inference/types.d.ts.map +1 -1
- package/dist/llm/telemetry.d.ts +8 -1
- package/dist/llm/telemetry.d.ts.map +1 -1
- package/dist/llm/telemetry.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/taxonomy.d.ts +6 -18
- package/dist/mcp/taxonomy.d.ts.map +1 -1
- package/dist/mcp/taxonomy.js +15 -25
- package/dist/mcp/taxonomy.js.map +1 -1
- package/dist/mcp/taxonomy.test.js +23 -7
- package/dist/mcp/taxonomy.test.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +355 -17
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/tools.test.js +279 -5
- package/dist/mcp/tools.test.js.map +1 -1
- package/dist/privacy/redaction.d.ts +21 -0
- package/dist/privacy/redaction.d.ts.map +1 -0
- package/dist/privacy/redaction.js +83 -0
- package/dist/privacy/redaction.js.map +1 -0
- package/dist/privacy/redaction.test.d.ts +2 -0
- package/dist/privacy/redaction.test.d.ts.map +1 -0
- package/dist/privacy/redaction.test.js +51 -0
- package/dist/privacy/redaction.test.js.map +1 -0
- package/dist/prompts/distill.d.ts.map +1 -1
- package/dist/prompts/distill.js +3 -2
- 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.map +1 -1
- package/dist/prompts/episode-summary.js +17 -3
- package/dist/prompts/episode-summary.js.map +1 -1
- package/dist/prompts/extraction.d.ts.map +1 -1
- package/dist/prompts/extraction.js +114 -5
- package/dist/prompts/extraction.js.map +1 -1
- package/dist/prompts/index.d.ts +1 -0
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +1 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/relations.d.ts.map +1 -1
- package/dist/prompts/relations.js +72 -4
- package/dist/prompts/relations.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +7 -1
- package/dist/render.js.map +1 -1
- package/dist/render.test.js +3 -2
- package/dist/render.test.js.map +1 -1
- package/dist/status.d.ts +94 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +378 -0
- package/dist/status.js.map +1 -0
- package/dist/status.test.d.ts +5 -0
- package/dist/status.test.d.ts.map +1 -0
- package/dist/status.test.js +203 -0
- package/dist/status.test.js.map +1 -0
- package/package.json +1 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -6,7 +6,10 @@ 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, 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";
|
|
12
|
+
import { addRedactionPayload, combineRedactions, redactStorageText, } from "../privacy/redaction.js";
|
|
10
13
|
// ---------------------------------------------------------------------------
|
|
11
14
|
// Runtime state
|
|
12
15
|
// ---------------------------------------------------------------------------
|
|
@@ -22,15 +25,6 @@ function nowISO() {
|
|
|
22
25
|
function newId() {
|
|
23
26
|
return crypto.randomUUID();
|
|
24
27
|
}
|
|
25
|
-
function redactionOptions() {
|
|
26
|
-
return { enabled: false, redactPii: false };
|
|
27
|
-
}
|
|
28
|
-
function redactStorageText(text) {
|
|
29
|
-
return { text, redacted: false, summary: "none", matches: [] };
|
|
30
|
-
}
|
|
31
|
-
function combineRedactions(_items) {
|
|
32
|
-
return { redacted: false, summary: "none", matches: [] };
|
|
33
|
-
}
|
|
34
28
|
function resolveScope(workspaceId, includeLegacyWorkspace = false) {
|
|
35
29
|
return {
|
|
36
30
|
workspaceId: workspaceId?.trim() || undefined,
|
|
@@ -50,9 +44,6 @@ function addWorkspacePayload(payload, scope) {
|
|
|
50
44
|
if (scope.actorId)
|
|
51
45
|
payload["actor_id"] = scope.actorId;
|
|
52
46
|
}
|
|
53
|
-
function addRedactionPayload(_payload, _summary) {
|
|
54
|
-
// Task 243 keeps storage pass-through; redaction policy is out of scope for this branch.
|
|
55
|
-
}
|
|
56
47
|
async function getPointForWorkspaceWrite(factId, _scope) {
|
|
57
48
|
const existing = await qdrantGetPoints([factId]);
|
|
58
49
|
const point = existing.result?.[0];
|
|
@@ -199,6 +190,44 @@ export function registerTools(mcp) {
|
|
|
199
190
|
status["embedding_connected"] = true;
|
|
200
191
|
}
|
|
201
192
|
catch { /* ignore */ }
|
|
193
|
+
// Watcher / extraction health (issue #58)
|
|
194
|
+
const warnings = [];
|
|
195
|
+
try {
|
|
196
|
+
const cfg = loadConfig();
|
|
197
|
+
status["watcher_path"] = cfg.watchers.copilot.path;
|
|
198
|
+
for (const issue of inspectWatcherPaths(cfg)) {
|
|
199
|
+
warnings.push(formatIssue(issue));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch { /* ignore */ }
|
|
203
|
+
try {
|
|
204
|
+
if (existsSync(EXTRACTION_HEALTH_PATH)) {
|
|
205
|
+
const health = JSON.parse(readFileSync(EXTRACTION_HEALTH_PATH, "utf-8"));
|
|
206
|
+
status["extraction_last_tick_at"] = health.last_tick_at ?? null;
|
|
207
|
+
status["extraction_last_active_session_at"] = health.last_active_session_at ?? null;
|
|
208
|
+
status["extraction_active_session_count"] = health.active_session_count ?? 0;
|
|
209
|
+
if (health.last_active_session_at) {
|
|
210
|
+
const hours = (Date.now() - Date.parse(health.last_active_session_at)) / 3_600_000;
|
|
211
|
+
status["extraction_hours_since_active_session"] = Math.round(hours * 10) / 10;
|
|
212
|
+
if (hours > 6) {
|
|
213
|
+
warnings.push(`Watcher has not seen any active Copilot sessions for ${Math.round(hours)}h — ` +
|
|
214
|
+
`check watcher_path (${health.watcher_path ?? "unknown"}) and that the daemon is running.`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
status["extraction_hours_since_active_session"] = null;
|
|
219
|
+
warnings.push("Daemon has never observed an active Copilot session — extraction may be stalled.");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
status["extraction_last_tick_at"] = null;
|
|
224
|
+
status["extraction_last_active_session_at"] = null;
|
|
225
|
+
status["extraction_hours_since_active_session"] = null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch { /* ignore */ }
|
|
229
|
+
if (warnings.length > 0)
|
|
230
|
+
status["warnings"] = warnings;
|
|
202
231
|
if (!status["ready"] && missing.length > 0) {
|
|
203
232
|
status["setup_instructions"] =
|
|
204
233
|
"Run `bikky setup` or guide the user. Pick one Qdrant option:\n" +
|
|
@@ -353,11 +382,17 @@ export function registerTools(mcp) {
|
|
|
353
382
|
type: redactStorageText(relation.type),
|
|
354
383
|
to: redactStorageText(relation.to),
|
|
355
384
|
} : null;
|
|
356
|
-
const
|
|
385
|
+
const factRedactionSummary = combineRedactions([
|
|
357
386
|
redactedContent,
|
|
358
387
|
...redactedEntities,
|
|
388
|
+
]);
|
|
389
|
+
const relationRedactionSummary = combineRedactions([
|
|
359
390
|
...(redactedRelation ? [redactedRelation.from, redactedRelation.type, redactedRelation.to] : []),
|
|
360
391
|
]);
|
|
392
|
+
const redactionSummary = combineRedactions([
|
|
393
|
+
factRedactionSummary,
|
|
394
|
+
relationRedactionSummary,
|
|
395
|
+
]);
|
|
361
396
|
const hash = contentHash(normalizedCategory, redactedContent.text);
|
|
362
397
|
const normalizedEntities = sanitizedEntities.map((e) => e.toLowerCase());
|
|
363
398
|
const sanitizedRelation = redactedRelation ? {
|
|
@@ -509,7 +544,7 @@ export function registerTools(mcp) {
|
|
|
509
544
|
if (review_status)
|
|
510
545
|
payload["review_status"] = review_status;
|
|
511
546
|
addWorkspacePayload(payload, scope);
|
|
512
|
-
addRedactionPayload(payload,
|
|
547
|
+
addRedactionPayload(payload, factRedactionSummary);
|
|
513
548
|
if (metadata && Object.keys(metadata).length > 0) {
|
|
514
549
|
payload["metadata"] = metadata;
|
|
515
550
|
}
|
|
@@ -541,7 +576,7 @@ export function registerTools(mcp) {
|
|
|
541
576
|
to_entity: sanitizedRelation.to.toLowerCase(),
|
|
542
577
|
};
|
|
543
578
|
addWorkspacePayload(relPayload, scope);
|
|
544
|
-
addRedactionPayload(relPayload,
|
|
579
|
+
addRedactionPayload(relPayload, relationRedactionSummary);
|
|
545
580
|
await qdrantUpsert(relationId, relVector, relPayload);
|
|
546
581
|
}
|
|
547
582
|
const result = {
|
|
@@ -669,6 +704,22 @@ export function registerTools(mcp) {
|
|
|
669
704
|
return guard;
|
|
670
705
|
const entityName = name.toLowerCase();
|
|
671
706
|
const scope = resolveScope(workspace_id, include_legacy_workspace);
|
|
707
|
+
// Look up the daemon-classified entity type, if any.
|
|
708
|
+
let entityType = null;
|
|
709
|
+
try {
|
|
710
|
+
const typeFilter = scopedFilter(scope) ?? { must: [] };
|
|
711
|
+
typeFilter.must.push({ key: "kind", match: { value: "entity_type" } });
|
|
712
|
+
typeFilter.must.push({ key: "entity_name", match: { value: entityName } });
|
|
713
|
+
const typePoints = await qdrantScroll(typeFilter, 1);
|
|
714
|
+
const typePoint = typePoints.result?.points?.[0];
|
|
715
|
+
const payload = typePoint?.payload;
|
|
716
|
+
if (payload?.entity_type) {
|
|
717
|
+
entityType = String(payload.entity_type);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
// Type lookup is best-effort — never fails the request.
|
|
722
|
+
}
|
|
672
723
|
const factsFilter = scopedFilter(scope) ?? { must: [] };
|
|
673
724
|
factsFilter.must.push({ key: "entities", match: { value: entityName } });
|
|
674
725
|
const facts = await qdrantScroll(factsFilter, limit ?? 20);
|
|
@@ -681,13 +732,19 @@ export function registerTools(mcp) {
|
|
|
681
732
|
const output = [];
|
|
682
733
|
const factPoints = facts.result?.points ?? [];
|
|
683
734
|
if (factPoints.length > 0) {
|
|
684
|
-
|
|
735
|
+
const header = entityType
|
|
736
|
+
? `## Facts about ${name} [type: ${entityType}] (${factPoints.length})`
|
|
737
|
+
: `## Facts about ${name} (${factPoints.length})`;
|
|
738
|
+
output.push(header);
|
|
685
739
|
for (const p of factPoints) {
|
|
686
740
|
if (p.payload.category !== "relation") {
|
|
687
741
|
output.push(`- ${formatFact(p)}`);
|
|
688
742
|
}
|
|
689
743
|
}
|
|
690
744
|
}
|
|
745
|
+
else if (entityType) {
|
|
746
|
+
output.push(`## ${name} [type: ${entityType}]`);
|
|
747
|
+
}
|
|
691
748
|
const allRelations = [
|
|
692
749
|
...(relationsFrom.result?.points ?? []),
|
|
693
750
|
...(relationsTo.result?.points ?? []),
|
|
@@ -787,6 +844,13 @@ export function registerTools(mcp) {
|
|
|
787
844
|
superseded_by: `forgotten:${redactedReason.text}`,
|
|
788
845
|
superseded_at: now,
|
|
789
846
|
updated_at: now,
|
|
847
|
+
// Mark this fact's vector as a bad-exemplar centroid: future
|
|
848
|
+
// candidates with high cosine similarity will be auto-flagged
|
|
849
|
+
// for review. Forgotten facts keep their original vector — the
|
|
850
|
+
// is_bad_exemplar payload flag opts them into the centroid set
|
|
851
|
+
// without requiring a new point.
|
|
852
|
+
is_bad_exemplar: true,
|
|
853
|
+
bad_exemplar_reason: redactedReason.text,
|
|
790
854
|
});
|
|
791
855
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
792
856
|
status: "forgotten",
|
|
@@ -843,6 +907,280 @@ export function registerTools(mcp) {
|
|
|
843
907
|
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
844
908
|
}
|
|
845
909
|
});
|
|
910
|
+
// ── memory_mark_useful ──────────────────────────────────────────────────
|
|
911
|
+
mcp.tool("memory_mark_useful", [
|
|
912
|
+
"Report that a previously recalled fact actually helped you answer the user's question or complete a task.",
|
|
913
|
+
"Bumps a 'useful_count' counter on the fact and writes a telemetry feedback_event row that future ranking work can aggregate.",
|
|
914
|
+
"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.",
|
|
915
|
+
].join(" "), {
|
|
916
|
+
fact_id: z.string().describe("ID of the fact that was useful (from memory_recall or memory_entity)."),
|
|
917
|
+
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."),
|
|
918
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
919
|
+
}, async ({ fact_id, note, workspace_id }) => {
|
|
920
|
+
const guard = requireReady();
|
|
921
|
+
if (guard)
|
|
922
|
+
return guard;
|
|
923
|
+
const now = nowISO();
|
|
924
|
+
try {
|
|
925
|
+
const scope = resolveScope(workspace_id);
|
|
926
|
+
const writable = await getPointForWorkspaceWrite(fact_id, scope);
|
|
927
|
+
if (writable.error) {
|
|
928
|
+
return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
|
|
929
|
+
}
|
|
930
|
+
const existingPt = writable.point;
|
|
931
|
+
const currentCount = existingPt?.payload.useful_count ?? 0;
|
|
932
|
+
const newCount = currentCount + 1;
|
|
933
|
+
await qdrantSetPayload([fact_id], {
|
|
934
|
+
useful_count: newCount,
|
|
935
|
+
last_useful_at: now,
|
|
936
|
+
updated_at: now,
|
|
937
|
+
});
|
|
938
|
+
// Write a telemetry feedback_event row so the signal is also visible
|
|
939
|
+
// to aggregations and review tooling.
|
|
940
|
+
const eventId = newId();
|
|
941
|
+
const eventContent = note
|
|
942
|
+
? `Fact ${fact_id} marked useful: ${note}`
|
|
943
|
+
: `Fact ${fact_id} marked useful.`;
|
|
944
|
+
const redactedEvent = redactStorageText(eventContent);
|
|
945
|
+
const eventPayload = {
|
|
946
|
+
content: redactedEvent.text,
|
|
947
|
+
category: "observations",
|
|
948
|
+
domain: "software_engineering",
|
|
949
|
+
kind: "telemetry",
|
|
950
|
+
memory_subtype: "feedback_event",
|
|
951
|
+
layer: "memory_object",
|
|
952
|
+
entities: [],
|
|
953
|
+
source: "agent",
|
|
954
|
+
confidence: 1.0,
|
|
955
|
+
importance: 0.3,
|
|
956
|
+
content_hash: contentHash("feedback_event", `${fact_id}:useful:${now}`),
|
|
957
|
+
target_fact_id: fact_id,
|
|
958
|
+
feedback_kind: "useful",
|
|
959
|
+
created_at: now,
|
|
960
|
+
updated_at: now,
|
|
961
|
+
};
|
|
962
|
+
addWorkspacePayload(eventPayload, scope);
|
|
963
|
+
addRedactionPayload(eventPayload, redactedEvent);
|
|
964
|
+
try {
|
|
965
|
+
const eventVector = await embed(redactedEvent.text);
|
|
966
|
+
await qdrantUpsert(eventId, eventVector, eventPayload);
|
|
967
|
+
}
|
|
968
|
+
catch (e) {
|
|
969
|
+
log("WARN", `Failed to record feedback_event: ${e instanceof Error ? e.message : String(e)}`);
|
|
970
|
+
}
|
|
971
|
+
return {
|
|
972
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
973
|
+
status: "marked_useful",
|
|
974
|
+
fact_id,
|
|
975
|
+
useful_count: newCount,
|
|
976
|
+
event_id: eventId,
|
|
977
|
+
}) }],
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
catch (e) {
|
|
981
|
+
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
// ── memory_report_outcome ───────────────────────────────────────────────
|
|
985
|
+
mcp.tool("memory_report_outcome", [
|
|
986
|
+
"Report the downstream outcome of using a recalled fact — useful, misleading, irrelevant, or wrong.",
|
|
987
|
+
"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.",
|
|
988
|
+
"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).",
|
|
989
|
+
].join(" "), {
|
|
990
|
+
fact_id: z.string().describe("ID of the fact whose outcome you are reporting."),
|
|
991
|
+
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."),
|
|
992
|
+
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."),
|
|
993
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
994
|
+
}, async ({ fact_id, outcome, notes, workspace_id }) => {
|
|
995
|
+
const guard = requireReady();
|
|
996
|
+
if (guard)
|
|
997
|
+
return guard;
|
|
998
|
+
const now = nowISO();
|
|
999
|
+
try {
|
|
1000
|
+
const scope = resolveScope(workspace_id);
|
|
1001
|
+
const target = await getPointForWorkspaceWrite(fact_id, scope);
|
|
1002
|
+
if (target.error) {
|
|
1003
|
+
return { content: [{ type: "text", text: JSON.stringify(target.error, null, 2) }], isError: true };
|
|
1004
|
+
}
|
|
1005
|
+
const eventId = newId();
|
|
1006
|
+
const eventContent = notes
|
|
1007
|
+
? `Fact ${fact_id} outcome=${outcome}: ${notes}`
|
|
1008
|
+
: `Fact ${fact_id} outcome=${outcome}.`;
|
|
1009
|
+
const redactedEvent = redactStorageText(eventContent);
|
|
1010
|
+
const eventPayload = {
|
|
1011
|
+
content: redactedEvent.text,
|
|
1012
|
+
category: "observations",
|
|
1013
|
+
domain: "software_engineering",
|
|
1014
|
+
kind: "telemetry",
|
|
1015
|
+
memory_subtype: "outcome_event",
|
|
1016
|
+
layer: "memory_object",
|
|
1017
|
+
entities: [],
|
|
1018
|
+
source: "agent",
|
|
1019
|
+
confidence: 1.0,
|
|
1020
|
+
importance: outcome === "wrong" || outcome === "misleading" ? 0.6 : 0.3,
|
|
1021
|
+
content_hash: contentHash("outcome_event", `${fact_id}:${outcome}:${now}`),
|
|
1022
|
+
target_fact_id: fact_id,
|
|
1023
|
+
outcome,
|
|
1024
|
+
created_at: now,
|
|
1025
|
+
updated_at: now,
|
|
1026
|
+
};
|
|
1027
|
+
addWorkspacePayload(eventPayload, scope);
|
|
1028
|
+
addRedactionPayload(eventPayload, redactedEvent);
|
|
1029
|
+
const eventVector = await embed(redactedEvent.text);
|
|
1030
|
+
await qdrantUpsert(eventId, eventVector, eventPayload);
|
|
1031
|
+
return {
|
|
1032
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
1033
|
+
status: "outcome_recorded",
|
|
1034
|
+
fact_id,
|
|
1035
|
+
outcome,
|
|
1036
|
+
event_id: eventId,
|
|
1037
|
+
}) }],
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
catch (e) {
|
|
1041
|
+
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
// ── memory_session_summary ──────────────────────────────────────────────
|
|
1045
|
+
mcp.tool("memory_session_summary", [
|
|
1046
|
+
"Persist a compact summary of the current session — what got done, what decisions were made, what's still open.",
|
|
1047
|
+
"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.",
|
|
1048
|
+
"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.",
|
|
1049
|
+
].join(" "), {
|
|
1050
|
+
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?"),
|
|
1051
|
+
entities: z.array(z.string()).optional().describe("Lowercase entity names mentioned by the summary (services, repos, people, concepts). Used for entity-scoped recall later."),
|
|
1052
|
+
episode_id: z.string().optional().describe("Coherent activity-segment ID for grouping with related captures."),
|
|
1053
|
+
workstream_key: z.string().optional().describe("Durable continuity key for a long-running objective (survives across sessions)."),
|
|
1054
|
+
task_key: z.string().optional().describe("Task or issue key (e.g. GitHub issue number, JIRA key)."),
|
|
1055
|
+
repo: z.string().optional().describe("Repository or project surface this summary relates to."),
|
|
1056
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
1057
|
+
}, async ({ content, entities, episode_id, workstream_key, task_key, repo, workspace_id }) => {
|
|
1058
|
+
const guard = requireReady();
|
|
1059
|
+
if (guard)
|
|
1060
|
+
return guard;
|
|
1061
|
+
lastStoreTime = Date.now();
|
|
1062
|
+
const now = nowISO();
|
|
1063
|
+
try {
|
|
1064
|
+
const scope = resolveScope(workspace_id);
|
|
1065
|
+
const normalizedEntities = (entities ?? []).map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
1066
|
+
const summaryId = newId();
|
|
1067
|
+
const redactedContent = redactStorageText(content);
|
|
1068
|
+
const vector = await embed(redactedContent.text);
|
|
1069
|
+
const payload = {
|
|
1070
|
+
content: redactedContent.text,
|
|
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", redactedContent.text),
|
|
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
|
+
addRedactionPayload(payload, redactedContent);
|
|
1098
|
+
await qdrantUpsert(summaryId, vector, payload);
|
|
1099
|
+
return {
|
|
1100
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
1101
|
+
status: "summary_stored",
|
|
1102
|
+
summary_id: summaryId,
|
|
1103
|
+
workspace_id: scope.workspaceId,
|
|
1104
|
+
}) }],
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
catch (e) {
|
|
1108
|
+
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
// ── memory_distill ──────────────────────────────────────────────────────
|
|
1112
|
+
mcp.tool("memory_distill", [
|
|
1113
|
+
"Persist a distilled convention — a reusable learning, pattern, or runbook synthesized from multiple prior memories.",
|
|
1114
|
+
"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.",
|
|
1115
|
+
"Provide 'supersedes' if this distillation replaces an earlier convention. The original stays in storage but is excluded from recall.",
|
|
1116
|
+
].join(" "), {
|
|
1117
|
+
content: z.string().describe("One-sentence reusable convention or pattern. Should be self-contained and applicable beyond a single situation."),
|
|
1118
|
+
entities: z.array(z.string()).describe("Lowercase entity names this distillation applies to (services, tools, concepts)."),
|
|
1119
|
+
supersedes: z.string().optional().describe("ID of an earlier distilled fact that this one replaces. Old fact is marked superseded and excluded from recall."),
|
|
1120
|
+
task_key: z.string().optional().describe("Task or issue key associated with this learning, if relevant."),
|
|
1121
|
+
repo: z.string().optional().describe("Repository or project surface this learning applies to."),
|
|
1122
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
1123
|
+
}, async ({ content, entities, supersedes, task_key, repo, workspace_id }) => {
|
|
1124
|
+
const guard = requireReady();
|
|
1125
|
+
if (guard)
|
|
1126
|
+
return guard;
|
|
1127
|
+
lastStoreTime = Date.now();
|
|
1128
|
+
const now = nowISO();
|
|
1129
|
+
try {
|
|
1130
|
+
const scope = resolveScope(workspace_id);
|
|
1131
|
+
const normalizedEntities = entities.map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
1132
|
+
const distilledId = newId();
|
|
1133
|
+
const redactedContent = redactStorageText(content);
|
|
1134
|
+
const vector = await embed(redactedContent.text);
|
|
1135
|
+
if (supersedes) {
|
|
1136
|
+
const existing = await getPointForWorkspaceWrite(supersedes, scope);
|
|
1137
|
+
if (existing.error) {
|
|
1138
|
+
return { content: [{ type: "text", text: JSON.stringify(existing.error, null, 2) }], isError: true };
|
|
1139
|
+
}
|
|
1140
|
+
await qdrantSetPayload([supersedes], {
|
|
1141
|
+
superseded_by: distilledId,
|
|
1142
|
+
superseded_at: now,
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
const payload = {
|
|
1146
|
+
content: redactedContent.text,
|
|
1147
|
+
category: categoryForMemorySubtype("convention") ?? "observations",
|
|
1148
|
+
domain: "software_engineering",
|
|
1149
|
+
kind: "distilled",
|
|
1150
|
+
memory_subtype: "convention",
|
|
1151
|
+
layer: layerForMemorySubtype("convention") ?? "domain",
|
|
1152
|
+
entities: normalizedEntities,
|
|
1153
|
+
source: "agent",
|
|
1154
|
+
confidence: 0.9,
|
|
1155
|
+
importance: 0.7,
|
|
1156
|
+
content_hash: contentHash("distilled", redactedContent.text),
|
|
1157
|
+
reinforcement_count: 1,
|
|
1158
|
+
last_reinforced_at: now,
|
|
1159
|
+
superseded_by: null,
|
|
1160
|
+
superseded_at: null,
|
|
1161
|
+
created_at: now,
|
|
1162
|
+
updated_at: now,
|
|
1163
|
+
};
|
|
1164
|
+
if (task_key)
|
|
1165
|
+
payload["task_key"] = task_key;
|
|
1166
|
+
if (repo)
|
|
1167
|
+
payload["repo"] = repo;
|
|
1168
|
+
addWorkspacePayload(payload, scope);
|
|
1169
|
+
addRedactionPayload(payload, redactedContent);
|
|
1170
|
+
await qdrantUpsert(distilledId, vector, payload);
|
|
1171
|
+
return {
|
|
1172
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
1173
|
+
status: "distilled_stored",
|
|
1174
|
+
distilled_id: distilledId,
|
|
1175
|
+
supersedes: supersedes ?? null,
|
|
1176
|
+
workspace_id: scope.workspaceId,
|
|
1177
|
+
}) }],
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
catch (e) {
|
|
1181
|
+
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
846
1184
|
// ── memory_review ───────────────────────────────────────────────────────
|
|
847
1185
|
mcp.tool("memory_review", [
|
|
848
1186
|
"Triage facts that were extracted automatically by the bikky daemon (source='daemon').",
|