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
@@ -1,4 +1,5 @@
1
1
  import Is from "../dependencies/strong-type/index.js";
2
+ import { createModelResourceClient, createModelResourceHost } from './model-resource-bridge.mjs';
2
3
 
3
4
  const is = new Is(false);
4
5
 
@@ -922,14 +923,16 @@ function createOrdinaryRouteReader(
922
923
  scope,
923
924
  configuration,
924
925
  originalFetch,
925
- originalCaches,
926
+ resourceClient,
927
+ getSignal,
928
+ getProgress,
929
+ sourceUrl,
926
930
  ) {
927
931
  const routeTable = createOrdinaryRoutes(scope, configuration);
928
- const nativeCaches = new Map();
929
932
  const RequestConstructor = scope.Request ?? globalThis.Request;
930
933
 
931
934
  function sourceBase(modulePath) {
932
- return routeTable.filesByPath.get(modulePath)?.sourceUrl ?? scope.location?.href;
935
+ return routeTable.filesByPath.get(modulePath)?.sourceUrl ?? sourceUrl;
933
936
  }
934
937
 
935
938
  function nativeInput(input, modulePath) {
@@ -951,52 +954,40 @@ function createOrdinaryRouteReader(
951
954
  return originalFetch(mappedInput, init);
952
955
  }
953
956
 
954
- async function nativeCache(name) {
955
- if (!originalCaches || !is.function(originalCaches.open)) return null;
956
- if (!nativeCaches.has(name)) {
957
- nativeCaches.set(name, Promise.resolve(originalCaches.open.call(originalCaches, name)));
957
+ function fetchResource(input, init, modulePath) {
958
+ const options = resourceOptions(input, init);
959
+ const resolution = resolve(input, modulePath);
960
+ if (resolution) return responseFor(resolution, input, options);
961
+ const target = nativeInput(input, modulePath);
962
+ const url = ordinaryRequestUrl(target, scope, sourceBase(modulePath));
963
+ if (url?.startsWith('blob:') || url?.startsWith('data:')) {
964
+ return originalFetch(target, options);
958
965
  }
959
- return nativeCaches.get(name);
966
+ return resourceClient.fetch(target, options);
967
+ }
968
+
969
+ function resourceOptions(input, init) {
970
+ const signal = getSignal();
971
+ const reportProgress = getProgress();
972
+ if (!signal && !reportProgress) return init;
973
+ const requestSignal = init?.signal ?? input?.signal;
974
+ return {
975
+ ...init,
976
+ signal: signal && requestSignal && requestSignal !== signal
977
+ ? AbortSignal.any([requestSignal, signal])
978
+ : signal ?? requestSignal,
979
+ onProgress: function reportResourceProgress(progress) {
980
+ reportProgress?.(progress);
981
+ if (init?.onProgress !== reportProgress) init?.onProgress?.(progress);
982
+ },
983
+ };
960
984
  }
961
985
 
962
- function cacheForName(name, modulePath) {
963
- return completeValue({
964
- async match(input, options) {
965
- const resolution = resolve(input, modulePath);
966
- if (resolution) return responseFor(resolution, input);
967
- const cache = await nativeCache(name);
968
- return cache?.match(nativeInput(input, modulePath), options);
969
- },
970
- async put(input, response) {
971
- const cache = await nativeCache(name);
972
- if (!cache) throw new TypeError("Browser CacheStorage is unavailable.");
973
- return cache.put(nativeInput(input, modulePath), response);
974
- },
975
- async add(input) {
976
- const cache = await nativeCache(name);
977
- if (!cache) throw new TypeError("Browser CacheStorage is unavailable.");
978
- return cache.add(nativeInput(input, modulePath));
979
- },
980
- async addAll(inputs) {
981
- const cache = await nativeCache(name);
982
- if (!cache) throw new TypeError("Browser CacheStorage is unavailable.");
983
- return cache.addAll(Array.from(inputs, (input) => nativeInput(input, modulePath)));
984
- },
985
- async delete(input, options) {
986
- const cache = await nativeCache(name);
987
- return cache ? cache.delete(nativeInput(input, modulePath), options) : false;
988
- },
989
- async keys(input, options) {
990
- const cache = await nativeCache(name);
991
- if (!cache) return [];
992
- return input === undefined
993
- ? cache.keys()
994
- : cache.keys(nativeInput(input, modulePath), options);
995
- },
996
- });
986
+ function fetchResourceRecord(input, init) {
987
+ return resourceClient.fetchResource(input, resourceOptions(input, init));
997
988
  }
998
989
 
999
- return completeValue({ cacheForName, nativeInput, resolve, responseFor });
990
+ return completeValue({ fetchResource, fetchResourceRecord, nativeInput, resolve, responseFor });
1000
991
  }
1001
992
 
1002
993
  function createOrdinaryArtifactModuleRouter(
@@ -1004,7 +995,6 @@ function createOrdinaryArtifactModuleRouter(
1004
995
  configuration,
1005
996
  role,
1006
997
  reader,
1007
- originalFetch,
1008
998
  originalWorker,
1009
999
  ) {
1010
1000
  const nestedWorkers = new Set();
@@ -1021,14 +1011,11 @@ function createOrdinaryArtifactModuleRouter(
1021
1011
  },
1022
1012
 
1023
1013
  fetch(modulePath, input, init) {
1024
- const resolution = reader.resolve(input, modulePath);
1025
- return resolution
1026
- ? reader.responseFor(resolution, input, init)
1027
- : originalFetch(reader.nativeInput(input, modulePath), init);
1014
+ return reader.fetchResource(input, init, modulePath);
1028
1015
  },
1029
1016
 
1030
- openCache(modulePath, name) {
1031
- return Promise.resolve(reader.cacheForName(name, modulePath));
1017
+ async openCache() {
1018
+ throw new Error('Browser CacheStorage is unavailable in this speech Worker; resources use DBOPFS through fetch.');
1032
1019
  },
1033
1020
 
1034
1021
  createWorker(modulePath, specifier, options = {}) {
@@ -1041,15 +1028,60 @@ function createOrdinaryArtifactModuleRouter(
1041
1028
  );
1042
1029
  }
1043
1030
  const resolution = reader.resolve(specifier, modulePath);
1044
- if (!resolution) {
1031
+ if (!resolution && options?.type !== 'module') {
1045
1032
  return new originalWorker(reader.nativeInput(specifier, modulePath), options);
1046
1033
  }
1047
1034
  const workerOptions = options && is.object(options)
1048
1035
  ? { ...options, type: "module" }
1049
1036
  : { type: "module" };
1050
1037
  const worker = new originalWorker(nestedWorkerUrl(role), workerOptions);
1038
+ const resourceHost = createModelResourceHost({
1039
+ fetchResource: reader.fetchResourceRecord,
1040
+ send: function sendNestedSpeechResource(message) {
1041
+ worker.postMessage(message);
1042
+ },
1043
+ onError: function nestedResourceTransportFailed(error) {
1044
+ const failure = workerError(
1045
+ 'ARCANE_AI_WORKER_MESSAGE_ERROR',
1046
+ 'Unable to deliver a resource to the nested speech Worker.',
1047
+ error,
1048
+ 'speech-module-worker-resource-message-rejected',
1049
+ );
1050
+ void terminateNestedSpeechWorker(failure);
1051
+ worker.dispatchEvent?.(nestedWorkerFailureEvent(scope, failure));
1052
+ },
1053
+ });
1054
+ const terminateWorker = worker.terminate.bind(worker);
1055
+ let closing = null;
1056
+ function terminateNestedSpeechWorker(reason) {
1057
+ if (closing) return closing;
1058
+ closing = Promise.allSettled([
1059
+ Promise.resolve().then(function closeNestedResources() {
1060
+ return resourceHost.close(reason);
1061
+ }),
1062
+ Promise.resolve().then(function stopNestedWorker() {
1063
+ return terminateWorker();
1064
+ }),
1065
+ ]).then(function finishNestedTermination(settlements) {
1066
+ const failures = [];
1067
+ for (const settlement of settlements) {
1068
+ if (settlement.status === 'rejected') failures.push(settlement.reason);
1069
+ }
1070
+ if (failures.length) throw new AggregateError(failures, 'Unable to terminate the nested speech Worker.', { cause: reason });
1071
+ });
1072
+ // Upstream Worker.terminate() is fire-and-forget; the parent joins it.
1073
+ closing.catch(function reportNestedTermination(error) {
1074
+ scope.console?.error?.('Speech Worker resource cleanup failed.', error);
1075
+ });
1076
+ return closing;
1077
+ }
1078
+ worker.terminate = terminateNestedSpeechWorker;
1051
1079
  nestedWorkers.add(worker);
1052
1080
  const onBootstrapMessage = (event) => {
1081
+ if (resourceHost.receive(event.data)) {
1082
+ event.stopImmediatePropagation?.();
1083
+ return;
1084
+ }
1053
1085
  if (event.data?.protocol !== NESTED_WORKER_PROTOCOL
1054
1086
  || event.data?.event !== "artifact-module-worker-bootstrap-rejected") return;
1055
1087
  event.stopImmediatePropagation?.();
@@ -1070,11 +1102,11 @@ function createOrdinaryArtifactModuleRouter(
1070
1102
  protocol: NESTED_WORKER_PROTOCOL,
1071
1103
  op: "initialize-artifact-module-worker",
1072
1104
  role,
1073
- targetPath: resolution.file.path,
1105
+ targetPath: resolution?.file.path,
1106
+ targetUrl: resolution ? undefined : reader.nativeInput(specifier, modulePath),
1074
1107
  configuration,
1075
1108
  });
1076
1109
  } catch (error) {
1077
- nestedWorkers.delete(worker);
1078
1110
  worker.terminate();
1079
1111
  throw workerError(
1080
1112
  "ARCANE_AI_WORKER_MESSAGE_ERROR",
@@ -1088,24 +1120,28 @@ function createOrdinaryArtifactModuleRouter(
1088
1120
  });
1089
1121
  return completeValue({
1090
1122
  router,
1091
- transformersCache: reader.cacheForName("transformers-cache", null),
1092
- cleanup() {
1123
+ async cleanup() {
1124
+ const terminations = [];
1093
1125
  for (const worker of nestedWorkers) {
1094
- try {
1095
- worker.terminate();
1096
- } catch {
1097
- // The owning speech Worker is already being torn down.
1098
- }
1126
+ terminations.push(worker.terminate());
1099
1127
  }
1100
1128
  nestedWorkers.clear();
1129
+ const settlements = await Promise.allSettled(terminations);
1130
+ const failures = [];
1131
+ for (const settlement of settlements) {
1132
+ if (settlement.status === 'rejected') failures.push(settlement.reason);
1133
+ }
1134
+ if (failures.length) throw new AggregateError(failures, 'Unable to close nested speech Workers.');
1101
1135
  },
1102
1136
  });
1103
1137
  }
1104
1138
 
1105
- function installOrdinaryArtifactModuleRouter(scope, configuration, role) {
1139
+ function installOrdinaryArtifactModuleRouter(scope, configuration, role, send, sourceUrl = scope.location?.href) {
1106
1140
  const originalFetch = scope.fetch?.bind(scope);
1107
1141
  const originalWorker = scope.Worker;
1108
- const originalCaches = scope.caches;
1142
+ const resourceClient = createModelResourceClient({ send });
1143
+ let requestSignal = null;
1144
+ let reportProgress = null;
1109
1145
  if (!is.function(originalFetch)) {
1110
1146
  throw workerError(
1111
1147
  "ARCANE_AI_PROVIDER_UNAVAILABLE",
@@ -1118,19 +1154,63 @@ function installOrdinaryArtifactModuleRouter(scope, configuration, role) {
1118
1154
  scope,
1119
1155
  configuration,
1120
1156
  originalFetch,
1121
- originalCaches,
1157
+ resourceClient,
1158
+ function getRequestSignal() { return requestSignal; },
1159
+ function getResourceProgress() { return reportProgress; },
1160
+ sourceUrl,
1122
1161
  );
1123
1162
  const moduleRouter = createOrdinaryArtifactModuleRouter(
1124
1163
  scope,
1125
1164
  configuration,
1126
1165
  role,
1127
1166
  reader,
1128
- originalFetch,
1129
1167
  originalWorker,
1130
1168
  );
1131
- const hadOwn = Object.prototype.hasOwnProperty.call(scope, MODULE_ROUTER_NAME);
1132
- const previous = scope[MODULE_ROUTER_NAME];
1169
+ const originalProperties = ['fetch', 'Worker', MODULE_ROUTER_NAME].map(
1170
+ function originalWorkerProperty(name) {
1171
+ return { name, descriptor: Object.getOwnPropertyDescriptor(scope, name) };
1172
+ },
1173
+ );
1174
+ const removedCacheProperties = [];
1175
+ function restoreEnvironment() {
1176
+ const failures = [];
1177
+ for (const { owner, descriptor } of removedCacheProperties.splice(0).reverse()) {
1178
+ try {
1179
+ Object.defineProperty(owner, 'caches', descriptor);
1180
+ } catch (error) {
1181
+ failures.push(error);
1182
+ }
1183
+ }
1184
+ for (const { name, descriptor } of originalProperties) {
1185
+ try {
1186
+ if (descriptor) Object.defineProperty(scope, name, descriptor);
1187
+ else delete scope[name];
1188
+ } catch (error) {
1189
+ failures.push(error);
1190
+ }
1191
+ }
1192
+ if (failures.length) throw new AggregateError(failures, 'Unable to restore the speech Worker resource environment.');
1193
+ }
1194
+ function resourceFetch(input, init) {
1195
+ return reader.fetchResource(input, init, null);
1196
+ }
1133
1197
  try {
1198
+ // Upstream checks `caches in self` before import. Remove only this Worker's
1199
+ // API descriptors so its normal fetch path reaches the parent DBOPFS owner.
1200
+ for (let owner = scope; owner; owner = Object.getPrototypeOf(owner)) {
1201
+ const descriptor = Object.getOwnPropertyDescriptor(owner, 'caches');
1202
+ if (!descriptor) continue;
1203
+ if (!delete owner.caches) {
1204
+ throw new Error('The speech Worker cannot disable its native CacheStorage API.');
1205
+ }
1206
+ removedCacheProperties.push({ owner, descriptor });
1207
+ }
1208
+ scope.fetch = resourceFetch;
1209
+ if (is.function(originalWorker)) {
1210
+ scope.Worker = function ResourceSpeechWorker(specifier, options) {
1211
+ return moduleRouter.router.createWorker(null, specifier, options);
1212
+ };
1213
+ }
1134
1214
  scope[MODULE_ROUTER_NAME] = moduleRouter.router;
1135
1215
  if (scope[MODULE_ROUTER_NAME] !== moduleRouter.router) {
1136
1216
  Object.defineProperty(scope, MODULE_ROUTER_NAME, {
@@ -1141,24 +1221,41 @@ function installOrdinaryArtifactModuleRouter(scope, configuration, role) {
1141
1221
  });
1142
1222
  }
1143
1223
  } catch (error) {
1144
- moduleRouter.cleanup();
1224
+ resourceClient.close(error);
1225
+ let cause = error;
1226
+ try {
1227
+ restoreEnvironment();
1228
+ } catch (restoreError) {
1229
+ cause = new AggregateError([error, restoreError], 'Speech Worker resource setup and restoration failed.');
1230
+ }
1145
1231
  throw workerError(
1146
1232
  "ARCANE_AI_PROVIDER_UNAVAILABLE",
1147
1233
  "The speech Worker cannot install the artifact module router.",
1148
- error,
1234
+ cause,
1149
1235
  "speech-module-router-unavailable",
1150
1236
  );
1151
1237
  }
1152
1238
  return completeValue({
1153
- cache: moduleRouter.transformersCache,
1154
- cleanup() {
1155
- moduleRouter.cleanup();
1239
+ receive: resourceClient.receive,
1240
+ setSignal(signal) { requestSignal = signal; },
1241
+ setProgress(report) { reportProgress = report; },
1242
+ async cleanup() {
1243
+ resourceClient.close(new Error('The speech Worker resource owner was unloaded.'));
1244
+ let cleanupFailure = null;
1156
1245
  try {
1157
- if (hadOwn) scope[MODULE_ROUTER_NAME] = previous;
1158
- else delete scope[MODULE_ROUTER_NAME];
1159
- } catch {
1160
- // Worker teardown releases the remaining module router reference.
1246
+ await moduleRouter.cleanup();
1247
+ } catch (error) {
1248
+ cleanupFailure = error;
1249
+ } finally {
1250
+ try {
1251
+ restoreEnvironment();
1252
+ } catch (error) {
1253
+ cleanupFailure = cleanupFailure
1254
+ ? new AggregateError([cleanupFailure, error], 'Speech Worker resource cleanup and restoration failed.')
1255
+ : error;
1256
+ }
1161
1257
  }
1258
+ if (cleanupFailure) throw cleanupFailure;
1162
1259
  },
1163
1260
  });
1164
1261
  }
@@ -1243,7 +1340,7 @@ function configuredWasmPaths(configuration) {
1243
1340
  return completeValue({ mjs: mjs.moduleUrl, wasm: wasm.moduleUrl });
1244
1341
  }
1245
1342
 
1246
- function configureRuntimeNamespace(namespace, configuration, role, cache) {
1343
+ function configureRuntimeNamespace(namespace, configuration, role) {
1247
1344
  const cleanup = [];
1248
1345
  const restoreSettings = () => {
1249
1346
  for (const restore of cleanup.splice(0).reverse()) {
@@ -1296,6 +1393,9 @@ function configureRuntimeNamespace(namespace, configuration, role, cache) {
1296
1393
  );
1297
1394
  }
1298
1395
  try {
1396
+ for (const name of ['useBrowserCache', 'useCustomCache', 'useFSCache']) {
1397
+ if (name in env) assignSetting(env, name, false, cleanup);
1398
+ }
1299
1399
  assignSetting(env, "allowLocalModels", false, cleanup, {
1300
1400
  assignmentRejectedReason: "transformers-env-allow-local-models-assignment-rejected",
1301
1401
  unavailableReason: "transformers-env-allow-local-models-unavailable",
@@ -1641,14 +1741,9 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
1641
1741
  configuration = validateConfiguration(request.payload?.configuration, role);
1642
1742
  const entry = configuration.runtime.files.find((file) =>
1643
1743
  file.path === configuration.runtime.entry);
1644
- if (graphConfiguration(configuration)) {
1645
- environment = installOrdinaryArtifactModuleRouter(scope, configuration, role);
1646
- } else {
1647
- environment = completeValue({
1648
- cache: null,
1649
- cleanup() {},
1650
- });
1651
- }
1744
+ environment = installOrdinaryArtifactModuleRouter(scope, configuration, role, send);
1745
+ environment.setSignal(signal);
1746
+ environment.setProgress(publishProgress);
1652
1747
  loadFailureReason = `${role}-worker-runtime-import-rejected`;
1653
1748
  publishProgress(
1654
1749
  {
@@ -1665,7 +1760,6 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
1665
1760
  namespace,
1666
1761
  configuration,
1667
1762
  role,
1668
- environment.cache,
1669
1763
  );
1670
1764
  loadFailureReason = `${role}-worker-model-load-rejected`;
1671
1765
  publishProgress(
@@ -1698,8 +1792,9 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
1698
1792
  restoreNamespace?.();
1699
1793
  restoreNamespace = null;
1700
1794
  try {
1701
- environment?.cleanup();
1702
- } catch {
1795
+ await environment?.cleanup();
1796
+ } catch (cleanupError) {
1797
+ scope.console?.error?.('Speech Worker resource cleanup failed after load rejection.', cleanupError);
1703
1798
  // A rejected load is followed by provider-owned Worker termination.
1704
1799
  }
1705
1800
  environment = null;
@@ -1785,8 +1880,9 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
1785
1880
  restoreNamespace?.();
1786
1881
  restoreNamespace = null;
1787
1882
  try {
1788
- environment?.cleanup();
1789
- } catch {
1883
+ await environment?.cleanup();
1884
+ } catch (cleanupError) {
1885
+ scope.console?.error?.('Speech Worker resource cleanup failed during unload.', cleanupError);
1790
1886
  // Worker termination remains the final cleanup boundary.
1791
1887
  }
1792
1888
  environment = null;
@@ -1833,6 +1929,7 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
1833
1929
  }
1834
1930
 
1835
1931
  function handleMessage(request) {
1932
+ if (environment?.receive(request)) return Promise.resolve();
1836
1933
  if (request?.protocol !== SPEECH_WORKER_PROTOCOL
1837
1934
  || !is.safeInteger(request.id)
1838
1935
  || request.id < 1) {
@@ -1871,7 +1968,14 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
1871
1968
  const record = { controller, op, publicOperation };
1872
1969
  const execute = tail.catch(() => undefined).then(() => {
1873
1970
  throwIfAborted(controller.signal, operationReason(role, op, "cancelled"));
1874
- return dispatch(request, controller.signal, op);
1971
+ environment?.setSignal(controller.signal);
1972
+ environment?.setProgress(function publishResourceProgress(progress) {
1973
+ send({ protocol: SPEECH_WORKER_PROTOCOL, id: request.id, type: 'progress', progress });
1974
+ });
1975
+ return dispatch(request, controller.signal, op).finally(function releaseResourceSignal() {
1976
+ environment?.setSignal(null);
1977
+ environment?.setProgress(null);
1978
+ });
1875
1979
  });
1876
1980
  operations.set(request.id, record);
1877
1981
  const operation = execute.finally(() => operations.delete(request.id));
@@ -1913,7 +2017,12 @@ export function installBrowserSpeechArtifactModuleWorker(role, scope = globalThi
1913
2017
  throw new TypeError('Speech artifact module Worker role must be "stt" or "tts".');
1914
2018
  }
1915
2019
  let initializing = false;
2020
+ let environment = null;
1916
2021
  const queued = [];
2022
+ function receiveResource(event) {
2023
+ if (environment?.receive(event.data)) event.stopImmediatePropagation?.();
2024
+ }
2025
+ scope.addEventListener('message', receiveResource);
1917
2026
  const bootstrap = (event) => {
1918
2027
  if (initializing) {
1919
2028
  queued.push(event);
@@ -1925,12 +2034,11 @@ export function installBrowserSpeechArtifactModuleWorker(role, scope = globalThi
1925
2034
  || request.role !== role) return;
1926
2035
  initializing = true;
1927
2036
  void (async () => {
1928
- let environment = null;
1929
2037
  try {
1930
2038
  const configuration = validateConfiguration(request.configuration, role);
1931
2039
  const target = configuration.runtime.files.find((file) =>
1932
2040
  file.path === request.targetPath);
1933
- if (!target?.moduleUrl) {
2041
+ if (!target?.moduleUrl && !request.targetUrl) {
1934
2042
  throw workerError(
1935
2043
  "ARCANE_AI_ARTIFACT_GRAPH_CONFIGURATION_INVALID",
1936
2044
  "The nested module Worker target was not materialized.",
@@ -1938,15 +2046,22 @@ export function installBrowserSpeechArtifactModuleWorker(role, scope = globalThi
1938
2046
  "artifact-graph-module-worker-target-not-materialized",
1939
2047
  );
1940
2048
  }
1941
- environment = installOrdinaryArtifactModuleRouter(scope, configuration, role);
1942
- await import(target.moduleUrl);
2049
+ environment = installOrdinaryArtifactModuleRouter(
2050
+ scope,
2051
+ configuration,
2052
+ role,
2053
+ function sendNestedResource(message) { scope.postMessage(message); },
2054
+ target?.sourceUrl ?? request.targetUrl,
2055
+ );
2056
+ await import(target?.moduleUrl ?? request.targetUrl);
1943
2057
  scope.removeEventListener("message", bootstrap);
1944
2058
  await new Promise((resolve) => queueMicrotask(resolve));
1945
2059
  for (const queuedEvent of queued.splice(0)) replayWorkerMessage(scope, queuedEvent);
1946
2060
  } catch (error) {
1947
2061
  try {
1948
- environment?.cleanup();
1949
- } catch {
2062
+ await environment?.cleanup();
2063
+ } catch (cleanupError) {
2064
+ scope.console?.error?.('Nested speech Worker resource cleanup failed.', cleanupError);
1950
2065
  // Preserve the exact nested Worker bootstrap failure.
1951
2066
  }
1952
2067
  try {
@@ -5,7 +5,7 @@ export const CORE_READY_EVENTS=['transport.ready','core.ready'];
5
5
  export const CORE_FRAME_CONTRACTS={
6
6
  request:{type:'request',fields:['protocol','type','id','method','parameters','sentAt']},
7
7
  response:{type:'response',fields:['protocol','type','id','ok'],success:['result'],failure:['error'],optional:['time']},
8
- event:{type:'event',fields:['protocol','type','event','data'],optional:['time']},
8
+ event:{type:'event',fields:['protocol','type','event','data'],optional:['time','requestId']},
9
9
  cancel:{type:'control',control:'request.cancel',fields:['protocol','type','control','requestId']},
10
10
  cancelAll:{type:'control',control:'requests.cancelAll',fields:['protocol','type','control']},
11
11
  replay:{type:'control',control:'runtime.replay',fields:['protocol','type','control']}
@@ -52,6 +52,8 @@ alone does not make bare module names resolve in a browser.
52
52
  | Present Light / Dark / System choices with complete labels | [Theme-switcher configuration](runtime-components.md#theme-switcherhtml) |
53
53
  | Use optional Core preferences and appearance without masking real failures | [PreferenceStore](runtime-modules.md#preferencestorejs), [SystemAppearance](runtime-modules.md#systemappearancejs), and [Core lookup errors](core-runtime.md#state-and-frames) |
54
54
  | Submit complete speech parts immediately and play them in order | [`SpeechPlayback`](runtime-modules.md#speechplaybackjs) and the [basic browser example](ai/browser-speech.md#play-a-complete-array-with-speechplayback) |
55
+ | Preserve exact text in an explicit complete synthesis call | [Exact complete synthesis input](ai/browser-speech.md#exact-complete-synthesis-input) and [managed AI narration](sdk-api.md#managed-ai-narration) |
56
+ | Select initial native window dimensions and resizing behavior | [Initial native window size](core-native-packaging.md#initial-native-window-size) and [Windows provider](sdk-api.md#createwindowsnativeprovider) |
55
57
  | Call `globalThis.Arcane` | [Arcane Core API](core/arcane-api.md) |
56
58
  | Connect a browser to one Core transport | [Core browser client](core-client.md) |
57
59
  | Cancel one Core speech synthesis request without changing its payload | [Speech request lifetime](core-client.md#events-and-request-lifetime) and [Core facade](sdk-api.md#createcorefacade) |
@@ -70,9 +70,12 @@ it does not substitute a quantized graph or another model.
70
70
  | `dtype` | Exact upstream precision selection; defaults to `fp16` for Laya and `fp32` for Julia. For Laya FP32, supply `dtype: 'fp32'`. Unsupported selections fail at the selected upstream loader rather than being replaced. |
71
71
  | `runtime.moduleUrl` | Defaults to the self-contained CDN entry `https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0/dist/transformers.min.js`. The `.web.js` build expects a bundler to resolve its bare ONNX Runtime import and is unsuitable for a native module Worker. An explicit compatible public runtime URL may be supplied. |
72
72
  | `runtime.wasmPaths` | Optional upstream ONNX WASM location passed to the selected runtime's public environment API. |
73
+ | `store` | Optional existing SDK DBOPFS model or speech artifact store exposing `fetchResource()`. The store remains outside Worker configuration; the SDK bridges the loader's actual resource requests to it. |
73
74
 
74
- The runtime, tokenizer and weights use normal upstream loading and caching;
75
- the SDK does not redistribute them or create a second model store. Browser
75
+ Without `store`, the runtime, tokenizer and weights use normal upstream loading
76
+ and caching. With `store`, the selected loader still owns filenames and model
77
+ selection while model resources persist through that existing SDK store. The
78
+ SDK does not redistribute upstream runtimes or models. Browser
76
79
  network, cross-origin and backend availability still apply. WebGPU needs a
77
80
  compatible browser/device context. Unsupported precision or backend errors
78
81
  are returned, not replaced with a different model, backend or precision.
@@ -88,6 +91,57 @@ Julia selects `dtype: 'fp32'`, loading root `model.onnx` with the exact
88
91
  through the public generic `PreTrainedModel` API, not a text-generation
89
92
  pipeline. Existing Wllama, cloud, speech and chat APIs are unchanged.
90
93
 
94
+ ## Shared DBOPFS model resources
95
+
96
+ ```js
97
+ import {createDbopfsModelStore} from 'arcane-os/ai/browser-wasm';
98
+ import {createBrowserDecisionModel} from 'arcane-os/ai/browser-decisions';
99
+
100
+ // dbopfs is the application's existing Arcane DBOPFS instance.
101
+ const store = createDbopfsModelStore({dbopfs});
102
+ const decisions = createBrowserDecisionModel(
103
+ {
104
+ family: 'laya',
105
+ model: 'onnx-community/laya-typed-decisions-ONNX',
106
+ store
107
+ }
108
+ );
109
+ await decisions.load();
110
+ ```
111
+
112
+ The same `store` may serve other SDK model clients. The application supplies no
113
+ tokenizer/configuration/graph manifest and performs no file routing. Inside the
114
+ dedicated decision Worker, Transformers.js's public `env.fetch` hook routes the
115
+ actual selected tokenizer, configuration, ONNX graph and external-data requests
116
+ to `store.fetchResource(input, options)`. Its competing browser, custom and
117
+ filesystem model caches are disabled only for this explicit stored mode. The
118
+ default upstream ONNX WASM binary and factory preloads use this same hook.
119
+ Native support-file fetches in the dedicated Worker also use the store. The
120
+ runtime module itself still loads from `runtime.moduleUrl` through normal module
121
+ loading. Native ESM imports remain browser-owned: an upstream custom factory
122
+ import outside its preload path is not claimed as a DBOPFS resource. No second
123
+ application cache or model substitution is introduced.
124
+
125
+ The store returns `{file, status, statusText, headers, url, redirected}`. The
126
+ Worker reconstructs the complete response with its actual HTTP status and
127
+ headers, so a missing optional upstream file remains a missing-file response.
128
+ New response bodies stream into ordered persistent shards, including resources
129
+ with unknown totals. Completed resources are reused by their semantic request
130
+ and URL selection. Interrupted closed shards remain available for HTTP Range
131
+ resume; a server returning a full response restarts that member without adding
132
+ the saved prefix twice. Independent resources remain concurrent, and repeated
133
+ requests for the same resource share its storage mutation owner. Existing saved
134
+ model formats are retained without a migration.
135
+
136
+ Resource progress uses `{phase: 'download', completed, total, unit: 'shards',
137
+ url}`. `completed` counts closed persistent shards; `total` is `null` until the
138
+ resource finishes. This progress carries no network-size, rate or ETA fields.
139
+ Cancellation aborts the owned fetch and joins reader/writer cleanup before the
140
+ resource owner becomes available to a later request. Interrupted closed shards
141
+ are retained. `unload()` and `dispose()` still acknowledge their lifecycle
142
+ transition synchronously; outstanding `load()`/`evaluate()` calls settle after
143
+ their resource cleanup.
144
+
91
145
  ## Complete rows and results
92
146
 
93
147
  `evaluate(rows, {signal})` accepts an array in caller order. Each row has
@@ -139,7 +193,7 @@ errors, including graph diagnostics when decoding fails, remain available in
139
193
 
140
194
  Work is acknowledged synchronously before loading/inference waits. Progress
141
195
  reports real semantic phases such as `waiting-for-model`, `loading-runtime`, `loading-tokenizer`,
142
- `loading-model`, `ready`, `evaluating` and `complete`; it is not a synthetic
196
+ `loading-model`, `download`, `ready`, `evaluating` and `complete`; it is not a synthetic
143
197
  percentage. Loading the model and tokenizer proceeds concurrently. Repeated
144
198
  identical state text is tokenized once per batch. Independent requests are
145
199
  accepted concurrently; the selected backend still owns its execution ordering.