@sentry/junior 0.70.0 → 0.71.0
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
|
@@ -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()) ?? []);
|
|
@@ -23299,24 +23366,50 @@ async function processConversationWork(message, options) {
|
|
|
23299
23366
|
);
|
|
23300
23367
|
}
|
|
23301
23368
|
const destination = initial.destination;
|
|
23302
|
-
|
|
23303
|
-
|
|
23304
|
-
|
|
23305
|
-
|
|
23306
|
-
|
|
23369
|
+
let lease;
|
|
23370
|
+
try {
|
|
23371
|
+
lease = await startConversationWork({
|
|
23372
|
+
conversationId,
|
|
23373
|
+
nowMs: now2(options),
|
|
23374
|
+
state: options.state
|
|
23375
|
+
});
|
|
23376
|
+
} catch (error) {
|
|
23377
|
+
logException(
|
|
23378
|
+
error,
|
|
23379
|
+
"conversation_work_lease_acquire_failed",
|
|
23380
|
+
{ conversationId },
|
|
23381
|
+
{},
|
|
23382
|
+
"Conversation work lease acquisition failed; heartbeat will recover"
|
|
23383
|
+
);
|
|
23384
|
+
return { status: "no_work" };
|
|
23385
|
+
}
|
|
23307
23386
|
if (lease.status === "no_work") {
|
|
23308
23387
|
return { status: "no_work" };
|
|
23309
23388
|
}
|
|
23310
23389
|
if (lease.status === "active") {
|
|
23311
23390
|
const nudgeNowMs = now2(options);
|
|
23312
|
-
|
|
23313
|
-
|
|
23314
|
-
|
|
23315
|
-
|
|
23316
|
-
|
|
23317
|
-
|
|
23318
|
-
|
|
23319
|
-
|
|
23391
|
+
try {
|
|
23392
|
+
await sendWakeNudge({
|
|
23393
|
+
conversationId,
|
|
23394
|
+
destination,
|
|
23395
|
+
delayMs: CONVERSATION_WORK_DEFER_DELAY_MS,
|
|
23396
|
+
idempotencyKey: nudgeIdempotencyKey(
|
|
23397
|
+
"active",
|
|
23398
|
+
conversationId,
|
|
23399
|
+
nudgeNowMs
|
|
23400
|
+
),
|
|
23401
|
+
nowMs: nudgeNowMs,
|
|
23402
|
+
options
|
|
23403
|
+
});
|
|
23404
|
+
} catch (error) {
|
|
23405
|
+
logException(
|
|
23406
|
+
error,
|
|
23407
|
+
"conversation_work_active_nudge_failed",
|
|
23408
|
+
{ conversationId },
|
|
23409
|
+
{},
|
|
23410
|
+
"Conversation work active-lease nudge failed; heartbeat will recover"
|
|
23411
|
+
);
|
|
23412
|
+
}
|
|
23320
23413
|
logInfo(
|
|
23321
23414
|
"conversation_work_nudge_deferred_for_active_lease",
|
|
23322
23415
|
{ conversationId },
|
|
@@ -23470,35 +23563,97 @@ async function processConversationWork(message, options) {
|
|
|
23470
23563
|
return { status: "completed" };
|
|
23471
23564
|
} catch (error) {
|
|
23472
23565
|
const errorNowMs = now2(options);
|
|
23566
|
+
let failure2;
|
|
23473
23567
|
try {
|
|
23474
|
-
|
|
23568
|
+
failure2 = await recordConversationWorkFailure({
|
|
23475
23569
|
conversationId,
|
|
23476
|
-
destination,
|
|
23477
|
-
leaseToken: lease.leaseToken,
|
|
23478
23570
|
nowMs: errorNowMs,
|
|
23479
23571
|
state: options.state
|
|
23480
23572
|
});
|
|
23481
|
-
|
|
23482
|
-
|
|
23573
|
+
} catch (recordError) {
|
|
23574
|
+
logException(
|
|
23575
|
+
recordError,
|
|
23576
|
+
"conversation_work_failure_record_failed",
|
|
23577
|
+
{ conversationId },
|
|
23578
|
+
{},
|
|
23579
|
+
"Conversation work failure counter update failed"
|
|
23580
|
+
);
|
|
23581
|
+
}
|
|
23582
|
+
if (!isProviderRetryError(error)) {
|
|
23583
|
+
logException(
|
|
23584
|
+
error,
|
|
23585
|
+
"conversation_work_failed",
|
|
23586
|
+
{ conversationId },
|
|
23587
|
+
{
|
|
23588
|
+
"app.worker.consecutive_failure_count": failure2?.consecutiveFailureCount ?? null,
|
|
23589
|
+
"app.worker.elapsed_ms": now2(options) - startedAtMs
|
|
23590
|
+
},
|
|
23591
|
+
"Conversation work failed"
|
|
23592
|
+
);
|
|
23593
|
+
}
|
|
23594
|
+
if (failure2?.abandoned) {
|
|
23595
|
+
logWarn(
|
|
23596
|
+
"conversation_work_abandoned",
|
|
23597
|
+
{ conversationId },
|
|
23598
|
+
{
|
|
23599
|
+
"app.worker.consecutive_failure_count": failure2.consecutiveFailureCount,
|
|
23600
|
+
"app.worker.max_consecutive_failures": CONVERSATION_WORK_MAX_CONSECUTIVE_FAILURES
|
|
23601
|
+
},
|
|
23602
|
+
"Conversation work abandoned after repeated failures; stopping retries"
|
|
23603
|
+
);
|
|
23604
|
+
if (!failure2.releasedLease) {
|
|
23605
|
+
try {
|
|
23606
|
+
await releaseConversationWork({
|
|
23607
|
+
conversationId,
|
|
23608
|
+
leaseToken: lease.leaseToken,
|
|
23609
|
+
nowMs: errorNowMs,
|
|
23610
|
+
state: options.state
|
|
23611
|
+
});
|
|
23612
|
+
} catch (releaseError) {
|
|
23613
|
+
logException(
|
|
23614
|
+
releaseError,
|
|
23615
|
+
"conversation_work_release_failed",
|
|
23616
|
+
{ conversationId },
|
|
23617
|
+
{},
|
|
23618
|
+
"Conversation work release failed after abandoning"
|
|
23619
|
+
);
|
|
23620
|
+
}
|
|
23621
|
+
}
|
|
23622
|
+
return { status: "abandoned" };
|
|
23623
|
+
}
|
|
23624
|
+
let requeueSucceeded = false;
|
|
23625
|
+
if (failure2) {
|
|
23626
|
+
try {
|
|
23627
|
+
const continuationMarked = await requestConversationContinuation({
|
|
23483
23628
|
conversationId,
|
|
23484
23629
|
destination,
|
|
23485
|
-
|
|
23486
|
-
"error",
|
|
23487
|
-
conversationId,
|
|
23488
|
-
errorNowMs
|
|
23489
|
-
),
|
|
23630
|
+
leaseToken: lease.leaseToken,
|
|
23490
23631
|
nowMs: errorNowMs,
|
|
23491
|
-
options
|
|
23632
|
+
state: options.state
|
|
23492
23633
|
});
|
|
23634
|
+
if (continuationMarked) {
|
|
23635
|
+
await sendWakeNudge({
|
|
23636
|
+
conversationId,
|
|
23637
|
+
destination,
|
|
23638
|
+
idempotencyKey: nudgeIdempotencyKey(
|
|
23639
|
+
"error",
|
|
23640
|
+
conversationId,
|
|
23641
|
+
errorNowMs
|
|
23642
|
+
),
|
|
23643
|
+
nowMs: errorNowMs,
|
|
23644
|
+
options
|
|
23645
|
+
});
|
|
23646
|
+
requeueSucceeded = true;
|
|
23647
|
+
}
|
|
23648
|
+
} catch (requeueError) {
|
|
23649
|
+
logException(
|
|
23650
|
+
requeueError,
|
|
23651
|
+
"conversation_work_requeue_failed",
|
|
23652
|
+
{ conversationId },
|
|
23653
|
+
{},
|
|
23654
|
+
"Conversation work requeue failed after runner error"
|
|
23655
|
+
);
|
|
23493
23656
|
}
|
|
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
23657
|
}
|
|
23503
23658
|
try {
|
|
23504
23659
|
await releaseConversationWork({
|
|
@@ -23516,16 +23671,8 @@ async function processConversationWork(message, options) {
|
|
|
23516
23671
|
"Conversation work release failed after runner error"
|
|
23517
23672
|
);
|
|
23518
23673
|
}
|
|
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
|
-
);
|
|
23674
|
+
if (requeueSucceeded) {
|
|
23675
|
+
return { status: "pending_requeued" };
|
|
23529
23676
|
}
|
|
23530
23677
|
throw error;
|
|
23531
23678
|
} finally {
|
|
@@ -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;
|
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.0",
|
|
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.0"
|
|
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.0"
|
|
82
82
|
},
|
|
83
83
|
"scripts": {
|
|
84
84
|
"build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
|