@qvac/harness 0.4.0 → 0.6.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 (299) hide show
  1. package/CHANGELOG.md +740 -141
  2. package/README.md +75 -7
  3. package/dist/harness.bundle +72 -50
  4. package/dist-lib/index.d.ts +49 -39
  5. package/dist-lib/index.js +448 -161
  6. package/dist-lib/lib/audio-transcode-bare.d.ts +2 -2
  7. package/dist-lib/lib/audio-transcode-bare.js +28 -18
  8. package/dist-lib/lib/audio-transcode-node.d.ts +2 -2
  9. package/dist-lib/lib/capabilities/audio-resample.d.ts +7 -6
  10. package/dist-lib/lib/capabilities/audio-resample.js +30 -18
  11. package/dist-lib/lib/capabilities/completion.d.ts +20 -19
  12. package/dist-lib/lib/capabilities/completion.js +193 -132
  13. package/dist-lib/lib/capabilities/download.d.ts +6 -6
  14. package/dist-lib/lib/capabilities/download.js +15 -8
  15. package/dist-lib/lib/capabilities/embed.js +2 -1
  16. package/dist-lib/lib/capabilities/enhance.d.ts +6 -1
  17. package/dist-lib/lib/capabilities/enhance.js +7 -4
  18. package/dist-lib/lib/capabilities/invoke.js +7 -4
  19. package/dist-lib/lib/capabilities/kv-cache.js +2 -1
  20. package/dist-lib/lib/capabilities/preview-model-source.d.ts +9 -7
  21. package/dist-lib/lib/capabilities/preview-model-source.js +80 -28
  22. package/dist-lib/lib/capabilities/speech-text.js +5 -4
  23. package/dist-lib/lib/capabilities/tool-selection.js +10 -9
  24. package/dist-lib/lib/capabilities/tool-turns.js +30 -16
  25. package/dist-lib/lib/capabilities/usage-breakdown.js +2 -1
  26. package/dist-lib/lib/capabilities/voice.d.ts +5 -1
  27. package/dist-lib/lib/capabilities/voice.js +22 -12
  28. package/dist-lib/lib/connect.d.ts +13 -32
  29. package/dist-lib/lib/connect.js +170 -112
  30. package/dist-lib/lib/context-window.js +25 -21
  31. package/dist-lib/lib/contract-types.d.ts +35 -15
  32. package/dist-lib/lib/diagram-repair.d.ts +4 -4
  33. package/dist-lib/lib/diagram-repair.js +6 -3
  34. package/dist-lib/lib/docx/index.d.ts +3 -2
  35. package/dist-lib/lib/{error-fields.d.ts → guards.d.ts} +1 -0
  36. package/dist-lib/lib/{error-fields.js → guards.js} +3 -0
  37. package/dist-lib/lib/hardware/capability-probe.js +20 -10
  38. package/dist-lib/lib/hardware/gpu-classify.js +6 -3
  39. package/dist-lib/lib/hardware/gpu-probe-addon.js +8 -4
  40. package/dist-lib/lib/hardware/gpu-probe.js +4 -2
  41. package/dist-lib/lib/hardware/gpu-stats.native.cjs +8 -9
  42. package/dist-lib/lib/hardware/parsers.js +8 -4
  43. package/dist-lib/lib/helpers/buffer-burst.js +4 -2
  44. package/dist-lib/lib/helpers/control-tokens.js +4 -2
  45. package/dist-lib/lib/helpers/join-guard.d.ts +2 -2
  46. package/dist-lib/lib/helpers/join-guard.js +4 -2
  47. package/dist-lib/lib/helpers/load-stall.js +4 -2
  48. package/dist-lib/lib/helpers/markup-stream-filter.d.ts +2 -2
  49. package/dist-lib/lib/helpers/markup-stream-filter.js +6 -3
  50. package/dist-lib/lib/helpers/race-abort.js +2 -1
  51. package/dist-lib/lib/helpers/race-timeout.d.ts +5 -0
  52. package/dist-lib/lib/helpers/race-timeout.js +14 -0
  53. package/dist-lib/lib/helpers/run-outcome.d.ts +6 -8
  54. package/dist-lib/lib/helpers/run-outcome.js +36 -20
  55. package/dist-lib/lib/helpers/spawn-path.js +6 -3
  56. package/dist-lib/lib/helpers/text-coalescer.js +6 -3
  57. package/dist-lib/lib/helpers/thinking.js +38 -45
  58. package/dist-lib/lib/helpers/tool-call-markup.d.ts +9 -7
  59. package/dist-lib/lib/helpers/tool-call-markup.js +36 -20
  60. package/dist-lib/lib/helpers/tool-intent.d.ts +3 -2
  61. package/dist-lib/lib/helpers/tool-intent.js +4 -2
  62. package/dist-lib/lib/helpers/tool-label.js +18 -9
  63. package/dist-lib/lib/hooks/built-in/arg-repair.js +23 -13
  64. package/dist-lib/lib/hooks/built-in/egress.js +2 -1
  65. package/dist-lib/lib/hooks/built-in/skill-catalog.js +2 -1
  66. package/dist-lib/lib/hooks/pipeline.d.ts +5 -4
  67. package/dist-lib/lib/hooks/pipeline.js +6 -3
  68. package/dist-lib/lib/hooks/run.js +12 -8
  69. package/dist-lib/lib/model-download-progress.js +10 -8
  70. package/dist-lib/lib/models/context.d.ts +1 -1
  71. package/dist-lib/lib/models/context.js +5 -1
  72. package/dist-lib/lib/models/custom-model-cache.d.ts +10 -1
  73. package/dist-lib/lib/models/custom-model-cache.js +43 -15
  74. package/dist-lib/lib/models/gguf-header.d.ts +3 -2
  75. package/dist-lib/lib/models/gguf-header.js +44 -22
  76. package/dist-lib/lib/models/idle-unload.d.ts +14 -0
  77. package/dist-lib/lib/models/idle-unload.js +51 -0
  78. package/dist-lib/lib/models/kv-cache.d.ts +3 -2
  79. package/dist-lib/lib/models/kv-cache.js +4 -3
  80. package/dist-lib/lib/models/llm-presets.d.ts +2 -2
  81. package/dist-lib/lib/models/llm-presets.js +4 -2
  82. package/dist-lib/lib/models/model-catalog-constants.js +3 -2
  83. package/dist-lib/lib/models/model-catalog.d.ts +15 -20
  84. package/dist-lib/lib/models/model-catalog.js +66 -31
  85. package/dist-lib/lib/models/model-delete.js +18 -8
  86. package/dist-lib/lib/models/model-handle.js +2 -1
  87. package/dist-lib/lib/models/model-list.d.ts +8 -3
  88. package/dist-lib/lib/models/model-list.js +11 -6
  89. package/dist-lib/lib/models/model-pool.d.ts +48 -10
  90. package/dist-lib/lib/models/model-pool.js +106 -45
  91. package/dist-lib/lib/models/model-source.js +10 -5
  92. package/dist-lib/lib/models/music-catalog.d.ts +2 -2
  93. package/dist-lib/lib/models/nmt-catalog.d.ts +4 -3
  94. package/dist-lib/lib/models/nmt-catalog.js +10 -5
  95. package/dist-lib/lib/models/opencl-cache.d.ts +6 -1
  96. package/dist-lib/lib/models/opencl-cache.js +5 -3
  97. package/dist-lib/lib/models/sdk-faults.d.ts +11 -8
  98. package/dist-lib/lib/models/sdk-faults.js +9 -6
  99. package/dist-lib/lib/models/shards.d.ts +11 -0
  100. package/dist-lib/lib/models/shards.js +15 -0
  101. package/dist-lib/lib/models/tool-dialect.js +10 -5
  102. package/dist-lib/lib/no-redirect-fetch-bare.d.ts +3 -3
  103. package/dist-lib/lib/no-redirect-fetch-bare.js +1 -1
  104. package/dist-lib/lib/no-redirect-fetch-impl.js +6 -3
  105. package/dist-lib/lib/no-redirect-fetch-node.d.ts +1 -1
  106. package/dist-lib/lib/no-redirect-fetch-node.js +9 -9
  107. package/dist-lib/lib/office/index.d.ts +3 -2
  108. package/dist-lib/lib/office/python.d.ts +3 -2
  109. package/dist-lib/lib/office/python.js +2 -1
  110. package/dist-lib/lib/prompt.d.ts +5 -4
  111. package/dist-lib/lib/prompt.js +8 -4
  112. package/dist-lib/lib/queue.js +10 -5
  113. package/dist-lib/lib/resolution-detector.js +21 -11
  114. package/dist-lib/lib/rpc-session.js +2 -1
  115. package/dist-lib/lib/sdk-loader.js +2 -1
  116. package/dist-lib/lib/sdk-storage.js +4 -2
  117. package/dist-lib/lib/serve.js +122 -66
  118. package/dist-lib/lib/spawn.js +7 -4
  119. package/dist-lib/lib/tmp-path.d.ts +7 -6
  120. package/dist-lib/lib/tool-call-display.d.ts +2 -2
  121. package/dist-lib/lib/tools/create-documents.js +18 -10
  122. package/dist-lib/lib/tools/edit-documents.js +23 -13
  123. package/dist-lib/lib/tools/edit-image.d.ts +2 -2
  124. package/dist-lib/lib/tools/edit-image.js +60 -36
  125. package/dist-lib/lib/tools/exec/adapters/adapter.d.ts +10 -10
  126. package/dist-lib/lib/tools/exec/adapters/python/attachments.d.ts +17 -11
  127. package/dist-lib/lib/tools/exec/adapters/python/attachments.js +53 -40
  128. package/dist-lib/lib/tools/exec/adapters/python/create-effect.d.ts +2 -2
  129. package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +60 -32
  130. package/dist-lib/lib/tools/exec/adapters/python/guarded-fetch.d.ts +33 -22
  131. package/dist-lib/lib/tools/exec/adapters/python/guarded-fetch.js +58 -29
  132. package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.d.ts +2 -2
  133. package/dist-lib/lib/tools/exec/adapters/python/loader.js +23 -12
  134. package/dist-lib/lib/tools/exec/adapters/python/memfs.d.ts +13 -11
  135. package/dist-lib/lib/tools/exec/adapters/python/memfs.js +10 -5
  136. package/dist-lib/lib/tools/exec/adapters/python/net-shim.js +3 -1
  137. package/dist-lib/lib/tools/exec/adapters/python/output-policy.d.ts +6 -4
  138. package/dist-lib/lib/tools/exec/adapters/python/output-policy.js +20 -10
  139. package/dist-lib/lib/tools/exec/adapters/python/pyodide.d.ts +2 -2
  140. package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +14 -7
  141. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +41 -34
  142. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +316 -166
  143. package/dist-lib/lib/tools/exec/adapters/python/run-session.d.ts +9 -8
  144. package/dist-lib/lib/tools/exec/adapters/python/run-session.js +56 -28
  145. package/dist-lib/lib/tools/exec/adapters/python/wheels.d.ts +8 -10
  146. package/dist-lib/lib/tools/exec/adapters/python/wheels.js +22 -11
  147. package/dist-lib/lib/tools/exec/adapters/python/worker/client.d.ts +3 -2
  148. package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +41 -26
  149. package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +27 -18
  150. package/dist-lib/lib/tools/exec/adapters/python/worker/protocol.d.ts +2 -2
  151. package/dist-lib/lib/tools/exec/adapters/python/worker/protocol.js +14 -13
  152. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-node.d.ts +4 -4
  153. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-node.js +10 -5
  154. package/dist-lib/lib/tools/exec/adapters/shell/shell-adapter.d.ts +4 -4
  155. package/dist-lib/lib/tools/exec/adapters/shell/shell-adapter.js +2 -1
  156. package/dist-lib/lib/tools/exec/obsidian/cli-process.d.ts +6 -1
  157. package/dist-lib/lib/tools/exec/obsidian/cli-process.js +8 -5
  158. package/dist-lib/lib/tools/exec/obsidian/cli.d.ts +6 -6
  159. package/dist-lib/lib/tools/exec/obsidian/cli.js +4 -4
  160. package/dist-lib/lib/tools/exec/obsidian/command.d.ts +3 -2
  161. package/dist-lib/lib/tools/exec/obsidian/command.js +6 -3
  162. package/dist-lib/lib/tools/exec/obsidian/output.js +2 -1
  163. package/dist-lib/lib/tools/exec/obsidian/route.d.ts +4 -6
  164. package/dist-lib/lib/tools/exec/obsidian/route.js +17 -11
  165. package/dist-lib/lib/tools/exec/obsidian/safe-path.js +2 -1
  166. package/dist-lib/lib/tools/exec/obsidian/vault-create.js +15 -7
  167. package/dist-lib/lib/tools/exec/obsidian/vault-paths.d.ts +2 -2
  168. package/dist-lib/lib/tools/exec/obsidian/vault-paths.js +20 -10
  169. package/dist-lib/lib/tools/exec/obsidian/vault-read.js +20 -13
  170. package/dist-lib/lib/tools/exec/obsidian/vault-write.js +24 -12
  171. package/dist-lib/lib/tools/exec/runner.d.ts +14 -14
  172. package/dist-lib/lib/tools/exec/runner.js +5 -3
  173. package/dist-lib/lib/tools/exec/scope.js +52 -28
  174. package/dist-lib/lib/tools/exec/shell-argv.js +31 -18
  175. package/dist-lib/lib/tools/exec/shell-guidance.d.ts +6 -1
  176. package/dist-lib/lib/tools/exec/shell-guidance.js +35 -20
  177. package/dist-lib/lib/tools/exec/video-tools.js +2 -2
  178. package/dist-lib/lib/tools/exec.d.ts +6 -8
  179. package/dist-lib/lib/tools/exec.js +113 -54
  180. package/dist-lib/lib/tools/file-glob.d.ts +4 -5
  181. package/dist-lib/lib/tools/file-glob.js +16 -11
  182. package/dist-lib/lib/tools/fs/glob.js +13 -18
  183. package/dist-lib/lib/tools/fs/remote-protocol.d.ts +10 -10
  184. package/dist-lib/lib/tools/fs/remote.d.ts +3 -2
  185. package/dist-lib/lib/tools/fs/remote.js +13 -7
  186. package/dist-lib/lib/tools/fs/scope.d.ts +8 -3
  187. package/dist-lib/lib/tools/fs/scope.js +20 -12
  188. package/dist-lib/lib/tools/fs/walk.js +6 -3
  189. package/dist-lib/lib/tools/fs/write.d.ts +3 -2
  190. package/dist-lib/lib/tools/generate-image.d.ts +11 -9
  191. package/dist-lib/lib/tools/generate-image.js +73 -37
  192. package/dist-lib/lib/tools/generate-music.d.ts +22 -11
  193. package/dist-lib/lib/tools/generate-music.js +79 -48
  194. package/dist-lib/lib/tools/grep.js +66 -41
  195. package/dist-lib/lib/tools/http-request/encode-fields.js +18 -9
  196. package/dist-lib/lib/tools/http-request/{utils.js → request.js} +20 -11
  197. package/dist-lib/lib/tools/http-request/truncate-response.js +57 -46
  198. package/dist-lib/lib/tools/http-request.d.ts +1 -1
  199. package/dist-lib/lib/tools/http-request.js +33 -18
  200. package/dist-lib/lib/tools/mcp/protocol.d.ts +53 -0
  201. package/dist-lib/lib/tools/mcp/{utils.js → protocol.js} +45 -23
  202. package/dist-lib/lib/tools/mcp-call.d.ts +1 -1
  203. package/dist-lib/lib/tools/mcp-call.js +37 -16
  204. package/dist-lib/lib/tools/operations/builders.js +10 -6
  205. package/dist-lib/lib/tools/operations/compile.js +20 -10
  206. package/dist-lib/lib/tools/operations/decoders.js +12 -6
  207. package/dist-lib/lib/tools/operations/gmail-message.js +3 -2
  208. package/dist-lib/lib/tools/outbound-guard.d.ts +4 -3
  209. package/dist-lib/lib/tools/outbound-guard.js +14 -8
  210. package/dist-lib/lib/tools/private-ip.js +8 -7
  211. package/dist-lib/lib/tools/rag-search.js +14 -7
  212. package/dist-lib/lib/tools/read-files.js +23 -20
  213. package/dist-lib/lib/tools/skill.js +19 -10
  214. package/dist-lib/lib/tools/skills/auth-resolver.d.ts +11 -18
  215. package/dist-lib/lib/tools/skills/auth-resolver.js +30 -18
  216. package/dist-lib/lib/tools/skills/bin-check.d.ts +3 -2
  217. package/dist-lib/lib/tools/skills/bin-check.js +10 -3
  218. package/dist-lib/lib/tools/skills/bundled-dir.js +20 -10
  219. package/dist-lib/lib/tools/skills/bundled-hash.d.ts +3 -2
  220. package/dist-lib/lib/tools/skills/bundled-hash.js +6 -1
  221. package/dist-lib/lib/tools/skills/catalog.d.ts +3 -2
  222. package/dist-lib/lib/tools/skills/catalog.js +33 -28
  223. package/dist-lib/lib/tools/skills/cli-argv.d.ts +3 -2
  224. package/dist-lib/lib/tools/skills/cli-argv.js +55 -52
  225. package/dist-lib/lib/tools/skills/cli-schema-check.js +21 -10
  226. package/dist-lib/lib/tools/skills/cli-schema-load.js +28 -20
  227. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +6 -6
  228. package/dist-lib/lib/tools/skills/cli-schema.js +2 -1
  229. package/dist-lib/lib/tools/skills/credential-check.d.ts +6 -3
  230. package/dist-lib/lib/tools/skills/credential-check.js +24 -12
  231. package/dist-lib/lib/tools/skills/credential-grants.js +1 -6
  232. package/dist-lib/lib/tools/skills/credential-health.d.ts +5 -1
  233. package/dist-lib/lib/tools/skills/credential-health.js +5 -3
  234. package/dist-lib/lib/tools/skills/frontmatter-parser.js +10 -5
  235. package/dist-lib/lib/tools/skills/manifest-parse.js +84 -64
  236. package/dist-lib/lib/tools/skills/mention.js +7 -6
  237. package/dist-lib/lib/tools/skills/obsidian-readiness.d.ts +2 -2
  238. package/dist-lib/lib/tools/skills/obsidian-readiness.js +11 -3
  239. package/dist-lib/lib/tools/skills/overlay-crud.d.ts +6 -4
  240. package/dist-lib/lib/tools/skills/overlay-crud.js +12 -6
  241. package/dist-lib/lib/tools/skills/overlay-dir.js +4 -2
  242. package/dist-lib/lib/tools/skills/overlay-watch.d.ts +3 -2
  243. package/dist-lib/lib/tools/skills/overlay-watch.js +8 -4
  244. package/dist-lib/lib/tools/skills/setup-routes.js +1 -3
  245. package/dist-lib/lib/tools/skills/skill-settings.js +4 -1
  246. package/dist-lib/lib/tools/skills/skill-state.d.ts +6 -11
  247. package/dist-lib/lib/tools/skills/skill-state.js +48 -29
  248. package/dist-lib/lib/tools/skills/tool-grants.js +6 -3
  249. package/dist-lib/lib/tools/toolsets.js +2 -1
  250. package/dist-lib/lib/tools/translate.d.ts +4 -5
  251. package/dist-lib/lib/tools/translate.js +31 -14
  252. package/dist-lib/lib/tools/url-guard.js +2 -1
  253. package/dist-lib/lib/tools/weather-lookup/place.js +8 -4
  254. package/dist-lib/lib/tools/weather-lookup/report.d.ts +7 -1
  255. package/dist-lib/lib/tools/weather-lookup/report.js +46 -9
  256. package/dist-lib/lib/tools/weather-lookup.js +18 -12
  257. package/dist-lib/lib/tools/web-fetch.js +47 -24
  258. package/dist-lib/lib/tools/web-search.js +8 -3
  259. package/dist-lib/lib/validating-lookup.d.ts +4 -4
  260. package/dist-lib/lib/wav-encode.d.ts +1 -1
  261. package/dist-lib/lib/wav-encode.js +11 -10
  262. package/dist-lib/lib/web-fetch/embedded-state.js +24 -12
  263. package/dist-lib/lib/web-fetch/extract.js +4 -2
  264. package/dist-lib/lib/web-fetch/images.js +8 -4
  265. package/dist-lib/lib/web-fetch/video.d.ts +2 -2
  266. package/dist-lib/lib/web-fetch/video.js +8 -4
  267. package/dist-lib/lib/web-search/engine-registry.d.ts +2 -2
  268. package/dist-lib/lib/web-search/engines/aol.js +1 -1
  269. package/dist-lib/lib/web-search/engines/bing.js +1 -1
  270. package/dist-lib/lib/web-search/engines/brave.js +4 -1
  271. package/dist-lib/lib/web-search/engines/duckduckgo.js +3 -2
  272. package/dist-lib/lib/web-search/engines/google.js +7 -3
  273. package/dist-lib/lib/web-search/engines/wikipedia.js +5 -4
  274. package/dist-lib/lib/web-search/engines/yahoo.js +1 -1
  275. package/dist-lib/lib/web-search/fetch-html.d.ts +5 -4
  276. package/dist-lib/lib/web-search/fetch-html.js +13 -7
  277. package/dist-lib/lib/web-search/orchestrator.d.ts +9 -5
  278. package/dist-lib/lib/web-search/orchestrator.js +3 -3
  279. package/dist-lib/lib/wire.d.ts +3 -2
  280. package/dist-lib/lib/wire.js +23 -15
  281. package/dist-lib/schema/baseline.js +10 -5
  282. package/dist-lib/schema/build.js +2 -1
  283. package/dist-lib/schema/harness.d.ts +15 -2
  284. package/dist-lib/schema/harness.js +39 -2
  285. package/dist-lib/schema/release-tags.d.ts +3 -2
  286. package/dist-lib/schema/release-tags.js +6 -3
  287. package/dist-lib/spec/bind.js +9 -0
  288. package/dist-lib/spec/calls.js +3 -0
  289. package/dist-lib/spec/hrpc/hrpc.json +39 -0
  290. package/dist-lib/spec/hrpc/index.js +39 -3
  291. package/dist-lib/spec/hrpc/messages.js +105 -8
  292. package/dist-lib/spec/hyperschema/index.js +105 -8
  293. package/dist-lib/spec/hyperschema/schema.json +77 -1
  294. package/dist-lib/spec/hyperschema/types.d.ts +25 -1
  295. package/package.json +14 -14
  296. package/worker/harness-sidecar.mjs +15 -22
  297. package/worker/stow-entry.mjs +7 -4
  298. package/dist-lib/lib/tools/mcp/utils.d.ts +0 -47
  299. /package/dist-lib/lib/tools/http-request/{utils.d.ts → request.d.ts} +0 -0
