arcane-os 0.80.0 → 0.82.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +97 -19
  4. package/browser-runtime/ai/dbopfs-model-parts.mjs +12 -5
  5. package/browser-runtime/ai/dbopfs-model-resources.mjs +2 -5
  6. package/browser-runtime/core/app-control.mjs +439 -0
  7. package/docs/reference/ai/browser-speech.md +11 -0
  8. package/docs/reference/ai/browser-wasm.md +21 -10
  9. package/docs/reference/cli.md +9 -1
  10. package/docs/reference/core-native-packaging.md +14 -0
  11. package/docs/reference/inventory/package-api.json +23 -9
  12. package/docs/reference/inventory/runtime-components.json +2 -1
  13. package/docs/reference/native-app-control.md +147 -0
  14. package/docs/reference/native-decisions.md +74 -16
  15. package/docs/reference/runtime-components.md +20 -0
  16. package/docs/reference/sdk-api.md +49 -6
  17. package/package.json +2 -1
  18. package/runtime/arcane/components/chat.html +140 -18
  19. package/runtime/arcane/modules/AI.js +10 -5
  20. package/src/cli/app-control.mjs +106 -0
  21. package/src/cli/main.mjs +5 -0
  22. package/src/core/app-control.mjs +23 -0
  23. package/src/core/hosts/windows/ArcaneAppControl.cs +525 -0
  24. package/src/core/hosts/windows/ArcaneCoreProcess.cs +2 -63
  25. package/src/core/hosts/windows/ArcaneFrameTransport.cs +80 -0
  26. package/src/core/hosts/windows/ArcaneHost.cs +45 -7
  27. package/src/core/hosts/windows/ArcaneLauncher.cs +5 -0
  28. package/src/core/hosts/windows/ArcaneWindowControl.cs +248 -0
  29. package/src/core/services/decisions.mjs +16 -2
  30. package/src/core/shared-host.mjs +7 -91
  31. package/src/core/socket-connection.mjs +109 -0
  32. package/src/local-ai/decision-tokenizer-worker.mjs +1 -1
  33. package/src/local-ai/decision-tokenizer.mjs +2 -2
  34. package/src/local-ai/decisions.mjs +33 -10
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Runs inside the existing app document. The native host serializes this named
3
+ * function with toString(), so all helpers must remain inside its lexical scope.
4
+ *
5
+ * inspect({selector?}) reads the top-level light DOM. Omission selects the whole
6
+ * document; a CSS selector may select any number of roots. HTML, textContent,
7
+ * attributes and live control state are returned completely. Frame documents
8
+ * and shadow-root contents are outside this CSS scope; frame elements and open
9
+ * shadow hosts are identified only. Closed shadow roots cannot be discovered.
10
+ * domRole/domName are DOM-derived hints, not an accessibility-tree computation.
11
+ *
12
+ * act({action,selector,...}) requires one current CSS match. fill uses an exact
13
+ * string value; select uses either value:string or values:string[]. scroll uses
14
+ * absolute left/top CSS-pixel offsets, preserving any omitted axis. Actions use
15
+ * DOM APIs and untrusted events without focus or desktop input. Results describe
16
+ * immediate DOM state, including any browser value sanitization or scroll
17
+ * clamping, not completion of asynchronous application work.
18
+ *
19
+ * The native host owns document-generation checks, scheduling and cancellation.
20
+ * This function retains no state, retries no action and returns a JSON envelope
21
+ * because ExecuteScriptAsync otherwise loses uncaught JavaScript exceptions.
22
+ */
23
+ export function runAppControl(operation, params = {}) {
24
+ let actionAttempted = false;
25
+ const eventsDispatched = [];
26
+
27
+ try {
28
+ if (params === null || typeof params !== 'object' || Array.isArray(params)) {
29
+ fail('INVALID_ARGUMENT', 'App-control parameters must be an object.');
30
+ }
31
+ if (operation === 'inspect') {
32
+ return {ok: true, result: inspectDocument()};
33
+ }
34
+ if (operation === 'act') {
35
+ return {ok: true, result: actOnElement()};
36
+ }
37
+ fail('INVALID_ARGUMENT', 'Expected the inspect or act operation.');
38
+ } catch (error) {
39
+ return {
40
+ ok: false,
41
+ error: {
42
+ name: error?.name ?? 'Error',
43
+ message: error?.message ?? String(error),
44
+ stack: error?.stack ?? null,
45
+ code: error?.code ?? 'ARCANE_APP_CONTROL_FAILED',
46
+ details: error?.details ?? null,
47
+ actionAttempted,
48
+ eventsDispatched
49
+ }
50
+ };
51
+ }
52
+
53
+ function fail(code, message, details = null) {
54
+ const error = new Error(message);
55
+ error.code = code;
56
+ error.details = details;
57
+ throw error;
58
+ }
59
+
60
+ function queryElements(selector) {
61
+ if (typeof selector !== 'string') {
62
+ fail('INVALID_ARGUMENT', 'selector must be a CSS selector string.');
63
+ }
64
+ return Array.from(document.querySelectorAll(selector));
65
+ }
66
+
67
+ function inspectDocument() {
68
+ const roots = params.selector === undefined
69
+ ? (document.documentElement ? [document.documentElement] : [])
70
+ : queryElements(params.selector);
71
+ const rootSet = new Set(roots);
72
+ const elements = Array.from(document.querySelectorAll('*'));
73
+ const idCounts = new Map();
74
+ const childCounts = new Map();
75
+ const childIndexes = new Map();
76
+ const scope = new Set();
77
+ const selectors = new Map();
78
+ const controls = [];
79
+ const frames = [];
80
+ const shadowHosts = [];
81
+
82
+ // One traversal establishes selector paths and scope for all records.
83
+ for (const element of elements) {
84
+ if (element.id) {
85
+ idCounts.set(element.id, (idCounts.get(element.id) ?? 0) + 1);
86
+ }
87
+ const parent = element.parentElement;
88
+ const index = (childCounts.get(parent) ?? 0) + 1;
89
+ childCounts.set(parent, index);
90
+ childIndexes.set(element, index);
91
+ if (rootSet.has(element) || scope.has(parent)) {
92
+ scope.add(element);
93
+ }
94
+ }
95
+
96
+ for (const element of elements) {
97
+ if (!scope.has(element)) continue;
98
+ if (element.matches('input,textarea,select,button,option,optgroup,fieldset,output,progress,meter,details,summary,a[href],area[href],audio,video,[contenteditable],[role],[tabindex]')) {
99
+ controls.push(describeElement(element, uniqueSelector(element)));
100
+ }
101
+ if (element.localName === 'iframe' || element.localName === 'frame') {
102
+ frames.push(describeElement(element, uniqueSelector(element)));
103
+ }
104
+ if (element.shadowRoot) {
105
+ shadowHosts.push(
106
+ {
107
+ selector: uniqueSelector(element),
108
+ mode: element.shadowRoot.mode
109
+ }
110
+ );
111
+ }
112
+ }
113
+
114
+ return {
115
+ scope: 'top-level-light-dom',
116
+ title: document.title,
117
+ readyState: document.readyState,
118
+ doctype: document.doctype
119
+ ? new XMLSerializer().serializeToString(document.doctype)
120
+ : null,
121
+ roots: roots.map(describeRoot),
122
+ controls,
123
+ frames,
124
+ shadowHosts,
125
+ activeElement: document.activeElement
126
+ ? describeElement(document.activeElement, uniqueSelector(document.activeElement))
127
+ : null,
128
+ scrollingElement: document.scrollingElement
129
+ ? uniqueSelector(document.scrollingElement)
130
+ : null,
131
+ selection: {text: document.getSelection()?.toString() ?? ''}
132
+ };
133
+
134
+ function describeRoot(element) {
135
+ return {
136
+ selector: uniqueSelector(element),
137
+ html: element.outerHTML,
138
+ text: element.textContent,
139
+ state: readState(element)
140
+ };
141
+ }
142
+
143
+ function uniqueSelector(element) {
144
+ if (!element.isConnected || element.getRootNode() !== document) return null;
145
+ const path = [];
146
+ let current = element;
147
+ let prefix = '';
148
+ while (current) {
149
+ if (selectors.has(current)) {
150
+ prefix = selectors.get(current);
151
+ break;
152
+ }
153
+ if (current.id && !current.id.includes('\0') && idCounts.get(current.id) === 1) {
154
+ prefix = `#${CSS.escape(current.id)}`;
155
+ selectors.set(current, prefix);
156
+ break;
157
+ }
158
+ path.push(current);
159
+ current = current.parentElement;
160
+ }
161
+ for (let index = path.length - 1; index >= 0; index -= 1) {
162
+ const part = path[index];
163
+ const segment = `*:nth-child(${childIndexes.get(part)})`;
164
+ prefix = prefix ? `${prefix} > ${segment}` : ':root';
165
+ selectors.set(part, prefix);
166
+ }
167
+ return prefix;
168
+ }
169
+ }
170
+
171
+ function describeElement(element, selector) {
172
+ const attributeEntries = [];
173
+ for (const attribute of element.attributes) {
174
+ attributeEntries.push(
175
+ [attribute.name, attribute.value]
176
+ );
177
+ }
178
+ return {
179
+ selector,
180
+ tagName: element.localName,
181
+ attributes: Object.fromEntries(attributeEntries),
182
+ domRole: element.getAttribute('role'),
183
+ domName: readDOMName(element),
184
+ state: readState(element)
185
+ };
186
+ }
187
+
188
+ function readDOMName(element) {
189
+ const labelledBy = element.getAttribute('aria-labelledby');
190
+ if (labelledBy) {
191
+ const labels = [];
192
+ for (const id of labelledBy.split(/\s+/u)) {
193
+ const label = document.getElementById(id);
194
+ if (label) labels.push(label.textContent);
195
+ }
196
+ if (labels.length) return {source: 'aria-labelledby', value: labels.join(' ')};
197
+ }
198
+ if (element.hasAttribute('aria-label')) {
199
+ return {source: 'aria-label', value: element.getAttribute('aria-label')};
200
+ }
201
+ if (element.labels?.length) {
202
+ const labels = [];
203
+ for (const label of element.labels) labels.push(label.textContent);
204
+ return {source: 'labels', value: labels.join(' ')};
205
+ }
206
+ if (element.hasAttribute('alt')) {
207
+ return {source: 'alt', value: element.getAttribute('alt')};
208
+ }
209
+ if (element.localName === 'input' && ['button', 'submit', 'reset'].includes(element.type)) {
210
+ return {source: 'value', value: element.value};
211
+ }
212
+ return {source: 'textContent', value: element.textContent};
213
+ }
214
+
215
+ function readState(element) {
216
+ const state = {
217
+ connected: element.isConnected,
218
+ contentEditable: element.isContentEditable === true,
219
+ effectivelyDisabled: element.matches(':disabled'),
220
+ scrollLeft: element.scrollLeft,
221
+ scrollTop: element.scrollTop,
222
+ scrollWidth: element.scrollWidth,
223
+ scrollHeight: element.scrollHeight,
224
+ clientWidth: element.clientWidth,
225
+ clientHeight: element.clientHeight
226
+ };
227
+ for (const field of [
228
+ 'value', 'type', 'checked', 'indeterminate', 'disabled', 'readOnly',
229
+ 'required', 'multiple', 'selected', 'selectedIndex', 'hidden', 'open',
230
+ 'tabIndex', 'selectionStart', 'selectionEnd', 'selectionDirection',
231
+ 'willValidate', 'validationMessage', 'paused', 'ended', 'currentTime',
232
+ 'muted', 'volume', 'playbackRate'
233
+ ]) {
234
+ const value = element[field];
235
+ if (value === null || ['string', 'boolean', 'number'].includes(typeof value)) {
236
+ state[field] = value;
237
+ }
238
+ }
239
+ if (element.isContentEditable) state.editableText = element.textContent;
240
+ if (element.validity) {
241
+ state.validity = {};
242
+ for (const field of [
243
+ 'badInput', 'customError', 'patternMismatch', 'rangeOverflow',
244
+ 'rangeUnderflow', 'stepMismatch', 'tooLong', 'tooShort',
245
+ 'typeMismatch', 'valid', 'valueMissing'
246
+ ]) {
247
+ state.validity[field] = element.validity[field];
248
+ }
249
+ }
250
+ if (element.localName === 'select') {
251
+ state.options = [];
252
+ for (const option of element.options) {
253
+ state.options.push(
254
+ {
255
+ value: option.value,
256
+ text: option.text,
257
+ label: option.label,
258
+ selected: option.selected,
259
+ disabled: option.disabled,
260
+ effectivelyDisabled: option.matches(':disabled')
261
+ }
262
+ );
263
+ }
264
+ }
265
+ if (element.localName === 'input' && element.type === 'file') {
266
+ state.files = [];
267
+ for (const file of element.files) {
268
+ state.files.push(
269
+ {name: file.name, type: file.type, lastModified: file.lastModified}
270
+ );
271
+ }
272
+ }
273
+ return state;
274
+ }
275
+
276
+ function actOnElement() {
277
+ const targets = queryElements(params.selector);
278
+ if (targets.length !== 1) {
279
+ fail(
280
+ 'ARCANE_APP_CONTROL_TARGET_COUNT',
281
+ 'An action requires exactly one matching element.',
282
+ {matches: targets.length}
283
+ );
284
+ }
285
+ const target = targets[0];
286
+ const action = params.action;
287
+ if (!['click', 'fill', 'select', 'scroll'].includes(action)) {
288
+ fail('INVALID_ARGUMENT', 'Expected a click, fill, select or scroll action.');
289
+ }
290
+ if (action !== 'scroll' && target.matches(':disabled')) {
291
+ fail('ARCANE_APP_CONTROL_DISABLED', 'The selected control is disabled.');
292
+ }
293
+
294
+ let method;
295
+ if (action === 'click') {
296
+ if (typeof target.click !== 'function') {
297
+ fail('ARCANE_APP_CONTROL_UNSUPPORTED', 'The selected element does not provide a DOM click method.');
298
+ }
299
+ actionAttempted = true;
300
+ target.click();
301
+ method = 'click';
302
+ } else if (action === 'fill') {
303
+ fillElement(target);
304
+ method = 'fill';
305
+ } else if (action === 'select') {
306
+ selectOptions(target);
307
+ method = 'select';
308
+ } else {
309
+ if (params.left === undefined && params.top === undefined) {
310
+ fail('INVALID_ARGUMENT', 'scroll requires left, top or both absolute offsets.');
311
+ }
312
+ for (const field of ['left', 'top']) {
313
+ if (params[field] !== undefined && (typeof params[field] !== 'number' || !Number.isFinite(params[field]))) {
314
+ fail('INVALID_ARGUMENT', `${field} must be a finite number.`);
315
+ }
316
+ }
317
+ actionAttempted = true;
318
+ target.scrollTo(
319
+ {
320
+ left: params.left === undefined ? target.scrollLeft : params.left,
321
+ top: params.top === undefined ? target.scrollTop : params.top,
322
+ behavior: 'instant'
323
+ }
324
+ );
325
+ method = 'scrollTo';
326
+ }
327
+ return {
328
+ action,
329
+ method,
330
+ invoked: true,
331
+ eventsDispatched,
332
+ target: describeElement(target, params.selector)
333
+ };
334
+ }
335
+
336
+ function fillElement(target) {
337
+ if (typeof params.value !== 'string') {
338
+ fail('INVALID_ARGUMENT', 'fill requires an exact string value.');
339
+ }
340
+ if (target.readOnly) fail('ARCANE_APP_CONTROL_READ_ONLY', 'The selected control is read-only.');
341
+ let prototype;
342
+ if (target.localName === 'textarea') {
343
+ prototype = HTMLTextAreaElement.prototype;
344
+ } else if (target.localName === 'input') {
345
+ if (['file', 'checkbox', 'radio', 'button', 'submit', 'reset', 'image'].includes(target.type)) {
346
+ fail('ARCANE_APP_CONTROL_UNSUPPORTED', 'The selected input type does not support filling text.');
347
+ }
348
+ prototype = HTMLInputElement.prototype;
349
+ } else if (!target.isContentEditable) {
350
+ fail('ARCANE_APP_CONTROL_UNSUPPORTED', 'fill requires an input, textarea or contenteditable element.');
351
+ }
352
+
353
+ actionAttempted = true;
354
+ if (prototype) {
355
+ Object.getOwnPropertyDescriptor(prototype, 'value').set.call(target, params.value);
356
+ } else {
357
+ Object.getOwnPropertyDescriptor(Node.prototype, 'textContent').set.call(target, params.value);
358
+ }
359
+ dispatchInputAndChange(target, params.value, true);
360
+ }
361
+
362
+ function selectOptions(target) {
363
+ if (target.localName !== 'select') {
364
+ fail('ARCANE_APP_CONTROL_UNSUPPORTED', 'select requires a native select element.');
365
+ }
366
+ if ((params.value === undefined) === (params.values === undefined)) {
367
+ fail('INVALID_ARGUMENT', 'select requires either value or values.');
368
+ }
369
+ const values = params.values === undefined ? [params.value] : params.values;
370
+ if (!Array.isArray(values)) fail('INVALID_ARGUMENT', 'values must be an array of exact strings.');
371
+ for (const value of values) {
372
+ if (typeof value !== 'string') fail('INVALID_ARGUMENT', 'Every selected value must be an exact string.');
373
+ }
374
+ if (!target.multiple && values.length > 1) {
375
+ fail('INVALID_ARGUMENT', 'A single-select control accepts at most one selected value.');
376
+ }
377
+ const optionsByValue = new Map();
378
+ for (const option of target.options) {
379
+ if (!optionsByValue.has(option.value)) {
380
+ optionsByValue.set(
381
+ option.value,
382
+ []
383
+ );
384
+ }
385
+ optionsByValue.get(option.value).push(option);
386
+ }
387
+ const selectedOptions = new Set();
388
+ for (const value of values) {
389
+ const options = optionsByValue.get(value);
390
+ if (!options) {
391
+ fail(
392
+ 'ARCANE_APP_CONTROL_OPTION_MISSING',
393
+ 'No option has the requested value.',
394
+ {value}
395
+ );
396
+ }
397
+ for (const option of options) {
398
+ if (option.matches(':disabled')) {
399
+ fail(
400
+ 'ARCANE_APP_CONTROL_DISABLED',
401
+ 'A requested option is disabled.',
402
+ {value}
403
+ );
404
+ }
405
+ selectedOptions.add(option);
406
+ if (!target.multiple) break;
407
+ }
408
+ }
409
+ actionAttempted = true;
410
+ const setter = Object.getOwnPropertyDescriptor(HTMLOptionElement.prototype, 'selected').set;
411
+ for (const option of target.options) {
412
+ setter.call(option, selectedOptions.has(option));
413
+ }
414
+ if (selectedOptions.size === 0) {
415
+ Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'selectedIndex').set.call(target, -1);
416
+ }
417
+ dispatchInputAndChange(target, null, false);
418
+ }
419
+
420
+ function dispatchInputAndChange(target, data, textInput) {
421
+ const input = textInput
422
+ ? new InputEvent(
423
+ 'input',
424
+ {bubbles: true, composed: true, inputType: 'insertReplacementText', data}
425
+ )
426
+ : new Event(
427
+ 'input',
428
+ {bubbles: true, composed: true}
429
+ );
430
+ target.dispatchEvent(input);
431
+ eventsDispatched.push('input');
432
+ const change = new Event(
433
+ 'change',
434
+ {bubbles: true}
435
+ );
436
+ target.dispatchEvent(change);
437
+ eventsDispatched.push('change');
438
+ }
439
+ }
@@ -1116,6 +1116,17 @@ prepared playback without invoking `finishTTS()` or `streamTTS()`. Unmuted
1116
1116
  completion keeps its ordinary speech flush. Explicit calls to either public
