arcane-os 0.33.6 → 0.34.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.
@@ -41,7 +41,7 @@ authoritative in the linked contract document.
41
41
  | `arcane/entities/Theme.js#themeTokens` | constant | `themeTokens` | Frozen ordered definitions that map public theme-token keys to CSS custom properties, labels, and defaults. |
42
42
  | `arcane/entities/Theme.js#themeColorToHex` | function | `themeColorToHex` | Validates an RGB, RGBA, or six-digit hexadecimal color and returns its RGB channels as a normalized six-digit hexadecimal color. |
43
43
  | `arcane/entities/Theme.js#default` | class | `Theme` | Validates and freezes a named light or dark token set and can serialize, restore, apply, or clear that theme on a document root. |
44
- | `arcane/entities/User.js#default` | class | `UserEntity` | Owns the browser user's validated settings and profile record, including DBOPFS load, refresh, serialized updates, and optional persistence. |
44
+ | `arcane/entities/User.js#default` | class | `UserEntity` | Owns the browser user's settings and profile record, including DBOPFS load, refresh, serialized partial or fresh-baseline callback updates, and optional persistence. Callback null skips saving; timestamp setters merge only their fields through the same write owner. |
45
45
  | `arcane/entities/Weather.js#WeatherLocation` | class | `WeatherLocation` | Validates and freezes geographic identity, coordinates, and timezone metadata for a weather location. |
46
46
  | `arcane/entities/Weather.js#WeatherObservation` | class | `WeatherObservation` | Validates and freezes one timestamped current-weather observation and its units. |
47
47
  | `arcane/entities/Weather.js#WeatherDay` | class | `WeatherDay` | Validates and freezes one daily forecast with temperature, precipitation, and optional sunrise and sunset times. |
@@ -6,7 +6,7 @@
6
6
  "minimumVersion": "22.23.2 for Node entrypoints",
7
7
  "moduleSystem": "ESM"
8
8
  },
9
- "memberCount": 217,
9
+ "memberCount": 218,
10
10
  "runtimeSubpathPatterns": {
11
11
  "arcane-os/modules/*": "./runtime/arcane/modules/*",
12
12
  "arcane-os/entities/*": "./runtime/arcane/entities/*"
@@ -3106,6 +3106,20 @@
3106
3106
  "protocol": "arcane-ai-browser-speech-artifacts/1 over arcane-ai-provider/2",
3107
3107
  "normalization": "Exact protocol string; it grants no model authority and names no model download"
3108
3108
  },
3109
+ {
3110
+ "id": "browser-speech:createDigitalOceanFalTTSProvider",
3111
+ "name": "createDigitalOceanFalTTSProvider",
3112
+ "displayName": "createDigitalOceanFalTTSProvider()",
3113
+ "kind": "function",
3114
+ "signature": "createDigitalOceanFalTTSProvider({id,model,getApiKey,maxConcurrentRequests=4,fetch=globalThis.fetch}={})",
3115
+ "entrypoints": ["arcane-os/ai/browser-speech"],
3116
+ "primaryImport": "arcane-os/ai/browser-speech",
3117
+ "group": "Browser speech providers",
3118
+ "summary": "Creates an explicitly selected DigitalOcean FAL remote TTS provider using standard Fetch and completed audio jobs.",
3119
+ "availability": "Browser or Node with Fetch, Blob, AbortController, and an application-owned DigitalOcean inference key",
3120
+ "protocol": "arcane-ai-provider/2 tts/synthesize",
3121
+ "normalization": "Construction and activation make no adapter network requests. Activation awaits the application-owned string or promise key reader. localOnly false, concurrent requests, complete audio Blob results, and local cancellation without claiming remote-job cancellation. Applications own model, voice, credentials, and activation."
3122
+ },
3109
3123
  {
3110
3124
  "id": "browser-speech:createBrowserKokoroProvider",
3111
3125
  "name": "createBrowserKokoroProvider",
@@ -19,7 +19,7 @@
19
19
  {"file":"runtime/arcane/entities/Preference.js","exports":["default","preferenceSchema"],"availability":"cross-host","normalization":"normalized"},
20
20
  {"file":"runtime/arcane/entities/TerminalSession.js","exports":["default","terminalShells"],"availability":"cross-host","normalization":"normalized"},
21
21
  {"file":"runtime/arcane/entities/Theme.js","exports":["arcaneDarkThemeTokens","arcaneLightThemeTokens","default","themeColorToHex","themeTokens"],"availability":"cross-host/dom-when-applied","normalization":"normalized/mixed"},
22
- {"file":"runtime/arcane/entities/User.js","exports":["default","profileUpdateFromSearchParams"],"availability":"browser/native-webview","normalization":"Existing User lifecycle is unchanged; profileUpdateFromSearchParams purely maps nonempty supplied URLSearchParams values without further decoding, trimming, or persistence."},
22
+ {"file":"runtime/arcane/entities/User.js","exports":["default","profileUpdateFromSearchParams"],"availability":"browser/native-webview","normalization":"updateExplicit accepts existing partial/JSON updates or an awaited fresh-profile callback under the existing serialized write owner; callback null skips saving and returns the fresh profile, while failed writes restore the fresh baseline. Timestamp setters persist partial fields through that owner. profileUpdateFromSearchParams purely maps nonempty supplied URLSearchParams values without further decoding, trimming, or persistence."},
23
23
  {"file":"runtime/arcane/entities/Weather.js","exports":["WeatherDay","WeatherLocation","WeatherObservation","WeatherSnapshot"],"availability":"cross-host","normalization":"normalized"}
24
24
  ]
