arcane-os 0.2.2 → 0.3.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 (117) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
  4. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
  5. package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
  6. package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
  8. package/browser-runtime/ai/browser-speech.mjs +2 -0
  9. package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
  10. package/browser-runtime/ai/model-controller.mjs +285 -95
  11. package/browser-runtime/ai/speech-worker-client.mjs +247 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
  13. package/browser-runtime/event-manager.mjs +1097 -1
  14. package/docs/architecture.md +2 -2
  15. package/docs/event-manager.md +155 -27
  16. package/docs/reference/README.md +27 -27
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1162 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +6 -3
  21. package/docs/reference/behavioral-testing.md +29 -6
  22. package/docs/reference/cli.md +117 -9
  23. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  24. package/docs/reference/event-manager.md +577 -32
  25. package/docs/reference/inventory/package-api.json +478 -2
  26. package/docs/reference/inventory/runtime-components.json +108 -44
  27. package/docs/reference/inventory/runtime-modules.json +131 -53
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +157 -43
  30. package/docs/reference/runtime-components.md +258 -83
  31. package/docs/reference/runtime-modules.md +613 -77
  32. package/docs/reference/sdk-api.md +1014 -25
  33. package/package.json +5 -4
  34. package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
  35. package/runtime/arcane/components/app-bar.html +34 -13
  36. package/runtime/arcane/components/assistant-panel.html +110 -57
  37. package/runtime/arcane/components/calculator.html +7 -4
  38. package/runtime/arcane/components/chart.html +58 -17
  39. package/runtime/arcane/components/chat.html +606 -136
  40. package/runtime/arcane/components/conversation-view.html +13 -6
  41. package/runtime/arcane/components/dashboard-config.html +96 -59
  42. package/runtime/arcane/components/data-maintenance.html +69 -14
  43. package/runtime/arcane/components/data-view.html +53 -7
  44. package/runtime/arcane/components/directory-picker.html +118 -32
  45. package/runtime/arcane/components/document-inspector.html +47 -10
  46. package/runtime/arcane/components/file-drop.html +81 -35
  47. package/runtime/arcane/components/file-inspector.html +72 -22
  48. package/runtime/arcane/components/file-manager.html +374 -79
  49. package/runtime/arcane/components/integration-settings.html +12 -5
  50. package/runtime/arcane/components/local-ai-status.html +48 -19
  51. package/runtime/arcane/components/markdown-document.html +161 -68
  52. package/runtime/arcane/components/markdown-editor.html +110 -33
  53. package/runtime/arcane/components/media-embed.html +8 -5
  54. package/runtime/arcane/components/modal.html +15 -5
  55. package/runtime/arcane/components/output-panel.html +28 -23
  56. package/runtime/arcane/components/preferences-form.html +22 -4
  57. package/runtime/arcane/components/record-timeline.html +18 -2
  58. package/runtime/arcane/components/relationship-board.html +23 -3
  59. package/runtime/arcane/components/screen-capture.html +10 -4
  60. package/runtime/arcane/components/source-code-viewer.html +76 -9
  61. package/runtime/arcane/components/source-explanation.html +23 -3
  62. package/runtime/arcane/components/speech.html +462 -384
  63. package/runtime/arcane/components/summary-strip.html +22 -11
  64. package/runtime/arcane/components/table.html +39 -21
  65. package/runtime/arcane/components/task-progress.html +79 -21
  66. package/runtime/arcane/components/terminal-workspace.html +7 -4
  67. package/runtime/arcane/components/theme-editor.html +7 -3
  68. package/runtime/arcane/components/unified-inbox.html +9 -4
  69. package/runtime/arcane/components/voice-transcription.html +639 -98
  70. package/runtime/arcane/components/weather-widget.html +5 -3
  71. package/runtime/arcane/components/web-navigator.html +48 -8
  72. package/runtime/arcane/entities/Chat.js +1 -1
  73. package/runtime/arcane/entities/User.js +110 -23
  74. package/runtime/arcane/modules/AI.js +2109 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
  76. package/runtime/arcane/modules/AIRuntimeState.js +109 -52
  77. package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
  78. package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
  79. package/runtime/arcane/modules/CalculatorEngine.js +63 -3
  80. package/runtime/arcane/modules/CommunicationAppController.js +588 -28
  81. package/runtime/arcane/modules/CommunicationHub.js +590 -10
  82. package/runtime/arcane/modules/ComponentContracts.js +470 -0
  83. package/runtime/arcane/modules/ConversationTimebox.js +152 -33
  84. package/runtime/arcane/modules/DBLS.js +40 -7
  85. package/runtime/arcane/modules/DBOPFS.js +35 -11
  86. package/runtime/arcane/modules/DataMaintenance.js +12 -2
  87. package/runtime/arcane/modules/Errors.js +65 -7
  88. package/runtime/arcane/modules/HTMLImport.js +198 -14
  89. package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
  90. package/runtime/arcane/modules/Mail.js +738 -115
  91. package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
  92. package/runtime/arcane/modules/MailTransport.mjs +197 -39
  93. package/runtime/arcane/modules/Ollama.js +36 -1
  94. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
  95. package/runtime/arcane/modules/PreferenceStore.js +367 -33
  96. package/runtime/arcane/modules/RecordReviewStore.js +322 -23
  97. package/runtime/arcane/modules/ScreenCapture.js +1397 -15
  98. package/runtime/arcane/modules/SpeechPlayback.js +438 -41
  99. package/runtime/arcane/modules/TerminalClient.js +277 -12
  100. package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
  101. package/runtime/arcane/modules/ThemeManager.js +39 -7
  102. package/runtime/arcane/modules/TimeGuard.js +131 -20
  103. package/runtime/arcane/modules/WaitForComponent.js +386 -33
  104. package/schemas/arcane-lock.schema.json +2 -2
  105. package/src/cli/main.mjs +435 -9
  106. package/src/event-manager.mjs +1097 -1
  107. package/src/import-map.mjs +21 -3
  108. package/src/index.mjs +13 -0
  109. package/src/installed-sdk-runtime.mjs +112 -0
  110. package/src/mail-api.mjs +22 -0
  111. package/src/mail-credentials.mjs +667 -0
  112. package/src/mail-server.mjs +1769 -0
  113. package/src/mail.mjs +261 -0
  114. package/src/sdk-browser-runtime.mjs +85 -41
  115. package/src/testing-loader.mjs +7 -0
  116. package/src/toolchain.mjs +3 -0
  117. package/src/workspace.mjs +1 -1
