bikky 0.3.1 → 0.3.3
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 +124 -35
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +22 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +61 -6
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +17 -9
- package/dist/config.test.js.map +1 -1
- package/dist/daemon/capture-policy.d.ts +95 -0
- package/dist/daemon/capture-policy.d.ts.map +1 -0
- package/dist/daemon/capture-policy.js +139 -0
- package/dist/daemon/capture-policy.js.map +1 -0
- package/dist/daemon/capture-policy.test.d.ts +2 -0
- package/dist/daemon/capture-policy.test.d.ts.map +1 -0
- package/dist/daemon/capture-policy.test.js +46 -0
- package/dist/daemon/capture-policy.test.js.map +1 -0
- package/dist/daemon/consolidation.d.ts.map +1 -1
- package/dist/daemon/consolidation.js +84 -98
- package/dist/daemon/consolidation.js.map +1 -1
- package/dist/daemon/episode-summary.d.ts +72 -0
- package/dist/daemon/episode-summary.d.ts.map +1 -0
- package/dist/daemon/episode-summary.js +208 -0
- package/dist/daemon/episode-summary.js.map +1 -0
- package/dist/daemon/episode-summary.test.d.ts +2 -0
- package/dist/daemon/episode-summary.test.d.ts.map +1 -0
- package/dist/daemon/episode-summary.test.js +101 -0
- package/dist/daemon/episode-summary.test.js.map +1 -0
- package/dist/daemon/extraction.d.ts +25 -0
- package/dist/daemon/extraction.d.ts.map +1 -1
- package/dist/daemon/extraction.js +244 -124
- package/dist/daemon/extraction.js.map +1 -1
- package/dist/daemon/extraction.test.d.ts +2 -0
- package/dist/daemon/extraction.test.d.ts.map +1 -0
- package/dist/daemon/extraction.test.js +106 -0
- package/dist/daemon/extraction.test.js.map +1 -0
- package/dist/daemon/loop.d.ts.map +1 -1
- package/dist/daemon/loop.js +8 -6
- package/dist/daemon/loop.js.map +1 -1
- package/dist/daemon/qdrant.d.ts +59 -8
- package/dist/daemon/qdrant.d.ts.map +1 -1
- package/dist/daemon/qdrant.js +74 -23
- package/dist/daemon/qdrant.js.map +1 -1
- package/dist/daemon/qdrant.test.js +2 -2
- package/dist/daemon/qdrant.test.js.map +1 -1
- package/dist/daemon/relations.d.ts +6 -1
- package/dist/daemon/relations.d.ts.map +1 -1
- package/dist/daemon/relations.js +44 -63
- package/dist/daemon/relations.js.map +1 -1
- package/dist/daemon/session-index.d.ts +60 -0
- package/dist/daemon/session-index.d.ts.map +1 -0
- package/dist/daemon/session-index.js +136 -0
- package/dist/daemon/session-index.js.map +1 -0
- package/dist/daemon/session-index.test.d.ts +2 -0
- package/dist/daemon/session-index.test.d.ts.map +1 -0
- package/dist/daemon/session-index.test.js +54 -0
- package/dist/daemon/session-index.test.js.map +1 -0
- package/dist/daemon/session-summary.d.ts +69 -0
- package/dist/daemon/session-summary.d.ts.map +1 -0
- package/dist/daemon/session-summary.js +200 -0
- package/dist/daemon/session-summary.js.map +1 -0
- package/dist/daemon/session-summary.test.d.ts +2 -0
- package/dist/daemon/session-summary.test.d.ts.map +1 -0
- package/dist/daemon/session-summary.test.js +160 -0
- package/dist/daemon/session-summary.test.js.map +1 -0
- package/dist/daemon/staleness.test.d.ts +7 -0
- package/dist/daemon/staleness.test.d.ts.map +1 -0
- package/dist/daemon/staleness.test.js +128 -0
- package/dist/daemon/staleness.test.js.map +1 -0
- package/dist/daemon/workstream-summary.d.ts +61 -0
- package/dist/daemon/workstream-summary.d.ts.map +1 -0
- package/dist/daemon/workstream-summary.js +220 -0
- package/dist/daemon/workstream-summary.js.map +1 -0
- package/dist/daemon/workstream-summary.test.d.ts +2 -0
- package/dist/daemon/workstream-summary.test.d.ts.map +1 -0
- package/dist/daemon/workstream-summary.test.js +86 -0
- package/dist/daemon/workstream-summary.test.js.map +1 -0
- package/dist/lib/qdrant-client.d.ts +6 -1
- package/dist/lib/qdrant-client.d.ts.map +1 -1
- package/dist/lib/qdrant-client.js +3 -4
- package/dist/lib/qdrant-client.js.map +1 -1
- package/dist/lib/qdrant-client.test.js +21 -2
- package/dist/lib/qdrant-client.test.js.map +1 -1
- package/dist/lifecycle.test.d.ts +8 -0
- package/dist/lifecycle.test.d.ts.map +1 -0
- package/dist/lifecycle.test.js +74 -0
- package/dist/lifecycle.test.js.map +1 -0
- package/dist/llm/embedding/index.d.ts +42 -0
- package/dist/llm/embedding/index.d.ts.map +1 -0
- package/dist/llm/embedding/index.js +78 -0
- package/dist/llm/embedding/index.js.map +1 -0
- package/dist/llm/embedding/index.test.d.ts +8 -0
- package/dist/llm/embedding/index.test.d.ts.map +1 -0
- package/dist/llm/embedding/index.test.js +100 -0
- package/dist/llm/embedding/index.test.js.map +1 -0
- package/dist/llm/embedding/providers/bedrock.d.ts +16 -0
- package/dist/llm/embedding/providers/bedrock.d.ts.map +1 -0
- package/dist/llm/embedding/providers/bedrock.js +90 -0
- package/dist/llm/embedding/providers/bedrock.js.map +1 -0
- package/dist/llm/embedding/providers/bedrock.test.d.ts +2 -0
- package/dist/llm/embedding/providers/bedrock.test.d.ts.map +1 -0
- package/dist/llm/embedding/providers/bedrock.test.js +24 -0
- package/dist/llm/embedding/providers/bedrock.test.js.map +1 -0
- package/dist/llm/embedding/providers/index.d.ts +9 -0
- package/dist/llm/embedding/providers/index.d.ts.map +1 -0
- package/dist/llm/embedding/providers/index.js +9 -0
- package/dist/llm/embedding/providers/index.js.map +1 -0
- package/dist/llm/embedding/providers/ollama.d.ts +6 -0
- package/dist/llm/embedding/providers/ollama.d.ts.map +1 -0
- package/dist/llm/embedding/providers/ollama.js +39 -0
- package/dist/llm/embedding/providers/ollama.js.map +1 -0
- package/dist/llm/embedding/providers/ollama.test.d.ts +2 -0
- package/dist/llm/embedding/providers/ollama.test.d.ts.map +1 -0
- package/dist/llm/embedding/providers/ollama.test.js +54 -0
- package/dist/llm/embedding/providers/ollama.test.js.map +1 -0
- package/dist/llm/embedding/providers/openai.d.ts +6 -0
- package/dist/llm/embedding/providers/openai.d.ts.map +1 -0
- package/dist/llm/embedding/providers/openai.js +44 -0
- package/dist/llm/embedding/providers/openai.js.map +1 -0
- package/dist/llm/embedding/providers/openai.test.d.ts +2 -0
- package/dist/llm/embedding/providers/openai.test.d.ts.map +1 -0
- package/dist/llm/embedding/providers/openai.test.js +48 -0
- package/dist/llm/embedding/providers/openai.test.js.map +1 -0
- package/dist/llm/embedding/providers/portkey.d.ts +15 -0
- package/dist/llm/embedding/providers/portkey.d.ts.map +1 -0
- package/dist/llm/embedding/providers/portkey.js +58 -0
- package/dist/llm/embedding/providers/portkey.js.map +1 -0
- package/dist/llm/embedding/providers/portkey.test.d.ts +2 -0
- package/dist/llm/embedding/providers/portkey.test.d.ts.map +1 -0
- package/dist/llm/embedding/providers/portkey.test.js +56 -0
- package/dist/llm/embedding/providers/portkey.test.js.map +1 -0
- package/dist/llm/embedding/registry.d.ts +14 -0
- package/dist/llm/embedding/registry.d.ts.map +1 -0
- package/dist/llm/embedding/registry.js +27 -0
- package/dist/llm/embedding/registry.js.map +1 -0
- package/dist/llm/embedding/registry.test.d.ts +7 -0
- package/dist/llm/embedding/registry.test.d.ts.map +1 -0
- package/dist/llm/embedding/registry.test.js +68 -0
- package/dist/llm/embedding/registry.test.js.map +1 -0
- package/dist/llm/embedding/types.d.ts +55 -0
- package/dist/llm/embedding/types.d.ts.map +1 -0
- package/dist/llm/embedding/types.js +12 -0
- package/dist/llm/embedding/types.js.map +1 -0
- package/dist/llm/errors.d.ts +95 -0
- package/dist/llm/errors.d.ts.map +1 -0
- package/dist/llm/errors.js +164 -0
- package/dist/llm/errors.js.map +1 -0
- package/dist/llm/errors.test.d.ts +2 -0
- package/dist/llm/errors.test.d.ts.map +1 -0
- package/dist/llm/errors.test.js +103 -0
- package/dist/llm/errors.test.js.map +1 -0
- package/dist/llm/fetch.d.ts +39 -0
- package/dist/llm/fetch.d.ts.map +1 -0
- package/dist/llm/fetch.js +52 -0
- package/dist/llm/fetch.js.map +1 -0
- package/dist/llm/index.d.ts +6 -3
- package/dist/llm/index.d.ts.map +1 -1
- package/dist/llm/index.js +2 -2
- package/dist/llm/index.js.map +1 -1
- package/dist/llm/inference/index.d.ts +39 -0
- package/dist/llm/inference/index.d.ts.map +1 -0
- package/dist/llm/inference/index.js +118 -0
- package/dist/llm/inference/index.js.map +1 -0
- package/dist/llm/inference/index.test.d.ts +6 -0
- package/dist/llm/inference/index.test.d.ts.map +1 -0
- package/dist/llm/inference/index.test.js +109 -0
- package/dist/llm/inference/index.test.js.map +1 -0
- package/dist/llm/inference/providers/bedrock.d.ts +18 -0
- package/dist/llm/inference/providers/bedrock.d.ts.map +1 -0
- package/dist/llm/inference/providers/bedrock.js +105 -0
- package/dist/llm/inference/providers/bedrock.js.map +1 -0
- package/dist/llm/inference/providers/bedrock.test.d.ts +2 -0
- package/dist/llm/inference/providers/bedrock.test.d.ts.map +1 -0
- package/dist/llm/inference/providers/bedrock.test.js +21 -0
- package/dist/llm/inference/providers/bedrock.test.js.map +1 -0
- package/dist/llm/inference/providers/index.d.ts +10 -0
- package/dist/llm/inference/providers/index.d.ts.map +1 -0
- package/dist/llm/inference/providers/index.js +10 -0
- package/dist/llm/inference/providers/index.js.map +1 -0
- package/dist/llm/inference/providers/ollama.d.ts +8 -0
- package/dist/llm/inference/providers/ollama.d.ts.map +1 -0
- package/dist/llm/inference/providers/ollama.js +63 -0
- package/dist/llm/inference/providers/ollama.js.map +1 -0
- package/dist/llm/inference/providers/ollama.test.d.ts +2 -0
- package/dist/llm/inference/providers/ollama.test.d.ts.map +1 -0
- package/dist/llm/inference/providers/ollama.test.js +57 -0
- package/dist/llm/inference/providers/ollama.test.js.map +1 -0
- package/dist/llm/inference/providers/openai.d.ts +11 -0
- package/dist/llm/inference/providers/openai.d.ts.map +1 -0
- package/dist/llm/inference/providers/openai.js +73 -0
- package/dist/llm/inference/providers/openai.js.map +1 -0
- package/dist/llm/inference/providers/openai.test.d.ts +2 -0
- package/dist/llm/inference/providers/openai.test.d.ts.map +1 -0
- package/dist/llm/inference/providers/openai.test.js +46 -0
- package/dist/llm/inference/providers/openai.test.js.map +1 -0
- package/dist/llm/inference/providers/portkey.d.ts +13 -0
- package/dist/llm/inference/providers/portkey.d.ts.map +1 -0
- package/dist/llm/inference/providers/portkey.js +80 -0
- package/dist/llm/inference/providers/portkey.js.map +1 -0
- package/dist/llm/inference/providers/portkey.test.d.ts +2 -0
- package/dist/llm/inference/providers/portkey.test.d.ts.map +1 -0
- package/dist/llm/inference/providers/portkey.test.js +48 -0
- package/dist/llm/inference/providers/portkey.test.js.map +1 -0
- package/dist/llm/inference/registry.d.ts +15 -0
- package/dist/llm/inference/registry.d.ts.map +1 -0
- package/dist/llm/inference/registry.js +28 -0
- package/dist/llm/inference/registry.js.map +1 -0
- package/dist/llm/inference/registry.test.d.ts +6 -0
- package/dist/llm/inference/registry.test.d.ts.map +1 -0
- package/dist/llm/inference/registry.test.js +63 -0
- package/dist/llm/inference/registry.test.js.map +1 -0
- package/dist/llm/inference/types.d.ts +84 -0
- package/dist/llm/inference/types.d.ts.map +1 -0
- package/dist/llm/inference/types.js +9 -0
- package/dist/llm/inference/types.js.map +1 -0
- package/dist/llm/telemetry.d.ts +25 -0
- package/dist/llm/telemetry.d.ts.map +1 -0
- package/dist/llm/telemetry.js +43 -0
- package/dist/llm/telemetry.js.map +1 -0
- package/dist/llm/telemetry.test.d.ts +5 -0
- package/dist/llm/telemetry.test.d.ts.map +1 -0
- package/dist/llm/telemetry.test.js +89 -0
- package/dist/llm/telemetry.test.js.map +1 -0
- package/dist/llm/types.d.ts +4 -37
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/llm/types.js +4 -1
- package/dist/llm/types.js.map +1 -1
- package/dist/logger.d.ts +18 -3
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +102 -20
- package/dist/logger.js.map +1 -1
- package/dist/logger.test.d.ts +5 -0
- package/dist/logger.test.d.ts.map +1 -0
- package/dist/logger.test.js +103 -0
- package/dist/logger.test.js.map +1 -0
- package/dist/mcp/api.d.ts +15 -1
- package/dist/mcp/api.d.ts.map +1 -1
- package/dist/mcp/api.js +44 -19
- package/dist/mcp/api.js.map +1 -1
- package/dist/mcp/api.test.d.ts +6 -0
- package/dist/mcp/api.test.d.ts.map +1 -0
- package/dist/mcp/api.test.js +130 -0
- package/dist/mcp/api.test.js.map +1 -0
- package/dist/mcp/helpers.d.ts +1 -0
- package/dist/mcp/helpers.d.ts.map +1 -1
- package/dist/mcp/helpers.js +62 -6
- package/dist/mcp/helpers.js.map +1 -1
- package/dist/mcp/helpers.test.js +71 -10
- package/dist/mcp/helpers.test.js.map +1 -1
- package/dist/mcp/index.d.ts +7 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +46 -21
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/taxonomy.d.ts +251 -31
- package/dist/mcp/taxonomy.d.ts.map +1 -1
- package/dist/mcp/taxonomy.js +603 -171
- package/dist/mcp/taxonomy.js.map +1 -1
- package/dist/mcp/taxonomy.test.d.ts +1 -1
- package/dist/mcp/taxonomy.test.js +141 -302
- package/dist/mcp/taxonomy.test.js.map +1 -1
- package/dist/mcp/tools.d.ts +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.integration.itest.d.ts +23 -0
- package/dist/mcp/tools.integration.itest.d.ts.map +1 -0
- package/dist/mcp/tools.integration.itest.js +172 -0
- package/dist/mcp/tools.integration.itest.js.map +1 -0
- package/dist/mcp/tools.js +422 -357
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/tools.test.d.ts +16 -0
- package/dist/mcp/tools.test.d.ts.map +1 -0
- package/dist/mcp/tools.test.js +472 -0
- package/dist/mcp/tools.test.js.map +1 -0
- package/dist/mcp/types.d.ts +63 -8
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/prompts/brief.d.ts +19 -0
- package/dist/prompts/brief.d.ts.map +1 -0
- package/dist/prompts/brief.js +67 -0
- package/dist/prompts/brief.js.map +1 -0
- package/dist/prompts/contradiction.d.ts +24 -0
- package/dist/prompts/contradiction.d.ts.map +1 -0
- package/dist/prompts/contradiction.js +73 -0
- package/dist/prompts/contradiction.js.map +1 -0
- package/dist/prompts/distill.d.ts +21 -0
- package/dist/prompts/distill.d.ts.map +1 -0
- package/dist/prompts/distill.js +92 -0
- package/dist/prompts/distill.js.map +1 -0
- package/dist/prompts/episode-summary.d.ts +15 -0
- package/dist/prompts/episode-summary.d.ts.map +1 -0
- package/dist/prompts/episode-summary.js +60 -0
- package/dist/prompts/episode-summary.js.map +1 -0
- package/dist/prompts/extraction.d.ts +14 -0
- package/dist/prompts/extraction.d.ts.map +1 -0
- package/dist/prompts/extraction.js +110 -0
- package/dist/prompts/extraction.js.map +1 -0
- package/dist/prompts/index.d.ts +52 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +104 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/prompts.test.d.ts +8 -0
- package/dist/prompts/prompts.test.d.ts.map +1 -0
- package/dist/prompts/prompts.test.js +140 -0
- package/dist/prompts/prompts.test.js.map +1 -0
- package/dist/prompts/relations.d.ts +17 -0
- package/dist/prompts/relations.d.ts.map +1 -0
- package/dist/prompts/relations.js +72 -0
- package/dist/prompts/relations.js.map +1 -0
- package/dist/prompts/workstream-summary.d.ts +17 -0
- package/dist/prompts/workstream-summary.d.ts.map +1 -0
- package/dist/prompts/workstream-summary.js +72 -0
- package/dist/prompts/workstream-summary.js.map +1 -0
- package/dist/render.d.ts +41 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +185 -0
- package/dist/render.js.map +1 -0
- package/dist/render.test.d.ts +8 -0
- package/dist/render.test.d.ts.map +1 -0
- package/dist/render.test.js +243 -0
- package/dist/render.test.js.map +1 -0
- package/docs/diagrams/architecture.svg +87 -0
- package/docs/diagrams/team-memory.svg +250 -0
- package/docs/screenshots/dashboard.png +0 -0
- package/docs/screenshots/graph.png +0 -0
- package/docs/screenshots/memory.png +0 -0
- package/package.json +12 -3
- package/dist/llm/embedding.d.ts +0 -13
- package/dist/llm/embedding.d.ts.map +0 -1
- package/dist/llm/embedding.js +0 -127
- package/dist/llm/embedding.js.map +0 -1
- package/dist/llm/embedding.test.d.ts +0 -8
- package/dist/llm/embedding.test.d.ts.map +0 -1
- package/dist/llm/embedding.test.js +0 -117
- package/dist/llm/embedding.test.js.map +0 -1
- package/dist/llm/inference.d.ts +0 -12
- package/dist/llm/inference.d.ts.map +0 -1
- package/dist/llm/inference.js +0 -146
- package/dist/llm/inference.js.map +0 -1
- package/dist/llm/inference.test.d.ts +0 -8
- package/dist/llm/inference.test.d.ts.map +0 -1
- package/dist/llm/inference.test.js +0 -117
- package/dist/llm/inference.test.js.map +0 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP tool definitions
|
|
2
|
+
* MCP tool definitions for memory.
|
|
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, domainValues, kindValues, sourceValues, DEFAULT_DOMAIN, DEFAULT_KIND, DEFAULT_SOURCE, } from "./taxonomy.js";
|
|
7
|
-
import { contentHash, daysSince, lastActivityDate, computeCombinedScore, buildFilter, formatFact, } from "./helpers.js";
|
|
8
|
-
import { ready, qdrantUrl, qdrantApiKey, setQdrantUrl, setQdrantApiKey, setReady, getCollection, log, embed, getEmbeddingConfig,
|
|
6
|
+
import { STALENESS_DAYS, THRESHOLD_DUPLICATE, THRESHOLD_RELATED, QDRANT_INDEXES, categoryValues, categoryEnumDescription, domainValues, domainEnumDescription, kindValues, kindEnumDescription, memorySubtypeValues, memorySubtypeEnumDescription, sourceValues, sourceEnumDescription, DEFAULT_CATEGORY, DEFAULT_DOMAIN, DEFAULT_KIND, DEFAULT_SOURCE, categoryForMemorySubtype, layerForMemorySubtype, normalizeCategory, normalizeDomain, normalizeKind, validateMemorySubtype, } from "./taxonomy.js";
|
|
7
|
+
import { contentHash, daysSince, lastActivityDate, computeCombinedScore, buildFilter, formatFact, MEMORY_RECALL_EXCLUDED_KINDS, } from "./helpers.js";
|
|
8
|
+
import { ready, qdrantUrl, qdrantApiKey, setupError, setQdrantUrl, setQdrantApiKey, setReady, getCollection, log, embed, getEmbeddingConfig, qdrantReq, ensureCollection, qdrantUpsert, qdrantSearch, qdrantScroll, qdrantSetPayload, qdrantGetPoints, } from "./api.js";
|
|
9
9
|
import { saveConfig, loadConfig } from "../config.js";
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Runtime state
|
|
@@ -22,13 +22,50 @@ function nowISO() {
|
|
|
22
22
|
function newId() {
|
|
23
23
|
return crypto.randomUUID();
|
|
24
24
|
}
|
|
25
|
+
function redactionOptions() {
|
|
26
|
+
return { enabled: false, redactPii: false };
|
|
27
|
+
}
|
|
28
|
+
function redactStorageText(text) {
|
|
29
|
+
return { text, redacted: false, summary: "none", matches: [] };
|
|
30
|
+
}
|
|
31
|
+
function combineRedactions(_items) {
|
|
32
|
+
return { redacted: false, summary: "none", matches: [] };
|
|
33
|
+
}
|
|
34
|
+
function resolveScope(workspaceId, includeLegacyWorkspace = false) {
|
|
35
|
+
return {
|
|
36
|
+
workspaceId: workspaceId?.trim() || undefined,
|
|
37
|
+
includeLegacy: includeLegacyWorkspace,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function scopedFilter(scope, extra = {}) {
|
|
41
|
+
return buildFilter({
|
|
42
|
+
...extra,
|
|
43
|
+
workspace_id: scope.workspaceId,
|
|
44
|
+
includeLegacyWorkspace: scope.includeLegacy,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function addWorkspacePayload(payload, scope) {
|
|
48
|
+
if (scope.workspaceId)
|
|
49
|
+
payload["workspace_id"] = scope.workspaceId;
|
|
50
|
+
if (scope.actorId)
|
|
51
|
+
payload["actor_id"] = scope.actorId;
|
|
52
|
+
}
|
|
53
|
+
function addRedactionPayload(_payload, _summary) {
|
|
54
|
+
// Task 243 keeps storage pass-through; redaction policy is out of scope for this branch.
|
|
55
|
+
}
|
|
56
|
+
async function getPointForWorkspaceWrite(factId, _scope) {
|
|
57
|
+
const existing = await qdrantGetPoints([factId]);
|
|
58
|
+
const point = existing.result?.[0];
|
|
59
|
+
if (!point) {
|
|
60
|
+
return { error: { status: "not_found", fact_id: factId } };
|
|
61
|
+
}
|
|
62
|
+
return { point };
|
|
63
|
+
}
|
|
25
64
|
function requireReady() {
|
|
26
65
|
if (!ready) {
|
|
27
66
|
const missing = [];
|
|
28
67
|
if (!qdrantUrl)
|
|
29
68
|
missing.push("qdrant-url");
|
|
30
|
-
if (!qdrantApiKey)
|
|
31
|
-
missing.push("qdrant-api-key");
|
|
32
69
|
return {
|
|
33
70
|
content: [{
|
|
34
71
|
type: "text",
|
|
@@ -36,6 +73,10 @@ function requireReady() {
|
|
|
36
73
|
status: "setup_required",
|
|
37
74
|
ready: false,
|
|
38
75
|
missing,
|
|
76
|
+
// Surface the underlying init failure (embedding / Qdrant) when
|
|
77
|
+
// present so users see an actionable reason instead of a generic
|
|
78
|
+
// "setup required" message.
|
|
79
|
+
...(setupError ? { setup_error: setupError } : {}),
|
|
39
80
|
setup_instructions: "Memory is not configured. Run `bikky setup` or call configure_credentials:\n" +
|
|
40
81
|
"1. Go to cloud.qdrant.io → sign up (free tier: 1GB, no credit card)\n" +
|
|
41
82
|
"2. Create a cluster → copy the REST URL and API key\n" +
|
|
@@ -52,14 +93,20 @@ function buildMemoryNudge() {
|
|
|
52
93
|
if (elapsed < NUDGE_INTERVAL_MS)
|
|
53
94
|
return null;
|
|
54
95
|
const mins = Math.round(elapsed / 60000);
|
|
96
|
+
// Suggest the most likely category to record based on what an engineering
|
|
97
|
+
// session typically produces. The agent picks the best fit.
|
|
55
98
|
return `🧠 Memory nudge: No memory_store calls in ${mins} minutes. ` +
|
|
56
|
-
"
|
|
57
|
-
"
|
|
99
|
+
"Reflect on what's worth persisting:\n" +
|
|
100
|
+
" • infrastructure — new services, ports, configs touched?\n" +
|
|
101
|
+
" • decisions — architectural choices made (with rationale)?\n" +
|
|
102
|
+
" • observation — debugging findings, gotchas, workarounds?\n" +
|
|
103
|
+
" • projects — work-in-progress, blockers, completions?\n" +
|
|
104
|
+
"If yes, call memory_store now so future sessions inherit the knowledge.";
|
|
58
105
|
}
|
|
59
106
|
/**
|
|
60
107
|
* Entity-graph traversal for memory_recall.
|
|
61
108
|
*/
|
|
62
|
-
async function graphTraversal(primaryResults, limit) {
|
|
109
|
+
async function graphTraversal(primaryResults, limit, scope) {
|
|
63
110
|
try {
|
|
64
111
|
const primaryEntities = new Set();
|
|
65
112
|
const primaryIds = new Set();
|
|
@@ -73,22 +120,16 @@ async function graphTraversal(primaryResults, limit) {
|
|
|
73
120
|
return [];
|
|
74
121
|
const relatedEntities = new Set();
|
|
75
122
|
for (const entity of primaryEntities) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{ is_null: { key: "superseded_by" } },
|
|
80
|
-
],
|
|
81
|
-
}, 10).catch(() => ({ result: { points: [] } }));
|
|
123
|
+
const outgoingFilter = scopedFilter(scope, { excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS }) ?? { must: [] };
|
|
124
|
+
outgoingFilter.must.push({ key: "from_entity", match: { value: entity } });
|
|
125
|
+
const outgoing = await qdrantScroll(outgoingFilter, 10).catch(() => ({ result: { points: [] } }));
|
|
82
126
|
for (const pt of (outgoing.result?.points ?? [])) {
|
|
83
127
|
if (pt.payload.to_entity)
|
|
84
128
|
relatedEntities.add(pt.payload.to_entity);
|
|
85
129
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{ is_null: { key: "superseded_by" } },
|
|
90
|
-
],
|
|
91
|
-
}, 10).catch(() => ({ result: { points: [] } }));
|
|
130
|
+
const incomingFilter = scopedFilter(scope, { excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS }) ?? { must: [] };
|
|
131
|
+
incomingFilter.must.push({ key: "to_entity", match: { value: entity } });
|
|
132
|
+
const incoming = await qdrantScroll(incomingFilter, 10).catch(() => ({ result: { points: [] } }));
|
|
92
133
|
for (const pt of (incoming.result?.points ?? [])) {
|
|
93
134
|
if (pt.payload.from_entity)
|
|
94
135
|
relatedEntities.add(pt.payload.from_entity);
|
|
@@ -101,12 +142,9 @@ async function graphTraversal(primaryResults, limit) {
|
|
|
101
142
|
const relatedFacts = [];
|
|
102
143
|
const maxPerEntity = Math.max(2, Math.floor(limit / relatedEntities.size));
|
|
103
144
|
for (const entity of relatedEntities) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{ is_null: { key: "superseded_by" } },
|
|
108
|
-
],
|
|
109
|
-
}, maxPerEntity).catch(() => ({ result: { points: [] } }));
|
|
145
|
+
const filter = scopedFilter(scope, { excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS }) ?? { must: [] };
|
|
146
|
+
filter.must.push({ key: "entities", match: { value: entity } });
|
|
147
|
+
const result = await qdrantScroll(filter, maxPerEntity).catch(() => ({ result: { points: [] } }));
|
|
110
148
|
for (const pt of (result.result?.points ?? [])) {
|
|
111
149
|
if (!primaryIds.has(pt.id)) {
|
|
112
150
|
relatedFacts.push(pt);
|
|
@@ -128,7 +166,11 @@ async function graphTraversal(primaryResults, limit) {
|
|
|
128
166
|
// ---------------------------------------------------------------------------
|
|
129
167
|
export function registerTools(mcp) {
|
|
130
168
|
// ── get_setup_status ────────────────────────────────────────────────────
|
|
131
|
-
mcp.tool("get_setup_status",
|
|
169
|
+
mcp.tool("get_setup_status", [
|
|
170
|
+
"Check whether the memory system is configured and reachable.",
|
|
171
|
+
"Use this when memory tools return a 'setup_required' error, or once at session start if you're not sure bikky is wired up. Reports which credentials are missing and includes onboarding instructions if anything is incomplete.",
|
|
172
|
+
"Read-only — safe to call any time.",
|
|
173
|
+
].join(" "), {}, async () => {
|
|
132
174
|
const status = {
|
|
133
175
|
ready,
|
|
134
176
|
qdrant_url: !!qdrantUrl,
|
|
@@ -139,13 +181,13 @@ export function registerTools(mcp) {
|
|
|
139
181
|
embedding_provider: getEmbeddingConfig().provider,
|
|
140
182
|
embedding_model: getEmbeddingConfig().model,
|
|
141
183
|
embedding_dimensions: getEmbeddingConfig().dimensions,
|
|
184
|
+
...(setupError ? { setup_error: setupError } : {}),
|
|
142
185
|
};
|
|
143
186
|
const missing = status["missing"];
|
|
144
187
|
if (!qdrantUrl)
|
|
145
188
|
missing.push("qdrant-url");
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (qdrantUrl && qdrantApiKey) {
|
|
189
|
+
// qdrant-api-key is optional (local / self-hosted Qdrant doesn't need it).
|
|
190
|
+
if (qdrantUrl) {
|
|
149
191
|
try {
|
|
150
192
|
await qdrantReq("GET", "/collections");
|
|
151
193
|
status["qdrant_connected"] = true;
|
|
@@ -159,17 +201,21 @@ export function registerTools(mcp) {
|
|
|
159
201
|
catch { /* ignore */ }
|
|
160
202
|
if (!status["ready"] && missing.length > 0) {
|
|
161
203
|
status["setup_instructions"] =
|
|
162
|
-
"Run `bikky setup` or guide the user:\n" +
|
|
163
|
-
"
|
|
164
|
-
"
|
|
165
|
-
"
|
|
204
|
+
"Run `bikky setup` or guide the user. Pick one Qdrant option:\n" +
|
|
205
|
+
" • Qdrant Cloud (managed, free tier, 1GB): https://cloud.qdrant.io — copy the REST URL + API key\n" +
|
|
206
|
+
" • Local Docker: `docker run -p 6333:6333 qdrant/qdrant` → URL `http://localhost:6333` (no API key needed)\n" +
|
|
207
|
+
" • Self-hosted: any reachable Qdrant; API key only required if QDRANT__SERVICE__API_KEY is set on the server\n" +
|
|
208
|
+
"Then call configure_credentials with the URL (and API key if applicable).";
|
|
166
209
|
}
|
|
167
210
|
return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
|
|
168
211
|
});
|
|
169
212
|
// ── configure_credentials ───────────────────────────────────────────────
|
|
170
|
-
mcp.tool("configure_credentials",
|
|
171
|
-
|
|
172
|
-
|
|
213
|
+
mcp.tool("configure_credentials", [
|
|
214
|
+
"Persist Qdrant and embedding credentials to ~/.bikky/config.json and bring the memory system online.",
|
|
215
|
+
"Call this only during onboarding (or when rotating credentials). After it succeeds, the collection is created if missing and embeddings are tested. For day-to-day use, prefer get_setup_status.",
|
|
216
|
+
].join(" "), {
|
|
217
|
+
qdrant_url: z.string().optional().describe("Qdrant REST URL — Qdrant Cloud (https://xxx.cloud.qdrant.io:6333), local Docker (http://localhost:6333), or self-hosted"),
|
|
218
|
+
qdrant_api_key: z.string().optional().describe("Qdrant API key — required for Qdrant Cloud; optional / leave blank for unauthenticated local or self-hosted instances"),
|
|
173
219
|
openai_api_key: z.string().optional().describe("OpenAI API key (for OpenAI embedding/LLM provider)"),
|
|
174
220
|
}, async ({ qdrant_url, qdrant_api_key, openai_api_key }) => {
|
|
175
221
|
const results = {};
|
|
@@ -191,7 +237,7 @@ export function registerTools(mcp) {
|
|
|
191
237
|
results["openai_api_key"] = "stored ✓";
|
|
192
238
|
}
|
|
193
239
|
saveConfig(cfg);
|
|
194
|
-
if (qdrantUrl
|
|
240
|
+
if (qdrantUrl) {
|
|
195
241
|
try {
|
|
196
242
|
await ensureCollection(QDRANT_INDEXES);
|
|
197
243
|
results["qdrant_collection"] = `'${getCollection()}' ready ✓`;
|
|
@@ -208,14 +254,18 @@ export function registerTools(mcp) {
|
|
|
208
254
|
catch (e) {
|
|
209
255
|
results["embedding"] = `error: ${e instanceof Error ? e.message : String(e)}`;
|
|
210
256
|
}
|
|
211
|
-
setReady(!!
|
|
257
|
+
setReady(!!qdrantUrl);
|
|
212
258
|
results["ready"] = ready;
|
|
213
259
|
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
214
260
|
});
|
|
215
261
|
// ── verify_connection ───────────────────────────────────────────────────
|
|
216
|
-
mcp.tool("verify_connection",
|
|
262
|
+
mcp.tool("verify_connection", [
|
|
263
|
+
"Confirm Qdrant is reachable, embeddings work, and the collection exists.",
|
|
264
|
+
"Use this to debug a sudden 'setup_required' or empty-recall after a network blip or credential change. Lighter than configure_credentials — does not write to disk.",
|
|
265
|
+
"Read-only.",
|
|
266
|
+
].join(" "), {}, async () => {
|
|
217
267
|
const results = { qdrant: false, embedding: false, collection: false };
|
|
218
|
-
if (qdrantUrl
|
|
268
|
+
if (qdrantUrl) {
|
|
219
269
|
try {
|
|
220
270
|
await qdrantReq("GET", "/collections");
|
|
221
271
|
results["qdrant"] = true;
|
|
@@ -242,42 +292,84 @@ export function registerTools(mcp) {
|
|
|
242
292
|
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
243
293
|
});
|
|
244
294
|
// ── memory_store ────────────────────────────────────────────────────────
|
|
245
|
-
mcp.tool("memory_store",
|
|
246
|
-
"
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
295
|
+
mcp.tool("memory_store", [
|
|
296
|
+
"Persist one atomic fact to long-term memory.",
|
|
297
|
+
"Call this whenever you learn something a future session would need: a service detail, a decision rationale, a workaround, a user preference, an ownership fact, a task-resume pointer. One fact per call — split compound observations into separate calls.",
|
|
298
|
+
"Dedup is automatic (content hash + vector similarity), so you do NOT need to recall first. The tool returns one of: inserted (new fact), reinforced (exact or near-duplicate found — counters bumped), or — if there are similar-but-different facts — a list of potential conflicts so you can decide whether to use 'supersedes'.",
|
|
299
|
+
"To create a typed edge between two entities at the same time, set the optional 'relation' field — no separate tool call needed.",
|
|
300
|
+
"Do NOT use for ephemeral state (current cursor, in-flight todo). Use the harness task folder instead.",
|
|
301
|
+
].join(" "), {
|
|
302
|
+
content: z.string().describe("The fact to store. Should be one atomic, self-contained statement (no compound 'A and B') that makes sense out of context."),
|
|
303
|
+
category: z.enum(categoryValues()).describe(categoryEnumDescription()),
|
|
304
|
+
entities: z.array(z.string()).describe("Lowercase entity names mentioned by this fact (e.g. ['qdrant', 'workspace_id']). Used for entity-scoped recall and graph traversal — keep them short and canonical."),
|
|
305
|
+
domain: z.enum(domainValues()).default(DEFAULT_DOMAIN).describe(domainEnumDescription()),
|
|
306
|
+
kind: z.enum(kindValues()).default(DEFAULT_KIND).describe(kindEnumDescription()),
|
|
307
|
+
memory_subtype: z.enum(memorySubtypeValues()).optional().describe(memorySubtypeEnumDescription()),
|
|
308
|
+
workspace_id: z.string().optional().describe("Workspace namespace for team-shared memory. Omit to use the default workspace from config."),
|
|
309
|
+
episode_id: z.string().optional().describe("Coherent activity-segment ID. Group facts captured during the same coherent task or transcript."),
|
|
310
|
+
workstream_key: z.string().optional().describe("Durable continuity key for a long-running objective (survives across sessions)."),
|
|
311
|
+
task_key: z.string().optional().describe("Task or issue key (e.g. GitHub issue number, JIRA key)."),
|
|
312
|
+
repo: z.string().optional().describe("Repository or project surface this fact relates to (e.g. 'bikky-dev/bikky')."),
|
|
313
|
+
branch: z.string().optional().describe("Branch or working surface (e.g. 'main', 'feat/x')."),
|
|
314
|
+
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."),
|
|
315
|
+
source: z.enum(sourceValues()).default(DEFAULT_SOURCE).describe(sourceEnumDescription()),
|
|
316
|
+
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."),
|
|
317
|
+
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."),
|
|
318
|
+
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."),
|
|
260
319
|
relation: z.object({
|
|
261
|
-
from: z.string().describe("Source entity"),
|
|
262
|
-
type: z.string().describe("Relation type (owns, uses, decided, prefers, works-on
|
|
263
|
-
to: z.string().describe("Target entity"),
|
|
264
|
-
}).optional().describe("Optional typed
|
|
265
|
-
metadata: z.record(z.string(), z.string()).optional()
|
|
266
|
-
|
|
267
|
-
}, async ({ content, category, entities, domain, kind, source, confidence, importance, supersedes, relation, metadata }) => {
|
|
320
|
+
from: z.string().describe("Source entity (lowercase)."),
|
|
321
|
+
type: z.string().describe("Relation type (e.g. 'owns', 'uses', 'decided', 'prefers', 'works-on')."),
|
|
322
|
+
to: z.string().describe("Target entity (lowercase)."),
|
|
323
|
+
}).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."),
|
|
324
|
+
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)."),
|
|
325
|
+
}, async ({ content, category, entities, domain, kind, memory_subtype, workspace_id, episode_id, workstream_key, task_key, repo, branch, review_status, source, confidence, importance, supersedes, relation, metadata, }) => {
|
|
268
326
|
const guard = requireReady();
|
|
269
327
|
if (guard)
|
|
270
328
|
return guard;
|
|
271
329
|
lastStoreTime = Date.now();
|
|
272
330
|
const now = nowISO();
|
|
273
|
-
const
|
|
274
|
-
const
|
|
331
|
+
const scope = resolveScope(workspace_id);
|
|
332
|
+
const normalizedKind = normalizeKind(kind);
|
|
333
|
+
let normalizedSubtype = null;
|
|
334
|
+
try {
|
|
335
|
+
normalizedSubtype = validateMemorySubtype(normalizedKind, memory_subtype);
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
return {
|
|
339
|
+
content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
|
|
340
|
+
isError: true,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const normalizedCategory = normalizedSubtype
|
|
344
|
+
? categoryForMemorySubtype(normalizedSubtype) ?? normalizeCategory(category)
|
|
345
|
+
: normalizeCategory(category);
|
|
346
|
+
const normalizedDomain = normalizeDomain(domain);
|
|
347
|
+
const normalizedLayer = normalizedSubtype ? layerForMemorySubtype(normalizedSubtype) : null;
|
|
348
|
+
const redactedContent = redactStorageText(content);
|
|
349
|
+
const redactedEntities = entities.map((entity) => redactStorageText(entity));
|
|
350
|
+
const sanitizedEntities = redactedEntities.map((entity) => entity.text);
|
|
351
|
+
const redactedRelation = relation ? {
|
|
352
|
+
from: redactStorageText(relation.from),
|
|
353
|
+
type: redactStorageText(relation.type),
|
|
354
|
+
to: redactStorageText(relation.to),
|
|
355
|
+
} : null;
|
|
356
|
+
const redactionSummary = combineRedactions([
|
|
357
|
+
redactedContent,
|
|
358
|
+
...redactedEntities,
|
|
359
|
+
...(redactedRelation ? [redactedRelation.from, redactedRelation.type, redactedRelation.to] : []),
|
|
360
|
+
]);
|
|
361
|
+
const hash = contentHash(normalizedCategory, redactedContent.text);
|
|
362
|
+
const normalizedEntities = sanitizedEntities.map((e) => e.toLowerCase());
|
|
363
|
+
const sanitizedRelation = redactedRelation ? {
|
|
364
|
+
from: redactedRelation.from.text,
|
|
365
|
+
type: redactedRelation.type.text,
|
|
366
|
+
to: redactedRelation.to.text,
|
|
367
|
+
} : null;
|
|
275
368
|
// 1. Exact dedup via content hash
|
|
276
369
|
try {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
] }, 1);
|
|
370
|
+
const hashFilter = scopedFilter(scope) ?? { must: [] };
|
|
371
|
+
hashFilter.must.push({ key: "content_hash", match: { value: hash } });
|
|
372
|
+
const existing = await qdrantScroll(hashFilter, 1);
|
|
281
373
|
const existingPoint = existing.result?.points?.[0];
|
|
282
374
|
if (existingPoint) {
|
|
283
375
|
const point = existingPoint;
|
|
@@ -301,17 +393,16 @@ export function registerTools(mcp) {
|
|
|
301
393
|
log("WARN", `Hash dedup check failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
302
394
|
}
|
|
303
395
|
// 2. Generate embedding
|
|
304
|
-
const vector = await embed(
|
|
396
|
+
const vector = await embed(redactedContent.text);
|
|
305
397
|
// 3. Semantic dedup
|
|
306
398
|
let similarFacts = [];
|
|
307
399
|
let potentialConflicts = [];
|
|
308
400
|
try {
|
|
309
|
-
const filter = { must: [] };
|
|
401
|
+
const filter = scopedFilter(scope) ?? { must: [] };
|
|
310
402
|
if (normalizedEntities.length > 0) {
|
|
311
403
|
filter.must.push({ key: "entities", match: { any: normalizedEntities } });
|
|
312
404
|
}
|
|
313
|
-
|
|
314
|
-
const results = await qdrantSearch(vector, filter.must.length > 0 ? filter : undefined, 3);
|
|
405
|
+
const results = await qdrantSearch(vector, filter, 3);
|
|
315
406
|
const firstResult = results.result?.[0];
|
|
316
407
|
if (results.result?.length > 0 && firstResult) {
|
|
317
408
|
const topScore = firstResult.score ?? 0;
|
|
@@ -368,6 +459,10 @@ export function registerTools(mcp) {
|
|
|
368
459
|
// 5. Supersede old fact if requested
|
|
369
460
|
if (supersedes) {
|
|
370
461
|
try {
|
|
462
|
+
const existing = await getPointForWorkspaceWrite(supersedes, scope);
|
|
463
|
+
if (existing.error) {
|
|
464
|
+
return { content: [{ type: "text", text: JSON.stringify(existing.error, null, 2) }], isError: true };
|
|
465
|
+
}
|
|
371
466
|
await qdrantSetPayload([supersedes], {
|
|
372
467
|
superseded_by: factId,
|
|
373
468
|
superseded_at: now,
|
|
@@ -379,10 +474,10 @@ export function registerTools(mcp) {
|
|
|
379
474
|
}
|
|
380
475
|
// 6. Insert new fact
|
|
381
476
|
const payload = {
|
|
382
|
-
content,
|
|
383
|
-
category,
|
|
384
|
-
domain,
|
|
385
|
-
kind,
|
|
477
|
+
content: redactedContent.text,
|
|
478
|
+
category: normalizedCategory,
|
|
479
|
+
domain: normalizedDomain,
|
|
480
|
+
kind: normalizedKind,
|
|
386
481
|
entities: normalizedEntities,
|
|
387
482
|
source,
|
|
388
483
|
confidence,
|
|
@@ -395,22 +490,43 @@ export function registerTools(mcp) {
|
|
|
395
490
|
created_at: now,
|
|
396
491
|
updated_at: now,
|
|
397
492
|
};
|
|
493
|
+
if (normalizedSubtype) {
|
|
494
|
+
payload["memory_subtype"] = normalizedSubtype;
|
|
495
|
+
}
|
|
496
|
+
if (normalizedLayer) {
|
|
497
|
+
payload["layer"] = normalizedLayer;
|
|
498
|
+
}
|
|
499
|
+
if (episode_id)
|
|
500
|
+
payload["episode_id"] = episode_id;
|
|
501
|
+
if (workstream_key)
|
|
502
|
+
payload["workstream_key"] = workstream_key;
|
|
503
|
+
if (task_key)
|
|
504
|
+
payload["task_key"] = task_key;
|
|
505
|
+
if (repo)
|
|
506
|
+
payload["repo"] = repo;
|
|
507
|
+
if (branch)
|
|
508
|
+
payload["branch"] = branch;
|
|
509
|
+
if (review_status)
|
|
510
|
+
payload["review_status"] = review_status;
|
|
511
|
+
addWorkspacePayload(payload, scope);
|
|
512
|
+
addRedactionPayload(payload, redactionSummary);
|
|
398
513
|
if (metadata && Object.keys(metadata).length > 0) {
|
|
399
514
|
payload["metadata"] = metadata;
|
|
400
515
|
}
|
|
401
516
|
await qdrantUpsert(factId, vector, payload);
|
|
402
517
|
// 7. Insert relation point if provided
|
|
403
518
|
let relationId = null;
|
|
404
|
-
if (
|
|
519
|
+
if (sanitizedRelation) {
|
|
405
520
|
relationId = newId();
|
|
406
|
-
const relContent = `${
|
|
521
|
+
const relContent = `${sanitizedRelation.from} ${sanitizedRelation.type} ${sanitizedRelation.to}`;
|
|
407
522
|
const relVector = await embed(relContent);
|
|
408
523
|
const relPayload = {
|
|
409
524
|
content: relContent,
|
|
410
|
-
category,
|
|
411
|
-
domain,
|
|
525
|
+
category: normalizedCategory,
|
|
526
|
+
domain: normalizedDomain,
|
|
412
527
|
kind: "relation",
|
|
413
|
-
|
|
528
|
+
layer: "memory_object",
|
|
529
|
+
entities: [sanitizedRelation.from.toLowerCase(), sanitizedRelation.to.toLowerCase()],
|
|
414
530
|
source,
|
|
415
531
|
confidence,
|
|
416
532
|
content_hash: contentHash("relation", relContent),
|
|
@@ -420,18 +536,23 @@ export function registerTools(mcp) {
|
|
|
420
536
|
superseded_at: null,
|
|
421
537
|
created_at: now,
|
|
422
538
|
updated_at: now,
|
|
423
|
-
from_entity:
|
|
424
|
-
relation_type:
|
|
425
|
-
to_entity:
|
|
539
|
+
from_entity: sanitizedRelation.from.toLowerCase(),
|
|
540
|
+
relation_type: sanitizedRelation.type.toLowerCase(),
|
|
541
|
+
to_entity: sanitizedRelation.to.toLowerCase(),
|
|
426
542
|
};
|
|
543
|
+
addWorkspacePayload(relPayload, scope);
|
|
544
|
+
addRedactionPayload(relPayload, redactionSummary);
|
|
427
545
|
await qdrantUpsert(relationId, relVector, relPayload);
|
|
428
546
|
}
|
|
429
547
|
const result = {
|
|
430
548
|
action: "inserted",
|
|
431
549
|
fact_id: factId,
|
|
550
|
+
workspace_id: scope.workspaceId,
|
|
432
551
|
};
|
|
433
552
|
if (relationId)
|
|
434
553
|
result["relation_id"] = relationId;
|
|
554
|
+
if (redactionSummary.redacted)
|
|
555
|
+
result["redaction"] = redactionSummary;
|
|
435
556
|
if (similarFacts.length > 0)
|
|
436
557
|
result["similar_facts"] = similarFacts;
|
|
437
558
|
if (potentialConflicts.length > 0) {
|
|
@@ -443,26 +564,72 @@ export function registerTools(mcp) {
|
|
|
443
564
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
444
565
|
});
|
|
445
566
|
// ── memory_recall ───────────────────────────────────────────────────────
|
|
446
|
-
mcp.tool("memory_recall",
|
|
447
|
-
"
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
567
|
+
mcp.tool("memory_recall", [
|
|
568
|
+
"Semantic + filtered search over memory. Returns facts ranked by relevance (vector similarity blended with recency, importance, and reinforcement).",
|
|
569
|
+
"Three main uses:",
|
|
570
|
+
" 1. Session-start briefing — broad query like 'session briefing: user preferences, active projects, recent decisions'.",
|
|
571
|
+
" 2. Per-prompt contextual recall — focused query derived from what the user just asked.",
|
|
572
|
+
" 3. Pre-store conflict check — recall similar facts before storing, so you can use 'supersedes' if the new fact replaces an older one.",
|
|
573
|
+
"Combine the natural-language query with structured filters (category, domain, entity, date range, metadata) for tighter results.",
|
|
574
|
+
"If you have a known entity name and want everything about it, prefer memory_entity. For 'what does X own/use?' style questions, prefer memory_relations.",
|
|
575
|
+
].join("\n"), {
|
|
576
|
+
query: z.string().describe("Natural-language description of what you're looking for. Embedded and matched semantically — full sentences work better than keyword lists."),
|
|
577
|
+
category: z.string().optional().describe("Filter by category (same vocabulary as memory_store.category). Optional."),
|
|
578
|
+
domain: z.string().optional().describe("Filter by domain activity profile (same vocabulary as memory_store.domain). Optional."),
|
|
579
|
+
kind: z.string().optional().describe("Filter by kind: fact, summary, distilled, relation. Optional. Telemetry is excluded by default."),
|
|
580
|
+
memory_subtype: z.string().optional().describe("Filter by memory subtype (must be valid for the chosen kind). Optional."),
|
|
581
|
+
workspace_id: z.string().optional().describe("Filter to facts in this workspace namespace. Omit to use the default workspace from config."),
|
|
582
|
+
include_legacy_workspace: z.boolean().optional().describe("Backwards-compatibility flag: also include legacy facts that have no workspace_id. Default false. Only set this if you suspect pre-migration data is missing from results."),
|
|
583
|
+
entity: z.string().optional().describe("Restrict to facts mentioning this entity (case-insensitive). For full entity context prefer memory_entity."),
|
|
584
|
+
episode_id: z.string().optional().describe("Filter by coherent episode ID."),
|
|
585
|
+
workstream_key: z.string().optional().describe("Filter by durable workstream key."),
|
|
586
|
+
task_key: z.string().optional().describe("Filter by task or issue key."),
|
|
587
|
+
repo: z.string().optional().describe("Filter by repository or project surface."),
|
|
588
|
+
branch: z.string().optional().describe("Filter by branch or working surface."),
|
|
589
|
+
review_status: z.string().optional().describe("Filter by review lifecycle status (candidate / reviewed / approved / rejected)."),
|
|
590
|
+
since: z.string().optional().describe("Only facts created on or after this ISO 8601 date or datetime."),
|
|
591
|
+
until: z.string().optional().describe("Only facts created on or before this ISO 8601 date or datetime."),
|
|
592
|
+
limit: z.number().optional().default(10).describe("Max results to return (default 10)."),
|
|
593
|
+
graph_depth: z.number().optional().default(0).describe("Entity-graph traversal depth. 0 = vector search only (fast, default). 1 = also surface 1-hop entity-related facts (slower; use when the user asks 'what's connected to X?')."),
|
|
594
|
+
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)."),
|
|
595
|
+
}, async ({ query, category, domain, kind, memory_subtype, workspace_id, include_legacy_workspace, entity, episode_id, workstream_key, task_key, repo, branch, review_status, since, until, limit, graph_depth, metadata_filter, }) => {
|
|
460
596
|
const guard = requireReady();
|
|
461
597
|
if (guard)
|
|
462
598
|
return guard;
|
|
463
599
|
const requestedLimit = limit ?? 10;
|
|
464
|
-
const
|
|
465
|
-
const
|
|
600
|
+
const scope = resolveScope(workspace_id, include_legacy_workspace);
|
|
601
|
+
const redactedQuery = redactStorageText(query);
|
|
602
|
+
const vector = await embed(redactedQuery.text);
|
|
603
|
+
const normalizedKind = kind ? normalizeKind(kind) : undefined;
|
|
604
|
+
let normalizedSubtype;
|
|
605
|
+
if (memory_subtype) {
|
|
606
|
+
try {
|
|
607
|
+
normalizedSubtype = validateMemorySubtype(normalizedKind, memory_subtype) ?? undefined;
|
|
608
|
+
}
|
|
609
|
+
catch (e) {
|
|
610
|
+
return {
|
|
611
|
+
content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
|
|
612
|
+
isError: true,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const filter = scopedFilter(scope, {
|
|
617
|
+
category: category ? normalizeCategory(category) : undefined,
|
|
618
|
+
domain: domain ? normalizeDomain(domain) : undefined,
|
|
619
|
+
kind: normalizedKind,
|
|
620
|
+
memory_subtype: normalizedSubtype,
|
|
621
|
+
entity,
|
|
622
|
+
episode_id,
|
|
623
|
+
workstream_key,
|
|
624
|
+
task_key,
|
|
625
|
+
repo,
|
|
626
|
+
branch,
|
|
627
|
+
review_status,
|
|
628
|
+
since,
|
|
629
|
+
until,
|
|
630
|
+
metadata: metadata_filter,
|
|
631
|
+
excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS,
|
|
632
|
+
});
|
|
466
633
|
const results = await qdrantSearch(vector, filter, requestedLimit * 2);
|
|
467
634
|
if (!results.result?.length) {
|
|
468
635
|
const nudge = buildMemoryNudge();
|
|
@@ -475,7 +642,7 @@ export function registerTools(mcp) {
|
|
|
475
642
|
.slice(0, requestedLimit);
|
|
476
643
|
const lines = ranked.map((r) => formatFact(r));
|
|
477
644
|
if ((graph_depth ?? 0) >= 1) {
|
|
478
|
-
const relatedLines = await graphTraversal(ranked, requestedLimit);
|
|
645
|
+
const relatedLines = await graphTraversal(ranked, requestedLimit, scope);
|
|
479
646
|
if (relatedLines.length > 0) {
|
|
480
647
|
lines.push("", "── Related (1-hop) ──");
|
|
481
648
|
lines.push(...relatedLines);
|
|
@@ -487,28 +654,30 @@ export function registerTools(mcp) {
|
|
|
487
654
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
488
655
|
});
|
|
489
656
|
// ── memory_entity ───────────────────────────────────────────────────────
|
|
490
|
-
mcp.tool("memory_entity",
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
657
|
+
mcp.tool("memory_entity", [
|
|
658
|
+
"Get everything bikky knows about a specific entity — facts mentioning it plus typed relations into and out of it.",
|
|
659
|
+
"Prefer this over memory_recall when the user asks 'tell me about X' or 'what do we know about X' and X is a known entity name (service, person, repo, concept). Faster and more complete than semantic search for entity-centric queries.",
|
|
660
|
+
"If you only have a fuzzy description, use memory_recall first to find the entity name.",
|
|
661
|
+
].join(" "), {
|
|
662
|
+
name: z.string().describe("Entity name (case-insensitive, e.g. 'qdrant', 'workspace_id'). Should match the lowercase canonical form used when facts were stored."),
|
|
663
|
+
limit: z.number().optional().default(20).describe("Max facts to return (default 20). Relations are always returned in full, capped at 50 each direction."),
|
|
664
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
665
|
+
include_legacy_workspace: z.boolean().optional().describe("Backwards-compatibility: also include legacy facts with no workspace_id. Default false."),
|
|
666
|
+
}, async ({ name, limit, workspace_id, include_legacy_workspace }) => {
|
|
494
667
|
const guard = requireReady();
|
|
495
668
|
if (guard)
|
|
496
669
|
return guard;
|
|
497
670
|
const entityName = name.toLowerCase();
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const relationsFrom = await qdrantScroll(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const relationsTo = await qdrantScroll({ must: [
|
|
509
|
-
{ key: "to_entity", match: { value: entityName } },
|
|
510
|
-
{ is_null: { key: "superseded_by" } },
|
|
511
|
-
] }, 50);
|
|
671
|
+
const scope = resolveScope(workspace_id, include_legacy_workspace);
|
|
672
|
+
const factsFilter = scopedFilter(scope) ?? { must: [] };
|
|
673
|
+
factsFilter.must.push({ key: "entities", match: { value: entityName } });
|
|
674
|
+
const facts = await qdrantScroll(factsFilter, limit ?? 20);
|
|
675
|
+
const fromFilter = scopedFilter(scope) ?? { must: [] };
|
|
676
|
+
fromFilter.must.push({ key: "from_entity", match: { value: entityName } });
|
|
677
|
+
const relationsFrom = await qdrantScroll(fromFilter, 50);
|
|
678
|
+
const toFilter = scopedFilter(scope) ?? { must: [] };
|
|
679
|
+
toFilter.must.push({ key: "to_entity", match: { value: entityName } });
|
|
680
|
+
const relationsTo = await qdrantScroll(toFilter, 50);
|
|
512
681
|
const output = [];
|
|
513
682
|
const factPoints = facts.result?.points ?? [];
|
|
514
683
|
if (factPoints.length > 0) {
|
|
@@ -543,22 +712,26 @@ export function registerTools(mcp) {
|
|
|
543
712
|
return { content: [{ type: "text", text: output.join("\n") }] };
|
|
544
713
|
});
|
|
545
714
|
// ── memory_relations ────────────────────────────────────────────────────
|
|
546
|
-
mcp.tool("memory_relations",
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
715
|
+
mcp.tool("memory_relations", [
|
|
716
|
+
"Query typed edges between entities. Returns 'A --[type]--> B' triples that semantic search alone wouldn't surface.",
|
|
717
|
+
"Use for 'what does X own / use / depend on?' and 'who owns Y?' style questions. Optionally filter by direction (from / to / both) and relation type.",
|
|
718
|
+
"To create relations, use memory_store with the 'relation' field — there is no separate create-relation tool.",
|
|
719
|
+
].join(" "), {
|
|
720
|
+
entity: z.string().describe("Entity name to query (case-insensitive)."),
|
|
721
|
+
relation_type: z.string().optional().describe("Filter to a specific edge label (e.g. 'owns', 'uses', 'decided', 'prefers', 'works-on'). Optional."),
|
|
722
|
+
direction: z.enum(["from", "to", "both"]).optional().default("both").describe("Which side of the edge the entity is on. 'from' = entity is the source (X --[?]--> ?). 'to' = entity is the target (? --[?]--> X). 'both' = either (default)."),
|
|
723
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
724
|
+
include_legacy_workspace: z.boolean().optional().describe("Backwards-compatibility: also include legacy facts with no workspace_id. Default false."),
|
|
725
|
+
}, async ({ entity, relation_type, direction, workspace_id, include_legacy_workspace }) => {
|
|
552
726
|
const guard = requireReady();
|
|
553
727
|
if (guard)
|
|
554
728
|
return guard;
|
|
555
729
|
const entityName = entity.toLowerCase();
|
|
730
|
+
const scope = resolveScope(workspace_id, include_legacy_workspace);
|
|
556
731
|
const results = [];
|
|
557
732
|
if (direction === "from" || direction === "both") {
|
|
558
|
-
const filter = { must: [
|
|
559
|
-
|
|
560
|
-
{ is_null: { key: "superseded_by" } },
|
|
561
|
-
] };
|
|
733
|
+
const filter = scopedFilter(scope) ?? { must: [] };
|
|
734
|
+
filter.must.push({ key: "from_entity", match: { value: entityName } });
|
|
562
735
|
if (relation_type) {
|
|
563
736
|
filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
|
|
564
737
|
}
|
|
@@ -566,10 +739,8 @@ export function registerTools(mcp) {
|
|
|
566
739
|
results.push(...(r.result?.points ?? []));
|
|
567
740
|
}
|
|
568
741
|
if (direction === "to" || direction === "both") {
|
|
569
|
-
const filter = { must: [
|
|
570
|
-
|
|
571
|
-
{ is_null: { key: "superseded_by" } },
|
|
572
|
-
] };
|
|
742
|
+
const filter = scopedFilter(scope) ?? { must: [] };
|
|
743
|
+
filter.must.push({ key: "to_entity", match: { value: entityName } });
|
|
573
744
|
if (relation_type) {
|
|
574
745
|
filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
|
|
575
746
|
}
|
|
@@ -593,38 +764,62 @@ export function registerTools(mcp) {
|
|
|
593
764
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
594
765
|
});
|
|
595
766
|
// ── memory_forget ───────────────────────────────────────────────────────
|
|
596
|
-
mcp.tool("memory_forget",
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
767
|
+
mcp.tool("memory_forget", [
|
|
768
|
+
"Mark a fact as superseded/wrong. The fact stays in storage (for audit) but is excluded from all recall results.",
|
|
769
|
+
"Use this when a fact was simply incorrect or no longer applies and there is no replacement. If you have a corrected version, use memory_store with 'supersedes: <fact_id>' instead — that way the new fact stays linked to the old one.",
|
|
770
|
+
].join(" "), {
|
|
771
|
+
fact_id: z.string().describe("ID of the fact to forget (returned by memory_store / memory_recall as 'id')."),
|
|
772
|
+
reason: z.string().describe("Short human-readable reason this fact is being retired (stored in 'superseded_by' for future audit)."),
|
|
773
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
774
|
+
}, async ({ fact_id, reason, workspace_id }) => {
|
|
600
775
|
const guard = requireReady();
|
|
601
776
|
if (guard)
|
|
602
777
|
return guard;
|
|
603
778
|
const now = nowISO();
|
|
604
779
|
try {
|
|
780
|
+
const scope = resolveScope(workspace_id);
|
|
781
|
+
const existing = await getPointForWorkspaceWrite(fact_id, scope);
|
|
782
|
+
if (existing.error) {
|
|
783
|
+
return { content: [{ type: "text", text: JSON.stringify(existing.error, null, 2) }], isError: true };
|
|
784
|
+
}
|
|
785
|
+
const redactedReason = redactStorageText(reason);
|
|
605
786
|
await qdrantSetPayload([fact_id], {
|
|
606
|
-
superseded_by: `forgotten:${
|
|
787
|
+
superseded_by: `forgotten:${redactedReason.text}`,
|
|
607
788
|
superseded_at: now,
|
|
608
789
|
updated_at: now,
|
|
609
790
|
});
|
|
610
|
-
return { content: [{ type: "text", text: JSON.stringify({
|
|
791
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
792
|
+
status: "forgotten",
|
|
793
|
+
fact_id,
|
|
794
|
+
reason: redactedReason.text,
|
|
795
|
+
...(redactedReason.redacted ? { redaction: redactedReason } : {}),
|
|
796
|
+
}) }] };
|
|
611
797
|
}
|
|
612
798
|
catch (e) {
|
|
613
799
|
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
614
800
|
}
|
|
615
801
|
});
|
|
616
802
|
// ── memory_verify ───────────────────────────────────────────────────────
|
|
617
|
-
mcp.tool("memory_verify",
|
|
618
|
-
|
|
619
|
-
|
|
803
|
+
mcp.tool("memory_verify", [
|
|
804
|
+
"Confirm an existing fact is still accurate, without re-storing it. Resets the staleness clock and bumps a verification counter.",
|
|
805
|
+
"Use this when memory_heartbeat surfaces a stale fact ID and you can confirm it's still true (e.g. you just observed the system in that state). Lighter than memory_store(supersedes:) — same content, fresh timestamp.",
|
|
806
|
+
"If the fact is no longer true, use memory_forget or memory_store(supersedes:) instead.",
|
|
807
|
+
].join(" "), {
|
|
808
|
+
fact_id: z.string().describe("ID of the fact to verify (from memory_recall or memory_heartbeat)."),
|
|
809
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
810
|
+
}, async ({ fact_id, workspace_id }) => {
|
|
620
811
|
const guard = requireReady();
|
|
621
812
|
if (guard)
|
|
622
813
|
return guard;
|
|
623
814
|
const now = nowISO();
|
|
624
815
|
try {
|
|
625
|
-
const
|
|
816
|
+
const scope = resolveScope(workspace_id);
|
|
817
|
+
const writable = await getPointForWorkspaceWrite(fact_id, scope);
|
|
818
|
+
if (writable.error) {
|
|
819
|
+
return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
|
|
820
|
+
}
|
|
626
821
|
let currentCount = 0;
|
|
627
|
-
const existingPt =
|
|
822
|
+
const existingPt = writable.point;
|
|
628
823
|
if (existingPt) {
|
|
629
824
|
currentCount = existingPt.payload.verification_count ?? 0;
|
|
630
825
|
}
|
|
@@ -649,24 +844,26 @@ export function registerTools(mcp) {
|
|
|
649
844
|
}
|
|
650
845
|
});
|
|
651
846
|
// ── memory_review ───────────────────────────────────────────────────────
|
|
652
|
-
mcp.tool("memory_review",
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
847
|
+
mcp.tool("memory_review", [
|
|
848
|
+
"Triage facts that were extracted automatically by the bikky daemon (source='daemon').",
|
|
849
|
+
"Only useful when the daemon is running and capturing memories from logs/transcripts; otherwise this returns an empty list. Supports four actions: list (default — show recent daemon facts), approve (mark verified), reject (mark superseded with reason), correct (replace with edited content as a new fact).",
|
|
850
|
+
].join(" "), {
|
|
851
|
+
limit: z.number().optional().default(10).describe("Max facts to return when action=list (default 10)."),
|
|
852
|
+
action: z.enum(["list", "approve", "reject", "correct"]).optional().default("list").describe("What to do. list = show recent daemon-extracted facts (default). approve = confirm a fact is correct (bumps verification count). reject = mark a fact as wrong (requires 'reason'). correct = supersede with an edited version (requires 'corrected_content')."),
|
|
853
|
+
fact_id: z.string().optional().describe("Fact ID to act on. Required for approve / reject / correct."),
|
|
854
|
+
reason: z.string().optional().describe("Required for action=reject. Short reason the fact is wrong."),
|
|
855
|
+
corrected_content: z.string().optional().describe("Required for action=correct. The fixed fact text. Stored as a new fact that supersedes the original."),
|
|
856
|
+
workspace_id: z.string().optional().describe("Workspace namespace. Omit to use the default from config."),
|
|
857
|
+
include_legacy_workspace: z.boolean().optional().describe("Backwards-compatibility: also include legacy facts with no workspace_id. Default false."),
|
|
858
|
+
}, async ({ limit, action, fact_id, reason, corrected_content, workspace_id, include_legacy_workspace }) => {
|
|
660
859
|
const guard = requireReady();
|
|
661
860
|
if (guard)
|
|
662
861
|
return guard;
|
|
862
|
+
const scope = resolveScope(workspace_id, include_legacy_workspace);
|
|
663
863
|
if (action === "list") {
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
{ is_null: { key: "superseded_by" } },
|
|
668
|
-
],
|
|
669
|
-
}, (limit ?? 10) * 2);
|
|
864
|
+
const filter = scopedFilter(scope) ?? { must: [] };
|
|
865
|
+
filter.must.push({ key: "source", match: { value: "daemon" } });
|
|
866
|
+
const result = await qdrantScroll(filter, (limit ?? 10) * 2);
|
|
670
867
|
const points = (result.result?.points ?? [])
|
|
671
868
|
.sort((a, b) => (b.payload.created_at ?? "").localeCompare(a.payload.created_at ?? ""))
|
|
672
869
|
.slice(0, limit ?? 10);
|
|
@@ -684,9 +881,12 @@ export function registerTools(mcp) {
|
|
|
684
881
|
}
|
|
685
882
|
const now = nowISO();
|
|
686
883
|
if (action === "approve") {
|
|
687
|
-
const
|
|
884
|
+
const writable = await getPointForWorkspaceWrite(fact_id, scope);
|
|
885
|
+
if (writable.error) {
|
|
886
|
+
return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
|
|
887
|
+
}
|
|
688
888
|
let currentCount = 0;
|
|
689
|
-
const approvePt =
|
|
889
|
+
const approvePt = writable.point;
|
|
690
890
|
if (approvePt) {
|
|
691
891
|
currentCount = approvePt.payload.verification_count ?? 0;
|
|
692
892
|
}
|
|
@@ -701,28 +901,52 @@ export function registerTools(mcp) {
|
|
|
701
901
|
if (!reason) {
|
|
702
902
|
return { content: [{ type: "text", text: "Error: reason is required for reject action." }] };
|
|
703
903
|
}
|
|
904
|
+
const writable = await getPointForWorkspaceWrite(fact_id, scope);
|
|
905
|
+
if (writable.error) {
|
|
906
|
+
return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
|
|
907
|
+
}
|
|
908
|
+
const redactedReason = redactStorageText(reason);
|
|
704
909
|
await qdrantSetPayload([fact_id], {
|
|
705
|
-
superseded_by: `rejected:${
|
|
910
|
+
superseded_by: `rejected:${redactedReason.text}`,
|
|
706
911
|
superseded_at: now,
|
|
707
912
|
updated_at: now,
|
|
708
913
|
});
|
|
709
|
-
return { content: [{ type: "text", text: JSON.stringify({
|
|
914
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
915
|
+
status: "rejected",
|
|
916
|
+
fact_id,
|
|
917
|
+
reason: redactedReason.text,
|
|
918
|
+
...(redactedReason.redacted ? { redaction: redactedReason } : {}),
|
|
919
|
+
}) }] };
|
|
710
920
|
}
|
|
711
921
|
if (action === "correct") {
|
|
712
922
|
if (!corrected_content) {
|
|
713
923
|
return { content: [{ type: "text", text: "Error: corrected_content is required for correct action." }] };
|
|
714
924
|
}
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
925
|
+
const writable = await getPointForWorkspaceWrite(fact_id, scope);
|
|
926
|
+
if (writable.error) {
|
|
927
|
+
return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
|
|
928
|
+
}
|
|
929
|
+
const origPayload = writable.point?.payload;
|
|
930
|
+
const redactedCorrected = redactStorageText(corrected_content);
|
|
931
|
+
const correctionScope = origPayload?.workspace_id
|
|
932
|
+
? resolveScope(origPayload.workspace_id, false)
|
|
933
|
+
: scope;
|
|
934
|
+
const vector = await embed(redactedCorrected.text);
|
|
718
935
|
const correctedId = crypto.randomUUID();
|
|
719
|
-
const origCategory = origPayload?.category ??
|
|
720
|
-
const hash = contentHash(origCategory,
|
|
721
|
-
|
|
722
|
-
content:
|
|
936
|
+
const origCategory = normalizeCategory(origPayload?.category ?? DEFAULT_CATEGORY);
|
|
937
|
+
const hash = contentHash(origCategory, redactedCorrected.text);
|
|
938
|
+
const correctedPayload = {
|
|
939
|
+
content: redactedCorrected.text,
|
|
723
940
|
category: origCategory,
|
|
724
|
-
domain: origPayload?.domain ??
|
|
725
|
-
kind: origPayload?.kind ?? "fact",
|
|
941
|
+
domain: normalizeDomain(origPayload?.domain ?? DEFAULT_DOMAIN),
|
|
942
|
+
kind: normalizeKind(origPayload?.kind ?? "fact"),
|
|
943
|
+
...(origPayload?.memory_subtype ? { memory_subtype: origPayload.memory_subtype } : {}),
|
|
944
|
+
...(origPayload?.layer ? { layer: origPayload.layer } : {}),
|
|
945
|
+
...(origPayload?.episode_id ? { episode_id: origPayload.episode_id } : {}),
|
|
946
|
+
...(origPayload?.workstream_key ? { workstream_key: origPayload.workstream_key } : {}),
|
|
947
|
+
...(origPayload?.task_key ? { task_key: origPayload.task_key } : {}),
|
|
948
|
+
...(origPayload?.repo ? { repo: origPayload.repo } : {}),
|
|
949
|
+
...(origPayload?.branch ? { branch: origPayload.branch } : {}),
|
|
726
950
|
entities: origPayload?.entities ?? [],
|
|
727
951
|
source: "user",
|
|
728
952
|
confidence: 0.95,
|
|
@@ -735,7 +959,10 @@ export function registerTools(mcp) {
|
|
|
735
959
|
created_at: now,
|
|
736
960
|
updated_at: now,
|
|
737
961
|
metadata: { ...(origPayload?.metadata ?? {}), corrected_from: fact_id },
|
|
738
|
-
}
|
|
962
|
+
};
|
|
963
|
+
addWorkspacePayload(correctedPayload, correctionScope);
|
|
964
|
+
addRedactionPayload(correctedPayload, redactedCorrected);
|
|
965
|
+
await qdrantUpsert(correctedId, vector, correctedPayload);
|
|
739
966
|
await qdrantSetPayload([fact_id], {
|
|
740
967
|
superseded_by: correctedId,
|
|
741
968
|
superseded_at: now,
|
|
@@ -745,177 +972,11 @@ export function registerTools(mcp) {
|
|
|
745
972
|
}
|
|
746
973
|
return { content: [{ type: "text", text: `Unknown action: ${String(action)}` }] };
|
|
747
974
|
});
|
|
748
|
-
// ── memory_session_summary ──────────────────────────────────────────────
|
|
749
|
-
mcp.tool("memory_session_summary", "Store a compressed summary of the current session. Call before a session ends or when a major task completes. " +
|
|
750
|
-
"Future sessions receive this via memory_recall('session briefing'). Idempotent per session_id.", {
|
|
751
|
-
session_id: z.string().describe("Session UUID"),
|
|
752
|
-
summary: z.string().describe("2-5 sentence compressed summary of what happened this session"),
|
|
753
|
-
tasks_completed: z.array(z.string()).optional().default([]).describe("Task slugs completed"),
|
|
754
|
-
decisions_made: z.array(z.string()).optional().default([]).describe("Key decisions"),
|
|
755
|
-
entities_touched: z.array(z.string()).optional().default([]).describe("Entities involved (lowercase)"),
|
|
756
|
-
}, async ({ session_id, summary, tasks_completed, decisions_made, entities_touched }) => {
|
|
757
|
-
const guard = requireReady();
|
|
758
|
-
if (guard)
|
|
759
|
-
return guard;
|
|
760
|
-
const now = nowISO();
|
|
761
|
-
const normalizedEntities = entities_touched.map((e) => e.toLowerCase());
|
|
762
|
-
// Check for existing summary for this session
|
|
763
|
-
try {
|
|
764
|
-
const existing = await qdrantScroll({ must: [
|
|
765
|
-
{ key: "session_id", match: { value: session_id } },
|
|
766
|
-
{ key: "kind", match: { value: "summary" } },
|
|
767
|
-
{ is_null: { key: "superseded_by" } },
|
|
768
|
-
] }, 1);
|
|
769
|
-
const summaryPoint = existing.result?.points?.[0];
|
|
770
|
-
if (summaryPoint) {
|
|
771
|
-
const point = summaryPoint;
|
|
772
|
-
const vector = await embed(summary);
|
|
773
|
-
await qdrantUpsert(point.id, vector, {
|
|
774
|
-
...point.payload,
|
|
775
|
-
content: summary,
|
|
776
|
-
kind: "summary",
|
|
777
|
-
domain: "work",
|
|
778
|
-
source: "system",
|
|
779
|
-
tasks_completed,
|
|
780
|
-
decisions_made,
|
|
781
|
-
entities: normalizedEntities,
|
|
782
|
-
content_hash: contentHash("observation", summary),
|
|
783
|
-
updated_at: now,
|
|
784
|
-
});
|
|
785
|
-
return {
|
|
786
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
787
|
-
action: "updated", fact_id: point.id, session_id,
|
|
788
|
-
}) }],
|
|
789
|
-
};
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
catch (e) {
|
|
793
|
-
log("WARN", `Session summary lookup failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
794
|
-
}
|
|
795
|
-
// Insert new summary
|
|
796
|
-
const vector = await embed(summary);
|
|
797
|
-
const factId = newId();
|
|
798
|
-
await qdrantUpsert(factId, vector, {
|
|
799
|
-
content: summary,
|
|
800
|
-
category: "observation",
|
|
801
|
-
domain: "work",
|
|
802
|
-
kind: "summary",
|
|
803
|
-
entities: normalizedEntities,
|
|
804
|
-
source: "system",
|
|
805
|
-
confidence: 1.0,
|
|
806
|
-
content_hash: contentHash("observation", summary),
|
|
807
|
-
reinforcement_count: 1,
|
|
808
|
-
last_reinforced_at: now,
|
|
809
|
-
superseded_by: null,
|
|
810
|
-
superseded_at: null,
|
|
811
|
-
created_at: now,
|
|
812
|
-
updated_at: now,
|
|
813
|
-
session_id,
|
|
814
|
-
tasks_completed,
|
|
815
|
-
decisions_made,
|
|
816
|
-
});
|
|
817
|
-
return {
|
|
818
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
819
|
-
action: "stored", fact_id: factId, session_id,
|
|
820
|
-
}) }],
|
|
821
|
-
};
|
|
822
|
-
});
|
|
823
|
-
// ── memory_distill ──────────────────────────────────────────────────────
|
|
824
|
-
mcp.tool("memory_distill", "Consolidate recent session summaries into distilled patterns. Call when 5+ session summaries exist. " +
|
|
825
|
-
"Uses LLM to extract recurring patterns and key learnings, then supersedes the source summaries.", {
|
|
826
|
-
days: z.number().optional().default(14).describe("Look-back period in days (default 14)"),
|
|
827
|
-
max_summaries: z.number().optional().default(20).describe("Max summaries to consolidate"),
|
|
828
|
-
}, async ({ days, max_summaries }) => {
|
|
829
|
-
const guard = requireReady();
|
|
830
|
-
if (guard)
|
|
831
|
-
return guard;
|
|
832
|
-
const now = nowISO();
|
|
833
|
-
const daysVal = days ?? 14;
|
|
834
|
-
const maxVal = max_summaries ?? 20;
|
|
835
|
-
const since = new Date(Date.now() - daysVal * 86400000).toISOString();
|
|
836
|
-
const summaryResults = await qdrantScroll({ must: [
|
|
837
|
-
{ key: "kind", match: { value: "summary" } },
|
|
838
|
-
{ key: "created_at", range: { gte: since } },
|
|
839
|
-
{ is_null: { key: "superseded_by" } },
|
|
840
|
-
] }, maxVal);
|
|
841
|
-
const summaries = summaryResults.result?.points ?? [];
|
|
842
|
-
if (summaries.length < 3) {
|
|
843
|
-
return {
|
|
844
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
845
|
-
action: "skipped",
|
|
846
|
-
reason: `Only ${summaries.length} session summaries in the last ${daysVal} days. Need at least 3.`,
|
|
847
|
-
}) }],
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
const summaryTexts = summaries.map((s, i) => {
|
|
851
|
-
const p = s.payload;
|
|
852
|
-
const parts = [`Session ${i + 1} (${p.created_at}):\n${p.content}`];
|
|
853
|
-
if (p.tasks_completed?.length)
|
|
854
|
-
parts.push(`Tasks: ${p.tasks_completed.join(", ")}`);
|
|
855
|
-
if (p.decisions_made?.length)
|
|
856
|
-
parts.push(`Decisions: ${p.decisions_made.join("; ")}`);
|
|
857
|
-
return parts.join("\n");
|
|
858
|
-
}).join("\n\n---\n\n");
|
|
859
|
-
const systemPrompt = "You are a memory consolidation system. Given session summaries from an engineering agent, " +
|
|
860
|
-
"extract recurring patterns, consolidated learnings, and key facts. Output:\n" +
|
|
861
|
-
"1. Recurring patterns (things that keep coming up)\n" +
|
|
862
|
-
"2. Key infrastructure/project facts learned\n" +
|
|
863
|
-
"3. Decisions made and their rationale\n" +
|
|
864
|
-
"4. Open issues or recurring problems\n" +
|
|
865
|
-
"Be concise — one line per point. Omit ephemeral details.";
|
|
866
|
-
let distilledContent;
|
|
867
|
-
try {
|
|
868
|
-
distilledContent = await chatComplete(systemPrompt, summaryTexts);
|
|
869
|
-
}
|
|
870
|
-
catch (e) {
|
|
871
|
-
return { content: [{ type: "text", text: `Distillation failed: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
872
|
-
}
|
|
873
|
-
const allEntities = [...new Set(summaries.flatMap((s) => s.payload.entities ?? []))];
|
|
874
|
-
const vector = await embed(distilledContent);
|
|
875
|
-
const factId = newId();
|
|
876
|
-
const sourceIds = summaries.map((s) => s.id);
|
|
877
|
-
await qdrantUpsert(factId, vector, {
|
|
878
|
-
content: distilledContent,
|
|
879
|
-
category: "observation",
|
|
880
|
-
domain: "work",
|
|
881
|
-
kind: "distilled",
|
|
882
|
-
entities: allEntities,
|
|
883
|
-
source: "system",
|
|
884
|
-
confidence: 0.9,
|
|
885
|
-
content_hash: contentHash("observation", distilledContent),
|
|
886
|
-
reinforcement_count: 1,
|
|
887
|
-
last_reinforced_at: now,
|
|
888
|
-
superseded_by: null,
|
|
889
|
-
superseded_at: null,
|
|
890
|
-
created_at: now,
|
|
891
|
-
updated_at: now,
|
|
892
|
-
distilled_from: sourceIds,
|
|
893
|
-
distilled_period_start: since,
|
|
894
|
-
distilled_period_end: now,
|
|
895
|
-
summary_count: summaries.length,
|
|
896
|
-
});
|
|
897
|
-
try {
|
|
898
|
-
await qdrantSetPayload(sourceIds, {
|
|
899
|
-
superseded_by: factId,
|
|
900
|
-
superseded_at: now,
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
catch (e) {
|
|
904
|
-
log("WARN", `Failed to supersede some source summaries: ${e instanceof Error ? e.message : String(e)}`);
|
|
905
|
-
}
|
|
906
|
-
return {
|
|
907
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
908
|
-
action: "distilled",
|
|
909
|
-
fact_id: factId,
|
|
910
|
-
summaries_consolidated: summaries.length,
|
|
911
|
-
period: { start: since, end: now },
|
|
912
|
-
entities: allEntities,
|
|
913
|
-
preview: distilledContent.substring(0, 300) + (distilledContent.length > 300 ? "..." : ""),
|
|
914
|
-
}, null, 2) }],
|
|
915
|
-
};
|
|
916
|
-
});
|
|
917
975
|
// ── memory_heartbeat ────────────────────────────────────────────────────
|
|
918
|
-
mcp.tool("memory_heartbeat",
|
|
976
|
+
mcp.tool("memory_heartbeat", [
|
|
977
|
+
"Reflection check-in. Returns up to three things: a memory nudge if you haven't stored anything in 10+ minutes, stale-fact alerts every 3rd call (with IDs you can pass to memory_verify or memory_forget), and a reflection prompt asking whether the last few minutes of work produced anything worth storing.",
|
|
978
|
+
"Call periodically during interactive sessions — roughly every 10 minutes or every 3rd user prompt. No arguments. Cheap and read-only.",
|
|
979
|
+
].join(" "), {}, async () => {
|
|
919
980
|
heartbeatCount++;
|
|
920
981
|
const sections = [];
|
|
921
982
|
const nudge = buildMemoryNudge();
|
|
@@ -924,17 +985,17 @@ export function registerTools(mcp) {
|
|
|
924
985
|
if (heartbeatCount % 3 === 0 && ready) {
|
|
925
986
|
try {
|
|
926
987
|
const staleThreshold = new Date(Date.now() - STALENESS_DAYS * 86400000).toISOString();
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
988
|
+
const scope = resolveScope();
|
|
989
|
+
const staleFilter = scopedFilter(scope) ?? { must: [] };
|
|
990
|
+
staleFilter.must.push({ key: "category", match: { any: ["infrastructure", "projects", "decisions"] } });
|
|
991
|
+
staleFilter.should = [
|
|
992
|
+
{ key: "last_reinforced_at", range: { lte: staleThreshold } },
|
|
993
|
+
{ is_null: { key: "last_reinforced_at" } },
|
|
994
|
+
];
|
|
995
|
+
staleFilter.must_not = [
|
|
996
|
+
{ key: "last_verified_at", range: { gte: staleThreshold } },
|
|
997
|
+
];
|
|
998
|
+
const staleResults = await qdrantScroll(staleFilter, 3);
|
|
938
999
|
const staleFacts = staleResults.result?.points ?? [];
|
|
939
1000
|
if (staleFacts.length > 0) {
|
|
940
1001
|
const staleLines = staleFacts.map((f) => {
|
|
@@ -950,8 +1011,12 @@ export function registerTools(mcp) {
|
|
|
950
1011
|
log("WARN", `Staleness check failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
951
1012
|
}
|
|
952
1013
|
}
|
|
953
|
-
sections.push("🔍
|
|
954
|
-
"
|
|
1014
|
+
sections.push("🔍 Reflect: think about the LAST 10 minutes of work and answer in your head:\n" +
|
|
1015
|
+
" 1. Did you touch a service, port, config, or file path you hadn't seen before?\n" +
|
|
1016
|
+
" 2. Did you make a choice (library, pattern, approach) you'd want a future session to know about?\n" +
|
|
1017
|
+
" 3. Did you hit an error and find a workaround?\n" +
|
|
1018
|
+
" 4. Did the user state a preference or constraint?\n" +
|
|
1019
|
+
"If any answer is yes, call memory_store now — one atomic fact per item, with category/domain/entities.");
|
|
955
1020
|
return { content: [{ type: "text", text: sections.join("\n\n") }] };
|
|
956
1021
|
});
|
|
957
1022
|
}
|