@sentry/junior 0.9.3 → 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.
@@ -7,13 +7,13 @@ import {
7
7
  logCapabilityCatalogLoadedOnce,
8
8
  parseSkillInvocation,
9
9
  stripFrontmatter
10
- } from "./chunk-VM3CPAZF.js";
10
+ } from "./chunk-WM66QDLA.js";
11
11
  import {
12
12
  SANDBOX_SKILLS_ROOT,
13
13
  SANDBOX_WORKSPACE_ROOT,
14
14
  botConfig,
15
15
  buildNonInteractiveShellScript,
16
- getConnectedStateContext,
16
+ getChatConfig,
17
17
  getRuntimeDependencyProfileHash,
18
18
  getRuntimeMetadata,
19
19
  getSlackBotToken,
@@ -27,28 +27,22 @@ import {
27
27
  runNonInteractiveCommand,
28
28
  sandboxSkillDir,
29
29
  toOptionalTrimmed
30
- } from "./chunk-HRA2FXYH.js";
30
+ } from "./chunk-BJ4EBVQK.js";
31
31
  import {
32
32
  CredentialUnavailableError,
33
33
  createPluginBroker,
34
+ createRequestContext,
35
+ extractGenAiUsageAttributes,
34
36
  getPluginDefinition,
35
37
  getPluginMcpProviders,
36
38
  getPluginOAuthConfig,
37
39
  getPluginProviders,
38
40
  isPluginProvider,
39
- resolveAuthTokenPlaceholder
40
- } from "./chunk-ZBWWHP6Q.js";
41
- import {
42
- aboutPathCandidates,
43
- homeDir,
44
- soulPathCandidates
45
- } from "./chunk-KCLEEKYX.js";
46
- import {
47
- extractGenAiUsageAttributes,
48
41
  logError,
49
42
  logException,
50
43
  logInfo,
51
44
  logWarn,
45
+ resolveAuthTokenPlaceholder,
52
46
  resolveErrorReference,
53
47
  serializeGenAiAttribute,
54
48
  setSpanAttributes,
@@ -57,7 +51,16 @@ import {
57
51
  toOptionalString,
58
52
  withContext,
59
53
  withSpan
60
- } from "./chunk-ZW4OVKF5.js";
54
+ } from "./chunk-MY7JNCS2.js";
55
+ import {
56
+ aboutPathCandidates,
57
+ homeDir,
58
+ soulPathCandidates
59
+ } from "./chunk-KCLEEKYX.js";
60
+
61
+ // src/handlers/webhooks.ts
62
+ import { after } from "next/server";
63
+ import * as Sentry from "@sentry/nextjs";
61
64
 
62
65
  // src/chat/app/production.ts
63
66
  import { createSlackAdapter } from "@chat-adapter/slack";
@@ -79,9 +82,10 @@ var replyDecisionSchema = z.object({
79
82
  confidence: z.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
80
83
  reason: z.string().optional().describe("Short reason for the decision.")
81
84
  });
82
- var ROUTER_CONFIDENCE_THRESHOLD = 0.9;
85
+ var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
83
86
  var LEADING_SLACK_MENTION_RE = /^\s*<@([A-Z0-9]+)(?:\|([^>]+))?>[\s,:-]*/i;
84
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;
85
89
  var THREAD_OPTOUT_PATTERNS = [
86
90
  /\bstop (?:watching|replying|participating)\b/i,
87
91
  /\bstay out\b/i,
@@ -125,6 +129,36 @@ function isThreadOptOutInstruction(rawText, text) {
125
129
  (pattern) => pattern.test(rawText) || pattern.test(text)
126
130
  );
127
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
+ }
128
162
  function getSubscribedReplyPreflightDecision(args) {
129
163
  const text = args.text.trim();
130
164
  const rawText = args.rawText.trim();
@@ -146,6 +180,7 @@ function getSubscribedReplyPreflightDecision(args) {
146
180
  };
147
181
  }
