@gmickel/gno 1.46.0 → 2.0.0

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 (230) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES.md +46 -0
  3. package/assets/skill/SKILL.md +7 -6
  4. package/assets/spa-production.json.gz +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip +0 -0
  6. package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip.sha256 +1 -0
  7. package/browser-extension/dist/chunk-4tc9v0ja.js +74 -0
  8. package/browser-extension/dist/manifest.json +1 -1
  9. package/browser-extension/dist/preview.html +1 -1
  10. package/browser-extension/dist/service-worker.js +32 -33
  11. package/bunfig.toml +2 -0
  12. package/package.json +40 -26
  13. package/spec/cli.md +21 -4
  14. package/spec/db/schema.sql +146 -1
  15. package/spec/mcp.md +26 -0
  16. package/src/app/context-runtime-types.ts +3 -0
  17. package/src/app/context-runtime.ts +2 -0
  18. package/src/cli/commands/ask.ts +6 -1
  19. package/src/cli/commands/daemon.ts +21 -8
  20. package/src/cli/commands/embed.ts +77 -41
  21. package/src/cli/detach.ts +3 -2
  22. package/src/config/types.ts +3 -3
  23. package/src/converters/adapters/markitdownTs/adapter.ts +1 -2
  24. package/src/converters/adapters/officeparser/adapter.ts +1 -2
  25. package/src/converters/versions.ts +6 -8
  26. package/src/core/context-evidence.ts +8 -4
  27. package/src/core/job-manager.ts +95 -13
  28. package/src/core/network-boundary-inventory.ts +10 -0
  29. package/src/core/shutdown-budget.ts +45 -0
  30. package/src/embed/backlog.ts +107 -4
  31. package/src/embed/batch.ts +42 -2
  32. package/src/embed/fingerprint.ts +16 -0
  33. package/src/embed/retry.ts +113 -5
  34. package/src/embed/variant-backlog.ts +105 -0
  35. package/src/embed/variant-plan.ts +62 -0
  36. package/src/embed/variant-retry.ts +113 -0
  37. package/src/ingestion/graph-reconciliation.ts +327 -0
  38. package/src/ingestion/sync.ts +9 -272
  39. package/src/llm/http-inference.ts +6 -0
  40. package/src/llm/httpEmbedding.ts +37 -6
  41. package/src/llm/httpGeneration.ts +18 -3
  42. package/src/llm/httpRerank.ts +23 -5
  43. package/src/llm/inference-cancellation.ts +168 -0
  44. package/src/llm/inference-scope.ts +202 -0
  45. package/src/llm/lazy-ports.ts +115 -0
  46. package/src/llm/native-worker/client.ts +541 -0
  47. package/src/llm/native-worker/dispatcher.ts +228 -0
  48. package/src/llm/native-worker/embedding-identity.ts +33 -0
  49. package/src/llm/native-worker/entry.ts +173 -0
  50. package/src/llm/native-worker/errors.ts +32 -0
  51. package/src/llm/native-worker/evaluation.ts +16 -0
  52. package/src/llm/native-worker/owned-exit.ts +108 -0
  53. package/src/llm/native-worker/owner.ts +141 -0
  54. package/src/llm/native-worker/ports.ts +317 -0
  55. package/src/llm/native-worker/protocol.ts +442 -0
  56. package/src/llm/native-worker/runtime-config.ts +92 -0
  57. package/src/llm/nodeLlamaCpp/adapter.ts +77 -20
  58. package/src/llm/nodeLlamaCpp/embedding.ts +130 -46
  59. package/src/llm/nodeLlamaCpp/generation.ts +34 -5
  60. package/src/llm/nodeLlamaCpp/lifecycle-options.ts +99 -0
  61. package/src/llm/nodeLlamaCpp/lifecycle.ts +209 -204
  62. package/src/llm/nodeLlamaCpp/rerank-capacity.ts +111 -0
  63. package/src/llm/nodeLlamaCpp/rerank.ts +118 -27
  64. package/src/llm/nodeLlamaCpp/simulator-handle.ts +73 -0
  65. package/src/llm/nodeLlamaCpp/simulator-install.ts +124 -0
  66. package/src/llm/nodeLlamaCpp/simulator-session.ts +240 -0
  67. package/src/llm/nodeLlamaCpp/simulator-types.ts +80 -0
  68. package/src/llm/types.ts +35 -5
  69. package/src/mcp/context.ts +27 -0
  70. package/src/mcp/http-transport.ts +12 -10
  71. package/src/mcp/server.ts +3 -0
  72. package/src/mcp/tool-profile.ts +30 -8
  73. package/src/mcp/tools/context.ts +8 -11
  74. package/src/mcp/tools/embed.ts +1 -1
  75. package/src/mcp/tools/index-cmd.ts +1 -1
  76. package/src/mcp/tools/index.ts +10 -8
  77. package/src/mcp/tools/query.ts +14 -30
  78. package/src/mcp/tools/vsearch.ts +1 -1
  79. package/src/pipeline/answer.ts +23 -3
  80. package/src/pipeline/claim-verifier.ts +6 -0
  81. package/src/pipeline/expansion.ts +43 -40
  82. package/src/pipeline/explain.ts +6 -2
  83. package/src/pipeline/filters.ts +63 -0
  84. package/src/pipeline/fusion.ts +29 -9
  85. package/src/pipeline/graph-retrieval.ts +29 -9
  86. package/src/pipeline/hybrid.ts +198 -55
  87. package/src/pipeline/hydration.ts +161 -0
  88. package/src/pipeline/owner-fusion.ts +87 -0
  89. package/src/pipeline/rerank.ts +35 -11
  90. package/src/pipeline/search.ts +13 -2
  91. package/src/pipeline/types.ts +5 -3
  92. package/src/pipeline/vsearch.ts +87 -7
  93. package/src/sdk/client.ts +47 -3
  94. package/src/sdk/embed.ts +63 -39
  95. package/src/serve/background-runtime.ts +1 -1
  96. package/src/serve/context.ts +41 -56
  97. package/src/serve/embed-scheduler.ts +58 -35
  98. package/src/serve/public/components/IndexingProgress.tsx +46 -60
  99. package/src/serve/public/globals.built.css +1 -1
  100. package/src/serve/public/lib/shiki-language-ids.ts +14 -0
  101. package/src/serve/resident-admission.ts +36 -36
  102. package/src/serve/resident-background-work.ts +20 -2
  103. package/src/serve/resident-request.ts +11 -5
  104. package/src/serve/resident-runtime.ts +97 -61
  105. package/src/serve/resident-shutdown.ts +153 -0
  106. package/src/serve/routes/api.ts +3 -1
  107. package/src/serve/server.ts +47 -26
  108. package/src/store/migrations/028-vector-variants.ts +54 -0
  109. package/src/store/migrations/029-graph-reference-state.ts +77 -0
  110. package/src/store/migrations/index.ts +4 -0
  111. package/src/store/sqlite/adapter.ts +251 -183
  112. package/src/store/sqlite/eligibility.ts +174 -0
  113. package/src/store/sqlite/graph-edge-application.ts +66 -0
  114. package/src/store/sqlite/graph-reference-state.ts +194 -0
  115. package/src/store/sqlite/legacy-vector-ownership.ts +79 -0
  116. package/src/store/types.ts +80 -12
  117. package/src/store/vector/eligibility.ts +36 -0
  118. package/src/store/vector/freshness.ts +33 -6
  119. package/src/store/vector/lazy.ts +81 -0
  120. package/src/store/vector/sqlite-vec.ts +106 -54
  121. package/src/store/vector/stats.ts +14 -3
  122. package/src/store/vector/types.ts +35 -2
  123. package/src/store/vector/variant-search.ts +192 -0
  124. package/src/store/vector/variants.ts +451 -0
  125. package/vendor/converters/markitdown-ts/LICENSE +21 -0
  126. package/vendor/converters/markitdown-ts/dist/index.cjs +1180 -0
  127. package/vendor/converters/markitdown-ts/dist/index.d.cts +46 -0
  128. package/vendor/converters/markitdown-ts/dist/index.d.mts +46 -0
  129. package/vendor/converters/markitdown-ts/dist/index.d.ts +46 -0
  130. package/vendor/converters/markitdown-ts/dist/index.mjs +1152 -0
  131. package/vendor/converters/markitdown-ts/package.json +77 -0
  132. package/vendor/converters/officeparser/LICENSE +21 -0
  133. package/vendor/converters/officeparser/dist/OfficeConverter.d.ts +47 -0
  134. package/vendor/converters/officeparser/dist/OfficeConverter.js +76 -0
  135. package/vendor/converters/officeparser/dist/OfficeGenerator.d.ts +23 -0
  136. package/vendor/converters/officeparser/dist/OfficeGenerator.js +73 -0
  137. package/vendor/converters/officeparser/dist/OfficeParser.d.ts +106 -0
  138. package/vendor/converters/officeparser/dist/OfficeParser.js +332 -0
  139. package/vendor/converters/officeparser/dist/cli.d.ts +28 -0
  140. package/vendor/converters/officeparser/dist/cli.js +381 -0
  141. package/vendor/converters/officeparser/dist/defaults.d.ts +41 -0
  142. package/vendor/converters/officeparser/dist/defaults.js +218 -0
  143. package/vendor/converters/officeparser/dist/generators/BaseGenerator.d.ts +107 -0
  144. package/vendor/converters/officeparser/dist/generators/BaseGenerator.js +248 -0
  145. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.d.ts +82 -0
  146. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.js +797 -0
  147. package/vendor/converters/officeparser/dist/generators/CsvGenerator.d.ts +38 -0
  148. package/vendor/converters/officeparser/dist/generators/CsvGenerator.js +245 -0
  149. package/vendor/converters/officeparser/dist/generators/EpubGenerator.d.ts +43 -0
  150. package/vendor/converters/officeparser/dist/generators/EpubGenerator.js +315 -0
  151. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.d.ts +59 -0
  152. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.js +1942 -0
  153. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.d.ts +96 -0
  154. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.js +1175 -0
  155. package/vendor/converters/officeparser/dist/generators/PdfGenerator.d.ts +22 -0
  156. package/vendor/converters/officeparser/dist/generators/PdfGenerator.js +194 -0
  157. package/vendor/converters/officeparser/dist/generators/RtfGenerator.d.ts +29 -0
  158. package/vendor/converters/officeparser/dist/generators/RtfGenerator.js +316 -0
  159. package/vendor/converters/officeparser/dist/generators/TextGenerator.d.ts +13 -0
  160. package/vendor/converters/officeparser/dist/generators/TextGenerator.js +201 -0
  161. package/vendor/converters/officeparser/dist/index.d.ts +60 -0
  162. package/vendor/converters/officeparser/dist/index.js +72 -0
  163. package/vendor/converters/officeparser/dist/index.mjs +18 -0
  164. package/vendor/converters/officeparser/dist/officeparser.browser.d.ts +2621 -0
  165. package/vendor/converters/officeparser/dist/officeparser.browser.iife.js +1336 -0
  166. package/vendor/converters/officeparser/dist/officeparser.browser.mjs +1335 -0
  167. package/vendor/converters/officeparser/dist/officeparser.browser.slim.d.ts +2621 -0
  168. package/vendor/converters/officeparser/dist/officeparser.browser.slim.iife.js +1336 -0
  169. package/vendor/converters/officeparser/dist/officeparser.browser.slim.mjs +1335 -0
  170. package/vendor/converters/officeparser/dist/parsers/CsvParser.d.ts +9 -0
  171. package/vendor/converters/officeparser/dist/parsers/CsvParser.js +115 -0
  172. package/vendor/converters/officeparser/dist/parsers/EpubParser.d.ts +8 -0
  173. package/vendor/converters/officeparser/dist/parsers/EpubParser.js +217 -0
  174. package/vendor/converters/officeparser/dist/parsers/ExcelParser.d.ts +32 -0
  175. package/vendor/converters/officeparser/dist/parsers/ExcelParser.js +736 -0
  176. package/vendor/converters/officeparser/dist/parsers/HtmlParser.d.ts +2 -0
  177. package/vendor/converters/officeparser/dist/parsers/HtmlParser.js +1287 -0
  178. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.d.ts +2 -0
  179. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.js +1272 -0
  180. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.d.ts +31 -0
  181. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.js +1819 -0
  182. package/vendor/converters/officeparser/dist/parsers/PdfParser.d.ts +67 -0
  183. package/vendor/converters/officeparser/dist/parsers/PdfParser.js +848 -0
  184. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.d.ts +32 -0
  185. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.js +950 -0
  186. package/vendor/converters/officeparser/dist/parsers/RtfParser.d.ts +187 -0
  187. package/vendor/converters/officeparser/dist/parsers/RtfParser.js +1801 -0
  188. package/vendor/converters/officeparser/dist/parsers/WordParser.d.ts +79 -0
  189. package/vendor/converters/officeparser/dist/parsers/WordParser.js +1177 -0
  190. package/vendor/converters/officeparser/dist/sbom.cdx.json +1763 -0
  191. package/vendor/converters/officeparser/dist/types.d.ts +2507 -0
  192. package/vendor/converters/officeparser/dist/types.js +107 -0
  193. package/vendor/converters/officeparser/dist/utils/astUtils.d.ts +16 -0
  194. package/vendor/converters/officeparser/dist/utils/astUtils.js +33 -0
  195. package/vendor/converters/officeparser/dist/utils/chartUtils.d.ts +6 -0
  196. package/vendor/converters/officeparser/dist/utils/chartUtils.js +257 -0
  197. package/vendor/converters/officeparser/dist/utils/configUtils.d.ts +44 -0
  198. package/vendor/converters/officeparser/dist/utils/configUtils.js +315 -0
  199. package/vendor/converters/officeparser/dist/utils/dateUtils.d.ts +17 -0
  200. package/vendor/converters/officeparser/dist/utils/dateUtils.js +69 -0
  201. package/vendor/converters/officeparser/dist/utils/envUtils.d.ts +29 -0
  202. package/vendor/converters/officeparser/dist/utils/envUtils.js +152 -0
  203. package/vendor/converters/officeparser/dist/utils/errorUtils.d.ts +72 -0
  204. package/vendor/converters/officeparser/dist/utils/errorUtils.js +245 -0
  205. package/vendor/converters/officeparser/dist/utils/imageUtils.d.ts +66 -0
  206. package/vendor/converters/officeparser/dist/utils/imageUtils.js +133 -0
  207. package/vendor/converters/officeparser/dist/utils/mathUtils.d.ts +42 -0
  208. package/vendor/converters/officeparser/dist/utils/mathUtils.js +385 -0
  209. package/vendor/converters/officeparser/dist/utils/moduleLoader.d.ts +18 -0
  210. package/vendor/converters/officeparser/dist/utils/moduleLoader.js +106 -0
  211. package/vendor/converters/officeparser/dist/utils/ocrUtils.d.ts +42 -0
  212. package/vendor/converters/officeparser/dist/utils/ocrUtils.js +428 -0
  213. package/vendor/converters/officeparser/dist/utils/sanitize.d.ts +148 -0
  214. package/vendor/converters/officeparser/dist/utils/sanitize.js +344 -0
  215. package/vendor/converters/officeparser/dist/utils/sheetUtils.d.ts +7 -0
  216. package/vendor/converters/officeparser/dist/utils/sheetUtils.js +35 -0
  217. package/vendor/converters/officeparser/dist/utils/styleMapper.d.ts +36 -0
  218. package/vendor/converters/officeparser/dist/utils/styleMapper.js +224 -0
  219. package/vendor/converters/officeparser/dist/utils/xmlUtils.d.ts +163 -0
  220. package/vendor/converters/officeparser/dist/utils/xmlUtils.js +461 -0
  221. package/vendor/converters/officeparser/dist/utils/zipUtils.d.ts +134 -0
  222. package/vendor/converters/officeparser/dist/utils/zipUtils.js +337 -0
  223. package/vendor/converters/officeparser/package.json +147 -0
  224. package/vendor/converters/upstream-manifest.json +124 -0
  225. package/vendor/dependency-fixes/README.md +77 -0
  226. package/vendor/dependency-fixes/vendor-converters.py +83 -0
  227. package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip +0 -0
  228. package/browser-extension/artifacts/gno-browser-clipper-v1.46.0.zip.sha256 +0 -1
  229. package/browser-extension/dist/chunk-627emwpj.js +0 -75
  230. /package/browser-extension/dist/{chunk-ydfx5d7p.css → chunk-z74y8n8c.css} +0 -0
