bikky 0.3.1 → 0.3.2
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 +79 -26
- 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 +77 -0
- package/dist/daemon/episode-summary.d.ts.map +1 -0
- package/dist/daemon/episode-summary.js +239 -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 +68 -0
- package/dist/daemon/workstream-summary.d.ts.map +1 -0
- package/dist/daemon/workstream-summary.js +253 -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 +38 -20
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/taxonomy.d.ts +237 -31
- package/dist/mcp/taxonomy.d.ts.map +1 -1
- package/dist/mcp/taxonomy.js +533 -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 +338 -302
- 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 +74 -0
- package/dist/prompts/distill.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 +87 -0
- package/dist/prompts/extraction.js.map +1 -0
- package/dist/prompts/index.d.ts +50 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +102 -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/render.d.ts +41 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +173 -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 +212 -0
- package/dist/render.test.js.map +1 -0
- package/package.json +9 -2
- 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, domainValues, kindValues, memorySubtypeValues, sourceValues, 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);
|
|
@@ -139,13 +177,13 @@ export function registerTools(mcp) {
|
|
|
139
177
|
embedding_provider: getEmbeddingConfig().provider,
|
|
140
178
|
embedding_model: getEmbeddingConfig().model,
|
|
141
179
|
embedding_dimensions: getEmbeddingConfig().dimensions,
|
|
180
|
+
...(setupError ? { setup_error: setupError } : {}),
|
|
142
181
|
};
|
|
143
182
|
const missing = status["missing"];
|
|
144
183
|
if (!qdrantUrl)
|
|
145
184
|
missing.push("qdrant-url");
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (qdrantUrl && qdrantApiKey) {
|
|
185
|
+
// qdrant-api-key is optional (local / self-hosted Qdrant doesn't need it).
|
|
186
|
+
if (qdrantUrl) {
|
|
149
187
|
try {
|
|
150
188
|
await qdrantReq("GET", "/collections");
|
|
151
189
|
status["qdrant_connected"] = true;
|
|
@@ -159,17 +197,18 @@ export function registerTools(mcp) {
|
|
|
159
197
|
catch { /* ignore */ }
|
|
160
198
|
if (!status["ready"] && missing.length > 0) {
|
|
161
199
|
status["setup_instructions"] =
|
|
162
|
-
"Run `bikky setup` or guide the user:\n" +
|
|
163
|
-
"
|
|
164
|
-
"
|
|
165
|
-
"
|
|
200
|
+
"Run `bikky setup` or guide the user. Pick one Qdrant option:\n" +
|
|
201
|
+
" • Qdrant Cloud (managed, free tier, 1GB): https://cloud.qdrant.io — copy the REST URL + API key\n" +
|
|
202
|
+
" • Local Docker: `docker run -p 6333:6333 qdrant/qdrant` → URL `http://localhost:6333` (no API key needed)\n" +
|
|
203
|
+
" • Self-hosted: any reachable Qdrant; API key only required if QDRANT__SERVICE__API_KEY is set on the server\n" +
|
|
204
|
+
"Then call configure_credentials with the URL (and API key if applicable).";
|
|
166
205
|
}
|
|
167
206
|
return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
|
|
168
207
|
});
|
|
169
208
|
// ── configure_credentials ───────────────────────────────────────────────
|
|
170
209
|
mcp.tool("configure_credentials", "Store Qdrant + embedding credentials in ~/.bikky/config.json. Tests connectivity and creates the collection if needed.", {
|
|
171
|
-
qdrant_url: z.string().optional().describe("Qdrant
|
|
172
|
-
qdrant_api_key: z.string().optional().describe("Qdrant Cloud
|
|
210
|
+
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"),
|
|
211
|
+
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
212
|
openai_api_key: z.string().optional().describe("OpenAI API key (for OpenAI embedding/LLM provider)"),
|
|
174
213
|
}, async ({ qdrant_url, qdrant_api_key, openai_api_key }) => {
|
|
175
214
|
const results = {};
|
|
@@ -191,7 +230,7 @@ export function registerTools(mcp) {
|
|
|
191
230
|
results["openai_api_key"] = "stored ✓";
|
|
192
231
|
}
|
|
193
232
|
saveConfig(cfg);
|
|
194
|
-
if (qdrantUrl
|
|
233
|
+
if (qdrantUrl) {
|
|
195
234
|
try {
|
|
196
235
|
await ensureCollection(QDRANT_INDEXES);
|
|
197
236
|
results["qdrant_collection"] = `'${getCollection()}' ready ✓`;
|
|
@@ -208,14 +247,14 @@ export function registerTools(mcp) {
|
|
|
208
247
|
catch (e) {
|
|
209
248
|
results["embedding"] = `error: ${e instanceof Error ? e.message : String(e)}`;
|
|
210
249
|
}
|
|
211
|
-
setReady(!!
|
|
250
|
+
setReady(!!qdrantUrl);
|
|
212
251
|
results["ready"] = ready;
|
|
213
252
|
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
214
253
|
});
|
|
215
254
|
// ── verify_connection ───────────────────────────────────────────────────
|
|
216
255
|
mcp.tool("verify_connection", "Test that Qdrant is reachable, embeddings work, and the collection exists.", {}, async () => {
|
|
217
256
|
const results = { qdrant: false, embedding: false, collection: false };
|
|
218
|
-
if (qdrantUrl
|
|
257
|
+
if (qdrantUrl) {
|
|
219
258
|
try {
|
|
220
259
|
await qdrantReq("GET", "/collections");
|
|
221
260
|
results["qdrant"] = true;
|
|
@@ -246,12 +285,23 @@ export function registerTools(mcp) {
|
|
|
246
285
|
"Returns the action taken (inserted/reinforced/duplicate) and any similar facts found.", {
|
|
247
286
|
content: z.string().describe("The fact to store (atomic, single piece of knowledge)"),
|
|
248
287
|
category: z.enum(categoryValues())
|
|
249
|
-
.describe("
|
|
288
|
+
.describe("Subject matter: codebase, infrastructure, operations, decisions, product_domain, projects, people, preferences, observations"),
|
|
250
289
|
entities: z.array(z.string()).describe("Related entities (lowercase, e.g. ['qdrant', 'platform'])"),
|
|
251
290
|
domain: z.enum(domainValues()).default(DEFAULT_DOMAIN)
|
|
252
|
-
.describe("
|
|
291
|
+
.describe("Activity profile — e.g. software_engineering, product_strategy, business_operations, research, personal_productivity"),
|
|
253
292
|
kind: z.enum(kindValues()).default(DEFAULT_KIND)
|
|
254
293
|
.describe("Knowledge form — fact, summary, distilled, relation"),
|
|
294
|
+
memory_subtype: z.enum(memorySubtypeValues()).optional()
|
|
295
|
+
.describe("Optional subtype within kind, such as codebase_map, episode, workstream, convention, or recall_event"),
|
|
296
|
+
workspace_id: z.string().optional()
|
|
297
|
+
.describe("Optional workspace namespace for team memory."),
|
|
298
|
+
episode_id: z.string().optional().describe("Optional coherent episode identifier"),
|
|
299
|
+
workstream_key: z.string().optional().describe("Optional durable workstream key"),
|
|
300
|
+
task_key: z.string().optional().describe("Optional task or issue key"),
|
|
301
|
+
repo: z.string().optional().describe("Optional repository or project surface"),
|
|
302
|
+
branch: z.string().optional().describe("Optional branch or working surface"),
|
|
303
|
+
review_status: z.enum(["candidate", "reviewed", "approved", "rejected"]).optional()
|
|
304
|
+
.describe("Optional review lifecycle status"),
|
|
255
305
|
source: z.enum(sourceValues()).default(DEFAULT_SOURCE)
|
|
256
306
|
.describe("Creator — agent, daemon, system, user"),
|
|
257
307
|
confidence: z.number().min(0).max(1).default(0.9).describe("How certain (0.0-1.0)"),
|
|
@@ -264,20 +314,54 @@ export function registerTools(mcp) {
|
|
|
264
314
|
}).optional().describe("Optional typed relation between two entities"),
|
|
265
315
|
metadata: z.record(z.string(), z.string()).optional()
|
|
266
316
|
.describe("Optional key-value metadata. Stored with the fact and filterable via memory_recall."),
|
|
267
|
-
}, async ({ content, category, entities, domain, kind, source, confidence, importance, supersedes, relation, metadata }) => {
|
|
317
|
+
}, 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
318
|
const guard = requireReady();
|
|
269
319
|
if (guard)
|
|
270
320
|
return guard;
|
|
271
321
|
lastStoreTime = Date.now();
|
|
272
322
|
const now = nowISO();
|
|
273
|
-
const
|
|
274
|
-
const
|
|
323
|
+
const scope = resolveScope(workspace_id);
|
|
324
|
+
const normalizedKind = normalizeKind(kind);
|
|
325
|
+
let normalizedSubtype = null;
|
|
326
|
+
try {
|
|
327
|
+
normalizedSubtype = validateMemorySubtype(normalizedKind, memory_subtype);
|
|
328
|
+
}
|
|
329
|
+
catch (e) {
|
|
330
|
+
return {
|
|
331
|
+
content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
|
|
332
|
+
isError: true,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const normalizedCategory = normalizedSubtype
|
|
336
|
+
? categoryForMemorySubtype(normalizedSubtype) ?? normalizeCategory(category)
|
|
337
|
+
: normalizeCategory(category);
|
|
338
|
+
const normalizedDomain = normalizeDomain(domain);
|
|
339
|
+
const normalizedLayer = normalizedSubtype ? layerForMemorySubtype(normalizedSubtype) : null;
|
|
340
|
+
const redactedContent = redactStorageText(content);
|
|
341
|
+
const redactedEntities = entities.map((entity) => redactStorageText(entity));
|
|
342
|
+
const sanitizedEntities = redactedEntities.map((entity) => entity.text);
|
|
343
|
+
const redactedRelation = relation ? {
|
|
344
|
+
from: redactStorageText(relation.from),
|
|
345
|
+
type: redactStorageText(relation.type),
|
|
346
|
+
to: redactStorageText(relation.to),
|
|
347
|
+
} : null;
|
|
348
|
+
const redactionSummary = combineRedactions([
|
|
349
|
+
redactedContent,
|
|
350
|
+
...redactedEntities,
|
|
351
|
+
...(redactedRelation ? [redactedRelation.from, redactedRelation.type, redactedRelation.to] : []),
|
|
352
|
+
]);
|
|
353
|
+
const hash = contentHash(normalizedCategory, redactedContent.text);
|
|
354
|
+
const normalizedEntities = sanitizedEntities.map((e) => e.toLowerCase());
|
|
355
|
+
const sanitizedRelation = redactedRelation ? {
|
|
356
|
+
from: redactedRelation.from.text,
|
|
357
|
+
type: redactedRelation.type.text,
|
|
358
|
+
to: redactedRelation.to.text,
|
|
359
|
+
} : null;
|
|
275
360
|
// 1. Exact dedup via content hash
|
|
276
361
|
try {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
] }, 1);
|
|
362
|
+
const hashFilter = scopedFilter(scope) ?? { must: [] };
|
|
363
|
+
hashFilter.must.push({ key: "content_hash", match: { value: hash } });
|
|
364
|
+
const existing = await qdrantScroll(hashFilter, 1);
|
|
281
365
|
const existingPoint = existing.result?.points?.[0];
|
|
282
366
|
if (existingPoint) {
|
|
283
367
|
const point = existingPoint;
|
|
@@ -301,17 +385,16 @@ export function registerTools(mcp) {
|
|
|
301
385
|
log("WARN", `Hash dedup check failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
302
386
|
}
|
|
303
387
|
// 2. Generate embedding
|
|
304
|
-
const vector = await embed(
|
|
388
|
+
const vector = await embed(redactedContent.text);
|
|
305
389
|
// 3. Semantic dedup
|
|
306
390
|
let similarFacts = [];
|
|
307
391
|
let potentialConflicts = [];
|
|
308
392
|
try {
|
|
309
|
-
const filter = { must: [] };
|
|
393
|
+
const filter = scopedFilter(scope) ?? { must: [] };
|
|
310
394
|
if (normalizedEntities.length > 0) {
|
|
311
395
|
filter.must.push({ key: "entities", match: { any: normalizedEntities } });
|
|
312
396
|
}
|
|
313
|
-
|
|
314
|
-
const results = await qdrantSearch(vector, filter.must.length > 0 ? filter : undefined, 3);
|
|
397
|
+
const results = await qdrantSearch(vector, filter, 3);
|
|
315
398
|
const firstResult = results.result?.[0];
|
|
316
399
|
if (results.result?.length > 0 && firstResult) {
|
|
317
400
|
const topScore = firstResult.score ?? 0;
|
|
@@ -368,6 +451,10 @@ export function registerTools(mcp) {
|
|
|
368
451
|
// 5. Supersede old fact if requested
|
|
369
452
|
if (supersedes) {
|
|
370
453
|
try {
|
|
454
|
+
const existing = await getPointForWorkspaceWrite(supersedes, scope);
|
|
455
|
+
if (existing.error) {
|
|
456
|
+
return { content: [{ type: "text", text: JSON.stringify(existing.error, null, 2) }], isError: true };
|
|
457
|
+
}
|
|
371
458
|
await qdrantSetPayload([supersedes], {
|
|
372
459
|
superseded_by: factId,
|
|
373
460
|
superseded_at: now,
|
|
@@ -379,10 +466,10 @@ export function registerTools(mcp) {
|
|
|
379
466
|
}
|
|
380
467
|
// 6. Insert new fact
|
|
381
468
|
const payload = {
|
|
382
|
-
content,
|
|
383
|
-
category,
|
|
384
|
-
domain,
|
|
385
|
-
kind,
|
|
469
|
+
content: redactedContent.text,
|
|
470
|
+
category: normalizedCategory,
|
|
471
|
+
domain: normalizedDomain,
|
|
472
|
+
kind: normalizedKind,
|
|
386
473
|
entities: normalizedEntities,
|
|
387
474
|
source,
|
|
388
475
|
confidence,
|
|
@@ -395,22 +482,43 @@ export function registerTools(mcp) {
|
|
|
395
482
|
created_at: now,
|
|
396
483
|
updated_at: now,
|
|
397
484
|
};
|
|
485
|
+
if (normalizedSubtype) {
|
|
486
|
+
payload["memory_subtype"] = normalizedSubtype;
|
|
487
|
+
}
|
|
488
|
+
if (normalizedLayer) {
|
|
489
|
+
payload["layer"] = normalizedLayer;
|
|
490
|
+
}
|
|
491
|
+
if (episode_id)
|
|
492
|
+
payload["episode_id"] = episode_id;
|
|
493
|
+
if (workstream_key)
|
|
494
|
+
payload["workstream_key"] = workstream_key;
|
|
495
|
+
if (task_key)
|
|
496
|
+
payload["task_key"] = task_key;
|
|
497
|
+
if (repo)
|
|
498
|
+
payload["repo"] = repo;
|
|
499
|
+
if (branch)
|
|
500
|
+
payload["branch"] = branch;
|
|
501
|
+
if (review_status)
|
|
502
|
+
payload["review_status"] = review_status;
|
|
503
|
+
addWorkspacePayload(payload, scope);
|
|
504
|
+
addRedactionPayload(payload, redactionSummary);
|
|
398
505
|
if (metadata && Object.keys(metadata).length > 0) {
|
|
399
506
|
payload["metadata"] = metadata;
|
|
400
507
|
}
|
|
401
508
|
await qdrantUpsert(factId, vector, payload);
|
|
402
509
|
// 7. Insert relation point if provided
|
|
403
510
|
let relationId = null;
|
|
404
|
-
if (
|
|
511
|
+
if (sanitizedRelation) {
|
|
405
512
|
relationId = newId();
|
|
406
|
-
const relContent = `${
|
|
513
|
+
const relContent = `${sanitizedRelation.from} ${sanitizedRelation.type} ${sanitizedRelation.to}`;
|
|
407
514
|
const relVector = await embed(relContent);
|
|
408
515
|
const relPayload = {
|
|
409
516
|
content: relContent,
|
|
410
|
-
category,
|
|
411
|
-
domain,
|
|
517
|
+
category: normalizedCategory,
|
|
518
|
+
domain: normalizedDomain,
|
|
412
519
|
kind: "relation",
|
|
413
|
-
|
|
520
|
+
layer: "memory_object",
|
|
521
|
+
entities: [sanitizedRelation.from.toLowerCase(), sanitizedRelation.to.toLowerCase()],
|
|
414
522
|
source,
|
|
415
523
|
confidence,
|
|
416
524
|
content_hash: contentHash("relation", relContent),
|
|
@@ -420,18 +528,23 @@ export function registerTools(mcp) {
|
|
|
420
528
|
superseded_at: null,
|
|
421
529
|
created_at: now,
|
|
422
530
|
updated_at: now,
|
|
423
|
-
from_entity:
|
|
424
|
-
relation_type:
|
|
425
|
-
to_entity:
|
|
531
|
+
from_entity: sanitizedRelation.from.toLowerCase(),
|
|
532
|
+
relation_type: sanitizedRelation.type.toLowerCase(),
|
|
533
|
+
to_entity: sanitizedRelation.to.toLowerCase(),
|
|
426
534
|
};
|
|
535
|
+
addWorkspacePayload(relPayload, scope);
|
|
536
|
+
addRedactionPayload(relPayload, redactionSummary);
|
|
427
537
|
await qdrantUpsert(relationId, relVector, relPayload);
|
|
428
538
|
}
|
|
429
539
|
const result = {
|
|
430
540
|
action: "inserted",
|
|
431
541
|
fact_id: factId,
|
|
542
|
+
workspace_id: scope.workspaceId,
|
|
432
543
|
};
|
|
433
544
|
if (relationId)
|
|
434
545
|
result["relation_id"] = relationId;
|
|
546
|
+
if (redactionSummary.redacted)
|
|
547
|
+
result["redaction"] = redactionSummary;
|
|
435
548
|
if (similarFacts.length > 0)
|
|
436
549
|
result["similar_facts"] = similarFacts;
|
|
437
550
|
if (potentialConflicts.length > 0) {
|
|
@@ -447,22 +560,63 @@ export function registerTools(mcp) {
|
|
|
447
560
|
"Use on session start with a broad query for context briefing.", {
|
|
448
561
|
query: z.string().describe("What to search for (natural language)"),
|
|
449
562
|
category: z.string().optional().describe("Filter by category"),
|
|
450
|
-
domain: z.string().optional().describe("Filter by domain
|
|
563
|
+
domain: z.string().optional().describe("Filter by domain activity profile"),
|
|
451
564
|
kind: z.string().optional().describe("Filter by kind (fact, summary, distilled, relation)"),
|
|
565
|
+
memory_subtype: z.string().optional().describe("Filter by memory subtype"),
|
|
566
|
+
workspace_id: z.string().optional().describe("Filter by optional workspace namespace."),
|
|
567
|
+
include_legacy_workspace: z.boolean().optional()
|
|
568
|
+
.describe("Include legacy facts without workspace_id in this workspace query."),
|
|
452
569
|
entity: z.string().optional().describe("Filter by entity name"),
|
|
570
|
+
episode_id: z.string().optional().describe("Filter by coherent episode ID"),
|
|
571
|
+
workstream_key: z.string().optional().describe("Filter by durable workstream key"),
|
|
572
|
+
task_key: z.string().optional().describe("Filter by task or issue key"),
|
|
573
|
+
repo: z.string().optional().describe("Filter by repository or project surface"),
|
|
574
|
+
branch: z.string().optional().describe("Filter by branch or working surface"),
|
|
575
|
+
review_status: z.string().optional().describe("Filter by review lifecycle status"),
|
|
453
576
|
since: z.string().optional().describe("Only facts created after this ISO date"),
|
|
454
577
|
until: z.string().optional().describe("Only facts created before this ISO date"),
|
|
455
578
|
limit: z.number().optional().default(10).describe("Max results (default 10)"),
|
|
456
579
|
graph_depth: z.number().optional().default(0).describe("Entity graph traversal depth (0=none, 1=include 1-hop related entity facts)."),
|
|
457
580
|
metadata_filter: z.record(z.string(), z.string()).optional()
|
|
458
581
|
.describe("Filter by metadata key-value pairs. All pairs must match."),
|
|
459
|
-
}, async ({ query, category, domain, kind, entity, since, until, limit, graph_depth, metadata_filter }) => {
|
|
582
|
+
}, 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
583
|
const guard = requireReady();
|
|
461
584
|
if (guard)
|
|
462
585
|
return guard;
|
|
463
586
|
const requestedLimit = limit ?? 10;
|
|
464
|
-
const
|
|
465
|
-
const
|
|
587
|
+
const scope = resolveScope(workspace_id, include_legacy_workspace);
|
|
588
|
+
const redactedQuery = redactStorageText(query);
|
|
589
|
+
const vector = await embed(redactedQuery.text);
|
|
590
|
+
const normalizedKind = kind ? normalizeKind(kind) : undefined;
|
|
591
|
+
let normalizedSubtype;
|
|
592
|
+
if (memory_subtype) {
|
|
593
|
+
try {
|
|
594
|
+
normalizedSubtype = validateMemorySubtype(normalizedKind, memory_subtype) ?? undefined;
|
|
595
|
+
}
|
|
596
|
+
catch (e) {
|
|
597
|
+
return {
|
|
598
|
+
content: [{ type: "text", text: e instanceof Error ? e.message : String(e) }],
|
|
599
|
+
isError: true,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const filter = scopedFilter(scope, {
|
|
604
|
+
category: category ? normalizeCategory(category) : undefined,
|
|
605
|
+
domain: domain ? normalizeDomain(domain) : undefined,
|
|
606
|
+
kind: normalizedKind,
|
|
607
|
+
memory_subtype: normalizedSubtype,
|
|
608
|
+
entity,
|
|
609
|
+
episode_id,
|
|
610
|
+
workstream_key,
|
|
611
|
+
task_key,
|
|
612
|
+
repo,
|
|
613
|
+
branch,
|
|
614
|
+
review_status,
|
|
615
|
+
since,
|
|
616
|
+
until,
|
|
617
|
+
metadata: metadata_filter,
|
|
618
|
+
excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS,
|
|
619
|
+
});
|
|
466
620
|
const results = await qdrantSearch(vector, filter, requestedLimit * 2);
|
|
467
621
|
if (!results.result?.length) {
|
|
468
622
|
const nudge = buildMemoryNudge();
|
|
@@ -475,7 +629,7 @@ export function registerTools(mcp) {
|
|
|
475
629
|
.slice(0, requestedLimit);
|
|
476
630
|
const lines = ranked.map((r) => formatFact(r));
|
|
477
631
|
if ((graph_depth ?? 0) >= 1) {
|
|
478
|
-
const relatedLines = await graphTraversal(ranked, requestedLimit);
|
|
632
|
+
const relatedLines = await graphTraversal(ranked, requestedLimit, scope);
|
|
479
633
|
if (relatedLines.length > 0) {
|
|
480
634
|
lines.push("", "── Related (1-hop) ──");
|
|
481
635
|
lines.push(...relatedLines);
|
|
@@ -490,25 +644,24 @@ export function registerTools(mcp) {
|
|
|
490
644
|
mcp.tool("memory_entity", "Get everything known about an entity — all facts mentioning it plus its relationships.", {
|
|
491
645
|
name: z.string().describe("Entity name (e.g. 'qdrant', 'platform')"),
|
|
492
646
|
limit: z.number().optional().default(20).describe("Max facts to return"),
|
|
493
|
-
|
|
647
|
+
workspace_id: z.string().optional().describe("Filter by optional workspace namespace."),
|
|
648
|
+
include_legacy_workspace: z.boolean().optional()
|
|
649
|
+
.describe("Include legacy facts without workspace_id in this workspace query."),
|
|
650
|
+
}, async ({ name, limit, workspace_id, include_legacy_workspace }) => {
|
|
494
651
|
const guard = requireReady();
|
|
495
652
|
if (guard)
|
|
496
653
|
return guard;
|
|
497
654
|
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);
|
|
655
|
+
const scope = resolveScope(workspace_id, include_legacy_workspace);
|
|
656
|
+
const factsFilter = scopedFilter(scope) ?? { must: [] };
|
|
657
|
+
factsFilter.must.push({ key: "entities", match: { value: entityName } });
|
|
658
|
+
const facts = await qdrantScroll(factsFilter, limit ?? 20);
|
|
659
|
+
const fromFilter = scopedFilter(scope) ?? { must: [] };
|
|
660
|
+
fromFilter.must.push({ key: "from_entity", match: { value: entityName } });
|
|
661
|
+
const relationsFrom = await qdrantScroll(fromFilter, 50);
|
|
662
|
+
const toFilter = scopedFilter(scope) ?? { must: [] };
|
|
663
|
+
toFilter.must.push({ key: "to_entity", match: { value: entityName } });
|
|
664
|
+
const relationsTo = await qdrantScroll(toFilter, 50);
|
|
512
665
|
const output = [];
|
|
513
666
|
const factPoints = facts.result?.points ?? [];
|
|
514
667
|
if (factPoints.length > 0) {
|
|
@@ -548,17 +701,19 @@ export function registerTools(mcp) {
|
|
|
548
701
|
relation_type: z.string().optional().describe("Filter by relation type (e.g. 'owns', 'uses', 'decided')"),
|
|
549
702
|
direction: z.enum(["from", "to", "both"]).optional().default("both")
|
|
550
703
|
.describe("Direction: 'from' (entity as source), 'to' (entity as target), 'both'"),
|
|
551
|
-
|
|
704
|
+
workspace_id: z.string().optional().describe("Filter by optional workspace namespace."),
|
|
705
|
+
include_legacy_workspace: z.boolean().optional()
|
|
706
|
+
.describe("Include legacy facts without workspace_id in this workspace query."),
|
|
707
|
+
}, async ({ entity, relation_type, direction, workspace_id, include_legacy_workspace }) => {
|
|
552
708
|
const guard = requireReady();
|
|
553
709
|
if (guard)
|
|
554
710
|
return guard;
|
|
555
711
|
const entityName = entity.toLowerCase();
|
|
712
|
+
const scope = resolveScope(workspace_id, include_legacy_workspace);
|
|
556
713
|
const results = [];
|
|
557
714
|
if (direction === "from" || direction === "both") {
|
|
558
|
-
const filter = { must: [
|
|
559
|
-
|
|
560
|
-
{ is_null: { key: "superseded_by" } },
|
|
561
|
-
] };
|
|
715
|
+
const filter = scopedFilter(scope) ?? { must: [] };
|
|
716
|
+
filter.must.push({ key: "from_entity", match: { value: entityName } });
|
|
562
717
|
if (relation_type) {
|
|
563
718
|
filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
|
|
564
719
|
}
|
|
@@ -566,10 +721,8 @@ export function registerTools(mcp) {
|
|
|
566
721
|
results.push(...(r.result?.points ?? []));
|
|
567
722
|
}
|
|
568
723
|
if (direction === "to" || direction === "both") {
|
|
569
|
-
const filter = { must: [
|
|
570
|
-
|
|
571
|
-
{ is_null: { key: "superseded_by" } },
|
|
572
|
-
] };
|
|
724
|
+
const filter = scopedFilter(scope) ?? { must: [] };
|
|
725
|
+
filter.must.push({ key: "to_entity", match: { value: entityName } });
|
|
573
726
|
if (relation_type) {
|
|
574
727
|
filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
|
|
575
728
|
}
|
|
@@ -596,18 +749,30 @@ export function registerTools(mcp) {
|
|
|
596
749
|
mcp.tool("memory_forget", "Mark a fact as superseded/wrong. The fact remains but is excluded from recall results.", {
|
|
597
750
|
fact_id: z.string().describe("ID of the fact to forget"),
|
|
598
751
|
reason: z.string().describe("Why this fact is being superseded"),
|
|
599
|
-
|
|
752
|
+
workspace_id: z.string().optional().describe("Optional workspace namespace."),
|
|
753
|
+
}, async ({ fact_id, reason, workspace_id }) => {
|
|
600
754
|
const guard = requireReady();
|
|
601
755
|
if (guard)
|
|
602
756
|
return guard;
|
|
603
757
|
const now = nowISO();
|
|
604
758
|
try {
|
|
759
|
+
const scope = resolveScope(workspace_id);
|
|
760
|
+
const existing = await getPointForWorkspaceWrite(fact_id, scope);
|
|
761
|
+
if (existing.error) {
|
|
762
|
+
return { content: [{ type: "text", text: JSON.stringify(existing.error, null, 2) }], isError: true };
|
|
763
|
+
}
|
|
764
|
+
const redactedReason = redactStorageText(reason);
|
|
605
765
|
await qdrantSetPayload([fact_id], {
|
|
606
|
-
superseded_by: `forgotten:${
|
|
766
|
+
superseded_by: `forgotten:${redactedReason.text}`,
|
|
607
767
|
superseded_at: now,
|
|
608
768
|
updated_at: now,
|
|
609
769
|
});
|
|
610
|
-
return { content: [{ type: "text", text: JSON.stringify({
|
|
770
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
771
|
+
status: "forgotten",
|
|
772
|
+
fact_id,
|
|
773
|
+
reason: redactedReason.text,
|
|
774
|
+
...(redactedReason.redacted ? { redaction: redactedReason } : {}),
|
|
775
|
+
}) }] };
|
|
611
776
|
}
|
|
612
777
|
catch (e) {
|
|
613
778
|
return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
|
|
@@ -616,15 +781,20 @@ export function registerTools(mcp) {
|
|
|
616
781
|
// ── memory_verify ───────────────────────────────────────────────────────
|
|
617
782
|
mcp.tool("memory_verify", "Confirm a fact is still accurate. Resets the staleness clock and bumps verification count.", {
|
|
618
783
|
fact_id: z.string().describe("ID of the fact to verify"),
|
|
619
|
-
|
|
784
|
+
workspace_id: z.string().optional().describe("Optional workspace namespace."),
|
|
785
|
+
}, async ({ fact_id, workspace_id }) => {
|
|
620
786
|
const guard = requireReady();
|
|
621
787
|
if (guard)
|
|
622
788
|
return guard;
|
|
623
789
|
const now = nowISO();
|
|
624
790
|
try {
|
|
625
|
-
const
|
|
791
|
+
const scope = resolveScope(workspace_id);
|
|
792
|
+
const writable = await getPointForWorkspaceWrite(fact_id, scope);
|
|
793
|
+
if (writable.error) {
|
|
794
|
+
return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
|
|
795
|
+
}
|
|
626
796
|
let currentCount = 0;
|
|
627
|
-
const existingPt =
|
|
797
|
+
const existingPt = writable.point;
|
|
628
798
|
if (existingPt) {
|
|
629
799
|
currentCount = existingPt.payload.verification_count ?? 0;
|
|
630
800
|
}
|
|
@@ -656,17 +826,18 @@ export function registerTools(mcp) {
|
|
|
656
826
|
fact_id: z.string().optional().describe("Fact ID (required for approve/reject/correct)"),
|
|
657
827
|
reason: z.string().optional().describe("Reason for rejection"),
|
|
658
828
|
corrected_content: z.string().optional().describe("Corrected fact text (for correct action)"),
|
|
659
|
-
|
|
829
|
+
workspace_id: z.string().optional().describe("Filter by optional workspace namespace."),
|
|
830
|
+
include_legacy_workspace: z.boolean().optional()
|
|
831
|
+
.describe("Include legacy facts without workspace_id in this workspace query."),
|
|
832
|
+
}, async ({ limit, action, fact_id, reason, corrected_content, workspace_id, include_legacy_workspace }) => {
|
|
660
833
|
const guard = requireReady();
|
|
661
834
|
if (guard)
|
|
662
835
|
return guard;
|
|
836
|
+
const scope = resolveScope(workspace_id, include_legacy_workspace);
|
|
663
837
|
if (action === "list") {
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
{ is_null: { key: "superseded_by" } },
|
|
668
|
-
],
|
|
669
|
-
}, (limit ?? 10) * 2);
|
|
838
|
+
const filter = scopedFilter(scope) ?? { must: [] };
|
|
839
|
+
filter.must.push({ key: "source", match: { value: "daemon" } });
|
|
840
|
+
const result = await qdrantScroll(filter, (limit ?? 10) * 2);
|
|
670
841
|
const points = (result.result?.points ?? [])
|
|
671
842
|
.sort((a, b) => (b.payload.created_at ?? "").localeCompare(a.payload.created_at ?? ""))
|
|
672
843
|
.slice(0, limit ?? 10);
|
|
@@ -684,9 +855,12 @@ export function registerTools(mcp) {
|
|
|
684
855
|
}
|
|
685
856
|
const now = nowISO();
|
|
686
857
|
if (action === "approve") {
|
|
687
|
-
const
|
|
858
|
+
const writable = await getPointForWorkspaceWrite(fact_id, scope);
|
|
859
|
+
if (writable.error) {
|
|
860
|
+
return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
|
|
861
|
+
}
|
|
688
862
|
let currentCount = 0;
|
|
689
|
-
const approvePt =
|
|
863
|
+
const approvePt = writable.point;
|
|
690
864
|
if (approvePt) {
|
|
691
865
|
currentCount = approvePt.payload.verification_count ?? 0;
|
|
692
866
|
}
|
|
@@ -701,28 +875,52 @@ export function registerTools(mcp) {
|
|
|
701
875
|
if (!reason) {
|
|
702
876
|
return { content: [{ type: "text", text: "Error: reason is required for reject action." }] };
|
|
703
877
|
}
|
|
878
|
+
const writable = await getPointForWorkspaceWrite(fact_id, scope);
|
|
879
|
+
if (writable.error) {
|
|
880
|
+
return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
|
|
881
|
+
}
|
|
882
|
+
const redactedReason = redactStorageText(reason);
|
|
704
883
|
await qdrantSetPayload([fact_id], {
|
|
705
|
-
superseded_by: `rejected:${
|
|
884
|
+
superseded_by: `rejected:${redactedReason.text}`,
|
|
706
885
|
superseded_at: now,
|
|
707
886
|
updated_at: now,
|
|
708
887
|
});
|
|
709
|
-
return { content: [{ type: "text", text: JSON.stringify({
|
|
888
|
+
return { content: [{ type: "text", text: JSON.stringify({
|
|
889
|
+
status: "rejected",
|
|
890
|
+
fact_id,
|
|
891
|
+
reason: redactedReason.text,
|
|
892
|
+
...(redactedReason.redacted ? { redaction: redactedReason } : {}),
|
|
893
|
+
}) }] };
|
|
710
894
|
}
|
|
711
895
|
if (action === "correct") {
|
|
712
896
|
if (!corrected_content) {
|
|
713
897
|
return { content: [{ type: "text", text: "Error: corrected_content is required for correct action." }] };
|
|
714
898
|
}
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
899
|
+
const writable = await getPointForWorkspaceWrite(fact_id, scope);
|
|
900
|
+
if (writable.error) {
|
|
901
|
+
return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
|
|
902
|
+
}
|
|
903
|
+
const origPayload = writable.point?.payload;
|
|
904
|
+
const redactedCorrected = redactStorageText(corrected_content);
|
|
905
|
+
const correctionScope = origPayload?.workspace_id
|
|
906
|
+
? resolveScope(origPayload.workspace_id, false)
|
|
907
|
+
: scope;
|
|
908
|
+
const vector = await embed(redactedCorrected.text);
|
|
718
909
|
const correctedId = crypto.randomUUID();
|
|
719
|
-
const origCategory = origPayload?.category ??
|
|
720
|
-
const hash = contentHash(origCategory,
|
|
721
|
-
|
|
722
|
-
content:
|
|
910
|
+
const origCategory = normalizeCategory(origPayload?.category ?? DEFAULT_CATEGORY);
|
|
911
|
+
const hash = contentHash(origCategory, redactedCorrected.text);
|
|
912
|
+
const correctedPayload = {
|
|
913
|
+
content: redactedCorrected.text,
|
|
723
914
|
category: origCategory,
|
|
724
|
-
domain: origPayload?.domain ??
|
|
725
|
-
kind: origPayload?.kind ?? "fact",
|
|
915
|
+
domain: normalizeDomain(origPayload?.domain ?? DEFAULT_DOMAIN),
|
|
916
|
+
kind: normalizeKind(origPayload?.kind ?? "fact"),
|
|
917
|
+
...(origPayload?.memory_subtype ? { memory_subtype: origPayload.memory_subtype } : {}),
|
|
918
|
+
...(origPayload?.layer ? { layer: origPayload.layer } : {}),
|
|
919
|
+
...(origPayload?.episode_id ? { episode_id: origPayload.episode_id } : {}),
|
|
920
|
+
...(origPayload?.workstream_key ? { workstream_key: origPayload.workstream_key } : {}),
|
|
921
|
+
...(origPayload?.task_key ? { task_key: origPayload.task_key } : {}),
|
|
922
|
+
...(origPayload?.repo ? { repo: origPayload.repo } : {}),
|
|
923
|
+
...(origPayload?.branch ? { branch: origPayload.branch } : {}),
|
|
726
924
|
entities: origPayload?.entities ?? [],
|
|
727
925
|
source: "user",
|
|
728
926
|
confidence: 0.95,
|
|
@@ -735,7 +933,10 @@ export function registerTools(mcp) {
|
|
|
735
933
|
created_at: now,
|
|
736
934
|
updated_at: now,
|
|
737
935
|
metadata: { ...(origPayload?.metadata ?? {}), corrected_from: fact_id },
|
|
738
|
-
}
|
|
936
|
+
};
|
|
937
|
+
addWorkspacePayload(correctedPayload, correctionScope);
|
|
938
|
+
addRedactionPayload(correctedPayload, redactedCorrected);
|
|
939
|
+
await qdrantUpsert(correctedId, vector, correctedPayload);
|
|
739
940
|
await qdrantSetPayload([fact_id], {
|
|
740
941
|
superseded_by: correctedId,
|
|
741
942
|
superseded_at: now,
|
|
@@ -745,175 +946,6 @@ export function registerTools(mcp) {
|
|
|
745
946
|
}
|
|
746
947
|
return { content: [{ type: "text", text: `Unknown action: ${String(action)}` }] };
|
|
747
948
|
});
|
|
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
949
|
// ── memory_heartbeat ────────────────────────────────────────────────────
|
|
918
950
|
mcp.tool("memory_heartbeat", "Lightweight reflection check. Returns memory nudge (if no stores in 10+ min), staleness alerts (every 3rd call), and reflection prompt.", {}, async () => {
|
|
919
951
|
heartbeatCount++;
|
|
@@ -924,17 +956,17 @@ export function registerTools(mcp) {
|
|
|
924
956
|
if (heartbeatCount % 3 === 0 && ready) {
|
|
925
957
|
try {
|
|
926
958
|
const staleThreshold = new Date(Date.now() - STALENESS_DAYS * 86400000).toISOString();
|
|
927
|
-
const
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
959
|
+
const scope = resolveScope();
|
|
960
|
+
const staleFilter = scopedFilter(scope) ?? { must: [] };
|
|
961
|
+
staleFilter.must.push({ key: "category", match: { any: ["infrastructure", "projects", "decisions"] } });
|
|
962
|
+
staleFilter.should = [
|
|
963
|
+
{ key: "last_reinforced_at", range: { lte: staleThreshold } },
|
|
964
|
+
{ is_null: { key: "last_reinforced_at" } },
|
|
965
|
+
];
|
|
966
|
+
staleFilter.must_not = [
|
|
967
|
+
{ key: "last_verified_at", range: { gte: staleThreshold } },
|
|
968
|
+
];
|
|
969
|
+
const staleResults = await qdrantScroll(staleFilter, 3);
|
|
938
970
|
const staleFacts = staleResults.result?.points ?? [];
|
|
939
971
|
if (staleFacts.length > 0) {
|
|
940
972
|
const staleLines = staleFacts.map((f) => {
|
|
@@ -950,8 +982,12 @@ export function registerTools(mcp) {
|
|
|
950
982
|
log("WARN", `Staleness check failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
951
983
|
}
|
|
952
984
|
}
|
|
953
|
-
sections.push("🔍
|
|
954
|
-
"
|
|
985
|
+
sections.push("🔍 Reflect: think about the LAST 10 minutes of work and answer in your head:\n" +
|
|
986
|
+
" 1. Did you touch a service, port, config, or file path you hadn't seen before?\n" +
|
|
987
|
+
" 2. Did you make a choice (library, pattern, approach) you'd want a future session to know about?\n" +
|
|
988
|
+
" 3. Did you hit an error and find a workaround?\n" +
|
|
989
|
+
" 4. Did the user state a preference or constraint?\n" +
|
|
990
|
+
"If any answer is yes, call memory_store now — one atomic fact per item, with category/domain/entities.");
|
|
955
991
|
return { content: [{ type: "text", text: sections.join("\n\n") }] };
|
|
956
992
|
});
|
|
957
993
|
}
|