arcane-os 0.69.0 → 0.71.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 (37) hide show
  1. package/CHANGELOG.md +28 -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/README.md +2 -0
  16. package/docs/reference/ai/browser-decisions.md +57 -3
  17. package/docs/reference/ai/browser-speech.md +45 -17
  18. package/docs/reference/ai/browser-wasm.md +28 -20
  19. package/docs/reference/core-native-packaging.md +13 -0
  20. package/docs/reference/core-runtime.md +9 -0
  21. package/docs/reference/core-shared-host.md +190 -0
  22. package/docs/reference/inventory/package-api.json +3 -3
  23. package/docs/reference/inventory/runtime-components.json +2 -2
  24. package/docs/reference/inventory/runtime-modules.json +6 -6
  25. package/docs/reference/runtime-components.md +35 -1
  26. package/docs/reference/runtime-modules.md +97 -7
  27. package/docs/reference/sdk-api.md +69 -31
  28. package/package.json +2 -2
  29. package/runtime/arcane/components/theme-switcher.html +4 -4
  30. package/runtime/arcane/modules/AI.js +2 -18
  31. package/runtime/arcane/modules/AIProviderRuntime.js +77 -3
  32. package/runtime/arcane/modules/DBOPFS.js +180 -26
  33. package/src/core/host.mjs +2 -0
  34. package/src/core/runtime.mjs +9 -4
  35. package/src/core/services/packaged-web.mjs +8 -1
  36. package/src/core/shared-host.mjs +500 -0
  37. package/src/native/portable-layout.mjs +66 -34
@@ -477,6 +477,13 @@ the caller's ready DBOPFS instance; the separate JSON-compatible `identity`
477
477
  adds application-owned semantic context. Reuse compares complete inputs rather
478
478
  than an SDK version alone.
479
479
 
480
+ Only missing audio joins TTS activation. Each preparation supplies its own
481
+ cancellation lifetime to the existing provider runtime. The last pending
482
+ activation interest cancels the owned load; other preparation/playback or
483
+ joining direct-load interests preserve it. Cache-only preparation never loads
484
+ the model, and cancelling after activation does not unload an already-ready
485
+ model. Provider work owns actual cancellation and cleanup completion.
486
+
480
487
  `state` is `queued`, `preparing`, `ready`, `error`, or `cancelled`.
481
488
  `onState({state,completed,total,segments,error})` synchronously observes
482
489
  preparation progress. `ready` resolves the complete record after every segment
@@ -486,8 +493,8 @@ the retained MIME type. Preparation failure rejects; cancellation rejects as
486
493
  `AbortError` and preserves successfully stored segments. Matching pending
487
494
  requests share synthesis only on the same AI instance and storage group;
488
495
  cancelling one handle does not cancel another active matching caller.
489
- Preparation cancellation prevents later synthesis, but an already-started
490
- shared provider load/unmute has no per-preparation signal and may finish.
496
+ Preparation cancellation prevents later synthesis and releases its activation
497
+ interest. The provider's operation remains responsible for actual cleanup.
491
498
 
492
499
  `playPreparedTTS(prepared,{signal,onState})` can attach immediately. Its returned
493
500
  `{state,error,finished,pause(),resume(),stop()}` handle schedules segments in
@@ -1024,9 +1031,20 @@ The singleton exposes read-only `protocol`, `configured`, and `speechMuted`;
1024
1031
  `supportsTranscriptionCapture(providerId=null)`;
1025
1032
  `createTranscriptionCapture(options={})`;
1026
1033
  `synthesize(payload,options={},preparation={})`; and
1027
- `setSpeechMuted(muted)`. Provider payloads must be data-only; callbacks,
1034
+ `setSpeechMuted(muted,{signal}={})`. Provider payloads must be data-only; callbacks,
1028
1035
  accessors, symbols, and cycles are rejected at the provider boundary.
1029
1036
 
