arcane-os 0.86.1 → 0.88.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.86.1",
3
+ "version": "0.88.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",
@@ -19,6 +19,7 @@
19
19
  "./entities/*": "./runtime/arcane/entities/*",
20
20
  "./logging": "./browser-runtime/logging.mjs",
21
21
  "./browser-device": "./browser-runtime/browser-device.mjs",
22
+ "./document-acquisition": "./browser-runtime/document-acquisition.mjs",
22
23
  "./pwa": "./browser-runtime/pwa.mjs",
23
24
  "./preference-store": "./runtime/arcane/modules/PreferenceStore.js",
24
25
  "./ai": "./runtime/arcane/modules/AI.js",
@@ -69,6 +70,7 @@
69
70
  "./core/repositories": "./src/core/repositories.mjs",
70
71
  "./core/packaged-web": "./src/core/services/packaged-web.mjs",
71
72
  "./core/preferences": "./src/core/services/preferences.mjs",
73
+ "./core/document-acquisition": "./src/core/services/document-acquisition.mjs",
72
74
  "./core/diarization": "./src/diarization/service.mjs",
73
75
  "./core/local-ai": "./src/core/services/local-ai.mjs",
74
76
  "./core/execution-devices": "./src/core/services/execution-devices.mjs",
@@ -128,7 +130,7 @@
128
130
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
129
131
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
130
132
  "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",
131
- "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-kokoro-complete-input.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/execution-devices.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",
133
+ "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-kokoro-complete-input.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/document-acquisition.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/execution-devices.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",
132
134
  "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",
133
135
  "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/ai-audio-output.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
134
136
  "check": "node tools/check-source.mjs && npm test",
@@ -38,7 +38,7 @@
38
38
  overflow-wrap:anywhere;
39
39
  }
40
40
 
41
- button:hover:not(:disabled):not([aria-pressed="true"]) {
41
+ button:hover:not(:disabled):not([aria-disabled="true"]):not([aria-pressed="true"]) {
42
42
  background:color-mix(in srgb,currentColor 14%,transparent);
43
43
  }
44
44
 
@@ -48,7 +48,8 @@
48
48
  box-shadow:inset 0 0 0 .1rem currentColor;
49
49
  }
50
50
 
