@agentuity/core 3.0.0-alpha.7 → 3.0.0-beta.1

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 (105) hide show
  1. package/README.md +2 -2
  2. package/dist/index.d.ts +0 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +0 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/services/coder/client.d.ts +13 -1
  7. package/dist/services/coder/client.d.ts.map +1 -1
  8. package/dist/services/coder/client.js +22 -1
  9. package/dist/services/coder/client.js.map +1 -1
  10. package/dist/services/coder/protocol.d.ts +121 -5
  11. package/dist/services/coder/protocol.d.ts.map +1 -1
  12. package/dist/services/coder/protocol.js +117 -0
  13. package/dist/services/coder/protocol.js.map +1 -1
  14. package/dist/services/coder/types.d.ts +73 -0
  15. package/dist/services/coder/types.d.ts.map +1 -1
  16. package/dist/services/coder/types.js +83 -1
  17. package/dist/services/coder/types.js.map +1 -1
  18. package/dist/services/coder/workspaces.d.ts +11 -1
  19. package/dist/services/coder/workspaces.d.ts.map +1 -1
  20. package/dist/services/coder/workspaces.js +34 -1
  21. package/dist/services/coder/workspaces.js.map +1 -1
  22. package/dist/services/keyvalue/service.d.ts +9 -3
  23. package/dist/services/keyvalue/service.d.ts.map +1 -1
  24. package/dist/services/keyvalue/service.js +6 -3
  25. package/dist/services/keyvalue/service.js.map +1 -1
  26. package/dist/services/oauth/token-storage.d.ts +4 -1
  27. package/dist/services/oauth/token-storage.d.ts.map +1 -1
  28. package/dist/services/oauth/token-storage.js +4 -1
  29. package/dist/services/oauth/token-storage.js.map +1 -1
  30. package/dist/services/queue/service.d.ts +31 -33
  31. package/dist/services/queue/service.d.ts.map +1 -1
  32. package/dist/services/queue/service.js +13 -13
  33. package/dist/services/queue/service.js.map +1 -1
  34. package/dist/services/sandbox/api-reference.js +8 -8
  35. package/dist/services/sandbox/api-reference.js.map +1 -1
  36. package/dist/services/sandbox/client.d.ts +3 -2
  37. package/dist/services/sandbox/client.d.ts.map +1 -1
  38. package/dist/services/sandbox/client.js.map +1 -1
  39. package/dist/services/sandbox/create.d.ts +5 -0
  40. package/dist/services/sandbox/create.d.ts.map +1 -1
  41. package/dist/services/sandbox/create.js +8 -0
  42. package/dist/services/sandbox/create.js.map +1 -1
  43. package/dist/services/sandbox/get.d.ts +8 -4
  44. package/dist/services/sandbox/get.d.ts.map +1 -1
  45. package/dist/services/sandbox/get.js +28 -3
  46. package/dist/services/sandbox/get.js.map +1 -1
  47. package/dist/services/sandbox/getStatus.d.ts +3 -0
  48. package/dist/services/sandbox/getStatus.d.ts.map +1 -1
  49. package/dist/services/sandbox/getStatus.js +19 -2
  50. package/dist/services/sandbox/getStatus.js.map +1 -1
  51. package/dist/services/sandbox/index.d.ts +1 -1
  52. package/dist/services/sandbox/index.d.ts.map +1 -1
  53. package/dist/services/sandbox/list.d.ts +3 -0
  54. package/dist/services/sandbox/list.d.ts.map +1 -1
  55. package/dist/services/sandbox/list.js +5 -0
  56. package/dist/services/sandbox/list.js.map +1 -1
  57. package/dist/services/sandbox/pause.d.ts +17 -1
  58. package/dist/services/sandbox/pause.d.ts.map +1 -1
  59. package/dist/services/sandbox/pause.js +21 -3
  60. package/dist/services/sandbox/pause.js.map +1 -1
  61. package/dist/services/sandbox/run.d.ts +3 -2
  62. package/dist/services/sandbox/run.d.ts.map +1 -1
  63. package/dist/services/sandbox/run.js +229 -82
  64. package/dist/services/sandbox/run.js.map +1 -1
  65. package/dist/services/sandbox/types.d.ts +11 -4
  66. package/dist/services/sandbox/types.d.ts.map +1 -1
  67. package/dist/services/sandbox/types.js +12 -0
  68. package/dist/services/sandbox/types.js.map +1 -1
  69. package/dist/services/session/get.d.ts +0 -2
  70. package/dist/services/session/get.d.ts.map +1 -1
  71. package/dist/services/session/get.js.map +1 -1
  72. package/dist/services/stream/namespaces.d.ts +2 -2
  73. package/dist/services/stream/namespaces.js +2 -2
  74. package/dist/services/stream/namespaces.js.map +1 -1
  75. package/dist/services/vector/service.d.ts +11 -11
  76. package/dist/services/vector/service.d.ts.map +1 -1
  77. package/dist/services/vector/service.js.map +1 -1
  78. package/package.json +3 -4
  79. package/src/env.d.ts +1 -31
  80. package/src/index.ts +0 -1
  81. package/src/services/coder/client.ts +34 -0
  82. package/src/services/coder/protocol.ts +121 -0
  83. package/src/services/coder/types.ts +94 -1
  84. package/src/services/coder/workspaces.ts +74 -0
  85. package/src/services/keyvalue/service.ts +16 -7
  86. package/src/services/oauth/token-storage.ts +4 -1
  87. package/src/services/queue/service.ts +31 -33
  88. package/src/services/sandbox/api-reference.ts +8 -8
  89. package/src/services/sandbox/client.ts +4 -4
  90. package/src/services/sandbox/create.ts +10 -0
  91. package/src/services/sandbox/get.ts +32 -3
  92. package/src/services/sandbox/getStatus.ts +23 -2
  93. package/src/services/sandbox/index.ts +1 -1
  94. package/src/services/sandbox/list.ts +5 -0
  95. package/src/services/sandbox/pause.ts +38 -4
  96. package/src/services/sandbox/run.ts +339 -103
  97. package/src/services/sandbox/types.ts +17 -2
  98. package/src/services/session/get.ts +0 -2
  99. package/src/services/stream/namespaces.ts +2 -2
  100. package/src/services/vector/service.ts +11 -21
  101. package/dist/deprecation.d.ts +0 -20
  102. package/dist/deprecation.d.ts.map +0 -1
  103. package/dist/deprecation.js +0 -102
  104. package/dist/deprecation.js.map +0 -1
  105. package/src/deprecation.ts +0 -120