1037
+ `setSpeechMuted(false,{signal})` shares pending activation while retaining an
1038
+ independent interest for each caller. Aborting one signal rejects only that
1039
+ caller's wait. When no pending interests remain, abort reaches the owned load.
1040
+ No-signal callers and direct `load('tts',...)` calls joining that activation
1041
+ also retain it. A rejected load preserves its complete error for the remaining
1042
+ callers. An already-aborted signal rejects before changing mute intent.
1043
+ Explicit `setSpeechMuted(true)` still cancels shared activation and unloads TTS;
1044
+ once accepted, that mute cleanup runs to completion rather than being undone
1045
+ by cancellation of its caller's signal. Completed activation is not unloaded
1046
+ merely because a former observer later aborts.
1047
+
1030
1048
  Selection options admit `localOnly=false`; inspection admits
1031
1049
  `{localOnly=false,signal=null}`; startup admits
1032
1050
  `{startLanguageModel=true,startMuted=true,startTranscription=false,signal=null}` (including an omitted
@@ -2203,6 +2221,59 @@ default `DBOPFS`; installs `window.dbopfs`, emits `dbopfs-ready`; table/file/bac
2203
2221
 
2204
2222
  Exact exports: `DBOPFS_EVENT_TYPES`, `DBOPFS_REASONS`, `default`.
2205
2223
 
2224
+ `subscribeChanges(listener, {signal, once})` observes
2225
+ `DBOPFS_EVENT_TYPES.change` (`dbopfs-change`) through the existing canonical
2226
+ `arcane-os/event-manager` source. It returns an idempotent unsubscribe function
2227
+ with `.dispose()`; aborting the supplied signal also removes the subscription.
2228
+ Each listener receives an event whose `detail` contains:
2229
+
2230
+ | Field | Meaning |
2231
+ | --- | --- |
2232
+ | `applicationId`, `storagePath` | Existing application identity and `apps/<application-id>` OPFS scope. |
2233
+ | `tableName`, `directoryName` | Logical table and physical directory; `memories` / `memory` retain their existing alias. |
2234
+ | `fileName` | Complete record filename, or `null` for a removed table. |
2235
+ | `action` | `write`, `append`, `delete`, or `table-delete`. A write does not claim whether a record was newly created. |
2236
+ | `originId`, `sequence`, `changeId` | Per-document origin and incrementing committed occurrence, shared by local and remote notifications for that occurrence. These are correlation fields, separate from realm-local canonical event-source IDs. |
2237
+ | `remote` | `false` at the writing DBOPFS source, `true` when received from another live document. |
2238
+
2239
+ A write is published after writable-stream `close()` succeeds, or after the
2240
+ existing OPFS worker acknowledges its completed write/flush/close. A removal
2241
+ is published after OPFS removes the record or table. The affected cached record
2242
+ or table, including alias keys and removed-table handles, is invalidated before
2243
+ listeners run. A read already in progress may still resolve its earlier
2244
+ snapshot; it cannot repopulate the cache after an intervening delivered change.
2245
+ Notification metadata does not contain, rewrite, or reread record bodies.
2246
+
2247
+ `set()` and raw `writeFile()` publish one occurrence per completed write;
2248
+ `setMany()`, `deleteMany()`, clear operations, and restore retain their existing
2249
+ per-record results and publish only their actual successful mutations. A
2250
+ partial batch is not a transaction: successful records are announced even when
2251
+ siblings fail. Missing records, absent/non-empty empty-table removal, failed
2252
+ writes/removals, and reads emit no committed-change event. `deleteTable()` keeps
2253
+ its existing return/error-reporting behavior; its returned `true` alone does
2254
+ not imply that a committed-change event was emitted.
2255
+
2256
+ Within one document, subscribe through this source or the canonical event
2257
+ manager, rather than consuming both as separate changes. Across documents,
2258
+ DBOPFS owns one native `BroadcastChannel` named from the application storage
2259
+ path. Native storage-partition matching plus that path selects the receiving
2260
+ documents. A remote receiver invalidates its affected cache before publishing
2261
+ locally and never rebroadcasts the occurrence. There is no echo back to the
2262
+ writing DBOPFS source. Ordinary storage success remains success if a transport
2263
+ notification fails; the notification error is reported separately.
2264
+
2265
+ Subscriptions are live-only: subscribe before the application's initial read,
2266
+ then let the application map the table/file/action to its own records and UI.
2267
+ DBOPFS performs no polling, initial full-table read, automatic picker refresh,
2268
+ or event-history replay. The channel lasts for the owning page, independent of
2269
+ individual listener lifetimes, so a document that only writes still announces
2270
+ its commits. `pagehide` closes the channel; `pageshow` after suspension reopens
2271
+ it and clears only that document's in-memory caches. Resuming a page does not
2272
+ fabricate a storage mutation. Applications own any resume-time view refresh.
2273
+ Missed events during suspension, process termination, or realm loss are not
2274
+ replayed; the stored records remain the authority. Direct OPFS writes made
2275
+ outside DBOPFS are not observed by this contract.
2276
+
2206
2277
  `createCompressedPNG({tableNames, additionalTables, signal})` returns an
2207
2278
  `image/png` Blob without downloading or writing records. Omitted `tableNames`
2208
2279
  selects all discovered saved tables; `[]` selects none. Named absent tables
@@ -2244,16 +2315,35 @@ unreadable row remains in its original string form so the owning application
2244
2315
  can display, diagnose, or recover it without silent data loss. DOM and storage
2245
2316
  errors remain observable. The logical `memories` table continues to map to the
2246
2317
  physical `memory` directory, and successful or already-absent empty-table
2247
- removal invalidates both alias and cached-handle state. Transport: OPFS,
2248
- DBOPFSWorker, Compression Streams.
2318
+ removal invalidates both alias and cached-handle state. Cross-document change
2319
+ delivery requires native `BroadcastChannel` in the same origin and storage
2320
+ partition, with the same application scope; separate WebView profiles do not
2321
+ share this transport. If it is unavailable, local change subscriptions and
2322
+ ordinary DBOPFS storage continue, without cross-document notification or a
2323
+ polling replacement. Transport: OPFS, DBOPFSWorker, Compression Streams, native
2324
+ BroadcastChannel, and the existing event-pubsub-backed canonical event owner.
2249
2325
  [Deep protocol details](protocols.md).
2250
2326
 
2251
2327
  ### Example
2252
2328
 
2253
2329
  ```javascript
2254
- import * as module from '/arcane/modules/DBOPFS.js';
2330
+ import '/arcane/modules/DBOPFS.js';
2331
+
2332
+ await dbopfs.readyPromise;
2333
+ const lifetime = new AbortController();
2334
+ const stop = dbopfs.subscribeChanges(function showCommittedParade(event) {
2335
+ const change = event.detail;
2336
+ if (change.tableName === 'moon_bus') {
2337
+ console.log(change.action, change.fileName, change.remote);
2338
+ }
2339
+ }, {signal: lifetime.signal});
2255
2340
 
2256
- console.log(Object.keys(module));
2341
+ await dbopfs.set('moon_bus', 'squid-parade.json', {
2342
+ destination: 'The moon', passengers: ['Captain Squid', 'One confused lobster']
2343
+ });
2344
+ // Another live page using this app/storage scope receives the same occurrence.
2345
+ // The application can read that one record when its view needs the body.
2346
+ stop(); // lifetime.abort() is the equivalent subscription cleanup.
2257
2347
  ```
2258
2348
 
2259
2349
  ## DBOPFSDocumentLibrary.js
@@ -136,7 +136,7 @@ Root applications import `AI` through `arcane-os/ai` in the managed browser map.
136
136
  `ai.prepareTTSPlayback(payload,signal)` silently prepares real Blob audio or a
137
137
  native speech descriptor for playback. Native browser voices expose no audio
138
138
  file; use this path or shared streaming/SpeechPlayback for native output.
139
- `ai.prepareTTS({parts,storage,identity,signal,onState})` prepares complete
139
+ `ai.prepareTTS({parts,storage,identity,textFormat,signal,onState})` prepares complete
140
140
  punctuation segments without playing them and optionally saves/reuses their
141
141
  audio through application-owned DBOPFS. Its immediate handle exposes ordered
142
142
  segments, preparation state, `ready`, `getAudio(index)`, and `cancel()`.
@@ -149,6 +149,17 @@ for complete inputs, storage ownership, progress, errors, and cancellation,
149
149
  and [AI.js](runtime-modules.md#aijs) for the managed runtime contract.
150
150
  These are AI instance methods, not new npm export names.
151
151
 
152
+ Complete synthesis calls can select `textFormat:'plain'` to preserve supplied
153
+ speech text, including repeated formatting marks and surrounding whitespace.
154
+ This applies to `ai.fetchTTS(payload,signal)`, audio-file preparation through
155
+ `ai.prepareTTSPlayback(payload,signal)`, durable `ai.prepareTTS(...)`, provider
156
+ runtime TTS requests and direct Kokoro synthesis. The SDK consumes the control
157
+ before engine dispatch; durable preparation retains the existing punctuation
158
+ segmentation. Omitted or other values retain automatic speech cleanup.
159
+ Streaming `streamTTS`, `finishTTS` and shared `SpeechPlayback` retain their
160
+ automatic cleanup; this option does not change upstream engine capacity.
161
+ See [exact complete synthesis input](ai/browser-speech.md#exact-complete-synthesis-input).
162
+
152
163
  ## Shared operation contract
153
164
 
154
165
  High-level operations accept one options object. Common fields are `workspaceRoot`, `appId`, `target`, `signal`, and `onEvent`; only fields meaningful to that operation are consumed. Long work acknowledges first, owns its child tasks, keeps event delivery ordered and backpressured, emits progress or heartbeats, observes cancellation where safe, and rejects with `ArcaneError` on failure.
@@ -7530,6 +7541,12 @@ published SDK/runtime dependencies and the canonical Core entry/client into a
7530
7541
  fresh output directory. Service factories are imported only when a real native
7531
7542
  host later launches the generated Core entry, never during packaging.
7532
7543
 
7544
+ An optional `appDescriptor.native.window` record carries `width`, `height` and
7545
+ `resizable` into `manifest.window`. Dimensions are positive integral logical
7546
+ client units; `resizable` is an optional boolean. Omitted fields remain omitted.
7547
+ The portable payload records the selection; the selected platform host owns
7548
+ its window behavior. See [initial native window size](core-native-packaging.md#initial-native-window-size).
7549
+
7533
7550
  ### Availability and normalization
7534
7551
 
7535
7552
  **Node; produces a portable payload rather than an executable host.** Existing
@@ -7641,6 +7658,16 @@ the selected output root. There is no compiler or OS checkout requirement for
7641
7658
  application builds. The build machine needs its normal Node/npm toolchain and
7642
7659
  `tar`; execution needs Windows x64, .NET Framework 4.6.2 or later and WebView2.
7643
7660
 
7661
+ The matching Windows host consumes `appDescriptor.native.window` through
7662
+ `manifest.window`. Optional `width` and `height` select initial client dimensions
7663
+ in 96-DPI logical units; the host scales them for its actual DPI and fits the
7664
+ outer window within the current screen's usable area. Optional `resizable:false`
7665
+ selects a fixed-size border and disables maximizing. Omitted fields preserve
7666
+ the existing host defaults. Initial sizing preserves a window already restored
7667
+ to a non-normal state and does not add saved-bounds storage. The initial-fit
7668
+ logic is not reapplied during later user resizing. See [initial native window size](core-native-packaging.md#initial-native-window-size)
7669
+ for a complete descriptor example and the other-platform boundary.
7670
+
7644
7671
  The returned `arcane-native-builder/1` provider exposes `describe`, `doctor`,
7645
7672
  `prepare`, `build`, `verify` and `run`. Build returns `{app,target,manifest}`
7646
7673
  with the actual directory at `target.rootDir`. Verification reads the artifact;
@@ -8844,13 +8871,14 @@ occurrences use source `ai-model-controller`; every load or unload owns one
8844
8871
  non-null operation ID shared by its state and progress occurrences. Public
8845
8872
  progress retains the exact selected `modelId`, `phase`, state, and nested
8846
8873
  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.
8874
+ `completed`, `total`, and `unit:'files'`, with complete per-file `members`
8875
+ reporting stored shards. A member's `total` remains `null` until its complete
8876
+ response is stored, unless the HTTP range plan already establishes its shard
8877
+ total. `activeTransfers`, `transferLimit`, and `transferMode` describe the
8878
+ active download work. Closed shards, active-worker changes, and completion
8879
+ boundaries publish semantic progress; the existing heartbeat repeats the latest
8880
+ record. These fields describe files and shards, without byte-based display
8881
+ requirements or estimated transfer rates.
8854
8882
  Accessor-bearing provider status
8855
8883
  and malformed progress reject with `ARCANE_AI_PROVIDER_STATUS_INVALID` and
8856
8884
  `ARCANE_AI_PROVIDER_PROGRESS_INVALID`. After disposal, lifecycle operations fail with
@@ -9443,8 +9471,8 @@ console.log(provider.status().state); // unloaded
9443
9471
  ### Overview
9444
9472
 
9445
9473
  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
9474
+ multi-file cache without renaming its public methods. Every new full response
9475
+ or Range transfer is committed in ordered parts. An interrupted set
9448
9476
  is not a cache hit, but its completed members remain available to the next
9449
9477
  install so only missing work is fetched.
9450
9478
 
@@ -9462,25 +9490,26 @@ workers
9462
9490
  when the probe returns a usable total from exposed `Content-Range` or optional
9463
9491
  descriptor `bytes`. The mutable
9464
9492
  result contains `kind`, `tableName`, `downloadConcurrency`, the original
9465
- `adapter`, and `ready`, `install`, `ensure`, and `remove`. `ensure()` returns
9493
+ `adapter`, and `ready`, `install`, `ensure`, `remove`, and `fetchResource`. `ensure()` returns
9466
9494
  `files` and `cache:'cached'|'installed'`. It preserves the complete model content.
9467
9495
  `offline:true` never downloads and rejects a miss with
9468
9496
  `ARCANE_AI_MODEL_OFFLINE_MISS`.
9469
9497
 
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.
9498
+ When supplied, `onProgress` receives ordered file progress and `members`
9499
+ records with each file's `name`, committed `completed` parts, `total`, and
9500
+ `unit:'shards'`. Streaming totals remain `null` until the response finishes;
9501
+ previously complete files identify `state:'cached'`. `activeTransfers` is the
9502
+ current transfer-worker count, `transferLimit` is selected concurrency, and
9503
+ `transferMode` is `probing`, `files`, `ranges`, or `single`. Closed-part and
9504
+ lifecycle changes publish promptly, with an existing 250 ms heartbeat. No
9505
+ byte progress, transfer rate, or estimated completion time is produced.
9506
+
9507
+ `fetchResource(input,{signal,onProgress,...requestOptions})` returns
9508
+ `{file,status,statusText,headers,url,redirected}` for a complete stored Fetch
9509
+ response. SDK browser model loaders use this shared owner for configuration,
9510
+ tokenizer, graph, external-data, and other fetched resources. Successful
9511
+ complete GET resources may be reused; supported HTTP ranges resume closed
9512
+ parts after interruption. Request framing stays at the transport boundary.
9484
9513
 
9485
9514
  ### Availability and normalization
9486
9515
 
@@ -9491,9 +9520,9 @@ resumable Range parts. For any source member, a followed redirect that
9491
9520
  turns the first `bytes=0-0` probe into `200` triggers one direct probe of the
9492
9521
  final URL. Confirmed support cancels the original body and starts the Range
9493
9522
  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
9523
+ without requiring a readable `Content-Length`. A missing `Content-Range` can
9495
9524
  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
9525
+ store uses one full fetch streamed into ordered persistent parts. A confirmed source member is divided into
9497
9526
  deterministic OPFS Range parts of roughly 4 MB each, up to 4,096 parts. A
9498
9527
  monolithic source may use at most
9499
9528
  `downloadConcurrency` Range workers; split sources keep one Range worker per
@@ -9502,7 +9531,7 @@ in order as one logical Blob. A later non-206, contradictory exposed Range
9502
9531
  response, or incorrectly framed body fails rather than assembling partial
9503
9532
  content. On failure or cancellation, peers settle while completed shards and
9504
9533
  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
9534
+ refresh. Existing whole-file caches remain readable without migration. A persisted Range part whose length does not match its requested HTTP
9506
9535
  frame is discarded and fetched again. A complete whole member supersedes its
9507
9536
  redundant current Range fragments. Cleanup failure is warned without
9508
9537
  hiding the usable model. Wllama requires every selected model Blob or File to
@@ -9784,7 +9813,7 @@ retains the complete list of deletions already completed.
9784
9813
 
9785
9814
  Adapts an existing DBOPFS instance into a complete caller-selected speech
9786
9815
  runtime/model store. It serializes each selected model with an exclusive Web
9787
- Lock, removes partial state after failure, stores ordinary mutable selection
9816
+ Lock, preserves complete peer files and closed parts after failure, stores ordinary mutable selection
9788
9817
  metadata before content, and treats a changed file inventory or source mapping
9789
9818
  as a cache miss that is downloaded again.
9790
9819
 
@@ -9794,7 +9823,7 @@ as a cache miss that is downloaded again.
9794
9823
  createDbopfsSpeechArtifactStore({ dbopfs, tableName='arcane_ai_browser_speech', fetchImpl=null, objectUrlFactory=null }={})
9795
9824
  ```
9796
9825
 
9797
- The mutable result is `{protocol,tableName,prepare,remove}`.
9826
+ The mutable result is `{protocol,tableName,prepare,remove,fetchResource}`.
9798
9827
  `prepare(authority,{signal,onProgress,offline=false,security})` uses a complete
9799
9828
  compatible cache or downloads every declared file, preserves complete content
9800
9829
  and materializes object URLs, then returns
@@ -9804,9 +9833,18 @@ needs those URLs. `offline:true` never fetches and rejects a miss with
9804
9833
  authority's files and selection metadata. Optional `onProgress` receives
9805
9834
  preparation and download records with `phase`, `stage`, `message`, and the
9806
9835
  current `file` when available. File operations report actual `completed` and
9807
- `total` inventory counts with `unit:'files'`; module preparation uses
9836
+ `total` inventory counts with `unit:'files'`, and `resource` carries the current
9837
+ closed-part progress with `unit:'shards'`; module preparation uses
9808
9838
  `total:null`. Cancellation stops progress along with the owning operation.
9809
9839
 
9840
+ New files stream into ordered DBOPFS parts with no required response length.
9841
+ Existing complete whole files stay readable without migration.
9842
+ `fetchResource(input,{signal,onProgress,...requestOptions})` returns
9843
+ `{file,status,statusText,headers,url,redirected}` and gives the dedicated
9844
+ speech Worker the same complete resource storage owner for runtime fetches.
9845
+ Native ESM imports and unmapped classic Worker internals retain their platform
9846
+ loader; they are not claimed as intercepted HTTP fetches.
9847
+
9810
9848
  ### Availability and normalization
9811
9849
 
9812
9850
  **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.71.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",
@@ -15,8 +15,8 @@
15
15
  max-inline-size:100%;
16
16
  padding:.2rem;
17
17
  border:1px solid var(--app-bar-border,var(--border-color,rgba(255,255,255,.25)));
18
- border-radius:1.6rem;
19
- background:color-mix(in srgb,currentColor 8%,transparent);
18
+ border-radius:var(--theme-switcher-radius,1.6rem);
19
+ background:var(--theme-switcher-background,color-mix(in srgb,currentColor 8%,transparent));
20
20
  }
21
21
 
22
22
  button {
@@ -28,11 +28,11 @@
28
28
  min-block-size:2.75rem;
29
29
  padding:.5rem .75rem;
30
30
  border:0;
31
- border-radius:1.4rem;
31
+ border-radius:var(--theme-switcher-option-radius,1.4rem);
32
32
  background:transparent;
33
33
  color:inherit;
34
34
  cursor:pointer;
35
- font:700 .875rem/1.25 system-ui,sans-serif;
35
+ font:var(--theme-switcher-font,700 .875rem/1.25 system-ui,sans-serif);
36
36
  text-align:center;
37
37
  white-space:normal;
38
38
  overflow-wrap:anywhere;
@@ -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) {