deepline 0.3.261 → 0.3.262

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.261',
203
+ version: '0.3.262',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -314,7 +314,9 @@ const POSTGRES_SCHEDULER_SCHEMA_INIT_STATEMENT_TIMEOUT_MS = 60_000;
314
314
  // deploy readiness require it; v50 only built it on the concurrent path.
315
315
  // v52 indexes only delivered outbox rows that still contain raw log bodies.
316
316
  // The concurrent build lets retention skip already-scrubbed history.
317
- export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 52;
317
+ // v53 refreshes the indexed runtime log reconciliation projection. Its query
318
+ // indexes remain operator-prepared concurrently, outside shared bootstrap.
319
+ export const POSTGRES_SCHEDULER_SCHEMA_VERSION = 53;
318
320
  const POSTGRES_SCHEDULER_SCHEMA_MIGRATIONS_TABLE =
319
321
  'scheduler_schema_migrations';
320
322
  export const POSTGRES_SCHEDULER_OPEN_RECOVERY_INDEX =
@@ -9,10 +9,39 @@ import { normalizeRuntimeSchedulerSchema } from '../runtime-scheduler-topology';
9
9
  import type { PostgresSchedulerQueryClient } from './postgres';
10
10
 
11
11
  const EVENTS_TABLE = 'runtime_observability.events';
12
+ const RUNTIME_EVENT_LOG_DEDUPE_INDEX = 'runtime_events_log_attempt_digest_idx';
13
+ const RUNTIME_EVENT_LOG_POSITION_INDEX =
14
+ 'runtime_events_log_attempt_position_idx';
15
+ const RUNTIME_EVENT_LOG_LEGACY_INDEX =
16
+ 'runtime_events_legacy_log_normalize_idx';
17
+ const RUNTIME_EVENT_LOG_BODY_PAGE_INDEX = 'runtime_events_run_log_body_seq_idx';
18
+ const RUNTIME_EVENT_LOG_LEGACY_PREDICATE = `kind = 'log.appended'
19
+ AND source_digest IS NOT NULL
20
+ AND NOT (search_doc ? 'logContentDigest')
21
+ AND (search_doc->>'producerAttempt') ~ '^[1-9][0-9]{0,5}$'`;
22
+ const RUNTIME_EVENT_LOG_BODY_PAGE_PREDICATE = `kind = 'log.appended'
23
+ AND log_seq IS NOT NULL
24
+ AND (log_body IS NOT NULL OR message IS NOT NULL)`;
25
+ const RUNTIME_EVENT_LOG_DEDUPE_PREDICATE = `kind = 'log.appended'
26
+ AND source_digest IS NOT NULL
27
+ AND search_doc ? 'producerAttempt'
28
+ AND search_doc ? 'logContentDigest'`;
29
+ const RUNTIME_EVENT_LOG_POSITION_PREDICATE = `kind = 'log.appended'
30
+ AND search_doc ? 'producerAttempt'
31
+ AND search_doc ? 'logPositions'`;
32
+ const RUNTIME_EVENT_LOG_POSITION_EXPRESSION = `jsonb_set(
33
+ jsonb_set(
34
+ jsonb_set(
35
+ jsonb_set(coalesce(search_doc->'logPositions', '{}'::jsonb),
36
+ ARRAY['producerAttempt'], search_doc->'producerAttempt', true),
37
+ ARRAY['_runtimeScope:' || runtime_scope], 'true'::jsonb, true),
38
+ ARRAY['_orgId:' || org_id], 'true'::jsonb, true),
39
+ ARRAY['_runId:' || run_id], 'true'::jsonb, true
40
+ )`;
12
41
  // Migration readiness tracks this projection body independently of scheduler
13
42
  // wire and schema versions. Bump when a changed trigger must be reinstalled.
14
43
  export const RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER =
15
- 'runtime-event-search-log-context-v8-terminal-progress-counts-compact-first-progress-channel-aliases';
44
+ 'runtime-event-search-log-context-v9-indexed-channel-reconciliation';
16
45
  export const RUNTIME_EVENTS_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
17
46
  const MAX_SEARCH_WINDOW_MS = RUNTIME_EVENTS_RETENTION_MS;
