@sentry/junior 0.9.4 → 0.10.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.
@@ -1,9 +1,19 @@
1
+ import {
2
+ discoverSkills,
3
+ findSkillByName,
4
+ getCapabilityProvider,
5
+ listCapabilityProviders,
6
+ loadSkillsByName,
7
+ logCapabilityCatalogLoadedOnce,
8
+ parseSkillInvocation,
9
+ stripFrontmatter
10
+ } from "./chunk-WM66QDLA.js";
1
11
  import {
2
12
  SANDBOX_SKILLS_ROOT,
3
13
  SANDBOX_WORKSPACE_ROOT,
4
14
  botConfig,
5
15
  buildNonInteractiveShellScript,
6
- getConnectedStateContext,
16
+ getChatConfig,
7
17
  getRuntimeDependencyProfileHash,
8
18
  getRuntimeMetadata,
9
19
  getSlackBotToken,
@@ -17,20 +27,11 @@ import {
17
27
  runNonInteractiveCommand,
18
28
  sandboxSkillDir,
19
29
  toOptionalTrimmed
20
- } from "./chunk-FS5Y4CF2.js";
21
- import {
22
- discoverSkills,
23
- findSkillByName,
24
- getCapabilityProvider,
25
- listCapabilityProviders,
26
- loadSkillsByName,
27
- logCapabilityCatalogLoadedOnce,
28
- parseSkillInvocation,
29
- stripFrontmatter
30
- } from "./chunk-WM66QDLA.js";
30
+ } from "./chunk-BJ4EBVQK.js";
31
31
  import {
32
32
  CredentialUnavailableError,
33
33
  createPluginBroker,
34
+ createRequestContext,
34
35
  extractGenAiUsageAttributes,
35
36
  getPluginDefinition,
36
37
  getPluginMcpProviders,
@@ -57,102 +58,9 @@ import {
57
58
  soulPathCandidates
58
59
  } from "./chunk-KCLEEKYX.js";
59
60
 
60
- // src/chat/queue/errors.ts
61
- var DeferredThreadMessageError = class extends Error {
62
- code = "deferred_thread_message";
63
- reason;
64
- constructor(reason, threadId, details) {
65
- if (reason === "thread_locked") {
66
- super(
67
- `Queue message deferred because thread ${threadId} is already locked`
68
- );
69
- } else {
70
- super(
71
- `Queue message deferred for thread ${threadId} because activeTurnId=${details?.activeTurnId ?? "unknown"} is still in progress for currentTurnId=${details?.currentTurnId ?? "unknown"}`
72
- );
73
- }
74
- this.name = "DeferredThreadMessageError";
75
- this.reason = reason;
76
- }
77
- };
78
- function isDeferredThreadMessageError(error, reason) {
79
- if (!(error instanceof DeferredThreadMessageError)) {
80
- return false;
81
- }
82
- if (!reason) {
83
- return true;
84
- }
85
- return error.reason === reason;
86
- }
87
-
88
- // src/chat/queue/transport.ts
89
- import { handleCallback, send } from "@vercel/queue";
90
- async function sendQueueMessage(topicName, payload, options) {
91
- const result = await send(topicName, payload, {
92
- ...options?.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}
93
- });
94
- return result.messageId ?? void 0;
95
- }
96
- function createTransportCallbackHandler(handler, options) {
97
- return handleCallback(
98
- async (message, metadata) => {
99
- await handler(message, {
100
- messageId: metadata.messageId,
101
- deliveryCount: metadata.deliveryCount,
102
- topicName: metadata.topicName
103
- });
104
- },
105
- options ? {
106
- retry: options.retry ? (error, metadata) => options.retry?.(error, {
107
- messageId: metadata.messageId,
108
- deliveryCount: metadata.deliveryCount,
109
- topicName: metadata.topicName
110
- }) : void 0
111
- } : void 0
112
- );
113
- }
114
-
115
- // src/chat/queue/client.ts
116
- var THREAD_MESSAGE_TOPIC = "junior-thread-message";
117
- var MAX_DELIVERY_ATTEMPTS = 10;
118
- var THREAD_LOCK_RETRY_MAX_SECONDS = 30;
119
- var ACTIVE_TURN_RETRY_MAX_SECONDS = 300;
120
- function getThreadMessageTopic() {
121
- return THREAD_MESSAGE_TOPIC;
122
- }
123
- async function enqueueThreadMessage(payload, options) {
124
- return await sendQueueMessage(getThreadMessageTopic(), payload, options);
125
- }
126
- function createQueueCallbackHandler(handler) {
127
- return createTransportCallbackHandler(handler, {
128
- retry: (error, metadata) => {
129
- if (isDeferredThreadMessageError(error, "thread_locked")) {
130
- return {
131
- afterSeconds: Math.min(
132
- THREAD_LOCK_RETRY_MAX_SECONDS,
133
- Math.max(5, metadata.deliveryCount * 5)
134
- )
135
- };
136
- }
137
- if (isDeferredThreadMessageError(error, "active_turn")) {
138
- return {
139
- afterSeconds: Math.min(
140
- ACTIVE_TURN_RETRY_MAX_SECONDS,
141
- Math.max(30, metadata.deliveryCount * 30)
142
- )
143
- };
144
- }
145
- if (metadata.deliveryCount >= MAX_DELIVERY_ATTEMPTS) {
146
- return { acknowledge: true };
147
- }
148
- const backoffSeconds = Math.min(
149
- 300,
150
- Math.max(5, metadata.deliveryCount * 5)
151
- );
152
- return { afterSeconds: backoffSeconds };
153
- }
154
- });
155
- }
61
+ // src/handlers/webhooks.ts
62
+ import { after } from "next/server";
63
+ import * as Sentry from "@sentry/nextjs";
156
64
 
157
65
  // src/chat/app/production.ts
158
66
  import { createSlackAdapter } from "@chat-adapter/slack";
@@ -174,9 +82,10 @@ var replyDecisionSchema = z.object({
174
82
  confidence: z.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
175
83
  reason: z.string().optional().describe("Short reason for the decision.")
176
84
  });
