deepline 0.2.30 → 0.2.32

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.
@@ -0,0 +1,421 @@
1
+ export const FIXTURE_BEHAVIOR_VERSION = 1 as const;
2
+ export const FIXTURE_BEHAVIOR_RESPONSE_VERSION = 2 as const;
3
+ export const MAX_FIXTURE_RESPONSE_DELAY_SAMPLES = 256;
4
+ export const MAX_FIXTURE_RESPONSE_DELAY_MS = 8 * 60_000;
5
+ export const MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH = 500;
6
+
7
+ export type FixtureBehaviorV1 = {
8
+ version: typeof FIXTURE_BEHAVIOR_VERSION;
9
+ responseDelaySamplesMs: number[];
10
+ };
11
+
12
+ export type FixtureResponseSample = {
13
+ delayMs: number;
14
+ when?: {
15
+ provider?: string;
16
+ operation?: string;
17
+ };
18
+ httpError?: {
19
+ status: number;
20
+ message: string;
21
+ };
22
+ };
23
+
24
+ export type FixtureBehaviorV2 = {
25
+ version: typeof FIXTURE_BEHAVIOR_RESPONSE_VERSION;
26
+ responseSamples: FixtureResponseSample[];
27
+ };
28
+
29
+ export type FixtureBehavior = FixtureBehaviorV1 | FixtureBehaviorV2;
30
+
31
+ /** Providers whose fixture-mode path intentionally remains a real local/test adapter. */
32
+ const FIXTURE_MODE_BYPASS_PROVIDERS = new Set([
33
+ 'neon',
34
+ 'local',
35
+ 'test',
36
+ 'test_high',
37
+ 'test_low',
38
+ 'test_unhinted',
39
+ ]);
40
+
41
+ type FixtureBehaviorValidationResult =
42
+ | { ok: true; behavior: FixtureBehavior | null }
43
+ | { ok: false; error: string };
44
+
45
+ export function validateFixtureBehavior(
46
+ value: unknown,
47
+ ): FixtureBehaviorValidationResult {
48
+ if (value === undefined || value === null) {
49
+ return { ok: true, behavior: null };
50
+ }
51
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
52
+ return { ok: false, error: 'fixtureBehavior must be a JSON object.' };
53
+ }
54
+ const record = value as Record<string, unknown>;
55
+ const supportedKeys =
56
+ record.version === FIXTURE_BEHAVIOR_RESPONSE_VERSION
57
+ ? new Set(['version', 'responseSamples'])
58
+ : new Set(['version', 'responseDelaySamplesMs']);
59
+ const unknownKeys = Object.keys(record).filter(
60
+ (key) => !supportedKeys.has(key),
61
+ );
62
+ if (unknownKeys.length > 0) {
63
+ return {
64
+ ok: false,
65
+ error: `Unsupported fixtureBehavior field "${unknownKeys[0]}".`,
66
+ };
67
+ }
68
+ if (
69
+ record.version !== FIXTURE_BEHAVIOR_VERSION &&
70
+ record.version !== FIXTURE_BEHAVIOR_RESPONSE_VERSION
71
+ ) {
72
+ return {
73
+ ok: false,
74
+ error:
75
+ `fixtureBehavior.version must be ${FIXTURE_BEHAVIOR_VERSION} or ` +
76
+ `${FIXTURE_BEHAVIOR_RESPONSE_VERSION}.`,
77
+ };
78
+ }
79
+ if (record.version === FIXTURE_BEHAVIOR_RESPONSE_VERSION) {
80
+ if (
81
+ !Array.isArray(record.responseSamples) ||
82
+ record.responseSamples.length === 0 ||
83
+ record.responseSamples.length > MAX_FIXTURE_RESPONSE_DELAY_SAMPLES
84
+ ) {
85
+ return {
86
+ ok: false,
87
+ error:
88
+ 'fixtureBehavior.responseSamples must contain between 1 and ' +
89
+ `${MAX_FIXTURE_RESPONSE_DELAY_SAMPLES} values.`,
90
+ };
91
+ }
92
+ const samples: FixtureResponseSample[] = [];
93
+ for (let index = 0; index < record.responseSamples.length; index += 1) {
94
+ const value = record.responseSamples[index];
95
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
96
+ return {
97
+ ok: false,
98
+ error: `fixtureBehavior.responseSamples[${index}] must be an object.`,
99
+ };
100
+ }
101
+ const sample = value as Record<string, unknown>;
102
+ const sampleUnknownKeys = Object.keys(sample).filter(
103
+ (key) => key !== 'delayMs' && key !== 'when' && key !== 'httpError',
104
+ );
105
+ if (sampleUnknownKeys.length > 0) {
106
+ return {
107
+ ok: false,
108
+ error:
109
+ `Unsupported fixtureBehavior.responseSamples[${index}] field ` +
110
+ `"${sampleUnknownKeys[0]}".`,
111
+ };
112
+ }
113
+ if (
114
+ typeof sample.delayMs !== 'number' ||
115
+ !Number.isSafeInteger(sample.delayMs) ||
116
+ sample.delayMs < 0 ||
117
+ sample.delayMs > MAX_FIXTURE_RESPONSE_DELAY_MS
118
+ ) {
119
+ return {
120
+ ok: false,
121
+ error:
122
+ `fixtureBehavior.responseSamples[${index}].delayMs must be an ` +
123
+ `integer between 0 and ${MAX_FIXTURE_RESPONSE_DELAY_MS}.`,
124
+ };
125
+ }
126
+ let when: FixtureResponseSample['when'];
127
+ if (sample.when !== undefined) {
128
+ if (
129
+ !sample.when ||
130
+ typeof sample.when !== 'object' ||
131
+ Array.isArray(sample.when)
132
+ ) {
133
+ return {
134
+ ok: false,
135
+ error:
136
+ `fixtureBehavior.responseSamples[${index}].when must be an ` +
137
+ 'object.',
138
+ };
139
+ }
140
+ const condition = sample.when as Record<string, unknown>;
141
+ const conditionUnknownKeys = Object.keys(condition).filter(
142
+ (key) => key !== 'provider' && key !== 'operation',
143
+ );
144
+ if (conditionUnknownKeys.length > 0) {
145
+ return {
146
+ ok: false,
147
+ error:
148
+ `Unsupported fixtureBehavior.responseSamples[${index}].when ` +
149
+ `field "${conditionUnknownKeys[0]}".`,
150
+ };
151
+ }
152
+ const provider = normalizeFixtureScopeValue(condition.provider, 100);
153
+ const operation = normalizeFixtureScopeValue(condition.operation, 200);
154
+ if (provider.error || operation.error) {
155
+ return {
156
+ ok: false,
157
+ error:
158
+ `fixtureBehavior.responseSamples[${index}].when ` +
159
+ (provider.error ?? operation.error),
160
+ };
161
+ }
162
+ if (!provider.value && !operation.value) {
163
+ return {
164
+ ok: false,
165
+ error:
166
+ `fixtureBehavior.responseSamples[${index}].when must select a ` +
167
+ 'provider or operation.',
168
+ };
169
+ }
170
+ when = {
171
+ ...(provider.value ? { provider: provider.value } : {}),
172
+ ...(operation.value ? { operation: operation.value } : {}),
173
+ };
174
+ }
175
+ let httpError: FixtureResponseSample['httpError'];
176
+ if (sample.httpError !== undefined) {
177
+ if (
178
+ !sample.httpError ||
179
+ typeof sample.httpError !== 'object' ||
180
+ Array.isArray(sample.httpError)
181
+ ) {
182
+ return {
183
+ ok: false,
184
+ error:
185
+ `fixtureBehavior.responseSamples[${index}].httpError must be ` +
186
+ 'an object.',
187
+ };
188
+ }
189
+ const error = sample.httpError as Record<string, unknown>;
190
+ const errorUnknownKeys = Object.keys(error).filter(
191
+ (key) => key !== 'status' && key !== 'message',
192
+ );
193
+ if (errorUnknownKeys.length > 0) {
194
+ return {
195
+ ok: false,
196
+ error:
197
+ `Unsupported fixtureBehavior.responseSamples[${index}]` +
198
+ `.httpError field "${errorUnknownKeys[0]}".`,
199
+ };
200
+ }
201
+ if (
202
+ typeof error.status !== 'number' ||
203
+ !Number.isSafeInteger(error.status) ||
204
+ error.status < 400 ||
205
+ error.status > 599
206
+ ) {
207
+ return {
208
+ ok: false,
209
+ error:
210
+ `fixtureBehavior.responseSamples[${index}].httpError.status ` +
211
+ 'must be an integer between 400 and 599.',
212
+ };
213
+ }
214
+ if (
215
+ typeof error.message !== 'string' ||
216
+ !error.message.trim() ||
217
+ error.message.length > MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH
218
+ ) {
219
+ return {
220
+ ok: false,
221
+ error:
222
+ `fixtureBehavior.responseSamples[${index}].httpError.message ` +
223
+ `must contain 1-${MAX_FIXTURE_RESPONSE_ERROR_MESSAGE_LENGTH} characters.`,
224
+ };
225
+ }
226
+ httpError = { status: error.status, message: error.message.trim() };
227
+ }
228
+ samples.push({
229
+ delayMs: sample.delayMs,
230
+ ...(when ? { when } : {}),
231
+ ...(httpError ? { httpError } : {}),
232
+ });
233
+ }
234
+ return {
235
+ ok: true,
236
+ behavior: {
237
+ version: FIXTURE_BEHAVIOR_RESPONSE_VERSION,
238
+ responseSamples: samples,
239
+ },
240
+ };
241
+ }
242
+ if (
243
+ !Array.isArray(record.responseDelaySamplesMs) ||
244
+ record.responseDelaySamplesMs.length === 0 ||
245
+ record.responseDelaySamplesMs.length > MAX_FIXTURE_RESPONSE_DELAY_SAMPLES
246
+ ) {
247
+ return {
248
+ ok: false,
249
+ error:
250
+ 'fixtureBehavior.responseDelaySamplesMs must contain between 1 and ' +
251
+ `${MAX_FIXTURE_RESPONSE_DELAY_SAMPLES} values.`,
252
+ };
253
+ }
254
+ const samples: number[] = [];
255
+ for (
256
+ let index = 0;
257
+ index < record.responseDelaySamplesMs.length;
258
+ index += 1
259
+ ) {
260
+ const value = record.responseDelaySamplesMs[index];
261
+ if (
262
+ typeof value !== 'number' ||
263
+ !Number.isSafeInteger(value) ||
264
+ value < 0 ||
265
+ value > MAX_FIXTURE_RESPONSE_DELAY_MS
266
+ ) {
267
+ return {
268
+ ok: false,
269
+ error:
270
+ `fixtureBehavior.responseDelaySamplesMs[${index}] must be an integer ` +
271
+ `between 0 and ${MAX_FIXTURE_RESPONSE_DELAY_MS}.`,
272
+ };
273
+ }
274
+ samples.push(value);
275
+ }
276
+ return {
277
+ ok: true,
278
+ behavior: {
279
+ version: FIXTURE_BEHAVIOR_VERSION,
280
+ responseDelaySamplesMs: samples,
281
+ },
282
+ };
283
+ }
284
+
285
+ function normalizeFixtureScopeValue(
286
+ value: unknown,
287
+ maxLength: number,
288
+ ): { value?: string; error?: string } {
289
+ if (value === undefined) return {};
290
+ if (typeof value !== 'string' || !value.trim()) {
291
+ return { error: 'values must be non-empty strings.' };
292
+ }
293
+ const normalized = value.trim().toLowerCase();
294
+ if (normalized.length > maxLength) {
295
+ return { error: `values must be at most ${maxLength} characters.` };
296
+ }
297
+ return { value: normalized };
298
+ }
299
+
300
+ export function requireFixtureBehavior(value: unknown): FixtureBehavior {
301
+ const result = validateFixtureBehavior(value);
302
+ if (result.ok === false) throw new Error(result.error);
303
+ if (!result.behavior) {
304
+ throw new Error('fixtureBehavior is required.');
305
+ }
306
+ return result.behavior;
307
+ }
308
+
309
+ export function shouldRouteFixtureProvider(provider: string): boolean {
310
+ const normalized = provider.trim().toLowerCase();
311
+ return Boolean(normalized) && !FIXTURE_MODE_BYPASS_PROVIDERS.has(normalized);
312
+ }
313
+
314
+ export function shouldRouteFixtureToolId(toolId: string): boolean {
315
+ const normalized = toolId.trim().toLowerCase();
316
+ if (!normalized) return false;
317
+ return ![...FIXTURE_MODE_BYPASS_PROVIDERS].some(
318
+ (provider) =>
319
+ normalized === provider || normalized.startsWith(`${provider}_`),
320
+ );
321
+ }
322
+
323
+ function stableFixtureRequestHash(value: string): number {
324
+ let hash = 0x811c9dc5;
325
+ for (let index = 0; index < value.length; index += 1) {
326
+ hash ^= value.charCodeAt(index);
327
+ hash = Math.imul(hash, 0x01000193) >>> 0;
328
+ }
329
+ return hash >>> 0;
330
+ }
331
+
332
+ export function selectFixtureResponseDelay(input: {
333
+ behavior: FixtureBehavior;
334
+ stableRequestKey: string;
335
+ }): { delayMs: number; sampleIndex: number } {
336
+ const selected = selectFixtureResponseSample(input);
337
+ return { delayMs: selected.delayMs, sampleIndex: selected.sampleIndex };
338
+ }
339
+
340
+ export function selectFixtureResponseSample(input: {
341
+ behavior: FixtureBehavior;
342
+ stableRequestKey: string;
343
+ }): FixtureResponseSample & { sampleIndex: number } {
344
+ const stableRequestKey = input.stableRequestKey.trim();
345
+ if (!stableRequestKey) {
346
+ throw new Error(
347
+ 'Configured fixture response timing requires a stable request key.',
348
+ );
349
+ }
350
+ const samples =
351
+ input.behavior.version === FIXTURE_BEHAVIOR_VERSION
352
+ ? input.behavior.responseDelaySamplesMs.map((delayMs, sampleIndex) => ({
353
+ delayMs,
354
+ sampleIndex,
355
+ }))
356
+ : selectEligibleResponseSamples(
357
+ input.behavior.responseSamples,
358
+ stableRequestKey,
359
+ );
360
+ const eligibleIndex =
361
+ stableFixtureRequestHash(
362
+ `fixture-behavior-v${input.behavior.version}:${stableRequestKey}`,
363
+ ) % samples.length;
364
+ return { ...samples[eligibleIndex]! };
365
+ }
366
+
367
+ function selectEligibleResponseSamples(
368
+ samples: FixtureResponseSample[],
369
+ stableRequestKey: string,
370
+ ): Array<FixtureResponseSample & { sampleIndex: number }> {
371
+ const [provider = '', operation = ''] = stableRequestKey
372
+ .toLowerCase()
373
+ .split(':', 3);
374
+ const indexed = samples.map((sample, sampleIndex) => ({
375
+ ...sample,
376
+ sampleIndex,
377
+ }));
378
+ const scoped = indexed.filter(
379
+ (sample) =>
380
+ sample.when &&
381
+ (!sample.when.provider || sample.when.provider === provider) &&
382
+ (!sample.when.operation || sample.when.operation === operation),
383
+ );
384
+ if (scoped.length > 0) return scoped;
385
+ const fallback = indexed.filter((sample) => !sample.when);
386
+ if (fallback.length > 0) return fallback;
387
+ throw new Error(
388
+ `Configured fixture behavior has no response samples for ${provider}:${operation}.`,
389
+ );
390
+ }
391
+
392
+ export async function waitForFixtureResponseDelay(input: {
393
+ behavior: FixtureBehavior;
394
+ stableRequestKey: string;
395
+ signal?: AbortSignal | null;
396
+ onSelected?: (selected: { delayMs: number; sampleIndex: number }) => void;
397
+ }): Promise<{ delayMs: number; sampleIndex: number }> {
398
+ const selected = selectFixtureResponseDelay(input);
399
+ input.onSelected?.(selected);
400
+ if (selected.delayMs === 0) return selected;
401
+ await new Promise<void>((resolve, reject) => {
402
+ if (input.signal?.aborted) {
403
+ reject(input.signal.reason ?? new DOMException('Aborted', 'AbortError'));
404
+ return;
405
+ }
406
+ const cleanup = () => {
407
+ input.signal?.removeEventListener('abort', onAbort);
408
+ };
409
+ const timer = setTimeout(() => {
410
+ cleanup();
411
+ resolve();
412
+ }, selected.delayMs);
413
+ const onAbort = () => {
414
+ clearTimeout(timer);
415
+ cleanup();
416
+ reject(input.signal?.reason ?? new DOMException('Aborted', 'AbortError'));
417
+ };
418
+ input.signal?.addEventListener('abort', onAbort, { once: true });
419
+ });
420
+ return selected;
421
+ }
@@ -6,7 +6,8 @@
6
6
  * at once" policy. See ADR 0007 + CONTEXT.md.