18
47
  const MAX_SEARCH_RESULTS = 200;
@@ -455,12 +484,21 @@ export async function getRuntimeRunLogPage(
455
484
  // Match the event-then-cursor relation lock order used by migrations and
456
485
  // log projection. Cursor-first reads can deadlock a schema expansion.
457
486
  `WITH retained AS (
458
- SELECT min(log_seq) AS first_seq, max(log_seq) AS last_seq
459
- FROM runtime_observability.events
460
- WHERE runtime_scope = $1 AND org_id = $2 AND run_id = $3
461
- AND kind = 'log.appended' AND log_seq IS NOT NULL
462
- AND (log_body IS NOT NULL OR message IS NOT NULL)
463
- AND ingested_at >= clock_timestamp() - interval '7 days'
487
+ SELECT
488
+ (SELECT log_seq
489
+ FROM runtime_observability.events
490
+ WHERE runtime_scope = $1 AND org_id = $2 AND run_id = $3
491
+ AND kind = 'log.appended' AND log_seq IS NOT NULL
492
+ AND (log_body IS NOT NULL OR message IS NOT NULL)
493
+ AND ingested_at >= clock_timestamp() - interval '7 days'
494
+ ORDER BY log_seq ASC LIMIT 1) AS first_seq,
495
+ (SELECT log_seq
496
+ FROM runtime_observability.events
497
+ WHERE runtime_scope = $1 AND org_id = $2 AND run_id = $3
498
+ AND kind = 'log.appended' AND log_seq IS NOT NULL
499
+ AND (log_body IS NOT NULL OR message IS NOT NULL)
500
+ AND ingested_at >= clock_timestamp() - interval '7 days'
501
+ ORDER BY log_seq DESC LIMIT 1) AS last_seq
464
502
  ), run_cursor AS (
465
503
  SELECT last_seq
466
504
  FROM runtime_observability.run_log_cursors
@@ -807,7 +845,8 @@ export async function attachRuntimeEventSearchTriggers(
807
845
  doc jsonb; line_doc jsonb; line_context jsonb;
808
846
  eid text; existing_digest text; digest text; assigned_seq bigint; terminal_progress_identity boolean;
809
847
  log_channel text; log_position bigint; matched_event_id text; matched_doc jsonb;
810
- legacy_position_prefix text; log_alias jsonb; envelope_attempt jsonb;
848
+ legacy_position_prefix text; position_event_ids text[];
849
+ log_alias jsonb; position_alias jsonb; envelope_attempt jsonb;
811
850
  line_count bigint; fresh_event_ids text[] := ARRAY[]::text[];
812
851
  recent_log_keys jsonb := '{}'::jsonb; dedupe_key text;
813
852
  terminal_overlap_counts jsonb := '{}'::jsonb;
@@ -1013,15 +1052,53 @@ export async function attachRuntimeEventSearchTriggers(
1013
1052
  END IF;
1014
1053
  log_alias := jsonb_build_object('producerAttempt', (item->>'producerAttempt')::integer,
1015
1054
  'logPositions', jsonb_build_object(log_channel, log_position));
1055
+ position_alias := jsonb_build_object(
1056
+ 'producerAttempt', (item->>'producerAttempt')::integer,
1057
+ '_runtimeScope:' || ${quoteLiteral(scope)}, true,
1058
+ '_orgId:' || run_org, true,
1059
+ '_runId:' || observation.run_id, true)
1060
+ || jsonb_build_object(log_channel, log_position);
1016
1061
  matched_event_id := NULL;
1017
- -- A channel position names one canonical occurrence. Sorting
1018
- -- here makes cached plans scan the primary key on a miss.
1062
+ position_event_ids := ARRAY[
1063
+ legacy_position_prefix || log_position::text,
1064
+ 'log-worker:' || observation.run_id || ':' || (item->>'producerAttempt') || ':' || log_position::text,
1065
+ 'log-replay:' || observation.run_id || ':' || (item->>'producerAttempt') || ':' || log_position::text,
1066
+ 'log-envelope:' || observation.run_id || ':' || (item->>'producerAttempt') || ':' || log_position::text
1067
+ ];
1068
+ -- Positional identities have a bounded set of historical IDs.
1069
+ -- Probe those primary keys directly; scanning every event in
1070
+ -- the run for JSON containment made one lookup grow with run age.
1019
1071
  SELECT event.event_id, event.source_digest, event.search_doc
1020
1072
  INTO matched_event_id, existing_digest, matched_doc
1021
1073
  FROM runtime_observability.events AS event
1022
1074
  WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
1023
1075
  AND event.run_id = observation.run_id AND event.kind = 'log.appended'
1024
- AND event.search_doc @> log_alias;
1076
+ AND event.event_id = ANY(position_event_ids)
1077
+ AND (
1078
+ event.search_doc->'logPositions'->>log_channel = log_position::text
1079
+ OR (NOT (event.search_doc ? 'logPositions')
1080
+ AND event.event_id = legacy_position_prefix || log_position::text
1081
+ AND (CASE WHEN event.search_doc->>'source' = 'coordinator'
1082
+ THEN 'replay' ELSE 'worker' END) = log_channel)
1083
+ )
1084
+ ORDER BY event.event_id
1085
+ LIMIT 1;
1086
+ IF matched_event_id IS NULL THEN
1087
+ -- A transport may already be aliased onto an event whose
1088
+ -- primary key belongs to another channel. Find that alias
1089
+ -- independently of content so changed bytes still fail the
1090
+ -- immutable-position check below.
1091
+ SELECT event.event_id, event.source_digest, event.search_doc
1092
+ INTO matched_event_id, existing_digest, matched_doc
1093
+ FROM runtime_observability.events AS event
1094
+ WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
1095
+ AND event.run_id = observation.run_id AND event.kind = 'log.appended'
1096
+ AND event.search_doc ? 'producerAttempt'
1097
+ AND event.search_doc ? 'logPositions'
1098
+ AND (${RUNTIME_EVENT_LOG_POSITION_EXPRESSION.replaceAll('search_doc', 'event.search_doc')}) @> position_alias
1099
+ ORDER BY event.log_seq NULLS LAST, event.event_id
1100
+ LIMIT 1;
1101
+ END IF;
1025
1102
  IF matched_event_id IS NOT NULL THEN
1026
1103
  IF existing_digest IS DISTINCT FROM digest THEN
1027
1104
  RAISE EXCEPTION 'Positional log event identity changed its content';
@@ -1036,9 +1113,12 @@ export async function attachRuntimeEventSearchTriggers(
1036
1113
  FROM runtime_observability.events AS event
1037
1114
  WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
1038
1115
  AND event.run_id = observation.run_id AND event.kind = 'log.appended'
1039
- AND event.search_doc @> jsonb_build_object(
1040
- 'producerAttempt', (item->>'producerAttempt')::integer, 'logContentDigest', digest)
1116
+ AND event.source_digest IS NOT NULL
1117
+ AND event.search_doc ? 'producerAttempt'
1118
+ AND event.search_doc ? 'logContentDigest'
1119
+ AND event.search_doc->>'producerAttempt' = ((item->>'producerAttempt')::integer)::text
1041
1120
  AND event.source_digest = digest
1121
+ AND event.search_doc @> jsonb_build_object('logContentDigest', digest)
1042
1122
  AND NOT (coalesce(event.search_doc->'logPositions', '{}'::jsonb) ? log_channel)
1043
1123
  AND (event.search_doc ? 'logPositions' OR log_channel <>
1044
1124
  CASE WHEN event.search_doc->>'source' = 'coordinator' THEN 'replay' ELSE 'worker' END)
@@ -1408,10 +1488,40 @@ export async function attachRuntimeEventSearchTriggers(
1408
1488
  `);
