arcane-os 0.6.2 → 0.7.1

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,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.1
4
+
5
+ - Make Profile NPU and GPU setup concise and copy-only, with **Copy NPU flag
6
+ address** as the NPU action. Confirm a reported discrete GPU with **Already
7
+ using the performance GPU.** and show GPU setup only for explicitly reported
8
+ integrated or software adapters. Unknown GPU classes receive no flag advice.
9
+ - Retain the loaded Wllama Worker's adapter details for the GPU-only chat alert.
10
+ Show that alert only for an explicitly reported integrated or software adapter,
11
+ with manual browser-address instructions and no attempted navigation.
12
+ - Preserve complete adapter descriptions and optional class/fallback metadata
13
+ through the Wllama projection. Remove obsolete projection identity gates.
14
+ - Forward speech artifact and upstream model loading progress through sticky AI
15
+ state. Show the current file, completed files, elapsed time, and initialization
16
+ in both transcription controls, with indeterminate progress for unknown totals.
17
+ Preserve cancellation, complete diagnostics, models, and inference precision.
18
+
19
+ ## 0.7.0
20
+
21
+ - Add `arcane dev --public`, including forwarding through
22
+ `npm run dev -- --app <id> --public`, to bind the selected app's existing
23
+ source server to all IPv4 interfaces. Keep the ordinary localhost default
24
+ and let an explicit `--host` select the bind address.
25
+ - Allow explicit network hosts in the shared development server. Report usable
26
+ local and network application URLs, including `networkUrls` in server results
27
+ and lifecycle events, while preserving selected routes and cancellation.
28
+ - Document LAN use and the browser HTTPS requirement for features that need a
29
+ secure context. No app-specific server, firewall rule, or tunnel is added.
30
+
3
31
  ## 0.6.2
4
32
 
5
33
  - Replace browser NPU setup buttons with direct browser-specific flags links
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.6.2` SDK contract. Applications pin one exact npm
22
+ This checkout defines the `0.7.1` 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,12 +35,27 @@ 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.6.2 new hello-speech --path ./hello-speech --target browser
38
+ npx arcane-os@0.7.1 new hello-speech --path ./hello-speech --target browser
39
39
  cd hello-speech
40
40
  npm install
41
41
  npm run dev
42
42
  ```
43
43
 
44
+ To open a selected app from another device on the network, append `--public`
45
+ to the same command:
46
+
47
+ ```bash
48
+ npm run dev -- --app hello-speech --public
49
+ ```
50
+
51
+ Replace `hello-speech` with any app in the workspace. The server binds to
52
+ `0.0.0.0` and prints network URLs to open on the other device. Plain `npm run dev`
53
+ keeps its localhost default. An explicit `--host` overrides the public bind
54
+ address, and `--port` selects the port. Network reachability depends on the
55
+ machine's firewall and network; the command does not configure internet hosting
56
+ or HTTPS. Browser features requiring a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts)
57
+ need HTTPS when opened through a LAN address.
58
+
44
59
  Open the URL printed by the server. The generated page owns its import map and
45
60
  Arcane theme; its application module is `apps/hello-speech/modules/App.js`.
46
61
  `arcane/AI` is a managed **browser import**, not an npm-exported Node inference
@@ -363,7 +378,7 @@ uses the same controller for automatic memory extraction.
363
378
  Create a new repository-shaped Arcane application with the exact stable SDK:
364
379
 
365
380
  ```bash
366
- npx arcane-os@0.6.2 new my-app --path ./my-app --target portable --git
381
+ npx arcane-os@0.7.1 new my-app --path ./my-app --target portable --git
367
382
  cd my-app
368
383
  npm install
369
384
  npm run dev
@@ -373,7 +388,7 @@ To enroll an existing repository, install the exact SDK and initialize only
373
388
  missing Arcane files:
374
389
 
375
390
  ```bash
376
- npm install --save-dev --save-exact arcane-os@0.6.2
391
+ npm install --save-dev --save-exact arcane-os@0.7.1
377
392
  npm exec -- arcane init my-app --target portable
378
393
  ```
379
394
 
@@ -389,7 +404,7 @@ npm exec -- arcane-os targets
389
404
  No global SDK install or standalone Arcane CLI is required. The application
390
405
  repository's exact npm dependency and lockfile own the CLI and toolchain version.
391
406
 
392
- Use `npx arcane-os@0.6.2` for the initial bootstrap because it names this npm
407
+ Use `npx arcane-os@0.7.1` for the initial bootstrap because it names this npm
393
408
  package explicitly; bare `npx arcane` outside an installed project could resolve
394
409
  a different package. Both installed commands invoke the same headless toolchain.
395
410
  Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
@@ -409,7 +424,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
409
424
 
410
425
  # From the generated app repository
411
426
  cd ../local-app
412
- npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.6.2.tgz
427
+ npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.7.1.tgz
413
428
  npm ci
414
429
  ```
415
430
 
@@ -418,7 +433,7 @@ same location. The lockfile retains the selected package dependency while
418
433
  Arcane uses the installed package name and version. Local directory `file:` dependencies are not
419
434
  accepted because npm may install them as links; use a packed `.tgz`. A GitHub
420
435
  runner also needs that tarball at the locked path. After publication, replace
421
- the local declaration with the exact `arcane-os@0.6.2` registry package and
436
+ the local declaration with the exact `arcane-os@0.7.1` registry package and
422
437
  commit the regenerated lock.
423
438
 
424
439
  Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
@@ -475,7 +490,7 @@ arcane new <id> [--path <directory>] [--display-name <name>] [--target <target>]
475
490
  arcane init [id] [--workspace <directory>] [--display-name <name>] [--target <target>]
476
491
  arcane doctor [--workspace <directory>] [--arcane-root <directory>]
477
492
  arcane import-map [--workspace <directory>] [--app <id>]
478
- arcane dev [--app <id>] [--host 127.0.0.1] [--port 8000]
493
+ arcane dev [--app <id>] [--public] [--host <address>] [--port 8000]
479
494
  arcane test [--app <id>] [--scope app]
480
495
  arcane test --scope shared --test-file <repo-relative.test.mjs>
481
496
  arcane check [--app <id>] [--scope app] [--skip-tests]
@@ -557,7 +572,7 @@ package installation, or assertions.
557
572
 
558
573
  ## Current target support
559
574
 
560
- Version `0.6.2` exposes one browser target and five explicitly paired
575
+ Version `0.7.1` exposes one browser target and five explicitly paired
561
576
  native development targets: a non-runnable portable directory, a
562
577
  Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
563
578
  unsigned-local-test DEBs, and an Android development-signed APK. The
@@ -683,7 +698,7 @@ read-only managed-service assessment where the host supports one. Native apps
683
698
  use an app-scoped Arcane Core and `Arcane.localAI.status()` in this SDK version.
684
699
  Ollama is optional for packaging and non-AI applications.
685
700
 
686
- The loopback development server serves complete selected files and does not
701
+ The development server serves complete selected files and does not
687
702
  apply byte-count or truncation gates. Optional hardening is inactive unless the
688
703
  caller explicitly selects `secure:true`. Required credential protection,
689
704
  malformed-request rejection, and unavoidable browser or operating-system safety
@@ -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,