arcane-os 0.86.1 → 0.88.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,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.88.0
4
+
5
+ - Add optional `signal` and `startLanguageModel` controls to `AI.setAI`,
6
+ `configureProviders`, `transitionAI`, and `transitionProviders`, preserving
7
+ their existing arguments, return values, and eager default. Explicitly
8
+ deferred selections stay unloaded through later readiness notifications;
9
+ an explicit runtime load or startup can activate them.
10
+ - Keep cancellation and supersession with the owning selection. Accepted
11
+ cleanup settles before a cancelled transition can configure or preload a
12
+ replacement, and stale continuations cannot alter a newer selection.
13
+ Completed activation is independent of a later caller abort.
14
+ - Preserve truthful busy, loaded `unloading` state while an accepted request
15
+ finishes cancellation; new inference remains unavailable during cleanup.
16
+ - Preserve keyboard focus in the shared theme switcher during asynchronous
17
+ selection. Pending choices remain focusable and expose `aria-disabled` while
18
+ repeated activation is ignored; completion never overrides an intentional
19
+ focus move. Unavailable Skin choices remain natively disabled.
20
+
21
+ ## 0.87.0
22
+
23
+ - Add `createDocumentAcquisitionService` through
24
+ `arcane-os/core/document-acquisition` and `acquireCoreDocument` through
25
+ `arcane-os/document-acquisition`. Applications can acquire selected HTTP(S)
26
+ documents through their existing native Core without depending on browser
27
+ cross-origin access.
28
+ - Preserve complete response bodies, requested and final URLs, response
29
+ metadata and ordered redirect evidence. An optional application-owned
30
+ destination predicate runs before each native request; domain selection and
31
+ document interpretation remain application policy.
32
+ - Correlate semantic progress and cancellation with the existing Core request,
33
+ preserve complete error evidence, and return browser bodies as `Blob`
34
+ objects. Native service disposal cancels and joins its owned operations.
35
+ No dependency or compiled native host change is required.
36
+
3
37
  ## 0.86.1
4
38
 
5
39
  - Include the existing public `arcane-os/ai/model-observation` export in
package/README.md CHANGED
@@ -25,7 +25,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
25
25
  share the theme, packaging, event, cancellation,
26
26
  and browser run contracts while retaining their selected application layout.
27
27
 