package/dist-lib/index.js CHANGED
@@ -21,11 +21,13 @@ import { buildWavBuffer } from './lib/wav-encode.js';
21
21
  import { createVoiceLane, packSamples, speakOnce, speakStreamOnce, transcribeOnce, transcribeStreaming, SUPERTONIC_SAMPLE_RATE } from './lib/capabilities/voice.js';
22
22
  import { pushQueue } from './lib/queue.js';
23
23
  import { AbortedError, raceAbort } from './lib/helpers/race-abort.js';
24
- import { audioGenDownloadMetadata, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, infraModelsFor, modelNameToCapabilityProfile, musicModelSet, setCustomModels } from './lib/models/model-catalog.js';
24
+ import { audioGenDownloadMetadata, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, curatedModelAsset, infraModelsFor, modelAddonOf, modelNameToCapabilityProfile, musicModelSet, residentSrc, setCustomModels } from './lib/models/model-catalog.js';
25
25
  import { createModelStatusCache, customModelRows, invalidateModelStatusCache, mergeCustomRows, listModels } from './lib/models/model-list.js';
26
26
  import { deleteModelFiles, summarizeDelete } from './lib/models/model-delete.js';
27
- import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, modelKey, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
27
+ import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictIdleWhere, evictModel, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, modelKey, modelOf, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
28
+ import { defaultContextSize } from './lib/models/context.js';
28
29
  import { resolveMainGpu } from './lib/models/gpu-selection.js';
