bikky 0.3.7 → 0.3.9

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 (109) hide show
  1. package/README.md +21 -5
  2. package/dist/config.d.ts +6 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +15 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/config.test.js +30 -0
  7. package/dist/config.test.js.map +1 -1
  8. package/dist/daemon/capture-policy.d.ts +2 -2
  9. package/dist/daemon/capture-policy.d.ts.map +1 -1
  10. package/dist/daemon/capture-policy.js +15 -10
  11. package/dist/daemon/capture-policy.js.map +1 -1
  12. package/dist/daemon/capture-policy.test.js +7 -5
  13. package/dist/daemon/capture-policy.test.js.map +1 -1
  14. package/dist/daemon/consolidation.d.ts +1 -1
  15. package/dist/daemon/consolidation.d.ts.map +1 -1
  16. package/dist/daemon/consolidation.js +24 -17
  17. package/dist/daemon/consolidation.js.map +1 -1
  18. package/dist/daemon/entity-typing.test.js +1 -1
  19. package/dist/daemon/entity-typing.test.js.map +1 -1
  20. package/dist/daemon/episode-summary.js +2 -2
  21. package/dist/daemon/episode-summary.js.map +1 -1
  22. package/dist/daemon/extraction-quality.test.js +4 -4
  23. package/dist/daemon/extraction-quality.test.js.map +1 -1
  24. package/dist/daemon/extraction-rules.d.ts +8 -8
  25. package/dist/daemon/extraction-rules.d.ts.map +1 -1
  26. package/dist/daemon/extraction-rules.js +57 -14
  27. package/dist/daemon/extraction-rules.js.map +1 -1
  28. package/dist/daemon/extraction-rules.test.js +32 -12
  29. package/dist/daemon/extraction-rules.test.js.map +1 -1
  30. package/dist/daemon/extraction.d.ts +6 -2
  31. package/dist/daemon/extraction.d.ts.map +1 -1
  32. package/dist/daemon/extraction.js +58 -14
  33. package/dist/daemon/extraction.js.map +1 -1
  34. package/dist/daemon/extraction.test.js +31 -6
  35. package/dist/daemon/extraction.test.js.map +1 -1
  36. package/dist/daemon/loop.d.ts.map +1 -1
  37. package/dist/daemon/loop.js.map +1 -1
  38. package/dist/daemon/loop.test.js +1 -1
  39. package/dist/daemon/loop.test.js.map +1 -1
  40. package/dist/daemon/qdrant.d.ts.map +1 -1
  41. package/dist/daemon/qdrant.js +2 -2
  42. package/dist/daemon/qdrant.js.map +1 -1
  43. package/dist/daemon/qdrant.test.js +8 -8
  44. package/dist/daemon/qdrant.test.js.map +1 -1
  45. package/dist/daemon/relations.d.ts +1 -1
  46. package/dist/daemon/relations.js +4 -4
  47. package/dist/daemon/relations.js.map +1 -1
  48. package/dist/daemon/relations.test.js +1 -1
  49. package/dist/daemon/relations.test.js.map +1 -1
  50. package/dist/daemon/session-index.js +2 -2
  51. package/dist/daemon/session-index.js.map +1 -1
  52. package/dist/daemon/session-summary.d.ts.map +1 -1
  53. package/dist/daemon/session-summary.js +5 -29
  54. package/dist/daemon/session-summary.js.map +1 -1
  55. package/dist/daemon/session-summary.test.js +3 -3
  56. package/dist/daemon/session-summary.test.js.map +1 -1
  57. package/dist/daemon/staleness.js +1 -1
  58. package/dist/daemon/staleness.js.map +1 -1
  59. package/dist/daemon/staleness.test.js +2 -2
  60. package/dist/daemon/staleness.test.js.map +1 -1
  61. package/dist/daemon/watcher.test.js +1 -1
  62. package/dist/daemon/watcher.test.js.map +1 -1
  63. package/dist/daemon/workstream-summary.js +2 -2
  64. package/dist/daemon/workstream-summary.js.map +1 -1
  65. package/dist/lib/qdrant-client.test.js.map +1 -1
  66. package/dist/llm/inference/index.d.ts +1 -1
  67. package/dist/llm/inference/index.d.ts.map +1 -1
  68. package/dist/mcp/helpers.d.ts +39 -0
  69. package/dist/mcp/helpers.d.ts.map +1 -1
  70. package/dist/mcp/helpers.js +53 -0
  71. package/dist/mcp/helpers.js.map +1 -1
  72. package/dist/mcp/helpers.test.js +35 -35
  73. package/dist/mcp/helpers.test.js.map +1 -1
  74. package/dist/mcp/index.js +1 -1
  75. package/dist/mcp/index.js.map +1 -1
  76. package/dist/mcp/taxonomy.d.ts +55 -58
  77. package/dist/mcp/taxonomy.d.ts.map +1 -1
  78. package/dist/mcp/taxonomy.js +169 -148
  79. package/dist/mcp/taxonomy.js.map +1 -1
  80. package/dist/mcp/taxonomy.test.js +43 -24
  81. package/dist/mcp/taxonomy.test.js.map +1 -1
  82. package/dist/mcp/tools.d.ts.map +1 -1
  83. package/dist/mcp/tools.integration.itest.js +4 -5
  84. package/dist/mcp/tools.integration.itest.js.map +1 -1
  85. package/dist/mcp/tools.js +130 -54
  86. package/dist/mcp/tools.js.map +1 -1
  87. package/dist/mcp/tools.test.js +159 -23
  88. package/dist/mcp/tools.test.js.map +1 -1
  89. package/dist/prompts/brief.d.ts +2 -2
  90. package/dist/prompts/brief.d.ts.map +1 -1
  91. package/dist/prompts/brief.js +5 -7
  92. package/dist/prompts/brief.js.map +1 -1
  93. package/dist/prompts/distill.js +3 -3
  94. package/dist/prompts/extraction.d.ts.map +1 -1
  95. package/dist/prompts/extraction.js +70 -31
  96. package/dist/prompts/extraction.js.map +1 -1
  97. package/dist/prompts/prompts.test.js +4 -4
  98. package/dist/prompts/prompts.test.js.map +1 -1
  99. package/dist/provenance/actor.d.ts +17 -0
  100. package/dist/provenance/actor.d.ts.map +1 -0
  101. package/dist/provenance/actor.js +82 -0
  102. package/dist/provenance/actor.js.map +1 -0
  103. package/dist/provenance/actor.test.d.ts +2 -0
  104. package/dist/provenance/actor.test.d.ts.map +1 -0
  105. package/dist/provenance/actor.test.js +49 -0
  106. package/dist/provenance/actor.test.js.map +1 -0
  107. package/dist/render.test.js +4 -4
  108. package/dist/render.test.js.map +1 -1
  109. package/package.json +4 -1
