arcane-os 0.7.0 → 0.7.2

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,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.2
4
+
5
+ - Keep both supported browser NPU and GPU flag address copy controls available
6
+ regardless of API availability, GPU classification, or detection failure.
7
+ Use one short explanation per flag and one shared paste/enable/relaunch
8
+ instruction. Preserve browser-reported statuses and the GPU-only chat notice.
9
+
10
+ ## 0.7.1
11
+
12
+ - Make Profile NPU and GPU setup concise and copy-only, with **Copy NPU flag
13
+ address** as the NPU action. Confirm a reported discrete GPU with **Already
14
+ using the performance GPU.** and show GPU setup only for explicitly reported
15
+ integrated or software adapters. Unknown GPU classes receive no flag advice.
16
+ - Retain the loaded Wllama Worker's adapter details for the GPU-only chat alert.
17
+ Show that alert only for an explicitly reported integrated or software adapter,
18
+ with manual browser-address instructions and no attempted navigation.
19
+ - Preserve complete adapter descriptions and optional class/fallback metadata
20
+ through the Wllama projection. Remove obsolete projection identity gates.
21
+ - Forward speech artifact and upstream model loading progress through sticky AI
22
+ state. Show the current file, completed files, elapsed time, and initialization
23
+ in both transcription controls, with indeterminate progress for unknown totals.
24
+ Preserve cancellation, complete diagnostics, models, and inference precision.
25
+
3
26
  ## 0.7.0
4
27
 
5
28
  - Add `arcane dev --public`, including forwarding through
package/NOTICE CHANGED
@@ -8,27 +8,24 @@ informational notice and is not itself a grant of commercial rights.
8
8
  Third-party material retains its own terms:
9
9
 
10
10
  - event-pubsub 6.1.0: MIT License; see its installed `licence` file.
11
- - strong-type 2.0.0: MIT License; see its installed `licence` file.
11
+ - strong-type 2.0.1: MIT License; see its installed `licence` file.
12
12
  - vanilla-test 2.1.3: MIT License; see its installed `licence` file.
13
13
  - ansi-colors-es6 5.0.0: MIT License; see its installed `LICENSE` file.
14
14
  - vanilla-test's strong-type 2.0.1 dependency: MIT License; see its installed
15
15
  `licence` file.
16
- - bundled strong-type 1.1.0: MIT License; see runtime/strong-type/licence.
16
+ - bundled strong-type 2.0.1: MIT License; see runtime/strong-type/licence.
17
17
  - uPlot: MIT License; see runtime/arcane/modules/uPlot.LICENSE.txt.
18
18
  - @wllama/wllama 3.6.0 (source revision
19
19
  f16050d8d51a00602c6a2a6b8ac9c09f490eea7f): MIT License; see
20
20
  browser-runtime/ai/wllama/LICENCE.
21
21
  - llama.cpp b10454 (source revision
22
- 4df29be4f4c3673f428170fda944a5b19f743bb8), embedded in the authenticated
22
+ 4df29be4f4c3673f428170fda944a5b19f743bb8), embedded in the
23
23
  Wllama runtime: MIT License; see browser-runtime/ai/wllama/llama.cpp-LICENSE.
24
24
  - Marked: MIT License; complete notice and terms reproduced below.
25
25
  - QRCode.js: MIT License; complete notice and terms reproduced below.
26
26
 
27
- No model weights, speech runtimes, or speech model/voice bytes are included in
28
- this package. Browser model and speech downloads require caller-supplied
29
- authorities. Byte-length and SHA-256 checks inherit the app security policy,
30
- default to disabled, and may be enabled or overridden for one provider/load;
31
- an enabled check requires its expected value before cache admission.
27
+ No model weights, speech runtimes, or speech model/voice files are included in
28
+ this package. Browser models and speech resources use their upstream downloads.
32
29
 
33
30
  Marked
34
31
  ------
@@ -57,9 +54,7 @@ SOFTWARE.
57
54
  QRCode.js
58
55
  ---------
59
56
 
60
- runtime/arcane/modules/QRCode.min.js is byte-for-byte the qrcode.min.js file
61
- from the davidshimjs/qrcodejs master branch. Its SHA-256 is
62
- c541ef06327885a8415bca8df6071e14189b4855336def4f36db54bde8484f36.
57
+ runtime/arcane/modules/QRCode.min.js comes from davidshimjs/qrcodejs.
63
58
 
