deepline 0.1.235 → 0.1.236

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.
@@ -3828,6 +3828,9 @@ function createWorkerPacingResolver(req: RunRequest): WorkerPacingResolver {
3828
3828
  if (builtin) {
3829
3829
  return Promise.resolve({
3830
3830
  ...builtin,
3831
+ // Builtin pacing has no per-org coordinator rate scope. Keep the
3832
+ // required resolver contract explicit so the Governor treats it as
3833
+ // intentionally unscoped rather than receiving a malformed policy.
3831
3834
  r: null,
3832
3835
  retrySafeTransientHttp: false,
3833
3836
  });
@@ -108,10 +108,10 @@ export const SDK_RELEASE = {
108
108
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
109
109
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
110
110
  // automatically without blocking their current command.
111
- version: '0.1.235',
111
+ version: '0.1.236',
112
112
  apiContract: '2026-06-dataset-handle-results-hard-cutover',
113
113
  supportPolicy: {
114
- latest: '0.1.235',
114
+ latest: '0.1.236',
115
115
  minimumSupported: '0.1.53',
116
116
  deprecatedBelow: '0.1.219',
117
117
  commandMinimumSupported: [
@@ -12,6 +12,7 @@ import {
12
12
  isPlayRowExecutionSuspendedError,
13
13
  } from './suspension';
14
14
  import { getToolHttpErrorReceiptFailureKind } from './tool-http-errors';
15
+ import { isLegacyRepairableWorkReceiptError } from './work-receipt-state-machine';
15
16
  import type { WorkReceiptFailureKind } from './work-receipts';
16
17
  import {
17
18
  PLAY_RUNTIME_WORK_RECEIPT_LEASE_TTL_MS,
@@ -63,8 +64,24 @@ export function runtimeReceiptFailureKindForError(
63
64
  const toolFailureKind = getToolHttpErrorReceiptFailureKind(error);
64
65
  if (toolFailureKind) return toolFailureKind;
65
66
 
66
- return isPlayExecutionSuspendedError(error) ||
67
+ if (
68
+ isPlayExecutionSuspendedError(error) ||
67
69
  isPlayRowExecutionSuspendedError(error)
70
+ ) {
71
+ return 'repairable';
72
+ }
73
+
74
+ // Transport/timeout failures never reach a `ToolHttpError` (there was no HTTP
75
+ // response to classify), so they would otherwise default to `terminal` and
76
+ // poison the durable receipt: the next run would replay the cached transport
77
+ // failure instead of re-executing. These are transient infra failures, so
78
+ // mark them repairable. `isLegacyRepairableWorkReceiptError` already encodes
79
+ // the exact transient-string contract (5xx, "transport failed calling",
80
+ // "runtime api call timed out") while excluding hard billing caps, and the
81
+ // read-side SQL predicate uses the same rules, so write and read stay aligned.
82
+ const message =
83
+ error instanceof Error ? error.message : error != null ? String(error) : '';
84
+ return isLegacyRepairableWorkReceiptError(message)
68
85
  ? 'repairable'
69
86
  : 'terminal';
70
87
  }
@@ -1,6 +1,19 @@
1
1
  const encoder = typeof TextEncoder === 'undefined' ? null : new TextEncoder();
2
2
 
3
3
  export const TERMINAL_RUN_RESULT_MAX_BYTES = 2_500_000;
4
+ /**
5
+ * Convex hard-caps a single document at 1 MiB and rejects nesting deeper than
6
+ * 16 levels. A run-ledger `run.completed` / `run.failed` event embeds its
7
+ * `result` inside one `playRunEvents` document, so the ledger-bound result must
8
+ * stay comfortably under that ceiling (the event also carries seq/type/
9
+ * timestamps and the surrounding doc envelope). Keep well below 1 MiB so no
10
+ * single event can wedge the append mutation with `Value is too large (>1MiB)`.
11
+ * Historically a failed run embedded its full `PlayRunnerResult` — including a
12
+ * `checkpoint` whose `completedToolBatches[*].toolResponse.raw` held megabytes
13
+ * of provider payload — which is durable in scheduler Postgres and never read
14
+ * back from Convex, yet deterministically wedged the append.
15
+ */
16
+ export const LEDGER_TERMINAL_RESULT_MAX_BYTES = 768 * 1024;
4
17
  export const CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 1 * 1024 * 1024;
5
18
  export const CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 2 * 1024 * 1024;
6
19
  /** Inline Postgres receipt contract: one serialized output is at most 10 MiB. */
@@ -86,6 +99,81 @@ export function persistableTerminalRunResult(value: unknown): unknown {
86
99
  return valueWithinJsonByteLimit(value, TERMINAL_RUN_RESULT_MAX_BYTES);
87
100
  }
88
101
 
102
+ /**
103
+ * Replay-only keys a terminal `PlayRunnerResult` carries for the scheduler
104
+ * (Postgres) data plane. These are NEVER read back from the Convex run ledger:
105
+ * resume rehydrates the checkpoint/suspension from scheduler Postgres
106
+ * (`work_runs.checkpoint_json` / `suspension_json`), and customer reads use the
107
+ * run's `output`. Embedding them in a `playRunEvents` document only bloats the
108
+ * write and can exceed Convex's 1 MiB / 16-level document limits.
109
+ */
110
+ const LEDGER_STRIPPED_TERMINAL_RESULT_KEYS = [
111
+ 'checkpoint',
112
+ 'suspension',
113
+ ] as const;
114
+
115
+ /** A tiny reference descriptor left in place of a dropped over-limit result. */
116
+ export type LedgerTerminalResultRef = {
117
+ __kind: 'deepline.ledger_terminal_result_ref.v1';
118
+ /** Where the full terminal result is durably stored. */
119
+ store: 'scheduler_postgres';
120
+ /** Column on the scheduler `work_runs` row that holds the full result. */
121
+ key: 'terminal_result_json';
122
+ /** Serialized byte size of the result that was elided from the ledger. */
123
+ bytes: number;
124
+ reason: 'terminal_result_exceeds_ledger_limit';
125
+ };
126
+
127
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
128
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
129
+ }
130
+
131
+ /**
132
+ * Shape a terminal run result for the Convex run ledger. Control-plane only:
133
+ *
134
+ * 1. Strip replay-only keys ({@link LEDGER_STRIPPED_TERMINAL_RESULT_KEYS}) from
135
+ * the top-level result object — the scheduler Postgres data plane owns them.
136
+ * 2. If what remains still exceeds {@link LEDGER_TERMINAL_RESULT_MAX_BYTES},
137
+ * replace the whole result with a {@link LedgerTerminalResultRef} pointing at
138
+ * the durable scheduler copy, so the ledger event stays small and the
139
+ * Convex append cannot wedge on an oversized/over-nested document.
140
+ *
141
+ * The full result (including its checkpoint) remains durable in scheduler
142
+ * Postgres regardless; this only governs what the ledger embeds.
143
+ */
144
+ export function terminalRunResultForLedger(value: unknown): unknown {
145
+ if (value === undefined || value === null) {
146
+ return value;
147
+ }
148
+ let candidate = value;
149
+ if (isPlainObject(value)) {
150
+ const stripped: Record<string, unknown> = {};
151
+ for (const [key, entry] of Object.entries(value)) {
152
+ if (
153
+ (LEDGER_STRIPPED_TERMINAL_RESULT_KEYS as readonly string[]).includes(
154
+ key,
155
+ )
156
+ ) {
157
+ continue;
158
+ }
159
+ stripped[key] = entry;
160
+ }
161
+ candidate = stripped;
162
+ }
163
+ const bytes = jsonByteLength(candidate);
164
+ if (bytes <= LEDGER_TERMINAL_RESULT_MAX_BYTES) {
165
+ return candidate;
166
+ }
167
+ const ref: LedgerTerminalResultRef = {
168
+ __kind: 'deepline.ledger_terminal_result_ref.v1',
169
+ store: 'scheduler_postgres',
170
+ key: 'terminal_result_json',
171
+ bytes,
172
+ reason: 'terminal_result_exceeds_ledger_limit',
173
+ };
174
+ return ref;
175
+ }
176
+
89
177
  export function assertCustomerOutputValueWithinLimit(input: {
90
178
  value: unknown;
91
179
  path: string;
@@ -1,6 +1,16 @@
1
1
  import { RECEIPT_STATUS_CODE } from './receipt-status';
2
2
  import { workReceiptFailureKindCode } from './work-receipts';
3
3
 
4
+ // This predicate MUST stay behaviorally identical to
5
+ // `isLegacyRepairableWorkReceiptError` in ./work-receipt-state-machine.ts.
6
+ // Write-side classification and this read-side reclaim gate share one contract:
7
+ // a receipt is repairable if its failure_kind is repairable, OR it is a legacy
8
+ // terminal receipt whose error text proves a transient-class failure (5xx,
9
+ // transport, timeout, or a transient billing-plane outage). The
10
+ // billing_unavailable clause recovers the poisoned backlog written during a
11
+ // billing-plane incident, where transient 503s were formatted with the hard
12
+ // "billing cap" template — it is checked BEFORE the hard-billing exclusions so
13
+ // those receipts re-execute on the next run instead of replaying the failure.
4
14
  export function workReceiptRepairableFailurePredicateSql(
5
15
  receiptTable: string,
6
16
  ): string {
@@ -9,13 +19,20 @@ export function workReceiptRepairableFailurePredicateSql(
9
19
  OR (
10
20
  ${receiptTable}.failure_kind = ${workReceiptFailureKindCode('terminal')}::smallint
11
21
  AND ${receiptTable}.error IS NOT NULL
12
- AND lower(${receiptTable}.error) NOT LIKE '%billing cap%'
13
- AND lower(${receiptTable}.error) NOT LIKE '%insufficient credits%'
14
- AND lower(${receiptTable}.error) NOT LIKE '%monthly billing limit%'
15
22
  AND (
16
- ${receiptTable}.error ~* '(^|[^0-9])5[0-9]{2}([^0-9]|$)'
17
- OR lower(${receiptTable}.error) LIKE '%transport failed calling%'
18
- OR lower(${receiptTable}.error) LIKE '%runtime api call timed out%'
23
+ lower(${receiptTable}.error) LIKE '%billing_unavailable%'
24
+ OR lower(${receiptTable}.error) LIKE '%billing is temporarily unavailable%'
25
+ OR lower(${receiptTable}.error) LIKE '%billing temporarily unavailable%'
26
+ OR (
27
+ lower(${receiptTable}.error) NOT LIKE '%billing cap%'
28
+ AND lower(${receiptTable}.error) NOT LIKE '%insufficient credits%'
29
+ AND lower(${receiptTable}.error) NOT LIKE '%monthly billing limit%'
30
+ AND (
31
+ ${receiptTable}.error ~* '(^|[^0-9])5[0-9]{2}([^0-9]|$)'
32
+ OR lower(${receiptTable}.error) LIKE '%transport failed calling%'
33
+ OR lower(${receiptTable}.error) LIKE '%runtime api call timed out%'
34
+ )
35
+ )
19
36
  )
20
37
  )
21
38
  )`;
@@ -52,10 +52,40 @@ function isInsufficientCreditsBilling(
52
52
  return billing?.kind === 'insufficient_credits';
53
53
  }
54
54
 
55
+ /**
56
+ * A transient failure of the Deepline billing PLANE (Convex outage, OCC
57
+ * exhaustion) surfaced as HTTP 503 `BILLING_UNAVAILABLE` with `retryable: true`
58
+ * and `failure_description` stating no credits were charged. This is NOT a hard
59
+ * credit/cap denial: a funded org is temporarily blocked because our billing
60
+ * store hiccuped, so it must be repairable on the next run rather than cached as
61
+ * a terminal failure. See `billingUnavailableResponse` in
62
+ * src/lib/integrations/execute-billing.ts. This class is the exact incident
63
+ * poison closed by issues #2561/#2706: without this carve-out a billing-plane
64
+ * outage classifies every affected tool call as `hard_billing_error` ->
65
+ * `terminal` and the next run replays the cached failure instead of retrying.
66
+ */
67
+ export function isTransientBillingFailurePayload(
68
+ payload: Record<string, unknown> | null,
69
+ ): boolean {
70
+ if (!payload) return false;
71
+ const code = String(payload.code ?? payload.error_code ?? '').toUpperCase();
72
+ if (code === 'BILLING_UNAVAILABLE') return true;
73
+ // Any billing-origin payload that explicitly declares itself retryable is a
74
+ // billing-infra hiccup, never a hard denial (denials are never retryable).
75
+ const category = String(
76
+ payload.error_category ?? payload.errorCategory ?? '',
77
+ ).toLowerCase();
78
+ return category === 'billing' && payload.retryable === true;
79
+ }
80
+
55
81
  function isHardBillingFailurePayload(
56
82
  payload: Record<string, unknown> | null,
57
83
  ): payload is Record<string, unknown> {
58
84
  if (!payload) return false;
85
+ // Billing-plane outages (503 BILLING_UNAVAILABLE) are transient infra, not
86
+ // hard denials. Never let them reach the terminal, run-fatal hard-billing
87
+ // path — they must stay repairable so the next run retries.
88
+ if (isTransientBillingFailurePayload(payload)) return false;
59
89
  const category = String(
60
90
  payload.error_category ?? payload.errorCategory ?? '',
61
91
  ).toLowerCase();
@@ -100,6 +130,10 @@ function normalizeHardBillingPayload(
100
130
  function receiptFailureKindForToolErrorPayload(
101
131
  payload: Record<string, unknown> | null,
102
132
  ): WorkReceiptFailureKind {
133
+ // A transient billing-plane outage (503 BILLING_UNAVAILABLE) charged nothing
134
+ // and is explicitly retryable. It must be repairable so the next run
135
+ // re-executes instead of replaying the cached billing-infra failure.
136
+ if (isTransientBillingFailurePayload(payload)) return 'repairable';
103
137
  const code = getStringField(payload, 'code')?.toUpperCase();
104
138
  // A credential connection can be added or replaced between play runs. Keep
105
139
  // the failure terminal within its owning run, but let a later run reclaim
@@ -22,6 +22,21 @@ export function isLegacyRepairableWorkReceiptError(
22
22
  ): boolean {
23
23
  const message = error?.trim().toLowerCase() ?? '';
24
24
  if (!message) return false;
25
+ // Transient billing-PLANE outages (503 BILLING_UNAVAILABLE) were historically
26
+ // formatted with the hard "billing cap exceeded" template (see
27
+ // formatHardBillingFailureMessage), so the receipts written during a
28
+ // billing-plane incident carry a "billing cap" string even though they are
29
+ // retryable infra failures that charged nothing. Recognize them BEFORE the
30
+ // hard-billing exclusion below so the poisoned backlog re-executes on the next
31
+ // run instead of replaying the cached failure. Real hard denials never carry a
32
+ // BILLING_UNAVAILABLE code or "temporarily unavailable" text.
33
+ if (
34
+ message.includes('billing_unavailable') ||
35
+ message.includes('billing is temporarily unavailable') ||
36
+ message.includes('billing temporarily unavailable')
37
+ ) {
38
+ return true;
39
+ }
25
40
  if (
26
41
  message.includes('billing cap') ||
27
42
  message.includes('insufficient credits') ||
package/dist/cli/index.js CHANGED
@@ -625,10 +625,10 @@ var SDK_RELEASE = {
625
625
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
626
626
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
627
627
  // automatically without blocking their current command.
628
- version: "0.1.235",
628
+ version: "0.1.236",
629
629
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
630
630
  supportPolicy: {
631
- latest: "0.1.235",
631
+ latest: "0.1.236",
632
632
  minimumSupported: "0.1.53",
633
633
  deprecatedBelow: "0.1.219",
634
634
  commandMinimumSupported: [
@@ -1456,6 +1456,7 @@ function createSecretRedactionContext(initialValues = []) {
1456
1456
 
1457
1457
  // ../shared_libs/play-runtime/output-size-limits.ts
1458
1458
  var encoder = typeof TextEncoder === "undefined" ? null : new TextEncoder();
1459
+ var LEDGER_TERMINAL_RESULT_MAX_BYTES = 768 * 1024;
1459
1460
  var CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 1 * 1024 * 1024;
1460
1461
  var CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 2 * 1024 * 1024;
1461
1462
  var RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 10 * 1024 * 1024;
@@ -12080,6 +12081,9 @@ function getRunIdFromLiveEvent(event) {
12080
12081
  const packageRunId = getRunRecordFromPackage(payload)?.id;
12081
12082
  return typeof packageRunId === "string" && packageRunId.trim() ? packageRunId : null;
12082
12083
  }
12084
+ function eventConfirmsPlayRunLaunch(event) {
12085
+ return event.type === "play.run.status" && getEventPayload(event).launchConfirmed === true;
12086
+ }
12083
12087
  function getStatusFromLiveEvent(event) {
12084
12088
  if (event.type !== "play.run.status" && event.type !== "play.run.snapshot" && event.type !== "play.run.final_status") {
12085
12089
  return null;
@@ -12506,70 +12510,6 @@ async function waitForPlayCompletionByStream(input2) {
12506
12510
  }
12507
12511
  return null;
12508
12512
  };
12509
- const watchViaObserveTransport = async () => {
12510
- const controller = new AbortController();
12511
- let timedOut = false;
12512
- const remaining = remainingWaitMs();
12513
- const timeout = remaining === null ? null : setTimeout(
12514
- () => {
12515
- timedOut = true;
12516
- controller.abort();
12517
- },
12518
- Math.max(1, remaining)
12519
- );
12520
- try {
12521
- for await (const event of input2.client.observeRunEvents(
12522
- input2.workflowId,
12523
- {
12524
- signal: controller.signal,
12525
- fallback: "none",
12526
- onNotice: input2.jsonOutput ? void 0 : (message) => input2.progress.writeLine(message)
12527
- }
12528
- )) {
12529
- const terminal2 = await handleLiveEvent(event);
12530
- if (terminal2) {
12531
- return terminal2;
12532
- }
12533
- }
12534
- } finally {
12535
- if (timeout) {
12536
- clearTimeout(timeout);
12537
- }
12538
- }
12539
- const terminal = await fetchTerminalStatus();
12540
- if (terminal) {
12541
- return terminal;
12542
- }
12543
- if (timedOut) {
12544
- assertPlayWaitNotTimedOut({ ...input2, lastPhase });
12545
- }
12546
- throw new DeeplineError(
12547
- `Run observation for ${input2.workflowId} ended before a terminal status.`,
12548
- void 0,
12549
- "PLAY_LIVE_STREAM_ENDED",
12550
- { runId: input2.workflowId, workflowId: input2.workflowId }
12551
- );
12552
- };
12553
- try {
12554
- return await watchViaObserveTransport();
12555
- } catch (error) {
12556
- if (!(error instanceof RunObserveTransportUnavailableError)) {
12557
- throw error;
12558
- }
12559
- if (!input2.jsonOutput) {
12560
- input2.progress.writeLine(
12561
- `[play watch] live subscription unavailable (${error.reason}); falling back to SSE tail (support window, ADR-0008)`
12562
- );
12563
- }
12564
- recordCliTrace({
12565
- phase: "cli.play_observe_transport_fallback",
12566
- ms: Date.now() - input2.startedAt,
12567
- ok: true,
12568
- playName: input2.playName,
12569
- workflowId: input2.workflowId,
12570
- reason: error.reason
12571
- });
12572
- }
12573
12513
  const streamOneWindow = async () => {
12574
12514
  const controller = new AbortController();
12575
12515
  let timedOut = false;
@@ -12702,6 +12642,7 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
12702
12642
  let launchedRevisionId = input2.request.revisionId?.trim() || null;
12703
12643
  let eventCount = 0;
12704
12644
  let firstRunIdMs = null;
12645
+ let launchConfirmed = false;
12705
12646
  let lastPhase = null;
12706
12647
  const timeout = input2.waitTimeoutMs === null ? null : setTimeout(
12707
12648
  () => {
@@ -12752,6 +12693,9 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
12752
12693
  input2.onRunStarted?.(eventRunId);
12753
12694
  }
12754
12695
  }
12696
+ if (eventConfirmsPlayRunLaunch(event) && eventRunId === lastKnownWorkflowId) {
12697
+ launchConfirmed = true;
12698
+ }
12755
12699
  const workflowId = lastKnownWorkflowId || "pending";
12756
12700
  if (workflowId !== "pending" && !emittedDashboardUrl) {
12757
12701
  if (!input2.jsonOutput) {
@@ -12828,6 +12772,16 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
12828
12772
  const finalStatus = getFinalStatusFromLiveEvent(event);
12829
12773
  if (finalStatus) {
12830
12774
  lastKnownWorkflowId ||= finalStatus.runId ?? "";
12775
+ if (finalStatus.status === "failed" && !finalStatus.runId) {
12776
+ return {
12777
+ ...finalStatus,
12778
+ runId: "pending",
12779
+ name: input2.playName,
12780
+ playName: input2.playName,
12781
+ dashboardUrl,
12782
+ ...launchedRevisionId ? { revisionId: launchedRevisionId } : {}
12783
+ };
12784
+ }
12831
12785
  const canonicalTerminal = input2.request.profile === "absurd" ? await fetchKnownTerminalStatus() : {
12832
12786
  ...finalStatus,
12833
12787
  dashboardUrl,
@@ -12859,6 +12813,34 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
12859
12813
  });
12860
12814
  return canonicalTerminal;
12861
12815
  }
12816
+ if (lastKnownWorkflowId && launchConfirmed) {
12817
+ if (timeout) {
12818
+ clearTimeout(timeout);
12819
+ }
12820
+ controller.abort();
12821
+ recordCliTrace({
12822
+ phase: "cli.play_start_stream_handoff_to_tail",
12823
+ ms: Date.now() - startedAt,
12824
+ ok: true,
12825
+ playName: input2.playName,
12826
+ workflowId: lastKnownWorkflowId,
12827
+ eventCount,
12828
+ firstRunIdMs,
12829
+ lastPhase
12830
+ });
12831
+ return waitForPlayCompletionByStream({
12832
+ client: input2.client,
12833
+ playName: input2.playName,
12834
+ workflowId: lastKnownWorkflowId,
12835
+ dashboardUrl,
12836
+ jsonOutput: input2.jsonOutput,
12837
+ emitLogs: input2.emitLogs,
12838
+ waitTimeoutMs: input2.waitTimeoutMs,
12839
+ startedAt,
12840
+ state,
12841
+ progress: input2.progress
12842
+ });
12843
+ }
12862
12844
  }
12863
12845
  } catch (error) {
12864
12846
  if (timedOut) {
@@ -14031,7 +14013,7 @@ function playRunPackageTextLedgerIncomplete(pkg) {
14031
14013
  }
14032
14014
  async function resolvePlayRunOutputStatus(input2) {
14033
14015
  const runId = input2.status.runId;
14034
- if (!runId) {
14016
+ if (!runId || runId === "pending") {
14035
14017
  return input2.status;
14036
14018
  }
14037
14019
  const streamedPackage = getPlayRunPackage(input2.status);
@@ -610,10 +610,10 @@ var SDK_RELEASE = {
610
610
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
611
611
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
612
612
  // automatically without blocking their current command.
613
- version: "0.1.235",
613
+ version: "0.1.236",
614
614
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
615
615
  supportPolicy: {
616
- latest: "0.1.235",
616
+ latest: "0.1.236",
617
617
  minimumSupported: "0.1.53",
618
618
  deprecatedBelow: "0.1.219",
619
619
  commandMinimumSupported: [
@@ -1441,6 +1441,7 @@ function createSecretRedactionContext(initialValues = []) {
1441
1441
 
1442
1442
  // ../shared_libs/play-runtime/output-size-limits.ts
1443
1443
  var encoder = typeof TextEncoder === "undefined" ? null : new TextEncoder();
1444
+ var LEDGER_TERMINAL_RESULT_MAX_BYTES = 768 * 1024;
1444
1445
  var CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 1 * 1024 * 1024;
1445
1446
  var CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 2 * 1024 * 1024;
1446
1447
  var RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 10 * 1024 * 1024;
@@ -12109,6 +12110,9 @@ function getRunIdFromLiveEvent(event) {
12109
12110
  const packageRunId = getRunRecordFromPackage(payload)?.id;
12110
12111
  return typeof packageRunId === "string" && packageRunId.trim() ? packageRunId : null;
12111
12112
  }
12113
+ function eventConfirmsPlayRunLaunch(event) {
12114
+ return event.type === "play.run.status" && getEventPayload(event).launchConfirmed === true;
12115
+ }
12112
12116
  function getStatusFromLiveEvent(event) {
12113
12117
  if (event.type !== "play.run.status" && event.type !== "play.run.snapshot" && event.type !== "play.run.final_status") {
12114
12118
  return null;
@@ -12535,70 +12539,6 @@ async function waitForPlayCompletionByStream(input2) {
12535
12539
  }
12536
12540
  return null;
12537
12541
  };
12538
- const watchViaObserveTransport = async () => {
12539
- const controller = new AbortController();
12540
- let timedOut = false;
12541
- const remaining = remainingWaitMs();
12542
- const timeout = remaining === null ? null : setTimeout(
12543
- () => {
12544
- timedOut = true;
12545
- controller.abort();
12546
- },
12547
- Math.max(1, remaining)
12548
- );
12549
- try {
12550
- for await (const event of input2.client.observeRunEvents(
12551
- input2.workflowId,
12552
- {
12553
- signal: controller.signal,
12554
- fallback: "none",
12555
- onNotice: input2.jsonOutput ? void 0 : (message) => input2.progress.writeLine(message)
12556
- }
12557
- )) {
12558
- const terminal2 = await handleLiveEvent(event);
12559
- if (terminal2) {
12560
- return terminal2;
12561
- }
12562
- }
12563
- } finally {
12564
- if (timeout) {
12565
- clearTimeout(timeout);
12566
- }
12567
- }
12568
- const terminal = await fetchTerminalStatus();
12569
- if (terminal) {
12570
- return terminal;
12571
- }
12572
- if (timedOut) {
12573
- assertPlayWaitNotTimedOut({ ...input2, lastPhase });
12574
- }
12575
- throw new DeeplineError(
12576
- `Run observation for ${input2.workflowId} ended before a terminal status.`,
12577
- void 0,
12578
- "PLAY_LIVE_STREAM_ENDED",
12579
- { runId: input2.workflowId, workflowId: input2.workflowId }
12580
- );
12581
- };
12582
- try {
12583
- return await watchViaObserveTransport();
12584
- } catch (error) {
12585
- if (!(error instanceof RunObserveTransportUnavailableError)) {
12586
- throw error;
12587
- }
12588
- if (!input2.jsonOutput) {
12589
- input2.progress.writeLine(
12590
- `[play watch] live subscription unavailable (${error.reason}); falling back to SSE tail (support window, ADR-0008)`
12591
- );
12592
- }
12593
- recordCliTrace({
12594
- phase: "cli.play_observe_transport_fallback",
12595
- ms: Date.now() - input2.startedAt,
12596
- ok: true,
12597
- playName: input2.playName,
12598
- workflowId: input2.workflowId,
12599
- reason: error.reason
12600
- });
12601
- }
12602
12542
  const streamOneWindow = async () => {
12603
12543
  const controller = new AbortController();
12604
12544
  let timedOut = false;
@@ -12731,6 +12671,7 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
12731
12671
  let launchedRevisionId = input2.request.revisionId?.trim() || null;
12732
12672
  let eventCount = 0;
12733
12673
  let firstRunIdMs = null;
12674
+ let launchConfirmed = false;
12734
12675
  let lastPhase = null;
12735
12676
  const timeout = input2.waitTimeoutMs === null ? null : setTimeout(
12736
12677
  () => {
@@ -12781,6 +12722,9 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
12781
12722
  input2.onRunStarted?.(eventRunId);
12782
12723
  }
12783
12724
  }
12725
+ if (eventConfirmsPlayRunLaunch(event) && eventRunId === lastKnownWorkflowId) {
12726
+ launchConfirmed = true;
12727
+ }
12784
12728
  const workflowId = lastKnownWorkflowId || "pending";
12785
12729
  if (workflowId !== "pending" && !emittedDashboardUrl) {
12786
12730
  if (!input2.jsonOutput) {
@@ -12857,6 +12801,16 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
12857
12801
  const finalStatus = getFinalStatusFromLiveEvent(event);
12858
12802
  if (finalStatus) {
12859
12803
  lastKnownWorkflowId ||= finalStatus.runId ?? "";
12804
+ if (finalStatus.status === "failed" && !finalStatus.runId) {
12805
+ return {
12806
+ ...finalStatus,
12807
+ runId: "pending",
12808
+ name: input2.playName,
12809
+ playName: input2.playName,
12810
+ dashboardUrl,
12811
+ ...launchedRevisionId ? { revisionId: launchedRevisionId } : {}
12812
+ };
12813
+ }
12860
12814
  const canonicalTerminal = input2.request.profile === "absurd" ? await fetchKnownTerminalStatus() : {
12861
12815
  ...finalStatus,
12862
12816
  dashboardUrl,
@@ -12888,6 +12842,34 @@ async function startAndWaitForPlayCompletionByStreamOnce(input2) {
12888
12842
  });
12889
12843
  return canonicalTerminal;
12890
12844
  }
12845
+ if (lastKnownWorkflowId && launchConfirmed) {
12846
+ if (timeout) {
12847
+ clearTimeout(timeout);
12848
+ }
12849
+ controller.abort();
12850
+ recordCliTrace({
12851
+ phase: "cli.play_start_stream_handoff_to_tail",
12852
+ ms: Date.now() - startedAt,
12853
+ ok: true,
12854
+ playName: input2.playName,
12855
+ workflowId: lastKnownWorkflowId,
12856
+ eventCount,
12857
+ firstRunIdMs,
12858
+ lastPhase
12859
+ });
12860
+ return waitForPlayCompletionByStream({
12861
+ client: input2.client,
12862
+ playName: input2.playName,
12863
+ workflowId: lastKnownWorkflowId,
12864
+ dashboardUrl,
12865
+ jsonOutput: input2.jsonOutput,
12866
+ emitLogs: input2.emitLogs,
12867
+ waitTimeoutMs: input2.waitTimeoutMs,
12868
+ startedAt,
12869
+ state,
12870
+ progress: input2.progress
12871
+ });
12872
+ }
12891
12873
  }
12892
12874
  } catch (error) {
12893
12875
  if (timedOut) {
@@ -14060,7 +14042,7 @@ function playRunPackageTextLedgerIncomplete(pkg) {
14060
14042
  }
14061
14043
  async function resolvePlayRunOutputStatus(input2) {
14062
14044
  const runId = input2.status.runId;
14063
- if (!runId) {
14045
+ if (!runId || runId === "pending") {
14064
14046
  return input2.status;
14065
14047
  }
14066
14048
  const streamedPackage = getPlayRunPackage(input2.status);
package/dist/index.js CHANGED
@@ -424,10 +424,10 @@ var SDK_RELEASE = {
424
424
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
425
425
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
426
426
  // automatically without blocking their current command.
427
- version: "0.1.235",
427
+ version: "0.1.236",
428
428
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
429
429
  supportPolicy: {
430
- latest: "0.1.235",
430
+ latest: "0.1.236",
431
431
  minimumSupported: "0.1.53",
432
432
  deprecatedBelow: "0.1.219",
433
433
  commandMinimumSupported: [
@@ -1255,6 +1255,7 @@ function createSecretRedactionContext(initialValues = []) {
1255
1255
 
1256
1256
  // ../shared_libs/play-runtime/output-size-limits.ts
1257
1257
  var encoder = typeof TextEncoder === "undefined" ? null : new TextEncoder();
1258
+ var LEDGER_TERMINAL_RESULT_MAX_BYTES = 768 * 1024;
1258
1259
  var CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 1 * 1024 * 1024;
1259
1260
  var CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 2 * 1024 * 1024;
1260
1261
  var RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 10 * 1024 * 1024;
package/dist/index.mjs CHANGED
@@ -354,10 +354,10 @@ var SDK_RELEASE = {
354
354
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
355
355
  // 0.1.220 deprecates SDK CLI versions below 0.1.219 and updates them
356
356
  // automatically without blocking their current command.
357
- version: "0.1.235",
357
+ version: "0.1.236",
358
358
  apiContract: "2026-06-dataset-handle-results-hard-cutover",
359
359
  supportPolicy: {
360
- latest: "0.1.235",
360
+ latest: "0.1.236",
361
361
  minimumSupported: "0.1.53",
362
362
  deprecatedBelow: "0.1.219",
363
363
  commandMinimumSupported: [
@@ -1185,6 +1185,7 @@ function createSecretRedactionContext(initialValues = []) {
1185
1185
 
1186
1186
  // ../shared_libs/play-runtime/output-size-limits.ts
1187
1187
  var encoder = typeof TextEncoder === "undefined" ? null : new TextEncoder();
1188
+ var LEDGER_TERMINAL_RESULT_MAX_BYTES = 768 * 1024;
1188
1189
  var CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 1 * 1024 * 1024;
1189
1190
  var CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 2 * 1024 * 1024;
1190
1191
  var RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 10 * 1024 * 1024;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.235",
3
+ "version": "0.1.236",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {