arcane-os 0.69.0 → 0.70.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 (32) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/browser-decisions.mjs +46 -2
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +58 -62
  5. package/browser-runtime/ai/browser-speech-providers.mjs +3 -0
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +76 -164
  7. package/browser-runtime/ai/dbopfs-model-parts.mjs +344 -0
  8. package/browser-runtime/ai/dbopfs-model-resources.mjs +190 -0
  9. package/browser-runtime/ai/decision-runtime.mjs +17 -1
  10. package/browser-runtime/ai/decision-worker.mjs +21 -1
  11. package/browser-runtime/ai/model-resource-bridge.mjs +231 -0
  12. package/browser-runtime/ai/speech-worker-client.mjs +68 -28
  13. package/browser-runtime/ai/speech-worker-runtime.mjs +212 -97
  14. package/browser-runtime/core/contracts.mjs +1 -1
  15. package/docs/reference/ai/browser-decisions.md +57 -3
  16. package/docs/reference/ai/browser-speech.md +45 -17
  17. package/docs/reference/ai/browser-wasm.md +28 -20
  18. package/docs/reference/core-native-packaging.md +13 -0
  19. package/docs/reference/core-runtime.md +9 -0
  20. package/docs/reference/core-shared-host.md +190 -0
  21. package/docs/reference/inventory/runtime-modules.json +3 -3
  22. package/docs/reference/runtime-modules.md +97 -7
  23. package/docs/reference/sdk-api.md +41 -30
  24. package/package.json +2 -2
  25. package/runtime/arcane/modules/AI.js +2 -18
  26. package/runtime/arcane/modules/AIProviderRuntime.js +77 -3
  27. package/runtime/arcane/modules/DBOPFS.js +180 -26
  28. package/src/core/host.mjs +2 -0
  29. package/src/core/runtime.mjs +9 -4
  30. package/src/core/services/packaged-web.mjs +8 -1
  31. package/src/core/shared-host.mjs +500 -0
  32. package/src/native/portable-layout.mjs +66 -34
@@ -8844,13 +8844,14 @@ occurrences use source `ai-model-controller`; every load or unload owns one
8844
8844
  non-null operation ID shared by its state and progress occurrences. Public
8845
8845
  progress retains the exact selected `modelId`, `phase`, state, and nested
8846
8846
  file-progress field names. Browser-WASM download progress preserves
8847
- `completed`, `total`, and `unit:'files'` and adds `loadedBytes`, `totalBytes`,
8848
- `remainingBytes`, `bytesPerSecond`, `etaSeconds`, `activeTransfers`,
8849
- `transferLimit`, and `transferMode`. Unknown totals, rates, remaining values,
8850
- and ETAs are `null`; applications format the raw byte and second measures for
8851
- display. Chunk-driven transfer changes are coalesced on a 250 ms cadence, while
8852
- plan, active-worker, and completion boundaries publish immediately and the existing five-second
8853
- heartbeat repeats the latest record.
8847
+ `completed`, `total`, and `unit:'files'`, with complete per-file `members`
8848
+ reporting stored shards. A member's `total` remains `null` until its complete
8849
+ response is stored, unless the HTTP range plan already establishes its shard
8850
+ total. `activeTransfers`, `transferLimit`, and `transferMode` describe the
8851
+ active download work. Closed shards, active-worker changes, and completion
8852
+ boundaries publish semantic progress; the existing heartbeat repeats the latest
8853
+ record. These fields describe files and shards, without byte-based display
8854
+ requirements or estimated transfer rates.
8854
8855
  Accessor-bearing provider status
8855
8856
  and malformed progress reject with `ARCANE_AI_PROVIDER_STATUS_INVALID` and
8856
8857
  `ARCANE_AI_PROVIDER_PROGRESS_INVALID`. After disposal, lifecycle operations fail with
@@ -9443,8 +9444,8 @@ console.log(provider.status().state); // unloaded
9443
9444
  ### Overview
9444
9445
 
9445
9446
  Adapts an existing DBOPFS instance into a complete multi-model, ordered
9446
- multi-file cache without renaming its public methods. Each completed whole
9447
- member or Range part is committed independently. An interrupted set
9447
+ multi-file cache without renaming its public methods. Every new full response
9448
+ or Range transfer is committed in ordered parts. An interrupted set
9448
9449
  is not a cache hit, but its completed members remain available to the next
9449
9450
  install so only missing work is fetched.
9450
9451
 
@@ -9462,25 +9463,26 @@ workers
9462
9463
  when the probe returns a usable total from exposed `Content-Range` or optional
9463
9464
  descriptor `bytes`. The mutable
