bikky 0.4.5 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +75 -50
- package/dist/daemon/consolidation.js +10 -3
- package/dist/daemon/extraction.js +9 -2
- package/dist/daemon/qdrant.d.ts +3 -2
- package/dist/daemon/qdrant.js +16 -4
- package/dist/daemon/relations.d.ts +1 -1
- package/dist/daemon/relations.js +11 -2
- package/dist/mcp/tools.js +198 -55
- package/dist/routing.d.ts +8 -1
- package/dist/routing.js +15 -0
- package/docs/configuration.md +33 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,34 @@ This project uses npm package versions for release tracking:
|
|
|
9
9
|
|
|
10
10
|
## Unreleased
|
|
11
11
|
|
|
12
|
+
## 0.4.6
|
|
13
|
+
|
|
14
|
+
- Added configurable `ignore` rules for memory writes using the same `cwd`, `entity`, `content`, and `metadata` filters as destination routing.
|
|
15
|
+
- Applied ignore checks before MCP writes, daemon writes, relation sidecars, superseding, telemetry upserts, embedding, deduplication, and Qdrant persistence.
|
|
16
|
+
- Documented ignore configuration and added regression coverage for config validation, routing precedence, MCP write paths, daemon writes, telemetry, and local MCP E2E behavior.
|
|
17
|
+
|
|
18
|
+
## 0.4.5
|
|
19
|
+
|
|
20
|
+
- Fixed destination routing so daemon provenance and agent metadata no longer influence where unrelated facts are stored.
|
|
21
|
+
- Added regression coverage for non-Bikky facts with Bikky daemon origin metadata.
|
|
22
|
+
- Moved the README top diagram closer to the audience/context section.
|
|
23
|
+
|
|
24
|
+
## 0.4.4
|
|
25
|
+
|
|
26
|
+
- Refreshed README positioning and configuration guidance for supported MCP clients, transcript capture, Qdrant storage, and built-in model providers.
|
|
27
|
+
- Added memory quality/usefulness signals and surfaced usefulness data in the UI.
|
|
28
|
+
- Fixed MCP and daemon write routing to use the full memory context consistently.
|
|
29
|
+
- Removed the old Human memory category and filtered internal dashboard memory rows.
|
|
30
|
+
- Added Playwright coverage for memory filters and edge cases.
|
|
31
|
+
|
|
32
|
+
## 0.4.3
|
|
33
|
+
|
|
34
|
+
- Fixed daemon destination routing and dynamic config-path handling.
|
|
35
|
+
- Added canonical origin provenance metadata to memory writes.
|
|
36
|
+
- Recommended Portkey as the default cloud inference provider and canonicalized 1024-dimension embedding guidance.
|
|
37
|
+
- Fixed `bikky-ui` browser launches when the server binds to `localhost:0`.
|
|
38
|
+
- Added higher-priority test coverage across core routing, config, and UI behavior.
|
|
39
|
+
|
|
12
40
|
## 0.4.2
|
|
13
41
|
|
|
14
42
|
- Republished the core package from current `main` so the npm package README uses GitHub documentation links instead of stale jsDelivr links.
|
package/dist/config.d.ts
CHANGED
|
@@ -119,6 +119,14 @@ export interface Destination {
|
|
|
119
119
|
/** Routing rules. Omit for a destination that is only reachable by override. */
|
|
120
120
|
match?: DestinationMatch;
|
|
121
121
|
}
|
|
122
|
+
export interface IgnoreRule {
|
|
123
|
+
/** Stable identifier returned in ignored-write responses and logs. */
|
|
124
|
+
name?: string;
|
|
125
|
+
/** Human-readable reason/guidance for why the rule exists. */
|
|
126
|
+
description?: string;
|
|
127
|
+
/** Match rules using the same semantics as destination routing. */
|
|
128
|
+
match: DestinationMatch;
|
|
129
|
+
}
|
|
122
130
|
export type SearchScopeTarget = "routed" | "all" | string | string[];
|
|
123
131
|
export interface SearchScopeDefinition {
|
|
124
132
|
/** Stable scope name that MCP clients can pass as `search_scope`. */
|
|
@@ -143,6 +151,8 @@ export interface BikkyConfig {
|
|
|
143
151
|
* default flag → first entry.
|
|
144
152
|
*/
|
|
145
153
|
destinations: Destination[];
|
|
154
|
+
/** Memory write exclusion rules. First matching rule skips persistence. */
|
|
155
|
+
ignore: IgnoreRule[];
|
|
146
156
|
/**
|
|
147
157
|
* Default read/search scope. "routed" preserves historical behavior
|
|
148
158
|
* (one destination via routing rules); "all" fans out to every destination;
|
package/dist/config.js
CHANGED
|
@@ -55,6 +55,7 @@ const DEFAULTS = {
|
|
|
55
55
|
qdrant_api_key: null,
|
|
56
56
|
collection: "bikky",
|
|
57
57
|
destinations: [],
|
|
58
|
+
ignore: [],
|
|
58
59
|
default_search_scope: "routed",
|
|
59
60
|
search_scopes: [],
|
|
60
61
|
aws_profile: null,
|
|
@@ -241,6 +242,11 @@ const destinationFileSchema = z.object({
|
|
|
241
242
|
default: z.boolean().optional(),
|
|
242
243
|
match: destinationMatchSchema.optional(),
|
|
243
244
|
}).passthrough();
|
|
245
|
+
const ignoreRuleFileSchema = z.object({
|
|
246
|
+
name: z.string().min(1).optional(),
|
|
247
|
+
description: z.string().optional(),
|
|
248
|
+
match: destinationMatchSchema,
|
|
249
|
+
}).passthrough();
|
|
244
250
|
const searchScopeTargetSchema = z.union([
|
|
245
251
|
z.string().min(1),
|
|
246
252
|
z.array(z.string().min(1)).min(1),
|
|
@@ -255,6 +261,7 @@ const configFileSchema = z.object({
|
|
|
255
261
|
qdrant_api_key: z.string().nullable().optional(),
|
|
256
262
|
collection: z.string().optional(),
|
|
257
263
|
destinations: z.array(destinationFileSchema).optional(),
|
|
264
|
+
ignore: z.array(ignoreRuleFileSchema).optional(),
|
|
258
265
|
default_search_scope: searchScopeTargetSchema.optional(),
|
|
259
266
|
search_scopes: z.array(searchScopeDefinitionFileSchema).optional(),
|
|
260
267
|
aws_profile: z.string().nullable().optional(),
|
|
@@ -313,6 +320,58 @@ function validateUrlLike(value, pathName, issues) {
|
|
|
313
320
|
});
|
|
314
321
|
}
|
|
315
322
|
}
|
|
323
|
+
function validateMatchBlock(match, base, issues) {
|
|
324
|
+
for (const field of ["cwd", "entity", "content"]) {
|
|
325
|
+
const value = match[field];
|
|
326
|
+
if (value === undefined)
|
|
327
|
+
continue;
|
|
328
|
+
if (!Array.isArray(value)) {
|
|
329
|
+
issues.push({ severity: "error", path: `${base}.${field}`, message: "must be an array of regex strings" });
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
value.forEach((pattern, pIdx) => {
|
|
333
|
+
if (typeof pattern !== "string") {
|
|
334
|
+
issues.push({ severity: "error", path: `${base}.${field}[${pIdx}]`, message: "must be a string" });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
new RegExp(pattern);
|
|
339
|
+
}
|
|
340
|
+
catch (e) {
|
|
341
|
+
issues.push({
|
|
342
|
+
severity: "error",
|
|
343
|
+
path: `${base}.${field}[${pIdx}]`,
|
|
344
|
+
message: `invalid regex: ${e instanceof Error ? e.message : String(e)}`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
const metadata = childObject(match, "metadata");
|
|
350
|
+
if (metadata) {
|
|
351
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
352
|
+
if (!Array.isArray(value)) {
|
|
353
|
+
issues.push({ severity: "error", path: `${base}.metadata.${key}`, message: "must be an array of regex strings" });
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
value.forEach((pattern, pIdx) => {
|
|
357
|
+
if (typeof pattern !== "string") {
|
|
358
|
+
issues.push({ severity: "error", path: `${base}.metadata.${key}[${pIdx}]`, message: "must be a string" });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
new RegExp(pattern);
|
|
363
|
+
}
|
|
364
|
+
catch (e) {
|
|
365
|
+
issues.push({
|
|
366
|
+
severity: "error",
|
|
367
|
+
path: `${base}.metadata.${key}[${pIdx}]`,
|
|
368
|
+
message: `invalid regex: ${e instanceof Error ? e.message : String(e)}`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
316
375
|
export function validateConfigObject(raw) {
|
|
317
376
|
const parsed = configFileSchema.safeParse(raw);
|
|
318
377
|
const issues = [];
|
|
@@ -368,62 +427,28 @@ export function validateConfigObject(raw) {
|
|
|
368
427
|
defaultCount++;
|
|
369
428
|
const match = childObject(entry, "match");
|
|
370
429
|
if (match) {
|
|
371
|
-
|
|
372
|
-
const value = match[field];
|
|
373
|
-
if (value === undefined)
|
|
374
|
-
continue;
|
|
375
|
-
if (!Array.isArray(value)) {
|
|
376
|
-
issues.push({ severity: "error", path: `${base}.match.${field}`, message: "must be an array of regex strings" });
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
value.forEach((pattern, pIdx) => {
|
|
380
|
-
if (typeof pattern !== "string") {
|
|
381
|
-
issues.push({ severity: "error", path: `${base}.match.${field}[${pIdx}]`, message: "must be a string" });
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
try {
|
|
385
|
-
new RegExp(pattern);
|
|
386
|
-
}
|
|
387
|
-
catch (e) {
|
|
388
|
-
issues.push({
|
|
389
|
-
severity: "error",
|
|
390
|
-
path: `${base}.match.${field}[${pIdx}]`,
|
|
391
|
-
message: `invalid regex: ${e instanceof Error ? e.message : String(e)}`,
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
const metadata = childObject(match, "metadata");
|
|
397
|
-
if (metadata) {
|
|
398
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
399
|
-
if (!Array.isArray(value)) {
|
|
400
|
-
issues.push({ severity: "error", path: `${base}.match.metadata.${key}`, message: "must be an array of regex strings" });
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
value.forEach((pattern, pIdx) => {
|
|
404
|
-
if (typeof pattern !== "string") {
|
|
405
|
-
issues.push({ severity: "error", path: `${base}.match.metadata.${key}[${pIdx}]`, message: "must be a string" });
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
try {
|
|
409
|
-
new RegExp(pattern);
|
|
410
|
-
}
|
|
411
|
-
catch (e) {
|
|
412
|
-
issues.push({
|
|
413
|
-
severity: "error",
|
|
414
|
-
path: `${base}.match.metadata.${key}[${pIdx}]`,
|
|
415
|
-
message: `invalid regex: ${e instanceof Error ? e.message : String(e)}`,
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
}
|
|
430
|
+
validateMatchBlock(match, `${base}.match`, issues);
|
|
421
431
|
}
|
|
422
432
|
});
|
|
423
433
|
if (defaultCount > 1) {
|
|
424
434
|
issues.push({ severity: "error", path: "destinations", message: `at most one destination may set 'default: true' (found ${defaultCount})` });
|
|
425
435
|
}
|
|
426
436
|
}
|
|
437
|
+
if (Array.isArray(raw.ignore)) {
|
|
438
|
+
raw.ignore.forEach((entry, idx) => {
|
|
439
|
+
const base = `ignore[${idx}]`;
|
|
440
|
+
if (!isObject(entry)) {
|
|
441
|
+
issues.push({ severity: "error", path: base, message: "must be an object" });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const match = entry.match;
|
|
445
|
+
if (!isObject(match)) {
|
|
446
|
+
issues.push({ severity: "error", path: `${base}.match`, message: "must be an object" });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
validateMatchBlock(match, `${base}.match`, issues);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
427
452
|
const destinationNames = new Set();
|
|
428
453
|
if (Array.isArray(raw.destinations)) {
|
|
429
454
|
for (const entry of raw.destinations) {
|
|
@@ -100,11 +100,12 @@ const autoDistill = async (_config, { minSummaries = 5 } = {}) => {
|
|
|
100
100
|
return { distilled: false };
|
|
101
101
|
const promptStamp = `${DISTILL_PROMPT_DESCRIPTOR.id}@${DISTILL_PROMPT_DESCRIPTOR.version}`;
|
|
102
102
|
// Store distilled patterns
|
|
103
|
+
let storedPatterns = 0;
|
|
103
104
|
for (const pattern of patterns) {
|
|
104
105
|
if (!pattern.content)
|
|
105
106
|
continue;
|
|
106
107
|
const hash = createHash("sha256").update(`distilled:${pattern.content}`).digest("hex");
|
|
107
|
-
await qdrant.storeFact({
|
|
108
|
+
const storedId = await qdrant.storeFact({
|
|
108
109
|
content: pattern.content,
|
|
109
110
|
category: normalizeCategory(pattern.category ?? "engineering"),
|
|
110
111
|
domain: normalizeDomain(pattern.domain ?? "software_engineering"),
|
|
@@ -128,6 +129,12 @@ const autoDistill = async (_config, { minSummaries = 5 } = {}) => {
|
|
|
128
129
|
},
|
|
129
130
|
}),
|
|
130
131
|
}, { destination });
|
|
132
|
+
if (storedId)
|
|
133
|
+
storedPatterns++;
|
|
134
|
+
}
|
|
135
|
+
if (storedPatterns === 0) {
|
|
136
|
+
logFn("INFO", `Auto-distill: all ${patterns.length} patterns were ignored by config rules`);
|
|
137
|
+
return { distilled: false };
|
|
131
138
|
}
|
|
132
139
|
// Supersede the source summaries
|
|
133
140
|
for (const pt of batch) {
|
|
@@ -138,8 +145,8 @@ const autoDistill = async (_config, { minSummaries = 5 } = {}) => {
|
|
|
138
145
|
metadata: { prompt: promptStamp },
|
|
139
146
|
}));
|
|
140
147
|
}
|
|
141
|
-
logFn("INFO", `Auto-distill: consolidated ${batch.length} summaries into ${
|
|
142
|
-
return { distilled: true, count:
|
|
148
|
+
logFn("INFO", `Auto-distill: consolidated ${batch.length} summaries into ${storedPatterns} patterns`);
|
|
149
|
+
return { distilled: true, count: storedPatterns };
|
|
143
150
|
}
|
|
144
151
|
catch (e) {
|
|
145
152
|
logFn("ERROR", `Auto-distill failed: ${e.message}`);
|
|
@@ -446,6 +446,10 @@ const storeFacts = async (facts, sessionId, config, source) => {
|
|
|
446
446
|
};
|
|
447
447
|
try {
|
|
448
448
|
const dedup = await qdrant.dedupCheck(sanitizedFact.content, hash, undefined, undefined, routeInput);
|
|
449
|
+
if (dedup.action === "ignore") {
|
|
450
|
+
logFn("DEBUG", `Extraction: ignoring fact due to config rule '${dedup.ignoreRule ?? "unknown"}': "${sanitizedFact.content.slice(0, 80)}…"`);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
449
453
|
if (dedup.action === "skip") {
|
|
450
454
|
// Reinforce existing fact
|
|
451
455
|
if (dedup.existingId) {
|
|
@@ -610,6 +614,8 @@ const storeFacts = async (facts, sessionId, config, source) => {
|
|
|
610
614
|
};
|
|
611
615
|
if (dedup.action === "supersede" && dedup.existingId) {
|
|
612
616
|
const newId = await qdrant.storeFact(storePayload, routeInput);
|
|
617
|
+
if (!newId)
|
|
618
|
+
continue;
|
|
613
619
|
await qdrant.supersedeFact(dedup.existingId, newId, dedup.destination, buildOperationOrigin({
|
|
614
620
|
interface: "daemon",
|
|
615
621
|
action: "supersede",
|
|
@@ -624,8 +630,9 @@ const storeFacts = async (facts, sessionId, config, source) => {
|
|
|
624
630
|
stored++;
|
|
625
631
|
}
|
|
626
632
|
else {
|
|
627
|
-
await qdrant.storeFact(storePayload, routeInput);
|
|
628
|
-
|
|
633
|
+
const newId = await qdrant.storeFact(storePayload, routeInput);
|
|
634
|
+
if (newId)
|
|
635
|
+
stored++;
|
|
629
636
|
}
|
|
630
637
|
}
|
|
631
638
|
catch (e) {
|
package/dist/daemon/qdrant.d.ts
CHANGED
|
@@ -162,10 +162,11 @@ export interface QdrantScrollFilters {
|
|
|
162
162
|
direction: "asc" | "desc";
|
|
163
163
|
};
|
|
164
164
|
}
|
|
165
|
-
export type DedupAction = "insert" | "skip" | "supersede";
|
|
165
|
+
export type DedupAction = "insert" | "skip" | "supersede" | "ignore";
|
|
166
166
|
export interface DedupResult {
|
|
167
167
|
action: DedupAction;
|
|
168
168
|
destination?: string;
|
|
169
|
+
ignoreRule?: string;
|
|
169
170
|
existingId?: string;
|
|
170
171
|
existingCount?: number;
|
|
171
172
|
score?: number;
|
|
@@ -190,7 +191,7 @@ declare const activeDestinations: () => Destination[];
|
|
|
190
191
|
declare const searchFacts: (query: string, filters?: QdrantSearchFilters, limit?: number, destinationRef?: DestinationRef) => Promise<QdrantSearchResult[]>;
|
|
191
192
|
declare const scrollFacts: (filters?: QdrantScrollFilters, limit?: number, destinationRef?: DestinationRef) => Promise<QdrantScrollResult[]>;
|
|
192
193
|
declare const scrollFactsAcrossDestinations: (filters?: QdrantScrollFilters, limit?: number) => Promise<QdrantScrollResult[]>;
|
|
193
|
-
declare const storeFact: (fact: StoreFact, routeInput?: RoutingInput) => Promise<string>;
|
|
194
|
+
declare const storeFact: (fact: StoreFact, routeInput?: RoutingInput) => Promise<string | null>;
|
|
194
195
|
declare const supersedeFact: (oldFactId: string, newFactId: string, destinationRef?: DestinationRef, origin?: OperationOrigin) => Promise<void>;
|
|
195
196
|
declare const reinforceFact: (factId: string, currentCount: number, destinationRef?: DestinationRef, origin?: OperationOrigin) => Promise<void>;
|
|
196
197
|
declare const dedupCheck: (content: string, contentHashVal: string, { exactThreshold, supersedeThreshold }?: DedupThresholds, workspaceId?: string, routeInput?: RoutingInput) => Promise<DedupResult>;
|
package/dist/daemon/qdrant.js
CHANGED
|
@@ -11,7 +11,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
11
11
|
import { getEffectiveDestinations, loadConfig } from "../config.js";
|
|
12
12
|
import { embed, initEmbedding, getEmbeddingConfig } from "../llm/index.js";
|
|
13
13
|
import { QdrantPool } from "../lib/qdrant-pool.js";
|
|
14
|
-
import { buildResolver } from "../routing.js";
|
|
14
|
+
import { buildResolver, findMatchingIgnoreRule } from "../routing.js";
|
|
15
15
|
import { DEFAULT_DOMAIN, QDRANT_INDEXES, categoryForMemorySubtype, layerForMemorySubtype, normalizeCategory, normalizeDomain, normalizeKind, validateMemorySubtype, } from "../mcp/taxonomy.js";
|
|
16
16
|
import { combineRedactions, redactStorageText, } from "../privacy/redaction.js";
|
|
17
17
|
import { buildOperationOrigin } from "../provenance/origin.js";
|
|
@@ -395,12 +395,18 @@ const storeFact = async (fact, routeInput) => {
|
|
|
395
395
|
if (redaction.redacted) {
|
|
396
396
|
payload.redaction = redaction;
|
|
397
397
|
}
|
|
398
|
-
const
|
|
398
|
+
const writeInput = mergeRoutingInputs(routingInputForFact(fact, redactedContent.text, payload.entities, {
|
|
399
399
|
category: normalizedCategory,
|
|
400
400
|
domain: normalizedDomain,
|
|
401
401
|
kind: normalizedKind,
|
|
402
402
|
...(normalizedSubtype ? { memory_subtype: normalizedSubtype } : {}),
|
|
403
|
-
}), routeInput)
|
|
403
|
+
}), routeInput);
|
|
404
|
+
const ignored = findMatchingIgnoreRule(writeInput, loadConfig().ignore);
|
|
405
|
+
if (ignored) {
|
|
406
|
+
logFn("INFO", `Qdrant: ignored fact write due to ignore rule '${ignored.name}' [${normalizedCategory}] ${redactedContent.text.slice(0, 60)}`);
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
const destination = resolveDestination(writeInput);
|
|
404
410
|
const vector = await embed(redactedContent.text);
|
|
405
411
|
await qdrantRequest("PUT", `/collections/${destination.collection}/points`, {
|
|
406
412
|
points: [{ id, vector, payload }],
|
|
@@ -446,7 +452,13 @@ const reinforceFact = async (factId, currentCount, destinationRef, origin) => {
|
|
|
446
452
|
logFn("DEBUG", `Qdrant: reinforced fact ${factId} in '${destination.name}'`);
|
|
447
453
|
};
|
|
448
454
|
const dedupCheck = async (content, contentHashVal, { exactThreshold = 0.92, supersedeThreshold = 0.80 } = {}, workspaceId, routeInput) => {
|
|
449
|
-
const
|
|
455
|
+
const input = mergeRoutingInputs({ content }, routeInput);
|
|
456
|
+
const ignored = findMatchingIgnoreRule(input, loadConfig().ignore);
|
|
457
|
+
if (ignored) {
|
|
458
|
+
logFn("INFO", `Qdrant: ignored dedup/write candidate due to ignore rule '${ignored.name}'`);
|
|
459
|
+
return { action: "ignore", ignoreRule: ignored.name };
|
|
460
|
+
}
|
|
461
|
+
const destination = resolveDestination(input);
|
|
450
462
|
// First: hash-based exact check (fast, no embedding)
|
|
451
463
|
try {
|
|
452
464
|
const must = [
|
|
@@ -69,7 +69,7 @@ declare const storeRelation: (fromEntity: string, toEntity: string, relationType
|
|
|
69
69
|
durability?: string;
|
|
70
70
|
directionality_clarity?: string;
|
|
71
71
|
};
|
|
72
|
-
}) => Promise<string>;
|
|
72
|
+
}) => Promise<string | null>;
|
|
73
73
|
declare const tick: (config: BikkyConfig) => Promise<void>;
|
|
74
74
|
/** Reset state (for testing). */
|
|
75
75
|
declare const _reset: () => void;
|
package/dist/daemon/relations.js
CHANGED
|
@@ -280,6 +280,10 @@ const storeRelation = async (fromEntity, toEntity, relationType, content, candid
|
|
|
280
280
|
to: toEntity,
|
|
281
281
|
},
|
|
282
282
|
}, destination ? { destination } : undefined);
|
|
283
|
+
if (!id) {
|
|
284
|
+
logFn("INFO", `Relations: ignored ${fromEntity} —[${relationType}]→ ${toEntity} due to ignore rules`);
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
283
287
|
logFn("INFO", `Relations: inferred ${fromEntity} —[${relationType}]→ ${toEntity} (id: ${id})`);
|
|
284
288
|
return id;
|
|
285
289
|
};
|
|
@@ -358,12 +362,17 @@ const tick = async (config) => {
|
|
|
358
362
|
.update(`daemon-relation:${pairKey(result.from, result.to)}:${result.type}`)
|
|
359
363
|
.digest("hex");
|
|
360
364
|
const dedup = await qdrant.dedupCheck(result.content, hash, undefined, undefined, { destination });
|
|
365
|
+
if (dedup.action === "ignore") {
|
|
366
|
+
logFn("DEBUG", `Relations: ignoring ${candidate.entityA}↔${candidate.entityB} due to config rule '${dedup.ignoreRule ?? "unknown"}'`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
361
369
|
if (dedup.action === "skip") {
|
|
362
370
|
logFn("DEBUG", `Relations: skipping duplicate ${candidate.entityA}↔${candidate.entityB}`);
|
|
363
371
|
continue;
|
|
364
372
|
}
|
|
365
|
-
await storeRelation(result.from, result.to, result.type, result.content, candidate, destination, { evidence: result.evidence, confidence: result.confidence, inVocabulary: result.inVocabulary, judgment: result.judgment });
|
|
366
|
-
|
|
373
|
+
const storedId = await storeRelation(result.from, result.to, result.type, result.content, candidate, destination, { evidence: result.evidence, confidence: result.confidence, inVocabulary: result.inVocabulary, judgment: result.judgment });
|
|
374
|
+
if (storedId)
|
|
375
|
+
inferred++;
|
|
367
376
|
}
|
|
368
377
|
catch (e) {
|
|
369
378
|
failures++;
|
package/dist/mcp/tools.js
CHANGED
|
@@ -6,7 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
import { STALENESS_DAYS, THRESHOLD_DUPLICATE, THRESHOLD_RELATED, QDRANT_INDEXES, categoryValues, categoryEnumDescription, domainValues, domainEnumDescription, kindValues, kindEnumDescription, memorySubtypeValues, memorySubtypeEnumDescription, DEFAULT_CATEGORY, DEFAULT_DOMAIN, DEFAULT_KIND, categoryForMemorySubtype, layerForMemorySubtype, normalizeCategory, normalizeDomain, normalizeKind, validateMemorySubtype, } from "./taxonomy.js";
|
|
7
7
|
import { contentHash, daysSince, lastActivityDate, computeCombinedScore, buildFilter, formatFact, structuredFact, MEMORY_RECALL_EXCLUDED_KINDS, } from "./helpers.js";
|
|
8
8
|
import { ready, setupError, setReady, log, embed, getEmbeddingConfig, qdrantReq, ensureCollectionsAll, qdrantUpsert, qdrantSearch, qdrantScroll, qdrantSetPayload, qdrantGetPoints, rebuildPool, hasPool, listDestinations, resolveDest, findPointById, } from "./api.js";
|
|
9
|
-
import { DestinationNotFoundError } from "../routing.js";
|
|
9
|
+
import { DestinationNotFoundError, findMatchingIgnoreRule } from "../routing.js";
|
|
10
10
|
import { saveConfig, loadConfig, EXTRACTION_HEALTH_PATH } from "../config.js";
|
|
11
11
|
import { availableSearchScopes, resolveSearchScope, SearchScopeNotFoundError, } from "../search-scope.js";
|
|
12
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -114,6 +114,53 @@ function resolveDestOrError(input) {
|
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
+
function ignoredWriteResult(tool, match) {
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
120
|
+
action: "ignored",
|
|
121
|
+
status: "ignored",
|
|
122
|
+
ignored: true,
|
|
123
|
+
tool,
|
|
124
|
+
rule: match.name,
|
|
125
|
+
rule_index: match.index,
|
|
126
|
+
description: match.rule.description ?? null,
|
|
127
|
+
message: `Memory write ignored by config rule '${match.name}'.`,
|
|
128
|
+
}) }],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function ignoreWriteOrError(input, tool) {
|
|
132
|
+
try {
|
|
133
|
+
const match = findMatchingIgnoreRule(input, loadConfig().ignore);
|
|
134
|
+
return match ? { match } : {};
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
138
|
+
return {
|
|
139
|
+
error: {
|
|
140
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
141
|
+
status: "error",
|
|
142
|
+
message: `Failed to evaluate ignore rules for ${tool}: ${msg}`,
|
|
143
|
+
}, null, 2) }],
|
|
144
|
+
isError: true,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function telemetryIgnored(input, subtype) {
|
|
150
|
+
try {
|
|
151
|
+
const match = findMatchingIgnoreRule(input, loadConfig().ignore);
|
|
152
|
+
if (!match)
|
|
153
|
+
return false;
|
|
154
|
+
log("INFO", `Skipped ${subtype} write due to ignore rule '${match.name}'`);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
log("WARN", `Failed to evaluate ignore rules for ${subtype}: ${e instanceof Error ? e.message : String(e)}`);
|
|
159
|
+
// Telemetry can include recall queries or feedback notes, so fail closed
|
|
160
|
+
// rather than risk persisting content that an ignore rule was meant to block.
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
117
164
|
function withDestination(point, destination) {
|
|
118
165
|
return { ...point, _destination: destination };
|
|
119
166
|
}
|
|
@@ -167,6 +214,22 @@ async function recordRecallTelemetry(args) {
|
|
|
167
214
|
const entities = [...new Set(points.flatMap((point) => point.payload.entities ?? []))].slice(0, 25);
|
|
168
215
|
const eventContent = `Recall returned ${points.length} fact(s) from ${dest.name}: ${args.redactedQuery.text}`;
|
|
169
216
|
const redactedEvent = redactStorageText(eventContent);
|
|
217
|
+
const eventRoutingInput = buildMemoryRoutingInput({
|
|
218
|
+
cwd: process.cwd(),
|
|
219
|
+
content: redactedEvent.text,
|
|
220
|
+
entities,
|
|
221
|
+
context: {
|
|
222
|
+
category: categoryForMemorySubtype("recall_event") ?? "system",
|
|
223
|
+
domain: "software_engineering",
|
|
224
|
+
kind: "telemetry",
|
|
225
|
+
memory_subtype: "recall_event",
|
|
226
|
+
layer: "memory_object",
|
|
227
|
+
search_scope: args.searchScope.name,
|
|
228
|
+
destination: dest.name,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
if (telemetryIgnored(eventRoutingInput, "recall_event"))
|
|
232
|
+
continue;
|
|
170
233
|
const eventPayload = {
|
|
171
234
|
content: redactedEvent.text,
|
|
172
235
|
category: categoryForMemorySubtype("recall_event") ?? "system",
|
|
@@ -698,7 +761,7 @@ export function registerTools(mcp) {
|
|
|
698
761
|
type: redactedRelation.type.text,
|
|
699
762
|
to: redactedRelation.to.text,
|
|
700
763
|
} : null;
|
|
701
|
-
const
|
|
764
|
+
const writeInput = memoryWriteRoutingInput({
|
|
702
765
|
tool: "memory_store",
|
|
703
766
|
destination,
|
|
704
767
|
content: redactedContent.text,
|
|
@@ -719,7 +782,13 @@ export function registerTools(mcp) {
|
|
|
719
782
|
review_status,
|
|
720
783
|
supersedes,
|
|
721
784
|
},
|
|
722
|
-
})
|
|
785
|
+
});
|
|
786
|
+
const ignored = ignoreWriteOrError(writeInput, "memory_store");
|
|
787
|
+
if (ignored.error)
|
|
788
|
+
return ignored.error;
|
|
789
|
+
if (ignored.match)
|
|
790
|
+
return ignoredWriteResult("memory_store", ignored.match);
|
|
791
|
+
const resolved = resolveDestOrError(writeInput);
|
|
723
792
|
if (resolved.error)
|
|
724
793
|
return resolved.error;
|
|
725
794
|
const dest = resolved.dest;
|
|
@@ -1514,35 +1583,52 @@ export function registerTools(mcp) {
|
|
|
1514
1583
|
});
|
|
1515
1584
|
// Write a telemetry feedback_event row so the signal is also visible
|
|
1516
1585
|
// to aggregations and review tooling.
|
|
1517
|
-
const eventId = newId();
|
|
1518
1586
|
const eventContent = note
|
|
1519
1587
|
? `Fact ${fact_id} marked useful: ${note}`
|
|
1520
1588
|
: `Fact ${fact_id} marked useful.`;
|
|
1521
1589
|
const redactedEvent = redactStorageText(eventContent);
|
|
1522
|
-
|
|
1590
|
+
let eventId = null;
|
|
1591
|
+
const eventInput = buildMemoryRoutingInput({
|
|
1592
|
+
cwd: process.cwd(),
|
|
1523
1593
|
content: redactedEvent.text,
|
|
1524
|
-
category: categoryForMemorySubtype("feedback_event") ?? "system",
|
|
1525
|
-
domain: "software_engineering",
|
|
1526
|
-
kind: "telemetry",
|
|
1527
|
-
memory_subtype: "feedback_event",
|
|
1528
|
-
layer: "memory_object",
|
|
1529
1594
|
entities: [],
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1595
|
+
context: {
|
|
1596
|
+
category: categoryForMemorySubtype("feedback_event") ?? "system",
|
|
1597
|
+
domain: "software_engineering",
|
|
1598
|
+
kind: "telemetry",
|
|
1599
|
+
memory_subtype: "feedback_event",
|
|
1600
|
+
layer: "memory_object",
|
|
1601
|
+
target_fact_id: fact_id,
|
|
1602
|
+
feedback_kind: "useful",
|
|
1603
|
+
},
|
|
1604
|
+
});
|
|
1605
|
+
if (!telemetryIgnored(eventInput, "feedback_event")) {
|
|
1606
|
+
eventId = newId();
|
|
1607
|
+
const eventPayload = {
|
|
1608
|
+
content: redactedEvent.text,
|
|
1609
|
+
category: categoryForMemorySubtype("feedback_event") ?? "system",
|
|
1610
|
+
domain: "software_engineering",
|
|
1611
|
+
kind: "telemetry",
|
|
1612
|
+
memory_subtype: "feedback_event",
|
|
1613
|
+
layer: "memory_object",
|
|
1614
|
+
entities: [],
|
|
1615
|
+
origin: feedbackOrigin,
|
|
1616
|
+
confidence: 1.0,
|
|
1617
|
+
importance: 0.3,
|
|
1618
|
+
content_hash: contentHash("feedback_event", `${fact_id}:useful:${now}`),
|
|
1619
|
+
target_fact_id: fact_id,
|
|
1620
|
+
feedback_kind: "useful",
|
|
1621
|
+
created_at: now,
|
|
1622
|
+
updated_at: now,
|
|
1623
|
+
};
|
|
1624
|
+
addRedactionPayload(eventPayload, redactedEvent);
|
|
1625
|
+
try {
|
|
1626
|
+
const eventVector = await embed(redactedEvent.text);
|
|
1627
|
+
await qdrantUpsert(dest, eventId, eventVector, eventPayload);
|
|
1628
|
+
}
|
|
1629
|
+
catch (e) {
|
|
1630
|
+
log("WARN", `Failed to record feedback_event: ${e instanceof Error ? e.message : String(e)}`);
|
|
1631
|
+
}
|
|
1546
1632
|
}
|
|
1547
1633
|
return {
|
|
1548
1634
|
content: [{ type: "text", text: JSON.stringify({
|
|
@@ -1550,7 +1636,7 @@ export function registerTools(mcp) {
|
|
|
1550
1636
|
fact_id,
|
|
1551
1637
|
destination: dest.name,
|
|
1552
1638
|
useful_count: newCount,
|
|
1553
|
-
event_id: eventId,
|
|
1639
|
+
...(eventId ? { event_id: eventId } : { event_ignored: true }),
|
|
1554
1640
|
}) }],
|
|
1555
1641
|
};
|
|
1556
1642
|
}
|
|
@@ -1591,31 +1677,48 @@ export function registerTools(mcp) {
|
|
|
1591
1677
|
updated_at: now,
|
|
1592
1678
|
last_operation_origin: feedbackOrigin,
|
|
1593
1679
|
});
|
|
1594
|
-
const eventId = newId();
|
|
1595
1680
|
const eventContent = notes
|
|
1596
1681
|
? `Fact ${fact_id} outcome=${outcome}: ${notes}`
|
|
1597
1682
|
: `Fact ${fact_id} outcome=${outcome}.`;
|
|
1598
1683
|
const redactedEvent = redactStorageText(eventContent);
|
|
1599
|
-
|
|
1684
|
+
let eventId = null;
|
|
1685
|
+
const eventInput = buildMemoryRoutingInput({
|
|
1686
|
+
cwd: process.cwd(),
|
|
1600
1687
|
content: redactedEvent.text,
|
|
1601
|
-
category: categoryForMemorySubtype("outcome_event") ?? "system",
|
|
1602
|
-
domain: "software_engineering",
|
|
1603
|
-
kind: "telemetry",
|
|
1604
|
-
memory_subtype: "outcome_event",
|
|
1605
|
-
layer: "memory_object",
|
|
1606
1688
|
entities: [],
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1689
|
+
context: {
|
|
1690
|
+
category: categoryForMemorySubtype("outcome_event") ?? "system",
|
|
1691
|
+
domain: "software_engineering",
|
|
1692
|
+
kind: "telemetry",
|
|
1693
|
+
memory_subtype: "outcome_event",
|
|
1694
|
+
layer: "memory_object",
|
|
1695
|
+
target_fact_id: fact_id,
|
|
1696
|
+
outcome,
|
|
1697
|
+
},
|
|
1698
|
+
});
|
|
1699
|
+
if (!telemetryIgnored(eventInput, "outcome_event")) {
|
|
1700
|
+
eventId = newId();
|
|
1701
|
+
const eventPayload = {
|
|
1702
|
+
content: redactedEvent.text,
|
|
1703
|
+
category: categoryForMemorySubtype("outcome_event") ?? "system",
|
|
1704
|
+
domain: "software_engineering",
|
|
1705
|
+
kind: "telemetry",
|
|
1706
|
+
memory_subtype: "outcome_event",
|
|
1707
|
+
layer: "memory_object",
|
|
1708
|
+
entities: [],
|
|
1709
|
+
origin: feedbackOrigin,
|
|
1710
|
+
confidence: 1.0,
|
|
1711
|
+
importance: outcome === "wrong" || outcome === "misleading" ? 0.6 : 0.3,
|
|
1712
|
+
content_hash: contentHash("outcome_event", `${fact_id}:${outcome}:${now}`),
|
|
1713
|
+
target_fact_id: fact_id,
|
|
1714
|
+
outcome,
|
|
1715
|
+
created_at: now,
|
|
1716
|
+
updated_at: now,
|
|
1717
|
+
};
|
|
1718
|
+
addRedactionPayload(eventPayload, redactedEvent);
|
|
1719
|
+
const eventVector = await embed(redactedEvent.text);
|
|
1720
|
+
await qdrantUpsert(dest, eventId, eventVector, eventPayload);
|
|
1721
|
+
}
|
|
1619
1722
|
return {
|
|
1620
1723
|
content: [{ type: "text", text: JSON.stringify({
|
|
1621
1724
|
status: "outcome_recorded",
|
|
@@ -1623,7 +1726,7 @@ export function registerTools(mcp) {
|
|
|
1623
1726
|
destination: dest.name,
|
|
1624
1727
|
outcome,
|
|
1625
1728
|
[counterField]: counterValue,
|
|
1626
|
-
event_id: eventId,
|
|
1729
|
+
...(eventId ? { event_id: eventId } : { event_ignored: true }),
|
|
1627
1730
|
}) }],
|
|
1628
1731
|
};
|
|
1629
1732
|
}
|
|
@@ -1654,7 +1757,7 @@ export function registerTools(mcp) {
|
|
|
1654
1757
|
try {
|
|
1655
1758
|
const normalizedEntities = (entities ?? []).map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
1656
1759
|
const redactedContent = redactStorageText(content);
|
|
1657
|
-
const
|
|
1760
|
+
const writeInput = memoryWriteRoutingInput({
|
|
1658
1761
|
tool: "memory_session_summary",
|
|
1659
1762
|
destination,
|
|
1660
1763
|
content: redactedContent.text,
|
|
@@ -1670,7 +1773,13 @@ export function registerTools(mcp) {
|
|
|
1670
1773
|
task_key,
|
|
1671
1774
|
repo,
|
|
1672
1775
|
},
|
|
1673
|
-
})
|
|
1776
|
+
});
|
|
1777
|
+
const ignored = ignoreWriteOrError(writeInput, "memory_session_summary");
|
|
1778
|
+
if (ignored.error)
|
|
1779
|
+
return ignored.error;
|
|
1780
|
+
if (ignored.match)
|
|
1781
|
+
return ignoredWriteResult("memory_session_summary", ignored.match);
|
|
1782
|
+
const resolved = resolveDestOrError(writeInput);
|
|
1674
1783
|
if (resolved.error)
|
|
1675
1784
|
return resolved.error;
|
|
1676
1785
|
const dest = resolved.dest;
|
|
@@ -1751,7 +1860,7 @@ export function registerTools(mcp) {
|
|
|
1751
1860
|
try {
|
|
1752
1861
|
const normalizedEntities = entities.map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
1753
1862
|
const redactedContent = redactStorageText(content);
|
|
1754
|
-
const
|
|
1863
|
+
const writeInput = memoryWriteRoutingInput({
|
|
1755
1864
|
tool: "memory_distill",
|
|
1756
1865
|
destination,
|
|
1757
1866
|
content: redactedContent.text,
|
|
@@ -1766,7 +1875,13 @@ export function registerTools(mcp) {
|
|
|
1766
1875
|
repo,
|
|
1767
1876
|
supersedes,
|
|
1768
1877
|
},
|
|
1769
|
-
})
|
|
1878
|
+
});
|
|
1879
|
+
const ignored = ignoreWriteOrError(writeInput, "memory_distill");
|
|
1880
|
+
if (ignored.error)
|
|
1881
|
+
return ignored.error;
|
|
1882
|
+
if (ignored.match)
|
|
1883
|
+
return ignoredWriteResult("memory_distill", ignored.match);
|
|
1884
|
+
const resolved = resolveDestOrError(writeInput);
|
|
1770
1885
|
if (resolved.error)
|
|
1771
1886
|
return resolved.error;
|
|
1772
1887
|
const dest = resolved.dest;
|
|
@@ -1949,9 +2064,37 @@ export function registerTools(mcp) {
|
|
|
1949
2064
|
const { dest, point } = located;
|
|
1950
2065
|
const origPayload = point.payload;
|
|
1951
2066
|
const redactedCorrected = redactStorageText(corrected_content);
|
|
2067
|
+
const origCategory = normalizeCategory(origPayload?.category ?? DEFAULT_CATEGORY);
|
|
2068
|
+
const origDomain = normalizeDomain(origPayload?.domain ?? DEFAULT_DOMAIN);
|
|
2069
|
+
const origKind = normalizeKind(origPayload?.kind ?? "fact");
|
|
2070
|
+
const correctedEntities = origPayload?.entities ?? [];
|
|
2071
|
+
const writeInput = memoryWriteRoutingInput({
|
|
2072
|
+
tool: "memory_review",
|
|
2073
|
+
destination: dest.name,
|
|
2074
|
+
content: redactedCorrected.text,
|
|
2075
|
+
entities: correctedEntities,
|
|
2076
|
+
metadata: origPayload?.metadata,
|
|
2077
|
+
context: {
|
|
2078
|
+
category: origCategory,
|
|
2079
|
+
domain: origDomain,
|
|
2080
|
+
kind: origKind,
|
|
2081
|
+
memory_subtype: origPayload?.memory_subtype,
|
|
2082
|
+
layer: origPayload?.layer,
|
|
2083
|
+
episode_id: origPayload?.episode_id,
|
|
2084
|
+
workstream_key: origPayload?.workstream_key,
|
|
2085
|
+
task_key: origPayload?.task_key,
|
|
2086
|
+
repo: origPayload?.repo,
|
|
2087
|
+
branch: origPayload?.branch,
|
|
2088
|
+
corrected_from: fact_id,
|
|
2089
|
+
},
|
|
2090
|
+
});
|
|
2091
|
+
const ignored = ignoreWriteOrError(writeInput, "memory_review");
|
|
2092
|
+
if (ignored.error)
|
|
2093
|
+
return ignored.error;
|
|
2094
|
+
if (ignored.match)
|
|
2095
|
+
return ignoredWriteResult("memory_review", ignored.match);
|
|
1952
2096
|
const vector = await embed(redactedCorrected.text);
|
|
1953
2097
|
const correctedId = crypto.randomUUID();
|
|
1954
|
-
const origCategory = normalizeCategory(origPayload?.category ?? DEFAULT_CATEGORY);
|
|
1955
2098
|
const hash = contentHash(origCategory, redactedCorrected.text);
|
|
1956
2099
|
const correctOrigin = mcpOrigin({
|
|
1957
2100
|
action: "correct",
|
|
@@ -1961,8 +2104,8 @@ export function registerTools(mcp) {
|
|
|
1961
2104
|
const correctedPayload = {
|
|
1962
2105
|
content: redactedCorrected.text,
|
|
1963
2106
|
category: origCategory,
|
|
1964
|
-
domain:
|
|
1965
|
-
kind:
|
|
2107
|
+
domain: origDomain,
|
|
2108
|
+
kind: origKind,
|
|
1966
2109
|
...(origPayload?.memory_subtype ? { memory_subtype: origPayload.memory_subtype } : {}),
|
|
1967
2110
|
...(origPayload?.layer ? { layer: origPayload.layer } : {}),
|
|
1968
2111
|
...(origPayload?.episode_id ? { episode_id: origPayload.episode_id } : {}),
|
|
@@ -1970,7 +2113,7 @@ export function registerTools(mcp) {
|
|
|
1970
2113
|
...(origPayload?.task_key ? { task_key: origPayload.task_key } : {}),
|
|
1971
2114
|
...(origPayload?.repo ? { repo: origPayload.repo } : {}),
|
|
1972
2115
|
...(origPayload?.branch ? { branch: origPayload.branch } : {}),
|
|
1973
|
-
entities:
|
|
2116
|
+
entities: correctedEntities,
|
|
1974
2117
|
origin: correctOrigin,
|
|
1975
2118
|
confidence: 0.95,
|
|
1976
2119
|
importance: origPayload?.importance ?? 0.5,
|
package/dist/routing.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Pure: no I/O, no globals. Easy to unit test.
|
|
17
17
|
*/
|
|
18
|
-
import type { Destination } from "./config.js";
|
|
18
|
+
import type { Destination, DestinationMatch, IgnoreRule } from "./config.js";
|
|
19
19
|
export interface RoutingInput {
|
|
20
20
|
/** Explicit destination name override. Throws if it doesn't match a destination. */
|
|
21
21
|
destination?: string | null;
|
|
@@ -36,6 +36,13 @@ export declare class DestinationNotFoundError extends Error {
|
|
|
36
36
|
export declare class NoDestinationsConfiguredError extends Error {
|
|
37
37
|
constructor();
|
|
38
38
|
}
|
|
39
|
+
export interface IgnoreMatch {
|
|
40
|
+
rule: IgnoreRule;
|
|
41
|
+
index: number;
|
|
42
|
+
name: string;
|
|
43
|
+
}
|
|
44
|
+
export declare function routingInputMatches(match: DestinationMatch | undefined, input: RoutingInput): boolean;
|
|
45
|
+
export declare function findMatchingIgnoreRule(input: RoutingInput, ignoreRules: ReadonlyArray<IgnoreRule>): IgnoreMatch | null;
|
|
39
46
|
/**
|
|
40
47
|
* Resolve a destination from caller input + a (pre-loaded) list of destinations.
|
|
41
48
|
*
|
package/dist/routing.js
CHANGED
|
@@ -69,6 +69,21 @@ function destinationMatches(compiled, input) {
|
|
|
69
69
|
}
|
|
70
70
|
return false;
|
|
71
71
|
}
|
|
72
|
+
export function routingInputMatches(match, input) {
|
|
73
|
+
return destinationMatches(compileMatch(match), input);
|
|
74
|
+
}
|
|
75
|
+
export function findMatchingIgnoreRule(input, ignoreRules) {
|
|
76
|
+
for (const [index, rule] of ignoreRules.entries()) {
|
|
77
|
+
if (routingInputMatches(rule.match, input)) {
|
|
78
|
+
return {
|
|
79
|
+
rule,
|
|
80
|
+
index,
|
|
81
|
+
name: rule.name?.trim() || `ignore[${index}]`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
72
87
|
function pickFallback(destinations) {
|
|
73
88
|
const explicit = destinations.find((d) => d.default === true);
|
|
74
89
|
if (explicit)
|
package/docs/configuration.md
CHANGED
|
@@ -362,6 +362,39 @@ Matching details:
|
|
|
362
362
|
- Read/search tools also accept `search_scope`; call `memory_search_scopes` or `get_setup_status` to see available scopes and descriptions.
|
|
363
363
|
- All destinations share one embedding provider, so every destination collection must use the same vector dimensions.
|
|
364
364
|
|
|
365
|
+
### Ignore rules
|
|
366
|
+
|
|
367
|
+
Use top-level `ignore` rules when certain memories should not be stored at all. Ignore rules use the same `match` block shape as destination routing (`cwd`, `entity`, `content`, and `metadata` regex arrays), and memory-write `content` matching sees the same flattened memory context used by destination routing.
|
|
368
|
+
|
|
369
|
+
Ignore rules run before destination resolution, embedding, deduplication, superseding, relation sidecars, telemetry upserts, and Qdrant writes. They also take precedence over explicit `destination` overrides, so an ignored topic cannot be stored by selecting a destination manually.
|
|
370
|
+
|
|
371
|
+
```jsonc
|
|
372
|
+
{
|
|
373
|
+
"ignore": [
|
|
374
|
+
{
|
|
375
|
+
"name": "personal-topics",
|
|
376
|
+
"description": "Do not persist personal-topic memories.",
|
|
377
|
+
"match": {
|
|
378
|
+
"entity": ["^[Rr]esume$"],
|
|
379
|
+
"content": ["\\b[Rr]esume\\b"],
|
|
380
|
+
"metadata": { "repo": ["^private/"] }
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
]
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
When an MCP write tool is ignored, it returns a structured non-error response such as:
|
|
388
|
+
|
|
389
|
+
```json
|
|
390
|
+
{
|
|
391
|
+
"action": "ignored",
|
|
392
|
+
"status": "ignored",
|
|
393
|
+
"ignored": true,
|
|
394
|
+
"rule": "personal-topics"
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
365
398
|
Migrating from `workspace_id` pre-v0.4:
|
|
366
399
|
|
|
367
400
|
- Existing top-level `qdrant_url`, `qdrant_api_key`, and `collection` configs still work as a single synthesized destination.
|