deepline 0.3.258 → 0.3.260

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 (69) hide show
  1. package/dist/bundling-sources/sdk/src/api-error.ts +45 -0
  2. package/dist/bundling-sources/sdk/src/client.ts +98 -459
  3. package/dist/bundling-sources/sdk/src/http.ts +1 -34
  4. package/dist/bundling-sources/sdk/src/index.ts +8 -0
  5. package/dist/bundling-sources/sdk/src/plays/csv-data-input.ts +139 -0
  6. package/dist/bundling-sources/sdk/src/plays/run-request.ts +855 -0
  7. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  8. package/dist/bundling-sources/sdk/src/runs/overview.ts +45 -2
  9. package/dist/bundling-sources/sdk/src/types.ts +5 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +8 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +42 -10
  12. package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +3 -0
  13. package/dist/bundling-sources/shared_libs/play-runtime/launch-source-policy.ts +23 -0
  14. package/dist/bundling-sources/shared_libs/play-runtime/live-state-contract.ts +3 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/play-admission-outcome.ts +23 -5
  16. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/projection/gateway-adapter.ts +1 -1
  17. package/dist/bundling-sources/shared_libs/play-runtime/run-execution-state.ts +845 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +106 -6
  19. package/dist/bundling-sources/shared_libs/play-runtime/run-observation.ts +440 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/run-snapshot-stream.ts +35 -2
  21. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +66 -7
  22. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-crash-details.ts +95 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +13 -2
  24. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +0 -29
  25. package/dist/bundling-sources/shared_libs/play-runtime/runtime-sheet-row-transition.ts +47 -2
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +6 -0
  27. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +475 -36
  28. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +78 -11
  29. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +550 -42
  30. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +181 -8
  31. package/dist/bundling-sources/shared_libs/play-runtime/step-lifecycle-tracker.ts +0 -18
  32. package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +80 -7
  33. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +37 -2
  34. package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +148 -18
  35. package/dist/bundling-sources/shared_libs/plays/cascade-falloff.ts +86 -0
  36. package/dist/bundling-sources/shared_libs/plays/docflow-unreached-reason.ts +611 -0
  37. package/dist/bundling-sources/shared_libs/plays/docflow.ts +21 -0
  38. package/dist/bundling-sources/shared_libs/plays/play-exports.ts +28 -5
  39. package/dist/bundling-sources/shared_libs/plays/settings-contract.ts +92 -10
  40. package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +5 -0
  41. package/dist/bundling-sources/shared_libs/plays/trigger-config-contract.ts +222 -0
  42. package/dist/bundling-sources/shared_libs/plays/trigger-config-route.ts +15 -0
  43. package/dist/cli/index.js +4957 -771
  44. package/dist/cli/index.mjs +4957 -771
  45. package/dist/client-B5XbKrrs.d.mts +6564 -0
  46. package/dist/client-C9Y49yya.d.ts +6564 -0
  47. package/dist/{compiler-manifest-C4mdRJOP.d.mts → compiler-manifest-CUuh8C3b.d.mts} +14 -1
  48. package/dist/{compiler-manifest-C4mdRJOP.d.ts → compiler-manifest-CUuh8C3b.d.ts} +14 -1
  49. package/dist/index.d.mts +7 -6275
  50. package/dist/index.d.ts +7 -6275
  51. package/dist/index.js +360 -326
  52. package/dist/index.mjs +360 -326
  53. package/dist/install-integrity.json +22 -2
  54. package/dist/plays/bundle-play-file.d.mts +2 -2
  55. package/dist/plays/bundle-play-file.d.ts +2 -2
  56. package/dist/plays/bundle-play-file.mjs +117 -18
  57. package/dist/plays/csv-data-input.d.mts +37 -0
  58. package/dist/plays/csv-data-input.d.ts +37 -0
  59. package/dist/plays/csv-data-input.js +3135 -0
  60. package/dist/plays/csv-data-input.mjs +3108 -0
  61. package/dist/plays/run-request.d.mts +6 -0
  62. package/dist/plays/run-request.d.ts +6 -0
  63. package/dist/plays/run-request.js +664 -0
  64. package/dist/plays/run-request.mjs +614 -0
  65. package/dist/release.d.mts +1 -1
  66. package/dist/release.d.ts +1 -1
  67. package/dist/release.js +1 -1
  68. package/dist/release.mjs +1 -1
  69. package/package.json +12 -1
@@ -100,6 +100,7 @@ import {
100
100
  runtimeWorkCountersFromResources,
101
101
  runtimeWorkCountersFromStats,
102
102
  } from '../runtime-work-counters';
103
+ import { readPlayLaunchSurface } from '../launch-source-policy';
103
104
  import {
104
105
  isCtxFetchHttpFailureDiagnostic,
105
106
  parseCtxFetchHttpFailureDiagnostic,
@@ -311,7 +312,9 @@ const POSTGRES_SCHEDULER_SCHEMA_INIT_STATEMENT_TIMEOUT_MS = 60_000;
311
312
  // v50 adds a partial run-scoped outbox index for projection lease verification.
312
313
  // v51 also creates that index in isolated (transactional) bootstraps and makes
313
314
  // deploy readiness require it; v50 only built it on the concurrent path.
314
- export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 51;
315
+ // v52 indexes only delivered outbox rows that still contain raw log bodies.
316
+ // The concurrent build lets retention skip already-scrubbed history.
317
+ export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 52;
315
318
  const POSTGRES_SCHEDULER_SCHEMA_MIGRATIONS_TABLE =
316
319
  'scheduler_schema_migrations';
317
320
  export const POSTGRES_SCHEDULER_OPEN_RECOVERY_INDEX =
@@ -446,6 +449,15 @@ type PostgresSchedulerClaimExecutionOptions = Pick<
446
449
  /** Internal provider-read coordinates which must survive terminal-worker
447
450
  * loss. They are persisted only on the matching durable cleanup obligation. */
448
451
  sandboxCrashDiagnostic?: PostgresSchedulerSandboxCrashDiagnosticRequest;
452
+ /** Settled progress facts for a failed runner result, folded before the
453
+ * terminal write. They travel in the `run.failed` outbox row ahead of the
454
+ * terminal fact, the same carrier a direct completion uses, so the ledger
455
+ * settles them before the run's terminal makes its steps immutable. */
456
+ failedTerminalProgressEvents?: readonly unknown[];
457
+ /** Forwarded to `failPostgresSchedulerAttempt`: a failure terminal written
458
+ * for a detached-runner attempt must not overwrite an authoritative pushed
459
+ * runner terminal that won after the attempt was claimed. */
460
+ preserveRunnerTerminal?: boolean;
449
461
  };
