arcane-os 0.80.0 → 0.82.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 (34) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +97 -19
  4. package/browser-runtime/ai/dbopfs-model-parts.mjs +12 -5
  5. package/browser-runtime/ai/dbopfs-model-resources.mjs +2 -5
  6. package/browser-runtime/core/app-control.mjs +439 -0
  7. package/docs/reference/ai/browser-speech.md +11 -0
  8. package/docs/reference/ai/browser-wasm.md +21 -10
  9. package/docs/reference/cli.md +9 -1
  10. package/docs/reference/core-native-packaging.md +14 -0
  11. package/docs/reference/inventory/package-api.json +23 -9
  12. package/docs/reference/inventory/runtime-components.json +2 -1
  13. package/docs/reference/native-app-control.md +147 -0
  14. package/docs/reference/native-decisions.md +74 -16
  15. package/docs/reference/runtime-components.md +20 -0
  16. package/docs/reference/sdk-api.md +49 -6
  17. package/package.json +2 -1
  18. package/runtime/arcane/components/chat.html +140 -18
  19. package/runtime/arcane/modules/AI.js +10 -5
  20. package/src/cli/app-control.mjs +106 -0
  21. package/src/cli/main.mjs +5 -0
  22. package/src/core/app-control.mjs +23 -0
  23. package/src/core/hosts/windows/ArcaneAppControl.cs +525 -0
  24. package/src/core/hosts/windows/ArcaneCoreProcess.cs +2 -63
  25. package/src/core/hosts/windows/ArcaneFrameTransport.cs +80 -0
  26. package/src/core/hosts/windows/ArcaneHost.cs +45 -7
  27. package/src/core/hosts/windows/ArcaneLauncher.cs +5 -0
  28. package/src/core/hosts/windows/ArcaneWindowControl.cs +248 -0
  29. package/src/core/services/decisions.mjs +16 -2
  30. package/src/core/shared-host.mjs +7 -91
  31. package/src/core/socket-connection.mjs +109 -0
  32. package/src/local-ai/decision-tokenizer-worker.mjs +1 -1
  33. package/src/local-ai/decision-tokenizer.mjs +2 -2
  34. package/src/local-ai/decisions.mjs +33 -10
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.82.0
4
+
5
+ - Add `connectAppControl` from `arcane-os/core/app-control` and the
6
+ `arcane app-control` CLI for an explicitly selected running Windows WebView2
7
+ application. Inspect complete document content and live controls, capture the
8
+ rendered viewport, and perform targeted click, fill, select and scroll actions
9
+ without activating the desktop window or injecting global input. Preserve the
10
+ application's existing Core, profile and lifecycle; closing the client closes
11
+ only its connection. The matching Windows host archive includes the generated
12
+ DOM operation source. Other native host adapters remain unavailable.
13
+ - Render stored `arcane-media:` images in shared Chat through MarkdownMedia,
14
+ including restored history, live chunks and final responses. Preserve complete
15
+ original Markdown and immediate text rendering. Release obsolete display URLs
16
+ on replacement, removal and destruction; retain requested print snapshots
17
+ through `afterprint` and await current images for explicit printing.
18
+ - Keep dependencies, saved-data formats, model and speech routing unchanged.
19
+ Package verification and host compilation do not establish actual built-app,
20
+ Core/profile, covered-window capture or model behavior.
21
+
22
+ ## 0.81.0
23
+
24
+ - Add per-activation native decision `family`, `model`, `revision`, and `dtype`
25
+ selection through the direct runtime and Core service. Support Laya FP16 and
26
+ Julia 1 FP32 with their own model companions and tokenizer family, preserving
27
+ Laya FP32 defaults, complete input rows and actual output tensors. Keep
28
+ requested replacement metadata separate from the active model and retain
29
+ prepared files through actual worker exit.
30
+ - Report cached model loading as real stored-part progress and preserve known
31
+ GGUF shard totals. Forward complete progress details and cancellation through
32
+ cache reads without presenting cached data as a download or inventing totals
33
+ for an unfinished network stream.
34
+ - Add object-form `AI.streamRequest({speech:false})` for text-only operations.
35
+ Completion, failure and cancellation leave unrelated prepared, native and
36
+ buffered speech alone. Existing defaults, positional methods and explicit
37
+ speech APIs retain their behavior.
38
+ - Close and drain the Windows host after WebView2 `BrowserProcessExited`,
39
+ preserving its full diagnostic instead of leaving a blank window awaiting
40
+ manual closure. Existing renderer/Core-only handling and saved profiles stay
41
+ unchanged. This source correction does not establish the cause of the browser
42
+ exit; executable consumers require the matching rebuilt host archive.
43
+ - Keep dependencies, app-control, native GGUF activation and native Kokoro
44
+ routing unchanged. Source/package verification is separate from actual model,
45
+ audio or application execution.
46
+
3
47
  ## 0.80.0
