deepline 0.3.87 → 0.3.89

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 (69) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +532 -2
  2. package/dist/bundling-sources/sdk/src/config.ts +77 -0
  3. package/dist/bundling-sources/sdk/src/http.ts +76 -30
  4. package/dist/bundling-sources/sdk/src/index.ts +6 -0
  5. package/dist/bundling-sources/sdk/src/play.ts +15 -1
  6. package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
  7. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  8. package/dist/bundling-sources/sdk/src/types.ts +83 -4
  9. package/dist/bundling-sources/shared_libs/monitors/deactivation-reasons.ts +16 -0
  10. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +263 -0
  11. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-identity-claims.ts +9 -1
  12. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-state.ts +122 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/activity-observation.ts +6 -11
  14. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +90 -12
  15. package/dist/bundling-sources/shared_libs/play-runtime/async-play-invocation.ts +78 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/checkpoint-boundaries.ts +70 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +407 -53
  18. package/dist/bundling-sources/shared_libs/play-runtime/coordinator-headers.ts +1 -32
  19. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +57 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/default-batch-strategies.ts +20 -1
  21. package/dist/bundling-sources/shared_libs/play-runtime/execution-capabilities.ts +6 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/execution-protocol.ts +7 -1
  23. package/dist/bundling-sources/shared_libs/play-runtime/lane-retirement-gate.ts +189 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-semantics.ts +28 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +2 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +23 -0
  27. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +1 -1
  28. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +18 -4
  29. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-start-admission.ts +68 -0
  30. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +2 -0
  31. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/runtime-sandbox-reconciliation.ts +1 -0
  32. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +10 -8
  33. package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +20 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/runtime-contract.ts +6 -0
  35. package/dist/bundling-sources/shared_libs/play-runtime/runtime-scheduler-topology.ts +24 -0
  36. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +30 -1
  37. package/dist/bundling-sources/shared_libs/plays/artifact-types.ts +6 -3
  38. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +152 -17
  39. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +19 -4
  40. package/dist/bundling-sources/shared_libs/plays/contracts.ts +6 -14
  41. package/dist/bundling-sources/shared_libs/plays/core.ts +11 -6
  42. package/dist/bundling-sources/shared_libs/plays/docflow-binding-owner.ts +16 -11
  43. package/dist/bundling-sources/shared_libs/plays/resolve-static-pipeline.ts +1 -1
  44. package/dist/bundling-sources/shared_libs/plays/slug.ts +115 -0
  45. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +44 -14
  46. package/dist/bundling-sources/shared_libs/product-notifications/catalog.ts +877 -0
  47. package/dist/bundling-sources/shared_libs/product-notifications/configuration-draft.ts +456 -0
  48. package/dist/bundling-sources/shared_libs/product-notifications/configuration.ts +560 -0
  49. package/dist/bundling-sources/shared_libs/product-notifications/content.ts +28 -0
  50. package/dist/bundling-sources/shared_libs/product-notifications/default-policy.ts +59 -0
  51. package/dist/bundling-sources/shared_libs/product-notifications/email.ts +246 -0
  52. package/dist/bundling-sources/shared_libs/product-notifications/evaluation.ts +280 -0
  53. package/dist/bundling-sources/shared_libs/product-notifications/events.ts +433 -0
  54. package/dist/bundling-sources/shared_libs/product-notifications/legacy-slack.ts +80 -0
  55. package/dist/bundling-sources/shared_libs/product-notifications/workspace.ts +38 -0
  56. package/dist/bundling-sources/shared_libs/security/internal-actor.ts +50 -0
  57. package/dist/cli/index.js +1065 -190
  58. package/dist/cli/index.mjs +1052 -177
  59. package/dist/{compiler-manifest-Dbov16F7.d.mts → compiler-manifest-DsLbQaew.d.mts} +132 -8
  60. package/dist/{compiler-manifest-Dbov16F7.d.ts → compiler-manifest-DsLbQaew.d.ts} +132 -8
  61. package/dist/index.d.mts +833 -11
  62. package/dist/index.d.ts +833 -11
  63. package/dist/index.js +467 -38
  64. package/dist/index.mjs +467 -38
  65. package/dist/install-integrity.json +20 -2
  66. package/dist/plays/bundle-play-file.d.mts +8 -3
  67. package/dist/plays/bundle-play-file.d.ts +8 -3
  68. package/dist/plays/bundle-play-file.mjs +100 -20
  69. package/package.json +1 -1
