deepline 0.1.209 → 0.1.211

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.
Files changed (24) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +0 -2
  2. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +155 -139
  3. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-chunk-plan.ts +15 -0
  4. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/map-latency-profile.ts +90 -0
  5. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-dispatch.ts +173 -103
  6. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/tool-receipts.ts +25 -92
  7. package/dist/bundling-sources/sdk/src/play.ts +4 -2
  8. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  9. package/dist/bundling-sources/shared_libs/play-runtime/child-run-id.ts +4 -5
  10. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +348 -109
  11. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +1 -2
  12. package/dist/bundling-sources/shared_libs/play-runtime/durable-call-cache.ts +2 -4
  13. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +42 -3
  14. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +5 -1
  15. package/dist/bundling-sources/shared_libs/play-runtime/single-flight.ts +31 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +12 -1
  17. package/dist/bundling-sources/shared_libs/plays/dataset.ts +41 -5
  18. package/dist/cli/index.js +94 -16
  19. package/dist/cli/index.mjs +94 -16
  20. package/dist/index.d.mts +18 -3
  21. package/dist/index.d.ts +18 -3
  22. package/dist/index.js +22 -5
  23. package/dist/index.mjs +22 -5
  24. package/package.json +1 -1
@@ -54,6 +54,7 @@ import {
54
54
  markWorkerToolReceiptResultCached,
55
55
  markWorkerToolReceiptResultExecution,
56
56
  planWorkerToolReceiptGroups,
57
+ resolveRunningReceiptWaitMaxAttempts,
57
58
  resolveWorkerToolReceiptGroupWaitMaxAttempts,
58
59
  resolveWorkerToolRuntimeTimeoutMs,
59
60
  } from './tool-receipts';
