deepline 0.3.247 → 0.3.249

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 (67) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +177 -13
  2. package/dist/bundling-sources/sdk/src/index.ts +8 -0
  3. package/dist/bundling-sources/sdk/src/plays/input-example.ts +40 -0
  4. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  5. package/dist/bundling-sources/sdk/src/runs/overview.ts +457 -0
  6. package/dist/bundling-sources/sdk/src/runs/receipts.ts +23 -0
  7. package/dist/bundling-sources/sdk/src/tool-calls.ts +21 -1
  8. package/dist/bundling-sources/sdk/src/types.ts +16 -1
  9. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +19 -1
  10. package/dist/bundling-sources/shared_libs/play-data-plane/column-names.ts +17 -3
  11. package/dist/bundling-sources/shared_libs/play-data-plane/sheet-contract.ts +40 -8
  12. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +26 -1
  13. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +144 -14
  14. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +2 -1
  15. package/dist/bundling-sources/shared_libs/play-runtime/daytona-runtime-config.ts +6 -3
  16. package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-janitor.ts +26 -21
  17. package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-reaper.ts +23 -21
  18. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +17 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/log-context.ts +105 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +8 -1
  21. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +1 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +1 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +9 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +40 -4
  25. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +50 -5
  26. package/dist/bundling-sources/shared_libs/play-runtime/run-lifecycle-policy.ts +3 -1
  27. package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +28 -1
  28. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +14 -1
  29. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +56 -16
  30. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-control-transport.ts +184 -0
  31. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-launch-transport.ts +327 -0
  32. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +87 -30
  33. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +24 -13
  34. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-start-admission.ts +23 -6
  35. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +76 -33
  36. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +90 -35
  37. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +16 -0
  38. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +112 -50
  39. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +447 -77
  40. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +22 -4
  41. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +105 -121
  42. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +1648 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-queue-health.ts +16 -19
  44. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +14 -6
  45. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +16 -0
  46. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +40 -0
  47. package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +5 -2
  48. package/dist/bundling-sources/shared_libs/plays/dataset-summary.ts +11 -7
  49. package/dist/bundling-sources/shared_libs/plays/static-pipeline-storage-codec.ts +5 -2
  50. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +11 -2
  51. package/dist/cli/index.js +3504 -2368
  52. package/dist/cli/index.mjs +3188 -2052
  53. package/dist/{compiler-manifest-n8ZtPv2h.d.mts → compiler-manifest-DavmLa8y.d.mts} +1 -0
  54. package/dist/{compiler-manifest-n8ZtPv2h.d.ts → compiler-manifest-DavmLa8y.d.ts} +1 -0
  55. package/dist/index.d.mts +97 -5
  56. package/dist/index.d.ts +97 -5
  57. package/dist/index.js +135 -15
  58. package/dist/index.mjs +135 -15
  59. package/dist/install-integrity.json +9 -2
  60. package/dist/plays/bundle-play-file.d.mts +2 -2
  61. package/dist/plays/bundle-play-file.d.ts +2 -2
  62. package/dist/plays/bundle-play-file.mjs +1 -1
  63. package/dist/release.d.mts +1 -1
  64. package/dist/release.d.ts +1 -1
  65. package/dist/release.js +1 -1
  66. package/dist/release.mjs +1 -1
  67. package/package.json +1 -1
@@ -25,6 +25,7 @@ import type {
25
25
  PlayRunnerRuntimeTiming,
26
26
  } from '../protocol';
27
27
  import { PROD_DEFAULT_RUNTIME_SCHEDULER_SCHEMA } from '../runtime-scheduler-topology';
28
+ import { RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER } from './runtime-event-search';
28
29
  import type { PlayRunnerRuntimeResource } from '../runner-backends/types';
29
30
  import {
30
31
  RuntimeSandboxOperationState,
@@ -76,6 +77,10 @@ import {
76
77
  type DocflowObservationUpsert,
77
78
  } from '../docflow-observation';
78
79
  import { normalizePlayDocflowNodeIoState } from '../docflow-node-io';
80
+ import {
81
+ normalizePlayLogContext,
82
+ type PlayLogContext,
83
+ } from '../log-context';
79
84
  import {
80
85
  workReceiptFailureKindCodeForWrite,
81
86
  workReceiptFailureKindFromCode,
@@ -299,7 +304,11 @@ const POSTGRES_SCHEDULER_SCHEMA_INIT_STATEMENT_TIMEOUT_MS = 60_000;
299
304
  // v46 adds a bounded provider-scope Retry-After hold with shared adaptive-rate
300
305
  // state. The scope owns learning and dispatch across runs and tool rules;
301
306
  // existing deadline rows expand additively and initialize on their first seed.
302
- export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 46;
307
+ // v47 attaches the append-only runtime event projection to durable outbox and
308
+ // receipt transitions; no existing runtime wire field changes.
309
+ // v48 expands optional compact-log columns and readers before writer enablement.
310
+ // v49 installs transactional compact-log writers; only new admissions opt in.
311
+ export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 49;
303
312
  const POSTGRES_SCHEDULER_SCHEMA_MIGRATIONS_TABLE =
304
313
  'scheduler_schema_migrations';
305
314
  export const POSTGRES_SCHEDULER_OPEN_RECOVERY_INDEX =
@@ -885,6 +894,8 @@ const OUTBOX_RETRY_MAX_MS = 15 * 60 * 1_000;
885
894
  // diagnostic window, then delete in small, independently committed batches.
886
895
  export const POSTGRES_SCHEDULER_OUTBOX_DELIVERED_RETENTION_MS =
887
896
  30 * 24 * 60 * 60 * 1_000;
897
+ export const POSTGRES_SCHEDULER_OUTBOX_LOG_BODY_RETENTION_MS =
898
+ 7 * 24 * 60 * 60 * 1_000;
888
899
  export const POSTGRES_SCHEDULER_OUTBOX_DELIVERED_RETENTION_BATCH_LIMIT = 1_000;
889
900
  const RUN_LEDGER_DISPATCH_TOKEN_TIMEOUT_MS = 15_000;
890
901
  const COMPUTE_BILLING_DISPATCH_TOKEN_TIMEOUT_MS = 15_000;
@@ -928,6 +939,20 @@ function schemaName(
928
939
  );
929
940
  }
930
941
 
942
+ /** Only new immutable admissions opt into the fully installed recorder. */
943
+ function newRunLogStoreSql(
944
+ launchSql: 'launch_payload.launch_json',
945
+ options?: Pick<PostgresSchedulerOptions, 'schema'>,
946
+ ): string {
947
+ return `CASE WHEN ${launchSql} ? 'playReference' AND EXISTS (
948
+ SELECT 1 FROM pg_proc AS proc
949
+ JOIN pg_namespace AS namespace ON namespace.oid = proc.pronamespace
950
+ WHERE namespace.nspname = '${schemaName(options).replace(/'/g, "''")}'
951
+ AND proc.proname = 'search_outbox_event'
952
+ AND obj_description(proc.oid, 'pg_proc') = '${RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER}'
953
+ ) THEN 'planet_scale' END`;
954
+ }
955
+
931
956
  function tableName(
932
957
  table: string,
933
958
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
@@ -1101,6 +1126,8 @@ export type PostgresSchedulerSchemaShapeRequirements = Readonly<{
1101
1126
  rateRequesterFairShare?: boolean;
1102
1127
  /** v45 added durable Retry-After holds shared across rules and runs. */
1103
1128
  rateScopeRetryAfter?: boolean;
1129
+ /** v48 expands compact log storage; prior readers ignore these columns. */
1130
+ runtimeLogStorage?: boolean;
1104
1131
  }>;
1105
1132
 
1106
1133
  export async function hasCurrentPostgresSchedulerSchemaShape(
@@ -1718,6 +1745,22 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
1718
1745
  AND index_row.indisvalid
1719
1746
  )
1720
1747
  )
1748
+ AND (
1749
+ $38::boolean = false
1750
+ OR (
1751
+ EXISTS (
1752
+ SELECT 1 FROM information_schema.columns
1753
+ WHERE table_schema = $1 AND table_name = 'runs'
1754
+ AND column_name = 'run_log_store' AND is_nullable = 'YES'
1755
+ )
1756
+ AND (
1757
+ SELECT count(*) FROM information_schema.columns
1758
+ WHERE table_schema = $1 AND table_name = 'attempts'
1759
+ AND column_name IN ('runner_result_digest', 'runner_log_payload_json')
1760
+ AND is_nullable = 'YES'
1761
+ ) = 2
1762
+ )
1763
+ )
1721
1764
  AS ready
1722
1765
  `,
1723
1766
  [
@@ -1758,6 +1801,7 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
1758
1801
  `${quotedSchema}.${quoteIdent('rate_requesters')}`,
1759
1802
  requirements.rateScopeRetryAfter !== false,
1760
1803
  `${quotedSchema}.${quoteIdent('rate_scope_holds')}`,
1804
+ requirements.runtimeLogStorage !== false,
1761
1805
  ],
1762
1806
  );
1763
1807
  if (currentShape.rows[0]?.ready !== true) return false;
@@ -1980,6 +2024,19 @@ export async function ensurePostgresSchedulerOnlineAdditiveExpansion(
1980
2024
  options,
1981
2025
  )} (state, updated_at)
1982
2026
  `);
