deepline 0.3.197 → 0.3.199

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.197',
203
+ version: '0.3.199',
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: {
@@ -13735,51 +13735,70 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13735
13735
  const suspendAfterTimeoutMs = timeoutPolicy
13736
13736
  ? timeoutPolicy.suspendAfterTimeoutMs
13737
13737
  : handler.suspendAfterTimeoutMs;
13738
- try {
13739
- await handler.onTimeout?.({
13740
- payload: input,
13741
- context: eventContext,
13742
- boundary: {
13743
- boundaryId: preparedBoundary.boundaryId,
13744
- eventKey: preparedBoundary.eventKey,
13745
- timeoutMs: preparedBoundary.timeoutMs,
13746
- provider: existing.provider ?? handler.provider,
13747
- toolId: existing.toolId ?? handler.toolId,
13748
- ...(existing.messageRef
13749
- ? { messageRef: existing.messageRef }
13750
- : {}),
13751
- ...(existing.requestKey
13752
- ? { requestKey: existing.requestKey }
13753
- : {}),
13754
- ...(existing.interactionId
13755
- ? { interactionId: existing.interactionId }
13738
+ let timeoutFinalizedAt = existing.timeoutFinalizedAt;
13739
+ // This is a replay acknowledgement, not exactly-once execution: a
13740
+ // crash after the provider succeeds but before this checkpoint is
13741
+ // durable can still repeat the finalizer on a later attempt.
13742
+ if (timeoutFinalizedAt === undefined && handler.onTimeout) {
13743
+ try {
13744
+ await handler.onTimeout({
13745
+ payload: input,
13746
+ context: eventContext,
13747
+ boundary: {
13748
+ boundaryId: preparedBoundary.boundaryId,
13749
+ eventKey: preparedBoundary.eventKey,
13750
+ timeoutMs: preparedBoundary.timeoutMs,
13751
+ provider: existing.provider ?? handler.provider,
13752
+ toolId: existing.toolId ?? handler.toolId,
13753
+ ...(existing.messageRef
13754
+ ? { messageRef: existing.messageRef }
13755
+ : {}),
13756
+ ...(existing.requestKey
13757
+ ? { requestKey: existing.requestKey }
13758
+ : {}),
13759
+ ...(existing.interactionId
13760
+ ? { interactionId: existing.interactionId }
13761
+ : {}),
13762
+ ...(timeoutPolicy ? { timeoutPolicy } : {}),
13763
+ },
13764
+ });
13765
+ timeoutFinalizedAt = Date.now();
13766
+ } catch (error) {
13767
+ this.recordSlackHitlTimeoutFailure(error);
13768
+ throw error;
13769
+ }
13770
+ }
13771
+ const persistTimeoutFinalization = (completedAt?: number) => {
13772
+ if (
13773
+ timeoutFinalizedAt === existing.timeoutFinalizedAt &&
13774
+ completedAt === existing.completedAt
13775
+ ) {
13776
+ return;
13777
+ }
13778
+ this.checkpoint.resolvedBoundaries = {
13779
+ ...(this.checkpoint.resolvedBoundaries ?? {}),
13780
+ [preparedBoundary.boundaryId]: {
13781
+ ...existing,
13782
+ ...(completedAt !== undefined ? { completedAt } : {}),
13783
+ ...(timeoutFinalizedAt !== undefined
13784
+ ? { timeoutFinalizedAt }
13756
13785
  : {}),
13757
- ...(timeoutPolicy ? { timeoutPolicy } : {}),
13758
13786
  },
13759
- });
13760
- } catch (error) {
13761
- this.recordSlackHitlTimeoutFailure(error);
13762
- throw error;
13763
- }
13787
+ };
13788
+ this.#options.onBatchComplete?.(this.checkpoint);
13789
+ };
13764
13790
  if (timeoutPolicy?.timeoutOutcome === 'error') {
13765
13791
  // The provider finalizer above must complete first. That ordering
13766
13792
  // makes an expired Slack card and the terminal Play state agree,
13767
13793
  // while provider finalization failures stay retryable instead.
13794
+ persistTimeoutFinalization();
13768
13795
  const error = new SlackHitlTimeoutError(timeoutPolicy.timeoutError);
13769
13796
  this.recordSlackHitlTimeoutFailure(error);
13770
13797
  throw error;
13771
13798
  }
13772
13799
  if (suspendAfterTimeoutMs !== undefined) {
13773
13800
  const finalizedAtMs = existing.completedAt ?? Date.now();
13774
- if (existing.completedAt === undefined) {
13775
- this.checkpoint.resolvedBoundaries = {
13776
- ...(this.checkpoint.resolvedBoundaries ?? {}),
13777
- [preparedBoundary.boundaryId]: {
13778
- ...existing,
13779
- completedAt: finalizedAtMs,
13780
- },
13781
- };
13782
- }
13801
+ persistTimeoutFinalization(finalizedAtMs);
13783
13802
  this.runtimeLog(
13784
13803
  `Integration event ${preparedBoundary.boundaryId}: timeout finalized externally; keeping Play parked`,
13785
13804
  );
@@ -13792,6 +13811,8 @@ export class PlayContextImpl implements ScalarPlayAuthoringRuntimeContext {
13792
13811
  postTimeoutPark: true,
13793
13812
  ...(existing.messageRef ? { messageRef: existing.messageRef } : {}),
13794
13813
  });
13814
+ } else {
13815
+ persistTimeoutFinalization();
13795
13816
  }
13796
13817
  }
13797
13818
  this.runtimeLog(
@@ -1210,6 +1210,8 @@ export interface PlayCheckpoint {
1210
1210
  timeoutPolicy?: IntegrationEventWaitBoundary['timeoutPolicy'];
1211
1211
  output?: unknown;
1212
1212
  completedAt?: number;
1213
+ /** Timestamp of the successful provider timeout finalizer, if any. */
1214
+ timeoutFinalizedAt?: number;
1213
1215
  }
1214
1216
  | {
1215
1217
  kind: 'fetch';
@@ -78,19 +78,44 @@ type PreviewOptions = {
78
78
 
79
79
  const utf8Encoder = new TextEncoder();
80
80
  const PLAY_DATASET_BRAND = Symbol.for('deepline.play.dataset');
81
+ const MAX_CONVEX_PREVIEW_KEY_LENGTH = 1_024;
81
82
 
82
83
  /**
83
84
  * Preview-map keys must be persistable everywhere the ledger lands, and Convex
84
- * reserves field names beginning with '$' (a terminal flush carrying one 500s,
85
- * dropping the WHOLE terminal event batch). `$output` is the wrapper's
86
- * whole-result path; strip the sigil so previews survive every store. Applied
87
- * at every build/normalize seam so live events, durable observations, and
88
- * replayed historical payloads all converge on the safe representation.
85
+ * reserves leading '$', non-ASCII/control characters, and keys over 1,024
86
+ * bytes. A rejected preview key poisons the whole ordered terminal batch, even
87
+ * when the terminal result is replaced by a scheduler reference. Keep already
88
+ * valid keys unchanged and encode only invalid bytes, so repeated normalization
89
+ * of durable observations converges on the same key. `$output` remains `output`.
89
90
  */
90
91
  export function ledgerSafeDocflowPreviewKey(key: string): string {
91
- if (!key.startsWith('$')) return key;
92
- const stripped = key.replace(/^\$+/, '');
93
- return stripped || 'output';
92
+ const source = key.startsWith('$')
93
+ ? key.replace(/^\$+/, '') || 'output'
94
+ : key || 'output';
95
+ let encoded = '';
96
+ for (const character of source) {
97
+ const code = character.charCodeAt(0);
98
+ if (code >= 32 && code < 127) {
99
+ encoded += character;
100
+ continue;
101
+ }
102
+ for (const byte of utf8Encoder.encode(character)) {
103
+ encoded += `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;
104
+ }
105
+ }
106
+ if (encoded.length <= MAX_CONVEX_PREVIEW_KEY_LENGTH) return encoded;
107
+
108
+ // Retain a readable prefix while making long, otherwise identical paths
109
+ // distinguishable. The suffix is stable across runtime and app deployments.
110
+ let hashA = 0x811c9dc5;
111
+ let hashB = 0x9e3779b9;
112
+ for (let index = 0; index < encoded.length; index += 1) {
113
+ const code = encoded.charCodeAt(index);
114
+ hashA = Math.imul(hashA ^ code, 0x01000193);
115
+ hashB = Math.imul(hashB ^ code, 0x01000193);
116
+ }
117
+ const suffix = `~${(hashA >>> 0).toString(16).padStart(8, '0')}${(hashB >>> 0).toString(16).padStart(8, '0')}`;
118
+ return `${encoded.slice(0, MAX_CONVEX_PREVIEW_KEY_LENGTH - suffix.length)}${suffix}`;
94
119
  }
95
120
 
96
121
  function utf8Bytes(value: string): number {
@@ -226,4 +226,5 @@ const fullenrichBulkSelfBatchStrategy: BatchOperationStrategy<
226
226
 
227
227
  export const fullenrichBatchStrategies = defineBatchStrategyMap({
228
228
  fullenrich_bulk_enrich: fullenrichBulkSelfBatchStrategy,
229
+ fullenrich_enrich: fullenrichBulkSelfBatchStrategy,
229
230
  });
@@ -26,7 +26,5 @@ export function getPlayRuntimeBatchStrategy(
26
26
  if (!operation) {
27
27
  return null;
28
28
  }
29
- const canonicalOperation =
30
- operation === 'fullenrich_enrich' ? 'fullenrich_bulk_enrich' : operation;
31
- return PLAY_RUNTIME_BATCH_OPERATION_REGISTRY[canonicalOperation] ?? null;
29
+ return PLAY_RUNTIME_BATCH_OPERATION_REGISTRY[operation] ?? null;
32
30
  }
@@ -12213,10 +12213,9 @@ export async function cancelPostgresSchedulerRun(
12213
12213
  engineTaskTable: string | null;
12214
12214
  };
12215
12215
  /**
12216
- * Optional control-plane ownership for the pending causal prefix. The
12217
- * caller resumes this same lease after commit to make cancellation visible
12218
- * synchronously in the canonical Run Ledger, even when a later predecessor
12219
- * was waiting on its retry schedule.
12216
+ * Optional control-plane ownership for the oldest pending causal fact.
12217
+ * Cancellation makes the remaining prefix due without sharing this lease
12218
+ * across distinct append batches, then the caller drains it after commit.
12220
12219
  */
12221
12220
  outboxLease?: { leaseOwner: string; leaseSeconds: number };
12222
12221
  },
@@ -12349,11 +12348,15 @@ export async function cancelPostgresSchedulerRun(
12349
12348
  AND NULLIF(resource->>'sandboxId', '') IS NOT NULL
12350
12349
  ON CONFLICT (provider, sandbox_id, routing_domain_key) DO NOTHING
12351
12350
  ),
12352
- -- A head-only fence leaves a delayed successor between this request
12353
- -- and its terminal event. Own and make due the complete pending prefix;
12354
- -- the projector still claims it in causal order after commit.
12351
+ -- Revoke any in-flight background lease and make the complete prefix
12352
+ -- due, but reserve only its head. The app-side delivery validator treats
12353
+ -- every row under one lease owner as one exact append batch. Leasing the
12354
+ -- whole prefix here makes the first run.events append fail that check.
12355
+ -- The head lease fences background claims until the direct projector
12356
+ -- acknowledges it, then the next causal head can be claimed normally.
12355
12357
  cancellation_outbox_pending AS MATERIALIZED (
12356
- SELECT pending.event_id
12358
+ SELECT pending.event_id,
12359
+ row_number() OVER (ORDER BY ${runLedgerOrderSql('pending')}) AS position
12357
12360
  FROM ${tableName('outbox', options)} AS pending
12358
12361
  JOIN cancelled_run ON cancelled_run.run_id = pending.run_id
12359
12362
  WHERE pending.delivered_at IS NULL
@@ -12361,11 +12364,15 @@ export async function cancelPostgresSchedulerRun(
12361
12364
  ),
12362
12365
  cancellation_outbox_lease AS (
12363
12366
  UPDATE ${tableName('outbox', options)} AS pending
12364
- SET lease_owner = $6::text,
12365
- lease_expires_at = now() + make_interval(secs => $7::integer),
12367
+ SET lease_owner = CASE WHEN prefix.position = 1 THEN $6::text ELSE NULL END,
12368
+ lease_expires_at = CASE
12369
+ WHEN prefix.position = 1 THEN now() + make_interval(secs => $7::integer)
12370
+ ELSE NULL
12371
+ END,
12366
12372
  next_attempt_at = NULL
12373
+ FROM cancellation_outbox_pending AS prefix
12367
12374
  WHERE $6::text IS NOT NULL
12368
- AND pending.event_id IN (SELECT event_id FROM cancellation_outbox_pending)
12375
+ AND pending.event_id = prefix.event_id
12369
12376
  RETURNING pending.event_id
12370
12377
  ),
12371
12378
  terminal_outbox AS (
@@ -12420,16 +12427,26 @@ export async function cancelPostgresSchedulerRun(
12420
12427
  // commits so the request can still project the winning terminal state
12421
12428
  // without waiting for a background lease.
12422
12429
  await client.query(
12423
- `UPDATE ${tableName('outbox', options)} AS pending
12424
- SET lease_owner = $2,
12425
- lease_expires_at = now() + make_interval(secs => $3::integer),
12426
- next_attempt_at = NULL
12427
- FROM ${tableName('runs', options)} AS terminal_run
12428
- WHERE pending.run_id = $1
12429
- AND terminal_run.run_id = pending.run_id
12430
- AND terminal_run.status IN ('completed', 'failed', 'cancelled')
12431
- AND pending.delivered_at IS NULL
12432
- AND pending.failed_at IS NULL`,
12430
+ `WITH terminal_pending AS MATERIALIZED (
12431
+ SELECT pending.event_id,
12432
+ row_number() OVER (ORDER BY ${runLedgerOrderSql('pending')}) AS position
12433
+ FROM ${tableName('outbox', options)} AS pending
12434
+ JOIN ${tableName('runs', options)} AS terminal_run
12435
+ ON terminal_run.run_id = pending.run_id
12436
+ WHERE pending.run_id = $1
12437
+ AND terminal_run.status IN ('completed', 'failed', 'cancelled')
12438
+ AND pending.delivered_at IS NULL
12439
+ AND pending.failed_at IS NULL
12440
+ )
12441
+ UPDATE ${tableName('outbox', options)} AS pending
12442
+ SET lease_owner = CASE WHEN prefix.position = 1 THEN $2 ELSE NULL END,
12443
+ lease_expires_at = CASE
12444
+ WHEN prefix.position = 1 THEN now() + make_interval(secs => $3::integer)
12445
+ ELSE NULL
12446
+ END,
12447
+ next_attempt_at = NULL
12448
+ FROM terminal_pending AS prefix
12449
+ WHERE pending.event_id = prefix.event_id`,
12433
12450
  [input.runId, outboxLeaseOwner, outboxLeaseSeconds],
12434
12451
  );
12435
12452
  }
@@ -15697,7 +15714,7 @@ export async function renewPostgresRunLedgerDelivery(
15697
15714
  throw new RunLedgerProjectionLeaseLostError();
15698
15715
  }
15699
15716
 
15700
- /** Only a currently claimed outbox batch may use the projection write path. */
15717
+ /** Only the currently claimed causal prefix may use the projection write path. */
15701
15718
  export async function assertPostgresRunLedgerDelivery(
15702
15719
  client: PostgresSchedulerQueryClient,
15703
15720
  input: {
@@ -15711,64 +15728,106 @@ export async function assertPostgresRunLedgerDelivery(
15711
15728
  const result = await client.query<{
15712
15729
  delivery_key: string;
15713
15730
  event_type: string;
15714
- lease_owner: string;
15731
+ lease_owner: string | null;
15732
+ lease_live: boolean;
15733
+ due: boolean;
15734
+ blocked: boolean;
15715
15735
  }>(
15716
15736
  `
15717
- SELECT outbox.delivery_key, outbox.event_type, outbox.lease_owner
15737
+ SELECT outbox.delivery_key,
15738
+ outbox.event_type,
15739
+ outbox.lease_owner,
15740
+ outbox.lease_expires_at > now() AS lease_live,
15741
+ coalesce(outbox.next_attempt_at, outbox.created_at) <= now() AS due,
15742
+ outbox.failed_at IS NOT NULL AS blocked
15718
15743
  FROM ${tableName('outbox', options)} AS outbox
15719
15744
  JOIN ${tableName('runs', options)} AS runs ON runs.run_id = outbox.run_id
15720
15745
  WHERE outbox.run_id = $1 AND runs.org_id = $2
15721
- AND outbox.delivered_at IS NULL AND outbox.failed_at IS NULL
15722
- AND outbox.lease_owner IS NOT NULL AND outbox.lease_expires_at > now()
15723
- AND ($3::text IS NULL OR outbox.lease_owner = $3)
15724
- ORDER BY outbox.created_at, outbox.event_id
15746
+ AND outbox.delivered_at IS NULL
15747
+ ORDER BY ${runLedgerOrderSql('outbox')}
15725
15748
  `,
15726
- [input.runId, input.orgId, input.leaseOwner ?? null],
15749
+ [input.runId, input.orgId],
15727
15750
  );
15728
- const groups = new Map<string, typeof result.rows>();
15729
- for (const row of result.rows) {
15730
- const group = groups.get(row.lease_owner) ?? [];
15731
- group.push(row);
15732
- groups.set(row.lease_owner, group);
15751
+ const rows = result.rows;
15752
+ const leaseOwner = input.leaseOwner?.trim() || rows[0]?.lease_owner;
15753
+ if (!leaseOwner || rows[0]?.lease_owner !== leaseOwner) {
15754
+ throw new RunLedgerProjectionLeaseLostError();
15733
15755
  }
15734
- for (const rows of groups.values()) {
15735
- const digest = createHash('sha256')
15736
- .update(rows.map((row) => row.delivery_key).join('\n'))
15737
- .digest('hex');
15738
- const batchKey = buildRunLedgerEventsIdempotencyKey({
15739
- runId: input.runId,
15740
- deliveryDigest: digest,
15741
- });
15742
- const terminal = [...rows]
15743
- .reverse()
15744
- .find((row) =>
15745
- ['run.completed', 'run.failed', 'run.cancelled'].includes(
15746
- row.event_type,
15747
- ),
15748
- );
15749
- // The old one-row dispatcher uses run-terminal for nonterminal facts too.
15750
- const single = rows.length === 1 ? rows[0] : terminal;
15751
- const terminalKey = single
15752
- ? buildRunLedgerTerminalIdempotencyKey({
15756
+
15757
+ // A normal projector leases only the batch it is about to send. Cancellation
15758
+ // intentionally transfers the entire pending causal prefix to one stable
15759
+ // owner before synchronously draining it, while run.events are still claimed
15760
+ // one row at a time. Validate the submitted key against a *contiguous due
15761
+ // prefix*, not every row carrying that owner; otherwise a valid cancellation
15762
+ // head is rejected whenever later facts share the cancellation lease.
15763
+ const prefix: typeof rows = [];
15764
+ for (const row of rows) {
15765
+ if (
15766
+ row.lease_owner !== leaseOwner ||
15767
+ row.lease_live !== true ||
15768
+ row.due !== true ||
15769
+ row.blocked === true
15770
+ ) {
15771
+ break;
15772
+ }
15773
+ if (prefix.length > 0 && row.event_type === 'run.events') break;
15774
+ prefix.push(row);
15775
+
15776
+ const first = prefix[0]!;
15777
+ const terminal = prefix.find((candidate) =>
15778
+ ['run.completed', 'run.failed', 'run.cancelled'].includes(
15779
+ candidate.event_type,
15780
+ ),
15781
+ );
15782
+ let expectedKey: string;
15783
+ if (terminal) {
15784
+ expectedKey = buildRunLedgerTerminalIdempotencyKey({
15785
+ runId: input.runId,
15786
+ deliveryKey: terminal.delivery_key,
15787
+ });
15788
+ } else if (
15789
+ prefix.length === 1 &&
15790
+ ['run.created', 'run.started'].includes(first.event_type)
15791
+ ) {
15792
+ expectedKey = buildRunLedgerStartIdempotencyKey({
15793
+ runId: input.runId,
15794
+ deliveryKey: first.delivery_key,
15795
+ });
15796
+ } else {
15797
+ const digest = createHash('sha256')
15798
+ .update(prefix.map((candidate) => candidate.delivery_key).join('\n'))
15799
+ .digest('hex');
15800
+ expectedKey = buildRunLedgerEventsIdempotencyKey({
15801
+ runId: input.runId,
15802
+ deliveryDigest: digest,
15803
+ });
15804
+ }
15805
+ if (input.idempotencyKey === expectedKey) return;
15806
+
15807
+ // The pre-batch projector used run-terminal:<delivery-key> for a single
15808
+ // nonterminal fact. Retain that wire-compatibility key for the current
15809
+ // singleton head, but never let it authorize a later row in the prefix.
15810
+ if (
15811
+ prefix.length === 1 &&
15812
+ !['run.created', 'run.started'].includes(first.event_type) &&
15813
+ input.idempotencyKey ===
15814
+ buildRunLedgerTerminalIdempotencyKey({
15753
15815
  runId: input.runId,
15754
- deliveryKey: single.delivery_key,
15816
+ deliveryKey: first.delivery_key,
15755
15817
  })
15756
- : null;
15757
- const startKey =
15758
- rows.length === 1 &&
15759
- single &&
15760
- ['run.created', 'run.started'].includes(single.event_type)
15761
- ? buildRunLedgerStartIdempotencyKey({
15762
- runId: input.runId,
15763
- deliveryKey: single.delivery_key,
15764
- })
15765
- : null;
15766
- if (
15767
- input.idempotencyKey === batchKey ||
15768
- input.idempotencyKey === terminalKey ||
15769
- input.idempotencyKey === startKey
15770
- )
15818
+ ) {
15771
15819
  return;
15820
+ }
15821
+
15822
+ if (
15823
+ prefix.length === 1 &&
15824
+ ['run.created', 'run.started', 'run.events'].includes(first.event_type)
15825
+ ) {
15826
+ break;
15827
+ }
15828
+ if (terminal || prefix.length >= 8) {
15829
+ break;
15830
+ }
15772
15831
  }
15773
15832
  throw new RunLedgerProjectionLeaseLostError();
15774
15833
  }
@@ -16948,6 +17007,7 @@ export async function dispatchRunLedgerOutboxToLedgerThroughEventWithClientScope
16948
17007
  withClient,
16949
17008
  {
16950
17009
  ...input,
17010
+ waitForRunLock: true,
16951
17011
  limit:
16952
17012
  remaining === null ? batchSize : Math.min(batchSize, remaining),
16953
17013
  },
@@ -17022,7 +17082,7 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
17022
17082
  withClient: <T>(
17023
17083
  operation: (client: PostgresSchedulerQueryClient) => Promise<T>,
17024
17084
  ) => Promise<T>,
17025
- input: RunLedgerOutboxDispatchInput,
17085
+ input: RunLedgerOutboxDispatchInput & { waitForRunLock?: boolean },
17026
17086
  options?: Pick<PostgresSchedulerOptions, 'schema'>,
17027
17087
  ): Promise<PostgresSchedulerOutboxProjectionResult> {
17028
17088
  const leaseOwner =
@@ -17035,6 +17095,26 @@ export async function dispatchRunLedgerOutboxToLedgerBatchOnceWithClientScope(
17035
17095
  await client.query('BEGIN');
17036
17096
  try {
17037
17097
  const targetRunId = input.runId?.trim() || null;
17098
+ if (input.waitForRunLock && targetRunId) {
17099
+ // A synchronous drain must not mistake a busy run for an empty
17100
+ // outbox. In particular, cancellation owns its pending prefix lease,
17101
+ // so returning early also prevents background recovery from helping.
17102
+ // Wait only for this run, under the existing cancellation SQL budget,
17103
+ // then select the prefix using a fresh READ COMMITTED snapshot. The
17104
+ // transaction still ends before HTTP; background claims keep SKIP LOCKED.
17105
+ await client.query(
17106
+ `SELECT set_config('lock_timeout', $1, true),
17107
+ set_config('statement_timeout', $2, true)`,
17108
+ [
17109
+ `${RUNTIME_SCHEDULER_CANCEL_LOCK_TIMEOUT_MS}ms`,
17110
+ `${RUNTIME_SCHEDULER_CANCEL_STATEMENT_TIMEOUT_MS}ms`,
17111
+ ],
17112
+ );
17113
+ await client.query(
17114
+ `SELECT run_id FROM ${tableName('runs', options)} WHERE run_id = $1 FOR UPDATE`,
17115
+ [targetRunId],
17116
+ );
17117
+ }
17038
17118
  // A worker fast path always names one run. Keep that lookup on the
17039
17119
  // run-scoped outbox prefix: combining it with global recovery through a
17040
17120
  // nullable predicate made PostgreSQL materialize every pending run head
package/dist/cli/index.js CHANGED
@@ -3051,7 +3051,7 @@ var SDK_RELEASE = {
3051
3051
  // getters keep their established compatibility behavior.
3052
3052
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3053
3053
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3054
- version: "0.3.197",
3054
+ version: "0.3.199",
3055
3055
  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.",
3056
3056
  packageCapabilities: {
3057
3057
  updatePreferences: 1
@@ -4699,10 +4699,30 @@ var DOCFLOW_NODE_IO_LIMITS = {
4699
4699
  };
4700
4700
  var utf8Encoder = new TextEncoder();
4701
4701
  var PLAY_DATASET_BRAND = Symbol.for("deepline.play.dataset");
4702
+ var MAX_CONVEX_PREVIEW_KEY_LENGTH = 1024;
4702
4703
  function ledgerSafeDocflowPreviewKey(key) {
4703
- if (!key.startsWith("$")) return key;
4704
- const stripped = key.replace(/^\$+/, "");
4705
- return stripped || "output";
4704
+ const source = key.startsWith("$") ? key.replace(/^\$+/, "") || "output" : key || "output";
4705
+ let encoded = "";
4706
+ for (const character of source) {
4707
+ const code = character.charCodeAt(0);
4708
+ if (code >= 32 && code < 127) {
4709
+ encoded += character;
4710
+ continue;
4711
+ }
4712
+ for (const byte of utf8Encoder.encode(character)) {
4713
+ encoded += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
4714
+ }
4715
+ }
4716
+ if (encoded.length <= MAX_CONVEX_PREVIEW_KEY_LENGTH) return encoded;
4717
+ let hashA = 2166136261;
4718
+ let hashB = 2654435769;
4719
+ for (let index = 0; index < encoded.length; index += 1) {
4720
+ const code = encoded.charCodeAt(index);
4721
+ hashA = Math.imul(hashA ^ code, 16777619);
4722
+ hashB = Math.imul(hashB ^ code, 16777619);
4723
+ }
4724
+ const suffix = `~${(hashA >>> 0).toString(16).padStart(8, "0")}${(hashB >>> 0).toString(16).padStart(8, "0")}`;
4725
+ return `${encoded.slice(0, MAX_CONVEX_PREVIEW_KEY_LENGTH - suffix.length)}${suffix}`;
4706
4726
  }
4707
4727
  function utf8Bytes(value) {
4708
4728
  return utf8Encoder.encode(value).byteLength;
@@ -3046,7 +3046,7 @@ var SDK_RELEASE = {
3046
3046
  // getters keep their established compatibility behavior.
3047
3047
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
3048
3048
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
3049
- version: "0.3.197",
3049
+ version: "0.3.199",
3050
3050
  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.",
3051
3051
  packageCapabilities: {
3052
3052
  updatePreferences: 1
@@ -4694,10 +4694,30 @@ var DOCFLOW_NODE_IO_LIMITS = {
4694
4694
  };
4695
4695
  var utf8Encoder = new TextEncoder();
4696
4696
  var PLAY_DATASET_BRAND = Symbol.for("deepline.play.dataset");
4697
+ var MAX_CONVEX_PREVIEW_KEY_LENGTH = 1024;
4697
4698
  function ledgerSafeDocflowPreviewKey(key) {
4698
- if (!key.startsWith("$")) return key;
4699
- const stripped = key.replace(/^\$+/, "");
4700
- return stripped || "output";
4699
+ const source = key.startsWith("$") ? key.replace(/^\$+/, "") || "output" : key || "output";
4700
+ let encoded = "";
4701
+ for (const character of source) {
4702
+ const code = character.charCodeAt(0);
4703
+ if (code >= 32 && code < 127) {
4704
+ encoded += character;
4705
+ continue;
4706
+ }
4707
+ for (const byte of utf8Encoder.encode(character)) {
4708
+ encoded += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
4709
+ }
4710
+ }
4711
+ if (encoded.length <= MAX_CONVEX_PREVIEW_KEY_LENGTH) return encoded;
4712
+ let hashA = 2166136261;
4713
+ let hashB = 2654435769;
4714
+ for (let index = 0; index < encoded.length; index += 1) {
4715
+ const code = encoded.charCodeAt(index);
4716
+ hashA = Math.imul(hashA ^ code, 16777619);
4717
+ hashB = Math.imul(hashB ^ code, 16777619);
4718
+ }
4719
+ const suffix = `~${(hashA >>> 0).toString(16).padStart(8, "0")}${(hashB >>> 0).toString(16).padStart(8, "0")}`;
4720
+ return `${encoded.slice(0, MAX_CONVEX_PREVIEW_KEY_LENGTH - suffix.length)}${suffix}`;
4701
4721
  }
4702
4722
  function utf8Bytes(value) {
4703
4723
  return utf8Encoder.encode(value).byteLength;
package/dist/index.js CHANGED
@@ -864,7 +864,7 @@ var SDK_RELEASE = {
864
864
  // getters keep their established compatibility behavior.
865
865
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
866
866
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
867
- version: "0.3.197",
867
+ version: "0.3.199",
868
868
  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.",
869
869
  packageCapabilities: {
870
870
  updatePreferences: 1
@@ -2460,10 +2460,30 @@ var DOCFLOW_NODE_IO_LIMITS = {
2460
2460
  };
2461
2461
  var utf8Encoder = new TextEncoder();
2462
2462
  var PLAY_DATASET_BRAND = Symbol.for("deepline.play.dataset");
2463
+ var MAX_CONVEX_PREVIEW_KEY_LENGTH = 1024;
2463
2464
  function ledgerSafeDocflowPreviewKey(key) {
2464
- if (!key.startsWith("$")) return key;
2465
- const stripped = key.replace(/^\$+/, "");
2466
- return stripped || "output";
2465
+ const source = key.startsWith("$") ? key.replace(/^\$+/, "") || "output" : key || "output";
2466
+ let encoded = "";
2467
+ for (const character of source) {
2468
+ const code = character.charCodeAt(0);
2469
+ if (code >= 32 && code < 127) {
2470
+ encoded += character;
2471
+ continue;
2472
+ }
2473
+ for (const byte of utf8Encoder.encode(character)) {
2474
+ encoded += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
2475
+ }
2476
+ }
2477
+ if (encoded.length <= MAX_CONVEX_PREVIEW_KEY_LENGTH) return encoded;
2478
+ let hashA = 2166136261;
2479
+ let hashB = 2654435769;
2480
+ for (let index = 0; index < encoded.length; index += 1) {
2481
+ const code = encoded.charCodeAt(index);
2482
+ hashA = Math.imul(hashA ^ code, 16777619);
2483
+ hashB = Math.imul(hashB ^ code, 16777619);
2484
+ }
2485
+ const suffix = `~${(hashA >>> 0).toString(16).padStart(8, "0")}${(hashB >>> 0).toString(16).padStart(8, "0")}`;
2486
+ return `${encoded.slice(0, MAX_CONVEX_PREVIEW_KEY_LENGTH - suffix.length)}${suffix}`;
2467
2487
  }
2468
2488
  function utf8Bytes(value) {
2469
2489
  return utf8Encoder.encode(value).byteLength;
package/dist/index.mjs CHANGED
@@ -768,7 +768,7 @@ var SDK_RELEASE = {
768
768
  // getters keep their established compatibility behavior.
769
769
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
770
770
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
771
- version: "0.3.197",
771
+ version: "0.3.199",
772
772
  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.",
773
773
  packageCapabilities: {
774
774
  updatePreferences: 1
@@ -2364,10 +2364,30 @@ var DOCFLOW_NODE_IO_LIMITS = {
2364
2364
  };
2365
2365
  var utf8Encoder = new TextEncoder();
2366
2366
  var PLAY_DATASET_BRAND = Symbol.for("deepline.play.dataset");
2367
+ var MAX_CONVEX_PREVIEW_KEY_LENGTH = 1024;
2367
2368
  function ledgerSafeDocflowPreviewKey(key) {
2368
- if (!key.startsWith("$")) return key;
2369
- const stripped = key.replace(/^\$+/, "");
2370
- return stripped || "output";
2369
+ const source = key.startsWith("$") ? key.replace(/^\$+/, "") || "output" : key || "output";
2370
+ let encoded = "";
2371
+ for (const character of source) {
2372
+ const code = character.charCodeAt(0);
2373
+ if (code >= 32 && code < 127) {
2374
+ encoded += character;
2375
+ continue;
2376
+ }
2377
+ for (const byte of utf8Encoder.encode(character)) {
2378
+ encoded += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
2379
+ }
2380
+ }
2381
+ if (encoded.length <= MAX_CONVEX_PREVIEW_KEY_LENGTH) return encoded;
2382
+ let hashA = 2166136261;
2383
+ let hashB = 2654435769;
2384
+ for (let index = 0; index < encoded.length; index += 1) {
2385
+ const code = encoded.charCodeAt(index);
2386
+ hashA = Math.imul(hashA ^ code, 16777619);
2387
+ hashB = Math.imul(hashB ^ code, 16777619);
2388
+ }
2389
+ const suffix = `~${(hashA >>> 0).toString(16).padStart(8, "0")}${(hashB >>> 0).toString(16).padStart(8, "0")}`;
2390
+ return `${encoded.slice(0, MAX_CONVEX_PREVIEW_KEY_LENGTH - suffix.length)}${suffix}`;
2371
2391
  }
2372
2392
  function utf8Bytes(value) {
2373
2393
  return utf8Encoder.encode(value).byteLength;
@@ -149,7 +149,7 @@ type SdkRelease = {
149
149
  supportPolicy: SdkSupportPolicy;
150
150
  };
151
151
  declare const SDK_RELEASE: {
152
- readonly version: "0.3.197";
152
+ readonly version: "0.3.199";
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.197";
152
+ readonly version: "0.3.199";
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.197",
77
+ version: "0.3.199",
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.197",
51
+ version: "0.3.199",
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.197",
3
+ "version": "0.3.199",
4
4
  "description": "GTM data CLI and TypeScript SDK for coding agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://code.deepline.com",