@qvac/harness 0.4.0 → 0.5.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 +689 -141
  2. package/README.md +29 -7
  3. package/dist/harness.bundle +71 -50
  4. package/dist-lib/index.d.ts +36 -39
  5. package/dist-lib/index.js +279 -114
  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 +11 -32
  29. package/dist-lib/lib/connect.js +152 -112
  30. package/dist-lib/lib/context-window.js +25 -21
  31. package/dist-lib/lib/contract-types.d.ts +22 -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 +13 -20
  84. package/dist-lib/lib/models/model-catalog.js +59 -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 +44 -10
  90. package/dist-lib/lib/models/model-pool.js +47 -42
  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 +110 -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 +14 -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 +3 -0
  288. package/dist-lib/spec/calls.js +1 -0
  289. package/dist-lib/spec/hrpc/hrpc.json +13 -0
  290. package/dist-lib/spec/hrpc/index.js +15 -3
  291. package/dist-lib/spec/hrpc/messages.js +39 -8
  292. package/dist-lib/spec/hyperschema/index.js +39 -8
  293. package/dist-lib/spec/hyperschema/schema.json +30 -1
  294. package/dist-lib/spec/hyperschema/types.d.ts +9 -1
  295. package/package.json +13 -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
@@ -26,6 +26,7 @@ import { createModelStatusCache, customModelRows, invalidateModelStatusCache, me
26
26
  import { deleteModelFiles, summarizeDelete } from './lib/models/model-delete.js';
27
27
  import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, modelKey, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
28
28
  import { resolveMainGpu } from './lib/models/gpu-selection.js';
29
+ import { createIdleReaper } from './lib/models/idle-unload.js';
29
30
  import { openclCacheDir } from './lib/models/opencl-cache.js';
30
31
  import { assertEmbedDevice, createEmbedConfig } from './lib/models/embed-config.js';
31
32
  import { createGpuFaultState, isSdkGpuDeviceLostError, isSdkModelWedgedError } from './lib/models/sdk-faults.js';
@@ -39,6 +40,7 @@ import { ensureBundledSkillsDir } from './lib/tools/skills/bundled-dir.js';
39
40
  import { ensureSkillsOverlayDir } from './lib/tools/skills/overlay-dir.js';
40
41
  import { copyBaseForEdit, deleteOverlaySkill, listManagedSkills, readSkillFile, writeSkillFile } from './lib/tools/skills/overlay-crud.js';
41
42
  import { watchSkillsOverlay } from './lib/tools/skills/overlay-watch.js';
43
+ import { errorMessage } from './lib/guards.js';
42
44
  import { hlog } from './lib/log.js';
43
45
  import { harness } from './spec/index.js';
44
46
  import { installResolutionDetector, resolutionDetail } from './lib/resolution-detector.js';
@@ -133,6 +135,7 @@ export class Harness extends ReadyResource {
133
135
  _capability = null;
134
136
  _capabilityInflight = null;
135
137
  _modelFaults = new Map();
138
+ _idle;
136
139
  constructor({ tools = [], sdkLoader, skillsDir, modelsDir, projectionModel, imageModel, imageClamps, musicModel, musicClamps, llmDevice, llmGpuLayers, contextSize, kvCacheK, kvCacheV, embedDevice, embedGpuLayers, embedBatchSize, transcriptionDevice, completionModelCap, credentialStore, platform, gpuStats } = {}) {
137
140
  super();
138
141
  this._sdk = sdkLoader ?? createSdkLoader();
@@ -177,8 +180,9 @@ export class Harness extends ReadyResource {
177
180
  },
178
181
  onLoaded: (model) => {
179
182
  const transfer = imageTransfers.get(model);
180
- if (!transfer)
183
+ if (!transfer) {
181
184
  return;
185
+ }
182
186
  imageTransfers.delete(model);
183
187
  invalidateModelStatusCache(this._modelStatus);
184
188
  this.emit('model', { ...transfer, status: 'downloaded' });
@@ -186,9 +190,14 @@ export class Harness extends ReadyResource {
186
190
  onBeforeLoad: async (signal) => {
187
191
  if (this._models.destroyed)
188
192
  return;
189
- const evicted = await awaitModelCapacity(this._models, 1, DIFFUSION_EVICT_WAIT_MS, undefined, signal);
190
- if (signal?.aborted)
193
+ const evicted = await awaitModelCapacity(this._models, {
194
+ cap: 1,
195
+ deadlineMs: DIFFUSION_EVICT_WAIT_MS,
196
+ signal
197
+ });
198
+ if (signal?.aborted) {
191
199
  throw new AbortedError();
200
+ }
192
201
  if (!evicted) {
193
202
  throw new Error('the chat model is busy; try the image again once the current reply finishes');
194
203
  }
@@ -205,9 +214,14 @@ export class Harness extends ReadyResource {
205
214
  onBeforeLoad: async (signal) => {
206
215
  if (this._models.destroyed)
207
216
  return;
208
- const evicted = await awaitModelCapacity(this._models, 1, MUSIC_EVICT_WAIT_MS, undefined, signal);
209
- if (signal?.aborted)
217
+ const evicted = await awaitModelCapacity(this._models, {
218
+ cap: 1,
219
+ deadlineMs: MUSIC_EVICT_WAIT_MS,
220
+ signal
221
+ });
222
+ if (signal?.aborted) {
210
223
  throw new AbortedError();
224
+ }
211
225
  if (!evicted) {
212
226
  throw new Error('the chat model is busy; try the music again once the current reply finishes');
213
227
  }
@@ -245,13 +259,18 @@ export class Harness extends ReadyResource {
245
259
  this._diffusionSdk = null;
246
260
  this._musicSdk = null;
247
261
  this._translationSdk = null;
248
- }
249
- async _open() {
250
- installResolutionDetector(import.meta);
251
262
  this._models = this._createPool();
252
263
  this._embedModels = this._createEmbedPool();
253
264
  this._ttsModels = this._createTtsPool();
254
265
  this._transcriptionModels = this._createTranscriptionPool();
266
+ this._idle = createIdleReaper({
267
+ sweep: () => this._unloadIdleModels(),
268
+ waitMs: WEDGED_EVICT_WAIT_MS,
269
+ onError: (err) => hlog('idle unload failed', { err: errorMessage(err) })
270
+ });
271
+ }
272
+ async _open() {
273
+ installResolutionDetector(import.meta);
255
274
  await this._reloadSkills();
256
275
  await this._startOverlayWatch();
257
276
  hlog('open', { skills: this._supportedSkills.length, tools: this._tools.length });
@@ -269,10 +288,7 @@ export class Harness extends ReadyResource {
269
288
  if (!token)
270
289
  continue;
271
290
  const storeKey = scopeKey({ agentId, skill, key });
272
- const record = {
273
- token,
274
- ...(typeof expiresAt === 'number' && expiresAt > 0 ? { expiresAt } : {})
275
- };
291
+ const record = typeof expiresAt === 'number' && expiresAt > 0 ? { token, expiresAt } : { token };
276
292
  const current = previous.get(storeKey);
277
293
  if (!current || current.token !== record.token || current.expiresAt !== record.expiresAt) {
278
294
  resetCredentialHealthAfterSnapshot(key, agentId, skill);
@@ -310,48 +326,57 @@ export class Harness extends ReadyResource {
310
326
  }
311
327
  async writeSkillFile(name, content) {
312
328
  const overlay = await ensureSkillsOverlayDir();
313
- if (!overlay)
329
+ if (!overlay) {
314
330
  throw new Error('no writable skills overlay is configured');
331
+ }
315
332
  await writeSkillFile(overlay, name, content);
316
333
  await this._reloadSkills();
317
334
  return { ok: true };
318
335
  }
319
336
  async copyBaseForEdit(name) {
320
337
  const overlay = await ensureSkillsOverlayDir();
321
- if (!overlay)
338
+ if (!overlay) {
322
339
  throw new Error('no writable skills overlay is configured');
340
+ }
323
341
  const result = await copyBaseForEdit(await this._baseSkillsDir(), overlay, name);
324
- if (result.copied)
342
+ if (result.copied) {
325
343
  await this._reloadSkills();
344
+ }
326
345
  return result;
327
346
  }
328
347
  async deleteOverlaySkill(name) {
329
348
  const overlay = await ensureSkillsOverlayDir();
330
- if (!overlay)
349
+ if (!overlay) {
331
350
  throw new Error('no writable skills overlay is configured');
351
+ }
332
352
  const result = await deleteOverlaySkill(overlay, name);
333
- if (result.removed)
353
+ if (result.removed) {
334
354
  await this._reloadSkills();
355
+ }
335
356
  return result;
336
357
  }
337
358
  resolveCredential(key, ctx) {
338
- if (this._credentialStore)
359
+ if (this._credentialStore) {
339
360
  return this._credentialStore(key, ctx);
361
+ }
340
362
  const agentId = ctx?.agentId ?? null;
341
363
  const deviceShared = isDeviceSharedCredentialKey(key);
342
364
  for (const skill of ctx?.skills ?? []) {
343
365
  const owned = this._credentials.get(scopeKey({ agentId, skill, key })) ??
344
366
  (agentId && deviceShared ? this._credentials.get(scopeKey({ skill, key })) : undefined);
345
- if (owned)
367
+ if (owned) {
346
368
  return owned;
369
+ }
347
370
  }
348
371
  if (agentId) {
349
372
  const scoped = this._credentials.get(scopeKey({ agentId, key }));
350
- if (scoped)
373
+ if (scoped) {
351
374
  return scoped;
375
+ }
352
376
  }
353
- if (!agentId || deviceShared)
377
+ if (!agentId || deviceShared) {
354
378
  return this._credentials.get(scopeKey({ key })) ?? null;
379
+ }
355
380
  return null;
356
381
  }
357
382
  async _completionSkills(agentId, model) {
@@ -362,7 +387,8 @@ export class Harness extends ReadyResource {
362
387
  }));
363
388
  }
364
389
  async skills(agentId, modelName, options) {
365
- await this.ready();
390
+ if (!this.opened)
391
+ await this.ready();
366
392
  const platform = this._platform ?? detectPlatform();
367
393
  const refresh = options?.refresh === true;
368
394
  return resolveSkillStatuses(this._allSkills, {
@@ -376,7 +402,7 @@ export class Harness extends ReadyResource {
376
402
  : await this._probeBinVersions(platform, refresh),
377
403
  refresh,
378
404
  skipCredentialProbe: options?.skipCredentialProbe === true,
379
- ...(modelName ? { activeProfile: modelNameToCapabilityProfile(modelName) } : {})
405
+ activeProfile: modelName ? modelNameToCapabilityProfile(modelName) : undefined
380
406
  });
381
407
  }
382
408
  async _reloadSkills() {
@@ -391,14 +417,15 @@ export class Harness extends ReadyResource {
391
417
  return;
392
418
  try {
393
419
  const overlay = await ensureSkillsOverlayDir();
394
- if (!overlay)
420
+ if (!overlay) {
395
421
  return;
422
+ }
396
423
  this._overlayWatcher = watchSkillsOverlay(overlay, () => {
397
- this._reloadSkills().catch((err) => hlog('overlay reload failed', { err: err instanceof Error ? err.message : String(err) }));
424
+ this._reloadSkills().catch((err) => hlog('overlay reload failed', { err: errorMessage(err) }));
398
425
  });
399
426
  }
400
427
  catch (err) {
401
- hlog('overlay watch unavailable', { err: err instanceof Error ? err.message : String(err) });
428
+ hlog('overlay watch unavailable', { err: errorMessage(err) });
402
429
  }
403
430
  }
404
431
  async tools() {
@@ -411,8 +438,9 @@ export class Harness extends ReadyResource {
411
438
  if (!refresh && this._binVersionCache && this._binVersionCache.expiresAt > now) {
412
439
  return this._binVersionCache.bySkill;
413
440
  }
414
- if (this._binVersionInflight)
441
+ if (this._binVersionInflight) {
415
442
  return this._binVersionInflight;
443
+ }
416
444
  this._binVersionInflight = this._runBinVersionProbes(platform)
417
445
  .then((bySkill) => {
418
446
  this._binVersionCache = {
@@ -427,12 +455,14 @@ export class Harness extends ReadyResource {
427
455
  return this._binVersionInflight;
428
456
  }
429
457
  async _runBinVersionProbes(platform) {
430
- if (process.env.QVAC_NO_SKILL_BIN_PROBE)
458
+ if (process.env.QVAC_NO_SKILL_BIN_PROBE) {
431
459
  return new Map();
460
+ }
432
461
  const installed = new Set(this._tools.map((tool) => tool.name));
433
462
  const candidates = versionProbeSkills(this._allSkills, installed, platform);
434
- if (candidates.length === 0)
463
+ if (candidates.length === 0) {
435
464
  return new Map();
465
+ }
436
466
  let runtime;
437
467
  try {
438
468
  runtime = await bareRuntime();
@@ -452,8 +482,9 @@ export class Harness extends ReadyResource {
452
482
  const bySkill = new Map();
453
483
  for (const skill of candidates) {
454
484
  const states = await probeBinVersions(skill.requires, { platform, run });
455
- if (states.size > 0)
485
+ if (states.size > 0) {
456
486
  bySkill.set(skill.name, states);
487
+ }
457
488
  }
458
489
  return bySkill;
459
490
  }
@@ -472,17 +503,22 @@ export class Harness extends ReadyResource {
472
503
  }
473
504
  _projectionFor(model) {
474
505
  const custom = this._customProjection[model];
475
- if (custom)
506
+ if (custom) {
476
507
  return custom;
508
+ }
477
509
  const projection = this._projectionModel;
478
- if (!projection)
510
+ if (!projection) {
479
511
  return undefined;
512
+ }
480
513
  return typeof projection === 'string' ? projection : projection[model];
481
514
  }
482
515
  setCustomModels(entries) {
483
516
  setCustomModels(entries);
484
517
  this._customProjection = customProjectionMap();
485
518
  }
519
+ setModelIdleTimeout(ms) {
520
+ this._idle.configure(ms);
521
+ }
486
522
  previewModelSource(context) {
487
523
  return previewModelSource(context, {
488
524
  assessFit: async ({ sha256Checksum, name, contextTokens }) => {
@@ -519,11 +555,12 @@ export class Harness extends ReadyResource {
519
555
  };
520
556
  }
521
557
  _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);
558
+ const pools = [
559
+ this._embedModels,
560
+ ...this._retiredEmbedModels,
561
+ this._ttsModels,
562
+ this._transcriptionModels
563
+ ];
527
564
  return [...loadedModels(this._models), ...pools.flatMap((pool) => loadedModels(pool))];
528
565
  }
529
566
  _loadPluginSdk(current, store, importPlugin) {
@@ -557,8 +594,9 @@ export class Harness extends ReadyResource {
557
594
  const loadOpts = resolveLoadOptions({}, this._deviceConfig());
558
595
  const key = modelKey(model, loadOpts);
559
596
  const pool = this._models;
560
- if (!isResident(pool, key))
597
+ if (!isResident(pool, key)) {
561
598
  await this._makeRoomForCompletion(signal);
599
+ }
562
600
  try {
563
601
  const { modelId } = await raceAbort(pool.open(key, model, loadOpts).load(), signal);
564
602
  const sdk = await this._sdk.load();
@@ -592,21 +630,40 @@ export class Harness extends ReadyResource {
592
630
  }
593
631
  _createPool() {
594
632
  const loadSdk = this._sdk.load;
595
- return createModelPool(loadSdk, (state) => this._notifyModel(state, true), this._deviceConfig(), this._loads);
633
+ return createModelPool(loadSdk, (state) => this._notifyModel(state, { languageModel: true }), {
634
+ deviceConfig: this._deviceConfig(),
635
+ loads: this._loads
636
+ });
596
637
  }
597
638
  _createEmbedPool() {
598
639
  const loadSdk = () => this._sdk.loadEmbed();
599
- return createEmbedPool(loadSdk, (state) => this._notifyModel(state, false), this._embedFaults, this._embedBatchSize, openclCacheDir, this._loads);
640
+ return createEmbedPool(loadSdk, (state) => this._notifyModel(state), {
641
+ faults: this._embedFaults,
642
+ batchSize: this._embedBatchSize,
643
+ openclCacheDir,
644
+ loads: this._loads
645
+ });
600
646
  }
601
647
  _createTtsPool() {
602
648
  const loadSdk = () => this._sdk.loadVoice();
603
- return createVoicePool(loadSdk, (state) => this._notifyModel(state, false), 'tts-ggml', 'tts', TTS_MODEL_CONFIG, undefined, this._loads);
649
+ return createVoicePool(loadSdk, (state) => this._notifyModel(state), {
650
+ modelType: 'tts-ggml',
651
+ addon: 'tts',
652
+ modelConfig: TTS_MODEL_CONFIG,
653
+ loads: this._loads
654
+ });
604
655
  }
605
656
  _createTranscriptionPool() {
606
657
  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);
658
+ return createVoicePool(loadSdk, (state) => this._notifyModel({ ...state, model: TRANSCRIPTION_MODEL_LABEL }), {
659
+ modelType: 'parakeet-transcription',
660
+ addon: 'parakeet',
661
+ modelConfig: { useGPU: this._transcriptionDevice !== 'cpu' },
662
+ openclCacheDir,
663
+ loads: this._loads
664
+ });
608
665
  }
609
- _notifyModel(state, languageModel) {
666
+ _notifyModel(state, { languageModel = false } = {}) {
610
667
  const catalogChanged = languageModel && (state.status === 'ready' || state.status === 'unloaded');
611
668
  if (catalogChanged) {
612
669
  invalidateModelStatusCache(this._modelStatus);
@@ -646,14 +703,17 @@ export class Harness extends ReadyResource {
646
703
  return this._enqueueEmbedControl(() => this._setEmbeddingDevice(device));
647
704
  }
648
705
  async _setEmbeddingDevice(device) {
649
- await this.ready();
706
+ if (!this.opened)
707
+ await this.ready();
650
708
  if (this.closing)
651
709
  throw new Error('harness is closing');
652
- if (this._embedFaults.device === device)
710
+ if (this._embedFaults.device === device) {
653
711
  return;
712
+ }
654
713
  this._embedFaults = createGpuFaultState({ device, gpuLayers: this._embedGpuLayers });
655
- if (this.suspended)
714
+ if (this.suspended) {
656
715
  return;
716
+ }
657
717
  const retired = this._embedModels;
658
718
  this._retiredEmbedModels.add(retired);
659
719
  this._embedModels = this._createEmbedPool();
@@ -667,15 +727,18 @@ export class Harness extends ReadyResource {
667
727
  }
668
728
  async suspend() {
669
729
  hlog('suspend called');
670
- if (this._state === HarnessState.SUSPEND)
730
+ if (this._state === HarnessState.SUSPEND) {
671
731
  return;
672
- if (this.closing)
732
+ }
733
+ if (this.closing) {
673
734
  return;
735
+ }
674
736
  this._state = HarnessState.SUSPEND;
675
737
  hlog('suspending', { models: loadedModels(this._models).map((entry) => entry.model) });
676
738
  const pending = this._sdk.peek();
677
- if (pending)
739
+ if (pending) {
678
740
  this._loads.drain(await pending);
741
+ }
679
742
  await this._releaseRuntimes();
680
743
  await this._sdkLifecycle('suspend');
681
744
  invalidateModelStatusCache(this._modelStatus);
@@ -683,18 +746,20 @@ export class Harness extends ReadyResource {
683
746
  }
684
747
  async _sdkLifecycle(op) {
685
748
  const sdk = await this._sdk.peek()?.catch(noop);
686
- if (!sdk)
749
+ if (!sdk) {
687
750
  return;
751
+ }
688
752
  try {
689
753
  await sdk[op]();
690
754
  }
691
755
  catch (err) {
692
- hlog(`sdk ${op} failed`, { err: err instanceof Error ? err.message : String(err) });
693
- if (op === 'suspend')
756
+ hlog(`sdk ${op} failed`, { err: errorMessage(err) });
757
+ if (op === 'suspend') {
694
758
  return;
759
+ }
695
760
  await sdk.resume().catch((retryErr) => {
696
761
  hlog('sdk resume retry failed', {
697
- err: retryErr instanceof Error ? retryErr.message : String(retryErr)
762
+ err: errorMessage(retryErr)
698
763
  });
699
764
  });
700
765
  }
@@ -713,15 +778,38 @@ export class Harness extends ReadyResource {
713
778
  if (outcome.status !== 'rejected')
714
779
  continue;
715
780
  const err = outcome.reason;
716
- hlog('unload failed', { err: err instanceof Error ? err.message : String(err) });
781
+ hlog('unload failed', { err: errorMessage(err) });
717
782
  }
718
783
  }
784
+ async _unloadIdleModels() {
785
+ if (!this.opened || this.closing || this.suspended)
786
+ return;
787
+ hlog('idle unload', { models: this._loadedModels().map((entry) => entry.model) });
788
+ const released = await Promise.allSettled([
789
+ evictIdle(this._models),
790
+ evictIdle(this._embedModels),
791
+ ...[...this._retiredEmbedModels].map((pool) => this._collectRetiredEmbedPool(pool)),
792
+ evictIdle(this._ttsModels),
793
+ evictIdle(this._transcriptionModels),
794
+ this._imageRuntime?.unloadIdle(),
795
+ this._musicRuntime?.unloadIdle(),
796
+ this._translationRuntime.unload()
797
+ ]);
798
+ for (const outcome of released) {
799
+ if (outcome.status !== 'rejected')
800
+ continue;
801
+ hlog('idle unload failed', { err: errorMessage(outcome.reason) });
802
+ }
803
+ invalidateModelStatusCache(this._modelStatus);
804
+ }
719
805
  async resume() {
720
806
  hlog('resume called');
721
- if (this._state === HarnessState.RESUME)
807
+ if (this._state === HarnessState.RESUME) {
722
808
  return;
723
- if (this.closing)
809
+ }
810
+ if (this.closing) {
724
811
  return;
812
+ }
725
813
  if (this._state === HarnessState.PRESUSPEND) {
726
814
  this._state = HarnessState.RESUME;
727
815
  return;
@@ -744,22 +832,26 @@ export class Harness extends ReadyResource {
744
832
  }
745
833
  async _close() {
746
834
  hlog('closing');
835
+ this._idle.close();
747
836
  this._overlayWatcher?.stop();
748
837
  this._overlayWatcher = null;
749
838
  const pending = this._sdk.peek();
750
- if (pending)
839
+ if (pending) {
751
840
  this._loads.drain(await pending);
841
+ }
752
842
  await Promise.all([this._releaseRuntimes(), closePythonWorker()]);
753
843
  const sdk = await this._sdk.peek()?.catch(noop);
754
- if (sdk)
844
+ if (sdk) {
755
845
  await sdk.close();
846
+ }
756
847
  }
757
848
  download(args) {
758
849
  const projection = (args.addon ?? 'llm') === 'llm' ? this._projectionFor(args.model) : undefined;
759
850
  const multiFile = diffusionDownloadMetadata(args.model, args.addon) ??
760
851
  audioGenDownloadMetadata(args.model, args.addon);
761
852
  return download(async () => {
762
- await this.ready();
853
+ if (!this.opened)
854
+ await this.ready();
763
855
  return this._sdk.load();
764
856
  }, {
765
857
  ...args,
@@ -768,12 +860,17 @@ export class Harness extends ReadyResource {
768
860
  assetSizes: multiFile?.assetSizes
769
861
  }, () => invalidateModelStatusCache(this._modelStatus));
770
862
  }
771
- async embed({ model, texts, signal }) {
772
- await this.ready();
863
+ embed(context) {
864
+ return this._busy(() => this._embed(context));
865
+ }
866
+ async _embed({ model, texts, signal }) {
867
+ if (!this.opened)
868
+ await this.ready();
773
869
  if (this.suspended)
774
870
  throw new Error('harness is suspended');
775
- if (texts.length === 0)
871
+ if (texts.length === 0) {
776
872
  return [];
873
+ }
777
874
  const pool = this._embedModels;
778
875
  const faults = this._embedFaults;
779
876
  const generation = faults.generation;
@@ -784,7 +881,7 @@ export class Harness extends ReadyResource {
784
881
  let releaseAfter = null;
785
882
  try {
786
883
  loaded = await raceAbort(handle.load(), signal);
787
- const sdk = (await this._sdk.loadEmbed());
884
+ const sdk = await this._sdk.loadEmbed();
788
885
  job = this._embedLane(sdk, { modelId: loaded.modelId, texts });
789
886
  const vectors = await raceAbort(job, signal);
790
887
  faults.reset();
@@ -807,15 +904,18 @@ export class Harness extends ReadyResource {
807
904
  throw err;
808
905
  }
809
906
  finally {
810
- if (releaseAfter)
907
+ if (releaseAfter) {
811
908
  this._releaseEmbedResourceAfter(releaseAfter, pool, key);
812
- else
909
+ }
910
+ else {
813
911
  await this._releaseEmbedResource(pool, key);
912
+ }
814
913
  }
815
914
  }
816
915
  async _defaultVoiceSrc(category) {
817
- if (category !== 'tts')
916
+ if (category !== 'tts') {
818
917
  return defaultVoiceModel(category);
918
+ }
819
919
  try {
820
920
  const { capabilityProfile } = await this.capability();
821
921
  return defaultTtsFor(capabilityProfile).asset.src;
@@ -824,8 +924,9 @@ export class Harness extends ReadyResource {
824
924
  return defaultVoiceModel('tts');
825
925
  }
826
926
  }
827
- async _voiceJob(pool, category, model, signal, run) {
828
- await this.ready();
927
+ async _voiceJob(pool, { category, model, signal }, run) {
928
+ if (!this.opened)
929
+ await this.ready();
829
930
  if (this.suspended)
830
931
  throw new Error('harness is suspended');
831
932
  const name = model || (await this._defaultVoiceSrc(category));
@@ -844,8 +945,9 @@ export class Harness extends ReadyResource {
844
945
  }
845
946
  catch (err) {
846
947
  if (err instanceof AbortedError) {
847
- if (sdk && modelId)
948
+ if (sdk && modelId) {
848
949
  void sdk.cancel({ modelId, operation: 'inference' }).catch(noop);
950
+ }
849
951
  releaseAfter = job;
850
952
  }
851
953
  throw err;
@@ -854,19 +956,25 @@ export class Harness extends ReadyResource {
854
956
  const release = () => {
855
957
  releaseModel(pool, name);
856
958
  };
857
- if (releaseAfter)
959
+ if (releaseAfter) {
858
960
  void releaseAfter.then(release, release);
859
- else
961
+ }
962
+ else {
860
963
  release();
964
+ }
861
965
  }
862
966
  }
863
- async transcribe({ model, audio, signal }) {
864
- return this._voiceJob(this._transcriptionModels, 'transcription', model, signal, async (sdk, id) => {
967
+ transcribe(context) {
968
+ return this._busy(() => this._transcribe(context));
969
+ }
970
+ async _transcribe({ model, audio, signal }) {
971
+ return this._voiceJob(this._transcriptionModels, { category: 'transcription', model, signal }, async (sdk, id) => {
865
972
  const pcm = await pcmForTranscription(audio);
866
- if (signal?.aborted)
973
+ if (signal?.aborted) {
867
974
  throw new AbortedError();
975
+ }
868
976
  if (pcm && pcm.length >= TRANSCRIBE_STREAM_MIN_BYTES) {
869
- return transcribeStreaming(sdk, id, pcm, signal);
977
+ return transcribeStreaming(sdk, pcm, { modelId: id, signal });
870
978
  }
871
979
  return transcribeOnce(sdk, id, pcm ? buildWavBuffer(pcm) : audio);
872
980
  });
@@ -874,12 +982,19 @@ export class Harness extends ReadyResource {
874
982
  detectLanguage(context) {
875
983
  return detectLanguage(context);
876
984
  }
877
- async speak({ model, text, signal }) {
878
- const samples = await this._voiceJob(this._ttsModels, 'tts', model, signal, (sdk, id) => speakOnce(sdk, id, text));
985
+ speak(context) {
986
+ return this._busy(() => this._speak(context));
987
+ }
988
+ async _speak({ model, text, signal }) {
989
+ const samples = await this._voiceJob(this._ttsModels, { category: 'tts', model, signal }, (sdk, id) => speakOnce(sdk, id, text));
879
990
  return { audio: packSamples(samples), sampleRate: SUPERTONIC_SAMPLE_RATE };
880
991
  }
881
- async *speakStream({ model, text, signal }) {
882
- await this.ready();
992
+ speakStream(context) {
993
+ return this._busyStream(() => this._speakStream(context));
994
+ }
995
+ async *_speakStream({ model, text, signal }) {
996
+ if (!this.opened)
997
+ await this.ready();
883
998
  if (this.suspended)
884
999
  throw new Error('harness is suspended');
885
1000
  const name = model || (await this._defaultVoiceSrc('tts'));
@@ -889,18 +1004,21 @@ export class Harness extends ReadyResource {
889
1004
  let sdk = null;
890
1005
  let modelId = null;
891
1006
  const cancel = () => {
892
- if (sdk && modelId)
893
- void sdk.cancel({ modelId, operation: 'inference' }).catch(() => { });
1007
+ if (sdk && modelId) {
1008
+ void sdk.cancel({ modelId, operation: 'inference' }).catch(noop);
1009
+ }
894
1010
  };
895
1011
  signal?.addEventListener('abort', cancel, { once: true });
896
1012
  const job = this._voiceLane(async () => {
897
1013
  try {
898
1014
  modelId = (await handle.load()).modelId;
899
1015
  sdk = (await this._sdk.loadVoice());
900
- if (signal?.aborted)
1016
+ if (signal?.aborted) {
901
1017
  return;
902
- for await (const chunk of speakStreamOnce(sdk, modelId, text))
1018
+ }
1019
+ for await (const chunk of speakStreamOnce(sdk, modelId, text)) {
903
1020
  chunks.push(chunk);
1021
+ }
904
1022
  }
905
1023
  finally {
906
1024
  chunks.end();
@@ -914,18 +1032,23 @@ export class Harness extends ReadyResource {
914
1032
  }
915
1033
  finally {
916
1034
  signal?.removeEventListener('abort', cancel);
917
- if (!settled)
1035
+ if (!settled) {
918
1036
  cancel();
1037
+ }
919
1038
  void job.catch(noop).finally(() => {
920
1039
  releaseModel(pool, name);
921
1040
  });
922
1041
  }
923
1042
  }
924
1043
  async list(options = {}) {
925
- await this.ready();
926
- const sdk = (await this._sdk.load());
1044
+ if (!this.opened)
1045
+ await this.ready();
1046
+ const sdk = await this._sdk.load();
927
1047
  const [rows, custom] = await Promise.all([
928
- listModels(sdk, options, this._modelStatus, (name) => this._projectionFor(name)),
1048
+ listModels(sdk, options, {
1049
+ cache: this._modelStatus,
1050
+ projectionFor: (name) => this._projectionFor(name)
1051
+ }),
929
1052
  customModelRows(options, {
930
1053
  modelsDir: this._modelsDir,
931
1054
  loaded: new Set(this._loadedModels().map((entry) => entry.model))
@@ -934,18 +1057,21 @@ export class Harness extends ReadyResource {
934
1057
  return mergeCustomRows(rows, custom);
935
1058
  }
936
1059
  async deleteModel(name) {
937
- await this.ready();
938
- const sdk = (await this._sdk.load());
1060
+ if (!this.opened)
1061
+ await this.ready();
1062
+ const sdk = await this._sdk.load();
939
1063
  const result = await deleteModelFiles(sdk, name, { modelsDir: this._modelsDir });
940
1064
  invalidateModelStatusCache(this._modelStatus);
941
1065
  return summarizeDelete(result);
942
1066
  }
943
1067
  async clearKvCache() {
944
- await this.ready();
1068
+ if (!this.opened)
1069
+ await this.ready();
945
1070
  return deleteAllKvCaches(() => this._sdk.load());
946
1071
  }
947
1072
  async deleteKvCache(kvCacheKey) {
948
- await this.ready();
1073
+ if (!this.opened)
1074
+ await this.ready();
949
1075
  return deleteKvCacheKey(() => this._sdk.load(), kvCacheKey);
950
1076
  }
951
1077
  async infraModels(profile) {
@@ -989,8 +1115,12 @@ export class Harness extends ReadyResource {
989
1115
  });
990
1116
  return this._capabilityInflight;
991
1117
  }
992
- async warmModel({ model, agent, signal }) {
993
- await this.ready();
1118
+ warmModel(context) {
1119
+ return this._busy(() => this._warmModel(context));
1120
+ }
1121
+ async _warmModel({ model, agent, signal }) {
1122
+ if (!this.opened)
1123
+ await this.ready();
994
1124
  if (this.suspended)
995
1125
  return;
996
1126
  const projectionModel = this._projectionFor(model);
@@ -1004,8 +1134,9 @@ export class Harness extends ReadyResource {
1004
1134
  const pool = this._models;
1005
1135
  hlog('warm model', { model, resident: isResident(pool, key) });
1006
1136
  if (!isResident(pool, key)) {
1007
- if (!(await this._makeRoomForCompletion(signal, WARM_ROOM_WAIT_MS)))
1137
+ if (!(await this._makeRoomForCompletion(signal, WARM_ROOM_WAIT_MS))) {
1008
1138
  return;
1139
+ }
1009
1140
  await this._imageRuntime?.unloadIdle().catch(noop);
1010
1141
  }
1011
1142
  try {
@@ -1015,11 +1146,15 @@ export class Harness extends ReadyResource {
1015
1146
  releaseModel(pool, key);
1016
1147
  }
1017
1148
  }
1018
- async *invoke({ capability, args, enhance, agent, attach, fetchAttachment, progress, signal }) {
1149
+ invoke(context) {
1150
+ return this._busyStream(() => this._invoke(context));
1151
+ }
1152
+ async *_invoke({ capability, args, enhance, agent, attach, fetchAttachment, progress, signal }) {
1019
1153
  if (!this.opened)
1020
1154
  await this.ready();
1021
- if (this.suspended)
1155
+ if (this.suspended) {
1022
1156
  return yield { type: 'aborted' };
1157
+ }
1023
1158
  hlog('invoke start', { capability, enhance });
1024
1159
  try {
1025
1160
  yield* invoke({
@@ -1041,8 +1176,12 @@ export class Harness extends ReadyResource {
1041
1176
  hlog('invoke end', { capability });
1042
1177
  }
1043
1178
  }
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();
1179
+ completion(context) {
1180
+ return this._busyStream(() => this._completion(context));
1181
+ }
1182
+ async *_completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
1183
+ if (!this.opened)
1184
+ await this.ready();
1046
1185
  if (this.suspended)
1047
1186
  throw new Error('harness is suspended');
1048
1187
  const projectionModel = this._projectionFor(model);
@@ -1137,16 +1276,12 @@ export class Harness extends ReadyResource {
1137
1276
  yield { type: 'aborted' };
1138
1277
  }
1139
1278
  else {
1140
- const message = err instanceof Error ? err.message : String(err);
1279
+ const message = errorMessage(err);
1141
1280
  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 };
1281
+ const wedged = isSdkModelWedgedError(err) ? await this._evictWedgedModel(model, key) : null;
1282
+ yield { type: 'error', message: wedged ? wedged.message : message };
1283
+ if (wedged?.fatal) {
1284
+ process.exit(1);
1150
1285
  }
1151
1286
  }
1152
1287
  }
@@ -1160,14 +1295,19 @@ export class Harness extends ReadyResource {
1160
1295
  _residentEnhancer() {
1161
1296
  return async (prompt, signal) => {
1162
1297
  const lease = await leaseResidentModel(this._models, (model) => !isTranslationModel(model));
1163
- if (!lease)
1298
+ if (!lease) {
1164
1299
  return null;
1300
+ }
1165
1301
  try {
1166
1302
  const sdk = await this._sdk.load();
1167
- return await expandImagePrompt(sdk, lease.modelId, lease.model, prompt, signal);
1303
+ return await expandImagePrompt(sdk, prompt, {
1304
+ modelId: lease.modelId,
1305
+ model: lease.model,
1306
+ signal
1307
+ });
1168
1308
  }
1169
1309
  catch (err) {
1170
- hlog('enhance failed', { err: err instanceof Error ? err.message : String(err) });
1310
+ hlog('enhance failed', { err: errorMessage(err) });
1171
1311
  return null;
1172
1312
  }
1173
1313
  finally {
@@ -1176,7 +1316,11 @@ export class Harness extends ReadyResource {
1176
1316
  };
1177
1317
  }
1178
1318
  async _makeRoomForCompletion(signal, deadlineMs = MODEL_ROOM_WAIT_MS) {
1179
- const roomed = await awaitModelCapacity(this._models, this._completionModelCap, deadlineMs, 250, signal);
1319
+ const roomed = await awaitModelCapacity(this._models, {
1320
+ cap: this._completionModelCap,
1321
+ deadlineMs,
1322
+ signal
1323
+ });
1180
1324
  if (!roomed) {
1181
1325
  hlog('model room wait gave up', {
1182
1326
  cap: this._completionModelCap,
@@ -1189,10 +1333,13 @@ export class Harness extends ReadyResource {
1189
1333
  const faults = (this._modelFaults.get(model) ?? 0) + 1;
1190
1334
  this._modelFaults.set(model, faults);
1191
1335
  const leased = leaseCount(this._models, key) > 0;
1192
- const evicted = leased ? false : await evictModelWithin(this._models, key, WEDGED_EVICT_WAIT_MS);
1336
+ const evicted = leased
1337
+ ? false
1338
+ : await evictModelWithin(this._models, key, { deadlineMs: WEDGED_EVICT_WAIT_MS });
1193
1339
  hlog('wedged model evicted', { model, faults, evicted, leased });
1194
- if (leased)
1340
+ if (leased) {
1195
1341
  this._evictAfterLeases(model, key);
1342
+ }
1196
1343
  if (!evicted && !leased) {
1197
1344
  return {
1198
1345
  message: `${model} is stuck in device memory — restarting the model runtime`,
@@ -1207,9 +1354,27 @@ export class Harness extends ReadyResource {
1207
1354
  };
1208
1355
  }
1209
1356
  _evictAfterLeases(model, key) {
1210
- void evictModelWithin(this._models, key, WEDGED_EVICT_LEASED_MS)
1357
+ void evictModelWithin(this._models, key, { deadlineMs: WEDGED_EVICT_LEASED_MS })
1211
1358
  .then((evicted) => hlog('wedged model evicted after leases', { model, evicted }))
1212
1359
  .catch(noop);
1213
1360
  }
1361
+ async _busy(run) {
1362
+ await this._idle.enter();
1363
+ try {
1364
+ return await run();
1365
+ }
1366
+ finally {
1367
+ this._idle.exit();
1368
+ }
1369
+ }
1370
+ async *_busyStream(run) {
1371
+ await this._idle.enter();
1372
+ try {
1373
+ yield* run();
1374
+ }
1375
+ finally {
1376
+ this._idle.exit();
1377
+ }
1378
+ }
1214
1379
  }
1215
1380
  function noop() { }