arcane-os 0.77.0 → 0.79.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,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.79.0
4
+
5
+ - Add public `createExecutionDeviceCatalog()` and `createExecutionDeviceService()`
6
+ with lazy Windows CPU/GPU/NPU discovery, stable host-local identities,
7
+ complete discovery issues, explicit refresh, cancellation and disposal.
8
+ `localai.devices` and `localai.resolveTarget` use the existing Core transport
9
+ without loading models or changing saved preferences.
10
+ - Share one catalog per local-AI Core lifetime. Native ONNX and Laya accept
11
+ `executionTarget` for a physical CPU or mapped DirectML GPU. Automatic GPU
12
+ selection can use the largest reported dedicated capacity among addressable
13
+ hardware GPUs; integrated GPUs remain eligible. Unsupported or unavailable
14
+ selections are reported without silently choosing another physical device.
15
+ - Preserve caller options and complete execution diagnostics. Distinguish the
16
+ requested, resolved and configured target from observed execution; successful
17
+ configuration is not GPU/NPU execution proof. Laya target replacement cancels
18
+ and drains only its retiring activation before the successor starts.
19
+ - Keep discovery independent from model readiness and preserve existing engine
20
+ behavior when a target is omitted. Browser Wllama, native llama.cpp, image and
21
+ Whisper device routing are unchanged; this release adds no driver, model or
22
+ native runtime dependency.
23
+
24
+ ## 0.78.0
25
+
26
+ - Add `AI.readPreparedTTS()` and shared `readPreparedSpeech()` for complete
27
+ cache-only narration lookup. Return a ready playback handle or `null` without
28
+ loading a model, unmuting, synthesizing, writing storage or repairing partial
29
+ audio. Preserve complete semantic matching and genuine storage errors.
30
+ - Keep lookup cancellation independent from a successfully returned handle.
31
+ Replay uses the existing prepared-playback owner and can borrow the caller's
32
+ silent reusable audio output; no autoplay permission is implied.
33
+ - Add optional existing-only DBOPFS table/file reads while preserving ordinary
34
+ lazy-creation defaults. Cache-only lookup and later audio reads leave missing
35
+ tables absent.
36
+ - Give Kokoro voice caching a real Worker-lifetime response cache after runtime
37
+ import, while DBOPFS remains the durable model/resource owner. Preserve saved
38
+ native caches and clear transient responses on unload.
39
+ - Report a complete stored model resource as cached loading rather than a new
40
+ network download. This release changes no model selection, native engine or
41
+ physical-device routing and makes no live-model execution claim.
42
+
3
43
  ## 0.77.0
4
44
 
5
45
  - Add `AI.prepareAudioOutput()` for synchronous, silent preparation of a reusable
package/README.md CHANGED
@@ -20,12 +20,17 @@ 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.77.0` SDK contract. Development applications declare
23
+ This checkout defines the `0.79.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;
27
27
  registry state is deliberately not baked into application artifacts.
28
28
 
29
+ [Physical execution devices](docs/reference/execution-devices.md) exposes Windows
30
+ CPU/GPU/NPU discovery and native ONNX/Laya target selection through the existing
31
+ Core host, independently of model loading. Device presence, configured provider
32
+ placement and observed model execution remain separate facts.
33
+
29
34
  Standalone browser apps use their repository root and installed npm package directly, without a
30
35
  generated workspace `arcane/` directory or `arcane.lock.json`. Select the
31
36
  [four installed-package routes](docs/reference/protocols.md#installed-package-browser-routes)
@@ -106,9 +106,13 @@ export function createCoreONNXRuntime({client: suppliedClient} = {}) {
106
106
  }
107
107
  }
108
108
 
109
- async function load({id, model, sessionOptions, signal, timeoutMs} = {}) {
109
+ async function load({id, model, sessionOptions, executionPreference, executionTarget, signal, timeoutMs} = {}) {
110
110
  const currentConnection = connection;
111
- const result = await invoke('onnx.load', {id, model, sessionOptions}, {signal, timeoutMs});
111
+ const result = await invoke(
112
+ 'onnx.load',
113
+ {id, model, sessionOptions, executionPreference, executionTarget},
114
+ {signal, timeoutMs}
115
+ );
112
116
  currentConnection.controller.signal.throwIfAborted();
113
117
  return result;
114
118
  }
@@ -96,7 +96,7 @@ export function createDbopfsResourceStore({dbopfs, tableName, fetchImpl = global
96
96
  const file = await parts.read(entry.name, {signal});
97
97
  if (file) {
98
98
  const state = await parts.state(entry.name, {signal});
99
- onProgress?.({phase: 'download', completed: state.parts.length,
99
+ onProgress?.({phase: 'load', message: 'Reading stored model resource', completed: state.parts.length,
100
100
  total: state.parts.length, unit: 'shards', cached: true, url: request.url});
101
101
  return {...entry.metadata, file};
102
102
  }
@@ -997,6 +997,7 @@ function createOrdinaryArtifactModuleRouter(
997
997
  role,
998
998
  reader,
999
999
  originalWorker,
1000
+ responseCaches,
1000
1001
  ) {
1001
1002
  const nestedWorkers = new Set();
1002
1003
  const router = completeValue({
@@ -1015,8 +1016,8 @@ function createOrdinaryArtifactModuleRouter(
1015
1016
  return reader.fetchResource(input, init, modulePath);
1016
1017
  },
1017
1018
 
1018
- async openCache() {
1019
- throw new Error('Browser CacheStorage is unavailable in this speech Worker; resources use DBOPFS through fetch.');
1019
+ openCache(modulePath, name) {
1020
+ return responseCaches.open(name);
1020
1021
  },
1021
1022
 
1022
1023
  createWorker(modulePath, specifier, options = {}) {
@@ -1143,6 +1144,63 @@ function installOrdinaryArtifactModuleRouter(scope, configuration, role, send, s
1143
1144
  const resourceClient = createModelResourceClient({ send });
1144
1145
  let requestSignal = null;
1145
1146
  let reportProgress = null;
1147
+ let cacheClosed = false;
1148
+ const responseCacheEntries = new Map();
1149
+ const responseCaches = {
1150
+ async open(name) {
1151
+ assertCacheActive(requestSignal);
1152
+ const cacheName = String(name);
1153
+ let cache = responseCacheEntries.get(cacheName);
1154
+ if (!cache) {
1155
+ const responses = new Map();
1156
+ cache = {
1157
+ responses,
1158
+ api: {
1159
+ async match(input) {
1160
+ assertCacheActive(requestSignal);
1161
+ const stored = responses.get(cacheRequestKey(input));
1162
+ if (!stored) return undefined;
1163
+ const response = new Response(stored.body, stored);
1164
+ Object.defineProperties(response, {
1165
+ url: { value: stored.url },
1166
+ redirected: { value: stored.redirected },
1167
+ type: { value: stored.type },
1168
+ });
1169
+ return response;
1170
+ },
1171
+ async put(input, response) {
1172
+ const signal = requestSignal;
1173
+ assertCacheActive(signal);
1174
+ const key = cacheRequestKey(input);
1175
+ const copy = response.clone();
1176
+ const body = await copy.blob();
1177
+ assertCacheActive(signal);
1178
+ responses.set(key, {
1179
+ body,
1180
+ status: response.status,
1181
+ statusText: response.statusText,
1182
+ headers: [...response.headers],
1183
+ url: response.url,
1184
+ redirected: response.redirected,
1185
+ type: response.type,
1186
+ });
1187
+ },
1188
+ },
1189
+ };
1190
+ responseCacheEntries.set(cacheName, cache);
1191
+ }
1192
+ return cache.api;
1193
+ },
1194
+ };
1195
+ function assertCacheActive(signal) {
1196
+ throwIfAborted(signal, 'speech-resource-cache-cancelled');
1197
+ if (cacheClosed) throw new Error('The speech Worker response cache was unloaded.');
1198
+ }
1199
+ function cacheRequestKey(input) {
1200
+ const RequestConstructor = scope.Request ?? Request;
1201
+ const request = new RequestConstructor(input);
1202
+ return JSON.stringify([request.method, request.url]);
1203
+ }
1146
1204
  if (!is.function(originalFetch)) {
1147
1205
  throw workerError(
1148
1206
  "ARCANE_AI_PROVIDER_UNAVAILABLE",
@@ -1166,8 +1224,9 @@ function installOrdinaryArtifactModuleRouter(scope, configuration, role, send, s
1166
1224
  role,
1167
1225
  reader,
1168
1226
  originalWorker,
1227
+ responseCaches,
1169
1228
  );
1170
- const originalProperties = ['fetch', 'Worker', MODULE_ROUTER_NAME].map(
1229
+ const originalProperties = ['fetch', 'Worker', 'caches', MODULE_ROUTER_NAME].map(
1171
1230
  function originalWorkerProperty(name) {
1172
1231
  return { name, descriptor: Object.getOwnPropertyDescriptor(scope, name) };
1173
1232
  },
@@ -1240,7 +1299,22 @@ function installOrdinaryArtifactModuleRouter(scope, configuration, role, send, s
1240
1299
  receive: resourceClient.receive,
1241
1300
  setSignal(signal) { requestSignal = signal; },
1242
1301
  setProgress(report) { reportProgress = report; },
1302
+ enableResponseCache() {
1303
+ // Transformers captures cache capability during import. Kokoro then uses
1304
+ // this real, Worker-lifetime match/put cache for voices; fetch remains the
1305
+ // sole route to durable DBOPFS resources. No native saved cache is touched.
1306
+ assertCacheActive(requestSignal);
1307
+ Object.defineProperty(scope, 'caches', {
1308
+ configurable: true,
1309
+ enumerable: true,
1310
+ writable: true,
1311
+ value: responseCaches,
1312
+ });
1313
+ },
1243
1314
  async cleanup() {
1315
+ cacheClosed = true;
1316
+ for (const cache of responseCacheEntries.values()) cache.responses.clear();
1317
+ responseCacheEntries.clear();
1244
1318
  resourceClient.close(new Error('The speech Worker resource owner was unloaded.'));
1245
1319
  let cleanupFailure = null;
1246
1320
  try {
@@ -1773,6 +1847,7 @@ export function createSpeechWorkerRuntime({ role, scope = globalThis, send } = {
1773
1847
  );
1774
1848
  const namespace = await import(entry.moduleUrl);
1775
1849
  throwIfAborted(signal, `${role}-load-cancelled`);
1850
+ environment.enableResponseCache();
1776
1851
  restoreNamespace = configureRuntimeNamespace(
1777
1852
  namespace,
1778
1853
  configuration,
@@ -2071,6 +2146,7 @@ export function installBrowserSpeechArtifactModuleWorker(role, scope = globalThi
2071
2146
  target?.sourceUrl ?? request.targetUrl,
2072
2147
  );
2073
2148
  await import(target?.moduleUrl ?? request.targetUrl);
2149
+ environment.enableResponseCache();
2074
2150
  scope.removeEventListener("message", bootstrap);
2075
2151
  await new Promise((resolve) => queueMicrotask(resolve));
2076
2152
  for (const queuedEvent of queued.splice(0)) replayWorkerMessage(scope, queuedEvent);
@@ -32,6 +32,16 @@ export const CORE_METHOD_CONTRACTS={
32
32
  'preferences.set':{input:'{key,value}',output:'{key,value}'},
33
33
  'preferences.setMany':{input:'{entries}',output:'preference-batch-result'},
34
34
  'preferences.delete':{input:'{key}',output:'{key,deleted}'},
35
+ 'localai.devices': {
36
+ input: '{refresh?}',
37
+ output: '{platform,state,devices,issues}',
38
+ meaning: 'Physical host inventory, independent of model startup. Presence does not establish engine support or observed execution.'
39
+ },
40
+ 'localai.resolveTarget': {
41
+ input: '{executionTarget:{deviceId}|null,refresh?}',
42
+ output: '{requestedTarget,resolvedDevice,resolution,reason}',
43
+ meaning: 'Resolve a saved physical identity. Explicit null selects engine-owned automatic resolution; it does not claim a configured or observed device.'
44
+ },
35
45
  'window.setTheme': {
36
46
  input: '{backgroundColor?,textColor?}: null reset or {red,green,blue,alpha}',
37
47
  output: '{platform,supported,applied,unsupported}',
@@ -53,6 +53,22 @@ and reports npm's response. It tolerates npm's publish-time scanning. If
53
53
  scanning or manual review remains pending, the workflow reports that state and
54
54
  a rerun safely resumes without republishing the version.
55
55
 
56
+ Registry verification requires both the selected version/channel metadata and
57
+ an ordinary complete GET of that exact version's published `dist.tarball` URL.
58
+ Metadata visibility alone is not downloadable-package delivery. The verifier
59
+ reads exact-version metadata, follows its tarball URL, and consumes the full
60
+ successful response before reporting completion. Missing metadata, an HTTP
61
+ failure, or an interrupted body remains pending within the existing bounded
62
+ retry window; the remaining window also bounds the metadata/tarball requests.
63
+ The existing npm metadata subprocesses retain their current behavior. If they
64
+ consume the window, including a zero-wait invocation, the verifier reports
65
+ pending without starting a new download observation.
66
+ It records the actual failure and exits unsuccessfully if availability does not
67
+ arrive. It does not save a second artifact, repack, republish, alter registry
68
+ tags, add cache-bypass parameters, or impose a content-size or hash comparison.
69
+ Requester notices follow successful version/channel and download observation;
70
+ consumer installation and application verification remain separately owned.
71
+
56
72
  The public `0.3.2` release is fixed to package-source commit
57
73
  `445bd2d982f12e6ef8dd2b615c70512000cc5224`, selected Check run
58
74
  `33264677687`, and publication/registry run `33264829711`. Its numeric Git tag
@@ -77,9 +93,10 @@ The npm package already has its trusted-publishing relationship. Each later
77
93
  release therefore follows the same direct selected-artifact path: push the
78
94
  intended `main` source, manually run Check for that exact revision, review the
79
95
  resulting package inventory and legal notices, then manually dispatch
80
- publication with that Check run and artifact. Confirm the selected version and
81
- dist-tag after publication. Never rebuild or repack the artifact under
82
- publication authority, and never substitute a different source revision.
96
+ publication with that Check run and artifact. Confirm the selected version,
97
+ dist-tag, and complete published tarball availability after publication. Never
98
+ rebuild or repack the artifact under publication authority, and never substitute
99
+ a different source revision.
83
100
 
84
101
  Generated app CI uses `npm ci --ignore-scripts`, so its lock must exist and its
85
102
  dependency source must be reachable by the runner. External application CI uses
@@ -727,6 +727,54 @@ pause delays the next queued audio on the existing `AudioContext` clock; it
727
727
  does not delay the preceding promise after that passage's last buffer ends.
728
728
  The promise is a playback result, not a listener acknowledgement.
729
729
 
730
+ ## Read stored narration without starting generation
731
+
732
+ `await ai.readPreparedTTS({parts,textFormat,storage,identity,signal,onState})`
733
+ returns a complete prepared handle or `null`. It uses the same complete inputs,
734
+ selected speech configuration, formatting treatment, segmentation, storage key,
735
+ and semantic `identity` as `prepareTTS()`. Supply the same options used to
736
+ prepare the audio. A changed selection or input is a cache miss, not permission
737
+ to generate replacement audio.
738
+
739
+ This operation never activates or unmutes a provider, synthesizes speech,
740
+ creates stored records, repairs partial audio, or joins pending generation.
741
+ With `storage`, it reads the existing manifest and every ordered audio file.
742
+ Missing or incomplete audio returns `null`; unreadable metadata and other
743
+ storage errors reject. Without storage, it finds only already-complete audio
744
+ retained by this same AI instance. A miss leaves all existing data unchanged.
745
+
746
+ ```javascript
747
+ async function readNarration(text, key, signal) {
748
+ return ai.readPreparedTTS({
749
+ parts: [{
750
+ input: text,
751
+ voice: speechSelection.model.defaultVoice,
752
+ speed: 1,
753
+ pauseAfterMs: 0
754
+ }],
755
+ storage: {db: dbopfs, table: 'saved_narration', key},
756
+ identity: {
757
+ model: speechSelection.model,
758
+ runtime: speechSelection.runtime
759
+ },
760
+ signal
761
+ });
762
+ }
763
+ ```
764
+
765
+ A returned handle has the ordinary `segments`, `state`, `ready`, `getAudio(index)`,
766
+ and `cancel()` contract and can be passed to `ai.playPreparedTTS()` from the
767
+ application's playback action, including with its existing `audioOutput`.
768
+ The lookup signal governs only the read and rejects cancellation as
769
+ `AbortError`. After a successful return, aborting that signal does not invalidate
770
+ the retained handle; explicit handle cancellation and playback cancellation
771
+ remain separate. A successful read does not promise future autoplay permission
772
+ or prevent a later application-owned removal of stored files.
773
+
774
+ Applications decide which saved narration candidates to inspect. For example,
775
+ newest-first lookup can skip a newer partial preparation and then inspect an
776
+ older complete candidate without activating any model.
777
+
730
778
  ## Prepare narration once and replay stored audio
731
779
 
732
780
  Use `ai.prepareTTS()` when preparation must continue independently of playback,
@@ -1631,8 +1679,13 @@ as graph runtimes:
1631
1679
  Inside the dedicated speech Worker, native CacheStorage is temporarily
1632
1680
  unavailable before runtime import, so the selected runtime takes its existing
1633
1681
  fetch path. Transformers' public browser/custom/filesystem cache switches are
1634
- disabled there. The SDK does not fabricate Cache matches or mutation results,
1635
- and it neither clears nor changes saved native caches. The original Worker
1682
+ disabled there. After the runtime import, Kokoro's voice `caches.open()` calls
1683
+ use a Worker-local response cache with actual `match`/`put` results. It retains
1684
+ complete response bodies and metadata in memory for that Worker only; a miss
1685
+ does not fetch, and ordinary fetch still owns durable DBOPFS persistence. This
1686
+ temporary response retention is separate from Kokoro's own in-memory voice
1687
+ arrays. Unload clears it, and an unfinished put cannot repopulate it afterward.
1688
+ The SDK neither clears nor changes saved native caches. The original Worker
1636
1689
  environment is restored on unload or failed import. Existing explicit
1637
1690
  `removeBrowserSpeechModelCache()` remains a separate caller-selected operation.
1638
1691
 
@@ -1646,6 +1699,10 @@ closed resource parts survive cancellation. Existing whole-file caches remain
1646
1699
  readable without migration. HTTP resume is used only when the server returns
1647
1700
  the requested range; otherwise the complete resource is downloaded anew.
1648
1701
  Worker termination aborts and joins its parent-owned resource operations.
1702
+ An existing complete resource reports `phase:'load'`, `cached:true`, and
1703
+ `message:'Reading stored model resource'` with its complete shard progress.
1704
+ That event describes a DBOPFS read, not a network download or model-session
1705
+ reload. The resource owner does not fetch that stored response again.
1649
1706
 
1650
1707
  Routing discovery is best effort and is not an admission gate. If the scanner
1651
1708
  cannot interpret a module, that module is left unchanged and follows its native
@@ -0,0 +1,157 @@
1
+ # Physical execution devices
2
+
3
+ The local-AI Core host exposes physical device discovery independently of model
4
+ installation, loading and inference. A device being present does not establish
5
+ that a particular engine can use it. The engine resolves that second question
6
+ when the application explicitly loads its selected model.
7
+
8
+ ## List devices without loading a model
9
+
10
+ Use the application's existing Core client:
11
+
12
+ ```js
13
+ const catalog = await client.invoke('localai.devices', {refresh: false}, {signal});
14
+ console.log(catalog.devices);
15
+ console.log(catalog.issues);
16
+ ```
17
+
18
+ The inventory has `{platform, state, devices, issues}`. `platform` uses Node's
19
+ platform name. `state` is `ready`, `partial` or `unavailable`. Incomplete or failed
20
+ discovery is reported in `issues`; it is not evidence that the machine has no
21
+ devices. This initial discovery implementation uses Windows host APIs. Other
22
+ platforms report this capability as unavailable while retaining their existing
23
+ AI runtime behavior.
24
+
25
+ Each device has the following fields:
26
+
27
+ | Field | Meaning |
28
+ | --- | --- |
29
+ | `deviceId` | Stable host-local selection key, or `null` when identity could not be established. |
30
+ | `identitySource` | `windows-pnp-instance`, `host-cpu`, or `null`. |
31
+ | `kind` | `cpu`, `gpu` or `npu`. |
32
+ | `name` | Complete reported display name. Names are not selection identities. |
33
+ | `present` | Whether the queried device is present. |
34
+ | `isHardware`, `isIntegrated` | Reported hardware facts, or `null` when unavailable. |
35
+ | `dedicatedMemoryMiB` | Reported dedicated capacity, or `null`; used only for supported automatic GPU selection. |
36
+ | `addresses` | Observed adapter addresses described below. These are not persistent selection keys. |
37
+ | `processors` | Actual processor records for the aggregate CPU option; an empty array for GPU/NPU records. |
38
+
39
+ GPU and NPU identities are actual, uniquely correlated Windows PnP instance IDs.
40
+ An adapter whose stable identity cannot be established remains in the inventory
41
+ with `deviceId: null` and a corresponding issue. The SDK does not invent a key
42
+ from a name, enumeration order, hardware model number or adapter LUID.
43
+
44
+ The CPU choice uses `deviceId: 'cpu'` and `identitySource: 'host-cpu'`. It selects
45
+ the whole-host CPU execution target, without claiming socket or core affinity.
46
+ Its `processors` entries contain `{deviceId, name, socketDesignation, cores,
47
+ logicalProcessors}`; unavailable optional facts are `null`.
48
+
49
+ `addresses` contains:
50
+
51
+ ```js
52
+ {
53
+ adapterLuid: {highPart, lowPart}, // or null
54
+ pci: {bus, device, function: pciFunction}, // or null
55
+ dxgiAdapterIndex // or null
56
+ }
57
+ ```
58
+
59
+ The DXGI index is the actual `EnumAdapters` index joined to the adapter's LUID.
60
+ It is never inferred from DXCore or PnP enumeration order. It supplies a possible
61
+ DirectML address, not proof of provider availability or successful execution.
62
+ No CUDA ordinal is inferred from these Windows addresses.
63
+
64
+ An issue contains `{source, code, message, deviceId}`. `source` identifies `cpu`,
65
+ `gpu`, `npu`, `identity` or `dxgi`; `deviceId` is `null` when no stable identity
66
+ was established. Keep the complete diagnostic available to developers rather
67
+ than substituting an empty inventory.
68
+
69
+ ## Resolve a saved selection
70
+
71
+ Store the selected physical ID with the application's existing preferences,
72
+ keyed by provider and model. The SDK introduces no new preference store and
73
+ does not change the six-slot model preference tuple.
74
+
75
+ ```js
76
+ const resolution = await client.invoke('localai.resolveTarget', {
77
+ executionTarget: {deviceId: savedDeviceId}
78
+ }, {signal});
79
+ ```
80
+
81
+ The result contains `{requestedTarget, resolvedDevice, resolution, reason}`:
82
+
83
+ - `matched`: the complete device record was found. Engine support is still a
84
+ separate question.
85
+ - `unavailable` with `device-not-present`: complete relevant discovery did not
86
+ find that saved identity. Preserve the saved selection so the application can
87
+ show that it is currently unavailable.
88
+ - `unsupported` with `inventory-unavailable` or `identity-unresolved`: discovery
89
+ cannot establish the requested identity. Do not describe it as an absent device.
90
+ - `automatic` with `engine-default-required`: an explicit `executionTarget: null`
91
+ asks the selected engine to choose at its activation boundary. The physical
92
+ resolver returns `resolvedDevice: null` because it cannot establish the
93
+ engine's routing capabilities.
94
+
95
+ Calling the resolver does not load a model, change preferences or replace an
96
+ existing activation. A model load that omits `executionTarget` retains that
97
+ API's existing constructor, session-option and default behavior. Explicit
98
+ `null` selects automatic resolution for the new activation; it does not erase
99
+ or relabel a running session.
100
+
101
+ ## Engine configuration and execution evidence
102
+
103
+ The physical-target integration described here applies to native ONNX and
104
+ native Laya through Core. Listing host devices does not add physical-device
105
+ selection to browser WebGPU/Wllama or to the separate native llama.cpp, image
106
+ and Whisper owners. Their existing backend options remain unchanged. A browser
107
+ uses the existing Core connection for this inventory; the catalog does not
108
+ create a native connection or replace a browser provider.
109
+
110
+ An engine receives the saved selection as `executionTarget: {deviceId}`. It
111
+ owns provider availability, the physical-to-provider address mapping and the
112
+ actual session configuration. Its public state distinguishes the requested
113
+ target, physical resolution, configured target and observed execution. A
114
+ successful session configuration alone is not evidence that every operation ran
115
+ on a GPU or NPU.
116
+
117
+ Automatic selection can prefer the hardware GPU with the highest reported
118
+ dedicated capacity among supported targets with established routing. An
119
+ integrated GPU remains eligible when it meets those same conditions. An unavailable saved
120
+ target is not silently replaced by another device or a cloud route. See
121
+ [native ONNX](local-ai.md#native-onnx-sessions) and [native decisions](native-decisions.md)
122
+ for the selected engine's load, replacement, cancellation and status contracts.
123
+
124
+ ## Catalog and Core ownership
125
+
126
+ SDK development local-AI composition and generated native composition with
127
+ `localAI` selected register one lazy catalog and its independent
128
+ `execution-devices` service. An empty runtime selection still permits device
129
+ discovery. Hosts without local-AI composition retain their existing service
130
+ selection. A manually composed Core can register the service explicitly:
131
+
132
+ ```js
133
+ import {createExecutionDeviceCatalog} from 'arcane-os/local-ai/execution-devices';
134
+ import {createExecutionDeviceService} from 'arcane-os/core/execution-devices';
135
+ import {createLocalAIService} from 'arcane-os/core/local-ai';
136
+
137
+ const executionDevices = createExecutionDeviceCatalog({signal});
138
+ const services = [
139
+ createExecutionDeviceService({catalog: executionDevices}),
140
+ createLocalAIService(localAI, {appRoot, runtimes, signal, executionDevices})
141
+ ];
142
+ ```
143
+
144
+ Pass these definitions to the existing Core host. The independent service owns
145
+ catalog disposal. The local-AI engine borrows the same catalog; it does not
146
+ dispose a supplied owner or create another discovery process per model.
147
+
148
+ For direct Node use, `createExecutionDeviceCatalog({signal})` returns
149
+ `devices({refresh, signal})`, `resolveTarget({executionTarget, refresh, signal})`
150
+ and `dispose()`. The direct caller owns disposal. Construction performs no
151
+ discovery I/O. Calls reuse/coalesce the host's discovery; `refresh: true` requests
152
+ an explicit new observation. There is no background polling. Closing the owner
153
+ releases its discovery work and native query resources.
154
+
155
+ The catalog queries installed operating-system facilities. It does not install
156
+ drivers, download models, alter system policy or introduce a hardware admission
157
+ rule. A host capability failure remains an honest unavailable/partial result.