25
25
  }
@@ -274,6 +274,46 @@ Owns persisted user preference/profile fields, explicit-preference updates,
274
274
  fresh reads, saves, and JSON projection. It installs `window.user` after its
275
275
  DBLS/DBOPFS lifecycle and emits `user-entity-loaded`.
276
276
 
277
+ ### updateExplicit(update)
278
+
279
+ Accepts an ordinary partial profile object, a JSON string, or a function
280
+ `current => partialOrNull`. It returns `Promise<UserEntityData>` with the
281
+ resulting explicit profile, retaining the existing field setters and their
282
+ validation behavior.
283
+
284
+ The function is called once after the latest durable profile has been read,
285
+ inside the existing serialized update and Web Lock. It may return a promise.
286
+ Return only the fields to change, or return `null` to skip the durable write
287
+ and receive the fresh profile unchanged. This no-write meaning applies only
288
+ to the callback result; ordinary object/string inputs and direct `null` retain
289
+ their existing behavior. With `persist=false`, the callback uses current
290
+ memory and performs no durable read or write.
291
+
292
+ ```javascript
293
+ const displayedName = user.username;
294
+ const profile = await user.updateExplicit(function renameTheCaptain(current) {
295
+ if (current.username !== displayedName) return null;
296
+ return {username: 'Captain Moonboots'};
297
+ });
298
+ ```
299
+
300
+ Callbacks compute a patch without mutating `current`. Do not call or await
301
+ `updateExplicit()`, `withFreshExplicit()`, or `refresh()` on the same instance
302
+ inside the callback: those operations depend on the current queue entry
303
+ finishing. Fetch outside the callback; compare against `current` at the write
304
+ boundary. Rendering and unrelated background work need not await this update.
305
+
306
+ A thrown or rejected callback causes no save and rejects with the original
307
+ error. A failed save restores the fresh baseline in memory and rejects with
308
+ the save error. The persistence setting is restored and later queued updates
309
+ remain usable. The existing `current_time` and `last_successful_time` setters
310
+ keep their immediate in-memory assignment, then save only their timestamp
311
+ field through this same fresh partial-update owner. This preserves newer
312
+ durable profile fields when TimeGuard records a check. Automatic timestamp
313
+ save failures are reported through Arcane logging; callers requiring durable
314
+ completion use and await `updateExplicit({current_time: value})` or the
315
+ corresponding `last_successful_time` partial update.
316
+
277
317
  ### profileUpdateFromSearchParams(searchParams)
278
318
 
279
319
  Named export from `arcane-os/entities/User.js`. Pass an existing