51
- button:disabled {
51
+ button:disabled,
52
+ button[aria-disabled="true"] {
52
53
  cursor:not-allowed;
53
54
  opacity:.55;
54
55
  }
@@ -88,7 +89,7 @@
88
89
  host.refresh=refresh;
89
90
  group.addEventListener('click',async function selectScheme(event){
90
91
  const button=event.target.closest('[data-scheme]');
91
- if(!button||button.disabled) return;
92
+ if(!button||button.disabled||busy) return;
92
93
  setBusy(true);
93
94
  try{
94
95
  const state=button.dataset.scheme==='custom'?await manager.activateCustom():await manager.setScheme(button.dataset.scheme);
@@ -134,10 +135,14 @@
134
135
  }
135
136
  function setBusy(value){
136
137
  busy=Boolean(value);
137
- for(const button of buttons) button.disabled=busy;
138
+ // Keep the focused choice in the keyboard flow while its operation
139
+ // settles. The click owner rejects repeat activation without moving focus.
140
+ for(const button of buttons) button.setAttribute('aria-disabled',String(busy||button.disabled));
138
141
  }
139
142
  function setCustomAvailable(value){
140
- buttons.find(function customScheme(button){return button.dataset.scheme==='custom';}).disabled=busy||!value;
143
+ const button=buttons.find(function customScheme(button){return button.dataset.scheme==='custom';});
144
+ button.disabled=!value;
145
+ button.setAttribute('aria-disabled',String(busy||button.disabled));
141
146
  }
142
147
  async function refresh(){const state=await manager.load();setMode(state.mode);setCustomAvailable(Boolean(state.theme));return state;}
143
148
 
@@ -759,6 +759,19 @@ function isAbortSignal(value){
759
759
  &&is.function(value.removeEventListener);
760
760
  }
761
761
 
762
+ function normalizeAISelectionOptions({signal=null,startLanguageModel=true}={}){
763
+ if(signal!==null&&!isAbortSignal(signal)){
764
+ throw new TypeError('AI selection signal must be an AbortSignal.');
765
+ }
766
+ if(!is.boolean(startLanguageModel)){
767
+ throw new TypeError('AI selection startLanguageModel must be a boolean.');
768
+ }
769
+ if(signal?.aborted){
770
+ throw normalizeAIRequestAbort(signal.reason);
771
+ }
772
+ return {signal,startLanguageModel};
773
+ }
774
+
762
775
  function closedRecord(value,keys,required,label){
763
776
  if(!value
764
777
  ||!is.object(value)
@@ -1173,6 +1186,8 @@ class AI {
1173
1186
  #browserSpeechTransition=Promise.resolve();
1174
1187
  #builtInLLMProviders=new Map();
1175
1188
  #builtInLLMReadiness=Promise.resolve(null);
1189
+ #providerSelectionGeneration=0;
1190
+ #providerSelectionOperation=null;
1176
1191
  #builtInSpeechProviders=new Map();
1177
1192
  #configuredSpeechProviders=new Map();
1178
1193
  #builtInSpeechReadiness=Promise.resolve(null);
@@ -2146,7 +2161,8 @@ class AI {
2146
2161
  });
2147
2162
  }
2148
2163
 
2149
- #ensureBuiltInLLMProvider(providerId){
2164
+ #ensureBuiltInLLMProvider(providerId,operation=null){
2165
+ if(operation)this.#assertProviderSelection(operation);
2150
2166
  if(providerId!=='TWIN'&&providerId!=='OLLAMA'){
2151
2167
  return false;
2152
2168
  }
@@ -2154,15 +2170,34 @@ class AI {
2154
2170
  return false;
2155
2171
  }
2156
2172
  const provider=this.#createBuiltInLLMProvider(providerId);
2157
- const unregister=this.#providerRuntime.register(provider);
2158
- this.#builtInLLMProviders.set(
2159
- providerId,
2160
- completeValue({provider,unregister})
2161
- );
2173
+ const runtime=this;
2174
+ let unregister=null;
2175
+ const record={
2176
+ provider,
2177
+ unregister:function unregisterBuiltInLLMProvider(){
2178
+ if(unregister)return unregister();
2179
+ if(!runtime.#providerRuntime.ownsProvider('llm',provider))return false;
2180
+ return runtime.#providerRuntime.unregister('llm',providerId);
2181
+ }
2182
+ };
2183
+ // Registration can publish a pending route synchronously; its owner must
2184
+ // already be visible to a selection accepted by that callback.
2185
+ this.#builtInLLMProviders.set(providerId,record);
2186
+ try{
2187
+ unregister=this.#providerRuntime.register(provider);
2188
+ }catch(error){
2189
+ if(!this.#providerRuntime.ownsProvider('llm',provider)
2190
+ &&this.#builtInLLMProviders.get(providerId)===record){
2191
+ this.#builtInLLMProviders.delete(providerId);
2192
+ }
2193
+ throw error;
2194
+ }
2195
+ if(operation)this.#assertProviderSelection(operation);
2162
2196
  return true;
2163
2197
  }
2164
2198
 
2165
- #ensureBuiltInSpeechProvider(role,providerId){
2199
+ #ensureBuiltInSpeechProvider(role,providerId,operation=null){
2200
+ if(operation)this.#assertProviderSelection(operation);
2166
2201
  if(!['stt','tts'].includes(role)||providerId!=='LOCAL_SPEACH'){
2167
2202
  return false;
2168
2203
  }
@@ -2170,33 +2205,54 @@ class AI {
2170
2205
  return false;
2171
2206
  }
2172
2207
  const provider=this.#createBuiltInSpeechProvider(role,providerId);
2173
- const unregister=this.#providerRuntime.register(provider);
2174
- this.#builtInSpeechProviders.set(
2175
- this.#builtInSpeechProviderKey(role,providerId),
2176
- completeValue({role,providerId,provider,unregister})
2177
- );
2208
+ const runtime=this;
2209
+ const key=this.#builtInSpeechProviderKey(role,providerId);
2210
+ let unregister=null;
2211
+ const record={
2212
+ role,providerId,provider,
2213
+ unregister:function unregisterBuiltInSpeechProvider(){
2214
+ if(unregister)return unregister();
2215
+ if(!runtime.#providerRuntime.ownsProvider(role,provider))return false;
2216
+ return runtime.#providerRuntime.unregister(role,providerId);
2217
+ }
2218
+ };
2219
+ this.#builtInSpeechProviders.set(key,record);
2220
+ try{
2221
+ unregister=this.#providerRuntime.register(provider);
2222
+ }catch(error){
2223
+ if(!this.#providerRuntime.ownsProvider(role,provider)
2224
+ &&this.#builtInSpeechProviders.get(key)===record){
2225
+ this.#builtInSpeechProviders.delete(key);
2226
+ }
2227
+ throw error;
2228
+ }
2229
+ if(operation)this.#assertProviderSelection(operation);
2178
2230
  return true;
2179
2231
  }
2180
2232
 
2181
- #releaseInactiveBuiltInLLMProviders(activeProviderId){
2233
+ #releaseInactiveBuiltInLLMProviders(activeProviderId,operation=null){
2182
2234
  for(const [providerId,record] of this.#builtInLLMProviders){
2235
+ if(operation)this.#assertProviderSelection(operation);
2183
2236
  if(providerId===activeProviderId){
2184
2237
  continue;
2185
2238
  }
2186
- if(record.unregister()){
2239
+ if(record.unregister()&&this.#builtInLLMProviders.get(providerId)===record){
2187
2240
  this.#builtInLLMProviders.delete(providerId);
2188
2241
  }
2189
2242
  }
2190
2243
  }
2191
2244
 
2192
- #releaseInactiveBuiltInSpeechProviders(activeProviders){
2245
+ #releaseInactiveBuiltInSpeechProviders(activeProviders,operation=null){
2193
2246
  for(const [key,record] of this.#builtInSpeechProviders){
2247
+ if(operation)this.#assertProviderSelection(operation);
2194
2248
  if(activeProviders[record.role]===record.providerId){
2195
2249
  continue;
2196
2250
  }
2197
2251
  if(record.unregister()){
2198
2252
  record.provider.dispose();
2199
- this.#builtInSpeechProviders.delete(key);
2253
+ if(this.#builtInSpeechProviders.get(key)===record){
2254
+ this.#builtInSpeechProviders.delete(key);
2255
+ }
2200
2256
  }
2201
2257
  }
2202
2258
  }
@@ -2246,7 +2302,85 @@ class AI {
2246
2302
  return this.#builtInSpeechReadiness;
2247
2303
  }
2248
2304
 
2249
- #reconcileBuiltInLLMReadiness(){
2305
+ #beginProviderSelection({signal,startLanguageModel},deferSupersession=false){
2306
+ const previous=this.#providerSelectionOperation;
2307
+ const controller=new AbortController();
2308
+ const operation={
2309
+ generation:++this.#providerSelectionGeneration,
2310
+ controller,
2311
+ startLanguageModel,
2312
+ previous:deferSupersession?previous:null,
2313
+ restorePrevious:deferSupersession,
2314
+ committed:false,
2315
+ settled:false,
2316
+ detachSignal:function detachAISelectionSignal(){
2317
+ signal?.removeEventListener('abort',forwardAbort);
2318
+ }
2319
+ };
2320
+ function forwardAbort(){
2321
+ controller.abort(signal.reason);
2322
+ }
2323
+ this.#providerSelectionOperation=operation;
2324
+ signal?.addEventListener('abort',forwardAbort,{once:true});
2325
+ if(signal?.aborted)forwardAbort();
2326
+ if(!deferSupersession)this.#supersedeProviderSelection(previous);
2327
+ return operation;
2328
+ }
2329
+
2330
+ #supersedeProviderSelection(previous){
2331
+ for(let operation=previous;operation;operation=operation.previous){
2332
+ operation.detachSignal();
2333
+ if(!operation.settled){
2334
+ operation.controller.abort(aiProviderError(
2335
+ 'AI provider selection was superseded.',
2336
+ 'ARCANE_AI_OPERATION_SUPERSEDED'
2337
+ ));
2338
+ }
2339
+ }
2340
+ }
2341
+
2342
+ #assertProviderSelection(operation,allowAborted=false){
2343
+ if(operation.generation!==this.#providerSelectionOperation?.generation){
2344
+ throw aiProviderError(
2345
+ 'AI provider selection was superseded.',
2346
+ 'ARCANE_AI_OPERATION_SUPERSEDED'
2347
+ );
2348
+ }
2349
+ if(!allowAborted&&operation.controller.signal.aborted){
2350
+ throw normalizeAIRequestAbort(operation.controller.signal.reason);
2351
+ }
2352
+ }
2353
+
2354
+ #commitProviderSelection(operation,tuple){
2355
+ // Configuration already committed its routes before notifying listeners.
2356
+ // Keep its tuple together even when that notification aborts this caller;
2357
+ // a newer owner alone prevents this continuation from applying the tuple.
2358
+ this.#assertProviderSelection(operation,true);
2359
+ this.#applyPreferenceTuple(tuple);
2360
+ operation.committed=true;
2361
+ const previous=operation.previous;
2362
+ operation.previous=null;
2363
+ this.#supersedeProviderSelection(previous);
2364
+ this.#assertProviderSelection(operation);
2365
+ }
2366
+
2367
+ #finishProviderSelection(operation){
2368
+ if(operation.restorePrevious&&!operation.committed
2369
+ &&this.#providerSelectionOperation===operation){
2370
+ this.#providerSelectionOperation=operation.previous;
2371
+ }
2372
+ operation.previous=null;
2373
+ operation.settled=true;
2374
+ operation.detachSignal();
2375
+ }
2376
+
2377
+ #reconcileBuiltInLLMReadiness(operation=this.#providerSelectionOperation){
2378
+ if(operation&&(
2379
+ operation!==this.#providerSelectionOperation
2380
+ ||!operation.committed
2381
+ )){
2382
+ return Promise.resolve(this.#providerRuntime.status('llm'));
2383
+ }
2250
2384
  const selection=this.#builtInLLMSelection(false);
2251
2385
  if(!selection){
2252
2386
  return Promise.resolve(this.#providerRuntime.status('llm'));
@@ -2259,10 +2393,14 @@ class AI {
2259
2393
  return this.#providerRuntime.unload('llm');
2260
2394
  }
2261
2395
  if(this.#builtInLLMCapability(selection.providerId)){
2262
- if(status.state==='ready'&&status.loaded===true){
2396
+ if(operation?.startLanguageModel===false
2397
+ ||operation?.controller.signal.aborted
2398
+ ||(status.state==='ready'&&status.loaded===true)){
2263
2399
  return Promise.resolve(status);
2264
2400
  }
2265
- return this.#providerRuntime.load('llm');
2401
+ return this.#providerRuntime.load('llm',{
2402
+ signal:operation?.controller.signal??null
2403
+ });
2266
2404
  }
2267
2405
  if(status.loaded===true
2268
2406
  ||status.busy===true
@@ -2591,12 +2729,15 @@ class AI {
2591
2729
  }
2592
2730
  }
2593
2731
 
2594
- async #unloadProviderRolesForTransition(){
2595
- const settlements=await Promise.allSettled([
2596
- this.#providerRuntime.unload('llm'),
2597
- this.#providerRuntime.unload('stt'),
2598
- this.#providerRuntime.unload('tts')
2599
- ]);
2732
+ async #unloadProviderRolesForTransition(operation){
2733
+ const runtime=this;
2734
+ const settlements=await Promise.allSettled(
2735
+ ['llm','stt','tts'].map(async function unloadPreviousAIProviderRole(role){
2736
+ runtime.#assertProviderSelection(operation);
2737
+ // Accepted cleanup owns completion independently of page cancellation.
2738
+ return runtime.#providerRuntime.unload(role);
2739
+ })
2740
+ );
2600
2741
  const failure=settlements.find(function findAITransitionCleanupFailure(result){
2601
2742
  return result.status==='rejected';
2602
2743
  });
@@ -2643,7 +2784,8 @@ class AI {
2643
2784
  ttsService,
2644
2785
  model,
2645
2786
  modelTTS,
2646
- modelSTT
2787
+ modelSTT,
2788
+ options
2647
2789
  ) {
2648
2790
  if (
2649
2791
  !(
@@ -2667,59 +2809,83 @@ class AI {
2667
2809
  ]);
2668
2810
  this.#assertValidProviderTuple(tuple);
2669
2811
  this.#assertSynchronousBrowserSpeechSupersession('AI.setAI');
2670
- this.#ensureBuiltInLLMProvider(tuple[0]);
2671
- this.#ensureBuiltInSpeechProvider('stt',tuple[1]);
2672
- this.#ensureBuiltInSpeechProvider('tts',tuple[2]);
2673
- this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
2674
- this.#invalidateSpeechControl();
2675
- this.#applyPreferenceTuple(tuple);
2676
- this.#releaseInactiveBuiltInLLMProviders(tuple[0]);
2677
- this.#releaseInactiveBuiltInSpeechProviders({
2678
- stt:tuple[1],
2679
- tts:tuple[2]
2680
- });
2681
- this.#retainBuiltInLLMReadiness(
2682
- this.#reconcileBuiltInLLMReadiness()
2683
- );
2684
- this.#retainBuiltInSpeechReadiness(
2685
- this.#reconcileBuiltInSpeechReadiness()
2686
- );
2687
- return true;
2812
+ const normalized=normalizeAISelectionOptions(options);
2813
+ const operation=this.#beginProviderSelection(normalized,true);
2814
+ try{
2815
+ this.#assertProviderSelection(operation);
2816
+ this.#ensureBuiltInLLMProvider(tuple[0],operation);
2817
+ this.#ensureBuiltInSpeechProvider('stt',tuple[1],operation);
2818
+ this.#ensureBuiltInSpeechProvider('tts',tuple[2],operation);
2819
+ this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
2820
+ this.#commitProviderSelection(operation,tuple);
2821
+ this.#invalidateSpeechControl();
2822
+ this.#assertProviderSelection(operation);
2823
+ this.#releaseInactiveBuiltInLLMProviders(tuple[0],operation);
2824
+ this.#assertProviderSelection(operation);
2825
+ this.#releaseInactiveBuiltInSpeechProviders({
2826
+ stt:tuple[1],
2827
+ tts:tuple[2]
2828
+ },operation);
2829
+ this.#assertProviderSelection(operation);
2830
+ const runtime=this;
2831
+ this.#retainBuiltInLLMReadiness(
2832
+ this.#reconcileBuiltInLLMReadiness(operation).finally(
2833
+ function settleAISelectionReadiness(){
2834
+ runtime.#finishProviderSelection(operation);
2835
+ }
2836
+ )
2837
+ );
2838
+ this.#assertProviderSelection(operation);
2839
+ this.#retainBuiltInSpeechReadiness(
2840
+ this.#reconcileBuiltInSpeechReadiness()
2841
+ );
2842
+ return true;
2843
+ }catch(error){
2844
+ this.#finishProviderSelection(operation);
2845
+ throw error;
2846
+ }
2688
2847
  }
