arcane-os 0.84.0 → 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.
@@ -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) {',