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.
@@ -12,7 +12,7 @@ const EVENTS_TABLE = 'runtime_observability.events';
12
12
  // Migration readiness tracks this projection body independently of scheduler
13
13
  // wire and schema versions. Bump when a changed trigger must be reinstalled.
14
14
  export const RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER =
15
- 'runtime-event-search-log-context-v7-terminal-progress-counts-auto-scope-log-mutex-channel-aliases';
15
+ 'runtime-event-search-log-context-v8-terminal-progress-counts-compact-first-progress-channel-aliases';
16
16
  export const RUNTIME_EVENTS_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
17
17
  const MAX_SEARCH_WINDOW_MS = RUNTIME_EVENTS_RETENTION_MS;
18
18
  const MAX_SEARCH_RESULTS = 200;
@@ -459,6 +459,7 @@ export async function getRuntimeRunLogPage(
459
459
  FROM runtime_observability.events
460
460
  WHERE runtime_scope = $1 AND org_id = $2 AND run_id = $3
461
461
  AND kind = 'log.appended' AND log_seq IS NOT NULL
462
+ AND (log_body IS NOT NULL OR message IS NOT NULL)
462
463
  AND ingested_at >= clock_timestamp() - interval '7 days'
463
464
  ), run_cursor AS (
464
465
  SELECT last_seq
@@ -469,6 +470,7 @@ export async function getRuntimeRunLogPage(
469
470
  FROM runtime_observability.events
470
471
  WHERE runtime_scope = $1 AND org_id = $2 AND run_id = $3
471
472
  AND kind = 'log.appended' AND log_seq IS NOT NULL
473
+ AND (log_body IS NOT NULL OR message IS NOT NULL)
472
474
  AND ingested_at >= clock_timestamp() - interval '7 days'
473
475
  ${pageWhere}
474
476
  ORDER BY log_seq ${pageOrder}
@@ -798,56 +800,60 @@ export async function attachRuntimeEventSearchTriggers(
798
800
  END IF;
799
801
  END $create$`);
800
802
  await client.query(`
801
- CREATE OR REPLACE FUNCTION ${schema}.search_outbox_event() RETURNS trigger
803
+ CREATE OR REPLACE FUNCTION ${schema}.project_outbox_event(observation ${schema}.outbox) RETURNS void
802
804
  LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
803
- DECLARE run_org text; play text; item jsonb; line jsonb; event_batch jsonb;
805
+ DECLARE run_org text; play text; compact_logs boolean; run_status text; item jsonb; line jsonb; event_batch jsonb;
804
806
  ordinal bigint; line_ordinal bigint; kind text; at_time timestamptz;
805
807
  doc jsonb; line_doc jsonb; line_context jsonb;
806
- eid text; existing_digest text; digest text; assigned_seq bigint;
808
+ eid text; existing_digest text; digest text; assigned_seq bigint; terminal_progress_identity boolean;
807
809
  log_channel text; log_position bigint; matched_event_id text; matched_doc jsonb;
808
810
  legacy_position_prefix text; log_alias jsonb; envelope_attempt jsonb;
809
811
  line_count bigint; fresh_event_ids text[] := ARRAY[]::text[];
810
812
  recent_log_keys jsonb := '{}'::jsonb; dedupe_key text;
811
813
  terminal_overlap_counts jsonb := '{}'::jsonb;
812
- recent_log_keys_loaded boolean := false;
814
+ recent_log_keys_loaded boolean := false; recent_log_attempt text;
813
815
  BEGIN
814
- -- Serialize log ordering without taking scheduler Run/attempt row locks.
815
- -- Completion writers can own an attempt before updating its Run.
816
- PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(NEW.run_id));
817
- SELECT org_id, play_name INTO run_org, play FROM ${schema}.runs WHERE run_id = NEW.run_id;
816
+ -- Log ordering owns a transaction-scoped mutex, independent of the
817
+ -- scheduler's Run/attempt row-lock order. In particular, old completion
818
+ -- writers lock attempt before Run; this recorder must not invert that.
819
+ PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(observation.run_id));
820
+ SELECT org_id, play_name, run_log_store = 'planet_scale', status
821
+ INTO run_org, play, compact_logs, run_status
822
+ FROM ${schema}.runs WHERE run_id = observation.run_id;
818
823
  IF run_org IS NULL OR play IS NULL THEN
819
824
  RAISE EXCEPTION 'Outbox search projection has no matching run';
820
825
  END IF;
821
- IF NEW.event_type = 'run.events' THEN
822
- event_batch := NEW.payload_json->'events';
826
+ IF observation.event_type = 'run.events' THEN
827
+ event_batch := observation.payload_json->'events';
823
828
  IF jsonb_typeof(event_batch) IS DISTINCT FROM 'array' THEN
824
829
  RAISE EXCEPTION 'Search projection requires run.events array';
825
830
  END IF;
826
- IF jsonb_array_length(event_batch) = 0 THEN
831
+ IF jsonb_array_length(event_batch) = 0
832
+ AND observation.payload_json->>'logBodyStore' IS DISTINCT FROM 'planet_scale' THEN
827
833
  RAISE EXCEPTION 'Search projection requires nonempty run.events array';
828
834
  END IF;
829
- ELSIF NEW.event_type IN ('run.completed', 'run.failed')
830
- AND NEW.payload_json ? 'terminalProgressEvents' THEN
831
- event_batch := NEW.payload_json->'terminalProgressEvents';
835
+ ELSIF observation.event_type IN ('run.completed', 'run.failed', 'runner.logs')
836
+ AND observation.payload_json ? 'terminalProgressEvents' THEN
837
+ event_batch := observation.payload_json->'terminalProgressEvents';
832
838
  IF jsonb_typeof(event_batch) IS DISTINCT FROM 'array' THEN
833
839
  RAISE EXCEPTION 'Search projection requires terminalProgressEvents array';
834
840
  END IF;
835
841
  END IF;
836
842
  -- Ordinary terminal SQL already binds the attempt in its counter
837
843
  -- segment; compact carriers also name it at the top level.
838
- envelope_attempt := coalesce(NULLIF(NEW.payload_json->'producerAttempt', 'null'::jsonb),
839
- NEW.payload_json->'terminalWorkCounterSegment'->'attempt');
844
+ envelope_attempt := coalesce(NULLIF(observation.payload_json->'producerAttempt', 'null'::jsonb),
845
+ observation.payload_json->'terminalWorkCounterSegment'->'attempt');
840
846
  -- The retained envelope has its own position space: it can contain
841
847
  -- diagnostics absent from both live delivery and ctx-only replay.
842
- IF NEW.event_type IN ('run.completed', 'run.failed')
843
- AND NEW.payload_json ? 'terminalLogLines'
848
+ IF observation.event_type IN ('run.completed', 'run.failed', 'runner.logs')
849
+ AND observation.payload_json ? 'terminalLogLines'
844
850
  AND (envelope_attempt #>> '{}') ~ '^[1-9][0-9]{0,5}$' THEN
845
851
  event_batch := coalesce(event_batch, '[]'::jsonb) || jsonb_build_array(
846
852
  jsonb_strip_nulls(jsonb_build_object(
847
- 'type', 'log.appended', 'runId', NEW.run_id, 'source', 'terminal',
853
+ 'type', 'log.appended', 'runId', observation.run_id, 'source', 'terminal',
848
854
  'producerAttempt', envelope_attempt, 'channelOffset', 0,
849
- 'lines', NEW.payload_json->'terminalLogLines',
850
- 'lineContexts', NEW.payload_json->'terminalLogContexts'
855
+ 'lines', observation.payload_json->'terminalLogLines',
856
+ 'lineContexts', observation.payload_json->'terminalLogContexts'
851
857
  ))
852
858
  );
853
859
  END IF;
@@ -860,21 +866,21 @@ export async function attachRuntimeEventSearchTriggers(
860
866
  UPDATE runtime_observability.events AS event SET search_doc = event.search_doc ||
861
867
  jsonb_build_object('logContentDigest', event.source_digest) ||
862
868
  CASE WHEN NOT (event.search_doc ? 'logPositions')
863
- AND left(event.event_id, length('log:' || NEW.run_id || ':' || (event.search_doc->>'producerAttempt') || ':')) =
864
- 'log:' || NEW.run_id || ':' || (event.search_doc->>'producerAttempt') || ':'
865
- AND substring(event.event_id FROM length('log:' || NEW.run_id || ':' || (event.search_doc->>'producerAttempt') || ':') + 1) ~ '^[0-9]{1,12}$'
869
+ AND left(event.event_id, length('log:' || observation.run_id || ':' || (event.search_doc->>'producerAttempt') || ':')) =
870
+ 'log:' || observation.run_id || ':' || (event.search_doc->>'producerAttempt') || ':'
871
+ AND substring(event.event_id FROM length('log:' || observation.run_id || ':' || (event.search_doc->>'producerAttempt') || ':') + 1) ~ '^[0-9]{1,12}$'
866
872
  THEN jsonb_build_object('logPositions', jsonb_build_object(
867
873
  CASE WHEN event.search_doc->>'source' = 'coordinator' THEN 'replay' ELSE 'worker' END,
868
- substring(event.event_id FROM length('log:' || NEW.run_id || ':' || (event.search_doc->>'producerAttempt') || ':') + 1)::bigint))
874
+ substring(event.event_id FROM length('log:' || observation.run_id || ':' || (event.search_doc->>'producerAttempt') || ':') + 1)::bigint))
869
875
  ELSE '{}'::jsonb END
870
876
  WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
871
- AND event.run_id = NEW.run_id AND event.kind = 'log.appended'
877
+ AND event.run_id = observation.run_id AND event.kind = 'log.appended'
872
878
  AND event.source_digest IS NOT NULL AND NOT (event.search_doc ? 'logContentDigest')
873
879
  AND (event.search_doc->>'producerAttempt') ~ '^[1-9][0-9]{0,5}$';
874
880
  END IF;
875
881
  FOR item, ordinal IN SELECT value, ordinality FROM jsonb_array_elements(event_batch) WITH ORDINALITY LOOP
876
882
  IF jsonb_typeof(item) IS DISTINCT FROM 'object'
877
- OR item->>'runId' IS DISTINCT FROM NEW.run_id THEN
883
+ OR item->>'runId' IS DISTINCT FROM observation.run_id THEN
878
884
  RAISE EXCEPTION 'Search projection event has invalid run identity';
879
885
  END IF;
880
886
  kind := coalesce(item->>'type', item->>'key');
@@ -889,7 +895,7 @@ export async function attachRuntimeEventSearchTriggers(
889
895
  END IF;
890
896
  at_time := CASE WHEN (item->>'occurredAt') ~ '^[0-9]{12,14}$'
891
897
  THEN to_timestamp((item->>'occurredAt')::double precision / 1000)
892
- ELSE NEW.created_at END;
898
+ ELSE observation.created_at END;
893
899
  doc := jsonb_strip_nulls(jsonb_build_object(
894
900
  'kind', kind, 'source', left(item->>'source', 64),
895
901
  'producerAttempt', CASE WHEN (item->>'producerAttempt') ~ '^[0-9]{1,6}$'
@@ -920,19 +926,22 @@ export async function attachRuntimeEventSearchTriggers(
920
926
  END IF;
921
927
  IF (item->>'source' IS DISTINCT FROM 'coordinator')
922
928
  AND NOT coalesce((item->>'channelOffset') ~ '^[0-9]{1,12}$', false) THEN
923
- IF NOT recent_log_keys_loaded THEN
929
+ IF NOT recent_log_keys_loaded OR recent_log_attempt IS DISTINCT FROM item->>'producerAttempt' THEN
924
930
  SELECT coalesce(jsonb_object_agg(recent.dedupe_key, true), '{}'::jsonb)
925
931
  INTO recent_log_keys
926
932
  FROM (
927
- SELECT encode(sha256(convert_to(btrim(event.log_body), 'UTF8')), 'hex') AS dedupe_key
933
+ SELECT coalesce(event.log_text_digest, encode(sha256(convert_to(btrim(event.log_body), 'UTF8')), 'hex')) AS dedupe_key
928
934
  FROM runtime_observability.events AS event
929
935
  WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
930
- AND event.run_id = NEW.run_id AND event.kind = 'log.appended'
931
- AND event.log_seq IS NOT NULL AND event.log_body IS NOT NULL
936
+ AND event.run_id = observation.run_id AND event.kind = 'log.appended'
937
+ AND event.log_seq IS NOT NULL AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
938
+ AND (item->>'producerAttempt' IS NULL
939
+ OR event.search_doc->>'producerAttempt' = item->>'producerAttempt')
932
940
  ORDER BY event.log_seq DESC
933
941
  LIMIT 256
934
942
  ) AS recent;
935
943
  recent_log_keys_loaded := true;
944
+ recent_log_attempt := item->>'producerAttempt';
936
945
  END IF;
937
946
  dedupe_key := encode(
938
947
  sha256(convert_to(btrim(line #>> '{}'), 'UTF8')), 'hex'
@@ -973,9 +982,9 @@ export async function attachRuntimeEventSearchTriggers(
973
982
  ELSE doc || jsonb_build_object('context', line_context) END;
974
983
  eid := CASE WHEN (item->>'channelOffset') ~ '^[0-9]{1,12}$'
975
984
  AND (item->>'producerAttempt') ~ '^[0-9]{1,6}$'
976
- THEN 'log:' || NEW.run_id || ':' || (item->>'producerAttempt') || ':' ||
985
+ THEN 'log:' || observation.run_id || ':' || (item->>'producerAttempt') || ':' ||
977
986
  ((item->>'channelOffset')::bigint + line_ordinal - 1)::text
978
- ELSE NEW.event_id || ':' || ordinal || ':' || line_ordinal END;
987
+ ELSE observation.event_id || ':' || ordinal || ':' || line_ordinal END;
979
988
  digest := CASE WHEN line_context IS NULL
980
989
  THEN encode(sha256(convert_to(line #>> '{}', 'UTF8')), 'hex')
981
990
  ELSE encode(sha256(convert_to(jsonb_build_array(line #>> '{}', line_context)::text, 'UTF8')), 'hex') END;
@@ -987,9 +996,9 @@ export async function attachRuntimeEventSearchTriggers(
987
996
  WHEN 'coordinator' THEN 'replay' WHEN 'terminal' THEN 'envelope'
988
997
  ELSE 'worker' END;
989
998
  log_position := (item->>'channelOffset')::bigint + line_ordinal - 1;
990
- legacy_position_prefix := 'log:' || NEW.run_id || ':' || (item->>'producerAttempt') || ':';
999
+ legacy_position_prefix := 'log:' || observation.run_id || ':' || (item->>'producerAttempt') || ':';
991
1000
  eid := CASE WHEN log_channel = 'worker' THEN legacy_position_prefix
992
- ELSE 'log-' || log_channel || ':' || NEW.run_id || ':' || (item->>'producerAttempt') || ':' END
1001
+ ELSE 'log-' || log_channel || ':' || observation.run_id || ':' || (item->>'producerAttempt') || ':' END
993
1002
  || log_position::text;
994
1003
  -- Older coordinator replays used the worker-shaped primary
995
1004
  -- key. Do not overwrite it when a delayed worker owns a
@@ -1000,7 +1009,7 @@ export async function attachRuntimeEventSearchTriggers(
1000
1009
  AND search_doc->>'source' = 'coordinator'
1001
1010
  AND NOT (coalesce(search_doc->'logPositions', '{}'::jsonb) ? 'worker')
1002
1011
  ) THEN
1003
- eid := 'log-worker:' || NEW.run_id || ':' || (item->>'producerAttempt') || ':' || log_position::text;
1012
+ eid := 'log-worker:' || observation.run_id || ':' || (item->>'producerAttempt') || ':' || log_position::text;
1004
1013
  END IF;
1005
1014
  log_alias := jsonb_build_object('producerAttempt', (item->>'producerAttempt')::integer,
1006
1015
  'logPositions', jsonb_build_object(log_channel, log_position));
@@ -1011,7 +1020,7 @@ export async function attachRuntimeEventSearchTriggers(
1011
1020
  INTO matched_event_id, existing_digest, matched_doc
1012
1021
  FROM runtime_observability.events AS event
1013
1022
  WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
1014
- AND event.run_id = NEW.run_id AND event.kind = 'log.appended'
1023
+ AND event.run_id = observation.run_id AND event.kind = 'log.appended'
1015
1024
  AND event.search_doc @> log_alias;
1016
1025
  IF matched_event_id IS NOT NULL THEN
1017
1026
  IF existing_digest IS DISTINCT FROM digest THEN
@@ -1026,7 +1035,7 @@ export async function attachRuntimeEventSearchTriggers(
1026
1035
  INTO matched_event_id, matched_doc
1027
1036
  FROM runtime_observability.events AS event
1028
1037
  WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
1029
- AND event.run_id = NEW.run_id AND event.kind = 'log.appended'
1038
+ AND event.run_id = observation.run_id AND event.kind = 'log.appended'
1030
1039
  AND event.search_doc @> jsonb_build_object(
1031
1040
  'producerAttempt', (item->>'producerAttempt')::integer, 'logContentDigest', digest)
1032
1041
  AND event.source_digest = digest
@@ -1043,12 +1052,24 @@ export async function attachRuntimeEventSearchTriggers(
1043
1052
  END IF;
1044
1053
  line_doc := line_doc || log_alias || jsonb_build_object('logContentDigest', digest);
1045
1054
  END IF;
1055
+ -- Serialize with terminal acceptance on the log mutex. A late
1056
+ -- retry of an accepted position is harmless; a new position
1057
+ -- after terminal closure must never recreate expired history.
1058
+ IF compact_logs AND observation.event_type = 'run.events'
1059
+ AND run_status IN ('completed', 'failed', 'cancelled')
1060
+ AND NOT EXISTS (SELECT 1 FROM runtime_observability.events
1061
+ WHERE runtime_scope = ${quoteLiteral(scope)} AND org_id = run_org
1062
+ AND event_id = eid) THEN
1063
+ RAISE EXCEPTION 'Run log ingress is closed after terminal acceptance';
1064
+ END IF;
1046
1065
  INSERT INTO runtime_observability.events (
1047
1066
  runtime_scope, org_id, event_id, occurred_at, run_id, play_name,
1048
- kind, level, message, log_body, source_digest, search_doc
1067
+ kind, level, message, log_body, source_digest, log_text_digest, search_doc
1049
1068
  ) VALUES (
1050
- ${quoteLiteral(scope)}, run_org, eid, at_time, NEW.run_id, play,
1051
- 'log.appended', NULL, left(line #>> '{}', 2048), line #>> '{}', digest, line_doc
1069
+ ${quoteLiteral(scope)}, run_org, eid, at_time, observation.run_id, play,
1070
+ 'log.appended', NULL, CASE WHEN compact_logs THEN NULL ELSE left(line #>> '{}', 2048) END,
1071
+ line #>> '{}', digest,
1072
+ encode(sha256(convert_to(btrim(line #>> '{}'), 'UTF8')), 'hex'), line_doc
1052
1073
  ) ON CONFLICT DO NOTHING;
1053
1074
  IF FOUND THEN
1054
1075
  fresh_event_ids := array_append(fresh_event_ids, eid);
@@ -1062,45 +1083,64 @@ export async function attachRuntimeEventSearchTriggers(
1062
1083
  END IF;
1063
1084
  END LOOP;
1064
1085
  ELSE
1086
+ -- Final progress may arrive in the first terminal receipt and
1087
+ -- again in completion. One attempt/ordinal owns one immutable fact.
1088
+ terminal_progress_identity := observation.event_type IN ('runner.logs', 'run.completed', 'run.failed')
1089
+ AND coalesce((observation.payload_json->>'producerAttempt') ~ '^[1-9][0-9]{0,5}$', false);
1090
+ eid := CASE WHEN terminal_progress_identity
1091
+ THEN 'terminal-progress:' || observation.run_id || ':' ||
1092
+ (observation.payload_json->>'producerAttempt') || ':' ||
1093
+ coalesce(item->>'_runtimeProgressOrdinal', ordinal::text)
1094
+ ELSE observation.event_id || ':' || ordinal END;
1095
+ digest := CASE WHEN terminal_progress_identity
1096
+ THEN encode(sha256(convert_to(item::text, 'UTF8')), 'hex') ELSE NULL END;
1065
1097
  INSERT INTO runtime_observability.events (
1066
1098
  runtime_scope, org_id, event_id, occurred_at, run_id, play_name,
1067
- kind, search_doc
1099
+ kind, source_digest, search_doc
1068
1100
  ) VALUES (
1069
- ${quoteLiteral(scope)}, run_org, NEW.event_id || ':' || ordinal,
1070
- at_time, NEW.run_id, play, kind, doc
1071
- );
1101
+ ${quoteLiteral(scope)}, run_org, eid,
1102
+ at_time, observation.run_id, play, kind, digest, doc
1103
+ ) ON CONFLICT DO NOTHING;
1104
+ IF NOT FOUND AND terminal_progress_identity THEN
1105
+ SELECT source_digest INTO existing_digest FROM runtime_observability.events
1106
+ WHERE runtime_scope = ${quoteLiteral(scope)} AND org_id = run_org AND event_id = eid;
1107
+ IF existing_digest IS DISTINCT FROM digest THEN
1108
+ RAISE EXCEPTION 'Progress event identity changed its content';
1109
+ END IF;
1110
+ END IF;
1072
1111
  END IF;
1073
1112
  END LOOP;
1074
1113
  END IF;
1075
- IF NEW.event_type IN (
1114
+ IF observation.event_type IN (
1076
1115
  'run.created', 'run.started', 'run.waiting', 'run.resumed',
1077
1116
  'run.completed', 'run.failed', 'run.cancelled'
1078
- ) AND NOT (NEW.event_type IN ('run.waiting', 'run.resumed')
1079
- AND NEW.payload_json->>'kind' = 'detached_runner') THEN
1117
+ ) AND NOT (observation.event_type IN ('run.waiting', 'run.resumed')
1118
+ AND observation.payload_json->>'kind' = 'detached_runner') THEN
1080
1119
  INSERT INTO runtime_observability.events (
1081
1120
  runtime_scope, org_id, event_id, occurred_at, run_id, play_name,
1082
1121
  kind, search_doc
1083
1122
  ) VALUES (
1084
- ${quoteLiteral(scope)}, run_org, NEW.event_id, NEW.created_at,
1085
- NEW.run_id, play, NEW.event_type, jsonb_build_object('kind', NEW.event_type, 'source', 'system')
1086
- );
1123
+ ${quoteLiteral(scope)}, run_org, observation.event_id, observation.created_at,
1124
+ observation.run_id, play, observation.event_type, jsonb_build_object('kind', observation.event_type, 'source', 'system')
1125
+ ) ON CONFLICT DO NOTHING;
1126
+ END IF;
1127
+ IF observation.event_type IN ('run.completed', 'run.failed', 'runner.logs') THEN
1087
1128
  -- Detached runners deliver their retained ctx.log tail in the terminal
1088
1129
  -- outbox fact rather than a run.events batch. Project it from that
1089
1130
  -- authoritative write so search and the existing log stream agree.
1090
- IF NEW.event_type IN ('run.completed', 'run.failed')
1091
- AND NEW.payload_json ? 'terminalLogContexts'
1092
- AND NOT (NEW.payload_json ? 'terminalLogLines') THEN
1131
+ IF observation.payload_json ? 'terminalLogContexts'
1132
+ AND NOT (observation.payload_json ? 'terminalLogLines') THEN
1093
1133
  RAISE EXCEPTION 'Search projection requires terminalLogLines for terminalLogContexts';
1094
1134
  END IF;
1095
- IF NEW.event_type IN ('run.completed', 'run.failed')
1096
- AND NEW.payload_json ? 'terminalLogLines'
1135
+ IF observation.event_type IN ('run.completed', 'run.failed', 'runner.logs')
1136
+ AND observation.payload_json ? 'terminalLogLines'
1097
1137
  AND NOT coalesce((envelope_attempt #>> '{}') ~ '^[1-9][0-9]{0,5}$', false) THEN
1098
- IF jsonb_typeof(NEW.payload_json->'terminalLogLines') IS DISTINCT FROM 'array' THEN
1138
+ IF jsonb_typeof(observation.payload_json->'terminalLogLines') IS DISTINCT FROM 'array' THEN
1099
1139
  RAISE EXCEPTION 'Search projection requires terminalLogLines array';
1100
1140
  END IF;
1101
- IF NEW.payload_json ? 'terminalLogContexts'
1102
- AND (jsonb_typeof(NEW.payload_json->'terminalLogContexts') IS DISTINCT FROM 'array'
1103
- OR jsonb_array_length(NEW.payload_json->'terminalLogContexts') <> jsonb_array_length(NEW.payload_json->'terminalLogLines')) THEN
1141
+ IF observation.payload_json ? 'terminalLogContexts'
1142
+ AND (jsonb_typeof(observation.payload_json->'terminalLogContexts') IS DISTINCT FROM 'array'
1143
+ OR jsonb_array_length(observation.payload_json->'terminalLogContexts') <> jsonb_array_length(observation.payload_json->'terminalLogLines')) THEN
1104
1144
  RAISE EXCEPTION 'Search projection requires terminalLogContexts aligned to terminalLogLines';
1105
1145
  END IF;
1106
1146
  -- Count matching bodies already projected, including progress lines
@@ -1115,30 +1155,34 @@ export async function attachRuntimeEventSearchTriggers(
1115
1155
  FROM (
1116
1156
  SELECT candidate.dedupe_key, count(*) AS occurrences
1117
1157
  FROM (
1118
- SELECT encode(sha256(convert_to(btrim(recent.log_body), 'UTF8')), 'hex') AS dedupe_key
1158
+ SELECT coalesce(recent.log_text_digest, encode(sha256(convert_to(btrim(recent.log_body), 'UTF8')), 'hex')) AS dedupe_key
1119
1159
  FROM (
1120
- SELECT event.log_body
1160
+ SELECT event.log_body, event.log_text_digest
1121
1161
  FROM runtime_observability.events AS event
1122
1162
  WHERE event.runtime_scope = ${quoteLiteral(scope)}
1123
- AND event.org_id = run_org AND event.run_id = NEW.run_id
1163
+ AND event.org_id = run_org AND event.run_id = observation.run_id
1124
1164
  AND event.kind = 'log.appended'
1125
- AND event.log_seq IS NOT NULL AND event.log_body IS NOT NULL
1165
+ AND (observation.payload_json->>'producerAttempt' IS NULL
1166
+ OR event.search_doc->>'producerAttempt' = observation.payload_json->>'producerAttempt')
1167
+ AND event.log_seq IS NOT NULL AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
1126
1168
  ORDER BY event.log_seq DESC
1127
1169
  LIMIT 256
1128
1170
  ) AS recent
1129
1171
  UNION ALL
1130
- SELECT encode(sha256(convert_to(btrim(event.log_body), 'UTF8')), 'hex')
1172
+ SELECT coalesce(event.log_text_digest, encode(sha256(convert_to(btrim(event.log_body), 'UTF8')), 'hex'))
1131
1173
  FROM runtime_observability.events AS event
1132
1174
  WHERE event.runtime_scope = ${quoteLiteral(scope)}
1133
- AND event.org_id = run_org AND event.run_id = NEW.run_id
1134
- AND event.kind = 'log.appended' AND event.log_body IS NOT NULL
1175
+ AND event.org_id = run_org AND event.run_id = observation.run_id
1176
+ AND event.kind = 'log.appended' AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
1135
1177
  AND event.event_id = ANY(fresh_event_ids)
1178
+ AND (observation.payload_json->>'producerAttempt' IS NULL
1179
+ OR event.search_doc->>'producerAttempt' = observation.payload_json->>'producerAttempt')
1136
1180
  ) AS candidate
1137
1181
  GROUP BY candidate.dedupe_key
1138
1182
  ) AS counted;
1139
1183
  FOR line, line_ordinal IN
1140
1184
  SELECT value, ordinality
1141
- FROM jsonb_array_elements(NEW.payload_json->'terminalLogLines') WITH ORDINALITY
1185
+ FROM jsonb_array_elements(observation.payload_json->'terminalLogLines') WITH ORDINALITY
1142
1186
  LOOP
1143
1187
  IF jsonb_typeof(line) IS DISTINCT FROM 'string' THEN
1144
1188
  RAISE EXCEPTION 'Search projection requires terminal log strings';
@@ -1154,8 +1198,8 @@ export async function attachRuntimeEventSearchTriggers(
1154
1198
  CONTINUE;
1155
1199
  END IF;
1156
1200
  line_context := NULL;
1157
- IF NEW.payload_json ? 'terminalLogContexts' THEN
1158
- line_context := NEW.payload_json->'terminalLogContexts'->((line_ordinal - 1)::int);
1201
+ IF observation.payload_json ? 'terminalLogContexts' THEN
1202
+ line_context := observation.payload_json->'terminalLogContexts'->((line_ordinal - 1)::int);
1159
1203
  IF jsonb_typeof(line_context) = 'null' THEN
1160
1204
  line_context := NULL;
1161
1205
  ELSIF jsonb_typeof(line_context) IS DISTINCT FROM 'object' THEN
@@ -1179,28 +1223,32 @@ export async function attachRuntimeEventSearchTriggers(
1179
1223
  RAISE EXCEPTION 'Search projection terminal log context has an unsupported key or value';
1180
1224
  END IF;
1181
1225
  END IF;
1182
- line_doc := jsonb_build_object('kind', 'log.appended', 'source', 'terminal');
1226
+ line_doc := jsonb_strip_nulls(jsonb_build_object(
1227
+ 'kind', 'log.appended', 'source', 'terminal',
1228
+ 'producerAttempt', observation.payload_json->'producerAttempt'
1229
+ ));
1183
1230
  IF line_context IS NOT NULL THEN
1184
1231
  line_doc := line_doc || jsonb_build_object('context', line_context);
1185
1232
  END IF;
1186
1233
  INSERT INTO runtime_observability.events (
1187
1234
  runtime_scope, org_id, event_id, occurred_at, run_id, play_name,
1188
- kind, level, message, log_body, source_digest, search_doc
1235
+ kind, level, message, log_body, source_digest, log_text_digest, search_doc
1189
1236
  ) VALUES (
1190
- ${quoteLiteral(scope)}, run_org, NEW.event_id || ':log:' || line_ordinal,
1191
- NEW.created_at, NEW.run_id, play, 'log.appended', NULL,
1192
- left(regexp_replace(line #>> '{}',
1193
- '^' || chr(1) || 'prov:[^' || chr(1) || ']*' || chr(1), ''), 2048),
1237
+ ${quoteLiteral(scope)}, run_org, observation.event_id || ':log:' || line_ordinal,
1238
+ observation.created_at, observation.run_id, play, 'log.appended', NULL,
1239
+ CASE WHEN compact_logs THEN NULL ELSE left(regexp_replace(line #>> '{}',
1240
+ '^' || chr(1) || 'prov:[^' || chr(1) || ']*' || chr(1), ''), 2048) END,
1194
1241
  line #>> '{}',
1195
1242
  CASE WHEN line_context IS NULL
1196
1243
  THEN encode(sha256(convert_to(line #>> '{}', 'UTF8')), 'hex')
1197
1244
  ELSE encode(sha256(convert_to(jsonb_build_array(line #>> '{}', line_context)::text, 'UTF8')), 'hex') END,
1245
+ encode(sha256(convert_to(btrim(line #>> '{}'), 'UTF8')), 'hex'),
1198
1246
  line_doc
1199
1247
  ) ON CONFLICT DO NOTHING;
1200
1248
  IF FOUND THEN
1201
1249
  fresh_event_ids := array_append(
1202
1250
  fresh_event_ids,
1203
- NEW.event_id || ':log:' || line_ordinal
1251
+ observation.event_id || ':log:' || line_ordinal
1204
1252
  );
1205
1253
  END IF;
1206
1254
  END LOOP;
@@ -1210,7 +1258,7 @@ export async function attachRuntimeEventSearchTriggers(
1210
1258
  IF line_count > 0 THEN
1211
1259
  INSERT INTO runtime_observability.run_log_cursors (
1212
1260
  runtime_scope, org_id, run_id, last_seq
1213
- ) VALUES (${quoteLiteral(scope)}, run_org, NEW.run_id, line_count)
1261
+ ) VALUES (${quoteLiteral(scope)}, run_org, observation.run_id, line_count)
1214
1262
  ON CONFLICT (runtime_scope, org_id, run_id)
1215
1263
  DO UPDATE SET last_seq = runtime_observability.run_log_cursors.last_seq + EXCLUDED.last_seq,
1216
1264
  updated_at = clock_timestamp()
@@ -1222,20 +1270,168 @@ export async function attachRuntimeEventSearchTriggers(
1222
1270
  AND event.org_id = run_org
1223
1271
  AND event.event_id = fresh.event_id;
1224
1272
  END IF;
1273
+ RETURN;
1274
+ END
1275
+ $function$
1276
+ `);
1277
+ await client.query(`
1278
+ CREATE OR REPLACE FUNCTION ${schema}.without_log_events(batch jsonb) RETURNS jsonb
1279
+ LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $function$
1280
+ SELECT coalesce(jsonb_agg(item.value ORDER BY item.ordinality), '[]'::jsonb)
1281
+ FROM jsonb_array_elements(batch) WITH ORDINALITY AS item(value, ordinality)
1282
+ WHERE coalesce(item.value->>'type', item.value->>'key') IS DISTINCT FROM 'log.appended'
1283
+ $function$
1284
+ `);
1285
+ await client.query(`
1286
+ CREATE OR REPLACE FUNCTION ${schema}.without_runtime_log_tail(value jsonb) RETURNS jsonb
1287
+ LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $function$
1288
+ SELECT CASE
1289
+ -- This reserved metadata shape is produced by the runtime. Authored
1290
+ -- output/checkpoint strings and unrelated metadata are left intact.
1291
+ WHEN jsonb_typeof(value#>'{_metadata,runLogTail,tail}') = 'array'
1292
+ AND jsonb_typeof(value#>'{_metadata,runLogTail,totalLogCount}') = 'number'
1293
+ THEN value #- '{_metadata,runLogTail}' ELSE value END
1294
+ $function$
1295
+ `);
1296
+ await client.query(`
1297
+ CREATE OR REPLACE FUNCTION ${schema}.compact_runner_log_envelope(value jsonb) RETURNS jsonb
1298
+ LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $function$
1299
+ WITH envelope AS (
1300
+ SELECT CASE WHEN jsonb_typeof(value) = 'object' THEN
1301
+ (value - 'logContexts') || CASE WHEN value ? 'logs'
1302
+ THEN jsonb_build_object('logs', '[]'::jsonb) ELSE '{}'::jsonb END
1303
+ ELSE value END AS result
1304
+ )
1305
+ SELECT CASE WHEN result ? 'output' THEN jsonb_set(result, '{output}',
1306
+ ${schema}.without_runtime_log_tail(result->'output')) ELSE result END
1307
+ FROM envelope
1308
+ $function$
1309
+ `);
1310
+ await client.query(`
1311
+ CREATE OR REPLACE FUNCTION ${schema}.search_outbox_event() RETURNS trigger
1312
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
1313
+ DECLARE compact_logs boolean;
1314
+ BEGIN
1315
+ -- Project the original validated observation before replacing its
1316
+ -- transport body. Failure rolls back the outbox and canonical rows.
1317
+ PERFORM ${schema}.project_outbox_event(NEW);
1318
+ SELECT run_log_store = 'planet_scale' INTO compact_logs
1319
+ FROM ${schema}.runs WHERE run_id = NEW.run_id;
1320
+ IF compact_logs THEN
1321
+ NEW.payload_json := NEW.payload_json - 'terminalLogLines' - 'terminalLogContexts';
1322
+ IF NEW.event_type = 'run.events' THEN
1323
+ NEW.payload_json := jsonb_set(NEW.payload_json, '{events}',
1324
+ ${schema}.without_log_events(NEW.payload_json->'events'))
1325
+ || jsonb_build_object('logBodyStore', 'planet_scale');
1326
+ END IF;
1327
+ IF NEW.payload_json ? 'terminalProgressEvents' THEN
1328
+ NEW.payload_json := jsonb_set(NEW.payload_json, '{terminalProgressEvents}',
1329
+ ${schema}.without_log_events(NEW.payload_json->'terminalProgressEvents'));
1330
+ END IF;
1331
+ IF NEW.event_type = 'run.failed' AND NEW.payload_json ? 'result' THEN
1332
+ NEW.payload_json := jsonb_set(NEW.payload_json, '{result}',
1333
+ ${schema}.compact_runner_log_envelope(NEW.payload_json->'result'));
1334
+ END IF;
1335
+ IF NEW.event_type = 'run.completed' AND NEW.payload_json ? 'output' THEN
1336
+ NEW.payload_json := jsonb_set(NEW.payload_json, '{output}',
1337
+ ${schema}.without_runtime_log_tail(NEW.payload_json->'output'));
1338
+ END IF;
1339
+ END IF;
1225
1340
  RETURN NEW;
1226
1341
  END
1227
1342
  $function$
1228
1343
  `);
1229
- await client.query(
1230
- `COMMENT ON FUNCTION ${schema}.search_outbox_event() IS ${quoteLiteral(RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER)}`,
1231
- );
1344
+ await client.query(`
1345
+ CREATE OR REPLACE FUNCTION ${schema}.accept_runner_log_payload() RETURNS trigger
1346
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
1347
+ DECLARE observation ${schema}.outbox; compact_logs boolean;
1348
+ BEGIN
1349
+ SELECT run_log_store = 'planet_scale' INTO compact_logs
1350
+ FROM ${schema}.runs WHERE run_id = NEW.run_id;
1351
+ IF NOT coalesce(compact_logs, false) OR NEW.runner_log_payload_json IS NULL THEN
1352
+ RETURN NEW;
1353
+ END IF;
1354
+ IF OLD.runner_result_json IS NULL THEN
1355
+ NEW.runner_result_digest := encode(
1356
+ sha256(convert_to(NEW.runner_result_json::text, 'UTF8')), 'hex');
1357
+ observation.event_id := 'runner-terminal-logs:' || NEW.run_id || ':' || NEW.attempt;
1358
+ observation.run_id := NEW.run_id;
1359
+ observation.event_type := 'runner.logs';
1360
+ observation.created_at := coalesce(NEW.runner_terminal_received_at, clock_timestamp());
1361
+ observation.payload_json := jsonb_set(NEW.runner_log_payload_json,
1362
+ '{terminalProgressEvents}', (
1363
+ SELECT coalesce(jsonb_agg(
1364
+ CASE WHEN event->>'type' = 'log.appended' THEN event
1365
+ ELSE event || jsonb_build_object('_runtimeProgressOrdinal', position) END
1366
+ ORDER BY position), '[]'::jsonb)
1367
+ FROM jsonb_array_elements(CASE
1368
+ WHEN jsonb_typeof(NEW.runner_log_payload_json->'terminalProgressEvents') = 'array'
1369
+ THEN NEW.runner_log_payload_json->'terminalProgressEvents'
1370
+ ELSE '[]'::jsonb END) WITH ORDINALITY AS progress(event, position)
1371
+ ));
1372
+ PERFORM ${schema}.project_outbox_event(observation);
1373
+ NEW.runner_result_json := ${schema}.compact_runner_log_envelope(NEW.runner_result_json);
1374
+ IF jsonb_array_length(observation.payload_json->'recoveredRuntimeDiagnostics') > 0
1375
+ AND (NOT (NEW.runner_result_json ? 'runtimeDiagnostics')
1376
+ OR jsonb_typeof(NEW.runner_result_json->'runtimeDiagnostics') = 'array') THEN
1377
+ NEW.runner_result_json := jsonb_set(NEW.runner_result_json, '{runtimeDiagnostics}',
1378
+ coalesce(NEW.runner_result_json->'runtimeDiagnostics', '[]'::jsonb)
1379
+ || (observation.payload_json->'recoveredRuntimeDiagnostics'));
1380
+ END IF;
1381
+ -- Retain only the first accepted non-log facts. Gateway retries
1382
+ -- regenerate transport timestamps, so completion must reuse these
1383
+ -- facts rather than compare newly generated progress with the first.
1384
+ NEW.runner_log_payload_json := jsonb_build_object(
1385
+ 'terminalProgressEvents', ${schema}.without_log_events(
1386
+ observation.payload_json->'terminalProgressEvents'));
1387
+ ELSE
1388
+ NEW.runner_log_payload_json := OLD.runner_log_payload_json;
1389
+ END IF;
1390
+ RETURN NEW;
1391
+ END
1392
+ $function$
1393
+ `);
1394
+ await client.query(`
1395
+ CREATE OR REPLACE FUNCTION ${schema}.compact_run_log_result() RETURNS trigger
1396
+ LANGUAGE plpgsql SET search_path = pg_catalog AS $function$
1397
+ BEGIN
1398
+ IF NEW.run_log_store = 'planet_scale' THEN
1399
+ IF NEW.status = 'failed' THEN
1400
+ NEW.terminal_result_json := ${schema}.compact_runner_log_envelope(NEW.terminal_result_json);
1401
+ ELSIF NEW.status = 'completed' THEN
1402
+ NEW.terminal_result_json := ${schema}.without_runtime_log_tail(NEW.terminal_result_json);
1403
+ END IF;
1404
+ END IF;
1405
+ RETURN NEW;
1406
+ END
1407
+ $function$
1408
+ `);
1409
+ // Replacing AFTER with BEFORE is one DDL transaction, so live inserts cannot
1410
+ // observe an interval with no projector. All older unmarked runs stay full.
1232
1411
  await client.query(`DO $create$ BEGIN
1233
- IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.outbox'::regclass
1234
- AND tgname = 'runtime_outbox_event_v1') THEN
1235
- CREATE TRIGGER runtime_outbox_event_v1 AFTER INSERT ON ${schema}.outbox
1236
- FOR EACH ROW EXECUTE FUNCTION ${schema}.search_outbox_event();
1412
+ DROP TRIGGER IF EXISTS runtime_outbox_event_v1 ON ${schema}.outbox;
1413
+ CREATE TRIGGER runtime_outbox_event_v1 BEFORE INSERT ON ${schema}.outbox
1414
+ FOR EACH ROW EXECUTE FUNCTION ${schema}.search_outbox_event();
1415
+ IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.attempts'::regclass
1416
+ AND tgname = 'runtime_attempt_log_acceptance') THEN
1417
+ CREATE TRIGGER runtime_attempt_log_acceptance BEFORE UPDATE OF runner_result_json ON ${schema}.attempts
1418
+ FOR EACH ROW EXECUTE FUNCTION ${schema}.accept_runner_log_payload();
1419
+ END IF;
1420
+ IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.runs'::regclass
1421
+ AND tgname = 'runtime_run_log_result') THEN
1422
+ CREATE TRIGGER runtime_run_log_result BEFORE UPDATE OF terminal_result_json ON ${schema}.runs
1423
+ FOR EACH ROW EXECUTE FUNCTION ${schema}.compact_run_log_result();
1237
1424
  END IF;
1238
1425
  END $create$`);
1426
+ await client.query(`REVOKE ALL ON FUNCTION ${schema}.project_outbox_event(${schema}.outbox),
1427
+ ${schema}.accept_runner_log_payload(), ${schema}.search_outbox_event(),
1428
+ ${schema}.compact_run_log_result(), ${schema}.without_log_events(jsonb),
1429
+ ${schema}.compact_runner_log_envelope(jsonb), ${schema}.without_runtime_log_tail(jsonb)
1430
+ FROM PUBLIC`);
1431
+ // Publish readiness last, after all acceptance/compaction functions exist.
1432
+ await client.query(
1433
+ `COMMENT ON FUNCTION ${schema}.search_outbox_event() IS ${quoteLiteral(RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER)}`,
1434
+ );
1239
1435
  }
1240
1436
 
1241
1437
  export async function hasRuntimeEventSearchShape(
@@ -1252,6 +1448,9 @@ export async function hasRuntimeEventSearchShape(
1252
1448
  AND EXISTS (SELECT 1 FROM pg_attribute
1253
1449
  WHERE attrelid = to_regclass('runtime_observability.events')
1254
1450
  AND attname = 'log_body' AND NOT attisdropped)
1451
+ AND EXISTS (SELECT 1 FROM pg_attribute
1452
+ WHERE attrelid = to_regclass('runtime_observability.events')
1453
+ AND attname = 'log_text_digest' AND NOT attisdropped)
1255
1454
  AND EXISTS (SELECT 1 FROM pg_class
1256
1455
  WHERE oid = to_regclass('runtime_observability.events') AND relrowsecurity)
1257
1456
  AND EXISTS (SELECT 1 FROM pg_class
@@ -1259,7 +1458,12 @@ export async function hasRuntimeEventSearchShape(
1259
1458
  AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.receipts'::regclass
1260
1459
  AND tgname = 'runtime_receipt_event_v1' AND NOT tgisinternal)
1261
1460
  AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.outbox'::regclass
1262
- AND tgname = 'runtime_outbox_event_v1' AND NOT tgisinternal)
1461
+ AND tgname = 'runtime_outbox_event_v1' AND NOT tgisinternal
1462
+ AND tgenabled <> 'D' AND (tgtype & 2) = 2)
1463
+ AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.attempts'::regclass
1464
+ AND tgname = 'runtime_attempt_log_acceptance' AND NOT tgisinternal AND tgenabled <> 'D')
1465
+ AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.runs'::regclass
1466
+ AND tgname = 'runtime_run_log_result' AND NOT tgisinternal AND tgenabled <> 'D')
1263
1467
  AND EXISTS (SELECT 1 FROM pg_proc AS proc
1264
1468
  JOIN pg_namespace AS namespace ON namespace.oid = proc.pronamespace
1265
1469
  WHERE namespace.nspname = $1 AND proc.proname = 'search_outbox_event'
@@ -1271,6 +1475,83 @@ export async function hasRuntimeEventSearchShape(
1271
1475
  return result.rows[0]?.ready === true;
1272
1476
  }
1273
1477
 
1478
+ const RUNTIME_EVENT_BODY_RETENTION_PREDICATE = `kind <> 'log.appended' OR log_body IS NOT NULL
1479
+ OR message IS NOT NULL OR search_doc ? 'context'`;
1480
+
1481
+ // pg_get_expr adds grouping and text casts. Preserve quoted literals exactly;
1482
+ // this known OR-only predicate rejects other operators, keys and literal values.
1483
+ function normalizedRetentionPredicate(value: string): string {
1484
+ return (value.match(/'(?:''|[^'])*'|::text\b|[()\s]+|./g) ?? [])
1485
+ .filter((token) => !/^(?:::text|[()\s]+)$/.test(token))
1486
+ .join('');
1487
+ }
1488
+
1489
+ /** Operator-only concurrent index build; never part of runtime bootstrap. */
1490
+ export async function prepareRuntimeEventRetentionIndex(
1491
+ client: PostgresSchedulerQueryClient,
1492
+ ): Promise<void> {
1493
+ type IndexShape = {
1494
+ valid: boolean;
1495
+ ready: boolean;
1496
+ is_unique: boolean;
1497
+ table_matches: boolean;
1498
+ key_count: number;
1499
+ attribute_count: number;
1500
+ method: string;
1501
+ key1: string | null;
1502
+ key2: string | null;
1503
+ predicate: string | null;
1504
+ };
1505
+ const inspect = () =>
1506
+ client.query<IndexShape>(
1507
+ `SELECT index.indisvalid AS valid, index.indisready AS ready, index.indisunique AS is_unique,
1508
+ index.indrelid = to_regclass('runtime_observability.events') AS table_matches,
1509
+ index.indnkeyatts AS key_count, index.indnatts AS attribute_count,
1510
+ method.amname AS method,
1511
+ pg_get_indexdef(index.indexrelid, 1, true) AS key1,
1512
+ pg_get_indexdef(index.indexrelid, 2, true) AS key2,
1513
+ pg_get_expr(index.indpred, index.indrelid) AS predicate
1514
+ FROM pg_index AS index
1515
+ JOIN pg_class AS relation ON relation.oid = index.indexrelid
1516
+ JOIN pg_am AS method ON method.oid = relation.relam
1517
+ WHERE index.indexrelid = to_regclass('runtime_observability.runtime_events_body_retention_idx')`,
1518
+ );
1519
+ const assertReady = (shape: IndexShape): void => {
1520
+ if (!shape.valid || !shape.ready) {
1521
+ throw new Error(
1522
+ 'Runtime body retention index is invalid or incomplete. Inspect the failed build; repair it explicitly before retrying.',
1523
+ );
1524
+ }
1525
+ if (
1526
+ !shape.table_matches ||
1527
+ shape.is_unique ||
1528
+ shape.key_count !== 2 ||
1529
+ shape.attribute_count !== 2 ||
1530
+ shape.method !== 'btree' ||
1531
+ shape.key1 !== 'ingested_at' ||
1532
+ shape.key2 !== 'event_id' ||
1533
+ shape.predicate === null ||
1534
+ normalizedRetentionPredicate(shape.predicate) !==
1535
+ normalizedRetentionPredicate(RUNTIME_EVENT_BODY_RETENTION_PREDICATE)
1536
+ ) {
1537
+ throw new Error(
1538
+ 'Runtime body retention index definition differs from its expected table, uniqueness, keys or predicate. Inspect and repair it explicitly.',
1539
+ );
1540
+ }
1541
+ };
1542
+ const existing = (await inspect()).rows[0];
1543
+ if (existing) assertReady(existing);
1544
+ await client.query(`CREATE INDEX CONCURRENTLY IF NOT EXISTS runtime_events_body_retention_idx
1545
+ ON runtime_observability.events (ingested_at, event_id)
1546
+ WHERE ${RUNTIME_EVENT_BODY_RETENTION_PREDICATE}`);
1547
+ const built = (await inspect()).rows[0];
1548
+ if (!built)
1549
+ throw new Error(
1550
+ 'Runtime body retention index was not found after preparation.',
1551
+ );
1552
+ assertReady(built);
1553
+ }
1554
+
1274
1555
  /** One bounded operator-owned TTL pass. Never called by a customer request. */
1275
1556
  export async function pruneRuntimeEvents(
1276
1557
  client: PostgresSchedulerQueryClient,
@@ -1295,16 +1576,32 @@ export async function pruneRuntimeEvents(
1295
1576
  `WITH candidates AS (
1296
1577
  SELECT ctid FROM runtime_observability.events
1297
1578
  WHERE ingested_at < $1::timestamptz
1579
+ AND (kind <> 'log.appended' OR log_body IS NOT NULL
1580
+ OR message IS NOT NULL OR search_doc ? 'context')
1298
1581
  ORDER BY ingested_at, event_id
1299
1582
  LIMIT $2::integer FOR UPDATE SKIP LOCKED
1583
+ ), scrubbed AS (
1584
+ UPDATE runtime_observability.events AS event
1585
+ SET log_text_digest = coalesce(event.log_text_digest,
1586
+ encode(sha256(convert_to(btrim(coalesce(event.log_body, event.message)), 'UTF8')), 'hex')),
1587
+ log_body = NULL, message = NULL,
1588
+ search_doc = jsonb_strip_nulls(jsonb_build_object(
1589
+ 'kind', 'log.appended', 'source', event.search_doc->'source',
1590
+ 'producerAttempt', event.search_doc->'producerAttempt',
1591
+ 'logPositions', event.search_doc->'logPositions',
1592
+ 'logContentDigest', event.search_doc->'logContentDigest'))
1593
+ FROM candidates WHERE event.ctid = candidates.ctid AND event.kind = 'log.appended'
1594
+ RETURNING event.event_id
1595
+ ), deleted AS (
1596
+ DELETE FROM runtime_observability.events AS event USING candidates
1597
+ WHERE event.ctid = candidates.ctid AND event.kind <> 'log.appended'
1598
+ RETURNING event.event_id
1300
1599
  )
1301
- DELETE FROM runtime_observability.events AS event USING candidates
1302
- WHERE event.ctid = candidates.ctid
1303
- RETURNING event.event_id`,
1600
+ SELECT event_id FROM scrubbed UNION ALL SELECT event_id FROM deleted`,
1304
1601
  [input.before.toISOString(), input.limit],
1305
1602
  );
1306
- // Keep the small per-run high-water marks after log bodies expire. A delayed
1307
- // accepted batch must not restart that run at sequence 1 and break cursors.
1603
+ // Keep identity/digests until fenced ingress and pending delivery drain. A
1604
+ // high-water mark alone cannot recognize a resent producer offset.
1308
1605
  return result.rows.length;
1309
1606
  }
1310
1607
 
@@ -1326,10 +1623,14 @@ export async function inspectRuntimeEventRetention(
1326
1623
  }>(
1327
1624
  `WITH oldest AS (
1328
1625
  SELECT ingested_at FROM runtime_observability.events
1626
+ WHERE kind <> 'log.appended' OR log_body IS NOT NULL
1627
+ OR message IS NOT NULL OR search_doc ? 'context'
1329
1628
  ORDER BY ingested_at, event_id LIMIT 1
1330
1629
  ), bounded AS (
1331
1630
  SELECT 1 FROM runtime_observability.events
1332
1631
  WHERE ingested_at < $1::timestamptz
1632
+ AND (kind <> 'log.appended' OR log_body IS NOT NULL
1633
+ OR message IS NOT NULL OR search_doc ? 'context')
1333
1634
  ORDER BY ingested_at, event_id LIMIT 10001
1334
1635
  )
1335
1636
  SELECT (SELECT ingested_at FROM oldest) AS oldest,