@zvec/zvec-grep 0.1.5 → 0.1.6

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.
Files changed (317) hide show
  1. package/README.md +88 -249
  2. package/README_CN.md +82 -246
  3. package/dist/authorization/index.d.ts +7 -0
  4. package/dist/authorization/index.js +7 -0
  5. package/dist/authorization/index.js.map +1 -0
  6. package/dist/authorization/manager.d.ts +8 -0
  7. package/dist/authorization/manager.js +19 -0
  8. package/dist/authorization/manager.js.map +1 -0
  9. package/dist/authorization/operation.d.ts +7 -0
  10. package/dist/authorization/operation.js +50 -0
  11. package/dist/authorization/operation.js.map +1 -0
  12. package/dist/authorization/planner.d.ts +21 -0
  13. package/dist/authorization/planner.js +97 -0
  14. package/dist/authorization/planner.js.map +1 -0
  15. package/dist/authorization/prompt.d.ts +11 -0
  16. package/dist/authorization/prompt.js +62 -0
  17. package/dist/authorization/prompt.js.map +1 -0
  18. package/dist/authorization/store.d.ts +20 -0
  19. package/dist/authorization/store.js +251 -0
  20. package/dist/authorization/store.js.map +1 -0
  21. package/dist/authorization/target.d.ts +15 -0
  22. package/dist/authorization/target.js +49 -0
  23. package/dist/authorization/target.js.map +1 -0
  24. package/dist/authorization/types.d.ts +61 -0
  25. package/dist/authorization/types.js +2 -0
  26. package/dist/authorization/types.js.map +1 -0
  27. package/dist/cli/args.d.ts +1 -1
  28. package/dist/cli/args.js +304 -54
  29. package/dist/cli/args.js.map +1 -1
  30. package/dist/cli/auth.d.ts +21 -0
  31. package/dist/cli/auth.js +212 -0
  32. package/dist/cli/auth.js.map +1 -0
  33. package/dist/cli/commands.js +433 -618
  34. package/dist/cli/commands.js.map +1 -1
  35. package/dist/cli/errors.js +2 -2
  36. package/dist/cli/errors.js.map +1 -1
  37. package/dist/cli/format/context.d.ts +1 -0
  38. package/dist/cli/format/context.js +241 -45
  39. package/dist/cli/format/context.js.map +1 -1
  40. package/dist/cli/format/debug.js +2 -2
  41. package/dist/cli/format/debug.js.map +1 -1
  42. package/dist/cli/format/progress.d.ts +4 -0
  43. package/dist/cli/format/progress.js +219 -46
  44. package/dist/cli/format/progress.js.map +1 -1
  45. package/dist/cli/format/status.d.ts +77 -6
  46. package/dist/cli/format/status.js +302 -106
  47. package/dist/cli/format/status.js.map +1 -1
  48. package/dist/cli/help.js +258 -59
  49. package/dist/cli/help.js.map +1 -1
  50. package/dist/cli/index.js +0 -0
  51. package/dist/cli/install.d.ts +4 -0
  52. package/dist/cli/install.js +845 -0
  53. package/dist/cli/install.js.map +1 -0
  54. package/dist/cli/managed-rg.d.ts +10 -0
  55. package/dist/cli/managed-rg.js +329 -0
  56. package/dist/cli/managed-rg.js.map +1 -0
  57. package/dist/cli/types.d.ts +20 -6
  58. package/dist/cli/types.js.map +1 -1
  59. package/dist/client/daemon-client.d.ts +19 -0
  60. package/dist/client/daemon-client.js +180 -0
  61. package/dist/client/daemon-client.js.map +1 -0
  62. package/dist/client/mode-router.d.ts +9 -0
  63. package/dist/client/mode-router.js +28 -0
  64. package/dist/client/mode-router.js.map +1 -0
  65. package/dist/client/search-policy.d.ts +11 -0
  66. package/dist/client/search-policy.js +15 -0
  67. package/dist/client/search-policy.js.map +1 -0
  68. package/dist/daemon/backend.d.ts +76 -0
  69. package/dist/daemon/backend.js +1049 -0
  70. package/dist/daemon/backend.js.map +1 -0
  71. package/dist/daemon/change-set.d.ts +25 -0
  72. package/dist/daemon/change-set.js +101 -0
  73. package/dist/daemon/change-set.js.map +1 -0
  74. package/dist/daemon/config.d.ts +24 -0
  75. package/dist/daemon/config.js +88 -0
  76. package/dist/daemon/config.js.map +1 -0
  77. package/dist/daemon/errors.d.ts +5 -0
  78. package/dist/daemon/errors.js +11 -0
  79. package/dist/daemon/errors.js.map +1 -0
  80. package/dist/daemon/http-server.d.ts +30 -0
  81. package/dist/daemon/http-server.js +287 -0
  82. package/dist/daemon/http-server.js.map +1 -0
  83. package/dist/daemon/index-coordinator.d.ts +23 -0
  84. package/dist/daemon/index-coordinator.js +68 -0
  85. package/dist/daemon/index-coordinator.js.map +1 -0
  86. package/dist/daemon/job-scheduler.d.ts +70 -0
  87. package/dist/daemon/job-scheduler.js +366 -0
  88. package/dist/daemon/job-scheduler.js.map +1 -0
  89. package/dist/daemon/logger.d.ts +9 -0
  90. package/dist/daemon/logger.js +53 -0
  91. package/dist/daemon/logger.js.map +1 -0
  92. package/dist/daemon/model-pool.d.ts +45 -0
  93. package/dist/daemon/model-pool.js +187 -0
  94. package/dist/daemon/model-pool.js.map +1 -0
  95. package/dist/daemon/root-lease.d.ts +14 -0
  96. package/dist/daemon/root-lease.js +188 -0
  97. package/dist/daemon/root-lease.js.map +1 -0
  98. package/dist/daemon/root-runtime.d.ts +78 -0
  99. package/dist/daemon/root-runtime.js +333 -0
  100. package/dist/daemon/root-runtime.js.map +1 -0
  101. package/dist/daemon/runtime-manager.d.ts +45 -0
  102. package/dist/daemon/runtime-manager.js +243 -0
  103. package/dist/daemon/runtime-manager.js.map +1 -0
  104. package/dist/daemon/runtime.d.ts +12 -0
  105. package/dist/daemon/runtime.js +102 -0
  106. package/dist/daemon/runtime.js.map +1 -0
  107. package/dist/daemon/server-controller.d.ts +39 -0
  108. package/dist/daemon/server-controller.js +203 -0
  109. package/dist/daemon/server-controller.js.map +1 -0
  110. package/dist/daemon/watch-manager.d.ts +50 -0
  111. package/dist/daemon/watch-manager.js +293 -0
  112. package/dist/daemon/watch-manager.js.map +1 -0
  113. package/dist/daemon/workspace-read-session-cache.d.ts +31 -0
  114. package/dist/daemon/workspace-read-session-cache.js +115 -0
  115. package/dist/daemon/workspace-read-session-cache.js.map +1 -0
  116. package/dist/engine/config.d.ts +30 -8
  117. package/dist/engine/config.js +171 -45
  118. package/dist/engine/config.js.map +1 -1
  119. package/dist/engine/{errors/index.d.ts → errors.d.ts} +6 -1
  120. package/dist/engine/{errors/details.js → errors.js} +17 -4
  121. package/dist/engine/errors.js.map +1 -0
  122. package/dist/engine/extraction/code/adapter.d.ts +1 -1
  123. package/dist/engine/extraction/code/extractor.d.ts +5 -11
  124. package/dist/engine/extraction/code/extractor.js +105 -44
  125. package/dist/engine/extraction/code/extractor.js.map +1 -1
  126. package/dist/engine/extraction/code/families/c-family.js +1 -1
  127. package/dist/engine/extraction/code/families/c-family.js.map +1 -1
  128. package/dist/engine/extraction/code/families/js-ts.d.ts +1 -1
  129. package/dist/engine/extraction/code/families/metadata.d.ts +1 -1
  130. package/dist/engine/extraction/code/tree-sitter/grammar.js.map +1 -0
  131. package/dist/engine/extraction/code/tree-sitter/nodes.js.map +1 -0
  132. package/dist/engine/extraction/code/tree-sitter/parser.js.map +1 -0
  133. package/dist/engine/extraction/image/extractor.d.ts +5 -0
  134. package/dist/engine/extraction/image/extractor.js +35 -0
  135. package/dist/engine/extraction/image/extractor.js.map +1 -0
  136. package/dist/engine/extraction/index.d.ts +3 -44
  137. package/dist/engine/extraction/index.js +1 -280
  138. package/dist/engine/extraction/index.js.map +1 -1
  139. package/dist/engine/extraction/markdown/extractor.d.ts +5 -11
  140. package/dist/engine/extraction/markdown/extractor.js +35 -16
  141. package/dist/engine/extraction/markdown/extractor.js.map +1 -1
  142. package/dist/engine/extraction/runtime.d.ts +4 -0
  143. package/dist/engine/extraction/runtime.js +26 -0
  144. package/dist/engine/extraction/runtime.js.map +1 -0
  145. package/dist/engine/extraction/source.d.ts +18 -0
  146. package/dist/engine/extraction/source.js +22 -0
  147. package/dist/engine/extraction/source.js.map +1 -0
  148. package/dist/engine/extraction/text/extractor.d.ts +7 -0
  149. package/dist/engine/extraction/text/extractor.js +168 -0
  150. package/dist/engine/extraction/text/extractor.js.map +1 -0
  151. package/dist/engine/extraction/types.d.ts +4 -0
  152. package/dist/engine/{models → extraction}/types.js.map +1 -1
  153. package/dist/engine/{files/file-type.d.ts → file-type.d.ts} +1 -1
  154. package/dist/engine/file-type.js.map +1 -0
  155. package/dist/engine/index-status.d.ts +9 -0
  156. package/dist/engine/index-status.js +38 -0
  157. package/dist/engine/index-status.js.map +1 -0
  158. package/dist/engine/manifest.d.ts +12 -0
  159. package/dist/engine/manifest.js +120 -0
  160. package/dist/engine/manifest.js.map +1 -0
  161. package/dist/engine/models/{providers/llama-cpp/embedding.d.ts → backends/llama-cpp.d.ts} +25 -20
  162. package/dist/engine/models/{providers/llama-cpp/embedding.js → backends/llama-cpp.js} +127 -106
  163. package/dist/engine/models/backends/llama-cpp.js.map +1 -0
  164. package/dist/engine/models/backends/model2vec.d.ts +57 -0
  165. package/dist/engine/models/backends/model2vec.js +332 -0
  166. package/dist/engine/models/backends/model2vec.js.map +1 -0
  167. package/dist/engine/models/backends/qwen.d.ts +44 -0
  168. package/dist/engine/models/{providers/qwen/embedding.js → backends/qwen.js} +155 -94
  169. package/dist/engine/models/backends/qwen.js.map +1 -0
  170. package/dist/engine/models/backends/transformers-js.d.ts +87 -0
  171. package/dist/engine/models/backends/transformers-js.js +316 -0
  172. package/dist/engine/models/backends/transformers-js.js.map +1 -0
  173. package/dist/engine/models/catalog.d.ts +188 -23
  174. package/dist/engine/models/catalog.js +172 -25
  175. package/dist/engine/models/catalog.js.map +1 -1
  176. package/dist/engine/models/download-progress.d.ts +15 -0
  177. package/dist/engine/models/download-progress.js +48 -0
  178. package/dist/engine/models/download-progress.js.map +1 -0
  179. package/dist/engine/models/embeddings.d.ts +58 -18
  180. package/dist/engine/models/embeddings.js +52 -28
  181. package/dist/engine/models/embeddings.js.map +1 -1
  182. package/dist/engine/models/factory.d.ts +2 -5
  183. package/dist/engine/models/factory.js +41 -56
  184. package/dist/engine/models/factory.js.map +1 -1
  185. package/dist/engine/models/index.d.ts +4 -6
  186. package/dist/engine/models/index.js +4 -4
  187. package/dist/engine/models/index.js.map +1 -1
  188. package/dist/engine/models/ranking.d.ts +7 -3
  189. package/dist/engine/models/resolution.d.ts +8 -0
  190. package/dist/engine/models/resolution.js +22 -0
  191. package/dist/engine/models/resolution.js.map +1 -0
  192. package/dist/engine/pipeline/indexing/index.d.ts +9 -7
  193. package/dist/engine/pipeline/indexing/index.js +360 -121
  194. package/dist/engine/pipeline/indexing/index.js.map +1 -1
  195. package/dist/engine/pipeline/indexing/root-paths.js +6 -6
  196. package/dist/engine/pipeline/indexing/root-paths.js.map +1 -1
  197. package/dist/engine/pipeline/indexing/scanner/index.d.ts +6 -1
  198. package/dist/engine/pipeline/indexing/scanner/index.js +233 -29
  199. package/dist/engine/pipeline/indexing/scanner/index.js.map +1 -1
  200. package/dist/engine/pipeline/search/index.d.ts +6 -6
  201. package/dist/engine/pipeline/search/index.js +8 -10
  202. package/dist/engine/pipeline/search/index.js.map +1 -1
  203. package/dist/engine/service/index.d.ts +3 -2
  204. package/dist/engine/service/index.js +1 -1
  205. package/dist/engine/service/index.js.map +1 -1
  206. package/dist/engine/service/root.d.ts +13 -12
  207. package/dist/engine/service/root.js +35 -24
  208. package/dist/engine/service/root.js.map +1 -1
  209. package/dist/engine/service/structure-enrichment.js +12 -14
  210. package/dist/engine/service/structure-enrichment.js.map +1 -1
  211. package/dist/engine/service/types.d.ts +32 -37
  212. package/dist/engine/service/workspace-index.d.ts +24 -0
  213. package/dist/engine/service/workspace-index.js +151 -0
  214. package/dist/engine/service/workspace-index.js.map +1 -0
  215. package/dist/engine/service/zvec-grep.d.ts +12 -1
  216. package/dist/engine/service/zvec-grep.js +677 -486
  217. package/dist/engine/service/zvec-grep.js.map +1 -1
  218. package/dist/engine/storage/index.d.ts +8 -4
  219. package/dist/engine/storage/index.js +1 -1
  220. package/dist/engine/storage/index.js.map +1 -1
  221. package/dist/engine/storage/zvec.d.ts +10 -33
  222. package/dist/engine/storage/zvec.js +95 -38
  223. package/dist/engine/storage/zvec.js.map +1 -1
  224. package/dist/engine/types.d.ts +15 -23
  225. package/dist/engine/types.js +1 -1
  226. package/dist/engine/types.js.map +1 -1
  227. package/dist/engine/utils/daemon-lease.d.ts +17 -0
  228. package/dist/engine/utils/daemon-lease.js +175 -0
  229. package/dist/engine/utils/daemon-lease.js.map +1 -0
  230. package/dist/engine/utils/file-selection.js +38 -1
  231. package/dist/engine/utils/file-selection.js.map +1 -1
  232. package/dist/engine/utils/lock.js +1 -1
  233. package/dist/engine/utils/lock.js.map +1 -1
  234. package/dist/index-progress.d.ts +10 -0
  235. package/dist/index-progress.js +159 -0
  236. package/dist/index-progress.js.map +1 -0
  237. package/dist/index.d.ts +4 -4
  238. package/dist/index.js +1 -1
  239. package/dist/index.js.map +1 -1
  240. package/dist/mcp/http-transport.d.ts +27 -0
  241. package/dist/mcp/http-transport.js +182 -0
  242. package/dist/mcp/http-transport.js.map +1 -0
  243. package/dist/mcp/input-normalization.d.ts +38 -0
  244. package/dist/mcp/input-normalization.js +95 -0
  245. package/dist/mcp/input-normalization.js.map +1 -0
  246. package/dist/mcp/progress-heartbeat.d.ts +17 -0
  247. package/dist/mcp/progress-heartbeat.js +46 -0
  248. package/dist/mcp/progress-heartbeat.js.map +1 -0
  249. package/dist/mcp/request-metadata.d.ts +2 -0
  250. package/dist/mcp/request-metadata.js +11 -0
  251. package/dist/mcp/request-metadata.js.map +1 -0
  252. package/dist/mcp/request-state.d.ts +40 -0
  253. package/dist/mcp/request-state.js +210 -0
  254. package/dist/mcp/request-state.js.map +1 -0
  255. package/dist/mcp/result-format.d.ts +13 -0
  256. package/dist/mcp/result-format.js +75 -0
  257. package/dist/mcp/result-format.js.map +1 -0
  258. package/dist/mcp/schemas.d.ts +430 -0
  259. package/dist/mcp/schemas.js +447 -0
  260. package/dist/mcp/schemas.js.map +1 -0
  261. package/dist/mcp/tools.d.ts +150 -0
  262. package/dist/mcp/tools.js +448 -0
  263. package/dist/mcp/tools.js.map +1 -0
  264. package/dist/mcp/toolset.d.ts +5 -0
  265. package/dist/mcp/toolset.js +12 -0
  266. package/dist/mcp/toolset.js.map +1 -0
  267. package/dist/observability/trace-context.d.ts +11 -0
  268. package/dist/observability/trace-context.js +138 -0
  269. package/dist/observability/trace-context.js.map +1 -0
  270. package/docs/01-agents.md +111 -0
  271. package/docs/02-cli.md +229 -0
  272. package/docs/03-mcp.md +167 -0
  273. package/docs/04-pipeline.md +218 -0
  274. package/docs/05-architecture.md +86 -0
  275. package/docs/06-server.md +210 -0
  276. package/docs/07-embedding.md +179 -0
  277. package/docs/08-roadmap.md +85 -0
  278. package/docs/README.md +53 -0
  279. package/package.json +17 -8
  280. package/skills/zvec-grep/SKILL.md +43 -133
  281. package/skills/zvec-grep/agents/openai.yaml +2 -2
  282. package/skills/zvec-grep/references/cli-fallback.md +85 -0
  283. package/dist/cli/mcp.d.ts +0 -2
  284. package/dist/cli/mcp.js +0 -354
  285. package/dist/cli/mcp.js.map +0 -1
  286. package/dist/engine/collection/index.d.ts +0 -55
  287. package/dist/engine/collection/index.js +0 -710
  288. package/dist/engine/collection/index.js.map +0 -1
  289. package/dist/engine/errors/details.d.ts +0 -6
  290. package/dist/engine/errors/details.js.map +0 -1
  291. package/dist/engine/errors/index.js +0 -16
  292. package/dist/engine/errors/index.js.map +0 -1
  293. package/dist/engine/extraction/tree-sitter/grammar.js.map +0 -1
  294. package/dist/engine/extraction/tree-sitter/nodes.js.map +0 -1
  295. package/dist/engine/extraction/tree-sitter/parser.js.map +0 -1
  296. package/dist/engine/files/file-type.js.map +0 -1
  297. package/dist/engine/models/providers/index.d.ts +0 -2
  298. package/dist/engine/models/providers/index.js +0 -3
  299. package/dist/engine/models/providers/index.js.map +0 -1
  300. package/dist/engine/models/providers/llama-cpp/embedding.js.map +0 -1
  301. package/dist/engine/models/providers/llama-cpp/index.d.ts +0 -1
  302. package/dist/engine/models/providers/llama-cpp/index.js +0 -2
  303. package/dist/engine/models/providers/llama-cpp/index.js.map +0 -1
  304. package/dist/engine/models/providers/qwen/embedding.d.ts +0 -42
  305. package/dist/engine/models/providers/qwen/embedding.js.map +0 -1
  306. package/dist/engine/models/providers/qwen/index.d.ts +0 -1
  307. package/dist/engine/models/providers/qwen/index.js +0 -2
  308. package/dist/engine/models/providers/qwen/index.js.map +0 -1
  309. package/dist/engine/models/types.d.ts +0 -30
  310. /package/dist/engine/extraction/{tree-sitter → code/tree-sitter}/grammar.d.ts +0 -0
  311. /package/dist/engine/extraction/{tree-sitter → code/tree-sitter}/grammar.js +0 -0
  312. /package/dist/engine/extraction/{tree-sitter → code/tree-sitter}/nodes.d.ts +0 -0
  313. /package/dist/engine/extraction/{tree-sitter → code/tree-sitter}/nodes.js +0 -0
  314. /package/dist/engine/extraction/{tree-sitter → code/tree-sitter}/parser.d.ts +0 -0
  315. /package/dist/engine/extraction/{tree-sitter → code/tree-sitter}/parser.js +0 -0
  316. /package/dist/engine/{models → extraction}/types.js +0 -0
  317. /package/dist/engine/{files/file-type.js → file-type.js} +0 -0