7
7
  *
8
8
  * Surface (small, by design):
9
- * - acquireRowSlot / acquireToolSlot local execution leases
9
+ * - acquireRowSlot / acquireToolSlot / acquireIntegrationRequestSlot
10
+ * → local execution leases
10
11
  * - acquireProviderPermit → per-attempt provider admission
11
12
  * - chargeBudget → throws on breach
12
13
  * - forkInlineChild → inline recursion scope
@@ -111,6 +112,14 @@ export interface PlayExecutionGovernor {
111
112
  opts?: { signal?: AbortSignal },
112
113
  ): Promise<WorkLease>;
113
114
 
115
+ /**
116
+ * Admit one brief runtime-to-app HTTP fetch/body exchange. Long provider or
117
+ * fixture residence happens before this edge and must not occupy this slot.
118
+ */
119
+ acquireIntegrationRequestSlot(opts?: {
120
+ signal?: AbortSignal;
121
+ }): Promise<WorkLease>;
122
+
114
123
  /**
115
124
  * Block until one physical provider HTTP attempt is admitted under the
116
125
  * provider's shared rate/concurrency rules. Call this immediately before the
@@ -259,6 +268,9 @@ export function createPlayExecutionGovernor(
259
268
 
260
269
  const rowSlots = new Semaphore(policy.concurrency.rowMax);
261
270
  const toolSlots = new Semaphore(policy.concurrency.toolCalls);
271
+ const integrationRequestSlots = new Semaphore(
272
+ policy.concurrency.integrationRequests,
273
+ );
262
274
  const adaptiveAdmission =
263
275
  input.adaptiveAdmission ??
264
276
  createInMemoryAdaptiveAdmission({
@@ -402,6 +414,9 @@ export function createPlayExecutionGovernor(
402
414
  return slot;
403
415
  },
404
416
 
417
+ acquireIntegrationRequestSlot: (opts) =>
418
+ integrationRequestSlots.acquire(opts?.signal),
419
+
405
420
  async acquireProviderPermit(toolId, opts) {
406
421
  // The rate ticket is an admission for a physical outbound attempt, not a
407
422
  // reservation made while receipt ownership, headers, or local tool slots
@@ -575,6 +590,8 @@ function createInlineChildGovernor(
575
590
  policy: root.policy,
576
591
  acquireRowSlot: (opts) => root.acquireRowSlot(opts),
577
592
  acquireToolSlot: (toolId, opts) => root.acquireToolSlot(toolId, opts),
593
+ acquireIntegrationRequestSlot: (opts) =>
594
+ root.acquireIntegrationRequestSlot(opts),
578
595
  acquireProviderPermit: (toolId, opts) =>
579
596
  root.acquireProviderPermit(toolId, opts),
580
597
  suggestedParallelism: (toolId, fallback) =>
@@ -28,6 +28,11 @@ export interface ExecutionConcurrencyPolicy {
28
28
  readonly rowMax: number;
29
29
  /** Global backstop on concurrently in-flight tool calls across all providers. */
