@sentry/junior 0.44.0 → 0.45.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
@@ -1274,6 +1274,37 @@ async function addReactionToMessage(input) {
1274
1274
  }
1275
1275
  return { ok: true };
1276
1276
  }
1277
+ async function removeReactionFromMessage(input) {
1278
+ const channelId = requireSlackConversationId(
1279
+ input.channelId,
1280
+ "Slack reaction removal"
1281
+ );
1282
+ const timestamp = requireSlackMessageTimestamp(
1283
+ input.timestamp,
1284
+ "Slack reaction removal"
1285
+ );
1286
+ const emoji = normalizeSlackEmojiName(input.emoji);
1287
+ if (!emoji) {
1288
+ throw new Error("Slack reaction removal requires a valid emoji alias name");
1289
+ }
1290
+ try {
1291
+ await withSlackRetries(
1292
+ () => getSlackClient().reactions.remove({
1293
+ channel: channelId,
1294
+ timestamp,
1295
+ name: emoji
1296
+ }),
1297
+ 3,
1298
+ { action: "reactions.remove" }
1299
+ );
1300
+ } catch (error) {
1301
+ if (error instanceof SlackActionError && error.code === "no_reaction") {
1302
+ return { ok: true };
1303
+ }
1304
+ throw error;
1305
+ }
1306
+ return { ok: true };
1307
+ }
1277
1308
 
1278
1309
  // src/chat/oauth-flow.ts
1279
1310
  var OAUTH_STATE_TTL_MS = 10 * 60 * 1e3;
@@ -10620,86 +10651,477 @@ function normalizeToolResult(result, isSandboxResult) {
10620
10651
  };
10621
10652
  }
10622
10653
 
10623
- // src/chat/tools/execution/tool-error-handler.ts
10624
- function getToolErrorAttributes(error) {
10625
- if (!(error instanceof SlackActionError)) {
10626
- return {};
10654
+ // src/chat/credentials/unlink-provider.ts
10655
+ async function unlinkProvider(userId, provider, userTokenStore) {
10656
+ await Promise.all([
10657
+ userTokenStore.delete(userId, provider),
10658
+ deleteMcpStoredOAuthCredentials(userId, provider),
10659
+ deleteMcpServerSessionId(userId, provider),
10660
+ deleteMcpAuthSessionsForUserProvider(userId, provider)
10661
+ ]);
10662
+ }
10663
+
10664
+ // src/chat/state/turn-session-store.ts
10665
+ var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
10666
+ var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
10667
+ function agentTurnSessionKey(conversationId, sessionId) {
10668
+ return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
10669
+ }
10670
+ function parseAgentTurnSessionCheckpoint(value) {
10671
+ if (typeof value !== "string") {
10672
+ return void 0;
10627
10673
  }
10628
- return {
10629
- "app.slack.error_code": error.code,
10630
- ...error.apiError ? { "app.slack.api_error": error.apiError } : {},
10631
- ...error.detail ? { "app.slack.detail": error.detail } : {},
10632
- ...error.detailLine !== void 0 ? { "app.slack.detail_line": error.detailLine } : {},
10633
- ...error.detailRule ? { "app.slack.detail_rule": error.detailRule } : {}
10674
+ try {
10675
+ const parsed = JSON.parse(value);
10676
+ if (!isRecord(parsed)) {
10677
+ return void 0;
10678
+ }
10679
+ const status = parsed.state;
10680
+ if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed" && status !== "superseded") {
10681
+ return void 0;
10682
+ }
10683
+ const conversationId = parsed.conversationId;
10684
+ const sessionId = parsed.sessionId;
10685
+ const sliceId = parsed.sliceId;
10686
+ const checkpointVersion = parsed.checkpointVersion;
10687
+ const updatedAtMs = parsed.updatedAtMs;
10688
+ if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
10689
+ return void 0;
10690
+ }
10691
+ return {
10692
+ checkpointVersion,
10693
+ conversationId,
10694
+ sessionId,
10695
+ sliceId,
10696
+ state: status,
10697
+ updatedAtMs,
10698
+ piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
10699
+ ...Array.isArray(parsed.loadedSkillNames) ? {
10700
+ loadedSkillNames: parsed.loadedSkillNames.filter(
10701
+ (value2) => typeof value2 === "string"
10702
+ )
10703
+ } : {},
10704
+ ...parsed.resumeReason === "timeout" || parsed.resumeReason === "auth" ? { resumeReason: parsed.resumeReason } : {},
10705
+ ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
10706
+ ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
10707
+ };
10708
+ } catch {
10709
+ return void 0;
10710
+ }
10711
+ }
10712
+ async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
10713
+ const stateAdapter = getStateAdapter();
10714
+ await stateAdapter.connect();
10715
+ const value = await stateAdapter.get(
10716
+ agentTurnSessionKey(conversationId, sessionId)
10717
+ );
10718
+ return parseAgentTurnSessionCheckpoint(value);
10719
+ }
10720
+ async function upsertAgentTurnSessionCheckpoint(args) {
10721
+ const stateAdapter = getStateAdapter();
10722
+ await stateAdapter.connect();
10723
+ const existing = await getAgentTurnSessionCheckpoint(
10724
+ args.conversationId,
10725
+ args.sessionId
10726
+ );
10727
+ const checkpoint = {
10728
+ checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
10729
+ conversationId: args.conversationId,
10730
+ sessionId: args.sessionId,
10731
+ sliceId: args.sliceId,
10732
+ state: args.state,
10733
+ updatedAtMs: Date.now(),
10734
+ piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
10735
+ ...Array.isArray(args.loadedSkillNames) ? {
10736
+ loadedSkillNames: args.loadedSkillNames.filter(
10737
+ (value) => typeof value === "string"
10738
+ )
10739
+ } : {},
10740
+ ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
10741
+ ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
10742
+ ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
10634
10743
  };
10744
+ const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
10745
+ await stateAdapter.set(
10746
+ agentTurnSessionKey(args.conversationId, args.sessionId),
10747
+ JSON.stringify(checkpoint),
10748
+ ttlMs
10749
+ );
10750
+ return checkpoint;
10635
10751
  }