2027
+ await ensurePostgresSchedulerRunLogStorageExpansion(client, options);
2028
+ }
2029
+
2030
+ /** Deployment-only expansion; absent values keep legacy storage authoritative. */
2031
+ async function ensurePostgresSchedulerRunLogStorageExpansion(
2032
+ client: PostgresSchedulerQueryClient,
2033
+ options?: Pick<PostgresSchedulerOptions, 'schema'>,
2034
+ ): Promise<void> {
2035
+ await client.query(`ALTER TABLE ${tableName('runs', options)}
2036
+ ADD COLUMN IF NOT EXISTS run_log_store text`);
2037
+ await client.query(`ALTER TABLE ${tableName('attempts', options)}
2038
+ ADD COLUMN IF NOT EXISTS runner_result_digest text,
2039
+ ADD COLUMN IF NOT EXISTS runner_log_payload_json jsonb`);
1983
2040
  }
1984
2041
 
1985
2042
  /** Read the highest deployment-owned scheduler schema marker. */
@@ -2546,6 +2603,7 @@ export async function ensurePostgresSchedulerSchema(
2546
2603
  ALTER TABLE ${tableName('attempts', options)}
2547
2604
  ADD COLUMN IF NOT EXISTS runner_terminal_received_at timestamptz
2548
2605
  `);
2606
+ await ensurePostgresSchedulerRunLogStorageExpansion(client, options);
2549
2607
  // Authoritative provenance for terminal reporting. Do not infer this from
2550
2608
  // the completed/cleanup state shape: direct ingress and compatibility wake
2551
2609
  // can both produce those rows.
@@ -4882,7 +4940,8 @@ async function submitPostgresSchedulerRunTransaction(
4882
4940
  input_json,
4883
4941
  status,
4884
4942
  absurd_task_id,
4885
- admission_json
4943
+ admission_json,
4944
+ run_log_store
4886
4945
  )
4887
4946
  SELECT $1, $2, $3, $4, $5, $6, $7, launch_payload.launch_json, coalesce(nullif(launch_payload.launch_json -> 'input', 'null'::jsonb), '{}'::jsonb), 'queued',
4888
4947
  (SELECT task_id FROM task_spawn),
@@ -4894,7 +4953,8 @@ async function submitPostgresSchedulerRunTransaction(
4894
4953
  'runtimeDeployVersion',
4895
4954
  'coordinatorInternalToken',
4896
4955
  'vercelProtectionBypassToken'
4897
- ]::text[]
4956
+ ]::text[],
4957
+ ${newRunLogStoreSql('launch_payload.launch_json', options)}
4898
4958
  FROM admission
4899
4959
  CROSS JOIN launch_payload
4900
4960
  WHERE NOT existing_run
@@ -4902,7 +4962,7 @@ async function submitPostgresSchedulerRunTransaction(
4902
4962
  AND parent_accepting
4903
4963
  AND NOT ${existingOnlySql}
4904
4964
  ON CONFLICT (run_id) DO NOTHING
4905
- RETURNING run_id, absurd_task_id
4965
+ RETURNING run_id, absurd_task_id, run_log_store
4906
4966
  ), run_recovery AS (
4907
4967
  UPDATE ${tableName('runs', options)} AS runs
4908
4968
  SET absurd_task_id = (SELECT task_id FROM task_spawn),
@@ -4949,7 +5009,8 @@ async function submitPostgresSchedulerRunTransaction(
4949
5009
  'graphHash', $5::text,
4950
5010
  'runtimeBackend', $6::text,
4951
5011
  'schedulerBackend', launch_payload.launch_json ->> 'schedulerBackend',
4952
- 'schedulerSchema', launch_payload.launch_json ->> 'schedulerSchema',
5012
+ 'schedulerSchema', COALESCE(launch_payload.launch_json ->> 'schedulerSchema', '${schemaName(options)}'),
5013
+ 'runLogStore', run_insert.run_log_store,
4953
5014
  'executionProfile', launch_payload.launch_json ->> 'executionProfile',
4954
5015
  'runtimeReleaseId', launch_payload.launch_json ->> 'runtimeReleaseId',
4955
5016
  'runtimeDeployVersion', launch_payload.launch_json ->> 'runtimeDeployVersion',
@@ -5951,7 +6012,7 @@ export async function firePostgresSchedulerCronSchedule(
5951
6012
  INSERT INTO ${tableName('runs', options)} (
5952
6013
  run_id, org_id, play_id, play_name, graph_hash, runtime_profile,
5953
6014
  artifact_storage_key, launch_json, input_json, status,
5954
- absurd_task_id, admission_json
6015
+ absurd_task_id, admission_json, run_log_store
5955
6016
  )
5956
6017
  SELECT $5::text,
5957
6018
  launch_payload.launch_json ->> 'orgId',
@@ -5972,12 +6033,13 @@ export async function firePostgresSchedulerCronSchedule(
5972
6033
  'runtimeDeployVersion',
5973
6034
  'coordinatorInternalToken',
5974
6035
  'vercelProtectionBypassToken'
5975
- ]::text[]
6036
+ ]::text[],
6037
+ ${newRunLogStoreSql('launch_payload.launch_json', options)}
5976
6038
  FROM occurrence
5977
6039
  CROSS JOIN launch_payload
5978
6040
  WHERE occurrence.schedule_current
5979
6041
  AND NOT occurrence.existing_run
5980
- RETURNING run_id
6042
+ RETURNING run_id, run_log_store
5981
6043
  ), occurrence_admitted AS MATERIALIZED (
5982
6044
  SELECT true AS admitted
5983
6045
  FROM occurrence
@@ -6023,7 +6085,8 @@ export async function firePostgresSchedulerCronSchedule(
6023
6085
  'graphHash', launch_payload.launch_json ->> 'graphHash',
6024
6086
  'runtimeBackend', launch_payload.launch_json ->> 'runtimeBackend',
6025
6087
  'schedulerBackend', launch_payload.launch_json ->> 'schedulerBackend',
6026
- 'schedulerSchema', launch_payload.launch_json ->> 'schedulerSchema',
6088
+ 'schedulerSchema', COALESCE(launch_payload.launch_json ->> 'schedulerSchema', '${schemaName(options)}'),
6089
+ 'runLogStore', run_insert.run_log_store,
6027
6090
  'executionProfile', launch_payload.launch_json ->> 'executionProfile',
6028
6091
  'runtimeReleaseId', launch_payload.launch_json ->> 'runtimeReleaseId',
6029
6092
  'runtimeDeployVersion', launch_payload.launch_json ->> 'runtimeDeployVersion',
@@ -8472,6 +8535,36 @@ export type PostgresSchedulerAttemptAuthority = Pick<
8472
8535
  (typeof POSTGRES_SCHEDULER_ATTEMPT_AUTHORITY_FIELDS)[number]
8473
8536
  >;
8474
8537
 
8538
+ type PostgresSchedulerAuthorityRow = PostgresSchedulerAttemptAuthority & {
8539
+ launch_required_fields_valid: boolean;
8540
+ };
8541
+
8542
+ // Keep JSON types intact: text columns would coerce malformed launch scalars.
8543
+ const POSTGRES_SCHEDULER_AUTHORITY_COLUMNS = [
8544
+ ...POSTGRES_SCHEDULER_ATTEMPT_AUTHORITY_FIELDS,
8545
+ 'playId',
8546
+ 'executorToken',
8547
+ ]
8548
+ .map((field) => `${quoteIdent(field)} jsonb`)
8549
+ .join(', ');
8550
+ const POSTGRES_SCHEDULER_AUTHORITY_SELECT = `${POSTGRES_SCHEDULER_ATTEMPT_AUTHORITY_FIELDS.map((field) => `launch.${quoteIdent(field)}`).join(', ')},
8551
+ ${['runId', 'playId', 'playName', 'executorToken', 'baseUrl']
8552
+ .map((field) => `jsonb_typeof(launch.${quoteIdent(field)}) = 'string'`)
8553
+ .join(' AND ')} AS launch_required_fields_valid`;
8554
+
8555
+ function parsePostgresSchedulerAuthorityRow(
8556
+ row: PostgresSchedulerAuthorityRow | undefined,
8557
+ ): PostgresSchedulerAttemptAuthority | null {
8558
+ if (!row) return null;
8559
+ const { launch_required_fields_valid, ...authority } = row;
8560
+ if (launch_required_fields_valid !== true) {
8561
+ throw new Error(
8562
+ 'Postgres scheduler launch_json is missing required fields.',
8563
+ );
8564
+ }
8565
+ return authority;
8566
+ }
8567
+
8475
8568
  /**
8476
8569
  * Prove the same attempt lease as the full execution reader, returning only
8477
8570
  * the token issuer's authority. Input, graph, and artifact bytes never cross
@@ -8484,29 +8577,13 @@ export async function readPostgresSchedulerAttemptAuthorityForLease(
8484
8577
  input: { runId: string; attempt: number; leaseToken: string },
8485
8578
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
8486
8579
  ): Promise<PostgresSchedulerAttemptAuthority | null> {
8487
- const fields = POSTGRES_SCHEDULER_ATTEMPT_AUTHORITY_FIELDS;
8488
- // Keep JSON types intact. A text record column would coerce malformed launch
8489
- // scalars into strings, bypassing the full reader's required-field checks.
8490
- const columns = [...fields, 'playId', 'executorToken']
8491
- .map((field) => `${quoteIdent(field)} jsonb`)
8492
- .join(', ');
8493
- const result = await client.query<
8494
- PostgresSchedulerAttemptAuthority & {
8495
- launch_required_fields_valid: boolean;
8496
- }
8497
- >(
8580
+ const result = await client.query<PostgresSchedulerAuthorityRow>(
8498
8581
  `
8499
- SELECT ${fields.map((field) => `launch.${quoteIdent(field)}`).join(', ')},
8500
- ${['runId', 'playId', 'playName', 'executorToken', 'baseUrl']
8501
- .map(
8502
- (field) =>
8503
- `jsonb_typeof(launch.${quoteIdent(field)}) = 'string'`,
8504
- )
8505
- .join(' AND ')} AS launch_required_fields_valid
8582
+ SELECT ${POSTGRES_SCHEDULER_AUTHORITY_SELECT}
8506
8583
  FROM ${tableName('runs', options)} AS runs
8507
8584
  JOIN ${tableName('attempts', options)} AS attempts
8508
8585
  ON attempts.run_id = runs.run_id
8509
- CROSS JOIN LATERAL jsonb_to_record(runs.launch_json) AS launch(${columns})
8586
+ CROSS JOIN LATERAL jsonb_to_record(runs.launch_json) AS launch(${POSTGRES_SCHEDULER_AUTHORITY_COLUMNS})
8510
8587
  WHERE runs.run_id = $1
8511
8588
  AND runs.status IN ('queued', 'running', 'waiting')
8512
8589
  AND attempts.attempt = $2
@@ -8516,15 +8593,7 @@ export async function readPostgresSchedulerAttemptAuthorityForLease(
8516
8593
  `,
8517
8594
  [input.runId, input.attempt, input.leaseToken],
8518
8595
  );
8519
- const row = result.rows[0];
8520
- if (!row) return null;
8521
- const { launch_required_fields_valid, ...authority } = row;
8522
- if (launch_required_fields_valid !== true) {
8523
- throw new Error(
8524
- 'Postgres scheduler launch_json is missing required fields.',
8525
- );
8526
- }
8527
- return authority;
8596
+ return parsePostgresSchedulerAuthorityRow(result.rows[0]);
8528
8597
  }
8529
8598
 
8530
8599
  /**
@@ -9655,16 +9724,29 @@ export async function settlePostgresSchedulerRuntimeResources(
9655
9724
  */
9656
9725
  export async function recordPostgresSchedulerRunnerTerminal(
9657
9726
  client: PostgresSchedulerQueryClient,
9658
- input: { runId: string; attempt: number; result: unknown },
9727
+ input: {
9728
+ runId: string;
9729
+ attempt: number;
9730
+ result: unknown;
9731
+ terminalProgressEvents?: readonly unknown[];
9732
+ },
9659
9733
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
9660
9734
  ): Promise<boolean> {
9661
9735
  const recorded = await client.query<{ run_id: string }>(
9662
9736
  `
9663
9737
  WITH accepted AS MATERIALIZED (
9738
+ -- Match direct completion's Run -> attempt lock order. The log
9739
+ -- acceptance trigger also locks the Run for sequence assignment.
9664
9740
  SELECT clock_timestamp() AS accepted_at
9665
- )
9741
+ FROM ${tableName('runs', options)}
9742
+ WHERE run_id = $1
9743
+ )
9666
9744
  UPDATE ${tableName('attempts', options)} AS target
9667
9745
  SET runner_result_json = COALESCE(target.runner_result_json, $3::jsonb),
9746
+ runner_log_payload_json = CASE WHEN EXISTS (
9747
+ SELECT 1 FROM ${tableName('runs', options)} AS log_run
9748
+ WHERE log_run.run_id = target.run_id AND log_run.run_log_store = 'planet_scale'
9749
+ ) THEN $4::jsonb ELSE NULL END,
9668
9750
  runner_terminal_received_at = COALESCE(
9669
9751
  target.runner_terminal_received_at,
9670
9752
  accepted.accepted_at
@@ -9722,7 +9804,12 @@ export async function recordPostgresSchedulerRunnerTerminal(
9722
9804
  AND target.attempt = $2
9723
9805
  RETURNING target.run_id
9724
9806
  `,
9725
- [input.runId, input.attempt, stringifyPostgresJson(input.result ?? null)],
9807
+ [
9808
+ input.runId,
9809
+ input.attempt,
9810
+ stringifyPostgresJson(input.result ?? null),
9811
+ stringifyPostgresJson(runnerTerminalLogPayload(input)),
9812
+ ],
9726
9813
  );
9727
9814
  return recorded.rows.length > 0;
9728
9815
  }
@@ -9817,10 +9904,11 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
9817
9904
  const ledgerTerminalResult = terminalRunResultForLedger(terminalResult, {
9818
9905
  content: 'full',
9819
9906
  });
9820
- const terminalLogLines = terminalLogLinesForOutbox(
9907
+ const terminalLogProjection = terminalLogProjectionForOutbox(
9821
9908
  input.result,
9822
9909
  input.terminalLogOutbox,
9823
9910
  );
9911
+ const terminalLogLines = terminalLogProjection.lines;
9824
9912
  const runtimeTiming = extractRunnerRuntimeTiming(input.result);
9825
9913
  const terminalWorkCounterSegment = terminalWorkCounterSegmentFromRunnerResult(
9826
9914
  input.result,
@@ -9855,6 +9943,10 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
9855
9943
  SET status = 'completed',
9856
9944
  terminalization_mode = 'direct',
9857
9945
  runner_result_json = COALESCE(target.runner_result_json, $3::jsonb),
9946
+ runner_log_payload_json = CASE WHEN EXISTS (
9947
+ SELECT 1 FROM ${tableName('runs', options)} AS log_run
9948
+ WHERE log_run.run_id = target.run_id AND log_run.run_log_store = 'planet_scale'
9949
+ ) THEN $19::jsonb ELSE NULL END,
9858
9950
  runner_terminal_received_at = COALESCE(
9859
9951
  target.runner_terminal_received_at,
9860
9952
  run.accepted_at
@@ -9910,7 +10002,13 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
9910
10002
  AND target.attempt = $2
9911
10003
  AND target.status IN ('running', 'waiting')
9912
10004
  AND target.lease_token = $11
9913
- AND (target.runner_result_json IS NULL OR target.runner_result_json = $3::jsonb)
10005
+ AND (
10006
+ target.runner_result_json IS NULL
10007
+ OR target.runner_result_json = $3::jsonb
10008
+ OR target.runner_result_digest = encode(
10009
+ sha256(convert_to(($3::jsonb)::text, 'UTF8')), 'hex'
10010
+ )
10011
+ )
9914
10012
  AND run.run_id = target.run_id
9915
10013
  AND NOT EXISTS (
9916
10014
  SELECT 1
@@ -9923,7 +10021,8 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
9923
10021
  target.run_id,
9924
10022
  target.attempt,
9925
10023
  target.runner_terminal_received_at,
9926
- target.resource_json
10024
+ target.resource_json,
10025
+ target.runner_log_payload_json
9927
10026
  ),
9928
10027
  settled_attempt_resources AS (
9929
10028
  UPDATE ${tableName('attempts', options)} AS attempt_resources
@@ -10069,10 +10168,16 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
10069
10168
  'run.completed',
10070
10169
  jsonb_build_object(
10071
10170
  'output', $6::jsonb,
10171
+ 'producerAttempt', $2::integer,
10072
10172
  'terminalLogLines', $7::jsonb,
10073
- 'terminalProgressEvents', $8::jsonb,
10173
+ 'terminalLogContexts', $17::jsonb,
10174
+ 'terminalProgressEvents', coalesce((
10175
+ SELECT accepted.runner_log_payload_json->'terminalProgressEvents'
10176
+ FROM completed_attempt AS accepted
10177
+ WHERE accepted.run_id = run_terminal.run_id
10178
+ ), $8::jsonb),
10074
10179
  'resultSummary', $16::jsonb,
10075
- 'terminalWorkCounterSegment', $17::jsonb
10180
+ 'terminalWorkCounterSegment', $18::jsonb
10076
10181
  ),
10077
10182
  ${causalOutboxCreatedAt('run_terminal.run_id', options)}
10078
10183
  FROM run_terminal
@@ -10107,7 +10212,9 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
10107
10212
  input.engineWake.wakeChannel,
10108
10213
  input.allowCappedCompletion === true,
10109
10214
  stringifyPostgresJson(resultSummary),
10215
+ JSON.stringify(terminalLogProjection.lineContexts),
10110
10216
  stringifyPostgresJson(terminalWorkCounterSegment ?? null),
10217
+ stringifyPostgresJson(runnerTerminalLogPayload(input)),
10111
10218
  ],
10112
10219
  );
10113
10220
  if (completed.rows.length > 0) return 'completed';
@@ -10618,6 +10725,36 @@ export async function claimPostgresSchedulerComputeBillingJobs(
10618
10725
  }));
10619
10726
  }
10620
10727
 
10728
+ /**
10729
+ * Issue terminal compute-billing authority under the same settlement lease as
10730
+ * the full worker reader, without hydrating execution input or retained graphs.
10731
+ * Historical launches keep their absent-descriptor fallback in the issuer.
10732
+ */
10733
+ export async function readComputeBillingDispatchAuthorityForLease(
10734
+ client: PostgresSchedulerQueryClient,
10735
+ input: { runId: string; leaseOwner: string; attemptCount?: number },
10736
+ options?: Pick<PostgresSchedulerOptions, 'schema'>,
10737
+ ): Promise<PostgresSchedulerAttemptAuthority | null> {
10738
+ const result = await client.query<PostgresSchedulerAuthorityRow>(
10739
+ `
10740
+ SELECT ${POSTGRES_SCHEDULER_AUTHORITY_SELECT}
10741
+ FROM ${tableName('compute_billing_jobs_v2', options)} AS job
10742
+ JOIN ${tableName('runs', options)} AS runs ON runs.run_id = job.run_id
10743
+ CROSS JOIN LATERAL jsonb_to_record(runs.launch_json)
10744
+ AS launch(${POSTGRES_SCHEDULER_AUTHORITY_COLUMNS})
10745
+ WHERE job.run_id = $1
10746
+ AND job.status = 'settling'
10747
+ AND job.lease_owner = $2
10748
+ AND job.lease_expires_at > now()
10749
+ AND ($3::integer IS NULL OR job.attempt_count = $3)
10750
+ AND runs.status IN ('completed', 'failed', 'cancelled')
10751
+ LIMIT 1
10752
+ `,
10753
+ [input.runId, input.leaseOwner, input.attemptCount ?? null],
10754
+ );
10755
+ return parsePostgresSchedulerAuthorityRow(result.rows[0]);
10756
+ }
10757
+
10621
10758
  export async function readComputeBillingDispatchLaunchForLease(
10622
10759
  client: PostgresSchedulerQueryClient,
10623
10760
  input: { runId: string; leaseOwner: string; attemptCount?: number },
@@ -12075,7 +12212,8 @@ export async function isPostgresSchedulerRunnerProgressAttemptCurrent(
12075
12212
  * Bumps `heartbeat_at` on the runner's attempt row (the same liveness column
12076
12213
  * the worker's own state transitions maintain — no new schema) and returns the
12077
12214
  * play-level run status so the gateway can put cancellation on the heartbeat
12078
- * channel. Returns null when the run does not exist.
12215
+ * channel. Returns null when the run is missing or this attempt is no longer
12216
+ * current while the run remains active.
12079
12217
  */
12080
12218
  export async function heartbeatPostgresSchedulerDetachedRunner(
12081
12219
  client: PostgresSchedulerQueryClient,
@@ -12125,7 +12263,26 @@ export async function heartbeatPostgresSchedulerDetachedRunner(
12125
12263
  `,
12126
12264
  [input.runId, input.attempt],
12127
12265
  );
12128
- return heartbeat.rows[0]?.status ?? null;
12266
+ const status = heartbeat.rows[0]?.status;
12267
+ if (status) return status;
12268
+
12269
+ // A terminal writer can commit while the UPDATE waits for its attempt lock.
12270
+ // PostgreSQL then rechecks the changed attempt and skips it, but the UNION
12271
+ // still reads the statement's earlier snapshot. Read terminal state with a
12272
+ // fresh snapshot after that wait; never acknowledge a replacement's active
12273
+ // state without renewing this exact attempt through the fence above.
12274
+ const terminal = await client.query<{
12275
+ status: 'completed' | 'failed' | 'cancelled';
12276
+ }>(
12277
+ `
12278
+ SELECT status
12279
+ FROM ${tableName('runs', options)}
12280
+ WHERE run_id = $1
12281
+ AND status IN ('completed', 'failed', 'cancelled')
12282
+ `,
12283
+ [input.runId],
12284
+ );
12285
+ return terminal.rows[0]?.status ?? null;
12129
12286
  }
12130
12287
 
12131
12288
  export async function claimPostgresSchedulerSignal(
@@ -12214,6 +12371,7 @@ export async function completePostgresSchedulerAttempt(
12214
12371
  rowOutcomes?: unknown;
12215
12372
  runtimeTiming?: PlayRunnerRuntimeTiming | null;
12216
12373
  terminalLogLines?: readonly string[] | null;
12374
+ terminalLogContexts?: readonly (PlayLogContext | null)[] | null;
12217
12375
  terminalWorkCounterSegment?: PlayRunLedgerTerminalWorkCounterSegment;
12218
12376
  },
12219
12377
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
@@ -12223,10 +12381,25 @@ export async function completePostgresSchedulerAttempt(
12223
12381
  content: 'full',
12224
12382
  });
12225
12383
  const resultSummary = terminalRowOutcomesSummary(input.rowOutcomes);
12226
- const terminalLogLines = (input.terminalLogLines ?? [])
12227
- .map((line) => line.trim())
12228
- .filter(Boolean)
12384
+ if (
12385
+ input.terminalLogContexts != null &&
12386
+ input.terminalLogContexts.length !== (input.terminalLogLines ?? []).length
12387
+ ) {
12388
+ throw new TypeError(
12389
+ 'Terminal log contexts must align with terminal log lines.',
12390
+ );
12391
+ }
12392
+ const terminalLogEntries = (input.terminalLogLines ?? [])
12393
+ .map((line, index) => ({
12394
+ line: line.trim(),
12395
+ context: input.terminalLogContexts?.[index] ?? null,
12396
+ }))
12397
+ .filter((entry) => entry.line.length > 0)
12229
12398
  .slice(-LOG_TAIL_LIMIT);
12399
+ const terminalLogLines = terminalLogEntries.map((entry) => entry.line);
12400
+ const terminalLogContexts = terminalLogEntries.map((entry) =>
12401
+ normalizePlayLogContext(entry.context),
12402
+ );
12230
12403
  const completed = await client.query<{ run_id: string }>(
12231
12404
  `
12232
12405
  WITH completed AS (
@@ -12401,9 +12574,11 @@ export async function completePostgresSchedulerAttempt(
12401
12574
  'run.completed',
12402
12575
  jsonb_build_object(
12403
12576
  'output', $9::jsonb,
12577
+ 'producerAttempt', $2::integer,
12404
12578
  'terminalLogLines', $8::jsonb,
12579
+ 'terminalLogContexts', $11::jsonb,
12405
12580
  'resultSummary', $10::jsonb,
12406
- 'terminalWorkCounterSegment', $11::jsonb
12581
+ 'terminalWorkCounterSegment', $12::jsonb
12407
12582
  ),
12408
12583
  ${causalOutboxCreatedAt('run_terminal.run_id', options)}
12409
12584
  FROM run_terminal
@@ -12425,6 +12600,7 @@ export async function completePostgresSchedulerAttempt(
12425
12600
  JSON.stringify(terminalLogLines),
12426
12601
  stringifyPostgresJson(ledgerTerminalResult ?? null),
12427
12602
  stringifyPostgresJson(resultSummary),
12603
+ JSON.stringify(terminalLogContexts),
12428
12604
  stringifyPostgresJson(input.terminalWorkCounterSegment ?? null),
12429
12605
  ],
12430
12606
  );
@@ -12443,6 +12619,7 @@ export async function failPostgresSchedulerAttempt(
12443
12619
  error: string;
12444
12620
  result?: unknown;
12445
12621
  terminalLogLines?: readonly string[] | null;
12622
+ terminalLogContexts?: readonly (PlayLogContext | null)[] | null;
12446
12623
  /** Refuse to overwrite an authoritative detached runner terminal that won
12447
12624
  * after claim. Resumable `suspended` receipts may be superseded by the
12448
12625
  * hard ceiling in the same fenced statement. */
@@ -12478,10 +12655,25 @@ export async function failPostgresSchedulerAttempt(
12478
12655
  );
12479
12656
  // Keep the recovery tail separate from result: large failures are represented
12480
12657
  // by a result reference, but their customer guidance must still be logged.
12481
- const terminalLogLines = (input.terminalLogLines ?? [])
12482
- .map((line) => line.trim())
12483
- .filter(Boolean)
12658
+ if (
12659
+ input.terminalLogContexts != null &&
12660
+ input.terminalLogContexts.length !== (input.terminalLogLines ?? []).length
12661
+ ) {
12662
+ throw new TypeError(
12663
+ 'Terminal log contexts must align with terminal log lines.',
12664
+ );
12665
+ }
12666
+ const terminalLogEntries = (input.terminalLogLines ?? [])
12667
+ .map((line, index) => ({
12668
+ line: line.trim(),
12669
+ context: input.terminalLogContexts?.[index] ?? null,
12670
+ }))
12671
+ .filter((entry) => entry.line.length > 0)
12484
12672
  .slice(-LOG_TAIL_LIMIT);
12673
+ const terminalLogLines = terminalLogEntries.map((entry) => entry.line);
12674
+ const terminalLogContexts = terminalLogEntries.map((entry) =>
12675
+ normalizePlayLogContext(entry.context),
12676
+ );
12485
12677
  const failed = await client.query<{ run_id: string }>(
12486
12678
  `
12487
12679
  WITH failed AS (
@@ -12716,9 +12908,11 @@ export async function failPostgresSchedulerAttempt(
12716
12908
  jsonb_build_object(
12717
12909
  'error', $4::jsonb ->> 'message',
12718
12910
  'result', $8::jsonb,
12911
+ 'producerAttempt', $2::integer,
12719
12912
  'terminalLogLines', $12::jsonb,
12913
+ 'terminalLogContexts', $14::jsonb,
12720
12914
  'resultSummary', $13::jsonb,
12721
- 'terminalWorkCounterSegment', $14::jsonb
12915
+ 'terminalWorkCounterSegment', $15::jsonb
12722
12916
  ),
12723
12917
  ${causalOutboxCreatedAt('run_terminal.run_id', options)}
12724
12918
  FROM run_terminal
@@ -12743,6 +12937,7 @@ export async function failPostgresSchedulerAttempt(
12743
12937
  input.preserveRunnerTerminal === true,
12744
12938
  JSON.stringify(terminalLogLines),
12745
12939
  stringifyPostgresJson(resultSummary),
12940
+ JSON.stringify(terminalLogContexts),
12746
12941
  stringifyPostgresJson(terminalWorkCounterSegment ?? null),
12747
12942
  ],
12748
12943
  );
@@ -13431,37 +13626,93 @@ function boundedRunLogTailMetadata(logs: string[]): {
13431
13626
  return tail.length > 0 ? { tail, totalLogCount: logs.length } : null;
13432
13627
  }
13433
13628
 
13434
- function terminalLogLinesForOutbox(
13629
+ /** Full carrier exists only in the statement that consumes its observations. */
13630
+ function runnerTerminalLogPayload(input: {
13631
+ attempt: number;
13632
+ result: unknown;
13633
+ terminalProgressEvents?: readonly unknown[];
13634
+ }): Record<string, unknown> {
13635
+ const projection = terminalLogProjectionForOutbox(
13636
+ input.result,
13637
+ 'ctx_log_tail',
13638
+ );
13639
+ return {
13640
+ producerAttempt: input.attempt,
13641
+ terminalLogLines: projection.lines,
13642
+ terminalLogContexts: projection.lineContexts,
13643
+ terminalProgressEvents: input.terminalProgressEvents ?? [],
13644
+ // Older runner envelopes conveyed these diagnostics only in tagged log
13645
+ // text. Preserve their small structured facts before removing that text.
13646
+ recoveredRuntimeDiagnostics:
13647
+ input.result &&
13648
+ typeof input.result === 'object' &&
13649
+ 'logs' in input.result &&
13650
+ Array.isArray(input.result.logs)
13651
+ ? input.result.logs
13652
+ .filter((line): line is string => typeof line === 'string')
13653
+ .map(parseCtxFetchHttpFailureDiagnostic)
13654
+ .filter((diagnostic) => diagnostic !== null)
13655
+ .slice(0, 16)
13656
+ : [],
13657
+ };
13658
+ }
13659
+
13660
+ function terminalLogProjectionForOutbox(
13435
13661
  runnerResult: unknown,
13436
13662
  mode: PostgresSchedulerTerminalLogOutbox | undefined,
13437
- ): string[] {
13663
+ ): { lines: string[]; lineContexts: Array<PlayLogContext | null> } {
13438
13664
  if (
13439
13665
  mode !== 'ctx_log_tail' ||
13440
13666
  !runnerResult ||
13441
13667
  typeof runnerResult !== 'object'
13442
13668
  ) {
13443
- return [];
13669
+ return { lines: [], lineContexts: [] };
13444
13670
  }
13445
- const logs = (runnerResult as { logs?: unknown }).logs;
13671
+ const result = runnerResult as {
13672
+ logs?: unknown;
13673
+ logContexts?: unknown;
13674
+ };
13675
+ const logs = result.logs;
13446
13676
  if (!Array.isArray(logs)) {
13447
- return [];
13677
+ return { lines: [], lineContexts: [] };
13448
13678
  }
13449
- return logs
13450
- .filter((line): line is string => {
13451
- if (typeof line !== 'string') return false;
13679
+ if (
13680
+ result.logContexts !== undefined &&
13681
+ (!Array.isArray(result.logContexts) ||
13682
+ result.logContexts.length !== logs.length)
13683
+ ) {
13684
+ throw new TypeError('Terminal log contexts must align with runner logs.');
13685
+ }
13686
+ const logContexts = Array.isArray(result.logContexts)
13687
+ ? result.logContexts
13688
+ : undefined;
13689
+ const entries = logs
13690
+ .map((line, index) => {
13691
+ if (typeof line !== 'string') return null;
13452
13692
  const normalized = readProvenanceTag(line).line.trim();
13453
13693
  // Retain bounded terminal measurement summaries alongside timestamped
13454
13694
  // customer/recovery lines after a detached runner has exited.
13455
- return (
13695
+ const retained =
13456
13696
  CTX_LOG_TIMESTAMP_PREFIX_RE.test(normalized) ||
13457
13697
  normalized.startsWith('[runtime.failure_diagnostic] ') ||
13458
13698
  normalized.startsWith('[runtime.phase_summary] ') ||
13459
13699
  normalized.startsWith('[runtime.logs] ') ||
13460
- normalized.startsWith('[runtime.log_transport] ')
13461
- );
13700
+ normalized.startsWith('[runtime.log_transport] ');
13701
+ if (!retained) return null;
13702
+ return {
13703
+ line: line.trim(),
13704
+ context: normalizePlayLogContext(logContexts?.[index]),
13705
+ };
13462
13706
  })
13463
- .map((line) => line.trim())
13707
+ .filter(
13708
+ (entry): entry is { line: string; context: PlayLogContext | null } =>
13709
+ entry !== null,
13710
+ )
13464
13711
  .slice(-LOG_TAIL_LIMIT);
13712
+ return {
13713
+ lines: entries.map((entry) => entry.line),
13714
+ lineContexts: entries.map((entry) => entry.context),
13715
+ };
13465
13716
  }
13466
13717
 
13467
13718
  export function attachRunnerWorkProgressMetadata(
@@ -13694,6 +13945,10 @@ export async function executePostgresSchedulerClaim(
13694
13945
  { logMetadata: options.completionLogMetadata ?? 'attach' },
13695
13946
  )
13696
13947
  : output;
13948
+ const terminalLogProjection = terminalLogProjectionForOutbox(
13949
+ output,
13950
+ options.terminalLogOutbox,
13951
+ );
13697
13952
  const completed = await completePostgresSchedulerAttempt(
13698
13953
  client,
13699
13954
  {
@@ -13703,14 +13958,12 @@ export async function executePostgresSchedulerClaim(
13703
13958
  output: terminalOutput,
13704
13959
  rowOutcomes: (output as { rowOutcomes?: unknown } | null)?.rowOutcomes,
13705
13960
  runtimeTiming: extractRunnerRuntimeTiming(output),
13961
+ terminalLogLines: terminalLogProjection.lines,
13962
+ terminalLogContexts: terminalLogProjection.lineContexts,
13706
13963
  terminalWorkCounterSegment: terminalWorkCounterSegmentFromRunnerResult(
13707
13964
  output,
13708
13965
  claim.attempt,
13709
13966
  ),
13710
- terminalLogLines: terminalLogLinesForOutbox(
13711
- output,
13712
- options.terminalLogOutbox,
13713
- ),
13714
13967
  },
13715
13968
  options,
13716
13969
  );
@@ -13754,6 +14007,10 @@ export async function executePostgresSchedulerClaim(
13754
14007
  checkpoint: null,
13755
14008
  tableNamespace: null,
13756
14009
  };
14010
+ const terminalLogProjection = terminalLogProjectionForOutbox(
14011
+ terminalFailureResult,
14012
+ options.terminalLogOutbox,
14013
+ );
13757
14014
  const failed = await failPostgresSchedulerAttempt(
13758
14015
  client,
13759
14016
  {
@@ -13762,10 +14019,8 @@ export async function executePostgresSchedulerClaim(
13762
14019
  leaseToken: claim.leaseToken,
13763
14020
  error: message,
13764
14021
  result: terminalFailureResult,
13765
- terminalLogLines: terminalLogLinesForOutbox(
13766
- terminalFailureResult,
13767
- options.terminalLogOutbox,
13768
- ),
14022
+ terminalLogLines: terminalLogProjection.lines,
14023
+ terminalLogContexts: terminalLogProjection.lineContexts,
13769
14024
  sandboxCrashDiagnostic: options.sandboxCrashDiagnostic,
13770
14025
  },
13771
14026
  options,
@@ -17032,9 +17287,101 @@ export async function listPostgresSchedulerDeliveredOutboxRetentionCandidates(
17032
17287
  */
17033
17288
  export async function purgePostgresSchedulerDeliveredOutboxRetentionBatch(
17034
17289
  client: PostgresSchedulerQueryClient,
17035
- input: { olderThan: Date; limit?: number },
17290
+ input: {
17291
+ olderThan: Date;
17292
+ logBodiesOlderThan?: Date;
17293
+ limit?: number;
17294
+ },
17036
17295
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
17037
17296
  ): Promise<PostgresSchedulerDeliveredOutboxRow[]> {
17297
+ if (input.logBodiesOlderThan) {
17298
+ const logCutoff = input.logBodiesOlderThan.getTime();
17299
+ if (
17300
+ !Number.isFinite(logCutoff) ||
17301
+ logCutoff > Date.now() - 60 * 60 * 1_000
17302
+ ) {
17303
+ throw new TypeError(
17304
+ 'Delivered outbox log-body cleanup cutoff must be at least one hour old.',
17305
+ );
17306
+ }
17307
+ const limit = normalizeOutboxRetentionLimit(input.limit);
17308
+ await client.query(
17309
+ `WITH candidates AS (
17310
+ SELECT event_id
17311
+ FROM ${tableName('outbox', options)} AS outbox
17312
+ WHERE delivered_at IS NOT NULL
17313
+ AND failed_at IS NULL
17314
+ AND delivered_at < $1::timestamptz
17315
+ AND (
17316
+ (event_type = 'run.events'
17317
+ AND jsonb_typeof(payload_json->'events') = 'array'
17318
+ AND EXISTS (
17319
+ SELECT 1
17320
+ FROM jsonb_array_elements(
17321
+ CASE WHEN jsonb_typeof(payload_json->'events') = 'array'
17322
+ THEN payload_json->'events' ELSE '[]'::jsonb END
17323
+ ) AS item
17324
+ WHERE item->>'type' = 'log.appended'
17325
+ AND (item ? 'lines' OR item ? 'lineContexts')
17326
+ ))
17327
+ OR (event_type IN ('run.completed', 'run.failed')
17328
+ AND (
17329
+ payload_json ? 'terminalLogLines'
17330
+ OR (jsonb_typeof(payload_json->'terminalProgressEvents') = 'array'
17331
+ AND EXISTS (
17332
+ SELECT 1
17333
+ FROM jsonb_array_elements(
17334
+ CASE WHEN jsonb_typeof(payload_json->'terminalProgressEvents') = 'array'
17335
+ THEN payload_json->'terminalProgressEvents' ELSE '[]'::jsonb END
17336
+ ) AS item
17337
+ WHERE item->>'type' = 'log.appended'
17338
+ AND (item ? 'lines' OR item ? 'lineContexts')
17339
+ ))
17340
+ ))
17341
+ )
17342
+ ORDER BY delivered_at ASC, event_id ASC
17343
+ LIMIT $2::integer
17344
+ FOR UPDATE SKIP LOCKED
17345
+ )
17346
+ UPDATE ${tableName('outbox', options)} AS outbox
17347
+ SET payload_json = CASE
17348
+ WHEN outbox.event_type = 'run.events' THEN jsonb_set(
17349
+ outbox.payload_json,
17350
+ '{events}',
17351
+ (SELECT jsonb_agg(
17352
+ CASE WHEN item.value->>'type' = 'log.appended'
17353
+ THEN item.value - 'lines' - 'lineContexts'
17354
+ ELSE item.value
17355
+ END ORDER BY item.ordinality
17356
+ )
17357
+ FROM jsonb_array_elements(
17358
+ CASE WHEN jsonb_typeof(outbox.payload_json->'events') = 'array'
17359
+ THEN outbox.payload_json->'events' ELSE '[]'::jsonb END
17360
+ )
17361
+ WITH ORDINALITY AS item(value, ordinality)),
17362
+ false
17363
+ )
17364
+ WHEN jsonb_typeof(outbox.payload_json->'terminalProgressEvents') = 'array'
17365
+ THEN jsonb_set(
17366
+ outbox.payload_json - 'terminalLogLines' - 'terminalLogContexts',
17367
+ '{terminalProgressEvents}',
17368
+ coalesce((SELECT jsonb_agg(
17369
+ CASE WHEN item.value->>'type' = 'log.appended'
17370
+ THEN item.value - 'lines' - 'lineContexts'
17371
+ ELSE item.value
17372
+ END ORDER BY item.ordinality
17373
+ )
17374
+ FROM jsonb_array_elements(outbox.payload_json->'terminalProgressEvents')
17375
+ WITH ORDINALITY AS item(value, ordinality)), '[]'::jsonb),
17376
+ false
17377
+ )
17378
+ ELSE outbox.payload_json - 'terminalLogLines' - 'terminalLogContexts'
17379
+ END
17380
+ FROM candidates
17381
+ WHERE outbox.event_id = candidates.event_id`,
17382
+ [input.logBodiesOlderThan.toISOString(), limit],
17383
+ );
17384
+ }
17038
17385
  const result = await client.query<{
17039
17386
  event_id: string;
17040
17387
  run_id: string;
@@ -17317,9 +17664,11 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
17317
17664
  const occurredAt = event.createdAt.getTime();
17318
17665
  if (event.eventType === 'run.events') {
17319
17666
  const events = payloadField(event.payload, 'events');
17667
+ const logsStored =
17668
+ payloadField(event.payload, 'logBodyStore') === 'planet_scale';
17320
17669
  if (
17321
17670
  !Array.isArray(events) ||
17322
- events.length === 0 ||
17671
+ (events.length === 0 && !logsStored) ||
17323
17672
  events.some(
17324
17673
  (value) =>
17325
17674
  !recordPayload(value) ||
@@ -17364,6 +17713,7 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
17364
17713
  event.runId,
17365
17714
  );
17366
17715
  const lines = stringArrayPayloadField(event.payload, 'terminalLogLines');
17716
+ const lineContexts = terminalLogContextsPayloadField(event.payload, lines);
17367
17717
  const resultSummary = payloadField(event.payload, 'resultSummary');
17368
17718
  return [
17369
17719
  ...progressEvents.map(normalizeOutboxIngressPreview),
@@ -17376,6 +17726,7 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
17376
17726
  occurredAt,
17377
17727
  source: 'worker',
17378
17728
  lines,
17729
+ ...(lineContexts ? { lineContexts } : {}),
17379
17730
  } as PlayRunLedgerEvent,
17380
17731
  ]),
17381
17732
  {
@@ -17404,6 +17755,7 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
17404
17755
  event.runId,
17405
17756
  );
17406
17757
  const lines = stringArrayPayloadField(event.payload, 'terminalLogLines');
17758
+ const lineContexts = terminalLogContextsPayloadField(event.payload, lines);
17407
17759
  const result = terminalRunResultForLedger(
17408
17760
  payloadField(event.payload, 'result'),
17409
17761
  );
@@ -17419,6 +17771,7 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
17419
17771
  occurredAt,
17420
17772
  source: 'worker',
17421
17773
  lines,
17774
+ ...(lineContexts ? { lineContexts } : {}),
17422
17775
  } as PlayRunLedgerEvent,
17423
17776
  ]),
17424
17777
  {
@@ -18236,6 +18589,9 @@ async function projectPostgresSchedulerCreatedEvent(
18236
18589
  runtimeBackend: value('runtimeBackend'),
18237
18590
  schedulerBackend: value('schedulerBackend'),
18238
18591
  schedulerSchema: value('schedulerSchema'),
18592
+ ...(value('runLogStore') === 'planet_scale'
18593
+ ? { runLogStore: 'planet_scale' as const }
18594
+ : {}),
18239
18595
  executionProfile: value('executionProfile'),
18240
18596
  runtimeReleaseId: value('runtimeReleaseId'),
18241
18597
  runtimeDeployVersion: value('runtimeDeployVersion'),
@@ -18666,6 +19022,20 @@ function stringArrayPayloadField(payload: unknown, key: string): string[] {
18666
19022
  : [];
18667
19023
  }
18668
19024
 
19025
+ function terminalLogContextsPayloadField(
19026
+ payload: unknown,
19027
+ lines: readonly string[],
19028
+ ): Array<PlayLogContext | null> | null {
19029
+ const value = payloadField(payload, 'terminalLogContexts');
19030
+ if (value === undefined) return null;
19031
+ if (!Array.isArray(value) || value.length !== lines.length) {
19032
+ throw new TypeError(
19033
+ 'Terminal log contexts must align with terminal log lines.',
19034
+ );
19035
+ }
19036
+ return value.map(normalizePlayLogContext);
19037
+ }
19038
+
18669
19039
  function ledgerEventsPayloadField(
18670
19040
  payload: unknown,
18671
19041
  key: string,