30
+ import { createIdleReaper } from './lib/models/idle-unload.js';
29
31
  import { openclCacheDir } from './lib/models/opencl-cache.js';
30
32
  import { assertEmbedDevice, createEmbedConfig } from './lib/models/embed-config.js';
31
33
  import { createGpuFaultState, isSdkGpuDeviceLostError, isSdkModelWedgedError } from './lib/models/sdk-faults.js';
@@ -39,6 +41,7 @@ import { ensureBundledSkillsDir } from './lib/tools/skills/bundled-dir.js';
39
41
  import { ensureSkillsOverlayDir } from './lib/tools/skills/overlay-dir.js';
40
42
  import { copyBaseForEdit, deleteOverlaySkill, listManagedSkills, readSkillFile, writeSkillFile } from './lib/tools/skills/overlay-crud.js';
41
43
  import { watchSkillsOverlay } from './lib/tools/skills/overlay-watch.js';
44
+ import { errorMessage } from './lib/guards.js';
42
45
  import { hlog } from './lib/log.js';
43
46
  import { harness } from './spec/index.js';
44
47
  import { installResolutionDetector, resolutionDetail } from './lib/resolution-detector.js';
@@ -79,6 +82,7 @@ const MUSIC_EVICT_WAIT_MS = 15_000;
79
82
  const WEDGED_EVICT_WAIT_MS = 10_000;
80
83
  const MODEL_ROOM_WAIT_MS = 300_000;
81
84
  const WARM_ROOM_WAIT_MS = 2_000;
85
+ const UNLOAD_LEASE_WAIT_MS = 10_000;
82
86
  const DEFAULT_COMPLETION_MODEL_CAP = 1;
83
87
  const WEDGED_EVICT_LEASED_MS = 120_000;