30
30
  readonly toolCalls: number;
31
+ /**
32
+ * Brief runtime-to-app HTTP requests concurrently using sockets. Logical
33
+ * provider residence does not consume this admission.
34
+ */
35
+ readonly integrationRequests: number;
31
36
  /** Runner-local cap on concurrently active tool scheduling groups. */
32
37
  readonly toolDispatchGroups: number;
33
38
  /** Runner-local cap on active scheduling groups for one scalar/batch lane. */
@@ -89,9 +94,14 @@ export const SHARED_EXECUTION_POLICY: ResolvedExecutionPolicy = {
89
94
  // matches the default: throughput grows through provider pacing/batching,
90
95
  // while row payload size can only reduce admission through the byte budget.
91
96
  rowMax: 1_000,
92
- // Global all-provider backstop. Per-provider pacing is the real limit; this
93
- // just stops a single run from opening an absurd number of sockets at once.
97
+ // Global logical-call backstop. A logical call can spend most of its life
98
+ // waiting on a provider/fixture response without holding a network socket.
94
99
  toolCalls: 256,
100
+ // Physical runtime-to-app fetch/body admission. Keep short socket bursts
101
+ // below the sandbox/Vercel transport cliff while allowing many more logical
102
+ // calls to remain resident. This is deliberately independent of provider
103
+ // rate limits: it protects Deepline's own transport edge.
104
+ integrationRequests: 64,
95
105
  // Scheduling groups contain bounded-dispatch workers and receipt buffers.
96
106
  // Keep their promise topology finite even when pipelined replacement rows
97
107
  // become ready one at a time behind a slower provider pacer.
@@ -15,6 +15,7 @@ import type { PlaySandboxRuntimeLimits } from './sandbox-runtime-limits';
15
15
  import type { PlayRunInputPayload } from './play-input';
16
16
  import type { PlayRunFailureDetails } from './run-failure';
17
17
  import type { ToolExecutionErrorSchemaVersion } from '../tool-execution-error';
18
+ import type { FixtureBehavior } from './fixture-behavior';
18
19
 
19
20
  export type PlayRunnerRateStateBackendConfig =
20
21
  | {
@@ -122,6 +123,8 @@ export interface PlayRunnerContextConfig {
122
123
  runtimeTestFaultHeader?: string | null;
123
124
  vercelProtectionBypassToken?: string | null;
124
125
  integrationMode?: 'live' | 'eval_stub' | 'fixture';
126
+ /** Validated internal fixture-only provider response simulation. */
127
+ fixtureBehavior?: FixtureBehavior | null;
125
128
  /** Preview/dev test seam that applies provider pacing to fixture responses. */
126
129
  enforceFixtureProviderPacing?: boolean;
127
130
  /** Immutable tool-error payload schema copied from the run contract. */
@@ -64,6 +64,7 @@ export type OneShotDaytonaSandboxLifecycle = {
64
64
 
65
65
  export type DaytonaSandboxAcquisitionUnavailableReason =
66
66
  | 'daytona_total_cpu_limit_exceeded'
67
+ | 'daytona_acquisition_rate_limited'
67
68
  | 'daytona_sandbox_start_timeout';
68
69
 
69
70
  /**
@@ -86,14 +87,19 @@ export class DaytonaSandboxAcquisitionUnavailableError extends Error {
86
87
  export function resolveDaytonaSandboxAcquisitionUnavailableReason(
87
88
  errors: readonly string[],
88
89
  ): DaytonaSandboxAcquisitionUnavailableReason | null {
89
- if (
90
- errors.length > 0 &&
91
- errors.every((error) =>
92
- /Total CPU limit exceeded\.\s*Maximum allowed:\s*\d+/i.test(error),
93
- )
94
- ) {
90
+ if (errors.length === 0) return null;
91
+ const isCpuLimit = (error: string) =>
92
+ /Total CPU limit exceeded\.\s*Maximum allowed:\s*\d+/i.test(error);
93
+ const isRateLimit = (error: string) =>
94
+ /ThrottlerException|Too Many Requests|(?:status code|HTTP)\s*429/i.test(
95
+ error,
96
+ );
97
+ if (errors.every(isCpuLimit)) {
95
98
  return 'daytona_total_cpu_limit_exceeded';
96
99
  }
100
+ if (errors.every((error) => isCpuLimit(error) || isRateLimit(error))) {
101
+ return 'daytona_acquisition_rate_limited';
102
+ }
97
103
  return null;
98
104
  }
99
105
 
@@ -47,7 +47,10 @@ function classifyDaytonaAcquisitionFailure(
47
47
  if (!(error instanceof DaytonaSandboxAcquisitionUnavailableError)) {
48
48
  return null;
49
49
  }
50
- if (error.reason === 'daytona_total_cpu_limit_exceeded') {
50
+ if (
51
+ error.reason === 'daytona_total_cpu_limit_exceeded' ||
52
+ error.reason === 'daytona_acquisition_rate_limited'
53
+ ) {
51
54
  return 'provider_capacity_exhausted';
52
55
  }
53
56
  if (error.reason === 'daytona_sandbox_start_timeout') {