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
@@ -18,6 +18,7 @@ import {
18
18
  isPlayDataset,
19
19
  iteratePlayDatasetInputPages,
20
20
  materializePlayDatasetInput,
21
+ resolveMaterializeLimitCap,
21
22
  } from '@shared_libs/plays/dataset';
22
23
  import type { PlayDataset, PlayDatasetInput } from '@shared_libs/plays/dataset';
23
24
  import {
@@ -107,7 +108,7 @@ import {
107
108
  } from './tool-execute-retry-policy';
108
109
  import {
109
110
  buildDurableCtxCallCacheKey,
110
- buildDurableRunPlaySemanticKey,
111
+ buildDurableRunPlayInvocationScope,
111
112
  buildDurableToolAggregateProviderIdempotencyKey,
112
113
  buildDurableToolAggregateReceiptKey,
113
114
  buildDurableToolCallAuthScopeDigest,
@@ -261,6 +262,7 @@ const rowContext = new AsyncLocalStorage<{
261
262
  rowKey?: string;
262
263
  mapScope?: MapExecutionScope;
263
264
  }>();
265
+ const toolExecutionOverrides = new AsyncLocalStorage<{ force: boolean }>();
264
266
  const PROGRESS_HEARTBEAT_INTERVAL_MS = 1_000;
265
267
  const PURE_JS_HEARTBEAT_ROW_INTERVAL = 250;
266
268
  /**
@@ -819,7 +821,112 @@ function stableDigest(value: string): string {
819
821
  return sha256Hex(value);
820
822
  }
821
823
 
822
- type DurableCtxOperation = 'step' | 'tool' | 'fetch' | 'runPlay';
824
+ // Receipt keys contain canonicalized tool inputs. Runtime diagnostics must never
825
+ // emit them directly; a short digest is enough to correlate gateway, runner,
826
+ // and waiter observations for one failing run.
827
+ function runtimeReceiptKeyDigest(key: string): string {
828
+ return stableDigest(key).slice(0, 12);
829
+ }
830
+
831
+ function runtimeReceiptReadSummary(input: {
832
+ requested: readonly string[];
833
+ receipts: readonly (RuntimeStepReceipt | null | undefined)[];
834
+ resolved: ReadonlyMap<string, RuntimeStepReceipt>;
835
+ }): string {
836
+ const statuses = input.receipts.reduce<Record<string, number>>(
837
+ (counts, receipt) => {
838
+ const status = receipt?.status ?? 'missing';
839
+ counts[status] = (counts[status] ?? 0) + 1;
840
+ return counts;
841
+ },
842
+ {},
843
+ );
844
+ const positionalMismatches = input.receipts.reduce(
845
+ (count, receipt, index) =>
846
+ receipt && receipt.key.trim() !== (input.requested[index] ?? '').trim()
847
+ ? count + 1
848
+ : count,
849
+ 0,
850
+ );
851
+ const missingRequested = input.requested.filter(
852
+ (key) => !input.resolved.has(key),
853
+ );
854
+ return (
855
+ `requested=${input.requested.length} returned=${input.receipts.length} ` +
856
+ `resolved=${input.resolved.size} positional_mismatches=${positionalMismatches} ` +
857
+ `request_digests=${input.requested.map(runtimeReceiptKeyDigest).join(',')} ` +
858
+ `returned_digests=${input.receipts
859
+ .map((receipt) =>
860
+ receipt ? runtimeReceiptKeyDigest(receipt.key) : 'null',
861
+ )
862
+ .join(',')} ` +
863
+ `missing_digests=${missingRequested
864
+ .map(runtimeReceiptKeyDigest)
865
+ .join(',')} ` +
866
+ `statuses=${Object.entries(statuses)
867
+ .map(([status, count]) => `${status}:${count}`)
868
+ .join(',')}`
869
+ );
870
+ }
871
+
872
+ const runtimeReceiptReadTraceEnabled =
873
+ process.env.DEEPLINE_RUNTIME_RECEIPT_TRACE === '1';
874
+
875
+ function runtimeReceiptReadTrace(input: {
876
+ keys: readonly string[];
877
+ receipts: Array<RuntimeStepReceipt | null>;
878
+ byKey: ReadonlyMap<string, RuntimeStepReceipt>;
879
+ }): string | null {
880
+ if (!runtimeReceiptReadTraceEnabled) return null;
881
+ const digest = (value: string) => stableDigest(value).slice(0, 16);
882
+ const statusCounts = input.receipts.reduce<Record<string, number>>(
883
+ (counts, receipt) => {
884
+ if (receipt) {
885
+ counts[receipt.status] = (counts[receipt.status] ?? 0) + 1;
886
+ }
887
+ return counts;
888
+ },
889
+ {},
890
+ );
891
+ const expiredInFlightCount = input.receipts.filter((receipt) => {
892
+ if (
893
+ receipt?.status !== 'queued' &&
894
+ receipt?.status !== 'pending' &&
895
+ receipt?.status !== 'running'
896
+ ) {
897
+ return false;
898
+ }
899
+ const expiresAt = receipt.leaseExpiresAt
900
+ ? Date.parse(receipt.leaseExpiresAt)
901
+ : Number.NaN;
902
+ return Number.isFinite(expiresAt) && expiresAt <= Date.now();
903
+ }).length;
904
+ return (
905
+ '[runtime-receipt-context.read-trace] ' +
906
+ JSON.stringify({
907
+ requestedCount: input.keys.length,
908
+ returnedCount: input.receipts.filter(Boolean).length,
909
+ mapSize: input.byKey.size,
910
+ statusCounts,
911
+ expiredInFlightCount,
912
+ requestedOrderDigest: digest(input.keys.join('\u0000')),
913
+ returnedOrderDigest: digest(
914
+ input.receipts
915
+ .map((receipt) =>
916
+ receipt ? `${digest(receipt.key)}:${receipt.status}` : '-',
917
+ )
918
+ .join('\u0000'),
919
+ ),
920
+ mapOrderDigest: digest(
921
+ [...input.byKey.entries()]
922
+ .map(([key, receipt]) => `${digest(key)}:${receipt.status}`)
923
+ .join('\u0000'),
924
+ ),
925
+ })
926
+ );
927
+ }
928
+
929
+ type DurableCtxOperation = 'step' | 'tool' | 'fetch';
823
930
 
824
931
  function durableCtxKey(input: {
825
932
  orgId?: string | null;
@@ -1176,12 +1283,15 @@ export class PlayContextImpl {
1176
1283
  );
1177
1284
  }
1178
1285
  assertNoSecretTaint(request.input, 'ctx.tools.execute input');
1286
+ const force =
1287
+ request.force === true ||
1288
+ toolExecutionOverrides.getStore()?.force === true;
1179
1289
  return this.executeTool(
1180
1290
  request.id.trim(),
1181
1291
  request.tool,
1182
1292
  request.input,
1183
1293
  request.description ||
1184
- request.force === true ||
1294
+ force ||
1185
1295
  request.staleAfterSeconds !== undefined ||
1186
1296
  request.timeoutMs !== undefined ||
1187
1297
  request.receiptWaitMs !== undefined
@@ -1189,7 +1299,7 @@ export class PlayContextImpl {
1189
1299
  ...(request.description
1190
1300
  ? { description: request.description }
1191
1301
  : {}),
1192
- ...(request.force === true ? { force: true } : {}),
1302
+ ...(force ? { force: true } : {}),
1193
1303
  ...(request.staleAfterSeconds !== undefined
1194
1304
  ? { staleAfterSeconds: request.staleAfterSeconds }
1195
1305
  : {}),
@@ -1204,6 +1314,10 @@ export class PlayContextImpl {
1204
1314
  ) as Promise<TOutput>;
1205
1315
  },
1206
1316
  };
1317
+
1318
+ async __deeplineRunWithForcedTools<T>(run: () => Promise<T>): Promise<T> {
1319
+ return await toolExecutionOverrides.run({ force: true }, run);
1320
+ }
1207
1321
  readonly customerDb = {
1208
1322
  query: async <TRow extends object = Record<string, unknown>>(
1209
1323
  statement:
@@ -2161,6 +2275,21 @@ export class PlayContextImpl {
2161
2275
  );
2162
2276
  if (normalized) byKey.set(normalized.key, normalized);
2163
2277
  }
2278
+ if (runtimeReceiptReadTraceEnabled) {
2279
+ this.log(
2280
+ `[runtime-receipt-normalize] ${runtimeReceiptReadSummary({
2281
+ requested: uniqueKeys,
2282
+ receipts,
2283
+ resolved: byKey,
2284
+ })}`,
2285
+ );
2286
+ const trace = runtimeReceiptReadTrace({
2287
+ keys: uniqueKeys,
2288
+ receipts,
2289
+ byKey,
2290
+ });
2291
+ if (trace) this.log(trace);
2292
+ }
2164
2293
  return byKey;
2165
2294
  }
2166
2295
 
@@ -2207,6 +2336,16 @@ export class PlayContextImpl {
2207
2336
  );
2208
2337
  if (normalized) byKey.set(normalized.key, normalized);
2209
2338
  }
2339
+ if (runtimeReceiptReadTraceEnabled) {
2340
+ this.log(
2341
+ `[runtime-receipt-claim-normalize] reclaim_running=${reclaimRunning} force_refresh=${forceRefresh} force_failed_refresh=${forceFailedRefresh} ` +
2342
+ runtimeReceiptReadSummary({
2343
+ requested: uniqueKeys,
2344
+ receipts,
2345
+ resolved: byKey,
2346
+ }),
2347
+ );
2348
+ }
2210
2349
  return byKey;
2211
2350
  }
2212
2351
 
@@ -2667,6 +2806,7 @@ export class PlayContextImpl {
2667
2806
  receiptKeys: keys,
2668
2807
  store: this.durableReceiptExecutionStore(),
2669
2808
  maxAttempts,
2809
+ log: (message) => this.log(message),
2670
2810
  });
2671
2811
  }
2672
2812
 
@@ -3755,6 +3895,13 @@ export class PlayContextImpl {
3755
3895
  let duplicateReuseCount = 0;
3756
3896
  let executedCount = 0;
3757
3897
  const previewRows: Record<string, unknown>[] = [];
3898
+ // A bounded dataset may be materialized by authored code immediately
3899
+ // after run(). Keep the rows produced by this exact awaited execution so
3900
+ // that read-after-write does not depend on a separate sheet read becoming
3901
+ // visible. Datasets above the public materialize cap remain fully
3902
+ // streaming and sheet-backed.
3903
+ const materializedResultRowLimit = resolveMaterializeLimitCap();
3904
+ const immediateMaterializedRows: Record<string, unknown>[] = [];
3758
3905
 
3759
3906
  const persistMapRows = async (rows: PersistableMapRow[]) => {
3760
3907
  if (!this.#options.onMapRowsCompleted || rows.length === 0) {
@@ -4038,8 +4185,11 @@ export class PlayContextImpl {
4038
4185
  this.activeMapCellMeta = null;
4039
4186
 
4040
4187
  for (const row of mapResult.completedRows) {
4041
- if (previewRows.length >= 5) break;
4042
- previewRows.push(this.toMaterializedOutputRow(row.data));
4188
+ const materializedRow = this.toMaterializedOutputRow(row.data);
4189
+ if (previewRows.length < 5) previewRows.push(materializedRow);
4190
+ if (immediateMaterializedRows.length < materializedResultRowLimit) {
4191
+ immediateMaterializedRows.push(materializedRow);
4192
+ }
4043
4193
  }
4044
4194
  executedCount += rowsToExecute.length;
4045
4195
  successfulCount += mapResult.completedRows.length;
@@ -4100,6 +4250,42 @@ export class PlayContextImpl {
4100
4250
  return result.rows.map((row) => this.toMaterializedOutputRow(row));
4101
4251
  };
4102
4252
  const materializeRuntimeBackedMapRows = async (limit?: number) => {
4253
+ if (limit !== undefined && limit <= 0) return [];
4254
+ const availableResultRows = Math.min(
4255
+ successfulCount,
4256
+ materializedResultRowLimit,
4257
+ );
4258
+ if (
4259
+ immediateMaterializedRows.length === availableResultRows &&
4260
+ (limit !== undefined || successfulCount <= materializedResultRowLimit)
4261
+ ) {
4262
+ return limit === undefined
4263
+ ? immediateMaterializedRows.slice()
4264
+ : immediateMaterializedRows.slice(0, limit);
4265
+ }
4266
+ const pageSize = 1000;
4267
+ const materialized: Record<string, unknown>[] = [];
4268
+ let offset = 0;
4269
+ while (true) {
4270
+ const remaining =
4271
+ limit === undefined
4272
+ ? pageSize
4273
+ : Math.max(0, limit - materialized.length);
4274
+ if (remaining === 0) break;
4275
+ const rows = await readRuntimeBackedMapRows({
4276
+ limit: Math.min(pageSize, remaining),
4277
+ offset,
4278
+ });
4279
+ if (rows.length === 0) break;
4280
+ materialized.push(...rows);
4281
+ if (limit !== undefined && materialized.length >= limit) {
4282
+ return materialized.slice(0, limit);
4283
+ }
4284
+ offset += rows.length;
4285
+ }
4286
+ return materialized;
4287
+ };
4288
+ const materializeFullPersistedMapRows = async (limit?: number) => {
4103
4289
  if (limit !== undefined && limit <= 0) return [];
4104
4290
  const pageSize = 1000;
4105
4291
  const materialized: Record<string, unknown>[] = [];
@@ -4156,14 +4342,22 @@ export class PlayContextImpl {
4156
4342
  peek: async (limit) =>
4157
4343
  limit <= 0
4158
4344
  ? []
4159
- : await readRuntimeBackedMapRows({
4160
- limit,
4161
- offset: 0,
4162
- }),
4345
+ : immediateMaterializedRows.length >=
4346
+ Math.min(limit, successfulCount)
4347
+ ? immediateMaterializedRows.slice(0, limit)
4348
+ : await readRuntimeBackedMapRows({
4349
+ limit,
4350
+ offset: 0,
4351
+ }),
4163
4352
  materialize: materializeRuntimeBackedMapRows,
4353
+ materializeFullPersistedDataset: materializeFullPersistedMapRows,
4164
4354
  iterate: () =>
4165
4355
  ({
4166
4356
  async *[Symbol.asyncIterator]() {
4357
+ if (immediateMaterializedRows.length === successfulCount) {
4358
+ for (const row of immediateMaterializedRows) yield row;
4359
+ return;
4360
+ }
4167
4361
  const pageSize = 1000;
4168
4362
  let offset = 0;
4169
4363
  while (true) {
@@ -4598,39 +4792,33 @@ export class PlayContextImpl {
4598
4792
  });
4599
4793
  }
4600
4794
 
4601
- const readRuntimeBackedMapRows =
4795
+ // This map's terminal rows have already crossed the persistence barrier
4796
+ // above. Keep that verified snapshot as the authored return value instead
4797
+ // of immediately re-reading the Runtime Sheet projection. A second read
4798
+ // can observe a lagging/dynamically incomplete physical projection even
4799
+ // though the terminal write succeeded (notably for inline-child object
4800
+ // outputs on Absurd), making `materialize()` disagree with durable truth.
4801
+ // Neon remains the backing for later API/export reads of this handle.
4802
+ const terminalRows = results.map((row) =>
4803
+ this.toMaterializedOutputRow(row),
4804
+ );
4805
+ const materializeTerminalRows = async (limit?: number) =>
4806
+ limit === undefined
4807
+ ? terminalRows.slice()
4808
+ : terminalRows.slice(0, Math.max(0, limit));
4809
+ const runtimeSheetBacked =
4602
4810
  this.#options.runtimeSheetBackedMapDatasets === true &&
4603
- this.#options.baseUrl &&
4604
- this.#options.executorToken &&
4605
- this.#options.playName &&
4606
- this.#options.runId
4607
- ? async (input: { limit: number; offset: number }) => {
4608
- const result = await readRuntimeSheetDatasetRows(
4609
- {
4610
- baseUrl: this.#options.baseUrl!,
4611
- executorToken: this.#options.executorToken!,
4612
- dbSessionStrategy: this.#options.dbSessionStrategy,
4613
- playName: this.#options.playName!,
4614
- userEmail: this.#options.userEmail,
4615
- runId: this.#options.runId!,
4616
- },
4617
- {
4618
- tableNamespace: resolvedTableNamespace,
4619
- runId: this.#options.runId!,
4620
- limit: input.limit,
4621
- offset: input.offset,
4622
- },
4623
- );
4624
- return result.rows.map((row) => this.toMaterializedOutputRow(row));
4625
- }
4626
- : null;
4627
- const materializeRuntimeBackedMapRows = async (limit?: number) => {
4628
- if (!readRuntimeBackedMapRows) {
4629
- return limit === undefined
4630
- ? results.map((row) => this.toMaterializedOutputRow(row))
4631
- : results
4632
- .slice(0, Math.max(0, limit))
4633
- .map((row) => this.toMaterializedOutputRow(row));
4811
+ Boolean(
4812
+ this.#options.baseUrl &&
4813
+ this.#options.executorToken &&
4814
+ this.#options.playName &&
4815
+ this.#options.runId,
4816
+ );
4817
+ const materializeFullPersistedMapRows = async (limit?: number) => {
4818
+ if (!runtimeSheetBacked) {
4819
+ throw new Error(
4820
+ 'The full persisted dataset is unavailable because this run result has no Runtime Sheet backing.',
4821
+ );
4634
4822
  }
4635
4823
  if (limit !== undefined && limit <= 0) return [];
4636
4824
  const pageSize = 1000;
@@ -4642,10 +4830,25 @@ export class PlayContextImpl {
4642
4830
  ? pageSize
4643
4831
  : Math.max(0, limit - materialized.length);
4644
4832
  if (remaining === 0) break;
4645
- const rows = await readRuntimeBackedMapRows({
4646
- limit: Math.min(pageSize, remaining),
4647
- offset,
4648
- });
4833
+ const result = await readRuntimeSheetDatasetRows(
4834
+ {
4835
+ baseUrl: this.#options.baseUrl!,
4836
+ executorToken: this.#options.executorToken!,
4837
+ dbSessionStrategy: this.#options.dbSessionStrategy,
4838
+ playName: this.#options.playName!,
4839
+ userEmail: this.#options.userEmail,
4840
+ runId: this.#options.runId!,
4841
+ },
4842
+ {
4843
+ tableNamespace: resolvedTableNamespace,
4844
+ runId: this.#options.runId!,
4845
+ limit: Math.min(pageSize, remaining),
4846
+ offset,
4847
+ },
4848
+ );
4849
+ const rows = result.rows.map((row) =>
4850
+ this.toMaterializedOutputRow(row),
4851
+ );
4649
4852
  if (rows.length === 0) break;
4650
4853
  materialized.push(...rows);
4651
4854
  if (limit !== undefined && materialized.length >= limit) {
@@ -4663,7 +4866,7 @@ export class PlayContextImpl {
4663
4866
  resolvedTableNamespace,
4664
4867
  ),
4665
4868
  count: results.length,
4666
- backing: readRuntimeBackedMapRows
4869
+ backing: runtimeSheetBacked
4667
4870
  ? {
4668
4871
  storage: 'neon_sheet',
4669
4872
  sheet: {
@@ -4689,37 +4892,17 @@ export class PlayContextImpl {
4689
4892
  },
4690
4893
  resolvers: {
4691
4894
  count: async () => results.length,
4692
- peek: async (limit) =>
4693
- readRuntimeBackedMapRows
4694
- ? limit <= 0
4695
- ? []
4696
- : await readRuntimeBackedMapRows({
4697
- limit,
4698
- offset: 0,
4699
- })
4700
- : results
4701
- .slice(0, Math.max(0, limit))
4702
- .map((row) => this.toMaterializedOutputRow(row)),
4703
- materialize: materializeRuntimeBackedMapRows,
4895
+ peek: async (limit) => terminalRows.slice(0, Math.max(0, limit)),
4896
+ materialize: materializeTerminalRows,
4897
+ ...(runtimeSheetBacked
4898
+ ? {
4899
+ materializeFullPersistedDataset: materializeFullPersistedMapRows,
4900
+ }
4901
+ : {}),
4704
4902
  iterate: () =>
4705
4903
  ({
4706
4904
  async *[Symbol.asyncIterator]() {
4707
- if (readRuntimeBackedMapRows) {
4708
- const pageSize = 1000;
4709
- let offset = 0;
4710
- while (true) {
4711
- const rows = await readRuntimeBackedMapRows({
4712
- limit: pageSize,
4713
- offset,
4714
- });
4715
- if (rows.length === 0) return;
4716
- for (const row of rows) {
4717
- yield row;
4718
- }
4719
- offset += rows.length;
4720
- }
4721
- }
4722
- for (const row of results) {
4905
+ for (const row of terminalRows) {
4723
4906
  yield row;
4724
4907
  }
4725
4908
  },
@@ -6370,8 +6553,6 @@ export class PlayContextImpl {
6370
6553
  `Unable to resolve play "${resolvedName}" for ctx.runPlay(...).`,
6371
6554
  );
6372
6555
  }
6373
- const childRevisionFingerprint =
6374
- resolvedPlayRevisionFingerprint(resolvedPlay);
6375
6556
  const childExecutionDecision = resolveChildExecutionStrategy({
6376
6557
  pipeline: resolvedPlay.staticPipeline,
6377
6558
  });
@@ -6413,14 +6594,12 @@ export class PlayContextImpl {
6413
6594
  ),
6414
6595
  );
6415
6596
  }
6416
- const runPlaySemanticKey = buildDurableRunPlaySemanticKey({
6597
+ const runPlayInvocationScope = buildDurableRunPlayInvocationScope({
6417
6598
  childPlayName: resolvedName,
6418
- childRevisionFingerprint,
6419
6599
  input,
6420
6600
  rowScope: runPlayRowScope
6421
6601
  ? {
6422
6602
  fieldName: runPlayRowScope.fieldName,
6423
- rowId: runPlayRowScope.rowId,
6424
6603
  rowKey: runPlayRowScope.rowKey ?? null,
6425
6604
  tableNamespace: runPlayRowScope.tableNamespace ?? null,
6426
6605
  }
@@ -6458,7 +6637,7 @@ export class PlayContextImpl {
6458
6637
  parentRunId: this.currentRunId,
6459
6638
  parentPlayName: this.#options.playName,
6460
6639
  key: normalizedKey,
6461
- runPlaySemanticKey,
6640
+ runPlaySemanticKey: runPlayInvocationScope,
6462
6641
  input,
6463
6642
  graphHash: null,
6464
6643
  }),
@@ -6700,24 +6879,7 @@ export class PlayContextImpl {
6700
6879
  }
6701
6880
  };
6702
6881
 
6703
- return await this.executeWithRuntimeReceipt<TOutput>(
6704
- 'runPlay',
6705
- normalizedKey,
6706
- this.currentRunId,
6707
- {
6708
- semanticKey: runPlaySemanticKey,
6709
- staleAfterSeconds: options?.staleAfterSeconds,
6710
- markSkipped: () => {
6711
- this.log(
6712
- `ctx.runPlay(${normalizedKey}): no-op due completed receipt`,
6713
- );
6714
- },
6715
- repairRunningReceiptForSameRunAfterWaitTimeout: true,
6716
- runningReceiptWaitMaxAttempts:
6717
- resolveRuntimeToolReceiptWaitMaxAttempts(input),
6718
- execute: executePlayCall,
6719
- },
6720
- );
6882
+ return await executePlayCall();
6721
6883
  } finally {
6722
6884
  scheduledChildPlayCall?.release();
6723
6885
  }
@@ -7452,18 +7614,32 @@ export class PlayContextImpl {
7452
7614
  const forcedExisting = forcedReceiptKeys.length
7453
7615
  ? await this.getRuntimeStepReceipts(forcedReceiptKeys)
7454
7616
  : new Map<string, RuntimeStepReceipt>();
7455
- const forcedRunningReceiptKeys = new Set(
7617
+ // A forced refresh must never duplicate a provider call owned by a
7618
+ // live lease. It can, however, immediately claim pending/unleased
7619
+ // work and any expired lease. Waiting first for those receipts turns
7620
+ // an already-recoverable interruption into a five-minute stall.
7621
+ const forcedLiveReceiptKeys = new Set(
7456
7622
  [...forcedExisting]
7457
- .filter(
7458
- ([, receipt]) =>
7459
- receipt.status === 'queued' ||
7460
- receipt.status === 'pending' ||
7461
- receipt.status === 'running',
7462
- )
7623
+ .filter(([, receipt]) => {
7624
+ if (
7625
+ receipt.status !== 'queued' &&
7626
+ receipt.status !== 'pending' &&
7627
+ receipt.status !== 'running'
7628
+ ) {
7629
+ return false;
7630
+ }
7631
+ if (!receipt.leaseId?.trim()) return false;
7632
+ const expiresAt = receipt.leaseExpiresAt
7633
+ ? Date.parse(receipt.leaseExpiresAt)
7634
+ : Number.NaN;
7635
+ // Keep an unparseable leased receipt conservative. The
7636
+ // fenced claim remains the recovery path after the wait.
7637
+ return !Number.isFinite(expiresAt) || expiresAt > Date.now();
7638
+ })
7463
7639
  .map(([receiptKey]) => receiptKey),
7464
7640
  );
7465
7641
  const claimableForcedReceiptKeys = forcedReceiptKeys.filter(
7466
- (receiptKey) => !forcedRunningReceiptKeys.has(receiptKey),
7642
+ (receiptKey) => !forcedLiveReceiptKeys.has(receiptKey),
7467
7643
  );
7468
7644
  const claims =
7469
7645
  normalReceiptKeys.length > 0
@@ -7727,16 +7903,39 @@ export class PlayContextImpl {
7727
7903
  const processExistingRunningReceiptWaits =
7728
7904
  async (): Promise<void> => {
7729
7905
  if (durableExistingRunningWaits.length === 0) return;
7906
+ const waitingReceiptKeys = durableExistingRunningWaits.map(
7907
+ (wait) => wait.receiptKey,
7908
+ );
7730
7909
  const waitMaxAttempts = Math.max(
7731
7910
  ...durableExistingRunningWaits.map(
7732
7911
  (wait) => wait.waitMaxAttempts ?? 0,
7733
7912
  ),
7734
7913
  );
7914
+ if (runtimeReceiptReadTraceEnabled) {
7915
+ this.log(
7916
+ `[runtime-receipt-wait] phase=start requested=${waitingReceiptKeys.length} ` +
7917
+ `request_digests=${waitingReceiptKeys
7918
+ .map(runtimeReceiptKeyDigest)
7919
+ .join(',')} max_attempts=${waitMaxAttempts || 'default'}`,
7920
+ );
7921
+ }
7735
7922
  const waited = await this.waitForCompletedRuntimeToolReceipts(
7736
- durableExistingRunningWaits.map((wait) => wait.receiptKey),
7923
+ waitingReceiptKeys,
7737
7924
  waitMaxAttempts > 0 ? waitMaxAttempts : undefined,
7738
7925
  );
7739
- await Promise.allSettled(
7926
+ if (runtimeReceiptReadTraceEnabled) {
7927
+ this.log(
7928
+ `[runtime-receipt-wait] phase=settled completed=${waited.completed.size} ` +
7929
+ `failed=${waited.failed.size} timed_out=${waited.timedOut.size} ` +
7930
+ `completed_digests=${[...waited.completed.keys()]
7931
+ .map(runtimeReceiptKeyDigest)
7932
+ .join(',')} ` +
7933
+ `timed_out_digests=${[...waited.timedOut]
7934
+ .map(runtimeReceiptKeyDigest)
7935
+ .join(',')}`,
7936
+ );
7937
+ }
7938
+ const deliverySettlements = await Promise.allSettled(
7740
7939
  durableExistingRunningWaits.map(async (wait) => {
7741
7940
  const failed = waited.failed.get(wait.receiptKey);
7742
7941
  if (failed) {
@@ -7758,6 +7957,28 @@ export class PlayContextImpl {
7758
7957
  );
7759
7958
  }),
7760
7959
  );
7960
+ if (runtimeReceiptReadTraceEnabled) {
7961
+ const rejected = deliverySettlements.flatMap(
7962
+ (settlement, index) => {
7963
+ if (settlement.status !== 'rejected') return [];
7964
+ const receiptKey =
7965
+ durableExistingRunningWaits[index]?.receiptKey;
7966
+ return receiptKey
7967
+ ? [
7968
+ `${runtimeReceiptKeyDigest(receiptKey)}:${
7969
+ settlement.reason instanceof Error
7970
+ ? settlement.reason.name
7971
+ : 'non_error'
7972
+ }`,
7973
+ ]
7974
+ : ['unknown'];
7975
+ },
7976
+ );
7977
+ this.log(
7978
+ `[runtime-receipt-wait] phase=delivery settled=${deliverySettlements.length} ` +
7979
+ `rejected=${rejected.length} rejected_digests=${rejected.join(',')}`,
7980
+ );
7981
+ }
7761
7982
  };
7762
7983
 
7763
7984
  for (const [receiptKey, group] of requestsByReceiptKey) {
@@ -8243,11 +8464,17 @@ export class PlayContextImpl {
8243
8464
  // The Governor's tool slot is the single seam for tool-call budget + the
8244
8465
  // global tool-concurrency backstop + per-(org, provider) pacing. It blocks
8245
8466
  // until all three are satisfied and is held (across retries) until release.
8467
+ const admissionStartedAt = Date.now();
8246
8468
  const toolSlot = await this.resourceGovernor.acquireTool({
8247
8469
  orgId: this.#options.orgId ?? null,
8248
8470
  providerResourceKey: `tool:${toolId}`,
8249
8471
  toolId,
8250
8472
  });
8473
+ if (runtimeReceiptReadTraceEnabled) {
8474
+ this.log(
8475
+ `[perf] tool call id=${toolId} phase=governor_admission elapsed_ms=${Date.now() - admissionStartedAt}`,
8476
+ );
8477
+ }
8251
8478
  try {
8252
8479
  return await withActiveSpan(
8253
8480
  'plays.tool.execute',
@@ -8307,7 +8534,13 @@ export class PlayContextImpl {
8307
8534
  : null;
8308
8535
  try {
8309
8536
  const providerCallStartedAt = Date.now();
8537
+ const ownershipStartedAt = Date.now();
8310
8538
  await options?.beforeProviderCall?.();
8539
+ if (runtimeReceiptReadTraceEnabled) {
8540
+ this.log(
8541
+ `[perf] tool call id=${toolId} phase=before_provider_ownership elapsed_ms=${Date.now() - ownershipStartedAt}`,
8542
+ );
8543
+ }
8311
8544
  const heartbeatIntervalMs = hasReceiptHeartbeat
8312
8545
  ? runtimeLeaseHeartbeatIntervalFromExpiry({
8313
8546
  leaseExpiresAt: options?.receiptLeaseExpiresAt,
@@ -8337,6 +8570,7 @@ export class PlayContextImpl {
8337
8570
  : null;
8338
8571
  receiptHeartbeatSupervisor?.start();
8339
8572
  try {
8573
+ const integrationFetchStartedAt = Date.now();
8340
8574
  response = await fetch(url, {
8341
8575
  method: 'POST',
8342
8576
  signal: abortController?.signal,
@@ -8401,6 +8635,11 @@ export class PlayContextImpl {
8401
8635
  : {}),
8402
8636
  }),
8403
8637
  });
8638
+ if (runtimeReceiptReadTraceEnabled) {
8639
+ this.log(
8640
+ `[perf] tool call id=${toolId} phase=integration_fetch_headers elapsed_ms=${Date.now() - integrationFetchStartedAt} status=${response.status}`,
8641
+ );
8642
+ }
8404
8643
  this.resourceGovernor.observe({
8405
8644
  toolId,
8406
8645
  providerResourceKey: `tool:${toolId}`,