deepline 0.3.289 → 0.3.291

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +8 -6
  2. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  3. package/dist/bundling-sources/sdk/src/runs/overview.ts +16 -6
  4. package/dist/bundling-sources/sdk/src/types.ts +21 -2
  5. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +41 -11
  6. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +317 -63
  7. package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +10 -2
  8. package/dist/bundling-sources/shared_libs/play-runtime/fixture-behavior.ts +1 -0
  9. package/dist/bundling-sources/shared_libs/play-runtime/governor/rate-state-backend.ts +3 -0
  10. package/dist/bundling-sources/shared_libs/play-runtime/launch-identity.ts +1 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/log-context.ts +36 -0
  12. package/dist/bundling-sources/shared_libs/play-runtime/log-provenance.ts +19 -1
  13. package/dist/bundling-sources/shared_libs/play-runtime/play-runs/attempts/worker-terminal-projection.ts +6 -5
  14. package/dist/bundling-sources/shared_libs/play-runtime/play-runtime-batching-registry.ts +2 -0
  15. package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +12 -1
  16. package/dist/bundling-sources/shared_libs/play-runtime/run-event-catalog.ts +2 -0
  17. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +74 -33
  18. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +73 -3
  19. package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +39 -2
  20. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +72 -9
  21. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +2 -3
  22. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +77 -10
  23. package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +1 -1
  24. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +2 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-pool-options.ts +145 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +17 -9
  27. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +8 -139
  28. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +19 -5
  29. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +176 -18
  30. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-rate-state.ts +7 -5
  31. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +1404 -478
  32. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +55 -25
  33. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-dispatch-authority.ts +97 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +34 -15
  35. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +558 -57
  36. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +10 -1
  37. package/dist/bundling-sources/shared_libs/play-runtime/step-progress.ts +6 -1
  38. package/dist/bundling-sources/shared_libs/play-runtime/test-admission-batching.ts +103 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/tool-call/runtime-step-receipts-adapter.ts +22 -5
  40. package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +35 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/tool-result.ts +68 -8
  42. package/dist/bundling-sources/shared_libs/play-runtime/worker-api-types.ts +2 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/zerobounce-batching.ts +75 -28
  44. package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +2 -0
  45. package/dist/cli/index.js +195 -582
  46. package/dist/cli/index.mjs +195 -582
  47. package/dist/{client-FR_u0dxT.d.ts → client-Ct4jmvAg.d.ts} +29 -8
  48. package/dist/{client-phK1Ixcl.d.mts → client-Ct9GKrwu.d.mts} +29 -8
  49. package/dist/{compiler-manifest-9SQtUuKb.d.mts → compiler-manifest-qtTNYvBl.d.mts} +2 -0
  50. package/dist/{compiler-manifest-9SQtUuKb.d.ts → compiler-manifest-qtTNYvBl.d.ts} +2 -0
  51. package/dist/index.d.mts +4 -4
  52. package/dist/index.d.ts +4 -4
  53. package/dist/index.js +14 -5
  54. package/dist/index.mjs +14 -5
  55. package/dist/install-integrity.json +7 -4
  56. package/dist/plays/bundle-play-file.d.mts +2 -2
  57. package/dist/plays/bundle-play-file.d.ts +2 -2
  58. package/dist/plays/run-request.d.mts +2 -2
  59. package/dist/plays/run-request.d.ts +2 -2
  60. package/dist/release.d.mts +1 -1
  61. package/dist/release.d.ts +1 -1
  62. package/dist/release.js +1 -1
  63. package/dist/release.mjs +1 -1
  64. package/package.json +1 -1
@@ -1,9 +1,14 @@
1
1
  /**
2
2
  * The customer-visible, append-only search projection of durable Play facts.
3
3
  *
4
- * Outbox and receipt triggers write small rows in their authoritative SQL
5
- * transaction. They never deliver work, replay a Play, or copy receipt output.
6
- * The API may read only through fixed, tenant-scoped queries in this module.
4
+ * After reader-first cutover, triggers persist raw input in the existing
5
+ * outbox. The leased dispatcher projects and compacts it before delivery.
6
+ * PlanetScale runtime_observability.events is the single log/event/receipt
7
+ * discovery surface. JSONPath runs natively against its indexed search_doc;
8
+ * exact identities use indexed columns on that same table. Full saved results
9
+ * resolve through the scoped fetch_receipt capability, never a second search
10
+ * store or a CLI-side scan. Receipt output remains authoritative in its store.
11
+ * The API uses only the fixed, tenant-scoped readers in this module.
7
12
  */
8
13
  import { normalizeRuntimeSchedulerSchema } from '../runtime-scheduler-topology';
9
14
  import type { PostgresSchedulerQueryClient } from './postgres';
