@sentry/junior 0.22.0 → 0.23.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.
Files changed (2) hide show
  1. package/dist/app.js +829 -296
  2. package/package.json +1 -1
package/dist/app.js CHANGED
@@ -311,9 +311,6 @@ async function GET3() {
311
311
  });
312
312
  }
313
313
 
314
- // src/handlers/mcp-oauth-callback.ts
315
- import { Buffer as Buffer2 } from "buffer";
316
-
317
314
  // src/chat/state/conversation.ts
318
315
  function coerceRole(value) {
319
316
  return value === "assistant" || value === "system" || value === "user" ? value : "user";
@@ -729,81 +726,6 @@ async function deleteMcpServerSessionId(userId, provider) {
729
726
  await stateAdapter.delete(serverSessionKey(userId, provider));
730
727
  }
731
728
 
732
- // src/chat/slack/output.ts
733
- var MAX_INLINE_CHARS = 2200;
734
- var MAX_INLINE_LINES = 45;
735
- function ensureBlockSpacing(text) {
736
- const codeBlockPattern = /^```/;
737
- const listItemPattern = /^[-*•]\s|^\d+\.\s/;
738
- const lines = text.split("\n");
739
- const result = [];
740
- let inCodeBlock = false;
741
- for (let i = 0; i < lines.length; i++) {
742
- const line = lines[i];
743
- const isCodeFence = codeBlockPattern.test(line.trimStart());
744
- if (isCodeFence) {
745
- if (!inCodeBlock) {
746
- const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
747
- if (prev2 !== void 0 && prev2.trim() !== "") {
748
- result.push("");
749
- }
750
- }
751
- inCodeBlock = !inCodeBlock;
752
- result.push(line);
753
- continue;
754
- }
755
- if (inCodeBlock) {
756
- result.push(line);
757
- continue;
758
- }
759
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
760
- if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
761
- result.push("");
762
- }
763
- result.push(line);
764
- }
765
- return result.join("\n");
766
- }
767
- function normalizeForSlack(text) {
768
- let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
769
- normalized = ensureBlockSpacing(normalized);
770
- return normalized.replace(/\n{3,}/g, "\n\n").trim();
771
- }
772
- function buildSlackOutputMessage(text, files) {
773
- const normalized = normalizeForSlack(text);
774
- const fileCount = files?.length ?? 0;
775
- if (!normalized) {
776
- if (fileCount > 0) {
777
- return {
778
- raw: "",
779
- files
780
- };
781
- }
782
- logWarn(
783
- "slack_output_normalized_empty",
784
- {},
785
- {
786
- "app.output.original_length": text.length,
787
- "app.output.parsed_length": normalized.length,
788
- "app.output.file_count": fileCount
789
- },
790
- "Slack output normalized to empty content"
791
- );
792
- return {
793
- markdown: "I couldn't produce a response.",
794
- files
795
- };
796
- }
797
- return {
798
- markdown: normalized,
799
- files
800
- };
801
- }
802
- var slackOutputPolicy = {
803
- maxInlineChars: MAX_INLINE_CHARS,
804
- maxInlineLines: MAX_INLINE_LINES
805
- };
806
-
807
729
  // src/chat/mcp/oauth.ts
808
730
  import { randomUUID } from "crypto";
809
731
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -2176,24 +2098,6 @@ function markTurnFailed(args) {
2176
2098
  });
2177
2099
  args.updateConversationStats(args.conversation);
2178
2100
  }
2179
- function resolveReplyDelivery(args) {
2180
- const replyHasFiles = Boolean(
2181
- args.reply.files && args.reply.files.length > 0
2182
- );
2183
- const deliveryPlan = args.reply.deliveryPlan ?? {
2184
- mode: args.reply.deliveryMode ?? "thread",
2185
- postThreadText: (args.reply.deliveryMode ?? "thread") !== "channel_only",
2186
- attachFiles: replyHasFiles ? args.hasStreamedThreadReply ? "followup" : "inline" : "none"
2187
- };
2188
- let attachFiles = replyHasFiles ? deliveryPlan.attachFiles : "none";
2189
- if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
2190
- attachFiles = "inline";
2191
- }
2192
- return {
2193
- shouldPostThreadReply: deliveryPlan.postThreadText,
2194
- attachFiles
2195
- };
2196
- }
2197
2101
 
2198
2102
  // src/chat/runtime/turn-user-message.ts
2199
2103
  function getTurnUserMessage(conversation, sessionId) {
@@ -2799,6 +2703,276 @@ import { Agent } from "@mariozechner/pi-agent-core";
2799
2703
  // src/chat/prompt.ts
2800
2704
  import fs from "fs";
2801
2705
  import path2 from "path";
2706
+
2707
+ // src/chat/slack/output.ts
2708
+ var MAX_INLINE_CHARS = 2200;
2709
+ var MAX_INLINE_LINES = 45;
2710
+ var CONTINUED_MARKER = "\n\n[Continued below]";
2711
+ var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
2712
+ var STREAMING_FENCE_CLOSE_GUARD = "\n```";
2713
+ function ensureBlockSpacing(text) {
2714
+ const codeBlockPattern = /^```/;
2715
+ const listItemPattern = /^[-*•]\s|^\d+\.\s/;
2716
+ const lines = text.split("\n");
2717
+ const result = [];
2718
+ let inCodeBlock = false;
2719
+ for (let i = 0; i < lines.length; i++) {
2720
+ const line = lines[i];
2721
+ const isCodeFence = codeBlockPattern.test(line.trimStart());
2722
+ if (isCodeFence) {
2723
+ if (!inCodeBlock) {
2724
+ const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
2725
+ if (prev2 !== void 0 && prev2.trim() !== "") {
2726
+ result.push("");
2727
+ }
2728
+ }
2729
+ inCodeBlock = !inCodeBlock;
2730
+ result.push(line);
2731
+ continue;
2732
+ }
2733
+ if (inCodeBlock) {
2734
+ result.push(line);
2735
+ continue;
2736
+ }
2737
+ const prev = result.length > 0 ? result[result.length - 1] : void 0;
2738
+ if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
2739
+ result.push("");
2740
+ }
2741
+ result.push(line);
2742
+ }
2743
+ return result.join("\n");
2744
+ }
2745
+ function normalizeForSlack(text) {
2746
+ let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
2747
+ normalized = ensureBlockSpacing(normalized);
2748
+ return normalized.replace(/\n{3,}/g, "\n\n").trim();
2749
+ }
2750
+ function countSlackLines(text) {
2751
+ if (!text) {
2752
+ return 0;
2753
+ }
2754
+ return text.split("\n").length;
2755
+ }
2756
+ function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
2757
+ return text.length <= maxChars && countSlackLines(text) <= maxLines;
2758
+ }
2759
+ function findSplitIndex(text, maxChars) {
2760
+ if (text.length <= maxChars) {
2761
+ return text.length;
2762
+ }
2763
+ const bounded = text.slice(0, maxChars);
2764
+ const newlineIndex = bounded.lastIndexOf("\n");
2765
+ if (newlineIndex > 0) {
2766
+ return newlineIndex;
2767
+ }
2768
+ const spaceIndex = bounded.lastIndexOf(" ");
2769
+ if (spaceIndex > 0) {
2770
+ return spaceIndex;
2771
+ }
2772
+ return maxChars;
2773
+ }
2774
+ function splitByLineBudget(text, maxLines) {
2775
+ if (maxLines <= 0) {
2776
+ return "";
2777
+ }
2778
+ const lines = text.split("\n");
2779
+ if (lines.length <= maxLines) {
2780
+ return text;
2781
+ }
2782
+ return lines.slice(0, maxLines).join("\n");
2783
+ }
2784
+ function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
2785
+ return {
2786
+ maxChars: Math.max(1, maxChars - suffix.length),
2787
+ maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
2788
+ };
2789
+ }
2790
+ function forceSplitBudget(text, budget) {
2791
+ const lineCount = countSlackLines(text);
2792
+ return {
2793
+ maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
2794
+ maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
2795
+ };
2796
+ }
2797
+ function getFenceContinuation(text) {
2798
+ let open;
2799
+ for (const line of text.split("\n")) {
2800
+ const trimmed = line.trimStart();
2801
+ const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
2802
+ if (!openerMatch) {
2803
+ continue;
2804
+ }
2805
+ if (!open) {
2806
+ open = {
2807
+ fence: openerMatch[1],
2808
+ openerLine: trimmed
2809
+ };
2810
+ continue;
2811
+ }
2812
+ if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
2813
+ open = void 0;
2814
+ }
2815
+ }
2816
+ if (!open) {
2817
+ return null;
2818
+ }
2819
+ return {
2820
+ closeSuffix: text.endsWith("\n") ? open.fence : `
2821
+ ${open.fence}`,
2822
+ reopenPrefix: `${open.openerLine}
2823
+ `
2824
+ };
2825
+ }
2826
+ function appendSlackSuffix(text, marker) {
2827
+ const carryover = getFenceContinuation(text);
2828
+ return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
2829
+ }
2830
+ function takeSlackContinuationChunk(text, budget) {
2831
+ let { prefix, rest } = takeSlackInlinePrefix(text, budget);
2832
+ if (!rest) {
2833
+ ({ prefix, rest } = takeSlackInlinePrefix(
2834
+ text,
2835
+ forceSplitBudget(text, budget)
2836
+ ));
2837
+ }
2838
+ let carryover = rest ? getFenceContinuation(prefix) : null;
2839
+ if (!carryover) {
2840
+ return { prefix, rest };
2841
+ }
2842
+ const carryoverBudget = reserveInlineBudgetForSuffix(
2843
+ `${carryover.closeSuffix}${CONTINUED_MARKER}`
2844
+ );
2845
+ ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
2846
+ if (!rest) {
2847
+ ({ prefix, rest } = takeSlackInlinePrefix(
2848
+ text,
2849
+ forceSplitBudget(text, carryoverBudget)
2850
+ ));
2851
+ }
2852
+ carryover = rest ? getFenceContinuation(prefix) : null;
2853
+ if (!carryover) {
2854
+ return { prefix, rest };
2855
+ }
2856
+ return {
2857
+ prefix,
2858
+ rest: `${carryover.reopenPrefix}${rest}`
2859
+ };
2860
+ }
2861
+ function takeSlackContinuationPrefix(text, options) {
2862
+ const budget = {
2863
+ maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
2864
+ maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
2865
+ };
2866
+ const { prefix, rest } = (() => {
2867
+ if (options?.forceSplit) {
2868
+ return takeSlackContinuationChunk(text, budget);
2869
+ }
2870
+ const initial = takeSlackInlinePrefix(text, budget);
2871
+ return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
2872
+ })();
2873
+ return {
2874
+ prefix,
2875
+ renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
2876
+ rest
2877
+ };
2878
+ }
2879
+ function takeSlackInlinePrefix(text, options) {
2880
+ const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
2881
+ const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
2882
+ const normalized = text.replace(/\r\n?/g, "\n");
2883
+ if (!normalized) {
2884
+ return { prefix: "", rest: "" };
2885
+ }
2886
+ if (fitsInlineBudget(normalized, maxChars, maxLines)) {
2887
+ return { prefix: normalized, rest: "" };
2888
+ }
2889
+ const lineBounded = splitByLineBudget(normalized, maxLines);
2890
+ const cutIndex = findSplitIndex(lineBounded, maxChars);
2891
+ const prefix = lineBounded.slice(0, cutIndex).trimEnd();
2892
+ if (prefix) {
2893
+ return {
2894
+ prefix,
2895
+ rest: normalized.slice(prefix.length).trimStart()
2896
+ };
2897
+ }
2898
+ const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
2899
+ return {
2900
+ prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
2901
+ rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
2902
+ };
2903
+ }
2904
+ function splitSlackReplyText(text, options) {
2905
+ const normalized = normalizeForSlack(text);
2906
+ if (!normalized) {
2907
+ return [];
2908
+ }
2909
+ const chunks = [];
2910
+ const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
2911
+ let remaining = normalized;
2912
+ while (remaining) {
2913
+ const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, INTERRUPTED_MARKER)) : fitsInlineBudget(remaining);
2914
+ if (fitsFinalChunk) {
2915
+ chunks.push(
2916
+ options?.interrupted ? appendSlackSuffix(remaining, INTERRUPTED_MARKER) : remaining
2917
+ );
2918
+ break;
2919
+ }
2920
+ const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
2921
+ ...continuationBudget,
2922
+ forceSplit: true
2923
+ });
2924
+ chunks.push(renderedPrefix);
2925
+ remaining = rest;
2926
+ }
2927
+ return chunks;
2928
+ }
2929
+ function getSlackInterruptionMarker() {
2930
+ return INTERRUPTED_MARKER;
2931
+ }
2932
+ function getSlackContinuationBudget() {
2933
+ return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
2934
+ }
2935
+ function getSlackStreamingContinuationBudget() {
2936
+ return reserveInlineBudgetForSuffix(
2937
+ `${STREAMING_FENCE_CLOSE_GUARD}${CONTINUED_MARKER}`
2938
+ );
2939
+ }
2940
+ function buildSlackOutputMessage(text, files) {
2941
+ const normalized = normalizeForSlack(text);
2942
+ const fileCount = files?.length ?? 0;
2943
+ if (!normalized) {
2944
+ if (fileCount > 0) {
2945
+ return {
2946
+ raw: "",
2947
+ files
2948
+ };
2949
+ }
2950
+ logWarn(
2951
+ "slack_output_normalized_empty",
2952
+ {},
2953
+ {
2954
+ "app.output.original_length": text.length,
2955
+ "app.output.parsed_length": normalized.length,
2956
+ "app.output.file_count": fileCount
2957
+ },
2958
+ "Slack output normalized to empty content"
2959
+ );
2960
+ return {
2961
+ markdown: "I couldn't produce a response.",
2962
+ files
2963
+ };
2964
+ }
2965
+ return {
2966
+ markdown: normalized,
2967
+ files
2968
+ };
2969
+ }
2970
+ var slackOutputPolicy = {
2971
+ maxInlineChars: MAX_INLINE_CHARS,
2972
+ maxInlineLines: MAX_INLINE_LINES
2973
+ };
2974
+
2975
+ // src/chat/prompt.ts
2802
2976
  var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
2803
2977
  function getLoggedMarkdownFiles() {
2804
2978
  const globalState = globalThis;
@@ -3169,6 +3343,9 @@ function buildSystemPrompt(params) {
3169
3343
  "- 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.",
3170
3344
  "- If a routine prerequisite check finds nothing notable, omit it entirely from the final reply and report only the user-relevant outcome.",
3171
3345
  "- 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.",
3346
+ "- For external/factual research requests that require tools, do not send any preliminary conclusion, 'let me check', or progress narration before the evidence-gathering work is done. Use assistant status for in-progress work and make the first visible reply the researched answer.",
3347
+ "- For evidence-gathering tasks, never state a factual conclusion before you have actually gathered and checked the sources.",
3348
+ "- Do not include internal process chatter such as 'let me find', 'fetching now', 'good, I have sources', 'trying smaller limits', or 'I now have sufficient context' in the final user-facing reply.",
3172
3349
  "- Use `attachFile` for files that actually exist in the sandbox (for example screenshots, PDFs, logs), or for `attachment_path` values returned by `imageGenerate`.",
3173
3350
  "- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
3174
3351
  "- Never claim a screenshot/file is attached unless `attachFile` succeeded in this turn.",
@@ -3227,7 +3404,8 @@ function buildSystemPrompt(params) {
3227
3404
  "- Use plain Slack-safe markdown (headings, bullets, short code blocks).",
3228
3405
  "- Keep normal responses brief and scannable.",
3229
3406
  "- If depth is needed, start with a concise summary and then provide fuller detail.",
3230
- "- A brief initial acknowledgment before significant tool work is fine; avoid extended process chatter or repeated status updates.",
3407
+ "- For tool-heavy research, discovery, or source-checking requests, do not send an initial acknowledgment. Start the visible reply only once you can present the actual answer.",
3408
+ "- Do not narrate tool execution or repeated status updates in the visible reply.",
3231
3409
  "- Avoid tables unless explicitly requested.",
3232
3410
  "- End every turn with a final user-facing markdown response.",
3233
3411
  "</output>"
@@ -8847,13 +9025,13 @@ function buildToolStatus(toolName, input) {
8847
9025
  return makeAssistantStatus("loading", skillName);
8848
9026
  }
8849
9027
  if (query && toolName === "webSearch") {
8850
- return makeAssistantStatus("searching", `"${query}"`);
8851
- }
8852
- if (query && provider && toolName === "searchTools") {
8853
- return makeAssistantStatus("searching", `${provider} "${query}"`);
9028
+ return makeAssistantStatus("searching", "sources");
8854
9029
  }
8855
9030
  if (query && toolName === "searchTools") {
8856
- return makeAssistantStatus("searching", `"${query}"`);
9031
+ return makeAssistantStatus(
9032
+ "searching",
9033
+ provider ? `${provider} tools` : "tools"
9034
+ );
8857
9035
  }
8858
9036
  if (domain && toolName === "webFetch") {
8859
9037
  return makeAssistantStatus("fetching", domain);
@@ -9157,13 +9335,34 @@ function buildReplyDeliveryPlan(args) {
9157
9335
  attachFiles
9158
9336
  };
9159
9337
  }
9160
-
9161
- // src/chat/services/channel-intent.ts
9162
- function isExplicitChannelPostIntent(text) {
9163
- if (!/\bchannel\b/i.test(text)) {
9164
- return false;
9165
- }
9166
- const directChannelVerb = /\b(show|post|send|share|say|announce|broadcast)\b[\s\S]{0,80}\b(?:the\s+)?channel\b/i;
9338
+ function resolveReplyDelivery(args) {
9339
+ const replyHasFiles = Boolean(
9340
+ args.reply.files && args.reply.files.length > 0
9341
+ );
9342
+ const deliveryPlan = args.reply.deliveryPlan ?? {
9343
+ mode: args.reply.deliveryMode ?? "thread",
9344
+ postThreadText: (args.reply.deliveryMode ?? "thread") !== "channel_only",
9345
+ attachFiles: replyHasFiles ? args.hasStreamedThreadReply ? "followup" : "inline" : "none"
9346
+ };
9347
+ let attachFiles = replyHasFiles ? deliveryPlan.attachFiles : "none";
9348
+ if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
9349
+ attachFiles = "inline";
9350
+ }
9351
+ if (attachFiles === "inline" && args.hasStreamedThreadReply) {
9352
+ attachFiles = "followup";
9353
+ }
9354
+ return {
9355
+ shouldPostThreadReply: deliveryPlan.postThreadText,
9356
+ attachFiles
9357
+ };
9358
+ }
9359
+
9360
+ // src/chat/services/channel-intent.ts
9361
+ function isExplicitChannelPostIntent(text) {
9362
+ if (!/\bchannel\b/i.test(text)) {
9363
+ return false;
9364
+ }
9365
+ const directChannelVerb = /\b(show|post|send|share|say|announce|broadcast)\b[\s\S]{0,80}\b(?:the\s+)?channel\b/i;
9167
9366
  if (directChannelVerb.test(text)) {
9168
9367
  return true;
9169
9368
  }
@@ -9954,6 +10153,17 @@ async function generateAssistantReply(messageText, context = {}) {
9954
10153
  const agentToolHooks = {
9955
10154
  onToolCall: (toolName) => {
9956
10155
  toolCalls.push(toolName);
10156
+ Promise.resolve(context.onToolCall?.(toolName)).catch((error) => {
10157
+ logWarn(
10158
+ "streaming_tool_call_error",
10159
+ {},
10160
+ {
10161
+ "error.message": error instanceof Error ? error.message : String(error),
10162
+ "gen_ai.tool.name": toolName
10163
+ },
10164
+ "Failed to deliver tool call event to stream coordinator"
10165
+ );
10166
+ });
9957
10167
  }
9958
10168
  };
9959
10169
  const baseAgentTools = createAgentTools(
@@ -9994,6 +10204,16 @@ async function generateAssistantReply(messageText, context = {}) {
9994
10204
  let needsSeparator = false;
9995
10205
  const unsubscribe = agent.subscribe((event) => {
9996
10206
  if (event.type === "message_start") {
10207
+ Promise.resolve(context.onAssistantMessageStart?.()).catch((error) => {
10208
+ logWarn(
10209
+ "streaming_message_start_error",
10210
+ {},
10211
+ {
10212
+ "error.message": error instanceof Error ? error.message : String(error)
10213
+ },
10214
+ "Failed to deliver assistant message start to stream coordinator"
10215
+ );
10216
+ });
9997
10217
  if (hasEmittedText) {
9998
10218
  needsSeparator = true;
9999
10219
  }
@@ -10385,7 +10605,194 @@ function createProgressReporter(args) {
10385
10605
  };
10386
10606
  }
10387
10607
 
10388
- // src/handlers/oauth-resume.ts
10608
+ // src/chat/slack/reply.ts
10609
+ import { Buffer as Buffer2 } from "buffer";
10610
+ function isInterruptedVisibleReply(reply) {
10611
+ return reply.diagnostics.outcome === "provider_error";
10612
+ }
10613
+ function buildChunkMessage(chunk, files) {
10614
+ return {
10615
+ markdown: chunk,
10616
+ ...files ? { files } : {}
10617
+ };
10618
+ }
10619
+ function buildTextPosts(args) {
10620
+ const chunks = splitSlackReplyText(args.text, {
10621
+ interrupted: args.interrupted
10622
+ });
10623
+ return chunks.map((chunk, index) => ({
10624
+ message: buildChunkMessage(
10625
+ chunk,
10626
+ index === 0 ? args.firstFiles : void 0
10627
+ ),
10628
+ stage: index === 0 ? args.firstStage ?? "thread_reply" : "thread_reply_continuation"
10629
+ }));
10630
+ }
10631
+ async function normalizeFileUploads(files) {
10632
+ return await Promise.all(
10633
+ files.map(async (file) => {
10634
+ let data;
10635
+ if (Buffer2.isBuffer(file.data)) {
10636
+ data = file.data;
10637
+ } else if (file.data instanceof ArrayBuffer) {
10638
+ data = Buffer2.from(file.data);
10639
+ } else {
10640
+ data = Buffer2.from(await file.data.arrayBuffer());
10641
+ }
10642
+ return {
10643
+ data,
10644
+ filename: file.filename
10645
+ };
10646
+ })
10647
+ );
10648
+ }
10649
+ async function uploadReplyFilesBestEffort(args) {
10650
+ try {
10651
+ await uploadFilesToThread({
10652
+ channelId: args.channelId,
10653
+ threadTs: args.threadTs,
10654
+ files: await normalizeFileUploads(args.files)
10655
+ });
10656
+ } catch {
10657
+ }
10658
+ }
10659
+ function getReplyMessageText(message) {
10660
+ if (typeof message !== "object" || message === null) {
10661
+ return void 0;
10662
+ }
10663
+ if ("markdown" in message && typeof message.markdown === "string") {
10664
+ return message.markdown;
10665
+ }
10666
+ if ("raw" in message && typeof message.raw === "string") {
10667
+ return message.raw;
10668
+ }
10669
+ return void 0;
10670
+ }
10671
+ function getReplyMessageFiles(message) {
10672
+ if (typeof message === "object" && message !== null && "files" in message && Array.isArray(message.files)) {
10673
+ return message.files;
10674
+ }
10675
+ return void 0;
10676
+ }
10677
+ function createSlackStreamAccumulator() {
10678
+ let pendingCarriageReturn = false;
10679
+ let streamedVisibleText = "";
10680
+ let streamedRenderedText = "";
10681
+ let overflowText = "";
10682
+ let streamOverflowed = false;
10683
+ const continuationBudget = getSlackStreamingContinuationBudget();
10684
+ const normalizeDelta = (deltaText) => {
10685
+ let text = deltaText;
10686
+ if (pendingCarriageReturn) {
10687
+ text = `\r${text}`;
10688
+ pendingCarriageReturn = false;
10689
+ }
10690
+ if (text.endsWith("\r")) {
10691
+ text = text.slice(0, -1);
10692
+ pendingCarriageReturn = true;
10693
+ }
10694
+ return text.replace(/\r\n?/g, "\n");
10695
+ };
10696
+ return {
10697
+ append(deltaText) {
10698
+ const normalizedDeltaText = normalizeDelta(deltaText);
10699
+ if (!normalizedDeltaText) {
10700
+ return "";
10701
+ }
10702
+ if (streamOverflowed) {
10703
+ overflowText += normalizedDeltaText;
10704
+ return "";
10705
+ }
10706
+ const candidate = `${streamedVisibleText}${normalizedDeltaText}`;
10707
+ const { prefix, renderedPrefix, rest } = takeSlackContinuationPrefix(
10708
+ candidate,
10709
+ continuationBudget
10710
+ );
10711
+ const additional = renderedPrefix.length > streamedRenderedText.length ? renderedPrefix.slice(streamedRenderedText.length) : "";
10712
+ streamedVisibleText = prefix;
10713
+ streamedRenderedText = renderedPrefix;
10714
+ if (rest) {
10715
+ overflowText += rest;
10716
+ streamOverflowed = true;
10717
+ }
10718
+ return additional;
10719
+ },
10720
+ getOverflowText() {
10721
+ return overflowText;
10722
+ }
10723
+ };
10724
+ }
10725
+ function planSlackReplyPosts(args) {
10726
+ const replyFiles = args.reply.files && args.reply.files.length > 0 ? args.reply.files : void 0;
10727
+ const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
10728
+ reply: args.reply,
10729
+ hasStreamedThreadReply: args.hasStreamedThreadReply
10730
+ });
10731
+ const interrupted = isInterruptedVisibleReply(args.reply);
10732
+ const posts = [];
10733
+ if (args.hasStreamedThreadReply) {
10734
+ if (shouldPostThreadReply && args.streamedOverflowText) {
10735
+ posts.push(
10736
+ ...buildTextPosts({
10737
+ text: args.streamedOverflowText,
10738
+ interrupted,
10739
+ firstStage: "thread_reply_continuation"
10740
+ })
10741
+ );
10742
+ } else if (shouldPostThreadReply && interrupted) {
10743
+ posts.push({
10744
+ message: buildSlackOutputMessage(
10745
+ getSlackInterruptionMarker().trimStart()
10746
+ ),
10747
+ stage: "thread_reply_continuation"
10748
+ });
10749
+ }
10750
+ } else {
10751
+ const textPosts = shouldPostThreadReply ? buildTextPosts({
10752
+ text: args.reply.text,
10753
+ interrupted,
10754
+ firstFiles: attachFiles === "inline" ? replyFiles : void 0
10755
+ }) : [];
10756
+ posts.push(...textPosts);
10757
+ if (attachFiles === "inline" && replyFiles && textPosts.length === 0) {
10758
+ posts.push({
10759
+ message: buildSlackOutputMessage("", replyFiles),
10760
+ stage: "thread_reply"
10761
+ });
10762
+ } else if (shouldPostThreadReply && textPosts.length === 0) {
10763
+ posts.push({
10764
+ message: buildSlackOutputMessage(args.reply.text),
10765
+ stage: "thread_reply"
10766
+ });
10767
+ }
10768
+ }
10769
+ if (attachFiles === "followup" && replyFiles) {
10770
+ posts.push({
10771
+ message: buildSlackOutputMessage("", replyFiles),
10772
+ stage: "thread_reply_files_followup"
10773
+ });
10774
+ }
10775
+ return posts;
10776
+ }
10777
+ async function postSlackApiReplyPosts(args) {
10778
+ for (const post of args.posts) {
10779
+ const text = getReplyMessageText(post.message);
10780
+ if (text && text.trim().length > 0) {
10781
+ await args.postMessage(args.channelId, args.threadTs, text);
10782
+ }
10783
+ const files = getReplyMessageFiles(post.message);
10784
+ if (!files?.length) {
10785
+ continue;
10786
+ }
10787
+ await uploadReplyFilesBestEffort({
10788
+ channelId: args.channelId,
10789
+ threadTs: args.threadTs,
10790
+ files
10791
+ });
10792
+ }
10793
+ }
10794
+
10795
+ // src/chat/slack/resume.ts
10389
10796
  function resolveReplyTimeoutMs(explicitTimeoutMs) {
10390
10797
  if (typeof explicitTimeoutMs === "number" && explicitTimeoutMs > 0) {
10391
10798
  return explicitTimeoutMs;
@@ -10531,11 +10938,15 @@ async function resumeSlackTurn(args) {
10531
10938
  )
10532
10939
  ]) : await replyPromise;
10533
10940
  await progress.stop();
10534
- if (args.onReply) {
10535
- await args.onReply(reply);
10536
- } else if (reply.text) {
10537
- await postSlackMessage(args.channelId, args.threadTs, reply.text);
10538
- }
10941
+ await postSlackApiReplyPosts({
10942
+ channelId: args.channelId,
10943
+ threadTs: args.threadTs,
10944
+ posts: planSlackReplyPosts({
10945
+ reply,
10946
+ hasStreamedThreadReply: false
10947
+ }),
10948
+ postMessage: postSlackMessage
10949
+ });
10539
10950
  await args.onSuccess?.(reply);
10540
10951
  } catch (error) {
10541
10952
  await progress.stop();
@@ -10592,7 +11003,6 @@ async function resumeAuthorizedRequest(args) {
10592
11003
  initialText: args.connectedText,
10593
11004
  failureText: args.failureText,
10594
11005
  generateReply: args.generateReply,
10595
- onReply: args.onReply,
10596
11006
  onSuccess: args.onSuccess,
10597
11007
  onFailure: args.onFailure,
10598
11008
  onAuthPause: args.onAuthPause,
@@ -10756,65 +11166,6 @@ function htmlResponse(kind) {
10756
11166
  const page = CALLBACK_PAGES[kind];
10757
11167
  return htmlCallbackResponse(page.title, page.message, page.status);
10758
11168
  }
10759
- function extractSlackText(text, files) {
10760
- const message = buildSlackOutputMessage(text, files);
10761
- if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
10762
- return message.markdown;
10763
- }
10764
- if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
10765
- return message.raw;
10766
- }
10767
- return text;
10768
- }
10769
- async function normalizeFileUploads(files) {
10770
- const normalized = [];
10771
- for (const file of files) {
10772
- let data;
10773
- if (Buffer2.isBuffer(file.data)) {
10774
- data = file.data;
10775
- } else if (file.data instanceof ArrayBuffer) {
10776
- data = Buffer2.from(file.data);
10777
- } else {
10778
- data = Buffer2.from(await file.data.arrayBuffer());
10779
- }
10780
- normalized.push({
10781
- data,
10782
- filename: file.filename
10783
- });
10784
- }
10785
- return normalized;
10786
- }
10787
- async function deliverReplyToThread(channelId, threadTs, reply) {
10788
- const replyFiles = reply.files && reply.files.length > 0 ? reply.files : void 0;
10789
- const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
10790
- reply,
10791
- hasStreamedThreadReply: false
10792
- });
10793
- if (shouldPostThreadReply) {
10794
- const text = extractSlackText(
10795
- reply.text,
10796
- attachFiles === "inline" ? replyFiles : void 0
10797
- );
10798
- if (text.trim().length > 0) {
10799
- await postSlackMessage(channelId, threadTs, text);
10800
- }
10801
- }
10802
- if (!replyFiles || attachFiles === "none") {
10803
- return;
10804
- }
10805
- const files = await normalizeFileUploads(replyFiles);
10806
- if (files.length === 0) {
10807
- return;
10808
- }
10809
- try {
10810
- await uploadFilesToThread({
10811
- channelId,
10812
- threadTs,
10813
- files
10814
- });
10815
- } catch {
10816
- }
10817
- }
10818
11169
  async function buildResumeConversationContext(channelId, threadTs, sessionId) {
10819
11170
  const threadId = `slack:${channelId}:${threadTs}`;
10820
11171
  const conversation = coerceThreadConversationState(
@@ -10899,7 +11250,6 @@ async function resumeAuthorizedMcpTurn(args) {
10899
11250
  );
10900
11251
  await resumeAuthorizedRequest({
10901
11252
  messageText: authSession.userMessage,
10902
- provider,
10903
11253
  channelId: authSession.channelId,
10904
11254
  threadTs: authSession.threadTs,
10905
11255
  lockKey: authSession.conversationId,
@@ -10927,13 +11277,6 @@ async function resumeAuthorizedMcpTurn(args) {
10927
11277
  sandbox: getPersistedSandboxState(currentState),
10928
11278
  threadParticipants: buildThreadParticipants(conversation.messages)
10929
11279
  },
10930
- onReply: async (reply) => {
10931
- await deliverReplyToThread(
10932
- authSession.channelId,
10933
- authSession.threadTs,
10934
- reply
10935
- );
10936
- },
10937
11280
  onSuccess: async (reply) => {
10938
11281
  try {
10939
11282
  await persistCompletedReplyState(
@@ -11230,7 +11573,6 @@ async function resumePendingOAuthMessage(stored) {
11230
11573
  );
11231
11574
  await resumeAuthorizedRequest({
11232
11575
  messageText: stored.pendingMessage,
11233
- provider: stored.provider,
11234
11576
  channelId: stored.channelId,
11235
11577
  threadTs: stored.threadTs,
11236
11578
  connectedText: `Your ${providerLabel} account is now connected. Processing your request...`,
@@ -11428,9 +11770,6 @@ async function GET5(request, provider, waitUntil) {
11428
11770
  });
11429
11771
  }
11430
11772
 
11431
- // src/handlers/turn-resume.ts
11432
- import { Buffer as Buffer3 } from "buffer";
11433
-
11434
11773
  // src/chat/slack/context.ts
11435
11774
  function toTrimmedSlackString(value) {
11436
11775
  const normalized = toOptionalString(value);
@@ -11478,65 +11817,6 @@ function resolveSlackChannelIdFromMessage(message) {
11478
11817
  }
11479
11818
 
11480
11819
  // src/handlers/turn-resume.ts
11481
- function extractSlackText2(text, files) {
11482
- const message = buildSlackOutputMessage(text, files);
11483
- if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
11484
- return message.markdown;
11485
- }
11486
- if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
11487
- return message.raw;
11488
- }
11489
- return text;
11490
- }
11491
- async function normalizeFileUploads2(files) {
11492
- const normalized = [];
11493
- for (const file of files) {
11494
- let data;
11495
- if (Buffer3.isBuffer(file.data)) {
11496
- data = file.data;
11497
- } else if (file.data instanceof ArrayBuffer) {
11498
- data = Buffer3.from(file.data);
11499
- } else {
11500
- data = Buffer3.from(await file.data.arrayBuffer());
11501
- }
11502
- normalized.push({
11503
- data,
11504
- filename: file.filename
11505
- });
11506
- }
11507
- return normalized;
11508
- }
11509
- async function deliverReplyToThread2(args) {
11510
- const replyFiles = args.reply.files && args.reply.files.length > 0 ? args.reply.files : void 0;
11511
- const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
11512
- reply: args.reply,
11513
- hasStreamedThreadReply: false
11514
- });
11515
- if (shouldPostThreadReply) {
11516
- const text = extractSlackText2(
11517
- args.reply.text,
11518
- attachFiles === "inline" ? replyFiles : void 0
11519
- );
11520
- if (text.trim().length > 0) {
11521
- await postSlackMessage(args.channelId, args.threadTs, text);
11522
- }
11523
- }
11524
- if (!replyFiles || attachFiles === "none") {
11525
- return;
11526
- }
11527
- const files = await normalizeFileUploads2(replyFiles);
11528
- if (files.length === 0) {
11529
- return;
11530
- }
11531
- try {
11532
- await uploadFilesToThread({
11533
- channelId: args.channelId,
11534
- threadTs: args.threadTs,
11535
- files
11536
- });
11537
- } catch {
11538
- }
11539
- }
11540
11820
  async function persistCompletedReplyState2(args) {
11541
11821
  const currentState = await getPersistedThreadState(
11542
11822
  args.checkpoint.conversationId
@@ -11651,13 +11931,6 @@ async function resumeTimedOutTurn(payload) {
11651
11931
  sandbox,
11652
11932
  threadParticipants: buildThreadParticipants(conversation.messages)
11653
11933
  },
11654
- onReply: async (reply) => {
11655
- await deliverReplyToThread2({
11656
- channelId: thread.channelId,
11657
- threadTs: thread.threadTs,
11658
- reply
11659
- });
11660
- },
11661
11934
  onSuccess: async (reply) => {
11662
11935
  try {
11663
11936
  await persistCompletedReplyState2({ checkpoint, reply });
@@ -11766,9 +12039,6 @@ async function POST(request, waitUntil) {
11766
12039
  return new Response("Accepted", { status: 202 });
11767
12040
  }
11768
12041
 
11769
- // src/chat/app/production.ts
11770
- import { createSlackAdapter } from "@chat-adapter/slack";
11771
-
11772
12042
  // src/chat/services/subscribed-decision.ts
11773
12043
  import { z } from "zod";
11774
12044
  var replyDecisionSchema = z.object({
@@ -12615,6 +12885,15 @@ var MAX_USER_ATTACHMENTS = 3;
12615
12885
  var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
12616
12886
  var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
12617
12887
  var MAX_VISION_SUMMARY_CHARS = 500;
12888
+ function hasPotentialImageAttachment(attachments) {
12889
+ return attachments?.some((attachment) => {
12890
+ if (attachment.type === "image") {
12891
+ return true;
12892
+ }
12893
+ const mimeType = attachment.mimeType ?? "";
12894
+ return attachment.type === "file" && mimeType.startsWith("image/");
12895
+ }) ?? false;
12896
+ }
12618
12897
  function isVisionEnabled() {
12619
12898
  return Boolean(botConfig.visionModelId);
12620
12899
  }
@@ -12957,6 +13236,7 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
12957
13236
  continue;
12958
13237
  }
12959
13238
  hydratedMessageIds.add(conversationMessage.id);
13239
+ const existingMeta = conversationMessage.meta ?? {};
12960
13240
  const imageFiles = (reply.files ?? []).filter((file) => {
12961
13241
  const mimeType = toOptionalString(file.mimetype);
12962
13242
  return Boolean(
@@ -12964,10 +13244,15 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
12964
13244
  );
12965
13245
  }).slice(0, MAX_MESSAGE_IMAGE_ATTACHMENTS);
12966
13246
  if (imageFiles.length === 0) {
13247
+ conversationMessage.meta = {
13248
+ ...existingMeta,
13249
+ slackTs: existingMeta.slackTs ?? ts,
13250
+ imagesHydrated: true
13251
+ };
13252
+ mutated = true;
12967
13253
  continue;
12968
13254
  }
12969
13255
  const imageFileIds = imageFiles.map((file) => toOptionalString(file.id)).filter((fileId) => Boolean(fileId));
12970
- const existingMeta = conversationMessage.meta ?? {};
12971
13256
  conversationMessage.meta = {
12972
13257
  ...existingMeta,
12973
13258
  slackTs: existingMeta.slackTs ?? ts,
@@ -13196,6 +13481,14 @@ function getExecutionFailureReason(reply) {
13196
13481
  }
13197
13482
  return "empty assistant turn";
13198
13483
  }
13484
+ function shouldAutoStartStreaming(args) {
13485
+ const { text, deltaCount } = args;
13486
+ const trimmed = text.trim();
13487
+ if (!trimmed || isPotentialRedundantReactionAckText(trimmed)) {
13488
+ return false;
13489
+ }
13490
+ return deltaCount >= 2;
13491
+ }
13199
13492
  function createReplyToThread(deps) {
13200
13493
  return async function replyToThread(thread, message, options = {}) {
13201
13494
  if (message.author.isMe) {
@@ -13304,7 +13597,10 @@ function createReplyToThread(deps) {
13304
13597
  const textStream = createTextStreamBridge();
13305
13598
  let streamedReplyPromise;
13306
13599
  let pendingStreamText = "";
13600
+ let pendingStreamDeltaCount = 0;
13601
+ let awaitingPostToolAssistantMessage = false;
13307
13602
  let beforeFirstResponsePostCalled = false;
13603
+ let streamedReplyState = createSlackStreamAccumulator();
13308
13604
  const beforeFirstResponsePost = async () => {
13309
13605
  if (beforeFirstResponsePostCalled) {
13310
13606
  return;
@@ -13330,6 +13626,51 @@ function createReplyToThread(deps) {
13330
13626
  startStreamingReply();
13331
13627
  textStream.push(pendingStreamText);
13332
13628
  pendingStreamText = "";
13629
+ pendingStreamDeltaCount = 0;
13630
+ };
13631
+ const clearPendingStreamText = () => {
13632
+ pendingStreamText = "";
13633
+ pendingStreamDeltaCount = 0;
13634
+ };
13635
+ const discardPendingStreamPreview = () => {
13636
+ clearPendingStreamText();
13637
+ streamedReplyState = createSlackStreamAccumulator();
13638
+ };
13639
+ const finalizePendingStreamText = () => {
13640
+ if (!pendingStreamText || streamedReplyPromise || isPotentialRedundantReactionAckText(pendingStreamText)) {
13641
+ return;
13642
+ }
13643
+ flushPendingStreamText();
13644
+ };
13645
+ const queueVisibleStreamText = (text) => {
13646
+ if (!text) {
13647
+ return;
13648
+ }
13649
+ if (awaitingPostToolAssistantMessage) {
13650
+ return;
13651
+ }
13652
+ if (streamedReplyPromise) {
13653
+ textStream.push(text);
13654
+ return;
13655
+ }
13656
+ pendingStreamText += text;
13657
+ pendingStreamDeltaCount += 1;
13658
+ if (isPotentialRedundantReactionAckText(pendingStreamText)) {
13659
+ return;
13660
+ }
13661
+ if (!shouldAutoStartStreaming({
13662
+ text: pendingStreamText,
13663
+ deltaCount: pendingStreamDeltaCount
13664
+ })) {
13665
+ return;
13666
+ }
13667
+ flushPendingStreamText();
13668
+ };
13669
+ const appendVisibleStreamDelta = (deltaText) => {
13670
+ if (awaitingPostToolAssistantMessage && !streamedReplyPromise) {
13671
+ return;
13672
+ }
13673
+ queueVisibleStreamText(streamedReplyState.append(deltaText));
13333
13674
  };
13334
13675
  const postThreadReply = async (payload, stage) => {
13335
13676
  await beforeFirstResponsePost();
@@ -13402,19 +13743,26 @@ function createReplyToThread(deps) {
13402
13743
  if (explicitChannelPostIntent) {
13403
13744
  return;
13404
13745
  }
13405
- if (streamedReplyPromise) {
13406
- textStream.push(deltaText);
13746
+ appendVisibleStreamDelta(deltaText);
13747
+ },
13748
+ onAssistantMessageStart: () => {
13749
+ if (!awaitingPostToolAssistantMessage) {
13407
13750
  return;
13408
13751
  }
13409
- pendingStreamText += deltaText;
13410
- if (isPotentialRedundantReactionAckText(pendingStreamText)) {
13411
- return;
13752
+ awaitingPostToolAssistantMessage = false;
13753
+ discardPendingStreamPreview();
13754
+ },
13755
+ onToolCall: () => {
13756
+ if (!streamedReplyPromise) {
13757
+ awaitingPostToolAssistantMessage = true;
13758
+ discardPendingStreamPreview();
13412
13759
  }
13413
- flushPendingStreamText();
13414
13760
  }
13415
13761
  });
13416
13762
  if (streamedReplyPromise) {
13417
13763
  flushPendingStreamText();
13764
+ } else {
13765
+ finalizePendingStreamText();
13418
13766
  }
13419
13767
  textStream.end();
13420
13768
  const diagnosticsContext = {
@@ -13488,28 +13836,32 @@ function createReplyToThread(deps) {
13488
13836
  }
13489
13837
  });
13490
13838
  const artifactStatePatch = reply.artifactStatePatch ? { ...reply.artifactStatePatch } : {};
13491
- const replyFiles = reply.files && reply.files.length > 0 ? reply.files : void 0;
13492
- const { shouldPostThreadReply, attachFiles: resolvedAttachFiles } = resolveReplyDelivery({
13493
- reply,
13494
- hasStreamedThreadReply: Boolean(streamedReplyPromise)
13495
- });
13496
13839
  const reactionPerformed = reply.diagnostics.toolCalls.includes(
13497
13840
  "slackMessageAddReaction"
13498
13841
  );
13499
- if (shouldPostThreadReply) {
13842
+ const plannedPosts = planSlackReplyPosts({
13843
+ reply,
13844
+ hasStreamedThreadReply: Boolean(streamedReplyPromise),
13845
+ streamedOverflowText: streamedReplyState.getOverflowText()
13846
+ });
13847
+ if (streamedReplyPromise) {
13848
+ await streamedReplyPromise;
13849
+ }
13850
+ if (plannedPosts.length > 0) {
13500
13851
  if (!streamedReplyPromise) {
13501
- const sent = await postThreadReply(
13502
- buildSlackOutputMessage(
13503
- reply.text,
13504
- resolvedAttachFiles === "inline" ? replyFiles : void 0
13505
- ),
13506
- "thread_reply"
13507
- );
13508
- if (reactionPerformed && isRedundantReactionAckText(reply.text)) {
13852
+ let sent;
13853
+ for (const post of plannedPosts) {
13854
+ sent = await postThreadReply(post.message, post.stage);
13855
+ }
13856
+ const firstPlannedMessage = plannedPosts[0]?.message;
13857
+ const firstPlannedMessageHasFiles = typeof firstPlannedMessage === "object" && firstPlannedMessage !== null && "files" in firstPlannedMessage && Array.isArray(firstPlannedMessage.files) && firstPlannedMessage.files.length > 0;
13858
+ if (sent && reactionPerformed && plannedPosts.length === 1 && !firstPlannedMessageHasFiles && isRedundantReactionAckText(reply.text)) {
13509
13859
  await sent.delete();
13510
13860
  }
13511
13861
  } else {
13512
- await streamedReplyPromise;
13862
+ for (const post of plannedPosts) {
13863
+ await postThreadReply(post.message, post.stage);
13864
+ }
13513
13865
  }
13514
13866
  }
13515
13867
  const shouldPersistArtifacts = Object.keys(artifactStatePatch).length > 0;
@@ -13585,12 +13937,6 @@ function createReplyToThread(deps) {
13585
13937
  );
13586
13938
  });
13587
13939
  }
13588
- if (shouldPostThreadReply && resolvedAttachFiles === "followup" && replyFiles) {
13589
- await postThreadReply(
13590
- buildSlackOutputMessage("", replyFiles),
13591
- "thread_reply_files_followup"
13592
- );
13593
- }
13594
13940
  } catch (error) {
13595
13941
  if (isRetryableTurnError(error, "mcp_auth_resume")) {
13596
13942
  shouldPersistFailureState = false;
@@ -13722,6 +14068,11 @@ async function initializeAssistantThread(event) {
13722
14068
  }
13723
14069
 
13724
14070
  // src/chat/runtime/turn-preparation.ts
14071
+ function hasPendingImageHydration(conversation) {
14072
+ return conversation.messages.some(
14073
+ (message) => isHumanConversationMessage(message) && !message.meta?.imagesHydrated
14074
+ );
14075
+ }
13725
14076
  function createPrepareTurnState(deps) {
13726
14077
  return async function prepareTurnState(args) {
13727
14078
  const existingState = await args.thread.state;
@@ -13739,14 +14090,8 @@ function createPrepareTurnState(deps) {
13739
14090
  messageId: args.message.id,
13740
14091
  messageCreatedAtMs: args.message.metadata.dateSent.getTime()
13741
14092
  });
13742
- const messageHasPotentialImageAttachment = args.message.attachments.some(
13743
- (attachment) => {
13744
- if (attachment.type === "image") {
13745
- return true;
13746
- }
13747
- const mimeType = attachment.mimeType ?? "";
13748
- return attachment.type === "file" && mimeType.startsWith("image/");
13749
- }
14093
+ const messageHasPotentialImageAttachment = hasPotentialImageAttachment(
14094
+ args.message.attachments
13750
14095
  );
13751
14096
  const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
13752
14097
  const slackTs = getSlackMessageTs(args.message);
@@ -13771,7 +14116,8 @@ function createPrepareTurnState(deps) {
13771
14116
  conversation,
13772
14117
  incomingUserMessage
13773
14118
  );
13774
- if (isVisionEnabled() && (!conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment)) {
14119
+ const shouldHydrateVisionContext = !conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment || hasPendingImageHydration(conversation);
14120
+ if (isVisionEnabled() && shouldHydrateVisionContext) {
13775
14121
  await deps.hydrateConversationVisionContext(conversation, {
13776
14122
  threadId: args.context.threadId,
13777
14123
  channelId: args.context.channelId,
@@ -13867,7 +14213,7 @@ function createSlackRuntime(options) {
13867
14213
  slackTs,
13868
14214
  replied: false,
13869
14215
  skippedReason: decision.reason,
13870
- imagesHydrated: true
14216
+ imagesHydrated: !hasPotentialImageAttachment(message.attachments)
13871
14217
  }
13872
14218
  });
13873
14219
  conversation.processing.activeTurnId = void 0;
@@ -14151,6 +14497,166 @@ var JuniorChat = class extends Chat {
14151
14497
  }
14152
14498
  };
14153
14499
 
14500
+ // src/chat/slack/adapter.ts
14501
+ import {
14502
+ createSlackAdapter
14503
+ } from "@chat-adapter/slack";
14504
+ import {
14505
+ StreamingMarkdownRenderer
14506
+ } from "chat";
14507
+ var STREAM_BUFFER_SIZE = 64;
14508
+ var CLIENT_STREAM_PATCHED = /* @__PURE__ */ Symbol("junior.slack.client_stream_patched");
14509
+ var ADAPTER_STREAM_PATCHED = /* @__PURE__ */ Symbol("junior.slack.adapter_stream_patched");
14510
+ function assertSlackAdapterInternals(internals) {
14511
+ if (!internals.client || typeof internals.client.chatStream !== "function") {
14512
+ throw new Error("Slack adapter client does not expose chatStream()");
14513
+ }
14514
+ if (typeof internals.stream !== "function") {
14515
+ throw new Error("Slack adapter does not expose stream()");
14516
+ }
14517
+ if (typeof internals.decodeThreadId !== "function") {
14518
+ throw new Error("Slack adapter does not expose decodeThreadId()");
14519
+ }
14520
+ if (typeof internals.getToken !== "function") {
14521
+ throw new Error("Slack adapter does not expose getToken()");
14522
+ }
14523
+ if (!internals.logger || typeof internals.logger.debug !== "function" || typeof internals.logger.warn !== "function") {
14524
+ throw new Error("Slack adapter does not expose logger debug/warn methods");
14525
+ }
14526
+ }
14527
+ function shouldEagerFlushPlainText(text) {
14528
+ return text.length > 0 && !text.includes("\n") && !/[`*~[\]|]/.test(text);
14529
+ }
14530
+ function getNextRenderableDelta(renderer, lastAppended) {
14531
+ const committable = renderer.getCommittableText();
14532
+ if (committable.startsWith(lastAppended)) {
14533
+ const delta = committable.slice(lastAppended.length);
14534
+ if (delta) {
14535
+ return { delta, nextAppended: committable };
14536
+ }
14537
+ }
14538
+ const rawText = renderer.getText();
14539
+ if (shouldEagerFlushPlainText(rawText) && rawText.startsWith(lastAppended) && rawText.length > lastAppended.length) {
14540
+ return {
14541
+ delta: rawText.slice(lastAppended.length),
14542
+ nextAppended: rawText
14543
+ };
14544
+ }
14545
+ return { delta: "", nextAppended: lastAppended };
14546
+ }
14547
+ function patchSlackClientStream(adapter) {
14548
+ const internals = adapter;
14549
+ const { client: client2 } = internals;
14550
+ if (client2[CLIENT_STREAM_PATCHED]) {
14551
+ return;
14552
+ }
14553
+ const originalChatStream = client2.chatStream.bind(client2);
14554
+ client2.chatStream = (params) => originalChatStream({
14555
+ ...params,
14556
+ buffer_size: STREAM_BUFFER_SIZE
14557
+ });
14558
+ client2[CLIENT_STREAM_PATCHED] = true;
14559
+ }
14560
+ function patchSlackAdapterStream(adapter) {
14561
+ const internals = adapter;
14562
+ if (internals[ADAPTER_STREAM_PATCHED]) {
14563
+ return;
14564
+ }
14565
+ const originalStream = internals.stream.bind(adapter);
14566
+ internals.stream = async function(threadId, textStream, options) {
14567
+ if (!(options?.recipientUserId && options?.recipientTeamId)) {
14568
+ return originalStream(threadId, textStream, options);
14569
+ }
14570
+ const { channel, threadTs } = internals.decodeThreadId(threadId);
14571
+ internals.logger.debug("Slack: starting stream", { channel, threadTs });
14572
+ const token = internals.getToken();
14573
+ const streamer = internals.client.chatStream({
14574
+ channel,
14575
+ thread_ts: threadTs,
14576
+ recipient_user_id: options.recipientUserId,
14577
+ recipient_team_id: options.recipientTeamId,
14578
+ ...options.taskDisplayMode ? { task_display_mode: options.taskDisplayMode } : {}
14579
+ });
14580
+ let first = true;
14581
+ let lastAppended = "";
14582
+ let structuredChunksSupported = true;
14583
+ const renderer = new StreamingMarkdownRenderer();
14584
+ const flushMarkdownDelta = async (delta) => {
14585
+ if (delta.length === 0) {
14586
+ return;
14587
+ }
14588
+ if (first) {
14589
+ await streamer.append({ markdown_text: delta, token, chunks: [] });
14590
+ first = false;
14591
+ return;
14592
+ }
14593
+ await streamer.append({ markdown_text: delta });
14594
+ };
14595
+ const flushText = async () => {
14596
+ const { delta, nextAppended } = getNextRenderableDelta(
14597
+ renderer,
14598
+ lastAppended
14599
+ );
14600
+ await flushMarkdownDelta(delta);
14601
+ lastAppended = nextAppended;
14602
+ };
14603
+ const sendStructuredChunk = async (chunk) => {
14604
+ if (!structuredChunksSupported) {
14605
+ return;
14606
+ }
14607
+ await flushText();
14608
+ try {
14609
+ if (first) {
14610
+ await streamer.append({ chunks: [chunk], token });
14611
+ first = false;
14612
+ return;
14613
+ }
14614
+ await streamer.append({ chunks: [chunk] });
14615
+ } catch (error) {
14616
+ structuredChunksSupported = false;
14617
+ internals.logger.warn(
14618
+ "Structured streaming chunk failed, falling back to text-only streaming. Ensure your Slack app manifest includes assistant_view, assistant:write scope, and @slack/web-api >= 7.14.0",
14619
+ { chunkType: chunk.type, error }
14620
+ );
14621
+ }
14622
+ };
14623
+ const pushTextAndFlush = async (text) => {
14624
+ renderer.push(text);
14625
+ await flushText();
14626
+ };
14627
+ for await (const chunk of textStream) {
14628
+ if (typeof chunk === "string") {
14629
+ await pushTextAndFlush(chunk);
14630
+ } else if (chunk.type === "markdown_text") {
14631
+ await pushTextAndFlush(chunk.text);
14632
+ } else {
14633
+ await sendStructuredChunk(chunk);
14634
+ }
14635
+ }
14636
+ renderer.finish();
14637
+ await flushText();
14638
+ const result = await streamer.stop(
14639
+ options?.stopBlocks ? { blocks: options.stopBlocks } : void 0
14640
+ );
14641
+ const messageTs = result.message?.ts ?? result.ts;
14642
+ internals.logger.debug("Slack: stream complete", { messageId: messageTs });
14643
+ return {
14644
+ id: messageTs,
14645
+ threadId,
14646
+ raw: result
14647
+ };
14648
+ };
14649
+ internals[ADAPTER_STREAM_PATCHED] = true;
14650
+ }
14651
+ function createJuniorSlackAdapter(config) {
14652
+ const adapter = createSlackAdapter(config);
14653
+ const internals = adapter;
14654
+ assertSlackAdapterInternals(internals);
14655
+ patchSlackClientStream(adapter);
14656
+ patchSlackAdapterStream(adapter);
14657
+ return adapter;
14658
+ }
14659
+
14154
14660
  // src/chat/queue/thread-message-dispatcher.ts
14155
14661
  function rehydrateAttachmentFetchers(message, downloadPrivateSlackFile2 = downloadPrivateSlackFile) {
14156
14662
  for (const attachment of message.attachments) {
@@ -14269,7 +14775,7 @@ function createProductionBot() {
14269
14775
  if (!signingSecret) {
14270
14776
  throw new Error("SLACK_SIGNING_SECRET is required");
14271
14777
  }
14272
- return createSlackAdapter({
14778
+ return createJuniorSlackAdapter({
14273
14779
  logger: logger.child("slack"),
14274
14780
  signingSecret,
14275
14781
  ...botToken ? { botToken } : {},
@@ -14405,6 +14911,32 @@ function isMessageChangedEnvelope(value) {
14405
14911
  function textMentionsBot(text, botUserId) {
14406
14912
  return text.includes(`<@${botUserId}>`);
14407
14913
  }
14914
+ function getAttachmentType(mimeType) {
14915
+ if (mimeType?.startsWith("image/")) {
14916
+ return "image";
14917
+ }
14918
+ if (mimeType?.startsWith("video/")) {
14919
+ return "video";
14920
+ }
14921
+ if (mimeType?.startsWith("audio/")) {
14922
+ return "audio";
14923
+ }
14924
+ return "file";
14925
+ }
14926
+ function extractEditedMessageAttachments(files) {
14927
+ if (!files || files.length === 0) {
14928
+ return [];
14929
+ }
14930
+ return files.map((file) => ({
14931
+ type: getAttachmentType(file.mimetype),
14932
+ url: file.url_private_download ?? file.url_private,
14933
+ name: file.name,
14934
+ mimeType: file.mimetype,
14935
+ size: file.size,
14936
+ width: file.original_w,
14937
+ height: file.original_h
14938
+ }));
14939
+ }
14408
14940
  function extractMessageChangedMention(body, botUserId, adapter) {
14409
14941
  if (!isMessageChangedEnvelope(body)) return null;
14410
14942
  const { event } = body;
@@ -14430,7 +14962,7 @@ function extractMessageChangedMention(body, botUserId, adapter) {
14430
14962
  threadId,
14431
14963
  text: newText,
14432
14964
  isMention: true,
14433
- attachments: [],
14965
+ attachments: extractEditedMessageAttachments(event.message.files),
14434
14966
  metadata: { dateSent: new Date(Number(messageTs) * 1e3), edited: true },
14435
14967
  formatted: { type: "root", children: [] },
14436
14968
  raw,
@@ -14483,6 +15015,7 @@ async function handleAuthenticatedSlackMessageChangedMention(args) {
14483
15015
  if (!result) {
14484
15016
  return false;
14485
15017
  }
15018
+ rehydrateAttachmentFetchers(result.message);
14486
15019
  args.bot.processMessage(
14487
15020
  slackAdapter,
14488
15021
  result.threadId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"