deepline 0.3.476 → 0.3.478

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 (171) hide show
  1. package/dist/bundling-sources/sdk/src/client-core.ts +5 -4
  2. package/dist/bundling-sources/sdk/src/http-core.ts +84 -3
  3. package/dist/bundling-sources/sdk/src/http.ts +2 -0
  4. package/dist/bundling-sources/sdk/src/play.ts +8 -2
  5. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  6. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +33 -0
  8. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +10 -2
  9. package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +77 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +1 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +3 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +11 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +13 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +77 -4
  15. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +83 -1
  16. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +7 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-compute-usage.ts +6 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +215 -22
  19. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +42 -0
  20. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +13 -12
  21. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +5 -2
  22. package/dist/bundling-sources/shared_libs/plays/sandbox-runtime-limits.ts +37 -12
  23. package/dist/bundling-sources/shared_libs/product-notifications/billing-sales.ts +1 -0
  24. package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +29 -0
  25. package/dist/bundling-sources/shared_libs/product-notifications/events.ts +15 -0
  26. package/dist/cli/{auth-A22724UX.mjs → auth-SA4DJCSO.mjs} +5 -5
  27. package/dist/cli/auth-U5SNVC5F.js +41 -0
  28. package/dist/cli/{billing-T5Q6OO6M.js → billing-462SAHWX.js} +107 -87
  29. package/dist/cli/{billing-IIBEZ6J7.mjs → billing-Z4D35MV5.mjs} +34 -14
  30. package/dist/cli/{chunk-NPNQCPFP.mjs → chunk-2FTUPGTN.mjs} +2 -2
  31. package/dist/cli/{chunk-2WEPQSA2.mjs → chunk-2JS5DNVX.mjs} +5 -5
  32. package/dist/cli/{chunk-L45IECBZ.js → chunk-2MSKG4ER.js} +7 -6
  33. package/dist/cli/{chunk-JU2KBLOR.mjs → chunk-3LTLQAU4.mjs} +4 -4
  34. package/dist/cli/{chunk-ZV4OWYLZ.mjs → chunk-3V7INABZ.mjs} +2 -2
  35. package/dist/cli/{chunk-VPAVNEDQ.mjs → chunk-4C3TMIF3.mjs} +4 -4
  36. package/dist/cli/{chunk-H3GOAZHV.mjs → chunk-4RJRGQ5Y.mjs} +2 -2
  37. package/dist/cli/{chunk-AS3VECCY.js → chunk-4RV7U3RP.js} +7 -7
  38. package/dist/cli/{chunk-UHTJWDYB.mjs → chunk-645RNF2Y.mjs} +1 -1
  39. package/dist/cli/{chunk-TEJSRJNH.js → chunk-6EEMW7YP.js} +8 -8
  40. package/dist/cli/{chunk-ZOXV6WFI.mjs → chunk-75DAGCEX.mjs} +1 -1
  41. package/dist/cli/{chunk-NTJDZURV.mjs → chunk-7HTWT3Z4.mjs} +2 -2
  42. package/dist/cli/chunk-7I4B6V2S.mjs +127 -0
  43. package/dist/cli/{chunk-MZSLFWOT.js → chunk-7UFFWPQ2.js} +13 -9
  44. package/dist/cli/{chunk-5FBCMLJW.js → chunk-A5LPDB6N.js} +6 -6
  45. package/dist/cli/{chunk-4YWYLHED.mjs → chunk-ABBP5ZCI.mjs} +3 -3
  46. package/dist/cli/{chunk-IC3BZJR7.js → chunk-AQYSZYNE.js} +8 -8
  47. package/dist/cli/{chunk-WTPRRKZA.js → chunk-AYW242XF.js} +37 -37
  48. package/dist/cli/{chunk-CFSCHLNQ.mjs → chunk-BLAZVZVD.mjs} +23 -5
  49. package/dist/cli/{chunk-KGLIDLJ2.js → chunk-D2LKEQKI.js} +5 -5
  50. package/dist/cli/{chunk-4WMIMSSI.mjs → chunk-F2SJYUBW.mjs} +5 -5
  51. package/dist/cli/{chunk-KCP6XNRR.mjs → chunk-F44O4PEP.mjs} +1 -1
  52. package/dist/cli/{chunk-EBWYNNTJ.js → chunk-HMUBLAT4.js} +5 -5
  53. package/dist/cli/{chunk-UYA7LJGC.mjs → chunk-IL5AF6AR.mjs} +4 -2
  54. package/dist/cli/{chunk-B7PEXXRW.js → chunk-IWYWZXCA.js} +53 -53
  55. package/dist/cli/{chunk-PGNLD6L6.js → chunk-J3ZYRN5L.js} +10 -10
  56. package/dist/cli/{chunk-VKBZLV2K.mjs → chunk-JH775FF6.mjs} +1 -1
  57. package/dist/cli/{chunk-5AEEINTU.js → chunk-KBBZXWXF.js} +63 -11
  58. package/dist/cli/{chunk-FVPT44IJ.mjs → chunk-M5USVQ33.mjs} +55 -3
  59. package/dist/cli/{chunk-2YHDNHRR.mjs → chunk-N573OK4W.mjs} +4 -4
  60. package/dist/cli/{chunk-WFVEMDQL.js → chunk-OKAZHDYN.js} +4 -2
  61. package/dist/cli/{chunk-J4FCTFQS.mjs → chunk-PHBOI24Y.mjs} +10 -10
  62. package/dist/cli/{chunk-D7EQ4PI5.mjs → chunk-PLJV6SX4.mjs} +1 -1
  63. package/dist/cli/{chunk-YQOWMQEW.mjs → chunk-PPHHPAQV.mjs} +1 -1
  64. package/dist/cli/{chunk-CBVCCXIY.js → chunk-PZHRNSMU.js} +21 -21
  65. package/dist/cli/{chunk-LJFIUT5R.mjs → chunk-QB3Q73LP.mjs} +2 -2
  66. package/dist/cli/{chunk-ULKGYVYP.js → chunk-QVZNUQAP.js} +2 -2
  67. package/dist/cli/{chunk-A6BETJ2A.js → chunk-S3BKOCVB.js} +1 -1
  68. package/dist/cli/{chunk-F5PYBZ2H.mjs → chunk-S7UEKKOX.mjs} +2 -1
  69. package/dist/cli/{chunk-W3BHZUPL.js → chunk-SCHE5QRN.js} +3 -3
  70. package/dist/cli/{chunk-YEJDFZJP.js → chunk-SXY4DOVA.js} +16 -16
  71. package/dist/cli/{chunk-LZCYDU6Q.mjs → chunk-TLF3KGWG.mjs} +2 -2
  72. package/dist/cli/{chunk-5PYSYW63.js → chunk-TQWI7LJX.js} +26 -26
  73. package/dist/cli/{chunk-7HAUVMRF.js → chunk-TW3TTKW5.js} +3 -3
  74. package/dist/cli/{chunk-FK7SBMSZ.js → chunk-UXQVSDFP.js} +3 -3
  75. package/dist/cli/{chunk-HSANVBVL.js → chunk-V7I65ME7.js} +2 -2
  76. package/dist/cli/{chunk-6YZYCKWD.js → chunk-VE25VUTE.js} +70 -52
  77. package/dist/cli/{chunk-VLYUFZUT.js → chunk-XFY3JOP2.js} +9 -9
  78. package/dist/cli/{chunk-MYYOCJZ2.js → chunk-XOORR6P3.js} +93 -93
  79. package/dist/cli/chunk-YGCKMI5P.js +127 -0
  80. package/dist/cli/{chunk-3A7TDUZV.mjs → chunk-ZBSUXXZS.mjs} +13 -9
  81. package/dist/cli/{chunk-NREO7TT4.mjs → chunk-ZSU3C4ST.mjs} +2 -2
  82. package/dist/cli/command-docs.js +2 -2
  83. package/dist/cli/command-docs.mjs +1 -1
  84. package/dist/cli/{csv-GPVCETS2.mjs → csv-3SNWBO5F.mjs} +4 -4
  85. package/dist/cli/{csv-FXOQ2PBE.js → csv-LAYTDYIF.js} +13 -13
  86. package/dist/cli/{db-F5UJTBGP.mjs → db-72JKGQSK.mjs} +6 -6
  87. package/dist/cli/{db-LXYV4GPZ.js → db-OOUOSURL.js} +27 -27
  88. package/dist/cli/describe-7HYV44MO.js +48 -0
  89. package/dist/cli/{describe-GYWCI7I6.mjs → describe-KDPNOEUJ.mjs} +17 -17
  90. package/dist/cli/describe-core.d.mts +3 -3
  91. package/dist/cli/describe-core.d.ts +3 -3
  92. package/dist/cli/{executions-6NL2YW76.mjs → executions-O6JS3UKX.mjs} +6 -6
  93. package/dist/cli/{executions-6SJKTY2L.js → executions-RJNMGJEE.js} +8 -8
  94. package/dist/cli/{feedback-E3HULSQD.js → feedback-FXB6NV3Y.js} +8 -8
  95. package/dist/cli/{feedback-7AMQCNTB.mjs → feedback-T3JH3CWC.mjs} +4 -4
  96. package/dist/cli/git-5LTKPXGN.js +25 -0
  97. package/dist/cli/{git-74IS2EFH.mjs → git-6KAQSH5J.mjs} +5 -5
  98. package/dist/cli/index.js +161 -142
  99. package/dist/cli/index.mjs +65 -46
  100. package/dist/cli/{legacy-noops-52FFRTRZ.mjs → legacy-noops-7HDGEGTK.mjs} +5 -5
  101. package/dist/cli/{legacy-noops-ALWUQPKS.js → legacy-noops-PAOGRKUK.js} +8 -8
  102. package/dist/cli/{monitors-Z4ADOGTY.js → monitors-KQFI7TVJ.js} +91 -91
  103. package/dist/cli/{monitors-VTJIWWW2.mjs → monitors-LE2AYK3Z.mjs} +7 -7
  104. package/dist/cli/{org-2BTEKDF5.js → org-J6XFHDJE.js} +72 -72
  105. package/dist/cli/{org-7GU6H7JL.mjs → org-Z4DMJ72I.mjs} +6 -6
  106. package/dist/cli/{pages-I6PFQGEU.mjs → pages-5LYS6Y4I.mjs} +6 -6
  107. package/dist/cli/{pages-MG36E7RY.js → pages-FIXBZQJG.js} +17 -17
  108. package/dist/cli/{play-VN52S3RV.js → play-D7WPXADH.js} +604 -680
  109. package/dist/cli/{play-7KFSLJX2.mjs → play-GILXXIN7.mjs} +45 -121
  110. package/dist/cli/{providers-W5XLII66.js → providers-KN7YACT6.js} +9 -9
  111. package/dist/cli/{providers-D46JDEL2.mjs → providers-OQWL4Q4W.mjs} +6 -6
  112. package/dist/cli/{quickstart-4GTXBZHY.mjs → quickstart-2LRIORFM.mjs} +4 -4
  113. package/dist/cli/{quickstart-TIZAC63S.js → quickstart-M2OBP3W3.js} +6 -6
  114. package/dist/cli/{run-FOVEMYYX.js → run-6YZZO7VM.js} +36 -36
  115. package/dist/cli/{run-LE3D4LVU.mjs → run-GYRBKQZT.mjs} +22 -22
  116. package/dist/cli/search-OVKWA4AV.js +32 -0
  117. package/dist/cli/{search-6YL6WJMQ.mjs → search-VUPK6BNU.mjs} +7 -7
  118. package/dist/cli/{secrets-CSTTTMZH.js → secrets-RE3UMZB7.js} +15 -15
  119. package/dist/cli/{secrets-D66U6RTT.mjs → secrets-ULRGU7NM.mjs} +6 -6
  120. package/dist/cli/{sessions-XIO3OUER.mjs → sessions-2ZZVJ3P2.mjs} +5 -5
  121. package/dist/cli/{sessions-45M2NRAA.js → sessions-T4UMHNDW.js} +6 -6
  122. package/dist/cli/{settings-N42CVKSK.js → settings-BRV6AYFM.js} +78 -78
  123. package/dist/cli/{settings-R33X2D5T.mjs → settings-PDY4TJU6.mjs} +11 -11
  124. package/dist/cli/{setup-CBBC4FQP.mjs → setup-CXL6C5Z5.mjs} +9 -9
  125. package/dist/cli/{setup-HREDVNF2.js → setup-JONYJYBK.js} +44 -44
  126. package/dist/cli/{skills-4X6WS2PT.mjs → skills-TOMVQD5O.mjs} +7 -7
  127. package/dist/cli/skills-UK5AK35I.js +33 -0
  128. package/dist/cli/{tools-YR46YFR2.mjs → tools-B2LJK4MM.mjs} +16 -16
  129. package/dist/cli/tools-HH6E5G7J.js +55 -0
  130. package/dist/cli/{update-5CYAZX5X.mjs → update-CD6T2REC.mjs} +9 -9
  131. package/dist/cli/update-EGXT3EIJ.js +33 -0
  132. package/dist/cli/{workflow-M7AA722W.js → workflow-PZML43E2.js} +17 -17
  133. package/dist/cli/{workflow-NKQ3CWPK.mjs → workflow-ZAOBP6I7.mjs} +7 -7
  134. package/dist/{client-AjmfA5nR.d.mts → client-B1s1yJdT.d.mts} +1 -1
  135. package/dist/{client-DMyY-QL8.d.ts → client-DqUseRzd.d.ts} +1 -1
  136. package/dist/{client-core-C705vFyV.d.ts → client-core-Bd7tP9Uz.d.ts} +45 -6
  137. package/dist/{client-core-CpBbvLEX.d.mts → client-core-BijQEQ70.d.mts} +45 -6
  138. package/dist/{compiler-manifest-hqIAukR0.d.mts → compiler-manifest-B1m5tRzz.d.mts} +23 -17
  139. package/dist/{compiler-manifest-hqIAukR0.d.ts → compiler-manifest-B1m5tRzz.d.ts} +23 -17
  140. package/dist/{errors-CRO2qQ6k.d.ts → errors-3g71OMXy.d.ts} +1 -1
  141. package/dist/{errors-DIHVetNY.d.mts → errors-QiC6am-q.d.mts} +1 -1
  142. package/dist/{index-DVZ_X4Gb.d.mts → index-Biz5toO4.d.mts} +1 -1
  143. package/dist/{index-B8DVoeYt.d.ts → index-CIBiwzhK.d.ts} +1 -1
  144. package/dist/index.d.mts +15 -9
  145. package/dist/index.d.ts +15 -9
  146. package/dist/index.js +9 -9
  147. package/dist/index.mjs +6 -6
  148. package/dist/install-integrity.json +112 -110
  149. package/dist/plays/bundle-play-file.d.mts +3 -3
  150. package/dist/plays/bundle-play-file.d.ts +3 -3
  151. package/dist/plays/bundle-play-file.mjs +2 -1
  152. package/dist/plays/play-description.d.mts +2 -2
  153. package/dist/plays/play-description.d.ts +2 -2
  154. package/dist/plays/run-request.d.mts +2 -2
  155. package/dist/plays/run-request.d.ts +2 -2
  156. package/dist/release.d.mts +1 -1
  157. package/dist/release.d.ts +1 -1
  158. package/dist/release.js +2 -2
  159. package/dist/release.mjs +1 -1
  160. package/dist/server.d.mts +4 -4
  161. package/dist/server.d.ts +4 -4
  162. package/dist/server.js +5 -5
  163. package/dist/server.mjs +3 -3
  164. package/package.json +1 -1
  165. package/dist/cli/auth-2ZY57Q7W.js +0 -41
  166. package/dist/cli/describe-IB7SUIUT.js +0 -48
  167. package/dist/cli/git-A2QIIMW5.js +0 -25
  168. package/dist/cli/search-HN6SVDQU.js +0 -32
  169. package/dist/cli/skills-QZ6HZX2T.js +0 -33
  170. package/dist/cli/tools-KFIH7UWB.js +0 -55
  171. package/dist/cli/update-VZLEJLWJ.js +0 -33
