deepline 0.3.267 → 0.3.269

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.267',
203
+ version: '0.3.269',
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: {
@@ -132,7 +132,7 @@
132
132
  "@deepline/product-notifications": "workspace:*",
133
133
  "@vercel/oidc": "^3.0.5",
134
134
  "@vercel/queue": "^0.4.0",
135
- "next": "16.3.0-canary.60",
135
+ "next": "16.3.6",
136
136
  "pg": "^8.19.0",
137
137
  "react": "19.2.5",
138
138
  "@deepline/integration-contracts": "workspace:*"
@@ -265,6 +265,7 @@ type RuntimeApiRequest =
265
265
  schedulerBackend?: string | null;
266
266
  schedulerSchema?: string | null;
267
267
  runLogStore?: 'planet_scale';
268
+ runEventStore?: 'planet_scale';
268
269
  executionProfile?: string | null;
269
270
  runtimeReleaseId?: string | null;
270
271
  runtimeDeployVersion?: string | null;
@@ -25,7 +25,10 @@ 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
+ import {
29
+ RUNTIME_EVENT_CANONICAL_RETENTION_INDEX_READY_MARKER,
30
+ RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER,
31
+ } from './runtime-event-search';
29
32
  import type { PlayRunnerRuntimeResource } from '../runner-backends/types';
30
33
  import {
31
34
  RuntimeSandboxOperationState,
@@ -317,7 +320,9 @@ const POSTGRES_SCHEDULER_SCHEMA_INIT_STATEMENT_TIMEOUT_MS = 60_000;
317
320
  // v53 refreshes the indexed runtime log reconciliation projection. Its query
318
321
  // indexes remain operator-prepared concurrently, outside shared bootstrap.
319
322
  // v54 expands the optional per-run canonical event-store marker.
320
- export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 54;
323
+ // v55 refreshes the unified event journal projection. The canonical retention
324
+ // index stays operator-prepared concurrently before marked-run admission.
325
+ export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 55;
321
326
  const POSTGRES_SCHEDULER_SCHEMA_MIGRATIONS_TABLE =
322
327
  'scheduler_schema_migrations';
323
328
  export const POSTGRES_SCHEDULER_OPEN_RECOVERY_INDEX =
@@ -985,6 +990,36 @@ function newRunLogStoreSql(
985
990
  ) THEN 'planet_scale' END`;
986
991
  }
987
992
 
993
+ /** Only new immutable admissions opt into the full typed event journal. */
994
+ function newRunEventStoreSql(
995
+ launchSql: 'launch_payload.launch_json',
996
+ options?: Pick<PostgresSchedulerOptions, 'schema'>,
997
+ ): string {
998
+ return `CASE WHEN ${launchSql} ? 'playReference' AND EXISTS (
999
+ SELECT 1 FROM pg_proc AS proc
1000
+ JOIN pg_namespace AS namespace ON namespace.oid = proc.pronamespace
1001
+ WHERE namespace.nspname = '${schemaName(options).replace(/'/g, "''")}'
1002
+ AND proc.proname = 'search_outbox_event'
1003
+ AND obj_description(proc.oid, 'pg_proc') = '${RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER.replace(/'/g, "''")}'
1004
+ ) AND EXISTS (
1005
+ SELECT 1 FROM pg_index AS index_row
1006
+ WHERE index_row.indexrelid = to_regclass('runtime_observability.runtime_events_canonical_retention_idx')
1007
+ AND index_row.indisready AND index_row.indisvalid
1008
+ AND obj_description(index_row.indexrelid, 'pg_class') = '${RUNTIME_EVENT_CANONICAL_RETENTION_INDEX_READY_MARKER.replace(/'/g, "''")}'
1009
+ ) AND to_regclass('runtime_observability.runtime_event_order_seq') IS NOT NULL
1010
+ AND EXISTS (
1011
+ SELECT 1 FROM pg_attribute AS event_column
1012
+ WHERE event_column.attrelid = to_regclass('runtime_observability.events')
1013
+ AND event_column.attname = 'event_seq' AND event_column.attnum > 0
1014
+ AND NOT event_column.attisdropped
1015
+ ) AND EXISTS (
1016
+ SELECT 1 FROM pg_attribute AS payload_column
1017
+ WHERE payload_column.attrelid = to_regclass('runtime_observability.events')
1018
+ AND payload_column.attname = 'event_payload' AND payload_column.attnum > 0
1019
+ AND NOT payload_column.attisdropped
1020
+ ) THEN 'planet_scale' END`;
1021
+ }
1022
+
988
1023
  function tableName(
989
1024
  table: string,
990
1025
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
@@ -5041,7 +5076,8 @@ async function submitPostgresSchedulerRunTransaction(
5041
5076
  status,
5042
5077
  absurd_task_id,
5043
5078
  admission_json,
5044
- run_log_store
5079
+ run_log_store,
5080
+ run_event_store
5045
5081
  )
5046
5082
  SELECT $1, $2, $3, $4, $5, $6, $7, launch_payload.launch_json, coalesce(nullif(launch_payload.launch_json -> 'input', 'null'::jsonb), '{}'::jsonb), 'queued',
5047
5083
  (SELECT task_id FROM task_spawn),
@@ -5054,7 +5090,8 @@ async function submitPostgresSchedulerRunTransaction(
5054
5090
  'coordinatorInternalToken',
5055
5091
  'vercelProtectionBypassToken'
5056
5092
  ]::text[],
5057
- ${newRunLogStoreSql('launch_payload.launch_json', options)}
5093
+ ${newRunLogStoreSql('launch_payload.launch_json', options)},
5094
+ ${newRunEventStoreSql('launch_payload.launch_json', options)}
5058
5095
  FROM admission
5059
5096
  CROSS JOIN launch_payload
5060
5097
  WHERE NOT existing_run
@@ -5062,7 +5099,7 @@ async function submitPostgresSchedulerRunTransaction(
5062
5099
  AND parent_accepting
5063
5100
  AND NOT ${existingOnlySql}
5064
5101
  ON CONFLICT (run_id) DO NOTHING
5065
- RETURNING run_id, absurd_task_id, run_log_store
5102
+ RETURNING run_id, absurd_task_id, run_log_store, run_event_store
5066
5103
  ), run_recovery AS (
5067
5104
  UPDATE ${tableName('runs', options)} AS runs
5068
5105
  SET absurd_task_id = (SELECT task_id FROM task_spawn),
@@ -5111,6 +5148,7 @@ async function submitPostgresSchedulerRunTransaction(
5111
5148
  'schedulerBackend', launch_payload.launch_json ->> 'schedulerBackend',
5112
5149
  'schedulerSchema', COALESCE(launch_payload.launch_json ->> 'schedulerSchema', '${schemaName(options)}'),
5113
5150
  'runLogStore', run_insert.run_log_store,
5151
+ 'runEventStore', run_insert.run_event_store,
5114
5152
  'executionProfile', launch_payload.launch_json ->> 'executionProfile',
5115
5153
  'runtimeReleaseId', launch_payload.launch_json ->> 'runtimeReleaseId',
5116
5154
  'runtimeDeployVersion', launch_payload.launch_json ->> 'runtimeDeployVersion',
@@ -6113,7 +6151,7 @@ export async function firePostgresSchedulerCronSchedule(
6113
6151
  INSERT INTO ${tableName('runs', options)} (
6114
6152
  run_id, org_id, play_id, play_name, graph_hash, runtime_profile,
6115
6153
  artifact_storage_key, launch_json, input_json, status,
6116
- absurd_task_id, admission_json, run_log_store
6154
+ absurd_task_id, admission_json, run_log_store, run_event_store
6117
6155
  )
6118
6156
  SELECT $5::text,
6119
6157
  launch_payload.launch_json ->> 'orgId',
@@ -6135,12 +6173,13 @@ export async function firePostgresSchedulerCronSchedule(
6135
6173
  'coordinatorInternalToken',
6136
6174
  'vercelProtectionBypassToken'
6137
6175
  ]::text[],
6138
- ${newRunLogStoreSql('launch_payload.launch_json', options)}
6176
+ ${newRunLogStoreSql('launch_payload.launch_json', options)},
6177
+ ${newRunEventStoreSql('launch_payload.launch_json', options)}
6139
6178
  FROM occurrence
6140
6179
  CROSS JOIN launch_payload
6141
6180
  WHERE occurrence.schedule_current
6142
6181
  AND NOT occurrence.existing_run
6143
- RETURNING run_id, run_log_store
6182
+ RETURNING run_id, run_log_store, run_event_store
6144
6183
  ), occurrence_admitted AS MATERIALIZED (
6145
6184
  SELECT true AS admitted
6146
6185
  FROM occurrence
@@ -6188,6 +6227,7 @@ export async function firePostgresSchedulerCronSchedule(
6188
6227
  'schedulerBackend', launch_payload.launch_json ->> 'schedulerBackend',
6189
6228
  'schedulerSchema', COALESCE(launch_payload.launch_json ->> 'schedulerSchema', '${schemaName(options)}'),
6190
6229
  'runLogStore', run_insert.run_log_store,
6230
+ 'runEventStore', run_insert.run_event_store,
6191
6231
  'executionProfile', launch_payload.launch_json ->> 'executionProfile',
6192
6232
  'runtimeReleaseId', launch_payload.launch_json ->> 'runtimeReleaseId',
6193
6233
  'runtimeDeployVersion', launch_payload.launch_json ->> 'runtimeDeployVersion',
@@ -18040,18 +18080,23 @@ export async function markPostgresSchedulerOutboxDeliveredRows(
18040
18080
  }>(
18041
18081
  `
