bikky 0.3.13 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/CODE_OF_CONDUCT.md +80 -0
- package/CONTRIBUTING.md +206 -0
- package/README.md +94 -20
- package/SECURITY.md +58 -0
- package/SUPPORT.md +22 -0
- package/dist/config.d.ts +68 -1
- package/dist/config.js +197 -4
- package/dist/daemon/extraction.d.ts +12 -2
- package/dist/daemon/extraction.js +85 -133
- package/dist/daemon/loop.js +15 -1
- package/dist/daemon/qdrant.js +0 -1
- package/dist/daemon/transcript-sources.d.ts +26 -0
- package/dist/daemon/transcript-sources.js +193 -0
- package/dist/daemon/watcher.d.ts +3 -2
- package/dist/daemon/watcher.js +51 -2
- package/dist/install.d.ts +9 -1
- package/dist/install.js +62 -34
- package/dist/lib/qdrant-pool.d.ts +57 -0
- package/dist/lib/qdrant-pool.js +104 -0
- package/dist/mcp/api.d.ts +57 -19
- package/dist/mcp/api.js +134 -72
- package/dist/mcp/helpers.d.ts +0 -1
- package/dist/mcp/helpers.js +2 -15
- package/dist/mcp/index.js +29 -14
- package/dist/mcp/tools.d.ts +0 -7
- package/dist/mcp/tools.js +618 -276
- package/dist/mcp/types.d.ts +0 -3
- package/dist/routing.d.ts +53 -0
- package/dist/routing.js +129 -0
- package/dist/search-scope.d.ts +24 -0
- package/dist/search-scope.js +174 -0
- package/docs/config/fully-hosted.md +57 -0
- package/docs/config/hosted-models.md +50 -0
- package/docs/config/hosted-qdrant-local-models.md +39 -0
- package/docs/config/local.md +34 -0
- package/docs/configuration.md +403 -0
- package/docs/privacy-first.md +140 -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 +28 -7
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/config.test.d.ts +0 -9
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -576
- package/dist/config.test.js.map +0 -1
- package/dist/daemon/capture-policy.d.ts.map +0 -1
- package/dist/daemon/capture-policy.js.map +0 -1
- package/dist/daemon/capture-policy.test.d.ts +0 -2
- package/dist/daemon/capture-policy.test.d.ts.map +0 -1
- package/dist/daemon/capture-policy.test.js +0 -48
- package/dist/daemon/capture-policy.test.js.map +0 -1
- package/dist/daemon/consolidation.d.ts.map +0 -1
- package/dist/daemon/consolidation.js.map +0 -1
- package/dist/daemon/entity-typing.d.ts.map +0 -1
- package/dist/daemon/entity-typing.js.map +0 -1
- package/dist/daemon/entity-typing.test.d.ts +0 -2
- package/dist/daemon/entity-typing.test.d.ts.map +0 -1
- package/dist/daemon/entity-typing.test.js +0 -50
- package/dist/daemon/entity-typing.test.js.map +0 -1
- package/dist/daemon/episode-summary.d.ts.map +0 -1
- package/dist/daemon/episode-summary.js.map +0 -1
- package/dist/daemon/episode-summary.test.d.ts +0 -2
- package/dist/daemon/episode-summary.test.d.ts.map +0 -1
- package/dist/daemon/episode-summary.test.js +0 -104
- package/dist/daemon/episode-summary.test.js.map +0 -1
- package/dist/daemon/extraction-quality.test.d.ts +0 -2
- package/dist/daemon/extraction-quality.test.d.ts.map +0 -1
- package/dist/daemon/extraction-quality.test.js +0 -283
- package/dist/daemon/extraction-quality.test.js.map +0 -1
- package/dist/daemon/extraction-rules.d.ts.map +0 -1
- package/dist/daemon/extraction-rules.js.map +0 -1
- package/dist/daemon/extraction-rules.test.d.ts +0 -2
- package/dist/daemon/extraction-rules.test.d.ts.map +0 -1
- package/dist/daemon/extraction-rules.test.js +0 -203
- package/dist/daemon/extraction-rules.test.js.map +0 -1
- package/dist/daemon/extraction.d.ts.map +0 -1
- package/dist/daemon/extraction.js.map +0 -1
- package/dist/daemon/extraction.test.d.ts +0 -2
- package/dist/daemon/extraction.test.d.ts.map +0 -1
- package/dist/daemon/extraction.test.js +0 -225
- package/dist/daemon/extraction.test.js.map +0 -1
- package/dist/daemon/index.d.ts.map +0 -1
- package/dist/daemon/index.js.map +0 -1
- package/dist/daemon/loop.d.ts.map +0 -1
- package/dist/daemon/loop.js.map +0 -1
- package/dist/daemon/loop.test.d.ts +0 -2
- package/dist/daemon/loop.test.d.ts.map +0 -1
- package/dist/daemon/loop.test.js +0 -85
- package/dist/daemon/loop.test.js.map +0 -1
- package/dist/daemon/maintenance-state.d.ts.map +0 -1
- package/dist/daemon/maintenance-state.js.map +0 -1
- package/dist/daemon/maintenance-state.test.d.ts +0 -2
- package/dist/daemon/maintenance-state.test.d.ts.map +0 -1
- package/dist/daemon/maintenance-state.test.js +0 -56
- package/dist/daemon/maintenance-state.test.js.map +0 -1
- package/dist/daemon/qdrant.d.ts.map +0 -1
- package/dist/daemon/qdrant.js.map +0 -1
- package/dist/daemon/qdrant.test.d.ts +0 -8
- package/dist/daemon/qdrant.test.d.ts.map +0 -1
- package/dist/daemon/qdrant.test.js +0 -265
- package/dist/daemon/qdrant.test.js.map +0 -1
- package/dist/daemon/relations-vocab.d.ts.map +0 -1
- package/dist/daemon/relations-vocab.js.map +0 -1
- package/dist/daemon/relations-vocab.test.d.ts +0 -2
- package/dist/daemon/relations-vocab.test.d.ts.map +0 -1
- package/dist/daemon/relations-vocab.test.js +0 -69
- package/dist/daemon/relations-vocab.test.js.map +0 -1
- package/dist/daemon/relations.d.ts.map +0 -1
- package/dist/daemon/relations.js.map +0 -1
- package/dist/daemon/relations.test.d.ts +0 -2
- package/dist/daemon/relations.test.d.ts.map +0 -1
- package/dist/daemon/relations.test.js +0 -36
- package/dist/daemon/relations.test.js.map +0 -1
- package/dist/daemon/session-index.d.ts.map +0 -1
- package/dist/daemon/session-index.js.map +0 -1
- package/dist/daemon/session-index.test.d.ts +0 -2
- package/dist/daemon/session-index.test.d.ts.map +0 -1
- package/dist/daemon/session-index.test.js +0 -60
- package/dist/daemon/session-index.test.js.map +0 -1
- package/dist/daemon/session-summary.d.ts.map +0 -1
- package/dist/daemon/session-summary.js.map +0 -1
- package/dist/daemon/session-summary.test.d.ts +0 -2
- package/dist/daemon/session-summary.test.d.ts.map +0 -1
- package/dist/daemon/session-summary.test.js +0 -162
- package/dist/daemon/session-summary.test.js.map +0 -1
- package/dist/daemon/staleness.d.ts.map +0 -1
- package/dist/daemon/staleness.js.map +0 -1
- package/dist/daemon/staleness.test.d.ts +0 -7
- package/dist/daemon/staleness.test.d.ts.map +0 -1
- package/dist/daemon/staleness.test.js +0 -128
- package/dist/daemon/staleness.test.js.map +0 -1
- package/dist/daemon/watcher-health.d.ts.map +0 -1
- package/dist/daemon/watcher-health.js.map +0 -1
- package/dist/daemon/watcher-health.test.d.ts +0 -5
- package/dist/daemon/watcher-health.test.d.ts.map +0 -1
- package/dist/daemon/watcher-health.test.js +0 -119
- package/dist/daemon/watcher-health.test.js.map +0 -1
- package/dist/daemon/watcher.d.ts.map +0 -1
- package/dist/daemon/watcher.js.map +0 -1
- package/dist/daemon/watcher.test.d.ts +0 -9
- package/dist/daemon/watcher.test.d.ts.map +0 -1
- package/dist/daemon/watcher.test.js +0 -204
- package/dist/daemon/watcher.test.js.map +0 -1
- package/dist/daemon/workstream-resolver.d.ts.map +0 -1
- package/dist/daemon/workstream-resolver.js.map +0 -1
- package/dist/daemon/workstream-resolver.test.d.ts +0 -2
- package/dist/daemon/workstream-resolver.test.d.ts.map +0 -1
- package/dist/daemon/workstream-resolver.test.js +0 -128
- package/dist/daemon/workstream-resolver.test.js.map +0 -1
- package/dist/daemon/workstream-summary.d.ts.map +0 -1
- package/dist/daemon/workstream-summary.js.map +0 -1
- package/dist/daemon/workstream-summary.test.d.ts +0 -2
- package/dist/daemon/workstream-summary.test.d.ts.map +0 -1
- package/dist/daemon/workstream-summary.test.js +0 -89
- package/dist/daemon/workstream-summary.test.js.map +0 -1
- package/dist/install.d.ts.map +0 -1
- package/dist/install.js.map +0 -1
- package/dist/install.test.d.ts +0 -9
- package/dist/install.test.d.ts.map +0 -1
- package/dist/install.test.js +0 -126
- package/dist/install.test.js.map +0 -1
- package/dist/lib/qdrant-client.d.ts.map +0 -1
- package/dist/lib/qdrant-client.js.map +0 -1
- package/dist/lib/qdrant-client.test.d.ts +0 -8
- package/dist/lib/qdrant-client.test.d.ts.map +0 -1
- package/dist/lib/qdrant-client.test.js +0 -274
- package/dist/lib/qdrant-client.test.js.map +0 -1
- package/dist/lifecycle.d.ts.map +0 -1
- package/dist/lifecycle.js.map +0 -1
- package/dist/lifecycle.test.d.ts +0 -8
- package/dist/lifecycle.test.d.ts.map +0 -1
- package/dist/lifecycle.test.js +0 -74
- package/dist/lifecycle.test.js.map +0 -1
- package/dist/llm/embedding/index.d.ts.map +0 -1
- package/dist/llm/embedding/index.js.map +0 -1
- package/dist/llm/embedding/index.test.d.ts +0 -8
- package/dist/llm/embedding/index.test.d.ts.map +0 -1
- package/dist/llm/embedding/index.test.js +0 -100
- package/dist/llm/embedding/index.test.js.map +0 -1
- package/dist/llm/embedding/providers/bedrock.d.ts.map +0 -1
- package/dist/llm/embedding/providers/bedrock.js.map +0 -1
- package/dist/llm/embedding/providers/bedrock.test.d.ts +0 -2
- package/dist/llm/embedding/providers/bedrock.test.d.ts.map +0 -1
- package/dist/llm/embedding/providers/bedrock.test.js +0 -24
- package/dist/llm/embedding/providers/bedrock.test.js.map +0 -1
- package/dist/llm/embedding/providers/index.d.ts.map +0 -1
- package/dist/llm/embedding/providers/index.js.map +0 -1
- package/dist/llm/embedding/providers/ollama.d.ts.map +0 -1
- package/dist/llm/embedding/providers/ollama.js.map +0 -1
- package/dist/llm/embedding/providers/ollama.test.d.ts +0 -2
- package/dist/llm/embedding/providers/ollama.test.d.ts.map +0 -1
- package/dist/llm/embedding/providers/ollama.test.js +0 -54
- package/dist/llm/embedding/providers/ollama.test.js.map +0 -1
- package/dist/llm/embedding/providers/openai.d.ts.map +0 -1
- package/dist/llm/embedding/providers/openai.js.map +0 -1
- package/dist/llm/embedding/providers/openai.test.d.ts +0 -2
- package/dist/llm/embedding/providers/openai.test.d.ts.map +0 -1
- package/dist/llm/embedding/providers/openai.test.js +0 -48
- package/dist/llm/embedding/providers/openai.test.js.map +0 -1
- package/dist/llm/embedding/providers/portkey.d.ts.map +0 -1
- package/dist/llm/embedding/providers/portkey.js.map +0 -1
- package/dist/llm/embedding/providers/portkey.test.d.ts +0 -2
- package/dist/llm/embedding/providers/portkey.test.d.ts.map +0 -1
- package/dist/llm/embedding/providers/portkey.test.js +0 -56
- package/dist/llm/embedding/providers/portkey.test.js.map +0 -1
- package/dist/llm/embedding/registry.d.ts.map +0 -1
- package/dist/llm/embedding/registry.js.map +0 -1
- package/dist/llm/embedding/registry.test.d.ts +0 -7
- package/dist/llm/embedding/registry.test.d.ts.map +0 -1
- package/dist/llm/embedding/registry.test.js +0 -68
- package/dist/llm/embedding/registry.test.js.map +0 -1
- package/dist/llm/embedding/types.d.ts.map +0 -1
- package/dist/llm/embedding/types.js.map +0 -1
- package/dist/llm/errors.d.ts.map +0 -1
- package/dist/llm/errors.js.map +0 -1
- package/dist/llm/errors.test.d.ts +0 -2
- package/dist/llm/errors.test.d.ts.map +0 -1
- package/dist/llm/errors.test.js +0 -103
- package/dist/llm/errors.test.js.map +0 -1
- package/dist/llm/fetch.d.ts.map +0 -1
- package/dist/llm/fetch.js.map +0 -1
- package/dist/llm/index.d.ts.map +0 -1
- package/dist/llm/index.js.map +0 -1
- package/dist/llm/inference/index.d.ts.map +0 -1
- package/dist/llm/inference/index.js.map +0 -1
- package/dist/llm/inference/index.test.d.ts +0 -6
- package/dist/llm/inference/index.test.d.ts.map +0 -1
- package/dist/llm/inference/index.test.js +0 -150
- package/dist/llm/inference/index.test.js.map +0 -1
- package/dist/llm/inference/providers/bedrock.d.ts.map +0 -1
- package/dist/llm/inference/providers/bedrock.js.map +0 -1
- package/dist/llm/inference/providers/bedrock.test.d.ts +0 -2
- package/dist/llm/inference/providers/bedrock.test.d.ts.map +0 -1
- package/dist/llm/inference/providers/bedrock.test.js +0 -68
- package/dist/llm/inference/providers/bedrock.test.js.map +0 -1
- package/dist/llm/inference/providers/index.d.ts.map +0 -1
- package/dist/llm/inference/providers/index.js.map +0 -1
- package/dist/llm/inference/providers/ollama.d.ts.map +0 -1
- package/dist/llm/inference/providers/ollama.js.map +0 -1
- package/dist/llm/inference/providers/ollama.test.d.ts +0 -2
- package/dist/llm/inference/providers/ollama.test.d.ts.map +0 -1
- package/dist/llm/inference/providers/ollama.test.js +0 -57
- package/dist/llm/inference/providers/ollama.test.js.map +0 -1
- package/dist/llm/inference/providers/openai.d.ts.map +0 -1
- package/dist/llm/inference/providers/openai.js.map +0 -1
- package/dist/llm/inference/providers/openai.test.d.ts +0 -2
- package/dist/llm/inference/providers/openai.test.d.ts.map +0 -1
- package/dist/llm/inference/providers/openai.test.js +0 -82
- package/dist/llm/inference/providers/openai.test.js.map +0 -1
- package/dist/llm/inference/providers/portkey.d.ts.map +0 -1
- package/dist/llm/inference/providers/portkey.js.map +0 -1
- package/dist/llm/inference/providers/portkey.test.d.ts +0 -2
- package/dist/llm/inference/providers/portkey.test.d.ts.map +0 -1
- package/dist/llm/inference/providers/portkey.test.js +0 -48
- package/dist/llm/inference/providers/portkey.test.js.map +0 -1
- package/dist/llm/inference/registry.d.ts.map +0 -1
- package/dist/llm/inference/registry.js.map +0 -1
- package/dist/llm/inference/registry.test.d.ts +0 -6
- package/dist/llm/inference/registry.test.d.ts.map +0 -1
- package/dist/llm/inference/registry.test.js +0 -63
- package/dist/llm/inference/registry.test.js.map +0 -1
- package/dist/llm/inference/types.d.ts.map +0 -1
- package/dist/llm/inference/types.js.map +0 -1
- package/dist/llm/telemetry.d.ts.map +0 -1
- package/dist/llm/telemetry.js.map +0 -1
- package/dist/llm/telemetry.test.d.ts +0 -5
- package/dist/llm/telemetry.test.d.ts.map +0 -1
- package/dist/llm/telemetry.test.js +0 -89
- package/dist/llm/telemetry.test.js.map +0 -1
- package/dist/llm/types.d.ts.map +0 -1
- package/dist/llm/types.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/logger.test.d.ts +0 -5
- package/dist/logger.test.d.ts.map +0 -1
- package/dist/logger.test.js +0 -103
- package/dist/logger.test.js.map +0 -1
- package/dist/mcp/api.d.ts.map +0 -1
- package/dist/mcp/api.js.map +0 -1
- package/dist/mcp/api.test.d.ts +0 -6
- package/dist/mcp/api.test.d.ts.map +0 -1
- package/dist/mcp/api.test.js +0 -130
- package/dist/mcp/api.test.js.map +0 -1
- package/dist/mcp/helpers.d.ts.map +0 -1
- package/dist/mcp/helpers.js.map +0 -1
- package/dist/mcp/helpers.test.d.ts +0 -5
- package/dist/mcp/helpers.test.d.ts.map +0 -1
- package/dist/mcp/helpers.test.js +0 -548
- package/dist/mcp/helpers.test.js.map +0 -1
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/index.js.map +0 -1
- package/dist/mcp/taxonomy.d.ts.map +0 -1
- package/dist/mcp/taxonomy.js.map +0 -1
- package/dist/mcp/taxonomy.test.d.ts +0 -5
- package/dist/mcp/taxonomy.test.d.ts.map +0 -1
- package/dist/mcp/taxonomy.test.js +0 -215
- package/dist/mcp/taxonomy.test.js.map +0 -1
- package/dist/mcp/tools.d.ts.map +0 -1
- package/dist/mcp/tools.integration.itest.d.ts +0 -23
- package/dist/mcp/tools.integration.itest.d.ts.map +0 -1
- package/dist/mcp/tools.integration.itest.js +0 -171
- package/dist/mcp/tools.integration.itest.js.map +0 -1
- package/dist/mcp/tools.js.map +0 -1
- package/dist/mcp/tools.test.d.ts +0 -16
- package/dist/mcp/tools.test.d.ts.map +0 -1
- package/dist/mcp/tools.test.js +0 -908
- package/dist/mcp/tools.test.js.map +0 -1
- package/dist/mcp/types.d.ts.map +0 -1
- package/dist/mcp/types.js.map +0 -1
- package/dist/postinstall.d.ts.map +0 -1
- package/dist/postinstall.js.map +0 -1
- package/dist/privacy/redaction.d.ts.map +0 -1
- package/dist/privacy/redaction.js.map +0 -1
- package/dist/privacy/redaction.test.d.ts +0 -2
- package/dist/privacy/redaction.test.d.ts.map +0 -1
- package/dist/privacy/redaction.test.js +0 -51
- package/dist/privacy/redaction.test.js.map +0 -1
- package/dist/prompts/brief.d.ts.map +0 -1
- package/dist/prompts/brief.js.map +0 -1
- package/dist/prompts/contradiction.d.ts.map +0 -1
- package/dist/prompts/contradiction.js.map +0 -1
- package/dist/prompts/distill.d.ts.map +0 -1
- package/dist/prompts/distill.js.map +0 -1
- package/dist/prompts/entity-typing.d.ts.map +0 -1
- package/dist/prompts/entity-typing.js.map +0 -1
- package/dist/prompts/episode-summary.d.ts.map +0 -1
- package/dist/prompts/episode-summary.js.map +0 -1
- package/dist/prompts/extraction.d.ts.map +0 -1
- package/dist/prompts/extraction.js.map +0 -1
- package/dist/prompts/index.d.ts.map +0 -1
- package/dist/prompts/index.js.map +0 -1
- package/dist/prompts/prompts.test.d.ts +0 -8
- package/dist/prompts/prompts.test.d.ts.map +0 -1
- package/dist/prompts/prompts.test.js +0 -140
- package/dist/prompts/prompts.test.js.map +0 -1
- package/dist/prompts/relations.d.ts.map +0 -1
- package/dist/prompts/relations.js.map +0 -1
- package/dist/prompts/workstream-summary.d.ts.map +0 -1
- package/dist/prompts/workstream-summary.js.map +0 -1
- package/dist/provenance/actor.d.ts.map +0 -1
- package/dist/provenance/actor.js.map +0 -1
- package/dist/provenance/actor.test.d.ts +0 -2
- package/dist/provenance/actor.test.d.ts.map +0 -1
- package/dist/provenance/actor.test.js +0 -49
- package/dist/provenance/actor.test.js.map +0 -1
- package/dist/render.d.ts.map +0 -1
- package/dist/render.js.map +0 -1
- package/dist/render.test.d.ts +0 -8
- package/dist/render.test.d.ts.map +0 -1
- package/dist/render.test.js +0 -244
- package/dist/render.test.js.map +0 -1
- package/dist/status.d.ts.map +0 -1
- package/dist/status.js.map +0 -1
- package/dist/status.test.d.ts +0 -5
- package/dist/status.test.d.ts.map +0 -1
- package/dist/status.test.js +0 -203
- package/dist/status.test.js.map +0 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -5,8 +5,10 @@ import crypto from "node:crypto";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
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
7
|
import { contentHash, daysSince, lastActivityDate, computeCombinedScore, buildFilter, formatFact, structuredFact, MEMORY_RECALL_EXCLUDED_KINDS, } from "./helpers.js";
|
|
8
|
-
import { ready,
|
|
8
|
+
import { ready, setupError, setReady, log, embed, getEmbeddingConfig, qdrantReq, ensureCollectionsAll, qdrantUpsert, qdrantSearch, qdrantScroll, qdrantSetPayload, qdrantGetPoints, rebuildPool, hasPool, listDestinations, resolveDest, findPointById, } from "./api.js";
|
|
9
|
+
import { DestinationNotFoundError } from "../routing.js";
|
|
9
10
|
import { saveConfig, loadConfig, EXTRACTION_HEALTH_PATH } from "../config.js";
|
|
11
|
+
import { availableSearchScopes, resolveSearchScope, SearchScopeNotFoundError, } from "../search-scope.js";
|
|
10
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
11
13
|
import { inspectWatcherPaths, formatIssue, repairSuspiciousWatcherPaths } from "../daemon/watcher-health.js";
|
|
12
14
|
import { normalizeActorId, resolveActorIdentity } from "../provenance/actor.js";
|
|
@@ -17,6 +19,7 @@ import { addRedactionPayload, combineRedactions, redactStorageText, } from "../p
|
|
|
17
19
|
const NUDGE_INTERVAL_MS = 10 * 60 * 1000;
|
|
18
20
|
const MEMORY_RECALL_DEFAULT_LIMIT = 10;
|
|
19
21
|
const MEMORY_RECALL_MAX_LIMIT = 50;
|
|
22
|
+
const searchScopeSchema = z.union([z.string(), z.array(z.string())]).optional();
|
|
20
23
|
let lastStoreTime = Date.now();
|
|
21
24
|
let heartbeatCount = 0;
|
|
22
25
|
// ---------------------------------------------------------------------------
|
|
@@ -28,32 +31,105 @@ function nowISO() {
|
|
|
28
31
|
function newId() {
|
|
29
32
|
return crypto.randomUUID();
|
|
30
33
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|| process.env.BIKKY_WORKSPACE?.trim()
|
|
34
|
-
|| loadConfig().default_workspace?.trim()
|
|
35
|
-
|| undefined;
|
|
36
|
-
// The literal "default" workspace also includes legacy facts that have no
|
|
37
|
-
// workspace_id payload (pre-migration data). Any other named workspace stays
|
|
38
|
-
// strict. An explicit includeLegacyWorkspace=true from the caller still wins.
|
|
39
|
-
const isDefault = resolved === "default";
|
|
34
|
+
// Build a RoutingInput from the standard memory-tool fields.
|
|
35
|
+
function routingInput(args) {
|
|
40
36
|
return {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
destination: args.destination,
|
|
38
|
+
cwd: process.cwd(),
|
|
39
|
+
content: args.content,
|
|
40
|
+
entities: args.entities,
|
|
41
|
+
metadata: args.metadata,
|
|
44
42
|
};
|
|
45
43
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
44
|
+
// Resolve a destination from a routing input, returning either the destination
|
|
45
|
+
// or an MCP error result if the override is invalid / no destinations exist.
|
|
46
|
+
function resolveDestOrError(input) {
|
|
47
|
+
try {
|
|
48
|
+
return { dest: resolveDest(input) };
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
52
|
+
if (e instanceof DestinationNotFoundError) {
|
|
53
|
+
return {
|
|
54
|
+
error: {
|
|
55
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
56
|
+
status: "destination_not_found",
|
|
57
|
+
message: msg,
|
|
58
|
+
available_destinations: listDestinations().map((d) => d.name),
|
|
59
|
+
}, null, 2) }],
|
|
60
|
+
isError: true,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
error: {
|
|
66
|
+
content: [{ type: "text", text: JSON.stringify({ status: "error", message: msg }, null, 2) }],
|
|
67
|
+
isError: true,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function withDestination(point, destination) {
|
|
73
|
+
return { ...point, _destination: destination };
|
|
74
|
+
}
|
|
75
|
+
function structuredScopedFact(point) {
|
|
76
|
+
return {
|
|
77
|
+
...structuredFact(point),
|
|
78
|
+
destination: point._destination.name,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function formatScopedFact(point, includeDestination) {
|
|
82
|
+
const formatted = formatFact(point);
|
|
83
|
+
return includeDestination ? `[${point._destination.name}] ${formatted}` : formatted;
|
|
84
|
+
}
|
|
85
|
+
function resolveSearchScopeOrError(args) {
|
|
86
|
+
if (args.destination && args.search_scope !== undefined) {
|
|
87
|
+
return {
|
|
88
|
+
error: {
|
|
89
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
90
|
+
status: "ambiguous_search_scope",
|
|
91
|
+
message: "Use either destination for a single destination override or search_scope for routed/all/list search, not both.",
|
|
92
|
+
}, null, 2) }],
|
|
93
|
+
isError: true,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (args.destination) {
|
|
98
|
+
const resolved = resolveDestOrError({ ...args.input, destination: args.destination });
|
|
99
|
+
if (resolved.error)
|
|
100
|
+
return { error: resolved.error };
|
|
101
|
+
return {
|
|
102
|
+
scope: {
|
|
103
|
+
name: resolved.dest.name,
|
|
104
|
+
description: resolved.dest.description ?? `Search only the '${resolved.dest.name}' destination.`,
|
|
105
|
+
requested: resolved.dest.name,
|
|
106
|
+
destinations: [resolved.dest],
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const cfg = loadConfig();
|
|
111
|
+
const dests = listDestinations();
|
|
112
|
+
try {
|
|
113
|
+
return { scope: resolveSearchScope(args.search_scope, cfg, dests, args.input) };
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
117
|
+
return {
|
|
118
|
+
error: {
|
|
119
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
120
|
+
status: e instanceof SearchScopeNotFoundError ? "search_scope_not_found" : "search_scope_error",
|
|
121
|
+
message: msg,
|
|
122
|
+
available_search_scopes: availableSearchScopes(cfg, dests),
|
|
123
|
+
}, null, 2) }],
|
|
124
|
+
isError: true,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
52
128
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const actorId = actor?.actor_id ??
|
|
129
|
+
// Add actor identity payload fields. Workspace was removed in v0.4.0 — physical
|
|
130
|
+
// separation now happens via routing destinations (see routing.ts).
|
|
131
|
+
function addActorPayload(payload, actor, actorIdOverride) {
|
|
132
|
+
const actorId = actor?.actor_id ?? normalizeActorId(actorIdOverride);
|
|
57
133
|
if (actorId)
|
|
58
134
|
payload["actor_id"] = actorId;
|
|
59
135
|
if (actor?.actor_label) {
|
|
@@ -66,19 +142,34 @@ function addWorkspacePayload(payload, scope, actor) {
|
|
|
66
142
|
payload["metadata"] = metadata;
|
|
67
143
|
}
|
|
68
144
|
}
|
|
69
|
-
async function
|
|
70
|
-
const existing = await qdrantGetPoints([factId]);
|
|
145
|
+
async function getPointForWrite(dest, factId) {
|
|
146
|
+
const existing = await qdrantGetPoints(dest, [factId]);
|
|
71
147
|
const point = existing.result?.[0];
|
|
72
148
|
if (!point) {
|
|
73
149
|
return { error: { status: "not_found", fact_id: factId } };
|
|
74
150
|
}
|
|
75
151
|
return { point };
|
|
76
152
|
}
|
|
153
|
+
// Locate which destination owns a fact ID (fan-out across pool). Used by
|
|
154
|
+
// ID-based ops where the caller doesn't know upfront which destination holds
|
|
155
|
+
// the point (memory_forget, memory_verify, memory_report_outcome, etc.).
|
|
156
|
+
async function locatePoint(factId) {
|
|
157
|
+
const found = await findPointById(factId);
|
|
158
|
+
if (!found)
|
|
159
|
+
return null;
|
|
160
|
+
return { dest: found.destination, point: found.point };
|
|
161
|
+
}
|
|
162
|
+
function notFoundResult(factId) {
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text", text: JSON.stringify({ status: "not_found", fact_id: factId }, null, 2) }],
|
|
165
|
+
isError: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
77
168
|
function requireReady() {
|
|
78
169
|
if (!ready) {
|
|
79
170
|
const missing = [];
|
|
80
|
-
if (!
|
|
81
|
-
missing.push("
|
|
171
|
+
if (!hasPool())
|
|
172
|
+
missing.push("destinations");
|
|
82
173
|
return {
|
|
83
174
|
content: [{
|
|
84
175
|
type: "text",
|
|
@@ -86,9 +177,6 @@ function requireReady() {
|
|
|
86
177
|
status: "setup_required",
|
|
87
178
|
ready: false,
|
|
88
179
|
missing,
|
|
89
|
-
// Surface the underlying init failure (embedding / Qdrant) when
|
|
90
|
-
// present so users see an actionable reason instead of a generic
|
|
91
|
-
// "setup required" message.
|
|
92
180
|
...(setupError ? { setup_error: setupError } : {}),
|
|
93
181
|
setup_instructions: "Memory is not configured. Run `bikky setup` or call configure_credentials:\n" +
|
|
94
182
|
"1. Go to cloud.qdrant.io → sign up (free tier: 1GB, no credit card)\n" +
|
|
@@ -126,7 +214,7 @@ function clampRecallLimit(limit) {
|
|
|
126
214
|
/**
|
|
127
215
|
* Entity-graph traversal for memory_recall.
|
|
128
216
|
*/
|
|
129
|
-
async function graphTraversal(primaryResults, limit
|
|
217
|
+
async function graphTraversal(dest, primaryResults, limit) {
|
|
130
218
|
try {
|
|
131
219
|
const primaryEntities = new Set();
|
|
132
220
|
const primaryIds = new Set();
|
|
@@ -140,16 +228,16 @@ async function graphTraversal(primaryResults, limit, scope) {
|
|
|
140
228
|
return { points: [] };
|
|
141
229
|
const relatedEntities = new Set();
|
|
142
230
|
for (const entity of primaryEntities) {
|
|
143
|
-
const outgoingFilter =
|
|
231
|
+
const outgoingFilter = buildFilter({ excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS }) ?? { must: [] };
|
|
144
232
|
outgoingFilter.must.push({ key: "from_entity", match: { value: entity } });
|
|
145
|
-
const outgoing = await qdrantScroll(outgoingFilter, 10).catch(() => ({ result: { points: [] } }));
|
|
233
|
+
const outgoing = await qdrantScroll(dest, outgoingFilter, 10).catch(() => ({ result: { points: [] } }));
|
|
146
234
|
for (const pt of (outgoing.result?.points ?? [])) {
|
|
147
235
|
if (pt.payload.to_entity)
|
|
148
236
|
relatedEntities.add(pt.payload.to_entity);
|
|
149
237
|
}
|
|
150
|
-
const incomingFilter =
|
|
238
|
+
const incomingFilter = buildFilter({ excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS }) ?? { must: [] };
|
|
151
239
|
incomingFilter.must.push({ key: "to_entity", match: { value: entity } });
|
|
152
|
-
const incoming = await qdrantScroll(incomingFilter, 10).catch(() => ({ result: { points: [] } }));
|
|
240
|
+
const incoming = await qdrantScroll(dest, incomingFilter, 10).catch(() => ({ result: { points: [] } }));
|
|
153
241
|
for (const pt of (incoming.result?.points ?? [])) {
|
|
154
242
|
if (pt.payload.from_entity)
|
|
155
243
|
relatedEntities.add(pt.payload.from_entity);
|
|
@@ -162,9 +250,9 @@ async function graphTraversal(primaryResults, limit, scope) {
|
|
|
162
250
|
const relatedFacts = [];
|
|
163
251
|
const maxPerEntity = Math.max(2, Math.floor(limit / relatedEntities.size));
|
|
164
252
|
for (const entity of relatedEntities) {
|
|
165
|
-
const filter =
|
|
253
|
+
const filter = buildFilter({ excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS }) ?? { must: [] };
|
|
166
254
|
filter.must.push({ key: "entities", match: { value: entity } });
|
|
167
|
-
const result = await qdrantScroll(filter, maxPerEntity).catch(() => ({ result: { points: [] } }));
|
|
255
|
+
const result = await qdrantScroll(dest, filter, maxPerEntity).catch(() => ({ result: { points: [] } }));
|
|
168
256
|
for (const pt of (result.result?.points ?? [])) {
|
|
169
257
|
if (!primaryIds.has(pt.id)) {
|
|
170
258
|
relatedFacts.push(pt);
|
|
@@ -189,33 +277,57 @@ export function registerTools(mcp) {
|
|
|
189
277
|
"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.",
|
|
190
278
|
"Read-only — safe to call any time.",
|
|
191
279
|
].join(" "), {}, async () => {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|| null;
|
|
280
|
+
const cfg = loadConfig();
|
|
281
|
+
const dests = listDestinations();
|
|
195
282
|
const status = {
|
|
196
283
|
ready,
|
|
197
|
-
|
|
198
|
-
qdrant_api_key: !!qdrantApiKey,
|
|
284
|
+
destinations_configured: dests.length,
|
|
199
285
|
missing: [],
|
|
200
|
-
qdrant_connected: false,
|
|
201
286
|
embedding_connected: false,
|
|
202
287
|
embedding_provider: getEmbeddingConfig().provider,
|
|
203
288
|
embedding_model: getEmbeddingConfig().model,
|
|
204
289
|
embedding_dimensions: getEmbeddingConfig().dimensions,
|
|
205
|
-
...(activeWorkspace ? { active_workspace: activeWorkspace } : {}),
|
|
206
290
|
...(setupError ? { setup_error: setupError } : {}),
|
|
207
291
|
};
|
|
208
292
|
const missing = status["missing"];
|
|
209
|
-
if (
|
|
210
|
-
missing.push("
|
|
211
|
-
//
|
|
212
|
-
|
|
293
|
+
if (dests.length === 0)
|
|
294
|
+
missing.push("destinations");
|
|
295
|
+
// Per-destination health
|
|
296
|
+
const destStatus = [];
|
|
297
|
+
for (const d of dests) {
|
|
298
|
+
const block = {
|
|
299
|
+
name: d.name,
|
|
300
|
+
qdrant_url_host: (() => { try {
|
|
301
|
+
return new URL(d.qdrant_url).host;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return d.qdrant_url;
|
|
305
|
+
} })(),
|
|
306
|
+
collection: d.collection,
|
|
307
|
+
default: d.default ?? false,
|
|
308
|
+
...(d.description ? { description: d.description } : {}),
|
|
309
|
+
connected: false,
|
|
310
|
+
collection_exists: false,
|
|
311
|
+
};
|
|
312
|
+
try {
|
|
313
|
+
await qdrantReq(d, "GET", "/collections");
|
|
314
|
+
block["connected"] = true;
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
block["last_error"] = e instanceof Error ? e.message : String(e);
|
|
318
|
+
}
|
|
213
319
|
try {
|
|
214
|
-
await qdrantReq("GET",
|
|
215
|
-
|
|
320
|
+
await qdrantReq(d, "GET", `/collections/${d.collection}`);
|
|
321
|
+
block["collection_exists"] = true;
|
|
216
322
|
}
|
|
217
323
|
catch { /* ignore */ }
|
|
324
|
+
destStatus.push(block);
|
|
218
325
|
}
|
|
326
|
+
status["destinations"] = destStatus;
|
|
327
|
+
status["default_search_scope"] = cfg.default_search_scope;
|
|
328
|
+
status["search_scopes"] = availableSearchScopes(cfg, dests);
|
|
329
|
+
status["search_scope_hint"] =
|
|
330
|
+
"Read/search tools accept search_scope: 'routed', 'all', a destination name, a configured scope name, a comma-separated destination list, or an array of destination names. Use destination only when you want an exact single-destination override.";
|
|
219
331
|
try {
|
|
220
332
|
await embed("test");
|
|
221
333
|
status["embedding_connected"] = true;
|
|
@@ -224,8 +336,11 @@ export function registerTools(mcp) {
|
|
|
224
336
|
// Watcher / extraction health (issue #58)
|
|
225
337
|
const warnings = [];
|
|
226
338
|
try {
|
|
227
|
-
const cfg = loadConfig();
|
|
228
339
|
status["watcher_path"] = cfg.watchers.copilot.path;
|
|
340
|
+
status["watcher_paths"] = {
|
|
341
|
+
copilot: cfg.watchers.copilot.path,
|
|
342
|
+
claude: cfg.watchers.claude.path,
|
|
343
|
+
};
|
|
229
344
|
for (const issue of inspectWatcherPaths(cfg)) {
|
|
230
345
|
warnings.push(formatIssue(issue));
|
|
231
346
|
}
|
|
@@ -237,17 +352,19 @@ export function registerTools(mcp) {
|
|
|
237
352
|
status["extraction_last_tick_at"] = health.last_tick_at ?? null;
|
|
238
353
|
status["extraction_last_active_session_at"] = health.last_active_session_at ?? null;
|
|
239
354
|
status["extraction_active_session_count"] = health.active_session_count ?? 0;
|
|
355
|
+
if (health.sources)
|
|
356
|
+
status["extraction_sources"] = health.sources;
|
|
240
357
|
if (health.last_active_session_at) {
|
|
241
358
|
const hours = (Date.now() - Date.parse(health.last_active_session_at)) / 3_600_000;
|
|
242
359
|
status["extraction_hours_since_active_session"] = Math.round(hours * 10) / 10;
|
|
243
360
|
if (hours > 6) {
|
|
244
|
-
warnings.push(`Watcher has not seen any active
|
|
361
|
+
warnings.push(`Watcher has not seen any active transcript sources for ${Math.round(hours)}h — ` +
|
|
245
362
|
`check watcher_path (${health.watcher_path ?? "unknown"}) and that the daemon is running.`);
|
|
246
363
|
}
|
|
247
364
|
}
|
|
248
365
|
else {
|
|
249
366
|
status["extraction_hours_since_active_session"] = null;
|
|
250
|
-
warnings.push("Daemon has never observed an active
|
|
367
|
+
warnings.push("Daemon has never observed an active transcript source — extraction may be stalled.");
|
|
251
368
|
}
|
|
252
369
|
}
|
|
253
370
|
else {
|
|
@@ -269,6 +386,25 @@ export function registerTools(mcp) {
|
|
|
269
386
|
}
|
|
270
387
|
return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
|
|
271
388
|
});
|
|
389
|
+
// ── memory_search_scopes ─────────────────────────────────────────────────
|
|
390
|
+
mcp.tool("memory_search_scopes", [
|
|
391
|
+
"List the configured memory search scopes and destination descriptions.",
|
|
392
|
+
"Use this before memory_recall, memory_entity, or memory_relations when multiple destinations exist so you can choose the right search_scope.",
|
|
393
|
+
"Read-only — returns built-in scopes ('routed', 'all'), destination-name scopes, configured named scopes, and the default_search_scope.",
|
|
394
|
+
].join(" "), {}, async () => {
|
|
395
|
+
const cfg = loadConfig();
|
|
396
|
+
const dests = listDestinations();
|
|
397
|
+
return {
|
|
398
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
399
|
+
default_search_scope: cfg.default_search_scope,
|
|
400
|
+
scopes: availableSearchScopes(cfg, dests),
|
|
401
|
+
usage: {
|
|
402
|
+
search_scope: "Pass one scope name, 'routed', 'all', a destination name, a comma-separated destination list, or an array of destination names.",
|
|
403
|
+
destination: "Use this older parameter only for an exact single-destination override. Do not combine it with search_scope.",
|
|
404
|
+
},
|
|
405
|
+
}, null, 2) }],
|
|
406
|
+
};
|
|
407
|
+
});
|
|
272
408
|
// ── configure_credentials ───────────────────────────────────────────────
|
|
273
409
|
mcp.tool("configure_credentials", [
|
|
274
410
|
"Persist Qdrant and embedding credentials to ~/.bikky/config.json and bring the memory system online.",
|
|
@@ -283,12 +419,10 @@ export function registerTools(mcp) {
|
|
|
283
419
|
if (qdrant_url) {
|
|
284
420
|
const url = qdrant_url.replace(/\/+$/, "");
|
|
285
421
|
cfg.qdrant_url = url;
|
|
286
|
-
setQdrantUrl(url);
|
|
287
422
|
results["qdrant_url"] = "stored ✓";
|
|
288
423
|
}
|
|
289
424
|
if (qdrant_api_key) {
|
|
290
425
|
cfg.qdrant_api_key = qdrant_api_key;
|
|
291
|
-
setQdrantApiKey(qdrant_api_key);
|
|
292
426
|
results["qdrant_api_key"] = "stored ✓";
|
|
293
427
|
}
|
|
294
428
|
if (openai_api_key) {
|
|
@@ -301,14 +435,22 @@ export function registerTools(mcp) {
|
|
|
301
435
|
results["watcher_path_repairs"] = watcherRepairs;
|
|
302
436
|
}
|
|
303
437
|
saveConfig(cfg);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
438
|
+
// Rebuild the destination pool from the updated config so the
|
|
439
|
+
// synthesized default destination picks up the new url/key.
|
|
440
|
+
try {
|
|
441
|
+
rebuildPool();
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
results["pool_rebuild"] = `error: ${e instanceof Error ? e.message : String(e)}`;
|
|
445
|
+
}
|
|
446
|
+
if (hasPool()) {
|
|
447
|
+
const ensured = await ensureCollectionsAll(QDRANT_INDEXES);
|
|
448
|
+
results["destinations"] = ensured.map((r) => ({
|
|
449
|
+
name: r.destination.name,
|
|
450
|
+
collection: r.destination.collection,
|
|
451
|
+
ok: r.ok,
|
|
452
|
+
...(r.error ? { error: r.error } : {}),
|
|
453
|
+
}));
|
|
312
454
|
}
|
|
313
455
|
try {
|
|
314
456
|
await embed("memory system test");
|
|
@@ -318,7 +460,7 @@ export function registerTools(mcp) {
|
|
|
318
460
|
catch (e) {
|
|
319
461
|
results["embedding"] = `error: ${e instanceof Error ? e.message : String(e)}`;
|
|
320
462
|
}
|
|
321
|
-
setReady(
|
|
463
|
+
setReady(hasPool());
|
|
322
464
|
results["ready"] = ready;
|
|
323
465
|
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
324
466
|
});
|
|
@@ -328,21 +470,31 @@ export function registerTools(mcp) {
|
|
|
328
470
|
"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.",
|
|
329
471
|
"Read-only.",
|
|
330
472
|
].join(" "), {}, async () => {
|
|
331
|
-
const results = {
|
|
332
|
-
|
|
473
|
+
const results = { embedding: false };
|
|
474
|
+
const dests = listDestinations();
|
|
475
|
+
const destResults = [];
|
|
476
|
+
for (const d of dests) {
|
|
477
|
+
const block = {
|
|
478
|
+
name: d.name,
|
|
479
|
+
collection: d.collection,
|
|
480
|
+
qdrant: false,
|
|
481
|
+
collection_exists: false,
|
|
482
|
+
};
|
|
333
483
|
try {
|
|
334
|
-
await qdrantReq("GET", "/collections");
|
|
335
|
-
|
|
484
|
+
await qdrantReq(d, "GET", "/collections");
|
|
485
|
+
block["qdrant"] = true;
|
|
336
486
|
}
|
|
337
487
|
catch (e) {
|
|
338
|
-
|
|
488
|
+
block["qdrant_error"] = e instanceof Error ? e.message : String(e);
|
|
339
489
|
}
|
|
340
490
|
try {
|
|
341
|
-
await qdrantReq("GET", `/collections/${
|
|
342
|
-
|
|
491
|
+
await qdrantReq(d, "GET", `/collections/${d.collection}`);
|
|
492
|
+
block["collection_exists"] = true;
|
|
343
493
|
}
|
|
344
494
|
catch { /* ignore */ }
|
|
495
|
+
destResults.push(block);
|
|
345
496
|
}
|
|
497
|
+
results["destinations"] = destResults;
|
|
346
498
|
try {
|
|
347
499
|
await embed("connection test");
|
|
348
500
|
results["embedding"] = true;
|
|
@@ -350,7 +502,8 @@ export function registerTools(mcp) {
|
|
|
350
502
|
catch (e) {
|
|
351
503
|
results["embedding_error"] = e instanceof Error ? e.message : String(e);
|
|
352
504
|
}
|
|
353
|
-
const allReady = results["
|
|
505
|
+
const allReady = results["embedding"] === true && destResults.length > 0
|
|
506
|
+
&& destResults.every((b) => b["qdrant"] === true && b["collection_exists"] === true);
|
|
354
507
|
results["ready"] = allReady;
|
|
355
508
|
setReady(allReady);
|
|
356
509
|
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
|
@@ -369,7 +522,8 @@ export function registerTools(mcp) {
|
|
|
369
522
|
domain: z.enum(domainValues()).default(DEFAULT_DOMAIN).describe(domainEnumDescription()),
|
|
370
523
|
kind: z.enum(kindValues()).default(DEFAULT_KIND).describe(kindEnumDescription()),
|
|
371
524
|
memory_subtype: z.enum(memorySubtypeValues()).optional().describe(memorySubtypeEnumDescription()),
|
|
372
|
-
workspace_id: z.string().optional().describe("
|
|
525
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op. Routing now uses destinations — see destination."),
|
|
526
|
+
destination: z.string().optional().describe("Optional destination override. When set, routes to that destination by name. Hard-errors if no such destination exists. Omit to let routing rules in ~/.bikky/config.json decide based on cwd/entities/content/metadata."),
|
|
373
527
|
actor_id: z.string().optional().describe("Stable actor/person/agent identity associated with this capture. Overrides identity config/env/Git-derived fallback for this write."),
|
|
374
528
|
episode_id: z.string().optional().describe("Coherent activity-segment ID. Group facts captured during the same coherent task or transcript."),
|
|
375
529
|
workstream_key: z.string().optional().describe("Durable continuity key for a long-running objective (survives across sessions)."),
|
|
@@ -387,13 +541,21 @@ export function registerTools(mcp) {
|
|
|
387
541
|
to: z.string().describe("Target entity (lowercase)."),
|
|
388
542
|
}).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."),
|
|
389
543
|
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)."),
|
|
390
|
-
}, async ({ content, category, entities, domain, kind, memory_subtype, workspace_id, actor_id, episode_id, workstream_key, task_key, repo, branch, review_status, source, confidence, importance, supersedes, relation, metadata, }) => {
|
|
544
|
+
}, async ({ content, category, entities, domain, kind, memory_subtype, workspace_id: _workspace_id, destination, actor_id, episode_id, workstream_key, task_key, repo, branch, review_status, source, confidence, importance, supersedes, relation, metadata, }) => {
|
|
391
545
|
const guard = requireReady();
|
|
392
546
|
if (guard)
|
|
393
547
|
return guard;
|
|
394
548
|
lastStoreTime = Date.now();
|
|
395
549
|
const now = nowISO();
|
|
396
|
-
const
|
|
550
|
+
const resolved = resolveDestOrError(routingInput({
|
|
551
|
+
destination,
|
|
552
|
+
content,
|
|
553
|
+
entities,
|
|
554
|
+
metadata,
|
|
555
|
+
}));
|
|
556
|
+
if (resolved.error)
|
|
557
|
+
return resolved.error;
|
|
558
|
+
const dest = resolved.dest;
|
|
397
559
|
const actor = resolveActorIdentity({ actorId: actor_id, config: loadConfig() });
|
|
398
560
|
const normalizedKind = normalizeKind(kind);
|
|
399
561
|
let normalizedSubtype = null;
|
|
@@ -439,14 +601,14 @@ export function registerTools(mcp) {
|
|
|
439
601
|
} : null;
|
|
440
602
|
// 1. Exact dedup via content hash
|
|
441
603
|
try {
|
|
442
|
-
const hashFilter =
|
|
604
|
+
const hashFilter = buildFilter({}) ?? { must: [] };
|
|
443
605
|
hashFilter.must.push({ key: "content_hash", match: { value: hash } });
|
|
444
|
-
const existing = await qdrantScroll(hashFilter, 1);
|
|
606
|
+
const existing = await qdrantScroll(dest, hashFilter, 1);
|
|
445
607
|
const existingPoint = existing.result?.points?.[0];
|
|
446
608
|
if (existingPoint) {
|
|
447
609
|
const point = existingPoint;
|
|
448
610
|
const count = (point.payload.reinforcement_count || 1) + 1;
|
|
449
|
-
await qdrantSetPayload([point.id], {
|
|
611
|
+
await qdrantSetPayload(dest, [point.id], {
|
|
450
612
|
reinforcement_count: count,
|
|
451
613
|
last_reinforced_at: now,
|
|
452
614
|
updated_at: now,
|
|
@@ -470,18 +632,18 @@ export function registerTools(mcp) {
|
|
|
470
632
|
let similarFacts = [];
|
|
471
633
|
let potentialConflicts = [];
|
|
472
634
|
try {
|
|
473
|
-
const filter =
|
|
635
|
+
const filter = buildFilter({}) ?? { must: [] };
|
|
474
636
|
if (normalizedEntities.length > 0) {
|
|
475
637
|
filter.must.push({ key: "entities", match: { any: normalizedEntities } });
|
|
476
638
|
}
|
|
477
|
-
const results = await qdrantSearch(vector, filter, 3);
|
|
639
|
+
const results = await qdrantSearch(dest, vector, filter, 3);
|
|
478
640
|
const firstResult = results.result?.[0];
|
|
479
641
|
if (results.result?.length > 0 && firstResult) {
|
|
480
642
|
const topScore = firstResult.score ?? 0;
|
|
481
643
|
if (topScore > THRESHOLD_DUPLICATE) {
|
|
482
644
|
const point = firstResult;
|
|
483
645
|
const count = (point.payload.reinforcement_count || 1) + 1;
|
|
484
|
-
await qdrantSetPayload([point.id], {
|
|
646
|
+
await qdrantSetPayload(dest, [point.id], {
|
|
485
647
|
reinforcement_count: count,
|
|
486
648
|
last_reinforced_at: now,
|
|
487
649
|
updated_at: now,
|
|
@@ -531,11 +693,11 @@ export function registerTools(mcp) {
|
|
|
531
693
|
// 5. Supersede old fact if requested
|
|
532
694
|
if (supersedes) {
|
|
533
695
|
try {
|
|
534
|
-
const existing = await
|
|
696
|
+
const existing = await getPointForWrite(dest, supersedes);
|
|
535
697
|
if (existing.error) {
|
|
536
698
|
return { content: [{ type: "text", text: JSON.stringify(existing.error, null, 2) }], isError: true };
|
|
537
699
|
}
|
|
538
|
-
await qdrantSetPayload([supersedes], {
|
|
700
|
+
await qdrantSetPayload(dest, [supersedes], {
|
|
539
701
|
superseded_by: factId,
|
|
540
702
|
superseded_at: now,
|
|
541
703
|
});
|
|
@@ -583,9 +745,9 @@ export function registerTools(mcp) {
|
|
|
583
745
|
}
|
|
584
746
|
if (review_status)
|
|
585
747
|
payload["review_status"] = review_status;
|
|
586
|
-
|
|
748
|
+
addActorPayload(payload, actor);
|
|
587
749
|
addRedactionPayload(payload, factRedactionSummary);
|
|
588
|
-
await qdrantUpsert(factId, vector, payload);
|
|
750
|
+
await qdrantUpsert(dest, factId, vector, payload);
|
|
589
751
|
// 7. Insert relation point if provided
|
|
590
752
|
let relationId = null;
|
|
591
753
|
if (sanitizedRelation) {
|
|
@@ -612,14 +774,14 @@ export function registerTools(mcp) {
|
|
|
612
774
|
relation_type: sanitizedRelation.type.toLowerCase(),
|
|
613
775
|
to_entity: sanitizedRelation.to.toLowerCase(),
|
|
614
776
|
};
|
|
615
|
-
|
|
777
|
+
addActorPayload(relPayload, actor);
|
|
616
778
|
addRedactionPayload(relPayload, relationRedactionSummary);
|
|
617
|
-
await qdrantUpsert(relationId, relVector, relPayload);
|
|
779
|
+
await qdrantUpsert(dest, relationId, relVector, relPayload);
|
|
618
780
|
}
|
|
619
781
|
const result = {
|
|
620
782
|
action: "inserted",
|
|
621
783
|
fact_id: factId,
|
|
622
|
-
|
|
784
|
+
destination: dest.name,
|
|
623
785
|
};
|
|
624
786
|
if (actor.actor_id)
|
|
625
787
|
result["actor_id"] = actor.actor_id;
|
|
@@ -646,6 +808,7 @@ export function registerTools(mcp) {
|
|
|
646
808
|
" 3. Conflict/replacement check — recall similar facts when you suspect new information may supersede an older fact. Deduplication during memory_store is automatic.",
|
|
647
809
|
"Combine the natural-language query with structured filters (category, domain, entity, date range, metadata) for tighter results.",
|
|
648
810
|
"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.",
|
|
811
|
+
"When multiple Qdrant destinations are configured, use search_scope to choose 'routed' (routing/default behavior), 'all', a destination name, a configured scope name, a comma-separated destination list, or an array of destination names. Call memory_search_scopes to inspect available scopes and descriptions.",
|
|
649
812
|
`By default output is human-readable text. Use output_format=json for machine-parseable results with separate results and related arrays. Default limit is ${MEMORY_RECALL_DEFAULT_LIMIT}; maximum effective limit is ${MEMORY_RECALL_MAX_LIMIT}.`,
|
|
650
813
|
].join("\n"), {
|
|
651
814
|
query: z.string().describe("Natural-language description of what you're looking for. Embedded and matched semantically — full sentences work better than keyword lists."),
|
|
@@ -653,9 +816,11 @@ export function registerTools(mcp) {
|
|
|
653
816
|
domain: z.string().optional().describe("Filter by domain activity profile (same vocabulary as memory_store.domain). Optional."),
|
|
654
817
|
kind: z.string().optional().describe("Filter by kind: fact, summary, distilled, relation. Optional. Telemetry is excluded by default."),
|
|
655
818
|
memory_subtype: z.string().optional().describe("Filter by memory subtype (must be valid for the chosen kind). Optional."),
|
|
656
|
-
workspace_id: z.string().optional().describe("
|
|
819
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
820
|
+
destination: z.string().optional().describe("Optional legacy single-destination override. Do not combine with search_scope. Prefer search_scope for routed/all/list search."),
|
|
821
|
+
search_scope: searchScopeSchema.describe("Optional read/search scope. Accepts 'routed', 'all', a destination name, a configured scope name, a comma-separated destination list, or an array of destination names. Omit to use config.default_search_scope."),
|
|
657
822
|
actor_id: z.string().optional().describe("Filter to facts captured by or associated with this stable actor identity. Optional."),
|
|
658
|
-
include_legacy_workspace: z.boolean().optional().describe("
|
|
823
|
+
include_legacy_workspace: z.boolean().optional().describe("[Removed in v0.4.0] No-op."),
|
|
659
824
|
entity: z.string().optional().describe("Restrict to facts mentioning this entity (case-insensitive). For full entity context prefer memory_entity."),
|
|
660
825
|
episode_id: z.string().optional().describe("Filter by coherent episode ID."),
|
|
661
826
|
workstream_key: z.string().optional().describe("Filter by durable workstream key."),
|
|
@@ -669,14 +834,25 @@ export function registerTools(mcp) {
|
|
|
669
834
|
graph_depth: z.number().optional().default(0).describe("Entity-graph traversal depth. 0 = vector search only (fast, default). 1 = also surface up to ceil(limit / 2) extra 1-hop entity-related facts (slower; use when the user asks 'what's connected to X?'). In JSON output these are returned separately as related."),
|
|
670
835
|
output_format: z.enum(["text", "json"]).optional().default("text").describe("Response format. text = backward-compatible human-readable lines (default). json = parseable object with query, limit metadata, results, related, counts, and optional nudge."),
|
|
671
836
|
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)."),
|
|
672
|
-
}, async ({ query, category, domain, kind, memory_subtype, workspace_id, actor_id, include_legacy_workspace, entity, episode_id, workstream_key, task_key, repo, branch, review_status, since, until, limit, graph_depth, output_format, metadata_filter, }) => {
|
|
837
|
+
}, async ({ query, category, domain, kind, memory_subtype, workspace_id: _workspace_id, destination, search_scope, actor_id, include_legacy_workspace: _include_legacy_workspace, entity, episode_id, workstream_key, task_key, repo, branch, review_status, since, until, limit, graph_depth, output_format, metadata_filter, }) => {
|
|
673
838
|
const guard = requireReady();
|
|
674
839
|
if (guard)
|
|
675
840
|
return guard;
|
|
676
841
|
const requestedLimit = limit ?? MEMORY_RECALL_DEFAULT_LIMIT;
|
|
677
842
|
const effectiveLimit = clampRecallLimit(limit);
|
|
678
843
|
const actorFilter = resolveActorIdentity({ actorId: actor_id, useGitFallback: false });
|
|
679
|
-
const
|
|
844
|
+
const scopeResolved = resolveSearchScopeOrError({
|
|
845
|
+
destination,
|
|
846
|
+
search_scope,
|
|
847
|
+
input: routingInput({
|
|
848
|
+
content: query,
|
|
849
|
+
entities: entity ? [entity] : [],
|
|
850
|
+
metadata: metadata_filter,
|
|
851
|
+
}),
|
|
852
|
+
});
|
|
853
|
+
if (scopeResolved.error)
|
|
854
|
+
return scopeResolved.error;
|
|
855
|
+
const searchScope = scopeResolved.scope;
|
|
680
856
|
const redactedQuery = redactStorageText(query);
|
|
681
857
|
const vector = await embed(redactedQuery.text);
|
|
682
858
|
const normalizedKind = kind ? normalizeKind(kind) : undefined;
|
|
@@ -692,11 +868,12 @@ export function registerTools(mcp) {
|
|
|
692
868
|
};
|
|
693
869
|
}
|
|
694
870
|
}
|
|
695
|
-
const filter =
|
|
871
|
+
const filter = buildFilter({
|
|
696
872
|
category: category ? normalizeCategory(category) : undefined,
|
|
697
873
|
domain: domain ? normalizeDomain(domain) : undefined,
|
|
698
874
|
kind: normalizedKind,
|
|
699
875
|
memory_subtype: normalizedSubtype,
|
|
876
|
+
actor_id: actorFilter.actor_id,
|
|
700
877
|
entity,
|
|
701
878
|
episode_id,
|
|
702
879
|
workstream_key,
|
|
@@ -709,12 +886,42 @@ export function registerTools(mcp) {
|
|
|
709
886
|
metadata: metadata_filter,
|
|
710
887
|
excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS,
|
|
711
888
|
});
|
|
712
|
-
const
|
|
713
|
-
|
|
889
|
+
const searchedDestinations = searchScope.destinations.map((dest) => dest.name);
|
|
890
|
+
const failedDestinations = [];
|
|
891
|
+
const scopedResults = [];
|
|
892
|
+
for (const dest of searchScope.destinations) {
|
|
893
|
+
try {
|
|
894
|
+
const results = await qdrantSearch(dest, vector, filter, effectiveLimit * 2);
|
|
895
|
+
scopedResults.push(...(results.result ?? []).map((point) => withDestination(point, dest)));
|
|
896
|
+
}
|
|
897
|
+
catch (e) {
|
|
898
|
+
failedDestinations.push({
|
|
899
|
+
destination: dest.name,
|
|
900
|
+
error: e instanceof Error ? e.message : String(e),
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (failedDestinations.length === searchScope.destinations.length && searchScope.destinations.length > 0) {
|
|
905
|
+
return {
|
|
906
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
907
|
+
status: "search_failed",
|
|
908
|
+
query: redactedQuery.text,
|
|
909
|
+
search_scope: searchScope.name,
|
|
910
|
+
searched_destinations: searchedDestinations,
|
|
911
|
+
failed_destinations: failedDestinations,
|
|
912
|
+
}, null, 2) }],
|
|
913
|
+
isError: true,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
if (scopedResults.length === 0) {
|
|
714
917
|
const nudge = buildMemoryNudge();
|
|
715
918
|
if (output_format === "json") {
|
|
716
919
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
717
920
|
query: redactedQuery.text,
|
|
921
|
+
search_scope: searchScope.name,
|
|
922
|
+
search_scope_description: searchScope.description,
|
|
923
|
+
searched_destinations: searchedDestinations,
|
|
924
|
+
failed_destinations: failedDestinations,
|
|
718
925
|
requested_limit: requestedLimit,
|
|
719
926
|
effective_limit: effectiveLimit,
|
|
720
927
|
max_limit: MEMORY_RECALL_MAX_LIMIT,
|
|
@@ -728,31 +935,61 @@ export function registerTools(mcp) {
|
|
|
728
935
|
...(redactedQuery.redacted ? { query_redaction: redactedQuery } : {}),
|
|
729
936
|
}, null, 2) }] };
|
|
730
937
|
}
|
|
731
|
-
const
|
|
938
|
+
const warning = failedDestinations.length > 0
|
|
939
|
+
? `\n\nSearch warnings: ${failedDestinations.map((failure) => `${failure.destination}: ${failure.error}`).join("; ")}`
|
|
940
|
+
: "";
|
|
941
|
+
const text = nudge
|
|
942
|
+
? `No matching facts found.${warning}\n\n${nudge}`
|
|
943
|
+
: `No matching facts found.${warning}`;
|
|
732
944
|
return { content: [{ type: "text", text }] };
|
|
733
945
|
}
|
|
734
|
-
const ranked =
|
|
946
|
+
const ranked = scopedResults
|
|
735
947
|
.map((r) => ({ ...r, _combinedScore: computeCombinedScore(r) }))
|
|
736
948
|
.sort((a, b) => b._combinedScore - a._combinedScore)
|
|
737
949
|
.slice(0, effectiveLimit);
|
|
738
|
-
const
|
|
739
|
-
|
|
950
|
+
const includeDestination = searchScope.destinations.length > 1 || searchScope.name === "all";
|
|
951
|
+
const lines = ranked.map((r) => formatScopedFact(r, includeDestination));
|
|
952
|
+
const related = { points: [], errors: [] };
|
|
740
953
|
if ((graph_depth ?? 0) >= 1) {
|
|
741
|
-
|
|
954
|
+
const byDestination = new Map();
|
|
955
|
+
for (const point of ranked) {
|
|
956
|
+
const existing = byDestination.get(point._destination.name);
|
|
957
|
+
if (existing) {
|
|
958
|
+
existing.points.push(point);
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
byDestination.set(point._destination.name, { dest: point._destination, points: [point] });
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
for (const entry of byDestination.values()) {
|
|
965
|
+
const traversal = await graphTraversal(entry.dest, entry.points, effectiveLimit);
|
|
966
|
+
related.points.push(...traversal.points.map((point) => withDestination(point, entry.dest)));
|
|
967
|
+
if (traversal.error) {
|
|
968
|
+
related.errors.push({ destination: entry.dest.name, error: traversal.error });
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
related.points = related.points.slice(0, Math.ceil(effectiveLimit / 2));
|
|
742
972
|
if (related.points.length > 0) {
|
|
743
973
|
lines.push("", "── Related (1-hop) ──");
|
|
744
|
-
lines.push(...related.points.map((r) =>
|
|
974
|
+
lines.push(...related.points.map((r) => formatScopedFact(r, includeDestination)));
|
|
745
975
|
}
|
|
746
|
-
|
|
747
|
-
lines.push("", `(graph traversal
|
|
976
|
+
if (related.errors.length > 0) {
|
|
977
|
+
lines.push("", `(graph traversal warnings: ${related.errors.map((failure) => `${failure.destination}: ${failure.error}`).join("; ")})`);
|
|
748
978
|
}
|
|
749
979
|
}
|
|
980
|
+
if (failedDestinations.length > 0) {
|
|
981
|
+
lines.push("", `(search warnings: ${failedDestinations.map((failure) => `${failure.destination}: ${failure.error}`).join("; ")})`);
|
|
982
|
+
}
|
|
750
983
|
const nudge = buildMemoryNudge();
|
|
751
984
|
if (nudge)
|
|
752
985
|
lines.push("", nudge);
|
|
753
986
|
if (output_format === "json") {
|
|
754
987
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
755
988
|
query: redactedQuery.text,
|
|
989
|
+
search_scope: searchScope.name,
|
|
990
|
+
search_scope_description: searchScope.description,
|
|
991
|
+
searched_destinations: searchedDestinations,
|
|
992
|
+
failed_destinations: failedDestinations,
|
|
756
993
|
requested_limit: requestedLimit,
|
|
757
994
|
effective_limit: effectiveLimit,
|
|
758
995
|
max_limit: MEMORY_RECALL_MAX_LIMIT,
|
|
@@ -760,9 +997,9 @@ export function registerTools(mcp) {
|
|
|
760
997
|
graph_depth: graph_depth ?? 0,
|
|
761
998
|
result_count: ranked.length,
|
|
762
999
|
related_count: related.points.length,
|
|
763
|
-
results: ranked.map((r) =>
|
|
764
|
-
related: related.points.map((r) =>
|
|
765
|
-
...(related.
|
|
1000
|
+
results: ranked.map((r) => structuredScopedFact(r)),
|
|
1001
|
+
related: related.points.map((r) => structuredScopedFact(r)),
|
|
1002
|
+
...(related.errors.length > 0 ? { graph_errors: related.errors } : {}),
|
|
766
1003
|
...(nudge ? { nudge } : {}),
|
|
767
1004
|
...(redactedQuery.redacted ? { query_redaction: redactedQuery } : {}),
|
|
768
1005
|
}, null, 2) }] };
|
|
@@ -777,73 +1014,113 @@ export function registerTools(mcp) {
|
|
|
777
1014
|
].join(" "), {
|
|
778
1015
|
name: z.string().describe("Entity name (case-insensitive, e.g. 'qdrant', 'workspace_id'). Should match the lowercase canonical form used when facts were stored."),
|
|
779
1016
|
limit: z.number().optional().default(20).describe("Max facts to return (default 20). Relations are always returned in full, capped at 50 each direction."),
|
|
780
|
-
workspace_id: z.string().optional().describe("
|
|
781
|
-
|
|
782
|
-
|
|
1017
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1018
|
+
destination: z.string().optional().describe("Optional legacy single-destination override. Do not combine with search_scope."),
|
|
1019
|
+
search_scope: searchScopeSchema.describe("Optional read/search scope. Accepts 'routed', 'all', a destination name, a configured scope name, a comma-separated destination list, or an array of destination names. Omit to use config.default_search_scope."),
|
|
1020
|
+
include_legacy_workspace: z.boolean().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1021
|
+
}, async ({ name, limit, workspace_id: _workspace_id, destination, search_scope, include_legacy_workspace: _include_legacy_workspace }) => {
|
|
783
1022
|
const guard = requireReady();
|
|
784
1023
|
if (guard)
|
|
785
1024
|
return guard;
|
|
786
1025
|
const entityName = name.toLowerCase();
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1026
|
+
const scopeResolved = resolveSearchScopeOrError({
|
|
1027
|
+
destination,
|
|
1028
|
+
search_scope,
|
|
1029
|
+
input: routingInput({ entities: [entityName] }),
|
|
1030
|
+
});
|
|
1031
|
+
if (scopeResolved.error)
|
|
1032
|
+
return scopeResolved.error;
|
|
1033
|
+
const searchScope = scopeResolved.scope;
|
|
1034
|
+
const effectiveLimit = Math.max(1, Math.trunc(limit ?? 20));
|
|
1035
|
+
const entityTypes = new Map();
|
|
1036
|
+
const factPoints = [];
|
|
1037
|
+
const relationPoints = [];
|
|
1038
|
+
const failures = [];
|
|
1039
|
+
for (const dest of searchScope.destinations) {
|
|
1040
|
+
// Look up the daemon-classified entity type, if any.
|
|
1041
|
+
try {
|
|
1042
|
+
const typeFilter = buildFilter({}) ?? { must: [] };
|
|
1043
|
+
typeFilter.must.push({ key: "kind", match: { value: "entity_type" } });
|
|
1044
|
+
typeFilter.must.push({ key: "entity_name", match: { value: entityName } });
|
|
1045
|
+
const typePoints = await qdrantScroll(dest, typeFilter, 1);
|
|
1046
|
+
const typePoint = typePoints.result?.points?.[0];
|
|
1047
|
+
const payload = typePoint?.payload;
|
|
1048
|
+
if (payload?.entity_type) {
|
|
1049
|
+
entityTypes.set(dest.name, String(payload.entity_type));
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
catch {
|
|
1053
|
+
// Type lookup is best-effort — never fails the request.
|
|
1054
|
+
}
|
|
1055
|
+
try {
|
|
1056
|
+
const factsFilter = buildFilter({}) ?? { must: [] };
|
|
1057
|
+
factsFilter.must.push({ key: "entities", match: { value: entityName } });
|
|
1058
|
+
const facts = await qdrantScroll(dest, factsFilter, effectiveLimit);
|
|
1059
|
+
factPoints.push(...(facts.result?.points ?? []).map((point) => withDestination(point, dest)));
|
|
1060
|
+
const fromFilter = buildFilter({}) ?? { must: [] };
|
|
1061
|
+
fromFilter.must.push({ key: "from_entity", match: { value: entityName } });
|
|
1062
|
+
const relationsFrom = await qdrantScroll(dest, fromFilter, 50);
|
|
1063
|
+
const toFilter = buildFilter({}) ?? { must: [] };
|
|
1064
|
+
toFilter.must.push({ key: "to_entity", match: { value: entityName } });
|
|
1065
|
+
const relationsTo = await qdrantScroll(dest, toFilter, 50);
|
|
1066
|
+
relationPoints.push(...[
|
|
1067
|
+
...(relationsFrom.result?.points ?? []),
|
|
1068
|
+
...(relationsTo.result?.points ?? []),
|
|
1069
|
+
].map((point) => withDestination(point, dest)));
|
|
1070
|
+
}
|
|
1071
|
+
catch (e) {
|
|
1072
|
+
failures.push({ destination: dest.name, error: e instanceof Error ? e.message : String(e) });
|
|
799
1073
|
}
|
|
800
1074
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1075
|
+
if (failures.length === searchScope.destinations.length && searchScope.destinations.length > 0) {
|
|
1076
|
+
return {
|
|
1077
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
1078
|
+
status: "search_failed",
|
|
1079
|
+
entity: entityName,
|
|
1080
|
+
search_scope: searchScope.name,
|
|
1081
|
+
searched_destinations: searchScope.destinations.map((dest) => dest.name),
|
|
1082
|
+
failed_destinations: failures,
|
|
1083
|
+
}, null, 2) }],
|
|
1084
|
+
isError: true,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
813
1087
|
const output = [];
|
|
814
|
-
const
|
|
1088
|
+
const includeDestination = searchScope.destinations.length > 1 || searchScope.name === "all";
|
|
1089
|
+
const entityTypeValues = [...new Set(entityTypes.values())];
|
|
1090
|
+
const entityType = entityTypeValues.length === 1 ? entityTypeValues[0] : null;
|
|
815
1091
|
if (factPoints.length > 0) {
|
|
816
1092
|
const header = entityType
|
|
817
|
-
? `## Facts about ${name} [type: ${entityType}] (${factPoints.length})`
|
|
818
|
-
: `## Facts about ${name} (${factPoints.length})`;
|
|
1093
|
+
? `## Facts about ${name} [type: ${entityType}] (${Math.min(factPoints.length, effectiveLimit)})`
|
|
1094
|
+
: `## Facts about ${name} (${Math.min(factPoints.length, effectiveLimit)})`;
|
|
819
1095
|
output.push(header);
|
|
820
|
-
for (const p of factPoints) {
|
|
1096
|
+
for (const p of factPoints.slice(0, effectiveLimit)) {
|
|
821
1097
|
if (p.payload.category !== "relation") {
|
|
822
|
-
output.push(`- ${
|
|
1098
|
+
output.push(`- ${formatScopedFact(p, includeDestination)}`);
|
|
823
1099
|
}
|
|
824
1100
|
}
|
|
825
1101
|
}
|
|
826
1102
|
else if (entityType) {
|
|
827
1103
|
output.push(`## ${name} [type: ${entityType}]`);
|
|
828
1104
|
}
|
|
829
|
-
const allRelations = [
|
|
830
|
-
...(relationsFrom.result?.points ?? []),
|
|
831
|
-
...(relationsTo.result?.points ?? []),
|
|
832
|
-
];
|
|
833
1105
|
const seen = new Set();
|
|
834
|
-
const uniqueRelations =
|
|
835
|
-
|
|
1106
|
+
const uniqueRelations = relationPoints.filter((r) => {
|
|
1107
|
+
const key = `${r._destination.name}:${r.id}`;
|
|
1108
|
+
if (seen.has(key))
|
|
836
1109
|
return false;
|
|
837
|
-
seen.add(
|
|
1110
|
+
seen.add(key);
|
|
838
1111
|
return true;
|
|
839
1112
|
});
|
|
840
1113
|
if (uniqueRelations.length > 0) {
|
|
841
1114
|
output.push(`\n## Relations (${uniqueRelations.length})`);
|
|
842
1115
|
for (const r of uniqueRelations) {
|
|
843
1116
|
const p = r.payload;
|
|
844
|
-
|
|
1117
|
+
const prefix = includeDestination ? `[${r._destination.name}] ` : "";
|
|
1118
|
+
output.push(`- ${prefix}${p.from_entity} --[${p.relation_type}]--> ${p.to_entity}`);
|
|
845
1119
|
}
|
|
846
1120
|
}
|
|
1121
|
+
if (failures.length > 0) {
|
|
1122
|
+
output.push(`\nSearch warnings: ${failures.map((failure) => `${failure.destination}: ${failure.error}`).join("; ")}`);
|
|
1123
|
+
}
|
|
847
1124
|
if (output.length === 0) {
|
|
848
1125
|
return { content: [{ type: "text", text: `No facts or relations found for '${name}'.` }] };
|
|
849
1126
|
}
|
|
@@ -858,47 +1135,85 @@ export function registerTools(mcp) {
|
|
|
858
1135
|
entity: z.string().describe("Entity name to query (case-insensitive)."),
|
|
859
1136
|
relation_type: z.string().optional().describe("Filter to a specific edge label (e.g. 'owns', 'uses', 'decided', 'prefers', 'works-on'). Optional."),
|
|
860
1137
|
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)."),
|
|
861
|
-
workspace_id: z.string().optional().describe("
|
|
862
|
-
|
|
863
|
-
|
|
1138
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1139
|
+
destination: z.string().optional().describe("Optional legacy single-destination override. Do not combine with search_scope."),
|
|
1140
|
+
search_scope: searchScopeSchema.describe("Optional read/search scope. Accepts 'routed', 'all', a destination name, a configured scope name, a comma-separated destination list, or an array of destination names. Omit to use config.default_search_scope."),
|
|
1141
|
+
include_legacy_workspace: z.boolean().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1142
|
+
}, async ({ entity, relation_type, direction, workspace_id: _workspace_id, destination, search_scope, include_legacy_workspace: _include_legacy_workspace }) => {
|
|
864
1143
|
const guard = requireReady();
|
|
865
1144
|
if (guard)
|
|
866
1145
|
return guard;
|
|
867
1146
|
const entityName = entity.toLowerCase();
|
|
868
|
-
const
|
|
1147
|
+
const scopeResolved = resolveSearchScopeOrError({
|
|
1148
|
+
destination,
|
|
1149
|
+
search_scope,
|
|
1150
|
+
input: routingInput({ entities: [entityName] }),
|
|
1151
|
+
});
|
|
1152
|
+
if (scopeResolved.error)
|
|
1153
|
+
return scopeResolved.error;
|
|
1154
|
+
const searchScope = scopeResolved.scope;
|
|
869
1155
|
const results = [];
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1156
|
+
const failures = [];
|
|
1157
|
+
for (const dest of searchScope.destinations) {
|
|
1158
|
+
try {
|
|
1159
|
+
if (direction === "from" || direction === "both") {
|
|
1160
|
+
const filter = buildFilter({}) ?? { must: [] };
|
|
1161
|
+
filter.must.push({ key: "from_entity", match: { value: entityName } });
|
|
1162
|
+
if (relation_type) {
|
|
1163
|
+
filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
|
|
1164
|
+
}
|
|
1165
|
+
const r = await qdrantScroll(dest, filter, 50);
|
|
1166
|
+
results.push(...(r.result?.points ?? []).map((point) => withDestination(point, dest)));
|
|
1167
|
+
}
|
|
1168
|
+
if (direction === "to" || direction === "both") {
|
|
1169
|
+
const filter = buildFilter({}) ?? { must: [] };
|
|
1170
|
+
filter.must.push({ key: "to_entity", match: { value: entityName } });
|
|
1171
|
+
if (relation_type) {
|
|
1172
|
+
filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
|
|
1173
|
+
}
|
|
1174
|
+
const r = await qdrantScroll(dest, filter, 50);
|
|
1175
|
+
results.push(...(r.result?.points ?? []).map((point) => withDestination(point, dest)));
|
|
1176
|
+
}
|
|
875
1177
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
879
|
-
if (direction === "to" || direction === "both") {
|
|
880
|
-
const filter = scopedFilter(scope) ?? { must: [] };
|
|
881
|
-
filter.must.push({ key: "to_entity", match: { value: entityName } });
|
|
882
|
-
if (relation_type) {
|
|
883
|
-
filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
|
|
1178
|
+
catch (e) {
|
|
1179
|
+
failures.push({ destination: dest.name, error: e instanceof Error ? e.message : String(e) });
|
|
884
1180
|
}
|
|
885
|
-
|
|
886
|
-
|
|
1181
|
+
}
|
|
1182
|
+
if (failures.length === searchScope.destinations.length && searchScope.destinations.length > 0) {
|
|
1183
|
+
return {
|
|
1184
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
1185
|
+
status: "search_failed",
|
|
1186
|
+
entity: entityName,
|
|
1187
|
+
search_scope: searchScope.name,
|
|
1188
|
+
searched_destinations: searchScope.destinations.map((dest) => dest.name),
|
|
1189
|
+
failed_destinations: failures,
|
|
1190
|
+
}, null, 2) }],
|
|
1191
|
+
isError: true,
|
|
1192
|
+
};
|
|
887
1193
|
}
|
|
888
1194
|
const seen = new Set();
|
|
889
1195
|
const unique = results.filter((r) => {
|
|
890
|
-
|
|
1196
|
+
const key = `${r._destination.name}:${r.id}`;
|
|
1197
|
+
if (seen.has(key))
|
|
891
1198
|
return false;
|
|
892
|
-
seen.add(
|
|
1199
|
+
seen.add(key);
|
|
893
1200
|
return true;
|
|
894
1201
|
});
|
|
895
1202
|
if (unique.length === 0) {
|
|
896
|
-
|
|
1203
|
+
const warning = failures.length > 0
|
|
1204
|
+
? ` Search warnings: ${failures.map((failure) => `${failure.destination}: ${failure.error}`).join("; ")}`
|
|
1205
|
+
: "";
|
|
1206
|
+
return { content: [{ type: "text", text: `No relations found for '${entity}'.${warning}` }] };
|
|
897
1207
|
}
|
|
1208
|
+
const includeDestination = searchScope.destinations.length > 1 || searchScope.name === "all";
|
|
898
1209
|
const lines = unique.map((r) => {
|
|
899
1210
|
const p = r.payload;
|
|
900
|
-
|
|
1211
|
+
const prefix = includeDestination ? `[${r._destination.name}] ` : "";
|
|
1212
|
+
return `${prefix}${p.from_entity} --[${p.relation_type}]--> ${p.to_entity} (confidence: ${p.confidence}, id: ${r.id})`;
|
|
901
1213
|
});
|
|
1214
|
+
if (failures.length > 0) {
|
|
1215
|
+
lines.push("", `Search warnings: ${failures.map((failure) => `${failure.destination}: ${failure.error}`).join("; ")}`);
|
|
1216
|
+
}
|
|
902
1217
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
903
1218
|
});
|
|
904
1219
|
// ── memory_forget ───────────────────────────────────────────────────────
|
|
@@ -908,21 +1223,19 @@ export function registerTools(mcp) {
|
|
|
908
1223
|
].join(" "), {
|
|
909
1224
|
fact_id: z.string().describe("ID of the fact to forget (returned by memory_store / memory_recall as 'id')."),
|
|
910
1225
|
reason: z.string().describe("Short human-readable reason this fact is being retired (stored in 'superseded_by' for future audit)."),
|
|
911
|
-
workspace_id: z.string().optional().describe("
|
|
912
|
-
}, async ({ fact_id, reason, workspace_id }) => {
|
|
1226
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1227
|
+
}, async ({ fact_id, reason, workspace_id: _workspace_id }) => {
|
|
913
1228
|
const guard = requireReady();
|
|
914
1229
|
if (guard)
|
|
915
1230
|
return guard;
|
|
916
1231
|
const now = nowISO();
|
|
917
1232
|
try {
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
return { content: [{ type: "text", text: JSON.stringify(existing.error, null, 2) }], isError: true };
|
|
923
|
-
}
|
|
1233
|
+
const located = await locatePoint(fact_id);
|
|
1234
|
+
if (!located)
|
|
1235
|
+
return notFoundResult(fact_id);
|
|
1236
|
+
const { dest } = located;
|
|
924
1237
|
const redactedReason = redactStorageText(reason);
|
|
925
|
-
await qdrantSetPayload([fact_id], {
|
|
1238
|
+
await qdrantSetPayload(dest, [fact_id], {
|
|
926
1239
|
superseded_by: `forgotten:${redactedReason.text}`,
|
|
927
1240
|
superseded_at: now,
|
|
928
1241
|
updated_at: now,
|
|
@@ -937,6 +1250,7 @@ export function registerTools(mcp) {
|
|
|
937
1250
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
938
1251
|
status: "forgotten",
|
|
939
1252
|
fact_id,
|
|
1253
|
+
destination: dest.name,
|
|
940
1254
|
reason: redactedReason.text,
|
|
941
1255
|
...(redactedReason.redacted ? { redaction: redactedReason } : {}),
|
|
942
1256
|
}) }] };
|
|
@@ -952,26 +1266,20 @@ export function registerTools(mcp) {
|
|
|
952
1266
|
"If the fact is no longer true, use memory_forget or memory_store(supersedes:) instead.",
|
|
953
1267
|
].join(" "), {
|
|
954
1268
|
fact_id: z.string().describe("ID of the fact to verify (from memory_recall or memory_heartbeat)."),
|
|
955
|
-
workspace_id: z.string().optional().describe("
|
|
956
|
-
}, async ({ fact_id, workspace_id }) => {
|
|
1269
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1270
|
+
}, async ({ fact_id, workspace_id: _workspace_id }) => {
|
|
957
1271
|
const guard = requireReady();
|
|
958
1272
|
if (guard)
|
|
959
1273
|
return guard;
|
|
960
1274
|
const now = nowISO();
|
|
961
1275
|
try {
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
let currentCount = 0;
|
|
969
|
-
const existingPt = writable.point;
|
|
970
|
-
if (existingPt) {
|
|
971
|
-
currentCount = existingPt.payload.verification_count ?? 0;
|
|
972
|
-
}
|
|
1276
|
+
const located = await locatePoint(fact_id);
|
|
1277
|
+
if (!located)
|
|
1278
|
+
return notFoundResult(fact_id);
|
|
1279
|
+
const { dest, point } = located;
|
|
1280
|
+
const currentCount = point.payload.verification_count ?? 0;
|
|
973
1281
|
const newCount = currentCount + 1;
|
|
974
|
-
await qdrantSetPayload([fact_id], {
|
|
1282
|
+
await qdrantSetPayload(dest, [fact_id], {
|
|
975
1283
|
last_verified_at: now,
|
|
976
1284
|
last_reinforced_at: now,
|
|
977
1285
|
verification_count: newCount,
|
|
@@ -981,6 +1289,7 @@ export function registerTools(mcp) {
|
|
|
981
1289
|
content: [{ type: "text", text: JSON.stringify({
|
|
982
1290
|
status: "verified",
|
|
983
1291
|
fact_id,
|
|
1292
|
+
destination: dest.name,
|
|
984
1293
|
verification_count: newCount,
|
|
985
1294
|
message: "Fact confirmed as still accurate. Staleness clock reset.",
|
|
986
1295
|
}) }],
|
|
@@ -998,23 +1307,21 @@ export function registerTools(mcp) {
|
|
|
998
1307
|
].join(" "), {
|
|
999
1308
|
fact_id: z.string().describe("ID of the fact that was useful (from memory_recall or memory_entity)."),
|
|
1000
1309
|
note: z.string().optional().describe("Optional short note about how the fact was useful (e.g. 'unblocked auth debug'). Stored on the telemetry event for future analysis."),
|
|
1001
|
-
workspace_id: z.string().optional().describe("
|
|
1002
|
-
}, async ({ fact_id, note, workspace_id }) => {
|
|
1310
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1311
|
+
}, async ({ fact_id, note, workspace_id: _workspace_id }) => {
|
|
1003
1312
|
const guard = requireReady();
|
|
1004
1313
|
if (guard)
|
|
1005
1314
|
return guard;
|
|
1006
1315
|
const now = nowISO();
|
|
1007
1316
|
try {
|
|
1008
|
-
const
|
|
1317
|
+
const located = await locatePoint(fact_id);
|
|
1318
|
+
if (!located)
|
|
1319
|
+
return notFoundResult(fact_id);
|
|
1320
|
+
const { dest, point } = located;
|
|
1009
1321
|
const actor = resolveActorIdentity({ config: loadConfig() });
|
|
1010
|
-
const
|
|
1011
|
-
if (writable.error) {
|
|
1012
|
-
return { content: [{ type: "text", text: JSON.stringify(writable.error, null, 2) }], isError: true };
|
|
1013
|
-
}
|
|
1014
|
-
const existingPt = writable.point;
|
|
1015
|
-
const currentCount = existingPt?.payload.useful_count ?? 0;
|
|
1322
|
+
const currentCount = point.payload.useful_count ?? 0;
|
|
1016
1323
|
const newCount = currentCount + 1;
|
|
1017
|
-
await qdrantSetPayload([fact_id], {
|
|
1324
|
+
await qdrantSetPayload(dest, [fact_id], {
|
|
1018
1325
|
useful_count: newCount,
|
|
1019
1326
|
last_useful_at: now,
|
|
1020
1327
|
updated_at: now,
|
|
@@ -1043,11 +1350,11 @@ export function registerTools(mcp) {
|
|
|
1043
1350
|
created_at: now,
|
|
1044
1351
|
updated_at: now,
|
|
1045
1352
|
};
|
|
1046
|
-
|
|
1353
|
+
addActorPayload(eventPayload, actor);
|
|
1047
1354
|
addRedactionPayload(eventPayload, redactedEvent);
|
|
1048
1355
|
try {
|
|
1049
1356
|
const eventVector = await embed(redactedEvent.text);
|
|
1050
|
-
await qdrantUpsert(eventId, eventVector, eventPayload);
|
|
1357
|
+
await qdrantUpsert(dest, eventId, eventVector, eventPayload);
|
|
1051
1358
|
}
|
|
1052
1359
|
catch (e) {
|
|
1053
1360
|
log("WARN", `Failed to record feedback_event: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -1056,6 +1363,7 @@ export function registerTools(mcp) {
|
|
|
1056
1363
|
content: [{ type: "text", text: JSON.stringify({
|
|
1057
1364
|
status: "marked_useful",
|
|
1058
1365
|
fact_id,
|
|
1366
|
+
destination: dest.name,
|
|
1059
1367
|
useful_count: newCount,
|
|
1060
1368
|
event_id: eventId,
|
|
1061
1369
|
}) }],
|
|
@@ -1074,19 +1382,18 @@ export function registerTools(mcp) {
|
|
|
1074
1382
|
fact_id: z.string().describe("ID of the fact whose outcome you are reporting."),
|
|
1075
1383
|
outcome: z.enum(["useful", "misleading", "irrelevant", "wrong"]).describe("How the fact actually played out. 'useful' = helped you finish the task; 'misleading' = sent you the wrong way; 'irrelevant' = semantically matched but didn't help; 'wrong' = factually incorrect."),
|
|
1076
1384
|
notes: z.string().optional().describe("Optional short context for the outcome (e.g. 'API moved in v2', 'wrong port number'). Stored on the telemetry event for future analysis."),
|
|
1077
|
-
workspace_id: z.string().optional().describe("
|
|
1078
|
-
}, async ({ fact_id, outcome, notes, workspace_id }) => {
|
|
1385
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1386
|
+
}, async ({ fact_id, outcome, notes, workspace_id: _workspace_id }) => {
|
|
1079
1387
|
const guard = requireReady();
|
|
1080
1388
|
if (guard)
|
|
1081
1389
|
return guard;
|
|
1082
1390
|
const now = nowISO();
|
|
1083
1391
|
try {
|
|
1084
|
-
const
|
|
1392
|
+
const located = await locatePoint(fact_id);
|
|
1393
|
+
if (!located)
|
|
1394
|
+
return notFoundResult(fact_id);
|
|
1395
|
+
const { dest } = located;
|
|
1085
1396
|
const actor = resolveActorIdentity({ config: loadConfig() });
|
|
1086
|
-
const target = await getPointForWorkspaceWrite(fact_id, scope);
|
|
1087
|
-
if (target.error) {
|
|
1088
|
-
return { content: [{ type: "text", text: JSON.stringify(target.error, null, 2) }], isError: true };
|
|
1089
|
-
}
|
|
1090
1397
|
const eventId = newId();
|
|
1091
1398
|
const eventContent = notes
|
|
1092
1399
|
? `Fact ${fact_id} outcome=${outcome}: ${notes}`
|
|
@@ -1109,14 +1416,15 @@ export function registerTools(mcp) {
|
|
|
1109
1416
|
created_at: now,
|
|
1110
1417
|
updated_at: now,
|
|
1111
1418
|
};
|
|
1112
|
-
|
|
1419
|
+
addActorPayload(eventPayload, actor);
|
|
1113
1420
|
addRedactionPayload(eventPayload, redactedEvent);
|
|
1114
1421
|
const eventVector = await embed(redactedEvent.text);
|
|
1115
|
-
await qdrantUpsert(eventId, eventVector, eventPayload);
|
|
1422
|
+
await qdrantUpsert(dest, eventId, eventVector, eventPayload);
|
|
1116
1423
|
return {
|
|
1117
1424
|
content: [{ type: "text", text: JSON.stringify({
|
|
1118
1425
|
status: "outcome_recorded",
|
|
1119
1426
|
fact_id,
|
|
1427
|
+
destination: dest.name,
|
|
1120
1428
|
outcome,
|
|
1121
1429
|
event_id: eventId,
|
|
1122
1430
|
}) }],
|
|
@@ -1138,16 +1446,24 @@ export function registerTools(mcp) {
|
|
|
1138
1446
|
workstream_key: z.string().optional().describe("Durable continuity key for a long-running objective (survives across sessions)."),
|
|
1139
1447
|
task_key: z.string().optional().describe("Task or issue key (e.g. GitHub issue number, JIRA key)."),
|
|
1140
1448
|
repo: z.string().optional().describe("Repository or project surface this summary relates to."),
|
|
1141
|
-
workspace_id: z.string().optional().describe("
|
|
1449
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1450
|
+
destination: z.string().optional().describe("Optional destination override. Omit to let routing rules decide."),
|
|
1142
1451
|
actor_id: z.string().optional().describe("Stable actor identity associated with this session summary. Overrides identity config/env/Git fallback."),
|
|
1143
|
-
}, async ({ content, entities, episode_id, workstream_key, task_key, repo, workspace_id, actor_id }) => {
|
|
1452
|
+
}, async ({ content, entities, episode_id, workstream_key, task_key, repo, workspace_id: _workspace_id, destination, actor_id }) => {
|
|
1144
1453
|
const guard = requireReady();
|
|
1145
1454
|
if (guard)
|
|
1146
1455
|
return guard;
|
|
1147
1456
|
lastStoreTime = Date.now();
|
|
1148
1457
|
const now = nowISO();
|
|
1149
1458
|
try {
|
|
1150
|
-
const
|
|
1459
|
+
const resolved = resolveDestOrError(routingInput({
|
|
1460
|
+
destination,
|
|
1461
|
+
content,
|
|
1462
|
+
entities: entities ?? [],
|
|
1463
|
+
}));
|
|
1464
|
+
if (resolved.error)
|
|
1465
|
+
return resolved.error;
|
|
1466
|
+
const dest = resolved.dest;
|
|
1151
1467
|
const actor = resolveActorIdentity({ actorId: actor_id, config: loadConfig() });
|
|
1152
1468
|
const normalizedEntities = (entities ?? []).map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
1153
1469
|
const summaryId = newId();
|
|
@@ -1180,14 +1496,14 @@ export function registerTools(mcp) {
|
|
|
1180
1496
|
payload["task_key"] = task_key;
|
|
1181
1497
|
if (repo)
|
|
1182
1498
|
payload["repo"] = repo;
|
|
1183
|
-
|
|
1499
|
+
addActorPayload(payload, actor);
|
|
1184
1500
|
addRedactionPayload(payload, redactedContent);
|
|
1185
|
-
await qdrantUpsert(summaryId, vector, payload);
|
|
1501
|
+
await qdrantUpsert(dest, summaryId, vector, payload);
|
|
1186
1502
|
return {
|
|
1187
1503
|
content: [{ type: "text", text: JSON.stringify({
|
|
1188
1504
|
status: "summary_stored",
|
|
1189
1505
|
summary_id: summaryId,
|
|
1190
|
-
|
|
1506
|
+
destination: dest.name,
|
|
1191
1507
|
actor_id: actor.actor_id,
|
|
1192
1508
|
}) }],
|
|
1193
1509
|
};
|
|
@@ -1207,27 +1523,35 @@ export function registerTools(mcp) {
|
|
|
1207
1523
|
supersedes: z.string().optional().describe("ID of an earlier distilled fact that this one replaces. Old fact is marked superseded and excluded from recall."),
|
|
1208
1524
|
task_key: z.string().optional().describe("Task or issue key associated with this learning, if relevant."),
|
|
1209
1525
|
repo: z.string().optional().describe("Repository or project surface this learning applies to."),
|
|
1210
|
-
workspace_id: z.string().optional().describe("
|
|
1526
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1527
|
+
destination: z.string().optional().describe("Optional destination override. Omit to let routing rules decide."),
|
|
1211
1528
|
actor_id: z.string().optional().describe("Stable actor identity associated with this distillation. Overrides identity config/env/Git fallback."),
|
|
1212
|
-
}, async ({ content, entities, supersedes, task_key, repo, workspace_id, actor_id }) => {
|
|
1529
|
+
}, async ({ content, entities, supersedes, task_key, repo, workspace_id: _workspace_id, destination, actor_id }) => {
|
|
1213
1530
|
const guard = requireReady();
|
|
1214
1531
|
if (guard)
|
|
1215
1532
|
return guard;
|
|
1216
1533
|
lastStoreTime = Date.now();
|
|
1217
1534
|
const now = nowISO();
|
|
1218
1535
|
try {
|
|
1219
|
-
const
|
|
1536
|
+
const resolved = resolveDestOrError(routingInput({
|
|
1537
|
+
destination,
|
|
1538
|
+
content,
|
|
1539
|
+
entities,
|
|
1540
|
+
}));
|
|
1541
|
+
if (resolved.error)
|
|
1542
|
+
return resolved.error;
|
|
1543
|
+
const dest = resolved.dest;
|
|
1220
1544
|
const actor = resolveActorIdentity({ actorId: actor_id, config: loadConfig() });
|
|
1221
1545
|
const normalizedEntities = entities.map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
1222
1546
|
const distilledId = newId();
|
|
1223
1547
|
const redactedContent = redactStorageText(content);
|
|
1224
1548
|
const vector = await embed(redactedContent.text);
|
|
1225
1549
|
if (supersedes) {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
await qdrantSetPayload([supersedes], {
|
|
1550
|
+
// Supersede may live in a different destination — locate it.
|
|
1551
|
+
const located = await locatePoint(supersedes);
|
|
1552
|
+
if (!located)
|
|
1553
|
+
return notFoundResult(supersedes);
|
|
1554
|
+
await qdrantSetPayload(located.dest, [supersedes], {
|
|
1231
1555
|
superseded_by: distilledId,
|
|
1232
1556
|
superseded_at: now,
|
|
1233
1557
|
});
|
|
@@ -1255,15 +1579,15 @@ export function registerTools(mcp) {
|
|
|
1255
1579
|
payload["task_key"] = task_key;
|
|
1256
1580
|
if (repo)
|
|
1257
1581
|
payload["repo"] = repo;
|
|
1258
|
-
|
|
1582
|
+
addActorPayload(payload, actor);
|
|
1259
1583
|
addRedactionPayload(payload, redactedContent);
|
|
1260
|
-
await qdrantUpsert(distilledId, vector, payload);
|
|
1584
|
+
await qdrantUpsert(dest, distilledId, vector, payload);
|
|
1261
1585
|
return {
|
|
1262
1586
|
content: [{ type: "text", text: JSON.stringify({
|
|
1263
1587
|
status: "distilled_stored",
|
|
1264
1588
|
distilled_id: distilledId,
|
|
1589
|
+
destination: dest.name,
|
|
1265
1590
|
supersedes: supersedes ?? null,
|
|
1266
|
-
workspace_id: scope.workspaceId,
|
|
1267
1591
|
actor_id: actor.actor_id,
|
|
1268
1592
|
}) }],
|
|
1269
1593
|
};
|
|
@@ -1282,26 +1606,37 @@ export function registerTools(mcp) {
|
|
|
1282
1606
|
fact_id: z.string().optional().describe("Fact ID to act on. Required for approve / reject / correct."),
|
|
1283
1607
|
reason: z.string().optional().describe("Required for action=reject. Short reason the fact is wrong."),
|
|
1284
1608
|
corrected_content: z.string().optional().describe("Required for action=correct. The fixed fact text. Stored as a new fact that supersedes the original."),
|
|
1285
|
-
workspace_id: z.string().optional().describe("
|
|
1286
|
-
include_legacy_workspace: z.boolean().optional().describe("
|
|
1287
|
-
}, async ({ limit, action, fact_id, reason, corrected_content, workspace_id, include_legacy_workspace }) => {
|
|
1609
|
+
workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1610
|
+
include_legacy_workspace: z.boolean().optional().describe("[Removed in v0.4.0] No-op."),
|
|
1611
|
+
}, async ({ limit, action, fact_id, reason, corrected_content, workspace_id: _workspace_id, include_legacy_workspace: _include_legacy_workspace }) => {
|
|
1288
1612
|
const guard = requireReady();
|
|
1289
1613
|
if (guard)
|
|
1290
1614
|
return guard;
|
|
1291
|
-
const scope = resolveScope(workspace_id, include_legacy_workspace);
|
|
1292
1615
|
if (action === "list") {
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1616
|
+
// List spans all destinations.
|
|
1617
|
+
const destinations = listDestinations();
|
|
1618
|
+
const allPoints = [];
|
|
1619
|
+
const filter = { must: [{ key: "source", match: { any: ["system", "daemon"] } }] };
|
|
1620
|
+
for (const dest of destinations) {
|
|
1621
|
+
try {
|
|
1622
|
+
const result = await qdrantScroll(dest, filter, (limit ?? 10) * 2);
|
|
1623
|
+
const points = result.result?.points ?? [];
|
|
1624
|
+
for (const pt of points)
|
|
1625
|
+
allPoints.push({ dest: dest.name, point: pt });
|
|
1626
|
+
}
|
|
1627
|
+
catch (e) {
|
|
1628
|
+
log("WARN", `memory_review list scroll failed on ${dest.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
const sorted = allPoints
|
|
1632
|
+
.sort((a, b) => (b.point.payload.created_at ?? "").localeCompare(a.point.payload.created_at ?? ""))
|
|
1298
1633
|
.slice(0, limit ?? 10);
|
|
1299
|
-
if (
|
|
1634
|
+
if (sorted.length === 0) {
|
|
1300
1635
|
return { content: [{ type: "text", text: "No system-captured facts found." }] };
|
|
1301
1636
|
}
|
|
1302
|
-
const lines =
|
|
1637
|
+
const lines = sorted.map(({ dest, point: pt }) => {
|
|
1303
1638
|
const p = pt.payload;
|
|
1304
|
-
return `[${p.category}] ${p.content}\n id: ${pt.id} | confidence: ${p.confidence} | importance: ${p.importance} | entities: ${(p.entities ?? []).join(", ")} | created: ${p.created_at}`;
|
|
1639
|
+
return `[${p.category}] ${p.content}\n id: ${pt.id} | dest: ${dest} | confidence: ${p.confidence} | importance: ${p.importance} | entities: ${(p.entities ?? []).join(", ")} | created: ${p.created_at}`;
|
|
1305
1640
|
});
|
|
1306
1641
|
return { content: [{ type: "text", text: lines.join("\n\n") }] };
|
|
1307
1642
|
}
|
|
@@ -1310,32 +1645,28 @@ export function registerTools(mcp) {
|
|
|
1310
1645
|
}
|
|
1311
1646
|
const now = nowISO();
|
|
1312
1647
|
if (action === "approve") {
|
|
1313
|
-
const
|
|
1314
|
-
if (
|
|
1315
|
-
return
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
if (approvePt) {
|
|
1320
|
-
currentCount = approvePt.payload.verification_count ?? 0;
|
|
1321
|
-
}
|
|
1322
|
-
await qdrantSetPayload([fact_id], {
|
|
1648
|
+
const located = await locatePoint(fact_id);
|
|
1649
|
+
if (!located)
|
|
1650
|
+
return notFoundResult(fact_id);
|
|
1651
|
+
const { dest, point } = located;
|
|
1652
|
+
const currentCount = point.payload.verification_count ?? 0;
|
|
1653
|
+
await qdrantSetPayload(dest, [fact_id], {
|
|
1323
1654
|
last_verified_at: now,
|
|
1324
1655
|
verification_count: currentCount + 1,
|
|
1325
1656
|
updated_at: now,
|
|
1326
1657
|
});
|
|
1327
|
-
return { content: [{ type: "text", text: JSON.stringify({ status: "approved", fact_id }) }] };
|
|
1658
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "approved", fact_id, destination: dest.name }) }] };
|
|
1328
1659
|
}
|
|
1329
1660
|
if (action === "reject") {
|
|
1330
1661
|
if (!reason) {
|
|
1331
1662
|
return { content: [{ type: "text", text: "Error: reason is required for reject action." }] };
|
|
1332
1663
|
}
|
|
1333
|
-
const
|
|
1334
|
-
if (
|
|
1335
|
-
return
|
|
1336
|
-
}
|
|
1664
|
+
const located = await locatePoint(fact_id);
|
|
1665
|
+
if (!located)
|
|
1666
|
+
return notFoundResult(fact_id);
|
|
1667
|
+
const { dest } = located;
|
|
1337
1668
|
const redactedReason = redactStorageText(reason);
|
|
1338
|
-
await qdrantSetPayload([fact_id], {
|
|
1669
|
+
await qdrantSetPayload(dest, [fact_id], {
|
|
1339
1670
|
superseded_by: `rejected:${redactedReason.text}`,
|
|
1340
1671
|
superseded_at: now,
|
|
1341
1672
|
updated_at: now,
|
|
@@ -1343,6 +1674,7 @@ export function registerTools(mcp) {
|
|
|
1343
1674
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
1344
1675
|
status: "rejected",
|
|
1345
1676
|
fact_id,
|
|
1677
|
+
destination: dest.name,
|
|
1346
1678
|
reason: redactedReason.text,
|
|
1347
1679
|
...(redactedReason.redacted ? { redaction: redactedReason } : {}),
|
|
1348
1680
|
}) }] };
|
|
@@ -1351,15 +1683,12 @@ export function registerTools(mcp) {
|
|
|
1351
1683
|
if (!corrected_content) {
|
|
1352
1684
|
return { content: [{ type: "text", text: "Error: corrected_content is required for correct action." }] };
|
|
1353
1685
|
}
|
|
1354
|
-
const
|
|
1355
|
-
if (
|
|
1356
|
-
return
|
|
1357
|
-
}
|
|
1358
|
-
const origPayload =
|
|
1686
|
+
const located = await locatePoint(fact_id);
|
|
1687
|
+
if (!located)
|
|
1688
|
+
return notFoundResult(fact_id);
|
|
1689
|
+
const { dest, point } = located;
|
|
1690
|
+
const origPayload = point.payload;
|
|
1359
1691
|
const redactedCorrected = redactStorageText(corrected_content);
|
|
1360
|
-
const correctionScope = origPayload?.workspace_id
|
|
1361
|
-
? resolveScope(origPayload.workspace_id, false)
|
|
1362
|
-
: scope;
|
|
1363
1692
|
const actor = resolveActorIdentity({ config: loadConfig() });
|
|
1364
1693
|
const vector = await embed(redactedCorrected.text);
|
|
1365
1694
|
const correctedId = crypto.randomUUID();
|
|
@@ -1390,15 +1719,15 @@ export function registerTools(mcp) {
|
|
|
1390
1719
|
updated_at: now,
|
|
1391
1720
|
metadata: { ...(origPayload?.metadata ?? {}), corrected_from: fact_id },
|
|
1392
1721
|
};
|
|
1393
|
-
|
|
1722
|
+
addActorPayload(correctedPayload, actor);
|
|
1394
1723
|
addRedactionPayload(correctedPayload, redactedCorrected);
|
|
1395
|
-
await qdrantUpsert(correctedId, vector, correctedPayload);
|
|
1396
|
-
await qdrantSetPayload([fact_id], {
|
|
1724
|
+
await qdrantUpsert(dest, correctedId, vector, correctedPayload);
|
|
1725
|
+
await qdrantSetPayload(dest, [fact_id], {
|
|
1397
1726
|
superseded_by: correctedId,
|
|
1398
1727
|
superseded_at: now,
|
|
1399
1728
|
updated_at: now,
|
|
1400
1729
|
});
|
|
1401
|
-
return { content: [{ type: "text", text: JSON.stringify({ status: "corrected", old_fact_id: fact_id, new_fact_id: correctedId }) }] };
|
|
1730
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "corrected", old_fact_id: fact_id, new_fact_id: correctedId, destination: dest.name }) }] };
|
|
1402
1731
|
}
|
|
1403
1732
|
return { content: [{ type: "text", text: `Unknown action: ${String(action)}` }] };
|
|
1404
1733
|
});
|
|
@@ -1415,8 +1744,7 @@ export function registerTools(mcp) {
|
|
|
1415
1744
|
if (heartbeatCount % 3 === 0 && ready) {
|
|
1416
1745
|
try {
|
|
1417
1746
|
const staleThreshold = new Date(Date.now() - STALENESS_DAYS * 86400000).toISOString();
|
|
1418
|
-
const
|
|
1419
|
-
const staleFilter = scopedFilter(scope) ?? { must: [] };
|
|
1747
|
+
const staleFilter = { must: [] };
|
|
1420
1748
|
staleFilter.must.push({ key: "category", match: { any: ["engineering", "product", "human", "system"] } });
|
|
1421
1749
|
staleFilter.should = [
|
|
1422
1750
|
{ key: "last_reinforced_at", range: { lte: staleThreshold } },
|
|
@@ -1425,10 +1753,24 @@ export function registerTools(mcp) {
|
|
|
1425
1753
|
staleFilter.must_not = [
|
|
1426
1754
|
{ key: "last_verified_at", range: { gte: staleThreshold } },
|
|
1427
1755
|
];
|
|
1428
|
-
|
|
1429
|
-
const staleFacts =
|
|
1430
|
-
|
|
1431
|
-
|
|
1756
|
+
// Aggregate stale facts across all destinations.
|
|
1757
|
+
const staleFacts = [];
|
|
1758
|
+
for (const dest of listDestinations()) {
|
|
1759
|
+
try {
|
|
1760
|
+
const r = await qdrantScroll(dest, staleFilter, 3);
|
|
1761
|
+
const pts = r.result?.points ?? [];
|
|
1762
|
+
for (const pt of pts)
|
|
1763
|
+
staleFacts.push(pt);
|
|
1764
|
+
if (staleFacts.length >= 3)
|
|
1765
|
+
break;
|
|
1766
|
+
}
|
|
1767
|
+
catch (e) {
|
|
1768
|
+
log("WARN", `Staleness check failed on ${dest.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
const trimmed = staleFacts.slice(0, 3);
|
|
1772
|
+
if (trimmed.length > 0) {
|
|
1773
|
+
const staleLines = trimmed.map((f) => {
|
|
1432
1774
|
const d = Math.round(daysSince(lastActivityDate(f.payload)));
|
|
1433
1775
|
return ` • [${f.payload.category}] ${f.payload.content} (${d}d old, id: ${f.id})`;
|
|
1434
1776
|
});
|