deepline 0.1.210 → 0.1.212

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 (40) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +203 -12
  2. package/dist/bundling-sources/apps/play-runner-workers/src/runtime/customer-console.ts +23 -0
  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 +162 -100
  6. package/dist/bundling-sources/sdk/src/client.ts +118 -3
  7. package/dist/bundling-sources/sdk/src/play.ts +2 -0
  8. package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
  9. package/dist/bundling-sources/sdk/src/release.ts +2 -2
  10. package/dist/bundling-sources/sdk/src/types.ts +47 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +253 -30
  12. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +365 -103
  13. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +12 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/dataset-id.ts +10 -4
  15. package/dist/bundling-sources/shared_libs/play-runtime/db-session.ts +9 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +45 -8
  17. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +14 -2
  18. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +4 -2
  19. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +21 -1
  20. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +128 -6
  21. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +7 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +5 -1
  23. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +37 -5
  24. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +19 -10
  25. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +63 -7
  26. package/dist/bundling-sources/shared_libs/play-runtime/secret-capability.ts +18 -4
  27. package/dist/bundling-sources/shared_libs/play-runtime/single-flight.ts +31 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +12 -1
  29. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +29 -9
  30. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +22 -1
  31. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +79 -1
  32. package/dist/bundling-sources/shared_libs/plays/dataset.ts +41 -5
  33. package/dist/cli/index.js +728 -239
  34. package/dist/cli/index.mjs +728 -239
  35. package/dist/index.d.mts +93 -7
  36. package/dist/index.d.ts +93 -7
  37. package/dist/index.js +245 -46
  38. package/dist/index.mjs +245 -46
  39. package/dist/plays/bundle-play-file.mjs +65 -4
  40. 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 {
@@ -105,6 +106,7 @@ import {
105
106
  parseToolExecuteAuthScopeChangedError,
106
107
  ToolExecuteAuthScopeChangedError,
107
108
  } from './tool-execute-retry-policy';
109
+ import { ToolHttpError } from './tool-http-errors';
108
110
  import {
109
111
  buildDurableCtxCallCacheKey,
110
112
  buildDurableRunPlayInvocationScope,
@@ -261,6 +263,7 @@ const rowContext = new AsyncLocalStorage<{
261
263
  rowKey?: string;
262
264
  mapScope?: MapExecutionScope;
263
265
  }>();
266
+ const toolExecutionOverrides = new AsyncLocalStorage<{ force: boolean }>();
264
267
  const PROGRESS_HEARTBEAT_INTERVAL_MS = 1_000;
265
268
  const PURE_JS_HEARTBEAT_ROW_INTERVAL = 250;
266
269
  /**
@@ -819,6 +822,111 @@ function stableDigest(value: string): string {
819
822
  return sha256Hex(value);
820
823
  }
821
824
 
825
+ // Receipt keys contain canonicalized tool inputs. Runtime diagnostics must never
826
+ // emit them directly; a short digest is enough to correlate gateway, runner,
827
+ // and waiter observations for one failing run.
828
+ function runtimeReceiptKeyDigest(key: string): string {
829
+ return stableDigest(key).slice(0, 12);
830
+ }
831
+
832
+ function runtimeReceiptReadSummary(input: {
833
+ requested: readonly string[];
834
+ receipts: readonly (RuntimeStepReceipt | null | undefined)[];
835
+ resolved: ReadonlyMap<string, RuntimeStepReceipt>;
836
+ }): string {
837
+ const statuses = input.receipts.reduce<Record<string, number>>(
838
+ (counts, receipt) => {
839
+ const status = receipt?.status ?? 'missing';
840
+ counts[status] = (counts[status] ?? 0) + 1;
841
+ return counts;
842
+ },
843
+ {},
844
+ );
845
+ const positionalMismatches = input.receipts.reduce(
846
+ (count, receipt, index) =>
847
+ receipt && receipt.key.trim() !== (input.requested[index] ?? '').trim()
848
+ ? count + 1
849
+ : count,
850
+ 0,
851
+ );
852
+ const missingRequested = input.requested.filter(
853
+ (key) => !input.resolved.has(key),
854
+ );
855
+ return (
856
+ `requested=${input.requested.length} returned=${input.receipts.length} ` +
857
+ `resolved=${input.resolved.size} positional_mismatches=${positionalMismatches} ` +
858
+ `request_digests=${input.requested.map(runtimeReceiptKeyDigest).join(',')} ` +
859
+ `returned_digests=${input.receipts
860
+ .map((receipt) =>
861
+ receipt ? runtimeReceiptKeyDigest(receipt.key) : 'null',
862
+ )
863
+ .join(',')} ` +
864
+ `missing_digests=${missingRequested
865
+ .map(runtimeReceiptKeyDigest)
866
+ .join(',')} ` +
867
+ `statuses=${Object.entries(statuses)
868
+ .map(([status, count]) => `${status}:${count}`)
869
+ .join(',')}`
870
+ );
871
+ }
872
+
873
+ const runtimeReceiptReadTraceEnabled =
874
+ process.env.DEEPLINE_RUNTIME_RECEIPT_TRACE === '1';
875
+
876
+ function runtimeReceiptReadTrace(input: {
877
+ keys: readonly string[];
878
+ receipts: Array<RuntimeStepReceipt | null>;
879
+ byKey: ReadonlyMap<string, RuntimeStepReceipt>;
880
+ }): string | null {
881
+ if (!runtimeReceiptReadTraceEnabled) return null;
882
+ const digest = (value: string) => stableDigest(value).slice(0, 16);
883
+ const statusCounts = input.receipts.reduce<Record<string, number>>(
884
+ (counts, receipt) => {
885
+ if (receipt) {
886
+ counts[receipt.status] = (counts[receipt.status] ?? 0) + 1;
887
+ }
888
+ return counts;
889
+ },
890
+ {},
891
+ );
892
+ const expiredInFlightCount = input.receipts.filter((receipt) => {
893
+ if (
894
+ receipt?.status !== 'queued' &&
895
+ receipt?.status !== 'pending' &&
896
+ receipt?.status !== 'running'
897
+ ) {
898
+ return false;
899
+ }
900
+ const expiresAt = receipt.leaseExpiresAt
901
+ ? Date.parse(receipt.leaseExpiresAt)
902
+ : Number.NaN;
903
+ return Number.isFinite(expiresAt) && expiresAt <= Date.now();
904
+ }).length;
905
+ return (
906
+ '[runtime-receipt-context.read-trace] ' +
907
+ JSON.stringify({
908
+ requestedCount: input.keys.length,
909
+ returnedCount: input.receipts.filter(Boolean).length,
910
+ mapSize: input.byKey.size,
911
+ statusCounts,
912
+ expiredInFlightCount,
913
+ requestedOrderDigest: digest(input.keys.join('\u0000')),
914
+ returnedOrderDigest: digest(
915
+ input.receipts
916
+ .map((receipt) =>
917
+ receipt ? `${digest(receipt.key)}:${receipt.status}` : '-',
918
+ )
919
+ .join('\u0000'),
920
+ ),
921
+ mapOrderDigest: digest(
922
+ [...input.byKey.entries()]
923
+ .map(([key, receipt]) => `${digest(key)}:${receipt.status}`)
924
+ .join('\u0000'),
925
+ ),
926
+ })
927
+ );
928
+ }
929
+
822
930
  type DurableCtxOperation = 'step' | 'tool' | 'fetch';
823
931
 
824
932
  function durableCtxKey(input: {
@@ -1176,12 +1284,15 @@ export class PlayContextImpl {
1176
1284
  );
1177
1285
  }
1178
1286
  assertNoSecretTaint(request.input, 'ctx.tools.execute input');
1287
+ const force =
1288
+ request.force === true ||
1289
+ toolExecutionOverrides.getStore()?.force === true;
1179
1290
  return this.executeTool(
1180
1291
  request.id.trim(),
1181
1292
  request.tool,
1182
1293
  request.input,
1183
1294
  request.description ||
1184
- request.force === true ||
1295
+ force ||
1185
1296
  request.staleAfterSeconds !== undefined ||
1186
1297
  request.timeoutMs !== undefined ||
1187
1298
  request.receiptWaitMs !== undefined
@@ -1189,7 +1300,7 @@ export class PlayContextImpl {
1189
1300
  ...(request.description
1190
1301
  ? { description: request.description }
1191
1302
  : {}),
1192
- ...(request.force === true ? { force: true } : {}),
1303
+ ...(force ? { force: true } : {}),
1193
1304
  ...(request.staleAfterSeconds !== undefined
1194
1305
  ? { staleAfterSeconds: request.staleAfterSeconds }
1195
1306
  : {}),
@@ -1204,6 +1315,10 @@ export class PlayContextImpl {
1204
1315
  ) as Promise<TOutput>;
1205
1316
  },
1206
1317
  };
1318
+
1319
+ async __deeplineRunWithForcedTools<T>(run: () => Promise<T>): Promise<T> {
1320
+ return await toolExecutionOverrides.run({ force: true }, run);
1321
+ }
1207
1322
  readonly customerDb = {
1208
1323
  query: async <TRow extends object = Record<string, unknown>>(
1209
1324
  statement:
@@ -2161,6 +2276,21 @@ export class PlayContextImpl {
2161
2276
  );
2162
2277
  if (normalized) byKey.set(normalized.key, normalized);
2163
2278
  }
2279
+ if (runtimeReceiptReadTraceEnabled) {
2280
+ this.log(
2281
+ `[runtime-receipt-normalize] ${runtimeReceiptReadSummary({
2282
+ requested: uniqueKeys,
2283
+ receipts,
2284
+ resolved: byKey,
2285
+ })}`,
2286
+ );
2287
+ const trace = runtimeReceiptReadTrace({
2288
+ keys: uniqueKeys,
2289
+ receipts,
2290
+ byKey,
2291
+ });
2292
+ if (trace) this.log(trace);
2293
+ }
2164
2294
  return byKey;
2165
2295
  }
2166
2296
 
@@ -2207,6 +2337,16 @@ export class PlayContextImpl {
2207
2337
  );
2208
2338
  if (normalized) byKey.set(normalized.key, normalized);
2209
2339
  }
2340
+ if (runtimeReceiptReadTraceEnabled) {
2341
+ this.log(
2342
+ `[runtime-receipt-claim-normalize] reclaim_running=${reclaimRunning} force_refresh=${forceRefresh} force_failed_refresh=${forceFailedRefresh} ` +
2343
+ runtimeReceiptReadSummary({
2344
+ requested: uniqueKeys,
2345
+ receipts,
2346
+ resolved: byKey,
2347
+ }),
2348
+ );
2349
+ }
2210
2350
  return byKey;
2211
2351
  }
2212
2352
 
@@ -2667,6 +2807,7 @@ export class PlayContextImpl {
2667
2807
  receiptKeys: keys,
2668
2808
  store: this.durableReceiptExecutionStore(),
2669
2809
  maxAttempts,
2810
+ log: (message) => this.log(message),
2670
2811
  });
2671
2812
  }
2672
2813
 
@@ -3346,7 +3487,8 @@ export class PlayContextImpl {
3346
3487
  );
3347
3488
  }
3348
3489
  const finalWrapped =
3349
- completed?.status === 'completed' || completed?.status === 'skipped'
3490
+ (completed?.status === 'completed' || completed?.status === 'skipped') &&
3491
+ completed.output !== undefined
3350
3492
  ? await this.wrapToolExecutionResult({
3351
3493
  toolId,
3352
3494
  status:
@@ -3755,6 +3897,13 @@ export class PlayContextImpl {
3755
3897
  let duplicateReuseCount = 0;
3756
3898
  let executedCount = 0;
3757
3899
  const previewRows: Record<string, unknown>[] = [];
3900
+ // A bounded dataset may be materialized by authored code immediately
3901
+ // after run(). Keep the rows produced by this exact awaited execution so
3902
+ // that read-after-write does not depend on a separate sheet read becoming
3903
+ // visible. Datasets above the public materialize cap remain fully
3904
+ // streaming and sheet-backed.
3905
+ const materializedResultRowLimit = resolveMaterializeLimitCap();
3906
+ const immediateMaterializedRows: Record<string, unknown>[] = [];
3758
3907
 
3759
3908
  const persistMapRows = async (rows: PersistableMapRow[]) => {
3760
3909
  if (!this.#options.onMapRowsCompleted || rows.length === 0) {
@@ -4038,8 +4187,11 @@ export class PlayContextImpl {
4038
4187
  this.activeMapCellMeta = null;
4039
4188
 
4040
4189
  for (const row of mapResult.completedRows) {
4041
- if (previewRows.length >= 5) break;
4042
- previewRows.push(this.toMaterializedOutputRow(row.data));
4190
+ const materializedRow = this.toMaterializedOutputRow(row.data);
4191
+ if (previewRows.length < 5) previewRows.push(materializedRow);
4192
+ if (immediateMaterializedRows.length < materializedResultRowLimit) {
4193
+ immediateMaterializedRows.push(materializedRow);
4194
+ }
4043
4195
  }
4044
4196
  executedCount += rowsToExecute.length;
4045
4197
  successfulCount += mapResult.completedRows.length;
@@ -4100,6 +4252,42 @@ export class PlayContextImpl {
4100
4252
  return result.rows.map((row) => this.toMaterializedOutputRow(row));
4101
4253
  };
4102
4254
  const materializeRuntimeBackedMapRows = async (limit?: number) => {
4255
+ if (limit !== undefined && limit <= 0) return [];
4256
+ const availableResultRows = Math.min(
4257
+ successfulCount,
4258
+ materializedResultRowLimit,
4259
+ );
4260
+ if (
4261
+ immediateMaterializedRows.length === availableResultRows &&
4262
+ (limit !== undefined || successfulCount <= materializedResultRowLimit)
4263
+ ) {
4264
+ return limit === undefined
4265
+ ? immediateMaterializedRows.slice()
4266
+ : immediateMaterializedRows.slice(0, limit);
4267
+ }
4268
+ const pageSize = 1000;
4269
+ const materialized: Record<string, unknown>[] = [];
4270
+ let offset = 0;
4271
+ while (true) {
4272
+ const remaining =
4273
+ limit === undefined
4274
+ ? pageSize
4275
+ : Math.max(0, limit - materialized.length);
4276
+ if (remaining === 0) break;
4277
+ const rows = await readRuntimeBackedMapRows({
4278
+ limit: Math.min(pageSize, remaining),
4279
+ offset,
4280
+ });
4281
+ if (rows.length === 0) break;
4282
+ materialized.push(...rows);
4283
+ if (limit !== undefined && materialized.length >= limit) {
4284
+ return materialized.slice(0, limit);
4285
+ }
4286
+ offset += rows.length;
4287
+ }
4288
+ return materialized;
4289
+ };
4290
+ const materializeFullPersistedMapRows = async (limit?: number) => {
4103
4291
  if (limit !== undefined && limit <= 0) return [];
4104
4292
  const pageSize = 1000;
4105
4293
  const materialized: Record<string, unknown>[] = [];
@@ -4156,14 +4344,22 @@ export class PlayContextImpl {
4156
4344
  peek: async (limit) =>
4157
4345
  limit <= 0
4158
4346
  ? []
4159
- : await readRuntimeBackedMapRows({
4160
- limit,
4161
- offset: 0,
4162
- }),
4347
+ : immediateMaterializedRows.length >=
4348
+ Math.min(limit, successfulCount)
4349
+ ? immediateMaterializedRows.slice(0, limit)
4350
+ : await readRuntimeBackedMapRows({
4351
+ limit,
4352
+ offset: 0,
4353
+ }),
4163
4354
  materialize: materializeRuntimeBackedMapRows,
4355
+ materializeFullPersistedDataset: materializeFullPersistedMapRows,
4164
4356
  iterate: () =>
4165
4357
  ({
4166
4358
  async *[Symbol.asyncIterator]() {
4359
+ if (immediateMaterializedRows.length === successfulCount) {
4360
+ for (const row of immediateMaterializedRows) yield row;
4361
+ return;
4362
+ }
4167
4363
  const pageSize = 1000;
4168
4364
  let offset = 0;
4169
4365
  while (true) {
@@ -4598,39 +4794,33 @@ export class PlayContextImpl {
4598
4794
  });
4599
4795
  }
4600
4796
 
4601
- const readRuntimeBackedMapRows =
4797
+ // This map's terminal rows have already crossed the persistence barrier
4798
+ // above. Keep that verified snapshot as the authored return value instead
4799
+ // of immediately re-reading the Runtime Sheet projection. A second read
4800
+ // can observe a lagging/dynamically incomplete physical projection even
4801
+ // though the terminal write succeeded (notably for inline-child object
4802
+ // outputs on Absurd), making `materialize()` disagree with durable truth.
4803
+ // Neon remains the backing for later API/export reads of this handle.
4804
+ const terminalRows = results.map((row) =>
4805
+ this.toMaterializedOutputRow(row),
4806
+ );
4807
+ const materializeTerminalRows = async (limit?: number) =>
4808
+ limit === undefined
4809
+ ? terminalRows.slice()
4810
+ : terminalRows.slice(0, Math.max(0, limit));
4811
+ const runtimeSheetBacked =
4602
4812
  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));
4813
+ Boolean(
4814
+ this.#options.baseUrl &&
4815
+ this.#options.executorToken &&
4816
+ this.#options.playName &&
4817
+ this.#options.runId,
4818
+ );
4819
+ const materializeFullPersistedMapRows = async (limit?: number) => {
4820
+ if (!runtimeSheetBacked) {
4821
+ throw new Error(
4822
+ 'The full persisted dataset is unavailable because this run result has no Runtime Sheet backing.',
4823
+ );
4634
4824
  }
4635
4825
  if (limit !== undefined && limit <= 0) return [];
4636
4826
  const pageSize = 1000;
@@ -4642,10 +4832,25 @@ export class PlayContextImpl {
4642
4832
  ? pageSize
4643
4833
  : Math.max(0, limit - materialized.length);
4644
4834
  if (remaining === 0) break;
4645
- const rows = await readRuntimeBackedMapRows({
4646
- limit: Math.min(pageSize, remaining),
4647
- offset,
4648
- });
4835
+ const result = await readRuntimeSheetDatasetRows(
4836
+ {
4837
+ baseUrl: this.#options.baseUrl!,
4838
+ executorToken: this.#options.executorToken!,
4839
+ dbSessionStrategy: this.#options.dbSessionStrategy,
4840
+ playName: this.#options.playName!,
4841
+ userEmail: this.#options.userEmail,
4842
+ runId: this.#options.runId!,
4843
+ },
4844
+ {
4845
+ tableNamespace: resolvedTableNamespace,
4846
+ runId: this.#options.runId!,
4847
+ limit: Math.min(pageSize, remaining),
4848
+ offset,
4849
+ },
4850
+ );
4851
+ const rows = result.rows.map((row) =>
4852
+ this.toMaterializedOutputRow(row),
4853
+ );
4649
4854
  if (rows.length === 0) break;
4650
4855
  materialized.push(...rows);
4651
4856
  if (limit !== undefined && materialized.length >= limit) {
@@ -4663,7 +4868,7 @@ export class PlayContextImpl {
4663
4868
  resolvedTableNamespace,
4664
4869
  ),
4665
4870
  count: results.length,
4666
- backing: readRuntimeBackedMapRows
4871
+ backing: runtimeSheetBacked
4667
4872
  ? {
4668
4873
  storage: 'neon_sheet',
4669
4874
  sheet: {
@@ -4689,37 +4894,17 @@ export class PlayContextImpl {
4689
4894
  },
4690
4895
  resolvers: {
4691
4896
  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,
4897
+ peek: async (limit) => terminalRows.slice(0, Math.max(0, limit)),
4898
+ materialize: materializeTerminalRows,
4899
+ ...(runtimeSheetBacked
4900
+ ? {
4901
+ materializeFullPersistedDataset: materializeFullPersistedMapRows,
4902
+ }
4903
+ : {}),
4704
4904
  iterate: () =>
4705
4905
  ({
4706
4906
  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) {
4907
+ for (const row of terminalRows) {
4723
4908
  yield row;
4724
4909
  }
4725
4910
  },
@@ -6136,11 +6321,6 @@ export class PlayContextImpl {
6136
6321
  semanticKey: toolRequestIdentity,
6137
6322
  force: toolCachePolicy.force,
6138
6323
  staleAfterSeconds: toolCachePolicy.staleAfterSeconds,
6139
- markSkipped: () => {
6140
- this.log(
6141
- `ctx.tools.execute(${toolId}): no-op due completed receipt ${toolRequestIdentity} (label: ${normalizedKey})`,
6142
- );
6143
- },
6144
6324
  onClaimedResult: (output, receiptKey) =>
6145
6325
  markToolExecuteResultExecutionOutcome(output, {
6146
6326
  kind: 'live',
@@ -7094,11 +7274,6 @@ export class PlayContextImpl {
7094
7274
  egressSlot.release();
7095
7275
  }
7096
7276
  },
7097
- markSkipped: (output) => {
7098
- this.log(
7099
- `ctx.fetch(${output?.url ?? ''}): no-op due completed receipt`,
7100
- );
7101
- },
7102
7277
  },
7103
7278
  );
7104
7279
  }
@@ -7165,7 +7340,6 @@ export class PlayContextImpl {
7165
7340
  : options?.semanticKey,
7166
7341
  staleAfterSeconds: options?.staleAfterSeconds,
7167
7342
  markSkipped: (output) => {
7168
- this.log(`ctx.step(${normalizedKey}): no-op due completed receipt`);
7169
7343
  assertJsonSerializableStepOutput(normalizedKey, output);
7170
7344
  },
7171
7345
  execute: executeStep,
@@ -7431,18 +7605,32 @@ export class PlayContextImpl {
7431
7605
  const forcedExisting = forcedReceiptKeys.length
7432
7606
  ? await this.getRuntimeStepReceipts(forcedReceiptKeys)
7433
7607
  : new Map<string, RuntimeStepReceipt>();
7434
- const forcedRunningReceiptKeys = new Set(
7608
+ // A forced refresh must never duplicate a provider call owned by a
7609
+ // live lease. It can, however, immediately claim pending/unleased
7610
+ // work and any expired lease. Waiting first for those receipts turns
7611
+ // an already-recoverable interruption into a five-minute stall.
7612
+ const forcedLiveReceiptKeys = new Set(
7435
7613
  [...forcedExisting]
7436
- .filter(
7437
- ([, receipt]) =>
7438
- receipt.status === 'queued' ||
7439
- receipt.status === 'pending' ||
7440
- receipt.status === 'running',
7441
- )
7614
+ .filter(([, receipt]) => {
7615
+ if (
7616
+ receipt.status !== 'queued' &&
7617
+ receipt.status !== 'pending' &&
7618
+ receipt.status !== 'running'
7619
+ ) {
7620
+ return false;
7621
+ }
7622
+ if (!receipt.leaseId?.trim()) return false;
7623
+ const expiresAt = receipt.leaseExpiresAt
7624
+ ? Date.parse(receipt.leaseExpiresAt)
7625
+ : Number.NaN;
7626
+ // Keep an unparseable leased receipt conservative. The
7627
+ // fenced claim remains the recovery path after the wait.
7628
+ return !Number.isFinite(expiresAt) || expiresAt > Date.now();
7629
+ })
7442
7630
  .map(([receiptKey]) => receiptKey),
7443
7631
  );
7444
7632
  const claimableForcedReceiptKeys = forcedReceiptKeys.filter(
7445
- (receiptKey) => !forcedRunningReceiptKeys.has(receiptKey),
7633
+ (receiptKey) => !forcedLiveReceiptKeys.has(receiptKey),
7446
7634
  );
7447
7635
  const claims =
7448
7636
  normalReceiptKeys.length > 0
@@ -7706,16 +7894,39 @@ export class PlayContextImpl {
7706
7894
  const processExistingRunningReceiptWaits =
7707
7895
  async (): Promise<void> => {
7708
7896
  if (durableExistingRunningWaits.length === 0) return;
7897
+ const waitingReceiptKeys = durableExistingRunningWaits.map(
7898
+ (wait) => wait.receiptKey,
7899
+ );
7709
7900
  const waitMaxAttempts = Math.max(
7710
7901
  ...durableExistingRunningWaits.map(
7711
7902
  (wait) => wait.waitMaxAttempts ?? 0,
7712
7903
  ),
7713
7904
  );
7905
+ if (runtimeReceiptReadTraceEnabled) {
7906
+ this.log(
7907
+ `[runtime-receipt-wait] phase=start requested=${waitingReceiptKeys.length} ` +
7908
+ `request_digests=${waitingReceiptKeys
7909
+ .map(runtimeReceiptKeyDigest)
7910
+ .join(',')} max_attempts=${waitMaxAttempts || 'default'}`,
7911
+ );
7912
+ }
7714
7913
  const waited = await this.waitForCompletedRuntimeToolReceipts(
7715
- durableExistingRunningWaits.map((wait) => wait.receiptKey),
7914
+ waitingReceiptKeys,
7716
7915
  waitMaxAttempts > 0 ? waitMaxAttempts : undefined,
7717
7916
  );
7718
- await Promise.allSettled(
7917
+ if (runtimeReceiptReadTraceEnabled) {
7918
+ this.log(
7919
+ `[runtime-receipt-wait] phase=settled completed=${waited.completed.size} ` +
7920
+ `failed=${waited.failed.size} timed_out=${waited.timedOut.size} ` +
7921
+ `completed_digests=${[...waited.completed.keys()]
7922
+ .map(runtimeReceiptKeyDigest)
7923
+ .join(',')} ` +
7924
+ `timed_out_digests=${[...waited.timedOut]
7925
+ .map(runtimeReceiptKeyDigest)
7926
+ .join(',')}`,
7927
+ );
7928
+ }
7929
+ const deliverySettlements = await Promise.allSettled(
7719
7930
  durableExistingRunningWaits.map(async (wait) => {
7720
7931
  const failed = waited.failed.get(wait.receiptKey);
7721
7932
  if (failed) {
@@ -7737,6 +7948,28 @@ export class PlayContextImpl {
7737
7948
  );
7738
7949
  }),
7739
7950
  );
7951
+ if (runtimeReceiptReadTraceEnabled) {
7952
+ const rejected = deliverySettlements.flatMap(
7953
+ (settlement, index) => {
7954
+ if (settlement.status !== 'rejected') return [];
7955
+ const receiptKey =
7956
+ durableExistingRunningWaits[index]?.receiptKey;
7957
+ return receiptKey
7958
+ ? [
7959
+ `${runtimeReceiptKeyDigest(receiptKey)}:${
7960
+ settlement.reason instanceof Error
7961
+ ? settlement.reason.name
7962
+ : 'non_error'
7963
+ }`,
7964
+ ]
7965
+ : ['unknown'];
7966
+ },
7967
+ );
7968
+ this.log(
7969
+ `[runtime-receipt-wait] phase=delivery settled=${deliverySettlements.length} ` +
7970
+ `rejected=${rejected.length} rejected_digests=${rejected.join(',')}`,
7971
+ );
7972
+ }
7740
7973
  };
7741
7974
 
7742
7975
  for (const [receiptKey, group] of requestsByReceiptKey) {
@@ -8222,11 +8455,17 @@ export class PlayContextImpl {
8222
8455
  // The Governor's tool slot is the single seam for tool-call budget + the
8223
8456
  // global tool-concurrency backstop + per-(org, provider) pacing. It blocks
8224
8457
  // until all three are satisfied and is held (across retries) until release.
8458
+ const admissionStartedAt = Date.now();
8225
8459
  const toolSlot = await this.resourceGovernor.acquireTool({
8226
8460
  orgId: this.#options.orgId ?? null,
8227
8461
  providerResourceKey: `tool:${toolId}`,
8228
8462
  toolId,
8229
8463
  });
8464
+ if (runtimeReceiptReadTraceEnabled) {
8465
+ this.log(
8466
+ `[perf] tool call id=${toolId} phase=governor_admission elapsed_ms=${Date.now() - admissionStartedAt}`,
8467
+ );
8468
+ }
8230
8469
  try {
8231
8470
  return await withActiveSpan(
8232
8471
  'plays.tool.execute',
@@ -8252,6 +8491,8 @@ export class PlayContextImpl {
8252
8491
 
8253
8492
  while (true) {
8254
8493
  let response: Response;
8494
+ let providerCallStartedAt: number | null = null;
8495
+ let providerCallElapsedMs: number | null = null;
8255
8496
  const durableCallReceiptKey =
8256
8497
  options?.durableCallReceiptKey?.trim() || null;
8257
8498
  const executionAuthScopeDigest =
@@ -8285,8 +8526,14 @@ export class PlayContextImpl {
8285
8526
  }, timeoutMs)
8286
8527
  : null;
8287
8528
  try {
8288
- const providerCallStartedAt = Date.now();
8529
+ const ownershipStartedAt = Date.now();
8289
8530
  await options?.beforeProviderCall?.();
8531
+ if (runtimeReceiptReadTraceEnabled) {
8532
+ this.log(
8533
+ `[perf] tool call id=${toolId} phase=before_provider_ownership elapsed_ms=${Date.now() - ownershipStartedAt}`,
8534
+ );
8535
+ }
8536
+ providerCallStartedAt = Date.now();
8290
8537
  const heartbeatIntervalMs = hasReceiptHeartbeat
8291
8538
  ? runtimeLeaseHeartbeatIntervalFromExpiry({
8292
8539
  leaseExpiresAt: options?.receiptLeaseExpiresAt,
@@ -8316,6 +8563,7 @@ export class PlayContextImpl {
8316
8563
  : null;
8317
8564
  receiptHeartbeatSupervisor?.start();
8318
8565
  try {
8566
+ const integrationFetchStartedAt = Date.now();
8319
8567
  response = await fetch(url, {
8320
8568
  method: 'POST',
8321
8569
  signal: abortController?.signal,
@@ -8380,10 +8628,16 @@ export class PlayContextImpl {
8380
8628
  : {}),
8381
8629
  }),
8382
8630
  });
8631
+ if (runtimeReceiptReadTraceEnabled) {
8632
+ this.log(
8633
+ `[perf] tool call id=${toolId} phase=integration_fetch_headers elapsed_ms=${Date.now() - integrationFetchStartedAt} status=${response.status}`,
8634
+ );
8635
+ }
8636
+ providerCallElapsedMs = Date.now() - providerCallStartedAt;
8383
8637
  this.resourceGovernor.observe({
8384
8638
  toolId,
8385
8639
  providerResourceKey: `tool:${toolId}`,
8386
- providerLatencyMs: Date.now() - providerCallStartedAt,
8640
+ providerLatencyMs: providerCallElapsedMs,
8387
8641
  providerSuccess: response.ok,
8388
8642
  provider429: response.status === 429,
8389
8643
  });
@@ -8394,16 +8648,18 @@ export class PlayContextImpl {
8394
8648
  if (heartbeatFailure) {
8395
8649
  throw heartbeatFailure;
8396
8650
  }
8397
- if (abortController?.signal.aborted) {
8398
- throw abortController.signal.reason instanceof Error
8651
+ const transportError = abortController?.signal.aborted
8652
+ ? abortController.signal.reason instanceof Error
8399
8653
  ? abortController.signal.reason
8400
8654
  : new Error(
8401
8655
  `Tool ${toolId} runtime API call timed out after ${timeoutMs}ms.`,
8402
- );
8403
- }
8656
+ )
8657
+ : error;
8404
8658
  transportAttempt += 1;
8405
8659
  const message =
8406
- error instanceof Error ? error.message : String(error);
8660
+ transportError instanceof Error
8661
+ ? transportError.message
8662
+ : String(transportError);
8407
8663
  if (transportAttempt < TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS) {
8408
8664
  await this.governor.chargeBudget('retry');
8409
8665
  const retryAfterMs =
@@ -8419,8 +8675,11 @@ export class PlayContextImpl {
8419
8675
  await this.sleepWithCheckpointHeartbeat(retryAfterMs);
8420
8676
  continue;
8421
8677
  }
8422
- throw new Error(
8678
+ throw new ToolHttpError(
8423
8679
  `Tool ${toolId} transport failed calling ${url} after ${transportAttempt}/${TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS} attempts: ${message}`,
8680
+ null,
8681
+ 0,
8682
+ 'repairable',
8424
8683
  );
8425
8684
  } finally {
8426
8685
  if (timeoutHandle) {
@@ -8451,6 +8710,9 @@ export class PlayContextImpl {
8451
8710
  bodyText: text,
8452
8711
  retryAfterHeader: response.headers.get('retry-after'),
8453
8712
  transientHttpRetrySafe: retrySafeTransientHttp,
8713
+ ...(providerCallElapsedMs !== null
8714
+ ? { providerLatencyMs: providerCallElapsedMs }
8715
+ : {}),
8454
8716
  });
8455
8717
  if (failure.backpressureDelayMs !== null) {
8456
8718
  // Feed the server-observed Retry-After back into the shared