deepline 0.3.128 → 0.3.129

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 (128) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +34 -9
  2. package/dist/bundling-sources/sdk/src/http.ts +22 -0
  3. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  4. package/dist/bundling-sources/shared_libs/play-data-plane/r2.ts +22 -0
  5. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +265 -2
  6. package/dist/bundling-sources/shared_libs/play-runtime/bounded-batch-function.ts +682 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +21 -5
  8. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +5390 -1732
  9. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +107 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +7 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +229 -7
  12. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +25 -4
  13. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-capacity.ts +71 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +1 -1
  15. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +4 -1
  16. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +694 -28
  17. package/dist/bundling-sources/shared_libs/play-runtime/egress-bandwidth-probe.ts +167 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/execute-server-timing.ts +103 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-receipt-output-reference.ts +34 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/execution-gateway-response-size.ts +27 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/gateway-postgres-admission.ts +66 -1
  22. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +425 -71
  23. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +210 -32
  24. package/dist/bundling-sources/shared_libs/play-runtime/governor/in-memory-rate-state-backend.ts +27 -13
  25. package/dist/bundling-sources/shared_libs/play-runtime/governor/policy.ts +19 -6
  26. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +61 -2
  27. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/contracts.ts +33 -1
  28. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/job.ts +42 -1
  29. package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +11 -11
  30. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +49 -16
  31. package/dist/bundling-sources/shared_libs/play-runtime/map-memory-limits.ts +1653 -98
  32. package/dist/bundling-sources/shared_libs/play-runtime/memory-ledger.ts +807 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/memory-pressure-signal.ts +138 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +85 -3
  35. package/dist/bundling-sources/shared_libs/play-runtime/persistence-latch.ts +60 -2
  36. package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +46 -1
  37. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/contracts.ts +2 -0
  38. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +1 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +1 -0
  40. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/postgres-adapter.ts +1 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +70 -27
  42. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +56 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/adaptive-provider-capacity-controller.ts +192 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/epoch-aimd-rate-controller.ts +194 -0
  45. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/fixture-capacity-simulator.ts +220 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/gradient2-concurrency-controller.ts +214 -0
  47. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-capacity-policy.ts +43 -0
  48. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-concurrency-policy.ts +30 -0
  49. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-rate-policy.ts +95 -0
  50. package/dist/bundling-sources/shared_libs/play-runtime/provider-capacity/provider-throttle-classifier.ts +40 -0
  51. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +1535 -11
  52. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +49 -2
  53. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +6 -1
  54. package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +55 -0
  55. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/egress-probe.ts +221 -0
  56. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +339 -29
  57. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +28 -1
  58. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +192 -165
  59. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +6 -0
  60. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +90 -5
  61. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +281 -6
  62. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +27 -1
  63. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +19 -0
  64. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/modal.ts +108 -0
  65. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +40 -0
  66. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +11 -5
  67. package/dist/bundling-sources/shared_libs/play-runtime/runtime-actions.ts +30 -0
  68. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api-compression.ts +276 -0
  69. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +723 -115
  70. package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +98 -2
  71. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +3 -2
  72. package/dist/bundling-sources/shared_libs/play-runtime/runtime-environment.ts +41 -4
  73. package/dist/bundling-sources/shared_libs/play-runtime/runtime-flow.ts +448 -0
  74. package/dist/bundling-sources/shared_libs/play-runtime/runtime-map-row-feed.ts +215 -0
  75. package/dist/bundling-sources/shared_libs/play-runtime/runtime-operation-contract.ts +11 -0
  76. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-store-adapter.ts +161 -14
  77. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-value.ts +184 -0
  78. package/dist/bundling-sources/shared_libs/play-runtime/runtime-receipt-writer.ts +607 -121
  79. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sandbox-placement-policy.ts +4 -0
  80. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-writer.ts +21 -0
  81. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/contract.ts +15 -2
  82. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +71 -14
  83. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-play-deploy-task.ts +2 -2
  84. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +288 -119
  85. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -6
  86. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-worker-required-env.ts +28 -3
  87. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/canonical-integration-operation-store.ts +179 -0
  88. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-heavy-request-admission.ts +80 -2
  89. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/gateway-progress-registry.ts +31 -2
  90. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-store-contract.ts +104 -0
  91. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-reference.ts +47 -0
  92. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-retirement.ts +193 -0
  93. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/integration-operation-value-storage.ts +420 -0
  94. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +41 -0
  95. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +134 -3
  96. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +1657 -95
  97. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +885 -73
  98. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-completion-coalescer.ts +452 -0
  99. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +601 -42
  100. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-terminal-transition.ts +1 -0
  101. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +371 -0
  102. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/worker-resource-metrics.ts +86 -7
  103. package/dist/bundling-sources/shared_libs/play-runtime/secret-redaction.ts +153 -22
  104. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/index.ts +1 -0
  105. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/receipt-cohort.ts +188 -38
  106. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +28 -13
  107. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +44 -1
  108. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +56 -4
  109. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +2 -0
  110. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +16 -10
  111. package/dist/bundling-sources/shared_libs/plays/dataset.ts +4 -1
  112. package/dist/cli/index.js +250 -38
  113. package/dist/cli/index.mjs +250 -38
  114. package/dist/{compiler-manifest-DfSnbCTW.d.mts → compiler-manifest-BIqRyj5m.d.mts} +1 -1
  115. package/dist/{compiler-manifest-DfSnbCTW.d.ts → compiler-manifest-BIqRyj5m.d.ts} +1 -1
  116. package/dist/index.d.mts +4 -2
  117. package/dist/index.d.ts +4 -2
  118. package/dist/index.js +231 -35
  119. package/dist/index.mjs +231 -35
  120. package/dist/install-integrity.json +31 -2
  121. package/dist/plays/bundle-play-file.d.mts +2 -2
  122. package/dist/plays/bundle-play-file.d.ts +2 -2
  123. package/dist/plays/bundle-play-file.mjs +65 -1
  124. package/dist/release.d.mts +1 -1
  125. package/dist/release.d.ts +1 -1
  126. package/dist/release.js +1 -1
  127. package/dist/release.mjs +1 -1
  128. package/package.json +1 -1
