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.
- package/README.md +64 -37
- package/dist/config.d.ts +15 -1
- package/dist/config.js +116 -20
- package/dist/daemon/capture-policy.d.ts +0 -1
- package/dist/daemon/capture-policy.js +0 -2
- package/dist/daemon/consolidation.d.ts +2 -1
- package/dist/daemon/consolidation.js +32 -15
- package/dist/daemon/entity-typing.js +10 -0
- package/dist/daemon/episode-summary.d.ts +4 -0
- package/dist/daemon/episode-summary.js +39 -8
- package/dist/daemon/extraction.d.ts +2 -2
- package/dist/daemon/extraction.js +65 -22
- package/dist/daemon/loop.js +8 -0
- package/dist/daemon/maintenance-state.d.ts +1 -1
- package/dist/daemon/maintenance-state.js +2 -0
- package/dist/daemon/qdrant.d.ts +32 -10
- package/dist/daemon/qdrant.js +199 -60
- package/dist/daemon/quality-rollups.d.ts +51 -0
- package/dist/daemon/quality-rollups.js +378 -0
- package/dist/daemon/relations.d.ts +3 -3
- package/dist/daemon/relations.js +28 -16
- package/dist/daemon/session-index.d.ts +5 -0
- package/dist/daemon/session-index.js +36 -9
- package/dist/daemon/session-summary.d.ts +3 -0
- package/dist/daemon/session-summary.js +48 -15
- package/dist/daemon/staleness.js +3 -3
- package/dist/daemon/transcript-sources.js +3 -2
- package/dist/daemon/watcher.js +2 -0
- package/dist/daemon/workstream-summary.d.ts +4 -0
- package/dist/daemon/workstream-summary.js +58 -16
- package/dist/install.d.ts +11 -0
- package/dist/install.js +38 -0
- package/dist/lifecycle.js +7 -5
- package/dist/llm/embedding/index.js +2 -1
- package/dist/llm/embedding/providers/openai.js +8 -2
- package/dist/llm/embedding/providers/portkey.js +9 -2
- package/dist/llm/inference/index.js +2 -1
- package/dist/llm/util.d.ts +12 -0
- package/dist/llm/util.js +18 -0
- package/dist/mcp/helpers.d.ts +8 -0
- package/dist/mcp/helpers.js +36 -3
- package/dist/mcp/taxonomy.d.ts +9 -13
- package/dist/mcp/taxonomy.js +59 -42
- package/dist/mcp/tools.js +351 -83
- package/dist/mcp/types.d.ts +35 -0
- package/dist/package-verifier.d.ts +19 -0
- package/dist/package-verifier.js +83 -0
- package/dist/prompts/brief.d.ts +2 -2
- package/dist/prompts/brief.js +0 -1
- package/dist/prompts/extraction.js +9 -11
- package/dist/provenance/origin.d.ts +57 -0
- package/dist/provenance/origin.js +254 -0
- package/dist/routing-context.d.ts +16 -0
- package/dist/routing-context.js +55 -0
- package/dist/status.d.ts +1 -0
- package/dist/status.js +7 -1
- package/docs/config/fully-hosted.md +33 -13
- package/docs/config/hosted-models.md +33 -13
- package/docs/config/hosted-qdrant-local-models.md +1 -0
- package/docs/config/local.md +1 -0
- package/docs/configuration.md +42 -17
- package/package.json +2 -2
package/dist/daemon/qdrant.js
CHANGED
|
@@ -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 {
|
|
14
|
-
import {
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 =
|
|
124
|
+
const ready = destinations.length > 0;
|
|
54
125
|
if (ready) {
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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 = () => !!(
|
|
140
|
+
const isReady = () => !!(pool && destinations.length > 0);
|
|
72
141
|
const ensureCollection = async () => {
|
|
73
|
-
if (!
|
|
142
|
+
if (!pool) {
|
|
74
143
|
throw new Error("Qdrant client not initialized — call init() first");
|
|
75
144
|
}
|
|
76
145
|
const embCfg = getEmbeddingConfig();
|
|
77
|
-
await
|
|
78
|
-
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|