arcane-os 0.68.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +9 -4
  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 +8 -3
  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 +94 -26
  18. package/docs/reference/ai/browser-wasm.md +28 -20
  19. package/docs/reference/core-native-packaging.md +54 -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-modules.json +3 -3
  24. package/docs/reference/runtime-components.md +4 -2
  25. package/docs/reference/runtime-modules.md +120 -20
  26. package/docs/reference/sdk-api.md +64 -30
  27. package/package.json +2 -2
  28. package/runtime/arcane/components/theme-switcher.html +1 -1
  29. package/runtime/arcane/modules/AI.js +7 -22
  30. package/runtime/arcane/modules/AIProviderRuntime.js +84 -6
  31. package/runtime/arcane/modules/DBOPFS.js +180 -26
  32. package/schemas/arcane-app.schema.json +10 -0
  33. package/src/app-descriptor.mjs +23 -1
  34. package/src/core/host.mjs +2 -0
  35. package/src/core/hosts/windows/ArcaneHost.cs +46 -0
  36. package/src/core/hosts/windows/ArcaneLauncher.cs +21 -0
  37. package/src/core/runtime.mjs +9 -4
  38. package/src/core/services/packaged-web.mjs +8 -1
  39. package/src/core/shared-host.mjs +500 -0
  40. package/src/native/portable-layout.mjs +66 -34
  41. package/src/native/portable-provider.mjs +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.70.0
4
+
5
+ - Add app-scoped DBOPFS committed-change subscriptions through the existing
6
+ event owner, with targeted cache invalidation and same-origin/storage-partition
7
+ live document notifications. Preserve complete records, successful partial
8
+ batch outcomes, cancellation and existing storage without migration or polling.
9
+ - Keep cold TTS activation owned by its pending preparation interests. Cancelling
10
+ the last pending interest cancels its activation; another preparation or direct
11
+ load retains shared work. Ready models and cache hits keep their existing lifetime.
12
+ - Add explicit same-app shared Core hosts over named pipes or Unix sockets,
13
+ retaining default stdio and one runtime owner. Correlate per-connection requests,
14
+ cancellation and events without confusing equal caller IDs across connections;
15
+ replay current packaged-web state while preserving complete payloads and errors.
16
+ - Store complete browser model/support resources as ordered DBOPFS parts and add
17
+ public model/speech-store `fetchResource()` support. Decision and speech Worker
18
+ resource fetches share the parent store, including nested module Workers, with
19
+ file/shard progress and cancellation that joins cleanup. Existing whole/range
20
+ cache formats remain readable; native ESM and unmapped classic Worker loading
21
+ remain platform-owned. No model/provider/default switch or cache migration.
22
+
23
+ ## 0.69.0
24
+
25
+ - Add explicit `textFormat:'plain'` speech input through prepared speech,
26
+ direct TTS requests and the browser Kokoro provider. Preserve complete input,
27
+ whitespace and formatting characters, original-part cache identity and
28
+ cancellation while leaving default narration behavior unchanged. This input
29
+ option does not change the selected engine's synthesis limits.
30
+ - Add optional `native.window` width, height and resizable configuration.
31
+ Forward logical client dimensions through native manifests; the matching
32
+ Windows host applies initial DPI-aware usable-screen fitting while preserving
33
+ omitted defaults and subsequent application/user sizing. Other platform
34
+ adapters retain their existing sizing behavior.
35
+ - Preserve selected theme-switcher colors during hover by applying ordinary
36
+ hover fill only to unselected enabled buttons. Keep complete labels, focus,
37
+ disabled states, tokens and per-instance configuration unchanged.
38
+
3
39
  ## 0.68.0
4
40
 
5
41
  - Add per-instance theme-switcher `configure({modes,labels})` and initial
package/README.md CHANGED
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
20
20
  share the theme, packaging, event, cancellation,
21
21
  and browser run contracts while retaining their selected application layout.
22
22
 
23
- This checkout defines the `0.68.0` SDK contract. Development applications declare
23
+ This checkout defines the `0.70.0` SDK contract. Development applications declare
24
24
  `arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
25
25
  designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
26
26
  to refresh and compare the installed SDK and lockfile with the published channel;
@@ -469,11 +469,16 @@ keeping apostrophes, commas, and hyphens that join Unicode letters or numbers
469
469
  inside the same segment.
470
470
  Mute, stop, provider transition, and cancellation still govern the whole queue.
471
471
 
472
- Every TTS call automatically removes repeated same formatting marks (`*`, `#`,
472
+ TTS narration removes repeated same formatting marks (`*`, `#`,
473
473
  `_`, backtick, `~`) from its outbound speech-input copy before synthesis.