@@ -144,10 +144,11 @@ playback.
144
144
 
145
145
  ### Public surface
146
146
 
147
- default `AI`; read-only `providerRuntime`, `browserSpeechConfiguration`, and
147
+ default `AI`; read-only `providerRuntime`, `speechActivationPending`, `browserSpeechConfiguration`, and
148
148
  `browserSpeechDescriptor`; `configureBrowserSpeech(configuration,{signal})`,
149
149
  `disposeBrowserSpeech({signal})`, `setAI()`, `configureProviders()`,
150
- `configureSpeechProviders()`, `transitionAI()`, `transitionProviders()`,
150
+ `configureSpeechProviders()`, `configureSpeechProvider(role,provider,options)`,
151
+ `transitionAI()`, `transitionProviders()`,
151
152
  `transitionSpeechProviders()`, `startProviders()`, `setSpeechMuted()`,
152
153
  `streamRequest()`, `streamMessage()`, `fetchRequest()`, `fetch()`,
153
154
  read-only `ttsSegmentation`, `configureTTSSegmentation()`,
@@ -163,6 +164,21 @@ entry points. `streamMessage(...)` and `streamRequest(options)` deliver
163
164
  incremental responses. The positional and object forms share the existing
164
165
  provider implementations; neither form is a retired compatibility API.
165
166
 
167
+ `fetchRequest({model,...})` and `streamRequest({model,...})` accept an optional
168
+ request-local model. On the built-in TWiN Cloud route, its exact value becomes
169
+ the outbound `model` field; native Ollama receives the same request-local value.
170
+ Omitting it or passing `undefined` preserves the configured model. Each built-in
171
+ request captures its model before asynchronous callbacks, so concurrent requests
172
+ may use different models without assigning `ai.model`, changing provider
173
+ selection, or loading/unloading another model. Request observers and retries
174
+ receive that request's model. Stream/native completion metadata uses it only
175
+ when the provider omits its own `model` field.
176
+
177
+ Registered providers receive a supplied `model` unchanged in their request
178
+ payload; an omitted value remains absent. The provider owns interpretation.
179
+ This option does not switch a loaded browser-WASM model or its lifecycle:
180
+ explicit provider/model selection and activation remain necessary there.
181
+
166
182
  Built-in cloud chat decodes an HTTP error body once as JSON or text and rejects
167
183
  with that complete value unchanged. It does not reconstruct an Error, replace
168
184
  the message, or add `providerMessage`, `status`, or an SDK failure code to the
@@ -217,16 +233,17 @@ returns aggregate runtime status; `transitionProviders()` returns the configured
217
233
  three-role route configuration. Selected TWiN Cloud `TWIN` LLM, `OLLAMA` LLM,
218
234
  and Core `LOCAL_SPEACH` STT/TTS built-in routes expose truthful capability-only
219
235
  readiness through internal provider/2 adapters without probing, downloading, or
220
- hiding a load. Configured `OPENAI` speech preferences migrate to on-device
221
- `LOCAL_SPEACH`, with Whisper for STT and Kokoro for TTS. TWiN Cloud availability
236
+ hiding a load. Explicit speech preference slots remain unchanged, including a
237
+ selected external provider that the application registers after AI startup.
238
+ TWiN Cloud availability
222
239
  requires the selected LLM route, its model, a credential, and `fetch`; Core speech
223
240
  availability requires the exact selected `Arcane.speech.transcribe` or
224
241
  `synthesize` method. `fetchRequest()`
225
242
  keeps the selected provider's public response shape. Browser speech routes
226
243
  translate the existing AI.js STT `{audio:Blob|File,mimeType,model}` and TTS
227
244
  `{model,input,responseFormat,voice?,speed?}` requests at the provider boundary;
228
- only WAV is accepted for the shared TTS result. TTS voice selection comes from
229
- the exact selected local provider/model catalog `defaultVoice`; a saved
245
+ the selected provider declares its supported audio format. TTS voice selection comes from
246
+ the exact selected provider/model catalog `defaultVoice`; a saved
230
247
  OpenAI-route voice is never forwarded to another provider route.
