@gmickel/gno 1.45.1 → 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 (236) 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/skill/cli-reference.md +14 -6
  5. package/assets/skill/mcp-reference.md +4 -1
  6. package/assets/spa-production.json.gz +0 -0
  7. package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip +0 -0
  8. package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip.sha256 +1 -0
  9. package/browser-extension/dist/chunk-4tc9v0ja.js +74 -0
  10. package/browser-extension/dist/manifest.json +1 -1
  11. package/browser-extension/dist/preview.html +1 -1
  12. package/browser-extension/dist/service-worker.js +32 -33
  13. package/bunfig.toml +2 -0
  14. package/package.json +40 -26
  15. package/spec/cli.md +30 -11
  16. package/spec/db/schema.sql +146 -1
  17. package/spec/mcp.md +26 -0
  18. package/src/app/context-runtime-types.ts +3 -0
  19. package/src/app/context-runtime.ts +2 -0
  20. package/src/cli/commands/ask.ts +6 -1
  21. package/src/cli/commands/daemon.ts +21 -8
  22. package/src/cli/commands/embed.ts +77 -41
  23. package/src/cli/commands/mcp/install.ts +20 -0
  24. package/src/cli/commands/mcp/paths.ts +25 -0
  25. package/src/cli/commands/mcp/status.ts +6 -0
  26. package/src/cli/detach.ts +3 -2
  27. package/src/cli/program.ts +6 -0
  28. package/src/config/types.ts +3 -3
  29. package/src/converters/adapters/markitdownTs/adapter.ts +1 -2
  30. package/src/converters/adapters/officeparser/adapter.ts +1 -2
  31. package/src/converters/versions.ts +6 -8
  32. package/src/core/context-evidence.ts +8 -4
  33. package/src/core/job-manager.ts +95 -13
  34. package/src/core/network-boundary-inventory.ts +10 -0
  35. package/src/core/shutdown-budget.ts +45 -0
  36. package/src/embed/backlog.ts +107 -4
  37. package/src/embed/batch.ts +42 -2
  38. package/src/embed/fingerprint.ts +16 -0
  39. package/src/embed/retry.ts +113 -5
  40. package/src/embed/variant-backlog.ts +105 -0
  41. package/src/embed/variant-plan.ts +62 -0
  42. package/src/embed/variant-retry.ts +113 -0
  43. package/src/ingestion/graph-reconciliation.ts +327 -0
  44. package/src/ingestion/sync.ts +9 -272
  45. package/src/llm/http-inference.ts +6 -0
  46. package/src/llm/httpEmbedding.ts +37 -6
  47. package/src/llm/httpGeneration.ts +18 -3
  48. package/src/llm/httpRerank.ts +23 -5
  49. package/src/llm/inference-cancellation.ts +168 -0
  50. package/src/llm/inference-scope.ts +202 -0
  51. package/src/llm/lazy-ports.ts +115 -0
  52. package/src/llm/native-worker/client.ts +541 -0
  53. package/src/llm/native-worker/dispatcher.ts +228 -0
  54. package/src/llm/native-worker/embedding-identity.ts +33 -0
  55. package/src/llm/native-worker/entry.ts +173 -0
  56. package/src/llm/native-worker/errors.ts +32 -0
  57. package/src/llm/native-worker/evaluation.ts +16 -0
  58. package/src/llm/native-worker/owned-exit.ts +108 -0
  59. package/src/llm/native-worker/owner.ts +141 -0
  60. package/src/llm/native-worker/ports.ts +317 -0
  61. package/src/llm/native-worker/protocol.ts +442 -0
  62. package/src/llm/native-worker/runtime-config.ts +92 -0
  63. package/src/llm/nodeLlamaCpp/adapter.ts +77 -20
  64. package/src/llm/nodeLlamaCpp/embedding.ts +130 -46
  65. package/src/llm/nodeLlamaCpp/generation.ts +34 -5
  66. package/src/llm/nodeLlamaCpp/lifecycle-options.ts +99 -0
  67. package/src/llm/nodeLlamaCpp/lifecycle.ts +209 -204
  68. package/src/llm/nodeLlamaCpp/rerank-capacity.ts +111 -0
  69. package/src/llm/nodeLlamaCpp/rerank.ts +118 -27
  70. package/src/llm/nodeLlamaCpp/simulator-handle.ts +73 -0
  71. package/src/llm/nodeLlamaCpp/simulator-install.ts +124 -0
  72. package/src/llm/nodeLlamaCpp/simulator-session.ts +240 -0
  73. package/src/llm/nodeLlamaCpp/simulator-types.ts +80 -0
  74. package/src/llm/types.ts +35 -5
  75. package/src/mcp/context.ts +27 -0
  76. package/src/mcp/http-transport.ts +12 -10
  77. package/src/mcp/server.ts +3 -0
  78. package/src/mcp/tool-profile.ts +30 -8
  79. package/src/mcp/tools/context.ts +8 -11
  80. package/src/mcp/tools/embed.ts +1 -1
  81. package/src/mcp/tools/index-cmd.ts +1 -1
  82. package/src/mcp/tools/index.ts +10 -8
  83. package/src/mcp/tools/query.ts +14 -30
  84. package/src/mcp/tools/vsearch.ts +1 -1
  85. package/src/pipeline/answer.ts +23 -3
  86. package/src/pipeline/claim-verifier.ts +6 -0
  87. package/src/pipeline/expansion.ts +43 -40
  88. package/src/pipeline/explain.ts +6 -2
  89. package/src/pipeline/filters.ts +63 -0
  90. package/src/pipeline/fusion.ts +29 -9
  91. package/src/pipeline/graph-retrieval.ts +29 -9
  92. package/src/pipeline/hybrid.ts +198 -55
  93. package/src/pipeline/hydration.ts +161 -0
  94. package/src/pipeline/owner-fusion.ts +87 -0
  95. package/src/pipeline/rerank.ts +35 -11
  96. package/src/pipeline/search.ts +13 -2
  97. package/src/pipeline/types.ts +5 -3
  98. package/src/pipeline/vsearch.ts +87 -7
  99. package/src/sdk/client.ts +47 -3
  100. package/src/sdk/embed.ts +63 -39
  101. package/src/serve/background-runtime.ts +1 -1
  102. package/src/serve/context.ts +41 -56
  103. package/src/serve/embed-scheduler.ts +58 -35
  104. package/src/serve/public/components/IndexingProgress.tsx +46 -60
  105. package/src/serve/public/globals.built.css +1 -1
  106. package/src/serve/public/lib/shiki-language-ids.ts +14 -0
  107. package/src/serve/resident-admission.ts +36 -36
  108. package/src/serve/resident-background-work.ts +20 -2
  109. package/src/serve/resident-request.ts +11 -5
  110. package/src/serve/resident-runtime.ts +97 -61
  111. package/src/serve/resident-shutdown.ts +153 -0
  112. package/src/serve/routes/api.ts +3 -1
  113. package/src/serve/server.ts +47 -26
  114. package/src/store/migrations/028-vector-variants.ts +54 -0
  115. package/src/store/migrations/029-graph-reference-state.ts +77 -0
  116. package/src/store/migrations/index.ts +4 -0
  117. package/src/store/sqlite/adapter.ts +251 -183
  118. package/src/store/sqlite/eligibility.ts +174 -0
  119. package/src/store/sqlite/graph-edge-application.ts +66 -0
  120. package/src/store/sqlite/graph-reference-state.ts +194 -0
  121. package/src/store/sqlite/legacy-vector-ownership.ts +79 -0
  122. package/src/store/types.ts +80 -12
  123. package/src/store/vector/eligibility.ts +36 -0
  124. package/src/store/vector/freshness.ts +33 -6
  125. package/src/store/vector/lazy.ts +81 -0
  126. package/src/store/vector/sqlite-vec.ts +106 -54
  127. package/src/store/vector/stats.ts +14 -3
  128. package/src/store/vector/types.ts +35 -2
  129. package/src/store/vector/variant-search.ts +192 -0
  130. package/src/store/vector/variants.ts +451 -0
  131. package/vendor/converters/markitdown-ts/LICENSE +21 -0
  132. package/vendor/converters/markitdown-ts/dist/index.cjs +1180 -0
  133. package/vendor/converters/markitdown-ts/dist/index.d.cts +46 -0
  134. package/vendor/converters/markitdown-ts/dist/index.d.mts +46 -0
  135. package/vendor/converters/markitdown-ts/dist/index.d.ts +46 -0
  136. package/vendor/converters/markitdown-ts/dist/index.mjs +1152 -0
  137. package/vendor/converters/markitdown-ts/package.json +77 -0
  138. package/vendor/converters/officeparser/LICENSE +21 -0
  139. package/vendor/converters/officeparser/dist/OfficeConverter.d.ts +47 -0
  140. package/vendor/converters/officeparser/dist/OfficeConverter.js +76 -0
  141. package/vendor/converters/officeparser/dist/OfficeGenerator.d.ts +23 -0
  142. package/vendor/converters/officeparser/dist/OfficeGenerator.js +73 -0
  143. package/vendor/converters/officeparser/dist/OfficeParser.d.ts +106 -0
  144. package/vendor/converters/officeparser/dist/OfficeParser.js +332 -0
  145. package/vendor/converters/officeparser/dist/cli.d.ts +28 -0
  146. package/vendor/converters/officeparser/dist/cli.js +381 -0
  147. package/vendor/converters/officeparser/dist/defaults.d.ts +41 -0
  148. package/vendor/converters/officeparser/dist/defaults.js +218 -0
  149. package/vendor/converters/officeparser/dist/generators/BaseGenerator.d.ts +107 -0
  150. package/vendor/converters/officeparser/dist/generators/BaseGenerator.js +248 -0
  151. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.d.ts +82 -0
  152. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.js +797 -0
  153. package/vendor/converters/officeparser/dist/generators/CsvGenerator.d.ts +38 -0
  154. package/vendor/converters/officeparser/dist/generators/CsvGenerator.js +245 -0
  155. package/vendor/converters/officeparser/dist/generators/EpubGenerator.d.ts +43 -0
  156. package/vendor/converters/officeparser/dist/generators/EpubGenerator.js +315 -0
  157. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.d.ts +59 -0
  158. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.js +1942 -0
  159. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.d.ts +96 -0
  160. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.js +1175 -0
  161. package/vendor/converters/officeparser/dist/generators/PdfGenerator.d.ts +22 -0
  162. package/vendor/converters/officeparser/dist/generators/PdfGenerator.js +194 -0
  163. package/vendor/converters/officeparser/dist/generators/RtfGenerator.d.ts +29 -0
  164. package/vendor/converters/officeparser/dist/generators/RtfGenerator.js +316 -0
  165. package/vendor/converters/officeparser/dist/generators/TextGenerator.d.ts +13 -0
  166. package/vendor/converters/officeparser/dist/generators/TextGenerator.js +201 -0
  167. package/vendor/converters/officeparser/dist/index.d.ts +60 -0
  168. package/vendor/converters/officeparser/dist/index.js +72 -0
  169. package/vendor/converters/officeparser/dist/index.mjs +18 -0
  170. package/vendor/converters/officeparser/dist/officeparser.browser.d.ts +2621 -0
  171. package/vendor/converters/officeparser/dist/officeparser.browser.iife.js +1336 -0
  172. package/vendor/converters/officeparser/dist/officeparser.browser.mjs +1335 -0
  173. package/vendor/converters/officeparser/dist/officeparser.browser.slim.d.ts +2621 -0
  174. package/vendor/converters/officeparser/dist/officeparser.browser.slim.iife.js +1336 -0
  175. package/vendor/converters/officeparser/dist/officeparser.browser.slim.mjs +1335 -0
  176. package/vendor/converters/officeparser/dist/parsers/CsvParser.d.ts +9 -0
  177. package/vendor/converters/officeparser/dist/parsers/CsvParser.js +115 -0
  178. package/vendor/converters/officeparser/dist/parsers/EpubParser.d.ts +8 -0
  179. package/vendor/converters/officeparser/dist/parsers/EpubParser.js +217 -0
  180. package/vendor/converters/officeparser/dist/parsers/ExcelParser.d.ts +32 -0
  181. package/vendor/converters/officeparser/dist/parsers/ExcelParser.js +736 -0
  182. package/vendor/converters/officeparser/dist/parsers/HtmlParser.d.ts +2 -0
  183. package/vendor/converters/officeparser/dist/parsers/HtmlParser.js +1287 -0
  184. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.d.ts +2 -0
  185. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.js +1272 -0
  186. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.d.ts +31 -0
  187. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.js +1819 -0
  188. package/vendor/converters/officeparser/dist/parsers/PdfParser.d.ts +67 -0
  189. package/vendor/converters/officeparser/dist/parsers/PdfParser.js +848 -0
  190. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.d.ts +32 -0
  191. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.js +950 -0
  192. package/vendor/converters/officeparser/dist/parsers/RtfParser.d.ts +187 -0
  193. package/vendor/converters/officeparser/dist/parsers/RtfParser.js +1801 -0
  194. package/vendor/converters/officeparser/dist/parsers/WordParser.d.ts +79 -0
  195. package/vendor/converters/officeparser/dist/parsers/WordParser.js +1177 -0
  196. package/vendor/converters/officeparser/dist/sbom.cdx.json +1763 -0
  197. package/vendor/converters/officeparser/dist/types.d.ts +2507 -0
  198. package/vendor/converters/officeparser/dist/types.js +107 -0
  199. package/vendor/converters/officeparser/dist/utils/astUtils.d.ts +16 -0
  200. package/vendor/converters/officeparser/dist/utils/astUtils.js +33 -0
  201. package/vendor/converters/officeparser/dist/utils/chartUtils.d.ts +6 -0
  202. package/vendor/converters/officeparser/dist/utils/chartUtils.js +257 -0
  203. package/vendor/converters/officeparser/dist/utils/configUtils.d.ts +44 -0
  204. package/vendor/converters/officeparser/dist/utils/configUtils.js +315 -0
  205. package/vendor/converters/officeparser/dist/utils/dateUtils.d.ts +17 -0
  206. package/vendor/converters/officeparser/dist/utils/dateUtils.js +69 -0
  207. package/vendor/converters/officeparser/dist/utils/envUtils.d.ts +29 -0
  208. package/vendor/converters/officeparser/dist/utils/envUtils.js +152 -0
  209. package/vendor/converters/officeparser/dist/utils/errorUtils.d.ts +72 -0
  210. package/vendor/converters/officeparser/dist/utils/errorUtils.js +245 -0
  211. package/vendor/converters/officeparser/dist/utils/imageUtils.d.ts +66 -0
  212. package/vendor/converters/officeparser/dist/utils/imageUtils.js +133 -0
  213. package/vendor/converters/officeparser/dist/utils/mathUtils.d.ts +42 -0
  214. package/vendor/converters/officeparser/dist/utils/mathUtils.js +385 -0
  215. package/vendor/converters/officeparser/dist/utils/moduleLoader.d.ts +18 -0
  216. package/vendor/converters/officeparser/dist/utils/moduleLoader.js +106 -0
  217. package/vendor/converters/officeparser/dist/utils/ocrUtils.d.ts +42 -0
  218. package/vendor/converters/officeparser/dist/utils/ocrUtils.js +428 -0
  219. package/vendor/converters/officeparser/dist/utils/sanitize.d.ts +148 -0
  220. package/vendor/converters/officeparser/dist/utils/sanitize.js +344 -0
  221. package/vendor/converters/officeparser/dist/utils/sheetUtils.d.ts +7 -0
  222. package/vendor/converters/officeparser/dist/utils/sheetUtils.js +35 -0
  223. package/vendor/converters/officeparser/dist/utils/styleMapper.d.ts +36 -0
  224. package/vendor/converters/officeparser/dist/utils/styleMapper.js +224 -0
  225. package/vendor/converters/officeparser/dist/utils/xmlUtils.d.ts +163 -0
  226. package/vendor/converters/officeparser/dist/utils/xmlUtils.js +461 -0
  227. package/vendor/converters/officeparser/dist/utils/zipUtils.d.ts +134 -0
  228. package/vendor/converters/officeparser/dist/utils/zipUtils.js +337 -0
  229. package/vendor/converters/officeparser/package.json +147 -0
  230. package/vendor/converters/upstream-manifest.json +124 -0
  231. package/vendor/dependency-fixes/README.md +77 -0
  232. package/vendor/dependency-fixes/vendor-converters.py +83 -0
  233. package/browser-extension/artifacts/gno-browser-clipper-v1.45.1.zip +0 -0
  234. package/browser-extension/artifacts/gno-browser-clipper-v1.45.1.zip.sha256 +0 -1
  235. package/browser-extension/dist/chunk-627emwpj.js +0 -75
  236. /package/browser-extension/dist/{chunk-ydfx5d7p.css → chunk-z74y8n8c.css} +0 -0
@@ -1,33 +1,29 @@
1
- /**
2
- * Model lifecycle manager.
3
- * Handles lazy loading, caching, and disposal of LLM models.
4
- *
5
- * @module src/llm/nodeLlamaCpp/lifecycle
6
- */
7
-
8
- import type { LlamaOptions } from "node-llama-cpp";
9
-
10
- import { platform } from "node:os";
1
+ import type { LlamaInitOptions } from "./lifecycle-options";
2
+
3
+ import {
4
+ resolveLlamaGpuMode,
5
+ resolveLlamaBuildMode,
6
+ resolveLlamaBackendInitTimeoutMs,
7
+ shouldRetryLlamaWithCpu,
8
+ } from "./lifecycle-options";
9
+ export {
10
+ resolveLlamaGpuMode,
11
+ resolveLlamaBuildMode,
12
+ resolveLlamaBackendInitTimeoutMs,
13
+ shouldRetryLlamaWithCpu,
14
+ } from "./lifecycle-options";
15
+ export type { LlamaGpuMode, LlamaBuildMode } from "./lifecycle-options";
11
16
 
12
17
  import type { ModelConfig } from "../../config/types";
13
18
  import type { LlmResult, LoadedModel, ModelType } from "../types";
14
19
 
15
20
  import { loadFailedError, outOfMemoryError, timeoutError } from "../errors";