450
462
 
451
463
  export type PostgresSchedulerWorkerOnceResult =
@@ -900,6 +912,20 @@ export const POSTGRES_SCHEDULER_OUTBOX_DELIVERED_RETENTION_MS =
900
912
  export const POSTGRES_SCHEDULER_OUTBOX_LOG_BODY_RETENTION_MS =
901
913
  7 * 24 * 60 * 60 * 1_000;
902
914
  export const POSTGRES_SCHEDULER_OUTBOX_DELIVERED_RETENTION_BATCH_LIMIT = 1_000;
915
+ // Keep this expression identical in the partial index and retention query so
916
+ // PostgreSQL can prove that the ordered candidate scan is index-qualified.
917
+ // Strict JSONPath plus the array guards matches the old jsonb_array_elements
918
+ // eligibility test, including present-but-null log fields.
919
+ const DELIVERED_OUTBOX_RAW_LOG_BODY_PREDICATE = `
920
+ (
921
+ (event_type = 'run.events'
922
+ AND jsonb_typeof(payload_json->'events') = 'array'
923
+ AND payload_json @? 'strict $.events[*] ? (@.type == "log.appended" && (exists(@.lines) || exists(@.lineContexts)))'::jsonpath)
924
+ OR (event_type IN ('run.completed', 'run.failed')
925
+ AND (payload_json ? 'terminalLogLines'
926
+ OR (jsonb_typeof(payload_json->'terminalProgressEvents') = 'array'
927
+ AND payload_json @? 'strict $.terminalProgressEvents[*] ? (@.type == "log.appended" && (exists(@.lines) || exists(@.lineContexts)))'::jsonpath)))
928
+ )`;
903
929
  const RUN_LEDGER_DISPATCH_TOKEN_TIMEOUT_MS = 15_000;
904
930
  const COMPUTE_BILLING_DISPATCH_TOKEN_TIMEOUT_MS = 15_000;
905
931
 
@@ -1933,6 +1959,15 @@ export async function hasCurrentPostgresSchedulerSchemaShape(
1933
1959
  AND index_row.indisready AND index_row.indisvalid
1934
1960
  )
1935
1961
  )
