deepline 0.3.248 → 0.3.250

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.
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.248',
203
+ version: '0.3.250',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  flattenStaticPipeline,
3
+ getCompiledPipelineSubsteps,
4
+ type PlayStaticSubstep,
3
5
  type PlayStaticPipeline,
4
6
  } from '../plays/static-pipeline';
5
7
  import type { PlayCallExecution } from './play-call-execution';
@@ -21,6 +23,8 @@ export type CtxRunPlayInlineOnlyReason =
21
23
  | 'dataset_child'
22
24
  | 'event_wait_child'
23
25
  | 'suspending_child'
26
+ | 'row_sleep'
27
+ | 'concurrent_sleep'
24
28
  | 'explicit_timeout'
25
29
  | 'explicit_child_workflow';
26
30
 
@@ -46,7 +50,9 @@ export function ctxRunPlayInlineOnlyMessage(
46
50
  missing_static_contract: 'its static contract could not be resolved',
47
51
  dataset_child: 'it uses ctx.dataset(), ctx.csv(), or a Runtime Sheet',
48
52
  event_wait_child: 'it waits for an event',
49
- suspending_child: 'it attempted to suspend the parent execution',
53
+ suspending_child: 'it attempted an unsupported parent suspension',
54
+ row_sleep: 'it calls ctx.sleep from a Dataset column resolver',
55
+ concurrent_sleep: 'it calls ctx.sleep while sibling inline calls overlap',
50
56
  explicit_timeout: 'it sets an explicit timeout',
51
57
  explicit_child_workflow: 'it requests execution: "child-workflow"',
52
58
  };