231
248
 
232
249
  The TWiN Cloud built-in provider and default-model preference sentinel are
@@ -252,14 +269,34 @@ tool declarations, emitted tool calls, or callback ordering.
252
269
 
253
270
  `configureSpeechProviders({stt,tts})` commits only the two speech routes and
254
271
  leaves the current LLM route and sticky lifecycle record unchanged. Both speech
255
- roles must be unloaded, use local-only selections, and own no request, load,
256
- unload, or dispose operation. Non-local STT and TTS selections reject with
257
- `AI_STT_DEVICE_ONLY` and `AI_TTS_DEVICE_ONLY`, respectively.
272
+ roles must be unloaded and own no request, load, unload, or dispose operation.
273
+ STT remains local-only (`AI_STT_DEVICE_ONLY` for a remote selection); an
274
+ explicit TTS provider may declare `localOnly:false`.
258
275
  `transitionSpeechProviders({stt,tts})` stops queued audio, explicitly unloads
259
276
  only STT and TTS, then commits that same closed speech route record. Neither
260
277
  method loads a model, selects a fallback, or changes caller-owned model or voice
261
278
  policy.
262
279
 
280
+ `configureSpeechProvider(role,provider,{modelId,signal,expectedProvider})`
281
+ asynchronously registers and selects one provider/2 speech role, unloading and
282
+ disposing only its previous SDK-owned provider. `modelId` defaults to the first
283
+ catalog entry. It returns the selected `{providerId,modelId,localOnly}` record;
284
+ passing `null` removes that role and returns `null`. An optional
285
+ `expectedProvider` is compared inside the configuration lane; if another
286
+ provider has taken ownership, removal resolves `false` without changing it.
287
+ An unaccepted candidate is disposed on failure; a failure after replacement
288
+ has `committed:true`, and runtime ownership identifies the installed provider.
289
+
290
+ This method composes with an STT-only or TTS-only `configureBrowserSpeech()`
291
+ in either order. The other speech role and LLM keep their routes, readiness,
292
+ requests, and storage. Use sequential disposal when combining this method
293
+ with `disposeBrowserSpeech()`. Configuration does not activate the new provider
294
+ or make a network request. `setSpeechMuted(false)` waits for an in-progress
295
+ configuration before activating the committed TTS selection; a later mute
296
+ cancels that pending unmute. The application keeps model, voice, and credential
297
+ selection. See [DigitalOcean FAL speech](ai/browser-speech.md#digitalocean-fal-text-to-speech)
298
+ for the public remote adapter and completed-job audio transport.
299
+
263
300
  `startProviders({startLanguageModel=true,startMuted=true,startTranscription=false,signal=null}={})`
264
301
  starts provider-owned text chat without requesting an STT load by default.
265
302
  Callers selecting a browser-WASM LLM pass `startLanguageModel:false` so it
@@ -273,6 +310,14 @@ user activation intent exposed by the shared speech component.
273
310
  `setSpeechMuted(false)` records the public unmuted state only after the selected
274
311
  TTS route reaches ready; a failed load leaves the public state muted. In contrast,
275
312
  `setSpeechMuted(true)` cancels active TTS work and unloads that role.
313
+ An explicit unmute may wait for its selected external provider to register.
314
+ `speechActivationPending` distinguishes that intent from an ordinary muted
315
+ state. Streamed text and its final remainder may enter the existing speech
316
+ queue during that wait; synthesis waits for the same activation promise.
317
+ Text never activates a provider itself. Stop cancels queued preparation even
318
+ while registration is pending; mute, disposal, a new startup operation, or an
319
+ actual route replacement also revoke the pending activation. Initial
320
+ registration of the already-selected provider/model preserves queued speech.
276
321
  The optional browser-speech `stt.execution` and `tts.execution` records select
277
322
  `device:'auto'|'webnn-npu'|'webgpu'|'wasm'` and `maxConcurrentRequests`.
278
323
  Omission uses NPU, GPU, then CPU automatic selection with one Whisper slot and
@@ -67,7 +67,7 @@ runtime layouts.
67
67
  | `arcane-os/ai/browser-wasm` | Caller-selected browser-local Wllama inference, complete DBOPFS model storage, streaming, cancellation, and structural tool-call results. |
68
68
  | `arcane-os/ai/tool-text-stream` | Shared selected tool-argument text observer for provider integration. |
69
69
  | `arcane-os/ai/twin-cloud` | Complete TWiN Cloud requests from Node or a browser with an explicit key/model and shared retry/cancellation behavior. |
70
- | `arcane-os/ai/browser-speech` | Caller-selected browser-local Whisper STT and Kokoro TTS provider mechanisms, ordinary upstream assets, materialized/native routing, Workers, and cancellation. |
70
+ | `arcane-os/ai/browser-speech` | Caller-selected browser-local Whisper STT and Kokoro TTS, explicit DigitalOcean FAL remote TTS, ordinary upstream assets, role lifecycle, and cancellation. |
71
71
  | `arcane-os/mail` | Portable Mail runtime, durable outbox, complete transport responses, and provider-neutral acceptance contracts. |
72
72
 
73
73
  These lowercase runtime-module entrypoints expose their existing exports; they
@@ -178,6 +178,7 @@ browser map are cataloged separately in [Runtime modules](runtime-modules.md).
178
178
  | `getPwaInstall()` | function | `arcane-os/pwa` | Progressive web applications | Browser native installation events; waiting state while no prompt is available |
179
179
  | `mountPwaInstallPrompt()` | function | `arcane-os/pwa` | Progressive web applications | Browser document and managed HTML import; resolves to null without a document |
180
180
  | `createDbopfsSpeechArtifactStore()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser with ready DBOPFS, Web Locks, Fetch, File/Blob, and object URLs |
181
+ | `createDigitalOceanFalTTSProvider()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser or Node with Fetch, Blob, and an application-supplied DigitalOcean inference key |
181
182
  | `removeBrowserSpeechModelCache()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser CacheStorage; explicit removal of one selected upstream model |
182
183
  | `createNativeBuildPlan()` | function | `arcane-os` | Targets, native plans, and providers | Node; selected browser/native target or provider as documented |
183
184
  | `createNativeTargetAdapter()` | function | `arcane-os` | Targets, native plans, and providers | Node; selected browser/native target or provider as documented |
@@ -6763,7 +6764,7 @@ releaseProvider();
6763
6764
 
6764
6765
  # Browser speech providers
6765
6766
 
6766
- `arcane-os/ai/browser-speech` exports exactly the seven package members below.
6767
+ `arcane-os/ai/browser-speech` exports the package members below.
6767
6768
  It ships provider, authority, artifact-store, and Worker mechanisms but no
6768
6769
  Whisper/Kokoro model weights, adapter runtime artifacts, voices, download URLs,
6769
6770
  default catalog, CDN loader, native bridge, or cloud fallback. The providers
@@ -7121,6 +7122,69 @@ async function transcribeAfterUserChoice(audioBlob) {
7121
7122
  }
7122
7123
  ```
7123
7124
 
7125
+ ## createDigitalOceanFalTTSProvider()
7126
+
7127
+ ### Overview
7128
+
7129
+ Creates a `localOnly:false` provider/2 TTS adapter for the caller-selected
7130
+ DigitalOcean FAL model. It submits independent synthesis jobs, polls their
7131
+ completion, and returns each complete downloaded audio `Blob`. The shared AI
7132
+ queue owns punctuation segmentation, bounded concurrent requests, and playback
7133
+ in input order; this is not provider-native live audio streaming.
7134
+
7135
+ ### Signature and parameters
7136
+
7137
+ ```text
7138
+ createDigitalOceanFalTTSProvider({id,model,getApiKey,maxConcurrentRequests=4,fetch=globalThis.fetch}={})
7139
+ ```
7140
+
7141
+ `id` is the application-owned provider identifier. `model` contains the selected
7142
+ `id` and `defaultVoice`; the supported multilingual-v2 selection is
7143
+ `fal-ai/elevenlabs/tts/multilingual-v2`. `getApiKey()` returns the
7144
+ application's current DigitalOcean inference key or a promise for it. The credential is never
7145
+ included in catalog or status. `maxConcurrentRequests` is a positive integer;
7146
+ the runtime owns admission and its FIFO queue. Optional `fetch` supplies the
7147
+ standard Fetch interface.
7148
+
7149
+ ### Availability and normalization
7150
+
7151
+ The adapter needs Fetch, Blob, and AbortController; it needs no Worker or
7152
+ DBOPFS. Browser access depends on endpoint CORS support. Construction makes
7153
+ no network request. Inspection and activation await the application's key
7154
+ reader without making an SDK network request. Activation checks
7155
+ local credential presence, not remote validity. Status reports remote
7156
+ execution and current activity. Requests preserve complete text, selected
7157
+ voice, and speed; multilingual-v2 speed outside `0.7` through `1.2` rejects
7158
+ instead of clamping. Returned media is fetched without the inference key.
7159
+ Cancelling a credential wait leaves the application's shared refresh alone
7160
+ and prevents a late result from starting this cancelled operation.
7161
+
7162
+ Each provider/2 `tts/synthesize` request returns one complete audio Blob.
7163
+ Request cancellation, unload, and disposal stop local polling/downloads and
7164
+ discard late results; they cannot promise cancellation of an already submitted
7165
+ remote job. HTTP, provider, and media failures remain observable. The adapter
7166
+ does not select a fallback or silently resubmit a paid synthesis POST.
7167
+
7168
+ ### Example
7169
+
7170
+ ```javascript
7171
+ import {createDigitalOceanFalTTSProvider} from 'arcane-os/ai/browser-speech';
7172
+
7173
+ async function configureCloudVoice(ai, getApiKey) {
7174
+ const provider = createDigitalOceanFalTTSProvider({
7175
+ id: 'selected-cloud-voice',
7176
+ model: {id: 'fal-ai/elevenlabs/tts/multilingual-v2', defaultVoice: 'Rachel'},
7177
+ getApiKey
7178
+ });
7179
+ await ai.configureSpeechProvider('tts', provider);
7180
+ return provider;
7181
+ }
7182
+ ```
7183
+
7184
+ Call activation and synthesis from the application's explicit speech action.
7185
+ For streaming text, cancellation, and independent STT configuration, see
7186
+ [DigitalOcean FAL speech](ai/browser-speech.md#digitalocean-fal-text-to-speech).
7187
+
7124
7188
  ## createBrowserKokoroProvider()
7125
7189
 
7126
7190
  ### Overview
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.33.6",
3
+ "version": "0.34.1",
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",
@@ -86,7 +86,7 @@
86
86
  "test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
87
87
  "test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
88
88
  "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/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/targets.test.mjs test/workspace-operation-lock.test.mjs",
89
- "test:functional": "node ./bin/arcane-test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/cli.test.mjs test/dbopfs-document-library.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/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.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/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs",
89
+ "test:functional": "node ./bin/arcane-test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/cli.test.mjs test/dbopfs-document-library.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/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/modal.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/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/twin-cloud.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
90
90
  "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-plan.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",
91
91
  "test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.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",
92
92
  "check": "node tools/check-source.mjs && npm test",
@@ -2965,13 +2965,13 @@
2965
2965
  const clearThinking=thinkingHTML;
2966
2966
 
2967
2967
  //console.table(arguments);
2968
+ const aiRuntime=boundChatAI??globalThis.ai;
2968
2969
  if(
2969
2970
  !isThinking
2970
2971
  &&text
2971
- &&host.aiAvailability.tts
2972
- &&speech.muted===false
2972
+ &&((host.aiAvailability.tts&&speech.muted===false)
2973
+ ||aiRuntime?.speechActivationPending===true)
2973
2974
  ){
2974
- const aiRuntime=boundChatAI??globalThis.ai;
2975
2975
  if(is.function(aiRuntime?.streamTTS)){
2976
2976
  void Promise.resolve(aiRuntime.streamTTS(text)).catch(
2977
2977
  reportTTSError
@@ -3837,10 +3837,12 @@
3837
3837
  }
3838
3838
  scrollTranscriptToBottom(!eventContext.synthetic);
3839
3839
 
3840
- if(host.aiAvailability.tts&&speech.muted===false){
3841
- const aiRuntime=boundChatAI??globalThis.ai;
3840
+ const aiRuntime=boundChatAI??globalThis.ai;
3841
+ if((host.aiAvailability.tts&&speech.muted===false)
3842
+ ||aiRuntime?.speechActivationPending===true){
3842
3843
  aiRuntime?.stopAudio?.();
3843
- if(is.function(aiRuntime?.resumeAudio)){
3844
+ if(host.aiAvailability.tts&&speech.muted===false
3845
+ &&is.function(aiRuntime?.resumeAudio)){
3844
3846
  void Promise.resolve(aiRuntime.resumeAudio()).catch(
3845
3847
  function reportPlaybackResumeFailure(error){
3846
3848
  reportTTSError(error,'playback-resume');
@@ -392,6 +392,7 @@
392
392
  recordButton.addEventListener('blur', handleRecordBlur, lifecycleListenerOptions);
393
393
  stopButton.addEventListener('click', stop, lifecycleListenerOptions);
394
394
  muteButton.addEventListener('click', muteToggle, lifecycleListenerOptions);
395
+ window.addEventListener('ai-ready', synchronizeAIReadiness, lifecycleListenerOptions);
395
396
  window.addEventListener(
396
397
  'pagehide',
397
398
  handlePageHide,
@@ -417,11 +418,7 @@
417
418
  renderStatus();
418
419
  void observeMicrophonePermission();
419
420
  void aiModuleImport.then(
420
- function synchronizeMutedStateAfterAIImport() {
421
- if (!destroyed) {
422
- synchronizeAIMutedState();
423
- }
424
- },
421
+ synchronizeAIReadiness,
425
422
  function reportAIModuleImportFailure(error) {
426
423
  if (destroyed) {
427
424
  return;
@@ -546,6 +543,17 @@
546
543
  applyRoleTransitions(previousSTTRole, previousTTSRole);
547
544
  }
548
545
 
546
+ function synchronizeAIReadiness() {
547
+ if (destroyed) {
548
+ return;
549
+ }
550
+ settleSuccessfulUnmute();
551
+ continuePendingUnmute();
552
+ synchronizeAIMutedState();
553
+ renderControls();
554
+ renderStatus();
555
+ }
556
+
549
557
  function applyRoleTransitions(previousSTTRole, previousTTSRole) {
550
558
  sttActivationController?.synchronize(sttRole);
551
559
  const sttBecameUnready = previousSTTRole.state === 'ready'
@@ -996,6 +1004,13 @@
996
1004
  ) {
997
1005
  return false;
998
1006
  }
1007
+ if (!is.function(globalThis.ai?.setSpeechMuted)) {
1008
+ pendingUnmute = true;
1009
+ host.muted = true;
1010
+ renderControls();
1011
+ renderStatus();
1012
+ return false;
1013
+ }
999
1014
  const providerSelected = is.string(ttsRole.providerId)
1000
1015
  && ttsRole.providerId.length > 0
1001
1016
  && is.string(ttsRole.modelId)
@@ -1016,8 +1031,12 @@
1016
1031
  }
1017
1032
 
1018
1033
  pendingUnmute = true;
1034
+ const generation = ttsIntentGeneration + 1;
1019
1035
  const requested = await requestTTSIntent('load', 'user');
1020
- if (!requested || destroyed) {
1036
+ if (destroyed || generation !== ttsIntentGeneration) {
1037
+ return false;
1038
+ }
1039
+ if (!requested) {
1021
1040
  pendingUnmute = false;
1022
1041
  host.muted = true;
1023
1042
  stopTTSPlayback();
@@ -1774,7 +1793,12 @@
1774
1793
  return;
1775
1794
  }
1776
1795
 
1796
+ const cancelPendingUnmute = activeTTSIntent?.action === 'load';
1777
1797
  destroyed = true;
1798
+ ttsIntentGeneration += 1;
1799
+ pendingUnmute = false;
1800
+ pendingTTSIntent = null;
1801
+ activeTTSIntent = null;
1778
1802
  runtimeStateAbortController.abort();
1779
1803
  sttActivationController?.destroy();
1780
1804
  window.removeEventListener('pagehide', handlePageHide);
@@ -1785,6 +1809,16 @@
1785
1809
  );
1786
1810
  cancelSTTOperation('component-destroyed');
1787
1811
  stopTTSPlayback();
1812
+ const aiRuntime = globalThis.ai;
1813
+ if (cancelPendingUnmute
1814
+ && aiRuntime?.speechActivationPending === true
1815
+ && is.function(aiRuntime.setSpeechMuted)) {
1816
+ void Promise.resolve(aiRuntime.setSpeechMuted(true)).catch(
1817
+ function reportSpeechTeardownFailure(error) {
1818
+ arcaneLogging.error('Speech activation could not be cancelled during component teardown.', error);
1819
+ }
1820
+ );
1821
+ }
1788
1822
  events.dispose();
1789
1823
  host.componentReady = false;
1790
1824
  renderControls();
@@ -370,7 +370,7 @@ class UserEntity {
370
370
 
371
371
  this.#current_time = v;
372
372
 
373
- this.#persist();
373
+ this.#persist({current_time:v});
374
374
  }
375
375
 
376
376
  /** @returns {number} */
@@ -386,7 +386,7 @@ class UserEntity {
386
386
 
387
387
  this.#last_successful_time = v;
388
388
 
389
- this.#persist();
389
+ this.#persist({last_successful_time:v});
390
390
  }
391
391
 
392
392
  /** @returns {string} */
@@ -822,7 +822,7 @@ class UserEntity {
822
822
  src=this.explicit;
823
823
  }
824
824
 
825
- if(!is.union(src,'object','string')){
825
+ if(!is.union(src,'object|string')){
826
826
  throw new Error('UserEntity.explicit setter expects object or JSON string');
827
827
  }
828
828
 
@@ -857,9 +857,11 @@ class UserEntity {
857
857
 
858
858
  /**
859
859
  * Applies and durably saves an explicit profile update as one serialized
860
- * operation. A failed write restores the prior in-memory values.
860
+ * operation. A callback computes a partial update from the fresh profile;
861
+ * its null result skips the write. It must not await another queued User
862
+ * operation on this instance. A failed write restores the fresh baseline.
861
863
  *
862
- * @param {UserEntityData|string|Object} src
864
+ * @param {UserEntityData|string|Object|function(UserEntityData): (Object|null|Promise<Object|null>)} src
863
865
  * @returns {Promise<UserEntityData>}
864
866
  */
865
867
  updateExplicit(src){
@@ -883,7 +885,19 @@ class UserEntity {
883
885
  this.persist=false;
884
886
  try{
885
887
  this.explicit=baseline;
886
- this.explicit=src;
888
+ }finally{
889
+ this.persist=persist;
890
+ }
891
+
892
+ const conditional=is.function(src);
893
+ const update=conditional?await src(this.explicit):src;
894
+ if(conditional&&update===null){
895
+ return this.explicit;
896
+ }
897
+
898
+ this.persist=false;
899
+ try{
900
+ this.explicit=update;
887
901
  }finally{
888
902
  this.persist=persist;
889
903
  }
@@ -1107,8 +1121,17 @@ class UserEntity {
1107
1121
  /**
1108
1122
  * Internal persistence trigger
1109
1123
  */
1110
- #persist(){
1124
+ #persist(update){
1111
1125
  if(this.persist){
1126
+ // TimeGuard's ordinary timestamp setters must merge only their
1127
+ // fields into the fresh profile, never overwrite another tab's edits.
1128
+ if(!is.undefined(update)){
1129
+ const operation=this.updateExplicit(update);
1130
+ void operation.catch(function reportTimestampSaveFailure(error){
1131
+ arcaneLogging.error('The user timestamp could not be saved.',error);
1132
+ });
1133
+ return operation;
1134
+ }
1112
1135
  return this.save();
1113
1136
  }
1114
1137