arcane-os 0.83.0 → 0.84.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.
@@ -1,11 +1,13 @@
1
1
  import {parentPort,workerData} from 'node:worker_threads';
2
2
  import {pathToFileURL} from 'node:url';
3
3
  import Is from 'strong-type';
4
+ import {encodeTensorMap,encodeTensorFetches} from '../../browser-runtime/ai/onnx-tensors.mjs';
4
5
 
5
6
  const is=new Is(false);
6
7
  const imported=await import(pathToFileURL(workerData.modulePath).href);
7
8
  const ort=imported.default??imported;
8
9
  let session;
10
+ let sessionExecution;
9
11
 
10
12
  function describeError(error){
11
13
  if(!(error instanceof Error))return {name:'Error',message:String(error),cause:error};
@@ -27,6 +29,7 @@ async function createSession(request) {
27
29
  resolution: 'automatic',
28
30
  reason: selectProviders ? 'engine-default-required' : 'caller-session-options',
29
31
  ...request.targetResolution,
32
+ deviceInventory: request.deviceInventory ?? null,
30
33
  configuredTarget: null,
31
34
  observedTarget: null,
32
35
  supportedBackends: [],
@@ -225,28 +228,35 @@ function tensorMap(records,inputs=false){
225
228
  }
226
229
 
227
230
  async function handleRequest(request){
231
+ let runStage;
232
+ let feeds;
233
+ let fetches;
228
234
  try{
229
235
  let result;
230
236
  if(request.operation==='load'){
231
- const execution = await createSession(request);
237
+ sessionExecution = await createSession(request);
232
238
  result={
233
239
  inputNames:session.inputNames,
234
240
  outputNames:session.outputNames,
235
241
  inputMetadata:session.inputMetadata,
236
242
  outputMetadata:session.outputMetadata
237
243
  };
238
- if (execution) result.execution = execution;
244
+ if (sessionExecution) result.execution = sessionExecution;
239
245
  }else if(request.operation==='run'){
240
- const feeds=tensorMap(request.feeds,true);
241
- const fetches=request.fetches===undefined||request.fetches===null||is.array(request.fetches)
246
+ runStage='feed-conversion';
247
+ feeds=tensorMap(request.feeds,true);
248
+ runStage='fetch-conversion';
249
+ fetches=request.fetches===undefined||request.fetches===null||is.array(request.fetches)
242
250
  ?request.fetches:tensorMap(request.fetches);
243
251
  // ORT treats an empty fetch map as its options overload and would
244
252
  // ignore the third argument. Both forms select every output.
245
253
  const allOutputs=fetches===undefined
246
254
  ||(fetches!==null&&!is.array(fetches)&&Object.keys(fetches).length===0);
255
+ runStage='session.run';
247
256
  const outputs=allOutputs
248
257
  ?await session.run(feeds,request.runOptions)
249
258
  :await session.run(feeds,fetches,request.runOptions);
259
+ runStage='output-conversion';
250
260
  result=Object.fromEntries(Object.entries(outputs).map(function tensorRecord([name,tensor]){
251
261
  return [name,{type:tensor.type,data:tensor.data,dims:tensor.dims}];
252
262
  }));
@@ -258,9 +268,33 @@ async function handleRequest(request){
258
268
  }
259
269
  // Structured cloning retains typed tensors and BigInt without taking
260
270
  // ownership of the caller's buffers or converting their contents.
271
+ if(request.operation==='run')runStage='result-delivery';
261
272
  parentPort.postMessage({requestId:request.requestId,operation:request.operation,result});
262
273
  }catch(error){
263
- parentPort.postMessage({requestId:request.requestId,operation:request.operation,error:describeError(error)});
274
+ const record=describeError(error);
275
+ if(request.operation==='run'){
276
+ const diagnostic={
277
+ sessionId:workerData.sessionId,requestId:request.requestId,stage:runStage,
278
+ model:workerData.model,runtimeModulePath:workerData.modulePath,
279
+ execution:sessionExecution??null,runOptions:request.runOptions,
280
+ feedsSource:feeds===undefined?'request':'converted',
281
+ fetchesSource:runStage==='feed-conversion'||runStage==='fetch-conversion'?'request':'converted'
282
+ };
283
+ record.onnxRun=diagnostic;
284
+ try{
285
+ diagnostic.inputNames=session?.inputNames??null;
286
+ diagnostic.inputMetadata=session?.inputMetadata??null;
287
+ diagnostic.outputNames=session?.outputNames??null;
288
+ diagnostic.outputMetadata=session?.outputMetadata??null;
289
+ // Copy complete tensors only after failure. The same codec used
290
+ // by Core tensor transport preserves BigInt and typed values.
291
+ diagnostic.feeds=encodeTensorMap(feeds===undefined?request.feeds:feeds);
292
+ diagnostic.fetches=encodeTensorFetches(diagnostic.fetchesSource==='request'?request.fetches:fetches);
293
+ }catch(diagnosticError){
294
+ diagnostic.diagnosticError=describeError(diagnosticError);
295
+ }
296
+ }
297
+ parentPort.postMessage({requestId:request.requestId,operation:request.operation,error:record});
264
298
  }finally{
265
299
  if(request.operation==='unload')parentPort.close();
266
300
  }
@@ -216,7 +216,7 @@ export function createONNXRuntime({modulePath,onEvent,signal,executionDevices:su
216
216
  operationSignal.throwIfAborted();
217
217
  if (entry.stopping || entry.exited || entry.active !== operation) return;
218
218
  operation.payload = {...operation.payload, targetResolution, deviceInventory};
219
- entry.state.execution = {...entry.state.execution, ...targetResolution};
219
+ entry.state.execution = {...entry.state.execution, ...targetResolution, deviceInventory: deviceInventory ?? null};
220
220
  publish(entry);
221
221
  postOperation(entry, operation);
222
222
  }
@@ -348,7 +348,7 @@ export function createONNXRuntime({modulePath,onEvent,signal,executionDevices:su
348
348
  state:{id,model,state:'loading',loaded:false,error:null,inputNames:[],outputNames:[],inputMetadata:[],outputMetadata:[],
349
349
  execution: {requestedTarget: executionTarget ?? null, resolvedDevice: null,
350
350
  resolution: executionTarget === undefined || executionTarget === null ? 'automatic' : null,
351
- reason: 'session-loading', configuredTarget: null, observedTarget: null}},
351
+ reason: 'session-loading', configuredTarget: null, observedTarget: null, deviceInventory: null}},
352
352
  controller: new AbortController(), preparation: null,
353
353
  worker:null,active:null,queue:[],stopping:false,exited:false,stopTask:null,terminalError:null,releaseError:null,terminationRequested:false,outputTasks:[]
354
354
  };
@@ -356,7 +356,7 @@ export function createONNXRuntime({modulePath,onEvent,signal,executionDevices:su
356
356
  sessions.set(id,entry);
357
357
  try{
358
358
  entry.worker=new Worker(new URL('./onnx-worker.mjs',import.meta.url),{
359
- workerData:{modulePath},stdout:true,stderr:true
359
+ workerData:{modulePath,sessionId:id,model},stdout:true,stderr:true
360
360
  });
361
361
  entry.outputTasks.push(
362
362
  captureOutput(entry,entry.worker.stdout,'local-ai.onnx.stdout'),
@@ -203,7 +203,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
203
203
  "import path from 'node:path';",
204
204
  "import {isSea} from 'node:sea';",
205
205
  ...(launchDefaults === undefined ? [] : ["import {mkdir} from 'node:fs/promises';"]),
206
- "import {readCoreLaunchContext, startCoreHost, runSharedCoreHost, startSharedCoreBridge} from 'arcane-os/core/host';",
206
+ "import {readCoreLaunchContext, startCoreHost, startCoreListener, runSharedCoreHost, startSharedCoreBridge} from 'arcane-os/core/host';",
207
207
  '',
208
208
  ...(localAI === undefined ? [] : [
209
209
  '// Bundled runtime paths are artifact-root-relative POSIX paths.',
@@ -249,6 +249,10 @@ export function coreEntrySource(application, version, services, {localAI, runtim
249
249
  : ` ...await readCoreLaunchContext({appId: ${JSON.stringify(application.id)}, defaults: JSON.parse(${JSON.stringify(JSON.stringify(launchDefaults))})})`,
250
250
  '};',
251
251
  '',
252
+ 'if (context.coreListener !== undefined && context.sharedHost !== undefined) {',
253
+ " throw new TypeError('coreListener attaches to the window-owned Core; sharedHost selects its separate owning host.');",
254
+ '}',
255
+ '',
252
256
  ...(launchDefaults === undefined ? [] : [
253
257
  '// The shared endpoint and diagnostic log require their selected state directory.',
254
258
  'if (context.sharedHost !== undefined) await mkdir(context.stateRoot, {recursive: true});',
@@ -284,10 +288,29 @@ export function coreEntrySource(application, version, services, {localAI, runtim
284
288
  `const application = JSON.parse(${JSON.stringify(JSON.stringify(application))});`,
285
289
  `const version = ${JSON.stringify(version)};`,
286
290
  'let host;',
291
+ 'let listener;',
287
292
  'if (context.sharedHost === undefined) {',
288
293
  ' const services = [];',
289
294
  ' await createServices(function register(service) { services.push(service); });',
290
295
  ' host = startCoreHost({application, version, services});',
296
+ ' if (context.coreListener !== undefined) {',
297
+ " process.on('SIGINT', closeCore);",
298
+ " process.on('SIGTERM', closeCore);",
299
+ ' try {',
300
+ ' listener = await startCoreListener({runtime: host.runtime, endpoint: context.coreListener?.endpoint});',
301
+ ' listener.closed.catch(function listenerFailed() { process.exitCode = 1; });',
302
+ ' } catch (error) {',
303
+ ' try {',
304
+ ' await host.close();',
305
+ ' } catch (closeError) {',
306
+ " throw new AggregateError([error, closeError], 'Core listener startup and host drain failed.');",
307
+ ' }',
308
+ ' throw error;',
309
+ ' } finally {',
310
+ " process.off('SIGINT', closeCore);",
311
+ " process.off('SIGTERM', closeCore);",
312
+ ' }',
313
+ ' }',
291
314
  "} else if (process.argv.includes('--arcane-core-headless')) {",
292
315
  ' host = await runSharedCoreHost({',
293
316
  ' endpoint: context.sharedHost.endpoint, application, version,',
@@ -319,6 +342,12 @@ export function coreEntrySource(application, version, services, {localAI, runtim
319
342
  " console.error('Arcane Core host failed:', error);",
320
343
  ' process.exitCode = 1;',
321
344
  '} finally {',
345
+ ' try {',
346
+ ' await listener?.close();',
347
+ ' } catch {',
348
+ ' // The listener reports its complete failure through onError.',
349
+ ' process.exitCode = 1;',
350
+ ' }',
322
351
  " process.off('SIGINT', closeCore);",
323
352
  " process.off('SIGTERM', closeCore);",
324
353
  '}',