@@ -29,6 +29,18 @@ export type DetachedDaytonaStartupDiagnostic = {
29
29
  errorCode?: string;
30
30
  transportAttemptId?: string;
31
31
  } | null;
32
+ /** Fixed summaries only: raw runner output can contain customer secrets. */
33
+ launcherFailure?: {
34
+ reason:
35
+ | 'node_not_found'
36
+ | 'node_loader_failed'
37
+ | 'permission_denied'
38
+ | 'argument_list_too_long'
39
+ | 'exec_format_error'
40
+ | 'unknown_exit_127'
41
+ | 'output_unavailable';
42
+ nodeCheckExitCode: number;
43
+ };
32
44
  errors: string[];
33
45
  };
34
46
 
@@ -124,6 +136,7 @@ function summarizeProcesses(value: unknown): {
124
136
  * Process names are reduced to a Node count before logging; runner logs,
125
137
  * output, and arbitrary process names are deliberately excluded because no
126
138
  * heuristic redactor can guarantee removal of arbitrary customer secrets.
139
+ * Exit-127 shell failures are classified inside the sandbox into fixed enums.
127
140
  */
128
141
  export async function inspectDetachedDaytonaStartup(input: {
129
142
  sandbox: DaytonaSandbox;
@@ -131,6 +144,7 @@ export async function inspectDetachedDaytonaStartup(input: {
131
144
  cmdId: string;
132
145
  exitCodePath: string;
133
146
  startupDiagnosticPath?: string;
147
+ outputPath?: string;
134
148
  }): Promise<DetachedDaytonaStartupDiagnostic> {
135
149
  const errors: string[] = [];
136
150
  const diagnostic: DetachedDaytonaStartupDiagnostic = {
@@ -189,6 +203,70 @@ export async function inspectDetachedDaytonaStartup(input: {
189
203
  .catch(() => errors.push('process_snapshot_failed')),
190
204
  ]);
191
205
 
206
+ if (
207
+ input.outputPath &&
208
+ diagnostic.exitCodeFile === 127 &&
209
+ !diagnostic.runnerStartup
210
+ ) {
211
+ // The failed shell is retired immediately after this inspection. Classify
212
+ // its bounded output inside the sandbox, returning only known enum values
213
+ // and an exit code. Never ship arbitrary output through a heuristic redactor.
214
+ const quotedPath = "'" + input.outputPath.replace(/'/g, "'\\''") + "'";
215
+ try {
216
+ const result = await input.sandbox.process.executeCommand(
217
+ `
218
+ if [ -r ${quotedPath} ]; then
219
+ startup_tail="$(tail -c 4096 -- ${quotedPath} 2>/dev/null)"
220
+ case "$startup_tail" in
221
+ *"error while loading shared libraries"*) startup_reason=node_loader_failed ;;
222
+ *"not found: node"*|*node*"not found"*|*node*"No such file"*) startup_reason=node_not_found ;;
223
+ *"Permission denied"*) startup_reason=permission_denied ;;
224
+ *"Argument list too long"*) startup_reason=argument_list_too_long ;;
225
+ *"Exec format error"*) startup_reason=exec_format_error ;;
226
+ *) startup_reason=unknown_exit_127 ;;
227
+ esac
228
+ else
229
+ startup_reason=output_unavailable
230
+ fi
231
+ node --version >/dev/null 2>&1
232
+ startup_node_code=$?
233
+ printf '{"reason":"%s","nodeCheckExitCode":%s}\\n' "$startup_reason" "$startup_node_code"
234
+ `,
235
+ undefined,
236
+ {},
237
+ 5,
238
+ );
239
+ const value = JSON.parse(result.result) as Record<string, unknown>;
240
+ const reasons = [
241
+ 'node_not_found',
242
+ 'node_loader_failed',
243
+ 'permission_denied',
244
+ 'argument_list_too_long',
245
+ 'exec_format_error',
246
+ 'unknown_exit_127',
247
+ 'output_unavailable',
248
+ ];
249
+ if (
250
+ typeof value.reason !== 'string' ||
251
+ !reasons.includes(value.reason) ||
252
+ typeof value.nodeCheckExitCode !== 'number' ||
253
+ !Number.isInteger(value.nodeCheckExitCode) ||
254
+ value.nodeCheckExitCode < 0 ||
255
+ value.nodeCheckExitCode > 255
256
+ ) {
257
+ errors.push('launcher_diagnostic_invalid');
258
+ } else {
259
+ diagnostic.launcherFailure = {
260
+ reason: value.reason as NonNullable<
261
+ DetachedDaytonaStartupDiagnostic['launcherFailure']
262
+ >['reason'],
263
+ nodeCheckExitCode: value.nodeCheckExitCode,
264
+ };
265
+ }
266
+ } catch {
267
+ errors.push('launcher_diagnostic_unavailable');
268
+ }
269
+ }
192
270
  return diagnostic;
193
271
  }
194
272
 
@@ -225,6 +303,7 @@ export type DetachedDaytonaRunnerSupervisor = {
225
303
  command: string;
226
304
  exitCodePath: string;
227
305
  startupDiagnosticPath?: string;
306
+ outputPath?: string;
228
307
  }): Promise<Extract<DetachedDaytonaRunnerState, { phase: 'ready' }>>;
229
308
  };
230
309
 
@@ -394,7 +473,7 @@ export function prepareDetachedDaytonaRunner(input: {
394
473
  let started = false;
395
474
 
396
475
  return {
397
- async start({ command, exitCodePath, startupDiagnosticPath }) {
476
+ async start({ command, exitCodePath, startupDiagnosticPath, outputPath }) {
398
477
  if (started) {
399
478
  throw new Error(
400
479
  `Detached Daytona runner session ${input.sessionId} was already started.`,
@@ -435,6 +514,7 @@ export function prepareDetachedDaytonaRunner(input: {
435
514
  cmdId: state.cmdId,
436
515
  exitCodePath,
437
516
  startupDiagnosticPath,
517
+ outputPath,
438
518
  readiness: input.readiness,
439
519
  baselineHeartbeatAt: state.baselineHeartbeatAt,
440
520
  runControl: input.runControl,
@@ -461,6 +541,7 @@ export async function confirmDetachedDaytonaRunnerReady(input: {
461
541
  cmdId: string;
462
542
  exitCodePath: string;
463
543
  startupDiagnosticPath?: string;
544
+ outputPath?: string;
464
545
  readiness: DetachedRunnerReadinessPort;
465
546
  baselineHeartbeatAt: string | null;
466
547
  runControl?: <T>(
@@ -517,6 +598,7 @@ export async function confirmDetachedDaytonaRunnerReady(input: {
517
598
  cmdId: input.cmdId,
518
599
  exitCodePath: input.exitCodePath,
519
600
  startupDiagnosticPath: input.startupDiagnosticPath,
601
+ outputPath: input.outputPath,
520
602
  });
521
603
  const diagnostic = await (input.runControl
522
604
  ? input.runControl(inspect)
@@ -67,6 +67,7 @@ import {
67
67
  recoveryForRunnerStartupNotLive,
68
68
  } from '../../play-run-recovery-policy';
69
69
  import {
70
+ DaytonaNodeUnavailableError,
70
71
  stageDaytonaRunnerPayload,
71
72
  withRunnerSandboxProvider,
72
73
  } from './daytona-payload-transport';
@@ -1506,6 +1507,7 @@ const DAYTONA_UNANSWERED_REQUEST_CODES: ReadonlySet<string> = new Set([
1506
1507
  * loud too, because its input may be too large for any sandbox.
1507
1508
  */
1508
1509
  function isFailedDaytonaRunnerStart(error: unknown): boolean {
1510
+ if (error instanceof DaytonaNodeUnavailableError) return true;
1509
1511
  if (error instanceof DaytonaControlTimeoutError) return true;
1510
1512
  if (!isDaytonaProviderError(error)) return false;
1511
1513
  const status = daytonaErrorStatusForLog(error);
@@ -1536,6 +1538,9 @@ function startFailureForLog(error: unknown) {
1536
1538
  errorName: error instanceof Error ? error.name : null,
1537
1539
  providerStatus: daytonaErrorStatusForLog(error),
1538
1540
  providerCode: daytonaErrorCodeForLog(error),
1541
+ ...(error instanceof DaytonaNodeUnavailableError
1542
+ ? { nodeReadiness: error.diagnostic }
1543
+ : {}),
1539
1544
  ...(diagnostic
1540
1545
  ? {
1541
1546
  // Retain the already-sanitized evidence before sandbox retirement.
@@ -1563,6 +1568,7 @@ function startFailureForLog(error: unknown) {
1563
1568
  transportAttemptId: startup.transportAttemptId,
1564
1569
  }
1565
1570
  : null,
1571
+ launcherFailure: diagnostic.launcherFailure,
1566
1572
  inspectionErrorCount: diagnostic.errors.length,
1567
1573
  },
1568
1574
  }
@@ -2092,6 +2098,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
2092
2098
  command: stagedPayload.command,
2093
2099
  exitCodePath: stagedPayload.exitCodePath,
2094
2100
  startupDiagnosticPath: stagedPayload.startupDiagnosticPath,
2101
+ outputPath: stagedPayload.outputPath,
2095
2102
  });
2096
2103
  } catch (error) {
2097
2104
  if (
@@ -88,6 +88,9 @@ export function resolveRuntimeSandboxComputeItems(input: {
88
88
  sandboxId,
89
89
  startedAt: resource.startedAt,
90
90
  endedAt,
91
+ maxBillingDurationSeconds: resource.maxBillingDurationSeconds,
92
+ observedWallTimeSeconds:
93
+ Math.max(0, observedEndedAt - resource.startedAt) / 1_000,
91
94
  }),
92
95
  );
93
96
  }
@@ -201,6 +204,9 @@ export function resolveDaytonaRuntimeComputeItems(input: {
201
204
  sandboxId,
202
205
  startedAt: resource.startedAt,
203
206
  endedAt,
207
+ maxBillingDurationSeconds: resource.maxBillingDurationSeconds,
208
+ observedWallTimeSeconds:
209
+ Math.max(0, observedEndedAt - resource.startedAt) / 1_000,
204
210
  }),
205
211
  );
206
212
  }
@@ -38,6 +38,26 @@ const RATE_SCOPE_HOLD_TABLE = 'rate_scope_holds';
38
38
  const PHYSICAL_DISPATCH_RULE_ID = 'runtime:physical-dispatch';
39
39
 
40
40
  const MIN_CONCURRENCY_WAIT_MS = 10;
41
+ /**
42
+ * Longest re-ask when every concurrency slot is taken. A full scope re-asks
43
+ * about when its next slot should free: the average age of its live leases
44
+ * divided by their count. A fast provider stays at the 10 ms floor; one whose
45
+ * calls hold a slot for minutes no longer re-locks its scope row every 10 ms.
46
+ * On 2026-11-06 five such runs re-asked about 160 times a second for hours.
47
+ */
48
+ const MAX_CONCURRENCY_WAIT_MS = 1_000;
49
+ /**
50
+ * A healthy scope lock is held for milliseconds. An acquire that cannot get it
51
+ * within this budget stops waiting on the server instead of leaving a backend
52
+ * queued behind a stuck holder after its caller gave up. On 2026-11-06 one
53
+ * stuck holder queued such abandoned waits for 16 minutes until they exhausted
54
+ * the shared scheduler pool.
55
+ */
56
+ const RATE_STATE_LOCK_TIMEOUT_MS = 3_000;
57
+ /** Ends a bounded acquire statement before the gateway's 30 s caller budget. */
58
+ const RATE_STATE_STATEMENT_TIMEOUT_MS = 15_000;
59
+ /** Re-ask after a lock timeout, as for any other busy scope. */
60
+ const RATE_STATE_LOCK_BUSY_WAIT_MS = 1_000;
41
61
  /**
42
62
  * Longest a requester held back by the fair-share cap sleeps before it re-asks.
43
63
  * The natural wait is the rest of the current fair-share window (a sibling's
@@ -308,13 +328,13 @@ async function queryRateStateWithProbeBaseline<
308
328
  parameters: unknown[],
309
329
  ) {
310
330
  try {
311
- return await client.query<Row>(sql(true), parameters);
331
+ return await queryBoundedRateState<Row>(client, sql(true), parameters);
312
332
  } catch (error) {
313
333
  if (!isMissingProbeBaselineColumnError(error)) throw error;
314
334
  // PostgreSQL rejects the statement before any grant or reservation writes.
315
335
  // Retry only that statement; previously accepted success feedback is not
316
336
  // applied twice. No DDL, migration preflight, or global schema floor.
317
- return await client.query<Row>(sql(false), parameters);
337
+ return await queryBoundedRateState<Row>(client, sql(false), parameters);
318
338
  }
319
339
  }
320
340
 
@@ -771,6 +791,32 @@ async function seedBucketRows(
771
791
  },
772
792
  seeds: RateBucketSeed[],
773
793
  nowMs: number | null,
794
+ ): Promise<void> {
795
+ // Cold path only, so its own bounded transaction costs two round trips once
796
+ // per bucket per process. Its insert waits on the same scope row as a grant.
797
+ await client.query('BEGIN');
798
+ try {
799
+ await boundRateStateTransaction(client);
800
+ await seedBucketRowsStatement(client, input, seeds, nowMs);
801
+ await client.query('COMMIT');
802
+ } catch (error) {
803
+ try {
804
+ await client.query('ROLLBACK');
805
+ } catch {
806
+ // Preserve the original seed/storage error.
807
+ }
808
+ throw rateStateLockBusyOr(error);
809
+ }
810
+ }
811
+
812
+ async function seedBucketRowsStatement(
813
+ client: PostgresSchedulerQueryClient,
814
+ input: { bucketId: string } & {
815
+ schema?: string | null;
816
+ schedulerSchema?: string | null;
817
+ },
818
+ seeds: RateBucketSeed[],
819
+ nowMs: number | null,
774
820
  ): Promise<void> {
775
821
  await client.query(
776
822
  `
@@ -1797,6 +1843,7 @@ async function acquireWithConcurrency(
1797
1843
  while (true) {
1798
1844
  await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');
1799
1845
  try {
1846
+ await boundRateStateTransaction(client);
1800
1847
  await client.query(
1801
1848
  `SELECT bucket_id FROM ${scopeHoldTableName(input)}
1802
1849
  WHERE bucket_id = $1 FOR UPDATE`,
@@ -1824,11 +1871,73 @@ async function acquireWithConcurrency(
1824
1871
  probeBaselineSupported = false;
1825
1872
  continue;
1826
1873
  }
1827
- throw error;
1874
+ throw rateStateLockBusyOr(error);
1828
1875
  }
1829
1876
  }
1830
1877
  }
1831
1878
 
1879
+ /**
1880
+ * Bound the current rate-state transaction. `SET LOCAL` keeps the limits inside
1881
+ * this transaction: the scheduler pool runs behind PgBouncer transaction
1882
+ * pooling, where a session setting would leak to another caller.
1883
+ */
1884
+ async function boundRateStateTransaction(
1885
+ client: PostgresSchedulerQueryClient,
1886
+ ): Promise<void> {
1887
+ await client.query(
1888
+ `SELECT set_config('lock_timeout', $1, true),
1889
+ set_config('statement_timeout', $2, true)`,
1890
+ [`${RATE_STATE_LOCK_TIMEOUT_MS}ms`, `${RATE_STATE_STATEMENT_TIMEOUT_MS}ms`],
1891
+ );
1892
+ }
1893
+
1894
+ /** Bound one formerly-autocommit statement without leaking session settings. */
1895
+ async function queryBoundedRateState<
1896
+ Row extends Record<string, unknown> = Record<string, unknown>,
1897
+ >(client: PostgresSchedulerQueryClient, sql: string, parameters?: unknown[]) {
1898
+ await client.query('BEGIN');
1899
+ try {
1900
+ await boundRateStateTransaction(client);
1901
+ const result = await client.query<Row>(sql, parameters);
1902
+ await client.query('COMMIT');
1903
+ return result;
1904
+ } catch (error) {
1905
+ try {
1906
+ await client.query('ROLLBACK');
1907
+ } catch {
1908
+ // Preserve the original statement error.
1909
+ }
1910
+ throw rateStateLockBusyOr(error);
1911
+ }
1912
+ }
1913
+
1914
+ /** A scope row another transaction still holds after the lock budget. */
1915
+ class RateStateLockBusyError extends Error {
1916
+ constructor(cause: unknown) {
1917
+ super('Rate-state scope lock was not available within its budget.', {
1918
+ cause,
1919
+ });
1920
+ this.name = 'RateStateLockBusyError';
1921
+ }
1922
+ }
1923
+
1924
+ /**
1925
+ * A bounded rate-state transaction that ran out of its lock or statement
1926
+ * budget (57014 is statement_timeout).
1927
+ */
1928
+ function isRateStateBusy(error: unknown): boolean {
1929
+ return (
1930
+ error instanceof RateStateLockBusyError ||
1931
+ (error as { code?: unknown } | null)?.code === '57014'
1932
+ );
1933
+ }
1934
+
1935
+ function rateStateLockBusyOr(error: unknown): unknown {
1936
+ return (error as { code?: unknown } | null)?.code === '55P03'
1937
+ ? new RateStateLockBusyError(error)
1938
+ : error;
1939
+ }
1940
+
1832
1941
  async function grantWithConcurrency(
1833
1942
  client: PostgresSchedulerQueryClient,
1834
1943
  input: RateStateAcquireInput,
@@ -2315,7 +2424,29 @@ async function grantWithConcurrency(
2315
2424
  (SELECT cool_until_ms FROM cooling),
2316
2425
  (SELECT retry_at_ms FROM cooling)
2317
2426
  ) - (SELECT now_ms FROM refreshed LIMIT 1)${fair?.waitBranch ?? ''}
2318
- WHEN (SELECT free FROM free_slots) <= 0 THEN $7::bigint
2427
+ WHEN (SELECT free FROM free_slots) <= 0 THEN (
2428
+ -- Re-ask about when the next slot should free. Leases are never
2429
+ -- renewed, so a lease's age is its TTL minus its remaining life.
2430
+ SELECT LEAST(
2431
+ ${MAX_CONCURRENCY_WAIT_MS}::bigint,
2432
+ GREATEST(
2433
+ $7::bigint,
2434
+ CEIL(
2435
+ COALESCE(
2436
+ AVG(
2437
+ (SELECT now_ms FROM now_value)
2438
+ - (cl.expires_at_ms - $8::bigint)
2439
+ ),
2440
+ 0
2441
+ ) / GREATEST((SELECT active_count FROM concurrency_context), 1)
2442
+ )::bigint
2443
+ )
2444
+ )
2445
+ FROM ${concurrencyLeaseTableName(input)} cl
2446
+ WHERE cl.bucket_id = $1
2447
+ AND cl.rule_id = ANY($5::text[])
2448
+ AND cl.expires_at_ms > (SELECT now_ms FROM now_value)
2449
+ )
2319
2450
  ELSE LEAST((SELECT pending_recovery_wait_ms FROM provider_hold), (
2320
2451
  SELECT CEIL(
2321
2452
  MAX(
@@ -2417,9 +2548,28 @@ export async function acquirePostgresRateState(
2417
2548
  client: PostgresSchedulerQueryClient,
2418
2549
  input: RateStateAcquireInput,
2419
2550
  ): Promise<PlayRunnerRateStateAcquireResult> {
2420
- return await withRateStateAvailabilityGuard(input, () =>
2421
- acquireRateStateStatements(client, input),
2422
- );
2551
+ return await withRateStateAvailabilityGuard(input, async () => {
2552
+ try {
2553
+ const result = await acquireRateStateStatements(client, input);
2554
+ return {
2555
+ ...result,
2556
+ acknowledgedObservationBatch: concurrencyObservationBatchFrom(
2557
+ input.concurrencyObservationBatch,
2558
+ ),
2559
+ };
2560
+ } catch (error) {
2561
+ // A busy scope is normal backpressure. A rolled-back feedback fold must
2562
+ // keep its exact batch on the runner; a committed fold is safe to replay
2563
+ // after an uncertain grant because the observer watermark deduplicates it.
2564
+ if (!(error instanceof RateStateLockBusyError)) throw error;
2565
+ return {
2566
+ granted: 0,
2567
+ waitMs: RATE_STATE_LOCK_BUSY_WAIT_MS,
2568
+ leaseIds: [],
2569
+ acknowledgedObservationBatch: null,
2570
+ };
2571
+ }
2572
+ });
2423
2573
  }
2424
2574
 
2425
2575
  /**
@@ -2434,10 +2584,11 @@ export async function validatePostgresRateStatePermit(
2434
2584
  },
2435
2585
  ): Promise<PlayRunnerRateStateValidateResult> {
2436
2586
  return await withRateStateAvailabilityGuard(input, async () => {
2437
- const result = await client.query<{
2587
+ const result = await queryBoundedRateState<{
2438
2588
  server_now_ms: string | number;
2439
2589
  retry_at_ms: string | number;
2440
2590
  }>(
2591
+ client,
2441
2592
  `
2442
2593
  WITH now_value AS (
2443
2594
  SELECT COALESCE(
@@ -2495,13 +2646,14 @@ export async function claimPostgresProviderDispatch(
2495
2646
  }
2496
2647
  > {
2497
2648
  return withRateStateAvailabilityGuard(input, async () => {
2498
- const result = await client.query<{
2649
+ const result = await queryBoundedRateState<{
2499
2650
  server_now_ms: string | number;
2500
2651
  retry_at_ms: string | number;
2501
2652
  allowed: boolean;
2502
2653
  cadence_wait: boolean;
2503
2654
  stale_permit: boolean;
2504
2655
  }>(
2656
+ client,
2505
2657
  `
2506
2658
  WITH now_value AS (
2507
2659
  SELECT COALESCE($2::bigint,
@@ -2758,11 +2910,12 @@ async function concurrencyThrottleFrom(
2758
2910
  if (concurrencyRuleIds.length === 0) {
2759
2911
  return { attribution: 'not_observed', observations: [], batch: null };
2760
2912
  }
2761
- const attributedLease = await client.query<{
2913
+ const attributedLease = await queryBoundedRateState<{
2762
2914
  in_flight_at_start: number | null;
2763
2915
  target_requests_per_second: number | null;
2764
2916
  minimum_rtt_ms: number | null;
2765
2917
  }>(
2918
+ client,
2766
2919
  `
2767
2920
  SELECT
2768
2921
  in_flight_at_start,
@@ -2850,13 +3003,17 @@ async function applyConcurrencyObservations(
2850
3003
  const concurrencyRules = rules.filter(
2851
3004
  (rule) => rule.maxConcurrency != null && rule.maxConcurrency > 0,
2852
3005
  );
2853
- const countSuccesses = async () => {
3006
+ const countSuccesses = async (bounded = false) => {
2854
3007
  const successes = Math.max(
2855
3008
  0,
2856
3009
  Math.min(1_000_000, Math.trunc(input.observedSuccesses ?? 0)),
2857
3010
  );
2858
3011
  if (successes <= 0) return;
2859
- await client.query(
3012
+ const query = bounded
3013
+ ? (sql: string, params: unknown[]) =>
3014
+ queryBoundedRateState(client, sql, params)
3015
+ : (sql: string, params: unknown[]) => client.query(sql, params);
3016
+ await query(
2860
3017
  `
2861
3018
  UPDATE ${scopeHoldTableName(input)}
2862
3019
  SET rate_successes = rate_successes + $2::bigint,
@@ -2881,7 +3038,7 @@ async function applyConcurrencyObservations(
2881
3038
  if (batch === null) {
2882
3039
  // Legacy writers have no observation identity. Preserve their success
2883
3040
  // input; new writers always supply the replay-safe batch below.
2884
- await countSuccesses();
3041
+ await countSuccesses(true);
2885
3042
  return {
2886
3043
  disposition: 'unbatched',
2887
3044
  ruleCount: 0,
@@ -2895,6 +3052,7 @@ async function applyConcurrencyObservations(
2895
3052
 
2896
3053
  await client.query('BEGIN');
2897
3054
  try {
3055
+ await boundRateStateTransaction(client);
2898
3056
  const accepted = await client.query<{ last_sequence: number }>(
2899
3057
  `
2900
3058
  WITH now_value AS (
@@ -3103,7 +3261,7 @@ async function applyConcurrencyObservations(
3103
3261
  } catch {
3104
3262
  // Preserve the original controller/storage error.
3105
3263
  }
3106
- throw error;
3264
+ throw rateStateLockBusyOr(error);
3107
3265
  }
3108
3266
  }
3109
3267
 
@@ -3130,8 +3288,8 @@ async function acquireRateStateStatements(
3130
3288
  : acquirePureRate(client, grantInput, rules, seeds, requested);
3131
3289
 
3132
3290
  // Cold path only: seed missing rows / refresh changed configuration. A warm
3133
- // pure-rate acquire is exactly one statement. Concurrency-bearing acquires
3134
- // take the scope lock before the lease-count statement gets its snapshot.
3291
+ // pure-rate acquire has one grant statement in a bounded transaction.
3292
+ // Concurrency-bearing acquires lock the scope before the lease snapshot.
3135
3293
  let seeded = false;
3136
3294
  if (!warmRateBuckets.has(warmKey)) {
3137
3295
  await seedBucketRows(client, input, seeds, nowMs);
@@ -3179,7 +3337,16 @@ async function acquireRateStateStatements(
3179
3337
  // so a row widened out of band is repaired here instead of waiting for a
3180
3338
  // process restart. Repairing after the grant keeps the acquire itself to a
3181
3339
  // single granting statement, so no permit is ever issued twice.
3182
- await seedBucketRows(client, input, seeds, nowMs);
3340
+ try {
3341
+ await seedBucketRows(client, input, seeds, nowMs);
3342
+ } catch (error) {
3343
+ // The grant above already committed. A busy scope must not turn its
3344
+ // leases into a zero answer the runner never releases. Leave the bucket
3345
+ // cold so the next acquire repairs before it grants.
3346
+ if (!isRateStateBusy(error)) throw error;
3347
+ warmRateBuckets.delete(warmKey);
3348
+ return acquireResultOf(outcome);
3349
+ }
3183
3350
  }
3184
3351
  markRateBucketWarm(warmKey);
3185
3352
  return acquireResultOf(outcome);
@@ -3199,6 +3366,27 @@ async function releaseRateStateStatements(
3199
3366
  input: PlayRunnerRateStateReleaseInput & { nowMs?: number },
3200
3367
  ): Promise<void> {
3201
3368
  const allRules = normalizeRules(input.rules as AcquirePacingRule[]);
3369
+ if (
3370
+ input.observedSuccesses !== undefined ||
3371
+ input.concurrencyObservations !== undefined ||
3372
+ input.concurrencyObservationBatch !== undefined
3373
+ ) {
3374
+ const batch = concurrencyObservationBatchFrom(
3375
+ input.concurrencyObservationBatch,
3376
+ );
3377
+ if (!batch) {
3378
+ throw new Error(
3379
+ 'Rate-state success feedback requires a replay-safe observation batch.',
3380
+ );
3381
+ }
3382
+ await applyConcurrencyObservations(
3383
+ client,
3384
+ input,
3385
+ allRules,
3386
+ concurrencyObservationsFrom(input.concurrencyObservations),
3387
+ batch,
3388
+ );
3389
+ }
3202
3390
  if (allRules.length === 0) return;
3203
3391
  const refundReservationIds = [
3204
3392
  ...new Set(
@@ -3216,7 +3404,8 @@ async function releaseRateStateStatements(
3216
3404
  // no scope owner. Create its uninitialized hold before taking the normal
3217
3405
  // hold -> reservation -> rule lock path; seedBucketRows will initialize its
3218
3406
  // learner on the next admission and preserve this inherited deadline.
3219
- await client.query(
3407
+ await queryBoundedRateState(
3408
+ client,
3220
3409
  `
3221
3410
  INSERT INTO ${scopeHoldTableName(input)} (bucket_id, retry_at_ms)
3222
3411
  SELECT bucket_id, MAX(claimed_retry_at_ms)
@@ -3232,7 +3421,8 @@ async function releaseRateStateStatements(
3232
3421
  // a bucket, and an HTTP retry sees no reservation left to refund. The shared
3233
3422
  // dispatch cursor deliberately stays monotonic, so cancellation can never
3234
3423
  // compress provider starts into a burst.
3235
- await client.query(
3424
+ await queryBoundedRateState(
3425
+ client,
3236
3426
  `
3237
3427
  WITH now_value AS (
3238
3428
  SELECT COALESCE(
@@ -3318,7 +3508,8 @@ async function releaseRateStateStatements(
3318
3508
  ];
3319
3509
  if (leaseIds.length === 0) return;
3320
3510
  // DELETE by holder token is naturally idempotent, including HTTP retries.
3321
- await client.query(
3511
+ await queryBoundedRateState(
3512
+ client,
3322
3513
  `
3323
3514
  DELETE FROM ${concurrencyLeaseTableName(input)}
3324
3515
  WHERE bucket_id = $1
@@ -3376,11 +3567,12 @@ async function penalizeRateStateStatements(
3376
3567
  // min_rps.
3377
3568
  // - seen_backpressure flips the ramp from slow-start (x2) to additive.
3378
3569
  // - tokens drained: no more grants until refill re-accrues past the cooldown.
3379
- await client.query(
3570
+ await queryBoundedRateState(
3571
+ client,
3380
3572
  `INSERT INTO ${scopeHoldTableName(input)} (bucket_id) VALUES ($1) ON CONFLICT DO NOTHING`,
3381
3573
  [input.bucketId],
3382
3574
  );
3383
- const rateStateResult = await client.query<{
3575
+ const rateStateResult = await queryBoundedRateState<{
3384
3576
  retry_at_ms: string | number | null;
3385
3577
  cool_until_ms: string | number | null;
3386
3578
  rate_rule_count: string | number;
@@ -3392,6 +3584,7 @@ async function penalizeRateStateStatements(
3392
3584
  observation_cycle_max_ms: string | number | null;
3393
3585
  confirmed_successes_max: string | number | null;
3394
3586
  }>(
3587
+ client,
3395
3588
  `
3396
3589
  WITH now_value AS (
3397
3590
  SELECT COALESCE(
@@ -98,6 +98,32 @@ function sandboxSizeMetadata(cpu: number, memoryGiB: number) {
98
98
  return entry ? { sandboxSize: entry[0] } : {};
99
99
  }
100
100
 
101
+ /**
102
+ * Lets settlement tell a run that used its declared runtime from a duration no
103
+ * runtime limit explains. Billed time stops at the ceiling, so the unclamped
104
+ * duration travels beside it: without it an overrun looks like a run that
105
+ * stayed inside its limit. Items without a ceiling keep their historical shape.
106
+ */
107
+ function runtimeLimitMetadata(
108
+ maxBillingDurationSeconds: number | null | undefined,
109
+ observedWallTimeSeconds: number | undefined,
110
+ ) {
111
+ if (
112
+ typeof maxBillingDurationSeconds !== 'number' ||
113
+ !Number.isFinite(maxBillingDurationSeconds) ||
114
+ maxBillingDurationSeconds <= 0
115
+ ) {
116
+ return {};
117
+ }
118
+ return {
119
+ maxBillingDurationSeconds,
120
+ ...(typeof observedWallTimeSeconds === 'number' &&
121
+ Number.isFinite(observedWallTimeSeconds)
122
+ ? { observedWallTimeSeconds: Math.max(0, observedWallTimeSeconds) }
123
+ : {}),
124
+ };
125
+ }
126
+
101
127
  export function resolveDaytonaSandboxComputeItem(input: {
102
128
  itemId: string;
103
129
  source?: string;
@@ -108,6 +134,10 @@ export function resolveDaytonaSandboxComputeItem(input: {
108
134
  sandboxId?: string | null;
109
135
  startedAt?: number;
110
136
  endedAt?: number;
137
+ /** The resource's own billing ceiling: its timeout plus terminal grace. */
138
+ maxBillingDurationSeconds?: number | null;
139
+ /** Wall time before the ceiling clamped it; recorded only with a ceiling. */
140
+ observedWallTimeSeconds?: number;
111
141
  }): ComputeBillingItem {
112
142
  const billableSeconds = Math.max(1, Math.ceil(input.wallTimeSeconds));
113
143
  const cpu = Math.max(0, input.cpu);
@@ -138,6 +168,10 @@ export function resolveDaytonaSandboxComputeItem(input: {
138
168
  billableDiskGiB,
139
169
  startedAt: input.startedAt,
140
170
  endedAt: input.endedAt,
171
+ ...runtimeLimitMetadata(
172
+ input.maxBillingDurationSeconds,
173
+ input.observedWallTimeSeconds,
174
+ ),
141
175
  },
142
176
  };
143
177
  }
@@ -152,6 +186,10 @@ export function resolveModalSandboxComputeItem(input: {
152
186
  sandboxId?: string | null;
153
187
  startedAt?: number;
154
188
  endedAt?: number;
189
+ /** The resource's own billing ceiling: its timeout plus terminal grace. */
190
+ maxBillingDurationSeconds?: number | null;
191
+ /** Wall time before the ceiling clamped it; recorded only with a ceiling. */
192
+ observedWallTimeSeconds?: number;
155
193
  }): ComputeBillingItem {
156
194
  const billableSeconds = Math.max(1, Math.ceil(input.wallTimeSeconds));
157
195
  const physicalCpuCores = Math.max(0, input.physicalCpuCores);
@@ -186,6 +224,10 @@ export function resolveModalSandboxComputeItem(input: {
186
224
  }),
187
225
  startedAt: input.startedAt,
188
226
  endedAt: input.endedAt,
227
+ ...runtimeLimitMetadata(
228
+ input.maxBillingDurationSeconds,
229
+ input.observedWallTimeSeconds,
230
+ ),
189
231
  },
190
232
  };
191
233
  }