18042
18082
  WITH delivered AS (
18043
- UPDATE ${tableName('outbox', options)}
18083
+ UPDATE ${tableName('outbox', options)} AS outbox
18044
18084
  SET delivered_at = now(),
18045
18085
  next_attempt_at = NULL,
18046
18086
  last_error_json = NULL,
18047
18087
  lease_owner = NULL,
18048
- lease_expires_at = NULL
18049
- WHERE event_id = ANY($1::text[])
18050
- AND delivered_at IS NULL
18051
- AND ($2::text IS NULL OR lease_owner = $2)
18052
- RETURNING event_id,
18053
- event_type,
18054
- (extract(epoch FROM (delivered_at - created_at)) * 1000)::float8
18088
+ lease_expires_at = NULL,
18089
+ payload_json = CASE WHEN EXISTS (
18090
+ SELECT 1 FROM ${tableName('runs', options)} AS run
18091
+ WHERE run.run_id = outbox.run_id
18092
+ AND run.run_event_store = 'planet_scale'
18093
+ ) THEN '{}'::jsonb ELSE outbox.payload_json END
18094
+ WHERE outbox.event_id = ANY($1::text[])
18095
+ AND outbox.delivered_at IS NULL
18096
+ AND ($2::text IS NULL OR outbox.lease_owner = $2)
18097
+ RETURNING outbox.event_id,
18098
+ outbox.event_type,
18099
+ (extract(epoch FROM (outbox.delivered_at - outbox.created_at)) * 1000)::float8
18055
18100
  AS lag_ms
18056
18101
  )
18057
18102
  SELECT event_id,
@@ -19238,6 +19283,9 @@ async function projectPostgresSchedulerCreatedEvent(
19238
19283
  ...(value('runLogStore') === 'planet_scale'
19239
19284
  ? { runLogStore: 'planet_scale' as const }
19240
19285
  : {}),
19286
+ ...(value('runEventStore') === 'planet_scale'
19287
+ ? { runEventStore: 'planet_scale' as const }
19288
+ : {}),
19241
19289
  executionProfile: value('executionProfile'),
19242
19290
  runtimeReleaseId: value('runtimeReleaseId'),
19243
19291
  runtimeDeployVersion: value('runtimeDeployVersion'),
@@ -41,7 +41,9 @@ const RUNTIME_EVENT_LOG_POSITION_EXPRESSION = `jsonb_set(
41
41
  // Migration readiness tracks this projection body independently of scheduler
42
42
  // wire and schema versions. Bump when a changed trigger must be reinstalled.
43
43
  export const RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER =
44
- 'runtime-event-search-log-context-v10-custom-position-plan';
44
+ 'runtime-event-search-log-context-v11-unified-event-journal';
45
+ export const RUNTIME_EVENT_CANONICAL_RETENTION_INDEX_READY_MARKER =
46
+ 'runtime-event-canonical-retention-v1';
45
47
  export const RUNTIME_EVENTS_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
46
48
  const MAX_SEARCH_WINDOW_MS = RUNTIME_EVENTS_RETENTION_MS;
47
49
  const MAX_SEARCH_RESULTS = 200;
@@ -66,6 +68,8 @@ export type RuntimeSearchEvent = {
66
68
  message: string | null;
67
69
  receiptRef: string | null;
68
70
  searchDoc: Record<string, unknown>;
71
+ /** Full accepted fact, read only through the explicit bounded payload path. */
72
+ eventPayload?: Record<string, unknown>;
69
73
  };
70
74
 
71
75
  export type RuntimeSearchCursor = { occurredAt: string; eventId: string };
@@ -80,6 +84,7 @@ export type RuntimeEventSearchInput = {
80
84
  playName?: string;
81
85
  kind?: string;
82
86
  jsonpath?: string;
87
+ includePayloads?: boolean;
83
88
  cursor?: RuntimeSearchCursor;
84
89
  };
85
90
 
@@ -252,6 +257,7 @@ type EventRow = {
252
257
  message: string | null;
253
258
  receipt_ref: string | null;
254
259
  search_doc: Record<string, unknown>;
260
+ event_payload: unknown;
255
261
  };
256
262
 
257
263
  function iso(value: Date | string): string {
@@ -357,7 +363,10 @@ export async function searchRuntimeEvents(
357
363
  `(occurred_at, event_id) < ($${params.length - 1}::timestamptz, $${params.length})`,
358
364
  );
359
365
  }
366
+ const limitParam = params.length + 1;
360
367
  params.push(input.limit + 1);
368
+ const payloadParam = params.length + 1;
369
+ params.push(input.includePayloads === true);
361
370
  const result = await client.query<EventRow>(
362
371
  `SELECT event_id, occurred_at, ingested_at, run_id, play_name, kind, level,
363
372
  coalesce(message, CASE WHEN kind = 'log.appended' THEN
@@ -366,14 +375,27 @@ export async function searchRuntimeEvents(
366
375
  '^' || chr(1) || 'prov:[^' || chr(1) || ']*' || chr(1), '')
367
376
  ELSE log_body END, 2048)
368
377
  END) AS message,
369
- receipt_ref, search_doc
378
+ receipt_ref, search_doc,
379
+ CASE WHEN $${payloadParam}::boolean THEN event_payload END AS event_payload
370
380
  FROM ${EVENTS_TABLE}
371
381
  WHERE ${predicates.join(' AND ')}
372
382
  ORDER BY occurred_at DESC, event_id DESC
373
- LIMIT $${params.length}::integer`,
383
+ LIMIT $${limitParam}::integer`,
374
384
  params,
375
385
  );
376
386
  const hasMore = result.rows.length > input.limit;