84
88
  const TTS_MODEL_CONFIG = {
@@ -91,6 +95,8 @@ export class Harness extends ReadyResource {
91
95
  _tools;
92
96
  _sdk;
93
97
  _loads = createLoadTracker();
98
+ _pinned = new Set();
99
+ _replying = [];
94
100
  _modelStatus = createModelStatusCache();
95
101
  _models;
96
102
  _embedModels;
@@ -133,6 +139,7 @@ export class Harness extends ReadyResource {
133
139
  _capability = null;
134
140
  _capabilityInflight = null;
135
141
  _modelFaults = new Map();
142
+ _idle;
136
143
  constructor({ tools = [], sdkLoader, skillsDir, modelsDir, projectionModel, imageModel, imageClamps, musicModel, musicClamps, llmDevice, llmGpuLayers, contextSize, kvCacheK, kvCacheV, embedDevice, embedGpuLayers, embedBatchSize, transcriptionDevice, completionModelCap, credentialStore, platform, gpuStats } = {}) {
137
144
  super();
138
145
  this._sdk = sdkLoader ?? createSdkLoader();
@@ -177,8 +184,9 @@ export class Harness extends ReadyResource {
177
184
  },
178
185
  onLoaded: (model) => {
179
186
  const transfer = imageTransfers.get(model);
180
- if (!transfer)
187
+ if (!transfer) {
181
188
  return;
189
+ }
182
190
  imageTransfers.delete(model);
183
191
  invalidateModelStatusCache(this._modelStatus);
184
192
  this.emit('model', { ...transfer, status: 'downloaded' });
@@ -186,9 +194,14 @@ export class Harness extends ReadyResource {
186
194
  onBeforeLoad: async (signal) => {
187
195
  if (this._models.destroyed)
188
196
  return;
189
- const evicted = await awaitModelCapacity(this._models, 1, DIFFUSION_EVICT_WAIT_MS, undefined, signal);
190
- if (signal?.aborted)
197
+ const evicted = await awaitModelCapacity(this._models, {
198
+ cap: 1,
199
+ deadlineMs: DIFFUSION_EVICT_WAIT_MS,
200
+ signal
201
+ });
202
+ if (signal?.aborted) {
191
203
  throw new AbortedError();
204
+ }
192
205
  if (!evicted) {
193
206
  throw new Error('the chat model is busy; try the image again once the current reply finishes');
194
207
  }
@@ -205,9 +218,14 @@ export class Harness extends ReadyResource {
205
218
  onBeforeLoad: async (signal) => {
206
219
  if (this._models.destroyed)
207
220
  return;
208
- const evicted = await awaitModelCapacity(this._models, 1, MUSIC_EVICT_WAIT_MS, undefined, signal);
209
- if (signal?.aborted)
221
+ const evicted = await awaitModelCapacity(this._models, {
222
+ cap: 1,
223
+ deadlineMs: MUSIC_EVICT_WAIT_MS,
224
+ signal
225
+ });
226
+ if (signal?.aborted) {
210
227
  throw new AbortedError();
228
+ }
211
229
  if (!evicted) {
212
230
  throw new Error('the chat model is busy; try the music again once the current reply finishes');
213
231
  }
@@ -245,13 +263,18 @@ export class Harness extends ReadyResource {
245
263
  this._diffusionSdk = null;
246
264
  this._musicSdk = null;
247
265
  this._translationSdk = null;
248
- }
249
- async _open() {
250
- installResolutionDetector(import.meta);
251
266
  this._models = this._createPool();
252
267
  this._embedModels = this._createEmbedPool();
253
268
  this._ttsModels = this._createTtsPool();
254
269
  this._transcriptionModels = this._createTranscriptionPool();
270
+ this._idle = createIdleReaper({
271
+ sweep: () => this._unloadIdleModels(),
272
+ waitMs: WEDGED_EVICT_WAIT_MS,
273
+ onError: (err) => hlog('idle unload failed', { err: errorMessage(err) })
274
+ });
275
+ }
276
+ async _open() {
277
+ installResolutionDetector(import.meta);
255
278
  await this._reloadSkills();
256
279
  await this._startOverlayWatch();
257
280
  hlog('open', { skills: this._supportedSkills.length, tools: this._tools.length });
@@ -269,10 +292,7 @@ export class Harness extends ReadyResource {
269
292
  if (!token)
270
293
  continue;
271
294
  const storeKey = scopeKey({ agentId, skill, key });
272
- const record = {
273
- token,
274
- ...(typeof expiresAt === 'number' && expiresAt > 0 ? { expiresAt } : {})
275
- };
295
+ const record = typeof expiresAt === 'number' && expiresAt > 0 ? { token, expiresAt } : { token };
276
296
  const current = previous.get(storeKey);
277
297
  if (!current || current.token !== record.token || current.expiresAt !== record.expiresAt) {
278
298
  resetCredentialHealthAfterSnapshot(key, agentId, skill);
@@ -310,48 +330,57 @@ export class Harness extends ReadyResource {
310
330
  }
311
331
  async writeSkillFile(name, content) {
312
332
  const overlay = await ensureSkillsOverlayDir();
313
- if (!overlay)
333
+ if (!overlay) {
314
334
  throw new Error('no writable skills overlay is configured');
335
+ }
315
336
  await writeSkillFile(overlay, name, content);
316
337
  await this._reloadSkills();
317
338
  return { ok: true };
318
339
  }
319
340
  async copyBaseForEdit(name) {
320
341
  const overlay = await ensureSkillsOverlayDir();
321
- if (!overlay)
342
+ if (!overlay) {
322
343
  throw new Error('no writable skills overlay is configured');
344
+ }
323
345
  const result = await copyBaseForEdit(await this._baseSkillsDir(), overlay, name);
324
- if (result.copied)
346
+ if (result.copied) {
325
347
  await this._reloadSkills();
348
+ }
326
349
  return result;
327
350
  }
328
351
  async deleteOverlaySkill(name) {
329
352
  const overlay = await ensureSkillsOverlayDir();
330
- if (!overlay)
353
+ if (!overlay) {
331
354
  throw new Error('no writable skills overlay is configured');
355
+ }
332
356
  const result = await deleteOverlaySkill(overlay, name);
333
- if (result.removed)
357
+ if (result.removed) {
334
358
  await this._reloadSkills();
359
+ }
335
360
  return result;
336
361
  }
337
362
  resolveCredential(key, ctx) {
338
- if (this._credentialStore)
363
+ if (this._credentialStore) {
339
364
  return this._credentialStore(key, ctx);
365
+ }
340
366
  const agentId = ctx?.agentId ?? null;
341
367
  const deviceShared = isDeviceSharedCredentialKey(key);
342
368
  for (const skill of ctx?.skills ?? []) {
343
369
  const owned = this._credentials.get(scopeKey({ agentId, skill, key })) ??
344
370
  (agentId && deviceShared ? this._credentials.get(scopeKey({ skill, key })) : undefined);
345
- if (owned)
371
+ if (owned) {
346
372
  return owned;
373
+ }
347
374
  }
348
375
  if (agentId) {
349
376
  const scoped = this._credentials.get(scopeKey({ agentId, key }));
350
- if (scoped)
377
+ if (scoped) {
351
378
  return scoped;
379
+ }
352
380
  }
353
- if (!agentId || deviceShared)
381
+ if (!agentId || deviceShared) {
354
382
  return this._credentials.get(scopeKey({ key })) ?? null;
383
+ }
355
384
  return null;
356
385
  }
357
386
  async _completionSkills(agentId, model) {
@@ -362,7 +391,8 @@ export class Harness extends ReadyResource {
362
391
  }));
363
392
  }
364
393
  async skills(agentId, modelName, options) {
365
- await this.ready();
394
+ if (!this.opened)
395
+ await this.ready();
366
396
  const platform = this._platform ?? detectPlatform();
367
397
  const refresh = options?.refresh === true;
368
398
  return resolveSkillStatuses(this._allSkills, {
@@ -376,7 +406,7 @@ export class Harness extends ReadyResource {
376
406
  : await this._probeBinVersions(platform, refresh),
377
407
  refresh,
378
408
  skipCredentialProbe: options?.skipCredentialProbe === true,
379
- ...(modelName ? { activeProfile: modelNameToCapabilityProfile(modelName) } : {})
409
+ activeProfile: modelName ? modelNameToCapabilityProfile(modelName) : undefined
380
410
  });
381
411
  }
382
412
  async _reloadSkills() {
@@ -391,14 +421,15 @@ export class Harness extends ReadyResource {
391
421
  return;
392
422
  try {
393
423
  const overlay = await ensureSkillsOverlayDir();
394
- if (!overlay)
424
+ if (!overlay) {
395
425
  return;
426
+ }
396
427
  this._overlayWatcher = watchSkillsOverlay(overlay, () => {
397
- this._reloadSkills().catch((err) => hlog('overlay reload failed', { err: err instanceof Error ? err.message : String(err) }));
428
+ this._reloadSkills().catch((err) => hlog('overlay reload failed', { err: errorMessage(err) }));
398
429
  });
399
430
  }
400
431
  catch (err) {
401
- hlog('overlay watch unavailable', { err: err instanceof Error ? err.message : String(err) });
432
+ hlog('overlay watch unavailable', { err: errorMessage(err) });
402
433
  }
403
434
  }
404
435
  async tools() {
@@ -411,8 +442,9 @@ export class Harness extends ReadyResource {
411
442
  if (!refresh && this._binVersionCache && this._binVersionCache.expiresAt > now) {
412
443
  return this._binVersionCache.bySkill;
413
444
  }
414
- if (this._binVersionInflight)
445
+ if (this._binVersionInflight) {
415
446
  return this._binVersionInflight;
447
+ }
416
448
  this._binVersionInflight = this._runBinVersionProbes(platform)
417
449
  .then((bySkill) => {
418
450
  this._binVersionCache = {
@@ -427,12 +459,14 @@ export class Harness extends ReadyResource {
427
459
  return this._binVersionInflight;
428
460
  }
429
461
  async _runBinVersionProbes(platform) {
430
- if (process.env.QVAC_NO_SKILL_BIN_PROBE)
462
+ if (process.env.QVAC_NO_SKILL_BIN_PROBE) {
431
463
  return new Map();
464
+ }
432
465
  const installed = new Set(this._tools.map((tool) => tool.name));
433
466
  const candidates = versionProbeSkills(this._allSkills, installed, platform);
434
- if (candidates.length === 0)
467
+ if (candidates.length === 0) {
435
468
  return new Map();
469
+ }
436
470
  let runtime;
437
471
  try {
438
472
  runtime = await bareRuntime();
@@ -452,8 +486,9 @@ export class Harness extends ReadyResource {
452
486
  const bySkill = new Map();
453
487
  for (const skill of candidates) {
454
488
  const states = await probeBinVersions(skill.requires, { platform, run });
455
- if (states.size > 0)
489
+ if (states.size > 0) {
456
490
  bySkill.set(skill.name, states);
491
+ }
457
492
  }
458
493
  return bySkill;
459
494
  }
@@ -472,31 +507,35 @@ export class Harness extends ReadyResource {
472
507
  }
473
508
  _projectionFor(model) {
474
509
  const custom = this._customProjection[model];
475
- if (custom)
510
+ if (custom) {
476
511
  return custom;
512
+ }
477
513
  const projection = this._projectionModel;
478
- if (!projection)
514
+ if (!projection) {
479
515
  return undefined;
516
+ }
480
517
  return typeof projection === 'string' ? projection : projection[model];
481
518
  }
482
519
  setCustomModels(entries) {
483
520
  setCustomModels(entries);
484
521
  this._customProjection = customProjectionMap();
485
522
  }
523
+ setModelIdleTimeout(ms) {
524
+ this._idle.configure(ms);
525
+ }
486
526
  previewModelSource(context) {
487
- return previewModelSource(context, {
488
- assessFit: async ({ sha256Checksum, name, contextTokens }) => {
489
- const sdk = await this._sdk.load();
490
- const result = await sdk.assessModelFit({
491
- models: [{ model: { sha256Checksum, name }, workload: { kind: 'llm', contextTokens } }]
492
- });
493
- const model = result.models[0];
494
- return {
495
- fit: model?.verdict ?? result.verdict,
496
- fitReasons: [...(model?.reasons ?? result.reasons)]
497
- };
498
- }
527
+ return previewModelSource(context, { assessFit: (query) => this._assessFit(query) });
528
+ }
529
+ async _assessFit({ sha256Checksum, name, contextTokens }) {
530
+ const sdk = await this._sdk.load();
531
+ const result = await sdk.assessModelFit({
532
+ models: [{ model: { sha256Checksum, name }, workload: { kind: 'llm', contextTokens } }]
499
533
  });
534
+ const model = result.models[0];
535
+ return {
536
+ fit: model?.verdict ?? result.verdict,
537
+ fitReasons: [...(model?.reasons ?? result.reasons)]
538
+ };
500
539
  }
501
540
  async describe() {
502
541
  if (!this.opened)
@@ -519,11 +558,12 @@ export class Harness extends ReadyResource {
519
558
  };
520
559
  }
521
560
  _loadedModels() {
522
- const pools = [this._embedModels, ...this._retiredEmbedModels];
523
- if (this._ttsModels)
524
- pools.push(this._ttsModels);
525
- if (this._transcriptionModels)
526
- pools.push(this._transcriptionModels);
561
+ const pools = [
562
+ this._embedModels,
563
+ ...this._retiredEmbedModels,
564
+ this._ttsModels,
565
+ this._transcriptionModels
566
+ ];
527
567
  return [...loadedModels(this._models), ...pools.flatMap((pool) => loadedModels(pool))];
528
568
  }
529
569
  _loadPluginSdk(current, store, importPlugin) {
@@ -557,8 +597,9 @@ export class Harness extends ReadyResource {
557
597
  const loadOpts = resolveLoadOptions({}, this._deviceConfig());
558
598
  const key = modelKey(model, loadOpts);
559
599
  const pool = this._models;
560
- if (!isResident(pool, key))
561
- await this._makeRoomForCompletion(signal);
600
+ if (!isResident(pool, key)) {
601
+ await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
602
+ }
562
603
  try {
563
604
  const { modelId } = await raceAbort(pool.open(key, model, loadOpts).load(), signal);
564
605
  const sdk = await this._sdk.load();
@@ -592,25 +633,51 @@ export class Harness extends ReadyResource {
592
633
  }
593
634
  _createPool() {
594
635
  const loadSdk = this._sdk.load;
595
- return createModelPool(loadSdk, (state) => this._notifyModel(state, true), this._deviceConfig(), this._loads);
636
+ return createModelPool(loadSdk, (state) => this._notifyModel(state), {
637
+ deviceConfig: this._deviceConfig(),
638
+ loads: this._loads
639
+ });
596
640
  }
597
641
  _createEmbedPool() {
598
642
  const loadSdk = () => this._sdk.loadEmbed();
599
- return createEmbedPool(loadSdk, (state) => this._notifyModel(state, false), this._embedFaults, this._embedBatchSize, openclCacheDir, this._loads);
643
+ return createEmbedPool(loadSdk, (state) => this._notifyModel(state), {
644
+ faults: this._embedFaults,
645
+ batchSize: this._embedBatchSize,
646
+ openclCacheDir,
647
+ loads: this._loads
648
+ });
600
649
  }
601
650
  _createTtsPool() {
602
651
  const loadSdk = () => this._sdk.loadVoice();
603
- return createVoicePool(loadSdk, (state) => this._notifyModel(state, false), 'tts-ggml', 'tts', TTS_MODEL_CONFIG, undefined, this._loads);
652
+ return createVoicePool(loadSdk, (state) => this._notifyModel(state), {
653
+ modelType: 'tts-ggml',
654
+ addon: 'tts',
655
+ modelConfig: TTS_MODEL_CONFIG,
656
+ loads: this._loads
657
+ });
604
658
  }
605
659
  _createTranscriptionPool() {
606
660
  const loadSdk = () => this._sdk.loadVoice();
607
- return createVoicePool(loadSdk, (state) => this._notifyModel({ ...state, model: TRANSCRIPTION_MODEL_LABEL }, false), 'parakeet-transcription', 'parakeet', { useGPU: this._transcriptionDevice !== 'cpu' }, openclCacheDir, this._loads);
661
+ return createVoicePool(loadSdk, (state) => this._notifyModel({ ...state, model: TRANSCRIPTION_MODEL_LABEL }), {
662
+ modelType: 'parakeet-transcription',
663
+ addon: 'parakeet',
664
+ modelConfig: { useGPU: this._transcriptionDevice !== 'cpu' },
665
+ openclCacheDir,
666
+ loads: this._loads
667
+ });
608
668
  }
609
- _notifyModel(state, languageModel) {
610
- const catalogChanged = languageModel && (state.status === 'ready' || state.status === 'unloaded');
669
+ _notifyModel(state) {
670
+ const catalogChanged = state.status === 'ready' || state.status === 'unloaded';
611
671
  if (catalogChanged) {
612
672
  invalidateModelStatusCache(this._modelStatus);
613
673
  }
674
+ if (state.status === 'unloaded') {
675
+ for (const key of this._pinned) {
676
+ if (!isResident(this._models, key)) {
677
+ this._pinned.delete(key);
678
+ }
679
+ }
680
+ }
614
681
  this.emit('model', { ...state, catalogChanged, loaded: this._loadedModels() });
615
682
  }
616
683
  async _collectRetiredEmbedPool(pool) {
@@ -646,14 +713,17 @@ export class Harness extends ReadyResource {
646
713
  return this._enqueueEmbedControl(() => this._setEmbeddingDevice(device));
647
714
  }
648
715
  async _setEmbeddingDevice(device) {
649
- await this.ready();
716
+ if (!this.opened)
717
+ await this.ready();
650
718
  if (this.closing)
651
719
  throw new Error('harness is closing');
652
- if (this._embedFaults.device === device)
720
+ if (this._embedFaults.device === device) {
653
721
  return;
722
+ }
654
723
  this._embedFaults = createGpuFaultState({ device, gpuLayers: this._embedGpuLayers });
655
- if (this.suspended)
724
+ if (this.suspended) {
656
725
  return;
726
+ }
657
727
  const retired = this._embedModels;
658
728
  this._retiredEmbedModels.add(retired);
659
729
  this._embedModels = this._createEmbedPool();
@@ -667,15 +737,18 @@ export class Harness extends ReadyResource {
667
737
  }
668
738
  async suspend() {
669
739
  hlog('suspend called');
670
- if (this._state === HarnessState.SUSPEND)
740
+ if (this._state === HarnessState.SUSPEND) {
671
741
  return;
672
- if (this.closing)
742
+ }
743
+ if (this.closing) {
673
744
  return;
745
+ }
674
746
  this._state = HarnessState.SUSPEND;
675
747
  hlog('suspending', { models: loadedModels(this._models).map((entry) => entry.model) });
676
748
  const pending = this._sdk.peek();
677
- if (pending)
749
+ if (pending) {
678
750
  this._loads.drain(await pending);
751
+ }
679
752
  await this._releaseRuntimes();
680
753
  await this._sdkLifecycle('suspend');
681
754
  invalidateModelStatusCache(this._modelStatus);
@@ -683,18 +756,20 @@ export class Harness extends ReadyResource {
683
756
  }
684
757
  async _sdkLifecycle(op) {
685
758
  const sdk = await this._sdk.peek()?.catch(noop);
686
- if (!sdk)
759
+ if (!sdk) {
687
760
  return;
761
+ }
688
762
  try {
689
763
  await sdk[op]();
690
764
  }
691
765
  catch (err) {
692
- hlog(`sdk ${op} failed`, { err: err instanceof Error ? err.message : String(err) });
693
- if (op === 'suspend')
766
+ hlog(`sdk ${op} failed`, { err: errorMessage(err) });
767
+ if (op === 'suspend') {
694
768
  return;
769
+ }
695
770
  await sdk.resume().catch((retryErr) => {
696
771
  hlog('sdk resume retry failed', {
697
- err: retryErr instanceof Error ? retryErr.message : String(retryErr)
772
+ err: errorMessage(retryErr)
698
773
  });
699
774
  });
700
775
  }
@@ -713,15 +788,38 @@ export class Harness extends ReadyResource {
713
788
  if (outcome.status !== 'rejected')
714
789
  continue;
715
790
  const err = outcome.reason;
716
- hlog('unload failed', { err: err instanceof Error ? err.message : String(err) });
791
+ hlog('unload failed', { err: errorMessage(err) });
717
792
  }
718
793
  }
794
+ async _unloadIdleModels() {
795
+ if (!this.opened || this.closing || this.suspended)
796
+ return;
797
+ hlog('idle unload', { models: this._loadedModels().map((entry) => entry.model) });
798
+ const released = await Promise.allSettled([
799
+ evictIdle(this._models),
800
+ evictIdle(this._embedModels),
801
+ ...[...this._retiredEmbedModels].map((pool) => this._collectRetiredEmbedPool(pool)),
802
+ evictIdle(this._ttsModels),
803
+ evictIdle(this._transcriptionModels),
804
+ this._imageRuntime?.unloadIdle(),
805
+ this._musicRuntime?.unloadIdle(),
806
+ this._translationRuntime.unload()
807
+ ]);
808
+ for (const outcome of released) {
809
+ if (outcome.status !== 'rejected')
810
+ continue;
811
+ hlog('idle unload failed', { err: errorMessage(outcome.reason) });
812
+ }
813
+ invalidateModelStatusCache(this._modelStatus);
814
+ }
719
815
  async resume() {
720
816
  hlog('resume called');
721
- if (this._state === HarnessState.RESUME)
817
+ if (this._state === HarnessState.RESUME) {
722
818
  return;
723
- if (this.closing)
819
+ }
820
+ if (this.closing) {
724
821
  return;
822
+ }
725
823
  if (this._state === HarnessState.PRESUSPEND) {
726
824
  this._state = HarnessState.RESUME;
727
825
  return;
@@ -744,22 +842,26 @@ export class Harness extends ReadyResource {
744
842
  }
745
843
  async _close() {
746
844
  hlog('closing');
845
+ this._idle.close();
747
846
  this._overlayWatcher?.stop();
748
847
  this._overlayWatcher = null;
749
848
  const pending = this._sdk.peek();
750
- if (pending)
849
+ if (pending) {
751
850
  this._loads.drain(await pending);
851
+ }
752
852
  await Promise.all([this._releaseRuntimes(), closePythonWorker()]);
753
853
  const sdk = await this._sdk.peek()?.catch(noop);
754
- if (sdk)
854
+ if (sdk) {
755
855
  await sdk.close();
856
+ }
756
857
  }
757
858
  download(args) {
758
859
  const projection = (args.addon ?? 'llm') === 'llm' ? this._projectionFor(args.model) : undefined;
759
860
  const multiFile = diffusionDownloadMetadata(args.model, args.addon) ??
760
861
  audioGenDownloadMetadata(args.model, args.addon);
761
862
  return download(async () => {
762
- await this.ready();
863
+ if (!this.opened)
864
+ await this.ready();
763
865
  return this._sdk.load();
764
866
  }, {
765
867
  ...args,
@@ -768,23 +870,28 @@ export class Harness extends ReadyResource {
768
870
  assetSizes: multiFile?.assetSizes
769
871
  }, () => invalidateModelStatusCache(this._modelStatus));
770
872
  }
771
- async embed({ model, texts, signal }) {
772
- await this.ready();
873
+ embed(context) {
874
+ return this._busy(() => this._embed(context));
875
+ }
876
+ async _embed({ model, texts, signal }) {
877
+ if (!this.opened)
878
+ await this.ready();
773
879
  if (this.suspended)
774
880
  throw new Error('harness is suspended');
775
- if (texts.length === 0)
881
+ if (texts.length === 0) {
776
882
  return [];
883
+ }
777
884
  const pool = this._embedModels;
778
885
  const faults = this._embedFaults;
779
886
  const generation = faults.generation;
780
- const key = faults.key(model);
887
+ const key = faults.key(residentSrc(model, 'embeddings'));
781
888
  const handle = pool.open(key, model);
782
889
  let loaded = null;
783
890
  let job = null;
784
891
  let releaseAfter = null;
785
892
  try {
786
893
  loaded = await raceAbort(handle.load(), signal);
787
- const sdk = (await this._sdk.loadEmbed());
894
+ const sdk = await this._sdk.loadEmbed();
788
895
  job = this._embedLane(sdk, { modelId: loaded.modelId, texts });
789
896
  const vectors = await raceAbort(job, signal);
790
897
  faults.reset();
@@ -807,15 +914,18 @@ export class Harness extends ReadyResource {
807
914
  throw err;
808
915
  }
809
916
  finally {
810
- if (releaseAfter)
917
+ if (releaseAfter) {
811
918
  this._releaseEmbedResourceAfter(releaseAfter, pool, key);
812
- else
919
+ }
920
+ else {
813
921
  await this._releaseEmbedResource(pool, key);
922
+ }
814
923
  }
815
924
  }
816
925
  async _defaultVoiceSrc(category) {
817
- if (category !== 'tts')
926
+ if (category !== 'tts') {
818
927
  return defaultVoiceModel(category);
928
+ }
819
929
  try {
820
930
  const { capabilityProfile } = await this.capability();
821
931
  return defaultTtsFor(capabilityProfile).asset.src;
@@ -824,11 +934,13 @@ export class Harness extends ReadyResource {
824
934
  return defaultVoiceModel('tts');
825
935
  }
826
936
  }
827
- async _voiceJob(pool, category, model, signal, run) {
828
- await this.ready();
937
+ async _voiceJob(pool, { category, model, signal }, run) {
938
+ if (!this.opened)
939
+ await this.ready();
829
940
  if (this.suspended)
830
941
  throw new Error('harness is suspended');
831
- const name = model || (await this._defaultVoiceSrc(category));
942
+ const addon = category === 'tts' ? 'tts' : 'parakeet';
943
+ const name = residentSrc(model || (await this._defaultVoiceSrc(category)), addon);
832
944
  const handle = pool.open(name, name);
833
945
  let job = null;
834
946
  let releaseAfter = null;
@@ -844,8 +956,9 @@ export class Harness extends ReadyResource {
844
956
  }
845
957
  catch (err) {
846
958
  if (err instanceof AbortedError) {
847
- if (sdk && modelId)
959
+ if (sdk && modelId) {
848
960
  void sdk.cancel({ modelId, operation: 'inference' }).catch(noop);
961
+ }
849
962
  releaseAfter = job;
850
963
  }
851
964
  throw err;
@@ -854,19 +967,25 @@ export class Harness extends ReadyResource {
854
967
  const release = () => {
855
968
  releaseModel(pool, name);
856
969
  };
857
- if (releaseAfter)
970
+ if (releaseAfter) {
858
971
  void releaseAfter.then(release, release);
859
- else
972
+ }
973
+ else {
860
974
  release();
975
+ }
861
976
  }
862
977
  }
863
- async transcribe({ model, audio, signal }) {
864
- return this._voiceJob(this._transcriptionModels, 'transcription', model, signal, async (sdk, id) => {
978
+ transcribe(context) {
979
+ return this._busy(() => this._transcribe(context));
980
+ }
981
+ async _transcribe({ model, audio, signal }) {
982
+ return this._voiceJob(this._transcriptionModels, { category: 'transcription', model, signal }, async (sdk, id) => {
865
983
  const pcm = await pcmForTranscription(audio);
866
- if (signal?.aborted)
984
+ if (signal?.aborted) {
867
985
  throw new AbortedError();
986
+ }
868
987
  if (pcm && pcm.length >= TRANSCRIBE_STREAM_MIN_BYTES) {
869
- return transcribeStreaming(sdk, id, pcm, signal);
988
+ return transcribeStreaming(sdk, pcm, { modelId: id, signal });
870
989
  }
871
990
  return transcribeOnce(sdk, id, pcm ? buildWavBuffer(pcm) : audio);
872
991
  });
@@ -874,33 +993,43 @@ export class Harness extends ReadyResource {
874
993
  detectLanguage(context) {
875
994
  return detectLanguage(context);
876
995
  }
877
- async speak({ model, text, signal }) {
878
- const samples = await this._voiceJob(this._ttsModels, 'tts', model, signal, (sdk, id) => speakOnce(sdk, id, text));
996
+ speak(context) {
997
+ return this._busy(() => this._speak(context));
998
+ }
999
+ async _speak({ model, text, signal }) {
1000
+ const samples = await this._voiceJob(this._ttsModels, { category: 'tts', model, signal }, (sdk, id) => speakOnce(sdk, id, text));
879
1001
  return { audio: packSamples(samples), sampleRate: SUPERTONIC_SAMPLE_RATE };
880
1002
  }
881
- async *speakStream({ model, text, signal }) {
882
- await this.ready();
1003
+ speakStream(context) {
1004
+ return this._busyStream(() => this._speakStream(context));
1005
+ }
1006
+ async *_speakStream({ model, text, signal }) {
1007
+ if (!this.opened)
1008
+ await this.ready();
883
1009
  if (this.suspended)
884
1010
  throw new Error('harness is suspended');
885
- const name = model || (await this._defaultVoiceSrc('tts'));
1011
+ const name = residentSrc(model || (await this._defaultVoiceSrc('tts')), 'tts');
886
1012
  const pool = this._ttsModels;
887
1013
  const handle = pool.open(name, name);
888
1014
  const chunks = pushQueue();
889
1015
  let sdk = null;
890
1016
  let modelId = null;
891
1017
  const cancel = () => {
892
- if (sdk && modelId)
893
- void sdk.cancel({ modelId, operation: 'inference' }).catch(() => { });
1018
+ if (sdk && modelId) {
1019
+ void sdk.cancel({ modelId, operation: 'inference' }).catch(noop);
1020
+ }
894
1021
  };
895
1022
  signal?.addEventListener('abort', cancel, { once: true });
896
1023
  const job = this._voiceLane(async () => {
897
1024
  try {
898
1025
  modelId = (await handle.load()).modelId;
899
1026
  sdk = (await this._sdk.loadVoice());
900
- if (signal?.aborted)
1027
+ if (signal?.aborted) {
901
1028
  return;
902
- for await (const chunk of speakStreamOnce(sdk, modelId, text))
1029
+ }
1030
+ for await (const chunk of speakStreamOnce(sdk, modelId, text)) {
903
1031
  chunks.push(chunk);
1032
+ }
904
1033
  }
905
1034
  finally {
906
1035
  chunks.end();
@@ -914,18 +1043,23 @@ export class Harness extends ReadyResource {
914
1043
  }
915
1044
  finally {
916
1045
  signal?.removeEventListener('abort', cancel);
917
- if (!settled)
1046
+ if (!settled) {
918
1047
  cancel();
1048
+ }
919
1049
  void job.catch(noop).finally(() => {
920
1050
  releaseModel(pool, name);
921
1051
  });
922
1052
  }
923
1053
  }
924
1054
  async list(options = {}) {
925
- await this.ready();
926
- const sdk = (await this._sdk.load());
1055
+ if (!this.opened)
1056
+ await this.ready();
1057
+ const sdk = await this._sdk.load();
927
1058
  const [rows, custom] = await Promise.all([
928
- listModels(sdk, options, this._modelStatus, (name) => this._projectionFor(name)),
1059
+ listModels(sdk, options, {
1060
+ cache: this._modelStatus,
1061
+ projectionFor: (name) => this._projectionFor(name)
1062
+ }),
929
1063
  customModelRows(options, {
930
1064
  modelsDir: this._modelsDir,
931
1065
  loaded: new Set(this._loadedModels().map((entry) => entry.model))
@@ -934,18 +1068,21 @@ export class Harness extends ReadyResource {
934
1068
  return mergeCustomRows(rows, custom);
935
1069
  }
936
1070
  async deleteModel(name) {
937
- await this.ready();
938
- const sdk = (await this._sdk.load());
1071
+ if (!this.opened)
1072
+ await this.ready();
1073
+ const sdk = await this._sdk.load();
939
1074
  const result = await deleteModelFiles(sdk, name, { modelsDir: this._modelsDir });
940
1075
  invalidateModelStatusCache(this._modelStatus);
941
1076
  return summarizeDelete(result);
942
1077
  }
943
1078
  async clearKvCache() {
944
- await this.ready();
1079
+ if (!this.opened)
1080
+ await this.ready();
945
1081
  return deleteAllKvCaches(() => this._sdk.load());
946
1082
  }
947
1083
  async deleteKvCache(kvCacheKey) {
948
- await this.ready();
1084
+ if (!this.opened)
1085
+ await this.ready();
949
1086
  return deleteKvCacheKey(() => this._sdk.load(), kvCacheKey);
950
1087
  }
951
1088
  async infraModels(profile) {
@@ -989,37 +1126,160 @@ export class Harness extends ReadyResource {
989
1126
  });
990
1127
  return this._capabilityInflight;
991
1128
  }
992
- async warmModel({ model, agent, signal }) {
993
- await this.ready();
1129
+ warmModel(context) {
1130
+ return this._busy(() => this._warmModel(context));
1131
+ }
1132
+ async _warmModel({ model, agent, signal }) {
1133
+ if (!this.opened)
1134
+ await this.ready();
994
1135
  if (this.suspended)
995
1136
  return;
996
- const projectionModel = this._projectionFor(model);
997
- const loadOpts = resolveLoadOptions({
998
- ctxSize: agent?.ctxSize ?? undefined,
999
- kvCacheK: agent?.kvCacheK ?? undefined,
1000
- kvCacheV: agent?.kvCacheV ?? undefined,
1001
- projectionModel
1002
- }, this._deviceConfig());
1003
- const key = modelKey(model, loadOpts);
1004
- const pool = this._models;
1005
- hlog('warm model', { model, resident: isResident(pool, key) });
1006
- if (!isResident(pool, key)) {
1007
- if (!(await this._makeRoomForCompletion(signal, WARM_ROOM_WAIT_MS)))
1137
+ const target = this._residency(model, agent);
1138
+ const resident = isResident(target.pool, target.key);
1139
+ hlog('warm model', { model, resident });
1140
+ if (target.pool === this._models && !resident) {
1141
+ const roomed = await this._makeRoomForCompletion(model, {
1142
+ ctxSize: target.ctxSize,
1143
+ signal,
1144
+ deadlineMs: WARM_ROOM_WAIT_MS
1145
+ });
1146
+ if (!roomed) {
1008
1147
  return;
1009
- await this._imageRuntime?.unloadIdle().catch(noop);
1148
+ }
1149
+ await this._unloadIdleGenerators();
1010
1150
  }
1151
+ await this._loadAndRelease(target, signal);
1152
+ }
1153
+ loadModel(context) {
1154
+ return this._busy(() => this._loadModel(context));
1155
+ }
1156
+ async _loadModel({ model, agent, signal }) {
1157
+ if (!this.opened)
1158
+ await this.ready();
1159
+ if (this.suspended) {
1160
+ throw new Error('harness is suspended');
1161
+ }
1162
+ const target = this._residency(model, agent);
1163
+ const resident = isResident(target.pool, target.key);
1164
+ hlog('load model', { model, resident });
1165
+ if (target.pool !== this._models) {
1166
+ await this._loadAndRelease(target, signal);
1167
+ return { loaded: true };
1168
+ }
1169
+ if (!resident) {
1170
+ if (!(await this._fits(model, target.ctxSize))) {
1171
+ return { loaded: false, reason: 'memory' };
1172
+ }
1173
+ await evictIdleWhere(this._models, (key) => modelOf(key) === model && !this._replying.includes(key));
1174
+ await this._unloadIdleGenerators();
1175
+ }
1176
+ this._pinned.add(target.key);
1177
+ try {
1178
+ await this._loadAndRelease(target, signal);
1179
+ }
1180
+ catch (err) {
1181
+ this._pinned.delete(target.key);
1182
+ throw err;
1183
+ }
1184
+ return { loaded: true };
1185
+ }
1186
+ unloadModel(context) {
1187
+ return this._busy(() => this._unloadModel(context));
1188
+ }
1189
+ async _unloadModel({ model }) {
1190
+ if (!this.opened)
1191
+ await this.ready();
1192
+ const target = this._residency(model);
1193
+ const name = modelOf(target.key);
1194
+ const unloaded = await evictModel(target.pool, name, { deadlineMs: UNLOAD_LEASE_WAIT_MS });
1195
+ hlog('unload model', { model, unloaded });
1196
+ if (unloaded) {
1197
+ for (const key of this._pinned) {
1198
+ if (modelOf(key) === name) {
1199
+ this._pinned.delete(key);
1200
+ }
1201
+ }
1202
+ }
1203
+ return { unloaded };
1204
+ }
1205
+ async _loadAndRelease(target, signal) {
1011
1206
  try {
1012
- await raceAbort(pool.open(key, model, loadOpts).load(), signal);
1207
+ await raceAbort(target.open().load(), signal);
1013
1208
  }
1014
1209
  finally {
1015
- releaseModel(pool, key);
1210
+ releaseModel(target.pool, target.key);
1016
1211
  }
1017
1212
  }
1018
- async *invoke({ capability, args, enhance, agent, attach, fetchAttachment, progress, signal }) {
1213
+ async _unloadIdleGenerators() {
1214
+ await this._imageRuntime?.unloadIdle().catch(noop);
1215
+ await this._musicRuntime?.unloadIdle().catch(noop);
1216
+ }
1217
+ async _fits(model, ctxSize) {
1218
+ const asset = curatedModelAsset(model, 'llm');
1219
+ if (!asset) {
1220
+ return true;
1221
+ }
1222
+ const assessed = await this._assessFit({
1223
+ sha256Checksum: asset.sha256Checksum,
1224
+ name: asset.name,
1225
+ contextTokens: ctxSize || defaultContextSize(asset)
1226
+ }).catch((err) => {
1227
+ hlog('fit estimate failed', { model, err: errorMessage(err) });
1228
+ return null;
1229
+ });
1230
+ return assessed?.fit !== 'likely-too-large';
1231
+ }
1232
+ async _yieldPins(model, ctxSize) {
1233
+ for (const key of this._pinned) {
1234
+ if (leaseCount(this._models, key) > 0 || this._replying.includes(key)) {
1235
+ continue;
1236
+ }
1237
+ if (await this._fits(model, ctxSize))
1238
+ return;
1239
+ this._pinned.delete(key);
1240
+ await evictModelWithin(this._models, key, { deadlineMs: WEDGED_EVICT_WAIT_MS });
1241
+ }
1242
+ }
1243
+ _residency(model, agent) {
1244
+ const addon = modelAddonOf(model);
1245
+ if (addon === 'llm') {
1246
+ const loadOpts = this._completionLoadOptions(model, agent);
1247
+ const key = modelKey(model, loadOpts);
1248
+ return {
1249
+ pool: this._models,
1250
+ key,
1251
+ ctxSize: loadOpts.ctxSize,
1252
+ open: () => this._models.open(key, model, loadOpts)
1253
+ };
1254
+ }
1255
+ if (addon === 'embeddings') {
1256
+ const key = this._embedFaults.key(residentSrc(model, addon));
1257
+ return { pool: this._embedModels, key, open: () => this._embedModels.open(key, model) };
1258
+ }
1259
+ if (addon === 'tts' || addon === 'parakeet') {
1260
+ const pool = addon === 'tts' ? this._ttsModels : this._transcriptionModels;
1261
+ const key = residentSrc(model, addon);
1262
+ return { pool, key, open: () => pool.open(key, key) };
1263
+ }
1264
+ throw new Error(`${model} loads for each generation and is not kept in memory`);
1265
+ }
1266
+ _completionLoadOptions(model, agent) {
1267
+ return resolveLoadOptions({
1268
+ ctxSize: agent?.ctxSize ?? undefined,
1269
+ kvCacheK: agent?.kvCacheK ?? undefined,
1270
+ kvCacheV: agent?.kvCacheV ?? undefined,
1271
+ projectionModel: this._projectionFor(model)
1272
+ }, this._deviceConfig());
1273
+ }
1274
+ invoke(context) {
1275
+ return this._busyStream(() => this._invoke(context));
1276
+ }
1277
+ async *_invoke({ capability, args, enhance, agent, attach, fetchAttachment, progress, signal }) {
1019
1278
  if (!this.opened)
1020
1279
  await this.ready();
1021
- if (this.suspended)
1280
+ if (this.suspended) {
1022
1281
  return yield { type: 'aborted' };
1282
+ }
1023
1283
  hlog('invoke start', { capability, enhance });
1024
1284
  try {
1025
1285
  yield* invoke({
@@ -1041,31 +1301,28 @@ export class Harness extends ReadyResource {
1041
1301
  hlog('invoke end', { capability });
1042
1302
  }
1043
1303
  }
1044
- async *completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
1045
- await this.ready();
1304
+ completion(context) {
1305
+ return this._busyStream(() => this._completion(context));
1306
+ }
1307
+ async *_completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
1308
+ if (!this.opened)
1309
+ await this.ready();
1046
1310
  if (this.suspended)
1047
1311
  throw new Error('harness is suspended');
1048
- const projectionModel = this._projectionFor(model);
1049
- const loadOpts = resolveLoadOptions({
1050
- ctxSize: agent?.ctxSize ?? undefined,
1051
- kvCacheK: agent?.kvCacheK ?? undefined,
1052
- kvCacheV: agent?.kvCacheV ?? undefined,
1053
- projectionModel
1054
- }, this._deviceConfig());
1312
+ const loadOpts = this._completionLoadOptions(model, agent);
1313
+ const projectionModel = loadOpts.projectionModel;
1055
1314
  const key = modelKey(model, loadOpts);
1056
1315
  const pool = this._models;
1057
1316
  const resident = isResident(pool, key);
1058
1317
  hlog('completion start', { model, resident, projection: Boolean(projectionModel) });
1059
1318
  if (!resident) {
1060
- await this._makeRoomForCompletion(signal);
1061
- await this._imageRuntime?.unloadIdle().catch(noop);
1062
- await this._musicRuntime?.unloadIdle().catch(noop);
1319
+ await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
1320
+ await this._unloadIdleGenerators();
1063
1321
  }
1064
1322
  const acquireRound = async () => {
1065
1323
  if (!isResident(pool, key)) {
1066
- await this._makeRoomForCompletion(signal);
1067
- await this._imageRuntime?.unloadIdle().catch(noop);
1068
- await this._musicRuntime?.unloadIdle().catch(noop);
1324
+ await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
1325
+ await this._unloadIdleGenerators();
1069
1326
  }
1070
1327
  try {
1071
1328
  const { modelId } = await raceAbort(pool.open(key, model, loadOpts).load(), signal);
@@ -1089,6 +1346,7 @@ export class Harness extends ReadyResource {
1089
1346
  held = false;
1090
1347
  releaseModel(pool, key);
1091
1348
  };
1349
+ this._replying.push(key);
1092
1350
  try {
1093
1351
  const { modelId, ctxSize } = await raceAbort(handle.load(), signal);
1094
1352
  hlog('model ready', { model });
@@ -1137,21 +1395,18 @@ export class Harness extends ReadyResource {
1137
1395
  yield { type: 'aborted' };
1138
1396
  }
1139
1397
  else {
1140
- const message = err instanceof Error ? err.message : String(err);
1398
+ const message = errorMessage(err);
1141
1399
  hlog('completion failed', { model, ...resolutionDetail(err), err: message });
1142
- if (isSdkModelWedgedError(err)) {
1143
- const wedged = await this._evictWedgedModel(model, key);
1144
- yield { type: 'error', message: wedged.message };
1145
- if (wedged.fatal)
1146
- process.exit(1);
1147
- }
1148
- else {
1149
- yield { type: 'error', message };
1400
+ const wedged = isSdkModelWedgedError(err) ? await this._evictWedgedModel(model, key) : null;
1401
+ yield { type: 'error', message: wedged ? wedged.message : message };
1402
+ if (wedged?.fatal) {
1403
+ process.exit(1);
1150
1404
  }
1151
1405
  }
1152
1406
  }
1153
1407
  finally {
1154
1408
  hlog('completion end', { model });
1409
+ this._replying.splice(this._replying.indexOf(key), 1);
1155
1410
  releaseHeld();
1156
1411
  void this._imageRuntime?.unloadIdle().catch(noop);
1157
1412
  void this._musicRuntime?.unloadIdle().catch(noop);
@@ -1160,14 +1415,19 @@ export class Harness extends ReadyResource {
1160
1415
  _residentEnhancer() {
1161
1416
  return async (prompt, signal) => {
1162
1417
  const lease = await leaseResidentModel(this._models, (model) => !isTranslationModel(model));
1163
- if (!lease)
1418
+ if (!lease) {
1164
1419
  return null;
1420
+ }
1165
1421
  try {
1166
1422
  const sdk = await this._sdk.load();
1167
- return await expandImagePrompt(sdk, lease.modelId, lease.model, prompt, signal);
1423
+ return await expandImagePrompt(sdk, prompt, {
1424
+ modelId: lease.modelId,
1425
+ model: lease.model,
1426
+ signal
1427
+ });
1168
1428
  }
1169
1429
  catch (err) {
1170
- hlog('enhance failed', { err: err instanceof Error ? err.message : String(err) });
1430
+ hlog('enhance failed', { err: errorMessage(err) });
1171
1431
  return null;
1172
1432
  }
1173
1433
  finally {
@@ -1175,24 +1435,33 @@ export class Harness extends ReadyResource {
1175
1435
  }
1176
1436
  };
1177
1437
  }
1178
- async _makeRoomForCompletion(signal, deadlineMs = MODEL_ROOM_WAIT_MS) {
1179
- const roomed = await awaitModelCapacity(this._models, this._completionModelCap, deadlineMs, 250, signal);
1438
+ async _makeRoomForCompletion(model, { ctxSize, signal, deadlineMs = MODEL_ROOM_WAIT_MS } = {}) {
1439
+ const roomed = await awaitModelCapacity(this._models, {
1440
+ cap: this._completionModelCap,
1441
+ deadlineMs,
1442
+ signal,
1443
+ evictable: (key) => !this._pinned.has(key) || modelOf(key) === model
1444
+ });
1180
1445
  if (!roomed) {
1181
1446
  hlog('model room wait gave up', {
1182
1447
  cap: this._completionModelCap,
1183
1448
  loaded: loadedModels(this._models).length
1184
1449
  });
1185
1450
  }
1451
+ await this._yieldPins(model, ctxSize);
1186
1452
  return roomed;
1187
1453
  }
1188
1454
  async _evictWedgedModel(model, key) {
1189
1455
  const faults = (this._modelFaults.get(model) ?? 0) + 1;
1190
1456
  this._modelFaults.set(model, faults);
1191
1457
  const leased = leaseCount(this._models, key) > 0;
1192
- const evicted = leased ? false : await evictModelWithin(this._models, key, WEDGED_EVICT_WAIT_MS);
1458
+ const evicted = leased
1459
+ ? false
1460
+ : await evictModelWithin(this._models, key, { deadlineMs: WEDGED_EVICT_WAIT_MS });
1193
1461
  hlog('wedged model evicted', { model, faults, evicted, leased });
1194
- if (leased)
1462
+ if (leased) {
1195
1463
  this._evictAfterLeases(model, key);
1464
+ }
1196
1465
  if (!evicted && !leased) {
1197
1466
  return {
1198
1467
  message: `${model} is stuck in device memory — restarting the model runtime`,
@@ -1207,9 +1476,27 @@ export class Harness extends ReadyResource {
1207
1476
  };
1208
1477
  }
1209
1478
  _evictAfterLeases(model, key) {
1210
- void evictModelWithin(this._models, key, WEDGED_EVICT_LEASED_MS)
1479
+ void evictModelWithin(this._models, key, { deadlineMs: WEDGED_EVICT_LEASED_MS })
1211
1480
  .then((evicted) => hlog('wedged model evicted after leases', { model, evicted }))
1212
1481
  .catch(noop);
1213
1482
  }
1483
+ async _busy(run) {
1484
+ await this._idle.enter();
1485
+ try {
1486
+ return await run();
1487
+ }
1488
+ finally {
1489
+ this._idle.exit();
1490
+ }
1491
+ }
1492
+ async *_busyStream(run) {
1493
+ await this._idle.enter();
1494
+ try {
1495
+ yield* run();
1496
+ }
1497
+ finally {
1498
+ this._idle.exit();
1499
+ }
1500
+ }
1214
1501
  }
1215
1502
  function noop() { }