@@ -1,10 +1,132 @@
1
+ import { STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS } from '../../../plays/sandbox-runtime-limits';
2
+ import { RUNTIME_SANDBOX_MEMORY_ABORT_CODE } from '../../run-failure';
3
+ import {
4
+ RUNTIME_MEMORY_LEDGER_PATH_ENV,
5
+ RUNTIME_MEMORY_PRESSURE_PATH_ENV,
6
+ } from '../../memory-pressure-signal';
7
+
1
8
  /** Exit code reserved for the external runner watchdog's runtime deadline. */
2
9
  export const DAYTONA_RUNTIME_WATCHDOG_EXIT_CODE = 124;
3
10
 
11
+ /** The cgroup memory envelope one standard sandbox is given, in MiB. */
12
+ export const STANDARD_PLAY_SANDBOX_MEMORY_MIB =
13
+ STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS.memoryGiB * 1024;
14
+
15
+ /**
16
+ * Share of the sandbox cgroup that V8 may use for its old generation.
17
+ *
18
+ * This used to be 0.75, which was not an envelope at all: 768 MiB of old space
19
+ * plus the default ~192 MiB young generation let V8 alone reach 960 MiB inside
20
+ * a 1024 MiB cgroup before a single native buffer, code page, or in-flight
21
+ * transport body was counted. The cgroup kills on RSS, and RSS runs 1.3-1.5x
22
+ * the V8 live bytes, so V8 never had to collect before the kernel killed the
23
+ * process. Run 34064408068 died exactly that way at residentPeak ~1 GiB.
24
+ *
25
+ * At 0.5 the V8 heap ceiling sits at 512 MiB of the 1024 MiB cgroup, so V8
26
+ * collects hard while there is still cgroup headroom for everything V8 does
27
+ * not own.
28
+ */
29
+ export const STANDARD_PLAY_RUNNER_OLD_SPACE_SHARE = 0.5;
30
+
31
+ /**
32
+ * `--max-old-space-size` for the customer runner. This is the single V8
33
+ * envelope for both the Daytona and local-process substrates; map admission
34
+ * samples this actual V8 ceiling.
35
+ */
36
+ export const STANDARD_PLAY_RUNNER_MAX_OLD_SPACE_SIZE_MIB = Math.floor(
37
+ STANDARD_PLAY_SANDBOX_MEMORY_MIB * STANDARD_PLAY_RUNNER_OLD_SPACE_SHARE,
38
+ );
39
+
40
+ /**
41
+ * `--max-semi-space-size` for the customer runner. V8 reserves three
42
+ * semi-spaces, so 16 MiB adds ~48 MiB to `heap_size_limit` where the 64-bit
43
+ * default (64 MiB) adds ~192 MiB. Capping the young generation is what makes
44
+ * the flag list an envelope rather than an old-space number with an
45
+ * unaccounted nursery bolted on top.
46
+ */
47
+ export const STANDARD_PLAY_RUNNER_MAX_SEMI_SPACE_SIZE_MIB = 16;
48
+
49
+ /**
50
+ * Approximately what `v8.getHeapStatistics().heap_size_limit` reports under
51
+ * the flags below: 512 + 3 x 16 = 560 MiB of the 1024 MiB sandbox.
52
+ */
53
+ export const STANDARD_PLAY_RUNNER_HEAP_SIZE_LIMIT_MIB =
54
+ STANDARD_PLAY_RUNNER_MAX_OLD_SPACE_SIZE_MIB +
55
+ 3 * STANDARD_PLAY_RUNNER_MAX_SEMI_SPACE_SIZE_MIB;
56
+
57
+ /**
58
+ * The one Node exec argv every customer-runner spawn site uses. `--expose-gc`
59
+ * is not a debug flag here: the runner calls `globalThis.gc()` when this
60
+ * watchdog signals memory pressure, which is the only way a hold can return
61
+ * bytes to the cgroup before the abort threshold.
62
+ */
63
+ export const PLAY_RUNNER_NODE_EXEC_ARGV: readonly string[] = [
64
+ `--max-old-space-size=${STANDARD_PLAY_RUNNER_MAX_OLD_SPACE_SIZE_MIB}`,
65
+ `--max-semi-space-size=${STANDARD_PLAY_RUNNER_MAX_SEMI_SPACE_SIZE_MIB}`,
66
+ '--expose-gc',
67
+ // The sandbox reports the host's 48-64 cores while its cgroup quota is one
68
+ // core, so V8 would spawn parallel and concurrent GC helpers that only
69
+ // contend with the main thread for that core (51-run Daytona CPU
70
+ // experiment, 2026-09-07: gzip on the threadpool accounted for every GC
71
+ // event and every event-loop stall). One core gets one GC thread.
72
+ '--single-threaded-gc',
73
+ ];
74
+
4
75
  /**
5
- * Build the tiny parent process that owns the hard runtime deadline outside
6
- * the customer-code Node event loop. A synchronous infinite loop can block the
7
- * runner's own timer, but it cannot block this separate process.
76
+ * libuv threadpool size for the customer runner. The default is 4 and it is
77
+ * sized for a machine, not a one-core cgroup: zlib and fs work on four threads
78
+ * plus the main thread thrash on one core. Two keeps gzip and file I/O off the
79
+ * main thread without oversubscribing it.
80
+ */
81
+ export const PLAY_RUNNER_UV_THREADPOOL_SIZE = 2;
82
+
83
+ /** How often the watchdog parent samples the cgroup's memory usage. */
84
+ export const RUNNER_MEMORY_WATCH_INTERVAL_MS = 250;
85
+
86
+ /**
87
+ * Fraction of the cgroup limit at which the watchdog signals the runner
88
+ * (SIGUSR2) to pause new admission, release caches, and collect.
89
+ */
90
+ export const RUNNER_MEMORY_PRESSURE_RATIO = 0.8;
91
+
92
+ /**
93
+ * Fraction of the cgroup limit at which the watchdog terminates the runner
94
+ * itself. A controlled abort carrying the runtime's byte ledger is a failure
95
+ * the run can report; the kernel's OOM kill is not.
96
+ */
97
+ export const RUNNER_MEMORY_ABORT_RATIO = 0.92;
98
+
99
+ /** Minimum spacing between pressure signals while usage stays above the ratio. */
100
+ export const RUNNER_MEMORY_PRESSURE_SIGNAL_INTERVAL_MS = 2_000;
101
+
102
+ /** Grace between SIGTERM and SIGKILL when the watchdog stops the runner. */
103
+ export const RUNNER_TERMINATION_KILL_GRACE_MS = 1_000;
104
+
105
+ /** Default cgroup mount point. Overridable by argv so tests can inject a root. */
106
+ export const RUNNER_MEMORY_CGROUP_ROOT = '/sys/fs/cgroup';
107
+
108
+ /**
109
+ * cgroup v1 reports "no limit" as a near-2^63 sentinel. Anything at or above
110
+ * this is not a real sandbox envelope, so the watch treats it as absent.
111
+ */
112
+ export const RUNNER_MEMORY_CGROUP_UNLIMITED_BYTES = 2 ** 50;
113
+
114
+ /** `reason` written into the termination diagnostic on a memory abort. */
115
+ export const RUNTIME_SANDBOX_MEMORY_ABORT_REASON =
116
+ RUNTIME_SANDBOX_MEMORY_ABORT_CODE;
117
+
118
+ /**
119
+ * Build the tiny parent process that owns the hard runtime deadline and the
120
+ * sandbox memory envelope outside the customer-code Node event loop. A
121
+ * synchronous infinite loop can block the runner's own timer, but it cannot
122
+ * block this separate process; nor can a runner whose heap is thrashing run
123
+ * its own memory watch reliably.
124
+ *
125
+ * argv (after `node -e <source>`):
126
+ * 1 runnerPath, 2 configPath, 3 runtimeStartedPath, 4 runtimeCompletedPath,
127
+ * 5 limitMs, 6 startupLimitMs, 7 terminalGraceMs, 8 runtimeLimitMarkerPath,
128
+ * 9 terminationDiagnosticPath, 10 memoryLedgerPath, 11 memoryPressurePath,
129
+ * 12 cgroupRoot.
8
130
  */
