@agentuity/core 1.0.54 → 1.0.56

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/dist/services/oauth/flow.d.ts +31 -0
  2. package/dist/services/oauth/flow.d.ts.map +1 -1
  3. package/dist/services/oauth/flow.js +138 -13
  4. package/dist/services/oauth/flow.js.map +1 -1
  5. package/dist/services/oauth/index.d.ts +1 -0
  6. package/dist/services/oauth/index.d.ts.map +1 -1
  7. package/dist/services/oauth/index.js +1 -0
  8. package/dist/services/oauth/index.js.map +1 -1
  9. package/dist/services/oauth/token-storage.d.ts +109 -0
  10. package/dist/services/oauth/token-storage.d.ts.map +1 -0
  11. package/dist/services/oauth/token-storage.js +140 -0
  12. package/dist/services/oauth/token-storage.js.map +1 -0
  13. package/dist/services/oauth/types.d.ts +11 -0
  14. package/dist/services/oauth/types.d.ts.map +1 -1
  15. package/dist/services/oauth/types.js +19 -0
  16. package/dist/services/oauth/types.js.map +1 -1
  17. package/dist/services/sandbox/execute.d.ts.map +1 -1
  18. package/dist/services/sandbox/execute.js +22 -11
  19. package/dist/services/sandbox/execute.js.map +1 -1
  20. package/dist/services/sandbox/run.d.ts.map +1 -1
  21. package/dist/services/sandbox/run.js +83 -30
  22. package/dist/services/sandbox/run.js.map +1 -1
  23. package/dist/services/sandbox/types.d.ts +8 -0
  24. package/dist/services/sandbox/types.d.ts.map +1 -1
  25. package/dist/services/sandbox/types.js +14 -0
  26. package/dist/services/sandbox/types.js.map +1 -1
  27. package/package.json +2 -2
  28. package/src/services/oauth/flow.ts +156 -15
  29. package/src/services/oauth/index.ts +1 -0
  30. package/src/services/oauth/token-storage.ts +220 -0
  31. package/src/services/oauth/types.ts +26 -0
  32. package/src/services/sandbox/execute.ts +26 -12
  33. package/src/services/sandbox/run.ts +129 -34
  34. package/src/services/sandbox/types.ts +14 -0