9464
9465
  result contains `kind`, `tableName`, `downloadConcurrency`, the original
9465
- `adapter`, and `ready`, `install`, `ensure`, and `remove`. `ensure()` returns
9466
+ `adapter`, and `ready`, `install`, `ensure`, `remove`, and `fetchResource`. `ensure()` returns
9466
9467
  `files` and `cache:'cached'|'installed'`. It preserves the complete model content.
9467
9468
  `offline:true` never downloads and rejects a miss with
9468
9469
  `ARCANE_AI_MODEL_OFFLINE_MISS`.
9469
9470
 
9470
- When supplied, `onProgress` receives existing ordered file progress plus
9471
- `loadedBytes`, `totalBytes`, `remainingBytes`, `bytesPerSecond`, `etaSeconds`,
9472
- `activeTransfers`, `transferLimit`, and `transferMode`. `activeTransfers` is the
9473
- current transfer-worker count. Transfer mode is
9474
- `probing`, `files`, `ranges`, or `single`. Unknown aggregate totals, remaining
9475
- values, rates, and ETAs are `null`. Chunk-driven changes are coalesced on a
9476
- 250 ms cadence, with immediate records at download start, plan or total
9477
- discovery, active-worker changes, and completion. `loadedBytes` includes current staged writes and
9478
- restored completed members or Range parts; bytes from a failed partial write
9479
- are removed from the total when that entry is discarded. A complete set of
9480
- declared member `bytes` values
9481
- makes split-model total and remaining progress available from the start;
9482
- otherwise aggregate-dependent fields remain `null` until they are honestly
9483
- known.
9471
+ When supplied, `onProgress` receives ordered file progress and `members`
9472
+ records with each file's `name`, committed `completed` parts, `total`, and
9473
+ `unit:'shards'`. Streaming totals remain `null` until the response finishes;
9474
+ previously complete files identify `state:'cached'`. `activeTransfers` is the
9475
+ current transfer-worker count, `transferLimit` is selected concurrency, and
9476
+ `transferMode` is `probing`, `files`, `ranges`, or `single`. Closed-part and
9477
+ lifecycle changes publish promptly, with an existing 250 ms heartbeat. No
9478
+ byte progress, transfer rate, or estimated completion time is produced.
9479
+
9480
+ `fetchResource(input,{signal,onProgress,...requestOptions})` returns
9481
+ `{file,status,statusText,headers,url,redirected}` for a complete stored Fetch
9482
+ response. SDK browser model loaders use this shared owner for configuration,
9483
+ tokenizer, graph, external-data, and other fetched resources. Successful
9484
+ complete GET resources may be reused; supported HTTP ranges resume closed
9485
+ parts after interruption. Request framing stays at the transport boundary.
9484
9486
 
9485
9487
  ### Availability and normalization
9486
9488
 
@@ -9491,9 +9493,9 @@ resumable Range parts. For any source member, a followed redirect that
9491
9493
  turns the first `bytes=0-0` probe into `200` triggers one direct probe of the
9492
9494
  final URL. Confirmed support cancels the original body and starts the Range
9493
9495
  pool; refusal keeps the original response as the ordinary single-fetch path,
9494
- with readable `Content-Length` used for progress. A missing `Content-Range` can
9496
+ without requiring a readable `Content-Length`. A missing `Content-Range` can
9495
9497
  use optional descriptor `bytes` for Range planning; without either total, the
9496
- store falls back to one full fetch. A confirmed source member is divided into
9498
+ store uses one full fetch streamed into ordered persistent parts. A confirmed source member is divided into
9497
9499
  deterministic OPFS Range parts of roughly 4 MB each, up to 4,096 parts. A
9498
9500
  monolithic source may use at most
9499
9501
  `downloadConcurrency` Range workers; split sources keep one Range worker per
@@ -9502,7 +9504,7 @@ in order as one logical Blob. A later non-206, contradictory exposed Range
9502
9504
  response, or incorrectly framed body fails rather than assembling partial
9503
9505
  content. On failure or cancellation, peers settle while completed shards and
9504
9506
  Range parts remain available for retry; an unfinished active part restarts after
9505
- refresh. A persisted Range part whose length does not match its requested HTTP
9507
+ refresh. Existing whole-file caches remain readable without migration. A persisted Range part whose length does not match its requested HTTP
9506
9508
  frame is discarded and fetched again. A complete whole member supersedes its
9507
9509
  redundant current Range fragments. Cleanup failure is warned without
9508
9510
  hiding the usable model. Wllama requires every selected model Blob or File to
