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 +41 -0
- package/README.md +1 -1
- package/browser-runtime/ai/model-observation.mjs +315 -0
- package/browser-runtime/core/app-control.mjs +27 -1
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +5 -2
- package/docs/reference/core-native-packaging.md +14 -0
- package/docs/reference/core-runtime.md +20 -11
- package/docs/reference/core-shared-host.md +9 -3
- package/docs/reference/execution-devices.md +6 -0
- package/docs/reference/inventory/package-api.json +49 -7
- package/docs/reference/model-observation.md +220 -0
- package/docs/reference/native-app-control.md +132 -16
- package/docs/reference/sdk-api.md +106 -8
- package/package.json +3 -1
- package/src/cli/app-control.mjs +14 -3
- package/src/core/app-control.mjs +2 -0
- package/src/core/hosts/windows/ArcaneAppControl.cs +2 -1
- package/src/core/hosts/windows/ArcaneWindowControl.cs +216 -31
- package/src/core/runtime-connection.mjs +4 -1
- package/src/core/runtime.mjs +15 -7
- package/src/core/services/model-observation.mjs +231 -0
- package/src/local-ai/execution-devices-windows.ps1 +3 -1
- package/src/native/portable-layout.mjs +16 -0
package/src/core/runtime.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
{
|
|
307
|
-
|
|
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) {',
|