2689
2848
 
2690
- configureProviders(selections){
2849
+ configureProviders(selections,options){
2691
2850
  const prepared=this.#providerRuntime.validateConfiguration(selections);
2692
2851
  this.#assertDeviceSpeechConfiguration(prepared);
2693
2852
  this.#assertSynchronousBrowserSpeechSupersession('AI.configureProviders');
2694
- this.#ensureBuiltInLLMProvider(
2695
- prepared.llm.default?.providerId
2696
- );
2697
- this.#ensureBuiltInSpeechProvider(
2698
- 'stt',
2699
- prepared.stt.default?.providerId
2700
- );
2701
- this.#ensureBuiltInSpeechProvider(
2702
- 'tts',
2703
- prepared.tts.default?.providerId
2704
- );
2705
- this.#assertRegisteredBuiltInRoutes(prepared);
2706
- const configured=this.#providerRuntime.configure(prepared);
2707
- this.#invalidateSpeechControl();
2708
- this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
2709
- this.#releaseInactiveBuiltInLLMProviders(
2710
- configured.llm.default?.providerId
2711
- );
2712
- this.#releaseInactiveBuiltInSpeechProviders({
2713
- stt:configured.stt.default?.providerId,
2714
- tts:configured.tts.default?.providerId
2715
- });
2716
- this.#retainBuiltInLLMReadiness(
2717
- this.#reconcileBuiltInLLMReadiness()
2718
- );
2719
- this.#retainBuiltInSpeechReadiness(
2720
- this.#reconcileBuiltInSpeechReadiness()
2721
- );
2722
- return configured;
2853
+ const normalized=normalizeAISelectionOptions(options);
2854
+ const operation=this.#beginProviderSelection(normalized,true);
2855
+ try{
2856
+ this.#assertProviderSelection(operation);
2857
+ this.#ensureBuiltInLLMProvider(prepared.llm.default?.providerId,operation);
2858
+ this.#ensureBuiltInSpeechProvider('stt',prepared.stt.default?.providerId,operation);
2859
+ this.#ensureBuiltInSpeechProvider('tts',prepared.tts.default?.providerId,operation);
2860
+ this.#assertRegisteredBuiltInRoutes(prepared);
2861
+ const configured=this.#providerRuntime.configure(prepared);
2862
+ this.#commitProviderSelection(operation,this.#tupleFromProviderRoutes(configured));
2863
+ this.#invalidateSpeechControl();
2864
+ this.#assertProviderSelection(operation);
2865
+ this.#releaseInactiveBuiltInLLMProviders(configured.llm.default?.providerId,operation);
2866
+ this.#assertProviderSelection(operation);
2867
+ this.#releaseInactiveBuiltInSpeechProviders({
2868
+ stt:configured.stt.default?.providerId,
2869
+ tts:configured.tts.default?.providerId
2870
+ },operation);
2871
+ this.#assertProviderSelection(operation);
2872
+ const runtime=this;
2873
+ this.#retainBuiltInLLMReadiness(
2874
+ this.#reconcileBuiltInLLMReadiness(operation).finally(
2875
+ function settleAIProviderConfigurationReadiness(){
2876
+ runtime.#finishProviderSelection(operation);
2877
+ }
2878
+ )
2879
+ );
2880
+ this.#assertProviderSelection(operation);
2881
+ this.#retainBuiltInSpeechReadiness(
2882
+ this.#reconcileBuiltInSpeechReadiness()
2883
+ );
2884
+ return configured;
2885
+ }catch(error){
2886
+ this.#finishProviderSelection(operation);
2887
+ throw error;
2888
+ }
2723
2889
  }
