bikky 0.4.2 → 0.4.4

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 (62) hide show
  1. package/README.md +64 -37
  2. package/dist/config.d.ts +15 -1
  3. package/dist/config.js +116 -20
  4. package/dist/daemon/capture-policy.d.ts +0 -1
  5. package/dist/daemon/capture-policy.js +0 -2
  6. package/dist/daemon/consolidation.d.ts +2 -1
  7. package/dist/daemon/consolidation.js +32 -15
  8. package/dist/daemon/entity-typing.js +10 -0
  9. package/dist/daemon/episode-summary.d.ts +4 -0
  10. package/dist/daemon/episode-summary.js +39 -8
  11. package/dist/daemon/extraction.d.ts +2 -2
  12. package/dist/daemon/extraction.js +65 -22
  13. package/dist/daemon/loop.js +8 -0
  14. package/dist/daemon/maintenance-state.d.ts +1 -1
  15. package/dist/daemon/maintenance-state.js +2 -0
  16. package/dist/daemon/qdrant.d.ts +32 -10
  17. package/dist/daemon/qdrant.js +199 -60
  18. package/dist/daemon/quality-rollups.d.ts +51 -0
  19. package/dist/daemon/quality-rollups.js +378 -0
  20. package/dist/daemon/relations.d.ts +3 -3
  21. package/dist/daemon/relations.js +28 -16
  22. package/dist/daemon/session-index.d.ts +5 -0
  23. package/dist/daemon/session-index.js +36 -9
  24. package/dist/daemon/session-summary.d.ts +3 -0
  25. package/dist/daemon/session-summary.js +48 -15
  26. package/dist/daemon/staleness.js +3 -3
  27. package/dist/daemon/transcript-sources.js +3 -2
  28. package/dist/daemon/watcher.js +2 -0
  29. package/dist/daemon/workstream-summary.d.ts +4 -0
  30. package/dist/daemon/workstream-summary.js +58 -16
  31. package/dist/install.d.ts +11 -0
  32. package/dist/install.js +38 -0
  33. package/dist/lifecycle.js +7 -5
  34. package/dist/llm/embedding/index.js +2 -1
  35. package/dist/llm/embedding/providers/openai.js +8 -2
  36. package/dist/llm/embedding/providers/portkey.js +9 -2
  37. package/dist/llm/inference/index.js +2 -1
  38. package/dist/llm/util.d.ts +12 -0
  39. package/dist/llm/util.js +18 -0
  40. package/dist/mcp/helpers.d.ts +8 -0
  41. package/dist/mcp/helpers.js +36 -3
  42. package/dist/mcp/taxonomy.d.ts +9 -13
  43. package/dist/mcp/taxonomy.js +59 -42
  44. package/dist/mcp/tools.js +351 -83
  45. package/dist/mcp/types.d.ts +35 -0
  46. package/dist/package-verifier.d.ts +19 -0
  47. package/dist/package-verifier.js +83 -0
  48. package/dist/prompts/brief.d.ts +2 -2
  49. package/dist/prompts/brief.js +0 -1
  50. package/dist/prompts/extraction.js +9 -11
  51. package/dist/provenance/origin.d.ts +57 -0
  52. package/dist/provenance/origin.js +254 -0
  53. package/dist/routing-context.d.ts +16 -0
  54. package/dist/routing-context.js +55 -0
  55. package/dist/status.d.ts +1 -0
  56. package/dist/status.js +7 -1
  57. package/docs/config/fully-hosted.md +33 -13
  58. package/docs/config/hosted-models.md +33 -13
  59. package/docs/config/hosted-qdrant-local-models.md +1 -0
  60. package/docs/config/local.md +1 -0
  61. package/docs/configuration.md +42 -17
  62. package/package.json +2 -2
package/dist/mcp/tools.js CHANGED
@@ -3,7 +3,7 @@
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, 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";
6
+ import { STALENESS_DAYS, THRESHOLD_DUPLICATE, THRESHOLD_RELATED, QDRANT_INDEXES, categoryValues, categoryEnumDescription, domainValues, domainEnumDescription, kindValues, kindEnumDescription, memorySubtypeValues, memorySubtypeEnumDescription, DEFAULT_CATEGORY, DEFAULT_DOMAIN, DEFAULT_KIND, categoryForMemorySubtype, layerForMemorySubtype, normalizeCategory, normalizeDomain, normalizeKind, validateMemorySubtype, } from "./taxonomy.js";
7
7
  import { contentHash, daysSince, lastActivityDate, computeCombinedScore, buildFilter, formatFact, structuredFact, MEMORY_RECALL_EXCLUDED_KINDS, } from "./helpers.js";
8
8
  import { ready, setupError, setReady, log, embed, getEmbeddingConfig, qdrantReq, ensureCollectionsAll, qdrantUpsert, qdrantSearch, qdrantScroll, qdrantSetPayload, qdrantGetPoints, rebuildPool, hasPool, listDestinations, resolveDest, findPointById, } from "./api.js";
9
9
  import { DestinationNotFoundError } from "../routing.js";
@@ -11,8 +11,9 @@ import { saveConfig, loadConfig, EXTRACTION_HEALTH_PATH } from "../config.js";
11
11
  import { availableSearchScopes, resolveSearchScope, SearchScopeNotFoundError, } from "../search-scope.js";
12
12
  import { existsSync, readFileSync } from "node:fs";
13
13
  import { inspectWatcherPaths, formatIssue, repairSuspiciousWatcherPaths } from "../daemon/watcher-health.js";
14
- import { normalizeActorId, resolveActorIdentity } from "../provenance/actor.js";
14
+ import { buildOperationOrigin } from "../provenance/origin.js";
15
15
  import { addRedactionPayload, combineRedactions, redactStorageText, } from "../privacy/redaction.js";
16
+ import { buildMemoryRoutingInput } from "../routing-context.js";
16
17
  // ---------------------------------------------------------------------------
17
18
  // Runtime state
18
19
  // ---------------------------------------------------------------------------
