deepline 0.3.209 → 0.3.211

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 (25) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +28 -0
  3. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +95 -19
  4. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +3 -3
  5. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +1 -1
  6. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +1 -1
  7. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +21 -0
  8. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +11 -1
  9. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +17 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +303 -2
  11. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +38 -2
  12. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-scope.ts +10 -6
  13. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +37 -13
  14. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +4 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +4 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +7 -2
  17. package/dist/cli/index.js +1 -1
  18. package/dist/cli/index.mjs +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/index.mjs +1 -1
  21. package/dist/release.d.mts +1 -1
  22. package/dist/release.d.ts +1 -1
  23. package/dist/release.js +1 -1
  24. package/dist/release.mjs +1 -1
  25. package/package.json +1 -1
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.209',
203
+ version: '0.3.211',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -122,6 +122,7 @@ import {
122
122
  import {
123
123
  AllAdmittedRowsFailedError,
124
124
  isProviderExhaustedFailure,
125
+ ProviderAdmissionWaitTimeoutError,
125
126
  ProviderExhaustedError,
126
127
  } from './run-failure';
127
128
  import { PROVIDER_EXHAUSTED_MAX_WAIT_MS } from './governor/app-runtime-rate-state-backend';
@@ -18682,6 +18683,30 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18682
18683
  networkScope: null,
18683
18684
  },
18684
18685
  );