@@ -1,20 +1,7 @@
1
1
  /**
2
2
  * HTTP headers attached to every server-to-coordinator request.
3
- *
4
- * The coordinator is a Cloudflare Worker (`deepline-play-coordinator-*`) that
5
- * we publish through Cloudflare's Versions + Deployments primitives. Whenever
6
- * a deployment is split across two versions (e.g. 10% canary), Cloudflare
7
- * uses the `Cloudflare-Workers-Version-Key` header to pick a stable version
8
- * per request. Sending the runId as the key keeps every request for one play
9
- * run pinned to whichever version started the run, so a partial rollout
10
- * cannot strand a Workflow's `/submit` and `/result` calls on different
11
- * code versions.
12
- *
13
- * The header is harmless when there is only one active version (the default).
14
3
  */
15
- export const COORDINATOR_VERSION_KEY_HEADER = 'Cloudflare-Workers-Version-Key';
16
- export const COORDINATOR_VERSION_OVERRIDES_HEADER =
17
- 'Cloudflare-Workers-Version-Overrides';
4
+
18
5
  /**
19
6
  * Shared secret the coordinator uses to authenticate dispatcher (Vercel app)
20
7
  * traffic. The coordinator also sanity-checks `x-deepline-run-scope` matches
@@ -88,8 +75,6 @@ export function coordinatorRequestHeaders(input: {
88
75
  runId: string;
89
76
  contentType?: string | null;
90
77
  internalToken?: string | null;
91
- runtimeDeployVersion?: string | null;
92
- coordinatorWorkerName?: string | null;
93
78
  /**
94
79
  * When set, the coordinator validates this matches the runId in the URL.
95
80
  * Pass the runId on `/cancel` / `/signal` calls so a leaked dispatcher
@@ -102,22 +87,6 @@ export function coordinatorRequestHeaders(input: {
102
87
  headers['content-type'] = input.contentType;
103
88
  }
104
89
  const trimmed = input.runId.trim();
105
- if (trimmed) {
106
- headers[COORDINATOR_VERSION_KEY_HEADER] = trimmed;
107
- }
108
- const runtimeDeployVersion = input.runtimeDeployVersion?.trim();
109
- const coordinatorWorkerName = input.coordinatorWorkerName?.trim();
110
- if (
111
- runtimeDeployVersion &&
112
- coordinatorWorkerName &&
113
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
114
- runtimeDeployVersion,
115
- ) &&
116
- /^[A-Za-z0-9_-]+$/.test(coordinatorWorkerName)
117
- ) {
118
- headers[COORDINATOR_VERSION_OVERRIDES_HEADER] =
119
- `${coordinatorWorkerName}="${runtimeDeployVersion}"`;
120
- }
121
90
  const internalToken =
122
91
  input.internalToken?.trim() || resolveInternalCoordinatorToken();
123
92
  if (internalToken) {
@@ -10,8 +10,10 @@ import type { PlayRunContractSnapshot } from '../plays/contracts';
10
10
  import type { PlayStructuredDefinition } from '../plays/definition';
11
11
  import type { PlayStaticPipeline } from '../plays/static-pipeline';
12
12
  import type {
13
+ AsyncPlayRunRef,
13
14
  DurableCallStaleAfterSeconds,
14
15
  PlayAuthoringCallOptions,
16
+ PlayAuthoringAsyncCallOptions,
15
17
  PlayAuthoringCsvOptions,
16
18
  PlayAuthoringFetchOptions,
17
19
  PlayAuthoringRuntimeStepOptions,
@@ -331,6 +333,19 @@ export type PlayCallOptions = Omit<
331
333
  timeoutMs?: number;
332
334
  };
333
335
 
336
+ export type PlayAsyncCallOptions = PlayAuthoringAsyncCallOptions;
337
+
338
+ export type AsyncPlayLaunchRequest = Readonly<{
339
+ key: string;
340
+ invocationKey: string;
341
+ playRef: string;
342
+ input: Record<string, unknown>;
343
+ description: string;
344
+ schedulerSchema: string | null;
345
+ /** Governor ancestry at the authored call site, before entering the child. */
346
+ ancestryPlayIds: readonly string[];
347
+ }>;
348
+
334
349
  export type RuntimeStepOptions = PlayAuthoringRuntimeStepOptions;
335
350
 
336
351
  export type FetchOptions = PlayAuthoringFetchOptions;
@@ -345,6 +360,16 @@ export interface ResolvedPlayExecution {
345
360
  definition?: PlayStructuredDefinition | null;
346
361
  staticPipeline?: PlayStaticPipeline | null;
347
362
  contractSnapshot?: PlayRunContractSnapshot | null;
363
+ launchMetadata?: {
364
+ revisionId?: string | null;
365
+ artifactStorageKey: string;
366
+ artifactHash: string;
367
+ graphHash: string;
368
+ definitionScope: 'org' | 'system';
369
+ authoringContract?:
370
+ | import('../plays/authoring-contract').AdmittedPlayAuthoringContract
371
+ | null;
372
+ } | null;
348
373
  }
