@sentry/junior 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/app.js +684 -234
  2. package/package.json +1 -1
package/dist/app.js CHANGED
@@ -392,6 +392,26 @@ function defaultConversationState() {
392
392
  }
393
393
  };
394
394
  }
395
+ function coercePendingAuthState(value) {
396
+ if (!isRecord(value)) {
397
+ return void 0;
398
+ }
399
+ const kind = value.kind;
400
+ const provider = toOptionalString(value.provider);
401
+ const requesterId = toOptionalString(value.requesterId);
402
+ const sessionId = toOptionalString(value.sessionId);
403
+ const linkSentAtMs = toOptionalNumber(value.linkSentAtMs);
404
+ if (kind !== "mcp" && kind !== "plugin" || !provider || !requesterId || !sessionId || typeof linkSentAtMs !== "number") {
405
+ return void 0;
406
+ }
407
+ return {
408
+ kind,
409
+ provider,
410
+ requesterId,
411
+ sessionId,
412
+ linkSentAtMs
413
+ };
414
+ }
395
415
  function coerceThreadConversationState(value) {
396
416
  if (!isRecord(value)) {
397
417
  return defaultConversationState();
@@ -442,7 +462,8 @@ function coerceThreadConversationState(value) {
442
462
  const rawProcessing = isRecord(rawConversation.processing) ? rawConversation.processing : {};
443
463
  const processing = {
444
464
  activeTurnId: toOptionalString(rawProcessing.activeTurnId),
445
- lastCompletedAtMs: toOptionalNumber(rawProcessing.lastCompletedAtMs)
465
+ lastCompletedAtMs: toOptionalNumber(rawProcessing.lastCompletedAtMs),
466
+ pendingAuth: coercePendingAuthState(rawProcessing.pendingAuth)
446
467
  };
447
468
  const rawStats = isRecord(rawConversation.stats) ? rawConversation.stats : {};
448
469
  const stats = {
@@ -2001,11 +2022,13 @@ function buildThreadParticipants(messages) {
2001
2022
  return participants;
2002
2023
  }
2003
2024
 
2004
- // src/chat/runtime/turn.ts
2025
+ // src/chat/state/turn-id.ts
2005
2026
  function buildDeterministicTurnId(messageId) {
2006
2027
  const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
2007
2028
  return `turn_${sanitized}`;
2008
2029
  }
2030
+
2031
+ // src/chat/runtime/turn.ts
2009
2032
  var RetryableTurnError = class extends Error {
2010
2033
  code = "retryable_turn";
2011
2034
  metadata;
@@ -2031,12 +2054,16 @@ function startActiveTurn(args) {
2031
2054
  args.updateConversationStats(args.conversation);
2032
2055
  }
2033
2056
  function markTurnCompleted(args) {
2034
- args.conversation.processing.activeTurnId = void 0;
2057
+ if (!args.sessionId || args.conversation.processing.activeTurnId === args.sessionId) {
2058
+ args.conversation.processing.activeTurnId = void 0;
2059
+ }
2035
2060
  args.conversation.processing.lastCompletedAtMs = args.nowMs;
2036
2061
  args.updateConversationStats(args.conversation);
2037
2062
  }
2038
2063
  function markTurnFailed(args) {
2039
- args.conversation.processing.activeTurnId = void 0;
2064
+ if (!args.sessionId || args.conversation.processing.activeTurnId === args.sessionId) {
2065
+ args.conversation.processing.activeTurnId = void 0;
2066
+ }
2040
2067
  args.conversation.processing.lastCompletedAtMs = args.nowMs;
2041
2068
  args.markConversationMessage(args.conversation, args.userMessageId, {
2042
2069
  replied: false,
@@ -8221,134 +8248,21 @@ function shouldEmitDevAgentTrace() {
8221
8248
  return process.env.NODE_ENV === "development";
8222
8249
  }
8223
8250
 
8224
- // src/chat/credentials/unlink-provider.ts
8225
- async function unlinkProvider(userId, provider, userTokenStore) {
8226
- await Promise.all([
8227
- userTokenStore.delete(userId, provider),
8228
- deleteMcpStoredOAuthCredentials(userId, provider),
8229
- deleteMcpServerSessionId(userId, provider),
8230
- deleteMcpAuthSessionsForUserProvider(userId, provider)
8231
- ]);
8232
- }
8233
-
8234
- // src/chat/services/plugin-auth-orchestration.ts
8235
- var PluginAuthorizationPauseError = class extends Error {
8251
+ // src/chat/services/auth-pause.ts
8252
+ var AuthorizationPauseError = class extends Error {
8253
+ disposition;
8254
+ kind;
8236
8255
  provider;
8237
- constructor(provider) {
8238
- super(`Plugin authorization started for ${provider}`);
8239
- this.name = "PluginAuthorizationPauseError";
8256
+ constructor(kind, provider, disposition) {
8257
+ super(
8258
+ kind === "mcp" ? `MCP authorization started for ${provider}` : `Plugin authorization started for ${provider}`
8259
+ );
8260
+ this.name = kind === "mcp" ? "McpAuthorizationPauseError" : "PluginAuthorizationPauseError";
8261
+ this.disposition = disposition;
8262
+ this.kind = kind;
8240
8263
  this.provider = provider;
8241
8264
  }
8242
8265
  };
8243
- function isCommandAuthFailure(details) {
8244
- if (!details || typeof details !== "object") {
8245
- return false;
8246
- }
8247
- const result = details;
8248
- if (typeof result.exit_code !== "number" || result.exit_code === 0) {
8249
- return false;
8250
- }
8251
- const text = `${typeof result.stdout === "string" ? result.stdout : ""}
8252
- ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
8253
- if (!text.trim()) {
8254
- return false;
8255
- }
8256
- return [
8257
- /\b401\b/,
8258
- /\bunauthorized\b/,
8259
- /\bbad credentials\b/,
8260
- /\binvalid token\b/,
8261
- /\btoken (?:expired|revoked)\b/,
8262
- /\bexpired token\b/,
8263
- /\bmissing scopes?\b/,
8264
- /\binsufficient scope\b/,
8265
- /\binvalid grant\b/,
8266
- /\breauthoriz/
8267
- ].some((pattern) => pattern.test(text));
8268
- }
8269
- function commandTargetsProvider(provider, command, details) {
8270
- const normalizedCommand = command.trim().toLowerCase();
8271
- if (!normalizedCommand) {
8272
- return false;
8273
- }
8274
- if (provider === "github" && /^(gh|git)\b/.test(normalizedCommand)) {
8275
- return true;
8276
- }
8277
- const plugin = getPluginDefinition(provider);
8278
- const candidates = /* @__PURE__ */ new Set([provider.toLowerCase()]);
8279
- const credentials = plugin?.manifest.credentials;
8280
- if (credentials) {
8281
- candidates.add(credentials.authTokenEnv.toLowerCase());
8282
- for (const domain of credentials.apiDomains) {
8283
- candidates.add(domain.toLowerCase());
8284
- }
8285
- }
8286
- const combinedText = `${normalizedCommand}
8287
- ${details.stdout?.toLowerCase() ?? ""}
8288
- ${details.stderr?.toLowerCase() ?? ""}`;
8289
- return [...candidates].some((candidate) => combinedText.includes(candidate));
8290
- }
8291
- function createPluginAuthOrchestration(deps, abortAgent) {
8292
- let pendingPause;
8293
- const startAuthorizationPause = async (provider, activeSkill, options) => {
8294
- if (pendingPause) {
8295
- throw pendingPause;
8296
- }
8297
- if (!deps.requesterId || !getPluginOAuthConfig(provider)) {
8298
- throw new Error(`Cannot start plugin authorization for ${provider}`);
8299
- }
8300
- const providerLabel = formatProviderLabel(provider);
8301
- const oauthResult = await startOAuthFlow(provider, {
8302
- requesterId: deps.requesterId,
8303
- channelId: deps.channelId,
8304
- threadTs: deps.threadTs,
8305
- userMessage: deps.userMessage,
8306
- channelConfiguration: deps.channelConfiguration,
8307
- activeSkillName: activeSkill?.name ?? void 0,
8308
- resumeConversationId: deps.conversationId,
8309
- resumeSessionId: deps.sessionId
8310
- });
8311
- if (!oauthResult.ok) {
8312
- throw new Error(oauthResult.error);
8313
- }
8314
- if (!oauthResult.delivery) {
8315
- throw new Error(
8316
- `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.`
8317
- );
8318
- }
8319
- if (options?.unlinkExistingProvider && deps.requesterId && deps.userTokenStore) {
8320
- await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
8321
- }
8322
- pendingPause = new PluginAuthorizationPauseError(provider);
8323
- abortAgent();
8324
- throw pendingPause;
8325
- };
8326
- const handleCredentialUnavailable = async (input) => {
8327
- if (pendingPause) {
8328
- throw pendingPause;
8329
- }
8330
- if (!deps.requesterId || !getPluginOAuthConfig(input.error.provider)) {
8331
- throw input.error;
8332
- }
8333
- return await startAuthorizationPause(
8334
- input.error.provider,
8335
- input.activeSkill
8336
- );
8337
- };
8338
- return {
8339
- handleCredentialUnavailable,
8340
- handleCommandFailure: async (input) => {
8341
- const provider = input.activeSkill?.pluginProvider;
8342
- if (!provider || !deps.requesterId || !deps.userTokenStore || !getPluginOAuthConfig(provider) || !isCommandAuthFailure(input.details) || !commandTargetsProvider(provider, input.command, input.details)) {
8343
- return;
8344
- }
8345
- await startAuthorizationPause(provider, input.activeSkill, {
8346
- unlinkExistingProvider: true
8347
- });
8348
- },
8349
- getPendingPause: () => pendingPause
8350
- };
8351
- }
8352
8266
 
8353
8267
  // src/chat/runtime/report-progress.ts
8354
8268
  function buildReportedProgressStatus(input) {
@@ -8396,15 +8310,16 @@ function resolveCredentialInjection(toolName, command, capabilityRuntime, sandbo
8396
8310
  const headerDomains = (headerTransforms ?? []).map(
8397
8311
  (transform) => transform.domain
8398
8312
  );
8313
+ const skillName = sandbox.getActiveSkill()?.name;
8399
8314
  logInfo(
8400
8315
  "credential_inject_start",
8401
8316
  {},
8402
8317
  {
8403
- "app.skill.name": sandbox.getActiveSkill()?.name,
8318
+ "app.skill.name": skillName,
8404
8319
  "app.credential.delivery": "header_transform",
8405
8320
  "app.credential.header_domains": headerDomains
8406
8321
  },
8407
- "Injecting scoped credential headers for sandbox command"
8322
+ `Injecting scoped credential headers for sandbox command (${skillName ?? "unknown skill"} \u2192 ${headerDomains.join(", ")})`
8408
8323
  );
8409
8324
  }
8410
8325
  return { headerTransforms, env };
@@ -8577,7 +8492,7 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
8577
8492
  }
8578
8493
  return normalized;
8579
8494
  } catch (error) {
8580
- if (error instanceof PluginAuthorizationPauseError) {
8495
+ if (error instanceof AuthorizationPauseError) {
8581
8496
  throw error;
8582
8497
  }
8583
8498
  handleToolExecutionError(
@@ -8788,14 +8703,6 @@ function getTerminalAssistantMessages(messages) {
8788
8703
  }
8789
8704
  return messages.slice(lastToolResultIndex + 1).filter(isAssistantMessage);
8790
8705
  }
8791
- function hasCompletedAssistantTurn(messages) {
8792
- const message = getTerminalAssistantMessages(messages).at(-1);
8793
- if (!message) {
8794
- return false;
8795
- }
8796
- const stopReason = message.stopReason;
8797
- return typeof stopReason === "string" && stopReason !== "error" && extractAssistantText(message).trim().length > 0;
8798
- }
8799
8706
  function upsertActiveSkill(activeSkills, next) {
8800
8707
  const existing = activeSkills.find((skill) => skill.name === next.name);
8801
8708
  if (existing) {
@@ -9023,7 +8930,10 @@ function buildTurnResult(input) {
9023
8930
  // src/chat/services/turn-thinking-level.ts
9024
8931
  import { z } from "zod";
9025
8932
  var CLASSIFIER_CONFIDENCE_THRESHOLD = 0.75;
9026
- var MAX_ROUTER_CONTEXT_CHARS = 1200;
8933
+ var MAX_ROUTER_CONTEXT_CHARS = 8e3;
8934
+ var ROUTER_CONTEXT_HEAD_CHARS = 3e3;
8935
+ var ROUTER_CONTEXT_TAIL_CHARS = 5e3;
8936
+ var TRUNCATION_MARKER = "\n\u2026[truncated]\u2026\n";
9027
8937
  var TURN_THINKING_LEVELS = ["none", "low", "medium", "high"];
9028
8938
  var turnExecutionProfileSchema = z.object({
9029
8939
  thinking_level: z.enum(TURN_THINKING_LEVELS),
@@ -9034,9 +8944,22 @@ var DEFAULT_THINKING_LEVEL = "low";
9034
8944
  function trimContextForRouter(text) {
9035
8945
  const trimmed = text?.trim();
9036
8946
  if (!trimmed) {
9037
- return void 0;
8947
+ return null;
8948
+ }
8949
+ if (trimmed.length <= MAX_ROUTER_CONTEXT_CHARS) {
8950
+ return {
8951
+ text: trimmed,
8952
+ truncated: false,
8953
+ originalCharCount: trimmed.length
8954
+ };
9038
8955
  }
9039
- return trimmed.length <= MAX_ROUTER_CONTEXT_CHARS ? trimmed : trimmed.slice(-MAX_ROUTER_CONTEXT_CHARS);
8956
+ const head = trimmed.slice(0, ROUTER_CONTEXT_HEAD_CHARS).trimEnd();
8957
+ const tail = trimmed.slice(-ROUTER_CONTEXT_TAIL_CHARS).trimStart();
8958
+ return {
8959
+ text: `${head}${TRUNCATION_MARKER}${tail}`,
8960
+ truncated: true,
8961
+ originalCharCount: trimmed.length
8962
+ };
9040
8963
  }
9041
8964
  function buildClassifierSystemPrompt() {
9042
8965
  return [
@@ -9048,22 +8971,23 @@ function buildClassifierSystemPrompt() {
9048
8971
  "Use medium for investigations, ambiguous asks, multi-step analysis, or likely multi-tool work.",
9049
8972
  "Use high for code changes, debugging/root-cause analysis, research-heavy work, non-trivial drafting, or explicit requests to be thorough.",
9050
8973
  "",
8974
+ "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.",
8975
+ "",
9051
8976
  "Return JSON only with thinking_level, confidence, and reason."
9052
8977
  ].join("\n");
9053
8978
  }
9054
8979
  function buildClassifierPrompt(args) {
9055
8980
  const sections = [];
9056
- const context = trimContextForRouter(args.conversationContext);
9057
- if (context) {
9058
- sections.push("<thread-background>", context, "</thread-background>", "");
8981
+ if (args.conversationContext) {
8982
+ sections.push(
8983
+ "<thread-background>",
8984
+ args.conversationContext.text,
8985
+ "</thread-background>",
8986
+ ""
8987
+ );
9059
8988
  }
9060
8989
  sections.push(
9061
- "<turn-context>",
9062
- `- active_skills: ${args.activeSkillNames.join(", ") || "none"}`,
9063
- `- attachment_count: ${args.attachmentCount}`,
9064
- "</turn-context>",
9065
- "",
9066
- '<current-instruction priority="highest">',
8990
+ "<current-instruction>",
9067
8991
  args.messageText.trim() || "[empty]",
9068
8992
  "</current-instruction>"
9069
8993
  );
@@ -9077,42 +9001,81 @@ function buildClassifierPrompt(args) {
9077
9001
  return sections.join("\n");
9078
9002
  }
9079
9003
  async function selectTurnThinkingLevel(args) {
9080
- const activeSkillNames = [...new Set(args.activeSkillNames ?? [])].sort();
9004
+ const trimmedContext = trimContextForRouter(args.conversationContext);
9005
+ const instructionLength = args.messageText.trim().length;
9006
+ const turnBlockCount = (args.currentTurnBlocks ?? []).filter(
9007
+ (block) => block.trim().length > 0
9008
+ ).length;
9009
+ const prompt = buildClassifierPrompt({
9010
+ conversationContext: trimmedContext,
9011
+ currentTurnBlocks: args.currentTurnBlocks,
9012
+ messageText: args.messageText
9013
+ });
9014
+ const logContext = {
9015
+ slackThreadId: args.context?.threadId,
9016
+ slackChannelId: args.context?.channelId,
9017
+ slackUserId: args.context?.requesterId,
9018
+ runId: args.context?.runId,
9019
+ modelId: args.fastModelId
9020
+ };
9021
+ return withSpan(
9022
+ "chat.route_thinking",
9023
+ "chat.route_thinking",
9024
+ logContext,
9025
+ async () => {
9026
+ setSpanAttributes({
9027
+ "app.ai.router.prompt_char_count": prompt.length,
9028
+ "app.ai.router.instruction_char_count": instructionLength,
9029
+ "app.ai.router.context_char_count": trimmedContext?.originalCharCount ?? 0,
9030
+ "app.ai.router.context_trimmed": trimmedContext?.truncated ?? false,
9031
+ "app.ai.router.turn_block_count": turnBlockCount
9032
+ });
9033
+ const selection = await classifyTurn({
9034
+ completeObject: args.completeObject,
9035
+ fastModelId: args.fastModelId,
9036
+ metadata: {
9037
+ modelId: args.fastModelId,
9038
+ threadId: args.context?.threadId ?? "",
9039
+ channelId: args.context?.channelId ?? "",
9040
+ requesterId: args.context?.requesterId ?? "",
9041
+ runId: args.context?.runId ?? ""
9042
+ },
9043
+ prompt
9044
+ });
9045
+ setSpanAttributes({
9046
+ "app.ai.thinking_level": selection.thinkingLevel,
9047
+ "app.ai.thinking_level_reason": selection.reason,
9048
+ ...selection.confidence !== void 0 ? { "app.ai.thinking_level_confidence": selection.confidence } : {}
9049
+ });
9050
+ return selection;
9051
+ }
9052
+ );
9053
+ }
9054
+ async function classifyTurn(args) {
9081
9055
  try {
9082
9056
  const result = await args.completeObject({
9083
9057
  modelId: args.fastModelId,
9084
9058
  schema: turnExecutionProfileSchema,
9085
9059
  maxTokens: 120,
9086
- metadata: {
9087
- modelId: args.fastModelId,
9088
- threadId: args.context?.threadId ?? "",
9089
- channelId: args.context?.channelId ?? "",
9090
- requesterId: args.context?.requesterId ?? "",
9091
- runId: args.context?.runId ?? ""
9092
- },
9093
- prompt: buildClassifierPrompt({
9094
- activeSkillNames,
9095
- attachmentCount: args.attachmentCount ?? 0,
9096
- conversationContext: args.conversationContext,
9097
- currentTurnBlocks: args.currentTurnBlocks,
9098
- messageText: args.messageText
9099
- }),
9060
+ metadata: args.metadata,
9061
+ prompt: args.prompt,
9100
9062
  thinkingLevel: "low",
9101
9063
  system: buildClassifierSystemPrompt(),
9102
9064
  temperature: 0
9103
9065
  });
9104
9066
  const parsed = turnExecutionProfileSchema.parse(result.object);
9067
+ const reason = parsed.reason.trim();
9105
9068
  if (parsed.confidence < CLASSIFIER_CONFIDENCE_THRESHOLD) {
9106
9069
  return {
9107
9070
  confidence: parsed.confidence,
9108
9071
  thinkingLevel: DEFAULT_THINKING_LEVEL,
9109
- reason: `low_confidence_default:${parsed.reason.trim()}`
9072
+ reason: `low_confidence_default:${reason}`
9110
9073
  };
9111
9074
  }
9112
9075
  return {
9113
9076
  confidence: parsed.confidence,
9114
9077
  thinkingLevel: parsed.thinking_level,
9115
- reason: parsed.reason.trim()
9078
+ reason
9116
9079
  };
9117
9080
  } catch {
9118
9081
  return {
@@ -9150,7 +9113,7 @@ function parseAgentTurnSessionCheckpoint(value) {
9150
9113
  return void 0;
9151
9114
  }
9152
9115
  const status = parsed.state;
9153
- if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed") {
9116
+ if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed" && status !== "superseded") {
9154
9117
  return void 0;
9155
9118
  }
9156
9119
  const conversationId = parsed.conversationId;
@@ -9222,6 +9185,26 @@ async function upsertAgentTurnSessionCheckpoint(args) {
9222
9185
  );
9223
9186
  return checkpoint;
9224
9187
  }
9188
+ async function supersedeAgentTurnSessionCheckpoint(args) {
9189
+ const existing = await getAgentTurnSessionCheckpoint(
9190
+ args.conversationId,
9191
+ args.sessionId
9192
+ );
9193
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "superseded") {
9194
+ return void 0;
9195
+ }
9196
+ return await upsertAgentTurnSessionCheckpoint({
9197
+ conversationId: existing.conversationId,
9198
+ sessionId: existing.sessionId,
9199
+ sliceId: existing.sliceId,
9200
+ state: "superseded",
9201
+ piMessages: existing.piMessages,
9202
+ loadedSkillNames: existing.loadedSkillNames,
9203
+ resumeReason: existing.resumeReason,
9204
+ resumedFromSliceId: existing.resumedFromSliceId,
9205
+ errorMessage: args.errorMessage ?? existing.errorMessage
9206
+ });
9207
+ }
9225
9208
 
9226
9209
  // src/chat/services/turn-checkpoint.ts
9227
9210
  async function loadTurnCheckpoint(ctx) {
@@ -9336,13 +9319,67 @@ async function persistTimeoutCheckpoint(args) {
9336
9319
  }
9337
9320
  }
9338
9321
 
9322
+ // src/chat/services/pending-auth.ts
9323
+ var AUTH_LINK_REUSE_WINDOW_MS = 10 * 60 * 1e3;
9324
+ function canReusePendingAuthLink(args) {
9325
+ const { pendingAuth } = args;
9326
+ if (!pendingAuth) {
9327
+ return false;
9328
+ }
9329
+ return pendingAuth.kind === args.kind && pendingAuth.provider === args.provider && pendingAuth.requesterId === args.requesterId && pendingAuth.linkSentAtMs + AUTH_LINK_REUSE_WINDOW_MS > (args.nowMs ?? Date.now());
9330
+ }
9331
+ function buildAuthPauseReplyText(args) {
9332
+ const providerLabel = args.provider ? formatProviderLabel(args.provider) : "";
9333
+ if (args.disposition === "link_already_sent") {
9334
+ return providerLabel ? `I still need your ${providerLabel} access to continue. I already sent you a private link.` : "I still need additional access to continue. I already sent you a private link.";
9335
+ }
9336
+ return providerLabel ? `I need your ${providerLabel} access to continue. I sent you a private link.` : "I need additional access to continue. I sent you a private link.";
9337
+ }
9338
+ function getConversationPendingAuth(args) {
9339
+ const pendingAuth = args.conversation.processing.pendingAuth;
9340
+ if (!pendingAuth) {
9341
+ return void 0;
9342
+ }
9343
+ if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId) {
9344
+ return void 0;
9345
+ }
9346
+ return pendingAuth;
9347
+ }
9348
+ function clearPendingAuth(conversation, sessionId) {
9349
+ if (!conversation.processing.pendingAuth) {
9350
+ return;
9351
+ }
9352
+ if (sessionId && conversation.processing.pendingAuth.sessionId !== sessionId) {
9353
+ return;
9354
+ }
9355
+ conversation.processing.pendingAuth = void 0;
9356
+ }
9357
+ async function applyPendingAuthUpdate(args) {
9358
+ const previousPendingAuth = args.conversation.processing.pendingAuth;
9359
+ args.conversation.processing.pendingAuth = args.nextPendingAuth;
9360
+ if (previousPendingAuth && previousPendingAuth.sessionId !== args.nextPendingAuth.sessionId && args.conversationId) {
9361
+ await supersedeAgentTurnSessionCheckpoint({
9362
+ conversationId: args.conversationId,
9363
+ sessionId: previousPendingAuth.sessionId,
9364
+ errorMessage: "Superseded by a newer auth-blocked request in the same conversation."
9365
+ });
9366
+ }
9367
+ }
9368
+ function isPendingAuthLatestRequest(conversation, pendingAuth) {
9369
+ for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
9370
+ const message = conversation.messages[index];
9371
+ if (message?.role !== "user") {
9372
+ continue;
9373
+ }
9374
+ return buildDeterministicTurnId(message.id) === pendingAuth.sessionId;
9375
+ }
9376
+ return false;
9377
+ }
9378
+
9339
9379
  // src/chat/services/mcp-auth-orchestration.ts
9340
- var McpAuthorizationPauseError = class extends Error {
9341
- provider;
9342
- constructor(provider) {
9343
- super(`MCP authorization started for ${provider}`);
9344
- this.name = "McpAuthorizationPauseError";
9345
- this.provider = provider;
9380
+ var McpAuthorizationPauseError = class extends AuthorizationPauseError {
9381
+ constructor(provider, disposition) {
9382
+ super("mcp", provider, disposition);
9346
9383
  }
9347
9384
  };
9348
9385
  function createMcpAuthOrchestration(deps, abortAgent) {
@@ -9387,18 +9424,40 @@ function createMcpAuthOrchestration(deps, abortAgent) {
9387
9424
  if (!authSession?.authorizationUrl) {
9388
9425
  throw new Error(`Missing MCP authorization URL for plugin "${provider}"`);
9389
9426
  }
9390
- const delivery = await deliverPrivateMessage({
9391
- channelId: authSession.channelId,
9392
- threadTs: authSession.threadTs,
9393
- userId: authSession.userId,
9394
- text: `<${authSession.authorizationUrl}|Click here to link your ${formatProviderLabel(provider)} MCP access>. Once you've authorized, this thread will continue automatically.`
9427
+ const reusingPendingLink = canReusePendingAuthLink({
9428
+ pendingAuth: deps.currentPendingAuth,
9429
+ kind: "mcp",
9430
+ provider,
9431
+ requesterId: deps.requesterId
9395
9432
  });
9396
- if (!delivery) {
9397
- throw new Error(
9398
- `Unable to deliver MCP authorization link for plugin "${provider}"`
9399
- );
9433
+ if (!reusingPendingLink) {
9434
+ const delivery = await deliverPrivateMessage({
9435
+ channelId: authSession.channelId,
9436
+ threadTs: authSession.threadTs,
9437
+ userId: authSession.userId,
9438
+ text: `<${authSession.authorizationUrl}|Click here to link your ${formatProviderLabel(provider)} MCP access>. Once you've authorized, this thread will continue automatically.`
9439
+ });
9440
+ if (!delivery) {
9441
+ throw new Error(
9442
+ `Unable to deliver MCP authorization link for plugin "${provider}"`
9443
+ );
9444
+ }
9445
+ } else {
9446
+ await deleteMcpAuthSession(authSessionId);
9447
+ }
9448
+ if (deps.sessionId && deps.requesterId) {
9449
+ await deps.onPendingAuth?.({
9450
+ kind: "mcp",
9451
+ provider,
9452
+ requesterId: deps.requesterId,
9453
+ sessionId: deps.sessionId,
9454
+ linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
9455
+ });
9400
9456
  }
9401
- pendingPause = new McpAuthorizationPauseError(provider);
9457
+ pendingPause = new McpAuthorizationPauseError(
9458
+ provider,
9459
+ reusingPendingLink ? "link_already_sent" : "link_sent"
9460
+ );
9402
9461
  abortAgent();
9403
9462
  return true;
9404
9463
  };
@@ -9409,6 +9468,152 @@ function createMcpAuthOrchestration(deps, abortAgent) {
9409
9468
  };
9410
9469
  }
9411
9470
 
9471
+ // src/chat/credentials/unlink-provider.ts
9472
+ async function unlinkProvider(userId, provider, userTokenStore) {
9473
+ await Promise.all([
9474
+ userTokenStore.delete(userId, provider),
9475
+ deleteMcpStoredOAuthCredentials(userId, provider),
9476
+ deleteMcpServerSessionId(userId, provider),
9477
+ deleteMcpAuthSessionsForUserProvider(userId, provider)
9478
+ ]);
9479
+ }
9480
+
9481
+ // src/chat/services/plugin-auth-orchestration.ts
9482
+ var PluginAuthorizationPauseError = class extends AuthorizationPauseError {
9483
+ constructor(provider, disposition) {
9484
+ super("plugin", provider, disposition);
9485
+ }
9486
+ };
9487
+ function isCommandAuthFailure(details) {
9488
+ if (!details || typeof details !== "object") {
9489
+ return false;
9490
+ }
9491
+ const result = details;
9492
+ if (typeof result.exit_code !== "number" || result.exit_code === 0) {
9493
+ return false;
9494
+ }
9495
+ const text = `${typeof result.stdout === "string" ? result.stdout : ""}
9496
+ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
9497
+ if (!text.trim()) {
9498
+ return false;
9499
+ }
9500
+ return [
9501
+ /\b401\b/,
9502
+ /\bunauthorized\b/,
9503
+ /\bbad credentials\b/,
9504
+ /\binvalid token\b/,
9505
+ /\btoken (?:expired|revoked)\b/,
9506
+ /\bexpired token\b/,
9507
+ /\bmissing scopes?\b/,
9508
+ /\binsufficient scope\b/,
9509
+ /\binvalid grant\b/,
9510
+ /\breauthoriz/
9511
+ ].some((pattern) => pattern.test(text));
9512
+ }
9513
+ function commandTargetsProvider(provider, command, details) {
9514
+ const normalizedCommand = command.trim().toLowerCase();
9515
+ if (!normalizedCommand) {
9516
+ return false;
9517
+ }
9518
+ if (provider === "github" && /^(gh|git)\b/.test(normalizedCommand)) {
9519
+ return true;
9520
+ }
9521
+ const plugin = getPluginDefinition(provider);
9522
+ const candidates = /* @__PURE__ */ new Set([provider.toLowerCase()]);
9523
+ const credentials = plugin?.manifest.credentials;
9524
+ if (credentials) {
9525
+ candidates.add(credentials.authTokenEnv.toLowerCase());
9526
+ for (const domain of credentials.apiDomains) {
9527
+ candidates.add(domain.toLowerCase());
9528
+ }
9529
+ }
9530
+ const combinedText = `${normalizedCommand}
9531
+ ${details.stdout?.toLowerCase() ?? ""}
9532
+ ${details.stderr?.toLowerCase() ?? ""}`;
9533
+ return [...candidates].some((candidate) => combinedText.includes(candidate));
9534
+ }
9535
+ function createPluginAuthOrchestration(deps, abortAgent) {
9536
+ let pendingPause;
9537
+ const startAuthorizationPause = async (provider, activeSkill, options) => {
9538
+ if (pendingPause) {
9539
+ throw pendingPause;
9540
+ }
9541
+ if (!deps.requesterId || !getPluginOAuthConfig(provider)) {
9542
+ throw new Error(`Cannot start plugin authorization for ${provider}`);
9543
+ }
9544
+ const providerLabel = formatProviderLabel(provider);
9545
+ const reusingPendingLink = canReusePendingAuthLink({
9546
+ pendingAuth: deps.currentPendingAuth,
9547
+ kind: "plugin",
9548
+ provider,
9549
+ requesterId: deps.requesterId
9550
+ });
9551
+ if (!reusingPendingLink) {
9552
+ const oauthResult = await startOAuthFlow(provider, {
9553
+ requesterId: deps.requesterId,
9554
+ channelId: deps.channelId,
9555
+ threadTs: deps.threadTs,
9556
+ userMessage: deps.userMessage,
9557
+ channelConfiguration: deps.channelConfiguration,
9558
+ activeSkillName: activeSkill?.name ?? void 0,
9559
+ resumeConversationId: deps.conversationId,
9560
+ resumeSessionId: deps.sessionId
9561
+ });
9562
+ if (!oauthResult.ok) {
9563
+ throw new Error(oauthResult.error);
9564
+ }
9565
+ if (!oauthResult.delivery) {
9566
+ throw new Error(
9567
+ `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.`
9568
+ );
9569
+ }
9570
+ }
9571
+ if (options?.unlinkExistingProvider && deps.requesterId && deps.userTokenStore) {
9572
+ await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
9573
+ }
9574
+ if (deps.sessionId) {
9575
+ await deps.onPendingAuth?.({
9576
+ kind: "plugin",
9577
+ provider,
9578
+ requesterId: deps.requesterId,
9579
+ sessionId: deps.sessionId,
9580
+ linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
9581
+ });
9582
+ }
9583
+ pendingPause = new PluginAuthorizationPauseError(
9584
+ provider,
9585
+ reusingPendingLink ? "link_already_sent" : "link_sent"
9586
+ );
9587
+ abortAgent();
9588
+ throw pendingPause;
9589
+ };
9590
+ const handleCredentialUnavailable = async (input) => {
9591
+ if (pendingPause) {
9592
+ throw pendingPause;
9593
+ }
9594
+ if (!deps.requesterId || !getPluginOAuthConfig(input.error.provider)) {
9595
+ throw input.error;
9596
+ }
9597
+ return await startAuthorizationPause(
9598
+ input.error.provider,
9599
+ input.activeSkill
9600
+ );
9601
+ };
9602
+ return {
9603
+ handleCredentialUnavailable,
9604
+ handleCommandFailure: async (input) => {
9605
+ const provider = input.activeSkill?.pluginProvider;
9606
+ if (!provider || !deps.requesterId || !deps.userTokenStore || !getPluginOAuthConfig(provider) || !isCommandAuthFailure(input.details) || !commandTargetsProvider(provider, input.command, input.details)) {
9607
+ return;
9608
+ }
9609
+ await startAuthorizationPause(provider, input.activeSkill, {
9610
+ unlinkExistingProvider: true
9611
+ });
9612
+ },
9613
+ getPendingPause: () => pendingPause
9614
+ };
9615
+ }
9616
+
9412
9617
  // src/chat/respond.ts
9413
9618
  var startupDiscoveryLogged = false;
9414
9619
  var MAX_ROUTER_ATTACHMENT_PREVIEW_CHARS = 2e3;
@@ -9525,6 +9730,7 @@ async function generateAssistantReply(messageText, context = {}) {
9525
9730
  let timeoutResumeSessionId;
9526
9731
  let timeoutResumeSliceId = 1;
9527
9732
  let timeoutResumeMessages = [];
9733
+ let beforeMessageCount = 0;
9528
9734
  let lastKnownSandboxId = context.sandbox?.sandboxId;
9529
9735
  let lastKnownSandboxDependencyProfileHash = context.sandbox?.sandboxDependencyProfileHash;
9530
9736
  let loadedSkillNamesForResume = [];
@@ -9717,8 +9923,6 @@ async function generateAssistantReply(messageText, context = {}) {
9717
9923
  userTurnText
9718
9924
  });
9719
9925
  thinkingSelection = await selectTurnThinkingLevel({
9720
- activeSkillNames: activeSkills.map((skill) => skill.name),
9721
- attachmentCount: context.userAttachments?.length,
9722
9926
  completeObject,
9723
9927
  conversationContext: context.conversationContext,
9724
9928
  context: {
@@ -9754,9 +9958,11 @@ async function generateAssistantReply(messageText, context = {}) {
9754
9958
  threadTs: context.correlation?.threadTs,
9755
9959
  toolChannelId: context.toolChannelId,
9756
9960
  userMessage: userInput,
9961
+ currentPendingAuth: context.pendingAuth,
9757
9962
  getConfiguration: () => configurationValues,
9758
9963
  getArtifactState: () => context.artifactState,
9759
- getMergedArtifactState: () => mergeArtifactsState(context.artifactState ?? {}, artifactStatePatch)
9964
+ getMergedArtifactState: () => mergeArtifactsState(context.artifactState ?? {}, artifactStatePatch),
9965
+ onPendingAuth: context.onAuthPending
9760
9966
  },
9761
9967
  () => agent?.abort()
9762
9968
  );
@@ -9769,6 +9975,8 @@ async function generateAssistantReply(messageText, context = {}) {
9769
9975
  threadTs: context.correlation?.threadTs,
9770
9976
  userMessage: userInput,
9771
9977
  channelConfiguration: context.channelConfiguration,
9978
+ currentPendingAuth: context.pendingAuth,
9979
+ onPendingAuth: context.onAuthPending,
9772
9980
  userTokenStore
9773
9981
  },
9774
9982
  () => agent?.abort()
@@ -9987,9 +10195,8 @@ async function generateAssistantReply(messageText, context = {}) {
9987
10195
  );
9988
10196
  });
9989
10197
  });
9990
- let beforeMessageCount = agent.state.messages.length;
9991
10198
  let newMessages = [];
9992
- let completedAssistantTurn = false;
10199
+ beforeMessageCount = agent.state.messages.length;
9993
10200
  try {
9994
10201
  if (resumedFromCheckpoint) {
9995
10202
  agent.state.messages = existingCheckpoint.piMessages;
@@ -10056,11 +10263,6 @@ async function generateAssistantReply(messageText, context = {}) {
10056
10263
  }
10057
10264
  }
10058
10265
  newMessages = agent.state.messages.slice(beforeMessageCount);
10059
- completedAssistantTurn = hasCompletedAssistantTurn(newMessages);
10060
- if (getPendingAuthPause() && !completedAssistantTurn) {
10061
- timeoutResumeMessages = [...agent.state.messages];
10062
- throw getPendingAuthPause();
10063
- }
10064
10266
  const outputMessages = newMessages.filter(isAssistantMessage);
10065
10267
  const outputMessagesAttribute = serializeGenAiAttribute(outputMessages);
10066
10268
  const usageSummary = extractGenAiUsageSummary(
@@ -10076,6 +10278,10 @@ async function generateAssistantReply(messageText, context = {}) {
10076
10278
  ...usageSummary.inputTokens !== void 0 ? { "gen_ai.usage.input_tokens": usageSummary.inputTokens } : {},
10077
10279
  ...usageSummary.outputTokens !== void 0 ? { "gen_ai.usage.output_tokens": usageSummary.outputTokens } : {}
10078
10280
  });
10281
+ if (getPendingAuthPause()) {
10282
+ timeoutResumeMessages = [...agent.state.messages];
10283
+ throw getPendingAuthPause();
10284
+ }
10079
10285
  },
10080
10286
  {
10081
10287
  "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
@@ -10088,9 +10294,6 @@ async function generateAssistantReply(messageText, context = {}) {
10088
10294
  } finally {
10089
10295
  unsubscribe();
10090
10296
  }
10091
- if (getPendingAuthPause() && !completedAssistantTurn) {
10092
- throw getPendingAuthPause();
10093
- }
10094
10297
  if (checkpointState.canUseTurnSession && sessionConversationId && sessionId) {
10095
10298
  await persistCompletedCheckpoint({
10096
10299
  conversationId: sessionConversationId,
@@ -10148,7 +10351,15 @@ async function generateAssistantReply(messageText, context = {}) {
10148
10351
  );
10149
10352
  }
10150
10353
  }
10151
- if ((error instanceof McpAuthorizationPauseError || error instanceof PluginAuthorizationPauseError) && timeoutResumeConversationId && timeoutResumeSessionId) {
10354
+ if (error instanceof AuthorizationPauseError && timeoutResumeConversationId && timeoutResumeSessionId) {
10355
+ if (!turnUsage && timeoutResumeMessages.length > 0) {
10356
+ const fallbackUsage = extractGenAiUsageSummary(
10357
+ ...timeoutResumeMessages.slice(beforeMessageCount).filter(isAssistantMessage)
10358
+ );
10359
+ turnUsage = Object.values(fallbackUsage).some(
10360
+ (value) => value !== void 0
10361
+ ) ? fallbackUsage : void 0;
10362
+ }
10152
10363
  const nextSliceId = await persistAuthPauseCheckpoint({
10153
10364
  conversationId: timeoutResumeConversationId,
10154
10365
  sessionId: timeoutResumeSessionId,
@@ -10166,9 +10377,15 @@ async function generateAssistantReply(messageText, context = {}) {
10166
10377
  }
10167
10378
  });
10168
10379
  throw new RetryableTurnError(
10169
- error instanceof PluginAuthorizationPauseError ? "plugin_auth_resume" : "mcp_auth_resume",
10380
+ error.kind === "plugin" ? "plugin_auth_resume" : "mcp_auth_resume",
10170
10381
  `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`,
10171
10382
  {
10383
+ authDisposition: error.disposition,
10384
+ authDurationMs: Date.now() - replyStartedAtMs,
10385
+ authKind: error.kind,
10386
+ authProvider: error.provider,
10387
+ authThinkingLevel: thinkingSelection?.thinkingLevel,
10388
+ authUsage: turnUsage,
10172
10389
  conversationId: timeoutResumeConversationId,
10173
10390
  sessionId: timeoutResumeSessionId,
10174
10391
  sliceId: nextSliceId
@@ -11041,6 +11258,82 @@ async function resumeAuthorizedRequest(args) {
11041
11258
  });
11042
11259
  }
11043
11260
 
11261
+ // src/chat/runtime/auth-pause-reply.ts
11262
+ function buildAuthPauseSlackMessage(args) {
11263
+ const footer = buildSlackReplyFooter({
11264
+ conversationId: args.conversationId,
11265
+ durationMs: args.durationMs,
11266
+ thinkingLevel: args.thinkingLevel,
11267
+ usage: args.usage
11268
+ });
11269
+ const blocks = buildSlackReplyBlocks(args.text, footer);
11270
+ return blocks ? { text: args.text, blocks } : { text: args.text };
11271
+ }
11272
+ function completeAuthPauseTurn(args) {
11273
+ markConversationMessage(
11274
+ args.conversation,
11275
+ getTurnUserMessageId(args.conversation, args.sessionId),
11276
+ {
11277
+ replied: true,
11278
+ skippedReason: void 0
11279
+ }
11280
+ );
11281
+ upsertConversationMessage(args.conversation, {
11282
+ id: generateConversationId("assistant"),
11283
+ role: "assistant",
11284
+ text: normalizeConversationText(args.text) || "[empty response]",
11285
+ createdAtMs: Date.now(),
11286
+ author: {
11287
+ userName: botConfig.userName,
11288
+ isBot: true
11289
+ },
11290
+ meta: {
11291
+ replied: true
11292
+ }
11293
+ });
11294
+ markTurnCompleted({
11295
+ conversation: args.conversation,
11296
+ nowMs: Date.now(),
11297
+ sessionId: args.sessionId,
11298
+ updateConversationStats
11299
+ });
11300
+ }
11301
+ async function persistAuthPauseReplyState(args) {
11302
+ const currentState = await getPersistedThreadState(args.threadStateId);
11303
+ const conversation = coerceThreadConversationState(currentState);
11304
+ completeAuthPauseTurn({
11305
+ conversation,
11306
+ sessionId: args.sessionId,
11307
+ text: args.text
11308
+ });
11309
+ await persistThreadStateById(args.threadStateId, { conversation });
11310
+ }
11311
+ async function deliverAuthPauseReply(args) {
11312
+ const retryable = isRetryableTurnError(args.error) ? args.error : void 0;
11313
+ const text = retryable ? buildAuthPauseReplyText({
11314
+ disposition: retryable.metadata?.authDisposition,
11315
+ provider: retryable.metadata?.authProvider
11316
+ }) : buildAuthPauseReplyText({ provider: args.fallbackProvider });
11317
+ const message = buildAuthPauseSlackMessage({
11318
+ conversationId: args.conversationId,
11319
+ durationMs: retryable?.metadata?.authDurationMs,
11320
+ text,
11321
+ thinkingLevel: retryable?.metadata?.authThinkingLevel,
11322
+ usage: retryable?.metadata?.authUsage
11323
+ });
11324
+ await postSlackMessage({
11325
+ channelId: args.channelId,
11326
+ threadTs: args.threadTs,
11327
+ text: message.text,
11328
+ ...message.blocks ? { blocks: message.blocks } : {}
11329
+ });
11330
+ await persistAuthPauseReplyState({
11331
+ sessionId: args.sessionId,
11332
+ text,
11333
+ threadStateId: args.threadStateId
11334
+ });
11335
+ }
11336
+
11044
11337
  // src/chat/services/timeout-resume.ts
11045
11338
  import { createHmac, timingSafeEqual } from "crypto";
11046
11339
  var TURN_TIMEOUT_RESUME_PATH = "/api/internal/turn-resume";
@@ -11213,6 +11506,7 @@ async function persistCompletedReplyState(channelId, threadTs, sessionId, reply)
11213
11506
  const artifacts = coerceThreadArtifactsState(currentState);
11214
11507
  const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : void 0;
11215
11508
  const userMessageId = getTurnUserMessageId(conversation, sessionId);
11509
+ clearPendingAuth(conversation, sessionId);
11216
11510
  markConversationMessage(conversation, userMessageId, {
11217
11511
  replied: true,
11218
11512
  skippedReason: void 0
@@ -11233,6 +11527,7 @@ async function persistCompletedReplyState(channelId, threadTs, sessionId, reply)
11233
11527
  markTurnCompleted({
11234
11528
  conversation,
11235
11529
  nowMs: Date.now(),
11530
+ sessionId,
11236
11531
  updateConversationStats
11237
11532
  });
11238
11533
  await persistThreadStateById(threadId, {
@@ -11246,9 +11541,11 @@ async function persistFailedReplyState(channelId, threadTs, sessionId) {
11246
11541
  const threadId = `slack:${channelId}:${threadTs}`;
11247
11542
  const currentState = await getPersistedThreadState(threadId);
11248
11543
  const conversation = coerceThreadConversationState(currentState);
11544
+ clearPendingAuth(conversation, sessionId);
11249
11545
  markTurnFailed({
11250
11546
  conversation,
11251
11547
  nowMs: Date.now(),
11548
+ sessionId,
11252
11549
  userMessageId: getTurnUserMessageId(conversation, sessionId),
11253
11550
  markConversationMessage,
11254
11551
  updateConversationStats
@@ -11266,8 +11563,29 @@ async function resumeAuthorizedMcpTurn(args) {
11266
11563
  const currentState = await getPersistedThreadState(threadId);
11267
11564
  const conversation = coerceThreadConversationState(currentState);
11268
11565
  const artifacts = coerceThreadArtifactsState(currentState);
11269
- const userMessage = getTurnUserMessage(conversation, authSession.sessionId);
11270
- if (conversation.processing.activeTurnId !== authSession.sessionId) {
11566
+ const pendingAuth = getConversationPendingAuth({
11567
+ conversation,
11568
+ kind: "mcp",
11569
+ provider,
11570
+ requesterId: authSession.userId
11571
+ });
11572
+ const resolvedSessionId = pendingAuth?.sessionId ?? authSession.sessionId;
11573
+ const userMessage = getTurnUserMessage(conversation, resolvedSessionId);
11574
+ if (pendingAuth) {
11575
+ if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
11576
+ clearPendingAuth(conversation, pendingAuth.sessionId);
11577
+ await persistThreadStateById(threadId, { conversation });
11578
+ await supersedeAgentTurnSessionCheckpoint({
11579
+ conversationId: authSession.conversationId,
11580
+ sessionId: pendingAuth.sessionId,
11581
+ errorMessage: "Auth completed after a newer thread message superseded this blocked request."
11582
+ });
11583
+ return;
11584
+ }
11585
+ } else if (conversation.processing.activeTurnId !== authSession.sessionId) {
11586
+ return;
11587
+ }
11588
+ if (!userMessage) {
11271
11589
  return;
11272
11590
  }
11273
11591
  const channelConfiguration = getChannelConfigurationServiceById(
@@ -11276,14 +11594,14 @@ async function resumeAuthorizedMcpTurn(args) {
11276
11594
  const conversationContext = await buildResumeConversationContext(
11277
11595
  authSession.channelId,
11278
11596
  authSession.threadTs,
11279
- authSession.sessionId
11597
+ resolvedSessionId
11280
11598
  );
11281
11599
  await resumeAuthorizedRequest({
11282
- messageText: authSession.userMessage,
11600
+ messageText: userMessage.text,
11283
11601
  channelId: authSession.channelId,
11284
11602
  threadTs: authSession.threadTs,
11285
11603
  lockKey: authSession.conversationId,
11286
- connectedText: `Your ${provider} MCP access is now connected. Continuing the original request...`,
11604
+ connectedText: "",
11287
11605
  failureText: "MCP authorization completed, but resuming the request failed. Please retry the original command.",
11288
11606
  replyContext: {
11289
11607
  assistant: { userName: botConfig.userName },
@@ -11294,7 +11612,7 @@ async function resumeAuthorizedMcpTurn(args) {
11294
11612
  },
11295
11613
  correlation: {
11296
11614
  conversationId: authSession.conversationId,
11297
- turnId: authSession.sessionId,
11615
+ turnId: resolvedSessionId,
11298
11616
  channelId: authSession.channelId,
11299
11617
  threadTs: authSession.threadTs,
11300
11618
  requesterId: authSession.userId
@@ -11303,9 +11621,18 @@ async function resumeAuthorizedMcpTurn(args) {
11303
11621
  conversationContext,
11304
11622
  artifactState: artifacts,
11305
11623
  configuration: authSession.configuration,
11624
+ pendingAuth,
11306
11625
  channelConfiguration,
11307
11626
  sandbox: getPersistedSandboxState(currentState),
11308
11627
  threadParticipants: buildThreadParticipants(conversation.messages),
11628
+ onAuthPending: async (nextPendingAuth) => {
11629
+ await applyPendingAuthUpdate({
11630
+ conversation,
11631
+ conversationId: authSession.conversationId,
11632
+ nextPendingAuth
11633
+ });
11634
+ await persistThreadStateById(threadId, { conversation });
11635
+ },
11309
11636
  ...getTurnUserReplyAttachmentContext(userMessage)
11310
11637
  },
11311
11638
  onSuccess: async (reply) => {
@@ -11313,7 +11640,7 @@ async function resumeAuthorizedMcpTurn(args) {
11313
11640
  await persistCompletedReplyState(
11314
11641
  authSession.channelId,
11315
11642
  authSession.threadTs,
11316
- authSession.sessionId,
11643
+ resolvedSessionId,
11317
11644
  reply
11318
11645
  );
11319
11646
  } catch (persistError) {
@@ -11338,7 +11665,7 @@ async function resumeAuthorizedMcpTurn(args) {
11338
11665
  await persistFailedReplyState(
11339
11666
  authSession.channelId,
11340
11667
  authSession.threadTs,
11341
- authSession.sessionId
11668
+ resolvedSessionId
11342
11669
  );
11343
11670
  } catch (persistError) {
11344
11671
  logException(
@@ -11350,7 +11677,16 @@ async function resumeAuthorizedMcpTurn(args) {
11350
11677
  );
11351
11678
  }
11352
11679
  },
11353
- onAuthPause: async () => {
11680
+ onAuthPause: async (error) => {
11681
+ await deliverAuthPauseReply({
11682
+ channelId: authSession.channelId,
11683
+ conversationId: authSession.conversationId,
11684
+ error,
11685
+ fallbackProvider: provider,
11686
+ sessionId: resolvedSessionId,
11687
+ threadStateId: `slack:${authSession.channelId}:${authSession.threadTs}`,
11688
+ threadTs: authSession.threadTs
11689
+ });
11354
11690
  logWarn(
11355
11691
  "mcp_oauth_callback_resume_reparked_for_auth",
11356
11692
  {},
@@ -11385,7 +11721,7 @@ async function resumeAuthorizedMcpTurn(args) {
11385
11721
  }
11386
11722
  await scheduleTurnTimeoutResume({
11387
11723
  conversationId: authSession.conversationId,
11388
- sessionId: authSession.sessionId,
11724
+ sessionId: resolvedSessionId,
11389
11725
  expectedCheckpointVersion: checkpointVersion
11390
11726
  });
11391
11727
  }
@@ -11610,6 +11946,7 @@ async function persistCompletedOAuthReplyState(args) {
11610
11946
  const artifacts = coerceThreadArtifactsState(currentState);
11611
11947
  const nextArtifacts = args.reply.artifactStatePatch ? mergeArtifactsState(artifacts, args.reply.artifactStatePatch) : void 0;
11612
11948
  const userMessage = getTurnUserMessage(conversation, args.sessionId);
11949
+ clearPendingAuth(conversation, args.sessionId);
11613
11950
  markConversationMessage(conversation, userMessage?.id, {
11614
11951
  replied: true,
11615
11952
  skippedReason: void 0
@@ -11630,6 +11967,7 @@ async function persistCompletedOAuthReplyState(args) {
11630
11967
  markTurnCompleted({
11631
11968
  conversation,
11632
11969
  nowMs: Date.now(),
11970
+ sessionId: args.sessionId,
11633
11971
  updateConversationStats
11634
11972
  });
11635
11973
  await persistThreadStateById(args.conversationId, {
@@ -11642,9 +11980,11 @@ async function persistCompletedOAuthReplyState(args) {
11642
11980
  async function persistFailedOAuthReplyState(args) {
11643
11981
  const currentState = await getPersistedThreadState(args.conversationId);
11644
11982
  const conversation = coerceThreadConversationState(currentState);
11983
+ clearPendingAuth(conversation, args.sessionId);
11645
11984
  markTurnFailed({
11646
11985
  conversation,
11647
11986
  nowMs: Date.now(),
11987
+ sessionId: args.sessionId,
11648
11988
  userMessageId: getTurnUserMessage(conversation, args.sessionId)?.id,
11649
11989
  markConversationMessage,
11650
11990
  updateConversationStats
@@ -11661,35 +12001,65 @@ async function resumeCheckpointedOAuthTurn(stored) {
11661
12001
  stored.resumeConversationId,
11662
12002
  stored.resumeSessionId
11663
12003
  );
11664
- if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "auth") {
12004
+ if (!checkpoint) {
11665
12005
  return false;
11666
12006
  }
12007
+ if (checkpoint.state === "completed" || checkpoint.state === "failed" || checkpoint.state === "superseded") {
12008
+ return true;
12009
+ }
12010
+ if (checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "auth") {
12011
+ return true;
12012
+ }
11667
12013
  const currentState = await getPersistedThreadState(
11668
12014
  stored.resumeConversationId
11669
12015
  );
11670
12016
  const conversation = coerceThreadConversationState(currentState);
11671
12017
  const artifacts = coerceThreadArtifactsState(currentState);
11672
- const userMessage = getTurnUserMessage(conversation, stored.resumeSessionId);
11673
- if (!userMessage?.author?.userId) {
11674
- return false;
12018
+ const pendingAuth = getConversationPendingAuth({
12019
+ conversation,
12020
+ kind: "plugin",
12021
+ provider: stored.provider,
12022
+ requesterId: stored.userId
12023
+ });
12024
+ const resolvedSessionId = pendingAuth?.sessionId ?? stored.resumeSessionId;
12025
+ const userMessage = resolvedSessionId ? getTurnUserMessage(conversation, resolvedSessionId) : void 0;
12026
+ if (pendingAuth) {
12027
+ if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
12028
+ clearPendingAuth(conversation, pendingAuth.sessionId);
12029
+ await persistThreadStateById(stored.resumeConversationId, {
12030
+ conversation
12031
+ });
12032
+ await supersedeAgentTurnSessionCheckpoint({
12033
+ conversationId: stored.resumeConversationId,
12034
+ sessionId: pendingAuth.sessionId,
12035
+ errorMessage: "Auth completed after a newer thread message superseded this blocked request."
12036
+ });
12037
+ return true;
12038
+ }
12039
+ } else {
12040
+ if (!userMessage?.author?.userId) {
12041
+ return false;
12042
+ }
12043
+ if (conversation.processing.activeTurnId !== stored.resumeSessionId) {
12044
+ return true;
12045
+ }
11675
12046
  }
11676
- if (conversation.processing.activeTurnId !== stored.resumeSessionId) {
11677
- return true;
12047
+ if (!userMessage?.author?.userId || !resolvedSessionId) {
12048
+ return false;
11678
12049
  }
11679
12050
  const conversationContext = await buildCheckpointConversationContext(
11680
12051
  stored.resumeConversationId,
11681
- stored.resumeSessionId
12052
+ resolvedSessionId
11682
12053
  );
11683
12054
  const channelConfiguration = getChannelConfigurationServiceById(
11684
12055
  stored.channelId
11685
12056
  );
11686
- const providerLabel = formatProviderLabel(stored.provider);
11687
12057
  await resumeSlackTurn({
11688
12058
  messageText: stored.pendingMessage ?? userMessage.text,
11689
12059
  channelId: stored.channelId,
11690
12060
  threadTs: stored.threadTs,
11691
12061
  lockKey: stored.resumeConversationId,
11692
- initialText: `Your ${providerLabel} account is now connected. Processing your request...`,
12062
+ initialText: "",
11693
12063
  failureText: "I connected your account but hit an error processing your request. Please try the command again.",
11694
12064
  replyContext: {
11695
12065
  assistant: { userName: botConfig.userName },
@@ -11705,10 +12075,21 @@ async function resumeCheckpointedOAuthTurn(stored) {
11705
12075
  },
11706
12076
  toolChannelId: artifacts.assistantContextChannelId ?? stored.channelId,
11707
12077
  artifactState: artifacts,
12078
+ pendingAuth,
11708
12079
  conversationContext,
11709
12080
  channelConfiguration,
11710
12081
  sandbox: getPersistedSandboxState(currentState),
11711
12082
  threadParticipants: buildThreadParticipants(conversation.messages),
12083
+ onAuthPending: async (nextPendingAuth) => {
12084
+ await applyPendingAuthUpdate({
12085
+ conversation,
12086
+ conversationId: stored.resumeConversationId,
12087
+ nextPendingAuth
12088
+ });
12089
+ await persistThreadStateById(stored.resumeConversationId, {
12090
+ conversation
12091
+ });
12092
+ },
11712
12093
  ...getTurnUserReplyAttachmentContext(userMessage)
11713
12094
  },
11714
12095
  onSuccess: async (reply) => {
@@ -11720,11 +12101,11 @@ async function resumeCheckpointedOAuthTurn(stored) {
11720
12101
  "app.ai.outcome": reply.diagnostics.outcome,
11721
12102
  "app.ai.tool_calls": reply.diagnostics.toolCalls.length
11722
12103
  },
11723
- "Auto-resumed checkpointed turn after OAuth callback"
12104
+ "OAuth callback auto-resumed checkpoint finished replying"
11724
12105
  );
11725
12106
  await persistCompletedOAuthReplyState({
11726
12107
  conversationId: stored.resumeConversationId,
11727
- sessionId: stored.resumeSessionId,
12108
+ sessionId: resolvedSessionId,
11728
12109
  reply
11729
12110
  });
11730
12111
  },
@@ -11738,17 +12119,19 @@ async function resumeCheckpointedOAuthTurn(stored) {
11738
12119
  );
11739
12120
  await persistFailedOAuthReplyState({
11740
12121
  conversationId: stored.resumeConversationId,
11741
- sessionId: stored.resumeSessionId
12122
+ sessionId: resolvedSessionId
11742
12123
  });
11743
12124
  },
11744
12125
  onAuthPause: async (error) => {
11745
- logException(
12126
+ await deliverAuthPauseReply({
12127
+ channelId: stored.channelId,
12128
+ conversationId: stored.resumeConversationId,
11746
12129
  error,
11747
- "oauth_callback_resume_reparked_for_auth",
11748
- {},
11749
- { "app.credential.provider": stored.provider },
11750
- "Resumed OAuth turn requested another authorization flow"
11751
- );
12130
+ fallbackProvider: stored.provider,
12131
+ sessionId: resolvedSessionId,
12132
+ threadStateId: stored.resumeConversationId,
12133
+ threadTs: stored.threadTs
12134
+ });
11752
12135
  },
11753
12136
  onTimeoutPause: async (error) => {
11754
12137
  if (!isRetryableTurnError(error, "turn_timeout_resume")) {
@@ -11768,7 +12151,7 @@ async function resumeCheckpointedOAuthTurn(stored) {
11768
12151
  }
11769
12152
  await scheduleTurnTimeoutResume({
11770
12153
  conversationId: stored.resumeConversationId,
11771
- sessionId: stored.resumeSessionId,
12154
+ sessionId: resolvedSessionId,
11772
12155
  expectedCheckpointVersion: checkpointVersion
11773
12156
  });
11774
12157
  }
@@ -11777,7 +12160,6 @@ async function resumeCheckpointedOAuthTurn(stored) {
11777
12160
  }
11778
12161
  async function resumePendingOAuthMessage(stored) {
11779
12162
  if (!stored.pendingMessage || !stored.channelId || !stored.threadTs) return;
11780
- const providerLabel = formatProviderLabel(stored.provider);
11781
12163
  const conversationContext = await buildResumeConversationContext2(
11782
12164
  stored.channelId,
11783
12165
  stored.threadTs
@@ -11786,7 +12168,7 @@ async function resumePendingOAuthMessage(stored) {
11786
12168
  messageText: stored.pendingMessage,
11787
12169
  channelId: stored.channelId,
11788
12170
  threadTs: stored.threadTs,
11789
- connectedText: `Your ${providerLabel} account is now connected. Processing your request...`,
12171
+ connectedText: "",
11790
12172
  failureText: `I connected your account but hit an error processing your request. Please try \`${stored.pendingMessage}\` again.`,
11791
12173
  replyContext: {
11792
12174
  requester: { userId: stored.userId },
@@ -11802,7 +12184,7 @@ async function resumePendingOAuthMessage(stored) {
11802
12184
  "app.ai.outcome": reply.diagnostics.outcome,
11803
12185
  "app.ai.tool_calls": reply.diagnostics.toolCalls.length
11804
12186
  },
11805
- "Auto-resumed pending message after OAuth callback"
12187
+ "OAuth callback auto-resumed pending message finished replying"
11806
12188
  );
11807
12189
  },
11808
12190
  onFailure: async (error) => {
@@ -12051,6 +12433,7 @@ async function persistCompletedReplyState2(args) {
12051
12433
  conversation,
12052
12434
  args.checkpoint.sessionId
12053
12435
  );
12436
+ clearPendingAuth(conversation, args.checkpoint.sessionId);
12054
12437
  markConversationMessage(conversation, userMessage?.id, {
12055
12438
  replied: true,
12056
12439
  skippedReason: void 0
@@ -12071,6 +12454,7 @@ async function persistCompletedReplyState2(args) {
12071
12454
  markTurnCompleted({
12072
12455
  conversation,
12073
12456
  nowMs: Date.now(),
12457
+ sessionId: args.checkpoint.sessionId,
12074
12458
  updateConversationStats
12075
12459
  });
12076
12460
  await persistThreadStateById(args.checkpoint.conversationId, {
@@ -12083,9 +12467,11 @@ async function persistCompletedReplyState2(args) {
12083
12467
  async function persistFailedReplyState2(checkpoint) {
12084
12468
  const currentState = await getPersistedThreadState(checkpoint.conversationId);
12085
12469
  const conversation = coerceThreadConversationState(currentState);
12470
+ clearPendingAuth(conversation, checkpoint.sessionId);
12086
12471
  markTurnFailed({
12087
12472
  conversation,
12088
12473
  nowMs: Date.now(),
12474
+ sessionId: checkpoint.sessionId,
12089
12475
  userMessageId: getTurnUserMessage(conversation, checkpoint.sessionId)?.id,
12090
12476
  markConversationMessage,
12091
12477
  updateConversationStats
@@ -12149,10 +12535,21 @@ async function resumeTimedOutTurn(payload) {
12149
12535
  },
12150
12536
  toolChannelId: artifacts.assistantContextChannelId ?? thread.channelId,
12151
12537
  artifactState: artifacts,
12538
+ pendingAuth: conversation.processing.pendingAuth,
12152
12539
  conversationContext,
12153
12540
  channelConfiguration,
12154
12541
  sandbox,
12155
12542
  threadParticipants: buildThreadParticipants(conversation.messages),
12543
+ onAuthPending: async (nextPendingAuth) => {
12544
+ await applyPendingAuthUpdate({
12545
+ conversation,
12546
+ conversationId: payload.conversationId,
12547
+ nextPendingAuth
12548
+ });
12549
+ await persistThreadStateById(payload.conversationId, {
12550
+ conversation
12551
+ });
12552
+ },
12156
12553
  ...getTurnUserReplyAttachmentContext(userMessage)
12157
12554
  },
12158
12555
  onSuccess: async (reply) => {
@@ -12184,7 +12581,15 @@ async function resumeTimedOutTurn(payload) {
12184
12581
  );
12185
12582
  await persistFailedReplyState2(checkpoint);
12186
12583
  },
12187
- onAuthPause: async () => {
12584
+ onAuthPause: async (error) => {
12585
+ await deliverAuthPauseReply({
12586
+ channelId: thread.channelId,
12587
+ conversationId: payload.conversationId,
12588
+ error,
12589
+ sessionId: payload.sessionId,
12590
+ threadStateId: payload.conversationId,
12591
+ threadTs: thread.threadTs
12592
+ });
12188
12593
  logWarn(
12189
12594
  "timeout_resume_reparked_for_auth",
12190
12595
  {},
@@ -13923,6 +14328,7 @@ function createReplyToThread(deps) {
13923
14328
  },
13924
14329
  conversationContext: preparedState.routingContext ?? preparedState.conversationContext,
13925
14330
  artifactState: preparedState.artifacts,
14331
+ pendingAuth: preparedState.conversation.processing.pendingAuth,
13926
14332
  configuration: preparedState.configuration,
13927
14333
  channelConfiguration: preparedState.channelConfiguration,
13928
14334
  inboundAttachmentCount: message.attachments.length,
@@ -13952,6 +14358,16 @@ function createReplyToThread(deps) {
13952
14358
  onArtifactStateUpdated: async (artifacts) => {
13953
14359
  await persistThreadState(thread, { artifacts });
13954
14360
  },
14361
+ onAuthPending: async (pendingAuth) => {
14362
+ await applyPendingAuthUpdate({
14363
+ conversation: preparedState.conversation,
14364
+ conversationId,
14365
+ nextPendingAuth: pendingAuth
14366
+ });
14367
+ await persistThreadState(thread, {
14368
+ conversation: preparedState.conversation
14369
+ });
14370
+ },
13955
14371
  threadParticipants,
13956
14372
  onStatus: (nextStatus) => status.update(nextStatus)
13957
14373
  });
@@ -14129,8 +14545,42 @@ function createReplyToThread(deps) {
14129
14545
  }
14130
14546
  } catch (error) {
14131
14547
  if (isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) {
14548
+ const authPauseText = buildAuthPauseReplyText({
14549
+ disposition: error.metadata?.authDisposition,
14550
+ provider: error.metadata?.authProvider
14551
+ });
14552
+ const authPauseFooter = buildSlackReplyFooter({
14553
+ conversationId,
14554
+ durationMs: error.metadata?.authDurationMs,
14555
+ thinkingLevel: error.metadata?.authThinkingLevel,
14556
+ usage: error.metadata?.authUsage
14557
+ });
14558
+ const useSlackFooterForAuthPause = Boolean(authPauseFooter) && Boolean(channelId && threadTs) && thread.adapter?.name === "slack";
14559
+ if (useSlackFooterForAuthPause && channelId && threadTs) {
14560
+ await beforeFirstResponsePost();
14561
+ await postSlackApiReplyPosts({
14562
+ channelId,
14563
+ threadTs,
14564
+ footer: authPauseFooter,
14565
+ posts: [{ stage: "thread_reply", text: authPauseText }]
14566
+ });
14567
+ } else {
14568
+ await postThreadReply(
14569
+ buildSlackOutputMessage(authPauseText),
14570
+ "thread_reply"
14571
+ );
14572
+ }
14573
+ completeAuthPauseTurn({
14574
+ conversation: preparedState.conversation,
14575
+ sessionId: error.metadata?.sessionId ?? turnId,
14576
+ text: authPauseText
14577
+ });
14578
+ await persistThreadState(thread, {
14579
+ conversation: preparedState.conversation
14580
+ });
14581
+ persistedAtLeastOnce = true;
14132
14582
  shouldPersistFailureState = false;
14133
- throw error;
14583
+ return;
14134
14584
  }
14135
14585
  if (isRetryableTurnError(error, "turn_timeout_resume")) {
14136
14586
  const conversationIdForResume = error.metadata?.conversationId;