28
- This checkout defines the `0.86.1` SDK contract. Development applications declare
28
+ This checkout defines the `0.88.0` SDK contract. Development applications declare
29
29
  `arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
30
30
  designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
31
31
  to refresh and compare the installed SDK and lockfile with the published channel;
@@ -19,6 +19,7 @@ export const CORE_METHOD_CONTRACTS={
19
19
  'capabilities.list':{input:'empty-object',output:'{app,grants,methods}'},
20
20
  'platform.status':{input:'empty-object-v1',output:'platform-status-v1'},
21
21
  'network.status':{input:'empty-object-v1',output:'network-status-v1',meaning:'A non-loopback interface is present; this is not proof of Internet reachability.'},
22
+ 'documents.acquire':{input:'{url}',output:'document-acquisition-result-v1',meaning:'Complete HTTP(S) GET response and redirects; native application owns any optional destination predicate. Request-scoped documents.progress events.'},
22
23
  'apps.list':{input:'empty-object-v1',output:'application-catalog-v1'},
23
24
  'apps.launch':{input:'application-launch-v1',output:'application-launch-result-v1',meaning:'Host acceptance, not application readiness.'},
24
25
  'external.open':{input:'external-open-v1',output:'external-open-result-v1',meaning:'Host acceptance, not confirmation of a user action.'},
@@ -0,0 +1,112 @@
1
+ import Is from './dependencies/strong-type/index.js';
2
+ import {getInstalledCoreClient} from './core/client.mjs';
3
+ import {CoreError, serializeCoreError} from './core/contracts.mjs';
4
+
5
+ const is = new Is(false);
6
+
7
+ /** Acquires one complete document through the application's installed native service. */
8
+ export async function acquireCoreDocument({url, client = getInstalledCoreClient(), signal, onProgress} = {}) {
9
+ if (!is.function(client?.invoke) || !is.function(client?.events?.on)) {
10
+ throw new CoreError({
11
+ code: 'DOCUMENT_ACQUISITION_CORE_UNAVAILABLE',
12
+ message: 'Document acquisition requires an available native Core connection.'
13
+ });
14
+ }
15
+ if (!is.string(url) || url === '') {
16
+ throw new TypeError('Document acquisition url must be a nonempty string.');
17
+ }
18
+ if (onProgress !== undefined && !is.function(onProgress)) {
19
+ throw new TypeError('Document acquisition onProgress must be a function.');
20
+ }
21
+ const controller = new AbortController();
22
+ const operationSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
23
+ operationSignal.throwIfAborted();
24
+ const observers = new Set();
25
+ let requestId;
26
+ let active = true;
27
+ const observerErrors = [];
28
+
29
+ function retainObserverFailure(error) {
30
+ observerErrors.push(error);
31
+ controller.abort(error);
32
+ }
33
+
34
+ const unsubscribe = client.events.on('documents.progress', function documentProgress(progress) {
35
+ if (!active || operationSignal.aborted || !requestId || progress.requestId !== requestId || !onProgress) return;
36
+ let observation;
37
+ try { observation = Promise.resolve(onProgress(progress)); }
38
+ catch (error) { retainObserverFailure(error); return; }
39
+ observers.add(observation);
40
+ function observed() { observers.delete(observation); }
41
+ observation.then(observed, function failed(error) {
42
+ observed();
43
+ retainObserverFailure(error);
44
+ });
45
+ });
46
+
47
+ let result;
48
+ let failed = false;
49
+ let failure;
50
+ try {
51
+ result = await client.invoke('documents.acquire', {url}, {
52
+ signal: operationSignal, timeoutMs: 0,
53
+ onRequest(request) { requestId = request.requestId; }
54
+ });
55
+ } catch (error) {
56
+ failed = true;
57
+ failure = error;
58
+ } finally {
59
+ active = false;
60
+ try { unsubscribe(); } catch (error) { observerErrors.push(error); }
61
+ await Promise.allSettled([...observers]);
62
+ }
63
+ if (failed) failure = decodeFailure(failure);
64
+ let document;
65
+ if (!failed) {
66
+ const {redirects, ...response} = result;
67
+ document = {...decodeResponse(response), redirects: redirects.map(decodeResponse)};
68
+ }
69
+ if (observerErrors.length) {
70
+ const error = !failed && observerErrors.length === 1
71
+ ? new CoreError({...serializeCoreError(observerErrors[0]), cause: observerErrors[0]})
72
+ : new AggregateError(
73
+ failed ? [failure, ...observerErrors] : observerErrors,
74
+ 'Document acquisition progress observers failed.',
75
+ failed ? {cause: failure} : undefined
76
+ );
77
+ if (document) {
78
+ const {requestedUrl, finalUrl, redirects, ...response} = document;
79
+ error.documentAcquisition = {requestedUrl, requestUrl: finalUrl, redirects, response};
80
+ } else if (failure?.documentAcquisition) {
81
+ error.documentAcquisition = failure.documentAcquisition;
82
+ }
83
+ throw error;
84
+ }
85
+ if (failed) throw failure;
86
+ operationSignal.throwIfAborted();
87
+ return document;
88
+ }
89
+
90
+ function decodeFailure(error) {
91
+ if (!error?.documentAcquisition) return error;
92
+ const {response, redirects, ...metadata} = error.documentAcquisition;
93
+ return new CoreError({
94
+ ...serializeCoreError(error),
95
+ documentAcquisition: {
96
+ ...metadata,
97
+ redirects: redirects.map(decodeResponse),
98
+ ...(response ? {response: decodeResponse(response)} : {})
99
+ }
100
+ });
101
+ }
102
+
103
+ function decodeResponse(response) {
104
+ const {body, ...metadata} = response;
105
+ if (body.encoding !== 'base64') {
106
+ throw new TypeError(`Unsupported document transport encoding ${body.encoding}.`);
107
+ }
108
+ const content = Uint8Array.from(globalThis.atob(body.data), function decodeCharacter(character) {
109
+ return character.charCodeAt(0);
110
+ });
111
+ return {...metadata, body: new Blob([content], {type: metadata.mediaType ?? ''})};
112
+ }
@@ -71,6 +71,7 @@ alone does not make bare module names resolve in a browser.
71
71
  | Expose application-owned tools and static resources through Node STDIO | [MCP STDIO server](mcp-stdio.md) |
72
72
  | Run application-selected llama.cpp, Ollama or ONNX through Core | [Local AI through Core](local-ai.md) |
73
73
  | Observe existing renderer and Core model state, progress and failures | [Existing model observation](model-observation.md) |
74
+ | Acquire complete HTTP(S) documents and redirect responses through an application-selected native Core service | [Native document acquisition](document-acquisition.md) |
74
75
  | Generate complete local PNG images with a retained native model | [Local image generation](local-image-generation.md) |
75
76
  | Select FLUX.2 Klein 4B and its three complementary model resources | [FLUX.2 Klein 4B through Core](local-image-flux.md) |
76
77
  | Select the complete SDXL Base 1.0 checkpoint for generation and whole-image editing | [SDXL Base 1.0 through Core](local-image-sdxl.md) |
@@ -0,0 +1,159 @@
1
+ # Native document acquisition
2
+
3
+ `acquireCoreDocument` from `arcane-os/document-acquisition` retrieves one
4
+ HTTP(S) document through the application's selected native Core service.
5
+ `createDocumentAcquisitionService` from `arcane-os/core/document-acquisition`
6
+ owns the reusable native acquisition. It performs a GET, follows redirects
7
+ manually, and retains the complete response bodies without parsing or rewriting
8
+ the document. Applications own source selection, document interpretation,
9
+ enumeration, storage, and any destination predicate.
10
+
11
+ ## Application composition
12
+
13
+ Select an application-owned native factory through the existing
14
+ [`native.services` composition](core-native-packaging.md#explicit-application-services).
15
+ Include its source through the application's `package.nativeResources` as
16
+ described there. The factory runs in the native Core process; its options are
17
+ JSON data, while its predicate is an ordinary locally imported function.
18
+ Functions are never serialized through Core RPC.
19
+
20
+ ```js
21
+ // native/documents.mjs, owned by the application
22
+ import {createDocumentAcquisitionService} from 'arcane-os/core/document-acquisition';
23
+
24
+ export default function createDocuments() {
25
+ return createDocumentAcquisitionService();
26
+ }
27
+ ```
28
+
29
+ ```json
30
+ {
31
+ "native": {
32
+ "services": [{"module": "native/documents.mjs"}]
33
+ }
34
+ }
35
+ ```
36
+
37
+ The ordinary service works without a predicate. When an application selects a
38
+ destination restriction, its native factory supplies
39
+ `createDocumentAcquisitionService({destinationPredicate})`. The SDK awaits
40
+ `destinationPredicate(destinationUrl, {requestedUrl, previousUrl, signal})`
41
+ before **every** HTTP(S) request, including the first URL and each redirect.
42
+ Return `true` to request that destination. Any other return value declines it
43
+ with `DOCUMENT_DESTINATION_DECLINED`. A thrown or rejected error remains the
44
+ acquisition error's original `cause`. The predicate receives the resolved URL
45
+ string, the caller's original URL string, the prior response URL or `null`, and
46
+ the operation's signal. It stays local to the application's native process.
47
+
48
+ An asynchronous predicate must finish or honor its signal so native shutdown
49
+ can join it. There is no automatic retry or redirect-count limit. The caller
50
+ can cancel its operation, including a repeating redirect chain. Redirects
51
+ with statuses 301, 302, 303, 307, and 308 follow their `Location`, resolved
52
+ against the response URL. A response without a redirect destination is returned
53
+ as received. The service performs GET on each destination; it does not submit
54
+ forms or add application credentials.
55
+
56
+ ## Browser API
57
+
58
+ ```js
59
+ import {acquireCoreDocument} from 'arcane-os/document-acquisition';
60
+
61
+ const cancellation = new AbortController();
62
+ const document = await acquireCoreDocument({
63
+ url: selectedDocumentUrl,
64
+ signal: cancellation.signal,
65
+ onProgress(progress) {
66
+ status.textContent = progress.phase;
67
+ }
68
+ });
69
+
70
+ // The original entity content is available even for an HTTP error response.
71
+ const completeContent = document.body; // Blob
72
+ const receivedSuccessfully = document.ok;
73
+ ```
74
+
75
+ The options are `{url, client?, signal?, onProgress?}`. `client` defaults to the
76
+ installed Core client. The helper invokes `documents.acquire` with `{url}`;
77
+ callbacks and the signal stay local. It uses existing request cancellation and
78
+ `documents.progress` subscriptions, with no extra event bus or transport.
79
+ There is no implicit request timeout. The caller owns any selected deadline
80
+ through its signal.
81
+
82
+ The native service must be selected by the application. An absent Core client
83
+ reports `DOCUMENT_ACQUISITION_CORE_UNAVAILABLE`; an installed Core without the
84
+ service returns its existing unavailable-method error. Browser-only preview
85
+ does not acquire native capability and does not silently switch to browser
86
+ Fetch. Portable Windows, Linux, and macOS applications use their existing Core
87
+ transport; Android requires its host to provide this same native service.
88
+
89
+ The result contains:
90
+
91
+ | Field | Meaning |
92
+ | --- | --- |
93
+ | `requestedUrl` | Caller-supplied URL string, retained unchanged. |
94
+ | `finalUrl` / `url` | Final response URL reported by Fetch. |
95
+ | `status`, `statusText`, `ok` | Actual Fetch response status fields. `ok:false` is returned with its body, not replaced by a generic error. |
96
+ | `headers` | All entries exposed by the Fetch `Headers` object, represented as `[name, value]` pairs. |
97
+ | `mediaType` | Complete `Content-Type` field value exposed by Fetch, or `null`. |
98
+ | `body` | Complete entity content as a `Blob` in the browser and a `Uint8Array` in native calls. |
99
+ | `complete` | `true` after the response body reaches its end. |
100
+ | `redirects` | Ordered preceding response records, each with `url`, status fields, headers, media type, body, and completion state. |
101
+
102
+ The content is the response entity supplied by Fetch, including Fetch's normal
103
+ content-decoding behavior. It is not a capture of compressed network framing.
104
+ Likewise, headers reflect Fetch's casing and combination behavior; the SDK does
105
+ not invent original wire casing or unavailable header distinctions. Document
106
+ content is never converted to text, normalized, summarized, or wrapped in
107
+ instructions. Query and progress metadata remain outside the content.
108
+
109
+ ## Progress, cancellation, and errors
110
+
111
+ Progress records contain `phase`, `completed`, `total:1`, `unit:'documents'`,
112
+ `requestedUrl`, and the current `url`. Browser records additionally contain
113
+ the originating `requestId`. Phases are `accepted`, `destination`, `request`,
114
+ `response`, `redirect`, and `complete`. The completed count becomes one only
115
+ after the final response has been fully acquired, including an HTTP error
116
+ response. Status fields, not acquisition progress, establish HTTP success.
117
+ Progress does not measure content and carries no document body.
118
+
119
+ The browser helper filters request-correlated events and removes its listener
120
+ on every settlement. Synchronous or asynchronous progress callbacks are
121
+ observed; all accepted callback promises settle before the helper settles.
122
+ A callback failure cancels the request and remains observable. Simultaneous
123
+ operation and callback failures are retained in an `AggregateError`.
124
+ If a callback or subscription cleanup fails after the complete response has
125
+ arrived, that response remains in the error's `documentAcquisition` evidence;
126
+ the original callback failure remains its `cause` or an aggregated error.
127
+ Callbacks must finish or honor the application's cancellation signal.
128
+
129
+ Native acquisition errors retain the original thrown value as `cause` and
130
+ include `documentAcquisition: {requestedUrl, requestUrl, redirects, response?}`.
131
+ Every acquired redirect body is retained. If reading a response fails, its
132
+ available original content is retained with `complete:false`; that record is
133
+ explicitly incomplete. Cleanup failures remain observable alongside the
134
+ original error. On errors delivered through Core, the browser helper restores
135
+ the response and redirect bodies as Blobs and retains the native error record.
136
+
137
+ An abort uses the existing Core client's immediate local rejection and sends
138
+ request cancellation to the native owner. That local cancellation does not
139
+ wait for a native error record or return its response evidence. The native
140
+ owner cancels and joins its active response reader. Independent acquisitions
141
+ remain concurrent, and cancelling one request leaves sibling requests active.
142
+ Service `dispose()` aborts and joins all of that service's active operations.
143
+
144
+ ## Native calls and transport
145
+
146
+ The synchronous factory accepts `{destinationPredicate?, signal?}` and returns
147
+ the existing Core service definition plus
148
+ `acquire({url, signal?, onProgress?})` and idempotent asynchronous `dispose()`.
149
+ Direct native results and error evidence contain `Uint8Array` bodies (Node
150
+ Buffers implement that interface). The optional factory signal controls the
151
+ service lifetime; a per-call signal controls only that acquisition.
152
+
153
+ At the JSON RPC boundary only, each body becomes
154
+ `{encoding:'base64', data:'...'}`. One final response carries the complete result
155
+ and ordered redirect records. The browser decodes those transport fields into
156
+ Blobs. This path buffers complete response bodies; phase events are not a
157
+ body stream and do not supply streaming backpressure. Existing Core lifecycle,
158
+ request routing, and connection-close cancellation retain ownership of the
159
+ operation through native settlement.
@@ -1,17 +1,45 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "sdkVersion": "0.86.0",
3
+ "sdkVersion": "0.87.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": 306,
9
+ "memberCount": 308,
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": "core-document-acquisition:createDocumentAcquisitionService",
17
+ "name": "createDocumentAcquisitionService",
18
+ "displayName": "createDocumentAcquisitionService()",
19
+ "kind": "function",
20
+ "signature": "createDocumentAcquisitionService({destinationPredicate,signal}={})",
21
+ "entrypoints": ["arcane-os/core/document-acquisition"],
22
+ "primaryImport": "arcane-os/core/document-acquisition",
23
+ "group": "Native document acquisition",
24
+ "summary": "Acquires complete HTTP(S) documents in the native Core process with application-owned destination selection before every request.",
25
+ "availability": "Node Core on Windows, Linux and macOS; Android requires the same service through its host adaptation",
26
+ "protocol": "arcane/1 documents.acquire and request-correlated documents.progress; selected through the existing application-native service factory contract",
27
+ "normalization": "Returns name:documents, methods, acquire({url,signal,onProgress}) and idempotent asynchronous dispose(). GET follows 301/302/303/307/308 manually, awaiting the optional native destinationPredicate(url,{requestedUrl,previousUrl,signal}) before every request; only true proceeds. Omission leaves ordinary acquisition functional. Results retain requestedUrl, finalUrl/url, actual Fetch status/statusText/ok, all exposed header entries, mediaType, complete Uint8Array body and ordered complete redirect records, including non-2xx bodies. Fetch entity decoding is distinct from compressed wire framing. Errors retain their cause and complete available response evidence, with interrupted bodies marked complete:false. Independent jobs remain concurrent; cancellation stops the selected job and dispose aborts and joins all owned work. Progress is phase/document-count information, never content measurement. Base64 is used only at JSON transport; no parsing, text transformation, storage, automatic retry or application domain policy. See document-acquisition.md for composition, fields and lifecycle."
28
+ },
29
+ {
30
+ "id": "document-acquisition:acquireCoreDocument",
31
+ "name": "acquireCoreDocument",
32
+ "displayName": "acquireCoreDocument()",
33
+ "kind": "function",
34
+ "signature": "async acquireCoreDocument({url,client=getInstalledCoreClient(),signal,onProgress}={})",
35
+ "entrypoints": ["arcane-os/document-acquisition"],
36
+ "primaryImport": "arcane-os/document-acquisition",
37
+ "group": "Native document acquisition",
38
+ "summary": "Retrieves a complete selected document as a Blob through the application's existing native Core connection.",
39
+ "availability": "JavaScript with an existing Core client and explicitly composed documents service; no browser-only Fetch fallback",
40
+ "protocol": "documents.acquire({url}) with existing cancellation and correlated documents.progress subscriptions",
41
+ "normalization": "Preserves complete native response metadata and ordered redirect evidence, decoding only transport base64 into Blobs. The installed Core client is the default; unavailable Core reports DOCUMENT_ACQUISITION_CORE_UNAVAILABLE and a missing service uses the existing Core method error. The native application owns the optional destination predicate; no function crosses RPC. onProgress receives complete phase/document-count records plus requestId; accepted synchronous/asynchronous callbacks are observed and joined on settlement, listener cleanup always runs, and simultaneous operation/callback failures remain observable. The caller signal owns cancellation and any deadline; timeoutMs is disabled for the acquisition. Existing Core cancellation rejects locally and sends native cancellation without waiting for a native error evidence response. Complete bodies are buffered for one final RPC response; progress events are not streaming body backpressure. See document-acquisition.md for public usage and errors."
42
+ },
15
43
  {
16
44
  "id": "core-model-observation:createModelObservationService",
17
45
  "name": "createModelObservationService",
@@ -267,6 +267,23 @@ owners; they are separate from this runtime service.
267
267
 
268
268
  ### Selected Ollama model readiness
269
269
 
270
+ High-level `AI.setAI()` and `AI.transitionAI()` accept optional
271
+ `{signal,startLanguageModel}` after their six existing preference arguments;
272
+ `configureProviders()` and `transitionProviders()` accept it as their second
273
+ argument. The default remains eager. `startLanguageModel:false` registers the
274
+ built-in adapter and commits the selected provider/model without preloading it,
275
+ including when later Ollama-ready, credential, or residency callbacks arrive.
276
+ Activate that selection explicitly with `ai.providerRuntime.load('llm',{signal})`
277
+ or `ai.startProviders({startLanguageModel:true,signal})`.
278
+
279
+ Transition cancellation waits for accepted old-provider cleanup and prevents a
280
+ subsequent configuration or preload. Cancellation during the first owned preload
281
+ reaches the native request, retaining the already-committed selection and its real
282
+ runtime state. A newer selection supersedes older pending transition work.
283
+ Cancelled activation stays inactive through readiness callbacks; a caller abort
284
+ after successful settlement does not unload the ready model. These selection
285
+ controls do not change the ownership of separate callers joining an existing load.
286
+
270
287
  The built-in `OLLAMA` LLM provider loads its selected model through Ollama's
271
288
  empty `generate` request and separately confirms that selected model in
272
289
  `running()` (`/api/ps`). A model catalog entry, a connected bridge, and a
@@ -45,6 +45,13 @@ retain their separate [app-control contract](native-app-control.md).
45
45
 
46
46
  ## Attach existing renderer owners
47
47
 
48
+ SDK 0.86.1 adds the public `arcane-os/ai/model-observation` export to normal
49
+ generated browser import maps. Development applications keep `arcane-os` on
50
+ `latest`; after updating through their existing dependency workflow, run
51
+ `arcane import-map` before building or serving. The mapping uses the same
52
+ published browser module and changes no observation runtime behavior. Regenerate
53
+ through the public command rather than editing map entries by hand.
54
+
48
55
  ```js