1117
1117
  speech method retain their existing behavior.
1118
1118
 
1119
+ For a text-only request, pass `speech:false` to the object-form
1120
+ `ai.streamRequest({messages, speech:false, onChunk, signal})`. Its completion,
1121
+ failure, and cancellation leave buffered speech, prepared playback, and native
1122
+ playback alone. The option controls only the request's automatic speech
1123
+ finalization and stopping; it is not sent to the language provider and does not
1124
+ mute or unload TTS. Explicit speech calls made by your callbacks still run with
1125
+ their ordinary behavior. Omitted `speech` or `speech:true` preserves the
1126
+ automatic lifecycle above, and positional `streamMessage(...)` is unchanged.
1127
+ Explicit `streamTTS()`, `finishTTS()`, and global `stopAudio()` keep their existing
1128
+ scope.
1129
+
1119
1130
  ## Automatic speech-input formatting cleanup
1120
1131
 
1121
1132
  The shared streaming, audio-file, and `SpeechPlayback` paths remove repeated
@@ -115,13 +115,17 @@ The controller emits `statechange` and `progress` through
115
115
  current mutable status snapshot. Provider states are `unloaded`, `loading`, `ready`,
116
116
  `unloading`, and `error`.
117
117
 
118
- Browser-WASM model loads report `cache-check`, `download`, and `initialize`
119
- phases. Download records preserve the ordered model-file fields `completed`,
118
+ Browser-WASM model loads report `cache-check`, `load`, `download`, and `initialize`
119
+ phases. Cache-read and download records preserve the ordered model-file fields `completed`,
120
120
  `total`, and `unit:'files'`. Their ordered `members` records contain each file's