1409
1489
  // Replacing AFTER with BEFORE is one DDL transaction, so live inserts cannot
1410
1490
  // observe an interval with no projector. All older unmarked runs stay full.
1491
+ // Once installed, leave the matching trigger attached: dropping it takes an
1492
+ // ACCESS EXCLUSIVE lock on the busy outbox table even when only its function
1493
+ // body or projection marker changed.
1494
+ const outboxTrigger = await client.query<{ matches: boolean }>(
1495
+ `SELECT EXISTS (
1496
+ SELECT 1
1497
+ FROM pg_trigger AS trg
1498
+ JOIN pg_class AS rel ON rel.oid = trg.tgrelid
1499
+ JOIN pg_namespace AS rel_ns ON rel_ns.oid = rel.relnamespace
1500
+ JOIN pg_proc AS fn ON fn.oid = trg.tgfoid
1501
+ JOIN pg_namespace AS fn_ns ON fn_ns.oid = fn.pronamespace
1502
+ WHERE rel_ns.nspname = $1 AND rel.relname = 'outbox'
1503
+ AND trg.tgname = 'runtime_outbox_event_v1'
1504
+ AND NOT trg.tgisinternal
1505
+ AND trg.tgenabled IN ('O', 'A')
1506
+ AND trg.tgtype = 7
1507
+ AND trg.tgqual IS NULL
1508
+ AND trg.tgargs = ''::bytea
1509
+ AND fn_ns.nspname = $1 AND fn.proname = 'search_outbox_event'
1510
+ AND fn.pronargs = 0
1511
+ ) AS matches`,
1512
+ [scope],
1513
+ );
1514
+ if (outboxTrigger.rows[0]?.matches !== true) {
1515
+ await client.query(`DO $replace$ BEGIN
1516
+ IF EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.outbox'::regclass
1517
+ AND tgname = 'runtime_outbox_event_v1' AND NOT tgisinternal) THEN
1518
+ DROP TRIGGER runtime_outbox_event_v1 ON ${schema}.outbox;
1519
+ END IF;
1520
+ CREATE TRIGGER runtime_outbox_event_v1 BEFORE INSERT ON ${schema}.outbox
1521
+ FOR EACH ROW EXECUTE FUNCTION ${schema}.search_outbox_event();
1522
+ END $replace$`);
1523
+ }
1411
1524
  await client.query(`DO $create$ BEGIN
1412
- DROP TRIGGER IF EXISTS runtime_outbox_event_v1 ON ${schema}.outbox;
1413
- CREATE TRIGGER runtime_outbox_event_v1 BEFORE INSERT ON ${schema}.outbox
1414
- FOR EACH ROW EXECUTE FUNCTION ${schema}.search_outbox_event();
1415
1525
  IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid = '${schema}.attempts'::regclass
1416
1526
  AND tgname = 'runtime_attempt_log_acceptance') THEN
1417
1527
  CREATE TRIGGER runtime_attempt_log_acceptance BEFORE UPDATE OF runner_result_json ON ${schema}.attempts
@@ -1486,6 +1596,204 @@ function normalizedRetentionPredicate(value: string): string {
1486
1596
  .join('');
1487
1597
  }
1488
1598
 
1599
+ // Query indexes include mixed AND/OR predicates. Remove catalog-added wrappers
1600
+ // around atomic terms, but keep boolean grouping so a changed predicate cannot
1601
+ // silently widen an index to unrelated tenant/event rows.
1602
+ function normalizedQueryIndexPredicate(value: string): string {
1603
+ let normalized = (
1604
+ value.match(/'(?:''|[^'])*'|::text\b|\b(?:AND|OR)\b|[()\s]+|./gi) ?? []
1605
+ )
1606
+ .filter((token) => token !== '::text')
1607
+ .map((token) => {
1608
+ if (/^(AND|OR)$/i.test(token))
1609
+ return `\u0001${token.toUpperCase()}\u0001`;
1610
+ return token.startsWith("'") ? token : token.replace(/\s/g, '');
1611
+ })
1612
+ .join('');
1613
+ let previous: string;
1614
+ do {
1615
+ previous = normalized;
1616
+ normalized = normalized
1617
+ .replace(/\(([^()]*)\)/g, (group, expression: string) =>
1618
+ /\u0001(?:AND|OR)\u0001/.test(expression) ? group : expression,
1619
+ )
1620
+ .replace(
1621
+ /\(\(([^()]*(?:\u0001AND\u0001|\u0001OR\u0001)[^()]*)\)\)/g,
1622
+ '($1)',
1623
+ );
1624
+ while (isWholeExpressionWrapped(normalized)) {
1625
+ normalized = normalized.slice(1, -1);
1626
+ }
1627
+ } while (normalized !== previous);
1628
+ return normalized;
1629
+ }
1630
+
1631
+ function isWholeExpressionWrapped(value: string): boolean {
1632
+ if (!value.startsWith('(') || !value.endsWith(')')) return false;
1633
+ let depth = 0;
1634
+ for (let index = 0; index < value.length; index += 1) {
1635
+ if (value[index] === "'") {
1636
+ if (value[index + 1] === "'") index += 1;
1637
+ else {
1638
+ const closingQuote = value.indexOf("'", index + 1);
1639
+ if (closingQuote === -1) return false;
1640
+ index = closingQuote;
1641
+ }
1642
+ } else if (value[index] === '(') depth += 1;
1643
+ else if (value[index] === ')') {
1644
+ depth -= 1;
1645
+ if (depth === 0 && index !== value.length - 1) return false;
1646
+ if (depth < 0) return false;
1647
+ }
1648
+ }
1649
+ return depth === 0;
1650
+ }
1651
+
1652
+ type RuntimeEventIndexExpectation = {
1653
+ name: string;
1654
+ keys: string[];
1655
+ predicate: string;
1656
+ createSql: string;
1657
+ method?: 'btree' | 'gin';
1658
+ opclasses?: string[];
1659
+ };
1660
+
1661
+ const RUNTIME_EVENT_QUERY_INDEXES: RuntimeEventIndexExpectation[] = [
1662
+ {
1663
+ name: RUNTIME_EVENT_LOG_POSITION_INDEX,
1664
+ method: 'gin',
1665
+ opclasses: ['jsonb_path_ops'],
1666
+ keys: [
1667
+ "jsonb_set(jsonb_set(jsonb_set(jsonb_set(COALESCE(search_doc -> 'logPositions'::text, '{}'::jsonb), ARRAY['producerAttempt'::text], search_doc -> 'producerAttempt'::text, true), ARRAY['_runtimeScope:'::text || runtime_scope], 'true'::jsonb, true), ARRAY['_orgId:'::text || org_id], 'true'::jsonb, true), ARRAY['_runId:'::text || run_id], 'true'::jsonb, true)",
1668
+ ],
1669
+ predicate: RUNTIME_EVENT_LOG_POSITION_PREDICATE,
1670
+ createSql: `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${RUNTIME_EVENT_LOG_POSITION_INDEX}
1671
+ ON runtime_observability.events USING gin
1672
+ ((${RUNTIME_EVENT_LOG_POSITION_EXPRESSION}) jsonb_path_ops)
1673
+ WHERE ${RUNTIME_EVENT_LOG_POSITION_PREDICATE}`,
1674
+ },
1675
+ {
1676
+ name: RUNTIME_EVENT_LOG_DEDUPE_INDEX,
1677
+ keys: [
1678
+ 'runtime_scope',
1679
+ 'org_id',
1680
+ 'run_id',
1681
+ "(search_doc ->> 'producerAttempt'::text)",
1682
+ 'source_digest',
1683
+ ],
1684
+ predicate: RUNTIME_EVENT_LOG_DEDUPE_PREDICATE,
1685
+ createSql: `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${RUNTIME_EVENT_LOG_DEDUPE_INDEX}
1686
+ ON runtime_observability.events
1687
+ (runtime_scope, org_id, run_id, (search_doc->>'producerAttempt'), source_digest)
1688
+ WHERE ${RUNTIME_EVENT_LOG_DEDUPE_PREDICATE}`,
1689
+ },
1690
+ {
1691
+ name: RUNTIME_EVENT_LOG_LEGACY_INDEX,
1692
+ keys: ['runtime_scope', 'org_id', 'run_id'],
1693
+ predicate: RUNTIME_EVENT_LOG_LEGACY_PREDICATE,
1694
+ createSql: `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${RUNTIME_EVENT_LOG_LEGACY_INDEX}
1695
+ ON runtime_observability.events (runtime_scope, org_id, run_id)
1696
+ WHERE ${RUNTIME_EVENT_LOG_LEGACY_PREDICATE}`,
1697
+ },
1698
+ {
1699
+ name: RUNTIME_EVENT_LOG_BODY_PAGE_INDEX,
1700
+ keys: ['runtime_scope', 'org_id', 'run_id', 'log_seq'],
1701
+ predicate: RUNTIME_EVENT_LOG_BODY_PAGE_PREDICATE,
1702
+ createSql: `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${RUNTIME_EVENT_LOG_BODY_PAGE_INDEX}
1703
+ ON runtime_observability.events (runtime_scope, org_id, run_id, log_seq)
1704
+ WHERE ${RUNTIME_EVENT_LOG_BODY_PAGE_PREDICATE}`,
1705
+ },
1706
+ ];
1707
+
1708
+ /** Build only operator-owned indexes, concurrently and outside a transaction. */
1709
+ export async function prepareRuntimeEventQueryIndexes(
1710
+ client: PostgresSchedulerQueryClient,
1711
+ ): Promise<void> {
1712
+ for (const expected of RUNTIME_EVENT_QUERY_INDEXES) {
1713
+ type IndexShape = {
1714
+ valid: boolean;
1715
+ ready: boolean;
1716
+ is_unique: boolean;
1717
+ table_matches: boolean;
1718
+ key_count: number;
1719
+ attribute_count: number;
1720
+ method: string;
1721
+ keys: string[];
1722
+ opclasses: string[];
1723
+ predicate: string | null;
1724
+ };
1725
+ const inspect = () =>
1726
+ client.query<IndexShape>(
1727
+ `SELECT index_row.indisvalid AS valid,
1728
+ index_row.indisready AS ready,
1729
+ index_row.indisunique AS is_unique,
1730
+ index_row.indrelid = to_regclass('runtime_observability.events') AS table_matches,
1731
+ index_row.indnkeyatts AS key_count,
1732
+ index_row.indnatts AS attribute_count,
1733
+ method.amname AS method,
1734
+ ARRAY(
1735
+ SELECT pg_get_indexdef(index_row.indexrelid, key_number.position, true)
1736
+ FROM generate_series(1, index_row.indnkeyatts) AS key_number(position)
1737
+ ORDER BY key_number.position
1738
+ ) AS keys,
1739
+ (
1740
+ SELECT coalesce(json_agg(operator_class.opcname ORDER BY classes.position), '[]'::json)
1741
+ FROM unnest(index_row.indclass) WITH ORDINALITY AS classes(opclass_oid, position)
1742
+ JOIN pg_opclass AS operator_class ON operator_class.oid = classes.opclass_oid
1743
+ ) AS opclasses,
1744
+ pg_get_expr(index_row.indpred, index_row.indrelid) AS predicate
1745
+ FROM pg_index AS index_row
1746
+ JOIN pg_class AS index_class ON index_class.oid = index_row.indexrelid
1747
+ JOIN pg_am AS method ON method.oid = index_class.relam
1748
+ WHERE index_class.oid = to_regclass($1)`,
1749
+ [`runtime_observability.${expected.name}`],
1750
+ );
1751
+ const assertShape = (shape: IndexShape): void => {
1752
+ if (!shape.valid || !shape.ready) {
1753
+ throw new Error(
1754
+ `Runtime event index ${expected.name} is invalid or incomplete; inspect and repair the failed build explicitly.`,
1755
+ );
1756
+ }
1757
+ if (
1758
+ !shape.table_matches ||
1759
+ shape.is_unique ||
1760
+ shape.method !== (expected.method ?? 'btree') ||
1761
+ (expected.opclasses !== undefined &&
1762
+ (shape.opclasses.length !== expected.opclasses.length ||
1763
+ shape.opclasses.some(
1764
+ (name, index) => name !== expected.opclasses![index],
1765
+ ))) ||
1766
+ shape.key_count !== expected.keys.length ||
1767
+ shape.attribute_count !== expected.keys.length ||
1768
+ shape.keys.length !== expected.keys.length ||
1769
+ shape.keys.some(
1770
+ (key, index) =>
1771
+ normalizedRetentionPredicate(key) !==
1772
+ normalizedRetentionPredicate(expected.keys[index]!),
1773
+ ) ||
1774
+ shape.predicate === null ||
1775
+ normalizedQueryIndexPredicate(shape.predicate) !==
1776
+ normalizedQueryIndexPredicate(expected.predicate)
1777
+ ) {
1778
+ throw new Error(
1779
+ `Runtime event index ${expected.name} differs from its expected table, keys, method, uniqueness or predicate. Actual catalog shape: ${JSON.stringify(shape)}. Inspect and repair it explicitly.`,
1780
+ );
1781
+ }
1782
+ };
1783
+
1784
+ const existing = (await inspect()).rows[0];
1785
+ if (existing) assertShape(existing);
1786
+ await client.query(expected.createSql);
1787
+ const built = (await inspect()).rows[0];
1788
+ if (!built) {
1789
+ throw new Error(
1790
+ `Runtime event index ${expected.name} was not found after preparation.`,
1791
+ );
1792
+ }
1793
+ assertShape(built);
1794
+ }
1795
+ }
1796
+
1489
1797
  /** Operator-only concurrent index build; never part of runtime bootstrap. */
1490
1798
  export async function prepareRuntimeEventRetentionIndex(
1491
1799
  client: PostgresSchedulerQueryClient,
package/dist/cli/index.js CHANGED
@@ -3512,7 +3512,7 @@ var SDK_RELEASE = {
3512
3512
  // getters keep their established compatibility behavior.
3513
3513
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3514
3514
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3515
- version: "0.3.261",
3515
+ version: "0.3.262",
3516
3516
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
3517
3517
  packageCapabilities: {
3518
3518
  updatePreferences: 1
@@ -3507,7 +3507,7 @@ var SDK_RELEASE = {
3507
3507
  // getters keep their established compatibility behavior.
3508
3508
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3509
3509
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3510
- version: "0.3.261",
3510
+ version: "0.3.262",
3511
3511
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
3512
3512
  packageCapabilities: {
3513
3513
  updatePreferences: 1
package/dist/index.js CHANGED
@@ -1325,7 +1325,7 @@ var SDK_RELEASE = {
1325
1325
  // getters keep their established compatibility behavior.
1326
1326
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
1327
1327
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
1328
- version: "0.3.261",
1328
+ version: "0.3.262",
1329
1329
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
1330
1330
  packageCapabilities: {
1331
1331
  updatePreferences: 1
package/dist/index.mjs CHANGED
@@ -1229,7 +1229,7 @@ var SDK_RELEASE = {
1229
1229
  // getters keep their established compatibility behavior.
1230
1230
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
1231
1231
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
1232
- version: "0.3.261",
1232
+ version: "0.3.262",
1233
1233
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
1234
1234
  packageCapabilities: {
1235
1235
  updatePreferences: 1
@@ -149,7 +149,7 @@ type SdkRelease = {
149
149
  supportPolicy: SdkSupportPolicy;
150
150
  };
151
151
  declare const SDK_RELEASE: {
152
- readonly version: "0.3.261";
152
+ readonly version: "0.3.262";
153
153
  readonly updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.";
154
154
  readonly packageCapabilities: {
155
155
  readonly updatePreferences: 1;
package/dist/release.d.ts CHANGED
@@ -149,7 +149,7 @@ type SdkRelease = {
149
149
  supportPolicy: SdkSupportPolicy;
150
150
  };
151
151
  declare const SDK_RELEASE: {
152
- readonly version: "0.3.261";
152
+ readonly version: "0.3.262";
153
153
  readonly updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.";
154
154
  readonly packageCapabilities: {
155
155
  readonly updatePreferences: 1;
package/dist/release.js CHANGED
@@ -74,7 +74,7 @@ var SDK_RELEASE = {
74
74
  // getters keep their established compatibility behavior.
75
75
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
76
76
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
77
- version: "0.3.261",
77
+ version: "0.3.262",
78
78
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
79
79
  packageCapabilities: {
80
80
  updatePreferences: 1
package/dist/release.mjs CHANGED
@@ -48,7 +48,7 @@ var SDK_RELEASE = {
48
48
  // getters keep their established compatibility behavior.
49
49
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
50
50
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
51
- version: "0.3.261",
51
+ version: "0.3.262",
52
52
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
53
53
  packageCapabilities: {
54
54
  updatePreferences: 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.3.261",
3
+ "version": "0.3.262",
4
4
  "description": "GTM data CLI and TypeScript SDK for coding agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://code.deepline.com",