49
56
  import * as aiRuntimeState from 'arcane-os/ai-runtime-state';
50
57
  import {attachModelObservation} from 'arcane-os/ai/model-observation';
@@ -943,6 +943,21 @@ switches, or falls back implicitly. Generic provider-promise settlement is not
943
943
  a claim that underlying work stopped; only a provider's documented positive
944
944
  acknowledgement or destructive worker teardown can prove that stronger fact.
945
945
 
946
+ High-level built-in selection has its own activation boundary:
947
+ `AI.setAI(...sixPreferences,{signal,startLanguageModel:false})`,
948
+ `AI.transitionAI(...sixPreferences,{signal,startLanguageModel:false})`,
949
+ `AI.configureProviders(selections,{signal,startLanguageModel:false})`, and
950
+ `AI.transitionProviders(selections,{signal,startLanguageModel:false})` register
951
+ the selected adapters and keep the AI tuple and runtime routes together without
952
+ an implicit LLM load. The omitted option remains eager. Deferred or cancelled
953
+ activation remains inactive through later readiness callbacks; explicit runtime
954
+ load or startup can activate that selected route. Accepted transition cleanup
955
+ settles independently, cancellation is checked before configuring/preloading the
956
+ replacement, and the first owned load receives the caller signal. Newer accepted
957
+ selections supersede older pending work without allowing older continuations to
958
+ alter the newer selection. After settlement, later caller abort does not unload
959
+ the completed model. This does not change same-route runtime-load join ownership.
960
+
946
961
  `startAIRuntime({startLanguageModel:false,startTranscription:false})` is the
