deepline 0.3.276 → 0.3.278

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.276',
203
+ version: '0.3.278',
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: {
@@ -17391,6 +17391,22 @@ function runLedgerOrderSql(alias: string): string {
17391
17391
  return `CASE ${alias}.event_type WHEN ${RUN_EVENT_KIND_SQL_LITERAL.runCreated} THEN 0 WHEN ${RUN_EVENT_KIND_SQL_LITERAL.runStarted} THEN 1 ELSE 2 END, ${alias}.created_at, ${alias}.event_id`;
17392
17392
  }
17393
17393
 
17394
+ /** Prioritize a terminal's causal head without skipping its predecessors. */
17395
+ function runLedgerTerminalPendingSql(
17396
+ alias: string,
17397
+ options?: Pick<PostgresSchedulerOptions, 'schema'>,
17398
+ ): string {
17399
+ return `EXISTS (
17400
+ SELECT 1 FROM ${tableName('outbox', options)} AS terminal_event
17401
+ WHERE terminal_event.event_id = 'run-terminal:' || ${alias}.run_id
17402
+ AND terminal_event.run_id = ${alias}.run_id
17403
+ AND terminal_event.event_type IN (${RUN_EVENT_KIND_SQL_LITERAL.runCompleted}, ${RUN_EVENT_KIND_SQL_LITERAL.runFailed}, ${RUN_EVENT_KIND_SQL_LITERAL.runCancelled})
17404
+ AND terminal_event.delivered_at IS NULL
17405
+ AND terminal_event.failed_at IS NULL
17406
+ AND coalesce(terminal_event.next_attempt_at, terminal_event.created_at) <= now()
17407
+ )`;
17408
+ }
17409
+
17394
17410
  /**
17395
17411
  * Claim a causal prefix, preserving the v33 adapter while the additive batch
17396
17412
  * marker is absent. An enclosing transaction uses a savepoint so the missing
@@ -17463,16 +17479,14 @@ export async function readPendingPostgresSchedulerOutboxEvents(
17463
17479
  -- terminal facts. Project it first so a lifecycle backlog cannot make
17464
17480
  -- an accepted run invisible; per-run predecessor checks above retain
17465
17481
  -- strict ordering within each run.
17466
- -- A terminal head comes next: it is a run's last fact (one per run, so
17467
- -- it cannot starve anything), its predecessors are already delivered
17468
- -- (checked above), and it is what a caller is waiting on. Under a
17469
- -- lifecycle backlog, FIFO left a committed terminal queued behind other
17470
- -- runs' progress for seconds (live-run-observation-e2e: 4.9 s from
17471
- -- commit to first claim while the projector drained continuously).
17482
+ -- A run with a pending terminal comes next, including its causal
17483
+ -- predecessors. Giving priority only to a terminal head leaves a
17484
+ -- finished run's earlier progress behind unrelated progress. The
17485
+ -- predecessor checks still forbid skipping any fact within the run.
17472
17486
  ORDER BY
17473
17487
  CASE
17474
17488
  WHEN candidate.event_type = ${RUN_EVENT_KIND_SQL_LITERAL.runCreated} THEN 0
17475
- WHEN candidate.event_id LIKE 'run-terminal:%' THEN 1
17489
+ WHEN ${runLedgerTerminalPendingSql('candidate', options)} THEN 1
17476
17490
  ELSE 2
17477
17491
  END,
17478
17492
  coalesce(candidate.next_attempt_at, candidate.created_at),
@@ -18892,6 +18906,7 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
18892
18906
  input: RunLedgerOutboxDispatchInput & { waitForRunLock?: boolean },
18893
18907
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
18894
18908
  ): Promise<PostgresSchedulerOutboxProjectionResult> {
18909
+ const dispatchStartedAtMs = Date.now();
18895
18910
  const leaseOwner =
18896
18911
  input.leaseOwner?.trim() || `run-ledger-outbox-dispatch:${randomUUID()}`;
18897
18912
  const events = await withClient(async (client) => {
@@ -18986,16 +19001,16 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
18986
19001
  AND active.lease_owner IS NOT NULL
18987
19002
  AND active.lease_expires_at > now()
18988
19003
  )
18989
- -- admission_first: admission, then a run whose head is its
18990
- -- terminal (its last fact; see
18991
- -- readPendingPostgresSchedulerOutboxEvents), then the FIFO
18992
- -- backlog. oldest_ready: pure age order, the anti-starvation turn.
19004
+ -- admission_first: admission, then the causal head of a run with
19005
+ -- a pending terminal, then FIFO progress. oldest_ready keeps its
19006
+ -- pure age order so continuous admissions/terminals cannot starve
19007
+ -- ordinary progress.
18993
19008
  ORDER BY ${
18994
19009
  input.headPriority === 'oldest_ready'
18995
19010
  ? ''
18996
19011
  : `CASE
18997
19012
  WHEN head.event_type = ${RUN_EVENT_KIND_SQL_LITERAL.runCreated} THEN 0
18998
- WHEN head.event_id LIKE 'run-terminal:%' THEN 1
19013
+ WHEN ${runLedgerTerminalPendingSql('head', options)} THEN 1
18999
19014
  ELSE 2
19000
19015
  END,`
19001
19016
  }
@@ -19035,6 +19050,10 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
19035
19050
  };
19036
19051
  }
19037
19052
 
19053
+ const claimedAtMs = Date.now();
19054
+ let authorityReadyAtMs: number | null = null;
19055
+ let appendFinishedAtMs: number | null = null;
19056
+ let acknowledgedAtMs: number | null = null;
19038
19057
  try {
19039
19058
  const first = events[0]!;
19040
19059
  const tokenAuthority = input.tokenAuthority ?? 'local_mint';
@@ -19091,6 +19110,7 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
19091
19110
  // The caller-owned admission deadline (see `signal` above) wins.
19092
19111
  ...(input.signal ? { signal: input.signal } : {}),
19093
19112
  };
19113
+ authorityReadyAtMs = Date.now();
19094
19114
  const renewDelivery = () =>
19095
19115
  withClient((client) =>
19096
19116
  renewPostgresRunLedgerDelivery(
@@ -19155,6 +19175,7 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
19155
19175
  });
19156
19176
  }
19157
19177
  }
19178
+ appendFinishedAtMs = Date.now();
19158
19179
  const deliveredRows = await withClient((client) =>
19159
19180
  markPostgresSchedulerOutboxDeliveredRows(
19160
19181
  client,
@@ -19162,6 +19183,7 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
19162
19183
  { ...options, leaseOwner },
19163
19184
  ),
19164
19185
  );
19186
+ acknowledgedAtMs = Date.now();
19165
19187
  const markedDelivered = deliveredRows.length;
19166
19188
  if (input.onDelivered && deliveredRows.length > 0) {
19167
19189
  try {
@@ -19253,6 +19275,37 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
19253
19275
  retrying: settlement.retrying.length,
19254
19276
  retryingEventIds: settlement.retrying,
19255
19277
  };
19278
+ } finally {
19279
+ const settledAtMs = Date.now();
19280
+ const headAgeAtClaimMs = Math.max(
19281
+ 0,
19282
+ claimedAtMs - events[0]!.createdAt.getTime(),
19283
+ );
19284
+ if (
19285
+ settledAtMs - dispatchStartedAtMs >= 1_000 ||
19286
+ headAgeAtClaimMs >= 1_000
19287
+ ) {
19288
+ console.info('[run-ledger-outbox.slow_delivery]', {
19289
+ runId: events[0]!.runId,
19290
+ headEventId: events[0]!.eventId,
19291
+ eventCount: events.length,
19292
+ headAgeAtClaimMs,
19293
+ claimMs: claimedAtMs - dispatchStartedAtMs,
19294
+ authorityMs:
19295
+ authorityReadyAtMs === null ? null : authorityReadyAtMs - claimedAtMs,
19296
+ appendMs:
19297
+ appendFinishedAtMs === null || authorityReadyAtMs === null
19298
+ ? null
19299
+ : appendFinishedAtMs - authorityReadyAtMs,
19300
+ acknowledgeMs:
19301
+ acknowledgedAtMs === null || appendFinishedAtMs === null
19302
+ ? null
19303
+ : acknowledgedAtMs - appendFinishedAtMs,
19304
+ continuationMs:
19305
+ acknowledgedAtMs === null ? null : settledAtMs - acknowledgedAtMs,
19306
+ durationMs: settledAtMs - dispatchStartedAtMs,
19307
+ });
19308
+ }
19256
19309
  }
19257
19310
  }
19258
19311
 
@@ -51,7 +51,7 @@ const RUNTIME_EVENT_LOG_POSITION_EXPRESSION = `jsonb_set(
51
51
  // Migration readiness tracks this projection body independently of scheduler
52
52
  // wire and schema versions. Bump when a changed trigger must be reinstalled.
53
53
  export const RUNTIME_EVENT_SEARCH_OUTBOX_PROJECTION_MARKER =
54
- 'runtime-event-search-log-context-v12-scheduler-wait-logs';
54
+ 'runtime-event-search-log-context-v13-index-first-position';
55
55
  export const RUNTIME_EVENT_CANONICAL_RETENTION_INDEX_READY_MARKER =
56
56
  'runtime-event-canonical-retention-v1';
57
57
  export const RUNTIME_EVENTS_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
@@ -1018,7 +1018,6 @@ export async function attachRuntimeEventSearchTriggers(
1018
1018
  doc jsonb; line_doc jsonb; line_context jsonb;
1019
1019
  eid text; existing_digest text; digest text; assigned_seq bigint; terminal_progress_identity boolean;
1020
1020
  log_channel text; log_position bigint; matched_event_id text; matched_doc jsonb;
1021
- previous_plan_cache_mode text;
1022
1021
  legacy_position_prefix text; position_event_ids text[];
1023
1022
  log_alias jsonb; position_alias jsonb; envelope_attempt jsonb;
1024
1023
  line_count bigint; fresh_event_ids text[] := ARRAY[]::text[];
@@ -1297,23 +1296,29 @@ export async function attachRuntimeEventSearchTriggers(
1297
1296
  -- A transport may already be aliased onto an event whose
1298
1297
  -- primary key belongs to another channel. Find that alias
1299
1298
  -- independently of content so changed bytes still fail the
1300
- -- immutable-position check below. PL/pgSQL's generic plan
1301
- -- underestimates large runs and scans every row in the run;
1302
- -- plan this lookup with its concrete position so the scoped
1303
- -- GIN index can seek directly to the alias.
1304
- previous_plan_cache_mode := current_setting('plan_cache_mode');
1305
- PERFORM set_config('plan_cache_mode', 'force_custom_plan', true);
1299
+ -- immutable-position check below. Materialize the scoped
1300
+ -- position lookup before applying run filters: otherwise
1301
+ -- the planner can choose the run-history index and inspect
1302
+ -- every old log for each incoming position, even with a
1303
+ -- custom plan. The indexed document already binds scope,
1304
+ -- org, run, attempt and channel position; retain the outer
1305
+ -- identity checks as defense in depth.
1306
+ WITH position_matches AS MATERIALIZED (
1307
+ SELECT event.event_id, event.source_digest, event.search_doc,
1308
+ event.runtime_scope, event.org_id, event.run_id, event.log_seq
1309
+ FROM runtime_observability.events AS event
1310
+ WHERE event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
1311
+ AND event.search_doc ? 'producerAttempt'
1312
+ AND event.search_doc ? 'logPositions'
1313
+ AND (${RUNTIME_EVENT_LOG_POSITION_EXPRESSION.replaceAll('search_doc', 'event.search_doc')}) @> position_alias
1314
+ )
1306
1315
  SELECT event.event_id, event.source_digest, event.search_doc
1307
1316
  INTO matched_event_id, existing_digest, matched_doc
1308
- FROM runtime_observability.events AS event
1317
+ FROM position_matches AS event
1309
1318
  WHERE event.runtime_scope = ${quoteLiteral(scope)} AND event.org_id = run_org
1310
- AND event.run_id = observation.run_id AND event.kind = ${RUN_EVENT_KIND_SQL_LITERAL.logAppended}
1311
- AND event.search_doc ? 'producerAttempt'
1312
- AND event.search_doc ? 'logPositions'
1313
- AND (${RUNTIME_EVENT_LOG_POSITION_EXPRESSION.replaceAll('search_doc', 'event.search_doc')}) @> position_alias
1319
+ AND event.run_id = observation.run_id
1314
1320
  ORDER BY event.log_seq NULLS LAST, event.event_id
1315
1321
  LIMIT 1;
1316
- PERFORM set_config('plan_cache_mode', previous_plan_cache_mode, true);
1317
1322
  END IF;
1318
1323
  IF matched_event_id IS NOT NULL THEN
1319
1324
  IF existing_digest IS DISTINCT FROM digest THEN
@@ -2015,20 +2020,22 @@ type RuntimeEventIndexExpectation = {
2015
2020
  opclasses?: string[];
2016
2021
  };
2017
2022
 
2018
- const RUNTIME_EVENT_QUERY_INDEXES: RuntimeEventIndexExpectation[] = [
2019
- {
2020
- name: RUNTIME_EVENT_LOG_POSITION_INDEX,
2021
- method: 'gin',
2022
- opclasses: ['jsonb_path_ops'],
2023
- keys: [
2024
- "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)",
2025
- ],
2026
- predicate: RUNTIME_EVENT_LOG_POSITION_PREDICATE,
2027
- createSql: `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${RUNTIME_EVENT_LOG_POSITION_INDEX}
2023
+ const RUNTIME_EVENT_LOG_POSITION_INDEX_SPEC: RuntimeEventIndexExpectation = {
2024
+ name: RUNTIME_EVENT_LOG_POSITION_INDEX,
2025
+ method: 'gin',
2026
+ opclasses: ['jsonb_path_ops'],
2027
+ keys: [
2028
+ "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)",
2029
+ ],
2030
+ predicate: RUNTIME_EVENT_LOG_POSITION_PREDICATE,
2031
+ createSql: `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${RUNTIME_EVENT_LOG_POSITION_INDEX}
2028
2032
  ON runtime_observability.events USING gin
2029
2033
  ((${RUNTIME_EVENT_LOG_POSITION_EXPRESSION}) jsonb_path_ops)
2030
2034
  WHERE ${RUNTIME_EVENT_LOG_POSITION_PREDICATE}`,
2031
- },
2035
+ };
2036
+
2037
+ const RUNTIME_EVENT_QUERY_INDEXES: RuntimeEventIndexExpectation[] = [
2038
+ RUNTIME_EVENT_LOG_POSITION_INDEX_SPEC,
2032
2039
  {
2033
2040
  name: RUNTIME_EVENT_LOG_DEDUPE_INDEX,
2034
2041
  keys: [
@@ -2062,92 +2069,126 @@ const RUNTIME_EVENT_QUERY_INDEXES: RuntimeEventIndexExpectation[] = [
2062
2069
  },
2063
2070
  ];
2064
2071
 
2065
- /** Build only operator-owned indexes, concurrently and outside a transaction. */
2066
- export async function prepareRuntimeEventQueryIndexes(
2072
+ async function hasRuntimeEventQueryIndex(
2067
2073
  client: PostgresSchedulerQueryClient,
2068
- ): Promise<void> {
2069
- for (const expected of RUNTIME_EVENT_QUERY_INDEXES) {
2070
- type IndexShape = {
2071
- valid: boolean;
2072
- ready: boolean;
2073
- is_unique: boolean;
2074
- table_matches: boolean;
2075
- key_count: number;
2076
- attribute_count: number;
2077
- method: string;
2078
- keys: string[];
2079
- opclasses: string[];
2080
- predicate: string | null;
2081
- };
2082
- const inspect = () =>
2083
- client.query<IndexShape>(
2084
- `SELECT index_row.indisvalid AS valid,
2085
- index_row.indisready AS ready,
2086
- index_row.indisunique AS is_unique,
2087
- index_row.indrelid = to_regclass('runtime_observability.events') AS table_matches,
2088
- index_row.indnkeyatts AS key_count,
2089
- index_row.indnatts AS attribute_count,
2090
- method.amname AS method,
2091
- ARRAY(
2092
- SELECT pg_get_indexdef(index_row.indexrelid, key_number.position, true)
2093
- FROM generate_series(1, index_row.indnkeyatts) AS key_number(position)
2094
- ORDER BY key_number.position
2095
- ) AS keys,
2096
- (
2097
- SELECT coalesce(json_agg(operator_class.opcname ORDER BY classes.position), '[]'::json)
2098
- FROM unnest(index_row.indclass) WITH ORDINALITY AS classes(opclass_oid, position)
2099
- JOIN pg_opclass AS operator_class ON operator_class.oid = classes.opclass_oid
2100
- ) AS opclasses,
2101
- pg_get_expr(index_row.indpred, index_row.indrelid) AS predicate
2102
- FROM pg_index AS index_row
2103
- JOIN pg_class AS index_class ON index_class.oid = index_row.indexrelid
2104
- JOIN pg_am AS method ON method.oid = index_class.relam
2105
- WHERE index_class.oid = to_regclass($1)`,
2106
- [`runtime_observability.${expected.name}`],
2074
+ expected: RuntimeEventIndexExpectation,
2075
+ ): Promise<boolean> {
2076
+ type IndexShape = {
2077
+ valid: boolean;
2078
+ ready: boolean;
2079
+ is_unique: boolean;
2080
+ table_matches: boolean;
2081
+ key_count: number;
2082
+ attribute_count: number;
2083
+ method: string;
2084
+ keys: string[];
2085
+ opclasses: string[];
2086
+ predicate: string | null;
2087
+ };
2088
+ const inspect = () =>
2089
+ client.query<IndexShape>(
2090
+ `SELECT index_row.indisvalid AS valid,
2091
+ index_row.indisready AS ready,
2092
+ index_row.indisunique AS is_unique,
2093
+ index_row.indrelid = to_regclass('runtime_observability.events') AS table_matches,
2094
+ index_row.indnkeyatts AS key_count,
2095
+ index_row.indnatts AS attribute_count,
2096
+ method.amname AS method,
2097
+ ARRAY(
2098
+ SELECT pg_get_indexdef(index_row.indexrelid, key_number.position, true)
2099
+ FROM generate_series(1, index_row.indnkeyatts) AS key_number(position)
2100
+ ORDER BY key_number.position
2101
+ ) AS keys,
2102
+ (
2103
+ SELECT coalesce(json_agg(operator_class.opcname ORDER BY classes.position), '[]'::json)
2104
+ FROM unnest(index_row.indclass) WITH ORDINALITY AS classes(opclass_oid, position)
2105
+ JOIN pg_opclass AS operator_class ON operator_class.oid = classes.opclass_oid
2106
+ ) AS opclasses,
2107
+ pg_get_expr(index_row.indpred, index_row.indrelid) AS predicate
2108
+ FROM pg_index AS index_row
2109
+ JOIN pg_class AS index_class ON index_class.oid = index_row.indexrelid
2110
+ JOIN pg_am AS method ON method.oid = index_class.relam
2111
+ WHERE index_class.oid = to_regclass($1)`,
2112
+ [`runtime_observability.${expected.name}`],
2113
+ );
2114
+ const assertShape = (shape: IndexShape): void => {
2115
+ if (!shape.valid || !shape.ready) {
2116
+ throw new Error(
2117
+ `Runtime event index ${expected.name} is invalid or incomplete; inspect and repair the failed build explicitly.`,
2107
2118
  );
2108
- const assertShape = (shape: IndexShape): void => {
2109
- if (!shape.valid || !shape.ready) {
2110
- throw new Error(
2111
- `Runtime event index ${expected.name} is invalid or incomplete; inspect and repair the failed build explicitly.`,
2112
- );
2113
- }
2114
- if (
2115
- !shape.table_matches ||
2116
- shape.is_unique ||
2117
- shape.method !== (expected.method ?? 'btree') ||
2118
- (expected.opclasses !== undefined &&
2119
- (shape.opclasses.length !== expected.opclasses.length ||
2120
- shape.opclasses.some(
2121
- (name, index) => name !== expected.opclasses![index],
2122
- ))) ||
2123
- shape.key_count !== expected.keys.length ||
2124
- shape.attribute_count !== expected.keys.length ||
2125
- shape.keys.length !== expected.keys.length ||
2126
- shape.keys.some(
2127
- (key, index) =>
2128
- normalizedRetentionPredicate(key) !==
2129
- normalizedRetentionPredicate(expected.keys[index]!),
2130
- ) ||
2131
- shape.predicate === null ||
2132
- normalizedQueryIndexPredicate(shape.predicate) !==
2133
- normalizedQueryIndexPredicate(expected.predicate)
2134
- ) {
2135
- throw new Error(
2136
- `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.`,
2137
- );
2138
- }
2139
- };
2140
-
2141
- const existing = (await inspect()).rows[0];
2142
- if (existing) assertShape(existing);
2143
- await client.query(expected.createSql);
2144
- const built = (await inspect()).rows[0];
2145
- if (!built) {
2119
+ }
2120
+ if (
2121
+ !shape.table_matches ||
2122
+ shape.is_unique ||
2123
+ shape.method !== (expected.method ?? 'btree') ||
2124
+ (expected.opclasses !== undefined &&
2125
+ (shape.opclasses.length !== expected.opclasses.length ||
2126
+ shape.opclasses.some(
2127
+ (name, index) => name !== expected.opclasses![index],
2128
+ ))) ||
2129
+ shape.key_count !== expected.keys.length ||
2130
+ shape.attribute_count !== expected.keys.length ||
2131
+ shape.keys.length !== expected.keys.length ||
2132
+ shape.keys.some(
2133
+ (key, index) =>
2134
+ normalizedRetentionPredicate(key) !==
2135
+ normalizedRetentionPredicate(expected.keys[index]!),
2136
+ ) ||
2137
+ shape.predicate === null ||
2138
+ normalizedQueryIndexPredicate(shape.predicate) !==
2139
+ normalizedQueryIndexPredicate(expected.predicate)
2140
+ ) {
2146
2141
  throw new Error(
2147
- `Runtime event index ${expected.name} was not found after preparation.`,
2142
+ `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.`,
2148
2143
  );
2149
2144
  }
2150
- assertShape(built);
2145
+ };
2146
+
2147
+ const existing = (await inspect()).rows[0];
2148
+ if (!existing) return false;
2149
+ assertShape(existing);
2150
+ return true;
2151
+ }
2152
+
2153
+ async function prepareRuntimeEventQueryIndex(
2154
+ client: PostgresSchedulerQueryClient,
2155
+ expected: RuntimeEventIndexExpectation,
2156
+ ): Promise<void> {
2157
+ if (await hasRuntimeEventQueryIndex(client, expected)) return;
2158
+ await client.query(expected.createSql);
2159
+ if (!(await hasRuntimeEventQueryIndex(client, expected))) {
2160
+ throw new Error(
2161
+ `Runtime event index ${expected.name} was not found after preparation.`,
2162
+ );
2163
+ }
2164
+ }
2165
+
2166
+ /** Deployment-only check; does not gate requests or worker admission. */
2167
+ export async function hasRuntimeEventLogPositionIndex(
2168
+ client: PostgresSchedulerQueryClient,
2169
+ ): Promise<boolean> {
2170
+ return hasRuntimeEventQueryIndex(
2171
+ client,
2172
+ RUNTIME_EVENT_LOG_POSITION_INDEX_SPEC,
2173
+ );
2174
+ }
2175
+
2176
+ /** Prepare the existing position index before replacing the projection body. */
2177
+ export async function prepareRuntimeEventLogPositionIndex(
2178
+ client: PostgresSchedulerQueryClient,
2179
+ ): Promise<void> {
2180
+ await prepareRuntimeEventQueryIndex(
2181
+ client,
2182
+ RUNTIME_EVENT_LOG_POSITION_INDEX_SPEC,
2183
+ );
2184
+ }
2185
+
2186
+ /** Build only operator-owned indexes, concurrently and outside a transaction. */
2187
+ export async function prepareRuntimeEventQueryIndexes(
2188
+ client: PostgresSchedulerQueryClient,
2189
+ ): Promise<void> {
2190
+ for (const expected of RUNTIME_EVENT_QUERY_INDEXES) {
2191
+ await prepareRuntimeEventQueryIndex(client, expected);
2151
2192
  }
2152
2193
  }
2153
2194
 
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.276",
3515
+ version: "0.3.278",
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
@@ -13502,12 +13502,14 @@ async function handleTopUpRecover(creditsRaw, options) {
13502
13502
  );
13503
13503
  }
13504
13504
  async function handleTargetPlan(planSku, options) {
13505
- if (planSku !== "payg-v1" && planSku !== "builder-v1" && planSku !== "growth-v1" && planSku !== "growth-canary-v1" && planSku !== "team-v1") {
13505
+ if (planSku !== "payg-v1" && planSku !== "builder-v1" && planSku !== "growth-v1" && // Retained Canary requests may replay their original operation. The API
13506
+ // rejects new acquisition even when a caller supplies an idempotency key.
13507
+ !(planSku === "growth-canary-v1" && options.idempotencyKey) && planSku !== "team-v1") {
13506
13508
  reportBillingFailure(
13507
13509
  {
13508
13510
  exitCode: 2,
13509
13511
  code: "INVALID_PLAN",
13510
- message: "Plan must be payg-v1, builder-v1, growth-v1, growth-canary-v1, or team-v1."
13512
+ message: "Plan must be payg-v1, builder-v1, growth-v1, or team-v1."
13511
13513
  },
13512
13514
  options
13513
13515
  );
@@ -13818,10 +13820,7 @@ Examples:
13818
13820
  printCommandEnvelope({ ok: true, ...result }, { json: options.json });
13819
13821
  })
13820
13822
  );
13821
- billing.command("change-plan").description("Start or change the target billing plan.").argument(
13822
- "<plan_sku>",
13823
- "payg-v1, builder-v1, growth-v1, growth-canary-v1, or team-v1"
13824
- ).option(
13823
+ billing.command("change-plan").description("Start or change the target billing plan.").argument("<plan_sku>", "payg-v1, builder-v1, growth-v1, or team-v1").option(
13825
13824
  "--billing-email <email>",
13826
13825
  "Invoice recipient when the account has no billing email"
13827
13826
  ).option("--idempotency-key <key>", "Stable retry key").option("--json", "Emit JSON output").action(handleTargetPlan);
@@ -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.276",
3510
+ version: "0.3.278",
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
@@ -13509,12 +13509,14 @@ async function handleTopUpRecover(creditsRaw, options) {
13509
13509
  );
13510
13510
  }
13511
13511
  async function handleTargetPlan(planSku, options) {
13512
- if (planSku !== "payg-v1" && planSku !== "builder-v1" && planSku !== "growth-v1" && planSku !== "growth-canary-v1" && planSku !== "team-v1") {
13512
+ if (planSku !== "payg-v1" && planSku !== "builder-v1" && planSku !== "growth-v1" && // Retained Canary requests may replay their original operation. The API
13513
+ // rejects new acquisition even when a caller supplies an idempotency key.
13514
+ !(planSku === "growth-canary-v1" && options.idempotencyKey) && planSku !== "team-v1") {
13513
13515
  reportBillingFailure(
13514
13516
  {
13515
13517
  exitCode: 2,
13516
13518
  code: "INVALID_PLAN",
13517
- message: "Plan must be payg-v1, builder-v1, growth-v1, growth-canary-v1, or team-v1."
13519
+ message: "Plan must be payg-v1, builder-v1, growth-v1, or team-v1."
13518
13520
  },
13519
13521
  options
13520
13522
  );
@@ -13825,10 +13827,7 @@ Examples:
13825
13827
  printCommandEnvelope({ ok: true, ...result }, { json: options.json });
13826
13828
  })
13827
13829
  );
13828
- billing.command("change-plan").description("Start or change the target billing plan.").argument(
13829
- "<plan_sku>",
13830
- "payg-v1, builder-v1, growth-v1, growth-canary-v1, or team-v1"
13831
- ).option(
13830
+ billing.command("change-plan").description("Start or change the target billing plan.").argument("<plan_sku>", "payg-v1, builder-v1, growth-v1, or team-v1").option(
13832
13831
  "--billing-email <email>",
13833
13832
  "Invoice recipient when the account has no billing email"
13834
13833
  ).option("--idempotency-key <key>", "Stable retry key").option("--json", "Emit JSON output").action(handleTargetPlan);
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.276",
1328
+ version: "0.3.278",
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.276",
1232
+ version: "0.3.278",
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.276";
152
+ readonly version: "0.3.278";
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.276";
152
+ readonly version: "0.3.278";
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.276",
77
+ version: "0.3.278",
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.276",
51
+ version: "0.3.278",
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.276",
3
+ "version": "0.3.278",
4
4
  "description": "GTM data CLI and TypeScript SDK for coding agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://code.deepline.com",