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
@@ -8,35 +8,106 @@
8
8
  * self-hosted instances.
9
9
  */
10
10
  import { createHash, randomUUID } from "node:crypto";
11
- import { loadConfig } from "../config.js";
11
+ import { getEffectiveDestinations, loadConfig } from "../config.js";
12
12
  import { embed, initEmbedding, getEmbeddingConfig } from "../llm/index.js";
13
- import { QdrantClient } from "../lib/qdrant-client.js";
14
- import { DEFAULT_DOMAIN, QDRANT_INDEXES, categoryForMemorySubtype, layerForMemorySubtype, normalizeCategory, normalizeDomain, normalizeKind, normalizeSource, validateMemorySubtype, } from "../mcp/taxonomy.js";
13
+ import { QdrantPool } from "../lib/qdrant-pool.js";
14
+ import { buildResolver } from "../routing.js";
15
+ import { DEFAULT_DOMAIN, QDRANT_INDEXES, categoryForMemorySubtype, layerForMemorySubtype, normalizeCategory, normalizeDomain, normalizeKind, validateMemorySubtype, } from "../mcp/taxonomy.js";
15
16
  import { combineRedactions, redactStorageText, } from "../privacy/redaction.js";
17
+ import { buildOperationOrigin } from "../provenance/origin.js";
18
+ import { buildMemoryRoutingInput, mergeRoutingInputs } from "../routing-context.js";
16
19
  // ---------------------------------------------------------------------------
17
20
  // State
18
21
  // ---------------------------------------------------------------------------
19
- let qdrantUrl = null;
20
- let qdrantApiKey = null;
21
22
  let collection = "bikky";
22
23
  let logFn = () => { };
23
- let client = null;
24
+ let destinations = [];
25
+ let pool = null;
26
+ let resolver = null;
24
27
  const setLogger = (fn) => { logFn = fn; };
25
28
  const setEmbeddingConfig = (overrides) => {
26
29
  if (overrides && overrides.provider)
27
30
  initEmbedding(overrides);
28
31
  };
29
32
  const clientLogAdapter = (level, msg) => logFn(level, msg);
