deepline 0.3.228 → 0.3.230

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 (48) hide show
  1. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  2. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
  3. package/dist/bundling-sources/shared_libs/play-data-plane/contracts.ts +12 -0
  4. package/dist/bundling-sources/shared_libs/play-data-plane/index.ts +1 -0
  5. package/dist/bundling-sources/shared_libs/play-data-plane/sheet-column-storage.ts +252 -0
  6. package/dist/bundling-sources/shared_libs/play-data-plane/sheet-contract.ts +29 -8
  7. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +19 -0
  8. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +297 -69
  9. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +5 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +38 -2
  11. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +8 -3
  12. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +9 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +39 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +293 -4
  15. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +29 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +56 -11
  17. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/progress-sidecar.ts +1 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/local-process.ts +4 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +4 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +380 -183
  21. package/dist/bundling-sources/shared_libs/play-runtime/runtime-capacity-policy.ts +187 -14
  22. package/dist/bundling-sources/shared_libs/play-runtime/runtime-work-counters.ts +52 -2
  23. package/dist/bundling-sources/shared_libs/play-runtime/sandbox-runs/runner-backend-adapter.ts +5 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +30 -2
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +118 -6
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +135 -21
  27. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +1 -0
  28. package/dist/bundling-sources/shared_libs/plays/dataset.ts +16 -2
  29. package/dist/bundling-sources/shared_libs/plays/execution-pipeline.ts +72 -0
  30. package/dist/bundling-sources/shared_libs/plays/launch/prepare.ts +1 -3
  31. package/dist/bundling-sources/shared_libs/plays/sheet-filter.ts +9 -13
  32. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +28 -31
  33. package/dist/cli/index.js +102 -2
  34. package/dist/cli/index.mjs +102 -2
  35. package/dist/{compiler-manifest-Dpd2hEet.d.mts → compiler-manifest-n8ZtPv2h.d.mts} +31 -21
  36. package/dist/{compiler-manifest-Dpd2hEet.d.ts → compiler-manifest-n8ZtPv2h.d.ts} +31 -21
  37. package/dist/index.d.mts +2 -2
  38. package/dist/index.d.ts +2 -2
  39. package/dist/index.js +98 -2
  40. package/dist/index.mjs +98 -2
  41. package/dist/install-integrity.json +4 -2
  42. package/dist/plays/bundle-play-file.d.mts +2 -2
  43. package/dist/plays/bundle-play-file.d.ts +2 -2
  44. package/dist/release.d.mts +1 -1
  45. package/dist/release.d.ts +1 -1
  46. package/dist/release.js +1 -1
  47. package/dist/release.mjs +1 -1
  48. package/package.json +1 -1
@@ -26,6 +26,7 @@ import {
26
26
  serializePlayDatasetCell,
27
27
  } from '../plays/dataset';
28
28
  import type { PlayDataset, PlayDatasetInput } from '../plays/dataset';
