deepline 0.3.306 → 0.3.308

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 (104) hide show
  1. package/README.md +2 -1
  2. package/dist/bundling-sources/sdk/src/client.ts +270 -11
  3. package/dist/bundling-sources/sdk/src/compat.ts +27 -9
  4. package/dist/bundling-sources/sdk/src/index.ts +35 -0
  5. package/dist/bundling-sources/sdk/src/release.ts +1 -1
  6. package/dist/bundling-sources/shared_libs/play-runtime/batch-runtime.ts +4 -0
  7. package/dist/bundling-sources/shared_libs/play-runtime/batching-types.ts +3 -0
  8. package/dist/bundling-sources/shared_libs/play-runtime/context.ts +46 -16
  9. package/dist/bundling-sources/shared_libs/play-runtime/fullenrich-batching.ts +24 -1
  10. package/dist/bundling-sources/shared_libs/play-runtime/run-event-query.ts +872 -0
  11. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-shared.ts +10 -5
  12. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +4 -1
  13. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +720 -19
  14. package/dist/cli/auth-LWNR7TQB.js +31 -0
  15. package/dist/cli/{auth-WTC7LCQ6.mjs → auth-ZWX4BF5K.mjs} +4 -4
  16. package/dist/cli/{billing-VQM65LVP.mjs → billing-CLV5QRFQ.mjs} +4 -4
  17. package/dist/cli/{billing-3DYTHO32.js → billing-H24FGAKE.js} +70 -70
  18. package/dist/cli/{chunk-WV64SNDV.mjs → chunk-3GIUCJAT.mjs} +1 -1
  19. package/dist/cli/{chunk-5V56XCQN.mjs → chunk-ABMGEW3C.mjs} +1 -1
  20. package/dist/cli/{chunk-UT3VLSLM.mjs → chunk-ASWZTTKT.mjs} +13 -5
  21. package/dist/cli/{chunk-FJ3DANI4.mjs → chunk-BPD7TR27.mjs} +1 -1
  22. package/dist/cli/{chunk-7XORS6WQ.mjs → chunk-CXU7ME5X.mjs} +2 -2
  23. package/dist/cli/{chunk-RODIZCGU.js → chunk-GBFIAI4L.js} +68 -60
  24. package/dist/cli/{chunk-OZIFVQRY.js → chunk-GONK2N7U.js} +1 -1
  25. package/dist/cli/{chunk-LLGF6V6I.mjs → chunk-HGI6RGHB.mjs} +653 -5
  26. package/dist/cli/{chunk-FZNO6B4W.js → chunk-IFIGQWHC.js} +6 -6
  27. package/dist/cli/{chunk-6CRUYVU7.mjs → chunk-JUAVGRD7.mjs} +4 -4
  28. package/dist/cli/{chunk-SZQPJW2L.js → chunk-O7NNH76S.js} +6 -6
  29. package/dist/cli/{chunk-2IKIG333.js → chunk-QJBJZVG6.js} +15 -4
  30. package/dist/cli/{chunk-M6HCRRV2.js → chunk-QTCFSRGC.js} +4 -4
  31. package/dist/cli/{chunk-W7ELTWDM.js → chunk-SRZ6XDX3.js} +26 -26
  32. package/dist/cli/{chunk-4PSSDZQB.mjs → chunk-T3P4NRZU.mjs} +1 -1
  33. package/dist/cli/{chunk-PHNAFCSB.js → chunk-TH3YBIJT.js} +852 -204
  34. package/dist/cli/{chunk-U37SW6IK.js → chunk-UGTYNMRX.js} +48 -48
  35. package/dist/cli/{chunk-2KR56MPF.js → chunk-WEZVPZPE.js} +2 -2
  36. package/dist/cli/{chunk-UABLQKTZ.mjs → chunk-WXLOW623.mjs} +11 -0
  37. package/dist/cli/{chunk-QDC4XTO3.mjs → chunk-XFYVL7EM.mjs} +2 -2
  38. package/dist/cli/{chunk-5HS5EEFE.mjs → chunk-XSDL3NHZ.mjs} +3 -3
  39. package/dist/cli/{chunk-MMNZ6GBT.js → chunk-YEJMVAD3.js} +6 -6
  40. package/dist/cli/{chunk-WWPI4GJI.mjs → chunk-YSZSERRZ.mjs} +1 -1
  41. package/dist/cli/{chunk-OF7QP2XX.js → chunk-ZHHF24O5.js} +15 -15
  42. package/dist/cli/client-3J76AMVE.js +20 -0
  43. package/dist/cli/{client-DCEK34D5.mjs → client-A5ILG5GT.mjs} +3 -3
  44. package/dist/cli/{csv-LXBF5UIV.mjs → csv-NZ7KAYEF.mjs} +3 -3
  45. package/dist/cli/{csv-H42JDJ6G.js → csv-VSSAAXFX.js} +12 -12
  46. package/dist/cli/{db-DP5GU4MU.js → db-ICLEBSNO.js} +20 -20
  47. package/dist/cli/{db-3O5CFLNP.mjs → db-YSAJUBLD.mjs} +4 -4
  48. package/dist/cli/{executions-QC2UZKIP.mjs → executions-MUJRAT5R.mjs} +4 -4
  49. package/dist/cli/{executions-5FFTPHEX.js → executions-XCAJB5TF.js} +6 -6
  50. package/dist/cli/{feedback-JCGE6JFW.mjs → feedback-6C7Y2SCJ.mjs} +3 -3
  51. package/dist/cli/{feedback-XAU3FI5S.js → feedback-DBZMBHZG.js} +6 -6
  52. package/dist/cli/git-2NSE4WIK.js +23 -0
  53. package/dist/cli/{git-S3Y3E2NL.mjs → git-EAH67ITA.mjs} +4 -4
  54. package/dist/cli/index.js +79 -77
  55. package/dist/cli/index.mjs +33 -31
  56. package/dist/cli/{legacy-noops-TXOUX4FX.js → legacy-noops-EJTOLTVO.js} +7 -7
  57. package/dist/cli/{legacy-noops-FQF2IRVS.mjs → legacy-noops-O5INQP6Q.mjs} +4 -4
  58. package/dist/cli/{monitors-4BQCO6B7.js → monitors-5OYO2DPJ.js} +85 -85
  59. package/dist/cli/{monitors-F3PELPSH.mjs → monitors-SBBBMUFY.mjs} +4 -4
  60. package/dist/cli/{org-VQM6OOLK.js → org-SJRPJG42.js} +60 -60
  61. package/dist/cli/{org-ER5J3ATG.mjs → org-Z7LTOL3H.mjs} +4 -4
  62. package/dist/cli/{play-B5H2RN4N.mjs → play-2JTIEOZM.mjs} +472 -55
  63. package/dist/cli/{play-WKZPAOW3.js → play-VQJ6ABZ2.js} +961 -544
  64. package/dist/cli/{providers-5TEA2FJG.js → providers-43MR5XOV.js} +7 -7
  65. package/dist/cli/{providers-6ODEAYC2.mjs → providers-MX4CV2EH.mjs} +4 -4
  66. package/dist/cli/{quickstart-GJYCNMFC.js → quickstart-A6W3HEE2.js} +5 -5
  67. package/dist/cli/{quickstart-BCYT7BCY.mjs → quickstart-Y4YLKRZ3.mjs} +3 -3
  68. package/dist/cli/{search-KKJ535OX.js → search-AMUFSAFP.js} +6 -6
  69. package/dist/cli/{search-RY547KOT.mjs → search-FRKDLJQG.mjs} +5 -5
  70. package/dist/cli/{secrets-U42ZEYIU.mjs → secrets-E4EX7O3J.mjs} +4 -4
  71. package/dist/cli/{secrets-RFGDXSEW.js → secrets-XITHKZW2.js} +13 -13
  72. package/dist/cli/{sessions-ZRTWHMIH.js → sessions-6HJFDIR2.js} +5 -5
  73. package/dist/cli/{sessions-YWEFB62D.mjs → sessions-6YXAUJII.mjs} +4 -4
  74. package/dist/cli/{settings-XUWFXIL7.js → settings-J5NRLCV2.js} +74 -74
  75. package/dist/cli/{settings-AS4U6NVP.mjs → settings-LBJSZIET.mjs} +7 -7
  76. package/dist/cli/{setup-2QPWP2FI.js → setup-2ES4CTO5.js} +41 -41
  77. package/dist/cli/{setup-2GUIV2EW.mjs → setup-75FENNVH.mjs} +6 -6
  78. package/dist/cli/skills-VLTAVA4K.js +28 -0
  79. package/dist/cli/{skills-5QS7ERVF.mjs → skills-XGZDUXW6.mjs} +4 -4
  80. package/dist/cli/{tools-PVWSGCZD.mjs → tools-2LEKXOGY.mjs} +58 -42
  81. package/dist/cli/{tools-J47LIO2H.js → tools-ZOQOGDKI.js} +111 -95
  82. package/dist/cli/update-5CZOVYQT.js +28 -0
  83. package/dist/cli/{update-KS4TRSZI.mjs → update-SNK3IN3I.mjs} +6 -6
  84. package/dist/cli/{workflow-QCLR7GVC.mjs → workflow-G7ZM3MAC.mjs} +5 -5
  85. package/dist/cli/{workflow-HQRUUTTL.js → workflow-W3AYTB3J.js} +15 -15
  86. package/dist/{client-Dj8MAkDc.d.mts → client-DpnkGBn-.d.mts} +324 -11
  87. package/dist/{client-DsrFGXJf.d.ts → client-ypdBvf1o.d.ts} +324 -11
  88. package/dist/index.d.mts +2 -2
  89. package/dist/index.d.ts +2 -2
  90. package/dist/index.js +8 -6
  91. package/dist/index.mjs +5 -3
  92. package/dist/install-integrity.json +73 -72
  93. package/dist/plays/run-request.d.mts +1 -1
  94. package/dist/plays/run-request.d.ts +1 -1
  95. package/dist/release.d.mts +1 -1
  96. package/dist/release.d.ts +1 -1
  97. package/dist/release.js +2 -2
  98. package/dist/release.mjs +1 -1
  99. package/package.json +1 -1
  100. package/dist/cli/auth-ZT3UF33R.js +0 -31
  101. package/dist/cli/client-UQXVU2T6.js +0 -20
  102. package/dist/cli/git-CNO2UFZE.js +0 -23
  103. package/dist/cli/skills-4E5KYW2Y.js +0 -28
  104. package/dist/cli/update-GZKGOMKD.js +0 -28