349
374
 
350
375
  export interface BatchRequest {
@@ -548,6 +573,7 @@ export type IntegrationEventWaitRuntimeContext = {
548
573
  workflowId?: string;
549
574
  orgId?: string;
550
575
  executorToken?: string;
576
+ slackHitlDefaultTimeoutMs?: number;
551
577
  };
552
578
 
553
579
  export type IntegrationEventWaitBoundary = {
@@ -560,6 +586,14 @@ export type IntegrationEventWaitBoundary = {
560
586
  channel: string;
561
587
  ts: string;
562
588
  };
589
+ /**
590
+ * Optional control-plane timeout behavior. Missing preserves the legacy
591
+ * "return the timed_out result" replay behavior for older app deployments.
592
+ */
593
+ timeoutPolicy?: {
594
+ suspendAfterTimeoutMs?: number;
595
+ finalizeOnTimeout?: boolean;
596
+ };
563
597
  };
564
598
 
565
599
  export type IntegrationEventWaitHandler = {
@@ -584,6 +618,11 @@ export type IntegrationEventWaitHandler = {
584
618
  context: IntegrationEventWaitRuntimeContext;
585
619
  boundary: IntegrationEventWaitBoundary;
586
620
  }) => Promise<void>;
621
+ /**
622
+ * When set, timeout finalization is side-effect-only: the Play stays parked
623
+ * instead of consuming a `timed_out` result.
624
+ */
625
+ suspendAfterTimeoutMs?: number;
587
626
  };
588
627
 
589
628
  export interface ContextOptions {
@@ -769,6 +808,8 @@ export interface ContextOptions {
769
808
  }) => Promise<void>;
770
809
  playId?: string;
771
810
  runId?: string;
811
+ /** Shared governance/billing root. Differs from runId for async children. */
812
+ rootRunId?: string;
772
813
  /** Physical executor run that owns leases for an inline child context. */
773
814
  runtimeReceiptOwnerRunId?: string;
774
815
  /** Logical invocation scope for receipts created inside an inline child. */
@@ -800,6 +841,10 @@ export interface ContextOptions {
800
841
  */
801
842
  runtimeSheetBackedMapDatasets?: boolean;
802
843
  resolvePlay?: (playRef: string) => Promise<ResolvedPlayExecution | null>;
844
+ /** Admit an independently scheduled scalar/HITL Play Run. */
845
+ launchPlayAsync?: (
846
+ request: AsyncPlayLaunchRequest,
847
+ ) => Promise<AsyncPlayRunRef>;
803
848
  getToolQueueHints?: (toolId: string) => Promise<readonly PlayQueueHint[]>;
804
849
  getToolProvider?: (toolId: string) => Promise<string | null>;
805
850
  getToolOperation?: (toolId: string) => Promise<string | null>;
@@ -1147,6 +1192,12 @@ export type PlayStep =
1147
1192
  nestedSteps: PlayStep[];
1148
1193
  description?: string;
1149
1194
  }
1195
+ | {
1196
+ type: 'play_launch';
1197
+ playId: string;
1198
+ runId: string;
1199
+ description?: string;
1200
+ }
1150
1201
  | {
1151
1202
  type: 'run_javascript';
1152
1203
  alias: string;
@@ -1187,6 +1238,12 @@ export type PlayDatasetSubstep =
1187
1238
  nestedSteps: PlayStep[];
1188
1239
  description?: string;
1189
1240
  }
