arcane-os 0.67.0 → 0.68.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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.68.0
4
+
5
+ - Add per-instance theme-switcher `configure({modes,labels})` and initial
6
+ `data-modes` / `data-system-label` presentation. Applications can select
7
+ Light / Dark / System with complete readable labels while the default
8
+ Auto / Light / Dark / Skin choices remain unchanged. Preserve selected,
9
+ disabled and keyboard-focus states with wrapping based on component space.
10
+ - Distinguish a wholly absent Core namespace from a missing method in a
11
+ registered service. Retain `METHOD_NOT_ALLOWED` and add exact `method`,
12
+ `namespace` and `reason` lookup metadata; genuine handler errors remain intact.
13
+ - Let the default preference adapter complete single operations through its
14
+ existing app-scoped local store only after an exact absent preferences
15
+ namespace response, retaining cancellation, disposal and full values.
16
+ An advertised rejected atomic batch is never retried as serial writes.
17
+ - Treat an exactly absent optional appearance namespace as unsupported, keeping
18
+ the original diagnostic. Preserve partial-service and genuine errors, the
19
+ existing theme persistence order, native window theming and app-owned host
20
+ preferences. No OS-wide appearance change, migration or dependency is added.
21
+
3
22
  ## 0.67.0
4
23
 
5
24
  - Add native Laya FP32 typed decisions through `arcane-os/local-ai/decisions`
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.67.0` SDK contract. Development applications declare
23
+ This checkout defines the `0.68.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;
@@ -54,6 +54,7 @@ alone does not make bare module names resolve in a browser.
54
54
  | Connect a browser to one Core transport | [Core browser client](core-client.md) |