177
- var ROUTER_CONFIDENCE_THRESHOLD = 0.9;
85
+ var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
178
86
  var LEADING_SLACK_MENTION_RE = /^\s*<@([A-Z0-9]+)(?:\|([^>]+))?>[\s,:-]*/i;
179
87
  var LEADING_NAMED_MENTION_RE = /^\s*@([a-z0-9._-]+)\b[\s,:-]*/i;
88
+ var TRANSCRIPT_MESSAGE_LINE_RE = /^\[(assistant|system|user)\]\s+[^:]+:\s+([\s\S]+)$/i;
180
89
  var THREAD_OPTOUT_PATTERNS = [
181
90
  /\bstop (?:watching|replying|participating)\b/i,
182
91
  /\bstay out\b/i,
@@ -220,6 +129,36 @@ function isThreadOptOutInstruction(rawText, text) {
220
129
  (pattern) => pattern.test(rawText) || pattern.test(text)
221
130
  );
222
131
  }
132
+ function getTranscriptMessageHints(conversationContext) {
133
+ if (!conversationContext) {
134
+ return {
135
+ latestPriorMessageRole: "[none]",
136
+ latestPriorAssistantMessage: "[none]"
137
+ };
138
+ }
139
+ const lines = conversationContext.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
140
+ let latestPriorMessageRole = "[none]";
141
+ let latestPriorAssistantMessage = "[none]";
142
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
143
+ const match = lines[index]?.match(TRANSCRIPT_MESSAGE_LINE_RE);
144
+ if (!match) {
145
+ continue;
146
+ }
147
+ if (latestPriorMessageRole === "[none]") {
148
+ latestPriorMessageRole = match[1].toLowerCase();
149
+ }
150
+ if (latestPriorAssistantMessage === "[none]" && match[1].toLowerCase() === "assistant") {
151
+ latestPriorAssistantMessage = match[2];
152
+ }
153
+ if (latestPriorMessageRole !== "[none]" && latestPriorAssistantMessage !== "[none]") {
154
+ break;
155
+ }
156
+ }
157
+ return {
158
+ latestPriorMessageRole,
159
+ latestPriorAssistantMessage
160
+ };
161
+ }
223
162
  function getSubscribedReplyPreflightDecision(args) {
224
163
  const text = args.text.trim();
225
164
  const rawText = args.rawText.trim();
@@ -241,6 +180,7 @@ function getSubscribedReplyPreflightDecision(args) {
241
180
  };
242
181
  }