9
131
  export function buildDaytonaRuntimeWatchdogSource(): string {
10
132
  return `
@@ -19,18 +141,30 @@ const startupLimitMs = Math.max(1, Number.parseInt(process.argv[6] || '', 10) ||
19
141
  const terminalGraceMs = Math.max(1, Number.parseInt(process.argv[7] || '', 10) || 1);
20
142
  const runtimeLimitMarkerPath = process.argv[8] || '';
21
143
  const terminationDiagnosticPath = process.argv[9] || '';
144
+ const memoryLedgerPath = process.argv[10] || '';
145
+ const memoryPressurePath = process.argv[11] || '';
146
+ const cgroupRoot = process.argv[12] || ${JSON.stringify(RUNNER_MEMORY_CGROUP_ROOT)};
22
147
  const runtimeLimitSeconds = Math.max(1, Math.ceil(limitMs / 1_000));
23
148
  const child = spawn(
24
149
  process.execPath,
25
- ['--max-old-space-size=512', runnerPath, configPath],
150
+ [${PLAY_RUNNER_NODE_EXEC_ARGV.map((flag) => JSON.stringify(flag)).join(', ')}, runnerPath, configPath],
26
151
  {
27
- env: process.env,
152
+ env: {
153
+ ...process.env,
154
+ ...(memoryLedgerPath
155
+ ? { ${JSON.stringify(RUNTIME_MEMORY_LEDGER_PATH_ENV)}: memoryLedgerPath }
156
+ : {}),
157
+ ...(memoryPressurePath
158
+ ? { ${JSON.stringify(RUNTIME_MEMORY_PRESSURE_PATH_ENV)}: memoryPressurePath }
159
+ : {}),
160
+ },
28
161
  stdio: 'inherit',
29
162
  },
30
163
  );
31
164
  let timedOut = false;
32
165
  let startupTimedOut = false;
33
166
  let terminalFlushTimedOut = false;
167
+ let memoryAborted = false;
34
168
  let settled = false;
35
169
  let forceKillTimer = null;
36
170
  let runtimeTimer = null;
@@ -56,7 +190,7 @@ function writeTerminationDiagnostic(reason, details = {}) {
56
190
  function stopChild(reason, details) {
57
191
  writeTerminationDiagnostic(reason, details);
58
192
  child.kill('SIGTERM');
59
- forceKillTimer = setTimeout(() => child.kill('SIGKILL'), 1_000);
193
+ forceKillTimer = setTimeout(() => child.kill('SIGKILL'), ${RUNNER_TERMINATION_KILL_GRACE_MS});
60
194
  }
61
195
 
62
196
  function markRuntimeLimit() {
@@ -66,6 +200,144 @@ function markRuntimeLimit() {
66
200
  } catch {}
67
201
  }
68
202
 
203
+ // --- sandbox memory envelope ------------------------------------------------
204
+ // Reads the cgroup the sandbox actually lives in, from OUTSIDE the customer
205
+ // event loop. cgroup v2 first, then v1; where neither exists (macOS
206
+ // local-process, for instance) the watch reports itself off once and stays off.
207
+ let memorySource = null;
208
+ let memoryWatchDisabledReported = false;
209
+ let lastMemoryPressureSignalAt = 0;
210
+
211
+ function readMemoryNumberFile(path) {
212
+ try {
213
+ const raw = fs.readFileSync(path, 'utf8').trim();
214
+ if (!raw) return null;
215
+ if (raw === 'max') return Number.POSITIVE_INFINITY;
216
+ const value = Number.parseInt(raw, 10);
217
+ return Number.isFinite(value) ? value : null;
218
+ } catch {}
219
+ return null;
220
+ }
221
+
222
+ function memorySourceCandidates() {
223
+ if (memorySource) return [memorySource];
224
+ return [
225
+ {
226
+ kind: 'cgroup_v2',
227
+ usagePath: cgroupRoot + '/memory.current',
228
+ limitPath: cgroupRoot + '/memory.max',
229
+ },
230
+ {
231
+ kind: 'cgroup_v1',
232
+ usagePath: cgroupRoot + '/memory.usage_in_bytes',
233
+ limitPath: cgroupRoot + '/memory.limit_in_bytes',
234
+ },
235
+ {
236
+ kind: 'cgroup_v1_nested',
237
+ usagePath: cgroupRoot + '/memory/memory.usage_in_bytes',
238
+ limitPath: cgroupRoot + '/memory/memory.limit_in_bytes',
239
+ },
240
+ ];
241
+ }
242
+
243
+ function readCgroupMemory() {
244
+ const candidates = memorySourceCandidates();
245
+ for (let index = 0; index < candidates.length; index += 1) {
246
+ const candidate = candidates[index];
247
+ const usageBytes = readMemoryNumberFile(candidate.usagePath);
248
+ const limitBytes = readMemoryNumberFile(candidate.limitPath);
249
+ if (usageBytes === null || limitBytes === null) continue;
250
+ if (!(limitBytes > 0)) continue;
251
+ if (limitBytes >= ${RUNNER_MEMORY_CGROUP_UNLIMITED_BYTES}) continue;
252
+ memorySource = candidate;
253
+ return {
254
+ source: candidate.kind,
255
+ usageBytes: usageBytes,
256
+ limitBytes: limitBytes,
257
+ ratio: usageBytes / limitBytes,
258
+ };
259
+ }
260
+ return null;
261
+ }
262
+
263
+ function reportMemoryWatchDisabled() {
264
+ if (memoryWatchDisabledReported) return;
265
+ memoryWatchDisabledReported = true;
266
+ // stdout, not stderr: an absent cgroup is an expected substrate fact on a
267
+ // host without memory accounting, and stderr is reserved for terminations.
268
+ process.stdout.write(
269
+ '[memory] sandbox memory watch disabled: no cgroup memory accounting under ' +
270
+ cgroupRoot +
271
+ '\\n',
272
+ );
273
+ }
274
+
275
+ function readMemoryLedger() {
276
+ if (!memoryLedgerPath) return null;
277
+ try {
278
+ const raw = fs.readFileSync(memoryLedgerPath, 'utf8');
279
+ if (!raw.trim()) return null;
280
+ return JSON.parse(raw);
281
+ } catch {}
282
+ return null;
283
+ }
284
+
285
+ function publishMemoryPressure(sample) {
286
+ if (!memoryPressurePath) return;
287
+ try {
288
+ fs.writeFileSync(
289
+ memoryPressurePath,
290
+ JSON.stringify({
291
+ schemaVersion: 1,
292
+ source: sample.source,
293
+ usageBytes: sample.usageBytes,
294
+ limitBytes: sample.limitBytes,
295
+ ratio: sample.ratio,
296
+ at: Date.now(),
297
+ }),
298
+ );
299
+ } catch {}
300
+ }
301
+
302
+ const memoryWatch = setInterval(() => {
303
+ if (settled || memoryAborted) return;
304
+ const sample = readCgroupMemory();
305
+ if (!sample) {
306
+ reportMemoryWatchDisabled();
307
+ return;
308
+ }
309
+ if (sample.ratio >= ${RUNNER_MEMORY_ABORT_RATIO}) {
310
+ memoryAborted = true;
311
+ clearInterval(memoryWatch);
312
+ process.stderr.write(
313
+ '${RUNTIME_SANDBOX_MEMORY_ABORT_REASON}: sandbox memory usage ' +
314
+ sample.usageBytes +
315
+ ' of ' +
316
+ sample.limitBytes +
317
+ ' bytes reached the abort threshold; the runner was stopped before the kernel could kill it.\\n',
318
+ );
319
+ stopChild('${RUNTIME_SANDBOX_MEMORY_ABORT_REASON}', {
320
+ memorySource: sample.source,
321
+ memoryUsageBytes: sample.usageBytes,
322
+ memoryLimitBytes: sample.limitBytes,
323
+ memoryUsageRatio: Math.round(sample.ratio * 10_000) / 10_000,
324
+ memoryLedger: readMemoryLedger(),
325
+ });
326
+ return;
327
+ }
328
+ if (sample.ratio >= ${RUNNER_MEMORY_PRESSURE_RATIO}) {
329
+ const now = Date.now();
330
+ if (now - lastMemoryPressureSignalAt < ${RUNNER_MEMORY_PRESSURE_SIGNAL_INTERVAL_MS}) {
331
+ return;
332
+ }
333
+ lastMemoryPressureSignalAt = now;
334
+ publishMemoryPressure(sample);
335
+ try {
336
+ child.kill('SIGUSR2');
337
+ } catch {}
338
+ }
339
+ }, ${RUNNER_MEMORY_WATCH_INTERVAL_MS});
340
+
69
341
  function armRuntimeDeadline() {
70
342
  if (settled || runtimeTimer) return;
71
343
  clearTimeout(startupTimer);
@@ -124,6 +396,7 @@ function finish(code) {
124
396
  if (settled) return;
125
397
  settled = true;
126
398
  clearInterval(runtimeBoundaryPoll);
399
+ clearInterval(memoryWatch);
127
400
  clearTimeout(startupTimer);
128
401
  if (runtimeTimer) clearTimeout(runtimeTimer);
129
402
  if (terminalGraceTimer) clearTimeout(terminalGraceTimer);
@@ -134,6 +407,8 @@ function finish(code) {
134
407
  ? 1
135
408
  : terminalFlushTimedOut
136
409
  ? 1
410
+ : memoryAborted
411
+ ? 1
137
412
  : typeof code === 'number'
138
413
  ? code
139
414
  : 1;
@@ -511,6 +511,8 @@ export async function inspectDetachedDaytonaRunner(input: {
511
511
  cmdId: string;
512
512
  outputPath: string;
513
513
  exitCodePath: string;
514
+ /** Refuse to read a sandbox outside the launch-pinned Daytona organization. */
515
+ expectedOrganizationId?: string | null;
514
516
  }): Promise<{
515
517
  exitCode: number | null;
516
518
  result: PlayRunnerResult | null;
@@ -521,7 +523,8 @@ export async function inspectDetachedDaytonaRunner(input: {
521
523
  | 'result_missing'
522
524
  | 'command_inspection_failed'
523
525
  | 'sandbox_missing'
524
- | 'sandbox_lookup_failed';
526
+ | 'sandbox_lookup_failed'
527
+ | 'wrong_routing_domain';
525
528
  detail: string | null;
526
529
  };
527
530
  }> {
@@ -529,6 +532,21 @@ export async function inspectDetachedDaytonaRunner(input: {
529
532
  const { clientOptions } = loadDaytonaRequiredConfig();
530
533
  const daytona = daytonaSdkClientFactory.createFull(clientOptions);
531
534
  const sandbox = (await daytona.get(input.sandboxId)) as DaytonaSandbox;
535
+ const expectedOrganizationId = input.expectedOrganizationId?.trim() || null;
536
+ const observedOrganizationId = sandbox.organizationId?.trim() || null;
537
+ if (
538
+ expectedOrganizationId &&
539
+ observedOrganizationId !== expectedOrganizationId
540
+ ) {
541
+ return {
542
+ exitCode: null,
543
+ result: null,
544
+ diagnosis: {
545
+ stage: 'wrong_routing_domain',
546
+ detail: 'Sandbox belongs to a different Daytona organization.',
547
+ },
548
+ };
549
+ }
532
550
  let exitCode: number | null = null;
533
551
  let commandFound = false;
534
552
  let commandInspectionError: string | null = null;
@@ -911,6 +929,7 @@ export function retryableRuntimeInitializationFailureReason(
911
929
  | 'runtime_receipt_gateway_transport'
912
930
  | 'daytona_public_status'
913
931
  | 'daytona_runner_startup_transport'
932
+ | 'daytona_infrastructure'
914
933
  | null {
915
934
  if (result.status !== 'failed') return null;
916
935
  if (RUNTIME_POSTGRES_CONNECT_RETRY_PATTERN.test(result.error)) {
@@ -941,6 +960,13 @@ export function retryableRuntimeInitializationFailureReason(
941
960
  ) {
942
961
  return 'runtime_api_transport';
943
962
  }
963
+ if (
964
+ rowsProcessed === 0 &&
965
+ result.steps.length === 0 &&
966
+ DAYTONA_INFRASTRUCTURE_RETRY_PATTERN.test(result.error)
967
+ ) {
968
+ return 'daytona_infrastructure';
969
+ }
944
970
  return null;
945
971
  }
946
972
 
@@ -441,6 +441,22 @@ export const localProcessPlayRunnerBackend: PlayRunnerBackend = {
441
441
  workspaceRoot,
442
442
  'runtime-limit.marker',
443
443
  );
444
+ const terminationDiagnosticPath = join(
445
+ workspaceRoot,
446
+ 'runtime-termination.json',
447
+ );
448
+ // The local substrate shares the sandbox memory envelope: same
449
+ // watchdog, same flags, same signal contract. On a host without
450
+ // cgroup memory accounting the watch reports itself off once and
451
+ // the runtime deadline behaviour is unchanged.
452
+ const memoryLedgerPath = join(
453
+ workspaceRoot,
454
+ 'memory-ledger.json',
455
+ );
456
+ const memoryPressurePath = join(
457
+ workspaceRoot,
458
+ 'memory-pressure.json',
459
+ );
444
460
  const result = await runCommand(
445
461
  'node',
446
462
  [
@@ -454,6 +470,9 @@ export const localProcessPlayRunnerBackend: PlayRunnerBackend = {
454
470
  String(PLAY_RUNNER_STARTUP_GRACE_SECONDS * 1_000),
455
471
  String(PLAY_RUNNER_TERMINAL_GRACE_SECONDS * 1_000),
456
472
  runtimeLimitMarkerPath,
473
+ terminationDiagnosticPath,
474
+ memoryLedgerPath,
475
+ memoryPressurePath,
457
476
  ],
458
477
  {
459
478
  cwd: workspaceRoot,
@@ -6,6 +6,7 @@ import {
6
6
  SandboxRunnerPreCodeDeferralError,
7
7
  } from '../types';
8
8
  import { buildPlayRunnerBundle } from '../bundle';
9
+ import { findPlayRunnerResult, parsePlayRunnerEvents } from '../runner-events';
9
10
  import type {
10
11
  PlayRunnerExecutionConfig,
11
12
  PlayRunnerResult,
@@ -39,6 +40,7 @@ const MODAL_SANDBOX_CREATE_TIMEOUT_MS =
39
40
  RUNTIME_RELIABILITY_POLICY.sandbox.modalSandboxCreateTimeoutMs;
40
41
  const MODAL_PAYLOAD_UPLOAD_TIMEOUT_MS = 90_000;
41
42
  const MODAL_TERMINATION_TIMEOUT_MS = 10_000;
43
+ const MODAL_DETACHED_INSPECTION_TIMEOUT_MS = 10_000;
42
44
  const MODAL_CAPACITY_RELEASE_RETRY_DELAYS_MS = [0, 100, 500, 2_000] as const;
43
45
  const MODAL_LATE_CREATE_PERSIST_RETRY_DELAYS_MS = [0, 100, 500, 2_000] as const;
44
46
  type ModalSandbox = Awaited<ReturnType<ModalClient['sandboxes']['create']>>;
@@ -820,6 +822,112 @@ export async function findModalSandboxIdsByTags(input: {
820
822
  return [...new Set(sandboxIds)];
821
823
  }
822
824
 
825
+ /**
826
+ * Best-effort terminal recovery for an already-detached Modal runner. This
827
+ * reads only the payload wrapper's captured files; it never executes Play
828
+ * code or launches a replacement sandbox.
829
+ */
830
+ export async function inspectDetachedModalRunner(input: {
831
+ sandboxId: string;
832
+ outputPath: string;
833
+ exitCodePath: string;
834
+ expectedAppId?: string | null;
835
+ }): Promise<{
836
+ exitCode: number | null;
837
+ result: PlayRunnerResult | null;
838
+ diagnosis: { stage: string; detail: string | null };
839
+ }> {
840
+ const expectedAppId = input.expectedAppId?.trim() || null;
841
+ if (!expectedAppId) {
842
+ return {
843
+ exitCode: null,
844
+ result: null,
845
+ diagnosis: {
846
+ stage: 'missing_routing_domain',
847
+ detail: 'Modal sandbox inspection requires the launch-pinned app.',
848
+ },
849
+ };
850
+ }
851
+
852
+ let timer: ReturnType<typeof setTimeout> | undefined;
853
+ try {
854
+ return await Promise.race([
855
+ (async () => {
856
+ const { client } = await loadModalClientConfig();
857
+ for await (const sandbox of client.sandboxes.list({
858
+ appId: expectedAppId,
859
+ })) {
860
+ if (sandbox.sandboxId !== input.sandboxId) continue;
861
+ const [output, exitCode] = await Promise.allSettled([
862
+ sandbox.filesystem.readText(input.outputPath),
863
+ sandbox.filesystem.readText(input.exitCodePath),
864
+ ]);
865
+ if (output.status !== 'fulfilled') {
866
+ return {
867
+ exitCode: null,
868
+ result: null,
869
+ diagnosis: {
870
+ stage: 'result_missing',
871
+ detail: 'Captured runner output is unavailable.',
872
+ },
873
+ };
874
+ }
875
+ const parsedExitCode =
876
+ exitCode.status === 'fulfilled' &&
877
+ /^-?\d+$/u.test(exitCode.value.trim())
878
+ ? Number(exitCode.value.trim())
879
+ : null;
880
+ const result =
881
+ findPlayRunnerResult(parsePlayRunnerEvents(output.value)) ?? null;
882
+ return {
883
+ exitCode:
884
+ parsedExitCode !== null && Number.isSafeInteger(parsedExitCode)
885
+ ? parsedExitCode
886
+ : null,
887
+ result,
888
+ diagnosis: result
889
+ ? { stage: 'salvaged', detail: null }
890
+ : { stage: 'result_missing', detail: null },
891
+ };
892
+ }
893
+ return {
894
+ exitCode: null,
895
+ result: null,
896
+ diagnosis: {
897
+ stage: 'sandbox_missing',
898
+ detail: 'Sandbox was not found in the launch-pinned Modal app.',
899
+ },
900
+ };
901
+ })(),
902
+ new Promise<never>((_resolve, reject) => {
903
+ timer = setTimeout(() => {
904
+ reject(
905
+ new Error(
906
+ `Modal detached-runner inspection exceeded ${MODAL_DETACHED_INSPECTION_TIMEOUT_MS}ms.`,
907
+ ),
908
+ );
909
+ }, MODAL_DETACHED_INSPECTION_TIMEOUT_MS);
910
+ }),
911
+ ]);
912
+ } catch (error) {
913
+ console.warn('[play-runner.modal.detached_inspect_failed]', {
914
+ sandboxId: input.sandboxId,
915
+ expectedAppId,
916
+ error: error instanceof Error ? error.message : String(error),
917
+ });
918
+ return {
919
+ exitCode: null,
920
+ result: null,
921
+ diagnosis: {
922
+ stage: 'sandbox_lookup_failed',
923
+ detail: 'Modal sandbox inspection could not complete.',
924
+ },
925
+ };
926
+ } finally {
927
+ if (timer) clearTimeout(timer);
928
+ }
929
+ }
930
+
823
931
  export async function readDetachedModalRuntimeCompletion(input: {
824
932
  sandboxId: string;
825
933
  runtimeCompletedPath: string;
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  deleteDaytonaSandboxByIdWithOutcome,
3
+ inspectDetachedDaytonaRunner,
3
4
  readDetachedDaytonaRuntimeCompletion,
4
5
  } from './backends/daytona';
5
6
  import {
6
7
  deleteModalSandboxById,
8
+ inspectDetachedModalRunner,
7
9
  readDetachedModalRuntimeCompletion,
8
10
  } from './backends/modal';
9
11
  import {
@@ -12,6 +14,7 @@ import {
12
14
  type RuntimeSandboxRef,
13
15
  } from '../runtime-sandbox-contract';
14
16
  import type { DetachedRunnerSuspension } from '../suspension';
17
+ import type { PlayRunnerResult } from '../protocol';
15
18
 
16
19
  export type {
17
20
  RuntimeSandboxProviderId,
@@ -21,6 +24,15 @@ export type {
21
24
  export type RuntimeSandboxReconciliationAdapter = {
22
25
  provider: RuntimeSandboxProviderId;
23
26
  delete(ref: RuntimeSandboxRef): Promise<RuntimeSandboxDeleteOutcome>;
27
+ /**
28
+ * Read an already-captured detached-runner terminal without executing Play
29
+ * code. The scheduler owns whether this is safe to commit; adapters only
30
+ * implement their provider's file read.
31
+ */
32
+ inspectDetachedRunner(
33
+ ref: RuntimeSandboxRef,
34
+ suspension: DetachedRunnerSuspension,
35
+ ): Promise<RuntimeSandboxDetachedRunnerInspection | null>;
24
36
  readWorkCompletedAt(
25
37
  ref: RuntimeSandboxRef,
26
38
  path: string,
@@ -29,12 +41,22 @@ export type RuntimeSandboxReconciliationAdapter = {
29
41
 
30
42
  export type RuntimeSandboxReconciliation = {
31
43
  delete(ref: RuntimeSandboxRef): Promise<RuntimeSandboxDeleteOutcome>;
44
+ inspectDetachedRunner(
45
+ ref: RuntimeSandboxRef,
46
+ suspension: DetachedRunnerSuspension,
47
+ ): Promise<RuntimeSandboxDetachedRunnerInspection | null>;
32
48
  readWorkCompletedAt(
33
49
  ref: RuntimeSandboxRef,
34
50
  path: string,
35
51
  ): Promise<number | null>;
36
52
  };
37
53
 
54
+ export type RuntimeSandboxDetachedRunnerInspection = {
55
+ exitCode: number | null;
56
+ result: PlayRunnerResult | null;
57
+ diagnosis: { stage: string; detail: string | null };
58
+ };
59
+
38
60
  export type RuntimeSandboxDeleteOutcome =
39
61
  | {
40
62
  kind: 'deleted' | 'already_absent';
@@ -94,6 +116,8 @@ export function createRuntimeSandboxReconciliation(input: {
94
116
  };
95
117
  return {
96
118
  delete: (ref) => resolve(ref).delete(ref),
119
+ inspectDetachedRunner: (ref, suspension) =>
120
+ resolve(ref).inspectDetachedRunner(ref, suspension),
97
121
  readWorkCompletedAt: (ref, path) =>
98
122
  resolve(ref).readWorkCompletedAt(ref, path),
99
123
  };
@@ -181,6 +205,15 @@ export const defaultRuntimeSandboxReconciliation =
181
205
  routingDomain: outcome.organizationId,
182
206
  };
183
207
  },
208
+ inspectDetachedRunner: (ref, suspension) =>
209
+ inspectDetachedDaytonaRunner({
210
+ sandboxId: ref.resourceId,
211
+ sessionId: suspension.sessionId,
212
+ cmdId: suspension.cmdId,
213
+ outputPath: suspension.outputPath,
214
+ exitCodePath: suspension.exitCodePath,
215
+ expectedOrganizationId: ref.routingDomain,
216
+ }),
184
217
  readWorkCompletedAt: (ref, path) =>
185
218
  readDetachedDaytonaRuntimeCompletion({
186
219
  sandboxId: ref.resourceId,
@@ -200,6 +233,13 @@ export const defaultRuntimeSandboxReconciliation =
200
233
  routingDomain: outcome.appId,
201
234
  };
202
235
  },
236
+ inspectDetachedRunner: (ref, suspension) =>
237
+ inspectDetachedModalRunner({
238
+ sandboxId: ref.resourceId,
239
+ outputPath: suspension.outputPath,
240
+ exitCodePath: suspension.exitCodePath,
241
+ expectedAppId: ref.routingDomain,
242
+ }),
203
243
  readWorkCompletedAt: (ref, path) =>
204
244
  readDetachedModalRuntimeCompletion({
205
245
  sandboxId: ref.resourceId,