1962
+ AND EXISTS (
1963
+ SELECT 1 FROM pg_index AS index_row
1964
+ JOIN pg_class AS index_class ON index_class.oid = index_row.indexrelid
1965
+ JOIN pg_namespace AS index_namespace ON index_namespace.oid = index_class.relnamespace
1966
+ WHERE index_namespace.nspname = $1
1967
+ AND index_class.relname = 'runtime_scheduler_outbox_delivered_log_body_idx'
1968
+ AND index_row.indrelid = to_regclass($3)
1969
+ AND index_row.indisready AND index_row.indisvalid
1970
+ )
1936
1971
  AND (
1937
1972
  $6::boolean = false
1938
1973
  OR EXISTS (
@@ -3852,6 +3887,12 @@ export async function ensurePostgresSchedulerSchema(
3852
3887
  ON ${tableName('outbox', options)} (delivered_at, event_id)
3853
3888
  WHERE delivered_at IS NOT NULL AND failed_at IS NULL
3854
3889
  `);
3890
+ await client.query(`
3891
+ CREATE INDEX IF NOT EXISTS runtime_scheduler_outbox_delivered_log_body_idx
3892
+ ON ${tableName('outbox', options)} (delivered_at, event_id)
3893
+ WHERE delivered_at IS NOT NULL AND failed_at IS NULL
3894
+ AND ${DELIVERED_OUTBOX_RAW_LOG_BODY_PREDICATE}
3895
+ `);
3855
3896
  await client.query(`
3856
3897
  CREATE INDEX IF NOT EXISTS runtime_scheduler_outbox_blocked_oldest_idx
3857
3898
  ON ${tableName('outbox', options)} (created_at, event_id)
@@ -4004,6 +4045,13 @@ const POSTGRES_SCHEDULER_RUNTIME_ADMIN_INDEXES = [
4004
4045
  definition: `(delivered_at, event_id)
4005
4046
  WHERE delivered_at IS NOT NULL AND failed_at IS NULL`,
4006
4047
  },
4048
+ {
4049
+ name: 'runtime_scheduler_outbox_delivered_log_body_idx',
4050
+ table: 'outbox',
4051
+ definition: `(delivered_at, event_id)
4052
+ WHERE delivered_at IS NOT NULL AND failed_at IS NULL
4053
+ AND ${DELIVERED_OUTBOX_RAW_LOG_BODY_PREDICATE}`,
4054
+ },
4007
4055
  {
4008
4056
  name: 'runtime_scheduler_outbox_blocked_oldest_idx',
4009
4057
  table: 'outbox',
@@ -5053,6 +5101,7 @@ async function submitPostgresSchedulerRunTransaction(
5053
5101
  'staticPipeline', launch_payload.launch_json -> 'staticPipeline',
5054
5102
  'source', launch_payload.launch_json ->> 'source',
5055
5103
  'triggerSource', launch_payload.launch_json ->> 'triggerSource',
5104
+ 'launchSurface', launch_payload.launch_json ->> 'launchSurface',
5056
5105
  'inputFileId', launch_payload.launch_json ->> 'inputFileId',
5057
5106
  'inputBytes', launch_payload.launch_json -> 'inputBytes',
5058
5107
  'inputSha256', launch_payload.launch_json ->> 'inputSha256',
@@ -7184,6 +7233,10 @@ export async function markPostgresSchedulerAttemptWaiting(
7184
7233
  const eventKey = suspensionEventKey(input.suspension);
7185
7234
  const detachedRunnerCeilingDeadlineMs =
7186
7235
  detachedRunnerCeilingDeadlineMsFromSuspension(input.suspension);
7236
+ const sleepWakeAtMs = sleepWakeAtMsFromSuspension(
7237
+ input.suspension,
7238
+ Date.now(),
7239
+ );
7187
7240
  const waiting = await client.query<{ run_id: string; event_id: string }>(
7188
7241
  `
7189
7242
  WITH waiting_attempt AS (
@@ -7252,7 +7305,8 @@ export async function markPostgresSchedulerAttemptWaiting(
7252
7305
  'run.waiting',
7253
7306
  jsonb_strip_nulls(jsonb_build_object(
7254
7307
  'kind', NULLIF(btrim($4::jsonb ->> 'kind'), ''),
7255
- 'eventKey', NULLIF($7::text, '')
7308
+ 'eventKey', NULLIF($7::text, ''),
7309
+ 'waitUntil', $10::double precision
7256
7310
  )),
7257
7311
  ${causalOutboxCreatedAt('waiting_run.run_id', options)}
7258
7312
  FROM waiting_run
@@ -7270,6 +7324,7 @@ export async function markPostgresSchedulerAttemptWaiting(
7270
7324
  eventKey,
7271
7325
  input.preserveHeartbeatAt === true,
7272
7326
  detachedRunnerCeilingDeadlineMs,
7327
+ sleepWakeAtMs,
7273
7328
  ],
7274
7329
  );
7275
7330
  const row = waiting.rows[0];
@@ -7283,7 +7338,29 @@ export async function markPostgresSchedulerAttemptWaiting(
7283
7338
  * lane. Other wait kinds intentionally return null: event/HITL waits are not
7284
7339
  * execution timeouts.
7285
7340
  */
7286
- function detachedRunnerCeilingDeadlineMsFromSuspension(
7341
+ /**
7342
+ * When a `ctx.sleep` park will wake, measured from the moment the scheduler
7343
+ * records the wait. Observers render it ("sleeping until 14:05:09") so a sleep
7344
+ * is never described as an approval or event wait.
7345
+ */
7346
+ export function sleepWakeAtMsFromSuspension(
7347
+ suspension: unknown,
7348
+ nowMs: number,
7349
+ ): number | null {
7350
+ if (
7351
+ !suspension ||
7352
+ typeof suspension !== 'object' ||
7353
+ Array.isArray(suspension)
7354
+ ) {
7355
+ return null;
7356
+ }
7357
+ const candidate = suspension as { kind?: unknown; delayMs?: unknown };
7358
+ if (candidate.kind !== 'sleep') return null;
7359
+ const delayMs = Number(candidate.delayMs);
7360
+ return Number.isFinite(delayMs) && delayMs >= 0 ? nowMs + delayMs : null;
7361
+ }
7362
+
7363
+ export function detachedRunnerCeilingDeadlineMsFromSuspension(
7287
7364
  suspension: unknown,
7288
7365
  ): number | null {
7289
7366
  if (
@@ -7964,6 +8041,204 @@ export async function deferPostgresSchedulerAttemptForCapacity(
7964
8041
  return deferred.rows.length > 0;
7965
8042
  }
7966
8043
 
8044
+ /** `error_json.kind` stamped on an attempt retired by an executor-token
8045
+ * outage. Consecutive attempts carrying it form one bounded outage. */
8046
+ export const EXECUTOR_TOKEN_UNAVAILABLE_ATTEMPT_ERROR_KIND =
8047
+ 'executor_token_unavailable';
8048
+
8049
+ /**
8050
+ * Return a fresh, pre-code attempt to durable queue admission after the
8051
+ * executor-token route was transiently unreachable. Like a capacity deferral,
8052
+ * no Play code ran and no engine attempt is spent. Unlike capacity, an
8053
+ * unreachable app may never recover, so the deferral is bounded by wall-clock:
8054
+ * the outage starts at the oldest attempt in the unbroken tail of attempts
8055
+ * retired by this same outage, and once it is older than `maxOutageMs` the
8056
+ * attempt is left untouched (`deferred: false`) for the caller to terminalize.
8057
+ */
8058
+ export async function deferPostgresSchedulerAttemptForExecutorTokenOutage(
8059
+ client: PostgresSchedulerQueryClient,
8060
+ input: {
8061
+ runId: string;
8062
+ attempt: number;
8063
+ leaseToken: string;
8064
+ reason: string;
8065
+ maxOutageMs: number;
8066
+ },
8067
+ options?: Pick<PostgresSchedulerOptions, 'schema'>,
8068
+ ): Promise<{ deferred: boolean; outageStartedAtMs: number | null }> {
8069
+ const result = await client.query<{
8070
+ outage_started_at_ms: number | string | null;
8071
+ deferred: boolean;
8072
+ }>(
8073
+ `
8074
+ WITH current_attempt AS (
8075
+ SELECT created_at
8076
+ FROM ${tableName('attempts', options)}
8077
+ WHERE run_id = $1
8078
+ AND attempt = $2
8079
+ AND lease_token = $3
8080
+ AND status IN ('claimed', 'running')
8081
+ ),
8082
+ outage AS (
8083
+ SELECT LEAST(
8084
+ (SELECT created_at FROM current_attempt),
8085
+ (
8086
+ SELECT min(prior.created_at)
8087
+ FROM ${tableName('attempts', options)} AS prior
8088
+ WHERE prior.run_id = $1
8089
+ AND prior.attempt < $2
8090
+ AND prior.error_json ->> 'kind' = $6
8091
+ AND NOT EXISTS (
8092
+ SELECT 1
8093
+ FROM ${tableName('attempts', options)} AS gap
8094
+ WHERE gap.run_id = $1
8095
+ AND gap.attempt > prior.attempt
8096
+ AND gap.attempt < $2
8097
+ AND gap.error_json ->> 'kind' IS DISTINCT FROM $6
8098
+ )
8099
+ )
8100
+ ) AS started_at
8101
+ ),
8102
+ deferred_attempt AS (
8103
+ UPDATE ${tableName('attempts', options)}
8104
+ SET status = 'stale',
8105
+ lease_expires_at = NULL,
8106
+ heartbeat_at = now(),
8107
+ error_json = $4::jsonb,
8108
+ updated_at = now()
8109
+ WHERE run_id = $1
8110
+ AND attempt = $2
8111
+ AND status IN ('claimed', 'running')
8112
+ AND lease_token = $3
8113
+ AND (SELECT started_at FROM outage) >
8114
+ clock_timestamp() - ($5::double precision * interval '1 millisecond')
8115
+ RETURNING run_id
8116
+ ),
8117
+ requeued_run AS (
8118
+ UPDATE ${tableName('runs', options)}
8119
+ SET status = 'queued',
8120
+ suspension_json = NULL,
8121
+ wait_deadline = NULL,
8122
+ updated_at = now()
8123
+ WHERE run_id IN (SELECT run_id FROM deferred_attempt)
8124
+ AND status IN ('queued', 'running', 'waiting')
8125
+ RETURNING run_id
8126
+ )
8127
+ SELECT
8128
+ (extract(epoch FROM (SELECT started_at FROM outage)) * 1000)::float8
8129
+ AS outage_started_at_ms,
8130
+ EXISTS (SELECT 1 FROM requeued_run) AS deferred
8131
+ `,
8132
+ [
8133
+ input.runId,
8134
+ input.attempt,
8135
+ input.leaseToken,
8136
+ stringifyPostgresJson({
8137
+ kind: EXECUTOR_TOKEN_UNAVAILABLE_ATTEMPT_ERROR_KIND,
8138
+ message: input.reason,
8139
+ }),
8140
+ input.maxOutageMs,
8141
+ EXECUTOR_TOKEN_UNAVAILABLE_ATTEMPT_ERROR_KIND,
8142
+ ],
8143
+ );
8144
+ const row = result.rows[0];
8145
+ const startedAtMs =
8146
+ row?.outage_started_at_ms == null ? NaN : Number(row.outage_started_at_ms);
8147
+ return {
8148
+ deferred: row?.deferred === true,
8149
+ outageStartedAtMs: Number.isFinite(startedAtMs) ? startedAtMs : null,
8150
+ };
8151
+ }
8152
+
8153
+ /**
8154
+ * Take the fence of one parked detached-runner attempt so its owner can write
8155
+ * a failure terminal. This is the single-run form of the overdue-ceiling claim
8156
+ * (same guards, same `waiting -> running` transition and `run.resumed` fact):
8157
+ * it requires the exact attempt and lease token, refuses when an authoritative
8158
+ * runner terminal was already pushed, and refuses when a newer attempt exists.
8159
+ * Returns false (no write) when any guard fails.
8160
+ */
8161
+ export async function claimPostgresSchedulerWaitingDetachedAttemptForTerminal(
8162
+ client: PostgresSchedulerQueryClient,
8163
+ input: { runId: string; attempt: number; leaseToken: string },
8164
+ options?: Pick<PostgresSchedulerOptions, 'schema'>,
8165
+ ): Promise<boolean> {
8166
+ const claimed = await client.query<{ run_id: string }>(
8167
+ `
8168
+ WITH candidate AS MATERIALIZED (
8169
+ SELECT runs.run_id, attempts.attempt
8170
+ FROM ${tableName('runs', options)} AS runs
8171
+ JOIN ${tableName('attempts', options)} AS attempts
8172
+ ON attempts.run_id = runs.run_id
8173
+ AND attempts.attempt = (runs.suspension_json ->> 'runnerAttempt')::integer
8174
+ WHERE runs.run_id = $1
8175
+ AND runs.status = 'waiting'
8176
+ AND runs.suspension_json ->> 'kind' = 'detached_runner'
8177
+ AND attempts.attempt = $2
8178
+ AND attempts.status = 'waiting'
8179
+ AND attempts.lease_token = $3
8180
+ AND (
8181
+ attempts.runner_result_json IS NULL
8182
+ OR attempts.runner_result_json ->> 'status' = 'suspended'
8183
+ )
8184
+ AND NOT EXISTS (
8185
+ SELECT 1
8186
+ FROM ${tableName('attempts', options)} AS newer
8187
+ WHERE newer.run_id = attempts.run_id
8188
+ AND newer.attempt > attempts.attempt
8189
+ AND newer.status IN ('claimed', 'running', 'waiting', 'completed', 'failed')
8190
+ )
8191
+ FOR UPDATE OF runs, attempts
8192
+ ),
8193
+ running_attempt AS (
8194
+ UPDATE ${tableName('attempts', options)} AS target
8195
+ SET status = 'running',
8196
+ heartbeat_at = now(),
8197
+ updated_at = now()
8198
+ FROM candidate
8199
+ WHERE target.run_id = candidate.run_id
8200
+ AND target.attempt = candidate.attempt
8201
+ AND target.status = 'waiting'
8202
+ AND target.lease_token = $3
8203
+ RETURNING target.run_id
8204
+ ),
8205
+ running_run AS (
8206
+ UPDATE ${tableName('runs', options)} AS target
8207
+ SET status = 'running',
8208
+ suspension_json = NULL,
8209
+ wait_deadline = NULL,
8210
+ updated_at = now()
8211
+ WHERE target.run_id IN (SELECT run_id FROM running_attempt)
8212
+ AND target.status = 'waiting'
8213
+ RETURNING target.run_id
8214
+ ),
8215
+ resumed_outbox AS (
8216
+ INSERT INTO ${tableName('outbox', options)} (
8217
+ event_id, run_id, event_type, payload_json
8218
+ )
8219
+ SELECT
8220
+ $4,
8221
+ running_run.run_id,
8222
+ 'run.resumed',
8223
+ jsonb_build_object(
8224
+ 'kind', 'detached_runner',
8225
+ 'eventKey', 'detached_runner_terminal_salvage'
8226
+ )
8227
+ FROM running_run
8228
+ ON CONFLICT (event_id) DO NOTHING
8229
+ )
8230
+ SELECT run_id FROM running_run
8231
+ `,
8232
+ [
8233
+ input.runId,
8234
+ input.attempt,
8235
+ input.leaseToken,
8236
+ `run-resumed:detached-runner-salvage:${input.runId}:${input.attempt}`,
8237
+ ],
8238
+ );
8239
+ return claimed.rows.length > 0;
8240
+ }
8241
+
7967
8242
  export type PostgresSchedulerRecoveryCircuit = {
7968
8243
  dependency: string;
7969
8244
  state: 'closed' | 'open' | 'half_open';
@@ -12733,6 +13008,9 @@ export async function failPostgresSchedulerAttempt(
12733
13008
  * hard ceiling in the same fenced statement. */
12734
13009
  preserveRunnerTerminal?: boolean;
12735
13010
  sandboxCrashDiagnostic?: PostgresSchedulerSandboxCrashDiagnosticRequest;
13011
+ /** Progress facts the ledger must apply before `run.failed` (see
13012
+ * `failedTerminalProgressEvents`). Omitted from the payload when empty. */
13013
+ terminalProgressEvents?: readonly unknown[];
12736
13014
  },
12737
13015
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
12738
13016
  ): Promise<boolean> {
@@ -13021,7 +13299,11 @@ export async function failPostgresSchedulerAttempt(
13021
13299
  'terminalLogContexts', $14::jsonb,
13022
13300
  'resultSummary', $13::jsonb,
13023
13301
  'terminalWorkCounterSegment', $15::jsonb
13024
- ),
13302
+ ) || CASE
13303
+ WHEN jsonb_array_length($16::jsonb) > 0
13304
+ THEN jsonb_build_object('terminalProgressEvents', $16::jsonb)
13305
+ ELSE '{}'::jsonb
13306
+ END,
13025
13307
  ${causalOutboxCreatedAt('run_terminal.run_id', options)}
13026
13308
  FROM run_terminal
13027
13309
  ON CONFLICT (event_id) DO NOTHING
@@ -13047,6 +13329,7 @@ export async function failPostgresSchedulerAttempt(
13047
13329
  stringifyPostgresJson(resultSummary),
13048
13330
  JSON.stringify(terminalLogContexts),
13049
13331
  stringifyPostgresJson(terminalWorkCounterSegment ?? null),
13332
+ stringifyPostgresJson(input.terminalProgressEvents ?? []),
13050
13333
  ],
13051
13334
  );
13052
13335
  return failed.rows.length > 0;
@@ -14130,6 +14413,12 @@ export async function executePostgresSchedulerClaim(
14130
14413
  terminalLogLines: terminalLogProjection.lines,
14131
14414
  terminalLogContexts: terminalLogProjection.lineContexts,
14132
14415
  sandboxCrashDiagnostic: options.sandboxCrashDiagnostic,
14416
+ ...(options.preserveRunnerTerminal
14417
+ ? { preserveRunnerTerminal: true }
14418
+ : {}),
14419
+ ...(runnerFailureResult && options.failedTerminalProgressEvents?.length
14420
+ ? { terminalProgressEvents: options.failedTerminalProgressEvents }
14421
+ : {}),
14133
14422
  },
14134
14423
  options,
14135
14424
  );
@@ -17021,8 +17310,18 @@ export async function readPendingPostgresSchedulerOutboxEvents(
17021
17310
  -- terminal facts. Project it first so a lifecycle backlog cannot make
17022
17311
  -- an accepted run invisible; per-run predecessor checks above retain
17023
17312
  -- strict ordering within each run.
17313
+ -- A terminal head comes next: it is a run's last fact (one per run, so
17314
+ -- it cannot starve anything), its predecessors are already delivered
17315
+ -- (checked above), and it is what a caller is waiting on. Under a
17316
+ -- lifecycle backlog, FIFO left a committed terminal queued behind other
17317
+ -- runs' progress for seconds (live-run-observation-e2e: 4.9 s from
17318
+ -- commit to first claim while the projector drained continuously).
17024
17319
  ORDER BY
17025
- CASE WHEN candidate.event_type = 'run.created' THEN 0 ELSE 1 END,
17320
+ CASE
17321
+ WHEN candidate.event_type = 'run.created' THEN 0
17322
+ WHEN candidate.event_id LIKE 'run-terminal:%' THEN 1
17323
+ ELSE 2
17324
+ END,
17026
17325
  coalesce(candidate.next_attempt_at, candidate.created_at),
17027
17326
  candidate.created_at,
17028
17327
  candidate.event_id
@@ -17416,37 +17715,11 @@ export async function purgePostgresSchedulerDeliveredOutboxRetentionBatch(
17416
17715
  await client.query(
17417
17716
  `WITH candidates AS (
17418
17717
  SELECT event_id
17419
- FROM ${tableName('outbox', options)} AS outbox
17718
+ FROM ${tableName('outbox', options)}
17420
17719
  WHERE delivered_at IS NOT NULL
17421
17720
  AND failed_at IS NULL
17422
17721
  AND delivered_at < $1::timestamptz
17423
- AND (
17424
- (event_type = 'run.events'
17425
- AND jsonb_typeof(payload_json->'events') = 'array'
17426
- AND EXISTS (
17427
- SELECT 1
17428
- FROM jsonb_array_elements(
17429
- CASE WHEN jsonb_typeof(payload_json->'events') = 'array'
17430
- THEN payload_json->'events' ELSE '[]'::jsonb END
17431
- ) AS item
17432
- WHERE item->>'type' = 'log.appended'
17433
- AND (item ? 'lines' OR item ? 'lineContexts')
17434
- ))
17435
- OR (event_type IN ('run.completed', 'run.failed')
17436
- AND (
17437
- payload_json ? 'terminalLogLines'
17438
- OR (jsonb_typeof(payload_json->'terminalProgressEvents') = 'array'
17439
- AND EXISTS (
17440
- SELECT 1
17441
- FROM jsonb_array_elements(
17442
- CASE WHEN jsonb_typeof(payload_json->'terminalProgressEvents') = 'array'
17443
- THEN payload_json->'terminalProgressEvents' ELSE '[]'::jsonb END
17444
- ) AS item
17445
- WHERE item->>'type' = 'log.appended'
17446
- AND (item ? 'lines' OR item ? 'lineContexts')
17447
- ))
17448
- ))
17449
- )
17722
+ AND ${DELIVERED_OUTBOX_RAW_LOG_BODY_PREDICATE}
17450
17723
  ORDER BY delivered_at ASC, event_id ASC
17451
17724
  LIMIT $2::integer
17452
17725
  FOR UPDATE SKIP LOCKED
@@ -17720,8 +17993,29 @@ export async function markPostgresSchedulerOutboxDelivered(
17720
17993
  leaseOwner?: string | null;
17721
17994
  },
17722
17995
  ): Promise<number> {
17723
- if (eventIds.length === 0) return 0;
17724
- const result = await client.query<{ event_id: string }>(
17996
+ return (
17997
+ await markPostgresSchedulerOutboxDeliveredRows(client, eventIds, options)
17998
+ ).length;
17999
+ }
18000
+
18001
+ /**
18002
+ * Lease-fenced acknowledgement that also reports each row's event type and
18003
+ * commit-to-acknowledgement lag on the database clock, so lag metrics never
18004
+ * depend on skew between the projector host and Postgres.
18005
+ */
18006
+ export async function markPostgresSchedulerOutboxDeliveredRows(
18007
+ client: PostgresSchedulerQueryClient,
18008
+ eventIds: string[],
18009
+ options?: Pick<PostgresSchedulerOptions, 'schema'> & {
18010
+ leaseOwner?: string | null;
18011
+ },
18012
+ ): Promise<RunLedgerOutboxDeliveredRow[]> {
18013
+ if (eventIds.length === 0) return [];
18014
+ const result = await client.query<{
18015
+ event_id: string;
18016
+ event_type: string;
18017
+ lag_ms: number | string | null;
18018
+ }>(
17725
18019
  `
17726
18020
  WITH delivered AS (
17727
18021
  UPDATE ${tableName('outbox', options)}
@@ -17733,9 +18027,14 @@ export async function markPostgresSchedulerOutboxDelivered(
17733
18027
  WHERE event_id = ANY($1::text[])
17734
18028
  AND delivered_at IS NULL
17735
18029
  AND ($2::text IS NULL OR lease_owner = $2)
17736
- RETURNING event_id
18030
+ RETURNING event_id,
18031
+ event_type,
18032
+ (extract(epoch FROM (delivered_at - created_at)) * 1000)::float8
18033
+ AS lag_ms
17737
18034
  )
17738
18035
  SELECT event_id,
18036
+ event_type,
18037
+ lag_ms,
17739
18038
  pg_notify(
17740
18039
  $3,
17741
18040
  json_build_object('eventId', event_id, 'kind', 'outbox_delivered')::text
@@ -17748,7 +18047,51 @@ export async function markPostgresSchedulerOutboxDelivered(
17748
18047
  postgresSchedulerNotifyChannel('terminal', options),
17749
18048
  ],
17750
18049
  );
17751
- return result.rows.length;
18050
+ return result.rows.map((row) => {
18051
+ const lagMs = Number(row.lag_ms);
18052
+ return {
18053
+ eventType: row.event_type,
18054
+ lagMs: Number.isFinite(lagMs) ? Math.max(0, lagMs) : 0,
18055
+ };
18056
+ });
18057
+ }
18058
+
18059
+ /**
18060
+ * Give an unfinished delivery lease back without settling a failure. Used
18061
+ * only when the caller abandoned its own latency budget (the inline admission
18062
+ * attempt): the row keeps its attempt count and due time, and the outbox
18063
+ * channel wakes the durable projector to take it immediately.
18064
+ */
18065
+ export async function releasePostgresSchedulerOutboxLease(
18066
+ client: PostgresSchedulerQueryClient,
18067
+ eventIds: string[],
18068
+ options: Pick<PostgresSchedulerOptions, 'schema'> & {
18069
+ leaseOwner: string;
18070
+ },
18071
+ ): Promise<string[]> {
18072
+ const leaseOwner = options.leaseOwner.trim();
18073
+ if (eventIds.length === 0 || !leaseOwner) return [];
18074
+ const result = await client.query<{ event_id: string }>(
18075
+ `
18076
+ WITH released AS (
18077
+ UPDATE ${tableName('outbox', options)}
18078
+ SET lease_owner = NULL,
18079
+ lease_expires_at = NULL
18080
+ WHERE event_id = ANY($1::text[])
18081
+ AND delivered_at IS NULL
18082
+ AND lease_owner = $2
18083
+ RETURNING event_id
18084
+ )
18085
+ SELECT event_id,
18086
+ pg_notify(
18087
+ $3,
18088
+ json_build_object('eventId', event_id, 'kind', 'outbox_released')::text
18089
+ )
18090
+ FROM released
18091
+ `,
18092
+ [eventIds, leaseOwner, postgresSchedulerNotifyChannel('outbox', options)],
18093
+ );
18094
+ return result.rows.map((row) => row.event_id);
17752
18095
  }
17753
18096
 
17754
18097
  function normalizeOutboxIngressPreview(
@@ -17915,6 +18258,7 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
17915
18258
  // fabricates an integration-event log from its missing event key.
17916
18259
  if (waitKind === 'detached_runner') return [];
17917
18260
  const eventKey = stringPayloadField(event.payload, 'eventKey');
18261
+ const waitUntil = numberPayloadField(event.payload, 'waitUntil');
17918
18262
  const lifecycle = {
17919
18263
  type: event.eventType,
17920
18264
  runId: event.runId,
@@ -17923,6 +18267,9 @@ export function buildRunLedgerEventsFromPostgresSchedulerOutbox(
17923
18267
  ...(event.eventType === 'run.waiting' && waitKind !== null
17924
18268
  ? { waitKind }
17925
18269
  : {}),
18270
+ ...(event.eventType === 'run.waiting' && waitUntil !== null
18271
+ ? { waitUntil }
18272
+ : {}),
17926
18273
  } as PlayRunLedgerEvent;
17927
18274
  if (
17928
18275
  waitKind !== null &&
@@ -18181,6 +18528,39 @@ export type RunLedgerOutboxDispatchInput = {
18181
18528
  * app-side authoritative projector. The Fly worker MUST pass `gateway`.
18182
18529
  */
18183
18530
  tokenAuthority?: RunLedgerOutboxTokenAuthority;
18531
+ /**
18532
+ * Run-scoped claims only: claim nothing unless this exact event is the
18533
+ * run's pending causal head. The inline admission attempt uses it so it can
18534
+ * only ever deliver the one `run.created` row it just committed.
18535
+ */
18536
+ headEventId?: string | null;
18537
+ /**
18538
+ * Caller-owned deadline for the app-runtime append. When it fires, the
18539
+ * attempt is abandoned rather than failed: its lease is released without a
18540
+ * projection-attempt increment or retry backoff, so the durable projector
18541
+ * can take the same row immediately under the same idempotency key.
18542
+ */
18543
+ signal?: AbortSignal;
18544
+ /**
18545
+ * Observation hook for acknowledged rows (event type and commit-to-ack lag
18546
+ * measured on the database clock). Never affects delivery.
18547
+ */
18548
+ onDelivered?: (deliveries: readonly RunLedgerOutboxDeliveredRow[]) => void;
18549
+ /**
18550
+ * Background lease holders (the outbox projector and the worker's
18551
+ * untargeted fast path) set this: after acknowledging a batch, also carry
18552
+ * the run's terminal fact through when it is now pending (see
18553
+ * `continueRunLedgerOutboxThroughTerminal`). Callers that drive their own
18554
+ * bounded batches over a pre-leased prefix (cancellation, inline
18555
+ * `run.created`, the targeted terminal drain) leave it unset.
18556
+ */
18557
+ continueThroughTerminal?: boolean;
18558
+ };
18559
+
18560
+ /** One acknowledged outbox row, for bounded-cardinality lag metrics. */
18561
+ export type RunLedgerOutboxDeliveredRow = {
18562
+ eventType: string;
18563
+ lagMs: number;
18184
18564
  };
18185
18565
 
18186
18566
  export async function dispatchRunLedgerOutboxToLedgerOnce(
@@ -18267,6 +18647,7 @@ export async function dispatchRunLedgerOutboxToLedgerThroughEventWithClientScope
18267
18647
  {
18268
18648
  ...input,
18269
18649
  waitForRunLock: true,
18650
+ continueThroughTerminal: false,
18270
18651
  limit:
18271
18652
  remaining === null ? batchSize : Math.min(batchSize, remaining),
18272
18653
  },
@@ -18400,6 +18781,7 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
18400
18781
  JOIN ${tableName('runs', options)} AS runs ON runs.run_id = head.run_id
18401
18782
  WHERE head.failed_at IS NULL
18402
18783
  AND coalesce(head.next_attempt_at, head.created_at) <= now()
18784
+ AND ($3::text IS NULL OR head.event_id = $3::text)
18403
18785
  AND NOT EXISTS (
18404
18786
  SELECT 1 FROM ${tableName('outbox', options)} AS active
18405
18787
  WHERE active.run_id = runs.run_id
@@ -18410,7 +18792,7 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
18410
18792
  )
18411
18793
  FOR UPDATE OF runs SKIP LOCKED
18412
18794
  `,
18413
- [targetRunId, leaseOwner],
18795
+ [targetRunId, leaseOwner, input.headEventId?.trim() || null],
18414
18796
  )
18415
18797
  : await client.query<{ run_id: string }>(`
18416
18798
  WITH heads AS MATERIALIZED (
@@ -18437,10 +18819,18 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
18437
18819
  AND active.lease_owner IS NOT NULL
18438
18820
  AND active.lease_expires_at > now()
18439
18821
  )
18822
+ -- admission_first: admission, then a run whose head is its
18823
+ -- terminal (its last fact; see
18824
+ -- readPendingPostgresSchedulerOutboxEvents), then the FIFO
18825
+ -- backlog. oldest_ready: pure age order, the anti-starvation turn.
18440
18826
  ORDER BY ${
18441
18827
  input.headPriority === 'oldest_ready'
18442
18828
  ? ''
18443
- : "CASE WHEN head.event_type = 'run.created' THEN 0 ELSE 1 END,"
18829
+ : `CASE
18830
+ WHEN head.event_type = 'run.created' THEN 0
18831
+ WHEN head.event_id LIKE 'run-terminal:%' THEN 1
18832
+ ELSE 2
18833
+ END,`
18444
18834
  }
18445
18835
  head.created_at, runs.run_id
18446
18836
  FOR UPDATE OF runs SKIP LOCKED LIMIT 1
@@ -18531,6 +18921,8 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
18531
18921
  }
18532
18922
  : {}),
18533
18923
  retryPolicy: 'none' as const,
18924
+ // The caller-owned admission deadline (see `signal` above) wins.
18925
+ ...(input.signal ? { signal: input.signal } : {}),
18534
18926
  };
18535
18927
  const renewDelivery = () =>
18536
18928
  withClient((client) =>
@@ -18598,14 +18990,28 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
18598
18990
  });
18599
18991
  }
18600
18992
  }
18601
- const markedDelivered = await withClient((client) =>
18602
- markPostgresSchedulerOutboxDelivered(
18993
+ const deliveredRows = await withClient((client) =>
18994
+ markPostgresSchedulerOutboxDeliveredRows(
18603
18995
  client,
18604
18996
  events.map((event) => event.eventId),
18605
18997
  { ...options, leaseOwner },
18606
18998
  ),
18607
18999
  );
18608
- return {
19000
+ const markedDelivered = deliveredRows.length;
19001
+ if (input.onDelivered && deliveredRows.length > 0) {
19002
+ try {
19003
+ input.onDelivered(deliveredRows);
19004
+ } catch (observerError) {
19005
+ // Metrics are observation only; they never change a settled delivery.
19006
+ console.warn('[run-ledger-outbox.delivery_observer_failed]', {
19007
+ error:
19008
+ observerError instanceof Error
19009
+ ? observerError.message
19010
+ : String(observerError),
19011
+ });
19012
+ }
19013
+ }
19014
+ const delivered: PostgresSchedulerOutboxProjectionResult = {
18609
19015
  attempted: events.length,
18610
19016
  projected: markedDelivered,
18611
19017
  eventIds:
@@ -18617,7 +19023,42 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
18617
19023
  retrying: 0,
18618
19024
  retryingEventIds: [],
18619
19025
  };
19026
+ if (
19027
+ input.continueThroughTerminal !== true ||
19028
+ input.headEventId ||
19029
+ markedDelivered !== events.length
19030
+ ) {
19031
+ return delivered;
19032
+ }
19033
+ return await continueRunLedgerOutboxThroughTerminal(
19034
+ withClient,
19035
+ { input, leaseOwner, runId: first.runId, delivered, events },
19036
+ options,
19037
+ );
18620
19038
  } catch (error) {
19039
+ if (input.signal?.aborted) {
19040
+ // The caller's latency budget expired; this is not a delivery failure.
19041
+ // Release the lease (fenced to this owner) without charging a
19042
+ // projection attempt or scheduling backoff, and wake the durable
19043
+ // projector. If the append already reached the ledger, the projector's
19044
+ // replay carries the same idempotency key and is a no-op there.
19045
+ const released = await withClient((client) =>
19046
+ releasePostgresSchedulerOutboxLease(
19047
+ client,
19048
+ events.map((event) => event.eventId),
19049
+ { ...options, leaseOwner },
19050
+ ),
19051
+ );
19052
+ return {
19053
+ attempted: events.length,
19054
+ projected: 0,
19055
+ eventIds: [],
19056
+ failed: 0,
19057
+ failedEventIds: [],
19058
+ retrying: released.length,
19059
+ retryingEventIds: released,
19060
+ };
19061
+ }
18621
19062
  const settlement = await withClient(async (client) => {
18622
19063
  const blocked: string[] = [];
18623
19064
  const retrying: string[] = [];
@@ -18650,6 +19091,68 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
18650
19091
  }
18651
19092
  }
18652
19093
 
19094
+ /**
19095
+ * Whoever holds a run's outbox lease delivers its terminal fact.
19096
+ *
19097
+ * A worker commits a run's terminal and immediately asks to deliver it
19098
+ * (`run-terminal:<runId>`). When another dispatcher (normally the projector)
19099
+ * is mid-append on an earlier event of the same run, that run-scoped claim
19100
+ * finds the run leased and correctly claims nothing, and the terminal then
19101
+ * waited for the projector's next wave: seconds of a finished run still
19102
+ * reading "running" (live-run-observation-e2e, 4.2 s). So the lease holder,
19103
+ * having acknowledged its batch, carries the run's terminal fact through in
19104
+ * the same turn when it is now pending. The terminal is the run's last fact,
19105
+ * so this is one finite causal drain (the same leased, idempotent
19106
+ * `dispatchRunLedgerOutboxToLedgerThroughEvent` the worker uses), not a loop;
19107
+ * a run without a pending terminal costs one indexed read.
19108
+ */
19109
+ async function continueRunLedgerOutboxThroughTerminal(
19110
+ withClient: <T>(
19111
+ operation: (client: PostgresSchedulerQueryClient) => Promise<T>,
19112
+ ) => Promise<T>,
19113
+ input: {
19114
+ input: RunLedgerOutboxDispatchInput & { waitForRunLock?: boolean };
19115
+ leaseOwner: string;
19116
+ runId: string;
19117
+ delivered: PostgresSchedulerOutboxProjectionResult;
19118
+ events: readonly PostgresSchedulerOutboxEvent[];
19119
+ },
19120
+ options?: Pick<PostgresSchedulerOptions, 'schema'>,
19121
+ ): Promise<PostgresSchedulerOutboxProjectionResult> {
19122
+ const terminalEventId = `run-terminal:${input.runId}`;
19123
+ if (input.events.some((event) => event.eventId === terminalEventId)) {
19124
+ return input.delivered;
19125
+ }
19126
+ const terminal = await withClient((client) =>
19127
+ readPostgresSchedulerOutboxDeliveryStatus(
19128
+ client,
19129
+ { runId: input.runId, eventId: terminalEventId },
19130
+ { ...options, includeBlocked: false },
19131
+ ),
19132
+ );
19133
+ if (!terminal.exists || terminal.delivered) return input.delivered;
19134
+ const drained =
19135
+ await dispatchRunLedgerOutboxToLedgerThroughEventWithClientScope(
19136
+ withClient,
19137
+ {
19138
+ ...input.input,
19139
+ runId: input.runId,
19140
+ leaseOwner: input.leaseOwner,
19141
+ targetEventId: terminalEventId,
19142
+ },
19143
+ options,
19144
+ );
19145
+ return {
19146
+ attempted: input.delivered.attempted + drained.attempted,
19147
+ projected: input.delivered.projected + drained.projected,
19148
+ eventIds: [...input.delivered.eventIds, ...drained.eventIds],
19149
+ failed: drained.failed,
19150
+ failedEventIds: drained.failedEventIds,
19151
+ retrying: drained.retrying,
19152
+ retryingEventIds: drained.retryingEventIds,
19153
+ };
19154
+ }
19155
+
18653
19156
  async function projectPostgresSchedulerCreatedEvent(
18654
19157
  runtimeApi: WorkerRuntimeApiContext,
18655
19158
  event: PostgresSchedulerOutboxEvent,
@@ -18660,6 +19163,7 @@ async function projectPostgresSchedulerCreatedEvent(
18660
19163
  const raw = (key: string) => payloadField(event.payload, key);
18661
19164
  const definitionScope = value('definitionScope');
18662
19165
  const triggerSource = value('triggerSource');
19166
+ const launchSurface = readPlayLaunchSurface(value('launchSurface'));
18663
19167
  const secretRefs = raw('secretRefs');
18664
19168
  const inputSummary = raw('inputSummary');
18665
19169
  const inputBytes = number('inputBytes');
@@ -18678,6 +19182,9 @@ async function projectPostgresSchedulerCreatedEvent(
18678
19182
  workflowId: event.runId,
18679
19183
  runId: event.runId,
18680
19184
  workflowFamilyKey: value('workflowFamilyKey') ?? event.runId,
19185
+ // Written in the admission transaction, so this is when the run was
19186
+ // accepted, not when the projection happened to deliver it.
19187
+ acceptedAt: event.createdAt.getTime(),
18681
19188
  ...(value('parentRunId') ? { parentRunId: value('parentRunId') } : {}),
18682
19189
  ...(value('rootRunId') ? { rootRunId: value('rootRunId') } : {}),
18683
19190
  ...(value('asyncLaunchKey')
@@ -18729,6 +19236,7 @@ async function projectPostgresSchedulerCreatedEvent(
18729
19236
  triggerSource === 'sql_listener'
18730
19237
  ? { triggerSource }
18731
19238
  : {}),
19239
+ ...(launchSurface ? { launchSurface } : {}),
18732
19240
  ...(inputFileId ? { inputFileId } : {}),
18733
19241
  ...(inputBytes !== null ? { inputBytes } : {}),
18734
19242
  ...(inputSha256 ? { inputSha256 } : {}),