121
121
  `name`, committed-part `completed`, `total`, and `unit:'shards'`. A streaming
122
122
  response has `total:null` until its end is observed; completion then supplies
123
- the actual committed-part count. Previously complete members identify
124
- `state:'cached'`. `activeTransfers` is the current transfer-worker count,
123
+ the actual committed-part count. Reading saved parts reports `phase:'load'`,
124
+ `cached:true`, and the known saved-part total before the first read and after
125
+ each part is opened. An existing HTTP Range plan likewise supplies its actual
126
+ shard total. Previously complete members identify `state:'cached'`; their
127
+ per-member records remain cache reads even if another member is downloading.
128
+ A fully cached activation emits no download phase. `activeTransfers` is the current transfer-worker count,
125
129
  `transferLimit` is the selected concurrency, and `transferMode` identifies
126
130
  `probing`, `files`, `ranges`, or `single`. No byte progress, rate, or estimated
127
131
  completion time is produced. Closed-part and lifecycle changes publish promptly;
@@ -136,7 +140,7 @@ These messages do not change the runtime's log level or replace its complete
136
140
  diagnostic output.
137
141
 
138
142
  Initialization has `total:null` and omits `completed` and `unit`, so its progress
139
- bar is indeterminate. Downloaded-file completion is not activation completion.
143
+ bar is indeterminate. Cached or downloaded file completion is not activation completion.
140
144
  The metadata tensor count and GPU layer assignment are descriptive only:
141
145
  upstream reports assigned layers before model-weight loading has finished.
142
146
  The pinned runtime does not expose a measured overall initialization fraction
@@ -658,10 +662,13 @@ contains `kind`, `tableName`, `downloadConcurrency`, the original `adapter`,
658
662
  and `ready`, `install`, `ensure`, `remove`, and `fetchResource`. `ensure()` preserves the complete
659
663
  ordered model set and reports whether it was cached or installed.
660
664
  `install(source,{signal,onProgress})` and
661
- `ensure(source,{signal,onProgress,offline})` publish `cache-check` and
662
- `download` records using ordered file counts and per-member closed-part progress
665
+ `ensure(source,{signal,onProgress,offline})` publish `cache-check`, cache-reading
666
+ `load`, and actual network `download` records using ordered file counts and per-member closed-part progress
663
667
  when `onProgress` is supplied. Each member reports `completed`, `total`, and
664
- `unit:'shards'`; streaming totals remain `null` until completion. Records retain
668
+ `unit:'shards'`; streaming totals remain `null` until completion. Cache reads
669
+ publish the known stored-part plan from zero through each opened part, with
670
+ `cached:true`; the existing Range plan supplies known totals during resume.
671
+ No model length is converted into product progress. Records retain
665
672
  `activeTransfers`, `transferLimit`, and `transferMode`, with prompt part/lifecycle