243
182
  function buildRouterSystemPrompt(botUserName, conversationContext, isExplicitMention) {
183
+ const { latestPriorMessageRole, latestPriorAssistantMessage } = getTranscriptMessageHints(conversationContext);
244
184
  return [
245
185
  "You are a message router for a Slack assistant named Junior in a subscribed Slack thread.",
246
186
  "Decide whether Junior should reply to the latest message.",
@@ -264,8 +204,14 @@ function buildRouterSystemPrompt(botUserName, conversationContext, isExplicitMen
264
204
  "",
265
205
  "Examples of messages Junior SHOULD reply to (should_reply=true):",
266
206
  "- Direct follow-ups to Junior's response: 'Can you explain that last point in more detail?'",
207
+ "- Self-referential follow-ups after Junior just answered: 'What did you just say about the budget?', 'Can you explain your last response in more detail?'",
267
208
  "- Explicit requests for Junior's help: 'Junior, what's causing this error?'",
268
209
  "",
210
+ "Treat a message as directed at Junior when it explicitly refers to Junior's immediately previous reply",
211
+ "using language like 'you just said', 'your last response', 'your last answer', or similar self-reference.",
212
+ "Do not confuse that with general topic continuation. A message like 'What about the billing worker timeline?'",
213
+ "still should_reply=false unless it clearly asks Junior for help.",
214
+ "",
269
215
  "When in doubt, should_reply=false. Most messages in a thread are human-to-human conversation.",
270
216
  "",
271
217
  "If the user is clearly telling Junior to stop watching, replying, or participating in the thread,",
@@ -278,6 +224,8 @@ function buildRouterSystemPrompt(botUserName, conversationContext, isExplicitMen
278
224
  "",
279
225
  `<assistant-name>${escapeXml(botUserName)}</assistant-name>`,
280
226
  `<explicit-mention>${isExplicitMention ? "true" : "false"}</explicit-mention>`,
227
+ `<latest-prior-message-role>${escapeXml(latestPriorMessageRole)}</latest-prior-message-role>`,
228
+ `<latest-prior-assistant-message>${escapeXml(latestPriorAssistantMessage)}</latest-prior-assistant-message>`,
281
229
  `<thread-context>${escapeXml(conversationContext?.trim() || "[none]")}</thread-context>`
282
230
  ].join("\n");
283
231
  }
@@ -710,16 +658,6 @@ function createSlackTurnRuntime(deps) {
710
658
  );
711
659
  return;
712
660
  }
713
- if (isRetryableTurnError(error)) {
714
- deps.logException(
715
- error,
716
- "mention_handler_retryable_failure",
717
- errorContext,
718
- { "app.turn.retryable_reason": error.reason },
719
- "onNewMention failed with retryable error"
720
- );
721
- throw error;
722
- }
723
661
  const eventId = deps.logException(
724
662
  error,
725
663
  "mention_handler_failed",
@@ -852,16 +790,6 @@ function createSlackTurnRuntime(deps) {
852
790
  );
853
791
  return;
854
792
  }
855
- if (isRetryableTurnError(error)) {
856
- deps.logException(
857
- error,
858
- "subscribed_message_handler_retryable_failure",
859
- errorContext,
860
- { "app.turn.retryable_reason": error.reason },
861
- "onSubscribedMessage failed with retryable error"
862
- );
863
- throw error;
864
- }
865
793
  const eventId = deps.logException(
866
794
  error,
867
795
  "subscribed_message_handler_failed",
@@ -2592,6 +2520,7 @@ function buildSystemPrompt(params) {
2592
2520
  "- Use `bash` to inspect skill files from `skill_dir` and run shell commands inside the sandbox workspace.",
2593
2521
  "- When using CLI tools through `bash`, prefer deterministic non-interactive flags and avoid commands that wait for prompts or editors.",
2594
2522
  "- Keep routine setup and research steps silent in user-facing replies. Do not narrate duplicate checks, credential issuance, file writes, or similar internal progress unless the result is user-relevant.",
2523
+ "- If a routine prerequisite check finds nothing notable, omit it entirely from the final reply and report only the user-relevant outcome.",
2595
2524
  "- Prefer a single result-focused reply after tool work completes. Only send an interim reply when you need user input or have a concrete blocking problem to report.",
2596
2525
  "- Use `attachFile` for files that actually exist in the sandbox (for example screenshots, PDFs, logs), or for `attachment_path` values returned by `imageGenerate`.",
2597
2526
  "- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
@@ -4893,31 +4822,6 @@ async function addReactionToMessage(input) {
4893
4822
  );
4894
4823
  return { ok: true };
4895
4824
  }
4896
- async function removeReactionFromMessage(input) {
4897
- const client2 = getSlackClient();
4898
- const channelId = normalizeSlackConversationId(input.channelId);
4899
- if (!channelId) {
4900
- throw new Error("Slack reaction requires a valid channel ID");
4901
- }
4902
- const timestamp = input.timestamp.trim();
4903
- if (!timestamp) {
4904
- throw new Error("Slack reaction requires a target message timestamp");
4905
- }
4906
- const emoji = normalizeSlackEmojiName(input.emoji);
4907
- if (!emoji) {
4908
- throw new Error("Slack reaction requires a valid emoji alias name");
4909
- }
4910
- await withSlackRetries(
4911
- () => client2.reactions.remove({
4912
- channel: channelId,
4913
- timestamp,
4914
- name: emoji
4915
- }),
4916
- 3,
4917
- { action: "reactions.remove" }
4918
- );
4919
- return { ok: true };
4920
- }
4921
4825
  async function listChannelMessages(input) {
4922
4826
  const client2 = getSlackClient();
4923
4827
  const channelId = normalizeSlackConversationId(input.channelId);
@@ -8391,6 +8295,9 @@ function enforceAttachmentClaimTruth(text, hasAttachedFiles) {
8391
8295
  Note: No file was attached in this turn. I need to attach the file before claiming it is shared.`;
8392
8296
  }
8393
8297
 
8298
+ // src/chat/runtime/thread-state.ts
8299
+ import { THREAD_STATE_TTL_MS } from "chat";
8300
+
8394
8301
  // src/chat/configuration/validation.ts
8395
8302
  var CONFIG_KEY_RE = /^[a-z0-9]+(?:\.[a-z0-9-]+)+$/;
8396
8303
  var SECRET_KEY_RE = /(?:^|[_.-])(token|secret|password|passphrase|api[-_]?key|private[-_]?key|credential|auth)(?:$|[_.-])/i;
@@ -8823,6 +8730,25 @@ function buildArtifactStatePatch(patch) {
8823
8730
  }
8824
8731
 
8825
8732
  // src/chat/runtime/thread-state.ts
8733
+ function threadStateKey(threadId) {
8734
+ return `thread-state:${threadId}`;
8735
+ }
8736
+ function buildThreadStatePayload(patch) {
8737
+ const payload = {};
8738
+ if (patch.artifacts) {
8739
+ Object.assign(payload, buildArtifactStatePatch(patch.artifacts));
8740
+ }
8741
+ if (patch.conversation) {
8742
+ Object.assign(payload, buildConversationStatePatch(patch.conversation));
8743
+ }
8744
+ if (patch.sandboxId !== void 0) {
8745
+ payload.app_sandbox_id = patch.sandboxId;
8746
+ }
8747
+ if (patch.sandboxDependencyProfileHash !== void 0) {
8748
+ payload.app_sandbox_dependency_profile_hash = patch.sandboxDependencyProfileHash;
8749
+ }
8750
+ return payload;
8751
+ }
8826
8752
  function mergeArtifactsState(artifacts, patch) {
8827
8753
  if (!patch) {
8828
8754
  return artifacts;
@@ -8837,24 +8763,30 @@ function mergeArtifactsState(artifacts, patch) {
8837
8763
  };
8838
8764
  }
8839
8765
  async function persistThreadState(thread, patch) {
8840
- const payload = {};
8841
- if (patch.artifacts) {
8842
- Object.assign(payload, buildArtifactStatePatch(patch.artifacts));
8843
- }
8844
- if (patch.conversation) {
8845
- Object.assign(payload, buildConversationStatePatch(patch.conversation));
8846
- }
8847
- if (patch.sandboxId) {
8848
- payload.app_sandbox_id = patch.sandboxId;
8849
- }
8850
- if (patch.sandboxDependencyProfileHash) {
8851
- payload.app_sandbox_dependency_profile_hash = patch.sandboxDependencyProfileHash;
8852
- }
8766
+ const payload = buildThreadStatePayload(patch);
8853
8767
  if (Object.keys(payload).length === 0) {
8854
8768
  return;
8855
8769
  }
8856
8770
  await thread.setState(payload);
8857
8771
  }
8772
+ async function getPersistedThreadState(threadId) {
8773
+ const stateAdapter = getStateAdapter();
8774
+ await stateAdapter.connect();
8775
+ return await stateAdapter.get(
8776
+ threadStateKey(threadId)
8777
+ ) ?? {};
8778
+ }
8779
+ async function persistThreadStateById(threadId, patch) {
8780
+ const payload = buildThreadStatePayload(patch);
8781
+ if (Object.keys(payload).length === 0) {
8782
+ return;
8783
+ }
8784
+ const stateAdapter = getStateAdapter();
8785
+ await stateAdapter.connect();
8786
+ const key = threadStateKey(threadId);
8787
+ const existing = await stateAdapter.get(key) ?? {};
8788
+ await stateAdapter.set(key, { ...existing, ...payload }, THREAD_STATE_TTL_MS);
8789
+ }
8858
8790
  function getChannelConfigurationService(thread) {
8859
8791
  const channel = thread.channel;
8860
8792
  return createChannelConfigurationService({
@@ -8876,14 +8808,6 @@ function getSessionIdentifiers(context) {
8876
8808
  sessionId: context.correlation?.turnId
8877
8809
  };
8878
8810
  }
8879
- var AgentTurnTimeoutError = class extends Error {
8880
- timeoutMs;
8881
- constructor(timeoutMs) {
8882
- super(`Agent turn timed out after ${timeoutMs}ms`);
8883
- this.name = "AgentTurnTimeoutError";
8884
- this.timeoutMs = timeoutMs;
8885
- }
8886
- };
8887
8811
  var McpAuthorizationPauseError = class extends Error {
8888
8812
  provider;
8889
8813
  constructor(provider) {
@@ -9502,7 +9426,11 @@ async function generateAssistantReply(messageText, context = {}) {
9502
9426
  timeoutId = setTimeout(() => {
9503
9427
  didTimeout = true;
9504
9428
  agent.abort();
9505
- reject(new AgentTurnTimeoutError(botConfig.turnTimeoutMs));
9429
+ reject(
9430
+ new Error(
9431
+ `Agent turn timed out after ${botConfig.turnTimeoutMs}ms`
9432
+ )
9433
+ );
9506
9434
  }, botConfig.turnTimeoutMs);
9507
9435
  });
9508
9436
  try {
@@ -9737,71 +9665,6 @@ async function generateAssistantReply(messageText, context = {}) {
9737
9665
  `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`
9738
9666
  );
9739
9667
  }
9740
- if (error instanceof AgentTurnTimeoutError && timeoutResumeConversationId && timeoutResumeSessionId) {
9741
- const nextSliceId = timeoutResumeSliceId + 1;
9742
- logException(
9743
- error,
9744
- "agent_turn_timeout_resume_triggered",
9745
- {
9746
- slackThreadId: context.correlation?.threadId,
9747
- slackUserId: context.correlation?.requesterId,
9748
- slackChannelId: context.correlation?.channelId,
9749
- runId: context.correlation?.runId,
9750
- assistantUserName: context.assistant?.userName,
9751
- modelId: botConfig.modelId
9752
- },
9753
- {
9754
- "app.ai.turn_timeout_ms": error.timeoutMs,
9755
- "app.ai.resume_conversation_id": timeoutResumeConversationId,
9756
- "app.ai.resume_session_id": timeoutResumeSessionId,
9757
- "app.ai.resume_from_slice_id": timeoutResumeSliceId,
9758
- "app.ai.resume_next_slice_id": nextSliceId
9759
- },
9760
- "Agent turn timed out and will be resumed"
9761
- );
9762
- try {
9763
- const latestCheckpoint = await getAgentTurnSessionCheckpoint(
9764
- timeoutResumeConversationId,
9765
- timeoutResumeSessionId
9766
- );
9767
- const piMessages = timeoutResumeMessages.length > 0 ? timeoutResumeMessages : latestCheckpoint?.piMessages ?? [];
9768
- await upsertAgentTurnSessionCheckpoint({
9769
- conversationId: timeoutResumeConversationId,
9770
- sessionId: timeoutResumeSessionId,
9771
- sliceId: nextSliceId,
9772
- state: "awaiting_resume",
9773
- piMessages,
9774
- loadedSkillNames: loadedSkillNamesForResume,
9775
- resumeReason: "timeout",
9776
- resumedFromSliceId: timeoutResumeSliceId,
9777
- errorMessage: error.message
9778
- });
9779
- } catch (checkpointError) {
9780
- logException(
9781
- checkpointError,
9782
- "agent_turn_timeout_resume_checkpoint_failed",
9783
- {
9784
- slackThreadId: context.correlation?.threadId,
9785
- slackUserId: context.correlation?.requesterId,
9786
- slackChannelId: context.correlation?.channelId,
9787
- runId: context.correlation?.runId,
9788
- assistantUserName: context.assistant?.userName,
9789
- modelId: botConfig.modelId
9790
- },
9791
- {
9792
- "app.ai.resume_conversation_id": timeoutResumeConversationId,
9793
- "app.ai.resume_session_id": timeoutResumeSessionId,
9794
- "app.ai.resume_from_slice_id": timeoutResumeSliceId,
9795
- "app.ai.resume_next_slice_id": nextSliceId
9796
- },
9797
- "Failed to persist timeout checkpoint before retry"
9798
- );
9799
- }
9800
- throw new RetryableTurnError(
9801
- "agent_turn_timeout_resume",
9802
- `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`
9803
- );
9804
- }
9805
9668
  if (isRetryableTurnError(error)) {
9806
9669
  throw error;
9807
9670
  }
@@ -11236,7 +11099,10 @@ function createReplyToThread(deps) {
11236
11099
  );
11237
11100
  }
11238
11101
  } catch (error) {
11239
- shouldPersistFailureState = !isRetryableTurnError(error);
11102
+ shouldPersistFailureState = !isRetryableTurnError(
11103
+ error,
11104
+ "mcp_auth_resume"
11105
+ );
11240
11106
  throw error;
11241
11107
  } finally {
11242
11108
  textStream.end();
@@ -11510,57 +11376,7 @@ import {
11510
11376
  Chat
11511
11377
  } from "chat";
11512
11378
 
11513
- // src/chat/state/queue-ingress-store.ts
11514
- var QUEUE_INGRESS_DEDUP_PREFIX = "junior:queue_ingress";
11515
- function queueIngressDedupKey(rawKey) {
11516
- return `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
11517
- }
11518
- async function claimQueueIngressDedup(rawKey, ttlMs) {
11519
- const { stateAdapter, redisStateAdapter } = await getConnectedStateContext();
11520
- const key = queueIngressDedupKey(rawKey);
11521
- if (redisStateAdapter) {
11522
- const result = await redisStateAdapter.getClient().set(key, "1", {
11523
- NX: true,
11524
- PX: ttlMs
11525
- });
11526
- return result === "OK";
11527
- }
11528
- return await stateAdapter.setIfNotExists(key, "1", ttlMs);
11529
- }
11530
- async function hasQueueIngressDedup(rawKey) {
11531
- const { stateAdapter, redisStateAdapter } = await getConnectedStateContext();
11532
- const key = queueIngressDedupKey(rawKey);
11533
- const value = redisStateAdapter ? await redisStateAdapter.getClient().get(key) : await stateAdapter.get(key);
11534
- return typeof value === "string" && value.length > 0;
11535
- }
11536
-
11537
11379
  // src/chat/ingress/message-router.ts
11538
- var QUEUE_INGRESS_DEDUP_TTL_MS = 24 * 60 * 60 * 1e3;
11539
- function nonEmptyString(value) {
11540
- if (typeof value !== "string") return void 0;
11541
- const trimmed = value.trim();
11542
- return trimmed || void 0;
11543
- }
11544
- function serializeMessageForQueue(message) {
11545
- const candidate = message;
11546
- if (typeof candidate.toJSON === "function") {
11547
- return candidate.toJSON();
11548
- }
11549
- return {
11550
- _type: "chat:Message",
11551
- ...message
11552
- };
11553
- }
11554
- function serializeThreadForQueue(thread) {
11555
- const candidate = thread;
11556
- if (typeof candidate.toJSON === "function") {
11557
- return candidate.toJSON();
11558
- }
11559
- return {
11560
- _type: "chat:Thread",
11561
- ...thread
11562
- };
11563
- }
11564
11380
  function normalizeIncomingSlackThreadId(threadId, message) {
11565
11381
  if (!threadId.startsWith("slack:")) {
11566
11382
  return threadId;
@@ -11579,260 +11395,10 @@ function normalizeIncomingSlackThreadId(threadId, message) {
11579
11395
  }
11580
11396
  return `slack:${channelId}:${threadTs}`;
11581
11397
  }
11582
- function buildQueueIngressDedupKey(normalizedThreadId, messageId) {
11583
- return `${normalizedThreadId}:${messageId}`;
11584
- }
11585
- function isSlackDirectMessageThreadId(threadId) {
11586
- const parts = threadId.split(":");
11587
- return parts.length === 3 && parts[0] === "slack" && parts[1]?.startsWith("D");
11588
- }
11589
- function determineThreadMessageKind(args) {
11590
- if (args.isDirectMessage) {
11591
- return "new_mention";
11592
- }
11593
- if (args.isSubscribed) {
11594
- return "subscribed_message";
11595
- }
11596
- if (args.isMention) {
11597
- return "new_mention";
11598
- }
11599
- return void 0;
11600
- }
11601
- function getMessageLogContext(args) {
11602
- return {
11603
- slackThreadId: args.normalizedThreadId,
11604
- slackChannelId: nonEmptyString(args.message.raw?.channel),
11605
- slackUserId: args.message.author?.userId
11606
- };
11607
- }
11608
- function logIgnoredIngressResult(args) {
11609
- logInfo(
11610
- args.eventName,
11611
- args.logContext,
11612
- {
11613
- ...args.messageId ? { "messaging.message.id": args.messageId } : {},
11614
- ...args.kind ? { "app.queue.message_kind": args.kind } : {},
11615
- ...args.dedupKey ? { "app.queue.dedup_key": args.dedupKey } : {},
11616
- ...args.decisionReason ? { "app.decision.reason": args.decisionReason } : {},
11617
- "app.queue.route_result": args.routeResult
11618
- },
11619
- args.body
11620
- );
11621
- }
11622
- async function enqueueQueueIngressMessage(args) {
11623
- if (args.enqueueThreadMessage) {
11624
- return await args.enqueueThreadMessage(args.payload, args.dedupKey);
11625
- }
11626
- return await enqueueThreadMessage(args.payload, {
11627
- idempotencyKey: args.dedupKey
11628
- });
11629
- }
11630
- async function routeIncomingMessageToQueue(args) {
11631
- const { adapter, runtime } = args;
11632
- const message = args.message;
11633
- if (!message || typeof message !== "object") {
11634
- return "ignored_non_object";
11635
- }
11636
- const normalizedThreadId = normalizeIncomingSlackThreadId(
11637
- args.threadId,
11638
- message
11639
- );
11640
- const baseLogContext = getMessageLogContext({
11641
- message,
11642
- normalizedThreadId
11643
- });
11644
- if ("threadId" in message) {
11645
- message.threadId = normalizedThreadId;
11646
- }
11647
- const typedMessage = message;
11648
- if (typedMessage.author?.isMe) {
11649
- logIgnoredIngressResult({
11650
- eventName: "queue_ingress_ignored_self_message",
11651
- logContext: baseLogContext,
11652
- messageId: nonEmptyString(typedMessage.id),
11653
- routeResult: "ignored_self_message",
11654
- body: "Ignoring self-authored message before queue routing"
11655
- });
11656
- return "ignored_self_message";
11657
- }
11658
- const messageId = nonEmptyString(typedMessage.id);
11659
- if (!messageId) {
11660
- logIgnoredIngressResult({
11661
- eventName: "queue_ingress_ignored_missing_message_id",
11662
- logContext: baseLogContext,
11663
- routeResult: "ignored_missing_message_id",
11664
- body: "Ignoring message without an id before queue routing"
11665
- });
11666
- return "ignored_missing_message_id";
11667
- }
11668
- const isSubscribed = await getStateAdapter().isSubscribed(normalizedThreadId);
11669
- const mentionSource = typedMessage.isMention ? "sdk_flag" : runtime.detectMention?.(adapter, message) ? "fallback_detector" : void 0;
11670
- const isMention = mentionSource !== void 0;
11671
- if (isMention && !typedMessage.isMention) {
11672
- typedMessage.isMention = true;
11673
- }
11674
- const isDirectMessage = isSlackDirectMessageThreadId(normalizedThreadId);
11675
- const kind = determineThreadMessageKind({
11676
- isDirectMessage,
11677
- isSubscribed,
11678
- isMention
11679
- });
11680
- if (!kind) {
11681
- logIgnoredIngressResult({
11682
- eventName: "queue_ingress_ignored_unsubscribed_non_mention",
11683
- logContext: baseLogContext,
11684
- messageId,
11685
- routeResult: "ignored_unsubscribed_non_mention",
11686
- body: "Ignoring unsubscribed non-mention message before queue routing"
11687
- });
11688
- return "ignored_unsubscribed_non_mention";
11689
- }
11690
- const dedupKey = buildQueueIngressDedupKey(normalizedThreadId, messageId);
11691
- const alreadyDeduped = await hasQueueIngressDedup(dedupKey);
11692
- if (alreadyDeduped) {
11693
- logInfo(
11694
- "queue_ingress_dedup_hit",
11695
- baseLogContext,
11696
- {
11697
- "messaging.message.id": messageId,
11698
- "app.queue.message_kind": kind,
11699
- "app.queue.dedup_key": dedupKey,
11700
- "app.queue.dedup_outcome": "duplicate",
11701
- ...mentionSource ? { "app.slack.mention_source": mentionSource } : {},
11702
- "app.queue.route_result": "ignored_duplicate"
11703
- },
11704
- "Skipping duplicate incoming message before queue enqueue"
11705
- );
11706
- return "ignored_duplicate";
11707
- }
11708
- const thread = await runtime.createThread(
11709
- adapter,
11710
- normalizedThreadId,
11711
- message,
11712
- isSubscribed
11713
- );
11714
- const serializedMessage = serializeMessageForQueue(message);
11715
- const serializedThread = serializeThreadForQueue(thread);
11716
- const payload = {
11717
- dedupKey,
11718
- kind,
11719
- message: serializedMessage,
11720
- normalizedThreadId,
11721
- thread: serializedThread
11722
- };
11723
- await withContext(
11724
- {
11725
- slackThreadId: normalizedThreadId,
11726
- slackChannelId: thread.channelId,
11727
- slackUserId: message.author.userId
11728
- },
11729
- async () => {
11730
- let processingReactionAdded = false;
11731
- let queueMessageId;
11732
- try {
11733
- await addReactionToMessage({
11734
- channelId: thread.channelId,
11735
- timestamp: messageId,
11736
- emoji: "eyes"
11737
- });
11738
- processingReactionAdded = true;
11739
- } catch (error) {
11740
- const errorMessage = error instanceof Error ? error.message : String(error);
11741
- logWarn(
11742
- "queue_ingress_reaction_add_failed",
11743
- {},
11744
- {
11745
- "messaging.message.id": messageId,
11746
- "app.queue.message_kind": kind,
11747
- ...mentionSource ? { "app.slack.mention_source": mentionSource } : {},
11748
- "error.message": errorMessage
11749
- },
11750
- "Failed to add ingress processing reaction"
11751
- );
11752
- }
11753
- try {
11754
- await withSpan(
11755
- "queue.enqueue_message",
11756
- "queue.enqueue_message",
11757
- {
11758
- slackThreadId: normalizedThreadId,
11759
- slackChannelId: thread.channelId,
11760
- slackUserId: message.author.userId
11761
- },
11762
- async () => {
11763
- queueMessageId = await enqueueQueueIngressMessage({
11764
- dedupKey,
11765
- enqueueThreadMessage: args.enqueueThreadMessage,
11766
- payload
11767
- });
11768
- if (queueMessageId) {
11769
- setSpanAttributes({
11770
- "app.queue.message_id": queueMessageId
11771
- });
11772
- }
11773
- },
11774
- {
11775
- "messaging.message.id": messageId,
11776
- "app.queue.message_kind": kind
11777
- }
11778
- );
11779
- } catch (error) {
11780
- if (processingReactionAdded) {
11781
- try {
11782
- await removeReactionFromMessage({
11783
- channelId: thread.channelId,
11784
- timestamp: messageId,
11785
- emoji: "eyes"
11786
- });
11787
- } catch (cleanupError) {
11788
- const cleanupErrorMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
11789
- logWarn(
11790
- "queue_ingress_reaction_cleanup_failed",
11791
- {},
11792
- {
11793
- "messaging.message.id": messageId,
11794
- "app.queue.message_kind": kind,
11795
- "error.message": cleanupErrorMessage
11796
- },
11797
- "Failed to remove ingress processing reaction after enqueue failure"
11798
- );
11799
- }
11800
- }
11801
- throw error;
11802
- }
11803
- logInfo(
11804
- "queue_ingress_enqueued",
11805
- {},
11806
- {
11807
- "messaging.message.id": messageId,
11808
- "app.queue.message_kind": kind,
11809
- ...mentionSource ? { "app.slack.mention_source": mentionSource } : {},
11810
- "app.queue.dedup_key": dedupKey,
11811
- "app.queue.dedup_outcome": "primary",
11812
- "app.queue.route_result": "routed",
11813
- ...queueMessageId ? { "app.queue.message_id": queueMessageId } : {}
11814
- },
11815
- "Routing incoming message to queue"
11816
- );
11817
- const marked = await claimQueueIngressDedup(
11818
- dedupKey,
11819
- QUEUE_INGRESS_DEDUP_TTL_MS
11820
- );
11821
- if (!marked) {
11822
- logInfo(
11823
- "queue_ingress_dedup_mark_failed",
11824
- {},
11825
- {
11826
- "messaging.message.id": messageId,
11827
- "app.queue.message_kind": kind,
11828
- "app.queue.dedup_key": dedupKey
11829
- },
11830
- "Queue ingress dedup state write failed after enqueue"
11831
- );
11832
- }
11833
- }
11834
- );
11835
- return "routed";
11398
+ function nonEmptyString(value) {
11399
+ if (typeof value !== "string") return void 0;
11400
+ const trimmed = value.trim();
11401
+ return trimmed || void 0;
11836
11402
  }
11837
11403
 
11838
11404
  // src/chat/ingress/junior-chat.ts
@@ -11843,42 +11409,62 @@ function enqueueBackgroundTask(options, task) {
11843
11409
  options.waitUntil(task);
11844
11410
  }
11845
11411
  var JuniorChat = class extends Chat {
11412
+ /**
11413
+ * Normalize Slack thread IDs before the SDK's concurrency queue.
11414
+ *
11415
+ * The SDK uses the `threadId` parameter as the lock/queue key
11416
+ * (Chat.handleIncomingMessage → getLockKey). @chat-adapter/slack
11417
+ * (as of 4.22.0) builds DM thread IDs as `slack:<channel>:` (empty
11418
+ * thread_ts) when the Slack event has no `thread_ts` field — it uses
11419
+ * `event.thread_ts || ""` instead of falling back to `event.ts`.
11420
+ * See @chat-adapter/slack/dist/index.js:1466.
11421
+ *
11422
+ * A DM root event arrives as `slack:D123:` while a reply in the same
11423
+ * thread carries `slack:D123:<ts>`, splitting the lock/state/subscription
11424
+ * keys and breaking conversation continuity.
11425
+ *
11426
+ * We fix this by resolving the message eagerly (even when the adapter
11427
+ * provides a factory), deriving the canonical thread ID from
11428
+ * `raw.channel` + `raw.thread_ts ?? raw.ts`, and passing both the
11429
+ * normalized threadId and concrete message to super.processMessage.
11430
+ *
11431
+ * Remove this override when @chat-adapter/slack uses `event.ts` as
11432
+ * the DM thread_ts fallback.
11433
+ */
11846
11434
  processMessage(adapter, threadId, messageOrFactory, options) {
11847
- const runtime = this;
11848
- enqueueBackgroundTask(
11849
- options,
11850
- (async () => {
11851
- try {
11852
- const message = typeof messageOrFactory === "function" ? await messageOrFactory() : messageOrFactory;
11853
- const result = await routeIncomingMessageToQueue({
11854
- adapter,
11855
- threadId,
11856
- message,
11857
- runtime: {
11858
- createThread: runtime.createThread.bind(
11859
- this
11860
- ),
11861
- detectMention: runtime.detectMention?.bind(this)
11862
- }
11863
- });
11864
- if (result === "ignored_missing_message_id") {
11865
- const normalizedThreadId = normalizeIncomingSlackThreadId(
11435
+ if (typeof messageOrFactory === "function") {
11436
+ const runtime = this;
11437
+ enqueueBackgroundTask(
11438
+ options,
11439
+ (async () => {
11440
+ try {
11441
+ const message = await messageOrFactory();
11442
+ const normalized2 = normalizeIncomingSlackThreadId(
11866
11443
  threadId,
11867
11444
  message
11868
11445
  );
11869
- runtime.logger?.error?.("Message processing error", {
11870
- threadId: normalizedThreadId,
11871
- reason: "missing_message_id"
11446
+ if (normalized2 !== threadId && "threadId" in message) {
11447
+ message.threadId = normalized2;
11448
+ }
11449
+ super.processMessage(adapter, normalized2, message, options);
11450
+ } catch (error) {
11451
+ runtime.logger?.error?.("Message factory resolution error", {
11452
+ error,
11453
+ threadId
11872
11454
  });
11873
11455
  }
11874
- } catch (error) {
11875
- runtime.logger?.error?.("Message processing error", {
11876
- error,
11877
- threadId
11878
- });
11879
- }
11880
- })()
11456
+ })()
11457
+ );
11458
+ return;
11459
+ }
11460
+ const normalized = normalizeIncomingSlackThreadId(
11461
+ threadId,
11462
+ messageOrFactory
11881
11463
  );
11464
+ if (normalized !== threadId && "threadId" in messageOrFactory) {
11465
+ messageOrFactory.threadId = normalized;
11466
+ }
11467
+ super.processMessage(adapter, normalized, messageOrFactory, options);
11882
11468
  }
11883
11469
  processReaction(event, options) {
11884
11470
  const runtime = this;
@@ -12159,6 +11745,15 @@ async function publishAppHomeView(slackClient, userId, userTokenStore) {
12159
11745
  await slackClient.views.publish({ user_id: userId, view });
12160
11746
  }
12161
11747
 
11748
+ // src/chat/queue/thread-message-dispatcher.ts
11749
+ function rehydrateAttachmentFetchers(message, downloadPrivateSlackFile2 = downloadPrivateSlackFile) {
11750
+ for (const attachment of message.attachments) {
11751
+ if (!attachment.fetchData && attachment.url) {
11752
+ attachment.fetchData = () => downloadPrivateSlackFile2(attachment.url);
11753
+ }
11754
+ }
11755
+ }
11756
+
12162
11757
  // src/chat/ingress/slash-command.ts
12163
11758
  async function postEphemeral(event, text) {
12164
11759
  await event.channel.postEphemeral(event.user, text, { fallbackToDM: false });
@@ -12248,6 +11843,15 @@ var productionSlackRuntime;
12248
11843
  function createProductionBot() {
12249
11844
  return new JuniorChat({
12250
11845
  userName: botConfig.userName,
11846
+ concurrency: {
11847
+ strategy: "queue",
11848
+ // The SDK's default queueEntryTtlMs is 90s, but Junior turns can
11849
+ // run up to botConfig.turnTimeoutMs (default 12min). A follow-up
11850
+ // message that arrives during a long turn would expire in the
11851
+ // queue before the lock is released. Set the TTL to exceed the
11852
+ // maximum turn duration so queued messages survive.
11853
+ queueEntryTtlMs: botConfig.turnTimeoutMs + 6e4
11854
+ },
12251
11855
  adapters: {
12252
11856
  slack: (() => {
12253
11857
  const signingSecret = getSlackSigningSecret();
@@ -12268,9 +11872,22 @@ function createProductionBot() {
12268
11872
  state: getStateAdapter()
12269
11873
  });
12270
11874
  }
11875
+ function rehydrateAttachments(message) {
11876
+ rehydrateAttachmentFetchers(message);
11877
+ }
12271
11878
  function registerProductionHandlers(bot, slackRuntime) {
12272
- bot.onNewMention(slackRuntime.handleNewMention);
12273
- bot.onSubscribedMessage(slackRuntime.handleSubscribedMessage);
11879
+ bot.onNewMention((thread, message) => {
11880
+ rehydrateAttachments(message);
11881
+ return slackRuntime.handleNewMention(thread, message);
11882
+ });
11883
+ bot.onDirectMessage((thread, message) => {
11884
+ rehydrateAttachments(message);
11885
+ return slackRuntime.handleNewMention(thread, message);
11886
+ });
11887
+ bot.onSubscribedMessage((thread, message) => {
11888
+ rehydrateAttachments(message);
11889
+ return slackRuntime.handleSubscribedMessage(thread, message);
11890
+ });
12274
11891
  bot.onAssistantThreadStarted(
12275
11892
  (event) => slackRuntime.handleAssistantThreadStarted(event)
12276
11893
  );
@@ -12365,9 +11982,91 @@ function getProductionBot() {
12365
11982
  initializeProductionApp();
12366
11983
  return productionBot;
12367
11984
  }
12368
- function getProductionSlackRuntime() {
12369
- initializeProductionApp();
12370
- return productionSlackRuntime;
11985
+
11986
+ // src/handlers/webhooks.ts
11987
+ var maxDuration = getChatConfig().functionMaxDurationSeconds;
11988
+ async function POST(request, context) {
11989
+ const bot = getProductionBot();
11990
+ const { platform } = await context.params;
11991
+ const handler = bot.webhooks[platform];
11992
+ const requestContext = createRequestContext(request, { platform });
11993
+ const requestUrl = new URL(request.url);
11994
+ return withContext(requestContext, async () => {
11995
+ if (!handler) {
11996
+ const error = new Error(`Unknown platform: ${platform}`);
11997
+ logException(
11998
+ error,
11999
+ "webhook_platform_unknown",
12000
+ {},
12001
+ {
12002
+ "http.response.status_code": 404
12003
+ },
12004
+ `Unknown platform: ${platform}`
12005
+ );
12006
+ return new Response(`Unknown platform: ${platform}`, { status: 404 });
12007
+ }
12008
+ try {
12009
+ return await withSpan(
12010
+ "http.server.request",
12011
+ "http.server",
12012
+ requestContext,
12013
+ async () => {
12014
+ try {
12015
+ const activeSpan = Sentry.getActiveSpan();
12016
+ const response = await handler(request, {
12017
+ waitUntil: (task) => after(() => {
12018
+ const runTask = () => {
12019
+ const taskOrFactory = task;
12020
+ return typeof taskOrFactory === "function" ? taskOrFactory() : taskOrFactory;
12021
+ };
12022
+ if (activeSpan) {
12023
+ return Sentry.withActiveSpan(activeSpan, runTask);
12024
+ }
12025
+ return runTask();
12026
+ })
12027
+ });
12028
+ if (response.status >= 400) {
12029
+ let responseBodySnippet;
12030
+ try {
12031
+ responseBodySnippet = (await response.clone().text()).slice(
12032
+ 0,
12033
+ 300
12034
+ );
12035
+ } catch {
12036
+ responseBodySnippet = void 0;
12037
+ }
12038
+ logWarn(
12039
+ "webhook_non_success_response",
12040
+ {},
12041
+ {
12042
+ "http.response.status_code": response.status,
12043
+ "http.request.header.x_slack_signature": request.headers.get("x-slack-signature") ?? void 0,
12044
+ "http.request.header.x_slack_request_timestamp": request.headers.get("x-slack-request-timestamp") ?? void 0,
12045
+ ...responseBodySnippet ? { "app.webhook.response_body": responseBodySnippet } : {}
12046
+ },
12047
+ `Webhook ${platform} returned ${response.status}`
12048
+ );
12049
+ }
12050
+ setSpanAttributes({
12051
+ "http.response.status_code": response.status
12052
+ });
12053
+ setSpanStatus(response.status >= 500 ? "error" : "ok");
12054
+ return response;
12055
+ } catch (error) {
12056
+ setSpanStatus("error");
12057
+ throw error;
12058
+ }
12059
+ },
12060
+ {
12061
+ "http.request.method": request.method,
12062
+ "url.path": requestUrl.pathname
12063
+ }
12064
+ );
12065
+ } catch (error) {
12066
+ logException(error, "webhook_handler_failed");
12067
+ throw error;
12068
+ }
12069
+ });
12371
12070
  }
12372
12071
 
12373
12072
  export {
@@ -12376,13 +12075,13 @@ export {
12376
12075
  buildSlackOutputMessage,
12377
12076
  getSlackClient,
12378
12077
  uploadFilesToThread,
12379
- downloadPrivateSlackFile,
12380
12078
  formatProviderLabel,
12381
12079
  resolveBaseUrl,
12382
12080
  finalizeMcpAuthorization,
12383
12081
  coerceThreadArtifactsState,
12384
12082
  mergeArtifactsState,
12385
- persistThreadState,
12083
+ getPersistedThreadState,
12084
+ persistThreadStateById,
12386
12085
  generateConversationId,
12387
12086
  normalizeConversationText,
12388
12087
  updateConversationStats,
@@ -12391,18 +12090,13 @@ export {
12391
12090
  buildConversationContext,
12392
12091
  escapeXml,
12393
12092
  createUserTokenStore,
12394
- removeReactionFromMessage,
12395
12093
  truncateStatusText,
12396
- buildDeterministicTurnId,
12397
12094
  isRetryableTurnError,
12398
12095
  markTurnCompleted,
12399
12096
  markTurnFailed,
12400
12097
  resolveReplyDelivery,
12401
12098
  generateAssistantReply,
12402
12099
  publishAppHomeView,
12403
- DeferredThreadMessageError,
12404
- getThreadMessageTopic,
12405
- createQueueCallbackHandler,
12406
- getProductionBot,
12407
- getProductionSlackRuntime
12100
+ maxDuration,
12101
+ POST
12408
12102
  };