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.
Files changed (218) hide show
  1. package/README.md +26 -6
  2. package/dist/cli.js +26 -6
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +22 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +255 -3
  7. package/dist/config.js.map +1 -1
  8. package/dist/config.test.d.ts +3 -2
  9. package/dist/config.test.d.ts.map +1 -1
  10. package/dist/config.test.js +95 -6
  11. package/dist/config.test.js.map +1 -1
  12. package/dist/daemon/capture-policy.d.ts +4 -4
  13. package/dist/daemon/capture-policy.d.ts.map +1 -1
  14. package/dist/daemon/capture-policy.js +8 -17
  15. package/dist/daemon/capture-policy.js.map +1 -1
  16. package/dist/daemon/capture-policy.test.js +2 -2
  17. package/dist/daemon/capture-policy.test.js.map +1 -1
  18. package/dist/daemon/consolidation.d.ts +4 -1
  19. package/dist/daemon/consolidation.d.ts.map +1 -1
  20. package/dist/daemon/consolidation.js +18 -4
  21. package/dist/daemon/consolidation.js.map +1 -1
  22. package/dist/daemon/entity-typing.d.ts +42 -0
  23. package/dist/daemon/entity-typing.d.ts.map +1 -0
  24. package/dist/daemon/entity-typing.js +295 -0
  25. package/dist/daemon/entity-typing.js.map +1 -0
  26. package/dist/daemon/entity-typing.test.d.ts +2 -0
  27. package/dist/daemon/entity-typing.test.d.ts.map +1 -0
  28. package/dist/daemon/entity-typing.test.js +50 -0
  29. package/dist/daemon/entity-typing.test.js.map +1 -0
  30. package/dist/daemon/episode-summary.d.ts +4 -8
  31. package/dist/daemon/episode-summary.d.ts.map +1 -1
  32. package/dist/daemon/episode-summary.js +52 -18
  33. package/dist/daemon/episode-summary.js.map +1 -1
  34. package/dist/daemon/episode-summary.test.js +10 -7
  35. package/dist/daemon/episode-summary.test.js.map +1 -1
  36. package/dist/daemon/extraction-quality.test.d.ts +2 -0
  37. package/dist/daemon/extraction-quality.test.d.ts.map +1 -0
  38. package/dist/daemon/extraction-quality.test.js +283 -0
  39. package/dist/daemon/extraction-quality.test.js.map +1 -0
  40. package/dist/daemon/extraction-rules.d.ts +131 -0
  41. package/dist/daemon/extraction-rules.d.ts.map +1 -0
  42. package/dist/daemon/extraction-rules.js +321 -0
  43. package/dist/daemon/extraction-rules.js.map +1 -0
  44. package/dist/daemon/extraction-rules.test.d.ts +2 -0
  45. package/dist/daemon/extraction-rules.test.d.ts.map +1 -0
  46. package/dist/daemon/extraction-rules.test.js +183 -0
  47. package/dist/daemon/extraction-rules.test.js.map +1 -0
  48. package/dist/daemon/extraction.d.ts +19 -1
  49. package/dist/daemon/extraction.d.ts.map +1 -1
  50. package/dist/daemon/extraction.js +183 -26
  51. package/dist/daemon/extraction.js.map +1 -1
  52. package/dist/daemon/extraction.test.js +96 -2
  53. package/dist/daemon/extraction.test.js.map +1 -1
  54. package/dist/daemon/loop.d.ts.map +1 -1
  55. package/dist/daemon/loop.js +22 -0
  56. package/dist/daemon/loop.js.map +1 -1
  57. package/dist/daemon/loop.test.d.ts +2 -0
  58. package/dist/daemon/loop.test.d.ts.map +1 -0
  59. package/dist/daemon/loop.test.js +85 -0
  60. package/dist/daemon/loop.test.js.map +1 -0
  61. package/dist/daemon/maintenance-state.d.ts +36 -0
  62. package/dist/daemon/maintenance-state.d.ts.map +1 -0
  63. package/dist/daemon/maintenance-state.js +95 -0
  64. package/dist/daemon/maintenance-state.js.map +1 -0
  65. package/dist/daemon/maintenance-state.test.d.ts +2 -0
  66. package/dist/daemon/maintenance-state.test.d.ts.map +1 -0
  67. package/dist/daemon/maintenance-state.test.js +56 -0
  68. package/dist/daemon/maintenance-state.test.js.map +1 -0
  69. package/dist/daemon/qdrant.d.ts +37 -1
  70. package/dist/daemon/qdrant.d.ts.map +1 -1
  71. package/dist/daemon/qdrant.js +107 -12
  72. package/dist/daemon/qdrant.js.map +1 -1
  73. package/dist/daemon/qdrant.test.js +57 -1
  74. package/dist/daemon/qdrant.test.js.map +1 -1
  75. package/dist/daemon/relations-vocab.d.ts +44 -0
  76. package/dist/daemon/relations-vocab.d.ts.map +1 -0
  77. package/dist/daemon/relations-vocab.js +168 -0
  78. package/dist/daemon/relations-vocab.js.map +1 -0
  79. package/dist/daemon/relations-vocab.test.d.ts +2 -0
  80. package/dist/daemon/relations-vocab.test.d.ts.map +1 -0
  81. package/dist/daemon/relations-vocab.test.js +69 -0
  82. package/dist/daemon/relations-vocab.test.js.map +1 -0
  83. package/dist/daemon/relations.d.ts +49 -34
  84. package/dist/daemon/relations.d.ts.map +1 -1
  85. package/dist/daemon/relations.js +249 -153
  86. package/dist/daemon/relations.js.map +1 -1
  87. package/dist/daemon/relations.test.d.ts +2 -0
  88. package/dist/daemon/relations.test.d.ts.map +1 -0
  89. package/dist/daemon/relations.test.js +36 -0
  90. package/dist/daemon/relations.test.js.map +1 -0
  91. package/dist/daemon/session-index.d.ts +1 -8
  92. package/dist/daemon/session-index.d.ts.map +1 -1
  93. package/dist/daemon/session-index.js +8 -10
  94. package/dist/daemon/session-index.js.map +1 -1
  95. package/dist/daemon/session-index.test.js +15 -9
  96. package/dist/daemon/session-index.test.js.map +1 -1
  97. package/dist/daemon/session-summary.d.ts +1 -8
  98. package/dist/daemon/session-summary.d.ts.map +1 -1
  99. package/dist/daemon/session-summary.js +17 -12
  100. package/dist/daemon/session-summary.js.map +1 -1
  101. package/dist/daemon/session-summary.test.js +5 -3
  102. package/dist/daemon/session-summary.test.js.map +1 -1
  103. package/dist/daemon/watcher-health.d.ts +20 -0
  104. package/dist/daemon/watcher-health.d.ts.map +1 -0
  105. package/dist/daemon/watcher-health.js +78 -0
  106. package/dist/daemon/watcher-health.js.map +1 -0
  107. package/dist/daemon/watcher-health.test.d.ts +5 -0
  108. package/dist/daemon/watcher-health.test.d.ts.map +1 -0
  109. package/dist/daemon/watcher-health.test.js +96 -0
  110. package/dist/daemon/watcher-health.test.js.map +1 -0
  111. package/dist/daemon/watcher.test.d.ts +3 -2
  112. package/dist/daemon/watcher.test.d.ts.map +1 -1
  113. package/dist/daemon/watcher.test.js +9 -19
  114. package/dist/daemon/watcher.test.js.map +1 -1
  115. package/dist/daemon/workstream-resolver.d.ts +76 -0
  116. package/dist/daemon/workstream-resolver.d.ts.map +1 -0
  117. package/dist/daemon/workstream-resolver.js +180 -0
  118. package/dist/daemon/workstream-resolver.js.map +1 -0
  119. package/dist/daemon/workstream-resolver.test.d.ts +2 -0
  120. package/dist/daemon/workstream-resolver.test.d.ts.map +1 -0
  121. package/dist/daemon/workstream-resolver.test.js +128 -0
  122. package/dist/daemon/workstream-resolver.test.js.map +1 -0
  123. package/dist/daemon/workstream-summary.d.ts +1 -8
  124. package/dist/daemon/workstream-summary.d.ts.map +1 -1
  125. package/dist/daemon/workstream-summary.js +22 -14
  126. package/dist/daemon/workstream-summary.js.map +1 -1
  127. package/dist/daemon/workstream-summary.test.js +9 -6
  128. package/dist/daemon/workstream-summary.test.js.map +1 -1
  129. package/dist/lib/qdrant-client.d.ts +34 -0
  130. package/dist/lib/qdrant-client.d.ts.map +1 -1
  131. package/dist/lib/qdrant-client.js +54 -0
  132. package/dist/lib/qdrant-client.js.map +1 -1
  133. package/dist/lib/qdrant-client.test.js +49 -1
  134. package/dist/lib/qdrant-client.test.js.map +1 -1
  135. package/dist/llm/inference/index.d.ts +2 -1
  136. package/dist/llm/inference/index.d.ts.map +1 -1
  137. package/dist/llm/inference/index.js +37 -2
  138. package/dist/llm/inference/index.js.map +1 -1
  139. package/dist/llm/inference/index.test.js +44 -3
  140. package/dist/llm/inference/index.test.js.map +1 -1
  141. package/dist/llm/inference/providers/bedrock.d.ts +23 -0
  142. package/dist/llm/inference/providers/bedrock.d.ts.map +1 -1
  143. package/dist/llm/inference/providers/bedrock.js +10 -1
  144. package/dist/llm/inference/providers/bedrock.js.map +1 -1
  145. package/dist/llm/inference/providers/bedrock.test.js +49 -2
  146. package/dist/llm/inference/providers/bedrock.test.js.map +1 -1
  147. package/dist/llm/inference/providers/ollama.d.ts.map +1 -1
  148. package/dist/llm/inference/providers/ollama.js +7 -1
  149. package/dist/llm/inference/providers/ollama.js.map +1 -1
  150. package/dist/llm/inference/providers/openai.d.ts.map +1 -1
  151. package/dist/llm/inference/providers/openai.js +7 -1
  152. package/dist/llm/inference/providers/openai.js.map +1 -1
  153. package/dist/llm/inference/providers/openai.test.js +38 -2
  154. package/dist/llm/inference/providers/openai.test.js.map +1 -1
  155. package/dist/llm/inference/providers/portkey.d.ts.map +1 -1
  156. package/dist/llm/inference/providers/portkey.js +7 -1
  157. package/dist/llm/inference/providers/portkey.js.map +1 -1
  158. package/dist/llm/inference/types.d.ts +15 -0
  159. package/dist/llm/inference/types.d.ts.map +1 -1
  160. package/dist/llm/telemetry.d.ts +8 -1
  161. package/dist/llm/telemetry.d.ts.map +1 -1
  162. package/dist/llm/telemetry.js.map +1 -1
  163. package/dist/mcp/helpers.d.ts.map +1 -1
  164. package/dist/mcp/helpers.js +17 -2
  165. package/dist/mcp/helpers.js.map +1 -1
  166. package/dist/mcp/taxonomy.d.ts +6 -18
  167. package/dist/mcp/taxonomy.d.ts.map +1 -1
  168. package/dist/mcp/taxonomy.js +15 -25
  169. package/dist/mcp/taxonomy.js.map +1 -1
  170. package/dist/mcp/taxonomy.test.js +23 -7
  171. package/dist/mcp/taxonomy.test.js.map +1 -1
  172. package/dist/mcp/tools.d.ts.map +1 -1
  173. package/dist/mcp/tools.js +355 -17
  174. package/dist/mcp/tools.js.map +1 -1
  175. package/dist/mcp/tools.test.js +279 -5
  176. package/dist/mcp/tools.test.js.map +1 -1
  177. package/dist/privacy/redaction.d.ts +21 -0
  178. package/dist/privacy/redaction.d.ts.map +1 -0
  179. package/dist/privacy/redaction.js +83 -0
  180. package/dist/privacy/redaction.js.map +1 -0
  181. package/dist/privacy/redaction.test.d.ts +2 -0
  182. package/dist/privacy/redaction.test.d.ts.map +1 -0
  183. package/dist/privacy/redaction.test.js +51 -0
  184. package/dist/privacy/redaction.test.js.map +1 -0
  185. package/dist/prompts/distill.d.ts.map +1 -1
  186. package/dist/prompts/distill.js +3 -2
  187. package/dist/prompts/distill.js.map +1 -1
  188. package/dist/prompts/entity-typing.d.ts +18 -0
  189. package/dist/prompts/entity-typing.d.ts.map +1 -0
  190. package/dist/prompts/entity-typing.js +60 -0
  191. package/dist/prompts/entity-typing.js.map +1 -0
  192. package/dist/prompts/episode-summary.d.ts.map +1 -1
  193. package/dist/prompts/episode-summary.js +17 -3
  194. package/dist/prompts/episode-summary.js.map +1 -1
  195. package/dist/prompts/extraction.d.ts.map +1 -1
  196. package/dist/prompts/extraction.js +114 -5
  197. package/dist/prompts/extraction.js.map +1 -1
  198. package/dist/prompts/index.d.ts +1 -0
  199. package/dist/prompts/index.d.ts.map +1 -1
  200. package/dist/prompts/index.js +1 -0
  201. package/dist/prompts/index.js.map +1 -1
  202. package/dist/prompts/relations.d.ts.map +1 -1
  203. package/dist/prompts/relations.js +72 -4
  204. package/dist/prompts/relations.js.map +1 -1
  205. package/dist/render.d.ts.map +1 -1
  206. package/dist/render.js +7 -1
  207. package/dist/render.js.map +1 -1
  208. package/dist/render.test.js +3 -2
  209. package/dist/render.test.js.map +1 -1
  210. package/dist/status.d.ts +94 -0
  211. package/dist/status.d.ts.map +1 -0
  212. package/dist/status.js +378 -0
  213. package/dist/status.js.map +1 -0
  214. package/dist/status.test.d.ts +5 -0
  215. package/dist/status.test.d.ts.map +1 -0
  216. package/dist/status.test.js +203 -0
  217. package/dist/status.test.js.map +1 -0
  218. 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 redactionSummary = combineRedactions([
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, redactionSummary);
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, redactionSummary);
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
- output.push(`## Facts about ${name} (${factPoints.length})`);
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').",