deepline 0.3.231 → 0.3.233

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.
@@ -92,7 +92,9 @@ import {
92
92
  } from './run-execution-scope';
93
93
  import { DEFAULT_RUNTIME_EXECUTION_CAPABILITIES } from './execution-capabilities';
94
94
  import { vercelProtectionBypassHeader } from './vercel-protection';
95
- import { RUNTIME_RELIABILITY_POLICY } from './runtime-reliability-policy';
95
+ import { positiveTimeoutMs } from './runtime-reliability-policy';
96
+ import { sendPlayHttpRequest } from './play-http-request';
97
+ export { CtxFetchTimeoutError } from './play-http-request';
96
98
  export {
97
99
  RuntimeSheetRowsBlockedError,
98
100
  type RuntimeSheetBlockedRowDetail,
@@ -205,10 +207,6 @@ import {
205
207
  TOOL_EXECUTE_PROVIDER_THROTTLE_HEADER,
206
208
  classifyToolExecuteHttpFailure,
207
209
  createToolExecuteHttpFailureAttemptTracker,
208
- TOOL_EXECUTE_BARE_RATE_LIMIT_MAX_ATTEMPTS,
209
- TOOL_EXECUTE_RATE_LIMIT_MAX_ATTEMPTS,
210
- TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS,
211
- TOOL_EXECUTE_RATE_LIMIT_RETRY_DELAY_MAX_MS,
212
210
  parseToolExecuteRetryAfterMs,
213
211
  parseToolExecuteRetryAtMs,
214
212
  parseToolExecuteAuthScopeChangedError,
@@ -590,19 +588,10 @@ const DEEPLINEAGENT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000;
590
588
  // a bounded, repairable transport failure that row-isolation settles per row
591
589
  // instead of hanging the whole run.
592
590
  const DEFAULT_TOOL_RUNTIME_TIMEOUT_MS = 15 * 60 * 1000 + 30_000;
593
- const FETCH_TRANSPORT_MAX_ATTEMPTS =
594
- RUNTIME_RELIABILITY_POLICY.egress.fetchMaxAttempts;
595
- const FETCH_TRANSPORT_RETRY_DELAY_MS = 100;
596
591
  // Diagnostic logs are retained in the context until terminalization. Keep a
597
592
  // representative, deduplicated set so row-heavy continued failures cannot turn
598
593
  // customer-safe observability into unbounded runner memory or log traffic.
599
594
  const MAX_CTX_FETCH_HTTP_FAILURE_DIAGNOSTICS = 16;
600
- const CTX_FETCH_HEADERS_TIMEOUT_MS =
601
- RUNTIME_RELIABILITY_POLICY.egress.fetchHeadersTimeoutMs;
602
- const CTX_FETCH_BODY_TIMEOUT_MS =
603
- RUNTIME_RELIABILITY_POLICY.egress.fetchBodyTimeoutMs;
604
- const CTX_FETCH_TOTAL_TIMEOUT_MS =
605
- RUNTIME_RELIABILITY_POLICY.egress.fetchTotalTimeoutMs;
606
595
  // cloudflared returns this branded HTML when its connection to the local app
607
596
  // resets. It is a transport failure before the app can reach a provider, not a
608
597
  // provider 502; require both the branded page and our development tunnel host.
@@ -647,185 +636,6 @@ class ToolExecuteResponseBodyTimeoutError extends Error {
647
636
  }
648
637
  }
649
638
 
650
- export class CtxFetchTimeoutError extends Error {
651
- readonly code = 'CTX_FETCH_TIMEOUT';
652
-
653
- constructor(
654
- readonly phase: 'headers' | 'body' | 'total',
655
- readonly timeoutMs: number,
656
- ) {
657
- super(
658
- `[CTX_FETCH_TIMEOUT phase=${phase} timeout_ms=${timeoutMs}] ` +
659
- `ctx.fetch exceeded its ${phase} deadline after ${timeoutMs}ms.`,
660
- );
661
- this.name = 'CtxFetchTimeoutError';
662
- }
663
- }
664
-
665
- type CtxFetchDeadline = {
666
- bodyMs: number;
667
- headersMs: number;
668
- startedAt: number;
669
- totalMs: number;
670
- };
671
-
672
- function positiveTimeoutMs(
673
- value: number | undefined,
674
- fallback: number,
675
- ): number {
676
- return value === undefined || !Number.isFinite(value) || value <= 0
677
- ? fallback
678
- : Math.max(1, Math.floor(value));
679
- }
680
-
681
- function remainingCtxFetchMs(deadline: CtxFetchDeadline): number {
682
- return Math.max(0, deadline.totalMs - (Date.now() - deadline.startedAt));
683
- }
684
-
685
- async function runCtxFetchPhase<T>(input: {
686
- callerSignal?: AbortSignal | null;
687
- controller: AbortController;
688
- deadline: CtxFetchDeadline;
689
- phase: 'headers' | 'body' | 'total';
690
- run: () => Promise<T>;
691
- }): Promise<T> {
692
- const remainingMs = remainingCtxFetchMs(input.deadline);
693
- if (remainingMs <= 0) {
694
- throw new CtxFetchTimeoutError('total', input.deadline.totalMs);
695
- }
696
- if (input.callerSignal?.aborted) {
697
- throw (
698
- input.callerSignal.reason ?? new DOMException('Aborted', 'AbortError')
699
- );
700
- }
701
- const phaseLimitMs =
702
- input.phase === 'headers'
703
- ? input.deadline.headersMs
704
- : input.phase === 'body'
705
- ? input.deadline.bodyMs
706
- : remainingMs;
707
- const timeoutMs = Math.min(phaseLimitMs, remainingMs);
708
- const timeoutPhase = timeoutMs >= remainingMs ? 'total' : input.phase;
709
- let timeout: ReturnType<typeof setTimeout> | null = null;
710
- let onCallerAbort: (() => void) | null = null;
711
- const phaseTimeout = new Promise<never>((_resolve, reject) => {
712
- timeout = setTimeout(() => {
713
- const error = new CtxFetchTimeoutError(
714
- timeoutPhase,
715
- timeoutPhase === 'total' ? input.deadline.totalMs : phaseLimitMs,
716
- );
717
- reject(error);
718
- input.controller.abort(error);
719
- }, timeoutMs);
720
- });
721
- const callerAbort = input.callerSignal
722
- ? new Promise<never>((_resolve, reject) => {
723
- onCallerAbort = () => {
724
- const reason =
725
- input.callerSignal?.reason ??
726
- new DOMException('Aborted', 'AbortError');
727
- reject(reason);
728
- input.controller.abort(reason);
729
- };
730
- input.callerSignal!.addEventListener('abort', onCallerAbort, {
731
- once: true,
732
- });
733
- })
734
- : null;
735
- try {
736
- return await Promise.race([
737
- input.run(),
738
- phaseTimeout,
739
- ...(callerAbort ? [callerAbort] : []),
740
- ]);
741
- } finally {
742
- if (timeout) clearTimeout(timeout);
743
- if (onCallerAbort) {
744
- input.callerSignal?.removeEventListener('abort', onCallerAbort);
745
- }
746
- }
747
- }
748
-
749
- async function sleepWithinCtxFetchDeadline(input: {
750
- callerSignal?: AbortSignal | null;
751
- deadline: CtxFetchDeadline;
752
- delayMs: number;
753
- }): Promise<void> {
754
- const controller = new AbortController();
755
- let timer: ReturnType<typeof setTimeout> | undefined;
756
- try {
757
- await runCtxFetchPhase({
758
- callerSignal: input.callerSignal,
759
- controller,
760
- deadline: input.deadline,
761
- phase: 'total',
762
- run: () =>
763
- new Promise<void>((resolve) => {
764
- timer = setTimeout(resolve, input.delayMs);
765
- }),
766
- });
767
- } finally {
768
- if (timer !== undefined) clearTimeout(timer);
769
- }
770
- }
771
-
772
- function isCtxFetchBackpressure(response: Response, bodyText: string): boolean {
773
- if (response.status === 429) return true;
774
- if (response.status !== 503) return false;
775
- // This Salesforce endpoint reports a rejected burst as 503. Match the final
776
- // destination and exact response contract; arbitrary 503s remain failures.
777
- try {
778
- const destination = new URL(response.url);
779
- const body: unknown = JSON.parse(bodyText);
780
- return (
781
- destination.protocol === 'https:' &&
782
- destination.hostname.endsWith('.salesforce-sites.com') &&
783
- body !== null &&
784
- typeof body === 'object' &&
785
- !Array.isArray(body) &&
786
- (body as Record<string, unknown>).status === 'burst_circuit_open'
787
- );
788
- } catch {
789
- return false;
790
- }
791
- }
792
-
793
- async function readCtxFetchBody(input: {
794
- callerSignal?: AbortSignal | null;
795
- controller: AbortController;
796
- deadline: CtxFetchDeadline;
797
- response: Response;
798
- }): Promise<string> {
799
- if (!input.response.body) return '';
800
- const reader = input.response.body.getReader();
801
- const decoder = new TextDecoder();
802
- try {
803
- return await runCtxFetchPhase({
804
- callerSignal: input.callerSignal,
805
- controller: input.controller,
806
- deadline: input.deadline,
807
- phase: 'body',
808
- run: async () => {
809
- let body = '';
810
- for (;;) {
811
- const chunk = await reader.read();
812
- if (chunk.done) return body + decoder.decode();
813
- body += decoder.decode(chunk.value, { stream: true });
814
- }
815
- },
816
- });
817
- } catch (error) {
818
- // Cancellation is socket hygiene, not part of the timeout contract. A
819
- // hostile/custom stream may never resolve its cancel hook; awaiting it
820
- // here would recreate the exact forever-pending ctx.fetch this deadline
821
- // is meant to eliminate.
822
- void reader.cancel(error).catch(() => {});
823
- throw error;
824
- } finally {
825
- reader.releaseLock();
826
- }
827
- }
828
-
829
639
  /** What a map is parked on when nothing in it is settling. */
830
640
  export type RuntimeMapReceiptWaitParking = {
831
641
  /** Receipt-wait groups parked right now. */
@@ -1336,9 +1146,6 @@ export function chunkArrayForReceiptRequest<T>(items: readonly T[]): T[][] {
1336
1146
  return chunks;
1337
1147
  }
1338
1148
 
1339
- type SafeFetchModule = typeof import('../security/safe-fetch');
1340
- let safeFetchModule: Promise<SafeFetchModule> | null = null;
1341
-
1342
1149
  export async function waitForNodeRuntimeMapRowsVisible(input: {
1343
1150
  mapName: string;
1344
1151
  tableNamespace: string;
@@ -1546,11 +1353,6 @@ function isUnmarkedExecutionGateway502(input: {
1546
1353
  }
1547
1354
  }
1548
1355
 
1549
- function loadSafeFetch(): Promise<SafeFetchModule> {
1550
- safeFetchModule ??= import('../security/safe-fetch');
1551
- return safeFetchModule;
1552
- }
1553
-
1554
1356
  function waitForSecretResolutionRetry(ms: number): Promise<void> {
1555
1357
  if (ms <= 0) return Promise.resolve();
1556
1358
  return new Promise<void>((resolve) => setTimeout(resolve, ms));
@@ -1578,10 +1380,6 @@ const NO_SECRET_RESOLUTION_RETRY: SecretResolutionRetryDecision = {
1578
1380
  retryAfterExceedsCap: false,
1579
1381
  };
1580
1382
 
1581
- function isUnsafeOutboundUrlError(error: unknown): boolean {
1582
- return error instanceof Error && error.name === 'UnsafeOutboundUrlError';
1583
- }
1584
-
1585
1383
  function publicToolResponseEnvelope(value: unknown): {
1586
1384
  status: string;
1587
1385
  raw: unknown;
@@ -2635,7 +2433,9 @@ const WAITING_ROW = Symbol('deepline.waiting_row');
2635
2433
  const FAILED_ROW = Symbol('deepline.failed_row');
2636
2434
  const COMPLETED_ROW = Symbol('deepline.completed_row');
2637
2435
  type MapRowExecutionResult =
2638
- typeof COMPLETED_ROW | typeof WAITING_ROW | typeof FAILED_ROW;
2436
+ | typeof COMPLETED_ROW
2437
+ | typeof WAITING_ROW
2438
+ | typeof FAILED_ROW;
2639
2439
 
2640
2440
  function normalizeFetchHeaders(
2641
2441
  headers: RequestInit['headers'],
@@ -4433,7 +4233,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4433
4233
  artifactTableNamespace: string;
4434
4234
  mapNodeId?: string | null;
4435
4235
  explicitKey?:
4436
- ((row: Record<string, unknown>, index: number) => string) | null;
4236
+ | ((row: Record<string, unknown>, index: number) => string)
4237
+ | null;
4437
4238
  }): MapExecutionScope {
4438
4239
  const mapInvocationId = `${input.logicalNamespace}:${this.mapInvocationIndex}`;
4439
4240
  this.mapInvocationIndex += 1;
@@ -4683,10 +4484,12 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4683
4484
  // run actually owns. The map/cohort publisher keeps the claim-free
4684
4485
  // batched write, where the pointer genuinely shares the canonical
4685
4486
  // result's single bounded request.
4686
- if (!(
4687
- this.#options.claimRuntimeStepReceipt ||
4688
- this.#options.claimRuntimeStepReceipts
4689
- )) {
4487
+ if (
4488
+ !(
4489
+ this.#options.claimRuntimeStepReceipt ||
4490
+ this.#options.claimRuntimeStepReceipts
4491
+ )
4492
+ ) {
4690
4493
  return;
4691
4494
  }
4692
4495
  const existing = await this.getRuntimeStepReceipts(
@@ -8470,7 +8273,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8470
8273
  _key: string,
8471
8274
  _items: PlayDatasetInput<T>,
8472
8275
  _input?:
8473
- MapFieldDefinition<T, Record<string, unknown>> | RuntimeStepProgram,
8276
+ | MapFieldDefinition<T, Record<string, unknown>>
8277
+ | RuntimeStepProgram,
8474
8278
  _options?: RuntimeDatasetOptions<T>,
8475
8279
  ): never {
8476
8280
  void _key;
@@ -8594,7 +8398,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8594
8398
 
8595
8399
  const userKeyOption = options?.key;
8596
8400
  let explicitKeyResolver:
8597
- ((row: Record<string, unknown>, index: number) => string) | null = null;
8401
+ | ((row: Record<string, unknown>, index: number) => string)
8402
+ | null = null;
8598
8403
  if (userKeyOption !== undefined) {
8599
8404
  explicitKeyResolver = (row, index) => {
8600
8405
  const inputRow = stripFieldOutputs(row);
@@ -15080,206 +14885,17 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15080
14885
  rowStore?.mapStallObserver?.fetchStarted(
15081
14886
  rowStore.mapStallResolverToken,
15082
14887
  );
15083
- const fetchDeadline: CtxFetchDeadline = {
15084
- startedAt: Date.now(),
15085
- headersMs: positiveTimeoutMs(
15086
- this.#options.ctxFetchTimeouts?.headersMs,
15087
- CTX_FETCH_HEADERS_TIMEOUT_MS,
15088
- ),
15089
- bodyMs: positiveTimeoutMs(
15090
- this.#options.ctxFetchTimeouts?.bodyMs,
15091
- CTX_FETCH_BODY_TIMEOUT_MS,
15092
- ),
15093
- totalMs: positiveTimeoutMs(
15094
- this.#options.ctxFetchTimeouts?.totalMs,
15095
- CTX_FETCH_TOTAL_TIMEOUT_MS,
15096
- ),
15097
- };
15098
- let response: Response | null = null;
15099
- let bodyText: string | null = null;
15100
14888
  try {
15101
- // Only replay a body whose bytes are stable. In particular, a
15102
- // consumed ReadableStream cannot be retried even with a write key.
15103
- const replayableBody =
15104
- init.body == null || typeof init.body === 'string';
15105
- const canRetryTransport =
15106
- replayableBody && ['GET', 'HEAD', 'OPTIONS'].includes(method);
15107
- const { safePublicFetch } = await loadSafeFetch();
15108
- let transportAttempt = 1;
15109
- let rateLimitAttempt = 0;
15110
- while (true) {
15111
- const attemptController = new AbortController();
15112
- let permit: Awaited<
15113
- ReturnType<RuntimeResourceGovernor['acquireProviderPermit']>
15114
- > | null = null;
15115
- let dispatched = false;
15116
- const releasePhysicalPermit = async () => {
15117
- const current = permit;
15118
- permit = null;
15119
- if (!current) return;
15120
- if (dispatched) current.release();
15121
- else await current.cancel();
15122
- dispatched = false;
15123
- };
15124
- let transportRetry = false;
15125
- try {
15126
- const attemptInit = {
15127
- ...fetchInit,
15128
- signal: init.signal
15129
- ? AbortSignal.any([init.signal, attemptController.signal])
15130
- : attemptController.signal,
15131
- };
15132
- response = await runCtxFetchPhase({
15133
- callerSignal: init.signal,
15134
- controller: attemptController,
15135
- deadline: fetchDeadline,
15136
- phase: 'total',
15137
- run: () =>
15138
- safePublicFetch(url, attemptInit, {
15139
- // safePublicFetch validates each URL before invoking this
15140
- // wrapper. Redirect hops are physical dispatches too.
15141
- fetchImpl: async (physicalUrl, physicalInit) => {
15142
- await releasePhysicalPermit();
15143
- const signal = attemptInit.signal;
15144
- permit = await runCtxFetchPhase({
15145
- callerSignal: init.signal,
15146
- controller: attemptController,
15147
- deadline: fetchDeadline,
15148
- phase: 'total',
15149
- run: async () => {
15150
- for (;;) {
15151
- const acquired =
15152
- await this.resourceGovernor.acquireProviderPermit(
15153
- { toolId: CTX_FETCH_EGRESS_TOOL_ID, signal },
15154
- );
15155
- try {
15156
- signal.throwIfAborted();
15157
- const valid =
15158
- !acquired.validate ||
15159
- (await acquired.validate());
15160
- signal.throwIfAborted();
15161
- if (valid) return acquired;
15162
- } catch (error) {
15163
- await acquired.cancel();
15164
- throw error;
15165
- }
15166
- await acquired.cancel();
15167
- }
15168
- },
15169
- });
15170
- return await runCtxFetchPhase({
15171
- callerSignal: init.signal,
15172
- controller: attemptController,
15173
- deadline: fetchDeadline,
15174
- phase: 'headers',
15175
- run: () => {
15176
- dispatched = true;
15177
- return (
15178
- this.#options.fetchImpl ?? globalThis.fetch
15179
- )(physicalUrl, physicalInit);
15180
- },
15181
- });
15182
- },
15183
- sensitiveHeaders: Object.keys(secretHeaderMarkers),
15184
- stripHeadersOnCrossOriginRedirect: true,
15185
- headersOnCrossOriginRedirect: {
15186
- 'user-agent': 'Deepline Play Runtime',
15187
- },
15188
- }),
15189
- });
15190
- bodyText = await readCtxFetchBody({
15191
- callerSignal: init.signal,
15192
- controller: attemptController,
15193
- deadline: fetchDeadline,
15194
- response,
15195
- });
15196
- } catch (error) {
15197
- void response?.body?.cancel(error).catch(() => undefined);
15198
- response = null;
15199
- bodyText = null;
15200
- if (error instanceof CtxFetchTimeoutError) throw error;
15201
- if (init.signal?.aborted) {
15202
- throw init.signal.reason ?? error;
15203
- }
15204
- if (isUnsafeOutboundUrlError(error)) {
15205
- throw error;
15206
- }
15207
- const message =
15208
- error instanceof Error ? error.message : String(error);
15209
- if (
15210
- canRetryTransport &&
15211
- transportAttempt < FETCH_TRANSPORT_MAX_ATTEMPTS
15212
- ) {
15213
- this.runtimeLog(
15214
- `ctx.fetch(${method} ${url}) transport failed on attempt ${transportAttempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}; retrying: ${message}`,
15215
- );
15216
- transportRetry = true;
15217
- } else {
15218
- throw new Error(
15219
- `ctx.fetch(${method} ${url}) failed on attempt ${transportAttempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}: ${message}`,
15220
- );
15221
- }
15222
- } finally {
15223
- await releasePhysicalPermit();
15224
- }
15225
- if (transportRetry) {
15226
- await sleepWithinCtxFetchDeadline({
15227
- callerSignal: init.signal,
15228
- deadline: fetchDeadline,
15229
- delayMs: FETCH_TRANSPORT_RETRY_DELAY_MS * transportAttempt,
15230
- });
15231
- transportAttempt += 1;
15232
- continue;
15233
- }
15234
- if (
15235
- !response ||
15236
- bodyText === null ||
15237
- !replayableBody ||
15238
- !isCtxFetchBackpressure(response, bodyText)
15239
- )
15240
- break;
15241
- rateLimitAttempt += 1;
15242
- const retryAfter = response.headers.get('retry-after');
15243
- const attemptCap = retryAfter?.trim()
15244
- ? TOOL_EXECUTE_RATE_LIMIT_MAX_ATTEMPTS
15245
- : TOOL_EXECUTE_BARE_RATE_LIMIT_MAX_ATTEMPTS;
15246
- if (rateLimitAttempt >= attemptCap) break;
15247
- const delayMs = Math.max(
15248
- parseToolExecuteRetryAfterMs(retryAfter),
15249
- Math.min(
15250
- TOOL_EXECUTE_RATE_LIMIT_RETRY_DELAY_MAX_MS,
15251
- TOOL_EXECUTE_RETRY_DELAY_FALLBACK_MS * rateLimitAttempt,
15252
- ),
15253
- );
15254
- this.runtimeLog(
15255
- `[runtime.ctx_fetch_backpressure] ${JSON.stringify({
15256
- key: normalizedKey,
15257
- method,
15258
- origin: new URL(response.url || url).origin,
15259
- http_status: response.status,
15260
- attempt: rateLimitAttempt,
15261
- max_attempts: attemptCap,
15262
- retry_delay_ms: delayMs,
15263
- })}`,
15264
- );
15265
- // Destination feedback must not cool the shared generic_http
15266
- // bucket used by unrelated websites. Pace this logical request.
15267
- await sleepWithinCtxFetchDeadline({
15268
- callerSignal: init.signal,
15269
- deadline: fetchDeadline,
15270
- delayMs,
15271
- });
15272
- }
15273
- if (!response) {
15274
- throw new Error(
15275
- `ctx.fetch(${method} ${url}) failed before receiving a response.`,
15276
- );
15277
- }
15278
- if (bodyText === null) {
15279
- throw new Error(
15280
- `ctx.fetch(${method} ${url}) failed while reading the response body.`,
15281
- );
15282
- }
14889
+ const { response, bodyText } = await sendPlayHttpRequest({
14890
+ key: normalizedKey,
14891
+ url,
14892
+ init: fetchInit,
14893
+ governor: this.resourceGovernor,
14894
+ sensitiveHeaders: Object.keys(secretHeaderMarkers),
14895
+ timeouts: this.#options.ctxFetchTimeouts,
14896
+ fetchImpl: this.#options.fetchImpl,
14897
+ log: (message) => this.runtimeLog(message),
14898
+ });
15283
14899
  const rawJson = parseJsonOrNull(bodyText);
15284
14900
  const redactedBodyText = this.secretRedactor.redactString(bodyText);
15285
14901
  const output: PlayFetchResponse = {
@@ -16685,8 +16301,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16685
16301
  },
16686
16302
  })),
16687
16303
  buildPhysicalCall: async ({ members }) => {
16688
- const owned = members.map((member) =>
16689
- batchRequestByCallId.get(member.memberId)!,
16304
+ const owned = members.map(
16305
+ (member) => batchRequestByCallId.get(member.memberId)!,
16690
16306
  );
16691
16307
  const compiled = compileOwnedPhysicalBatch(owned);
16692
16308
  const providerActionVersion =
@@ -19012,7 +18628,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19012
18628
  // running for.
19013
18629
  const activeRowScope = rowContext.getStore();
19014
18630
  const memoryLedgerRowScopes: readonly (
19015
- MemoryLedgerRowScope | null | undefined
18631
+ | MemoryLedgerRowScope
18632
+ | null
18633
+ | undefined
19016
18634
  )[] = options?.memoryLedgerRowScopes ?? [
19017
18635
  { rowKey: activeRowScope?.rowKey, fieldName: activeRowScope?.fieldName },
19018
18636
  ];
@@ -366,6 +366,7 @@ export function buildBootMachineConfig(input: {
366
366
  componentInputHash?: string;
367
367
  region?: string;
368
368
  rollbackHoldUntilMs?: number;
369
+ draining?: boolean;
369
370
  }): Record<string, unknown> {
370
371
  return {
371
372
  region: input.region ?? DEFAULT_ABSURD_FLY_REGION,
@@ -389,6 +390,7 @@ export function buildBootMachineConfig(input: {
389
390
  },
390
391
  env: {
391
392
  [RUNTIME_DEPLOY_VERSION_ENV]: input.releaseId,
393
+ ...(input.draining ? { [DRAIN_EXIT_ENV]: '1' } : {}),
392
394
  },
393
395
  // Override the image CMD so rollback and recovery of an older image still
394
396
  // honor today's fleet-wide slot ceiling. Every historical Absurd worker