666
673
  updates and an existing 250 ms heartbeat. They contain no transfer rate or ETA.
667
674
  Multi-file sources use up to that many concurrent member workers
@@ -693,8 +700,12 @@ loaders share this owner for actual configuration, tokenizer, graph,
693
700
  external-data and support-file requests. Complete successful GET resources may
694
701
  be reused. Cancellation joins reader/writer cleanup and preserves closed parts
695
702
  for supported HTTP resume; a full response restarts that resource without
696
- duplicating its saved prefix. Resource progress uses `unit:'shards'` with
697
- `total:null` until completion. Independent resource requests remain concurrent.
703
+ duplicating its saved prefix. Resource progress uses `unit:'shards'`. A network
704
+ response without a known shard plan retains `total:null` until completion;
705
+ opening a saved complete or partial resource reports `phase:'load'`,
706
+ `cached:true`, and the known number of stored parts before and during the read.
707
+ The resource URL accompanies every update. These counts describe resource
708
+ preparation, never engine activation. Independent resource requests remain concurrent.
698
709
 
699
710
  ### Availability and normalization
700
711
 
@@ -9,6 +9,13 @@ work, emits progress or heartbeat records, observes cancellation where safe,
9
9
  and exits nonzero on failure. Machine output is defined by
10
10
  `arcane-cli-events/1`.
11
11
 
