deepline 0.3.0 → 0.3.2

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.
@@ -4656,6 +4656,12 @@ export class DeeplineClient {
4656
4656
  {
4657
4657
  method: 'POST',
4658
4658
  body: definition,
4659
+ // A provider can reject a create with a 429 after Deepline has begun
4660
+ // the lifecycle request. Do not replay a mutation or hide the server's
4661
+ // monitor-specific recovery guidance behind a generic transport retry.
4662
+ maxRetries: 0,
4663
+ exactUrlOnly: true,
4664
+ preserveRateLimitResponse: true,
4659
4665
  },
4660
4666
  );
4661
4667
  if (definition.tool !== 'deepline.analytics') return deployed;
@@ -70,6 +70,12 @@ interface RequestOptions {
70
70
  maxRetries?: number;
71
71
  /** Disable localhost/127.0.0.1 failover for non-idempotent requests. */
72
72
  exactUrlOnly?: boolean;
73
+ /**
74
+ * Return a server-owned 429 envelope as a DeeplineError instead of replacing
75
+ * it with the transport's generic RateLimitError. Lifecycle mutations use
76
+ * this when their response explains whether the upstream change applied.
77
+ */
78
+ preserveRateLimitResponse?: boolean;
73
79
  /** Enables endpoint-specific structured tool failure mapping. */
74
80
  toolId?: string;
75
81
  }
@@ -419,6 +425,17 @@ export class HttpClient {
419
425
  }
420
426
 