2724
2890
 
2725
2891
  configureSpeechProviders(selections){
@@ -2762,7 +2928,8 @@ class AI {
2762
2928
  ttsService,
2763
2929
  model,
2764
2930
  modelTTS,
2765
- modelSTT
2931
+ modelSTT,
2932
+ options
2766
2933
  ){
2767
2934
  const tuple=this.#nextPreferenceTuple([
2768
2935
  llmService,
@@ -2773,54 +2940,70 @@ class AI {
2773
2940
  modelSTT
2774
2941
  ]);
2775
2942
  this.#assertValidProviderTuple(tuple);
2776
- await this.#supersedeBrowserSpeechForRouteChange();
2777
- this.#invalidateSpeechControl();
2778
- await this.#unloadProviderRolesForTransition();
2779
- this.#ensureBuiltInLLMProvider(tuple[0]);
2780
- this.#ensureBuiltInSpeechProvider('stt',tuple[1]);
2781
- this.#ensureBuiltInSpeechProvider('tts',tuple[2]);
2782
- this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
2783
- this.#applyPreferenceTuple(tuple);
2784
- this.#releaseInactiveBuiltInLLMProviders(tuple[0]);
2785
- this.#releaseInactiveBuiltInSpeechProviders({
2786
- stt:tuple[1],
2787
- tts:tuple[2]
2788
- });
2789
- await this.#reconcileBuiltInLLMReadiness();
2790
- await this.#reconcileBuiltInSpeechReadiness();
2791
- return this.#providerRuntime.status();
2943
+ const operation=this.#beginProviderSelection(normalizeAISelectionOptions(options));
2944
+ try{
2945
+ this.#assertProviderSelection(operation);
2946
+ await this.#supersedeBrowserSpeechForRouteChange(operation);
2947
+ this.#assertProviderSelection(operation);
2948
+ this.#invalidateSpeechControl();
2949
+ await this.#unloadProviderRolesForTransition(operation);
2950
+ this.#assertProviderSelection(operation);
2951
+ this.#ensureBuiltInLLMProvider(tuple[0],operation);
2952
+ this.#ensureBuiltInSpeechProvider('stt',tuple[1],operation);
2953
+ this.#ensureBuiltInSpeechProvider('tts',tuple[2],operation);
2954
+ this.#assertProviderSelection(operation);
2955
+ this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
2956
+ this.#commitProviderSelection(operation,tuple);
2957
+ this.#releaseInactiveBuiltInLLMProviders(tuple[0],operation);
2958
+ this.#assertProviderSelection(operation);
2959
+ this.#releaseInactiveBuiltInSpeechProviders({
2960
+ stt:tuple[1],
2961
+ tts:tuple[2]
2962
+ },operation);
2963
+ this.#assertProviderSelection(operation);
2964
+ await this.#reconcileBuiltInLLMReadiness(operation);
2965
+ this.#assertProviderSelection(operation);
2966
+ await this.#reconcileBuiltInSpeechReadiness();
2967
+ this.#assertProviderSelection(operation);
2968
+ return this.#providerRuntime.status();
2969
+ }finally{
2970
+ this.#finishProviderSelection(operation);
2971
+ }
2792
2972
  }
