@agentuity/core 1.0.59 → 2.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 (44) hide show
  1. package/dist/deprecation.d.ts +20 -0
  2. package/dist/deprecation.d.ts.map +1 -0
  3. package/dist/deprecation.js +102 -0
  4. package/dist/deprecation.js.map +1 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/services/oauth/flow.d.ts +0 -31
  10. package/dist/services/oauth/flow.d.ts.map +1 -1
  11. package/dist/services/oauth/flow.js +13 -138
  12. package/dist/services/oauth/flow.js.map +1 -1
  13. package/dist/services/oauth/index.d.ts +0 -1
  14. package/dist/services/oauth/index.d.ts.map +1 -1
  15. package/dist/services/oauth/index.js +0 -1
  16. package/dist/services/oauth/index.js.map +1 -1
  17. package/dist/services/oauth/types.d.ts +0 -11
  18. package/dist/services/oauth/types.d.ts.map +1 -1
  19. package/dist/services/oauth/types.js +0 -19
  20. package/dist/services/oauth/types.js.map +1 -1
  21. package/dist/services/sandbox/execute.d.ts.map +1 -1
  22. package/dist/services/sandbox/execute.js +11 -22
  23. package/dist/services/sandbox/execute.js.map +1 -1
  24. package/dist/services/sandbox/run.d.ts.map +1 -1
  25. package/dist/services/sandbox/run.js +30 -83
  26. package/dist/services/sandbox/run.js.map +1 -1
  27. package/dist/services/sandbox/types.d.ts +0 -8
  28. package/dist/services/sandbox/types.d.ts.map +1 -1
  29. package/dist/services/sandbox/types.js +0 -14
  30. package/dist/services/sandbox/types.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/deprecation.ts +120 -0
  33. package/src/index.ts +3 -0
  34. package/src/services/oauth/flow.ts +15 -156
  35. package/src/services/oauth/index.ts +0 -1
  36. package/src/services/oauth/types.ts +0 -26
  37. package/src/services/sandbox/execute.ts +12 -26
  38. package/src/services/sandbox/run.ts +34 -129
  39. package/src/services/sandbox/types.ts +0 -14
  40. package/dist/services/oauth/token-storage.d.ts +0 -109
  41. package/dist/services/oauth/token-storage.d.ts.map +0 -1
  42. package/dist/services/oauth/token-storage.js +0 -140
  43. package/dist/services/oauth/token-storage.js.map +0 -1
  44. package/src/services/oauth/token-storage.ts +0 -220
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Deprecation warning for v1 SDK packages.
3
+ *
4
+ * This module logs a deprecation warning when v1 packages are used,
5
+ * recommending migration to v2.
6
+ *
7
+ * The warning is only shown once per process to avoid noise.
8
+ */
9
+
10
+ import type { Logger } from './logger';
11
+
12
+ let deprecationWarningShown = false;
13
+
14
+ /**
15
+ * Package version info
16
+ */
17
+ interface PackageVersion {
18
+ version: string;
19
+ major: number;
20
+ }
21
+
22
+ /**
23
+ * Get version from package.json using various strategies.
24
+ */
25
+ function getPackageVersion(): PackageVersion | null {
26
+ try {
27
+ // Try to read from the bundled package.json
28
+ // When bundled by Bun/Vite, import.meta.resolve can find the package.json
29
+ const pkgUrl = import.meta.resolve('@agentuity/core/package.json');
30
+ const pkg = require(pkgUrl);
31
+ if (pkg?.version) {
32
+ const match = pkg.version.match(/^(\d+)\.\d+\.\d+/);
33
+ return {
34
+ version: pkg.version,
35
+ major: match ? parseInt(match[1], 10) : 0,
36
+ };
37
+ }
38
+ } catch {
39
+ // Try require fallback
40
+ try {
41
+ const pkg = require('@agentuity/core/package.json');
42
+ if (pkg?.version) {
43
+ const match = pkg.version.match(/^(\d+)\.\d+\.\d+/);
44
+ return {
45
+ version: pkg.version,
46
+ major: match ? parseInt(match[1], 10) : 0,
47
+ };
48
+ }
49
+ } catch {
50
+ // Ignore
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * Show deprecation warning for v1 packages.
58
+ *
59
+ * @param logger - Optional logger instance (falls back to console)
60
+ */
61
+ export function showDeprecationWarning(logger?: Logger): void {
62
+ // Only show once per process
63
+ if (deprecationWarningShown) {
64
+ return;
65
+ }
66
+
67
+ // Skip if explicitly disabled
68
+ if (process.env.AGENTUITY_NO_DEPRECATION_WARNING === 'true') {
69
+ return;
70
+ }
71
+
72
+ // Skip in test environments
73
+ if (process.env.NODE_ENV === 'test' || process.env.VITEST === 'true') {
74
+ return;
75
+ }
76
+
77
+ const pkgVersion = getPackageVersion();
78
+
79
+ // Only warn if this is a v1 package
80
+ if (!pkgVersion || pkgVersion.major !== 1) {
81
+ return;
82
+ }
83
+
84
+ deprecationWarningShown = true;
85
+
86
+ const message =
87
+ '\n' +
88
+ '┌──────────────────────────────────────────────────────────────────────┐\n' +
89
+ '│ │\n' +
90
+ '│ ⚠️ Agentuity SDK v1 is deprecated │\n' +
91
+ '│ │\n' +
92
+ '│ You are using @agentuity/core@' +
93
+ pkgVersion.version.padEnd(22) +
94
+ ' │\n' +
95
+ '│ │\n' +
96
+ '│ v2 introduces major improvements: │\n' +
97
+ '│ • Hono RPC for end-to-end type safety │\n' +
98
+ '│ • Vite-native dev server with HMR │\n' +
99
+ '│ • Simplified configuration in createApp() │\n' +
100
+ '│ │\n' +
101
+ '│ → Run `npx @agentuity/migrate` to upgrade your project │\n' +
102
+ '│ │\n' +
103
+ '│ Docs: https://docs.agentuity.com/migration/v1-to-v2 │\n' +
104
+ '│ │\n' +
105
+ '└──────────────────────────────────────────────────────────────────────┘\n';
106
+
107
+ if (logger) {
108
+ logger.warn(message);
109
+ } else {
110
+ console.warn(message);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Check if the current package is v1.
116
+ */
117
+ export function isV1Package(): boolean {
118
+ const pkgVersion = getPackageVersion();
119
+ return pkgVersion?.major === 1;
120
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,9 @@
2
2
  export type { EnvField, ResourceType } from './env-example.ts';
3
3
  export { detectResourceFromKey, parseEnvExample } from './env-example.ts';
4
4
 
5
+ // deprecation.ts exports
6
+ export { isV1Package, showDeprecationWarning } from './deprecation';
7
+
5
8
  // error.ts exports
6
9
  export { isStructuredError, RichError, StructuredError } from './error.ts';
7
10
 
@@ -25,28 +25,11 @@ function resolveConfig(config?: OAuthFlowConfig) {
25
25
  config?.userinfoUrl ??
26
26
  getEnv('OAUTH_USERINFO_URL') ??
27
27
  (issuer ? `${issuer}/userinfo` : undefined);
28
- const revokeUrl =
29
- config?.revokeUrl ?? getEnv('OAUTH_REVOKE_URL') ?? (issuer ? `${issuer}/revoke` : undefined);
30
- const endSessionUrl =
31
- config?.endSessionUrl ??
32
- getEnv('OAUTH_END_SESSION_URL') ??
33
- (issuer ? `${issuer}/end_session` : undefined);
34
28
  const scopes = config?.scopes ?? getEnv('OAUTH_SCOPES') ?? 'openid profile email';
35
29
 
36
30
  const prompt = config?.prompt;
37
31
 
38
- return {
39
- clientId,
40
- clientSecret,
41
- issuer,
42
- authorizeUrl,
43
- tokenUrl,
44
- userinfoUrl,
45
- revokeUrl,
46
- endSessionUrl,
47
- scopes,
48
- prompt,
49
- };
32
+ return { clientId, clientSecret, issuer, authorizeUrl, tokenUrl, userinfoUrl, scopes, prompt };
50
33
  }
51
34
 
52
35
  /**
@@ -135,6 +118,9 @@ export async function exchangeToken(
135
118
  });
136
119
  }
137
120
 
121
+ const controller = new AbortController();
122
+ const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
123
+
138
124
  let response: Response;
139
125
  try {
140
126
  response = await fetch(resolved.tokenUrl, {
@@ -147,16 +133,18 @@ export async function exchangeToken(
147
133
  client_id: resolved.clientId,
148
134
  client_secret: resolved.clientSecret,
149
135
  }),
150
- signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
136
+ signal: controller.signal,
151
137
  });
152
138
  } catch (err) {
153
- if (err instanceof DOMException && err.name === 'TimeoutError') {
139
+ clearTimeout(timer);
140
+ if (err instanceof DOMException && err.name === 'AbortError') {
154
141
  throw new OAuthResponseError({
155
142
  message: `Token exchange timed out after ${DEFAULT_TIMEOUT_MS}ms`,
156
143
  });
157
144
  }
158
145
  throw err;
159
146
  }
147
+ clearTimeout(timer);
160
148
 
161
149
  if (!response.ok) {
162
150
  const error = await response.text();
@@ -169,140 +157,6 @@ export async function exchangeToken(
169
157
  return OAuthTokenResponseSchema.parse(data);
170
158
  }
171
159
 
172
- /**
173
- * Refresh an access token using a refresh token.
174
- *
175
- * @param refreshTokenValue - The refresh token obtained from a previous token exchange
176
- * @param config - Optional OAuth configuration. Falls back to environment variables.
177
- * @returns The token response including a new access_token, and optionally a new refresh_token
178
- *
179
- * @example
180
- * ```typescript
181
- * const newToken = await refreshToken(previousToken.refresh_token!);
182
- * console.log(newToken.access_token);
183
- * ```
184
- */
185
- export async function refreshToken(
186
- refreshTokenValue: string,
187
- config?: OAuthFlowConfig
188
- ): Promise<OAuthTokenResponse> {
189
- const resolved = resolveConfig(config);
190
-
191
- if (!resolved.tokenUrl) {
192
- throw new OAuthResponseError({
193
- message:
194
- 'No token URL configured. Set OAUTH_TOKEN_URL or OAUTH_ISSUER environment variable.',
195
- });
196
- }
197
- if (!resolved.clientId) {
198
- throw new OAuthResponseError({
199
- message: 'No client ID configured. Set OAUTH_CLIENT_ID environment variable.',
200
- });
201
- }
202
-
203
- const params = new URLSearchParams({
204
- grant_type: 'refresh_token',
205
- refresh_token: refreshTokenValue,
206
- client_id: resolved.clientId,
207
- });
208
-
209
- if (resolved.clientSecret) {
210
- params.set('client_secret', resolved.clientSecret);
211
- }
212
-
213
- let response: Response;
214
- try {
215
- response = await fetch(resolved.tokenUrl, {
216
- method: 'POST',
217
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
218
- body: params,
219
- signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
220
- });
221
- } catch (err) {
222
- if (err instanceof DOMException && err.name === 'TimeoutError') {
223
- throw new OAuthResponseError({
224
- message: `Token refresh timed out after ${DEFAULT_TIMEOUT_MS}ms`,
225
- });
226
- }
227
- throw err;
228
- }
229
-
230
- if (!response.ok) {
231
- const error = await response.text();
232
- throw new OAuthResponseError({
233
- message: `Token refresh failed (${response.status}): ${error}`,
234
- });
235
- }
236
-
237
- const data = await response.json();
238
- return OAuthTokenResponseSchema.parse(data);
239
- }
240
-
241
- /**
242
- * Revoke an OAuth token (access token or refresh token) to log the user out.
243
- *
244
- * Calls the token revocation endpoint (RFC 7009). The server will invalidate
245
- * the token so it can no longer be used. Per the spec, the endpoint returns
246
- * a success response even if the token was already invalid.
247
- *
248
- * @param token - The access token or refresh token to revoke
249
- * @param config - Optional OAuth configuration. Falls back to environment variables.
250
- *
251
- * @example
252
- * ```typescript
253
- * // Revoke the refresh token to fully log out
254
- * await logout(token.refresh_token!);
255
- * ```
256
- */
257
- export async function logout(token: string, config?: OAuthFlowConfig): Promise<void> {
258
- const resolved = resolveConfig(config);
259
-
260
- if (!resolved.revokeUrl) {
261
- throw new OAuthResponseError({
262
- message:
263
- 'No revoke URL configured. Set OAUTH_REVOKE_URL or OAUTH_ISSUER environment variable.',
264
- });
265
- }
266
- if (!resolved.clientId) {
267
- throw new OAuthResponseError({
268
- message: 'No client ID configured. Set OAUTH_CLIENT_ID environment variable.',
269
- });
270
- }
271
-
272
- const params = new URLSearchParams({
273
- token,
274
- client_id: resolved.clientId,
275
- });
276
-
277
- if (resolved.clientSecret) {
278
- params.set('client_secret', resolved.clientSecret);
279
- }
280
-
281
- let response: Response;
282
- try {
283
- response = await fetch(resolved.revokeUrl, {
284
- method: 'POST',
285
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
286
- body: params,
287
- signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
288
- });
289
- } catch (err) {
290
- if (err instanceof DOMException && err.name === 'TimeoutError') {
291
- throw new OAuthResponseError({
292
- message: `Token revocation timed out after ${DEFAULT_TIMEOUT_MS}ms`,
293
- });
294
- }
295
- throw err;
296
- }
297
-
298
- if (!response.ok) {
299
- const error = await response.text();
300
- throw new OAuthResponseError({
301
- message: `Token revocation failed (${response.status}): ${error}`,
302
- });
303
- }
304
- }
305
-
306
160
  /**
307
161
  * Fetch user information from the OIDC userinfo endpoint using an access token.
308
162
  *
@@ -329,20 +183,25 @@ export async function fetchUserInfo(
329
183
  });
330
184
  }
331
185
 
186
+ const controller = new AbortController();
187
+ const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
188
+
332
189
  let response: Response;
333
190
  try {
334
191
  response = await fetch(resolved.userinfoUrl, {
335
192
  headers: { Authorization: `Bearer ${accessToken}` },
336
- signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
193
+ signal: controller.signal,
337
194
  });
338
195
  } catch (err) {
339
- if (err instanceof DOMException && err.name === 'TimeoutError') {
196
+ clearTimeout(timer);
197
+ if (err instanceof DOMException && err.name === 'AbortError') {
340
198
  throw new OAuthResponseError({
341
199
  message: `Userinfo request timed out after ${DEFAULT_TIMEOUT_MS}ms`,
342
200
  });
343
201
  }
344
202
  throw err;
345
203
  }
204
+ clearTimeout(timer);
346
205
 
347
206
  if (!response.ok) {
348
207
  const error = await response.text();
@@ -7,4 +7,3 @@ export * from './members.ts';
7
7
  export * from './keys.ts';
8
8
  export * from './util.ts';
9
9
  export * from './flow.ts';
10
- export * from './token-storage.ts';
@@ -268,18 +268,6 @@ export const OAuthFlowConfigSchema = z.object({
268
268
  .string()
269
269
  .optional()
270
270
  .describe('UserInfo endpoint. Defaults to OAUTH_USERINFO_URL or {issuer}/userinfo'),
271
- revokeUrl: z
272
- .string()
273
- .optional()
274
- .describe(
275
- 'Token revocation endpoint (RFC 7009). Defaults to OAUTH_REVOKE_URL or {issuer}/revoke'
276
- ),
277
- endSessionUrl: z
278
- .string()
279
- .optional()
280
- .describe(
281
- 'OIDC end session endpoint. Defaults to OAUTH_END_SESSION_URL or {issuer}/end_session'
282
- ),
283
271
  scopes: z
284
272
  .string()
285
273
  .optional()
@@ -317,17 +305,3 @@ export const OAuthUserInfoSchema = z
317
305
  .catchall(z.unknown());
318
306
 
319
307
  export type OAuthUserInfo = z.infer<typeof OAuthUserInfoSchema>;
320
-
321
- export const StoredTokenSchema = z.object({
322
- access_token: z.string(),
323
- token_type: z.string().optional(),
324
- refresh_token: z.string().optional(),
325
- scope: z.string().optional(),
326
- id_token: z.string().optional(),
327
- expires_at: z
328
- .number()
329
- .optional()
330
- .describe('Unix timestamp (seconds) when the access token expires'),
331
- });
332
-
333
- export type StoredToken = z.infer<typeof StoredTokenSchema>;
@@ -1,7 +1,7 @@
1
1
  import type { ExecuteOptions, Execution, ExecutionStatus } from './types.ts';
2
2
  import { z } from 'zod';
3
3
  import type { APIClient } from '../api.ts';
4
- import { SandboxBusyError, SandboxNotFoundError, throwSandboxError } from './util.ts';
4
+ import { SandboxBusyError, throwSandboxError } from './util.ts';
5
5
 
6
6
  export const ExecuteRequestSchema = z
7
7
  .object({
@@ -114,37 +114,23 @@ export async function sandboxExecute(
114
114
  signal ?? options.signal
115
115
  );
116
116
  } catch (error: unknown) {
117
+ // Detect 409 Conflict (sandbox busy) and throw a specific error.
118
+ // The sandbox API client is configured with maxRetries: 0 to fail fast
119
+ // when sandbox is busy (retrying wouldn't help - sandbox is still busy).
120
+ // Convert APIErrorResponse with status 409 to SandboxBusyError for clarity.
117
121
  if (
118
122
  error &&
119
123
  typeof error === 'object' &&
120
124
  '_tag' in error &&
121
125
  (error as { _tag: string })._tag === 'APIErrorResponse' &&
122
- 'status' in error
126
+ 'status' in error &&
127
+ (error as { status: number }).status === 409
123
128
  ) {
124
- const status = (error as { status: number }).status;
125
-
126
- // Detect 404 Not Found sandbox may not exist or may still be initializing.
127
- // The server normally handles the creating→idle wait transparently, but this
128
- // provides a clear typed error if a 404 reaches the SDK for any reason.
129
- if (status === 404) {
130
- throw new SandboxNotFoundError({
131
- message:
132
- 'Sandbox not found. If you just created this sandbox, it may still be initializing. The server should handle this automatically — if this persists, the sandbox may have been destroyed.',
133
- sandboxId,
134
- });
135
- }
136
-
137
- // Detect 409 Conflict (sandbox busy) and throw a specific error.
138
- // The sandbox API client is configured with maxRetries: 0 to fail fast
139
- // when sandbox is busy (retrying wouldn't help - sandbox is still busy).
140
- // Convert APIErrorResponse with status 409 to SandboxBusyError for clarity.
141
- if (status === 409) {
142
- throw new SandboxBusyError({
143
- message:
144
- 'Sandbox is currently busy executing another command. Please wait for the current execution to complete before submitting a new one.',
145
- sandboxId,
146
- });
147
- }
129
+ throw new SandboxBusyError({
130
+ message:
131
+ 'Sandbox is currently busy executing another command. Please wait for the current execution to complete before submitting a new one.',
132
+ sandboxId,
133
+ });
148
134
  }
149
135
  throw error;
150
136
  }
@@ -224,103 +224,20 @@ 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. 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
-
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.
255
229
  let exitCode = 0;
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
- }
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;
316
236
  }
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
- );
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');
324
241
  }
325
242
 
326
243
  if (timingLogsEnabled)
@@ -470,56 +387,44 @@ async function streamUrlToWritable(
470
387
  writable: Writable,
471
388
  signal: AbortSignal,
472
389
  logger?: Logger,
473
- _started?: number
390
+ started?: number
474
391
  ): Promise<void> {
475
- const streamStart = Date.now();
476
392
  try {
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
- );
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
+ );
488
400
 
489
401
  if (!response.ok || !response.body) {
490
- logger?.debug('[stream] not ok or no body (status=%d) — returning empty', response.status);
402
+ logger?.debug('stream response not ok or no body');
491
403
  return;
492
404
  }
493
405
 
494
406
  const reader = response.body.getReader();
495
- let chunks = 0;
496
- let totalBytes = 0;
407
+ let firstChunk = true;
497
408
 
498
409
  // Read until EOF - Pulse will block until data is available
499
410
  while (true) {
500
411
  const { done, value } = await reader.read();
501
412
  if (done) {
502
- logger?.debug(
503
- '[stream] EOF after %dms (%d chunks, %d bytes)',
504
- Date.now() - streamStart,
505
- chunks,
506
- totalBytes
507
- );
413
+ logger?.debug('stream EOF');
414
+ if (timingLogsEnabled && started)
415
+ console.error(`[TIMING] +${Date.now() - started}ms: stream EOF`);
508
416
  break;
509
417
  }
510
418
 
511
419
  if (value) {
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
- );
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;
522
426
  }
427
+ logger?.debug('stream chunk: %d bytes', value.length);
523
428
  await writeAndDrain(writable, value);
524
429
  }
525
430
  }
@@ -528,9 +433,9 @@ async function streamUrlToWritable(
528
433
  writable.end();
529
434
  } catch (err) {
530
435
  if (err instanceof Error && err.name === 'AbortError') {
531
- logger?.debug('[stream] aborted after %dms', Date.now() - streamStart);
436
+ logger?.debug('stream aborted');
532
437
  return;
533
438
  }
534
- logger?.debug('[stream] error after %dms: %s', Date.now() - streamStart, err);
439
+ logger?.debug('stream error: %s', err);
535
440
  }
536
441
  }
@@ -398,20 +398,6 @@ 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'),
415
401
  });
416
402
  export type Sandbox = z.infer<typeof SandboxSchema>;
417
403