947
962
  explicit startup boundary for browser-WASM LLM and default STT activation. It
948
963
  leaves the selected LLM unloaded until the shared chat control publishes an
@@ -2084,8 +2084,12 @@ changing these attributes later does not reconfigure an existing instance.
2084
2084
  `setMode()` updates pressed state and the host's `data-mode` only. `refresh()`
2085
2085
  loads the existing ThemeManager state, updates selection and Skin availability,
2086
2086
  and resolves with the complete manager state. Clicking a choice uses the same
2087
- manager persistence/activation operation; buttons remain disabled while that
2088
- operation is pending. Presentation configuration preserves that pending state.
2087
+ manager persistence/activation operation. While it is pending, choices expose
2088
+ `aria-disabled="true"` and ignore repeated activation while remaining in the
2089
+ keyboard focus flow. Native disabling remains for an unavailable Skin choice.
2090
+ Selection does not move focus when it completes or fails, so the initiating
2091
+ button retains focus unless the user moves elsewhere. Presentation configuration
2092
+ preserves that pending state; an actual reorder retains its focus behavior above.
2089
2093
 
2090
2094
  The host inherits text color. Existing `--app-bar-border` (falling back to
2091
2095
  `--border-color`), `--app-bar-accent` (then `--secondary-color`),
@@ -257,10 +257,41 @@ provider. `transitionAI()` and `transitionProviders()` are deliberate
257
257
  cross-role transitions: each stops queued audio, unloads the current LLM, STT,
