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.
@@ -218,7 +218,7 @@ class CoreRuntime {
218
218
  return this.current();
219
219
  }
220
220
 
221
- async handle(frame, {contextRequestId} = {}) {
221
+ async handle(frame, {contextRequestId, contextClientRequestId} = {}) {
222
222
  if (frame?.protocol !== CORE_PROTOCOL) throw failure('INVALID_RPC_REQUEST', 'Unknown Core protocol.');
223
223
  if (frame.type === 'control') {
224
224
  if (frame.control === 'runtime.replay') {
@@ -238,7 +238,10 @@ class CoreRuntime {
238
238
  if (contextRequestId !== undefined && (!is.string(contextRequestId) || !contextRequestId)) {
239
239
  throw failure('INVALID_RPC_REQUEST', 'A transport context request id must be a nonempty string.');
240
240
  }
241
- const task = this.#answer(frame, contextRequestId);
241
+ if (contextClientRequestId !== undefined && (!is.string(contextClientRequestId) || !contextClientRequestId)) {
242
+ throw failure('INVALID_RPC_REQUEST', 'A transport client request id must be a nonempty string.');
243
+ }
244
+ const task = this.#answer(frame, contextRequestId, contextClientRequestId);
242
245
  this.#responses.add(task);
243
246
  try {
244
247
  return await task;
@@ -247,7 +250,7 @@ class CoreRuntime {
247
250
  }
248
251
  }
249
252
 
250
- async #answer(frame, contextRequestId) {
253
+ async #answer(frame, contextRequestId, contextClientRequestId) {
251
254
  let result;
252
255
  let error;
253
256
  let ok = true;
@@ -257,7 +260,7 @@ class CoreRuntime {
257
260
  if (frame.method === 'system.ping') result = {ok: true};
258
261
  else if (frame.method === 'version.current') result = this.#version;
259
262
  else if (frame.method === 'app.current') result = this.#application;
260
- else result = await this.#request(frame, contextRequestId);
263
+ else result = await this.#request(frame, contextRequestId, contextClientRequestId);
261
264
  } catch (caught) {
262
265
  ok = false;
263
266
  error = caught;
@@ -273,7 +276,7 @@ class CoreRuntime {
273
276
  return response;
274
277
  }
275
278
 
276
- #request(frame, contextRequestId) {
279
+ #request(frame, contextRequestId, contextClientRequestId) {
277
280
  const registration = this.#methods.get(frame.method);
278
281
  if (!registration) {
279
282
  const namespace = frame.method.split('.')[0];
@@ -303,8 +306,13 @@ class CoreRuntime {
303
306
  const result = await handle.call(
304
307
  service.definition,
305
308
  frame.parameters,
306
- {...runtime.#serviceContext(service, frame.id),
307
- requestId: contextRequestId ?? frame.id, coreRequestId: frame.id, signal: controller.signal}
309
+ {
310
+ ...runtime.#serviceContext(service, frame.id),
311
+ requestId: contextRequestId ?? frame.id,
312
+ clientRequestId: contextClientRequestId ?? contextRequestId ?? frame.id,
313
+ coreRequestId: frame.id,
314
+ signal: controller.signal
315
+ }
308
316
  );
309
317
  aborted(controller);
310
318
  return result;
@@ -0,0 +1,231 @@
1
+ import Is from 'strong-type';
2
+ import {CoreError} from '../../../browser-runtime/core/contracts.mjs';
3
+
4
+ const is = new Is(false);
5
+
6
+ function observationError(code, message) {
7
+ return new CoreError({code, message});
8
+ }
9
+
10
+ /** Observe supplied owners without acquiring, starting or replacing a model. */
11
+ export function createModelObservationService({runtime, localAI = null, image = null, decisions = null} = {}) {
12
+ if (!is.function(runtime?.current) || !is.function(runtime?.onFrame)) {
13
+ throw new TypeError('Model observation requires the existing Core runtime.');
14
+ }
15
+ const owners = {localAI, image, decisions};
16
+ const observedMethods = new Set();
17
+ for (const owner of Object.values(owners)) {
18
+ if (owner === null) continue;
19
+ if (!is.function(owner.current)) throw new TypeError('An observed Core model owner must expose current().');
20
+ for (const method of Object.keys(owner.methods ?? {})) observedMethods.add(method);
21
+ }
22
+
23
+ const renderers = new Map();
24
+ const watchers = new Set();
25
+ const requests = new Map();
26
+ let stopFrames = null;
27
+ let disposed = false;
28
+
29
+ function current() {
30
+ return {
31
+ core: runtime.current(),
32
+ native: {
33
+ localAI: localAI?.current() ?? null,
34
+ image: image?.current() ?? null,
35
+ decisions: decisions?.current() ?? null
36
+ },
37
+ renderers: [...renderers.values()].map(function rendererState(renderer) {
38
+ return {
39
+ attachmentRequestId: renderer.request.clientRequestId,
40
+ coreRequestId: renderer.request.coreRequestId,
41
+ owners: renderer.snapshot
42
+ };
43
+ })
44
+ };
45
+ }
46
+
47
+ function requireActive(request) {
48
+ request.signal.throwIfAborted();
49
+ if (disposed) throw observationError('MODEL_OBSERVATION_CLOSED', 'Model observation is closed.');
50
+ }
51
+
52
+ function requireContext(request) {
53
+ requireActive(request);
54
+ if (!is.string(request.clientRequestId) || !is.string(request.coreRequestId)) {
55
+ throw observationError('MODEL_OBSERVATION_CONTEXT_UNAVAILABLE',
56
+ 'Model observation requires the actual client and Core request identities.');
57
+ }
58
+ }
59
+
60
+ function observeActiveRequests(snapshot) {
61
+ for (const request of snapshot.activeRequests) {
62
+ if (observedMethods.has(request.method)) requests.set(request.id, request.method);
63
+ }
64
+ // Core publishes request removal before its response. Keep correlation
65
+ // until that response arrives, rather than treating removal as settlement.
66
+ }
67
+
68
+ function stopFrameObservation() {
69
+ if (watchers.size) return;
70
+ stopFrames?.();
71
+ stopFrames = null;
72
+ requests.clear();
73
+ }
74
+
75
+ function send(watcher, event, value) {
76
+ if (!watchers.has(watcher) || watcher.request.signal.aborted) return;
77
+ try {
78
+ watcher.request.emit(event, {watchRequestId: watcher.request.clientRequestId, ...value});
79
+ } catch (error) {
80
+ watcher.stop(error);
81
+ }
82
+ }
83
+
84
+ function publishState() {
85
+ if (!watchers.size) return;
86
+ let snapshot;
87
+ try {
88
+ snapshot = current();
89
+ } catch (error) {
90
+ for (const watcher of [...watchers]) watcher.stop(error);
91
+ return;
92
+ }
93
+ for (const watcher of [...watchers]) send(watcher, 'model.observation.state', {snapshot});
94
+ }
95
+
96
+ function observeFrame(frame) {
97
+ try {
98
+ if (frame.type === 'response') {
99
+ const method = requests.get(frame.id);
100
+ requests.delete(frame.id);
101
+ if (method !== undefined && frame.ok === false) {
102
+ for (const watcher of [...watchers]) {
103
+ if (watcher.diagnostics) {
104
+ send(watcher, 'model.observation.error', {source: 'core', method, frame});
105
+ }
106
+ }
107
+ }
108
+ return;
109
+ }
110
+ if (frame.type !== 'event') return;
111
+ if (frame.event === 'core.state') {
112
+ observeActiveRequests(frame.data);
113
+ publishState();
114
+ } else if (frame.event === 'localai.state' || frame.event === 'image.state'
115
+ || frame.event === 'decisions.state') {
116
+ publishState();
117
+ }
118
+ } catch (error) {
119
+ // An observation failure must settle observers, never the operation
120
+ // whose existing owner emitted this frame.
121
+ for (const watcher of [...watchers]) watcher.stop(error);
122
+ }
123
+ }
124
+
125
+ function hold(request, release) {
126
+ let reject;
127
+ let stopped = false;
128
+ const task = new Promise(function ownObservationRequest(_resolve, rejectRequest) {
129
+ reject = rejectRequest;
130
+ });
131
+ function stop(error) {
132
+ if (stopped) return;
133
+ stopped = true;
134
+ request.signal.removeEventListener('abort', aborted);
135
+ try {
136
+ release();
137
+ } catch (releaseError) {
138
+ error = new AggregateError([error, releaseError], 'Observation retirement failed.');
139
+ }
140
+ reject(error);
141
+ }
142
+ function aborted() {
143
+ stop(request.signal.reason ?? observationError('ARCANE_REQUEST_ABORTED', 'Model observation was cancelled.'));
144
+ }
145
+ request.signal.addEventListener('abort', aborted, {once: true});
146
+ return {task, stop, checkAbort: function checkObservationAbort() { if (request.signal.aborted) aborted(); }};
147
+ }
148
+
149
+ function watch({diagnostics = false} = {}, request) {
150
+ requireContext(request);
151
+ if (!is.boolean(diagnostics)) throw new TypeError('diagnostics must be a boolean.');
152
+ const watcher = {request, diagnostics, stop: null};
153
+ const owned = hold(request, function releaseWatcher() {
154
+ watchers.delete(watcher);
155
+ stopFrameObservation();
156
+ });
157
+ watcher.stop = owned.stop;
158
+ watchers.add(watcher);
159
+ try {
160
+ if (!stopFrames) stopFrames = runtime.onFrame(observeFrame);
161
+ observeActiveRequests(runtime.current());
162
+ send(watcher, 'model.observation.state', {snapshot: current()});
163
+ owned.checkAbort();
164
+ } catch (error) {
165
+ owned.stop(error);
166
+ }
167
+ return owned.task;
168
+ }
169
+
170
+ function attach({snapshot} = {}, request) {
171
+ requireContext(request);
172
+ if (!snapshot || !is.object(snapshot)) throw new TypeError('Renderer observation requires its current owner snapshots.');
173
+ if (renderers.has(request.coreRequestId)) {
174
+ throw observationError('MODEL_OBSERVATION_ATTACHMENT_ACTIVE', 'This Core attachment request is already active.');
175
+ }
176
+ const renderer = {request, snapshot, stop: null};
177
+ const owned = hold(request, function releaseRenderer() {
178
+ if (renderers.get(request.coreRequestId) === renderer) renderers.delete(request.coreRequestId);
179
+ publishState();
180
+ });
181
+ renderer.stop = owned.stop;
182
+ renderers.set(request.coreRequestId, renderer);
183
+ try {
184
+ request.emit('model.observation.renderer.attached', {
185
+ attachmentRequestId: request.clientRequestId,
186
+ coreRequestId: request.coreRequestId
187
+ });
188
+ publishState();
189
+ owned.checkAbort();
190
+ } catch (error) {
191
+ owned.stop(error);
192
+ }
193
+ return owned.task;
194
+ }
195
+
196
+ function publishRenderer({attachmentRequestId, coreRequestId, snapshot} = {}, request) {
197
+ requireActive(request);
198
+ const renderer = renderers.get(coreRequestId);
199
+ if (!renderer || renderer.request.clientRequestId !== attachmentRequestId || renderer.request.signal.aborted) {
200
+ throw observationError('MODEL_OBSERVATION_ATTACHMENT_RETIRED', 'The selected renderer observation attachment is no longer active.');
201
+ }
202
+ if (!snapshot || !is.object(snapshot)) throw new TypeError('Renderer observation requires its current owner snapshots.');
203
+ renderer.snapshot = snapshot;
204
+ publishState();
205
+ return {attachmentRequestId, coreRequestId};
206
+ }
207
+
208
+ function dispose() {
209
+ if (disposed) return;
210
+ disposed = true;
211
+ const error = observationError('MODEL_OBSERVATION_CLOSED', 'Model observation is closed.');
212
+ for (const watcher of [...watchers]) watcher.stop(error);
213
+ for (const renderer of [...renderers.values()]) renderer.stop(error);
214
+ stopFrameObservation();
215
+ }
216
+
217
+ return {
218
+ name: 'model-observation',
219
+ current,
220
+ dispose,
221
+ methods: {
222
+ 'model.observation.current': function currentObservation(_parameters, request) {
223
+ requireActive(request);
224
+ return current();
225
+ },
226
+ 'model.observation.watch': watch,
227
+ 'model.observation.renderer.attach': attach,
228
+ 'model.observation.renderer.publish': publishRenderer
229
+ }
230
+ };
231
+ }
@@ -107,8 +107,10 @@ function Get-PresentPciDevices {
107
107
  }
108
108
  }
109
109
  if ($devicesById.Count -gt 0) {
110
+ # Concurrent CIM property operations can duplicate some device results
111
+ # and omit others. Serialize only this query's internal operations.
110
112
  $properties = @(
111
- Get-PnpDeviceProperty -InstanceId ([string[]]@($devicesById.Keys)) -KeyName 'DEVPKEY_Device_BusNumber', 'DEVPKEY_Device_Address' -ErrorAction Continue 2>&1 3>&1
113
+ Get-PnpDeviceProperty -InstanceId ([string[]]@($devicesById.Keys)) -KeyName 'DEVPKEY_Device_BusNumber', 'DEVPKEY_Device_Address' -ThrottleLimit 1 -ErrorAction Continue 2>&1 3>&1
112
114
  )
113
115
  foreach ($property in $properties) {
114
116
  if ($property -is [System.Management.Automation.ErrorRecord]) {
@@ -204,6 +204,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
204
204
  "import {isSea} from 'node:sea';",
205
205
  ...(launchDefaults === undefined ? [] : ["import {mkdir} from 'node:fs/promises';"]),
206
206
  "import {readCoreLaunchContext, startCoreHost, startCoreListener, runSharedCoreHost, startSharedCoreBridge} from 'arcane-os/core/host';",
207
+ "import {createModelObservationService} from 'arcane-os/core/model-observation';",
207
208
  '',
208
209
  ...(localAI === undefined ? [] : [
209
210
  '// Bundled runtime paths are artifact-root-relative POSIX paths.',
@@ -297,6 +298,21 @@ export function coreEntrySource(application, version, services, {localAI, runtim
297
298
  " process.on('SIGINT', closeCore);",
298
299
  " process.on('SIGTERM', closeCore);",
299
300
  ' try {',
301
+ ' const observation = createModelObservationService(',
302
+ ' {',
303
+ ' runtime: host.runtime,',
304
+ ' localAI: services.find(',
305
+ " function localAIOwner(service) { return Object.hasOwn(service.methods ?? {}, 'localai.status'); }",
306
+ ' ) ?? null,',
307
+ ' image: services.find(',
308
+ " function imageOwner(service) { return Object.hasOwn(service.methods ?? {}, 'image.status'); }",
309
+ ' ) ?? null,',
310
+ ' decisions: services.find(',
311
+ " function decisionOwner(service) { return Object.hasOwn(service.methods ?? {}, 'decisions.status'); }",
312
+ ' ) ?? null',
313
+ ' }',
314
+ ' );',
315
+ ' host.runtime.registerService(observation);',
300
316
  ' listener = await startCoreListener({runtime: host.runtime, endpoint: context.coreListener?.endpoint});',
301
317
  ' listener.closed.catch(function listenerFailed() { process.exitCode = 1; });',
302
318
  ' } catch (error) {',