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 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.83.1` SDK contract. Development applications declare
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 invokes the host-owned
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 transport substitutes an
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` is the Core-side correlation ID; put it in the event's
127
- separate `requestId` field, not inside application content.
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` sends only that connection the current `core.ready`,
141
- `core.state`, and `core.service.state` snapshot. Optional synchronous
142
- `getReplayEvents()` returns current `{event,data}` records from their actual
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,