@@ -1232,10 +1233,17 @@ export class WorkerToolBatchScheduler {
1232
1233
  try {
1233
1234
  const result = await this.waitForDurableToolReceipt({
1234
1235
  receiptKey,
1235
- maxAttempts: resolveWorkerToolReceiptGroupWaitMaxAttempts(
1236
- group,
1237
- (request) => request.receiptWaitMaxAttempts,
1238
- ),
1236
+ maxAttempts: resolveRunningReceiptWaitMaxAttempts({
1237
+ ownerRunId: claim.receipt.runId,
1238
+ currentRunId: this.options.req.runId,
1239
+ updatedAt: claim.receipt.updatedAt,
1240
+ requestedMaxAttempts:
1241
+ resolveWorkerToolReceiptGroupWaitMaxAttempts(
1242
+ group,
1243
+ (request) => request.receiptWaitMaxAttempts,
1244
+ ),
1245
+ nowMs: this.options.nowMs(),
1246
+ }),
1239
1247
  });
1240
1248
  if (groupState.forceDurableRefresh) {
1241
1249
  return await this.claimForcedDurableToolReceiptGroupAfterWait(
@@ -1992,8 +2000,14 @@ export class WorkerToolBatchScheduler {
1992
2000
  toolId: string,
1993
2001
  requests: WorkerToolBatchRequest[],
1994
2002
  ): Promise<void> {
2003
+ const receiptPrepareStartedAt = this.options.nowMs();
1995
2004
  const { claimedRequests, deferredClaimedRequests } =
1996
2005
  await this.prepareDurableToolRequests(requests);
2006
+ this.options.recordPerfTrace?.({
2007
+ phase: 'runner.tool.receipt_prepare',
2008
+ ms: this.options.nowMs() - receiptPrepareStartedAt,
2009
+ extra: { toolId, requests: requests.length },
2010
+ });
1997
2011
  await this.executeClaimedToolRequests(
1998
2012
  toolId,
1999
2013
  requests.length,
@@ -2063,6 +2077,7 @@ export class WorkerToolBatchScheduler {
2063
2077
  // charges tool budget, holds a global tool-concurrency slot, and
2064
2078
  // applies per-(org,provider) pacing before the call runs.
2065
2079
  let slot: { release: () => void };
2080
+ const admissionStartedAt = this.options.nowMs();
2066
2081
  try {
2067
2082
  slot = await this.options.governor.acquireToolSlot(toolId, {
2068
2083
  signal: this.options.abortSignal,
@@ -2074,6 +2089,11 @@ export class WorkerToolBatchScheduler {
2074
2089
  );
2075
2090
  return;
2076
2091
  }
2092
+ this.options.recordPerfTrace?.({
2093
+ phase: 'runner.tool.admission',
2094
+ ms: this.options.nowMs() - admissionStartedAt,
2095
+ extra: { toolId, requests: 1 },
2096
+ });
2077
2097
  try {
2078
2098
  let result: unknown;
2079
2099
  try {
@@ -2092,59 +2112,68 @@ export class WorkerToolBatchScheduler {
2092
2112
  claimed,
2093
2113
  })
2094
2114
  : null;
2095
- result = await executeWithWorkerReceiptHeartbeats({
2096
- playName: this.options.req.playName,
2097
- runId: this.options.req.runId,
2098
- runAttempt: this.options.req.runAttempt ?? 0,
2099
- ...(this.options.receiptStore
2100
- ? { receiptStore: this.options.receiptStore }
2101
- : {}),
2102
- budgetMeter: this.options.budgetMeter,
2103
- heartbeatIntervalMs:
2104
- this.options.runtimeReceiptHeartbeatIntervalMs,
2105
- targets: claimed.receiptKey
2106
- ? [
2107
- {
2108
- key: claimed.receiptKey,
2109
- leaseId: claimed.receiptLeaseId,
2110
- },
2111
- ]
2112
- : [],
2113
- execute: () =>
2114
- this.options.executeTool({
2115
- id: request.id,
2116
- toolId,
2117
- input: request.input,
2118
- workflowStep: request.workflowStep,
2119
- callbacks: this.options.callbacks,
2120
- onProviderBackpressure: (retryAfterMs) =>
2121
- this.reportBackpressure(toolId, retryAfterMs),
2122
- onRetryAttempt: () =>
2123
- this.options.governor.chargeBudget('retry'),
2124
- transientHttpRetrySafe:
2125
- toolContract?.retrySafeTransientHttp === true,
2126
- ...(durableReceiptKey
2127
- ? {
2128
- durableCallReceiptKey: durableReceiptKey,
2129
- executionAuthScopeDigest:
2130
- request.executionAuthScopeDigest,
2131
- providerIdempotencyKey:
2132
- providerIdempotencyKeyForClaimed({
2133
- req: this.options.req,
2134
- claimed,
2135
- }),
2136
- }
2137
- : {}),
2138
- runtimeTimeoutMs: resolveWorkerToolRuntimeTimeoutMs(
2139
- [claimed],
2140
- {
2141
- resolveOwnerTimeoutMs: (request) =>
2142
- request.runtimeTimeoutMs,
2143
- },
2144
- ),
2145
- directOptions: request.directOptions,
2146
- }),
2147
- });
2115
+ const providerStartedAt = this.options.nowMs();
2116
+ try {
2117
+ result = await executeWithWorkerReceiptHeartbeats({
2118
+ playName: this.options.req.playName,
2119
+ runId: this.options.req.runId,
2120
+ runAttempt: this.options.req.runAttempt ?? 0,
2121
+ ...(this.options.receiptStore
2122
+ ? { receiptStore: this.options.receiptStore }
2123
+ : {}),
2124
+ budgetMeter: this.options.budgetMeter,
2125
+ heartbeatIntervalMs:
2126
+ this.options.runtimeReceiptHeartbeatIntervalMs,
2127
+ targets: claimed.receiptKey
2128
+ ? [
2129
+ {
2130
+ key: claimed.receiptKey,
2131
+ leaseId: claimed.receiptLeaseId,
2132
+ },
2133
+ ]
2134
+ : [],
2135
+ execute: () =>
2136
+ this.options.executeTool({
2137
+ id: request.id,
2138
+ toolId,
2139
+ input: request.input,
2140
+ workflowStep: request.workflowStep,
2141
+ callbacks: this.options.callbacks,
2142
+ onProviderBackpressure: (retryAfterMs) =>
2143
+ this.reportBackpressure(toolId, retryAfterMs),
2144
+ onRetryAttempt: () =>
2145
+ this.options.governor.chargeBudget('retry'),
2146
+ transientHttpRetrySafe:
2147
+ toolContract?.retrySafeTransientHttp === true,
2148
+ ...(durableReceiptKey
2149
+ ? {
2150
+ durableCallReceiptKey: durableReceiptKey,
2151
+ executionAuthScopeDigest:
2152
+ request.executionAuthScopeDigest,
2153
+ providerIdempotencyKey:
2154
+ providerIdempotencyKeyForClaimed({
2155
+ req: this.options.req,
2156
+ claimed,
2157
+ }),
2158
+ }
2159
+ : {}),
2160
+ runtimeTimeoutMs: resolveWorkerToolRuntimeTimeoutMs(
2161
+ [claimed],
2162
+ {
2163
+ resolveOwnerTimeoutMs: (request) =>
2164
+ request.runtimeTimeoutMs,
2165
+ },
2166
+ ),
2167
+ directOptions: request.directOptions,
2168
+ }),
2169
+ });
2170
+ } finally {
2171
+ this.options.recordPerfTrace?.({
2172
+ phase: 'runner.tool.provider',
2173
+ ms: this.options.nowMs() - providerStartedAt,
2174
+ extra: { toolId, requests: 1 },
2175
+ });
2176
+ }
2148
2177
  } catch (error) {
2149
2178
  if (error instanceof RuntimeLeaseLostError) {
2150
2179
  this.rejectRequests(claimed, error);
@@ -2165,10 +2194,16 @@ export class WorkerToolBatchScheduler {
2165
2194
  return;
2166
2195
  }
2167
2196
  try {
2197
+ const receiptCompleteStartedAt = this.options.nowMs();
2168
2198
  this.settleRequests(
2169
2199
  claimed,
2170
2200
  await this.completeDurableToolRequest(claimed, result),
2171
2201
  );
2202
+ this.options.recordPerfTrace?.({
2203
+ phase: 'runner.tool.receipt_complete',
2204
+ ms: this.options.nowMs() - receiptCompleteStartedAt,
2205
+ extra: { toolId, requests: 1 },
2206
+ });
2172
2207
  } catch (receiptError) {
2173
2208
  this.rejectRequests(claimed, receiptError);
2174
2209
  }
@@ -2253,12 +2288,22 @@ export class WorkerToolBatchScheduler {
2253
2288
  }
2254
2289
  // One provider call per batch -> one tool slot (budget + global
2255
2290
  // concurrency + per-(org,provider) pacing) around the whole batch.
2291
+ const admissionStartedAt = this.options.nowMs();
2256
2292
  const slot = await this.options.governor.acquireToolSlot(
2257
2293
  batch.batchOperation,
2258
2294
  {
2259
2295
  signal: this.options.abortSignal,
2260
2296
  },
2261
2297
  );
2298
+ this.options.recordPerfTrace?.({
2299
+ phase: 'runner.tool.admission',
2300
+ ms: this.options.nowMs() - admissionStartedAt,
2301
+ extra: {
2302
+ toolId: batch.batchOperation,
2303
+ requests: batch.memberRequests.length,
2304
+ batched: true,
2305
+ },
2306
+ });
2262
2307
  try {
2263
2308
  await this.markAdmittedToolRequestsRunning(batch.memberRequests);
2264
2309
  this.options.budgetMeter?.count('provider');
@@ -2273,54 +2318,69 @@ export class WorkerToolBatchScheduler {
2273
2318
  batchOperation: batch.batchOperation,
2274
2319
  receiptKeys,
2275
2320
  });
2276
- return await executeWithWorkerReceiptHeartbeats({
2277
- playName: this.options.req.playName,
2278
- runId: this.options.req.runId,
2279
- runAttempt: this.options.req.runAttempt ?? 0,
2280
- ...(this.options.receiptStore
2281
- ? { receiptStore: this.options.receiptStore }
2282
- : {}),
2283
- budgetMeter: this.options.budgetMeter,
2284
- heartbeatIntervalMs: this.options.runtimeReceiptHeartbeatIntervalMs,
2285
- targets: batch.memberRequests.map((member) => ({
2286
- key: member.receiptKey,
2287
- leaseId: member.receiptLeaseId,
2288
- })),
2289
- execute: () =>
2290
- this.options.executeTool({
2291
- id: `batch:${batch.memberRequests.map((request) => request.request.id).join('|')}`,
2292
- toolId: batch.batchOperation,
2293
- input: batch.batchPayload,
2294
- callbacks: this.options.callbacks,
2295
- onProviderBackpressure: (retryAfterMs) =>
2296
- this.reportBackpressure(input.sourceToolId, retryAfterMs),
2297
- onRetryAttempt: () =>
2298
- this.options.governor.chargeBudget('retry'),
2299
- transientHttpRetrySafe:
2300
- toolContract?.retrySafeTransientHttp === true,
2301
- durableCallReceiptKey: aggregateReceiptKey,
2302
- executionAuthScopeDigest:
2303
- batch.memberRequests[0]?.request.executionAuthScopeDigest ??
2304
- null,
2305
- providerIdempotencyKey: buildBatchProviderIdempotencyKey({
2306
- aggregateReceiptKey,
2307
- receiptKeys,
2308
- providerIdempotencyKeys: batch.memberRequests.map((member) =>
2309
- providerIdempotencyKeyForClaimed({
2310
- req: this.options.req,
2311
- claimed: member,
2312
- }),
2321
+ const providerStartedAt = this.options.nowMs();
2322
+ try {
2323
+ return await executeWithWorkerReceiptHeartbeats({
2324
+ playName: this.options.req.playName,
2325
+ runId: this.options.req.runId,
2326
+ runAttempt: this.options.req.runAttempt ?? 0,
2327
+ ...(this.options.receiptStore
2328
+ ? { receiptStore: this.options.receiptStore }
2329
+ : {}),
2330
+ budgetMeter: this.options.budgetMeter,
2331
+ heartbeatIntervalMs:
2332
+ this.options.runtimeReceiptHeartbeatIntervalMs,
2333
+ targets: batch.memberRequests.map((member) => ({
2334
+ key: member.receiptKey,
2335
+ leaseId: member.receiptLeaseId,
2336
+ })),
2337
+ execute: () =>
2338
+ this.options.executeTool({
2339
+ id: `batch:${batch.memberRequests.map((request) => request.request.id).join('|')}`,
2340
+ toolId: batch.batchOperation,
2341
+ input: batch.batchPayload,
2342
+ callbacks: this.options.callbacks,
2343
+ onProviderBackpressure: (retryAfterMs) =>
2344
+ this.reportBackpressure(input.sourceToolId, retryAfterMs),
2345
+ onRetryAttempt: () =>
2346
+ this.options.governor.chargeBudget('retry'),
2347
+ transientHttpRetrySafe:
2348
+ toolContract?.retrySafeTransientHttp === true,
2349
+ durableCallReceiptKey: aggregateReceiptKey,
2350
+ executionAuthScopeDigest:
2351
+ batch.memberRequests[0]?.request.executionAuthScopeDigest ??
2352
+ null,
2353
+ providerIdempotencyKey: buildBatchProviderIdempotencyKey({
2354
+ aggregateReceiptKey,
2355
+ receiptKeys,
2356
+ providerIdempotencyKeys: batch.memberRequests.map(
2357
+ (member) =>
2358
+ providerIdempotencyKeyForClaimed({
2359
+ req: this.options.req,
2360
+ claimed: member,
2361
+ }),
2362
+ ),
2363
+ }),
2364
+ runtimeTimeoutMs: resolveWorkerToolRuntimeTimeoutMs(
2365
+ batch.memberRequests,
2366
+ {
2367
+ resolveOwnerTimeoutMs: (request) =>
2368
+ request.runtimeTimeoutMs,
2369
+ },
2313
2370
  ),
2314
2371
  }),
2315
- runtimeTimeoutMs: resolveWorkerToolRuntimeTimeoutMs(
2316
- batch.memberRequests,
2317
- {
2318
- resolveOwnerTimeoutMs: (request) =>
2319
- request.runtimeTimeoutMs,
2320
- },
2321
- ),
2322
- }),
2323
- });
2372
+ });
2373
+ } finally {
2374
+ this.options.recordPerfTrace?.({
2375
+ phase: 'runner.tool.provider',
2376
+ ms: this.options.nowMs() - providerStartedAt,
2377
+ extra: {
2378
+ toolId: batch.batchOperation,
2379
+ requests: batch.memberRequests.length,
2380
+ batched: true,
2381
+ },
2382
+ });
2383
+ }
2324
2384
  } finally {
2325
2385
  slot.release();
2326
2386
  }
@@ -2381,6 +2441,7 @@ export class WorkerToolBatchScheduler {
2381
2441
  : entry.request.memberRequests.map(() => null);
2382
2442
  let completedResults: WorkerToolCompletionResult[];
2383
2443
  try {
2444
+ const receiptCompleteStartedAt = this.options.nowMs();
2384
2445
  completedResults = await this.completeDurableToolRequests(
2385
2446
  entry.request.memberRequests.map((claimed, index) => ({
2386
2447
  claimed,
@@ -2391,6 +2452,15 @@ export class WorkerToolBatchScheduler {
2391
2452
  ),
2392
2453
  })),
2393
2454
  );
2455
+ this.options.recordPerfTrace?.({
2456
+ phase: 'runner.tool.receipt_complete',
2457
+ ms: this.options.nowMs() - receiptCompleteStartedAt,
2458
+ extra: {
2459
+ toolId: entry.request.batchOperation,
2460
+ requests: entry.request.memberRequests.length,
2461
+ batched: true,
2462
+ },
2463
+ });
2394
2464
  } catch (receiptError) {
2395
2465
  for (const claimed of entry.request.memberRequests) {
2396
2466
  this.rejectRequests(claimed, receiptError);
@@ -39,106 +39,39 @@ export function canReclaimTimedOutWorkerToolReceipt(input: {
39
39
  * conservative constant, not a heartbeat multiple.
40
40
  */
41
41
  export const RUNNING_RECEIPT_DEAD_OWNER_STALENESS_MS = 60_000;
42
+ export const RUNNING_RECEIPT_POLL_INTERVAL_MS = 250;
42
43
 
43
- /**
44
- * Decide whether a `running` receipt should be reclaimed IMMEDIATELY, skipping
45
- * the completion poll.
46
- *
47
- * The poll (`waitForCompletedRuntimeReceipt`) issues one service-binding
48
- * subrequest per 250ms tick, up to the receipt's wait budget (~1320 ticks for a
49
- * 5.5-minute default). On resume, a map can carry many receipts left `running`
50
- * by a DEAD prior run; polling each one burns its full tick budget waiting on a
51
- * receipt no live worker will complete, which blows Cloudflare's ~1000
52
- * subrequests-per-invocation budget and kills the resume with
53
- * "Too many subrequests by single Worker invocation" (prod resume deaths
54
- * 20260703t230209 / 20260703t232104).
55
- *
56
- * A receipt is a dead-owner orphan when it is owned by a DIFFERENT run (a
57
- * same-run `running` is this run replaying — its owner may still be live) and
58
- * its last write is stale (or missing entirely — a live owner would carry a
59
- * recent claim timestamp). Those reclaim immediately, matching the dispatch
60
- * code's own contract ("on resume it reclaims and re-executes"). A live or
61
- * freshly-claimed owner keeps the poll.
62
- */
63
- export function shouldReclaimRunningReceiptWithoutPolling(input: {
44
+ /** Bound a follower poll by when a different-run owner becomes reclaimable. */
45
+ export function resolveRunningReceiptWaitMaxAttempts(input: {
64
46
  ownerRunId?: string | null;
65
47
  currentRunId: string;
66
48
  updatedAt?: string | null;
49
+ requestedMaxAttempts: number;
67
50
  nowMs?: number;
68
51
  stalenessFloorMs?: number;
69
- }): boolean {
70
- const currentRunId = input.currentRunId.trim();
52
+ pollIntervalMs?: number;
53
+ }): number {
54
+ const requested = Math.max(1, Math.floor(input.requestedMaxAttempts));
71
55
  const ownerRunId = input.ownerRunId?.trim() ?? '';
72
- // Only a receipt owned by a different run can be a dead prior-run orphan.
73
- if (!currentRunId || !ownerRunId || ownerRunId === currentRunId) return false;
74
- const floorMs =
75
- input.stalenessFloorMs ?? RUNNING_RECEIPT_DEAD_OWNER_STALENESS_MS;
76
- // No freshness signal at all: treat as stale. A live owner would carry a
77
- // recent claim timestamp; an orphan from a dead run may carry none.
78
- if (!input.updatedAt) return true;
79
- const updatedAtMs = Date.parse(input.updatedAt);
80
- if (!Number.isFinite(updatedAtMs)) return true;
81
- const now = input.nowMs ?? Date.now();
82
- return now - updatedAtMs >= floorMs;
83
- }
84
-
85
- /** Metadata a `running` receipt carries about its owner and last write. */
86
- export interface RunningReceiptOwnershipInput {
87
- ownerRunId?: string | null;
88
- currentRunId: string;
89
- updatedAt?: string | null;
90
- nowMs?: number;
91
- stalenessFloorMs?: number;
92
- }
93
-
94
- /** Outcome of polling a `running` receipt for completion. */
95
- export type RunningReceiptPollOutcome =
96
- /** The poll saw the receipt complete and already resolved the group. */
97
- | { kind: 'completed' }
98
- /** The poll timed out; the receipt is still `running`. */
99
- | { kind: 'timed_out'; error: unknown }
100
- /** The poll failed for a non-timeout reason (e.g. the receipt failed). */
101
- | { kind: 'errored'; error: unknown };
102
-
103
- /**
104
- * Orchestrate the resume-time handling of a `running` receipt: reclaim it
105
- * immediately when it is a dead-owner orphan, otherwise poll for completion and
106
- * only reclaim after the poll times out. Kept as a pure, dependency-injected
107
- * function (the poll and reclaim side effects are passed in) so the
108
- * "skip the poll for a dead owner" decision is unit-testable with a poll counter
109
- * — the Worker scheduler that owns the real effects imports `cloudflare:workers`
110
- * and cannot be loaded under vitest.
111
- *
112
- * Behavior (must match the scheduler's prior inline logic exactly):
113
- * - dead-owner + reclaim-eligible → `reclaim` now, never polling;
114
- * - otherwise `poll`; on `completed` the group is already resolved (return []);
115
- * on a non-timeout error `reject` and stop; on timeout, `reclaim` if still
116
- * eligible else `reject` with the timeout error.
117
- */
118
- export async function reclaimRunningReceiptGroupAfterWait<TClaimed>(input: {
119
- ownership: RunningReceiptOwnershipInput;
120
- timeoutError: unknown;
121
- poll: () => Promise<RunningReceiptPollOutcome>;
122
- reclaim: (rejectionOnFallthrough: unknown) => Promise<TClaimed[]>;
123
- reject: (error: unknown) => void;
124
- }): Promise<TClaimed[]> {
125
- if (
126
- shouldReclaimRunningReceiptWithoutPolling(input.ownership) &&
127
- canReclaimTimedOutWorkerToolReceipt(input.ownership)
128
- ) {
129
- return await input.reclaim(input.timeoutError);
130
- }
131
- const outcome = await input.poll();
132
- if (outcome.kind === 'completed') return [];
133
- if (outcome.kind === 'errored') {
134
- input.reject(outcome.error);
135
- return [];
136
- }
137
- if (!canReclaimTimedOutWorkerToolReceipt(input.ownership)) {
138
- input.reject(outcome.error);
139
- return [];
56
+ const currentRunId = input.currentRunId.trim();
57
+ if (!ownerRunId || !currentRunId || ownerRunId === currentRunId) {
58
+ return requested;
140
59
  }
141
- return await input.reclaim(outcome.error);
60
+ const updatedAtMs = input.updatedAt ? Date.parse(input.updatedAt) : NaN;
61
+ if (!Number.isFinite(updatedAtMs)) return 1;
62
+ const remainingMs = Math.max(
63
+ 0,
64
+ updatedAtMs +
65
+ (input.stalenessFloorMs ?? RUNNING_RECEIPT_DEAD_OWNER_STALENESS_MS) -
66
+ (input.nowMs ?? Date.now()),
67
+ );
68
+ const attemptsUntilReclaim = Math.max(
69
+ 1,
70
+ Math.floor(
71
+ remainingMs / (input.pollIntervalMs ?? RUNNING_RECEIPT_POLL_INTERVAL_MS),
72
+ ) + 1,
73
+ );
74
+ return Math.min(requested, attemptsUntilReclaim);
142
75
  }
143
76
 
144
77
  export function canReclaimFailedWorkerToolReceipt(input: {
@@ -173,6 +173,8 @@ export type PlayBindings = {
173
173
  * older clients can continue to register revisions during the migration.
174
174
  */
175
175
  description?: string;
176
+ /** Allow compilers to bundle this named handler directly without a child run. */
177
+ inline?: boolean;
176
178
  /** Optional per-run billing controls enforced by the runtime. */
177
179
  billing?: {
178
180
  /** Stop the run before a billed action would push total run credits above this cap. */
@@ -400,7 +402,7 @@ export type StepOptions<Row, Value = unknown> = {
400
402
  * Legacy dataset-column recompute flag accepted for older authored plays.
401
403
  *
402
404
  * Prefer putting freshness on the actual reusable call
403
- * (`ctx.tools.execute`, `ctx.step`, `ctx.fetch`, or `ctx.runPlay`).
405
+ * (`ctx.tools.execute`, `ctx.step`, or `ctx.fetch`).
404
406
  */
405
407
  readonly recompute?: boolean;
406
408
  /** Legacy error-recompute flag accepted for older authored plays. */
@@ -889,7 +891,7 @@ export interface DeeplinePlayRuntimeContext {
889
891
  key: string,
890
892
  playRef: string | PlayReferenceLike,
891
893
  input: Record<string, unknown>,
892
- options: { description: string; staleAfterSeconds?: number },
894
+ options: { description: string },
893
895
  ): Promise<TOutput>;
894
896
 
895
897
  /**
@@ -106,10 +106,10 @@ export const SDK_RELEASE = {
106
106
  // 0.1.154 removes the short-lived generated enrich StepOptions recompute
107
107
  // fields shipped in 0.1.153.
108
108
  // 0.1.175 ships loud `deepline enrich` empty-waterfall reporting.
109
- version: '0.1.209',
109
+ version: '0.1.211',
110
110
  apiContract: '2026-06-dataset-handle-results-hard-cutover',
111
111
  supportPolicy: {
112
- latest: '0.1.209',
112
+ latest: '0.1.211',
113
113
  minimumSupported: '0.1.53',
114
114
  deprecatedBelow: '0.1.53',
115
115
  commandMinimumSupported: [
@@ -44,8 +44,7 @@ export interface ChildRunIdInputs {
44
44
  key?: string | null;
45
45
  /**
46
46
  * Canonical durable runPlay semantic key when the launching substrate has one.
47
- * In-process runtimes pass the same key used by durable receipts so row-scoped
48
- * receipt identity and child run identity cannot drift apart.
47
+ * This scopes each fresh child invocation to its parent call site and row.
49
48
  */
50
49
  runPlaySemanticKey?: string | null;
51
50
  /** Child input payload (semantic-fallback input). */
@@ -57,9 +56,9 @@ export interface ChildRunIdInputs {
57
56
  }
58
57
 
59
58
  /**
60
- * A failed durable runPlay receipt must launch a fresh child attempt while
61
- * ordinary submit retries keep attaching to the original child. The receipt
62
- * lease is the attempt fence, so it is the only extra identity required.
59
+ * A failed child launch must use a fresh attempt identity while ordinary
60
+ * transport retries keep attaching to the original child. The caller-provided
61
+ * lease is the attempt fence.
63
62
  */
64
63
  export function childSubmitIdempotencyKey(input: {
65
64
  receiptKey: string;