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
@@ -10,6 +10,8 @@ import {
10
10
  import { createPackagedWllamaRuntime } from "./browser-wllama-runtime.mjs";
11
11
  import { getBrowserDeviceSettings, describeBrowserGpu } from "./browser-device-settings.mjs";
12
12
  import { arcaneEvents } from "../event-manager.mjs";
13
+ import { createDbopfsModelPartStore } from "./dbopfs-model-parts.mjs";
14
+ import { createDbopfsResourceStore } from "./dbopfs-model-resources.mjs";
13
15
 
14
16
  const is = new Is(false);
15
17
 
@@ -28,7 +30,6 @@ const WEBGPU_ADAPTER_SELECTION_PROTOCOL = "arcane-ai-webgpu-adapter-selection/1"
28
30
  const MODEL_LOAD_HEARTBEAT_MS = 5_000;
29
31
  const DEFAULT_MODEL_DOWNLOAD_CONCURRENCY = 4;
30
32
  const MODEL_DOWNLOAD_PROGRESS_INTERVAL_MS = 250;
31
- const MODEL_DOWNLOAD_SPEED_WINDOW_MS = 5_000;
32
33
  const MODEL_DOWNLOAD_MAX_RANGE_PARTS = 4_096;
33
34
  const MODEL_DOWNLOAD_TARGET_RANGE_BYTES = 4_000_000;
34
35
  const CAPABILITY_POLICY_PROTOCOL = "arcane-ai-browser-capability-policy/1";
@@ -632,6 +633,8 @@ export function createDbopfsModelStore({
632
633
  throw new TypeError("estimateStorage must be a function or null.");
633
634
  }
634
635
  const workerLimit = downloadConcurrencyValue(downloadConcurrency);
636
+ const parts = createDbopfsModelPartStore({dbopfs, tableName});
637
+ const resources = createDbopfsResourceStore({dbopfs, tableName});
635
638
  let tablePromise = null;
636
639
 
637
640
  async function table() {
@@ -665,51 +668,25 @@ export function createDbopfsModelStore({
665
668
  }
666
669
 
667
670
  function createDownloadProgressReporter(members, onProgress, retainFailure) {
668
- const memberTotals = members.map((member) => member.bytes ?? null);
669
- let loadedBytes = 0;
671
+ const shards = members.map(function initialMemberProgress(member) {
672
+ return {name: member.name, completed: 0, total: null, unit: 'shards'};
673
+ });
670
674
  let completed = 0;
671
675
  let activeTransfers = 0;
672
676
  let transferMode = members.length === 1 ? "probing" : "files";
673
677
  let timer = null;
674
678
  let lastPublishedAt = 0;
675
- let samples = [];
676
-
677
- function totalBytesValue() {
678
- if (memberTotals.some((value) => value === null)) return null;
679
- const totalBytes = memberTotals.reduce((sum, value) => sum + value, 0);
680
- return is.safeInteger(totalBytes) ? totalBytes : null;
681
- }
682
-
683
- function progressRecord(now) {
684
- samples.push(completeValue({ at: now, loadedBytes }));
685
- const oldestUsefulTime = now - MODEL_DOWNLOAD_SPEED_WINDOW_MS;
686
- while (samples.length > 1 && samples[1].at <= oldestUsefulTime) samples.shift();
687
- const firstSample = samples[0];
688
- const elapsedSeconds = (now - firstSample.at) / 1_000;
689
- const sampledBytes = loadedBytes - firstSample.loadedBytes;
690
- const bytesPerSecond = sampledBytes > 0 && elapsedSeconds > 0
691
- ? Math.round(sampledBytes / elapsedSeconds)
692
- : null;
693
- const totalBytes = totalBytesValue();
694
- const remainingBytes = totalBytes === null
695
- ? null
696
- : Math.max(0, totalBytes - loadedBytes);
697
- const etaSeconds = remainingBytes === 0
698
- ? 0
699
- : bytesPerSecond === null || remainingBytes === null
700
- ? null
701
- : Math.ceil(remainingBytes / bytesPerSecond);
679
+
680
+ function progressRecord() {
702
681
  return completeValue({
703
682
  phase: "download",
704
683
  completed,
705
684
  total: members.length,
706
685
  unit: "files",
707
686
  heartbeat: false,
708
- loadedBytes,
709
- totalBytes,
710
- remainingBytes,
711
- bytesPerSecond,
712
- etaSeconds,
687
+ members: shards.map(function memberProgressSnapshot(member) {
688
+ return {...member};
689
+ }),
713
690
  activeTransfers,
714
691
  transferLimit: workerLimit,
715
692
  transferMode,
@@ -726,7 +703,7 @@ export function createDbopfsModelStore({
726
703
  if (onProgress === null) return;
727
704
  const now = progressClock();
728
705
  if (!force && now - lastPublishedAt < MODEL_DOWNLOAD_PROGRESS_INTERVAL_MS) return;
729
- onProgress(progressRecord(now));
706
+ onProgress(progressRecord());
730
707
  lastPublishedAt = now;
731
708
  }
732
709
 
@@ -741,40 +718,19 @@ export function createDbopfsModelStore({
741
718
  }
742
719
  }
743
720
 
744
- function setMemberTotal(memberIndex, totalBytes) {
745
- if (!is.safeInteger(totalBytes) || totalBytes <= 0) return;
746
- if (memberTotals[memberIndex] === totalBytes) return;
747
- memberTotals[memberIndex] = totalBytes;
748
- publishSafely({ force: true });
749
- }
750
-
751
- function addBytes(value) {
752
- if (!is.safeInteger(value) || value <= 0) return;
753
- loadedBytes += value;
754
- publishSafely();
755
- }
756
-
757
- function discardBytes(value) {
758
- if (!is.safeInteger(value) || value <= 0) return;
759
- loadedBytes = Math.max(0, loadedBytes - value);
760
- samples = [];
761
- publishSafely({ force: true });
762
- }
763
-
764
721
  return completeValue({
765
- addBytes,
722
+ reportShards(memberIndex, value) {
723
+ shards[memberIndex] = {...value, name: members[memberIndex].name};
724
+ publishSafely({force: true});
725
+ },
766
726
  beginTransfer() {
767
727
  activeTransfers += 1;
768
728
  publishSafely({ force: true });
769
729
  },
770
- completeMember(memberIndex, totalBytes) {
771
- if (is.safeInteger(totalBytes) && totalBytes > 0) {
772
- memberTotals[memberIndex] = totalBytes;
773
- }
730
+ completeMember() {
774
731
  completed += 1;
775
732
  publishSafely({ force: true });
776
733
  },
777
- discardBytes,
778
734
  dispose: stopTimer,
779
735
  endTransfer({ publishProgress = true } = {}) {
780
736
  activeTransfers = Math.max(0, activeTransfers - 1);
@@ -783,24 +739,14 @@ export function createDbopfsModelStore({
783
739
  finish() {
784
740
  publishSafely({ force: true });
785
741
  },
786
- setMemberTotal,
787
742
  setMode(value) {
788
743
  if (transferMode === value) return;
789
744
  transferMode = value;
790
745
  publishSafely({ force: true });
791
746
  },
792
- restoreBytes(value) {
793
- if (!is.safeInteger(value) || value <= 0) return;
794
- loadedBytes += value;
795
- samples = [];
796
- publishSafely({ force: true });
797
- },
798
- restoreMember(memberIndex, totalBytes) {
799
- if (!is.safeInteger(totalBytes) || totalBytes <= 0) return;
800
- memberTotals[memberIndex] = totalBytes;
801
- loadedBytes += totalBytes;
747
+ restoreMember(memberIndex) {
748
+ shards[memberIndex] = {...shards[memberIndex], state: 'cached'};
802
749
  completed += 1;
803
- samples = [];
804
750
  publishSafely({ force: true });
805
751
  },
806
752
  start() {
@@ -814,56 +760,21 @@ export function createDbopfsModelStore({
814
760
  });
815
761
  }
816
762
 
817
- async function write(name, body, {
818
- signal,
819
- onChunk = null,
820
- onDiscard = null,
821
- } = {}) {
822
- const directory = await table();
823
- const handle = await directory.getFileHandle(name, { create: true });
824
- const writable = await handle.createWritable();
825
- let written = 0;
826
- try {
827
- for await (const chunk of byteChunks(body, signal)) {
828
- await writable.write(chunk);
829
- written += chunk.byteLength;
830
- onChunk?.(chunk.byteLength);
831
- throwIfAborted(signal, "install");
832
- }
833
- throwIfAborted(signal, "install");
834
- await writable.close();
835
- return written;
836
- } catch (error) {
837
- await writable.abort?.(error).catch(() => undefined);
838
- await directory.removeEntry(name).catch(() => undefined);
839
- onDiscard?.(written);
840
- throw error;
841
- }
842
- }
843
-
844
- async function storedModelFile(name) {
845
- const modelFile = await file(name);
846
- if (!modelFile || modelFile.size === 0) {
847
- throw fail(
848
- "ARCANE_AI_MODEL_CACHE_REJECTED",
849
- "A stored model file could not be reopened as a non-empty model.",
850
- );
851
- }
852
- return modelFile;
853
- }
854
-
855
763
  async function writeOpenedModel(name, opened, {
856
764
  signal,
857
- onChunk = null,
858
- onDiscard = null,
765
+ onProgress,
859
766
  } = {}) {
860
- let written = 0;
861
767
  try {
862
- written = await write(name, opened.body, { signal, onChunk, onDiscard });
863
- return await storedModelFile(name);
768
+ return await parts.write(name, opened.body, {signal, onProgress});
864
769
  } catch (error) {
865
- if (written > 0) onDiscard?.(written);
866
770
  await cancelOpenedDownload(opened, error);
771
+ if (signal?.aborted && error?.code !== "ARCANE_AI_REQUEST_ABORTED") {
772
+ throw new ArcaneAIError(
773
+ "ARCANE_AI_REQUEST_ABORTED",
774
+ "The Arcane AI request was cancelled.",
775
+ {cause: error, kind: "llm", operation: "install"},
776
+ );
777
+ }
867
778
  throw error;
868
779
  }
869
780
  }
@@ -883,6 +794,7 @@ export function createDbopfsModelStore({
883
794
  const ranges = rangePlanForInstall(total, signal);
884
795
  const partFiles = new Array(ranges.length);
885
796
  const pendingRangeIndexes = [];
797
+ let completedParts = 0;
886
798
  for (let rangeIndex = 0; rangeIndex < ranges.length; rangeIndex += 1) {
887
799
  throwIfAborted(signal, "install");
888
800
  const range = ranges[rangeIndex];
@@ -891,12 +803,17 @@ export function createDbopfsModelStore({
891
803
  const expected = range.end - range.start + 1;
892
804
  if (partFile?.size === expected) {
893
805
  partFiles[rangeIndex] = partFile;
894
- progress?.restoreBytes(expected);
806
+ completedParts += 1;
895
807
  } else {
896
808
  if (partFile) await removeEntry(partName);
897
809
  pendingRangeIndexes.push(rangeIndex);
898
810
  }
899
811
  }
812
+ progress?.reportShards(memberIndex, {
813
+ completed: completedParts,
814
+ total: ranges.length,
815
+ unit: 'shards'
816
+ });
900
817
  const linked = linkAbortSignal(signal);
901
818
  const downloadSignal = linked.controller.signal;
902
819
  let nextPendingIndex = 0;
@@ -915,7 +832,6 @@ export function createDbopfsModelStore({
915
832
  const range = ranges[rangeIndex];
916
833
  const partName = rangePartName(name, range);
917
834
  let opened = null;
918
- let writable = null;
919
835
  let received = 0;
920
836
  progress?.beginTransfer();
921
837
  try {
@@ -925,33 +841,37 @@ export function createDbopfsModelStore({
925
841
  end: range.end,
926
842
  total: range.total,
927
843
  });
928
- const directory = await table();
929
- const handle = await directory.getFileHandle(partName, { create: true });
930
- writable = await handle.createWritable();
931
844
  const expected = range.end - range.start + 1;
932
- for await (const chunk of byteChunks(opened.body, downloadSignal)) {
933
- const nextReceived = received + chunk.byteLength;
934
- if (!is.safeInteger(nextReceived) || nextReceived > expected) {
845
+ async function* rangeContent() {
846
+ for await (const chunk of byteChunks(opened.body, downloadSignal)) {
847
+ const nextReceived = received + chunk.byteLength;
848
+ if (!is.safeInteger(nextReceived) || nextReceived > expected) {
849
+ throw fail(
850
+ "ARCANE_AI_MODEL_DOWNLOAD_FAILED",
851
+ "The model server returned more content than the requested HTTP byte range.",
852
+ );
853
+ }
854
+ received = nextReceived;
855
+ yield chunk;
856
+ throwIfAborted(downloadSignal, "install");
857
+ }
858
+ if (received !== expected) {
935
859
  throw fail(
936
860
  "ARCANE_AI_MODEL_DOWNLOAD_FAILED",
937
- "The model server returned more content than the requested HTTP byte range.",
861
+ "The model server returned less content than the requested HTTP byte range.",
938
862
  );
939
863
  }
940
- await writable.write(chunk);
941
- received = nextReceived;
942
- progress?.addBytes(chunk.byteLength);
943
- throwIfAborted(downloadSignal, "install");
944
- }
945
- if (received !== expected) {
946
- throw fail(
947
- "ARCANE_AI_MODEL_DOWNLOAD_FAILED",
948
- "The model server returned less content than the requested HTTP byte range.",
949
- );
950
864
  }
865
+ partFiles[rangeIndex] = await parts.writePart(partName, rangeContent(), {
866
+ signal: downloadSignal,
867
+ });
951
868
  throwIfAborted(downloadSignal, "install");
952
- await writable.close();
953
- writable = null;
954
- partFiles[rangeIndex] = await storedModelFile(partName);
869
+ completedParts += 1;
870
+ progress?.reportShards(memberIndex, {
871
+ completed: completedParts,
872
+ total: ranges.length,
873
+ unit: 'shards'
874
+ });
955
875
  } catch (error) {
956
876
  let transferError = error;
957
877
  if (downloadSignal.aborted
@@ -970,13 +890,6 @@ export function createDbopfsModelStore({
970
890
  }
971
891
  retainFailure(transferError);
972
892
  await cancelOpenedDownload(opened, transferError);
973
- try {
974
- await writable?.abort?.(transferError);
975
- } catch {
976
- // The range part is removed below even if its writable already closed.
977
- }
978
- await removeEntry(partName).catch(() => undefined);
979
- progress?.discardBytes(received);
980
893
  throw transferError;
981
894
  } finally {
982
895
  progress?.endTransfer({ publishProgress: failure === null });
@@ -1038,12 +951,12 @@ export function createDbopfsModelStore({
1038
951
  }
1039
952
  if (probe.kind === "complete") {
1040
953
  if (!multiFile) progress?.setMode("single");
1041
- progress?.setMemberTotal(memberIndex, probe.opened.contentLength);
1042
954
  try {
1043
955
  const modelFile = await writeOpenedModel(name, probe.opened, {
1044
956
  signal,
1045
- onChunk: progress?.addBytes,
1046
- onDiscard: progress?.discardBytes,
957
+ onProgress: function reportCompleteResponseShards(value) {
958
+ progress?.reportShards(memberIndex, value);
959
+ },
1047
960
  });
1048
961
  await removeRangePartsAfterWholeFile(name, probe.opened.contentLength);
1049
962
  return modelFile;
@@ -1057,7 +970,6 @@ export function createDbopfsModelStore({
1057
970
  progress?.endTransfer({ publishProgress: false });
1058
971
  if (probe.kind !== "supported") return null;
1059
972
  if (!multiFile) progress?.setMode("ranges");
1060
- progress?.setMemberTotal(memberIndex, probe.total);
1061
973
  return writeParallelRanges(source, memberIndex, name, probe.total, {
1062
974
  signal,
1063
975
  progress,
@@ -1067,7 +979,10 @@ export function createDbopfsModelStore({
1067
979
 
1068
980
  async function removeNames(names) {
1069
981
  const removed = [];
1070
- for (const entry of names.models) removed.push(await removeEntry(entry.name));
982
+ for (const entry of names.models) {
983
+ removed.push(await parts.remove(entry.name));
984
+ removed.push(await removeEntry(entry.name));
985
+ }
1071
986
  return removed.some(Boolean);
1072
987
  }
1073
988
 
@@ -1209,13 +1124,9 @@ export function createDbopfsModelStore({
1209
1124
  const modelFiles = [];
1210
1125
  for (const entry of names.models) {
1211
1126
  throwIfAborted(signal, "install");
1212
- const modelFile = await file(entry.name);
1213
- if (modelFile?.size === 0) {
1214
- await removeEntry(entry.name);
1215
- modelFiles.push(null);
1216
- } else {
1217
- modelFiles.push(modelFile);
1218
- }
1127
+ // Existing whole-file caches remain readable; new downloads use parts.
1128
+ const modelFile = await parts.read(entry.name, {signal}) ?? await file(entry.name);
1129
+ modelFiles.push(modelFile);
1219
1130
  }
1220
1131
  return modelFiles;
1221
1132
  }
@@ -1384,14 +1295,14 @@ export function createDbopfsModelStore({
1384
1295
  let opened = null;
1385
1296
  try {
1386
1297
  opened = await source.open(memberIndex, { signal: downloadSignal });
1387
- progress.setMemberTotal(memberIndex, opened.contentLength);
1388
1298
  modelFile = await writeOpenedModel(
1389
1299
  names.models[memberIndex].name,
1390
1300
  opened,
1391
1301
  {
1392
1302
  signal: downloadSignal,
1393
- onChunk: progress.addBytes,
1394
- onDiscard: progress.discardBytes,
1303
+ onProgress: function reportSourceResponseShards(value) {
1304
+ progress.reportShards(memberIndex, value);
1305
+ },
1395
1306
  },
1396
1307
  );
1397
1308
  await removeRangePartsAfterWholeFile(
@@ -1407,7 +1318,7 @@ export function createDbopfsModelStore({
1407
1318
  }
1408
1319
  throwIfAborted(downloadSignal, "install");
1409
1320
  modelFiles[memberIndex] = modelFile;
1410
- progress.completeMember(memberIndex, modelFile.size);
1321
+ progress.completeMember();
1411
1322
  }
1412
1323
 
1413
1324
  async function installWorker() {
@@ -1432,7 +1343,7 @@ export function createDbopfsModelStore({
1432
1343
  const modelFile = modelFiles[memberIndex];
1433
1344
  if (!modelFile) continue;
1434
1345
  pendingMembers -= 1;
1435
- progress.restoreMember(memberIndex, modelFile.size);
1346
+ progress.restoreMember(memberIndex);
1436
1347
  }
1437
1348
  const workers = [];
1438
1349
  const workerCount = Math.min(workerLimit, pendingMembers);
@@ -1507,6 +1418,7 @@ export function createDbopfsModelStore({
1507
1418
  install,
1508
1419
  ensure,
1509
1420
  remove,
1421
+ fetchResource: resources.fetchResource,
1510
1422
  });
1511
1423
  DBOPFS_MODEL_STORES.add(store);
1512
1424
  return store;