@@ -56,7 +61,11 @@ const RUNTIME_EVENT_LOG_POSITION_EXPRESSION = `jsonb_set(
56
61
  // Migration readiness tracks this projection body independently of scheduler
57
62
  // wire and schema versions. Bump when a changed trigger must be reinstalled.
58
63
  export const RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER =
59
- 'runtime-event-search-log-context-v14-btree-position';
64
+ 'runtime-event-search-origin-batches-log-metadata';
65
+ /** Trusted trigger-authored state; absent on already-projected legacy rows. */
66
+ export const RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD =
67
+ '_runtimeSearchProjectionPending';
68
+ export const RUNTIME_EVENT_SEARCH_ONLY_FIELD = '_runtimeSearchOnly';
60
69
  export const RUNTIME_EVENT_CANONICAL_RETENTION_INDEX_READY_MARKER =
61
70
  'runtime-event-canonical-retention-v1';
62
71
  export const RUNTIME_EVENTS_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
@@ -172,7 +181,7 @@ function parseRuntimeEventJsonpath(value: string): RuntimeEventPredicate[] {
172
181
  );
173
182
  }
174
183
  const clause =
175
- /\$\.(orgId|runId|playName|runtimeScope|producerAttempt|kind|status|source|stepId|stepKind|datasetId|phase|provider|operation|context\.[A-Za-z_][A-Za-z0-9_]{0,63})\s*==\s*("(?:\\.|[^"\\])*"|true|false|-?\d+(?:\.\d+)?)/y;
184
+ /\$\.(eventId|orgId|runId|playName|runtimeScope|producerAttempt|kind|status|source|level|stepId|stepKind|datasetId|phase|provider|operation|context\.[A-Za-z_][A-Za-z0-9_]{0,63})\s*==\s*("(?:\\.|[^"\\])*"|true|false|-?\d+(?:\.\d+)?)/y;
176
185
  let position = 0;
177
186
  let clauses = 0;
178
187
  const predicates: RuntimeEventPredicate[] = [];
@@ -209,7 +218,9 @@ function parseRuntimeEventJsonpath(value: string): RuntimeEventPredicate[] {
209
218
  );
210
219
  }
211
220
  if (
212
- ['orgId', 'runId', 'playName', 'runtimeScope'].includes(match[1]) &&
221
+ ['eventId', 'orgId', 'runId', 'playName', 'runtimeScope'].includes(
222
+ match[1],
223
+ ) &&
213
224
  typeof parsed !== 'string'
214
225
  ) {
215
226
  throw new RuntimeEventInputError(
@@ -226,6 +237,14 @@ function parseRuntimeEventJsonpath(value: string): RuntimeEventPredicate[] {
226
237
  'Runtime event producerAttempt requires a positive integer.',
227
238
  );
228
239
  }
240
+ if (
241
+ match[1] === 'level' &&
242
+ !['debug', 'info', 'warn', 'error'].includes(String(parsed))
243
+ ) {
244
+ throw new RuntimeEventInputError(
245
+ 'Runtime event level requires debug, info, warn or error.',
246
+ );
247
+ }
229
248
  predicates.push({ path: match[1], literal: match[2], value: parsed });
230
249
  if (
231
250
  match[1].startsWith('context.') &&
@@ -343,6 +362,7 @@ export async function searchRuntimeEvents(
343
362
  addExact('kind', input.kind);
344
363
  if (input.jsonpath !== undefined) {
345
364
  const identityColumns = {
365
+ eventId: 'event_id',
346
366
  orgId: 'org_id',
347
367
  runId: 'run_id',
348
368
  playName: 'play_name',
@@ -950,11 +970,131 @@ export async function hasRuntimeEventReaderGrant(
950
970
  return result.rows[0]?.ready === true;
951
971
  }
952
972
 
973
+ /** Deployment-owned cutover; install readers everywhere before enabling. */
974
+ export async function setRuntimeEventDeferredProjection(
975
+ client: PostgresSchedulerQueryClient,
976
+ runtimeScope: string,
977
+ deferred: boolean,
978
+ independent?: boolean,
979
+ ): Promise<void> {
980
+ const schema = quoteIdent(requiredScope(runtimeScope));
981
+ if (!(await hasRuntimeEventSearchShape(client, runtimeScope))) {
982
+ throw new Error(
983
+ 'Install the deferred outbox reader before changing projection mode.',
984
+ );
985
+ }
986
+ if (deferred && !(await hasRuntimeEventLogPositionIndex(client))) {
987
+ throw new Error(
988
+ 'Prepare the log-position indexes before enabling deferred projection.',
989
+ );
990
+ }
991
+ await client.query('BEGIN');
992
+ try {
993
+ await client.query("SELECT set_config('lock_timeout', '1000', true)");
994
+ // Fence trigger execution while changing mode. This operator-only lock
995
+ // never spans projection; normal producer writes acquire no such lock.
996
+ await client.query(
997
+ `LOCK TABLE ${schema}.outbox IN SHARE ROW EXCLUSIVE MODE`,
998
+ );
999
+ if (!deferred) {
1000
+ const pending = await client.query<{ event_id: string }>(
1001
+ `SELECT event_id FROM ${schema}.outbox
1002
+ WHERE delivered_at IS NULL
1003
+ AND payload_json->'${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' = 'true'::jsonb
1004
+ LIMIT 1`,
1005
+ );
1006
+ if (pending.rows.length > 0) {
1007
+ throw new Error(
1008
+ 'Drain deferred outbox rows before returning to synchronous projection.',
1009
+ );
1010
+ }
1011
+ }
1012
+ if (independent === false || !deferred) {
1013
+ const raw = await client.query<{ event_id: string }>(
1014
+ `SELECT event_id FROM ${schema}.outbox WHERE delivered_at IS NULL AND payload_json->'${RUNTIME_EVENT_SEARCH_ONLY_FIELD}' = 'true'::jsonb LIMIT 1`,
1015
+ );
1016
+ if (raw.rows.length)
1017
+ throw new Error(
1018
+ 'Drain independent search rows before disabling independent projection.',
1019
+ );
1020
+ }
1021
+ await installRuntimeEventProjectionMode(client, schema, deferred);
1022
+ if (independent !== undefined || !deferred)
1023
+ await installIndependentProjectionMode(
1024
+ client,
1025
+ schema,
1026
+ deferred && independent === true,
1027
+ );
1028
+ await client.query('COMMIT');
1029
+ } catch (error) {
1030
+ await client.query('ROLLBACK');
1031
+ throw error;
1032
+ }
1033
+ }
1034
+
1035
+ /** Operator state uses the same durable pending set as the outbox projector. */
1036
+ export async function inspectRuntimeEventProjection(
1037
+ client: PostgresSchedulerQueryClient,
1038
+ runtimeScope: string,
1039
+ ): Promise<{
1040
+ deferred: boolean;
1041
+ independent: boolean;
1042
+ pending: number;
1043
+ blocked: number;
1044
+ oldestPendingAt: string | null;
1045
+ }> {
1046
+ const schema = quoteIdent(requiredScope(runtimeScope));
1047
+ const result = await client.query<{
1048
+ deferred: boolean;
1049
+ independent: boolean;
1050
+ pending: string | number;
1051
+ blocked: string | number;
1052
+ oldest: Date | string | null;
1053
+ }>(`SELECT ${schema}.defer_runtime_event_projection() AS deferred, ${schema}.independent_runtime_event_projection() AS independent,
1054
+ count(*) AS pending, count(*) FILTER (WHERE failed_at IS NOT NULL) AS blocked,
1055
+ min(created_at) AS oldest
1056
+ FROM ${schema}.outbox
1057
+ WHERE delivered_at IS NULL
1058
+ AND payload_json->'${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' = 'true'::jsonb`);
1059
+ const state = result.rows[0]!;
1060
+ return {
1061
+ deferred: state.deferred,
1062
+ independent: state.independent,
1063
+ pending: Number(state.pending),
1064
+ blocked: Number(state.blocked),
1065
+ oldestPendingAt: state.oldest === null ? null : iso(state.oldest),
1066
+ };
1067
+ }
1068
+
1069
+ async function installRuntimeEventProjectionMode(
1070
+ client: PostgresSchedulerQueryClient,
1071
+ schema: string,
1072
+ deferred: boolean,
1073
+ ): Promise<void> {
1074
+ await client.query(`CREATE OR REPLACE FUNCTION ${schema}.defer_runtime_event_projection()
1075
+ RETURNS boolean LANGUAGE sql VOLATILE SET search_path = pg_catalog
1076
+ AS 'SELECT ${deferred ? 'true' : 'false'}'`);
1077
+ await client.query(
1078
+ `REVOKE ALL ON FUNCTION ${schema}.defer_runtime_event_projection() FROM PUBLIC`,
1079
+ );
1080
+ }
1081
+
1082
+ async function installIndependentProjectionMode(
1083
+ client: PostgresSchedulerQueryClient,
1084
+ schema: string,
1085
+ enabled: boolean,
1086
+ ): Promise<void> {
1087
+ await client.query(`CREATE OR REPLACE FUNCTION ${schema}.independent_runtime_event_projection()
1088
+ RETURNS boolean LANGUAGE sql VOLATILE SET search_path = pg_catalog AS 'SELECT ${enabled ? 'true' : 'false'}'`);
1089
+ await client.query(
1090
+ `REVOKE ALL ON FUNCTION ${schema}.independent_runtime_event_projection() FROM PUBLIC`,
1091
+ );
1092
+ }
1093
+
953
1094
  /**
954
- * Triggers are attached to each scheduler namespace by the explicit migration.
955
- * Search writes are atomic with the already-durable outbox/receipt write. A
956
- * trigger failure is loud so no authoritative write can silently lose its
957
- * corresponding search history after enablement.
1095
+ * Migration installs readers with synchronous production unchanged. After
1096
+ * cutover, the producer persists raw input and only the leased dispatcher
1097
+ * projects and compacts it, atomically, before acknowledging its outbox row.
958
1098
  */
959
1099
  export async function attachRuntimeEventSearchTriggers(
960
1100
  client: PostgresSchedulerQueryClient,
@@ -962,6 +1102,33 @@ export async function attachRuntimeEventSearchTriggers(
962
1102
  ): Promise<void> {
963
1103
  const scope = requiredScope(runtimeScope);
964
1104
  const schema = quoteIdent(scope);
1105
+ // Refreshing the reader must preserve an already-enabled producer cutover.
1106
+ const mode = await client.query<{ installed: boolean }>(
1107
+ `SELECT to_regprocedure($1) IS NOT NULL AS installed`,
1108
+ [`${schema}.defer_runtime_event_projection()`],
1109
+ );
1110
+ if (!mode.rows[0]?.installed) {
1111
+ await installRuntimeEventProjectionMode(client, schema, false);
1112
+ }
1113
+ const independentMode = await client.query<{ installed: boolean }>(
1114
+ `SELECT to_regprocedure($1) IS NOT NULL AS installed`,
1115
+ [`${schema}.independent_runtime_event_projection()`],
1116
+ );
1117
+ if (!independentMode.rows[0]?.installed)
1118
+ await installIndependentProjectionMode(client, schema, false);
1119
+ // Stamp new admissions once. Existing launches never acquire a producer pin
1120
+ // on replay or worker upgrade. Ignore a caller-supplied pin before cutover.
1121
+ await client.query(`CREATE OR REPLACE FUNCTION ${schema}.pin_runtime_log_identity() RETURNS trigger
1122
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
1123
+ BEGIN
1124
+ NEW.launch_json := NEW.launch_json - 'runtimeLogIdentity';
1125
+ NEW.admission_json := NEW.admission_json - 'runtimeLogIdentity';
1126
+ IF ${schema}.independent_runtime_event_projection() THEN
1127
+ NEW.launch_json := NEW.launch_json || '{"runtimeLogIdentity":"origin"}'::jsonb;
1128
+ NEW.admission_json := NEW.admission_json || '{"runtimeLogIdentity":"origin"}'::jsonb;
1129
+ END IF;
1130
+ RETURN NEW;
1131
+ END $function$`);
965
1132
  await client.query(`
966
1133
  CREATE OR REPLACE FUNCTION ${schema}.search_receipt_event() RETURNS trigger
967
1134
  LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
@@ -978,6 +1145,23 @@ export async function attachRuntimeEventSearchTriggers(
978
1145
  IF NEW.run_id IS NULL OR NEW.org_id IS NULL THEN
979
1146
  RAISE EXCEPTION 'Terminal receipt search projection requires run and organization';
980
1147
  END IF;
1148
+ IF ${schema}.defer_runtime_event_projection() THEN
1149
+ IF NOT EXISTS (SELECT 1 FROM ${schema}.runs
1150
+ WHERE run_id = NEW.run_id AND org_id = NEW.org_id) THEN
1151
+ RAISE EXCEPTION 'Terminal receipt search projection has no matching run';
1152
+ END IF;
1153
+ ref := gen_random_uuid();
1154
+ INSERT INTO ${schema}.outbox (event_id, run_id, event_type, payload_json, created_at)
1155
+ VALUES ('receipt:' || ref::text, NEW.run_id,
1156
+ ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.receiptEvent},
1157
+ jsonb_strip_nulls(jsonb_build_object(
1158
+ 'orgId', NEW.org_id, 'receiptRef', ref, 'receiptKey', NEW.k,
1159
+ 'status', NEW.status, 'occurredAt', NEW.updated_at,
1160
+ 'producerAttempt', NEW.lease_owner_attempt)),
1161
+ GREATEST(clock_timestamp(), coalesce((SELECT max(created_at) + interval '1 microsecond'
1162
+ FROM ${schema}.outbox WHERE run_id = NEW.run_id), clock_timestamp())));
1163
+ RETURN NEW;
1164
+ END IF;
981
1165
  -- Serialize event ordering with the same per-run lock as outbox facts.
