@sentry/junior 0.21.1 → 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.
package/dist/app.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  loadSkillsByName,
7
7
  logCapabilityCatalogLoadedOnce,
8
8
  parseSkillInvocation
9
- } from "./chunk-NRSP2MLC.js";
9
+ } from "./chunk-JWBWBJYJ.js";
10
10
  import {
11
11
  SANDBOX_DATA_ROOT,
12
12
  SANDBOX_SKILLS_ROOT,
@@ -27,10 +27,11 @@ import {
27
27
  sandboxSkillDir,
28
28
  sandboxSkillFile,
29
29
  toOptionalTrimmed
30
- } from "./chunk-Z43DS7XN.js";
30
+ } from "./chunk-THPM7NSG.js";
31
31
  import {
32
32
  CredentialUnavailableError,
33
33
  buildOAuthTokenRequest,
34
+ createChatSdkLogger,
34
35
  createPluginBroker,
35
36
  createRequestContext,
36
37
  extractGenAiUsageAttributes,
@@ -57,7 +58,7 @@ import {
57
58
  toOptionalString,
58
59
  withContext,
59
60
  withSpan
60
- } from "./chunk-N4ICA2BC.js";
61
+ } from "./chunk-MCJJKEB3.js";
61
62
  import "./chunk-Z3YD6NHK.js";
62
63
  import {
63
64
  discoverInstalledPluginPackageContent,
@@ -310,9 +311,6 @@ async function GET3() {
310
311
  });
311
312
  }
312
313
 
313
- // src/handlers/mcp-oauth-callback.ts
314
- import { Buffer as Buffer2 } from "buffer";
315
-
316
314
  // src/chat/state/conversation.ts
