{"version":3,"file":"dense.d.ts","sourceRoot":"","sources":["../../../src/core/capabilities/dense.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,EAAE,KAAK,aAAa,EAAqB,MAAM,eAAe,CAAC;AAKtE,sEAAsE;AACtE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAsB,GAAG,MAAM,CAEzF;AAED,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,MAAM,UAAU,GACnB;IAAE,MAAM,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,GACtB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAOtC,wBAAgB,UAAU,IAAI,UAAU,CAEvC;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoDvG;AAED,8EAA8E;AAC9E,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,SAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAO5E;AAQD,8EAA8E;AAC9E,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAKvD","sourcesContent":["/**\n * Optional dense leg for capability retrieval.\n *\n * A separate embsearch store from the repo index, for two reasons that both had\n * to hold. The repo service goes **dormant below a byte threshold**, and a\n * capability index that stops working in a small repo is worse than none — the\n * whole point is finding tools, which have nothing to do with how much code is\n * checked out. And the lifecycles do not line up: the repo index invalidates on\n * file edits, this one on a server appearing or a plugin being installed.\n *\n * Everything here is best-effort. It never downloads the binary — it uses one\n * already on disk, and if there is none, retrieval is lexical-only and says so\n * rather than waiting. Nothing on this path may block a session or a tool call.\n *\n * See docs/plugin-system-architecture.md §6.2.\n */\n\nimport { join } from \"node:path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { getToolPath } from \"../../utils/tools-manager.js\";\nimport { EmbSearchClient } from \"../embsearch/client.js\";\nimport { type CapabilityDoc, capabilitySetHash } from \"./registry.js\";\n\n/** The Rust mock backend's model id — semantically meaningless, never index with it. */\nconst MOCK_MODEL_ID = \"mock-hash-v1\";\n\n/** Where a capability index for a given capability-set hash lives. */\nexport function capabilityStoreDir(hash: string, agentDir: string = getAgentDir()): string {\n\treturn join(agentDir, \"capability-index\", hash);\n}\n\nexport interface DenseHit {\n\tid: string;\n\tscore: number;\n}\n\n/**\n * State of the dense leg, as reported to callers so they can say *why* a search\n * was lexical-only rather than silently returning fewer results.\n */\nexport type DenseState =\n\t| { status: \"off\"; reason: string }\n\t| { status: \"building\" }\n\t| { status: \"ready\"; count: number };\n\nlet client: EmbSearchClient | undefined;\nlet state: DenseState = { status: \"off\", reason: \"not started\" };\nlet building: Promise<void> | undefined;\nlet indexedHash: string | undefined;\n\nexport function denseState(): DenseState {\n\treturn state;\n}\n\n/**\n * Bring the dense index up for `docs`, if it can be. Idempotent, and a no-op\n * when the same capability set is already indexed.\n *\n * Returns once the index is usable or has been ruled out. Callers that must not\n * wait should not await it — {@link denseSearch} works the moment it is ready\n * and returns nothing before that.\n */\nexport async function ensureDenseIndex(docs: readonly CapabilityDoc[], agentDir?: string): Promise<void> {\n\tconst hash = capabilitySetHash(docs);\n\tif (indexedHash === hash && state.status === \"ready\") return;\n\tif (building) return building;\n\n\tbuilding = (async () => {\n\t\ttry {\n\t\t\tconst binary = getToolPath(\"embsearch\");\n\t\t\tif (!binary) {\n\t\t\t\t// Deliberately not `ensureTool`: downloading a model runtime because\n\t\t\t\t// someone has MCP servers installed is not a trade the user agreed to.\n\t\t\t\tstate = { status: \"off\", reason: \"embsearch binary not installed\" };\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (docs.length === 0) {\n\t\t\t\tstate = { status: \"off\", reason: \"no capabilities registered\" };\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tstate = { status: \"building\" };\n\t\t\tawait closeClient();\n\t\t\tconst next = new EmbSearchClient({\n\t\t\t\tbinaryPath: binary,\n\t\t\t\tstorePath: capabilityStoreDir(hash, agentDir),\n\t\t\t\thybrid: false, // The lexical leg is ours; the daemon only provides dense.\n\t\t\t});\n\t\t\tconst info = await next.info();\n\t\t\tif (info.modelId === MOCK_MODEL_ID) {\n\t\t\t\t// The mock embedder returns hashes, not semantics. Indexing with it\n\t\t\t\t// would produce a store that looks healthy and ranks at random.\n\t\t\t\tawait next.close().catch(() => {});\n\t\t\t\tstate = { status: \"off\", reason: \"embsearch is using the mock embedder\" };\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// The store is keyed on the content hash, so a hit means these exact\n\t\t\t// documents were embedded before and nothing needs re-embedding.\n\t\t\tif (info.count < docs.length) {\n\t\t\t\tawait next.bulk(docs.map((d) => ({ id: d.id, text: `${d.name}. ${d.description}` })));\n\t\t\t\tawait next.save();\n\t\t\t}\n\t\t\tclient = next;\n\t\t\tindexedHash = hash;\n\t\t\tstate = { status: \"ready\", count: docs.length };\n\t\t} catch (error) {\n\t\t\tstate = { status: \"off\", reason: `embsearch unavailable: ${(error as Error).message}` };\n\t\t\tawait closeClient();\n\t\t} finally {\n\t\t\tbuilding = undefined;\n\t\t}\n\t})();\n\treturn building;\n}\n\n/** Top `k` dense hits, or nothing when the leg is not ready. Never throws. */\nexport async function denseSearch(query: string, k = 10): Promise<DenseHit[]> {\n\tif (!client || state.status !== \"ready\") return [];\n\ttry {\n\t\treturn await client.query(query, k, \"dense\");\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nasync function closeClient(): Promise<void> {\n\tconst current = client;\n\tclient = undefined;\n\tif (current) await current.close().catch(() => {});\n}\n\n/** Shut the daemon down and forget the index. Session teardown, and tests. */\nexport async function disposeDenseIndex(): Promise<void> {\n\tawait closeClient();\n\tindexedHash = undefined;\n\tbuilding = undefined;\n\tstate = { status: \"off\", reason: \"not started\" };\n}\n"]}