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