arcane-os 0.83.1 → 0.85.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 +38 -0
- package/README.md +1 -1
- package/browser-runtime/ai/model-observation.mjs +315 -0
- package/docs/reference/core-native-packaging.md +35 -0
- package/docs/reference/core-runtime.md +37 -1
- package/docs/reference/core-shared-host.md +79 -10
- package/docs/reference/execution-devices.md +6 -0
- package/docs/reference/inventory/package-api.json +64 -8
- package/docs/reference/model-observation.md +220 -0
- package/docs/reference/sdk-api.md +185 -3
- package/package.json +3 -1
- package/src/core/host.mjs +1 -1
- package/src/core/runtime-connection.mjs +168 -0
- package/src/core/runtime.mjs +18 -6
- package/src/core/services/model-observation.mjs +231 -0
- package/src/core/shared-host.mjs +270 -70
- package/src/core/stdio.mjs +18 -3
- package/src/local-ai/execution-devices-windows.ps1 +3 -1
- package/src/native/portable-layout.mjs +46 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.85.0
|
|
4
|
+
|
|
5
|
+
- Add `createModelObservationService({runtime, localAI, image, decisions})`
|
|
6
|
+
through `arcane-os/core/model-observation`, and renderer
|
|
7
|
+
`attachModelObservation` / observer `subscribeModelObservation` through
|
|
8
|
+
`arcane-os/ai/model-observation`. Observe the actual existing Core and
|
|
9
|
+
renderer model owners without creating, loading or replacing a runtime,
|
|
10
|
+
provider or model.
|
|
11
|
+
- Replay complete current owner state and retain its reported errors, with
|
|
12
|
+
explicit opt-in live diagnostics and connection-owned cancellation. Keep
|
|
13
|
+
original client request identity separate from actual internal Core request
|
|
14
|
+
identity. Native `coreListener` composition reuses existing selected services;
|
|
15
|
+
applications explicitly attach their renderer owners.
|
|
16
|
+
- Preserve the complete Windows PnP property batch while narrowing only the
|
|
17
|
+
property cmdlet's internal concurrency. Keep asynchronous shared discovery,
|
|
18
|
+
cancellation, explicit refresh and model/device selection unchanged. This
|
|
19
|
+
addresses the observed missing/duplicate property rows without adding retries
|
|
20
|
+
or another hardware query.
|
|
21
|
+
- These capabilities expose current state and correct the property query;
|
|
22
|
+
they do not claim to repair or verify native model inference. No dependency
|
|
23
|
+
changes or native host rebuild are required for this revision.
|
|
24
|
+
|
|
25
|
+
## 0.84.0
|
|
26
|
+
|
|
27
|
+
- Add `startCoreListener({runtime, endpoint, onError})` through
|
|
28
|
+
`arcane-os/core/host` so local clients can use an application's existing
|
|
29
|
+
window-owned Core without creating another runtime or changing its profile,
|
|
30
|
+
service instances or saved-data location. Native packages select it with
|
|
31
|
+
`native.launchContext.coreListener: {endpoint}`.
|
|
32
|
+
- Keep response correlation, cancellation and current-state replay scoped to
|
|
33
|
+
each connection. Preserve complete application payloads, expose the original
|
|
34
|
+
handler `requestId` and actual internal `coreRequestId`, and prevent retired
|
|
35
|
+
request events from attaching to a later reuse of a client ID.
|
|
36
|
+
- Closing a client or listener leaves accepted service-owned work with its
|
|
37
|
+
runtime. Window shutdown drains Core and then closes its listener. Existing
|
|
38
|
+
explicitly selected headless shared-host behavior remains available. This
|
|
39
|
+
capability does not claim to correct or verify model inference.
|
|
40
|
+
|
|
3
41
|
## 0.83.1
|
|
4
42
|
|
|
5
43
|
- Preserve complete failure-only native ONNX diagnostics in `error.onnxRun`,
|
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.85.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;
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import Is from '../dependencies/strong-type/index.js';
|
|
2
|
+
import {CoreError, serializeCoreError} from '../core/contracts.mjs';
|
|
3
|
+
import {getInstalledCoreClient, subscribeCoreClient} from '../core/client.mjs';
|
|
4
|
+
|
|
5
|
+
const is = new Is(false);
|
|
6
|
+
|
|
7
|
+
/** Attach the caller's existing renderer owners to its existing Core client. */
|
|
8
|
+
export function attachModelObservation({client, aiRuntimeState = null, imageRuntime = null,
|
|
9
|
+
decisionModel = null, modelController = null, signal, onError} = {}) {
|
|
10
|
+
requireOwner(aiRuntimeState, ['getAIRuntimeState', 'subscribeAIRuntimeState'], 'aiRuntimeState');
|
|
11
|
+
requireOwner(imageRuntime, ['current', 'subscribe'], 'imageRuntime');
|
|
12
|
+
requireOwner(decisionModel, ['status', 'subscribe'], 'decisionModel');
|
|
13
|
+
requireOwner(modelController, ['status', 'on'], 'modelController');
|
|
14
|
+
const lifetime = observationLifetime({client, signal, onError});
|
|
15
|
+
let attachmentRequestId;
|
|
16
|
+
let coreRequestId;
|
|
17
|
+
let snapshot = null;
|
|
18
|
+
let attached = false;
|
|
19
|
+
|
|
20
|
+
function readOwners() {
|
|
21
|
+
return {
|
|
22
|
+
aiRuntimeState: aiRuntimeState?.getAIRuntimeState() ?? null,
|
|
23
|
+
imageRuntime: imageRuntime?.current() ?? null,
|
|
24
|
+
decisionModel: decisionModel?.status() ?? null,
|
|
25
|
+
modelController: modelController?.status() ?? null
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function publishOwners() {
|
|
30
|
+
if (!attached || lifetime.stopped) return;
|
|
31
|
+
try {
|
|
32
|
+
snapshot = readOwners();
|
|
33
|
+
lifetime.invoke('model.observation.renderer.publish', {
|
|
34
|
+
attachmentRequestId, coreRequestId, snapshot: transportSnapshot(snapshot)
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
lifetime.fail(error);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function observeOwners() {
|
|
42
|
+
if (aiRuntimeState) lifetime.cleanup(aiRuntimeState.subscribeAIRuntimeState(
|
|
43
|
+
publishOwners, {emitCurrent: false, signal: lifetime.signal}
|
|
44
|
+
));
|
|
45
|
+
if (imageRuntime) lifetime.cleanup(imageRuntime.subscribe(
|
|
46
|
+
publishOwners, {replay: false, signal: lifetime.signal}
|
|
47
|
+
));
|
|
48
|
+
if (decisionModel) lifetime.cleanup(decisionModel.subscribe(
|
|
49
|
+
publishOwners, {emitCurrent: false, signal: lifetime.signal}
|
|
50
|
+
));
|
|
51
|
+
if (modelController) {
|
|
52
|
+
lifetime.cleanup(modelController.on('statechange', publishOwners));
|
|
53
|
+
lifetime.cleanup(modelController.on('progress', publishOwners));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function acknowledgeAttachment(value) {
|
|
58
|
+
if (lifetime.stopped || attached || value?.attachmentRequestId !== attachmentRequestId) return;
|
|
59
|
+
try {
|
|
60
|
+
if (!is.string(value.coreRequestId)) {
|
|
61
|
+
throw observationError('MODEL_OBSERVATION_CONTEXT_UNAVAILABLE',
|
|
62
|
+
'The renderer acknowledgement omitted the actual Core request identity.');
|
|
63
|
+
}
|
|
64
|
+
coreRequestId = value.coreRequestId;
|
|
65
|
+
attached = true;
|
|
66
|
+
observeOwners();
|
|
67
|
+
// Read again after all subscriptions exist so changes during the
|
|
68
|
+
// attachment handshake are represented by current owner state.
|
|
69
|
+
publishOwners();
|
|
70
|
+
lifetime.acknowledge(value);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
lifetime.fail(error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!lifetime.stopped) {
|
|
77
|
+
try {
|
|
78
|
+
snapshot = readOwners();
|
|
79
|
+
lifetime.cleanup(client.events.on('model.observation.renderer.attached', acknowledgeAttachment));
|
|
80
|
+
lifetime.invoke('model.observation.renderer.attach', {snapshot: transportSnapshot(snapshot)}, {
|
|
81
|
+
long: true,
|
|
82
|
+
onRequest: function identifyAttachment(value) { attachmentRequestId = value.requestId; }
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
lifetime.fail(error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
ready: lifetime.ready,
|
|
91
|
+
closed: lifetime.closed,
|
|
92
|
+
current: function currentRendererObservation() { return snapshot; },
|
|
93
|
+
close: lifetime.close
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Subscribe through an existing Core client; model diagnostics are explicit. */
|
|
98
|
+
export function subscribeModelObservation({client, signal, onState, onDiagnostic, onError} = {}) {
|
|
99
|
+
if (!is.function(onState)) throw new TypeError('Model observation requires an onState listener.');
|
|
100
|
+
if (onDiagnostic !== undefined && !is.function(onDiagnostic)) {
|
|
101
|
+
throw new TypeError('onDiagnostic must be a function.');
|
|
102
|
+
}
|
|
103
|
+
const lifetime = observationLifetime({client, signal, onError});
|
|
104
|
+
let watchRequestId;
|
|
105
|
+
let snapshot = null;
|
|
106
|
+
|
|
107
|
+
function receiveState(value) {
|
|
108
|
+
if (lifetime.stopped || value?.watchRequestId !== watchRequestId) return;
|
|
109
|
+
if (!value.snapshot || !is.object(value.snapshot)) {
|
|
110
|
+
lifetime.fail(observationError('MODEL_OBSERVATION_STATE_INVALID', 'Core omitted the observation snapshot.'));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
snapshot = value.snapshot;
|
|
114
|
+
lifetime.acknowledge(snapshot);
|
|
115
|
+
lifetime.notify(onState, snapshot);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function receiveDiagnostic(value) {
|
|
119
|
+
if (lifetime.stopped || value?.watchRequestId !== watchRequestId) return;
|
|
120
|
+
lifetime.notify(onDiagnostic, value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!lifetime.stopped) {
|
|
124
|
+
try {
|
|
125
|
+
lifetime.cleanup(client.events.on('model.observation.state', receiveState));
|
|
126
|
+
if (onDiagnostic) lifetime.cleanup(client.events.on('model.observation.error', receiveDiagnostic));
|
|
127
|
+
lifetime.invoke('model.observation.watch', {diagnostics: Boolean(onDiagnostic)}, {
|
|
128
|
+
long: true,
|
|
129
|
+
onRequest: function identifyWatch(value) { watchRequestId = value.requestId; }
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
lifetime.fail(error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
ready: lifetime.ready,
|
|
138
|
+
closed: lifetime.closed,
|
|
139
|
+
current: function currentModelObservation() { return snapshot; },
|
|
140
|
+
close: lifetime.close
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function requireOwner(owner, methods, label) {
|
|
145
|
+
if (owner === null) return;
|
|
146
|
+
for (const method of methods) {
|
|
147
|
+
if (!is.function(owner?.[method])) throw new TypeError(`${label} must be an existing owner exposing ${method}().`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function observationError(code, message) {
|
|
152
|
+
return new CoreError({code, message});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function cancelledObservation() {
|
|
156
|
+
return new CoreError({name: 'AbortError', code: 'ARCANE_REQUEST_ABORTED', message: 'Model observation was cancelled.'});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isCancellation(error) {
|
|
160
|
+
return error?.name === 'AbortError' || error?.code === 'ARCANE_REQUEST_ABORTED' || error?.code === 'REQUEST_ABORTED';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Error encoding belongs only to the existing JSON transport boundary. */
|
|
164
|
+
function transportSnapshot(snapshot) {
|
|
165
|
+
return Object.fromEntries(Object.entries(snapshot).map(function transportOwner([name, value]) {
|
|
166
|
+
return [name, value && is.error(value.error) ? {...value, error: serializeCoreError(value.error)} : value];
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function reportObservationFailure(error) {
|
|
171
|
+
globalThis.console?.error('Model observation failed.', error);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Own only observation RPCs and subscriptions, never the supplied owners. */
|
|
175
|
+
function observationLifetime({client, signal, onError = reportObservationFailure}) {
|
|
176
|
+
if (!is.function(client?.invoke) || !is.function(client?.events?.on)) {
|
|
177
|
+
throw new TypeError('Model observation requires an existing Core client.');
|
|
178
|
+
}
|
|
179
|
+
if (!is.function(onError)) throw new TypeError('onError must be a function.');
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
const cleanups = [];
|
|
182
|
+
const pending = new Set();
|
|
183
|
+
const failures = [];
|
|
184
|
+
let stopped = false;
|
|
185
|
+
let acknowledged = false;
|
|
186
|
+
let completed = false;
|
|
187
|
+
let resolveReady;
|
|
188
|
+
let rejectReady;
|
|
189
|
+
let resolveClosed;
|
|
190
|
+
let rejectClosed;
|
|
191
|
+
const ready = new Promise(function ownObservationReadiness(resolve, reject) {
|
|
192
|
+
resolveReady = resolve;
|
|
193
|
+
rejectReady = reject;
|
|
194
|
+
});
|
|
195
|
+
const closed = new Promise(function ownObservationClosure(resolve, reject) {
|
|
196
|
+
resolveClosed = resolve;
|
|
197
|
+
rejectClosed = reject;
|
|
198
|
+
});
|
|
199
|
+
// The same failures are reported through onError and retained on these
|
|
200
|
+
// public promises; unused companion promises must not create extra alerts.
|
|
201
|
+
ready.catch(function readinessFailureObservedByLifetime() {});
|
|
202
|
+
closed.catch(function closureFailureObservedByLifetime() {});
|
|
203
|
+
|
|
204
|
+
function recordFailure(error) {
|
|
205
|
+
if (failures.includes(error)) return;
|
|
206
|
+
failures.push(error);
|
|
207
|
+
try {
|
|
208
|
+
Promise.resolve(onError(error)).catch(function reportErrorCallbackFailed(callbackError) {
|
|
209
|
+
reportObservationFailure(callbackError);
|
|
210
|
+
});
|
|
211
|
+
} catch (callbackError) {
|
|
212
|
+
reportObservationFailure(callbackError);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function finish() {
|
|
217
|
+
if (!stopped || pending.size || completed) return;
|
|
218
|
+
completed = true;
|
|
219
|
+
if (failures.length === 1) rejectClosed(failures[0]);
|
|
220
|
+
else if (failures.length) rejectClosed(new AggregateError(failures, 'Model observation closed with errors.'));
|
|
221
|
+
else resolveClosed();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function close() {
|
|
225
|
+
if (!stopped) {
|
|
226
|
+
stopped = true;
|
|
227
|
+
for (const cleanup of cleanups.splice(0)) {
|
|
228
|
+
try { cleanup(); } catch (error) { recordFailure(error); }
|
|
229
|
+
}
|
|
230
|
+
controller.abort();
|
|
231
|
+
if (!acknowledged) rejectReady(failures[0] ?? cancelledObservation());
|
|
232
|
+
}
|
|
233
|
+
finish();
|
|
234
|
+
return closed;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function fail(error) {
|
|
238
|
+
recordFailure(error);
|
|
239
|
+
close();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function cleanup(release) {
|
|
243
|
+
if (!is.function(release)) throw new TypeError('An observation subscription must return an unsubscribe function.');
|
|
244
|
+
if (stopped) release();
|
|
245
|
+
else cleanups.push(release);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function invoke(method, parameters, {long = false, onRequest} = {}) {
|
|
249
|
+
if (stopped) return;
|
|
250
|
+
// Reserve ownership before invoke: an in-process transport can deliver
|
|
251
|
+
// acknowledgement synchronously while invoke is still on the stack.
|
|
252
|
+
const operation = {};
|
|
253
|
+
pending.add(operation);
|
|
254
|
+
let task;
|
|
255
|
+
try {
|
|
256
|
+
task = client.invoke(method, parameters, {signal: controller.signal, timeoutMs: 0, onRequest});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
task = Promise.reject(error);
|
|
259
|
+
}
|
|
260
|
+
Promise.resolve(task).then(function observationCompleted() {
|
|
261
|
+
pending.delete(operation);
|
|
262
|
+
if (long && !stopped) {
|
|
263
|
+
if (!acknowledged) fail(observationError('MODEL_OBSERVATION_ENDED', 'The observation ended before acknowledgement.'));
|
|
264
|
+
else close();
|
|
265
|
+
}
|
|
266
|
+
finish();
|
|
267
|
+
}, function observationFailed(error) {
|
|
268
|
+
pending.delete(operation);
|
|
269
|
+
if (isCancellation(error)) close();
|
|
270
|
+
else fail(error);
|
|
271
|
+
finish();
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function acknowledge(value) {
|
|
276
|
+
if (stopped || acknowledged) return;
|
|
277
|
+
acknowledged = true;
|
|
278
|
+
resolveReady(value);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function notify(listener, value) {
|
|
282
|
+
if (stopped || !listener) return;
|
|
283
|
+
try {
|
|
284
|
+
// Callback lifetimes belong to callers, who may await close().
|
|
285
|
+
// Observe rejection without making that callback a close barrier.
|
|
286
|
+
Promise.resolve(listener(value)).catch(fail);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
fail(error);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function retireObservation() { close(); }
|
|
293
|
+
if (signal) {
|
|
294
|
+
signal.addEventListener('abort', retireObservation, {once: true});
|
|
295
|
+
cleanup(function releaseCallerSignal() { signal.removeEventListener('abort', retireObservation); });
|
|
296
|
+
}
|
|
297
|
+
if (is.function(globalThis.addEventListener)) {
|
|
298
|
+
globalThis.addEventListener('pagehide', retireObservation, {once: true});
|
|
299
|
+
cleanup(function releasePageLifetime() { globalThis.removeEventListener('pagehide', retireObservation); });
|
|
300
|
+
}
|
|
301
|
+
if (getInstalledCoreClient() === client) {
|
|
302
|
+
cleanup(subscribeCoreClient(function installedOwnerChanged(value) {
|
|
303
|
+
if (value.client !== client) {
|
|
304
|
+
if (value.error) fail(value.error);
|
|
305
|
+
else close();
|
|
306
|
+
}
|
|
307
|
+
}, {emitCurrent: false}));
|
|
308
|
+
}
|
|
309
|
+
if (signal?.aborted) close();
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
ready, closed, signal: controller.signal, cleanup, invoke, acknowledge, notify, fail, close,
|
|
313
|
+
get stopped() { return stopped; }
|
|
314
|
+
};
|
|
315
|
+
}
|
|
@@ -234,6 +234,41 @@ Windows and macOS launchers use the same packaged defaults for native state
|
|
|
234
234
|
selection, so an ordinary executable launch and external MCP can select the
|
|
235
235
|
same app-owned Core.
|
|
236
236
|
|
|
237
|
+
To expose the actual window-owned Core to an external local client while
|
|
238
|
+
preserving that stdio lifetime, select
|
|
239
|
+
`native.launchContext:{coreListener:{endpoint:"APPLICATION-SELECTED-LOCAL-ENDPOINT"}}`
|
|
240
|
+
instead. The generated entry calls
|
|
241
|
+
`startCoreListener({runtime:host.runtime,endpoint})` after creating its ordinary
|
|
242
|
+
stdio host. It reuses that exact service set and launch context, including the
|
|
243
|
+
existing origin, profile and state root; no headless runtime is created. The
|
|
244
|
+
listener endpoint has no default and its parent directory must already exist
|
|
245
|
+
where required by the operating system. `coreListener` and `sharedHost` are
|
|
246
|
+
alternative selections; supplying both reports `TypeError`.
|
|
247
|
+
|
|
248
|
+
That same `coreListener` opt-in registers one
|
|
249
|
+
[`createModelObservationService()`](model-observation.md) after `startCoreHost()`
|
|
250
|
+
and before binding the listener. It receives `host.runtime` and the exact
|
|
251
|
+
already-created service objects exposing `localai.status`, `image.status` and
|
|
252
|
+
`decisions.status`; a configurable decision-service name remains supported.
|
|
253
|
+
Absent owners are `null`. Composition creates no model or second runtime,
|
|
254
|
+
performs no service lookup and adds no model-readiness wait. Ordinary stdio
|
|
255
|
+
without this opt-in and the existing `sharedHost` branches retain their behavior.
|
|
256
|
+
The application separately calls `attachModelObservation()` with its existing
|
|
257
|
+
renderer owners and Core client; native composition cannot infer those objects.
|
|
258
|
+
Observers select `onDiagnostic` to receive complete live model failure frames
|
|
259
|
+
in their developer diagnostic surface. Observation does not start inference or
|
|
260
|
+
establish that an existing model failure has been corrected.
|
|
261
|
+
|
|
262
|
+
Clients use `connectSharedCoreHost({endpoint})` with `start` omitted and close
|
|
263
|
+
only their own connection. This listener provides no host-shutdown RPC. Closing
|
|
264
|
+
the window still drains its Core, including accepted service-owned operations,
|
|
265
|
+
and then closes the listener. Signal-driven host shutdown remains observed while
|
|
266
|
+
the listener is binding; a bind failure drains the newly created stdio host and
|
|
267
|
+
reports the failure. The generated entry joins listener closure without changing
|
|
268
|
+
the existing shared-host/headless branches. See
|
|
269
|
+
[the existing-runtime listener contract](core-shared-host.md#attach-to-an-existing-window-owned-core)
|
|
270
|
+
for direct composition, correlation, cancellation and complete error behavior.
|
|
271
|
+
|
|
237
272
|
`resolveNativeLaunchContext({appId,context={}})` from `arcane-os/core/host` is
|
|
238
273
|
the public, synchronous location owner for native applications and external MCP.
|
|
239
274
|
It returns a new complete context and fills only omitted fields:
|
|
@@ -67,7 +67,7 @@ complete diagnostic serialization; the startup promise retains its original
|
|
|
67
67
|
rejection value.
|
|
68
68
|
|
|
69
69
|
Each lifecycle hook receives `{application, service, emit, getService}`. Each request handler
|
|
70
|
-
receives `(parameters, {application, service, emit, getService, requestId, signal})`, with
|
|
70
|
+
receives `(parameters, {application, service, emit, getService, requestId, clientRequestId, coreRequestId, signal})`, with
|
|
71
71
|
`this` bound to its service definition. A hook or handler may publish a complete
|
|
72
72
|
service-owned event through `emit(event, data)`.
|
|
73
73
|
|
|
@@ -132,6 +132,27 @@ The shared `arcane-os/core/contracts` module owns `CORE_PROTOCOL` (`arcane/1`),
|
|
|
132
132
|
availability errors remain observable; unavailable services are not replaced
|
|
133
133
|
with browser behavior.
|
|
134
134
|
|
|
135
|
+
A transport that assigns internal dispatch IDs may call
|
|
136
|
+
`runtime.handle(frame, {contextRequestId, contextClientRequestId})`. Each supplied
|
|
137
|
+
metadata value must be a nonempty string. Handler identities are:
|
|
138
|
+
|
|
139
|
+
| Field | Value |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `requestId` | `contextRequestId ?? frame.id`, preserving the transport's established handler contract |
|
|
142
|
+
| `clientRequestId` | `contextClientRequestId ?? contextRequestId ?? frame.id`, exposing the actual incoming client identity |
|
|
143
|
+
| `coreRequestId` | `frame.id`, the actual internal dispatch identity |
|
|
144
|
+
|
|
145
|
+
Ordinary `handle(frame)` exposes all three equal to `frame.id`. Existing callers
|
|
146
|
+
that supply only `contextRequestId` also receive that value as `clientRequestId`.
|
|
147
|
+
Stdio and both IPC listener forms always supply the actual client ID separately.
|
|
148
|
+
Stdio and the borrowed listener preserve it as `requestId` too; the owning
|
|
149
|
+
shared-host adapter retains its established internal `requestId` contract.
|
|
150
|
+
|
|
151
|
+
The supplied `frame.id` still owns active-request state, cancellation lookup,
|
|
152
|
+
response correlation and the request event's top-level envelope. It distinguishes
|
|
153
|
+
simultaneous requests whose clients selected equal IDs. Parameters, results and
|
|
154
|
+
event data remain unchanged; these identity fields are separate context metadata.
|
|
155
|
+
|
|
135
156
|
A request with no registered handler retains code `METHOD_NOT_ALLOWED` and
|
|
136
157
|
adds the exact `method`, its first dotted `namespace`, and a `reason`.
|
|
137
158
|
`core-namespace-unavailable` means the dispatcher has neither a service with
|
|
@@ -222,6 +243,21 @@ reach `onError` and reject `closed`. The transport detaches
|
|
|
222
243
|
its listeners and pauses its input; it does not end caller-owned output streams
|
|
223
244
|
or take ownership of the host process.
|
|
224
245
|
|
|
246
|
+
Stdio routes responses and request-context events only for its own requests.
|
|
247
|
+
`request.cancel`, `requests.cancelAll` and `runtime.replay` are connection-scoped;
|
|
248
|
+
they do not cancel or replay another attached connection's work. The protocol
|
|
249
|
+
owner gives every accepted request a fresh internal ID and restores its client
|
|
250
|
+
ID on responses, event envelopes and stdio's own `core.state` entries. Handler
|
|
251
|
+
`context.requestId` and complete application payloads keep their original values.
|
|
252
|
+
Uncorrelated runtime/service events remain shared.
|
|
253
|
+
|
|
254
|
+
`startCoreListener({runtime,endpoint,onError})` from `arcane-os/core/host` can
|
|
255
|
+
attach additional local clients to this same runtime. Its explicit `close()`
|
|
256
|
+
closes only that listener; stdio EOF still closes the runtime and drains all
|
|
257
|
+
accepted service work, then the listener closes automatically. Observe the
|
|
258
|
+
listener's `closed` as well as the stdio owner's `closed` to join both transport
|
|
259
|
+
lifecycles. See [existing-runtime listeners](core-shared-host.md#attach-to-an-existing-window-owned-core).
|
|
260
|
+
|
|
225
261
|
## Platform and delivery boundary
|
|
226
262
|
|
|
227
263
|
These modules use portable Node and JavaScript facilities for Windows, Linux
|
|
@@ -13,6 +13,70 @@ event history. Application methods and policy stay in the application services.
|
|
|
13
13
|
Use the [MCP server](mcp-stdio.md) for external MCP framing; local Core continues
|
|
14
14
|
to use the existing `arcane/1` protocol and Content-Length stream framing.
|
|
15
15
|
|
|
16
|
+
## Attach to an existing window-owned Core
|
|
17
|
+
|
|
18
|
+
`startCoreListener({runtime,endpoint,onError})` adds a local IPC listener to an
|
|
19
|
+
existing Core runtime. It resolves `{endpoint,closed,close}` after binding the
|
|
20
|
+
selected endpoint. It neither constructs nor starts a runtime or service. The
|
|
21
|
+
existing host retains its application, services, origin, profile, state location
|
|
22
|
+
and shutdown ownership.
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import {startCoreListener} from 'arcane-os/core/host';
|
|
26
|
+
|
|
27
|
+
// host is the application's existing startCoreHost/startCoreStdio handle.
|
|
28
|
+
const listener = await startCoreListener({runtime: host.runtime, endpoint: selectedEndpoint});
|
|
29
|
+
try {
|
|
30
|
+
await host.closed;
|
|
31
|
+
} finally {
|
|
32
|
+
await listener.close();
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
An external client uses `connectSharedCoreHost({endpoint: selectedEndpoint})`
|
|
37
|
+
with `start` omitted, invokes the application's existing methods through
|
|
38
|
+
`connection.client.invoke(method, parameters)`, and calls `connection.close()`
|
|
39
|
+
when finished. This connection uses the existing SDK framing, correlation and
|
|
40
|
+
current-runtime replay; it does not launch another Core or replay requests.
|
|
41
|
+
The listener does not implement or intercept `core.host.shutdown`.
|
|
42
|
+
`connection.shutdown()` therefore has no listener-owned shutdown operation;
|
|
43
|
+
without an application method of that name it returns the ordinary
|
|
44
|
+
`METHOD_NOT_ALLOWED` error. The window's host remains the shutdown owner.
|
|
45
|
+
|
|
46
|
+
Disconnect and `listener.close()` cancel only the affected connections'
|
|
47
|
+
request-lifetime work. Explicit listener close drains queued output and releases
|
|
48
|
+
the endpoint without closing the runtime or waiting for accepted service-lifetime
|
|
49
|
+
work; that work remains tracked by its service/runtime owner. When the runtime
|
|
50
|
+
itself begins shutdown, the listener rejects new requests and retains its endpoint
|
|
51
|
+
through runtime drain/disposal, then closes its connections automatically.
|
|
52
|
+
Observe both the host's `closed` and the listener's `closed` for their respective
|
|
53
|
+
completion or failure. `close()` is idempotent. Transport errors reach `onError`
|
|
54
|
+
(complete console diagnostics by default); terminal listener failures reject
|
|
55
|
+
`closed`. Bind failures reject startup with their actual native error and leave
|
|
56
|
+
the supplied runtime alive. A draining/closed runtime rejects attachment with
|
|
57
|
+
`CORE_CLOSING`.
|
|
58
|
+
|
|
59
|
+
Stdio and each IPC connection have independent request correlation and
|
|
60
|
+
cancellation. Handlers reached through stdio or this listener receive the
|
|
61
|
+
original client ID in both `context.requestId` and `context.clientRequestId`;
|
|
62
|
+
`context.coreRequestId` exposes the
|
|
63
|
+
fresh internal ID used by runtime dispatch and protocol routing. Responses and
|
|
64
|
+
top-level event correlation return to their originating connection. The new listener leaves all event data,
|
|
65
|
+
including any authored `data.requestId`, and all parameters/results unchanged.
|
|
66
|
+
Its runtime-state snapshot retains the runtime's internal active IDs. Late events
|
|
67
|
+
from a retired request never attach to a later reuse of that client's ID.
|
|
68
|
+
Uncorrelated service events remain shared. See the
|
|
69
|
+
[runtime metadata contract](core-runtime.md#state-and-frames).
|
|
70
|
+
|
|
71
|
+
For a generated native entry, select
|
|
72
|
+
`native.launchContext.coreListener:{endpoint}` and omit `sharedHost`. This adds
|
|
73
|
+
the listener to the ordinary window/stdio runtime; closing the native window
|
|
74
|
+
still drains Core. The endpoint is explicit, with no inferred headless startup
|
|
75
|
+
or new default location. Its parent directory must already exist on platforms
|
|
76
|
+
that require one. Selecting both `coreListener` and `sharedHost` reports
|
|
77
|
+
`TypeError` because they select different runtime lifetimes. See
|
|
78
|
+
[native launch locations](core-native-packaging.md#launch-time-locations).
|
|
79
|
+
|
|
16
80
|
## Host entry
|
|
17
81
|
|
|
18
82
|
```js
|
|
@@ -107,8 +171,8 @@ running. `closed` observes connection termination and rejects on a transport
|
|
|
107
171
|
error. A subsequent connection uses the still-running owner. Reconnection is
|
|
108
172
|
explicit; failed or uncertain requests are never resubmitted.
|
|
109
173
|
|
|
110
|
-
`await connection.shutdown(invokeOptions)` explicitly
|
|
111
|
-
`core.host.shutdown` operation. It drains the shared runtime, returns
|
|
174
|
+
For an owning shared host, `await connection.shutdown(invokeOptions)` explicitly
|
|
175
|
+
invokes the host-owned `core.host.shutdown` operation. It drains the shared runtime, returns
|
|
112
176
|
`{state:'closed'}` after successful service cleanup, then closes the connection.
|
|
113
177
|
A drain failure returns the full Core error. Shutdown affects **all** clients
|
|
114
178
|
of that app endpoint and belongs to the application owner's explicit lifecycle,
|
|
@@ -118,13 +182,18 @@ after shutdown acceptance does not undo the shared shutdown.
|
|
|
118
182
|
|
|
119
183
|
## Correlation, events, and state
|
|
120
184
|
|
|
121
|
-
Each connection has its own request-ID space. The
|
|
122
|
-
internal Core request ID and restores the originating ID on responses and
|
|
185
|
+
Each connection has its own request-ID space. The owning shared-host adapter
|
|
186
|
+
substitutes an internal Core request ID and restores the originating ID on responses and
|
|
123
187
|
the optional top-level event `requestId`. Existing `data.requestId` values that
|
|
124
188
|
carry that same Core correlation ID are restored too. `parameters`, returned results, schemas,
|
|
125
189
|
documents, and content remain complete and unchanged. A handler's
|
|
126
|
-
`context.requestId`
|
|
127
|
-
separate `requestId` field, not
|
|
190
|
+
`context.requestId` remains the Core-side correlation ID, equal to
|
|
191
|
+
`context.coreRequestId`; put it in the event's separate `requestId` field, not
|
|
192
|
+
inside application content. The additive `context.clientRequestId` exposes the
|
|
193
|
+
actual incoming client ID independently of that compatibility behavior. Services
|
|
194
|
+
such as [model observation](model-observation.md) can therefore publish explicit
|
|
195
|
+
client-facing acknowledgement fields without changing the existing adapter or
|
|
196
|
+
rewriting application payloads.
|
|
128
197
|
|
|
129
198
|
Responses and request-context events go to the originating connection in
|
|
130
199
|
order. Core supplies the top-level request correlation outside `data`, so two
|
|
@@ -137,10 +206,10 @@ connection's IDs; other connections' active requests retain Core-side IDs.
|
|
|
137
206
|
requests. A disconnect performs the same scoped cancellation. The underlying
|
|
138
207
|
runtime continues to preserve service-lifetime acceptance.
|
|
139
208
|
|
|
140
|
-
`runtime.replay`
|
|
141
|
-
`core.state`, and `core.service.state` snapshot. Optional
|
|
142
|
-
`getReplayEvents()`
|
|
143
|
-
service owners. It is for current domain state, not historical chunks or
|
|
209
|
+
Both listener forms handle `runtime.replay` by sending only that connection the
|
|
210
|
+
current `core.ready`, `core.state`, and `core.service.state` snapshot. Optional
|
|
211
|
+
synchronous `getReplayEvents()` on the owning shared host returns current
|
|
212
|
+
`{event,data}` records from their actual service owners. It is for current domain state, not historical chunks or
|
|
144
213
|
retired requests. Subscribe before requesting a domain snapshot when its live
|
|
145
214
|
events may change; applications own those domain methods and state.
|
|
146
215
|
|
|
@@ -41,6 +41,12 @@ An adapter whose stable identity cannot be established remains in the inventory
|
|
|
41
41
|
with `deviceId: null` and a corresponding issue. The SDK does not invent a key
|
|
42
42
|
from a name, enumeration order, hardware model number or adapter LUID.
|
|
43
43
|
|
|
44
|
+
Windows discovery reads the present PCI devices' bus and address properties in
|
|
45
|
+
one batched `Get-PnpDeviceProperty` call with `-ThrottleLimit 1`. This limits
|
|
46
|
+
concurrency inside that query because concurrent CIM operations have been
|
|
47
|
+
observed to duplicate some device results and omit others. Discovery remains
|
|
48
|
+
asynchronous at the host, with the same shared catalog and explicit refresh.
|
|
49
|
+
|
|
44
50
|
The CPU choice uses `deviceId: 'cpu'` and `identitySource: 'host-cpu'`. It selects
|
|
45
51
|
the whole-host CPU execution target, without claiming socket or core affinity.
|
|
46
52
|
Its `processors` entries contain `{deviceId, name, socketDesignation, cores,
|