33
+ const fallbackDestination = () => {
34
+ if (destinations.length === 0) {
35
+ throw new Error("Qdrant client not initialized — call init() first");
36
+ }
37
+ return destinations.find((destination) => destination.default === true) ?? destinations[0];
38
+ };
39
+ const resolveDestination = (input = {}) => {
40
+ if (!resolver)
41
+ return fallbackDestination();
42
+ return resolver(input);
43
+ };
44
+ const destinationFromRef = (ref) => {
45
+ if (!ref)
46
+ return fallbackDestination();
47
+ if (typeof ref !== "string")
48
+ return ref;
49
+ const found = destinations.find((destination) => destination.name === ref);
50
+ if (!found) {
51
+ throw new Error(`Unknown Qdrant destination '${ref}'. Configured destinations: ${destinations.map((d) => d.name).join(", ") || "(none)"}`);
52
+ }
53
+ return found;
54
+ };
55
+ const pathForDestination = (urlPath, destination) => {
56
+ if (!urlPath.startsWith("/collections/"))
57
+ return urlPath;
58
+ return urlPath.replace(/^\/collections\/[^/]+/, `/collections/${destination.collection}`);
59
+ };
60
+ const routingInputForFact = (fact, normalizedContent, normalizedEntities, extraMetadata = {}) => {
61
+ const metadata = {
62
+ ...(fact.metadata ?? {}),
63
+ ...extraMetadata,
64
+ category: fact.category,
65
+ domain: fact.domain ?? DEFAULT_DOMAIN,
66
+ kind: fact.kind ?? "fact",
67
+ ...(fact.memory_subtype ? { memory_subtype: fact.memory_subtype } : {}),
68
+ ...(fact.source ? { source: fact.source } : {}),
69
+ ...(fact.actor_id ? { actor_id: fact.actor_id } : {}),
70
+ ...(fact.session_id ? { session_id: fact.session_id } : {}),
71
+ ...(fact.episode_id ? { episode_id: fact.episode_id } : {}),
72
+ ...(fact.workstream_key ? { workstream_key: fact.workstream_key } : {}),
73
+ ...(fact.task_key ? { task_key: fact.task_key } : {}),
74
+ ...(fact.repo ? { repo: fact.repo } : {}),
75
+ ...(fact.branch ? { branch: fact.branch } : {}),
76
+ ...(fact.surface ? { surface: fact.surface } : {}),
77
+ ...(fact.issue_id ? { issue_id: fact.issue_id } : {}),
78
+ ...(fact.pr_id ? { pr_id: fact.pr_id } : {}),
79
+ ...(fact.source_event_ids ? { source_event_ids: fact.source_event_ids } : {}),
80
+ ...(fact.source_fact_ids ? { source_fact_ids: fact.source_fact_ids } : {}),
81
+ ...(fact.source_episode_ids ? { source_episode_ids: fact.source_episode_ids } : {}),
82
+ ...(fact.prompt_version ? { prompt_version: fact.prompt_version } : {}),
83
+ ...(fact.capture_policy_version ? { capture_policy_version: fact.capture_policy_version } : {}),
84
+ ...(fact.review_status ? { review_status: fact.review_status } : {}),
85
+ ...(fact.volatility ? { volatility: fact.volatility } : {}),
86
+ ...(fact.valid_from ? { valid_from: fact.valid_from } : {}),
87
+ ...(fact.expires_at ? { expires_at: fact.expires_at } : {}),
88
+ ...(fact.confidence_reason ? { confidence_reason: fact.confidence_reason } : {}),
89
+ ...(fact.relation ? {
90
+ from_entity: fact.relation.from,
91
+ relation_type: fact.relation.type,
92
+ to_entity: fact.relation.to,
93
+ } : {}),
94
+ };
95
+ return buildMemoryRoutingInput({
96
+ content: normalizedContent,
97
+ entities: normalizedEntities,
98
+ metadata,
99
+ extraContent: [fact.origin, fact.last_operation_origin, fact.relation],
100
+ });
101
+ };
30
102
  // ---------------------------------------------------------------------------
31
103
  // Init — reads credentials from loadConfig()
32
104
  // ---------------------------------------------------------------------------