@@ -1,16 +1,21 @@
1
1
  import type { Logger } from '../../logger.ts';
2
2
  import type { Readable, Writable } from 'node:stream';
3
3
  import { PassThrough } from 'node:stream';
4
+ import { finished } from 'node:stream/promises';
4
5
  import { z } from 'zod';
5
6
  import { APIClient, PaymentRequiredError } from '../api.ts';
6
7
  import { sandboxCreate } from './create.ts';
7
8
  import { sandboxDestroy } from './destroy.ts';
9
+ import { executionGet } from './execution.ts';
8
10
  import { sandboxGetStatus } from './getStatus.ts';
9
11
  import { ExecutionCancelledError, writeAndDrain } from './util.ts';
10
12
  import { SandboxRunOptionsSchema, type SandboxRunResult } from './types.ts';
11
13
  import { getServiceUrls } from '../config.ts';
12
14
 
13
15
  const timingLogsEnabled = false;
16
+ const EXECUTION_WAIT_DURATION = '5m';
17
+ const EXIT_CODE_FAST_WAIT_DURATION = '250ms';
18
+ const TERMINAL_EXECUTION_STATUSES = new Set(['completed', 'failed', 'timeout', 'cancelled']);
14
19
 
15
20
  /**
16
21
  * Creates a Writable stream that captures all chunks to a buffer array
@@ -187,52 +192,46 @@ export async function sandboxRun(
187
192
  }
188
193
  }
189
194
 
190
- // Wait for streams to complete Pulse closes streams on sandbox termination (EOF).
191
- // This is our primary completion signal; no polling needed.
192
- logger?.debug('waiting for streams to complete...');
195
+ // Wait for execution completion in parallel with stream consumption. The old
196
+ // flow waited for stream EOF first and only then started polling for the
197
+ // final exit code, which adds avoidable tail latency now that create returns
198
+ // an execution ID immediately for oneshot sandboxes.
199
+ let finalExecution:
200
+ | {
201
+ exitCode?: number;
202
+ status: string;
203
+ }
204
+ | undefined;
205
+ if (createResponse.executionId) {
206
+ logger?.debug(
207
+ 'waiting for execution %s and %d stream(s) in parallel',
208
+ createResponse.executionId,
209
+ streamPromises.length
210
+ );
211
+ const completionPromise = waitForRunCompletion(
212
+ client,
213
+ sandboxId,
214
+ createResponse.executionId,
215
+ orgId,
216
+ signal,
217
+ logger,
218
+ started
219
+ );
193
220
 
194
- if (streamPromises.length > 0) {
195
- if (signal) {
196
- // Race streams against abort signal, cleaning up the listener
197
- // in all cases so an orphaned reject cannot fire after settlement.
198
- let onAbort: (() => void) | undefined;
199
- try {
200
- await Promise.race([
201
- Promise.allSettled(streamPromises),
202
- new Promise<never>((_, reject) => {
203
- onAbort = () => {
204
- abortController.abort();
205
- reject(
206
- new ExecutionCancelledError({
207
- message: 'Sandbox execution cancelled',
208
- sandboxId,
209
- })
210
- );
211
- };
212
- if (signal.aborted) {
213
- onAbort();
214
- } else {
215
- signal.addEventListener('abort', onAbort, { once: true });
216
- }
217
- }),
218
- ]);
219
- } finally {
220
- if (onAbort && signal) {
221
- signal.removeEventListener('abort', onAbort);
222
- }
223
- }
224
- } else {
225
- await Promise.allSettled(streamPromises);
226
- }
221
+ finalExecution = signal
222
+ ? await raceWithAbort(completionPromise, signal, abortController, sandboxId)
223
+ : await completionPromise;
224
+ await waitForStreamsToDrain(streamPromises, signal, abortController, sandboxId);
227
225
  } else {
228
- // No streams available (shouldn't happen for oneshot, but handle defensively).
229
- // Fall back to a single wait then check.
230
- logger?.debug('no streams to wait on, checking sandbox status directly');
226
+ logger?.debug(
227
+ 'missing executionId on create response, falling back to stream-first completion'
228
+ );
229
+ await waitForStreamsToDrain(streamPromises, signal, abortController, sandboxId);
231
230
  }
232
231
 
233
232
  if (timingLogsEnabled)
234
- console.error(`[TIMING] +${Date.now() - started}ms: all streams done, fetching exit code`);
235
- logger?.debug('streams completed, fetching final status');
233
+ console.error(`[TIMING] +${Date.now() - started}ms: completion wait finished`);
234
+ logger?.debug('completion wait finished, resolving final exit code');
236
235
 
237
236
  // Stream EOF means the sandbox is done — hadron only closes streams after the
238
237
  // container exits. Poll for the exit code with retries because the lifecycle
@@ -245,94 +244,108 @@ export async function sandboxRun(
245
244
  // linear 1s polling interval (not exponential backoff) so we don't overshoot
246
245
  // the window — 15 attempts × 1s = 15s total, which comfortably covers the
247
246
  // drain + lifecycle propagation delay.
248
- // Abort-aware sleep that rejects when the caller's signal fires.
249
- const abortAwareSleep = (ms: number): Promise<void> =>
250
- new Promise((resolve, reject) => {
251
- if (signal?.aborted) {
252
- reject(new DOMException('Aborted', 'AbortError'));
253
- return;
254
- }
255
- const timer = setTimeout(resolve, ms);
256
- signal?.addEventListener(
257
- 'abort',
258
- () => {
259
- clearTimeout(timer);
260
- reject(new DOMException('Aborted', 'AbortError'));
261
- },
262
- { once: true }
263
- );
264
- });
265
-
266
- let exitCode = 0;
267
- const maxStatusRetries = 15;
268
- const statusPollInterval = 1000;
247
+ let exitCode = finalExecution?.exitCode ?? 0;
269
248
  const statusPollStart = Date.now();
270
- for (let attempt = 0; attempt < maxStatusRetries; attempt++) {
271
- if (signal?.aborted) {
272
- break;
249
+ let shouldWaitForSandboxStatus = finalExecution?.exitCode == null;
250
+ let sandboxStatusReconciled = false;
251
+ if (finalExecution?.exitCode == null) {
252
+ if (createResponse.executionId && finalExecution?.status === 'completed') {
253
+ try {
254
+ const execution = await executionGet(client, {
255
+ executionId: createResponse.executionId,
256
+ orgId,
257
+ wait: EXIT_CODE_FAST_WAIT_DURATION,
258
+ signal,
259
+ });
260
+ if (execution.exitCode != null) {
261
+ exitCode = execution.exitCode;
262
+ finalExecution.exitCode = execution.exitCode;
263
+ shouldWaitForSandboxStatus = false;
264
+ logger?.debug(
265
+ '[run] exit code %d found from fast execution retry (+%dms)',
266
+ exitCode,
267
+ Date.now() - statusPollStart
268
+ );
269
+ }
270
+ } catch (err) {
271
+ if (!(err instanceof DOMException && err.name === 'AbortError')) {
272
+ logger?.debug(
273
+ '[run] fast execution exit code retry failed (+%dms): %s',
274
+ Date.now() - statusPollStart,
275
+ err
276
+ );
277
+ }
278
+ }
273
279
  }
280
+ }
281
+ if (shouldWaitForSandboxStatus) {
274
282
  try {
275
- const sandboxStatus = await sandboxGetStatus(client, { sandboxId, orgId });
283
+ const sandboxStatus = await sandboxGetStatus(client, {
284
+ sandboxId,
285
+ orgId,
286
+ waitForStatus: ['terminated', 'failed'],
287
+ waitMs: 15000,
288
+ });
276
289
  if (sandboxStatus.exitCode != null) {
277
290
  exitCode = sandboxStatus.exitCode;
291
+ sandboxStatusReconciled = true;
278
292
  logger?.debug(
279
- '[run] exit code %d found on attempt %d/%d (+%dms)',
293
+ '[run] exit code %d found after server-side wait (+%dms)',
280
294
  exitCode,
281
- attempt + 1,
282
- maxStatusRetries,
283
295
  Date.now() - statusPollStart
284
296
  );
285
- break;
286
297
  } else if (sandboxStatus.status === 'failed') {
287
298
  exitCode = 1;
299
+ sandboxStatusReconciled = true;
288
300
  logger?.debug(
289
- '[run] sandbox failed on attempt %d/%d (+%dms)',
290
- attempt + 1,
291
- maxStatusRetries,
301
+ '[run] sandbox failed after server-side wait (+%dms)',
292
302
  Date.now() - statusPollStart
293
303
  );
294
- break;
295
304
  } else if (sandboxStatus.status === 'terminated') {
296
- // Sandbox was destroyed. If exit code is missing, the
297
- // terminated event may have overwritten it. Stop polling —
298
- // no further updates will come.
305
+ sandboxStatusReconciled = true;
299
306
  logger?.debug(
300
- '[run] sandbox terminated without exit code on attempt %d/%d (+%dms)',
301
- attempt + 1,
302
- maxStatusRetries,
307
+ '[run] sandbox terminated without exit code after server-side wait (+%dms)',
308
+ Date.now() - statusPollStart
309
+ );
310
+ } else {
311
+ logger?.debug(
312
+ '[run] sandbox status wait expired with status=%s (+%dms)',
313
+ sandboxStatus.status,
303
314
  Date.now() - statusPollStart
304
315
  );
305
- break;
306
- }
307
- // Exit code not yet propagated — wait before next poll.
308
- if (attempt < maxStatusRetries - 1) {
309
- await abortAwareSleep(statusPollInterval);
310
316
  }
311
317
  } catch (err) {
312
- if (err instanceof DOMException && err.name === 'AbortError') {
313
- break;
314
- }
315
- // Transient failure (sandbox briefly unavailable, network error).
316
- // Retry instead of giving up — the lifecycle event may still arrive.
317
- logger?.debug(
318
- '[run] sandboxGetStatus attempt %d/%d failed (+%dms): %s',
319
- attempt + 1,
320
- maxStatusRetries,
321
- Date.now() - statusPollStart,
322
- err
323
- );
324
- if (attempt < maxStatusRetries - 1) {
325
- await abortAwareSleep(statusPollInterval);
318
+ if (!(err instanceof DOMException && err.name === 'AbortError')) {
319
+ logger?.debug(
320
+ '[run] sandboxGetStatus server-side wait failed (+%dms): %s',
321
+ Date.now() - statusPollStart,
322
+ err
323
+ );
326
324
  }
327
325
  }
328
326
  }
329
- if (exitCode === 0) {
327
+ if (
328
+ finalExecution &&
329
+ finalExecution?.exitCode == null &&
330
+ finalExecution?.status !== 'completed' &&
331
+ !sandboxStatusReconciled
332
+ ) {
333
+ exitCode = 1;
330
334
  logger?.debug(
331
- '[run] exit code polling finished with default 0 after %d attempts (+%dms)',
332
- maxStatusRetries,
333
- Date.now() - statusPollStart
335
+ '[run] using fallback exit code 1 for terminal status=%s after sandbox status reconciliation failed',
336
+ finalExecution?.status
334
337
  );
335
338
  }
339
+ if (exitCode === 0) {
340
+ if (finalExecution?.exitCode != null) {
341
+ logger?.debug('[run] using execution exit code 0 from long-poll result');
342
+ } else {
343
+ logger?.debug(
344
+ '[run] exit code wait finished with default 0 (+%dms)',
345
+ Date.now() - statusPollStart
346
+ );
347
+ }
348
+ }
336
349
 
337
350
  if (timingLogsEnabled)
338
351
  console.error(
@@ -363,6 +376,223 @@ export async function sandboxRun(
363
376
  }
364
377
  }
365
378
 
379
+ async function waitForRunCompletion(
380
+ client: APIClient,
381
+ sandboxId: string,
382
+ executionId: string,
383
+ orgId: string | undefined,
384
+ signal: AbortSignal | undefined,
385
+ logger: Logger | undefined,
386
+ started: number
387
+ ): Promise<{ exitCode?: number; status: string }> {
388
+ const completionAbortController = new AbortController();
389
+ let onAbort: (() => void) | undefined;
390
+ if (signal) {
391
+ onAbort = () => completionAbortController.abort(signal.reason);
392
+ if (signal.aborted) {
393
+ onAbort();
394
+ } else {
395
+ signal.addEventListener('abort', onAbort, { once: true });
396
+ }
397
+ }
398
+
399
+ try {
400
+ const completionSignal = completionAbortController.signal;
401
+ const executionPromise = waitForExecutionCompletion(
402
+ client,
403
+ executionId,
404
+ orgId,
405
+ completionSignal,
406
+ logger,
407
+ started
408
+ );
409
+ const statusPromise = waitForSandboxStatusCompletion(
410
+ client,
411
+ sandboxId,
412
+ orgId,
413
+ completionSignal,
414
+ logger,
415
+ started
416
+ ).catch((err) => {
417
+ if (completionSignal.aborted) {
418
+ throw err;
419
+ }
420
+ logger?.debug('[run] sandbox status completion wait failed: %s', err);
421
+ return new Promise<never>(() => {});
422
+ });
423
+
424
+ const result = await Promise.race([executionPromise, statusPromise]);
425
+ return result;
426
+ } finally {
427
+ if (onAbort && signal) {
428
+ signal.removeEventListener('abort', onAbort);
429
+ }
430
+ }
431
+ }
432
+
433
+ async function waitForExecutionCompletion(
434
+ client: APIClient,
435
+ executionId: string,
436
+ orgId: string | undefined,
437
+ signal: AbortSignal | undefined,
438
+ logger: Logger | undefined,
439
+ started: number
440
+ ): Promise<{ exitCode?: number; status: string }> {
441
+ while (true) {
442
+ if (signal?.aborted) {
443
+ throw new DOMException('Aborted', 'AbortError');
444
+ }
445
+
446
+ const result = await executionGet(client, {
447
+ executionId,
448
+ orgId,
449
+ wait: EXECUTION_WAIT_DURATION,
450
+ signal,
451
+ });
452
+ logger?.debug(
453
+ '[run] execution wait: id=%s status=%s exit=%s +%dms',
454
+ executionId,
455
+ result.status,
456
+ result.exitCode ?? 'undefined',
457
+ Date.now() - started
458
+ );
459
+
460
+ if (TERMINAL_EXECUTION_STATUSES.has(result.status)) {
461
+ return {
462
+ exitCode: result.exitCode,
463
+ status: result.status,
464
+ };
465
+ }
466
+ }
467
+ }
468
+
469
+ async function waitForSandboxStatusCompletion(
470
+ client: APIClient,
471
+ sandboxId: string,
472
+ orgId: string | undefined,
473
+ signal: AbortSignal | undefined,
474
+ logger: Logger | undefined,
475
+ started: number
476
+ ): Promise<{ exitCode?: number; status: string }> {
477
+ while (true) {
478
+ if (signal?.aborted) {
479
+ throw new DOMException('Aborted', 'AbortError');
480
+ }
481
+
482
+ const result = await sandboxGetStatus(client, {
483
+ sandboxId,
484
+ orgId,
485
+ waitForStatus: ['idle', 'terminated', 'failed'],
486
+ waitMs: 300000,
487
+ signal,
488
+ });
489
+ logger?.debug(
490
+ '[run] sandbox status wait: sandbox=%s status=%s exit=%s +%dms',
491
+ sandboxId,
492
+ result.status,
493
+ result.exitCode ?? 'undefined',
494
+ Date.now() - started
495
+ );
496
+
497
+ if (result.exitCode != null) {
498
+ return {
499
+ exitCode: result.exitCode,
500
+ status: 'completed',
501
+ };
502
+ }
503
+ if (result.status === 'failed') {
504
+ return {
505
+ exitCode: 1,
506
+ status: 'failed',
507
+ };
508
+ }
509
+ if (result.status === 'terminated') {
510
+ return {
511
+ status: 'completed',
512
+ };
513
+ }
514
+
515
+ await new Promise((resolve) => setTimeout(resolve, 25));
516
+ }
517
+ }
518
+
519
+ async function waitForStreamsToDrain(
520
+ streamPromises: Promise<void>[],
521
+ signal: AbortSignal | undefined,
522
+ abortController: AbortController,
523
+ sandboxId: string
524
+ ): Promise<void> {
525
+ if (streamPromises.length === 0) {
526
+ return;
527
+ }
528
+
529
+ if (signal) {
530
+ let onAbort: (() => void) | undefined;
531
+ try {
532
+ await Promise.race([
533
+ Promise.allSettled(streamPromises).then(() => undefined),
534
+ new Promise<never>((_, reject) => {
535
+ onAbort = () => {
536
+ abortController.abort();
537
+ reject(
538
+ new ExecutionCancelledError({
539
+ message: 'Sandbox execution cancelled',
540
+ sandboxId,
541
+ })
542
+ );
543
+ };
544
+ if (signal.aborted) {
545
+ onAbort();
546
+ } else {
547
+ signal.addEventListener('abort', onAbort, { once: true });
548
+ }
549
+ }),
550
+ ]);
551
+ } finally {
552
+ if (onAbort) {
553
+ signal.removeEventListener('abort', onAbort);
554
+ }
555
+ }
556
+ return;
557
+ }
558
+
559
+ await Promise.allSettled(streamPromises);
560
+ }
561
+
562
+ async function raceWithAbort<T>(
563
+ promise: Promise<T>,
564
+ signal: AbortSignal,
565
+ abortController: AbortController,
566
+ sandboxId: string
567
+ ): Promise<T> {
568
+ let onAbort: (() => void) | undefined;
569
+ try {
570
+ return await Promise.race([
571
+ promise,
572
+ new Promise<never>((_, reject) => {
573
+ onAbort = () => {
574
+ abortController.abort();
575
+ reject(
576
+ new ExecutionCancelledError({
577
+ message: 'Sandbox execution cancelled',
578
+ sandboxId,
579
+ })
580
+ );
581
+ };
582
+ if (signal.aborted) {
583
+ onAbort();
584
+ } else {
585
+ signal.addEventListener('abort', onAbort, { once: true });
586
+ }
587
+ }),
588
+ ]);
589
+ } finally {
590
+ if (onAbort) {
591
+ signal.removeEventListener('abort', onAbort);
592
+ }
593
+ }
594
+ }
595
+
366
596
  async function createStdinStream(
367
597
  region: string,
368
598
  apiKey: string,
@@ -537,6 +767,12 @@ async function streamUrlToWritable(
537
767
  // Signal end-of-stream to the tee/pipe chain so downstream
538
768
  // consumers (e.g. process.stdout pipe) know no more data is coming.
539
769
  writable.end();
770
+ if ('once' in writable) {
771
+ await finished(writable as NodeJS.WritableStream).catch(() => {
772
+ // Ignore finish errors here; the main read/write path already
773
+ // reported meaningful stream errors.
774
+ });
775
+ }
540
776
  } catch (err) {
541
777
  if (err instanceof Error && err.name === 'AbortError') {
542
778
  logger?.debug('[stream] aborted after %dms', Date.now() - streamStart);
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { StructuredError } from '../../error.ts';
3
3
  import { SortDirectionSchema } from '../pagination.ts';
4
+ import type { SandboxPauseResult } from './pause.ts';
4
5
 
5
6
  /**
6
7
  * Resource limits for a sandbox using Kubernetes-style units
@@ -274,6 +275,13 @@ export const SandboxTimeoutConfigSchema = z.object({
274
275
  .string()
275
276
  .optional()
276
277
  .describe('Maximum execution time per command (e.g., "5m", "1h")'),
278
+ /** Maximum duration a sandbox can remain paused before termination (e.g., "24h", "0" for infinite) */
279
+ paused: z
280
+ .string()
281
+ .optional()
282
+ .describe(
283
+ 'Maximum duration a sandbox can remain paused before termination (e.g., "24h", "0" for infinite)'
284
+ ),
277
285
  });