258
258
  and TTS roles, then applies the replacement configuration. `transitionAI()`
259
259
  returns aggregate runtime status; `transitionProviders()` returns the configured
260
- three-role route configuration. Selected TWiN Cloud `TWIN` LLM, `OLLAMA` LLM,
261
- and Core `LOCAL_SPEACH` STT/TTS built-in routes expose truthful capability-only
262
- readiness through internal provider/2 adapters without probing, downloading, or
263
- hiding a load. Explicit speech preference slots remain unchanged, including a
260
+ three-role route configuration.
261
+
262
+ `setAI(llmService,sttService,ttsService,model,modelTTS,modelSTT,options)` and
263
+ `transitionAI(llmService,sttService,ttsService,model,modelTTS,modelSTT,options)`
264
+ preserve their six positional preference arguments. `configureProviders(selections,options)`
265
+ and `transitionProviders(selections,options)` accept the same optional
266
+ `{signal=null,startLanguageModel=true}` controls. Omission preserves eager built-in
267
+ LLM activation. `startLanguageModel:false` still registers built-in adapters and
268
+ updates both the runtime routes and AI preference tuple, while leaving the selected
269
+ LLM unloaded. Readiness events, credential changes, and native residency observations
270
+ retain that deferred intent; an explicit `providerRuntime.load('llm',{signal})` or
271
+ `startProviders({startLanguageModel:true,signal})` can activate it. Deferral never
272
+ changes the selected model or substitutes another provider.
273
+
274
+ An already-aborted signal rejects before selection changes. During an asynchronous
275
+ transition, accepted old-provider cleanup finishes, then cancellation prevents new
276
+ configuration or preload. A newer accepted selection supersedes pending older
277
+ transition work; older completion cannot configure, load, or clean up the newer
278
+ selection. Once replacement routes are committed, cancellation reaches the first
279
+ owned load and leaves that replacement explicitly selected with its actual runtime
280
+ state. Later readiness callbacks do not restart the cancelled activation. Completed
281
+ selection/load settlement detaches its caller signal, so later abort never unloads
282
+ the ready model. `setAI()` retains its synchronous boolean and `configureProviders()`
283
+ its synchronous route result; their asynchronous readiness remains observable through
284
+ the provider runtime. Rejected synchronous configuration preserves the prior owned
285
+ selection/load. A selection accepted by a synchronous registration or configuration
286
+ listener supersedes the older continuation before it can apply preferences or load.
287
+ These controls do not change cancellation ownership for a
288
+ separate caller joining an already-pending runtime load.
289
+
290
+ Selected TWiN Cloud `TWIN` LLM, `OLLAMA` LLM,
291
+ and Core `LOCAL_SPEACH` STT/TTS built-in routes use internal provider/2 adapters.
292
+ TWiN Cloud and Core speech expose capability readiness; Ollama explicitly
293
+ preloads its selected model and confirms its residency before reporting ready.
294
+ Explicit speech preference slots remain unchanged, including a
264
295
  selected external provider that the application registers after AI startup.
