@qvac/harness 0.7.1 → 1.0.0-rc.1

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 (201) hide show
  1. package/README.md +224 -0
  2. package/dist-lib/index.d.ts +55 -3
  3. package/dist-lib/index.js +721 -236
  4. package/dist-lib/lib/capabilities/completion-engine/abort-polyfill.d.ts +1 -0
  5. package/dist-lib/lib/capabilities/completion-engine/abort-polyfill.js +10 -0
  6. package/dist-lib/lib/capabilities/completion-engine/actors/answering.d.ts +3 -0
  7. package/dist-lib/lib/capabilities/completion-engine/actors/answering.js +17 -0
  8. package/dist-lib/lib/capabilities/completion-engine/actors/approval.d.ts +4 -0
  9. package/dist-lib/lib/capabilities/completion-engine/actors/approval.js +29 -0
  10. package/dist-lib/lib/capabilities/completion-engine/actors/direct-search.d.ts +4 -0
  11. package/dist-lib/lib/capabilities/completion-engine/actors/direct-search.js +73 -0
  12. package/dist-lib/lib/capabilities/completion-engine/actors/execute.d.ts +41 -0
  13. package/dist-lib/lib/capabilities/completion-engine/actors/execute.js +49 -0
  14. package/dist-lib/lib/capabilities/completion-engine/actors/executing.d.ts +3 -0
  15. package/dist-lib/lib/capabilities/completion-engine/actors/executing.js +102 -0
  16. package/dist-lib/lib/capabilities/completion-engine/actors/execution-plan.d.ts +47 -0
  17. package/dist-lib/lib/capabilities/completion-engine/actors/execution-plan.js +87 -0
  18. package/dist-lib/lib/capabilities/completion-engine/actors/finishing.d.ts +2 -0
  19. package/dist-lib/lib/capabilities/completion-engine/actors/finishing.js +32 -0
  20. package/dist-lib/lib/capabilities/completion-engine/actors/inspect-preload.d.ts +3 -0
  21. package/dist-lib/lib/capabilities/completion-engine/actors/inspect-preload.js +75 -0
  22. package/dist-lib/lib/capabilities/completion-engine/actors/inspecting.d.ts +3 -0
  23. package/dist-lib/lib/capabilities/completion-engine/actors/inspecting.js +152 -0
  24. package/dist-lib/lib/capabilities/completion-engine/actors/loading-skill.d.ts +3 -0
  25. package/dist-lib/lib/capabilities/completion-engine/actors/loading-skill.js +94 -0
  26. package/dist-lib/lib/capabilities/completion-engine/actors/preload-failure.d.ts +9 -0
  27. package/dist-lib/lib/capabilities/completion-engine/actors/preload-failure.js +11 -0
  28. package/dist-lib/lib/capabilities/completion-engine/actors/preloading.d.ts +3 -0
  29. package/dist-lib/lib/capabilities/completion-engine/actors/preloading.js +114 -0
  30. package/dist-lib/lib/capabilities/completion-engine/actors/preparing.d.ts +3 -0
  31. package/dist-lib/lib/capabilities/completion-engine/actors/preparing.js +198 -0
  32. package/dist-lib/lib/capabilities/completion-engine/actors/rejecting.d.ts +3 -0
  33. package/dist-lib/lib/capabilities/completion-engine/actors/rejecting.js +71 -0
  34. package/dist-lib/lib/capabilities/completion-engine/actors/rejection.d.ts +18 -0
  35. package/dist-lib/lib/capabilities/completion-engine/actors/rejection.js +70 -0
  36. package/dist-lib/lib/capabilities/completion-engine/actors/resolving.d.ts +3 -0
  37. package/dist-lib/lib/capabilities/completion-engine/actors/resolving.js +60 -0
  38. package/dist-lib/lib/capabilities/completion-engine/actors/stream.d.ts +21 -0
  39. package/dist-lib/lib/capabilities/completion-engine/actors/stream.js +48 -0
  40. package/dist-lib/lib/capabilities/completion-engine/actors/streaming.d.ts +3 -0
  41. package/dist-lib/lib/capabilities/completion-engine/actors/streaming.js +156 -0
  42. package/dist-lib/lib/capabilities/completion-engine/actors/wrap-up.d.ts +7 -0
  43. package/dist-lib/lib/capabilities/completion-engine/actors/wrap-up.js +61 -0
  44. package/dist-lib/lib/capabilities/completion-engine/driver.d.ts +25 -0
  45. package/dist-lib/lib/capabilities/completion-engine/driver.js +553 -0
  46. package/dist-lib/lib/capabilities/completion-engine/durable.d.ts +61 -0
  47. package/dist-lib/lib/capabilities/completion-engine/durable.js +150 -0
  48. package/dist-lib/lib/capabilities/completion-engine/operations.d.ts +6 -0
  49. package/dist-lib/lib/capabilities/completion-engine/operations.js +239 -0
  50. package/dist-lib/lib/capabilities/completion-engine/ports.d.ts +3 -0
  51. package/dist-lib/lib/capabilities/completion-engine/ports.js +267 -0
  52. package/dist-lib/lib/capabilities/completion-engine/session.d.ts +76 -0
  53. package/dist-lib/lib/capabilities/completion-engine/session.js +70 -0
  54. package/dist-lib/lib/capabilities/completion-loop/index.d.ts +80 -0
  55. package/dist-lib/lib/capabilities/completion-loop/index.js +158 -0
  56. package/dist-lib/lib/capabilities/completion-loop/metrics.d.ts +5 -0
  57. package/dist-lib/lib/capabilities/completion-loop/metrics.js +29 -0
  58. package/dist-lib/lib/capabilities/completion-loop/round.d.ts +33 -0
  59. package/dist-lib/lib/capabilities/completion-loop/round.js +353 -0
  60. package/dist-lib/lib/capabilities/completion-loop/tool-execution.d.ts +60 -0
  61. package/dist-lib/lib/capabilities/completion-loop/tool-execution.js +270 -0
  62. package/dist-lib/lib/capabilities/completion-loop/turns.d.ts +23 -0
  63. package/dist-lib/lib/capabilities/completion-loop/turns.js +112 -0
  64. package/dist-lib/lib/capabilities/completion-runtime.d.ts +46 -0
  65. package/dist-lib/lib/capabilities/completion-runtime.js +74 -0
  66. package/dist-lib/lib/capabilities/completion.d.ts +18 -335
  67. package/dist-lib/lib/capabilities/completion.js +20 -1301
  68. package/dist-lib/lib/capabilities/download.d.ts +1 -1
  69. package/dist-lib/lib/capabilities/download.js +7 -2
  70. package/dist-lib/lib/capabilities/invoke.js +5 -3
  71. package/dist-lib/lib/capabilities/tool-plan.d.ts +35 -0
  72. package/dist-lib/lib/capabilities/tool-plan.js +66 -0
  73. package/dist-lib/lib/capabilities/tool-turns.d.ts +11 -2
  74. package/dist-lib/lib/capabilities/tool-turns.js +30 -2
  75. package/dist-lib/lib/capabilities/types.d.ts +287 -0
  76. package/dist-lib/lib/capabilities/types.js +1 -0
  77. package/dist-lib/lib/completion-lifecycle.d.ts +14 -0
  78. package/dist-lib/lib/completion-lifecycle.js +126 -0
  79. package/dist-lib/lib/connect.d.ts +10 -3
  80. package/dist-lib/lib/connect.js +329 -23
  81. package/dist-lib/lib/contract-types.d.ts +19 -2
  82. package/dist-lib/lib/harness-lifecycle.d.ts +66 -0
  83. package/dist-lib/lib/harness-lifecycle.js +129 -0
  84. package/dist-lib/lib/helpers/detach-json.d.ts +1 -0
  85. package/dist-lib/lib/helpers/detach-json.js +146 -0
  86. package/dist-lib/lib/helpers/markup-scanner.d.ts +26 -0
  87. package/dist-lib/lib/helpers/markup-scanner.js +156 -0
  88. package/dist-lib/lib/helpers/markup-stream-filter.js +138 -35
  89. package/dist-lib/lib/helpers/race-abort.d.ts +9 -4
  90. package/dist-lib/lib/helpers/race-abort.js +2 -7
  91. package/dist-lib/lib/helpers/recovery-required.d.ts +1 -0
  92. package/dist-lib/lib/helpers/recovery-required.js +1 -0
  93. package/dist-lib/lib/helpers/run-outcome.d.ts +10 -2
  94. package/dist-lib/lib/helpers/run-outcome.js +8 -5
  95. package/dist-lib/lib/helpers/tool-call-markup.d.ts +2 -0
  96. package/dist-lib/lib/helpers/tool-call-markup.js +197 -82
  97. package/dist-lib/lib/hooks/built-in/arg-repair.d.ts +1 -0
  98. package/dist-lib/lib/hooks/built-in/arg-repair.js +1 -0
  99. package/dist-lib/lib/hooks/built-in/egress.d.ts +1 -0
  100. package/dist-lib/lib/hooks/built-in/egress.js +2 -1
  101. package/dist-lib/lib/hooks/built-in/sandbox.d.ts +1 -0
  102. package/dist-lib/lib/hooks/built-in/sandbox.js +6 -1
  103. package/dist-lib/lib/hooks/built-in/skill-catalog.d.ts +1 -0
  104. package/dist-lib/lib/hooks/built-in/skill-catalog.js +2 -1
  105. package/dist-lib/lib/hooks/pipeline.d.ts +4 -1
  106. package/dist-lib/lib/hooks/pipeline.js +24 -2
  107. package/dist-lib/lib/hooks/run.d.ts +2 -1
  108. package/dist-lib/lib/hooks/run.js +19 -7
  109. package/dist-lib/lib/hooks/types.d.ts +17 -6
  110. package/dist-lib/lib/machine/completion/actions/approval.d.ts +3 -0
  111. package/dist-lib/lib/machine/completion/actions/approval.js +16 -0
  112. package/dist-lib/lib/machine/completion/actions/completion.d.ts +23 -0
  113. package/dist-lib/lib/machine/completion/actions/completion.js +142 -0
  114. package/dist-lib/lib/machine/completion/actions/skills.d.ts +9 -0
  115. package/dist-lib/lib/machine/completion/actions/skills.js +83 -0
  116. package/dist-lib/lib/machine/completion/actions/tools.d.ts +16 -0
  117. package/dist-lib/lib/machine/completion/actions/tools.js +173 -0
  118. package/dist-lib/lib/machine/completion/context.d.ts +9 -0
  119. package/dist-lib/lib/machine/completion/context.js +44 -0
  120. package/dist-lib/lib/machine/completion/contract.d.ts +6 -0
  121. package/dist-lib/lib/machine/completion/contract.js +1 -0
  122. package/dist-lib/lib/machine/completion/guards/retry.d.ts +9 -0
  123. package/dist-lib/lib/machine/completion/guards/retry.js +25 -0
  124. package/dist-lib/lib/machine/completion/implementations.d.ts +81 -0
  125. package/dist-lib/lib/machine/completion/implementations.js +152 -0
  126. package/dist-lib/lib/machine/completion/machine.d.ts +375 -0
  127. package/dist-lib/lib/machine/completion/machine.js +511 -0
  128. package/dist-lib/lib/machine/completion/ports.d.ts +65 -0
  129. package/dist-lib/lib/machine/completion/ports.js +224 -0
  130. package/dist-lib/lib/machine/completion/rules.d.ts +39 -0
  131. package/dist-lib/lib/machine/completion/rules.js +115 -0
  132. package/dist-lib/lib/machine/completion/types.d.ts +493 -0
  133. package/dist-lib/lib/machine/completion/types.js +1 -0
  134. package/dist-lib/lib/machine/engine.d.ts +93 -0
  135. package/dist-lib/lib/machine/engine.js +483 -0
  136. package/dist-lib/lib/machine/errors.d.ts +44 -0
  137. package/dist-lib/lib/machine/errors.js +373 -0
  138. package/dist-lib/lib/machine/guards.d.ts +2 -0
  139. package/dist-lib/lib/machine/guards.js +6 -0
  140. package/dist-lib/lib/machine/index.d.ts +20 -0
  141. package/dist-lib/lib/machine/index.js +11 -0
  142. package/dist-lib/lib/machine/replay-output.d.ts +24 -0
  143. package/dist-lib/lib/machine/replay-output.js +50 -0
  144. package/dist-lib/lib/machine/stable-stringify.d.ts +1 -0
  145. package/dist-lib/lib/machine/stable-stringify.js +11 -0
  146. package/dist-lib/lib/models/model-pool.d.ts +14 -6
  147. package/dist-lib/lib/models/model-pool.js +95 -30
  148. package/dist-lib/lib/run-log/history-codec.d.ts +23 -0
  149. package/dist-lib/lib/run-log/history-codec.js +973 -0
  150. package/dist-lib/lib/run-log/patch.d.ts +6 -0
  151. package/dist-lib/lib/run-log/patch.js +93 -0
  152. package/dist-lib/lib/run-log/persisted-actor.d.ts +18 -0
  153. package/dist-lib/lib/run-log/persisted-actor.js +218 -0
  154. package/dist-lib/lib/run-log/protocol-types.d.ts +153 -0
  155. package/dist-lib/lib/run-log/protocol-types.js +1 -0
  156. package/dist-lib/lib/run-log/protocol.d.ts +27 -0
  157. package/dist-lib/lib/run-log/protocol.js +1056 -0
  158. package/dist-lib/lib/run-log/replay.d.ts +25 -0
  159. package/dist-lib/lib/run-log/replay.js +129 -0
  160. package/dist-lib/lib/run-log/run-journal.d.ts +125 -0
  161. package/dist-lib/lib/run-log/run-journal.js +783 -0
  162. package/dist-lib/lib/run-log-inspect.d.ts +25 -0
  163. package/dist-lib/lib/run-log-inspect.js +627 -0
  164. package/dist-lib/lib/run-log-protocol.d.ts +2 -0
  165. package/dist-lib/lib/run-log-protocol.js +1 -0
  166. package/dist-lib/lib/run-log-redact.d.ts +27 -0
  167. package/dist-lib/lib/run-log-redact.js +1107 -0
  168. package/dist-lib/lib/serve.js +307 -11
  169. package/dist-lib/lib/spawn.js +3 -2
  170. package/dist-lib/lib/tool-call-display.d.ts +1 -21
  171. package/dist-lib/lib/tools/exec/adapters/python/wheels.js +2 -1
  172. package/dist-lib/lib/tools/exec.d.ts +2 -1
  173. package/dist-lib/lib/tools/exec.js +57 -2
  174. package/dist-lib/lib/tools/generate-image.js +1 -1
  175. package/dist-lib/lib/tools/generate-music.js +1 -1
  176. package/dist-lib/lib/tools/http-request.d.ts +2 -1
  177. package/dist-lib/lib/tools/http-request.js +2 -1
  178. package/dist-lib/lib/tools/mcp-call.js +8 -1
  179. package/dist-lib/lib/tools/operations/builders.d.ts +1 -0
  180. package/dist-lib/lib/tools/operations/builders.js +1 -0
  181. package/dist-lib/lib/tools/operations/compile.js +11 -1
  182. package/dist-lib/lib/tools/operations/manifest.js +1 -0
  183. package/dist-lib/lib/tools/outbound-guard.d.ts +8 -6
  184. package/dist-lib/lib/tools/outbound-guard.js +25 -8
  185. package/dist-lib/lib/tools/skills/catalog.d.ts +1 -0
  186. package/dist-lib/lib/tools/skills/catalog.js +35 -0
  187. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +1 -0
  188. package/dist-lib/lib/tools/skills/cli-schema.js +25 -0
  189. package/dist-lib/lib/wire.d.ts +5 -0
  190. package/dist-lib/lib/wire.js +128 -5
  191. package/dist-lib/schema/harness.js +20 -5
  192. package/dist-lib/spec/hrpc/messages.js +95 -15
  193. package/dist-lib/spec/hyperschema/index.js +95 -15
  194. package/dist-lib/spec/hyperschema/schema.json +69 -1
  195. package/dist-lib/spec/hyperschema/types.d.ts +10 -3
  196. package/package.json +20 -2
  197. package/patches/@qvac%2Finference@0.20.0.patch +67 -0
  198. package/patches/bare-stow@0.1.5.patch +68 -0
  199. package/patches/hypercore-blob-server@1.15.0.patch +15 -0
  200. package/patches/localwatch@1.4.3.patch +13 -0
  201. package/patches/pyodide@314.0.2.patch +144 -0
