@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 +258 -56
- package/dist/chat/task-execution/queue-signing.d.ts +13 -0
- package/dist/chat/task-execution/queue.d.ts +10 -0
- package/dist/chat/task-execution/store.d.ts +23 -0
- package/dist/chat/task-execution/vercel-callback.d.ts +1 -1
- package/dist/chat/task-execution/vercel-queue.d.ts +1 -0
- package/dist/chat/task-execution/worker.d.ts +2 -2
- package/dist/{chunk-IGLNC5H6.js → chunk-XE2VFQQN.js} +21 -8
- package/dist/nitro.js +1 -1
- package/package.json +3 -3
package/dist/app.js
CHANGED
|
@@ -93,8 +93,8 @@ import {
|
|
|
93
93
|
pluginCatalogConfigFromPluginSet,
|
|
94
94
|
pluginHookRegistrationsFromPluginSet,
|
|
95
95
|
resolveConversationWorkQueueTopic,
|
|
96
|
-
|
|
97
|
-
} from "./chunk-
|
|
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
|
-
|
|
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
|
-
|
|
23216
|
-
|
|
23217
|
-
|
|
23218
|
-
|
|
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
|
|
23298
|
-
|
|
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
|
-
|
|
23303
|
-
|
|
23304
|
-
|
|
23305
|
-
|
|
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
|
-
|
|
23313
|
-
|
|
23314
|
-
|
|
23315
|
-
|
|
23316
|
-
|
|
23317
|
-
|
|
23318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23482
|
-
|
|
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
|
-
|
|
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 (
|
|
23520
|
-
|
|
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
|
|
23569
|
-
if (
|
|
23570
|
-
throw new
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
170
|
-
return
|
|
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
|
|
183
|
+
return { status: "rejected", reason: "signature_mismatch" };
|
|
175
184
|
}
|
|
176
185
|
return {
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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-
|
|
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.
|
|
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.
|
|
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.
|
|
81
|
+
"@sentry/junior-scheduler": "0.71.1"
|
|
82
82
|
},
|
|
83
83
|
"scripts": {
|
|
84
84
|
"build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
|