265
296
  TWiN Cloud availability
266
297
  requires the selected LLM route, its model, a credential, and `fetch`; Core speech
@@ -57,6 +57,8 @@ runtime layouts.
57
57
  | `arcane-os/core/runtime` | App-neutral native Core dispatcher and service lifecycle. |
58
58
  | `arcane-os/core/model-observation` | Passive observation of an existing Core and its actual native model service owners. |
59
59
  | `arcane-os/ai/model-observation` | Attach existing renderer model owners and subscribe through an existing Core client. |
60
+ | `arcane-os/document-acquisition` | Acquire complete document and redirect bodies through the selected native Core service. |
61
+ | `arcane-os/core/document-acquisition` | Native HTTP(S) GET service, application-owned destination predicate and acquisition lifecycle. |
60
62
  | `arcane-os/core/development` | App-service composition and document-owned Core transport during source development, independently of local AI. |
61
63
  | `arcane-os/core/stdio` | Framed Node stdio transport with graceful runtime drain. |
62
64
  | `arcane-os/core/host` | Node-only reusable Core host lifecycle, app-selected native launch defaults and shared native location resolution. |
@@ -425,6 +427,8 @@ The remaining data-only subpaths are eight JSON Schemas and package metadata.
425
427
  | `createModelObservationService()` | function | `arcane-os/core/model-observation` | Existing model observation | Node with an existing Core runtime and optional existing native model services |