1241
+ | {
1242
+ type: 'play_launch';
1243
+ playId: string;
1244
+ runId: string;
1245
+ description?: string;
1246
+ }
1190
1247
  | {
1191
1248
  type: 'run_javascript';
1192
1249
  alias: string;
@@ -10,6 +10,15 @@ type TestRateLimitPayload = {
10
10
  lead_id?: string;
11
11
  row_number?: number;
12
12
  simulated_delay_ms?: number;
13
+ emit_security_gateway?: boolean;
14
+ /**
15
+ * Synthetic-provider-only switch. The large-payload gauntlet uses the batch
16
+ * response as a transport fixture and does not inspect per-item execution
17
+ * counters, so it can avoid creating thousands of unrelated test-state rows.
18
+ */
19
+ track_item_execution?: boolean;
20
+ simulated_cooldown_ms?: number;
21
+ cooldown_failures_before_success?: number;
13
22
  };
14
23
 
15
24
  type TestRateLimitBatchPayload = {
@@ -83,7 +92,17 @@ const testRateLimitBatchStrategy: BatchOperationStrategy<
83
92
  maxBatchSize: 200,
84
93
  bucketKeyPayloadFields: ['stage'],
85
94
  canBatchWith(left, right) {
86
- return String(left.stage || '') === String(right.stage || '');
95
+ // One upstream native batch has one 429/Retry-After outcome. Cooldown
96
+ // fixtures therefore only share a physical request when they requested
97
+ // the exact same policy; a normal member must never inherit another
98
+ // member's synthetic provider failure.
99
+ return (
100
+ String(left.stage || '') === String(right.stage || '') &&
101
+ Number(left.simulated_cooldown_ms ?? 0) ===
102
+ Number(right.simulated_cooldown_ms ?? 0) &&
103
+ Number(left.cooldown_failures_before_success ?? 0) ===
104
+ Number(right.cooldown_failures_before_success ?? 0)
105
+ );
87
106
  },
88
107
  toBucketKey(payload) {
89
108
  return `test_batch_rate_limit:${String(payload.stage || '')}`;
@@ -2,6 +2,7 @@ import {
2
2
  validateFixtureBehavior,
3
3
  type FixtureBehavior,
4
4
  } from './fixture-behavior';
5
+ import { normalizeRuntimeSchedulerSchema } from './runtime-scheduler-topology';
5
6
 
6
7
  export const RUNTIME_EXECUTION_CAPABILITIES = [
7
8
  'artifact.read',
@@ -46,6 +47,8 @@ export type RuntimeAuthorityDescriptor = {
46
47
  synthetic?: boolean | null;
47
48
  actorUserId?: string | null;
48
49
  actorEmail?: string | null;
50
+ /** Durable scheduler topology, carried into each newly minted attempt token. */
51
+ schedulerSchema?: string | null;
49
52
  /**
50
53
  * Whether THIS run may capture the docflow provenance trace.
51
54
  *
@@ -142,6 +145,9 @@ export function requireRuntimeAuthorityDescriptor(
142
145
  synthetic: descriptor.synthetic === true,
143
146
  actorUserId: descriptor.actorUserId ?? null,
144
147
  actorEmail: descriptor.actorEmail ?? null,
148
+ schedulerSchema: normalizeRuntimeSchedulerSchema(
149
+ descriptor.schedulerSchema,
150
+ ),
145
151
  // `=== true`, not `?? false`: the normalizer is what a durable launch is
146
152
  // read back through, so anything that is not an explicit yes — missing,
147
153
  // null, a string that survived a JSON round trip — denies.
@@ -11,7 +11,13 @@
11
11
  export const PLAY_RUNTIME_PROTOCOL = {
12
12
  /** Launches written before this field existed. */
13
13
  legacy: 0,
14
- /** Current scheduler/runner protocol. */
14
+ /**
15
+ * Current scheduler/runner protocol. This number STAYS AT 1: the additive
16
+ * compatibility rule (`docs/runtime-compatibility-contract.md`) covers new
17
+ * fields and new named actions without a bump. A bump is a fence release —
18
+ * two PRs, expand worker support first, then enable — and needs a written
19
+ * reason the additive rule cannot serve.
20
+ */
15
21
  current: 1,
16
22
  } as const;
17
23
 
@@ -0,0 +1,189 @@
1
+ /**
2
+ * The Absurd release-lane retirement gate, as dependency-free pure logic.
3
+ *
4
+ * This lives in its own module because three very different processes must
5
+ * reach exactly the same verdict and none of them may drag the others'
6
+ * dependencies in:
7
+ *
8
+ * - the Next.js app (`src/lib/plays/runtime-lanes.ts`, the admin and internal
9
+ * lane endpoints), which also talks to Convex;
10
+ * - `scripts/fly-absurd-lane.ts` retire, run by the release controller; and
11
+ * - `apps/absurd-release-scaler/index.ts`, which owns retirement of every lane
12
+ * the release controller handed over, and whose Fly image does not contain
13
+ * `packages/convex/`.
14
+ *
15
+ * The gate clears two ways:
16
+ *
17
+ * 1. **Empty.** No non-terminal task on the lane's queue and no non-terminal
18
+ * run row referencing its release id.
19
+ * 2. **Adopted parked (lane handoff).** The lane has been marked not claiming,
20
+ * nothing on it is claimable or running, and every remaining non-terminal
21
+ * run is parked in a durable wait the newest lane adopts.
22
+ *
23
+ * Anything else is blocked, and `blockedBy` names which of the two halves is
24
+ * responsible so an operator is not left guessing at a bare depth count.
25
+ */
26
+
27
+ /**
28
+ * The subset of a lane's depth the gate reads.
29
+ *
30
+ * Every field is optional because the gate is also evaluated against a lane
31
+ * view that crossed the wire from a control plane which may be older than its
32
+ * caller: the release-aware scaler is deployed from the same release as the
33
+ * app, but a scaler image can outlive the app deployment it polls. A fact that
34
+ * did not arrive is never guessed — see `adoption-facts-unavailable`.
35
+ */
36
+ export type LaneRetirementGateFacts = {
37
+ /** Non-terminal tasks on `absurd.t_<queue>`. */
38
+ queuedTasks?: number;
39
+ claimableTasks?: number;
40
+ runningTasks?: number;
41
+ /** Non-terminal `runs` rows whose launch envelope pins this release id. */
42
+ nonTerminalRuns?: number;
43
+ /** Subset of `nonTerminalRuns` parked in a wait the newest lane adopts. */
44
+ adoptableParkedRuns?: number;
45
+ /** Whether this lane still claims work off its own queue. */
46
+ claiming?: boolean;
47
+ /** `queuedTasks === 0 && nonTerminalRuns === 0`. */
48
+ empty?: boolean;
49
+ /** `claimableTasks === 0 && runningTasks === 0 && nonTerminalRuns === adoptableParkedRuns`. */
50
+ emptyExceptAdoptableParked?: boolean;
51
+ };
52
+
53
+ export type LaneRetirementGateBasis = 'empty' | 'adopted-parked' | 'blocked';
54
+
55
+ /**
56
+ * Why a blocked lane is blocked. This is the operator-facing half of the gate:
57
+ * each value maps to exactly one remediation.
58
+ */
59
+ export type LaneRetirementBlockedReason =
60
+ /** Not blocked. */
61
+ | 'none'
62
+ /** Something on this lane is claimable or running right now. Wait. */
63
+ | 'executable-work'
64
+ /**
65
+ * The lane holds nothing but adoptable parked waits, and the ONLY thing
66
+ * keeping it alive is that its claiming switch was never flipped. The lane
67
+ * cannot flip it itself, so without an external write this lane is immortal.
68
+ */
69
+ | 'still-claiming'
70
+ /**
71
+ * Non-terminal runs this lane can no longer execute and no other lane can
72
+ * adopt: a `detached_runner` park whose sandbox is gone, or a run row with no
73
+ * engine task at all. Nothing will ever wake these; they need an explicit,
74
+ * evidence-gated terminal transition.
75
+ */
76
+ | 'unadoptable-non-terminal-runs'
77
+ /**
78
+ * The caller's lane view predates the adoption fields. The gate refuses to
79
+ * infer adoption from absent evidence and falls back to the strict `empty`
80
+ * half only.
81
+ */
82
+ | 'adoption-facts-unavailable';
83
+
84
+ export type LaneRetirementGateResult = {
85
+ ok: boolean;
86
+ basis: LaneRetirementGateBasis;
87
+ blockedBy: LaneRetirementBlockedReason;
88
+ };
89
+
90
+ /**
91
+ * Evaluate the retirement gate.
92
+ *
93
+ * Deliberately conservative in one direction only: it never clears a lane on a
94
+ * fact it did not receive. It is safe for the caller to retire on `ok`, because
95
+ * retirement deletes Machines and flips the registry row; it does not drop the
96
+ * queue tables an adopting lane still claims from.
97
+ */
98
+ export function laneRetirementGate(
99
+ depth: LaneRetirementGateFacts,
100
+ ): LaneRetirementGateResult {
101
+ if (depth.empty === true) {
102
+ return { ok: true, basis: 'empty', blockedBy: 'none' };
103
+ }
104
+ if (
105
+ typeof depth.claiming !== 'boolean' ||
106
+ typeof depth.emptyExceptAdoptableParked !== 'boolean' ||
107
+ typeof depth.adoptableParkedRuns !== 'number'
108
+ ) {
109
+ return {
110
+ ok: false,
111
+ basis: 'blocked',
112
+ blockedBy: 'adoption-facts-unavailable',
113
+ };
114
+ }
115
+ if (!depth.claiming && depth.emptyExceptAdoptableParked) {
116
+ return { ok: true, basis: 'adopted-parked', blockedBy: 'none' };
117
+ }
118
+ const executable =
119
+ (depth.claimableTasks ?? 0) > 0 || (depth.runningTasks ?? 0) > 0;
120
+ if (executable) {
121
+ return { ok: false, basis: 'blocked', blockedBy: 'executable-work' };
122
+ }
123
+ if (depth.claiming && depth.emptyExceptAdoptableParked) {
124
+ return { ok: false, basis: 'blocked', blockedBy: 'still-claiming' };
125
+ }
126
+ return {
127
+ ok: false,
128
+ basis: 'blocked',
129
+ blockedBy: 'unadoptable-non-terminal-runs',
130
+ };
131
+ }
132
+
133
+ /**
134
+ * The one operator-actionable sentence for a blocked lane.
135
+ *
136
+ * A depth count alone ("11 queued") has repeatedly sent operators looking for
137
+ * live work that is not there. Each reason below has exactly one next step.
138
+ */
139
+ export function laneRetirementRemediation(
140
+ result: LaneRetirementGateResult,
141
+ ): string {
142
+ switch (result.blockedBy) {
143
+ case 'none':
144
+ return result.basis === 'adopted-parked'
145
+ ? 'Retirable: nothing here is claimable or running and every remaining run is a parked wait the newest lane adopts.'
146
+ : 'Retirable: this lane holds no non-terminal task and no non-terminal run.';
147
+ case 'executable-work':
148
+ return 'Wait. Work on this lane is claimable or running right now; the scaler keeps a version-matched machine for it.';
149
+ case 'still-claiming':
150
+ return 'Demote the lane. It holds only adoptable parked waits; the newest lane cannot adopt them until this lane stops claiming (lane handoff). The release-aware scaler converges this, or run `bun scripts/fly-absurd-lane.ts stop-claiming --release <id>`.';
151
+ case 'unadoptable-non-terminal-runs':
152
+ return 'Inspect the runs. This lane holds non-terminal runs it can no longer execute and no lane can adopt. Run `deepline-admin runtime stranded scan --release <id> --environment production --json`, then remediate the reviewed cohort with `runtime stranded plan` / `runtime stranded resolve --dry-run`.';
153
+ case 'adoption-facts-unavailable':
154
+ return 'The lane view did not carry the lane handoff adoption fields, so only the strict empty gate was evaluated. Re-read the lane against a control plane from this release.';
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Should this lane's claiming switch be converged to `false`?
160
+ *
161
+ * The switch is written exactly once, by the release controller, as a
162
+ * `reclamation` effect whose failure is explicitly tolerated — a failed
163
+ * `stop-prior-lane-claiming` is logged and the release still SUCCEEDS. Absence
164
+ * of the control row reads as "claiming", so a lost write leaves a demoted lane
165
+ * claiming forever. With no machines the lane cannot make progress, and with
166
+ * `claiming` true the newest lane may not adopt it: a permanently unclaimable
167
+ * steady state.
168
+ *
169
+ * This predicate is intentionally narrow. It fires only for a lane whose gate
170
+ * is blocked by the lost write and nothing else, so it can never demote a lane
171
+ * that is still doing something, and it cannot fight `abort_retire` (which
172
+ * re-arms claiming for a lane that has real work, and therefore is not blocked
173
+ * by `still-claiming`).
174
+ */
175
+ export function shouldConvergeLaneToNotClaiming(lane: {
176
+ active?: boolean;
177
+ status?: string;
178
+ queueDepth?: LaneRetirementGateFacts;
179
+ }): boolean {
180
+ if (lane.active !== false) return false;
181
+ if (lane.status !== 'draining' && lane.status !== 'retiring') return false;
182
+ const depth = lane.queueDepth;
183
+ if (!depth) return false;
184
+ if ((depth.claimableTasks ?? 0) > 0 || (depth.runningTasks ?? 0) > 0) {
185
+ return false;
186
+ }
187
+ if ((depth.adoptableParkedRuns ?? 0) <= 0) return false;
188
+ return laneRetirementGate(depth).blockedBy === 'still-claiming';
189
+ }
@@ -34,6 +34,9 @@ export type PlayRuntimeSemantics = Readonly<{
34
34
  */
35
35
  receiptIdentityEdition: PlayAuthoringContractEdition | null;
36
36
  }>;
37
+ slackHitl: Readonly<{
38
+ defaultTimeoutMs: number;
39
+ }>;
37
40
  }>;
38
41
 
39
42
  function definePlayRuntimeSemantics(
@@ -43,6 +46,7 @@ function definePlayRuntimeSemantics(
43
46
  ...semantics,
44
47
  secrets: Object.freeze({ ...semantics.secrets }),
45
48
  fetch: Object.freeze({ ...semantics.fetch }),
49
+ slackHitl: Object.freeze({ ...semantics.slackHitl }),
46
50
  });
47
51
  }
48
52
 
@@ -66,6 +70,14 @@ const PLAINTEXT_SECRET_SEMANTICS = {
66
70
  secrets: { value: 'plaintext_promise' },
67
71
  } as const;
68
72
 
73
+ const LEGACY_SLACK_HITL_SEMANTICS = {
74
+ defaultTimeoutMs: 24 * 60 * 60 * 1000,
75
+ } as const;
76
+
77
+ const WEEK_LONG_SLACK_HITL_SEMANTICS = {
78
+ defaultTimeoutMs: 7 * 24 * 60 * 60 * 1000,
79
+ } as const;
80
+
69
81
  /**
70
82
  * Exhaustive compatibility registry for worker-visible Play behavior.
71
83
  *
@@ -77,22 +89,27 @@ const PLAY_RUNTIME_SEMANTICS_BY_EDITION = {
77
89
  1: definePlayRuntimeSemantics({
78
90
  authoringContractEdition: 1,
79
91
  ...LEGACY_SEMANTICS,
92
+ slackHitl: LEGACY_SLACK_HITL_SEMANTICS,
80
93
  }),
81
94
  2: definePlayRuntimeSemantics({
82
95
  authoringContractEdition: 2,
83
96
  ...STRICT_SEMANTICS,
97
+ slackHitl: LEGACY_SLACK_HITL_SEMANTICS,
84
98
  }),
85
99
  3: definePlayRuntimeSemantics({
86
100
  authoringContractEdition: 3,
87
101
  ...STRICT_SEMANTICS,
102
+ slackHitl: LEGACY_SLACK_HITL_SEMANTICS,
88
103
  }),
89
104
  4: definePlayRuntimeSemantics({
90
105
  authoringContractEdition: 4,
91
106
  ...PLAINTEXT_SECRET_SEMANTICS,
107
+ slackHitl: LEGACY_SLACK_HITL_SEMANTICS,
92
108
  }),
93
109
  5: definePlayRuntimeSemantics({
94
110
  authoringContractEdition: 5,
95
111
  ...PLAINTEXT_SECRET_SEMANTICS,
112
+ slackHitl: LEGACY_SLACK_HITL_SEMANTICS,
96
113
  fetch: {
97
114
  httpFailure: 'throw_typed_error',
98
115
  persistHttpFailure: false,
@@ -102,6 +119,7 @@ const PLAY_RUNTIME_SEMANTICS_BY_EDITION = {
102
119
  6: definePlayRuntimeSemantics({
103
120
  authoringContractEdition: 6,
104
121
  ...PLAINTEXT_SECRET_SEMANTICS,
122
+ slackHitl: LEGACY_SLACK_HITL_SEMANTICS,
105
123
  fetch: {
106
124
  httpFailure: 'throw_typed_error',
107
125
  persistHttpFailure: false,
@@ -110,6 +128,16 @@ const PLAY_RUNTIME_SEMANTICS_BY_EDITION = {
110
128
  receiptIdentityEdition: 6,
111
129
  },
112
130
  }),
131
+ 7: definePlayRuntimeSemantics({
132
+ authoringContractEdition: 7,
133
+ ...PLAINTEXT_SECRET_SEMANTICS,
134
+ slackHitl: WEEK_LONG_SLACK_HITL_SEMANTICS,
135
+ fetch: {
136
+ httpFailure: 'throw_typed_error',
137
+ persistHttpFailure: false,
138
+ receiptIdentityEdition: 6,
139
+ },
140
+ }),
113
141
  } satisfies Record<PlayAuthoringContractEdition, PlayRuntimeSemantics>;
114
142
 
115
143
  export function resolvePlayRuntimeSemantics(
@@ -152,6 +152,8 @@ export interface PlayRunnerContextConfig {
152
152
  workflowId?: string;
153
153
  playId?: string;
154
154
  runId?: string;
155
+ /** Shared governance/billing root. Differs from runId for async children. */
156
+ rootRunId?: string;
155
157
  runAttempt?: number | null;
156
158
  playName?: string;
157
159
  userEmail?: string;
@@ -170,6 +170,29 @@ export class ProviderExhaustedError extends Error {
170
170
  }
171
171
  }
172
172
 
173
+ /**
174
+ * `ProviderExhaustedError` is created inside the runtime, but a tool execution
175
+ * can cross the execution gateway before a step program sees it. The gateway
176
+ * deliberately serializes that error as the public, provider-owned
177
+ * `ToolExecutionError` shape. Treat those two representations as the same
178
+ * typed exhaustion outcome; do not fall back based on a message substring.
179
+ */
180
+ export function isProviderExhaustedFailure(
181
+ error: unknown,
182
+ ): error is
183
+ | ProviderExhaustedError
184
+ | (ToolExecutionError & {
185
+ readonly origin: 'provider';
186
+ readonly code: typeof PROVIDER_EXHAUSTED_CODE;
187
+ }) {
188
+ return (
189
+ error instanceof ProviderExhaustedError ||
190
+ (error instanceof ToolExecutionError &&
191
+ error.origin === 'provider' &&
192
+ error.code === PROVIDER_EXHAUSTED_CODE)
193
+ );
194
+ }
195
+
173
196
  /**
174
197
  * Derive the human provider name for a PROVIDER_EXHAUSTED error from a pacing
175
198
  * bucketId. Bucket ids are `${orgId}:${provider}`; the provider itself is
@@ -130,7 +130,7 @@ export function isActivePlayRunStatus(status: unknown): boolean {
130
130
  * The minimal persisted Play Run document shape required to project the live
131
131
  * snapshot. This is a structural subset of the Convex `playRuns` doc and of
132
132
  * the run-observer projection returned by
133
- * `convex/runObservers.getPlayRunSnapshotForObserver`.
133
+ * `packages/convex/runObservers.getPlayRunSnapshotForObserver`.
134
134
  */
135
135
  export type LedgerBackedRunLike = {
136
136
  workflowId: string;
@@ -11,8 +11,15 @@ import {
11
11
  } from '../../../plays/sandbox-runtime-limits';
12
12
  import type { PlayRunnerRuntimeLifecycleEvent } from '../types';
13
13
  import { DAYTONA_PLAY_RUNNER_LABEL_SOURCE } from './daytona-labels';
14
-
15
- const DAYTONA_CREATE_TIMEOUT_SECONDS = 10;
14
+ import type { DaytonaSandboxStartAdmission } from './daytona-start-admission';
15
+
16
+ // Match Daytona's supported default create window. Ten seconds is shorter
17
+ // than normal provider startup under burst load: the SDK abandons an otherwise
18
+ // healthy create, and the resulting provider-side transition cannot safely be
19
+ // deleted or hedged with a second sandbox. The runner deadline still bounds
20
+ // the full lifecycle, while ambiguous creates retain the fail-closed cleanup
21
+ // path below.
22
+ const DAYTONA_CREATE_TIMEOUT_SECONDS = 60;
16
23
  const DAYTONA_CREATE_RETRY_DELAYS_MS = [0, 500, 1_500] as const;
17
24
  const DAYTONA_TIMED_OUT_CREATE_RECONCILE_DELAYS_MS = [
18
25
  0, 1_000, 3_000, 6_000, 12_000, 24_000, 36_000,
@@ -105,7 +112,7 @@ export function resolveDaytonaSandboxAcquisitionUnavailableReason(
105
112
  }
106
113
 
107
114
  function isDaytonaSandboxStartTimeout(error: string): boolean {
108
- return /Failed to create and start sandbox within 10 seconds\. Operation timed out\./.test(
115
+ return /Failed to create and start sandbox within \d+ seconds\. Operation timed out\./.test(
109
116
  error,
110
117
  );
111
118
  }
@@ -575,6 +582,7 @@ async function acquireOneShotDaytonaSandbox(input: {
575
582
  releaseSandboxCapacity?: (leaseId: string) => Promise<void>;
576
583
  nextProviderAttempt: () => number;
577
584
  startedAt: number;
585
+ startAdmission?: DaytonaSandboxStartAdmission;
578
586
  }): Promise<AcquiredDaytonaSandbox> {
579
587
  const limits = validatePlaySandboxRuntimeLimits(
580
588
  input.context.sandboxRuntimeLimits ?? {
@@ -582,7 +590,11 @@ async function acquireOneShotDaytonaSandbox(input: {
582
590
  },
583
591
  );
584
592
  input.emitStage('create:start');
585
- const result = await createRetriedOneShotDaytonaSandbox(input);
593
+ const result = input.startAdmission
594
+ ? await input.startAdmission.run(() =>
595
+ createRetriedOneShotDaytonaSandbox(input),
596
+ )
597
+ : await createRetriedOneShotDaytonaSandbox(input);
586
598
  const granted = {
587
599
  cpu: result.sandbox.cpu,
588
600
  memoryGiB: result.sandbox.memory,
@@ -683,6 +695,7 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
683
695
  providerOperationToken: string | null;
684
696
  }>;
685
697
  releaseSandboxCapacity?: (leaseId: string) => Promise<void>;
698
+ startAdmission?: DaytonaSandboxStartAdmission;
686
699
  startedAt?: number;
687
700
  }): OneShotDaytonaSandboxLifecycle {
688
701
  const orgId = validateDaytonaExecutionContext(input.context);
@@ -700,6 +713,7 @@ export function createOneShotDaytonaSandboxLifecycle(input: {
700
713
  observeCreateCall: input.observeCreateCall,
701
714
  reserveSandboxCapacity: input.reserveSandboxCapacity,
702
715
  releaseSandboxCapacity: input.releaseSandboxCapacity,
716
+ startAdmission: input.startAdmission,
703
717
  nextProviderAttempt: () => {
704
718
  providerAttempt += 1;
705
719
  return providerAttempt;