12
+ `app-control` is the dedicated native-window client described in
13
+ [native application control](native-app-control.md). It writes a running JSON
14
+ acknowledgement to stderr, then complete result JSON to stdout, or complete error
15
+ JSON to stderr and a nonzero exit. Capture writes its image to the explicit
16
+ `--output` filename and returns metadata. Its options are scoped to that command;
17
+ the general output-format parser below applies to the other commands.
18
+
12
19
  ## Command inventory
13
20
 
14
21
  | Command | Scope and result |
@@ -31,6 +38,7 @@ and exits nonzero on failure. Machine output is defined by
31
38
  | `arcane run` | Serves an existing browser release, or packages, plans, builds, and launches one paired native artifact. |
32
39
  | `arcane update-check` | Performs one explicit, read-only npm dist-tag query for the installed SDK version. |
33
40
  | `arcane targets` | Lists target ids, declared status, formats, architectures, signing profiles, methods, and pairing reason. |
41
+ | `arcane app-control status\|inspect\|capture\|act` | Connects to an explicit running native window endpoint for document inspection, viewport capture or targeted DOM actions. |
34
42
  | `arcane repo status\|pull\|push` | Runs one selected repository operation for the current app workspace. |
35
43
  | `arcane mail key set\|status\|delete` | Manages one server-only Resend API-key profile in `.arcane.env.json`. |
36
44
  | `arcane mail send` | Performs one explicit, idempotency-keyed Resend attempt from a complete JSON report on redirected stdin. |
@@ -69,7 +77,7 @@ meaning and cardinality rules:
69
77
  | `--allow-to` | optional comma-separated addresses | `mail serve` |
70
78
  | `--report-key` | nonempty string | `mail send`; caller-owned stable Resend idempotency key, forwarded unchanged |
71
79
  | `--request-timeout` | optional integer from 1 through 2147483647 milliseconds | `mail send`, `mail serve` |
72
- | `--output` | `human`, `json`, `ndjson` | Every invocation; the final occurrence wins. |
80
+ | `--output` | `human`, `json`, `ndjson` | General toolchain invocations; the final occurrence wins. `app-control capture` instead uses its command-specific output filename. |
73
81
  | `--git` | flag | `new` |
74
82
  | `--skip-tests` | flag | `check --scope app` |
75
83
  | `--dry-run` | flag | `package`; parser-supported on `build` with the boundary below |
@@ -592,7 +592,21 @@ before the window closes; complete engineering errors go to the supplied
592
592
  diagnostic/error callbacks. Callbacks must not synchronously wait for the
593
593
  window's lifetime promises.
594
594
 
595
+ A WebView2 `BrowserProcessExited` failure retains its complete diagnostic,
596
+ fails a pending `Ready`, and begins the existing Core drain and window close.
597
+ It does not wait for a user to close a blank window or invoke document callbacks
598
+ and script cleanup in the closed browser. The launcher reports the failure
599
+ after that drain. Main-renderer exits and Core-only transport failures retain
600
+ their existing handling. This terminal-flow correction does not identify or
601
+ prevent the underlying browser-process exit, restart the application, or change
602
+ its profile and saved data.
603
+
595
604
  `ArcaneLauncher.cs` supplies the executable entry point for that composition.
605
+ An explicit `native.launchContext.appControl.endpoint` enables the current
606
+ Windows window's background document-control listener. The public client and
607
+ CLI inspect, capture and act on that existing WebView without selecting a desktop
608
+ window or changing its Core/profile. See [native application control](native-app-control.md)
609
+ for launch configuration, document generations, cancellation and platform scope.
596
610
  `tools/build-core-windows-host.mjs` builds the selected SDK host output from the
597
611
  approved WebView2 package, existing .NET Framework compiler and selected Node
598
612
  SEA output. It retains complete compiler diagnostics and produces a fresh
@@ -1,17 +1,31 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "sdkVersion": "0.79.0",
3
+ "sdkVersion": "0.81.0",
4
4
  "environment": {
5
5
  "runtime": "Node.js for Node entrypoints; browser for browser-only entrypoints",
6
6
  "minimumVersion": "22.23.2 for Node entrypoints",
7
7
  "moduleSystem": "ESM"
8
8
  },
9
- "memberCount": 301,
9
+ "memberCount": 302,
10
10
  "runtimeSubpathPatterns": {
11
11
  "arcane-os/modules/*": "./runtime/arcane/modules/*",
12
12
  "arcane-os/entities/*": "./runtime/arcane/entities/*"
13
13
  },