982
1166
  PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(NEW.run_id));
983
1167
  SELECT play_name, run_event_store = 'planet_scale'
@@ -1001,7 +1185,19 @@ export async function attachRuntimeEventSearchTriggers(
1001
1185
  NEW.updated_at, NEW.run_id, play, 'receipt.' || NEW.status,
1002
1186
  ref, NEW.k, jsonb_strip_nulls(jsonb_build_object(
1003
1187
  'kind', 'receipt.' || NEW.status, 'status', NEW.status,
1004
- 'source', 'receipt', 'producerAttempt', NEW.lease_owner_attempt
1188
+ 'source', 'receipt', 'producerAttempt', NEW.lease_owner_attempt,
1189
+ -- Only the reserved runtime diagnostic record supplies this flat
1190
+ -- context. Ordinary authored receipt outputs are never indexed.
1191
+ 'context', CASE WHEN NEW.k LIKE 'tool-evidence:%'
1192
+ AND NEW.output_json->>'kind' = 'tool_call_evidence' THEN jsonb_strip_nulls(jsonb_build_object(
1193
+ 'callKey', NEW.output_json#>'{context,callKey}',
1194
+ 'toolId', NEW.output_json#>'{context,toolId}',
1195
+ 'callStatus', NEW.output_json#>'{context,callStatus}',
1196
+ 'stepId', NEW.output_json#>'{context,stepId}',
1197
+ 'dataset', NEW.output_json#>'{context,dataset}',
1198
+ 'column', NEW.output_json#>'{context,column}',
1199
+ 'rowKey', NEW.output_json#>'{context,rowKey}'
1200
+ )) END
1005
1201
  ))
1006
1202
  );
1007
1203
  RETURN NEW;
@@ -1009,18 +1205,129 @@ export async function attachRuntimeEventSearchTriggers(
1009
1205
  $function$
1010
1206
  `);
1011
1207
  await client.query(`DO $create$ BEGIN
1208
+ IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.runs'::regclass
1209
+ AND tgname = 'runtime_run_log_identity') THEN
1210
+ CREATE TRIGGER runtime_run_log_identity BEFORE INSERT ON ${schema}.runs
1211
+ FOR EACH ROW EXECUTE FUNCTION ${schema}.pin_runtime_log_identity();
1212
+ END IF;
1012
1213
  IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.receipts'::regclass
1013
1214
  AND tgname = 'runtime_receipt_event_v1') THEN
1014
1215
  CREATE TRIGGER runtime_receipt_event_v1 AFTER INSERT OR UPDATE ON ${schema}.receipts
1015
1216
  FOR EACH ROW EXECUTE FUNCTION ${schema}.search_receipt_event();
1016
1217
  END IF;