package/dist-lib/index.js CHANGED
@@ -5,6 +5,9 @@ import ReadyResource from 'ready-resource';
5
5
  import { probeGpuStats } from './lib/hardware/gpu-probe.js';
6
6
  import { probeCapabilityProfile } from './lib/hardware/capability-probe.js';
7
7
  import { closePythonWorker } from './lib/tools/exec/adapters/python/worker/active.js';
8
+ import { trackCompletion } from './lib/completion-lifecycle.js';
9
+ import { createHarnessLifecycle } from './lib/harness-lifecycle.js';
10
+ import { didDrainTimeOut, raceDrain } from './lib/machine/index.js';
8
11
  import { completion, imageSettingsFrom } from './lib/capabilities/completion.js';
9
12
  import { detectLanguage } from './lib/capabilities/detect-language.js';
10
13
  import { invoke } from './lib/capabilities/invoke.js';
@@ -24,7 +27,7 @@ import { AbortedError, raceAbort } from './lib/helpers/race-abort.js';
24
27
  import { audioGenDownloadMetadata, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, curatedModelAsset, infraModelsFor, modelAddonOf, modelNameToCapabilityProfile, musicModelSet, residentSrc, setCustomModels } from './lib/models/model-catalog.js';
25
28
  import { createModelStatusCache, customModelRows, invalidateModelStatusCache, mergeCustomRows, listModels } from './lib/models/model-list.js';
26
29
  import { deleteModelFiles, summarizeDelete } from './lib/models/model-delete.js';
27
- import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictIdleWhere, evictModel, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, loadingModels, settledModels, modelKey, modelOf, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
30
+ import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, ModelUnloadError, awaitModelCapacity, evictIdle, evictIdleWhere, evictModel, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, loadingModels, settledModels, modelKey, modelOf, releaseModel, resolveCtxSize, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
28
31
  import { defaultContextSize } from './lib/models/context.js';
29
32
  import { resolveMainGpu } from './lib/models/gpu-selection.js';
30
33
  import { createIdleReaper } from './lib/models/idle-unload.js';
@@ -32,6 +35,8 @@ import { openclCacheDir } from './lib/models/opencl-cache.js';
32
35
  import { assertEmbedDevice, createEmbedConfig } from './lib/models/embed-config.js';
33
36
  import { createGpuFaultState, isSdkGpuDeviceLostError, isSdkModelWedgedError } from './lib/models/sdk-faults.js';
