arcane-os 0.80.0 → 0.81.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.81.0
4
+
5
+ - Add per-activation native decision `family`, `model`, `revision`, and `dtype`
6
+ selection through the direct runtime and Core service. Support Laya FP16 and
7
+ Julia 1 FP32 with their own model companions and tokenizer family, preserving
8
+ Laya FP32 defaults, complete input rows and actual output tensors. Keep
9
+ requested replacement metadata separate from the active model and retain
10
+ prepared files through actual worker exit.
11
+ - Report cached model loading as real stored-part progress and preserve known
12
+ GGUF shard totals. Forward complete progress details and cancellation through
13
+ cache reads without presenting cached data as a download or inventing totals
14
+ for an unfinished network stream.
15
+ - Add object-form `AI.streamRequest({speech:false})` for text-only operations.
16
+ Completion, failure and cancellation leave unrelated prepared, native and
17
+ buffered speech alone. Existing defaults, positional methods and explicit
18
+ speech APIs retain their behavior.
19
+ - Close and drain the Windows host after WebView2 `BrowserProcessExited`,
20
+ preserving its full diagnostic instead of leaving a blank window awaiting
21
+ manual closure. Existing renderer/Core-only handling and saved profiles stay
22
+ unchanged. This source correction does not establish the cause of the browser
23
+ exit; executable consumers require the matching rebuilt host archive.
24
+ - Keep dependencies, app-control, native GGUF activation and native Kokoro
25
+ routing unchanged. Source/package verification is separate from actual model,
26
+ audio or application execution.
27
+
3
28
  ## 0.80.0
4
29
 
5
30
  - 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.81.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);
@@ -1116,6 +1116,17 @@ prepared playback without invoking `finishTTS()` or `streamTTS()`. Unmuted
1116
1116
  completion keeps its ordinary speech flush. Explicit calls to either public
1117
1117
  speech method retain their existing behavior.
1118
1118
 
1119
+ For a text-only request, pass `speech:false` to the object-form
1120
+ `ai.streamRequest({messages, speech:false, onChunk, signal})`. Its completion,
1121
+ failure, and cancellation leave buffered speech, prepared playback, and native
1122
+ playback alone. The option controls only the request's automatic speech
1123
+ finalization and stopping; it is not sent to the language provider and does not
1124
+ mute or unload TTS. Explicit speech calls made by your callbacks still run with
1125
+ their ordinary behavior. Omitted `speech` or `speech:true` preserves the
1126
+ automatic lifecycle above, and positional `streamMessage(...)` is unchanged.
1127
+ Explicit `streamTTS()`, `finishTTS()`, and global `stopAudio()` keep their existing
1128
+ scope.
1129
+
1119
1130
  ## Automatic speech-input formatting cleanup
1120
1131
 
1121
1132
  The shared streaming, audio-file, and `SpeechPlayback` paths remove repeated
@@ -115,13 +115,17 @@ The controller emits `statechange` and `progress` through
115
115
  current mutable status snapshot. Provider states are `unloaded`, `loading`, `ready`,
116
116
  `unloading`, and `error`.
117
117
 
118
- Browser-WASM model loads report `cache-check`, `download`, and `initialize`
119
- phases. Download records preserve the ordered model-file fields `completed`,
118
+ Browser-WASM model loads report `cache-check`, `load`, `download`, and `initialize`
119
+ phases. Cache-read and download records preserve the ordered model-file fields `completed`,
120
120
  `total`, and `unit:'files'`. Their ordered `members` records contain each file's
121
121
  `name`, committed-part `completed`, `total`, and `unit:'shards'`. A streaming
122
122
  response has `total:null` until its end is observed; completion then supplies
123
- the actual committed-part count. Previously complete members identify
124
- `state:'cached'`. `activeTransfers` is the current transfer-worker count,
123
+ the actual committed-part count. Reading saved parts reports `phase:'load'`,
124
+ `cached:true`, and the known saved-part total before the first read and after
125
+ each part is opened. An existing HTTP Range plan likewise supplies its actual
126
+ shard total. Previously complete members identify `state:'cached'`; their
127
+ per-member records remain cache reads even if another member is downloading.
128
+ A fully cached activation emits no download phase. `activeTransfers` is the current transfer-worker count,
125
129
  `transferLimit` is the selected concurrency, and `transferMode` identifies
126
130
  `probing`, `files`, `ranges`, or `single`. No byte progress, rate, or estimated
127
131
  completion time is produced. Closed-part and lifecycle changes publish promptly;
@@ -136,7 +140,7 @@ These messages do not change the runtime's log level or replace its complete
136
140
  diagnostic output.
137
141
 
138
142
  Initialization has `total:null` and omits `completed` and `unit`, so its progress
139
- bar is indeterminate. Downloaded-file completion is not activation completion.
143
+ bar is indeterminate. Cached or downloaded file completion is not activation completion.
140
144
  The metadata tensor count and GPU layer assignment are descriptive only:
141
145
  upstream reports assigned layers before model-weight loading has finished.
142
146
  The pinned runtime does not expose a measured overall initialization fraction
@@ -658,10 +662,13 @@ contains `kind`, `tableName`, `downloadConcurrency`, the original `adapter`,
658
662
  and `ready`, `install`, `ensure`, `remove`, and `fetchResource`. `ensure()` preserves the complete
659
663
  ordered model set and reports whether it was cached or installed.
660
664
  `install(source,{signal,onProgress})` and
661
- `ensure(source,{signal,onProgress,offline})` publish `cache-check` and
662
- `download` records using ordered file counts and per-member closed-part progress
665
+ `ensure(source,{signal,onProgress,offline})` publish `cache-check`, cache-reading
666
+ `load`, and actual network `download` records using ordered file counts and per-member closed-part progress
663
667
  when `onProgress` is supplied. Each member reports `completed`, `total`, and
664
- `unit:'shards'`; streaming totals remain `null` until completion. Records retain
668
+ `unit:'shards'`; streaming totals remain `null` until completion. Cache reads
669
+ publish the known stored-part plan from zero through each opened part, with
670
+ `cached:true`; the existing Range plan supplies known totals during resume.
671
+ No model length is converted into product progress. Records retain
665
672
  `activeTransfers`, `transferLimit`, and `transferMode`, with prompt part/lifecycle
666
673
  updates and an existing 250 ms heartbeat. They contain no transfer rate or ETA.
667
674
  Multi-file sources use up to that many concurrent member workers
@@ -693,8 +700,12 @@ loaders share this owner for actual configuration, tokenizer, graph,
693
700
  external-data and support-file requests. Complete successful GET resources may
694
701
  be reused. Cancellation joins reader/writer cleanup and preserves closed parts
695
702
  for supported HTTP resume; a full response restarts that resource without
696
- duplicating its saved prefix. Resource progress uses `unit:'shards'` with
697
- `total:null` until completion. Independent resource requests remain concurrent.
703
+ duplicating its saved prefix. Resource progress uses `unit:'shards'`. A network
704
+ response without a known shard plan retains `total:null` until completion;
705
+ opening a saved complete or partial resource reports `phase:'load'`,
706
+ `cached:true`, and the known number of stored parts before and during the read.
707
+ The resource URL accompanies every update. These counts describe resource
708
+ preparation, never engine activation. Independent resource requests remain concurrent.
698
709
 