@@ -31,6 +32,19 @@ function nowISO() {
31
32
  function newId() {
32
33
  return crypto.randomUUID();
33
34
  }
35
+ function outcomeCounterField(outcome) {
36
+ if (outcome === "useful")
37
+ return "useful_count";
38
+ if (outcome === "misleading")
39
+ return "misleading_count";
40
+ if (outcome === "wrong")
41
+ return "wrong_count";
42
+ return "irrelevant_count";
43
+ }
44
+ function payloadCounter(payload, field) {
45
+ const value = payload[field];
46
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
47
+ }
34
48
  // Build a RoutingInput from the standard memory-tool fields.
35
49
  function routingInput(args) {
36
50
  return {
@@ -41,6 +55,37 @@ function routingInput(args) {
41
55
  metadata: args.metadata,
42
56
  };
43
57
  }
58
+ function compactMetadata(values) {
59
+ const metadata = {};
60
+ for (const [key, value] of Object.entries(values)) {
61
+ if (value === undefined || value === null)
62
+ continue;
63
+ metadata[key] = value;
64
+ }
65
+ return metadata;
66
+ }
67
+ function memoryWriteRoutingInput(args) {
68
+ const relationMetadata = args.relation ? {
69
+ from_entity: args.relation.from,
70
+ relation_type: args.relation.type,
71
+ to_entity: args.relation.to,
72
+ } : {};
73
+ return buildMemoryRoutingInput({
74
+ destination: args.destination,
75
+ cwd: process.cwd(),
76
+ content: args.content,
77
+ entities: args.entities,
78
+ metadata: args.metadata,
79
+ context: compactMetadata({
80
+ origin_interface: "mcp",
81
+ origin_action: "create",
82
+ origin_tool: args.tool,
83
+ ...(args.context ?? {}),
84
+ ...relationMetadata,
85
+ }),
86
+ extraContent: args.relation ? [args.relation] : [],
87
+ });
88
+ }
44
89
  // Resolve a destination from a routing input, returning either the destination
45
90
  // or an MCP error result if the override is invalid / no destinations exist.
46
91
  function resolveDestOrError(input) {
@@ -82,6 +127,77 @@ function formatScopedFact(point, includeDestination) {
82
127
  const formatted = formatFact(point);
83
128
  return includeDestination ? `[${point._destination.name}] ${formatted}` : formatted;
84
129
  }
130
+ async function recordRecallTelemetry(args) {
131
+ const now = nowISO();
132
+ const byDestination = new Map();
133
+ for (const point of args.ranked) {
134
+ const existing = byDestination.get(point._destination.name);
135
+ if (existing) {
136
+ existing.points.push(point);
137
+ }
138
+ else {
139
+ byDestination.set(point._destination.name, { dest: point._destination, points: [point] });
140
+ }
141
+ }
142
+ for (const { dest, points } of byDestination.values()) {
143
+ const returnedFactIds = points.map((point) => point.id);
144
+ const recallOrigin = mcpOrigin({
145
+ action: "recall",
146
+ tool: "memory_recall",
147
+ metadata: {
148
+ destination: dest.name,
149
+ result_count: points.length,
150
+ search_scope: args.searchScope.name,
151
+ },
152
+ });
153
+ for (const point of points) {
154
+ const currentCount = point.payload.recall_count ?? 0;
155
+ try {
156
+ await qdrantSetPayload(dest, [point.id], {
157
+ recall_count: currentCount + 1,
158
+ last_recalled_at: now,
159
+ updated_at: now,
160
+ last_operation_origin: recallOrigin,
161
+ });
162
+ }
163
+ catch (e) {
164
+ log("WARN", `Failed to update recall_count for ${point.id}: ${e instanceof Error ? e.message : String(e)}`);
165
+ }
166
+ }
167
+ const entities = [...new Set(points.flatMap((point) => point.payload.entities ?? []))].slice(0, 25);
168
+ const eventContent = `Recall returned ${points.length} fact(s) from ${dest.name}: ${args.redactedQuery.text}`;
169
+ const redactedEvent = redactStorageText(eventContent);
170
+ const eventPayload = {
171
+ content: redactedEvent.text,
172
+ category: categoryForMemorySubtype("recall_event") ?? "system",
173
+ domain: "software_engineering",
174
+ kind: "telemetry",
175
+ memory_subtype: "recall_event",
176
+ layer: "memory_object",
177
+ entities,
178
+ origin: recallOrigin,
179
+ confidence: 1.0,
180
+ importance: 0.3,
181
+ content_hash: contentHash("recall_event", `${dest.name}:${returnedFactIds.join(",")}:${now}`),
182
+ recall_query: args.redactedQuery.text,
183
+ returned_fact_ids: returnedFactIds,
184
+ result_count: points.length,
185
+ search_scope: args.searchScope.name,
186
+ searched_destinations: args.searchedDestinations,
187
+ failed_destinations: args.failedDestinations.map((failure) => `${failure.destination}: ${failure.error}`),
188
+ created_at: now,
189
+ updated_at: now,
190
+ };
191
+ addRedactionPayload(eventPayload, redactedEvent);
192
+ try {
193
+ const eventVector = await embed(redactedEvent.text);
194
+ await qdrantUpsert(dest, newId(), eventVector, eventPayload);
195
+ }
196
+ catch (e) {
197
+ log("WARN", `Failed to record recall_event: ${e instanceof Error ? e.message : String(e)}`);
198
+ }
199
+ }
200
+ }
85
201
  function resolveSearchScopeOrError(args) {
86
202
  if (args.destination && args.search_scope !== undefined) {
87
203
  return {
@@ -126,21 +242,17 @@ function resolveSearchScopeOrError(args) {
126
242
  };
127
243
  }
128
244
  }
129
- // Add actor identity payload fields. Workspace was removed in v0.4.0 — physical
130
- // separation now happens via routing destinations (see routing.ts).
131
- function addActorPayload(payload, actor, actorIdOverride) {
132
- const actorId = actor?.actor_id ?? normalizeActorId(actorIdOverride);
133
- if (actorId)
134
- payload["actor_id"] = actorId;
135
- if (actor?.actor_label) {
136
- const metadata = payload["metadata"] && typeof payload["metadata"] === "object" && !Array.isArray(payload["metadata"])
137
- ? payload["metadata"]
138
- : {};
139
- metadata["actor_label"] = actor.actor_label;
140
- if (actor.source)
141
- metadata["actor_source"] = actor.source;
142
- payload["metadata"] = metadata;
143
- }
245
+ function mcpOrigin(input) {
246
+ return buildOperationOrigin({
247
+ interface: "mcp",
248
+ agentType: "coding_agent",
249
+ action: input.action,
250
+ tool: input.tool,
251
+ outcome: input.outcome,
252
+ config: loadConfig(),
253
+ cwd: process.cwd(),
254
+ metadata: input.metadata,
255
+ });
144
256
  }
145
257
  async function getPointForWrite(dest, factId) {
146
258
  const existing = await qdrantGetPoints(dest, [factId]);
@@ -198,9 +310,8 @@ function buildMemoryNudge() {
198
310
  // session typically produces. The agent picks the best fit.
199
311
  return `🧠 Memory nudge: No memory_store calls in ${mins} minutes. ` +
200
312
  "Reflect on what's worth persisting:\n" +
201
- " • engineering — codebase maps, architecture, infra, ops, troubleshooting?\n" +
313
+ " • engineering — codebase maps, architecture, infra, ops, troubleshooting, preferences, ownership, working agreements, activity events?\n" +
202
314
  " • product — requirements, decisions, workflows, roadmap, metrics, market insight?\n" +
203
- " • human — preferences, owners, working agreements, durable activity events?\n" +
204
315
  " • system — session, episode, workstream, or quality-rollup memory?\n" +
205
316
  "If yes, call memory_store now so future sessions inherit the knowledge.";
206
317
  }
@@ -524,14 +635,12 @@ export function registerTools(mcp) {
524
635
  memory_subtype: z.enum(memorySubtypeValues()).optional().describe(memorySubtypeEnumDescription()),
525
636
  workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op. Routing now uses destinations — see destination."),
526
637
  destination: z.string().optional().describe("Optional destination override. When set, routes to that destination by name. Hard-errors if no such destination exists. Omit to let routing rules in ~/.bikky/config.json decide based on cwd/entities/content/metadata."),
527
- actor_id: z.string().optional().describe("Stable actor/person/agent identity associated with this capture. Overrides identity config/env/Git-derived fallback for this write."),
528
638
  episode_id: z.string().optional().describe("Coherent activity-segment ID. Group facts captured during the same coherent task or transcript."),
529
639
  workstream_key: z.string().optional().describe("Durable continuity key for a long-running objective (survives across sessions)."),
530
640
  task_key: z.string().optional().describe("Task or issue key (e.g. GitHub issue number, JIRA key)."),
531
641
  repo: z.string().optional().describe("Repository or project surface this fact relates to (e.g. 'bikky-dev/bikky')."),
532
642
  branch: z.string().optional().describe("Branch or working surface (e.g. 'main', 'feat/x')."),
533
643
  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."),
534
- source: z.enum(sourceValues()).default(DEFAULT_SOURCE).describe(sourceEnumDescription()),
535
644
  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."),
536
645
  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."),
537
646
  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."),
@@ -541,22 +650,12 @@ export function registerTools(mcp) {
541
650
  to: z.string().describe("Target entity (lowercase)."),
542
651
  }).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."),
543
652
  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)."),
544
- }, async ({ content, category, entities, domain, kind, memory_subtype, workspace_id: _workspace_id, destination, actor_id, episode_id, workstream_key, task_key, repo, branch, review_status, source, confidence, importance, supersedes, relation, metadata, }) => {
653
+ }, async ({ content, category, entities, domain, kind, memory_subtype, workspace_id: _workspace_id, destination, episode_id, workstream_key, task_key, repo, branch, review_status, confidence, importance, supersedes, relation, metadata, }) => {
545
654
  const guard = requireReady();
546
655
  if (guard)
547
656
  return guard;
548
657
  lastStoreTime = Date.now();
549
658
  const now = nowISO();
550
- const resolved = resolveDestOrError(routingInput({
551
- destination,
552
- content,
553
- entities,
554
- metadata,
555
- }));
556
- if (resolved.error)
557
- return resolved.error;
558
- const dest = resolved.dest;
559
- const actor = resolveActorIdentity({ actorId: actor_id, config: loadConfig() });
560
659
  const normalizedKind = normalizeKind(kind);
561
660
  let normalizedSubtype = null;
562
661
  try {
@@ -599,6 +698,44 @@ export function registerTools(mcp) {
599
698
  type: redactedRelation.type.text,
600
699
  to: redactedRelation.to.text,
601
700
  } : null;
701
+ const resolved = resolveDestOrError(memoryWriteRoutingInput({
702
+ tool: "memory_store",
703
+ destination,
704
+ content: redactedContent.text,
705
+ entities: normalizedEntities,
706
+ metadata,
707
+ relation: sanitizedRelation,
708
+ context: {
709
+ category: normalizedCategory,
710
+ domain: normalizedDomain,
711
+ kind: normalizedKind,
712
+ memory_subtype: normalizedSubtype,
713
+ layer: normalizedLayer,
714
+ episode_id,
715
+ workstream_key,
716
+ task_key,
717
+ repo,
718
+ branch,
719
+ review_status,
720
+ supersedes,
721
+ },
722
+ }));
723
+ if (resolved.error)
724
+ return resolved.error;
725
+ const dest = resolved.dest;
726
+ const createOrigin = mcpOrigin({
727
+ action: "create",
728
+ tool: "memory_store",
729
+ metadata: {
730
+ destination: dest.name,
731
+ category: normalizedCategory,
732
+ kind: normalizedKind,
733
+ ...(normalizedSubtype ? { memory_subtype: normalizedSubtype } : {}),
734
+ ...(task_key ? { task_key } : {}),
735
+ ...(repo ? { repo } : {}),
736
+ ...(branch ? { branch } : {}),
737
+ },
738
+ });
602
739
  // 1. Exact dedup via content hash
603
740
  try {
604
741
  const hashFilter = buildFilter({}) ?? { must: [] };
@@ -612,6 +749,12 @@ export function registerTools(mcp) {
612
749
  reinforcement_count: count,
613
750
  last_reinforced_at: now,
614
751
  updated_at: now,
752
+ last_operation_origin: mcpOrigin({
753
+ action: "reinforce",
754
+ tool: "memory_store",
755
+ outcome: "exact_duplicate",
756
+ metadata: { destination: dest.name, fact_id: point.id },
757
+ }),
615
758
  });
616
759
  return {
617
760
  content: [{ type: "text", text: JSON.stringify({
@@ -647,6 +790,12 @@ export function registerTools(mcp) {
647
790
  reinforcement_count: count,
648
791
  last_reinforced_at: now,
649
792
  updated_at: now,
793
+ last_operation_origin: mcpOrigin({
794
+ action: "reinforce",
795
+ tool: "memory_store",
796
+ outcome: "semantic_duplicate",
797
+ metadata: { destination: dest.name, fact_id: point.id, similarity: topScore },
798
+ }),
650
799
  });
651
800
  return {
652
801
  content: [{ type: "text", text: JSON.stringify({
@@ -700,6 +849,12 @@ export function registerTools(mcp) {
700
849
  await qdrantSetPayload(dest, [supersedes], {
701
850
  superseded_by: factId,
702
851
  superseded_at: now,
852
+ updated_at: now,
853
+ last_operation_origin: mcpOrigin({
854
+ action: "supersede",
855
+ tool: "memory_store",
856
+ metadata: { destination: dest.name, new_fact_id: factId },
857
+ }),
703
858
  });
704
859
  }
705
860
  catch (e) {
@@ -713,7 +868,7 @@ export function registerTools(mcp) {
713
868
  domain: normalizedDomain,
714
869
  kind: normalizedKind,
715
870
  entities: normalizedEntities,
716
- source,
871
+ origin: createOrigin,
717
872
  confidence,
718
873
  importance: importance ?? 0.5,
719
874
  content_hash: hash,
@@ -745,7 +900,6 @@ export function registerTools(mcp) {
745
900
  }
746
901
  if (review_status)
747
902
  payload["review_status"] = review_status;
748
- addActorPayload(payload, actor);
749
903
  addRedactionPayload(payload, factRedactionSummary);
750
904
  await qdrantUpsert(dest, factId, vector, payload);
751
905
  // 7. Insert relation point if provided
@@ -761,7 +915,15 @@ export function registerTools(mcp) {
761
915
  kind: "relation",
762
916
  layer: "memory_object",
763
917
  entities: [sanitizedRelation.from.toLowerCase(), sanitizedRelation.to.toLowerCase()],
764
- source,
918
+ origin: mcpOrigin({
919
+ action: "create",
920
+ tool: "memory_store",
921
+ metadata: {
922
+ destination: dest.name,
923
+ parent_fact_id: factId,
924
+ relation_type: sanitizedRelation.type.toLowerCase(),
925
+ },
926
+ }),
765
927
  confidence,
766
928
  content_hash: contentHash("relation", relContent),
767
929
  reinforcement_count: 1,
@@ -774,7 +936,6 @@ export function registerTools(mcp) {
774
936
  relation_type: sanitizedRelation.type.toLowerCase(),
775
937
  to_entity: sanitizedRelation.to.toLowerCase(),
776
938
  };
777
- addActorPayload(relPayload, actor);
778
939
  addRedactionPayload(relPayload, relationRedactionSummary);
779
940
  await qdrantUpsert(dest, relationId, relVector, relPayload);
780
941
  }
@@ -782,9 +943,8 @@ export function registerTools(mcp) {
782
943
  action: "inserted",
783
944
  fact_id: factId,
784
945
  destination: dest.name,
946
+ origin: createOrigin,
785
947
  };
786
- if (actor.actor_id)
787
- result["actor_id"] = actor.actor_id;
788
948
  if (relationId)
789
949
  result["relation_id"] = relationId;
790
950
  if (redactionSummary.redacted)
@@ -819,7 +979,9 @@ export function registerTools(mcp) {
819
979
  workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
820
980
  destination: z.string().optional().describe("Optional legacy single-destination override. Do not combine with search_scope. Prefer search_scope for routed/all/list search."),
821
981
  search_scope: searchScopeSchema.describe("Optional read/search scope. Accepts 'routed', 'all', a destination name, a configured scope name, a comma-separated destination list, or an array of destination names. Omit to use config.default_search_scope."),
822
- actor_id: z.string().optional().describe("Filter to facts captured by or associated with this stable actor identity. Optional."),
982
+ origin_user_id: z.string().optional().describe("Filter to facts whose creation origin.user.id matches this value. Optional."),
983
+ origin_agent_id: z.string().optional().describe("Filter to facts whose creation origin.agent.id matches this value. Optional."),
984
+ origin_interface: z.enum(["mcp", "daemon", "ui", "api", "cli", "system"]).optional().describe("Filter to facts created through this origin interface. Optional."),
823
985
  include_legacy_workspace: z.boolean().optional().describe("[Removed in v0.4.0] No-op."),
824
986
  entity: z.string().optional().describe("Restrict to facts mentioning this entity (case-insensitive). For full entity context prefer memory_entity."),
825
987
  episode_id: z.string().optional().describe("Filter by coherent episode ID."),
@@ -834,13 +996,12 @@ export function registerTools(mcp) {
834
996
  graph_depth: z.number().optional().default(0).describe("Entity-graph traversal depth. 0 = vector search only (fast, default). 1 = also surface up to ceil(limit / 2) extra 1-hop entity-related facts (slower; use when the user asks 'what's connected to X?'). In JSON output these are returned separately as related."),
835
997
  output_format: z.enum(["text", "json"]).optional().default("text").describe("Response format. text = backward-compatible human-readable lines (default). json = parseable object with query, limit metadata, results, related, counts, and optional nudge."),
836
998
  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)."),
837
- }, async ({ query, category, domain, kind, memory_subtype, workspace_id: _workspace_id, destination, search_scope, actor_id, include_legacy_workspace: _include_legacy_workspace, entity, episode_id, workstream_key, task_key, repo, branch, review_status, since, until, limit, graph_depth, output_format, metadata_filter, }) => {
999
+ }, async ({ query, category, domain, kind, memory_subtype, workspace_id: _workspace_id, destination, search_scope, origin_user_id, origin_agent_id, origin_interface, include_legacy_workspace: _include_legacy_workspace, entity, episode_id, workstream_key, task_key, repo, branch, review_status, since, until, limit, graph_depth, output_format, metadata_filter, }) => {
838
1000
  const guard = requireReady();
839
1001
  if (guard)
840
1002
  return guard;
841
1003
  const requestedLimit = limit ?? MEMORY_RECALL_DEFAULT_LIMIT;
842
1004
  const effectiveLimit = clampRecallLimit(limit);
843
- const actorFilter = resolveActorIdentity({ actorId: actor_id, useGitFallback: false });
844
1005
  const scopeResolved = resolveSearchScopeOrError({
845
1006
  destination,
846
1007
  search_scope,
@@ -873,7 +1034,9 @@ export function registerTools(mcp) {
873
1034
  domain: domain ? normalizeDomain(domain) : undefined,
874
1035
  kind: normalizedKind,
875
1036
  memory_subtype: normalizedSubtype,
876
- actor_id: actorFilter.actor_id,
1037
+ origin_user_id,
1038
+ origin_agent_id,
1039
+ origin_interface,
877
1040
  entity,
878
1041
  episode_id,
879
1042
  workstream_key,
@@ -947,6 +1110,13 @@ export function registerTools(mcp) {
947
1110
  .map((r) => ({ ...r, _combinedScore: computeCombinedScore(r) }))
948
1111
  .sort((a, b) => b._combinedScore - a._combinedScore)
949
1112
  .slice(0, effectiveLimit);
1113
+ await recordRecallTelemetry({
1114
+ ranked,
1115
+ redactedQuery,
1116
+ searchScope,
1117
+ searchedDestinations,
1118
+ failedDestinations,
1119
+ });
950
1120
  const includeDestination = searchScope.destinations.length > 1 || searchScope.name === "all";
951
1121
  const lines = ranked.map((r) => formatScopedFact(r, includeDestination));
952
1122
  const related = { points: [], errors: [] };
@@ -1239,6 +1409,11 @@ export function registerTools(mcp) {
1239
1409
  superseded_by: `forgotten:${redactedReason.text}`,
1240
1410
  superseded_at: now,
1241
1411
  updated_at: now,
1412
+ last_operation_origin: mcpOrigin({
1413
+ action: "forget",
1414
+ tool: "memory_forget",
1415
+ metadata: { destination: dest.name, fact_id },
1416
+ }),
1242
1417
  // Mark this fact's vector as a bad-exemplar centroid: future
1243
1418
  // candidates with high cosine similarity will be auto-flagged
1244
1419
  // for review. Forgotten facts keep their original vector — the
@@ -1284,6 +1459,11 @@ export function registerTools(mcp) {
1284
1459
  last_reinforced_at: now,
1285
1460
  verification_count: newCount,
1286
1461
  updated_at: now,
1462
+ last_operation_origin: mcpOrigin({
1463
+ action: "verify",
1464
+ tool: "memory_verify",
1465
+ metadata: { destination: dest.name, fact_id },
1466
+ }),
1287
1467
  });
1288
1468
  return {
1289
1469
  content: [{ type: "text", text: JSON.stringify({
@@ -1318,13 +1498,19 @@ export function registerTools(mcp) {
1318
1498
  if (!located)
1319
1499
  return notFoundResult(fact_id);
1320
1500
  const { dest, point } = located;
1321
- const actor = resolveActorIdentity({ config: loadConfig() });
1322
1501
  const currentCount = point.payload.useful_count ?? 0;
1323
1502
  const newCount = currentCount + 1;
1503
+ const feedbackOrigin = mcpOrigin({
1504
+ action: "feedback",
1505
+ tool: "memory_mark_useful",
1506
+ outcome: "useful",
1507
+ metadata: { destination: dest.name, fact_id },
1508
+ });
1324
1509
  await qdrantSetPayload(dest, [fact_id], {
1325
1510
  useful_count: newCount,
1326
1511
  last_useful_at: now,
1327
1512
  updated_at: now,
1513
+ last_operation_origin: feedbackOrigin,
1328
1514
  });
1329
1515
  // Write a telemetry feedback_event row so the signal is also visible
1330
1516
  // to aggregations and review tooling.
@@ -1341,7 +1527,7 @@ export function registerTools(mcp) {
1341
1527
  memory_subtype: "feedback_event",
1342
1528
  layer: "memory_object",
1343
1529
  entities: [],
1344
- source: "agent",
1530
+ origin: feedbackOrigin,
1345
1531
  confidence: 1.0,
1346
1532
  importance: 0.3,
1347
1533
  content_hash: contentHash("feedback_event", `${fact_id}:useful:${now}`),
@@ -1350,7 +1536,6 @@ export function registerTools(mcp) {
1350
1536
  created_at: now,
1351
1537
  updated_at: now,
1352
1538
  };
1353
- addActorPayload(eventPayload, actor);
1354
1539
  addRedactionPayload(eventPayload, redactedEvent);
1355
1540
  try {
1356
1541
  const eventVector = await embed(redactedEvent.text);
@@ -1392,8 +1577,20 @@ export function registerTools(mcp) {
1392
1577
  const located = await locatePoint(fact_id);
1393
1578
  if (!located)
1394
1579
  return notFoundResult(fact_id);
1395
- const { dest } = located;
1396
- const actor = resolveActorIdentity({ config: loadConfig() });
1580
+ const { dest, point } = located;
1581
+ const counterField = outcomeCounterField(outcome);
1582
+ const counterValue = payloadCounter(point.payload, counterField) + 1;
1583
+ const feedbackOrigin = mcpOrigin({
1584
+ action: "feedback",
1585
+ tool: "memory_report_outcome",
1586
+ outcome,
1587
+ metadata: { destination: dest.name, fact_id },
1588
+ });
1589
+ await qdrantSetPayload(dest, [fact_id], {
1590
+ [counterField]: counterValue,
1591
+ updated_at: now,
1592
+ last_operation_origin: feedbackOrigin,
1593
+ });
1397
1594
  const eventId = newId();
1398
1595
  const eventContent = notes
1399
1596
  ? `Fact ${fact_id} outcome=${outcome}: ${notes}`
@@ -1407,7 +1604,7 @@ export function registerTools(mcp) {
1407
1604
  memory_subtype: "outcome_event",
1408
1605
  layer: "memory_object",
1409
1606
  entities: [],
1410
- source: "agent",
1607
+ origin: feedbackOrigin,
1411
1608
  confidence: 1.0,
1412
1609
  importance: outcome === "wrong" || outcome === "misleading" ? 0.6 : 0.3,
1413
1610
  content_hash: contentHash("outcome_event", `${fact_id}:${outcome}:${now}`),
@@ -1416,7 +1613,6 @@ export function registerTools(mcp) {
1416
1613
  created_at: now,
1417
1614
  updated_at: now,
1418
1615
  };
1419
- addActorPayload(eventPayload, actor);
1420
1616
  addRedactionPayload(eventPayload, redactedEvent);
1421
1617
  const eventVector = await embed(redactedEvent.text);
1422
1618
  await qdrantUpsert(dest, eventId, eventVector, eventPayload);
@@ -1426,6 +1622,7 @@ export function registerTools(mcp) {
1426
1622
  fact_id,
1427
1623
  destination: dest.name,
1428
1624
  outcome,
1625
+ [counterField]: counterValue,
1429
1626
  event_id: eventId,
1430
1627
  }) }],
1431
1628
  };
@@ -1437,7 +1634,7 @@ export function registerTools(mcp) {
1437
1634
  // ── memory_session_summary ──────────────────────────────────────────────
1438
1635
  mcp.tool("memory_session_summary", [
1439
1636
  "Persist a compact summary of the current session — what got done, what decisions were made, what's still open.",
1440
- "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.",
1637
+ "Stored as kind='summary', memory_subtype='session_index' with canonical origin metadata. Keep it short (target 30-80 words). Future sessions retrieve these via memory_recall to bootstrap context faster than re-reading the original transcript.",
1441
1638
  "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.",
1442
1639
  ].join(" "), {
1443
1640
  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?"),
@@ -1448,27 +1645,48 @@ export function registerTools(mcp) {
1448
1645
  repo: z.string().optional().describe("Repository or project surface this summary relates to."),
1449
1646
  workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
1450
1647
  destination: z.string().optional().describe("Optional destination override. Omit to let routing rules decide."),
1451
- actor_id: z.string().optional().describe("Stable actor identity associated with this session summary. Overrides identity config/env/Git fallback."),
1452
- }, async ({ content, entities, episode_id, workstream_key, task_key, repo, workspace_id: _workspace_id, destination, actor_id }) => {
1648
+ }, async ({ content, entities, episode_id, workstream_key, task_key, repo, workspace_id: _workspace_id, destination }) => {
1453
1649
  const guard = requireReady();
1454
1650
  if (guard)
1455
1651
  return guard;
1456
1652
  lastStoreTime = Date.now();
1457
1653
  const now = nowISO();
1458
1654
  try {
1459
- const resolved = resolveDestOrError(routingInput({
1655
+ const normalizedEntities = (entities ?? []).map((e) => e.trim().toLowerCase()).filter(Boolean);
1656
+ const redactedContent = redactStorageText(content);
1657
+ const resolved = resolveDestOrError(memoryWriteRoutingInput({
1658
+ tool: "memory_session_summary",
1460
1659
  destination,
1461
- content,
1462
- entities: entities ?? [],
1660
+ content: redactedContent.text,
1661
+ entities: normalizedEntities,
1662
+ context: {
1663
+ category: categoryForMemorySubtype("session_index") ?? "system",
1664
+ domain: "software_engineering",
1665
+ kind: "summary",
1666
+ memory_subtype: "session_index",
1667
+ layer: layerForMemorySubtype("session_index") ?? "episode",
1668
+ episode_id,
1669
+ workstream_key,
1670
+ task_key,
1671
+ repo,
1672
+ },
1463
1673
  }));
1464
1674
  if (resolved.error)
1465
1675
  return resolved.error;
1466
1676
  const dest = resolved.dest;
1467
- const actor = resolveActorIdentity({ actorId: actor_id, config: loadConfig() });
1468
- const normalizedEntities = (entities ?? []).map((e) => e.trim().toLowerCase()).filter(Boolean);
1469
1677
  const summaryId = newId();
1470
- const redactedContent = redactStorageText(content);
1471
1678
  const vector = await embed(redactedContent.text);
1679
+ const origin = mcpOrigin({
1680
+ action: "create",
1681
+ tool: "memory_session_summary",
1682
+ metadata: {
1683
+ destination: dest.name,
1684
+ ...(episode_id ? { episode_id } : {}),
1685
+ ...(workstream_key ? { workstream_key } : {}),
1686
+ ...(task_key ? { task_key } : {}),
1687
+ ...(repo ? { repo } : {}),
1688
+ },
1689
+ });
1472
1690
  const payload = {
1473
1691
  content: redactedContent.text,
1474
1692
  category: categoryForMemorySubtype("session_index") ?? "system",
@@ -1477,7 +1695,7 @@ export function registerTools(mcp) {
1477
1695
  memory_subtype: "session_index",
1478
1696
  layer: layerForMemorySubtype("session_index") ?? "episode",
1479
1697
  entities: normalizedEntities,
1480
- source: "agent",
1698
+ origin,
1481
1699
  confidence: 0.9,
1482
1700
  importance: 0.6,
1483
1701
  content_hash: contentHash("summary", redactedContent.text),
@@ -1496,7 +1714,6 @@ export function registerTools(mcp) {
1496
1714
  payload["task_key"] = task_key;
1497
1715
  if (repo)
1498
1716
  payload["repo"] = repo;
1499
- addActorPayload(payload, actor);
1500
1717
  addRedactionPayload(payload, redactedContent);
1501
1718
  await qdrantUpsert(dest, summaryId, vector, payload);
1502
1719
  return {
@@ -1504,7 +1721,7 @@ export function registerTools(mcp) {
1504
1721
  status: "summary_stored",
1505
1722
  summary_id: summaryId,
1506
1723
  destination: dest.name,
1507
- actor_id: actor.actor_id,
1724
+ origin,
1508
1725
  }) }],
1509
1726
  };
1510
1727
  }
@@ -1515,7 +1732,7 @@ export function registerTools(mcp) {
1515
1732
  // ── memory_distill ──────────────────────────────────────────────────────
1516
1733
  mcp.tool("memory_distill", [
1517
1734
  "Persist a distilled convention — a reusable learning, pattern, or runbook synthesized from multiple prior memories.",
1518
- "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.",
1735
+ "Stored as kind='distilled', memory_subtype='convention' with canonical origin metadata. 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.",
1519
1736
  "Provide 'supersedes' if this distillation replaces an earlier convention. The original stays in storage but is excluded from recall.",
1520
1737
  ].join(" "), {
1521
1738
  content: z.string().describe("One-sentence reusable convention or pattern. Should be self-contained and applicable beyond a single situation."),
@@ -1525,27 +1742,46 @@ export function registerTools(mcp) {
1525
1742
  repo: z.string().optional().describe("Repository or project surface this learning applies to."),
1526
1743
  workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
1527
1744
  destination: z.string().optional().describe("Optional destination override. Omit to let routing rules decide."),
1528
- actor_id: z.string().optional().describe("Stable actor identity associated with this distillation. Overrides identity config/env/Git fallback."),
1529
- }, async ({ content, entities, supersedes, task_key, repo, workspace_id: _workspace_id, destination, actor_id }) => {
1745
+ }, async ({ content, entities, supersedes, task_key, repo, workspace_id: _workspace_id, destination }) => {
1530
1746
  const guard = requireReady();
1531
1747
  if (guard)
1532
1748
  return guard;
1533
1749
  lastStoreTime = Date.now();
1534
1750
  const now = nowISO();
1535
1751
  try {
1536
- const resolved = resolveDestOrError(routingInput({
1752
+ const normalizedEntities = entities.map((e) => e.trim().toLowerCase()).filter(Boolean);
1753
+ const redactedContent = redactStorageText(content);
1754
+ const resolved = resolveDestOrError(memoryWriteRoutingInput({
1755
+ tool: "memory_distill",
1537
1756
  destination,
1538
- content,
1539
- entities,
1757
+ content: redactedContent.text,
1758
+ entities: normalizedEntities,
1759
+ context: {
1760
+ category: categoryForMemorySubtype("convention") ?? "engineering",
1761
+ domain: "software_engineering",
1762
+ kind: "distilled",
1763
+ memory_subtype: "convention",
1764
+ layer: layerForMemorySubtype("convention") ?? "domain",
1765
+ task_key,
1766
+ repo,
1767
+ supersedes,
1768
+ },
1540
1769
  }));
1541
1770
  if (resolved.error)
1542
1771
  return resolved.error;
1543
1772
  const dest = resolved.dest;
1544
- const actor = resolveActorIdentity({ actorId: actor_id, config: loadConfig() });
1545
- const normalizedEntities = entities.map((e) => e.trim().toLowerCase()).filter(Boolean);
1546
1773
  const distilledId = newId();
1547
- const redactedContent = redactStorageText(content);
1548
1774
  const vector = await embed(redactedContent.text);
1775
+ const origin = mcpOrigin({
1776
+ action: "create",
1777
+ tool: "memory_distill",
1778
+ metadata: {
1779
+ destination: dest.name,
1780
+ ...(task_key ? { task_key } : {}),
1781
+ ...(repo ? { repo } : {}),
1782
+ ...(supersedes ? { supersedes } : {}),
1783
+ },
1784
+ });
1549
1785
  if (supersedes) {
1550
1786
  // Supersede may live in a different destination — locate it.
1551
1787
  const located = await locatePoint(supersedes);
@@ -1554,6 +1790,12 @@ export function registerTools(mcp) {
1554
1790
  await qdrantSetPayload(located.dest, [supersedes], {
1555
1791
  superseded_by: distilledId,
1556
1792
  superseded_at: now,
1793
+ updated_at: now,
1794
+ last_operation_origin: mcpOrigin({
1795
+ action: "supersede",
1796
+ tool: "memory_distill",
1797
+ metadata: { destination: located.dest.name, new_fact_id: distilledId },
1798
+ }),
1557
1799
  });
1558
1800
  }
1559
1801
  const payload = {
@@ -1564,7 +1806,7 @@ export function registerTools(mcp) {
1564
1806
  memory_subtype: "convention",
1565
1807
  layer: layerForMemorySubtype("convention") ?? "domain",
1566
1808
  entities: normalizedEntities,
1567
- source: "agent",
1809
+ origin,
1568
1810
  confidence: 0.9,
1569
1811
  importance: 0.7,
1570
1812
  content_hash: contentHash("distilled", redactedContent.text),
@@ -1579,7 +1821,6 @@ export function registerTools(mcp) {
1579
1821
  payload["task_key"] = task_key;
1580
1822
  if (repo)
1581
1823
  payload["repo"] = repo;
1582
- addActorPayload(payload, actor);
1583
1824
  addRedactionPayload(payload, redactedContent);
1584
1825
  await qdrantUpsert(dest, distilledId, vector, payload);
1585
1826
  return {
@@ -1588,7 +1829,7 @@ export function registerTools(mcp) {
1588
1829
  distilled_id: distilledId,
1589
1830
  destination: dest.name,
1590
1831
  supersedes: supersedes ?? null,
1591
- actor_id: actor.actor_id,
1832
+ origin,
1592
1833
  }) }],
1593
1834
  };
1594
1835
  }
@@ -1598,8 +1839,8 @@ export function registerTools(mcp) {
1598
1839
  });
1599
1840
  // ── memory_review ───────────────────────────────────────────────────────
1600
1841
  mcp.tool("memory_review", [
1601
- "Triage facts that were captured automatically by Bikky (source='system').",
1602
- "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 system-captured facts), approve (mark verified), reject (mark superseded with reason), correct (replace with edited content as a new fact).",
1842
+ "Triage facts that were captured automatically by Bikky (origin.interface='daemon' or legacy source='system').",
1843
+ "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/system-captured facts), approve (mark verified), reject (mark superseded with reason), correct (replace with edited content as a new fact).",
1603
1844
  ].join(" "), {
1604
1845
  limit: z.number().optional().default(10).describe("Max facts to return when action=list (default 10)."),
1605
1846
  action: z.enum(["list", "approve", "reject", "correct"]).optional().default("list").describe("What to do. list = show recent system-captured 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')."),
@@ -1616,7 +1857,14 @@ export function registerTools(mcp) {
1616
1857
  // List spans all destinations.
1617
1858
  const destinations = listDestinations();
1618
1859
  const allPoints = [];
1619
- const filter = { must: [{ key: "source", match: { any: ["system", "daemon"] } }] };
1860
+ const filter = {
1861
+ must: [],
1862
+ should: [
1863
+ { key: "origin.interface", match: { value: "daemon" } },
1864
+ { key: "origin.agent.type", match: { any: ["daemon", "system"] } },
1865
+ { key: "source", match: { any: ["system", "daemon"] } },
1866
+ ],
1867
+ };
1620
1868
  for (const dest of destinations) {
1621
1869
  try {
1622
1870
  const result = await qdrantScroll(dest, filter, (limit ?? 10) * 2);
@@ -1654,6 +1902,12 @@ export function registerTools(mcp) {
1654
1902
  last_verified_at: now,
1655
1903
  verification_count: currentCount + 1,
1656
1904
  updated_at: now,
1905
+ last_operation_origin: mcpOrigin({
1906
+ action: "review",
1907
+ tool: "memory_review",
1908
+ outcome: "approved",
1909
+ metadata: { destination: dest.name, fact_id },
1910
+ }),
1657
1911
  });
1658
1912
  return { content: [{ type: "text", text: JSON.stringify({ status: "approved", fact_id, destination: dest.name }) }] };
1659
1913
  }
@@ -1670,6 +1924,12 @@ export function registerTools(mcp) {
1670
1924
  superseded_by: `rejected:${redactedReason.text}`,
1671
1925
  superseded_at: now,
1672
1926
  updated_at: now,
1927
+ last_operation_origin: mcpOrigin({
1928
+ action: "review",
1929
+ tool: "memory_review",
1930
+ outcome: "rejected",
1931
+ metadata: { destination: dest.name, fact_id },
1932
+ }),
1673
1933
  });
1674
1934
  return { content: [{ type: "text", text: JSON.stringify({
1675
1935
  status: "rejected",
@@ -1689,11 +1949,15 @@ export function registerTools(mcp) {
1689
1949
  const { dest, point } = located;
1690
1950
  const origPayload = point.payload;
1691
1951
  const redactedCorrected = redactStorageText(corrected_content);
1692
- const actor = resolveActorIdentity({ config: loadConfig() });
1693
1952
  const vector = await embed(redactedCorrected.text);
1694
1953
  const correctedId = crypto.randomUUID();
1695
1954
  const origCategory = normalizeCategory(origPayload?.category ?? DEFAULT_CATEGORY);
1696
1955
  const hash = contentHash(origCategory, redactedCorrected.text);
1956
+ const correctOrigin = mcpOrigin({
1957
+ action: "correct",
1958
+ tool: "memory_review",
1959
+ metadata: { destination: dest.name, corrected_from: fact_id },
1960
+ });
1697
1961
  const correctedPayload = {
1698
1962
  content: redactedCorrected.text,
1699
1963
  category: origCategory,
@@ -1707,7 +1971,7 @@ export function registerTools(mcp) {
1707
1971
  ...(origPayload?.repo ? { repo: origPayload.repo } : {}),
1708
1972
  ...(origPayload?.branch ? { branch: origPayload.branch } : {}),
1709
1973
  entities: origPayload?.entities ?? [],
1710
- source: "user",
1974
+ origin: correctOrigin,
1711
1975
  confidence: 0.95,
1712
1976
  importance: origPayload?.importance ?? 0.5,
1713
1977
  content_hash: hash,
@@ -1719,13 +1983,18 @@ export function registerTools(mcp) {
1719
1983
  updated_at: now,
1720
1984
  metadata: { ...(origPayload?.metadata ?? {}), corrected_from: fact_id },
1721
1985
  };
1722
- addActorPayload(correctedPayload, actor);
1723
1986
  addRedactionPayload(correctedPayload, redactedCorrected);
1724
1987
  await qdrantUpsert(dest, correctedId, vector, correctedPayload);
1725
1988
  await qdrantSetPayload(dest, [fact_id], {
1726
1989
  superseded_by: correctedId,
1727
1990
  superseded_at: now,
1728
1991
  updated_at: now,
1992
+ last_operation_origin: mcpOrigin({
1993
+ action: "supersede",
1994
+ tool: "memory_review",
1995
+ outcome: "corrected",
1996
+ metadata: { destination: dest.name, new_fact_id: correctedId },
1997
+ }),
1729
1998
  });
1730
1999
  return { content: [{ type: "text", text: JSON.stringify({ status: "corrected", old_fact_id: fact_id, new_fact_id: correctedId, destination: dest.name }) }] };
1731
2000
  }
@@ -1745,7 +2014,7 @@ export function registerTools(mcp) {
1745
2014
  try {
1746
2015
  const staleThreshold = new Date(Date.now() - STALENESS_DAYS * 86400000).toISOString();
1747
2016
  const staleFilter = { must: [] };
1748
- staleFilter.must.push({ key: "category", match: { any: ["engineering", "product", "human", "system"] } });
2017
+ staleFilter.must.push({ key: "category", match: { any: ["engineering", "product", "system"] } });
1749
2018
  staleFilter.should = [
1750
2019
  { key: "last_reinforced_at", range: { lte: staleThreshold } },
1751
2020
  { is_null: { key: "last_reinforced_at" } },
@@ -1784,10 +2053,9 @@ export function registerTools(mcp) {
1784
2053
  }
1785
2054
  }
1786
2055
  sections.push("🔍 Reflect: think about the LAST 10 minutes of work and answer in your head:\n" +
1787
- " 1. Did you touch engineering context: code, infra, ops, access, troubleshooting, or conventions?\n" +
2056
+ " 1. Did you touch engineering context: code, infra, ops, access, troubleshooting, conventions, preferences, ownership, working agreements, or durable activity events?\n" +
1788
2057
  " 2. Did you capture product context: a requirement, decision, workflow, roadmap item, metric, or market insight?\n" +
1789
- " 3. Did you learn human context: a preference, owner, working agreement, person profile, or durable activity event?\n" +
1790
- " 4. Did the work produce system context: session, episode, workstream, recall, feedback, outcome, or rollup state?\n" +
2058
+ " 3. Did the work produce system context: session, episode, workstream, recall, feedback, outcome, or rollup state?\n" +
1791
2059
  "If any answer is yes, call memory_store now — one atomic fact per item, with category/domain/entities.");
1792
2060
  return { content: [{ type: "text", text: sections.join("\n\n") }] };
1793
2061
  });