@@ -1,5 +1,7 @@
1
1
  export {
2
+ BROWSER_SPEECH_ARTIFACT_GRAPH_PROTOCOL,
2
3
  BROWSER_SPEECH_ARTIFACT_PROTOCOL,
4
+ createBrowserSpeechArtifactGraph,
3
5
  createBrowserSpeechAuthority,
4
6
  createDbopfsSpeechArtifactStore,
5
7
  } from "./browser-speech-artifacts.mjs";
@@ -1,3 +1,12 @@
1
- import { installBrowserSpeechWorker } from "./speech-worker-runtime.mjs";
1
+ import {
2
+ installBrowserSpeechArtifactModuleWorker,
3
+ installBrowserSpeechWorker,
4
+ } from "./speech-worker-runtime.mjs";
2
5
 
3
- installBrowserSpeechWorker("stt");
6
+ const mode = new URL(import.meta.url).searchParams.get("arcaneSpeechWorkerMode");
7
+
8
+ if (mode === "artifact-module-worker") {
9
+ installBrowserSpeechArtifactModuleWorker("stt");
10
+ } else {
11
+ installBrowserSpeechWorker("stt");
12
+ }
@@ -1,9 +1,13 @@
1
+ import { createArcaneEventSource } from "arcane-os/event-manager";
2
+
1
3
  export const ARCANE_AI_ADAPTER_PROTOCOL = "arcane-ai-adapter/1";
2
4
 
3
5
  const SECURITY_KEYS = Object.freeze(["secure", "checks"]);
4
6
  const SECURITY_CHECK_KEYS = Object.freeze(["byteLength", "sha256"]);
5
7
  const EMPTY_SECURITY_CHECKS = Object.freeze({});
6
8
  const EMPTY_MODEL_SECURITY = Object.freeze({ checks: EMPTY_SECURITY_CHECKS });
9
+ const MODEL_CONTROLLER_EVENT_TYPES = Object.freeze(["statechange", "progress"]);
10
+ const MODEL_CONTROLLER_EVENT_TYPE_SET = new Set(MODEL_CONTROLLER_EVENT_TYPES);
7
11
 
