arcane-os 0.69.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/browser-decisions.mjs +46 -2
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +58 -62
  5. package/browser-runtime/ai/browser-speech-providers.mjs +3 -0
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +76 -164
  7. package/browser-runtime/ai/dbopfs-model-parts.mjs +344 -0
  8. package/browser-runtime/ai/dbopfs-model-resources.mjs +190 -0
  9. package/browser-runtime/ai/decision-runtime.mjs +17 -1
  10. package/browser-runtime/ai/decision-worker.mjs +21 -1
  11. package/browser-runtime/ai/model-resource-bridge.mjs +231 -0
  12. package/browser-runtime/ai/speech-worker-client.mjs +68 -28
  13. package/browser-runtime/ai/speech-worker-runtime.mjs +212 -97
  14. package/browser-runtime/core/contracts.mjs +1 -1
  15. package/docs/reference/ai/browser-decisions.md +57 -3
  16. package/docs/reference/ai/browser-speech.md +45 -17
  17. package/docs/reference/ai/browser-wasm.md +28 -20
  18. package/docs/reference/core-native-packaging.md +13 -0
  19. package/docs/reference/core-runtime.md +9 -0
  20. package/docs/reference/core-shared-host.md +190 -0
  21. package/docs/reference/inventory/runtime-modules.json +3 -3
  22. package/docs/reference/runtime-modules.md +97 -7
  23. package/docs/reference/sdk-api.md +41 -30
  24. package/package.json +2 -2
  25. package/runtime/arcane/modules/AI.js +2 -18
  26. package/runtime/arcane/modules/AIProviderRuntime.js +77 -3
  27. package/runtime/arcane/modules/DBOPFS.js +180 -26
  28. package/src/core/host.mjs +2 -0
  29. package/src/core/runtime.mjs +9 -4
  30. package/src/core/services/packaged-web.mjs +8 -1
  31. package/src/core/shared-host.mjs +500 -0
  32. package/src/native/portable-layout.mjs +66 -34
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
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
+
3
23
  ## 0.69.0
4
24
 
5
25
  - Add explicit `textFormat:'plain'` speech input through prepared speech,
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.69.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;
@@ -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;
@@ -1316,6 +1316,9 @@ function createBrowserSpeechProvider({
1316
1316
  };
1317
1317
  slot.client = createSpeechWorkerClient({
1318
1318
  role,
1319
+ fetchResource: function fetchSpeechWorkerResource(input, options) {
1320
+ return store.fetchResource(input, options);
1321
+ },
1319
1322
  onTermination: function handleSpeechWorkerTermination(termination) {
1320
1323
  handlePoolTermination(pool, slot, termination);
1321
1324
  },