@@ -1,27 +1,87 @@
1
- import { mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
2
- import { createHash } from "node:crypto";
3
- import { dirname, join, relative, resolve } from "node:path";
4
- import { globalConfigPath, readGlobalConfig, } from "../config.js";
5
- import { Collection, CollectionRegistry, isCollectionIndexed, } from "../collection/index.js";
6
- import { collectionDetail, detail, EngineError, errorDetails, } from "../errors/index.js";
7
- import { createEmbeddingModel, createEmbeddingModelFromReference, } from "../models/index.js";
8
- import { ANONYMOUS_COLLECTION_NAME, anonymousCollectionPath, anonymousHome, anonymousIndexLocation, resolveZvecGrepRoot, } from "./root.js";
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
3
+ import { basename, dirname, join, relative, resolve } from "node:path";
4
+ import { globalConfigPath, readGlobalConfig, resolveEmbeddingRuntimeOptions, } from "../config.js";
5
+ import { isWorkspaceIndexed, WorkspaceIndex } from "./workspace-index.js";
6
+ import { workspaceIndexDetail, detail, EngineError, errorDetails, } from "../errors.js";
7
+ import { createEmbeddingModel, EmbeddingPurpose, resolveEmbeddingReference, } from "../models/index.js";
8
+ import { CURRENT_INDEX_VERSION } from "../types.js";
9
+ import { indexStatusNeedsRefresh } from "../index-status.js";
10
+ import { workspaceIndexInfoFromManifest, CURRENT_MANIFEST_VERSION, readWorkspaceManifest, writeWorkspaceManifest, } from "../manifest.js";
11
+ import { validateRootPaths } from "../pipeline/indexing/root-paths.js";
12
+ import { findNearestWorkspace, hasWorkspaceIndex, resetWorkspaceIndexStorage, resolveZvecGrepRoot, workspaceHome, workspaceIndexLocation, } from "./root.js";
9
13
  import { runRgSearch } from "./lexical.js";
10
14
  import { defaultHome, normalizePath } from "../utils/path.js";
11
15
  import { acquireReadWriteLock, assertNoWriteLock, } from "../utils/lock.js";
16
+ import { assertDaemonWriteAllowed } from "../utils/daemon-lease.js";
12
17
  import { TimingCollector } from "../utils/timing.js";
13
18
  import { enrichLexicalItemsWithStructure } from "./structure-enrichment.js";
19
+ import { remoteEmbeddingAuthorizationGuard } from "../../authorization/operation.js";
20
+ import { RemoteEmbeddingAuthorizationStore } from "../../authorization/store.js";
14
21
  const DEFAULT_CONTEXT_LIMIT = 10;
15
22
  const DEFAULT_CONTEXT_TOTAL_LIMIT = 30;
16
23
  const DEFAULT_LOCAL_EMBEDDING = "local/embeddinggemma-300m";
24
+ const PROVIDER_API_KEY_IDENTITY_SECRET = randomBytes(32);
17
25
  const MAX_RECOVERED_EMBEDDING_MODELS = 4;
18
26
  export async function createZvecGrep(options = {}) {
19
27
  return new ZvecGrepService(options);
20
28
  }
29
+ export function openWorkspaceReadSession(startRoot, embeddingModel) {
30
+ const start = resolveZvecGrepRoot(startRoot);
31
+ assertNearestWorkspaceHomeUnlocked(start, "daemon.context.open");
32
+ const nearest = findNearestWorkspaceIndex(start);
33
+ if (!nearest) {
34
+ throw workspaceIndexMissingError(start, "undecided");
35
+ }
36
+ const { location, info } = nearest;
37
+ if (info.indexPolicy === "disabled") {
38
+ throw workspaceIndexDisabledError(location.root);
39
+ }
40
+ if (!isWorkspaceIndexed(info) || !hasWorkspaceIndex(location)) {
41
+ throw workspaceIndexMissingError(location.root, info.indexPolicy ?? "enabled");
42
+ }
43
+ const workspaceIndex = new WorkspaceIndex(info, embeddingModel, true, location.filesPath);
44
+ let closed = false;
45
+ return {
46
+ root: location.root,
47
+ async context(options) {
48
+ if (closed) {
49
+ throw new EngineError("Workspace read session is already closed", {
50
+ code: "ZVEC_GREP.ENGINE.SERVICE.READ_SESSION_CLOSED",
51
+ });
52
+ }
53
+ const timings = new TimingCollector();
54
+ const request = normalizeContextRequest(options);
55
+ const result = await timings.time("total", () => withHomeReadLock(location.home, "daemon.context", () => contextFromOpenWorkspaceIndex({
56
+ root: location.root,
57
+ request,
58
+ workspaceIndex,
59
+ options: { ...options, autoUpdate: false },
60
+ timings,
61
+ })));
62
+ return withContextTimings(result, timings);
63
+ },
64
+ async close() {
65
+ if (closed) {
66
+ return;
67
+ }
68
+ workspaceIndex.close();
69
+ closed = true;
70
+ },
71
+ };
72
+ }
73
+ export function createEmbeddingModelForIdentity(identity, options = {}, workspaceRuntime = {}) {
74
+ const reference = embeddingModelReference(identity);
75
+ return createServiceEmbeddingModel(reference, providerOptions(options, identity, readGlobalConfig(), workspaceRuntime), options);
76
+ }
77
+ export function embeddingModelPoolKeyForIdentity(identity, options = {}, workspaceRuntime = {}) {
78
+ const reference = embeddingModelReference(identity);
79
+ const fingerprint = providerOptionsFingerprint(providerOptions(options, identity, readGlobalConfig(), workspaceRuntime));
80
+ return [reference, fingerprint].join("/");
81
+ }
21
82
  class ZvecGrepService {
22
83
  options;
23
84
  root;
24
- collections;
25
85
  embeddingModel;
26
86
  recoveredEmbeddingModels = new Map();
27
87
  retiredEmbeddingModels = new Set();
@@ -31,23 +91,21 @@ class ZvecGrepService {
31
91
  this.options = options;
32
92
  this.root = resolveZvecGrepRoot(options.root);
33
93
  this.embeddingModel = options.embeddingModel;
34
- this.collections = {
35
- list: () => this.listCollections(),
36
- info: (name) => this.collectionInfo(name),
37
- status: (name) => this.collectionStatus(name),
38
- index: (name, paths, indexOptions) => this.indexCollection(name, paths, indexOptions),
39
- remove: (name) => this.removeCollection(name),
40
- };
41
94
  }
42
95
  async index(options = {}) {
43
96
  this.ensureOpen();
44
97
  const root = resolveZvecGrepRoot(options.root ?? this.root);
45
- const location = anonymousIndexLocation(root);
46
- return await this.withEmbeddingModelOperation(() => withHomeWriteLock(location.home, options.rebuild ? "index.rebuild" : "index", async () => {
47
- const existing = readCollectionInfo(location.home, ANONYMOUS_COLLECTION_NAME);
48
- const embeddingModel = this.embeddingModelForIndex(existing, location.home, "index");
49
- const registry = new CollectionRegistry(location.home, embeddingModel);
50
- try {
98
+ const daemonWritePermit = assertDaemonWriteAllowed(root, this.options.daemonInstanceToken);
99
+ const location = workspaceIndexLocation(root);
100
+ try {
101
+ return await this.withEmbeddingModelOperation(() => withHomeWriteLock(location.home, options.rebuild ? "index.rebuild" : "index", async () => {
102
+ const existing = readWorkspaceManifest(location.home);
103
+ const existingRuntime = existing?.embeddingRuntime ?? {};
104
+ const embeddingModel = this.embeddingModelForIndex(existing, "index", existingRuntime);
105
+ const effectiveRuntime = effectiveEmbeddingRuntime(this.options, embeddingModel, runtimeForModelProvider(existing, embeddingModel, existingRuntime));
106
+ if (!options.rebuild) {
107
+ assertWorkspaceEndpointMatchesCurrentRuntime(existing, existingRuntime, effectiveRuntime, "zg index --endpoint <url> --rebuild");
108
+ }
51
109
  const rootPaths = resolveIndexRootPaths(existing, options.rootPaths, root, {
52
110
  resetPaths: options.resetPaths === true,
53
111
  includePaths: options.includePaths,
@@ -63,56 +121,100 @@ class ZvecGrepService {
63
121
  maxFileSizeBytes: options.maxFileSizeBytes,
64
122
  follow: options.follow,
65
123
  });
66
- if (options.rebuild) {
67
- registry.remove(ANONYMOUS_COLLECTION_NAME);
124
+ if (options.rebuild || !isWorkspaceIndexed(existing)) {
125
+ resetWorkspaceIndexStorage(location);
68
126
  }
69
- const existingAfterRebuild = registry.get(ANONYMOUS_COLLECTION_NAME);
70
- if (isCollectionIndexed(existingAfterRebuild)) {
71
- assertCollectionEmbeddingMatchesCurrentModel(existingAfterRebuild, embeddingModel, "zg index --rebuild");
127
+ const existingAfterRebuild = options.rebuild ? null : existing;
128
+ if (isWorkspaceIndexed(existingAfterRebuild)) {
129
+ assertWorkspaceEmbeddingMatchesCurrentModel(existingAfterRebuild, embeddingModel, "zg index --rebuild");
72
130
  }
73
- registry.prepareIndex(ANONYMOUS_COLLECTION_NAME, rootPaths, anonymousCollectionPath(root));
74
- return await registry.open(ANONYMOUS_COLLECTION_NAME).index({
75
- rebuild: false,
76
- embeddingConcurrency: options.embeddingConcurrency,
77
- onProgress: options.onProgress,
78
- });
79
- }
80
- finally {
81
- registry.close();
82
- }
83
- }));
131
+ const manifest = prepareWorkspaceManifest(location, existingAfterRebuild, rootPaths, embeddingModel, existingRuntime);
132
+ const workspaceIndex = new WorkspaceIndex(manifest, embeddingModel, false, location.filesPath);
133
+ writeWorkspaceManifest(location.home, manifest);
134
+ try {
135
+ const releaseWriterContext = options.onWriterContext?.((contextOptions) => this.contextFromWriterWorkspaceIndex(root, workspaceIndex, contextOptions));
136
+ try {
137
+ const result = await workspaceIndex.index({
138
+ rebuild: false,
139
+ embeddingConcurrency: options.embeddingConcurrency,
140
+ onProgress: options.onProgress,
141
+ changedPaths: options.changedPaths,
142
+ signal: options.signal,
143
+ });
144
+ writeWorkspaceManifest(location.home, {
145
+ ...manifest,
146
+ embeddingRuntime: embeddingRuntimeAfterSuccessfulIndex(existing, existingRuntime, embeddingModel, effectiveRuntime, this.options),
147
+ updatedTime: Date.now(),
148
+ });
149
+ return result;
150
+ }
151
+ catch (error) {
152
+ writeWorkspaceManifest(location.home, {
153
+ ...manifest,
154
+ embeddingRuntime: existingRuntime,
155
+ updatedTime: Date.now(),
156
+ });
157
+ throw error;
158
+ }
159
+ finally {
160
+ await releaseWriterContext?.();
161
+ }
162
+ }
163
+ finally {
164
+ workspaceIndex.close();
165
+ }
166
+ }));
167
+ }
168
+ finally {
169
+ daemonWritePermit?.release();
170
+ }
84
171
  }
85
172
  async dropIndex(options = {}) {
86
173
  this.ensureOpen();
87
174
  const root = resolveZvecGrepRoot(options.root ?? this.root);
88
- const location = anonymousIndexLocation(root);
89
- if (!readCollectionInfo(location.home, ANONYMOUS_COLLECTION_NAME)) {
90
- return false;
91
- }
92
- return await withHomeWriteLock(location.home, "index.drop", async () => {
93
- const registry = new CollectionRegistry(location.home, undefined);
94
- try {
95
- return registry.remove(ANONYMOUS_COLLECTION_NAME);
96
- }
97
- finally {
98
- registry.close();
175
+ const daemonWritePermit = assertDaemonWriteAllowed(root, this.options.daemonInstanceToken);
176
+ const location = workspaceIndexLocation(root);
177
+ try {
178
+ if (!readWorkspaceManifest(location.home)) {
179
+ return false;
99
180
  }
100
- });
181
+ return await withHomeWriteLock(location.home, "index.drop", async () => {
182
+ resetWorkspaceIndexStorage(location);
183
+ return true;
184
+ });
185
+ }
186
+ finally {
187
+ daemonWritePermit?.release();
188
+ }
101
189
  }
102
190
  async disableIndex(options = {}) {
103
191
  this.ensureOpen();
104
192
  const root = resolveZvecGrepRoot(options.root ?? this.root);
105
- const location = anonymousIndexLocation(root);
106
- await withHomeWriteLock(location.home, "index.disable", async () => {
107
- const registry = new CollectionRegistry(location.home, undefined);
108
- try {
109
- registry.disableIndex(ANONYMOUS_COLLECTION_NAME, [root], location.collectionPath);
110
- }
111
- finally {
112
- registry.close();
113
- }
114
- });
115
- return this.info({ root });
193
+ const daemonWritePermit = assertDaemonWriteAllowed(root, this.options.daemonInstanceToken);
194
+ const location = workspaceIndexLocation(root);
195
+ try {
196
+ await withHomeWriteLock(location.home, "index.disable", async () => {
197
+ const existing = readWorkspaceManifest(location.home);
198
+ const now = Date.now();
199
+ writeWorkspaceManifest(location.home, {
200
+ manifestVersion: CURRENT_MANIFEST_VERSION,
201
+ id: existing?.id ?? randomUUID(),
202
+ name: existing?.name ?? workspaceDisplayName(root),
203
+ path: location.home,
204
+ rootPaths: existing?.rootPaths ?? validateRootPaths([root]),
205
+ indexPolicy: "disabled",
206
+ embedding: null,
207
+ indexVersion: null,
208
+ createdTime: existing?.createdTime ?? now,
209
+ updatedTime: now,
210
+ embeddingRuntime: existing?.embeddingRuntime ?? {},
211
+ });
212
+ });
213
+ return this.info({ root });
214
+ }
215
+ finally {
216
+ daemonWritePermit?.release();
217
+ }
116
218
  }
117
219
  async context(options) {
118
220
  this.ensureOpen();
@@ -124,76 +226,73 @@ class ZvecGrepService {
124
226
  }
125
227
  async contextWithTimings(options, timings) {
126
228
  const request = normalizeContextRequest(options);
127
- if (options.collection) {
128
- return this.contextFromNamedCollection(request, options, timings);
129
- }
130
229
  const startRoot = resolveZvecGrepRoot(options.root ?? this.root);
131
230
  if (options.rg) {
132
231
  return this.contextFromRg(startRoot, request, options, timings);
133
232
  }
134
- assertNearestAnonymousHomeUnlocked(startRoot, "context");
135
- const nearest = findNearestAnonymousCollection(startRoot);
233
+ assertNearestWorkspaceHomeUnlocked(startRoot, "context");
234
+ const nearest = findNearestWorkspaceIndex(startRoot);
136
235
  if (nearest) {
137
236
  const { location, info } = nearest;
138
237
  if (info.indexPolicy === "disabled") {
139
- throw anonymousIndexDisabledError(location.root);
238
+ throw workspaceIndexDisabledError(location.root);
140
239
  }
141
- if (!isCollectionIndexed(info)) {
142
- throw anonymousIndexMissingError(location.root, info.indexPolicy ?? "enabled");
240
+ if (!isWorkspaceIndexed(info) || !hasWorkspaceIndex(location)) {
241
+ throw workspaceIndexMissingError(location.root, info.indexPolicy ?? "enabled");
143
242
  }
144
243
  if (options.autoUpdate !== false) {
145
- await this.refreshAnonymousIndexForContext(location, options, timings);
244
+ await this.refreshWorkspaceIndexForContext(location, options, timings);
146
245
  }
147
- return await withHomeReadLock(location.home, "context", () => this.contextFromAnonymousIndex(location, request, options, timings));
246
+ return await withHomeReadLock(location.home, "context", () => this.contextFromWorkspaceIndex(location, request, options, timings));
148
247
  }
149
- throw anonymousIndexMissingError(startRoot, "undecided");
248
+ throw workspaceIndexMissingError(startRoot, "undecided");
150
249
  }
151
250
  async info(options = {}) {
152
251
  this.ensureOpen();
153
252
  const startRoot = resolveZvecGrepRoot(options.root ?? this.root);
154
- assertNearestAnonymousHomeUnlocked(startRoot, "info");
155
- const nearest = findNearestAnonymousCollection(startRoot);
253
+ assertNearestWorkspaceHomeUnlocked(startRoot, "info");
254
+ const nearest = findNearestWorkspaceIndex(startRoot);
156
255
  if (!nearest) {
157
- const location = anonymousIndexLocation(startRoot);
256
+ const location = workspaceIndexLocation(startRoot);
158
257
  return {
159
258
  root: startRoot,
160
259
  indexed: false,
161
260
  indexPolicy: "undecided",
162
261
  home: location.home,
163
- indexPath: location.collectionPath,
262
+ indexPath: location.indexPath,
164
263
  source: "unindexed",
165
264
  suggestion: "zg index or zg query --rg",
166
265
  };
167
266
  }
168
267
  return await withHomeReadLock(nearest.location.home, "info", async () => {
169
- const registry = new CollectionRegistry(nearest.location.home, undefined, true);
170
- try {
171
- const collection = registry.get(ANONYMOUS_COLLECTION_NAME);
172
- const indexed = collection !== null &&
173
- collection.indexPolicy !== "disabled" &&
174
- isCollectionIndexed(collection);
175
- return {
176
- root: nearest.location.root,
177
- indexed,
178
- indexPolicy: collection?.indexPolicy ?? "undecided",
179
- home: nearest.location.home,
180
- indexPath: nearest.location.collectionPath,
181
- source: indexed ? "index" : "unindexed",
182
- collection: collection ?? undefined,
183
- status: indexed
184
- ? await registry.status(ANONYMOUS_COLLECTION_NAME)
185
- : null,
186
- suggestion: anonymousInfoSuggestion(collection),
187
- };
188
- }
189
- finally {
190
- registry.close();
191
- }
268
+ const workspaceIndex = readWorkspaceManifest(nearest.location.home);
269
+ const indexed = workspaceIndex !== null &&
270
+ workspaceIndex.indexPolicy !== "disabled" &&
271
+ isWorkspaceIndexed(workspaceIndex) &&
272
+ hasWorkspaceIndex(nearest.location);
273
+ return {
274
+ root: nearest.location.root,
275
+ indexed,
276
+ indexPolicy: workspaceIndex?.indexPolicy ?? "undecided",
277
+ home: nearest.location.home,
278
+ indexPath: nearest.location.indexPath,
279
+ source: indexed ? "index" : "unindexed",
280
+ workspaceIndex: workspaceIndex
281
+ ? workspaceIndexInfoFromManifest(workspaceIndex)
282
+ : undefined,
283
+ status: indexed && options.includeStatus !== false
284
+ ? await workspaceIndexStatus(workspaceIndex, nearest.location)
285
+ : null,
286
+ suggestion: workspaceInfoSuggestion(workspaceIndex),
287
+ };
192
288
  });
193
289
  }
194
290
  async close() {
195
291
  const models = new Set([
196
- ...(this.embeddingModel ? [this.embeddingModel] : []),
292
+ ...(this.embeddingModel &&
293
+ this.options.embeddingModelOwnership !== "borrowed"
294
+ ? [this.embeddingModel]
295
+ : []),
197
296
  ...this.recoveredEmbeddingModels.values(),
198
297
  ...this.retiredEmbeddingModels,
199
298
  ]);
@@ -204,248 +303,83 @@ class ZvecGrepService {
204
303
  }
205
304
  this.closed = true;
206
305
  }
207
- async listCollections() {
208
- this.ensureOpen();
209
- return await withHomeReadLock(serviceHome(this.options), "collections.list", async () => {
210
- const registry = this.createRegistry(true);
211
- try {
212
- return registry.list();
213
- }
214
- finally {
215
- registry.close();
216
- }
217
- });
218
- }
219
- async collectionInfo(name) {
220
- this.ensureOpen();
221
- return await withHomeReadLock(serviceHome(this.options), "collections.info", async () => {
222
- const registry = this.createRegistry(true);
223
- try {
224
- return registry.get(name);
225
- }
226
- finally {
227
- registry.close();
228
- }
229
- });
230
- }
231
- async collectionStatus(name) {
232
- this.ensureOpen();
233
- return await withHomeReadLock(serviceHome(this.options), "collections.status", async () => {
234
- const registry = this.createRegistry(true);
235
- try {
236
- return await registry.status(name);
237
- }
238
- finally {
239
- registry.close();
240
- }
241
- });
242
- }
243
- async indexCollection(name, paths, options = {}) {
244
- this.ensureOpen();
245
- const home = serviceHome(this.options);
246
- return await this.withEmbeddingModelOperation(() => withHomeWriteLock(home, options.rebuild ? "collections.index.rebuild" : "collections.index", async () => {
247
- const existing = readCollectionInfo(home, name);
248
- const embeddingModel = this.embeddingModelForIndex(existing, home, "collections.index");
249
- const registry = this.createRegistry(false, embeddingModel);
250
- try {
251
- const requestedRootPaths = paths === undefined
252
- ? undefined
253
- : Array.isArray(paths)
254
- ? paths
255
- : [paths];
256
- const rootPaths = resolveIndexRootPaths(existing, requestedRootPaths, this.root, {
257
- resetPaths: options.resetPaths === true,
258
- includePaths: options.includePaths,
259
- excludePaths: options.excludePaths,
260
- globs: options.globs,
261
- insensitiveGlobs: options.insensitiveGlobs,
262
- fileTypes: options.fileTypes,
263
- excludedFileTypes: options.excludedFileTypes,
264
- hidden: options.hidden,
265
- noIgnore: options.noIgnore,
266
- ignoreFiles: options.ignoreFiles,
267
- maxDepth: options.maxDepth,
268
- maxFileSizeBytes: options.maxFileSizeBytes,
269
- follow: options.follow,
270
- });
271
- if (options.rebuild) {
272
- registry.remove(name);
273
- }
274
- const existingAfterRebuild = registry.get(name);
275
- if (isCollectionIndexed(existingAfterRebuild)) {
276
- assertCollectionEmbeddingMatchesCurrentModel(existingAfterRebuild, embeddingModel, "zg collections index <name> --rebuild");
277
- }
278
- registry.prepareIndex(name, rootPaths);
279
- return await registry.open(name).index({
280
- embeddingConcurrency: options.embeddingConcurrency,
281
- onProgress: options.onProgress,
282
- });
283
- }
284
- finally {
285
- registry.close();
286
- }
287
- }));
288
- }
289
- async removeCollection(name) {
290
- this.ensureOpen();
291
- return await withHomeWriteLock(serviceHome(this.options), "collections.remove", async () => {
292
- const registry = this.createRegistry(false);
293
- try {
294
- return registry.remove(name);
295
- }
296
- finally {
297
- registry.close();
298
- }
299
- });
300
- }
301
- async contextFromAnonymousIndex(location, request, options, timings) {
302
- const registry = new CollectionRegistry(location.home, undefined, true);
306
+ async contextFromWorkspaceIndex(location, request, options, timings) {
307
+ const info = readWorkspaceManifest(location.home);
308
+ if (!info) {
309
+ throw new EngineError("Workspace index manifest not found", {
310
+ code: "ZVEC_GREP.ENGINE.MANIFEST.NOT_FOUND",
311
+ });
312
+ }
313
+ const workspaceIndex = this.openWorkspaceIndexForSearch(info, request, location, info.embeddingRuntime);
303
314
  try {
304
- const info = registry.get(ANONYMOUS_COLLECTION_NAME);
305
- if (!info) {
306
- throw new EngineError("Collection not found", {
307
- code: "ZVEC_GREP.ENGINE.COLLECTION.NOT_FOUND",
308
- });
309
- }
310
- const collection = this.openCollectionForSearch(info, request, location.home);
311
- try {
312
- return await this.contextFromCollection({
313
- root: location.root,
314
- request,
315
- collection,
316
- anonymous: true,
317
- options,
318
- timings,
319
- });
320
- }
321
- finally {
322
- collection.close();
323
- }
315
+ return await contextFromOpenWorkspaceIndex({
316
+ root: location.root,
317
+ request,
318
+ workspaceIndex,
319
+ options,
320
+ timings,
321
+ });
324
322
  }
325
323
  finally {
326
- registry.close();
324
+ workspaceIndex.close();
327
325
  }
328
326
  }
329
- async refreshAnonymousIndexForContext(location, options, timings) {
330
- const needsRefresh = await timings.time("status_scan", () => withHomeReadLock(location.home, "context.status", async () => {
331
- const registry = new CollectionRegistry(location.home, undefined, true);
332
- try {
333
- const status = await registry.status(ANONYMOUS_COLLECTION_NAME);
334
- return status ? collectionIndexStatusNeedsRefresh(status) : false;
335
- }
336
- finally {
337
- registry.close();
338
- }
339
- }));
340
- if (!needsRefresh) {
341
- return;
342
- }
343
- await timings.time("auto_update", () => withHomeWriteLock(location.home, "context.refresh", async () => {
344
- const existing = readCollectionInfo(location.home, ANONYMOUS_COLLECTION_NAME);
345
- if (!existing) {
346
- return;
347
- }
348
- const stillNeedsRefresh = await timings.time("refresh_status_scan", () => collectionNeedsRefresh(location.home, ANONYMOUS_COLLECTION_NAME));
349
- if (!stillNeedsRefresh) {
327
+ async refreshWorkspaceIndexForContext(location, options, timings) {
328
+ const daemonWritePermit = assertDaemonWriteAllowed(location.root, this.options.daemonInstanceToken);
329
+ try {
330
+ const needsRefresh = await timings.time("status_scan", () => withHomeReadLock(location.home, "context.status", async () => {
331
+ const manifest = readWorkspaceManifest(location.home);
332
+ const status = manifest
333
+ ? await workspaceIndexStatus(manifest, location)
334
+ : null;
335
+ return indexStatusNeedsRefresh(status);
336
+ }));
337
+ if (!needsRefresh) {
350
338
  return;
351
339
  }
352
- const embeddingModel = this.embeddingModelForIndex(existing, location.home, "context.refresh");
353
- assertCollectionEmbeddingMatchesCurrentModel(existing, embeddingModel, "zg index --rebuild");
354
- const registry = new CollectionRegistry(location.home, embeddingModel);
355
- try {
356
- const result = await registry.open(ANONYMOUS_COLLECTION_NAME).index({
357
- embeddingConcurrency: options.embeddingConcurrency,
358
- onProgress: options.onAutoUpdateProgress,
359
- });
360
- timings.addEntries(result.timings, "auto_update_");
361
- }
362
- finally {
363
- registry.close();
364
- }
365
- }));
366
- }
367
- async contextFromNamedCollection(request, options, timings) {
368
- const collectionName = options.collection;
369
- return await withHomeReadLock(serviceHome(this.options), "collection.context", async () => {
370
- const registry = this.createRegistry(true, undefined);
371
- try {
372
- const info = registry.get(collectionName);
373
- if (!info) {
374
- throw new EngineError("Collection not found", {
375
- code: "ZVEC_GREP.ENGINE.COLLECTION.NOT_FOUND",
376
- context: errorDetails([collectionDetail(collectionName)]),
377
- });
340
+ await timings.time("auto_update", () => withHomeWriteLock(location.home, "context.refresh", async () => {
341
+ const existing = readWorkspaceManifest(location.home);
342
+ if (!existing) {
343
+ return;
344
+ }
345
+ const stillNeedsRefresh = await timings.time("refresh_status_scan", () => workspaceIndexNeedsRefresh(location));
346
+ if (!stillNeedsRefresh) {
347
+ return;
378
348
  }
379
- const collection = this.openCollectionForSearch(info, request, registry.home);
380
- const root = collection.info.rootPaths[0]?.absolutePath ??
381
- resolveZvecGrepRoot(options.root ?? this.root);
349
+ const workspaceRuntime = existing.embeddingRuntime;
350
+ const embeddingModel = this.embeddingModelForIndex(existing, "context.refresh", workspaceRuntime);
351
+ assertWorkspaceEndpointMatchesCurrentRuntime(existing, workspaceRuntime, effectiveEmbeddingRuntime(this.options, embeddingModel, workspaceRuntime), "zg index --endpoint <url> --rebuild");
352
+ assertWorkspaceEmbeddingMatchesCurrentModel(existing, embeddingModel, "zg index --rebuild");
353
+ const workspaceIndex = new WorkspaceIndex(existing, embeddingModel, false, location.filesPath);
382
354
  try {
383
- return await this.contextFromCollection({
384
- root,
385
- request,
386
- collection,
387
- anonymous: false,
388
- options: {
389
- ...options,
390
- },
391
- timings,
355
+ const result = await workspaceIndex.index({
356
+ embeddingConcurrency: options.embeddingConcurrency,
357
+ onProgress: options.onAutoUpdateProgress,
392
358
  });
359
+ timings.addEntries(result.timings, "auto_update_");
393
360
  }
394
361
  finally {
395
- collection.close();
362
+ workspaceIndex.close();
396
363
  }
397
- }
398
- finally {
399
- registry.close();
400
- }
401
- });
402
- }
403
- async contextFromCollection(input) {
404
- const searches = [];
405
- const groups = input.options.fuse
406
- ? [{ routes: input.request.routes }]
407
- : input.request.groups;
408
- const limit = contextGroupLimit(input.options.limit, groups.length);
409
- for (const group of groups) {
410
- const search = await input.collection.searchPlan({
411
- routes: group.routes,
412
- limit,
413
- trace: input.options.trace,
414
- preferSymbol: input.options.preferSymbol,
415
- symbolTypes: input.options.symbolTypes,
416
- includePaths: input.options.includePaths,
417
- excludePaths: input.options.excludePaths,
418
- globs: input.options.globs,
419
- insensitiveGlobs: input.options.insensitiveGlobs,
420
- fileTypes: input.options.fileTypes,
421
- excludedFileTypes: input.options.excludedFileTypes,
422
- modifiedAfter: input.options.modifiedAfter,
423
- modifiedBefore: input.options.modifiedBefore,
424
- });
425
- input.timings.addEntries(search.timings);
426
- searches.push(search);
364
+ }));
427
365
  }
428
- const items = dedupeAndRerankContextItems(searches.flatMap((search) => searchPlanToContextItems(search, input.root)));
429
- return {
430
- query: input.request.displayQuery,
431
- root: input.root,
432
- source: "index",
433
- coverage: "ranked_sample",
434
- collection: {
435
- id: input.collection.info.id,
436
- name: input.collection.info.name,
437
- path: input.collection.info.path,
438
- anonymous: input.anonymous,
439
- },
440
- items,
441
- diagnostics: {
442
- emptyReason: items.length === 0 ? "no_matches" : undefined,
443
- index: {
444
- hitsReturned: items.length,
445
- routes: searches.flatMap((search) => search.plan.routes),
446
- },
447
- },
448
- };
366
+ finally {
367
+ daemonWritePermit?.release();
368
+ }
369
+ }
370
+ async contextFromWriterWorkspaceIndex(root, workspaceIndex, options) {
371
+ return await this.withEmbeddingModelOperation(async () => {
372
+ const timings = new TimingCollector();
373
+ const request = normalizeContextRequest(options);
374
+ const result = await timings.time("total", () => contextFromOpenWorkspaceIndex({
375
+ root,
376
+ request,
377
+ workspaceIndex,
378
+ options: { ...options, autoUpdate: false },
379
+ timings,
380
+ }));
381
+ return withContextTimings(result, timings);
382
+ });
449
383
  }
450
384
  async contextFromRg(root, request, options, timings) {
451
385
  let rgResult;
@@ -497,60 +431,68 @@ class ZvecGrepService {
497
431
  },
498
432
  };
499
433
  }
500
- createRegistry(readOnly, embeddingModel = this.embeddingModel) {
501
- return new CollectionRegistry(this.options.home, embeddingModel, readOnly);
502
- }
503
- openCollectionForSearch(info, request, registryHome) {
504
- return new Collection(info, this.embeddingModelForSearch(indexedEmbeddingSchema(info), request, registryHome), true, join(registryHome, "files.zvec"));
434
+ openWorkspaceIndexForSearch(info, request, location, workspaceRuntime) {
435
+ return new WorkspaceIndex(info, this.embeddingModelForSearch(indexedEmbeddingSchema(info), request, workspaceRuntime, info), true, location.filesPath);
505
436
  }
506
- embeddingModelForSearch(schema, request, registryHome) {
437
+ embeddingModelForSearch(schema, request, workspaceRuntime, info) {
507
438
  if (!request.routes.some((route) => route.mode === "vector")) {
508
439
  return undefined;
509
440
  }
441
+ const identity = {
442
+ provider: schema.provider,
443
+ name: schema.model,
444
+ };
445
+ const effectiveRuntime = resolveEmbeddingRuntimeOptions(embeddingModelReference(identity), this.options, workspaceRuntime, readGlobalConfig());
446
+ assertSearchEndpointMatchesWorkspace(info, workspaceRuntime, effectiveRuntime);
510
447
  if (this.embeddingModel) {
511
448
  return this.embeddingModel;
512
449
  }
513
- return this.recoverEmbeddingModel(schema, registryHome);
450
+ return this.recoverEmbeddingModel(schema, workspaceRuntime);
514
451
  }
515
- embeddingModelForIndex(existing, registryHome, operation) {
516
- if (isCollectionIndexed(existing) &&
452
+ embeddingModelForIndex(existing, operation, workspaceRuntime = {}) {
453
+ if (isWorkspaceIndexed(existing) &&
517
454
  !this.embeddingModel &&
518
455
  !this.options.embedding) {
519
- return this.recoverEmbeddingModel(existing.embedding, registryHome);
456
+ return this.recoverEmbeddingModel(existing.embedding, workspaceRuntime);
520
457
  }
521
- const reference = this.options.embedding ??
522
- (this.options.defaultEmbedding === true
458
+ const config = readGlobalConfig();
459
+ const reference = resolveEmbeddingReference({
460
+ explicit: this.options.embedding,
461
+ globalDefault: config.defaults?.embedding,
462
+ fallback: this.options.defaultEmbedding === true
523
463
  ? DEFAULT_LOCAL_EMBEDDING
524
- : undefined);
464
+ : undefined,
465
+ });
466
+ const referenceIdentity = reference
467
+ ? parseEmbeddingModelReference(reference)
468
+ : undefined;
469
+ const selectedWorkspaceRuntime = isWorkspaceIndexed(existing) &&
470
+ referenceIdentity &&
471
+ existing.embedding.provider !== referenceIdentity.provider
472
+ ? {}
473
+ : workspaceRuntime;
525
474
  return (this.embeddingModel ??
526
475
  (reference
527
- ? this.embeddingModelFromReference(reference, registryHome)
476
+ ? this.embeddingModelFromReference(reference, config, selectedWorkspaceRuntime)
528
477
  : undefined) ??
529
- this.configuredEmbeddingModel(registryHome) ??
530
478
  this.requireEmbeddingModel(operation));
531
479
  }
532
- recoverEmbeddingModel(schema, registryHome) {
480
+ recoverEmbeddingModel(schema, workspaceRuntime = {}) {
533
481
  const config = readGlobalConfig();
534
- const options = providerOptions(this.options, this.root, registryHome, schema.provider, config);
535
- const key = `${schema.provider}/${schema.model}/${providerOptionsFingerprint(options)}`;
536
- return this.cachedEmbeddingModel(key, () => createEmbeddingModel({
482
+ const identity = {
537
483
  provider: schema.provider,
538
- model: schema.model,
539
- }, options));
540
- }
541
- configuredEmbeddingModel(registryHome) {
542
- const config = readGlobalConfig();
543
- const reference = config.defaults?.embedding;
544
- if (!reference) {
545
- return undefined;
546
- }
547
- return this.embeddingModelFromReference(reference, registryHome, config);
548
- }
549
- embeddingModelFromReference(reference, registryHome, config = readGlobalConfig()) {
550
- const provider = providerFromReference(reference);
551
- const options = providerOptions(this.options, this.root, registryHome, provider, config);
484
+ name: schema.model,
485
+ };
486
+ const reference = embeddingModelReference(identity);
487
+ const options = providerOptions(this.options, identity, config, workspaceRuntime);
488
+ const key = `${reference}/${providerOptionsFingerprint(options)}`;
489
+ return this.cachedEmbeddingModel(key, () => createServiceEmbeddingModel(reference, options, this.options));
490
+ }
491
+ embeddingModelFromReference(reference, config = readGlobalConfig(), workspaceRuntime = {}) {
492
+ const identity = parseEmbeddingModelReference(reference);
493
+ const options = providerOptions(this.options, identity, config, workspaceRuntime);
552
494
  const key = `configured/${reference}/${providerOptionsFingerprint(options)}`;
553
- return this.cachedEmbeddingModel(key, () => createEmbeddingModelFromReference(reference, options));
495
+ return this.cachedEmbeddingModel(key, () => createServiceEmbeddingModel(reference, options, this.options));
554
496
  }
555
497
  cachedEmbeddingModel(key, create) {
556
498
  const cached = this.recoveredEmbeddingModels.get(key);
@@ -617,6 +559,66 @@ class ZvecGrepService {
617
559
  }
618
560
  }
619
561
  }
562
+ async function contextFromOpenWorkspaceIndex(input) {
563
+ const searches = [];
564
+ const groups = input.options.fuse
565
+ ? [
566
+ {
567
+ id: "Q1",
568
+ query: input.request.displayQuery,
569
+ role: input.request.groups.some((group) => group.role === "primary")
570
+ ? "primary"
571
+ : "supplemental",
572
+ routes: input.request.routes,
573
+ },
574
+ ]
575
+ : input.request.groups;
576
+ const limit = contextGroupLimit(input.options.limit, groups.length);
577
+ for (const group of groups) {
578
+ const search = await input.workspaceIndex.searchPlan({
579
+ routes: group.routes,
580
+ limit,
581
+ trace: input.options.trace,
582
+ preferSymbol: input.options.preferSymbol,
583
+ symbolTypes: input.options.symbolTypes,
584
+ includePaths: input.options.includePaths,
585
+ excludePaths: input.options.excludePaths,
586
+ globs: input.options.globs,
587
+ insensitiveGlobs: input.options.insensitiveGlobs,
588
+ fileTypes: input.options.fileTypes,
589
+ excludedFileTypes: input.options.excludedFileTypes,
590
+ modifiedAfter: input.options.modifiedAfter,
591
+ modifiedBefore: input.options.modifiedBefore,
592
+ });
593
+ input.timings.addEntries(search.timings);
594
+ searches.push(search);
595
+ }
596
+ const items = selectAndRankContextItems(searches.flatMap((search, index) => searchPlanToContextItems(search, input.root, groups[index])), groups.filter((group) => group.role === "primary").map((group) => group.id));
597
+ return {
598
+ query: input.request.displayQuery,
599
+ root: input.root,
600
+ source: "index",
601
+ coverage: "ranked_sample",
602
+ workspaceIndex: {
603
+ id: input.workspaceIndex.info.id,
604
+ name: input.workspaceIndex.info.name,
605
+ path: input.workspaceIndex.info.path,
606
+ },
607
+ items,
608
+ diagnostics: {
609
+ emptyReason: items.length === 0 ? "no_matches" : undefined,
610
+ index: {
611
+ hitsReturned: items.length,
612
+ queryGroups: groups.map((group) => ({
613
+ id: group.id,
614
+ query: group.query,
615
+ role: group.role,
616
+ })),
617
+ routes: searches.flatMap((search) => search.plan.routes),
618
+ },
619
+ },
620
+ };
621
+ }
620
622
  async function withHomeReadLock(home, operation, task) {
621
623
  const lock = acquireHomeLock(home, "read", operation);
622
624
  try {
@@ -635,22 +637,12 @@ async function withHomeWriteLock(home, operation, task) {
635
637
  lock.release();
636
638
  }
637
639
  }
638
- async function collectionNeedsRefresh(home, collectionName) {
639
- const registry = new CollectionRegistry(home, undefined, true);
640
- try {
641
- const status = await registry.status(collectionName);
642
- return status ? collectionIndexStatusNeedsRefresh(status) : false;
643
- }
644
- finally {
645
- registry.close();
646
- }
647
- }
648
- function collectionIndexStatusNeedsRefresh(status) {
649
- return (status.filesAdded > 0 ||
650
- status.filesModified > 0 ||
651
- status.filesDeleted > 0 ||
652
- status.filesPending > 0 ||
653
- status.filesFailed > 0);
640
+ async function workspaceIndexNeedsRefresh(location) {
641
+ const manifest = readWorkspaceManifest(location.home);
642
+ const status = manifest
643
+ ? await workspaceIndexStatus(manifest, location)
644
+ : null;
645
+ return indexStatusNeedsRefresh(status);
654
646
  }
655
647
  function acquireHomeLock(home, mode, operation) {
656
648
  return acquireReadWriteLock(homeLockPath(home), mode, { operation });
@@ -658,10 +650,10 @@ function acquireHomeLock(home, mode, operation) {
658
650
  function assertHomeUnlocked(home, operation) {
659
651
  assertNoWriteLock(homeLockPath(home), operation);
660
652
  }
661
- function assertNearestAnonymousHomeUnlocked(start, operation) {
653
+ function assertNearestWorkspaceHomeUnlocked(start, operation) {
662
654
  let current = resolve(start);
663
655
  while (true) {
664
- assertHomeUnlocked(anonymousHome(current), operation);
656
+ assertHomeUnlocked(workspaceHome(current), operation);
665
657
  const parent = dirname(current);
666
658
  if (parent === current) {
667
659
  return;
@@ -672,9 +664,6 @@ function assertNearestAnonymousHomeUnlocked(start, operation) {
672
664
  function homeLockPath(home) {
673
665
  return join(home, "locks", "home");
674
666
  }
675
- function serviceHome(options) {
676
- return options.home ?? defaultHome();
677
- }
678
667
  function rgEmptyReason(diagnostics) {
679
668
  return diagnostics.missingPaths &&
680
669
  diagnostics.missingPaths.length > 0 &&
@@ -696,36 +685,28 @@ function withContextTimings(result, timings) {
696
685
  },
697
686
  };
698
687
  }
699
- function findNearestAnonymousCollection(start) {
700
- let current = resolve(start);
701
- while (true) {
702
- const location = anonymousIndexLocation(current);
703
- const info = readCollectionInfo(location.home, ANONYMOUS_COLLECTION_NAME);
704
- if (info) {
705
- return { location, info };
706
- }
707
- const parent = dirname(current);
708
- if (parent === current) {
709
- return null;
710
- }
711
- current = parent;
712
- }
688
+ function findNearestWorkspaceIndex(start) {
689
+ const location = findNearestWorkspace(start);
690
+ if (!location)
691
+ return null;
692
+ const info = readWorkspaceManifest(location.home);
693
+ return info ? { location, info } : null;
713
694
  }
714
- function anonymousInfoSuggestion(collection) {
715
- if (!collection) {
695
+ function workspaceInfoSuggestion(workspaceIndex) {
696
+ if (!workspaceIndex) {
716
697
  return "zg index or zg query --rg";
717
698
  }
718
- if (collection.indexPolicy === "disabled") {
699
+ if (workspaceIndex.indexPolicy === "disabled") {
719
700
  return "zg query --rg";
720
701
  }
721
- if (!isCollectionIndexed(collection)) {
702
+ if (!isWorkspaceIndexed(workspaceIndex)) {
722
703
  return "zg index";
723
704
  }
724
705
  return undefined;
725
706
  }
726
- function anonymousIndexMissingError(root, policy) {
727
- return new EngineError("No anonymous zvec-grep index found for this workspace", {
728
- code: "ZVEC_GREP.ENGINE.SERVICE.ANONYMOUS_INDEX_NOT_FOUND",
707
+ function workspaceIndexMissingError(root, policy) {
708
+ return new EngineError("No zvec-grep index found for this workspace", {
709
+ code: "ZVEC_GREP.ENGINE.SERVICE.WORKSPACE_INDEX_NOT_FOUND",
729
710
  context: errorDetails([
730
711
  detail("root", root),
731
712
  detail("policy", policy),
@@ -738,9 +719,9 @@ function anonymousIndexMissingError(root, policy) {
738
719
  ]),
739
720
  });
740
721
  }
741
- function anonymousIndexDisabledError(root) {
742
- return new EngineError("Anonymous zvec-grep index is disabled for this workspace", {
743
- code: "ZVEC_GREP.ENGINE.SERVICE.ANONYMOUS_INDEX_DISABLED",
722
+ function workspaceIndexDisabledError(root) {
723
+ return new EngineError("The zvec-grep index is disabled for this workspace", {
724
+ code: "ZVEC_GREP.ENGINE.SERVICE.WORKSPACE_INDEX_DISABLED",
744
725
  context: errorDetails([
745
726
  detail("root", root),
746
727
  detail("policy", "disabled"),
@@ -836,117 +817,216 @@ function applyRootPathOverrides(rootPath, options) {
836
817
  follow: options.follow ?? normalized.follow,
837
818
  };
838
819
  }
839
- function readCollectionInfo(home, name) {
840
- const registry = new CollectionRegistry(home, undefined, true);
820
+ function prepareWorkspaceManifest(location, existing, rootPaths, embeddingModel, embeddingRuntime) {
821
+ const now = Date.now();
822
+ return {
823
+ manifestVersion: CURRENT_MANIFEST_VERSION,
824
+ id: existing?.id ?? randomUUID(),
825
+ name: existing?.name ?? workspaceDisplayName(location.root),
826
+ path: location.home,
827
+ rootPaths: validateRootPaths(rootPaths),
828
+ indexPolicy: "enabled",
829
+ embedding: currentEmbeddingSchema(embeddingModel),
830
+ indexVersion: CURRENT_INDEX_VERSION,
831
+ createdTime: existing?.createdTime ?? now,
832
+ updatedTime: now,
833
+ embeddingRuntime,
834
+ };
835
+ }
836
+ function currentEmbeddingSchema(embeddingModel) {
837
+ return {
838
+ provider: embeddingModel.info.provider,
839
+ model: embeddingModel.info.name,
840
+ dimension: embeddingModel.info.dimension,
841
+ metric: embeddingModel.info.metric,
842
+ };
843
+ }
844
+ function workspaceDisplayName(root) {
845
+ return basename(normalizePath(root)) || "workspace";
846
+ }
847
+ async function workspaceIndexStatus(info, location) {
848
+ if (info.indexPolicy === "disabled" ||
849
+ !isWorkspaceIndexed(info) ||
850
+ !hasWorkspaceIndex(location)) {
851
+ return null;
852
+ }
853
+ const workspaceIndex = new WorkspaceIndex(info, undefined, true, location.filesPath);
841
854
  try {
842
- return registry.get(name);
855
+ return await workspaceIndex.status();
843
856
  }
844
857
  finally {
845
- registry.close();
858
+ workspaceIndex.close();
846
859
  }
847
860
  }
848
861
  function indexedEmbeddingSchema(info) {
849
- if (isCollectionIndexed(info)) {
862
+ if (isWorkspaceIndexed(info)) {
850
863
  return info.embedding;
851
864
  }
852
865
  throw new EngineError("zvec-grep index has not been built", {
853
866
  code: "ZVEC_GREP.ENGINE.SERVICE.INDEX_MISSING",
854
867
  context: errorDetails([
855
- collectionDetail(info.name),
868
+ workspaceIndexDetail(info.name),
856
869
  detail("hint", "Run zg index to build this index."),
857
870
  ]),
858
871
  });
859
872
  }
860
- function providerOptions(options, root, registryHome, provider, config = readGlobalConfig()) {
861
- const providerConfig = provider ? config.providers?.[provider] : undefined;
873
+ function providerOptions(options, identity, config = readGlobalConfig(), workspaceRuntime = {}) {
874
+ const reference = embeddingModelReference(identity);
875
+ const runtime = resolveEmbeddingRuntimeOptions(reference, options, workspaceRuntime, config);
862
876
  return {
863
- apiKey: options.apiKey ?? providerConfig?.apiKey ?? "",
864
- endpoint: options.endpoint ?? providerConfig?.endpoint,
877
+ apiKey: runtime.apiKey,
878
+ endpoint: runtime.endpoint,
865
879
  modelCacheDir: options.modelCacheDir ??
866
- process.env.ZVEC_GREP_MODEL_CACHE ??
867
880
  config.defaults?.modelCacheDir ??
868
- modelCacheDir(options, root, registryHome),
869
- llamaGpu: options.llamaGpu ?? config.defaults?.llamaGpu,
870
- embeddingParallelism: options.embeddingParallelism ?? config.defaults?.embeddingParallelism,
881
+ process.env.ZVEC_GREP_MODEL_CACHE ??
882
+ join(options.home ?? defaultHome(), "models"),
883
+ device: runtime.device,
871
884
  };
872
885
  }
873
- function providerFromReference(reference) {
886
+ function createServiceEmbeddingModel(reference, modelOptions, serviceOptions) {
887
+ const model = createEmbeddingModel(reference, modelOptions);
888
+ if (model.info.provider !== "qwen") {
889
+ return model;
890
+ }
891
+ const endpoint = model.info.endpoint;
892
+ if (endpoint === undefined) {
893
+ throw new EngineError("Remote embedding model did not provide an endpoint", {
894
+ code: "ZVEC_GREP.ENGINE.MODELS.REMOTE_ENDPOINT_NOT_IMPLEMENTED",
895
+ context: `model=${model.info.reference}`,
896
+ });
897
+ }
898
+ const authorize = remoteEmbeddingAuthorizationGuard({
899
+ store: new RemoteEmbeddingAuthorizationStore({
900
+ signingKeyPath: serviceOptions.authorizationSigningKeyPath,
901
+ }),
902
+ });
903
+ const authorizedModel = {
904
+ info: model.info,
905
+ async embed(contents, embedOptions) {
906
+ await authorize({
907
+ provider: model.info.provider,
908
+ model: model.info.name,
909
+ endpoint,
910
+ purpose: embedOptions?.purpose ?? EmbeddingPurpose.Document,
911
+ contentKinds: contents.map((content) => content.kind),
912
+ contentCount: contents.length,
913
+ });
914
+ return await model.embed(contents, embedOptions);
915
+ },
916
+ dispose: () => model.dispose(),
917
+ };
918
+ return authorizedModel;
919
+ }
920
+ function parseEmbeddingModelReference(reference) {
874
921
  const separator = reference.indexOf("/");
875
- return separator > 0 ? reference.slice(0, separator) : undefined;
922
+ if (separator <= 0 || separator === reference.length - 1) {
923
+ throw new EngineError("Embedding model reference is invalid", {
924
+ code: "ZVEC_GREP.ENGINE.MODELS.EMBEDDING_INVALID_REFERENCE",
925
+ context: `reference=${reference}`,
926
+ });
927
+ }
928
+ return {
929
+ provider: reference.slice(0, separator),
930
+ name: reference.slice(separator + 1),
931
+ };
932
+ }
933
+ function embeddingModelReference(identity) {
934
+ return `${identity.provider}/${identity.name}`;
876
935
  }
877
936
  function providerOptionsFingerprint(options) {
878
- return createHash("sha256")
879
- .update(JSON.stringify([
880
- options.apiKey,
881
- options.endpoint,
882
- options.modelCacheDir,
883
- options.llamaGpu,
884
- options.embeddingParallelism,
885
- ]))
937
+ const publicOptionsFingerprint = createHash("sha256")
938
+ .update(JSON.stringify([options.endpoint, options.modelCacheDir, options.device]))
939
+ .digest("hex");
940
+ return `${providerApiKeyIdentity(options.apiKey)}:${publicOptionsFingerprint}`;
941
+ }
942
+ function providerApiKeyIdentity(apiKey) {
943
+ return createHmac("sha256", PROVIDER_API_KEY_IDENTITY_SECRET)
944
+ .update(apiKey)
886
945
  .digest("hex");
887
946
  }
888
- function assertCollectionEmbeddingMatchesCurrentModel(info, model, rebuildCommand) {
889
- if (!isCollectionIndexed(info)) {
947
+ function effectiveEmbeddingRuntime(options, model, workspaceRuntime) {
948
+ const resolved = resolveEmbeddingRuntimeOptions(model.info.reference, options, workspaceRuntime, readGlobalConfig());
949
+ return {
950
+ apiKey: resolved.apiKey,
951
+ ...((model.info.endpoint ?? resolved.endpoint)
952
+ ? { endpoint: model.info.endpoint ?? resolved.endpoint }
953
+ : {}),
954
+ ...(model.info.provider === "local"
955
+ ? { device: resolved.device ?? "auto" }
956
+ : {}),
957
+ };
958
+ }
959
+ function runtimeForModelProvider(existing, model, workspaceRuntime) {
960
+ return isWorkspaceIndexed(existing) &&
961
+ existing.embedding.provider !== model.info.provider
962
+ ? {}
963
+ : workspaceRuntime;
964
+ }
965
+ function embeddingRuntimeAfterSuccessfulIndex(existing, existingRuntime, model, effectiveRuntime, explicit) {
966
+ const sameProvider = isWorkspaceIndexed(existing) &&
967
+ existing.embedding.provider === model.info.provider;
968
+ const apiKey = model.info.provider === "local"
969
+ ? undefined
970
+ : (explicit.apiKey ??
971
+ (sameProvider ? existingRuntime.apiKey : undefined));
972
+ return {
973
+ ...(apiKey !== undefined ? { apiKey } : {}),
974
+ ...(model.info.provider !== "local" &&
975
+ effectiveRuntime.endpoint !== undefined
976
+ ? { endpoint: effectiveRuntime.endpoint }
977
+ : {}),
978
+ ...(model.info.provider === "local"
979
+ ? { device: effectiveRuntime.device ?? "auto" }
980
+ : {}),
981
+ };
982
+ }
983
+ function assertWorkspaceEndpointMatchesCurrentRuntime(info, workspaceRuntime, effectiveRuntime, rebuildCommand) {
984
+ if (!isWorkspaceIndexed(info) ||
985
+ workspaceRuntime.endpoint === effectiveRuntime.endpoint) {
986
+ return;
987
+ }
988
+ throw new EngineError("Existing zvec-grep index uses a different embedding endpoint", {
989
+ code: "ZVEC_GREP.ENGINE.SERVICE.EMBEDDING_ENDPOINT_CHANGE_REQUIRES_REBUILD",
990
+ context: errorDetails([
991
+ workspaceIndexDetail(info.name),
992
+ detail("hint", `Run "${rebuildCommand}" to rebuild this index with the requested endpoint.`),
993
+ ]),
994
+ });
995
+ }
996
+ function assertSearchEndpointMatchesWorkspace(info, workspaceRuntime, effectiveRuntime) {
997
+ if (workspaceRuntime.endpoint === effectiveRuntime.endpoint) {
998
+ return;
999
+ }
1000
+ throw new EngineError("Search cannot override the embedding endpoint of an existing index", {
1001
+ code: "ZVEC_GREP.ENGINE.SERVICE.SEARCH_ENDPOINT_CHANGE_REQUIRES_REBUILD",
1002
+ context: errorDetails([
1003
+ workspaceIndexDetail(info.name),
1004
+ detail("hint", 'Run "zg index --endpoint <url> --rebuild" before searching with this endpoint.'),
1005
+ ]),
1006
+ });
1007
+ }
1008
+ function assertWorkspaceEmbeddingMatchesCurrentModel(info, model, rebuildCommand) {
1009
+ if (!isWorkspaceIndexed(info)) {
890
1010
  return;
891
1011
  }
892
1012
  const expected = info.embedding;
893
- const changed = expected.provider !== model.ref.provider ||
894
- expected.model !== model.ref.model ||
895
- expected.dimension !== model.dimension ||
896
- expected.metric !== model.metric;
1013
+ const changed = expected.provider !== model.info.provider ||
1014
+ expected.model !== model.info.name ||
1015
+ expected.dimension !== model.info.dimension ||
1016
+ expected.metric !== model.info.metric;
897
1017
  if (!changed) {
898
1018
  return;
899
1019
  }
900
1020
  throw new EngineError("Existing zvec-grep index uses a different embedding model", {
901
1021
  code: "ZVEC_GREP.ENGINE.SERVICE.EMBEDDING_SCHEMA_CHANGE_REQUIRES_REBUILD",
902
1022
  context: errorDetails([
903
- collectionDetail(info.name),
1023
+ workspaceIndexDetail(info.name),
904
1024
  detail("existing", `${expected.provider}/${expected.model}`),
905
- detail("requested", `${model.ref.provider}/${model.ref.model}`),
1025
+ detail("requested", model.info.reference),
906
1026
  detail("hint", `Run "${rebuildCommand}" to rebuild this index with the requested embedding model.`),
907
1027
  ]),
908
1028
  });
909
1029
  }
910
- function modelCacheDir(options, root, registryHome) {
911
- if (options.modelCacheDir || process.env.ZVEC_GREP_MODEL_CACHE) {
912
- return options.modelCacheDir ?? process.env.ZVEC_GREP_MODEL_CACHE;
913
- }
914
- const globalCache = join(options.home ?? defaultHome(), "models");
915
- const legacyCaches = [
916
- registryHome ? join(registryHome, "models") : undefined,
917
- join(anonymousHome(root), "models"),
918
- ];
919
- const globalHasModel = directoryHasGguf(globalCache);
920
- for (const legacyCache of legacyCaches) {
921
- if (legacyCache && directoryHasGguf(legacyCache) && !globalHasModel) {
922
- return legacyCache;
923
- }
924
- }
925
- if (globalHasModel || canWriteCacheDirectory(globalCache)) {
926
- return globalCache;
927
- }
928
- return join(anonymousHome(root), "models");
929
- }
930
- function directoryHasGguf(path) {
931
- try {
932
- return readdirSync(path, { withFileTypes: true }).some((entry) => entry.isFile() && entry.name.endsWith(".gguf"));
933
- }
934
- catch {
935
- return false;
936
- }
937
- }
938
- function canWriteCacheDirectory(path) {
939
- const probe = join(path, `.zvec-grep-write-test-${Date.now()}-${Math.random().toString(16).slice(2)}`);
940
- try {
941
- mkdirSync(path, { recursive: true });
942
- writeFileSync(probe, "");
943
- unlinkSync(probe);
944
- return true;
945
- }
946
- catch {
947
- return false;
948
- }
949
- }
950
1030
  function contextGroupLimit(limit, groupCount) {
951
1031
  if (limit !== undefined) {
952
1032
  return limit;
@@ -1021,18 +1101,24 @@ function normalizeContextRoutes(routes) {
1021
1101
  }
1022
1102
  function contextGroups(primaryQueries, extraRoutes) {
1023
1103
  return [
1024
- ...primaryQueries.map((query) => ({
1104
+ ...primaryQueries.map((query, index) => ({
1105
+ id: `Q${index + 1}`,
1106
+ query,
1107
+ role: "primary",
1025
1108
  routes: [
1026
1109
  { mode: "fts", query },
1027
1110
  { mode: "vector", query },
1028
1111
  ],
1029
1112
  })),
1030
- ...extraRoutes.map((route) => ({
1113
+ ...extraRoutes.map((route, index) => ({
1114
+ id: `Q${primaryQueries.length + index + 1}`,
1115
+ query: route.query,
1116
+ role: "supplemental",
1031
1117
  routes: [route],
1032
1118
  })),
1033
1119
  ];
1034
1120
  }
1035
- function searchPlanToContextItems(result, root) {
1121
+ function searchPlanToContextItems(result, root, group) {
1036
1122
  return result.hits.map((hit) => {
1037
1123
  const target = contextItemTarget(hit);
1038
1124
  return {
@@ -1054,9 +1140,75 @@ function searchPlanToContextItems(result, root) {
1054
1140
  metadata: hit.entity.metadata,
1055
1141
  entityId: hit.entity.id,
1056
1142
  trace: hit.trace,
1143
+ queryGroups: [
1144
+ {
1145
+ id: group.id,
1146
+ query: group.query,
1147
+ role: group.role,
1148
+ rank: hit.rank,
1149
+ matchedBy: hit.matchedBy,
1150
+ },
1151
+ ],
1057
1152
  };
1058
1153
  });
1059
1154
  }
1155
+ const DEFAULT_CONTEXT_DETAIL_LIMIT = 6;
1156
+ const CONTEXT_GROUP_RRF_K = 60;
1157
+ function selectAndRankContextItems(items, coverageGroupIds) {
1158
+ const deduped = new Map();
1159
+ for (const item of items) {
1160
+ const key = contextItemDedupeKey(item);
1161
+ const existing = deduped.get(key);
1162
+ if (existing) {
1163
+ existing.queryGroups = mergeContextQueryGroupMatches(existing.queryGroups ?? [], item.queryGroups ?? []);
1164
+ existing.matchedBy = mergedContextMatchedBy(existing.queryGroups);
1165
+ continue;
1166
+ }
1167
+ deduped.set(key, { ...item });
1168
+ }
1169
+ const candidates = [...deduped.values()];
1170
+ const globallyRanked = [...candidates].sort(compareContextGlobalRank);
1171
+ const selected = new Set();
1172
+ const detailed = [];
1173
+ for (const groupId of coverageGroupIds) {
1174
+ if (detailed.length >= DEFAULT_CONTEXT_DETAIL_LIMIT) {
1175
+ break;
1176
+ }
1177
+ const candidate = candidates
1178
+ .filter((item) => !selected.has(contextItemDedupeKey(item)))
1179
+ .filter((item) => contextQueryGroupMatch(item, groupId) !== undefined)
1180
+ .sort((left, right) => compareContextGroupRank(left, right, groupId))[0];
1181
+ if (!candidate) {
1182
+ continue;
1183
+ }
1184
+ selected.add(contextItemDedupeKey(candidate));
1185
+ detailed.push({
1186
+ ...candidate,
1187
+ selectionReason: "coverage",
1188
+ coverageGroup: groupId,
1189
+ });
1190
+ }
1191
+ for (const candidate of globallyRanked) {
1192
+ if (detailed.length >= DEFAULT_CONTEXT_DETAIL_LIMIT) {
1193
+ break;
1194
+ }
1195
+ const key = contextItemDedupeKey(candidate);
1196
+ if (selected.has(key)) {
1197
+ continue;
1198
+ }
1199
+ selected.add(key);
1200
+ detailed.push({
1201
+ ...candidate,
1202
+ selectionReason: "global_fill",
1203
+ coverageGroup: undefined,
1204
+ });
1205
+ }
1206
+ const additional = globallyRanked.filter((item) => !selected.has(contextItemDedupeKey(item)));
1207
+ return [...detailed, ...additional].map((item, index) => ({
1208
+ ...item,
1209
+ rank: index + 1,
1210
+ }));
1211
+ }
1060
1212
  function dedupeAndRerankContextItems(items) {
1061
1213
  const seen = new Set();
1062
1214
  const deduped = [];
@@ -1066,13 +1218,52 @@ function dedupeAndRerankContextItems(items) {
1066
1218
  continue;
1067
1219
  }
1068
1220
  seen.add(key);
1069
- deduped.push({
1070
- ...item,
1071
- rank: deduped.length + 1,
1072
- });
1221
+ deduped.push({ ...item, rank: deduped.length + 1 });
1073
1222
  }
1074
1223
  return deduped;
1075
1224
  }
1225
+ function mergeContextQueryGroupMatches(left, right) {
1226
+ const matches = new Map(left.map((match) => [match.id, match]));
1227
+ for (const match of right) {
1228
+ const existing = matches.get(match.id);
1229
+ if (!existing || match.rank < existing.rank) {
1230
+ matches.set(match.id, match);
1231
+ }
1232
+ }
1233
+ return [...matches.values()].sort((a, b) => contextGroupNumber(a.id) - contextGroupNumber(b.id));
1234
+ }
1235
+ function mergedContextMatchedBy(matches) {
1236
+ const hasFts = matches.some((match) => match.matchedBy === "fts" || match.matchedBy === "fts+vector");
1237
+ const hasVector = matches.some((match) => match.matchedBy === "vector" || match.matchedBy === "fts+vector");
1238
+ return hasFts && hasVector ? "fts+vector" : hasFts ? "fts" : "vector";
1239
+ }
1240
+ function compareContextGlobalRank(left, right) {
1241
+ return (contextGlobalRrfScore(right) - contextGlobalRrfScore(left) ||
1242
+ contextBestGroupRank(left) - contextBestGroupRank(right) ||
1243
+ contextFirstGroupNumber(left) - contextFirstGroupNumber(right) ||
1244
+ contextItemDedupeKey(left).localeCompare(contextItemDedupeKey(right)));
1245
+ }
1246
+ function contextGlobalRrfScore(item) {
1247
+ return (item.queryGroups ?? []).reduce((score, match) => score + 1 / (CONTEXT_GROUP_RRF_K + match.rank), 0);
1248
+ }
1249
+ function compareContextGroupRank(left, right, groupId) {
1250
+ return (contextQueryGroupMatch(left, groupId).rank -
1251
+ contextQueryGroupMatch(right, groupId).rank ||
1252
+ compareContextGlobalRank(left, right));
1253
+ }
1254
+ function contextQueryGroupMatch(item, groupId) {
1255
+ return item.queryGroups?.find((match) => match.id === groupId);
1256
+ }
1257
+ function contextBestGroupRank(item) {
1258
+ return Math.min(...(item.queryGroups ?? []).map((match) => match.rank));
1259
+ }
1260
+ function contextFirstGroupNumber(item) {
1261
+ return Math.min(...(item.queryGroups ?? []).map((match) => contextGroupNumber(match.id)));
1262
+ }
1263
+ function contextGroupNumber(id) {
1264
+ const value = Number.parseInt(id.slice(1), 10);
1265
+ return Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
1266
+ }
1076
1267
  function contextItemDedupeKey(item) {
1077
1268
  if (item.entityId) {
1078
1269
  return `entity:${item.entityId}`;