deepline 0.3.307 → 0.3.309

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 (103) 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/runtime-event-search.ts +595 -0
  12. package/dist/bundling-sources/shared_libs/plays/share/static-pipeline-summary.ts +97 -22
  13. package/dist/cli/{auth-AJPTFFC7.mjs → auth-AA5SXOB6.mjs} +4 -4
  14. package/dist/cli/auth-MXBOMMLC.js +31 -0
  15. package/dist/cli/{billing-SPBWUEOS.js → billing-6DJWMFV7.js} +70 -70
  16. package/dist/cli/{billing-LPDYJLIL.mjs → billing-CXUIXODY.mjs} +4 -4
  17. package/dist/cli/{chunk-QACDLYNO.mjs → chunk-3EVKBS6J.mjs} +1 -1
  18. package/dist/cli/{chunk-MZQP7CIL.js → chunk-3SZSFTLQ.js} +1 -1
  19. package/dist/cli/{chunk-G4SSYZND.mjs → chunk-4A6BYN66.mjs} +1 -1
  20. package/dist/cli/{chunk-IVXD2KA2.js → chunk-4SAHSARB.js} +15 -15
  21. package/dist/cli/{chunk-DF3XJ2V2.js → chunk-52FXVKVC.js} +48 -48
  22. package/dist/cli/{chunk-K2CQ2RBJ.js → chunk-5VHI2LFM.js} +6 -6
  23. package/dist/cli/{chunk-76MFV3AC.js → chunk-6QWXPOZM.js} +2 -2
  24. package/dist/cli/{chunk-RXYYANYP.mjs → chunk-A74BQEUC.mjs} +13 -5
  25. package/dist/cli/{chunk-ILOCS73R.mjs → chunk-ATTN5BOA.mjs} +2 -2
  26. package/dist/cli/{chunk-HWCB5MHF.js → chunk-CO5TGXG2.js} +68 -60
  27. package/dist/cli/{chunk-D55NIPGU.mjs → chunk-GUUQJJBS.mjs} +1 -1
  28. package/dist/cli/{chunk-GUIQWUCB.js → chunk-IQYTESPK.js} +6 -6
  29. package/dist/cli/{chunk-ZIF6JOOI.js → chunk-M2V27CEE.js} +26 -26
  30. package/dist/cli/{chunk-MPVXXDWH.mjs → chunk-N6P7NBX3.mjs} +1 -1
  31. package/dist/cli/{chunk-AKE6ZFIJ.js → chunk-NZCFZXDP.js} +4 -4
  32. package/dist/cli/{chunk-BDFVIOLY.mjs → chunk-OTX2Z2Y7.mjs} +3 -3
  33. package/dist/cli/{chunk-KCHBE2J3.js → chunk-PAJVT47Z.js} +852 -204
  34. package/dist/cli/{chunk-2IKIG333.js → chunk-QJBJZVG6.js} +15 -4
  35. package/dist/cli/{chunk-ZGICKTW7.mjs → chunk-TQ5BV5RT.mjs} +2 -2
  36. package/dist/cli/{chunk-JNHXSZPB.js → chunk-V5PB43SQ.js} +6 -6
  37. package/dist/cli/{chunk-TLTKAJ6S.mjs → chunk-W3CSORIT.mjs} +653 -5
  38. package/dist/cli/{chunk-UABLQKTZ.mjs → chunk-WXLOW623.mjs} +11 -0
  39. package/dist/cli/{chunk-O4OESDIN.mjs → chunk-YBKMQLH2.mjs} +1 -1
  40. package/dist/cli/{chunk-7ONALPWY.mjs → chunk-YV5USGOP.mjs} +4 -4
  41. package/dist/cli/client-B2MYCJW5.js +20 -0
  42. package/dist/cli/{client-7ZL4ROII.mjs → client-GXT2WRBH.mjs} +3 -3
  43. package/dist/cli/{csv-JUDFG7JV.js → csv-FY7CVAUY.js} +12 -12
  44. package/dist/cli/{csv-Y3LYCTGD.mjs → csv-OLMTSK4E.mjs} +3 -3
  45. package/dist/cli/{db-GCOJD2BD.mjs → db-HWYKGEOK.mjs} +4 -4
  46. package/dist/cli/{db-5IXGEXNT.js → db-XHHF7SFB.js} +20 -20
  47. package/dist/cli/{executions-FIICTMSY.mjs → executions-5L7YUA7K.mjs} +4 -4
  48. package/dist/cli/{executions-LIEQKFT7.js → executions-K2IJEKEN.js} +6 -6
  49. package/dist/cli/{feedback-3OVAA3C7.mjs → feedback-BIN2IWO4.mjs} +3 -3
  50. package/dist/cli/{feedback-FAADYNAR.js → feedback-FZLBZKF7.js} +6 -6
  51. package/dist/cli/{git-ESMYCYJO.mjs → git-G4ULJPIH.mjs} +4 -4
  52. package/dist/cli/git-KSXSQSH2.js +23 -0
  53. package/dist/cli/index.js +79 -77
  54. package/dist/cli/index.mjs +33 -31
  55. package/dist/cli/{legacy-noops-PKI3RFZW.js → legacy-noops-TBTYKXMK.js} +7 -7
  56. package/dist/cli/{legacy-noops-KP5CPZRK.mjs → legacy-noops-YBBLHKUV.mjs} +4 -4
  57. package/dist/cli/{monitors-KCFBLAFZ.js → monitors-PXBJTE6Q.js} +85 -85
  58. package/dist/cli/{monitors-A6KGGSBO.mjs → monitors-ZRNDHLTP.mjs} +4 -4
  59. package/dist/cli/{org-WI6DEXZQ.js → org-AUPHYG4W.js} +60 -60
  60. package/dist/cli/{org-DHI24UOK.mjs → org-GR7CKJDU.mjs} +4 -4
  61. package/dist/cli/{play-2YXLQD24.mjs → play-7C4XXDLJ.mjs} +472 -55
  62. package/dist/cli/{play-4EUUN76H.js → play-BXX6KDC5.js} +961 -544
  63. package/dist/cli/{providers-2WZ6KHH2.mjs → providers-2ALIWVHI.mjs} +4 -4
  64. package/dist/cli/{providers-DWS3XIM6.js → providers-X3W5I7AI.js} +7 -7
  65. package/dist/cli/{quickstart-ZFZVG6UO.mjs → quickstart-HFLMM6CB.mjs} +3 -3
  66. package/dist/cli/{quickstart-TDTDKM6F.js → quickstart-YU2FOUGV.js} +5 -5
  67. package/dist/cli/{search-BBE2ZMCI.mjs → search-5W6CX5FT.mjs} +5 -5
  68. package/dist/cli/{search-GMPXRQF4.js → search-LBAPKYPL.js} +6 -6
  69. package/dist/cli/{secrets-SMBX4IE6.js → secrets-4JYV6372.js} +13 -13
  70. package/dist/cli/{secrets-JEJL3KIM.mjs → secrets-N3LH3EFK.mjs} +4 -4
  71. package/dist/cli/{sessions-AVU353UT.mjs → sessions-4TCBDROP.mjs} +4 -4
  72. package/dist/cli/{sessions-S6AWG747.js → sessions-66MKLRBI.js} +5 -5
  73. package/dist/cli/{settings-GY5CYRLF.mjs → settings-33RZDJOH.mjs} +7 -7
  74. package/dist/cli/{settings-KBW4ROHC.js → settings-GQMXHDZN.js} +74 -74
  75. package/dist/cli/{setup-4CW3XGGX.mjs → setup-754356IS.mjs} +6 -6
  76. package/dist/cli/{setup-WSVIDSEV.js → setup-HU6LC4VR.js} +41 -41
  77. package/dist/cli/{skills-U3NXFSFK.mjs → skills-FLWPPQMH.mjs} +4 -4
  78. package/dist/cli/skills-WWO2EIQU.js +28 -0
  79. package/dist/cli/{tools-XU6KLJZT.mjs → tools-IRLN4KYG.mjs} +58 -42
  80. package/dist/cli/{tools-KZKELW25.js → tools-N4OW6HZK.js} +111 -95
  81. package/dist/cli/update-CLSJ7ODP.js +28 -0
  82. package/dist/cli/{update-RZLH46DR.mjs → update-TOQOHGVE.mjs} +6 -6
  83. package/dist/cli/{workflow-ZQUKQI35.mjs → workflow-DRH7T3P4.mjs} +5 -5
  84. package/dist/cli/{workflow-GDOGDGTZ.js → workflow-WSYT32Y3.js} +15 -15
  85. package/dist/{client-Dj8MAkDc.d.mts → client-DpnkGBn-.d.mts} +324 -11
  86. package/dist/{client-DsrFGXJf.d.ts → client-ypdBvf1o.d.ts} +324 -11
  87. package/dist/index.d.mts +2 -2
  88. package/dist/index.d.ts +2 -2
  89. package/dist/index.js +8 -6
  90. package/dist/index.mjs +5 -3
  91. package/dist/install-integrity.json +73 -72
  92. package/dist/plays/run-request.d.mts +1 -1
  93. package/dist/plays/run-request.d.ts +1 -1
  94. package/dist/release.d.mts +1 -1
  95. package/dist/release.d.ts +1 -1
  96. package/dist/release.js +2 -2
  97. package/dist/release.mjs +1 -1
  98. package/package.json +1 -1
  99. package/dist/cli/auth-TUQTQIH4.js +0 -31
  100. package/dist/cli/client-S3MIFPM2.js +0 -20
  101. package/dist/cli/git-GG6LWE2R.js +0 -23
  102. package/dist/cli/skills-6AU3RRAD.js +0 -28
  103. package/dist/cli/update-LVXXQF4A.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,
@@ -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