148
182
  function buildRouterSystemPrompt(botUserName, conversationContext, isExplicitMention) {
183
+ const { latestPriorMessageRole, latestPriorAssistantMessage } = getTranscriptMessageHints(conversationContext);
149
184
  return [
150
185
  "You are a message router for a Slack assistant named Junior in a subscribed Slack thread.",
151
186
  "Decide whether Junior should reply to the latest message.",
@@ -169,8 +204,14 @@ function buildRouterSystemPrompt(botUserName, conversationContext, isExplicitMen
169
204
  "",
170
205
  "Examples of messages Junior SHOULD reply to (should_reply=true):",
171
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?'",
172
208
  "- Explicit requests for Junior's help: 'Junior, what's causing this error?'",
173
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
+ "",
174
215
  "When in doubt, should_reply=false. Most messages in a thread are human-to-human conversation.",
175
216
  "",
176
217
  "If the user is clearly telling Junior to stop watching, replying, or participating in the thread,",
@@ -183,6 +224,8 @@ function buildRouterSystemPrompt(botUserName, conversationContext, isExplicitMen
183
224
  "",
184
225
  `<assistant-name>${escapeXml(botUserName)}</assistant-name>`,
185
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>`,
186
229
  `<thread-context>${escapeXml(conversationContext?.trim() || "[none]")}</thread-context>`
187
230
  ].join("\n");
188
231
  }
@@ -615,16 +658,6 @@ function createSlackTurnRuntime(deps) {
615
658
  );
616
659
  return;
617
660
  }
618
- if (isRetryableTurnError(error)) {
619
- deps.logException(
620
- error,
621
- "mention_handler_retryable_failure",
622
- errorContext,
623
- { "app.turn.retryable_reason": error.reason },
624
- "onNewMention failed with retryable error"
625
- );
626
- throw error;
627
- }
628
661
  const eventId = deps.logException(
629
662
  error,
630
663
  "mention_handler_failed",
@@ -757,16 +790,6 @@ function createSlackTurnRuntime(deps) {
757
790
  );
758
791
  return;
759
792
  }
760
- if (isRetryableTurnError(error)) {
761
- deps.logException(
762
- error,
763
- "subscribed_message_handler_retryable_failure",
764
- errorContext,
765
- { "app.turn.retryable_reason": error.reason },
766
- "onSubscribedMessage failed with retryable error"
767
- );
768
- throw error;
769
- }
770
793
  const eventId = deps.logException(
771
794
  error,
772
795
  "subscribed_message_handler_failed",
@@ -2497,6 +2520,7 @@ function buildSystemPrompt(params) {
2497
2520
  "- Use `bash` to inspect skill files from `skill_dir` and run shell commands inside the sandbox workspace.",
2498
2521
  "- When using CLI tools through `bash`, prefer deterministic non-interactive flags and avoid commands that wait for prompts or editors.",
2499
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.",
2500
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.",
2501
2525
  "- Use `attachFile` for files that actually exist in the sandbox (for example screenshots, PDFs, logs), or for `attachment_path` values returned by `imageGenerate`.",
2502
2526
  "- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
@@ -4798,31 +4822,6 @@ async function addReactionToMessage(input) {
4798
4822
  );
4799
4823
  return { ok: true };
4800
4824
  }
4801
- async function removeReactionFromMessage(input) {
4802
- const client2 = getSlackClient();
4803
- const channelId = normalizeSlackConversationId(input.channelId);
4804
- if (!channelId) {
4805
- throw new Error("Slack reaction requires a valid channel ID");
4806
- }
4807
- const timestamp = input.timestamp.trim();
4808
- if (!timestamp) {
4809
- throw new Error("Slack reaction requires a target message timestamp");
4810
- }
4811
- const emoji = normalizeSlackEmojiName(input.emoji);
4812
- if (!emoji) {
4813
- throw new Error("Slack reaction requires a valid emoji alias name");
4814
- }
4815
- await withSlackRetries(
4816
- () => client2.reactions.remove({
4817
- channel: channelId,
4818
- timestamp,
4819
- name: emoji
4820
- }),
4821
- 3,
4822
- { action: "reactions.remove" }
4823
- );
4824
- return { ok: true };
4825
- }
4826
4825
  async function listChannelMessages(input) {
4827
4826
  const client2 = getSlackClient();
4828
4827
  const channelId = normalizeSlackConversationId(input.channelId);
@@ -8296,6 +8295,9 @@ function enforceAttachmentClaimTruth(text, hasAttachedFiles) {
8296
8295
  Note: No file was attached in this turn. I need to attach the file before claiming it is shared.`;
8297
8296
  }
8298
8297
 
8298
+ // src/chat/runtime/thread-state.ts
8299
+ import { THREAD_STATE_TTL_MS } from "chat";
8300
+
8299
8301
  // src/chat/configuration/validation.ts
8300
8302
  var CONFIG_KEY_RE = /^[a-z0-9]+(?:\.[a-z0-9-]+)+$/;
8301
8303
  var SECRET_KEY_RE = /(?:^|[_.-])(token|secret|password|passphrase|api[-_]?key|private[-_]?key|credential|auth)(?:$|[_.-])/i;
@@ -8728,6 +8730,25 @@ function buildArtifactStatePatch(patch) {
8728
8730
  }
8729
8731
 
8730
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
+ }
8731
8752
  function mergeArtifactsState(artifacts, patch) {
8732
8753
  if (!patch) {
8733
8754
  return artifacts;
@@ -8742,24 +8763,30 @@ function mergeArtifactsState(artifacts, patch) {
8742
8763
  };
8743
8764
  }
8744
8765
  async function persistThreadState(thread, patch) {
8745
- const payload = {};
8746
- if (patch.artifacts) {
8747
- Object.assign(payload, buildArtifactStatePatch(patch.artifacts));
8748
- }
8749
- if (patch.conversation) {
8750
- Object.assign(payload, buildConversationStatePatch(patch.conversation));
8751
- }
8752
- if (patch.sandboxId) {
8753
- payload.app_sandbox_id = patch.sandboxId;
8754
- }
8755
- if (patch.sandboxDependencyProfileHash) {
8756
- payload.app_sandbox_dependency_profile_hash = patch.sandboxDependencyProfileHash;
8757
- }
8766
+ const payload = buildThreadStatePayload(patch);
8758
8767
  if (Object.keys(payload).length === 0) {
8759
8768
  return;
8760
8769
  }
8761
8770
  await thread.setState(payload);
8762
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
+ }
8763
8790
  function getChannelConfigurationService(thread) {
8764
8791
  const channel = thread.channel;
8765
8792
  return createChannelConfigurationService({
@@ -8781,14 +8808,6 @@ function getSessionIdentifiers(context) {
8781
8808
  sessionId: context.correlation?.turnId
8782
8809
  };
8783
8810
  }
8784
- var AgentTurnTimeoutError = class extends Error {
8785
- timeoutMs;
8786
- constructor(timeoutMs) {
8787
- super(`Agent turn timed out after ${timeoutMs}ms`);
8788
- this.name = "AgentTurnTimeoutError";
8789
- this.timeoutMs = timeoutMs;
8790
- }
8791
- };
8792
8811
  var McpAuthorizationPauseError = class extends Error {
8793
8812
  provider;
8794
8813
  constructor(provider) {
@@ -9407,7 +9426,11 @@ async function generateAssistantReply(messageText, context = {}) {
9407
9426
  timeoutId = setTimeout(() => {
9408
9427
  didTimeout = true;
9409
9428
  agent.abort();
9410
- reject(new AgentTurnTimeoutError(botConfig.turnTimeoutMs));
9429
+ reject(
9430
+ new Error(
9431
+ `Agent turn timed out after ${botConfig.turnTimeoutMs}ms`
9432
+ )
9433
+ );
9411
9434
  }, botConfig.turnTimeoutMs);
9412
9435
  });
9413
9436
  try {
@@ -9642,71 +9665,6 @@ async function generateAssistantReply(messageText, context = {}) {
9642
9665
  `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`
9643
9666
  );
9644
9667
  }
9645
- if (error instanceof AgentTurnTimeoutError && timeoutResumeConversationId && timeoutResumeSessionId) {
9646
- const nextSliceId = timeoutResumeSliceId + 1;
9647
- logException(
9648
- error,
9649
- "agent_turn_timeout_resume_triggered",
9650
- {
9651
- slackThreadId: context.correlation?.threadId,
9652
- slackUserId: context.correlation?.requesterId,
9653
- slackChannelId: context.correlation?.channelId,
9654
- runId: context.correlation?.runId,
9655
- assistantUserName: context.assistant?.userName,
9656
- modelId: botConfig.modelId
9657
- },
9658
- {
9659
- "app.ai.turn_timeout_ms": error.timeoutMs,
9660
- "app.ai.resume_conversation_id": timeoutResumeConversationId,
9661
- "app.ai.resume_session_id": timeoutResumeSessionId,
9662
- "app.ai.resume_from_slice_id": timeoutResumeSliceId,
9663
- "app.ai.resume_next_slice_id": nextSliceId
9664
- },
9665
- "Agent turn timed out and will be resumed"
9666
- );
9667
- try {
9668
- const latestCheckpoint = await getAgentTurnSessionCheckpoint(
9669
- timeoutResumeConversationId,
9670
- timeoutResumeSessionId
9671
- );
9672
- const piMessages = timeoutResumeMessages.length > 0 ? timeoutResumeMessages : latestCheckpoint?.piMessages ?? [];
9673
- await upsertAgentTurnSessionCheckpoint({
9674
- conversationId: timeoutResumeConversationId,
9675
- sessionId: timeoutResumeSessionId,
9676
- sliceId: nextSliceId,
9677
- state: "awaiting_resume",
9678
- piMessages,
9679
- loadedSkillNames: loadedSkillNamesForResume,
9680
- resumeReason: "timeout",
9681
- resumedFromSliceId: timeoutResumeSliceId,
9682
- errorMessage: error.message
9683
- });
9684
- } catch (checkpointError) {
9685
- logException(
9686
- checkpointError,
9687
- "agent_turn_timeout_resume_checkpoint_failed",
9688
- {
9689
- slackThreadId: context.correlation?.threadId,
9690
- slackUserId: context.correlation?.requesterId,
9691
- slackChannelId: context.correlation?.channelId,
9692
- runId: context.correlation?.runId,
9693
- assistantUserName: context.assistant?.userName,
9694
- modelId: botConfig.modelId
9695
- },
9696
- {
9697
- "app.ai.resume_conversation_id": timeoutResumeConversationId,
9698
- "app.ai.resume_session_id": timeoutResumeSessionId,
9699
- "app.ai.resume_from_slice_id": timeoutResumeSliceId,
9700
- "app.ai.resume_next_slice_id": nextSliceId
9701
- },
9702
- "Failed to persist timeout checkpoint before retry"
9703
- );
9704
- }
9705
- throw new RetryableTurnError(
9706
- "agent_turn_timeout_resume",
9707
- `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`
9708
- );
9709
- }
9710
9668
  if (isRetryableTurnError(error)) {
9711
9669
  throw error;
9712
9670
  }
@@ -11141,7 +11099,10 @@ function createReplyToThread(deps) {
11141
11099
  );
11142
11100
  }
11143
11101
  } catch (error) {
11144
- shouldPersistFailureState = !isRetryableTurnError(error);
11102
+ shouldPersistFailureState = !isRetryableTurnError(
11103
+ error,
11104
+ "mcp_auth_resume"
11105
+ );
11145
11106
  throw error;
11146
11107
  } finally {
11147
11108
  textStream.end();
@@ -11415,154 +11376,7 @@ import {
11415
11376
  Chat
11416
11377
  } from "chat";
11417
11378
 
11418
- // src/chat/queue/errors.ts
11419
- var DeferredThreadMessageError = class extends Error {
11420
- code = "deferred_thread_message";
11421
- reason;
11422
- constructor(reason, threadId, details) {
11423
- if (reason === "thread_locked") {
11424
- super(
11425
- `Queue message deferred because thread ${threadId} is already locked`
11426
- );
11427
- } else {
11428
- super(
11429
- `Queue message deferred for thread ${threadId} because activeTurnId=${details?.activeTurnId ?? "unknown"} is still in progress for currentTurnId=${details?.currentTurnId ?? "unknown"}`
11430
- );
11431
- }
11432
- this.name = "DeferredThreadMessageError";
11433
- this.reason = reason;
11434
- }
11435
- };
11436
- function isDeferredThreadMessageError(error, reason) {
11437
- if (!(error instanceof DeferredThreadMessageError)) {
11438
- return false;
11439
- }
11440
- if (!reason) {
11441
- return true;
11442
- }
11443
- return error.reason === reason;
11444
- }
11445
-
11446
- // src/chat/queue/transport.ts
11447
- import { handleCallback, send } from "@vercel/queue";
11448
- async function sendQueueMessage(topicName, payload, options) {
11449
- const result = await send(topicName, payload, {
11450
- ...options?.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}
11451
- });
11452
- return result.messageId ?? void 0;
11453
- }
11454
- function createTransportCallbackHandler(handler, options) {
11455
- return handleCallback(
11456
- async (message, metadata) => {
11457
- await handler(message, {
11458
- messageId: metadata.messageId,
11459
- deliveryCount: metadata.deliveryCount,
11460
- topicName: metadata.topicName
11461
- });
11462
- },
11463
- options ? {
11464
- retry: options.retry ? (error, metadata) => options.retry?.(error, {
11465
- messageId: metadata.messageId,
11466
- deliveryCount: metadata.deliveryCount,
11467
- topicName: metadata.topicName
11468
- }) : void 0
11469
- } : void 0
11470
- );
11471
- }
11472
-
11473
- // src/chat/queue/client.ts
11474
- var THREAD_MESSAGE_TOPIC = "junior-thread-message";
11475
- var MAX_DELIVERY_ATTEMPTS = 10;
11476
- var THREAD_LOCK_RETRY_MAX_SECONDS = 30;
11477
- var ACTIVE_TURN_RETRY_MAX_SECONDS = 300;
11478
- function getThreadMessageTopic() {
11479
- return THREAD_MESSAGE_TOPIC;
11480
- }
11481
- async function enqueueThreadMessage(payload, options) {
11482
- return await sendQueueMessage(getThreadMessageTopic(), payload, options);
11483
- }
11484
- function createQueueCallbackHandler(handler) {
11485
- return createTransportCallbackHandler(handler, {
11486
- retry: (error, metadata) => {
11487
- if (isDeferredThreadMessageError(error, "thread_locked")) {
11488
- return {
11489
- afterSeconds: Math.min(
11490
- THREAD_LOCK_RETRY_MAX_SECONDS,
11491
- Math.max(5, metadata.deliveryCount * 5)
11492
- )
11493
- };
11494
- }
11495
- if (isDeferredThreadMessageError(error, "active_turn")) {
11496
- return {
11497
- afterSeconds: Math.min(
11498
- ACTIVE_TURN_RETRY_MAX_SECONDS,
11499
- Math.max(30, metadata.deliveryCount * 30)
11500
- )
11501
- };
11502
- }
11503
- if (metadata.deliveryCount >= MAX_DELIVERY_ATTEMPTS) {
11504
- return { acknowledge: true };
11505
- }
11506
- const backoffSeconds = Math.min(
11507
- 300,
11508
- Math.max(5, metadata.deliveryCount * 5)
11509
- );
11510
- return { afterSeconds: backoffSeconds };
11511
- }
11512
- });
11513
- }
11514
-
11515
- // src/chat/state/queue-ingress-store.ts
11516
- var QUEUE_INGRESS_DEDUP_PREFIX = "junior:queue_ingress";
11517
- function queueIngressDedupKey(rawKey) {
11518
- return `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
11519
- }
11520
- async function claimQueueIngressDedup(rawKey, ttlMs) {
11521
- const { stateAdapter, redisStateAdapter } = await getConnectedStateContext();
11522
- const key = queueIngressDedupKey(rawKey);
11523
- if (redisStateAdapter) {
11524
- const result = await redisStateAdapter.getClient().set(key, "1", {
11525
- NX: true,
11526
- PX: ttlMs
11527
- });
11528
- return result === "OK";
11529
- }
11530
- return await stateAdapter.setIfNotExists(key, "1", ttlMs);
11531
- }
11532
- async function hasQueueIngressDedup(rawKey) {
11533
- const { stateAdapter, redisStateAdapter } = await getConnectedStateContext();
11534
- const key = queueIngressDedupKey(rawKey);
11535
- const value = redisStateAdapter ? await redisStateAdapter.getClient().get(key) : await stateAdapter.get(key);
11536
- return typeof value === "string" && value.length > 0;
11537
- }
11538
-
11539
11379
  // src/chat/ingress/message-router.ts
11540
- var QUEUE_INGRESS_DEDUP_TTL_MS = 24 * 60 * 60 * 1e3;
11541
- function nonEmptyString(value) {
11542
- if (typeof value !== "string") return void 0;
11543
- const trimmed = value.trim();
11544
- return trimmed || void 0;
11545
- }
11546
- function serializeMessageForQueue(message) {
11547
- const candidate = message;
11548
- if (typeof candidate.toJSON === "function") {
11549
- return candidate.toJSON();
11550
- }
11551
- return {
11552
- _type: "chat:Message",
11553
- ...message
11554
- };
11555
- }
11556
- function serializeThreadForQueue(thread) {
11557
- const candidate = thread;
11558
- if (typeof candidate.toJSON === "function") {
11559
- return candidate.toJSON();
11560
- }
11561
- return {
11562
- _type: "chat:Thread",
11563
- ...thread
11564
- };
11565
- }
11566
11380
  function normalizeIncomingSlackThreadId(threadId, message) {
11567
11381
  if (!threadId.startsWith("slack:")) {
11568
11382
  return threadId;
@@ -11581,260 +11395,10 @@ function normalizeIncomingSlackThreadId(threadId, message) {
11581
11395
  }
11582
11396
  return `slack:${channelId}:${threadTs}`;
11583
11397
  }
11584
- function buildQueueIngressDedupKey(normalizedThreadId, messageId) {
11585
- return `${normalizedThreadId}:${messageId}`;
11586
- }
11587
- function isSlackDirectMessageThreadId(threadId) {
11588
- const parts = threadId.split(":");
11589
- return parts.length === 3 && parts[0] === "slack" && parts[1]?.startsWith("D");
11590
- }
11591
- function determineThreadMessageKind(args) {
11592
- if (args.isDirectMessage) {
11593
- return "new_mention";
11594
- }
11595
- if (args.isSubscribed) {
11596
- return "subscribed_message";
11597
- }
11598
- if (args.isMention) {
11599
- return "new_mention";
11600
- }
11601
- return void 0;
11602
- }
11603
- function getMessageLogContext(args) {
11604
- return {
11605
- slackThreadId: args.normalizedThreadId,
11606
- slackChannelId: nonEmptyString(args.message.raw?.channel),
11607
- slackUserId: args.message.author?.userId
11608
- };
11609
- }
11610
- function logIgnoredIngressResult(args) {
11611
- logInfo(
11612
- args.eventName,
11613
- args.logContext,
11614
- {
11615
- ...args.messageId ? { "messaging.message.id": args.messageId } : {},
11616
- ...args.kind ? { "app.queue.message_kind": args.kind } : {},
11617
- ...args.dedupKey ? { "app.queue.dedup_key": args.dedupKey } : {},
11618
- ...args.decisionReason ? { "app.decision.reason": args.decisionReason } : {},
11619
- "app.queue.route_result": args.routeResult
11620
- },
11621
- args.body
11622
- );
11623
- }
11624
- async function enqueueQueueIngressMessage(args) {
11625
- if (args.enqueueThreadMessage) {
11626
- return await args.enqueueThreadMessage(args.payload, args.dedupKey);
11627
- }
11628
- return await enqueueThreadMessage(args.payload, {
11629
- idempotencyKey: args.dedupKey
11630
- });
11631
- }
11632
- async function routeIncomingMessageToQueue(args) {
11633
- const { adapter, runtime } = args;
11634
- const message = args.message;
11635
- if (!message || typeof message !== "object") {
11636
- return "ignored_non_object";
11637
- }
11638
- const normalizedThreadId = normalizeIncomingSlackThreadId(
11639
- args.threadId,
11640
- message
11641
- );
11642
- const baseLogContext = getMessageLogContext({
11643
- message,
11644
- normalizedThreadId
11645
- });
11646
- if ("threadId" in message) {
11647
- message.threadId = normalizedThreadId;
11648
- }
11649
- const typedMessage = message;
11650
- if (typedMessage.author?.isMe) {
11651
- logIgnoredIngressResult({
11652
- eventName: "queue_ingress_ignored_self_message",
11653
- logContext: baseLogContext,
11654
- messageId: nonEmptyString(typedMessage.id),
11655
- routeResult: "ignored_self_message",
11656
- body: "Ignoring self-authored message before queue routing"
11657
- });
11658
- return "ignored_self_message";
11659
- }
11660
- const messageId = nonEmptyString(typedMessage.id);
11661
- if (!messageId) {
11662
- logIgnoredIngressResult({
11663
- eventName: "queue_ingress_ignored_missing_message_id",
11664
- logContext: baseLogContext,
11665
- routeResult: "ignored_missing_message_id",
11666
- body: "Ignoring message without an id before queue routing"
11667
- });
11668
- return "ignored_missing_message_id";
11669
- }
11670
- const isSubscribed = await getStateAdapter().isSubscribed(normalizedThreadId);
11671
- const mentionSource = typedMessage.isMention ? "sdk_flag" : runtime.detectMention?.(adapter, message) ? "fallback_detector" : void 0;
11672
- const isMention = mentionSource !== void 0;
11673
- if (isMention && !typedMessage.isMention) {
11674
- typedMessage.isMention = true;
11675
- }
11676
- const isDirectMessage = isSlackDirectMessageThreadId(normalizedThreadId);
11677
- const kind = determineThreadMessageKind({
11678
- isDirectMessage,
11679
- isSubscribed,
11680
- isMention
11681
- });
11682
- if (!kind) {
11683
- logIgnoredIngressResult({
11684
- eventName: "queue_ingress_ignored_unsubscribed_non_mention",
11685
- logContext: baseLogContext,
11686
- messageId,
11687
- routeResult: "ignored_unsubscribed_non_mention",
11688
- body: "Ignoring unsubscribed non-mention message before queue routing"
11689
- });
11690
- return "ignored_unsubscribed_non_mention";
11691
- }
11692
- const dedupKey = buildQueueIngressDedupKey(normalizedThreadId, messageId);
11693
- const alreadyDeduped = await hasQueueIngressDedup(dedupKey);
11694
- if (alreadyDeduped) {
11695
- logInfo(
11696
- "queue_ingress_dedup_hit",
11697
- baseLogContext,
11698
- {
11699
- "messaging.message.id": messageId,
11700
- "app.queue.message_kind": kind,
11701
- "app.queue.dedup_key": dedupKey,
11702
- "app.queue.dedup_outcome": "duplicate",
11703
- ...mentionSource ? { "app.slack.mention_source": mentionSource } : {},
11704
- "app.queue.route_result": "ignored_duplicate"
11705
- },
11706
- "Skipping duplicate incoming message before queue enqueue"
11707
- );
11708
- return "ignored_duplicate";
11709
- }
11710
- const thread = await runtime.createThread(
11711
- adapter,
11712
- normalizedThreadId,
11713
- message,
11714
- isSubscribed
11715
- );
11716
- const serializedMessage = serializeMessageForQueue(message);
11717
- const serializedThread = serializeThreadForQueue(thread);
11718
- const payload = {
11719
- dedupKey,
11720
- kind,
11721
- message: serializedMessage,
11722
- normalizedThreadId,
11723
- thread: serializedThread
11724
- };
11725
- await withContext(
11726
- {
11727
- slackThreadId: normalizedThreadId,
11728
- slackChannelId: thread.channelId,
11729
- slackUserId: message.author.userId
11730
- },
11731
- async () => {
11732
- let processingReactionAdded = false;
11733
- let queueMessageId;
11734
- try {
11735
- await addReactionToMessage({
11736
- channelId: thread.channelId,
11737
- timestamp: messageId,
11738
- emoji: "eyes"
11739
- });
11740
- processingReactionAdded = true;
11741
- } catch (error) {
11742
- const errorMessage = error instanceof Error ? error.message : String(error);
11743
- logWarn(
11744
- "queue_ingress_reaction_add_failed",
11745
- {},
11746
- {
11747
- "messaging.message.id": messageId,
11748
- "app.queue.message_kind": kind,
11749
- ...mentionSource ? { "app.slack.mention_source": mentionSource } : {},
11750
- "error.message": errorMessage
11751
- },
11752
- "Failed to add ingress processing reaction"
11753
- );
11754
- }
11755
- try {
11756
- await withSpan(
11757
- "queue.enqueue_message",
11758
- "queue.enqueue_message",
11759
- {
11760
- slackThreadId: normalizedThreadId,
11761
- slackChannelId: thread.channelId,
11762
- slackUserId: message.author.userId
11763
- },
11764
- async () => {
11765
- queueMessageId = await enqueueQueueIngressMessage({
11766
- dedupKey,
11767
- enqueueThreadMessage: args.enqueueThreadMessage,
11768
- payload
11769
- });
11770
- if (queueMessageId) {
11771
- setSpanAttributes({
11772
- "app.queue.message_id": queueMessageId
11773
- });
11774
- }
11775
- },
11776
- {
11777
- "messaging.message.id": messageId,
11778
- "app.queue.message_kind": kind
11779
- }
11780
- );
11781
- } catch (error) {
11782
- if (processingReactionAdded) {
11783
- try {
11784
- await removeReactionFromMessage({
11785
- channelId: thread.channelId,
11786
- timestamp: messageId,
11787
- emoji: "eyes"
11788
- });
11789
- } catch (cleanupError) {
11790
- const cleanupErrorMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
11791
- logWarn(
11792
- "queue_ingress_reaction_cleanup_failed",
11793
- {},
11794
- {
11795
- "messaging.message.id": messageId,
11796
- "app.queue.message_kind": kind,
11797
- "error.message": cleanupErrorMessage
11798
- },
11799
- "Failed to remove ingress processing reaction after enqueue failure"
11800
- );
11801
- }
11802
- }
11803
- throw error;
11804
- }
11805
- logInfo(
11806
- "queue_ingress_enqueued",
11807
- {},
11808
- {
11809
- "messaging.message.id": messageId,
11810
- "app.queue.message_kind": kind,
11811
- ...mentionSource ? { "app.slack.mention_source": mentionSource } : {},
11812
- "app.queue.dedup_key": dedupKey,
11813
- "app.queue.dedup_outcome": "primary",
11814
- "app.queue.route_result": "routed",
11815
- ...queueMessageId ? { "app.queue.message_id": queueMessageId } : {}
11816
- },
11817
- "Routing incoming message to queue"
11818
- );
11819
- const marked = await claimQueueIngressDedup(
11820
- dedupKey,
11821
- QUEUE_INGRESS_DEDUP_TTL_MS
11822
- );
11823
- if (!marked) {
11824
- logInfo(
11825
- "queue_ingress_dedup_mark_failed",
11826
- {},
11827
- {
11828
- "messaging.message.id": messageId,
11829
- "app.queue.message_kind": kind,
11830
- "app.queue.dedup_key": dedupKey
11831
- },
11832
- "Queue ingress dedup state write failed after enqueue"
11833
- );
11834
- }
11835
- }
11836
- );
11837
- return "routed";
11398
+ function nonEmptyString(value) {
11399
+ if (typeof value !== "string") return void 0;
11400
+ const trimmed = value.trim();
11401
+ return trimmed || void 0;
11838
11402
  }
11839
11403
 
11840
11404
  // src/chat/ingress/junior-chat.ts
@@ -11845,42 +11409,62 @@ function enqueueBackgroundTask(options, task) {
11845
11409
  options.waitUntil(task);
11846
11410
  }
11847
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
+ */
11848
11434
  processMessage(adapter, threadId, messageOrFactory, options) {
11849
- const runtime = this;
11850
- enqueueBackgroundTask(
11851
- options,
11852
- (async () => {
11853
- try {
11854
- const message = typeof messageOrFactory === "function" ? await messageOrFactory() : messageOrFactory;
11855
- const result = await routeIncomingMessageToQueue({
11856
- adapter,
11857
- threadId,
11858
- message,
11859
- runtime: {
11860
- createThread: runtime.createThread.bind(
11861
- this
11862
- ),
11863
- detectMention: runtime.detectMention?.bind(this)
11864
- }
11865
- });
11866
- if (result === "ignored_missing_message_id") {
11867
- 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(
11868
11443
  threadId,
11869
11444
  message
11870
11445
  );
11871
- runtime.logger?.error?.("Message processing error", {
11872
- threadId: normalizedThreadId,
11873
- 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
11874
11454
  });
11875
11455
  }
11876
- } catch (error) {
11877
- runtime.logger?.error?.("Message processing error", {
11878
- error,
11879
- threadId
11880
- });
11881
- }
11882
- })()
11456
+ })()
11457
+ );
11458
+ return;
11459
+ }
11460
+ const normalized = normalizeIncomingSlackThreadId(
11461
+ threadId,
11462
+ messageOrFactory
11883
11463
  );
11464
+ if (normalized !== threadId && "threadId" in messageOrFactory) {
11465
+ messageOrFactory.threadId = normalized;
11466
+ }
11467
+ super.processMessage(adapter, normalized, messageOrFactory, options);
11884
11468
  }
11885
11469
  processReaction(event, options) {
11886
11470
  const runtime = this;
@@ -12161,6 +11745,15 @@ async function publishAppHomeView(slackClient, userId, userTokenStore) {
12161
11745
  await slackClient.views.publish({ user_id: userId, view });
12162
11746
  }
12163
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
+
12164
11757
  // src/chat/ingress/slash-command.ts
12165
11758
  async function postEphemeral(event, text) {
12166
11759
  await event.channel.postEphemeral(event.user, text, { fallbackToDM: false });
@@ -12245,112 +11838,236 @@ async function handleSlashCommand(event) {
12245
11838
  }
12246
11839
 
12247
11840
  // src/chat/app/production.ts
12248
- var bot = new JuniorChat({
12249
- userName: botConfig.userName,
12250
- adapters: {
12251
- slack: (() => {
12252
- const signingSecret = getSlackSigningSecret();
12253
- const botToken = getSlackBotToken();
12254
- const clientId = getSlackClientId();
12255
- const clientSecret = getSlackClientSecret();
12256
- if (!signingSecret) {
12257
- throw new Error("SLACK_SIGNING_SECRET is required");
12258
- }
12259
- return createSlackAdapter({
12260
- signingSecret,
12261
- ...botToken ? { botToken } : {},
12262
- ...clientId ? { clientId } : {},
12263
- ...clientSecret ? { clientSecret } : {}
12264
- });
12265
- })()
12266
- },
12267
- state: getStateAdapter()
12268
- });
12269
- var registerSingleton = bot.registerSingleton;
12270
- if (typeof registerSingleton === "function") {
12271
- registerSingleton.call(bot);
11841
+ var productionBot;
11842
+ var productionSlackRuntime;
11843
+ function createProductionBot() {
11844
+ return new JuniorChat({
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
+ },
11855
+ adapters: {
11856
+ slack: (() => {
11857
+ const signingSecret = getSlackSigningSecret();
11858
+ const botToken = getSlackBotToken();
11859
+ const clientId = getSlackClientId();
11860
+ const clientSecret = getSlackClientSecret();
11861
+ if (!signingSecret) {
11862
+ throw new Error("SLACK_SIGNING_SECRET is required");
11863
+ }
11864
+ return createSlackAdapter({
11865
+ signingSecret,
11866
+ ...botToken ? { botToken } : {},
11867
+ ...clientId ? { clientId } : {},
11868
+ ...clientSecret ? { clientSecret } : {}
11869
+ });
11870
+ })()
11871
+ },
11872
+ state: getStateAdapter()
11873
+ });
12272
11874
  }
12273
- function getSlackAdapter() {
12274
- return bot.getAdapter("slack");
11875
+ function rehydrateAttachments(message) {
11876
+ rehydrateAttachmentFetchers(message);
12275
11877
  }
12276
- var slackRuntime = createSlackRuntime({
12277
- getSlackAdapter
12278
- });
12279
- bot.onNewMention(slackRuntime.handleNewMention);
12280
- bot.onSubscribedMessage(slackRuntime.handleSubscribedMessage);
12281
- bot.onAssistantThreadStarted(
12282
- (event) => slackRuntime.handleAssistantThreadStarted(event)
12283
- );
12284
- bot.onAssistantContextChanged(
12285
- (event) => slackRuntime.handleAssistantContextChanged(event)
12286
- );
12287
- bot.onSlashCommand(
12288
- "/jr",
12289
- (event) => withSpan(
12290
- "chat.slash_command",
12291
- "chat.slash_command",
12292
- { slackUserId: event.user.userId },
12293
- async () => {
12294
- try {
12295
- await handleSlashCommand(event);
12296
- } catch (error) {
12297
- logException(error, "slash_command_failed", {
12298
- slackUserId: event.user.userId
12299
- });
12300
- throw error;
11878
+ function registerProductionHandlers(bot, slackRuntime) {
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
+ });
11891
+ bot.onAssistantThreadStarted(
11892
+ (event) => slackRuntime.handleAssistantThreadStarted(event)
11893
+ );
11894
+ bot.onAssistantContextChanged(
11895
+ (event) => slackRuntime.handleAssistantContextChanged(event)
11896
+ );
11897
+ bot.onSlashCommand(
11898
+ "/jr",
11899
+ (event) => withSpan(
11900
+ "chat.slash_command",
11901
+ "chat.slash_command",
11902
+ { slackUserId: event.user.userId },
11903
+ async () => {
11904
+ try {
11905
+ await handleSlashCommand(event);
11906
+ } catch (error) {
11907
+ logException(error, "slash_command_failed", {
11908
+ slackUserId: event.user.userId
11909
+ });
11910
+ throw error;
11911
+ }
12301
11912
  }
12302
- }
12303
- )
12304
- );
12305
- bot.onAppHomeOpened(
12306
- (event) => withSpan(
12307
- "chat.app_home_opened",
12308
- "chat.app_home_opened",
12309
- { slackUserId: event.userId },
12310
- async () => {
12311
- try {
12312
- await publishAppHomeView(
12313
- getSlackClient(),
12314
- event.userId,
12315
- createUserTokenStore()
12316
- );
12317
- } catch (error) {
12318
- logException(error, "app_home_opened_failed", {
12319
- slackUserId: event.userId
12320
- });
11913
+ )
11914
+ );
11915
+ bot.onAppHomeOpened(
11916
+ (event) => withSpan(
11917
+ "chat.app_home_opened",
11918
+ "chat.app_home_opened",
11919
+ { slackUserId: event.userId },
11920
+ async () => {
11921
+ try {
11922
+ await publishAppHomeView(
11923
+ getSlackClient(),
11924
+ event.userId,
11925
+ createUserTokenStore()
11926
+ );
11927
+ } catch (error) {
11928
+ logException(error, "app_home_opened_failed", {
11929
+ slackUserId: event.userId
11930
+ });
11931
+ }
12321
11932
  }
11933
+ )
11934
+ );
11935
+ bot.onAction("app_home_disconnect", async (event) => {
11936
+ const provider = event.value;
11937
+ if (!provider) return;
11938
+ const userId = event.user.userId;
11939
+ await withSpan(
11940
+ "chat.app_home_disconnect",
11941
+ "chat.app_home_disconnect",
11942
+ { slackUserId: userId },
11943
+ async () => {
11944
+ try {
11945
+ await unlinkProvider(userId, provider, createUserTokenStore());
11946
+ await publishAppHomeView(
11947
+ getSlackClient(),
11948
+ userId,
11949
+ createUserTokenStore()
11950
+ );
11951
+ } catch (error) {
11952
+ logException(
11953
+ error,
11954
+ "app_home_disconnect_failed",
11955
+ { slackUserId: userId },
11956
+ {
11957
+ "app.credential.provider": provider
11958
+ }
11959
+ );
11960
+ }
11961
+ }
11962
+ );
11963
+ });
11964
+ }
11965
+ function initializeProductionApp() {
11966
+ if (productionBot && productionSlackRuntime) {
11967
+ return;
11968
+ }
11969
+ const bot = createProductionBot();
11970
+ const registerSingleton = bot.registerSingleton;
11971
+ if (typeof registerSingleton === "function") {
11972
+ registerSingleton.call(bot);
11973
+ }
11974
+ const slackRuntime = createSlackRuntime({
11975
+ getSlackAdapter: () => bot.getAdapter("slack")
11976
+ });
11977
+ registerProductionHandlers(bot, slackRuntime);
11978
+ productionBot = bot;
11979
+ productionSlackRuntime = slackRuntime;
11980
+ }
11981
+ function getProductionBot() {
11982
+ initializeProductionApp();
11983
+ return productionBot;
11984
+ }
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 });
12322
12007
  }
12323
- )
12324
- );
12325
- bot.onAction("app_home_disconnect", async (event) => {
12326
- const provider = event.value;
12327
- if (!provider) return;
12328
- const userId = event.user.userId;
12329
- await withSpan(
12330
- "chat.app_home_disconnect",
12331
- "chat.app_home_disconnect",
12332
- { slackUserId: userId },
12333
- async () => {
12334
- try {
12335
- await unlinkProvider(userId, provider, createUserTokenStore());
12336
- await publishAppHomeView(
12337
- getSlackClient(),
12338
- userId,
12339
- createUserTokenStore()
12340
- );
12341
- } catch (error) {
12342
- logException(
12343
- error,
12344
- "app_home_disconnect_failed",
12345
- { slackUserId: userId },
12346
- {
12347
- "app.credential.provider": provider
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;
12348
12058
  }
12349
- );
12350
- }
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;
12351
12068
  }
12352
- );
12353
- });
12069
+ });
12070
+ }
12354
12071
 
12355
12072
  export {
12356
12073
  coerceThreadConversationState,
@@ -12358,13 +12075,13 @@ export {
12358
12075
  buildSlackOutputMessage,
12359
12076
  getSlackClient,
12360
12077
  uploadFilesToThread,
12361
- downloadPrivateSlackFile,
12362
12078
  formatProviderLabel,
12363
12079
  resolveBaseUrl,
12364
12080
  finalizeMcpAuthorization,
12365
12081
  coerceThreadArtifactsState,
12366
12082
  mergeArtifactsState,
12367
- persistThreadState,
12083
+ getPersistedThreadState,
12084
+ persistThreadStateById,
12368
12085
  generateConversationId,
12369
12086
  normalizeConversationText,
12370
12087
  updateConversationStats,
@@ -12373,19 +12090,13 @@ export {
12373
12090
  buildConversationContext,
12374
12091
  escapeXml,
12375
12092
  createUserTokenStore,
12376
- removeReactionFromMessage,
12377
12093
  truncateStatusText,
12378
- buildDeterministicTurnId,
12379
12094
  isRetryableTurnError,
12380
12095
  markTurnCompleted,
12381
12096
  markTurnFailed,
12382
12097
  resolveReplyDelivery,
12383
12098
  generateAssistantReply,
12384
12099
  publishAppHomeView,
12385
- createNormalizingStream,
12386
- DeferredThreadMessageError,
12387
- getThreadMessageTopic,
12388
- createQueueCallbackHandler,
12389
- bot,
12390
- slackRuntime
12100
+ maxDuration,
12101
+ POST
12391
12102
  };