8
12
  function closedSecurityRecord(value, keys, label) {
9
13
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -81,7 +85,8 @@ export function resolveModelSecurity({ app, binding, load } = {}) {
81
85
  }
82
86
 
83
87
  export function sameModelSecurity(left, right) {
84
- return left?.checks?.byteLength === right?.checks?.byteLength
88
+ return left?.secure === right?.secure
89
+ && left?.checks?.byteLength === right?.checks?.byteLength
85
90
  && left?.checks?.sha256 === right?.checks?.sha256;
86
91
  }
87
92
 
@@ -142,11 +147,149 @@ function providerMethod(provider, name) {
142
147
  return typeof provider?.[name] === "function" ? provider[name].bind(provider) : null;
143
148
  }
144
149
 
150
+ function invalidStatus(cause) {
151
+ return new ArcaneAIError(
152
+ "ARCANE_AI_PROVIDER_STATUS_INVALID",
153
+ "The LLM provider returned an invalid status record.",
154
+ { cause, operation: "status" },
155
+ );
156
+ }
157
+
145
158
  function copyError(error) {
146
159
  if (!error) return null;
160
+ let code;
161
+ let message;
162
+ if (typeof error === "object" || typeof error === "function") {
163
+ try {
164
+ const codeDescriptor = Object.getOwnPropertyDescriptor(error, "code");
165
+ const messageDescriptor = Object.getOwnPropertyDescriptor(error, "message");
166
+ if ((codeDescriptor && !("value" in codeDescriptor))
167
+ || (messageDescriptor && !("value" in messageDescriptor))) {
168
+ throw invalidStatus();
169
+ }
170
+ code = codeDescriptor?.value;
171
+ message = messageDescriptor?.value;
172
+ } catch (copyErrorFailure) {
173
+ if (copyErrorFailure instanceof ArcaneAIError) throw copyErrorFailure;
174
+ throw invalidStatus(copyErrorFailure);
175
+ }
176
+ }
177
+ return Object.freeze({
178
+ code: String(code ?? "ARCANE_AI_REQUEST_FAILED"),
179
+ message: String(message ?? "The Arcane AI operation failed."),
180
+ });
181
+ }
182
+
183
+ function isModelControllerListener(value) {
184
+ return typeof value === "function"
185
+ || Boolean(value && typeof value === "object" && typeof value.handleEvent === "function");
186
+ }
187
+
188
+ function copyProviderStatus(value) {
189
+ if (value === undefined || value === null) return {};
190
+ if (typeof value !== "object" || Array.isArray(value)) throw invalidStatus();
191
+ try {
192
+ const copy = {};
193
+ for (const key of Reflect.ownKeys(value)) {
194
+ if (typeof key !== "string") throw invalidStatus();
195
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
196
+ if (!descriptor || !("value" in descriptor)) throw invalidStatus();
197
+ Object.defineProperty(copy, key, {
198
+ value: descriptor.value,
199
+ enumerable: descriptor.enumerable,
200
+ configurable: true,
201
+ writable: true,
202
+ });
203
+ }
204
+ return copy;
205
+ } catch (error) {
206
+ if (error instanceof ArcaneAIError) throw error;
207
+ throw invalidStatus(error);
208
+ }
209
+ }
210
+
211
+ const MAX_PROGRESS_DEPTH = 8;
212
+ const MAX_PROGRESS_ENTRIES = 256;
213
+
214
+ function invalidProgress(cause) {
215
+ return new ArcaneAIError(
216
+ "ARCANE_AI_PROVIDER_PROGRESS_INVALID",
217
+ "The LLM provider returned an invalid progress record.",
218
+ { cause, operation: "load" },
219
+ );
220
+ }
221
+
222
+ function copyProgressValue(value, state, depth = 0) {
223
+ if (value === null || typeof value !== "object") return value;
224
+ if (depth > MAX_PROGRESS_DEPTH || state.seen.has(value)) {
225
+ throw invalidProgress();
226
+ }
227
+ const prototype = Object.getPrototypeOf(value);
228
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
229
+ throw invalidProgress();
230
+ }
231
+ state.seen.add(value);
232
+ const copy = Array.isArray(value) ? [] : prototype === null ? Object.create(null) : {};
233
+ try {
234
+ for (const key of Reflect.ownKeys(value)) {
235
+ if (Array.isArray(value) && key === "length") continue;
236
+ state.entries += 1;
237
+ if (state.entries > MAX_PROGRESS_ENTRIES || typeof key !== "string") {
238
+ throw invalidProgress();
239
+ }
240
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
241
+ if (!descriptor || !("value" in descriptor)) throw invalidProgress();
242
+ Object.defineProperty(copy, key, {
243
+ value: copyProgressValue(descriptor.value, state, depth + 1),
244
+ enumerable: descriptor.enumerable,
245
+ configurable: false,
246
+ writable: false,
247
+ });
248
+ }
249
+ return Object.freeze(copy);
250
+ } finally {
251
+ state.seen.delete(value);
252
+ }
253
+ }
254
+
255
+ function copyProgress(progress) {
256
+ if (progress === undefined || progress === null) return null;
257
+ if (typeof progress !== "object" || Array.isArray(progress)) {
258
+ throw invalidProgress();
259
+ }
260
+ try {
261
+ return copyProgressValue(progress, { seen: new WeakSet(), entries: 0 });
262
+ } catch (error) {
263
+ if (error instanceof ArcaneAIError) throw error;
264
+ throw invalidProgress(error);
265
+ }
266
+ }
267
+
268
+ function publicProgress(progress) {
269
+ if (!progress || typeof progress !== "object") return null;
270
+ const file = progress.file && typeof progress.file === "object"
271
+ ? Object.freeze({
272
+ ...(Number.isSafeInteger(progress.file.index) ? { index: progress.file.index } : {}),
273
+ ...(Number.isSafeInteger(progress.file.count) ? { count: progress.file.count } : {}),
274
+ ...(typeof progress.file.name === "string" ? { name: progress.file.name } : {}),
275
+ ...(Number.isSafeInteger(progress.file.loaded) ? { loaded: progress.file.loaded } : {}),
276
+ ...(progress.file.total === null || Number.isSafeInteger(progress.file.total)
277
+ ? { total: progress.file.total }
278
+ : {}),
279
+ })
280
+ : null;
147
281
  return Object.freeze({
148
- code: String(error.code ?? "ARCANE_AI_REQUEST_FAILED"),
149
- message: String(error.message ?? "The Arcane AI operation failed."),
282
+ ...(typeof progress.modelId === "string" ? { modelId: progress.modelId } : {}),
283
+ ...(typeof progress.phase === "string" ? { phase: progress.phase } : {}),
284
+ ...(Number.isSafeInteger(progress.loaded) ? { loaded: progress.loaded } : {}),
285
+ ...(progress.total === null || Number.isSafeInteger(progress.total)
286
+ ? { total: progress.total }
287
+ : {}),
288
+ ...(progress.percent === null
289
+ || (typeof progress.percent === "number" && Number.isFinite(progress.percent))
290
+ ? { percent: progress.percent }
291
+ : {}),
292
+ ...(file ? { file } : {}),
150
293
  });
151
294
  }
