deepline 0.1.287 → 0.1.289
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/context.ts +228 -94
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +13 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +65 -4
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +167 -8
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/suspension.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +2 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +80 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
|
@@ -155,7 +155,7 @@ export const SDK_RELEASE = {
|
|
|
155
155
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
156
156
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
157
157
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
158
|
-
version: '0.1.
|
|
158
|
+
version: '0.1.289',
|
|
159
159
|
contracts: {
|
|
160
160
|
api: {
|
|
161
161
|
name: 'sdk-http-api',
|
|
@@ -309,6 +309,10 @@ const MAP_FRAME_FLUSH_INTERVAL_MS = 250;
|
|
|
309
309
|
const TOOL_BATCH_COALESCE_WINDOW_MS = 5;
|
|
310
310
|
const TOOL_SCALAR_COALESCE_WINDOW_MS = 5;
|
|
311
311
|
const TOOL_RETRY_AFTER_FALLBACK_MS = 1_000;
|
|
312
|
+
// The receipt gateway fully buffers the integration response before sending
|
|
313
|
+
// headers to the runner. Once those headers arrive, a long body stall is a
|
|
314
|
+
// broken gateway-to-runner connection, not slow provider work.
|
|
315
|
+
export const TOOL_EXECUTE_RESPONSE_BODY_TIMEOUT_MS = 30_000;
|
|
312
316
|
const TOOL_RETRY_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
313
317
|
const DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000;
|
|
314
318
|
// Every runtime-API tool fetch needs a client-side deadline. Without one, a
|
|
@@ -352,6 +356,35 @@ class ToolExecuteResponseBodyTransportError extends Error {
|
|
|
352
356
|
}
|
|
353
357
|
}
|
|
354
358
|
|
|
359
|
+
class ToolExecuteResponseBodyTimeoutError extends Error {
|
|
360
|
+
constructor(toolId: string) {
|
|
361
|
+
super(
|
|
362
|
+
`Tool ${toolId} response body was not delivered within ${TOOL_EXECUTE_RESPONSE_BODY_TIMEOUT_MS}ms after response headers.`,
|
|
363
|
+
);
|
|
364
|
+
this.name = 'ToolExecuteResponseBodyTimeoutError';
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function readToolExecuteResponseBody<T>(input: {
|
|
369
|
+
toolId: string;
|
|
370
|
+
abortController: AbortController | null;
|
|
371
|
+
read: () => Promise<T>;
|
|
372
|
+
}): Promise<T> {
|
|
373
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
374
|
+
const timeout = new Promise<never>((_resolve, reject) => {
|
|
375
|
+
timeoutHandle = setTimeout(() => {
|
|
376
|
+
const error = new ToolExecuteResponseBodyTimeoutError(input.toolId);
|
|
377
|
+
input.abortController?.abort(error);
|
|
378
|
+
reject(error);
|
|
379
|
+
}, TOOL_EXECUTE_RESPONSE_BODY_TIMEOUT_MS);
|
|
380
|
+
});
|
|
381
|
+
try {
|
|
382
|
+
return await Promise.race([input.read(), timeout]);
|
|
383
|
+
} finally {
|
|
384
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
355
388
|
class ToolExecuteInvalidJsonError extends Error {
|
|
356
389
|
readonly cause: unknown;
|
|
357
390
|
|
|
@@ -1289,6 +1322,7 @@ export class PlayContextImpl {
|
|
|
1289
1322
|
createSecretRedactionContext();
|
|
1290
1323
|
private mapInvocationIndex = 0;
|
|
1291
1324
|
private readonly stepCallIndexByKey = new Map<string, number>();
|
|
1325
|
+
private readonly toolCallIndexByKey = new Map<string, number>();
|
|
1292
1326
|
/**
|
|
1293
1327
|
* Parent-level inline-child aggregates. Maintained only by the single-writer
|
|
1294
1328
|
* progress path (never per child) so concurrent fan-out cannot contend. See
|
|
@@ -3281,7 +3315,9 @@ export class PlayContextImpl {
|
|
|
3281
3315
|
const executionNonce =
|
|
3282
3316
|
typeof wrapped.job_id === 'string' && wrapped.job_id.trim()
|
|
3283
3317
|
? wrapped.job_id.trim()
|
|
3284
|
-
:
|
|
3318
|
+
: stableDigest(
|
|
3319
|
+
`${this.currentRunId}:${toolId}:${stableStringify(originalRequestInput)}:${datasetLimit}`,
|
|
3320
|
+
);
|
|
3285
3321
|
const datasetId = `tool-list:${sha256Hex(
|
|
3286
3322
|
`${toolId}:${this.currentRunId}:${sql}:${datasetLimit}:${executionNonce}`,
|
|
3287
3323
|
)}`;
|
|
@@ -3294,6 +3330,10 @@ export class PlayContextImpl {
|
|
|
3294
3330
|
toolId,
|
|
3295
3331
|
originalRequestInput,
|
|
3296
3332
|
{
|
|
3333
|
+
durableCallReceiptKey: `${buildDurableToolReceiptPrefix({
|
|
3334
|
+
orgId: this.#options.orgId ?? 'unknown-org',
|
|
3335
|
+
toolId,
|
|
3336
|
+
})}${stableDigest(`${datasetId}:${offset}:${limit}`)}`,
|
|
3297
3337
|
timeoutMs: resolveToolRuntimeTimeoutMs(toolId),
|
|
3298
3338
|
customerDbDataset: {
|
|
3299
3339
|
limit: datasetLimit,
|
|
@@ -6461,6 +6501,14 @@ export class PlayContextImpl {
|
|
|
6461
6501
|
toolId,
|
|
6462
6502
|
requestInput: input,
|
|
6463
6503
|
});
|
|
6504
|
+
const store = rowContext.getStore();
|
|
6505
|
+
let logicalCallId: string | null = null;
|
|
6506
|
+
if (!store) {
|
|
6507
|
+
const callIndexKey = `${this.currentExecutionScope.receipt.namespace}:workflow:${normalizedKey}:${toolRequestIdentity}`;
|
|
6508
|
+
const callIndex = this.toolCallIndexByKey.get(callIndexKey) ?? 0;
|
|
6509
|
+
this.toolCallIndexByKey.set(callIndexKey, callIndex + 1);
|
|
6510
|
+
logicalCallId = stableDigest(`${callIndexKey}:${callIndex}`);
|
|
6511
|
+
}
|
|
6464
6512
|
let executionAuthScopeDigest =
|
|
6465
6513
|
(await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null;
|
|
6466
6514
|
const eventWaitHandler =
|
|
@@ -6476,7 +6524,6 @@ export class PlayContextImpl {
|
|
|
6476
6524
|
staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
|
|
6477
6525
|
});
|
|
6478
6526
|
const checkpointCacheKeys = [durableCacheKey];
|
|
6479
|
-
const store = rowContext.getStore();
|
|
6480
6527
|
|
|
6481
6528
|
const executeTool = async (context?: {
|
|
6482
6529
|
leaseId?: string | null;
|
|
@@ -6531,18 +6578,28 @@ export class PlayContextImpl {
|
|
|
6531
6578
|
: `Calling tool: ${toolId}`,
|
|
6532
6579
|
);
|
|
6533
6580
|
const directReceiptLeaseId = context?.leaseId?.trim() || null;
|
|
6581
|
+
if (!logicalCallId) {
|
|
6582
|
+
throw new Error(
|
|
6583
|
+
'Direct tool execution requires a stable logical call identity.',
|
|
6584
|
+
);
|
|
6585
|
+
}
|
|
6586
|
+
const physicalDirectKey = cacheableToolResult
|
|
6587
|
+
? directCacheKey
|
|
6588
|
+
: `${buildDurableToolReceiptPrefix({
|
|
6589
|
+
orgId: this.#options.orgId ?? 'unknown-org',
|
|
6590
|
+
toolId,
|
|
6591
|
+
})}${stableDigest(`${this.currentRunId}:${logicalCallId}:always-fresh`)}`;
|
|
6534
6592
|
const execution = await this.callToolExecutionAPI(toolId, input, {
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
: {}),
|
|
6593
|
+
durableCallReceiptKey: physicalDirectKey,
|
|
6594
|
+
executionAuthScopeDigest,
|
|
6595
|
+
providerIdempotencyKey: cacheableToolResult
|
|
6596
|
+
? this.providerIdempotencyKeyForToolCall({
|
|
6597
|
+
cacheKey: directCacheKey,
|
|
6598
|
+
force: toolCachePolicy.force,
|
|
6599
|
+
leaseId: directReceiptLeaseId,
|
|
6600
|
+
logicalCallId,
|
|
6601
|
+
})
|
|
6602
|
+
: physicalDirectKey,
|
|
6546
6603
|
timeoutMs: resolveToolRuntimeTimeoutMs(toolId, options?.timeoutMs),
|
|
6547
6604
|
...(directReceiptLeaseId &&
|
|
6548
6605
|
(this.#options.markRuntimeStepReceiptRunning ||
|
|
@@ -6692,7 +6749,7 @@ export class PlayContextImpl {
|
|
|
6692
6749
|
) {
|
|
6693
6750
|
try {
|
|
6694
6751
|
const toolRetryPolicy = await this.#options
|
|
6695
|
-
.getToolRetryPolicy?.(toolId)
|
|
6752
|
+
.getToolRetryPolicy?.(toolId, input)
|
|
6696
6753
|
.catch(() => null);
|
|
6697
6754
|
return await this.executeWithRuntimeReceipt(
|
|
6698
6755
|
'tool',
|
|
@@ -8547,11 +8604,17 @@ export class PlayContextImpl {
|
|
|
8547
8604
|
cacheKey: string;
|
|
8548
8605
|
force?: boolean;
|
|
8549
8606
|
leaseId?: string | null;
|
|
8607
|
+
logicalCallId?: string;
|
|
8550
8608
|
}): string {
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8609
|
+
let fallbackAttemptId: string | null = null;
|
|
8610
|
+
if (input.force === true && !input.leaseId) {
|
|
8611
|
+
if (!input.logicalCallId) {
|
|
8612
|
+
throw new Error(
|
|
8613
|
+
'Forced tool execution without a receipt lease requires a stable logical call identity.',
|
|
8614
|
+
);
|
|
8615
|
+
}
|
|
8616
|
+
fallbackAttemptId = `${this.currentRunId}:${input.logicalCallId}`;
|
|
8617
|
+
}
|
|
8555
8618
|
return buildDurableToolProviderIdempotencyKey({
|
|
8556
8619
|
receiptKey: input.cacheKey,
|
|
8557
8620
|
force: input.force,
|
|
@@ -8570,7 +8633,13 @@ export class PlayContextImpl {
|
|
|
8570
8633
|
'executorToken and baseUrl are required for tool API calls (cloud execution only)',
|
|
8571
8634
|
);
|
|
8572
8635
|
}
|
|
8573
|
-
const
|
|
8636
|
+
const requestsDurableInvocationFence =
|
|
8637
|
+
this.#options.requestDurableInvocationFence === true ||
|
|
8638
|
+
this.#options.durableInvocationFence === true;
|
|
8639
|
+
const executeSuffix = requestsDurableInvocationFence
|
|
8640
|
+
? 'execute-fenced-v1'
|
|
8641
|
+
: 'execute';
|
|
8642
|
+
const url = `${this.#options.baseUrl}/api/v2/integrations/${encodeURIComponent(toolId)}/${executeSuffix}`;
|
|
8574
8643
|
const timeoutMs = resolveToolRuntimeTimeoutMs(toolId, options?.timeoutMs);
|
|
8575
8644
|
const provider = toolId.split(/[._]/)[0]?.trim() || 'provider';
|
|
8576
8645
|
const activityId = `provider:${toolId}`;
|
|
@@ -8607,12 +8676,89 @@ export class PlayContextImpl {
|
|
|
8607
8676
|
async (span) => {
|
|
8608
8677
|
const httpFailureAttempts =
|
|
8609
8678
|
createToolExecuteHttpFailureAttemptTracker();
|
|
8679
|
+
// Snapshot the caller-controlled payload exactly once. The replay
|
|
8680
|
+
// decision and every physical attempt must refer to the same bytes,
|
|
8681
|
+
// even if the original object has getters or is later mutated.
|
|
8682
|
+
const toolInputSnapshot = JSON.parse(JSON.stringify(input)) as Record<
|
|
8683
|
+
string,
|
|
8684
|
+
unknown
|
|
8685
|
+
>;
|
|
8610
8686
|
const retryPolicy = await this.#options
|
|
8611
|
-
.getToolRetryPolicy?.(toolId)
|
|
8687
|
+
.getToolRetryPolicy?.(toolId, toolInputSnapshot)
|
|
8612
8688
|
.catch(() => null);
|
|
8613
8689
|
const retrySafeTransientHttp =
|
|
8614
8690
|
retryPolicy?.retrySafeTransientHttp === true;
|
|
8691
|
+
const invocationOrgId = this.#options.orgId?.trim() ?? '';
|
|
8692
|
+
const durableCallReceiptKey =
|
|
8693
|
+
options?.durableCallReceiptKey?.trim() ||
|
|
8694
|
+
(requestsDurableInvocationFence && invocationOrgId
|
|
8695
|
+
? `${buildDurableToolReceiptPrefix({
|
|
8696
|
+
orgId: invocationOrgId,
|
|
8697
|
+
toolId,
|
|
8698
|
+
})}${stableDigest(
|
|
8699
|
+
`transport:${this.currentRunId}:${toolId}:${crypto.randomUUID()}`,
|
|
8700
|
+
)}`
|
|
8701
|
+
: null);
|
|
8702
|
+
const executionAuthScopeDigest = options?.executionAuthScopeDigest
|
|
8703
|
+
? options.executionAuthScopeDigest.trim() || null
|
|
8704
|
+
: ((await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null);
|
|
8705
|
+
const providerIdempotencyKey =
|
|
8706
|
+
options?.providerIdempotencyKey?.trim() || durableCallReceiptKey;
|
|
8707
|
+
// Correlation identity is stable across every transport retry,
|
|
8708
|
+
// including calls without a durable receipt.
|
|
8709
|
+
const deeplineRequestId = providerIdempotencyKey
|
|
8710
|
+
? `ctx-tool-${stableDigest(providerIdempotencyKey).slice(0, 32)}`
|
|
8711
|
+
: `ctx-tool-${crypto.randomUUID()}`;
|
|
8712
|
+
const serializedRequestBody = (invocationAttempt: number) =>
|
|
8713
|
+
JSON.stringify({
|
|
8714
|
+
payload: toolInputSnapshot,
|
|
8715
|
+
metadata: {
|
|
8716
|
+
parent_run_id: this.#options.runId,
|
|
8717
|
+
invocation_attempt: invocationAttempt,
|
|
8718
|
+
...(requestsDurableInvocationFence
|
|
8719
|
+
? { invocation_fence_version: 1 }
|
|
8720
|
+
: {}),
|
|
8721
|
+
...(durableCallReceiptKey
|
|
8722
|
+
? {
|
|
8723
|
+
durable_call_receipt_key: durableCallReceiptKey,
|
|
8724
|
+
...(executionAuthScopeDigest
|
|
8725
|
+
? {
|
|
8726
|
+
execution_auth_scope_digest:
|
|
8727
|
+
executionAuthScopeDigest,
|
|
8728
|
+
}
|
|
8729
|
+
: {}),
|
|
8730
|
+
}
|
|
8731
|
+
: {}),
|
|
8732
|
+
...(options?.customerDbDataset
|
|
8733
|
+
? {
|
|
8734
|
+
query_result_dataset: {
|
|
8735
|
+
limit: options.customerDbDataset.limit,
|
|
8736
|
+
offset: options.customerDbDataset.offset,
|
|
8737
|
+
page_size: options.customerDbDataset.pageSize,
|
|
8738
|
+
total_rows: options.customerDbDataset.totalRows,
|
|
8739
|
+
},
|
|
8740
|
+
...(isCustomerDbDatasetTool(toolId)
|
|
8741
|
+
? {
|
|
8742
|
+
customer_db_dataset: {
|
|
8743
|
+
limit: options.customerDbDataset.limit,
|
|
8744
|
+
offset: options.customerDbDataset.offset,
|
|
8745
|
+
page_size: options.customerDbDataset.pageSize,
|
|
8746
|
+
total_rows: options.customerDbDataset.totalRows,
|
|
8747
|
+
},
|
|
8748
|
+
}
|
|
8749
|
+
: {}),
|
|
8750
|
+
}
|
|
8751
|
+
: {}),
|
|
8752
|
+
...(providerIdempotencyKey
|
|
8753
|
+
? { provider_idempotency_key: providerIdempotencyKey }
|
|
8754
|
+
: {}),
|
|
8755
|
+
},
|
|
8756
|
+
...(this.#options.integrationMode
|
|
8757
|
+
? { integration_mode: this.#options.integrationMode }
|
|
8758
|
+
: {}),
|
|
8759
|
+
});
|
|
8615
8760
|
let transportAttempt = 0;
|
|
8761
|
+
let invocationAttempt = 0;
|
|
8616
8762
|
const retryToolTransportFailure = async (input: {
|
|
8617
8763
|
error: unknown;
|
|
8618
8764
|
elapsedMs: number;
|
|
@@ -8662,23 +8808,7 @@ export class PlayContextImpl {
|
|
|
8662
8808
|
let responseErrorText: string | null = null;
|
|
8663
8809
|
let providerCallStartedAt: number | null = null;
|
|
8664
8810
|
let providerCallElapsedMs: number | null = null;
|
|
8665
|
-
|
|
8666
|
-
options?.durableCallReceiptKey?.trim() || null;
|
|
8667
|
-
const executionAuthScopeDigest =
|
|
8668
|
-
durableCallReceiptKey && options?.executionAuthScopeDigest
|
|
8669
|
-
? options.executionAuthScopeDigest.trim() || null
|
|
8670
|
-
: durableCallReceiptKey
|
|
8671
|
-
? ((await this.resolveToolAuthScopeDigest(toolId))?.trim() ??
|
|
8672
|
-
null)
|
|
8673
|
-
: null;
|
|
8674
|
-
const providerIdempotencyKey =
|
|
8675
|
-
options?.providerIdempotencyKey?.trim() || durableCallReceiptKey;
|
|
8676
|
-
// Every gateway request needs a correlation ID. When the call has
|
|
8677
|
-
// an idempotency key, keep its stable ID across retries; otherwise
|
|
8678
|
-
// give this individual attempt a fresh ID for gateway pairing.
|
|
8679
|
-
const deeplineRequestId = providerIdempotencyKey
|
|
8680
|
-
? `ctx-tool-${stableDigest(providerIdempotencyKey).slice(0, 32)}`
|
|
8681
|
-
: `ctx-tool-${crypto.randomUUID()}`;
|
|
8811
|
+
let fetchDispatched = false;
|
|
8682
8812
|
// Receipt ownership is a liveness contract, not a one-time check.
|
|
8683
8813
|
// Keep it alive for the whole provider HTTP request. The cadence
|
|
8684
8814
|
// comes from the store-issued expiry because a remote runner may
|
|
@@ -8747,6 +8877,7 @@ export class PlayContextImpl {
|
|
|
8747
8877
|
const integrationFetchStartedAt = Date.now();
|
|
8748
8878
|
providerCallStartedAt = integrationFetchStartedAt;
|
|
8749
8879
|
try {
|
|
8880
|
+
fetchDispatched = true;
|
|
8750
8881
|
response = await fetch(url, {
|
|
8751
8882
|
method: 'POST',
|
|
8752
8883
|
signal: abortController?.signal,
|
|
@@ -8765,59 +8896,23 @@ export class PlayContextImpl {
|
|
|
8765
8896
|
}
|
|
8766
8897
|
: {}),
|
|
8767
8898
|
...protectionHeaders,
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
...(durableCallReceiptKey
|
|
8774
|
-
? {
|
|
8775
|
-
durable_call_receipt_key: durableCallReceiptKey,
|
|
8776
|
-
...(executionAuthScopeDigest
|
|
8777
|
-
? {
|
|
8778
|
-
execution_auth_scope_digest:
|
|
8779
|
-
executionAuthScopeDigest,
|
|
8780
|
-
}
|
|
8781
|
-
: {}),
|
|
8782
|
-
}
|
|
8783
|
-
: {}),
|
|
8784
|
-
...(options?.customerDbDataset
|
|
8785
|
-
? {
|
|
8786
|
-
query_result_dataset: {
|
|
8787
|
-
limit: options.customerDbDataset.limit,
|
|
8788
|
-
offset: options.customerDbDataset.offset,
|
|
8789
|
-
page_size: options.customerDbDataset.pageSize,
|
|
8790
|
-
total_rows: options.customerDbDataset.totalRows,
|
|
8791
|
-
},
|
|
8792
|
-
...(isCustomerDbDatasetTool(toolId)
|
|
8793
|
-
? {
|
|
8794
|
-
customer_db_dataset: {
|
|
8795
|
-
limit: options.customerDbDataset.limit,
|
|
8796
|
-
offset: options.customerDbDataset.offset,
|
|
8797
|
-
page_size:
|
|
8798
|
-
options.customerDbDataset.pageSize,
|
|
8799
|
-
total_rows:
|
|
8800
|
-
options.customerDbDataset.totalRows,
|
|
8801
|
-
},
|
|
8802
|
-
}
|
|
8803
|
-
: {}),
|
|
8804
|
-
}
|
|
8805
|
-
: {}),
|
|
8806
|
-
...(providerIdempotencyKey
|
|
8807
|
-
? { provider_idempotency_key: providerIdempotencyKey }
|
|
8808
|
-
: {}),
|
|
8809
|
-
},
|
|
8810
|
-
...(this.#options.integrationMode
|
|
8811
|
-
? { integration_mode: this.#options.integrationMode }
|
|
8899
|
+
...(this.#options.runtimeTestFaultHeader
|
|
8900
|
+
? {
|
|
8901
|
+
'x-deepline-test-fault':
|
|
8902
|
+
this.#options.runtimeTestFaultHeader,
|
|
8903
|
+
}
|
|
8812
8904
|
: {}),
|
|
8813
|
-
}
|
|
8905
|
+
},
|
|
8906
|
+
body: serializedRequestBody(invocationAttempt),
|
|
8814
8907
|
});
|
|
8815
8908
|
if (response.ok) {
|
|
8816
8909
|
try {
|
|
8817
|
-
responseData =
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8910
|
+
responseData = await readToolExecuteResponseBody({
|
|
8911
|
+
toolId,
|
|
8912
|
+
abortController,
|
|
8913
|
+
read: () =>
|
|
8914
|
+
response!.json() as Promise<Record<string, unknown>>,
|
|
8915
|
+
});
|
|
8821
8916
|
} catch (error) {
|
|
8822
8917
|
if (error instanceof SyntaxError) {
|
|
8823
8918
|
throw new ToolExecuteInvalidJsonError(error);
|
|
@@ -8826,7 +8921,11 @@ export class PlayContextImpl {
|
|
|
8826
8921
|
}
|
|
8827
8922
|
} else {
|
|
8828
8923
|
try {
|
|
8829
|
-
responseErrorText = await
|
|
8924
|
+
responseErrorText = await readToolExecuteResponseBody({
|
|
8925
|
+
toolId,
|
|
8926
|
+
abortController,
|
|
8927
|
+
read: () => response!.text(),
|
|
8928
|
+
});
|
|
8830
8929
|
} catch (error) {
|
|
8831
8930
|
throw new ToolExecuteResponseBodyTransportError(error);
|
|
8832
8931
|
}
|
|
@@ -8887,11 +8986,39 @@ export class PlayContextImpl {
|
|
|
8887
8986
|
402,
|
|
8888
8987
|
);
|
|
8889
8988
|
}
|
|
8890
|
-
const
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8989
|
+
const ambiguousDispatchedFailure =
|
|
8990
|
+
fetchDispatched &&
|
|
8991
|
+
(response === null ||
|
|
8992
|
+
error instanceof ToolExecuteResponseBodyTransportError);
|
|
8993
|
+
const hasDurableInvocationIdentity = Boolean(
|
|
8994
|
+
durableCallReceiptKey &&
|
|
8995
|
+
providerIdempotencyKey &&
|
|
8996
|
+
executionAuthScopeDigest,
|
|
8997
|
+
);
|
|
8998
|
+
// A post-header body deadline aborts the fetch controller solely
|
|
8999
|
+
// to stop consuming that response. It must not also cancel the
|
|
9000
|
+
// independent gateway health check that establishes whether the
|
|
9001
|
+
// committed response can be replayed safely. Preserve genuine
|
|
9002
|
+
// caller/runtime cancellation by dropping the signal only when
|
|
9003
|
+
// this exact body deadline caused the abort.
|
|
9004
|
+
const durableFenceVerificationSignal =
|
|
9005
|
+
abortController?.signal.aborted === true &&
|
|
9006
|
+
abortController.signal.reason instanceof
|
|
9007
|
+
ToolExecuteResponseBodyTimeoutError
|
|
9008
|
+
? undefined
|
|
9009
|
+
: abortController?.signal;
|
|
9010
|
+
const durableInvocationFenceVerified =
|
|
9011
|
+
hasDurableInvocationIdentity &&
|
|
9012
|
+
requestsDurableInvocationFence &&
|
|
9013
|
+
(this.#options.durableInvocationFence === true ||
|
|
9014
|
+
(await this.#options
|
|
9015
|
+
.verifyDurableInvocationFence?.(
|
|
9016
|
+
durableFenceVerificationSignal,
|
|
9017
|
+
)
|
|
9018
|
+
.catch(() => false)) === true);
|
|
9019
|
+
const transportReplaySafe =
|
|
9020
|
+
!ambiguousDispatchedFailure || durableInvocationFenceVerified;
|
|
9021
|
+
if (!transportReplaySafe) {
|
|
8895
9022
|
const diagnostic = describeTransportError(transportError);
|
|
8896
9023
|
this.log(
|
|
8897
9024
|
`[runtime.transport_failure] ${JSON.stringify({
|
|
@@ -8905,13 +9032,17 @@ export class PlayContextImpl {
|
|
|
8905
9032
|
: Date.now() - providerCallStartedAt,
|
|
8906
9033
|
request_id: deeplineRequestId,
|
|
8907
9034
|
aborted: abortController?.signal.aborted === true,
|
|
8908
|
-
response_headers_received:
|
|
9035
|
+
response_headers_received: response !== null,
|
|
8909
9036
|
retry_safe: false,
|
|
8910
9037
|
error: diagnostic,
|
|
8911
9038
|
})}`,
|
|
8912
9039
|
);
|
|
9040
|
+
const failureBoundary =
|
|
9041
|
+
error instanceof ToolExecuteResponseBodyTransportError
|
|
9042
|
+
? 'response body transport failed after response headers'
|
|
9043
|
+
: 'request transport failed after dispatch before response headers';
|
|
8913
9044
|
throw new ToolHttpError(
|
|
8914
|
-
`Tool ${toolId}
|
|
9045
|
+
`Tool ${toolId} ${failureBoundary}; the ambiguous call was not retried because this runtime has no durable invocation fence: ${diagnostic.message ?? 'unknown transport error'}`,
|
|
8915
9046
|
null,
|
|
8916
9047
|
0,
|
|
8917
9048
|
'repairable',
|
|
@@ -9004,6 +9135,9 @@ export class PlayContextImpl {
|
|
|
9004
9135
|
);
|
|
9005
9136
|
}
|
|
9006
9137
|
if (failure.shouldRetry) {
|
|
9138
|
+
if (failure.reason !== 'gateway_invocation_in_progress') {
|
|
9139
|
+
invocationAttempt += 1;
|
|
9140
|
+
}
|
|
9007
9141
|
if (failure.chargeRetryBudget) {
|
|
9008
9142
|
await this.governor.chargeBudget('retry');
|
|
9009
9143
|
}
|
|
@@ -512,6 +512,15 @@ export interface ContextOptions {
|
|
|
512
512
|
/** Short-lived HMAC-signed internal token for tool callbacks. Required for cloud execution. */
|
|
513
513
|
executorToken?: string;
|
|
514
514
|
baseUrl?: string;
|
|
515
|
+
/**
|
|
516
|
+
* The integration base URL persists each keyed execute response before
|
|
517
|
+
* exposing it. Only runtime adapters that route through that gateway may
|
|
518
|
+
* enable ambiguous transport replay.
|
|
519
|
+
*/
|
|
520
|
+
durableInvocationFence?: boolean;
|
|
521
|
+
requestDurableInvocationFence?: boolean;
|
|
522
|
+
verifyDurableInvocationFence?: (signal?: AbortSignal) => Promise<boolean>;
|
|
523
|
+
runtimeTestFaultHeader?: string | null;
|
|
515
524
|
/**
|
|
516
525
|
* Runtime-sheet transport selected by the runner. Daytona sandboxes use the
|
|
517
526
|
* execution gateway and must never fall back to minting direct DB sessions.
|
|
@@ -644,7 +653,10 @@ export interface ContextOptions {
|
|
|
644
653
|
resolvePlay?: (playRef: string) => Promise<ResolvedPlayExecution | null>;
|
|
645
654
|
getToolQueueHints?: (toolId: string) => Promise<readonly PlayQueueHint[]>;
|
|
646
655
|
getToolProvider?: (toolId: string) => Promise<string | null>;
|
|
647
|
-
getToolRetryPolicy?: (
|
|
656
|
+
getToolRetryPolicy?: (
|
|
657
|
+
toolId: string,
|
|
658
|
+
input: Record<string, unknown>,
|
|
659
|
+
) => Promise<{
|
|
648
660
|
retrySafeTransientHttp?: boolean;
|
|
649
661
|
requiresExecutionFence?: boolean;
|
|
650
662
|
} | null>;
|
package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts
CHANGED
|
@@ -27,7 +27,8 @@ const DAYTONA_NETWORK_ALLOW_LIST_ENV = 'DEEPLINE_DAYTONA_NETWORK_ALLOW_LIST';
|
|
|
27
27
|
|
|
28
28
|
export const DAYTONA_CANCELLED_ERROR = 'Daytona play runner cancelled';
|
|
29
29
|
|
|
30
|
-
export type DaytonaClient = Pick<Daytona, 'create'
|
|
30
|
+
export type DaytonaClient = Pick<Daytona, 'create'> &
|
|
31
|
+
Partial<Pick<Daytona, 'get'>>;
|
|
31
32
|
export type DaytonaSandbox = Awaited<ReturnType<DaytonaClient['create']>>;
|
|
32
33
|
export type DaytonaExecutionContext = PlayRunnerExecutionConfig['context'];
|
|
33
34
|
export type DaytonaStageEmitter = (
|
|
@@ -37,6 +38,7 @@ export type DaytonaStageEmitter = (
|
|
|
37
38
|
|
|
38
39
|
export type AcquiredDaytonaSandbox = {
|
|
39
40
|
sandbox: DaytonaSandbox;
|
|
41
|
+
daytonaOrganizationId: string;
|
|
40
42
|
billingStartedAt: number;
|
|
41
43
|
billingEndedAt?: number;
|
|
42
44
|
};
|
|
@@ -56,6 +58,22 @@ type DaytonaCreateResult = {
|
|
|
56
58
|
attemptElapsedMs: number;
|
|
57
59
|
};
|
|
58
60
|
|
|
61
|
+
async function rejectAcquiredSandbox(
|
|
62
|
+
sandbox: DaytonaSandbox,
|
|
63
|
+
reason: string,
|
|
64
|
+
): Promise<never> {
|
|
65
|
+
try {
|
|
66
|
+
await sandbox.delete(30);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const cleanupError = error instanceof Error ? error.message : String(error);
|
|
69
|
+
throw new Error(
|
|
70
|
+
`${reason} Defensive deletion of Daytona sandbox ${sandbox.id} also failed: ${cleanupError}`,
|
|
71
|
+
{ cause: error },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
throw new Error(reason);
|
|
75
|
+
}
|
|
76
|
+
|
|
59
77
|
function normalizeLabelValue(value: string | null | undefined): string | null {
|
|
60
78
|
const trimmed = value?.trim();
|
|
61
79
|
return trimmed ? trimmed.slice(0, 63) : null;
|
|
@@ -233,7 +251,15 @@ async function createRetriedOneShotDaytonaSandbox(input: {
|
|
|
233
251
|
} catch (error) {
|
|
234
252
|
const message = error instanceof Error ? error.message : String(error);
|
|
235
253
|
errors.push(message);
|
|
254
|
+
input.emitStage('create:attempt_failed', {
|
|
255
|
+
attempt,
|
|
256
|
+
elapsedMs: Date.now() - input.startedAt,
|
|
257
|
+
attemptElapsedMs: Date.now() - attemptStartedAt,
|
|
258
|
+
error: message,
|
|
259
|
+
});
|
|
236
260
|
console.warn('[play-runner.daytona.create_attempt_failed]', {
|
|
261
|
+
workflowId: input.context.workflowId ?? null,
|
|
262
|
+
runId: input.context.runId ?? null,
|
|
237
263
|
attempt,
|
|
238
264
|
error: message,
|
|
239
265
|
});
|
|
@@ -265,11 +291,46 @@ async function acquireOneShotDaytonaSandbox(input: {
|
|
|
265
291
|
granted.diskGiB !== DAYTONA_SANDBOX_DISK_GIB ||
|
|
266
292
|
granted.gpu !== DAYTONA_SANDBOX_GPU
|
|
267
293
|
) {
|
|
268
|
-
await
|
|
269
|
-
|
|
294
|
+
await rejectAcquiredSandbox(
|
|
295
|
+
result.sandbox,
|
|
270
296
|
`Daytona sandbox resource boundary mismatch: expected cpu=${DAYTONA_SANDBOX_CPU} memoryGiB=${DAYTONA_SANDBOX_MEMORY_GIB} diskGiB=${DAYTONA_SANDBOX_DISK_GIB} gpu=${DAYTONA_SANDBOX_GPU}, granted cpu=${granted.cpu} memoryGiB=${granted.memoryGiB} diskGiB=${granted.diskGiB} gpu=${granted.gpu}`,
|
|
271
297
|
);
|
|
272
298
|
}
|
|
299
|
+
const configuredOrganizationId =
|
|
300
|
+
process.env.DAYTONA_ORGANIZATION_ID?.trim() || null;
|
|
301
|
+
const observedOrganizationId = result.sandbox.organizationId?.trim() || null;
|
|
302
|
+
if (
|
|
303
|
+
configuredOrganizationId &&
|
|
304
|
+
observedOrganizationId &&
|
|
305
|
+
configuredOrganizationId !== observedOrganizationId
|
|
306
|
+
) {
|
|
307
|
+
await rejectAcquiredSandbox(
|
|
308
|
+
result.sandbox,
|
|
309
|
+
'Daytona sandbox organization routing mismatch. Refusing to run customer code in a sandbox whose observed organization differs from the configured organization.',
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
let lookupOrganizationId: string | null = null;
|
|
313
|
+
if (
|
|
314
|
+
!observedOrganizationId &&
|
|
315
|
+
!configuredOrganizationId &&
|
|
316
|
+
input.daytona.get
|
|
317
|
+
) {
|
|
318
|
+
try {
|
|
319
|
+
const lookedUpSandbox = await input.daytona.get(result.sandbox.id);
|
|
320
|
+
lookupOrganizationId = lookedUpSandbox.organizationId?.trim() || null;
|
|
321
|
+
} catch {
|
|
322
|
+
// The failure below is intentionally about the invariant, not the
|
|
323
|
+
// provider response. The newly created sandbox is still deleted.
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const daytonaOrganizationId =
|
|
327
|
+
observedOrganizationId ?? configuredOrganizationId ?? lookupOrganizationId;
|
|
328
|
+
if (!daytonaOrganizationId) {
|
|
329
|
+
return await rejectAcquiredSandbox(
|
|
330
|
+
result.sandbox,
|
|
331
|
+
'Daytona sandbox organization routing identity is missing. Refusing to run customer code without a durable cleanup routing domain.',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
273
334
|
const billingStartedAt = Date.now();
|
|
274
335
|
const sandbox = result.sandbox;
|
|
275
336
|
input.emitStage('create:done', {
|
|
@@ -281,7 +342,7 @@ async function acquireOneShotDaytonaSandbox(input: {
|
|
|
281
342
|
memoryGiB: granted.memoryGiB,
|
|
282
343
|
diskGiB: granted.diskGiB,
|
|
283
344
|
});
|
|
284
|
-
return { sandbox, billingStartedAt };
|
|
345
|
+
return { sandbox, daytonaOrganizationId, billingStartedAt };
|
|
285
346
|
}
|
|
286
347
|
|
|
287
348
|
export function createOneShotDaytonaSandboxLifecycle(input: {
|
|
@@ -61,6 +61,7 @@ export type StagedDaytonaPayload = {
|
|
|
61
61
|
command: string;
|
|
62
62
|
outputPath: string;
|
|
63
63
|
exitCodePath: string;
|
|
64
|
+
runtimeCompletedPath: string;
|
|
64
65
|
progressEventPath: string;
|
|
65
66
|
};
|
|
66
67
|
|
|
@@ -557,6 +558,7 @@ export async function stageDaytonaRunnerPayload(input: {
|
|
|
557
558
|
command,
|
|
558
559
|
outputPath,
|
|
559
560
|
exitCodePath,
|
|
561
|
+
runtimeCompletedPath,
|
|
560
562
|
progressEventPath,
|
|
561
563
|
};
|
|
562
564
|
}
|
|
@@ -289,27 +289,161 @@ export async function inspectDetachedDaytonaRunner(input: {
|
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
|
|
292
|
+
/** Read the runner's exact customer-code completion fence before terminal GC. */
|
|
293
|
+
export async function readDetachedDaytonaRuntimeCompletion(input: {
|
|
294
|
+
sandboxId: string;
|
|
295
|
+
runtimeCompletedPath: string;
|
|
296
|
+
}): Promise<number | null> {
|
|
297
|
+
try {
|
|
298
|
+
const { clientOptions } = loadDaytonaRequiredConfig();
|
|
299
|
+
const sandbox = (await daytonaSdkClientFactory
|
|
300
|
+
.createFull(clientOptions)
|
|
301
|
+
.get(input.sandboxId)) as DaytonaSandbox;
|
|
302
|
+
const marker = JSON.parse(
|
|
303
|
+
(await sandbox.fs.downloadFile(input.runtimeCompletedPath, 5)).toString(
|
|
304
|
+
'utf-8',
|
|
305
|
+
),
|
|
306
|
+
) as { at?: unknown };
|
|
307
|
+
return typeof marker.at === 'number' && Number.isFinite(marker.at)
|
|
308
|
+
? marker.at
|
|
309
|
+
: null;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.warn(
|
|
312
|
+
'[play-runner.daytona.runtime_completion_marker_unavailable]',
|
|
313
|
+
{
|
|
314
|
+
sandboxId: input.sandboxId,
|
|
315
|
+
// Path is generated per attempt and contains no customer data.
|
|
316
|
+
runtimeCompletedPath: input.runtimeCompletedPath,
|
|
317
|
+
error: error instanceof Error ? error.message : String(error),
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export type DaytonaSandboxDeleteOutcome =
|
|
325
|
+
| {
|
|
326
|
+
kind: 'deleted' | 'already_absent';
|
|
327
|
+
organizationId: string | null;
|
|
328
|
+
}
|
|
329
|
+
| {
|
|
330
|
+
kind: 'timed_out' | 'rate_limited' | 'failed';
|
|
331
|
+
organizationId: string | null;
|
|
332
|
+
code: string;
|
|
333
|
+
detail: string;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
export async function deleteDaytonaSandboxByIdWithOutcome(input: {
|
|
293
337
|
sandboxId: string;
|
|
294
338
|
timeoutSeconds?: number;
|
|
295
|
-
|
|
339
|
+
expectedOrganizationId?: string | null;
|
|
340
|
+
allowUnscopedAlreadyAbsent?: boolean;
|
|
341
|
+
}): Promise<DaytonaSandboxDeleteOutcome> {
|
|
296
342
|
const sandboxId = input.sandboxId?.trim();
|
|
297
|
-
|
|
343
|
+
const expectedOrganizationId = input.expectedOrganizationId?.trim() || null;
|
|
344
|
+
if (!sandboxId) {
|
|
345
|
+
return {
|
|
346
|
+
kind: 'failed',
|
|
347
|
+
organizationId: expectedOrganizationId,
|
|
348
|
+
code: 'invalid_sandbox_id',
|
|
349
|
+
detail: 'Sandbox ID is required.',
|
|
350
|
+
};
|
|
351
|
+
}
|
|
298
352
|
try {
|
|
299
353
|
const { clientOptions } = loadDaytonaRequiredConfig();
|
|
300
354
|
const daytona = daytonaSdkClientFactory.createFull(clientOptions);
|
|
301
355
|
const sandbox = await daytona.get(sandboxId);
|
|
356
|
+
const observedOrganizationId = sandbox.organizationId?.trim() || null;
|
|
357
|
+
if (
|
|
358
|
+
expectedOrganizationId &&
|
|
359
|
+
observedOrganizationId !== expectedOrganizationId
|
|
360
|
+
) {
|
|
361
|
+
return {
|
|
362
|
+
kind: 'failed',
|
|
363
|
+
organizationId: observedOrganizationId,
|
|
364
|
+
code: 'wrong_routing_domain',
|
|
365
|
+
detail: 'Sandbox belongs to a different Daytona organization.',
|
|
366
|
+
};
|
|
367
|
+
}
|
|
302
368
|
await daytona.delete(sandbox, input.timeoutSeconds ?? 30);
|
|
303
|
-
return
|
|
369
|
+
return {
|
|
370
|
+
kind: 'deleted',
|
|
371
|
+
organizationId: observedOrganizationId,
|
|
372
|
+
};
|
|
304
373
|
} catch (error) {
|
|
374
|
+
const failure = describeDaytonaLookupFailure(error);
|
|
375
|
+
// Cleanup is an idempotent "ensure absent" operation. Daytona returning
|
|
376
|
+
// not-found means another cleanup owner already satisfied the obligation.
|
|
377
|
+
// The durable expected organization came from the sandbox returned by the
|
|
378
|
+
// same organization-scoped credential at creation time. When an explicit
|
|
379
|
+
// worker organization is configured it must still match that evidence;
|
|
380
|
+
// deployments which rely only on the provider-returned organization retain
|
|
381
|
+
// that durable creation-domain proof. Legacy eager cleanup retains its
|
|
382
|
+
// previous unscoped behavior through the explicit compatibility option.
|
|
383
|
+
if (failure.httpStatus === 404) {
|
|
384
|
+
const configuredOrganizationId =
|
|
385
|
+
process.env.DAYTONA_ORGANIZATION_ID?.trim() || null;
|
|
386
|
+
if (
|
|
387
|
+
!input.allowUnscopedAlreadyAbsent &&
|
|
388
|
+
(!expectedOrganizationId ||
|
|
389
|
+
(configuredOrganizationId &&
|
|
390
|
+
configuredOrganizationId !== expectedOrganizationId))
|
|
391
|
+
) {
|
|
392
|
+
return {
|
|
393
|
+
kind: 'failed',
|
|
394
|
+
organizationId: configuredOrganizationId,
|
|
395
|
+
code: expectedOrganizationId
|
|
396
|
+
? 'wrong_routing_domain'
|
|
397
|
+
: 'missing_routing_domain',
|
|
398
|
+
detail:
|
|
399
|
+
'Daytona returned not-found without an exact creation-domain match.',
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
console.info('[play-runner.daytona.reclaim_sandbox_already_absent]', {
|
|
403
|
+
sandboxId,
|
|
404
|
+
});
|
|
405
|
+
return {
|
|
406
|
+
kind: 'already_absent',
|
|
407
|
+
organizationId:
|
|
408
|
+
configuredOrganizationId ?? expectedOrganizationId ?? null,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
305
411
|
console.warn('[play-runner.daytona.reclaim_sandbox_delete_failed]', {
|
|
306
412
|
sandboxId,
|
|
307
|
-
|
|
413
|
+
failure,
|
|
308
414
|
});
|
|
309
|
-
|
|
415
|
+
const timedOut =
|
|
416
|
+
failure.httpStatus === 408 ||
|
|
417
|
+
/(?:timeout|timed out|ETIMEDOUT)/i.test(
|
|
418
|
+
`${failure.errorCode ?? ''} ${failure.detail}`,
|
|
419
|
+
);
|
|
420
|
+
return {
|
|
421
|
+
kind:
|
|
422
|
+
failure.httpStatus === 429
|
|
423
|
+
? 'rate_limited'
|
|
424
|
+
: timedOut
|
|
425
|
+
? 'timed_out'
|
|
426
|
+
: 'failed',
|
|
427
|
+
organizationId: expectedOrganizationId,
|
|
428
|
+
code:
|
|
429
|
+
failure.errorCode ??
|
|
430
|
+
(failure.httpStatus ? `http_${failure.httpStatus}` : 'delete_failed'),
|
|
431
|
+
detail: failure.detail,
|
|
432
|
+
};
|
|
310
433
|
}
|
|
311
434
|
}
|
|
312
435
|
|
|
436
|
+
export async function deleteDaytonaSandboxById(input: {
|
|
437
|
+
sandboxId: string;
|
|
438
|
+
timeoutSeconds?: number;
|
|
439
|
+
}): Promise<boolean> {
|
|
440
|
+
const outcome = await deleteDaytonaSandboxByIdWithOutcome({
|
|
441
|
+
...input,
|
|
442
|
+
allowUnscopedAlreadyAbsent: true,
|
|
443
|
+
});
|
|
444
|
+
return outcome.kind === 'deleted' || outcome.kind === 'already_absent';
|
|
445
|
+
}
|
|
446
|
+
|
|
313
447
|
function formatDaytonaExecutionError(error: unknown): string {
|
|
314
448
|
const message = formatDaytonaError(error);
|
|
315
449
|
return isConfiguredDaytonaRuntimeLimit(error)
|
|
@@ -630,6 +764,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
630
764
|
let cancellationCleanupStarted = false;
|
|
631
765
|
let activeAcquiredResource: {
|
|
632
766
|
sandbox: DaytonaSandbox;
|
|
767
|
+
daytonaOrganizationId: string;
|
|
633
768
|
billingStartedAt: number;
|
|
634
769
|
billingEndedAt?: number;
|
|
635
770
|
} | null = null;
|
|
@@ -639,6 +774,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
639
774
|
const runtimeResourceReportErrors = new Set<unknown>();
|
|
640
775
|
const reportRuntimeResource = async (acquired: {
|
|
641
776
|
sandbox: DaytonaSandbox;
|
|
777
|
+
daytonaOrganizationId: string;
|
|
642
778
|
billingStartedAt: number;
|
|
643
779
|
billingEndedAt?: number;
|
|
644
780
|
}) => {
|
|
@@ -658,6 +794,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
658
794
|
process.env.DEEPLINE_RUNTIME_ENVIRONMENT === 'preview'
|
|
659
795
|
? 'preview'
|
|
660
796
|
: 'production',
|
|
797
|
+
daytonaOrganizationId: acquired.daytonaOrganizationId,
|
|
661
798
|
billingStartedAt: acquired.billingStartedAt,
|
|
662
799
|
billingEndedAt,
|
|
663
800
|
cpu: typeof sandbox.cpu === 'number' ? sandbox.cpu : null,
|
|
@@ -673,6 +810,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
673
810
|
};
|
|
674
811
|
const reportRetiringRuntimeResource = async (acquired: {
|
|
675
812
|
sandbox: DaytonaSandbox;
|
|
813
|
+
daytonaOrganizationId: string;
|
|
676
814
|
billingStartedAt: number;
|
|
677
815
|
billingEndedAt?: number;
|
|
678
816
|
}) => {
|
|
@@ -929,6 +1067,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
929
1067
|
cmdId: start.cmdId,
|
|
930
1068
|
outputPath: stagedPayload.outputPath,
|
|
931
1069
|
exitCodePath: stagedPayload.exitCodePath,
|
|
1070
|
+
runtimeCompletedPath: stagedPayload.runtimeCompletedPath,
|
|
932
1071
|
startedAtMs: Date.now(),
|
|
933
1072
|
ceilingMs: DAYTONA_DETACHED_CEILING_SECONDS * 1_000,
|
|
934
1073
|
},
|
|
@@ -988,8 +1127,28 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
|
|
|
988
1127
|
// Resource persistence belongs to the scheduler control plane. Preserve
|
|
989
1128
|
// its typed capacity/fence errors so Absurd can defer or fence the
|
|
990
1129
|
// attempt; converting them into a runner failure would terminally fail a
|
|
991
|
-
// play that never began executing customer code.
|
|
992
|
-
|
|
1130
|
+
// play that never began executing customer code. The sandbox exists
|
|
1131
|
+
// before that durable callback can succeed, so synchronously delete it
|
|
1132
|
+
// before returning the control-plane error. The old fire-and-forget
|
|
1133
|
+
// cleanup path was skipped by this rethrow and leaked the acquisition.
|
|
1134
|
+
if (runtimeResourceReportErrors.has(error)) {
|
|
1135
|
+
const sandbox = sandboxCleanup.currentSandbox();
|
|
1136
|
+
if (sandbox) {
|
|
1137
|
+
try {
|
|
1138
|
+
await sandbox.delete(30);
|
|
1139
|
+
console.info(
|
|
1140
|
+
'[play-runner.daytona.resource_report_failure_cleanup_done]',
|
|
1141
|
+
{ sandboxId: sandbox.id },
|
|
1142
|
+
);
|
|
1143
|
+
} catch (cleanupError) {
|
|
1144
|
+
throw new AggregateError(
|
|
1145
|
+
[error, cleanupError],
|
|
1146
|
+
`Failed to persist or delete acquired Daytona sandbox ${sandbox.id}.`,
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
throw error;
|
|
1151
|
+
}
|
|
993
1152
|
emitDaytonaStage(callbacks, config.context, 'execute:error', {
|
|
994
1153
|
sandboxId: sandboxCleanup.currentSandbox()?.id ?? null,
|
|
995
1154
|
error: formatDaytonaError(error),
|
|
@@ -13,6 +13,8 @@ export type PlayRunnerRuntimeResource = {
|
|
|
13
13
|
kind: 'daytona_sandbox';
|
|
14
14
|
sandboxId: string;
|
|
15
15
|
daytonaEnvironment?: 'preview' | 'production';
|
|
16
|
+
/** Stable, non-secret provider ownership domain returned by Daytona. */
|
|
17
|
+
daytonaOrganizationId?: string;
|
|
16
18
|
billingStartedAt: number;
|
|
17
19
|
billingEndedAt?: number | null;
|
|
18
20
|
terminalReason?: RuntimeResourceTerminalReason | null;
|
|
@@ -39,6 +39,8 @@ export type PlayExecutionSuspension =
|
|
|
39
39
|
* timeout-wake salvage verification. */
|
|
40
40
|
outputPath: string;
|
|
41
41
|
exitCodePath: string;
|
|
42
|
+
/** Exact customer-code completion fence inside the Daytona sandbox. */
|
|
43
|
+
runtimeCompletedPath?: string;
|
|
42
44
|
startedAtMs: number;
|
|
43
45
|
/** Overall run ceiling; the park timeout. */
|
|
44
46
|
ceilingMs: number;
|
|
@@ -6,6 +6,7 @@ export type RuntimeTestFaultName =
|
|
|
6
6
|
| 'receipt_complete_write_fail'
|
|
7
7
|
| 'receipt_fail_write_fail'
|
|
8
8
|
| 'worker_receipt_complete_write_fail'
|
|
9
|
+
| 'invocation_response_delivery_abort'
|
|
9
10
|
/**
|
|
10
11
|
* Holds an already checked-out receipt-gateway scheduler client for the
|
|
11
12
|
* supplied bounded millisecond value. Preview/CI only: this creates a
|
|
@@ -70,6 +71,7 @@ const SUPPORTED_RUNTIME_TEST_FAULTS = new Set<RuntimeTestFaultName>([
|
|
|
70
71
|
'receipt_complete_write_fail',
|
|
71
72
|
'receipt_fail_write_fail',
|
|
72
73
|
'worker_receipt_complete_write_fail',
|
|
74
|
+
'invocation_response_delivery_abort',
|
|
73
75
|
'receipt_gateway_hold_ms',
|
|
74
76
|
]);
|
|
75
77
|
|
|
@@ -21,6 +21,8 @@ export const TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS = 1_000;
|
|
|
21
21
|
export const TOOL_EXECUTE_RETRY_DELAY_MAX_MS = 5_000;
|
|
22
22
|
export const TOOL_EXECUTE_BARE_RATE_LIMIT_BACKPRESSURE_MS = 60_000;
|
|
23
23
|
export const TOOL_EXECUTE_AUTH_SCOPE_CHANGED_CODE = 'AUTH_SCOPE_CHANGED';
|
|
24
|
+
export const TOOL_EXECUTE_CUSTOMER_DB_STORAGE_UNAVAILABLE_CODE =
|
|
25
|
+
'CUSTOMER_DB_STORAGE_UNAVAILABLE';
|
|
24
26
|
/**
|
|
25
27
|
* A provider/action-local outcome contract emits this only when a provider has
|
|
26
28
|
* explicitly said that the same idempotency key is still executing. It is not
|
|
@@ -28,6 +30,9 @@ export const TOOL_EXECUTE_AUTH_SCOPE_CHANGED_CODE = 'AUTH_SCOPE_CHANGED';
|
|
|
28
30
|
*/
|
|
29
31
|
export const TOOL_EXECUTE_IDEMPOTENCY_IN_PROGRESS_CODE =
|
|
30
32
|
'UPSTREAM_IDEMPOTENCY_IN_PROGRESS';
|
|
33
|
+
export const TOOL_EXECUTE_GATEWAY_INVOCATION_IN_PROGRESS_CODE =
|
|
34
|
+
'GATEWAY_INVOCATION_IN_PROGRESS';
|
|
35
|
+
export const TOOL_EXECUTE_GATEWAY_INVOCATION_IN_PROGRESS_MAX_ATTEMPTS = 65;
|
|
31
36
|
|
|
32
37
|
export class ToolExecuteAuthScopeChangedError extends Error {
|
|
33
38
|
readonly code = TOOL_EXECUTE_AUTH_SCOPE_CHANGED_CODE;
|
|
@@ -45,6 +50,8 @@ export type ToolExecuteHttpRetryDecision = {
|
|
|
45
50
|
reason:
|
|
46
51
|
| 'rate_limit'
|
|
47
52
|
| 'idempotency_in_progress'
|
|
53
|
+
| 'gateway_invocation_in_progress'
|
|
54
|
+
| 'customer_db_storage_unavailable'
|
|
48
55
|
| 'retry_safe_transient_5xx'
|
|
49
56
|
| 'unsafe_transient_5xx'
|
|
50
57
|
| 'hard_billing_error'
|
|
@@ -94,6 +101,35 @@ function isIdempotencyInProgressResponse(input: {
|
|
|
94
101
|
);
|
|
95
102
|
}
|
|
96
103
|
|
|
104
|
+
function isGatewayInvocationInProgressResponse(input: {
|
|
105
|
+
status: number;
|
|
106
|
+
bodyText: string;
|
|
107
|
+
}): boolean {
|
|
108
|
+
return (
|
|
109
|
+
input.status === 409 &&
|
|
110
|
+
parseJsonObject(input.bodyText)?.code ===
|
|
111
|
+
TOOL_EXECUTE_GATEWAY_INVOCATION_IN_PROGRESS_CODE
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isCustomerDbStorageUnavailableResponse(input: {
|
|
116
|
+
toolId: string;
|
|
117
|
+
status: number;
|
|
118
|
+
bodyText: string;
|
|
119
|
+
}): boolean {
|
|
120
|
+
if (
|
|
121
|
+
input.status !== 503 ||
|
|
122
|
+
(input.toolId !== 'query_customer_db' &&
|
|
123
|
+
input.toolId !== 'customer_db_query_customer_db')
|
|
124
|
+
) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
return (
|
|
128
|
+
parseJsonObject(input.bodyText)?.code ===
|
|
129
|
+
TOOL_EXECUTE_CUSTOMER_DB_STORAGE_UNAVAILABLE_CODE
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
97
133
|
function idempotencyInProgressRetryDelayMs(attempt: number): number {
|
|
98
134
|
return (
|
|
99
135
|
TOOL_EXECUTE_IDEMPOTENCY_IN_PROGRESS_RETRY_DELAYS_MS[
|
|
@@ -125,6 +161,8 @@ export function parseToolExecuteAuthScopeChangedError(input: {
|
|
|
125
161
|
function decideToolExecuteHttpRetry(input: {
|
|
126
162
|
status: number;
|
|
127
163
|
idempotencyInProgress?: boolean;
|
|
164
|
+
gatewayInvocationInProgress?: boolean;
|
|
165
|
+
customerDbStorageUnavailable?: boolean;
|
|
128
166
|
hardBillingFailure?: boolean;
|
|
129
167
|
hasRetryAfterHeader?: boolean;
|
|
130
168
|
transientHttpRetrySafe?: boolean;
|
|
@@ -151,6 +189,13 @@ function decideToolExecuteHttpRetry(input: {
|
|
|
151
189
|
reason: 'rate_limit',
|
|
152
190
|
};
|
|
153
191
|
}
|
|
192
|
+
if (input.gatewayInvocationInProgress) {
|
|
193
|
+
return {
|
|
194
|
+
retryable: true,
|
|
195
|
+
attemptCap: TOOL_EXECUTE_GATEWAY_INVOCATION_IN_PROGRESS_MAX_ATTEMPTS,
|
|
196
|
+
reason: 'gateway_invocation_in_progress',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
154
199
|
if (input.idempotencyInProgress) {
|
|
155
200
|
return {
|
|
156
201
|
retryable: true,
|
|
@@ -158,6 +203,16 @@ function decideToolExecuteHttpRetry(input: {
|
|
|
158
203
|
reason: 'idempotency_in_progress',
|
|
159
204
|
};
|
|
160
205
|
}
|
|
206
|
+
// This typed response is emitted for a PostgreSQL permission denial. The
|
|
207
|
+
// failed statement cannot commit, so it is safe to retry even when the SQL
|
|
208
|
+
// itself is a mutation. Do not generalize this to arbitrary 503 responses.
|
|
209
|
+
if (input.customerDbStorageUnavailable) {
|
|
210
|
+
return {
|
|
211
|
+
retryable: true,
|
|
212
|
+
attemptCap: TOOL_EXECUTE_TRANSIENT_HTTP_MAX_ATTEMPTS,
|
|
213
|
+
reason: 'customer_db_storage_unavailable',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
161
216
|
if (input.status >= 500 && input.status < 600) {
|
|
162
217
|
if (!input.transientHttpRetrySafe) {
|
|
163
218
|
return {
|
|
@@ -186,6 +241,8 @@ export function createToolExecuteHttpFailureAttemptTracker(): ToolExecuteHttpFai
|
|
|
186
241
|
> = {
|
|
187
242
|
rate_limit: 0,
|
|
188
243
|
idempotency_in_progress: 0,
|
|
244
|
+
gateway_invocation_in_progress: 0,
|
|
245
|
+
customer_db_storage_unavailable: 0,
|
|
189
246
|
retry_safe_transient_5xx: 0,
|
|
190
247
|
unsafe_transient_5xx: 0,
|
|
191
248
|
hard_billing_error: 0,
|
|
@@ -200,6 +257,15 @@ export function createToolExecuteHttpFailureAttemptTracker(): ToolExecuteHttpFai
|
|
|
200
257
|
status: input.status,
|
|
201
258
|
bodyText: input.bodyText ?? '',
|
|
202
259
|
}),
|
|
260
|
+
gatewayInvocationInProgress: isGatewayInvocationInProgressResponse({
|
|
261
|
+
status: input.status,
|
|
262
|
+
bodyText: input.bodyText ?? '',
|
|
263
|
+
}),
|
|
264
|
+
customerDbStorageUnavailable: isCustomerDbStorageUnavailableResponse({
|
|
265
|
+
toolId: input.toolId,
|
|
266
|
+
status: input.status,
|
|
267
|
+
bodyText: input.bodyText ?? '',
|
|
268
|
+
}),
|
|
203
269
|
hasRetryAfterHeader: true,
|
|
204
270
|
transientHttpRetrySafe: input.transientHttpRetrySafe === true,
|
|
205
271
|
});
|
|
@@ -240,9 +306,15 @@ export function classifyToolExecuteHttpFailure(input: {
|
|
|
240
306
|
typeof input.retryAfterHeader === 'string' &&
|
|
241
307
|
input.retryAfterHeader.trim().length > 0;
|
|
242
308
|
const idempotencyInProgress = isIdempotencyInProgressResponse(input);
|
|
309
|
+
const gatewayInvocationInProgress =
|
|
310
|
+
isGatewayInvocationInProgressResponse(input);
|
|
311
|
+
const customerDbStorageUnavailable =
|
|
312
|
+
isCustomerDbStorageUnavailableResponse(input);
|
|
243
313
|
const initialRetryDecision = decideToolExecuteHttpRetry({
|
|
244
314
|
status: input.status,
|
|
245
315
|
idempotencyInProgress,
|
|
316
|
+
gatewayInvocationInProgress,
|
|
317
|
+
customerDbStorageUnavailable,
|
|
246
318
|
hasRetryAfterHeader,
|
|
247
319
|
transientHttpRetrySafe,
|
|
248
320
|
});
|
|
@@ -264,6 +336,8 @@ export function classifyToolExecuteHttpFailure(input: {
|
|
|
264
336
|
const retryDecision = decideToolExecuteHttpRetry({
|
|
265
337
|
status: input.status,
|
|
266
338
|
idempotencyInProgress,
|
|
339
|
+
gatewayInvocationInProgress,
|
|
340
|
+
customerDbStorageUnavailable,
|
|
267
341
|
hardBillingFailure,
|
|
268
342
|
hasRetryAfterHeader,
|
|
269
343
|
transientHttpRetrySafe,
|
|
@@ -295,6 +369,9 @@ export function classifyToolExecuteHttpFailure(input: {
|
|
|
295
369
|
),
|
|
296
370
|
)
|
|
297
371
|
: retryDecision.reason === 'idempotency_in_progress' &&
|
|
372
|
+
!hasRetryAfterHeader
|
|
373
|
+
? idempotencyInProgressRetryDelayMs(input.attempt)
|
|
374
|
+
: retryDecision.reason === 'gateway_invocation_in_progress' &&
|
|
298
375
|
!hasRetryAfterHeader
|
|
299
376
|
? idempotencyInProgressRetryDelayMs(input.attempt)
|
|
300
377
|
: retryAfterMs > 0
|
|
@@ -317,6 +394,8 @@ export function classifyToolExecuteHttpFailure(input: {
|
|
|
317
394
|
? retryAfterMs
|
|
318
395
|
: TOOL_EXECUTE_BARE_RATE_LIMIT_BACKPRESSURE_MS
|
|
319
396
|
: null,
|
|
320
|
-
chargeRetryBudget:
|
|
397
|
+
chargeRetryBudget:
|
|
398
|
+
shouldRetry &&
|
|
399
|
+
retryDecision.reason !== 'gateway_invocation_in_progress',
|
|
321
400
|
};
|
|
322
401
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -718,7 +718,7 @@ var SDK_RELEASE = {
|
|
|
718
718
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
719
719
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
720
720
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
721
|
-
version: "0.1.
|
|
721
|
+
version: "0.1.289",
|
|
722
722
|
contracts: {
|
|
723
723
|
api: {
|
|
724
724
|
name: "sdk-http-api",
|
package/dist/cli/index.mjs
CHANGED
|
@@ -703,7 +703,7 @@ var SDK_RELEASE = {
|
|
|
703
703
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
704
704
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
705
705
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
706
|
-
version: "0.1.
|
|
706
|
+
version: "0.1.289",
|
|
707
707
|
contracts: {
|
|
708
708
|
api: {
|
|
709
709
|
name: "sdk-http-api",
|
package/dist/index.js
CHANGED
|
@@ -438,7 +438,7 @@ var SDK_RELEASE = {
|
|
|
438
438
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
439
439
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
440
440
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
441
|
-
version: "0.1.
|
|
441
|
+
version: "0.1.289",
|
|
442
442
|
contracts: {
|
|
443
443
|
api: {
|
|
444
444
|
name: "sdk-http-api",
|
package/dist/index.mjs
CHANGED
|
@@ -367,7 +367,7 @@ var SDK_RELEASE = {
|
|
|
367
367
|
// 0.1.253 makes play-page browser opening opt-in and retires --no-open.
|
|
368
368
|
// 0.1.254 removes the internal operations tree from the published SDK CLI.
|
|
369
369
|
// Operators use the checkout-local deepline-admin binary instead.
|
|
370
|
-
version: "0.1.
|
|
370
|
+
version: "0.1.289",
|
|
371
371
|
contracts: {
|
|
372
372
|
api: {
|
|
373
373
|
name: "sdk-http-api",
|