55
55
  | Cancel one Core speech synthesis request without changing its payload | [Speech request lifetime](core-client.md#events-and-request-lifetime) and [Core facade](sdk-api.md#createcorefacade) |
56
56
  | Compose native Core dispatch, service lifecycle, and stdio hosting | [Native Core runtime](core-runtime.md) |
57
+ | Reuse dependency-ready native services without a renderer | [Core service composition](core-runtime.md#native-service-composition) and [existing ONNX owner](local-ai.md#native-service-owners) |
57
58
  | Compose application Core services during source development, independently of local AI | [Development Core services](core-development.md) |
58
59
  | Own an explicitly selected native Codex App Server session | [Codex App Server](codex-app-server.md) |
59
60
  | Expose application-owned tools and static resources through Node STDIO | [MCP STDIO server](mcp-stdio.md) |
@@ -66,12 +67,14 @@ alone does not make bare module names resolve in a browser.
66
67
  | Select the optional Intel NPU encoder with independent CUDA or CPU decoding | [Intel NPU Whisper](../guides/native-whisper-openvino.md) |
67
68
  | Edit an original PNG with a complete prompt and explicit strength | [Local image editing](local-image-editing.md) |
68
69
  | Project complete stored model files into native working files | [Model assets through Core](model-assets.md) |
70
+ | Prepare selected upstream model members in native services | [Native preparation and retained lifetime](model-assets.md#native-ownership) |
69
71
  | Retain a Nemotron model and diarize caller-fed audio streams | [Native speaker diarization](diarization.md) |
70
72
  | Persist application-selected preferences through Core | [Core preferences service](core-preferences.md) |
71
73
  | Read complete selected Git text without changing a checkout | [Git text snapshots](git-text-snapshot.md) |
72
74
  | Own a native working checkout under per-user ArcaneData/Repos | [Native repository workspaces](core-repositories.md) |
73
75
  | Read Git identity defaults or select child-only author and credential hints | [Git identity configuration](core-repositories.md#git-identity-configuration) |
74
76
  | Write, commit and push complete selected repository text | [Selected-text repository writing](core-repositories.md#write-commit-and-push-selected-text) |
77
+ | Observe existing checkout configuration and capture an explicit remote/ref | [Repository configuration and selected targets](core-repositories.md#existing-checkout-configuration-and-selected-targets) |
75
78
  | Build an installed-SDK portable Core payload | [Portable Core packaging](core-native-packaging.md) |
76
79
  | Pass host-selected state defaults and unchanged explicit launch values to services | [Launch-time locations](core-native-packaging.md#launch-time-locations) |
77
80
  | Build a Windows executable without an OS checkout | [Windows executable packaging](core-native-packaging.md#windows-executable) |
@@ -79,6 +82,7 @@ alone does not make bare module names resolve in a browser.
79
82
  | Subscribe to native events | [Arcane event reference](core/arcane-events.md) |
80
83
  | Use provider-neutral AI lifecycle, chat, speech, persistence, or document context | [Normalized AI](#normalized-ai) |
81
84
  | Choose a normalized typed browser decision surface | [Browser typed decisions](ai/browser-decisions.md) |
85
+ | Score complete typed options with native Laya FP32 | [Native typed decisions](native-decisions.md) |
82
86
  | Run a caller-selected local LLM in the browser | [Browser-WASM local AI](ai/browser-wasm.md) |
83
87
  | Run caller-selected Whisper/Kokoro, native browser recognition/voices or cloud TTS | [Browser speech providers](ai/browser-speech.md) |
84
88
  | Send TWiN chat, System One state/questions or image requests | [TWiN Cloud guide](ai/twin-cloud.md) |
@@ -239,6 +243,7 @@ Ollama, Wllama, Whisper, Kokoro, native, or cloud transport:
239
243
  | Run application-selected native llama.cpp/Ollama chat | [`arcane-os/ai/core-local`, `arcane-os/core/local-ai`](local-ai.md) | Explicit Core connection and selected native runtimes; installation, availability and loaded-model readiness remain distinct. |
240
244
  | Transcribe complete recordings with a retained native Whisper model | [`arcane-os/local-ai/whisper`](local-whisper.md) and [Core speech](native-speech.md) | Matching native helper, Whisper libraries, decoder and selected model; Windows defaults and explicit other-platform distributions have distinct preparation requirements. |
241
245
  | Run a local ONNX graph with named tensors | [`arcane-os/local-ai/onnx`, `arcane-os/ai/core-onnx`](local-ai.md#native-onnx-sessions) | Retained native workers; browser access requires an available Core ONNX service. |
246
+ | Score typed options without a renderer | [`arcane-os/local-ai/decisions`, `arcane-os/core/decisions`](native-decisions.md) | Explicit native Laya FP32 activation using the existing ONNX owner; complete scores and outputs, joined Worker cleanup. |
242
247
  | Generate local images from complete prompts and selected model resources | [`arcane-os/local-ai/image`, `arcane-os/ai/core-image`](local-image-generation.md) | Prepared native image runtime; browser access requires its Core service. CPU Windows/Linux and Mac Metal/CPU paths have distinct actual host requirements. |
243
248
  | Supply complete stored model members to a native engine | [`arcane-os/ai/core-model-assets`, `arcane-os/core/model-assets`](model-assets.md) | Available Core service; DBOPFS originals stay authoritative while native owners retain working files. |
244
249
  | Run caller-selected Whisper/Kokoro locally | [`arcane-os/ai/browser-speech`](ai/browser-speech.md) | Browser with DBOPFS, Web Locks, Workers and selected upstream sources; automatic WebNN NPU, WebGPU, then WASM loading, or an explicit backend. |
@@ -74,6 +74,7 @@ where listed. See [native output and host requirements](core-native-packaging.md
74
74
  | Share one native WebSocket by exact URI and ordered protocols | `arcane-os/websocket-client` | **Node** and **Browser**, using the host's native `WebSocket` | The selected upstream `ws-share` instance owns the connection pool and global observation. Native socket messages and events remain unchanged; there is no reconnect, heartbeat, application envelope, storage, or fallback. |
75
75
  | Build browser UI and app-local behavior | `/arcane/modules/*.js`, shared entities, and components | **Browser**; many modules also run inside every native renderer | Pure modules own their result contracts. Modules that call `Arcane` inherit the bridge boundary described below. |
76
76
  | Score caller-supplied typed options with Laya or Julia | `arcane-os/ai/browser-decisions` | **Browser** only; caller-selected upstream runtime/model and backend | Typed decisions retain complete rows and return typed values, scores, probabilities, and diagnostics in caller order. This surface is separate from chat and speech and does not silently change model, precision, backend, or family. |
77
+ | Score complete typed options with native Laya FP32 | [`arcane-os/local-ai/decisions`, `arcane-os/core/decisions`](native-decisions.md) | **Node** Windows/Linux/macOS with selected ONNX Runtime and model; Android needs host adaptation | Reuses the existing native ONNX owner and a tokenizer Worker, with explicit load and joined cleanup. Preserves separate state/question/options/type, original rows, raw scores and complete graph outputs. GPU preference attempts actual advertised providers before CPU; provider configuration does not prove physical acceleration. No renderer, chat provider or automatic persistence is required. |
77
78
  | Select and observe independent LLM/STT/TTS roles | `/arcane/modules/AIProviderRuntime.js` and `AIRuntimeState.js` | **Cross-host** controller/state; registered providers retain their own host requirements | Required/projected provider members, route/configuration records, and status fields; per-role lifecycle, cancellation, stream cleanup, sticky state, and startup barriers are normalized. `localOnly` creates no fallback. |
78
79
  | Run a caller-selected local LLM entirely in a browser renderer | `arcane-os/ai/browser-wasm` through `createArcaneAI()` | **Browser** only; secure context, WebAssembly, and OPFS/DBOPFS; full WebGPU offload by default or explicit CPU with `gpuLayers:0` | The public AI API module normalizes multi-model lifecycle, status, complete all-choice streaming, cancellation, exact ordered structural tool-call visibility, and session persistence. Model sources are canonical ordered file descriptors; licenses and model choice remain application policy. |
79
80
  | Run caller-selected Whisper or Kokoro in a browser renderer | `arcane-os/ai/browser-speech` registered with `AIProviderRuntime` | **Browser** only; DBOPFS, Web Locks, Workers, Fetch/object URLs, and a caller-supplied self-contained runtime/model closure are required | Independent provider/2 lifecycle and status, with `auto`, `webnn-npu`, `webgpu`, or `wasm` execution. Whisper owns one Worker/session; Kokoro defaults to four and accepts one through four. Complete model/runtime selection, offline behavior, cancellation, Worker teardown, and request/result shapes are normalized. No runtime/model content or cloud fallback is supplied. |
@@ -123,6 +123,20 @@ The shared `arcane-os/core/contracts` module owns `CORE_PROTOCOL` (`arcane/1`),
123
123
  availability errors remain observable; unavailable services are not replaced
124
124
  with browser behavior.
125
125
 
126
+ A request with no registered handler retains code `METHOD_NOT_ALLOWED` and
127
+ adds the exact `method`, its first dotted `namespace`, and a `reason`.
128
+ `core-namespace-unavailable` means the dispatcher has neither a service with
129
+ that name nor any registered method in that namespace. Otherwise the reason is
130
+ `core-method-unavailable`; the built-in `system`, `app`, and `version`
131
+ namespaces also count as present. This is a dispatch lookup result before any
132
+ handler or permission decision, not a capability-discovery call. A read-only
133
+ or partially registered service therefore never appears wholly absent.
134
+ Handler/startup/permission errors remain unchanged and acquire none of these
135
+ lookup markers. Generic or older `METHOD_NOT_ALLOWED` errors do not establish
136
+ namespace absence. An optional browser adapter may recognize the exact absent
137
+ namespace and method according to its own documented contract; the dispatcher
138
+ does not select or create another implementation.
139
+
126
140
  Events have `{protocol, type:'event', event, data, time}`:
127
141
 
128
142
  - `core.ready`: the dispatcher is connected; service/model readiness is separate.
@@ -1,17 +1,59 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "sdkVersion": "0.66.0",
3
+ "sdkVersion": "0.67.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",
7
7
  "moduleSystem": "ESM"
8
8
  },
9
- "memberCount": 290,
9
+ "memberCount": 293,
10
10
  "runtimeSubpathPatterns": {
11
11
  "arcane-os/modules/*": "./runtime/arcane/modules/*",
12
12
  "arcane-os/entities/*": "./runtime/arcane/entities/*"
13
13
  },
14
14
  "members": [
15
+ {
16
+ "id": "local-ai-decisions:createNativeDecisionModel",
17
+ "name": "createNativeDecisionModel",
18
+ "displayName": "createNativeDecisionModel()",
19
+ "kind": "function",
20
+ "signature": "createNativeDecisionModel({onnx,modelAssets,workingDirectory,paths,model='onnx-community/laya-typed-decisions-ONNX',revision='main',dtype='fp32',sessionOptions,executionPreference='gpu',signal}={})",
21
+ "entrypoints": ["arcane-os/local-ai/decisions"],
22
+ "primaryImport": "arcane-os/local-ai/decisions",
23
+ "group": "Native typed decisions",
24
+ "summary": "Explicit native Laya FP32 option scoring through an existing ONNX owner and a tokenizer Worker.",
25
+ "availability": "Node on Windows/Linux/macOS with selected ONNX Runtime and model files; Android needs host adaptation",
26
+ "protocol": "Shared browser/native decision encoding with native named tensor results",
27
+ "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. Explicit load prepares selected upstream files or uses caller-owned paths; GPU preference attempts advertised providers then CPU. Cancellation retires the shared activation; files remain retained through actual ONNX and tokenizer Worker exit. No chat history or duplicate inference engine."
28
+ },
29
+ {
30
+ "id": "core-decisions:createNativeDecisionService",
31
+ "name": "createNativeDecisionService",
32
+ "displayName": "createNativeDecisionService()",
33
+ "kind": "function",
34
+ "signature": "createNativeDecisionService(configuration={}, {appRoot=process.cwd()}={})",
35
+ "entrypoints": ["arcane-os/core/decisions"],
36
+ "primaryImport": "arcane-os/core/decisions",
37
+ "group": "Native typed decisions",
38
+ "summary": "Renderer-independent native Laya service reusing the registered local-AI and model-assets owners.",
39
+ "availability": "Node Core with selected native ONNX and model files",
40
+ "protocol": "arcane/1 decisions.status/load/evaluate/unload and decisions.state",
41
+ "normalization": "Registration starts no model or tokenizer. Explicit load obtains dependency-ready same-process owners and reacquires getONNXRuntime after recovery. 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."
42
+ },
43
+ {
44
+ "id": "core-decisions:default",
45
+ "name": "default",
46
+ "displayName": "createNativeDecisionService default export",
47
+ "kind": "function",
48
+ "signature": "default as createNativeDecisionService",
49
+ "entrypoints": ["arcane-os/core/decisions"],
50
+ "primaryImport": "arcane-os/core/decisions",
51
+ "group": "Native typed decisions",
52
+ "summary": "Default binding for the same native decision Core service factory.",
53
+ "availability": "Node Core runtime",
54
+ "protocol": "arcane/1 service definition",
55
+ "normalization": "Binding identity equals the named createNativeDecisionService export"
56
+ },
15
57
  {
16
58
  "id": "local-ai-whisper:createWhisperRuntime",
17
59
  "name": "createWhisperRuntime",
@@ -227,14 +269,14 @@
227
269
  "name": "createCoreImageRuntime",
228
270
  "displayName": "createCoreImageRuntime()",
229
271
  "kind": "function",
230
- "signature": "createCoreImageRuntime({client=getInstalledCoreClient(),signal,onEvent}={})",
272
+ "signature": "createCoreImageRuntime({client,signal,onEvent}={})",
231
273
  "entrypoints": ["arcane-os/ai/core-image"],
232
274
  "primaryImport": "arcane-os/ai/core-image",
233
275
  "group": "Core local images",
234
276
  "summary": "Uses an available Core image service for generation and original-PNG editing with synchronous Thinking acknowledgement, lifecycle replay and complete PNG Blobs.",
235
277
  "availability": "Browser/native WebView with an available Core image service; construction opens no connection or model",
236
278
  "protocol": "arcane/1 image.status/load/generate/edit/unload and image.state/progress",
237
- "normalization": "Returns load/generate/edit/unload/inspect/current/subscribe/close; edit preserves the complete original PNG Blob/File through transport with explicit strength; timeoutMs is zero, close cancels only this accessor and explicit unload owns the shared model"
279
+ "normalization": "Returns load/generate/edit/unload/inspect/current/subscribe/close; omitted client follows installation/replacement/retirement, explicit client including null remains fixed. Retirement cancels owned work and clears readiness; late old-client output is suppressed. Edit preserves the complete original PNG Blob/File through transport with explicit strength; timeoutMs is zero, close cancels only this accessor and explicit unload owns the shared model"
238
280
  },
239
281
  {
240
282
  "id": "core-image-service:createLocalImageService",
@@ -290,7 +332,7 @@
290
332
  "summary": "Owns selected native working-file projections and engine retain handles while original stored model files remain unchanged.",
291
333
  "availability": "Node Core with an application-selected working directory",
292
334
  "protocol": "arcane/1 modelAssets.open/write/complete/release/status and modelAssets.state",
293
- "normalization": "Returns current/retain/dispose and Core service lifecycle; final native release joins writes and closes before deleting only its working projection"
335
+ "normalization": "Returns current/prepare/retain/release/dispose and Core service lifecycle. Explicit prepare({id,workingDirectory,members:[{path,url}],signal,onProgress}) streams complete upstream members through the existing file owner and reports file/phase progress. release(id) relinquishes preparation ownership; cancellation and cleanup join fetches/writes. Native retains survive until actual engine release before deleting only the owned working projection. Startup downloads nothing."
294
336
  },
295
337
  {
296
338
  "id": "core-model-assets-service:default",
@@ -318,21 +360,21 @@
318
360
  "summary": "Owns explicitly loaded ONNX graphs and complete named tensor inference in retained native workers.",
319
361
  "availability": "Node on Windows/Linux/macOS x64/arm64 with prepared onnxruntime-node; Android needs host adaptation",
320
362
  "protocol": "Per-session worker lifecycle with structured-clone tensors and replaying state",
321
- "normalization": "Returns load/run/unload/current/subscribe/close; explicit model and session IDs; native calls may delay worker termination, and close observes actual exit"
363
+ "normalization": "Returns load/run/unload/current/subscribe/close; load accepts executionPreference gpu for advertised provider creation attempts then CPU, while explicit sessionOptions.executionProviders takes precedence. Complete execution diagnostics distinguish configured providers from physical acceleration. stopping revokes new runs while exited records actual Worker exit; recoverable run errors preserve the session. Cleanup joins exit and output delivery even when termination fails."
322
364
  },
323
365
  {
324
366
  "id": "core-onnx:createCoreONNXRuntime",
325
367
  "name": "createCoreONNXRuntime",
326
368
  "displayName": "createCoreONNXRuntime()",
327
369
  "kind": "function",
328
- "signature": "createCoreONNXRuntime({client=getInstalledCoreClient()}={})",
370
+ "signature": "createCoreONNXRuntime({client}={})",
329
371
  "entrypoints": ["arcane-os/ai/core-onnx"],
330
372
  "primaryImport": "arcane-os/ai/core-onnx",
331
373
  "group": "Core local AI",
332
374
  "summary": "Accesses local system ONNX through an existing Core client with complete tensor transport.",
333
375
  "availability": "Browser with available Core ONNX service; construction installs no runtime or model",
334
376
  "protocol": "arcane/1 onnx.status/load/run/unload and localai.state",
335
- "normalization": "Returns load/run/unload/inspect/current/subscribe/close; preserves typed and string tensors; accessor close cancels its requests without closing shared Core or implicitly unloading shared models"
377
+ "normalization": "Returns load/run/unload/inspect/current/subscribe/close; omitted client follows late installation/replacement/retirement and reads current status, while explicit client including null stays fixed. Retirement clears readiness and cancels owned work; late old-client results are suppressed. Preserves typed and string tensors; close removes subscriptions and cancels requests without closing shared Core or implicitly unloading shared models"
336
378
  },
337
379
  {
338
380
  "id": "root:createGitTextSnapshot",
@@ -409,14 +451,14 @@
409
451
  "name": "createCoreLocalAIProvider",
410
452
  "displayName": "createCoreLocalAIProvider()",
411
453
  "kind": "function",
412
- "signature": "createCoreLocalAIProvider({client=getInstalledCoreClient(),id='llama.cpp'}={})",
454
+ "signature": "createCoreLocalAIProvider({client,id='llama.cpp'}={})",
413
455
  "entrypoints": ["arcane-os/ai/core-local"],
414
456
  "primaryImport": "arcane-os/ai/core-local",
415
457
  "group": "Core local AI",
416
458
  "summary": "Browser llama.cpp provider through an explicitly connected Core, with application-owned model selection and complete response streaming.",
417
459
  "availability": "Browser with an available Core local AI service; construction installs no runtime or model",
418
460
  "protocol": "arcane-ai-provider/2 LLM lifecycle over arcane/1",
419
- "normalization": "Preserves complete payloads and original parsed chunks; waits for the exact requested model and cancels on observed readiness loss"
461
+ "normalization": "Preserves complete payloads and original parsed chunks; waits for the exact requested model and cancels on observed readiness loss. Omitted client follows installation/replacement/retirement; explicit client including null stays fixed. Inspect reports load capability separately from loaded readiness; disposal releases only an actually retained selection through its captured client. Provider/2 role state reconciles at existing operation boundaries, not through an idle state subscription."
420
462
  },
421
463
  {
422
464
  "id": "core-local-ai:createLocalAIService",
@@ -430,7 +472,7 @@
430
472
  "summary": "Composes selected native llama.cpp, Ollama and ONNX engines as a Core service with retained process/worker ownership and model lifecycle.",
431
473
  "availability": "Node with selected upstream runtime distributions or available configured system services",
432
474
  "protocol": "arcane/1 methods and events; llama.cpp OpenAI HTTP/SSE, Ollama native HTTP/NDJSON and ONNX tensor transport",
433
- "normalization": "Application-owned runtime and model requirements; optional prepare supplies installed records during this service startup; independent services, complete responses and exact owned-process cleanup"
475
+ "normalization": "Application-owned runtime and model requirements; optional prepare supplies installed records during this service startup. getONNXRuntime() returns the actual prepared ONNX owner for native composition after context.getService('local-ai'); callers reacquire after recovery and retain the acquired handle for cleanup. llama.status/models observe without starting or restarting an engine. Independent services, complete responses and exact owned-process cleanup remain unchanged."
434
476
  },
435
477
  {
436
478
  "id": "core-local-ai:default",
@@ -4276,7 +4318,7 @@
4276
4318
  "summary": "Creates an app-neutral Core dispatcher with explicit services, request lifetimes, events and graceful drain.",
4277
4319
  "availability": "Node on Windows, Linux and macOS native hosts; Android requires its host adaptation",
4278
4320
  "protocol": "arcane/1 dispatcher and service lifecycle",
4279
- "normalization": "Starts services independently, preserves complete results/errors, cancels only request-lifetime work and drains accepted service work; runtime.replay publishes current dispatcher/service state without restart and preserves reentrant lifecycle publication order"
4321
+ "normalization": "Starts services independently; runtime.getService(name) and context.getService(name) return the same registered object after only its shared startup, without a renderer or global barrier. Lookup is readiness, not a lifetime lease; new lookups reject during shutdown. Preserves complete results/errors, cancels request-lifetime work and drains accepted service work; runtime.replay publishes current dispatcher/service state without restart and preserves reentrant lifecycle publication order"
4280
4322
  },
4281
4323
  {
4282
4324
  "id": "core-stdio:encodeCoreFrame",
@@ -967,8 +967,9 @@
967
967
  {
968
968
  "file": "runtime/arcane/components/theme-switcher.html",
969
969
  "name": "theme-switcher.html",
970
- "purpose": "Selects and refreshes system, light, dark, or custom theme mode.",
970
+ "purpose": "Selects and refreshes system, light, dark, or custom theme mode with per-instance choices and labels.",
971
971
  "methods": [
972
+ "configure()",
972
973
  "setMode()",
973
974
  "refresh()"
974
975
  ],
@@ -1083,7 +1083,7 @@
1083
1083
  "summary": "Loads and updates schema-defined app preferences through native storage with a narrow browser fallback.",
1084
1084
  "availability": "Browser/native hybrid",
1085
1085
  "protocol": "Arcane.preferences or app-scoped localStorage + per-realm globalThis.arcaneEvents authority",
1086
- "normalization": "Complete ordinary values remain mutable; setAll uses one optional atomic adapter batch for every selected value when advertised, otherwise retains complete serial per-key compatibility, never retries a rejected dispatched batch serially, and only exact unsupported native capability changes future operations to the browser fallback.",
1086
+ "normalization": "Complete values and operation context retained; the default adapter recognizes only exact missing preferences namespace/matching method or existing Android unsupported capability before selecting existing app-scoped localStorage. Single operations retry once after disposal/cancellation checks; partial-service and genuine failures reject unchanged. An advertised rejected batch is never retried serially, and its original rejection is preserved before later local operations.",
1087
1087
  "surface": "Event/error constants, default `PreferenceStore`, re-exported `Preference`/schema; load/set/setAll/reset APIs, required adapter get/set/delete plus optional setMany, state-free EventTarget/on compatibility, and `dispose()`."
1088
1088
  },
1089
1089
  {
@@ -1291,10 +1291,10 @@
1291
1291
  "exports": [
1292
1292
  "default"
1293
1293
  ],
1294
- "summary": "Reads or applies native appearance, returning an explicit unsupported browser state when no bridge exists.",
1294
+ "summary": "Reads or applies native appearance, returning an explicit unsupported result when the bridge or optional namespace is absent.",
1295
1295
  "availability": "Browser/native hybrid",
1296
1296
  "protocol": "Arcane.appearance",
1297
- "normalization": "Absent bridge normalized; native result/error preserved.",
1297
+ "normalization": "Absent bridge keeps its browser result. Exact missing appearance namespace/matching method returns supported:false with the complete original diagnostic; no platform guess or cached absence. Partial-method, permission, transport, cancellation and other genuine errors reject unchanged; available() is callable bridge presence only.",
1298
1298
  "surface": "default `SystemAppearance`; `available()`, `current()`, `apply()`."
1299
1299
  },
1300
1300
  {
@@ -112,7 +112,7 @@ snapshot remains owned until the browser's `afterprint` event.
112
112
  | [`task-progress.html`](#task-progresshtml) | Runs and displays a task list with started/change/complete/error state. | `configure()`<br>`setTasks()`<br>`updateTask()`<br>`runTasks()`<br>`clear()`<br>`destroy()` | `task-progress-ready`<br>`task-progress-started`<br>`task-progress-change`<br>`task-progress-complete`<br>`task-progress-error` | Task state normalized; injected task results mixed |
113
113
  | [`terminal-workspace.html`](#terminal-workspacehtml) | Presents multiple terminal sessions, output, active selection, theme, and terminal actions. | `configure()`<br>`addSession()`<br>`removeSession()`<br>`activateSession()`<br>`append()`<br>`clear()`<br>`setState()`<br>`setTheme()`<br>`focus()`<br>`destroy()` | `terminal-workspace-ready`<br>`terminal-submit`<br>`terminal-interrupt`<br>`terminal-clear`<br>`terminal-session-new`<br>`terminal-session-close`<br>`terminal-session-select`<br>`terminal-settings` | UI/session state normalized; native command results supplied externally |
114
114
  | [`theme-editor.html`](#theme-editorhtml) | Edits, previews, saves, and resets semantic custom theme tokens. | `configure()`<br>`getTheme()`<br>`setTheme()`<br>`setBusy()`<br>`setStatus()`<br>`destroy()` | `theme-editor-ready`<br>`theme-preview`<br>`theme-save`<br>`theme-reset` | Fully normalized Theme values |
115
- | [`theme-switcher.html`](#theme-switcherhtml) | Selects and refreshes system, light, dark, or custom theme mode. | `setMode()`<br>`refresh()` | No component-specific event | Preference/native appearance behavior mixed; no component-ready contract |
115
+ | [`theme-switcher.html`](#theme-switcherhtml) | Selects and refreshes system, light, dark, or custom theme mode with per-instance choices and labels. | `configure()`<br>`setMode()`<br>`refresh()` | No component-specific event | Preference/native appearance behavior mixed; no component-ready contract |
116
116
  | [`unified-inbox.html`](#unified-inboxhtml) | Displays provider-neutral communication threads with active/loading state. | `configure()`<br>`setThreads()`<br>`setActive()`<br>`setLoading()`<br>`destroy()` | `unified-inbox-ready`<br>`inbox-refresh`<br>`thread-select` | DOM-normalized |
117
117
  | [`voice-transcription.html`](#voice-transcriptionhtml) | Records manual clips or optional continuous microphone audio with pause/periodic segments, ordered transcription and saves, retained failures, and complete transcript delivery. | `configure()`<br>`requestSTTActivation()`<br>`startRecording()`<br>`stopRecording()`<br>`retryTranscription()`<br>`cancelRecording()`<br>`save()`<br>`completeTranscription()/complete()`<br>`clear()`<br>`reset()`<br>`destroy()` | `voice-transcription-ready`<br>`voice-transcription-state`<br>`voice-transcription-segment`<br>`voice-transcription-empty`<br>`voice-transcription-error`<br>`voice-transcription-change`<br>`voice-transcription-complete`<br>`speech-transcription-complete`<br>`speech-transcription-cancelled`<br>`speech-stt-activation-request`<br>`speech-stt-activation-error` | Sticky STT readiness, capture/queue lifecycle, retry, cancellation, and complete state/text are normalized; media/provider behavior remains external |
118
118
  | [`weather-widget.html`](#weather-widgethtml) | Displays normalized current and daily weather with refresh intent. | `setWeather()`<br>`clear()`<br>`destroy()` | `weather-widget-ready`<br>`weather-refresh` | Display normalized; provider supplied externally |
@@ -2015,11 +2015,61 @@ Shared dependencies: [`Theme.js`](runtime-entities.md#themejs).
2015
2015
 
2016
2016
  ### Overview
2017
2017
 
2018
- Selects and refreshes system, light, dark, or custom theme mode.
2018
+ Selects and refreshes system, light, dark, or custom theme mode. Native buttons
2019
+ retain complete readable labels and wrap when their component runs out of room,
2020
+ independently of viewport width. Each button has a `2.75rem` minimum target in
2021
+ both dimensions. Selected buttons retain `aria-pressed` and an inset outline;
2022
+ disabled choices and keyboard focus have distinct presentation.
2019
2023
 
2020
2024
  ### Public surface
2021
2025
 
2022
- Methods/properties: `setMode()`, `refresh()`.
2026
+ Methods/properties: `configure(options)`, `setMode(mode='system')`, `refresh()`.
2027
+
2028
+ `configure({modes,labels})` changes only this component's presentation:
2029
+
2030
+ - `modes` is an ordered array of the available identifiers `system`, `light`,
2031
+ `dark`, and `custom`. It selects both visible choices and DOM/keyboard order.
2032
+ Omitted `modes` retains the current presentation; an empty array displays no
2033
+ choices. Unknown or repeated identifiers throw `TypeError` before changing
2034
+ the presentation.
2035
+ - `labels` is a partial record of complete string labels keyed by those mode
2036
+ identifiers. Omitted labels keep their current text. Text is assigned through
2037
+ `textContent`, never interpreted as markup or reduced to initials. A label for
2038
+ a built-in mode that is not a string throws `TypeError`.
2039
+ - The return value is `{modes,labels}` containing independent mutable copies of
2040
+ the current presentation. Later changes to supplied/returned records do not
2041
+ reconfigure the component.
2042
+
2043
+ Label-only and unchanged-order configuration retain the existing buttons in
2044
+ place. An actual reorder restores keyboard focus to the previously focused
2045
+ button when that choice remains visible and enabled, without scrolling.
2046
+
2047
+ The default remains Auto / Light / Dark / Skin, using identifiers
2048
+ `system`, `light`, `dark`, and `custom`. A caller may omit Skin or change Auto's
2049
+ label to System without removing custom-theme support from other instances.
2050
+ Configuration never changes preferences, activates a theme, or replaces the
2051
+ current selected mode. If the active mode is omitted, no visible button claims
2052
+ to be selected. Restoring that choice restores its existing pressed and disabled
2053
+ state. Skin remains disabled until the manager has a saved custom theme.
2054
+
2055
+ For the initial presentation, `data-modes` accepts whitespace-separated mode
2056
+ identifiers and `data-system-label` supplies the complete system-mode label.
2057
+ These attributes are read once before the asynchronous ThemeManager import and
2058
+ preference refresh, so initial choices do not wait for `html-import-ready`.
2059
+ Use `configure()` after that event for subsequent presentation changes;
2060
+ changing these attributes later does not reconfigure an existing instance.
2061
+
2062
+ `setMode()` updates pressed state and the host's `data-mode` only. `refresh()`
2063
+ loads the existing ThemeManager state, updates selection and Skin availability,
2064
+ and resolves with the complete manager state. Clicking a choice uses the same
2065
+ manager persistence/activation operation; buttons remain disabled while that
2066
+ operation is pending. Presentation configuration preserves that pending state.
2067
+
2068
+ The host inherits text color. Existing `--app-bar-border` (falling back to
2069
+ `--border-color`), `--app-bar-accent` (then `--secondary-color`),
2070
+ `--app-bar-active-text` (then `--primary-color`), and `--focus-color` overrides
2071
+ remain supported. Applications own their palette and surrounding placement;
2072
+ the component adds no application-specific palette or settings section.
2023
2073
 
2024
2074
  This fragment declares no component-specific readiness or action event; use the loader's `html-import-ready` only to know that import execution completed.
2025
2075
 
@@ -2033,11 +2083,18 @@ Shared dependencies: [`ThemeManager.js`](runtime-modules.md#thememanagerjs).
2033
2083
 
2034
2084
  ```html
2035
2085
  <html-import
2036
- id="theme-switcher.html"
2037
- href="/arcane/components/theme-switcher.html">
2086
+ id="theme-switcher"
2087
+ href="/node_modules/arcane-os/runtime/arcane/components/theme-switcher.html"
2088
+ data-modes="light dark system"
2089
+ data-system-label="System">
2038
2090
  </html-import>
2039
2091
  ```
2040
2092
 
2093
+ This instance displays Light / Dark / System in that order. An instance without
2094
+ the two configuration attributes retains all four default choices. Both use
2095
+ the existing shared preference owner; presentation does not create a new store
2096
+ or migrate saved themes.
2097
+
2041
2098
  ## unified-inbox.html
2042
2099
 
2043
2100
  ### Overview
@@ -120,7 +120,7 @@ own asynchronous work, cancellation, and backpressure.
120
120
  | [`OllamaSettings.js`](#ollamasettingsjs) | esm | Defines complete runtime/service preference schemas and deterministic Arcane brain alias names. | Cross-host | Fully normalized settings/name contract. |
121
121
  | [`OpenMeteoWeatherProvider.js`](#openmeteoweatherproviderjs) | esm | Searches and loads Open-Meteo data into complete mutable Arcane weather entities. | Browser / native WebView / server with fetch + cloud | Provider data normalized to mutable entities; transport errors mixed. |
122
122
  | [`PersistentAIChatSession.js`](#persistentaichatsessionjs) | esm | Adds explicit retained-history/memory policy to complete configured chat without changing DBOPFS or ChatEntity semantics. | Browser / native WebView with DBOPFS and configured chat | Retained context commits atomically; `persist:false` turns are one-operation-only. |
123
- | [`PreferenceStore.js`](#preferencestorejs) | esm | Loads and updates schema-defined app preferences through native storage with a narrow browser fallback. | Browser/native hybrid | Complete ordinary values remain mutable; setAll uses one optional atomic adapter batch for every selected value when advertised, otherwise performs complete ordered serial writes, and only exact unsupported native capability changes future operations to the browser fallback. |
123
+ | [`PreferenceStore.js`](#preferencestorejs) | esm | Loads and updates schema-defined app preferences through native storage with a narrow browser fallback. | Browser/native hybrid | Complete values and operation context retained; exact optional namespace absence or existing Android unsupported capability selects the existing local adapter; partial-service and genuine failures remain errors; an advertised rejected batch is never retried serially. |
124
124
  | [`PreparedSpeech.js`](#preparedspeechjs) | esm | Owns detached ordered preparation, semantic audio reuse, and per-caller cancellation behind AI.prepareTTS. | Browser / native WebView with injected synthesis and optional DBOPFS | Complete original inputs, ordered audio metadata, durable reuse, and observable preparation results. |
125
125
  | [`PrintView.js`](#printviewjs) | esm | Prints current rendered content and title through one document-owned print lifecycle. | Browser / supported native WebView print implementation | Complete rendered snapshot, preparation cancellation, resource lifetime, and honest request result. |
126
126
  | [`QRCode.min.js`](#qrcodeminjs) | classic-script | Vendored QRCode generator for DOM, canvas, SVG, and image output. | Browser vendor script | Vendor-native. |
@@ -134,7 +134,7 @@ own asynchronous work, cancellation, and backpressure.
134
134
  | [`ScreenCapture.js`](#screencapturejs) | esm | Captures a display surface as image, video, or GIF with explicit lifecycle events. | Browser / native WebView | State/events normalized; permission and codec errors mixed. |
135
135
  | [`SpeechPlayback.js`](#speechplaybackjs) | esm | Admits complete speech segments to a capacity-advertising provider immediately, retains serialized native/custom lookahead, and plays every result in exact indexed order. | Browser + native bridge | Stored part text stays exact; the outbound speech-input copy receives automatic formatting-mark cleanup; provider/media failures remain mixed. |
136
136
  | [`StaticDocumentCatalog.js`](#staticdocumentcatalogjs) | esm | Loads a positive static document inventory with cache, search, and complete context. | Browser / native WebView / server with fetch | Mutable complete catalog/content normalization; malformed data and transport failures remain visible. |
137
- | [`SystemAppearance.js`](#systemappearancejs) | esm | Reads or applies native appearance, returning an explicit unsupported browser state when no bridge exists. | Browser/native hybrid | Absent bridge normalized; native result/error preserved. |
137
+ | [`SystemAppearance.js`](#systemappearancejs) | esm | Reads or applies native appearance, returning an explicit unsupported result when the bridge or optional namespace is absent. | Browser/native hybrid | Exact absent optional namespace normalized with its complete error; partial-method and genuine native errors preserved. |
138
138
  | [`SystemPlatformPresentation.js`](#systemplatformpresentationjs) | classic-script | Maps kernel names to presentation labels/classes without granting platform authority. | Browser / native WebView classic script | Fully normalized presentation only. |
139
139
  | [`SystemToolRegistry.js`](#systemtoolregistryjs) | esm | Registers validated command builders and constructs command strings without executing them. | Cross-host | Fully normalized definitions/quoting. |
140
140
  | [`TerminalClient.js`](#terminalclientjs) | esm | Maps native terminal sessions and Arcane events into an EventTarget client. | Native bridge | Client events/state normalized; native result/error mixed. |
@@ -3475,9 +3475,31 @@ Exact exports: `PREFERENCE_STORE_ERROR_CODES`,
3475
3475
 
3476
3476
  **Browser/native hybrid.** Complete ordinary values and returned snapshots remain
3477
3477
  mutable after schema normalization. Non-Android
3478
- `Arcane.preferences.setMany()` supplies the optional atomic batch. Only exact
3479
- unsupported native capability changes future operations to app-scoped
3480
- localStorage; an in-flight advertised batch is never downgraded after rejection.
3478
+ `Arcane.preferences.setMany()` supplies the optional atomic batch. The default
3479
+ adapter uses the existing app-scoped localStorage owner when there is no native
3480
+ preferences facade, on the existing Android route, or after either the existing
3481
+ `ANDROID_CAPABILITY_UNSUPPORTED` error or an exact Core lookup error:
3482
+ `code:'METHOD_NOT_ALLOWED'`, `reason:'core-namespace-unavailable'`,
3483
+ `namespace:'preferences'`, and `method:'preferences.<current operation>'`.
3484
+ This marker proves the entire optional namespace was absent before dispatch.
3485
+ A missing method in a partial/read-only service, an older unmarked error,
3486
+ permission denial, persistence failure, transport error, or cancellation never
3487
+ selects local storage. Explicit caller adapters retain their own behavior.
3488
+
3489
+ For `get`, `set`, and `delete`, that positively absent namespace retries the
3490
+ same operation once through the existing local adapter, then keeps that adapter
3491
+ for the store's later operations. Complete arguments and the existing
3492
+ `{operationId,signal}` context are retained. Disposal and caller cancellation
3493
+ are checked before selecting or invoking the local adapter, so a late native
3494
+ absence result cannot start a cancelled local write. No data location, schema,
3495
+ saved value, or migration is introduced.
3496
+
3497
+ An in-flight advertised `setMany` batch is never downgraded to serial writes
3498
+ after rejection. Exact namespace absence selects the local adapter for later
3499
+ operations and preserves that batch's original rejection with no local write.
3500
+ ThemeManager's first Light/Dark/System choice uses two ordinary `set` calls,
3501
+ not a batch: both may complete through the existing local owner before the
3502
+ selected theme is applied. Its persistence-first ordering is unchanged.
3481
3503
  If cancellation settles after a native batch was dispatched, reload the store to
3482
3504
  reconcile any atomic host commit that completed before cancellation. Transport:
3483
3505
  Arcane.preferences or app-scoped localStorage. [Deep protocol details](protocols.md).
@@ -4064,7 +4086,8 @@ console.log(Object.keys(module));
4064
4086
 
4065
4087
  ### Overview
4066
4088
 
4067
- Reads or applies native appearance, returning an explicit unsupported browser state when no bridge exists.
4089
+ Reads or applies native appearance, returning an explicit unsupported result
4090
+ when the bridge or its optional namespace is absent.
4068
4091
 
4069
4092
  ### Public surface
4070
4093
 
@@ -4074,7 +4097,21 @@ Exact exports: `default`.
4074
4097
 
4075
4098
  ### Availability and normalization
4076
4099
 
4077
- **Browser/native hybrid.** Absent bridge normalized; native result/error preserved. Transport: Arcane.appearance. [Deep protocol details](protocols.md).
4100
+ **Browser/native hybrid.** No bridge or callable method retains the existing
4101
+ `{supported:false,platform:'browser'}` result. A callable facade is not proof of
4102
+ an installed service: `available()` reports callable `apply` presence only.
4103
+ `current()` and `apply()` recognize only a matching Core
4104
+ `METHOD_NOT_ALLOWED` error with `reason:'core-namespace-unavailable'`,
4105
+ `namespace:'appearance'`, and the exact requested method. They return
4106
+ `{supported:false,reason:'core-namespace-unavailable',error}`, retaining the
4107
+ complete original error for diagnostics and making no platform or native
4108
+ application claim. Absence is not cached, so later service registration can
4109
+ serve the next operation. Partial-method absence, unmarked permission errors,
4110
+ transport/cancellation failures and all other native errors reject unchanged.
4111
+ Native success values and existing appearance input normalization remain
4112
+ unchanged. ThemeManager may therefore complete a browser color-scheme change
4113
+ when native appearance is explicitly unsupported, while genuine failures stay
4114
+ observable. Transport: Arcane.appearance. [Deep protocol details](protocols.md).
4078
4115
 
4079
4116
  ### Example
4080
4117
 
@@ -69,6 +69,8 @@ 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. |
73
+ | `arcane-os/core/decisions` | Renderer-independent typed-decision service composed with existing local-AI and model-assets owners. |
72
74
  | `arcane-os/ai/core-onnx` | Optional browser access to system ONNX through an existing Core connection. |
73
75
  | `arcane-os/local-ai/image` | Retained native stable-diffusion.cpp context, complete PNG generation and owned worker lifetime. |
74
76
  | `arcane-os/ai/core-image` | Browser image lifecycle and complete PNG Blobs through an available Core service. |
@@ -169,6 +171,9 @@ The remaining data-only subpaths are eight JSON Schemas and package metadata.
169
171
  | `acceptWebSocket()` | function | `arcane-os`, `arcane-os/websocket` | WebSocket protocol | Node; browser-native client interoperability |
170
172
  | `WS` | class | `arcane-os`, `arcane-os/websocket-client` | Shared WebSocket clients | Node and native-WebSocket browser hosts |
171
173
  | `createBrowserDecisionModel()` | function | `arcane-os/ai/browser-decisions` | Browser typed decisions | Browser module Workers and the caller-selected inference backend |
174
+ | `createNativeDecisionModel()` | function | `arcane-os/local-ai/decisions` | Native typed decisions | Node with an existing native ONNX owner and selected Laya FP32 files |
175
+ | `createNativeDecisionService()` | function | `arcane-os/core/decisions` | Native typed decisions | Node Core with existing local-AI and model-assets services |
176
+ | `createNativeDecisionService default export` | function | `arcane-os/core/decisions` | Native typed decisions | Same native decision service factory |
172
177
  | `fetchRequest()` | function | `arcane-os/ai/twin-cloud` | TWiN Cloud requests | Node and Browser; remote HTTPS provider |
173
178
  | `fetchSystemOneRequest()` | function | `arcane-os/ai/twin-cloud` | TWiN Cloud state-and-questions requests | Node and Browser; remote HTTPS provider |
174
179
  | `generateImages()` | function | `arcane-os/ai/twin-cloud` | TWiN Cloud image generation | Node and Browser with Fetch and Blob; remote HTTPS provider |
@@ -7297,9 +7302,18 @@ createCoreRuntime({application, version, services=[]}={})
7297
7302
  ```
7298
7303
 
7299
7304
  Import it from `arcane-os/core/runtime`. The runtime exposes `current()`,
7300
- `subscribe()`, `onFrame()`, `emit()`, `registerService()`, `start()`, `handle()`
7305
+ `subscribe()`, `onFrame()`, `emit()`, `registerService()`, `getService()`, `start()`, `handle()`
7301
7306
  and idempotent `close()`.
7302
7307
 
7308
+ `await runtime.getService(name)` and service `context.getService(name)` return
7309
+ the actual registered service after its shared startup. They wait for that
7310
+ dependency alone, may start it before `runtime.start()`, and add no renderer or
7311
+ all-service barrier. Unknown services reject with `CORE_SERVICE_UNAVAILABLE`;
7312
+ new lookups during shutdown reject with `CORE_CLOSING`. Startup failures retain
7313
+ their original error. This is readiness, not a lifetime lease: retain acquired
7314
+ native handles for their actual cleanup rather than looking them up during
7315
+ disposal. See [native service composition](core-runtime.md#native-service-composition).
7316
+
7303
7317
  Services start independently. Request-lifetime work receives cooperative
7304
7318
  cancellation; accepted `lifetime:'service'` work survives renderer cancellation.
7305
7319
  Shutdown stops admission, waits accepted responses, then runs each service's
@@ -8011,7 +8025,7 @@ Construction selects no model and installs no runtime or transport.
8011
8025
  ### Signature and result
8012
8026
 
8013
8027
  ```text
8014
- createCoreLocalAIProvider({client=getInstalledCoreClient(),id='llama.cpp'}={})
8028
+ createCoreLocalAIProvider({client,id='llama.cpp'}={})
8015
8029
  ```
8016
8030
 
8017
8031
  Import it from `arcane-os/ai/core-local`. The provider implements
@@ -8023,6 +8037,16 @@ payloads and parsed streaming chunks, and cancel on observed readiness loss.
8023
8037
  Missing Core reports unavailability; it does not choose a browser or cloud route.
8024
8038
  Dispose/unregister at the caller's owning lifecycle boundary.
8025
8039
 
8040
+ Omitted `client` follows late installation, replacement and retirement through
8041
+ the shared Core installation subscription. An explicit client, including
8042
+ `null`, remains fixed. Replacement revokes readiness and cancels owned work;
8043
+ late results cannot restore it. `inspect()` distinguishes explicit load
8044
+ capability from loaded readiness, while `status()` remains observational.
8045
+ Disposal releases only an actually retained selection through its captured
8046
+ client, so never-loaded and already-unloaded providers send no native unload.
8047
+ The provider/2 runtime reconciles status at its operation boundaries; this
8048
+ does not add an idle role-state subscription.
8049
+
8026
8050
  ```javascript
8027
8051
  import {createCoreLocalAIProvider} from 'arcane-os/ai/core-local';
8028
8052
  import {getAIProviderRuntime} from 'arcane-os/ai-provider-runtime';
@@ -8055,6 +8079,13 @@ resolved installed runtime records. It returns a Core service definition with
8055
8079
  existing Core lifecycle. Shutdown cancels owned work and joins owned processes;
8056
8080
  an already running external service remains under its external owner.
8057
8081
 
8082
+ After `await context.getService('local-ai')`, `service.getONNXRuntime()` returns
8083
+ that service's actual prepared ONNX owner. It creates no engine and rejects
8084
+ unavailable or closing access. Reacquire on each explicit load after recovery;
8085
+ retain the returned handle for cleanup. Core shutdown cancels request-lifetime
8086
+ work, and independent services dispose concurrently. See
8087
+ [native service owners](local-ai.md#native-service-owners).
8088
+
8058
8089
  Optional `prepare({signal,onEvent})` supplies resolved runtime records during
8059
8090
  this service's startup. Only operations depending on that service wait for its
8060
8091
  preparation; the development listener and independently composed image/model
@@ -8064,7 +8095,10 @@ requirements. The managed development and native entrypoints separate them.
8064
8095
 
8065
8096
  The service provides `localai.status`, explicit `localai.services.recover`,
8066
8097
  `llama.status/models/load/unload/chat`, `onnx.status/load/run/unload`, and the documented native Ollama methods.
8067
- `localai.state` reports runtime/model changes. `llama.chunk` and `ollama.chunk`
8098
+ `localai.state` reports runtime/model changes. `llama.status` and `llama.models`
8099
+ observe current state without starting, restarting or waiting for an engine.
8100
+ Explicit load retains startup and exact catalog-membership checks.
8101
+ `llama.chunk` and `ollama.chunk`
8068
8102
  carry original parsed chunks with `streamId`; terminal responses remain complete.
8069
8103
  There is no background health polling or implicit model choice.
8070
8104
 
@@ -8081,7 +8115,7 @@ and [native bundling](local-ai.md#native-bundling).
8081
8115
 
8082
8116
  `createONNXRuntime({modulePath,signal?,onEvent?})` from `arcane-os/local-ai/onnx`
8083
8117
  owns retained native ONNX sessions in workers. It exposes `load({id,model,
8084
- sessionOptions?,signal?})`, `run({id,feeds,fetches?,runOptions?,signal?})`,
8118
+ sessionOptions?,executionPreference?,signal?})`, `run({id,feeds,fetches?,runOptions?,signal?})`,
8085
8119
  `unload({id,signal?})`, `current()`, replaying `subscribe(listener)` and `close()`.
8086
8120
  The direct model path is absolute. Complete typed tensor records use
8087
8121
  `{type,dims,data}`; callers own the actual graph's inputs and preprocessing.
@@ -8091,11 +8125,22 @@ before replacement. Independent sessions run concurrently; same-session work
8091
8125
  waits at that session's loading/execution boundary. Queued cancellation removes
8092
8126
  that request; active cancellation retires the affected worker and suppresses
8093
8127
  its response. Native code can delay termination; cleanup waits for actual exit.
8128
+ `current().sessions[].stopping` revokes new-run availability at terminal
8129
+ shutdown; `exited` records actual Worker exit. A recoverable run error leaves
8130
+ the loaded session usable. Unload joins exit and output delivery even when a
8131
+ termination request fails, preserving the complete cleanup error.
8132
+
8133
+ The generic load defaults to CPU. `executionPreference:'gpu'` tries advertised
8134
+ CUDA, TensorRT, DirectML, CoreML and WebGPU session creation before CPU.
8135
+ Explicit `sessionOptions.executionProviders` takes precedence unchanged.
8136
+ The complete `execution` record reports discovery, provider attempts and
8137
+ fallback; successful creation establishes configuration, not physical GPU
8138
+ execution. Inference is never replayed on a different provider after failure.
8094
8139
  See [complete signatures, readiness, cancellation and usage](local-ai.md#native-onnx-sessions).
8095
8140
 
8096
8141
  ## createCoreONNXRuntime()
8097
8142
 
8098
- `createCoreONNXRuntime({client=getInstalledCoreClient()}={})` from
8143
+ `createCoreONNXRuntime({client}={})` from
8099
8144
  `arcane-os/ai/core-onnx` accesses an available Core ONNX service. Its `load`,
8100
8145
  `run`, and `unload` mirror the native session selection and accept `signal` and
8101
8146
  `timeoutMs` (default `0`). Relative model paths resolve at Core's `appRoot`.
@@ -8103,7 +8148,11 @@ The accessor owns complete tensor transport encoding/decoding, `inspect()`,
8103
8148
  `current()`, replaying `subscribe(listener)` and `close()`. Closing cancels this
8104
8149
  accessor's requests and subscriptions; unloading shared models is explicit.
8105
8150
  Construction installs no runtime and opens no transport. Existing browser
8106
- ONNX/Wllama remain independent. See [Core and browser usage](local-ai.md#onnx-through-core-and-the-browser).
8151
+ ONNX/Wllama remain independent. Omitted `client` follows late installation and
8152
+ reads each installed connection's current status; explicit clients, including
8153
+ `null`, remain fixed. Retirement or replacement clears readiness and cancels
8154
+ owned work, with late old-client results suppressed. Close also removes the
8155
+ installation subscription. See [Core and browser usage](local-ai.md#onnx-through-core-and-the-browser).
8107
8156
 
8108
8157
  ## createImageRuntime()
8109
8158
 
@@ -8143,12 +8192,18 @@ browser accessor's detach-only ownership below. See the
8143
8192
 
8144
8193
  ## createCoreImageRuntime()
8145
8194
 
8146
- `createCoreImageRuntime({client=getInstalledCoreClient(),signal?,onEvent?})`
8195
+ `createCoreImageRuntime({client,signal?,onEvent?})`
8147
8196
  from `arcane-os/ai/core-image` returns immediately with `load`, `generate`, `edit`,
8148
8197
  `unload`, `inspect`, `current`, `subscribe` and `close`. It subscribes before
8149
8198
  requesting an initial status snapshot so newer lifecycle events win. It opens
8150
8199
  no connection and installs or selects no model.
8151
8200
 
8201
+ Omitted `client` follows late Core installation, replacement and retirement;
8202
+ an explicit client, including `null`, remains fixed. Each installed client is
8203
+ observed before its status request. Retirement cancels owned work and removes
8204
+ old readiness; late results cannot restore it. Close removes both service and
8205
+ installation subscriptions without closing the shared client or model.
8206
+
8152
8207
  `load({model,context?,assetProjectionId?,resourcePaths?,signal?})` and
8153
8208
  `generate({model,prompt,parameters?,assetProjectionId?,resourcePaths?,signal?,
8154
8209
  onProgress?})` use the selected Core service. Generation acknowledges with
@@ -8445,6 +8500,14 @@ directory and complete member files.
8445
8500
 
8446
8501
  `current()` returns `{closing,projections}`. `retain(id)` accepts a completed
8447
8502
  projection and returns `{id,directory,members,release}` for a native owner.
8503
+ Native `prepare({id,workingDirectory,members,signal,onProgress})` accepts
8504
+ ordered `{path,url}` members and returns their ready projection. Explicit calls
8505
+ stream complete upstream responses through the same file owner; startup
8506
+ downloads nothing. Independent members transfer concurrently and progress
8507
+ reports phases and completed files. `release(id)` relinquishes preparation
8508
+ ownership; cancellation, release and disposal join fetches and writes before
8509
+ cleanup. Native retain handles keep completed files alive until actual engine
8510
+ release. See [native preparation](model-assets.md#native-ownership).
8448
8511
  `dispose()` releases preparation ownership and joins file writes, closes and
8449
8512
  all native retain lifetimes before cleanup. The native owner releases its
8450
8513
  handle only after its actual context/worker use ends. Original stored model
@@ -9106,6 +9169,110 @@ See the [mounting contract](pwa.md#mountpwainstallprompt) and
9106
9169
  [component reference](runtime-components.md#pwa-installhtml). The browser owns
9107
9170
  native installation eligibility and URL-bar promotion.
9108
9171
 
9172
+ ## createNativeDecisionModel()
9173
+
9174
+ ### Overview
9175
+
9176
+ Runs explicitly selected Laya FP32 option scoring through an existing native
9177
+ ONNX owner, with tokenization in a Node Worker. It creates no second inference
9178
+ engine and starts no model download at construction.
9179
+
9180
+ ### Signature and result
9181
+
9182
+ ```text
9183
+ createNativeDecisionModel({onnx,modelAssets,workingDirectory,paths,model='onnx-community/laya-typed-decisions-ONNX',revision='main',dtype='fp32',sessionOptions,executionPreference='gpu',signal}={})
9184
+ ```
9185
+
9186
+ Import from `arcane-os/local-ai/decisions`. Supply the existing `onnx` owner and
9187
+ either `modelAssets` plus `workingDirectory`, or native `paths` for the model,
9188
+ tokenizer and tokenizer configuration. The selected graph requires FP32. The
9189
+ handle exposes `load`, `evaluate`, `classify`, `current`, `status`, `subscribe`,
9190
+ `unload` and terminal `dispose`; `classify` aliases `evaluate` and `status`
9191
+ aliases `current`.
9192
+
9193
+ `evaluate(rows,{signal,runOptions})` preserves each complete `state`, `question`,
9194
+ `options` and optional `type` separately. It returns `{decisions,outputs}` with
9195
+ original rows, typed values, raw logits, unrounded probabilities and every
9196
+ named tensor output. It requires a loading or loaded activation. Cancellation
9197
+ retires the shared activation and joins its actual ONNX/tokenizer lifetime;
9198
+ working files remain retained until both Workers exit. Ordinary invalid rows
9199
+ leave a healthy session and sibling evaluations intact.
9200
+
9201
+ ### Availability and normalization
9202
+
9203
+ **Node on Windows, Linux and macOS with the selected ONNX Runtime; Android needs
9204
+ host adaptation.** GPU preference attempts actual advertised providers before
9205
+ CPU fallback; accepted configuration does not prove GPU node execution. Model
9206
+ selection, unchanged payloads, scoring interpretation and persistence remain
9207
+ application-owned. Complete lifecycle, errors and paths are documented in
9208
+ [native typed decisions](native-decisions.md).
9209
+
9210
+ ### Example
9211
+
9212
+ ```javascript
9213
+ import {createNativeDecisionModel} from 'arcane-os/local-ai/decisions';
9214
+
9215
+ function createMoonChefDecisions({onnx, paths}) {
9216
+ return createNativeDecisionModel({onnx, paths});
9217
+ }
9218
+ ```
9219
+
9220
+ The caller explicitly loads the returned model before evaluation and unloads
9221
+ or disposes it at its owning boundary; defining this function downloads nothing.
9222
+
9223
+ ## createNativeDecisionService()
9224
+
9225
+ ### Overview
9226
+
9227
+ Composes the same native decision model into an existing Core runtime without
9228
+ a renderer. The factory starts no inference, tokenizer or model preparation.
9229
+
9230
+ ### Signature and result
9231
+
9232
+ ```text
9233
+ createNativeDecisionService(configuration={}, {appRoot=process.cwd()}={})
9234
+ ```
9235
+
9236
+ Import from `arcane-os/core/decisions`. Configuration selects the model options
9237
+ above and an optional service `name` (default `decisions`); relative supplied
9238
+ `paths` resolve from `appRoot`. Explicit load obtains the registered local-AI
9239
+ and, when needed, model-assets owners through `context.getService`, then
9240
+ reacquires `getONNXRuntime()` after any recovery. Native members are `load`,
9241
+ `evaluate`, `classify`, `current`, `subscribe`, `unload`, `dispose` and Core
9242
+ `start(context)`.
9243
+
9244
+ RPC exposes `decisions.status`, `decisions.load`, `decisions.evaluate` and
9245
+ `decisions.unload`. Evaluation accepts `{rows,runOptions?}` and returns complete
9246
+ `{decisions,outputs}` using the existing tensor codec at the JSON boundary.
9247
+ `decisions.state` and replaying `subscribe(listener,{emitCurrent:true,signal})`
9248
+ report lifecycle. Unload is accepted service-lifetime cleanup; host shutdown
9249
+ still cancels request-lifetime load/evaluation.
9250
+
9251
+ ### Availability and normalization
9252
+
9253
+ **Node Core with an existing local-AI ONNX service and selected model files.**
9254
+ Registration does not wait for all services or open another host. Original
9255
+ inputs, complete outputs, failures and cleanup semantics match the native
9256
+ model. See [composition and example](native-decisions.md#compose-with-existing-core-services).
9257
+
9258
+ ### Example
9259
+
9260
+ ```javascript
9261
+ import {createNativeDecisionService} from 'arcane-os/core/decisions';
9262
+
9263
+ const decisions = createNativeDecisionService({
9264
+ model: 'onnx-community/laya-typed-decisions-ONNX',
9265
+ dtype: 'fp32',
9266
+ workingDirectory: '.models/laya'
9267
+ });
9268
+ // Add decisions to the application's existing Core services; load explicitly.
9269
+ ```
9270
+
9271
+ ## createNativeDecisionService default export
9272
+
9273
+ The default export from `arcane-os/core/decisions` is exactly the named
9274
+ `createNativeDecisionService` factory, with the same arguments and lifecycle.
9275
+
9109
9276
  ## createBrowserDecisionModel()
9110
9277
 
9111
9278
  ### Overview
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.67.0",
3
+ "version": "0.68.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",
@@ -1,11 +1,62 @@
1
1
  <style>
2
- :host{display:inline-block;color:inherit}
3
- .switcher{align-items:center;display:flex;gap:.15rem;padding:.2rem;border:1px solid var(--app-bar-border,var(--border-color,rgba(255,255,255,.25)));border-radius:999px;background:color-mix(in srgb,currentColor 8%,transparent)}
4
- button{appearance:none;border:0;border-radius:999px;background:transparent;color:inherit;cursor:pointer;font:700 .68rem/1 system-ui,sans-serif;min-height:1.85rem;padding:.42rem .58rem}
5
- button:hover{background:color-mix(in srgb,currentColor 14%,transparent)}
6
- button[aria-pressed="true"]{background:var(--app-bar-accent,var(--secondary-color,#c49a52));color:var(--app-bar-active-text,var(--primary-color,#172238))}
7
- button:focus-visible{outline:.18rem solid var(--focus-color,currentColor);outline-offset:.12rem}
8
- @media(max-width:32rem){button{font-size:0;padding-inline:.5rem}button::first-letter{font-size:.72rem}}
2
+ :host {
3
+ display:inline-block;
4
+ min-inline-size:0;
5
+ max-inline-size:100%;
6
+ color:inherit;
7
+ }
8
+
9
+ .switcher {
10
+ display:flex;
11
+ flex-wrap:wrap;
12
+ align-items:center;
13
+ gap:.15rem;
14
+ box-sizing:border-box;
15
+ max-inline-size:100%;
16
+ padding:.2rem;
17
+ border:1px solid var(--app-bar-border,var(--border-color,rgba(255,255,255,.25)));
18
+ border-radius:1.6rem;
19
+ background:color-mix(in srgb,currentColor 8%,transparent);
20
+ }
21
+
22
+ button {
23
+ appearance:none;
24
+ flex:1 0 auto;
25
+ box-sizing:border-box;
26
+ min-inline-size:2.75rem;
27
+ max-inline-size:100%;
28
+ min-block-size:2.75rem;
29
+ padding:.5rem .75rem;
30
+ border:0;
31
+ border-radius:1.4rem;
32
+ background:transparent;
33
+ color:inherit;
34
+ cursor:pointer;
35
+ font:700 .875rem/1.25 system-ui,sans-serif;
36
+ text-align:center;
37
+ white-space:normal;
38
+ overflow-wrap:anywhere;
39
+ }
40
+
41
+ button:hover:not(:disabled) {
42
+ background:color-mix(in srgb,currentColor 14%,transparent);
43
+ }
44
+
45
+ button[aria-pressed="true"] {
46
+ background:var(--app-bar-accent,var(--secondary-color,rgb(196,154,82)));
47
+ color:var(--app-bar-active-text,var(--primary-color,rgb(23,34,56)));
48
+ box-shadow:inset 0 0 0 .1rem currentColor;
49
+ }
50
+
51
+ button:disabled {
52
+ cursor:not-allowed;
53
+ opacity:.55;
54
+ }
55
+
56
+ button:focus-visible {
57
+ outline:.18rem solid var(--focus-color,currentColor);
58
+ outline-offset:.12rem;
59
+ }
9
60
  </style>
10
61
  <div class="switcher" role="group" aria-label="Color scheme">
11
62
  <button type="button" data-scheme="system" aria-pressed="false" title="Use device theme">Auto</button>
@@ -16,31 +67,79 @@
16
67
  <script type="module">
17
68
  const host=this;
18
69
  const root=host.shadowRoot;
70
+ const group=root.querySelector('.switcher');
19
71
  const buttons=Array.from(root.querySelectorAll('[data-scheme]'));
72
+ let modes=buttons.map(function scheme(button){return button.dataset.scheme;});
73
+ let labels=Object.fromEntries(buttons.map(function label(button){
74
+ return [button.dataset.scheme,button.textContent];
75
+ }));
76
+ let busy=false;
77
+
78
+ host.configure=configure;
79
+ configure({
80
+ modes:host.dataset.modes===undefined?modes:host.dataset.modes.split(/\s+/u).filter(Boolean),
81
+ labels:host.dataset.systemLabel===undefined?{}:{system:host.dataset.systemLabel}
82
+ });
83
+
20
84
  const {default:ThemeManager}=await import('../modules/ThemeManager.js');
21
85
  const manager=new ThemeManager();
22
86
 
23
87
  host.setMode=setMode;
24
88
  host.refresh=refresh;
25
- root.querySelector('.switcher').addEventListener('click',async event=>{
89
+ group.addEventListener('click',async function selectScheme(event){
26
90
  const button=event.target.closest('[data-scheme]');
27
- if(!button) return;
91
+ if(!button||button.disabled) return;
28
92
  setBusy(true);
29
93
  try{
30
94
  const state=button.dataset.scheme==='custom'?await manager.activateCustom():await manager.setScheme(button.dataset.scheme);
31
95
  setMode(state.mode);setCustomAvailable(Boolean(state.theme));
32
96
  }finally{setBusy(false);setCustomAvailable(Boolean(manager.customTheme));}
33
97
  });
34
- globalThis.addEventListener('arcane-theme-change',()=>refresh().catch(()=>{}));
98
+ globalThis.addEventListener('arcane-theme-change',function themeChanged(){
99
+ refresh().catch(function refreshRejected(){});
100
+ });
101
+
102
+ function configure(options={}){
103
+ const nextModes=options.modes===undefined?modes:[...options.modes];
104
+ const nextLabels={...labels,...options.labels};
105
+ const visible=[];
106
+ for(const mode of nextModes){
107
+ const button=buttons.find(function matchingScheme(candidate){return candidate.dataset.scheme===mode;});
108
+ if(!button) throw new TypeError(`Unknown theme-switcher mode: ${mode}`);
109
+ if(visible.includes(button)) throw new TypeError(`Repeated theme-switcher mode: ${mode}`);
110
+ visible.push(button);
111
+ }
112
+ for(const button of buttons){
113
+ const label=nextLabels[button.dataset.scheme];
114
+ if(typeof label!=='string') throw new TypeError('Theme-switcher labels must be strings.');
115
+ }
116
+ modes=nextModes;
117
+ labels=nextLabels;
118
+ for(const button of buttons) button.textContent=labels[button.dataset.scheme];
119
+ const reordered=group.children.length!==visible.length||visible.some(function moved(button,index){
120
+ return group.children[index]!==button;
121
+ });
122
+ if(reordered){
123
+ const focused=root.activeElement;
124
+ group.replaceChildren(...visible);
125
+ if(visible.includes(focused)&&!focused.disabled) focused.focus({preventScroll:true});
126
+ }
127
+ return {modes:[...modes],labels:{...labels}};
128
+ }
35
129
 
36
130
  function setMode(mode='system'){
37
131
  for(const button of buttons) button.setAttribute('aria-pressed',String(button.dataset.scheme===mode));
38
132
  host.dataset.mode=mode;
39
133
  return mode;
40
134
  }
41
- function setBusy(value){for(const button of buttons) button.disabled=Boolean(value);}
42
- function setCustomAvailable(value){root.querySelector('[data-scheme="custom"]').disabled=!value;}
135
+ function setBusy(value){
136
+ busy=Boolean(value);
137
+ for(const button of buttons) button.disabled=busy;
138
+ }
139
+ function setCustomAvailable(value){
140
+ buttons.find(function customScheme(button){return button.dataset.scheme==='custom';}).disabled=busy||!value;
141
+ }
43
142
  async function refresh(){const state=await manager.load();setMode(state.mode);setCustomAvailable(Boolean(state.theme));return state;}
44
143
 
45
- refresh().catch(()=>setMode('system'));
144
+ refresh().catch(function refreshRejected(){setMode('system');});
46
145
  </script>
@@ -153,21 +153,26 @@ function nativeAdapter(){
153
153
  return preferences;
154
154
  }
155
155
 
156
- function isUnsupportedNativeAdapter(error){
157
- return error?.code==='ANDROID_CAPABILITY_UNSUPPORTED';
156
+ function isUnsupportedNativeAdapter(error,method){
157
+ return error?.code==='ANDROID_CAPABILITY_UNSUPPORTED'
158
+ ||(error?.code==='METHOD_NOT_ALLOWED'
159
+ &&error.reason==='core-namespace-unavailable'
160
+ &&error.namespace==='preferences'
161
+ &&error.method===`preferences.${method}`);
158
162
  }
159
163
 
160
- function preferenceAdapter(){
164
+ function preferenceAdapter(assertActive){
161
165
  const local=localAdapter('arcane.preferences');
162
166
  if(is.function(globalThis.arcaneAndroid?.postMessage)) return local;
163
167
  const native=nativeAdapter();
164
168
  if(!native) return local;
165
169
  let active=native;
166
- async function call(method,args){
170
+ async function call(method,args,context){
167
171
  try{
168
172
  return await active[method](...args);
169
173
  }catch(error){
170
- if(active!==native||!isUnsupportedNativeAdapter(error)) throw error;
174
+ if(active!==native||!isUnsupportedNativeAdapter(error,method)) throw error;
175
+ assertActive(context?.signal);
171
176
  active=local;
172
177
  delete adapter.setMany;
173
178
  if(!is.function(active[method])) throw error;
@@ -175,19 +180,19 @@ function preferenceAdapter(){
175
180
  }
176
181
  }
177
182
  const adapter={
178
- async get(key){
179
- return call('get',[key]);
183
+ async get(key,context){
184
+ return call('get',[key,context],context);
180
185
  },
181
- async set(key,value){
182
- return call('set',[key,value]);
186
+ async set(key,value,context){
187
+ return call('set',[key,value,context],context);
183
188
  },
184
- async delete(key){
185
- return call('delete',[key]);
189
+ async delete(key,context){
190
+ return call('delete',[key,context],context);
186
191
  }
187
192
  };
188
193
  if(is.function(native.setMany)){
189
194
  adapter.setMany=async function setMany(entries,context){
190
- return call('setMany',[entries,context]);
195
+ return call('setMany',[entries,context],context);
191
196
  };
192
197
  }
193
198
  return adapter;
@@ -204,7 +209,7 @@ export default class PreferenceStore extends EventTarget{
204
209
  super();
205
210
  this.namespace=String(namespace||'arcane');
206
211
  this.schema=preferenceSchema(schema);
207
- this.adapter=validateAdapter(adapter||preferenceAdapter());
212
+ this.adapter=validateAdapter(adapter||preferenceAdapter(this.#assertOperationActive.bind(this)));
208
213
  this.values=this.defaults();
209
214
  this.#events=createArcaneEventSource(
210
215
  this,
@@ -8,16 +8,34 @@ export default class SystemAppearance{
8
8
 
9
9
  async current(){
10
10
  if(!this.api||!is.function(this.api.current)) return {supported:false,platform:'browser'};
11
- return this.api.current();
11
+ try{
12
+ return await this.api.current();
13
+ }catch(error){
14
+ return unsupportedNamespace(error,'current');
15
+ }
12
16
  }
13
17
 
14
18
  async apply(input={}){
15
19
  if(!this.available()) return {supported:false,platform:'browser'};
16
20
  const scheme=['system','light','dark'].includes(input.scheme)?input.scheme:'system';
17
- return this.api.apply({
18
- scheme,
19
- captionColor:scheme==='system'?null:input.captionColor||null,
20
- textColor:scheme==='system'?null:input.textColor||null
21
- });
21
+ try{
22
+ return await this.api.apply({
23
+ scheme,
24
+ captionColor:scheme==='system'?null:input.captionColor||null,
25
+ textColor:scheme==='system'?null:input.textColor||null
26
+ });
27
+ }catch(error){
28
+ return unsupportedNamespace(error,'apply');
29
+ }
22
30
  }
23
31
  }
32
+
33
+ function unsupportedNamespace(error,method){
34
+ if(error?.code==='METHOD_NOT_ALLOWED'
35
+ &&error.reason==='core-namespace-unavailable'
36
+ &&error.namespace==='appearance'
37
+ &&error.method===`appearance.${method}`){
38
+ return {supported:false,reason:'core-namespace-unavailable',error};
39
+ }
40
+ throw error;
41
+ }
@@ -267,7 +267,21 @@ class CoreRuntime {
267
267
 
268
268
  #request(frame) {
269
269
  const registration = this.#methods.get(frame.method);
270
- if (!registration) throw failure('METHOD_NOT_ALLOWED', `Core does not expose ${frame.method}.`);
270
+ if (!registration) {
271
+ const namespace = frame.method.split('.')[0];
272
+ const prefix = `${namespace}.`;
273
+ const namespacePresent = this.#services.has(namespace)
274
+ || [...this.#methods.keys(), 'system.ping', 'app.current', 'version.current'].some(
275
+ function inNamespace(method) { return method.startsWith(prefix); }
276
+ );
277
+ throw new CoreError({
278
+ code: 'METHOD_NOT_ALLOWED',
279
+ message: `Core does not expose ${frame.method}.`,
280
+ reason: namespacePresent ? 'core-method-unavailable' : 'core-namespace-unavailable',
281
+ namespace,
282
+ method: frame.method
283
+ });
284
+ }
271
285
  const {service, handle, lifetime} = registration;
272
286
  const controller = new AbortController();
273
287
  const request = {id: frame.id, method: frame.method, lifetime, controller, task: null};