@sentry/junior 0.70.0 → 0.71.1

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.
package/dist/app.js CHANGED
@@ -93,8 +93,8 @@ import {
93
93
  pluginCatalogConfigFromPluginSet,
94
94
  pluginHookRegistrationsFromPluginSet,
95
95
  resolveConversationWorkQueueTopic,
96
- verifySignedConversationQueueMessage
97
- } from "./chunk-IGLNC5H6.js";
96
+ verifyConversationQueueMessage
97
+ } from "./chunk-XE2VFQQN.js";
98
98
  import {
99
99
  SlackActionError,
100
100
  createSlackDestination,
@@ -13951,6 +13951,7 @@ var CONVERSATION_WORK_MUTATION_RETRY_MS = 25;
13951
13951
  var CONVERSATION_WORK_LEASE_TTL_MS = 9e4;
13952
13952
  var CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15e3;
13953
13953
  var CONVERSATION_WORK_STALE_ENQUEUE_MS = 6e4;
13954
+ var CONVERSATION_WORK_MAX_CONSECUTIVE_FAILURES = 5;
13954
13955
  function duplicateInboundNudgeIdempotencyKey(message, nowMs) {
13955
13956
  return `duplicate:${message.conversationId}:${message.inboundMessageId}:${nowMs}`;
13956
13957
  }
@@ -14072,14 +14073,18 @@ function normalizeWorkState(conversationId, value) {
14072
14073
  messages,
14073
14074
  needsRun: value.needsRun === true,
14074
14075
  updatedAtMs,
14076
+ consecutiveFailureCount: toOptionalNumber(value.consecutiveFailureCount) ?? 0,
14075
14077
  lastEnqueuedAtMs: toOptionalNumber(value.lastEnqueuedAtMs),
14076
- lease: normalizeLease(value.lease)
14078
+ lastFailureAtMs: toOptionalNumber(value.lastFailureAtMs),
14079
+ lease: normalizeLease(value.lease),
14080
+ terminallyFailedAtMs: toOptionalNumber(value.terminallyFailedAtMs)
14077
14081
  };
14078
14082
  }
