deepline 0.1.238 → 0.1.240

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.
@@ -365,6 +365,9 @@ function datasetAvailableLog(
365
365
  dataset: ReturnType<typeof runtimeDatasetIdentity>,
366
366
  count: number,
367
367
  ): string {
368
+ if (count === 0) {
369
+ return `Dataset ${dataset.path} available: 0 persisted rows; export unavailable: empty dataset`;
370
+ }
368
371
  return `Dataset ${dataset.path} available: ${count} persisted rows; export: deepline runs export ${runId} --dataset ${dataset.path} --out ${dataset.tableNamespace}.csv`;
369
372
  }
370
373
 
@@ -405,6 +405,7 @@ export type PlaySheetRow = {
405
405
  /** Runtime-sheet rows and aggregate progress for one dataset/table namespace. */
406
406
  export type PlaySheetRowsResult = {
407
407
  rows: PlaySheetRow[];
408
+ scope?: { kind: 'database' } | { kind: 'run'; runId: string };
408
409
  summary?: {
409
410
  stats?: {
410
411
  total?: number;
@@ -470,6 +471,15 @@ export type RunsNamespace = {
470
471
  stopAll: (options?: { reason?: string }) => Promise<StopAllPlayRunsResult>;
471
472
  };
472
473
 
474
+ /** Current mutable customer database namespace exposed as `client.db`. */
475
+ export type DbNamespace = {
476
+ /** Run a bounded SQL query against the current customer data plane. */
477
+ query: (input: {
478
+ sql: string;
479
+ maxRows?: number;
480
+ }) => Promise<CustomerDbQueryResult>;
481
+ };
482
+
473
483
  /** One credit grant pool reported by the billing subscription status endpoint. */
474
484
  export type BillingCreditPool = {
475
485
  pool: string;
@@ -1077,6 +1087,8 @@ export class DeeplineClient {
1077
1087
  private readonly config: ResolvedConfig;
1078
1088
  /** Canonical run lifecycle namespace backed by `/api/v2/runs`. */
1079
1089
  readonly runs: RunsNamespace;
1090
+ /** Current mutable customer database namespace backed by `/api/v2/db/query`. */
1091
+ readonly db: DbNamespace;
1080
1092
  /** Billing namespace: subscription status/cancel and invoice history. */
1081
1093
  readonly billing: BillingNamespace;
1082
1094
 
@@ -1101,6 +1113,9 @@ export class DeeplineClient {
1101
1113
  stop: (runId, options) => this.stopRun(runId, options),
1102
1114
  stopAll: (options) => this.stopAllRuns(options),
1103
1115
  };
1116
+ this.db = {
1117
+ query: (input) => this.queryDb(input),
1118
+ };
1104
1119
  this.billing = {
1105
1120
  topUp: (options) => this.topUpBillingBalance(options),
1106
1121
  plans: () => this.getBillingPlans(),
@@ -1460,20 +1475,36 @@ export class DeeplineClient {
1460
1475
  return this.executeTool<TData, TMeta>(toolId, input, options);
1461
1476
  }
1462
1477
 
1478
+ private async queryDb(input: {
1479
+ sql: string;
1480
+ maxRows?: number;
1481
+ }): Promise<CustomerDbQueryResult> {
1482
+ const result = await this.http.post<CustomerDbQueryResult>(
1483
+ '/api/v2/db/query',
1484
+ {
1485
+ sql: input.sql,
1486
+ ...(input.maxRows ? { max_rows: input.maxRows } : {}),
1487
+ },
1488
+ );
1489
+ return {
1490
+ ...result,
1491
+ scope: { kind: 'database', mutability: 'current' },
1492
+ };
1493
+ }
1494
+
1463
1495
  /**
1464
- * Run a bounded SQL query against the customer data plane.
1496
+ * Run a bounded SQL query against the current mutable customer database.
1497
+ *
1498
+ * This query is not scoped to one play run. Use `client.runs` export actions
1499
+ * when the caller needs the rows produced by a specific run.
1465
1500
  *
1466
- * Use this from trusted backend or agent contexts only. The API enforces
1467
- * workspace scoping and row limits.
1501
+ * @deprecated Use {@link DeeplineClient.db} `.query(...)`.
1468
1502
  */
1469
1503
  async queryCustomerDb(input: {
1470
1504
  sql: string;
1471
1505
  maxRows?: number;
1472
1506
  }): Promise<CustomerDbQueryResult> {
1473
- return this.http.post<CustomerDbQueryResult>('/api/v2/db/query', {
1474
- sql: input.sql,
1475
- ...(input.maxRows ? { max_rows: input.maxRows } : {}),
1476
- });
1507
+ return this.db.query(input);
1477
1508
  }
1478
1509
 
1479
1510
  /**
@@ -3000,9 +3031,42 @@ export class DeeplineClient {
3000
3031
  if (input.rowMode === 'all') {
3001
3032
  params.set('rowMode', 'all');
3002
3033
  }
3003
- return await this.http.get<PlaySheetRowsResult>(
3034
+ const result = await this.http.get<PlaySheetRowsResult>(
3004
3035
  `/api/v2/plays/${encodeURIComponent(input.playName)}/sheet?${params.toString()}`,
3005
3036
  );
3037
+ const requestedRunId = input.runId?.trim() || '';
3038
+ if (requestedRunId) {
3039
+ const confirmedRunId =
3040
+ result.scope?.kind === 'run' ? result.scope.runId.trim() : '';
3041
+ const foreignRowRunIds = [
3042
+ ...new Set(
3043
+ result.rows
3044
+ .map((row) =>
3045
+ typeof row.runId === 'string' ? row.runId.trim() : '',
3046
+ )
3047
+ .filter((runId) => runId && runId !== requestedRunId),
3048
+ ),
3049
+ ];
3050
+ if (
3051
+ result.scope?.kind !== 'run' ||
3052
+ confirmedRunId !== requestedRunId ||
3053
+ foreignRowRunIds.length > 0
3054
+ ) {
3055
+ throw new DeeplineError(
3056
+ `Run-scoped dataset export was not confirmed for ${requestedRunId}; no rows were returned to the caller. Update Deepline, then retry the same export command. Do not rerun the play.`,
3057
+ undefined,
3058
+ 'RUN_EXPORT_SCOPE_MISMATCH',
3059
+ {
3060
+ requestedRunId,
3061
+ confirmedScope: result.scope ?? null,
3062
+ foreignRowRunIds,
3063
+ playName: input.playName,
3064
+ tableNamespace: input.tableNamespace,
3065
+ },
3066
+ );
3067
+ }
3068
+ }
3069
+ return result;
3006
3070
  }
3007
3071
 
3008
3072
  /**
@@ -218,6 +218,7 @@ export class HttpClient {
218
218
  'X-Deepline-CLI-Version': SDK_VERSION,
219
219
  'X-Deepline-SDK-Version': SDK_VERSION,
220
220
  'X-Deepline-API-Contract': SDK_API_CONTRACT,
221
+ 'X-Deepline-Run-Result-Shape': 'canonical',
221
222
  ...extra,
222
223
  };
223
224
  const skillsVersion = this.readSkillsVersionHeader();
@@ -62,6 +62,7 @@ export type {
62
62
  BillingInvoiceEntry,
63
63
  BillingInvoicesResult,
64
64
  BillingNamespace,
65
+ DbNamespace,
65
66
  BillingPaymentMethodSummary,
66
67
  BillingSubscriptionCancelResult,
67
68
  BillingSubscriptionStatus,
@@ -141,6 +142,9 @@ export type {
141
142
  LiveEventEnvelope,
142
143
  PlayLiveEvent,
143
144
  PlayRunResult,
145
+ PlayRunActionPackage,
146
+ PlayRunDatasetActions,
147
+ PlayRunPackage,
144
148
  PlayRunStart,
145
149
  ClearPlayHistoryRequest,
146
150
  ClearPlayHistoryResult,
@@ -110,10 +110,10 @@ export const SDK_RELEASE = {
110
110
  // silently materializing them as unmatched results.
111
111
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
112
112
  // automatically without blocking their current command.
113
- version: '0.1.238',
113
+ version: '0.1.240',
114
114
  apiContract: '2026-06-dataset-handle-results-hard-cutover',
115
115
  supportPolicy: {
116
- latest: '0.1.238',
116
+ latest: '0.1.240',
117
117
  minimumSupported: '0.1.53',
118
118
  deprecatedBelow: '0.1.219',
119
119
  commandMinimumSupported: [
@@ -72,12 +72,14 @@ export interface CustomerDbColumn {
72
72
  }
73
73
 
74
74
  /**
75
- * Result returned by {@link DeeplineClient.queryCustomerDb}.
75
+ * Result returned by {@link DeeplineClient.db.query}.
76
76
  *
77
77
  * Rows are intentionally untyped because the schema depends on the caller's SQL
78
78
  * query and selected customer tables.
79
79
  */
80
80
  export interface CustomerDbQueryResult {
81
+ /** This query reads the current mutable customer database, not one run snapshot. */
82
+ scope?: { kind: 'database'; mutability: 'current' };
81
83
  /** Database command executed by the query endpoint. */
82
84
  command: string;
83
85
  /** Total affected row count when reported by the database. */
@@ -443,6 +445,19 @@ export type PlayRunActionPackage =
443
445
  | {
444
446
  kind: 'deepline_run_inspect';
445
447
  runId: string;
448
+ command?: string;
449
+ api: { method: 'GET'; path: string };
450
+ }
451
+ | {
452
+ kind: 'deepline_run_full';
453
+ runId: string;
454
+ command?: string;
455
+ api: { method: 'GET'; path: string };
456
+ }
457
+ | {
458
+ kind: 'deepline_run_billing';
459
+ runId: string;
460
+ command?: string;
446
461
  api: { method: 'GET'; path: string };
447
462
  }
448
463
  | {
@@ -453,6 +468,16 @@ export type PlayRunActionPackage =
453
468
  sqlQualifiedTableName?: string;
454
469
  sql: string;
455
470
  maxRows: number;
471
+ command?: string;
472
+ scope?:
473
+ | { kind: 'database'; mutability: 'current' }
474
+ | { kind: 'run'; runId: string };
475
+ note?: string;
476
+ deprecated?: {
477
+ replacement: 'queryCurrentTable';
478
+ removeAfter: string;
479
+ reason: string;
480
+ };
456
481
  api: {
457
482
  method: 'POST';
458
483
  path: '/api/v2/db/query';
@@ -463,6 +488,14 @@ export type PlayRunActionPackage =
463
488
  runId: string;
464
489
  datasetPath: string;
465
490
  format: 'csv';
491
+ command?: string;
492
+ scope?: { kind: 'run'; runId: string };
493
+ note?: string;
494
+ deprecated?: {
495
+ replacement: 'datasets[].actions.exportCsv';
496
+ removeAfter: string;
497
+ reason: string;
498
+ };
466
499
  }
467
500
  | {
468
501
  kind: 'deepline_run_logs';
@@ -473,6 +506,26 @@ export type PlayRunActionPackage =
473
506
  api: { method: 'GET'; path: string };
474
507
  };
475
508
 
509
+ type PlayRunDbQueryAction = Extract<
510
+ PlayRunActionPackage,
511
+ { kind: 'deepline_db_query' }
512
+ >;
513
+ type PlayRunExportAction = Extract<
514
+ PlayRunActionPackage,
515
+ { kind: 'deepline_run_export' }
516
+ >;
517
+
518
+ export type PlayRunDatasetActions = {
519
+ /** @deprecated Use queryCurrentTable or exportCsv. Removed after 2026-08-18. */
520
+ query?: PlayRunDbQueryAction & { scope?: { kind: 'run'; runId: string } };
521
+ queryCurrentTable?: PlayRunDbQueryAction & {
522
+ scope?: { kind: 'database'; mutability: 'current' };
523
+ };
524
+ exportCsv?: PlayRunExportAction & {
525
+ scope?: { kind: 'run'; runId: string };
526
+ };
527
+ };
528
+
476
529
  /**
477
530
  * Compact canonical package for an inspected play run.
478
531
  *
@@ -508,9 +561,15 @@ export interface PlayRunPackage {
508
561
  path: string;
509
562
  tableNamespace?: string;
510
563
  rowCount?: number;
564
+ sqlTableName?: string;
565
+ sqlQualifiedTableName?: string;
511
566
  recovered?: true;
567
+ exportUnavailable?: {
568
+ reason: 'empty_dataset' | 'shared_table_namespace';
569
+ message: string;
570
+ };
512
571
  preview?: Record<string, unknown>;
513
- actions?: Record<string, PlayRunActionPackage>;
572
+ actions?: PlayRunDatasetActions;
514
573
  }>;
515
574
  /** Small retained tail of customer and runtime logs; fetch the full stream through `runs.logs`. */
516
575
  logs?: {
@@ -522,6 +581,8 @@ export interface PlayRunPackage {
522
581
  /** Follow-up actions a caller can perform against the run. */
523
582
  next?: {
524
583
  inspect?: PlayRunActionPackage;
584
+ full?: PlayRunActionPackage;
585
+ billing?: PlayRunActionPackage;
525
586
  export?: PlayRunActionPackage;
526
587
  query?: PlayRunActionPackage;
527
588
  logs?: PlayRunActionPackage;
@@ -1202,7 +1202,6 @@ export class PlayContextImpl {
1202
1202
  }> = [];
1203
1203
  private processedRowCount = 0;
1204
1204
  private sleepBoundaryIndex = 0;
1205
- private fetchCallIndex = 0;
1206
1205
  private readonly secretRedactor: SecretRedactionContext =
1207
1206
  createSecretRedactionContext();
1208
1207
  private mapInvocationIndex = 0;
@@ -5853,6 +5852,10 @@ export class PlayContextImpl {
5853
5852
  for (const wake of waiters) wake();
5854
5853
  }
5855
5854
 
5855
+ private toolBatchDispatcherNowMs(): number {
5856
+ return this.#options.toolBatchDispatcherClock?.nowMs() ?? Date.now();
5857
+ }
5858
+
5856
5859
  private enqueueToolCall(request: ToolCallRequest): void {
5857
5860
  if (this.toolDispatcherFailure != null) {
5858
5861
  const resolver = this.toolCallResolvers.get(request.callId);
@@ -5865,7 +5868,10 @@ export class PlayContextImpl {
5865
5868
  this.toolCallQueue.push(request);
5866
5869
  const batchKey = this.toolBatchCoalesceKey(request);
5867
5870
  if (batchKey && !this.toolBatchQueuedAtByKey.has(batchKey)) {
5868
- this.toolBatchQueuedAtByKey.set(batchKey, Date.now());
5871
+ this.toolBatchQueuedAtByKey.set(
5872
+ batchKey,
5873
+ this.toolBatchDispatcherNowMs(),
5874
+ );
5869
5875
  }
5870
5876
  this.wakeToolDispatcher();
5871
5877
  }
@@ -5908,7 +5914,10 @@ export class PlayContextImpl {
5908
5914
  if (!this.toolBatchQueuedAtByKey.has(batchKey)) {
5909
5915
  this.toolBatchQueuedAtByKey.set(batchKey, queuedAt);
5910
5916
  }
5911
- const deadline = queuedAt + TOOL_BATCH_COALESCE_WINDOW_MS;
5917
+ const coalesceWindowMs =
5918
+ this.#options.toolBatchCoalesceWindowMs ??
5919
+ TOOL_BATCH_COALESCE_WINDOW_MS;
5920
+ const deadline = queuedAt + coalesceWindowMs;
5912
5921
  const maxBatchSize = batchMaxSizes.get(batchKey) ?? 1;
5913
5922
  if (count >= maxBatchSize || deadline <= nowMs) {
5914
5923
  readyBatchKeys.add(batchKey);
@@ -5939,15 +5948,26 @@ export class PlayContextImpl {
5939
5948
 
5940
5949
  private waitForToolDispatcherWake(deadlineMs: number | null): Promise<void> {
5941
5950
  return new Promise((resolve) => {
5942
- let timer: ReturnType<typeof setTimeout> | null = null;
5951
+ let cancelDeadline: (() => void) | null = null;
5943
5952
  const wake = () => {
5944
- if (timer) clearTimeout(timer);
5953
+ cancelDeadline?.();
5954
+ cancelDeadline = null;
5945
5955
  this.toolDispatcherWakeWaiters.delete(wake);
5946
5956
  resolve();
5947
5957
  };
5948
5958
  this.toolDispatcherWakeWaiters.add(wake);
5949
5959
  if (deadlineMs != null) {
5950
- timer = setTimeout(wake, Math.max(0, deadlineMs - Date.now()));
5960
+ const delayMs = Math.max(
5961
+ 0,
5962
+ deadlineMs - this.toolBatchDispatcherNowMs(),
5963
+ );
5964
+ const clock = this.#options.toolBatchDispatcherClock;
5965
+ if (clock) {
5966
+ cancelDeadline = clock.schedule(delayMs, wake);
5967
+ } else {
5968
+ const timer = setTimeout(wake, delayMs);
5969
+ cancelDeadline = () => clearTimeout(timer);
5970
+ }
5951
5971
  }
5952
5972
  });
5953
5973
  }
@@ -5975,7 +5995,9 @@ export class PlayContextImpl {
5975
5995
  throw this.toolDispatcherFailure;
5976
5996
  }
5977
5997
 
5978
- const dispatchable = this.takeDispatchableToolCalls(Date.now());
5998
+ const dispatchable = this.takeDispatchableToolCalls(
5999
+ this.toolBatchDispatcherNowMs(),
6000
+ );
5979
6001
  if (dispatchable.requests.length > 0) {
5980
6002
  pass += 1;
5981
6003
  this.log(` Batch pass ${pass}`);
@@ -6686,8 +6708,8 @@ export class PlayContextImpl {
6686
6708
  rowScopeKey ? `@${rowScopeKey}` : ''
6687
6709
  }`;
6688
6710
  if (rowScope) {
6689
- const namespaces =
6690
- rowScope.inlineChildInvocationNamespaces ??= new Set<string>();
6711
+ const namespaces = (rowScope.inlineChildInvocationNamespaces ??=
6712
+ new Set<string>());
6691
6713
  namespaces.add(namespace);
6692
6714
  if (namespaces.size > MAX_INLINE_CHILD_INVOCATIONS_PER_ROW) {
6693
6715
  throw new Error(
@@ -6818,11 +6840,15 @@ export class PlayContextImpl {
6818
6840
  options?: FetchOptions,
6819
6841
  ): Promise<PlayFetchResponse> {
6820
6842
  const normalizedKey = this.normalizeContextKey(key, 'fetch');
6821
- if (rowContext.getStore()) {
6822
- throw new Error(
6823
- 'ctx.fetch() must run outside ctx.dataset(); use ctx.tools.execute(...) for row-level external requests so Deepline can batch and checkpoint them.',
6824
- );
6825
- }
6843
+ const rowStore = rowContext.getStore();
6844
+ const rowFetchScope = rowStore
6845
+ ? {
6846
+ tableNamespace: rowStore.tableNamespace ?? null,
6847
+ rowKey: rowStore.rowKey ?? null,
6848
+ rowId: rowStore.rowKey ? null : rowStore.rowId,
6849
+ fieldName: rowStore.fieldName ?? null,
6850
+ }
6851
+ : null;
6826
6852
 
6827
6853
  if (valueContainsSecret(input) || valueContainsSecret(init.body)) {
6828
6854
  throw new Error(
@@ -6858,6 +6884,7 @@ export class PlayContextImpl {
6858
6884
  ...normalizeFetchHeaders(init.headers),
6859
6885
  ...secretHeaderMarkers,
6860
6886
  },
6887
+ row: rowFetchScope,
6861
6888
  }),
6862
6889
  ),
6863
6890
  staleAfterSeconds: options?.staleAfterSeconds,
@@ -6872,7 +6899,7 @@ export class PlayContextImpl {
6872
6899
  const fetchInit = { ...init, headers };
6873
6900
  delete fetchInit.auth;
6874
6901
  const boundaryId = this.durableBoundaryId(
6875
- `fetch-${this.fetchCallIndex}-${stableDigest(
6902
+ `fetch-${stableDigest(
6876
6903
  stableStringify({
6877
6904
  url,
6878
6905
  method,
@@ -6881,10 +6908,10 @@ export class PlayContextImpl {
6881
6908
  ...secretHeaderMarkers,
6882
6909
  },
6883
6910
  body: typeof init.body === 'string' ? init.body : null,
6911
+ row: rowFetchScope,
6884
6912
  }),
6885
6913
  )}`,
6886
6914
  );
6887
- this.fetchCallIndex += 1;
6888
6915
 
6889
6916
  const existing = this.checkpoint.resolvedBoundaries?.[boundaryId];
6890
6917
  if (existing?.kind === 'fetch' && 'output' in existing) {
@@ -680,6 +680,19 @@ export interface ContextOptions {
680
680
  getBatchOperationStrategy?: (
681
681
  operation: string,
682
682
  ) => AnyBatchOperationStrategy | null;
683
+ /**
684
+ * Internal deterministic clock for fixed-window dispatcher tests. Runtime
685
+ * adapters omit this and use the system clock.
686
+ */
687
+ toolBatchDispatcherClock?: {
688
+ nowMs: () => number;
689
+ schedule: (delayMs: number, wake: () => void) => () => void;
690
+ };
691
+ // How long batchable calls wait for same-key row continuations before
692
+ // dispatching, measured from the first queued item. Defaults to 5ms in
693
+ // production; tests override it to a large value to make coalescing
694
+ // deterministic instead of racing wall-clock milliseconds on slow CI.
695
+ toolBatchCoalesceWindowMs?: number;
683
696
  queryCustomerDb?: CustomerDbQueryHandler;
684
697
  executeStructuredPlayDefinition?: (input: {
685
698
  definition: PlayStructuredDefinition;
@@ -127,14 +127,13 @@ export const PLAY_LATENCY_SUMMARY_COLUMNS: PlayLatencyPhaseDefinition[] = [
127
127
  phase: 'server.start_route_runtime_data_plane_ready',
128
128
  source: 'server',
129
129
  description:
130
- 'Start route Runtime Sheet Data Plane readiness check; active tenants use the cached or record fast path, new tenants may provision here.',
130
+ 'Start route read-only Runtime Sheet Data Plane readiness check.',
131
131
  },
132
132
  {
133
133
  header: 'ready_plane',
134
- phase: 'server.runtime_data_plane_ensure_plane',
134
+ phase: 'server.runtime_data_plane_read_plane',
135
135
  source: 'server',
136
- description:
137
- 'Runtime Sheet readiness control-plane record lookup or first-workspace provisioning.',
136
+ description: 'Runtime Sheet readiness control-plane record lookup.',
138
137
  },
139
138
  {
140
139
  header: 'ready_markers',
@@ -143,20 +142,6 @@ export const PLAY_LATENCY_SUMMARY_COLUMNS: PlayLatencyPhaseDefinition[] = [
143
142
  description:
144
143
  'Single pooled tenant-Postgres read of the storage-contract fingerprint and runtime migration version.',
145
144
  },
146
- {
147
- header: 'ready_repair',
148
- phase: 'server.runtime_data_plane_repair_contract',
149
- source: 'server',
150
- description:
151
- 'Exceptional stale storage-contract repair; absent from the healthy path.',
152
- },
153
- {
154
- header: 'ready_migrate',
155
- phase: 'server.runtime_data_plane_migrate',
156
- source: 'server',
157
- description:
158
- 'Exceptional stale Runtime Sheet migration; absent from the healthy path.',
159
- },
160
145
  {
161
146
  header: 'route_wait',
162
147
  phase: 'server.start_route_wait_completion',
@@ -224,6 +224,20 @@ export type PlayRunnerRuntimeTiming = {
224
224
  daytonaExecuteMs?: number;
225
225
  };
226
226
 
227
+ /**
228
+ * Why replay checkpoint state is present or absent on the detached-runner
229
+ * terminal wire. This describes checkpoint transport only; it never classifies
230
+ * the customer's returned output as oversized.
231
+ */
232
+ export const RUNNER_TERMINAL_CHECKPOINT_DISPOSITION = {
233
+ OMITTED_TERMINAL_REPLAY: 'omitted_terminal_replay',
234
+ INCLUDED_RESUME_REQUIRED: 'included_resume_required',
235
+ ABSENT: 'absent',
236
+ } as const;
237
+
238
+ export type RunnerTerminalCheckpointDisposition =
239
+ (typeof RUNNER_TERMINAL_CHECKPOINT_DISPOSITION)[keyof typeof RUNNER_TERMINAL_CHECKPOINT_DISPOSITION];
240
+
227
241
  export type PlayRunnerResult =
228
242
  | {
229
243
  status: 'completed';
@@ -1055,7 +1055,12 @@ export function reducePlayRunLedgerEvent(
1055
1055
  if (ignoreNonTerminalAfterStickyTerminal) {
1056
1056
  return withTiming(base);
1057
1057
  }
1058
- const createdStatus = event.status ?? base.status;
1058
+ // Scheduler wait/resume events own the active lifecycle once a run is
1059
+ // parked. Worker registration can arrive later through an independent
1060
+ // ledger append lane; replaying that older `run.created` fact must not
1061
+ // make an externally-waiting run look active again.
1062
+ const createdStatus =
1063
+ base.status === 'waiting' ? 'waiting' : (event.status ?? base.status);
1059
1064
  return withTiming({
1060
1065
  ...base,
1061
1066
  playName: event.playName ?? base.playName ?? null,
@@ -1075,7 +1080,9 @@ export function reducePlayRunLedgerEvent(
1075
1080
  playName: event.playName ?? base.playName ?? null,
1076
1081
  status: isTerminalPlayRunLedgerStatus(base.status)
1077
1082
  ? base.status
1078
- : 'running',
1083
+ : base.status === 'waiting'
1084
+ ? 'waiting'
1085
+ : 'running',
1079
1086
  startedAt: base.startedAt ?? occurredAt,
1080
1087
  });
1081
1088
  case 'run.waiting':
@@ -1200,7 +1207,9 @@ export function reducePlayRunLedgerEvent(
1200
1207
  ...base,
1201
1208
  status: isTerminalPlayRunLedgerStatus(base.status)
1202
1209
  ? base.status
1203
- : 'running',
1210
+ : base.status === 'waiting'
1211
+ ? 'waiting'
1212
+ : 'running',
1204
1213
  startedAt: base.startedAt ?? occurredAt,
1205
1214
  orderedStepIds: appendOrderedStepId(base, event.stepId),
1206
1215
  stepsById: { ...base.stepsById, [event.stepId]: nextStep },
@@ -1,7 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { gzipSync } from 'node:zlib';
4
- import type { PlayRunnerExecutionConfig } from '@shared_libs/play-runtime/protocol';
4
+ import {
5
+ RUNNER_TERMINAL_CHECKPOINT_DISPOSITION,
6
+ type PlayRunnerExecutionConfig,
7
+ } from '@shared_libs/play-runtime/protocol';
5
8
  import {
6
9
  PLAY_RUNTIME_CONTRACT,
7
10
  PLAY_RUNTIME_CONTRACT_HEADER,
@@ -29,6 +32,7 @@ export type StagedDaytonaPayload = {
29
32
  workDir: string;
30
33
  command: string;
31
34
  readyPath: string;
35
+ startAckPath: string;
32
36
  outputPath: string;
33
37
  exitCodePath: string;
34
38
  progressEventPath: string;
@@ -126,6 +130,39 @@ function readResultFromOutput() {
126
130
  return null;
127
131
  }
128
132
 
133
+ function terminalResultForTransport(result) {
134
+ if (
135
+ result &&
136
+ ${JSON.stringify(Object.values(RUNNER_TERMINAL_CHECKPOINT_DISPOSITION))}.includes(
137
+ result.checkpointDisposition,
138
+ )
139
+ ) {
140
+ return result;
141
+ }
142
+ if (result && result.status === 'suspended') {
143
+ return {
144
+ ...result,
145
+ checkpointDisposition: '${RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.INCLUDED_RESUME_REQUIRED}',
146
+ checkpointBytes: Buffer.byteLength(JSON.stringify(result.checkpoint)),
147
+ };
148
+ }
149
+ const checkpoint = result && result.checkpoint;
150
+ const terminal = { ...(result || {}) };
151
+ delete terminal.checkpoint;
152
+ if (checkpoint === null || checkpoint === undefined) {
153
+ return {
154
+ ...terminal,
155
+ checkpointDisposition: '${RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.ABSENT}',
156
+ checkpointBytes: 0,
157
+ };
158
+ }
159
+ return {
160
+ ...terminal,
161
+ checkpointDisposition: '${RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.OMITTED_TERMINAL_REPLAY}',
162
+ checkpointBytes: Buffer.byteLength(JSON.stringify(checkpoint)),
163
+ };
164
+ }
165
+
129
166
  async function main() {
130
167
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
131
168
  const context = (config && config.context) || {};
@@ -136,7 +173,8 @@ async function main() {
136
173
  console.log('[crash-terminal] no push config; skipping');
137
174
  return;
138
175
  }
139
- const result = readResultFromOutput() || {
176
+ const parsedResult = readResultFromOutput();
177
+ const result = terminalResultForTransport(parsedResult || {
140
178
  status: 'failed',
141
179
  error:
142
180
  'Daytona play runner exited with code ' +
@@ -146,9 +184,10 @@ async function main() {
146
184
  logs: [],
147
185
  stats: {},
148
186
  steps: [],
149
- checkpoint: null,
187
+ checkpointDisposition: '${RUNNER_TERMINAL_CHECKPOINT_DISPOSITION.ABSENT}',
188
+ checkpointBytes: 0,
150
189
  tableNamespace: null,
151
- };
190
+ });
152
191
  for (let attempt = 1; attempt <= 3; attempt += 1) {
153
192
  try {
154
193
  const controller = new AbortController();
@@ -351,6 +390,7 @@ export async function stageDaytonaRunnerPayload(input: {
351
390
  const outputPath = `${workDir}/deepline-play-output-${randomUUID()}.log`;
352
391
  const exitCodePath = `${workDir}/deepline-play-exit-${randomUUID()}.txt`;
353
392
  const readyPath = `${workDir}/deepline-play-ready-${randomUUID()}.json`;
393
+ const startAckPath = `${workDir}/deepline-play-start-ack-${randomUUID()}.json`;
354
394
  const progressEventPath = `${workDir}/deepline-play-progress-${randomUUID()}.jsonl`;
355
395
  const runnerTraceEnv =
356
396
  process.env.DEEPLINE_RUNTIME_RECEIPT_TRACE === '1'
@@ -363,18 +403,19 @@ export async function stageDaytonaRunnerPayload(input: {
363
403
  artifactCodePath,
364
404
  artifactSourceMapPath,
365
405
  crashPusherPath,
366
- })} && DEEPLINE_PLAY_RUNNER_READY_PATH=${shellQuote(readyPath)} DEEPLINE_PLAY_RUNNER_PROGRESS_EVENT_PATH=${shellQuote(progressEventPath)} DEEPLINE_PLAY_RUNNER_STDOUT_EVENT_MODE=compact ${runnerTraceEnv}node ${shellQuote(runnerPath)} ${shellQuote(configPath)}`;
406
+ })} && DEEPLINE_PLAY_RUNNER_READY_PATH=${shellQuote(readyPath)} DEEPLINE_PLAY_RUNNER_START_ACK_PATH=${shellQuote(startAckPath)} DEEPLINE_PLAY_RUNNER_PROGRESS_EVENT_PATH=${shellQuote(progressEventPath)} DEEPLINE_PLAY_RUNNER_STDOUT_EVENT_MODE=compact ${runnerTraceEnv}node ${shellQuote(runnerPath)} ${shellQuote(configPath)}`;
367
407
  // Crash-containment epilogue: runs UNCONDITIONALLY after the runner exits and
368
408
  // pushes the parsed (or synthesized) terminal to the gateway so the parked
369
409
  // worker wakes within seconds of ANY runner death — process.exit abuse, OOM
370
410
  // SIGKILL, or a crash before the in-process push. Idempotent against a
371
411
  // successful in-runner push (terminals are first-write-wins).
372
- const command = `rm -f ${shellQuote(outputPath)} ${shellQuote(exitCodePath)} ${shellQuote(readyPath)} ${shellQuote(progressEventPath)} ${shellQuote(`${progressEventPath}.*`)}; ( ${runnerCommand} ) > ${shellQuote(outputPath)} 2>&1; code=$?; printf '%s' "$code" > ${shellQuote(exitCodePath)}; node ${shellQuote(crashPusherPath)} ${shellQuote(configPath)} "$code" ${shellQuote(outputPath)} >> ${shellQuote(outputPath)} 2>&1 || true; printf 'deepline runner output captured: %s\\n' ${shellQuote(outputPath)}; exit "$code"`;
412
+ const command = `rm -f ${shellQuote(outputPath)} ${shellQuote(exitCodePath)} ${shellQuote(readyPath)} ${shellQuote(startAckPath)} ${shellQuote(progressEventPath)} ${shellQuote(`${progressEventPath}.*`)}; ( ${runnerCommand} ) > ${shellQuote(outputPath)} 2>&1; code=$?; printf '%s' "$code" > ${shellQuote(exitCodePath)}; node ${shellQuote(crashPusherPath)} ${shellQuote(configPath)} "$code" ${shellQuote(outputPath)} >> ${shellQuote(outputPath)} 2>&1 || true; printf 'deepline runner output captured: %s\\n' ${shellQuote(outputPath)}; exit "$code"`;
373
413
 
374
414
  return {
375
415
  workDir: input.workDir,
376
416
  command,
377
417
  readyPath,
418
+ startAckPath,
378
419
  outputPath,
379
420
  exitCodePath,
380
421
  progressEventPath,