package/dist/mcp/tools.js CHANGED
@@ -4,16 +4,19 @@
4
4
  import crypto from "node:crypto";
5
5
  import { z } from "zod";
6
6
  import { STALENESS_DAYS, THRESHOLD_DUPLICATE, THRESHOLD_RELATED, QDRANT_INDEXES, categoryValues, categoryEnumDescription, domainValues, domainEnumDescription, kindValues, kindEnumDescription, memorySubtypeValues, memorySubtypeEnumDescription, sourceValues, sourceEnumDescription, DEFAULT_CATEGORY, DEFAULT_DOMAIN, DEFAULT_KIND, DEFAULT_SOURCE, categoryForMemorySubtype, layerForMemorySubtype, normalizeCategory, normalizeDomain, normalizeKind, validateMemorySubtype, } from "./taxonomy.js";
7
- import { contentHash, daysSince, lastActivityDate, computeCombinedScore, buildFilter, formatFact, MEMORY_RECALL_EXCLUDED_KINDS, } from "./helpers.js";
7
+ import { contentHash, daysSince, lastActivityDate, computeCombinedScore, buildFilter, formatFact, structuredFact, 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
9
  import { saveConfig, loadConfig, EXTRACTION_HEALTH_PATH } from "../config.js";
10
10
  import { existsSync, readFileSync } from "node:fs";
11
11
  import { inspectWatcherPaths, formatIssue, repairSuspiciousWatcherPaths } from "../daemon/watcher-health.js";
12
+ import { normalizeActorId, resolveActorIdentity } from "../provenance/actor.js";
12
13
  import { addRedactionPayload, combineRedactions, redactStorageText, } from "../privacy/redaction.js";
13
14
  // ---------------------------------------------------------------------------
14
15
  // Runtime state
15
16
  // ---------------------------------------------------------------------------
16
17
  const NUDGE_INTERVAL_MS = 10 * 60 * 1000;
18
+ const MEMORY_RECALL_DEFAULT_LIMIT = 10;
19
+ const MEMORY_RECALL_MAX_LIMIT = 50;
17
20
  let lastStoreTime = Date.now();
18
21
  let heartbeatCount = 0;
19
22
  // ---------------------------------------------------------------------------
@@ -25,9 +28,10 @@ function nowISO() {
25
28
  function newId() {
26
29
  return crypto.randomUUID();
27
30
  }
28
- function resolveScope(workspaceId, includeLegacyWorkspace = false) {
31
+ function resolveScope(workspaceId, includeLegacyWorkspace = false, actorId) {
29
32
  return {
30
33
  workspaceId: workspaceId?.trim() || undefined,
34
+ actorId: normalizeActorId(actorId),
31
35
  includeLegacy: includeLegacyWorkspace,
32
36
  };
33
37
  }
@@ -38,11 +42,21 @@ function scopedFilter(scope, extra = {}) {
38
42
  includeLegacyWorkspace: scope.includeLegacy,
39
43
  });
40
44
  }
41
- function addWorkspacePayload(payload, scope) {
45
+ function addWorkspacePayload(payload, scope, actor) {
42
46
  if (scope.workspaceId)
43
47
  payload["workspace_id"] = scope.workspaceId;
44
- if (scope.actorId)
45
- payload["actor_id"] = scope.actorId;
48
+ const actorId = actor?.actor_id ?? scope.actorId;
49
+ if (actorId)
50
+ payload["actor_id"] = actorId;
51
+ if (actor?.actor_label) {
52
+ const metadata = payload["metadata"] && typeof payload["metadata"] === "object" && !Array.isArray(payload["metadata"])
53
+ ? payload["metadata"]
54
+ : {};
55
+ metadata["actor_label"] = actor.actor_label;
56
+ if (actor.source)
57
+ metadata["actor_source"] = actor.source;
58
+ payload["metadata"] = metadata;
59
+ }
46
60
  }
47
61
  async function getPointForWorkspaceWrite(factId, _scope) {
48
62
  const existing = await qdrantGetPoints([factId]);
@@ -88,12 +102,19 @@ function buildMemoryNudge() {
88
102
  // session typically produces. The agent picks the best fit.
89
103
  return `🧠 Memory nudge: No memory_store calls in ${mins} minutes. ` +
90
104
  "Reflect on what's worth persisting:\n" +
91
- " • infrastructurenew services, ports, configs touched?\n" +
92
- " • decisionsarchitectural choices made (with rationale)?\n" +
93
- " • observationdebugging findings, gotchas, workarounds?\n" +
94
- " • projectswork-in-progress, blockers, completions?\n" +
105
+ " • engineeringcodebase maps, architecture, infra, ops, troubleshooting?\n" +
106
+ " • productrequirements, decisions, workflows, roadmap, metrics, market insight?\n" +
107
+ " • humanpreferences, owners, working agreements, durable activity events?\n" +
108
+ " • systemsession, episode, workstream, or quality-rollup memory?\n" +
95
109
  "If yes, call memory_store now so future sessions inherit the knowledge.";
96
110
  }
111
+ function clampRecallLimit(limit) {
112
+ const rawLimit = limit ?? MEMORY_RECALL_DEFAULT_LIMIT;
113
+ const integerLimit = Math.trunc(rawLimit);
114
+ if (!Number.isFinite(integerLimit))
115
+ return MEMORY_RECALL_DEFAULT_LIMIT;
116
+ return Math.min(Math.max(integerLimit, 1), MEMORY_RECALL_MAX_LIMIT);
117
+ }
97
118
  /**
98
119
  * Entity-graph traversal for memory_recall.
99
120
  */
@@ -108,7 +129,7 @@ async function graphTraversal(primaryResults, limit, scope) {
108
129
  }
109
130
  }
110
131
  if (primaryEntities.size === 0)
111
- return [];
132
+ return { points: [] };
112
133
  const relatedEntities = new Set();
113
134
  for (const entity of primaryEntities) {
114
135
  const outgoingFilter = scopedFilter(scope, { excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS }) ?? { must: [] };
@@ -129,7 +150,7 @@ async function graphTraversal(primaryResults, limit, scope) {
129
150
  for (const e of primaryEntities)
130
151
  relatedEntities.delete(e);
131
152
  if (relatedEntities.size === 0)
132
- return [];
153
+ return { points: [] };
133
154
  const relatedFacts = [];
134
155
  const maxPerEntity = Math.max(2, Math.floor(limit / relatedEntities.size));
135
156
  for (const entity of relatedEntities) {
@@ -144,12 +165,10 @@ async function graphTraversal(primaryResults, limit, scope) {
144
165
  if (relatedFacts.length >= limit)
145
166
  break;
146
167
  }
147
- return relatedFacts
148
- .slice(0, Math.ceil(limit / 2))
149
- .map((r) => formatFact(r));
168
+ return { points: relatedFacts.slice(0, Math.ceil(limit / 2)) };
150
169
  }
151
170
  catch (e) {
152
- return [`(graph traversal failed: ${e instanceof Error ? e.message : String(e)})`];
171
+ return { points: [], error: e instanceof Error ? e.message : String(e) };
153
172
  }
154
173
  }
155
174
  // ---------------------------------------------------------------------------
@@ -328,7 +347,7 @@ export function registerTools(mcp) {
328
347
  mcp.tool("memory_store", [
329
348
  "Persist one atomic fact to long-term memory.",
330
349
  "Call this whenever you learn something a future session would need: a service detail, a decision rationale, a workaround, a user preference, an ownership fact, a task-resume pointer. One fact per call — split compound observations into separate calls.",
331
- "Dedup is automatic (content hash + vector similarity), so you do NOT need to recall first. The tool returns one of: inserted (new fact), reinforced (exact or near-duplicate found — counters bumped), or — if there are similar-but-different facts — a list of potential conflicts so you can decide whether to use 'supersedes'.",
350
+ "Dedup is automatic (content hash + vector similarity), so you do NOT need to recall first for deduplication. Recall first only when you intentionally need broader context to decide whether a new fact supersedes an older one. The tool returns one of: inserted (new fact), reinforced (exact or near-duplicate found — counters bumped), or — if there are similar-but-different facts — a list of potential conflicts so you can decide whether to use 'supersedes'.",
332
351
  "To create a typed edge between two entities at the same time, set the optional 'relation' field — no separate tool call needed.",
333
352
  "Do NOT use for ephemeral state (current cursor, in-flight todo). Use the harness task folder instead.",
334
353
  ].join(" "), {
@@ -339,6 +358,7 @@ export function registerTools(mcp) {
339
358
  kind: z.enum(kindValues()).default(DEFAULT_KIND).describe(kindEnumDescription()),
340
359
  memory_subtype: z.enum(memorySubtypeValues()).optional().describe(memorySubtypeEnumDescription()),
341
360
  workspace_id: z.string().optional().describe("Workspace namespace for team-shared memory. Omit to use the default workspace from config."),
361
+ 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."),
342
362
  episode_id: z.string().optional().describe("Coherent activity-segment ID. Group facts captured during the same coherent task or transcript."),
343
363
  workstream_key: z.string().optional().describe("Durable continuity key for a long-running objective (survives across sessions)."),
344
364
  task_key: z.string().optional().describe("Task or issue key (e.g. GitHub issue number, JIRA key)."),
@@ -355,13 +375,14 @@ export function registerTools(mcp) {
355
375
  to: z.string().describe("Target entity (lowercase)."),
356
376
  }).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."),
357
377
  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)."),
358
- }, async ({ content, category, entities, domain, kind, memory_subtype, workspace_id, episode_id, workstream_key, task_key, repo, branch, review_status, source, confidence, importance, supersedes, relation, metadata, }) => {
378
+ }, async ({ content, category, entities, domain, kind, memory_subtype, workspace_id, actor_id, episode_id, workstream_key, task_key, repo, branch, review_status, source, confidence, importance, supersedes, relation, metadata, }) => {
359
379
  const guard = requireReady();
360
380
  if (guard)
361
381
  return guard;
362
382
  lastStoreTime = Date.now();
363
383
  const now = nowISO();
364
384
  const scope = resolveScope(workspace_id);
385
+ const actor = resolveActorIdentity({ actorId: actor_id, config: loadConfig() });
365
386
  const normalizedKind = normalizeKind(kind);
366
387
  let normalizedSubtype = null;
367
388
  try {
@@ -545,13 +566,13 @@ export function registerTools(mcp) {
545
566
  payload["repo"] = repo;
546
567
  if (branch)
547
568
  payload["branch"] = branch;
548
- if (review_status)
549
- payload["review_status"] = review_status;
550
- addWorkspacePayload(payload, scope);
551
- addRedactionPayload(payload, factRedactionSummary);
552
569
  if (metadata && Object.keys(metadata).length > 0) {
553
570
  payload["metadata"] = metadata;
554
571
  }
572
+ if (review_status)
573
+ payload["review_status"] = review_status;
574
+ addWorkspacePayload(payload, scope, actor);
575
+ addRedactionPayload(payload, factRedactionSummary);
555
576
  await qdrantUpsert(factId, vector, payload);
556
577
  // 7. Insert relation point if provided
557
578
  let relationId = null;
@@ -579,7 +600,7 @@ export function registerTools(mcp) {
579
600
  relation_type: sanitizedRelation.type.toLowerCase(),
580
601
  to_entity: sanitizedRelation.to.toLowerCase(),
581
602
  };
582
- addWorkspacePayload(relPayload, scope);
603
+ addWorkspacePayload(relPayload, scope, actor);
583
604
  addRedactionPayload(relPayload, relationRedactionSummary);
584
605
  await qdrantUpsert(relationId, relVector, relPayload);
585
606
  }
@@ -588,6 +609,8 @@ export function registerTools(mcp) {
588
609
  fact_id: factId,
589
610
  workspace_id: scope.workspaceId,
590
611
  };
612
+ if (actor.actor_id)
613
+ result["actor_id"] = actor.actor_id;
591
614
  if (relationId)
592
615
  result["relation_id"] = relationId;
593
616
  if (redactionSummary.redacted)
@@ -608,9 +631,10 @@ export function registerTools(mcp) {
608
631
  "Three main uses:",
609
632
  " 1. Session-start briefing — broad query like 'session briefing: user preferences, active projects, recent decisions'.",
610
633
  " 2. Per-prompt contextual recall — focused query derived from what the user just asked.",
611
- " 3. Pre-store conflict check — recall similar facts before storing, so you can use 'supersedes' if the new fact replaces an older one.",
634
+ " 3. Conflict/replacement check — recall similar facts when you suspect new information may supersede an older fact. Deduplication during memory_store is automatic.",
612
635
  "Combine the natural-language query with structured filters (category, domain, entity, date range, metadata) for tighter results.",
613
636
  "If you have a known entity name and want everything about it, prefer memory_entity. For 'what does X own/use?' style questions, prefer memory_relations.",
637
+ `By default output is human-readable text. Use output_format=json for machine-parseable results with separate results and related arrays. Default limit is ${MEMORY_RECALL_DEFAULT_LIMIT}; maximum effective limit is ${MEMORY_RECALL_MAX_LIMIT}.`,
614
638
  ].join("\n"), {
615
639
  query: z.string().describe("Natural-language description of what you're looking for. Embedded and matched semantically — full sentences work better than keyword lists."),
616
640
  category: z.string().optional().describe("Filter by category (same vocabulary as memory_store.category). Optional."),
@@ -618,6 +642,7 @@ export function registerTools(mcp) {
618
642
  kind: z.string().optional().describe("Filter by kind: fact, summary, distilled, relation. Optional. Telemetry is excluded by default."),
619
643
  memory_subtype: z.string().optional().describe("Filter by memory subtype (must be valid for the chosen kind). Optional."),
620
644
  workspace_id: z.string().optional().describe("Filter to facts in this workspace namespace. Omit to use the default workspace from config."),
645
+ actor_id: z.string().optional().describe("Filter to facts captured by or associated with this stable actor identity. Optional."),
621
646
  include_legacy_workspace: z.boolean().optional().describe("Backwards-compatibility flag: also include legacy facts that have no workspace_id. Default false. Only set this if you suspect pre-migration data is missing from results."),
622
647
  entity: z.string().optional().describe("Restrict to facts mentioning this entity (case-insensitive). For full entity context prefer memory_entity."),
623
648
  episode_id: z.string().optional().describe("Filter by coherent episode ID."),
@@ -628,15 +653,18 @@ export function registerTools(mcp) {
628
653
  review_status: z.string().optional().describe("Filter by review lifecycle status (candidate / reviewed / approved / rejected)."),
629
654
  since: z.string().optional().describe("Only facts created on or after this ISO 8601 date or datetime."),
630
655
  until: z.string().optional().describe("Only facts created on or before this ISO 8601 date or datetime."),
631
- limit: z.number().optional().default(10).describe("Max results to return (default 10)."),
632
- graph_depth: z.number().optional().default(0).describe("Entity-graph traversal depth. 0 = vector search only (fast, default). 1 = also surface 1-hop entity-related facts (slower; use when the user asks 'what's connected to X?')."),
656
+ limit: z.number().optional().default(MEMORY_RECALL_DEFAULT_LIMIT).describe(`Max primary results to return (default ${MEMORY_RECALL_DEFAULT_LIMIT}, maximum ${MEMORY_RECALL_MAX_LIMIT}). Values above the maximum are clamped.`),
657
+ 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."),
658
+ 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."),
633
659
  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)."),
634
- }, async ({ query, category, domain, kind, memory_subtype, workspace_id, include_legacy_workspace, entity, episode_id, workstream_key, task_key, repo, branch, review_status, since, until, limit, graph_depth, metadata_filter, }) => {
660
+ }, async ({ query, category, domain, kind, memory_subtype, workspace_id, actor_id, include_legacy_workspace, entity, episode_id, workstream_key, task_key, repo, branch, review_status, since, until, limit, graph_depth, output_format, metadata_filter, }) => {
635
661
  const guard = requireReady();
636
662
  if (guard)
637
663
  return guard;
638
- const requestedLimit = limit ?? 10;
639
- const scope = resolveScope(workspace_id, include_legacy_workspace);
664
+ const requestedLimit = limit ?? MEMORY_RECALL_DEFAULT_LIMIT;
665
+ const effectiveLimit = clampRecallLimit(limit);
666
+ const actorFilter = resolveActorIdentity({ actorId: actor_id, useGitFallback: false });
667
+ const scope = resolveScope(workspace_id, include_legacy_workspace, actorFilter.actor_id);
640
668
  const redactedQuery = redactStorageText(query);
641
669
  const vector = await embed(redactedQuery.text);
642
670
  const normalizedKind = kind ? normalizeKind(kind) : undefined;
@@ -669,27 +697,64 @@ export function registerTools(mcp) {
669
697
  metadata: metadata_filter,
670
698
  excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS,
671
699
  });
672
- const results = await qdrantSearch(vector, filter, requestedLimit * 2);
700
+ const results = await qdrantSearch(vector, filter, effectiveLimit * 2);
673
701
  if (!results.result?.length) {
674
702
  const nudge = buildMemoryNudge();
703
+ if (output_format === "json") {
704
+ return { content: [{ type: "text", text: JSON.stringify({
705
+ query: redactedQuery.text,
706
+ requested_limit: requestedLimit,
707
+ effective_limit: effectiveLimit,
708
+ max_limit: MEMORY_RECALL_MAX_LIMIT,
709
+ limit_clamped: effectiveLimit !== requestedLimit,
710
+ graph_depth: graph_depth ?? 0,
711
+ result_count: 0,
712
+ related_count: 0,
713
+ results: [],
714
+ related: [],
715
+ ...(nudge ? { nudge } : {}),
716
+ ...(redactedQuery.redacted ? { query_redaction: redactedQuery } : {}),
717
+ }, null, 2) }] };
718
+ }
675
719
  const text = nudge ? `No matching facts found.\n\n${nudge}` : "No matching facts found.";
676
720
  return { content: [{ type: "text", text }] };
677
721
  }
678
722
  const ranked = results.result
679
723
  .map((r) => ({ ...r, _combinedScore: computeCombinedScore(r) }))
680
724
  .sort((a, b) => b._combinedScore - a._combinedScore)
681
- .slice(0, requestedLimit);
725
+ .slice(0, effectiveLimit);
682
726
  const lines = ranked.map((r) => formatFact(r));
727
+ let related = { points: [] };
683
728
  if ((graph_depth ?? 0) >= 1) {
684
- const relatedLines = await graphTraversal(ranked, requestedLimit, scope);
685
- if (relatedLines.length > 0) {
729
+ related = await graphTraversal(ranked, effectiveLimit, scope);
730
+ if (related.points.length > 0) {
686
731
  lines.push("", "── Related (1-hop) ──");
687
- lines.push(...relatedLines);
732
+ lines.push(...related.points.map((r) => formatFact(r)));
733
+ }
734
+ else if (related.error) {
735
+ lines.push("", `(graph traversal failed: ${related.error})`);
688
736
  }
689
737
  }
690
738
  const nudge = buildMemoryNudge();
691
739
  if (nudge)
692
740
  lines.push("", nudge);
741
+ if (output_format === "json") {
742
+ return { content: [{ type: "text", text: JSON.stringify({
743
+ query: redactedQuery.text,
744
+ requested_limit: requestedLimit,
745
+ effective_limit: effectiveLimit,
746
+ max_limit: MEMORY_RECALL_MAX_LIMIT,
747
+ limit_clamped: effectiveLimit !== requestedLimit,
748
+ graph_depth: graph_depth ?? 0,
749
+ result_count: ranked.length,
750
+ related_count: related.points.length,
751
+ results: ranked.map((r) => structuredFact(r)),
752
+ related: related.points.map((r) => structuredFact(r)),
753
+ ...(related.error ? { graph_error: related.error } : {}),
754
+ ...(nudge ? { nudge } : {}),
755
+ ...(redactedQuery.redacted ? { query_redaction: redactedQuery } : {}),
756
+ }, null, 2) }] };
757
+ }
693
758
  return { content: [{ type: "text", text: lines.join("\n") }] };
694
759
  });
695
760
  // ── memory_entity ───────────────────────────────────────────────────────
@@ -839,6 +904,7 @@ export function registerTools(mcp) {
839
904
  const now = nowISO();
840
905
  try {
841
906
  const scope = resolveScope(workspace_id);
907
+ const _actor = resolveActorIdentity({ config: loadConfig() });
842
908
  const existing = await getPointForWorkspaceWrite(fact_id, scope);
843
909
  if (existing.error) {
844
910
  return { content: [{ type: "text", text: JSON.stringify(existing.error, null, 2) }], isError: true };
@@ -882,6 +948,7 @@ export function registerTools(mcp) {
882
948
  const now = nowISO();
883
949
  try {
884
950
  const scope = resolveScope(workspace_id);
951
+ const _actor = resolveActorIdentity({ config: loadConfig() });
885
952
  const writable = await getPointForWorkspaceWrite(fact_id, scope);
886
953
  if (writable.error) {
887
954
  return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
@@ -927,6 +994,7 @@ export function registerTools(mcp) {
927
994
  const now = nowISO();
928
995
  try {
929
996
  const scope = resolveScope(workspace_id);
997
+ const actor = resolveActorIdentity({ config: loadConfig() });
930
998
  const writable = await getPointForWorkspaceWrite(fact_id, scope);
931
999
  if (writable.error) {
932
1000
  return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
@@ -948,7 +1016,7 @@ export function registerTools(mcp) {
948
1016
  const redactedEvent = redactStorageText(eventContent);
949
1017
  const eventPayload = {
950
1018
  content: redactedEvent.text,
951
- category: "observations",
1019
+ category: categoryForMemorySubtype("feedback_event") ?? "system",
952
1020
  domain: "software_engineering",
953
1021
  kind: "telemetry",
954
1022
  memory_subtype: "feedback_event",
@@ -963,7 +1031,7 @@ export function registerTools(mcp) {
963
1031
  created_at: now,
964
1032
  updated_at: now,
965
1033
  };
966
- addWorkspacePayload(eventPayload, scope);
1034
+ addWorkspacePayload(eventPayload, scope, actor);
967
1035
  addRedactionPayload(eventPayload, redactedEvent);
968
1036
  try {
969
1037
  const eventVector = await embed(redactedEvent.text);
@@ -1002,6 +1070,7 @@ export function registerTools(mcp) {
1002
1070
  const now = nowISO();
1003
1071
  try {
1004
1072
  const scope = resolveScope(workspace_id);
1073
+ const actor = resolveActorIdentity({ config: loadConfig() });
1005
1074
  const target = await getPointForWorkspaceWrite(fact_id, scope);
1006
1075
  if (target.error) {
1007
1076
  return { content: [{ type: "text", text: JSON.stringify(target.error, null, 2) }], isError: true };
@@ -1013,7 +1082,7 @@ export function registerTools(mcp) {
1013
1082
  const redactedEvent = redactStorageText(eventContent);
1014
1083
  const eventPayload = {
1015
1084
  content: redactedEvent.text,
1016
- category: "observations",
1085
+ category: categoryForMemorySubtype("outcome_event") ?? "system",
1017
1086
  domain: "software_engineering",
1018
1087
  kind: "telemetry",
1019
1088
  memory_subtype: "outcome_event",
@@ -1028,7 +1097,7 @@ export function registerTools(mcp) {
1028
1097
  created_at: now,
1029
1098
  updated_at: now,
1030
1099
  };
1031
- addWorkspacePayload(eventPayload, scope);
1100
+ addWorkspacePayload(eventPayload, scope, actor);
1032
1101
  addRedactionPayload(eventPayload, redactedEvent);
1033
1102
  const eventVector = await embed(redactedEvent.text);
1034
1103
  await qdrantUpsert(eventId, eventVector, eventPayload);
@@ -1058,7 +1127,8 @@ export function registerTools(mcp) {
1058
1127
  task_key: z.string().optional().describe("Task or issue key (e.g. GitHub issue number, JIRA key)."),
1059
1128
  repo: z.string().optional().describe("Repository or project surface this summary relates to."),
1060
1129
  workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
1061
- }, async ({ content, entities, episode_id, workstream_key, task_key, repo, workspace_id }) => {
1130
+ actor_id: z.string().optional().describe("Stable actor identity associated with this session summary. Overrides identity config/env/Git fallback."),
1131
+ }, async ({ content, entities, episode_id, workstream_key, task_key, repo, workspace_id, actor_id }) => {
1062
1132
  const guard = requireReady();
1063
1133
  if (guard)
1064
1134
  return guard;
@@ -1066,13 +1136,14 @@ export function registerTools(mcp) {
1066
1136
  const now = nowISO();
1067
1137
  try {
1068
1138
  const scope = resolveScope(workspace_id);
1139
+ const actor = resolveActorIdentity({ actorId: actor_id, config: loadConfig() });
1069
1140
  const normalizedEntities = (entities ?? []).map((e) => e.trim().toLowerCase()).filter(Boolean);
1070
1141
  const summaryId = newId();
1071
1142
  const redactedContent = redactStorageText(content);
1072
1143
  const vector = await embed(redactedContent.text);
1073
1144
  const payload = {
1074
1145
  content: redactedContent.text,
1075
- category: categoryForMemorySubtype("session_index") ?? "projects",
1146
+ category: categoryForMemorySubtype("session_index") ?? "system",
1076
1147
  domain: "software_engineering",
1077
1148
  kind: "summary",
1078
1149
  memory_subtype: "session_index",
@@ -1097,7 +1168,7 @@ export function registerTools(mcp) {
1097
1168
  payload["task_key"] = task_key;
1098
1169
  if (repo)
1099
1170
  payload["repo"] = repo;
1100
- addWorkspacePayload(payload, scope);
1171
+ addWorkspacePayload(payload, scope, actor);
1101
1172
  addRedactionPayload(payload, redactedContent);
1102
1173
  await qdrantUpsert(summaryId, vector, payload);
1103
1174
  return {
@@ -1105,6 +1176,7 @@ export function registerTools(mcp) {
1105
1176
  status: "summary_stored",
1106
1177
  summary_id: summaryId,
1107
1178
  workspace_id: scope.workspaceId,
1179
+ actor_id: actor.actor_id,
1108
1180
  }) }],
1109
1181
  };
1110
1182
  }
@@ -1124,7 +1196,8 @@ export function registerTools(mcp) {
1124
1196
  task_key: z.string().optional().describe("Task or issue key associated with this learning, if relevant."),
1125
1197
  repo: z.string().optional().describe("Repository or project surface this learning applies to."),
1126
1198
  workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
1127
- }, async ({ content, entities, supersedes, task_key, repo, workspace_id }) => {
1199
+ actor_id: z.string().optional().describe("Stable actor identity associated with this distillation. Overrides identity config/env/Git fallback."),
1200
+ }, async ({ content, entities, supersedes, task_key, repo, workspace_id, actor_id }) => {
1128
1201
  const guard = requireReady();
1129
1202
  if (guard)
1130
1203
  return guard;
@@ -1132,6 +1205,7 @@ export function registerTools(mcp) {
1132
1205
  const now = nowISO();
1133
1206
  try {
1134
1207
  const scope = resolveScope(workspace_id);
1208
+ const actor = resolveActorIdentity({ actorId: actor_id, config: loadConfig() });
1135
1209
  const normalizedEntities = entities.map((e) => e.trim().toLowerCase()).filter(Boolean);
1136
1210
  const distilledId = newId();
1137
1211
  const redactedContent = redactStorageText(content);
@@ -1148,7 +1222,7 @@ export function registerTools(mcp) {
1148
1222
  }
1149
1223
  const payload = {
1150
1224
  content: redactedContent.text,
1151
- category: categoryForMemorySubtype("convention") ?? "observations",
1225
+ category: categoryForMemorySubtype("convention") ?? "engineering",
1152
1226
  domain: "software_engineering",
1153
1227
  kind: "distilled",
1154
1228
  memory_subtype: "convention",
@@ -1169,7 +1243,7 @@ export function registerTools(mcp) {
1169
1243
  payload["task_key"] = task_key;
1170
1244
  if (repo)
1171
1245
  payload["repo"] = repo;
1172
- addWorkspacePayload(payload, scope);
1246
+ addWorkspacePayload(payload, scope, actor);
1173
1247
  addRedactionPayload(payload, redactedContent);
1174
1248
  await qdrantUpsert(distilledId, vector, payload);
1175
1249
  return {
@@ -1178,6 +1252,7 @@ export function registerTools(mcp) {
1178
1252
  distilled_id: distilledId,
1179
1253
  supersedes: supersedes ?? null,
1180
1254
  workspace_id: scope.workspaceId,
1255
+ actor_id: actor.actor_id,
1181
1256
  }) }],
1182
1257
  };
1183
1258
  }
@@ -1187,11 +1262,11 @@ export function registerTools(mcp) {
1187
1262
  });
1188
1263
  // ── memory_review ───────────────────────────────────────────────────────
1189
1264
  mcp.tool("memory_review", [
1190
- "Triage facts that were extracted automatically by the bikky daemon (source='daemon').",
1191
- "Only useful when the daemon is running and capturing memories from logs/transcripts; otherwise this returns an empty list. Supports four actions: list (default — show recent daemon facts), approve (mark verified), reject (mark superseded with reason), correct (replace with edited content as a new fact).",
1265
+ "Triage facts that were captured automatically by Bikky (source='system').",
1266
+ "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).",
1192
1267
  ].join(" "), {
1193
1268
  limit: z.number().optional().default(10).describe("Max facts to return when action=list (default 10)."),
1194
- action: z.enum(["list", "approve", "reject", "correct"]).optional().default("list").describe("What to do. list = show recent daemon-extracted facts (default). approve = confirm a fact is correct (bumps verification count). reject = mark a fact as wrong (requires 'reason'). correct = supersede with an edited version (requires 'corrected_content')."),
1269
+ 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')."),
1195
1270
  fact_id: z.string().optional().describe("Fact ID to act on. Required for approve / reject / correct."),
1196
1271
  reason: z.string().optional().describe("Required for action=reject. Short reason the fact is wrong."),
1197
1272
  corrected_content: z.string().optional().describe("Required for action=correct. The fixed fact text. Stored as a new fact that supersedes the original."),
@@ -1204,13 +1279,13 @@ export function registerTools(mcp) {
1204
1279
  const scope = resolveScope(workspace_id, include_legacy_workspace);
1205
1280
  if (action === "list") {
1206
1281
  const filter = scopedFilter(scope) ?? { must: [] };
1207
- filter.must.push({ key: "source", match: { value: "daemon" } });
1282
+ filter.must.push({ key: "source", match: { any: ["system", "daemon"] } });
1208
1283
  const result = await qdrantScroll(filter, (limit ?? 10) * 2);
1209
1284
  const points = (result.result?.points ?? [])
1210
1285
  .sort((a, b) => (b.payload.created_at ?? "").localeCompare(a.payload.created_at ?? ""))
1211
1286
  .slice(0, limit ?? 10);
1212
1287
  if (points.length === 0) {
1213
- return { content: [{ type: "text", text: "No daemon-extracted facts found." }] };
1288
+ return { content: [{ type: "text", text: "No system-captured facts found." }] };
1214
1289
  }
1215
1290
  const lines = points.map((pt) => {
1216
1291
  const p = pt.payload;
@@ -1273,6 +1348,7 @@ export function registerTools(mcp) {
1273
1348
  const correctionScope = origPayload?.workspace_id
1274
1349
  ? resolveScope(origPayload.workspace_id, false)
1275
1350
  : scope;
1351
+ const actor = resolveActorIdentity({ config: loadConfig() });
1276
1352
  const vector = await embed(redactedCorrected.text);
1277
1353
  const correctedId = crypto.randomUUID();
1278
1354
  const origCategory = normalizeCategory(origPayload?.category ?? DEFAULT_CATEGORY);
@@ -1302,7 +1378,7 @@ export function registerTools(mcp) {
1302
1378
  updated_at: now,
1303
1379
  metadata: { ...(origPayload?.metadata ?? {}), corrected_from: fact_id },
1304
1380
  };
1305
- addWorkspacePayload(correctedPayload, correctionScope);
1381
+ addWorkspacePayload(correctedPayload, correctionScope, actor);
1306
1382
  addRedactionPayload(correctedPayload, redactedCorrected);
1307
1383
  await qdrantUpsert(correctedId, vector, correctedPayload);
1308
1384
  await qdrantSetPayload([fact_id], {
@@ -1329,7 +1405,7 @@ export function registerTools(mcp) {
1329
1405
  const staleThreshold = new Date(Date.now() - STALENESS_DAYS * 86400000).toISOString();
1330
1406
  const scope = resolveScope();
1331
1407
  const staleFilter = scopedFilter(scope) ?? { must: [] };
1332
- staleFilter.must.push({ key: "category", match: { any: ["infrastructure", "projects", "decisions"] } });
1408
+ staleFilter.must.push({ key: "category", match: { any: ["engineering", "product", "human", "system"] } });
1333
1409
  staleFilter.should = [
1334
1410
  { key: "last_reinforced_at", range: { lte: staleThreshold } },
1335
1411
  { is_null: { key: "last_reinforced_at" } },
@@ -1354,10 +1430,10 @@ export function registerTools(mcp) {
1354
1430
  }
1355
1431
  }
1356
1432
  sections.push("🔍 Reflect: think about the LAST 10 minutes of work and answer in your head:\n" +
1357
- " 1. Did you touch a service, port, config, or file path you hadn't seen before?\n" +
1358
- " 2. Did you make a choice (library, pattern, approach) you'd want a future session to know about?\n" +
1359
- " 3. Did you hit an error and find a workaround?\n" +
1360
- " 4. Did the user state a preference or constraint?\n" +
1433
+ " 1. Did you touch engineering context: code, infra, ops, access, troubleshooting, or conventions?\n" +
1434
+ " 2. Did you capture product context: a requirement, decision, workflow, roadmap item, metric, or market insight?\n" +
1435
+ " 3. Did you learn human context: a preference, owner, working agreement, person profile, or durable activity event?\n" +
1436
+ " 4. Did the work produce system context: session, episode, workstream, recall, feedback, outcome, or rollup state?\n" +
1361
1437
  "If any answer is yes, call memory_store now — one atomic fact per item, with category/domain/entities.");
1362
1438
  return { content: [{ type: "text", text: sections.join("\n\n") }] };
1363
1439
  });