14
14
  "members": [
15
+ {
16
+ "id": "core-app-control:connectAppControl",
17
+ "name": "connectAppControl",
18
+ "displayName": "connectAppControl()",
19
+ "kind": "function",
20
+ "signature": "connectAppControl({endpoint,signal,onError}={})",
21
+ "entrypoints": ["arcane-os/core/app-control"],
22
+ "primaryImport": "arcane-os/core/app-control",
23
+ "group": "Native application control",
24
+ "summary": "Connects to one explicit running native window for background document inspection, viewport capture and targeted DOM actions.",
25
+ "availability": "Node local IPC on Windows, Linux and macOS; the current window adapter is Windows WebView2, with other native adapters unavailable",
26
+ "protocol": "arcane/1 app.control.status, app.control.inspect, app.control.capture and app.control.act over the existing framed local connection. The window host selects native.launchContext.appControl.endpoint and preserves its existing Core child, origin, profile and lifetime.",
27
+ "normalization": "Returns endpoint, closed, status(options), inspect(parameters={},options), capture(parameters={},options), act(parameters,options), and close(). Parameters pass unchanged; per-operation options preserve Core signal/timeout behavior. Status reports actual app/window/document readiness. Inspect returns complete selected top-level light-DOM HTML/text and live control state with DOM-derived role/name hints; frames and open shadow hosts are descriptors only. Capture returns mimeType image/png, complete base64 data, documentGeneration and URL for the rendered viewport. Act requires documentGeneration and one CSS selector with click, exact-string fill, exact-value select or absolute-offset scroll. The native document marker prevents queued actions crossing replacement; cancellation before dispatch prevents execution while dispatched work is observed through completion without retry or reversal. close() cancels only this connection, never the application or Core. No launch, automatic reconnect, desktop activation, global input or public arbitrary evaluator. See native-app-control.md for complete result fields, CLI and platform scope."
28
+ },
15
29
  {
16
30
  "id": "local-ai-execution-devices:createExecutionDeviceCatalog",
17
31
  "name": "createExecutionDeviceCatalog",
@@ -45,14 +59,14 @@
45
59
  "name": "createNativeDecisionModel",
46
60
  "displayName": "createNativeDecisionModel()",
47
61
  "kind": "function",
48
- "signature": "createNativeDecisionModel({onnx,modelAssets,workingDirectory,paths,assetProjectionId,resourcePaths,model='onnx-community/laya-typed-decisions-ONNX',revision='main',dtype='fp32',sessionOptions,executionPreference='gpu',executionTarget,signal}={})",
62
+ "signature": "createNativeDecisionModel({onnx,modelAssets,workingDirectory,paths,assetProjectionId,resourcePaths,family='laya',model='onnx-community/laya-typed-decisions-ONNX',revision='main',dtype='fp32',sessionOptions,executionPreference='gpu',executionTarget,signal}={})",
49
63
  "entrypoints": ["arcane-os/local-ai/decisions"],
50
64
  "primaryImport": "arcane-os/local-ai/decisions",
51
65
  "group": "Native typed decisions",
52
- "summary": "Explicit native Laya FP32 option scoring through an existing ONNX owner and a tokenizer Worker.",
66
+ "summary": "Explicit native Laya FP32/FP16 or Julia FP32 option scoring through an existing ONNX owner and a tokenizer Worker.",
53
67
  "availability": "Node on Windows/Linux/macOS with selected ONNX Runtime and model files; Android needs host adaptation",
54
- "protocol": "Shared browser/native decision encoding with native named tensor results",
55
- "normalization": "Returns load/evaluate/classify/current/status/subscribe/unload/dispose; preserves complete separate state/question/options/type and original rows, raw scores and every graph output. load({assetProjectionId,resourcePaths,executionTarget,signal}) retains omitted or undefined source, mapping and target fields from the latest selection, initially the factory configuration. A supplied ready projection takes precedence over configured paths and upstream settings; the existing modelAssets owner retains it directly without upstream preparation or fetching. resourcePaths maps model, tokenizer and tokenizerConfig to exact projection member.path values; complete companion files remain at their original relative paths. Missing projections or mapped members report their actual failure. assetProjectionId:null returns to configured paths or existing upstream preparation when no paths were configured. Separately, executionTarget:null requests automatic resolution, while {deviceId} forwards the saved physical selection to the existing ONNX owner. With no target selection, GPU preference still attempts advertised providers then CPU and explicit session providers retain precedence. Equal source, complete mapping and target selections share activation; a changed selection cancels the earlier request and joins its actual ONNX/tokenizer cleanup before replacement. Incoming projection use is retained before retiring the previous activation; caller preparation ownership is independent, and native files remain retained through actual ONNX and tokenizer Worker exit. Unload ends native use; loading again after all ownership is released requires a fresh projection. pendingActivation reports the requested replacement and projected source/mapping separately; current().execution remains the complete ONNX load record, with requested/resolved/configured/observed target diagnostics in current().execution.execution. Configuration alone does not prove physical execution. Cancellation retires the shared activation. No preference writes, chat history or duplicate inference engine."
68
+ "protocol": "Shared browser/native decision encoding with native named tensor results. Factory and each load accept family/model/revision/dtype; omission or undefined retains the selected value. Laya FP32 defaults remain. Laya FP16 selects onnx/model_fp16.onnx and onnx/model_fp16.onnx_data; Julia FP32 selects model.onnx and model.onnx.data, with its own tokenizer files. Complete new-family selection and matching files are caller-owned. All model fields participate in activation equality; top-level metadata stays attributable to the retiring activation while pendingActivation records its requested successor. Decoder preserves raw probabilities, every original row and every actual output without inventing Julia action logits.",
69
+ "normalization": "Returns load/evaluate/classify/current/status/subscribe/unload/dispose; preserves complete separate state/question/options/type and original rows, raw scores and every graph output. load({family,model,revision,dtype,assetProjectionId,resourcePaths,executionTarget,signal}) retains omitted or undefined family, model, revision, dtype, source, mapping and target fields from the latest selection, initially the factory configuration. A supplied ready projection takes precedence over configured paths and upstream settings; the existing modelAssets owner retains it directly without upstream preparation or fetching. resourcePaths maps model, tokenizer and tokenizerConfig to exact projection member.path values; complete companion files remain at their original relative paths. Missing projections or mapped members report their actual failure. assetProjectionId:null returns to configured paths or existing upstream preparation when no paths were configured. Separately, executionTarget:null requests automatic resolution, while {deviceId} forwards the saved physical selection to the existing ONNX owner. With no target selection, GPU preference still attempts advertised providers then CPU and explicit session providers retain precedence. Equal family, model, revision, dtype, source, complete mapping and target selections share activation; a changed selection cancels the earlier request and joins its actual ONNX/tokenizer cleanup before replacement. Incoming projection use is retained before retiring the previous activation; caller preparation ownership is independent, and native files remain retained through actual ONNX and tokenizer Worker exit. Unload ends native use; loading again after all ownership is released requires a fresh projection. pendingActivation reports the requested replacement and projected source/mapping separately; current().execution remains the complete ONNX load record, with requested/resolved/configured/observed target diagnostics in current().execution.execution. Configuration alone does not prove physical execution. Cancellation retires the shared activation. No preference writes, chat history or duplicate inference engine."
56
70
  },
57
71
  {
58
72
  "id": "core-decisions:createNativeDecisionService",
@@ -63,10 +77,10 @@
63
77
  "entrypoints": ["arcane-os/core/decisions"],
64
78
  "primaryImport": "arcane-os/core/decisions",
65
79
  "group": "Native typed decisions",
66
- "summary": "Renderer-independent native Laya service reusing the registered local-AI and model-assets owners.",
80
+ "summary": "Renderer-independent native Laya/Julia service reusing the registered local-AI and model-assets owners.",
67
81
  "availability": "Node Core with selected native ONNX and model files",
68
- "protocol": "arcane/1 decisions.status/load/evaluate/unload and decisions.state",
69
- "normalization": "Registration starts no model or tokenizer. Explicit load obtains dependency-ready same-process owners and reacquires getONNXRuntime after recovery. Factory configuration accepts assetProjectionId, resourcePaths and executionTarget; direct load({assetProjectionId,resourcePaths,executionTarget,signal}) and Core decisions.load({assetProjectionId,resourcePaths,executionTarget}) preserve omitted/undefined selections, with the Core request signal forwarded separately. resourcePaths maps model, tokenizer and tokenizerConfig to exact ready projection member.path values; complete companion members retain their relative paths. Supplied projections take precedence over configured paths/upstream settings, are retained through the existing model-assets owner, and never invoke upstream preparation or fetching; unavailable projections or mapped members fail directly. assetProjectionId:null returns to configured paths or existing upstream preparation, independently of executionTarget:null requesting automatic device selection. Equal source, complete mapping and target selections coalesce; changed selections cancel pending work and join the prior model's disposal before replacement. Incoming retained ownership protects projection files while the previous activation retires; caller preparation ownership remains independent, and native use retains files through actual ONNX/tokenizer exit. pendingActivation includes a projected source/mapping separately from the full activation execution record and its nested execution diagnostics. Native load/evaluate/classify/current/subscribe/unload/dispose preserve complete results; RPC uses the existing tensor codec. Unload has accepted service lifetime; host shutdown cancels request-lifetime load/evaluation."
82
+ "protocol": "arcane/1 decisions.status/load/evaluate/unload and decisions.state. Factory, direct load and decisions.load accept family/model/revision/dtype with the direct model's exact variant, omission and replacement semantics. The service persists across explicit model changes; top-level activation metadata and pendingActivation distinguish the retiring and requested selections.",
83
+ "normalization": "Registration starts no model or tokenizer. Explicit load obtains dependency-ready same-process owners and reacquires getONNXRuntime after recovery. Factory configuration accepts family, model, revision, dtype, assetProjectionId, resourcePaths and executionTarget; direct load({family,model,revision,dtype,assetProjectionId,resourcePaths,executionTarget,signal}) and Core decisions.load({family,model,revision,dtype,assetProjectionId,resourcePaths,executionTarget}) preserve omitted/undefined selections, with the Core request signal forwarded separately. resourcePaths maps model, tokenizer and tokenizerConfig to exact ready projection member.path values; complete companion members retain their relative paths. Supplied projections take precedence over configured paths/upstream settings, are retained through the existing model-assets owner, and never invoke upstream preparation or fetching; unavailable projections or mapped members fail directly. assetProjectionId:null returns to configured paths or existing upstream preparation, independently of executionTarget:null requesting automatic device selection. Equal family, model, revision, dtype, source, complete mapping and target selections coalesce; changed selections cancel pending work and join the prior model's disposal before replacement. Incoming retained ownership protects projection files while the previous activation retires; caller preparation ownership remains independent, and native use retains files through actual ONNX/tokenizer exit. pendingActivation includes a projected source/mapping separately from the full activation execution record and its nested execution diagnostics. Native load/evaluate/classify/current/subscribe/unload/dispose preserve complete results; RPC uses the existing tensor codec. Unload has accepted service lifetime; host shutdown cancels request-lifetime load/evaluation."
70
84
  },
71
85
  {
72
86
  "id": "core-decisions:default",