arcane-os 0.84.0 → 0.86.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,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.86.0
4
+
5
+ - Add fixed `Tab`, `Shift+Tab`, `Enter` and `Space` key operations to
6
+ `connectAppControl` and the public `arcane app-control key` command. Each
7
+ request sends an ordered press/release pair to the selected WebView's current
8
+ focus without desktop activation or global input. Preserve complete phase
9
+ outcomes, original errors and observation of a dispatched release after
10
+ cancellation or navigation; never replay an ambiguous action.
11
+ - Add `resize` to the same client and CLI for a Normal window's native client
12
+ dimensions. Return actual previous and resulting dimensions plus the observed
13
+ CSS viewport; restoration is an explicit second request. Preserve window
14
+ state, position, profile, origin and zoom, and report minimized or maximized
15
+ state without changing it.
16
+ - Include viewport, document focus and accessible open-shadow focus-path
17
+ observations in inspection. These are DOM/native observations, not proof of
18
+ an application action or completed asynchronous handler. The matching Windows
19
+ host asset is required for the new operations; existing consumer artifacts
20
+ must adopt the public package and rebuild through their owning workflow.
21
+
22
+ ## 0.85.0
23
+
24
+ - Add `createModelObservationService({runtime, localAI, image, decisions})`
25
+ through `arcane-os/core/model-observation`, and renderer
26
+ `attachModelObservation` / observer `subscribeModelObservation` through
27
+ `arcane-os/ai/model-observation`. Observe the actual existing Core and
28
+ renderer model owners without creating, loading or replacing a runtime,
29
+ provider or model.
30
+ - Replay complete current owner state and retain its reported errors, with
31
+ explicit opt-in live diagnostics and connection-owned cancellation. Keep
32
+ original client request identity separate from actual internal Core request
33
+ identity. Native `coreListener` composition reuses existing selected services;
34
+ applications explicitly attach their renderer owners.
35
+ - Preserve the complete Windows PnP property batch while narrowing only the
36
+ property cmdlet's internal concurrency. Keep asynchronous shared discovery,
37
+ cancellation, explicit refresh and model/device selection unchanged. This
38
+ addresses the observed missing/duplicate property rows without adding retries
39
+ or another hardware query.
40
+ - These capabilities expose current state and correct the property query;
41
+ they do not claim to repair or verify native model inference. No dependency
42
+ changes or native host rebuild are required for this revision.
43
+
3
44
  ## 0.84.0
4
45
 
5
46
  - Add `startCoreListener({runtime, endpoint, onError})` through
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.84.0` SDK contract. Development applications declare
28
+ This checkout defines the `0.86.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
+ }
@@ -9,6 +9,9 @@
9
9
  * returned completely. Frame documents and unselected shadow-root contents stay
10
10
  * outside this scope. Closed shadow roots cannot be discovered or entered.
11
11
  * domRole/domName are DOM-derived hints, not an accessibility-tree computation.
12
+ * Document-level viewport/focus observations also describe the active open-shadow
13
+ * branch, independently of the selected inspection root. The native-only view
14
+ * operation reads that observation without collecting HTML/control roots.
12
15
  *
13
16
  * act({action,selector,shadowPath?,...}) requires one current CSS match in the
14
17
  * selected root. fill uses an exact
