deepline 0.3.312 → 0.3.314
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/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-run-recovery-policy.ts +50 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-event-query.ts +15 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-launch-transport.ts +210 -2
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +226 -14
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +4 -18
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +5 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +126 -51
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +11 -6
- package/dist/cli/{auth-ACDTPM3M.mjs → auth-GWF55S3N.mjs} +4 -4
- package/dist/cli/auth-QX2L4LPM.js +31 -0
- package/dist/cli/{billing-YTJFHEGU.js → billing-2YU6E2IF.js} +70 -70
- package/dist/cli/{billing-FVBBJ77L.mjs → billing-WWOJN7GV.mjs} +4 -4
- package/dist/cli/{chunk-WPTUKXIF.mjs → chunk-25SPKKF2.mjs} +1 -1
- package/dist/cli/{chunk-MK4K5MQU.mjs → chunk-2Q2IKUL5.mjs} +3 -3
- package/dist/cli/{chunk-EHVJ7XXD.mjs → chunk-3A4LJUX6.mjs} +2 -2
- package/dist/cli/{chunk-ENSDO4SG.mjs → chunk-3CKNUJVZ.mjs} +4 -4
- package/dist/cli/{chunk-AA66YTS7.mjs → chunk-3K6QD7Z3.mjs} +1 -1
- package/dist/cli/{chunk-TID5XLTZ.js → chunk-B5LUKLKH.js} +4 -4
- package/dist/cli/{chunk-I25C4KL7.mjs → chunk-CPN2N6EZ.mjs} +7 -6
- package/dist/cli/{chunk-GTEDCRKC.mjs → chunk-DR3S76BG.mjs} +1 -1
- package/dist/cli/{chunk-QFSLMI42.js → chunk-ENQZV7NR.js} +221 -220
- package/dist/cli/{chunk-BRJOVSTH.js → chunk-GSRDZD7R.js} +2 -2
- package/dist/cli/{chunk-6OGNRAS7.mjs → chunk-HTXZLCSP.mjs} +2 -2
- package/dist/cli/{chunk-NWFE4PGX.mjs → chunk-IZTIBG7A.mjs} +13 -3
- package/dist/cli/{chunk-V7N6BJQD.js → chunk-JNUHGWB2.js} +50 -50
- package/dist/cli/{chunk-OV6IFAXH.mjs → chunk-JZZT4GAY.mjs} +5 -5
- package/dist/cli/{chunk-ULI6UVV6.js → chunk-MLEJ2FYH.js} +212 -202
- package/dist/cli/{chunk-6KKETQSI.js → chunk-NG4WDOGN.js} +6 -6
- package/dist/cli/{chunk-6LLJWMYH.js → chunk-ODEWOXWW.js} +6 -6
- package/dist/cli/{chunk-NMZPK7LM.mjs → chunk-OUHCJL5B.mjs} +1 -1
- package/dist/cli/{chunk-ZH7KTVAB.mjs → chunk-QB5MAFCU.mjs} +1 -1
- package/dist/cli/{chunk-VJDKV5RU.js → chunk-UZTTPEG2.js} +6 -6
- package/dist/cli/{chunk-FQVTHBEH.js → chunk-VBRO5QCI.js} +15 -15
- package/dist/cli/{chunk-ZQJDRSR5.js → chunk-VFRZG4OW.js} +1 -1
- package/dist/cli/{chunk-7SW43OAX.js → chunk-VWW6VHQX.js} +60 -60
- package/dist/cli/{chunk-YYU3PYVK.js → chunk-WDPRE6DU.js} +48 -48
- package/dist/cli/{chunk-NJNYWVEF.mjs → chunk-WTO3ITBC.mjs} +2 -2
- package/dist/cli/{chunk-T4IJQKRE.js → chunk-YVMJTXEG.js} +26 -26
- package/dist/cli/{client-ENGOMTTU.mjs → client-EYGNDORT.mjs} +3 -3
- package/dist/cli/client-Z4VFNTUB.js +20 -0
- package/dist/cli/{csv-ENKTPYAJ.mjs → csv-NZCN7SVV.mjs} +3 -3
- package/dist/cli/{csv-6WOJC6NZ.js → csv-YOW5JMOH.js} +12 -12
- package/dist/cli/{db-BAEVP4QK.js → db-6BIYLZ6F.js} +20 -20
- package/dist/cli/{db-DSCJZYJU.mjs → db-HALJV3KY.mjs} +4 -4
- package/dist/cli/{describe-PAUHJF7H.mjs → describe-KY2ZYGBR.mjs} +8 -8
- package/dist/cli/{describe-X2AFAC4B.js → describe-WA6R2JSH.js} +15 -15
- package/dist/cli/{executions-R6JOR67D.mjs → executions-CBOAN6KI.mjs} +4 -4
- package/dist/cli/{executions-LXS6NLCV.js → executions-NMCTN7XM.js} +6 -6
- package/dist/cli/{feedback-CYRVNCGT.mjs → feedback-FOMPE3DJ.mjs} +3 -3
- package/dist/cli/{feedback-HXP2D7K6.js → feedback-ZZFDSCWS.js} +6 -6
- package/dist/cli/git-AC2ATBDK.js +23 -0
- package/dist/cli/{git-XBYBZB4R.mjs → git-YQR2NBCO.mjs} +4 -4
- package/dist/cli/index.js +74 -74
- package/dist/cli/index.mjs +31 -31
- package/dist/cli/{legacy-noops-OQP6VZKE.mjs → legacy-noops-KMU6P3K4.mjs} +4 -4
- package/dist/cli/{legacy-noops-AUNAFEZP.js → legacy-noops-XH2TFFI6.js} +7 -7
- package/dist/cli/{monitors-L2HAAPAL.mjs → monitors-2ONK5L3I.mjs} +4 -4
- package/dist/cli/{monitors-QSIZG3CJ.js → monitors-5M3BZ5OR.js} +85 -85
- package/dist/cli/{org-JUHQGBQE.js → org-6MKULUZI.js} +60 -60
- package/dist/cli/{org-UCSN5XRC.mjs → org-LR5BMCGL.mjs} +4 -4
- package/dist/cli/{play-5AT2RB3M.mjs → play-4NOSD7DW.mjs} +7 -7
- package/dist/cli/play-U3CZ4F2W.js +152 -0
- package/dist/cli/{providers-GVPAPZ4L.mjs → providers-UBGIKGCA.mjs} +4 -4
- package/dist/cli/{providers-CVG4L3VR.js → providers-UFJD3VKF.js} +7 -7
- package/dist/cli/{quickstart-Y6ZU27LE.mjs → quickstart-KDUKQRUH.mjs} +3 -3
- package/dist/cli/{quickstart-7OIFIQG3.js → quickstart-KQZHZE6C.js} +5 -5
- package/dist/cli/{search-4I2Y2NKK.mjs → search-4BVOOS5Q.mjs} +5 -5
- package/dist/cli/search-Y7OTFRCI.js +26 -0
- package/dist/cli/{secrets-O2UFLU52.js → secrets-TVXZ3WJ3.js} +13 -13
- package/dist/cli/{secrets-3Z4IUEBA.mjs → secrets-YGV23N3N.mjs} +4 -4
- package/dist/cli/{sessions-OMAXRY5I.mjs → sessions-G7ROUW35.mjs} +4 -4
- package/dist/cli/{sessions-T6MWWQRH.js → sessions-RWIQWDFD.js} +5 -5
- package/dist/cli/{settings-C47WXZY5.mjs → settings-QZEVGCXE.mjs} +7 -7
- package/dist/cli/{settings-R4LO5NMY.js → settings-TG3E2NR7.js} +74 -74
- package/dist/cli/{setup-FIUTV4DD.js → setup-3KDUNEEE.js} +41 -41
- package/dist/cli/{setup-JPJVTIQ6.mjs → setup-HUXINKOH.mjs} +6 -6
- package/dist/cli/skills-CO5YUWBK.js +28 -0
- package/dist/cli/{skills-6WVTMKP5.mjs → skills-LHU3DB7J.mjs} +4 -4
- package/dist/cli/{tools-TKS77TNU.mjs → tools-J5WQ33BB.mjs} +6 -6
- package/dist/cli/tools-MWPT5X47.js +38 -0
- package/dist/cli/{update-GD754XMA.mjs → update-T6744RM7.mjs} +6 -6
- package/dist/cli/update-VVIJW7ZJ.js +28 -0
- package/dist/cli/{workflow-YKQSPXQZ.mjs → workflow-CG2B65HD.mjs} +5 -5
- package/dist/cli/{workflow-OCUQCNXA.js → workflow-YF5SQJLG.js} +15 -15
- package/dist/index.js +6 -6
- package/dist/index.mjs +3 -3
- package/dist/install-integrity.json +74 -74
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +2 -2
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
- package/dist/cli/auth-TFDGA24W.js +0 -31
- package/dist/cli/client-T6YGH6TT.js +0 -20
- package/dist/cli/git-JOE2RM3Q.js +0 -23
- package/dist/cli/play-2SNMTZGK.js +0 -152
- package/dist/cli/search-DS36UHOX.js +0 -26
- package/dist/cli/skills-GLPKPF7R.js +0 -28
- package/dist/cli/tools-L3YLQ7BR.js +0 -38
- package/dist/cli/update-HXI35NME.js +0 -28
|
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
|
|
|
200
200
|
// getters keep their established compatibility behavior.
|
|
201
201
|
// 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
|
|
202
202
|
// 0.3.90 is the first deliberately versioned SDK release for API v3.
|
|
203
|
-
version: '0.3.
|
|
203
|
+
version: '0.3.314',
|
|
204
204
|
updateSummary:
|
|
205
205
|
'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
|
|
206
206
|
packageCapabilities: {
|
|
@@ -95,6 +95,56 @@ export function isPlayRunRecoveryError(
|
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
const RECOVERY_LOG_DETAIL_MAX_KEYS = 24;
|
|
99
|
+
const RECOVERY_LOG_DETAIL_MAX_KEY_CHARS = 64;
|
|
100
|
+
const RECOVERY_LOG_DETAIL_MAX_STRING_CHARS = 200;
|
|
101
|
+
|
|
102
|
+
export type PlayRunRecoveryLogDetail = Readonly<
|
|
103
|
+
Record<string, string | number | boolean | null>
|
|
104
|
+
>;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Provider facts an adapter attached to the cause of a recovery error, for the
|
|
108
|
+
* scheduler's defer log only. The decision's `reason` stays the single durable
|
|
109
|
+
* recovery fact: this detail is never persisted, never returned to a caller,
|
|
110
|
+
* and never read to choose a recovery action. It is bounded here, at the edge
|
|
111
|
+
* that logs it, whatever the adapter supplied. Total by construction, because
|
|
112
|
+
* a failure to describe a deferral must not alter the deferral.
|
|
113
|
+
*/
|
|
114
|
+
export function playRunRecoveryLogDetail(
|
|
115
|
+
error: unknown,
|
|
116
|
+
): PlayRunRecoveryLogDetail | null {
|
|
117
|
+
try {
|
|
118
|
+
if (!isPlayRunRecoveryError(error)) return null;
|
|
119
|
+
const cause: unknown = error.cause;
|
|
120
|
+
if (!cause || typeof cause !== 'object') return null;
|
|
121
|
+
const supplied = (cause as { recoveryLogDetail?: unknown })
|
|
122
|
+
.recoveryLogDetail;
|
|
123
|
+
if (!supplied || typeof supplied !== 'object' || Array.isArray(supplied)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const detail: Record<string, string | number | boolean | null> = {};
|
|
127
|
+
for (const [key, value] of Object.entries(supplied).slice(
|
|
128
|
+
0,
|
|
129
|
+
RECOVERY_LOG_DETAIL_MAX_KEYS,
|
|
130
|
+
)) {
|
|
131
|
+
const name = key.slice(0, RECOVERY_LOG_DETAIL_MAX_KEY_CHARS);
|
|
132
|
+
if (typeof value === 'string') {
|
|
133
|
+
detail[name] = value.slice(0, RECOVERY_LOG_DETAIL_MAX_STRING_CHARS);
|
|
134
|
+
} else if (
|
|
135
|
+
value === null ||
|
|
136
|
+
typeof value === 'boolean' ||
|
|
137
|
+
(typeof value === 'number' && Number.isFinite(value))
|
|
138
|
+
) {
|
|
139
|
+
detail[name] = value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return Object.keys(detail).length > 0 ? detail : null;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
98
148
|
function errorCode(error: unknown): string | number | null {
|
|
99
149
|
if (!error || typeof error !== 'object') return null;
|
|
100
150
|
const code = (error as { code?: unknown }).code;
|
|
@@ -535,8 +535,21 @@ function parseDsl(input: string): RunEventPredicate {
|
|
|
535
535
|
/^(==|!=|>=|<=|>|<|\b(?:eq|ne|gte|gt|lte|lt|in|exists)\b)/.exec(
|
|
536
536
|
input.slice(position),
|
|
537
537
|
);
|
|
538
|
-
if (!operation)
|
|
539
|
-
|
|
538
|
+
if (!operation) {
|
|
539
|
+
const token =
|
|
540
|
+
/^(?:[=!<>~]+|[A-Za-z_]+)/.exec(input.slice(position))?.[0] ??
|
|
541
|
+
input[position] ??
|
|
542
|
+
'<end>';
|
|
543
|
+
if (token === '=') {
|
|
544
|
+
const corrected = `${input.slice(0, position)}==${input.slice(position + 1)}`;
|
|
545
|
+
fail(
|
|
546
|
+
`Run event DSL does not support "=" at character ${position + 1}. For equality, replace this "=" with "==": ${JSON.stringify(corrected)}.`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
fail(
|
|
550
|
+
`Run event DSL does not support ${JSON.stringify(token)} after ${JSON.stringify(match[0])} at character ${position + 1}. Use ==, !=, >, >=, <, <=, in or exists.`,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
540
553
|
position += operation[0].length;
|
|
541
554
|
const symbols: Record<string, string> = {
|
|
542
555
|
'==': 'eq',
|
|
@@ -22,14 +22,216 @@ import {
|
|
|
22
22
|
DaytonaControlTimeoutError,
|
|
23
23
|
} from './daytona-control-transport';
|
|
24
24
|
|
|
25
|
+
// Diagnostics below are log-only. They never select an error, a retry, or a
|
|
26
|
+
// recovery decision, and they stay out of every persisted run document.
|
|
27
|
+
const DAYTONA_LOGGED_SANDBOX_STATES: ReadonlySet<string> = new Set([
|
|
28
|
+
'creating',
|
|
29
|
+
'restoring',
|
|
30
|
+
'destroyed',
|
|
31
|
+
'destroying',
|
|
32
|
+
'started',
|
|
33
|
+
'stopped',
|
|
34
|
+
'starting',
|
|
35
|
+
'stopping',
|
|
36
|
+
'error',
|
|
37
|
+
'build_failed',
|
|
38
|
+
'pending_build',
|
|
39
|
+
'building_snapshot',
|
|
40
|
+
'unknown',
|
|
41
|
+
'pulling_snapshot',
|
|
42
|
+
'archived',
|
|
43
|
+
'archiving',
|
|
44
|
+
'resizing',
|
|
45
|
+
'snapshotting',
|
|
46
|
+
'forking',
|
|
47
|
+
]);
|
|
48
|
+
const DAYTONA_LOGGED_ERROR_NAMES: ReadonlySet<string> = new Set([
|
|
49
|
+
'DaytonaError',
|
|
50
|
+
'DaytonaNotFoundError',
|
|
51
|
+
'DaytonaRateLimitError',
|
|
52
|
+
'DaytonaAuthenticationError',
|
|
53
|
+
'DaytonaAuthorizationError',
|
|
54
|
+
'DaytonaConflictError',
|
|
55
|
+
'DaytonaValidationError',
|
|
56
|
+
'DaytonaTimeoutError',
|
|
57
|
+
'DaytonaConnectionError',
|
|
58
|
+
'DaytonaControlTimeoutError',
|
|
59
|
+
'AbortError',
|
|
60
|
+
'TimeoutError',
|
|
61
|
+
'TypeError',
|
|
62
|
+
'Error',
|
|
63
|
+
]);
|
|
64
|
+
const DAYTONA_LOGGED_PROVIDER_TEXT_MAX_CHARS = 160;
|
|
65
|
+
|
|
66
|
+
/** A provider state from Daytona's published enum, or `unrecognized`. */
|
|
67
|
+
export function daytonaSandboxStateForLog(state: unknown): string | null {
|
|
68
|
+
if (state === undefined || state === null) return null;
|
|
69
|
+
return typeof state === 'string' && DAYTONA_LOGGED_SANDBOX_STATES.has(state)
|
|
70
|
+
? state
|
|
71
|
+
: 'unrecognized';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A known error class name, or `other`; never a provider-authored string. */
|
|
75
|
+
export function daytonaErrorClassForLog(error: unknown): string {
|
|
76
|
+
const name = error instanceof Error ? error.name : null;
|
|
77
|
+
return name !== null && DAYTONA_LOGGED_ERROR_NAMES.has(name) ? name : 'other';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The HTTP status carried by a Daytona error, when it has a plausible one. */
|
|
81
|
+
export function daytonaErrorStatusForLog(error: unknown): number | null {
|
|
82
|
+
if (!error || typeof error !== 'object') return null;
|
|
83
|
+
const status = (error as { statusCode?: unknown }).statusCode;
|
|
84
|
+
return typeof status === 'number' &&
|
|
85
|
+
Number.isInteger(status) &&
|
|
86
|
+
status >= 100 &&
|
|
87
|
+
status <= 599
|
|
88
|
+
? status
|
|
89
|
+
: null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The machine-readable code on a Daytona or transport error (`ERR_CANCELED`,
|
|
94
|
+
* `ECONNRESET`, ...). Only the conventional constant shape is logged, so a
|
|
95
|
+
* provider-authored sentence in that field is dropped rather than copied.
|
|
96
|
+
*/
|
|
97
|
+
export function daytonaErrorCodeForLog(error: unknown): string | null {
|
|
98
|
+
if (!error || typeof error !== 'object') return null;
|
|
99
|
+
const record = error as { errorCode?: unknown; code?: unknown };
|
|
100
|
+
const code = record.errorCode ?? record.code;
|
|
101
|
+
return typeof code === 'string' && /^[A-Z][A-Z0-9_]{1,39}$/.test(code)
|
|
102
|
+
? code
|
|
103
|
+
: null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Credential redaction for a Daytona control-plane error message. Shared by
|
|
108
|
+
* the backend's lookup-failure description and the provider text below.
|
|
109
|
+
*/
|
|
110
|
+
export function redactDaytonaErrorDetail(value: string): string {
|
|
111
|
+
return value
|
|
112
|
+
.replace(
|
|
113
|
+
/authorization\s*[=:]\s*bearer\s+[^\s,;]+/gi,
|
|
114
|
+
'Authorization=Bearer [redacted]',
|
|
115
|
+
)
|
|
116
|
+
.replace(
|
|
117
|
+
/(authorization\s*[=:]\s*)(?!bearer\s+\[redacted\])[^\s,;]+/gi,
|
|
118
|
+
'$1[redacted]',
|
|
119
|
+
)
|
|
120
|
+
.replace(/(bearer\s+)[^\s,;]+/gi, '$1[redacted]')
|
|
121
|
+
.replace(/((?:api[_-]?key|token)\s*[=:]\s*)[^\s,;]+/gi, '$1[redacted]')
|
|
122
|
+
.replace(/\s+/g, ' ')
|
|
123
|
+
.trim()
|
|
124
|
+
.slice(0, 1_000);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Provider free text (for example a sandbox `errorReason`) for logs. This
|
|
129
|
+
* field is written by the provider, so it is redacted more strictly than the
|
|
130
|
+
* shared function alone: URL userinfo, any authorization scheme, password,
|
|
131
|
+
* secret, token, signature, credential and key pairs in query, header or JSON
|
|
132
|
+
* form, email addresses, and opaque identifiers such as organization ids. It
|
|
133
|
+
* is then cut to one short fixed bound so an upstream response is never
|
|
134
|
+
* copied wholesale.
|
|
135
|
+
*/
|
|
136
|
+
export function boundedDaytonaProviderTextForLog(
|
|
137
|
+
value: unknown,
|
|
138
|
+
): string | null {
|
|
139
|
+
if (typeof value !== 'string') return null;
|
|
140
|
+
const strict = value
|
|
141
|
+
.replace(/(\b[a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, '$1[redacted]@')
|
|
142
|
+
.replace(
|
|
143
|
+
/(authorization\s*[=:]\s*["']?)(?:[a-z]+\s+)?[^\s,;"']+/gi,
|
|
144
|
+
'$1[redacted]',
|
|
145
|
+
)
|
|
146
|
+
.replace(/\b(basic|bearer)\s+[A-Za-z0-9+/=._~-]{8,}/gi, '$1 [redacted]')
|
|
147
|
+
.replace(
|
|
148
|
+
/(["']?[\w.-]*(?:password|passwd|secret|token|signature|credential|api[_-]?key|access[_-]?key)[\w.-]*["']?\s*[=:]\s*)(?:"[^"]*"|'[^']*'|[^\s&,;"'}\]]+)/gi,
|
|
149
|
+
'$1[redacted]',
|
|
150
|
+
)
|
|
151
|
+
.replace(
|
|
152
|
+
/[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/g,
|
|
153
|
+
'[redacted-email]',
|
|
154
|
+
)
|
|
155
|
+
.replace(
|
|
156
|
+
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi,
|
|
157
|
+
'[redacted-id]',
|
|
158
|
+
)
|
|
159
|
+
.replace(
|
|
160
|
+
/\b(?=[A-Za-z0-9_-]*\d)(?=[A-Za-z0-9_-]*[A-Za-z])[A-Za-z0-9_-]{24,}\b/g,
|
|
161
|
+
'[redacted-id]',
|
|
162
|
+
);
|
|
163
|
+
const bounded = redactDaytonaErrorDetail(strict).slice(
|
|
164
|
+
0,
|
|
165
|
+
DAYTONA_LOGGED_PROVIDER_TEXT_MAX_CHARS,
|
|
166
|
+
);
|
|
167
|
+
return bounded || null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** What this client knew when a sent create ended without a verdict. */
|
|
171
|
+
export type DaytonaLaunchOutcomeDiagnostic = {
|
|
172
|
+
/** Whether the create request returned a response before the deadline. */
|
|
173
|
+
createResponseReceived: boolean;
|
|
174
|
+
/**
|
|
175
|
+
* Provider sandbox id from the create response, when one was returned. It
|
|
176
|
+
* is not named `sandboxId`: lifecycle tooling reads that key on a stage
|
|
177
|
+
* event as an acquired sandbox, and this one never started.
|
|
178
|
+
*/
|
|
179
|
+
createResponseSandboxId: string | null;
|
|
180
|
+
/** Last provider state this client saw while waiting for `started`. */
|
|
181
|
+
lastObservedState: string | null;
|
|
182
|
+
providerErrorReason: string | null;
|
|
183
|
+
causeClass: string;
|
|
184
|
+
/** The cause's machine code, such as `ERR_CANCELED` or `ECONNRESET`. */
|
|
185
|
+
causeErrno: string | null;
|
|
186
|
+
causeStatus: number | null;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Describe an unknown create outcome for logs. Total by construction: if the
|
|
191
|
+
* description cannot be built, the caller still throws the same error without
|
|
192
|
+
* one, because a different error here would release the reservation and send
|
|
193
|
+
* a second create.
|
|
194
|
+
*/
|
|
195
|
+
function describeUnknownLaunchOutcome(input: {
|
|
196
|
+
data: SandboxData | undefined;
|
|
197
|
+
waiting: Sandbox | undefined;
|
|
198
|
+
error: unknown;
|
|
199
|
+
}): DaytonaLaunchOutcomeDiagnostic | null {
|
|
200
|
+
try {
|
|
201
|
+
const { data, waiting, error } = input;
|
|
202
|
+
return {
|
|
203
|
+
createResponseReceived: data !== undefined,
|
|
204
|
+
createResponseSandboxId: data?.id ? String(data.id).slice(0, 64) : null,
|
|
205
|
+
lastObservedState: daytonaSandboxStateForLog(
|
|
206
|
+
waiting?.state ?? data?.state,
|
|
207
|
+
),
|
|
208
|
+
providerErrorReason: boundedDaytonaProviderTextForLog(
|
|
209
|
+
waiting?.errorReason ?? data?.errorReason,
|
|
210
|
+
),
|
|
211
|
+
causeClass: daytonaErrorClassForLog(error),
|
|
212
|
+
causeErrno: daytonaErrorCodeForLog(error),
|
|
213
|
+
causeStatus: daytonaErrorStatusForLog(error),
|
|
214
|
+
};
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
25
220
|
/** A sent create may have committed even when no response reached the worker. */
|
|
26
221
|
export class DaytonaLaunchOutcomeUnknownError extends Error {
|
|
27
|
-
|
|
222
|
+
/** Log-only; absent when the error was not raised by the launch client. */
|
|
223
|
+
readonly diagnostic: DaytonaLaunchOutcomeDiagnostic | null;
|
|
224
|
+
|
|
225
|
+
constructor(
|
|
226
|
+
cause: unknown,
|
|
227
|
+
diagnostic: DaytonaLaunchOutcomeDiagnostic | null = null,
|
|
228
|
+
) {
|
|
28
229
|
super(
|
|
29
230
|
'Daytona named create outcome is unknown; reconcile the same operation before another launch.',
|
|
30
231
|
{ cause },
|
|
31
232
|
);
|
|
32
233
|
this.name = 'DaytonaLaunchOutcomeUnknownError';
|
|
234
|
+
this.diagnostic = diagnostic;
|
|
33
235
|
}
|
|
34
236
|
}
|
|
35
237
|
|
|
@@ -208,6 +410,8 @@ export function createDaytonaLaunchClient(
|
|
|
208
410
|
const phase = deadline(timeoutMs, cancellation);
|
|
209
411
|
let sent = false;
|
|
210
412
|
let data: SandboxData | undefined;
|
|
413
|
+
// Read only to describe an unknown outcome in logs.
|
|
414
|
+
let waiting: Sandbox | undefined;
|
|
211
415
|
try {
|
|
212
416
|
phase.signal.throwIfAborted();
|
|
213
417
|
const api = new SandboxApi(
|
|
@@ -244,6 +448,7 @@ export function createDaytonaLaunchClient(
|
|
|
244
448
|
);
|
|
245
449
|
data = response.data;
|
|
246
450
|
const sandbox = makeSandbox(data, () => phase.signal);
|
|
451
|
+
waiting = sandbox;
|
|
247
452
|
await sandbox.waitUntilStarted(options.timeout ?? 60);
|
|
248
453
|
// Future phases own fresh deadlines; none inherits this create scope.
|
|
249
454
|
return makeSandbox(
|
|
@@ -270,7 +475,10 @@ export function createDaytonaLaunchClient(
|
|
|
270
475
|
error instanceof DaytonaError &&
|
|
271
476
|
[400, 401, 403, 429].includes(error.statusCode ?? 0);
|
|
272
477
|
if (sent && !confirmedRejection)
|
|
273
|
-
throw new DaytonaLaunchOutcomeUnknownError(
|
|
478
|
+
throw new DaytonaLaunchOutcomeUnknownError(
|
|
479
|
+
error,
|
|
480
|
+
describeUnknownLaunchOutcome({ data, waiting, error }),
|
|
481
|
+
);
|
|
274
482
|
throw error;
|
|
275
483
|
} finally {
|
|
276
484
|
phase.dispose();
|
package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { CreateSandboxFromSnapshotParams, Sandbox } from '@daytonaio/sdk';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
boundedDaytonaProviderTextForLog,
|
|
4
|
+
type DaytonaLaunchOutcomeDiagnostic,
|
|
5
|
+
DaytonaLaunchOutcomeUnknownError,
|
|
6
|
+
daytonaErrorClassForLog,
|
|
7
|
+
daytonaErrorCodeForLog,
|
|
8
|
+
daytonaErrorStatusForLog,
|
|
9
|
+
daytonaSandboxStateForLog,
|
|
10
|
+
} from './daytona-launch-transport';
|
|
3
11
|
import type {
|
|
4
12
|
PlayRunnerExecutionConfig,
|
|
5
13
|
PlayRunnerResult,
|
|
@@ -110,7 +118,15 @@ export class DaytonaSandboxAcquisitionUnavailableError extends Error {
|
|
|
110
118
|
* creating another sandbox could execute the Play twice.
|
|
111
119
|
*/
|
|
112
120
|
export class DaytonaSandboxTransitionPendingError extends Error {
|
|
113
|
-
constructor(
|
|
121
|
+
constructor(
|
|
122
|
+
readonly sandboxName: string,
|
|
123
|
+
/**
|
|
124
|
+
* What Daytona reported when this decision was made. Log-only: a 404, a
|
|
125
|
+
* lookup error, and every non-started state all reach this same error, so
|
|
126
|
+
* the scheduler logs this to tell them apart. It never changes recovery.
|
|
127
|
+
*/
|
|
128
|
+
readonly recoveryLogDetail: DaytonaSandboxTransitionPendingLogDetail | null = null,
|
|
129
|
+
) {
|
|
114
130
|
super(
|
|
115
131
|
`Daytona sandbox ${sandboxName} is still transitioning after create timed out.`,
|
|
116
132
|
);
|
|
@@ -118,6 +134,140 @@ export class DaytonaSandboxTransitionPendingError extends Error {
|
|
|
118
134
|
}
|
|
119
135
|
}
|
|
120
136
|
|
|
137
|
+
/** One provider lookup of the named sandbox, reduced to bounded log facts. */
|
|
138
|
+
type DaytonaNamedSandboxObservation = {
|
|
139
|
+
lookup: 'found' | 'not_found' | 'error' | 'unsupported';
|
|
140
|
+
sandboxState: string | null;
|
|
141
|
+
/** Not `sandboxId`: that key means an acquired sandbox to lifecycle tooling. */
|
|
142
|
+
observedSandboxId: string | null;
|
|
143
|
+
providerErrorReason: string | null;
|
|
144
|
+
lookupErrorClass: string | null;
|
|
145
|
+
lookupErrno: string | null;
|
|
146
|
+
lookupErrorStatus: number | null;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Flat, bounded scalars only: enumerated states and classes, one provider
|
|
151
|
+
* sandbox id, counts, durations, and one capped provider text field.
|
|
152
|
+
*/
|
|
153
|
+
export type DaytonaSandboxTransitionPendingLogDetail =
|
|
154
|
+
DaytonaNamedSandboxObservation & {
|
|
155
|
+
phase: 'create_reconcile' | 'resume';
|
|
156
|
+
/** Names the case; all of them map to the same recovery decision. */
|
|
157
|
+
reasonDetail: string;
|
|
158
|
+
providerAttempt: number;
|
|
159
|
+
/** Lookups issued by this decision's reconcile ladder (at most 7). */
|
|
160
|
+
lookups: number;
|
|
161
|
+
lookupErrors: number;
|
|
162
|
+
deleteConflicts: number;
|
|
163
|
+
elapsedMs: number;
|
|
164
|
+
/** Known only in the process that sent the create. */
|
|
165
|
+
createResponseReceived: boolean | null;
|
|
166
|
+
createLastObservedState: string | null;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const NO_DAYTONA_NAMED_SANDBOX_LOOKUP: DaytonaNamedSandboxObservation = {
|
|
170
|
+
lookup: 'unsupported',
|
|
171
|
+
sandboxState: null,
|
|
172
|
+
observedSandboxId: null,
|
|
173
|
+
providerErrorReason: null,
|
|
174
|
+
lookupErrorClass: null,
|
|
175
|
+
lookupErrno: null,
|
|
176
|
+
lookupErrorStatus: null,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Both observers are total: they run between a provider lookup and the
|
|
180
|
+
// decision it feeds, so a failure to describe the lookup must not surface.
|
|
181
|
+
function observeDaytonaNamedSandbox(
|
|
182
|
+
sandbox: DaytonaSandbox,
|
|
183
|
+
): DaytonaNamedSandboxObservation {
|
|
184
|
+
try {
|
|
185
|
+
const record = sandbox as { id?: unknown; errorReason?: unknown };
|
|
186
|
+
return {
|
|
187
|
+
...NO_DAYTONA_NAMED_SANDBOX_LOOKUP,
|
|
188
|
+
lookup: 'found',
|
|
189
|
+
sandboxState: daytonaSandboxStateForLog(sandbox.state) ?? 'missing',
|
|
190
|
+
observedSandboxId:
|
|
191
|
+
typeof record.id === 'string' && record.id
|
|
192
|
+
? record.id.slice(0, 64)
|
|
193
|
+
: null,
|
|
194
|
+
providerErrorReason: boundedDaytonaProviderTextForLog(record.errorReason),
|
|
195
|
+
};
|
|
196
|
+
} catch {
|
|
197
|
+
return {
|
|
198
|
+
...NO_DAYTONA_NAMED_SANDBOX_LOOKUP,
|
|
199
|
+
lookup: 'found',
|
|
200
|
+
sandboxState: 'unreadable',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function observeDaytonaNamedSandboxLookupFailure(
|
|
206
|
+
error: unknown,
|
|
207
|
+
): DaytonaNamedSandboxObservation {
|
|
208
|
+
try {
|
|
209
|
+
return {
|
|
210
|
+
...NO_DAYTONA_NAMED_SANDBOX_LOOKUP,
|
|
211
|
+
lookup: isDaytonaSandboxNotFound(error) ? 'not_found' : 'error',
|
|
212
|
+
lookupErrorClass: daytonaErrorClassForLog(error),
|
|
213
|
+
lookupErrno: daytonaErrorCodeForLog(error),
|
|
214
|
+
lookupErrorStatus: daytonaErrorStatusForLog(error),
|
|
215
|
+
};
|
|
216
|
+
} catch {
|
|
217
|
+
return { ...NO_DAYTONA_NAMED_SANDBOX_LOOKUP, lookup: 'error' };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function daytonaTransitionPendingReasonDetail(
|
|
222
|
+
phase: DaytonaSandboxTransitionPendingLogDetail['phase'],
|
|
223
|
+
observation: DaytonaNamedSandboxObservation,
|
|
224
|
+
deleteConflicts: number,
|
|
225
|
+
): string {
|
|
226
|
+
if (observation.lookup !== 'found') {
|
|
227
|
+
return `${phase}:lookup_${observation.lookup}`;
|
|
228
|
+
}
|
|
229
|
+
// Reconcile reaches a pending decision with a visible sandbox only after
|
|
230
|
+
// every delete of it reported an in-progress state change.
|
|
231
|
+
return deleteConflicts > 0
|
|
232
|
+
? `${phase}:delete_conflict:${observation.sandboxState}`
|
|
233
|
+
: `${phase}:sandbox_state:${observation.sandboxState}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The log detail for one pending decision. Total by construction: failing to
|
|
238
|
+
* describe a decision must never replace the decision.
|
|
239
|
+
*/
|
|
240
|
+
function daytonaTransitionPendingLogDetail(input: {
|
|
241
|
+
phase: DaytonaSandboxTransitionPendingLogDetail['phase'];
|
|
242
|
+
observed: TimedOutDaytonaSandboxReconcileObserved;
|
|
243
|
+
providerAttempt: number;
|
|
244
|
+
launchOutcome: DaytonaLaunchOutcomeDiagnostic | null;
|
|
245
|
+
}): DaytonaSandboxTransitionPendingLogDetail | null {
|
|
246
|
+
try {
|
|
247
|
+
const { last, lookups, lookupErrors, deleteConflicts, elapsedMs } =
|
|
248
|
+
input.observed;
|
|
249
|
+
return {
|
|
250
|
+
...last,
|
|
251
|
+
phase: input.phase,
|
|
252
|
+
reasonDetail: daytonaTransitionPendingReasonDetail(
|
|
253
|
+
input.phase,
|
|
254
|
+
last,
|
|
255
|
+
deleteConflicts,
|
|
256
|
+
),
|
|
257
|
+
providerAttempt: input.providerAttempt,
|
|
258
|
+
lookups,
|
|
259
|
+
lookupErrors,
|
|
260
|
+
deleteConflicts,
|
|
261
|
+
elapsedMs,
|
|
262
|
+
createResponseReceived:
|
|
263
|
+
input.launchOutcome?.createResponseReceived ?? null,
|
|
264
|
+
createLastObservedState: input.launchOutcome?.lastObservedState ?? null,
|
|
265
|
+
};
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
121
271
|
export function resolveDaytonaSandboxAcquisitionUnavailableReason(
|
|
122
272
|
errors: readonly string[],
|
|
123
273
|
): DaytonaSandboxAcquisitionUnavailableReason | null {
|
|
@@ -369,11 +519,21 @@ async function createOneShotDaytonaSandbox(input: {
|
|
|
369
519
|
});
|
|
370
520
|
}
|
|
371
521
|
|
|
372
|
-
|
|
522
|
+
/** Log-only tally of one reconcile ladder; it never selects the result. */
|
|
523
|
+
type TimedOutDaytonaSandboxReconcileObserved = {
|
|
524
|
+
last: DaytonaNamedSandboxObservation;
|
|
525
|
+
lookups: number;
|
|
526
|
+
lookupErrors: number;
|
|
527
|
+
deleteConflicts: number;
|
|
528
|
+
elapsedMs: number;
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
type TimedOutDaytonaSandboxReconciliation = (
|
|
373
532
|
| { kind: 'started'; sandbox: DaytonaSandbox }
|
|
374
533
|
| { kind: 'deleted' }
|
|
375
534
|
| { kind: 'not_found' }
|
|
376
|
-
| { kind: 'transition_pending' }
|
|
535
|
+
| { kind: 'transition_pending' }
|
|
536
|
+
) & { observed: TimedOutDaytonaSandboxReconcileObserved };
|
|
377
537
|
|
|
378
538
|
async function reconcileAndDeleteTimedOutDaytonaSandbox(input: {
|
|
379
539
|
daytona: DaytonaClient;
|
|
@@ -386,7 +546,19 @@ async function reconcileAndDeleteTimedOutDaytonaSandbox(input: {
|
|
|
386
546
|
runId?: string;
|
|
387
547
|
};
|
|
388
548
|
}): Promise<TimedOutDaytonaSandboxReconciliation> {
|
|
389
|
-
|
|
549
|
+
const reconcileStartedAt = Date.now();
|
|
550
|
+
let last = NO_DAYTONA_NAMED_SANDBOX_LOOKUP;
|
|
551
|
+
let lookups = 0;
|
|
552
|
+
let lookupErrors = 0;
|
|
553
|
+
let deleteConflicts = 0;
|
|
554
|
+
const observed = (): TimedOutDaytonaSandboxReconcileObserved => ({
|
|
555
|
+
last,
|
|
556
|
+
lookups,
|
|
557
|
+
lookupErrors,
|
|
558
|
+
deleteConflicts,
|
|
559
|
+
elapsedMs: Date.now() - reconcileStartedAt,
|
|
560
|
+
});
|
|
561
|
+
if (!input.daytona.get) return { kind: 'not_found', observed: observed() };
|
|
390
562
|
let lastDeleteConflict: unknown = null;
|
|
391
563
|
for (const delayMs of DAYTONA_TIMED_OUT_CREATE_RECONCILE_DELAYS_MS) {
|
|
392
564
|
input.cancellationSignal?.throwIfAborted();
|
|
@@ -394,9 +566,11 @@ async function reconcileAndDeleteTimedOutDaytonaSandbox(input: {
|
|
|
394
566
|
await waitForAcquisitionRetry(delayMs, input.cancellationSignal);
|
|
395
567
|
}
|
|
396
568
|
let sandbox: DaytonaSandbox;
|
|
569
|
+
lookups += 1;
|
|
397
570
|
try {
|
|
398
571
|
sandbox = await input.daytona.get(input.sandboxName);
|
|
399
572
|
} catch (error) {
|
|
573
|
+
last = observeDaytonaNamedSandboxLookupFailure(error);
|
|
400
574
|
const providerStatus =
|
|
401
575
|
error && typeof error === 'object' && 'statusCode' in error
|
|
402
576
|
? error.statusCode
|
|
@@ -410,7 +584,10 @@ async function reconcileAndDeleteTimedOutDaytonaSandbox(input: {
|
|
|
410
584
|
// A current absence is not proof that the sent create cannot commit.
|
|
411
585
|
// Return it to the durable operation owner without another create or
|
|
412
586
|
// releasing capacity; later recovery re-reads the same identity.
|
|
413
|
-
if (isDaytonaSandboxNotFound(error))
|
|
587
|
+
if (isDaytonaSandboxNotFound(error)) {
|
|
588
|
+
return { kind: 'not_found', observed: observed() };
|
|
589
|
+
}
|
|
590
|
+
lookupErrors += 1;
|
|
414
591
|
continue;
|
|
415
592
|
}
|
|
416
593
|
input.emitStage?.('create:reconcile_state', {
|
|
@@ -423,8 +600,9 @@ async function reconcileAndDeleteTimedOutDaytonaSandbox(input: {
|
|
|
423
600
|
// is safer and faster than attempting a conflicting delete or creating a
|
|
424
601
|
// second sandbox.
|
|
425
602
|
if (sandbox.state === 'started') {
|
|
426
|
-
return { kind: 'started', sandbox };
|
|
603
|
+
return { kind: 'started', sandbox, observed: observed() };
|
|
427
604
|
}
|
|
605
|
+
last = observeDaytonaNamedSandbox(sandbox);
|
|
428
606
|
if (input.operationIdentity) {
|
|
429
607
|
const expected = input.operationIdentity;
|
|
430
608
|
const configuredOrganizationId =
|
|
@@ -455,6 +633,7 @@ async function reconcileAndDeleteTimedOutDaytonaSandbox(input: {
|
|
|
455
633
|
// sandbox while the first one is still becoming ready.
|
|
456
634
|
if (isDaytonaSandboxStateChangeConflict(error)) {
|
|
457
635
|
lastDeleteConflict = error;
|
|
636
|
+
deleteConflicts += 1;
|
|
458
637
|
continue;
|
|
459
638
|
}
|
|
460
639
|
throw new Error(
|
|
@@ -462,12 +641,12 @@ async function reconcileAndDeleteTimedOutDaytonaSandbox(input: {
|
|
|
462
641
|
{ cause: error },
|
|
463
642
|
);
|
|
464
643
|
}
|
|
465
|
-
return { kind: 'deleted' };
|
|
644
|
+
return { kind: 'deleted', observed: observed() };
|
|
466
645
|
}
|
|
467
646
|
if (lastDeleteConflict) {
|
|
468
|
-
return { kind: 'transition_pending' };
|
|
647
|
+
return { kind: 'transition_pending', observed: observed() };
|
|
469
648
|
}
|
|
470
|
-
return { kind: 'not_found' };
|
|
649
|
+
return { kind: 'not_found', observed: observed() };
|
|
471
650
|
}
|
|
472
651
|
|
|
473
652
|
function daytonaSandboxName(providerOperationToken?: string | null): string {
|
|
@@ -553,8 +732,17 @@ async function createRetriedOneShotDaytonaSandbox(input: {
|
|
|
553
732
|
);
|
|
554
733
|
}
|
|
555
734
|
// Neither absence nor an ongoing transition proves that create cannot
|
|
556
|
-
// commit. Retain the durable identity and its capacity reservation.
|
|
557
|
-
|
|
735
|
+
// commit. Retain the durable identity and its capacity reservation. The
|
|
736
|
+
// detail only records which of the two Daytona reported, for the log.
|
|
737
|
+
throw new DaytonaSandboxTransitionPendingError(
|
|
738
|
+
sandboxName,
|
|
739
|
+
daytonaTransitionPendingLogDetail({
|
|
740
|
+
phase: 'resume',
|
|
741
|
+
observed: reconciliation.observed,
|
|
742
|
+
providerAttempt: attempt,
|
|
743
|
+
launchOutcome: null,
|
|
744
|
+
}),
|
|
745
|
+
);
|
|
558
746
|
}
|
|
559
747
|
let sandbox: DaytonaSandbox;
|
|
560
748
|
try {
|
|
@@ -579,17 +767,29 @@ async function createRetriedOneShotDaytonaSandbox(input: {
|
|
|
579
767
|
errorClass: daytonaCreateErrorClass(error),
|
|
580
768
|
});
|
|
581
769
|
errors.push(message);
|
|
770
|
+
// Log-only: whether the create request returned before the deadline and
|
|
771
|
+
// what Daytona last reported, so an unknown outcome is attributable.
|
|
772
|
+
const launchOutcome =
|
|
773
|
+
error instanceof DaytonaLaunchOutcomeUnknownError
|
|
774
|
+
? error.diagnostic
|
|
775
|
+
: null;
|
|
776
|
+
const createFailureLogDetail = {
|
|
777
|
+
createErrorClass: daytonaCreateErrorClass(error),
|
|
778
|
+
...(launchOutcome ?? {}),
|
|
779
|
+
};
|
|
582
780
|
input.emitStage('create:attempt_failed', {
|
|
583
781
|
attempt,
|
|
584
782
|
elapsedMs: Date.now() - input.startedAt,
|
|
585
783
|
attemptElapsedMs: Date.now() - attemptStartedAt,
|
|
586
784
|
error: message,
|
|
785
|
+
...createFailureLogDetail,
|
|
587
786
|
});
|
|
588
787
|
console.warn('[play-runner.daytona.create_attempt_failed]', {
|
|
589
788
|
workflowId: input.context.workflowId ?? null,
|
|
590
789
|
runId: input.context.runId ?? null,
|
|
591
790
|
attempt,
|
|
592
791
|
error: message,
|
|
792
|
+
...createFailureLogDetail,
|
|
593
793
|
});
|
|
594
794
|
const immediateFallbackReason =
|
|
595
795
|
resolveDaytonaSandboxAcquisitionUnavailableReason([message]);
|
|
@@ -640,9 +840,21 @@ async function createRetriedOneShotDaytonaSandbox(input: {
|
|
|
640
840
|
};
|
|
641
841
|
}
|
|
642
842
|
await recordCreateFailure();
|
|
843
|
+
// Same decision as before; the detail records what the ladder saw.
|
|
844
|
+
const reconcileObserved = reconciliation.observed;
|
|
845
|
+
const reconcilePending = (): DaytonaSandboxTransitionPendingError =>
|
|
846
|
+
new DaytonaSandboxTransitionPendingError(
|
|
847
|
+
sandboxName,
|
|
848
|
+
daytonaTransitionPendingLogDetail({
|
|
849
|
+
phase: 'create_reconcile',
|
|
850
|
+
observed: reconcileObserved,
|
|
851
|
+
providerAttempt: attempt,
|
|
852
|
+
launchOutcome,
|
|
853
|
+
}),
|
|
854
|
+
);
|
|
643
855
|
if (reconciliation.kind === 'transition_pending') {
|
|
644
856
|
if (reservation?.providerOperationToken) {
|
|
645
|
-
throw
|
|
857
|
+
throw reconcilePending();
|
|
646
858
|
}
|
|
647
859
|
throw new Error(
|
|
648
860
|
`Daytona timed-out sandbox ${sandboxName} remained in a state transition across the bounded cleanup window. Modal fallback suppressed.`,
|
|
@@ -663,7 +875,7 @@ async function createRetriedOneShotDaytonaSandbox(input: {
|
|
|
663
875
|
);
|
|
664
876
|
}
|
|
665
877
|
if (reservation?.providerOperationToken) {
|
|
666
|
-
throw
|
|
878
|
+
throw reconcilePending();
|
|
667
879
|
}
|
|
668
880
|
throw new Error(
|
|
669
881
|
`${message} Daytona did not return a cleanup-addressable sandbox identity; Modal fallback suppressed. The named ephemeral sandbox retains Daytona's auto-delete-on-stop backstop.`,
|