64
59
  Copyright (c) 2012 davidshimjs
65
60
 
package/README.md CHANGED
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
19
19
  `arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
20
20
  event, cancellation, and browser run contracts.
21
21
 
22
- This checkout defines the `0.7.0` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.7.2` SDK contract. Applications pin one exact npm
23
23
  version and lockfile; registry state is deliberately not baked into application
24
24
  artifacts.
25
25
 
@@ -35,7 +35,7 @@ Create one browser application, install its pinned SDK, and start its source
35
35
  server:
36
36
 
37
37
  ```bash
38
- npx arcane-os@0.7.0 new hello-speech --path ./hello-speech --target browser
38
+ npx arcane-os@0.7.2 new hello-speech --path ./hello-speech --target browser
39
39
  cd hello-speech
40
40
  npm install
41
41
  npm run dev
@@ -378,7 +378,7 @@ uses the same controller for automatic memory extraction.
378
378
  Create a new repository-shaped Arcane application with the exact stable SDK:
379
379
 
380
380
  ```bash
381
- npx arcane-os@0.7.0 new my-app --path ./my-app --target portable --git
381
+ npx arcane-os@0.7.2 new my-app --path ./my-app --target portable --git
382
382
  cd my-app
383
383
  npm install
384
384
  npm run dev
@@ -388,7 +388,7 @@ To enroll an existing repository, install the exact SDK and initialize only
388
388
  missing Arcane files:
389
389
 
390
390
  ```bash
391
- npm install --save-dev --save-exact arcane-os@0.7.0
391
+ npm install --save-dev --save-exact arcane-os@0.7.2
392
392
  npm exec -- arcane init my-app --target portable
393
393
  ```
394
394
 
@@ -404,7 +404,7 @@ npm exec -- arcane-os targets
404
404
  No global SDK install or standalone Arcane CLI is required. The application
405
405
  repository's exact npm dependency and lockfile own the CLI and toolchain version.
406
406
 
407
- Use `npx arcane-os@0.7.0` for the initial bootstrap because it names this npm
407
+ Use `npx arcane-os@0.7.2` for the initial bootstrap because it names this npm
408
408
  package explicitly; bare `npx arcane` outside an installed project could resolve
409
409
  a different package. Both installed commands invoke the same headless toolchain.
410
410
  Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
@@ -424,7 +424,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
424
424
 
425
425
  # From the generated app repository
426
426
  cd ../local-app
427
- npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.7.0.tgz
427
+ npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.7.2.tgz
428
428
  npm ci