34
37
  import { createSdkLoader } from './lib/sdk-loader.js';
38
+ import { installAbortControllerPolyfill } from './lib/capabilities/completion-engine/abort-polyfill.js';
39
+ import { prepareCompletionRuntime } from './lib/capabilities/completion-runtime.js';
35
40
  import { createEditImageTool } from './lib/tools/edit-image.js';
36
41
  import { toolCatalog } from './lib/tools/catalog.js';
37
42
  import { createDiffusionRuntime, createGenerateImageTool } from './lib/tools/generate-image.js';
@@ -41,9 +46,9 @@ import { ensureBundledSkillsDir } from './lib/tools/skills/bundled-dir.js';
41
46
  import { ensureSkillsOverlayDir } from './lib/tools/skills/overlay-dir.js';
42
47
  import { copyBaseForEdit, deleteOverlaySkill, listManagedSkills, readSkillFile, writeSkillFile } from './lib/tools/skills/overlay-crud.js';
43
48
  import { watchSkillsOverlay } from './lib/tools/skills/overlay-watch.js';
44
- import { errorMessage } from './lib/guards.js';
49
+ import { errorMessage, isRecord } from './lib/guards.js';
50
+ import { redactedRecoveryFields } from './lib/wire.js';
45
51
  import { hlog } from './lib/log.js';
46
- import { harness } from './spec/index.js';
47
52
  import { installResolutionDetector, resolutionDetail } from './lib/resolution-detector.js';
48
53
  import { probeBinVersions } from './lib/tools/skills/bin-check.js';
49
54
  import { bareRuntime } from './lib/tools/exec/bare-runtime.js';
@@ -73,8 +78,8 @@ export { serveHarness } from './lib/serve.js';
73
78
  export { hlog, setHarnessLog, setHarnessLogSink } from './lib/log.js';
74
79
  export { harnessHooks } from './lib/hooks/registry.js';
75
80
  export { EgressGuardHook, SkillCatalogGuardHook } from './lib/hooks/built-in/index.js';
76
- const HarnessState = harness.getEnum('@harness/state');
77
81
  const BIN_VERSION_PROBE_TTL_MS = 45_000;
82
+ const SUSPEND_SETTLE_MS = 500;
78
83
  const TRANSCRIBE_STREAM_MIN_BYTES = 16_000 * 2 * 60;
79
84
  const TRANSCRIPTION_MODEL_LABEL = 'transcription model';
80
85
  const DIFFUSION_EVICT_WAIT_MS = 15_000;