4
48
 
5
49
  - Add `assetProjectionId` and `resourcePaths` to native decision factories and
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.80.0` SDK contract. Development applications declare
23
+ This checkout defines the `0.82.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;
@@ -667,19 +667,21 @@ export function createDbopfsModelStore({
667
667
  }
668
668
  }
669
669
 
670
- function createDownloadProgressReporter(members, onProgress, retainFailure) {
670
+ function createModelProgressReporter(members, onProgress, retainFailure) {
671
671
  const shards = members.map(function initialMemberProgress(member) {
672
672
  return {name: member.name, completed: 0, total: null, unit: 'shards'};
673
673
  });
674
674
  let completed = 0;
675
+ let phase = "cache-check";
675
676
  let activeTransfers = 0;
676
677
  let transferMode = members.length === 1 ? "probing" : "files";
677
678
  let timer = null;
678
679
  let lastPublishedAt = 0;
680
+ let progressFailure = null;
679
681
 
680
682
  function progressRecord() {
681
683
  return completeValue({
682
- phase: "download",
684
+ phase,
683
685
  completed,
684
686
  total: members.length,
685
687
  unit: "files",
@@ -713,18 +715,28 @@ export function createDbopfsModelStore({
713
715
  return true;
714
716
  } catch (error) {
715
717
  stopTimer();
718
+ progressFailure ??= error;
716
719
  retainFailure(error);
717
720
  return false;
718
721
  }
719
722
  }
720
723
 
724
+ function assertProgress() {
725
+ if (progressFailure !== null) throw progressFailure;
726
+ }
727
+
721
728
  return completeValue({
722
729
  reportShards(memberIndex, value) {
730
+ assertProgress();
723
731
  shards[memberIndex] = {...value, name: members[memberIndex].name};
732
+ if (value.phase === "download" || activeTransfers > 0) phase = "download";
733
+ else if (value.phase === "load") phase = "load";
724
734
  publishSafely({force: true});
735
+ if (value.phase === "load") assertProgress();
725
736
  },
726
737
  beginTransfer() {
727
738
  activeTransfers += 1;
739
+ phase = "download";
728
740
  publishSafely({ force: true });
729
741
  },
730
742
  completeMember() {
@@ -745,14 +757,17 @@ export function createDbopfsModelStore({
745
757
  publishSafely({ force: true });
746
758
  },
747
759
  restoreMember(memberIndex) {
748
- shards[memberIndex] = {...shards[memberIndex], state: 'cached'};
760
+ assertProgress();
761
+ shards[memberIndex] = {...shards[memberIndex], phase: 'load', cached: true, state: 'cached'};
762
+ if (activeTransfers === 0) phase = "load";
749
763
  completed += 1;
750
764
  publishSafely({ force: true });
765
+ assertProgress();
751
766
  },
752
767
  start() {
753
768
  if (!publishSafely({ force: true })) return;
754
769
  if (onProgress !== null && timer === null) {
755
- timer = setInterval(function publishDownloadProgressTick() {
770
+ timer = setInterval(function publishModelProgressTick() {
756
771
  publishSafely();
757
772
  }, MODEL_DOWNLOAD_PROGRESS_INTERVAL_MS);
758
773
  }
@@ -795,6 +810,9 @@ export function createDbopfsModelStore({
795
810
  const partFiles = new Array(ranges.length);
796
811
  const pendingRangeIndexes = [];
797
812
  let completedParts = 0;
813
+ progress?.reportShards(memberIndex, {
814
+ phase: 'load', completed: 0, total: ranges.length, unit: 'shards', cached: true,
815
+ });
798
816
  for (let rangeIndex = 0; rangeIndex < ranges.length; rangeIndex += 1) {
799
817
  throwIfAborted(signal, "install");
800
818
  const range = ranges[rangeIndex];
@@ -804,15 +822,20 @@ export function createDbopfsModelStore({
804
822
  if (partFile?.size === expected) {
805
823
  partFiles[rangeIndex] = partFile;
806
824
  completedParts += 1;
825
+ progress?.reportShards(memberIndex, {
826
+ phase: 'load', completed: completedParts, total: ranges.length, unit: 'shards', cached: true,
827
+ });
807
828
  } else {
808
829
  if (partFile) await removeEntry(partName);
809
830
  pendingRangeIndexes.push(rangeIndex);
810
831
  }
811
832
  }
812
833
  progress?.reportShards(memberIndex, {
834
+ phase: 'load',
813
835
  completed: completedParts,
814
836
  total: ranges.length,
815
- unit: 'shards'
837
+ unit: 'shards',
838
+ cached: true,
816
839
  });
817
840
  const linked = linkAbortSignal(signal);
818
841
  const downloadSignal = linked.controller.signal;
@@ -868,6 +891,7 @@ export function createDbopfsModelStore({
868
891
  throwIfAborted(downloadSignal, "install");
869
892
  completedParts += 1;
870
893
  progress?.reportShards(memberIndex, {
894
+ phase: 'download',
871
895
  completed: completedParts,
872
896
  total: ranges.length,
873
897
  unit: 'shards'
@@ -1120,13 +1144,27 @@ export function createDbopfsModelStore({
1120
1144
  });
1121
1145
  }
1122
1146
 
1123
- async function availableModelFiles(names, signal) {
1147
+ async function availableModelFiles(names, signal, progress) {
1124
1148
  const modelFiles = [];
1125
- for (const entry of names.models) {
1149
+ for (let memberIndex = 0; memberIndex < names.models.length; memberIndex += 1) {
1150
+ const entry = names.models[memberIndex];
1126
1151
  throwIfAborted(signal, "install");
1127
1152
  // Existing whole-file caches remain readable; new downloads use parts.
1128
- const modelFile = await parts.read(entry.name, {signal}) ?? await file(entry.name);
1153
+ let modelFile = await parts.read(entry.name, {
1154
+ signal,
1155
+ onProgress: function reportStoredShards(value) {
1156
+ progress?.reportShards(memberIndex, value);
1157
+ },
1158
+ });
1159
+ if (!modelFile) {
1160
+ modelFile = await file(entry.name);
1161
+ if (modelFile) progress?.reportShards(memberIndex, {
1162
+ phase: 'load', completed: 1, total: 1, unit: 'shards', cached: true,
1163
+ });
1164
+ }
1129
1165
  modelFiles.push(modelFile);
1166
+ if (modelFile) progress?.restoreMember(memberIndex);
1167
+ throwIfAborted(signal, "install");
1130
1168
  }
1131
1169
  return modelFiles;
1132
1170
  }
@@ -1136,9 +1174,16 @@ export function createDbopfsModelStore({
1136
1174
  ranges,
1137
1175
  signal,
1138
1176
  availableNames = null,
1177
+ onProgress = null,
1139
1178
  ) {
1140
1179
  const partFiles = [];
1141
1180
  let lastModified = 0;
1181
+ function reportStoredRange() {
1182
+ throwIfAborted(signal, "install");
1183
+ onProgress?.({phase: 'load', completed: partFiles.length,
1184
+ total: ranges.length, unit: 'shards', cached: true});
1185
+ }
1186
+ reportStoredRange();
1142
1187
  for (const range of ranges) {
1143
1188
  throwIfAborted(signal, "install");
1144
1189
  const partName = rangePartName(modelName, range);
@@ -1151,10 +1196,12 @@ export function createDbopfsModelStore({
1151
1196
  return null;
1152
1197
  }
1153
1198
  partFiles.push(partFile);
1199
+ reportStoredRange();
1154
1200
  if (is.finite(partFile.lastModified)) {
1155
1201
  lastModified = Math.max(lastModified, partFile.lastModified);
1156
1202
  }
1157
1203
  }
1204
+ throwIfAborted(signal, "install");
1158
1205
  return completeValue({
1159
1206
  file: new Blob(partFiles, { type: "application/octet-stream" }),
1160
1207
  lastModified,
@@ -1168,6 +1215,7 @@ export function createDbopfsModelStore({
1168
1215
  total,
1169
1216
  signal,
1170
1217
  availableNames = null,
1218
+ onProgress = null,
1171
1219
  ) {
1172
1220
  const ranges = rangePlanForInstall(total, signal);
1173
1221
  return storedRangeCandidateForPlan(
@@ -1175,10 +1223,11 @@ export function createDbopfsModelStore({
1175
1223
  ranges,
1176
1224
  signal,
1177
1225
  availableNames,
1226
+ onProgress,
1178
1227
  );
1179
1228
  }
1180
1229
 
1181
- async function storedRangeMember(member, modelName, signal) {
1230
+ async function storedRangeMember(member, modelName, signal, onProgress) {
1182
1231
  const names = await memberRangePartNames(modelName);
1183
1232
  const availableNames = names === null ? null : new Set(names);
1184
1233
  const declaredTotal = is.safeInteger(member.bytes) && member.bytes > 0
@@ -1190,6 +1239,7 @@ export function createDbopfsModelStore({
1190
1239
  declaredTotal,
1191
1240
  signal,
1192
1241
  availableNames,
1242
+ onProgress,
1193
1243
  );
1194
1244
  if (declared) {
1195
1245
  await removeStaleRangePartsAfterCompletion(
@@ -1214,6 +1264,7 @@ export function createDbopfsModelStore({
1214
1264
  total,
1215
1265
  signal,
1216
1266
  availableNames,
1267
+ onProgress,
1217
1268
  );
1218
1269
  if (
1219
1270
  candidate
@@ -1224,16 +1275,20 @@ export function createDbopfsModelStore({
1224
1275
  ) selected = candidate;
1225
1276
  }
1226
1277
  if (selected === null) return null;
1278
+ throwIfAborted(signal, "install");
1227
1279
  await removeStaleRangePartsAfterCompletion(
1228
1280
  modelName,
1229
1281
  selected.ranges,
1230
1282
  );
1283
+ onProgress?.({phase: 'load', completed: selected.ranges.length,
1284
+ total: selected.ranges.length, unit: 'shards', cached: true});
1285
+ throwIfAborted(signal, "install");
1231
1286
  return selected.file;
1232
1287
  }
1233
1288
 
1234
- async function availableResumableModelFiles(source, names, signal) {
1289
+ async function availableResumableModelFiles(source, names, signal, progress) {
1235
1290
  const members = sourceMetadata(source).files;
1236
- const modelFiles = await availableModelFiles(names, signal);
1291
+ const modelFiles = await availableModelFiles(names, signal, progress);
1237
1292
  await removeRangePartsBehindWholeFiles(members, names, modelFiles);
1238
1293
  for (let memberIndex = 0; memberIndex < modelFiles.length; memberIndex += 1) {
1239
1294
  if (modelFiles[memberIndex]) continue;
@@ -1241,20 +1296,40 @@ export function createDbopfsModelStore({
1241
1296
  members[memberIndex],
1242
1297
  names.models[memberIndex].name,
1243
1298
  signal,
1299
+ function reportStoredRangeShards(value) {
1300
+ progress?.reportShards(memberIndex, value);
1301
+ },
1244
1302
  );
1303
+ if (modelFiles[memberIndex]) progress?.restoreMember(memberIndex);
1304
+ throwIfAborted(signal, "install");
1245
1305
  }
1246
1306
  return modelFiles;
1247
1307
  }
1248
1308
 
1249
1309
  async function openCached(source, {
1250
1310
  signal,
1311
+ onProgress = null,
1251
1312
  } = {}) {
1252
1313
  const names = storageName(source);
1253
- const modelFiles = await availableResumableModelFiles(source, names, signal);
1254
- if (!modelFiles.every(Boolean)) return null;
1255
- return completeValue({
1256
- files: completeValue(modelFiles),
1314
+ let failure = null;
1315
+ const progress = createModelProgressReporter(sourceMetadata(source).files, onProgress, function retainProgressFailure(error) {
1316
+ failure ??= error;
1257
1317
  });
1318
+ try {
1319
+ progress.start();
1320
+ if (failure !== null) throw failure;
1321
+ const modelFiles = await availableResumableModelFiles(source, names, signal, progress);
1322
+ if (failure !== null) throw failure;
1323
+ if (!modelFiles.every(Boolean)) return null;
1324
+ progress.finish();
1325
+ if (failure !== null) throw failure;
1326
+ throwIfAborted(signal, "install");
1327
+ return completeValue({files: completeValue(modelFiles)});
1328
+ } catch (error) {
1329
+ throw failure ?? error;
1330
+ } finally {
1331
+ progress.dispose();
1332
+ }
1258
1333
  }
1259
1334
 
1260
1335
  async function install(source, { signal, onProgress = null } = {}) {
@@ -1263,7 +1338,7 @@ export function createDbopfsModelStore({
1263
1338
  }
1264
1339
  const names = storageName(source);
1265
1340
  const members = sourceMetadata(source).files;
1266
- const modelFiles = await availableResumableModelFiles(source, names, signal);
1341
+ let modelFiles;
1267
1342
  const linked = linkAbortSignal(signal);
1268
1343
  const downloadSignal = linked.controller.signal;
1269
1344
  let nextMemberIndex = 0;
@@ -1273,7 +1348,7 @@ export function createDbopfsModelStore({
1273
1348
  if (failure === null) failure = error;
1274
1349
  }
1275
1350
 
1276
- const progress = createDownloadProgressReporter(members, onProgress, retainFailure);
1351
+ const progress = createModelProgressReporter(members, onProgress, retainFailure);
1277
1352
 
1278
1353
  async function installMember(memberIndex) {
1279
1354
  let modelFile = null;
@@ -1338,12 +1413,14 @@ export function createDbopfsModelStore({
1338
1413
 
1339
1414
  try {
1340
1415
  progress.start();
1416
+ if (failure !== null) throw failure;
1417
+ modelFiles = await availableResumableModelFiles(source, names, downloadSignal, progress);
1418
+ if (failure !== null) throw failure;
1341
1419
  let pendingMembers = members.length;
1342
1420
  for (let memberIndex = 0; memberIndex < modelFiles.length; memberIndex += 1) {
1343
1421
  const modelFile = modelFiles[memberIndex];
1344
1422
  if (!modelFile) continue;
1345
1423
  pendingMembers -= 1;
1346
- progress.restoreMember(memberIndex);
1347
1424
  }
1348
1425
  const workers = [];
1349
1426
  const workerCount = Math.min(workerLimit, pendingMembers);
@@ -1392,10 +1469,11 @@ export function createDbopfsModelStore({
1392
1469
  unit: "files",
1393
1470
  heartbeat: false,
1394
1471
  }));
1395
- const cached = await openCached(source, { signal });
1472
+ const cached = await openCached(source, { signal, onProgress });
1396
1473
  if (cached) {
1397
1474
  const storage = await storagePolicy({ cached: true });
1398
1475
  onCapabilityPolicy?.(storage);
1476
+ throwIfAborted(signal, "install");
1399
1477
  return completeValue({ ...cached, cache: "cached", storage });
1400
1478
  }
1401
1479
  if (offline) {
@@ -178,13 +178,20 @@ export function createDbopfsModelPartStore({dbopfs, tableName}) {
178
178
  throw error;
179
179
  }
180
180
  }
181
- async function assemble(record, signal) {
181
+ async function assemble(record, signal, onProgress) {
182
182
  const parts = [];
183
+ function reportStoredPart() {
184
+ throwIfModelAborted(signal);
185
+ onProgress?.({phase: 'load', message: 'Reading stored model resource',
186
+ completed: parts.length, total: record.parts.length, unit: 'shards', cached: true});
187
+ }
188
+ reportStoredPart();
183
189
  for (const name of record.parts) {
184
190
  throwIfModelAborted(signal);
185
191
  const part = await file(name);
186
192
  if (!part) return null;
187
193
  parts.push(part);
194
+ reportStoredPart();
188
195
  }
189
196
  throwIfModelAborted(signal);
190
197
  return new Blob(parts);
@@ -209,18 +216,18 @@ export function createDbopfsModelPartStore({dbopfs, tableName}) {
209
216
  return readState(name);
210
217
  }, signal);
211
218
  }
212
- function read(name, {signal} = {}) {
219
+ function read(name, {signal, onProgress} = {}) {
213
220
  return locked(name, async function readCompleteModelParts() {
214
221
  throwIfModelAborted(signal);
215
222
  const record = await readState(name);
216
- return record?.complete ? assemble(record, signal) : null;
223
+ return record?.complete ? assemble(record, signal, onProgress) : null;
217
224
  }, signal);
218
225
  }
219
- function readPartial(name, {signal} = {}) {
226
+ function readPartial(name, {signal, onProgress} = {}) {
220
227
  return locked(name, async function readClosedModelParts() {
221
228
  throwIfModelAborted(signal);
222
229
  const record = await readState(name);
223
- return record ? assemble(record, signal) : null;
230
+ return record ? assemble(record, signal, onProgress) : null;
224
231
  }, signal);
225
232
  }
226
233
  function remove(name, {signal} = {}) {
@@ -93,11 +93,8 @@ export function createDbopfsResourceStore({dbopfs, tableName, fetchImpl = global
93
93
  const reusable = request.method === 'GET' && !request.headers.has('range')
94
94
  && !['reload', 'no-cache', 'no-store'].includes(request.cache);
95
95
  if (reusable && entry.metadata?.status === 200) {
96
- const file = await parts.read(entry.name, {signal});
96
+ const file = await parts.read(entry.name, {signal, onProgress: reportResourceProgress});
97
97
  if (file) {
98
- const state = await parts.state(entry.name, {signal});
99
- onProgress?.({phase: 'load', message: 'Reading stored model resource', completed: state.parts.length,
100
- total: state.parts.length, unit: 'shards', cached: true, url: request.url});
101
98
  return {...entry.metadata, file};
102
99
  }
103
100
  }
@@ -111,7 +108,7 @@ export function createDbopfsResourceStore({dbopfs, tableName, fetchImpl = global
111
108
  const encoding = previousHeaders.get('content-encoding');
112
109
  const partial = reusable && entry.metadata?.status === 200
113
110
  && (!encoding || encoding === 'identity')
114
- ? await parts.readPartial(entry.name, {signal}) : null;
111
+ ? await parts.readPartial(entry.name, {signal, onProgress: reportResourceProgress}) : null;
115
112
  try {
116
113
  if (partial?.size) {
117
114
  const headers = new Headers(request.headers);