2793
2973
 
2794
- async transitionProviders(selections){
2974
+ async transitionProviders(selections,options){
2795
2975
  const prepared=this.#providerRuntime.validateConfiguration(selections);
2796
2976
  this.#assertDeviceSpeechConfiguration(prepared);
2797
- await this.#supersedeBrowserSpeechForRouteChange();
2798
- this.#ensureBuiltInLLMProvider(
2799
- prepared.llm.default?.providerId
2800
- );
2801
- this.#ensureBuiltInSpeechProvider(
2802
- 'stt',
2803
- prepared.stt.default?.providerId
2804
- );
2805
- this.#ensureBuiltInSpeechProvider(
2806
- 'tts',
2807
- prepared.tts.default?.providerId
2808
- );
2809
- this.#assertRegisteredBuiltInRoutes(prepared);
2810
- this.#invalidateSpeechControl();
2811
- await this.#unloadProviderRolesForTransition();
2812
- const configured=this.#providerRuntime.configure(prepared);
2813
- this.#applyPreferenceTuple(this.#tupleFromProviderRoutes(configured));
2814
- this.#releaseInactiveBuiltInLLMProviders(
2815
- configured.llm.default?.providerId
2816
- );
2817
- this.#releaseInactiveBuiltInSpeechProviders({
2818
- stt:configured.stt.default?.providerId,
2819
- tts:configured.tts.default?.providerId
2820
- });
2821
- await this.#reconcileBuiltInLLMReadiness();
2822
- await this.#reconcileBuiltInSpeechReadiness();
2823
- return configured;
2977
+ const operation=this.#beginProviderSelection(normalizeAISelectionOptions(options));
2978
+ try{
2979
+ this.#assertProviderSelection(operation);
2980
+ await this.#supersedeBrowserSpeechForRouteChange(operation);
2981
+ this.#assertProviderSelection(operation);
2982
+ this.#invalidateSpeechControl();
2983
+ await this.#unloadProviderRolesForTransition(operation);
2984
+ this.#assertProviderSelection(operation);
2985
+ this.#ensureBuiltInLLMProvider(prepared.llm.default?.providerId,operation);
2986
+ this.#ensureBuiltInSpeechProvider('stt',prepared.stt.default?.providerId,operation);
2987
+ this.#ensureBuiltInSpeechProvider('tts',prepared.tts.default?.providerId,operation);
2988
+ this.#assertRegisteredBuiltInRoutes(prepared);
2989
+ this.#assertProviderSelection(operation);
2990
+ const configured=this.#providerRuntime.configure(prepared);
2991
+ this.#commitProviderSelection(operation,this.#tupleFromProviderRoutes(configured));
2992
+ this.#releaseInactiveBuiltInLLMProviders(configured.llm.default?.providerId,operation);
2993
+ this.#assertProviderSelection(operation);
2994
+ this.#releaseInactiveBuiltInSpeechProviders({
2995
+ stt:configured.stt.default?.providerId,
2996
+ tts:configured.tts.default?.providerId
2997
+ },operation);
2998
+ this.#assertProviderSelection(operation);
2999
+ await this.#reconcileBuiltInLLMReadiness(operation);
3000
+ this.#assertProviderSelection(operation);
3001
+ await this.#reconcileBuiltInSpeechReadiness();
3002
+ this.#assertProviderSelection(operation);
3003
+ return configured;
3004
+ }finally{
3005
+ this.#finishProviderSelection(operation);
3006
+ }
2824
3007
  }