33
105
  const init = () => {
34
106
  const cfg = loadConfig();
35
- qdrantUrl = cfg.qdrant_url;
36
- qdrantApiKey = cfg.qdrant_api_key;
37
- collection = cfg.collection || "bikky";
38
- if (qdrantUrl)
39
- qdrantUrl = qdrantUrl.replace(/\/+$/, "");
107
+ destinations = getEffectiveDestinations(cfg);
108
+ collection = (destinations.find((destination) => destination.default === true) ?? destinations[0])?.collection
109
+ ?? cfg.collection
110
+ ?? "bikky";
40
111
  // Initialize embedding provider from config
41
112
  const embCfg = initEmbedding({
42
113
  provider: cfg.embedding.provider,
@@ -50,48 +121,72 @@ const init = () => {
50
121
  retryBaseDelayMs: cfg.embedding.retry_base_delay_ms,
51
122
  });
52
123
  logFn("INFO", `Embedding provider: ${embCfg.provider}/${embCfg.model} (${embCfg.dimensions}d) @ ${embCfg.baseUrl}`);
53
- const ready = !!qdrantUrl;
124
+ const ready = destinations.length > 0;
54
125
  if (ready) {
55
- client = new QdrantClient({
56
- url: qdrantUrl,
57
- apiKey: qdrantApiKey,
58
- collection,
59
- timeoutMs: cfg.qdrant_client.timeout_ms,
60
- retries: cfg.qdrant_client.retries,
61
- retryBaseDelayMs: cfg.qdrant_client.retry_base_delay_ms,
126
+ pool = new QdrantPool(destinations, {
127
+ client: cfg.qdrant_client,
62
128
  log: clientLogAdapter,
63
129
  });
130
+ resolver = buildResolver(destinations);
131
+ logFn("INFO", `Qdrant destinations: ${destinations.map((destination) => `${destination.name}/${destination.collection}`).join(", ")}`);
64
132
  }
65
133
  else {
66
- client = null;
134
+ pool = null;
135
+ resolver = null;
67
136
  logFn("WARN", "Qdrant client: missing URL (some memory features disabled)");
68
137
  }
69
138
  return ready;
70
139
  };
71
- const isReady = () => !!(qdrantUrl && client);
140
+ const isReady = () => !!(pool && destinations.length > 0);
72
141
  const ensureCollection = async () => {
73
- if (!client) {
142
+ if (!pool) {
74
143
  throw new Error("Qdrant client not initialized — call init() first");
75
144
  }
76
145
  const embCfg = getEmbeddingConfig();
77
- await client.ensureCollection(embCfg.dimensions, QDRANT_INDEXES);
78
- logFn("INFO", `Qdrant collection '${collection}' ready (${QDRANT_INDEXES.length} indexes)`);
146
+ const results = await Promise.all(destinations.map(async (destination) => {
147
+ try {
148
+ await pool.ensureCollection(destination.name, embCfg.dimensions, QDRANT_INDEXES);
149
+ logFn("INFO", `Qdrant destination '${destination.name}' collection '${destination.collection}' ready (${QDRANT_INDEXES.length} indexes)`);
150
+ return { destination, ok: true, error: null };
151
+ }
152
+ catch (e) {
153
+ const message = e instanceof Error ? e.message : String(e);
154
+ logFn("WARN", `Qdrant destination '${destination.name}' readiness check failed: ${message}`);
155
+ return { destination, ok: false, error: message };
156
+ }
157
+ }));
158
+ if (!results.some((result) => result.ok)) {
159
+ throw new Error(`No Qdrant destinations ready: ${results.map((result) => `${result.destination.name}: ${result.error}`).join("; ")}`);
160
+ }
79
161
  };
80
162
  // ---------------------------------------------------------------------------
81
163
  // HTTP requests
82
164
  // ---------------------------------------------------------------------------
83
- const qdrantRequest = async (method, urlPath, body) => {
84
- if (!client) {
165
+ const qdrantRequest = async (method, urlPath, body, destinationRef) => {
166
+ if (!pool) {
85
167
  throw new Error(`Qdrant client not initialized — call init() first (${method} ${urlPath})`);
86
168
  }
87
- const result = await client.request(method, urlPath, body);
169
+ const destination = destinationFromRef(destinationRef);
170
+ const result = await pool.client(destination.name).request(method, pathForDestination(urlPath, destination), body);
88
171
  // Some Qdrant endpoints return empty bodies on success — preserve old return shape.
89
172
  return result ?? {};
90
173
  };
174
+ const collectionForDestination = (destinationRef) => destinationFromRef(destinationRef).collection;
175
+ const destinationNames = () => destinations.map((destination) => destination.name);
176
+ const readyDestinations = () => {
177
+ if (!pool)
178
+ return [];
179
+ return destinations.filter((destination) => pool.isCollectionReady(destination.name));
180
+ };
181
+ const activeDestinations = () => {
182
+ const ready = readyDestinations();
183
+ return ready.length > 0 ? ready : destinations;
184
+ };
91
185
  // ---------------------------------------------------------------------------
92
186
  // Read methods
93
187
  // ---------------------------------------------------------------------------
94
- const searchFacts = async (query, filters = {}, limit = 10) => {
188
+ const searchFacts = async (query, filters = {}, limit = 10, destinationRef) => {
189
+ const destination = destinationFromRef(destinationRef);
95
190
  const vector = await embed(query);
96
191
  const must = [
97
192
  { is_null: { key: "superseded_by" } },
@@ -114,14 +209,15 @@ const searchFacts = async (query, filters = {}, limit = 10) => {
114
209
  if (filters.workspaceId) {
115
210
  must.push({ key: "workspace_id", match: { value: filters.workspaceId } });
116
211
  }
117
- const result = await qdrantRequest("POST", `/collections/${collection}/points/search`, {
212
+ const result = await qdrantRequest("POST", `/collections/${destination.collection}/points/search`, {
118
213
  vector,
119
214
  filter: { must },
120
215
  limit,
121
216
  with_payload: true,
122
- });
217
+ }, destination);
123
218
  return (result.result || []).map((hit) => ({
124
219
  id: hit.id,
220
+ destination: destination.name,
125
221
  score: hit.score,
126
222
  content: hit.payload?.content ?? "",
127
223
  category: hit.payload?.category ?? "",
@@ -131,7 +227,8 @@ const searchFacts = async (query, filters = {}, limit = 10) => {
131
227
  created_at: hit.payload?.created_at ?? "",
132
228
  }));
133
229
  };
134
- const scrollFacts = async (filters = {}, limit = 10) => {
230
+ const scrollFacts = async (filters = {}, limit = 10, destinationRef) => {
231
+ const destination = destinationFromRef(destinationRef);
135
232
  const must = [
136
233
  { is_null: { key: "superseded_by" } },
137
234
  ];
@@ -179,14 +276,15 @@ const scrollFacts = async (filters = {}, limit = 10) => {
179
276
  if (filters.workspaceId) {
180
277
  must.push({ key: "workspace_id", match: { value: filters.workspaceId } });
181
278
  }
182
- const result = await qdrantRequest("POST", `/collections/${collection}/points/scroll`, {
279
+ const result = await qdrantRequest("POST", `/collections/${destination.collection}/points/scroll`, {
183
280
  filter: { must, must_not },
184
281
  limit,
185
282
  ...(filters.orderBy ? { order_by: { key: filters.orderBy.key, direction: filters.orderBy.direction } } : {}),
186
283
  with_payload: true,
187
- });
284
+ }, destination);
188
285
  return (result.result?.points || []).map((pt) => ({
189
286
  id: pt.id,
287
+ destination: destination.name,
190
288
  content: pt.payload?.content ?? "",
191
289
  category: pt.payload?.category ?? "",
192
290
  entities: pt.payload?.entities || [],
@@ -203,10 +301,17 @@ const scrollFacts = async (filters = {}, limit = 10) => {
203
301
  source_fact_ids: pt.payload?.source_fact_ids ?? [],
204
302
  }));
205
303
  };
304
+ const scrollFactsAcrossDestinations = async (filters = {}, limit = 10) => {
305
+ const results = await Promise.all(activeDestinations().map((destination) => scrollFacts(filters, limit, destination).catch((e) => {
306
+ logFn("WARN", `Qdrant scroll failed for destination '${destination.name}': ${e.message}`);
307
+ return [];
308
+ })));
309
+ return results.flat();
310
+ };
206
311
  // ---------------------------------------------------------------------------
207
312
  // Write methods
208
313
  // ---------------------------------------------------------------------------
209
- const storeFact = async (fact) => {
314
+ const storeFact = async (fact, routeInput) => {
210
315
  const normalizedKind = normalizeKind(fact.kind);
211
316
  const normalizedSubtype = validateMemorySubtype(normalizedKind, fact.memory_subtype);
212
317
  const normalizedCategory = normalizedSubtype
@@ -226,9 +331,18 @@ const storeFact = async (fact) => {
226
331
  ...redactedEntities,
227
332
  ...(redactedRelation ? [redactedRelation.from, redactedRelation.type, redactedRelation.to] : []),
228
333
  ]);
229
- const vector = await embed(redactedContent.text);
230
334
  const now = new Date().toISOString();
231
335
  const id = randomUUID();
336
+ const origin = fact.origin ?? buildOperationOrigin({
337
+ interface: "daemon",
338
+ action: "create",
339
+ subsystem: "qdrant.store_fact",
340
+ metadata: {
341
+ category: normalizedCategory,
342
+ kind: normalizedKind,
343
+ ...(normalizedSubtype ? { memory_subtype: normalizedSubtype } : {}),
344
+ },
345
+ });
232
346
  const payload = {
233
347
  content: redactedContent.text,
234
348
  category: normalizedCategory,
@@ -236,9 +350,9 @@ const storeFact = async (fact) => {
236
350
  kind: normalizedKind,
237
351
  ...(normalizedLayer ? { layer: normalizedLayer } : {}),
238
352
  ...(normalizedSubtype ? { memory_subtype: normalizedSubtype } : {}),
239
- ...(fact.actor_id ? { actor_id: fact.actor_id } : {}),
353
+ origin,
354
+ ...(fact.last_operation_origin ? { last_operation_origin: fact.last_operation_origin } : {}),
240
355
  entities: redactedEntities.map((entity) => entity.text.toLowerCase()),
241
- source: normalizeSource(fact.source ?? "system"),
242
356
  confidence: fact.confidence ?? 0.7,
243
357
  importance: fact.importance ?? 0.5,
244
358
  content_hash: redactedContent.redacted
@@ -281,37 +395,58 @@ const storeFact = async (fact) => {
281
395
  if (redaction.redacted) {
282
396
  payload.redaction = redaction;
283
397
  }
284
- await qdrantRequest("PUT", `/collections/${collection}/points`, {
398
+ const destination = resolveDestination(mergeRoutingInputs(routingInputForFact(fact, redactedContent.text, payload.entities, {
399
+ category: normalizedCategory,
400
+ domain: normalizedDomain,
401
+ kind: normalizedKind,
402
+ ...(normalizedSubtype ? { memory_subtype: normalizedSubtype } : {}),
403
+ }), routeInput));
404
+ const vector = await embed(redactedContent.text);
405
+ await qdrantRequest("PUT", `/collections/${destination.collection}/points`, {
285
406
  points: [{ id, vector, payload }],
286
- });
287
- logFn("DEBUG", `Qdrant: stored fact ${id} [${normalizedCategory}] ${redactedContent.text.slice(0, 60)}`);
407
+ }, destination);
408
+ logFn("DEBUG", `Qdrant: stored fact ${id} in '${destination.name}' [${normalizedCategory}] ${redactedContent.text.slice(0, 60)}`);
288
409
  return id;
289
410
  };
290
- const supersedeFact = async (oldFactId, newFactId) => {
411
+ const supersedeFact = async (oldFactId, newFactId, destinationRef, origin) => {
412
+ const destination = destinationFromRef(destinationRef);
291
413
  const now = new Date().toISOString();
292
- await qdrantRequest("POST", `/collections/${collection}/points/payload`, {
414
+ await qdrantRequest("POST", `/collections/${destination.collection}/points/payload`, {
293
415
  payload: {
294
416
  superseded_by: newFactId,
295
417
  superseded_at: now,
296
418
  updated_at: now,
419
+ last_operation_origin: origin ?? buildOperationOrigin({
420
+ interface: "daemon",
421
+ action: "supersede",
422
+ subsystem: "qdrant.supersede_fact",
423
+ metadata: { new_fact_id: newFactId },
424
+ }),
297
425
  },
298
426
  points: [oldFactId],
299
- });
300
- logFn("DEBUG", `Qdrant: superseded fact ${oldFactId} → ${newFactId}`);
427
+ }, destination);
428
+ logFn("DEBUG", `Qdrant: superseded fact ${oldFactId} → ${newFactId} in '${destination.name}'`);
301
429
  };
302
- const reinforceFact = async (factId, currentCount) => {
430
+ const reinforceFact = async (factId, currentCount, destinationRef, origin) => {
431
+ const destination = destinationFromRef(destinationRef);
303
432
  const now = new Date().toISOString();
304
- await qdrantRequest("POST", `/collections/${collection}/points/payload`, {
433
+ await qdrantRequest("POST", `/collections/${destination.collection}/points/payload`, {
305
434
  payload: {
306
435
  reinforcement_count: (currentCount || 1) + 1,
307
436
  last_reinforced_at: now,
308
437
  updated_at: now,
438
+ last_operation_origin: origin ?? buildOperationOrigin({
439
+ interface: "daemon",
440
+ action: "reinforce",
441
+ subsystem: "qdrant.reinforce_fact",
442
+ }),
309
443
  },
310
444
  points: [factId],
311
- });
312
- logFn("DEBUG", `Qdrant: reinforced fact ${factId}`);
445
+ }, destination);
446
+ logFn("DEBUG", `Qdrant: reinforced fact ${factId} in '${destination.name}'`);
313
447
  };
314
- const dedupCheck = async (content, contentHashVal, { exactThreshold = 0.92, supersedeThreshold = 0.80 } = {}, workspaceId) => {
448
+ const dedupCheck = async (content, contentHashVal, { exactThreshold = 0.92, supersedeThreshold = 0.80 } = {}, workspaceId, routeInput) => {
449
+ const destination = resolveDestination(routeInput ?? { content });
315
450
  // First: hash-based exact check (fast, no embedding)
316
451
  try {
317
452
  const must = [
@@ -320,15 +455,16 @@ const dedupCheck = async (content, contentHashVal, { exactThreshold = 0.92, supe
320
455
  ];
321
456
  if (workspaceId)
322
457
  must.push({ key: "workspace_id", match: { value: workspaceId } });
323
- const hashResult = await qdrantRequest("POST", `/collections/${collection}/points/scroll`, {
458
+ const hashResult = await qdrantRequest("POST", `/collections/${destination.collection}/points/scroll`, {
324
459
  filter: { must },
325
460
  limit: 1,
326
461
  with_payload: true,
327
- });
462
+ }, destination);
328
463
  const existing = hashResult.result?.points?.[0];
329
464
  if (existing) {
330
465
  return {
331
466
  action: "skip",
467
+ destination: destination.name,
332
468
  existingId: existing.id,
333
469
  existingCount: existing.payload?.reinforcement_count || 1,
334
470
  score: 1.0,
@@ -345,18 +481,19 @@ const dedupCheck = async (content, contentHashVal, { exactThreshold = 0.92, supe
345
481
  const must = [{ is_null: { key: "superseded_by" } }];
346
482
  if (workspaceId)
347
483
  must.push({ key: "workspace_id", match: { value: workspaceId } });
348
- const searchResult = await qdrantRequest("POST", `/collections/${collection}/points/search`, {
484
+ const searchResult = await qdrantRequest("POST", `/collections/${destination.collection}/points/search`, {
349
485
  vector,
350
486
  filter: { must },
351
487
  limit: 1,
352
488
  with_payload: true,
353
- });
489
+ }, destination);
354
490
  const top = searchResult.result?.[0];
355
491
  if (!top)
356
- return { action: "insert" };
492
+ return { action: "insert", destination: destination.name };
357
493
  if (top.score >= exactThreshold) {
358
494
  return {
359
495
  action: "skip",
496
+ destination: destination.name,
360
497
  existingId: top.id,
361
498
  existingCount: top.payload?.reinforcement_count || 1,
362
499
  score: top.score,
@@ -365,17 +502,18 @@ const dedupCheck = async (content, contentHashVal, { exactThreshold = 0.92, supe
365
502
  if (top.score >= supersedeThreshold) {
366
503
  return {
367
504
  action: "supersede",
505
+ destination: destination.name,
368
506
  existingId: top.id,
369
507
  existingCount: top.payload?.reinforcement_count || 1,
370
508
  score: top.score,
371
509
  };
372
510
  }
373
- return { action: "insert" };
511
+ return { action: "insert", destination: destination.name };
374
512
  }
375
513
  catch (e) {
376
514
  const msg = e instanceof Error ? e.message : String(e);
377
515
  logFn("WARN", `Qdrant dedup vector check failed: ${msg}`);
378
- return { action: "insert" }; // fail open — better to duplicate than lose
516
+ return { action: "insert", destination: destination.name }; // fail open — better to duplicate than lose
379
517
  }
380
518
  };
381
519
  /**
@@ -387,7 +525,8 @@ const dedupCheck = async (content, contentHashVal, { exactThreshold = 0.92, supe
387
525
  * No hardcoded vocabulary — purely embedding-similarity based, and the
388
526
  * exemplar set grows organically every time a user calls memory_forget.
389
527
  */
390
- const badExemplarCheck = async (content, workspaceId) => {
528
+ const badExemplarCheck = async (content, workspaceId, routeInput) => {
529
+ const destination = resolveDestination(routeInput ?? { content });
391
530
  try {
392
531
  const vector = await embed(content);
393
532
  const must = [
@@ -395,12 +534,12 @@ const badExemplarCheck = async (content, workspaceId) => {
395
534
  ];
396
535
  if (workspaceId)
397
536
  must.push({ key: "workspace_id", match: { value: workspaceId } });
398
- const result = await qdrantRequest("POST", `/collections/${collection}/points/search`, {
537
+ const result = await qdrantRequest("POST", `/collections/${destination.collection}/points/search`, {
399
538
  vector,
400
539
  filter: { must },
401
540
  limit: 1,
402
541
  with_payload: true,
403
- });
542
+ }, destination);
404
543
  const top = result.result?.[0];
405
544
  if (!top)
406
545
  return null;
@@ -419,5 +558,5 @@ const badExemplarCheck = async (content, workspaceId) => {
419
558
  // ---------------------------------------------------------------------------
420
559
  // Exports
421
560
  // ---------------------------------------------------------------------------
422
- export { init, isReady, ensureCollection, setLogger, setEmbeddingConfig, qdrantRequest, embed, searchFacts, scrollFacts, storeFact, supersedeFact, reinforceFact, dedupCheck, badExemplarCheck, collection, };
561
+ export { init, isReady, ensureCollection, setLogger, setEmbeddingConfig, qdrantRequest, resolveDestination, collectionForDestination, destinationNames, readyDestinations, activeDestinations, embed, searchFacts, scrollFacts, scrollFactsAcrossDestinations, storeFact, supersedeFact, reinforceFact, dedupCheck, badExemplarCheck, collection, };
423
562
  //# sourceMappingURL=qdrant.js.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Backend aggregation for memory quality telemetry.
3
+ */
4
+ import type { BikkyConfig, Destination } from "../config.js";
5
+ import type { FactPayload } from "../mcp/types.js";
6
+ import type { LogFn } from "./qdrant.js";
7
+ export type QualityScopeType = "destination" | "repo" | "workstream_key" | "task_key" | "entity" | "origin_user" | "origin_agent";
8
+ export interface QualityPoint {
9
+ id: string;
10
+ destination: string;
11
+ payload: Partial<FactPayload>;
12
+ }
13
+ export interface QualityRollup {
14
+ destination: string;
15
+ scope_type: QualityScopeType;
16
+ scope_value: string;
17
+ active_fact_count: number;
18
+ recall_count: number;
19
+ useful_count: number;
20
+ misleading_count: number;
21
+ wrong_count: number;
22
+ stale_count: number;
23
+ low_confidence_count: number;
24
+ generated_at: string;
25
+ source_fact_ids: string[];
26
+ source_event_ids: string[];
27
+ }
28
+ export interface QualityRollupResult {
29
+ destinations_seen: number;
30
+ facts_seen: number;
31
+ events_seen: number;
32
+ rollups_upserted: number;
33
+ scopes_capped: boolean;
34
+ }
35
+ export interface QualityRollupDeps {
36
+ isReady: () => boolean;
37
+ activeDestinations: () => Destination[];
38
+ qdrantRequest: (method: string, urlPath: string, body?: unknown, destinationRef?: Destination | string | null) => Promise<Record<string, unknown>>;
39
+ embed: (text: string) => Promise<number[]>;
40
+ }
41
+ export declare const setLogger: (fn: LogFn) => void;
42
+ export declare const buildQualityRollups: (input: {
43
+ facts: QualityPoint[];
44
+ events?: QualityPoint[];
45
+ generatedAt?: Date;
46
+ staleThresholdDays?: number;
47
+ lowConfidenceThreshold?: number;
48
+ }) => QualityRollup[];
49
+ export declare const aggregateMemoryQualitySignals: (config: BikkyConfig, deps?: QualityRollupDeps) => Promise<QualityRollupResult>;
50
+ export declare const tick: (config: BikkyConfig, deps?: QualityRollupDeps) => Promise<void>;
51
+ //# sourceMappingURL=quality-rollups.d.ts.map