@@ -101,6 +106,7 @@ export class Harness extends ReadyResource {
101
106
  _models;
102
107
  _embedModels;
103
108
  _retiredEmbedModels;
109
+ _faultedEmbedKeys = new WeakMap();
104
110
  _embedLane = createEmbedLane();
105
111
  _embedFaults;
106
112
  _embedGpuLayers;
@@ -109,7 +115,23 @@ export class Harness extends ReadyResource {
109
115
  _ttsModels;
110
116
  _transcriptionModels;
111
117
  _voiceLane = createVoiceLane();
112
- _state;
118
+ _lifecycle = createHarnessLifecycle({
119
+ suspend: () => this._suspendRuntimes(),
120
+ resume: () => this._resumeRuntimes()
121
+ });
122
+ _detachedRuns = new Set();
123
+ _effectDrainWaiters = new Set();
124
+ _activeWork = 0;
125
+ _workDrainWaiters = new Set();
126
+ _runtimeDraining = false;
127
+ _runtimeUnloadFailed = false;
128
+ _poolUnloadFailed = false;
129
+ _poolCleanupFailed = false;
130
+ _failedModelUnloads = new Set();
131
+ _sdkClosed = false;
132
+ _runtimesReleased = false;
133
+ _closeRetryNeeded = false;
134
+ _closeRequested = false;
113
135
  _skillsDir;
114
136
  _modelsDir;
115
137
  _overlayWatcher = null;
@@ -134,6 +156,7 @@ export class Harness extends ReadyResource {
134
156
  _musicSdk;
135
157
  _translationSdk;
136
158
  _musicRuntime;
159
+ _completionRuns;
137
160
  _translationRuntime;
138
161
  _capabilityTools;
139
162
  _capability = null;
@@ -142,6 +165,7 @@ export class Harness extends ReadyResource {
142
165
  _idle;
143
166
  constructor({ tools = [], sdkLoader, skillsDir, modelsDir, projectionModel, imageModel, imageClamps, musicModel, musicClamps, llmDevice, llmGpuLayers, contextSize, kvCacheK, kvCacheV, embedDevice, embedGpuLayers, embedBatchSize, transcriptionDevice, completionModelCap, credentialStore, platform, gpuStats } = {}) {
144
167
  super();
168
+ installAbortControllerPolyfill();
145
169
  this._sdk = sdkLoader ?? createSdkLoader();
146
170
  this._llmDevice = llmDevice;
147
171
  this._gpuStats = gpuStats;
@@ -214,7 +238,10 @@ export class Harness extends ReadyResource {
214
238
  ...music,
215
239
  loads: this._loads,
216
240
  loadSdk: () => this._loadMusicSdk(),
217
- onProgress: (model, progress) => this.emit('model', progressState(model, progress)),
241
+ onProgress: (model, progress) => {
242
+ const state = progressState(model, progress);
243
+ this.emit('model', state);
244
+ },
218
245
  onBeforeLoad: async (signal) => {
219
246
  if (this._models.destroyed)
220
247
  return;
@@ -253,7 +280,6 @@ export class Harness extends ReadyResource {
253
280
  ...(this._musicRuntime ? [createGenerateMusicTool(this._musicRuntime, musicClamps)] : [])
254
281
  ];
255
282
  this._projectionModel = projectionModel ?? defaultProjectionMap();
256
- this._state = HarnessState.RESUME;
257
283
  this._skillsDir = skillsDir;
258
284
  this._modelsDir = modelsDir;
259
285
  this._supportedSkills = [];
@@ -262,6 +288,7 @@ export class Harness extends ReadyResource {
262
288
  this._platform = platform;
263
289
  this._diffusionSdk = null;
264
290
  this._musicSdk = null;
291
+ this._completionRuns = new Set();
265
292
  this._translationSdk = null;
266
293
  this._models = this._createPool();
267
294
  this._embedModels = this._createEmbedPool();
@@ -274,13 +301,18 @@ export class Harness extends ReadyResource {
274
301
  });
275
302
  }
276
303
  async _open() {
277
- installResolutionDetector(import.meta);
278
- await this._reloadSkills();
279
- await this._startOverlayWatch();
280
- hlog('open', { skills: this._supportedSkills.length, tools: this._tools.length });
304
+ try {
305
+ installResolutionDetector(import.meta);
306
+ await this._reloadSkills();
307
+ await this._startOverlayWatch();
308
+ hlog('open', { skills: this._supportedSkills.length, tools: this._tools.length });
309
+ }
310
+ catch (error) {
311
+ throw error;
312
+ }
281
313
  }
282
314
  get suspended() {
283
- return this._state === HarnessState.SUSPEND;
315
+ return this._lifecycle.suspended;
284
316
  }
285
317
  get skillNames() {
286
318
  return this._supportedSkills.map((skill) => skill.name);
@@ -393,6 +425,7 @@ export class Harness extends ReadyResource {
393
425
  async skills(agentId, modelName, options) {
394
426
  if (!this.opened)
395
427
  await this.ready();
428
+ this._assertNotClosing();
396
429
  const platform = this._platform ?? detectPlatform();
397
430
  const refresh = options?.refresh === true;
398
431
  return resolveSkillStatuses(this._allSkills, {
@@ -641,19 +674,24 @@ export class Harness extends ReadyResource {
641
674
  };
642
675
  }
643
676
  _createPool() {
644
- const loadSdk = this._sdk.load;
677
+ const loadSdk = () => this._loadModelLifecycleSdk();
645
678
  return createModelPool(loadSdk, (state) => this._notifyModel(state), {
646
679
  deviceConfig: this._deviceConfig(),
647
- loads: this._loads
680
+ loads: this._loads,
681
+ onUnloadFailure: (modelId, error) => this._recordModelUnloadFailure(modelId, error)
648
682
  });
649
683
  }
684
+ _loadModelLifecycleSdk() {
685
+ return this._sdk.load();
686
+ }
650
687
  _createEmbedPool() {
651
688
  const loadSdk = () => this._sdk.loadEmbed();
652
689
  return createEmbedPool(loadSdk, (state) => this._notifyModel(state), {
653
690
  faults: this._embedFaults,
654
691
  batchSize: this._embedBatchSize,
655
692
  openclCacheDir,
656
- loads: this._loads
693
+ loads: this._loads,
694
+ onUnloadFailure: (modelId, error) => this._recordModelUnloadFailure(modelId, error)
657
695
  });
658
696
  }
659
697
  _createTtsPool() {
@@ -662,7 +700,8 @@ export class Harness extends ReadyResource {
662
700
  modelType: 'tts-ggml',
663
701
  addon: 'tts',
664
702
  modelConfig: TTS_MODEL_CONFIG,
665
- loads: this._loads
703
+ loads: this._loads,
704
+ onUnloadFailure: (modelId, error) => this._recordModelUnloadFailure(modelId, error)
666
705
  });
667
706
  }
668
707
  _createTranscriptionPool() {
@@ -672,9 +711,16 @@ export class Harness extends ReadyResource {
672
711
  addon: 'parakeet',
673
712
  modelConfig: { useGPU: this._transcriptionDevice !== 'cpu' },
674
713
  openclCacheDir,
675
- loads: this._loads
714
+ loads: this._loads,
715
+ onUnloadFailure: (modelId, error) => this._recordModelUnloadFailure(modelId, error)
676
716
  });
677
717
  }
718
+ _recordModelUnloadFailure(modelId, error) {
719
+ this._failedModelUnloads.add(modelId);
720
+ this._poolUnloadFailed = true;
721
+ this._runtimeUnloadFailed = true;
722
+ hlog('model unload failed', { modelId, err: errorMessage(error) });
723
+ }
678
724
  _notifyModel(state) {
679
725
  if (state.status === 'ready' || state.status === 'unloaded' || state.status === 'error') {
680
726
  invalidateModelStatusCache(this._modelStatus);
@@ -695,28 +741,166 @@ export class Harness extends ReadyResource {
695
741
  async _collectRetiredEmbedPool(pool) {
696
742
  if (!this._retiredEmbedModels.has(pool) || pool.idle !== pool.size)
697
743
  return;
744
+ try {
745
+ await pool.destroy();
746
+ }
747
+ catch (error) {
748
+ if (!(error instanceof ModelUnloadError)) {
749
+ this._poolCleanupFailed = true;
750
+ this._poolUnloadFailed = true;
751
+ this._runtimeUnloadFailed = true;
752
+ }
753
+ throw error;
754
+ }
698
755
  this._retiredEmbedModels.delete(pool);
699
- await pool.destroy();
700
756
  }
701
757
  async _releaseEmbedResource(pool, key) {
702
758
  releaseModel(pool, key);
759
+ const faulted = this._faultedEmbedKeys.get(pool);
760
+ if (faulted?.has(key)) {
761
+ await evictIdle(pool);
762
+ if (!isResident(pool, key)) {
763
+ faulted.delete(key);
764
+ }
765
+ }
703
766
  await this._collectRetiredEmbedPool(pool);
704
767
  }
705
- _releaseEmbedResourceAfter(settled, pool, key) {
706
- settled
707
- .then(() => this._releaseEmbedResource(pool, key), () => this._releaseEmbedResource(pool, key))
768
+ async _releaseEmbedResourceBeforeWork(releasing, releaseWork) {
769
+ try {
770
+ await releasing;
771
+ }
772
+ finally {
773
+ releaseWork();
774
+ }
775
+ }
776
+ _markEmbedFault({ pool, key, faults, generation }) {
777
+ faults.recordFault(generation);
778
+ let faulted = this._faultedEmbedKeys.get(pool);
779
+ if (!faulted) {
780
+ faulted = new Set();
781
+ this._faultedEmbedKeys.set(pool, faulted);
782
+ }
783
+ faulted.add(key);
784
+ }
785
+ _releaseEmbedResourceAfter(settled, fault) {
786
+ return settled
787
+ .then(() => this._releaseEmbedResource(fault.pool, fault.key), (error) => {
788
+ if (isSdkGpuDeviceLostError(error)) {
789
+ this._markEmbedFault(fault);
790
+ }
791
+ return this._releaseEmbedResource(fault.pool, fault.key);
792
+ })
708
793
  .catch(noop);
709
794
  }
710
795
  async _destroyEmbedPools() {
711
- const pools = [this._embedModels, ...this._retiredEmbedModels];
712
- this._retiredEmbedModels.clear();
713
- await Promise.all(pools.map((pool) => pool.destroy()));
796
+ const pools = [this._embedModels, ...this._retiredEmbedModels].filter((pool) => Boolean(pool));
797
+ const outcomes = await Promise.allSettled(pools.map(async (pool) => {
798
+ await pool.destroy();
799
+ this._retiredEmbedModels.delete(pool);
800
+ }));
801
+ const generic = outcomes.find((outcome) => outcome.status === 'rejected' && !(outcome.reason instanceof ModelUnloadError));
802
+ if (generic?.status === 'rejected') {
803
+ throw generic.reason;
804
+ }
805
+ const failed = outcomes.find((outcome) => outcome.status === 'rejected');
806
+ if (failed?.status === 'rejected') {
807
+ throw failed.reason;
808
+ }
714
809
  }
715
810
  _enqueueEmbedControl(run) {
716
811
  const next = this._embedControl.then(run, run);
717
812
  this._embedControl = next.catch(noop);
718
813
  return next;
719
814
  }
815
+ async _runLifecycle(type, { open = true } = {}) {
816
+ if (!this.opened) {
817
+ if (open) {
818
+ await this.ready();
819
+ }
820
+ }
821
+ if (this._closeRequested || this.closing) {
822
+ return;
823
+ }
824
+ await this._lifecycle.send(type);
825
+ }
826
+ _abortCompletions() {
827
+ for (const run of this._completionRuns) {
828
+ run.abort();
829
+ }
830
+ }
831
+ _assertNotClosing() {
832
+ if (this._closeRequested || this.closing || this.closed) {
833
+ throw new Error('harness is closing');
834
+ }
835
+ }
836
+ _enterWork() {
837
+ this._assertNotClosing();
838
+ if (this._runtimeDraining) {
839
+ throw new Error('harness is suspended');
840
+ }
841
+ if (this._runtimeUnloadFailed) {
842
+ throw new Error('Runtime unload failed; close the harness before loading models again');
843
+ }
844
+ this._activeWork++;
845
+ let released = false;
846
+ return () => {
847
+ if (released)
848
+ return;
849
+ released = true;
850
+ if (--this._activeWork !== 0) {
851
+ return;
852
+ }
853
+ for (const resolve of this._workDrainWaiters) {
854
+ resolve();
855
+ }
856
+ this._workDrainWaiters.clear();
857
+ };
858
+ }
859
+ _waitForWorkDrain() {
860
+ if (!this._activeWork) {
861
+ return Promise.resolve();
862
+ }
863
+ return new Promise((resolve) => this._workDrainWaiters.add(resolve));
864
+ }
865
+ _effectsSettled() {
866
+ return !this._detachedRuns.size && ![...this._completionRuns].some((run) => run.started);
867
+ }
868
+ _notifyEffectDrain() {
869
+ if (!this._effectsSettled())
870
+ return;
871
+ for (const resolve of this._effectDrainWaiters) {
872
+ resolve();
873
+ }
874
+ this._effectDrainWaiters.clear();
875
+ }
876
+ _waitForEffectDrain() {
877
+ if (this._effectsSettled()) {
878
+ return Promise.resolve();
879
+ }
880
+ return new Promise((resolve) => this._effectDrainWaiters.add(resolve));
881
+ }
882
+ _trackDetachedRun(pending, runId) {
883
+ const tracked = pending.catch((error) => {
884
+ hlog('detached completion effect failed', { runId, err: errorMessage(error) });
885
+ });
886
+ this._detachedRuns.add(tracked);
887
+ void tracked
888
+ .finally(() => {
889
+ this._detachedRuns.delete(tracked);
890
+ this._notifyEffectDrain();
891
+ })
892
+ .catch(noop);
893
+ }
894
+ async _closeCompletions(runs = this._completionRuns) {
895
+ const closed = await Promise.allSettled([...runs].map((run) => run.close()));
896
+ for (const outcome of closed) {
897
+ if (outcome.status === 'rejected') {
898
+ hlog('completion close failed', {
899
+ err: errorMessage(outcome.reason)
900
+ });
901
+ }
902
+ }
903
+ }
720
904
  getEmbeddingDevice() {
721
905
  return this._embedFaults.device;
722
906
  }
@@ -727,43 +911,61 @@ export class Harness extends ReadyResource {
727
911
  async _setEmbeddingDevice(device) {
728
912
  if (!this.opened)
729
913
  await this.ready();
730
- if (this.closing)
731
- throw new Error('harness is closing');
732
- if (this._embedFaults.device === device) {
733
- return;
734
- }
735
- this._embedFaults = createGpuFaultState({ device, gpuLayers: this._embedGpuLayers });
736
914
  if (this.suspended) {
915
+ this._assertNotClosing();
916
+ if (this._embedFaults.device === device) {
917
+ return;
918
+ }
919
+ this._embedFaults = createGpuFaultState({ device, gpuLayers: this._embedGpuLayers });
737
920
  return;
738
921
  }
739
- const retired = this._embedModels;
740
- this._retiredEmbedModels.add(retired);
741
- this._embedModels = this._createEmbedPool();
742
- await this._collectRetiredEmbedPool(retired);
922
+ const releaseWork = this._enterWork();
923
+ try {
924
+ if (this._embedFaults.device === device) {
925
+ return;
926
+ }
927
+ this._embedFaults = createGpuFaultState({ device, gpuLayers: this._embedGpuLayers });
928
+ if (this.suspended) {
929
+ return;
930
+ }
931
+ const retired = this._embedModels;
932
+ this._retiredEmbedModels.add(retired);
933
+ this._embedModels = this._createEmbedPool();
934
+ await this._collectRetiredEmbedPool(retired);
935
+ }
936
+ finally {
937
+ releaseWork();
938
+ }
743
939
  }
744
940
  async presuspend() {
745
- if (this.closing)
746
- return;
747
- this._state = HarnessState.PRESUSPEND;
748
- hlog('presuspend');
941
+ await this._runLifecycle('PRESUSPEND', { open: false });
749
942
  }
750
943
  async suspend() {
751
- hlog('suspend called');
752
- if (this._state === HarnessState.SUSPEND) {
753
- return;
944
+ const result = await raceDrain(this._runLifecycle('SUSPEND'), SUSPEND_SETTLE_MS);
945
+ if (didDrainTimeOut(result)) {
946
+ throw new Error('Harness suspend is still waiting for active work to settle');
754
947
  }
755
- if (this.closing) {
756
- return;
757
- }
758
- this._state = HarnessState.SUSPEND;
948
+ }
949
+ async _suspendRuntimes() {
950
+ this._runtimeDraining = true;
759
951
  hlog('suspending', { models: loadedModels(this._models).map((entry) => entry.model) });
952
+ this._abortCompletions();
953
+ this._loads.drain();
954
+ await this._closeCompletions([...this._completionRuns].filter((run) => run.started));
955
+ await this._waitForEffectDrain();
760
956
  const pending = this._sdk.peek();
761
957
  if (pending) {
762
- this._loads.drain(await pending);
958
+ const sdk = await pending.catch(noop);
959
+ if (sdk) {
960
+ this._loads.drain(sdk);
961
+ }
962
+ }
963
+ await this._waitForWorkDrain();
964
+ if (!(await this._releaseRuntimes())) {
965
+ throw new Error('Runtime unload failed; close the harness before loading models again');
763
966
  }
764
- await this._releaseRuntimes();
765
- await this._sdkLifecycle('suspend');
766
967
  invalidateModelStatusCache(this._modelStatus);
968
+ await this._sdkLifecycle('suspend');
767
969
  hlog('suspended');
768
970
  }
769
971
  async _sdkLifecycle(op) {
@@ -777,35 +979,60 @@ export class Harness extends ReadyResource {
777
979
  catch (err) {
778
980
  hlog(`sdk ${op} failed`, { err: errorMessage(err) });
779
981
  if (op === 'suspend') {
780
- return;
982
+ throw err;
781
983
  }
782
984
  await sdk.resume().catch((retryErr) => {
783
985
  hlog('sdk resume retry failed', {
784
986
  err: errorMessage(retryErr)
785
987
  });
988
+ throw retryErr;
786
989
  });
787
990
  }
788
991
  }
789
992
  async _releaseRuntimes() {
790
993
  const released = await Promise.allSettled([
791
- this._models.destroy(),
994
+ this._models?.destroy(),
792
995
  this._destroyEmbedPools(),
793
- this._ttsModels.destroy(),
794
- this._transcriptionModels.destroy(),
996
+ this._ttsModels?.destroy(),
997
+ this._transcriptionModels?.destroy(),
795
998
  this._imageRuntime?.unload(),
796
999
  this._musicRuntime?.unload(),
797
1000
  this._translationRuntime.unload()
798
1001
  ]);
799
- for (const outcome of released) {
1002
+ for (const [index, outcome] of released.entries()) {
800
1003
  if (outcome.status !== 'rejected')
801
1004
  continue;
1005
+ if (index < 4 && !(outcome.reason instanceof ModelUnloadError)) {
1006
+ this._poolCleanupFailed = true;
1007
+ }
802
1008
  const err = outcome.reason;
803
1009
  hlog('unload failed', { err: errorMessage(err) });
804
1010
  }
1011
+ const sdk = await this._sdk.peek()?.catch(noop);
1012
+ if (sdk) {
1013
+ for (const modelId of this._failedModelUnloads) {
1014
+ try {
1015
+ await sdk.unloadModel({ modelId });
1016
+ this._failedModelUnloads.delete(modelId);
1017
+ invalidateModelStatusCache(this._modelStatus);
1018
+ }
1019
+ catch (error) {
1020
+ hlog('model unload retry failed', {
1021
+ modelId,
1022
+ err: errorMessage(error)
1023
+ });
1024
+ }
1025
+ }
1026
+ }
1027
+ this._poolUnloadFailed = this._poolCleanupFailed || this._failedModelUnloads.size > 0;
1028
+ this._runtimeUnloadFailed =
1029
+ this._poolUnloadFailed || released.slice(4).some((outcome) => outcome.status === 'rejected');
1030
+ return !this._runtimeUnloadFailed;
805
1031
  }
806
1032
  async _unloadIdleModels() {
807
- if (!this.opened || this.closing || this.suspended)
1033
+ if (!this.opened || this.closing || this.suspended || this._runtimeDraining) {
808
1034
  return;
1035
+ }
809
1036
  hlog('idle unload', { models: this._loadedModels().map((entry) => entry.model) });
810
1037
  const released = await Promise.allSettled([
811
1038
  evictIdle(this._models),
@@ -825,62 +1052,164 @@ export class Harness extends ReadyResource {
825
1052
  invalidateModelStatusCache(this._modelStatus);
826
1053
  }
827
1054
  async resume() {
828
- hlog('resume called');
829
- if (this._state === HarnessState.RESUME) {
830
- return;
831
- }
832
- if (this.closing) {
833
- return;
834
- }
835
- if (this._state === HarnessState.PRESUSPEND) {
836
- this._state = HarnessState.RESUME;
837
- return;
1055
+ await this._runLifecycle('RESUME');
1056
+ }
1057
+ async _resumeRuntimes() {
1058
+ this._assertNotClosing();
1059
+ if (this._runtimeUnloadFailed) {
1060
+ throw new Error('Runtime unload failed; close the harness before loading models again');
838
1061
  }
839
- this._state = HarnessState.RESUME;
840
1062
  hlog('resuming', { pools: 'recreated' });
841
1063
  this._loads.undrain();
842
1064
  this._models = this._createPool();
843
1065
  this._embedModels = this._createEmbedPool();
844
1066
  this._ttsModels = this._createTtsPool();
845
1067
  this._transcriptionModels = this._createTranscriptionPool();
846
- await this._sdkLifecycle('resume');
1068
+ try {
1069
+ await this._sdkLifecycle('resume');
1070
+ this._runtimeDraining = false;
1071
+ }
1072
+ catch (error) {
1073
+ const sdk = await this._sdk.peek()?.catch(noop);
1074
+ if (sdk) {
1075
+ this._loads.drain(sdk);
1076
+ }
1077
+ await this._releaseRuntimes();
1078
+ throw error;
1079
+ }
847
1080
  }
848
1081
  async wakeup() {
849
- if (this.closing)
850
- return;
851
- this._state = HarnessState.WAKEUP;
852
- hlog('wakeup');
853
- await this.resume();
1082
+ await this._runLifecycle('WAKEUP');
1083
+ }
1084
+ ready() {
1085
+ if (this._closeRequested || this.closing || this.closed) {
1086
+ return Promise.reject(new Error('harness is closing'));
1087
+ }
1088
+ return super.ready();
854
1089
  }
855
1090
  async _close() {
856
1091
  hlog('closing');
857
1092
  this._idle.close();
858
- this._overlayWatcher?.stop();
859
- this._overlayWatcher = null;
860
- const pending = this._sdk.peek();
861
- if (pending) {
862
- this._loads.drain(await pending);
1093
+ this._lifecycle.cancelPending();
1094
+ this._abortCompletions();
1095
+ this._loads.drain();
1096
+ await this._lifecycle.settled();
1097
+ await this._finishClose();
1098
+ }
1099
+ close() {
1100
+ this._closeRequested = true;
1101
+ if (this._closeRetryNeeded) {
1102
+ this.closing = null;
1103
+ this._closeRetryNeeded = false;
1104
+ }
1105
+ const pending = !this.opened && this.opening !== null ? this._closeWhileOpening() : super.close();
1106
+ void pending.catch(() => {
1107
+ if (this.closing === pending) {
1108
+ this._closeRetryNeeded = true;
1109
+ }
1110
+ });
1111
+ const bounded = raceDrain(pending).then((result) => {
1112
+ if (didDrainTimeOut(result)) {
1113
+ throw new Error('Harness close is still waiting for active work to settle');
1114
+ }
1115
+ });
1116
+ void bounded.catch(noop);
1117
+ return bounded;
1118
+ }
1119
+ _closeWhileOpening() {
1120
+ if (this.closing)
1121
+ return this.closing;
1122
+ const opening = this.opening;
1123
+ const pending = (async () => {
1124
+ await opening?.catch(noop);
1125
+ this._abortCompletions();
1126
+ this._loads.drain();
1127
+ await this._finishClose();
1128
+ this.closed = true;
1129
+ this.emit('close');
1130
+ })();
1131
+ this.closing = pending;
1132
+ return pending;
1133
+ }
1134
+ async _finishClose() {
1135
+ let completed = false;
1136
+ try {
1137
+ const pending = this._sdk.peek();
1138
+ if (pending) {
1139
+ const sdk = await pending.catch(noop);
1140
+ if (sdk) {
1141
+ this._loads.drain(sdk);
1142
+ }
1143
+ }
1144
+ await this._closeCompletions();
1145
+ await this._waitForEffectDrain();
1146
+ await this._waitForWorkDrain();
1147
+ this._overlayWatcher?.stop();
1148
+ this._overlayWatcher = null;
1149
+ if (!this._runtimesReleased) {
1150
+ let [released] = await Promise.all([this._releaseRuntimes(), closePythonWorker()]);
1151
+ if (!released && !this._poolUnloadFailed) {
1152
+ released = await this._releaseRuntimes();
1153
+ }
1154
+ this._runtimesReleased = released;
1155
+ }
1156
+ let sdkCloseFailed = false;
1157
+ let sdkCloseError;
1158
+ const sdk = !this._sdkClosed && !this._runtimeUnloadFailed ? await this._sdk.peek()?.catch(noop) : null;
1159
+ if (sdk) {
1160
+ try {
1161
+ await sdk.close();
1162
+ this._sdkClosed = true;
1163
+ }
1164
+ catch (error) {
1165
+ sdkCloseFailed = true;
1166
+ sdkCloseError = error;
1167
+ hlog('sdk close failed', { err: errorMessage(error) });
1168
+ }
1169
+ }
1170
+ if (sdkCloseFailed) {
1171
+ throw sdkCloseError;
1172
+ }
1173
+ if (this._runtimeUnloadFailed) {
1174
+ throw new Error('Runtime unload failed; model cleanup could not be confirmed');
1175
+ }
1176
+ completed = true;
863
1177
  }
864
- await Promise.all([this._releaseRuntimes(), closePythonWorker()]);
865
- const sdk = await this._sdk.peek()?.catch(noop);
866
- if (sdk) {
867
- await sdk.close();
1178
+ finally {
1179
+ if (completed) {
1180
+ this._lifecycle.stop();
1181
+ }
868
1182
  }
869
1183
  }
870
1184
  download(args) {
1185
+ this._assertNotClosing();
1186
+ let releaseWork = null;
871
1187
  const projection = (args.addon ?? 'llm') === 'llm' ? this._projectionFor(args.model) : undefined;
872
1188
  const multiFile = diffusionDownloadMetadata(args.model, args.addon) ??
873
1189
  audioGenDownloadMetadata(args.model, args.addon);
874
1190
  return download(async () => {
875
1191
  if (!this.opened)
876
1192
  await this.ready();
877
- return this._sdk.load();
1193
+ releaseWork = this._enterWork();
1194
+ try {
1195
+ const sdk = await this._sdk.load();
1196
+ this._assertNotClosing();
1197
+ return sdk;
1198
+ }
1199
+ catch (error) {
1200
+ releaseWork?.();
1201
+ releaseWork = null;
1202
+ throw error;
1203
+ }
878
1204
  }, {
879
1205
  ...args,
880
1206
  projection,
881
1207
  companions: multiFile?.companions,
882
1208
  assetSizes: multiFile?.assetSizes
883
- }, () => invalidateModelStatusCache(this._modelStatus));
1209
+ }, () => invalidateModelStatusCache(this._modelStatus), () => {
1210
+ releaseWork?.();
1211
+ releaseWork = null;
1212
+ });
884
1213
  }
885
1214
  embed(context) {
886
1215
  return this._busy(() => this._embed(context));
@@ -888,8 +1217,10 @@ export class Harness extends ReadyResource {
888
1217
  async _embed({ model, texts, signal }) {
889
1218
  if (!this.opened)
890
1219
  await this.ready();
891
- if (this.suspended)
1220
+ this._assertNotClosing();
1221
+ if (this.suspended) {
892
1222
  throw new Error('harness is suspended');
1223
+ }
893
1224
  if (texts.length === 0) {
894
1225
  return [];
895
1226
  }
@@ -897,13 +1228,23 @@ export class Harness extends ReadyResource {
897
1228
  const faults = this._embedFaults;
898
1229
  const generation = faults.generation;
899
1230
  const key = faults.key(residentSrc(model, 'embeddings'));
900
- const handle = pool.open(key, model);
1231
+ const releaseWork = this._enterWork();
1232
+ let handle;
1233
+ try {
1234
+ handle = pool.open(key, model);
1235
+ }
1236
+ catch (error) {
1237
+ releaseWork();
1238
+ throw error;
1239
+ }
901
1240
  let loaded = null;
902
1241
  let job = null;
903
1242
  let releaseAfter = null;
1243
+ let drainWorkAfter = null;
904
1244
  try {
905
1245
  loaded = await raceAbort(handle.load(), signal);
906
1246
  const sdk = await this._sdk.loadEmbed();
1247
+ this._assertNotClosing();
907
1248
  job = this._embedLane(sdk, { modelId: loaded.modelId, texts });
908
1249
  const vectors = await raceAbort(job, signal);
909
1250
  faults.reset();
@@ -911,26 +1252,35 @@ export class Harness extends ReadyResource {
911
1252
  }
912
1253
  catch (err) {
913
1254
  if (err instanceof AbortedError) {
914
- releaseAfter = job ?? (this._retiredEmbedModels.has(pool) ? handle.peek() : null);
1255
+ if (job || this._retiredEmbedModels.has(pool)) {
1256
+ releaseAfter = job ?? handle.peek();
1257
+ }
1258
+ else {
1259
+ drainWorkAfter = handle.peek();
1260
+ }
915
1261
  }
916
1262
  if (isSdkGpuDeviceLostError(err)) {
917
- faults.recordFault(generation);
918
- if (loaded) {
919
- const dead = loaded.modelId;
920
- await this._sdk
921
- .loadEmbed()
922
- .then((sdk) => sdk.unloadModel({ modelId: dead }))
923
- .catch(noop);
924
- }
1263
+ this._markEmbedFault({ pool, key, faults, generation });
925
1264
  }
926
1265
  throw err;
927
1266
  }
928
1267
  finally {
929
1268
  if (releaseAfter) {
930
- this._releaseEmbedResourceAfter(releaseAfter, pool, key);
1269
+ void this._releaseEmbedResourceAfter(releaseAfter, {
1270
+ pool,
1271
+ key,
1272
+ faults,
1273
+ generation
1274
+ }).finally(releaseWork);
931
1275
  }
932
1276
  else {
933
- await this._releaseEmbedResource(pool, key);
1277
+ const releasing = this._releaseEmbedResource(pool, key);
1278
+ if (drainWorkAfter) {
1279
+ void Promise.allSettled([releasing, drainWorkAfter]).then(releaseWork);
1280
+ }
1281
+ else {
1282
+ await this._releaseEmbedResourceBeforeWork(releasing, releaseWork);
1283
+ }
934
1284
  }
935
1285
  }
936
1286
  }
@@ -953,7 +1303,16 @@ export class Harness extends ReadyResource {
953
1303
  throw new Error('harness is suspended');
954
1304
  const addon = category === 'tts' ? 'tts' : 'parakeet';
955
1305
  const name = residentSrc(model || (await this._defaultVoiceSrc(category)), addon);
956
- const handle = pool.open(name, name);
1306
+ this._assertNotClosing();
1307
+ const releaseWork = this._enterWork();
1308
+ let handle;
1309
+ try {
1310
+ handle = pool.open(name, name);
1311
+ }
1312
+ catch (error) {
1313
+ releaseWork();
1314
+ throw error;
1315
+ }
957
1316
  let job = null;
958
1317
  let releaseAfter = null;
959
1318
  let sdk = null;
@@ -961,6 +1320,7 @@ export class Harness extends ReadyResource {
961
1320
  try {
962
1321
  modelId = (await raceAbort(handle.load(), signal)).modelId;
963
1322
  sdk = (await this._sdk.loadVoice());
1323
+ this._assertNotClosing();
964
1324
  const boundSdk = sdk;
965
1325
  const boundId = modelId;
966
1326
  job = this._voiceLane(() => run(boundSdk, boundId));
@@ -971,7 +1331,7 @@ export class Harness extends ReadyResource {
971
1331
  if (sdk && modelId) {
972
1332
  void sdk.cancel({ modelId, operation: 'inference' }).catch(noop);
973
1333
  }
974
- releaseAfter = job;
1334
+ releaseAfter = job ?? handle.peek();
975
1335
  }
976
1336
  throw err;
977
1337
  }
@@ -980,10 +1340,15 @@ export class Harness extends ReadyResource {
980
1340
  releaseModel(pool, name);
981
1341
  };
982
1342
  if (releaseAfter) {
983
- void releaseAfter.then(release, release);
1343
+ void releaseAfter.then(release, release).finally(releaseWork).catch(noop);
984
1344
  }
985
1345
  else {
986
- release();
1346
+ try {
1347
+ release();
1348
+ }
1349
+ finally {
1350
+ releaseWork();
1351
+ }
987
1352
  }
988
1353
  }
989
1354
  }
@@ -1021,9 +1386,18 @@ export class Harness extends ReadyResource {
1021
1386
  if (this.suspended)
1022
1387
  throw new Error('harness is suspended');
1023
1388
  const name = residentSrc(model || (await this._defaultVoiceSrc('tts')), 'tts');
1389
+ this._assertNotClosing();
1024
1390
  const pool = this._ttsModels;
1025
- const handle = pool.open(name, name);
1026
1391
  const chunks = pushQueue();
1392
+ const releaseWork = this._enterWork();
1393
+ let handle;
1394
+ try {
1395
+ handle = pool.open(name, name);
1396
+ }
1397
+ catch (error) {
1398
+ releaseWork();
1399
+ throw error;
1400
+ }
1027
1401
  let sdk = null;
1028
1402
  let modelId = null;
1029
1403
  const cancel = () => {
@@ -1036,6 +1410,7 @@ export class Harness extends ReadyResource {
1036
1410
  try {
1037
1411
  modelId = (await handle.load()).modelId;
1038
1412
  sdk = (await this._sdk.loadVoice());
1413
+ this._assertNotClosing();
1039
1414
  if (signal?.aborted) {
1040
1415
  return;
1041
1416
  }
@@ -1058,53 +1433,98 @@ export class Harness extends ReadyResource {
1058
1433
  if (!settled) {
1059
1434
  cancel();
1060
1435
  }
1061
- void job.catch(noop).finally(() => {
1062
- releaseModel(pool, name);
1063
- });
1436
+ void job
1437
+ .catch(noop)
1438
+ .finally(() => {
1439
+ try {
1440
+ releaseModel(pool, name);
1441
+ }
1442
+ finally {
1443
+ releaseWork();
1444
+ }
1445
+ })
1446
+ .catch(noop);
1064
1447
  }
1065
1448
  }
1066
1449
  async list(options = {}) {
1067
1450
  if (!this.opened)
1068
1451
  await this.ready();
1069
- const sdk = await this._sdk.load();
1070
- const rows = await listModels(sdk, options, {
1071
- cache: this._modelStatus,
1072
- projectionFor: (name) => this._projectionFor(name)
1073
- });
1074
- const loaded = this._settledModels();
1075
- const loading = this._loadingModels();
1076
- const custom = await customModelRows(options, { modelsDir: this._modelsDir, loaded });
1077
- return mergeCustomRows(rows, custom).map((row) => ({
1078
- ...row,
1079
- isLoading: loading.has(row.name)
1080
- }));
1452
+ const releaseWork = this._enterWork();
1453
+ try {
1454
+ const sdk = await this._sdk.load();
1455
+ this._assertNotClosing();
1456
+ const rows = await listModels(sdk, options, {
1457
+ cache: this._modelStatus,
1458
+ projectionFor: (name) => this._projectionFor(name)
1459
+ });
1460
+ const loaded = this._settledModels();
1461
+ const loading = this._loadingModels();
1462
+ const custom = await customModelRows(options, { modelsDir: this._modelsDir, loaded });
1463
+ return mergeCustomRows(rows, custom).map((row) => ({
1464
+ ...row,
1465
+ isLoading: loading.has(row.name)
1466
+ }));
1467
+ }
1468
+ finally {
1469
+ releaseWork();
1470
+ }
1081
1471
  }
1082
1472
  async deleteModel(name) {
1083
1473
  if (!this.opened)
1084
1474
  await this.ready();
1085
- const sdk = await this._sdk.load();
1086
- const result = await deleteModelFiles(sdk, name, { modelsDir: this._modelsDir });
1087
- invalidateModelStatusCache(this._modelStatus);
1088
- return summarizeDelete(result);
1475
+ const releaseWork = this._enterWork();
1476
+ try {
1477
+ const sdk = await this._sdk.load();
1478
+ this._assertNotClosing();
1479
+ const result = await deleteModelFiles(sdk, name, { modelsDir: this._modelsDir });
1480
+ invalidateModelStatusCache(this._modelStatus);
1481
+ return summarizeDelete(result);
1482
+ }
1483
+ finally {
1484
+ releaseWork();
1485
+ }
1089
1486
  }
1090
1487
  async clearKvCache() {
1091
1488
  if (!this.opened)
1092
1489
  await this.ready();
1093
- return deleteAllKvCaches(() => this._sdk.load());
1490
+ const releaseWork = this._enterWork();
1491
+ try {
1492
+ return await deleteAllKvCaches(() => {
1493
+ this._assertNotClosing();
1494
+ return this._sdk.load();
1495
+ });
1496
+ }
1497
+ finally {
1498
+ releaseWork();
1499
+ }
1094
1500
  }
1095
1501
  async deleteKvCache(kvCacheKey) {
1096
1502
  if (!this.opened)
1097
1503
  await this.ready();
1098
- return deleteKvCacheKey(() => this._sdk.load(), kvCacheKey);
1504
+ const releaseWork = this._enterWork();
1505
+ try {
1506
+ return await deleteKvCacheKey(() => {
1507
+ this._assertNotClosing();
1508
+ return this._sdk.load();
1509
+ }, kvCacheKey);
1510
+ }
1511
+ finally {
1512
+ releaseWork();
1513
+ }
1099
1514
  }
1100
1515
  async infraModels(profile) {
1101
1516
  return infraModelsFor(profile);
1102
1517
  }
1103
1518
  async capability() {
1104
- if (this._capability)
1519
+ if (!this.opened)
1520
+ await this.ready();
1521
+ this._assertNotClosing();
1522
+ if (this._capability) {
1105
1523
  return this._capability;
1106
- if (this._capabilityInflight)
1524
+ }
1525
+ if (this._capabilityInflight) {
1107
1526
  return this._capabilityInflight;
1527
+ }
1108
1528
  const reads = {
1109
1529
  totalmem,
1110
1530
  platform,
@@ -1130,6 +1550,7 @@ export class Harness extends ReadyResource {
1130
1550
  };
1131
1551
  this._capabilityInflight = probeCapabilityProfile(reads)
1132
1552
  .then((capability) => {
1553
+ capability = { ...capability, hostRunLog: true, hostRunLogVersion: 1 };
1133
1554
  this._capability = capability;
1134
1555
  return capability;
1135
1556
  })
@@ -1144,23 +1565,26 @@ export class Harness extends ReadyResource {
1144
1565
  async _warmModel({ model, agent, signal }) {
1145
1566
  if (!this.opened)
1146
1567
  await this.ready();
1147
- if (this.suspended)
1568
+ this._assertNotClosing();
1569
+ if (this.suspended) {
1148
1570
  return;
1571
+ }
1149
1572
  const target = this._residency(model, agent);
1150
- const resident = isResident(target.pool, target.key);
1151
- hlog('warm model', { model, resident });
1152
- if (target.pool === this._models && !resident) {
1573
+ hlog('warm model', { model, resident: isResident(target.pool, target.key) });
1574
+ await this._loadAndRelease(target, signal, async () => {
1575
+ if (target.pool !== this._models || isResident(target.pool, target.key)) {
1576
+ return true;
1577
+ }
1153
1578
  const roomed = await this._makeRoomForCompletion(model, {
1154
1579
  ctxSize: target.ctxSize,
1155
1580
  signal,
1156
1581
  deadlineMs: WARM_ROOM_WAIT_MS
1157
1582
  });
1158
- if (!roomed) {
1159
- return;
1583
+ if (roomed) {
1584
+ await this._unloadIdleGenerators();
1160
1585
  }
1161
- await this._unloadIdleGenerators();
1162
- }
1163
- await this._loadAndRelease(target, signal);
1586
+ return roomed;
1587
+ });
1164
1588
  }
1165
1589
  loadModel(context) {
1166
1590
  return this._busy(() => this._loadModel(context));
@@ -1214,13 +1638,9 @@ export class Harness extends ReadyResource {
1214
1638
  }
1215
1639
  return { unloaded };
1216
1640
  }
1217
- async _loadAndRelease(target, signal) {
1218
- try {
1219
- await raceAbort(target.open().load(), signal);
1220
- }
1221
- finally {
1222
- releaseModel(target.pool, target.key);
1223
- }
1641
+ async _loadAndRelease({ pool, key, open }, signal, prepare = async () => true) {
1642
+ const lease = await this._acquireModelLease({ pool, key, open, signal, prepare });
1643
+ lease?.release();
1224
1644
  }
1225
1645
  async _unloadIdleGenerators() {
1226
1646
  await this._imageRuntime?.unloadIdle().catch(noop);
@@ -1283,6 +1703,57 @@ export class Harness extends ReadyResource {
1283
1703
  projectionModel: this._projectionFor(model)
1284
1704
  }, this._deviceConfig());
1285
1705
  }
1706
+ async _acquireModelLease({ pool, key, open, signal, prepare }) {
1707
+ const releaseWork = this._enterWork();
1708
+ let pendingLoad = null;
1709
+ let opened = false;
1710
+ let released = false;
1711
+ const release = () => {
1712
+ if (released) {
1713
+ return;
1714
+ }
1715
+ released = true;
1716
+ try {
1717
+ if (opened) {
1718
+ releaseModel(pool, key);
1719
+ }
1720
+ }
1721
+ finally {
1722
+ releaseWork();
1723
+ }
1724
+ };
1725
+ try {
1726
+ if (!(await prepare())) {
1727
+ release();
1728
+ return null;
1729
+ }
1730
+ if (signal?.aborted) {
1731
+ throw new AbortedError();
1732
+ }
1733
+ if (this.suspended || this._runtimeDraining) {
1734
+ throw new Error('harness is suspended');
1735
+ }
1736
+ this._assertNotClosing();
1737
+ if (this._runtimeUnloadFailed) {
1738
+ throw new Error('Runtime unload failed; close the harness before loading models again');
1739
+ }
1740
+ const handle = open();
1741
+ opened = true;
1742
+ const loading = handle.load();
1743
+ pendingLoad = loading;
1744
+ const loaded = await raceAbort(loading, signal);
1745
+ return { modelId: loaded.modelId, release };
1746
+ }
1747
+ catch (error) {
1748
+ if (pendingLoad) {
1749
+ void pendingLoad.then(release, release).catch(noop);
1750
+ }
1751
+ else {
1752
+ release();
1753
+ }
1754
+ throw error;
1755
+ }
1756
+ }
1286
1757
  invoke(context) {
1287
1758
  return this._busyStream(() => this._invoke(context));
1288
1759
  }
@@ -1290,8 +1761,10 @@ export class Harness extends ReadyResource {
1290
1761
  if (!this.opened)
1291
1762
  await this.ready();
1292
1763
  if (this.suspended) {
1764
+ this._assertNotClosing();
1293
1765
  return yield { type: 'aborted' };
1294
1766
  }
1767
+ const releaseWork = this._enterWork();
1295
1768
  hlog('invoke start', { capability, enhance });
1296
1769
  try {
1297
1770
  yield* invoke({
@@ -1311,117 +1784,101 @@ export class Harness extends ReadyResource {
1311
1784
  }
1312
1785
  finally {
1313
1786
  hlog('invoke end', { capability });
1787
+ releaseWork();
1314
1788
  }
1315
1789
  }
1316
1790
  completion(context) {
1317
- return this._busyStream(() => this._completion(context));
1791
+ this._assertNotClosing();
1792
+ if (this._runtimeDraining) {
1793
+ throw new Error('harness is suspended');
1794
+ }
1795
+ if (this._runtimeUnloadFailed) {
1796
+ throw new Error('Runtime unload failed; close the harness before loading models again');
1797
+ }
1798
+ const tracked = trackCompletion((signal) => this._busyStream(() => this._runCompletion({ ...context, signal })), context.signal, (run) => {
1799
+ this._completionRuns.delete(run);
1800
+ this._notifyEffectDrain();
1801
+ });
1802
+ const { iterator, run } = tracked;
1803
+ this._completionRuns.add(run);
1804
+ return iterator;
1318
1805
  }
1319
- async *_completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
1806
+ async *_runCompletion(request) {
1320
1807
  if (!this.opened)
1321
1808
  await this.ready();
1322
- if (this.suspended)
1323
- throw new Error('harness is suspended');
1324
- const loadOpts = this._completionLoadOptions(model, agent);
1325
- const projectionModel = loadOpts.projectionModel;
1326
- const key = modelKey(model, loadOpts);
1327
- const pool = this._models;
1328
- const resident = isResident(pool, key);
1329
- hlog('completion start', { model, resident, projection: Boolean(projectionModel) });
1330
- if (!resident) {
1331
- await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
1332
- await this._unloadIdleGenerators();
1809
+ this._assertNotClosing();
1810
+ if (this.suspended || this._runtimeDraining) {
1811
+ yield { type: 'aborted' };
1812
+ return;
1333
1813
  }
1334
- const acquireRound = async () => {
1335
- if (!isResident(pool, key)) {
1336
- await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
1337
- await this._unloadIdleGenerators();
1338
- }
1339
- try {
1340
- const { modelId } = await raceAbort(pool.open(key, model, loadOpts).load(), signal);
1341
- return {
1342
- modelId,
1343
- release: () => {
1344
- releaseModel(pool, key);
1345
- }
1346
- };
1814
+ const { boot, onStep, hostAppendTimeoutMs, requestDigestKey, ...provided } = request;
1815
+ let modelId = request.model ?? '';
1816
+ let key = null;
1817
+ try {
1818
+ const runtime = async (run) => {
1819
+ modelId = run.model ?? run.modelId;
1820
+ const runContext = { ...run, model: run.model ?? run.modelId };
1821
+ const lease = await this._completionRuntime(runContext);
1822
+ if (!key) {
1823
+ this._replying.push(lease.key);
1824
+ }
1825
+ key = lease.key;
1826
+ return lease;
1827
+ };
1828
+ const args = { ...provided, lockOwner: this };
1829
+ for await (const event of completion(runtime, args, {
1830
+ boot,
1831
+ onStep,
1832
+ hostAppendTimeoutMs,
1833
+ requestDigestKey,
1834
+ resumeRequest: provided,
1835
+ onDetached: (pending, runId) => this._trackDetachedRun(pending, runId)
1836
+ })) {
1837
+ yield event;
1347
1838
  }
1348
- catch (err) {
1349
- releaseModel(pool, key);
1350
- throw err;
1839
+ if (key) {
1840
+ this._modelFaults.delete(modelId);
1351
1841
  }
1352
- };
1353
- const handle = pool.open(key, model, loadOpts);
1354
- let held = true;
1355
- const releaseHeld = () => {
1356
- if (!held)
1357
- return;
1358
- held = false;
1359
- releaseModel(pool, key);
1360
- };
1361
- this._replying.push(key);
1362
- try {
1363
- const { modelId, ctxSize } = await raceAbort(handle.load(), signal);
1364
- hlog('model ready', { model });
1365
- const sdk = await this._sdk.load();
1366
- const turns = projectionModel ? messages : messages.map(({ attachments, ...turn }) => turn);
1367
- const remoteRoots = driveOp && grantedDrives?.length ? createRemoteRoots(grantedDrives, driveOp) : [];
1368
- const fsRoots = [
1369
- ...(grantedDirs?.length ? await createLocalRoots(grantedDirs) : []),
1370
- ...remoteRoots
1371
- ];
1372
- const fsWriteRoots = [
1373
- ...(grantedWriteDirs?.length ? await createLocalRoots(grantedWriteDirs) : []),
1374
- ...remoteRoots.filter((root) => root.writable)
1375
- ];
1376
- releaseHeld();
1377
- yield* completion(sdk, {
1378
- modelId,
1379
- acquireRound,
1380
- model,
1381
- agent,
1382
- ctxSize,
1383
- cacheKey,
1384
- thinkingDefault,
1385
- generationParams,
1386
- responseFormat,
1387
- captureInferenceCalls,
1388
- messages: turns,
1389
- tools: this._tools,
1390
- skillCatalog: await this._completionSkills(agent?.id ?? null, model),
1391
- toolConfigs,
1392
- fsRoots,
1393
- fsWriteRoots,
1394
- approve,
1395
- attach,
1396
- fetchAttachment,
1397
- ragSearch,
1398
- progress,
1399
- askUser,
1400
- signal
1401
- });
1402
- this._modelFaults.delete(model);
1403
1842
  }
1404
- catch (err) {
1405
- if (err instanceof AbortedError || this.suspended) {
1406
- hlog('completion aborted', { model, suspended: this.suspended });
1407
- yield { type: 'aborted' };
1843
+ catch (error) {
1844
+ yield* this._completionFailed(error, modelId, key);
1845
+ }
1846
+ finally {
1847
+ if (key) {
1848
+ hlog('completion end', { model: modelId });
1849
+ this._replying.splice(this._replying.indexOf(key), 1);
1850
+ void this._imageRuntime?.unloadIdle().catch(noop);
1851
+ void this._musicRuntime?.unloadIdle().catch(noop);
1408
1852
  }
1409
- else {
1410
- const message = errorMessage(err);
1411
- hlog('completion failed', { model, ...resolutionDetail(err), err: message });
1412
- const wedged = isSdkModelWedgedError(err) ? await this._evictWedgedModel(model, key) : null;
1413
- yield { type: 'error', message: wedged ? wedged.message : message };
1414
- if (wedged?.fatal) {
1415
- process.exit(1);
1416
- }
1853
+ }
1854
+ }
1855
+ async *_completionFailed(err, model, key) {
1856
+ if (err instanceof AbortedError || this.suspended || this._runtimeDraining) {
1857
+ hlog('completion aborted', { model, suspended: this.suspended || this._runtimeDraining });
1858
+ yield { type: 'aborted' };
1859
+ return;
1860
+ }
1861
+ const message = errorMessage(err);
1862
+ const name = err instanceof Error ? err.name : undefined;
1863
+ hlog('completion failed', { model, ...resolutionDetail(err), err: message });
1864
+ if (key && isSdkModelWedgedError(err)) {
1865
+ const wedged = await this._evictWedgedModel(model, key);
1866
+ yield completionErrorEvent(wedged.message, name, err);
1867
+ if (wedged.fatal) {
1868
+ process.exit(1);
1417
1869
  }
1418
1870
  }
1871
+ else {
1872
+ yield completionErrorEvent(message, name, err);
1873
+ }
1874
+ }
1875
+ async _completionRuntime(request) {
1876
+ const releaseWork = this._enterWork();
1877
+ try {
1878
+ return await prepareCompletionRuntime(this, request);
1879
+ }
1419
1880
  finally {
1420
- hlog('completion end', { model });
1421
- this._replying.splice(this._replying.indexOf(key), 1);
1422
- releaseHeld();
1423
- void this._imageRuntime?.unloadIdle().catch(noop);
1424
- void this._musicRuntime?.unloadIdle().catch(noop);
1881
+ releaseWork();
1425
1882
  }
1426
1883
  }
1427
1884
  _residentEnhancer() {
@@ -1511,4 +1968,32 @@ export class Harness extends ReadyResource {
1511
1968
  }
1512
1969
  }
1513
1970
  }
1971
+ function completionErrorEvent(message, name, error) {
1972
+ const event = { type: 'error', message, ...completionErrorDetails(error) };
1973
+ if (name) {
1974
+ event.name = name;
1975
+ }
1976
+ return event;
1977
+ }
1978
+ function completionErrorDetails(error) {
1979
+ if (!isRecord(error))
1980
+ return {};
1981
+ if (error.code === 'PENDING_ARGS_REDACTED') {
1982
+ return { code: 'PENDING_ARGS_REDACTED', ...redactedRecoveryFields(error) };
1983
+ }
1984
+ if (error.name !== 'RunIncompatible') {
1985
+ return {};
1986
+ }
1987
+ const match = errorMessage(error).match(/^Incompatible run log: (machine|log|xstate) version (\d{1,10}(?:\.\d{1,10}){0,3}(?:-[A-Za-z0-9.-]{1,16})?) \(expected (\d{1,10}(?:\.\d{1,10}){0,3}(?:-[A-Za-z0-9.-]{1,16})?)\)/);
1988
+ if (!match) {
1989
+ return {};
1990
+ }
1991
+ const kind = match[1];
1992
+ const found = match[2];
1993
+ const expected = match[3];
1994
+ if ((kind !== 'machine' && kind !== 'log' && kind !== 'xstate') || !found || !expected) {
1995
+ return {};
1996
+ }
1997
+ return { kind, found, expected };
1998
+ }
1514
1999
  function noop() { }