deepline 0.1.208 → 0.1.210
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.
- package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +0 -2
- package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +102 -138
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/receipts.ts +2 -12
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +12 -4
- package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +25 -92
- package/dist/bundling-sources/sdk/src/client.ts +10 -0
- package/dist/bundling-sources/sdk/src/play.ts +2 -2
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +16 -31
- package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +18 -0
- package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +4 -5
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +316 -232
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +16 -2
- package/dist/bundling-sources/shared_libs/play-runtime/daytona-runtime-config.ts +0 -5
- package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +2 -4
- package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/governor/app-runtime-rate-state-backend.ts +429 -102
- package/dist/bundling-sources/shared_libs/play-runtime/governor/governor.ts +27 -5
- package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +18 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +31 -12
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +103 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +105 -10
- package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +21 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +32 -86
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +141 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +45 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +180 -447
- package/dist/bundling-sources/shared_libs/play-runtime/runtime-constants.ts +12 -0
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +33 -1
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +11 -0
- package/dist/bundling-sources/shared_libs/plays/artifact-kind-guard.ts +112 -0
- package/dist/cli/index.js +33 -8
- package/dist/cli/index.mjs +33 -8
- package/dist/index.d.mts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +26 -4
- package/dist/index.mjs +26 -4
- package/package.json +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/child-run-lifecycle.ts +0 -70
|
@@ -56,7 +56,25 @@ export interface PacingPermit {
|
|
|
56
56
|
release(): void;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Discriminates the backend so the Governor can adapt its policy to it. Only
|
|
61
|
+
* `app_runtime_postgres` (the Absurd/Node substrate's DB-backed pacer) is
|
|
62
|
+
* db-authoritative: it runs the whole token-bucket + AIMD in Postgres, so the
|
|
63
|
+
* Governor's in-memory adaptive admission must become a passthrough and defer
|
|
64
|
+
* pacing to the row. The in-memory and coordinator backends leave `kind`
|
|
65
|
+
* undefined and keep the Governor's in-memory adaptive admission (byte-identical
|
|
66
|
+
* to today).
|
|
67
|
+
*/
|
|
68
|
+
export type RateStateBackendKind = 'app_runtime_postgres';
|
|
69
|
+
|
|
59
70
|
export interface RateStateBackend {
|
|
71
|
+
/**
|
|
72
|
+
* When present, tells the Governor this backend owns pacing authoritatively.
|
|
73
|
+
* Absent on the in-memory and coordinator backends — those keep the Governor's
|
|
74
|
+
* in-memory adaptive admission.
|
|
75
|
+
*/
|
|
76
|
+
readonly kind?: RateStateBackendKind;
|
|
77
|
+
|
|
60
78
|
/**
|
|
61
79
|
* Block until one outbound call is permitted for `bucketId` under all `rules`
|
|
62
80
|
* (request windows always; per-rule `maxConcurrency` only on the in-memory
|
|
@@ -34,6 +34,17 @@ export type PlayRunnerRateStateAcquireResult = {
|
|
|
34
34
|
waitMs: number;
|
|
35
35
|
/** One independently-expiring concurrency reservation per granted permit. */
|
|
36
36
|
leaseIds?: string[];
|
|
37
|
+
/**
|
|
38
|
+
* Absolute epoch-ms until which the pacer's own cooldown holds (internally
|
|
39
|
+
* capped, e.g. 45min probe cadence). Advisory: exposes the current backoff to
|
|
40
|
+
* observers; the client already sleeps on `waitMs`.
|
|
41
|
+
*/
|
|
42
|
+
coolUntilMs?: number;
|
|
43
|
+
/**
|
|
44
|
+
* The most recent server Retry-After stored verbatim (uncapped) as an absolute
|
|
45
|
+
* epoch-ms deadline. Purely observational — never gates a grant on its own.
|
|
46
|
+
*/
|
|
47
|
+
claimedRetryAtMs?: number;
|
|
37
48
|
};
|
|
38
49
|
|
|
39
50
|
export type PlayRunnerRateStateReleaseInput = {
|
|
@@ -126,9 +137,29 @@ export interface PlayRunnerContextConfig {
|
|
|
126
137
|
* path or on a pre-release parent.
|
|
127
138
|
*/
|
|
128
139
|
absurdReleaseId?: string | null;
|
|
140
|
+
/**
|
|
141
|
+
* Push-execution wiring for the Daytona sandbox runner. When present the
|
|
142
|
+
* runner keeps the Absurd run claim alive itself (heartbeating the receipt
|
|
143
|
+
* gateway on a `leaseSeconds / 3` cadence) instead of the worker babysitting a
|
|
144
|
+
* held `executeCommand` await. Absent for the worker-babysat local-process
|
|
145
|
+
* backend and every non-Daytona runtime, which keep their existing behavior.
|
|
146
|
+
*/
|
|
147
|
+
runnerPushExecution?: RunnerPushExecutionConfig | null;
|
|
129
148
|
governance?: GovernanceSnapshot | null;
|
|
130
149
|
}
|
|
131
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Runner-side push-execution parameters. The receipt gateway resolves the run's
|
|
153
|
+
* queue server-side, so the runner only needs the lease window (to derive the
|
|
154
|
+
* heartbeat cadence) and the run identity it heartbeats.
|
|
155
|
+
*/
|
|
156
|
+
export interface RunnerPushExecutionConfig {
|
|
157
|
+
/** Absurd claim lease window in seconds; heartbeat cadence is this / 3. */
|
|
158
|
+
leaseSeconds: number;
|
|
159
|
+
/** The run the runner heartbeats. Must match the executor token's run scope. */
|
|
160
|
+
runId: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
132
163
|
export interface PlayRunnerExecutionConfig {
|
|
133
164
|
artifact: PlayBundleArtifact;
|
|
134
165
|
artifactTransport?: {
|
|
@@ -172,15 +203,6 @@ export type PlayRunnerEvent =
|
|
|
172
203
|
result: PlayRunnerResult;
|
|
173
204
|
};
|
|
174
205
|
|
|
175
|
-
export type PlayRunnerDeferredRuntimeTask = {
|
|
176
|
-
kind: 'daytona_sandbox_cleanup';
|
|
177
|
-
sandboxId: string;
|
|
178
|
-
billingStartedAt: number;
|
|
179
|
-
cpu: number;
|
|
180
|
-
memoryGiB: number;
|
|
181
|
-
diskGiB: number;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
206
|
export type PlayRunnerRuntimeTiming = {
|
|
185
207
|
backend: 'daytona';
|
|
186
208
|
daytonaCreateMs?: number;
|
|
@@ -202,7 +224,6 @@ export type PlayRunnerResult =
|
|
|
202
224
|
inserted?: number;
|
|
203
225
|
skipped?: number;
|
|
204
226
|
runtimeTiming?: PlayRunnerRuntimeTiming;
|
|
205
|
-
deferredRuntimeTasks?: PlayRunnerDeferredRuntimeTask[];
|
|
206
227
|
}
|
|
207
228
|
| {
|
|
208
229
|
status: 'suspended';
|
|
@@ -216,7 +237,6 @@ export type PlayRunnerResult =
|
|
|
216
237
|
inserted?: number;
|
|
217
238
|
skipped?: number;
|
|
218
239
|
runtimeTiming?: PlayRunnerRuntimeTiming;
|
|
219
|
-
deferredRuntimeTasks?: PlayRunnerDeferredRuntimeTask[];
|
|
220
240
|
}
|
|
221
241
|
| {
|
|
222
242
|
status: 'failed';
|
|
@@ -230,5 +250,4 @@ export type PlayRunnerResult =
|
|
|
230
250
|
inserted?: number;
|
|
231
251
|
skipped?: number;
|
|
232
252
|
runtimeTiming?: PlayRunnerRuntimeTiming;
|
|
233
|
-
deferredRuntimeTasks?: PlayRunnerDeferredRuntimeTask[];
|
|
234
253
|
};
|
|
@@ -44,6 +44,89 @@ export class WorkspaceStorageNotReadyError extends Error {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
export const PROVIDER_EXHAUSTED_CODE = 'PROVIDER_EXHAUSTED';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format an absolute epoch-ms retry deadline as a human UTC string for the
|
|
51
|
+
* user-facing PROVIDER_EXHAUSTED message. Contains no provider internals.
|
|
52
|
+
*/
|
|
53
|
+
export function formatProviderRetryAtUtc(retryAtMs: number): string {
|
|
54
|
+
const millis = Number(retryAtMs);
|
|
55
|
+
if (!Number.isFinite(millis)) {
|
|
56
|
+
return 'an unknown time';
|
|
57
|
+
}
|
|
58
|
+
return new Date(millis).toUTCString();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build the user-facing PROVIDER_EXHAUSTED message. Names only the provider and
|
|
63
|
+
* the retry time — never Deepline provider spend, SQL, or tenant internals — so
|
|
64
|
+
* it is safe to surface verbatim past redaction.
|
|
65
|
+
*/
|
|
66
|
+
export function providerExhaustedMessage(input: {
|
|
67
|
+
provider: string;
|
|
68
|
+
retryAtMs: number;
|
|
69
|
+
}): string {
|
|
70
|
+
return (
|
|
71
|
+
`${input.provider} is exhausted and asked us to retry after ${formatProviderRetryAtUtc(input.retryAtMs)}. ` +
|
|
72
|
+
'This call was skipped with no spend. ' +
|
|
73
|
+
'For higher provider throughput guarantees, talk to us about an enterprise plan.'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Thrown by the DB-authoritative pacer (`app_runtime_postgres` rate-state
|
|
79
|
+
* backend) when a provider's hold exceeds the tolerable wait
|
|
80
|
+
* (PROVIDER_EXHAUSTED_MAX_WAIT_MS). The call is skipped BEFORE any tool
|
|
81
|
+
* execution request, so nothing is dispatched and nothing is billed. Row-failure
|
|
82
|
+
* isolation still applies: this is a row/step outcome, not a run-fatal error.
|
|
83
|
+
*/
|
|
84
|
+
export class ProviderExhaustedError extends Error {
|
|
85
|
+
readonly code = PROVIDER_EXHAUSTED_CODE;
|
|
86
|
+
readonly provider: string;
|
|
87
|
+
/** ISO 8601 retry deadline (from the server Retry-After / cooldown). */
|
|
88
|
+
readonly retryAt: string;
|
|
89
|
+
constructor(input: {
|
|
90
|
+
provider: string;
|
|
91
|
+
retryAtMs: number;
|
|
92
|
+
cause?: unknown;
|
|
93
|
+
}) {
|
|
94
|
+
// Prefix the code token so `String(error)` and any persisted error string
|
|
95
|
+
// carry it across the serialization boundary back to the CLI, exactly like
|
|
96
|
+
// WorkspaceStorageNotReadyError.
|
|
97
|
+
super(
|
|
98
|
+
`${PROVIDER_EXHAUSTED_CODE}: ${providerExhaustedMessage({
|
|
99
|
+
provider: input.provider,
|
|
100
|
+
retryAtMs: input.retryAtMs,
|
|
101
|
+
})}`,
|
|
102
|
+
);
|
|
103
|
+
this.name = 'ProviderExhaustedError';
|
|
104
|
+
this.provider = input.provider;
|
|
105
|
+
this.retryAt = Number.isFinite(Number(input.retryAtMs))
|
|
106
|
+
? new Date(Number(input.retryAtMs)).toISOString()
|
|
107
|
+
: new Date(0).toISOString();
|
|
108
|
+
if (input.cause !== undefined) {
|
|
109
|
+
(this as { cause?: unknown }).cause = input.cause;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Derive the human provider name for a PROVIDER_EXHAUSTED error from a pacing
|
|
116
|
+
* bucketId. Bucket ids are `${orgId}:${provider}`; the provider itself is
|
|
117
|
+
* `tool:${toolId}` for undeclared tools (see `defaultPacingForTool`). Strip the
|
|
118
|
+
* org prefix, then unwrap a `tool:` prefix down to the toolId so the message
|
|
119
|
+
* names something the user recognizes.
|
|
120
|
+
*/
|
|
121
|
+
export function providerNameFromBucketId(bucketId: string): string {
|
|
122
|
+
const separator = bucketId.indexOf(':');
|
|
123
|
+
const provider =
|
|
124
|
+
separator >= 0 ? bucketId.slice(separator + 1) : bucketId;
|
|
125
|
+
return provider.startsWith('tool:')
|
|
126
|
+
? provider.slice('tool:'.length)
|
|
127
|
+
: provider;
|
|
128
|
+
}
|
|
129
|
+
|
|
47
130
|
export type PlayRunFailureDetails = {
|
|
48
131
|
code: string;
|
|
49
132
|
phase: string;
|
|
@@ -100,6 +183,26 @@ export function normalizePlayRunFailure(error: unknown): PlayRunFailureDetails {
|
|
|
100
183
|
cause,
|
|
101
184
|
};
|
|
102
185
|
}
|
|
186
|
+
if (
|
|
187
|
+
error instanceof ProviderExhaustedError ||
|
|
188
|
+
/\bPROVIDER_EXHAUSTED\b/.test(cause)
|
|
189
|
+
) {
|
|
190
|
+
// The human message (provider + retry time) already lives in `cause` after
|
|
191
|
+
// the code token prefix; surface it verbatim minus the token so the run
|
|
192
|
+
// terminal reads cleanly. Contains no provider spend or tenant internals by
|
|
193
|
+
// construction (see providerExhaustedMessage).
|
|
194
|
+
const stripped = cause.replace(
|
|
195
|
+
new RegExp(`^${PROVIDER_EXHAUSTED_CODE}:\\s*`),
|
|
196
|
+
'',
|
|
197
|
+
);
|
|
198
|
+
return {
|
|
199
|
+
code: PROVIDER_EXHAUSTED_CODE,
|
|
200
|
+
phase: 'runtime',
|
|
201
|
+
message: stripped,
|
|
202
|
+
retryable: false,
|
|
203
|
+
cause,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
103
206
|
if (
|
|
104
207
|
error instanceof WorkspaceStorageNotReadyError ||
|
|
105
208
|
/\bWORKSPACE_STORAGE_NOT_READY\b/.test(cause)
|
|
@@ -316,6 +316,58 @@ export function isTerminalPlayRunLedgerStatus(
|
|
|
316
316
|
return TERMINAL_STATUSES.has(status);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Aggregate, row-level outcome of a run derived purely from the settled step
|
|
321
|
+
* progress. This is an ADDITIVE truthfulness signal, NOT a status: a run can be
|
|
322
|
+
* `status: 'completed'` (all steps ran to the end) while `failedRows > 0`
|
|
323
|
+
* because row-failure isolation persists failed rows for retry instead of
|
|
324
|
+
* failing the whole run. Readers use `hasRowFailures` to render a truthful
|
|
325
|
+
* "completed with failures" surface without inventing a new public run status.
|
|
326
|
+
*
|
|
327
|
+
* `completedRows` here is the reconciled successes-only count summed from the
|
|
328
|
+
* producer's final step progress (`completed`), which is the durable/persisted
|
|
329
|
+
* success count — NOT a live in-flight estimate. It is what should be compared
|
|
330
|
+
* against persisted dataset rows; the two diverging was the 21-vs-15 symptom of
|
|
331
|
+
* children being lost before their rows settled.
|
|
332
|
+
*/
|
|
333
|
+
export type PlayRunRowOutcomeSummary = {
|
|
334
|
+
completedRows: number;
|
|
335
|
+
failedRows: number;
|
|
336
|
+
totalRows: number;
|
|
337
|
+
hasRowFailures: boolean;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
export function summarizeRunRowOutcomes(
|
|
341
|
+
snapshot: Pick<PlayRunLedgerSnapshot, 'stepsById'>,
|
|
342
|
+
): PlayRunRowOutcomeSummary {
|
|
343
|
+
let completedRows = 0;
|
|
344
|
+
let failedRows = 0;
|
|
345
|
+
let totalRows = 0;
|
|
346
|
+
for (const step of Object.values(snapshot.stepsById)) {
|
|
347
|
+
const progress = step.progress;
|
|
348
|
+
if (!progress) continue;
|
|
349
|
+
// `completed` is successes-only; `failed` is settled-but-failed rows. Sum
|
|
350
|
+
// both across every map/step so a multi-map play reports the whole run's
|
|
351
|
+
// row outcome, not just the last step.
|
|
352
|
+
completedRows += Math.max(0, finiteNumber(progress.completed) ?? 0);
|
|
353
|
+
failedRows += Math.max(0, finiteNumber(progress.failed) ?? 0);
|
|
354
|
+
// Prefer the settled denominator; fall back to completed+failed when a
|
|
355
|
+
// producer never stamped `total` (per the terminal invariant those agree).
|
|
356
|
+
const stepTotal = finiteNumber(progress.total);
|
|
357
|
+
totalRows +=
|
|
358
|
+
stepTotal !== null && stepTotal >= 0
|
|
359
|
+
? stepTotal
|
|
360
|
+
: Math.max(0, finiteNumber(progress.completed) ?? 0) +
|
|
361
|
+
Math.max(0, finiteNumber(progress.failed) ?? 0);
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
completedRows,
|
|
365
|
+
failedRows,
|
|
366
|
+
totalRows,
|
|
367
|
+
hasRowFailures: failedRows > 0,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
319
371
|
export function createEmptyPlayRunLedgerSnapshot(input: {
|
|
320
372
|
runId: string;
|
|
321
373
|
playName?: string | null;
|
|
@@ -529,6 +581,32 @@ function mergeMonotonicStepProgress(input: {
|
|
|
529
581
|
};
|
|
530
582
|
}
|
|
531
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Zero the in-flight row accounting on a settled step. `activeRows` (calls in
|
|
586
|
+
* flight) and `waitingRows` (rows queued behind rate/concurrency limits) are
|
|
587
|
+
* live-only counters: once a step is terminal nothing is in flight or waiting,
|
|
588
|
+
* so leaving the last observed values renders a contradiction like
|
|
589
|
+
* "status: completed · 1 in flight · waiting 29". This preserves the settled
|
|
590
|
+
* counts (completed/failed/total/completedRows/startedRows) and only clears the
|
|
591
|
+
* two live accumulators. Idempotent: dropping already-absent fields is a no-op.
|
|
592
|
+
*/
|
|
593
|
+
function clearInFlightProgressOnTerminal(
|
|
594
|
+
progress: PlayRunLedgerStepProgress | null | undefined,
|
|
595
|
+
): PlayRunLedgerStepProgress | null | undefined {
|
|
596
|
+
if (!progress) return progress;
|
|
597
|
+
if (progress.activeRows === undefined && progress.waitingRows === undefined) {
|
|
598
|
+
return progress;
|
|
599
|
+
}
|
|
600
|
+
const cleared = { ...progress };
|
|
601
|
+
delete cleared.activeRows;
|
|
602
|
+
delete cleared.waitingRows;
|
|
603
|
+
return cleared;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function isTerminalStepStatus(status: PlayRunLedgerStepStatus): boolean {
|
|
607
|
+
return status === 'completed' || status === 'failed' || status === 'skipped';
|
|
608
|
+
}
|
|
609
|
+
|
|
532
610
|
function normalizeStepStatus(value: unknown): PlayRunLedgerStepStatus | null {
|
|
533
611
|
const normalized = String(value ?? '')
|
|
534
612
|
.trim()
|
|
@@ -838,11 +916,11 @@ function settleRunningStepsOnTerminal(
|
|
|
838
916
|
completedAt: step.completedAt ?? occurredAt,
|
|
839
917
|
updatedAt: occurredAt,
|
|
840
918
|
progress: step.progress
|
|
841
|
-
? {
|
|
919
|
+
? clearInFlightProgressOnTerminal({
|
|
842
920
|
...step.progress,
|
|
843
921
|
completedAt: step.progress.completedAt ?? occurredAt,
|
|
844
922
|
updatedAt: occurredAt,
|
|
845
|
-
}
|
|
923
|
+
})
|
|
846
924
|
: step.progress,
|
|
847
925
|
},
|
|
848
926
|
];
|
|
@@ -1043,19 +1121,30 @@ export function reducePlayRunLedgerEvent(
|
|
|
1043
1121
|
case 'step.progress': {
|
|
1044
1122
|
const current = base.stepsById[event.stepId];
|
|
1045
1123
|
const runTerminal = isTerminalPlayRunLedgerStatus(base.status);
|
|
1046
|
-
const
|
|
1124
|
+
const mergedProgress = {
|
|
1047
1125
|
...mergeMonotonicStepProgress({
|
|
1048
1126
|
current: current?.progress,
|
|
1049
1127
|
next: event.progress,
|
|
1050
1128
|
}),
|
|
1051
1129
|
updatedAt: event.progress.updatedAt ?? occurredAt,
|
|
1052
1130
|
};
|
|
1131
|
+
// An explicit `completedAt` on THIS event is the producer's authoritative
|
|
1132
|
+
// completion signal and always settles the step. Absent that, the
|
|
1133
|
+
// `completed >= total` heuristic is a stale-estimate trap while rows are
|
|
1134
|
+
// still in flight (total can shrink below the in-flight count under child
|
|
1135
|
+
// fan-out), so it must NOT infer completion while active rows remain.
|
|
1136
|
+
const eventSignalsCompletion =
|
|
1137
|
+
typeof event.progress.completedAt === 'number';
|
|
1138
|
+
const hasActiveRows =
|
|
1139
|
+
typeof mergedProgress.activeRows === 'number' &&
|
|
1140
|
+
mergedProgress.activeRows > 0;
|
|
1053
1141
|
const inferredStatus =
|
|
1054
|
-
|
|
1055
|
-
(
|
|
1056
|
-
typeof
|
|
1057
|
-
|
|
1058
|
-
|
|
1142
|
+
eventSignalsCompletion ||
|
|
1143
|
+
(!hasActiveRows &&
|
|
1144
|
+
typeof mergedProgress.total === 'number' &&
|
|
1145
|
+
typeof mergedProgress.completed === 'number' &&
|
|
1146
|
+
mergedProgress.total > 0 &&
|
|
1147
|
+
mergedProgress.completed >= mergedProgress.total)
|
|
1059
1148
|
? 'completed'
|
|
1060
1149
|
: 'running';
|
|
1061
1150
|
const status =
|
|
@@ -1085,6 +1174,11 @@ export function reducePlayRunLedgerEvent(
|
|
|
1085
1174
|
(status === 'completed' || status === 'failed'
|
|
1086
1175
|
? (completedAt ?? event.progress.updatedAt ?? occurredAt)
|
|
1087
1176
|
: null);
|
|
1177
|
+
// Once the step is settled, drop the live in-flight/waiting accumulators
|
|
1178
|
+
// so a terminal node never reports active or waiting rows.
|
|
1179
|
+
const progress = isTerminalStepStatus(status)
|
|
1180
|
+
? clearInFlightProgressOnTerminal(mergedProgress)
|
|
1181
|
+
: mergedProgress;
|
|
1088
1182
|
const nextStep: PlayRunLedgerStepSnapshot = {
|
|
1089
1183
|
...(current ?? { stepId: event.stepId, status }),
|
|
1090
1184
|
stepId: event.stepId,
|
|
@@ -1092,7 +1186,7 @@ export function reducePlayRunLedgerEvent(
|
|
|
1092
1186
|
kind: event.kind ?? current?.kind,
|
|
1093
1187
|
status,
|
|
1094
1188
|
artifactTableNamespace:
|
|
1095
|
-
progress
|
|
1189
|
+
progress?.artifactTableNamespace ??
|
|
1096
1190
|
current?.artifactTableNamespace ??
|
|
1097
1191
|
null,
|
|
1098
1192
|
startedAt,
|
|
@@ -1155,7 +1249,8 @@ export function reducePlayRunLedgerEvent(
|
|
|
1155
1249
|
: null),
|
|
1156
1250
|
completedAt,
|
|
1157
1251
|
updatedAt: occurredAt,
|
|
1158
|
-
|
|
1252
|
+
// Terminal step: clear live in-flight/waiting accounting.
|
|
1253
|
+
progress: clearInFlightProgressOnTerminal(current?.progress) ?? null,
|
|
1159
1254
|
};
|
|
1160
1255
|
const runningStepIds = base.orderedStepIds.filter((stepId) => {
|
|
1161
1256
|
if (stepId === event.stepId) return false;
|
|
@@ -2,8 +2,10 @@ import { resolveTimingWindow } from './live-events';
|
|
|
2
2
|
import {
|
|
3
3
|
LOG_TAIL_LIMIT,
|
|
4
4
|
normalizePlayRunLedgerSnapshot,
|
|
5
|
+
summarizeRunRowOutcomes,
|
|
5
6
|
type PlayRunLedgerSnapshot,
|
|
6
7
|
type PlayRunLedgerStepSnapshot,
|
|
8
|
+
type PlayRunRowOutcomeSummary,
|
|
7
9
|
} from './run-ledger';
|
|
8
10
|
import {
|
|
9
11
|
isActivePlayRunLifecycleStatus,
|
|
@@ -76,6 +78,14 @@ export type PlayRunLiveSnapshot = {
|
|
|
76
78
|
resultTableNamespace: string | null;
|
|
77
79
|
nodeStates: PlayRunStreamNodeState[];
|
|
78
80
|
activeNodeId: string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Additive, terminal-only aggregate of row outcomes across all steps. Present
|
|
83
|
+
* once the run is terminal so readers can render a truthful "completed with
|
|
84
|
+
* failures" surface (row-failure isolation persists failed rows for retry, so
|
|
85
|
+
* a run legitimately reports `status: 'completed'` with `rowOutcomes.failedRows
|
|
86
|
+
* > 0`). Absent on non-terminal snapshots and on runs with no row/map steps.
|
|
87
|
+
*/
|
|
88
|
+
rowOutcomes?: PlayRunRowOutcomeSummary;
|
|
79
89
|
};
|
|
80
90
|
|
|
81
91
|
export function normalizePlayRunLiveStatus(value: unknown): PlayRunLiveStatus {
|
|
@@ -179,9 +189,18 @@ function buildSnapshotFromLedger(
|
|
|
179
189
|
completedAt: step.completedAt ?? null,
|
|
180
190
|
updatedAt: step.updatedAt ?? null,
|
|
181
191
|
}));
|
|
192
|
+
const liveStatus = normalizePlayRunLiveStatus(snapshot.status);
|
|
193
|
+
// Only surface the row-outcome aggregate once the run is terminal and it
|
|
194
|
+
// actually observed row/map steps — mid-flight `failed` counts churn, and a
|
|
195
|
+
// no-map play has no meaningful row outcome to report.
|
|
196
|
+
const rowOutcomes =
|
|
197
|
+
isTerminalPlayRunLiveStatus(liveStatus) &&
|
|
198
|
+
Object.keys(snapshot.stepsById).length > 0
|
|
199
|
+
? summarizeRunRowOutcomes(snapshot)
|
|
200
|
+
: null;
|
|
182
201
|
return {
|
|
183
202
|
runId: snapshot.runId,
|
|
184
|
-
status:
|
|
203
|
+
status: liveStatus,
|
|
185
204
|
createdAt: snapshot.createdAt ?? null,
|
|
186
205
|
startedAt: snapshot.startedAt ?? null,
|
|
187
206
|
finishedAt: snapshot.finishedAt ?? null,
|
|
@@ -195,6 +214,7 @@ function buildSnapshotFromLedger(
|
|
|
195
214
|
resultTableNamespace: snapshot.resultTableNamespace ?? null,
|
|
196
215
|
nodeStates,
|
|
197
216
|
activeNodeId: snapshot.activeStepId ?? null,
|
|
217
|
+
...(rowOutcomes ? { rowOutcomes } : {}),
|
|
198
218
|
};
|
|
199
219
|
}
|
|
200
220
|
|
package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { Daytona } from '@daytonaio/sdk';
|
|
2
2
|
import type {
|
|
3
|
-
PlayRunnerDeferredRuntimeTask,
|
|
4
3
|
PlayRunnerExecutionConfig,
|
|
5
4
|
PlayRunnerResult,
|
|
6
5
|
} from '@shared_libs/play-runtime/protocol';
|
|
7
|
-
import { shouldCleanupSandboxInBackground } from '@shared_libs/play-runtime/daytona-runtime-config';
|
|
8
6
|
import { isIsolatedRuntimeSchedulerSchema } from '@shared_libs/play-runtime/runtime-scheduler-topology';
|
|
9
7
|
|
|
10
8
|
const DAYTONA_CREATE_TIMEOUT_SECONDS = 10;
|
|
@@ -337,19 +335,19 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
|
|
|
337
335
|
};
|
|
338
336
|
}
|
|
339
337
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
338
|
+
/**
|
|
339
|
+
* Deterministic sandbox delete on ALL backend exits (FIX 1). The old shape
|
|
340
|
+
* attached `deferredRuntimeTasks` cleanup records that NOTHING consumed, gated
|
|
341
|
+
* eager deletion behind a `DAYTONA_BACKGROUND_CLEANUP` env that was unset
|
|
342
|
+
* everywhere — every run leaked paid idle compute until Daytona's autoStop
|
|
343
|
+
* fired. Now every exit path deletes directly (fire-and-forget with structured
|
|
344
|
+
* logs; a failed delete never throws into the result path). autoStop remains
|
|
345
|
+
* only the backstop for a crashed worker process.
|
|
346
|
+
*
|
|
347
|
+
* Push execution note: a `detached_runner` suspended return deliberately
|
|
348
|
+
* BYPASSES `withCleanup` — the runner is still executing in the sandbox; the
|
|
349
|
+
* worker's wake leg owns that delete.
|
|
350
|
+
*/
|
|
353
351
|
export function createDaytonaSandboxCleanupManager(): {
|
|
354
352
|
activate(acquired: AcquiredDaytonaSandbox): void;
|
|
355
353
|
currentSandbox(): DaytonaSandbox | null;
|
|
@@ -362,49 +360,31 @@ export function createDaytonaSandboxCleanupManager(): {
|
|
|
362
360
|
} {
|
|
363
361
|
let sandbox: DaytonaSandbox | null = null;
|
|
364
362
|
let sandboxBillingStartedAt: number | null = null;
|
|
365
|
-
const
|
|
366
|
-
[];
|
|
367
|
-
const backgroundCleanupSandboxIds = new Set<string>();
|
|
368
|
-
|
|
369
|
-
const deferredCleanupTask = (): PlayRunnerDeferredRuntimeTask | null => {
|
|
370
|
-
if (!sandbox || sandboxBillingStartedAt === null) {
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
return {
|
|
374
|
-
kind: 'daytona_sandbox_cleanup',
|
|
375
|
-
sandboxId: sandbox.id,
|
|
376
|
-
billingStartedAt: sandboxBillingStartedAt,
|
|
377
|
-
cpu: typeof sandbox.cpu === 'number' ? sandbox.cpu : 1,
|
|
378
|
-
memoryGiB: typeof sandbox.memory === 'number' ? sandbox.memory : 1,
|
|
379
|
-
diskGiB: typeof sandbox.disk === 'number' ? sandbox.disk : 0,
|
|
380
|
-
};
|
|
381
|
-
};
|
|
363
|
+
const deletedSandboxIds = new Set<string>();
|
|
382
364
|
|
|
383
365
|
const deleteSandboxOnce = (input: {
|
|
384
366
|
reason: string;
|
|
385
|
-
task: PlayRunnerDeferredRuntimeTask | null;
|
|
386
367
|
sandboxToDelete: DaytonaSandbox | null;
|
|
387
368
|
}) => {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
!input.sandboxToDelete ||
|
|
391
|
-
backgroundCleanupSandboxIds.has(input.task.sandboxId)
|
|
392
|
-
) {
|
|
369
|
+
const target = input.sandboxToDelete;
|
|
370
|
+
if (!target || deletedSandboxIds.has(target.id)) {
|
|
393
371
|
return false;
|
|
394
372
|
}
|
|
395
|
-
|
|
396
|
-
|
|
373
|
+
deletedSandboxIds.add(target.id);
|
|
374
|
+
const billingStartedAt = sandboxBillingStartedAt;
|
|
375
|
+
void target.delete(30).then(
|
|
397
376
|
() => {
|
|
398
377
|
console.info('[play-runner.daytona.cleanup_done]', {
|
|
399
378
|
reason: input.reason,
|
|
400
|
-
sandboxId:
|
|
401
|
-
elapsedMs:
|
|
379
|
+
sandboxId: target.id,
|
|
380
|
+
elapsedMs:
|
|
381
|
+
billingStartedAt === null ? null : Date.now() - billingStartedAt,
|
|
402
382
|
});
|
|
403
383
|
},
|
|
404
384
|
(error: unknown) => {
|
|
405
385
|
console.warn('[play-runner.daytona.cleanup_failed]', {
|
|
406
386
|
reason: input.reason,
|
|
407
|
-
sandboxId:
|
|
387
|
+
sandboxId: target.id,
|
|
408
388
|
error: error instanceof Error ? error.message : String(error),
|
|
409
389
|
});
|
|
410
390
|
},
|
|
@@ -412,25 +392,6 @@ export function createDaytonaSandboxCleanupManager(): {
|
|
|
412
392
|
return true;
|
|
413
393
|
};
|
|
414
394
|
|
|
415
|
-
const scheduleBackgroundCleanup = (
|
|
416
|
-
task: PlayRunnerDeferredRuntimeTask | null,
|
|
417
|
-
sandboxToDelete: DaytonaSandbox | null,
|
|
418
|
-
) => {
|
|
419
|
-
if (
|
|
420
|
-
!shouldCleanupSandboxInBackground() ||
|
|
421
|
-
!task ||
|
|
422
|
-
!sandboxToDelete ||
|
|
423
|
-
backgroundCleanupSandboxIds.has(task.sandboxId)
|
|
424
|
-
) {
|
|
425
|
-
return false;
|
|
426
|
-
}
|
|
427
|
-
return deleteSandboxOnce({
|
|
428
|
-
reason: 'background',
|
|
429
|
-
task,
|
|
430
|
-
sandboxToDelete,
|
|
431
|
-
});
|
|
432
|
-
};
|
|
433
|
-
|
|
434
395
|
return {
|
|
435
396
|
activate(acquired) {
|
|
436
397
|
sandbox = acquired.sandbox;
|
|
@@ -442,39 +403,24 @@ export function createDaytonaSandboxCleanupManager(): {
|
|
|
442
403
|
cleanupActiveSandboxForCancellation() {
|
|
443
404
|
return deleteSandboxOnce({
|
|
444
405
|
reason: 'cancelled',
|
|
445
|
-
task: deferredCleanupTask(),
|
|
446
406
|
sandboxToDelete: sandbox,
|
|
447
407
|
});
|
|
448
408
|
},
|
|
449
409
|
stashActiveSandboxForRetry() {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
410
|
+
// The retired sandbox is dead — a fresh one replaces it for the retry.
|
|
411
|
+
// Delete NOW instead of deferring to a consumer that never existed.
|
|
412
|
+
deleteSandboxOnce({
|
|
413
|
+
reason: 'retired_for_retry',
|
|
414
|
+
sandboxToDelete: sandbox,
|
|
415
|
+
});
|
|
455
416
|
sandbox = null;
|
|
456
417
|
sandboxBillingStartedAt = null;
|
|
457
418
|
},
|
|
458
419
|
withCleanup(result, input) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
? result
|
|
462
|
-
: {
|
|
463
|
-
...result,
|
|
464
|
-
deferredRuntimeTasks: [
|
|
465
|
-
...deferredCleanupTasksFromPreviousAttempts,
|
|
466
|
-
...(result.deferredRuntimeTasks ?? []),
|
|
467
|
-
],
|
|
468
|
-
};
|
|
469
|
-
const task = deferredCleanupTask();
|
|
470
|
-
const sandboxToDelete = sandbox;
|
|
471
|
-
if (input.cancellationCleanupStarted) {
|
|
472
|
-
return resultWithPreviousCleanup;
|
|
473
|
-
}
|
|
474
|
-
if (scheduleBackgroundCleanup(task, sandboxToDelete)) {
|
|
475
|
-
return resultWithPreviousCleanup;
|
|
420
|
+
if (!input.cancellationCleanupStarted) {
|
|
421
|
+
deleteSandboxOnce({ reason: 'terminal', sandboxToDelete: sandbox });
|
|
476
422
|
}
|
|
477
|
-
return
|
|
423
|
+
return result;
|
|
478
424
|
},
|
|
479
425
|
};
|
|
480
426
|
}
|