21
+ import { installSimulatorLifetimeGuard } from "./simulator-install";
16
22
 
17
- // ─────────────────────────────────────────────────────────────────────────────
18
23
  // Types
19
- // ─────────────────────────────────────────────────────────────────────────────
20
24
 
21
25
  type Llama = Awaited<ReturnType<typeof import("node-llama-cpp").getLlama>>;
22
26
  type LlamaModel = Awaited<ReturnType<Llama["loadModel"]>>;
23
- export type LlamaGpuMode = "auto" | "metal" | "vulkan" | "cuda" | false;
24
- export type LlamaBuildMode = "never" | "autoAttempt";
25
-
26
- type LlamaInitOptions = LlamaOptions & {
27
- build: LlamaBuildMode;
28
- gpu: LlamaGpuMode;
29
- };
30
-
31
27
  interface CachedModel {
32
28
  uri: string;
33
29
  type: ModelType;
@@ -50,100 +46,14 @@ export interface ModelLifecycleStats {
50
46
  inflightLoads: number;
51
47
  }
52
48
 
53
- let invalidGpuModeWarned = false;
54
- let invalidBuildModeWarned = false;
55
49
  let gpuFallbackWarned = false;
56
50
  let backendTimeoutWarned = false;
57
51
 
58
- const DEFAULT_BACKEND_INIT_TIMEOUT_MS = 30_000;
59
-
60
- export function resolveLlamaGpuMode(
61
- env: NodeJS.ProcessEnv = process.env
62
- ): LlamaGpuMode {
63
- const raw = (env.GNO_LLAMA_GPU ?? env.NODE_LLAMA_CPP_GPU ?? "auto")
64
- .trim()
65
- .toLowerCase();
66
- if (!raw || raw === "auto") {
67
- return "auto";
68
- }
69
- if (raw === "metal" || raw === "vulkan" || raw === "cuda") {
70
- return raw;
71
- }
72
- if (
73
- raw === "false" ||
74
- raw === "off" ||
75
- raw === "none" ||
76
- raw === "disable" ||
77
- raw === "disabled" ||
78
- raw === "0"
79
- ) {
80
- return false;
81
- }
82
- if (!invalidGpuModeWarned) {
83
- invalidGpuModeWarned = true;
84
- console.warn(
85
- `[llama] Invalid GNO_LLAMA_GPU/NODE_LLAMA_CPP_GPU value "${raw}", using auto`
86
- );
87
- }
88
- return "auto";
89
- }
90
-
91
- export function resolveLlamaBuildMode(
92
- env: NodeJS.ProcessEnv = process.env
93
- ): LlamaBuildMode {
94
- const raw = (env.GNO_LLAMA_BUILD ?? "never").trim().toLowerCase();
95
- if (
96
- !raw ||
97
- raw === "never" ||
98
- raw === "prebuilt" ||
99
- raw === "prebuilt-only"
100
- ) {
101
- return "never";
102
- }
103
- if (
104
- raw === "autoattempt" ||
105
- raw === "auto-attempt" ||
106
- raw === "source" ||
107
- raw === "build"
108
- ) {
109
- return "autoAttempt";
110
- }
111
- if (!invalidBuildModeWarned) {
112
- invalidBuildModeWarned = true;
113
- console.warn(`[llama] Invalid GNO_LLAMA_BUILD value "${raw}", using never`);
114
- }
115
- return "never";
116
- }
117
-
118
- export function resolveLlamaBackendInitTimeoutMs(
119
- env: NodeJS.ProcessEnv = process.env
120
- ): number {
121
- const raw = env.GNO_LLAMA_INIT_TIMEOUT_MS;
122
- if (!raw) {
123
- return DEFAULT_BACKEND_INIT_TIMEOUT_MS;
124
- }
125
- const parsed = Number.parseInt(raw, 10);
126
- return Number.isFinite(parsed) && parsed > 0
127
- ? parsed
128
- : DEFAULT_BACKEND_INIT_TIMEOUT_MS;
129
- }
130
-
131
- export function shouldRetryLlamaWithCpu(
132
- gpu: LlamaGpuMode,
133
- platformName = platform()
134
- ): boolean {
135
- if (gpu === false) {
136
- return false;
137
- }
138
- return gpu !== "auto" || platformName === "win32";
139
- }
140
-
141
- // ─────────────────────────────────────────────────────────────────────────────
142
- // ModelManager
143
- // ─────────────────────────────────────────────────────────────────────────────
144
-
145
52
  export class ModelManager {
146
53
  private llama: Llama | null = null;
54
+ private llamaInit: Promise<Llama> | null = null;
55
+ private closing: Promise<void> | null = null;
56
+ private readonly lateCleanup = new Set<Promise<void>>();
147
57
  private readonly models: Map<string, CachedModel> = new Map();
148
58
  private readonly disposalTimers: Map<string, ReturnType<typeof setTimeout>> =
149
59
  new Map();
@@ -152,22 +62,38 @@ export class ModelManager {
152
62
  private readonly leaseDrainWaiters = new Set<() => void>();
153
63
  private readonly config: ModelConfig;
154
64
  private activeLeases = 0;
65
+ private broadLeases = 0;
66
+ private readonly modelLeases = new Map<string, number>();
67
+ private readonly expiresAt = new Map<string, number>();
68
+ private readonly retiringModels = new Map<string, Promise<void>>();
155
69
  private leaseAcquisitions = 0;
156
70
  private leaseReleases = 0;
157
71
  private loadAttempts = 0;
158
72
  private loadSuccesses = 0;
159
73
  private loadFailures = 0;
160
74
 
161
- constructor(config: ModelConfig) {
75
+ constructor(
76
+ config: ModelConfig,
77
+ private readonly awaitNativeLoadSettlement = false
78
+ ) {
162
79
  this.config = config;
163
80
  }
164
81
 
165
- /**
166
- * Get or initialize the Llama instance.
167
- * Uses lazy loading - only imports node-llama-cpp on first use.
168
- */
169
82
  async getLlama(): Promise<Llama> {
83
+ if (this.closing) throw new Error("Model manager is disposing");
84
+ if (this.llama) return this.llama;
85
+ this.llamaInit ??= this.initializeLlama().finally(() => {
86
+ this.llamaInit = null;
87
+ });
88
+ return this.llamaInit;
89
+ }
90
+
91
+ private async initializeLlama(): Promise<Llama> {
92
+ // A timed-out native constructor still owns work until its loser is cleaned.
93
+ await Promise.allSettled(this.lateCleanup);
94
+ if (this.closing) throw new Error("Model manager is disposing");
170
95
  if (!this.llama) {
96
+ await installSimulatorLifetimeGuard();
171
97
  const { getLlama, LlamaLogLevel } = await import("node-llama-cpp");
172
98
  const gpu = resolveLlamaGpuMode();
173
99
  const build = resolveLlamaBuildMode();
@@ -184,7 +110,11 @@ export class ModelManager {
184
110
  timeoutMs
185
111
  );
186
112
  } catch (error) {
187
- if (!shouldRetryLlamaWithCpu(gpu)) {
113
+ if (
114
+ this.closing ||
115
+ (error instanceof Error && error.name === "TimeoutError") ||
116
+ !shouldRetryLlamaWithCpu(gpu)
117
+ ) {
188
118
  throw error;
189
119
  }
190
120
  if (!gpuFallbackWarned) {
@@ -206,6 +136,12 @@ export class ModelManager {
206
136
  );
207
137
  }
208
138
  }
139
+ if (this.closing) {
140
+ const llama = this.llama;
141
+ this.llama = null;
142
+ await llama.dispose();
143
+ throw new Error("Model manager disposed during initialization");
144
+ }
209
145
  return this.llama;
210
146
  }
211
147
 
@@ -215,21 +151,46 @@ export class ModelManager {
215
151
  timeoutMs: number
216
152
  ): Promise<Llama> {
217
153
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
154
+ let expired = false;
155
+ const initializationDeadline = performance.now() + timeoutMs;
156
+ const initialization = getLlama(options).then(async (llama) => {
157
+ if (expired || performance.now() >= initializationDeadline) {
158
+ await llama.dispose();
159
+ throw new DOMException(
160
+ "Backend initialization expired",
161
+ "TimeoutError"
162
+ );
163
+ }
164
+ return llama;
165
+ });
166
+ const cleanup = initialization.then(
167
+ () => {},
168
+ () => {}
169
+ );
170
+ this.lateCleanup.add(cleanup);
171
+ void cleanup.finally(() => this.lateCleanup.delete(cleanup));
218
172
  try {
219
- return await Promise.race([
220
- getLlama(options),
221
- new Promise<never>((_, reject) => {
222
- timeoutId = setTimeout(() => {
223
- if (!backendTimeoutWarned) {
224
- backendTimeoutWarned = true;
225
- console.warn(
226
- `[llama] Backend initialization timed out after ${timeoutMs}ms`
227
- );
228
- }
229
- reject(new Error(`Backend init timeout after ${timeoutMs}ms`));
230
- }, timeoutMs);
231
- }),
232
- ]);
173
+ const deadline = new Promise<never>((_, reject) => {
174
+ timeoutId = setTimeout(() => {
175
+ expired = true;
176
+ if (!backendTimeoutWarned) {
177
+ backendTimeoutWarned = true;
178
+ console.warn(
179
+ `[llama] Backend initialization timed out after ${timeoutMs}ms`
180
+ );
181
+ }
182
+ const error = new Error(`Backend init timeout after ${timeoutMs}ms`);
183
+ error.name = "TimeoutError";
184
+ reject(error);
185
+ }, timeoutMs);
186
+ });
187
+ // Native child completion must wait for actual backend settlement even
188
+ // after the init budget expires. Expiry still rejects and forbids fallback.
189
+ if (this.awaitNativeLoadSettlement) {
190
+ void deadline.catch(() => {});
191
+ return await initialization;
192
+ }
193
+ return await Promise.race([initialization, deadline]);
233
194
  } finally {
234
195
  if (timeoutId) {
235
196
  clearTimeout(timeoutId);
@@ -237,19 +198,23 @@ export class ModelManager {
237
198
  }
238
199
  }
239
200
 
240
- /**
241
- * Load a model by path.
242
- * Uses caching, inflight deduplication, and TTL-based disposal.
243
- */
244
201
  loadModel(
245
202
  modelPath: string,
246
203
  uri: string,
247
- type: ModelType
204
+ type: ModelType,
205
+ signal?: AbortSignal
248
206
  ): Promise<LlmResult<LoadedModel>> {
207
+ const retiring = this.retiringModels.get(uri);
208
+ if (retiring)
209
+ return retiring.then(() => this.loadModel(modelPath, uri, type, signal));
210
+ if (this.closing)
211
+ return Promise.resolve({
212
+ ok: false,
213
+ error: loadFailedError(uri, new Error("Model manager is disposing")),
214
+ });
249
215
  // Check cache first
250
216
  const cached = this.models.get(uri);
251
217
  if (cached) {
252
- this.resetDisposalTimer(uri);
253
218
  return Promise.resolve({
254
219
  ok: true as const,
255
220
  value: {
@@ -268,50 +233,76 @@ export class ModelManager {
268
233
  }
269
234
 
270
235
  // Start new load with cleanup
271
- const loadPromise = this.loadModelInternal(modelPath, uri, type).finally(
272
- () => {
273
- this.inflightLoads.delete(uri);
274
- }
275
- );
236
+ const loadPromise = this.loadModelInternal(
237
+ modelPath,
238
+ uri,
239
+ type,
240
+ signal
241
+ ).finally(() => {
242
+ this.inflightLoads.delete(uri);
243
+ });
276
244
  this.inflightLoads.set(uri, loadPromise);
277
245
  return loadPromise;
278
246
  }
279
247
 
280
- /**
281
- * Internal model loading with timeout handling.
282
- */
283
248
  private async loadModelInternal(
284
249
  modelPath: string,
285
250
  uri: string,
286
- type: ModelType
251
+ type: ModelType,
252
+ signal?: AbortSignal
287
253
  ): Promise<LlmResult<LoadedModel>> {
288
254
  this.loadAttempts += 1;
289
255
  const timeoutMs = this.config.loadTimeout;
290
256
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
291
257
  let timedOut = false;
258
+ const controller = new AbortController();
259
+ const loadSignal = signal
260
+ ? AbortSignal.any([signal, controller.signal])
261
+ : controller.signal;
292
262
 
293
263
  // Capture loadPromise outside try block so we can dispose it on timeout
294
264
  let loadPromise: Promise<LlamaModel> | null = null;
295
265
 
296
266
  try {
297
267
  const llama = await this.getLlama();
298
- loadPromise = llama.loadModel({ modelPath });
268
+ loadSignal.throwIfAborted();
269
+ const loadDeadline = performance.now() + timeoutMs;
270
+ loadPromise = llama.loadModel({ modelPath, loadSignal });
299
271
 
300
272
  // Create timeout with proper cleanup
301
273
  const timeoutPromise = new Promise<never>((_, reject) => {
302
274
  timeoutId = setTimeout(() => {
303
275
  timedOut = true;
276
+ controller.abort(
277
+ new DOMException("Model load timed out", "TimeoutError")
278
+ );
304
279
  reject(new Error(`Load timeout after ${timeoutMs}ms`));
305
280
  }, timeoutMs);
306
281
  });
307
282
 
308
- const model = await Promise.race([loadPromise, timeoutPromise]);
283
+ // The isolated child cannot report completion while a noncooperative
284
+ // loader still owns native allocations. Parent cancellation delivers early.
285
+ if (this.awaitNativeLoadSettlement) void timeoutPromise.catch(() => {});
286
+ const model = this.awaitNativeLoadSettlement
287
+ ? await loadPromise
288
+ : await Promise.race([loadPromise, timeoutPromise]);
289
+ if (this.awaitNativeLoadSettlement && performance.now() >= loadDeadline)
290
+ timedOut = true;
291
+ if (this.awaitNativeLoadSettlement && (timedOut || loadSignal.aborted)) {
292
+ await model.dispose();
293
+ if (timedOut) throw new Error("Model load timeout");
294
+ loadSignal.throwIfAborted();
295
+ }
309
296
 
310
297
  // Clear timeout on success
311
298
  if (timeoutId) {
312
299
  clearTimeout(timeoutId);
313
300
  }
314
301
 
302
+ if (this.closing) {
303
+ await model.dispose();
304
+ throw new Error("Model manager disposed during model loading");
305
+ }
315
306
  const now = Date.now();
316
307
  const cachedModel: CachedModel = {
317
308
  uri,
@@ -321,6 +312,7 @@ export class ModelManager {
321
312
  };
322
313
 
323
314
  this.models.set(uri, cachedModel);
315
+ this.expiresAt.set(uri, Date.now() + this.config.warmModelTtl);
324
316
  this.setDisposalTimer(uri);
325
317
  this.loadSuccesses += 1;
326
318
 
@@ -341,11 +333,11 @@ export class ModelManager {
341
333
  }
342
334
 
343
335
  // Dispose late-arriving model after timeout to prevent memory leak
344
- if (timedOut && loadPromise) {
345
- loadPromise.then(
336
+ if (timedOut && loadPromise && !this.awaitNativeLoadSettlement) {
337
+ const cleanup = loadPromise.then(
346
338
  (model) => {
347
339
  // Dispose model that arrived after timeout
348
- model.dispose().catch(() => {
340
+ return model.dispose().catch(() => {
349
341
  // Ignore dispose errors
350
342
  });
351
343
  },
@@ -353,10 +345,16 @@ export class ModelManager {
353
345
  // Ignore load errors after timeout
354
346
  }
355
347
  );
348
+ this.lateCleanup.add(cleanup);
349
+ void cleanup.finally(() => this.lateCleanup.delete(cleanup));
356
350
  }
357
351
 
358
352
  if (e instanceof Error) {
359
- if (e.message.includes("timeout")) {
353
+ if (
354
+ timedOut ||
355
+ e.name === "TimeoutError" ||
356
+ e.message.includes("timeout")
357
+ ) {
360
358
  return {
361
359
  ok: false,
362
360
  error: timeoutError(uri, "load", this.config.loadTimeout),
@@ -370,23 +368,23 @@ export class ModelManager {
370
368
  }
371
369
  }
372
370
 
373
- /**
374
- * Get a loaded model by URI (no loading).
375
- */
376
371
  getLoadedModel(uri: string): CachedModel | undefined {
377
- const model = this.models.get(uri);
378
- if (model) {
379
- this.resetDisposalTimer(uri);
380
- }
381
- return model;
372
+ if (this.closing) return;
373
+ return this.models.get(uri);
382
374
  }
383
375
 
384
- /** Keep warm models alive while one request owns model-backed ports. */
385
- acquireLease(): ModelLease {
376
+ acquireLease(uri?: string, activity = true): ModelLease {
377
+ if (this.closing) throw new Error("Model manager is disposing");
386
378
  this.activeLeases += 1;
387
379
  this.leaseAcquisitions += 1;
388
- for (const timer of this.disposalTimers.values()) clearTimeout(timer);
389
- this.disposalTimers.clear();
380
+ if (uri === undefined) this.broadLeases += 1;
381
+ else this.modelLeases.set(uri, (this.modelLeases.get(uri) ?? 0) + 1);
382
+ for (const [key, timer] of this.disposalTimers) {
383
+ if (uri === undefined || key === uri) {
384
+ clearTimeout(timer);
385
+ this.disposalTimers.delete(key);
386
+ }
387
+ }
390
388
 
391
389
  let released = false;
392
390
  return {
@@ -395,10 +393,22 @@ export class ModelManager {
395
393
  released = true;
396
394
  this.activeLeases = Math.max(0, this.activeLeases - 1);
397
395
  this.leaseReleases += 1;
396
+ if (uri === undefined) this.broadLeases -= 1;
397
+ else {
398
+ const count = (this.modelLeases.get(uri) ?? 1) - 1;
399
+ if (count) this.modelLeases.set(uri, count);
400
+ else this.modelLeases.delete(uri);
401
+ }
402
+ for (const key of this.models.keys()) {
403
+ if (uri === undefined || key === uri) {
404
+ if (activity)
405
+ this.expiresAt.set(key, Date.now() + this.config.warmModelTtl);
406
+ this.resetDisposalTimer(key);
407
+ }
408
+ }
398
409
  if (this.activeLeases === 0) {
399
410
  for (const resolve of this.leaseDrainWaiters) resolve();
400
411
  this.leaseDrainWaiters.clear();
401
- for (const uri of this.models.keys()) this.setDisposalTimer(uri);
402
412
  }
403
413
  },
404
414
  };
@@ -417,21 +427,14 @@ export class ModelManager {
417
427
  };
418
428
  }
419
429
 
420
- /**
421
- * Check if a model is loaded.
422
- */
423
430
  isLoaded(uri: string): boolean {
424
431
  return this.models.has(uri);
425
432
  }
426
433
 
427
- /**
428
- * Dispose a specific model.
429
- */
430
434
  async dispose(uri: string): Promise<void> {
431
- if (this.activeLeases > 0) {
432
- this.resetDisposalTimer(uri);
433
- return;
434
- }
435
+ if (this.broadLeases > 0 || this.modelLeases.has(uri)) return;
436
+ const retiring = this.retiringModels.get(uri);
437
+ if (retiring) return retiring;
435
438
  const cached = this.models.get(uri);
436
439
  if (!cached) {
437
440
  return;
@@ -444,20 +447,29 @@ export class ModelManager {
444
447
  this.disposalTimers.delete(uri);
445
448
  }
446
449
 
447
- // Dispose the model
448
- try {
449
- await cached.model.dispose();
450
- } catch {
451
- // Ignore disposal errors
452
- }
453
-
450
+ // Remove ownership before yielding so acquisitions cannot see a retiring model.
454
451
  this.models.delete(uri);
452
+ this.expiresAt.delete(uri);
453
+ const cleanup = cached.model
454
+ .dispose()
455
+ .catch(() => {})
456
+ .finally(() => {
457
+ this.retiringModels.delete(uri);
458
+ });
459
+ this.retiringModels.set(uri, cleanup);
460
+ this.lateCleanup.add(cleanup);
461
+ await cleanup;
462
+ this.lateCleanup.delete(cleanup);
463
+ }
464
+
465
+ disposeAll(): Promise<void> {
466
+ this.closing ??= this.disposeAllInternal().finally(() => {
467
+ this.closing = null;
468
+ });
469
+ return this.closing;
455
470
  }
456
471
 
457
- /**
458
- * Dispose all loaded models.
459
- */
460
- async disposeAll(): Promise<void> {
472
+ private async disposeAllInternal(): Promise<void> {
461
473
  if (this.activeLeases > 0) {
462
474
  await new Promise<void>((resolve) => this.leaseDrainWaiters.add(resolve));
463
475
  }
@@ -479,13 +491,13 @@ export class ModelManager {
479
491
  this.models.delete(uri);
480
492
  }
481
493
 
482
- // Clear llama instance
494
+ await this.llamaInit?.catch(() => {});
495
+ await Promise.allSettled(this.lateCleanup);
496
+ const llama = this.llama;
483
497
  this.llama = null;
498
+ await llama?.dispose();
484
499
  }
485
500
 
486
- /**
487
- * Get list of loaded models.
488
- */
489
501
  getLoadedModels(): Array<{ uri: string; type: ModelType; loadedAt: number }> {
490
502
  return Array.from(this.models.values()).map((m) => ({
491
503
  uri: m.uri,
@@ -494,17 +506,18 @@ export class ModelManager {
494
506
  }));
495
507
  }
496
508
 
497
- // ───────────────────────────────────────────────────────────────────────────
498
509
  // Private
499
- // ───────────────────────────────────────────────────────────────────────────
500
510
 
501
511
  private setDisposalTimer(uri: string): void {
502
- if (this.activeLeases > 0) return;
503
- const timer = setTimeout(() => {
504
- this.dispose(uri).catch(() => {
505
- // Ignore disposal errors in timer callback
506
- });
507
- }, this.config.warmModelTtl);
512
+ if (this.broadLeases > 0 || this.modelLeases.has(uri)) return;
513
+ const timer = setTimeout(
514
+ () => {
515
+ this.dispose(uri).catch(() => {
516
+ // Ignore disposal errors in timer callback
517
+ });
518
+ },
519
+ Math.max(0, (this.expiresAt.get(uri) ?? Date.now()) - Date.now())
520
+ );
508
521
 
509
522
  // Allow CLI processes to exit without waiting for TTL timer
510
523
  if (typeof timer.unref === "function") {
@@ -523,15 +536,10 @@ export class ModelManager {
523
536
  }
524
537
  }
525
538
 
526
- // ─────────────────────────────────────────────────────────────────────────────
527
539
  // Singleton
528
- // ─────────────────────────────────────────────────────────────────────────────
529
540
 
530
541
  let defaultManager: ModelManager | null = null;
531
542
 
532
- /**
533
- * Get the default ModelManager singleton.
534
- */
535
543
  export function getModelManager(config: ModelConfig): ModelManager {
536
544
  if (!defaultManager) {
537
545
  defaultManager = new ModelManager(config);
@@ -539,9 +547,6 @@ export function getModelManager(config: ModelConfig): ModelManager {
539
547
  return defaultManager;
540
548
  }
541
549
 
542
- /**
543
- * Reset the default manager (for testing).
544
- */
545
550
  export async function resetModelManager(): Promise<void> {
546
551
  if (defaultManager) {
547
552
  await defaultManager.disposeAll();