@@ -224,20 +224,103 @@ export async function sandboxRun(
224
224
  logger?.debug('streams completed, fetching final status');
225
225
 
226
226
  // Stream EOF means the sandbox is done — hadron only closes streams after the
227
- // container exits. Fetch status once for the exit code; if lifecycle events
228
- // haven't propagated to Catalyst yet, default to exit code 0.
227
+ // container exits. Poll for the exit code with retries because the lifecycle
228
+ // event (carrying the exit code) may still be in flight to Catalyst when the
229
+ // stream completes.
230
+ //
231
+ // Hadron drains container logs for up to 5s after exit, then closes the
232
+ // stream, then sends the lifecycle event in a goroutine. So the exit code
233
+ // typically arrives at Catalyst 5–7s after the container exits. We use a
234
+ // linear 1s polling interval (not exponential backoff) so we don't overshoot
235
+ // the window — 15 attempts × 1s = 15s total, which comfortably covers the
236
+ // drain + lifecycle propagation delay.
237
+ // Abort-aware sleep that rejects when the caller's signal fires.
238
+ const abortAwareSleep = (ms: number): Promise<void> =>
239
+ new Promise((resolve, reject) => {
240
+ if (signal?.aborted) {
241
+ reject(new DOMException('Aborted', 'AbortError'));
242
+ return;
243
+ }
244
+ const timer = setTimeout(resolve, ms);
245
+ signal?.addEventListener(
246
+ 'abort',
247
+ () => {
248
+ clearTimeout(timer);
249
+ reject(new DOMException('Aborted', 'AbortError'));
250
+ },
251
+ { once: true }
252
+ );
253
+ });
254
+
229
255
  let exitCode = 0;
230
- try {
231
- const sandboxStatus = await sandboxGetStatus(client, { sandboxId, orgId });
232
- if (sandboxStatus.exitCode != null) {
233
- exitCode = sandboxStatus.exitCode;
234
- } else if (sandboxStatus.status === 'failed') {
235
- exitCode = 1;
256
+ const maxStatusRetries = 15;
257
+ const statusPollInterval = 1000;
258
+ const statusPollStart = Date.now();
259
+ for (let attempt = 0; attempt < maxStatusRetries; attempt++) {
260
+ if (signal?.aborted) {
261
+ break;
262
+ }
263
+ try {
264
+ const sandboxStatus = await sandboxGetStatus(client, { sandboxId, orgId });
265
+ if (sandboxStatus.exitCode != null) {
266
+ exitCode = sandboxStatus.exitCode;
267
+ logger?.debug(
268
+ '[run] exit code %d found on attempt %d/%d (+%dms)',
269
+ exitCode,
270
+ attempt + 1,
271
+ maxStatusRetries,
272
+ Date.now() - statusPollStart
273
+ );
274
+ break;
275
+ } else if (sandboxStatus.status === 'failed') {
276
+ exitCode = 1;
277
+ logger?.debug(
278
+ '[run] sandbox failed on attempt %d/%d (+%dms)',
279
+ attempt + 1,
280
+ maxStatusRetries,
281
+ Date.now() - statusPollStart
282
+ );
283
+ break;
284
+ } else if (sandboxStatus.status === 'terminated') {
285
+ // Sandbox was destroyed. If exit code is missing, the
286
+ // terminated event may have overwritten it. Stop polling —
287
+ // no further updates will come.
288
+ logger?.debug(
289
+ '[run] sandbox terminated without exit code on attempt %d/%d (+%dms)',
290
+ attempt + 1,
291
+ maxStatusRetries,
292
+ Date.now() - statusPollStart
293
+ );
294
+ break;
295
+ }
296
+ // Exit code not yet propagated — wait before next poll.
297
+ if (attempt < maxStatusRetries - 1) {
298
+ await abortAwareSleep(statusPollInterval);
299
+ }
300
+ } catch (err) {
301
+ if (err instanceof DOMException && err.name === 'AbortError') {
302
+ break;
303
+ }
304
+ // Transient failure (sandbox briefly unavailable, network error).
305
+ // Retry instead of giving up — the lifecycle event may still arrive.
306
+ logger?.debug(
307
+ '[run] sandboxGetStatus attempt %d/%d failed (+%dms): %s',
308
+ attempt + 1,
309
+ maxStatusRetries,
310
+ Date.now() - statusPollStart,
311
+ err
312
+ );
313
+ if (attempt < maxStatusRetries - 1) {
314
+ await abortAwareSleep(statusPollInterval);
315
+ }
236
316
  }
237
- } catch {
238
- // Sandbox may already be destroyed (fire-and-forget teardown).
239
- // Stream EOF already confirmed execution completed.
240
- logger?.debug('sandboxGetStatus failed after stream EOF, using default exit code 0');
317
+ }
318
+ if (exitCode === 0) {
319
+ logger?.debug(
320
+ '[run] exit code polling finished with default 0 after %d attempts (+%dms)',
321
+ maxStatusRetries,
322
+ Date.now() - statusPollStart
323
+ );
241
324
  }
242
325
 
243
326
  if (timingLogsEnabled)
@@ -387,44 +470,56 @@ async function streamUrlToWritable(
387
470
  writable: Writable,
388
471
  signal: AbortSignal,
389
472
  logger?: Logger,
390
- started?: number
473
+ _started?: number
391
474
  ): Promise<void> {
475
+ const streamStart = Date.now();
392
476
  try {
393
- logger?.debug('fetching stream: %s', url);
394
- const response = await fetch(url, { signal });
395
- logger?.debug('stream response status: %d', response.status);
396
- if (timingLogsEnabled && started)
397
- console.error(
398
- `[TIMING] +${Date.now() - started}ms: stream response received (status: ${response.status})`
399
- );
477
+ // Signal to Pulse that this is a v2 stream so it waits for v2 metadata
478
+ // instead of falling back to the legacy download path on a short timeout.
479
+ const v2Url = new URL(url);
480
+ v2Url.searchParams.set('v', '2');
481
+ logger?.debug('[stream] fetching: %s', v2Url.href);
482
+ const response = await fetch(v2Url.href, { signal });
483
+ logger?.debug(
484
+ '[stream] response status=%d in %dms',
485
+ response.status,
486
+ Date.now() - streamStart
487
+ );
400
488
 
401
489
  if (!response.ok || !response.body) {
402
- logger?.debug('stream response not ok or no body');
490
+ logger?.debug('[stream] not ok or no body (status=%d) — returning empty', response.status);
403
491
  return;
404
492
  }
405
493
 
406
494
  const reader = response.body.getReader();
407
- let firstChunk = true;
495
+ let chunks = 0;
496
+ let totalBytes = 0;
408
497
 
409
498
  // Read until EOF - Pulse will block until data is available
410
499
  while (true) {
411
500
  const { done, value } = await reader.read();
412
501
  if (done) {
413
- logger?.debug('stream EOF');
414
- if (timingLogsEnabled && started)
415
- console.error(`[TIMING] +${Date.now() - started}ms: stream EOF`);
502
+ logger?.debug(
503
+ '[stream] EOF after %dms (%d chunks, %d bytes)',
504
+ Date.now() - streamStart,
505
+ chunks,
506
+ totalBytes
507
+ );
416
508
  break;
417
509
  }
418
510
 
419
511
  if (value) {
420
- if (firstChunk && started) {
421
- if (timingLogsEnabled)
422
- console.error(
423
- `[TIMING] +${Date.now() - started}ms: first chunk (${value.length} bytes)`
424
- );
425
- firstChunk = false;
512
+ chunks++;
513
+ totalBytes += value.length;
514
+ if (chunks <= 3 || chunks % 100 === 0) {
515
+ logger?.debug(
516
+ '[stream] chunk #%d: %d bytes (total: %d bytes, +%dms)',
517
+ chunks,
518
+ value.length,
519
+ totalBytes,
520
+ Date.now() - streamStart
521
+ );
426
522
  }
427
- logger?.debug('stream chunk: %d bytes', value.length);
428
523
  await writeAndDrain(writable, value);
429
524
  }
430
525
  }
@@ -433,9 +528,9 @@ async function streamUrlToWritable(
433
528
  writable.end();
434
529
  } catch (err) {
435
530
  if (err instanceof Error && err.name === 'AbortError') {
436
- logger?.debug('stream aborted');
531
+ logger?.debug('[stream] aborted after %dms', Date.now() - streamStart);
437
532
  return;
438
533
  }
439
- logger?.debug('stream error: %s', err);
534
+ logger?.debug('[stream] error after %dms: %s', Date.now() - streamStart, err);
440
535
  }
441
536
  }
@@ -398,6 +398,20 @@ export const SandboxSchema = z.object({
398
398
  .describe('Resume the sandbox from a paused or evacuated state.'),
399
399
  /** Destroy the sandbox */
400
400
  destroy: z.custom<() => Promise<void>>().describe('Destroy the sandbox'),
401
+ /** Create a new job in the sandbox */
402
+ createJob: z
403
+ .custom<(options: CreateJobOptions) => Promise<Job>>()
404
+ .describe('Create a new job in the sandbox'),
405
+ /** Get a job by ID */
406
+ getJob: z.custom<(jobId: string) => Promise<Job>>().describe('Get a job by ID'),
407
+ /** List jobs in the sandbox */
408
+ listJobs: z
409
+ .custom<(limit?: number) => Promise<{ jobs: Job[] }>>()
410
+ .describe('List jobs in the sandbox'),
411
+ /** Stop a running job */
412
+ stopJob: z
413
+ .custom<(jobId: string, force?: boolean) => Promise<Job>>()
414
+ .describe('Stop a running job'),
401
415
  });
402
416
  export type Sandbox = z.infer<typeof SandboxSchema>;
403
417