278
286
  export type SandboxTimeoutConfig = z.infer<typeof SandboxTimeoutConfigSchema>;
279
287
 
@@ -399,7 +407,7 @@ export const SandboxSchema = z.object({
399
407
  .describe('Set environment variables on the sandbox. Pass null to delete a variable.'),
400
408
  /** Pause the sandbox, creating a checkpoint of its current state. */
401
409
  pause: z
402
- .custom<() => Promise<void>>()
410
+ .custom<() => Promise<SandboxPauseResult>>()
403
411
  .describe('Pause the sandbox, creating a checkpoint of its current state.'),
404
412
  /** Resume the sandbox from a paused or evacuated state. */
405
413
  resume: z
@@ -592,6 +600,11 @@ export const SandboxInfoSchema = z.object({
592
600
  idle: z.string().optional().describe('Idle timeout duration (e.g., "10m0s")'),
593
601
  /** Execution timeout duration (e.g., "5m0s") */
594
602
  execution: z.string().optional().describe('Execution timeout duration (e.g., "5m0s")'),
603
+ /** Paused timeout duration (e.g., "24h0s", "0s" for infinite) */
604
+ paused: z
605
+ .string()
606
+ .optional()
607
+ .describe('Paused timeout duration (e.g., "24h0s", "0s" for infinite)'),
595
608
  })
596
609
  .optional()
597
610
  .describe('Timeout configuration for this sandbox'),
@@ -830,6 +843,8 @@ export const SnapshotCreateOptionsSchema = z.object({
830
843
  tag: z.string().optional().describe('Tag for the snapshot (defaults to "latest")'),
831
844
  /** Make the snapshot publicly accessible */
832
845
  public: z.boolean().optional().describe('Make the snapshot publicly accessible'),
846
+ /** Organization ID to use for CLI-authenticated requests */
847
+ orgId: z.string().optional().describe('Organization ID for CLI-authenticated requests'),
833
848
  });
834
849
  export type SnapshotCreateOptions = z.infer<typeof SnapshotCreateOptionsSchema>;
835
850
 
@@ -905,7 +920,7 @@ export interface SandboxService {
905
920
  list(params?: ListSandboxesParams): Promise<ListSandboxesResponse>;
906
921
  destroy(sandboxId: string): Promise<void>;
907
922
  /** Pause a running sandbox, creating a checkpoint of its current state. */
908
- pause(sandboxId: string): Promise<void>;
923
+ pause(sandboxId: string): Promise<SandboxPauseResult>;
909
924
  /** Resume a paused or evacuated sandbox from its checkpoint. */
910
925
  resume(sandboxId: string): Promise<void>;
911
926
  snapshot: SnapshotService;
@@ -72,8 +72,6 @@ type SessionGetResponse = z.infer<typeof SessionGetResponseSchema>;
72
72
  * @returns
73
73
  */
74
74
  export type SessionEvalRun = z.infer<typeof SessionEvalRunSchema>;
75
- /** @deprecated Use SessionEvalRun instead */
76
- export type EvalRun = SessionEvalRun;
77
75
  export type RouteInfo = z.infer<typeof RouteInfoSchema>;
78
76
  export type AgentInfo = z.infer<typeof AgentInfoSchema>;
79
77
  export type EnrichedSession = {
@@ -40,11 +40,11 @@ export const StreamNamespaceEntrySchema = z.object({
40
40
  chunks: z.number().describe('number of chunks'),
41
41
  completed: z.boolean().describe('whether the stream upload is completed'),
42
42
  size_bytes: z.number().describe('size in bytes'),
43
- started_at: z.string().nullable().describe('ISO 8601 stream start timestamp'),
43
+ started_at: z.string().nullable().optional().describe('ISO 8601 stream start timestamp'),
44
44
  ended_at: z.string().nullable().describe('ISO 8601 stream end timestamp'),
45
45
  headers: z.record(z.string(), z.string()).nullable().optional().describe('stream headers'),
46
46
  metadata: z.record(z.string(), z.string()).nullable().optional().describe('stream metadata'),
47
- expires_at: z.string().nullable().describe('ISO 8601 expiration timestamp or null'),
47
+ expires_at: z.string().nullable().optional().describe('ISO 8601 expiration timestamp or null'),
48
48
  url: z.string().describe('public URL to access the stream'),
49
49
  });
50
50