429
429
  ```
430
430
 
@@ -433,7 +433,7 @@ same location. The lockfile retains the selected package dependency while
433
433
  Arcane uses the installed package name and version. Local directory `file:` dependencies are not
434
434
  accepted because npm may install them as links; use a packed `.tgz`. A GitHub
435
435
  runner also needs that tarball at the locked path. After publication, replace
436
- the local declaration with the exact `arcane-os@0.7.0` registry package and
436
+ the local declaration with the exact `arcane-os@0.7.2` registry package and
437
437
  commit the regenerated lock.
438
438
 
439
439
  Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
@@ -572,7 +572,7 @@ package installation, or assertions.
572
572
 
573
573
  ## Current target support
574
574
 
575
- Version `0.7.0` exposes one browser target and five explicitly paired
575
+ Version `0.7.2` exposes one browser target and five explicitly paired
576
576
  native development targets: a non-runnable portable directory, a
577
577
  Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
578
578
  unsigned-local-test DEBs, and an Android development-signed APK. The
@@ -7,17 +7,6 @@ export const WEBNN_BROWSER_SETTINGS = [
7
7
  { browserId: "edge", name: "Microsoft Edge", url: "edge://flags/#web-machine-learning-neural-network" },
8
8
  ];
9
9
 
10
- // Internal-page navigation may be blocked. The instructions remain available
11
- // regardless; returning from this function is not evidence that settings opened.
12
- export function openBrowserDeviceSettings(target, message) {
13
- try {
14
- globalThis.open?.(target.url, "_blank", "noopener,noreferrer");
15
- } catch {
16
- // Continue to the address-bar instructions when the browser rejects the URL.
17
- }
18
- globalThis.alert?.(message);
19
- }
20
-
21
10
  // Browser identity selects setup instructions, never hardware or model readiness.
22
11
  export function getBrowserDeviceSettings(navigatorObject = globalThis.navigator) {
23
12
  const userAgent = String(navigatorObject?.userAgent ?? "");
@@ -83,22 +72,37 @@ export function getBrowserDeviceSettings(navigatorObject = globalThis.navigator)
83
72
  };
84
73
  }
85
74
 
86
- // Adapter selection is availability evidence, not the browser flag's state or
87
- // proof that a model is executing on this GPU. The power preference is a hint.
75
+ // Only an explicit browser adapter type or fallback flag establishes its class.
76
+ // Vendor names and powerPreference (including Chromium's echoed request) do not.
77
+ export function describeBrowserGpu(info, legacyFallbackAdapter) {
78
+ const adapterType = is.string(info?.type) ? info.type : null;
79
+ const isFallbackAdapter = is.boolean(info?.isFallbackAdapter)
80
+ ? info.isFallbackAdapter
81
+ : is.boolean(legacyFallbackAdapter) ? legacyFallbackAdapter : null;
82
+ let performanceStatus = 'unknown';
83
+ if (isFallbackAdapter === true || adapterType === 'CPU') performanceStatus = 'fallback';
84
+ else if (adapterType === 'discrete GPU') performanceStatus = 'discrete';
85
+ else if (adapterType === 'integrated GPU') performanceStatus = 'integrated';
86
+ return {
87
+ name: info?.description || info?.name
88
+ || [info?.vendor, info?.architecture, info?.device].filter(Boolean).join(' ')
89
+ || 'WebGPU adapter',
90
+ adapterType,
91
+ isFallbackAdapter,
92
+ performanceStatus
93
+ };
94
+ }
95
+
96
+ // This is the adapter returned to this page, not another page's model adapter.
97
+ // Requesting high performance is a hint, not evidence that it was honored.
88
98
  export async function detectBrowserGpu(navigatorObject = globalThis.navigator) {
89
99
  if (!is.function(navigatorObject?.gpu?.requestAdapter)) {
90
100
  return { available: false, reason: "api-unavailable" };
91
101
  }
92
102
  const adapter = await navigatorObject.gpu.requestAdapter({ powerPreference: "high-performance" });
93
103
  if (!adapter) return { available: false, reason: "adapter-unavailable" };
94
- const info = adapter.info;
95
104
  return {
96
105
  available: true,
97
- name: info?.description
98
- || [info?.vendor, info?.architecture, info?.device].filter(Boolean).join(" ")
99
- || "WebGPU adapter",
100
- isFallbackAdapter: is.boolean(info?.isFallbackAdapter)
101
- ? info.isFallbackAdapter
102
- : is.boolean(adapter.isFallbackAdapter) ? adapter.isFallbackAdapter : null,
106
+ ...describeBrowserGpu(adapter.info, adapter.isFallbackAdapter),
103
107
  };
104
108
  }
@@ -1962,6 +1962,10 @@ export function createDbopfsSpeechArtifactStore({
1962
1962
  return artifactGraphError(graphFileReason(descriptor, boundary), message);
1963
1963
  }
1964
1964
 
1965
+ function reportArtifactProgress(onProgress, signal, progress) {
1966
+ if (!signal?.aborted && is.function(onProgress)) onProgress(progress);
1967
+ }
1968
+
1965
1969
  async function openCached(authority, { signal, onProgress } = {}) {
1966
1970
  const graph = ARTIFACT_GRAPHS.has(authority);
1967
1971
  const metadata = artifactMetadata(authority);
@@ -1975,14 +1979,50 @@ export function createDbopfsSpeechArtifactStore({
1975
1979
  for (let index = 0; index < metadata.files.length; index += 1) {
1976
1980
  throwIfAborted(signal);
1977
1981
  const descriptor = metadata.files[index];
1982
+ reportArtifactProgress(
1983
+ onProgress,
1984
+ signal,
1985
+ {
1986
+ phase: 'prepare',
1987
+ stage: 'cache',
1988
+ message: 'Opening cached speech file',
1989
+ file: descriptor.path,
1990
+ completed: index,
1991
+ total: metadata.files.length,
1992
+ unit: 'files',
1993
+ }
1994
+ );
1978
1995
  const file = await readFile(names.files[index]);
1979
1996
  if (!file) {
1980
1997
  await removeUnlocked(authority);
1981
1998
  return null;
1982
1999
  }
1983
2000
  files.push({ descriptor, file });
2001
+ reportArtifactProgress(
2002
+ onProgress,
2003
+ signal,
2004
+ {
2005
+ phase: 'prepare',
2006
+ stage: 'cache',
2007
+ message: 'Opened cached speech file',
2008
+ file: descriptor.path,
2009
+ completed: files.length,
2010
+ total: metadata.files.length,
2011
+ unit: 'files',
2012
+ }
2013
+ );
1984
2014
  }
1985
2015
  try {
2016
+ reportArtifactProgress(
2017
+ onProgress,
2018
+ signal,
2019
+ {
2020
+ phase: 'prepare',
2021
+ stage: 'runtime',
2022
+ message: 'Preparing speech runtime modules',
2023
+ total: null,
2024
+ }
2025
+ );
1986
2026
  const routing = graph
1987
2027
  ? await planOrdinaryMaterializedRuntime({ files }, metadata, signal)
1988
2028
  : null;
@@ -2020,6 +2060,19 @@ export function createDbopfsSpeechArtifactStore({
2020
2060
  throwIfAborted(signal);
2021
2061
  const descriptor = metadata.files[index];
2022
2062
  const sourceUrl = graph ? descriptor.sourceUrl : descriptor.url;
2063
+ reportArtifactProgress(
2064
+ onProgress,
2065
+ signal,
2066
+ {
2067
+ phase: 'download',
2068
+ stage: 'artifacts',
2069
+ message: 'Downloading speech file',
2070
+ file: descriptor.path,
2071
+ completed: index,
2072
+ total: metadata.files.length,
2073
+ unit: 'files',
2074
+ }
2075
+ );
2023
2076
  let response;
2024
2077
  try {
2025
2078
  response = await fetchFunction(sourceUrl, {
@@ -2095,7 +2148,30 @@ export function createDbopfsSpeechArtifactStore({
2095
2148
  throw speechError("ARCANE_AI_ARTIFACT_CACHE_REJECTED", "DBOPFS did not preserve a speech artifact.");
2096
2149
  }
2097
2150
  installed.push({ descriptor, file });
2151
+ reportArtifactProgress(
2152
+ onProgress,
2153
+ signal,
2154
+ {
2155
+ phase: 'download',
2156
+ stage: 'artifacts',
2157
+ message: 'Stored speech file',
2158
+ file: descriptor.path,
2159
+ completed: installed.length,
2160
+ total: metadata.files.length,
2161
+ unit: 'files',
2162
+ }
2163
+ );
2098
2164
  }
2165
+ reportArtifactProgress(
2166
+ onProgress,
2167
+ signal,
2168
+ {
2169
+ phase: 'prepare',
2170
+ stage: 'runtime',
2171
+ message: 'Preparing speech runtime modules',
2172
+ total: null,
2173
+ }
2174
+ );
2099
2175
  const routing = graph
2100
2176
  ? await planOrdinaryMaterializedRuntime({ files: installed }, metadata, signal)
2101
2177
  : null;
@@ -2145,6 +2221,16 @@ export function createDbopfsSpeechArtifactStore({
2145
2221
  }
2146
2222
  throw speechError("ARCANE_AI_ARTIFACT_OFFLINE_MISS", "No cached offline speech artifacts are available.");
2147
2223
  }
2224
+ reportArtifactProgress(
2225
+ onProgress,
2226
+ signal,
2227
+ {
2228
+ phase: 'prepare',
2229
+ stage: 'runtime',
2230
+ message: 'Opening prepared speech files',
2231
+ total: null,
2232
+ }
2233
+ );
2148
2234
  if (graph) {
2149
2235
  const materialized = await createOrdinaryArtifactObjectUrls(
2150
2236
  admitted,
@@ -621,6 +621,7 @@ function observeLoadOperation(record, { signal, progress }, role) {
621
621
  );
622
622
  return;
623
623
  }
624
+ if (record.lastProgress !== null) observer.progress(record.lastProgress);
624
625
  });
625
626
  }
626
627
 
@@ -1322,7 +1323,7 @@ function createBrowserSpeechProvider({
1322
1323
  return pool;
1323
1324
  }
1324
1325
 
1325
- async function loadWorkerPool(preparation, device, warnings, signal) {
1326
+ async function loadWorkerPool(preparation, device, warnings, signal, onProgress) {
1326
1327
  const pool = createWorkerPool(preparation, device, warnings);
1327
1328
  const configuration = completeValue({
1328
1329
  role,
@@ -1336,15 +1337,46 @@ function createBrowserSpeechProvider({
1336
1337
  let failure = pool.failure;
1337
1338
  if (!failure) {
1338
1339
  try {
1339
- await pool.slots[0].client.request("load", { configuration }, { signal });
1340
+ await pool.slots[0].client.request(
1341
+ 'load',
1342
+ { configuration },
1343
+ { signal, onProgress }
1344
+ );
1340
1345
  } catch (error) {
1341
1346
  failure = error;
1342
1347
  }
1343
1348
  }
1344
1349
  if (!failure) {
1345
1350
  const remainingLoads = [];
1351
+ let completedWorkers = 1;
1352
+ function reportPoolProgress(detail) {
1353
+ onProgress(
1354
+ {
1355
+ phase: 'initialize',
1356
+ stage: 'workers',
1357
+ message: `Opening speech model sessions on ${device}`,
1358
+ completed: completedWorkers,
1359
+ total: pool.slots.length,
1360
+ unit: 'sessions',
1361
+ detail,
1362
+ }
1363
+ );
1364
+ }
1365
+ async function loadRemainingWorker(slot) {
1366
+ await slot.client.request(
1367
+ 'load',
1368
+ { configuration },
1369
+ {
1370
+ signal,
1371
+ onProgress: reportPoolProgress,
1372
+ }
1373
+ );
1374
+ completedWorkers += 1;
1375
+ reportPoolProgress(null);
1376
+ }
1377
+ if (pool.slots.length > 1) reportPoolProgress(null);
1346
1378
  for (const slot of pool.slots.slice(1)) {
1347
- remainingLoads.push(slot.client.request("load", { configuration }, { signal }));
1379
+ remainingLoads.push(loadRemainingWorker(slot));
1348
1380
  }
1349
1381
  const settlements = await Promise.allSettled(remainingLoads);
1350
1382
  failure = pool.failure;
@@ -1488,6 +1520,7 @@ function createBrowserSpeechProvider({
1488
1520
  errorCode = null;
1489
1521
  const record = {
1490
1522
  promise: null,
1523
+ lastProgress: null,
1491
1524
  warnings: NO_PROVIDER_WARNINGS,
1492
1525
  observers: new Set(),
1493
1526
  settled: false,
@@ -1498,6 +1531,24 @@ function createBrowserSpeechProvider({
1498
1531
  linked.abort(reason, code);
1499
1532
  },
1500
1533
  };
1534
+ const loadStartedAt = Date.now();
1535
+ function reportLoadProgress(progress) {
1536
+ if (
1537
+ record.settled
1538
+ || linked.controller.signal.aborted
1539
+ || operationGeneration !== generation
1540
+ || state !== 'loading'
1541
+ ) return;
1542
+ const value = {
1543
+ ...progress,
1544
+ elapsedMs: Math.max(
1545
+ 0,
1546
+ Date.now() - loadStartedAt
1547
+ ),
1548
+ };
1549
+ record.lastProgress = value;
1550
+ for (const observer of [...record.observers]) observer.progress(value);
1551
+ }
1501
1552
  lastWarnings = NO_PROVIDER_WARNINGS;
1502
1553
  selectedDevice = null;
1503
1554
  const promise = Promise.resolve().then(async function loadBrowserSpeechProviderPool() {
@@ -1505,10 +1556,22 @@ function createBrowserSpeechProvider({
1505
1556
  let preparation = null;
1506
1557
  let pool = null;
1507
1558
  try {
1508
- prepared = await store.prepare(authority.graph ?? authority, {
1509
- signal: linked.controller.signal,
1510
- offline,
1511
- });
1559
+ reportLoadProgress(
1560
+ {
1561
+ phase: 'prepare',
1562
+ stage: 'cache',
1563
+ message: 'Opening stored speech runtime files',
1564
+ total: null,
1565
+ }
1566
+ );
1567
+ prepared = await store.prepare(
1568
+ authority.graph ?? authority,
1569
+ {
1570
+ signal: linked.controller.signal,
1571
+ offline,
1572
+ onProgress: reportLoadProgress,
1573
+ }
1574
+ );
1512
1575
  preparation = {
1513
1576
  prepared,
1514
1577
  released: false,
@@ -1548,6 +1611,7 @@ function createBrowserSpeechProvider({
1548
1611
  device,
1549
1612
  record.warnings,
1550
1613
  linked.controller.signal,
1614
+ reportLoadProgress,
1551
1615
  );
1552
1616
  break;
1553
1617
  } catch (error) {
@@ -8,7 +8,7 @@ import {
8
8
  resolveModelSecurity,
9
9
  } from "./model-controller.mjs";
10
10
  import { createPackagedWllamaRuntime } from "./browser-wllama-runtime.mjs";
11
- import { getBrowserDeviceSettings, openBrowserDeviceSettings } from "./browser-device-settings.mjs";
11
+ import { getBrowserDeviceSettings, describeBrowserGpu } from "./browser-device-settings.mjs";
12
12
  import { arcaneEvents } from "../event-manager.mjs";
13
13
 
14
14
  const is = new Is(false);
@@ -269,21 +269,22 @@ function notifyHighPerformanceGpu(adapter) {
269
269
  if (highPerformanceGpuNoticeShown) {
270
270
  return;
271
271
  }
272
+ const selection = describeBrowserGpu(adapter);
273
+ if (selection.performanceStatus !== "integrated" && selection.performanceStatus !== "fallback") {
274
+ return;
275
+ }
272
276
  const browser = getBrowserDeviceSettings().highPerformanceGpu;
273
277
  if (!browser) {
274
278
  return;
275
279
  }
276
280
  highPerformanceGpuNoticeShown = true;
277
- const adapterName = adapter.description || adapter.name
278
- || [adapter.vendor, adapter.architecture].filter(Boolean).join(" ")
279
- || "the available WebGPU adapter";
280
- openBrowserDeviceSettings(
281
- browser,
282
- `Selected WebGPU adapter: ${adapterName}.\n\n`
283
- + `If this computer has multiple GPUs, enable “Force High Performance GPU” in ${browser.name} `
284
- + "to request the high-performance GPU when available. "
285
- + `Then completely close and reopen ${browser.name} before loading the model again.\n\n`
286
- + `If the flags page did not open, paste ${browser.url} into the address bar.`,
281
+ const selectedKind = selection.performanceStatus === "fallback" ? "Software/fallback" : "Integrated";
282
+ globalThis.alert?.(
283
+ `${selectedKind} WebGPU adapter selected: ${selection.name}.\n\n`
284
+ + "If this computer also has a discrete GPU, the browser may be able to use it. "
285
+ + `Copy ${browser.url} into the address bar in ${browser.name} `
286
+ + "and enable “Force High Performance GPU” to request it when available. "
287
+ + `Save your work, then completely close and reopen ${browser.name} before loading the model again.`,
287
288
  );
288
289
  }
289
290
 
@@ -3057,7 +3058,9 @@ export function createBrowserWasmLlmProvider({
3057
3058
  "Wllama did not confirm that the model loaded successfully.",
3058
3059
  );
3059
3060
  }
3060
- emitWebgpuAdapterSelection(activeSource, runtime);
3061
+ if (!signal?.aborted && generation === lifecycleGeneration && state === 'loading') {
3062
+ emitWebgpuAdapterSelection(activeSource, runtime);
3063
+ }
3061
3064
  throwIfAborted(signal, "load");
3062
3065
  if (generation !== lifecycleGeneration || state !== "loading") {
3063
3066
  await runtime.exit();
@@ -1,6 +1,6 @@
1
1
  import Is from "../dependencies/strong-type/index.js";
2
2
  import { arcaneLogging } from '../logging.mjs';
3
- import { Wllama } from "./wllama/index.mjs";
3
+ import {Wllama, WllamaRuntimeError} from './wllama/index.mjs';
4
4
 
5
5
  const is = new Is(false);
6
6
 
@@ -567,7 +567,46 @@ export function createPackagedWllamaRuntime({ logger = arcaneLogging } = {}) {
567
567
  "Wllama did not confirm a successfully loaded model.",
568
568
  );
569
569
  }
570
- const webgpu = { observed: true, apiPresent: true };
570
+ let adapter = null;
571
+ const telemetryOperation = trackOperation(
572
+ Promise.resolve().then(
573
+ function observeLoadedGpu() {
574
+ return next.arcaneTelemetry();
575
+ }
576
+ )
577
+ );
578
+ function cancelGpuObservation() {
579
+ telemetryOperation.cancel(loadController.signal.reason);
580
+ }
581
+ loadController.signal.addEventListener(
582
+ 'abort',
583
+ cancelGpuObservation,
584
+ {once: true}
585
+ );
586
+ if (loadController.signal.aborted) cancelGpuObservation();
587
+ try {
588
+ // Observe the adapter this loaded Worker already selected; do not probe
589
+ // a second adapter in the page and attribute it to the model.
590
+ const telemetry = await telemetryOperation.result;
591
+ if (telemetry?.worker?.invalid === true) {
592
+ arcaneLogging.warn("The loaded model's GPU adapter observation is inconsistent.", telemetry.worker);
593
+ } else {
594
+ adapter = telemetry?.worker?.adapter ?? null;
595
+ }
596
+ } catch (error) {
597
+ if (loadController.signal.aborted || error instanceof WllamaRuntimeError) throw error;
598
+ arcaneLogging.warn("The loaded model's GPU adapter details are unavailable.", error);
599
+ } finally {
600
+ loadController.signal.removeEventListener('abort', cancelGpuObservation);
601
+ }
602
+ if (progressFailure) throw progressFailure;
603
+ if (loadController.signal.aborted) throw cancellationError(loadController.signal.reason);
604
+ if (pending?.engine !== next) throw new Error('Wllama load was cancelled.');
605
+ const webgpu = {
606
+ observed: true,
607
+ apiPresent: true,
608
+ adapter
609
+ };
571
610
  pending = null;
572
611
  engine = next;
573
612
  publishEvidence({ state: "ready", webgpu, cancellation: null, cleanup: null });
@@ -166,6 +166,11 @@ class SpeechWorkerClient {
166
166
  ), { intentional: false }).catch(() => undefined);
167
167
  return;
168
168
  }
169
+ if (is.safeInteger(message.id) && message.type === 'progress') {
170
+ const pending = this.#pending.get(message.id);
171
+ if (pending?.op === 'load') pending.onProgress?.(message.progress);
172
+ return;
173
+ }
169
174
  if (!is.safeInteger(message.id) || !is.boolean(message.ok)) {
170
175
  void this.terminate(clientError(
171
176
  "ARCANE_AI_WORKER_MESSAGE_ERROR",
@@ -207,7 +212,7 @@ class SpeechWorkerClient {
207
212
  ));
208
213
  }
209
214
 
210
- request(op, payload, { signal = null } = {}) {
215
+ request(op, payload, { signal = null, onProgress = null } = {}) {
211
216
  this.#trace("request.call", { op, payload, aborted: signal?.aborted });
212
217
  if (!PUBLIC_WORKER_OPERATIONS.has(op)) {
213
218
  this.#trace("request.rejected", { op, reason: "unknown-operation" });
@@ -283,7 +288,10 @@ class SpeechWorkerClient {
283
288
  });
284
289
  }
285
290
  signal?.addEventListener?.("abort", onAbort, { once: true });
286
- client.#pending.set(id, { resolve, reject, cleanup, op });
291
+ client.#pending.set(
292
+ id,
293
+ { resolve, reject, cleanup, op, onProgress }
294
+ );
287
295
  const message = {
288
296
  protocol: SPEECH_WORKER_PROTOCOL,
289
297
  id,