@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,11 +1,15 @@
1
+ import type { SyncResult } from "../ingestion";
1
2
  /**
2
3
  * Background job manager for MCP write operations.
3
4
  *
4
5
  * @module src/core/job-manager
5
6
  */
6
7
 
7
- import type { SyncResult } from "../ingestion";
8
-
8
+ import {
9
+ assertInferenceActive,
10
+ withOwnedInferenceScope,
11
+ withBackgroundInference,
12
+ } from "../llm/inference-scope";
9
13
  import { MCP_ERRORS } from "./errors";
10
14
  import { acquireWriteLock, type WriteLockHandle } from "./file-lock";
11
15
 
@@ -78,7 +82,10 @@ export class JobManager {
78
82
  #jobs = new Map<string, JobRecord>();
79
83
  #jobAuthorizationEpochs = new Map<string, string>();
80
84
  #activeJobs = new Set<Promise<void>>();
85
+ #jobControllers = new Map<string, AbortController>();
81
86
  #authorizationEpoch = "egress-epoch-uninitialized";
87
+ #accepting = true;
88
+ #releaseJobLocks = new Map<string, () => Promise<void>>();
82
89
 
83
90
  constructor(options: JobManagerOptions) {
84
91
  this.#lockPath = options.lockPath;
@@ -89,8 +96,10 @@ export class JobManager {
89
96
 
90
97
  async startJob(
91
98
  type: JobType,
92
- fn: () => Promise<SyncResult>
99
+ fn: (signal: AbortSignal) => Promise<SyncResult>
93
100
  ): Promise<string> {
101
+ assertInferenceActive();
102
+ this.#assertAccepting();
94
103
  this.#cleanupExpiredJobs();
95
104
 
96
105
  if (this.#activeJobId) {
@@ -105,14 +114,21 @@ export class JobManager {
105
114
  throw new JobError("LOCKED", MCP_ERRORS.LOCKED.message);
106
115
  }
107
116
 
117
+ if (!this.#accepting) {
118
+ await lock.release();
119
+ this.#assertAccepting();
120
+ }
121
+
108
122
  return this.#startJobWithLock(type, fn, lock);
109
123
  }
110
124
 
111
125
  async startJobWithLock(
112
126
  type: JobType,
113
127
  lock: WriteLockHandle,
114
- fn: () => Promise<SyncResult>
128
+ fn: (signal: AbortSignal) => Promise<SyncResult>
115
129
  ): Promise<string> {
130
+ assertInferenceActive();
131
+ this.#assertAccepting();
116
132
  this.#cleanupExpiredJobs();
117
133
 
118
134
  if (this.#activeJobId) {
@@ -132,8 +148,10 @@ export class JobManager {
132
148
  async startTypedJobWithLock(
133
149
  type: JobType,
134
150
  lock: WriteLockHandle,
135
- fn: () => Promise<JobResult>
151
+ fn: (signal: AbortSignal) => Promise<JobResult>
136
152
  ): Promise<string> {
153
+ assertInferenceActive();
154
+ this.#assertAccepting();
137
155
  this.#cleanupExpiredJobs();
138
156
 
139
157
  if (this.#activeJobId) {
@@ -205,10 +223,56 @@ export class JobManager {
205
223
  return { active, recent };
206
224
  }
207
225
 
226
+ /** Internal owner control; cancellation retains the existing failed job schema. */
227
+ cancelJob(jobId: string): boolean {
228
+ const controller = this.#jobControllers.get(jobId);
229
+ if (!controller) return false;
230
+ controller.abort();
231
+ return true;
232
+ }
233
+
208
234
  async shutdown(): Promise<void> {
235
+ this.stop();
209
236
  await Promise.allSettled(this.#activeJobs);
210
237
  }
211
238
 
239
+ stop(): void {
240
+ this.#accepting = false;
241
+ }
242
+
243
+ cancel(): void {
244
+ this.stop();
245
+ for (const controller of this.#jobControllers.values()) controller.abort();
246
+ }
247
+
248
+ /** Call only after the shared shutdown budget; late callbacks stay canceled. */
249
+ async failUnfinished(): Promise<void> {
250
+ this.cancel();
251
+ for (const job of this.#jobs.values()) {
252
+ if (job.status !== "running") continue;
253
+ job.status = "failed";
254
+ job.error = "Resident shutdown deadline exceeded";
255
+ job.completedAt = Date.now();
256
+ }
257
+ await Promise.allSettled(
258
+ Array.from(this.#releaseJobLocks.values(), (release) => release())
259
+ );
260
+ }
261
+
262
+ #assertAccepting(): void {
263
+ if (!this.#accepting) throw new Error("Resident runtime is shutting down");
264
+ }
265
+
266
+ #ownLock(jobId: string, lock: WriteLockHandle): WriteLockHandle {
267
+ let releasing: Promise<void> | undefined;
268
+ const owned = {
269
+ release: () =>
270
+ (releasing ??= Promise.resolve().then(() => lock.release())),
271
+ };
272
+ this.#releaseJobLocks.set(jobId, owned.release);
273
+ return owned;
274
+ }
275
+
212
276
  #track(jobPromise: Promise<void>): void {
213
277
  const tracked = jobPromise.catch(() => undefined);
214
278
  this.#activeJobs.add(tracked);
@@ -219,14 +283,16 @@ export class JobManager {
219
283
 
220
284
  async #runJob(
221
285
  job: JobRecord,
222
- fn: () => Promise<SyncResult>,
286
+ fn: (signal: AbortSignal) => Promise<SyncResult>,
223
287
  lock: { release: () => Promise<void> }
224
288
  ): Promise<void> {
225
289
  try {
226
290
  const release = await this.#toolMutex.acquire();
227
291
  try {
228
292
  this.#assertAuthorizationEpoch(job);
229
- const result = await fn();
293
+ assertInferenceActive();
294
+ const result = await fn(this.#jobControllers.get(job.id)!.signal);
295
+ assertInferenceActive();
230
296
  job.status = "completed";
231
297
  job.result = result;
232
298
  } catch (e) {
@@ -239,6 +305,8 @@ export class JobManager {
239
305
  job.status = "failed";
240
306
  job.error = e instanceof Error ? e.message : String(e);
241
307
  } finally {
308
+ this.#releaseJobLocks.delete(job.id);
309
+ this.#jobControllers.delete(job.id);
242
310
  job.completedAt = Date.now();
243
311
  this.#activeJobId = null;
244
312
  await lock.release().catch(() => undefined);
@@ -248,10 +316,11 @@ export class JobManager {
248
316
 
249
317
  #startJobWithLock(
250
318
  type: JobType,
251
- fn: () => Promise<SyncResult>,
319
+ fn: (signal: AbortSignal) => Promise<SyncResult>,
252
320
  lock: WriteLockHandle
253
321
  ): string {
254
322
  const jobId = crypto.randomUUID();
323
+ const ownedLock = this.#ownLock(jobId, lock);
255
324
  const job: JobRecord = {
256
325
  id: jobId,
257
326
  type,
@@ -260,11 +329,15 @@ export class JobManager {
260
329
  serverInstanceId: this.#serverInstanceId,
261
330
  };
262
331
 
332
+ this.#jobControllers.set(jobId, new AbortController());
263
333
  this.#jobs.set(jobId, job);
264
334
  this.#jobAuthorizationEpochs.set(jobId, this.#authorizationEpoch);
265
335
  this.#activeJobId = jobId;
266
336
 
267
- const jobPromise = this.#runJob(job, fn, lock);
337
+ const jobPromise = withOwnedInferenceScope(
338
+ { signal: this.#jobControllers.get(jobId)!.signal },
339
+ () => withBackgroundInference(() => this.#runJob(job, fn, ownedLock))
340
+ );
268
341
  this.#track(jobPromise);
269
342
 
270
343
  return jobId;
@@ -272,10 +345,11 @@ export class JobManager {
272
345
 
273
346
  #startTypedJobWithLock(
274
347
  type: JobType,
275
- fn: () => Promise<JobResult>,
348
+ fn: (signal: AbortSignal) => Promise<JobResult>,
276
349
  lock: WriteLockHandle
277
350
  ): string {
278
351
  const jobId = crypto.randomUUID();
352
+ const ownedLock = this.#ownLock(jobId, lock);
279
353
  const job: JobRecord = {
280
354
  id: jobId,
281
355
  type,
@@ -284,11 +358,15 @@ export class JobManager {
284
358
  serverInstanceId: this.#serverInstanceId,
285
359
  };
286
360
 
361
+ this.#jobControllers.set(jobId, new AbortController());
287
362
  this.#jobs.set(jobId, job);
288
363
  this.#jobAuthorizationEpochs.set(jobId, this.#authorizationEpoch);
289
364
  this.#activeJobId = jobId;
290
365
 
291
- const jobPromise = this.#runTypedJob(job, fn, lock);
366
+ const jobPromise = withOwnedInferenceScope(
367
+ { signal: this.#jobControllers.get(jobId)!.signal },
368
+ () => withBackgroundInference(() => this.#runTypedJob(job, fn, ownedLock))
369
+ );
292
370
  this.#track(jobPromise);
293
371
 
294
372
  return jobId;
@@ -296,14 +374,16 @@ export class JobManager {
296
374
 
297
375
  async #runTypedJob(
298
376
  job: JobRecord,
299
- fn: () => Promise<JobResult>,
377
+ fn: (signal: AbortSignal) => Promise<JobResult>,
300
378
  lock: { release: () => Promise<void> }
301
379
  ): Promise<void> {
302
380
  try {
303
381
  const release = await this.#toolMutex.acquire();
304
382
  try {
305
383
  this.#assertAuthorizationEpoch(job);
306
- const result = await fn();
384
+ assertInferenceActive();
385
+ const result = await fn(this.#jobControllers.get(job.id)!.signal);
386
+ assertInferenceActive();
307
387
  job.status = "completed";
308
388
  job.typedResult = result;
309
389
  } catch (e) {
@@ -316,6 +396,8 @@ export class JobManager {
316
396
  job.status = "failed";
317
397
  job.error = e instanceof Error ? e.message : String(e);
318
398
  } finally {
399
+ this.#releaseJobLocks.delete(job.id);
400
+ this.#jobControllers.delete(job.id);
319
401
  job.completedAt = Date.now();
320
402
  this.#activeJobId = null;
321
403
  await lock.release().catch(() => undefined);
@@ -149,6 +149,16 @@ export const NETWORK_BOUNDARY_INVENTORY = [
149
149
  action: "remote_inference",
150
150
  enforcement: "collection_policy",
151
151
  },
152
+ {
153
+ // Collection text crosses owned local IPC only. The child receives approved
154
+ // model paths and a restricted environment with downloads/builds disabled.
155
+ id: "native-inference-worker",
156
+ key: "src/llm/native-worker/client.ts::child_process#1",
157
+ path: "src/llm/native-worker/client.ts",
158
+ primitive: "child_process",
159
+ action: null,
160
+ enforcement: "local_process_only",
161
+ },
152
162
  {
153
163
  id: "pinned-http-fetch",
154
164
  key: "src/llm/pinned-http-connection.ts::fetch#1",
@@ -0,0 +1,45 @@
1
+ /** Internal resident shutdown clock. Every participant shares these deadlines. */
2
+ export const SHUTDOWN_DRAIN_MS = 5_000;
3
+ export const SHUTDOWN_ABORT_MS = 5_000;
4
+ export const SHUTDOWN_EXIT_MS = 1_000;
5
+ // The detached parent must not be killed before it can reap its native child.
6
+ export const RESIDENT_STOP_GRACE_MS = 12_000;
7
+
8
+ /** Observe settlement without abandoning rejection handling or retaining a timer. */
9
+ export async function settlesBy(
10
+ work: Promise<unknown>,
11
+ deadline: number,
12
+ interrupt?: AbortSignal
13
+ ): Promise<boolean> {
14
+ let timer: ReturnType<typeof setTimeout> | undefined;
15
+ let interrupted: (() => void) | undefined;
16
+ try {
17
+ return await Promise.race([
18
+ work.then(() => true),
19
+ new Promise<false>((resolve) => {
20
+ interrupted = () => resolve(false);
21
+ interrupt?.addEventListener("abort", interrupted, { once: true });
22
+ if (interrupt?.aborted) resolve(false);
23
+ timer = setTimeout(
24
+ () => resolve(false),
25
+ Math.max(0, deadline - performance.now())
26
+ );
27
+ }),
28
+ ]);
29
+ } finally {
30
+ clearTimeout(timer);
31
+ if (interrupted) interrupt?.removeEventListener("abort", interrupted);
32
+ }
33
+ }
34
+
35
+ export function shutdownDuration(
36
+ value: number | undefined,
37
+ fallback: number
38
+ ): number {
39
+ const duration = value ?? fallback;
40
+ if (!Number.isSafeInteger(duration) || duration < 0)
41
+ throw new RangeError(
42
+ "Shutdown duration must be a nonnegative finite integer"
43
+ );
44
+ return duration;
45
+ }
@@ -1,25 +1,35 @@
1
+ import type { EmbeddingPort } from "../llm/types";
1
2
  /**
2
3
  * Shared embedding backlog processor.
3
4
  * Used by CLI embed, Web scheduler, and MCP tools.
4
5
  *
5
6
  * @module src/embed/backlog
6
7
  */
7
-
8
- import type { EmbeddingPort } from "../llm/types";
9
8
  import type { StoreResult } from "../store/types";
10
9
  import type {
11
10
  BacklogItem,
12
11
  VectorIndexPort,
13
12
  VectorStatsPort,
14
13
  } from "../store/vector";
14
+ import type { VectorVariantStore } from "../store/vector/variants";
15
15
 
16
+ import {
17
+ assertInferenceActive,
18
+ isBackgroundInference,
19
+ } from "../llm/inference-scope";
16
20
  import { err, ok } from "../store/types";
17
- import { getEmbeddingFingerprint } from "./fingerprint";
21
+ import { getVectorStatsDatabase } from "../store/vector/stats";
22
+ import { createVectorVariantStore } from "../store/vector/variants";
23
+ import {
24
+ getEmbeddingFingerprint,
25
+ getVariantModelFingerprint,
26
+ } from "./fingerprint";
18
27
  import {
19
28
  chunkRetryKey,
20
29
  embedAndStoreBatch,
21
30
  MAX_EMBED_CHUNK_ATTEMPTS,
22
31
  } from "./retry";
32
+ import { embedVariantBacklog } from "./variant-backlog";
23
33
 
24
34
  // ─────────────────────────────────────────────────────────────────────────────
25
35
  // Types
@@ -32,6 +42,11 @@ export interface EmbedBacklogDeps {
32
42
  collection?: string;
33
43
  modelUri: string;
34
44
  batchSize?: number;
45
+ force?: boolean;
46
+ onProgress?: (embedded: number, errors: number) => void;
47
+ variantStore?: VectorVariantStore;
48
+ /** Recheck the effective runtime identity after asynchronous inference. */
49
+ identityStillCurrent?: () => boolean;
35
50
  }
36
51
 
37
52
  export interface EmbedBacklogResult {
@@ -62,8 +77,16 @@ interface Cursor {
62
77
  export async function embedBacklog(
63
78
  deps: EmbedBacklogDeps
64
79
  ): Promise<StoreResult<EmbedBacklogResult>> {
80
+ assertInferenceActive();
81
+ const prepared = await prepareEmbeddingBacklog(deps);
82
+ if (!prepared.ok) return prepared;
83
+ deps = prepared.value;
84
+ if (deps.variantStore) return embedVariantBacklog(deps, deps.variantStore);
65
85
  const { statsPort, embedPort, vectorIndex, modelUri, collection } = deps;
66
- const batchSize = deps.batchSize ?? 32;
86
+ const background = isBackgroundInference();
87
+ const batchSize = background
88
+ ? Math.min(deps.batchSize ?? 32, 32)
89
+ : (deps.batchSize ?? 32);
67
90
  const embedFingerprint = getEmbeddingFingerprint({
68
91
  modelUri,
69
92
  dimensions: vectorIndex.dimensions,
@@ -77,6 +100,7 @@ export async function embedBacklog(
77
100
 
78
101
  const enqueueRetryItems = (items: BacklogItem[], attempts: number): void => {
79
102
  for (const item of items) {
103
+ assertInferenceActive();
80
104
  const key = chunkRetryKey(item);
81
105
  const existing = retryQueue.get(key);
82
106
  retryQueue.set(key, {
@@ -97,8 +121,10 @@ export async function embedBacklog(
97
121
  );
98
122
 
99
123
  for (let idx = 0; idx < entries.length; idx += batchSize) {
124
+ assertInferenceActive();
100
125
  const slice = entries.slice(idx, idx + batchSize);
101
126
  for (const entry of slice) {
127
+ assertInferenceActive();
102
128
  retryQueue.delete(chunkRetryKey(entry.item));
103
129
  entry.attempts += 1;
104
130
  }
@@ -109,6 +135,8 @@ export async function embedBacklog(
109
135
  items: slice.map((entry) => entry.item),
110
136
  modelUri,
111
137
  embedFingerprint,
138
+ identityStillCurrent: deps.identityStillCurrent,
139
+ statsPort,
112
140
  });
113
141
 
114
142
  embedded += retryResult.embedded;
@@ -120,6 +148,7 @@ export async function embedBacklog(
120
148
  retryResult.retryItems.map((item) => chunkRetryKey(item))
121
149
  );
122
150
  for (const entry of slice) {
151
+ assertInferenceActive();
123
152
  if (!retryByKey.has(chunkRetryKey(entry.item))) {
124
153
  continue;
125
154
  }
@@ -136,6 +165,7 @@ export async function embedBacklog(
136
165
 
137
166
  try {
138
167
  while (true) {
168
+ assertInferenceActive();
139
169
  // Get next batch using seek pagination
140
170
  const batchResult = await statsPort.getBacklog(
141
171
  modelUri,
@@ -169,10 +199,20 @@ export async function embedBacklog(
169
199
  items: batch,
170
200
  modelUri,
171
201
  embedFingerprint,
202
+ identityStillCurrent: deps.identityStillCurrent,
203
+ statsPort,
172
204
  });
173
205
  embedded += batchStoreResult.embedded;
174
206
  errors += batchStoreResult.errors;
175
207
  contentionErrors += batchStoreResult.contentionErrors;
208
+ if (background) {
209
+ errors += batchStoreResult.retryItems.length;
210
+ deps.onProgress?.(embedded, errors);
211
+ // Each cursor page is a turn. Failed early pages cannot starve later work.
212
+ await Bun.sleep(0);
213
+ if (deps.identityStillCurrent && !deps.identityStillCurrent()) break;
214
+ continue;
215
+ }
176
216
  enqueueRetryItems(batchStoreResult.retryItems, 1);
177
217
 
178
218
  if (embedded > beforeEmbedded) {
@@ -198,6 +238,7 @@ export async function embedBacklog(
198
238
  }
199
239
  }
200
240
 
241
+ assertInferenceActive();
201
242
  return ok({ embedded, errors, contentionErrors, syncError });
202
243
  } catch (e) {
203
244
  return err(
@@ -206,3 +247,65 @@ export async function embedBacklog(
206
247
  );
207
248
  }
208
249
  }
250
+
251
+ /** Resolve authority before counts, dry runs, forced work, or early returns. */
252
+ export async function prepareEmbeddingBacklog(
253
+ deps: EmbedBacklogDeps
254
+ ): Promise<StoreResult<EmbedBacklogDeps>> {
255
+ if (deps.variantStore) return ok(deps);
256
+ const db = getVectorStatsDatabase(deps.statsPort);
257
+ if (db) {
258
+ try {
259
+ const initialized = await deps.embedPort.init();
260
+ if (!initialized.ok) return err("INTERNAL", initialized.error.message);
261
+ const identity = deps.embedPort.getIdentity?.();
262
+ if (identity) {
263
+ const identitySnapshot = JSON.stringify(identity);
264
+ const dimensions = deps.embedPort.dimensions();
265
+ const variantStore = await createVectorVariantStore(db, {
266
+ model: deps.modelUri,
267
+ modelFingerprint: getVariantModelFingerprint(
268
+ { modelUri: deps.modelUri, dimensions },
269
+ identity
270
+ ),
271
+ contextSize: identity.contextSize,
272
+ truncationPolicy: identity.truncationPolicy,
273
+ dimensions,
274
+ });
275
+ return ok({
276
+ ...deps,
277
+ variantStore,
278
+ identityStillCurrent: () =>
279
+ (deps.identityStillCurrent?.() ?? true) &&
280
+ deps.embedPort.modelUri === deps.modelUri &&
281
+ deps.embedPort.dimensions() === dimensions &&
282
+ JSON.stringify(deps.embedPort.getIdentity?.()) === identitySnapshot,
283
+ });
284
+ }
285
+ // Unverified/HTTP ports retain legacy behavior until variant authority exists.
286
+ if (
287
+ db
288
+ .query(
289
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'vector_partitions'"
290
+ )
291
+ .get() &&
292
+ db
293
+ .query(
294
+ "SELECT 1 FROM vector_partitions WHERE model = ? AND state = ? AND activated_epoch IS NOT NULL LIMIT 1"
295
+ )
296
+ .get(deps.modelUri, "active")
297
+ ) {
298
+ return err(
299
+ "INVALID_INPUT",
300
+ "Effective embedding identity unavailable after variant activation"
301
+ );
302
+ }
303
+ } catch (cause) {
304
+ return err(
305
+ "QUERY_FAILED",
306
+ cause instanceof Error ? cause.message : String(cause)
307
+ );
308
+ }
309
+ }
310
+ return ok(deps);
311
+ }
@@ -1,13 +1,17 @@
1
+ import type { EmbeddingPort, LlmResult } from "../llm/types";
1
2
  /**
2
3
  * Shared embedding batch helpers.
3
4
  *
4
5
  * @module src/embed/batch
5
6
  */
6
7
 
7
- import type { EmbeddingPort, LlmResult } from "../llm/types";
8
-
9
8
  import { getEmbeddingCompatibilityProfile } from "../llm/embedding-compatibility";
10
9
  import { inferenceFailedError } from "../llm/errors";
10
+ import {
11
+ assertInferenceActive,
12
+ assertInferenceResult,
13
+ isBackgroundInference,
14
+ } from "../llm/inference-scope";
11
15
 
12
16
  export interface EmbedBatchRecoveryResult {
13
17
  vectors: Array<number[] | null>;
@@ -70,8 +74,26 @@ export async function embedTextsWithRecovery(
70
74
  }
71
75
 
72
76
  const profile = getEmbeddingCompatibilityProfile(embedPort.modelUri);
77
+ // A background page gets one provider attempt. Recovery remains durable for
78
+ // the next pass instead of expanding the current native scheduling turn.
79
+ if (isBackgroundInference() && profile.batchEmbeddingTrusted) {
80
+ const result = await embedPort.embedBatch(texts);
81
+ assertInferenceResult(result);
82
+ if (!result.ok) return result;
83
+ const complete = result.value.length === texts.length;
84
+ return {
85
+ ok: true,
86
+ value: {
87
+ vectors: complete ? result.value : texts.map(() => null),
88
+ batchFailed: !complete,
89
+ fallbackErrors: complete ? 0 : texts.length,
90
+ failureSamples: complete ? [] : ["Embedding count mismatch"],
91
+ },
92
+ };
93
+ }
73
94
  if (profile.batchEmbeddingTrusted) {
74
95
  let batchResult = await embedPort.embedBatch(texts);
96
+ assertInferenceResult(batchResult);
75
97
  if (!batchResult.ok) {
76
98
  const formattedBatchError = formatFailureMessage(batchResult.error);
77
99
  if (isDisposedFailure(formattedBatchError)) {
@@ -80,6 +102,7 @@ export async function embedTextsWithRecovery(
80
102
  return reset;
81
103
  }
82
104
  batchResult = await embedPort.embedBatch(texts);
105
+ assertInferenceResult(batchResult);
83
106
  }
84
107
  }
85
108
  if (batchResult.ok && batchResult.value.length === texts.length) {
@@ -166,6 +189,7 @@ async function recoverWithAdaptiveBatches(
166
189
 
167
190
  if (rangeTexts.length === 1) {
168
191
  const result = await embedPort.embed(rangeTexts[0] ?? "");
192
+ assertInferenceResult(result);
169
193
  if (result.ok) {
170
194
  vectors[offset] = result.value;
171
195
  return;
@@ -179,6 +203,7 @@ async function recoverWithAdaptiveBatches(
179
203
  null;
180
204
  if (!batchAlreadyFailed) {
181
205
  batchResult = await embedPort.embedBatch(rangeTexts);
206
+ assertInferenceResult(batchResult);
182
207
  }
183
208
  if (
184
209
  batchResult &&
@@ -186,6 +211,7 @@ async function recoverWithAdaptiveBatches(
186
211
  batchResult.value.length === rangeTexts.length
187
212
  ) {
188
213
  for (const [index, vector] of batchResult.value.entries()) {
214
+ assertInferenceActive();
189
215
  vectors[offset + index] = vector;
190
216
  }
191
217
  return;
@@ -223,6 +249,12 @@ async function recoverWithAdaptiveBatches(
223
249
  },
224
250
  };
225
251
  } catch (error) {
252
+ assertInferenceActive();
253
+ if (
254
+ error instanceof Error &&
255
+ ["AbortError", "TimeoutError"].includes(error.name)
256
+ )
257
+ throw error;
226
258
  return {
227
259
  ok: false,
228
260
  error: inferenceFailedError(
@@ -245,7 +277,9 @@ async function recoverIndividually(
245
277
  let fallbackErrors = 0;
246
278
 
247
279
  for (const text of texts) {
280
+ assertInferenceActive();
248
281
  const result = await embedPort.embed(text);
282
+ assertInferenceResult(result);
249
283
  if (result.ok) {
250
284
  vectors.push(result.value);
251
285
  } else {
@@ -266,6 +300,12 @@ async function recoverIndividually(
266
300
  },
267
301
  };
268
302
  } catch (error) {
303
+ assertInferenceActive();
304
+ if (
305
+ error instanceof Error &&
306
+ ["AbortError", "TimeoutError"].includes(error.name)
307
+ )
308
+ throw error;
269
309
  return {
270
310
  ok: false,
271
311
  error: inferenceFailedError(
@@ -35,3 +35,19 @@ export function getEmbeddingFingerprint(
35
35
  .update(JSON.stringify(payload))
36
36
  .digest("hex");
37
37
  }
38
+
39
+ /** Partition provenance combines actual weights/runtime with the unchanged formatter policy. */
40
+ export function getVariantModelFingerprint(
41
+ input: EmbeddingFingerprintInput,
42
+ identity: { modelFingerprint: string; runtimeFingerprint: string }
43
+ ): string {
44
+ return new Bun.CryptoHasher("sha256")
45
+ .update(
46
+ JSON.stringify([
47
+ identity.modelFingerprint,
48
+ identity.runtimeFingerprint,
49
+ getEmbeddingFingerprint(input),
50
+ ])
51
+ )
52
+ .digest("hex");
53
+ }