arcane-os 0.87.0 → 0.88.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +10 -0
- package/docs/reference/inventory/package-api.json +1 -1
- package/docs/reference/local-ai.md +17 -0
- package/docs/reference/protocols.md +15 -0
- package/docs/reference/runtime-components.md +6 -2
- package/docs/reference/runtime-modules.md +35 -4
- package/docs/reference/sdk-api.md +73 -0
- package/package.json +1 -1
- package/runtime/arcane/components/theme-switcher.html +10 -5
- package/runtime/arcane/modules/AI.js +306 -120
- package/runtime/arcane/modules/AIRuntimeState.js +5 -2
- package/src/workspace-operation-lock.mjs +23 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.88.1
|
|
4
|
+
|
|
5
|
+
- Recover an ordinary workspace-operation lock when its recorded runner is
|
|
6
|
+
confirmed absent, including before the six-hour expiry. Retrying the public
|
|
7
|
+
operation uses the shared lock owner; existing published records stay readable.
|
|
8
|
+
- Preserve live or indeterminate owners and complete process-probe errors.
|
|
9
|
+
Recheck cancellation, nonce, PID and owner presence after asynchronous recovery
|
|
10
|
+
observation so an observed replacement remains owned by its actual writer.
|
|
11
|
+
The existing cooperative read/remove protocol is not an atomic cross-process
|
|
12
|
+
compare-and-delete.
|
|
13
|
+
|
|
14
|
+
## 0.88.0
|
|
15
|
+
|
|
16
|
+
- Add optional `signal` and `startLanguageModel` controls to `AI.setAI`,
|
|
17
|
+
`configureProviders`, `transitionAI`, and `transitionProviders`, preserving
|
|
18
|
+
their existing arguments, return values, and eager default. Explicitly
|
|
19
|
+
deferred selections stay unloaded through later readiness notifications;
|
|
20
|
+
an explicit runtime load or startup can activate them.
|
|
21
|
+
- Keep cancellation and supersession with the owning selection. Accepted
|
|
22
|
+
cleanup settles before a cancelled transition can configure or preload a
|
|
23
|
+
replacement, and stale continuations cannot alter a newer selection.
|
|
24
|
+
Completed activation is independent of a later caller abort.
|
|
25
|
+
- Preserve truthful busy, loaded `unloading` state while an accepted request
|
|
26
|
+
finishes cancellation; new inference remains unavailable during cleanup.
|
|
27
|
+
- Preserve keyboard focus in the shared theme switcher during asynchronous
|
|
28
|
+
selection. Pending choices remain focusable and expose `aria-disabled` while
|
|
29
|
+
repeated activation is ignored; completion never overrides an intentional
|
|
30
|
+
focus move. Unavailable Skin choices remain natively disabled.
|
|
31
|
+
|
|
3
32
|
## 0.87.0
|
|
4
33
|
|
|
5
34
|
- Add `createDocumentAcquisitionService` through
|
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.
|
|
28
|
+
This checkout defines the `0.88.1` 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;
|
package/docs/reference/README.md
CHANGED
|
@@ -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) |
|
package/docs/reference/cli.md
CHANGED
|
@@ -405,6 +405,16 @@ not package the app or produce `dist` output. With PWA enabled, the server
|
|
|
405
405
|
generates the installation and offline manifests directly; see
|
|
406
406
|
[PWA development and versioning](pwa.md#development-and-hosting).
|
|
407
407
|
|
|
408
|
+
Ordinary operation acquisition can recover a workspace-operation lock whose
|
|
409
|
+
recorded process is confirmed absent, including before its six-hour expiry.
|
|
410
|
+
Retry the intended command through its usual public entrypoint; no separate
|
|
411
|
+
unlock command or caller-created lease is needed. A live or indeterminate owner
|
|
412
|
+
remains `ARCANE_WORKSPACE_BUSY`, with an indeterminate process-probe error
|
|
413
|
+
retained as the cause. Recovery rechecks cancellation, nonce, PID and owner
|
|
414
|
+
presence after asynchronous observation, preserving a replacement detected
|
|
415
|
+
there. This cooperative read-then-remove protocol does not provide an
|
|
416
|
+
atomic compare-and-delete across competing processes.
|
|
417
|
+
|
|
408
418
|
The startup URL remains the selected app entry. At `/`, the server serves an
|
|
409
419
|
existing authored host-root `index.html` and suppresses the app-entry redirect
|
|
410
420
|
and PWA alias. A root outside the selected app mapping is served unchanged only
|
|
@@ -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
|
|
@@ -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
|
|
2088
|
-
|
|
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.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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 |
|
|
@@ -1864,6 +1868,17 @@ whole-tree replacement. The mutable result includes the installed-package
|
|
|
1864
1868
|
location, materialized workspace runtime paths, and `workspaceLock` path and
|
|
1865
1869
|
document. It does not install dependencies or merge application source.
|
|
1866
1870
|
|
|
1871
|
+
Ordinary acquisition automatically recovers a workspace-operation lock when
|
|
1872
|
+
the recorded owner process is confirmed absent, without waiting solely for its
|
|
1873
|
+
six-hour expiry. Retry this public operation normally; `workspaceOperationLease`
|
|
1874
|
+
is for active nested SDK work and cannot be reconstructed from the lock JSON.
|
|
1875
|
+
Live or indeterminate owners remain `ARCANE_WORKSPACE_BUSY`; an indeterminate
|
|
1876
|
+
process-probe error is retained as the cause. Existing published lock documents
|
|
1877
|
+
remain readable. Recovery rechecks cancellation, the captured nonce and PID,
|
|
1878
|
+
and owner presence after asynchronous observation, preserving a replacement
|
|
1879
|
+
detected there. This cooperative read-then-remove protocol does not provide an
|
|
1880
|
+
atomic compare-and-delete across competing processes.
|
|
1881
|
+
|
|
1867
1882
|
Replacement removes files absent from the selected runtime content and restores
|
|
1868
1883
|
the prior tree if commit fails. `signal` and
|
|
1869
1884
|
`onEvent` remain caller-owned before commit. Once the atomic replacement starts,
|
|
@@ -7671,6 +7686,64 @@ promises may await `close()` without becoming its completion barrier. It starts
|
|
|
7671
7686
|
no model, refresh, polling, endpoint discovery, host or reconnect. See
|
|
7672
7687
|
[observation records and limits](model-observation.md#read-or-subscribe-from-an-existing-client).
|
|
7673
7688
|
|
|
7689
|
+
## acquireCoreDocument()
|
|
7690
|
+
|
|
7691
|
+
```text
|
|
7692
|
+
acquireCoreDocument({url,client,signal,onProgress}={})
|
|
7693
|
+
```
|
|
7694
|
+
|
|
7695
|
+
Import from `arcane-os/document-acquisition`. The asynchronous helper uses the
|
|
7696
|
+
supplied or installed Core client to invoke `documents.acquire` with `{url}`.
|
|
7697
|
+
The application must compose the native document service. Missing Core reports
|
|
7698
|
+
`DOCUMENT_ACQUISITION_CORE_UNAVAILABLE`; a missing service retains the existing
|
|
7699
|
+
unavailable-method error. Browser-only previews have no browser Fetch fallback.
|
|
7700
|
+
|
|
7701
|
+
Resolves with `{requestedUrl,finalUrl,url,status,statusText,ok,headers,mediaType,
|
|
7702
|
+
body,complete,redirects}`. The final body and every ordered redirect body are
|
|
7703
|
+
complete `Blob` values. An HTTP error response retains its status and content;
|
|
7704
|
+
`ok:false` is a result, not a generic acquisition error. Headers and entity
|
|
7705
|
+
content reflect Fetch's public response, including ordinary content decoding.
|
|
7706
|
+
|
|
7707
|
+
`signal` cancels this request; there is no implicit timeout. `onProgress` receives
|
|
7708
|
+
request-correlated `documents.progress` records with phases, document counts
|
|
7709
|
+
and status fields, never body content. All accepted callback promises settle
|
|
7710
|
+
before the helper settles. Callback failures cancel acquisition and remain
|
|
7711
|
+
observable; concurrent operation and callback failures retain an `AggregateError`.
|
|
7712
|
+
A late callback or subscription cleanup failure retains the completed response
|
|
7713
|
+
in `error.documentAcquisition` with the original cause or aggregated failures.
|
|
7714
|
+
Every settlement removes the subscription. Local abort rejects immediately and
|
|
7715
|
+
does not wait for native error evidence. See
|
|
7716
|
+
[complete result, progress and error records](document-acquisition.md).
|
|
7717
|
+
|
|
7718
|
+
## createDocumentAcquisitionService()
|
|
7719
|
+
|
|
7720
|
+
```text
|
|
7721
|
+
createDocumentAcquisitionService({destinationPredicate,signal}={})
|
|
7722
|
+
```
|
|
7723
|
+
|
|
7724
|
+
Import from `arcane-os/core/document-acquisition`. This synchronous factory
|
|
7725
|
+
returns the Core service definition with `documents.acquire`, direct
|
|
7726
|
+
`acquire({url,signal,onProgress})` and idempotent asynchronous `dispose()`.
|
|
7727
|
+
Select it through the application's existing native service composition. The
|
|
7728
|
+
ordinary service performs HTTP(S) GET acquisition without a predicate.
|
|
7729
|
+
|
|
7730
|
+
An application-selected `destinationPredicate(destinationUrl,
|
|
7731
|
+
{requestedUrl,previousUrl,signal})` runs locally before every actual GET,
|
|
7732
|
+
including the first destination and each redirect. Only `true` permits the
|
|
7733
|
+
request; other returns produce `DOCUMENT_DESTINATION_DECLINED`, and a thrown
|
|
7734
|
+
or rejected value remains the original cause. The predicate is never serialized.
|
|
7735
|
+
Redirects retain every response body; no automatic retry or redirect-count cap
|
|
7736
|
+
is imposed. The caller can cancel a repeating redirect chain.
|
|
7737
|
+
|
|
7738
|
+
Direct results and error evidence retain `Uint8Array` bodies. Acquisition errors
|
|
7739
|
+
keep the original cause and `documentAcquisition` response/redirect records;
|
|
7740
|
+
an interrupted body read retains available content marked `complete:false`.
|
|
7741
|
+
Only Core JSON transport encodes bodies as base64; the browser restores Blobs.
|
|
7742
|
+
The factory signal owns the service lifetime; each call signal owns only its
|
|
7743
|
+
request and leaves sibling acquisitions active. `dispose()` aborts and joins all
|
|
7744
|
+
active operations. Bodies are buffered completely; progress is not a body stream.
|
|
7745
|
+
See [native composition and lifecycle](document-acquisition.md).
|
|
7746
|
+
|
|
7674
7747
|
## createPortableNativeProvider()
|
|
7675
7748
|
|
|
7676
7749
|
### Overview
|
package/package.json
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
overflow-wrap:anywhere;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
button:hover:not(:disabled):not([aria-pressed="true"]) {
|
|
41
|
+
button:hover:not(:disabled):not([aria-disabled="true"]):not([aria-pressed="true"]) {
|
|
42
42
|
background:color-mix(in srgb,currentColor 14%,transparent);
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
box-shadow:inset 0 0 0 .1rem currentColor;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
button:disabled
|
|
51
|
+
button:disabled,
|
|
52
|
+
button[aria-disabled="true"] {
|
|
52
53
|
cursor:not-allowed;
|
|
53
54
|
opacity:.55;
|
|
54
55
|
}
|
|
@@ -88,7 +89,7 @@
|
|
|
88
89
|
host.refresh=refresh;
|
|
89
90
|
group.addEventListener('click',async function selectScheme(event){
|
|
90
91
|
const button=event.target.closest('[data-scheme]');
|
|
91
|
-
if(!button||button.disabled) return;
|
|
92
|
+
if(!button||button.disabled||busy) return;
|
|
92
93
|
setBusy(true);
|
|
93
94
|
try{
|
|
94
95
|
const state=button.dataset.scheme==='custom'?await manager.activateCustom():await manager.setScheme(button.dataset.scheme);
|
|
@@ -134,10 +135,14 @@
|
|
|
134
135
|
}
|
|
135
136
|
function setBusy(value){
|
|
136
137
|
busy=Boolean(value);
|
|
137
|
-
|
|
138
|
+
// Keep the focused choice in the keyboard flow while its operation
|
|
139
|
+
// settles. The click owner rejects repeat activation without moving focus.
|
|
140
|
+
for(const button of buttons) button.setAttribute('aria-disabled',String(busy||button.disabled));
|
|
138
141
|
}
|
|
139
142
|
function setCustomAvailable(value){
|
|
140
|
-
buttons.find(function customScheme(button){return button.dataset.scheme==='custom';})
|
|
143
|
+
const button=buttons.find(function customScheme(button){return button.dataset.scheme==='custom';});
|
|
144
|
+
button.disabled=!value;
|
|
145
|
+
button.setAttribute('aria-disabled',String(busy||button.disabled));
|
|
141
146
|
}
|
|
142
147
|
async function refresh(){const state=await manager.load();setMode(state.mode);setCustomAvailable(Boolean(state.theme));return state;}
|
|
143
148
|
|
|
@@ -759,6 +759,19 @@ function isAbortSignal(value){
|
|
|
759
759
|
&&is.function(value.removeEventListener);
|
|
760
760
|
}
|
|
761
761
|
|
|
762
|
+
function normalizeAISelectionOptions({signal=null,startLanguageModel=true}={}){
|
|
763
|
+
if(signal!==null&&!isAbortSignal(signal)){
|
|
764
|
+
throw new TypeError('AI selection signal must be an AbortSignal.');
|
|
765
|
+
}
|
|
766
|
+
if(!is.boolean(startLanguageModel)){
|
|
767
|
+
throw new TypeError('AI selection startLanguageModel must be a boolean.');
|
|
768
|
+
}
|
|
769
|
+
if(signal?.aborted){
|
|
770
|
+
throw normalizeAIRequestAbort(signal.reason);
|
|
771
|
+
}
|
|
772
|
+
return {signal,startLanguageModel};
|
|
773
|
+
}
|
|
774
|
+
|
|
762
775
|
function closedRecord(value,keys,required,label){
|
|
763
776
|
if(!value
|
|
764
777
|
||!is.object(value)
|
|
@@ -1173,6 +1186,8 @@ class AI {
|
|
|
1173
1186
|
#browserSpeechTransition=Promise.resolve();
|
|
1174
1187
|
#builtInLLMProviders=new Map();
|
|
1175
1188
|
#builtInLLMReadiness=Promise.resolve(null);
|
|
1189
|
+
#providerSelectionGeneration=0;
|
|
1190
|
+
#providerSelectionOperation=null;
|
|
1176
1191
|
#builtInSpeechProviders=new Map();
|
|
1177
1192
|
#configuredSpeechProviders=new Map();
|
|
1178
1193
|
#builtInSpeechReadiness=Promise.resolve(null);
|
|
@@ -2146,7 +2161,8 @@ class AI {
|
|
|
2146
2161
|
});
|
|
2147
2162
|
}
|
|
2148
2163
|
|
|
2149
|
-
#ensureBuiltInLLMProvider(providerId){
|
|
2164
|
+
#ensureBuiltInLLMProvider(providerId,operation=null){
|
|
2165
|
+
if(operation)this.#assertProviderSelection(operation);
|
|
2150
2166
|
if(providerId!=='TWIN'&&providerId!=='OLLAMA'){
|
|
2151
2167
|
return false;
|
|
2152
2168
|
}
|
|
@@ -2154,15 +2170,34 @@ class AI {
|
|
|
2154
2170
|
return false;
|
|
2155
2171
|
}
|
|
2156
2172
|
const provider=this.#createBuiltInLLMProvider(providerId);
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2173
|
+
const runtime=this;
|
|
2174
|
+
let unregister=null;
|
|
2175
|
+
const record={
|
|
2176
|
+
provider,
|
|
2177
|
+
unregister:function unregisterBuiltInLLMProvider(){
|
|
2178
|
+
if(unregister)return unregister();
|
|
2179
|
+
if(!runtime.#providerRuntime.ownsProvider('llm',provider))return false;
|
|
2180
|
+
return runtime.#providerRuntime.unregister('llm',providerId);
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
// Registration can publish a pending route synchronously; its owner must
|
|
2184
|
+
// already be visible to a selection accepted by that callback.
|
|
2185
|
+
this.#builtInLLMProviders.set(providerId,record);
|
|
2186
|
+
try{
|
|
2187
|
+
unregister=this.#providerRuntime.register(provider);
|
|
2188
|
+
}catch(error){
|
|
2189
|
+
if(!this.#providerRuntime.ownsProvider('llm',provider)
|
|
2190
|
+
&&this.#builtInLLMProviders.get(providerId)===record){
|
|
2191
|
+
this.#builtInLLMProviders.delete(providerId);
|
|
2192
|
+
}
|
|
2193
|
+
throw error;
|
|
2194
|
+
}
|
|
2195
|
+
if(operation)this.#assertProviderSelection(operation);
|
|
2162
2196
|
return true;
|
|
2163
2197
|
}
|
|
2164
2198
|
|
|
2165
|
-
#ensureBuiltInSpeechProvider(role,providerId){
|
|
2199
|
+
#ensureBuiltInSpeechProvider(role,providerId,operation=null){
|
|
2200
|
+
if(operation)this.#assertProviderSelection(operation);
|
|
2166
2201
|
if(!['stt','tts'].includes(role)||providerId!=='LOCAL_SPEACH'){
|
|
2167
2202
|
return false;
|
|
2168
2203
|
}
|
|
@@ -2170,33 +2205,54 @@ class AI {
|
|
|
2170
2205
|
return false;
|
|
2171
2206
|
}
|
|
2172
2207
|
const provider=this.#createBuiltInSpeechProvider(role,providerId);
|
|
2173
|
-
const
|
|
2174
|
-
this.#
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2208
|
+
const runtime=this;
|
|
2209
|
+
const key=this.#builtInSpeechProviderKey(role,providerId);
|
|
2210
|
+
let unregister=null;
|
|
2211
|
+
const record={
|
|
2212
|
+
role,providerId,provider,
|
|
2213
|
+
unregister:function unregisterBuiltInSpeechProvider(){
|
|
2214
|
+
if(unregister)return unregister();
|
|
2215
|
+
if(!runtime.#providerRuntime.ownsProvider(role,provider))return false;
|
|
2216
|
+
return runtime.#providerRuntime.unregister(role,providerId);
|
|
2217
|
+
}
|
|
2218
|
+
};
|
|
2219
|
+
this.#builtInSpeechProviders.set(key,record);
|
|
2220
|
+
try{
|
|
2221
|
+
unregister=this.#providerRuntime.register(provider);
|
|
2222
|
+
}catch(error){
|
|
2223
|
+
if(!this.#providerRuntime.ownsProvider(role,provider)
|
|
2224
|
+
&&this.#builtInSpeechProviders.get(key)===record){
|
|
2225
|
+
this.#builtInSpeechProviders.delete(key);
|
|
2226
|
+
}
|
|
2227
|
+
throw error;
|
|
2228
|
+
}
|
|
2229
|
+
if(operation)this.#assertProviderSelection(operation);
|
|
2178
2230
|
return true;
|
|
2179
2231
|
}
|
|
2180
2232
|
|
|
2181
|
-
#releaseInactiveBuiltInLLMProviders(activeProviderId){
|
|
2233
|
+
#releaseInactiveBuiltInLLMProviders(activeProviderId,operation=null){
|
|
2182
2234
|
for(const [providerId,record] of this.#builtInLLMProviders){
|
|
2235
|
+
if(operation)this.#assertProviderSelection(operation);
|
|
2183
2236
|
if(providerId===activeProviderId){
|
|
2184
2237
|
continue;
|
|
2185
2238
|
}
|
|
2186
|
-
if(record.unregister()){
|
|
2239
|
+
if(record.unregister()&&this.#builtInLLMProviders.get(providerId)===record){
|
|
2187
2240
|
this.#builtInLLMProviders.delete(providerId);
|
|
2188
2241
|
}
|
|
2189
2242
|
}
|
|
2190
2243
|
}
|
|
2191
2244
|
|
|
2192
|
-
#releaseInactiveBuiltInSpeechProviders(activeProviders){
|
|
2245
|
+
#releaseInactiveBuiltInSpeechProviders(activeProviders,operation=null){
|
|
2193
2246
|
for(const [key,record] of this.#builtInSpeechProviders){
|
|
2247
|
+
if(operation)this.#assertProviderSelection(operation);
|
|
2194
2248
|
if(activeProviders[record.role]===record.providerId){
|
|
2195
2249
|
continue;
|
|
2196
2250
|
}
|
|
2197
2251
|
if(record.unregister()){
|
|
2198
2252
|
record.provider.dispose();
|
|
2199
|
-
this.#builtInSpeechProviders.
|
|
2253
|
+
if(this.#builtInSpeechProviders.get(key)===record){
|
|
2254
|
+
this.#builtInSpeechProviders.delete(key);
|
|
2255
|
+
}
|
|
2200
2256
|
}
|
|
2201
2257
|
}
|
|
2202
2258
|
}
|
|
@@ -2246,7 +2302,85 @@ class AI {
|
|
|
2246
2302
|
return this.#builtInSpeechReadiness;
|
|
2247
2303
|
}
|
|
2248
2304
|
|
|
2249
|
-
#
|
|
2305
|
+
#beginProviderSelection({signal,startLanguageModel},deferSupersession=false){
|
|
2306
|
+
const previous=this.#providerSelectionOperation;
|
|
2307
|
+
const controller=new AbortController();
|
|
2308
|
+
const operation={
|
|
2309
|
+
generation:++this.#providerSelectionGeneration,
|
|
2310
|
+
controller,
|
|
2311
|
+
startLanguageModel,
|
|
2312
|
+
previous:deferSupersession?previous:null,
|
|
2313
|
+
restorePrevious:deferSupersession,
|
|
2314
|
+
committed:false,
|
|
2315
|
+
settled:false,
|
|
2316
|
+
detachSignal:function detachAISelectionSignal(){
|
|
2317
|
+
signal?.removeEventListener('abort',forwardAbort);
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
function forwardAbort(){
|
|
2321
|
+
controller.abort(signal.reason);
|
|
2322
|
+
}
|
|
2323
|
+
this.#providerSelectionOperation=operation;
|
|
2324
|
+
signal?.addEventListener('abort',forwardAbort,{once:true});
|
|
2325
|
+
if(signal?.aborted)forwardAbort();
|
|
2326
|
+
if(!deferSupersession)this.#supersedeProviderSelection(previous);
|
|
2327
|
+
return operation;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
#supersedeProviderSelection(previous){
|
|
2331
|
+
for(let operation=previous;operation;operation=operation.previous){
|
|
2332
|
+
operation.detachSignal();
|
|
2333
|
+
if(!operation.settled){
|
|
2334
|
+
operation.controller.abort(aiProviderError(
|
|
2335
|
+
'AI provider selection was superseded.',
|
|
2336
|
+
'ARCANE_AI_OPERATION_SUPERSEDED'
|
|
2337
|
+
));
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
#assertProviderSelection(operation,allowAborted=false){
|
|
2343
|
+
if(operation.generation!==this.#providerSelectionOperation?.generation){
|
|
2344
|
+
throw aiProviderError(
|
|
2345
|
+
'AI provider selection was superseded.',
|
|
2346
|
+
'ARCANE_AI_OPERATION_SUPERSEDED'
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
if(!allowAborted&&operation.controller.signal.aborted){
|
|
2350
|
+
throw normalizeAIRequestAbort(operation.controller.signal.reason);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
#commitProviderSelection(operation,tuple){
|
|
2355
|
+
// Configuration already committed its routes before notifying listeners.
|
|
2356
|
+
// Keep its tuple together even when that notification aborts this caller;
|
|
2357
|
+
// a newer owner alone prevents this continuation from applying the tuple.
|
|
2358
|
+
this.#assertProviderSelection(operation,true);
|
|
2359
|
+
this.#applyPreferenceTuple(tuple);
|
|
2360
|
+
operation.committed=true;
|
|
2361
|
+
const previous=operation.previous;
|
|
2362
|
+
operation.previous=null;
|
|
2363
|
+
this.#supersedeProviderSelection(previous);
|
|
2364
|
+
this.#assertProviderSelection(operation);
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
#finishProviderSelection(operation){
|
|
2368
|
+
if(operation.restorePrevious&&!operation.committed
|
|
2369
|
+
&&this.#providerSelectionOperation===operation){
|
|
2370
|
+
this.#providerSelectionOperation=operation.previous;
|
|
2371
|
+
}
|
|
2372
|
+
operation.previous=null;
|
|
2373
|
+
operation.settled=true;
|
|
2374
|
+
operation.detachSignal();
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
#reconcileBuiltInLLMReadiness(operation=this.#providerSelectionOperation){
|
|
2378
|
+
if(operation&&(
|
|
2379
|
+
operation!==this.#providerSelectionOperation
|
|
2380
|
+
||!operation.committed
|
|
2381
|
+
)){
|
|
2382
|
+
return Promise.resolve(this.#providerRuntime.status('llm'));
|
|
2383
|
+
}
|
|
2250
2384
|
const selection=this.#builtInLLMSelection(false);
|
|
2251
2385
|
if(!selection){
|
|
2252
2386
|
return Promise.resolve(this.#providerRuntime.status('llm'));
|
|
@@ -2259,10 +2393,14 @@ class AI {
|
|
|
2259
2393
|
return this.#providerRuntime.unload('llm');
|
|
2260
2394
|
}
|
|
2261
2395
|
if(this.#builtInLLMCapability(selection.providerId)){
|
|
2262
|
-
if(
|
|
2396
|
+
if(operation?.startLanguageModel===false
|
|
2397
|
+
||operation?.controller.signal.aborted
|
|
2398
|
+
||(status.state==='ready'&&status.loaded===true)){
|
|
2263
2399
|
return Promise.resolve(status);
|
|
2264
2400
|
}
|
|
2265
|
-
return this.#providerRuntime.load('llm'
|
|
2401
|
+
return this.#providerRuntime.load('llm',{
|
|
2402
|
+
signal:operation?.controller.signal??null
|
|
2403
|
+
});
|
|
2266
2404
|
}
|
|
2267
2405
|
if(status.loaded===true
|
|
2268
2406
|
||status.busy===true
|
|
@@ -2591,12 +2729,15 @@ class AI {
|
|
|
2591
2729
|
}
|
|
2592
2730
|
}
|
|
2593
2731
|
|
|
2594
|
-
async #unloadProviderRolesForTransition(){
|
|
2595
|
-
const
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2732
|
+
async #unloadProviderRolesForTransition(operation){
|
|
2733
|
+
const runtime=this;
|
|
2734
|
+
const settlements=await Promise.allSettled(
|
|
2735
|
+
['llm','stt','tts'].map(async function unloadPreviousAIProviderRole(role){
|
|
2736
|
+
runtime.#assertProviderSelection(operation);
|
|
2737
|
+
// Accepted cleanup owns completion independently of page cancellation.
|
|
2738
|
+
return runtime.#providerRuntime.unload(role);
|
|
2739
|
+
})
|
|
2740
|
+
);
|
|
2600
2741
|
const failure=settlements.find(function findAITransitionCleanupFailure(result){
|
|
2601
2742
|
return result.status==='rejected';
|
|
2602
2743
|
});
|
|
@@ -2643,7 +2784,8 @@ class AI {
|
|
|
2643
2784
|
ttsService,
|
|
2644
2785
|
model,
|
|
2645
2786
|
modelTTS,
|
|
2646
|
-
modelSTT
|
|
2787
|
+
modelSTT,
|
|
2788
|
+
options
|
|
2647
2789
|
) {
|
|
2648
2790
|
if (
|
|
2649
2791
|
!(
|
|
@@ -2667,59 +2809,83 @@ class AI {
|
|
|
2667
2809
|
]);
|
|
2668
2810
|
this.#assertValidProviderTuple(tuple);
|
|
2669
2811
|
this.#assertSynchronousBrowserSpeechSupersession('AI.setAI');
|
|
2670
|
-
|
|
2671
|
-
this.#
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
this.#
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2812
|
+
const normalized=normalizeAISelectionOptions(options);
|
|
2813
|
+
const operation=this.#beginProviderSelection(normalized,true);
|
|
2814
|
+
try{
|
|
2815
|
+
this.#assertProviderSelection(operation);
|
|
2816
|
+
this.#ensureBuiltInLLMProvider(tuple[0],operation);
|
|
2817
|
+
this.#ensureBuiltInSpeechProvider('stt',tuple[1],operation);
|
|
2818
|
+
this.#ensureBuiltInSpeechProvider('tts',tuple[2],operation);
|
|
2819
|
+
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
2820
|
+
this.#commitProviderSelection(operation,tuple);
|
|
2821
|
+
this.#invalidateSpeechControl();
|
|
2822
|
+
this.#assertProviderSelection(operation);
|
|
2823
|
+
this.#releaseInactiveBuiltInLLMProviders(tuple[0],operation);
|
|
2824
|
+
this.#assertProviderSelection(operation);
|
|
2825
|
+
this.#releaseInactiveBuiltInSpeechProviders({
|
|
2826
|
+
stt:tuple[1],
|
|
2827
|
+
tts:tuple[2]
|
|
2828
|
+
},operation);
|
|
2829
|
+
this.#assertProviderSelection(operation);
|
|
2830
|
+
const runtime=this;
|
|
2831
|
+
this.#retainBuiltInLLMReadiness(
|
|
2832
|
+
this.#reconcileBuiltInLLMReadiness(operation).finally(
|
|
2833
|
+
function settleAISelectionReadiness(){
|
|
2834
|
+
runtime.#finishProviderSelection(operation);
|
|
2835
|
+
}
|
|
2836
|
+
)
|
|
2837
|
+
);
|
|
2838
|
+
this.#assertProviderSelection(operation);
|
|
2839
|
+
this.#retainBuiltInSpeechReadiness(
|
|
2840
|
+
this.#reconcileBuiltInSpeechReadiness()
|
|
2841
|
+
);
|
|
2842
|
+
return true;
|
|
2843
|
+
}catch(error){
|
|
2844
|
+
this.#finishProviderSelection(operation);
|
|
2845
|
+
throw error;
|
|
2846
|
+
}
|
|
2688
2847
|
}
|
|
2689
2848
|
|
|
2690
|
-
configureProviders(selections){
|
|
2849
|
+
configureProviders(selections,options){
|
|
2691
2850
|
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
2692
2851
|
this.#assertDeviceSpeechConfiguration(prepared);
|
|
2693
2852
|
this.#assertSynchronousBrowserSpeechSupersession('AI.configureProviders');
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
prepared.stt.default?.providerId
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2853
|
+
const normalized=normalizeAISelectionOptions(options);
|
|
2854
|
+
const operation=this.#beginProviderSelection(normalized,true);
|
|
2855
|
+
try{
|
|
2856
|
+
this.#assertProviderSelection(operation);
|
|
2857
|
+
this.#ensureBuiltInLLMProvider(prepared.llm.default?.providerId,operation);
|
|
2858
|
+
this.#ensureBuiltInSpeechProvider('stt',prepared.stt.default?.providerId,operation);
|
|
2859
|
+
this.#ensureBuiltInSpeechProvider('tts',prepared.tts.default?.providerId,operation);
|
|
2860
|
+
this.#assertRegisteredBuiltInRoutes(prepared);
|
|
2861
|
+
const configured=this.#providerRuntime.configure(prepared);
|
|
2862
|
+
this.#commitProviderSelection(operation,this.#tupleFromProviderRoutes(configured));
|
|
2863
|
+
this.#invalidateSpeechControl();
|
|
2864
|
+
this.#assertProviderSelection(operation);
|
|
2865
|
+
this.#releaseInactiveBuiltInLLMProviders(configured.llm.default?.providerId,operation);
|
|
2866
|
+
this.#assertProviderSelection(operation);
|
|
2867
|
+
this.#releaseInactiveBuiltInSpeechProviders({
|
|
2868
|
+
stt:configured.stt.default?.providerId,
|
|
2869
|
+
tts:configured.tts.default?.providerId
|
|
2870
|
+
},operation);
|
|
2871
|
+
this.#assertProviderSelection(operation);
|
|
2872
|
+
const runtime=this;
|
|
2873
|
+
this.#retainBuiltInLLMReadiness(
|
|
2874
|
+
this.#reconcileBuiltInLLMReadiness(operation).finally(
|
|
2875
|
+
function settleAIProviderConfigurationReadiness(){
|
|
2876
|
+
runtime.#finishProviderSelection(operation);
|
|
2877
|
+
}
|
|
2878
|
+
)
|
|
2879
|
+
);
|
|
2880
|
+
this.#assertProviderSelection(operation);
|
|
2881
|
+
this.#retainBuiltInSpeechReadiness(
|
|
2882
|
+
this.#reconcileBuiltInSpeechReadiness()
|
|
2883
|
+
);
|
|
2884
|
+
return configured;
|
|
2885
|
+
}catch(error){
|
|
2886
|
+
this.#finishProviderSelection(operation);
|
|
2887
|
+
throw error;
|
|
2888
|
+
}
|
|
2723
2889
|
}
|
|
2724
2890
|
|
|
2725
2891
|
configureSpeechProviders(selections){
|
|
@@ -2762,7 +2928,8 @@ class AI {
|
|
|
2762
2928
|
ttsService,
|
|
2763
2929
|
model,
|
|
2764
2930
|
modelTTS,
|
|
2765
|
-
modelSTT
|
|
2931
|
+
modelSTT,
|
|
2932
|
+
options
|
|
2766
2933
|
){
|
|
2767
2934
|
const tuple=this.#nextPreferenceTuple([
|
|
2768
2935
|
llmService,
|
|
@@ -2773,54 +2940,70 @@ class AI {
|
|
|
2773
2940
|
modelSTT
|
|
2774
2941
|
]);
|
|
2775
2942
|
this.#assertValidProviderTuple(tuple);
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2943
|
+
const operation=this.#beginProviderSelection(normalizeAISelectionOptions(options));
|
|
2944
|
+
try{
|
|
2945
|
+
this.#assertProviderSelection(operation);
|
|
2946
|
+
await this.#supersedeBrowserSpeechForRouteChange(operation);
|
|
2947
|
+
this.#assertProviderSelection(operation);
|
|
2948
|
+
this.#invalidateSpeechControl();
|
|
2949
|
+
await this.#unloadProviderRolesForTransition(operation);
|
|
2950
|
+
this.#assertProviderSelection(operation);
|
|
2951
|
+
this.#ensureBuiltInLLMProvider(tuple[0],operation);
|
|
2952
|
+
this.#ensureBuiltInSpeechProvider('stt',tuple[1],operation);
|
|
2953
|
+
this.#ensureBuiltInSpeechProvider('tts',tuple[2],operation);
|
|
2954
|
+
this.#assertProviderSelection(operation);
|
|
2955
|
+
this.#providerRuntime.configure(this.#routesFromPreferenceTuple(tuple));
|
|
2956
|
+
this.#commitProviderSelection(operation,tuple);
|
|
2957
|
+
this.#releaseInactiveBuiltInLLMProviders(tuple[0],operation);
|
|
2958
|
+
this.#assertProviderSelection(operation);
|
|
2959
|
+
this.#releaseInactiveBuiltInSpeechProviders({
|
|
2960
|
+
stt:tuple[1],
|
|
2961
|
+
tts:tuple[2]
|
|
2962
|
+
},operation);
|
|
2963
|
+
this.#assertProviderSelection(operation);
|
|
2964
|
+
await this.#reconcileBuiltInLLMReadiness(operation);
|
|
2965
|
+
this.#assertProviderSelection(operation);
|
|
2966
|
+
await this.#reconcileBuiltInSpeechReadiness();
|
|
2967
|
+
this.#assertProviderSelection(operation);
|
|
2968
|
+
return this.#providerRuntime.status();
|
|
2969
|
+
}finally{
|
|
2970
|
+
this.#finishProviderSelection(operation);
|
|
2971
|
+
}
|
|
2792
2972
|
}
|
|
2793
2973
|
|
|
2794
|
-
async transitionProviders(selections){
|
|
2974
|
+
async transitionProviders(selections,options){
|
|
2795
2975
|
const prepared=this.#providerRuntime.validateConfiguration(selections);
|
|
2796
2976
|
this.#assertDeviceSpeechConfiguration(prepared);
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
'
|
|
2807
|
-
prepared.tts.default?.providerId
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2977
|
+
const operation=this.#beginProviderSelection(normalizeAISelectionOptions(options));
|
|
2978
|
+
try{
|
|
2979
|
+
this.#assertProviderSelection(operation);
|
|
2980
|
+
await this.#supersedeBrowserSpeechForRouteChange(operation);
|
|
2981
|
+
this.#assertProviderSelection(operation);
|
|
2982
|
+
this.#invalidateSpeechControl();
|
|
2983
|
+
await this.#unloadProviderRolesForTransition(operation);
|
|
2984
|
+
this.#assertProviderSelection(operation);
|
|
2985
|
+
this.#ensureBuiltInLLMProvider(prepared.llm.default?.providerId,operation);
|
|
2986
|
+
this.#ensureBuiltInSpeechProvider('stt',prepared.stt.default?.providerId,operation);
|
|
2987
|
+
this.#ensureBuiltInSpeechProvider('tts',prepared.tts.default?.providerId,operation);
|
|
2988
|
+
this.#assertRegisteredBuiltInRoutes(prepared);
|
|
2989
|
+
this.#assertProviderSelection(operation);
|
|
2990
|
+
const configured=this.#providerRuntime.configure(prepared);
|
|
2991
|
+
this.#commitProviderSelection(operation,this.#tupleFromProviderRoutes(configured));
|
|
2992
|
+
this.#releaseInactiveBuiltInLLMProviders(configured.llm.default?.providerId,operation);
|
|
2993
|
+
this.#assertProviderSelection(operation);
|
|
2994
|
+
this.#releaseInactiveBuiltInSpeechProviders({
|
|
2995
|
+
stt:configured.stt.default?.providerId,
|
|
2996
|
+
tts:configured.tts.default?.providerId
|
|
2997
|
+
},operation);
|
|
2998
|
+
this.#assertProviderSelection(operation);
|
|
2999
|
+
await this.#reconcileBuiltInLLMReadiness(operation);
|
|
3000
|
+
this.#assertProviderSelection(operation);
|
|
3001
|
+
await this.#reconcileBuiltInSpeechReadiness();
|
|
3002
|
+
this.#assertProviderSelection(operation);
|
|
3003
|
+
return configured;
|
|
3004
|
+
}finally{
|
|
3005
|
+
this.#finishProviderSelection(operation);
|
|
3006
|
+
}
|
|
2824
3007
|
}
|
|
2825
3008
|
|
|
2826
3009
|
async transitionSpeechProviders(selections){
|
|
@@ -3061,10 +3244,13 @@ class AI {
|
|
|
3061
3244
|
);
|
|
3062
3245
|
}
|
|
3063
3246
|
|
|
3064
|
-
async #supersedeBrowserSpeechForRouteChange(){
|
|
3247
|
+
async #supersedeBrowserSpeechForRouteChange(operation=null){
|
|
3065
3248
|
for(const role of [...this.#configuredSpeechProviders.keys()]){
|
|
3249
|
+
if(operation)this.#assertProviderSelection(operation);
|
|
3066
3250
|
await this.configureSpeechProvider(role,null);
|
|
3251
|
+
if(operation)this.#assertProviderSelection(operation);
|
|
3067
3252
|
}
|
|
3253
|
+
if(operation)this.#assertProviderSelection(operation);
|
|
3068
3254
|
if(!this.#browserSpeechConfigurationRecord
|
|
3069
3255
|
&&this.#browserSpeechRetiredRecords.size===0){
|
|
3070
3256
|
return false;
|
|
@@ -304,8 +304,11 @@ function copyRoleRecord(role, record) {
|
|
|
304
304
|
}
|
|
305
305
|
const pendingRecovery = ['recovering', 'error'].includes(record.state)
|
|
306
306
|
&& record.operationId !== null;
|
|
307
|
-
|
|
308
|
-
|
|
307
|
+
const pendingUnload = record.state === 'unloading'
|
|
308
|
+
&& record.loaded && record.operationId !== null;
|
|
309
|
+
if (record.busy && !pendingRecovery && !pendingUnload
|
|
310
|
+
&& (record.state !== 'ready' || !record.loaded)) {
|
|
311
|
+
fail('a busy role must be ready and loaded, retain its recovering/error operation, or retain its loaded unloading operation.');
|
|
309
312
|
}
|
|
310
313
|
if (record.state === 'error' && error === null) {
|
|
311
314
|
fail('an error role state must include error details.');
|
|
@@ -98,17 +98,20 @@ async function ensureLockDirectory(workspace){
|
|
|
98
98
|
return physicalDirectory(directory,'Workspace operation-lock directory',{create:true});
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function
|
|
101
|
+
function ownerIsAbsent(pid){
|
|
102
102
|
if(!is.safeInteger(pid)||pid<1)return false;
|
|
103
103
|
try{
|
|
104
104
|
process.kill(pid,0);
|
|
105
|
-
return
|
|
105
|
+
return false;
|
|
106
106
|
}catch(error){
|
|
107
|
-
|
|
107
|
+
if(error?.code==='ESRCH')return true;
|
|
108
|
+
const busyError=workspaceBusy('The recorded Arcane workspace operation owner could not be determined.');
|
|
109
|
+
busyError.cause=error;
|
|
110
|
+
throw busyError;
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
113
|
|
|
111
|
-
function lockDocument(value,{workspaceRoot
|
|
114
|
+
function lockDocument(value,{workspaceRoot}={}){
|
|
112
115
|
if(!value||!is.object(value)||is.array(value)
|
|
113
116
|
||value.schemaVersion!==1
|
|
114
117
|
||value.kind!=='arcane-workspace-operation-lock'
|
|
@@ -124,7 +127,7 @@ function lockDocument(value,{workspaceRoot,now=Date.now()}={}){
|
|
|
124
127
|
}
|
|
125
128
|
const expiresAt=Date.parse(value.expiresAt);
|
|
126
129
|
if(!is.finite(expiresAt))return null;
|
|
127
|
-
return
|
|
130
|
+
return value;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
async function readLock(lockPath,workspaceRoot){
|
|
@@ -145,16 +148,24 @@ async function readLock(lockPath,workspaceRoot){
|
|
|
145
148
|
return lockDocument(value,{workspaceRoot});
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
async function
|
|
151
|
+
async function recoverAbsentOwnerLock(lockPath,workspaceRoot,onEvent,signal){
|
|
152
|
+
throwIfAborted(signal);
|
|
149
153
|
const document=await readLock(lockPath,workspaceRoot);
|
|
150
|
-
|
|
154
|
+
throwIfAborted(signal);
|
|
155
|
+
if(!document||!ownerIsAbsent(document.owner.pid))return false;
|
|
151
156
|
await onEvent?.({
|
|
152
157
|
type:'workspace.operation.stale-recovered',
|
|
153
158
|
workspaceRoot,
|
|
154
159
|
lockPath,
|
|
155
|
-
previousOwner:document.owner,
|
|
160
|
+
previousOwner:{...document.owner},
|
|
156
161
|
previousOperation:document.operation
|
|
157
162
|
});
|
|
163
|
+
throwIfAborted(signal);
|
|
164
|
+
const current=await readLock(lockPath,workspaceRoot);
|
|
165
|
+
throwIfAborted(signal);
|
|
166
|
+
// Observers may replace the record; their completion does not retain ownership.
|
|
167
|
+
if(current?.nonce!==document.nonce||current?.owner.pid!==document.owner.pid
|
|
168
|
+
||!ownerIsAbsent(current.owner.pid))return false;
|
|
158
169
|
await rm(lockPath);
|
|
159
170
|
return true;
|
|
160
171
|
}
|
|
@@ -254,15 +265,17 @@ async function acquire({
|
|
|
254
265
|
lockPath=path.join(directory.canonical,LOCK_NAME);
|
|
255
266
|
let recovered=false;
|
|
256
267
|
for(;;){
|
|
268
|
+
throwIfAborted(signal);
|
|
257
269
|
try{
|
|
258
270
|
handle=await open(lockPath,OPEN_EXCLUSIVE,0o600);
|
|
259
271
|
break;
|
|
260
272
|
}catch(error){
|
|
261
273
|
if(error?.code!=='EEXIST')throw error;
|
|
262
|
-
if(recovered||!(await
|
|
274
|
+
if(recovered||!(await recoverAbsentOwnerLock(
|
|
263
275
|
lockPath,
|
|
264
276
|
workspace.canonical,
|
|
265
|
-
onEvent
|
|
277
|
+
onEvent,
|
|
278
|
+
signal
|
|
266
279
|
))){
|
|
267
280
|
fail(
|
|
268
281
|
`Workspace is locked by another Arcane operation: ${lockPath}. `
|