29
+ import type { PlayDatasetColumnTypes } from '../play-data-plane/contracts';
29
30
  import {
30
31
  compileRequestsWithStrategy,
31
32
  executeChunkedRequests,
@@ -201,8 +202,13 @@ import {
201
202
  TOOL_EXECUTE_RUNNER_ACCEPTS_VALUE,
202
203
  TOOL_EXECUTE_PROVIDER_RATE_SCOPE_HEADER,
203
204
  TOOL_EXECUTE_PROVIDER_RATE_RETRY_SNAPSHOT_HEADER,
205
+ TOOL_EXECUTE_PROVIDER_THROTTLE_HEADER,
204
206
  classifyToolExecuteHttpFailure,
205
207
  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,
206
212
  parseToolExecuteRetryAfterMs,
207
213
  parseToolExecuteRetryAtMs,
208
214
  parseToolExecuteAuthScopeChangedError,
@@ -680,7 +686,7 @@ async function runCtxFetchPhase<T>(input: {
680
686
  callerSignal?: AbortSignal | null;
681
687
  controller: AbortController;
682
688
  deadline: CtxFetchDeadline;
683
- phase: 'headers' | 'body';
689
+ phase: 'headers' | 'body' | 'total';
684
690
  run: () => Promise<T>;
685
691
  }): Promise<T> {
686
692
  const remainingMs = remainingCtxFetchMs(input.deadline);
@@ -695,7 +701,9 @@ async function runCtxFetchPhase<T>(input: {
695
701
  const phaseLimitMs =
696
702
  input.phase === 'headers'
697
703
  ? input.deadline.headersMs
698
- : input.deadline.bodyMs;
704
+ : input.phase === 'body'
705
+ ? input.deadline.bodyMs
706
+ : remainingMs;
699
707
  const timeoutMs = Math.min(phaseLimitMs, remainingMs);
700
708
  const timeoutPhase = timeoutMs >= remainingMs ? 'total' : input.phase;
701
709
  let timeout: ReturnType<typeof setTimeout> | null = null;
@@ -744,19 +752,42 @@ async function sleepWithinCtxFetchDeadline(input: {
744
752
  delayMs: number;
745
753
  }): Promise<void> {
746
754
  const controller = new AbortController();
747
- await runCtxFetchPhase({
748
- callerSignal: input.callerSignal,
749
- controller,
750
- deadline: input.deadline,
751
- phase: 'headers',
752
- run: () =>
753
- new Promise<void>((resolve) => {
754
- setTimeout(
755
- resolve,
756
- Math.min(input.delayMs, remainingCtxFetchMs(input.deadline)),
757
- );
758
- }),
759
- });
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
+ }
760
791
  }
761
792
 
762
793
  async function readCtxFetchBody(input: {
@@ -1673,6 +1704,69 @@ function rowsFromUnknown(value: unknown): Record<string, unknown>[] {
1673
1704
  );
1674
1705
  }
1675
1706
 
1707
+ function customerDbColumnTypes(
1708
+ columns: unknown,
1709
+ rows: readonly Record<string, unknown>[],
1710
+ ): PlayDatasetColumnTypes | undefined {
1711
+ if (!Array.isArray(columns)) return undefined;
1712
+ // Core PostgreSQL OIDs only. Extension/domain identities are database-local,
1713
+ // and source SQL types never authorize coercing the values returned by a tool.
1714
+ const coreValueTypes: Record<
1715
+ number,
1716
+ PlayDatasetColumnTypes[string]['valueType']
1717
+ > = {
1718
+ 16: 'boolean',
1719
+ 20: 'string',
1720
+ 21: 'number',
1721
+ 23: 'number',
1722
+ 25: 'string',
1723
+ 114: 'json',
1724
+ 700: 'number',
1725
+ 701: 'number',
1726
+ 1042: 'string',
1727
+ 1043: 'string',
1728
+ 1082: 'string',
1729
+ 1083: 'string',
1730
+ 1114: 'string',
1731
+ 1184: 'string',
1732
+ 1266: 'string',
1733
+ 1700: 'string',
1734
+ 2950: 'string',
1735
+ 3802: 'json',
1736
+ };
1737
+ const entries: Array<[string, PlayDatasetColumnTypes[string]]> = [];
1738
+ for (const value of columns) {
1739
+ const column = recordOrNull(value);
1740
+ const postgresTypeId = column?.data_type_id;
1741
+ if (
1742
+ typeof column?.name !== 'string' ||
1743
+ typeof postgresTypeId !== 'number' ||
1744
+ !Object.hasOwn(coreValueTypes, postgresTypeId)
1745
+ )
1746
+ continue;
1747
+ const observed = new Set<PlayDatasetColumnTypes[string]['valueType']>();
1748
+ for (const row of rows) {
1749
+ const cell = row[column.name];
1750
+ if (cell === null || cell === undefined) continue;
1751
+ observed.add(
1752
+ typeof cell === 'boolean'
1753
+ ? 'boolean'
1754
+ : typeof cell === 'string'
1755
+ ? 'string'
1756
+ : typeof cell === 'number' && Number.isFinite(cell)
1757
+ ? 'number'
1758
+ : 'json',
1759
+ );
1760
+ }
1761
+ const valueType =
1762
+ postgresTypeId === 114 || postgresTypeId === 3802 || observed.size > 1
1763
+ ? 'json'
1764
+ : ([...observed][0] ?? coreValueTypes[postgresTypeId]!);
1765
+ entries.push([column.name, { postgresTypeId, valueType }]);
1766
+ }
1767
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
1768
+ }
1769
+
1676
1770
  function finiteNonNegativeInteger(value: unknown): number | null {
1677
1771
  if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
1678
1772
  return null;
@@ -2541,9 +2635,7 @@ const WAITING_ROW = Symbol('deepline.waiting_row');
2541
2635
  const FAILED_ROW = Symbol('deepline.failed_row');
2542
2636
  const COMPLETED_ROW = Symbol('deepline.completed_row');
2543
2637
  type MapRowExecutionResult =
2544
- | typeof COMPLETED_ROW
2545
- | typeof WAITING_ROW
2546
- | typeof FAILED_ROW;
2638
+ typeof COMPLETED_ROW | typeof WAITING_ROW | typeof FAILED_ROW;
2547
2639
 
2548
2640
  function normalizeFetchHeaders(
2549
2641
  headers: RequestInit['headers'],
@@ -4341,8 +4433,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4341
4433
  artifactTableNamespace: string;
4342
4434
  mapNodeId?: string | null;
4343
4435
  explicitKey?:
4344
- | ((row: Record<string, unknown>, index: number) => string)
4345
- | null;
4436
+ ((row: Record<string, unknown>, index: number) => string) | null;
4346
4437
  }): MapExecutionScope {
4347
4438
  const mapInvocationId = `${input.logicalNamespace}:${this.mapInvocationIndex}`;
4348
4439
  this.mapInvocationIndex += 1;
@@ -4592,12 +4683,10 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
4592
4683
  // run actually owns. The map/cohort publisher keeps the claim-free
4593
4684
  // batched write, where the pointer genuinely shares the canonical
4594
4685
  // result's single bounded request.
4595
- if (
4596
- !(
4597
- this.#options.claimRuntimeStepReceipt ||
4598
- this.#options.claimRuntimeStepReceipts
4599
- )
4600
- ) {
4686
+ if (!(
4687
+ this.#options.claimRuntimeStepReceipt ||
4688
+ this.#options.claimRuntimeStepReceipts
4689
+ )) {
4601
4690
  return;
4602
4691
  }
4603
4692
  const existing = await this.getRuntimeStepReceipts(
@@ -7914,6 +8003,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
7914
8003
  rows.length >= effectiveCount ? rows.slice(0, effectiveCount) : null,
7915
8004
  sourceLabel: 'query result rows',
7916
8005
  tableNamespace: null,
8006
+ columnTypes: isCustomerDbDatasetTool(toolId)
8007
+ ? customerDbColumnTypes(raw?.columns, rows)
8008
+ : undefined,
7917
8009
  resolvers: {
7918
8010
  count: async () => Math.min(totalRows, datasetLimit),
7919
8011
  at: async (index) => (await fetchPage(index, 1))[0],
@@ -8378,8 +8470,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8378
8470
  _key: string,
8379
8471
  _items: PlayDatasetInput<T>,
8380
8472
  _input?:
8381
- | MapFieldDefinition<T, Record<string, unknown>>
8382
- | RuntimeStepProgram,
8473
+ MapFieldDefinition<T, Record<string, unknown>> | RuntimeStepProgram,
8383
8474
  _options?: RuntimeDatasetOptions<T>,
8384
8475
  ): never {
8385
8476
  void _key;
@@ -8472,6 +8563,18 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8472
8563
  let itemsToProcess: Array<Record<string, unknown>> = [];
8473
8564
  let itemOriginalIndexes: number[] = [];
8474
8565
  const datasetColumnNames = Object.keys(input);
8566
+ // Generated fields no longer describe the source column, even when they
8567
+ // retain its name. Every writer still validates the actual incoming values.
8568
+ const inputColumnTypes = isPlayDataset(items)
8569
+ ? items.columnTypes
8570
+ : undefined;
8571
+ const unchangedColumnTypes = inputColumnTypes
8572
+ ? Object.fromEntries(
8573
+ Object.entries(inputColumnTypes).filter(
8574
+ ([field]) => !datasetColumnNames.includes(field),
8575
+ ),
8576
+ )
8577
+ : undefined;
8475
8578
  const stripFieldOutputs = (row: Record<string, unknown>) => {
8476
8579
  const stripped = cloneCsvAliasedRow(row);
8477
8580
  for (const fieldName of datasetColumnNames) {
@@ -8491,8 +8594,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8491
8594
 
8492
8595
  const userKeyOption = options?.key;
8493
8596
  let explicitKeyResolver:
8494
- | ((row: Record<string, unknown>, index: number) => string)
8495
- | null = null;
8597
+ ((row: Record<string, unknown>, index: number) => string) | null = null;
8496
8598
  if (userKeyOption !== undefined) {
8497
8599
  explicitKeyResolver = (row, index) => {
8498
8600
  const inputRow = stripFieldOutputs(row);
@@ -8863,6 +8965,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
8863
8965
  forceRefresh: this.#options.cachePolicy?.forceToolRefresh === true,
8864
8966
  inputOffset: page.offset,
8865
8967
  mode: options?.mode,
8968
+ columnTypes: unchangedColumnTypes,
8866
8969
  },
8867
8970
  );
8868
8971
  resolvedTableNamespace = normalizeTableNamespace(
@@ -9424,6 +9527,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9424
9527
  ? immediateMaterializedRows
9425
9528
  : null,
9426
9529
  tableNamespace: resolvedTableNamespace,
9530
+ columnTypes: unchangedColumnTypes,
9427
9531
  workProgress: {
9428
9532
  total: totalInputCount,
9429
9533
  executed: executedCount,
@@ -9666,6 +9770,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
9666
9770
  staticPipeline: this.currentStaticPipeline,
9667
9771
  forceRefresh: this.#options.cachePolicy?.forceToolRefresh === true,
9668
9772
  mode: options?.mode,
9773
+ columnTypes: unchangedColumnTypes,
9669
9774
  },
9670
9775
  );
9671
9776
  resolvedTableNamespace = normalizeTableNamespace(
@@ -10175,6 +10280,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
10175
10280
  .map((row) => this.toMaterializedOutputRow(row)),
10176
10281
  residentRows: terminalRows,
10177
10282
  tableNamespace: resolvedTableNamespace,
10283
+ columnTypes: unchangedColumnTypes,
10178
10284
  workProgress: {
10179
10285
  total: totalInputCount,
10180
10286
  executed: executedCount,
@@ -12895,6 +13001,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
12895
13001
  const callIndex = this.toolCallIndexByKey.get(callIndexKey) ?? 0;
12896
13002
  this.toolCallIndexByKey.set(callIndexKey, callIndex + 1);
12897
13003
  const logicalCallId = stableDigest(`${callIndexKey}:${callIndex}`);
13004
+ this.resourceGovernor.recordLogicalToolCall();
12898
13005
  let executionAuthScopeDigest =
12899
13006
  (await this.resolveToolAuthScopeDigest(toolId))?.trim() ?? null;
12900
13007
  const eventWaitHandler =
@@ -14991,16 +15098,30 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14991
15098
  let response: Response | null = null;
14992
15099
  let bodyText: string | null = null;
14993
15100
  try {
14994
- const canRetryTransport = ['GET', 'HEAD', 'OPTIONS'].includes(
14995
- method,
14996
- );
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);
14997
15107
  const { safePublicFetch } = await loadSafeFetch();
14998
- for (
14999
- let attempt = 1;
15000
- attempt <= FETCH_TRANSPORT_MAX_ATTEMPTS;
15001
- attempt += 1
15002
- ) {
15108
+ let transportAttempt = 1;
15109
+ let rateLimitAttempt = 0;
15110
+ while (true) {
15003
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;
15004
15125
  try {
15005
15126
  const attemptInit = {
15006
15127
  ...fetchInit,
@@ -15012,10 +15133,53 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15012
15133
  callerSignal: init.signal,
15013
15134
  controller: attemptController,
15014
15135
  deadline: fetchDeadline,
15015
- phase: 'headers',
15136
+ phase: 'total',
15016
15137
  run: () =>
15017
15138
  safePublicFetch(url, attemptInit, {
15018
- fetchImpl: this.#options.fetchImpl,
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
+ },
15019
15183
  sensitiveHeaders: Object.keys(secretHeaderMarkers),
15020
15184
  stripHeadersOnCrossOriginRedirect: true,
15021
15185
  headersOnCrossOriginRedirect: {
@@ -15029,7 +15193,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15029
15193
  deadline: fetchDeadline,
15030
15194
  response,
15031
15195
  });
15032
- break;
15033
15196
  } catch (error) {
15034
15197
  void response?.body?.cancel(error).catch(() => undefined);
15035
15198
  response = null;
@@ -15045,22 +15208,67 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15045
15208
  error instanceof Error ? error.message : String(error);
15046
15209
  if (
15047
15210
  canRetryTransport &&
15048
- attempt < FETCH_TRANSPORT_MAX_ATTEMPTS
15211
+ transportAttempt < FETCH_TRANSPORT_MAX_ATTEMPTS
15049
15212
  ) {
15050
15213
  this.runtimeLog(
15051
- `ctx.fetch(${method} ${url}) transport failed on attempt ${attempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}; retrying: ${message}`,
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}`,
15052
15220
  );
15053
- await sleepWithinCtxFetchDeadline({
15054
- callerSignal: init.signal,
15055
- deadline: fetchDeadline,
15056
- delayMs: FETCH_TRANSPORT_RETRY_DELAY_MS * attempt,
15057
- });
15058
- continue;
15059
15221
  }
15060
- throw new Error(
15061
- `ctx.fetch(${method} ${url}) failed on attempt ${attempt}/${FETCH_TRANSPORT_MAX_ATTEMPTS}: ${message}`,
15062
- );
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;
15063
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
+ });
15064
15272
  }
15065
15273
  if (!response) {
15066
15274
  throw new Error(
@@ -15283,6 +15491,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15283
15491
  // Terminal logs are intentionally bounded; keep the small aggregate in
15284
15492
  // stats so every retained terminal snapshot can explain queueing.
15285
15493
  runtimeResources: this.getRuntimeResourceSnapshot(),
15494
+ runtimeWorkCounters: this.getRuntimeWorkCounters(),
15286
15495
  };
15287
15496
  }
15288
15497
 
@@ -15291,6 +15500,15 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
15291
15500
  return this.resourceGovernor.snapshot();
15292
15501
  }
15293
15502
 
15503
+ /** Attempt-local counts without serializing the diagnostic resource map. */
15504
+ getRuntimeWorkCounters(): {
15505
+ logicalCalls: number;
15506
+ providerRequests: number;
15507
+ provider429s: number;
15508
+ } {
15509
+ return this.resourceGovernor.workCounters();
15510
+ }
15511
+
15294
15512
  /**
15295
15513
  * Record what one receipt gateway response said about itself.
15296
15514
  *
@@ -16467,8 +16685,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
16467
16685
  },
16468
16686
  })),
16469
16687
  buildPhysicalCall: async ({ members }) => {
16470
- const owned = members.map(
16471
- (member) => batchRequestByCallId.get(member.memberId)!,
16688
+ const owned = members.map((member) =>
16689
+ batchRequestByCallId.get(member.memberId)!,
16472
16690
  );
16473
16691
  const compiled = compileOwnedPhysicalBatch(owned);
16474
16692
  const providerActionVersion =
@@ -18794,9 +19012,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
18794
19012
  // running for.
18795
19013
  const activeRowScope = rowContext.getStore();
18796
19014
  const memoryLedgerRowScopes: readonly (
18797
- | MemoryLedgerRowScope
18798
- | null
18799
- | undefined
19015
+ MemoryLedgerRowScope | null | undefined
18800
19016
  )[] = options?.memoryLedgerRowScopes ?? [
18801
19017
  { rowKey: activeRowScope?.rowKey, fieldName: activeRowScope?.fieldName },
18802
19018
  ];
@@ -19639,6 +19855,9 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19639
19855
  logProvider429RetryPhase('http_dispatch_started');
19640
19856
  activeProviderRequest =
19641
19857
  this.beginProviderRequest(provider);
19858
+ // Count at dispatch, including ambiguous transport
19859
+ // failures that never produce a residence sample.
19860
+ this.resourceGovernor.recordProviderDispatch();
19642
19861
  const dispatchedResponse = await fetch(url, {
19643
19862
  method: 'POST',
19644
19863
  signal: abortController?.signal,
@@ -19694,21 +19913,13 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19694
19913
  // it has been read and verified below.
19695
19914
  response = dispatchedResponse;
19696
19915
  if (dispatchedResponse.status === 429) {
19697
- provider429RetryDiagnosticStartedAt =
19698
- performance.now();
19699
- provider429RetryDiagnosticNextInvocationAttempt =
19700
- invocationAttempt + 1;
19701
- logProvider429RetryPhase(
19702
- 'http_429_response_received',
19703
- );
19704
- providerRequestsInFlightAt429 =
19705
- activeProviderRequest?.inFlightCount() ?? null;
19706
19916
  const observedAt = Date.now();
19707
19917
  const retryAfter =
19708
19918
  dispatchedResponse.headers.get('retry-after');
19709
- // Without a provider deadline the classifier owns
19710
- // the conservative bare-429 cooldown. Do not replace
19711
- // it with the generic one-second HTTP retry fallback.
19919
+ // Preserve absolute provider deadlines even when the
19920
+ // response did not carry the gateway's counter
19921
+ // attestation. Classification below still decides
19922
+ // whether this 429 may update shared provider pacing.
19712
19923
  providerRetryAtMs = retryAfter?.trim()
19713
19924
  ? parseToolExecuteRetryAtMs(
19714
19925
  retryAfter,
@@ -19716,6 +19927,23 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19716
19927
  )
19717
19928
  : undefined;
19718
19929
  }
19930
+ if (
19931
+ dispatchedResponse.status === 429 &&
19932
+ dispatchedResponse.headers.get(
19933
+ TOOL_EXECUTE_PROVIDER_THROTTLE_HEADER,
19934
+ ) === '1'
19935
+ ) {
19936
+ this.resourceGovernor.recordProvider429();
19937
+ provider429RetryDiagnosticStartedAt =
19938
+ performance.now();
19939
+ provider429RetryDiagnosticNextInvocationAttempt =
19940
+ invocationAttempt + 1;
19941
+ logProvider429RetryPhase(
19942
+ 'http_429_response_received',
19943
+ );
19944
+ providerRequestsInFlightAt429 =
19945
+ activeProviderRequest?.inFlightCount() ?? null;
19946
+ }
19719
19947
  const dispatchedFetchToHeadersMs =
19720
19948
  Date.now() - fetchedAt;
19721
19949
  // The response writer owns these named wall times.
@@ -7,6 +7,7 @@ import type {
7
7
  } from './cell-provenance';
8
8
  import type { PlayBundleArtifact } from '../plays/artifact-types';
9
9
  import type { PlayRunContractSnapshot } from '../plays/contracts';
10
+ import type { PlayDatasetColumnTypes } from '../play-data-plane/contracts';
10
11
  import type { PlayStructuredDefinition } from '../plays/definition';
11
12
  import type { PlayStaticPipeline } from '../plays/static-pipeline';
12
13
  import type {
@@ -891,6 +892,7 @@ export interface ContextOptions {
891
892
  forceRefresh?: boolean;
892
893
  inputOffset?: number;
893
894
  mode?: RuntimeDatasetRunOptions['mode'];
895
+ columnTypes?: PlayDatasetColumnTypes;
894
896
  },
895
897
  ) => Promise<MapStartResult>;
896
898
  /**
@@ -1169,6 +1171,9 @@ export interface ContextOptions {
1169
1171
  }
1170
1172
 
1171
1173
  export interface PlayCheckpoint {
1174
+ /** Last completed runner invocation within the current scheduler attempt. */
1175
+ workCounterInvocationAttempt?: number;
1176
+ workCounterInvocationSeq?: number;
1172
1177
  /**
1173
1178
  * Clock captured when this logical run first creates its context. All
1174
1179
  * staleAfterSeconds receipt buckets use this value across durable resumes.
@@ -566,6 +566,7 @@ export class FlyMachinesClient {
566
566
  method: string,
567
567
  path: string,
568
568
  body?: unknown,
569
+ leaseNonce?: string,
569
570
  ): Promise<unknown> {
570
571
  const retryDelaysMs =
571
572
  this.rateLimitOptions.retryDelaysMs ??
@@ -585,6 +586,7 @@ export class FlyMachinesClient {
585
586
  headers: {
586
587
  authorization: `Bearer ${this.token}`,
587
588
  'content-type': 'application/json',
589
+ ...(leaseNonce ? { 'fly-machine-lease-nonce': leaseNonce } : {}),
588
590
  },
589
591
  body: body === undefined ? undefined : JSON.stringify(body),
590
592
  signal,
@@ -689,10 +691,43 @@ export class FlyMachinesClient {
689
691
  );
690
692
  }
691
693
 
692
- async deleteMachine(id: string, force: boolean): Promise<void> {
694
+ /** Hold exclusive provider ownership while rechecking and reclaiming a
695
+ * stopped Machine. A concurrent rollback cannot update its hold or restart
696
+ * it until this lease is released or expires. */
697
+ async acquireMachineLease(
698
+ id: string,
699
+ options: { ttl: number; description: string },
700
+ ): Promise<string> {
701
+ const result = await this.request('POST', `/machines/${id}/lease`, options);
702
+ const nonce =
703
+ result &&
704
+ typeof result === 'object' &&
705
+ 'data' in result &&
706
+ result.data &&
707
+ typeof result.data === 'object' &&
708
+ 'nonce' in result.data
709
+ ? result.data.nonce
710
+ : undefined;
711
+ if (typeof nonce !== 'string' || nonce.trim() === '') {
712
+ throw new Error('Fly Machines API did not return a lease nonce.');
713
+ }
714
+ return nonce;
715
+ }
716
+
717
+ async releaseMachineLease(id: string, nonce: string): Promise<void> {
718
+ await this.request('DELETE', `/machines/${id}/lease`, undefined, nonce);
719
+ }
720
+
721
+ async deleteMachine(
722
+ id: string,
723
+ force: boolean,
724
+ leaseNonce?: string,
725
+ ): Promise<void> {
693
726
  await this.request(
694
727
  'DELETE',
695
728
  `/machines/${id}?force=${force ? 'true' : 'false'}`,
729
+ undefined,
730
+ leaseNonce,
696
731
  );
697
732
  }
698
733
 
@@ -718,8 +753,9 @@ export class FlyMachinesClient {
718
753
  async stopMachine(
719
754
  id: string,
720
755
  options?: { signal?: string; timeout?: string },
756
+ leaseNonce?: string,
721
757
  ): Promise<void> {
722
- await this.request('POST', `/machines/${id}/stop`, options);
758
+ await this.request('POST', `/machines/${id}/stop`, options, leaseNonce);
723
759
  }
724
760
 
725
761
  async machinesInLane(releaseId: string): Promise<FlyMachine[]> {