10636
- function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, traceContext) {
10637
- const errorType = getMcpAwareErrorType(error, "tool_execution_error");
10638
- const errorMessage = getMcpAwareErrorMessage(error);
10639
- setSpanAttributes({
10640
- "error.type": errorType
10752
+ async function supersedeAgentTurnSessionCheckpoint(args) {
10753
+ const existing = await getAgentTurnSessionCheckpoint(
10754
+ args.conversationId,
10755
+ args.sessionId
10756
+ );
10757
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "superseded") {
10758
+ return void 0;
10759
+ }
10760
+ return await upsertAgentTurnSessionCheckpoint({
10761
+ conversationId: existing.conversationId,
10762
+ sessionId: existing.sessionId,
10763
+ sliceId: existing.sliceId,
10764
+ state: "superseded",
10765
+ piMessages: existing.piMessages,
10766
+ loadedSkillNames: existing.loadedSkillNames,
10767
+ resumeReason: existing.resumeReason,
10768
+ resumedFromSliceId: existing.resumedFromSliceId,
10769
+ errorMessage: args.errorMessage ?? existing.errorMessage
10641
10770
  });
10642
- if (shouldTrace) {
10643
- logWarn(
10644
- "agent_tool_call_failed",
10645
- traceContext,
10646
- {
10647
- "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
10648
- "gen_ai.operation.name": "execute_tool",
10649
- "gen_ai.tool.name": toolName,
10650
- ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
10651
- "error.type": errorType,
10652
- "exception.message": errorMessage
10653
- },
10654
- "Agent tool call failed"
10655
- );
10771
+ }
10772
+
10773
+ // src/chat/services/pending-auth.ts
10774
+ var AUTH_LINK_REUSE_WINDOW_MS = 10 * 60 * 1e3;
10775
+ function canReusePendingAuthLink(args) {
10776
+ const { pendingAuth } = args;
10777
+ if (!pendingAuth) {
10778
+ return false;
10656
10779
  }
10657
- if (!(error instanceof McpToolError)) {
10658
- logException(
10659
- error,
10660
- "agent_tool_call_failed",
10661
- {},
10662
- {
10663
- "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
10664
- "gen_ai.operation.name": "execute_tool",
10665
- "gen_ai.tool.name": toolName,
10666
- ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
10667
- ...getToolErrorAttributes(error)
10668
- },
10669
- "Agent tool call failed"
10670
- );
10780
+ return pendingAuth.kind === args.kind && pendingAuth.provider === args.provider && pendingAuth.requesterId === args.requesterId && pendingAuth.linkSentAtMs + AUTH_LINK_REUSE_WINDOW_MS > (args.nowMs ?? Date.now());
10781
+ }
10782
+ function getConversationPendingAuth(args) {
10783
+ const pendingAuth = args.conversation.processing.pendingAuth;
10784
+ if (!pendingAuth) {
10785
+ return void 0;
10671
10786
  }
10672
- throw error;
10787
+ if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId) {
10788
+ return void 0;
10789
+ }
10790
+ return pendingAuth;
10791
+ }
10792
+ function clearPendingAuth(conversation, sessionId) {
10793
+ if (!conversation.processing.pendingAuth) {
10794
+ return;
10795
+ }
10796
+ if (sessionId && conversation.processing.pendingAuth.sessionId !== sessionId) {
10797
+ return;
10798
+ }
10799
+ conversation.processing.pendingAuth = void 0;
10800
+ }
10801
+ async function applyPendingAuthUpdate(args) {
10802
+ const previousPendingAuth = args.conversation.processing.pendingAuth;
10803
+ args.conversation.processing.pendingAuth = args.nextPendingAuth;
10804
+ if (previousPendingAuth && previousPendingAuth.sessionId !== args.nextPendingAuth.sessionId && args.conversationId) {
10805
+ await supersedeAgentTurnSessionCheckpoint({
10806
+ conversationId: args.conversationId,
10807
+ sessionId: previousPendingAuth.sessionId,
10808
+ errorMessage: "Superseded by a newer auth-blocked request in the same conversation."
10809
+ });
10810
+ }
10811
+ }
10812
+ function isPendingAuthLatestRequest(conversation, pendingAuth) {
10813
+ for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
10814
+ const message = conversation.messages[index];
10815
+ if (message?.role !== "user") {
10816
+ continue;
10817
+ }
10818
+ return buildDeterministicTurnId(message.id) === pendingAuth.sessionId;
10819
+ }
10820
+ return false;
10673
10821
  }
10674
10822
 
10675
- // src/chat/tools/agent-tools.ts
10676
- function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, pluginAuthOrchestration, onToolCall) {
10677
- const shouldTrace = shouldEmitDevAgentTrace();
10678
- return Object.entries(tools).map(([toolName, toolDef]) => ({
10679
- name: toolName,
10680
- label: toolName,
10681
- description: toolDef.description,
10682
- parameters: toolDef.inputSchema,
10683
- prepareArguments: toolDef.prepareArguments,
10684
- executionMode: toolDef.executionMode,
10685
- execute: async (toolCallId, params) => {
10686
- const normalizedToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : void 0;
10687
- const toolArgumentsAttribute = serializeGenAiAttribute(params);
10688
- if (toolName === "reportProgress") {
10689
- const status = buildReportedProgressStatus(params);
10690
- if (status) {
10691
- await onStatus?.(status);
10692
- }
10693
- }
10694
- return withSpan(
10695
- `execute_tool ${toolName}`,
10696
- "gen_ai.execute_tool",
10697
- spanContext,
10698
- async () => {
10699
- const parsed = params;
10700
- onToolCall?.(toolName, parsed);
10701
- try {
10702
- if (typeof toolDef.execute !== "function") {
10823
+ // src/chat/services/plugin-auth-orchestration.ts
10824
+ var PluginAuthorizationPauseError = class extends AuthorizationPauseError {
10825
+ constructor(provider, disposition) {
10826
+ super("plugin", provider, disposition);
10827
+ }
10828
+ };
10829
+ var PluginCredentialFailureError = class extends Error {
10830
+ provider;
10831
+ constructor(provider, message) {
10832
+ super(message);
10833
+ this.name = "PluginCredentialFailureError";
10834
+ this.provider = provider;
10835
+ }
10836
+ };
10837
+ function isCommandAuthFailure(details) {
10838
+ if (!details || typeof details !== "object") {
10839
+ return false;
10840
+ }
10841
+ const result = details;
10842
+ if (typeof result.exit_code !== "number" || result.exit_code === 0) {
10843
+ return false;
10844
+ }
10845
+ const text = `${typeof result.stdout === "string" ? result.stdout : ""}
10846
+ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
10847
+ if (!text.trim()) {
10848
+ return false;
10849
+ }
10850
+ return [
10851
+ /\bjunior-auth-required\b/,
10852
+ /\b401\b/,
10853
+ /\bunauthorized\b/,
10854
+ /\bbad credentials\b/,
10855
+ /\binvalid token\b/,
10856
+ /\bgithub_token\b.*\binvalid\b/,
10857
+ /\btoken (?:expired|revoked)\b/,
10858
+ /\bexpired token\b/,
10859
+ /\bmissing scopes?\b/,
10860
+ /\binsufficient scope\b/,
10861
+ /\binvalid grant\b/,
10862
+ /\breauthoriz/
10863
+ ].some((pattern) => pattern.test(text));
10864
+ }
10865
+ function commandText(details) {
10866
+ if (!details || typeof details !== "object") {
10867
+ return "";
10868
+ }
10869
+ const result = details;
10870
+ return `${typeof result.stdout === "string" ? result.stdout : ""}
10871
+ ${typeof result.stderr === "string" ? result.stderr : ""}`;
10872
+ }
10873
+ function isGitHubSmartHttpAuthFailure(provider, command, details) {
10874
+ if (provider !== "github" || !/^\s*(?:gh|git)\b/i.test(command)) {
10875
+ return false;
10876
+ }
10877
+ const text = commandText(details).toLowerCase();
10878
+ return /\bgzip:\s*invalid header\b/.test(text);
10879
+ }
10880
+ function explicitAuthRequiredProvider(details) {
10881
+ const match = /\bjunior-auth-required\s+provider=([a-z0-9-]+)\b/.exec(
10882
+ commandText(details).toLowerCase()
10883
+ );
10884
+ return match?.[1];
10885
+ }
10886
+ function registeredProviderNames() {
10887
+ const providers = /* @__PURE__ */ new Set();
10888
+ for (const plugin of getPluginProviders()) {
10889
+ const domains = [
10890
+ ...plugin.manifest.credentials?.domains ?? [],
10891
+ ...plugin.manifest.domains ?? []
10892
+ ];
10893
+ if (domains.length > 0) {
10894
+ providers.add(plugin.manifest.name);
10895
+ }
10896
+ }
10897
+ return [...providers].sort((left, right) => left.localeCompare(right));
10898
+ }
10899
+ function commandTargetsProvider(provider, command, details) {
10900
+ const normalizedCommand = command.trim().toLowerCase();
10901
+ if (!normalizedCommand) {
10902
+ return false;
10903
+ }
10904
+ if (provider === "github" && /^(gh|git)\b/.test(normalizedCommand)) {
10905
+ return true;
10906
+ }
10907
+ const plugin = getPluginDefinition(provider);
10908
+ const candidates = /* @__PURE__ */ new Set([provider.toLowerCase()]);
10909
+ const manifest = plugin?.manifest;
10910
+ const credentials = manifest?.credentials;
10911
+ if (credentials) {
10912
+ candidates.add(credentials.authTokenEnv.toLowerCase());
10913
+ for (const domain of credentials.domains) {
10914
+ candidates.add(domain.toLowerCase());
10915
+ }
10916
+ }
10917
+ for (const domain of manifest?.domains ?? []) {
10918
+ candidates.add(domain.toLowerCase());
10919
+ }
10920
+ const combinedText = `${normalizedCommand}
10921
+ ${commandText(details).toLowerCase()}`;
10922
+ return [...candidates].some((candidate) => combinedText.includes(candidate));
10923
+ }
10924
+ function formatCommand(command) {
10925
+ const collapsed = command.replace(/\s+/g, " ").trim();
10926
+ return collapsed.length > 160 ? `${collapsed.slice(0, 157)}...` : collapsed;
10927
+ }
10928
+ function buildCredentialFailureError(provider, command) {
10929
+ const providerLabel = provider === "github" ? "GitHub" : formatProviderLabel(provider);
10930
+ const plugin = getPluginDefinition(provider);
10931
+ const credentialType = plugin?.manifest.credentials?.type;
10932
+ const commandSummary = formatCommand(command);
10933
+ const remediation = provider === "github" && credentialType === "github-app" ? "Verify the GitHub App installation covers the target repository and the host GitHub App environment variables are current." : `Verify the ${providerLabel} provider credentials before retrying.`;
10934
+ return new PluginCredentialFailureError(
10935
+ provider,
10936
+ `${providerLabel} credentials were rejected while running \`${commandSummary}\`. ${remediation}`
10937
+ );
10938
+ }
10939
+ function createPluginAuthOrchestration(deps, abortAgent) {
10940
+ let pendingPause;
10941
+ const startAuthorizationPause = async (provider, activeSkill, options) => {
10942
+ if (pendingPause) {
10943
+ throw pendingPause;
10944
+ }
10945
+ if (!deps.requesterId || !getPluginOAuthConfig(provider)) {
10946
+ throw new Error(`Cannot start plugin authorization for ${provider}`);
10947
+ }
10948
+ const providerLabel = formatProviderLabel(provider);
10949
+ const reusingPendingLink = canReusePendingAuthLink({
10950
+ pendingAuth: deps.currentPendingAuth,
10951
+ kind: "plugin",
10952
+ provider,
10953
+ requesterId: deps.requesterId
10954
+ });
10955
+ if (!reusingPendingLink) {
10956
+ const oauthResult = await startOAuthFlow(provider, {
10957
+ requesterId: deps.requesterId,
10958
+ channelId: deps.channelId,
10959
+ threadTs: deps.threadTs,
10960
+ userMessage: deps.userMessage,
10961
+ channelConfiguration: deps.channelConfiguration,
10962
+ activeSkillName: activeSkill?.name ?? void 0,
10963
+ resumeConversationId: deps.conversationId,
10964
+ resumeSessionId: deps.sessionId
10965
+ });
10966
+ if (!oauthResult.ok) {
10967
+ throw new Error(oauthResult.error);
10968
+ }
10969
+ if (!oauthResult.delivery) {
10970
+ throw new Error(
10971
+ `I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try again.`
10972
+ );
10973
+ }
10974
+ }
10975
+ if (options?.unlinkExistingProvider && deps.requesterId && deps.userTokenStore) {
10976
+ await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
10977
+ }
10978
+ if (deps.sessionId) {
10979
+ await deps.onPendingAuth?.({
10980
+ kind: "plugin",
10981
+ provider,
10982
+ requesterId: deps.requesterId,
10983
+ sessionId: deps.sessionId,
10984
+ linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
10985
+ });
10986
+ }
10987
+ pendingPause = new PluginAuthorizationPauseError(
10988
+ provider,
10989
+ reusingPendingLink ? "link_already_sent" : "link_sent"
10990
+ );
10991
+ abortAgent();
10992
+ throw pendingPause;
10993
+ };
10994
+ return {
10995
+ handleCommandFailure: async (input) => {
10996
+ const providers = registeredProviderNames();
10997
+ const explicitProvider = explicitAuthRequiredProvider(input.details);
10998
+ const provider = explicitProvider && providers.includes(explicitProvider) ? explicitProvider : providers.find(
10999
+ (availableProvider) => commandTargetsProvider(
11000
+ availableProvider,
11001
+ input.command,
11002
+ input.details
11003
+ )
11004
+ );
11005
+ if (!provider) {
11006
+ return;
11007
+ }
11008
+ const authFailure = isCommandAuthFailure(input.details) || isGitHubSmartHttpAuthFailure(provider, input.command, input.details);
11009
+ if (!authFailure) {
11010
+ return;
11011
+ }
11012
+ if (!deps.requesterId || !deps.userTokenStore) {
11013
+ throw buildCredentialFailureError(provider, input.command);
11014
+ }
11015
+ if (!getPluginOAuthConfig(provider)) {
11016
+ throw buildCredentialFailureError(provider, input.command);
11017
+ }
11018
+ await startAuthorizationPause(provider, input.activeSkill, {
11019
+ unlinkExistingProvider: true
11020
+ });
11021
+ },
11022
+ getPendingPause: () => pendingPause
11023
+ };
11024
+ }
11025
+
11026
+ // src/chat/tools/execution/tool-error-handler.ts
11027
+ function getToolErrorAttributes(error) {
11028
+ if (!(error instanceof SlackActionError)) {
11029
+ return {};
11030
+ }
11031
+ return {
11032
+ "app.slack.error_code": error.code,
11033
+ ...error.apiError ? { "app.slack.api_error": error.apiError } : {},
11034
+ ...error.detail ? { "app.slack.detail": error.detail } : {},
11035
+ ...error.detailLine !== void 0 ? { "app.slack.detail_line": error.detailLine } : {},
11036
+ ...error.detailRule ? { "app.slack.detail_rule": error.detailRule } : {}
11037
+ };
11038
+ }
11039
+ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, traceContext) {
11040
+ const errorType = getMcpAwareErrorType(error, "tool_execution_error");
11041
+ const errorMessage = getMcpAwareErrorMessage(error);
11042
+ setSpanAttributes({
11043
+ "error.type": errorType,
11044
+ ...error instanceof PluginCredentialFailureError ? { "app.credential.provider": error.provider } : {}
11045
+ });
11046
+ if (error instanceof PluginCredentialFailureError) {
11047
+ if (shouldTrace) {
11048
+ logInfo(
11049
+ "plugin_credential_rejected",
11050
+ traceContext,
11051
+ {
11052
+ "app.credential.provider": error.provider,
11053
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
11054
+ "gen_ai.operation.name": "execute_tool",
11055
+ "gen_ai.tool.name": toolName,
11056
+ ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
11057
+ "error.type": errorType
11058
+ },
11059
+ "Plugin credentials were rejected during tool execution"
11060
+ );
11061
+ }
11062
+ throw error;
11063
+ }
11064
+ if (shouldTrace) {
11065
+ logWarn(
11066
+ "agent_tool_call_failed",
11067
+ traceContext,
11068
+ {
11069
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
11070
+ "gen_ai.operation.name": "execute_tool",
11071
+ "gen_ai.tool.name": toolName,
11072
+ ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
11073
+ "error.type": errorType,
11074
+ "exception.message": errorMessage
11075
+ },
11076
+ "Agent tool call failed"
11077
+ );
11078
+ }
11079
+ if (!(error instanceof McpToolError)) {
11080
+ logException(
11081
+ error,
11082
+ "agent_tool_call_failed",
11083
+ {},
11084
+ {
11085
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
11086
+ "gen_ai.operation.name": "execute_tool",
11087
+ "gen_ai.tool.name": toolName,
11088
+ ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
11089
+ ...getToolErrorAttributes(error)
11090
+ },
11091
+ "Agent tool call failed"
11092
+ );
11093
+ }
11094
+ throw error;
11095
+ }
11096
+
11097
+ // src/chat/tools/agent-tools.ts
11098
+ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, pluginAuthOrchestration, onToolCall) {
11099
+ const shouldTrace = shouldEmitDevAgentTrace();
11100
+ return Object.entries(tools).map(([toolName, toolDef]) => ({
11101
+ name: toolName,
11102
+ label: toolName,
11103
+ description: toolDef.description,
11104
+ parameters: toolDef.inputSchema,
11105
+ prepareArguments: toolDef.prepareArguments,
11106
+ executionMode: toolDef.executionMode,
11107
+ execute: async (toolCallId, params) => {
11108
+ const normalizedToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : void 0;
11109
+ const toolArgumentsAttribute = serializeGenAiAttribute(params);
11110
+ if (toolName === "reportProgress") {
11111
+ const status = buildReportedProgressStatus(params);
11112
+ if (status) {
11113
+ await onStatus?.(status);
11114
+ }
11115
+ }
11116
+ return withSpan(
11117
+ `execute_tool ${toolName}`,
11118
+ "gen_ai.execute_tool",
11119
+ spanContext,
11120
+ async () => {
11121
+ const parsed = params;
11122
+ onToolCall?.(toolName, parsed);
11123
+ try {
11124
+ if (typeof toolDef.execute !== "function") {
10703
11125
  const resultDetails = { ok: true };
10704
11126
  const toolResultAttribute2 = serializeGenAiAttribute(resultDetails);
10705
11127
  if (toolResultAttribute2) {
@@ -10997,9 +11419,31 @@ var TURN_THINKING_LEVELS = [
10997
11419
  "high",
10998
11420
  "xhigh"
10999
11421
  ];
11422
+ var CONFIDENCE_LABELS = {
11423
+ low: 0.5,
11424
+ medium: CLASSIFIER_CONFIDENCE_THRESHOLD,
11425
+ high: 0.9
11426
+ };
11427
+ function coerceClassifierConfidence(value) {
11428
+ if (typeof value !== "string") {
11429
+ return value;
11430
+ }
11431
+ const trimmed = value.trim().toLowerCase();
11432
+ if (!trimmed) {
11433
+ return value;
11434
+ }
11435
+ const numeric = Number.parseFloat(trimmed);
11436
+ if (Number.isFinite(numeric)) {
11437
+ return numeric;
11438
+ }
11439
+ return CONFIDENCE_LABELS[trimmed] ?? value;
11440
+ }
11000
11441
  var turnExecutionProfileSchema = z.object({
11001
11442
  thinking_level: z.enum(TURN_THINKING_LEVELS),
11002
- confidence: z.number().min(0).max(1),
11443
+ confidence: z.preprocess(
11444
+ coerceClassifierConfidence,
11445
+ z.number().min(0).max(1)
11446
+ ),
11003
11447
  reason: z.string().min(1)
11004
11448
  });
11005
11449
  var DEFAULT_THINKING_LEVEL = "medium";
@@ -11044,7 +11488,8 @@ function buildClassifierSystemPrompt() {
11044
11488
  "",
11045
11489
  "Classify based on the substance of the task, not the length of the current message. When the current instruction is a short affirmation (for example: 'go', 'do it', 'yes please', 'proceed') and the thread-background contains a pending task, classify the pending task \u2014 not the affirmation.",
11046
11490
  "",
11047
- "Return JSON only with thinking_level, confidence, and reason."
11491
+ "Return JSON only with thinking_level, confidence, and reason.",
11492
+ "confidence must be a number from 0 to 1, not a word label."
11048
11493
  ].join("\n");
11049
11494
  }
11050
11495
  function buildClassifierPrompt(args) {
@@ -11179,120 +11624,11 @@ function toAgentThinkingLevel(level) {
11179
11624
  return "low";
11180
11625
  case "medium":
11181
11626
  return "medium";
11182
- case "high":
11183
- return "high";
11184
- case "xhigh":
11185
- return "xhigh";
11186
- }
11187
- }
11188
-
11189
- // src/chat/state/turn-session-store.ts
11190
- var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
11191
- var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
11192
- function agentTurnSessionKey(conversationId, sessionId) {
11193
- return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
11194
- }
11195
- function parseAgentTurnSessionCheckpoint(value) {
11196
- if (typeof value !== "string") {
11197
- return void 0;
11198
- }
11199
- try {
11200
- const parsed = JSON.parse(value);
11201
- if (!isRecord(parsed)) {
11202
- return void 0;
11203
- }
11204
- const status = parsed.state;
11205
- if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed" && status !== "superseded") {
11206
- return void 0;
11207
- }
11208
- const conversationId = parsed.conversationId;
11209
- const sessionId = parsed.sessionId;
11210
- const sliceId = parsed.sliceId;
11211
- const checkpointVersion = parsed.checkpointVersion;
11212
- const updatedAtMs = parsed.updatedAtMs;
11213
- if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
11214
- return void 0;
11215
- }
11216
- return {
11217
- checkpointVersion,
11218
- conversationId,
11219
- sessionId,
11220
- sliceId,
11221
- state: status,
11222
- updatedAtMs,
11223
- piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
11224
- ...Array.isArray(parsed.loadedSkillNames) ? {
11225
- loadedSkillNames: parsed.loadedSkillNames.filter(
11226
- (value2) => typeof value2 === "string"
11227
- )
11228
- } : {},
11229
- ...parsed.resumeReason === "timeout" || parsed.resumeReason === "auth" ? { resumeReason: parsed.resumeReason } : {},
11230
- ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
11231
- ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
11232
- };
11233
- } catch {
11234
- return void 0;
11235
- }
11236
- }
11237
- async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
11238
- const stateAdapter = getStateAdapter();
11239
- await stateAdapter.connect();
11240
- const value = await stateAdapter.get(
11241
- agentTurnSessionKey(conversationId, sessionId)
11242
- );
11243
- return parseAgentTurnSessionCheckpoint(value);
11244
- }
11245
- async function upsertAgentTurnSessionCheckpoint(args) {
11246
- const stateAdapter = getStateAdapter();
11247
- await stateAdapter.connect();
11248
- const existing = await getAgentTurnSessionCheckpoint(
11249
- args.conversationId,
11250
- args.sessionId
11251
- );
11252
- const checkpoint = {
11253
- checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
11254
- conversationId: args.conversationId,
11255
- sessionId: args.sessionId,
11256
- sliceId: args.sliceId,
11257
- state: args.state,
11258
- updatedAtMs: Date.now(),
11259
- piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
11260
- ...Array.isArray(args.loadedSkillNames) ? {
11261
- loadedSkillNames: args.loadedSkillNames.filter(
11262
- (value) => typeof value === "string"
11263
- )
11264
- } : {},
11265
- ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
11266
- ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
11267
- ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
11268
- };
11269
- const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
11270
- await stateAdapter.set(
11271
- agentTurnSessionKey(args.conversationId, args.sessionId),
11272
- JSON.stringify(checkpoint),
11273
- ttlMs
11274
- );
11275
- return checkpoint;
11276
- }
11277
- async function supersedeAgentTurnSessionCheckpoint(args) {
11278
- const existing = await getAgentTurnSessionCheckpoint(
11279
- args.conversationId,
11280
- args.sessionId
11281
- );
11282
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "superseded") {
11283
- return void 0;
11627
+ case "high":
11628
+ return "high";
11629
+ case "xhigh":
11630
+ return "xhigh";
11284
11631
  }
11285
- return await upsertAgentTurnSessionCheckpoint({
11286
- conversationId: existing.conversationId,
11287
- sessionId: existing.sessionId,
11288
- sliceId: existing.sliceId,
11289
- state: "superseded",
11290
- piMessages: existing.piMessages,
11291
- loadedSkillNames: existing.loadedSkillNames,
11292
- resumeReason: existing.resumeReason,
11293
- resumedFromSliceId: existing.resumedFromSliceId,
11294
- errorMessage: args.errorMessage ?? existing.errorMessage
11295
- });
11296
11632
  }
11297
11633
 
11298
11634
  // src/chat/services/turn-checkpoint.ts
@@ -11399,329 +11735,103 @@ async function persistTimeoutCheckpoint(args) {
11399
11735
  {
11400
11736
  "app.ai.resume_conversation_id": args.conversationId,
11401
11737
  "app.ai.resume_session_id": args.sessionId,
11402
- "app.ai.resume_from_slice_id": args.currentSliceId,
11403
- "app.ai.resume_next_slice_id": nextSliceId
11404
- },
11405
- "Failed to persist timeout checkpoint before scheduling resume"
11406
- );
11407
- return void 0;
11408
- }
11409
- }
11410
-
11411
- // src/chat/services/pending-auth.ts
11412
- var AUTH_LINK_REUSE_WINDOW_MS = 10 * 60 * 1e3;
11413
- function canReusePendingAuthLink(args) {
11414
- const { pendingAuth } = args;
11415
- if (!pendingAuth) {
11416
- return false;
11417
- }
11418
- return pendingAuth.kind === args.kind && pendingAuth.provider === args.provider && pendingAuth.requesterId === args.requesterId && pendingAuth.linkSentAtMs + AUTH_LINK_REUSE_WINDOW_MS > (args.nowMs ?? Date.now());
11419
- }
11420
- function getConversationPendingAuth(args) {
11421
- const pendingAuth = args.conversation.processing.pendingAuth;
11422
- if (!pendingAuth) {
11423
- return void 0;
11424
- }
11425
- if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId) {
11426
- return void 0;
11427
- }
11428
- return pendingAuth;
11429
- }
11430
- function clearPendingAuth(conversation, sessionId) {
11431
- if (!conversation.processing.pendingAuth) {
11432
- return;
11433
- }
11434
- if (sessionId && conversation.processing.pendingAuth.sessionId !== sessionId) {
11435
- return;
11436
- }
11437
- conversation.processing.pendingAuth = void 0;
11438
- }
11439
- async function applyPendingAuthUpdate(args) {
11440
- const previousPendingAuth = args.conversation.processing.pendingAuth;
11441
- args.conversation.processing.pendingAuth = args.nextPendingAuth;
11442
- if (previousPendingAuth && previousPendingAuth.sessionId !== args.nextPendingAuth.sessionId && args.conversationId) {
11443
- await supersedeAgentTurnSessionCheckpoint({
11444
- conversationId: args.conversationId,
11445
- sessionId: previousPendingAuth.sessionId,
11446
- errorMessage: "Superseded by a newer auth-blocked request in the same conversation."
11447
- });
11448
- }
11449
- }
11450
- function isPendingAuthLatestRequest(conversation, pendingAuth) {
11451
- for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
11452
- const message = conversation.messages[index];
11453
- if (message?.role !== "user") {
11454
- continue;
11455
- }
11456
- return buildDeterministicTurnId(message.id) === pendingAuth.sessionId;
11457
- }
11458
- return false;
11459
- }
11460
-
11461
- // src/chat/services/mcp-auth-orchestration.ts
11462
- var McpAuthorizationPauseError = class extends AuthorizationPauseError {
11463
- constructor(provider, disposition) {
11464
- super("mcp", provider, disposition);
11465
- }
11466
- };
11467
- function createMcpAuthOrchestration(deps, abortAgent) {
11468
- let pendingPause;
11469
- const authSessionIdsByProvider = /* @__PURE__ */ new Map();
11470
- const authProviderFactory = async (plugin) => {
11471
- if (!deps.conversationId || !deps.sessionId || !deps.requesterId) {
11472
- return void 0;
11473
- }
11474
- const provider = await createMcpOAuthClientProvider({
11475
- provider: plugin.manifest.name,
11476
- conversationId: deps.conversationId,
11477
- sessionId: deps.sessionId,
11478
- userId: deps.requesterId,
11479
- userMessage: deps.userMessage,
11480
- ...deps.channelId ? { channelId: deps.channelId } : {},
11481
- ...deps.threadTs ? { threadTs: deps.threadTs } : {},
11482
- ...deps.toolChannelId ? { toolChannelId: deps.toolChannelId } : {},
11483
- configuration: deps.getConfiguration(),
11484
- artifactState: deps.getArtifactState()
11485
- });
11486
- authSessionIdsByProvider.set(plugin.manifest.name, provider.authSessionId);
11487
- return provider;
11488
- };
11489
- const onAuthorizationRequired = async (provider) => {
11490
- if (pendingPause) {
11491
- return true;
11492
- }
11493
- const authSessionId = authSessionIdsByProvider.get(provider);
11494
- if (!authSessionId || !deps.requesterId) {
11495
- throw new Error(
11496
- `Missing MCP auth session context for plugin "${provider}"`
11497
- );
11498
- }
11499
- const latestArtifactState = deps.getMergedArtifactState();
11500
- await patchMcpAuthSession(authSessionId, {
11501
- configuration: { ...deps.getConfiguration() },
11502
- artifactState: latestArtifactState,
11503
- toolChannelId: deps.toolChannelId ?? latestArtifactState.assistantContextChannelId ?? deps.channelId
11504
- });
11505
- const authSession = await getMcpAuthSession(authSessionId);
11506
- if (!authSession?.authorizationUrl) {
11507
- throw new Error(`Missing MCP authorization URL for plugin "${provider}"`);
11508
- }
11509
- const reusingPendingLink = canReusePendingAuthLink({
11510
- pendingAuth: deps.currentPendingAuth,
11511
- kind: "mcp",
11512
- provider,
11513
- requesterId: deps.requesterId
11514
- });
11515
- if (!reusingPendingLink) {
11516
- const delivery = await deliverPrivateMessage({
11517
- channelId: authSession.channelId,
11518
- threadTs: authSession.threadTs,
11519
- userId: authSession.userId,
11520
- text: `<${authSession.authorizationUrl}|Click here to link your ${formatProviderLabel(provider)} MCP access>. Once you've authorized, this thread will continue automatically.`
11521
- });
11522
- if (!delivery) {
11523
- throw new Error(
11524
- `Unable to deliver MCP authorization link for plugin "${provider}"`
11525
- );
11526
- }
11527
- } else {
11528
- await deleteMcpAuthSession(authSessionId);
11529
- }
11530
- if (deps.sessionId && deps.requesterId) {
11531
- await deps.onPendingAuth?.({
11532
- kind: "mcp",
11533
- provider,
11534
- requesterId: deps.requesterId,
11535
- sessionId: deps.sessionId,
11536
- linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
11537
- });
11538
- }
11539
- pendingPause = new McpAuthorizationPauseError(
11540
- provider,
11541
- reusingPendingLink ? "link_already_sent" : "link_sent"
11542
- );
11543
- abortAgent();
11544
- return true;
11545
- };
11546
- return {
11547
- authProviderFactory,
11548
- onAuthorizationRequired,
11549
- getPendingPause: () => pendingPause
11550
- };
11551
- }
11552
-
11553
- // src/chat/credentials/unlink-provider.ts
11554
- async function unlinkProvider(userId, provider, userTokenStore) {
11555
- await Promise.all([
11556
- userTokenStore.delete(userId, provider),
11557
- deleteMcpStoredOAuthCredentials(userId, provider),
11558
- deleteMcpServerSessionId(userId, provider),
11559
- deleteMcpAuthSessionsForUserProvider(userId, provider)
11560
- ]);
11561
- }
11562
-
11563
- // src/chat/services/plugin-auth-orchestration.ts
11564
- var PluginAuthorizationPauseError = class extends AuthorizationPauseError {
11565
- constructor(provider, disposition) {
11566
- super("plugin", provider, disposition);
11567
- }
11568
- };
11569
- function isCommandAuthFailure(details) {
11570
- if (!details || typeof details !== "object") {
11571
- return false;
11572
- }
11573
- const result = details;
11574
- if (typeof result.exit_code !== "number" || result.exit_code === 0) {
11575
- return false;
11576
- }
11577
- const text = `${typeof result.stdout === "string" ? result.stdout : ""}
11578
- ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
11579
- if (!text.trim()) {
11580
- return false;
11581
- }
11582
- return [
11583
- /\bjunior-auth-required\b/,
11584
- /\b401\b/,
11585
- /\bunauthorized\b/,
11586
- /\bbad credentials\b/,
11587
- /\binvalid token\b/,
11588
- /\btoken (?:expired|revoked)\b/,
11589
- /\bexpired token\b/,
11590
- /\bmissing scopes?\b/,
11591
- /\binsufficient scope\b/,
11592
- /\binvalid grant\b/,
11593
- /\breauthoriz/
11594
- ].some((pattern) => pattern.test(text));
11595
- }
11596
- function commandText(details) {
11597
- if (!details || typeof details !== "object") {
11598
- return "";
11599
- }
11600
- const result = details;
11601
- return `${typeof result.stdout === "string" ? result.stdout : ""}
11602
- ${typeof result.stderr === "string" ? result.stderr : ""}`;
11603
- }
11604
- function explicitAuthRequiredProvider(details) {
11605
- const match = /\bjunior-auth-required\s+provider=([a-z0-9-]+)\b/.exec(
11606
- commandText(details).toLowerCase()
11607
- );
11608
- return match?.[1];
11609
- }
11610
- function registeredProviderNames() {
11611
- const providers = /* @__PURE__ */ new Set();
11612
- for (const plugin of getPluginProviders()) {
11613
- const domains = [
11614
- ...plugin.manifest.credentials?.domains ?? [],
11615
- ...plugin.manifest.domains ?? []
11616
- ];
11617
- if (domains.length > 0) {
11618
- providers.add(plugin.manifest.name);
11619
- }
11620
- }
11621
- return [...providers].sort((left, right) => left.localeCompare(right));
11622
- }
11623
- function commandTargetsProvider(provider, command, details) {
11624
- const normalizedCommand = command.trim().toLowerCase();
11625
- if (!normalizedCommand) {
11626
- return false;
11627
- }
11628
- if (provider === "github" && /^(gh|git)\b/.test(normalizedCommand)) {
11629
- return true;
11630
- }
11631
- const plugin = getPluginDefinition(provider);
11632
- const candidates = /* @__PURE__ */ new Set([provider.toLowerCase()]);
11633
- const manifest = plugin?.manifest;
11634
- const credentials = manifest?.credentials;
11635
- if (credentials) {
11636
- candidates.add(credentials.authTokenEnv.toLowerCase());
11637
- for (const domain of credentials.domains) {
11638
- candidates.add(domain.toLowerCase());
11639
- }
11640
- }
11641
- for (const domain of manifest?.domains ?? []) {
11642
- candidates.add(domain.toLowerCase());
11738
+ "app.ai.resume_from_slice_id": args.currentSliceId,
11739
+ "app.ai.resume_next_slice_id": nextSliceId
11740
+ },
11741
+ "Failed to persist timeout checkpoint before scheduling resume"
11742
+ );
11743
+ return void 0;
11643
11744
  }
11644
- const combinedText = `${normalizedCommand}
11645
- ${commandText(details).toLowerCase()}`;
11646
- return [...candidates].some((candidate) => combinedText.includes(candidate));
11647
11745
  }
11648
- function createPluginAuthOrchestration(deps, abortAgent) {
11746
+
11747
+ // src/chat/services/mcp-auth-orchestration.ts
11748
+ var McpAuthorizationPauseError = class extends AuthorizationPauseError {
11749
+ constructor(provider, disposition) {
11750
+ super("mcp", provider, disposition);
11751
+ }
11752
+ };
11753
+ function createMcpAuthOrchestration(deps, abortAgent) {
11649
11754
  let pendingPause;
11650
- const startAuthorizationPause = async (provider, activeSkill, options) => {
11755
+ const authSessionIdsByProvider = /* @__PURE__ */ new Map();
11756
+ const authProviderFactory = async (plugin) => {
11757
+ if (!deps.conversationId || !deps.sessionId || !deps.requesterId) {
11758
+ return void 0;
11759
+ }
11760
+ const provider = await createMcpOAuthClientProvider({
11761
+ provider: plugin.manifest.name,
11762
+ conversationId: deps.conversationId,
11763
+ sessionId: deps.sessionId,
11764
+ userId: deps.requesterId,
11765
+ userMessage: deps.userMessage,
11766
+ ...deps.channelId ? { channelId: deps.channelId } : {},
11767
+ ...deps.threadTs ? { threadTs: deps.threadTs } : {},
11768
+ ...deps.toolChannelId ? { toolChannelId: deps.toolChannelId } : {},
11769
+ configuration: deps.getConfiguration(),
11770
+ artifactState: deps.getArtifactState()
11771
+ });
11772
+ authSessionIdsByProvider.set(plugin.manifest.name, provider.authSessionId);
11773
+ return provider;
11774
+ };
11775
+ const onAuthorizationRequired = async (provider) => {
11651
11776
  if (pendingPause) {
11652
- throw pendingPause;
11777
+ return true;
11653
11778
  }
11654
- if (!deps.requesterId || !getPluginOAuthConfig(provider)) {
11655
- throw new Error(`Cannot start plugin authorization for ${provider}`);
11779
+ const authSessionId = authSessionIdsByProvider.get(provider);
11780
+ if (!authSessionId || !deps.requesterId) {
11781
+ throw new Error(
11782
+ `Missing MCP auth session context for plugin "${provider}"`
11783
+ );
11784
+ }
11785
+ const latestArtifactState = deps.getMergedArtifactState();
11786
+ await patchMcpAuthSession(authSessionId, {
11787
+ configuration: { ...deps.getConfiguration() },
11788
+ artifactState: latestArtifactState,
11789
+ toolChannelId: deps.toolChannelId ?? latestArtifactState.assistantContextChannelId ?? deps.channelId
11790
+ });
11791
+ const authSession = await getMcpAuthSession(authSessionId);
11792
+ if (!authSession?.authorizationUrl) {
11793
+ throw new Error(`Missing MCP authorization URL for plugin "${provider}"`);
11656
11794
  }
11657
- const providerLabel = formatProviderLabel(provider);
11658
11795
  const reusingPendingLink = canReusePendingAuthLink({
11659
11796
  pendingAuth: deps.currentPendingAuth,
11660
- kind: "plugin",
11797
+ kind: "mcp",
11661
11798
  provider,
11662
11799
  requesterId: deps.requesterId
11663
11800
  });
11664
11801
  if (!reusingPendingLink) {
11665
- const oauthResult = await startOAuthFlow(provider, {
11666
- requesterId: deps.requesterId,
11667
- channelId: deps.channelId,
11668
- threadTs: deps.threadTs,
11669
- userMessage: deps.userMessage,
11670
- channelConfiguration: deps.channelConfiguration,
11671
- activeSkillName: activeSkill?.name ?? void 0,
11672
- resumeConversationId: deps.conversationId,
11673
- resumeSessionId: deps.sessionId
11802
+ const delivery = await deliverPrivateMessage({
11803
+ channelId: authSession.channelId,
11804
+ threadTs: authSession.threadTs,
11805
+ userId: authSession.userId,
11806
+ text: `<${authSession.authorizationUrl}|Click here to link your ${formatProviderLabel(provider)} MCP access>. Once you've authorized, this thread will continue automatically.`
11674
11807
  });
11675
- if (!oauthResult.ok) {
11676
- throw new Error(oauthResult.error);
11677
- }
11678
- if (!oauthResult.delivery) {
11808
+ if (!delivery) {
11679
11809
  throw new Error(
11680
- `I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try again.`
11810
+ `Unable to deliver MCP authorization link for plugin "${provider}"`
11681
11811
  );
11682
11812
  }
11813
+ } else {
11814
+ await deleteMcpAuthSession(authSessionId);
11683
11815
  }
11684
- if (options?.unlinkExistingProvider && deps.requesterId && deps.userTokenStore) {
11685
- await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
11686
- }
11687
- if (deps.sessionId) {
11816
+ if (deps.sessionId && deps.requesterId) {
11688
11817
  await deps.onPendingAuth?.({
11689
- kind: "plugin",
11818
+ kind: "mcp",
11690
11819
  provider,
11691
11820
  requesterId: deps.requesterId,
11692
11821
  sessionId: deps.sessionId,
11693
11822
  linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
11694
11823
  });
11695
11824
  }
11696
- pendingPause = new PluginAuthorizationPauseError(
11825
+ pendingPause = new McpAuthorizationPauseError(
11697
11826
  provider,
11698
11827
  reusingPendingLink ? "link_already_sent" : "link_sent"
11699
11828
  );
11700
11829
  abortAgent();
11701
- throw pendingPause;
11830
+ return true;
11702
11831
  };
11703
11832
  return {
11704
- handleCommandFailure: async (input) => {
11705
- const providers = registeredProviderNames();
11706
- const authFailure = isCommandAuthFailure(input.details);
11707
- if (!authFailure) {
11708
- return;
11709
- }
11710
- const explicitProvider = explicitAuthRequiredProvider(input.details);
11711
- const provider = explicitProvider && providers.includes(explicitProvider) ? explicitProvider : providers.find(
11712
- (availableProvider) => commandTargetsProvider(
11713
- availableProvider,
11714
- input.command,
11715
- input.details
11716
- )
11717
- );
11718
- if (!provider || !deps.requesterId || !deps.userTokenStore || !getPluginOAuthConfig(provider)) {
11719
- return;
11720
- }
11721
- await startAuthorizationPause(provider, input.activeSkill, {
11722
- unlinkExistingProvider: true
11723
- });
11724
- },
11833
+ authProviderFactory,
11834
+ onAuthorizationRequired,
11725
11835
  getPendingPause: () => pendingPause
11726
11836
  };
11727
11837
  }
@@ -12708,6 +12818,12 @@ function finalizeFailedTurnReply(args) {
12708
12818
  };
12709
12819
  }
12710
12820
 
12821
+ // src/chat/services/turn-continuation-response.ts
12822
+ var TURN_CONTINUATION_RESPONSE = "I'm still working on this in the background. I'll post the final response here when it finishes.";
12823
+ function buildTurnContinuationResponse() {
12824
+ return TURN_CONTINUATION_RESPONSE;
12825
+ }
12826
+
12711
12827
  // src/chat/slack/assistant-thread/status-render.ts
12712
12828
  var DEFAULT_STATUS_CONTEXTS = {
12713
12829
  thinking: "\u2026",
@@ -13453,6 +13569,25 @@ async function postResumeFailureReply(args) {
13453
13569
  throw error;
13454
13570
  }
13455
13571
  }
13572
+ async function postTurnContinuationNoticeBestEffort(args) {
13573
+ try {
13574
+ await postSlackMessage({
13575
+ channelId: args.resumeArgs.channelId,
13576
+ threadTs: args.resumeArgs.threadTs,
13577
+ text: buildTurnContinuationResponse()
13578
+ });
13579
+ } catch (error) {
13580
+ logException(
13581
+ error,
13582
+ "slack_turn_continuation_notice_post_failed",
13583
+ getResumeLogContext(args.resumeArgs, args.lockKey),
13584
+ {
13585
+ "app.slack.reply_stage": "thread_reply_turn_continuation_notice"
13586
+ },
13587
+ "Failed to post turn continuation notice"
13588
+ );
13589
+ }
13590
+ }
13456
13591
  async function handleResumeFailure(args) {
13457
13592
  const logContext = getResumeLogContext(args.resumeArgs, args.lockKey);
13458
13593
  const capturedEventId = logException(
@@ -13520,6 +13655,7 @@ async function resumeSlackTurn(args) {
13520
13655
  channelId: args.channelId,
13521
13656
  threadTs: args.threadTs
13522
13657
  });
13658
+ let deferredPauseKind;
13523
13659
  let deferredPauseHandler;
13524
13660
  let deferredFailureHandler;
13525
13661
  try {
@@ -13573,10 +13709,12 @@ async function resumeSlackTurn(args) {
13573
13709
  const onAuthPause = args.onAuthPause;
13574
13710
  const onTimeoutPause = args.onTimeoutPause;
13575
13711
  if ((isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) && onAuthPause) {
13712
+ deferredPauseKind = "auth";
13576
13713
  deferredPauseHandler = async () => {
13577
13714
  await onAuthPause(error);
13578
13715
  };
13579
13716
  } else if (isRetryableTurnError(error, "turn_timeout_resume") && onTimeoutPause) {
13717
+ deferredPauseKind = "timeout";
13580
13718
  deferredPauseHandler = async () => {
13581
13719
  await onTimeoutPause(error);
13582
13720
  };
@@ -13597,6 +13735,12 @@ async function resumeSlackTurn(args) {
13597
13735
  if (deferredPauseHandler) {
13598
13736
  try {
13599
13737
  await deferredPauseHandler();
13738
+ if (deferredPauseKind === "timeout") {
13739
+ await postTurnContinuationNoticeBestEffort({
13740
+ lockKey,
13741
+ resumeArgs: args
13742
+ });
13743
+ }
13600
13744
  return;
13601
13745
  } catch (pauseError) {
13602
13746
  await handleResumeFailure({
@@ -13668,6 +13812,20 @@ var MAX_TURN_TIMEOUT_RESUME_SLICE_ID = 5;
13668
13812
  function canScheduleTurnTimeoutResume(nextSliceId) {
13669
13813
  return typeof nextSliceId === "number" && nextSliceId > 1 && nextSliceId <= MAX_TURN_TIMEOUT_RESUME_SLICE_ID;
13670
13814
  }
13815
+ async function getAwaitingTurnContinuationRequest(args) {
13816
+ const checkpoint = await getAgentTurnSessionCheckpoint(
13817
+ args.conversationId,
13818
+ args.sessionId
13819
+ );
13820
+ if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "timeout" || !canScheduleTurnTimeoutResume(checkpoint.sliceId)) {
13821
+ return void 0;
13822
+ }
13823
+ return {
13824
+ conversationId: args.conversationId,
13825
+ sessionId: args.sessionId,
13826
+ expectedCheckpointVersion: checkpoint.checkpointVersion
13827
+ };
13828
+ }
13671
13829
  function getTurnTimeoutResumeSecret() {
13672
13830
  const explicit = process.env.JUNIOR_INTERNAL_RESUME_SECRET?.trim();
13673
13831
  if (explicit) {
@@ -14764,6 +14922,10 @@ var PROXY_ONLY_HEADERS = /* @__PURE__ */ new Set([
14764
14922
  FORWARDED_SCHEME_HEADER,
14765
14923
  FORWARDED_PORT_HEADER
14766
14924
  ]);
14925
+ var DECODED_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
14926
+ "content-encoding",
14927
+ "content-length"
14928
+ ]);
14767
14929
  var AUTH_REJECTION_STATUS = /* @__PURE__ */ new Set([401, 403]);
14768
14930
  function jsonError(message, status) {
14769
14931
  return Response.json({ error: message }, { status });
@@ -14862,7 +15024,7 @@ function responseHeaders(upstream) {
14862
15024
  const headers = new Headers();
14863
15025
  upstream.headers.forEach((value, key) => {
14864
15026
  const normalized = key.toLowerCase();
14865
- if (!HOP_BY_HOP_HEADERS.has(normalized)) {
15027
+ if (!HOP_BY_HOP_HEADERS.has(normalized) && !DECODED_RESPONSE_HEADERS.has(normalized)) {
14866
15028
  headers.append(key, value);
14867
15029
  }
14868
15030
  });
@@ -15034,6 +15196,10 @@ function resolveSlackChannelIdFromMessage(message) {
15034
15196
  }
15035
15197
 
15036
15198
  // src/handlers/turn-resume.ts
15199
+ var TIMEOUT_RESUME_LOCK_RETRY_DELAYS_MS = [250, 1e3, 2e3];
15200
+ function sleep3(ms) {
15201
+ return new Promise((resolve) => setTimeout(resolve, ms));
15202
+ }
15037
15203
  async function persistCompletedReplyState2(args) {
15038
15204
  const currentState = await getPersistedThreadState(
15039
15205
  args.checkpoint.conversationId
@@ -15233,25 +15399,53 @@ async function resumeTimedOutTurn(payload) {
15233
15399
  }
15234
15400
  });
15235
15401
  }
15236
- async function POST(request, waitUntil) {
15237
- const payload = await verifyTurnTimeoutResumeRequest(request);
15238
- if (!payload) {
15239
- return new Response("Unauthorized", { status: 401 });
15240
- }
15241
- waitUntil(
15242
- () => resumeTimedOutTurn(payload).catch((error) => {
15243
- if (error instanceof ResumeTurnBusyError) {
15402
+ async function resumeTimedOutTurnWithLockRetry(payload) {
15403
+ for (const [attempt, delayMs] of [
15404
+ ...TIMEOUT_RESUME_LOCK_RETRY_DELAYS_MS,
15405
+ void 0
15406
+ ].entries()) {
15407
+ try {
15408
+ await resumeTimedOutTurn(payload);
15409
+ return;
15410
+ } catch (error) {
15411
+ if (!(error instanceof ResumeTurnBusyError)) {
15412
+ throw error;
15413
+ }
15414
+ if (typeof delayMs !== "number") {
15244
15415
  logWarn(
15245
15416
  "timeout_resume_lock_busy",
15246
15417
  {},
15247
15418
  {
15248
15419
  "app.ai.conversation_id": payload.conversationId,
15249
- "app.ai.session_id": payload.sessionId
15420
+ "app.ai.session_id": payload.sessionId,
15421
+ "app.ai.resume_lock_retry_count": attempt
15250
15422
  },
15251
- "Skipped timeout resume because another turn owns the thread lock"
15423
+ "Skipped timeout resume because another turn still owns the thread lock"
15252
15424
  );
15253
15425
  return;
15254
15426
  }
15427
+ logWarn(
15428
+ "timeout_resume_lock_busy_retrying",
15429
+ {},
15430
+ {
15431
+ "app.ai.conversation_id": payload.conversationId,
15432
+ "app.ai.session_id": payload.sessionId,
15433
+ "app.ai.resume_lock_retry_attempt": attempt + 1,
15434
+ "app.ai.resume_lock_retry_delay_ms": delayMs
15435
+ },
15436
+ "Timeout resume lock was busy; retrying"
15437
+ );
15438
+ await sleep3(delayMs);
15439
+ }
15440
+ }
15441
+ }
15442
+ async function POST(request, waitUntil) {
15443
+ const payload = await verifyTurnTimeoutResumeRequest(request);
15444
+ if (!payload) {
15445
+ return new Response("Unauthorized", { status: 401 });
15446
+ }
15447
+ waitUntil(
15448
+ () => resumeTimedOutTurnWithLockRetry(payload).catch((error) => {
15255
15449
  logException(
15256
15450
  error,
15257
15451
  "timeout_resume_handler_failed",
@@ -15615,21 +15809,162 @@ function getSlackErrorObservabilityAttributes(error) {
15615
15809
  if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
15616
15810
  attributes["app.slack.error_code"] = candidate.code;
15617
15811
  }
15618
- if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
15619
- attributes["app.slack.api_error"] = candidate.data.error;
15812
+ if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
15813
+ attributes["app.slack.api_error"] = candidate.data.error;
15814
+ }
15815
+ const requestId = getHeaderString(candidate.headers, "x-slack-req-id");
15816
+ if (requestId) {
15817
+ attributes["app.slack.request_id"] = requestId;
15818
+ }
15819
+ if (typeof candidate.statusCode === "number" && Number.isFinite(candidate.statusCode)) {
15820
+ attributes["http.response.status_code"] = candidate.statusCode;
15821
+ }
15822
+ return attributes;
15823
+ }
15824
+ function isSlackTitlePermissionError(error) {
15825
+ const code = getSlackApiErrorCode(error);
15826
+ return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
15827
+ }
15828
+
15829
+ // src/chat/runtime/thread-context.ts
15830
+ function escapeRegExp3(value) {
15831
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15832
+ }
15833
+ function stripLeadingBotMention(text, options = {}) {
15834
+ if (!text.trim()) return text;
15835
+ let next = text;
15836
+ if (options.stripLeadingSlackMentionToken) {
15837
+ next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
15838
+ }
15839
+ const mentionByNameRe = new RegExp(
15840
+ `^\\s*@${escapeRegExp3(botConfig.userName)}\\b[\\s,:-]*`,
15841
+ "i"
15842
+ );
15843
+ next = next.replace(mentionByNameRe, "").trim();
15844
+ const mentionByLabeledEntityRe = new RegExp(
15845
+ `^\\s*<@[^>|]+\\|${escapeRegExp3(botConfig.userName)}>[\\s,:-]*`,
15846
+ "i"
15847
+ );
15848
+ next = next.replace(mentionByLabeledEntityRe, "").trim();
15849
+ return next;
15850
+ }
15851
+ function getThreadId(thread, _message) {
15852
+ return toOptionalString(thread.id);
15853
+ }
15854
+ function getRunId(thread, message) {
15855
+ return toOptionalString(thread.runId) ?? toOptionalString(message.runId);
15856
+ }
15857
+ function getChannelId(thread, message) {
15858
+ return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
15859
+ }
15860
+ function getThreadTs(threadId) {
15861
+ return parseSlackThreadId(threadId)?.threadTs;
15862
+ }
15863
+ function getAssistantThreadContext(message) {
15864
+ const raw = message.raw;
15865
+ const rawRecord = raw && typeof raw === "object" ? raw : void 0;
15866
+ const channelId = toOptionalString(rawRecord?.channel);
15867
+ if (channelId) {
15868
+ const rawThreadTs = toOptionalString(rawRecord?.thread_ts);
15869
+ const threadTs = isDmChannel(channelId) ? rawThreadTs : rawThreadTs ?? toOptionalString(rawRecord?.ts);
15870
+ if (threadTs) {
15871
+ return { channelId, threadTs };
15872
+ }
15873
+ }
15874
+ const parsedThreadId = parseSlackThreadId(
15875
+ toOptionalString(message.threadId)
15876
+ );
15877
+ if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) {
15878
+ return void 0;
15879
+ }
15880
+ return parsedThreadId;
15881
+ }
15882
+ function getMessageTs(message) {
15883
+ const directTs = toOptionalString(
15884
+ message.ts
15885
+ );
15886
+ if (directTs) {
15887
+ return directTs;
15888
+ }
15889
+ const raw = message.raw;
15890
+ if (!raw || typeof raw !== "object") {
15891
+ return void 0;
15892
+ }
15893
+ const rawRecord = raw;
15894
+ return toOptionalString(rawRecord.ts) ?? toOptionalString(rawRecord.event_ts) ?? toOptionalString(rawRecord.message?.ts);
15895
+ }
15896
+
15897
+ // src/chat/runtime/processing-reaction.ts
15898
+ var PROCESSING_REACTION_EMOJI = "eyes";
15899
+ var noProcessingReaction = {
15900
+ keep: () => void 0,
15901
+ stop: async () => void 0
15902
+ };
15903
+ function isProcessingReactionEmoji(value) {
15904
+ return typeof value === "string" && normalizeSlackEmojiName(value) === PROCESSING_REACTION_EMOJI;
15905
+ }
15906
+ function shouldKeepProcessingReactionForToolInvocation(input) {
15907
+ return input.toolName === "slackMessageAddReaction" && isProcessingReactionEmoji(input.params.emoji);
15908
+ }
15909
+ async function startSlackProcessingReaction(args) {
15910
+ if (args.message.author.isMe) {
15911
+ return noProcessingReaction;
15620
15912
  }
15621
- const requestId = getHeaderString(candidate.headers, "x-slack-req-id");
15622
- if (requestId) {
15623
- attributes["app.slack.request_id"] = requestId;
15913
+ const channelId = getChannelId(args.thread, args.message);
15914
+ const messageTs = getMessageTs(args.message);
15915
+ if (!channelId || !messageTs) {
15916
+ return noProcessingReaction;
15624
15917
  }
15625
- if (typeof candidate.statusCode === "number" && Number.isFinite(candidate.statusCode)) {
15626
- attributes["http.response.status_code"] = candidate.statusCode;
15918
+ try {
15919
+ await addReactionToMessage({
15920
+ channelId,
15921
+ timestamp: messageTs,
15922
+ emoji: PROCESSING_REACTION_EMOJI
15923
+ });
15924
+ } catch (error) {
15925
+ args.logException(
15926
+ error,
15927
+ "slack_processing_reaction_add_failed",
15928
+ args.logContext,
15929
+ {
15930
+ "app.slack.action": "reactions.add",
15931
+ "messaging.message.id": messageTs,
15932
+ ...getSlackErrorObservabilityAttributes(error)
15933
+ },
15934
+ "Failed to add Slack processing reaction"
15935
+ );
15936
+ return noProcessingReaction;
15627
15937
  }
15628
- return attributes;
15629
- }
15630
- function isSlackTitlePermissionError(error) {
15631
- const code = getSlackApiErrorCode(error);
15632
- return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
15938
+ let shouldRemove = true;
15939
+ return {
15940
+ keep: () => {
15941
+ shouldRemove = false;
15942
+ },
15943
+ stop: async () => {
15944
+ if (!shouldRemove) {
15945
+ return;
15946
+ }
15947
+ try {
15948
+ await removeReactionFromMessage({
15949
+ channelId,
15950
+ timestamp: messageTs,
15951
+ emoji: PROCESSING_REACTION_EMOJI
15952
+ });
15953
+ } catch (error) {
15954
+ args.logException(
15955
+ error,
15956
+ "slack_processing_reaction_remove_failed",
15957
+ args.logContext,
15958
+ {
15959
+ "app.slack.action": "reactions.remove",
15960
+ "messaging.message.id": messageTs,
15961
+ ...getSlackErrorObservabilityAttributes(error)
15962
+ },
15963
+ "Failed to remove Slack processing reaction"
15964
+ );
15965
+ }
15966
+ }
15967
+ };
15633
15968
  }
15634
15969
 
15635
15970
  // src/chat/runtime/slack-runtime.ts
@@ -15657,6 +15992,14 @@ function buildLogContext(deps, args) {
15657
15992
  }
15658
15993
  function createSlackTurnRuntime(deps) {
15659
15994
  const logContext = (args) => buildLogContext(deps, args);
15995
+ const createToolInvocationHook = (processingReaction, hooks) => {
15996
+ return (invocation) => {
15997
+ if (shouldKeepProcessingReactionForToolInvocation(invocation)) {
15998
+ processingReaction.keep();
15999
+ }
16000
+ hooks?.onToolInvocation?.(invocation);
16001
+ };
16002
+ };
15660
16003
  const postFallbackErrorReplyWithLogging = async (args) => {
15661
16004
  try {
15662
16005
  await args.thread.post(buildTurnFailureResponse(args.eventId));
@@ -15710,6 +16053,7 @@ function createSlackTurnRuntime(deps) {
15710
16053
  };
15711
16054
  return {
15712
16055
  async handleNewMention(thread, message, hooks) {
16056
+ let processingReaction;
15713
16057
  try {
15714
16058
  const threadId = deps.getThreadId(thread, message);
15715
16059
  const channelId = deps.getChannelId(thread, message);
@@ -15721,11 +16065,22 @@ function createSlackTurnRuntime(deps) {
15721
16065
  requesterUserName: message.author.userName,
15722
16066
  runId
15723
16067
  });
16068
+ processingReaction = await startSlackProcessingReaction({
16069
+ thread,
16070
+ message,
16071
+ logException: deps.logException,
16072
+ logContext: context
16073
+ });
16074
+ const toolInvocationHook = createToolInvocationHook(
16075
+ processingReaction,
16076
+ hooks
16077
+ );
15724
16078
  await deps.withSpan("chat.turn", "chat.turn", context, async () => {
15725
16079
  await thread.subscribe();
15726
16080
  await deps.replyToThread(thread, message, {
15727
16081
  explicitMention: true,
15728
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost
16082
+ beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
16083
+ onToolInvocation: toolInvocationHook
15729
16084
  });
15730
16085
  });
15731
16086
  } catch (error) {
@@ -15766,113 +16121,123 @@ function createSlackTurnRuntime(deps) {
15766
16121
  postFailureEventName: "mention_handler_failure_reply_post_failed",
15767
16122
  postFailureBody: "Failed to post fallback error reply for mention handler"
15768
16123
  });
16124
+ } finally {
16125
+ await processingReaction?.stop();
15769
16126
  }
15770
16127
  },
15771
16128
  async handleSubscribedMessage(thread, message, hooks) {
16129
+ let processingReaction;
15772
16130
  try {
15773
16131
  const threadId = deps.getThreadId(thread, message);
15774
16132
  const channelId = deps.getChannelId(thread, message);
15775
16133
  const runId = deps.getRunId(thread, message);
15776
- await deps.withSpan(
15777
- "chat.turn",
15778
- "chat.turn",
15779
- logContext({
16134
+ const context = logContext({
16135
+ threadId,
16136
+ requesterId: message.author.userId,
16137
+ requesterUserName: message.author.userName,
16138
+ channelId,
16139
+ runId
16140
+ });
16141
+ processingReaction = await startSlackProcessingReaction({
16142
+ thread,
16143
+ message,
16144
+ logException: deps.logException,
16145
+ logContext: context
16146
+ });
16147
+ const toolInvocationHook = createToolInvocationHook(
16148
+ processingReaction,
16149
+ hooks
16150
+ );
16151
+ await deps.withSpan("chat.turn", "chat.turn", context, async () => {
16152
+ const legacyAttachmentText = renderSlackLegacyAttachmentText(
16153
+ message.raw
16154
+ );
16155
+ const rawUserText = appendSlackLegacyAttachmentText(
16156
+ message.text,
16157
+ message.raw
16158
+ );
16159
+ const strippedUserText = deps.stripLeadingBotMention(message.text, {
16160
+ stripLeadingSlackMentionToken: Boolean(message.isMention)
16161
+ });
16162
+ const userText = appendSlackLegacyAttachmentText(
16163
+ strippedUserText,
16164
+ message.raw
16165
+ );
16166
+ const context2 = {
15780
16167
  threadId,
15781
16168
  requesterId: message.author.userId,
15782
- requesterUserName: message.author.userName,
15783
16169
  channelId,
15784
16170
  runId
15785
- }),
15786
- async () => {
15787
- const legacyAttachmentText = renderSlackLegacyAttachmentText(
15788
- message.raw
15789
- );
15790
- const rawUserText = appendSlackLegacyAttachmentText(
15791
- message.text,
15792
- message.raw
15793
- );
15794
- const strippedUserText = deps.stripLeadingBotMention(message.text, {
15795
- stripLeadingSlackMentionToken: Boolean(message.isMention)
15796
- });
15797
- const userText = appendSlackLegacyAttachmentText(
15798
- strippedUserText,
15799
- message.raw
15800
- );
15801
- const context = {
15802
- threadId,
15803
- requesterId: message.author.userId,
15804
- channelId,
15805
- runId
15806
- };
15807
- const preflightDecision = getSubscribedReplyPreflightDecision({
15808
- botUserName: deps.assistantUserName,
15809
- rawText: rawUserText,
15810
- text: userText,
15811
- isExplicitMention: Boolean(message.isMention)
15812
- });
15813
- if (preflightDecision && !preflightDecision.shouldReply) {
15814
- const reason = preflightDecision.reasonDetail ? `${preflightDecision.reason}:${preflightDecision.reasonDetail}` : preflightDecision.reason;
15815
- await skipSubscribedMessage({
15816
- thread,
15817
- message,
15818
- decision: { shouldReply: false, reason },
15819
- context,
15820
- userText
15821
- });
15822
- return;
15823
- }
15824
- const preparedState = await deps.prepareTurnState({
16171
+ };
16172
+ const preflightDecision = getSubscribedReplyPreflightDecision({
16173
+ botUserName: deps.assistantUserName,
16174
+ rawText: rawUserText,
16175
+ text: userText,
16176
+ isExplicitMention: Boolean(message.isMention)
16177
+ });
16178
+ if (preflightDecision && !preflightDecision.shouldReply) {
16179
+ const reason = preflightDecision.reasonDetail ? `${preflightDecision.reason}:${preflightDecision.reasonDetail}` : preflightDecision.reason;
16180
+ await skipSubscribedMessage({
15825
16181
  thread,
15826
16182
  message,
15827
- userText,
15828
- explicitMention: Boolean(message.isMention),
15829
- context
16183
+ decision: { shouldReply: false, reason },
16184
+ context: context2,
16185
+ userText
15830
16186
  });
15831
- await deps.persistPreparedState({
16187
+ return;
16188
+ }
16189
+ const preparedState = await deps.prepareTurnState({
16190
+ thread,
16191
+ message,
16192
+ userText,
16193
+ explicitMention: Boolean(message.isMention),
16194
+ context: context2
16195
+ });
16196
+ await deps.persistPreparedState({
16197
+ thread,
16198
+ preparedState
16199
+ });
16200
+ const decision = await deps.decideSubscribedReply({
16201
+ rawText: rawUserText,
16202
+ text: userText,
16203
+ conversationContext: deps.getPreparedConversationContext(preparedState),
16204
+ hasAttachments: message.attachments.length > 0 || legacyAttachmentText !== "",
16205
+ isExplicitMention: Boolean(message.isMention),
16206
+ context: context2
16207
+ });
16208
+ if (await maybeHandleThreadOptOutDecision({
16209
+ thread,
16210
+ decision,
16211
+ beforeFirstResponsePost: hooks?.beforeFirstResponsePost
16212
+ })) {
16213
+ await skipSubscribedMessage({
15832
16214
  thread,
15833
- preparedState
15834
- });
15835
- const decision = await deps.decideSubscribedReply({
15836
- rawText: rawUserText,
15837
- text: userText,
15838
- conversationContext: deps.getPreparedConversationContext(preparedState),
15839
- hasAttachments: message.attachments.length > 0 || legacyAttachmentText !== "",
15840
- isExplicitMention: Boolean(message.isMention),
15841
- context
16215
+ message,
16216
+ decision,
16217
+ context: context2,
16218
+ preparedState,
16219
+ userText
15842
16220
  });
15843
- if (await maybeHandleThreadOptOutDecision({
16221
+ return;
16222
+ }
16223
+ if (!decision.shouldReply) {
16224
+ await skipSubscribedMessage({
15844
16225
  thread,
16226
+ message,
15845
16227
  decision,
15846
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost
15847
- })) {
15848
- await skipSubscribedMessage({
15849
- thread,
15850
- message,
15851
- decision,
15852
- context,
15853
- preparedState,
15854
- userText
15855
- });
15856
- return;
15857
- }
15858
- if (!decision.shouldReply) {
15859
- await skipSubscribedMessage({
15860
- thread,
15861
- message,
15862
- decision,
15863
- context,
15864
- preparedState,
15865
- userText
15866
- });
15867
- return;
15868
- }
15869
- await deps.replyToThread(thread, message, {
15870
- explicitMention: Boolean(message.isMention),
16228
+ context: context2,
15871
16229
  preparedState,
15872
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost
16230
+ userText
15873
16231
  });
16232
+ return;
15874
16233
  }
15875
- );
16234
+ await deps.replyToThread(thread, message, {
16235
+ explicitMention: Boolean(message.isMention),
16236
+ preparedState,
16237
+ beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
16238
+ onToolInvocation: toolInvocationHook
16239
+ });
16240
+ });
15876
16241
  } catch (error) {
15877
16242
  const errorContext = logContext({
15878
16243
  threadId: deps.getThreadId(thread, message),
@@ -15911,6 +16276,8 @@ function createSlackTurnRuntime(deps) {
15911
16276
  postFailureEventName: "subscribed_message_handler_failure_reply_post_failed",
15912
16277
  postFailureBody: "Failed to post fallback error reply for subscribed message handler"
15913
16278
  });
16279
+ } finally {
16280
+ await processingReaction?.stop();
15914
16281
  }
15915
16282
  },
15916
16283
  async handleAssistantThreadStarted(event) {
@@ -16608,6 +16975,7 @@ function createJuniorRuntimeServices(overrides = {}) {
16608
16975
  conversationMemory,
16609
16976
  replyExecutor: {
16610
16977
  generateAssistantReply: overrides.replyExecutor?.generateAssistantReply ?? generateAssistantReply,
16978
+ getAwaitingTurnContinuationRequest: overrides.replyExecutor?.getAwaitingTurnContinuationRequest ?? getAwaitingTurnContinuationRequest,
16611
16979
  lookupSlackUser: overrides.replyExecutor?.lookupSlackUser ?? lookupSlackUser,
16612
16980
  scheduleTurnTimeoutResume: overrides.replyExecutor?.scheduleTurnTimeoutResume ?? scheduleTurnTimeoutResume,
16613
16981
  generateThreadTitle: conversationMemory.generateThreadTitle
@@ -16630,74 +16998,6 @@ function getSlackMessageTs(message) {
16630
16998
  return message.id;
16631
16999
  }
16632
17000
 
16633
- // src/chat/runtime/thread-context.ts
16634
- function escapeRegExp3(value) {
16635
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16636
- }
16637
- function stripLeadingBotMention(text, options = {}) {
16638
- if (!text.trim()) return text;
16639
- let next = text;
16640
- if (options.stripLeadingSlackMentionToken) {
16641
- next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
16642
- }
16643
- const mentionByNameRe = new RegExp(
16644
- `^\\s*@${escapeRegExp3(botConfig.userName)}\\b[\\s,:-]*`,
16645
- "i"
16646
- );
16647
- next = next.replace(mentionByNameRe, "").trim();
16648
- const mentionByLabeledEntityRe = new RegExp(
16649
- `^\\s*<@[^>|]+\\|${escapeRegExp3(botConfig.userName)}>[\\s,:-]*`,
16650
- "i"
16651
- );
16652
- next = next.replace(mentionByLabeledEntityRe, "").trim();
16653
- return next;
16654
- }
16655
- function getThreadId(thread, _message) {
16656
- return toOptionalString(thread.id);
16657
- }
16658
- function getRunId(thread, message) {
16659
- return toOptionalString(thread.runId) ?? toOptionalString(message.runId);
16660
- }
16661
- function getChannelId(thread, message) {
16662
- return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
16663
- }
16664
- function getThreadTs(threadId) {
16665
- return parseSlackThreadId(threadId)?.threadTs;
16666
- }
16667
- function getAssistantThreadContext(message) {
16668
- const raw = message.raw;
16669
- const rawRecord = raw && typeof raw === "object" ? raw : void 0;
16670
- const channelId = toOptionalString(rawRecord?.channel);
16671
- if (channelId) {
16672
- const rawThreadTs = toOptionalString(rawRecord?.thread_ts);
16673
- const threadTs = isDmChannel(channelId) ? rawThreadTs : rawThreadTs ?? toOptionalString(rawRecord?.ts);
16674
- if (threadTs) {
16675
- return { channelId, threadTs };
16676
- }
16677
- }
16678
- const parsedThreadId = parseSlackThreadId(
16679
- toOptionalString(message.threadId)
16680
- );
16681
- if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) {
16682
- return void 0;
16683
- }
16684
- return parsedThreadId;
16685
- }
16686
- function getMessageTs(message) {
16687
- const directTs = toOptionalString(
16688
- message.ts
16689
- );
16690
- if (directTs) {
16691
- return directTs;
16692
- }
16693
- const raw = message.raw;
16694
- if (!raw || typeof raw !== "object") {
16695
- return void 0;
16696
- }
16697
- const rawRecord = raw;
16698
- return toOptionalString(rawRecord.ts) ?? toOptionalString(rawRecord.event_ts) ?? toOptionalString(rawRecord.message?.ts);
16699
- }
16700
-
16701
17001
  // src/chat/slack/assistant-thread/title.ts
16702
17002
  function maybeUpdateAssistantTitle(args) {
16703
17003
  const assistantThreadContext = args.assistantThreadContext;
@@ -16814,11 +17114,6 @@ function createReplyToThread(deps) {
16814
17114
  });
16815
17115
  const slackMessageTs = getSlackMessageTs(message);
16816
17116
  const turnId = buildDeterministicTurnId(message.id);
16817
- startActiveTurn({
16818
- conversation: preparedState.conversation,
16819
- nextTurnId: turnId,
16820
- updateConversationStats
16821
- });
16822
17117
  const turnTraceContext = {
16823
17118
  conversationId,
16824
17119
  slackThreadId: threadId,
@@ -16828,6 +17123,78 @@ function createReplyToThread(deps) {
16828
17123
  assistantUserName: botConfig.userName,
16829
17124
  modelId: botConfig.modelId
16830
17125
  };
17126
+ let beforeFirstResponsePostCalled = false;
17127
+ const beforeFirstResponsePost = async () => {
17128
+ if (beforeFirstResponsePostCalled) {
17129
+ return;
17130
+ }
17131
+ beforeFirstResponsePostCalled = true;
17132
+ await options.beforeFirstResponsePost?.();
17133
+ };
17134
+ const postTurnContinuationNotice = async () => {
17135
+ try {
17136
+ await beforeFirstResponsePost();
17137
+ await thread.post(
17138
+ buildSlackOutputMessage(buildTurnContinuationResponse())
17139
+ );
17140
+ } catch (error) {
17141
+ logException(
17142
+ error,
17143
+ "slack_turn_continuation_notice_post_failed",
17144
+ turnTraceContext,
17145
+ {
17146
+ "app.slack.reply_stage": "thread_reply_turn_continuation_notice",
17147
+ ...messageTs ? { "messaging.message.id": messageTs } : {},
17148
+ ...getSlackErrorObservabilityAttributes(error)
17149
+ },
17150
+ "Failed to post turn continuation notice"
17151
+ );
17152
+ throw error;
17153
+ }
17154
+ };
17155
+ const activeTurnId = preparedState.conversation.processing.activeTurnId;
17156
+ if (conversationId && activeTurnId) {
17157
+ const resumeRequest = await deps.services.getAwaitingTurnContinuationRequest({
17158
+ conversationId,
17159
+ sessionId: activeTurnId
17160
+ });
17161
+ if (resumeRequest) {
17162
+ try {
17163
+ await deps.services.scheduleTurnTimeoutResume(resumeRequest);
17164
+ } catch (error) {
17165
+ logException(
17166
+ error,
17167
+ "agent_turn_continuation_retry_schedule_failed",
17168
+ turnTraceContext,
17169
+ {
17170
+ "app.ai.resume_checkpoint_version": resumeRequest.expectedCheckpointVersion,
17171
+ "app.ai.resume_session_id": resumeRequest.sessionId,
17172
+ ...messageTs ? { "messaging.message.id": messageTs } : {}
17173
+ },
17174
+ "Failed to reschedule active turn continuation"
17175
+ );
17176
+ throw error;
17177
+ }
17178
+ await postTurnContinuationNotice();
17179
+ markConversationMessage(
17180
+ preparedState.conversation,
17181
+ preparedState.userMessageId,
17182
+ {
17183
+ replied: true,
17184
+ skippedReason: void 0
17185
+ }
17186
+ );
17187
+ await persistThreadState(thread, {
17188
+ conversation: preparedState.conversation
17189
+ });
17190
+ return;
17191
+ }
17192
+ }
17193
+ startActiveTurn({
17194
+ conversation: preparedState.conversation,
17195
+ nextTurnId: turnId,
17196
+ updateConversationStats
17197
+ });
16831
17198
  setTags({
16832
17199
  conversationId
16833
17200
  });
@@ -16869,14 +17236,6 @@ function createReplyToThread(deps) {
16869
17236
  threadTs: assistantThreadContext?.threadTs,
16870
17237
  getSlackAdapter: deps.getSlackAdapter
16871
17238
  });
16872
- let beforeFirstResponsePostCalled = false;
16873
- const beforeFirstResponsePost = async () => {
16874
- if (beforeFirstResponsePostCalled) {
16875
- return;
16876
- }
16877
- beforeFirstResponsePostCalled = true;
16878
- await options.beforeFirstResponsePost?.();
16879
- };
16880
17239
  const postThreadReply = async (payload, stage) => {
16881
17240
  await beforeFirstResponsePost();
16882
17241
  try {
@@ -16963,7 +17322,8 @@ function createReplyToThread(deps) {
16963
17322
  conversation: preparedState.conversation
16964
17323
  });
16965
17324
  },
16966
- onStatus: (nextStatus) => status.update(nextStatus)
17325
+ onStatus: (nextStatus) => status.update(nextStatus),
17326
+ onToolInvocation: options.onToolInvocation
16967
17327
  });
16968
17328
  const diagnosticsContext = {
16969
17329
  slackThreadId: threadId,
@@ -17130,7 +17490,6 @@ function createReplyToThread(deps) {
17130
17490
  expectedCheckpointVersion: checkpointVersion
17131
17491
  });
17132
17492
  shouldPersistFailureState = false;
17133
- return;
17134
17493
  } catch (scheduleError) {
17135
17494
  logException(
17136
17495
  scheduleError,
@@ -17142,7 +17501,11 @@ function createReplyToThread(deps) {
17142
17501
  },
17143
17502
  "Failed to schedule timeout resume callback"
17144
17503
  );
17504
+ shouldPersistFailureState = true;
17505
+ throw scheduleError;
17145
17506
  }
17507
+ await postTurnContinuationNotice();
17508
+ return;
17146
17509
  } else if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
17147
17510
  logWarn(
17148
17511
  "agent_turn_timeout_resume_slice_limit_reached",
@@ -17599,75 +17962,53 @@ function enqueueBackgroundTask(options, task) {
17599
17962
  throw new Error("Chat background processing requires waitUntil");
17600
17963
  }
17601
17964
  options.waitUntil(task);
17965
+ return task;
17602
17966
  }
17603
17967
  var JuniorChat = class extends Chat {
17604
17968
  /**
17605
17969
  * Normalize Slack thread IDs before the SDK's concurrency queue.
17606
17970
  *
17607
- * The SDK uses the `threadId` parameter as the lock/queue key
17608
- * (Chat.handleIncomingMessage getLockKey). @chat-adapter/slack
17609
- * (as of 4.22.0) builds DM thread IDs as `slack:<channel>:` (empty
17610
- * thread_ts) when the Slack event has no `thread_ts` field — it uses
17611
- * `event.thread_ts || ""` instead of falling back to `event.ts`.
17612
- * See @chat-adapter/slack/dist/index.js:1466.
17613
- *
17614
- * A DM root event arrives as `slack:D123:` while a reply in the same
17615
- * thread carries `slack:D123:<ts>`, splitting the lock/state/subscription
17616
- * keys and breaking conversation continuity.
17617
- *
17618
- * We fix this by resolving the message eagerly (even when the adapter
17619
- * provides a factory), deriving the canonical thread ID from
17620
- * `raw.channel` + `raw.thread_ts ?? raw.ts`, and passing both the
17621
- * normalized threadId and concrete message to super.processMessage.
17622
- *
17623
- * Remove this override when @chat-adapter/slack uses `event.ts` as
17624
- * the DM thread_ts fallback.
17971
+ * Slack DM roots can arrive with an empty thread timestamp, while
17972
+ * later replies include the root timestamp. Resolve factories before
17973
+ * delegating so the lock/state/subscription key is canonicalized before
17974
+ * the SDK computes its per-thread queue key.
17625
17975
  */
17626
17976
  processMessage(adapter, threadId, messageOrFactory, options) {
17627
17977
  if (typeof messageOrFactory === "function") {
17628
17978
  const runtime = this;
17629
- enqueueBackgroundTask(
17979
+ return enqueueBackgroundTask(
17630
17980
  options,
17631
17981
  (async () => {
17982
+ let message2;
17632
17983
  try {
17633
- const message = await messageOrFactory();
17634
- if (isExternalSlackUser(message.raw)) {
17635
- return;
17636
- }
17637
- const normalized = normalizeIncomingSlackThreadId(
17638
- threadId,
17639
- message
17640
- );
17641
- if (normalized !== threadId && "threadId" in message) {
17642
- message.threadId = normalized;
17643
- }
17644
- super.processMessage(adapter, normalized, message, options);
17984
+ message2 = await messageOrFactory();
17645
17985
  } catch (error) {
17646
17986
  runtime.logger?.error?.("Message factory resolution error", {
17647
17987
  error,
17648
17988
  threadId
17649
17989
  });
17990
+ return;
17991
+ }
17992
+ if (isExternalSlackUser(message2.raw)) {
17993
+ return;
17650
17994
  }
17995
+ const normalized2 = normalizeIncomingSlackThreadId(threadId, message2);
17996
+ if (normalized2 !== threadId && "threadId" in message2) {
17997
+ message2.threadId = normalized2;
17998
+ }
17999
+ await super.processMessage(adapter, normalized2, message2, options);
17651
18000
  })()
17652
18001
  );
17653
- return;
17654
18002
  }
17655
- if (isExternalSlackUser(messageOrFactory.raw)) {
17656
- return;
18003
+ const message = messageOrFactory;
18004
+ if (isExternalSlackUser(message.raw)) {
18005
+ return Promise.resolve();
17657
18006
  }
17658
- enqueueBackgroundTask(
17659
- options,
17660
- (async () => {
17661
- const normalized = normalizeIncomingSlackThreadId(
17662
- threadId,
17663
- messageOrFactory
17664
- );
17665
- if (normalized !== threadId && "threadId" in messageOrFactory) {
17666
- messageOrFactory.threadId = normalized;
17667
- }
17668
- super.processMessage(adapter, normalized, messageOrFactory, options);
17669
- })()
17670
- );
18007
+ const normalized = normalizeIncomingSlackThreadId(threadId, message);
18008
+ if (normalized !== threadId && "threadId" in message) {
18009
+ message.threadId = normalized;
18010
+ }
18011
+ return super.processMessage(adapter, normalized, message, options);
17671
18012
  }
17672
18013
  processReaction(event, options) {
17673
18014
  const runtime = this;