arcane-os 0.55.0 → 0.57.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 +51 -0
- package/README.md +5 -3
- package/browser-runtime/ai/core-local.mjs +306 -0
- package/browser-runtime/core/client.mjs +75 -14
- package/browser-runtime/core/contracts.mjs +2 -1
- package/docs/consumer-updates.md +82 -0
- package/docs/publishing.md +41 -18
- package/docs/reference/README.md +31 -1
- package/docs/reference/availability-and-normalization.md +11 -3
- package/docs/reference/cli.md +9 -1
- package/docs/reference/core-client.md +45 -2
- package/docs/reference/core-native-packaging.md +195 -11
- package/docs/reference/core-runtime.md +10 -0
- package/docs/reference/inventory/package-api.json +160 -20
- package/docs/reference/local-ai.md +197 -0
- package/docs/reference/protocols.md +91 -14
- package/docs/reference/pwa.md +17 -0
- package/docs/reference/runtime-modules.md +6 -5
- package/docs/reference/sdk-api.md +165 -26
- package/package.json +5 -1
- package/runtime/arcane/modules/LocalAIReadiness.js +3 -1
- package/schemas/arcane-app.schema.json +64 -0
- package/schemas/arcane-package.schema.json +54 -0
- package/src/app-descriptor.mjs +35 -2
- package/src/app-layout.mjs +6 -2
- package/src/cli/main.mjs +8 -2
- package/src/core/host.mjs +23 -0
- package/src/core/hosts/arcane-core-loader.cjs +2 -0
- package/src/core/hosts/linux/arcane_core_process.c +568 -0
- package/src/core/hosts/linux/arcane_core_process.h +127 -0
- package/src/core/hosts/macos/ArcaneCoreProcess.h +85 -0
- package/src/core/hosts/macos/ArcaneCoreProcess.m +357 -0
- package/src/core/hosts/sea-launcher.cjs +17 -0
- package/src/core/hosts/windows/ArcaneCoreProcess.cs +416 -0
- package/src/core/hosts/windows/ArcaneHost.cs +695 -0
- package/src/core/hosts/windows/ArcaneHost.manifest +22 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +239 -0
- package/src/core/runtime.mjs +15 -3
- package/src/core/services/local-ai.mjs +543 -0
- package/src/core-classic-source.mjs +2 -2
- package/src/dev-server.mjs +7 -0
- package/src/import-map.mjs +1 -0
- package/src/local-ai/archive.mjs +52 -0
- package/src/local-ai/config.mjs +62 -0
- package/src/local-ai/dev.mjs +390 -0
- package/src/local-ai/discover.mjs +71 -0
- package/src/local-ai/http.mjs +106 -0
- package/src/local-ai/install.mjs +344 -0
- package/src/local-ai/server.mjs +262 -0
- package/src/native/portable-layout.mjs +29 -4
- package/src/native/portable-provider.mjs +39 -8
- package/src/native/windows-provider.mjs +262 -0
- package/src/native-provider-loader.mjs +8 -2
- package/src/packager/core.mjs +89 -15
- package/src/process.mjs +74 -12
- package/src/toolchain.mjs +38 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.57.0
|
|
4
|
+
|
|
5
|
+
- Add local model chat through explicit llama.cpp/Ollama runtime requirements,
|
|
6
|
+
reusable installation and bundling, a retained Core local-AI service, and an
|
|
7
|
+
optional browser-to-Core provider. Preserve complete requests and streamed
|
|
8
|
+
responses, model readiness, cancellation and owned process shutdown. Apps own
|
|
9
|
+
model selection and defaults; preparation does not block ordinary page startup.
|
|
10
|
+
- Add authored Core service selection and launch context. Factories receive
|
|
11
|
+
authored options and the separate app-root/runtime context; explicit launch
|
|
12
|
+
context overrides remain supported. Portable packages can bundle explicitly
|
|
13
|
+
selected local-AI runtimes without changing browser-only output.
|
|
14
|
+
- Add Windows x64 executable packaging through the public Windows native
|
|
15
|
+
provider and the matching release's retained WebView2/Node SEA host asset.
|
|
16
|
+
Client injection precedes app scripts, service requests retain complete JSON,
|
|
17
|
+
and accepted service work drains before the owned Core process exits.
|
|
18
|
+
- Add `runProcess` cancellation mode `close-input` for owned graceful shutdown;
|
|
19
|
+
the existing `terminate-tree` default remains unchanged. Includes separately
|
|
20
|
+
delivered macOS process source, without claiming a complete macOS host.
|
|
21
|
+
- Correct the documented Node document-library subpath and lexical-search
|
|
22
|
+
example. No preferences service, Nemotron diarization, native ONNX, native
|
|
23
|
+
Kokoro or local image generation is introduced in this release.
|
|
24
|
+
|
|
25
|
+
## 0.56.0
|
|
26
|
+
|
|
27
|
+
- Add explicit `package.nativeResources` selection for native-only application
|
|
28
|
+
files and directories. Browser selection remains unchanged; non-browser
|
|
29
|
+
packaging keeps additional resources outside browser transformations and
|
|
30
|
+
stages them in a separate target output. Applications select their complete
|
|
31
|
+
required source closure, and target adapters receive the effective target
|
|
32
|
+
and browser-PWA selection.
|
|
33
|
+
- Add `getInstalledCoreClient()` to read the exact live SDK-installed client
|
|
34
|
+
without connecting or changing globals. Terminal `failTransport(error)` and
|
|
35
|
+
the owned native failure callback preserve complete errors, settle pending
|
|
36
|
+
requests, and release the failed installation's callbacks and subscriptions.
|
|
37
|
+
- Add opt-in `replayRuntimeState` for Core clients and classic injection. The
|
|
38
|
+
runtime replays its actual readiness and current service state for newly
|
|
39
|
+
connected documents without restarting services or delaying page startup.
|
|
40
|
+
|
|
41
|
+
- Preserve complete UTF-8 process output when a character spans stdout or stderr
|
|
42
|
+
chunks. Independent readable-stream decoders feed the same text to captured
|
|
43
|
+
results and existing event delivery; ordering, backpressure, cancellation,
|
|
44
|
+
graceful drain, nonzero error details and line-event behavior remain unchanged.
|
|
45
|
+
- Include committed Windows and Linux Core process-transport source and the
|
|
46
|
+
generic Windows SEA launcher source as native-host groundwork. These sources
|
|
47
|
+
do not activate a platform host or deliver a public native executable; app
|
|
48
|
+
host composition and the remaining native integration stay independently owned.
|
|
49
|
+
- Clarify development `latest` tracking, production-designated pins, matching
|
|
50
|
+
requester notices, and required documentation/site release follow-through.
|
|
51
|
+
- No new dependency, application service, automatic runtime installation,
|
|
52
|
+
application default change or saved-data migration is introduced.
|
|
53
|
+
|
|
3
54
|
## 0.55.0
|
|
4
55
|
|
|
5
56
|
- Add public Core client, protocol contracts and classic-script source generation
|
package/README.md
CHANGED
|
@@ -20,9 +20,11 @@ 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.
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
This checkout defines the `0.57.0` SDK contract. Development applications declare
|
|
24
|
+
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
25
|
+
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
26
|
+
to refresh and compare the installed SDK and lockfile with the published channel;
|
|
27
|
+
registry state is deliberately not baked into application artifacts.
|
|
26
28
|
|
|
27
29
|
Standalone browser apps use their repository root and installed npm package directly, without a
|
|
28
30
|
generated workspace `arcane/` directory or `arcane.lock.json`. Select the
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import Is from '../dependencies/strong-type/index.js';
|
|
2
|
+
import {getInstalledCoreClient} from '../core/client.mjs';
|
|
3
|
+
|
|
4
|
+
const is = new Is(false);
|
|
5
|
+
const PROVIDER_PROTOCOL = 'arcane-ai-provider/2';
|
|
6
|
+
const AUTHORITY_PROTOCOL = 'arcane-ai-model-authority/1';
|
|
7
|
+
|
|
8
|
+
function localAIError(code, message, cause) {
|
|
9
|
+
const error = cause === undefined ? new Error(message) : new Error(message, {cause});
|
|
10
|
+
error.code = code;
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function cancelled(reason) {
|
|
15
|
+
const error = localAIError('ARCANE_AI_REQUEST_ABORTED', 'The local AI request was cancelled.', reason);
|
|
16
|
+
error.name = 'AbortError';
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Browser-safe llama.cpp access through an existing Core connection. The app
|
|
22
|
+
* selects its model; Core owns the running engine and its loaded sessions.
|
|
23
|
+
* Creating this provider neither connects a transport nor installs a runtime.
|
|
24
|
+
*/
|
|
25
|
+
export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id = 'llama.cpp'} = {}) {
|
|
26
|
+
if (!is.string(id) || !id.trim()) throw new TypeError('The Core AI provider id must be a nonempty string.');
|
|
27
|
+
if (client !== null && (!is.function(client?.invoke) || !is.function(client?.events?.on))) {
|
|
28
|
+
throw new TypeError('The Core AI client must expose invoke and events.on.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let runtime = null;
|
|
32
|
+
let models = [];
|
|
33
|
+
let selection = null;
|
|
34
|
+
let disposed = false;
|
|
35
|
+
let loading = false;
|
|
36
|
+
let sequence = 0;
|
|
37
|
+
let lifecycleRevision = 0;
|
|
38
|
+
const requests = new Set();
|
|
39
|
+
|
|
40
|
+
function requireClient() {
|
|
41
|
+
if (disposed) throw localAIError('ARCANE_AI_PROVIDER_DISPOSED', 'The Core AI provider is disposed.');
|
|
42
|
+
if (!client) throw localAIError('ARCANE_AI_CORE_UNAVAILABLE', 'Local llama.cpp requires an available Core connection.');
|
|
43
|
+
return client;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requireSelection(value) {
|
|
47
|
+
if (value?.providerId !== id || !is.string(value?.modelId) || !value.modelId
|
|
48
|
+
|| value.localOnly !== true) {
|
|
49
|
+
throw localAIError('ARCANE_AI_MODEL_AUTHORITY_REQUIRED', 'The local provider requires an explicit matching provider and model.');
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadedModel() {
|
|
55
|
+
return selection && models.find(function selectedModel(model) {
|
|
56
|
+
return model.id === selection.modelId && model.loaded === true;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function modelReady() {
|
|
61
|
+
return runtime?.available === true && runtime.state === 'ready' && Boolean(loadedModel());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function status() {
|
|
65
|
+
const loaded = !disposed && modelReady();
|
|
66
|
+
return {
|
|
67
|
+
state: disposed ? 'disposed' : loading ? 'loading' : loaded ? 'ready'
|
|
68
|
+
: runtime?.error ? 'error' : runtime?.available === true ? 'unloaded' : 'unavailable',
|
|
69
|
+
loaded,
|
|
70
|
+
busy: loading || requests.size > 0,
|
|
71
|
+
modelId: selection?.modelId ?? null,
|
|
72
|
+
available: !disposed && runtime?.available === true,
|
|
73
|
+
installed: runtime?.installed === true,
|
|
74
|
+
error: runtime?.error ?? null
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function acceptRuntime(value) {
|
|
79
|
+
runtime = value ?? null;
|
|
80
|
+
models = is.array(runtime?.models) ? runtime.models : [];
|
|
81
|
+
for (const request of requests) {
|
|
82
|
+
const model = models.find(function requestModel(record) { return record.id === request.modelId; });
|
|
83
|
+
if (model?.loaded === true) request.loaded = true;
|
|
84
|
+
if (runtime?.available !== true || (request.loaded && (runtime.state !== 'ready' || model?.loaded !== true))) {
|
|
85
|
+
request.controller.abort(localAIError('ARCANE_AI_MODEL_NOT_READY', 'The requested local model is no longer ready.', runtime?.error));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return status();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const unsubscribe = client?.events.on('localai.state', function localRuntimeChanged(snapshot) {
|
|
92
|
+
if (disposed || !is.array(snapshot?.runtimes)) return;
|
|
93
|
+
lifecycleRevision += 1;
|
|
94
|
+
acceptRuntime(snapshot.runtimes.find(function llamaRuntime(value) {
|
|
95
|
+
return value.id === 'llama.cpp';
|
|
96
|
+
}));
|
|
97
|
+
}) ?? function noCoreSubscription() {};
|
|
98
|
+
|
|
99
|
+
function authority(value) {
|
|
100
|
+
return {protocol: AUTHORITY_PROTOCOL, providerId: id, modelId: value.modelId};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function inspect(value, {signal} = {}) {
|
|
104
|
+
requireSelection(value);
|
|
105
|
+
if (signal?.aborted) throw cancelled(signal.reason);
|
|
106
|
+
try {
|
|
107
|
+
const core = requireClient();
|
|
108
|
+
const statusRevision = lifecycleRevision;
|
|
109
|
+
const current = await core.invoke('llama.status', {}, {signal});
|
|
110
|
+
if (lifecycleRevision === statusRevision) acceptRuntime(current);
|
|
111
|
+
if (runtime?.available !== true) {
|
|
112
|
+
return {
|
|
113
|
+
available: false,
|
|
114
|
+
code: 'ARCANE_AI_PROVIDER_UNAVAILABLE',
|
|
115
|
+
message: 'The local llama.cpp runtime is unavailable.',
|
|
116
|
+
error: runtime?.error ?? null
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (!models.some(function matchesModel(model) { return model.id === value.modelId; })) {
|
|
120
|
+
return {
|
|
121
|
+
available: false,
|
|
122
|
+
code: 'ARCANE_AI_MODEL_UNAVAILABLE',
|
|
123
|
+
message: 'The selected model is unavailable in the local llama.cpp runtime.'
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {available: true, authority: authority(value)};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (signal?.aborted || error?.name === 'AbortError') throw error;
|
|
129
|
+
acceptRuntime({...runtime, available: false, state: 'error', error});
|
|
130
|
+
return {available: false, code: error.code ?? 'ARCANE_AI_CORE_UNAVAILABLE', message: error.message, error};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function load({selection: value, signal, progress} = {}) {
|
|
135
|
+
const core = requireClient();
|
|
136
|
+
requireSelection(value);
|
|
137
|
+
if (signal?.aborted) throw cancelled(signal.reason);
|
|
138
|
+
selection = value;
|
|
139
|
+
loading = true;
|
|
140
|
+
try {
|
|
141
|
+
progress?.({phase: 'loading', modelId: value.modelId});
|
|
142
|
+
const revision = lifecycleRevision;
|
|
143
|
+
const current = await core.invoke('llama.load', {model: value.modelId}, {signal});
|
|
144
|
+
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
145
|
+
if (!modelReady()) throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Core did not report the selected model loaded and ready.', current?.error);
|
|
146
|
+
progress?.({phase: 'ready', modelId: value.modelId});
|
|
147
|
+
return {authority: authority(value), status: {...status(), state: 'ready', busy: false}};
|
|
148
|
+
} finally {
|
|
149
|
+
loading = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ownRequest(signal, modelId) {
|
|
154
|
+
const controller = new AbortController();
|
|
155
|
+
function forwardAbort() { controller.abort(signal.reason); }
|
|
156
|
+
signal?.addEventListener('abort', forwardAbort, {once: true});
|
|
157
|
+
if (signal?.aborted) forwardAbort();
|
|
158
|
+
const request = {controller, result: null, modelId, loaded: models.some(function loadedTarget(model) {
|
|
159
|
+
return model.id === modelId && model.loaded === true;
|
|
160
|
+
})};
|
|
161
|
+
requests.add(request);
|
|
162
|
+
request.release = function releaseRequest() {
|
|
163
|
+
signal?.removeEventListener('abort', forwardAbort);
|
|
164
|
+
requests.delete(request);
|
|
165
|
+
};
|
|
166
|
+
return request;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function streamRequest(core, parameters, signal) {
|
|
170
|
+
const request = ownRequest(signal, parameters.model);
|
|
171
|
+
const chunks = [];
|
|
172
|
+
const readers = [];
|
|
173
|
+
let complete = false;
|
|
174
|
+
let failure = null;
|
|
175
|
+
sequence += 1;
|
|
176
|
+
const streamId = is.function(core.uuid) ? core.uuid()
|
|
177
|
+
: globalThis.crypto?.randomUUID?.() ?? `${id}:${Date.now()}:${sequence}:${Math.random()}`;
|
|
178
|
+
|
|
179
|
+
function flush() {
|
|
180
|
+
while (readers.length && chunks.length && !request.controller.signal.aborted) {
|
|
181
|
+
readers.shift().resolve({value: chunks.shift(), done: false});
|
|
182
|
+
}
|
|
183
|
+
if (!complete || (chunks.length && !request.controller.signal.aborted)) return;
|
|
184
|
+
while (readers.length) {
|
|
185
|
+
const reader = readers.shift();
|
|
186
|
+
if (failure) reader.reject(failure);
|
|
187
|
+
else reader.resolve({value: undefined, done: true});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const stopChunks = core.events.on('llama.chunk', function receiveChunk(event) {
|
|
192
|
+
if (event?.streamId !== streamId || complete || request.controller.signal.aborted) return;
|
|
193
|
+
chunks.push(event.chunk);
|
|
194
|
+
flush();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
request.result = Promise.resolve().then(function invokeStream() {
|
|
198
|
+
return core.invoke('llama.chat', {...parameters, stream: true, streamId}, {signal: request.controller.signal});
|
|
199
|
+
}).then(function streamCompleted(result) {
|
|
200
|
+
if (request.controller.signal.aborted) throw cancelled(request.controller.signal.reason);
|
|
201
|
+
return result;
|
|
202
|
+
}).catch(function streamFailed(error) {
|
|
203
|
+
failure = error;
|
|
204
|
+
throw error;
|
|
205
|
+
}).finally(function releaseStream() {
|
|
206
|
+
complete = true;
|
|
207
|
+
stopChunks();
|
|
208
|
+
request.release();
|
|
209
|
+
flush();
|
|
210
|
+
});
|
|
211
|
+
request.result.catch(function observeRetainedStreamFailure() {});
|
|
212
|
+
|
|
213
|
+
function cancel(reason) {
|
|
214
|
+
if (!complete) request.controller.abort(reason);
|
|
215
|
+
return request.result.then(function cancelledAfterSuccess() {}, function cancelledAfterFailure() {});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
result: request.result,
|
|
220
|
+
cancel,
|
|
221
|
+
[Symbol.asyncIterator]() {
|
|
222
|
+
return {
|
|
223
|
+
next() {
|
|
224
|
+
if (request.controller.signal.aborted) return Promise.reject(failure ?? cancelled(request.controller.signal.reason));
|
|
225
|
+
if (chunks.length) return Promise.resolve({value: chunks.shift(), done: false});
|
|
226
|
+
if (complete) return failure ? Promise.reject(failure) : Promise.resolve({value: undefined, done: true});
|
|
227
|
+
return new Promise(function awaitChunk(resolve, reject) { readers.push({resolve, reject}); });
|
|
228
|
+
},
|
|
229
|
+
async return() {
|
|
230
|
+
await cancel();
|
|
231
|
+
return {value: undefined, done: true};
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function request({selection: value, operation, payload, signal} = {}) {
|
|
239
|
+
const core = requireClient();
|
|
240
|
+
requireSelection(value);
|
|
241
|
+
if (signal?.aborted) throw cancelled(signal.reason);
|
|
242
|
+
if (loading || selection?.modelId !== value.modelId) {
|
|
243
|
+
throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Load the selected local model before requesting inference.');
|
|
244
|
+
}
|
|
245
|
+
const model = payload?.model ?? value.modelId;
|
|
246
|
+
if (!models.some(function knownModel(record) { return record.id === model; })) {
|
|
247
|
+
const revision = lifecycleRevision;
|
|
248
|
+
const current = await core.invoke('llama.status', {}, {signal});
|
|
249
|
+
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
250
|
+
}
|
|
251
|
+
if (!models.some(function knownModel(record) { return record.id === model; })) {
|
|
252
|
+
throw localAIError('ARCANE_AI_MODEL_UNAVAILABLE', 'The requested model is unavailable in the local llama.cpp runtime.');
|
|
253
|
+
}
|
|
254
|
+
if (runtime?.available !== true || runtime.state !== 'ready'
|
|
255
|
+
|| (model === value.modelId && !modelReady())) {
|
|
256
|
+
throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'The requested local runtime or selected model is not ready.');
|
|
257
|
+
}
|
|
258
|
+
const parameters = {model, payload};
|
|
259
|
+
if (operation === 'stream') return streamRequest(core, parameters, signal);
|
|
260
|
+
if (operation !== 'chat') throw localAIError('ARCANE_AI_PROVIDER_OPERATION_UNAVAILABLE', 'The local llama.cpp provider supports chat and stream.');
|
|
261
|
+
const pending = ownRequest(signal, model);
|
|
262
|
+
pending.result = Promise.resolve().then(function invokeChat() {
|
|
263
|
+
return core.invoke('llama.chat', {...parameters, stream: false}, {signal: pending.controller.signal});
|
|
264
|
+
}).then(function chatCompleted(result) {
|
|
265
|
+
if (pending.controller.signal.aborted) throw cancelled(pending.controller.signal.reason);
|
|
266
|
+
return result;
|
|
267
|
+
}).finally(pending.release);
|
|
268
|
+
return pending.result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function unload({selection: value = selection, signal} = {}) {
|
|
272
|
+
const core = requireClient();
|
|
273
|
+
if (signal?.aborted) throw cancelled(signal.reason);
|
|
274
|
+
for (const request of requests) request.controller.abort();
|
|
275
|
+
await Promise.allSettled([...requests].map(function activeResult(request) { return request.result; }));
|
|
276
|
+
if (!value) return status();
|
|
277
|
+
requireSelection(value);
|
|
278
|
+
const revision = lifecycleRevision;
|
|
279
|
+
const current = await core.invoke('llama.unload', {model: value.modelId}, {signal});
|
|
280
|
+
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
281
|
+
selection = null;
|
|
282
|
+
return status();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function dispose(options = {}) {
|
|
286
|
+
if (disposed) return status();
|
|
287
|
+
if (selection || requests.size) await unload(options);
|
|
288
|
+
disposed = true;
|
|
289
|
+
unsubscribe();
|
|
290
|
+
return status();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
protocol: PROVIDER_PROTOCOL,
|
|
295
|
+
role: 'llm',
|
|
296
|
+
id,
|
|
297
|
+
localOnly: true,
|
|
298
|
+
catalog: function catalog() { return [...models]; },
|
|
299
|
+
status,
|
|
300
|
+
inspect,
|
|
301
|
+
load,
|
|
302
|
+
request,
|
|
303
|
+
unload,
|
|
304
|
+
dispose
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -2,6 +2,7 @@ import {arcaneEvents} from '../event-manager.mjs';
|
|
|
2
2
|
import {CORE_PROTOCOL,CORE_READY_EVENTS,CoreError,serializeCoreError} from './contracts.mjs';
|
|
3
3
|
|
|
4
4
|
const CORE_CLIENT_KEY=Symbol.for('arcane-os.core.client');
|
|
5
|
+
const CORE_CLIENT_INSTALLATION=Symbol.for('arcane-os.core.client.installation');
|
|
5
6
|
const CORE_EVENT='core.rpc.event';
|
|
6
7
|
const LONG_OPERATION_TIMEOUT=50*60*1000;
|
|
7
8
|
|
|
@@ -12,6 +13,7 @@ export function createCoreClient({
|
|
|
12
13
|
eventOwner=arcaneEvents,
|
|
13
14
|
eventOwnerReady,
|
|
14
15
|
autoConnect=true,
|
|
16
|
+
replayRuntimeState=false,
|
|
15
17
|
onError=error=>global.console?.error('Arcane Core client failed.',error)
|
|
16
18
|
}={}){
|
|
17
19
|
const pending=new Map();
|
|
@@ -25,8 +27,14 @@ export function createCoreClient({
|
|
|
25
27
|
let transport=null;
|
|
26
28
|
let closed=false;
|
|
27
29
|
let eventOwnerFailure=null;
|
|
30
|
+
let transportFailure=null;
|
|
31
|
+
let transportFailureReported=false;
|
|
28
32
|
|
|
29
33
|
function report(error){
|
|
34
|
+
if(error===transportFailure){
|
|
35
|
+
if(transportFailureReported)return;
|
|
36
|
+
transportFailureReported=true;
|
|
37
|
+
}
|
|
30
38
|
try{onError(error instanceof CoreError?error:new CoreError(error));}
|
|
31
39
|
catch(reportError){global.console?.error('Arcane Core error listener failed.',reportError,error);}
|
|
32
40
|
}
|
|
@@ -260,7 +268,7 @@ export function createCoreClient({
|
|
|
260
268
|
throw new CoreError({code:'ARCANE_TRANSPORT_UNAVAILABLE',message:'This interface is not connected to an Arcane native host.'});
|
|
261
269
|
}
|
|
262
270
|
function connect(){
|
|
263
|
-
if(closed)throw new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'});
|
|
271
|
+
if(closed)throw transportFailure??new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'});
|
|
264
272
|
if(transport)return transport;
|
|
265
273
|
const cleanupStart=cleanups.length;
|
|
266
274
|
try{
|
|
@@ -271,7 +279,11 @@ export function createCoreClient({
|
|
|
271
279
|
transport=selected;
|
|
272
280
|
if(typeof transport.subscribe==='function'){
|
|
273
281
|
const unsubscribe=transport.subscribe(receive);
|
|
274
|
-
if(typeof unsubscribe==='function')
|
|
282
|
+
if(typeof unsubscribe==='function'){
|
|
283
|
+
if(closed){
|
|
284
|
+
try{unsubscribe();}catch(error){report(error);}
|
|
285
|
+
}else cleanups.push(unsubscribe);
|
|
286
|
+
}
|
|
275
287
|
}
|
|
276
288
|
}catch(error){
|
|
277
289
|
transport=null;
|
|
@@ -280,7 +292,11 @@ export function createCoreClient({
|
|
|
280
292
|
}
|
|
281
293
|
throw error;
|
|
282
294
|
}
|
|
295
|
+
if(closed)throw transportFailure??new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'});
|
|
283
296
|
complete('transport.ready',{protocol:CORE_PROTOCOL,transport:transport.name});
|
|
297
|
+
if(replayRuntimeState&&!closed)sendControl({protocol:CORE_PROTOCOL,type:'control',control:'runtime.replay'});
|
|
298
|
+
// A native callback may close the client synchronously during replay.
|
|
299
|
+
if(closed)throw transportFailure??new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'});
|
|
284
300
|
return transport;
|
|
285
301
|
}
|
|
286
302
|
function runtimeSnapshot(){
|
|
@@ -312,6 +328,7 @@ export function createCoreClient({
|
|
|
312
328
|
return true;
|
|
313
329
|
}
|
|
314
330
|
function invoke(method,parameters={},options={}){
|
|
331
|
+
if(transportFailure)return Promise.reject(transportFailure);
|
|
315
332
|
if(eventOwnerFailure)return Promise.reject(eventOwnerFailure);
|
|
316
333
|
let selected;
|
|
317
334
|
try{selected=connect();}catch(error){return Promise.reject(error);}
|
|
@@ -343,23 +360,37 @@ export function createCoreClient({
|
|
|
343
360
|
}catch(error){sendFailed(error);}
|
|
344
361
|
return promise;
|
|
345
362
|
}
|
|
346
|
-
function
|
|
347
|
-
if(closed)return false;
|
|
348
|
-
closed=true;
|
|
349
|
-
cancelAll();
|
|
363
|
+
function disposeTransport(error){
|
|
350
364
|
for(const record of acknowledgements.values()){
|
|
351
365
|
clearTimeout(record.timer);
|
|
352
|
-
record.reject(
|
|
366
|
+
record.reject(error);
|
|
353
367
|
}
|
|
354
368
|
acknowledgements.clear();
|
|
355
369
|
for(const cleanup of cleanups.splice(0)){
|
|
356
370
|
try{cleanup();}catch(error){report(error);}
|
|
357
371
|
}
|
|
358
|
-
source?.dispose();
|
|
372
|
+
try{source?.dispose();}catch(disposeError){report(disposeError);}
|
|
373
|
+
source=null;
|
|
374
|
+
transport=null;
|
|
359
375
|
// Registration closures and already-delivered payloads belong to this
|
|
360
376
|
// client lifetime and are released when the owner explicitly closes it.
|
|
361
377
|
eventActions.length=0;
|
|
362
378
|
completedEvents.clear();
|
|
379
|
+
}
|
|
380
|
+
function failTransport(error){
|
|
381
|
+
if(closed)return false;
|
|
382
|
+
transportFailure=error instanceof CoreError?error:new CoreError(error);
|
|
383
|
+
closed=true;
|
|
384
|
+
for(const id of pending.keys())settle(id,transportFailure);
|
|
385
|
+
disposeTransport(transportFailure);
|
|
386
|
+
report(transportFailure);
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
function close(){
|
|
390
|
+
if(closed)return false;
|
|
391
|
+
closed=true;
|
|
392
|
+
cancelAll();
|
|
393
|
+
disposeTransport(new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'}));
|
|
363
394
|
return true;
|
|
364
395
|
}
|
|
365
396
|
if(typeof global.addEventListener==='function'){
|
|
@@ -369,7 +400,7 @@ export function createCoreClient({
|
|
|
369
400
|
if(autoConnect){
|
|
370
401
|
try{connect();}catch(error){if(error.code!=='ARCANE_TRANSPORT_UNAVAILABLE')report(error);}
|
|
371
402
|
}
|
|
372
|
-
return {protocol:CORE_PROTOCOL,Error:CoreError,invoke,receive,connect,close,cancelAll,events,eventsReady,runtime:{current:runtimeSnapshot},uuid};
|
|
403
|
+
return {protocol:CORE_PROTOCOL,Error:CoreError,invoke,receive,connect,close,failTransport,cancelAll,events,eventsReady,runtime:{current:runtimeSnapshot},uuid};
|
|
373
404
|
}
|
|
374
405
|
|
|
375
406
|
/** Existing namespace call shapes; method implementations remain host-owned. */
|
|
@@ -461,28 +492,58 @@ export function createCoreFacade(client){
|
|
|
461
492
|
};
|
|
462
493
|
}
|
|
463
494
|
|
|
495
|
+
/** Reads an installed SDK client without creating a transport or changing globals. */
|
|
496
|
+
export function getInstalledCoreClient(global=globalThis){
|
|
497
|
+
const client=global?.[CORE_CLIENT_KEY];
|
|
498
|
+
const installation=client?.[CORE_CLIENT_INSTALLATION];
|
|
499
|
+
return installation?.global===global&&installation.active?client:null;
|
|
500
|
+
}
|
|
501
|
+
|
|
464
502
|
/** Installs the same synchronous facade used by classic native-host scripts. */
|
|
465
503
|
export function installCoreClient(global=globalThis,options={}){
|
|
466
504
|
if(global[CORE_CLIENT_KEY])return global[CORE_CLIENT_KEY];
|
|
467
505
|
const client=createCoreClient({...options,global,autoConnect:false});
|
|
468
506
|
const facade=createCoreFacade(client);
|
|
507
|
+
const installation={global,active:true};
|
|
508
|
+
client[CORE_CLIENT_INSTALLATION]=installation;
|
|
509
|
+
const previousFacade=global.Arcane;
|
|
469
510
|
global.Arcane=facade;
|
|
470
511
|
global[CORE_CLIENT_KEY]=client;
|
|
471
512
|
const previous=global.__arcaneReceive;
|
|
472
513
|
global.__arcaneReceive=client.receive;
|
|
514
|
+
const previousFailure=global.__arcaneTransportFailed;
|
|
515
|
+
function nativeTransportFailed(error){return client.failTransport(error);}
|
|
516
|
+
global.__arcaneTransportFailed=nativeTransportFailed;
|
|
473
517
|
const close=client.close;
|
|
474
|
-
client.
|
|
475
|
-
|
|
518
|
+
const failTransport=client.failTransport;
|
|
519
|
+
let installedTransportFailure=null;
|
|
520
|
+
function releaseInstalledClient(){
|
|
521
|
+
installation.active=false;
|
|
476
522
|
if(global[CORE_CLIENT_KEY]===client)delete global[CORE_CLIENT_KEY];
|
|
477
|
-
if(global.Arcane===facade)
|
|
523
|
+
if(global.Arcane===facade){
|
|
524
|
+
if(previousFacade===undefined)delete global.Arcane;
|
|
525
|
+
else global.Arcane=previousFacade;
|
|
526
|
+
}
|
|
478
527
|
if(global.__arcaneReceive===client.receive){
|
|
479
528
|
if(previous===undefined)delete global.__arcaneReceive;
|
|
480
529
|
else global.__arcaneReceive=previous;
|
|
481
530
|
}
|
|
482
|
-
|
|
531
|
+
if(global.__arcaneTransportFailed===nativeTransportFailed){
|
|
532
|
+
if(previousFailure===undefined)delete global.__arcaneTransportFailed;
|
|
533
|
+
else global.__arcaneTransportFailed=previousFailure;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
client.close=function closeInstalledClient(){
|
|
537
|
+
installation.active=false;
|
|
538
|
+
try{return close();}finally{releaseInstalledClient();}
|
|
539
|
+
};
|
|
540
|
+
client.failTransport=function failInstalledTransport(error){
|
|
541
|
+
if(installation.active)installedTransportFailure=error instanceof CoreError?error:new CoreError(error);
|
|
542
|
+
releaseInstalledClient();
|
|
543
|
+
return failTransport(installedTransportFailure??error);
|
|
483
544
|
};
|
|
484
545
|
try{client.connect();}catch(error){
|
|
485
|
-
if(error.code!=='ARCANE_TRANSPORT_UNAVAILABLE'){
|
|
546
|
+
if(error!==installedTransportFailure&&error.code!=='ARCANE_TRANSPORT_UNAVAILABLE'){
|
|
486
547
|
if(options.onError)options.onError(error);
|
|
487
548
|
else global.console?.error('Arcane Core transport initialization failed.',error);
|
|
488
549
|
}
|
|
@@ -7,7 +7,8 @@ export const CORE_FRAME_CONTRACTS={
|
|
|
7
7
|
response:{type:'response',fields:['protocol','type','id','ok'],success:['result'],failure:['error'],optional:['time']},
|
|
8
8
|
event:{type:'event',fields:['protocol','type','event','data'],optional:['time']},
|
|
9
9
|
cancel:{type:'control',control:'request.cancel',fields:['protocol','type','control','requestId']},
|
|
10
|
-
cancelAll:{type:'control',control:'requests.cancelAll',fields:['protocol','type','control']}
|
|
10
|
+
cancelAll:{type:'control',control:'requests.cancelAll',fields:['protocol','type','control']},
|
|
11
|
+
replay:{type:'control',control:'runtime.replay',fields:['protocol','type','control']}
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
/** These names describe existing methods; installed services determine availability. */
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Consumer updates
|
|
2
|
+
|
|
3
|
+
Development applications track the published npm `latest` channel. Declare
|
|
4
|
+
`"arcane-os": "latest"` in the application's existing dependency section.
|
|
5
|
+
Preserve an existing package alias where the application needs it; Arcane OS,
|
|
6
|
+
for example, uses `"arcane-sdk": "npm:arcane-os@latest"` to avoid a name collision.
|
|
7
|
+
Pin an application's SDK version only when Roshi explicitly designates it as a
|
|
8
|
+
production build. An exact version recorded in `package-lock.json` records what
|
|
9
|
+
was installed; it does not turn a development application into a production pin.
|
|
10
|
+
|
|
11
|
+
## Development refresh
|
|
12
|
+
|
|
13
|
+
Each application's existing owner performs its refresh through the public npm
|
|
14
|
+
package workflow in its canonical `main` checkout. Coordinate the dependency
|
|
15
|
+
and Git mutation window before cleanup, preserve active processes and other
|
|
16
|
+
owners' work, and establish applicable dependency permissions before removing
|
|
17
|
+
the installed tree. Respect explicit project stops, including Lifeline.
|
|
18
|
+
|
|
19
|
+
1. Keep the SDK declaration at `latest` and preserve `package-lock.json`.
|
|
20
|
+
2. Resolve the exact application path and remove only that application's
|
|
21
|
+
disposable `node_modules`, before pulling. Do not sweep sibling projects or
|
|
22
|
+
shared caches.
|
|
23
|
+
3. Run `git pull` and observe the result. Preserve existing changes and stop the
|
|
24
|
+
affected operation if the pull fails or leaves conflicts.
|
|
25
|
+
4. Run `npm i` through the application's supported install workflow, retaining
|
|
26
|
+
its applicable lifecycle-script options.
|
|
27
|
+
5. Explicitly run `npm update arcane-os` to refresh only the SDK. For an existing
|
|
28
|
+
alias, target its dependency key, such as `npm update arcane-sdk`.
|
|
29
|
+
6. Before materializing managed files or building, obtain npm's published
|
|
30
|
+
`latest` version with a fresh registry lookup, such as
|
|
31
|
+
`npm view arcane-os@latest version --prefer-online`. Read the actual installed
|
|
32
|
+
package version and the lockfile's resolved SDK version. Both must match that
|
|
33
|
+
published version. If the tag advances during the operation, refresh the SDK
|
|
34
|
+
again and report the actual selected version.
|
|
35
|
+
7. Update managed SDK projections only through the application's supported
|
|
36
|
+
public package workflow. Commit the intended declaration, retained updated
|
|
37
|
+
lockfile, managed files, and relevant instructions through its normal owner.
|
|
38
|
+
|
|
39
|
+
A `latest` declaration and `npm i` alone are insufficient: an existing lockfile
|
|
40
|
+
can preserve an older resolution. Keep and update the lockfile instead of
|
|
41
|
+
deleting it and re-resolving unrelated dependencies. Surface installation and
|
|
42
|
+
compatibility failures; do not claim current adoption from a manifest label.
|
|
43
|
+
|
|
44
|
+
This dependency-result comparison is part of the requested refresh. It does
|
|
45
|
+
not authorize application tests, unrelated checks or dependency updates,
|
|
46
|
+
recurring polling, broad cleanup, or production administration. A later release
|
|
47
|
+
can advance `latest`; record the exact version selected by each refresh.
|
|
48
|
+
|
|
49
|
+
## Production builds
|
|
50
|
+
|
|
51
|
+
On Roshi's explicit production designation, record the selected exact SDK
|
|
52
|
+
version consistently in the dependency declaration, lockfile, managed
|
|
53
|
+
projections, and application instructions. Use the supported locked install
|
|
54
|
+
workflow, normally `npm ci`, to reproduce that selection. A production pin
|
|
55
|
+
records reproducibility, not a permanent ceiling on otherwise authorized
|
|
56
|
+
needed updates. Preserve an explicit update deferral or actual compatibility
|
|
57
|
+
constraint.
|
|
58
|
+
|
|
59
|
+
## Feature requests and release notices
|
|
60
|
+
|
|
61
|
+
Inspect the published SDK's public API and documentation before implementing a
|
|
62
|
+
request. If it already supports the requested behavior, tell the requesting
|
|
63
|
+
application owner which API and usage provide it and link directly to the
|
|
64
|
+
relevant documentation. Do not present unpublished source as available support.
|
|
65
|
+
|
|
66
|
+
After a release's npm version and channel are confirmed, notify only existing
|
|
67
|
+
application owners whose requests it addresses. Notify every requester covered
|
|
68
|
+
by matching, similar, or bundled requests when one shared feature serves them.
|
|
69
|
+
Do not send routine release broadcasts to unrelated consumers.
|
|
70
|
+
|
|
71
|
+
Each notice includes the exact published version and channel, requested
|
|
72
|
+
functionality delivered, its relevance, release notes or a changelog link,
|
|
73
|
+
direct API and usage documentation references, and required compatibility or
|
|
74
|
+
adoption steps. Keep publication evidence distinct from application behavior.
|
|
75
|
+
Deduplicate by version and recipient and retain send outcomes in the release
|
|
76
|
+
handoff. Report unreachable owners and resume missing deliveries.
|
|
77
|
+
|
|
78
|
+
After every release and applicable requester notices, update the documentation
|
|
79
|
+
and existing SDK website for all affected public contracts, including releases
|
|
80
|
+
with no requesters. Record those delivery outcomes separately. The SDK owner
|
|
81
|
+
retains this follow-through; application upgrades and tests remain with their
|
|
82
|
+
owners and never gate SDK publication. See [npm publication](publishing.md).
|