@@ -36,7 +39,10 @@ export function runAppControl(operation, params = {}) {
36
39
  if (operation === 'act') {
37
40
  return {ok: true, result: actOnElement()};
38
41
  }
39
- fail('INVALID_ARGUMENT', 'Expected the inspect or act operation.');
42
+ if (operation === 'view') {
43
+ return {ok: true, result: readView()};
44
+ }
45
+ fail('INVALID_ARGUMENT', 'Expected the inspect, act, or view operation.');
40
46
  } catch (error) {
41
47
  return {
42
48
  ok: false,
@@ -66,6 +72,25 @@ export function runAppControl(operation, params = {}) {
66
72
  return Array.from(root.querySelectorAll(selector));
67
73
  }
68
74
 
75
+ // Native keys and resize need only this fixed observation, not a DOM scan.
76
+ function readView() {
77
+ const focusPath = [];
78
+ let element = document.activeElement;
79
+ while (element) {
80
+ focusPath.push(describeElement(element, null));
81
+ element = element.shadowRoot?.activeElement ?? null;
82
+ }
83
+ return {
84
+ viewport: {
85
+ width: globalThis.innerWidth,
86
+ height: globalThis.innerHeight,
87
+ devicePixelRatio: globalThis.devicePixelRatio
88
+ },
89
+ documentHasFocus: document.hasFocus(),
90
+ focusPath
91
+ };
92
+ }
93
+
69
94
  function selectedRoot() {
70
95
  const shadowPath = params.shadowPath === undefined ? [] : params.shadowPath;
71
96
  if (!Array.isArray(shadowPath)) {
@@ -156,6 +181,7 @@ export function runAppControl(operation, params = {}) {
156
181
  ...(isDocument ? {} : {shadowPath}),
157
182
  title: document.title,
158
183
  readyState: document.readyState,
184
+ ...readView(),
159
185
  doctype: document.doctype
160
186
  ? new XMLSerializer().serializeToString(document.doctype)
161
187
  : null,
@@ -70,6 +70,7 @@ alone does not make bare module names resolve in a browser.
70
70
  | Own an explicitly selected native Codex App Server session | [Codex App Server](codex-app-server.md) |
71
71
  | Expose application-owned tools and static resources through Node STDIO | [MCP STDIO server](mcp-stdio.md) |
72
72
  | Run application-selected llama.cpp, Ollama or ONNX through Core | [Local AI through Core](local-ai.md) |
73
+ | Observe existing renderer and Core model state, progress and failures | [Existing model observation](model-observation.md) |
73
74
  | Generate complete local PNG images with a retained native model | [Local image generation](local-image-generation.md) |
74
75
  | Select FLUX.2 Klein 4B and its three complementary model resources | [FLUX.2 Klein 4B through Core](local-image-flux.md) |
75
76
  | Select the complete SDXL Base 1.0 checkpoint for generation and whole-image editing | [SDXL Base 1.0 through Core](local-image-sdxl.md) |
@@ -17,7 +17,10 @@ JSON to stderr and a nonzero exit. Capture writes its image to the explicit
17
17
  the general output-format parser below applies to the other commands.
18
18
  Inspection accepts either `--selector` or `--request <json-file>`; the latter
19
19
  reads the complete inspection object, including an explicit `shadowPath` through
20
- open shadow roots. Actions continue to use `--request` for their complete object.
20
+ open shadow roots. Actions, fixed keys and Normal-window resizing use `--request`
21
+ for their complete parameter object. Keys require the observed document
22
+ generation; resize returns actual previous native client dimensions for an
23
+ explicit restoration request.
21
24
 
22
25
  ## Command inventory
23
26
 
@@ -41,7 +44,7 @@ open shadow roots. Actions continue to use `--request` for their complete object
41
44
  | `arcane run` | Serves an existing browser release, or packages, plans, builds, and launches one paired native artifact. |
42
45
  | `arcane update-check` | Performs one explicit, read-only npm dist-tag query for the installed SDK version. |
43
46
  | `arcane targets` | Lists target ids, declared status, formats, architectures, signing profiles, methods, and pairing reason. |
44
- | `arcane app-control status\|inspect\|capture\|act` | Connects to an explicit running native window endpoint for document inspection, viewport capture or targeted DOM actions. |
47
+ | `arcane app-control status\|inspect\|capture\|act\|key\|resize` | Connects to an explicit running native window endpoint for document inspection, viewport capture, targeted DOM actions, fixed keys or Normal-window client resizing. |
45
48
  | `arcane repo status\|pull\|push` | Runs one selected repository operation for the current app workspace. |
46
49
  | `arcane mail key set\|status\|delete` | Manages one server-only Resend API-key profile in `.arcane.env.json`. |
47
50
  | `arcane mail send` | Performs one explicit, idempotency-keyed Resend attempt from a complete JSON report on redirected stdin. |
@@ -245,6 +245,20 @@ listener endpoint has no default and its parent directory must already exist
245
245
  where required by the operating system. `coreListener` and `sharedHost` are
246
246
  alternative selections; supplying both reports `TypeError`.
247
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
+
248
262
  Clients use `connectSharedCoreHost({endpoint})` with `start` omitted and close
249
263
  only their own connection. This listener provides no host-shutdown RPC. Closing
250
264
  the window still drains its Core, including accepted service-owned operations,
@@ -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, coreRequestId, 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
 
@@ -133,16 +133,25 @@ availability errors remain observable; unavailable services are not replaced
133
133
  with browser behavior.
134
134
 
135
135
  A transport that assigns internal dispatch IDs may call
136
- `runtime.handle(frame, {contextRequestId})`. This optional nonempty string sets
137
- only the service handler's `context.requestId`. `context.coreRequestId` always
138
- exposes the actual `frame.id`; it distinguishes simultaneous requests whose
139
- clients selected equal IDs. The supplied `frame.id` still
140
- owns active-request state, cancellation lookup, response correlation and the
141
- request event's top-level envelope. Parameters and event data remain unchanged.
142
- Ordinary `handle(frame)` keeps its existing behavior: `context.requestId` equals
143
- `frame.id`, and `coreRequestId` equals that same ID. Stdio and
144
- the borrowed IPC listener use this metadata to preserve the original client ID
145
- in service-facing data while their transport owner routes unique dispatch IDs.
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.
146
155
 
147
156
  A request with no registered handler retains code `METHOD_NOT_ALLOWED` and
148
157
  adds the exact `method`, its first dotted `namespace`, and a `reason`.
@@ -58,7 +58,8 @@ the supplied runtime alive. A draining/closed runtime rejects attachment with
58
58
 
59
59
  Stdio and each IPC connection have independent request correlation and
60
60
  cancellation. Handlers reached through stdio or this listener receive the
61
- original client ID in `context.requestId`; `context.coreRequestId` exposes the
61
+ original client ID in both `context.requestId` and `context.clientRequestId`;
62
+ `context.coreRequestId` exposes the
62
63
  fresh internal ID used by runtime dispatch and protocol routing. Responses and
63
64
  top-level event correlation return to their originating connection. The new listener leaves all event data,
64
65
  including any authored `data.requestId`, and all parameters/results unchanged.
@@ -186,8 +187,13 @@ substitutes an internal Core request ID and restores the originating ID on respo
186
187
  the optional top-level event `requestId`. Existing `data.requestId` values that
187
188
  carry that same Core correlation ID are restored too. `parameters`, returned results, schemas,
188
189
  documents, and content remain complete and unchanged. A handler's
189
- `context.requestId` is the Core-side correlation ID; put it in the event's
190
- 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.
191
197
 
192
198
  Responses and request-context events go to the originating connection in
193
199
  order. Core supplies the top-level request correlation outside `data`, so two
@@ -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,