14079
14083
  function emptyWorkState(args) {
14080
14084
  return {
14081
14085
  schemaVersion: CONVERSATION_WORK_SCHEMA_VERSION,
14082
14086
  conversationId: args.conversationId,
14087
+ consecutiveFailureCount: 0,
14083
14088
  destination: args.destination,
14084
14089
  messages: [],
14085
14090
  needsRun: false,
@@ -14093,6 +14098,9 @@ function pendingMessages(state) {
14093
14098
  return state.messages.filter((message) => message.injectedAtMs === void 0).sort(compareMessages);
14094
14099
  }
14095
14100
  function shouldKeepIndexed(state) {
14101
+ if (state.terminallyFailedAtMs !== void 0) {
14102
+ return false;
14103
+ }
14096
14104
  return state.needsRun || Boolean(state.lease) || pendingMessages(state).length > 0;
14097
14105
  }
14098
14106
  async function getConnectedState(stateAdapter) {
@@ -14217,6 +14225,9 @@ async function writeWorkState(state, work) {
14217
14225
  }
14218
14226
  }
14219
14227
  function hasRunnableWork(state) {
14228
+ if (state.terminallyFailedAtMs !== void 0) {
14229
+ return false;
14230
+ }
14220
14231
  return state.needsRun || pendingMessages(state).length > 0;
14221
14232
  }
14222
14233
  function assertSameConversationDestination(args) {
@@ -14266,8 +14277,11 @@ async function appendInboundMessage(args) {
14266
14277
  }
14267
14278
  const next = {
14268
14279
  ...current,
14280
+ consecutiveFailureCount: 0,
14281
+ lastFailureAtMs: void 0,
14269
14282
  messages: [...current.messages, args.message].sort(compareMessages),
14270
14283
  needsRun: true,
14284
+ terminallyFailedAtMs: void 0,
14271
14285
  updatedAtMs: nowMs
14272
14286
  };
14273
14287
  await writeWorkState(state, next);
@@ -14435,6 +14449,8 @@ async function drainConversationMailbox(args) {
14435
14449
  );
14436
14450
  await writeWorkState(state, {
14437
14451
  ...current,
14452
+ consecutiveFailureCount: 0,
14453
+ lastFailureAtMs: void 0,
14438
14454
  messages,
14439
14455
  needsRun: hasPending,
14440
14456
  updatedAtMs: nowMs
@@ -14466,6 +14482,8 @@ async function markConversationMessagesInjected(args) {
14466
14482
  }
14467
14483
  await writeWorkState(state, {
14468
14484
  ...current,
14485
+ consecutiveFailureCount: 0,
14486
+ lastFailureAtMs: void 0,
14469
14487
  messages,
14470
14488
  updatedAtMs: nowMs
14471
14489
  });
@@ -14518,6 +14536,8 @@ async function completeConversationWork(args) {
14518
14536
  const hasRunnableWork2 = current.needsRun || hasPending;
14519
14537
  await writeWorkState(state, {
14520
14538
  ...current,
14539
+ consecutiveFailureCount: 0,
14540
+ lastFailureAtMs: void 0,
14521
14541
  lease: void 0,
14522
14542
  needsRun: hasRunnableWork2,
14523
14543
  updatedAtMs: nowMs
@@ -14541,6 +14561,53 @@ async function clearExpiredConversationLease(args) {
14541
14561
  return true;
14542
14562
  });
14543
14563
  }
14564
+ async function recordConversationWorkFailure(args) {
14565
+ const nowMs = args.nowMs ?? now();
14566
+ return await withConversationMutation(args, async (state) => {
14567
+ const current = await readWorkState(state, args.conversationId);
14568
+ if (!current) {
14569
+ return {
14570
+ abandoned: false,
14571
+ consecutiveFailureCount: 0,
14572
+ releasedLease: false
14573
+ };
14574
+ }
14575
+ const consecutiveFailureCount = current.consecutiveFailureCount + 1;
14576
+ const abandoned = consecutiveFailureCount >= CONVERSATION_WORK_MAX_CONSECUTIVE_FAILURES;
14577
+ if (!abandoned) {
14578
+ await writeWorkState(state, {
14579
+ ...current,
14580
+ consecutiveFailureCount,
14581
+ lastFailureAtMs: nowMs,
14582
+ updatedAtMs: nowMs
14583
+ });
14584
+ return {
14585
+ abandoned: false,
14586
+ consecutiveFailureCount,
14587
+ releasedLease: false
14588
+ };
14589
+ }
14590
+ const releasedLease = Boolean(current.lease);
14591
+ const drainedMessages = current.messages.filter(
14592
+ (message) => message.injectedAtMs !== void 0
14593
+ );
14594
+ await writeWorkState(state, {
14595
+ ...current,
14596
+ consecutiveFailureCount,
14597
+ lastFailureAtMs: nowMs,
14598
+ lease: void 0,
14599
+ messages: drainedMessages,
14600
+ needsRun: false,
14601
+ terminallyFailedAtMs: nowMs,
14602
+ updatedAtMs: nowMs
14603
+ });
14604
+ return {
14605
+ abandoned: true,
14606
+ consecutiveFailureCount,
14607
+ releasedLease
14608
+ };
14609
+ });
14610
+ }
14544
14611
  async function listConversationWorkIds(args = {}) {
14545
14612
  const state = await getConnectedState(args.state);
14546
14613
  const ids = uniqueStrings(await state.get(indexKey()) ?? []);
@@ -23192,6 +23259,21 @@ import {
23192
23259
  registerDevConsumer
23193
23260
  } from "@vercel/queue";
23194
23261
 
23262
+ // src/chat/task-execution/queue.ts
23263
+ var ConversationQueueMessageRejectedError = class extends Error {
23264
+ conversationId;
23265
+ reason;
23266
+ constructor(reason, message, options = {}) {
23267
+ super(message);
23268
+ this.name = "ConversationQueueMessageRejectedError";
23269
+ this.reason = reason;
23270
+ this.conversationId = options.conversationId;
23271
+ }
23272
+ };
23273
+ function isConversationQueueMessageRejectedError(error) {
23274
+ return error instanceof ConversationQueueMessageRejectedError;
23275
+ }
23276
+
23195
23277
  // src/chat/task-execution/worker.ts
23196
23278
  var CONVERSATION_WORK_DEFER_DELAY_MS = 15e3;
23197
23279
  var CONVERSATION_WORK_SOFT_YIELD_AFTER_MS = 24e4;
@@ -23212,11 +23294,21 @@ async function sendWakeNudge(args) {
23212
23294
  idempotencyKey: args.idempotencyKey
23213
23295
  }
23214
23296
  );
23215
- await markConversationWorkEnqueued({
23216
- conversationId: args.conversationId,
23217
- nowMs: args.nowMs,
23218
- state: args.options.state
23219
- });
23297
+ try {
23298
+ await markConversationWorkEnqueued({
23299
+ conversationId: args.conversationId,
23300
+ nowMs: args.nowMs,
23301
+ state: args.options.state
23302
+ });
23303
+ } catch (error) {
23304
+ logException(
23305
+ error,
23306
+ "conversation_work_enqueue_marker_failed",
23307
+ { conversationId: args.conversationId },
23308
+ {},
23309
+ "Conversation work enqueue marker failed after queue acceptance"
23310
+ );
23311
+ }
23220
23312
  }
23221
23313
  async function requestLostLeaseRecovery(args) {
23222
23314
  const continuationMarked = await requestConversationContinuation({
@@ -23294,29 +23386,57 @@ async function processConversationWork(message, options) {
23294
23386
  return { status: "no_work" };
23295
23387
  }
23296
23388
  if (!sameDestination(initial.destination, message.destination)) {
23297
- throw new Error(
23298
- `Conversation work queue destination changed for ${conversationId}`
23389
+ throw new ConversationQueueMessageRejectedError(
23390
+ "destination_mismatch",
23391
+ `Conversation work queue destination changed for ${conversationId}`,
23392
+ { conversationId }
23299
23393
  );
23300
23394
  }
23301
23395
  const destination = initial.destination;
23302
- const lease = await startConversationWork({
23303
- conversationId,
23304
- nowMs: now2(options),
23305
- state: options.state
23306
- });
23396
+ let lease;
23397
+ try {
23398
+ lease = await startConversationWork({
23399
+ conversationId,
23400
+ nowMs: now2(options),
23401
+ state: options.state
23402
+ });
23403
+ } catch (error) {
23404
+ logException(
23405
+ error,
23406
+ "conversation_work_lease_acquire_failed",
23407
+ { conversationId },
23408
+ {},
23409
+ "Conversation work lease acquisition failed; heartbeat will recover"
23410
+ );
23411
+ return { status: "no_work" };
23412
+ }
23307
23413
  if (lease.status === "no_work") {
23308
23414
  return { status: "no_work" };
23309
23415
  }
23310
23416
  if (lease.status === "active") {
23311
23417
  const nudgeNowMs = now2(options);
23312
- await sendWakeNudge({
23313
- conversationId,
23314
- destination,
23315
- delayMs: CONVERSATION_WORK_DEFER_DELAY_MS,
23316
- idempotencyKey: nudgeIdempotencyKey("active", conversationId, nudgeNowMs),
23317
- nowMs: nudgeNowMs,
23318
- options
23319
- });
23418
+ try {
23419
+ await sendWakeNudge({
23420
+ conversationId,
23421
+ destination,
23422
+ delayMs: CONVERSATION_WORK_DEFER_DELAY_MS,
23423
+ idempotencyKey: nudgeIdempotencyKey(
23424
+ "active",
23425
+ conversationId,
23426
+ nudgeNowMs
23427
+ ),
23428
+ nowMs: nudgeNowMs,
23429
+ options
23430
+ });
23431
+ } catch (error) {
23432
+ logException(
23433
+ error,
23434
+ "conversation_work_active_nudge_failed",
23435
+ { conversationId },
23436
+ {},
23437
+ "Conversation work active-lease nudge failed; heartbeat will recover"
23438
+ );
23439
+ }
23320
23440
  logInfo(
23321
23441
  "conversation_work_nudge_deferred_for_active_lease",
23322
23442
  { conversationId },
@@ -23470,35 +23590,97 @@ async function processConversationWork(message, options) {
23470
23590
  return { status: "completed" };
23471
23591
  } catch (error) {
23472
23592
  const errorNowMs = now2(options);
23593
+ let failure2;
23473
23594
  try {
23474
- const continuationMarked = await requestConversationContinuation({
23595
+ failure2 = await recordConversationWorkFailure({
23475
23596
  conversationId,
23476
- destination,
23477
- leaseToken: lease.leaseToken,
23478
23597
  nowMs: errorNowMs,
23479
23598
  state: options.state
23480
23599
  });
23481
- if (continuationMarked) {
23482
- await sendWakeNudge({
23600
+ } catch (recordError) {
23601
+ logException(
23602
+ recordError,
23603
+ "conversation_work_failure_record_failed",
23604
+ { conversationId },
23605
+ {},
23606
+ "Conversation work failure counter update failed"
23607
+ );
23608
+ }
23609
+ if (!isProviderRetryError(error)) {
23610
+ logException(
23611
+ error,
23612
+ "conversation_work_failed",
23613
+ { conversationId },
23614
+ {
23615
+ "app.worker.consecutive_failure_count": failure2?.consecutiveFailureCount ?? null,
23616
+ "app.worker.elapsed_ms": now2(options) - startedAtMs
23617
+ },
23618
+ "Conversation work failed"
23619
+ );
23620
+ }
23621
+ if (failure2?.abandoned) {
23622
+ logWarn(
23623
+ "conversation_work_abandoned",
23624
+ { conversationId },
23625
+ {
23626
+ "app.worker.consecutive_failure_count": failure2.consecutiveFailureCount,
23627
+ "app.worker.max_consecutive_failures": CONVERSATION_WORK_MAX_CONSECUTIVE_FAILURES
23628
+ },
23629
+ "Conversation work abandoned after repeated failures; stopping retries"
23630
+ );
23631
+ if (!failure2.releasedLease) {
23632
+ try {
23633
+ await releaseConversationWork({
23634
+ conversationId,
23635
+ leaseToken: lease.leaseToken,
23636
+ nowMs: errorNowMs,
23637
+ state: options.state
23638
+ });
23639
+ } catch (releaseError) {
23640
+ logException(
23641
+ releaseError,
23642
+ "conversation_work_release_failed",
23643
+ { conversationId },
23644
+ {},
23645
+ "Conversation work release failed after abandoning"
23646
+ );
23647
+ }
23648
+ }
23649
+ return { status: "abandoned" };
23650
+ }
23651
+ let requeueSucceeded = false;
23652
+ if (failure2) {
23653
+ try {
23654
+ const continuationMarked = await requestConversationContinuation({
23483
23655
  conversationId,
23484
23656
  destination,
23485
- idempotencyKey: nudgeIdempotencyKey(
23486
- "error",
23487
- conversationId,
23488
- errorNowMs
23489
- ),
23657
+ leaseToken: lease.leaseToken,
23490
23658
  nowMs: errorNowMs,
23491
- options
23659
+ state: options.state
23492
23660
  });
23661
+ if (continuationMarked) {
23662
+ await sendWakeNudge({
23663
+ conversationId,
23664
+ destination,
23665
+ idempotencyKey: nudgeIdempotencyKey(
23666
+ "error",
23667
+ conversationId,
23668
+ errorNowMs
23669
+ ),
23670
+ nowMs: errorNowMs,
23671
+ options
23672
+ });
23673
+ requeueSucceeded = true;
23674
+ }
23675
+ } catch (requeueError) {
23676
+ logException(
23677
+ requeueError,
23678
+ "conversation_work_requeue_failed",
23679
+ { conversationId },
23680
+ {},
23681
+ "Conversation work requeue failed after runner error"
23682
+ );
23493
23683
  }
23494
- } catch (requeueError) {
23495
- logException(
23496
- requeueError,
23497
- "conversation_work_requeue_failed",
23498
- { conversationId },
23499
- {},
23500
- "Conversation work requeue failed after runner error"
23501
- );
23502
23684
  }
23503
23685
  try {
23504
23686
  await releaseConversationWork({
@@ -23516,16 +23698,8 @@ async function processConversationWork(message, options) {
23516
23698
  "Conversation work release failed after runner error"
23517
23699
  );
23518
23700
  }
23519
- if (!isProviderRetryError(error)) {
23520
- logException(
23521
- error,
23522
- "conversation_work_failed",
23523
- { conversationId },
23524
- {
23525
- "app.worker.elapsed_ms": now2(options) - startedAtMs
23526
- },
23527
- "Conversation work failed"
23528
- );
23701
+ if (requeueSucceeded) {
23702
+ return { status: "pending_requeued" };
23529
23703
  }
23530
23704
  throw error;
23531
23705
  } finally {
@@ -23565,18 +23739,45 @@ async function processConversationQueueMessage(message, options) {
23565
23739
  });
23566
23740
  }
23567
23741
  async function handleConversationQueueMessage(message, options) {
23568
- const verified = verifySignedConversationQueueMessage(message);
23569
- if (!verified) {
23570
- throw new Error("Unauthorized conversation queue message");
23742
+ const verification = verifyConversationQueueMessage(message);
23743
+ if (verification.status === "rejected") {
23744
+ throw new ConversationQueueMessageRejectedError(
23745
+ verification.reason,
23746
+ "Unauthorized conversation queue message"
23747
+ );
23748
+ }
23749
+ if (verification.status === "unavailable") {
23750
+ throw new Error(
23751
+ `Conversation queue message verification unavailable: ${verification.reason}`
23752
+ );
23571
23753
  }
23572
23754
  await runWithTurnRequestDeadline(
23573
- () => processConversationQueueMessage(verified, options)
23755
+ () => processConversationQueueMessage(verification.message, options)
23756
+ );
23757
+ }
23758
+ function handleConversationQueueRetry(error, metadata) {
23759
+ if (!isConversationQueueMessageRejectedError(error)) {
23760
+ return void 0;
23761
+ }
23762
+ logWarn(
23763
+ "conversation_queue_message_rejected",
23764
+ error.conversationId ? { conversationId: error.conversationId } : {},
23765
+ {
23766
+ "app.queue.consumer_group": metadata.consumerGroup,
23767
+ "app.queue.delivery_count": metadata.deliveryCount,
23768
+ "app.queue.message_id": metadata.messageId,
23769
+ "app.queue.reject_reason": error.reason,
23770
+ "app.queue.topic_name": metadata.topicName
23771
+ },
23772
+ "Conversation queue message rejected without retry"
23574
23773
  );
23774
+ return { acknowledge: true };
23575
23775
  }
23576
23776
  function createVercelConversationWorkCallback(options) {
23577
23777
  return handleCallback(
23578
23778
  (message) => handleConversationQueueMessage(message, options),
23579
23779
  {
23780
+ retry: handleConversationQueueRetry,
23580
23781
  visibilityTimeoutSeconds: options.visibilityTimeoutSeconds ?? resolveConversationWorkVisibilityTimeoutSeconds()
23581
23782
  }
23582
23783
  );
@@ -23589,6 +23790,7 @@ function registerVercelConversationWorkDevConsumer(options) {
23589
23790
  client: new QueueClient(),
23590
23791
  consumerGroup: CONVERSATION_WORK_DEV_CONSUMER_GROUP,
23591
23792
  handler: (message) => handleConversationQueueMessage(message, options),
23793
+ retry: handleConversationQueueRetry,
23592
23794
  topic: resolveConversationWorkQueueTopic(options),
23593
23795
  visibilityTimeoutSeconds: options.visibilityTimeoutSeconds ?? resolveConversationWorkVisibilityTimeoutSeconds()
23594
23796
  });
@@ -1,12 +1,25 @@
1
1
  import type { ConversationQueueMessage } from "./queue";
2
2
  declare const CONVERSATION_WORK_QUEUE_SIGNATURE_VERSION = "v1";
3
+ export declare const CONVERSATION_WORK_QUEUE_SIGNATURE_MAX_SKEW_MS: number;
3
4
  interface SignedConversationQueueMessage extends ConversationQueueMessage {
4
5
  signature: string;
5
6
  signatureVersion: typeof CONVERSATION_WORK_QUEUE_SIGNATURE_VERSION;
6
7
  signedAtMs: number;
7
8
  }
9
+ export type ConversationQueueMessageVerificationResult = {
10
+ message: ConversationQueueMessage;
11
+ status: "verified";
12
+ } | {
13
+ reason: "expired" | "malformed" | "signature_mismatch";
14
+ status: "rejected";
15
+ } | {
16
+ reason: "invalid_clock" | "missing_secret";
17
+ status: "unavailable";
18
+ };
8
19
  /** Sign a conversation queue payload before it crosses the public callback route. */
9
20
  export declare function signConversationQueueMessage(message: ConversationQueueMessage, nowMs?: number): SignedConversationQueueMessage;
21
+ /** Explain whether a queue payload is verified, rejected, or temporarily unverifiable. */
22
+ export declare function verifyConversationQueueMessage(value: unknown, nowMs?: number): ConversationQueueMessageVerificationResult;
10
23
  /** Verify a signed conversation queue payload from the Vercel Queue callback. */
11
24
  export declare function verifySignedConversationQueueMessage(value: unknown, nowMs?: number): ConversationQueueMessage | undefined;
12
25
  export {};
@@ -3,6 +3,16 @@ export interface ConversationQueueMessage {
3
3
  conversationId: string;
4
4
  destination: Destination;
5
5
  }
6
+ export type ConversationQueueMessageRejectReason = "destination_mismatch" | "expired" | "malformed" | "signature_mismatch" | "unauthorized";
7
+ export declare class ConversationQueueMessageRejectedError extends Error {
8
+ conversationId?: string;
9
+ reason: ConversationQueueMessageRejectReason;
10
+ constructor(reason: ConversationQueueMessageRejectReason, message: string, options?: {
11
+ conversationId?: string;
12
+ });
13
+ }
14
+ /** Return whether a queue payload was permanently rejected at the message boundary. */
15
+ export declare function isConversationQueueMessageRejectedError(error: unknown): error is ConversationQueueMessageRejectedError;
6
16
  export interface ConversationQueueSendOptions {
7
17
  delayMs?: number;
8
18
  idempotencyKey?: string;
@@ -4,6 +4,7 @@ import type { ConversationWorkQueue } from "./queue";
4
4
  export declare const CONVERSATION_WORK_LEASE_TTL_MS = 90000;
5
5
  export declare const CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15000;
6
6
  export declare const CONVERSATION_WORK_STALE_ENQUEUE_MS = 60000;
7
+ export declare const CONVERSATION_WORK_MAX_CONSECUTIVE_FAILURES = 5;
7
8
  export type InboundMessageSource = "plugin" | "scheduler" | "slack";
8
9
  export interface AgentInputMessage {
9
10
  attachments?: unknown[];
@@ -28,13 +29,16 @@ export interface ConversationLease {
28
29
  leaseToken: string;
29
30
  }
30
31
  export interface ConversationWorkState {
32
+ consecutiveFailureCount: number;
31
33
  conversationId: string;
32
34
  destination: Destination;
33
35
  lastEnqueuedAtMs?: number;
36
+ lastFailureAtMs?: number;
34
37
  lease?: ConversationLease;
35
38
  messages: InboundMessageRecord[];
36
39
  needsRun: boolean;
37
40
  schemaVersion: 1;
41
+ terminallyFailedAtMs?: number;
38
42
  updatedAtMs: number;
39
43
  }
40
44
  export interface ConversationLeaseAcquired {
@@ -151,6 +155,25 @@ export declare function clearExpiredConversationLease(args: {
151
155
  nowMs?: number;
152
156
  state?: StateAdapter;
153
157
  }): Promise<boolean>;
158
+ export interface RecordConversationWorkFailureResult {
159
+ abandoned: boolean;
160
+ consecutiveFailureCount: number;
161
+ releasedLease: boolean;
162
+ }
163
+ /**
164
+ * Increment the durable failure counter after a caught worker error so
165
+ * deterministic poison work cannot churn the queue forever. When the counter
166
+ * crosses {@link CONVERSATION_WORK_MAX_CONSECUTIVE_FAILURES}, the conversation
167
+ * is marked terminally failed: the lease is cleared, pending mailbox messages
168
+ * are dropped, and the conversation drops out of the recovery index so neither
169
+ * the worker nor heartbeat will requeue it again. A later inbound message
170
+ * resets the counter and gives the conversation a fresh attempt.
171
+ */
172
+ export declare function recordConversationWorkFailure(args: {
173
+ conversationId: string;
174
+ nowMs?: number;
175
+ state?: StateAdapter;
176
+ }): Promise<RecordConversationWorkFailureResult>;
154
177
  /** List bounded conversation ids that may need heartbeat recovery. */
155
178
  export declare function listConversationWorkIds(args?: {
156
179
  limit?: number;
@@ -1,5 +1,5 @@
1
1
  import type { StateAdapter } from "chat";
2
- import type { ConversationWorkQueue } from "./queue";
2
+ import { type ConversationWorkQueue } from "./queue";
3
3
  import { type ConversationWorkProcessResult, type ConversationWorkerResult, type ConversationWorkerContext } from "./worker";
4
4
  export declare const CONVERSATION_WORK_VISIBILITY_TIMEOUT_BUFFER_SECONDS = 30;
5
5
  export declare const CONVERSATION_WORK_DEV_CONSUMER_GROUP = "junior_conversation_work_dev";
@@ -1,6 +1,7 @@
1
1
  import type { SendOptions, SendResult } from "@vercel/queue";
2
2
  import type { ConversationWorkQueue } from "./queue";
3
3
  export declare const DEFAULT_CONVERSATION_WORK_QUEUE_TOPIC = "junior_conversation_work";
4
+ export declare const CONVERSATION_WORK_QUEUE_RETENTION_SECONDS: number;
4
5
  interface QueueSender {
5
6
  send<T = unknown>(topicName: string, payload: T, options?: SendOptions): Promise<SendResult>;
6
7
  }
@@ -1,6 +1,6 @@
1
1
  import type { StateAdapter } from "chat";
2
2
  import type { Destination } from "@sentry/junior-plugin-api";
3
- import type { ConversationQueueMessage, ConversationWorkQueue } from "./queue";
3
+ import { type ConversationQueueMessage, type ConversationWorkQueue } from "./queue";
4
4
  import { type InboundMessageRecord } from "./store";
5
5
  export declare const CONVERSATION_WORK_DEFER_DELAY_MS = 15000;
6
6
  export declare const CONVERSATION_WORK_SOFT_YIELD_AFTER_MS = 240000;
@@ -16,7 +16,7 @@ export interface ConversationWorkerResult {
16
16
  status: "completed" | "lost_lease" | "yielded";
17
17
  }
18
18
  export interface ConversationWorkProcessResult {
19
- status: "active" | "completed" | "lost_lease" | "no_work" | "pending_requeued" | "yielded";
19
+ status: "abandoned" | "active" | "completed" | "lost_lease" | "no_work" | "pending_requeued" | "yielded";
20
20
  }
21
21
  export interface ProcessConversationWorkOptions {
22
22
  checkInIntervalMs?: number;
@@ -163,24 +163,37 @@ function signConversationQueueMessage(message, nowMs = Date.now()) {
163
163
  signature: signPayload(message, nowMs, secret)
164
164
  };
165
165
  }
166
- function verifySignedConversationQueueMessage(value, nowMs = Date.now()) {
166
+ function verifyConversationQueueMessage(value, nowMs = Date.now()) {
167
167
  const message = parseSignedConversationQueueMessage(value);
168
+ if (!message) {
169
+ return { status: "rejected", reason: "malformed" };
170
+ }
168
171
  const secret = getConversationWorkQueueSecret();
169
- if (!message || !secret || !Number.isFinite(nowMs) || Math.abs(nowMs - message.signedAtMs) > CONVERSATION_WORK_QUEUE_SIGNATURE_MAX_SKEW_MS) {
170
- return void 0;
172
+ if (!secret) {
173
+ return { status: "unavailable", reason: "missing_secret" };
174
+ }
175
+ if (!Number.isFinite(nowMs)) {
176
+ return { status: "unavailable", reason: "invalid_clock" };
177
+ }
178
+ if (Math.abs(nowMs - message.signedAtMs) > CONVERSATION_WORK_QUEUE_SIGNATURE_MAX_SKEW_MS) {
179
+ return { status: "rejected", reason: "expired" };
171
180
  }
172
181
  const expected = signPayload(message, message.signedAtMs, secret);
173
182
  if (!timingSafeMatch(expected, message.signature)) {
174
- return void 0;
183
+ return { status: "rejected", reason: "signature_mismatch" };
175
184
  }
176
185
  return {
177
- conversationId: message.conversationId,
178
- destination: message.destination
186
+ status: "verified",
187
+ message: {
188
+ conversationId: message.conversationId,
189
+ destination: message.destination
190
+ }
179
191
  };
180
192
  }
181
193
 
182
194
  // src/chat/task-execution/vercel-queue.ts
183
195
  var DEFAULT_CONVERSATION_WORK_QUEUE_TOPIC = "junior_conversation_work";
196
+ var CONVERSATION_WORK_QUEUE_RETENTION_SECONDS = CONVERSATION_WORK_QUEUE_SIGNATURE_MAX_SKEW_MS / 1e3;
184
197
  var defaultQueue;
185
198
  function resolveConversationWorkQueueTopic(options = {}) {
186
199
  const topic = options.topic?.trim();
@@ -203,7 +216,7 @@ function createVercelConversationWorkQueue(options = {}) {
203
216
  {
204
217
  idempotencyKey: sendOptions?.idempotencyKey,
205
218
  delaySeconds: toDelaySeconds(sendOptions),
206
- retentionSeconds: options.retentionSeconds
219
+ retentionSeconds: options.retentionSeconds ?? CONVERSATION_WORK_QUEUE_RETENTION_SECONDS
207
220
  }
208
221
  );
209
222
  return result.messageId ? { messageId: result.messageId } : {};
@@ -219,7 +232,7 @@ export {
219
232
  defineJuniorPlugins,
220
233
  pluginCatalogConfigFromPluginSet,
221
234
  pluginHookRegistrationsFromPluginSet,
222
- verifySignedConversationQueueMessage,
235
+ verifyConversationQueueMessage,
223
236
  resolveConversationWorkQueueTopic,
224
237
  getVercelConversationWorkQueue
225
238
  };
package/dist/nitro.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  pluginCatalogConfigFromPluginSet,
3
3
  pluginHookRegistrationsFromPluginSet,
4
4
  resolveConversationWorkQueueTopic
5
- } from "./chunk-IGLNC5H6.js";
5
+ } from "./chunk-XE2VFQQN.js";
6
6
  import "./chunk-76YMBKW7.js";
7
7
  import {
8
8
  JUNIOR_CONVERSATION_WORK_CALLBACK_ROUTE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior",
3
- "version": "0.70.0",
3
+ "version": "0.71.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -65,7 +65,7 @@
65
65
  "node-html-markdown": "^2.0.0",
66
66
  "yaml": "^2.9.0",
67
67
  "zod": "^4.4.3",
68
- "@sentry/junior-plugin-api": "0.70.0"
68
+ "@sentry/junior-plugin-api": "0.71.1"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.9.1",
@@ -78,7 +78,7 @@
78
78
  "typescript": "^6.0.3",
79
79
  "vercel": "^54.4.0",
80
80
  "vitest": "^4.1.7",
81
- "@sentry/junior-scheduler": "0.70.0"
81
+ "@sentry/junior-scheduler": "0.71.1"
82
82
  },
83
83
  "scripts": {
84
84
  "build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",