deepline 0.1.211 → 0.1.213

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 (54) hide show
  1. package/dist/bundling-sources/apps/play-runner-workers/src/entry.ts +317 -340
  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/tool-dispatch.ts +1 -1
  4. package/dist/bundling-sources/sdk/src/client.ts +118 -3
  5. package/dist/bundling-sources/sdk/src/index.ts +2 -0
  6. package/dist/bundling-sources/sdk/src/play.ts +8 -1
  7. package/dist/bundling-sources/sdk/src/plays/bundle-play-file.ts +1 -1
  8. package/dist/bundling-sources/sdk/src/plays/harness-stub.ts +11 -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 +302 -30
  12. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-placement.ts +14 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/child-execution-strategy.ts +57 -23
  14. package/dist/bundling-sources/shared_libs/play-runtime/completed-receipt-cache.ts +166 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +164 -121
  16. package/dist/bundling-sources/shared_libs/play-runtime/ctx-contract.ts +2 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +42 -1
  18. package/dist/bundling-sources/shared_libs/play-runtime/dataset-id.ts +10 -4
  19. package/dist/bundling-sources/shared_libs/play-runtime/db-session.ts +9 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/durable-receipt-execution.ts +163 -7
  21. package/dist/bundling-sources/shared_libs/play-runtime/gateway-auth-session.ts +93 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/ledger-safe-payload.ts +14 -2
  23. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +4 -2
  24. package/dist/bundling-sources/shared_libs/play-runtime/play-call-execution.ts +1 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/receipt-completion-sink.ts +16 -1
  26. package/dist/bundling-sources/shared_libs/play-runtime/receipt-sql.ts +21 -1
  27. package/dist/bundling-sources/shared_libs/play-runtime/receipt-state-sink.ts +10 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/resource-governor.ts +80 -3
  29. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +128 -6
  30. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +7 -0
  31. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +94 -0
  32. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +91 -6
  33. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/types.ts +19 -10
  34. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +540 -92
  35. package/dist/bundling-sources/shared_libs/play-runtime/runtime-postgres-admission.ts +162 -0
  36. package/dist/bundling-sources/shared_libs/play-runtime/secret-capability.ts +18 -4
  37. package/dist/bundling-sources/shared_libs/play-runtime/sheet-attempt-sql.ts +16 -17
  38. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +29 -9
  39. package/dist/bundling-sources/shared_libs/play-runtime/work-receipt-state-machine.ts +22 -1
  40. package/dist/bundling-sources/shared_libs/play-runtime/work-receipts.ts +1 -0
  41. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +79 -1
  42. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +2 -4
  43. package/dist/cli/index.js +637 -229
  44. package/dist/cli/index.mjs +637 -229
  45. package/dist/{compiler-manifest-j6z8qzpd.d.mts → compiler-manifest-BhgZ23A4.d.mts} +1 -0
  46. package/dist/{compiler-manifest-j6z8qzpd.d.ts → compiler-manifest-BhgZ23A4.d.ts} +1 -0
  47. package/dist/index.d.mts +84 -11
  48. package/dist/index.d.ts +84 -11
  49. package/dist/index.js +225 -43
  50. package/dist/index.mjs +225 -43
  51. package/dist/plays/bundle-play-file.d.mts +2 -2
  52. package/dist/plays/bundle-play-file.d.ts +2 -2
  53. package/dist/plays/bundle-play-file.mjs +65 -4
  54. package/package.json +1 -1
@@ -24,6 +24,7 @@ import type { PreviousCell } from './cell-staleness';
24
24
  import type { MapRowOutcome } from './durability-store';
25
25
  import type { WorkReceiptFailureKind } from './work-receipts';
26
26
  import type { RunExecutionScope } from './run-execution-scope';
27
+ import type { PlayCallExecution } from './play-call-execution';
27
28
 