2825
3008
 
2826
3009
  async transitionSpeechProviders(selections){
@@ -3061,10 +3244,13 @@ class AI {
3061
3244
  );
3062
3245
  }
3063
3246
 
3064
- async #supersedeBrowserSpeechForRouteChange(){
3247
+ async #supersedeBrowserSpeechForRouteChange(operation=null){
3065
3248
  for(const role of [...this.#configuredSpeechProviders.keys()]){
3249
+ if(operation)this.#assertProviderSelection(operation);
3066
3250
  await this.configureSpeechProvider(role,null);
3251
+ if(operation)this.#assertProviderSelection(operation);
3067
3252
  }
3253
+ if(operation)this.#assertProviderSelection(operation);
3068
3254
  if(!this.#browserSpeechConfigurationRecord
3069
3255
  &&this.#browserSpeechRetiredRecords.size===0){
3070
3256
  return false;
@@ -304,8 +304,11 @@ function copyRoleRecord(role, record) {
304
304
  }
305
305
  const pendingRecovery = ['recovering', 'error'].includes(record.state)
306
306
  && record.operationId !== null;
307
- if (record.busy && !pendingRecovery && (record.state !== 'ready' || !record.loaded)) {
308
- fail('a busy role must be ready and loaded or retain its recovering/error operation.');
307
+ const pendingUnload = record.state === 'unloading'
308
+ && record.loaded && record.operationId !== null;
309
+ if (record.busy && !pendingRecovery && !pendingUnload
310
+ && (record.state !== 'ready' || !record.loaded)) {
311
+ fail('a busy role must be ready and loaded, retain its recovering/error operation, or retain its loaded unloading operation.');
309
312
  }
310
313
  if (record.state === 'error' && error === null) {
311
314
  fail('an error role state must include error details.');