426
428
  | `attachModelObservation()` | function | `arcane-os/ai/model-observation` | Existing model observation | JavaScript with an existing Core client and optional existing renderer owners |
427
429
  | `subscribeModelObservation()` | function | `arcane-os/ai/model-observation` | Existing model observation | JavaScript with an existing Core client and composed observation service |
430
+ | `acquireCoreDocument()` | function | `arcane-os/document-acquisition` | Native document acquisition | Browser or native WebView with an existing Core client and selected document service |
431
+ | `createDocumentAcquisitionService()` | function | `arcane-os/core/document-acquisition` | Native document acquisition | Node Core on Windows, Linux and macOS; Android requires host adaptation |
428
432
  | `encodeCoreFrame()` | function | `arcane-os/core/stdio` | Native Core stdio transport | Node |
429
433
  | `createCoreFrameDecoder()` | function | `arcane-os/core/stdio` | Native Core stdio transport | Node |
430
434
  | `startCoreStdio()` | function | `arcane-os/core/stdio` | Native Core stdio transport | Node |
@@ -7671,6 +7675,64 @@ promises may await `close()` without becoming its completion barrier. It starts
7671
7675
  no model, refresh, polling, endpoint discovery, host or reconnect. See
7672
7676
  [observation records and limits](model-observation.md#read-or-subscribe-from-an-existing-client).
7673
7677
 
7678
+ ## acquireCoreDocument()
7679
+
7680
+ ```text
7681
+ acquireCoreDocument({url,client,signal,onProgress}={})
7682
+ ```
7683
+
7684
+ Import from `arcane-os/document-acquisition`. The asynchronous helper uses the
7685
+ supplied or installed Core client to invoke `documents.acquire` with `{url}`.
7686
+ The application must compose the native document service. Missing Core reports
7687
+ `DOCUMENT_ACQUISITION_CORE_UNAVAILABLE`; a missing service retains the existing
7688
+ unavailable-method error. Browser-only previews have no browser Fetch fallback.
7689
+
7690
+ Resolves with `{requestedUrl,finalUrl,url,status,statusText,ok,headers,mediaType,
7691
+ body,complete,redirects}`. The final body and every ordered redirect body are
7692
+ complete `Blob` values. An HTTP error response retains its status and content;
7693
+ `ok:false` is a result, not a generic acquisition error. Headers and entity
7694
+ content reflect Fetch's public response, including ordinary content decoding.
7695
+
7696
+ `signal` cancels this request; there is no implicit timeout. `onProgress` receives
7697
+ request-correlated `documents.progress` records with phases, document counts
7698
+ and status fields, never body content. All accepted callback promises settle
7699
+ before the helper settles. Callback failures cancel acquisition and remain
7700
+ observable; concurrent operation and callback failures retain an `AggregateError`.
7701
+ A late callback or subscription cleanup failure retains the completed response
7702
+ in `error.documentAcquisition` with the original cause or aggregated failures.
7703
+ Every settlement removes the subscription. Local abort rejects immediately and
7704
+ does not wait for native error evidence. See
7705
+ [complete result, progress and error records](document-acquisition.md).
7706
+
7707
+ ## createDocumentAcquisitionService()
7708
+
7709
+ ```text
7710
+ createDocumentAcquisitionService({destinationPredicate,signal}={})
7711
+ ```
7712
+
7713
+ Import from `arcane-os/core/document-acquisition`. This synchronous factory
7714
+ returns the Core service definition with `documents.acquire`, direct
7715
+ `acquire({url,signal,onProgress})` and idempotent asynchronous `dispose()`.
7716
+ Select it through the application's existing native service composition. The
7717
+ ordinary service performs HTTP(S) GET acquisition without a predicate.
7718
+
7719
+ An application-selected `destinationPredicate(destinationUrl,
7720
+ {requestedUrl,previousUrl,signal})` runs locally before every actual GET,
7721
+ including the first destination and each redirect. Only `true` permits the
7722
+ request; other returns produce `DOCUMENT_DESTINATION_DECLINED`, and a thrown
7723
+ or rejected value remains the original cause. The predicate is never serialized.
7724
+ Redirects retain every response body; no automatic retry or redirect-count cap
7725
+ is imposed. The caller can cancel a repeating redirect chain.
7726
+
7727
+ Direct results and error evidence retain `Uint8Array` bodies. Acquisition errors
7728
+ keep the original cause and `documentAcquisition` response/redirect records;
7729
+ an interrupted body read retains available content marked `complete:false`.
7730
+ Only Core JSON transport encodes bodies as base64; the browser restores Blobs.
7731
+ The factory signal owns the service lifetime; each call signal owns only its
7732
+ request and leaves sibling acquisitions active. `dispose()` aborts and joins all
7733
+ active operations. Bodies are buffered completely; progress is not a body stream.
7734
+ See [native composition and lifecycle](document-acquisition.md).
7735
+
7674
7736
  ## createPortableNativeProvider()
7675
7737
 
7676
7738
  ### Overview