@@ -57,6 +63,10 @@ export function ctxRunPlayInlineOnlyMessage(
57
63
  'Trigger it as a top-level play. Event waits need their own run lifecycle and cannot be awaited through ctx.runPlay().',
58
64
  suspending_child:
59
65
  'Trigger it as a top-level play. Work that can suspend needs its own run lifecycle and cannot be awaited through ctx.runPlay().',
66
+ row_sleep:
67
+ 'Sleep before or after Dataset.run(), or launch a scalar waiting Play with rowCtx.runPlayAsync and put its follow-up work there. runPlayAsync returns a Run reference, not the child result.',
68
+ concurrent_sleep:
69
+ 'Await sleeping inline children sequentially outside Dataset callbacks, or use ctx.runPlayAsync for independent waits. A sequential sleep parks and replays the parent Run.',
60
70
  missing_static_contract:
61
71
  'Use a published play with a resolvable static contract, or move lifecycle-owning work into a top-level play.',
62
72
  explicit_timeout:
@@ -72,7 +82,8 @@ export function ctxRunPlayInlineOnlyMessage(
72
82
 
73
83
  /**
74
84
  * Resolve an authored child contract. Any shape that would need a second run,
75
- * durable scheduler state, or a suspension is rejected before invocation.
85
+ * independent scheduler state is rejected before invocation. Sequential authored
86
+ * sleep may suspend the existing parent Run.
76
87
  */
77
88
  export function resolveChildExecutionStrategy(input: {
78
89
  pipeline: PlayStaticPipeline | null | undefined;
@@ -142,3 +153,52 @@ export function resolveChildExecutionStrategy(input: {
142
153
  reason: input.execution === 'inline' ? 'explicit_inline' : 'scalar_child',
143
154
  };
144
155
  }
156
+
157
+ /** Authored sleep may park one sequential inline chain, never a row or sibling fanout. */
158
+ export function assertInlineChildSleepPlacement(input: {
159
+ childPlayName: string;
160
+ rowScoped: boolean;
161
+ concurrent: boolean;
162
+ }): void {
163
+ if (input.rowScoped || input.concurrent) {
164
+ throw new Error(
165
+ ctxRunPlayInlineOnlyMessage(
166
+ input.childPlayName,
167
+ input.rowScoped ? 'row_sleep' : 'concurrent_sleep',
168
+ ),
169
+ );
170
+ }
171
+ }
172
+
173
+ /** Transitive authored sleep facts; independent launches do not suspend their caller. */
174
+ export function findInlineChildSleep(
175
+ pipeline: PlayStaticPipeline | null | undefined,
176
+ seen = new Set<PlayStaticPipeline>(),
177
+ ): {
178
+ path: string[];
179
+ source: NonNullable<PlayStaticPipeline['authoredSleep']>;
180
+ } | null {
181
+ if (!pipeline || seen.has(pipeline)) return null;
182
+ seen.add(pipeline);
183
+ if (pipeline.authoredSleep)
184
+ return { path: [], source: pipeline.authoredSleep };
185
+ function visit(
186
+ steps: PlayStaticSubstep[],
187
+ ): ReturnType<typeof findInlineChildSleep> {
188
+ for (const step of steps) {
189
+ if (step.type === 'play_call') {
190
+ const found = findInlineChildSleep(step.pipeline, seen);
191
+ if (found) return { ...found, path: [step.playId, ...found.path] };
192
+ } else if (
193
+ step.type === 'dataset' ||
194
+ step.type === 'step_suite' ||
195
+ step.type === 'control_flow'
196
+ ) {
197
+ const found = visit(step.steps ?? []);
198
+ if (found) return found;
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+ return visit(getCompiledPipelineSubsteps(pipeline));
204
+ }
@@ -481,6 +481,7 @@ import { executionGatewayDecodedBodyBytes } from './execution-gateway-response-s
481
481
  import {
482
482
  ctxRunPlayInlineOnlyMessage,
483
483
  resolveChildExecutionStrategy,
484
+ assertInlineChildSleepPlacement,
484
485
  } from './child-execution-strategy';
485
486
 
486
487
  type ResolvedPlayExecutor = (
@@ -531,6 +532,7 @@ type RowResolverGapTracker = {
531
532
  openedAtMs: number | null;
532
533
  };
533
534
  type InlineCompositionStore = {
535
+ invocationPath: readonly symbol[];
534
536
  context: PlayContextImpl;
535
537
  executionScope: RunExecutionScope;
536
538
  governor: PlayExecutionGovernor;
@@ -2758,6 +2760,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
2758
2760
  */
2759
2761
  private sleepBoundaryIndexByScope = new Map<string, number>();
2760
2762
  private sleepBoundaryIndex = 0;
2763
+ private readonly activeInlineInvocations = new Set<symbol>();
2761
2764
  private nextDocflowInvocationSequence = 0;
2762
2765
  /**
2763
2766
  * Whether this run captures the docflow trace at all (ADR 0019), decided at
@@ -14105,8 +14108,6 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14105
14108
  input: Record<string, unknown>,
14106
14109
  options?: PlayCallOptions,
14107
14110
  ): Promise<TOutput> {
14108
- if (!rowContext.getStore()) this.assertRowAdmissionOpen();
14109
- this.assertNoPendingDurableSuspension();
14110
14111
  if (
14111
14112
  arguments.length === 3 &&
14112
14113
  typeof playRef === 'object' &&
@@ -14116,6 +14117,36 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14116
14117
  'ctx.runPlay(...) signature is ctx.runPlay(key, playRef, input, { description }). Add a stable call key as the first argument.',
14117
14118
  );
14118
14119
  }
14120
+ const invocation = Symbol('inline-play');
14121
+ const invocationPath = [
14122
+ ...(this.activeInlineComposition?.invocationPath ?? []),
14123
+ invocation,
14124
+ ];
14125
+ // Register before resolver/governor awaits so Promise.all siblings cannot
14126
+ // slip through while one sibling is still preparing its artifact.
14127
+ this.activeInlineInvocations.add(invocation);
14128
+ try {
14129
+ return await this.runInlinePlay<TOutput>(
14130
+ key,
14131
+ playRef,
14132
+ input,
14133
+ options,
14134
+ invocationPath,
14135
+ );
14136
+ } finally {
14137
+ this.activeInlineInvocations.delete(invocation);
14138
+ }
14139
+ }
14140
+
14141
+ private async runInlinePlay<TOutput>(
14142
+ key: string,
14143
+ playRef: string | { playName?: string; name?: string },
14144
+ input: Record<string, unknown>,
14145
+ options: PlayCallOptions | undefined,
14146
+ invocationPath: readonly symbol[],
14147
+ ): Promise<TOutput> {
14148
+ if (!rowContext.getStore()) this.assertRowAdmissionOpen();
14149
+ this.assertNoPendingDurableSuspension();
14119
14150
  const resolvedName = resolvePlayReferenceName(playRef);
14120
14151
 
14121
14152
  if (!resolvedName.trim()) {
@@ -14230,6 +14261,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14230
14261
  const result = await inlineCompositionContext.run(
14231
14262
  {
14232
14263
  context: this,
14264
+ invocationPath,
14233
14265
  executionScope: childExecutionScope,
14234
14266
  governor: inlineChildGovernor,
14235
14267
  playName: resolvedName,
@@ -14749,7 +14781,19 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
14749
14781
  async sleep(ms: number): Promise<void> {
14750
14782
  if (!rowContext.getStore()) this.assertRowAdmissionOpen();
14751
14783
  this.assertNoPendingDurableSuspension();
14752
- this.assertInlineChildContract('suspending_child');
14784
+ const composition = this.activeInlineComposition;
14785
+ if (composition) {
14786
+ assertInlineChildSleepPlacement({
14787
+ childPlayName: composition.playName,
14788
+ rowScoped: Boolean(rowContext.getStore()),
14789
+ concurrent:
14790
+ this.pureMapExecutionActive ||
14791
+ this.activeMapCellMeta !== null ||
14792
+ [...this.activeInlineInvocations].some(
14793
+ (invocation) => !composition.invocationPath.includes(invocation),
14794
+ ),
14795
+ });
14796
+ }
14753
14797
  const delayMs =
14754
14798
  this.currentRuntimeSemantics.validation === 'strict'
14755
14799
  ? (validatePlayAuthoringField('ctx.sleep.ms', ms), ms)
@@ -19188,6 +19232,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
19188
19232
  if (
19189
19233
  transportAttempt < TOOL_EXECUTE_TRANSPORT_MAX_ATTEMPTS &&
19190
19234
  (input.knownNotDispatched === true ||
19235
+ usesDurableInvocationFence ||
19191
19236
  ambiguousTransportReplayAllowed ||
19192
19237
  fixtureReplaySafe)
19193
19238
  ) {
@@ -20126,6 +20171,7 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
20126
20171
  // this exact body deadline caused the abort.
20127
20172
  const transportReplaySafe =
20128
20173
  !ambiguousDispatchedFailure ||
20174
+ usesDurableInvocationFence ||
20129
20175
  ambiguousTransportReplayAllowed ||
20130
20176
  fixtureReplaySafe;
20131
20177
  if (!transportReplaySafe) {
@@ -65,6 +65,7 @@ export function createPlayRunTerminalJob(
65
65
  runId: input.runId,
66
66
  attempt: input.attempt,
67
67
  result: input.result,
68
+ terminalProgressEvents: input.terminalProgressEvents,
68
69
  });
69
70
  if (!recorded) {
70
71
  return { kind: 'not_found_or_fence_rejected' };
@@ -38,6 +38,7 @@ export type CompatibilityTerminalRecorder = {
38
38
  runId: string;
39
39
  attempt: number;
40
40
  result: unknown;
41
+ terminalProgressEvents?: readonly unknown[];
41
42
  }): Promise<boolean>;
42
43
  };
43
44
 
@@ -236,7 +236,8 @@ export type PostgresSchedulerProgressReporter = {
236
236
  };
237
237
 
238
238
  export type PostgresSchedulerDurableLogMode =
239
- 'progress_reporter' | 'direct_worker';
239
+ | 'progress_reporter'
240
+ | 'direct_worker';
240
241
 
241
242
  function shouldPublishDurableLogEvent(
242
243
  mode: PostgresSchedulerDurableLogMode,
@@ -418,7 +419,8 @@ export function createPostgresSchedulerProgressReporter(input: {
418
419
  let workCounters: RuntimeStatusUpdate['workCounters'];
419
420
  let workCountersDirty = false;
420
421
  let lastFlushAt = 0;
421
- let delayedFlushTimer: ReturnType<typeof setTimeout> | null = null;
422
+ let lastProgressFlushAt = 0;
423
+ let deferredFlushTimer: ReturnType<typeof setTimeout> | null = null;
422
424
  let totalRows = 0;
423
425
  let activeMapNodeId: string | null = null;
424
426
  let pendingDatasetLifecycleEvents: Array<
@@ -431,6 +433,7 @@ export function createPostgresSchedulerProgressReporter(input: {
431
433
 
432
434
  type ProgressFlushRequest = {
433
435
  status: 'running' | 'completed' | 'failed';
436
+ force: boolean;
434
437
  required: boolean;
435
438
  terminalLogReplay: TerminalLogReplayPayload | null;
436
439
  waiters: Array<{
@@ -451,6 +454,7 @@ export function createPostgresSchedulerProgressReporter(input: {
451
454
  next.status !== 'running'
452
455
  ? next.status
453
456
  : (current?.status ?? next.status),
457
+ force: next.force || current?.force === true,
454
458
  required: next.required || current?.required === true,
455
459
  terminalLogReplay:
456
460
  next.terminalLogReplay ?? current?.terminalLogReplay ?? null,
@@ -487,18 +491,32 @@ export function createPostgresSchedulerProgressReporter(input: {
487
491
  const request = pendingProgressUpload;
488
492
  pendingProgressUpload = null;
489
493
 
490
- const nextProgress = { ...liveNodeProgress };
494
+ // Customer log latency is independent of row-count progress sampling.
495
+ const includeProgress =
496
+ request.force ||
497
+ request.required ||
498
+ request.status !== 'running' ||
499
+ Date.now() - lastProgressFlushAt >=
500
+ getAdaptiveRunEventIntervalMs(totalRows);
501
+ const nextProgress = includeProgress
502
+ ? { ...liveNodeProgress }
503
+ : undefined;
491
504
  const nextLogs = liveLogs.slice();
492
505
  const nextLogContexts = liveLogContexts.slice();
493
506
  const nextLogTotalCount = liveLogsTotalCount;
494
- const nextDatasetLifecycleEvents = pendingDatasetLifecycleEvents;
495
- pendingDatasetLifecycleEvents = [];
496
- const nextActivityObservations = [
497
- ...pendingActivityObservations.values(),
498
- ];
507
+ const nextDatasetLifecycleEvents = includeProgress
508
+ ? pendingDatasetLifecycleEvents
509
+ : [];
510
+ const nextActivityObservations = includeProgress
511
+ ? [...pendingActivityObservations.values()]
512
+ : [];
499
513
  const nextWorkCounters = workCountersDirty ? workCounters : undefined;
500
- pendingActivityObservations.clear();
501
- liveNodeProgressDirty = false;
514
+ if (includeProgress) {
515
+ pendingDatasetLifecycleEvents = [];
516
+ pendingActivityObservations.clear();
517
+ liveNodeProgressDirty = false;
518
+ lastProgressFlushAt = Date.now();
519
+ }
502
520
  liveLogsDirty = false;
503
521
  workCountersDirty = false;
504
522
 
@@ -561,30 +579,31 @@ export function createPostgresSchedulerProgressReporter(input: {
561
579
  pendingDatasetLifecycleEvents.length === 0 &&
562
580
  pendingActivityObservations.size === 0
563
581
  ) {
564
- if (delayedFlushTimer) {
565
- clearTimeout(delayedFlushTimer);
566
- delayedFlushTimer = null;
582
+ if (deferredFlushTimer) {
583
+ clearTimeout(deferredFlushTimer);
584
+ deferredFlushTimer = null;
567
585
  }
568
586
  return activeProgressUpload ?? Promise.resolve();
569
587
  }
570
588
  const now = Date.now();
571
- const intervalMs = getAdaptiveRunEventIntervalMs(totalRows);
572
- if (!force && now - lastFlushAt < intervalMs) {
573
- if (!delayedFlushTimer) {
574
- delayedFlushTimer = setTimeout(
575
- () => {
576
- delayedFlushTimer = null;
577
- void flush().catch(() => undefined);
578
- },
579
- Math.max(1, intervalMs - (now - lastFlushAt)),
580
- );
581
- delayedFlushTimer.unref?.();
582
- }
589
+ const interval = liveLogsDirty
590
+ ? 1_500
591
+ : getAdaptiveRunEventIntervalMs(totalRows);
592
+ const remaining = interval - (now - lastFlushAt);
593
+ if (!force && remaining > 0) {
594
+ // Quiet runs must flush too: waiting for another event can retain one
595
+ // customer log until a long-running operation finally completes.
596
+ if (deferredFlushTimer !== null) clearTimeout(deferredFlushTimer);
597
+ deferredFlushTimer = setTimeout(() => {
598
+ deferredFlushTimer = null;
599
+ void flush().catch(() => undefined);
600
+ }, remaining);
601
+ deferredFlushTimer.unref?.();
583
602
  return activeProgressUpload ?? Promise.resolve();
584
603
  }
585
- if (delayedFlushTimer) {
586
- clearTimeout(delayedFlushTimer);
587
- delayedFlushTimer = null;
604
+ if (deferredFlushTimer !== null) {
605
+ clearTimeout(deferredFlushTimer);
606
+ deferredFlushTimer = null;
588
607
  }
589
608
  lastFlushAt = now;
590
609
  const requested = new Promise<void>((resolve, reject) => {
@@ -592,6 +611,7 @@ export function createPostgresSchedulerProgressReporter(input: {
592
611
  pendingProgressUpload,
593
612
  {
594
613
  status,
614
+ force,
595
615
  required,
596
616
  terminalLogReplay: input?.terminalLogReplay ?? null,
597
617
  },
@@ -1411,7 +1431,8 @@ export function createPostgresSchedulerProgressReporter(input: {
1411
1431
  ): Promise<void> => {
1412
1432
  const terminalCounters = runtimeWorkCountersFromStats(result?.stats);
1413
1433
  const terminalSegment = result?.stats?.runtimeWorkCounterSegment as
1414
- { invocationSeq?: unknown; segmentId?: unknown } | undefined;
1434
+ | { invocationSeq?: unknown; segmentId?: unknown }
1435
+ | undefined;
1415
1436
  if (
1416
1437
  terminalCounters &&
1417
1438
  terminalCounters.provider429s !== undefined &&
@@ -307,7 +307,8 @@ const POSTGRES_SCHEDULER_SCHEMA_INIT_STATEMENT_TIMEOUT_MS = 60_000;
307
307
  // v47 attaches the append-only runtime event projection to durable outbox and
308
308
  // receipt transitions; no existing runtime wire field changes.
309
309
  // v48 expands optional compact-log columns and readers before writer enablement.
310
- export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 48;
310
+ // v49 installs transactional compact-log writers; only new admissions opt in.
311
+ export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 49;
311
312
  const POSTGRES_SCHEDULER_SCHEMA_MIGRATIONS_TABLE =
312
313
  'scheduler_schema_migrations';
313
314
  export const POSTGRES_SCHEDULER_OPEN_RECOVERY_INDEX =
@@ -938,6 +939,20 @@ function schemaName(
938
939
  );
939
940
  }
940
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
+
941
956
  function tableName(
942
957
  table: string,
943
958
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
@@ -4925,7 +4940,8 @@ async function submitPostgresSchedulerRunTransaction(
4925
4940
  input_json,
4926
4941
  status,
4927
4942
  absurd_task_id,
4928
- admission_json
4943
+ admission_json,
4944
+ run_log_store
4929
4945
  )
4930
4946
  SELECT $1, $2, $3, $4, $5, $6, $7, launch_payload.launch_json, coalesce(nullif(launch_payload.launch_json -> 'input', 'null'::jsonb), '{}'::jsonb), 'queued',
4931
4947
  (SELECT task_id FROM task_spawn),
@@ -4937,7 +4953,8 @@ async function submitPostgresSchedulerRunTransaction(
4937
4953
  'runtimeDeployVersion',
4938
4954
  'coordinatorInternalToken',
4939
4955
  'vercelProtectionBypassToken'
4940
- ]::text[]
4956
+ ]::text[],
4957
+ ${newRunLogStoreSql('launch_payload.launch_json', options)}
4941
4958
  FROM admission
4942
4959
  CROSS JOIN launch_payload
4943
4960
  WHERE NOT existing_run
@@ -4945,7 +4962,7 @@ async function submitPostgresSchedulerRunTransaction(
4945
4962
  AND parent_accepting
4946
4963
  AND NOT ${existingOnlySql}
4947
4964
  ON CONFLICT (run_id) DO NOTHING
4948
- RETURNING run_id, absurd_task_id
4965
+ RETURNING run_id, absurd_task_id, run_log_store
4949
4966
  ), run_recovery AS (
4950
4967
  UPDATE ${tableName('runs', options)} AS runs
4951
4968
  SET absurd_task_id = (SELECT task_id FROM task_spawn),
@@ -4993,13 +5010,7 @@ async function submitPostgresSchedulerRunTransaction(
4993
5010
  'runtimeBackend', $6::text,
4994
5011
  'schedulerBackend', launch_payload.launch_json ->> 'schedulerBackend',
4995
5012
  'schedulerSchema', COALESCE(launch_payload.launch_json ->> 'schedulerSchema', '${schemaName(options)}'),
4996
- 'runLogStore', CASE WHEN EXISTS (
4997
- SELECT 1 FROM pg_proc AS proc
4998
- JOIN pg_namespace AS namespace ON namespace.oid = proc.pronamespace
4999
- WHERE namespace.nspname = '${schemaName(options)}'
5000
- AND proc.proname = 'search_outbox_event'
5001
- AND obj_description(proc.oid, 'pg_proc') = '${RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER}'
5002
- ) THEN 'planet_scale' END,
5013
+ 'runLogStore', run_insert.run_log_store,
5003
5014
  'executionProfile', launch_payload.launch_json ->> 'executionProfile',
5004
5015
  'runtimeReleaseId', launch_payload.launch_json ->> 'runtimeReleaseId',
5005
5016
  'runtimeDeployVersion', launch_payload.launch_json ->> 'runtimeDeployVersion',
@@ -6001,7 +6012,7 @@ export async function firePostgresSchedulerCronSchedule(
6001
6012
  INSERT INTO ${tableName('runs', options)} (
6002
6013
  run_id, org_id, play_id, play_name, graph_hash, runtime_profile,
6003
6014
  artifact_storage_key, launch_json, input_json, status,
6004
- absurd_task_id, admission_json
6015
+ absurd_task_id, admission_json, run_log_store
6005
6016
  )
6006
6017
  SELECT $5::text,
6007
6018
  launch_payload.launch_json ->> 'orgId',
@@ -6022,12 +6033,13 @@ export async function firePostgresSchedulerCronSchedule(
6022
6033
  'runtimeDeployVersion',
6023
6034
  'coordinatorInternalToken',
6024
6035
  'vercelProtectionBypassToken'
6025
- ]::text[]
6036
+ ]::text[],
6037
+ ${newRunLogStoreSql('launch_payload.launch_json', options)}
6026
6038
  FROM occurrence
6027
6039
  CROSS JOIN launch_payload
6028
6040
  WHERE occurrence.schedule_current
6029
6041
  AND NOT occurrence.existing_run
6030
- RETURNING run_id
6042
+ RETURNING run_id, run_log_store
6031
6043
  ), occurrence_admitted AS MATERIALIZED (
6032
6044
  SELECT true AS admitted
6033
6045
  FROM occurrence
@@ -6074,13 +6086,7 @@ export async function firePostgresSchedulerCronSchedule(
6074
6086
  'runtimeBackend', launch_payload.launch_json ->> 'runtimeBackend',
6075
6087
  'schedulerBackend', launch_payload.launch_json ->> 'schedulerBackend',
6076
6088
  'schedulerSchema', COALESCE(launch_payload.launch_json ->> 'schedulerSchema', '${schemaName(options)}'),
6077
- 'runLogStore', CASE WHEN EXISTS (
6078
- SELECT 1 FROM pg_proc AS proc
6079
- JOIN pg_namespace AS namespace ON namespace.oid = proc.pronamespace
6080
- WHERE namespace.nspname = '${schemaName(options)}'
6081
- AND proc.proname = 'search_outbox_event'
6082
- AND obj_description(proc.oid, 'pg_proc') = '${RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER}'
6083
- ) THEN 'planet_scale' END,
6089
+ 'runLogStore', run_insert.run_log_store,
6084
6090
  'executionProfile', launch_payload.launch_json ->> 'executionProfile',
6085
6091
  'runtimeReleaseId', launch_payload.launch_json ->> 'runtimeReleaseId',
6086
6092
  'runtimeDeployVersion', launch_payload.launch_json ->> 'runtimeDeployVersion',
@@ -9718,16 +9724,29 @@ export async function settlePostgresSchedulerRuntimeResources(
9718
9724
  */
9719
9725
  export async function recordPostgresSchedulerRunnerTerminal(
9720
9726
  client: PostgresSchedulerQueryClient,
9721
- input: { runId: string; attempt: number; result: unknown },
9727
+ input: {
9728
+ runId: string;
9729
+ attempt: number;
9730
+ result: unknown;
9731
+ terminalProgressEvents?: readonly unknown[];
9732
+ },
9722
9733
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
9723
9734
  ): Promise<boolean> {
9724
9735
  const recorded = await client.query<{ run_id: string }>(
9725
9736
  `
9726
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.
9727
9740
  SELECT clock_timestamp() AS accepted_at
9728
- )
9741
+ FROM ${tableName('runs', options)}
9742
+ WHERE run_id = $1
9743
+ )
9729
9744
  UPDATE ${tableName('attempts', options)} AS target
9730
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,
9731
9750
  runner_terminal_received_at = COALESCE(
9732
9751
  target.runner_terminal_received_at,
9733
9752
  accepted.accepted_at
@@ -9785,7 +9804,12 @@ export async function recordPostgresSchedulerRunnerTerminal(
9785
9804
  AND target.attempt = $2
9786
9805
  RETURNING target.run_id
9787
9806
  `,
9788
- [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
+ ],
9789
9813
  );
9790
9814
  return recorded.rows.length > 0;
9791
9815
  }
@@ -9919,6 +9943,10 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
9919
9943
  SET status = 'completed',
9920
9944
  terminalization_mode = 'direct',
9921
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,
9922
9950
  runner_terminal_received_at = COALESCE(
9923
9951
  target.runner_terminal_received_at,
9924
9952
  run.accepted_at
@@ -9993,7 +10021,8 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
9993
10021
  target.run_id,
9994
10022
  target.attempt,
9995
10023
  target.runner_terminal_received_at,
9996
- target.resource_json
10024
+ target.resource_json,
10025
+ target.runner_log_payload_json
9997
10026
  ),
9998
10027
  settled_attempt_resources AS (
9999
10028
  UPDATE ${tableName('attempts', options)} AS attempt_resources
@@ -10139,9 +10168,14 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
10139
10168
  'run.completed',
10140
10169
  jsonb_build_object(
10141
10170
  'output', $6::jsonb,
10171
+ 'producerAttempt', $2::integer,
10142
10172
  'terminalLogLines', $7::jsonb,
10143
10173
  'terminalLogContexts', $17::jsonb,
10144
- 'terminalProgressEvents', $8::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),
10145
10179
  'resultSummary', $16::jsonb,
10146
10180
  'terminalWorkCounterSegment', $18::jsonb
10147
10181
  ),
@@ -10180,6 +10214,7 @@ export async function directCompletePostgresSchedulerRunnerTerminal(
10180
10214
  stringifyPostgresJson(resultSummary),
10181
10215
  JSON.stringify(terminalLogProjection.lineContexts),
10182
10216
  stringifyPostgresJson(terminalWorkCounterSegment ?? null),
10217
+ stringifyPostgresJson(runnerTerminalLogPayload(input)),
10183
10218
  ],
10184
10219
  );
10185
10220
  if (completed.rows.length > 0) return 'completed';
@@ -12539,6 +12574,7 @@ export async function completePostgresSchedulerAttempt(
12539
12574
  'run.completed',
12540
12575
  jsonb_build_object(
12541
12576
  'output', $9::jsonb,
12577
+ 'producerAttempt', $2::integer,
12542
12578
  'terminalLogLines', $8::jsonb,
12543
12579
  'terminalLogContexts', $11::jsonb,
12544
12580
  'resultSummary', $10::jsonb,
@@ -12872,6 +12908,7 @@ export async function failPostgresSchedulerAttempt(
12872
12908
  jsonb_build_object(
12873
12909
  'error', $4::jsonb ->> 'message',
12874
12910
  'result', $8::jsonb,
12911
+ 'producerAttempt', $2::integer,
12875
12912
  'terminalLogLines', $12::jsonb,
12876
12913
  'terminalLogContexts', $14::jsonb,
12877
12914
  'resultSummary', $13::jsonb,
@@ -13589,6 +13626,37 @@ function boundedRunLogTailMetadata(logs: string[]): {
13589
13626
  return tail.length > 0 ? { tail, totalLogCount: logs.length } : null;
13590
13627
  }
13591
13628
 
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
+
13592
13660
  function terminalLogProjectionForOutbox(
13593
13661
  runnerResult: unknown,
13594
13662
  mode: PostgresSchedulerTerminalLogOutbox | undefined,