28
29
  export interface RowState {
29
30
  results: Map<string, unknown>;
@@ -69,12 +70,26 @@ export interface RuntimeStepReceipt {
69
70
  claimState?: 'claimed' | 'existing';
70
71
  }
71
72
 
73
+ export type AcquireRuntimeReceiptExecutionLockInput = {
74
+ key: string;
75
+ runId: string;
76
+ ownerExecutionId: string;
77
+ ttlMs: number;
78
+ };
79
+
80
+ export type ReleaseRuntimeReceiptExecutionLockInput = {
81
+ key: string;
82
+ runId: string;
83
+ ownerExecutionId: string;
84
+ };
85
+
72
86
  export interface GetRuntimeStepReceiptInput {
73
87
  key: string;
74
88
  }
75
89
 
76
90
  export interface ClaimRuntimeStepReceiptInput {
77
91
  key: string;
92
+ leaseId?: string;
78
93
  runId: string;
79
94
  runAttempt?: number | null;
80
95
  leaseAware?: boolean;
@@ -128,6 +143,7 @@ export interface GetRuntimeStepReceiptsInput {
128
143
 
129
144
  export interface ClaimRuntimeStepReceiptsInput {
130
145
  keys: string[];
146
+ leaseIds?: string[];
131
147
  runId: string;
132
148
  runAttempt?: number | null;
133
149
  leaseAware?: boolean;
@@ -281,6 +297,7 @@ export type CustomerDbQueryHandler = (
281
297
 
282
298
  export interface PlayCallOptions {
283
299
  description?: string;
300
+ execution?: PlayCallExecution;
284
301
  }
285
302
 
286
303
  export interface StepOptions {
@@ -380,6 +397,18 @@ export interface MapExecutionScope {
380
397
  }
381
398
 
382
399
  export type PlayExecutionEvent =
400
+ | {
401
+ type: 'dataset.lifecycle';
402
+ datasetId: string;
403
+ path: string;
404
+ tableNamespace: string;
405
+ phase: 'registered' | 'available' | 'failed';
406
+ persistedRows?: number;
407
+ succeededRows?: number;
408
+ failedRows?: number;
409
+ complete?: boolean;
410
+ at: number;
411
+ }
383
412
  | {
384
413
  type: 'map.preparing';
385
414
  mapInvocationId: string;
@@ -585,7 +614,10 @@ export interface ContextOptions {
585
614
  rows: MapRowOutcome[];
586
615
  outputFields: string[];
587
616
  staticPipeline?: PlayStaticPipeline | null;
588
- }) => Promise<void>;
617
+ }) => Promise<void | {
618
+ updated: number;
619
+ staleDroppedKeys?: string[];
620
+ }>;
589
621
  playId?: string;
590
622
  runId?: string;
591
623
  /** Physical executor run that owns leases for an inline child context. */
@@ -622,6 +654,7 @@ export interface ContextOptions {
622
654
  getToolQueueHints?: (toolId: string) => Promise<readonly PlayQueueHint[]>;
623
655
  getToolRetryPolicy?: (toolId: string) => Promise<{
624
656
  retrySafeTransientHttp?: boolean;
657
+ requiresExecutionFence?: boolean;
625
658
  } | null>;
626
659
  getToolActionCacheVersion?: (toolId: string) => Promise<string> | string;
627
660
  getToolAuthScopeDigest?: (toolId: string) => Promise<string> | string;
@@ -660,6 +693,12 @@ export interface ContextOptions {
660
693
  getRuntimeStepReceipt?: (
661
694
  input: GetRuntimeStepReceiptInput,
662
695
  ) => Promise<RuntimeStepReceipt | null> | RuntimeStepReceipt | null;
696
+ acquireRuntimeReceiptExecutionLock?: (
697
+ input: AcquireRuntimeReceiptExecutionLockInput,
698
+ ) => Promise<{ ownerExecutionId: string; expiresAt: string } | null>;
699
+ releaseRuntimeReceiptExecutionLock?: (
700
+ input: ReleaseRuntimeReceiptExecutionLockInput,
701
+ ) => Promise<boolean>;
663
702
  getRuntimeStepReceipts?: (
664
703
  input: GetRuntimeStepReceiptsInput,
665
704
  ) =>
@@ -920,6 +959,7 @@ export type PlayStep =
920
959
  | {
921
960
  type: 'play_call';
922
961
  playId: string;
962
+ execution?: PlayCallExecution;
923
963
  results?: PlayStepRowResult[];
924
964
  nestedSteps: PlayStep[];
925
965
  description?: string;
@@ -959,6 +999,7 @@ export type PlayDatasetSubstep =
959
999
  | {
960
1000
  type: 'play_call';
961
1001
  playId: string;
1002
+ execution?: PlayCallExecution;
962
1003
  results?: PlayStepRowResult[];
963
1004
  nestedSteps: PlayStep[];
964
1005
  description?: string;
@@ -1,10 +1,16 @@
1
- import { createHash } from 'node:crypto';
1
+ import {
2
+ normalizePlayNameForSheet,
3
+ normalizeTableNamespace,
4
+ sha256Hex,
5
+ } from '../plays/row-identity';
2
6
 
3
7
  export function createRuntimeDatasetId(
4
8
  playName: string,
5
9
  tableNamespace: string,
6
10
  ): string {
7
- return createHash('sha1')
8
- .update(`${playName}:${tableNamespace}`)
9
- .digest('hex');
11
+ const normalizedPlayName = normalizePlayNameForSheet(playName);
12
+ const normalizedTableNamespace = normalizeTableNamespace(tableNamespace);
13
+ return sha256Hex(
14
+ `${normalizedPlayName}:${normalizedTableNamespace}`,
15
+ ).slice(0, 40);
10
16
  }
@@ -56,6 +56,7 @@ export type CreateDbSessionRequest = {
56
56
 
57
57
  export type CreateDbSessionResponse = {
58
58
  sessionId: string;
59
+ executionRole?: string;
59
60
  postgresUrl?: string;
60
61
  encryptedPostgresUrl?: EncryptedPostgresUrl;
61
62
  postgres?: {
@@ -398,6 +399,14 @@ export const createDbSessionResponseSchema =
398
399
  }
399
400
  return {
400
401
  sessionId: parseString(value.sessionId, 'sessionId'),
402
+ ...(value.executionRole !== undefined
403
+ ? {
404
+ executionRole: parseOptionalString(
405
+ value.executionRole,
406
+ 'executionRole',
407
+ ),
408
+ }
409
+ : {}),
401
410
  ...(value.postgresUrl !== undefined
402
411
  ? { postgresUrl: parseOptionalString(value.postgresUrl, 'postgresUrl') }
403
412
  : {}),
@@ -25,6 +25,9 @@ const DURABLE_RECEIPT_WAIT_DELAY_MS = 250;
25
25
  const TOOL_RECEIPT_DEFAULT_WAIT_MS = 300_000;
26
26
  const TOOL_RECEIPT_COMPLETION_BUFFER_MS = 30_000;
27
27
  const TOOL_RECEIPT_MAX_WAIT_MS = 30 * 60_000;
28
+ const DANGEROUS_SIDE_EFFECT_LOCK_WAIT_MS = 15_000;
29
+ export const COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID =
30
+ '__deepline_completed_cache_insert__';
28
31
 
29
32
  export class RuntimeReceiptWaitTimeoutError extends Error {
30
33
  constructor(key: string) {
@@ -179,6 +182,15 @@ export type DurableReceiptExecutionStore = {
179
182
  ): Promise<RuntimeStepReceipt | null>;
180
183
  canPersistFailure: boolean;
181
184
  canPersistCompletion: boolean;
185
+ acquireExecutionLock?: (input: {
186
+ receiptKey: string;
187
+ ownerExecutionId: string;
188
+ ttlMs: number;
189
+ }) => Promise<{ ownerExecutionId: string; expiresAt: string } | null>;
190
+ releaseExecutionLock?: (input: {
191
+ receiptKey: string;
192
+ ownerExecutionId: string;
193
+ }) => Promise<boolean>;
182
194
  };
183
195
 
184
196
  async function executeWithDurableRuntimeReceiptHeartbeat<T>(input: {
@@ -243,7 +255,7 @@ async function executeWithDurableRuntimeReceiptHeartbeat<T>(input: {
243
255
  rejectLeaseLost(error as RuntimeReceiptLeaseLostError),
244
256
  });
245
257
 
246
- // markRunning is the authoritative pre-dispatch ownership fence. Renewal is
258
+ // The claim is the authoritative pre-dispatch ownership fence. Renewal is
247
259
  // only needed when the lease approaches expiry; an immediate read/renewal
248
260
  // adds a round trip to every short tool call without extending ownership.
249
261
  // Completion remains conditional on this lease, and long work still renews.
@@ -390,6 +402,11 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
390
402
  shouldPersistFailure?: (error: unknown) => boolean;
391
403
  heartbeatIntervalMs?: number;
392
404
  markRunningBeforeExecute?: boolean;
405
+ /** New receipt-cache path: read completed facts, execute without creating an
406
+ * in-flight receipt, then atomically insert the completed fact. */
407
+ completedCacheOnly?: boolean;
408
+ requiresExecutionLock?: boolean;
409
+ executionLockTtlMs?: number;
393
410
  formatError: (error: unknown) => string;
394
411
  log: (message: string) => void;
395
412
  execute: (context: { leaseId: string | null }) => Promise<T>;
@@ -412,9 +429,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
412
429
  receipt: RuntimeStepReceipt,
413
430
  source: DurableReceiptRecoverySource = 'cache',
414
431
  ): Promise<T> => {
415
- input.log(
416
- `ctx.${input.operation}(${input.id}): recovered result from receipt`,
417
- );
432
+ input.log(`ctx.${input.operation}(${input.id}): reused completed work`);
418
433
  if (receipt.output === undefined) {
419
434
  return receipt.output as T;
420
435
  }
@@ -428,6 +443,143 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
428
443
  return recovered;
429
444
  };
430
445
 
446
+ if (input.completedCacheOnly === true) {
447
+ if (input.force !== true) {
448
+ const cached = await input.store.get(input.receiptKey);
449
+ if (cached?.status === 'completed' || cached?.status === 'skipped') {
450
+ return await recoverCompletedReceipt(cached);
451
+ }
452
+ }
453
+ const needsLock = input.requiresExecutionLock === true;
454
+ const ownerExecutionId = `${input.runId}:${crypto.randomUUID()}`;
455
+ let ownsLock = false;
456
+ let lockSupervisor: ReturnType<
457
+ typeof createRuntimeReceiptHeartbeatSupervisor
458
+ > | null = null;
459
+ if (needsLock) {
460
+ if (
461
+ !input.store.acquireExecutionLock ||
462
+ !input.store.releaseExecutionLock
463
+ ) {
464
+ throw new Error(
465
+ `ctx.${input.operation}(${input.id}): non-idempotent provider requires the receipt execution-lock backend.`,
466
+ );
467
+ }
468
+ const deadline = Date.now() + DANGEROUS_SIDE_EFFECT_LOCK_WAIT_MS;
469
+ while (!ownsLock) {
470
+ const lock = await input.store.acquireExecutionLock({
471
+ receiptKey: input.receiptKey,
472
+ ownerExecutionId,
473
+ ttlMs: input.executionLockTtlMs ?? TOOL_RECEIPT_DEFAULT_WAIT_MS,
474
+ });
475
+ ownsLock = lock?.ownerExecutionId === ownerExecutionId;
476
+ if (ownsLock) break;
477
+ const winner = await input.store.get(input.receiptKey);
478
+ if (
479
+ input.force !== true &&
480
+ (winner?.status === 'completed' || winner?.status === 'skipped')
481
+ ) {
482
+ return await recoverCompletedReceipt(winner, 'in_flight');
483
+ }
484
+ if (Date.now() >= deadline) {
485
+ throw new RuntimeReceiptWaitTimeoutError(input.receiptKey);
486
+ }
487
+ await sleepReceiptWait(DURABLE_RECEIPT_WAIT_DELAY_MS);
488
+ }
489
+ const afterLock = await input.store.get(input.receiptKey);
490
+ if (
491
+ input.force !== true &&
492
+ (afterLock?.status === 'completed' ||
493
+ afterLock?.status === 'skipped')
494
+ ) {
495
+ await input.store.releaseExecutionLock({
496
+ receiptKey: input.receiptKey,
497
+ ownerExecutionId,
498
+ });
499
+ return await recoverCompletedReceipt(afterLock, 'in_flight');
500
+ }
501
+ const lockTtlMs =
502
+ input.executionLockTtlMs ?? TOOL_RECEIPT_DEFAULT_WAIT_MS;
503
+ lockSupervisor = createRuntimeReceiptHeartbeatSupervisor({
504
+ intervalMs: Math.max(1_000, Math.floor(lockTtlMs / 3)),
505
+ heartbeat: async () => {
506
+ const renewed = await input.store.acquireExecutionLock!({
507
+ receiptKey: input.receiptKey,
508
+ ownerExecutionId,
509
+ ttlMs: lockTtlMs,
510
+ });
511
+ if (renewed?.ownerExecutionId !== ownerExecutionId) {
512
+ throw new RuntimeReceiptLeaseLostError({
513
+ receiptKey: input.receiptKey,
514
+ runId: input.runId,
515
+ leaseId: ownerExecutionId,
516
+ });
517
+ }
518
+ return 'active';
519
+ },
520
+ isLeaseLost: (error) => error instanceof RuntimeReceiptLeaseLostError,
521
+ onLeaseLost: (error) => {
522
+ input.log(
523
+ `ctx.${input.operation}(${input.id}): dangerous side-effect execution fence lost: ${error instanceof Error ? error.message : String(error)}`,
524
+ );
525
+ },
526
+ });
527
+ lockSupervisor.start();
528
+ }
529
+ const releaseOwnLock = async (): Promise<void> => {
530
+ lockSupervisor?.stop();
531
+ lockSupervisor = null;
532
+ if (ownsLock) {
533
+ ownsLock = false;
534
+ await input.store.releaseExecutionLock!({
535
+ receiptKey: input.receiptKey,
536
+ ownerExecutionId,
537
+ });
538
+ }
539
+ };
540
+ let executed: T;
541
+ try {
542
+ executed = await input.execute({ leaseId: null });
543
+ } catch (error) {
544
+ await releaseOwnLock();
545
+ throw error;
546
+ }
547
+ try {
548
+ const result = input.onClaimedResult
549
+ ? input.onClaimedResult(executed, input.receiptKey)
550
+ : executed;
551
+ assertNoSecretTaint(result, `ctx.${input.operation} result`);
552
+ // Force bypasses the immutable cache. It does not grant permission to
553
+ // replace the completed fact already published under this semantic key.
554
+ if (input.force === true) {
555
+ return result;
556
+ }
557
+ const completed = await input.store.complete(
558
+ input.receiptKey,
559
+ input.runId,
560
+ isToolExecuteResult(result)
561
+ ? serializeToolExecuteResult(result)
562
+ : result,
563
+ COMPLETED_RECEIPT_CACHE_INSERT_LEASE_ID,
564
+ );
565
+ if (
566
+ completed?.status === 'completed' ||
567
+ completed?.status === 'skipped'
568
+ ) {
569
+ return await recoverCompletedReceipt(completed, 'owner');
570
+ }
571
+ const winner = await input.store.get(input.receiptKey);
572
+ if (winner?.status === 'completed' || winner?.status === 'skipped') {
573
+ return await recoverCompletedReceipt(winner, 'owner');
574
+ }
575
+ throw new Error(
576
+ `ctx.${input.operation}(${input.id}): completed receipt cache insert failed.`,
577
+ );
578
+ } finally {
579
+ await releaseOwnLock();
580
+ }
581
+ }
582
+
431
583
  let ownedLeaseId: string | null = null;
432
584
  let ownedLeaseExpiresAt: string | null = null;
433
585
 
@@ -447,7 +599,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
447
599
  }
448
600
  if (reclaimed?.status === 'failed') {
449
601
  throw new Error(
450
- `ctx.${input.operation}(${input.id}): receipt is failed and could not be reclaimed: ${reclaimed.error ?? 'unknown error'}.`,
602
+ `ctx.${input.operation}(${input.id}): previous execution failed and cannot be reused: ${reclaimed.error ?? 'unknown error'}.`,
451
603
  );
452
604
  }
453
605
  if (
@@ -509,7 +661,7 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
509
661
  receipt: RuntimeStepReceipt,
510
662
  ): Promise<never> => {
511
663
  throw new Error(
512
- `ctx.${input.operation}(${input.id}): receipt is failed and could not be claimed: ${receipt.error ?? 'unknown error'}.`,
664
+ `ctx.${input.operation}(${input.id}): previous execution failed and cannot be reused: ${receipt.error ?? 'unknown error'}.`,
513
665
  );
514
666
  };
515
667
 
@@ -556,7 +708,11 @@ export async function executeWithDurableRuntimeReceipt<T>(input: {
556
708
 
557
709
  let result: T;
558
710
  try {
559
- if (input.markRunningBeforeExecute !== false && input.store.markRunning) {
711
+ // New runners treat a claimed, nonterminal receipt as active. Keep the
712
+ // mark-running transition as an explicit compatibility option for older
713
+ // stores during their migration window, but do not pay the extra durable
714
+ // mutation on the normal claim -> terminal path.
715
+ if (input.markRunningBeforeExecute === true && input.store.markRunning) {
560
716
  const markRunningStartedAt = Date.now();
561
717
  const running = await input.store.markRunning(
562
718
  input.receiptKey,
@@ -0,0 +1,93 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { createSingleFlight } from './single-flight';
4
+
5
+ export type GatewayAuthSession<TAuth, TLaunch> = {
6
+ auth: TAuth;
7
+ launch: TLaunch;
8
+ source: 'hit' | 'miss' | 'coalesced';
9
+ };
10
+
11
+ type StoredSession<TAuth, TLaunch> = {
12
+ auth: TAuth;
13
+ launch: TLaunch;
14
+ expiresAt: number;
15
+ runId: string;
16
+ runAttempt: number | null;
17
+ };
18
+
19
+ /** Briefly reuse an authenticated immutable launch, bounded by token identity. */
20
+ export function createGatewayAuthSessionCache<TAuth, TLaunch>(options: {
21
+ ttlMs: number;
22
+ maxEntries: number;
23
+ identity: (auth: TAuth) => {
24
+ runId: string | null;
25
+ runAttempt: number | null;
26
+ };
27
+ now?: () => number;
28
+ }) {
29
+ const sessions = new Map<string, StoredSession<TAuth, TLaunch>>();
30
+ const flights = createSingleFlight<string, StoredSession<TAuth, TLaunch>>();
31
+ const now = options.now ?? Date.now;
32
+ const fingerprint = (token: string) =>
33
+ createHash('sha256').update(token).digest('base64url');
34
+
35
+ const prune = (at: number) => {
36
+ for (const [key, session] of sessions) {
37
+ if (session.expiresAt <= at) sessions.delete(key);
38
+ }
39
+ while (sessions.size >= options.maxEntries) {
40
+ const oldest = sessions.keys().next().value;
41
+ if (!oldest) break;
42
+ sessions.delete(oldest);
43
+ }
44
+ };
45
+
46
+ return {
47
+ async getOrEstablish(input: {
48
+ token: string;
49
+ tokenExpiresAt: number;
50
+ establish: () => Promise<{ auth: TAuth; launch: TLaunch }>;
51
+ }): Promise<GatewayAuthSession<TAuth, TLaunch>> {
52
+ const key = fingerprint(input.token);
53
+ const at = now();
54
+ const cached = sessions.get(key);
55
+ if (cached && cached.expiresAt > at) {
56
+ const identity = options.identity(cached.auth);
57
+ if (
58
+ identity.runId === cached.runId &&
59
+ identity.runAttempt === cached.runAttempt
60
+ ) {
61
+ return { ...cached, source: 'hit' };
62
+ }
63
+ }
64
+ if (cached) sessions.delete(key);
65
+
66
+ const coalesced = flights.has(key);
67
+ const session = await flights.run(key, async () => {
68
+ const established = await input.establish();
69
+ const identity = options.identity(established.auth);
70
+ if (!identity.runId) {
71
+ throw new Error('Gateway authentication produced no run identity.');
72
+ }
73
+ const establishedAt = now();
74
+ const stored: StoredSession<TAuth, TLaunch> = {
75
+ ...established,
76
+ expiresAt: Math.min(
77
+ establishedAt + options.ttlMs,
78
+ input.tokenExpiresAt,
79
+ ),
80
+ runId: identity.runId,
81
+ runAttempt: identity.runAttempt,
82
+ };
83
+ if (stored.expiresAt > establishedAt) {
84
+ prune(establishedAt);
85
+ sessions.set(key, stored);
86
+ }
87
+ return stored;
88
+ });
89
+ return { ...session, source: coalesced ? 'coalesced' : 'miss' };
90
+ },
91
+ size: () => sessions.size,
92
+ };
93
+ }
@@ -1,7 +1,10 @@
1
1
  import { createSecretRedactionContext } from './secret-redaction';
2
- import { RUNTIME_RECEIPT_OUTPUT_MAX_BYTES } from './output-size-limits';
2
+ import {
3
+ assertRuntimeReceiptOutputWithinLimit,
4
+ TERMINAL_RUN_RESULT_MAX_BYTES,
5
+ } from './output-size-limits';
3
6
 
4
- export const MAX_LEDGER_RESULT_PAYLOAD_BYTES = RUNTIME_RECEIPT_OUTPUT_MAX_BYTES;
7
+ export const MAX_LEDGER_RESULT_PAYLOAD_BYTES = TERMINAL_RUN_RESULT_MAX_BYTES;
5
8
  export const MAX_LEDGER_LOG_LINES_PER_EVENT = 500;
6
9
  export const MAX_LEDGER_LOG_LINE_LENGTH = 2_000;
7
10
 
@@ -21,6 +24,15 @@ export function sanitizeLedgerPayload(value: unknown): unknown {
21
24
  return ledgerIngressRedactor.redact(value);
22
25
  }
23
26
 
27
+ /** Secret-redact a receipt payload without coupling its 10 MiB cell limit to the ledger. */
28
+ export function sanitizeRuntimeReceiptPayload(value: unknown): unknown {
29
+ assertRuntimeReceiptOutputWithinLimit({
30
+ output: value,
31
+ path: 'runtime receipt output',
32
+ });
33
+ return ledgerIngressRedactor.redact(value);
34
+ }
35
+
24
36
  export function sanitizeLedgerLogLines(value: unknown): string[] {
25
37
  if (!Array.isArray(value)) return [];
26
38
  return value
@@ -3,8 +3,10 @@ const encoder = typeof TextEncoder === 'undefined' ? null : new TextEncoder();
3
3
  export const TERMINAL_RUN_RESULT_MAX_BYTES = 2_500_000;
4
4
  export const CUSTOMER_OUTPUT_VALUE_MAX_BYTES = 1 * 1024 * 1024;
5
5
  export const CUSTOMER_OUTPUT_TOTAL_MAX_BYTES = 2 * 1024 * 1024;
6
- export const RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 2 * 1024 * 1024;
7
- export const RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES = 16 * 1024 * 1024;
6
+ /** Inline Postgres receipt contract: one serialized output is at most 10 MiB. */
7
+ export const RUNTIME_RECEIPT_OUTPUT_MAX_BYTES = 10 * 1024 * 1024;
8
+ /** One completion request may contain multiple receipts up to 32 MiB total. */
9
+ export const RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES = 32 * 1024 * 1024;
8
10
 
9
11
  export class OutputTooLargeError extends Error {
10
12
  readonly code = 'OUTPUT_TOO_LARGE';
@@ -0,0 +1 @@
1
+ export type PlayCallExecution = 'inline' | 'child-workflow';
@@ -7,6 +7,7 @@ export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BATCH_SIZE = 200;
7
7
  export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_FLUSH_MS = 10;
8
8
  export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BUFFERED_BYTES =
9
9
  RUNTIME_RECEIPT_COMPLETION_BUFFER_MAX_BYTES;
10
+ export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BATCH_BYTES = 1_000_000;
10
11
  export const DEFAULT_RECEIPT_COMPLETION_SINK_MAX_INPUT_BYTES =
11
12
  RUNTIME_RECEIPT_OUTPUT_MAX_BYTES;
12
13
 
@@ -22,6 +23,8 @@ export interface RuntimeReceiptCompletionSinkOptions<TInput, TOutput> {
22
23
  maxBatchSize?: number;
23
24
  maxFlushMs?: number;
24
25
  maxBufferedBytes?: number;
26
+ /** Flush trigger and maximum ordinary transport batch size. */
27
+ maxBatchBytes?: number;
25
28
  maxInputBytes?: number;
26
29
  estimateBytes?: (input: TInput) => number;
27
30
  completeMany: (inputs: TInput[]) => Promise<TOutput[]>;
@@ -83,12 +86,14 @@ export class RuntimeReceiptCompletionSink<TInput, TOutput> {
83
86
  readonly maxBatchSize: number;
84
87
  readonly maxFlushMs: number;
85
88
  readonly maxBufferedBytes: number;
89
+ readonly maxBatchBytes: number;
86
90
  readonly maxInputBytes: number;
87
91
 
88
92
  #pending: Array<PendingReceiptCompletion<TInput, TOutput>> = [];
89
93
  #pendingHead = 0;
90
94
  /** Pending plus in-flight bytes. Released only after transport settles. */
91
95
  #residentBytes = 0;
96
+ #pendingBytes = 0;
92
97
  #capacityWaiters: CapacityWaiter[] = [];
93
98
  #flushTimer: ReceiptCompletionSinkTimer | null = null;
94
99
  #flushPromise: Promise<void> | null = null;
@@ -116,6 +121,10 @@ export class RuntimeReceiptCompletionSink<TInput, TOutput> {
116
121
  options.maxBufferedBytes,
117
122
  DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BUFFERED_BYTES,
118
123
  );
124
+ this.maxBatchBytes = normalizePositiveInteger(
125
+ options.maxBatchBytes,
126
+ DEFAULT_RECEIPT_COMPLETION_SINK_MAX_BATCH_BYTES,
127
+ );
119
128
  this.maxInputBytes = normalizePositiveInteger(
120
129
  options.maxInputBytes,
121
130
  DEFAULT_RECEIPT_COMPLETION_SINK_MAX_INPUT_BYTES,
@@ -194,12 +203,16 @@ export class RuntimeReceiptCompletionSink<TInput, TOutput> {
194
203
  }
195
204
  this.#throwIfUnavailable();
196
205
  this.#residentBytes += bytes;
206
+ this.#pendingBytes += bytes;
197
207
  const completion = new Promise<TOutput>((resolve, reject) => {
198
208
  this.#pending.push({ input, bytes, resolve, reject });
199
209
  if (this.#pendingCount() >= this.maxBatchSize) {
200
210
  void this.flush().catch(() => undefined);
201
211
  }
202
212
  });
213
+ if (this.#pendingBytes >= this.maxBatchBytes) {
214
+ void this.flush().catch(() => undefined);
215
+ }
203
216
  this.#scheduleFlush();
204
217
  return await completion;
205
218
  }
@@ -237,12 +250,13 @@ export class RuntimeReceiptCompletionSink<TInput, TOutput> {
237
250
  let batchBytes = 0;
238
251
  while (this.#pendingCount() > 0 && batch.length < this.maxBatchSize) {
239
252
  const next = this.#pending[this.#pendingHead]!;
240
- if (batch.length > 0 && batchBytes + next.bytes > this.maxBufferedBytes) {
253
+ if (batch.length > 0 && batchBytes + next.bytes > this.maxBatchBytes) {
241
254
  break;
242
255
  }
243
256
  this.#pendingHead += 1;
244
257
  batch.push(next);
245
258
  batchBytes += next.bytes;
259
+ this.#pendingBytes = Math.max(0, this.#pendingBytes - next.bytes);
246
260
  }
247
261
  if (
248
262
  this.#pendingHead > 1_024 &&
@@ -321,6 +335,7 @@ export class RuntimeReceiptCompletionSink<TInput, TOutput> {
321
335
  while (this.#pendingCount() > 0) {
322
336
  const pending = this.#pending[this.#pendingHead++]!;
323
337
  this.#residentBytes = Math.max(0, this.#residentBytes - pending.bytes);
338
+ this.#pendingBytes = Math.max(0, this.#pendingBytes - pending.bytes);
324
339
  pending.reject(this.#failure);
325
340
  }
326
341
  this.#wakeCapacityWaiters();
@@ -1,6 +1,26 @@
1
1
  import { RECEIPT_STATUS_CODE } from './receipt-status';
2
2
  import { workReceiptFailureKindCode } from './work-receipts';
3
3
 
4
+ export function workReceiptRepairableFailurePredicateSql(
5
+ receiptTable: string,
6
+ ): string {
7
+ return `(
8
+ ${receiptTable}.failure_kind = ${workReceiptFailureKindCode('repairable')}::smallint
9
+ OR (
10
+ ${receiptTable}.failure_kind = ${workReceiptFailureKindCode('terminal')}::smallint
11
+ AND ${receiptTable}.error IS NOT NULL
12
+ AND lower(${receiptTable}.error) NOT LIKE '%billing cap%'
13
+ AND lower(${receiptTable}.error) NOT LIKE '%insufficient credits%'
14
+ AND lower(${receiptTable}.error) NOT LIKE '%monthly billing limit%'
15
+ AND (
16
+ ${receiptTable}.error ~* '(^|[^0-9])5[0-9]{2}([^0-9]|$)'
17
+ OR lower(${receiptTable}.error) LIKE '%transport failed calling%'
18
+ OR lower(${receiptTable}.error) LIKE '%runtime api call timed out%'
19
+ )
20
+ )
21
+ )`;
22
+ }
23
+
4
24
  export function workReceiptClaimableStatusCodes(input: {
5
25
  forceRefresh?: boolean;
6
26
  forceFailedRefresh?: boolean;
@@ -47,7 +67,7 @@ export function workReceiptClaimConflictPredicateSql(input: {
47
67
  )
48
68
  OR ${table}.status <> ${RECEIPT_STATUS_CODE.failed}::smallint
49
69
  OR (
50
- ${table}.failure_kind = ${workReceiptFailureKindCode('repairable')}::smallint
70
+ ${workReceiptRepairableFailurePredicateSql(table)}
51
71
  AND (
52
72
  ${table}.run_id IS DISTINCT FROM ${input.claimantRunIdSql}
53
73
  OR COALESCE(${table}.lease_owner_attempt, 0) < ${claimantRunAttemptSql}::integer
@@ -1,8 +1,14 @@
1
1
  import { RuntimeReceiptCompletionSink } from './receipt-completion-sink';
2
2
 
3
+ /** Claims/state transitions are small; flush once a 512 KB SQL payload forms. */
4
+ export const DEFAULT_RECEIPT_STATE_SINK_MAX_BATCH_BYTES = 512_000;
5
+
3
6
  export interface RuntimeReceiptStateSinkOptions<TInput, TOutput> {
4
7
  maxBatchSize?: number;
5
8
  maxFlushMs?: number;
9
+ maxBatchBytes?: number;
10
+ maxBufferedBytes?: number;
11
+ maxInputBytes?: number;
6
12
  estimateBytes?: (input: TInput) => number;
7
13
  applyMany: (inputs: TInput[]) => Promise<TOutput[]>;
8
14
  onFlushStart?: (event: { count: number; bytes: number }) => void;
@@ -33,6 +39,10 @@ export class RuntimeReceiptStateSink<TInput, TOutput> {
33
39
  this.#sink = new RuntimeReceiptCompletionSink<TInput, TOutput>({
34
40
  maxBatchSize: options.maxBatchSize,
35
41
  maxFlushMs: options.maxFlushMs,
42
+ maxBatchBytes:
43
+ options.maxBatchBytes ?? DEFAULT_RECEIPT_STATE_SINK_MAX_BATCH_BYTES,
44
+ maxBufferedBytes: options.maxBufferedBytes,
45
+ maxInputBytes: options.maxInputBytes,
36
46
  estimateBytes: options.estimateBytes,
37
47
  completeMany: options.applyMany,
38
48
  onFlushStart: options.onFlushStart,