474
474
  Streaming calls also recognize a run split across chunks. Single marks and
475
- ordinary repeated punctuation remain literal. No application option is needed;
476
- an existing `textFormat` extra is ignored and cannot disable cleanup. Original
475
+ ordinary repeated punctuation remain literal. Complete synthesis calls may
476
+ select `textFormat:'plain'` to preserve exact input: supply it in `prepareTTS`
477
+ options or the payload of `fetchTTS`, provider-runtime TTS requests, or direct
478
+ Kokoro requests. The SDK consumes this control before engine/host dispatch;
479
+ omission keeps the default cleanup. Streaming narration and `SpeechPlayback`
480
+ retain their existing formatting behavior. This option does not change the
481
+ selected engine's input capacity or establish complete upstream synthesis. Original
477
482
  messages and caller payloads remain unchanged for display, storage, and model
478
483
  input. The `arcane-os/speech-text` entrypoint exports the same
479
484
  `MarkdownSpeech` streaming class and `stripSpeechFormatting()` one-pass helper
@@ -1,5 +1,6 @@
1
1
  import Is from '../dependencies/strong-type/index.js';
2
2
  import {createArcaneEventSource} from '../event-manager.mjs';
3
+ import {createModelResourceHost} from './model-resource-bridge.mjs';
3
4
 
4
5
  const is = new Is(false);
5
6
  const STATE_EVENT = 'ai.decisions.state';
