arcane-os 0.69.0 → 0.70.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 +20 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-decisions.mjs +46 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +58 -62
- package/browser-runtime/ai/browser-speech-providers.mjs +3 -0
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +76 -164
- package/browser-runtime/ai/dbopfs-model-parts.mjs +344 -0
- package/browser-runtime/ai/dbopfs-model-resources.mjs +190 -0
- package/browser-runtime/ai/decision-runtime.mjs +17 -1
- package/browser-runtime/ai/decision-worker.mjs +21 -1
- package/browser-runtime/ai/model-resource-bridge.mjs +231 -0
- package/browser-runtime/ai/speech-worker-client.mjs +68 -28
- package/browser-runtime/ai/speech-worker-runtime.mjs +212 -97
- package/browser-runtime/core/contracts.mjs +1 -1
- package/docs/reference/ai/browser-decisions.md +57 -3
- package/docs/reference/ai/browser-speech.md +45 -17
- package/docs/reference/ai/browser-wasm.md +28 -20
- package/docs/reference/core-native-packaging.md +13 -0
- package/docs/reference/core-runtime.md +9 -0
- package/docs/reference/core-shared-host.md +190 -0
- package/docs/reference/inventory/runtime-modules.json +3 -3
- package/docs/reference/runtime-modules.md +97 -7
- package/docs/reference/sdk-api.md +41 -30
- package/package.json +2 -2
- package/runtime/arcane/modules/AI.js +2 -18
- package/runtime/arcane/modules/AIProviderRuntime.js +77 -3
- package/runtime/arcane/modules/DBOPFS.js +180 -26
- package/src/core/host.mjs +2 -0
- package/src/core/runtime.mjs +9 -4
- package/src/core/services/packaged-web.mjs +8 -1
- package/src/core/shared-host.mjs +500 -0
- package/src/native/portable-layout.mjs +66 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.70.0
|
|
4
|
+
|
|
5
|
+
- Add app-scoped DBOPFS committed-change subscriptions through the existing
|
|
6
|
+
event owner, with targeted cache invalidation and same-origin/storage-partition
|
|
7
|
+
live document notifications. Preserve complete records, successful partial
|
|
8
|
+
batch outcomes, cancellation and existing storage without migration or polling.
|
|
9
|
+
- Keep cold TTS activation owned by its pending preparation interests. Cancelling
|
|
10
|
+
the last pending interest cancels its activation; another preparation or direct
|
|
11
|
+
load retains shared work. Ready models and cache hits keep their existing lifetime.
|
|
12
|
+
- Add explicit same-app shared Core hosts over named pipes or Unix sockets,
|
|
13
|
+
retaining default stdio and one runtime owner. Correlate per-connection requests,
|
|
14
|
+
cancellation and events without confusing equal caller IDs across connections;
|
|
15
|
+
replay current packaged-web state while preserving complete payloads and errors.
|
|
16
|
+
- Store complete browser model/support resources as ordered DBOPFS parts and add
|
|
17
|
+
public model/speech-store `fetchResource()` support. Decision and speech Worker
|
|
18
|
+
resource fetches share the parent store, including nested module Workers, with
|
|
19
|
+
file/shard progress and cancellation that joins cleanup. Existing whole/range
|
|
20
|
+
cache formats remain readable; native ESM and unmapped classic Worker loading
|
|
21
|
+
remain platform-owned. No model/provider/default switch or cache migration.
|
|
22
|
+
|
|
3
23
|
## 0.69.0
|
|
4
24
|
|
|
5
25
|
- Add explicit `textFormat:'plain'` speech input through prepared speech,
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
20
20
|
share the theme, packaging, event, cancellation,
|
|
21
21
|
and browser run contracts while retaining their selected application layout.
|
|
22
22
|
|
|
23
|
-
This checkout defines the `0.
|
|
23
|
+
This checkout defines the `0.70.0` SDK contract. Development applications declare
|
|
24
24
|
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
25
25
|
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
26
26
|
to refresh and compare the installed SDK and lockfile with the published channel;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Is from '../dependencies/strong-type/index.js';
|
|
2
2
|
import {createArcaneEventSource} from '../event-manager.mjs';
|
|
3
|
+
import {createModelResourceHost} from './model-resource-bridge.mjs';
|
|
3
4
|
|
|
4
5
|
const is = new Is(false);
|
|
5
6
|
const STATE_EVENT = 'ai.decisions.state';
|
|
@@ -65,6 +66,7 @@ function createDecisionOperation(op) {
|
|
|
65
66
|
*/
|
|
66
67
|
class BrowserDecisionModel {
|
|
67
68
|
#configuration;
|
|
69
|
+
#store;
|
|
68
70
|
#events;
|
|
69
71
|
#activation = null;
|
|
70
72
|
#generation = 0;
|
|
@@ -75,13 +77,17 @@ class BrowserDecisionModel {
|
|
|
75
77
|
#disposed = false;
|
|
76
78
|
|
|
77
79
|
constructor({family, model, revision = 'main', device = 'webgpu',
|
|
78
|
-
dtype = family === 'laya' ? 'fp16' : 'fp32', runtime = {}} = {}) {
|
|
80
|
+
dtype = family === 'laya' ? 'fp16' : 'fp32', runtime = {}, store = null} = {}) {
|
|
79
81
|
if (family !== 'laya' && family !== 'julia') {
|
|
80
82
|
throw new TypeError('family must be "laya" or "julia".');
|
|
81
83
|
}
|
|
82
84
|
if (runtime === null || !is.object(runtime)) {
|
|
83
85
|
throw new TypeError('runtime must be an object.');
|
|
84
86
|
}
|
|
87
|
+
if (store !== null && !is.function(store?.fetchResource)) {
|
|
88
|
+
throw new TypeError('store must expose fetchResource().');
|
|
89
|
+
}
|
|
90
|
+
this.#store = store;
|
|
85
91
|
const moduleUrl = runtime.moduleUrl === undefined
|
|
86
92
|
? DEFAULT_RUNTIME_MODULE
|
|
87
93
|
: runtime.moduleUrl;
|
|
@@ -197,6 +203,8 @@ class BrowserDecisionModel {
|
|
|
197
203
|
load: createDecisionOperation('load'),
|
|
198
204
|
pending: new Map(),
|
|
199
205
|
evaluations: new Set(),
|
|
206
|
+
resources: null,
|
|
207
|
+
cleanup: null,
|
|
200
208
|
reason: undefined
|
|
201
209
|
};
|
|
202
210
|
this.#activation = activation;
|
|
@@ -212,6 +220,22 @@ class BrowserDecisionModel {
|
|
|
212
220
|
{type: 'module', name: 'arcane-decisions'}
|
|
213
221
|
);
|
|
214
222
|
activation.worker = worker;
|
|
223
|
+
if (this.#store) {
|
|
224
|
+
const store = this.#store;
|
|
225
|
+
activation.resources = createModelResourceHost(
|
|
226
|
+
{
|
|
227
|
+
fetchResource: function fetchDecisionResource(input, options) {
|
|
228
|
+
return store.fetchResource(input, options);
|
|
229
|
+
},
|
|
230
|
+
send: function sendDecisionResource(message) {
|
|
231
|
+
worker.postMessage(message);
|
|
232
|
+
},
|
|
233
|
+
onError: function stopFailedDecisionResource(error) {
|
|
234
|
+
model.#stop(activation, error, 'error');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
}
|
|
215
239
|
function handleDecisionWorkerMessage(event) {
|
|
216
240
|
model.#receive(activation, event.data);
|
|
217
241
|
}
|
|
@@ -240,7 +264,7 @@ class BrowserDecisionModel {
|
|
|
240
264
|
const id = this.#nextId++;
|
|
241
265
|
activation.pending.set(id, activation.load);
|
|
242
266
|
worker.postMessage(
|
|
243
|
-
{id, op: 'load', payload: this.#configuration}
|
|
267
|
+
{id, op: 'load', payload: this.#configuration, storedResources: this.#store !== null}
|
|
244
268
|
);
|
|
245
269
|
} catch (error) {
|
|
246
270
|
this.#stop(activation, error, 'error');
|
|
@@ -250,6 +274,7 @@ class BrowserDecisionModel {
|
|
|
250
274
|
|
|
251
275
|
#receive(activation, message) {
|
|
252
276
|
if (!this.#isCurrent(activation)) return;
|
|
277
|
+
if (activation.resources?.receive(message)) return;
|
|
253
278
|
const operation = message && activation.pending.get(message.id);
|
|
254
279
|
if (!operation) {
|
|
255
280
|
this.#stop(
|
|
@@ -308,6 +333,23 @@ class BrowserDecisionModel {
|
|
|
308
333
|
this.#state = state;
|
|
309
334
|
this.#progress = null;
|
|
310
335
|
this.#error = reason;
|
|
336
|
+
if (activation.resources) {
|
|
337
|
+
const model = this;
|
|
338
|
+
activation.cleanup = activation.resources.close(reason).catch(
|
|
339
|
+
function preserveResourceCleanupFailure(error) {
|
|
340
|
+
activation.reason = new AggregateError([reason, error], 'Unable to settle decision model resources.');
|
|
341
|
+
throw activation.reason;
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
activation.cleanup.catch(
|
|
345
|
+
function reportResourceCleanupFailure(error) {
|
|
346
|
+
if (!model.#activation) {
|
|
347
|
+
model.#error = error;
|
|
348
|
+
if (!model.#disposed) model.#publish();
|
|
349
|
+
} else globalThis.console?.error('Decision model resource cleanup failed.', error);
|
|
350
|
+
}
|
|
351
|
+
);
|
|
352
|
+
}
|
|
311
353
|
if (activation.worker) {
|
|
312
354
|
for (const [type, listener] of Object.entries(activation.listeners ?? {})) {
|
|
313
355
|
activation.worker.removeEventListener(type, listener);
|
|
@@ -330,6 +372,7 @@ class BrowserDecisionModel {
|
|
|
330
372
|
return this.status();
|
|
331
373
|
} finally {
|
|
332
374
|
stopObserving();
|
|
375
|
+
if (activation.cleanup) await activation.cleanup;
|
|
333
376
|
}
|
|
334
377
|
}
|
|
335
378
|
|
|
@@ -366,6 +409,7 @@ class BrowserDecisionModel {
|
|
|
366
409
|
} finally {
|
|
367
410
|
stopObserving();
|
|
368
411
|
activation.evaluations.delete(evaluation);
|
|
412
|
+
if (activation.cleanup) await activation.cleanup;
|
|
369
413
|
if (this.#isCurrent(activation)) this.#publish();
|
|
370
414
|
}
|
|
371
415
|
}
|
|
@@ -2,6 +2,8 @@ import Is from "../dependencies/strong-type/index.js";
|
|
|
2
2
|
import {
|
|
3
3
|
normalizeModelSecurity,
|
|
4
4
|
} from "./model-controller.mjs";
|
|
5
|
+
import { createDbopfsModelPartStore } from "./dbopfs-model-parts.mjs";
|
|
6
|
+
import { createDbopfsResourceStore } from "./dbopfs-model-resources.mjs";
|
|
5
7
|
|
|
6
8
|
const is = new Is(false);
|
|
7
9
|
|
|
@@ -1090,37 +1092,6 @@ function storageNames(authority, files) {
|
|
|
1090
1092
|
});
|
|
1091
1093
|
}
|
|
1092
1094
|
|
|
1093
|
-
async function* byteChunks(body, signal) {
|
|
1094
|
-
if (is.instanceCheck(body, Uint8Array) || is.instanceCheck(body, ArrayBuffer) || is.arrayBufferView(body)) {
|
|
1095
|
-
throwIfAborted(signal);
|
|
1096
|
-
yield is.instanceCheck(body, Uint8Array)
|
|
1097
|
-
? body
|
|
1098
|
-
: new Uint8Array(body.buffer ?? body, body.byteOffset ?? 0, body.byteLength);
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
if (body && is.function(body.getReader)) {
|
|
1102
|
-
const reader = body.getReader();
|
|
1103
|
-
const abort = () => void reader.cancel(signal?.reason).catch(() => undefined);
|
|
1104
|
-
signal?.addEventListener?.("abort", abort, { once: true });
|
|
1105
|
-
try {
|
|
1106
|
-
while (true) {
|
|
1107
|
-
throwIfAborted(signal);
|
|
1108
|
-
const { done, value } = await reader.read();
|
|
1109
|
-
if (done) return;
|
|
1110
|
-
yield is.instanceCheck(value, Uint8Array) ? value : new Uint8Array(value);
|
|
1111
|
-
}
|
|
1112
|
-
} finally {
|
|
1113
|
-
signal?.removeEventListener?.("abort", abort);
|
|
1114
|
-
if (signal?.aborted) await reader.cancel(signal.reason).catch(() => undefined);
|
|
1115
|
-
reader.releaseLock?.();
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
throw speechError(
|
|
1119
|
-
"ARCANE_AI_ARTIFACT_SOURCE_INVALID",
|
|
1120
|
-
"A browser speech artifact did not provide readable bytes.",
|
|
1121
|
-
);
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
1095
|
function tokenizeArtifactGraphModule(source, modulePath) {
|
|
1125
1096
|
const tokens = [];
|
|
1126
1097
|
let index = 0;
|
|
@@ -1851,6 +1822,12 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
1851
1822
|
}
|
|
1852
1823
|
let tablePromise = null;
|
|
1853
1824
|
const operationTails = new Map();
|
|
1825
|
+
const parts = createDbopfsModelPartStore({dbopfs, tableName});
|
|
1826
|
+
const resources = createDbopfsResourceStore({
|
|
1827
|
+
dbopfs,
|
|
1828
|
+
tableName,
|
|
1829
|
+
fetchImpl: fetchImpl ?? globalThis.fetch?.bind(globalThis),
|
|
1830
|
+
});
|
|
1854
1831
|
|
|
1855
1832
|
function serializeAuthority(authority, operation) {
|
|
1856
1833
|
const key = storageKey(authority);
|
|
@@ -1916,22 +1893,9 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
1916
1893
|
}
|
|
1917
1894
|
}
|
|
1918
1895
|
|
|
1919
|
-
async function
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
const writable = await handle.createWritable();
|
|
1923
|
-
try {
|
|
1924
|
-
for await (const chunk of byteChunks(body, signal)) {
|
|
1925
|
-
await writable.write(chunk);
|
|
1926
|
-
}
|
|
1927
|
-
throwIfAborted(signal);
|
|
1928
|
-
await writable.close();
|
|
1929
|
-
return undefined;
|
|
1930
|
-
} catch (error) {
|
|
1931
|
-
await writable.abort?.(error).catch(() => undefined);
|
|
1932
|
-
await directory.removeEntry(name).catch(() => undefined);
|
|
1933
|
-
throw error;
|
|
1934
|
-
}
|
|
1896
|
+
async function readArtifact(name, signal) {
|
|
1897
|
+
// Reuse pre-existing complete files without rewriting saved model data.
|
|
1898
|
+
return await parts.read(name, {signal}) ?? await readFile(name);
|
|
1935
1899
|
}
|
|
1936
1900
|
|
|
1937
1901
|
async function removeUnlocked(authority) {
|
|
@@ -1951,6 +1915,9 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
1951
1915
|
const results = await Promise.all([
|
|
1952
1916
|
removeEntry(names.selection),
|
|
1953
1917
|
...fileNames.map(removeEntry),
|
|
1918
|
+
...fileNames.map(function removeArtifactParts(name) {
|
|
1919
|
+
return parts.remove(name);
|
|
1920
|
+
}),
|
|
1954
1921
|
]);
|
|
1955
1922
|
return results.some(Boolean);
|
|
1956
1923
|
}
|
|
@@ -1976,7 +1943,6 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
1976
1943
|
const names = storageNames(authority, metadata.files);
|
|
1977
1944
|
const storedSelection = await readJsonFile(names.selection);
|
|
1978
1945
|
if (JSON.stringify(storedSelection) !== JSON.stringify(cacheSelection(authority))) {
|
|
1979
|
-
await removeUnlocked(authority);
|
|
1980
1946
|
return null;
|
|
1981
1947
|
}
|
|
1982
1948
|
const files = [];
|
|
@@ -1996,9 +1962,8 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
1996
1962
|
unit: 'files',
|
|
1997
1963
|
}
|
|
1998
1964
|
);
|
|
1999
|
-
const file = await
|
|
1965
|
+
const file = await readArtifact(names.files[index], signal);
|
|
2000
1966
|
if (!file) {
|
|
2001
|
-
await removeUnlocked(authority);
|
|
2002
1967
|
return null;
|
|
2003
1968
|
}
|
|
2004
1969
|
files.push({ descriptor, file });
|
|
@@ -2037,7 +2002,6 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
2037
2002
|
routing,
|
|
2038
2003
|
});
|
|
2039
2004
|
} catch (error) {
|
|
2040
|
-
await removeUnlocked(authority);
|
|
2041
2005
|
throw error;
|
|
2042
2006
|
}
|
|
2043
2007
|
}
|
|
@@ -2050,19 +2014,36 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
2050
2014
|
if (!is.function(fetchFunction)) {
|
|
2051
2015
|
throw speechError("ARCANE_AI_ARTIFACT_SOURCE_UNAVAILABLE", "Browser fetch is unavailable.");
|
|
2052
2016
|
}
|
|
2053
|
-
await removeUnlocked(authority);
|
|
2054
2017
|
const installed = [];
|
|
2055
2018
|
try {
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2019
|
+
const selection = cacheSelection(authority);
|
|
2020
|
+
const storedSelection = await readJsonFile(names.selection);
|
|
2021
|
+
if (JSON.stringify(storedSelection) !== JSON.stringify(selection)) {
|
|
2022
|
+
await removeUnlocked(authority);
|
|
2023
|
+
// Selection describes the caller's resources, not download completion.
|
|
2024
|
+
await parts.writePart(
|
|
2025
|
+
names.selection,
|
|
2026
|
+
new TextEncoder().encode(`${JSON.stringify(selection)}\n`),
|
|
2027
|
+
{ signal },
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2063
2030
|
for (let index = 0; index < metadata.files.length; index += 1) {
|
|
2064
2031
|
throwIfAborted(signal);
|
|
2065
2032
|
const descriptor = metadata.files[index];
|
|
2033
|
+
const cachedFile = await readArtifact(names.files[index], signal);
|
|
2034
|
+
if (cachedFile) {
|
|
2035
|
+
installed.push({descriptor, file: cachedFile});
|
|
2036
|
+
reportArtifactProgress(onProgress, signal, {
|
|
2037
|
+
phase: 'prepare',
|
|
2038
|
+
stage: 'cache',
|
|
2039
|
+
message: 'Opened cached speech file',
|
|
2040
|
+
file: descriptor.path,
|
|
2041
|
+
completed: installed.length,
|
|
2042
|
+
total: metadata.files.length,
|
|
2043
|
+
unit: 'files'
|
|
2044
|
+
});
|
|
2045
|
+
continue;
|
|
2046
|
+
}
|
|
2066
2047
|
const sourceUrl = graph ? descriptor.sourceUrl : descriptor.url;
|
|
2067
2048
|
reportArtifactProgress(
|
|
2068
2049
|
onProgress,
|
|
@@ -2139,8 +2120,21 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
2139
2120
|
`A speech artifact server returned HTTP ${String(responseStatus)}.`,
|
|
2140
2121
|
);
|
|
2141
2122
|
}
|
|
2142
|
-
await
|
|
2143
|
-
|
|
2123
|
+
const file = await parts.write(names.files[index], responseBody, {
|
|
2124
|
+
signal,
|
|
2125
|
+
onProgress: function reportSpeechShards(shards) {
|
|
2126
|
+
reportArtifactProgress(onProgress, signal, {
|
|
2127
|
+
phase: 'download',
|
|
2128
|
+
stage: 'artifacts',
|
|
2129
|
+
message: 'Storing speech file',
|
|
2130
|
+
file: descriptor.path,
|
|
2131
|
+
completed: installed.length,
|
|
2132
|
+
total: metadata.files.length,
|
|
2133
|
+
unit: 'files',
|
|
2134
|
+
resource: {...shards, file: descriptor.path},
|
|
2135
|
+
});
|
|
2136
|
+
},
|
|
2137
|
+
});
|
|
2144
2138
|
if (!file) {
|
|
2145
2139
|
if (graph) {
|
|
2146
2140
|
throw graphVerificationError(
|
|
@@ -2186,7 +2180,8 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
2186
2180
|
routing,
|
|
2187
2181
|
});
|
|
2188
2182
|
} catch (error) {
|
|
2189
|
-
|
|
2183
|
+
// Complete resources remain available for the next explicit activation.
|
|
2184
|
+
// The active writer owns cleanup of only its unfinished resource.
|
|
2190
2185
|
throw error;
|
|
2191
2186
|
}
|
|
2192
2187
|
}
|
|
@@ -2330,6 +2325,7 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
2330
2325
|
tableName,
|
|
2331
2326
|
prepare,
|
|
2332
2327
|
remove,
|
|
2328
|
+
fetchResource: resources.fetchResource,
|
|
2333
2329
|
});
|
|
2334
2330
|
STORES.add(store);
|
|
2335
2331
|
return store;
|
|
@@ -1316,6 +1316,9 @@ function createBrowserSpeechProvider({
|
|
|
1316
1316
|
};
|
|
1317
1317
|
slot.client = createSpeechWorkerClient({
|
|
1318
1318
|
role,
|
|
1319
|
+
fetchResource: function fetchSpeechWorkerResource(input, options) {
|
|
1320
|
+
return store.fetchResource(input, options);
|
|
1321
|
+
},
|
|
1319
1322
|
onTermination: function handleSpeechWorkerTermination(termination) {
|
|
1320
1323
|
handlePoolTermination(pool, slot, termination);
|
|
1321
1324
|
},
|