package/src/sdk/client.ts CHANGED
@@ -1,10 +1,9 @@
1
+ import { mkdir } from "node:fs/promises";
1
2
  /**
2
3
  * GNO SDK client.
3
4
  *
4
5
  * @module src/sdk/client
5
6
  */
6
-
7
- import { mkdir } from "node:fs/promises";
8
7
  import { dirname } from "node:path";
9
8
 
10
9
  import type { Config } from "../config/types";
@@ -160,6 +159,10 @@ import {
160
159
  withContentTypeRules,
161
160
  } from "../ingestion";
162
161
  import { updateFrontmatterTags } from "../ingestion/frontmatter";
162
+ import {
163
+ withInferenceScope,
164
+ finishInferenceCleanup,
165
+ } from "../llm/inference-scope";
163
166
  import { LlmAdapter } from "../llm/nodeLlamaCpp/adapter";
164
167
  import { resolveDownloadPolicy } from "../llm/policy";
165
168
  import { resolveModelUri } from "../llm/registry";
@@ -170,6 +173,7 @@ import {
170
173
  } from "../pipeline/answer";
171
174
  import { formatQueryForEmbedding } from "../pipeline/contextual";
172
175
  import { searchHybrid } from "../pipeline/hybrid";
176
+ import { RequestHydration } from "../pipeline/hydration";
173
177
  import { searchBm25 } from "../pipeline/search";