@@ -65,6 +66,7 @@ function createDecisionOperation(op) {
65
66
  */
66
67
  class BrowserDecisionModel {
67
68
  #configuration;
69
+ #store;
68
70
  #events;
69
71
  #activation = null;
70
72
  #generation = 0;
@@ -75,13 +77,17 @@ class BrowserDecisionModel {
75
77
  #disposed = false;
76
78
 
77
79
  constructor({family, model, revision = 'main', device = 'webgpu',
78
- dtype = family === 'laya' ? 'fp16' : 'fp32', runtime = {}} = {}) {
80
+ dtype = family === 'laya' ? 'fp16' : 'fp32', runtime = {}, store = null} = {}) {
79
81
  if (family !== 'laya' && family !== 'julia') {
80
82
  throw new TypeError('family must be "laya" or "julia".');
81
83
  }
82
84
  if (runtime === null || !is.object(runtime)) {
83
85
  throw new TypeError('runtime must be an object.');
84
86
  }
87
+ if (store !== null && !is.function(store?.fetchResource)) {
88
+ throw new TypeError('store must expose fetchResource().');
89
+ }
90
+ this.#store = store;
85
91
  const moduleUrl = runtime.moduleUrl === undefined
86
92
  ? DEFAULT_RUNTIME_MODULE
87
93
  : runtime.moduleUrl;
@@ -197,6 +203,8 @@ class BrowserDecisionModel {
197
203
  load: createDecisionOperation('load'),
198
204
  pending: new Map(),
199
205
  evaluations: new Set(),
206
+ resources: null,
207
+ cleanup: null,
200
208
  reason: undefined
201
209
  };
202
210
  this.#activation = activation;
@@ -212,6 +220,22 @@ class BrowserDecisionModel {
212
220
  {type: 'module', name: 'arcane-decisions'}
213
221
  );
214
222
  activation.worker = worker;
223
+ if (this.#store) {
224
+ const store = this.#store;
225
+ activation.resources = createModelResourceHost(
226
+ {
227
+ fetchResource: function fetchDecisionResource(input, options) {
228
+ return store.fetchResource(input, options);
229
+ },
230
+ send: function sendDecisionResource(message) {
231
+ worker.postMessage(message);
232
+ },
233
+ onError: function stopFailedDecisionResource(error) {
234
+ model.#stop(activation, error, 'error');
235
+ }
236
+ }
237
+ );
238
+ }
215
239
  function handleDecisionWorkerMessage(event) {
216
240
  model.#receive(activation, event.data);
217
241
  }
@@ -240,7 +264,7 @@ class BrowserDecisionModel {
240
264
  const id = this.#nextId++;
241
265
  activation.pending.set(id, activation.load);
242
266
  worker.postMessage(
243
- {id, op: 'load', payload: this.#configuration}
267
+ {id, op: 'load', payload: this.#configuration, storedResources: this.#store !== null}
244
268
  );
245
269
  } catch (error) {
246
270
  this.#stop(activation, error, 'error');
@@ -250,6 +274,7 @@ class BrowserDecisionModel {
250
274
 
251
275
  #receive(activation, message) {
252
276
  if (!this.#isCurrent(activation)) return;
277
+ if (activation.resources?.receive(message)) return;
253
278
  const operation = message && activation.pending.get(message.id);
254
279
  if (!operation) {
255
280
  this.#stop(
@@ -308,6 +333,23 @@ class BrowserDecisionModel {
308
333
  this.#state = state;
309
334
  this.#progress = null;
310
335
  this.#error = reason;
336
+ if (activation.resources) {
337
+ const model = this;
338
+ activation.cleanup = activation.resources.close(reason).catch(
339
+ function preserveResourceCleanupFailure(error) {
340
+ activation.reason = new AggregateError([reason, error], 'Unable to settle decision model resources.');
341
+ throw activation.reason;
342
+ }
343
+ );
344
+ activation.cleanup.catch(
345
+ function reportResourceCleanupFailure(error) {
346
+ if (!model.#activation) {
347
+ model.#error = error;
348
+ if (!model.#disposed) model.#publish();
349
+ } else globalThis.console?.error('Decision model resource cleanup failed.', error);
350
+ }
351
+ );
352
+ }
311
353
  if (activation.worker) {
312
354
  for (const [type, listener] of Object.entries(activation.listeners ?? {})) {
313
355
  activation.worker.removeEventListener(type, listener);
@@ -330,6 +372,7 @@ class BrowserDecisionModel {
330
372
  return this.status();
331
373
  } finally {
332
374
  stopObserving();
375
+ if (activation.cleanup) await activation.cleanup;
333
376
  }
334
377
  }
335
378
 
@@ -366,6 +409,7 @@ class BrowserDecisionModel {
366
409
  } finally {
367
410
  stopObserving();
368
411
  activation.evaluations.delete(evaluation);
412
+ if (activation.cleanup) await activation.cleanup;
369
413
  if (this.#isCurrent(activation)) this.#publish();
370
414
  }
371
415
  }
@@ -2,6 +2,8 @@ import Is from "../dependencies/strong-type/index.js";
2
2
  import {
3
3
  normalizeModelSecurity,
4
4
  } from "./model-controller.mjs";
5
+ import { createDbopfsModelPartStore } from "./dbopfs-model-parts.mjs";
6
+ import { createDbopfsResourceStore } from "./dbopfs-model-resources.mjs";
5
7
 
6
8
  const is = new Is(false);
7
9
 
@@ -1090,37 +1092,6 @@ function storageNames(authority, files) {
1090
1092
  });
1091
1093
  }
1092
1094
 
1093
- async function* byteChunks(body, signal) {
1094
- if (is.instanceCheck(body, Uint8Array) || is.instanceCheck(body, ArrayBuffer) || is.arrayBufferView(body)) {
1095
- throwIfAborted(signal);
1096
- yield is.instanceCheck(body, Uint8Array)
1097
- ? body
1098
- : new Uint8Array(body.buffer ?? body, body.byteOffset ?? 0, body.byteLength);
1099
- return;
1100
- }
1101
- if (body && is.function(body.getReader)) {
1102
- const reader = body.getReader();
1103
- const abort = () => void reader.cancel(signal?.reason).catch(() => undefined);
1104
- signal?.addEventListener?.("abort", abort, { once: true });
1105
- try {
1106
- while (true) {
1107
- throwIfAborted(signal);
1108
- const { done, value } = await reader.read();
1109
- if (done) return;
1110
- yield is.instanceCheck(value, Uint8Array) ? value : new Uint8Array(value);
1111
- }
1112
- } finally {
1113
- signal?.removeEventListener?.("abort", abort);
1114
- if (signal?.aborted) await reader.cancel(signal.reason).catch(() => undefined);
1115
- reader.releaseLock?.();
1116
- }
1117
- }
1118
- throw speechError(
1119
- "ARCANE_AI_ARTIFACT_SOURCE_INVALID",
1120
- "A browser speech artifact did not provide readable bytes.",
1121
- );
1122
- }
1123
-
1124
1095
  function tokenizeArtifactGraphModule(source, modulePath) {
1125
1096
  const tokens = [];
1126
1097
  let index = 0;
@@ -1851,6 +1822,12 @@ export function createDbopfsSpeechArtifactStore({
1851
1822
  }
1852
1823
  let tablePromise = null;
1853
1824
  const operationTails = new Map();
1825
+ const parts = createDbopfsModelPartStore({dbopfs, tableName});
1826
+ const resources = createDbopfsResourceStore({
1827
+ dbopfs,
1828
+ tableName,
1829
+ fetchImpl: fetchImpl ?? globalThis.fetch?.bind(globalThis),
1830
+ });
1854
1831
 
1855
1832
  function serializeAuthority(authority, operation) {
1856
1833
  const key = storageKey(authority);
@@ -1916,22 +1893,9 @@ export function createDbopfsSpeechArtifactStore({
1916
1893
  }
1917
1894
  }
1918
1895
 
1919
- async function writeFile(name, body, { signal } = {}) {
1920
- const directory = await table();
1921
- const handle = await directory.getFileHandle(name, { create: true });
1922
- const writable = await handle.createWritable();
1923
- try {
1924
- for await (const chunk of byteChunks(body, signal)) {
1925
- await writable.write(chunk);
1926
- }
1927
- throwIfAborted(signal);
1928
- await writable.close();
1929
- return undefined;
1930
- } catch (error) {
1931
- await writable.abort?.(error).catch(() => undefined);
1932
- await directory.removeEntry(name).catch(() => undefined);
1933
- throw error;
1934
- }
1896
+ async function readArtifact(name, signal) {
1897
+ // Reuse pre-existing complete files without rewriting saved model data.
1898
+ return await parts.read(name, {signal}) ?? await readFile(name);
1935
1899
  }
1936
1900
 
1937
1901
  async function removeUnlocked(authority) {
@@ -1951,6 +1915,9 @@ export function createDbopfsSpeechArtifactStore({
1951
1915
  const results = await Promise.all([
1952
1916
  removeEntry(names.selection),
1953
1917
  ...fileNames.map(removeEntry),
1918
+ ...fileNames.map(function removeArtifactParts(name) {
1919
+ return parts.remove(name);
1920
+ }),
1954
1921
  ]);
1955
1922
  return results.some(Boolean);
1956
1923
  }
@@ -1976,7 +1943,6 @@ export function createDbopfsSpeechArtifactStore({
1976
1943
  const names = storageNames(authority, metadata.files);
1977
1944
  const storedSelection = await readJsonFile(names.selection);
1978
1945
  if (JSON.stringify(storedSelection) !== JSON.stringify(cacheSelection(authority))) {
1979
- await removeUnlocked(authority);
1980
1946
  return null;
1981
1947
  }
1982
1948
  const files = [];
@@ -1996,9 +1962,8 @@ export function createDbopfsSpeechArtifactStore({
1996
1962
  unit: 'files',
1997
1963
  }
1998
1964
  );
1999
- const file = await readFile(names.files[index]);
1965
+ const file = await readArtifact(names.files[index], signal);
2000
1966
  if (!file) {
2001
- await removeUnlocked(authority);
2002
1967
  return null;
2003
1968
  }
2004
1969
  files.push({ descriptor, file });
@@ -2037,7 +2002,6 @@ export function createDbopfsSpeechArtifactStore({
2037
2002
  routing,
2038
2003
  });
2039
2004
  } catch (error) {
2040
- await removeUnlocked(authority);
2041
2005
  throw error;
2042
2006
  }
2043
2007
  }
@@ -2050,19 +2014,36 @@ export function createDbopfsSpeechArtifactStore({
2050
2014
  if (!is.function(fetchFunction)) {
2051
2015
  throw speechError("ARCANE_AI_ARTIFACT_SOURCE_UNAVAILABLE", "Browser fetch is unavailable.");
2052
2016
  }
2053
- await removeUnlocked(authority);
2054
2017
  const installed = [];
2055
2018
  try {
2056
- // This mutable cache selection is written before content. It is only an
2057
- // invalidation record, never a completion, byte-identity, or integrity receipt.
2058
- await writeFile(
2059
- names.selection,
2060
- new TextEncoder().encode(`${JSON.stringify(cacheSelection(authority))}\n`),
2061
- { signal },
2062
- );
2019
+ const selection = cacheSelection(authority);
2020
+ const storedSelection = await readJsonFile(names.selection);
2021
+ if (JSON.stringify(storedSelection) !== JSON.stringify(selection)) {
2022
+ await removeUnlocked(authority);
2023
+ // Selection describes the caller's resources, not download completion.
2024
+ await parts.writePart(
2025
+ names.selection,
2026
+ new TextEncoder().encode(`${JSON.stringify(selection)}\n`),
2027
+ { signal },
2028
+ );
2029
+ }
2063
2030
  for (let index = 0; index < metadata.files.length; index += 1) {
2064
2031
  throwIfAborted(signal);
2065
2032
  const descriptor = metadata.files[index];
2033
+ const cachedFile = await readArtifact(names.files[index], signal);
2034
+ if (cachedFile) {
2035
+ installed.push({descriptor, file: cachedFile});
2036
+ reportArtifactProgress(onProgress, signal, {
2037
+ phase: 'prepare',
2038
+ stage: 'cache',
2039
+ message: 'Opened cached speech file',
2040
+ file: descriptor.path,
2041
+ completed: installed.length,
2042
+ total: metadata.files.length,
2043
+ unit: 'files'
2044
+ });
2045
+ continue;
2046
+ }
2066
2047
  const sourceUrl = graph ? descriptor.sourceUrl : descriptor.url;
2067
2048
  reportArtifactProgress(
2068
2049
  onProgress,
@@ -2139,8 +2120,21 @@ export function createDbopfsSpeechArtifactStore({
2139
2120
  `A speech artifact server returned HTTP ${String(responseStatus)}.`,
2140
2121
  );
2141
2122
  }
2142
- await writeFile(names.files[index], responseBody, { signal });
2143
- const file = await readFile(names.files[index]);
2123
+ const file = await parts.write(names.files[index], responseBody, {
2124
+ signal,
2125
+ onProgress: function reportSpeechShards(shards) {
2126
+ reportArtifactProgress(onProgress, signal, {
2127
+ phase: 'download',
2128
+ stage: 'artifacts',
2129
+ message: 'Storing speech file',
2130
+ file: descriptor.path,
2131
+ completed: installed.length,
2132
+ total: metadata.files.length,
2133
+ unit: 'files',
2134
+ resource: {...shards, file: descriptor.path},
2135
+ });
2136
+ },
2137
+ });
2144
2138
  if (!file) {
2145
2139
  if (graph) {
2146
2140
  throw graphVerificationError(
@@ -2186,7 +2180,8 @@ export function createDbopfsSpeechArtifactStore({
2186
2180
  routing,
2187
2181
  });
2188
2182
  } catch (error) {
2189
- await removeUnlocked(authority).catch(() => undefined);
2183
+ // Complete resources remain available for the next explicit activation.
2184
+ // The active writer owns cleanup of only its unfinished resource.
2190
2185
  throw error;
2191
2186
  }
2192
2187
  }
@@ -2330,6 +2325,7 @@ export function createDbopfsSpeechArtifactStore({
2330
2325
  tableName,
2331
2326
  prepare,
2332
2327
  remove,
2328
+ fetchResource: resources.fetchResource,
2333
2329
  });
2334
2330
  STORES.add(store);
2335
2331
  return store;
@@ -906,7 +906,7 @@ function normalizeSynthesisPayload(payload, authority, speechInputPrepared) {
906
906
  if (shared) {
907
907
  descriptors = genericPayloadDescriptors(
908
908
  payload,
909
- ["model", "voice", "input", "responseFormat", "speed"],
909
+ ["model", "voice", "input", "textFormat", "responseFormat", "speed"],
910
910
  ["model", "input", "responseFormat"],
911
911
  "Shared speech synthesis payload",
912
912
  "tts-synthesis",
@@ -927,7 +927,7 @@ function normalizeSynthesisPayload(payload, authority, speechInputPrepared) {
927
927
  } else {
928
928
  descriptors = genericPayloadDescriptors(
929
929
  payload,
930
- ["model", "voice", "text", "speed"],
930
+ ["model", "voice", "text", "textFormat", "speed"],
931
931
  ["text"],
932
932
  "Browser Kokoro synthesis payload",
933
933
  "tts-synthesis",
@@ -936,7 +936,9 @@ function normalizeSynthesisPayload(payload, authority, speechInputPrepared) {
936
936
  textValue = descriptors.text.value;
937
937
  }
938
938
  const suppliedText = is.string(textValue) ? textValue : "";
939
- const text = speechInputPrepared === true ? suppliedText : stripSpeechFormatting(suppliedText);
939
+ const text = speechInputPrepared === true || descriptors.textFormat?.value === "plain"
940
+ ? suppliedText
941
+ : stripSpeechFormatting(suppliedText);
940
942
  const voiceValue = Object.hasOwn(descriptors, "voice")
941
943
  ? descriptors.voice.value
942
944
  : authority.defaultVoice;
@@ -1314,6 +1316,9 @@ function createBrowserSpeechProvider({
1314
1316
  };
1315
1317
  slot.client = createSpeechWorkerClient({
1316
1318
  role,
1319
+ fetchResource: function fetchSpeechWorkerResource(input, options) {
1320
+ return store.fetchResource(input, options);
1321
+ },
1317
1322
  onTermination: function handleSpeechWorkerTermination(termination) {
1318
1323
  handlePoolTermination(pool, slot, termination);
1319
1324
  },