@@ -9784,7 +9786,7 @@ retains the complete list of deletions already completed.
9784
9786
 
9785
9787
  Adapts an existing DBOPFS instance into a complete caller-selected speech
9786
9788
  runtime/model store. It serializes each selected model with an exclusive Web
9787
- Lock, removes partial state after failure, stores ordinary mutable selection
9789
+ Lock, preserves complete peer files and closed parts after failure, stores ordinary mutable selection
9788
9790
  metadata before content, and treats a changed file inventory or source mapping
9789
9791
  as a cache miss that is downloaded again.
9790
9792
 
@@ -9794,7 +9796,7 @@ as a cache miss that is downloaded again.
9794
9796
  createDbopfsSpeechArtifactStore({ dbopfs, tableName='arcane_ai_browser_speech', fetchImpl=null, objectUrlFactory=null }={})
9795
9797
  ```
9796
9798
 
9797
- The mutable result is `{protocol,tableName,prepare,remove}`.
9799
+ The mutable result is `{protocol,tableName,prepare,remove,fetchResource}`.
9798
9800
  `prepare(authority,{signal,onProgress,offline=false,security})` uses a complete
9799
9801
  compatible cache or downloads every declared file, preserves complete content
9800
9802
  and materializes object URLs, then returns
@@ -9804,9 +9806,18 @@ needs those URLs. `offline:true` never fetches and rejects a miss with
9804
9806
  authority's files and selection metadata. Optional `onProgress` receives
9805
9807
  preparation and download records with `phase`, `stage`, `message`, and the
9806
9808
  current `file` when available. File operations report actual `completed` and
9807
- `total` inventory counts with `unit:'files'`; module preparation uses
9809
+ `total` inventory counts with `unit:'files'`, and `resource` carries the current
9810
+ closed-part progress with `unit:'shards'`; module preparation uses
9808
9811
  `total:null`. Cancellation stops progress along with the owning operation.
9809
9812
 
9813
+ New files stream into ordered DBOPFS parts with no required response length.
9814
+ Existing complete whole files stay readable without migration.
9815
+ `fetchResource(input,{signal,onProgress,...requestOptions})` returns
9816
+ `{file,status,statusText,headers,url,redirected}` and gives the dedicated
9817
+ speech Worker the same complete resource storage owner for runtime fetches.
9818
+ Native ESM imports and unmapped classic Worker internals retain their platform
9819
+ loader; they are not claimed as intercepted HTTP fetches.
9820
+
9810
9821
  ### Availability and normalization
9811
9822
 
9812
9823
  **Browser with a ready DBOPFS instance, OPFS, Web Locks, Fetch or an injected
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.69.0",
3
+ "version": "0.70.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -123,7 +123,7 @@
123
123
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
124
124
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
125
125
  "test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/conversation-closing-report.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/native-windows-icon.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
126
- "test:functional": "node ./bin/arcane-test.mjs test/ai-ollama-readiness.test.mjs test/core-ollama-lifecycle.test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-decisions.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/codex-app-server.test.mjs test/mcp-stdio-server.test.mjs test/core-classic-source.test.mjs test/core-client.test.mjs test/core-client-installation.test.mjs test/core-ai-installation.test.mjs test/core-llama-lifecycle.test.mjs test/core-development.test.mjs test/core-host.test.mjs test/core-model-assets.test.mjs test/core-native-services.test.mjs test/core-packaged-web.test.mjs test/core-runtime.test.mjs test/core-stdio.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/local-image-editing.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/native-decisions.test.mjs test/native-onnx.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
126
+ "test:functional": "node ./bin/arcane-test.mjs test/ai-ollama-readiness.test.mjs test/core-ollama-lifecycle.test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-decisions.test.mjs test/browser-speech-providers.test.mjs test/browser-speech-artifacts.test.mjs test/browser-speech-resource-bridge.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/codex-app-server.test.mjs test/mcp-stdio-server.test.mjs test/core-classic-source.test.mjs test/core-client.test.mjs test/core-client-installation.test.mjs test/core-ai-installation.test.mjs test/core-llama-lifecycle.test.mjs test/core-development.test.mjs test/core-host.test.mjs test/core-shared-host.test.mjs test/core-model-assets.test.mjs test/core-native-services.test.mjs test/core-packaged-web.test.mjs test/core-runtime.test.mjs test/core-stdio.test.mjs test/dbopfs-committed-events.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-model-resources.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/local-image-editing.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/model-resource-bridge.test.mjs test/native-decisions.test.mjs test/native-onnx.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
127
127
  "test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-macos-provider.test.mjs test/native-plan.test.mjs test/native-portable-provider.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
128
128
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/html-import-packaged-script.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
129
129
  "check": "node tools/check-source.mjs && npm test",
@@ -1182,7 +1182,6 @@ class AI {
1182
1182
  #speechJobSequence=0;
1183
1183
  #stopOllamaReady=null;
1184
1184
  #ttsSegmentation={...DEFAULT_TTS_SEGMENTATION};
1185
- #preparedSpeechReadiness = null;
1186
1185
  #preparedSpeechPlaybacks = new Set();
1187
1186
  #nativeSpeechPlaybacks = new Set();
1188
1187
  #speechUnlockContext = null;
@@ -4394,7 +4393,7 @@ class AI {
4394
4393
  runtime.#browserSpeechTransition,activation.controller.signal
4395
4394
  );
4396
4395
  activation.controller.signal.throwIfAborted();
4397
- await runtime.#providerRuntime.setSpeechMuted(false);
4396
+ await runtime.#providerRuntime.setSpeechMuted(false, {signal: activation.controller.signal});
4398
4397
  if(runtime.#speechActivation!==activation)return false;
4399
4398
  const status=runtime.#providerRuntime.status('tts');
4400
4399
  runtime.muted=status.state!=='ready'||status.loaded!==true;
@@ -6409,22 +6408,7 @@ class AI {
6409
6408
  if(selected) {
6410
6409
  const status = runtime.#providerRuntime.status('tts');
6411
6410
  if(status.state !== 'ready' || status.loaded !== true) {
6412
- if(!runtime.#preparedSpeechReadiness) {
6413
- const readiness = runtime.#providerRuntime.setSpeechMuted(false);
6414
- runtime.#preparedSpeechReadiness = readiness;
6415
- readiness.finally(
6416
- function releasePreparedSpeechReadiness() {
6417
- if(runtime.#preparedSpeechReadiness === readiness) {
6418
- runtime.#preparedSpeechReadiness = null;
6419
- }
6420
- }
6421
- ).catch(
6422
- function reportPreparedSpeechReadinessFailure(error) {
6423
- runtime.#traceSpeech('prepareTTS.readiness.error', {error});
6424
- }
6425
- );
6426
- }
6427
- await runtime.#preparedSpeechReadiness;
6411
+ await runtime.#providerRuntime.setSpeechMuted(false, {signal: requestSignal});
6428
6412
  assertPreparedSpeechSelection();
6429
6413
  }
6430
6414
  }
@@ -1688,6 +1688,7 @@ export class AIProviderRuntime {
1688
1688
  #speechDesiredMuted = true;
1689
1689
  #speechTransition = Promise.resolve();
1690
1690
  #speechTransitionOwners = 0;
1691
+ #speechActivation = null;
1691
1692
  #unsubscribeIntents = null;
1692
1693
  #closed = false;
1693
1694
  #closing = false;
@@ -2784,6 +2785,11 @@ export class AIProviderRuntime {
2784
2785
  }
2785
2786
  if (slot.loadPromise) {
2786
2787
  if (this.#sameSelection(slot.selection, targetSelection)) {
2788
+ const activation = this.#speechActivation;
2789
+ if (role === 'tts' && activation?.loadPromise === slot.loadPromise
2790
+ && signal !== activation.controller.signal) {
2791
+ return this.#observeSpeechActivation(activation, signal);
2792
+ }
2787
2793
  return slot.loadPromise;
2788
2794
  }
2789
2795
  return Promise.reject(
@@ -2957,6 +2963,10 @@ export class AIProviderRuntime {
2957
2963
  }
2958
2964
  );
2959
2965
  slot.loadPromise = loadOperation;
2966
+ if (role === 'tts' && this.#speechActivation
2967
+ && signal === this.#speechActivation.controller.signal) {
2968
+ this.#speechActivation.loadPromise = loadOperation;
2969
+ }
2960
2970
  publishAIRuntimeRoleState(
2961
2971
  role,
2962
2972
  roleRecord(
@@ -4270,19 +4280,34 @@ export class AIProviderRuntime {
4270
4280
  return true;
4271
4281
  }
4272
4282
 
4273
- async setSpeechMuted(muted) {
4283
+ async setSpeechMuted(muted, {signal = null} = {}) {
4274
4284
  arcaneLogging.debug('[Arcane speech runtime] setSpeechMuted', muted);
4275
4285
  this.#assertOpen();
4276
4286
  this.#assertNotConfiguring();
4277
4287
  if (!is.boolean(muted)) {
4278
4288
  fail('AI speech muted state must be a boolean.');
4279
4289
  }
4290
+ assertAbortSignal(signal);
4291
+ if (signal?.aborted) throw normalizedAbort(signal.reason);
4292
+ if (!muted && !this.#speechDesiredMuted && this.#speechActivation
4293
+ && !this.#speechActivation.controller.signal.aborted) {
4294
+ return this.#observeSpeechActivation(this.#speechActivation, signal);
4295
+ }
4280
4296
  this.#speechDesiredMuted = muted;
4281
4297
  if (muted) {
4282
4298
  this.#speechMuted = true;
4299
+ this.#speechActivation?.controller.abort();
4283
4300
  this.#slots.tts.loadController?.abort();
4284
4301
  this.cancel('tts');
4285
4302
  }
4303
+ const activation = muted ? null : {
4304
+ controller: new AbortController(),
4305
+ interests: new Set(),
4306
+ promise: null,
4307
+ loadPromise: null,
4308
+ settled: false
4309
+ };
4310
+ this.#speechActivation = activation;
4286
4311
  const runtime = this;
4287
4312
  this.#speechTransitionOwners += 1;
4288
4313
  const transition = this.#speechTransition.catch(
@@ -4290,6 +4315,9 @@ export class AIProviderRuntime {
4290
4315
  ).then(
4291
4316
  async function reconcileLatestAISpeechPreference() {
4292
4317
  while (true) {
4318
+ if (activation?.controller.signal.aborted) {
4319
+ throw normalizedAbort(activation.controller.signal.reason);
4320
+ }
4293
4321
  const desiredMuted = runtime.#speechDesiredMuted;
4294
4322
  if (desiredMuted) {
4295
4323
  runtime.cancel('tts');
@@ -4306,7 +4334,11 @@ export class AIProviderRuntime {
4306
4334
  if (runtime.#speechDesiredMuted) {
4307
4335
  continue;
4308
4336
  }
4309
- await runtime.load('tts');
4337
+ const currentActivation = runtime.#speechActivation;
4338
+ if (currentActivation?.controller.signal.aborted) {
4339
+ throw normalizedAbort(currentActivation.controller.signal.reason);
4340
+ }
4341
+ await runtime.load('tts', {signal: currentActivation?.controller.signal ?? null});
4310
4342
  }
4311
4343
  if (runtime.#speechDesiredMuted === desiredMuted) {
4312
4344
  runtime.#speechMuted = desiredMuted;
@@ -4318,10 +4350,52 @@ export class AIProviderRuntime {
4318
4350
  ).finally(
4319
4351
  function releaseAIProviderSpeechTransitionOwnership() {
4320
4352
  runtime.#speechTransitionOwners -= 1;
4353
+ if (activation) {
4354
+ activation.settled = true;
4355
+ if (runtime.#speechActivation === activation) {
4356
+ runtime.#speechActivation = null;
4357
+ }
4358
+ }
4321
4359
  }
4322
4360
  );
4323
4361
  this.#speechTransition = transition;
4324
- return transition;
4362
+ if (!activation) return transition;
4363
+ activation.promise = transition;
4364
+ return this.#observeSpeechActivation(activation, signal);
4365
+ }
4366
+
4367
+ #observeSpeechActivation(activation, signal) {
4368
+ return new Promise(function observeAIProviderSpeechActivation(resolve, reject) {
4369
+ const interest = {};
4370
+ let settled = false;
4371
+ activation.interests.add(interest);
4372
+ function releaseSpeechActivationInterest() {
4373
+ activation.interests.delete(interest);
4374
+ signal?.removeEventListener('abort', cancelSpeechActivationInterest);
4375
+ }
4376
+ function cancelSpeechActivationInterest() {
4377
+ if (settled) return;
4378
+ settled = true;
4379
+ releaseSpeechActivationInterest();
4380
+ if (!activation.settled && activation.interests.size === 0) {
4381
+ activation.controller.abort(signal?.reason);
4382
+ }
4383
+ reject(normalizedAbort(signal?.reason));
4384
+ }
4385
+ signal?.addEventListener('abort', cancelSpeechActivationInterest, {once: true});
4386
+ activation.promise.then(function speechActivationReady(status) {
4387
+ if (settled) return;
4388
+ settled = true;
4389
+ releaseSpeechActivationInterest();
4390
+ resolve(status);
4391
+ }, function speechActivationFailed(error) {
4392
+ if (settled) return;
4393
+ settled = true;
4394
+ releaseSpeechActivationInterest();
4395
+ reject(error);
4396
+ });
4397
+ if (signal?.aborted) cancelSpeechActivationInterest();
4398
+ });
4325
4399
  }
4326
4400
 
4327
4401
  #providerFor(slot) {