699
710
  ### Availability and normalization
700
711
 
@@ -592,6 +592,15 @@ before the window closes; complete engineering errors go to the supplied
592
592
  diagnostic/error callbacks. Callbacks must not synchronously wait for the
593
593
  window's lifetime promises.
594
594
 
595
+ A WebView2 `BrowserProcessExited` failure retains its complete diagnostic,
596
+ fails a pending `Ready`, and begins the existing Core drain and window close.
597
+ It does not wait for a user to close a blank window or invoke document callbacks
598
+ and script cleanup in the closed browser. The launcher reports the failure
599
+ after that drain. Main-renderer exits and Core-only transport failures retain
600
+ their existing handling. This terminal-flow correction does not identify or
601
+ prevent the underlying browser-process exit, restart the application, or change
602
+ its profile and saved data.
603
+
595
604
  `ArcaneLauncher.cs` supplies the executable entry point for that composition.
596
605
  `tools/build-core-windows-host.mjs` builds the selected SDK host output from the
597
606
  approved WebView2 package, existing .NET Framework compiler and selected Node
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "sdkVersion": "0.79.0",
3
+ "sdkVersion": "0.80.0",
4
4
  "environment": {
5
5
  "runtime": "Node.js for Node entrypoints; browser for browser-only entrypoints",
6
6
  "minimumVersion": "22.23.2 for Node entrypoints",
@@ -45,14 +45,14 @@
45
45
  "name": "createNativeDecisionModel",
46
46
  "displayName": "createNativeDecisionModel()",
47
47
  "kind": "function",
48
- "signature": "createNativeDecisionModel({onnx,modelAssets,workingDirectory,paths,assetProjectionId,resourcePaths,model='onnx-community/laya-typed-decisions-ONNX',revision='main',dtype='fp32',sessionOptions,executionPreference='gpu',executionTarget,signal}={})",
48
+ "signature": "createNativeDecisionModel({onnx,modelAssets,workingDirectory,paths,assetProjectionId,resourcePaths,family='laya',model='onnx-community/laya-typed-decisions-ONNX',revision='main',dtype='fp32',sessionOptions,executionPreference='gpu',executionTarget,signal}={})",
49
49
  "entrypoints": ["arcane-os/local-ai/decisions"],
50
50
  "primaryImport": "arcane-os/local-ai/decisions",
51
51
  "group": "Native typed decisions",
52
- "summary": "Explicit native Laya FP32 option scoring through an existing ONNX owner and a tokenizer Worker.",
52
+ "summary": "Explicit native Laya FP32/FP16 or Julia FP32 option scoring through an existing ONNX owner and a tokenizer Worker.",
53
53
  "availability": "Node on Windows/Linux/macOS with selected ONNX Runtime and model files; Android needs host adaptation",
54
- "protocol": "Shared browser/native decision encoding with native named tensor results",
55
- "normalization": "Returns load/evaluate/classify/current/status/subscribe/unload/dispose; preserves complete separate state/question/options/type and original rows, raw scores and every graph output. load({assetProjectionId,resourcePaths,executionTarget,signal}) retains omitted or undefined source, mapping and target fields from the latest selection, initially the factory configuration. A supplied ready projection takes precedence over configured paths and upstream settings; the existing modelAssets owner retains it directly without upstream preparation or fetching. resourcePaths maps model, tokenizer and tokenizerConfig to exact projection member.path values; complete companion files remain at their original relative paths. Missing projections or mapped members report their actual failure. assetProjectionId:null returns to configured paths or existing upstream preparation when no paths were configured. Separately, executionTarget:null requests automatic resolution, while {deviceId} forwards the saved physical selection to the existing ONNX owner. With no target selection, GPU preference still attempts advertised providers then CPU and explicit session providers retain precedence. Equal source, complete mapping and target selections share activation; a changed selection cancels the earlier request and joins its actual ONNX/tokenizer cleanup before replacement. Incoming projection use is retained before retiring the previous activation; caller preparation ownership is independent, and native files remain retained through actual ONNX and tokenizer Worker exit. Unload ends native use; loading again after all ownership is released requires a fresh projection. pendingActivation reports the requested replacement and projected source/mapping separately; current().execution remains the complete ONNX load record, with requested/resolved/configured/observed target diagnostics in current().execution.execution. Configuration alone does not prove physical execution. Cancellation retires the shared activation. No preference writes, chat history or duplicate inference engine."
54
+ "protocol": "Shared browser/native decision encoding with native named tensor results. Factory and each load accept family/model/revision/dtype; omission or undefined retains the selected value. Laya FP32 defaults remain. Laya FP16 selects onnx/model_fp16.onnx and onnx/model_fp16.onnx_data; Julia FP32 selects model.onnx and model.onnx.data, with its own tokenizer files. Complete new-family selection and matching files are caller-owned. All model fields participate in activation equality; top-level metadata stays attributable to the retiring activation while pendingActivation records its requested successor. Decoder preserves raw probabilities, every original row and every actual output without inventing Julia action logits.",
55
+ "normalization": "Returns load/evaluate/classify/current/status/subscribe/unload/dispose; preserves complete separate state/question/options/type and original rows, raw scores and every graph output. load({family,model,revision,dtype,assetProjectionId,resourcePaths,executionTarget,signal}) retains omitted or undefined family, model, revision, dtype, source, mapping and target fields from the latest selection, initially the factory configuration. A supplied ready projection takes precedence over configured paths and upstream settings; the existing modelAssets owner retains it directly without upstream preparation or fetching. resourcePaths maps model, tokenizer and tokenizerConfig to exact projection member.path values; complete companion files remain at their original relative paths. Missing projections or mapped members report their actual failure. assetProjectionId:null returns to configured paths or existing upstream preparation when no paths were configured. Separately, executionTarget:null requests automatic resolution, while {deviceId} forwards the saved physical selection to the existing ONNX owner. With no target selection, GPU preference still attempts advertised providers then CPU and explicit session providers retain precedence. Equal family, model, revision, dtype, source, complete mapping and target selections share activation; a changed selection cancels the earlier request and joins its actual ONNX/tokenizer cleanup before replacement. Incoming projection use is retained before retiring the previous activation; caller preparation ownership is independent, and native files remain retained through actual ONNX and tokenizer Worker exit. Unload ends native use; loading again after all ownership is released requires a fresh projection. pendingActivation reports the requested replacement and projected source/mapping separately; current().execution remains the complete ONNX load record, with requested/resolved/configured/observed target diagnostics in current().execution.execution. Configuration alone does not prove physical execution. Cancellation retires the shared activation. No preference writes, chat history or duplicate inference engine."
56
56
  },
57
57
  {
58
58
  "id": "core-decisions:createNativeDecisionService",
@@ -63,10 +63,10 @@
63
63
  "entrypoints": ["arcane-os/core/decisions"],
64
64
  "primaryImport": "arcane-os/core/decisions",
65
65
  "group": "Native typed decisions",
66
- "summary": "Renderer-independent native Laya service reusing the registered local-AI and model-assets owners.",
66
+ "summary": "Renderer-independent native Laya/Julia service reusing the registered local-AI and model-assets owners.",
67
67
  "availability": "Node Core with selected native ONNX and model files",
68
- "protocol": "arcane/1 decisions.status/load/evaluate/unload and decisions.state",
69
- "normalization": "Registration starts no model or tokenizer. Explicit load obtains dependency-ready same-process owners and reacquires getONNXRuntime after recovery. Factory configuration accepts assetProjectionId, resourcePaths and executionTarget; direct load({assetProjectionId,resourcePaths,executionTarget,signal}) and Core decisions.load({assetProjectionId,resourcePaths,executionTarget}) preserve omitted/undefined selections, with the Core request signal forwarded separately. resourcePaths maps model, tokenizer and tokenizerConfig to exact ready projection member.path values; complete companion members retain their relative paths. Supplied projections take precedence over configured paths/upstream settings, are retained through the existing model-assets owner, and never invoke upstream preparation or fetching; unavailable projections or mapped members fail directly. assetProjectionId:null returns to configured paths or existing upstream preparation, independently of executionTarget:null requesting automatic device selection. Equal source, complete mapping and target selections coalesce; changed selections cancel pending work and join the prior model's disposal before replacement. Incoming retained ownership protects projection files while the previous activation retires; caller preparation ownership remains independent, and native use retains files through actual ONNX/tokenizer exit. pendingActivation includes a projected source/mapping separately from the full activation execution record and its nested execution diagnostics. Native load/evaluate/classify/current/subscribe/unload/dispose preserve complete results; RPC uses the existing tensor codec. Unload has accepted service lifetime; host shutdown cancels request-lifetime load/evaluation."
68
+ "protocol": "arcane/1 decisions.status/load/evaluate/unload and decisions.state. Factory, direct load and decisions.load accept family/model/revision/dtype with the direct model's exact variant, omission and replacement semantics. The service persists across explicit model changes; top-level activation metadata and pendingActivation distinguish the retiring and requested selections.",
69
+ "normalization": "Registration starts no model or tokenizer. Explicit load obtains dependency-ready same-process owners and reacquires getONNXRuntime after recovery. Factory configuration accepts family, model, revision, dtype, assetProjectionId, resourcePaths and executionTarget; direct load({family,model,revision,dtype,assetProjectionId,resourcePaths,executionTarget,signal}) and Core decisions.load({family,model,revision,dtype,assetProjectionId,resourcePaths,executionTarget}) preserve omitted/undefined selections, with the Core request signal forwarded separately. resourcePaths maps model, tokenizer and tokenizerConfig to exact ready projection member.path values; complete companion members retain their relative paths. Supplied projections take precedence over configured paths/upstream settings, are retained through the existing model-assets owner, and never invoke upstream preparation or fetching; unavailable projections or mapped members fail directly. assetProjectionId:null returns to configured paths or existing upstream preparation, independently of executionTarget:null requesting automatic device selection. Equal family, model, revision, dtype, source, complete mapping and target selections coalesce; changed selections cancel pending work and join the prior model's disposal before replacement. Incoming retained ownership protects projection files while the previous activation retires; caller preparation ownership remains independent, and native use retains files through actual ONNX/tokenizer exit. pendingActivation includes a projected source/mapping separately from the full activation execution record and its nested execution diagnostics. Native load/evaluate/classify/current/subscribe/unload/dispose preserve complete results; RPC uses the existing tensor codec. Unload has accepted service lifetime; host shutdown cancels request-lifetime load/evaluation."
70
70
  },