421
427
  if (response.status === 429) {
428
+ if (
429
+ options?.preserveRateLimitResponse &&
430
+ isMonitorRateLimitEnvelope(parsed)
431
+ ) {
432
+ throw new DeeplineError(
433
+ apiErrorMessage(parsed, response.status),
434
+ response.status,
435
+ apiErrorCodeFromResponse(parsed),
436
+ { response: parsed },
437
+ );
438
+ }
422
439
  const retryAfter = parseRetryAfter(response);
423
440
  lastError = new RateLimitError(retryAfter);
424
441
  if (attempt < maxRetries) {
@@ -779,6 +796,21 @@ function isProviderOriginatedHttpError(parsed: unknown): boolean {
779
796
  return failureOrigin === 'provider' || code === 'UPSTREAM_BLOCKED';
780
797
  }
781
798
 
799
+ /**
800
+ * A monitor deployment may receive a safe, actionable 429 from Deepline after
801
+ * an upstream lifecycle attempt. Do not treat a CDN/WAF 429 as that envelope:
802
+ * it can be arbitrary HTML or text and must retain normal RateLimitError
803
+ * behavior.
804
+ */
805
+ function isMonitorRateLimitEnvelope(parsed: unknown): boolean {
806
+ const response = asRecord(parsed);
807
+ const error = asRecord(response?.error);
808
+ return (
809
+ error?.code === 'monitor_upstream_rate_limited' &&
810
+ typeof error.message === 'string'
811
+ );
812
+ }
813
+
782
814
  function apiErrorCodeFromResponse(parsed: unknown): string {
783
815
  const response = asRecord(parsed);
784
816
  const error = asRecord(response?.error);
@@ -2,6 +2,7 @@ import { tmpdir } from 'node:os';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { existsSync } from 'node:fs';
5
+ import { realpath } from 'node:fs/promises';
5
6
  import {
6
7
  bundlePlayFile as bundlePlayFileCore,
7
8
  type BundlePlayFileOptions,
@@ -145,10 +146,23 @@ export async function bundlePlayFile(
145
146
  filePath: string,
146
147
  options: BundlePlayFileOptions = {},
147
148
  ): Promise<BundledPlayFileResult> {
149
+ // The SDK sends this graph to a remote checker/runtime. Its file identities
150
+ // therefore must describe the authoring workspace, rather than the local
151
+ // machine paths used while building it. This is especially important on
152
+ // Windows: a raw `C:\\...` key cannot be reconciled beneath `/var/task`.
153
+ // Match the bundler's physical-path normalization. On macOS, for example,
154
+ // os.tmpdir() can report /var while the source graph resolves /private/var.
155
+ const localWorkspaceRoot = dirname(resolve(filePath));
156
+ const sourceIdentityRoot = await realpath(localWorkspaceRoot).catch(
157
+ () => localWorkspaceRoot,
158
+ );
148
159
  const result = await bundlePlayFileCore(filePath, {
149
160
  target: options.target ?? defaultPlayBundleTarget(),
150
161
  exportName: options.exportName,
151
- adapter: createSdkPlayBundlingAdapter(),
162
+ adapter: {
163
+ ...createSdkPlayBundlingAdapter(),
164
+ sourceIdentityRoot,
165
+ },
152
166
  });
153
167
  if (result.success)
154
168
  validatePlaySourceFilesHaveNoInlineSecrets(result.sourceFiles);
@@ -192,7 +192,7 @@ export const SDK_RELEASE = {
192
192
  // 0.3.0 introduces raw-v2: complete scrubbed provider responses are
193
193
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
194
194
  // getters keep their established compatibility behavior.
195
- version: '0.3.0',
195
+ version: '0.3.2',
196
196
  updateSummary:
197
197
  'New raw-v2 tool responses preserve complete scrubbed provider envelopes at toolResponse.rawV2, including JSON:API included resources, links, cursors, and totals. Existing toolResponse.raw and declared getters keep working unchanged.',
198
198
  contracts: {
@@ -51,6 +51,7 @@ export type AcquiredDaytonaSandbox = {
51
51
  daytonaOrganizationId: string;
52
52
  billingStartedAt: number;
53
53
  billingEndedAt?: number;
54
+ sandboxCapacityLeaseId?: string;
54
55
  };
55
56
 
56
57
  export type OneShotDaytonaSandboxLifecycle = {
@@ -118,6 +119,7 @@ type DaytonaCreateResult = {
118
119
  sandbox: DaytonaSandbox;
119
120
  attempt: number;
120
121
  attemptElapsedMs: number;
122
+ sandboxCapacityLeaseId?: string;
121
123
  };
122
124
 
123
125
  function daytonaCreateErrorClass(
@@ -133,6 +135,8 @@ function daytonaCreateErrorClass(
133
135
  async function rejectAcquiredSandbox(
134
136
  sandbox: DaytonaSandbox,
135
137
  reason: string,
138
+ reservation?: { leaseId: string },
139
+ releaseSandboxCapacity?: (leaseId: string) => Promise<void>,
136
140
  ): Promise<never> {
137
141
  try {
138
142
  await sandbox.delete(30);
@@ -143,6 +147,9 @@ async function rejectAcquiredSandbox(
143
147
  { cause: error },
144
148
  );
145
149
  }
150
+ if (reservation) {
151
+ await releaseSandboxCapacity?.(reservation.leaseId);
152
+ }
146
153
  throw new Error(reason);
147
154
  }
148
155
 
@@ -350,6 +357,8 @@ async function createRetriedOneShotDaytonaSandbox(input: {
350
357
  context: DaytonaExecutionContext;
351
358
  emitStage: DaytonaStageEmitter;
352
359
  observeCreateCall?: DaytonaCreateCallObserver;
360
+ reserveSandboxCapacity?: () => Promise<{ leaseId: string }>;
361
+ releaseSandboxCapacity?: (leaseId: string) => Promise<void>;
353
362
  nextProviderAttempt: () => number;
354
363
  startedAt: number;
355
364
  }): Promise<DaytonaCreateResult> {
@@ -372,6 +381,7 @@ async function createRetriedOneShotDaytonaSandbox(input: {
372
381
  occurredAtMs: attemptStartedAt,
373
382
  providerAttempt: attempt,
374
383
  });
384
+ const reservation = await input.reserveSandboxCapacity?.();
375
385
  let sandbox: DaytonaSandbox;
376
386
  try {
377
387
  sandbox = await createOneShotDaytonaSandbox({
@@ -381,6 +391,12 @@ async function createRetriedOneShotDaytonaSandbox(input: {
381
391
  sandboxName,
382
392
  });
383
393
  } catch (error) {
394
+ // A normal rejected create has no durable provider resource. A timeout
395
+ // can be ambiguous, so retain its lease until the crash TTL rather than
396
+ // admitting another sandbox against an unknown provider outcome.
397
+ if (reservation && !isDaytonaSandboxStartTimeout(String(error))) {
398
+ await input.releaseSandboxCapacity?.(reservation.leaseId);
399
+ }
384
400
  await input.observeCreateCall?.({
385
401
  type: 'daytona_create_call_failed',
386
402
  occurredAtMs: Date.now(),
@@ -407,6 +423,13 @@ async function createRetriedOneShotDaytonaSandbox(input: {
407
423
  sandboxName,
408
424
  });
409
425
  if (deleted) {
426
+ // The provider-confirmed reconciliation means this reservation can
427
+ // no longer represent a live sandbox. Release before returning the
428
+ // acquisition-unavailable result so Modal fallback is not blocked
429
+ // behind the unbound crash TTL.
430
+ if (reservation) {
431
+ await input.releaseSandboxCapacity?.(reservation.leaseId);
432
+ }
410
433
  throw new DaytonaSandboxAcquisitionUnavailableError(
411
434
  'daytona_sandbox_start_timeout',
412
435
  `${message} Timed-out Daytona sandbox was reconciled and deleted before fallback.`,
@@ -448,6 +471,7 @@ async function createRetriedOneShotDaytonaSandbox(input: {
448
471
  sandbox,
449
472
  attempt,
450
473
  attemptElapsedMs: acquiredAt - attemptStartedAt,
474
+ ...(reservation ? { sandboxCapacityLeaseId: reservation.leaseId } : {}),
451
475
  };
452
476
  }
453
477
  const message = `Daytona sandbox create failed across ${errors.length} bounded attempts: ${errors.join('; ')}`;
@@ -468,6 +492,8 @@ async function acquireOneShotDaytonaSandbox(input: {
468
492
  context: DaytonaExecutionContext;
469
493
  emitStage: DaytonaStageEmitter;
470
494
  observeCreateCall?: DaytonaCreateCallObserver;
495
+ reserveSandboxCapacity?: () => Promise<{ leaseId: string }>;
496
+ releaseSandboxCapacity?: (leaseId: string) => Promise<void>;
471
497
  nextProviderAttempt: () => number;
472
498
  startedAt: number;
473
499
  }): Promise<AcquiredDaytonaSandbox> {
@@ -493,6 +519,10 @@ async function acquireOneShotDaytonaSandbox(input: {
493
519
  await rejectAcquiredSandbox(
494
520
  result.sandbox,
495
521
  `Daytona sandbox resource boundary mismatch: expected cpu=${limits.cpu} memoryGiB=${limits.memoryGiB} diskGiB=${limits.diskGiB} gpu=${DAYTONA_SANDBOX_GPU}, granted cpu=${granted.cpu} memoryGiB=${granted.memoryGiB} diskGiB=${granted.diskGiB} gpu=${granted.gpu}`,
522
+ result.sandboxCapacityLeaseId
523
+ ? { leaseId: result.sandboxCapacityLeaseId }
524
+ : undefined,
525
+ input.releaseSandboxCapacity,
496
526
  );
497
527
  }
498
528
  const configuredOrganizationId =
@@ -506,6 +536,10 @@ async function acquireOneShotDaytonaSandbox(input: {
506
536
  await rejectAcquiredSandbox(
507
537
  result.sandbox,
508
538
  'Daytona sandbox organization routing mismatch. Refusing to run customer code in a sandbox whose observed organization differs from the configured organization.',
539
+ result.sandboxCapacityLeaseId
540
+ ? { leaseId: result.sandboxCapacityLeaseId }
541
+ : undefined,
542
+ input.releaseSandboxCapacity,
509
543
  );
510
544
  }
511
545
  let lookupOrganizationId: string | null = null;
@@ -528,6 +562,10 @@ async function acquireOneShotDaytonaSandbox(input: {
528
562
  return await rejectAcquiredSandbox(
529
563
  result.sandbox,
530
564
  'Daytona sandbox organization routing identity is missing. Refusing to run customer code without a durable cleanup routing domain.',
565
+ result.sandboxCapacityLeaseId
566
+ ? { leaseId: result.sandboxCapacityLeaseId }
567
+ : undefined,
568
+ input.releaseSandboxCapacity,
531
569
  );
532
570
  }
533
571
  const billingStartedAt = Date.now();
@@ -541,7 +579,14 @@ async function acquireOneShotDaytonaSandbox(input: {
541
579
  memoryGiB: granted.memoryGiB,
542
580
  diskGiB: granted.diskGiB,
543
581
  });
544
- return { sandbox, daytonaOrganizationId, billingStartedAt };
582
+ return {
583
+ sandbox,
584
+ daytonaOrganizationId,
585
+ billingStartedAt,
586
+ ...(result.sandboxCapacityLeaseId
587
+ ? { sandboxCapacityLeaseId: result.sandboxCapacityLeaseId }
588
+ : {}),
589
+ };
545
590
  }
546
591
 
547
592
  export function createOneShotDaytonaSandboxLifecycle(input: {
@@ -549,6 +594,8 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
549
594
  context: DaytonaExecutionContext;
550
595
  emitStage: DaytonaStageEmitter;
551
596
  observeCreateCall?: DaytonaCreateCallObserver;
597
+ reserveSandboxCapacity?: () => Promise<{ leaseId: string }>;
598
+ releaseSandboxCapacity?: (leaseId: string) => Promise<void>;
552
599
  startedAt?: number;
553
600
  }): OneShotDaytonaSandboxLifecycle {
554
601
  const orgId = validateDaytonaExecutionContext(input.context);
@@ -564,6 +611,8 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
564
611
  context: input.context,
565
612
  emitStage: input.emitStage,
566
613
  observeCreateCall: input.observeCreateCall,
614
+ reserveSandboxCapacity: input.reserveSandboxCapacity,
615
+ releaseSandboxCapacity: input.releaseSandboxCapacity,
567
616
  nextProviderAttempt: () => {
568
617
  providerAttempt += 1;
569
618
  return providerAttempt;
@@ -594,6 +643,13 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
594
643
  try {
595
644
  const acquired = await latestAcquiredSandboxPromise;
596
645
  await acquired.sandbox.delete(30);
646
+ // `dispose` has provider-confirmed that the sandbox is gone. This is
647
+ // normally the narrow cancellation window before the scheduler has
648
+ // durably recorded the resource, so no cleanup job will release an
649
+ // unbound lease on our behalf.
650
+ if (acquired.sandboxCapacityLeaseId) {
651
+ await input.releaseSandboxCapacity?.(acquired.sandboxCapacityLeaseId);
652
+ }
597
653
  } catch (error) {
598
654
  console.warn('[play-runner.daytona.dispose_failed_before_acquire]', {
599
655
  error: error instanceof Error ? error.message : String(error),
@@ -5,7 +5,10 @@ import type {
5
5
  PlayRunnerPreparedExecution,
6
6
  PlayRunnerPrepareInput,
7
7
  } from '../types';
8
- import { RuntimeResourceFenceLostError } from '../types';
8
+ import {
9
+ isRuntimeSandboxCapacityLimitError,
10
+ RuntimeResourceFenceLostError,
11
+ } from '../types';
9
12
  import { buildPlayRunnerBundle } from '../bundle';
10
13
  import { findPlayRunnerResult, parsePlayRunnerEvents } from '../runner-events';
11
14
  import type {
@@ -26,6 +29,7 @@ import {
26
29
  DaytonaSandboxAcquisitionUnavailableError,
27
30
  createDaytonaSandboxCleanupManager,
28
31
  createOneShotDaytonaSandboxLifecycle,
32
+ type AcquiredDaytonaSandbox,
29
33
  type DaytonaClient,
30
34
  type DaytonaExecutionContext,
31
35
  type DaytonaSandbox,
@@ -945,6 +949,10 @@ function prepareDaytonaExecution(
945
949
  emitStage: (stage, extra) =>
946
950
  emitDaytonaStage(callbacks, input.context, stage, extra),
947
951
  observeCreateCall: callbacks?.onRuntimeLifecycleEvent,
952
+ reserveSandboxCapacity: callbacks?.reserveSandboxCapacity
953
+ ? () => callbacks.reserveSandboxCapacity!('daytona')
954
+ : undefined,
955
+ releaseSandboxCapacity: callbacks?.releaseSandboxCapacity,
948
956
  });
949
957
  return {
950
958
  kind: 'daytona',
@@ -1055,22 +1063,12 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
1055
1063
  };
1056
1064
  let cancellationRequested = false;
1057
1065
  let cancellationCleanupStarted = false;
1058
- let activeAcquiredResource: {
1059
- sandbox: DaytonaSandbox;
1060
- daytonaOrganizationId: string;
1061
- billingStartedAt: number;
1062
- billingEndedAt?: number;
1063
- } | null = null;
1066
+ let activeAcquiredResource: AcquiredDaytonaSandbox | null = null;
1064
1067
  const uploadDeadlineBreaches: DaytonaUploadAttemptTiming[] = [];
1065
1068
  const reportedSandboxIds = new Set<string>();
1066
1069
  const reportedSandboxEndTimes = new Map<string, number | null>();
1067
1070
  const runtimeResourceReportErrors = new Set<unknown>();
1068
- const reportRuntimeResource = async (acquired: {
1069
- sandbox: DaytonaSandbox;
1070
- daytonaOrganizationId: string;
1071
- billingStartedAt: number;
1072
- billingEndedAt?: number;
1073
- }) => {
1071
+ const reportRuntimeResource = async (acquired: AcquiredDaytonaSandbox) => {
1074
1072
  const sandbox = acquired.sandbox;
1075
1073
  const billingEndedAt = acquired.billingEndedAt ?? null;
1076
1074
  if (
@@ -1101,6 +1099,9 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
1101
1099
  cpu: typeof sandbox.cpu === 'number' ? sandbox.cpu : null,
1102
1100
  memoryGiB: typeof sandbox.memory === 'number' ? sandbox.memory : null,
1103
1101
  diskGiB: typeof sandbox.disk === 'number' ? sandbox.disk : null,
1102
+ ...(acquired.sandboxCapacityLeaseId
1103
+ ? { sandboxCapacityLeaseId: acquired.sandboxCapacityLeaseId }
1104
+ : {}),
1104
1105
  });
1105
1106
  } catch (error) {
1106
1107
  runtimeResourceReportErrors.add(error);
@@ -1109,12 +1110,9 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
1109
1110
  reportedSandboxIds.add(sandbox.id);
1110
1111
  reportedSandboxEndTimes.set(sandbox.id, billingEndedAt);
1111
1112
  };
1112
- const reportRetiringRuntimeResource = async (acquired: {
1113
- sandbox: DaytonaSandbox;
1114
- daytonaOrganizationId: string;
1115
- billingStartedAt: number;
1116
- billingEndedAt?: number;
1117
- }) => {
1113
+ const reportRetiringRuntimeResource = async (
1114
+ acquired: AcquiredDaytonaSandbox,
1115
+ ) => {
1118
1116
  try {
1119
1117
  await reportRuntimeResource(acquired);
1120
1118
  } catch (error) {
@@ -1429,6 +1427,9 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
1429
1427
  }
1430
1428
  throw error;
1431
1429
  }
1430
+ if (isRuntimeSandboxCapacityLimitError(error)) {
1431
+ throw error;
1432
+ }
1432
1433
  if (error instanceof DaytonaSandboxAcquisitionUnavailableError) {
1433
1434
  throw error;
1434
1435
  }
@@ -1,6 +1,9 @@
1
1
  import type { ModalClient } from 'modal';
2
2
  import type { PlayRunnerBackend, PlayRunnerCallbacks } from '../types';
3
- import { RuntimeResourceFenceLostError } from '../types';
3
+ import {
4
+ isRuntimeSandboxCapacityLimitError,
5
+ RuntimeResourceFenceLostError,
6
+ } from '../types';
4
7
  import { buildPlayRunnerBundle } from '../bundle';
5
8
  import type {
6
9
  PlayRunnerExecutionConfig,
@@ -23,6 +26,7 @@ import { stageRunnerPayload } from './daytona-payload-transport';
23
26
  import { captureDetachedDaytonaRunnerReadinessBaseline } from './daytona-session-execution';
24
27
 
25
28
  const MODAL_RUNNER_READY_TIMEOUT_MS = 30_000;
29
+ const MODAL_CAPACITY_RELEASE_RETRY_DELAYS_MS = [0, 100, 500, 2_000] as const;
26
30
  type ModalSandbox = Awaited<ReturnType<ModalClient['sandboxes']['create']>>;
27
31
 
28
32
  export function modalSandboxLifetimeMs(
@@ -99,13 +103,38 @@ async function confirmDetachedModalRunnerReady(input: {
99
103
  );
100
104
  }
101
105
 
102
- async function terminateModalSandbox(sandbox: ModalSandbox): Promise<void> {
103
- await sandbox.terminate().catch((error) => {
106
+ async function terminateModalSandbox(sandbox: ModalSandbox): Promise<boolean> {
107
+ try {
108
+ // The default terminate call only acknowledges the request. Capacity may
109
+ // be released only after Modal confirms the sandbox itself has stopped.
110
+ await sandbox.terminate({ wait: true });
111
+ return true;
112
+ } catch (error) {
104
113
  console.warn('[play-runner.modal.terminate_failed]', {
105
114
  sandboxId: sandbox.sandboxId,
106
115
  error: error instanceof Error ? error.message : String(error),
107
116
  });
108
- });
117
+ return false;
118
+ }
119
+ }
120
+
121
+ async function releaseModalSandboxCapacityAfterConfirmedTermination(input: {
122
+ leaseId: string;
123
+ release: NonNullable<PlayRunnerCallbacks['releaseSandboxCapacity']>;
124
+ }): Promise<void> {
125
+ let lastError: unknown;
126
+ for (const delayMs of MODAL_CAPACITY_RELEASE_RETRY_DELAYS_MS) {
127
+ if (delayMs > 0) {
128
+ await new Promise<void>((resolve) => setTimeout(resolve, delayMs));
129
+ }
130
+ try {
131
+ await input.release(input.leaseId);
132
+ return;
133
+ } catch (error) {
134
+ lastError = error;
135
+ }
136
+ }
137
+ throw lastError;
109
138
  }
110
139
 
111
140
  export const modalPlayRunnerBackend: PlayRunnerBackend = {
@@ -139,24 +168,35 @@ export const modalPlayRunnerBackend: PlayRunnerBackend = {
139
168
  createIfMissing: true,
140
169
  });
141
170
  const image = modalConfig.client.images.fromRegistry(modalConfig.image);
142
- const sandbox = await modalConfig.client.sandboxes.create(app, image, {
143
- cpu: MODAL_SANDBOX_CPU_CORES,
144
- cpuLimit: MODAL_SANDBOX_CPU_CORES,
145
- memoryMiB: MODAL_SANDBOX_MEMORY_MIB,
146
- memoryLimitMiB: MODAL_SANDBOX_MEMORY_MIB,
147
- timeoutMs: modalSandboxLifetimeMs(modalConfig.limits),
148
- idleTimeoutMs: modalSandboxLifetimeMs(modalConfig.limits),
149
- workdir: modalConfig.workdir,
150
- tags: {
151
- source: 'deepline-play-runner',
152
- orgId: config.context.orgId ?? 'unknown',
153
- workflowId: config.context.workflowId ?? 'unknown',
154
- runId: config.context.runId ?? 'unknown',
155
- },
156
- ...(modalConfig.outboundCidrAllowlist
157
- ? { outboundCidrAllowlist: modalConfig.outboundCidrAllowlist }
158
- : {}),
159
- });
171
+ const capacityReservation =
172
+ await callbacks?.reserveSandboxCapacity?.('modal');
173
+ const sandbox = await modalConfig.client.sandboxes
174
+ .create(app, image, {
175
+ cpu: MODAL_SANDBOX_CPU_CORES,
176
+ cpuLimit: MODAL_SANDBOX_CPU_CORES,
177
+ memoryMiB: MODAL_SANDBOX_MEMORY_MIB,
178
+ memoryLimitMiB: MODAL_SANDBOX_MEMORY_MIB,
179
+ timeoutMs: modalSandboxLifetimeMs(modalConfig.limits),
180
+ idleTimeoutMs: modalSandboxLifetimeMs(modalConfig.limits),
181
+ workdir: modalConfig.workdir,
182
+ tags: {
183
+ source: 'deepline-play-runner',
184
+ orgId: config.context.orgId ?? 'unknown',
185
+ workflowId: config.context.workflowId ?? 'unknown',
186
+ runId: config.context.runId ?? 'unknown',
187
+ },
188
+ ...(modalConfig.outboundCidrAllowlist
189
+ ? { outboundCidrAllowlist: modalConfig.outboundCidrAllowlist }
190
+ : {}),
191
+ })
192
+ .catch(async (error) => {
193
+ if (capacityReservation) {
194
+ await callbacks?.releaseSandboxCapacity?.(
195
+ capacityReservation.leaseId,
196
+ );
197
+ }
198
+ throw error;
199
+ });
160
200
  const billingStartedAt = Date.now();
161
201
  runtimeTiming.modalCreateMs = billingStartedAt - startedAt;
162
202
  emitModalStage(config.context, 'create:done', {
@@ -164,6 +204,7 @@ export const modalPlayRunnerBackend: PlayRunnerBackend = {
164
204
  elapsedMs: runtimeTiming.modalCreateMs,
165
205
  });
166
206
  let detached = false;
207
+ let setupError: unknown;
167
208
  let cancelExecution!: (error: Error) => void;
168
209
  const cancellationPromise = new Promise<never>((_resolve, reject) => {
169
210
  cancelExecution = reject;
@@ -192,6 +233,9 @@ export const modalPlayRunnerBackend: PlayRunnerBackend = {
192
233
  cpu: MODAL_SANDBOX_CPU_CORES,
193
234
  memoryGiB: MODAL_SANDBOX_MEMORY_MIB / 1024,
194
235
  diskGiB: 0,
236
+ ...(capacityReservation
237
+ ? { sandboxCapacityLeaseId: capacityReservation.leaseId }
238
+ : {}),
195
239
  });
196
240
  } catch (error) {
197
241
  runtimeResourceRegistrationError = error;
@@ -290,14 +334,45 @@ export const modalPlayRunnerBackend: PlayRunnerBackend = {
290
334
  tableNamespace: null,
291
335
  runtimeTiming,
292
336
  };
337
+ } catch (error) {
338
+ setupError = error;
339
+ throw error;
293
340
  } finally {
294
341
  callbacks?.cancellationSignal?.removeEventListener('abort', onCancel);
295
- if (!detached) await terminateModalSandbox(sandbox);
342
+ if (!detached && (await terminateModalSandbox(sandbox))) {
343
+ // This branch synchronously confirms that the just-created resource
344
+ // is gone. Do not keep its admission lease until the cleanup loop
345
+ // sees a resource that no longer exists.
346
+ if (capacityReservation && callbacks?.releaseSandboxCapacity) {
347
+ try {
348
+ await releaseModalSandboxCapacityAfterConfirmedTermination({
349
+ leaseId: capacityReservation.leaseId,
350
+ release: callbacks.releaseSandboxCapacity,
351
+ });
352
+ } catch (releaseError) {
353
+ // The scheduler fence is the primary correctness signal. Keep
354
+ // it intact if its best-effort early capacity release is
355
+ // transiently unavailable; durable cleanup will retry release.
356
+ if (setupError) {
357
+ console.error('[play-runner.modal.capacity_release_failed]', {
358
+ sandboxId: sandbox.sandboxId,
359
+ error:
360
+ releaseError instanceof Error
361
+ ? releaseError.message
362
+ : String(releaseError),
363
+ });
364
+ } else {
365
+ throw releaseError;
366
+ }
367
+ }
368
+ }
369
+ }
296
370
  }
297
371
  } catch (error) {
298
372
  if (
299
373
  error === runtimeResourceRegistrationError ||
300
- error instanceof RuntimeResourceFenceLostError
374
+ error instanceof RuntimeResourceFenceLostError ||
375
+ isRuntimeSandboxCapacityLimitError(error)
301
376
  ) {
302
377
  throw error;
303
378
  }
@@ -335,7 +410,7 @@ export async function deleteModalSandboxById(input: {
335
410
  appId: expectedAppId,
336
411
  })) {
337
412
  if (sandbox.sandboxId !== input.sandboxId) continue;
338
- await sandbox.terminate();
413
+ await sandbox.terminate({ wait: true });
339
414
  return { kind: 'deleted', appId: expectedAppId };
340
415
  }
341
416
  // Cleanup means ensure absent. An exact app-scoped inventory proving the
@@ -28,6 +28,8 @@ export type PlayRunnerRuntimeResource = {
28
28
  cpu?: number | null;
29
29
  memoryGiB?: number | null;
30
30
  diskGiB?: number | null;
31
+ /** Scheduler-only soft-capacity reservation made before provider create. */
32
+ sandboxCapacityLeaseId?: string;
31
33
  };
32
34
 
33
35
  export type RuntimeResourceTerminalReason =
@@ -64,6 +66,23 @@ export class RuntimeResourceFenceLostError extends Error {
64
66
  }
65
67
  }
66
68
 
69
+ /**
70
+ * A scheduler-owned admission result crosses the provider-backend boundary.
71
+ * Keep it typed by name here so the shared backend can rethrow it without an
72
+ * import from the app-owned scheduler implementation.
73
+ */
74
+ export const RUNTIME_SANDBOX_CAPACITY_LIMIT_ERROR_NAME =
75
+ 'RuntimeSandboxCapacityLimitError';
76
+
77
+ export function isRuntimeSandboxCapacityLimitError(
78
+ error: unknown,
79
+ ): error is Error {
80
+ return (
81
+ error instanceof Error &&
82
+ error.name === RUNTIME_SANDBOX_CAPACITY_LIMIT_ERROR_NAME
83
+ );
84
+ }
85
+
67
86
  export interface PlayRunnerCallbacks {
68
87
  onLog?: (event: PlayRunnerLogEvent) => void;
69
88
  onCheckpoint?: (checkpoint: PlayCheckpoint) => void;
@@ -72,6 +91,12 @@ export interface PlayRunnerCallbacks {
72
91
  onRuntimeResourceAcquired?: (
73
92
  resource: PlayRunnerRuntimeResource,
74
93
  ) => void | Promise<void>;
94
+ /** Reserve one physical-sandbox slot immediately before a provider create. */
95
+ reserveSandboxCapacity?: (
96
+ provider: 'daytona' | 'modal',
97
+ ) => Promise<{ leaseId: string }>;
98
+ /** Release an unbound reservation when provider create never produced a resource. */
99
+ releaseSandboxCapacity?: (leaseId: string) => Promise<void>;
75
100
  /**
76
101
  * Durable scheduler-owned evidence for a provider create call. Backends
77
102
  * await this callback at the create boundary; it is not best-effort logging.
@@ -13,7 +13,6 @@ export const RUNTIME_CAPACITY_POLICY = {
13
13
  /** 32 Machines x 8 claim slots permits 256 concurrent launch/resume legs. */
14
14
  maxActiveLaneMachines: 32,
15
15
  workerSlotsPerMachine: 8,
16
- perOrgConcurrency: 4,
17
16
  /** Queue age is an escape hatch for small-but-stuck backlogs. */
18
17
  scaleUpQueueAgeMs: 10_000,
19
18
  /** Bound one Fly reconciliation without turning backlog into a stampede. */
@@ -113,14 +112,6 @@ export function assertRuntimeCapacityPolicy(): void {
113
112
  'Receipt claim operations must leave at least five seconds for cancellation and response delivery.',
114
113
  );
115
114
  }
116
- if (
117
- RUNTIME_CAPACITY_POLICY.absurd.perOrgConcurrency >
118
- ABSURD_GLOBAL_RUN_CONCURRENCY
119
- ) {
120
- throw new Error(
121
- 'Per-org runtime concurrency cannot exceed global runtime concurrency.',
122
- );
123
- }
124
115
  if (
125
116
  RUNTIME_CAPACITY_POLICY.absurd.activeLaneMachines >
126
117
  RUNTIME_CAPACITY_POLICY.absurd.maxActiveLaneMachines
@@ -8,11 +8,12 @@ export function transientServiceUnavailableDetail(
8
8
  if (error instanceof TypeError && /^fetch failed$/i.test(detail.trim())) {
9
9
  return detail;
10
10
  }
11
- // Convex uses both an explicit ServiceUnavailable response and this generic
12
- // InternalServerError envelope for short control-plane outages. The latter
13
- // text is intentionally exact: application exceptions must not become
14
- // retryable merely because they mention an internal-server error.
15
- return /ServiceUnavailable|Service temporarily unavailable|temporarily unavailable \(Code:\s*\d+\)|"code"\s*:\s*"InternalServerError"[\s\S]*(?:Your request couldn't be completed\. Try again later\.|Your request timed out performing too many system operations\.)/i.test(
11
+ // Convex uses explicit ServiceUnavailable and ExpiredInQueue responses, as
12
+ // well as this generic InternalServerError envelope for short control-plane
13
+ // outages. The latter text is intentionally exact: application exceptions
14
+ // must not become retryable merely because they mention an internal-server
15
+ // error.
16
+ return /ServiceUnavailable|Service temporarily unavailable|temporarily unavailable \(Code:\s*\d+\)|"code"\s*:\s*"ExpiredInQueue"|"code"\s*:\s*"InternalServerError"[\s\S]*(?:Your request couldn't be completed\. Try again later\.|Your request timed out performing too many system operations\.)/i.test(
16
17
  detail,
17
18
  )
18
19
  ? detail
@@ -71,6 +71,7 @@ export const PLAY_AUTHORING_CONTRACT_ISSUE_CODES = [
71
71
  'play_authoring_fetch_key_reused_in_loop',
72
72
  'play_authoring_binding_invalid',
73
73
  'play_authoring_input_schema_unresolved',
74
+ 'tool_response_raw_access',
74
75
  'check_error',
75
76
  'docflow_compile_error',
76
77
  'docflow_binding_drift',
@@ -223,9 +223,12 @@ function sourceIdentityPath(
223
223
  ): string {
224
224
  if (!sourceIdentityRoot) return filePath;
225
225
  const identityRoot = resolve(sourceIdentityRoot);
226
- const logicalPath = relative(identityRoot, resolve(filePath));
226
+ const resolvedFilePath = resolve(filePath);
227
+ const logicalPath = relative(identityRoot, resolvedFilePath);
228
+ if (!logicalPath) {
229
+ return basename(resolvedFilePath);
230
+ }
227
231
  if (
228
- !logicalPath ||
229
232
  logicalPath === '..' ||
230
233
  logicalPath.startsWith(`..${sep}`) ||
231
234
  isAbsolute(logicalPath)
package/dist/cli/index.js CHANGED
@@ -185,7 +185,7 @@ function configureProxyFromEnv() {
185
185
  configureProxyFromEnv();
186
186
 
187
187
  // src/cli/index.ts
188
- var import_promises9 = require("fs/promises");
188
+ var import_promises10 = require("fs/promises");
189
189
  var import_node_path26 = require("path");
190
190
  var import_node_os19 = require("os");
191
191
  var import_commander4 = require("commander");
@@ -1047,7 +1047,7 @@ var SDK_RELEASE = {
1047
1047
  // 0.3.0 introduces raw-v2: complete scrubbed provider responses are
1048
1048
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
1049
1049
  // getters keep their established compatibility behavior.
1050
- version: "0.3.0",
1050
+ version: "0.3.2",
1051
1051
  updateSummary: "New raw-v2 tool responses preserve complete scrubbed provider envelopes at toolResponse.rawV2, including JSON:API included resources, links, cursors, and totals. Existing toolResponse.raw and declared getters keep working unchanged.",
1052
1052
  contracts: {
1053
1053
  api: {
@@ -1466,6 +1466,14 @@ var HttpClient = class {
1466
1466
  throw structuredToolError;
1467
1467
  }
1468
1468
  if (response.status === 429) {
1469
+ if (options?.preserveRateLimitResponse && isMonitorRateLimitEnvelope(parsed)) {
1470
+ throw new DeeplineError(
1471
+ apiErrorMessage(parsed, response.status),
1472
+ response.status,
1473
+ apiErrorCodeFromResponse(parsed),
1474
+ { response: parsed }
1475
+ );
1476
+ }
1469
1477
  const retryAfter = parseRetryAfter(response);
1470
1478
  lastError = new RateLimitError(retryAfter);
1471
1479
  if (attempt < maxRetries) {
@@ -1727,6 +1735,11 @@ function isProviderOriginatedHttpError(parsed) {
1727
1735
  const code = error?.code ?? response?.code;
1728
1736
  return failureOrigin === "provider" || code === "UPSTREAM_BLOCKED";
1729
1737
  }
1738
+ function isMonitorRateLimitEnvelope(parsed) {
1739
+ const response = asRecord(parsed);
1740
+ const error = asRecord(response?.error);
1741
+ return error?.code === "monitor_upstream_rate_limited" && typeof error.message === "string";
1742
+ }
1730
1743
  function apiErrorCodeFromResponse(parsed) {
1731
1744
  const response = asRecord(parsed);
1732
1745
  const error = asRecord(response?.error);
@@ -6503,7 +6516,13 @@ var DeeplineClient = class {
6503
6516
  "/api/v2/monitors/deploy",
6504
6517
  {
6505
6518
  method: "POST",
6506
- body: definition
6519
+ body: definition,
6520
+ // A provider can reject a create with a 429 after Deepline has begun
6521
+ // the lifecycle request. Do not replay a mutation or hide the server's
6522
+ // monitor-specific recovery guidance behind a generic transport retry.
6523
+ maxRetries: 0,
6524
+ exactUrlOnly: true,
6525
+ preserveRateLimitResponse: true
6507
6526
  }
6508
6527
  );
6509
6528
  if (definition.tool !== "deepline.analytics") return deployed;
@@ -11068,7 +11087,7 @@ Examples:
11068
11087
  }
11069
11088
 
11070
11089
  // src/cli/commands/enrich.ts
11071
- var import_promises6 = require("fs/promises");
11090
+ var import_promises7 = require("fs/promises");
11072
11091
  var import_node_os10 = require("os");
11073
11092
  var import_node_path15 = require("path");
11074
11093
  var import_commander2 = require("commander");
@@ -11076,7 +11095,7 @@ var import_commander2 = require("commander");
11076
11095
  // src/cli/commands/play.ts
11077
11096
  var import_node_crypto6 = require("crypto");
11078
11097
  var import_node_fs12 = require("fs");
11079
- var import_promises5 = require("fs/promises");
11098
+ var import_promises6 = require("fs/promises");
11080
11099
  var import_node_path14 = require("path");
11081
11100
  var import_sync5 = require("csv-parse/sync");
11082
11101
 
@@ -12784,6 +12803,7 @@ var import_node_os9 = require("os");
12784
12803
  var import_node_path13 = require("path");
12785
12804
  var import_node_url = require("url");
12786
12805
  var import_node_fs11 = require("fs");
12806
+ var import_promises5 = require("fs/promises");
12787
12807
 
12788
12808
  // ../shared_libs/plays/bundling/index.ts
12789
12809
  var import_node_crypto4 = require("crypto");
@@ -18314,7 +18334,7 @@ var PLAY_RUN_RESERVED_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
18314
18334
  ]);
18315
18335
  async function pathExistsIncludingSymlink(path) {
18316
18336
  try {
18317
- await (0, import_promises5.lstat)(path);
18337
+ await (0, import_promises6.lstat)(path);
18318
18338
  return true;
18319
18339
  } catch (error) {
18320
18340
  if (error.code === "ENOENT") {
@@ -18331,7 +18351,7 @@ function runIdFileTempPath(destination) {
18331
18351
  }
18332
18352
  async function removeRunIdTempFile(path) {
18333
18353
  try {
18334
- await (0, import_promises5.unlink)(path);
18354
+ await (0, import_promises6.unlink)(path);
18335
18355
  } catch (error) {
18336
18356
  if (error.code !== "ENOENT") {
18337
18357
  throw error;
@@ -18348,7 +18368,7 @@ async function preflightRunIdFile(path) {
18348
18368
  const tempPath = runIdFileTempPath(destination);
18349
18369
  let handle = null;
18350
18370
  try {
18351
- handle = await (0, import_promises5.open)(tempPath, "wx", 384);
18371
+ handle = await (0, import_promises6.open)(tempPath, "wx", 384);
18352
18372
  await handle.sync();
18353
18373
  } catch (error) {
18354
18374
  throw new Error(
@@ -18366,7 +18386,7 @@ async function readRunIdFile(destination) {
18366
18386
  }
18367
18387
  let parsed;
18368
18388
  try {
18369
- parsed = JSON.parse(await (0, import_promises5.readFile)(destination, "utf8"));
18389
+ parsed = JSON.parse(await (0, import_promises6.readFile)(destination, "utf8"));
18370
18390
  } catch (error) {
18371
18391
  throw new Error(
18372
18392
  `--run-id-file destination contains invalid JSON: ${destination}. Refusing to overwrite it (${error instanceof Error ? error.message : String(error)}).`
@@ -18390,7 +18410,7 @@ async function writePlayRunIdFile(destination, runId) {
18390
18410
  const tempPath = runIdFileTempPath(destination);
18391
18411
  let handle = null;
18392
18412
  try {
18393
- handle = await (0, import_promises5.open)(tempPath, "wx", 384);
18413
+ handle = await (0, import_promises6.open)(tempPath, "wx", 384);
18394
18414
  await handle.writeFile(
18395
18415
  `${JSON.stringify({ version: 1, runId })}
18396
18416
  `,
@@ -18400,8 +18420,8 @@ async function writePlayRunIdFile(destination, runId) {
18400
18420
  await handle.close();
18401
18421
  handle = null;
18402
18422
  try {
18403
- await (0, import_promises5.link)(tempPath, destination);
18404
- const directory = await (0, import_promises5.open)((0, import_node_path14.dirname)(destination), "r");
18423
+ await (0, import_promises6.link)(tempPath, destination);
18424
+ const directory = await (0, import_promises6.open)((0, import_node_path14.dirname)(destination), "r");
18405
18425
  try {
18406
18426
  await directory.sync();
18407
18427
  } finally {
@@ -27693,6 +27713,7 @@ var ENRICH_DEBUG_T0 = Date.now();
27693
27713
  var GENERATED_ENRICH_ROWS_TABLE_NAMESPACE = "deepline_enrich_rows";
27694
27714
  var SDK_ENRICH_TELEMETRY_TIMEOUT_MS = 1e4;
27695
27715
  var SDK_ENRICH_TELEMETRY_COMMAND_MAX_LENGTH = 2e4;
27716
+ var ENRICH_DEPRECATION_NOTICE = "DEPRECATION: `deepline enrich` is deprecated and no longer actively maintained. Use `deepline plays` for the supported workflow.\n";
27696
27717
  var ENRICH_AI_RUNTIME_COMPACT_TOOLS = /* @__PURE__ */ new Set([
27697
27718
  "ai_inference",
27698
27719
  "aiinference",
@@ -27866,7 +27887,7 @@ async function readAtFileReference(value, argumentName, strip = true) {
27866
27887
  throw new Error(`Invalid ${argumentName} value: empty @file path.`);
27867
27888
  }
27868
27889
  try {
27869
- const text = await (0, import_promises6.readFile)(filePath, "utf8");
27890
+ const text = await (0, import_promises7.readFile)(filePath, "utf8");
27870
27891
  const normalized = text.replace(/^\uFEFF/, "");
27871
27892
  return strip ? normalized.trim() : normalized;
27872
27893
  } catch (error) {
@@ -28164,7 +28185,7 @@ async function buildPlanArgs(args) {
28164
28185
  async function assertInputCsvExists(inputCsv) {
28165
28186
  const path = (0, import_node_path15.resolve)(inputCsv);
28166
28187
  try {
28167
- const info = await (0, import_promises6.stat)(path);
28188
+ const info = await (0, import_promises7.stat)(path);
28168
28189
  if (info.isFile()) {
28169
28190
  return;
28170
28191
  }
@@ -28185,8 +28206,8 @@ async function assertSafeOutputPath(inputCsv, outputPath) {
28185
28206
  }
28186
28207
  try {
28187
28208
  const [inputInfo, outputInfo] = await Promise.all([
28188
- (0, import_promises6.stat)(input2),
28189
- (0, import_promises6.stat)(output2)
28209
+ (0, import_promises7.stat)(input2),
28210
+ (0, import_promises7.stat)(output2)
28190
28211
  ]);
28191
28212
  if (inputInfo.dev === outputInfo.dev && inputInfo.ino === outputInfo.ino) {
28192
28213
  throw new Error(
@@ -28206,7 +28227,7 @@ async function assertSafeOutputPath(inputCsv, outputPath) {
28206
28227
  }
28207
28228
  async function regularFileExists(path) {
28208
28229
  try {
28209
- const info = await (0, import_promises6.stat)((0, import_node_path15.resolve)(path));
28230
+ const info = await (0, import_promises7.stat)((0, import_node_path15.resolve)(path));
28210
28231
  return info.isFile();
28211
28232
  } catch (error) {
28212
28233
  const code = error && typeof error === "object" ? error.code : void 0;
@@ -28217,7 +28238,7 @@ async function regularFileExists(path) {
28217
28238
  }
28218
28239
  }
28219
28240
  async function readConfig(path) {
28220
- const source = await (0, import_promises6.readFile)((0, import_node_path15.resolve)(path), "utf8");
28241
+ const source = await (0, import_promises7.readFile)((0, import_node_path15.resolve)(path), "utf8");
28221
28242
  let parsed;
28222
28243
  try {
28223
28244
  parsed = JSON.parse(source);
@@ -28638,7 +28659,7 @@ async function writeOutputCsv(outputPath, status, options) {
28638
28659
  ]),
28639
28660
  options?.config
28640
28661
  );
28641
- await (0, import_promises6.writeFile)(
28662
+ await (0, import_promises7.writeFile)(
28642
28663
  (0, import_node_path15.resolve)(outputPath),
28643
28664
  csvStringFromRows(merged.rows, columns),
28644
28665
  "utf8"
@@ -30318,7 +30339,7 @@ async function persistEnrichFailureReport(input2) {
30318
30339
  }
30319
30340
  const stateDir = (0, import_node_path15.join)((0, import_node_os10.homedir)(), ".local", "deepline", "runtime", "state");
30320
30341
  const reportPrefix = input2.jobs.length > 0 ? "run-block-failures" : "enrich-issues";
30321
- await (0, import_promises6.mkdir)(stateDir, { recursive: true });
30342
+ await (0, import_promises7.mkdir)(stateDir, { recursive: true });
30322
30343
  const reportPath = (0, import_node_path15.join)(
30323
30344
  stateDir,
30324
30345
  `${reportPrefix}-${Math.floor(Date.now() / 1e3)}-${process.pid}.json`
@@ -30344,7 +30365,7 @@ async function persistEnrichFailureReport(input2) {
30344
30365
  if (input2.rows.rowStart !== null && input2.rows.rowEnd !== null) {
30345
30366
  report.rows = { start: input2.rows.rowStart, end: input2.rows.rowEnd };
30346
30367
  }
30347
- await (0, import_promises6.writeFile)(reportPath, `${JSON.stringify(report, null, 2)}
30368
+ await (0, import_promises7.writeFile)(reportPath, `${JSON.stringify(report, null, 2)}
30348
30369
  `, "utf8");
30349
30370
  return reportPath;
30350
30371
  }
@@ -31162,6 +31183,7 @@ function registerEnrichCommand(program) {
31162
31183
  "--max-credits-per-run <credits>",
31163
31184
  "Set a hard Deepline-credit ceiling enforced by the runtime for this run."
31164
31185
  ).action(async (options, _command) => {
31186
+ process.stderr.write(ENRICH_DEPRECATION_NOTICE);
31165
31187
  if (currentEnrichArgs().some(
31166
31188
  (arg) => arg === "--no-open" || arg.startsWith("--no-open=")
31167
31189
  )) {
@@ -31247,13 +31269,13 @@ function registerEnrichCommand(program) {
31247
31269
  sdkEnrichTelemetryCompleted = true;
31248
31270
  await completeSdkEnrichTelemetry(sdkEnrichTelemetry, input2);
31249
31271
  };
31250
- const tempDir = await (0, import_promises6.mkdtemp)((0, import_node_path15.join)((0, import_node_os10.tmpdir)(), "deepline-enrich-play-"));
31272
+ const tempDir = await (0, import_promises7.mkdtemp)((0, import_node_path15.join)((0, import_node_os10.tmpdir)(), "deepline-enrich-play-"));
31251
31273
  await emitSdkEnrichTelemetry(sdkEnrichTelemetry, "enrich_started");
31252
31274
  const tempPlay = (0, import_node_path15.join)(tempDir, "deepline-enrich.play.ts");
31253
31275
  let inPlaceTempDir = null;
31254
31276
  let inPlaceTempOutputPath = null;
31255
31277
  const inPlaceFinalOutputPath = options.inPlace ? (0, import_node_path15.resolve)(inputCsv) : null;
31256
- const inPlaceCommitOutputPath = options.inPlace ? (await (0, import_promises6.lstat)(inputCsv)).isSymbolicLink() ? await (0, import_promises6.realpath)(inputCsv) : inPlaceFinalOutputPath : null;
31278
+ const inPlaceCommitOutputPath = options.inPlace ? (await (0, import_promises7.lstat)(inputCsv)).isSymbolicLink() ? await (0, import_promises7.realpath)(inputCsv) : inPlaceFinalOutputPath : null;
31257
31279
  const failureReportOutputPath = options.inPlace ? inPlaceFinalOutputPath : null;
31258
31280
  const enrichIssueFollowUpOutputPath = options.inPlace && inPlaceFinalOutputPath ? sidecarEnrichRowsExportPath(inPlaceFinalOutputPath) : null;
31259
31281
  let activeRunId2 = null;
@@ -31274,16 +31296,16 @@ function registerEnrichCommand(program) {
31274
31296
  return;
31275
31297
  }
31276
31298
  if (inPlaceTempDir) {
31277
- await (0, import_promises6.rm)(inPlaceTempDir, { recursive: true, force: true });
31299
+ await (0, import_promises7.rm)(inPlaceTempDir, { recursive: true, force: true });
31278
31300
  }
31279
- inPlaceTempDir = await (0, import_promises6.mkdtemp)(
31301
+ inPlaceTempDir = await (0, import_promises7.mkdtemp)(
31280
31302
  (0, import_node_path15.join)(
31281
31303
  (0, import_node_path15.dirname)(inPlaceCommitOutputPath ?? (0, import_node_path15.resolve)(inputCsv)),
31282
31304
  ".deepline-enrich-in-place-"
31283
31305
  )
31284
31306
  );
31285
31307
  inPlaceTempOutputPath = (0, import_node_path15.join)(inPlaceTempDir, "output.csv");
31286
- await (0, import_promises6.copyFile)((0, import_node_path15.resolve)(inputCsv), inPlaceTempOutputPath);
31308
+ await (0, import_promises7.copyFile)((0, import_node_path15.resolve)(inputCsv), inPlaceTempOutputPath);
31287
31309
  outputPath = inPlaceTempOutputPath;
31288
31310
  };
31289
31311
  const commitInPlaceOutput = async (exportResult) => {
@@ -31291,12 +31313,12 @@ function registerEnrichCommand(program) {
31291
31313
  return exportResult;
31292
31314
  }
31293
31315
  const committedTempDir = inPlaceTempDir;
31294
- await (0, import_promises6.rename)(inPlaceTempOutputPath, inPlaceCommitOutputPath);
31316
+ await (0, import_promises7.rename)(inPlaceTempOutputPath, inPlaceCommitOutputPath);
31295
31317
  inPlaceTempDir = null;
31296
31318
  inPlaceTempOutputPath = null;
31297
31319
  outputPath = inPlaceFinalOutputPath;
31298
31320
  if (committedTempDir) {
31299
- await (0, import_promises6.rm)(committedTempDir, { recursive: true, force: true });
31321
+ await (0, import_promises7.rm)(committedTempDir, { recursive: true, force: true });
31300
31322
  }
31301
31323
  if (!exportResult) {
31302
31324
  return null;
@@ -31309,7 +31331,7 @@ function registerEnrichCommand(program) {
31309
31331
  try {
31310
31332
  process.once("SIGINT", onSigint);
31311
31333
  process.once("SIGTERM", onSigterm);
31312
- await (0, import_promises6.writeFile)(tempPlay, playSource, "utf8");
31334
+ await (0, import_promises7.writeFile)(tempPlay, playSource, "utf8");
31313
31335
  if (options.inPlace) {
31314
31336
  await prepareInPlaceOutput();
31315
31337
  }
@@ -31503,11 +31525,11 @@ function registerEnrichCommand(program) {
31503
31525
  process.removeListener("SIGINT", onSigint);
31504
31526
  process.removeListener("SIGTERM", onSigterm);
31505
31527
  if (inPlaceTempDir) {
31506
- await (0, import_promises6.rm)(inPlaceTempDir, { recursive: true, force: true });
31528
+ await (0, import_promises7.rm)(inPlaceTempDir, { recursive: true, force: true });
31507
31529
  } else if (inPlaceTempOutputPath) {
31508
- await (0, import_promises6.rm)(inPlaceTempOutputPath, { force: true });
31530
+ await (0, import_promises7.rm)(inPlaceTempOutputPath, { force: true });
31509
31531
  }
31510
- await (0, import_promises6.rm)(tempDir, { recursive: true, force: true });
31532
+ await (0, import_promises7.rm)(tempDir, { recursive: true, force: true });
31511
31533
  }
31512
31534
  });
31513
31535
  }
@@ -32399,7 +32421,7 @@ Examples:
32399
32421
 
32400
32422
  // src/cli/commands/monitors.ts
32401
32423
  var import_node_fs14 = require("fs");
32402
- var import_promises7 = require("readline/promises");
32424
+ var import_promises8 = require("readline/promises");
32403
32425
  var JSON_OPTION_DESCRIPTION = "Emit JSON output. Also automatic when stdout is piped";
32404
32426
  function withJsonOption(command) {
32405
32427
  return command.option("--json", JSON_OPTION_DESCRIPTION);
@@ -33168,7 +33190,7 @@ async function handleMonitorsValidate(key, options) {
33168
33190
  if (result.valid === false) process.exitCode = 7;
33169
33191
  }
33170
33192
  async function confirmMonitorDelete(key, options) {
33171
- const rl = (0, import_promises7.createInterface)({
33193
+ const rl = (0, import_promises8.createInterface)({
33172
33194
  input: process.stdin,
33173
33195
  output: process.stderr
33174
33196
  });
@@ -40185,7 +40207,7 @@ Examples:
40185
40207
  }
40186
40208
 
40187
40209
  // src/cli/commands/workflow.ts
40188
- var import_promises8 = require("fs/promises");
40210
+ var import_promises9 = require("fs/promises");
40189
40211
  var import_node_path24 = require("path");
40190
40212
 
40191
40213
  // src/cli/workflow-to-play.ts
@@ -40403,7 +40425,7 @@ function readStatus(payload) {
40403
40425
  }
40404
40426
  async function readJsonOption(payload, file) {
40405
40427
  if (file) {
40406
- const raw = await (0, import_promises8.readFile)((0, import_node_path24.resolve)(file), "utf8");
40428
+ const raw = await (0, import_promises9.readFile)((0, import_node_path24.resolve)(file), "utf8");
40407
40429
  return JSON.parse(raw);
40408
40430
  }
40409
40431
  if (payload) {
@@ -40438,8 +40460,8 @@ async function transformOne(api, workflowId, outDir, publish) {
40438
40460
  { workflowName: workflow.name, version: revision.version }
40439
40461
  );
40440
40462
  const file = (0, import_node_path24.join)((0, import_node_path24.resolve)(outDir), `${compiled.playName}.play.ts`);
40441
- await (0, import_promises8.mkdir)((0, import_node_path24.dirname)(file), { recursive: true });
40442
- await (0, import_promises8.writeFile)(file, compiled.sourceCode, "utf8");
40463
+ await (0, import_promises9.mkdir)((0, import_node_path24.dirname)(file), { recursive: true });
40464
+ await (0, import_promises9.writeFile)(file, compiled.sourceCode, "utf8");
40443
40465
  let published = false;
40444
40466
  if (publish) {
40445
40467
  const code = await handlePlayPublish([file]);
@@ -41573,10 +41595,10 @@ function topLevelCommandKnown(program, commandName) {
41573
41595
  );
41574
41596
  }
41575
41597
  async function runPlayRunnerHealthCheck() {
41576
- const dir = await (0, import_promises9.mkdtemp)((0, import_node_path26.join)((0, import_node_os19.tmpdir)(), "deepline-health-play-"));
41598
+ const dir = await (0, import_promises10.mkdtemp)((0, import_node_path26.join)((0, import_node_os19.tmpdir)(), "deepline-health-play-"));
41577
41599
  const file = (0, import_node_path26.join)(dir, "health-check.play.ts");
41578
41600
  try {
41579
- await (0, import_promises9.writeFile)(
41601
+ await (0, import_promises10.writeFile)(
41580
41602
  file,
41581
41603
  [
41582
41604
  "import { definePlay } from 'deepline';",
@@ -41625,7 +41647,7 @@ async function runPlayRunnerHealthCheck() {
41625
41647
  }
41626
41648
  };
41627
41649
  } finally {
41628
- await (0, import_promises9.rm)(dir, { recursive: true, force: true });
41650
+ await (0, import_promises10.rm)(dir, { recursive: true, force: true });
41629
41651
  }
41630
41652
  }
41631
41653
  function pickString(value, ...keys) {
@@ -1033,7 +1033,7 @@ var SDK_RELEASE = {
1033
1033
  // 0.3.0 introduces raw-v2: complete scrubbed provider responses are
1034
1034
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
1035
1035
  // getters keep their established compatibility behavior.
1036
- version: "0.3.0",
1036
+ version: "0.3.2",
1037
1037
  updateSummary: "New raw-v2 tool responses preserve complete scrubbed provider envelopes at toolResponse.rawV2, including JSON:API included resources, links, cursors, and totals. Existing toolResponse.raw and declared getters keep working unchanged.",
1038
1038
  contracts: {
1039
1039
  api: {
@@ -1452,6 +1452,14 @@ var HttpClient = class {
1452
1452
  throw structuredToolError;
1453
1453
  }
1454
1454
  if (response.status === 429) {
1455
+ if (options?.preserveRateLimitResponse && isMonitorRateLimitEnvelope(parsed)) {
1456
+ throw new DeeplineError(
1457
+ apiErrorMessage(parsed, response.status),
1458
+ response.status,
1459
+ apiErrorCodeFromResponse(parsed),
1460
+ { response: parsed }
1461
+ );
1462
+ }
1455
1463
  const retryAfter = parseRetryAfter(response);
1456
1464
  lastError = new RateLimitError(retryAfter);
1457
1465
  if (attempt < maxRetries) {
@@ -1713,6 +1721,11 @@ function isProviderOriginatedHttpError(parsed) {
1713
1721
  const code = error?.code ?? response?.code;
1714
1722
  return failureOrigin === "provider" || code === "UPSTREAM_BLOCKED";
1715
1723
  }
1724
+ function isMonitorRateLimitEnvelope(parsed) {
1725
+ const response = asRecord(parsed);
1726
+ const error = asRecord(response?.error);
1727
+ return error?.code === "monitor_upstream_rate_limited" && typeof error.message === "string";
1728
+ }
1716
1729
  function apiErrorCodeFromResponse(parsed) {
1717
1730
  const response = asRecord(parsed);
1718
1731
  const error = asRecord(response?.error);
@@ -6489,7 +6502,13 @@ var DeeplineClient = class {
6489
6502
  "/api/v2/monitors/deploy",
6490
6503
  {
6491
6504
  method: "POST",
6492
- body: definition
6505
+ body: definition,
6506
+ // A provider can reject a create with a 429 after Deepline has begun
6507
+ // the lifecycle request. Do not replay a mutation or hide the server's
6508
+ // monitor-specific recovery guidance behind a generic transport retry.
6509
+ maxRetries: 0,
6510
+ exactUrlOnly: true,
6511
+ preserveRateLimitResponse: true
6493
6512
  }
6494
6513
  );
6495
6514
  if (definition.tool !== "deepline.analytics") return deployed;
@@ -11080,7 +11099,7 @@ import {
11080
11099
  mkdir as mkdir4,
11081
11100
  mkdtemp,
11082
11101
  readFile as readFile3,
11083
- realpath as realpath2,
11102
+ realpath as realpath3,
11084
11103
  rename,
11085
11104
  rm,
11086
11105
  stat as stat3,
@@ -12828,6 +12847,7 @@ import { tmpdir as tmpdir3 } from "os";
12828
12847
  import { dirname as dirname8, join as join9, resolve as resolve10 } from "path";
12829
12848
  import { fileURLToPath } from "url";
12830
12849
  import { existsSync as existsSync8 } from "fs";
12850
+ import { realpath as realpath2 } from "fs/promises";
12831
12851
 
12832
12852
  // ../shared_libs/plays/bundling/index.ts
12833
12853
  import { createHash as createHash2 } from "crypto";
@@ -27753,6 +27773,7 @@ var ENRICH_DEBUG_T0 = Date.now();
27753
27773
  var GENERATED_ENRICH_ROWS_TABLE_NAMESPACE = "deepline_enrich_rows";
27754
27774
  var SDK_ENRICH_TELEMETRY_TIMEOUT_MS = 1e4;
27755
27775
  var SDK_ENRICH_TELEMETRY_COMMAND_MAX_LENGTH = 2e4;
27776
+ var ENRICH_DEPRECATION_NOTICE = "DEPRECATION: `deepline enrich` is deprecated and no longer actively maintained. Use `deepline plays` for the supported workflow.\n";
27756
27777
  var ENRICH_AI_RUNTIME_COMPACT_TOOLS = /* @__PURE__ */ new Set([
27757
27778
  "ai_inference",
27758
27779
  "aiinference",
@@ -31222,6 +31243,7 @@ function registerEnrichCommand(program) {
31222
31243
  "--max-credits-per-run <credits>",
31223
31244
  "Set a hard Deepline-credit ceiling enforced by the runtime for this run."
31224
31245
  ).action(async (options, _command) => {
31246
+ process.stderr.write(ENRICH_DEPRECATION_NOTICE);
31225
31247
  if (currentEnrichArgs().some(
31226
31248
  (arg) => arg === "--no-open" || arg.startsWith("--no-open=")
31227
31249
  )) {
@@ -31313,7 +31335,7 @@ function registerEnrichCommand(program) {
31313
31335
  let inPlaceTempDir = null;
31314
31336
  let inPlaceTempOutputPath = null;
31315
31337
  const inPlaceFinalOutputPath = options.inPlace ? resolve12(inputCsv) : null;
31316
- const inPlaceCommitOutputPath = options.inPlace ? (await lstat2(inputCsv)).isSymbolicLink() ? await realpath2(inputCsv) : inPlaceFinalOutputPath : null;
31338
+ const inPlaceCommitOutputPath = options.inPlace ? (await lstat2(inputCsv)).isSymbolicLink() ? await realpath3(inputCsv) : inPlaceFinalOutputPath : null;
31317
31339
  const failureReportOutputPath = options.inPlace ? inPlaceFinalOutputPath : null;
31318
31340
  const enrichIssueFollowUpOutputPath = options.inPlace && inPlaceFinalOutputPath ? sidecarEnrichRowsExportPath(inPlaceFinalOutputPath) : null;
31319
31341
  let activeRunId2 = null;
package/dist/index.js CHANGED
@@ -783,7 +783,7 @@ var SDK_RELEASE = {
783
783
  // 0.3.0 introduces raw-v2: complete scrubbed provider responses are
784
784
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
785
785
  // getters keep their established compatibility behavior.
786
- version: "0.3.0",
786
+ version: "0.3.2",
787
787
  updateSummary: "New raw-v2 tool responses preserve complete scrubbed provider envelopes at toolResponse.rawV2, including JSON:API included resources, links, cursors, and totals. Existing toolResponse.raw and declared getters keep working unchanged.",
788
788
  contracts: {
789
789
  api: {
@@ -1202,6 +1202,14 @@ var HttpClient = class {
1202
1202
  throw structuredToolError;
1203
1203
  }
1204
1204
  if (response.status === 429) {
1205
+ if (options?.preserveRateLimitResponse && isMonitorRateLimitEnvelope(parsed)) {
1206
+ throw new DeeplineError(
1207
+ apiErrorMessage(parsed, response.status),
1208
+ response.status,
1209
+ apiErrorCodeFromResponse(parsed),
1210
+ { response: parsed }
1211
+ );
1212
+ }
1205
1213
  const retryAfter = parseRetryAfter(response);
1206
1214
  lastError = new RateLimitError(retryAfter);
1207
1215
  if (attempt < maxRetries) {
@@ -1463,6 +1471,11 @@ function isProviderOriginatedHttpError(parsed) {
1463
1471
  const code = error?.code ?? response?.code;
1464
1472
  return failureOrigin === "provider" || code === "UPSTREAM_BLOCKED";
1465
1473
  }
1474
+ function isMonitorRateLimitEnvelope(parsed) {
1475
+ const response = asRecord(parsed);
1476
+ const error = asRecord(response?.error);
1477
+ return error?.code === "monitor_upstream_rate_limited" && typeof error.message === "string";
1478
+ }
1466
1479
  function apiErrorCodeFromResponse(parsed) {
1467
1480
  const response = asRecord(parsed);
1468
1481
  const error = asRecord(response?.error);
@@ -6239,7 +6252,13 @@ var DeeplineClient = class {
6239
6252
  "/api/v2/monitors/deploy",
6240
6253
  {
6241
6254
  method: "POST",
6242
- body: definition
6255
+ body: definition,
6256
+ // A provider can reject a create with a 429 after Deepline has begun
6257
+ // the lifecycle request. Do not replay a mutation or hide the server's
6258
+ // monitor-specific recovery guidance behind a generic transport retry.
6259
+ maxRetries: 0,
6260
+ exactUrlOnly: true,
6261
+ preserveRateLimitResponse: true
6243
6262
  }
6244
6263
  );
6245
6264
  if (definition.tool !== "deepline.analytics") return deployed;
package/dist/index.mjs CHANGED
@@ -706,7 +706,7 @@ var SDK_RELEASE = {
706
706
  // 0.3.0 introduces raw-v2: complete scrubbed provider responses are
707
707
  // available at toolResponse.rawV2 while toolResponse.raw and all declared
708
708
  // getters keep their established compatibility behavior.
709
- version: "0.3.0",
709
+ version: "0.3.2",
710
710
  updateSummary: "New raw-v2 tool responses preserve complete scrubbed provider envelopes at toolResponse.rawV2, including JSON:API included resources, links, cursors, and totals. Existing toolResponse.raw and declared getters keep working unchanged.",
711
711
  contracts: {
712
712
  api: {
@@ -1125,6 +1125,14 @@ var HttpClient = class {
1125
1125
  throw structuredToolError;
1126
1126
  }
1127
1127
  if (response.status === 429) {
1128
+ if (options?.preserveRateLimitResponse && isMonitorRateLimitEnvelope(parsed)) {
1129
+ throw new DeeplineError(
1130
+ apiErrorMessage(parsed, response.status),
1131
+ response.status,
1132
+ apiErrorCodeFromResponse(parsed),
1133
+ { response: parsed }
1134
+ );
1135
+ }
1128
1136
  const retryAfter = parseRetryAfter(response);
1129
1137
  lastError = new RateLimitError(retryAfter);
1130
1138
  if (attempt < maxRetries) {
@@ -1386,6 +1394,11 @@ function isProviderOriginatedHttpError(parsed) {
1386
1394
  const code = error?.code ?? response?.code;
1387
1395
  return failureOrigin === "provider" || code === "UPSTREAM_BLOCKED";
1388
1396
  }
1397
+ function isMonitorRateLimitEnvelope(parsed) {
1398
+ const response = asRecord(parsed);
1399
+ const error = asRecord(response?.error);
1400
+ return error?.code === "monitor_upstream_rate_limited" && typeof error.message === "string";
1401
+ }
1389
1402
  function apiErrorCodeFromResponse(parsed) {
1390
1403
  const response = asRecord(parsed);
1391
1404
  const error = asRecord(response?.error);
@@ -6162,7 +6175,13 @@ var DeeplineClient = class {
6162
6175
  "/api/v2/monitors/deploy",
6163
6176
  {
6164
6177
  method: "POST",
6165
- body: definition
6178
+ body: definition,
6179
+ // A provider can reject a create with a 429 after Deepline has begun
6180
+ // the lifecycle request. Do not replay a mutation or hide the server's
6181
+ // monitor-specific recovery guidance behind a generic transport retry.
6182
+ maxRetries: 0,
6183
+ exactUrlOnly: true,
6184
+ preserveRateLimitResponse: true
6166
6185
  }
6167
6186
  );
6168
6187
  if (definition.tool !== "deepline.analytics") return deployed;
@@ -9,6 +9,7 @@ import { tmpdir as tmpdir2 } from "os";
9
9
  import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
10
10
  import { fileURLToPath } from "url";
11
11
  import { existsSync as existsSync2 } from "fs";
12
+ import { realpath as realpath2 } from "fs/promises";
12
13
 
13
14
  // ../shared_libs/plays/bundling/index.ts
14
15
  import { createHash } from "crypto";
@@ -5286,8 +5287,12 @@ function sha256(value) {
5286
5287
  function sourceIdentityPath(filePath, sourceIdentityRoot) {
5287
5288
  if (!sourceIdentityRoot) return filePath;
5288
5289
  const identityRoot = resolve(sourceIdentityRoot);
5289
- const logicalPath = relative(identityRoot, resolve(filePath));
5290
- if (!logicalPath || logicalPath === ".." || logicalPath.startsWith(`..${sep}`) || isAbsolute(logicalPath)) {
5290
+ const resolvedFilePath = resolve(filePath);
5291
+ const logicalPath = relative(identityRoot, resolvedFilePath);
5292
+ if (!logicalPath) {
5293
+ return basename(resolvedFilePath);
5294
+ }
5295
+ if (logicalPath === ".." || logicalPath.startsWith(`..${sep}`) || isAbsolute(logicalPath)) {
5291
5296
  return filePath;
5292
5297
  }
5293
5298
  return logicalPath.split(/[\\/]+/).join("/");
@@ -7201,10 +7206,17 @@ function createSdkPlayBundlingAdapter() {
7201
7206
  };
7202
7207
  }
7203
7208
  async function bundlePlayFile2(filePath, options = {}) {
7209
+ const localWorkspaceRoot = dirname3(resolve3(filePath));
7210
+ const sourceIdentityRoot = await realpath2(localWorkspaceRoot).catch(
7211
+ () => localWorkspaceRoot
7212
+ );
7204
7213
  const result = await bundlePlayFile(filePath, {
7205
7214
  target: options.target ?? defaultPlayBundleTarget(),
7206
7215
  exportName: options.exportName,
7207
- adapter: createSdkPlayBundlingAdapter()
7216
+ adapter: {
7217
+ ...createSdkPlayBundlingAdapter(),
7218
+ sourceIdentityRoot
7219
+ }
7208
7220
  });
7209
7221
  if (result.success)
7210
7222
  validatePlaySourceFilesHaveNoInlineSecrets(result.sourceFiles);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "GTM data CLI and TypeScript SDK for coding agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://code.deepline.com",