18686
+ const providerAdmissionTimeoutError = (
18687
+ error: ProviderAdmissionWaitTimeoutError,
18688
+ ) =>
18689
+ createToolHttpError(
18690
+ toolErrorSchemaVersion,
18691
+ error.message,
18692
+ null,
18693
+ 503,
18694
+ 'repairable',
18695
+ {
18696
+ toolId,
18697
+ provider: error.provider,
18698
+ operation: toolId,
18699
+ code: error.code,
18700
+ origin: 'deepline',
18701
+ category: 'rate_limit',
18702
+ retryable: true,
18703
+ statusCode: 503,
18704
+ requestId: null,
18705
+ retryAfterMs: null,
18706
+ networkKind: null,
18707
+ networkScope: null,
18708
+ },
18709
+ );
18685
18710
  const observeProviderWait = (retryAtMs: number) => {
18686
18711
  const startedAtMsByScope =
18687
18712
  (this.checkpoint.providerNoProgressStartedAtMsByScope ??= {});
@@ -19934,6 +19959,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19934
19959
  if (heartbeatFailure) {
19935
19960
  throw heartbeatFailure;
19936
19961
  }
19962
+ if (error instanceof ProviderAdmissionWaitTimeoutError) {
19963
+ throw providerAdmissionTimeoutError(error);
19964
+ }
19937
19965
  if (error instanceof ProviderExhaustedError) {
19938
19966
  const retryAtMs = Date.parse(error.retryAt);
19939
19967
  observeProviderWait(
@@ -20,6 +20,7 @@ import {
20
20
  type RateStateBackendKind,
21
21
  } from './rate-state-backend';
22
22
  import {
23
+ ProviderAdmissionWaitTimeoutError,
23
24
  ProviderExhaustedError,
24
25
  providerNameFromBucketId,
25
26
  } from '../run-failure';
@@ -36,12 +37,18 @@ import type { PlayRunnerRateStateValidateResult } from '../protocol';
36
37
  */
37
38
  export const PROVIDER_EXHAUSTED_MAX_WAIT_MS = 300_000;
38
39
  /**
39
- * Total time one acquirer may spend cycling through window-exhausted refills.
40
- * Each refill's sleep is bounded, but nothing bounded how many of them a row
41
- * could take; with several runs sharing one org's provider window a late
42
- * starter parked here for twenty minutes with its core idle.
40
+ * Max total duration of a cached rate-state hold before it is yielded or its
41
+ * stale local no-progress warning is cleared. This is distinct from the shorter
42
+ * local permit-admission budget below.
43
43
  */
44
44
  export const RATE_ACQUIRE_MAX_WAIT_MS = 300_000;
45
+ /**
46
+ * Total time one acquirer may spend waiting for a locally paced permit. This
47
+ * covers both empty-block refills and future-scheduled tickets. A spent budget
48
+ * is a repairable Deepline admission error, not another retry loop or a
49
+ * provider-owned Retry-After hold.
50
+ */
51
+ export const PROVIDER_ADMISSION_WAIT_BUDGET_MS = 120_000;
45
52
  const PROVIDER_NO_PROGRESS_WARNING_MS = 180_000;
46
53
 
47
54
  /**
@@ -91,6 +98,31 @@ export const APP_RUNTIME_RATE_STATE_LEASE_BLOCK_SIZE = 8;
91
98
  const LEASE_BLOCK_MIN_TTL_MS = 250;
92
99
  const LEASE_BLOCK_MAX_TTL_MS = 5_000;
93
100
  const MAX_ACQUIRE_SLEEP_MS = 5_000;
101
+
102
+ function abortReason(signal: AbortSignal): Error {
103
+ return signal.reason instanceof Error
104
+ ? signal.reason
105
+ : new Error('Rate-state acquire aborted.');
106
+ }
107
+
108
+ function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<void> {
109
+ if (signal?.aborted) return Promise.reject(abortReason(signal));
110
+ return new Promise((resolve, reject) => {
111
+ const cleanup = () => signal?.removeEventListener('abort', onAbort);
112
+ const timer = setTimeout(() => {
113
+ cleanup();
114
+ resolve();
115
+ }, ms);
116
+ const onAbort = () => {
117
+ clearTimeout(timer);
118
+ cleanup();
119
+ reject(abortReason(signal!));
120
+ };
121
+ signal?.addEventListener('abort', onAbort, { once: true });
122
+ if (signal?.aborted) onAbort();
123
+ });
124
+ }
125
+
94
126
  /**
95
127
  * Horizon used to size the next lease block from the rate actually in force.
96
128
  * Kept short so a block stays inside its own TTL and a stalled runner can never
@@ -156,7 +188,7 @@ interface PendingRelease {
156
188
  interface Options {
157
189
  schedulerSchema?: string | null;
158
190
  now?: () => number;
159
- sleep?: (ms: number) => Promise<void>;
191
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
160
192
  onStoreFailure?: (info: { bucketId: string; error: string }) => void;
161
193
  /** Override the transport (tests inject a counting/faulting port). */
162
194
  port?: AppRuntimeRatePort;
@@ -177,7 +209,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
177
209
  private readonly port: AppRuntimeRatePort;
178
210
  private readonly schedulerSchema: string | null;
179
211
  private readonly now: () => number;
180
- private readonly sleep: (ms: number) => Promise<void>;
212
+ private readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
181
213
  private readonly jitter: () => number;
182
214
  private readonly onStoreFailure: (info: {
183
215
  bucketId: string;
@@ -238,9 +270,7 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
238
270
  this.port = options.port ?? contextRatePort(context);
239
271
  this.schedulerSchema = options.schedulerSchema?.trim() || null;
240
272
  this.now = options.now ?? (() => Date.now());
241
- this.sleep =
242
- options.sleep ??
243
- ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
273
+ this.sleep = options.sleep ?? sleepWithAbort;
244
274
  this.jitter = options.jitter ?? Math.random;
245
275
  this.onStoreFailure =
246
276
  options.onStoreFailure ??
@@ -412,20 +442,47 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
412
442
  const localPermit = this.drawFromBlock(bucketId, laneKey);
413
443
  if (localPermit) {
414
444
  const waitMs = localPermit.scheduledAtMs - this.now();
415
- if (waitMs > 0) await this.sleep(waitMs);
445
+ const elapsedMs = Math.max(0, this.now() - acquireStartedAt);
446
+ const remainingWaitMs = Math.max(
447
+ 0,
448
+ PROVIDER_ADMISSION_WAIT_BUDGET_MS - elapsedMs,
449
+ );
450
+ if (
451
+ elapsedMs >= PROVIDER_ADMISSION_WAIT_BUDGET_MS ||
452
+ waitMs > remainingWaitMs
453
+ ) {
454
+ await this.cancelUndispatchedPermit(localPermit);
455
+ throw new ProviderAdmissionWaitTimeoutError({
456
+ provider: providerNameFromBucketId(bucketId),
457
+ waitBudgetMs: PROVIDER_ADMISSION_WAIT_BUDGET_MS,
458
+ });
459
+ }
460
+ try {
461
+ if (waitMs > 0) await this.sleep(waitMs, signal);
462
+ } catch (error) {
463
+ await this.cancelUndispatchedPermit(localPermit);
464
+ throw error;
465
+ }
416
466
  if (signal?.aborted) {
417
- localPermit.permit.release();
418
- throw signal.reason instanceof Error
419
- ? signal.reason
420
- : new Error('Rate-state acquire aborted.');
467
+ await this.cancelUndispatchedPermit(localPermit);
468
+ throw abortReason(signal);
469
+ }
470
+ if (
471
+ this.now() - acquireStartedAt >= PROVIDER_ADMISSION_WAIT_BUDGET_MS
472
+ ) {
473
+ await this.cancelUndispatchedPermit(localPermit);
474
+ throw new ProviderAdmissionWaitTimeoutError({
475
+ provider: providerNameFromBucketId(bucketId),
476
+ waitBudgetMs: PROVIDER_ADMISSION_WAIT_BUDGET_MS,
477
+ });
421
478
  }
422
479
  return localPermit.permit;
423
480
  }
424
481
  const waitedMs = this.now() - acquireStartedAt;
425
- if (waitedMs >= RATE_ACQUIRE_MAX_WAIT_MS) {
426
- throw new ProviderExhaustedError({
482
+ if (waitedMs >= PROVIDER_ADMISSION_WAIT_BUDGET_MS) {
483
+ throw new ProviderAdmissionWaitTimeoutError({
427
484
  provider: providerNameFromBucketId(bucketId),
428
- retryAtMs: this.now() + MAX_ACQUIRE_SLEEP_MS,
485
+ waitBudgetMs: PROVIDER_ADMISSION_WAIT_BUDGET_MS,
429
486
  });
430
487
  }
431
488
  // Count this acquirer as waiting on the refill so the single-flight refill
@@ -1389,7 +1446,11 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1389
1446
  private drawFromBlock(
1390
1447
  bucketId: string,
1391
1448
  laneKey: string,
1392
- ): { permit: PacingPermit; scheduledAtMs: number } | null {
1449
+ ): {
1450
+ permit: PacingPermit;
1451
+ scheduledAtMs: number;
1452
+ reservationId: string | null;
1453
+ } | null {
1393
1454
  const block = this.liveBlock(bucketId, laneKey);
1394
1455
  if (!block) return null;
1395
1456
  // An emptied block stays until it expires: the next refill merges into it,
@@ -1399,9 +1460,24 @@ export class AppRuntimeRateStateBackend implements RateStateBackend {
1399
1460
  return {
1400
1461
  permit: this.permitFor(bucketId, laneKey, block, scheduled),
1401
1462
  scheduledAtMs: scheduled[0],
1463
+ reservationId: scheduled[2],
1402
1464
  };
1403
1465
  }
1404
1466
 
1467
+ private async cancelUndispatchedPermit(input: {
1468
+ permit: PacingPermit;
1469
+ reservationId: string | null;
1470
+ }): Promise<void> {
1471
+ if (input.reservationId) {
1472
+ await input.permit.cancel();
1473
+ return;
1474
+ }
1475
+ // Older gateways may grant a permit without a refundable reservation id.
1476
+ // Still release any concurrency lease; losing one rate token is safer than
1477
+ // manufacturing an immediate permit during a rolling deploy.
1478
+ input.permit.release();
1479
+ }
1480
+
1405
1481
  private drainBlocksForBucket(
1406
1482
  bucketId: string,
1407
1483
  beforeRetryAtMs?: number,
@@ -1546,7 +1622,7 @@ function withAdaptiveMaxRps(rules: readonly PacingRule[]): PacingRule[] {
1546
1622
  // This one shared default rule is synthesized for unknown providers whose
1547
1623
  // credential bucket spans operations. Keep the generic policy unchanged
1548
1624
  // for local/coordinator backends, but let Postgres start a new provider
1549
- // scope with one probe/sec before learning its sustainable rate.
1625
+ // scope at 10 RPS before learning its sustainable rate.
1550
1626
  const seed =
1551
1627
  rule.ruleId === 'default:provider-scope'
1552
1628
  ? {
@@ -511,9 +511,9 @@ export function createPlayExecutionGovernor(
511
511
  if (!declaredPacing) {
512
512
  // bucketId is the authenticated quota scope. Unknown tools sharing a
513
513
  // credential must not each mint an independent copy of the default rate.
514
- // Start its one shared lane at one probe/sec; the DB-owned AIMD quickly
515
- // ramps on sustained success and reacts to Retry-After if the provider
516
- // tells us the quota is lower.
514
+ // Start its one shared lane at 10 RPS, matching the generic fallback.
515
+ // The DB-owned AIMD ramps on sustained success and reacts to
516
+ // Retry-After if the provider tells us the quota is lower.
517
517
  pacing.rules = pacing.rules.map((rule) => ({
518
518
  ...rule,
519
519
  ruleId: 'default:provider-scope',
@@ -189,7 +189,7 @@ export const SHARED_EXECUTION_POLICY: ResolvedExecutionPolicy = {
189
189
  },
190
190
  pacing: {
191
191
  // Keep the generic fallback rate. The shared Postgres backend separately
192
- // cold-starts unknown provider scopes at one probe/sec.
192
+ // cold-starts unknown provider scopes at 10 RPS.
193
193
  // Declared providers (rate-limit-definitions.ts) keep their own rates.
194
194
  defaultProviderRequestsPerSecond: 10,
195
195
  // This is the scheduler's resident-call envelope, not an RPS target.
@@ -15,7 +15,7 @@
15
15
  export const DEFAULT_ADVISORY_PROVIDER_MAX_RPS = 100;
16
16
 
17
17
  /** Cold-start rate for an unknown provider bucket shared across a fleet key. */
18
- export const DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS = 1;
18
+ export const DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS = 10;
19
19
 
20
20
  /** AIMD state cooldown cap; provider Retry-After holds remain uncapped. */
21
21
  export const PROVIDER_AIMD_COOLDOWN_CAP_MS = 45 * 60 * 1_000;
@@ -240,6 +240,27 @@ export function providerExhaustedMessage(input: {
240
240
  );
241
241
  }
242
242
 
243
+ /**
244
+ * The local rate governor could not offer a provider permit inside its bounded
245
+ * admission window. No provider request was dispatched, so this is a Deepline
246
+ * pacing failure rather than a provider Retry-After hold.
247
+ */
248
+ export class ProviderAdmissionWaitTimeoutError extends Error {
249
+ readonly code = 'PROVIDER_ADMISSION_TIMEOUT';
250
+ readonly provider: string;
251
+ readonly waitBudgetMs: number;
252
+
253
+ constructor(input: { provider: string; waitBudgetMs: number }) {
254
+ const minutes = Math.max(1, Math.ceil(input.waitBudgetMs / 60_000));
255
+ super(
256
+ `PROVIDER_ADMISSION_TIMEOUT: Deepline could not grant a provider permit for ${input.provider} within its ${minutes}-minute wait budget. No provider request was sent.`,
257
+ );
258
+ this.name = 'ProviderAdmissionWaitTimeoutError';
259
+ this.provider = input.provider;
260
+ this.waitBudgetMs = input.waitBudgetMs;
261
+ }
262
+ }
263
+
243
264
  /**
244
265
  * Control signal from the DB-authoritative pacer when a provider hold exceeds
245
266
  * the in-process wait threshold. The next provider call is not dispatched.
@@ -42,6 +42,7 @@ import type {
42
42
  import {
43
43
  buildDaytonaRuntimeWatchdogSource,
44
44
  PLAY_RUNNER_UV_THREADPOOL_SIZE,
45
+ RUNNER_MEMORY_CGROUP_ROOT,
45
46
  RUNTIME_SANDBOX_MEMORY_ABORT_REASON,
46
47
  } from './daytona-runtime-watchdog';
47
48
 
@@ -72,6 +73,9 @@ export type StagedDaytonaPayload = {
72
73
  command: string;
73
74
  outputPath: string;
74
75
  exitCodePath: string;
76
+ oomKillBaselinePath: string;
77
+ runnerPidPath: string;
78
+ runtimeStartedPath: string;
75
79
  runtimeCompletedPath: string;
76
80
  terminationDiagnosticPath: string;
77
81
  progressEventPath: string;
@@ -656,6 +660,7 @@ export async function stageRunnerPayload(input: {
656
660
  const crashPusherLogPath = `${workDir}/deepline-play-crash-terminal-${randomUUID()}.log`;
657
661
  const exitCodePath = `${workDir}/deepline-play-exit-${randomUUID()}.txt`;
658
662
  const oomKillBaselinePath = `${workDir}/deepline-play-oom-kill-${randomUUID()}.txt`;
663
+ const runnerPidPath = `${workDir}/deepline-play-runner-pid-${randomUUID()}.txt`;
659
664
  const runtimeStartedPath = `${workDir}/deepline-play-runtime-started-${randomUUID()}.json`;
660
665
  const runtimeCompletedPath = `${workDir}/deepline-play-runtime-completed-${randomUUID()}.json`;
661
666
  const runtimeLimitMarkerPath = `${workDir}/deepline-play-runtime-limit-${randomUUID()}.txt`;
@@ -691,6 +696,8 @@ export async function stageRunnerPayload(input: {
691
696
  shellQuote(terminationDiagnosticPath),
692
697
  shellQuote(memoryLedgerPath),
693
698
  shellQuote(memoryPressurePath),
699
+ shellQuote(RUNNER_MEMORY_CGROUP_ROOT),
700
+ shellQuote(runnerPidPath),
694
701
  ].join(' ');
695
702
  const runnerCommand = `${nodeMaterializePayloadCommand({
696
703
  envelopePath,
@@ -708,13 +715,16 @@ export async function stageRunnerPayload(input: {
708
715
  // Its own retry diagnostics are kept in a separate log: appending them to
709
716
  // `outputPath` after a near-gateway-sized result could hide that result from
710
717
  // a later bounded-tail recovery read.
711
- const command = `rm -f ${shellQuote(outputPath)} ${shellQuote(crashPusherLogPath)} ${shellQuote(exitCodePath)} ${shellQuote(oomKillBaselinePath)} ${shellQuote(runtimeStartedPath)} ${shellQuote(runtimeCompletedPath)} ${shellQuote(runtimeLimitMarkerPath)} ${shellQuote(terminationDiagnosticPath)} ${shellQuote(memoryLedgerPath)} ${shellQuote(`${memoryLedgerPath}.tmp`)} ${shellQuote(memoryPressurePath)} ${shellQuote(progressEventPath)} ${shellQuote(`${progressEventPath}.*`)} ${shellQuote(startupDiagnosticPath)}; ( awk '$1 == "oom_kill" { print $2 }' /sys/fs/cgroup/memory.events 2>/dev/null || true ) > ${shellQuote(oomKillBaselinePath)}; ( ${runnerCommand} ) > ${shellQuote(outputPath)} 2>&1; code=$?; printf '%s' "$code" > ${shellQuote(exitCodePath)}; ${runtimeReliabilityEnv}node ${shellQuote(crashPusherPath)} ${shellQuote(configPath)} "$code" ${shellQuote(outputPath)} ${shellQuote(runtimeLimitMarkerPath)} ${shellQuote(oomKillBaselinePath)} /sys/fs/cgroup/memory.events ${shellQuote(terminationDiagnosticPath)} > ${shellQuote(crashPusherLogPath)} 2>&1 || true; printf 'deepline runner output captured: %s\\n' ${shellQuote(outputPath)}; exit "$code"`;
718
+ const command = `rm -f ${shellQuote(outputPath)} ${shellQuote(crashPusherLogPath)} ${shellQuote(exitCodePath)} ${shellQuote(oomKillBaselinePath)} ${shellQuote(runnerPidPath)} ${shellQuote(runtimeStartedPath)} ${shellQuote(runtimeCompletedPath)} ${shellQuote(runtimeLimitMarkerPath)} ${shellQuote(terminationDiagnosticPath)} ${shellQuote(memoryLedgerPath)} ${shellQuote(`${memoryLedgerPath}.tmp`)} ${shellQuote(memoryPressurePath)} ${shellQuote(progressEventPath)} ${shellQuote(`${progressEventPath}.*`)} ${shellQuote(startupDiagnosticPath)}; ( awk '$1 == "oom_kill" { print $2 }' /sys/fs/cgroup/memory.events 2>/dev/null || true ) > ${shellQuote(oomKillBaselinePath)}; ( ${runnerCommand} ) > ${shellQuote(outputPath)} 2>&1; code=$?; printf '%s' "$code" > ${shellQuote(exitCodePath)}; ${runtimeReliabilityEnv}node ${shellQuote(crashPusherPath)} ${shellQuote(configPath)} "$code" ${shellQuote(outputPath)} ${shellQuote(runtimeLimitMarkerPath)} ${shellQuote(oomKillBaselinePath)} /sys/fs/cgroup/memory.events ${shellQuote(terminationDiagnosticPath)} > ${shellQuote(crashPusherLogPath)} 2>&1 || true; printf 'deepline runner output captured: %s\\n' ${shellQuote(outputPath)}; exit "$code"`;
712
719
 
713
720
  return {
714
721
  workDir: input.workDir,
715
722
  command,
716
723
  outputPath,
717
724
  exitCodePath,
725
+ oomKillBaselinePath,
726
+ runnerPidPath,
727
+ runtimeStartedPath,
718
728
  runtimeCompletedPath,
719
729
  terminationDiagnosticPath,
720
730
  progressEventPath,
@@ -126,7 +126,7 @@ export const RUNTIME_SANDBOX_MEMORY_ABORT_REASON =
126
126
  * 1 runnerPath, 2 configPath, 3 runtimeStartedPath, 4 runtimeCompletedPath,
127
127
  * 5 limitMs, 6 startupLimitMs, 7 terminalGraceMs, 8 runtimeLimitMarkerPath,
128
128
  * 9 terminationDiagnosticPath, 10 memoryLedgerPath, 11 memoryPressurePath,
129
- * 12 cgroupRoot.
129
+ * 12 cgroupRoot, 13 runnerPidPath.
130
130
  */
131
131
  export function buildDaytonaRuntimeWatchdogSource(): string {
132
132
  return `
@@ -144,6 +144,7 @@ const terminationDiagnosticPath = process.argv[9] || '';
144
144
  const memoryLedgerPath = process.argv[10] || '';
145
145
  const memoryPressurePath = process.argv[11] || '';
146
146
  const cgroupRoot = process.argv[12] || ${JSON.stringify(RUNNER_MEMORY_CGROUP_ROOT)};
147
+ const runnerPidPath = process.argv[13] || '';
147
148
  const runtimeLimitSeconds = Math.max(1, Math.ceil(limitMs / 1_000));
148
149
  const child = spawn(
149
150
  process.execPath,
@@ -161,6 +162,21 @@ const child = spawn(
161
162
  stdio: 'inherit',
162
163
  },
163
164
  );
165
+ if (runnerPidPath && Number.isSafeInteger(child.pid)) {
166
+ try {
167
+ const rawStat = fs.readFileSync('/proc/' + child.pid + '/stat', 'utf8');
168
+ const close = rawStat.lastIndexOf(')');
169
+ const fields = close >= 0 ? rawStat.slice(close + 1).trim().split(/\\s+/) : [];
170
+ const startTimeTicks = Number.parseInt(fields[19], 10);
171
+ if (Number.isSafeInteger(startTimeTicks) && startTimeTicks >= 0) {
172
+ fs.writeFileSync(
173
+ runnerPidPath,
174
+ JSON.stringify({ pid: child.pid, startTimeTicks }),
175
+ { flag: 'wx' },
176
+ );
177
+ }
178
+ } catch {}
179
+ }
164
180
  let timedOut = false;
165
181
  let startupTimedOut = false;
166
182
  let terminalFlushTimedOut = false;
@@ -46,6 +46,7 @@ import {
46
46
  } from './daytona-session-execution';
47
47
  import { RUNTIME_RELIABILITY_POLICY } from '../../runtime-reliability-policy';
48
48
  import { daytonaSandboxStartAdmission } from './daytona-start-admission';
49
+ import { RUNNER_MEMORY_CGROUP_ROOT } from './daytona-runtime-watchdog';
49
50
 
50
51
  const DAYTONA_COMMAND_RECOVERY_TIMEOUT_MS =
51
52
  RUNTIME_RELIABILITY_POLICY.sandbox.daytonaCommandRecoveryTimeoutMs;
@@ -94,13 +95,37 @@ export type DaytonaCrashDiagnostic = {
94
95
  exitCodeFile: number | null;
95
96
  sessionExitCode: number | null;
96
97
  outputBytes: number | null;
98
+ outputFileAvailable?: boolean | null;
99
+ exitCodeFileAvailable?: boolean | null;
100
+ terminationFileAvailable?: boolean | null;
101
+ runtimeStarted?: boolean | null;
102
+ runtimeCompleted?: boolean | null;
97
103
  tailBytes: number;
98
104
  tailTruncated: boolean;
99
105
  signals: string[];
100
106
  unclassifiedLineCount: number;
101
107
  termination: DaytonaTerminationDiagnostic | null;
108
+ resourceSnapshot?: DaytonaCrashResourceSnapshot | null;
102
109
  collectionFailure: DaytonaLookupFailure | null;
103
110
  };
111
+ export type DaytonaCrashResourceSnapshot = {
112
+ memorySource: 'cgroup_v1' | 'cgroup_v1_nested' | 'cgroup_v2' | null;
113
+ memoryUsageBytes: number | null;
114
+ memoryLimitBytes: number | null;
115
+ memoryUsageRatio: number | null;
116
+ memoryOomCount: number | null;
117
+ memoryOomKillCount: number | null;
118
+ memoryOomKillDelta: number | null;
119
+ memoryOomGroupKillCount: number | null;
120
+ memoryHighCount: number | null;
121
+ memoryMaxCount: number | null;
122
+ cgroupCpuUsageUsecDelta: number | null;
123
+ cgroupCpuSampleWindowMs: number | null;
124
+ runnerPresent: boolean | null;
125
+ runnerState: string | null;
126
+ runnerRssPages: number | null;
127
+ runnerCpuTicksDelta: number | null;
128
+ };
104
129
  export type DaytonaTerminationDiagnostic = {
105
130
  schemaVersion: 1;
106
131
  reason:
@@ -185,6 +210,56 @@ function parseDaytonaTerminationDiagnostic(
185
210
  }
186
211
  return diagnostic;
187
212
  }
213
+
214
+ export function parseDaytonaCrashResourceSnapshot(
215
+ value: unknown,
216
+ ): DaytonaCrashResourceSnapshot | null {
217
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
218
+ const candidate = value as Record<string, unknown>;
219
+ const nonNegativeNumber = (key: string): number | null => {
220
+ const value = candidate[key];
221
+ return typeof value === 'number' &&
222
+ Number.isSafeInteger(value) &&
223
+ value >= 0
224
+ ? value
225
+ : null;
226
+ };
227
+ const ratio = candidate.memoryUsageRatio;
228
+ const source = candidate.memorySource;
229
+ return {
230
+ memorySource:
231
+ source === 'cgroup_v1' ||
232
+ source === 'cgroup_v1_nested' ||
233
+ source === 'cgroup_v2'
234
+ ? source
235
+ : null,
236
+ memoryUsageBytes: nonNegativeNumber('memoryUsageBytes'),
237
+ memoryLimitBytes: nonNegativeNumber('memoryLimitBytes'),
238
+ memoryUsageRatio:
239
+ typeof ratio === 'number' && Number.isFinite(ratio) && ratio >= 0
240
+ ? ratio
241
+ : null,
242
+ memoryOomCount: nonNegativeNumber('memoryOomCount'),
243
+ memoryOomKillCount: nonNegativeNumber('memoryOomKillCount'),
244
+ memoryOomKillDelta: nonNegativeNumber('memoryOomKillDelta'),
245
+ memoryOomGroupKillCount: nonNegativeNumber('memoryOomGroupKillCount'),
246
+ memoryHighCount: nonNegativeNumber('memoryHighCount'),
247
+ memoryMaxCount: nonNegativeNumber('memoryMaxCount'),
248
+ cgroupCpuUsageUsecDelta: nonNegativeNumber('cgroupCpuUsageUsecDelta'),
249
+ cgroupCpuSampleWindowMs: nonNegativeNumber('cgroupCpuSampleWindowMs'),
250
+ runnerPresent:
251
+ typeof candidate.runnerPresent === 'boolean'
252
+ ? candidate.runnerPresent
253
+ : null,
254
+ runnerState:
255
+ typeof candidate.runnerState === 'string' &&
256
+ /^[A-Za-z]$/.test(candidate.runnerState)
257
+ ? candidate.runnerState
258
+ : null,
259
+ runnerRssPages: nonNegativeNumber('runnerRssPages'),
260
+ runnerCpuTicksDelta: nonNegativeNumber('runnerCpuTicksDelta'),
261
+ };
262
+ }
188
263
  type DaytonaUploadAttemptTiming = {
189
264
  attempt: number;
190
265
  sandboxId: string;
@@ -368,6 +443,189 @@ export function extractDaytonaCrashDiagnosticSignals(outputTail: string): {
368
443
  return { signals, unclassifiedLineCount };
369
444
  }
370
445
 
446
+ export function buildDaytonaCrashDiagnosticSnapshotSource(): string {
447
+ return String.raw`
448
+ const fs = require('node:fs');
449
+ (async () => {
450
+ const output = process.argv[1];
451
+ const exit = process.argv[2];
452
+ const max = Number(process.argv[3]);
453
+ const terminationPath = process.argv[4] || '';
454
+ const startedPath = process.argv[5] || '';
455
+ const completedPath = process.argv[6] || '';
456
+ const pidPath = process.argv[7] || '';
457
+ const oomBaselinePath = process.argv[8] || '';
458
+ const cgroupRoot = process.argv[9] || '/sys/fs/cgroup';
459
+ const read = (path) => {
460
+ try { return fs.readFileSync(path, 'utf8'); } catch { return null; }
461
+ };
462
+ const exists = (path) => {
463
+ try { return fs.statSync(path).isFile(); } catch { return false; }
464
+ };
465
+ let outputStat = null;
466
+ try { outputStat = fs.statSync(output); } catch {}
467
+ let tail = Buffer.alloc(0);
468
+ if (outputStat) {
469
+ try {
470
+ const start = Math.max(0, outputStat.size - max);
471
+ const fd = fs.openSync(output, 'r');
472
+ tail = Buffer.alloc(outputStat.size - start);
473
+ fs.readSync(fd, tail, 0, tail.length, start);
474
+ fs.closeSync(fd);
475
+ } catch {}
476
+ }
477
+ const exitRaw = read(exit);
478
+ let exitCode = null;
479
+ if (exitRaw !== null) {
480
+ const value = Number.parseInt(exitRaw.trim(), 10);
481
+ if (Number.isFinite(value)) exitCode = value;
482
+ }
483
+ let termination = null;
484
+ try {
485
+ const value = JSON.parse(read(terminationPath) || 'null');
486
+ if (value && typeof value === 'object') termination = value;
487
+ } catch {}
488
+ const memoryPaths = [
489
+ ['cgroup_v2', cgroupRoot + '/memory.current', cgroupRoot + '/memory.max'],
490
+ ['cgroup_v1', cgroupRoot + '/memory.usage_in_bytes', cgroupRoot + '/memory.limit_in_bytes'],
491
+ ['cgroup_v1_nested', cgroupRoot + '/memory/memory.usage_in_bytes', cgroupRoot + '/memory/memory.limit_in_bytes'],
492
+ ];
493
+ const readNumber = (path) => {
494
+ const raw = read(path);
495
+ if (raw === null || !raw.trim() || raw.trim() === 'max') return null;
496
+ const value = Number.parseInt(raw.trim(), 10);
497
+ return Number.isSafeInteger(value) && value >= 0 ? value : null;
498
+ };
499
+ const readMemory = () => {
500
+ for (const [source, usagePath, limitPath] of memoryPaths) {
501
+ const usage = readNumber(usagePath);
502
+ const limit = readNumber(limitPath);
503
+ if (usage !== null && limit !== null && limit > 0 && limit < 2 ** 50) {
504
+ return {
505
+ memorySource: source,
506
+ memoryUsageBytes: usage,
507
+ memoryLimitBytes: limit,
508
+ memoryUsageRatio: usage / limit,
509
+ };
510
+ }
511
+ }
512
+ return {
513
+ memorySource: null,
514
+ memoryUsageBytes: null,
515
+ memoryLimitBytes: null,
516
+ memoryUsageRatio: null,
517
+ };
518
+ };
519
+ const readEvents = () => {
520
+ const result = {};
521
+ for (const line of (read(cgroupRoot + '/memory.events') || '').split(/\r?\n/)) {
522
+ const match = line.match(/^(oom|oom_kill|oom_group_kill|high|max)\s+(\d+)$/);
523
+ if (match) result[match[1]] = Number.parseInt(match[2], 10);
524
+ }
525
+ return result;
526
+ };
527
+ const readCpu = () => {
528
+ const line = (read(cgroupRoot + '/cpu.stat') || '')
529
+ .split(/\r?\n/)
530
+ .find((entry) => /^usage_usec\s+\d+$/.test(entry));
531
+ return line ? Number.parseInt(line.split(/\s+/)[1], 10) : null;
532
+ };
533
+ const readRunner = () => {
534
+ let identity = null;
535
+ try { identity = JSON.parse(read(pidPath) || 'null'); } catch {}
536
+ const pid = identity && identity.pid;
537
+ const expectedStartTimeTicks = identity && identity.startTimeTicks;
538
+ if (
539
+ !Number.isSafeInteger(pid) ||
540
+ pid < 1 ||
541
+ !Number.isSafeInteger(expectedStartTimeTicks) ||
542
+ expectedStartTimeTicks < 0
543
+ ) return null;
544
+ const rawStat = read('/proc/' + pid + '/stat');
545
+ if (rawStat === null) return { present: false };
546
+ const close = rawStat.lastIndexOf(')');
547
+ if (close < 0) return { present: true };
548
+ const fields = rawStat.slice(close + 1).trim().split(/\s+/);
549
+ const startTimeTicks = Number.parseInt(fields[19], 10);
550
+ if (startTimeTicks !== expectedStartTimeTicks) return null;
551
+ const userTicks = Number.parseInt(fields[11], 10);
552
+ const systemTicks = Number.parseInt(fields[12], 10);
553
+ const rssRaw = (read('/proc/' + pid + '/statm') || '').trim().split(/\s+/)[1];
554
+ const rssPages = Number.parseInt(rssRaw || '', 10);
555
+ return {
556
+ present: true,
557
+ state: fields[0] || null,
558
+ cpuTicks:
559
+ Number.isFinite(userTicks) && Number.isFinite(systemTicks)
560
+ ? userTicks + systemTicks
561
+ : null,
562
+ rssPages: Number.isSafeInteger(rssPages) && rssPages >= 0 ? rssPages : null,
563
+ };
564
+ };
565
+ const beforeCpu = readCpu();
566
+ const beforeRunner = readRunner();
567
+ const sampleStarted = Date.now();
568
+ await new Promise((resolve) => setTimeout(resolve, 200));
569
+ const afterCpu = readCpu();
570
+ const afterRunner = readRunner();
571
+ const sampleWindowMs = Date.now() - sampleStarted;
572
+ const events = readEvents();
573
+ const baseline = readNumber(oomBaselinePath);
574
+ const resourceSnapshot = {
575
+ ...readMemory(),
576
+ memoryOomCount: Number.isSafeInteger(events.oom) ? events.oom : null,
577
+ memoryOomKillCount: Number.isSafeInteger(events.oom_kill) ? events.oom_kill : null,
578
+ memoryOomKillDelta:
579
+ Number.isSafeInteger(events.oom_kill) &&
580
+ baseline !== null &&
581
+ events.oom_kill >= baseline
582
+ ? events.oom_kill - baseline
583
+ : null,
584
+ memoryOomGroupKillCount:
585
+ Number.isSafeInteger(events.oom_group_kill) ? events.oom_group_kill : null,
586
+ memoryHighCount: Number.isSafeInteger(events.high) ? events.high : null,
587
+ memoryMaxCount: Number.isSafeInteger(events.max) ? events.max : null,
588
+ cgroupCpuUsageUsecDelta:
589
+ Number.isSafeInteger(beforeCpu) &&
590
+ Number.isSafeInteger(afterCpu) &&
591
+ afterCpu >= beforeCpu
592
+ ? afterCpu - beforeCpu
593
+ : null,
594
+ cgroupCpuSampleWindowMs: sampleWindowMs,
595
+ runnerPresent: afterRunner ? afterRunner.present : null,
596
+ runnerState:
597
+ afterRunner && typeof afterRunner.state === 'string'
598
+ ? afterRunner.state
599
+ : null,
600
+ runnerRssPages:
601
+ afterRunner && Number.isSafeInteger(afterRunner.rssPages)
602
+ ? afterRunner.rssPages
603
+ : null,
604
+ runnerCpuTicksDelta:
605
+ beforeRunner &&
606
+ afterRunner &&
607
+ Number.isSafeInteger(beforeRunner.cpuTicks) &&
608
+ Number.isSafeInteger(afterRunner.cpuTicks) &&
609
+ afterRunner.cpuTicks >= beforeRunner.cpuTicks
610
+ ? afterRunner.cpuTicks - beforeRunner.cpuTicks
611
+ : null,
612
+ };
613
+ process.stdout.write(JSON.stringify({
614
+ outputBytes: outputStat ? outputStat.size : null,
615
+ outputFileAvailable: Boolean(outputStat),
616
+ exitCodeFileAvailable: exitRaw !== null,
617
+ tail: tail.toString('base64'),
618
+ exitCode,
619
+ terminationFileAvailable: exists(terminationPath),
620
+ termination,
621
+ runtimeStarted: exists(startedPath),
622
+ runtimeCompleted: exists(completedPath),
623
+ resourceSnapshot,
624
+ }));
625
+ })().catch(() => { process.exitCode = 1; });
626
+ `;
627
+ }
628
+
371
629
  /**
372
630
  * Read bounded crash evidence while the failed sandbox still exists. This is
373
631
  * operator telemetry only: it never becomes a run result or customer log.
@@ -378,6 +636,10 @@ export async function readDetachedDaytonaCrashDiagnostic(input: {
378
636
  cmdId: string;
379
637
  outputPath: string;
380
638
  exitCodePath: string;
639
+ oomKillBaselinePath?: string;
640
+ runnerPidPath?: string;
641
+ runtimeStartedPath?: string;
642
+ runtimeCompletedPath?: string;
381
643
  terminationDiagnosticPath?: string;
382
644
  expectedOrganizationId?: string | null;
383
645
  }): Promise<DaytonaCrashDiagnostic> {
@@ -389,11 +651,17 @@ export async function readDetachedDaytonaCrashDiagnostic(input: {
389
651
  exitCodeFile: null,
390
652
  sessionExitCode: null,
391
653
  outputBytes: null,
654
+ outputFileAvailable: null,
655
+ exitCodeFileAvailable: null,
656
+ terminationFileAvailable: null,
657
+ runtimeStarted: null,
658
+ runtimeCompleted: null,
392
659
  tailBytes: 0,
393
660
  tailTruncated: false,
394
661
  signals: [],
395
662
  unclassifiedLineCount: 0,
396
663
  termination: null,
664
+ resourceSnapshot: null,
397
665
  collectionFailure,
398
666
  });
399
667
  try {
@@ -419,10 +687,10 @@ export async function readDetachedDaytonaCrashDiagnostic(input: {
419
687
  };
420
688
  }
421
689
 
422
- const script = `const fs=require('node:fs');const output=process.argv[1];const exit=process.argv[2];const max=Number(process.argv[3]);const terminationPath=process.argv[4]||'';const stat=fs.statSync(output);const start=Math.max(0,stat.size-max);const fd=fs.openSync(output,'r');const tail=Buffer.alloc(stat.size-start);fs.readSync(fd,tail,0,tail.length,start);fs.closeSync(fd);let exitCode=null;try{const value=Number.parseInt(fs.readFileSync(exit,'utf8').trim(),10);if(Number.isFinite(value))exitCode=value}catch{}let termination=null;try{const value=JSON.parse(fs.readFileSync(terminationPath,'utf8'));if(value&&typeof value==='object')termination=value}catch{}process.stdout.write(JSON.stringify({outputBytes:stat.size,tail:tail.toString('base64'),exitCode,termination}));`;
690
+ const script = buildDaytonaCrashDiagnosticSnapshotSource();
423
691
  const [snapshotResult, sessionResult] = await Promise.allSettled([
424
692
  sandbox.process.executeCommand(
425
- `node -e ${shellQuoteDaytonaDiagnostic(script)} ${shellQuoteDaytonaDiagnostic(input.outputPath)} ${shellQuoteDaytonaDiagnostic(input.exitCodePath)} ${DAYTONA_CRASH_DIAGNOSTIC_TAIL_MAX_BYTES} ${shellQuoteDaytonaDiagnostic(input.terminationDiagnosticPath ?? '')}`,
693
+ `node -e ${shellQuoteDaytonaDiagnostic(script)} ${shellQuoteDaytonaDiagnostic(input.outputPath)} ${shellQuoteDaytonaDiagnostic(input.exitCodePath)} ${DAYTONA_CRASH_DIAGNOSTIC_TAIL_MAX_BYTES} ${shellQuoteDaytonaDiagnostic(input.terminationDiagnosticPath ?? '')} ${shellQuoteDaytonaDiagnostic(input.runtimeStartedPath ?? '')} ${shellQuoteDaytonaDiagnostic(input.runtimeCompletedPath ?? '')} ${shellQuoteDaytonaDiagnostic(input.runnerPidPath ?? '')} ${shellQuoteDaytonaDiagnostic(input.oomKillBaselinePath ?? '')} ${shellQuoteDaytonaDiagnostic(RUNNER_MEMORY_CGROUP_ROOT)}`,
426
694
  undefined,
427
695
  {},
428
696
  8,
@@ -434,9 +702,15 @@ export async function readDetachedDaytonaCrashDiagnostic(input: {
434
702
  }
435
703
  const snapshot = JSON.parse(snapshotResult.value.result) as {
436
704
  outputBytes?: unknown;
705
+ outputFileAvailable?: unknown;
706
+ exitCodeFileAvailable?: unknown;
707
+ terminationFileAvailable?: unknown;
708
+ runtimeStarted?: unknown;
709
+ runtimeCompleted?: unknown;
437
710
  tail?: unknown;
438
711
  exitCode?: unknown;
439
712
  termination?: unknown;
713
+ resourceSnapshot?: unknown;
440
714
  };
441
715
  const tailBuffer =
442
716
  typeof snapshot.tail === 'string'
@@ -456,6 +730,9 @@ export async function readDetachedDaytonaCrashDiagnostic(input: {
456
730
  ? sessionResult.value.exitCode
457
731
  : null;
458
732
  const termination = parseDaytonaTerminationDiagnostic(snapshot.termination);
733
+ const resourceSnapshot = parseDaytonaCrashResourceSnapshot(
734
+ snapshot.resourceSnapshot,
735
+ );
459
736
  return {
460
737
  schemaVersion: 1,
461
738
  collectionStatus: 'collected',
@@ -466,6 +743,26 @@ export async function readDetachedDaytonaCrashDiagnostic(input: {
466
743
  : null,
467
744
  sessionExitCode,
468
745
  outputBytes,
746
+ outputFileAvailable:
747
+ typeof snapshot.outputFileAvailable === 'boolean'
748
+ ? snapshot.outputFileAvailable
749
+ : null,
750
+ exitCodeFileAvailable:
751
+ typeof snapshot.exitCodeFileAvailable === 'boolean'
752
+ ? snapshot.exitCodeFileAvailable
753
+ : null,
754
+ terminationFileAvailable:
755
+ typeof snapshot.terminationFileAvailable === 'boolean'
756
+ ? snapshot.terminationFileAvailable
757
+ : null,
758
+ runtimeStarted:
759
+ typeof snapshot.runtimeStarted === 'boolean'
760
+ ? snapshot.runtimeStarted
761
+ : null,
762
+ runtimeCompleted:
763
+ typeof snapshot.runtimeCompleted === 'boolean'
764
+ ? snapshot.runtimeCompleted
765
+ : null,
469
766
  tailBytes: tailBuffer.byteLength,
470
767
  tailTruncated:
471
768
  outputBytes !== null &&
@@ -473,6 +770,7 @@ export async function readDetachedDaytonaCrashDiagnostic(input: {
473
770
  signals,
474
771
  unclassifiedLineCount,
475
772
  termination,
773
+ resourceSnapshot,
476
774
  collectionFailure:
477
775
  sessionResult.status === 'rejected'
478
776
  ? describeDaytonaLookupFailure(sessionResult.reason)
@@ -1418,6 +1716,9 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
1418
1716
  cmdId: start.cmdId,
1419
1717
  outputPath: stagedPayload.outputPath,
1420
1718
  exitCodePath: stagedPayload.exitCodePath,
1719
+ oomKillBaselinePath: stagedPayload.oomKillBaselinePath,
1720
+ runnerPidPath: stagedPayload.runnerPidPath,
1721
+ runtimeStartedPath: stagedPayload.runtimeStartedPath,
1421
1722
  runtimeCompletedPath: stagedPayload.runtimeCompletedPath,
1422
1723
  terminationDiagnosticPath:
1423
1724
  stagedPayload.terminationDiagnosticPath,
@@ -834,6 +834,10 @@ function buildDefaultDeps(absurd: Absurd): AbsurdRuntimeWorkerDeps {
834
834
  cmdId: request.cmdId,
835
835
  outputPath: request.outputPath,
836
836
  exitCodePath: request.exitCodePath,
837
+ oomKillBaselinePath: request.oomKillBaselinePath,
838
+ runnerPidPath: request.runnerPidPath,
839
+ runtimeStartedPath: request.runtimeStartedPath,
840
+ runtimeCompletedPath: request.runtimeCompletedPath,
837
841
  terminationDiagnosticPath: request.terminationDiagnosticPath,
838
842
  expectedOrganizationId: routingDomain,
839
843
  }),
@@ -4057,6 +4061,18 @@ function sandboxCrashDiagnosticRequest(input: {
4057
4061
  cmdId: suspension.cmdId,
4058
4062
  outputPath: suspension.outputPath,
4059
4063
  exitCodePath: suspension.exitCodePath,
4064
+ ...(suspension.oomKillBaselinePath
4065
+ ? { oomKillBaselinePath: suspension.oomKillBaselinePath }
4066
+ : {}),
4067
+ ...(suspension.runnerPidPath
4068
+ ? { runnerPidPath: suspension.runnerPidPath }
4069
+ : {}),
4070
+ ...(suspension.runtimeStartedPath
4071
+ ? { runtimeStartedPath: suspension.runtimeStartedPath }
4072
+ : {}),
4073
+ ...(suspension.runtimeCompletedPath
4074
+ ? { runtimeCompletedPath: suspension.runtimeCompletedPath }
4075
+ : {}),
4060
4076
  ...(suspension.terminationDiagnosticPath
4061
4077
  ? { terminationDiagnosticPath: suspension.terminationDiagnosticPath }
4062
4078
  : {}),
@@ -4371,7 +4387,7 @@ async function resolveDetachedRunnerOutcome(input: {
4371
4387
  runnerAttempt: suspension.runnerAttempt,
4372
4388
  sandboxId: suspension.sandboxId,
4373
4389
  exitCode: inspection.exitCode,
4374
- diagnosis: inspection.diagnosis?.stage,
4390
+ diagnosis: inspection.diagnosis?.stage ?? 'unknown',
4375
4391
  resultStatus: inspection.result.status,
4376
4392
  }),
4377
4393
  );
@@ -4385,7 +4401,7 @@ async function resolveDetachedRunnerOutcome(input: {
4385
4401
  runnerAttempt: suspension.runnerAttempt,
4386
4402
  sandboxId: suspension.sandboxId,
4387
4403
  exitCode: inspection.exitCode,
4388
- diagnosis: inspection.diagnosis?.stage,
4404
+ diagnosis: inspection.diagnosis?.stage ?? 'unknown',
4389
4405
  resultStatus: inspection.result?.status ?? null,
4390
4406
  }),
4391
4407
  );
@@ -6172,6 +6188,20 @@ function parseSandboxCrashDiagnosticRequest(
6172
6188
  ) {
6173
6189
  return null;
6174
6190
  }
6191
+ for (const key of [
6192
+ 'oomKillBaselinePath',
6193
+ 'runnerPidPath',
6194
+ 'runtimeStartedPath',
6195
+ 'runtimeCompletedPath',
6196
+ 'terminationDiagnosticPath',
6197
+ ] as const) {
6198
+ if (
6199
+ candidate[key] !== undefined &&
6200
+ (typeof candidate[key] !== 'string' || !(candidate[key] as string).trim())
6201
+ ) {
6202
+ return null;
6203
+ }
6204
+ }
6175
6205
  return candidate as PostgresSchedulerSandboxCrashDiagnosticRequest;
6176
6206
  }
6177
6207
 
@@ -6184,11 +6214,17 @@ function unavailableSandboxCrashDiagnostic(
6184
6214
  exitCodeFile: null,
6185
6215
  sessionExitCode: null,
6186
6216
  outputBytes: null,
6217
+ outputFileAvailable: null,
6218
+ exitCodeFileAvailable: null,
6219
+ terminationFileAvailable: null,
6220
+ runtimeStarted: null,
6221
+ runtimeCompleted: null,
6187
6222
  tailBytes: 0,
6188
6223
  tailTruncated: false,
6189
6224
  signals: [],
6190
6225
  unclassifiedLineCount: 0,
6191
6226
  termination: null,
6227
+ resourceSnapshot: null,
6192
6228
  collectionFailure: {
6193
6229
  errorName: 'CrashDiagnosticUnavailable',
6194
6230
  errorCode: null,
@@ -14,18 +14,22 @@ const AIMD_ADDITIVE_STEP_FRACTION = 0.25;
14
14
  export function acquireRateScopeSql(
15
15
  table: string,
16
16
  successesParameter: string,
17
+ minimumRateSql = 'NULL::float8',
17
18
  ): string {
19
+ const refillRate = `CASE WHEN ${minimumRateSql} IS NULL
20
+ THEN h.refill_per_sec
21
+ ELSE GREATEST(h.refill_per_sec, ${minimumRateSql}) END`;
18
22
  return `provider_hold_rows AS (
19
23
  SELECT * FROM ${table} WHERE bucket_id = $1 FOR UPDATE
20
24
  ), provider_hold AS (
21
25
  SELECT h.*,
22
26
  LEAST(
23
- GREATEST(CASE WHEN h.seen_backpressure THEN 1 ELSE h.capacity END, h.refill_per_sec),
24
- h.tokens + GREATEST(0, nv.now_ms - h.refilled_at_ms) * h.refill_per_sec / 1000.0
27
+ GREATEST(CASE WHEN h.seen_backpressure THEN 1 ELSE h.capacity END, ${refillRate}),
28
+ h.tokens + GREATEST(0, nv.now_ms - h.refilled_at_ms) * ${refillRate} / 1000.0
25
29
  ) AS tokens_after_refill,
26
30
  ${successesParameter}::int = 0
27
31
  AND nv.now_ms - GREATEST(h.refilled_at_ms, h.next_dispatch_at_ms, h.retry_at_ms)
28
- > GREATEST(1000, 2000.0 / h.refill_per_sec) AS observation_was_idle,
32
+ > GREATEST(1000, 2000.0 / ${refillRate}) AS observation_was_idle,
29
33
  CASE WHEN ${successesParameter}::int > 0
30
34
  AND h.rate_observation_started_at_ms IS NOT NULL
31
35
  AND nv.now_ms - h.last_ramp_at_ms >= CASE WHEN h.seen_backpressure
@@ -33,9 +37,9 @@ export function acquireRateScopeSql(
33
37
  ELSE h.rate_recovery_interval_ms END
34
38
  AND h.retry_at_ms <= nv.now_ms
35
39
  THEN LEAST(h.max_rps, CASE WHEN h.seen_backpressure
36
- THEN h.refill_per_sec * (1 + ${AIMD_ADDITIVE_STEP_FRACTION})
37
- ELSE h.refill_per_sec * 2 END)
38
- ELSE h.refill_per_sec END AS next_refill_per_sec,
40
+ THEN ${refillRate} * (1 + ${AIMD_ADDITIVE_STEP_FRACTION})
41
+ ELSE ${refillRate} * 2 END)
42
+ ELSE ${refillRate} END AS next_refill_per_sec,
39
43
  CASE WHEN ${successesParameter}::int > 0
40
44
  AND h.rate_observation_started_at_ms IS NOT NULL
41
45
  AND nv.now_ms - h.last_ramp_at_ms >= CASE WHEN h.seen_backpressure
@@ -390,9 +390,9 @@ function normalizeRules(
390
390
  return {
391
391
  ruleId: rule.ruleId,
392
392
  // Rates are fractional; only dispatched requests must be whole.
393
- // Keep the shared, unhinted credential scope conservative even when
394
- // an older runner submits the generic 10-RPS policy seed. The shared
395
- // row's AIMD controller learns upward from successful observations.
393
+ // Normalize every runner to the canonical 10-RPS seed for an unknown
394
+ // shared credential scope. The shared row's AIMD state can still be
395
+ // lower after backpressure and learns upward from successful calls.
396
396
  requestsPerWindow: isUnknownProviderScope
397
397
  ? DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS
398
398
  : rule.requestsPerWindow,
@@ -446,7 +446,13 @@ function seedForRule(rule: AcquirePacingRule): RateBucketSeed {
446
446
  // Preserve its fractional refill rate, with dispatch times resolved in ms.
447
447
  const capacity = Math.max(1, rule.requestsPerWindow);
448
448
  const base = (rule.requestsPerWindow / rule.windowMs) * 1000;
449
- const minRps = Math.min(base, MIN_ADAPTIVE_RPS_FLOOR);
449
+ // Undeclared providers use the shared fallback rate as their operating
450
+ // floor. Explicit provider Retry-After deadlines still block dispatch; a
451
+ // prior learned AIMD rate must not leave later runs paced below the fallback.
452
+ const minRps =
453
+ rule.ruleId === 'default:provider-scope'
454
+ ? base
455
+ : Math.min(base, MIN_ADAPTIVE_RPS_FLOOR);
450
456
  const maxRps =
451
457
  rule.adaptiveMaxRps != null && rule.adaptiveMaxRps > 0
452
458
  ? Math.max(rule.adaptiveMaxRps, base)
@@ -461,6 +467,12 @@ function seedForRule(rule: AcquirePacingRule): RateBucketSeed {
461
467
  };
462
468
  }
463
469
 
470
+ function minimumRateSqlForRules(rules: readonly AcquirePacingRule[]): string {
471
+ return rules.some((rule) => rule.ruleId === 'default:provider-scope')
472
+ ? `${DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS}::float8`
473
+ : 'NULL::float8';
474
+ }
475
+
464
476
  function positiveInt(value: number, label: string): number {
465
477
  if (!Number.isFinite(value) || value <= 0) {
466
478
  throw new Error(`${label} must be a positive finite number.`);
@@ -743,11 +755,10 @@ async function seedBucketRows(
743
755
  ON CONFLICT (bucket_id) DO UPDATE SET
744
756
  retry_at_ms = GREATEST(hold.retry_at_ms, EXCLUDED.retry_at_ms),
745
757
  refill_per_sec = CASE
746
- -- Older runners seeded this shared credential row at the generic
747
- -- default (10 RPS). If that old value never moved from its seed,
748
- -- adopt the conservative shared-scope seed on the first acquire
749
- -- after upgrade. Preserve any rate with actual ramp or throttle
750
- -- evidence, and never erase a provider Retry-After hold.
758
+ -- If this row never moved from its seed, adopt the canonical
759
+ -- shared-scope seed on the first acquire after upgrade. Preserve
760
+ -- any rate with actual ramp or throttle evidence, and never erase
761
+ -- a provider Retry-After hold.
751
762
  WHEN EXISTS (
752
763
  SELECT 1 FROM seed_config
753
764
  WHERE rule_id = 'default:provider-scope'
@@ -1295,7 +1306,11 @@ async function acquirePureRate(
1295
1306
  adaptive_max_configured boolean
1296
1307
  )
1297
1308
  ),
1298
- ${acquireRateScopeSql(scopeHoldTableName(input), '$5')},
1309
+ ${acquireRateScopeSql(
1310
+ scopeHoldTableName(input),
1311
+ '$5',
1312
+ minimumRateSqlForRules(rules),
1313
+ )},
1299
1314
  reclaim_reservations AS (
1300
1315
  DELETE FROM ${reservationTableName(input)}
1301
1316
  WHERE bucket_id = $1
@@ -1735,7 +1750,11 @@ async function acquireWithConcurrency(
1735
1750
  FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
1736
1751
  ) AS now_ms
1737
1752
  ),
1738
- ${acquireRateScopeSql(scopeHoldTableName(input), '$10')},
1753
+ ${acquireRateScopeSql(
1754
+ scopeHoldTableName(input),
1755
+ '$10',
1756
+ minimumRateSqlForRules(rules),
1757
+ )},
1739
1758
  reclaim_reservations AS (
1740
1759
  DELETE FROM ${reservationTableName(input)}
1741
1760
  WHERE bucket_id = $1
@@ -3015,6 +3034,11 @@ async function penalizeRateStateStatements(
3015
3034
  return emptyRateStatePenaltyTelemetry();
3016
3035
  }
3017
3036
  const rules = normalizeRules((input.rules ?? []) as AcquirePacingRule[]);
3037
+ const minimumRateRps = rules.some(
3038
+ (rule) => rule.ruleId === 'default:provider-scope',
3039
+ )
3040
+ ? DEFAULT_UNKNOWN_PROVIDER_COLD_START_RPS
3041
+ : MIN_ADAPTIVE_RPS_FLOOR;
3018
3042
  await applyConcurrencyObservations(
3019
3043
  client,
3020
3044
  input,
@@ -3091,7 +3115,7 @@ async function penalizeRateStateStatements(
3091
3115
  FROM held h CROSS JOIN now_value nv
3092
3116
  ), throttle_ceiling AS (
3093
3117
  SELECT e.*,
3094
- GREATEST(${MIN_ADAPTIVE_RPS_FLOOR},
3118
+ GREATEST(${minimumRateRps},
3095
3119
  CASE WHEN e.seen_backpressure AND e.rate_successes = 0 THEN
3096
3120
  CASE WHEN $5::bigint IS NOT NULL
3097
3121
  -- A fresh provider deadline is not a throughput measurement.
@@ -3106,7 +3130,7 @@ async function penalizeRateStateStatements(
3106
3130
  FROM evidence e
3107
3131
  ), learned AS (
3108
3132
  SELECT e.*,
3109
- CASE WHEN e.new_episode THEN GREATEST(${MIN_ADAPTIVE_RPS_FLOOR}, LEAST(
3133
+ CASE WHEN e.new_episode THEN GREATEST(${minimumRateRps}, LEAST(
3110
3134
  e.next_ceiling_rps,
3111
3135
  GREATEST(1, e.rate_successes) * 1000.0 / e.cycle_ms
3112
3136
  )) ELSE e.refill_before END AS refill_after
@@ -392,6 +392,10 @@ export type PostgresSchedulerSandboxCrashDiagnosticRequest = {
392
392
  cmdId: string;
393
393
  outputPath: string;
394
394
  exitCodePath: string;
395
+ oomKillBaselinePath?: string;
396
+ runnerPidPath?: string;
397
+ runtimeStartedPath?: string;
398
+ runtimeCompletedPath?: string;
395
399
  terminationDiagnosticPath?: string;
396
400
  };
397
401
 
@@ -68,6 +68,10 @@ export type PlayExecutionSuspension =
68
68
  * timeout-wake salvage verification. */
69
69
  outputPath: string;
70
70
  exitCodePath: string;
71
+ /** Bounded crash evidence paths for sandbox state at liveness loss. */
72
+ oomKillBaselinePath?: string;
73
+ runnerPidPath?: string;
74
+ runtimeStartedPath?: string;
71
75
  /** Exact customer-code completion fence inside the Daytona sandbox. */
72
76
  runtimeCompletedPath?: string;
73
77
  /** Bounded watchdog-owned cause marker for crash-only diagnostics. */
@@ -8,7 +8,12 @@ import {
8
8
  type ToolExecutionErrorSchemaVersion,
9
9
  } from '../plays/tool-execution-error';
10
10
 
11
- export const TOOL_EXECUTE_TRANSIENT_HTTP_MAX_ATTEMPTS = 2;
11
+ // Give an explicitly replay-safe transient 5xx one additional bounded retry.
12
+ // Preview gateway load runs can overlap brief Convex billing-plane 503s; this
13
+ // adds a second recovery chance without replaying operations that did not opt
14
+ // into the transient HTTP safety contract.
15
+ export const TOOL_EXECUTE_TRANSIENT_HTTP_MAX_ATTEMPTS = 3;
16
+ const TOOL_EXECUTE_CUSTOMER_DB_STORAGE_UNAVAILABLE_MAX_ATTEMPTS = 2;
12
17
  // A provider may need to finish the original request before it can replay the
13
18
  // result for this key. With no Retry-After signal, these two polls give the
14
19
  // original request ten seconds to finish without repeatedly hammering it.
@@ -344,7 +349,7 @@ function decideToolExecuteHttpRetry(input: {
344
349
  if (input.customerDbStorageUnavailable) {
345
350
  return {
346
351
  retryable: true,
347
- attemptCap: TOOL_EXECUTE_TRANSIENT_HTTP_MAX_ATTEMPTS,
352
+ attemptCap: TOOL_EXECUTE_CUSTOMER_DB_STORAGE_UNAVAILABLE_MAX_ATTEMPTS,
348
353
  reason: 'customer_db_storage_unavailable',
349
354
  };
350
355
  }
package/dist/cli/index.js CHANGED
@@ -3051,7 +3051,7 @@ var SDK_RELEASE = {
3051
3051
  // getters keep their established compatibility behavior.
3052
3052
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3053
3053
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3054
- version: "0.3.209",
3054
+ version: "0.3.211",
3055
3055
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
3056
3056
  packageCapabilities: {
3057
3057
  updatePreferences: 1
@@ -3046,7 +3046,7 @@ var SDK_RELEASE = {
3046
3046
  // getters keep their established compatibility behavior.
3047
3047
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3048
3048
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3049
- version: "0.3.209",
3049
+ version: "0.3.211",
3050
3050
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
3051
3051
  packageCapabilities: {
3052
3052
  updatePreferences: 1
package/dist/index.js CHANGED
@@ -864,7 +864,7 @@ var SDK_RELEASE = {
864
864
  // getters keep their established compatibility behavior.
865
865
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
866
866
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
867
- version: "0.3.209",
867
+ version: "0.3.211",
868
868
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
869
869
  packageCapabilities: {
870
870
  updatePreferences: 1
package/dist/index.mjs CHANGED
@@ -768,7 +768,7 @@ var SDK_RELEASE = {
768
768
  // getters keep their established compatibility behavior.
769
769
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
770
770
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
771
- version: "0.3.209",
771
+ version: "0.3.211",
772
772
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
773
773
  packageCapabilities: {
774
774
  updatePreferences: 1
@@ -149,7 +149,7 @@ type SdkRelease = {
149
149
  supportPolicy: SdkSupportPolicy;
150
150
  };
151
151
  declare const SDK_RELEASE: {
152
- readonly version: "0.3.209";
152
+ readonly version: "0.3.211";
153
153
  readonly updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.";
154
154
  readonly packageCapabilities: {
155
155
  readonly updatePreferences: 1;
package/dist/release.d.ts CHANGED
@@ -149,7 +149,7 @@ type SdkRelease = {
149
149
  supportPolicy: SdkSupportPolicy;
150
150
  };
151
151
  declare const SDK_RELEASE: {
152
- readonly version: "0.3.209";
152
+ readonly version: "0.3.211";
153
153
  readonly updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.";
154
154
  readonly packageCapabilities: {
155
155
  readonly updatePreferences: 1;
package/dist/release.js CHANGED
@@ -74,7 +74,7 @@ var SDK_RELEASE = {
74
74
  // getters keep their established compatibility behavior.
75
75
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
76
76
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
77
- version: "0.3.209",
77
+ version: "0.3.211",
78
78
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
79
79
  packageCapabilities: {
80
80
  updatePreferences: 1
package/dist/release.mjs CHANGED
@@ -48,7 +48,7 @@ var SDK_RELEASE = {
48
48
  // getters keep their established compatibility behavior.
49
49
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
50
50
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
51
- version: "0.3.209",
51
+ version: "0.3.211",
52
52
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
53
53
  packageCapabilities: {
54
54
  updatePreferences: 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.3.209",
3
+ "version": "0.3.211",
4
4
  "description": "GTM data CLI and TypeScript SDK for coding agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://code.deepline.com",