387
+ if (
388
+ input.includePayloads === true &&
389
+ result.rows.some(
390
+ (row) =>
391
+ row.event_payload !== null &&
392
+ row.event_payload !== undefined &&
393
+ (typeof row.event_payload !== 'object' ||
394
+ Array.isArray(row.event_payload)),
395
+ )
396
+ ) {
397
+ throw new Error('Runtime canonical event payload is not a JSON object.');
398
+ }
377
399
  const events = result.rows.slice(0, input.limit).map((row) => ({
378
400
  eventId: row.event_id,
379
401
  occurredAt: iso(row.occurred_at),
@@ -384,6 +406,11 @@ export async function searchRuntimeEvents(
384
406
  level: row.level,
385
407
  message: row.message,
386
408
  receiptRef: row.receipt_ref,
409
+ ...(row.event_payload &&
410
+ typeof row.event_payload === 'object' &&
411
+ !Array.isArray(row.event_payload)
412
+ ? { eventPayload: row.event_payload as Record<string, unknown> }
413
+ : {}),
387
414
  searchDoc: {
388
415
  ...row.search_doc,
389
416
  orgId: org,
@@ -561,10 +588,154 @@ export async function getRuntimeRunLogPage(
561
588
  };
562
589
  }
563
590
 
591
+ async function ensureRuntimeEventFetchReceiptFunction(
592
+ client: PostgresSchedulerQueryClient,
593
+ ): Promise<void> {
594
+ await client.query(`
595
+ CREATE OR REPLACE FUNCTION runtime_observability.fetch_receipt(
596
+ p_scope text, p_org text, p_run text, p_ref uuid
597
+ ) RETURNS jsonb
598
+ LANGUAGE plpgsql SECURITY DEFINER
599
+ SET search_path = pg_catalog
600
+ AS $function$
601
+ DECLARE answer jsonb; receipt_key_value text;
602
+ BEGIN
603
+ IF p_scope !~ '^[a-z_][a-z0-9_]{0,62}$'
604
+ OR p_org IS NULL OR p_run IS NULL OR p_ref IS NULL
605
+ OR p_org IS DISTINCT FROM current_setting('app.org_id', true)
606
+ OR p_scope IS DISTINCT FROM current_setting('app.runtime_scope', true)
607
+ THEN RETURN NULL; END IF;
608
+ SELECT event.receipt_key INTO receipt_key_value
609
+ FROM runtime_observability.events AS event
610
+ WHERE event.runtime_scope = p_scope AND event.org_id = p_org
611
+ AND event.run_id = p_run AND event.receipt_ref = p_ref
612
+ AND event.ingested_at >= clock_timestamp() - interval '7 days';
613
+ IF receipt_key_value IS NULL THEN RETURN NULL; END IF;
614
+ EXECUTE format(
615
+ 'SELECT jsonb_build_object(''receiptKey'', k, ''status'', status, ''output'', output_json, ''outputRef'', output_ref, ''error'', error, ''errorPayload'', error_payload) FROM %I.receipts WHERE org_id = $1 AND run_id = $2 AND k = $3 AND status IN (''completed'', ''failed'', ''skipped'')',
616
+ p_scope
617
+ ) INTO answer USING p_org, p_run, receipt_key_value;
618
+ RETURN answer;
619
+ END
620
+ $function$
621
+ `);
622
+ await client.query(
623
+ `REVOKE ALL ON FUNCTION runtime_observability.fetch_receipt(text,text,text,uuid) FROM PUBLIC`,
624
+ );
625
+ }
626
+
564
627
  /** Attach only on the explicit migration/bootstrap path, never on a hot read. */
628
+ export async function hasRuntimeEventSearchStorageShape(
629
+ client: PostgresSchedulerQueryClient,
630
+ ): Promise<boolean> {
631
+ const result = await client.query<{
632
+ ready: boolean;
633
+ events_policy: string | null;
634
+ cursors_policy: string | null;
635
+ }>(
636
+ `SELECT to_regclass('runtime_observability.events') IS NOT NULL
637
+ AND to_regclass('runtime_observability.run_log_cursors') IS NOT NULL
638
+ AND to_regclass('runtime_observability.runtime_event_order_seq') IS NOT NULL
639
+ AND to_regprocedure('runtime_observability.fetch_receipt(text,text,text,uuid)') IS NOT NULL
640
+ AND (SELECT count(*) = 19 FROM pg_attribute
641
+ WHERE attrelid = to_regclass('runtime_observability.events')
642
+ AND attname = ANY(ARRAY[
643
+ 'runtime_scope', 'org_id', 'event_id', 'event_seq', 'event_payload',
644
+ 'log_seq', 'occurred_at', 'ingested_at', 'run_id', 'play_name',
645
+ 'kind', 'level', 'message', 'log_body', 'receipt_ref', 'receipt_key',
646
+ 'source_digest', 'search_doc', 'log_text_digest'
647
+ ]::text[]) AND NOT attisdropped)
648
+ AND (SELECT count(*) = 5 FROM pg_attribute
649
+ WHERE attrelid = to_regclass('runtime_observability.run_log_cursors')
650
+ AND attname = ANY(ARRAY[
651
+ 'runtime_scope', 'org_id', 'run_id', 'last_seq', 'updated_at'
652
+ ]::text[]) AND NOT attisdropped)
653
+ AND EXISTS (SELECT 1 FROM pg_class
654
+ WHERE oid = to_regclass('runtime_observability.events') AND relrowsecurity)
655
+ AND EXISTS (SELECT 1 FROM pg_class
656
+ WHERE oid = to_regclass('runtime_observability.run_log_cursors') AND relrowsecurity)
657
+ AND NOT EXISTS (SELECT 1 FROM pg_namespace AS namespace,
658
+ LATERAL aclexplode(coalesce(namespace.nspacl,
659
+ acldefault('n', namespace.nspowner))) AS privilege
660
+ WHERE namespace.oid = to_regnamespace('runtime_observability')
661
+ AND privilege.grantee = 0)
662
+ AND NOT EXISTS (SELECT 1 FROM pg_class AS relation,
663
+ LATERAL aclexplode(coalesce(relation.relacl,
664
+ acldefault('r', relation.relowner))) AS privilege
665
+ WHERE relation.oid IN (
666
+ to_regclass('runtime_observability.events'),
667
+ to_regclass('runtime_observability.run_log_cursors')
668
+ ) AND privilege.grantee = 0)
669
+ AND NOT EXISTS (SELECT 1 FROM pg_class AS sequence_relation,
670
+ LATERAL aclexplode(coalesce(sequence_relation.relacl,
671
+ acldefault('S', sequence_relation.relowner))) AS privilege
672
+ WHERE sequence_relation.oid =
673
+ to_regclass('runtime_observability.runtime_event_order_seq')
674
+ AND privilege.grantee = 0)
675
+ AND NOT EXISTS (SELECT 1 FROM pg_proc AS receipt_function,
676
+ LATERAL aclexplode(coalesce(receipt_function.proacl,
677
+ acldefault('f', receipt_function.proowner))) AS privilege
678
+ WHERE receipt_function.oid = to_regprocedure(
679
+ 'runtime_observability.fetch_receipt(text,text,text,uuid)')
680
+ AND privilege.grantee = 0)
681
+ AND EXISTS (SELECT 1 FROM pg_policy
682
+ WHERE polrelid = to_regclass('runtime_observability.events')
683
+ AND polname = 'runtime_events_read_scope' AND polcmd = 'r'
684
+ AND polroles @> ARRAY[0]::oid[] AND polqual IS NOT NULL
685
+ AND polwithcheck IS NULL)
686
+ AND EXISTS (SELECT 1 FROM pg_policy
687
+ WHERE polrelid = to_regclass('runtime_observability.run_log_cursors')
688
+ AND polname = 'runtime_run_log_read_scope' AND polcmd = 'r'
689
+ AND polroles @> ARRAY[0]::oid[] AND polqual IS NOT NULL
690
+ AND polwithcheck IS NULL)
691
+ AND (SELECT count(*) = 6 FROM pg_index AS index_row
692
+ WHERE index_row.indrelid = to_regclass('runtime_observability.events')
693
+ AND index_row.indisready AND index_row.indisvalid
694
+ AND index_row.indexrelid = ANY(ARRAY[
695
+ to_regclass('runtime_observability.runtime_events_run_log_seq_idx'),
696
+ to_regclass('runtime_observability.runtime_events_org_time_idx'),
697
+ to_regclass('runtime_observability.runtime_events_run_time_idx'),
698
+ to_regclass('runtime_observability.runtime_events_doc_idx'),
699
+ to_regclass('runtime_observability.runtime_events_retention_idx'),
700
+ to_regclass('runtime_observability.runtime_events_receipt_ref_idx')
701
+ ]::oid[]))
702
+ AND to_regclass('runtime_observability.runtime_run_log_cursors_retention_idx') IS NULL
703
+ AS ready,
704
+ (SELECT pg_get_expr(polqual, polrelid) FROM pg_policy
705
+ WHERE polrelid = to_regclass('runtime_observability.events')
706
+ AND polname = 'runtime_events_read_scope') AS events_policy,
707
+ (SELECT pg_get_expr(polqual, polrelid) FROM pg_policy
708
+ WHERE polrelid = to_regclass('runtime_observability.run_log_cursors')
709
+ AND polname = 'runtime_run_log_read_scope') AS cursors_policy`,
710
+ );
711
+ const shape = result.rows[0];
712
+ const expectedPolicy = `org_id = current_setting('app.org_id', true)
713
+ AND runtime_scope = current_setting('app.runtime_scope', true)`;
714
+ return (
715
+ shape?.ready === true &&
716
+ shape.events_policy !== null &&
717
+ shape.events_policy !== undefined &&
718
+ normalizedRetentionPredicate(shape.events_policy) ===
719
+ normalizedRetentionPredicate(expectedPolicy) &&
720
+ shape.cursors_policy !== null &&
721
+ shape.cursors_policy !== undefined &&
722
+ normalizedRetentionPredicate(shape.cursors_policy) ===
723
+ normalizedRetentionPredicate(expectedPolicy)
724
+ );
725
+ }
726
+
565
727
  export async function ensureRuntimeEventSearchSchema(
566
728
  client: PostgresSchedulerQueryClient,
567
729
  ): Promise<void> {
730
+ // Projection marker refreshes only replace function bodies and triggers.
731
+ // Replaying ADD COLUMN IF NOT EXISTS, DROP POLICY, or CREATE INDEX against
732
+ // this busy journal unnecessarily takes heavyweight locks even when the
733
+ // reader-first storage expansion is already complete.
734
+ if (await hasRuntimeEventSearchStorageShape(client)) {
735
+ await ensureRuntimeEventFetchReceiptFunction(client);
736
+ return;
737
+ }
738
+
568
739
  await client.query('CREATE SCHEMA IF NOT EXISTS runtime_observability');
569
740
  await client.query(`
570
741
  CREATE TABLE IF NOT EXISTS runtime_observability.events (
@@ -663,37 +834,7 @@ export async function ensureRuntimeEventSearchSchema(
663
834
  )`);
664
835
  await client.query(`REVOKE ALL ON SCHEMA runtime_observability FROM PUBLIC`);
665
836
  await client.query(`REVOKE ALL ON runtime_observability.events FROM PUBLIC`);
666
- await client.query(`
667
- CREATE OR REPLACE FUNCTION runtime_observability.fetch_receipt(
668
- p_scope text, p_org text, p_run text, p_ref uuid
669
- ) RETURNS jsonb
670
- LANGUAGE plpgsql SECURITY DEFINER
671
- SET search_path = pg_catalog
672
- AS $function$
673
- DECLARE answer jsonb; receipt_key_value text;
674
- BEGIN
675
- IF p_scope !~ '^[a-z_][a-z0-9_]{0,62}$'
676
- OR p_org IS NULL OR p_run IS NULL OR p_ref IS NULL
677
- OR p_org IS DISTINCT FROM current_setting('app.org_id', true)
678
- OR p_scope IS DISTINCT FROM current_setting('app.runtime_scope', true)
679
- THEN RETURN NULL; END IF;
680
- SELECT event.receipt_key INTO receipt_key_value
681
- FROM runtime_observability.events AS event
682
- WHERE event.runtime_scope = p_scope AND event.org_id = p_org
683
- AND event.run_id = p_run AND event.receipt_ref = p_ref
684
- AND event.ingested_at >= clock_timestamp() - interval '7 days';
685
- IF receipt_key_value IS NULL THEN RETURN NULL; END IF;
686
- EXECUTE format(
687
- 'SELECT jsonb_build_object(''receiptKey'', k, ''status'', status, ''output'', output_json, ''outputRef'', output_ref, ''error'', error, ''errorPayload'', error_payload) FROM %I.receipts WHERE org_id = $1 AND run_id = $2 AND k = $3 AND status IN (''completed'', ''failed'', ''skipped'')',
688
- p_scope
689
- ) INTO answer USING p_org, p_run, receipt_key_value;
690
- RETURN answer;
691
- END
692
- $function$
693
- `);
694
- await client.query(
695
- `REVOKE ALL ON FUNCTION runtime_observability.fetch_receipt(text,text,text,uuid) FROM PUBLIC`,
696
- );
837
+ await ensureRuntimeEventFetchReceiptFunction(client);
697
838
  }
698
839
 
699
840
  /** The exact provisioned PlanetScale reader role; never grant to a guessed role. */
@@ -710,7 +851,8 @@ export async function grantRuntimeEventReader(
710
851
  await client.query(`GRANT USAGE ON SCHEMA runtime_observability TO ${role}`);
711
852
  await client.query(`GRANT SELECT (
712
853
  runtime_scope, org_id, event_id, occurred_at, ingested_at, run_id,
713
- log_seq, log_body, play_name, kind, level, message, receipt_ref, search_doc
854
+ log_seq, log_body, play_name, kind, level, message, receipt_ref, search_doc,
855
+ event_payload
714
856
  ) ON runtime_observability.events TO ${role}`);
715
857
  await client.query(`GRANT SELECT (runtime_scope, org_id, run_id, last_seq)
716
858
  ON runtime_observability.run_log_cursors TO ${role}`);
@@ -736,6 +878,7 @@ export async function hasRuntimeEventReaderGrant(
736
878
  AND has_column_privilege(roles.oid, 'runtime_observability.events', 'event_id', 'SELECT')
737
879
  AND has_column_privilege(roles.oid, 'runtime_observability.events', 'log_seq', 'SELECT')
738
880
  AND has_column_privilege(roles.oid, 'runtime_observability.events', 'log_body', 'SELECT')
881
+ AND has_column_privilege(roles.oid, 'runtime_observability.events', 'event_payload', 'SELECT')
739
882
  AND NOT has_column_privilege(roles.oid, 'runtime_observability.events', 'receipt_key', 'SELECT')
740
883
  AND NOT has_table_privilege(roles.oid, 'runtime_observability.events', 'INSERT')
741
884
  AND NOT has_any_column_privilege(roles.oid, 'runtime_observability.events', 'INSERT')
@@ -807,7 +950,7 @@ export async function attachRuntimeEventSearchTriggers(
807
950
  await client.query(`
808
951
  CREATE OR REPLACE FUNCTION ${schema}.search_receipt_event() RETURNS trigger
809
952
  LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
810
- DECLARE play text; ref uuid;
953
+ DECLARE play text; ref uuid; canonical_events boolean;
811
954
  BEGIN
812
955
  IF NEW.status NOT IN ('completed', 'failed', 'skipped') THEN RETURN NEW; END IF;
813
956
  IF TG_OP = 'UPDATE' THEN
@@ -820,17 +963,26 @@ export async function attachRuntimeEventSearchTriggers(
820
963
  IF NEW.run_id IS NULL OR NEW.org_id IS NULL THEN
821
964
  RAISE EXCEPTION 'Terminal receipt search projection requires run and organization';
822
965
  END IF;
823
- SELECT play_name INTO play FROM ${schema}.runs
966
+ -- Serialize event ordering with the same per-run lock as outbox facts.
967
+ PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(NEW.run_id));
968
+ SELECT play_name, run_event_store = 'planet_scale'
969
+ INTO play, canonical_events FROM ${schema}.runs
824
970
  WHERE run_id = NEW.run_id AND org_id = NEW.org_id;
825
971
  IF play IS NULL THEN
826
972
  RAISE EXCEPTION 'Terminal receipt search projection has no matching run';
827
973
  END IF;
828
974
  ref := gen_random_uuid();
829
975
  INSERT INTO runtime_observability.events (
830
- runtime_scope, org_id, event_id, occurred_at, run_id, play_name,
831
- kind, receipt_ref, receipt_key, search_doc
976
+ runtime_scope, org_id, event_id, event_seq, event_payload,
977
+ occurred_at, run_id, play_name, kind, receipt_ref, receipt_key, search_doc
832
978
  ) VALUES (
833
979
  ${quoteLiteral(scope)}, NEW.org_id, 'receipt:' || ref::text,
980
+ CASE WHEN canonical_events THEN nextval('runtime_observability.runtime_event_order_seq') END,
981
+ CASE WHEN canonical_events THEN jsonb_build_object(
982
+ 'type', 'receipt.' || NEW.status, 'runId', NEW.run_id,
983
+ 'occurredAt', floor(extract(epoch FROM NEW.updated_at) * 1000)::bigint,
984
+ 'source', 'receipt', 'status', NEW.status
985
+ ) END,
834
986
  NEW.updated_at, NEW.run_id, play, 'receipt.' || NEW.status,
835
987
  ref, NEW.k, jsonb_strip_nulls(jsonb_build_object(
836
988
  'kind', 'receipt.' || NEW.status, 'status', NEW.status,
@@ -851,7 +1003,7 @@ export async function attachRuntimeEventSearchTriggers(
851
1003
  await client.query(`
852
1004
  CREATE OR REPLACE FUNCTION ${schema}.project_outbox_event(observation ${schema}.outbox) RETURNS void
853
1005
  LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
854
- DECLARE run_org text; play text; compact_logs boolean; run_status text; item jsonb; line jsonb; event_batch jsonb;
1006
+ DECLARE run_org text; play text; compact_logs boolean; canonical_events boolean; run_status text; item jsonb; line jsonb; event_batch jsonb;
855
1007
  ordinal bigint; line_ordinal bigint; kind text; at_time timestamptz;
856
1008
  doc jsonb; line_doc jsonb; line_context jsonb;
857
1009
  eid text; existing_digest text; digest text; assigned_seq bigint; terminal_progress_identity boolean;
@@ -868,8 +1020,9 @@ export async function attachRuntimeEventSearchTriggers(
868
1020
  -- scheduler's Run/attempt row-lock order. In particular, old completion
869
1021
  -- writers lock attempt before Run; this recorder must not invert that.
870
1022
  PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(observation.run_id));
871
- SELECT org_id, play_name, run_log_store = 'planet_scale', status
872
- INTO run_org, play, compact_logs, run_status
1023
+ SELECT org_id, play_name, run_log_store = 'planet_scale', status,
1024
+ run_event_store = 'planet_scale'
1025
+ INTO run_org, play, compact_logs, run_status, canonical_events
873
1026
  FROM ${schema}.runs WHERE run_id = observation.run_id;
874
1027
  IF run_org IS NULL OR play IS NULL THEN
875
1028
  RAISE EXCEPTION 'Outbox search projection has no matching run';
@@ -940,7 +1093,7 @@ export async function attachRuntimeEventSearchTriggers(
940
1093
  'run.completed', 'run.failed', 'run.cancelled',
941
1094
  'step.started', 'step.progress', 'step.completed',
942
1095
  'step.failed', 'step.skipped', 'log.appended',
943
- 'dataset.lifecycle', 'activity.observed'
1096
+ 'dataset.lifecycle', 'activity.observed', 'work.counters'
944
1097
  ) THEN
945
1098
  kind := 'runtime.event';
946
1099
  END IF;
@@ -1161,10 +1314,20 @@ export async function attachRuntimeEventSearchTriggers(
1161
1314
  RAISE EXCEPTION 'Run log ingress is closed after terminal acceptance';
1162
1315
  END IF;
1163
1316
  INSERT INTO runtime_observability.events (
1164
- runtime_scope, org_id, event_id, occurred_at, run_id, play_name,
1165
- kind, level, message, log_body, source_digest, log_text_digest, search_doc
1317
+ runtime_scope, org_id, event_id, event_seq, event_payload,
1318
+ occurred_at, run_id, play_name, kind, level, message, log_body,
1319
+ source_digest, log_text_digest, search_doc
1166
1320
  ) VALUES (
1167
- ${quoteLiteral(scope)}, run_org, eid, at_time, observation.run_id, play,
1321
+ ${quoteLiteral(scope)}, run_org, eid,
1322
+ CASE WHEN canonical_events THEN nextval('runtime_observability.runtime_event_order_seq') END,
1323
+ CASE WHEN canonical_events THEN jsonb_strip_nulls(
1324
+ (item - 'lines' - 'lineContexts') || jsonb_build_object(
1325
+ 'type', 'log.appended', 'runId', observation.run_id,
1326
+ 'occurredAt', floor(extract(epoch FROM at_time) * 1000)::bigint,
1327
+ 'lineOrdinal', line_ordinal,
1328
+ 'context', line_context
1329
+ )) END,
1330
+ at_time, observation.run_id, play,
1168
1331
  'log.appended', NULL, CASE WHEN compact_logs THEN NULL ELSE left(line #>> '{}', 2048) END,
1169
1332
  line #>> '{}', digest,
1170
1333
  encode(sha256(convert_to(btrim(line #>> '{}'), 'UTF8')), 'hex'), line_doc
@@ -1193,10 +1356,12 @@ export async function attachRuntimeEventSearchTriggers(
1193
1356
  digest := CASE WHEN terminal_progress_identity
1194
1357
  THEN encode(sha256(convert_to(item::text, 'UTF8')), 'hex') ELSE NULL END;
1195
1358
  INSERT INTO runtime_observability.events (
1196
- runtime_scope, org_id, event_id, occurred_at, run_id, play_name,
1197
- kind, source_digest, search_doc
1359
+ runtime_scope, org_id, event_id, event_seq, event_payload,
1360
+ occurred_at, run_id, play_name, kind, source_digest, search_doc
1198
1361
  ) VALUES (
1199
1362
  ${quoteLiteral(scope)}, run_org, eid,
1363
+ CASE WHEN canonical_events THEN nextval('runtime_observability.runtime_event_order_seq') END,
1364
+ CASE WHEN canonical_events THEN item END,
1200
1365
  at_time, observation.run_id, play, kind, digest, doc
1201
1366
  ) ON CONFLICT DO NOTHING;
1202
1367
  IF NOT FOUND AND terminal_progress_identity THEN
@@ -1215,10 +1380,22 @@ export async function attachRuntimeEventSearchTriggers(
1215
1380
  ) AND NOT (observation.event_type IN ('run.waiting', 'run.resumed')
1216
1381
  AND observation.payload_json->>'kind' = 'detached_runner') THEN
1217
1382
  INSERT INTO runtime_observability.events (
1218
- runtime_scope, org_id, event_id, occurred_at, run_id, play_name,
1219
- kind, search_doc
1383
+ runtime_scope, org_id, event_id, event_seq, event_payload,
1384
+ occurred_at, run_id, play_name, kind, search_doc
1220
1385
  ) VALUES (
1221
- ${quoteLiteral(scope)}, run_org, observation.event_id, observation.created_at,
1386
+ ${quoteLiteral(scope)}, run_org, observation.event_id,
1387
+ CASE WHEN canonical_events THEN nextval('runtime_observability.runtime_event_order_seq') END,
1388
+ CASE WHEN canonical_events THEN jsonb_strip_nulls(jsonb_build_object(
1389
+ 'type', observation.event_type, 'runId', observation.run_id,
1390
+ 'occurredAt', floor(extract(epoch FROM observation.created_at) * 1000)::bigint,
1391
+ 'source', 'system', 'playName', play,
1392
+ 'status', observation.payload_json->>'status',
1393
+ 'error', observation.payload_json->>'error',
1394
+ 'waitKind', observation.payload_json->>'waitKind',
1395
+ 'waitUntil', observation.payload_json->'waitUntil',
1396
+ 'resultSummary', observation.payload_json->'resultSummary'
1397
+ )) END,
1398
+ observation.created_at,
1222
1399
  observation.run_id, play, observation.event_type, jsonb_build_object('kind', observation.event_type, 'source', 'system')
1223
1400
  ) ON CONFLICT DO NOTHING;
1224
1401
  END IF;
@@ -1329,10 +1506,18 @@ export async function attachRuntimeEventSearchTriggers(
1329
1506
  line_doc := line_doc || jsonb_build_object('context', line_context);
1330
1507
  END IF;
1331
1508
  INSERT INTO runtime_observability.events (
1332
- runtime_scope, org_id, event_id, occurred_at, run_id, play_name,
1333
- kind, level, message, log_body, source_digest, log_text_digest, search_doc
1509
+ runtime_scope, org_id, event_id, event_seq, event_payload,
1510
+ occurred_at, run_id, play_name, kind, level, message, log_body,
1511
+ source_digest, log_text_digest, search_doc
1334
1512
  ) VALUES (
1335
1513
  ${quoteLiteral(scope)}, run_org, observation.event_id || ':log:' || line_ordinal,
1514
+ CASE WHEN canonical_events THEN nextval('runtime_observability.runtime_event_order_seq') END,
1515
+ CASE WHEN canonical_events THEN jsonb_strip_nulls(jsonb_build_object(
1516
+ 'type', 'log.appended', 'runId', observation.run_id,
1517
+ 'occurredAt', floor(extract(epoch FROM observation.created_at) * 1000)::bigint,
1518
+ 'source', 'terminal', 'producerAttempt', observation.payload_json->'producerAttempt',
1519
+ 'lineOrdinal', line_ordinal, 'context', line_context
1520
+ )) END,
1336
1521
  observation.created_at, observation.run_id, play, 'log.appended', NULL,
1337
1522
  CASE WHEN compact_logs THEN NULL ELSE left(regexp_replace(line #>> '{}',
1338
1523
  '^' || chr(1) || 'prov:[^' || chr(1) || ']*' || chr(1), ''), 2048) END,
@@ -1380,6 +1565,92 @@ export async function attachRuntimeEventSearchTriggers(
1380
1565
  WHERE coalesce(item.value->>'type', item.value->>'key') IS DISTINCT FROM 'log.appended'
1381
1566
  $function$
1382
1567
  `);
1568
+ await client.query(`
1569
+ CREATE OR REPLACE FUNCTION ${schema}.convex_projection_event(value jsonb) RETURNS jsonb
1570
+ LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $function$
1571
+ WITH event AS (
1572
+ SELECT coalesce(value->>'type', value->>'key') AS kind
1573
+ )
1574
+ SELECT CASE
1575
+ WHEN event.kind IN ('rollup.membership.added', 'rollup.membership.removed')
1576
+ AND value->>'opaque' = 'true' THEN jsonb_strip_nulls(jsonb_build_object(
1577
+ 'type', event.kind, 'runId', value->'runId',
1578
+ 'occurredAt', value->'occurredAt', 'source', value->'source',
1579
+ 'opaque', true, 'payload', value->'payload'))
1580
+ WHEN event.kind IN (
1581
+ 'run.created', 'run.started', 'run.waiting', 'run.resumed',
1582
+ 'run.completed', 'run.failed', 'run.cancelled',
1583
+ 'step.started', 'step.progress', 'step.completed',
1584
+ 'step.failed', 'step.skipped', 'dataset.lifecycle',
1585
+ 'activity.observed', 'work.counters'
1586
+ ) THEN jsonb_strip_nulls(jsonb_build_object(
1587
+ 'type', event.kind, 'runId', value->'runId',
1588
+ 'occurredAt', value->'occurredAt', 'source', value->'source'
1589
+ ) || CASE event.kind
1590
+ WHEN 'run.created' THEN jsonb_build_object(
1591
+ 'playName', value->'playName', 'status', value->'status',
1592
+ 'runtimeBackend', value->'runtimeBackend')
1593
+ WHEN 'run.started' THEN jsonb_build_object(
1594
+ 'playName', value->'playName', 'runtimeBackend', value->'runtimeBackend')
1595
+ WHEN 'run.waiting' THEN jsonb_build_object(
1596
+ 'waitKind', value->'waitKind', 'waitUntil', value->'waitUntil')
1597
+ WHEN 'run.completed' THEN jsonb_build_object(
1598
+ 'result', value->'result', 'resultSummary', value->'resultSummary',
1599
+ 'terminalWorkCounterSegment', value->'terminalWorkCounterSegment')
1600
+ WHEN 'run.failed' THEN jsonb_build_object(
1601
+ 'error', value->'error', 'result', value->'result',
1602
+ 'resultSummary', value->'resultSummary',
1603
+ 'terminalWorkCounterSegment', value->'terminalWorkCounterSegment')
1604
+ WHEN 'run.cancelled' THEN jsonb_build_object(
1605
+ 'error', value->'error', 'result', value->'result')
1606
+ WHEN 'step.started' THEN jsonb_build_object(
1607
+ 'stepId', value->'stepId', 'label', value->'label',
1608
+ 'kind', value->'kind', 'artifactTableNamespace', value->'artifactTableNamespace')
1609
+ WHEN 'step.progress' THEN jsonb_build_object(
1610
+ 'stepId', value->'stepId', 'label', value->'label',
1611
+ 'kind', value->'kind', 'status', value->'status', 'progress', value->'progress')
1612
+ WHEN 'step.completed' THEN jsonb_build_object(
1613
+ 'stepId', value->'stepId', 'label', value->'label',
1614
+ 'kind', value->'kind', 'artifactTableNamespace', value->'artifactTableNamespace')
1615
+ WHEN 'step.failed' THEN jsonb_build_object(
1616
+ 'stepId', value->'stepId', 'label', value->'label', 'kind', value->'kind',
1617
+ 'error', value->'error', 'artifactTableNamespace', value->'artifactTableNamespace')
1618
+ WHEN 'step.skipped' THEN jsonb_build_object(
1619
+ 'stepId', value->'stepId', 'label', value->'label',
1620
+ 'kind', value->'kind', 'artifactTableNamespace', value->'artifactTableNamespace')
1621
+ WHEN 'dataset.lifecycle' THEN jsonb_build_object(
1622
+ 'datasetId', value->'datasetId', 'path', value->'path',
1623
+ 'tableNamespace', value->'tableNamespace', 'phase', value->'phase',
1624
+ 'persistedRows', value->'persistedRows', 'succeededRows', value->'succeededRows',
1625
+ 'failedRows', value->'failedRows', 'complete', value->'complete',
1626
+ 'bornFrom', value->'bornFrom')
1627
+ WHEN 'activity.observed' THEN jsonb_build_object(
1628
+ 'observation', value->'observation')
1629
+ WHEN 'work.counters' THEN jsonb_build_object(
1630
+ 'attempt', value->'attempt', 'invocationSeq', value->'invocationSeq',
1631
+ 'segmentId', value->'segmentId', 'logicalCalls', value->'logicalCalls',
1632
+ 'providerRequests', value->'providerRequests', 'provider429s', value->'provider429s',
1633
+ 'final', value->'final')
1634
+ ELSE '{}'::jsonb
1635
+ END)
1636
+ ELSE NULL
1637
+ END
1638
+ FROM event
1639
+ $function$
1640
+ `);
1641
+ await client.query(`
1642
+ CREATE OR REPLACE FUNCTION ${schema}.convex_projection_events(batch jsonb) RETURNS jsonb
1643
+ LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $function$
1644
+ WITH projected AS (
1645
+ SELECT item.ordinality,
1646
+ ${schema}.convex_projection_event(item.value) AS value
1647
+ FROM jsonb_array_elements(batch) WITH ORDINALITY AS item(value, ordinality)
1648
+ )
1649
+ SELECT coalesce(jsonb_agg(projected.value ORDER BY projected.ordinality)
1650
+ FILTER (WHERE projected.value IS NOT NULL), '[]'::jsonb)
1651
+ FROM projected
1652
+ $function$
1653
+ `);
1383
1654
  await client.query(`
1384
1655
  CREATE OR REPLACE FUNCTION ${schema}.without_runtime_log_tail(value jsonb) RETURNS jsonb
1385
1656
  LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $function$
@@ -1408,14 +1679,15 @@ export async function attachRuntimeEventSearchTriggers(
1408
1679
  await client.query(`
1409
1680
  CREATE OR REPLACE FUNCTION ${schema}.search_outbox_event() RETURNS trigger
1410
1681
  LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
1411
- DECLARE compact_logs boolean;
1682
+ DECLARE compact_logs boolean; canonical_events boolean;
1412
1683
  BEGIN
1413
1684
  -- Project the original validated observation before replacing its
1414
1685
  -- transport body. Failure rolls back the outbox and canonical rows.
1415
1686
  PERFORM ${schema}.project_outbox_event(NEW);
1416
- SELECT run_log_store = 'planet_scale' INTO compact_logs
1687
+ SELECT run_log_store = 'planet_scale', run_event_store = 'planet_scale'
1688
+ INTO compact_logs, canonical_events
1417
1689
  FROM ${schema}.runs WHERE run_id = NEW.run_id;
1418
- IF compact_logs THEN
1690
+ IF compact_logs OR canonical_events THEN
1419
1691
  NEW.payload_json := NEW.payload_json - 'terminalLogLines' - 'terminalLogContexts';
1420
1692
  IF NEW.event_type = 'run.events' THEN
1421
1693
  NEW.payload_json := jsonb_set(NEW.payload_json, '{events}',
@@ -1435,6 +1707,16 @@ export async function attachRuntimeEventSearchTriggers(
1435
1707
  ${schema}.without_runtime_log_tail(NEW.payload_json->'output'));
1436
1708
  END IF;
1437
1709
  END IF;
1710
+ IF canonical_events THEN
1711
+ IF NEW.event_type = 'run.events' THEN
1712
+ NEW.payload_json := jsonb_set(NEW.payload_json, '{events}',
1713
+ ${schema}.convex_projection_events(NEW.payload_json->'events'));
1714
+ END IF;
1715
+ IF NEW.payload_json ? 'terminalProgressEvents' THEN
1716
+ NEW.payload_json := jsonb_set(NEW.payload_json, '{terminalProgressEvents}',
1717
+ ${schema}.convex_projection_events(NEW.payload_json->'terminalProgressEvents'));
1718
+ END IF;
1719
+ END IF;
1438
1720
  RETURN NEW;
1439
1721
  END
1440
1722
  $function$
@@ -1444,7 +1726,8 @@ export async function attachRuntimeEventSearchTriggers(
1444
1726
  LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
1445
1727
  DECLARE observation ${schema}.outbox; compact_logs boolean;
1446
1728
  BEGIN
1447
- SELECT run_log_store = 'planet_scale' INTO compact_logs
1729
+ SELECT run_log_store = 'planet_scale' OR run_event_store = 'planet_scale'
1730
+ INTO compact_logs
1448
1731
  FROM ${schema}.runs WHERE run_id = NEW.run_id;
1449
1732
  IF NOT coalesce(compact_logs, false) OR NEW.runner_log_payload_json IS NULL THEN
1450
1733
  RETURN NEW;
@@ -1493,7 +1776,7 @@ export async function attachRuntimeEventSearchTriggers(
1493
1776
  CREATE OR REPLACE FUNCTION ${schema}.compact_run_log_result() RETURNS trigger
1494
1777
  LANGUAGE plpgsql SET search_path = pg_catalog AS $function$
1495
1778
  BEGIN
1496
- IF NEW.run_log_store = 'planet_scale' THEN
1779
+ IF NEW.run_log_store = 'planet_scale' OR NEW.run_event_store = 'planet_scale' THEN
1497
1780
  IF NEW.status = 'failed' THEN
1498
1781
  NEW.terminal_result_json := ${schema}.compact_runner_log_envelope(NEW.terminal_result_json);
1499
1782
  ELSIF NEW.status = 'completed' THEN
@@ -1554,6 +1837,7 @@ export async function attachRuntimeEventSearchTriggers(
1554
1837
  await client.query(`REVOKE ALL ON FUNCTION ${schema}.project_outbox_event(${schema}.outbox),
1555
1838
  ${schema}.accept_runner_log_payload(), ${schema}.search_outbox_event(),
1556
1839
  ${schema}.compact_run_log_result(), ${schema}.without_log_events(jsonb),
1840
+ ${schema}.convex_projection_event(jsonb), ${schema}.convex_projection_events(jsonb),
1557
1841
  ${schema}.compact_runner_log_envelope(jsonb), ${schema}.without_runtime_log_tail(jsonb)
1558
1842
  FROM PUBLIC`);
1559
1843
  // Publish readiness last, after all acceptance/compaction functions exist.
@@ -1612,6 +1896,10 @@ export async function hasRuntimeEventSearchShape(
1612
1896
 
1613
1897
  const RUNTIME_EVENT_BODY_RETENTION_PREDICATE = `kind <> 'log.appended' OR log_body IS NOT NULL
1614
1898
  OR message IS NOT NULL OR search_doc ? 'context'`;
1899
+ const RUNTIME_EVENT_CANONICAL_RETENTION_PREDICATE = `(kind <> 'log.appended' AND event_payload IS NULL)
1900
+ OR (substr(kind, 1, 8) = 'receipt.' AND event_payload IS NOT NULL
1901
+ AND (receipt_ref IS NOT NULL OR receipt_key IS NOT NULL))
1902
+ OR (kind = 'log.appended' AND (log_body IS NOT NULL OR message IS NOT NULL OR search_doc ? 'context'))`;
1615
1903
 
1616
1904
  // pg_get_expr adds grouping and text casts. Preserve quoted literals exactly;
1617
1905
  // this known OR-only predicate rejects other operators, keys and literal values.
@@ -1835,54 +2123,76 @@ export async function prepareRuntimeEventRetentionIndex(
1835
2123
  key2: string | null;
1836
2124
  predicate: string | null;
1837
2125
  };
1838
- const inspect = () =>
1839
- client.query<IndexShape>(
1840
- `SELECT index.indisvalid AS valid, index.indisready AS ready, index.indisunique AS is_unique,
1841
- index.indrelid = to_regclass('runtime_observability.events') AS table_matches,
1842
- index.indnkeyatts AS key_count, index.indnatts AS attribute_count,
1843
- method.amname AS method,
1844
- pg_get_indexdef(index.indexrelid, 1, true) AS key1,
1845
- pg_get_indexdef(index.indexrelid, 2, true) AS key2,
1846
- pg_get_expr(index.indpred, index.indrelid) AS predicate
1847
- FROM pg_index AS index
1848
- JOIN pg_class AS relation ON relation.oid = index.indexrelid
1849
- JOIN pg_am AS method ON method.oid = relation.relam
1850
- WHERE index.indexrelid = to_regclass('runtime_observability.runtime_events_body_retention_idx')`,
1851
- );
1852
- const assertReady = (shape: IndexShape): void => {
1853
- if (!shape.valid || !shape.ready) {
2126
+ const prepare = async (input: {
2127
+ name: string;
2128
+ predicate: string;
2129
+ marksJournalReady?: boolean;
2130
+ }) => {
2131
+ const inspect = () =>
2132
+ client.query<IndexShape>(
2133
+ `SELECT index.indisvalid AS valid, index.indisready AS ready, index.indisunique AS is_unique,
2134
+ index.indrelid = to_regclass('runtime_observability.events') AS table_matches,
2135
+ index.indnkeyatts AS key_count, index.indnatts AS attribute_count,
2136
+ method.amname AS method,
2137
+ pg_get_indexdef(index.indexrelid, 1, true) AS key1,
2138
+ pg_get_indexdef(index.indexrelid, 2, true) AS key2,
2139
+ pg_get_expr(index.indpred, index.indrelid) AS predicate
2140
+ FROM pg_index AS index
2141
+ JOIN pg_class AS relation ON relation.oid = index.indexrelid
2142
+ JOIN pg_am AS method ON method.oid = relation.relam
2143
+ WHERE index.indexrelid = to_regclass($1)`,
2144
+ [`runtime_observability.${input.name}`],
2145
+ );
2146
+ const assertReady = (shape: IndexShape): void => {
2147
+ if (!shape.valid || !shape.ready) {
2148
+ throw new Error(
2149
+ `Runtime retention index ${input.name} is invalid or incomplete. Inspect the failed build; repair it explicitly before retrying.`,
2150
+ );
2151
+ }
2152
+ if (
2153
+ !shape.table_matches ||
2154
+ shape.is_unique ||
2155
+ shape.key_count !== 2 ||
2156
+ shape.attribute_count !== 2 ||
2157
+ shape.method !== 'btree' ||
2158
+ shape.key1 !== 'ingested_at' ||
2159
+ shape.key2 !== 'event_id' ||
2160
+ shape.predicate === null ||
2161
+ normalizedRetentionPredicate(shape.predicate) !==
2162
+ normalizedRetentionPredicate(input.predicate)
2163
+ ) {
2164
+ throw new Error(
2165
+ `Runtime retention index ${input.name} definition differs from its expected table, uniqueness, keys or predicate. Actual catalog shape: ${JSON.stringify(shape)}. Inspect and repair it explicitly.`,
2166
+ );
2167
+ }
2168
+ };
2169
+ const existing = (await inspect()).rows[0];
2170
+ if (existing) assertReady(existing);
2171
+ await client.query(`CREATE INDEX CONCURRENTLY IF NOT EXISTS ${input.name}
2172
+ ON runtime_observability.events (ingested_at, event_id)
2173
+ WHERE ${input.predicate}`);
2174
+ const built = (await inspect()).rows[0];
2175
+ if (!built) {
1854
2176
  throw new Error(
1855
- 'Runtime body retention index is invalid or incomplete. Inspect the failed build; repair it explicitly before retrying.',
2177
+ `Runtime retention index ${input.name} was not found after preparation.`,
1856
2178
  );
1857
2179
  }
1858
- if (
1859
- !shape.table_matches ||
1860
- shape.is_unique ||
1861
- shape.key_count !== 2 ||
1862
- shape.attribute_count !== 2 ||
1863
- shape.method !== 'btree' ||
1864
- shape.key1 !== 'ingested_at' ||
1865
- shape.key2 !== 'event_id' ||
1866
- shape.predicate === null ||
1867
- normalizedRetentionPredicate(shape.predicate) !==
1868
- normalizedRetentionPredicate(RUNTIME_EVENT_BODY_RETENTION_PREDICATE)
1869
- ) {
1870
- throw new Error(
1871
- 'Runtime body retention index definition differs from its expected table, uniqueness, keys or predicate. Inspect and repair it explicitly.',
2180
+ assertReady(built);
2181
+ if (input.marksJournalReady) {
2182
+ await client.query(
2183
+ `COMMENT ON INDEX runtime_observability.runtime_events_canonical_retention_idx IS ${quoteLiteral(RUNTIME_EVENT_CANONICAL_RETENTION_INDEX_READY_MARKER)}`,
1872
2184
  );
1873
2185
  }
1874
2186
  };
1875
- const existing = (await inspect()).rows[0];
1876
- if (existing) assertReady(existing);
1877
- await client.query(`CREATE INDEX CONCURRENTLY IF NOT EXISTS runtime_events_body_retention_idx
1878
- ON runtime_observability.events (ingested_at, event_id)
1879
- WHERE ${RUNTIME_EVENT_BODY_RETENTION_PREDICATE}`);
1880
- const built = (await inspect()).rows[0];
1881
- if (!built)
1882
- throw new Error(
1883
- 'Runtime body retention index was not found after preparation.',
1884
- );
1885
- assertReady(built);
2187
+ await prepare({
2188
+ name: 'runtime_events_body_retention_idx',
2189
+ predicate: RUNTIME_EVENT_BODY_RETENTION_PREDICATE,
2190
+ });
2191
+ await prepare({
2192
+ name: 'runtime_events_canonical_retention_idx',
2193
+ predicate: RUNTIME_EVENT_CANONICAL_RETENTION_PREDICATE,
2194
+ marksJournalReady: true,
2195
+ });
1886
2196
  }
1887
2197
 
1888
2198
  /** One bounded operator-owned TTL pass. Never called by a customer request. */
@@ -1907,10 +2217,9 @@ export async function pruneRuntimeEvents(
1907
2217
  }
1908
2218
  const result = await client.query<{ event_id: string }>(
1909
2219
  `WITH candidates AS (
1910
- SELECT ctid FROM runtime_observability.events
2220
+ SELECT ctid FROM runtime_observability.events
1911
2221
  WHERE ingested_at < $1::timestamptz
1912
- AND (kind <> 'log.appended' OR log_body IS NOT NULL
1913
- OR message IS NOT NULL OR search_doc ? 'context')
2222
+ AND (${RUNTIME_EVENT_CANONICAL_RETENTION_PREDICATE})
1914
2223
  ORDER BY ingested_at, event_id
1915
2224
  LIMIT $2::integer FOR UPDATE SKIP LOCKED
1916
2225
  ), scrubbed AS (
@@ -1918,6 +2227,8 @@ export async function pruneRuntimeEvents(
1918
2227
  SET log_text_digest = coalesce(event.log_text_digest,
1919
2228
  encode(sha256(convert_to(btrim(coalesce(event.log_body, event.message)), 'UTF8')), 'hex')),
1920
2229
  log_body = NULL, message = NULL,
2230
+ event_payload = CASE WHEN event.event_payload IS NULL THEN NULL
2231
+ ELSE event.event_payload - 'context' END,
1921
2232
  search_doc = jsonb_strip_nulls(jsonb_build_object(
1922
2233
  'kind', 'log.appended', 'source', event.search_doc->'source',
1923
2234
  'producerAttempt', event.search_doc->'producerAttempt',
@@ -1925,12 +2236,24 @@ export async function pruneRuntimeEvents(
1925
2236
  'logContentDigest', event.search_doc->'logContentDigest'))
1926
2237
  FROM candidates WHERE event.ctid = candidates.ctid AND event.kind = 'log.appended'
1927
2238
  RETURNING event.event_id
2239
+ ), scrubbed_receipts AS (
2240
+ UPDATE runtime_observability.events AS event
2241
+ SET receipt_ref = NULL, receipt_key = NULL
2242
+ FROM candidates
2243
+ WHERE event.ctid = candidates.ctid
2244
+ AND left(event.kind, 8) = 'receipt.'
2245
+ AND event.event_payload IS NOT NULL
2246
+ AND (event.receipt_ref IS NOT NULL OR event.receipt_key IS NOT NULL)
2247
+ RETURNING event.event_id
1928
2248
  ), deleted AS (
1929
2249
  DELETE FROM runtime_observability.events AS event USING candidates
1930
2250
  WHERE event.ctid = candidates.ctid AND event.kind <> 'log.appended'
2251
+ AND event.event_payload IS NULL
1931
2252
  RETURNING event.event_id
1932
2253
  )
1933
- SELECT event_id FROM scrubbed UNION ALL SELECT event_id FROM deleted`,
2254
+ SELECT event_id FROM scrubbed
2255
+ UNION ALL SELECT event_id FROM scrubbed_receipts
2256
+ UNION ALL SELECT event_id FROM deleted`,
1934
2257
  [input.before.toISOString(), input.limit],
1935
2258
  );
1936
2259
  // Keep identity/digests until fenced ingress and pending delivery drain. A
@@ -1956,14 +2279,12 @@ export async function inspectRuntimeEventRetention(
1956
2279
  }>(
1957
2280
  `WITH oldest AS (
1958
2281
  SELECT ingested_at FROM runtime_observability.events
1959
- WHERE kind <> 'log.appended' OR log_body IS NOT NULL
1960
- OR message IS NOT NULL OR search_doc ? 'context'
2282
+ WHERE (${RUNTIME_EVENT_CANONICAL_RETENTION_PREDICATE})
1961
2283
  ORDER BY ingested_at, event_id LIMIT 1
1962
2284
  ), bounded AS (
1963
2285
  SELECT 1 FROM runtime_observability.events
1964
2286
  WHERE ingested_at < $1::timestamptz
1965
- AND (kind <> 'log.appended' OR log_body IS NOT NULL
1966
- OR message IS NOT NULL OR search_doc ? 'context')
2287
+ AND (${RUNTIME_EVENT_CANONICAL_RETENTION_PREDICATE})
1967
2288
  ORDER BY ingested_at, event_id LIMIT 10001
1968
2289
  )
1969
2290
  SELECT (SELECT ingested_at FROM oldest) AS oldest,
package/dist/cli/index.js CHANGED
@@ -3512,7 +3512,7 @@ var SDK_RELEASE = {
3512
3512
  // getters keep their established compatibility behavior.
3513
3513
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3514
3514
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3515
- version: "0.3.267",
3515
+ version: "0.3.269",
3516
3516
  updateSummary: "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.",
3517
3517
  packageCapabilities: {
3518
3518
  updatePreferences: 1
@@ -3507,7 +3507,7 @@ var SDK_RELEASE = {
3507
3507
  // getters keep their established compatibility behavior.
3508
3508
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3509
3509
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3510
- version: "0.3.267",
3510
+ version: "0.3.269",
3511
3511
  updateSummary: "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.",
3512
3512
  packageCapabilities: {
3513
3513
  updatePreferences: 1
package/dist/index.js CHANGED
@@ -1325,7 +1325,7 @@ var SDK_RELEASE = {
1325
1325
  // getters keep their established compatibility behavior.
1326
1326
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
1327
1327
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
1328
- version: "0.3.267",
1328
+ version: "0.3.269",
1329
1329
  updateSummary: "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.",
1330
1330
  packageCapabilities: {
1331
1331
  updatePreferences: 1
package/dist/index.mjs CHANGED
@@ -1229,7 +1229,7 @@ var SDK_RELEASE = {
1229
1229
  // getters keep their established compatibility behavior.
1230
1230
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
1231
1231
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
1232
- version: "0.3.267",
1232
+ version: "0.3.269",
1233
1233
  updateSummary: "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.",
1234
1234
  packageCapabilities: {
1235
1235
  updatePreferences: 1
@@ -149,7 +149,7 @@ type SdkRelease = {
149
149
  supportPolicy: SdkSupportPolicy;
150
150
  };
151
151
  declare const SDK_RELEASE: {
152
- readonly version: "0.3.267";
152
+ readonly version: "0.3.269";
153
153
  readonly updateSummary: "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.";
154
154
  readonly packageCapabilities: {
155
155
  readonly updatePreferences: 1;
package/dist/release.d.ts CHANGED
@@ -149,7 +149,7 @@ type SdkRelease = {
149
149
  supportPolicy: SdkSupportPolicy;
150
150
  };
151
151
  declare const SDK_RELEASE: {
152
- readonly version: "0.3.267";
152
+ readonly version: "0.3.269";
153
153
  readonly updateSummary: "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.";
154
154
  readonly packageCapabilities: {
155
155
  readonly updatePreferences: 1;
package/dist/release.js CHANGED
@@ -74,7 +74,7 @@ var SDK_RELEASE = {
74
74
  // getters keep their established compatibility behavior.
75
75
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
76
76
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
77
- version: "0.3.267",
77
+ version: "0.3.269",
78
78
  updateSummary: "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.",
79
79
  packageCapabilities: {
80
80
  updatePreferences: 1
package/dist/release.mjs CHANGED
@@ -48,7 +48,7 @@ var SDK_RELEASE = {
48
48
  // getters keep their established compatibility behavior.
49
49
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
50
50
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
51
- version: "0.3.267",
51
+ version: "0.3.269",
52
52
  updateSummary: "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.",
53
53
  packageCapabilities: {
54
54
  updatePreferences: 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.3.267",
3
+ "version": "0.3.269",
4
4
  "description": "GTM data CLI and TypeScript SDK for coding agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://code.deepline.com",