174
178
  import { searchVectorWithEmbedding } from "../pipeline/vsearch";
175
179
  import { SqliteAdapter } from "../store/sqlite/adapter";
@@ -591,6 +595,10 @@ class GnoClientImpl implements GnoClient {
591
595
  }
592
596
 
593
597
  private async disposeRuntimePorts(ports: RuntimePorts): Promise<void> {
598
+ return finishInferenceCleanup(() => this.disposeRuntimePortsOwned(ports));
599
+ }
600
+
601
+ private async disposeRuntimePortsOwned(ports: RuntimePorts): Promise<void> {
594
602
  if (ports.embedPort) {
595
603
  await ports.embedPort.dispose();
596
604
  }
@@ -618,6 +626,15 @@ class GnoClientImpl implements GnoClient {
618
626
  async search(
619
627
  query: string,
620
628
  options: GnoSearchOptions = {}
629
+ ): Promise<SearchResults> {
630
+ return withInferenceScope(options, () =>
631
+ this.searchRequest(query, options)
632
+ );
633
+ }
634
+
635
+ private async searchRequest(
636
+ query: string,
637
+ options: GnoSearchOptions
621
638
  ): Promise<SearchResults> {
622
639
  this.assertOpen();
623
640
  let traceSession: RetrievalTraceSession | null = null;
@@ -661,6 +678,15 @@ class GnoClientImpl implements GnoClient {
661
678
  async vsearch(
662
679
  query: string,
663
680
  options: GnoVectorSearchOptions = {}
681
+ ): Promise<SearchResults> {
682
+ return withInferenceScope(options, () =>
683
+ this.vsearchRequest(query, options)
684
+ );
685
+ }
686
+
687
+ private async vsearchRequest(
688
+ query: string,
689
+ options: GnoVectorSearchOptions
664
690
  ): Promise<SearchResults> {
665
691
  this.assertOpen();
666
692
 
@@ -745,6 +771,13 @@ class GnoClientImpl implements GnoClient {
745
771
  async query(
746
772
  query: string,
747
773
  options: GnoQueryOptions = {}
774
+ ): Promise<SearchResults> {
775
+ return withInferenceScope(options, () => this.queryRequest(query, options));
776
+ }
777
+
778
+ private async queryRequest(
779
+ query: string,
780
+ options: GnoQueryOptions
748
781
  ): Promise<SearchResults> {
749
782
  this.assertOpen();
750
783
 
@@ -852,6 +885,13 @@ class GnoClientImpl implements GnoClient {
852
885
  }
853
886
 
854
887
  async ask(query: string, options: GnoAskOptions = {}): Promise<AskResult> {
888
+ return withInferenceScope(options, () => this.askRequest(query, options));
889
+ }
890
+
891
+ private async askRequest(
892
+ query: string,
893
+ options: GnoAskOptions
894
+ ): Promise<AskResult> {
855
895
  this.assertOpen();
856
896
 
857
897
  const normalizedInput = normalizeStructuredQueryInput(
@@ -911,6 +951,7 @@ class GnoClientImpl implements GnoClient {
911
951
  let ports: RuntimePorts | null = null;
912
952
  let traceSession: RetrievalTraceSession | null = null;
913
953
 
954
+ const hydration = new RequestHydration(this.store);
914
955
  try {
915
956
  const { projectHints, ...askOptions } = options;
916
957
  const projectAffinity = await resolveSdkProjectAffinity(
@@ -960,6 +1001,7 @@ class GnoClientImpl implements GnoClient {
960
1001
  { ...askOptions, projectAffinity },
961
1002
  {
962
1003
  store: this.store,
1004
+ hydration,
963
1005
  config: this.config,
964
1006
  indexName: this.indexName,
965
1007
  vectorIndex: ports.vectorIndex,
@@ -987,6 +1029,7 @@ class GnoClientImpl implements GnoClient {
987
1029
  await searchHybrid(
988
1030
  {
989
1031
  store: this.store,
1032
+ hydration,
990
1033
  config: this.config,
991
1034
  vectorIndex: ports.vectorIndex,
992
1035
  embedPort: ports.embedPort,
@@ -1033,7 +1076,7 @@ class GnoClientImpl implements GnoClient {
1033
1076
  ) {
1034
1077
  await traceSession?.recordCapability("answer_generation", "attempted");
1035
1078
  const rawAnswer = await generateGroundedAnswer(
1036
- { genPort: ports.answerPort, store: this.store },
1079
+ { genPort: ports.answerPort, store: this.store, hydration },
1037
1080
  query,
1038
1081
  searchResult.results,
1039
1082
  options.maxAnswerTokens ?? 512
@@ -1100,6 +1143,7 @@ class GnoClientImpl implements GnoClient {
1100
1143
  await finishRetrievalTraceAfterError(traceSession, cause);
1101
1144
  throw cause;
1102
1145
  } finally {
1146
+ hydration.release();
1103
1147
  if (ports) await this.disposeRuntimePorts(ports);
1104
1148
  }
1105
1149
  }
package/src/sdk/embed.ts CHANGED
@@ -18,13 +18,14 @@ import type {
18
18
  } from "../store/vector";
19
19
  import type { GnoEmbedOptions, GnoEmbedResult } from "./types";
20
20
 
21
- import { embedBacklog } from "../embed";
21
+ import { embedBacklog, prepareEmbeddingBacklog } from "../embed/backlog";
22
22
  import { getEmbeddingFingerprint } from "../embed/fingerprint";
23
23
  import {
24
24
  chunkRetryKey,
25
25
  embedAndStoreBatch,
26
26
  MAX_EMBED_CHUNK_ATTEMPTS,
27
27
  } from "../embed/retry";
28
+ import { countVariantBacklog } from "../embed/variant-plan";
28
29
  import { resolveModelUri } from "../llm/registry";
29
30
  import { err, ok } from "../store/types";
30
31
  import { createVectorIndexPort, createVectorStatsPort } from "../store/vector";
@@ -71,7 +72,7 @@ function getActiveChunks(
71
72
  const sql = after
72
73
  ? `
73
74
  SELECT c.mirror_hash as mirrorHash, c.seq, c.text,
74
- (SELECT d.title FROM documents d WHERE d.mirror_hash = c.mirror_hash AND d.active = 1 LIMIT 1) as title,
75
+ (SELECT d.title FROM documents d WHERE d.mirror_hash = c.mirror_hash AND d.active = 1 ORDER BY d.id LIMIT 1) as title,
75
76
  'force' as reason
76
77
  FROM content_chunks c
77
78
  WHERE EXISTS (
@@ -84,7 +85,7 @@ function getActiveChunks(
84
85
  `
85
86
  : `
86
87
  SELECT c.mirror_hash as mirrorHash, c.seq, c.text,
87
- (SELECT d.title FROM documents d WHERE d.mirror_hash = c.mirror_hash AND d.active = 1 LIMIT 1) as title,
88
+ (SELECT d.title FROM documents d WHERE d.mirror_hash = c.mirror_hash AND d.active = 1 ORDER BY d.id LIMIT 1) as title,
88
89
  'force' as reason
89
90
  FROM content_chunks c
90
91
  WHERE EXISTS (
@@ -238,16 +239,6 @@ async function forceEmbedAll(
238
239
  return { embedded, errors, contentionErrors };
239
240
  }
240
241
 
241
- async function checkVecAvailable(db: Database): Promise<boolean> {
242
- try {
243
- const sqliteVec = await import("sqlite-vec");
244
- sqliteVec.load(db);
245
- return true;
246
- } catch {
247
- return false;
248
- }
249
- }
250
-
251
242
  export async function runEmbed(
252
243
  runtime: EmbedRuntimeOptions,
253
244
  options: GnoEmbedOptions = {}
@@ -265,26 +256,6 @@ export async function runEmbed(
265
256
  const stats: VectorStatsPort = createVectorStatsPort(db);
266
257
 
267
258
  let totalToEmbed = 0;
268
- if (force) {
269
- const forceCount = await getActiveChunkCount(db);
270
- if (!forceCount.ok) {
271
- throw sdkError("STORE", forceCount.error.message, {
272
- cause: forceCount.error.cause,
273
- });
274
- }
275
-
276
- totalToEmbed = forceCount.value;
277
- if (totalToEmbed === 0 || dryRun) {
278
- return {
279
- embedded: totalToEmbed,
280
- errors: 0,
281
- duration: 0,
282
- model: modelUri,
283
- searchAvailable: await checkVecAvailable(db),
284
- };
285
- }
286
- }
287
-
288
259
  const embedResult = await runtime.llm.createEmbeddingPort(modelUri, {
289
260
  egressCollections: options.collection ? [options.collection] : "all",
290
261
  policy: runtime.downloadPolicy,
@@ -297,16 +268,22 @@ export async function runEmbed(
297
268
 
298
269
  const embedPort = embedResult.value;
299
270
  try {
300
- const probeResult = await embedPort.embed("dimension probe");
301
- if (!probeResult.ok) {
302
- throw sdkError("MODEL", probeResult.error.message, {
303
- cause: probeResult.error.cause,
304
- });
271
+ const initializedPort = await embedPort.init();
272
+ if (!initializedPort.ok)
273
+ throw sdkError("MODEL", initializedPort.error.message);
274
+ let dimensions = embedPort.dimensions();
275
+ if (!embedPort.getIdentity?.()) {
276
+ const probeResult = await embedPort.embed("dimension probe");
277
+ if (!probeResult.ok)
278
+ throw sdkError("MODEL", probeResult.error.message, {
279
+ cause: probeResult.error.cause,
280
+ });
281
+ dimensions = probeResult.value.length;
305
282
  }
306
283
 
307
284
  const vectorResult = await createVectorIndexPort(db, {
308
285
  model: modelUri,
309
- dimensions: probeResult.value.length,
286
+ dimensions,
310
287
  });
311
288
  if (!vectorResult.ok) {
312
289
  throw sdkError("STORE", vectorResult.error.message, {
@@ -315,6 +292,53 @@ export async function runEmbed(
315
292
  }
316
293
 
317
294
  const vectorIndex = vectorResult.value;
295
+ const prepared = await prepareEmbeddingBacklog({
296
+ statsPort: stats,
297
+ embedPort,
298
+ vectorIndex,
299
+ modelUri,
300
+ collection: options.collection,
301
+ batchSize,
302
+ force,
303
+ });
304
+ if (!prepared.ok) throw sdkError("STORE", prepared.error.message);
305
+ if (prepared.value.variantStore) {
306
+ const count = countVariantBacklog(prepared.value);
307
+ const startedAt = Date.now();
308
+ if (dryRun)
309
+ return {
310
+ embedded: count,
311
+ errors: 0,
312
+ duration: 0,
313
+ model: modelUri,
314
+ searchAvailable: prepared.value.variantStore.searchAvailable,
315
+ };
316
+ const processed = await embedBacklog(prepared.value);
317
+ if (!processed.ok) throw sdkError("STORE", processed.error.message);
318
+ if (processed.value.syncError)
319
+ throw sdkError("STORE", processed.value.syncError);
320
+ return {
321
+ embedded: processed.value.embedded,
322
+ errors: processed.value.errors,
323
+ contentionErrors: processed.value.contentionErrors ?? 0,
324
+ duration: (Date.now() - startedAt) / 1000,
325
+ model: modelUri,
326
+ searchAvailable: prepared.value.variantStore.searchAvailable,
327
+ };
328
+ }
329
+ if (force) {
330
+ const count = await getActiveChunkCount(db);
331
+ if (!count.ok) throw sdkError("STORE", count.error.message);
332
+ totalToEmbed = count.value;
333
+ if (dryRun || !totalToEmbed)
334
+ return {
335
+ embedded: totalToEmbed,
336
+ errors: 0,
337
+ duration: 0,
338
+ model: modelUri,
339
+ searchAvailable: vectorIndex.searchAvailable,
340
+ };
341
+ }
318
342
  if (!force) {
319
343
  const embedFingerprint = getEmbeddingFingerprint({
320
344
  modelUri,
@@ -29,7 +29,7 @@ export interface BackgroundRuntime {
29
29
  runUpdateCmd?: boolean;
30
30
  triggerEmbed?: boolean;
31
31
  }): Promise<{ syncResult: SyncResult; embedResult: EmbedResult | null }>;
32
- dispose(): Promise<void>;
32
+ dispose(closeSurface?: () => Promise<unknown>): Promise<void>;
33
33
  }
34
34
 
35
35
  export type BackgroundRuntimeResult =
@@ -15,16 +15,22 @@ import type {
15
15
  RerankPort,
16
16
  } from "../llm/types";
17
17
  import type { SqliteAdapter } from "../store/sqlite/adapter";
18
+ import type { VectorIndexPort } from "../store/vector";
18
19
  import type { DocumentEventBus } from "./doc-events";
19
20
  import type { EmbedScheduler } from "./embed-scheduler";
20
21
  import type { CollectionWatchService } from "./watch-service";
21
22
 
22
23
  import { DEFAULT_INDEX_NAME } from "../app/constants";
23
24
  import { canonicalizeIndexName } from "../app/index-name";
25
+ import {
26
+ lazyEmbeddingPort,
27
+ lazyGenerationPort,
28
+ lazyRerankPort,
29
+ } from "../llm/lazy-ports";
24
30
  import { LlmAdapter } from "../llm/nodeLlamaCpp/adapter";
25
31
  import { resolveDownloadPolicy } from "../llm/policy";
26
32
  import { getActivePreset } from "../llm/registry";
27
- import { createVectorIndexPort, type VectorIndexPort } from "../store/vector";
33
+ import { createLazyVectorIndex } from "../store/vector/lazy";
28
34
 
29
35
  // ─────────────────────────────────────────────────────────────────────────────
30
36
  // Download State (in-memory, single user)
@@ -64,6 +70,7 @@ export function resetDownloadState(): void {
64
70
  // ─────────────────────────────────────────────────────────────────────────────
65
71
 
66
72
  export interface ServerContext {
73
+ llm?: LlmAdapter;
67
74
  store: SqliteAdapter;
68
75
  config: Config;
69
76
  /** Canonical identity of the already-open resident store. */
@@ -91,7 +98,7 @@ export interface CreateServerContextOptions {
91
98
 
92
99
  /**
93
100
  * Initialize server context with LLM ports.
94
- * Attempts to load models; missing models are logged but don't fail.
101
+ * Model resolution, downloads and loading wait until actual inference.
95
102
  */
96
103
  export async function createServerContext(
97
104
  store: SqliteAdapter,
@@ -104,9 +111,9 @@ export async function createServerContext(
104
111
  let rerankPort: RerankPort | null = null;
105
112
  let vectorIndex: VectorIndexPort | null = null;
106
113
 
114
+ const llm = new LlmAdapter(config);
107
115
  try {
108
116
  const preset = getActivePreset(config);
109
- const llm = new LlmAdapter(config);
110
117
 
111
118
  // Resolve download policy from env (serve has no CLI flags)
112
119
  const policy = resolveDownloadPolicy(process.env, {
@@ -127,64 +134,40 @@ export async function createServerContext(
127
134
  },
128
135
  });
129
136
 
130
- // Try to create embedding port
131
- const embedResult = await llm.createEmbeddingPort(
137
+ const resolvePort = async <T>(operation: () => Promise<T>): Promise<T> => {
138
+ try {
139
+ return await operation();
140
+ } finally {
141
+ downloadState.active = false;
142
+ downloadState.currentType = null;
143
+ }
144
+ };
145
+ embedPort = lazyEmbeddingPort(preset.embed, () =>
146
+ resolvePort(() =>
147
+ llm.createEmbeddingPort(preset.embed, createPortOptions("embed"))
148
+ )
149
+ );
150
+ vectorIndex = await createLazyVectorIndex(
151
+ store.getRawDb(),
132
152
  preset.embed,
133
- createPortOptions("embed")
153
+ embedPort
134
154
  );
135
- if (embedResult.ok) {
136
- embedPort = embedResult.value;
137
- const initResult = await embedPort.init();
138
- if (initResult.ok) {
139
- // Create vector index
140
- const dimensions = embedPort.dimensions();
141
- const db = store.getRawDb();
142
- const vectorResult = await createVectorIndexPort(db, {
143
- model: preset.embed,
144
- dimensions,
145
- });
146
- if (vectorResult.ok) {
147
- vectorIndex = vectorResult.value;
148
- console.log("Vector search enabled");
149
- }
150
- }
151
- }
152
-
153
- // Try to create expansion port
154
- const expandResult = await llm.createExpansionPort(
155
- preset.expand ?? preset.gen,
156
- createPortOptions("expand")
155
+ const expandUri = preset.expand ?? preset.gen;
156
+ expandPort = lazyGenerationPort(expandUri, () =>
157
+ resolvePort(() =>
158
+ llm.createExpansionPort(expandUri, createPortOptions("expand"))
159
+ )
157
160
  );
158
- if (expandResult.ok) {
159
- expandPort = expandResult.value;
160
- console.log("Query expansion enabled");
161
- }
162
-
163
- // Try to create answer generation port
164
- const answerResult = await llm.createGenerationPort(
165
- preset.gen,
166
- createPortOptions("gen")
161
+ answerPort = lazyGenerationPort(preset.gen, () =>
162
+ resolvePort(() =>
163
+ llm.createGenerationPort(preset.gen, createPortOptions("gen"))
164
+ )
167
165
  );
168
- if (answerResult.ok) {
169
- answerPort = answerResult.value;
170
- console.log("AI answer generation enabled");
171
- }
172
-
173
- // Try to create rerank port
174
- const rerankResult = await llm.createRerankPort(
175
- preset.rerank,
176
- createPortOptions("rerank")
166
+ rerankPort = lazyRerankPort(preset.rerank, () =>
167
+ resolvePort(() =>
168
+ llm.createRerankPort(preset.rerank, createPortOptions("rerank"))
169
+ )
177
170
  );
178
- if (rerankResult.ok) {
179
- rerankPort = rerankResult.value;
180
- console.log("Reranking enabled");
181
- }
182
-
183
- // Reset download state after initialization
184
- if (downloadState.active) {
185
- downloadState.active = false;
186
- downloadState.currentType = null;
187
- }
188
171
  } catch (e) {
189
172
  // Log but don't fail - models are optional
190
173
  console.log(
@@ -201,6 +184,7 @@ export async function createServerContext(
201
184
  };
202
185
 
203
186
  return {
187
+ llm,
204
188
  store,
205
189
  config,
206
190
  indexName: canonicalizeIndexName(options.indexName ?? DEFAULT_INDEX_NAME),
@@ -237,6 +221,7 @@ export async function disposeServerContext(ctx: ServerContext): Promise<void> {
237
221
  }
238
222
  }
239
223
  }
224
+ await ctx.llm?.dispose();
240
225
  }
241
226
 
242
227
  /**
@@ -7,11 +7,14 @@
7
7
 
8
8
  import type { Database } from "bun:sqlite";
9
9
 
10
- import type { ModelLease } from "../llm/nodeLlamaCpp/lifecycle";
11
10
  import type { EmbeddingPort } from "../llm/types";
12
11
  import type { VectorIndexPort } from "../store/vector";
13
12
 
14
13
  import { embedBacklog } from "../embed";
14
+ import {
15
+ withBackgroundInference,
16
+ withOwnedInferenceScope,
17
+ } from "../llm/inference-scope";
15
18
  import { createVectorStatsPort } from "../store/vector";
16
19
 
17
20
  // ─────────────────────────────────────────────────────────────────────────────
@@ -47,7 +50,6 @@ export interface EmbedSchedulerDeps {
47
50
  getVectorIndex: () => VectorIndexPort | null;
48
51
  /** Getter for current model URI (survives preset changes) */
49
52
  getModelUri: () => string;
50
- acquireModelLease?: () => ModelLease;
51
53
  onEmbedded?: (result: EmbedResult) => void;
52
54
  embedBacklogFn?: typeof embedBacklog;
53
55
  }
@@ -68,6 +70,8 @@ export interface EmbedScheduler {
68
70
 
69
71
  /** Cleanup on server shutdown */
70
72
  dispose(): Promise<void>;
73
+ /** Stop new turns while allowing the current pass to settle before cancellation. */
74
+ stop?(): Promise<void>;
71
75
  }
72
76
 
73
77
  /**
@@ -80,7 +84,6 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
80
84
  getEmbedPort,
81
85
  getVectorIndex,
82
86
  getModelUri,
83
- acquireModelLease,
84
87
  onEmbedded,
85
88
  embedBacklogFn = embedBacklog,
86
89
  } = deps;
@@ -96,6 +99,7 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
96
99
  let currentRun: Promise<EmbedResult | null> | null = null;
97
100
  let lastRunAt: number | null = null;
98
101
  let lastResult: EmbedResult | null = null;
102
+ const controller = new AbortController();
99
103
 
100
104
  const stats = createVectorStatsPort(db);
101
105
 
@@ -116,33 +120,48 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
116
120
  return { embedded: 0, errors: 0 };
117
121
  }
118
122
 
119
- const lease = acquireModelLease?.();
123
+ let result: Awaited<ReturnType<typeof embedBacklogFn>>;
120
124
  try {
121
- const result = await embedBacklogFn({
122
- statsPort: stats,
123
- embedPort,
124
- vectorIndex,
125
- modelUri,
126
- batchSize: BATCH_SIZE,
127
- });
128
-
129
- if (!result.ok) {
130
- console.error("[embed-scheduler] Embed failed:", result.error.message);
131
- return { embedded: 0, errors: 0 };
132
- }
133
- if ((result.value.contentionErrors ?? 0) > 0) {
134
- // Chunks deferred by SQLITE_BUSY stay in the backlog; retry the run
135
- // instead of silently leaving embeddings a pass behind (fn-127 R6).
136
- console.error(
137
- `[embed-scheduler] ${result.value.contentionErrors} chunks deferred by index contention; rescheduling`
138
- );
139
- needsRerun = true;
140
- }
141
- if (result.value.embedded > 0) onEmbedded?.(result.value);
142
- return result.value;
143
- } finally {
144
- lease?.release();
125
+ result = await withBackgroundInference(() =>
126
+ withOwnedInferenceScope({ signal: controller.signal }, () =>
127
+ embedBacklogFn({
128
+ statsPort: stats,
129
+ embedPort,
130
+ vectorIndex,
131
+ modelUri,
132
+ batchSize: BATCH_SIZE,
133
+ identityStillCurrent: () =>
134
+ getEmbedPort() === embedPort &&
135
+ getVectorIndex() === vectorIndex &&
136
+ getModelUri() === modelUri,
137
+ })
138
+ )
139
+ );
140
+ } catch (cause) {
141
+ if (controller.signal.aborted) return { embedded: 0, errors: 0 };
142
+ throw cause;
143
+ }
144
+
145
+ if (!result.ok) {
146
+ needsRerun = true;
147
+ console.error("[embed-scheduler] Embed failed:", result.error.message);
148
+ return { embedded: 0, errors: 0 };
149
+ }
150
+ if (
151
+ getEmbedPort() !== embedPort ||
152
+ getVectorIndex() !== vectorIndex ||
153
+ getModelUri() !== modelUri
154
+ )
155
+ needsRerun = true;
156
+ if ((result.value.contentionErrors ?? 0) > 0 || result.value.errors > 0) {
157
+ // Provider failures and contended checkpoints remain durably pending.
158
+ console.error(
159
+ `[embed-scheduler] ${result.value.errors} embedding errors, ${result.value.contentionErrors ?? 0} contended writes; rescheduling`
160
+ );
161
+ needsRerun = true;
145
162
  }
163
+ if (result.value.embedded > 0) onEmbedded?.(result.value);
164
+ return result.value;
146
165
  }
147
166
 
148
167
  /**
@@ -241,7 +260,16 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
241
260
  return operation;
242
261
  }
243
262
 
263
+ const stop = async (): Promise<void> => {
264
+ disposed = true;
265
+ if (timer) clearTimeout(timer);
266
+ timer = null;
267
+ nextRunAt = null;
268
+ await currentRun;
269
+ };
270
+
244
271
  return {
272
+ stop,
245
273
  notifySyncComplete(docIds: string[]): void {
246
274
  // Resolve embedPort at call time to check availability
247
275
  if (disposed || !getEmbedPort()) {
@@ -302,13 +330,8 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
302
330
  },
303
331
 
304
332
  async dispose(): Promise<void> {
305
- disposed = true;
306
- if (timer) {
307
- clearTimeout(timer);
308
- timer = null;
309
- nextRunAt = null;
310
- }
311
- await currentRun;
333
+ controller.abort();
334
+ await stop();
312
335
  },
313
336
  };
314
337
  }