317
315
  function coerceRole(value) {
318
316
  return value === "assistant" || value === "system" || value === "user" ? value : "user";
@@ -728,81 +726,6 @@ async function deleteMcpServerSessionId(userId, provider) {
728
726
  await stateAdapter.delete(serverSessionKey(userId, provider));
729
727
  }
730
728
 
731
- // src/chat/slack/output.ts
732
- var MAX_INLINE_CHARS = 2200;
733
- var MAX_INLINE_LINES = 45;
734
- function ensureBlockSpacing(text) {
735
- const codeBlockPattern = /^```/;
736
- const listItemPattern = /^[-*•]\s|^\d+\.\s/;
737
- const lines = text.split("\n");
738
- const result = [];
739
- let inCodeBlock = false;
740
- for (let i = 0; i < lines.length; i++) {
741
- const line = lines[i];
742
- const isCodeFence = codeBlockPattern.test(line.trimStart());
743
- if (isCodeFence) {
744
- if (!inCodeBlock) {
745
- const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
746
- if (prev2 !== void 0 && prev2.trim() !== "") {
747
- result.push("");
748
- }
749
- }
750
- inCodeBlock = !inCodeBlock;
751
- result.push(line);
752
- continue;
753
- }
754
- if (inCodeBlock) {
755
- result.push(line);
756
- continue;
757
- }
758
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
759
- if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
760
- result.push("");
761
- }
762
- result.push(line);
763
- }
764
- return result.join("\n");
765
- }
766
- function normalizeForSlack(text) {
767
- let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
768
- normalized = ensureBlockSpacing(normalized);
769
- return normalized.replace(/\n{3,}/g, "\n\n").trim();
770
- }
771
- function buildSlackOutputMessage(text, files) {
772
- const normalized = normalizeForSlack(text);
773
- const fileCount = files?.length ?? 0;
774
- if (!normalized) {
775
- if (fileCount > 0) {
776
- return {
777
- raw: "",
778
- files
779
- };
780
- }
781
- logWarn(
782
- "slack_output_normalized_empty",
783
- {},
784
- {
785
- "app.output.original_length": text.length,
786
- "app.output.parsed_length": normalized.length,
787
- "app.output.file_count": fileCount
788
- },
789
- "Slack output normalized to empty content"
790
- );
791
- return {
792
- markdown: "I couldn't produce a response.",
793
- files
794
- };
795
- }
796
- return {
797
- markdown: normalized,
798
- files
799
- };
800
- }
801
- var slackOutputPolicy = {
802
- maxInlineChars: MAX_INLINE_CHARS,
803
- maxInlineLines: MAX_INLINE_LINES
804
- };
805
-
806
729
  // src/chat/mcp/oauth.ts
807
730
  import { randomUUID } from "crypto";
808
731
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
@@ -1786,7 +1709,7 @@ function validateConfigKey(key) {
1786
1709
  return "Configuration key must not be empty";
1787
1710
  }
1788
1711
  if (!CONFIG_KEY_RE.test(trimmed)) {
1789
- return `Invalid configuration key "${key}"; expected dotted lowercase namespace (for example "github.repo")`;
1712
+ return `Invalid configuration key "${key}"; expected dotted lowercase namespace (for example "provider.repo")`;
1790
1713
  }
1791
1714
  if (SECRET_KEY_RE.test(trimmed)) {
1792
1715
  return `Configuration key "${key}" appears to be secret-related and is not allowed`;
@@ -1808,7 +1731,9 @@ function collectStringValues(value, output, depth = 0) {
1808
1731
  return;
1809
1732
  }
1810
1733
  if (value && typeof value === "object") {
1811
- for (const [key, nested] of Object.entries(value)) {
1734
+ for (const [key, nested] of Object.entries(
1735
+ value
1736
+ )) {
1812
1737
  output.push(key);
1813
1738
  collectStringValues(nested, output, depth + 1);
1814
1739
  }
@@ -2016,6 +1941,9 @@ function buildArtifactStatePatch(patch) {
2016
1941
  function threadStateKey(threadId) {
2017
1942
  return `thread-state:${threadId}`;
2018
1943
  }
1944
+ function channelStateKey(channelId) {
1945
+ return `channel-state:${channelId}`;
1946
+ }
2019
1947
  function buildThreadStatePayload(patch) {
2020
1948
  const payload = {};
2021
1949
  if (patch.artifacts) {
@@ -2045,6 +1973,14 @@ function mergeArtifactsState(artifacts, patch) {
2045
1973
  }
2046
1974
  };
2047
1975
  }
1976
+ function getPersistedSandboxState(state) {
1977
+ return {
1978
+ sandboxId: toOptionalString(state.app_sandbox_id),
1979
+ sandboxDependencyProfileHash: toOptionalString(
1980
+ state.app_sandbox_dependency_profile_hash
1981
+ )
1982
+ };
1983
+ }
2048
1984
  async function persistThreadState(thread, patch) {
2049
1985
  const payload = buildThreadStatePayload(patch);
2050
1986
  if (Object.keys(payload).length === 0) {
@@ -2059,6 +1995,13 @@ async function getPersistedThreadState(threadId) {
2059
1995
  threadStateKey(threadId)
2060
1996
  ) ?? {};
2061
1997
  }
1998
+ async function getPersistedChannelState(channelId) {
1999
+ const stateAdapter = getStateAdapter();
2000
+ await stateAdapter.connect();
2001
+ return await stateAdapter.get(
2002
+ channelStateKey(channelId)
2003
+ ) ?? {};
2004
+ }
2062
2005
  async function persistThreadStateById(threadId, patch) {
2063
2006
  const payload = buildThreadStatePayload(patch);
2064
2007
  if (Object.keys(payload).length === 0) {
@@ -2081,6 +2024,97 @@ function getChannelConfigurationService(thread) {
2081
2024
  }
2082
2025
  });
2083
2026
  }
2027
+ function getChannelConfigurationServiceById(channelId) {
2028
+ return createChannelConfigurationService({
2029
+ load: async () => await getPersistedChannelState(channelId),
2030
+ save: async (state) => {
2031
+ const stateAdapter = getStateAdapter();
2032
+ await stateAdapter.connect();
2033
+ const key = channelStateKey(channelId);
2034
+ const existing = await stateAdapter.get(key) ?? {};
2035
+ await stateAdapter.set(
2036
+ key,
2037
+ { ...existing, configuration: state },
2038
+ THREAD_STATE_TTL_MS
2039
+ );
2040
+ }
2041
+ });
2042
+ }
2043
+
2044
+ // src/chat/runtime/thread-participants.ts
2045
+ function buildThreadParticipants(messages) {
2046
+ const seen = /* @__PURE__ */ new Set();
2047
+ const participants = [];
2048
+ for (const message of messages) {
2049
+ const { userId, userName, fullName } = message.author ?? {};
2050
+ if (!userId || message.author?.isBot) continue;
2051
+ if (seen.has(userId)) continue;
2052
+ seen.add(userId);
2053
+ participants.push({ userId, userName, fullName });
2054
+ }
2055
+ return participants;
2056
+ }
2057
+
2058
+ // src/chat/runtime/turn.ts
2059
+ function buildDeterministicTurnId(messageId) {
2060
+ const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
2061
+ return `turn_${sanitized}`;
2062
+ }
2063
+ var RetryableTurnError = class extends Error {
2064
+ code = "retryable_turn";
2065
+ metadata;
2066
+ reason;
2067
+ constructor(reason, message, metadata) {
2068
+ super(message);
2069
+ this.name = "RetryableTurnError";
2070
+ this.reason = reason;
2071
+ this.metadata = metadata;
2072
+ }
2073
+ };
2074
+ function isRetryableTurnError(error, reason) {
2075
+ if (!(error instanceof RetryableTurnError)) {
2076
+ return false;
2077
+ }
2078
+ if (!reason) {
2079
+ return true;
2080
+ }
2081
+ return error.reason === reason;
2082
+ }
2083
+ function startActiveTurn(args) {
2084
+ args.conversation.processing.activeTurnId = args.nextTurnId;
2085
+ args.updateConversationStats(args.conversation);
2086
+ }
2087
+ function markTurnCompleted(args) {
2088
+ args.conversation.processing.activeTurnId = void 0;
2089
+ args.conversation.processing.lastCompletedAtMs = args.nowMs;
2090
+ args.updateConversationStats(args.conversation);
2091
+ }
2092
+ function markTurnFailed(args) {
2093
+ args.conversation.processing.activeTurnId = void 0;
2094
+ args.conversation.processing.lastCompletedAtMs = args.nowMs;
2095
+ args.markConversationMessage(args.conversation, args.userMessageId, {
2096
+ replied: false,
2097
+ skippedReason: "reply failed"
2098
+ });
2099
+ args.updateConversationStats(args.conversation);
2100
+ }
2101
+
2102
+ // src/chat/runtime/turn-user-message.ts
2103
+ function getTurnUserMessage(conversation, sessionId) {
2104
+ for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
2105
+ const message = conversation.messages[index];
2106
+ if (message?.role !== "user") {
2107
+ continue;
2108
+ }
2109
+ if (buildDeterministicTurnId(message.id) === sessionId) {
2110
+ return message;
2111
+ }
2112
+ }
2113
+ return void 0;
2114
+ }
2115
+ function getTurnUserMessageId(conversation, sessionId) {
2116
+ return getTurnUserMessage(conversation, sessionId)?.id;
2117
+ }
2084
2118
 
2085
2119
  // src/chat/pi/client.ts
2086
2120
  import {
@@ -2669,80 +2703,350 @@ import { Agent } from "@mariozechner/pi-agent-core";
2669
2703
  // src/chat/prompt.ts
2670
2704
  import fs from "fs";
2671
2705
  import path2 from "path";
2672
- var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
2673
- function getLoggedMarkdownFiles() {
2674
- const globalState = globalThis;
2675
- globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
2676
- return globalState.__juniorLoggedMarkdownFiles;
2677
- }
2678
- function loadOptionalMarkdownFile(candidates, fileName) {
2679
- for (const resolved of candidates) {
2680
- try {
2681
- const raw = fs.readFileSync(resolved, "utf8").trim();
2682
- if (raw.length > 0) {
2683
- const loggedMarkdownFiles = getLoggedMarkdownFiles();
2684
- const logKey = `${fileName}:${resolved}`;
2685
- if (!loggedMarkdownFiles.has(logKey)) {
2686
- loggedMarkdownFiles.add(logKey);
2687
- logInfo(
2688
- `${fileName.toLowerCase()}_loaded`,
2689
- {},
2690
- {
2691
- "file.path": resolved
2692
- },
2693
- `Loaded ${fileName}`
2694
- );
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("");
2695
2727
  }
2696
- return raw;
2697
2728
  }
2698
- } catch {
2729
+ inCodeBlock = !inCodeBlock;
2730
+ result.push(line);
2731
+ continue;
2732
+ }
2733
+ if (inCodeBlock) {
2734
+ result.push(line);
2699
2735
  continue;
2700
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);
2701
2742
  }
2702
- return null;
2743
+ return result.join("\n");
2703
2744
  }
2704
- function loadSoul() {
2705
- const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
2706
- if (soul) {
2707
- return soul;
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;
2708
2753
  }
2709
- logWarn(
2710
- "soul_load_fallback",
2711
- {},
2712
- {
2713
- "file.candidates": soulPathCandidates()
2714
- },
2715
- "SOUL.md not found; using built-in default personality"
2716
- );
2717
- return DEFAULT_SOUL;
2754
+ return text.split("\n").length;
2718
2755
  }
2719
- function loadWorld() {
2720
- return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
2756
+ function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
2757
+ return text.length <= maxChars && countSlackLines(text) <= maxLines;
2721
2758
  }
2722
- var JUNIOR_PERSONALITY = (() => {
2723
- try {
2724
- return loadSoul();
2725
- } catch (error) {
2726
- logWarn(
2727
- "soul_load_failed",
2728
- {},
2729
- {
2730
- "error.message": error instanceof Error ? error.message : String(error)
2731
- },
2732
- "Failed to load SOUL.md; using built-in default personality"
2733
- );
2734
- return DEFAULT_SOUL;
2759
+ function findSplitIndex(text, maxChars) {
2760
+ if (text.length <= maxChars) {
2761
+ return text.length;
2735
2762
  }
2736
- })();
2737
- var JUNIOR_WORLD = (() => {
2738
- try {
2739
- return loadWorld();
2740
- } catch (error) {
2741
- logWarn(
2742
- "world_load_failed",
2743
- {},
2744
- {
2745
- "error.message": error instanceof Error ? error.message : String(error)
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
2976
+ var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
2977
+ function getLoggedMarkdownFiles() {
2978
+ const globalState = globalThis;
2979
+ globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
2980
+ return globalState.__juniorLoggedMarkdownFiles;
2981
+ }
2982
+ function loadOptionalMarkdownFile(candidates, fileName) {
2983
+ for (const resolved of candidates) {
2984
+ try {
2985
+ const raw = fs.readFileSync(resolved, "utf8").trim();
2986
+ if (raw.length > 0) {
2987
+ const loggedMarkdownFiles = getLoggedMarkdownFiles();
2988
+ const logKey = `${fileName}:${resolved}`;
2989
+ if (!loggedMarkdownFiles.has(logKey)) {
2990
+ loggedMarkdownFiles.add(logKey);
2991
+ logInfo(
2992
+ `${fileName.toLowerCase()}_loaded`,
2993
+ {},
2994
+ {
2995
+ "file.path": resolved
2996
+ },
2997
+ `Loaded ${fileName}`
2998
+ );
2999
+ }
3000
+ return raw;
3001
+ }
3002
+ } catch {
3003
+ continue;
3004
+ }
3005
+ }
3006
+ return null;
3007
+ }
3008
+ function loadSoul() {
3009
+ const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
3010
+ if (soul) {
3011
+ return soul;
3012
+ }
3013
+ logWarn(
3014
+ "soul_load_fallback",
3015
+ {},
3016
+ {
3017
+ "file.candidates": soulPathCandidates()
3018
+ },
3019
+ "SOUL.md not found; using built-in default personality"
3020
+ );
3021
+ return DEFAULT_SOUL;
3022
+ }
3023
+ function loadWorld() {
3024
+ return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
3025
+ }
3026
+ var JUNIOR_PERSONALITY = (() => {
3027
+ try {
3028
+ return loadSoul();
3029
+ } catch (error) {
3030
+ logWarn(
3031
+ "soul_load_failed",
3032
+ {},
3033
+ {
3034
+ "error.message": error instanceof Error ? error.message : String(error)
3035
+ },
3036
+ "Failed to load SOUL.md; using built-in default personality"
3037
+ );
3038
+ return DEFAULT_SOUL;
3039
+ }
3040
+ })();
3041
+ var JUNIOR_WORLD = (() => {
3042
+ try {
3043
+ return loadWorld();
3044
+ } catch (error) {
3045
+ logWarn(
3046
+ "world_load_failed",
3047
+ {},
3048
+ {
3049
+ "error.message": error instanceof Error ? error.message : String(error)
2746
3050
  },
2747
3051
  "Failed to load WORLD.md; omitting world prompt context"
2748
3052
  );
@@ -3039,6 +3343,9 @@ function buildSystemPrompt(params) {
3039
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.",
3040
3344
  "- If a routine prerequisite check finds nothing notable, omit it entirely from the final reply and report only the user-relevant outcome.",
3041
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.",
3042
3349
  "- Use `attachFile` for files that actually exist in the sandbox (for example screenshots, PDFs, logs), or for `attachment_path` values returned by `imageGenerate`.",
3043
3350
  "- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
3044
3351
  "- Never claim a screenshot/file is attached unless `attachFile` succeeded in this turn.",
@@ -3055,14 +3362,14 @@ function buildSystemPrompt(params) {
3055
3362
  "- Use `slackMessageAddReaction` for rare lightweight acknowledgements. It reacts to the current inbound message via runtime context; never pick a target message yourself.",
3056
3363
  "- If the user explicitly asks for an emoji reaction instead of text, use `slackMessageAddReaction` with a Slack emoji alias name (for example `thumbsup`, `white_check_mark`, or `eyes`, not unicode emoji), and avoid redundant acknowledgment text.",
3057
3364
  "- Suggested acknowledgement reactions include `wave`, `white_check_mark`, `thumbsup`, and `eyes`, but choose what best fits the request.",
3058
- "- If a loaded skill or `loadSkill` result declares `requires_capabilities`, run `jr-rpc issue-credential <capability> [--repo <owner/repo>]` as a bash command before authenticated bash/API work for that skill.",
3365
+ "- If a loaded skill or `loadSkill` result declares `requires_capabilities`, run `jr-rpc issue-credential <capability> [--target <value>]` as a bash command before authenticated bash/API work for that skill.",
3059
3366
  "- Use the minimum declared capability needed for the current operation.",
3060
3367
  "- If `jr-rpc issue-credential` returns `oauth_started`, relay its `message` to the user and stop. The runtime will resume after authorization.",
3061
3368
  "- For disconnect + reconnect requests, run `jr-rpc delete-token <provider>` first, then `jr-rpc issue-credential` \u2014 the system handles the reconnect without auto-resuming the reconnect message.",
3062
3369
  "- Use `jr-rpc oauth-start <provider>` only when the user explicitly asks to connect a provider and there is no task to resume after authorization.",
3063
- "- GitHub capabilities need repository context, which can come from `--repo` or a configured `github.repo` default.",
3064
- "- To persist or read conversation defaults (for example `github.repo`), run `jr-rpc config get|set|unset|list ...` as a bash command.",
3065
- "- Capabilities are provider-qualified (for example `github.issues.write`).",
3370
+ "- Provider-targeted capabilities may need `--target <value>` or a provider-specific configured default target key when the provider catalog shows one.",
3371
+ "- To persist or read conversation defaults, choose a config key from the provider catalog or active skill metadata and run `jr-rpc config get|set|unset|list ...` as a bash command.",
3372
+ "- Capabilities must match the exact provider-qualified tokens declared by the loaded skill or provider catalog.",
3066
3373
  "- When your work is complete, provide the exact user-facing markdown response.",
3067
3374
  "- Do not use reaction-based progress signals; Assistants API status already covers in-progress UX.",
3068
3375
  "- Prefer `webSearch` before `webFetch` when the user gave no URL.",
@@ -3097,7 +3404,8 @@ function buildSystemPrompt(params) {
3097
3404
  "- Use plain Slack-safe markdown (headings, bullets, short code blocks).",
3098
3405
  "- Keep normal responses brief and scannable.",
3099
3406
  "- If depth is needed, start with a concise summary and then provide fuller detail.",
3100
- "- 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.",
3101
3409
  "- Avoid tables unless explicitly requested.",
3102
3410
  "- End every turn with a final user-facing markdown response.",
3103
3411
  "</output>"
@@ -3134,40 +3442,49 @@ var ProviderCredentialRouter = class {
3134
3442
  };
3135
3443
 
3136
3444
  // src/chat/capabilities/target.ts
3137
- var REPO_FLAG_RE = /(?:^|\s)--repo(?:\s+|=)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#[0-9]+)?)/;
3138
- function parseRepoTarget(value) {
3139
- const trimmed = value.trim();
3140
- if (!trimmed) {
3445
+ function escapeRegExp(value) {
3446
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3447
+ }
3448
+ function normalizeTargetValue(value) {
3449
+ let normalized = value.trim();
3450
+ if (normalized.startsWith('"') && normalized.endsWith('"') || normalized.startsWith("'") && normalized.endsWith("'")) {
3451
+ normalized = normalized.slice(1, -1).trim();
3452
+ }
3453
+ return normalized || void 0;
3454
+ }
3455
+ function extractFlagValue(text, flags) {
3456
+ if (flags.length === 0) {
3141
3457
  return void 0;
3142
3458
  }
3143
- const [repoRef] = trimmed.split("#");
3144
- const [owner, repo] = repoRef.split("/");
3145
- if (!owner || !repo) {
3459
+ const pattern = flags.map(escapeRegExp).join("|");
3460
+ const match = new RegExp(
3461
+ String.raw`(?:^|\s)(?:${pattern})(?:\s+|=)([^\s]+)`
3462
+ ).exec(text);
3463
+ return match ? normalizeTargetValue(match[1] ?? "") : void 0;
3464
+ }
3465
+ function createCapabilityTarget(type, value) {
3466
+ const normalizedType = type.trim();
3467
+ const normalizedValue = normalizeTargetValue(value);
3468
+ if (!normalizedType || !normalizedValue) {
3146
3469
  return void 0;
3147
3470
  }
3148
3471
  return {
3149
- owner: owner.toLowerCase(),
3150
- repo: repo.toLowerCase()
3472
+ type: normalizedType,
3473
+ value: normalizedValue
3151
3474
  };
3152
3475
  }
3153
- function extractRepoRef(text) {
3154
- const byFlag = REPO_FLAG_RE.exec(text);
3155
- if (byFlag) {
3156
- return parseRepoTarget(byFlag[1]);
3157
- }
3158
- return void 0;
3159
- }
3160
3476
  function extractCapabilityTarget(params) {
3477
+ const flags = params.target.commandFlags ?? [];
3161
3478
  if (params.commandText) {
3162
- const commandRepo = extractRepoRef(params.commandText);
3163
- if (commandRepo) {
3164
- return commandRepo;
3479
+ const value = extractFlagValue(params.commandText, flags);
3480
+ if (value) {
3481
+ return createCapabilityTarget(params.target.type, value);
3165
3482
  }
3166
3483
  }
3167
3484
  if (params.invocationArgs) {
3168
- const invocationRepo = extractRepoRef(params.invocationArgs);
3169
- if (invocationRepo) {
3170
- return invocationRepo;
3485
+ const value = extractFlagValue(params.invocationArgs, flags);
3486
+ if (value) {
3487
+ return createCapabilityTarget(params.target.type, value);
3171
3488
  }
3172
3489
  }
3173
3490
  return void 0;
@@ -3198,44 +3515,50 @@ var SkillCapabilityRuntime = class {
3198
3515
  }
3199
3516
  async resolveCapabilityTarget(input) {
3200
3517
  const activeSkill = input.activeSkill;
3201
- const explicitTarget = input.repoRef ? parseRepoTarget(input.repoRef) : void 0;
3518
+ const explicitTarget = input.targetRef ? createCapabilityTarget(input.target.type, input.targetRef) : void 0;
3202
3519
  if (explicitTarget) {
3203
3520
  return explicitTarget;
3204
3521
  }
3205
3522
  const inferredTarget = extractCapabilityTarget({
3206
- invocationArgs: this.invocationArgs
3523
+ invocationArgs: this.invocationArgs,
3524
+ target: input.target
3207
3525
  });
3208
3526
  if (inferredTarget) {
3209
3527
  return inferredTarget;
3210
3528
  }
3211
- if (!input.configKey || !this.resolveConfiguration) {
3529
+ if (!this.resolveConfiguration) {
3212
3530
  return void 0;
3213
3531
  }
3214
- const configuredRepo = await this.resolveConfiguration(input.configKey);
3215
- if (typeof configuredRepo !== "string" || configuredRepo.trim().length === 0) {
3532
+ const configuredValue = await this.resolveConfiguration(
3533
+ input.target.configKey
3534
+ );
3535
+ if (typeof configuredValue !== "string" || configuredValue.trim().length === 0) {
3216
3536
  return void 0;
3217
3537
  }
3218
- const configuredTarget = parseRepoTarget(configuredRepo);
3538
+ const configuredTarget = createCapabilityTarget(
3539
+ input.target.type,
3540
+ configuredValue
3541
+ );
3219
3542
  if (!configuredTarget) {
3220
3543
  logWarn(
3221
3544
  "config_value_invalid_for_capability_target",
3222
3545
  {},
3223
3546
  {
3224
3547
  "app.skill.name": activeSkill?.name,
3225
- "app.config.key": input.configKey
3548
+ "app.config.key": input.target.configKey
3226
3549
  },
3227
- `Configured ${input.configKey} is invalid for capability target resolution`
3550
+ `Configured ${input.target.configKey} is invalid for capability target resolution`
3228
3551
  );
3229
3552
  return void 0;
3230
3553
  }
3231
3554
  const declaredConfig = activeSkill?.usesConfig ?? [];
3232
- if (activeSkill && !declaredConfig.includes(input.configKey)) {
3555
+ if (activeSkill && !declaredConfig.includes(input.target.configKey)) {
3233
3556
  logWarn(
3234
3557
  "config_key_not_declared_for_skill",
3235
3558
  {},
3236
3559
  {
3237
3560
  "app.skill.name": activeSkill.name,
3238
- "app.config.key": input.configKey
3561
+ "app.config.key": input.target.configKey
3239
3562
  },
3240
3563
  "Configuration key used by runtime is not declared in active skill frontmatter (soft enforcement)"
3241
3564
  );
@@ -3243,9 +3566,7 @@ var SkillCapabilityRuntime = class {
3243
3566
  return configuredTarget;
3244
3567
  }
3245
3568
  capabilityCacheKey(capability, target) {
3246
- const owner = target?.owner?.trim().toLowerCase();
3247
- const repo = target?.repo?.trim().toLowerCase();
3248
- const scope = owner && repo ? `${owner}/${repo}` : "none";
3569
+ const scope = target ? `${target.type}:${target.value.trim()}` : "none";
3249
3570
  return `${capability}:${scope}`;
3250
3571
  }
3251
3572
  async issueCapabilityLease(input) {
@@ -3256,10 +3577,10 @@ var SkillCapabilityRuntime = class {
3256
3577
  );
3257
3578
  }
3258
3579
  const activeSkill = input.activeSkill;
3259
- const target = capabilityProvider.target?.type === "repo" ? await this.resolveCapabilityTarget({
3580
+ const target = capabilityProvider.target ? await this.resolveCapabilityTarget({
3260
3581
  activeSkill,
3261
- repoRef: input.repoRef,
3262
- configKey: capabilityProvider.target.configKey
3582
+ target: capabilityProvider.target,
3583
+ targetRef: input.targetRef
3263
3584
  }) : void 0;
3264
3585
  return await this.router.issue({
3265
3586
  capability: input.capability,
@@ -3294,14 +3615,14 @@ var SkillCapabilityRuntime = class {
3294
3615
  );
3295
3616
  }
3296
3617
  const activeSkill = input.activeSkill;
3297
- const capabilityTarget = capabilityProvider.target?.type === "repo" ? await this.resolveCapabilityTarget({
3618
+ const capabilityTarget = capabilityProvider.target ? await this.resolveCapabilityTarget({
3298
3619
  activeSkill,
3299
- repoRef: input.repoRef,
3300
- configKey: capabilityProvider.target.configKey
3620
+ target: capabilityProvider.target,
3621
+ targetRef: input.targetRef
3301
3622
  }) : void 0;
3302
- if (capabilityProvider.target?.type === "repo" && (!capabilityTarget?.owner || !capabilityTarget?.repo)) {
3623
+ if (capabilityProvider.target && !capabilityTarget?.value.trim()) {
3303
3624
  throw new Error(
3304
- "jr-rpc issue-credential requires repository context; use --repo <owner/repo>"
3625
+ `jr-rpc issue-credential requires ${capabilityProvider.target.type} target context; use --target <value>`
3305
3626
  );
3306
3627
  }
3307
3628
  const declared = activeSkill?.requiresCapabilities ?? [];
@@ -3338,7 +3659,7 @@ var SkillCapabilityRuntime = class {
3338
3659
  const lease = await this.issueCapabilityLease({
3339
3660
  activeSkill,
3340
3661
  capability,
3341
- repoRef: input.repoRef,
3662
+ targetRef: input.targetRef,
3342
3663
  reason: input.reason
3343
3664
  });
3344
3665
  const transforms = this.toHeaderTransforms(lease);
@@ -3465,7 +3786,7 @@ var TestCredentialBroker = class {
3465
3786
  expiresAt,
3466
3787
  metadata: {
3467
3788
  reason: input.reason,
3468
- target: input.target?.owner && input.target?.repo ? `${input.target.owner}/${input.target.repo}` : "none"
3789
+ target: input.target ? `${input.target.type}:${input.target.value}` : "none"
3469
3790
  }
3470
3791
  };
3471
3792
  }
@@ -3568,24 +3889,24 @@ async function handleIssueCredentialCommand(args, deps) {
3568
3889
  exitCode: 2
3569
3890
  });
3570
3891
  }
3571
- let repoRef;
3892
+ let targetRef;
3572
3893
  const extras = args.slice(1);
3573
3894
  if (extras.length > 0) {
3574
- if (extras.length === 2 && extras[0] === "--repo") {
3575
- repoRef = extras[1];
3576
- } else if (extras.length === 1 && extras[0].startsWith("--repo=")) {
3577
- repoRef = extras[0].slice("--repo=".length);
3895
+ if (extras.length === 2 && extras[0] === "--target") {
3896
+ targetRef = extras[1]?.trim();
3897
+ } else if (extras.length === 1 && extras[0].startsWith("--target=")) {
3898
+ targetRef = extras[0].slice("--target=".length).trim();
3578
3899
  } else {
3579
3900
  return {
3580
3901
  stdout: "",
3581
- stderr: "jr-rpc issue-credential requires exactly one capability argument and optional --repo <owner/repo>\n",
3902
+ stderr: "jr-rpc issue-credential requires exactly one capability argument and optional --target <value>\n",
3582
3903
  exitCode: 2
3583
3904
  };
3584
3905
  }
3585
- if (!parseRepoTarget(repoRef ?? "")) {
3906
+ if (!targetRef) {
3586
3907
  return {
3587
3908
  stdout: "",
3588
- stderr: "jr-rpc issue-credential --repo must be in owner/repo format\n",
3909
+ stderr: "jr-rpc issue-credential --target requires a non-empty value\n",
3589
3910
  exitCode: 2
3590
3911
  };
3591
3912
  }
@@ -3595,7 +3916,7 @@ async function handleIssueCredentialCommand(args, deps) {
3595
3916
  outcome = await deps.capabilityRuntime.enableCapabilityForTurn({
3596
3917
  activeSkill: deps.activeSkill,
3597
3918
  capability,
3598
- ...repoRef ? { repoRef } : {},
3919
+ ...targetRef ? { targetRef } : {},
3599
3920
  reason: `skill:${deps.activeSkill?.name ?? "unknown"}:jr-rpc:issue-credential`
3600
3921
  });
3601
3922
  } catch (error) {
@@ -3985,7 +4306,7 @@ async function handleDeleteTokenCommand(args, deps) {
3985
4306
  function createJrRpcCommand(deps) {
3986
4307
  return defineCommand("jr-rpc", async (args) => {
3987
4308
  const usage = [
3988
- "jr-rpc issue-credential <capability> [--repo <owner/repo>]",
4309
+ "jr-rpc issue-credential <capability> [--target <value>]",
3989
4310
  "jr-rpc oauth-start <provider>",
3990
4311
  "jr-rpc delete-token <provider>",
3991
4312
  "jr-rpc config get <key>",
@@ -5778,7 +6099,7 @@ function createSlackCanvasCreateTool(context, state) {
5778
6099
  channelId: targetChannelId
5779
6100
  });
5780
6101
  state.setTurnCreatedCanvasId(created.canvasId);
5781
- state.patchArtifactState({
6102
+ await state.patchArtifactState({
5782
6103
  lastCanvasId: created.canvasId,
5783
6104
  lastCanvasUrl: created.permalink,
5784
6105
  recentCanvases: mergeRecentCanvases(
@@ -5877,7 +6198,7 @@ function createSlackCanvasUpdateTool(state, _context) {
5877
6198
  operation: resolvedOperation,
5878
6199
  sectionId
5879
6200
  });
5880
- state.patchArtifactState({ lastCanvasId: targetCanvasId });
6201
+ await state.patchArtifactState({ lastCanvasId: targetCanvasId });
5881
6202
  const response = {
5882
6203
  ok: true,
5883
6204
  canvas_id: targetCanvasId,
@@ -6082,7 +6403,7 @@ function createSlackListCreateTool(state) {
6082
6403
  };
6083
6404
  }
6084
6405
  const list = await createTodoList(name);
6085
- state.patchArtifactState({
6406
+ await state.patchArtifactState({
6086
6407
  lastListId: list.listId,
6087
6408
  lastListUrl: list.permalink,
6088
6409
  listColumnMap: list.listColumnMap
@@ -6145,7 +6466,7 @@ function createSlackListAddItemsTool(state) {
6145
6466
  assigneeUserId: assignee_user_id,
6146
6467
  dueDate: due_date
6147
6468
  });
6148
- state.patchArtifactState({
6469
+ await state.patchArtifactState({
6149
6470
  lastListId: targetListId,
6150
6471
  listColumnMap: result.listColumnMap
6151
6472
  });
@@ -6237,7 +6558,7 @@ function createSlackListUpdateItemTool(state) {
6237
6558
  title,
6238
6559
  listColumnMap: state.artifactState.listColumnMap ?? {}
6239
6560
  });
6240
- state.patchArtifactState({ lastListId: targetListId });
6561
+ await state.patchArtifactState({ lastListId: targetListId });
6241
6562
  const response = {
6242
6563
  ok: true,
6243
6564
  list_id: targetListId,
@@ -6838,7 +7159,7 @@ function createToolState(hooks, context) {
6838
7159
  ...context.artifactState?.listColumnMap ?? {}
6839
7160
  }
6840
7161
  };
6841
- const patchArtifactState = (patch) => {
7162
+ const patchArtifactState = async (patch) => {
6842
7163
  Object.assign(artifactState, patch);
6843
7164
  if (patch.listColumnMap) {
6844
7165
  artifactState.listColumnMap = {
@@ -6846,7 +7167,7 @@ function createToolState(hooks, context) {
6846
7167
  ...patch.listColumnMap
6847
7168
  };
6848
7169
  }
6849
- hooks.onArtifactStatePatch?.(patch);
7170
+ await hooks.onArtifactStatePatch?.(patch);
6850
7171
  };
6851
7172
  return {
6852
7173
  artifactState,
@@ -7955,10 +8276,14 @@ function createSandboxSessionManager(options) {
7955
8276
  sandboxIdHint = void 0;
7956
8277
  toolExecutors = void 0;
7957
8278
  };
7958
- const rememberSandbox = (nextSandbox) => {
8279
+ const rememberSandbox = async (nextSandbox) => {
7959
8280
  sandbox = nextSandbox;
7960
8281
  sandboxIdHint = nextSandbox.sandboxId;
7961
8282
  toolExecutors = void 0;
8283
+ await options?.onSandboxAcquired?.({
8284
+ sandboxId: nextSandbox.sandboxId,
8285
+ ...dependencyProfileHash ? { sandboxDependencyProfileHash: dependencyProfileHash } : {}
8286
+ });
7962
8287
  return nextSandbox;
7963
8288
  };
7964
8289
  const failSetup = (error) => {
@@ -8148,7 +8473,7 @@ function createSandboxSessionManager(options) {
8148
8473
  } catch (error) {
8149
8474
  return failSetup(error);
8150
8475
  }
8151
- return rememberSandbox(createdSandbox);
8476
+ return await rememberSandbox(createdSandbox);
8152
8477
  };
8153
8478
  const discardHintIfProfileChanged = () => {
8154
8479
  if (sandbox || !sandboxIdHint || dependencyProfileHash === options?.sandboxDependencyProfileHash) {
@@ -8203,7 +8528,7 @@ function createSandboxSessionManager(options) {
8203
8528
  }
8204
8529
  try {
8205
8530
  await syncSkills(hintedSandbox);
8206
- return rememberSandbox(hintedSandbox);
8531
+ return await rememberSandbox(hintedSandbox);
8207
8532
  } catch (error) {
8208
8533
  if (isSandboxUnavailableError(error)) {
8209
8534
  return await recreateUnavailableSandbox("id_hint");
@@ -8450,7 +8775,8 @@ function createSandboxExecutor(options) {
8450
8775
  sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
8451
8776
  timeoutMs: options?.timeoutMs,
8452
8777
  traceContext,
8453
- onStatus: options?.onStatus
8778
+ onStatus: options?.onStatus,
8779
+ onSandboxAcquired: options?.onSandboxAcquired
8454
8780
  });
8455
8781
  const withSandboxSpan = (name, op, attributes, callback) => withSpan(name, op, traceContext, callback, attributes);
8456
8782
  const logSandboxBootRequest = (trigger, details = {}) => {
@@ -8699,13 +9025,13 @@ function buildToolStatus(toolName, input) {
8699
9025
  return makeAssistantStatus("loading", skillName);
8700
9026
  }
8701
9027
  if (query && toolName === "webSearch") {
8702
- return makeAssistantStatus("searching", `"${query}"`);
8703
- }
8704
- if (query && provider && toolName === "searchTools") {
8705
- return makeAssistantStatus("searching", `${provider} "${query}"`);
9028
+ return makeAssistantStatus("searching", "sources");
8706
9029
  }
8707
9030
  if (query && toolName === "searchTools") {
8708
- return makeAssistantStatus("searching", `"${query}"`);
9031
+ return makeAssistantStatus(
9032
+ "searching",
9033
+ provider ? `${provider} tools` : "tools"
9034
+ );
8709
9035
  }
8710
9036
  if (domain && toolName === "webFetch") {
8711
9037
  return makeAssistantStatus("fetching", domain);
@@ -8964,66 +9290,6 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
8964
9290
  }));
8965
9291
  }
8966
9292
 
8967
- // src/chat/runtime/turn.ts
8968
- function buildDeterministicTurnId(messageId) {
8969
- const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
8970
- return `turn_${sanitized}`;
8971
- }
8972
- var RetryableTurnError = class extends Error {
8973
- code = "retryable_turn";
8974
- reason;
8975
- constructor(reason, message) {
8976
- super(message);
8977
- this.name = "RetryableTurnError";
8978
- this.reason = reason;
8979
- }
8980
- };
8981
- function isRetryableTurnError(error, reason) {
8982
- if (!(error instanceof RetryableTurnError)) {
8983
- return false;
8984
- }
8985
- if (!reason) {
8986
- return true;
8987
- }
8988
- return error.reason === reason;
8989
- }
8990
- function startActiveTurn(args) {
8991
- args.conversation.processing.activeTurnId = args.nextTurnId;
8992
- args.updateConversationStats(args.conversation);
8993
- }
8994
- function markTurnCompleted(args) {
8995
- args.conversation.processing.activeTurnId = void 0;
8996
- args.conversation.processing.lastCompletedAtMs = args.nowMs;
8997
- args.updateConversationStats(args.conversation);
8998
- }
8999
- function markTurnFailed(args) {
9000
- args.conversation.processing.activeTurnId = void 0;
9001
- args.conversation.processing.lastCompletedAtMs = args.nowMs;
9002
- args.markConversationMessage(args.conversation, args.userMessageId, {
9003
- replied: false,
9004
- skippedReason: "reply failed"
9005
- });
9006
- args.updateConversationStats(args.conversation);
9007
- }
9008
- function resolveReplyDelivery(args) {
9009
- const replyHasFiles = Boolean(
9010
- args.reply.files && args.reply.files.length > 0
9011
- );
9012
- const deliveryPlan = args.reply.deliveryPlan ?? {
9013
- mode: args.reply.deliveryMode ?? "thread",
9014
- postThreadText: (args.reply.deliveryMode ?? "thread") !== "channel_only",
9015
- attachFiles: replyHasFiles ? args.hasStreamedThreadReply ? "followup" : "inline" : "none"
9016
- };
9017
- let attachFiles = replyHasFiles ? deliveryPlan.attachFiles : "none";
9018
- if (attachFiles === "followup" && !args.hasStreamedThreadReply) {
9019
- attachFiles = "inline";
9020
- }
9021
- return {
9022
- shouldPostThreadReply: deliveryPlan.postThreadText,
9023
- attachFiles
9024
- };
9025
- }
9026
-
9027
9293
  // src/chat/services/reply-delivery-plan.ts
9028
9294
  var REACTION_ONLY_ACK_RE = /^(?::[a-z0-9_+-]+:|[\p{Extended_Pictographic}\uFE0F\u200D]+)$/u;
9029
9295
  var REDUNDANT_REACTION_ACK_TEXT = ["done", "got it", "ok", "okay"];
@@ -9069,8 +9335,29 @@ function buildReplyDeliveryPlan(args) {
9069
9335
  attachFiles
9070
9336
  };
9071
9337
  }
9072
-
9073
- // src/chat/services/channel-intent.ts
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
9074
9361
  function isExplicitChannelPostIntent(text) {
9075
9362
  if (!/\bchannel\b/i.test(text)) {
9076
9363
  return false;
@@ -9372,6 +9659,50 @@ async function persistAuthPauseCheckpoint(args) {
9372
9659
  }
9373
9660
  return nextSliceId;
9374
9661
  }
9662
+ async function persistTimeoutCheckpoint(args) {
9663
+ const nextSliceId = args.currentSliceId + 1;
9664
+ try {
9665
+ const latestCheckpoint = await getAgentTurnSessionCheckpoint(
9666
+ args.conversationId,
9667
+ args.sessionId
9668
+ );
9669
+ const piMessages = trimTrailingAssistantMessages(
9670
+ args.messages.length > 0 ? args.messages : latestCheckpoint?.piMessages ?? []
9671
+ );
9672
+ return await upsertAgentTurnSessionCheckpoint({
9673
+ conversationId: args.conversationId,
9674
+ sessionId: args.sessionId,
9675
+ sliceId: nextSliceId,
9676
+ state: "awaiting_resume",
9677
+ piMessages,
9678
+ loadedSkillNames: args.loadedSkillNames,
9679
+ resumeReason: "timeout",
9680
+ resumedFromSliceId: args.currentSliceId,
9681
+ errorMessage: args.errorMessage
9682
+ });
9683
+ } catch (checkpointError) {
9684
+ logException(
9685
+ checkpointError,
9686
+ "agent_turn_timeout_resume_checkpoint_failed",
9687
+ {
9688
+ slackThreadId: args.logContext.threadId,
9689
+ slackUserId: args.logContext.requesterId,
9690
+ slackChannelId: args.logContext.channelId,
9691
+ runId: args.logContext.runId,
9692
+ assistantUserName: args.logContext.assistantUserName,
9693
+ modelId: args.logContext.modelId
9694
+ },
9695
+ {
9696
+ "app.ai.resume_conversation_id": args.conversationId,
9697
+ "app.ai.resume_session_id": args.sessionId,
9698
+ "app.ai.resume_from_slice_id": args.currentSliceId,
9699
+ "app.ai.resume_next_slice_id": nextSliceId
9700
+ },
9701
+ "Failed to persist timeout checkpoint before scheduling resume"
9702
+ );
9703
+ return void 0;
9704
+ }
9705
+ }
9375
9706
 
9376
9707
  // src/chat/services/mcp-auth-orchestration.ts
9377
9708
  var McpAuthorizationPauseError = class extends Error {
@@ -9462,21 +9793,6 @@ function mcpToolsToDefinitions(mcpTools) {
9462
9793
  }
9463
9794
  return defs;
9464
9795
  }
9465
- async function maybeReplaceAgentMessages(agent, messages) {
9466
- const resumable = agent;
9467
- if (typeof resumable.replaceMessages !== "function") {
9468
- return false;
9469
- }
9470
- await resumable.replaceMessages(messages);
9471
- return true;
9472
- }
9473
- async function runAgentContinuation(agent) {
9474
- const resumable = agent;
9475
- if (typeof resumable.continue !== "function") {
9476
- throw new Error("Agent continuation is unavailable in this runtime");
9477
- }
9478
- return await resumable.continue();
9479
- }
9480
9796
  async function generateAssistantReply(messageText, context = {}) {
9481
9797
  let timeoutResumeConversationId;
9482
9798
  let timeoutResumeSessionId;
@@ -9487,6 +9803,7 @@ async function generateAssistantReply(messageText, context = {}) {
9487
9803
  let loadedSkillNamesForResume = [];
9488
9804
  let mcpToolManager;
9489
9805
  let sandboxExecutor;
9806
+ let timedOut = false;
9490
9807
  const getSandboxMetadata = () => sandboxExecutor ? {
9491
9808
  sandboxId: sandboxExecutor.getSandboxId(),
9492
9809
  sandboxDependencyProfileHash: sandboxExecutor.getDependencyProfileHash()
@@ -9529,9 +9846,8 @@ async function generateAssistantReply(messageText, context = {}) {
9529
9846
  "Discovered startup SOUL/skills/plugins"
9530
9847
  );
9531
9848
  }
9532
- const configurationValues = {
9533
- ...context.configuration ?? {}
9534
- };
9849
+ let baseInstructions = "";
9850
+ let configurationValues;
9535
9851
  const userInput = messageText;
9536
9852
  if (shouldTrace) {
9537
9853
  logInfo(
@@ -9560,6 +9876,11 @@ async function generateAssistantReply(messageText, context = {}) {
9560
9876
  timeoutResumeConversationId = sessionConversationId;
9561
9877
  timeoutResumeSessionId = sessionId;
9562
9878
  timeoutResumeSliceId = currentSliceId;
9879
+ const persistedConfigurationValues = context.channelConfiguration ? await context.channelConfiguration.resolveValues() : {};
9880
+ configurationValues = {
9881
+ ...context.configuration ?? {},
9882
+ ...persistedConfigurationValues
9883
+ };
9563
9884
  const capabilityRuntime = createSkillCapabilityRuntime({
9564
9885
  invocationArgs: skillInvocation?.args,
9565
9886
  requesterId: context.requester?.userId,
@@ -9571,6 +9892,11 @@ async function generateAssistantReply(messageText, context = {}) {
9571
9892
  sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
9572
9893
  traceContext: spanContext,
9573
9894
  onStatus: context.onStatus,
9895
+ onSandboxAcquired: async (sandbox2) => {
9896
+ lastKnownSandboxId = sandbox2.sandboxId;
9897
+ lastKnownSandboxDependencyProfileHash = sandbox2.sandboxDependencyProfileHash;
9898
+ await context.onSandboxAcquired?.(sandbox2);
9899
+ },
9574
9900
  runBashCustomCommand: async (command) => {
9575
9901
  const result = await maybeExecuteJrRpcCustomCommand(command, {
9576
9902
  capabilityRuntime,
@@ -9710,8 +10036,14 @@ async function generateAssistantReply(messageText, context = {}) {
9710
10036
  onGeneratedFiles: (files) => {
9711
10037
  replyFiles.push(...files);
9712
10038
  },
9713
- onArtifactStatePatch: (patch) => {
10039
+ onArtifactStatePatch: async (patch) => {
9714
10040
  Object.assign(artifactStatePatch, patch);
10041
+ await context.onArtifactStateUpdated?.(
10042
+ mergeArtifactsState(
10043
+ context.artifactState ?? {},
10044
+ artifactStatePatch
10045
+ )
10046
+ );
9715
10047
  },
9716
10048
  toolOverrides: context.toolOverrides,
9717
10049
  onSkillLoaded: async (loadedSkill) => {
@@ -9761,7 +10093,7 @@ async function generateAssistantReply(messageText, context = {}) {
9761
10093
  }
9762
10094
  syncResumeState();
9763
10095
  const activeToolSummaries = turnMcpToolManager.getActiveToolCatalog(activeSkills).map(toExposedToolSummary);
9764
- const baseInstructions = buildSystemPrompt({
10096
+ baseInstructions = buildSystemPrompt({
9765
10097
  availableSkills,
9766
10098
  activeSkills,
9767
10099
  activeTools: activeToolSummaries,
@@ -9821,6 +10153,17 @@ async function generateAssistantReply(messageText, context = {}) {
9821
10153
  const agentToolHooks = {
9822
10154
  onToolCall: (toolName) => {
9823
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
+ });
9824
10167
  }
9825
10168
  };
9826
10169
  const baseAgentTools = createAgentTools(
@@ -9861,6 +10204,16 @@ async function generateAssistantReply(messageText, context = {}) {
9861
10204
  let needsSeparator = false;
9862
10205
  const unsubscribe = agent.subscribe((event) => {
9863
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
+ });
9864
10217
  if (hasEmittedText) {
9865
10218
  needsSeparator = true;
9866
10219
  }
@@ -9889,15 +10242,7 @@ async function generateAssistantReply(messageText, context = {}) {
9889
10242
  let completedAssistantTurn = false;
9890
10243
  try {
9891
10244
  if (resumedFromCheckpoint) {
9892
- const didReplace = await maybeReplaceAgentMessages(
9893
- agent,
9894
- existingCheckpoint.piMessages
9895
- );
9896
- if (!didReplace) {
9897
- throw new Error(
9898
- "Agent session resume requested but replaceMessages is unavailable"
9899
- );
9900
- }
10245
+ agent.replaceMessages(existingCheckpoint.piMessages);
9901
10246
  }
9902
10247
  beforeMessageCount = agent.state.messages.length;
9903
10248
  await withSpan(
@@ -9906,16 +10251,15 @@ async function generateAssistantReply(messageText, context = {}) {
9906
10251
  spanContext,
9907
10252
  async () => {
9908
10253
  let promptResult;
9909
- const promptPromise = resumedFromCheckpoint ? runAgentContinuation(agent) : agent.prompt({
10254
+ const promptPromise = resumedFromCheckpoint ? agent.continue() : agent.prompt({
9910
10255
  role: "user",
9911
10256
  content: userContentParts,
9912
10257
  timestamp: Date.now()
9913
10258
  });
9914
10259
  let timeoutId;
9915
- let didTimeout = false;
9916
10260
  const timeoutPromise = new Promise((_, reject) => {
9917
10261
  timeoutId = setTimeout(() => {
9918
- didTimeout = true;
10262
+ timedOut = true;
9919
10263
  agent.abort();
9920
10264
  reject(
9921
10265
  new Error(
@@ -9927,7 +10271,7 @@ async function generateAssistantReply(messageText, context = {}) {
9927
10271
  try {
9928
10272
  promptResult = await Promise.race([promptPromise, timeoutPromise]);
9929
10273
  } catch (error) {
9930
- if (didTimeout) {
10274
+ if (timedOut) {
9931
10275
  logWarn(
9932
10276
  "agent_turn_timeout",
9933
10277
  {},
@@ -9953,9 +10297,7 @@ async function generateAssistantReply(messageText, context = {}) {
9953
10297
  clearTimeout(timeoutId);
9954
10298
  }
9955
10299
  }
9956
- newMessages = agent.state.messages.slice(
9957
- beforeMessageCount
9958
- );
10300
+ newMessages = agent.state.messages.slice(beforeMessageCount);
9959
10301
  completedAssistantTurn = hasCompletedAssistantTurn(newMessages);
9960
10302
  if (mcpAuth.getPendingPause() && !completedAssistantTurn) {
9961
10303
  timeoutResumeMessages = [...agent.state.messages];
@@ -10011,6 +10353,36 @@ async function generateAssistantReply(messageText, context = {}) {
10011
10353
  assistantUserName: context.assistant?.userName
10012
10354
  });
10013
10355
  } catch (error) {
10356
+ if (timedOut && timeoutResumeConversationId && timeoutResumeSessionId) {
10357
+ const checkpoint = await persistTimeoutCheckpoint({
10358
+ conversationId: timeoutResumeConversationId,
10359
+ sessionId: timeoutResumeSessionId,
10360
+ currentSliceId: timeoutResumeSliceId,
10361
+ messages: timeoutResumeMessages,
10362
+ loadedSkillNames: loadedSkillNamesForResume,
10363
+ errorMessage: error instanceof Error ? error.message : String(error),
10364
+ logContext: {
10365
+ threadId: context.correlation?.threadId,
10366
+ requesterId: context.correlation?.requesterId,
10367
+ channelId: context.correlation?.channelId,
10368
+ runId: context.correlation?.runId,
10369
+ assistantUserName: context.assistant?.userName,
10370
+ modelId: botConfig.modelId
10371
+ }
10372
+ });
10373
+ if (checkpoint) {
10374
+ throw new RetryableTurnError(
10375
+ "turn_timeout_resume",
10376
+ `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${checkpoint.sliceId} version=${checkpoint.checkpointVersion}`,
10377
+ {
10378
+ conversationId: timeoutResumeConversationId,
10379
+ sessionId: timeoutResumeSessionId,
10380
+ sliceId: checkpoint.sliceId,
10381
+ checkpointVersion: checkpoint.checkpointVersion
10382
+ }
10383
+ );
10384
+ }
10385
+ }
10014
10386
  if (error instanceof McpAuthorizationPauseError && timeoutResumeConversationId && timeoutResumeSessionId) {
10015
10387
  const nextSliceId = await persistAuthPauseCheckpoint({
10016
10388
  conversationId: timeoutResumeConversationId,
@@ -10030,7 +10402,12 @@ async function generateAssistantReply(messageText, context = {}) {
10030
10402
  });
10031
10403
  throw new RetryableTurnError(
10032
10404
  "mcp_auth_resume",
10033
- `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`
10405
+ `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`,
10406
+ {
10407
+ conversationId: timeoutResumeConversationId,
10408
+ sessionId: timeoutResumeSessionId,
10409
+ sliceId: nextSliceId
10410
+ }
10034
10411
  );
10035
10412
  }
10036
10413
  if (isRetryableTurnError(error)) {
@@ -10228,7 +10605,194 @@ function createProgressReporter(args) {
10228
10605
  };
10229
10606
  }
10230
10607
 
10231
- // 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
10232
10796
  function resolveReplyTimeoutMs(explicitTimeoutMs) {
10233
10797
  if (typeof explicitTimeoutMs === "number" && explicitTimeoutMs > 0) {
10234
10798
  return explicitTimeoutMs;
@@ -10241,12 +10805,15 @@ function resolveReplyTimeoutMs(explicitTimeoutMs) {
10241
10805
  return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
10242
10806
  }
10243
10807
  async function postSlackMessage(channelId, threadTs, text) {
10808
+ await getSlackClient().chat.postMessage({
10809
+ channel: channelId,
10810
+ thread_ts: threadTs,
10811
+ text
10812
+ });
10813
+ }
10814
+ async function postSlackMessageBestEffort(channelId, threadTs, text) {
10244
10815
  try {
10245
- await getSlackClient().chat.postMessage({
10246
- channel: channelId,
10247
- thread_ts: threadTs,
10248
- text
10249
- });
10816
+ await postSlackMessage(channelId, threadTs, text);
10250
10817
  } catch {
10251
10818
  }
10252
10819
  }
@@ -10276,32 +10843,85 @@ function createReadOnlyConfigService(values) {
10276
10843
  }
10277
10844
  };
10278
10845
  }
10279
- async function resumeAuthorizedRequest(args) {
10846
+ var ResumeTurnBusyError = class extends Error {
10847
+ constructor(lockKey) {
10848
+ super(`A turn already owns resume lock "${lockKey}"`);
10849
+ this.name = "ResumeTurnBusyError";
10850
+ }
10851
+ };
10852
+ function getDefaultLockKey(channelId, threadTs) {
10853
+ return `slack:${channelId}:${threadTs}`;
10854
+ }
10855
+ function createResumeReplyContext(args, progress) {
10856
+ const replyContext = args.replyContext ?? {};
10857
+ const threadId = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
10858
+ const persistedChannelConfiguration = replyContext.channelConfiguration ?? (replyContext.configuration ? createReadOnlyConfigService(replyContext.configuration) : void 0);
10859
+ return {
10860
+ ...replyContext,
10861
+ assistant: {
10862
+ userName: botConfig.userName,
10863
+ ...replyContext.assistant
10864
+ },
10865
+ correlation: {
10866
+ ...replyContext.correlation,
10867
+ threadId: replyContext.correlation?.threadId ?? threadId,
10868
+ channelId: replyContext.correlation?.channelId ?? args.channelId,
10869
+ threadTs: replyContext.correlation?.threadTs ?? args.threadTs,
10870
+ requesterId: replyContext.correlation?.requesterId ?? replyContext.requester?.userId
10871
+ },
10872
+ channelConfiguration: persistedChannelConfiguration,
10873
+ onSandboxAcquired: async (sandbox) => {
10874
+ await persistThreadStateById(threadId, {
10875
+ sandboxId: sandbox.sandboxId,
10876
+ sandboxDependencyProfileHash: sandbox.sandboxDependencyProfileHash
10877
+ });
10878
+ await replyContext.onSandboxAcquired?.(sandbox);
10879
+ },
10880
+ onArtifactStateUpdated: async (artifacts) => {
10881
+ await persistThreadStateById(threadId, { artifacts });
10882
+ await replyContext.onArtifactStateUpdated?.(artifacts);
10883
+ },
10884
+ onStatus: async (status) => {
10885
+ await progress.setStatus(status);
10886
+ await replyContext.onStatus?.(status);
10887
+ }
10888
+ };
10889
+ }
10890
+ async function resumeSlackTurn(args) {
10891
+ const requesterUserId = args.replyContext?.requester?.userId;
10892
+ if (!requesterUserId) {
10893
+ throw new Error("Resumed turn requires replyContext.requester.userId");
10894
+ }
10895
+ const stateAdapter = getStateAdapter();
10896
+ await stateAdapter.connect();
10897
+ const lockKey = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
10898
+ const lock = await stateAdapter.acquireLock(
10899
+ lockKey,
10900
+ botConfig.turnTimeoutMs + 6e4
10901
+ );
10902
+ if (!lock) {
10903
+ throw new ResumeTurnBusyError(lockKey);
10904
+ }
10280
10905
  const progress = createProgressReporter({
10281
10906
  channelId: args.channelId,
10282
10907
  threadTs: args.threadTs,
10283
10908
  transport: createSlackWebApiAssistantStatusTransport()
10284
10909
  });
10285
- await postSlackMessage(args.channelId, args.threadTs, args.connectedText);
10286
- await progress.start();
10910
+ let deferredPauseHandler;
10911
+ let deferredFailureHandler;
10287
10912
  try {
10913
+ if (args.initialText) {
10914
+ await postSlackMessageBestEffort(
10915
+ args.channelId,
10916
+ args.threadTs,
10917
+ args.initialText
10918
+ );
10919
+ }
10920
+ await progress.start();
10288
10921
  const generateReply = args.generateReply ?? generateAssistantReply;
10922
+ const replyContext = createResumeReplyContext(args, progress);
10289
10923
  const replyPromise = generateReply(args.messageText, {
10290
- assistant: { userName: botConfig.userName },
10291
- requester: { userId: args.requesterUserId },
10292
- correlation: {
10293
- conversationId: args.correlation?.conversationId,
10294
- turnId: args.correlation?.turnId,
10295
- channelId: args.correlation?.channelId ?? args.channelId,
10296
- threadTs: args.correlation?.threadTs ?? args.threadTs,
10297
- requesterId: args.correlation?.requesterId ?? args.requesterUserId
10298
- },
10299
- toolChannelId: args.toolChannelId,
10300
- conversationContext: args.conversationContext,
10301
- artifactState: args.artifactState,
10302
- configuration: args.configuration,
10303
- channelConfiguration: args.configuration ? createReadOnlyConfigService(args.configuration) : void 0,
10304
- onStatus: (status) => progress.setStatus(status)
10924
+ ...replyContext
10305
10925
  });
10306
10926
  const replyTimeoutMs = resolveReplyTimeoutMs(args.replyTimeoutMs);
10307
10927
  const reply = typeof replyTimeoutMs === "number" ? await Promise.race([
@@ -10318,20 +10938,180 @@ async function resumeAuthorizedRequest(args) {
10318
10938
  )
10319
10939
  ]) : await replyPromise;
10320
10940
  await progress.stop();
10321
- if (args.onReply) {
10322
- await args.onReply(reply);
10323
- } else if (reply.text) {
10324
- await postSlackMessage(args.channelId, args.threadTs, reply.text);
10325
- }
10941
+ await postSlackApiReplyPosts({
10942
+ channelId: args.channelId,
10943
+ threadTs: args.threadTs,
10944
+ posts: planSlackReplyPosts({
10945
+ reply,
10946
+ hasStreamedThreadReply: false
10947
+ }),
10948
+ postMessage: postSlackMessage
10949
+ });
10326
10950
  await args.onSuccess?.(reply);
10327
10951
  } catch (error) {
10328
10952
  await progress.stop();
10329
10953
  if (isRetryableTurnError(error, "mcp_auth_resume") && args.onAuthPause) {
10330
- await args.onAuthPause(error);
10954
+ deferredPauseHandler = async () => {
10955
+ await args.onAuthPause?.(error);
10956
+ };
10957
+ } else if (isRetryableTurnError(error, "turn_timeout_resume") && args.onTimeoutPause) {
10958
+ deferredPauseHandler = async () => {
10959
+ await args.onTimeoutPause?.(error);
10960
+ };
10961
+ } else {
10962
+ deferredFailureHandler = async () => {
10963
+ await args.onFailure?.(error);
10964
+ if (args.failureText) {
10965
+ await postSlackMessageBestEffort(
10966
+ args.channelId,
10967
+ args.threadTs,
10968
+ args.failureText
10969
+ );
10970
+ }
10971
+ };
10972
+ }
10973
+ } finally {
10974
+ await stateAdapter.releaseLock(lock);
10975
+ }
10976
+ if (deferredPauseHandler) {
10977
+ try {
10978
+ await deferredPauseHandler();
10979
+ return;
10980
+ } catch (pauseError) {
10981
+ await args.onFailure?.(pauseError);
10982
+ if (args.failureText) {
10983
+ await postSlackMessageBestEffort(
10984
+ args.channelId,
10985
+ args.threadTs,
10986
+ args.failureText
10987
+ );
10988
+ }
10331
10989
  return;
10332
10990
  }
10333
- await args.onFailure?.(error);
10334
- await postSlackMessage(args.channelId, args.threadTs, args.failureText);
10991
+ }
10992
+ if (deferredFailureHandler) {
10993
+ await deferredFailureHandler();
10994
+ }
10995
+ }
10996
+ async function resumeAuthorizedRequest(args) {
10997
+ await resumeSlackTurn({
10998
+ messageText: args.messageText,
10999
+ channelId: args.channelId,
11000
+ threadTs: args.threadTs,
11001
+ replyContext: args.replyContext,
11002
+ lockKey: args.lockKey,
11003
+ initialText: args.connectedText,
11004
+ failureText: args.failureText,
11005
+ generateReply: args.generateReply,
11006
+ onSuccess: args.onSuccess,
11007
+ onFailure: args.onFailure,
11008
+ onAuthPause: args.onAuthPause,
11009
+ onTimeoutPause: args.onTimeoutPause,
11010
+ replyTimeoutMs: args.replyTimeoutMs
11011
+ });
11012
+ }
11013
+
11014
+ // src/chat/services/timeout-resume.ts
11015
+ import { createHmac, timingSafeEqual } from "crypto";
11016
+ var TURN_TIMEOUT_RESUME_PATH = "/api/internal/turn-resume";
11017
+ var TURN_TIMEOUT_RESUME_SIGNATURE_VERSION = "v1";
11018
+ var TURN_TIMEOUT_RESUME_MAX_SKEW_MS = 5 * 60 * 1e3;
11019
+ var TURN_TIMEOUT_RESUME_TIMESTAMP_HEADER = "x-junior-resume-timestamp";
11020
+ var TURN_TIMEOUT_RESUME_SIGNATURE_HEADER = "x-junior-resume-signature";
11021
+ var MAX_TURN_TIMEOUT_RESUME_SLICE_ID = 5;
11022
+ function canScheduleTurnTimeoutResume(nextSliceId) {
11023
+ return typeof nextSliceId === "number" && nextSliceId > 1 && nextSliceId <= MAX_TURN_TIMEOUT_RESUME_SLICE_ID;
11024
+ }
11025
+ function getTurnTimeoutResumeSecret() {
11026
+ const explicit = process.env.JUNIOR_INTERNAL_RESUME_SECRET?.trim();
11027
+ if (explicit) {
11028
+ return explicit;
11029
+ }
11030
+ return getSlackSigningSecret();
11031
+ }
11032
+ function buildSignedPayload(timestamp, body) {
11033
+ return `${timestamp}:${body}`;
11034
+ }
11035
+ function signTurnTimeoutResumeBody(secret, timestamp, body) {
11036
+ const digest = createHmac("sha256", secret).update(buildSignedPayload(timestamp, body)).digest("hex");
11037
+ return `${TURN_TIMEOUT_RESUME_SIGNATURE_VERSION}=${digest}`;
11038
+ }
11039
+ function timingSafeMatch(expected, actual) {
11040
+ const expectedBuffer = Buffer.from(expected);
11041
+ const actualBuffer = Buffer.from(actual);
11042
+ if (expectedBuffer.length !== actualBuffer.length) {
11043
+ return false;
11044
+ }
11045
+ return timingSafeEqual(expectedBuffer, actualBuffer);
11046
+ }
11047
+ function parseTurnTimeoutResumeRequest(value) {
11048
+ if (!value || typeof value !== "object") {
11049
+ return void 0;
11050
+ }
11051
+ const record = value;
11052
+ if (typeof record.conversationId !== "string" || typeof record.sessionId !== "string" || typeof record.expectedCheckpointVersion !== "number") {
11053
+ return void 0;
11054
+ }
11055
+ return {
11056
+ conversationId: record.conversationId,
11057
+ sessionId: record.sessionId,
11058
+ expectedCheckpointVersion: record.expectedCheckpointVersion
11059
+ };
11060
+ }
11061
+ async function scheduleTurnTimeoutResume(request) {
11062
+ const baseUrl = resolveBaseUrl();
11063
+ if (!baseUrl) {
11064
+ throw new Error(
11065
+ "Cannot determine base URL for timeout resume callback (set JUNIOR_BASE_URL or deploy to Vercel)"
11066
+ );
11067
+ }
11068
+ const secret = getTurnTimeoutResumeSecret();
11069
+ if (!secret) {
11070
+ throw new Error(
11071
+ "Cannot determine timeout resume secret (set JUNIOR_INTERNAL_RESUME_SECRET or SLACK_SIGNING_SECRET)"
11072
+ );
11073
+ }
11074
+ const body = JSON.stringify(request);
11075
+ const timestamp = Date.now().toString();
11076
+ const response = await fetch(`${baseUrl}${TURN_TIMEOUT_RESUME_PATH}`, {
11077
+ method: "POST",
11078
+ headers: {
11079
+ "content-type": "application/json",
11080
+ [TURN_TIMEOUT_RESUME_TIMESTAMP_HEADER]: timestamp,
11081
+ [TURN_TIMEOUT_RESUME_SIGNATURE_HEADER]: signTurnTimeoutResumeBody(
11082
+ secret,
11083
+ timestamp,
11084
+ body
11085
+ )
11086
+ },
11087
+ body
11088
+ });
11089
+ if (!response.ok) {
11090
+ throw new Error(
11091
+ `Timeout resume callback failed with status ${response.status}`
11092
+ );
11093
+ }
11094
+ }
11095
+ async function verifyTurnTimeoutResumeRequest(request) {
11096
+ const timestamp = request.headers.get(TURN_TIMEOUT_RESUME_TIMESTAMP_HEADER)?.trim() ?? "";
11097
+ const signature = request.headers.get(TURN_TIMEOUT_RESUME_SIGNATURE_HEADER)?.trim() ?? "";
11098
+ const secret = getTurnTimeoutResumeSecret();
11099
+ if (!timestamp || !signature || !secret) {
11100
+ return void 0;
11101
+ }
11102
+ const parsedTimestamp = Number.parseInt(timestamp, 10);
11103
+ if (!Number.isFinite(parsedTimestamp) || Math.abs(Date.now() - parsedTimestamp) > TURN_TIMEOUT_RESUME_MAX_SKEW_MS) {
11104
+ return void 0;
11105
+ }
11106
+ const body = await request.text();
11107
+ const expectedSignature = signTurnTimeoutResumeBody(secret, timestamp, body);
11108
+ if (!timingSafeMatch(expectedSignature, signature)) {
11109
+ return void 0;
11110
+ }
11111
+ try {
11112
+ return parseTurnTimeoutResumeRequest(JSON.parse(body));
11113
+ } catch {
11114
+ return void 0;
10335
11115
  }
10336
11116
  }
10337
11117
 
@@ -10386,87 +11166,12 @@ function htmlResponse(kind) {
10386
11166
  const page = CALLBACK_PAGES[kind];
10387
11167
  return htmlCallbackResponse(page.title, page.message, page.status);
10388
11168
  }
10389
- function extractSlackText(text, files) {
10390
- const message = buildSlackOutputMessage(text, files);
10391
- if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
10392
- return message.markdown;
10393
- }
10394
- if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
10395
- return message.raw;
10396
- }
10397
- return text;
10398
- }
10399
- async function normalizeFileUploads(files) {
10400
- const normalized = [];
10401
- for (const file of files) {
10402
- let data;
10403
- if (Buffer2.isBuffer(file.data)) {
10404
- data = file.data;
10405
- } else if (file.data instanceof ArrayBuffer) {
10406
- data = Buffer2.from(file.data);
10407
- } else {
10408
- data = Buffer2.from(await file.data.arrayBuffer());
10409
- }
10410
- normalized.push({
10411
- data,
10412
- filename: file.filename
10413
- });
10414
- }
10415
- return normalized;
10416
- }
10417
- async function deliverReplyToThread(channelId, threadTs, reply) {
10418
- const replyFiles = reply.files && reply.files.length > 0 ? reply.files : void 0;
10419
- const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
10420
- reply,
10421
- hasStreamedThreadReply: false
10422
- });
10423
- if (shouldPostThreadReply) {
10424
- const text = extractSlackText(
10425
- reply.text,
10426
- attachFiles === "inline" ? replyFiles : void 0
10427
- );
10428
- if (text.trim().length > 0) {
10429
- await postSlackMessage(channelId, threadTs, text);
10430
- }
10431
- }
10432
- if (!replyFiles || attachFiles === "none") {
10433
- return;
10434
- }
10435
- const files = await normalizeFileUploads(replyFiles);
10436
- if (files.length === 0) {
10437
- return;
10438
- }
10439
- try {
10440
- await uploadFilesToThread({
10441
- channelId,
10442
- threadTs,
10443
- files
10444
- });
10445
- } catch {
10446
- }
10447
- }
10448
- function buildDeterministicTurnId2(messageId) {
10449
- const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
10450
- return `turn_${sanitized}`;
10451
- }
10452
- function getUserMessageIdForTurn(conversation, sessionId) {
10453
- for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
10454
- const message = conversation.messages[index];
10455
- if (message?.role !== "user") {
10456
- continue;
10457
- }
10458
- if (buildDeterministicTurnId2(message.id) === sessionId) {
10459
- return message.id;
10460
- }
10461
- }
10462
- return void 0;
10463
- }
10464
11169
  async function buildResumeConversationContext(channelId, threadTs, sessionId) {
10465
11170
  const threadId = `slack:${channelId}:${threadTs}`;
10466
11171
  const conversation = coerceThreadConversationState(
10467
11172
  await getPersistedThreadState(threadId)
10468
11173
  );
10469
- const userMessageId = getUserMessageIdForTurn(conversation, sessionId);
11174
+ const userMessageId = getTurnUserMessageId(conversation, sessionId);
10470
11175
  return buildConversationContext(conversation, {
10471
11176
  excludeMessageId: userMessageId
10472
11177
  });
@@ -10477,7 +11182,7 @@ async function persistCompletedReplyState(channelId, threadTs, sessionId, reply)
10477
11182
  const conversation = coerceThreadConversationState(currentState);
10478
11183
  const artifacts = coerceThreadArtifactsState(currentState);
10479
11184
  const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : void 0;
10480
- const userMessageId = getUserMessageIdForTurn(conversation, sessionId);
11185
+ const userMessageId = getTurnUserMessageId(conversation, sessionId);
10481
11186
  markConversationMessage(conversation, userMessageId, {
10482
11187
  replied: true,
10483
11188
  skippedReason: void 0
@@ -10514,7 +11219,7 @@ async function persistFailedReplyState(channelId, threadTs, sessionId) {
10514
11219
  markTurnFailed({
10515
11220
  conversation,
10516
11221
  nowMs: Date.now(),
10517
- userMessageId: getUserMessageIdForTurn(conversation, sessionId),
11222
+ userMessageId: getTurnUserMessageId(conversation, sessionId),
10518
11223
  markConversationMessage,
10519
11224
  updateConversationStats
10520
11225
  });
@@ -10527,6 +11232,17 @@ async function resumeAuthorizedMcpTurn(args) {
10527
11232
  if (!authSession.channelId || !authSession.threadTs) {
10528
11233
  return;
10529
11234
  }
11235
+ const threadId = `slack:${authSession.channelId}:${authSession.threadTs}`;
11236
+ const currentState = await getPersistedThreadState(threadId);
11237
+ const conversation = coerceThreadConversationState(currentState);
11238
+ const artifacts = coerceThreadArtifactsState(currentState);
11239
+ const userMessage = getTurnUserMessage(conversation, authSession.sessionId);
11240
+ if (conversation.processing.activeTurnId !== authSession.sessionId) {
11241
+ return;
11242
+ }
11243
+ const channelConfiguration = getChannelConfigurationServiceById(
11244
+ authSession.channelId
11245
+ );
10530
11246
  const conversationContext = await buildResumeConversationContext(
10531
11247
  authSession.channelId,
10532
11248
  authSession.threadTs,
@@ -10534,29 +11250,32 @@ async function resumeAuthorizedMcpTurn(args) {
10534
11250
  );
10535
11251
  await resumeAuthorizedRequest({
10536
11252
  messageText: authSession.userMessage,
10537
- requesterUserId: authSession.userId,
10538
- provider,
10539
11253
  channelId: authSession.channelId,
10540
11254
  threadTs: authSession.threadTs,
11255
+ lockKey: authSession.conversationId,
10541
11256
  connectedText: `Your ${provider} MCP access is now connected. Continuing the original request...`,
10542
11257
  failureText: "MCP authorization completed, but resuming the request failed. Please retry the original command.",
10543
- correlation: {
10544
- conversationId: authSession.conversationId,
10545
- turnId: authSession.sessionId,
10546
- channelId: authSession.channelId,
10547
- threadTs: authSession.threadTs,
10548
- requesterId: authSession.userId
10549
- },
10550
- toolChannelId: authSession.toolChannelId ?? authSession.artifactState?.assistantContextChannelId ?? authSession.channelId,
10551
- conversationContext,
10552
- artifactState: authSession.artifactState,
10553
- configuration: authSession.configuration,
10554
- onReply: async (reply) => {
10555
- await deliverReplyToThread(
10556
- authSession.channelId,
10557
- authSession.threadTs,
10558
- reply
10559
- );
11258
+ replyContext: {
11259
+ assistant: { userName: botConfig.userName },
11260
+ requester: {
11261
+ userId: authSession.userId,
11262
+ userName: userMessage?.author?.userName,
11263
+ fullName: userMessage?.author?.fullName
11264
+ },
11265
+ correlation: {
11266
+ conversationId: authSession.conversationId,
11267
+ turnId: authSession.sessionId,
11268
+ channelId: authSession.channelId,
11269
+ threadTs: authSession.threadTs,
11270
+ requesterId: authSession.userId
11271
+ },
11272
+ toolChannelId: authSession.toolChannelId ?? artifacts.assistantContextChannelId ?? authSession.channelId,
11273
+ conversationContext,
11274
+ artifactState: artifacts,
11275
+ configuration: authSession.configuration,
11276
+ channelConfiguration,
11277
+ sandbox: getPersistedSandboxState(currentState),
11278
+ threadParticipants: buildThreadParticipants(conversation.messages)
10560
11279
  },
10561
11280
  onSuccess: async (reply) => {
10562
11281
  try {
@@ -10607,6 +11326,37 @@ async function resumeAuthorizedMcpTurn(args) {
10607
11326
  { "app.credential.provider": provider },
10608
11327
  "Resumed MCP turn requested another authorization flow"
10609
11328
  );
11329
+ },
11330
+ onTimeoutPause: async (error) => {
11331
+ if (!isRetryableTurnError(error, "turn_timeout_resume")) {
11332
+ throw error;
11333
+ }
11334
+ const checkpointVersion = error.metadata?.checkpointVersion;
11335
+ const nextSliceId = error.metadata?.sliceId;
11336
+ if (typeof checkpointVersion !== "number") {
11337
+ throw new Error(
11338
+ "Timed-out MCP resume did not include a checkpoint version"
11339
+ );
11340
+ }
11341
+ if (!canScheduleTurnTimeoutResume(nextSliceId)) {
11342
+ logWarn(
11343
+ "mcp_oauth_callback_resume_slice_limit_reached",
11344
+ {},
11345
+ {
11346
+ "app.credential.provider": provider,
11347
+ ...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
11348
+ },
11349
+ "Skipped automatic timeout resume because the turn exceeded the slice limit"
11350
+ );
11351
+ throw new Error(
11352
+ "Timed-out turn exceeded the automatic resume slice limit"
11353
+ );
11354
+ }
11355
+ await scheduleTurnTimeoutResume({
11356
+ conversationId: authSession.conversationId,
11357
+ sessionId: authSession.sessionId,
11358
+ expectedCheckpointVersion: checkpointVersion
11359
+ });
10610
11360
  }
10611
11361
  });
10612
11362
  }
@@ -10823,14 +11573,15 @@ async function resumePendingOAuthMessage(stored) {
10823
11573
  );
10824
11574
  await resumeAuthorizedRequest({
10825
11575
  messageText: stored.pendingMessage,
10826
- requesterUserId: stored.userId,
10827
- provider: stored.provider,
10828
11576
  channelId: stored.channelId,
10829
11577
  threadTs: stored.threadTs,
10830
11578
  connectedText: `Your ${providerLabel} account is now connected. Processing your request...`,
10831
11579
  failureText: `I connected your account but hit an error processing your request. Please try \`${stored.pendingMessage}\` again.`,
10832
- conversationContext,
10833
- configuration: stored.configuration,
11580
+ replyContext: {
11581
+ requester: { userId: stored.userId },
11582
+ conversationContext,
11583
+ configuration: stored.configuration
11584
+ },
10834
11585
  onSuccess: async (reply) => {
10835
11586
  logInfo(
10836
11587
  "oauth_callback_resume_complete",
@@ -10990,38 +11741,304 @@ async function GET5(request, provider, waitUntil) {
10990
11741
  } catch {
10991
11742
  }
10992
11743
  });
10993
- if (stored.pendingMessage && stored.channelId && stored.threadTs) {
10994
- waitUntil(() => resumePendingOAuthMessage(stored));
10995
- } else if (stored.channelId && stored.threadTs) {
10996
- const { channelId, threadTs } = stored;
10997
- waitUntil(
10998
- () => postSlackMessage(
10999
- channelId,
11000
- threadTs,
11001
- `Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
11002
- )
11003
- );
11744
+ if (stored.pendingMessage && stored.channelId && stored.threadTs) {
11745
+ waitUntil(() => resumePendingOAuthMessage(stored));
11746
+ } else if (stored.channelId && stored.threadTs) {
11747
+ const { channelId, threadTs } = stored;
11748
+ waitUntil(
11749
+ () => postSlackMessage(
11750
+ channelId,
11751
+ threadTs,
11752
+ `Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
11753
+ )
11754
+ );
11755
+ }
11756
+ const statusMessage = stored.pendingMessage ? "Your request is being processed in Slack." : "You can close this tab and return to Slack.";
11757
+ const html = `<!DOCTYPE html>
11758
+ <html>
11759
+ <head><title>${providerLabel} Connected</title></head>
11760
+ <body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
11761
+ <div style="text-align: center;">
11762
+ <h1>${providerLabel} account connected</h1>
11763
+ <p>${statusMessage}</p>
11764
+ </div>
11765
+ </body>
11766
+ </html>`;
11767
+ return new Response(html, {
11768
+ status: 200,
11769
+ headers: { "Content-Type": "text/html; charset=utf-8" }
11770
+ });
11771
+ }
11772
+
11773
+ // src/chat/slack/context.ts
11774
+ function toTrimmedSlackString(value) {
11775
+ const normalized = toOptionalString(value);
11776
+ return normalized?.trim() || void 0;
11777
+ }
11778
+ function parseSlackThreadId(threadId) {
11779
+ const normalizedThreadId = toTrimmedSlackString(threadId);
11780
+ if (!normalizedThreadId) {
11781
+ return void 0;
11782
+ }
11783
+ const parts = normalizedThreadId.split(":");
11784
+ if (parts.length !== 3 || parts[0] !== "slack") {
11785
+ return void 0;
11786
+ }
11787
+ const channelId = toTrimmedSlackString(parts[1]);
11788
+ const threadTs = toTrimmedSlackString(parts[2]);
11789
+ if (!channelId || !threadTs) {
11790
+ return void 0;
11791
+ }
11792
+ return { channelId, threadTs };
11793
+ }
11794
+ function resolveSlackChannelIdFromThreadId(threadId) {
11795
+ return parseSlackThreadId(threadId)?.channelId;
11796
+ }
11797
+ function resolveSlackChannelIdFromMessage(message) {
11798
+ const messageChannelId = toTrimmedSlackString(
11799
+ message.channelId
11800
+ );
11801
+ if (messageChannelId) {
11802
+ return messageChannelId;
11803
+ }
11804
+ const raw = message.raw;
11805
+ if (raw && typeof raw === "object") {
11806
+ const rawChannel = toTrimmedSlackString(
11807
+ raw.channel
11808
+ );
11809
+ if (rawChannel) {
11810
+ return rawChannel;
11811
+ }
11812
+ }
11813
+ const threadId = toTrimmedSlackString(
11814
+ message.threadId
11815
+ );
11816
+ return resolveSlackChannelIdFromThreadId(threadId);
11817
+ }
11818
+
11819
+ // src/handlers/turn-resume.ts
11820
+ async function persistCompletedReplyState2(args) {
11821
+ const currentState = await getPersistedThreadState(
11822
+ args.checkpoint.conversationId
11823
+ );
11824
+ const conversation = coerceThreadConversationState(currentState);
11825
+ const artifacts = coerceThreadArtifactsState(currentState);
11826
+ const nextArtifacts = args.reply.artifactStatePatch ? mergeArtifactsState(artifacts, args.reply.artifactStatePatch) : void 0;
11827
+ const userMessage = getTurnUserMessage(
11828
+ conversation,
11829
+ args.checkpoint.sessionId
11830
+ );
11831
+ markConversationMessage(conversation, userMessage?.id, {
11832
+ replied: true,
11833
+ skippedReason: void 0
11834
+ });
11835
+ upsertConversationMessage(conversation, {
11836
+ id: generateConversationId("assistant"),
11837
+ role: "assistant",
11838
+ text: normalizeConversationText(args.reply.text) || "[empty response]",
11839
+ createdAtMs: Date.now(),
11840
+ author: {
11841
+ userName: botConfig.userName,
11842
+ isBot: true
11843
+ },
11844
+ meta: {
11845
+ replied: true
11846
+ }
11847
+ });
11848
+ markTurnCompleted({
11849
+ conversation,
11850
+ nowMs: Date.now(),
11851
+ updateConversationStats
11852
+ });
11853
+ await persistThreadStateById(args.checkpoint.conversationId, {
11854
+ artifacts: nextArtifacts,
11855
+ conversation,
11856
+ sandboxId: args.reply.sandboxId,
11857
+ sandboxDependencyProfileHash: args.reply.sandboxDependencyProfileHash
11858
+ });
11859
+ }
11860
+ async function persistFailedReplyState2(checkpoint) {
11861
+ const currentState = await getPersistedThreadState(checkpoint.conversationId);
11862
+ const conversation = coerceThreadConversationState(currentState);
11863
+ markTurnFailed({
11864
+ conversation,
11865
+ nowMs: Date.now(),
11866
+ userMessageId: getTurnUserMessage(conversation, checkpoint.sessionId)?.id,
11867
+ markConversationMessage,
11868
+ updateConversationStats
11869
+ });
11870
+ await persistThreadStateById(checkpoint.conversationId, {
11871
+ conversation
11872
+ });
11873
+ }
11874
+ async function resumeTimedOutTurn(payload) {
11875
+ const checkpoint = await getAgentTurnSessionCheckpoint(
11876
+ payload.conversationId,
11877
+ payload.sessionId
11878
+ );
11879
+ if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "timeout" || checkpoint.checkpointVersion !== payload.expectedCheckpointVersion) {
11880
+ return;
11881
+ }
11882
+ const thread = parseSlackThreadId(payload.conversationId);
11883
+ if (!thread) {
11884
+ throw new Error(
11885
+ `Timeout resume requires a Slack thread conversation id, got "${payload.conversationId}"`
11886
+ );
11887
+ }
11888
+ const currentState = await getPersistedThreadState(payload.conversationId);
11889
+ const conversation = coerceThreadConversationState(currentState);
11890
+ const artifacts = coerceThreadArtifactsState(currentState);
11891
+ const userMessage = getTurnUserMessage(conversation, payload.sessionId);
11892
+ if (!userMessage?.author?.userId) {
11893
+ throw new Error(
11894
+ `Unable to locate the persisted user message for timeout resume session "${payload.sessionId}"`
11895
+ );
11896
+ }
11897
+ if (conversation.processing.activeTurnId !== payload.sessionId) {
11898
+ return;
11899
+ }
11900
+ const channelConfiguration = getChannelConfigurationServiceById(
11901
+ thread.channelId
11902
+ );
11903
+ const conversationContext = buildConversationContext(conversation, {
11904
+ excludeMessageId: userMessage.id
11905
+ });
11906
+ const sandbox = getPersistedSandboxState(currentState);
11907
+ await resumeSlackTurn({
11908
+ messageText: userMessage.text,
11909
+ channelId: thread.channelId,
11910
+ threadTs: thread.threadTs,
11911
+ lockKey: payload.conversationId,
11912
+ failureText: "I hit an error while resuming that request. Please try the command again.",
11913
+ replyContext: {
11914
+ assistant: { userName: botConfig.userName },
11915
+ requester: {
11916
+ userId: userMessage.author.userId,
11917
+ userName: userMessage.author.userName,
11918
+ fullName: userMessage.author.fullName
11919
+ },
11920
+ correlation: {
11921
+ conversationId: payload.conversationId,
11922
+ turnId: payload.sessionId,
11923
+ channelId: thread.channelId,
11924
+ threadTs: thread.threadTs,
11925
+ requesterId: userMessage.author.userId
11926
+ },
11927
+ toolChannelId: artifacts.assistantContextChannelId ?? thread.channelId,
11928
+ artifactState: artifacts,
11929
+ conversationContext,
11930
+ channelConfiguration,
11931
+ sandbox,
11932
+ threadParticipants: buildThreadParticipants(conversation.messages)
11933
+ },
11934
+ onSuccess: async (reply) => {
11935
+ try {
11936
+ await persistCompletedReplyState2({ checkpoint, reply });
11937
+ } catch (persistError) {
11938
+ logException(
11939
+ persistError,
11940
+ "timeout_resume_complete_persist_failed",
11941
+ {},
11942
+ {
11943
+ "app.ai.conversation_id": payload.conversationId,
11944
+ "app.ai.session_id": payload.sessionId
11945
+ },
11946
+ "Failed to persist completed timeout-resume state after reply delivery"
11947
+ );
11948
+ }
11949
+ },
11950
+ onFailure: async (error) => {
11951
+ logException(
11952
+ error,
11953
+ "timeout_resume_failed",
11954
+ {},
11955
+ {
11956
+ "app.ai.conversation_id": payload.conversationId,
11957
+ "app.ai.session_id": payload.sessionId
11958
+ },
11959
+ "Failed to resume timed-out turn"
11960
+ );
11961
+ await persistFailedReplyState2(checkpoint);
11962
+ },
11963
+ onAuthPause: async () => {
11964
+ logWarn(
11965
+ "timeout_resume_reparked_for_auth",
11966
+ {},
11967
+ {
11968
+ "app.ai.conversation_id": payload.conversationId,
11969
+ "app.ai.session_id": payload.sessionId
11970
+ },
11971
+ "Resumed timed-out turn parked for auth"
11972
+ );
11973
+ },
11974
+ onTimeoutPause: async (error) => {
11975
+ if (!isRetryableTurnError(error, "turn_timeout_resume")) {
11976
+ throw error;
11977
+ }
11978
+ const checkpointVersion = error.metadata?.checkpointVersion;
11979
+ const nextSliceId = error.metadata?.sliceId;
11980
+ if (typeof checkpointVersion !== "number") {
11981
+ throw new Error(
11982
+ "Timed-out resume turn did not include a checkpoint version"
11983
+ );
11984
+ }
11985
+ if (!canScheduleTurnTimeoutResume(nextSliceId)) {
11986
+ logWarn(
11987
+ "timeout_resume_slice_limit_reached",
11988
+ {},
11989
+ {
11990
+ "app.ai.conversation_id": payload.conversationId,
11991
+ "app.ai.session_id": payload.sessionId,
11992
+ ...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
11993
+ },
11994
+ "Skipped automatic timeout resume because the turn exceeded the slice limit"
11995
+ );
11996
+ throw new Error(
11997
+ "Timed-out turn exceeded the automatic resume slice limit"
11998
+ );
11999
+ }
12000
+ await scheduleTurnTimeoutResume({
12001
+ conversationId: payload.conversationId,
12002
+ sessionId: payload.sessionId,
12003
+ expectedCheckpointVersion: checkpointVersion
12004
+ });
12005
+ }
12006
+ });
12007
+ }
12008
+ async function POST(request, waitUntil) {
12009
+ const payload = await verifyTurnTimeoutResumeRequest(request);
12010
+ if (!payload) {
12011
+ return new Response("Unauthorized", { status: 401 });
11004
12012
  }
11005
- const statusMessage = stored.pendingMessage ? "Your request is being processed in Slack." : "You can close this tab and return to Slack.";
11006
- const html = `<!DOCTYPE html>
11007
- <html>
11008
- <head><title>${providerLabel} Connected</title></head>
11009
- <body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
11010
- <div style="text-align: center;">
11011
- <h1>${providerLabel} account connected</h1>
11012
- <p>${statusMessage}</p>
11013
- </div>
11014
- </body>
11015
- </html>`;
11016
- return new Response(html, {
11017
- status: 200,
11018
- headers: { "Content-Type": "text/html; charset=utf-8" }
11019
- });
12013
+ waitUntil(
12014
+ () => resumeTimedOutTurn(payload).catch((error) => {
12015
+ if (error instanceof ResumeTurnBusyError) {
12016
+ logWarn(
12017
+ "timeout_resume_lock_busy",
12018
+ {},
12019
+ {
12020
+ "app.ai.conversation_id": payload.conversationId,
12021
+ "app.ai.session_id": payload.sessionId
12022
+ },
12023
+ "Skipped timeout resume because another turn owns the thread lock"
12024
+ );
12025
+ return;
12026
+ }
12027
+ logException(
12028
+ error,
12029
+ "timeout_resume_handler_failed",
12030
+ {},
12031
+ {
12032
+ "app.ai.conversation_id": payload.conversationId,
12033
+ "app.ai.session_id": payload.sessionId
12034
+ },
12035
+ "Timeout resume handler failed"
12036
+ );
12037
+ })
12038
+ );
12039
+ return new Response("Accepted", { status: 202 });
11020
12040
  }