152
295
 
@@ -183,15 +326,6 @@ function fireAndForget(callback, ...args) {
183
326
  }
184
327
  }
185
328
 
186
- class ControllerEvent {
187
- constructor(type, detail, target) {
188
- this.type = type;
189
- this.detail = detail;
190
- this.target = target;
191
- this.currentTarget = target;
192
- }
193
- }
194
-
195
329
  let generatedRequestId = 0;
196
330
 
197
331
  function requestIdentity(value) {
@@ -225,7 +359,7 @@ export class ModelController {
225
359
  #provider;
226
360
  #loadPolicy;
227
361
  #security;
228
- #listeners = new Map();
362
+ #events;
229
363
  #loadPromise = null;
230
364
  #readyPolicyResolved = false;
231
365
  #unloadPromise = null;
@@ -233,6 +367,7 @@ export class ModelController {
233
367
  #operationGeneration = 0;
234
368
  #disposing = false;
235
369
  #disposed = false;
370
+ #disposeUnloadAdmission = false;
236
371
  #activeStreams = new Set();
237
372
  #fallbackState = "unloaded";
238
373
  #progress = null;
@@ -258,47 +393,68 @@ export class ModelController {
258
393
  this.#provider = provider;
259
394
  this.#loadPolicy = loadPolicy;
260
395
  this.#security = normalizeModelSecurity(security, "app security");
396
+ this.#events = createArcaneEventSource(this, {
397
+ source: "ai-model-controller",
398
+ eventTypes: MODEL_CONTROLLER_EVENT_TYPES,
399
+ });
261
400
  }
262
401
 
263
402
  status() {
264
- const providerStatus = providerMethod(this.#provider, "status")?.(
265
- Object.freeze({ security: this.#security }),
266
- ) ?? {};
403
+ const providerStatus = copyProviderStatus(
404
+ providerMethod(this.#provider, "status")?.(
405
+ Object.freeze({ security: this.#security }),
406
+ ),
407
+ );
408
+ const progress = copyProgress(providerStatus.progress ?? this.#progress);
267
409
  return Object.freeze({
268
410
  ...providerStatus,
269
411
  kind: "llm",
270
412
  state: providerStatus.state ?? this.#fallbackState,
271
- progress: providerStatus.progress ?? this.#progress,
272
- error: providerStatus.error ?? copyError(this.#error),
413
+ progress,
414
+ error: copyError(providerStatus.error ?? this.#error),
273
415
  });
274
416
  }
275
417
 
276
- addEventListener(type, listener) {
277
- if (typeof listener !== "function" && typeof listener?.handleEvent !== "function") return;
278
- const listeners = this.#listeners.get(type) ?? new Set();
279
- listeners.add(listener);
280
- this.#listeners.set(type, listeners);
418
+ addEventListener(type, listener, options) {
419
+ if (!MODEL_CONTROLLER_EVENT_TYPE_SET.has(type) || !isModelControllerListener(listener)) return;
420
+ return this.#events.addEventListener(type, listener, options);
281
421
  }
282
422
 
283
- removeEventListener(type, listener) {
284
- this.#listeners.get(type)?.delete(listener);
423
+ removeEventListener(type, listener, options) {
424
+ if (!MODEL_CONTROLLER_EVENT_TYPE_SET.has(type) || !isModelControllerListener(listener)) return;
425
+ return this.#events.removeEventListener(type, listener, options);
285
426
  }
286
427
 
287
428
  on(type, listener) {
288
429
  this.addEventListener(type, listener);
289
- return () => this.removeEventListener(type, listener);
430
+ const controller = this;
431
+ let active = true;
432
+ function unsubscribeModelControllerEvent() {
433
+ if (!active) return false;
434
+ active = false;
435
+ controller.removeEventListener(type, listener);
436
+ return true;
437
+ }
438
+ Object.defineProperty(unsubscribeModelControllerEvent, "dispose", {
439
+ value: unsubscribeModelControllerEvent,
440
+ enumerable: false,
441
+ configurable: false,
442
+ writable: false,
443
+ });
444
+ return unsubscribeModelControllerEvent;
290
445
  }
291
446
 
292
- #emit(type) {
293
- const event = new ControllerEvent(type, this.status(), this);
294
- for (const listener of [...(this.#listeners.get(type) ?? [])]) {
295
- try {
296
- if (typeof listener === "function") listener.call(this, event);
297
- else listener.handleEvent(event);
298
- } catch {
299
- // UI observers cannot alter lifecycle state.
300
- }
301
- }
447
+ #emit(type, operationId) {
448
+ const status = this.status();
449
+ const progress = type === "progress" ? publicProgress(status.progress) : null;
450
+ this.#events.dispatch(type, status, {
451
+ operationId,
452
+ publicDetail: Object.freeze({
453
+ ...(typeof status.state === "string" ? { state: status.state } : {}),
454
+ ...(progress ? { progress } : {}),
455
+ ...(typeof status.error?.code === "string" ? { code: status.error.code } : {}),
456
+ }),
457
+ });
302
458
  }
303
459
 
304
460
  #assertOperational() {
@@ -345,60 +501,71 @@ export class ModelController {
345
501
  }
346
502
  const wasReady = state === "ready";
347
503
  const operationGeneration = ++this.#operationGeneration;
504
+ const operationId = `${this.#events.instanceId}:load:${operationGeneration.toString(36)}`;
505
+ let resolveOperation;
506
+ let rejectOperation;
507
+ const operation = new Promise(function createModelLoadOperation(resolve, reject) {
508
+ resolveOperation = resolve;
509
+ rejectOperation = reject;
510
+ });
511
+ this.#loadPromise = operation;
348
512
  if (!wasReady) {
349
513
  this.#fallbackState = "loading";
350
514
  this.#progress = null;
351
515
  this.#error = null;
352
- this.#emit("statechange");
516
+ this.#emit("statechange", operationId);
517
+ }
518
+ const controller = this;
519
+ function reportModelLoadProgress(progress) {
520
+ if (
521
+ operationGeneration !== controller.#operationGeneration
522
+ || controller.#disposing
523
+ || controller.#disposed
524
+ ) return;
525
+ controller.#progress = copyProgress(progress);
526
+ controller.#emit("progress", operationId);
353
527
  }
354
- this.#loadPromise = (async () => {
528
+ async function executeModelLoad() {
355
529
  try {
356
530
  await load(options, Object.freeze({
357
531
  protocol: ARCANE_AI_ADAPTER_PROTOCOL,
358
532
  kind: "llm",
359
533
  operation: "load",
360
534
  signal,
361
- security: this.#security,
362
- reportProgress: (progress) => {
363
- if (
364
- operationGeneration !== this.#operationGeneration
365
- || this.#disposing
366
- || this.#disposed
367
- ) return;
368
- this.#progress = progress;
369
- this.#emit("progress");
370
- },
535
+ security: controller.#security,
536
+ reportProgress: reportModelLoadProgress,
371
537
  }));
372
538
  if (
373
- operationGeneration !== this.#operationGeneration
374
- || this.#disposing
375
- || this.#disposed
376
- ) return this.status();
377
- this.#readyPolicyResolved = true;
539
+ operationGeneration !== controller.#operationGeneration
540
+ || controller.#disposing
541
+ || controller.#disposed
542
+ ) return controller.status();
543
+ controller.#readyPolicyResolved = true;
378
544
  if (!wasReady) {
379
- this.#fallbackState = "ready";
380
- this.#progress = null;
381
- this.#emit("statechange");
545
+ controller.#fallbackState = "ready";
546
+ controller.#progress = null;
547
+ controller.#emit("statechange", operationId);
382
548
  }
383
- return this.status();
549
+ return controller.status();
384
550
  } catch (error) {
385
551
  const normalized = normalizeArcaneAIError(error, { operation: "load", signal });
386
552
  if (
387
- operationGeneration === this.#operationGeneration
388
- && !this.#disposing
389
- && !this.#disposed
553
+ operationGeneration === controller.#operationGeneration
554
+ && !controller.#disposing
555
+ && !controller.#disposed
390
556
  && !wasReady
391
557
  ) {
392
- this.#fallbackState = "error";
393
- this.#error = normalized;
394
- this.#emit("statechange");
558
+ controller.#fallbackState = "error";
559
+ controller.#error = normalized;
560
+ controller.#emit("statechange", operationId);
395
561
  }
396
562
  throw normalized;
397
563
  } finally {
398
- this.#loadPromise = null;
564
+ if (controller.#loadPromise === operation) controller.#loadPromise = null;
399
565
  }
400
- })();
401
- return this.#loadPromise;
566
+ }
567
+ void executeModelLoad().then(resolveOperation, rejectOperation);
568
+ return operation;
402
569
  }
403
570
 
404
571
  async #ready(loadOptions = {}) {
@@ -427,53 +594,66 @@ export class ModelController {
427
594
  }
428
595
 
429
596
  async unload(options = {}) {
597
+ if ((this.#disposed || this.#disposing) && !this.#disposeUnloadAdmission) {
598
+ this.#assertOperational();
599
+ }
430
600
  if (this.#unloadPromise) return this.#unloadPromise;
431
601
  const unload = providerMethod(this.#provider, "unload");
432
602
  if (!unload) throw new ArcaneAIError("ARCANE_AI_UNAVAILABLE", "The LLM provider cannot unload.");
433
603
  const signal = options.signal ?? null;
434
604
  const inFlightLoad = this.#loadPromise;
435
605
  const operationGeneration = ++this.#operationGeneration;
606
+ const operationId = `${this.#events.instanceId}:unload:${operationGeneration.toString(36)}`;
436
607
  const context = Object.freeze({
437
608
  protocol: ARCANE_AI_ADAPTER_PROTOCOL,
438
609
  kind: "llm",
439
610
  operation: "unload",
440
611
  signal,
441
612
  });
613
+ let resolveOperation;
614
+ let rejectOperation;
615
+ const operation = new Promise(function createModelUnloadOperation(resolve, reject) {
616
+ resolveOperation = resolve;
617
+ rejectOperation = reject;
618
+ });
619
+ this.#unloadPromise = operation;
442
620
  this.#fallbackState = "unloading";
443
- this.#emit("statechange");
444
- this.#unloadPromise = (async () => {
621
+ this.#emit("statechange", operationId);
622
+ const controller = this;
623
+ async function executeModelUnload() {
445
624
  try {
446
- await this.#closeStreams("The browser-WASM model is unloading.");
625
+ await controller.#closeStreams("The browser-WASM model is unloading.");
447
626
  // The first call asks the provider to cancel its in-flight load. A
448
627
  // provider owns its public status, so wait for the captured load and
449
628
  // reassert unload afterward; a late provider-owned `ready` state can
450
629
  // otherwise outlive this controller's generation guard.
451
630
  await unload(options, context);
452
631
  if (inFlightLoad) {
453
- await inFlightLoad.catch(() => undefined);
632
+ await inFlightLoad.catch(function ignoreSupersededModelLoad() {});
454
633
  await unload(options, context);
455
634
  }
456
- if (operationGeneration === this.#operationGeneration) {
457
- this.#fallbackState = "unloaded";
458
- this.#readyPolicyResolved = false;
459
- this.#progress = null;
460
- this.#error = null;
461
- this.#emit("statechange");
635
+ if (operationGeneration === controller.#operationGeneration) {
636
+ controller.#fallbackState = "unloaded";
637
+ controller.#readyPolicyResolved = false;
638
+ controller.#progress = null;
639
+ controller.#error = null;
640
+ controller.#emit("statechange", operationId);
462
641
  }
463
- return this.status();
642
+ return controller.status();
464
643
  } catch (error) {
465
644
  const normalized = normalizeArcaneAIError(error, { operation: "unload", signal });
466
- if (operationGeneration === this.#operationGeneration) {
467
- this.#fallbackState = "error";
468
- this.#error = normalized;
469
- this.#emit("statechange");
645
+ if (operationGeneration === controller.#operationGeneration) {
646
+ controller.#fallbackState = "error";
647
+ controller.#error = normalized;
648
+ controller.#emit("statechange", operationId);
470
649
  }
471
650
  throw normalized;
472
651
  } finally {
473
- this.#unloadPromise = null;
652
+ if (controller.#unloadPromise === operation) controller.#unloadPromise = null;
474
653
  }
475
- })();
476
- return this.#unloadPromise;
654
+ }
655
+ void executeModelUnload().then(resolveOperation, rejectOperation);
656
+ return operation;
477
657
  }
478
658
 
479
659
  async chat(request = {}) {
@@ -673,30 +853,40 @@ export class ModelController {
673
853
  this.#operationGeneration += 1;
674
854
  let resolveOperation;
675
855
  let rejectOperation;
676
- const operation = new Promise((resolve, reject) => {
856
+ const operation = new Promise(function createModelDisposeOperation(resolve, reject) {
677
857
  resolveOperation = resolve;
678
858
  rejectOperation = reject;
679
859
  });
680
860
  this.#disposePromise = operation;
681
- (async () => {
861
+ const controller = this;
862
+ async function executeModelDispose() {
682
863
  try {
683
- await this.unload(options);
684
- const dispose = providerMethod(this.#provider, "dispose");
864
+ let unloadOperation;
865
+ try {
866
+ controller.#disposeUnloadAdmission = true;
867
+ unloadOperation = controller.unload(options);
868
+ } finally {
869
+ controller.#disposeUnloadAdmission = false;
870
+ }
871
+ await unloadOperation;
872
+ const dispose = providerMethod(controller.#provider, "dispose");
685
873
  if (dispose) await dispose(options);
686
- this.#disposed = true;
687
- this.#listeners.clear();
688
- return this.status();
874
+ controller.#disposed = true;
875
+ const status = controller.status();
876
+ controller.#events.dispose();
877
+ return status;
689
878
  } catch (error) {
690
879
  throw normalizeArcaneAIError(error, {
691
880
  operation: "dispose",
692
881
  signal: options.signal,
693
882
  });
694
883
  } finally {
695
- this.#disposing = false;
884
+ controller.#disposing = false;
696
885
  }
697
- })().then(resolveOperation, rejectOperation);
698
- operation.catch(() => {
699
- if (this.#disposePromise === operation) this.#disposePromise = null;
886
+ }
887
+ void executeModelDispose().then(resolveOperation, rejectOperation);
888
+ operation.catch(function resetFailedModelDispose() {
889
+ if (controller.#disposePromise === operation) controller.#disposePromise = null;
700
890
  });
701
891
  return operation;
702
892
  }