71
71
  {
72
72
  "id": "core-decisions:default",
@@ -1,7 +1,7 @@
1
- # Native Laya typed decisions
1
+ # Native typed decisions
2
2
 
3
3
  `arcane-os/local-ai/decisions` and `arcane-os/core/decisions` run the selected
4
- Laya FP32 graph through the SDK's existing native ONNX owner. Tokenization runs
4
+ Laya FP32, Laya FP16 or Julia FP32 graph through the SDK's existing native ONNX owner. Tokenization runs
5
5
  in its own Node Worker. A Core service can load and evaluate decisions without
6
6
  a renderer, browser session, WebGPU browser context or chat provider.
7
7
 
@@ -58,7 +58,7 @@ name to `decisions`. It exposes `load`, `evaluate`, `classify` (an alias for
58
58
  | Method | Parameters and result |
59
59
  | --- | --- |
60
60
  | `decisions.status` | Current lifecycle snapshot; does not load anything. |
61
- | `decisions.load` | `{assetProjectionId?, resourcePaths?, executionTarget?}`; explicit activation or source/device replacement, returning the ready lifecycle snapshot. The request signal cancels activation. A target is `{deviceId:string}` or `null`. |
61
+ | `decisions.load` | `{family?, model?, revision?, dtype?, assetProjectionId?, resourcePaths?, executionTarget?}`; explicit activation or model/source/device replacement, returning the ready lifecycle snapshot. The request signal cancels activation. A target is `{deviceId:string}` or `null`. |
62
62
  | `decisions.evaluate` | `{rows, runOptions?}`; returns `{decisions, outputs}`. The request signal cancels the active model operation. |
63
63
  | `decisions.unload` | Releases the activation. Service-lifetime cleanup continues after the caller disconnects. |
64
64
 
@@ -94,11 +94,18 @@ const decisions = createNativeDecisionModel({
94
94
  These direct paths are native filesystem paths. Keep the graph's external data
95
95
  beside it under the exact filename required by the graph. The Core service
96
96
  resolves configured `paths` relative to `appRoot`; it does not copy or remove
97
- caller-owned files. For SDK-prepared files, the default selected upstream is
98
- `onnx-community/laya-typed-decisions-ONNX`, revision `main`, with exactly:
97
+ caller-owned files. Factory defaults remain `family:'laya'`,
98
+ `model:'onnx-community/laya-typed-decisions-ONNX'`, `revision:'main'` and
99
+ `dtype:'fp32'`. SDK upstream preparation selects these exact layouts:
99
100
 
100
- - `onnx/model.onnx` and `onnx/model.onnx_data`;
101
- - `tokenizer.json` and `tokenizer_config.json`.
101
+ | Family and dtype | Graph | Complete external data |
102
+ | --- | --- | --- |
103
+ | `laya`, `fp32` | `onnx/model.onnx` | `onnx/model.onnx_data` |
104
+ | `laya`, `fp16` | `onnx/model_fp16.onnx` | `onnx/model_fp16.onnx_data` |
105
+ | `julia`, `fp32` | `model.onnx` | `model.onnx.data` |
106
+
107
+ Each selection uses `tokenizer.json` and `tokenizer_config.json` from its own
108
+ selected repository. Julia's public repository is `SupersonicLabs/Julia-1-ONNX`.
102
109
 
103
110
  ## Load complete files already stored in DBOPFS
104
111
 
@@ -144,7 +151,7 @@ After loading, invoke `decisions.evaluate` with the complete `rows`, then
144
151
  `decisions.unload` when that native activation is no longer needed.
145
152
 
146
153
  Direct model and service calls accept
147
- `load({assetProjectionId, resourcePaths, executionTarget, signal})`; their
154
+ `load({family, model, revision, dtype, assetProjectionId, resourcePaths, executionTarget, signal})`; their
148
155
  factory configuration accepts the same source selection. A direct model needs
149
156
  the existing `modelAssets` owner when selecting a projection. `resourcePaths`
150
157
  maps all three roles (`model`, `tokenizer`, `tokenizerConfig`) to exact
@@ -161,7 +168,8 @@ Omitted or `undefined` source fields retain the selected source and mappings.
161
168
  `assetProjectionId: null` returns to the factory's configured `paths`, or its
162
169
  existing upstream preparation when no paths were configured. Selecting another
163
170
  projection or changing any resource mapping replaces the activation even when
164
- the execution target stays the same. Equal source, mapping and device selections
171
+ the execution target stays the same. Equal family, model, revision, dtype,
172
+ source, mapping and device selections
165
173
  coalesce. The incoming retain is acquired before the prior activation retires,
166
174
  so device replacement can reuse a projection after the caller has released its
167
175
  preparation ownership. Superseded pending retains are released as their cleanup
@@ -178,8 +186,12 @@ model store. The approved `@huggingface/tokenizers@0.2.0` dependency reads the
178
186
  selected tokenizer files in its Worker. No Transformers or second ONNX runtime
179
187
  is loaded by this API.
180
188
 
181
- This graph selection requires `dtype:'fp32'`, which is also the default; another
182
- dtype reports the incompatible selection rather than substituting a graph.
189
+ Native graph selections support Laya `fp32` and `fp16`, and Julia `fp32`.
190
+ Other family/precision combinations report an unsupported selection. Laya FP16
191
+ uses the published mixed-precision graph, which retains selected operations
192
+ and outputs in FP32; it does not claim every graph operation uses FP16. Julia
193
+ uses its typed five-input ONNX graph and raw option logits, without the upstream
194
+ display-rounded `predict()` wrapper or invented action outputs.
183
195
  `sessionOptions` and `runOptions` pass to the existing native ONNX API unchanged.
184
196
  With no `executionTarget` selection, `executionPreference` defaults to `gpu`
185
197
  here: actual advertised native GPU provider creation is attempted, followed by
@@ -189,6 +201,49 @@ continues to default to CPU. Accepted provider configuration is not evidence
189
201
  that GPU nodes executed. Full provider-attempt diagnostics remain in
190
202
  `execution`; see [native ONNX sessions](local-ai.md#native-onnx-sessions).
191
203
 
204
+ ## Select an activation's model
205
+
206
+ Both factories and each direct/Core `load` accept `family`, `model`, `revision`
207
+ and `dtype`. The same service persists while its selected activation changes:
208
+
209
+ ```js
210
+ await client.invoke('decisions.load', {
211
+ family: 'julia',
212
+ model: 'SupersonicLabs/Julia-1-ONNX',
213
+ revision: 'main',
214
+ dtype: 'fp32',
215
+ assetProjectionId: juliaProjection.id,
216
+ resourcePaths: {
217
+ model: 'model.onnx',
218
+ tokenizer: 'tokenizer.json',
219
+ tokenizerConfig: 'tokenizer_config.json'
220
+ }
221
+ }, {signal, timeoutMs: 0});
222
+ ```
223
+
224
+ Here `juliaProjection` is the caller's ready projection containing Julia's
225
+ complete graph, `model.onnx.data` and its own tokenizer files. The caller may
226
+ release its preparation ownership after loading, as in the projection example.
227
+ For upstream preparation without configured paths, select the same four model
228
+ fields with `assetProjectionId: null`.
229
+
230
+ Omitting a model field, or supplying `undefined`, retains its most recent
231
+ selection, initially the factory default. The selection survives unload for a
232
+ later explicit load. Changing any field replaces the activation; equal complete
233
+ selections share it. Supply the complete new model selection when changing
234
+ family, including its matching projection/mapping or configured native files.
235
+ Changing `family` alone does not infer a repository, change precision, replace
236
+ configured files, or rewrite a projection. Metadata describes the caller's
237
+ selection; the SDK does not inspect weights to establish checkpoint identity.
238
+ Configured `paths` and a selected projection keep their existing precedence
239
+ over upstream preparation.
240
+
241
+ Top-level `current().family`, `model`, `revision` and `dtype` follow the
242
+ activation being loaded, used or retired. While replacement waits for prior
243
+ cleanup, those fields and `execution` still describe the prior activation;
244
+ `pendingActivation` carries the requested successor separately. The new
245
+ selection takes ownership when its activation begins.
246
+
192
247
  ## Select an activation's execution device
193
248
 
194
249
  Both factories accept `executionTarget: {deviceId:string}` or `null` in their
@@ -228,15 +283,16 @@ resolution, accepted configuration and actual execution evidence. An unknown
228
283
  `observedTarget` remains `null`; successful configuration alone does not establish
229
284
  which device executed graph nodes.
230
285
 
231
- `pendingActivation` is separately `null` or `{executionTarget}` while a load or
232
- replacement is pending; its `executionTarget` property is omitted at JSON
286
+ `pendingActivation` is separately `null` or a selection containing `family`,
287
+ `model`, `revision`, `dtype` and `executionTarget` while a load or replacement
288
+ is pending; its `executionTarget` property is omitted at JSON
233
289
  transport when the selection is `undefined`. A projected selection also includes
234
290
  `assetProjectionId` and its complete `resourcePaths` mapping. A pending target never overwrites the previous
235
291
  activation's `execution` record. The old record remains attributable to the
236
292
  retiring activation until its cleanup completes, then the new activation owns
237
293
  its own record. A request is not a claim that the requested device is running.
238
294
 
239
- Concurrent loads for the same source, resource mapping and target share their
295
+ Concurrent loads for the same family, model, revision, dtype, source, resource mapping and target share their
240
296
  pending activation. A different selection supersedes the earlier pending request, which rejects with
241
297
  cancellation instead of returning another target's ready result. Replacement
242
298
  joins this model's actual tokenizer/native cleanup before creating its
@@ -280,7 +336,8 @@ progress includes model preparation, tokenization and evaluation; downloaded
280
336
  members report files, not byte progress. Complete technical errors belong in
281
337
  developer diagnostics, while the application owns its user-facing status.
282
338
 
283
- Concurrent callers selecting the same source, mapping and target share one explicit activation.
339
+ Concurrent callers selecting the same family, model, revision, dtype, source,
340
+ mapping and target share one explicit activation.
284
341
  Cancelling a load or evaluation cancels that activation and its outstanding operations, because the
285
342
  native session and tokenizer have one owner. Use separately owned models when
286
343
  independent cancellation is required. The construction `signal` governs the
@@ -304,5 +361,6 @@ availability. Android requires the host's supported native adaptation. Static
304
361
  source review and synthetic authored fixtures are not model execution or a
305
362
  claim of tested platform, precision or accelerator behavior.
306
363
 
307
- Upstream selection: [Laya FP32 model files](https://huggingface.co/onnx-community/laya-typed-decisions-ONNX/tree/main),
364
+ Upstream selection: [Laya model files](https://huggingface.co/onnx-community/laya-typed-decisions-ONNX/tree/main),
365
+ [Julia-1 model files](https://huggingface.co/SupersonicLabs/Julia-1-ONNX/tree/main),
308
366
  [Tokenizer package](https://www.npmjs.com/package/@huggingface/tokenizers).
@@ -69,7 +69,7 @@ runtime layouts.
69
69
  | `arcane-os/core/local-ai` | Native llama.cpp, Ollama and ONNX Core service, model readiness, complete responses and owned lifecycle. See [local AI through Core](local-ai.md). |
70
70
  | `arcane-os/local-ai` | Official runtime installation and native artifact bundling for selected llama.cpp/Ollama/ONNX/image requirements and NeMo libraries. |
71
71
  | `arcane-os/local-ai/onnx` | Retained native ONNX worker sessions with explicit graph loading and complete tensor inference. |
72
- | `arcane-os/local-ai/decisions` | Explicit native Laya FP32 decisions using the existing ONNX owner and a tokenizer Worker. |
72
+ | `arcane-os/local-ai/decisions` | Explicit native Laya FP32/FP16 and Julia FP32 decisions using the existing ONNX owner and a tokenizer Worker. |
73
73
  | `arcane-os/core/decisions` | Renderer-independent typed-decision service composed with existing local-AI and model-assets owners. |
74
74
  | `arcane-os/ai/core-onnx` | Optional browser access to system ONNX through an existing Core connection. |
75
75
  | `arcane-os/local-ai/image` | Retained native stable-diffusion.cpp context, complete PNG generation and owned worker lifetime. |
@@ -186,7 +186,7 @@ The remaining data-only subpaths are eight JSON Schemas and package metadata.
186
186
  | `acceptWebSocket()` | function | `arcane-os`, `arcane-os/websocket` | WebSocket protocol | Node; browser-native client interoperability |
187
187
  | `WS` | class | `arcane-os`, `arcane-os/websocket-client` | Shared WebSocket clients | Node and native-WebSocket browser hosts |
188
188
  | `createBrowserDecisionModel()` | function | `arcane-os/ai/browser-decisions` | Browser typed decisions | Browser module Workers and the caller-selected inference backend |
189
- | `createNativeDecisionModel()` | function | `arcane-os/local-ai/decisions` | Native typed decisions | Node with an existing native ONNX owner and selected Laya FP32 files |
189
+ | `createNativeDecisionModel()` | function | `arcane-os/local-ai/decisions` | Native typed decisions | Node with an existing native ONNX owner and selected Laya FP32/FP16 or Julia FP32 files |
190
190
  | `createNativeDecisionService()` | function | `arcane-os/core/decisions` | Native typed decisions | Node Core with existing local-AI and model-assets services |
191
191
  | `createNativeDecisionService default export` | function | `arcane-os/core/decisions` | Native typed decisions | Same native decision service factory |
192
192
  | `fetchRequest()` | function | `arcane-os/ai/twin-cloud` | TWiN Cloud requests | Node and Browser; remote HTTPS provider |
@@ -9505,23 +9505,35 @@ native installation eligibility and URL-bar promotion.
9505
9505
 
9506
9506
  ### Overview
9507
9507
 
9508
- Runs explicitly selected Laya FP32 option scoring through an existing native
9508
+ Runs explicitly selected Laya FP32/FP16 or Julia FP32 option scoring through an existing native
9509
9509
  ONNX owner, with tokenization in a Node Worker. It creates no second inference
9510
9510
  engine and starts no model download at construction.
9511
9511
 
9512
9512
  ### Signature and result
9513
9513
 
9514
9514
  ```text
9515
- createNativeDecisionModel({onnx,modelAssets,workingDirectory,paths,model='onnx-community/laya-typed-decisions-ONNX',revision='main',dtype='fp32',sessionOptions,executionPreference='gpu',signal}={})
9515
+ createNativeDecisionModel({onnx,modelAssets,workingDirectory,paths,assetProjectionId,resourcePaths,family='laya',model='onnx-community/laya-typed-decisions-ONNX',revision='main',dtype='fp32',sessionOptions,executionPreference='gpu',executionTarget,signal}={})
9516
9516
  ```
9517
9517
 
9518
9518
  Import from `arcane-os/local-ai/decisions`. Supply the existing `onnx` owner and
9519
9519
  either `modelAssets` plus `workingDirectory`, or native `paths` for the model,
9520
- tokenizer and tokenizer configuration. The selected graph requires FP32. The
9520
+ tokenizer and tokenizer configuration, or a ready `assetProjectionId` with
9521
+ `resourcePaths` for those three roles. Supported selections are Laya `fp32`,
9522
+ Laya `fp16` and Julia `fp32`; constructor defaults remain Laya FP32. The
9521
9523
  handle exposes `load`, `evaluate`, `classify`, `current`, `status`, `subscribe`,
9522
9524
  `unload` and terminal `dispose`; `classify` aliases `evaluate` and `status`
9523
9525
  aliases `current`.
9524
9526
 
9527
+ `load({family,model,revision,dtype,assetProjectionId,resourcePaths,executionTarget,signal})`
9528
+ selects each activation. Omitted or `undefined` fields retain their prior
9529
+ selection. A changed model field, source, mapping or device replaces the
9530
+ activation after actual cleanup; equal complete selections coalesce. Supply
9531
+ all matching model fields and files when changing family: metadata does not
9532
+ infer a repository or rewrite a supplied projection. Top-level model metadata
9533
+ continues to describe the retiring activation while `pendingActivation` records
9534
+ its successor. See [model selection](native-decisions.md#select-an-activations-model)
9535
+ for exact upstream graph/companion layouts and retained projection ownership.
9536
+
9525
9537
  `evaluate(rows,{signal,runOptions})` preserves each complete `state`, `question`,
9526
9538
  `options` and optional `type` separately. It returns `{decisions,outputs}` with
9527
9539
  original rows, typed values, raw logits, unrounded probabilities and every
@@ -9574,7 +9586,11 @@ reacquires `getONNXRuntime()` after any recovery. Native members are `load`,
9574
9586
  `start(context)`.
9575
9587
 
9576
9588
  RPC exposes `decisions.status`, `decisions.load`, `decisions.evaluate` and
9577
- `decisions.unload`. Evaluation accepts `{rows,runOptions?}` and returns complete
9589
+ `decisions.unload`. Load accepts optional `family`, `model`, `revision`, `dtype`,
9590
+ `assetProjectionId`, `resourcePaths` and `executionTarget`, with the same
9591
+ per-activation selection and omission behavior as direct native load; the Core
9592
+ request signal owns cancellation. The service persists across model changes.
9593
+ Evaluation accepts `{rows,runOptions?}` and returns complete
9578
9594
  `{decisions,outputs}` using the existing tensor codec at the JSON boundary.
9579
9595
  `decisions.state` and replaying `subscribe(listener,{emitCurrent:true,signal})`
9580
9596
  report lifecycle. Unload is accepted service-lifetime cleanup; host shutdown
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.80.0",
3
+ "version": "0.81.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -5018,6 +5018,7 @@ class AI {
5018
5018
  model,
5019
5019
  structuredOutput=false,
5020
5020
  localOnly=false,
5021
+ speech=true,
5021
5022
  onChunk=function ignoreStreamChunk(){},
5022
5023
  toolText,
5023
5024
  onToolText,
@@ -5062,6 +5063,9 @@ class AI {
5062
5063
  if(localOnly!==true&&localOnly!==false){
5063
5064
  throw new TypeError('AI localOnly must be a boolean.');
5064
5065
  }
5066
+ if(speech!==true&&speech!==false){
5067
+ throw new TypeError('AI speech must be a boolean.');
5068
+ }
5065
5069
  if(localOnly&&!this.#hasLocalRoute('llm',this.llmService)){
5066
5070
  const error=new Error(
5067
5071
  'This AI request requires a configured local model.'
@@ -5163,7 +5167,7 @@ class AI {
5163
5167
  if(signal?.aborted){
5164
5168
  throw normalizeAIRequestAbort();
5165
5169
  }
5166
- this.#finishStreamingSpeech();
5170
+ if(speech)this.#finishStreamingSpeech();
5167
5171
  return result;
5168
5172
  }catch(error){
5169
5173
  if(handle){
@@ -5171,7 +5175,7 @@ class AI {
5171
5175
  function retainProviderStreamCleanupFailure() {}
5172
5176
  );
5173
5177
  }
5174
- this.stopAudio();
5178
+ if(speech)this.stopAudio();
5175
5179
  throw isAIRequestAbort(error,signal)
5176
5180
  ?normalizeAIRequestAbort(error)
5177
5181
  :error;
@@ -5223,10 +5227,10 @@ class AI {
5223
5227
  if(signal?.aborted){
5224
5228
  throw normalizeAIRequestAbort();
5225
5229
  }
5226
- this.#finishStreamingSpeech();
5230
+ if(speech)this.#finishStreamingSpeech();
5227
5231
  return result;
5228
5232
  }catch(error){
5229
- this.stopAudio();
5233
+ if(speech)this.stopAudio();
5230
5234
  throw isAIRequestAbort(error,signal)
5231
5235
  ?normalizeAIRequestAbort(error)
5232
5236
  :error;
@@ -6026,7 +6030,8 @@ class AI {
6026
6030
  }
6027
6031
  throw error;
6028
6032
  }finally{
6029
- if(!speechTurnCompleted){
6033
+ // Provider transports leave speech lifecycle to their public caller.
6034
+ if(finishSpeech&&!speechTurnCompleted){
6030
6035
  this.stopAudio();
6031
6036
  }
6032
6037
  }
@@ -124,6 +124,7 @@ namespace Arcane.Core.Hosts.Windows
124
124
  private bool started;
125
125
  private bool documentReady;
126
126
  private bool deliveryScheduled;
127
+ private bool browserProcessExited;
127
128
  private bool closing;
128
129
  private bool closeAllowed;
129
130
 
@@ -551,11 +552,26 @@ namespace Arcane.Core.Hosts.Windows
551
552
  {
552
553
  error = new AggregateException("Reading WebView2 process-failure diagnostics failed.", error, diagnosticError);
553
554
  }
554
- // Only loss of the browser or top-level renderer ends this bridge.
555
+ if (kind == CoreWebView2ProcessFailedKind.BrowserProcessExited)
556
+ {
557
+ // WebView2 has closed this browser. Complete the owned host
558
+ // lifetime without trying to notify its vanished document.
559
+ browserProcessExited = true;
560
+ lock (stateLock)
561
+ {
562
+ if (transportFailure == null) transportFailure = error;
563
+ }
564
+ Report(error);
565
+ ready.TrySetException(error);
566
+ if (bridge != null) bridge.Stop(error);
567
+ if (activeBridge != null) activeBridge.Stop(error);
568
+ BeginClose();
569
+ return;
570
+ }
571
+ // Loss of the top-level renderer ends its bridge.
555
572
  // GPU/utility/plugin helpers recover independently; subframe loss,
556
573
  // unresponsiveness and unspecified failures remain full diagnostics.
557
- if (kind == CoreWebView2ProcessFailedKind.BrowserProcessExited
558
- || kind == CoreWebView2ProcessFailedKind.RenderProcessExited) CoreFailed(error);
574
+ if (kind == CoreWebView2ProcessFailedKind.RenderProcessExited) CoreFailed(error);
559
575
  else Report(error);
560
576
  }
561
577
 
@@ -665,7 +681,7 @@ namespace Arcane.Core.Hosts.Windows
665
681
 
666
682
  private void NotifyTransportFailure()
667
683
  {
668
- if (!documentReady || webView.CoreWebView2 == null || transportFailure == null || closing) return;
684
+ if (closing || browserProcessExited || !documentReady || webView.CoreWebView2 == null || transportFailure == null) return;
669
685
  if (failureNotifiedGeneration == generation) return;
670
686
  failureNotifiedGeneration = generation;
671
687
  Task failureNotification = NotifyTransportFailureAsync(transportFailure);
@@ -761,7 +777,7 @@ namespace Arcane.Core.Hosts.Windows
761
777
  ready.TrySetCanceled();
762
778
  try
763
779
  {
764
- if (webView.CoreWebView2 != null)
780
+ if (!browserProcessExited && webView.CoreWebView2 != null)
765
781
  {
766
782
  webView.CoreWebView2.RemoveHostObjectFromScript("arcaneBridge");
767
783
  if (injectedScript != null) webView.CoreWebView2.RemoveScriptToExecuteOnDocumentCreated(injectedScript);
@@ -10,6 +10,7 @@ function sameExecutionTarget(left, right) {
10
10
 
11
11
  function sameActivation(left, right) {
12
12
  if (!left || !right || !sameExecutionTarget(left.executionTarget, right.executionTarget) || left.assetProjectionId !== right.assetProjectionId) return false;
13
+ if (left.family !== right.family || left.model !== right.model || left.revision !== right.revision || left.dtype !== right.dtype) return false;
13
14
  if (left.resourcePaths === right.resourcePaths) return true;
14
15
  if (!left.resourcePaths || !right.resourcePaths) return false;
15
16
  const entries = Object.entries(left.resourcePaths);
@@ -32,6 +33,10 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
32
33
  let closing;
33
34
  let unsubscribe;
34
35
  let selectedActivation = {
36
+ family: configuration.family === undefined ? 'laya' : configuration.family,
37
+ model: configuration.model === undefined ? 'onnx-community/laya-typed-decisions-ONNX' : configuration.model,
38
+ revision: configuration.revision === undefined ? 'main' : configuration.revision,
39
+ dtype: configuration.dtype === undefined ? 'fp32' : configuration.dtype,
35
40
  executionTarget: configuration.executionTarget,
36
41
  ...(configuration.assetProjectionId == null ? {} : {assetProjectionId: configuration.assetProjectionId, resourcePaths: configuration.resourcePaths && {...configuration.resourcePaths}})
37
42
  };
@@ -40,7 +45,7 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
40
45
 
41
46
  function current() {
42
47
  const snapshot = model?.current() ?? {
43
- family: 'laya', model: configuration.model ?? 'onnx-community/laya-typed-decisions-ONNX',
48
+ family: configuration.family ?? 'laya', model: configuration.model ?? 'onnx-community/laya-typed-decisions-ONNX',
44
49
  revision: configuration.revision ?? 'main', dtype: configuration.dtype ?? 'fp32',
45
50
  state: closing ? 'disposed' : 'unloaded', loaded: false, busy: Boolean(loading),
46
51
  activeRequests: 0, progress: null, error: null, execution: null
@@ -66,12 +71,19 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
66
71
 
67
72
  function operationSignal(signal) { return signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal; }
68
73
 
69
- function load({signal, executionTarget: requestedTarget, assetProjectionId: requestedProjection, resourcePaths: requestedPaths} = {}) {
74
+ function load({
75
+ family: requestedFamily, model: requestedModel, revision: requestedRevision, dtype: requestedDtype,
76
+ signal, executionTarget: requestedTarget, assetProjectionId: requestedProjection, resourcePaths: requestedPaths
77
+ } = {}) {
70
78
  operationSignal(signal).throwIfAborted();
71
79
  if (!context) throw new CoreError({code: 'CORE_NOT_READY', message: 'Register and start the decision service with Core before loading.'});
72
80
  const projection = requestedProjection === undefined ? selectedActivation.assetProjectionId : requestedProjection;
73
81
  const mapping = requestedPaths === undefined ? selectedActivation.resourcePaths : requestedPaths;
74
82
  const selection = {
83
+ family: requestedFamily === undefined ? selectedActivation.family : requestedFamily,
84
+ model: requestedModel === undefined ? selectedActivation.model : requestedModel,
85
+ revision: requestedRevision === undefined ? selectedActivation.revision : requestedRevision,
86
+ dtype: requestedDtype === undefined ? selectedActivation.dtype : requestedDtype,
75
87
  executionTarget: requestedTarget === undefined ? selectedActivation.executionTarget : requestedTarget,
76
88
  ...(projection == null ? {} : {assetProjectionId: projection, resourcePaths: mapping && typeof mapping === 'object' && !Array.isArray(mapping) ? {...mapping} : mapping})
77
89
  };
@@ -116,6 +128,7 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
116
128
  );
117
129
  model = createNativeDecisionModel({
118
130
  ...configuration, paths, onnx: localAI.getONNXRuntime(), modelAssets,
131
+ family: selection.family, model: selection.model, revision: selection.revision, dtype: selection.dtype,
119
132
  executionTarget: selection.executionTarget, assetProjectionId: selection.assetProjectionId,
120
133
  resourcePaths: selection.resourcePaths, signal: lifetime.signal
121
134
  });
@@ -221,6 +234,7 @@ export function createNativeDecisionService(configuration = {}, {appRoot = proce
221
234
  'decisions.status': current,
222
235
  'decisions.load': function loadRequest(parameters, request) {
223
236
  return load({
237
+ family: parameters?.family, model: parameters?.model, revision: parameters?.revision, dtype: parameters?.dtype,
224
238
  executionTarget: parameters?.executionTarget, assetProjectionId: parameters?.assetProjectionId,
225
239
  resourcePaths: parameters?.resourcePaths, signal: request.signal
226
240
  });
@@ -17,7 +17,7 @@ try {
17
17
  };
18
18
  parentPort.on('message', function encodeRequest({id, rows}) {
19
19
  try {
20
- const {encoded, pad} = encodeDecisionRows(interfaceAdapter, rows, 'laya');
20
+ const {encoded, pad} = encodeDecisionRows(interfaceAdapter, rows, workerData.family);
21
21
  const inputs = createDecisionInputs(encoded, pad);
22
22
  parentPort.postMessage({id, result: inputs});
23
23
  } catch (error) {
@@ -6,7 +6,7 @@ function cancelled() {
6
6
  }
7
7
 
8
8
  /** One tokenizer worker owned by one native model activation. */
9
- export function createDecisionTokenizer({tokenizerPath, tokenizerConfigPath, signal, onError}) {
9
+ export function createDecisionTokenizer({family = 'laya', tokenizerPath, tokenizerConfigPath, signal, onError}) {
10
10
  signal?.throwIfAborted();
11
11
  const pending = new Map();
12
12
  let nextId = 0;
@@ -14,7 +14,7 @@ export function createDecisionTokenizer({tokenizerPath, tokenizerConfigPath, sig
14
14
  let terminalError;
15
15
  let exited = false;
16
16
  const worker = new Worker(new URL('./decision-tokenizer-worker.mjs', import.meta.url), {
17
- workerData: {tokenizerPath, tokenizerConfigPath}
17
+ workerData: {family, tokenizerPath, tokenizerConfigPath}
18
18
  });
19
19
  let resolveExit;
20
20
  const exit = new Promise(function tokenizerExit(resolve) { resolveExit = resolve; });
@@ -5,7 +5,13 @@ import {decodeDecisionOutputs} from '../../browser-runtime/ai/decision-runtime.m
5
5
  import {createDecisionTokenizer} from './decision-tokenizer.mjs';
6
6
 
7
7
  const STATE_EVENT = 'ai.decisions.state';
8
- const LAYA_FILES = ['onnx/model.onnx', 'onnx/model.onnx_data', 'tokenizer.json', 'tokenizer_config.json'];
8
+
9
+ function selectedGraph(family, dtype) {
10
+ if (family === 'laya' && dtype === 'fp32') return 'onnx/model.onnx';
11
+ if (family === 'laya' && dtype === 'fp16') return 'onnx/model_fp16.onnx';
12
+ if (family === 'julia' && dtype === 'fp32') return 'model.onnx';
13
+ throw new TypeError('Native decisions support Laya fp32/fp16 and Julia fp32 graph selections.');
14
+ }
9
15
 
10
16
  function cancellation(message) {
11
17
  return new CoreError({name: 'AbortError', code: 'ARCANE_AI_REQUEST_ABORTED', message});
@@ -18,6 +24,7 @@ function sameExecutionTarget(left, right) {
18
24
  }
19
25
 
20
26
  function sameActivation(left, right) {
27
+ if (left.family !== right.family || left.model !== right.model || left.revision !== right.revision || left.dtype !== right.dtype) return false;
21
28
  if (!sameExecutionTarget(left.executionTarget, right.executionTarget) || left.assetProjectionId !== right.assetProjectionId) return false;
22
29
  if (left.resourcePaths === right.resourcePaths) return true;
23
30
  if (!left.resourcePaths || !right.resourcePaths) return false;
@@ -27,15 +34,15 @@ function sameActivation(left, right) {
27
34
  }
28
35
 
29
36
  /**
30
- * One explicitly activated Laya FP32 model using an existing native ONNX owner.
37
+ * One explicitly activated typed-decision model using an existing native ONNX owner.
31
38
  * It does not create another inference runtime or load on construction.
32
39
  */
33
40
  export function createNativeDecisionModel({
34
41
  onnx, modelAssets, workingDirectory, paths, assetProjectionId, resourcePaths,
35
- model = 'onnx-community/laya-typed-decisions-ONNX', revision = 'main',
42
+ family = 'laya', model = 'onnx-community/laya-typed-decisions-ONNX', revision = 'main',
36
43
  dtype = 'fp32', sessionOptions, executionPreference = 'gpu', executionTarget, signal
37
44
  } = {}) {
38
- if (dtype !== 'fp32') throw new TypeError('This native Laya graph selection requires dtype fp32.');
45
+ selectedGraph(family, dtype);
39
46
  const owner = {};
40
47
  const events = createArcaneEventSource(owner, {source: 'native-decisions', eventTypes: [STATE_EVENT]});
41
48
  const loads = new Set();
@@ -49,15 +56,17 @@ export function createNativeDecisionModel({
49
56
  let lastError = null;
50
57
  let execution = null;
51
58
  let selectedActivation = {
52
- executionTarget,
59
+ family, model, revision, dtype, executionTarget,
53
60
  ...(assetProjectionId == null ? {} : {assetProjectionId, resourcePaths: resourcePaths && {...resourcePaths}})
54
61
  };
62
+ let activeSelection = selectedActivation;
55
63
  let pendingActivation;
56
64
 
57
65
  function current() {
58
66
  const pending = pendingActivation && (state !== 'ready' || pendingActivation.entry !== activation) ? pendingActivation : null;
59
67
  return {
60
- family: 'laya', model, revision, dtype, state,
68
+ family: activeSelection.family, model: activeSelection.model,
69
+ revision: activeSelection.revision, dtype: activeSelection.dtype, state,
61
70
  loaded: state === 'ready', busy: state === 'loading' || Boolean(pending) || Boolean(activation?.jobs.size),
62
71
  activeRequests: activation?.jobs.size ?? 0, progress,
63
72
  error: lastError ? serializeCoreError(lastError) : null,
@@ -163,10 +172,14 @@ export function createNativeDecisionModel({
163
172
  }
164
173
  if (paths) return paths;
165
174
  if (!modelAssets) throw new CoreError({code: 'ARCANE_DECISION_ASSETS_UNAVAILABLE', message: 'Native decisions need prepared paths or the Core model-assets owner.'});
166
- const base = `https://huggingface.co/${model}/resolve/${encodeURIComponent(revision)}/`;
175
+ const {family: selectedFamily, model: selectedModel, revision: selectedRevision, dtype: selectedDtype} = entry.selection;
176
+ const graph = selectedGraph(selectedFamily, selectedDtype);
177
+ const companion = selectedFamily === 'laya' ? `${graph}_data` : `${graph}.data`;
178
+ const members = [graph, companion, 'tokenizer.json', 'tokenizer_config.json'];
179
+ const base = `https://huggingface.co/${selectedModel}/resolve/${encodeURIComponent(selectedRevision)}/`;
167
180
  const projection = await modelAssets.prepare({
168
181
  id: entry.id, workingDirectory,
169
- members: LAYA_FILES.map(function selectFile(path) { return {path, url: new URL(path, base).href}; }),
182
+ members: members.map(function selectFile(path) { return {path, url: new URL(path, base).href}; }),
170
183
  signal: entry.controller.signal,
171
184
  onProgress(value) { updateProgress(entry, value); }
172
185
  });
@@ -176,7 +189,7 @@ export function createNativeDecisionModel({
176
189
  await modelAssets.release(entry.id);
177
190
  entry.prepared = false;
178
191
  const files = Object.fromEntries(projection.members.map(function memberFile(member) { return [member.path, member.nativePath]; }));
179
- return {model: files['onnx/model.onnx'], tokenizer: files['tokenizer.json'], tokenizerConfig: files['tokenizer_config.json']};
192
+ return {model: files[graph], tokenizer: files['tokenizer.json'], tokenizerConfig: files['tokenizer_config.json']};
180
193
  }
181
194
 
182
195
  function beginActivation(request) {
@@ -187,6 +200,7 @@ export function createNativeDecisionModel({
187
200
  request.retained = undefined;
188
201
  request.entry = entry;
189
202
  activation = entry;
203
+ activeSelection = entry.selection;
190
204
  state = 'loading';
191
205
  progress = {phase: 'preparing-model'};
192
206
  lastError = null;
@@ -196,6 +210,7 @@ export function createNativeDecisionModel({
196
210
  entry.controller.signal.throwIfAborted();
197
211
  updateProgress(entry, {phase: 'loading-model'});
198
212
  entry.tokenizer = createDecisionTokenizer({
213
+ family: entry.selection.family,
199
214
  tokenizerPath: files.tokenizer, tokenizerConfigPath: files.tokenizerConfig,
200
215
  signal: entry.controller.signal,
201
216
  onError(error) {
@@ -258,14 +273,22 @@ export function createNativeDecisionModel({
258
273
  }).finally(function detachLoadCaller() { operationSignal?.removeEventListener('abort', abort); });
259
274
  }
260
275
 
261
- async function load({signal: operationSignal, executionTarget: requestedTarget, assetProjectionId: requestedProjection, resourcePaths: requestedPaths} = {}) {
276
+ async function load({
277
+ family: requestedFamily, model: requestedModel, revision: requestedRevision, dtype: requestedDtype,
278
+ signal: operationSignal, executionTarget: requestedTarget, assetProjectionId: requestedProjection, resourcePaths: requestedPaths
279
+ } = {}) {
262
280
  assertOpen(operationSignal);
263
281
  const projection = requestedProjection === undefined ? selectedActivation.assetProjectionId : requestedProjection;
264
282
  const mapping = requestedPaths === undefined ? selectedActivation.resourcePaths : requestedPaths;
265
283
  const selection = {
284
+ family: requestedFamily === undefined ? selectedActivation.family : requestedFamily,
285
+ model: requestedModel === undefined ? selectedActivation.model : requestedModel,
286
+ revision: requestedRevision === undefined ? selectedActivation.revision : requestedRevision,
287
+ dtype: requestedDtype === undefined ? selectedActivation.dtype : requestedDtype,
266
288
  executionTarget: requestedTarget === undefined ? selectedActivation.executionTarget : requestedTarget,
267
289
  ...(projection == null ? {} : {assetProjectionId: projection, resourcePaths: mapping && typeof mapping === 'object' && !Array.isArray(mapping) ? {...mapping} : mapping})
268
290
  };
291
+ selectedGraph(selection.family, selection.dtype);
269
292
  if (pendingActivation && !pendingActivation.controller.signal.aborted && sameActivation(pendingActivation.selection, selection)) {
270
293
  return observeLoad(pendingActivation, operationSignal);
271
294
  }