arcane-os 0.90.0 → 0.91.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 +17 -0
- package/README.md +1 -1
- package/docs/reference/ai/browser-decisions.md +45 -18
- package/docs/reference/inventory/package-api.json +5 -5
- package/docs/reference/model-assets.md +63 -24
- package/docs/reference/native-decisions.md +19 -4
- package/docs/reference/sdk-api.md +43 -9
- package/package.json +1 -1
- package/src/browser-decisions-distribution.mjs +95 -0
- package/src/core/services/model-assets.mjs +256 -76
- package/src/packager/core.mjs +29 -2
- package/src/workspace-runtime.mjs +1 -79
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.91.0
|
|
4
|
+
|
|
5
|
+
- Persist complete native `modelAssets.prepare()` acquisitions across release,
|
|
6
|
+
unload, service disposal and process restart. Reuse the complete ordered
|
|
7
|
+
source selection from its selected store, with independent caller lifetimes,
|
|
8
|
+
cancellation and progress. Explicit `refresh: true` acquires a new complete
|
|
9
|
+
set while preserving earlier completed acquisitions and retained uses.
|
|
10
|
+
- Add `packageApp({browserDecisions: true})` to acquire the upstream browser
|
|
11
|
+
decision distribution inside the package stage beside the actual selected
|
|
12
|
+
SDK module, including installed aliases and relocated routes. Both module
|
|
13
|
+
formats retain complete executables and licenses; default and dry-run
|
|
14
|
+
behavior stay unchanged. Existing workspace materialization remains supported.
|
|
15
|
+
- Reuse the retained Windows host, Whisper and ONNX runtime archives; this
|
|
16
|
+
release changes shared JavaScript acquisition and packaging, not those native
|
|
17
|
+
binaries. Application adoption and native model acceptance remain separately
|
|
18
|
+
owned.
|
|
19
|
+
|
|
3
20
|
## 0.90.0
|
|
4
21
|
|
|
5
22
|
- Add optional `remoteBase` to repository workspaces, Git text snapshots and
|
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.91.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;
|
|
@@ -95,14 +95,18 @@ pipeline. Existing Wllama, cloud, speech and chat APIs are unchanged.
|
|
|
95
95
|
|
|
96
96
|
## Extension-local executables (Manifest V3)
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
When packaging the application, opt into the upstream browser distribution
|
|
99
|
+
through the public Node API:
|
|
100
100
|
|
|
101
101
|
```js
|
|
102
|
-
import {
|
|
102
|
+
import {packageApp} from 'arcane-os';
|
|
103
103
|
|
|
104
|
-
await
|
|
104
|
+
const release = await packageApp({
|
|
105
105
|
workspaceRoot: process.cwd(),
|
|
106
|
+
appId: 'moon-dashboard',
|
|
107
|
+
outputDirectory: 'dist/extension/app',
|
|
108
|
+
moduleFormat: 'native',
|
|
109
|
+
browserPwa: false,
|
|
106
110
|
browserDecisions: true
|
|
107
111
|
});
|
|
108
112
|
```
|
|
@@ -111,30 +115,50 @@ This acquires the official bundled Transformers.js `4.3.0`
|
|
|
111
115
|
`transformers.min.js`, matching ONNX Runtime
|
|
112
116
|
`1.31.0-dev.20260914-8d85527a0` `ort-wasm-simd-threaded.asyncify.mjs` and
|
|
113
117
|
`ort-wasm-simd-threaded.asyncify.wasm`, plus `TRANSFORMERS-LICENSE` and
|
|
114
|
-
`ONNX-RUNTIME-LICENSE`. The five complete files are
|
|
115
|
-
`
|
|
116
|
-
|
|
117
|
-
dependency
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
`ONNX-RUNTIME-LICENSE`. The five complete files are acquired only into the
|
|
119
|
+
package stage, under `decisions-runtime/` beside the selected SDK
|
|
120
|
+
`ai/browser-decisions.mjs`. Installed-package routes retain their actual
|
|
121
|
+
dependency path, including npm aliases: for example,
|
|
122
|
+
`node_modules/arcane-os/browser-runtime/ai/decisions-runtime/` inside
|
|
123
|
+
`release.outputRoot`. The installed dependency and application source remain
|
|
124
|
+
unchanged; this route creates no workspace-root `arcane/` projection.
|
|
125
|
+
|
|
126
|
+
Acquisition follows SDK module/asset rewriting, preserving complete upstream
|
|
127
|
+
executable content and licenses unchanged. It works with both
|
|
128
|
+
`moduleFormat: 'native'` and the default `'import-map'`. The returned `files`
|
|
129
|
+
and release manifest include the distribution; when browser PWA generation
|
|
130
|
+
is enabled, its ordinary offline include/exclude selection applies too.
|
|
131
|
+
Retain the supplied licenses with the deployment. No bundler, Node engine
|
|
132
|
+
dependency tree, model graph or tokenizer is installed by this option. Select
|
|
133
|
+
`browserDecisions: true` on each package build that should acquire the closure.
|
|
134
|
+
The default `false` and `dryRun: true` perform no optional acquisition; dry runs
|
|
135
|
+
report selected input files rather than downloaded distribution files.
|
|
136
|
+
|
|
137
|
+
Existing physical workspace projections retain
|
|
138
|
+
`materializeInstalledSdkRuntime({workspaceRoot, browserDecisions: true})`.
|
|
139
|
+
That operation stages these files under `arcane/sdk/ai/decisions-runtime/`
|
|
140
|
+
before replacing the projection, returning `directory`, `transformersVersion`,
|
|
141
|
+
`onnxRuntimeVersion` and `files` under `workspaceRuntime.browserDecisions`.
|
|
123
142
|
`materializeWorkspaceRuntimeContent()` accepts the same option and returns
|
|
124
|
-
that record
|
|
125
|
-
|
|
126
|
-
|
|
143
|
+
that record as `result.browserDecisions`. Repeat the option on each projection
|
|
144
|
+
refresh that should retain the closure. Installed-package applications use
|
|
145
|
+
`packageApp()` above, because their routes read from the installed dependency
|
|
146
|
+
rather than this separate projection.
|
|
147
|
+
|
|
148
|
+
Both operations share one distribution owner. Its fixed files are acquired
|
|
149
|
+
concurrently, once each per operation. Events
|
|
127
150
|
`workspace.decisions.started` and `workspace.decisions.progress` expose
|
|
128
151
|
`completed`/`total` file counts; each progress event also has `file` and `url`.
|
|
129
152
|
Cancellation and HTTP or observer failure abort/join pending acquisition and
|
|
130
|
-
preserve the prior runtime. An HTTP failure retains
|
|
153
|
+
preserve the prior package output or physical runtime. An HTTP failure retains
|
|
154
|
+
the complete response as
|
|
131
155
|
`error.response` (`url`, `status`, `statusText`, `headers`, `content`), including
|
|
132
156
|
inside `AggregateError.errors` when several operations fail.
|
|
133
157
|
|
|
134
158
|
Load from an extension-owned module script, using the deployed SDK path:
|
|
135
159
|
|
|
136
160
|
```js
|
|
137
|
-
import {createBrowserDecisionModel} from './arcane/
|
|
161
|
+
import {createBrowserDecisionModel} from './node_modules/arcane-os/browser-runtime/ai/browser-decisions.mjs';
|
|
138
162
|
|
|
139
163
|
const decisions = createBrowserDecisionModel({
|
|
140
164
|
family: 'laya',
|
|
@@ -147,6 +171,9 @@ const decisions = createBrowserDecisionModel({
|
|
|
147
171
|
await decisions.load();
|
|
148
172
|
```
|
|
149
173
|
|
|
174
|
+
Use the actual deployed alias path when the dependency has another name;
|
|
175
|
+
physical projections instead use `./arcane/sdk/ai/browser-decisions.mjs`.
|
|
176
|
+
|
|
150
177
|
For Julia-1, change the family/model to `julia` /
|
|
151
178
|
`SupersonicLabs/Julia-1-ONNX` and select `dtype: 'fp32'`. Model and tokenizer
|
|
152
179
|
data remain upstream resources served through the same store, preserving its
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"sdkVersion": "0.
|
|
3
|
+
"sdkVersion": "0.90.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",
|
|
@@ -441,10 +441,10 @@
|
|
|
441
441
|
"entrypoints": ["arcane-os/core/model-assets"],
|
|
442
442
|
"primaryImport": "arcane-os/core/model-assets",
|
|
443
443
|
"group": "Core model assets",
|
|
444
|
-
"summary": "Owns
|
|
444
|
+
"summary": "Owns persistent native model acquisitions, temporary browser projections and actual engine retain lifetimes.",
|
|
445
445
|
"availability": "Node Core with an application-selected working directory",
|
|
446
446
|
"protocol": "arcane/1 modelAssets.open/write/complete/release/status and modelAssets.state",
|
|
447
|
-
"normalization": "Returns current/prepare/retain/release/dispose and Core service lifecycle. Explicit prepare({id,workingDirectory,members:[{path,url}],signal,onProgress})
|
|
447
|
+
"normalization": "Returns current/prepare/retain/release/dispose and Core service lifecycle. Explicit prepare({id,workingDirectory,members:[{path,url}],signal,onProgress,refresh=false}) reuses complete exact ordered source selections stored beneath workingDirectory/model-assets or streams every selected upstream member concurrently. Complete acquisitions survive release, disposal and restart. refresh:true acquires a separate complete set without overwriting retained originals; ordinary reuse makes no network freshness claim. Matching same-process callers share acquisition with independent callbacks/cancellation, and the final interested cancellation joins unfinished I/O before removing only that attempt. Separate processes may fetch independently. release(id) relinquishes live preparation ownership; actual native retain lifetimes remain. Browser open/write/complete projections retain temporary cleanup after final native use. Startup downloads nothing; no migration, eviction or partial-transfer resumption."
|
|
448
448
|
},
|
|
449
449
|
{
|
|
450
450
|
"id": "core-model-assets-service:default",
|
|
@@ -654,7 +654,7 @@
|
|
|
654
654
|
"summary": "Explicit-load Laya and Julia-1 decisions with caller-selected precision, complete inputs and graph results.",
|
|
655
655
|
"availability": "Browser module Workers and the selected Transformers.js backend; import/construction starts no model load",
|
|
656
656
|
"protocol": "Caller-owned state/question/options rows; five-input decision graph; native Worker request/result lifecycle",
|
|
657
|
-
"normalization": "Full field tokenization, raw logits and unrounded softmax; dtype defaults to fp16 for Laya and fp32 for Julia, with explicit values forwarded unchanged. Optional existing SDK store routes actual loader resources through fetchResource outside cloned Worker configuration. By default stored activation also materializes the unchanged runtime entry as a JavaScript object URL, retains its configured URL for later loads and revokes the object URL after Worker termination. Explicit runtime.local:true instead imports same-deployment moduleUrl and wasmPaths.mjs/wasm directly, defaulting to ai/decisions-runtime
|
|
657
|
+
"normalization": "Full field tokenization, raw logits and unrounded softmax; dtype defaults to fp16 for Laya and fp32 for Julia, with explicit values forwarded unchanged. Optional existing SDK store routes actual loader resources through fetchResource outside cloned Worker configuration. By default stored activation also materializes the unchanged runtime entry as a JavaScript object URL, retains its configured URL for later loads and revokes the object URL after Worker termination. Explicit runtime.local:true instead imports same-deployment moduleUrl and wasmPaths.mjs/wasm directly, defaulting to adjacent ai/decisions-runtime acquired by packageApp({browserDecisions:true}) for installed-package routes or by the existing physical materializer option; only those exact executable URLs bypass the model store. It disables upstream executable WASM caching/proxy and selects one WASM thread, preserving the requested inference backend. Stored mode disables competing model caches, preserves complete HTTP responses and joins resource cleanup before outstanding calls settle. Default runtime-entry HTTP failures retain error.response; custom stored entries must support an object URL and changed import.meta.url. Omission retains existing browser behavior; no decision-result persistence or precision fallback"
|
|
658
658
|
},
|
|
659
659
|
{
|
|
660
660
|
"id": "root:acceptWebSocket",
|
|
@@ -1858,7 +1858,7 @@
|
|
|
1858
1858
|
"summary": "Builds one complete app release with browser selection by default. Nonbrowser targets add explicit nativeResources through a late complete-content copy and separate native input output; moduleFormat is independent. Adapters receive actual target and effective browserPwa. Explicit operation outputDirectory wins; staged replacement preserves source and unrelated outputs. No tests or checks run automatically.",
|
|
1859
1859
|
"availability": "Node",
|
|
1860
1860
|
"protocol": "SDK packager and deterministic bundle contract",
|
|
1861
|
-
"normalization": "
|
|
1861
|
+
"normalization": "Complete selected release content. Optional browserDecisions:true acquires bundled Transformers.js 4.3.0 and matched ONNX Runtime 1.31.0-dev.20260914-8d85527a0 asyncify module/WASM plus licenses beside the selected SDK browser-decisions module in staging only, following installed routes and aliases. Acquisition follows native-module and asset rewriting, preserves complete upstream content, and joins the final release inventory and enabled PWA offline selection. Default false and dryRun perform no optional acquisition. Existing workspace.decisions started/progress events, cancellation, full HTTP errors and joined failure cleanup are preserved; prior output, source and installed dependencies remain unchanged on acquisition failure. Repeat the option per selected package build."
|
|
1862
1862
|
},
|
|
1863
1863
|
{
|
|
1864
1864
|
"id": "root:packageApplication",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Native
|
|
1
|
+
# Native model acquisitions and working projections
|
|
2
2
|
|
|
3
3
|
Browser preparation and native retain/release ownership were first published in `arcane-os@0.61.0`. Browser
|
|
4
4
|
preparation is exported by `arcane-os/ai/core-model-assets`, and the native
|
|
@@ -110,29 +110,65 @@ try {
|
|
|
110
110
|
}
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
`prepare({id,workingDirectory,members,signal,onProgress})`
|
|
114
|
-
|
|
115
|
-
`path` and selected `url`; the caller
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
whole-model buffer or base64 conversion. No model or runtime
|
|
119
|
-
downloaded by service startup. The result
|
|
120
|
-
ordered `members: [{path,nativePath}]`.
|
|
113
|
+
`prepare({id,workingDirectory,members,signal,onProgress,refresh=false})` reuses
|
|
114
|
+
a complete native acquisition or fetches the explicitly selected members.
|
|
115
|
+
Every member has its original relative `path` and selected `url`; the caller
|
|
116
|
+
supplies the complete companion-file set. Independent members download
|
|
117
|
+
concurrently. Each complete response stream is written directly to its native
|
|
118
|
+
file, without a whole-model buffer or base64 conversion. No model or runtime
|
|
119
|
+
is selected or downloaded by service startup. The result remains the ready
|
|
120
|
+
projection snapshot with ordered `members: [{path,nativePath}]`.
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
Complete acquisitions persist under `workingDirectory/model-assets/` across
|
|
123
|
+
release, native unload, service disposal and process restart. Relative working
|
|
124
|
+
directories resolve from `appRoot`. Reuse matches the entire ordered
|
|
125
|
+
`[{path,url}]` selection in that actual store; the operation `id` is a separate
|
|
126
|
+
live lifetime, not the stored selection. Different URLs, relative paths, member
|
|
127
|
+
order or stores select separate acquisitions. Original member names and content
|
|
128
|
+
stay unchanged, and native engines use those files directly. Model, revision,
|
|
129
|
+
precision and device choices remain with their existing owners.
|
|
130
|
+
|
|
131
|
+
Ordinary preparation performs no network freshness check for a completed
|
|
132
|
+
selection. To acquire a mutable source again, explicitly pass `refresh:true` to
|
|
133
|
+
this native `prepare` method. Refresh fetches a new complete set in its own
|
|
134
|
+
directory and keeps earlier complete acquisitions and retained engine uses
|
|
135
|
+
untouched. Later ordinary preparations reuse the newer completed selection.
|
|
136
|
+
A failed refresh leaves the earlier completed acquisition available. Refresh
|
|
137
|
+
does not change the selected URLs or imply upstream revision discovery.
|
|
138
|
+
|
|
139
|
+
Progress uses `open`, `download`, `complete`, `reuse` and `ready` phases, with
|
|
123
140
|
`completed`, `total` and `unit:'files'`; member progress additionally includes
|
|
124
141
|
`memberIndex` and `path`. Progress callbacks may return a promise, which the
|
|
125
|
-
preparation observes.
|
|
126
|
-
|
|
142
|
+
preparation observes in order. Each caller owns its callbacks; a slow or failed
|
|
143
|
+
callback does not hold another caller's transfer or result. Callbacks must not
|
|
144
|
+
await cleanup of the preparation currently calling them. Callback failures
|
|
145
|
+
reject that caller's preparation. HTTP failures
|
|
127
146
|
retain `url`, `status` and the complete response text in `response`.
|
|
128
147
|
|
|
129
148
|
`release(id)` relinquishes preparation ownership without a request signal.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
149
|
+
Same-process callers sharing the actual store, complete source selection and
|
|
150
|
+
refresh mode share an acquisition, including across service instances. They
|
|
151
|
+
keep independent operation IDs, retain handles, cancellation and callbacks.
|
|
152
|
+
Cancelling, releasing or disposing one interested caller leaves the others
|
|
153
|
+
running. The final interested caller aborts the acquisition and joins its
|
|
154
|
+
streams, writes and closes before removing only its unfinished attempt. Original
|
|
155
|
+
failures and cleanup failures remain observable. A completed acquisition stays
|
|
156
|
+
available even if a caller cancels or its callback fails after completion.
|
|
157
|
+
|
|
158
|
+
The service records completion only after every selected response and file
|
|
159
|
+
close finishes. That record is ordinary cache state describing the selected
|
|
160
|
+
sources and completed acquisition. A missing original member requires another
|
|
161
|
+
complete acquisition. Failed attempts are removed after their owned I/O drains;
|
|
162
|
+
there is no partial-transfer resumption. An abruptly interrupted process can
|
|
163
|
+
leave an unfinished directory, which later calls ignore without sweeping it.
|
|
164
|
+
There is no automatic migration, eviction or removal of completed acquisitions.
|
|
165
|
+
|
|
166
|
+
Independent processes use disjoint attempt directories and may fetch the same
|
|
167
|
+
selection concurrently. They do not overwrite each other's files or coordinate
|
|
168
|
+
a cross-process acquisition lock. Concurrent completions can produce multiple
|
|
169
|
+
reusable sets; either complete result may be selected when their completion
|
|
170
|
+
order ties. Applications may retain their existing shared Core owner without
|
|
171
|
+
introducing an additional host or application-local cache copy.
|
|
136
172
|
|
|
137
173
|
```javascript
|
|
138
174
|
import {createModelAssetService} from 'arcane-os/core/model-assets';
|
|
@@ -158,15 +194,18 @@ release. Multiple contexts can retain one prepared set, and repeated inference
|
|
|
158
194
|
on a retained context reuses its files.
|
|
159
195
|
|
|
160
196
|
The browser's `projection.release()` relinquishes preparation ownership. It
|
|
161
|
-
does not delete files retained by a native engine.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
197
|
+
does not delete files retained by a native engine. For browser `open`/`write`/
|
|
198
|
+
`complete` projections, deletion follows the final native release and removes
|
|
199
|
+
only that operation's temporary working directory; DBOPFS originals remain
|
|
200
|
+
untouched. Native `prepare` acquisitions remain stored after their final use.
|
|
201
|
+
Core closes independent services concurrently, so the model-assets service
|
|
202
|
+
waits for all native retain handles during disposal instead of assuming service
|
|
203
|
+
order. Owners must release their handles after joining their actual engine
|
|
204
|
+
lifetime.
|
|
166
205
|
|
|
167
206
|
Closing a browser accessor does not by itself establish native unload.
|
|
168
207
|
Explicitly release unused prepared projections. Core disposal also releases
|
|
169
|
-
preparation ownership and
|
|
208
|
+
preparation ownership and ends the live records after retained engine uses end.
|
|
170
209
|
|
|
171
210
|
### Native Laya decisions
|
|
172
211
|
|
|
@@ -179,10 +179,25 @@ create a fresh projection from the stored originals before loading again.
|
|
|
179
179
|
## Existing upstream preparation
|
|
180
180
|
|
|
181
181
|
`model` selects the upstream Hugging Face repository and `revision` its
|
|
182
|
-
revision.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
182
|
+
revision. On explicit `load()`, the existing model-assets owner reuses complete
|
|
183
|
+
native acquisitions under the selected `workingDirectory/model-assets/`, or
|
|
184
|
+
fetches the complete selected members concurrently when absent. Complete
|
|
185
|
+
acquisitions survive unload, disposal and application restart. Reuse matches
|
|
186
|
+
the entire ordered original path/URL selection; it never substitutes another
|
|
187
|
+
model, revision, precision or device. Supplied projections and caller-owned
|
|
188
|
+
`paths` retain their existing precedence and lifecycle.
|
|
189
|
+
|
|
190
|
+
Ordinary preparation reuses the selected URLs without checking whether mutable
|
|
191
|
+
upstream content changed. Explicit reacquisition is available through native
|
|
192
|
+
[`modelAssets.prepare({refresh:true,...})`](model-assets.md#native-ownership),
|
|
193
|
+
whose ready projection can be supplied through the existing
|
|
194
|
+
`assetProjectionId`/`resourcePaths` contract. It preserves earlier complete
|
|
195
|
+
acquisitions and active native retains. Incomplete attempts are not reused or
|
|
196
|
+
resumed; separate processes may perform duplicate acquisitions in disjoint
|
|
197
|
+
directories. The shared model-assets owner handles storage without an
|
|
198
|
+
application-local cache copy or a second model store.
|
|
199
|
+
|
|
200
|
+
The approved `@huggingface/tokenizers@0.2.0` dependency reads the
|
|
186
201
|
selected tokenizer files in its Worker. No Transformers or second ONNX runtime
|
|
187
202
|
is loaded by this API.
|
|
188
203
|
|
|
@@ -1044,6 +1044,25 @@ ordinary browser output remain unchanged. See
|
|
|
1044
1044
|
[native-module package output](protocols.md#native-module-package-output)
|
|
1045
1045
|
for resource handling and the static transformation boundary.
|
|
1046
1046
|
|
|
1047
|
+
`browserDecisions` defaults to `false`. Explicit `browserDecisions: true`
|
|
1048
|
+
acquires the bundled Transformers.js 4.3.0 entry, matching ONNX Runtime
|
|
1049
|
+
1.31.0-dev.20260914-8d85527a0 asyncify module/WASM and both licenses into
|
|
1050
|
+
the package stage beside the selected SDK `ai/browser-decisions.mjs`, under
|
|
1051
|
+
`decisions-runtime/`. Installed-package routes retain their configured
|
|
1052
|
+
dependency path, including aliases; source and installed dependencies remain
|
|
1053
|
+
unchanged, with no workspace-root `arcane/` projection. The selected SDK decision
|
|
1054
|
+
module must be included in the package. Acquisition happens after native-module
|
|
1055
|
+
and asset rewriting, preserving upstream content unchanged in both module
|
|
1056
|
+
formats. The distribution joins the returned and release-manifest inventories
|
|
1057
|
+
and any enabled PWA's ordinary offline selection. The default and dry runs
|
|
1058
|
+
perform no optional acquisition; dry runs report selected inputs. Repeat this
|
|
1059
|
+
option on each package build that should acquire the closure. Cancellation and
|
|
1060
|
+
HTTP or observer failures abort/join acquisition before stage cleanup and
|
|
1061
|
+
preserve previous output. The existing `workspace.decisions.started` and
|
|
1062
|
+
`workspace.decisions.progress` events report file progress. See the
|
|
1063
|
+
[extension-local decision guide](ai/browser-decisions.md#extension-local-executables-manifest-v3)
|
|
1064
|
+
for `runtime.local: true`, upstream versions and complete error details.
|
|
1065
|
+
|
|
1047
1066
|
An operation may override `outputDirectory` without changing the saved app
|
|
1048
1067
|
configuration. For example, package a complete application beneath an
|
|
1049
1068
|
application-owned extension manifest and launcher:
|
|
@@ -1875,6 +1894,11 @@ module/WASM and license files from their upstream distributions into
|
|
|
1875
1894
|
`transformersVersion`, `onnxRuntimeVersion` and `files` under
|
|
1876
1895
|
`workspaceRuntime.browserDecisions`. The default `false` performs no optional
|
|
1877
1896
|
acquisition. Select this option on each refresh that should retain those files.
|
|
1897
|
+
For applications with installed-package routes, use
|
|
1898
|
+
`packageApp({workspaceRoot, appId, browserDecisions: true})` to place the
|
|
1899
|
+
distribution beside the actual deployed SDK module. This physical materializer
|
|
1900
|
+
continues to own only the separate `arcane/` projection; it does not populate or
|
|
1901
|
+
modify `node_modules`.
|
|
1878
1902
|
The [extension-local decision guide](ai/browser-decisions.md#extension-local-executables-manifest-v3)
|
|
1879
1903
|
describes `runtime.local: true`, complete resource ownership and MV3 usage.
|
|
1880
1904
|
|
|
@@ -9012,14 +9036,22 @@ directory and complete member files.
|
|
|
9012
9036
|
|
|
9013
9037
|
`current()` returns `{closing,projections}`. `retain(id)` accepts a completed
|
|
9014
9038
|
projection and returns `{id,directory,members,release}` for a native owner.
|
|
9015
|
-
Native `prepare({id,workingDirectory,members,signal,onProgress})`
|
|
9016
|
-
ordered `{path,url}` members and returns their ready projection.
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9039
|
+
Native `prepare({id,workingDirectory,members,signal,onProgress,refresh=false})`
|
|
9040
|
+
accepts ordered `{path,url}` members and returns their ready projection.
|
|
9041
|
+
Complete native acquisitions persist beneath `workingDirectory/model-assets/`
|
|
9042
|
+
through unload, disposal and restart; ordinary calls reuse the exact complete
|
|
9043
|
+
ordered source selection without a network freshness check. Explicit
|
|
9044
|
+
`refresh:true` acquires a separate complete set without replacing active files.
|
|
9045
|
+
Independent members transfer concurrently and progress reports phases and
|
|
9046
|
+
completed files. Matching same-process preparations share acquisition with
|
|
9047
|
+
independent callbacks and cancellation; the last interested cancellation joins
|
|
9048
|
+
unfinished I/O and removes only that attempt. Separate processes may acquire
|
|
9049
|
+
the same selection independently. Startup downloads nothing.
|
|
9050
|
+
`release(id)` relinquishes live preparation ownership. Native retains continue
|
|
9051
|
+
through actual engine release; completed native acquisitions remain stored.
|
|
9052
|
+
Browser `open/write/complete` projections retain their temporary lifetime and
|
|
9053
|
+
are removed only after all native uses end. See
|
|
9054
|
+
[native preparation](model-assets.md#native-ownership).
|
|
9023
9055
|
`dispose()` releases preparation ownership and joins file writes, closes and
|
|
9024
9056
|
all native retain lifetimes before cleanup. The native owner releases its
|
|
9025
9057
|
handle only after its actual context/worker use ends. Original stored model
|
|
@@ -10146,7 +10178,9 @@ Cancellation terminates that client's Worker and rejects its in-flight work.
|
|
|
10146
10178
|
No decisions are inserted into chat history or durable storage.
|
|
10147
10179
|
|
|
10148
10180
|
For explicitly selected extension-local execution, supply `runtime: {local:
|
|
10149
|
-
true}` after
|
|
10181
|
+
true}` after `packageApp({workspaceRoot, appId, browserDecisions: true})` for
|
|
10182
|
+
installed-package routes, or after opting into the existing physical runtime
|
|
10183
|
+
materializer. The SDK directly
|
|
10150
10184
|
imports its deployment-local `decisions-runtime/transformers.min.js` and
|
|
10151
10185
|
matched asyncify module/WASM. Explicit `runtime.moduleUrl` and
|
|
10152
10186
|
`runtime.wasmPaths: {mjs, wasm}` may select other compatible files in that same
|
package/package.json
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import Is from 'strong-type';
|
|
2
|
+
import {mkdir, writeFile} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const is = new Is(false);
|
|
6
|
+
|
|
7
|
+
function throwIfAborted(signal) {
|
|
8
|
+
if (!signal?.aborted) return;
|
|
9
|
+
const error = signal.reason instanceof Error ? signal.reason : new Error('Operation cancelled.');
|
|
10
|
+
error.code = error.code || 'ARCANE_CANCELLED';
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function emit(onEvent, event) {
|
|
15
|
+
if (is.function(onEvent)) await onEvent(event);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function materializeBrowserDecisions(destination, signal, onEvent) {
|
|
19
|
+
const transformers = 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0/';
|
|
20
|
+
const onnx = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.31.0-dev.20260914-8d85527a0/';
|
|
21
|
+
const files = [
|
|
22
|
+
{name: 'transformers.min.js', url: `${transformers}dist/transformers.min.js`},
|
|
23
|
+
{name: 'ort-wasm-simd-threaded.asyncify.mjs', url: `${onnx}dist/ort-wasm-simd-threaded.asyncify.mjs`},
|
|
24
|
+
{name: 'ort-wasm-simd-threaded.asyncify.wasm', url: `${onnx}dist/ort-wasm-simd-threaded.asyncify.wasm`},
|
|
25
|
+
{name: 'TRANSFORMERS-LICENSE', url: `${transformers}LICENSE`},
|
|
26
|
+
{name: 'ONNX-RUNTIME-LICENSE', url: `${onnx}LICENSE`}
|
|
27
|
+
];
|
|
28
|
+
const controller = new AbortController();
|
|
29
|
+
function cancelDistribution() {
|
|
30
|
+
controller.abort(signal.reason);
|
|
31
|
+
}
|
|
32
|
+
signal?.addEventListener('abort', cancelDistribution, {once: true});
|
|
33
|
+
if (signal?.aborted) cancelDistribution();
|
|
34
|
+
let completed = 0;
|
|
35
|
+
try {
|
|
36
|
+
throwIfAborted(controller.signal);
|
|
37
|
+
await emit(
|
|
38
|
+
onEvent,
|
|
39
|
+
{type: 'workspace.decisions.started', completed, total: files.length}
|
|
40
|
+
);
|
|
41
|
+
throwIfAborted(controller.signal);
|
|
42
|
+
await mkdir(destination, {recursive: true});
|
|
43
|
+
async function acquireDistributionFile(file) {
|
|
44
|
+
try {
|
|
45
|
+
throwIfAborted(controller.signal);
|
|
46
|
+
const response = await fetch(file.url, {signal: controller.signal});
|
|
47
|
+
const content = new Uint8Array(await response.arrayBuffer());
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
const error = new Error(`Browser decision distribution returned HTTP ${response.status} ${response.statusText}.`);
|
|
50
|
+
error.code = 'ARCANE_DECISION_DISTRIBUTION_DOWNLOAD_FAILED';
|
|
51
|
+
error.response = {
|
|
52
|
+
url: response.url,
|
|
53
|
+
status: response.status,
|
|
54
|
+
statusText: response.statusText,
|
|
55
|
+
headers: [...response.headers],
|
|
56
|
+
content
|
|
57
|
+
};
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
throwIfAborted(controller.signal);
|
|
61
|
+
// Upstream distribution content is opaque, including its own
|
|
62
|
+
// imports and license text. No SDK source rewriting applies.
|
|
63
|
+
await writeFile(path.join(destination, file.name), content);
|
|
64
|
+
throwIfAborted(controller.signal);
|
|
65
|
+
completed += 1;
|
|
66
|
+
await emit(
|
|
67
|
+
onEvent,
|
|
68
|
+
{type: 'workspace.decisions.progress', file: file.name, url: file.url, completed, total: files.length}
|
|
69
|
+
);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
controller.abort(error);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// One fixed distribution, acquired once for the selected projection or
|
|
76
|
+
// package; no model/app/architecture multiplier or installation script.
|
|
77
|
+
const outcomes = await Promise.allSettled(files.map(acquireDistributionFile));
|
|
78
|
+
const failures = new Set();
|
|
79
|
+
for (const outcome of outcomes) {
|
|
80
|
+
if (outcome.status === 'rejected') failures.add(outcome.reason);
|
|
81
|
+
}
|
|
82
|
+
if (failures.size === 1) throw [...failures][0];
|
|
83
|
+
if (failures.size > 1) {
|
|
84
|
+
throw new AggregateError([...failures], 'Browser decision distribution acquisition failed.');
|
|
85
|
+
}
|
|
86
|
+
throwIfAborted(controller.signal);
|
|
87
|
+
} finally {
|
|
88
|
+
signal?.removeEventListener('abort', cancelDistribution);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
transformersVersion: '4.3.0',
|
|
92
|
+
onnxRuntimeVersion: '1.31.0-dev.20260914-8d85527a0',
|
|
93
|
+
files: files.map(function distributionFilename(file) { return file.name; })
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -1,16 +1,231 @@
|
|
|
1
|
-
import {mkdir, mkdtemp, open, rm} from 'node:fs/promises';
|
|
1
|
+
import {mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, writeFile} from 'node:fs/promises';
|
|
2
2
|
import {Buffer} from 'node:buffer';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import Is from 'strong-type';
|
|
5
5
|
import {CoreError, serializeCoreError} from '../../../browser-runtime/core/contracts.mjs';
|
|
6
6
|
|
|
7
7
|
const is = new Is(false);
|
|
8
|
+
const nativeAcquisitions = new Map();
|
|
9
|
+
let nativeAcquisitionOrder = 0;
|
|
8
10
|
|
|
9
11
|
function failure(code, message) {
|
|
10
12
|
return new CoreError({code, message});
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
function memberLocation(directory, member) {
|
|
16
|
+
const nativePath = path.resolve(directory, member.path);
|
|
17
|
+
const relative = path.relative(directory, nativePath);
|
|
18
|
+
if (path.isAbsolute(member.path) || !relative || relative === '..'
|
|
19
|
+
|| relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
20
|
+
throw new TypeError('Model asset member paths must be relative files within their projection.');
|
|
21
|
+
}
|
|
22
|
+
return {path: member.path, nativePath};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sameMembers(left, right) {
|
|
26
|
+
return is.array(left) && left.length === right.length
|
|
27
|
+
&& left.every(function sameMember(member, index) {
|
|
28
|
+
return member.path === right[index].path && member.url === right[index].url;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function storedAcquisition(directory, members, signal) {
|
|
33
|
+
let latest;
|
|
34
|
+
for (const entry of await readdir(directory, {withFileTypes: true})) {
|
|
35
|
+
signal.throwIfAborted();
|
|
36
|
+
if (!entry.isDirectory() || !entry.name.startsWith('acquisition-')) continue;
|
|
37
|
+
const root = path.join(directory, entry.name);
|
|
38
|
+
let selected;
|
|
39
|
+
try { selected = JSON.parse(await readFile(path.join(root, 'acquisition.json'), {encoding: 'utf8', signal})); }
|
|
40
|
+
catch (error) { if (error.code === 'ENOENT') continue; throw error; }
|
|
41
|
+
if (!sameMembers(selected.members, members)) continue;
|
|
42
|
+
nativeAcquisitionOrder = Math.max(nativeAcquisitionOrder, selected.order);
|
|
43
|
+
const files = path.join(root, 'files');
|
|
44
|
+
const locations = members.map(function originalLocation(member) { return memberLocation(files, member); });
|
|
45
|
+
let available = true;
|
|
46
|
+
for (const member of locations) {
|
|
47
|
+
signal.throwIfAborted();
|
|
48
|
+
try { if (!(await stat(member.nativePath)).isFile()) available = false; }
|
|
49
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; available = false; }
|
|
50
|
+
}
|
|
51
|
+
if (available && (!latest || selected.order > latest.order
|
|
52
|
+
|| (selected.order === latest.order && entry.name > latest.name))) {
|
|
53
|
+
latest = {name: entry.name, order: selected.order, directory: files, members: locations};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return latest ? {directory: latest.directory, members: latest.members} : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function acquireNativeMembers(entry) {
|
|
60
|
+
const {directory, members, controller} = entry;
|
|
61
|
+
const {signal} = controller;
|
|
62
|
+
let attempt;
|
|
63
|
+
let completed = 0;
|
|
64
|
+
let committed = false;
|
|
65
|
+
let firstFailure;
|
|
66
|
+
function progress(phase, memberIndex) {
|
|
67
|
+
entry.publish({phase, completed, total: members.length, unit: 'files',
|
|
68
|
+
...(memberIndex === undefined ? {} : {memberIndex, path: members[memberIndex].path})});
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
signal.throwIfAborted();
|
|
72
|
+
const stored = await storedAcquisition(directory, members, signal);
|
|
73
|
+
if (stored && !entry.refresh) {
|
|
74
|
+
entry.location = stored;
|
|
75
|
+
completed = members.length;
|
|
76
|
+
progress('reuse');
|
|
77
|
+
progress('ready');
|
|
78
|
+
return stored;
|
|
79
|
+
}
|
|
80
|
+
signal.throwIfAborted();
|
|
81
|
+
attempt = await mkdtemp(path.join(directory, 'acquisition-'));
|
|
82
|
+
const files = path.join(attempt, 'files');
|
|
83
|
+
await mkdir(files);
|
|
84
|
+
entry.location = {directory: files, members: members.map(function selectedMember(member) { return memberLocation(files, member); })};
|
|
85
|
+
progress('open');
|
|
86
|
+
const results = await Promise.allSettled(members.map(async function downloadMember(member, memberIndex) {
|
|
87
|
+
let handle;
|
|
88
|
+
let response;
|
|
89
|
+
let downloadError;
|
|
90
|
+
const failures = [];
|
|
91
|
+
let bodyOwned = false;
|
|
92
|
+
try {
|
|
93
|
+
signal.throwIfAborted();
|
|
94
|
+
const nativePath = entry.location.members[memberIndex].nativePath;
|
|
95
|
+
await mkdir(path.dirname(nativePath), {recursive: true});
|
|
96
|
+
handle = await open(nativePath, 'wx');
|
|
97
|
+
signal.throwIfAborted();
|
|
98
|
+
progress('download', memberIndex);
|
|
99
|
+
response = await fetch(member.url, {signal});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
const error = failure('MODEL_ASSET_DOWNLOAD_FAILED', `Model asset request failed: ${response.status} ${response.statusText}`);
|
|
102
|
+
error.url = member.url;
|
|
103
|
+
error.status = response.status;
|
|
104
|
+
error.response = await response.text();
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
if (!response.body) throw failure('MODEL_ASSET_DOWNLOAD_FAILED', `Model asset response has no content stream: ${member.url}`);
|
|
108
|
+
bodyOwned = true;
|
|
109
|
+
for await (const content of response.body) {
|
|
110
|
+
signal.throwIfAborted();
|
|
111
|
+
await handle.appendFile(content);
|
|
112
|
+
progress('download', memberIndex);
|
|
113
|
+
}
|
|
114
|
+
signal.throwIfAborted();
|
|
115
|
+
} catch (error) {
|
|
116
|
+
downloadError = error;
|
|
117
|
+
failures.push(error);
|
|
118
|
+
firstFailure ??= error;
|
|
119
|
+
controller.abort(error);
|
|
120
|
+
}
|
|
121
|
+
if (response?.body && !bodyOwned && !response.bodyUsed && !response.body.locked) {
|
|
122
|
+
try { await response.body.cancel(downloadError); } catch (error) { if (error !== downloadError) failures.push(error); }
|
|
123
|
+
}
|
|
124
|
+
if (handle) {
|
|
125
|
+
try { await handle.close(); } catch (error) { failures.push(error); }
|
|
126
|
+
}
|
|
127
|
+
if (failures.length) {
|
|
128
|
+
const error = failures.length === 1 ? failures[0] : new AggregateError(failures, 'Downloading a model member and closing its resources failed.');
|
|
129
|
+
firstFailure ??= error;
|
|
130
|
+
controller.abort(error);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
completed += 1;
|
|
134
|
+
progress('download', memberIndex);
|
|
135
|
+
}));
|
|
136
|
+
const failures = results.filter(function rejected(result) { return result.status === 'rejected'; })
|
|
137
|
+
.map(function reason(result) { return result.reason; });
|
|
138
|
+
if (failures.length > 1) throw new AggregateError(failures, 'Downloading model asset members failed.', {cause: firstFailure});
|
|
139
|
+
if (failures.length) throw failures[0];
|
|
140
|
+
signal.throwIfAborted();
|
|
141
|
+
progress('complete');
|
|
142
|
+
// The record describes a completed acquisition, not content identity.
|
|
143
|
+
// Publish it after every stream and file close, leaving older entries alone.
|
|
144
|
+
const pending = path.join(attempt, 'acquisition.pending.json');
|
|
145
|
+
await writeFile(pending, JSON.stringify({members, order: ++nativeAcquisitionOrder}), {encoding: 'utf8', flag: 'wx', signal});
|
|
146
|
+
signal.throwIfAborted();
|
|
147
|
+
await rename(pending, path.join(attempt, 'acquisition.json'));
|
|
148
|
+
committed = true;
|
|
149
|
+
progress('ready');
|
|
150
|
+
return entry.location;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (attempt && !committed) {
|
|
153
|
+
try { await rm(attempt, {recursive: true}); }
|
|
154
|
+
catch (cleanupError) { throw new AggregateError([error, cleanupError], 'Preparing native model assets failed and cleanup failed.'); }
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function shareNativeAcquisition({directory, members, refresh, signal, onProgress}) {
|
|
161
|
+
signal.throwIfAborted();
|
|
162
|
+
const key = JSON.stringify([directory, members, refresh]);
|
|
163
|
+
let entry = nativeAcquisitions.get(key);
|
|
164
|
+
if (entry?.controller.signal.aborted) {
|
|
165
|
+
return entry.task.catch(function observeRetiringAcquisition() {}).then(function acquireAfterDrain() {
|
|
166
|
+
return shareNativeAcquisition({directory, members, refresh, signal, onProgress});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (!entry) {
|
|
170
|
+
entry = {directory, members, refresh, controller: new AbortController(), consumers: new Set(), task: null, location: null, progress: null};
|
|
171
|
+
nativeAcquisitions.set(key, entry);
|
|
172
|
+
entry.publish = function publishProgress(progress) {
|
|
173
|
+
entry.progress = progress;
|
|
174
|
+
for (const consumer of entry.consumers) consumer.progress(progress);
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const acquisition = entry;
|
|
178
|
+
return new Promise(function observeAcquisition(resolve, reject) {
|
|
179
|
+
let settled = false;
|
|
180
|
+
let cancelled;
|
|
181
|
+
let observer = Promise.resolve();
|
|
182
|
+
const consumer = {progress};
|
|
183
|
+
function finish(failed, error, location) {
|
|
184
|
+
if (settled) return;
|
|
185
|
+
settled = true;
|
|
186
|
+
signal.removeEventListener('abort', abort);
|
|
187
|
+
acquisition.consumers.delete(consumer);
|
|
188
|
+
if (consumer.cancelled && failed && error !== cancelled) {
|
|
189
|
+
reject(new AggregateError([cancelled, error], 'Native model acquisition cancellation and cleanup failed.', {cause: cancelled}));
|
|
190
|
+
} else if (consumer.cancelled) reject(cancelled);
|
|
191
|
+
else if (failed) reject(error);
|
|
192
|
+
else resolve(location);
|
|
193
|
+
}
|
|
194
|
+
function cancel(error) {
|
|
195
|
+
if (settled || consumer.cancelled) return;
|
|
196
|
+
cancelled = error;
|
|
197
|
+
consumer.cancelled = true;
|
|
198
|
+
const others = [...acquisition.consumers].some(function interested(other) { return other !== consumer && !other.cancelled; });
|
|
199
|
+
if (others) observer.then(function cancelledObserverSettled() { finish(true, error); });
|
|
200
|
+
else acquisition.controller.abort(error);
|
|
201
|
+
}
|
|
202
|
+
function abort() { cancel(signal.reason); }
|
|
203
|
+
function progress(value) {
|
|
204
|
+
const notification = {...value};
|
|
205
|
+
// Each subscriber owns its ordered observers. A slow or failing
|
|
206
|
+
// observer never holds another subscriber's transfer or result.
|
|
207
|
+
observer = observer.then(async function observeProgress() {
|
|
208
|
+
if (!consumer.cancelled && !settled) await onProgress?.(notification, acquisition.location);
|
|
209
|
+
}).catch(cancel);
|
|
210
|
+
}
|
|
211
|
+
acquisition.consumers.add(consumer);
|
|
212
|
+
signal.addEventListener('abort', abort, {once: true});
|
|
213
|
+
if (acquisition.progress) progress(acquisition.progress);
|
|
214
|
+
if (signal.aborted) abort();
|
|
215
|
+
if (!acquisition.task) {
|
|
216
|
+
acquisition.task = Promise.resolve().then(function acquireSelectedMembers() { return acquireNativeMembers(acquisition); });
|
|
217
|
+
function releaseAcquisition() { if (nativeAcquisitions.get(key) === acquisition) nativeAcquisitions.delete(key); }
|
|
218
|
+
acquisition.task.then(releaseAcquisition, releaseAcquisition);
|
|
219
|
+
}
|
|
220
|
+
acquisition.task.then(function completed(location) {
|
|
221
|
+
observer.then(function observersCompleted() { finish(false, null, location); });
|
|
222
|
+
}, function failed(error) {
|
|
223
|
+
observer.then(function observersCompleted() { finish(true, error); });
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Temporary browser projections and persistent selected native acquisitions. */
|
|
14
229
|
export function createModelAssetService({appRoot = process.cwd()} = {}) {
|
|
15
230
|
const projections = new Map();
|
|
16
231
|
let context;
|
|
@@ -66,7 +281,7 @@ export function createModelAssetService({appRoot = process.cwd()} = {}) {
|
|
|
66
281
|
try { await record.downloading; } catch (error) { record.error ??= error; }
|
|
67
282
|
try { await record.finishing; } catch (error) { record.error ??= error; }
|
|
68
283
|
try { await closeMembers(record); filesClosed = true; } catch (error) { errors.push(error); }
|
|
69
|
-
if (record.directory && filesClosed) {
|
|
284
|
+
if (record.directory && filesClosed && !record.persistent) {
|
|
70
285
|
try { await rm(record.directory, {recursive: true}); }
|
|
71
286
|
catch (error) { if (error.code !== 'ENOENT') errors.push(error); }
|
|
72
287
|
}
|
|
@@ -107,8 +322,8 @@ export function createModelAssetService({appRoot = process.cwd()} = {}) {
|
|
|
107
322
|
throw error;
|
|
108
323
|
}
|
|
109
324
|
|
|
110
|
-
|
|
111
|
-
|
|
325
|
+
function beginProjection({id, workingDirectory, members}, signal) {
|
|
326
|
+
signal.throwIfAborted();
|
|
112
327
|
if (closing) throw failure('MODEL_ASSETS_CLOSING', 'Model asset preparation is closing.');
|
|
113
328
|
if (!is.string(id) || !id) throw new TypeError('A model asset projection needs an operation id.');
|
|
114
329
|
if (!is.string(workingDirectory) || !workingDirectory) {
|
|
@@ -126,21 +341,20 @@ export function createModelAssetService({appRoot = process.cwd()} = {}) {
|
|
|
126
341
|
});
|
|
127
342
|
record.released.catch(function observeReleaseFailure() {});
|
|
128
343
|
projections.set(id, record);
|
|
344
|
+
return record;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function openProjection({id, workingDirectory, members}, request) {
|
|
348
|
+
const record = beginProjection({id, workingDirectory, members}, request.signal);
|
|
129
349
|
record.opening = Promise.resolve().then(async function createWorkingFiles() {
|
|
130
350
|
const selectedDirectory = path.resolve(appRoot, workingDirectory);
|
|
131
351
|
await mkdir(selectedDirectory, {recursive: true});
|
|
132
352
|
request.signal.throwIfAborted();
|
|
133
353
|
record.directory = await mkdtemp(path.join(selectedDirectory, 'arcane-model-'));
|
|
134
354
|
record.members = members.map(function memberRecord(member) {
|
|
135
|
-
const nativePath = path.resolve(record.directory, member.path);
|
|
136
|
-
const relative = path.relative(record.directory, nativePath);
|
|
137
355
|
// A projection consists of files under its one owned directory.
|
|
138
356
|
// Absolute or escaping members cannot participate in that lifetime.
|
|
139
|
-
|
|
140
|
-
|| relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
141
|
-
throw new TypeError('Model asset member paths must be relative files within their projection.');
|
|
142
|
-
}
|
|
143
|
-
return {path: member.path, nativePath, handle: null, tail: Promise.resolve()};
|
|
357
|
+
return {...memberLocation(record.directory, member), handle: null, tail: Promise.resolve()};
|
|
144
358
|
});
|
|
145
359
|
const results = await Promise.allSettled(record.members.map(async function createMember(member) {
|
|
146
360
|
request.signal.throwIfAborted();
|
|
@@ -189,78 +403,44 @@ export function createModelAssetService({appRoot = process.cwd()} = {}) {
|
|
|
189
403
|
return task;
|
|
190
404
|
}
|
|
191
405
|
|
|
192
|
-
async function prepare({id, workingDirectory, members, signal, onProgress} = {}) {
|
|
406
|
+
async function prepare({id, workingDirectory, members, signal, onProgress, refresh = false} = {}) {
|
|
193
407
|
const controller = new AbortController();
|
|
194
408
|
const operationSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
195
|
-
const
|
|
196
|
-
let
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
409
|
+
const record = beginProjection({id, workingDirectory, members}, operationSignal);
|
|
410
|
+
let selectedMembers;
|
|
411
|
+
try { selectedMembers = members.map(function selectedMember(member) { return {path: member.path, url: member.url}; }); }
|
|
412
|
+
catch (error) { return failedOperation(record, error); }
|
|
413
|
+
record.persistent = true;
|
|
414
|
+
record.downloadController = controller;
|
|
415
|
+
// This task owns only acquisition. Failure cleanup may join it without
|
|
416
|
+
// recursively waiting for the preparation promise that requests cleanup.
|
|
417
|
+
record.downloading = Promise.resolve().then(async function prepareNativeFiles() {
|
|
201
418
|
operationSignal.throwIfAborted();
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
});
|
|
419
|
+
const selectedDirectory = path.join(path.resolve(appRoot, workingDirectory), 'model-assets');
|
|
420
|
+
await mkdir(selectedDirectory, {recursive: true});
|
|
421
|
+
const directory = await realpath(selectedDirectory);
|
|
206
422
|
operationSignal.throwIfAborted();
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
error.response = await response.text();
|
|
218
|
-
throw error;
|
|
219
|
-
}
|
|
220
|
-
if (!response.body) throw failure('MODEL_ASSET_DOWNLOAD_FAILED', `Model asset response has no content stream: ${member.url}`);
|
|
221
|
-
for await (const content of response.body) {
|
|
222
|
-
operationSignal.throwIfAborted();
|
|
223
|
-
await appendMemberContent(record, record.members[memberIndex], content, operationSignal);
|
|
224
|
-
await progress('download', memberIndex);
|
|
225
|
-
}
|
|
226
|
-
completed += 1;
|
|
227
|
-
await progress('download', memberIndex);
|
|
228
|
-
} catch (error) {
|
|
229
|
-
firstFailure ??= error;
|
|
230
|
-
controller.abort(error);
|
|
231
|
-
throw error;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
423
|
+
return shareNativeAcquisition({directory, members: selectedMembers, refresh: refresh === true, signal: operationSignal,
|
|
424
|
+
async onProgress(value, location) {
|
|
425
|
+
if (location) {
|
|
426
|
+
record.directory = location.directory;
|
|
427
|
+
record.members = location.members;
|
|
428
|
+
}
|
|
429
|
+
await onProgress?.(value);
|
|
430
|
+
}});
|
|
431
|
+
});
|
|
432
|
+
publish(record);
|
|
235
433
|
try {
|
|
236
|
-
|
|
237
|
-
record = requireProjection(id);
|
|
434
|
+
const location = await record.downloading;
|
|
238
435
|
operationSignal.throwIfAborted();
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
record
|
|
243
|
-
|
|
244
|
-
// may release preparation without waiting on itself.
|
|
245
|
-
record.downloading = Promise.resolve().then(async function downloadMembers() {
|
|
246
|
-
await progress('open');
|
|
247
|
-
const results = await Promise.allSettled(members.map(downloadMember));
|
|
248
|
-
const errors = results.filter(function rejected(result) { return result.status === 'rejected'; })
|
|
249
|
-
.map(function reason(result) { return result.reason; });
|
|
250
|
-
if (errors.length > 1) {
|
|
251
|
-
throw new AggregateError(errors, 'Downloading model asset members failed.', {cause: firstFailure});
|
|
252
|
-
}
|
|
253
|
-
if (errors.length) throw errors[0];
|
|
254
|
-
});
|
|
255
|
-
await record.downloading;
|
|
256
|
-
await progress('complete');
|
|
257
|
-
const projection = await completeProjection({id}, request);
|
|
258
|
-
await progress('ready');
|
|
259
|
-
return projection;
|
|
436
|
+
record.directory = location.directory;
|
|
437
|
+
record.members = location.members;
|
|
438
|
+
record.state = 'ready';
|
|
439
|
+
publish(record);
|
|
440
|
+
return snapshot(record);
|
|
260
441
|
} catch (error) {
|
|
261
442
|
controller.abort(error);
|
|
262
|
-
|
|
263
|
-
throw error;
|
|
443
|
+
return failedOperation(record, error);
|
|
264
444
|
}
|
|
265
445
|
}
|
|
266
446
|
|
package/src/packager/core.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import {pathToFileURL} from 'node:url';
|
|
15
15
|
import {appRelativeRoot,resolveAppRoot,resolvePackageOutputRoot} from '../app-layout.mjs';
|
|
16
|
+
import {materializeBrowserDecisions} from '../browser-decisions-distribution.mjs';
|
|
16
17
|
import {TARGET_IDS} from '../constants.mjs';
|
|
17
18
|
import {normalizeLocalAIConfig} from '../local-ai/config.mjs';
|
|
18
19
|
import {readInstalledSdkLayout} from '../sdk-runtime-layout.mjs';
|
|
@@ -904,7 +905,8 @@ async function replaceDirectory(stagingRoot,outputRoot){
|
|
|
904
905
|
}
|
|
905
906
|
|
|
906
907
|
async function packageWithContext(context,options={}){
|
|
907
|
-
const {signal,onEvent,moduleFormat='import-map',browserPwa:requestedBrowserPwa=true}=options;
|
|
908
|
+
const {signal,onEvent,moduleFormat='import-map',browserPwa:requestedBrowserPwa=true,browserDecisions=false}=options;
|
|
909
|
+
if (!is.boolean(browserDecisions)) throw new TypeError('packageApp browserDecisions must be a boolean.');
|
|
908
910
|
const browserPwa=context.target==='browser'&&requestedBrowserPwa;
|
|
909
911
|
if(!['import-map','native'].includes(moduleFormat)){
|
|
910
912
|
throw new TypeError('packageApp moduleFormat must be "import-map" or "native".');
|
|
@@ -1151,7 +1153,32 @@ async function packageWithContext(context,options={}){
|
|
|
1151
1153
|
if(!files.some(file=>pathKey(file)===pathKey(entryPath))){
|
|
1152
1154
|
fail(`Package output is missing its entry file: ${context.config.entry}.`);
|
|
1153
1155
|
}
|
|
1154
|
-
const installed=pwaEnabled
|
|
1156
|
+
const installed=pwaEnabled||browserDecisions
|
|
1157
|
+
?await readInstalledSdkLayout(context.workspaceRoot,context.rootConfig):null;
|
|
1158
|
+
if (browserDecisions) {
|
|
1159
|
+
const moduleSource = installed
|
|
1160
|
+
? path.join(installed.packageRoot, 'browser-runtime', 'ai', 'browser-decisions.mjs')
|
|
1161
|
+
: path.join(context.workspaceRoot, 'arcane', 'sdk', 'ai', 'browser-decisions.mjs');
|
|
1162
|
+
const moduleRecord = browserRecords.find(
|
|
1163
|
+
function selectedDecisionModule(record) { return record.source === moduleSource; }
|
|
1164
|
+
);
|
|
1165
|
+
if (!moduleRecord || !inventory.has(moduleRecord.destination)) {
|
|
1166
|
+
fail('packageApp browserDecisions requires the selected SDK browser-decisions module in the package.');
|
|
1167
|
+
}
|
|
1168
|
+
const distributionPath = path.posix.join(path.posix.dirname(moduleRecord.destination), 'decisions-runtime');
|
|
1169
|
+
// Acquire after native module and asset rewriting. The upstream
|
|
1170
|
+
// distribution stays opaque and joins the final PWA/release inventory.
|
|
1171
|
+
const distribution = await materializeBrowserDecisions(
|
|
1172
|
+
path.join(stagingRoot, ...distributionPath.split('/')),
|
|
1173
|
+
signal,
|
|
1174
|
+
onEvent
|
|
1175
|
+
);
|
|
1176
|
+
for (const file of distribution.files) {
|
|
1177
|
+
const relative = `${distributionPath}/${file}`;
|
|
1178
|
+
if (!inventory.has(relative)) files.push(relative);
|
|
1179
|
+
inventory.add(relative);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1155
1182
|
const pwaArtifacts=pwaEnabled?createPwaArtifacts({
|
|
1156
1183
|
app:{
|
|
1157
1184
|
id:context.appId,
|
|
@@ -2,6 +2,7 @@ import Is from 'strong-type';
|
|
|
2
2
|
import {randomUUID} from 'node:crypto';
|
|
3
3
|
import {copyFile,lstat,mkdir,readFile,readdir,realpath,rename,rm,writeFile} from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import {materializeBrowserDecisions} from './browser-decisions-distribution.mjs';
|
|
5
6
|
import {SDK_VERSION} from './constants.mjs';
|
|
6
7
|
import {rewriteAssetReferences} from './import-map.mjs';
|
|
7
8
|
import {getSdkRoot} from './runtime.mjs';
|
|
@@ -85,85 +86,6 @@ async function removeTemporaryTree(location){
|
|
|
85
86
|
await rm(location,{recursive:true,force:true}).catch(()=>{});
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
async function materializeBrowserDecisions(destination, signal, onEvent) {
|
|
89
|
-
const transformers = 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0/';
|
|
90
|
-
const onnx = 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.31.0-dev.20260914-8d85527a0/';
|
|
91
|
-
const files = [
|
|
92
|
-
{name: 'transformers.min.js', url: `${transformers}dist/transformers.min.js`},
|
|
93
|
-
{name: 'ort-wasm-simd-threaded.asyncify.mjs', url: `${onnx}dist/ort-wasm-simd-threaded.asyncify.mjs`},
|
|
94
|
-
{name: 'ort-wasm-simd-threaded.asyncify.wasm', url: `${onnx}dist/ort-wasm-simd-threaded.asyncify.wasm`},
|
|
95
|
-
{name: 'TRANSFORMERS-LICENSE', url: `${transformers}LICENSE`},
|
|
96
|
-
{name: 'ONNX-RUNTIME-LICENSE', url: `${onnx}LICENSE`}
|
|
97
|
-
];
|
|
98
|
-
const controller = new AbortController();
|
|
99
|
-
function cancelDistribution() {
|
|
100
|
-
controller.abort(signal.reason);
|
|
101
|
-
}
|
|
102
|
-
signal?.addEventListener('abort', cancelDistribution, {once: true});
|
|
103
|
-
if (signal?.aborted) cancelDistribution();
|
|
104
|
-
let completed = 0;
|
|
105
|
-
try {
|
|
106
|
-
throwIfAborted(controller.signal);
|
|
107
|
-
await emit(
|
|
108
|
-
onEvent,
|
|
109
|
-
{type: 'workspace.decisions.started', completed, total: files.length}
|
|
110
|
-
);
|
|
111
|
-
throwIfAborted(controller.signal);
|
|
112
|
-
await mkdir(destination, {recursive: true});
|
|
113
|
-
async function acquireDistributionFile(file) {
|
|
114
|
-
try {
|
|
115
|
-
throwIfAborted(controller.signal);
|
|
116
|
-
const response = await fetch(file.url, {signal: controller.signal});
|
|
117
|
-
const content = new Uint8Array(await response.arrayBuffer());
|
|
118
|
-
if (!response.ok) {
|
|
119
|
-
const error = new Error(`Browser decision distribution returned HTTP ${response.status} ${response.statusText}.`);
|
|
120
|
-
error.code = 'ARCANE_DECISION_DISTRIBUTION_DOWNLOAD_FAILED';
|
|
121
|
-
error.response = {
|
|
122
|
-
url: response.url,
|
|
123
|
-
status: response.status,
|
|
124
|
-
statusText: response.statusText,
|
|
125
|
-
headers: [...response.headers],
|
|
126
|
-
content
|
|
127
|
-
};
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
130
|
-
throwIfAborted(controller.signal);
|
|
131
|
-
// Upstream distribution content is opaque, including its own
|
|
132
|
-
// imports and license text. No SDK source rewriting applies.
|
|
133
|
-
await writeFile(path.join(destination, file.name), content);
|
|
134
|
-
throwIfAborted(controller.signal);
|
|
135
|
-
completed += 1;
|
|
136
|
-
await emit(
|
|
137
|
-
onEvent,
|
|
138
|
-
{type: 'workspace.decisions.progress', file: file.name, url: file.url, completed, total: files.length}
|
|
139
|
-
);
|
|
140
|
-
} catch (error) {
|
|
141
|
-
controller.abort(error);
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// One fixed distribution, acquired once for the selected projection;
|
|
146
|
-
// no model/app/architecture multiplier or installation script.
|
|
147
|
-
const outcomes = await Promise.allSettled(files.map(acquireDistributionFile));
|
|
148
|
-
const failures = new Set();
|
|
149
|
-
for (const outcome of outcomes) {
|
|
150
|
-
if (outcome.status === 'rejected') failures.add(outcome.reason);
|
|
151
|
-
}
|
|
152
|
-
if (failures.size === 1) throw [...failures][0];
|
|
153
|
-
if (failures.size > 1) {
|
|
154
|
-
throw new AggregateError([...failures], 'Browser decision distribution acquisition failed.');
|
|
155
|
-
}
|
|
156
|
-
throwIfAborted(controller.signal);
|
|
157
|
-
} finally {
|
|
158
|
-
signal?.removeEventListener('abort', cancelDistribution);
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
transformersVersion: '4.3.0',
|
|
162
|
-
onnxRuntimeVersion: '1.31.0-dev.20260914-8d85527a0',
|
|
163
|
-
files: files.map(function distributionFilename(file) { return file.name; })
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
89
|
export async function materializeWorkspaceRuntimeContent({
|
|
168
90
|
workspaceRoot,
|
|
169
91
|
runtimeRoot=path.join(getSdkRoot(),'runtime'),
|