11021
12041
 
11022
- // src/chat/app/production.ts
11023
- import { createSlackAdapter } from "@chat-adapter/slack";
11024
-
11025
12042
  // src/chat/services/subscribed-decision.ts
11026
12043
  import { z } from "zod";
11027
12044
  var replyDecisionSchema = z.object({
@@ -11048,11 +12065,11 @@ var DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|
11048
12065
  var TERSE_CLARIFICATION_RE = /^(?:which one|which ones|why|how so|what do you mean|what did you mean|say more|explain that|clarify that|expand on that|elaborate on that)\??$/i;
11049
12066
  var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
11050
12067
  var RECENT_THREAD_WINDOW = 6;
11051
- function escapeRegExp(value) {
12068
+ function escapeRegExp2(value) {
11052
12069
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11053
12070
  }
11054
12071
  function containsAssistantInvocation(text, botUserName) {
11055
- const escapedUserName = escapeRegExp(botUserName);
12072
+ const escapedUserName = escapeRegExp2(botUserName);
11056
12073
  const plainNameMentionRe = new RegExp(`(^|\\s)@${escapedUserName}\\b`, "i");
11057
12074
  const labeledEntityMentionRe = new RegExp(
11058
12075
  `<@[^>|]+\\|${escapedUserName}>`,
@@ -11340,54 +12357,8 @@ async function decideSubscribedThreadReply(args) {
11340
12357
  }
11341
12358
  }
11342
12359
 
11343
- // src/chat/slack/context.ts
11344
- function toTrimmedSlackString(value) {
11345
- const normalized = toOptionalString(value);
11346
- return normalized?.trim() || void 0;
11347
- }
11348
- function parseSlackThreadId(threadId) {
11349
- const normalizedThreadId = toTrimmedSlackString(threadId);
11350
- if (!normalizedThreadId) {
11351
- return void 0;
11352
- }
11353
- const parts = normalizedThreadId.split(":");
11354
- if (parts.length !== 3 || parts[0] !== "slack") {
11355
- return void 0;
11356
- }
11357
- const channelId = toTrimmedSlackString(parts[1]);
11358
- const threadTs = toTrimmedSlackString(parts[2]);
11359
- if (!channelId || !threadTs) {
11360
- return void 0;
11361
- }
11362
- return { channelId, threadTs };
11363
- }
11364
- function resolveSlackChannelIdFromThreadId(threadId) {
11365
- return parseSlackThreadId(threadId)?.channelId;
11366
- }
11367
- function resolveSlackChannelIdFromMessage(message) {
11368
- const messageChannelId = toTrimmedSlackString(
11369
- message.channelId
11370
- );
11371
- if (messageChannelId) {
11372
- return messageChannelId;
11373
- }
11374
- const raw = message.raw;
11375
- if (raw && typeof raw === "object") {
11376
- const rawChannel = toTrimmedSlackString(
11377
- raw.channel
11378
- );
11379
- if (rawChannel) {
11380
- return rawChannel;
11381
- }
11382
- }
11383
- const threadId = toTrimmedSlackString(
11384
- message.threadId
11385
- );
11386
- return resolveSlackChannelIdFromThreadId(threadId);
11387
- }
11388
-
11389
12360
  // src/chat/runtime/thread-context.ts
11390
- function escapeRegExp2(value) {
12361
+ function escapeRegExp3(value) {
11391
12362
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11392
12363
  }
11393
12364
  function stripLeadingBotMention(text, options = {}) {
@@ -11397,12 +12368,12 @@ function stripLeadingBotMention(text, options = {}) {
11397
12368
  next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
11398
12369
  }
11399
12370
  const mentionByNameRe = new RegExp(
11400
- `^\\s*@${escapeRegExp2(botConfig.userName)}\\b[\\s,:-]*`,
12371
+ `^\\s*@${escapeRegExp3(botConfig.userName)}\\b[\\s,:-]*`,
11401
12372
  "i"
11402
12373
  );
11403
12374
  next = next.replace(mentionByNameRe, "").trim();
11404
12375
  const mentionByLabeledEntityRe = new RegExp(
11405
- `^\\s*<@[^>|]+\\|${escapeRegExp2(botConfig.userName)}>[\\s,:-]*`,
12376
+ `^\\s*<@[^>|]+\\|${escapeRegExp3(botConfig.userName)}>[\\s,:-]*`,
11406
12377
  "i"
11407
12378
  );
11408
12379
  next = next.replace(mentionByLabeledEntityRe, "").trim();
@@ -11914,6 +12885,15 @@ var MAX_USER_ATTACHMENTS = 3;
11914
12885
  var MAX_USER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
11915
12886
  var MAX_MESSAGE_IMAGE_ATTACHMENTS = 3;
11916
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
+ }
11917
12897
  function isVisionEnabled() {
11918
12898
  return Boolean(botConfig.visionModelId);
11919
12899
  }
@@ -12256,6 +13236,7 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
12256
13236
  continue;
12257
13237
  }
12258
13238
  hydratedMessageIds.add(conversationMessage.id);
13239
+ const existingMeta = conversationMessage.meta ?? {};
12259
13240
  const imageFiles = (reply.files ?? []).filter((file) => {
12260
13241
  const mimeType = toOptionalString(file.mimetype);
12261
13242
  return Boolean(
@@ -12263,10 +13244,15 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
12263
13244
  );
12264
13245
  }).slice(0, MAX_MESSAGE_IMAGE_ATTACHMENTS);
12265
13246
  if (imageFiles.length === 0) {
13247
+ conversationMessage.meta = {
13248
+ ...existingMeta,
13249
+ slackTs: existingMeta.slackTs ?? ts,
13250
+ imagesHydrated: true
13251
+ };
13252
+ mutated = true;
12266
13253
  continue;
12267
13254
  }
12268
13255
  const imageFileIds = imageFiles.map((file) => toOptionalString(file.id)).filter((fileId) => Boolean(fileId));
12269
- const existingMeta = conversationMessage.meta ?? {};
12270
13256
  conversationMessage.meta = {
12271
13257
  ...existingMeta,
12272
13258
  slackTs: existingMeta.slackTs ?? ts,
@@ -12430,6 +13416,7 @@ function createJuniorRuntimeServices(overrides = {}) {
12430
13416
  replyExecutor: {
12431
13417
  generateAssistantReply: overrides.replyExecutor?.generateAssistantReply ?? generateAssistantReply,
12432
13418
  lookupSlackUser: overrides.replyExecutor?.lookupSlackUser ?? lookupSlackUser,
13419
+ scheduleTurnTimeoutResume: overrides.replyExecutor?.scheduleTurnTimeoutResume ?? scheduleTurnTimeoutResume,
12433
13420
  generateThreadTitle: conversationMemory.generateThreadTitle
12434
13421
  },
12435
13422
  subscribedReplyPolicy: createSubscribedReplyPolicy({
@@ -12494,18 +13481,13 @@ function getExecutionFailureReason(reply) {
12494
13481
  }
12495
13482
  return "empty assistant turn";
12496
13483
  }
12497
- function buildParticipants(messages) {
12498
- const seen = /* @__PURE__ */ new Set();
12499
- const participants = [];
12500
- for (const message of messages) {
12501
- const { userId, userName, fullName } = message.author ?? {};
12502
- if (!userId || message.author?.isBot) continue;
12503
- if (!seen.has(userId)) {
12504
- seen.add(userId);
12505
- participants.push({ userId, userName, fullName });
12506
- }
13484
+ function shouldAutoStartStreaming(args) {
13485
+ const { text, deltaCount } = args;
13486
+ const trimmed = text.trim();
13487
+ if (!trimmed || isPotentialRedundantReactionAckText(trimmed)) {
13488
+ return false;
12507
13489
  }
12508
- return participants;
13490
+ return deltaCount >= 2;
12509
13491
  }
12510
13492
  function createReplyToThread(deps) {
12511
13493
  return async function replyToThread(thread, message, options = {}) {
@@ -12615,7 +13597,10 @@ function createReplyToThread(deps) {
12615
13597
  const textStream = createTextStreamBridge();
12616
13598
  let streamedReplyPromise;
12617
13599
  let pendingStreamText = "";
13600
+ let pendingStreamDeltaCount = 0;
13601
+ let awaitingPostToolAssistantMessage = false;
12618
13602
  let beforeFirstResponsePostCalled = false;
13603
+ let streamedReplyState = createSlackStreamAccumulator();
12619
13604
  const beforeFirstResponsePost = async () => {
12620
13605
  if (beforeFirstResponsePostCalled) {
12621
13606
  return;
@@ -12641,6 +13626,51 @@ function createReplyToThread(deps) {
12641
13626
  startStreamingReply();
12642
13627
  textStream.push(pendingStreamText);
12643
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));
12644
13674
  };
12645
13675
  const postThreadReply = async (payload, stage) => {
12646
13676
  await beforeFirstResponsePost();
@@ -12666,7 +13696,7 @@ function createReplyToThread(deps) {
12666
13696
  let shouldPersistFailureState = true;
12667
13697
  try {
12668
13698
  const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
12669
- const threadParticipants = buildParticipants(
13699
+ const threadParticipants = buildThreadParticipants(
12670
13700
  preparedState.conversation.messages
12671
13701
  );
12672
13702
  const reply = await deps.services.generateAssistantReply(userText, {
@@ -12698,25 +13728,41 @@ function createReplyToThread(deps) {
12698
13728
  sandboxId: preparedState.sandboxId,
12699
13729
  sandboxDependencyProfileHash: preparedState.sandboxDependencyProfileHash
12700
13730
  },
13731
+ onSandboxAcquired: async (sandbox) => {
13732
+ await persistThreadState(thread, {
13733
+ sandboxId: sandbox.sandboxId,
13734
+ sandboxDependencyProfileHash: sandbox.sandboxDependencyProfileHash
13735
+ });
13736
+ },
13737
+ onArtifactStateUpdated: async (artifacts) => {
13738
+ await persistThreadState(thread, { artifacts });
13739
+ },
12701
13740
  threadParticipants,
12702
13741
  onStatus: (status) => progress.setStatus(status),
12703
13742
  onTextDelta: (deltaText) => {
12704
13743
  if (explicitChannelPostIntent) {
12705
13744
  return;
12706
13745
  }
12707
- if (streamedReplyPromise) {
12708
- textStream.push(deltaText);
13746
+ appendVisibleStreamDelta(deltaText);
13747
+ },
13748
+ onAssistantMessageStart: () => {
13749
+ if (!awaitingPostToolAssistantMessage) {
12709
13750
  return;
12710
13751
  }
12711
- pendingStreamText += deltaText;
12712
- if (isPotentialRedundantReactionAckText(pendingStreamText)) {
12713
- return;
13752
+ awaitingPostToolAssistantMessage = false;
13753
+ discardPendingStreamPreview();
13754
+ },
13755
+ onToolCall: () => {
13756
+ if (!streamedReplyPromise) {
13757
+ awaitingPostToolAssistantMessage = true;
13758
+ discardPendingStreamPreview();
12714
13759
  }
12715
- flushPendingStreamText();
12716
13760
  }
12717
13761
  });
12718
13762
  if (streamedReplyPromise) {
12719
13763
  flushPendingStreamText();
13764
+ } else {
13765
+ finalizePendingStreamText();
12720
13766
  }
12721
13767
  textStream.end();
12722
13768
  const diagnosticsContext = {
@@ -12790,28 +13836,32 @@ function createReplyToThread(deps) {
12790
13836
  }
12791
13837
  });
12792
13838
  const artifactStatePatch = reply.artifactStatePatch ? { ...reply.artifactStatePatch } : {};
12793
- const replyFiles = reply.files && reply.files.length > 0 ? reply.files : void 0;
12794
- const { shouldPostThreadReply, attachFiles: resolvedAttachFiles } = resolveReplyDelivery({
12795
- reply,
12796
- hasStreamedThreadReply: Boolean(streamedReplyPromise)
12797
- });
12798
13839
  const reactionPerformed = reply.diagnostics.toolCalls.includes(
12799
13840
  "slackMessageAddReaction"
12800
13841
  );
12801
- 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) {
12802
13851
  if (!streamedReplyPromise) {
12803
- const sent = await postThreadReply(
12804
- buildSlackOutputMessage(
12805
- reply.text,
12806
- resolvedAttachFiles === "inline" ? replyFiles : void 0
12807
- ),
12808
- "thread_reply"
12809
- );
12810
- 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)) {
12811
13859
  await sent.delete();
12812
13860
  }
12813
13861
  } else {
12814
- await streamedReplyPromise;
13862
+ for (const post of plannedPosts) {
13863
+ await postThreadReply(post.message, post.stage);
13864
+ }
12815
13865
  }
12816
13866
  }
12817
13867
  const shouldPersistArtifacts = Object.keys(artifactStatePatch).length > 0;
@@ -12887,17 +13937,67 @@ function createReplyToThread(deps) {
12887
13937
  );
12888
13938
  });
12889
13939
  }
12890
- if (shouldPostThreadReply && resolvedAttachFiles === "followup" && replyFiles) {
12891
- await postThreadReply(
12892
- buildSlackOutputMessage("", replyFiles),
12893
- "thread_reply_files_followup"
12894
- );
12895
- }
12896
13940
  } catch (error) {
12897
- shouldPersistFailureState = !isRetryableTurnError(
12898
- error,
12899
- "mcp_auth_resume"
12900
- );
13941
+ if (isRetryableTurnError(error, "mcp_auth_resume")) {
13942
+ shouldPersistFailureState = false;
13943
+ throw error;
13944
+ }
13945
+ if (isRetryableTurnError(error, "turn_timeout_resume")) {
13946
+ textStream.end();
13947
+ const hasVisibleAssistantOutput = Boolean(streamedReplyPromise);
13948
+ if (hasVisibleAssistantOutput) {
13949
+ logWarn(
13950
+ "agent_turn_timeout_resume_skipped_after_visible_output",
13951
+ turnTraceContext,
13952
+ messageTs ? { "messaging.message.id": messageTs } : {},
13953
+ "Skipped automatic timeout resume because assistant text had already started streaming"
13954
+ );
13955
+ }
13956
+ const conversationIdForResume = error.metadata?.conversationId;
13957
+ const sessionIdForResume = error.metadata?.sessionId;
13958
+ const checkpointVersion = error.metadata?.checkpointVersion;
13959
+ const nextSliceId = error.metadata?.sliceId;
13960
+ if (!hasVisibleAssistantOutput && conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number" && canScheduleTurnTimeoutResume(nextSliceId)) {
13961
+ try {
13962
+ await deps.services.scheduleTurnTimeoutResume({
13963
+ conversationId: conversationIdForResume,
13964
+ sessionId: sessionIdForResume,
13965
+ expectedCheckpointVersion: checkpointVersion
13966
+ });
13967
+ shouldPersistFailureState = false;
13968
+ return;
13969
+ } catch (scheduleError) {
13970
+ logException(
13971
+ scheduleError,
13972
+ "agent_turn_timeout_resume_schedule_failed",
13973
+ turnTraceContext,
13974
+ {
13975
+ ...messageTs ? { "messaging.message.id": messageTs } : {},
13976
+ "app.ai.resume_checkpoint_version": checkpointVersion
13977
+ },
13978
+ "Failed to schedule timeout resume callback"
13979
+ );
13980
+ }
13981
+ } else if (!hasVisibleAssistantOutput && conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
13982
+ logWarn(
13983
+ "agent_turn_timeout_resume_slice_limit_reached",
13984
+ turnTraceContext,
13985
+ {
13986
+ ...messageTs ? { "messaging.message.id": messageTs } : {},
13987
+ ...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
13988
+ },
13989
+ "Skipped automatic timeout resume because the turn exceeded the slice limit"
13990
+ );
13991
+ } else {
13992
+ logWarn(
13993
+ "agent_turn_timeout_resume_metadata_missing",
13994
+ turnTraceContext,
13995
+ messageTs ? { "messaging.message.id": messageTs } : {},
13996
+ "Timed-out turn could not be scheduled for resume because retry metadata was incomplete"
13997
+ );
13998
+ }
13999
+ }
14000
+ shouldPersistFailureState = true;
12901
14001
  throw error;
12902
14002
  } finally {
12903
14003
  textStream.end();
@@ -12968,6 +14068,11 @@ async function initializeAssistantThread(event) {
12968
14068
  }
12969
14069
 
12970
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
+ }
12971
14076
  function createPrepareTurnState(deps) {
12972
14077
  return async function prepareTurnState(args) {
12973
14078
  const existingState = await args.thread.state;
@@ -12985,14 +14090,8 @@ function createPrepareTurnState(deps) {
12985
14090
  messageId: args.message.id,
12986
14091
  messageCreatedAtMs: args.message.metadata.dateSent.getTime()
12987
14092
  });
12988
- const messageHasPotentialImageAttachment = args.message.attachments.some(
12989
- (attachment) => {
12990
- if (attachment.type === "image") {
12991
- return true;
12992
- }
12993
- const mimeType = attachment.mimeType ?? "";
12994
- return attachment.type === "file" && mimeType.startsWith("image/");
12995
- }
14093
+ const messageHasPotentialImageAttachment = hasPotentialImageAttachment(
14094
+ args.message.attachments
12996
14095
  );
12997
14096
  const normalizedUserText = normalizeConversationText(args.userText) || "[non-text message]";
12998
14097
  const slackTs = getSlackMessageTs(args.message);
@@ -13017,7 +14116,8 @@ function createPrepareTurnState(deps) {
13017
14116
  conversation,
13018
14117
  incomingUserMessage
13019
14118
  );
13020
- if (isVisionEnabled() && (!conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment)) {
14119
+ const shouldHydrateVisionContext = !conversation.vision.backfillCompletedAtMs || messageHasPotentialImageAttachment || hasPendingImageHydration(conversation);
14120
+ if (isVisionEnabled() && shouldHydrateVisionContext) {
13021
14121
  await deps.hydrateConversationVisionContext(conversation, {
13022
14122
  threadId: args.context.threadId,
13023
14123
  channelId: args.context.channelId,
@@ -13113,7 +14213,7 @@ function createSlackRuntime(options) {
13113
14213
  slackTs,
13114
14214
  replied: false,
13115
14215
  skippedReason: decision.reason,
13116
- imagesHydrated: true
14216
+ imagesHydrated: !hasPotentialImageAttachment(message.attachments)
13117
14217
  }
13118
14218
  });
13119
14219
  conversation.processing.activeTurnId = void 0;
@@ -13397,6 +14497,166 @@ var JuniorChat = class extends Chat {
13397
14497
  }
13398
14498
  };
13399
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
+
13400
14660
  // src/chat/queue/thread-message-dispatcher.ts
13401
14661
  function rehydrateAttachmentFetchers(message, downloadPrivateSlackFile2 = downloadPrivateSlackFile) {
13402
14662
  for (const attachment of message.attachments) {
@@ -13493,8 +14753,10 @@ async function handleSlashCommand(event) {
13493
14753
  var productionBot;
13494
14754
  var productionSlackRuntime;
13495
14755
  function createProductionBot() {
14756
+ const logger = createChatSdkLogger();
13496
14757
  return new JuniorChat({
13497
14758
  userName: botConfig.userName,
14759
+ logger,
13498
14760
  concurrency: {
13499
14761
  strategy: "queue",
13500
14762
  // The SDK's default queueEntryTtlMs is 90s, but Junior turns can
@@ -13513,7 +14775,8 @@ function createProductionBot() {
13513
14775
  if (!signingSecret) {
13514
14776
  throw new Error("SLACK_SIGNING_SECRET is required");
13515
14777
  }
13516
- return createSlackAdapter({
14778
+ return createJuniorSlackAdapter({
14779
+ logger: logger.child("slack"),
13517
14780
  signingSecret,
13518
14781
  ...botToken ? { botToken } : {},
13519
14782
  ...clientId ? { clientId } : {},
@@ -13648,6 +14911,32 @@ function isMessageChangedEnvelope(value) {
13648
14911
  function textMentionsBot(text, botUserId) {
13649
14912
  return text.includes(`<@${botUserId}>`);
13650
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
+ }
13651
14940
  function extractMessageChangedMention(body, botUserId, adapter) {
13652
14941
  if (!isMessageChangedEnvelope(body)) return null;
13653
14942
  const { event } = body;
@@ -13673,7 +14962,7 @@ function extractMessageChangedMention(body, botUserId, adapter) {
13673
14962
  threadId,
13674
14963
  text: newText,
13675
14964
  isMention: true,
13676
- attachments: [],
14965
+ attachments: extractEditedMessageAttachments(event.message.files),
13677
14966
  metadata: { dateSent: new Date(Number(messageTs) * 1e3), edited: true },
13678
14967
  formatted: { type: "root", children: [] },
13679
14968
  raw,
@@ -13726,6 +15015,7 @@ async function handleAuthenticatedSlackMessageChangedMention(args) {
13726
15015
  if (!result) {
13727
15016
  return false;
13728
15017
  }
15018
+ rehydrateAttachmentFetchers(result.message);
13729
15019
  args.bot.processMessage(
13730
15020
  slackAdapter,
13731
15021
  result.threadId,
@@ -13847,7 +15137,7 @@ async function handlePlatformWebhook(request, platform, waitUntil, bot = getProd
13847
15137
  }
13848
15138
  });
13849
15139
  }
13850
- async function POST(request, platform, waitUntil) {
15140
+ async function POST2(request, platform, waitUntil) {
13851
15141
  return handlePlatformWebhook(request, platform, waitUntil);
13852
15142
  }
13853
15143
 
@@ -13900,8 +15190,11 @@ async function createApp(options) {
13900
15190
  app.get("/api/oauth/callback/:provider", (c) => {
13901
15191
  return GET5(c.req.raw, c.req.param("provider"), waitUntil);
13902
15192
  });
15193
+ app.post("/api/internal/turn-resume", (c) => {
15194
+ return POST(c.req.raw, waitUntil);
15195
+ });
13903
15196
  app.post("/api/webhooks/:platform", (c) => {
13904
- return POST(c.req.raw, c.req.param("platform"), waitUntil);
15197
+ return POST2(c.req.raw, c.req.param("platform"), waitUntil);
13905
15198
  });
13906
15199
  return app;
13907
15200
  }