@@ -11,6 +11,31 @@
11
11
  * The API uses only the fixed, tenant-scoped readers in this module.
12
12
  */
13
13
  import { normalizeRuntimeSchedulerSchema } from '../runtime-scheduler-topology';
14
+ import { createHash } from 'node:crypto';
15
+ import {
16
+ getRunEventQueryFields,
17
+ getRunEventQueryPredicate,
18
+ normalizeRunEventOperationRequest,
19
+ normalizeRunEventQuery,
20
+ normalizeRunEventQueryTimestamp,
21
+ RunEventQueryInputError,
22
+ RunEventQueryDataError,
23
+ RUN_EVENT_QUERY_RETENTION_MS,
24
+ RUN_EVENT_QUERY_MAX_CURSOR_BYTES,
25
+ RUN_EVENT_QUERY_MAX_REQUEST_BYTES,
26
+ type NormalizedRunEventQuery,
27
+ type RunEventOperationRequest,
28
+ type RunEventOperationResult,
29
+ type RunEventQueryEvent,
30
+ type RunEventField,
31
+ type RunEventPredicate,
32
+ type RunEventGroupValue,
33
+ type RunEventScalar,
34
+ type RunEventQueryPage,
35
+ type RunEventQueryRetention,
36
+ type RunEventSummaryResult,
37
+ } from '../run-event-query';
38
+ import { RUN_LOG_LEVELS, type RunLogLevel } from '../run-event-catalog';
14
39
  import type { PostgresSchedulerQueryClient } from './postgres';