1017
1218
  END $create$`);
1219
+ await client.query(`
1220
+ CREATE OR REPLACE FUNCTION ${schema}.run_log_metadata(line text, metadata jsonb, fallback_at timestamptz) RETURNS jsonb
1221
+ LANGUAGE plpgsql IMMUTABLE SET search_path = pg_catalog AS $function$
1222
+ DECLARE clean text; message text; level text; at_time timestamptz := fallback_at;
1223
+ BEGIN
1224
+ clean := regexp_replace(line, '^' || chr(1) || 'prov:[^' || chr(1) || ']*' || chr(1), '');
1225
+ IF metadata IS NOT NULL AND metadata <> 'null'::jsonb THEN
1226
+ IF jsonb_typeof(metadata) IS DISTINCT FROM 'object'
1227
+ OR NOT coalesce(metadata->>'level' IN ('debug','info','warn','error'), false)
1228
+ OR jsonb_typeof(metadata->'occurredAt') IS DISTINCT FROM 'number'
1229
+ OR NOT coalesce(metadata->>'occurredAt' ~ '^[1-9][0-9]{0,15}$', false)
1230
+ OR (metadata ? 'stepId' AND (jsonb_typeof(metadata->'stepId') IS DISTINCT FROM 'string'
1231
+ OR length(metadata->>'stepId') NOT BETWEEN 1 AND 256)) THEN
1232
+ RAISE EXCEPTION 'Run log metadata is invalid';
1233
+ END IF;
1234
+ RETURN jsonb_strip_nulls(jsonb_build_object('level', metadata->'level',
1235
+ 'occurredAt', metadata->'occurredAt', 'stepId', metadata->'stepId'));
1236
+ END IF;
1237
+ -- Historical lines already carry the original ISO time and severity.
1238
+ IF clean ~ '^\\[[-0-9]{10}T[0-9:.]+Z\\]' THEN
1239
+ BEGIN at_time := substring(clean from '^\\[([^]]+)\\]')::timestamptz;
1240
+ EXCEPTION WHEN invalid_datetime_format OR datetime_field_overflow THEN at_time := fallback_at; END;
1241
+ END IF;
1242
+ message := regexp_replace(clean, '^\\[[-0-9]{10}T[0-9:.]+Z\\][ ]*', '');
1243
+ level := CASE
1244
+ WHEN message ~* '^\\[(error|console.error)\\]' THEN 'error'
1245
+ WHEN message ~* '^\\[(warn|console.warn)\\]' THEN 'warn'
1246
+ WHEN message ~* '^\\[info\\]' THEN 'info'
1247
+ WHEN message ~* '^\\[(debug|console.debug|perf|runtime.(phase_summary|logs|log_transport))\\]'
1248
+ OR line ~ ('^' || chr(1) || 'prov:(infra|receipt|replay)' || chr(1)) THEN 'debug'
1249
+ ELSE 'info' END;
1250
+ RETURN jsonb_build_object('level', level, 'occurredAt', floor(extract(epoch FROM at_time) * 1000)::bigint);
1251
+ END
1252
+ $function$
1253
+ `);
1254
+ await client.query(`
1255
+ CREATE OR REPLACE FUNCTION ${schema}.project_origin_log_batch(observation ${schema}.outbox, item jsonb, run_org text, play text, compact_logs boolean, canonical_events boolean, at_time timestamptz, doc jsonb, closed boolean)
1256
+ RETURNS text[] LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
1257
+ DECLARE fresh text[]; mismatches bigint;
1258
+ BEGIN
1259
+ IF jsonb_typeof(item->'lineIds') IS DISTINCT FROM 'array'
1260
+ OR jsonb_array_length(item->'lineIds') <> jsonb_array_length(item->'lines')
1261
+ OR EXISTS (SELECT 1 FROM jsonb_array_elements(item->'lineIds') id WHERE jsonb_typeof(id) <> 'string' OR (id #>> '{}') !~ '^[A-Za-z0-9:_-]{1,128}$')
1262
+ OR (SELECT count(DISTINCT id) FROM jsonb_array_elements(item->'lineIds') id) <> jsonb_array_length(item->'lineIds') THEN
1263
+ RAISE EXCEPTION 'Origin log identities must be distinct bounded strings aligned with lines';
1264
+ END IF;
1265
+ IF EXISTS (SELECT 1 FROM jsonb_array_elements(item->'lines') line WHERE jsonb_typeof(line) <> 'string') THEN
1266
+ RAISE EXCEPTION 'Search projection requires log line strings';
1267
+ END IF;
1268
+ IF item ? 'lineContexts' AND EXISTS (
1269
+ SELECT 1 FROM jsonb_array_elements(item->'lineContexts') ctx
1270
+ WHERE jsonb_typeof(ctx) NOT IN ('object', 'null')
1271
+ ) THEN RAISE EXCEPTION 'Search projection log context must be an object'; END IF;
1272
+ IF item ? 'lineContexts' AND EXISTS (
1273
+ SELECT 1 FROM jsonb_array_elements(item->'lineContexts') ctx
1274
+ WHERE jsonb_typeof(ctx) = 'object' AND (
1275
+ (SELECT count(*) FROM jsonb_object_keys(ctx)) > 8 OR octet_length(ctx::text) > 2064
1276
+ OR EXISTS (SELECT 1 FROM jsonb_each(ctx) field WHERE
1277
+ field.key !~ '^[A-Za-z_][A-Za-z0-9_]{0,63}$' OR field.key IN ('__proto__', 'constructor', 'prototype')
1278
+ OR jsonb_typeof(field.value) NOT IN ('string', 'number', 'boolean')
1279
+ OR (jsonb_typeof(field.value) = 'string' AND octet_length(field.value #>> '{}') > 256)
1280
+ OR (jsonb_typeof(field.value) = 'number' AND octet_length(field.value #>> '{}') > 32))
1281
+ )
1282
+ ) THEN RAISE EXCEPTION 'Search projection log context has an unsupported key or value'; END IF;
1283
+ WITH entries AS MATERIALIZED (
1284
+ SELECT value #>> '{}' AS body, ordinality AS ordinal,
1285
+ 'log-origin:' || observation.run_id || ':' || (item->'lineIds'->>(ordinality-1)::int) AS eid,
1286
+ NULLIF(item->'lineContexts'->(ordinality-1)::int, 'null'::jsonb) AS ctx,
1287
+ ${schema}.run_log_metadata(value #>> '{}', item->'lineMetadata'->(ordinality-1)::int, at_time) AS metadata
1288
+ FROM jsonb_array_elements(item->'lines') WITH ORDINALITY
1289
+ ), inserted AS (
1290
+ INSERT INTO runtime_observability.events (
1291
+ runtime_scope, org_id, event_id, event_seq, event_payload, occurred_at,
1292
+ run_id, play_name, kind, level, message, log_body, source_digest, log_text_digest, search_doc
1293
+ ) SELECT ${quoteLiteral(scope)}, run_org, eid,
1294
+ CASE WHEN canonical_events THEN nextval('runtime_observability.runtime_event_order_seq') END,
1295
+ CASE WHEN canonical_events THEN jsonb_strip_nulls((item - 'lines' - 'lineContexts' - 'lineIds' - 'lineMetadata') || jsonb_build_object(
1296
+ 'type', ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, 'runId', observation.run_id,
1297
+ 'occurredAt', metadata->'occurredAt', 'level', metadata->'level', 'stepId', metadata->'stepId', 'lineOrdinal', ordinal,
1298
+ 'entryId', item->'lineIds'->(ordinal-1)::int, 'context', ctx)) END,
1299
+ to_timestamp((metadata->>'occurredAt')::double precision / 1000), observation.run_id, play, ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, metadata->>'level',
1300
+ CASE WHEN compact_logs THEN NULL ELSE left(body, 2048) END, body,
1301
+ CASE WHEN ctx IS NULL THEN encode(sha256(convert_to(body,'UTF8')),'hex')
1302
+ ELSE encode(sha256(convert_to(jsonb_build_array(body,ctx)::text,'UTF8')),'hex') END,
1303
+ encode(sha256(convert_to(btrim(body),'UTF8')),'hex'),
1304
+ doc || (metadata - 'occurredAt') || jsonb_strip_nulls(jsonb_build_object('entryId', item->'lineIds'->(ordinal-1)::int, 'context', ctx))
1305
+ FROM entries WHERE NOT closed ORDER BY ordinal
1306
+ ON CONFLICT (runtime_scope, org_id, event_id) DO NOTHING RETURNING event_id
1307
+ ) SELECT coalesce(array_agg(inserted.event_id ORDER BY entries.ordinal), ARRAY[]::text[])
1308
+ INTO fresh FROM inserted JOIN entries ON entries.eid = inserted.event_id;
1309
+ -- A new READ COMMITTED statement sees a concurrent uniqueness winner.
1310
+ -- Tombstones retain digest/identity: retry never restores an expired body.
1311
+ SELECT count(*) INTO mismatches
1312
+ FROM jsonb_array_elements(item->'lines') WITH ORDINALITY AS entries(body, ordinal)
1313
+ LEFT JOIN runtime_observability.events event ON event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
1314
+ AND event.event_id = 'log-origin:' || observation.run_id || ':' || (item->'lineIds'->>(ordinal-1)::int)
1315
+ WHERE event.run_id IS DISTINCT FROM observation.run_id OR event.kind IS DISTINCT FROM ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
1316
+ OR event.source_digest IS DISTINCT FROM CASE WHEN NULLIF(item->'lineContexts'->(ordinal-1)::int, 'null'::jsonb) IS NULL
1317
+ THEN encode(sha256(convert_to(body #>> '{}','UTF8')),'hex')
1318
+ ELSE encode(sha256(convert_to(jsonb_build_array(body #>> '{}',item->'lineContexts'->(ordinal-1)::int)::text,'UTF8')),'hex') END;
1319
+ IF mismatches > 0 THEN
1320
+ RAISE EXCEPTION 'Origin log identity changed its content or ingress is closed after terminal acceptance';
1321
+ END IF;
1322
+ RETURN fresh;
1323
+ END $function$
1324
+ `);
1018
1325
  await client.query(`
1019
1326
  CREATE OR REPLACE FUNCTION ${schema}.project_outbox_event(observation ${schema}.outbox) RETURNS void
1020
1327
  LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
1021
1328
  DECLARE run_org text; play text; compact_logs boolean; canonical_events boolean; run_status text; item jsonb; line jsonb; event_batch jsonb;
1022
1329
  ordinal bigint; line_ordinal bigint; kind text; at_time timestamptz;
1023
- doc jsonb; line_doc jsonb; line_context jsonb;
1330
+ doc jsonb; line_doc jsonb; line_context jsonb; line_metadata jsonb; line_at timestamptz;
1024
1331
  eid text; existing_digest text; digest text; assigned_seq bigint; terminal_progress_identity boolean;
1025
1332
  log_channel text; log_position bigint; matched_event_id text; matched_doc jsonb;
1026
1333
  legacy_position_prefix text; position_event_ids text[];
@@ -1033,7 +1340,11 @@ export async function attachRuntimeEventSearchTriggers(
1033
1340
  -- Log ordering owns a transaction-scoped mutex, independent of the
1034
1341
  -- scheduler's Run/attempt row-lock order. In particular, old completion
1035
1342
  -- writers lock attempt before Run; this recorder must not invert that.
1036
- PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(observation.run_id));
1343
+ -- Stable batches are ordered by the leased background prefix and cursor row.
1344
+ -- The legacy alias path still needs its old transaction mutex.
1345
+ IF NOT ${schema}.defer_runtime_event_projection() THEN
1346
+ PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(observation.run_id));
1347
+ END IF;
1037
1348
  SELECT org_id, play_name, run_log_store = 'planet_scale', status,
1038
1349
  run_event_store = 'planet_scale'
1039
1350
  INTO run_org, play, compact_logs, run_status, canonical_events
@@ -1041,6 +1352,35 @@ export async function attachRuntimeEventSearchTriggers(
1041
1352
  IF run_org IS NULL OR play IS NULL THEN
1042
1353
  RAISE EXCEPTION 'Outbox search projection has no matching run';
1043
1354
  END IF;
1355
+ IF observation.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.receiptEvent} THEN
1356
+ IF observation.payload_json->>'orgId' IS DISTINCT FROM run_org
1357
+ OR NOT coalesce(observation.payload_json->>'status' IN ('completed', 'failed', 'skipped'), false)
1358
+ OR nullif(observation.payload_json->>'receiptKey', '') IS NULL
1359
+ OR observation.payload_json->>'receiptRef' IS NULL
1360
+ OR observation.payload_json->>'occurredAt' IS NULL THEN
1361
+ RAISE EXCEPTION 'Deferred receipt event has invalid identity or status';
1362
+ END IF;
1363
+ INSERT INTO runtime_observability.events (
1364
+ runtime_scope, org_id, event_id, event_seq, event_payload,
1365
+ occurred_at, run_id, play_name, kind, receipt_ref, receipt_key, search_doc
1366
+ ) VALUES (
1367
+ ${quoteLiteral(scope)}, run_org, observation.event_id,
1368
+ CASE WHEN canonical_events THEN nextval('runtime_observability.runtime_event_order_seq') END,
1369
+ CASE WHEN canonical_events THEN jsonb_build_object(
1370
+ 'type', 'receipt.' || (observation.payload_json->>'status'), 'runId', observation.run_id,
1371
+ 'occurredAt', floor(extract(epoch FROM (observation.payload_json->>'occurredAt')::timestamptz) * 1000)::bigint,
1372
+ 'source', 'receipt', 'status', observation.payload_json->>'status') END,
1373
+ (observation.payload_json->>'occurredAt')::timestamptz,
1374
+ observation.run_id, play, 'receipt.' || (observation.payload_json->>'status'),
1375
+ (observation.payload_json->>'receiptRef')::uuid,
1376
+ observation.payload_json->>'receiptKey',
1377
+ jsonb_strip_nulls(jsonb_build_object(
1378
+ 'kind', 'receipt.' || (observation.payload_json->>'status'),
1379
+ 'status', observation.payload_json->>'status', 'source', 'receipt',
1380
+ 'producerAttempt', observation.payload_json->'producerAttempt'))
1381
+ ) ON CONFLICT DO NOTHING;
1382
+ RETURN;
1383
+ END IF;
1044
1384
  IF observation.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} THEN
1045
1385
  event_batch := observation.payload_json->'events';
1046
1386
  IF jsonb_typeof(event_batch) IS DISTINCT FROM 'array' THEN
@@ -1071,7 +1411,9 @@ export async function attachRuntimeEventSearchTriggers(
1071
1411
  'type', ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, 'runId', observation.run_id, 'source', 'terminal',
1072
1412
  'producerAttempt', envelope_attempt, 'channelOffset', 0,
1073
1413
  'lines', observation.payload_json->'terminalLogLines',
1074
- 'lineContexts', observation.payload_json->'terminalLogContexts'
1414
+ 'lineContexts', observation.payload_json->'terminalLogContexts',
1415
+ 'lineMetadata', observation.payload_json->'terminalLogMetadata',
1416
+ 'lineIds', NULLIF(observation.payload_json->'terminalLogIds', 'null'::jsonb)
1075
1417
  ))
1076
1418
  );
1077
1419
  END IF;
@@ -1117,11 +1459,14 @@ export async function attachRuntimeEventSearchTriggers(
1117
1459
  ));
1118
1460
  END IF;
1119
1461
  IF event_batch IS NOT NULL THEN
1462
+ IF ${schema}.defer_runtime_event_projection() AND EXISTS (SELECT 1 FROM jsonb_array_elements(event_batch) entry WHERE entry->>'type' = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} AND (NOT (entry ? 'lineIds') OR jsonb_typeof(entry->'lineIds') IS DISTINCT FROM 'array' OR EXISTS (SELECT 1 FROM jsonb_array_elements(CASE WHEN jsonb_typeof(entry->'lineIds') = 'array' THEN entry->'lineIds' ELSE '[]'::jsonb END) id WHERE id = 'null'::jsonb))) THEN
1463
+ PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(observation.run_id));
1464
+ END IF;
1120
1465
  -- Normalize old positional writers once per batch under the run mutex.
1121
1466
  -- Per-line lookups must remain digest-selective: an OR fallback for
1122
1467
  -- unmarked rows otherwise scans the whole attempt for every new line.
1123
1468
  IF EXISTS (SELECT 1 FROM jsonb_array_elements(event_batch) AS entry
1124
- WHERE entry->>'type' = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}) THEN
1469
+ WHERE entry->>'type' = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} AND (NOT (entry ? 'lineIds') OR EXISTS (SELECT 1 FROM jsonb_array_elements(CASE WHEN jsonb_typeof(entry->'lineIds') = 'array' THEN entry->'lineIds' ELSE '[]'::jsonb END) id WHERE id = 'null'::jsonb))) THEN
1125
1470
  UPDATE runtime_observability.events AS event SET search_doc = event.search_doc ||
1126
1471
  jsonb_build_object('logContentDigest', event.source_digest) ||
1127
1472
  CASE WHEN NOT (event.search_doc ? 'logPositions')
@@ -1134,7 +1479,7 @@ export async function attachRuntimeEventSearchTriggers(
1134
1479
  ELSE '{}'::jsonb END
1135
1480
  WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
1136
1481
  AND event.run_id = observation.run_id AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
1137
- AND event.source_digest IS NOT NULL AND NOT (event.search_doc ? 'logContentDigest')
1482
+ AND event.source_digest IS NOT NULL AND NOT (event.search_doc ? 'logContentDigest') AND NOT (event.search_doc ? 'entryId')
1138
1483
  AND (event.search_doc->>'producerAttempt') ~ '^[1-9][0-9]{0,5}$';
1139
1484
  END IF;
1140
1485
  FOR item, ordinal IN SELECT value, ordinality FROM jsonb_array_elements(event_batch) WITH ORDINALITY LOOP
@@ -1172,12 +1517,28 @@ export async function attachRuntimeEventSearchTriggers(
1172
1517
  OR jsonb_array_length(item->'lineContexts') <> jsonb_array_length(item->'lines')) THEN
1173
1518
  RAISE EXCEPTION 'Search projection requires lineContexts aligned to log lines';
1174
1519
  END IF;
1520
+ IF kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} AND item ? 'lineMetadata'
1521
+ AND (jsonb_typeof(item->'lineMetadata') IS DISTINCT FROM 'array'
1522
+ OR jsonb_array_length(item->'lineMetadata') <> jsonb_array_length(item->'lines')) THEN
1523
+ RAISE EXCEPTION 'Run log metadata must align with lines';
1524
+ END IF;
1525
+ IF kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} AND item ? 'lineIds' THEN
1526
+ IF jsonb_typeof(item->'lineIds') IS DISTINCT FROM 'array' OR jsonb_array_length(item->'lineIds') <> jsonb_array_length(item->'lines') THEN
1527
+ RAISE EXCEPTION 'Origin log identities must align with lines';
1528
+ END IF;
1529
+ IF EXISTS (SELECT 1 FROM jsonb_array_elements(item->'lineIds') id WHERE id <> 'null'::jsonb AND (jsonb_typeof(id) <> 'string' OR (id #>> '{}') !~ '^[A-Za-z0-9:_-]{1,128}$')) OR (SELECT count(id) FROM jsonb_array_elements(item->'lineIds') id WHERE id <> 'null'::jsonb) <> (SELECT count(DISTINCT id) FROM jsonb_array_elements(item->'lineIds') id WHERE id <> 'null'::jsonb) THEN RAISE EXCEPTION 'Origin log identities must be distinct bounded strings'; END IF;
1530
+ IF NOT EXISTS (SELECT 1 FROM jsonb_array_elements(item->'lineIds') id WHERE id = 'null'::jsonb) THEN
1531
+ fresh_event_ids := fresh_event_ids || ${schema}.project_origin_log_batch(observation, item, run_org, play, compact_logs, canonical_events, at_time, doc,
1532
+ compact_logs AND observation.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} AND coalesce((observation.payload_json->>'_runtimeSearchRunClosed')::boolean, run_status IN ('completed','failed','cancelled')));
1533
+ CONTINUE;
1534
+ END IF;
1535
+ END IF;
1175
1536
  IF kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} THEN
1176
1537
  FOR line, line_ordinal IN SELECT value, ordinality FROM jsonb_array_elements(item->'lines') WITH ORDINALITY LOOP
1177
1538
  IF jsonb_typeof(line) IS DISTINCT FROM 'string' THEN
1178
1539
  RAISE EXCEPTION 'Search projection requires log line strings';
1179
1540
  END IF;
1180
- IF (item->>'source' IS DISTINCT FROM 'coordinator')
1541
+ IF NULLIF(item->'lineIds'->((line_ordinal-1)::int), 'null'::jsonb) IS NULL AND (item->>'source' IS DISTINCT FROM 'coordinator')
1181
1542
  AND NOT coalesce((item->>'channelOffset') ~ '^[0-9]{1,12}$', false) THEN
1182
1543
  IF NOT recent_log_keys_loaded OR recent_log_attempt IS DISTINCT FROM item->>'producerAttempt' THEN
1183
1544
  SELECT coalesce(jsonb_object_agg(recent.dedupe_key, true), '{}'::jsonb)
@@ -1187,6 +1548,7 @@ export async function attachRuntimeEventSearchTriggers(
1187
1548
  FROM runtime_observability.events AS event
1188
1549
  WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
1189
1550
  AND event.run_id = observation.run_id AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
1551
+ AND NOT (event.search_doc ? 'entryId')
1190
1552
  AND event.log_seq IS NOT NULL AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
1191
1553
  AND (item->>'producerAttempt' IS NULL
1192
1554
  OR event.search_doc->>'producerAttempt' = item->>'producerAttempt')
@@ -1231,8 +1593,10 @@ export async function attachRuntimeEventSearchTriggers(
1231
1593
  RAISE EXCEPTION 'Search projection log context has an unsupported key or value';
1232
1594
  END IF;
1233
1595
  END IF;
1234
- line_doc := CASE WHEN line_context IS NULL THEN doc
1235
- ELSE doc || jsonb_build_object('context', line_context) END;
1596
+ line_metadata := ${schema}.run_log_metadata(line #>> '{}', item->'lineMetadata'->((line_ordinal - 1)::int), at_time);
1597
+ line_at := to_timestamp((line_metadata->>'occurredAt')::double precision / 1000);
1598
+ line_doc := (CASE WHEN line_context IS NULL THEN doc
1599
+ ELSE doc || jsonb_build_object('context', line_context) END) || (line_metadata - 'occurredAt');
1236
1600
  eid := CASE WHEN (item->>'channelOffset') ~ '^[0-9]{1,12}$'
1237
1601
  AND (item->>'producerAttempt') ~ '^[0-9]{1,6}$'
1238
1602
  THEN 'log:' || observation.run_id || ':' || (item->>'producerAttempt') || ':' ||
@@ -1241,7 +1605,11 @@ export async function attachRuntimeEventSearchTriggers(
1241
1605
  digest := CASE WHEN line_context IS NULL
1242
1606
  THEN encode(sha256(convert_to(line #>> '{}', 'UTF8')), 'hex')
1243
1607
  ELSE encode(sha256(convert_to(jsonb_build_array(line #>> '{}', line_context)::text, 'UTF8')), 'hex') END;
1244
- IF (item->>'channelOffset') ~ '^[0-9]{1,12}$'
1608
+ IF NULLIF(item->'lineIds'->((line_ordinal-1)::int), 'null'::jsonb) IS NOT NULL THEN
1609
+ IF jsonb_typeof(item->'lineIds'->((line_ordinal-1)::int)) <> 'string' OR (item->'lineIds'->>((line_ordinal-1)::int)) !~ '^[A-Za-z0-9:_-]{1,128}$' THEN RAISE EXCEPTION 'Origin log identity must be a bounded string'; END IF;
1610
+ eid := 'log-origin:' || observation.run_id || ':' || (item->'lineIds'->>((line_ordinal-1)::int));
1611
+ line_doc := line_doc || jsonb_build_object('entryId', item->'lineIds'->((line_ordinal-1)::int));
1612
+ ELSIF (item->>'channelOffset') ~ '^[0-9]{1,12}$'
1245
1613
  AND (item->>'producerAttempt') ~ '^[1-9][0-9]{0,5}$' THEN
1246
1614
  -- These transports retain different buffers. Their offsets
1247
1615
  -- are immutable within a channel, never interchangeable.
@@ -1340,7 +1708,7 @@ export async function attachRuntimeEventSearchTriggers(
1340
1708
  AND event.run_id = observation.run_id AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
1341
1709
  AND event.source_digest IS NOT NULL
1342
1710
  AND event.search_doc ? 'producerAttempt'
1343
- AND event.search_doc ? 'logContentDigest'
1711
+ AND event.search_doc ? 'logContentDigest' AND NOT (event.search_doc ? 'entryId')
1344
1712
  AND event.search_doc->>'producerAttempt' = ((item->>'producerAttempt')::integer)::text
1345
1713
  AND event.source_digest = digest
1346
1714
  AND event.search_doc @> jsonb_build_object('logContentDigest', digest)
@@ -1361,7 +1729,8 @@ export async function attachRuntimeEventSearchTriggers(
1361
1729
  -- retry of an accepted position is harmless; a new position
1362
1730
  -- after terminal closure must never recreate expired history.
1363
1731
  IF compact_logs AND observation.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents}
1364
- AND run_status IN ('completed', 'failed', 'cancelled')
1732
+ AND coalesce((observation.payload_json->>'_runtimeSearchRunClosed')::boolean,
1733
+ run_status IN ('completed', 'failed', 'cancelled'))
1365
1734
  AND NOT EXISTS (SELECT 1 FROM runtime_observability.events
1366
1735
  WHERE runtime_scope = ${quoteLiteral(scope)} AND org_id = run_org
1367
1736
  AND event_id = eid) THEN
@@ -1375,14 +1744,14 @@ export async function attachRuntimeEventSearchTriggers(
1375
1744
  ${quoteLiteral(scope)}, run_org, eid,
1376
1745
  CASE WHEN canonical_events THEN nextval('runtime_observability.runtime_event_order_seq') END,
1377
1746
  CASE WHEN canonical_events THEN jsonb_strip_nulls(
1378
- (item - 'lines' - 'lineContexts') || jsonb_build_object(
1747
+ (item - 'lines' - 'lineContexts' - 'lineMetadata' - 'lineIds') || jsonb_build_object(
1379
1748
  'type', ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, 'runId', observation.run_id,
1380
- 'occurredAt', floor(extract(epoch FROM at_time) * 1000)::bigint,
1749
+ 'occurredAt', line_metadata->'occurredAt', 'level', line_metadata->'level', 'stepId', line_metadata->'stepId',
1381
1750
  'lineOrdinal', line_ordinal,
1382
1751
  'context', line_context
1383
1752
  )) END,
1384
- at_time, observation.run_id, play,
1385
- ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, NULL, CASE WHEN compact_logs THEN NULL ELSE left(line #>> '{}', 2048) END,
1753
+ line_at, observation.run_id, play,
1754
+ ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, line_metadata->>'level', CASE WHEN compact_logs THEN NULL ELSE left(line #>> '{}', 2048) END,
1386
1755
  line #>> '{}', digest,
1387
1756
  encode(sha256(convert_to(btrim(line #>> '{}'), 'UTF8')), 'hex'), line_doc
1388
1757
  ) ON CONFLICT DO NOTHING;
@@ -1462,6 +1831,15 @@ export async function attachRuntimeEventSearchTriggers(
1462
1831
  AND NOT (observation.payload_json ? 'terminalLogLines') THEN
1463
1832
  RAISE EXCEPTION 'Search projection requires terminalLogLines for terminalLogContexts';
1464
1833
  END IF;
1834
+ IF observation.payload_json ? 'terminalLogMetadata'
1835
+ AND NOT (observation.payload_json ? 'terminalLogLines') THEN
1836
+ RAISE EXCEPTION 'Search projection requires terminalLogLines for terminalLogMetadata';
1837
+ END IF;
1838
+ IF observation.payload_json ? 'terminalLogMetadata'
1839
+ AND (jsonb_typeof(observation.payload_json->'terminalLogMetadata') IS DISTINCT FROM 'array'
1840
+ OR jsonb_array_length(observation.payload_json->'terminalLogMetadata') <> jsonb_array_length(observation.payload_json->'terminalLogLines')) THEN
1841
+ RAISE EXCEPTION 'Search projection requires terminalLogMetadata aligned to terminalLogLines';
1842
+ END IF;
1465
1843
  IF observation.event_type IN (${RUN_EVENT_TERMINAL_LOG_CARRIER_SQL_LIST})
1466
1844
  AND observation.payload_json ? 'terminalLogLines'
1467
1845
  AND NOT coalesce((envelope_attempt #>> '{}') ~ '^[1-9][0-9]{0,5}$', false) THEN
@@ -1494,6 +1872,7 @@ export async function attachRuntimeEventSearchTriggers(
1494
1872
  AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
1495
1873
  AND (observation.payload_json->>'producerAttempt' IS NULL
1496
1874
  OR event.search_doc->>'producerAttempt' = observation.payload_json->>'producerAttempt')
1875
+ AND NOT (event.search_doc ? 'entryId')
1497
1876
  AND event.log_seq IS NOT NULL AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
1498
1877
  ORDER BY event.log_seq DESC
1499
1878
  LIMIT 256
@@ -1503,7 +1882,7 @@ export async function attachRuntimeEventSearchTriggers(
1503
1882
  FROM runtime_observability.events AS event
1504
1883
  WHERE event.runtime_scope = ${quoteLiteral(scope)}
1505
1884
  AND event.org_id = run_org AND event.run_id = observation.run_id
1506
- AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
1885
+ AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended} AND NOT (event.search_doc ? 'entryId') AND (event.log_text_digest IS NOT NULL OR event.log_body IS NOT NULL)
1507
1886
  AND event.event_id = ANY(fresh_event_ids)
1508
1887
  AND (observation.payload_json->>'producerAttempt' IS NULL
1509
1888
  OR event.search_doc->>'producerAttempt' = observation.payload_json->>'producerAttempt')
@@ -1553,7 +1932,9 @@ export async function attachRuntimeEventSearchTriggers(
1553
1932
  RAISE EXCEPTION 'Search projection terminal log context has an unsupported key or value';
1554
1933
  END IF;
1555
1934
  END IF;
1556
- line_doc := jsonb_strip_nulls(jsonb_build_object(
1935
+ line_metadata := ${schema}.run_log_metadata(line #>> '{}', observation.payload_json->'terminalLogMetadata'->((line_ordinal - 1)::int), observation.created_at);
1936
+ line_at := to_timestamp((line_metadata->>'occurredAt')::double precision / 1000);
1937
+ line_doc := (line_metadata - 'occurredAt') || jsonb_strip_nulls(jsonb_build_object(
1557
1938
  'kind', ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, 'source', 'terminal',
1558
1939
  'producerAttempt', observation.payload_json->'producerAttempt'
1559
1940
  ));
@@ -1569,11 +1950,11 @@ export async function attachRuntimeEventSearchTriggers(
1569
1950
  CASE WHEN canonical_events THEN nextval('runtime_observability.runtime_event_order_seq') END,
1570
1951
  CASE WHEN canonical_events THEN jsonb_strip_nulls(jsonb_build_object(
1571
1952
  'type', ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, 'runId', observation.run_id,
1572
- 'occurredAt', floor(extract(epoch FROM observation.created_at) * 1000)::bigint,
1953
+ 'occurredAt', line_metadata->'occurredAt', 'level', line_metadata->'level', 'stepId', line_metadata->'stepId',
1573
1954
  'source', 'terminal', 'producerAttempt', observation.payload_json->'producerAttempt',
1574
1955
  'lineOrdinal', line_ordinal, 'context', line_context
1575
1956
  )) END,
1576
- observation.created_at, observation.run_id, play, ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, NULL,
1957
+ line_at, observation.run_id, play, ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, line_metadata->>'level',
1577
1958
  CASE WHEN compact_logs THEN NULL ELSE left(regexp_replace(line #>> '{}',
1578
1959
  '^' || chr(1) || 'prov:[^' || chr(1) || ']*' || chr(1), ''), 2048) END,
1579
1960
  line #>> '{}',
@@ -1720,7 +2101,7 @@ export async function attachRuntimeEventSearchTriggers(
1720
2101
  LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $function$
1721
2102
  WITH envelope AS (
1722
2103
  SELECT CASE WHEN jsonb_typeof(value) = 'object' THEN
1723
- (value - 'logContexts') || CASE WHEN value ? 'logs'
2104
+ (value - 'logContexts' - 'logMetadata' - 'logIds') || CASE WHEN value ? 'logs'
1724
2105
  THEN jsonb_build_object('logs', '[]'::jsonb) ELSE '{}'::jsonb END
1725
2106
  ELSE value END AS result
1726
2107
  )
@@ -1730,50 +2111,150 @@ export async function attachRuntimeEventSearchTriggers(
1730
2111
  $function$
1731
2112
  `);
1732
2113
  await client.query(`
1733
- CREATE OR REPLACE FUNCTION ${schema}.search_outbox_event() RETURNS trigger
2114
+ CREATE OR REPLACE FUNCTION ${schema}.compact_runtime_outbox_payload(observation ${schema}.outbox) RETURNS jsonb
1734
2115
  LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
1735
2116
  DECLARE compact_logs boolean; canonical_events boolean;
1736
2117
  BEGIN
1737
- -- Project the original validated observation before replacing its
1738
- -- transport body. Failure rolls back the outbox and canonical rows.
1739
- PERFORM ${schema}.project_outbox_event(NEW);
1740
2118
  SELECT run_log_store = 'planet_scale', run_event_store = 'planet_scale'
1741
2119
  INTO compact_logs, canonical_events
1742
- FROM ${schema}.runs WHERE run_id = NEW.run_id;
2120
+ FROM ${schema}.runs WHERE run_id = observation.run_id;
1743
2121
  IF compact_logs OR canonical_events THEN
1744
- NEW.payload_json := NEW.payload_json - 'terminalLogLines' - 'terminalLogContexts';
1745
- IF NEW.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} THEN
1746
- NEW.payload_json := jsonb_set(NEW.payload_json, '{events}',
1747
- ${schema}.without_log_events(NEW.payload_json->'events'))
2122
+ observation.payload_json := observation.payload_json - 'terminalLogLines' - 'terminalLogContexts' - 'terminalLogIds' - 'terminalLogMetadata';
2123
+ IF observation.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} THEN
2124
+ observation.payload_json := jsonb_set(observation.payload_json, '{events}',
2125
+ ${schema}.without_log_events(observation.payload_json->'events'))
1748
2126
  || jsonb_build_object('logBodyStore', 'planet_scale');
1749
2127
  END IF;
1750
- IF NEW.payload_json ? 'terminalProgressEvents' THEN
1751
- NEW.payload_json := jsonb_set(NEW.payload_json, '{terminalProgressEvents}',
1752
- ${schema}.without_log_events(NEW.payload_json->'terminalProgressEvents'));
2128
+ IF observation.payload_json ? 'terminalProgressEvents' THEN
2129
+ observation.payload_json := jsonb_set(observation.payload_json, '{terminalProgressEvents}',
2130
+ ${schema}.without_log_events(observation.payload_json->'terminalProgressEvents'));
1753
2131
  END IF;
1754
- IF NEW.event_type = ${RUN_EVENT_KIND_SQL_LITERAL.runFailed} AND NEW.payload_json ? 'result' THEN
1755
- NEW.payload_json := jsonb_set(NEW.payload_json, '{result}',
1756
- ${schema}.compact_runner_log_envelope(NEW.payload_json->'result'));
2132
+ IF observation.event_type = ${RUN_EVENT_KIND_SQL_LITERAL.runFailed} AND observation.payload_json ? 'result' THEN
2133
+ observation.payload_json := jsonb_set(observation.payload_json, '{result}',
2134
+ ${schema}.compact_runner_log_envelope(observation.payload_json->'result'));
1757
2135
  END IF;
1758
- IF NEW.event_type = ${RUN_EVENT_KIND_SQL_LITERAL.runCompleted} AND NEW.payload_json ? 'output' THEN
1759
- NEW.payload_json := jsonb_set(NEW.payload_json, '{output}',
1760
- ${schema}.without_runtime_log_tail(NEW.payload_json->'output'));
2136
+ IF observation.event_type = ${RUN_EVENT_KIND_SQL_LITERAL.runCompleted} AND observation.payload_json ? 'output' THEN
2137
+ observation.payload_json := jsonb_set(observation.payload_json, '{output}',
2138
+ ${schema}.without_runtime_log_tail(observation.payload_json->'output'));
1761
2139
  END IF;
1762
2140
  END IF;
1763
2141
  IF canonical_events THEN
2142
+ IF observation.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} THEN
2143
+ observation.payload_json := jsonb_set(observation.payload_json, '{events}',
2144
+ ${schema}.convex_projection_events(observation.payload_json->'events'));
2145
+ END IF;
2146
+ IF observation.payload_json ? 'terminalProgressEvents' THEN
2147
+ observation.payload_json := jsonb_set(observation.payload_json, '{terminalProgressEvents}',
2148
+ ${schema}.convex_projection_events(observation.payload_json->'terminalProgressEvents'));
2149
+ END IF;
2150
+ END IF;
2151
+ RETURN observation.payload_json - '${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' - '_runtimeSearchRunClosed';
2152
+ END
2153
+ $function$
2154
+ `);
2155
+ await client.query(`
2156
+ CREATE OR REPLACE FUNCTION ${schema}.search_outbox_event() RETURNS trigger
2157
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
2158
+ DECLARE run_closed boolean; search_payload jsonb;
2159
+ BEGIN
2160
+ IF NEW.payload_json->'${RUNTIME_EVENT_SEARCH_ONLY_FIELD}' = 'true'::jsonb THEN RETURN NEW; END IF;
2161
+ IF ${schema}.defer_runtime_event_projection()
2162
+ AND NEW.event_type <> ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runCreated} THEN
1764
2163
  IF NEW.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} THEN
1765
- NEW.payload_json := jsonb_set(NEW.payload_json, '{events}',
1766
- ${schema}.convex_projection_events(NEW.payload_json->'events'));
2164
+ -- One cheap Run fence captures whether ingress preceded terminal
2165
+ -- closure. No indexed log lookup or search mutex runs under it.
2166
+ SELECT status IN ('completed', 'failed', 'cancelled') INTO run_closed
2167
+ FROM ${schema}.runs WHERE run_id = NEW.run_id FOR SHARE;
2168
+ NEW.created_at := GREATEST(NEW.created_at, clock_timestamp(),
2169
+ coalesce((SELECT max(created_at) + interval '1 microsecond'
2170
+ FROM ${schema}.outbox WHERE run_id = NEW.run_id), clock_timestamp()));
2171
+ ELSE
2172
+ SELECT status IN ('completed', 'failed', 'cancelled') INTO run_closed
2173
+ FROM ${schema}.runs WHERE run_id = NEW.run_id;
1767
2174
  END IF;
1768
- IF NEW.payload_json ? 'terminalProgressEvents' THEN
1769
- NEW.payload_json := jsonb_set(NEW.payload_json, '{terminalProgressEvents}',
1770
- ${schema}.convex_projection_events(NEW.payload_json->'terminalProgressEvents'));
2175
+ NEW.payload_json := NEW.payload_json || jsonb_build_object(
2176
+ '${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}', true,
2177
+ '_runtimeSearchRunClosed', coalesce(run_closed, false));
2178
+ IF ${schema}.independent_runtime_event_projection() THEN
2179
+ IF NEW.event_type IN (${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.receiptEvent}, ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runnerLogs}) OR (NEW.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} AND jsonb_typeof(NEW.payload_json->'events') = 'array' AND NOT EXISTS (SELECT 1 FROM jsonb_array_elements(NEW.payload_json->'events') item WHERE item->>'type' IS DISTINCT FROM ${RUN_EVENT_KIND_SQL_LITERAL.logAppended})) THEN
2180
+ NEW.payload_json := NEW.payload_json || jsonb_build_object('${RUNTIME_EVENT_SEARCH_ONLY_FIELD}', true);
2181
+ ELSE
2182
+ search_payload := NEW.payload_json || jsonb_build_object('${RUNTIME_EVENT_SEARCH_ONLY_FIELD}', true, '_runtimeSearchEventId', NEW.event_id);
2183
+ INSERT INTO ${schema}.outbox(event_id, run_id, event_type, payload_json, created_at)
2184
+ VALUES ('search:' || NEW.event_id, NEW.run_id, NEW.event_type, search_payload, NEW.created_at)
2185
+ ON CONFLICT (event_id) DO NOTHING;
2186
+ NEW.payload_json := ${schema}.compact_runtime_outbox_payload(NEW);
2187
+ END IF;
1771
2188
  END IF;
2189
+ RETURN NEW;
2190
+ END IF;
2191
+ -- Legacy writers retain their atomic projection until explicit cutover.
2192
+ NEW.payload_json := NEW.payload_json - '${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' - '_runtimeSearchRunClosed';
2193
+ PERFORM ${schema}.project_outbox_event(NEW);
2194
+ NEW.payload_json := ${schema}.compact_runtime_outbox_payload(NEW);
2195
+ -- The search rows above are committed atomically with this outbox row.
2196
+ -- When compaction leaves no ledger event to project, settle the no-op
2197
+ -- receipt here so a delayed log-search fact cannot hold up run terminal
2198
+ -- projection behind the scheduler outbox.
2199
+ IF NEW.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents}
2200
+ AND NEW.payload_json->>'logBodyStore' = 'planet_scale'
2201
+ AND jsonb_typeof(NEW.payload_json->'events') = 'array'
2202
+ AND jsonb_array_length(NEW.payload_json->'events') = 0 THEN
2203
+ NEW.delivered_at := coalesce(NEW.delivered_at, clock_timestamp());
1772
2204
  END IF;
1773
2205
  RETURN NEW;
1774
2206
  END
1775
2207
  $function$
1776
2208
  `);
2209
+ await client.query(`
2210
+ CREATE OR REPLACE FUNCTION ${schema}.guard_runtime_outbox_ack() RETURNS trigger
2211
+ LANGUAGE plpgsql SET search_path = pg_catalog AS $function$
2212
+ BEGIN
2213
+ IF OLD.delivered_at IS NULL AND NEW.delivered_at IS NOT NULL
2214
+ AND OLD.payload_json->'${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' = 'true'::jsonb THEN
2215
+ RAISE EXCEPTION 'Deferred outbox input must be projected before acknowledgement' USING ERRCODE = '55000';
2216
+ END IF;
2217
+ RETURN NEW;
2218
+ END
2219
+ $function$
2220
+ `);
2221
+ await client.query(`DO $create$ BEGIN
2222
+ IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.outbox'::regclass
2223
+ AND tgname = 'runtime_outbox_projection_ack_v1') THEN
2224
+ CREATE TRIGGER runtime_outbox_projection_ack_v1 BEFORE UPDATE OF delivered_at ON ${schema}.outbox
2225
+ FOR EACH ROW EXECUTE FUNCTION ${schema}.guard_runtime_outbox_ack();
2226
+ END IF;
2227
+ END $create$`);
2228
+ await client.query(`
2229
+ CREATE OR REPLACE FUNCTION ${schema}.materialize_runtime_outbox_event(eid text, owner text) RETURNS jsonb
2230
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
2231
+ DECLARE observation ${schema}.outbox; projection_observation ${schema}.outbox;
2232
+ BEGIN
2233
+ SELECT * INTO observation FROM ${schema}.outbox
2234
+ WHERE event_id = eid AND lease_owner = owner AND lease_expires_at > clock_timestamp()
2235
+ AND delivered_at IS NULL AND failed_at IS NULL;
2236
+ IF NOT FOUND THEN
2237
+ RAISE EXCEPTION 'Deferred event projection lease is not current' USING ERRCODE = '40001';
2238
+ END IF;
2239
+ IF observation.payload_json->'${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' = 'true'::jsonb THEN
2240
+ projection_observation := observation;
2241
+ projection_observation.event_id := coalesce(observation.payload_json->>'_runtimeSearchEventId', observation.event_id);
2242
+ PERFORM ${schema}.project_outbox_event(projection_observation);
2243
+ observation.payload_json := ${schema}.compact_runtime_outbox_payload(observation);
2244
+ -- Project against immutable input without locking the outbox row.
2245
+ -- Cancellation may transfer its lease while indexing is in flight.
2246
+ -- This final fence rolls the journal back if ownership changed.
2247
+ UPDATE ${schema}.outbox SET payload_json = observation.payload_json
2248
+ WHERE event_id = eid AND lease_owner = owner AND lease_expires_at > clock_timestamp()
2249
+ AND delivered_at IS NULL AND failed_at IS NULL;
2250
+ IF NOT FOUND THEN
2251
+ RAISE EXCEPTION 'Deferred event projection lease is not current' USING ERRCODE = '40001';
2252
+ END IF;
2253
+ END IF;
2254
+ RETURN observation.payload_json;
2255
+ END
2256
+ $function$
2257
+ `);
1777
2258
  await client.query(`
1778
2259
  CREATE OR REPLACE FUNCTION ${schema}.accept_runner_log_payload() RETURNS trigger
1779
2260
  LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $function$
@@ -1803,7 +2284,16 @@ export async function attachRuntimeEventSearchTriggers(
1803
2284
  THEN NEW.runner_log_payload_json->'terminalProgressEvents'
1804
2285
  ELSE '[]'::jsonb END) WITH ORDINALITY AS progress(event, position)
1805
2286
  ));
1806
- PERFORM ${schema}.project_outbox_event(observation);
2287
+ IF ${schema}.defer_runtime_event_projection() THEN
2288
+ INSERT INTO ${schema}.outbox (event_id, run_id, event_type, payload_json, created_at)
2289
+ VALUES (observation.event_id, observation.run_id, observation.event_type,
2290
+ observation.payload_json,
2291
+ GREATEST(clock_timestamp(), coalesce((SELECT max(created_at) + interval '1 microsecond'
2292
+ FROM ${schema}.outbox WHERE run_id = NEW.run_id), clock_timestamp())))
2293
+ ON CONFLICT (event_id) DO NOTHING;
2294
+ ELSE
2295
+ PERFORM ${schema}.project_outbox_event(observation);
2296
+ END IF;
1807
2297
  NEW.runner_result_json := ${schema}.compact_runner_log_envelope(NEW.runner_result_json);
1808
2298
  IF jsonb_array_length(observation.payload_json->'recoveredRuntimeDiagnostics') > 0
1809
2299
  AND (NOT (NEW.runner_result_json ? 'runtimeDiagnostics')
@@ -1888,10 +2378,16 @@ export async function attachRuntimeEventSearchTriggers(
1888
2378
  END IF;
1889
2379
  END $create$`);
1890
2380
  await client.query(`REVOKE ALL ON FUNCTION ${schema}.project_outbox_event(${schema}.outbox),
2381
+ ${schema}.project_origin_log_batch(${schema}.outbox, jsonb, text, text, boolean, boolean, timestamptz, jsonb, boolean),
2382
+ ${schema}.independent_runtime_event_projection(),
2383
+ ${schema}.pin_runtime_log_identity(),
2384
+ ${schema}.materialize_runtime_outbox_event(text, text),
2385
+ ${schema}.guard_runtime_outbox_ack(),
2386
+ ${schema}.compact_runtime_outbox_payload(${schema}.outbox),
1891
2387
  ${schema}.accept_runner_log_payload(), ${schema}.search_outbox_event(),
1892
2388
  ${schema}.compact_run_log_result(), ${schema}.without_log_events(jsonb),
1893
2389
  ${schema}.convex_projection_event(jsonb), ${schema}.convex_projection_events(jsonb),
1894
- ${schema}.compact_runner_log_envelope(jsonb), ${schema}.without_runtime_log_tail(jsonb)
2390
+ ${schema}.run_log_metadata(text, jsonb, timestamptz), ${schema}.compact_runner_log_envelope(jsonb), ${schema}.without_runtime_log_tail(jsonb)
1895
2391
  FROM PUBLIC`);
1896
2392
  // Publish readiness last, after all acceptance/compaction functions exist.
1897
2393
  await client.query(
@@ -1905,7 +2401,11 @@ export async function hasRuntimeEventSearchShape(
1905
2401
  ): Promise<boolean> {
1906
2402
  const schema = quoteIdent(requiredScope(runtimeScope));
1907
2403
  const result = await client.query<{ ready: boolean }>(
1908
- `SELECT to_regclass('runtime_observability.events') IS NOT NULL
2404
+ `SELECT to_regprocedure('${schema}.materialize_runtime_outbox_event(text,text)') IS NOT NULL
2405
+ AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('${schema}.outbox')
2406
+ AND tgname = 'runtime_outbox_projection_ack_v1' AND tgenabled <> 'D')
2407
+ AND to_regprocedure('${schema}.defer_runtime_event_projection()') IS NOT NULL
2408
+ AND to_regclass('runtime_observability.events') IS NOT NULL
1909
2409
  AND to_regclass('runtime_observability.run_log_cursors') IS NOT NULL
1910
2410
  AND EXISTS (SELECT 1 FROM pg_attribute
1911
2411
  WHERE attrelid = to_regclass('runtime_observability.events')
@@ -2345,6 +2845,7 @@ export async function pruneRuntimeEvents(
2345
2845
  search_doc = jsonb_strip_nulls(jsonb_build_object(
2346
2846
  'kind', ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}, 'source', event.search_doc->'source',
2347
2847
  'producerAttempt', event.search_doc->'producerAttempt',
2848
+ 'entryId', event.search_doc->'entryId',
2348
2849
  'logPositions', event.search_doc->'logPositions',
2349
2850
  'logContentDigest', event.search_doc->'logContentDigest'))
2350
2851
  FROM candidates WHERE event.ctid = candidates.ctid AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}