15
40
  import {
16
41
  RUN_EVENT_CONVEX_INTERNAL_KIND_SQL_LITERAL,
@@ -61,7 +86,7 @@ const RUNTIME_EVENT_LOG_POSITION_EXPRESSION = `jsonb_set(
61
86
  // Migration readiness tracks this projection body independently of scheduler
62
87
  // wire and schema versions. Bump when a changed trigger must be reinstalled.
63
88
  export const RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER =
64
- 'runtime-event-search-origin-batches-log-metadata-point-replay';
89
+ 'runtime-event-search-origin-batches-new-run-cutover-fresh-origin';
65
90
  /** Trusted trigger-authored state; absent on already-projected legacy rows. */
66
91
  export const RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD =
67
92
  '_runtimeSearchProjectionPending';
@@ -464,6 +489,576 @@ export async function searchRuntimeEvents(
464
489
  };
465
490
  }
466
491
 
492
+ type RuntimeRunEventReadEvent = RunEventQueryEvent & {
493
+ receiptRef: string | null;
494
+ };
495
+ type RunEventQueryRow = EventRow & { cursor_occurred_at: string };
496
+ export type RuntimeRunEventQueryReadResult =
497
+ | Exclude<RunEventOperationResult, { logs: unknown } | { events: unknown }>
498
+ | {
499
+ logs: RuntimeRunEventReadEvent[];
500
+ page: RunEventQueryPage;
501
+ query: NormalizedRunEventQuery;
502
+ retention: RunEventQueryRetention;
503
+ }
504
+ | {
505
+ events: RuntimeRunEventReadEvent[];
506
+ page: RunEventQueryPage;
507
+ query: NormalizedRunEventQuery;
508
+ retention: RunEventQueryRetention;
509
+ };
510
+ type QueryCursor = {
511
+ format: 'run-event-query';
512
+ operation: 'list' | 'summarize';
513
+ fingerprint: string;
514
+ query: NormalizedRunEventQuery;
515
+ position: { occurredAt: string; eventId: string } | RunEventGroupValue[];
516
+ };
517
+ function invalidQueryCursor(): never {
518
+ throw new RunEventQueryInputError(
519
+ 'Run event cursor does not match this scope, query, resource or projection. Start a new query without cursor.',
520
+ 'invalid_run_event_cursor',
521
+ );
522
+ }
523
+ function decodeQueryCursor(value: string): QueryCursor {
524
+ try {
525
+ if (
526
+ !/^[A-Za-z0-9_-]+$/.test(value) ||
527
+ value.length > RUN_EVENT_QUERY_MAX_CURSOR_BYTES
528
+ )
529
+ return invalidQueryCursor();
530
+ const decoded = JSON.parse(
531
+ Buffer.from(value, 'base64url').toString('utf8'),
532
+ ) as QueryCursor;
533
+ if (
534
+ decoded.format !== 'run-event-query' ||
535
+ !['list', 'summarize'].includes(decoded.operation) ||
536
+ typeof decoded.fingerprint !== 'string' ||
537
+ !/^[a-f0-9]{64}$/.test(decoded.fingerprint) ||
538
+ !decoded.query ||
539
+ !decoded.position
540
+ )
541
+ return invalidQueryCursor();
542
+ decoded.query = normalizeRunEventQuery(decoded.query);
543
+ return decoded;
544
+ } catch {
545
+ return invalidQueryCursor();
546
+ }
547
+ }
548
+ function queryFingerprint(
549
+ scope: string,
550
+ org: string,
551
+ request: RunEventOperationRequest,
552
+ query: NormalizedRunEventQuery,
553
+ ): string {
554
+ return createHash('sha256')
555
+ .update(
556
+ JSON.stringify({
557
+ scope,
558
+ org,
559
+ resource: request.resource,
560
+ runId: request.runId ?? query.runId ?? null,
561
+ operation: request.operation,
562
+ query,
563
+ order:
564
+ request.operation === 'summarize'
565
+ ? 'group_key:asc'
566
+ : 'occurred_at:event_id:desc',
567
+ projection:
568
+ request.operation === 'list' && request.includePayloads === true,
569
+ groupBy:
570
+ request.operation === 'summarize' ? request.groupBy : undefined,
571
+ measures:
572
+ request.operation === 'summarize' ? request.measures : undefined,
573
+ }),
574
+ )
575
+ .digest('hex');
576
+ }
577
+ function queryFieldSql(field: RunEventField, params: unknown[]): string {
578
+ const columns: Record<string, string> = {
579
+ eventId: 'event_id',
580
+ orgId: 'org_id',
581
+ runtimeScope: 'runtime_scope',
582
+ runId: 'run_id',
583
+ playName: 'play_name',
584
+ kind: 'kind',
585
+ };
586
+ if (columns[field]) return `to_jsonb(${columns[field]})`;
587
+ if (field === 'occurredAt') return `to_jsonb(${queryTimestampSql()})`;
588
+ if (field === 'level') return 'to_jsonb(level)';
589
+ params.push(
590
+ field.startsWith('context.') ? ['context', field.slice(8)] : [field],
591
+ );
592
+ return `search_doc #> $${params.length}::text[]`;
593
+ }
594
+ function queryTimestampSql(): string {
595
+ return `to_char(occurred_at AT TIME ZONE 'UTC', CASE
596
+ WHEN mod(extract(microseconds FROM occurred_at)::bigint, 1000) = 0
597
+ THEN 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
598
+ ELSE 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"' END)`;
599
+ }
600
+ const QUERY_DIRECT_COLUMNS: Partial<Record<RunEventField, string>> = {
601
+ eventId: 'event_id',
602
+ orgId: 'org_id',
603
+ runtimeScope: 'runtime_scope',
604
+ runId: 'run_id',
605
+ playName: 'play_name',
606
+ kind: 'kind',
607
+ occurredAt: 'occurred_at',
608
+ level: 'level',
609
+ };
610
+ function queryLogBodySql(): string {
611
+ return `CASE WHEN kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
612
+ AND search_doc->>'source' = 'terminal' THEN
613
+ regexp_replace(coalesce(log_body, message),
614
+ '^' || chr(1) || 'prov:[^' || chr(1) || ']*' || chr(1), '')
615
+ ELSE coalesce(log_body, message) END`;
616
+ }
617
+ function queryPredicateSql(
618
+ predicate: RunEventPredicate,
619
+ params: unknown[],
620
+ ): string {
621
+ if ('and' in predicate)
622
+ return `(${predicate.and.map((term) => queryPredicateSql(term, params)).join(' AND ')})`;
623
+ if ('or' in predicate)
624
+ return `(${predicate.or.map((term) => queryPredicateSql(term, params)).join(' OR ')})`;
625
+ const column = QUERY_DIRECT_COLUMNS[predicate.field];
626
+ if (column) {
627
+ if (
628
+ predicate.field === 'level' &&
629
+ ['gt', 'gte', 'lt', 'lte'].includes(predicate.op)
630
+ ) {
631
+ const index = RUN_LOG_LEVELS.indexOf(predicate.value as RunLogLevel);
632
+ const levels =
633
+ predicate.op === 'gt'
634
+ ? RUN_LOG_LEVELS.slice(index + 1)
635
+ : predicate.op === 'gte'
636
+ ? RUN_LOG_LEVELS.slice(index)
637
+ : predicate.op === 'lt'
638
+ ? RUN_LOG_LEVELS.slice(0, index)
639
+ : RUN_LOG_LEVELS.slice(0, index + 1);
640
+ params.push(levels);
641
+ return `(${column} = ANY($${params.length}::text[]))`;
642
+ }
643
+ if (predicate.op === 'exists')
644
+ return `(${column} IS ${predicate.value ? 'NOT ' : ''}NULL)`;
645
+ const bind = (value: RunEventScalar) => {
646
+ params.push(value);
647
+ return `$${params.length}${predicate.field === 'occurredAt' ? '::timestamptz' : ''}`;
648
+ };
649
+ if (predicate.op === 'in') {
650
+ const values = predicate.value
651
+ .filter((value) => value !== null)
652
+ .map(bind);
653
+ const terms = values.length
654
+ ? [`${column} IN (${values.join(', ')})`]
655
+ : [];
656
+ if (predicate.value.includes(null)) terms.push(`${column} IS NULL`);
657
+ return `(${terms.join(' OR ')})`;
658
+ }
659
+ if (predicate.value === null)
660
+ return `(${column} IS ${predicate.op === 'ne' ? 'NOT ' : ''}NULL)`;
661
+ const operator = {
662
+ eq: '=',
663
+ ne: '<>',
664
+ gt: '>',
665
+ gte: '>=',
666
+ lt: '<',
667
+ lte: '<=',
668
+ }[predicate.op];
669
+ return `(${column} ${operator} ${bind(predicate.value)})`;
670
+ }
671
+ const field = queryFieldSql(predicate.field, params);
672
+ if (predicate.op === 'exists')
673
+ return `(${field} IS ${predicate.value ? 'NOT ' : ''}NULL)`;
674
+ if (predicate.op === 'in') {
675
+ const values = predicate.value.map((value) => {
676
+ params.push(JSON.stringify(value));
677
+ return `$${params.length}::jsonb`;
678
+ });
679
+ return `(${field} IN (${values.join(', ')}))`;
680
+ }
681
+ params.push(JSON.stringify(predicate.value));
682
+ const value = `$${params.length}::jsonb`;
683
+ const operator = {
684
+ eq: '=',
685
+ ne: '<>',
686
+ gt: '>',
687
+ gte: '>=',
688
+ lt: '<',
689
+ lte: '<=',
690
+ }[predicate.op];
691
+ if (predicate.op === 'eq' || predicate.op === 'ne')
692
+ return `(${field} IS NOT NULL AND ${field} ${operator} ${value})`;
693
+ return `(jsonb_typeof(${field}) = '${typeof predicate.value === 'number' ? 'number' : 'string'}' AND ${field} ${operator} ${value})`;
694
+ }
695
+ function queryScopeSql(
696
+ scope: string,
697
+ org: string,
698
+ request: RunEventOperationRequest,
699
+ query: NormalizedRunEventQuery,
700
+ ): { params: unknown[]; where: string } {
701
+ const params: unknown[] = [scope, org, query.since, query.until];
702
+ // Trusted admission, tenant, runtime and retention predicates always enclose
703
+ // the complete user expression. An OR can never widen these constraints.
704
+ const predicates = [
705
+ 'runtime_scope = $1',
706
+ 'org_id = $2',
707
+ 'occurred_at >= $3::timestamptz',
708
+ 'occurred_at < $4::timestamptz',
709
+ "ingested_at >= clock_timestamp() - interval '7 days'",
710
+ ];
711
+ const runId = request.runId ?? query.runId;
712
+ if (runId !== undefined) {
713
+ params.push(runId);
714
+ predicates.push(`run_id = $${params.length}`);
715
+ }
716
+ if (request.resource === 'logs') {
717
+ predicates.push(`kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}`);
718
+ predicates.push('(log_body IS NOT NULL OR message IS NOT NULL)');
719
+ }
720
+ for (const [field, value] of [
721
+ ['play_name', query.playName],
722
+ ['kind', query.kind],
723
+ ] as const) {
724
+ if (value !== undefined) {
725
+ params.push(value);
726
+ predicates.push(`${field} = $${params.length}`);
727
+ }
728
+ }
729
+ if (query.stepId !== undefined) {
730
+ params.push(query.stepId);
731
+ predicates.push(`search_doc->'stepId' = to_jsonb($${params.length}::text)`);
732
+ }
733
+ if (query.minLevel !== undefined) {
734
+ params.push(RUN_LOG_LEVELS.slice(RUN_LOG_LEVELS.indexOf(query.minLevel)));
735
+ predicates.push(`level = ANY($${params.length}::text[])`);
736
+ }
737
+ if (query.text !== undefined) {
738
+ // Escape LIKE metacharacters: this is a literal substring search, never a
739
+ // user-supplied wildcard, regex or a search over a shortened response tail.
740
+ params.push(`%${query.text.replace(/[\\%_]/g, '\\$&')}%`);
741
+ predicates.push(
742
+ `coalesce((${queryLogBodySql()}), '') ILIKE $${params.length} ESCAPE E'\\\\'`,
743
+ );
744
+ }
745
+ if (query.where)
746
+ predicates.push(
747
+ queryPredicateSql(getRunEventQueryPredicate(query.where), params),
748
+ );
749
+ return { params, where: predicates.join(' AND ') };
750
+ }
751
+ function exactQueryNumber(value: unknown, label: string): number {
752
+ const number = Number(value);
753
+ // SQL sums are exact numeric values. Public fractional results use JS number
754
+ // precision; integral results must never silently lose integer precision.
755
+ if (
756
+ !Number.isFinite(number) ||
757
+ (Number.isInteger(number) && !Number.isSafeInteger(number)) ||
758
+ (label === 'count' && (!Number.isSafeInteger(number) || number < 0))
759
+ )
760
+ throw new RunEventQueryDataError(
761
+ `Run event ${label} cannot be represented exactly in the response.`,
762
+ );
763
+ return number;
764
+ }
765
+ function flatQueryEvent(row: RunEventQueryRow): RuntimeRunEventReadEvent {
766
+ const document = row.search_doc;
767
+ const metadata: Record<string, unknown> = {};
768
+ for (const field of [
769
+ 'source',
770
+ 'stepId',
771
+ 'status',
772
+ 'stepKind',
773
+ 'datasetId',
774
+ 'phase',
775
+ 'provider',
776
+ 'operation',
777
+ ] as const) {
778
+ if (typeof document[field] === 'string') metadata[field] = document[field];
779
+ }
780
+ if (typeof document.producerAttempt === 'number')
781
+ metadata.producerAttempt = document.producerAttempt;
782
+ if (document.context !== undefined) {
783
+ if (
784
+ document.context === null ||
785
+ typeof document.context !== 'object' ||
786
+ Array.isArray(document.context) ||
787
+ Object.values(document.context).some(
788
+ (value) =>
789
+ value !== null &&
790
+ !['string', 'number', 'boolean'].includes(typeof value),
791
+ )
792
+ )
793
+ throw new RunEventQueryDataError(
794
+ 'Run event retained context is not a flat scalar object.',
795
+ );
796
+ metadata.context = document.context;
797
+ }
798
+ if (row.level !== null && !RUN_LOG_LEVELS.includes(row.level as RunLogLevel))
799
+ throw new RunEventQueryDataError(
800
+ 'Run event retained level is unsupported.',
801
+ );
802
+ if (
803
+ row.event_payload !== null &&
804
+ row.event_payload !== undefined &&
805
+ (typeof row.event_payload !== 'object' || Array.isArray(row.event_payload))
806
+ )
807
+ throw new RunEventQueryDataError(
808
+ 'Runtime canonical event payload is not a JSON object.',
809
+ );
810
+ return {
811
+ ...metadata,
812
+ eventId: row.event_id,
813
+ occurredAt: normalizeRunEventQueryTimestamp(row.cursor_occurred_at),
814
+ ingestedAt: iso(row.ingested_at),
815
+ runId: row.run_id,
816
+ playName: row.play_name,
817
+ kind: row.kind,
818
+ level: row.level as RunLogLevel | null,
819
+ message: row.message,
820
+ receiptRef: row.receipt_ref,
821
+ ...(row.event_payload == null
822
+ ? {}
823
+ : { eventPayload: row.event_payload as Record<string, unknown> }),
824
+ };
825
+ }
826
+
827
+ /** Reads through the existing restricted reader transaction and 3000ms budget. */
828
+ export async function executeRuntimeEventQuery(
829
+ client: PostgresSchedulerQueryClient,
830
+ input: {
831
+ runtimeScope: string;
832
+ orgId: string;
833
+ request: RunEventOperationRequest;
834
+ },
835
+ ): Promise<RuntimeRunEventQueryReadResult> {
836
+ const scope = requiredScope(input.runtimeScope);
837
+ const org = requiredOrg(input.orgId);
838
+ const request = normalizeRunEventOperationRequest(input.request);
839
+ if (request.operation === 'fields') return getRunEventQueryFields();
840
+ const cursor =
841
+ 'cursor' in request && request.cursor
842
+ ? decodeQueryCursor(request.cursor)
843
+ : undefined;
844
+ const criteria = Object.fromEntries(
845
+ Object.entries(request).filter(([key]) =>
846
+ [
847
+ 'since',
848
+ 'until',
849
+ 'where',
850
+ 'stepId',
851
+ 'minLevel',
852
+ 'text',
853
+ 'playName',
854
+ 'kind',
855
+ ...(request.resource === 'events' ? ['runId'] : []),
856
+ ].includes(key),
857
+ ),
858
+ );
859
+ // All omitted criteria are inherited from the cursor's admitted query. Any
860
+ // supplied criterion must normalize to exactly the original canonical fact.
861
+ const query = normalizeRunEventQuery({ ...cursor?.query, ...criteria });
862
+ const fingerprint = queryFingerprint(scope, org, request, query);
863
+ if (
864
+ cursor &&
865
+ (cursor.operation !== request.operation ||
866
+ cursor.fingerprint !== fingerprint)
867
+ )
868
+ invalidQueryCursor();
869
+ const retention = {
870
+ retainedSince: new Date(
871
+ Date.now() - RUN_EVENT_QUERY_RETENTION_MS,
872
+ ).toISOString(),
873
+ retentionDays: 7 as const,
874
+ };
875
+ const base = { query, retention };
876
+ const sql = queryScopeSql(scope, org, request, query);
877
+ if (request.operation === 'count') {
878
+ const result = await client.query<{ matched_count: string }>(
879
+ `SELECT count(*)::text AS matched_count FROM ${EVENTS_TABLE} WHERE ${sql.where}`,
880
+ sql.params,
881
+ );
882
+ return {
883
+ ...base,
884
+ matchedCount: exactQueryNumber(result.rows[0]?.matched_count, 'count'),
885
+ exact: true,
886
+ };
887
+ }
888
+ const limit = request.limit!;
889
+ const encode = (position: QueryCursor['position']) => {
890
+ const encoded = Buffer.from(
891
+ JSON.stringify({
892
+ format: 'run-event-query',
893
+ operation: request.operation,
894
+ fingerprint,
895
+ query,
896
+ position,
897
+ }),
898
+ ).toString('base64url');
899
+ const continuation = JSON.stringify({
900
+ ...request,
901
+ ...query,
902
+ cursor: encoded,
903
+ });
904
+ if (
905
+ encoded.length > RUN_EVENT_QUERY_MAX_CURSOR_BYTES ||
906
+ Buffer.byteLength(continuation, 'utf8') >
907
+ RUN_EVENT_QUERY_MAX_REQUEST_BYTES
908
+ ) {
909
+ throw new RunEventQueryDataError(
910
+ 'Run event retained pagination key exceeds the continuation contract.',
911
+ );
912
+ }
913
+ return encoded;
914
+ };
915
+ if (request.operation === 'list') {
916
+ if (cursor) {
917
+ const position = cursor.position as RuntimeSearchCursor;
918
+ if (
919
+ Array.isArray(position) ||
920
+ typeof position.occurredAt !== 'string' ||
921
+ typeof position.eventId !== 'string' ||
922
+ !position.eventId ||
923
+ position.eventId.length > 512 ||
924
+ !Number.isFinite(Date.parse(position.occurredAt))
925
+ )
926
+ invalidQueryCursor();
927
+ sql.params.push(position.occurredAt, position.eventId);
928
+ sql.where += ` AND (occurred_at, event_id) < ($${sql.params.length - 1}::timestamptz, $${sql.params.length})`;
929
+ }
930
+ sql.params.push(limit + 1, request.includePayloads === true);
931
+ const result = await client.query<RunEventQueryRow>(
932
+ `SELECT event_id, occurred_at, ingested_at, run_id, play_name, kind, level,
933
+ ${queryTimestampSql()} AS cursor_occurred_at,
934
+ ${queryLogBodySql()} AS message, receipt_ref, search_doc,
935
+ CASE WHEN $${sql.params.length}::boolean THEN event_payload END AS event_payload
936
+ FROM ${EVENTS_TABLE} WHERE ${sql.where}
937
+ ORDER BY occurred_at DESC, event_id DESC LIMIT $${sql.params.length - 1}::integer`,
938
+ sql.params,
939
+ );
940
+ const hasMore = result.rows.length > limit;
941
+ const events = result.rows.slice(0, limit).map(flatQueryEvent);
942
+ const last = result.rows[Math.min(result.rows.length, limit) - 1];
943
+ const page = {
944
+ returnedCount: events.length,
945
+ hasMore,
946
+ nextCursor:
947
+ hasMore && last
948
+ ? encode({
949
+ occurredAt: last.cursor_occurred_at,
950
+ eventId: last.event_id,
951
+ })
952
+ : null,
953
+ };
954
+ return request.resource === 'logs'
955
+ ? { ...base, logs: events, page }
956
+ : { ...base, events, page };
957
+ }
958
+ const fields = request.groupBy;
959
+ const measures = request.measures!;
960
+ const groupFields = fields.map(
961
+ (field) =>
962
+ `coalesce(${queryFieldSql(field, sql.params)}, '{"missing":true}'::jsonb)`,
963
+ );
964
+ const selected = [
965
+ `jsonb_build_array(${groupFields.join(', ')}) AS group_key`,
966
+ ];
967
+ const invalid: string[] = [];
968
+ const aggregations = measures.map((measure, i) => {
969
+ if (measure.op === 'count') return `count(*)::text AS measure_${i}`;
970
+ const field = queryFieldSql(measure.field, sql.params);
971
+ selected.push(
972
+ `CASE WHEN jsonb_typeof(${field}) = 'number' THEN (${field} #>> '{}')::numeric END AS sum_${i}`,
973
+ );
974
+ invalid.push(
975
+ `(${field} IS NOT NULL AND jsonb_typeof(${field}) <> 'number')`,
976
+ );
977
+ return `coalesce(sum(sum_${i}), 0)::text AS measure_${i}`;
978
+ });
979
+ selected.push(
980
+ `${invalid.length ? invalid.join(' OR ') : 'false'} AS invalid_sum`,
981
+ );
982
+ let continuation = '';
983
+ if (cursor) {
984
+ if (
985
+ !Array.isArray(cursor.position) ||
986
+ cursor.position.length !== fields.length ||
987
+ cursor.position.some((value) => {
988
+ if (
989
+ value === null ||
990
+ typeof value === 'string' ||
991
+ typeof value === 'boolean'
992
+ )
993
+ return false;
994
+ if (typeof value === 'number') return !Number.isFinite(value);
995
+ return (
996
+ !value || value.missing !== true || Object.keys(value).length !== 1
997
+ );
998
+ })
999
+ )
1000
+ invalidQueryCursor();
1001
+ sql.params.push(JSON.stringify(cursor.position));
1002
+ continuation = `WHERE group_key > $${sql.params.length}::jsonb`;
1003
+ }
1004
+ sql.params.push(limit + 1);
1005
+ const result = await client.query<Record<string, unknown>>(
1006
+ `WITH matched AS MATERIALIZED (
1007
+ SELECT ${selected.join(', ')} FROM ${EVENTS_TABLE} WHERE ${sql.where}
1008
+ ), grouped AS (
1009
+ SELECT ${fields.length ? 'group_key' : "'[]'::jsonb AS group_key"}, ${aggregations.join(', ')} FROM matched ${fields.length ? 'GROUP BY group_key' : ''}
1010
+ ), validation AS (
1011
+ SELECT count(*) FILTER (WHERE invalid_sum)::text AS invalid_sum_count FROM matched
1012
+ ), paged AS (
1013
+ SELECT * FROM grouped ${continuation} ORDER BY group_key ASC LIMIT $${sql.params.length}::integer
1014
+ ) SELECT paged.*, validation.invalid_sum_count FROM validation LEFT JOIN paged ON true ORDER BY group_key ASC`,
1015
+ sql.params,
1016
+ );
1017
+ if (exactQueryNumber(result.rows[0]?.invalid_sum_count, 'count') > 0) {
1018
+ const sumFields = measures.flatMap((measure) =>
1019
+ measure.op === 'sum' ? [measure.field] : [],
1020
+ );
1021
+ throw new RunEventQueryDataError(
1022
+ `Run event sum selected a nonnumeric value in ${sumFields.join(', ')}. Filter to numeric values or choose another measure.`,
1023
+ 'nonnumeric_run_event_sum',
1024
+ );
1025
+ }
1026
+ const rows = result.rows.filter(
1027
+ (row) => row.group_key !== null && row.group_key !== undefined,
1028
+ );
1029
+ const hasMore = rows.length > limit;
1030
+ const groups = rows.slice(0, limit).map((row) => {
1031
+ if (!Array.isArray(row.group_key) || row.group_key.length !== fields.length)
1032
+ throw new RunEventQueryDataError(
1033
+ 'Run event group projection is invalid.',
1034
+ );
1035
+ return {
1036
+ keys: Object.fromEntries(
1037
+ fields.map((field, i) => [field, (row.group_key as unknown[])[i]]),
1038
+ ),
1039
+ measures: Object.fromEntries(
1040
+ measures.map((measure, i) => [
1041
+ measure.as!,
1042
+ exactQueryNumber(row[`measure_${i}`], measure.op),
1043
+ ]),
1044
+ ),
1045
+ };
1046
+ });
1047
+ const last = rows[Math.min(rows.length, limit) - 1];
1048
+ return {
1049
+ ...base,
1050
+ groupBy: fields,
1051
+ measures,
1052
+ groups: groups as RunEventSummaryResult['groups'],
1053
+ page: {
1054
+ returnedCount: groups.length,
1055
+ hasMore,
1056
+ nextCursor:
1057
+ hasMore && last ? encode(last.group_key as RunEventGroupValue[]) : null,
1058
+ },
1059
+ };
1060
+ }
1061
+
467
1062
  /**
468
1063
  * Reads a receipt through a narrow SECURITY DEFINER function. The reader has
469
1064
  * no direct grant on any scheduler receipt table, and the function checks both
@@ -976,6 +1571,7 @@ export async function setRuntimeEventDeferredProjection(
976
1571
  runtimeScope: string,
977
1572
  deferred: boolean,
978
1573
  independent?: boolean,
1574
+ newRunsOnly?: true,
979
1575
  ): Promise<void> {
980
1576
  const schema = quoteIdent(requiredScope(runtimeScope));
981
1577
  if (!(await hasRuntimeEventSearchShape(client, runtimeScope))) {
@@ -991,11 +1587,38 @@ export async function setRuntimeEventDeferredProjection(
991
1587
  await client.query('BEGIN');
992
1588
  try {
993
1589
  await client.query("SELECT set_config('lock_timeout', '1000', true)");
1590
+ // Fence admission before outbox writes, in the normal Run -> outbox order.
1591
+ // The admission pin and producer flags change at one committed boundary.
1592
+ await client.query(`LOCK TABLE ${schema}.runs IN SHARE ROW EXCLUSIVE MODE`);
994
1593
  // Fence trigger execution while changing mode. This operator-only lock
995
1594
  // never spans projection; normal producer writes acquire no such lock.
996
1595
  await client.query(
997
1596
  `LOCK TABLE ${schema}.outbox IN SHARE ROW EXCLUSIVE MODE`,
998
1597
  );
1598
+ const previous = await client.query<{
1599
+ deferred: boolean;
1600
+ new_runs_only: boolean;
1601
+ }>(`SELECT ${schema}.defer_runtime_event_projection() AS deferred,
1602
+ ${schema}.new_runs_only_runtime_event_projection() AS new_runs_only`);
1603
+ const scoped =
1604
+ newRunsOnly === true || previous.rows[0]?.new_runs_only === true;
1605
+ if (deferred && scoped && independent === false) {
1606
+ throw new Error(
1607
+ 'Scoped cutover requires independent projection; use disable to stop it.',
1608
+ );
1609
+ }
1610
+ if (newRunsOnly === true) {
1611
+ if (!deferred || independent !== true) {
1612
+ throw new Error(
1613
+ 'New-run cutover requires independent deferred projection.',
1614
+ );
1615
+ }
1616
+ if (previous.rows[0]?.deferred && !previous.rows[0]?.new_runs_only) {
1617
+ throw new Error(
1618
+ 'Drain and disable global deferred projection before a new-run cutover.',
1619
+ );
1620
+ }
1621
+ }
999
1622
  if (!deferred) {
1000
1623
  const pending = await client.query<{ event_id: string }>(
1001
1624
  `SELECT event_id FROM ${schema}.outbox
@@ -1019,12 +1642,14 @@ export async function setRuntimeEventDeferredProjection(
1019
1642
  );
1020
1643
  }
1021
1644
  await installRuntimeEventProjectionMode(client, schema, deferred);
1022
- if (independent !== undefined || !deferred)
1645
+ if (independent !== undefined || !deferred || scoped)
1023
1646
  await installIndependentProjectionMode(
1024
1647
  client,
1025
1648
  schema,
1026
- deferred && independent === true,
1649
+ deferred && (independent === true || scoped),
1027
1650
  );
1651
+ if (newRunsOnly !== undefined)
1652
+ await installNewRunProjectionMode(client, schema, newRunsOnly);
1028
1653
  await client.query('COMMIT');
1029
1654
  } catch (error) {
1030
1655
  await client.query('ROLLBACK');
@@ -1036,10 +1661,12 @@ export type RuntimeEventProjectionModes = {
1036
1661
  runtimeScope: string;
1037
1662
  deferred: boolean | null;
1038
1663
  independent: boolean | null;
1664
+ newRunsOnly?: boolean | null;
1039
1665
  unavailableReason:
1040
1666
  | 'projection_mode_readers_missing'
1041
1667
  | 'deferred_projection_reader_missing'
1042
1668
  | 'independent_projection_reader_missing'
1669
+ | 'new_run_projection_reader_missing'
1043
1670
  | null;
1044
1671
  };
1045
1672
 
@@ -1053,25 +1680,31 @@ export async function readRuntimeEventProjectionModes(
1053
1680
  const installed = await client.query<{
1054
1681
  deferred_installed: boolean;
1055
1682
  independent_installed: boolean;
1683
+ new_runs_only_installed: boolean;
1056
1684
  }>(
1057
1685
  `SELECT to_regprocedure($1) IS NOT NULL AS deferred_installed,
1058
- to_regprocedure($2) IS NOT NULL AS independent_installed`,
1686
+ to_regprocedure($2) IS NOT NULL AS independent_installed,
1687
+ to_regprocedure($3) IS NOT NULL AS new_runs_only_installed`,
1059
1688
  [
1060
1689
  `${schema}.defer_runtime_event_projection()`,
1061
1690
  `${schema}.independent_runtime_event_projection()`,
1691
+ `${schema}.new_runs_only_runtime_event_projection()`,
1062
1692
  ],
1063
1693
  );
1064
1694
  const readers = installed.rows[0];
1065
1695
  if (
1066
1696
  !readers ||
1067
1697
  typeof readers.deferred_installed !== 'boolean' ||
1068
- typeof readers.independent_installed !== 'boolean'
1698
+ typeof readers.independent_installed !== 'boolean' ||
1699
+ typeof readers.new_runs_only_installed !== 'boolean'
1069
1700
  ) {
1070
1701
  throw new Error('Runtime projection mode lookup returned no reader state.');
1071
1702
  }
1072
1703
  const unavailableReason = readers.deferred_installed
1073
1704
  ? readers.independent_installed
1074
- ? null
1705
+ ? readers.new_runs_only_installed
1706
+ ? null
1707
+ : 'new_run_projection_reader_missing'
1075
1708
  : 'independent_projection_reader_missing'
1076
1709
  : readers.independent_installed
1077
1710
  ? 'deferred_projection_reader_missing'
@@ -1081,6 +1714,7 @@ export async function readRuntimeEventProjectionModes(
1081
1714
  runtimeScope: scope,
1082
1715
  deferred: null,
1083
1716
  independent: null,
1717
+ newRunsOnly: null,
1084
1718
  unavailableReason,
1085
1719
  };
1086
1720
  }
@@ -1091,18 +1725,25 @@ export async function readRuntimeEventProjectionModes(
1091
1725
  const independentExpression = readers.independent_installed
1092
1726
  ? `${schema}.independent_runtime_event_projection()`
1093
1727
  : 'NULL::boolean';
1728
+ const newRunsOnlyExpression = readers.new_runs_only_installed
1729
+ ? `${schema}.new_runs_only_runtime_event_projection()`
1730
+ : 'NULL::boolean';
1094
1731
  const result = await client.query<{
1095
1732
  deferred: boolean | null;
1096
1733
  independent: boolean | null;
1734
+ new_runs_only: boolean | null;
1097
1735
  }>(
1098
1736
  `SELECT ${deferredExpression} AS deferred,
1099
- ${independentExpression} AS independent`,
1737
+ ${independentExpression} AS independent,
1738
+ ${newRunsOnlyExpression} AS new_runs_only`,
1100
1739
  );
1101
1740
  const modes = result.rows[0];
1102
1741
  if (
1103
1742
  !modes ||
1104
1743
  (readers.deferred_installed && typeof modes.deferred !== 'boolean') ||
1105
- (readers.independent_installed && typeof modes.independent !== 'boolean')
1744
+ (readers.independent_installed && typeof modes.independent !== 'boolean') ||
1745
+ (readers.new_runs_only_installed &&
1746
+ typeof modes.new_runs_only !== 'boolean')
1106
1747
  ) {
1107
1748
  throw new Error('Runtime projection mode lookup returned invalid values.');
1108
1749
  }
@@ -1110,6 +1751,7 @@ export async function readRuntimeEventProjectionModes(
1110
1751
  runtimeScope: scope,
1111
1752
  deferred: modes.deferred,
1112
1753
  independent: modes.independent,
1754
+ newRunsOnly: modes.new_runs_only,
1113
1755
  unavailableReason,
1114
1756
  };
1115
1757
  }
@@ -1121,27 +1763,32 @@ export async function inspectRuntimeEventProjection(
1121
1763
  ): Promise<{
1122
1764
  deferred: boolean;
1123
1765
  independent: boolean;
1766
+ newRunsOnly: boolean | null;
1124
1767
  pending: number;
1125
1768
  blocked: number;
1126
1769
  oldestPendingAt: string | null;
1127
1770
  }> {
1128
1771
  const schema = quoteIdent(requiredScope(runtimeScope));
1772
+ const modes = await readRuntimeEventProjectionModes(client, runtimeScope);
1773
+ if (modes.deferred === null || modes.independent === null) {
1774
+ throw new Error(
1775
+ 'Install the deferred projection readers before inspecting the outbox.',
1776
+ );
1777
+ }
1129
1778
  const result = await client.query<{
1130
- deferred: boolean;
1131
- independent: boolean;
1132
1779
  pending: string | number;
1133
1780
  blocked: string | number;
1134
1781
  oldest: Date | string | null;
1135
- }>(`SELECT ${schema}.defer_runtime_event_projection() AS deferred, ${schema}.independent_runtime_event_projection() AS independent,
1136
- count(*) AS pending, count(*) FILTER (WHERE failed_at IS NOT NULL) AS blocked,
1782
+ }>(`SELECT count(*) AS pending, count(*) FILTER (WHERE failed_at IS NOT NULL) AS blocked,
1137
1783
  min(created_at) AS oldest
1138
1784
  FROM ${schema}.outbox
1139
1785
  WHERE delivered_at IS NULL
1140
1786
  AND payload_json->'${RUNTIME_EVENT_DEFERRED_PROJECTION_FIELD}' = 'true'::jsonb`);
1141
1787
  const state = result.rows[0]!;
1142
1788
  return {
1143
- deferred: state.deferred,
1144
- independent: state.independent,
1789
+ deferred: modes.deferred,
1790
+ independent: modes.independent,
1791
+ newRunsOnly: modes.newRunsOnly ?? null,
1145
1792
  pending: Number(state.pending),
1146
1793
  blocked: Number(state.blocked),
1147
1794
  oldestPendingAt: state.oldest === null ? null : iso(state.oldest),
@@ -1173,6 +1820,18 @@ async function installIndependentProjectionMode(
1173
1820
  );
1174
1821
  }
1175
1822
 
1823
+ async function installNewRunProjectionMode(
1824
+ client: PostgresSchedulerQueryClient,
1825
+ schema: string,
1826
+ enabled: boolean,
1827
+ ): Promise<void> {
1828
+ await client.query(`CREATE OR REPLACE FUNCTION ${schema}.new_runs_only_runtime_event_projection()
1829
+ RETURNS boolean LANGUAGE sql VOLATILE SET search_path = pg_catalog AS 'SELECT ${enabled ? 'true' : 'false'}'`);
1830
+ await client.query(
1831
+ `REVOKE ALL ON FUNCTION ${schema}.new_runs_only_runtime_event_projection() FROM PUBLIC`,
1832
+ );
1833
+ }
1834
+
1176
1835
  /**
1177
1836
  * Migration installs readers with synchronous production unchanged. After
1178
1837
  * cutover, the producer persists raw input and only the leased dispatcher
@@ -1198,6 +1857,19 @@ export async function attachRuntimeEventSearchTriggers(
1198
1857
  );
1199
1858
  if (!independentMode.rows[0]?.installed)
1200
1859
  await installIndependentProjectionMode(client, schema, false);
1860
+ const newRunMode = await client.query<{ installed: boolean }>(
1861
+ `SELECT to_regprocedure($1) IS NOT NULL AS installed`,
1862
+ [`${schema}.new_runs_only_runtime_event_projection()`],
1863
+ );
1864
+ if (!newRunMode.rows[0]?.installed)
1865
+ await installNewRunProjectionMode(client, schema, false);
1866
+ await client.query(`CREATE OR REPLACE FUNCTION ${schema}.defer_runtime_event_projection_for_run(rid text)
1867
+ RETURNS boolean LANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = pg_catalog AS $function$
1868
+ SELECT ${schema}.defer_runtime_event_projection() AND
1869
+ (NOT ${schema}.new_runs_only_runtime_event_projection() OR coalesce(
1870
+ (SELECT admission_json->>'runtimeEventProjection' = 'independent'
1871
+ FROM ${schema}.runs WHERE run_id = rid), false))
1872
+ $function$`);
1201
1873
  // Stamp new admissions once. Existing launches never acquire a producer pin
1202
1874
  // on replay or worker upgrade. Ignore a caller-supplied pin before cutover.
1203
1875
  await client.query(`CREATE OR REPLACE FUNCTION ${schema}.pin_runtime_log_identity() RETURNS trigger
@@ -1205,6 +1877,25 @@ export async function attachRuntimeEventSearchTriggers(
1205
1877
  BEGIN
1206
1878
  NEW.launch_json := NEW.launch_json - 'runtimeLogIdentity';
1207
1879
  NEW.admission_json := NEW.admission_json - 'runtimeLogIdentity';
1880
+ NEW.launch_json := NEW.launch_json - 'runtimeEventProjection';
1881
+ NEW.admission_json := NEW.admission_json - 'runtimeEventProjection';
1882
+ IF ${schema}.new_runs_only_runtime_event_projection()
1883
+ AND ${schema}.defer_runtime_event_projection()
1884
+ AND ${schema}.independent_runtime_event_projection() THEN
1885
+ -- Older writers may omit admission_json. Preserve their authority
1886
+ -- using the same transport-free projection as normal admission.
1887
+ NEW.admission_json := coalesce(NEW.admission_json,
1888
+ NEW.launch_json - ARRAY[
1889
+ 'executorToken',
1890
+ 'preloadedDbSessionUnwrapKey',
1891
+ 'preloadedDbSessions',
1892
+ 'submittedAtMs',
1893
+ 'runtimeDeployVersion',
1894
+ 'coordinatorInternalToken',
1895
+ 'vercelProtectionBypassToken'
1896
+ ]::text[])
1897
+ || '{"runtimeEventProjection":"independent"}'::jsonb;
1898
+ END IF;
1208
1899
  IF ${schema}.independent_runtime_event_projection() THEN
1209
1900
  NEW.launch_json := NEW.launch_json || '{"runtimeLogIdentity":"origin"}'::jsonb;
1210
1901
  NEW.admission_json := NEW.admission_json || '{"runtimeLogIdentity":"origin"}'::jsonb;
@@ -1227,7 +1918,7 @@ export async function attachRuntimeEventSearchTriggers(
1227
1918
  IF NEW.run_id IS NULL OR NEW.org_id IS NULL THEN
1228
1919
  RAISE EXCEPTION 'Terminal receipt search projection requires run and organization';
1229
1920
  END IF;
1230
- IF ${schema}.defer_runtime_event_projection() THEN
1921
+ IF ${schema}.defer_runtime_event_projection_for_run(NEW.run_id) THEN
1231
1922
  IF NOT EXISTS (SELECT 1 FROM ${schema}.runs
1232
1923
  WHERE run_id = NEW.run_id AND org_id = NEW.org_id) THEN
1233
1924
  RAISE EXCEPTION 'Terminal receipt search projection has no matching run';
@@ -1388,6 +2079,12 @@ export async function attachRuntimeEventSearchTriggers(
1388
2079
  ON CONFLICT (runtime_scope, org_id, event_id) DO NOTHING RETURNING event_id
1389
2080
  ) SELECT coalesce(array_agg(inserted.event_id ORDER BY entries.ordinal), ARRAY[]::text[])
1390
2081
  INTO fresh FROM inserted JOIN entries ON entries.eid = inserted.event_id;
2082
+ -- The INSERT already proves the content of every fresh identity. Only
2083
+ -- retries or a concurrent uniqueness winner need a journal read-back.
2084
+ -- This fast path serves both inline and background origin projection.
2085
+ IF cardinality(fresh) = jsonb_array_length(item->'lines') THEN
2086
+ RETURN fresh;
2087
+ END IF;
1391
2088
  -- A new READ COMMITTED statement sees a concurrent uniqueness winner.
1392
2089
  -- Tombstones retain digest/identity: retry never restores an expired body.
1393
2090
  SELECT count(*) INTO mismatches
@@ -1432,7 +2129,7 @@ export async function attachRuntimeEventSearchTriggers(
1432
2129
  -- writers lock attempt before Run; this recorder must not invert that.
1433
2130
  -- Stable batches are ordered by the leased background prefix and cursor row.
1434
2131
  -- The legacy alias path still needs its old transaction mutex.
1435
- IF NOT ${schema}.defer_runtime_event_projection() THEN
2132
+ IF NOT ${schema}.defer_runtime_event_projection_for_run(observation.run_id) THEN
1436
2133
  PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(observation.run_id));
1437
2134
  END IF;
1438
2135
  SELECT org_id, play_name, run_log_store = 'planet_scale', status,
@@ -1549,7 +2246,7 @@ export async function attachRuntimeEventSearchTriggers(
1549
2246
  ));
1550
2247
  END IF;
1551
2248
  IF event_batch IS NOT NULL THEN
1552
- 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
2249
+ IF ${schema}.defer_runtime_event_projection_for_run(observation.run_id) 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
1553
2250
  PERFORM pg_advisory_xact_lock(hashtext(${quoteLiteral(scope)}), hashtext(observation.run_id));
1554
2251
  END IF;
1555
2252
  -- Normalize old positional writers once per batch under the run mutex.
@@ -2252,7 +2949,7 @@ export async function attachRuntimeEventSearchTriggers(
2252
2949
  DECLARE run_closed boolean; search_payload jsonb;
2253
2950
  BEGIN
2254
2951
  IF NEW.payload_json->'${RUNTIME_EVENT_SEARCH_ONLY_FIELD}' = 'true'::jsonb THEN RETURN NEW; END IF;
2255
- IF ${schema}.defer_runtime_event_projection()
2952
+ IF ${schema}.defer_runtime_event_projection_for_run(NEW.run_id)
2256
2953
  AND NEW.event_type <> ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runCreated} THEN
2257
2954
  IF NEW.event_type = ${RUN_SCHEDULER_OUTBOX_EVENT_TYPE_SQL_LITERAL.runEvents} THEN
2258
2955
  -- One cheap Run fence captures whether ingress preceded terminal
@@ -2378,7 +3075,7 @@ export async function attachRuntimeEventSearchTriggers(
2378
3075
  THEN NEW.runner_log_payload_json->'terminalProgressEvents'
2379
3076
  ELSE '[]'::jsonb END) WITH ORDINALITY AS progress(event, position)
2380
3077
  ));
2381
- IF ${schema}.defer_runtime_event_projection() THEN
3078
+ IF ${schema}.defer_runtime_event_projection_for_run(NEW.run_id) THEN
2382
3079
  INSERT INTO ${schema}.outbox (event_id, run_id, event_type, payload_json, created_at)
2383
3080
  VALUES (observation.event_id, observation.run_id, observation.event_type,
2384
3081
  observation.payload_json,
@@ -2474,6 +3171,8 @@ export async function attachRuntimeEventSearchTriggers(
2474
3171
  await client.query(`REVOKE ALL ON FUNCTION ${schema}.project_outbox_event(${schema}.outbox),
2475
3172
  ${schema}.project_origin_log_batch(${schema}.outbox, jsonb, text, text, boolean, boolean, timestamptz, jsonb, boolean),
2476
3173
  ${schema}.independent_runtime_event_projection(),
3174
+ ${schema}.new_runs_only_runtime_event_projection(),
3175
+ ${schema}.defer_runtime_event_projection_for_run(text),
2477
3176
  ${schema}.pin_runtime_log_identity(),
2478
3177
  ${schema}.materialize_runtime_outbox_event(text, text),
2479
3178
  ${schema}.guard_runtime_outbox_ack(),
@@ -2499,6 +3198,8 @@ export async function hasRuntimeEventSearchShape(
2499
3198
  AND EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = to_regclass('${schema}.outbox')
2500
3199
  AND tgname = 'runtime_outbox_projection_ack_v1' AND tgenabled <> 'D')
2501
3200
  AND to_regprocedure('${schema}.defer_runtime_event_projection()') IS NOT NULL
3201
+ AND to_regprocedure('${schema}.new_runs_only_runtime_event_projection()') IS NOT NULL
3202
+ AND to_regprocedure('${schema}.defer_runtime_event_projection_for_run(text)') IS NOT NULL
2502
3203
  AND to_regclass('runtime_observability.events') IS NOT NULL
2503
3204
  AND to_regclass('runtime_observability.run_log_cursors') IS NOT NULL
2504
3205
  AND EXISTS (SELECT 1 FROM pg_attribute