@sentry/junior 0.44.0 → 0.46.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
@@ -31,7 +31,7 @@ import {
31
31
  runNonInteractiveCommand,
32
32
  sandboxSkillDir,
33
33
  sandboxSkillFile
34
- } from "./chunk-QAMTCT2R.js";
34
+ } from "./chunk-ELM6HJ6S.js";
35
35
  import {
36
36
  CredentialUnavailableError,
37
37
  buildOAuthTokenRequest,
@@ -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;
@@ -2088,6 +2119,10 @@ function markTurnFailed(args) {
2088
2119
  }
2089
2120
 
2090
2121
  // src/chat/runtime/turn-user-message.ts
2122
+ function normalizeSlackMessageTs(value) {
2123
+ const trimmed = value?.trim();
2124
+ return trimmed && /^\d+(?:\.\d+)?$/.test(trimmed) ? trimmed : void 0;
2125
+ }
2091
2126
  function getTurnUserMessage(conversation, sessionId) {
2092
2127
  for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
2093
2128
  const message = conversation.messages[index];
@@ -2103,6 +2138,9 @@ function getTurnUserMessage(conversation, sessionId) {
2103
2138
  function getTurnUserMessageId(conversation, sessionId) {
2104
2139
  return getTurnUserMessage(conversation, sessionId)?.id;
2105
2140
  }
2141
+ function getTurnUserSlackMessageTs(message) {
2142
+ return normalizeSlackMessageTs(message?.meta?.slackTs) ?? normalizeSlackMessageTs(message?.id);
2143
+ }
2106
2144
  function getTurnUserReplyAttachmentContext(message) {
2107
2145
  const inboundAttachmentCount = message?.meta?.attachmentCount ?? 0;
2108
2146
  const imageAttachmentCount = message?.meta?.imageAttachmentCount ?? 0;
@@ -8600,7 +8638,6 @@ function resolveChannelCapabilities(channelId) {
8600
8638
  import fs4 from "fs/promises";
8601
8639
 
8602
8640
  // src/chat/sandbox/egress-policy.ts
8603
- var SANDBOX_EGRESS_PROXY_PATH = "/api/internal/sandbox-egress";
8604
8641
  function matchesSandboxEgressDomain(host, domain) {
8605
8642
  return host.toLowerCase() === domain.toLowerCase();
8606
8643
  }
@@ -8622,31 +8659,24 @@ function resolveSandboxEgressProviderForHost(host) {
8622
8659
  (entry) => entry.domains.some((domain) => matchesSandboxEgressDomain(host, domain))
8623
8660
  )?.provider;
8624
8661
  }
8625
- function proxyUrl(egressId) {
8662
+ function sandboxProxyUrl() {
8626
8663
  const baseUrl = resolveBaseUrl();
8627
8664
  if (!baseUrl) {
8628
- return void 0;
8629
- }
8630
- const url = new URL(
8631
- `${SANDBOX_EGRESS_PROXY_PATH}/${encodeURIComponent(egressId)}`,
8632
- baseUrl
8633
- );
8634
- return url.toString();
8635
- }
8636
- function buildSandboxEgressNetworkPolicy(egressId) {
8637
- const entries = providerEntries();
8638
- if (entries.length === 0) {
8639
- return void 0;
8640
- }
8641
- const forwardURL = proxyUrl(egressId);
8642
- if (!forwardURL) {
8643
8665
  throw new Error(
8644
8666
  "Cannot determine base URL for sandbox credential egress (set JUNIOR_BASE_URL or deploy to Vercel)"
8645
8667
  );
8646
8668
  }
8669
+ return new URL("/", baseUrl).toString();
8670
+ }
8671
+ function buildSandboxEgressNetworkPolicy() {
8647
8672
  const allow = {
8648
8673
  "*": []
8649
8674
  };
8675
+ const entries = providerEntries();
8676
+ if (entries.length === 0) {
8677
+ return { allow };
8678
+ }
8679
+ const forwardURL = sandboxProxyUrl();
8650
8680
  for (const entry of entries) {
8651
8681
  for (const domain of entry.domains) {
8652
8682
  allow[domain] = [{ forwardURL }];
@@ -8654,11 +8684,14 @@ function buildSandboxEgressNetworkPolicy(egressId) {
8654
8684
  }
8655
8685
  return { allow };
8656
8686
  }
8657
- async function resolveSandboxCommandEnvironment() {
8687
+ async function resolveSandboxCommandEnvironment(provider) {
8658
8688
  const env = {};
8659
8689
  for (const plugin of getPluginProviders().sort(
8660
8690
  (left, right) => left.manifest.name.localeCompare(right.manifest.name)
8661
8691
  )) {
8692
+ if (provider && plugin.manifest.name !== provider) {
8693
+ continue;
8694
+ }
8662
8695
  Object.assign(env, resolvePluginCommandEnv(plugin.manifest));
8663
8696
  const credentials = plugin.manifest.credentials;
8664
8697
  if (credentials) {
@@ -9975,7 +10008,6 @@ function createSandboxSessionManager(options) {
9975
10008
  return {
9976
10009
  bash: async (input) => {
9977
10010
  const commandEgressId = sandboxInstance.sandboxEgressId;
9978
- await options?.beforeCommand?.(commandEgressId);
9979
10011
  let timedOut = false;
9980
10012
  let timeoutId;
9981
10013
  let commandFinished = false;
@@ -9985,6 +10017,7 @@ function createSandboxSessionManager(options) {
9985
10017
  }
9986
10018
  commandFinished = true;
9987
10019
  await options?.afterCommand?.(commandEgressId);
10020
+ await refreshNetworkPolicy(sandboxInstance);
9988
10021
  };
9989
10022
  const finishCommandBestEffort = async () => {
9990
10023
  try {
@@ -10002,6 +10035,8 @@ function createSandboxSessionManager(options) {
10002
10035
  }
10003
10036
  };
10004
10037
  try {
10038
+ await options?.beforeCommand?.(commandEgressId);
10039
+ await refreshNetworkPolicy(sandboxInstance);
10005
10040
  const sandboxCommandEnv = await resolveCommandEnv();
10006
10041
  const script = buildNonInteractiveShellScript(input.command, {
10007
10042
  env: { ...sandboxCommandEnv, ...input.env ?? {} },
@@ -10125,14 +10160,14 @@ function createSandboxExecutor(options) {
10125
10160
  let referenceFiles = [];
10126
10161
  const traceContext = options?.traceContext ?? {};
10127
10162
  const credentialEgress = options?.credentialEgress;
10128
- const syncSandboxEgressSession = credentialEgress ? async (egressId) => {
10163
+ const authorizeSandboxEgressForCommand = credentialEgress ? async (egressId) => {
10129
10164
  await upsertSandboxEgressSession({
10130
10165
  egressId,
10131
10166
  requesterId: credentialEgress.requesterId,
10132
10167
  ttlMs: options?.timeoutMs
10133
10168
  });
10134
10169
  } : void 0;
10135
- const clearSandboxEgressSessionForCommand = credentialEgress ? async (egressId) => {
10170
+ const clearSandboxEgressForCommand = credentialEgress ? async (egressId) => {
10136
10171
  await clearSandboxEgressSession(egressId);
10137
10172
  } : void 0;
10138
10173
  const sessionManager = createSandboxSessionManager({
@@ -10140,10 +10175,13 @@ function createSandboxExecutor(options) {
10140
10175
  sandboxDependencyProfileHash: options?.sandboxDependencyProfileHash,
10141
10176
  timeoutMs: options?.timeoutMs,
10142
10177
  traceContext,
10143
- commandEnv: credentialEgress ? async () => await resolveSandboxCommandEnvironment() : void 0,
10178
+ commandEnv: credentialEgress ? async () => {
10179
+ const provider = credentialEgress.activeProvider?.();
10180
+ return provider ? await resolveSandboxCommandEnvironment(provider) : {};
10181
+ } : void 0,
10144
10182
  createNetworkPolicy: credentialEgress ? buildSandboxEgressNetworkPolicy : void 0,
10145
- beforeCommand: syncSandboxEgressSession,
10146
- afterCommand: clearSandboxEgressSessionForCommand,
10183
+ beforeCommand: authorizeSandboxEgressForCommand,
10184
+ afterCommand: clearSandboxEgressForCommand,
10147
10185
  onSandboxAcquired: async (sandbox) => {
10148
10186
  await options?.onSandboxAcquired?.(sandbox);
10149
10187
  }
@@ -10620,70 +10658,461 @@ function normalizeToolResult(result, isSandboxResult) {
10620
10658
  };
10621
10659
  }
10622
10660
 
10623
- // src/chat/tools/execution/tool-error-handler.ts
10624
- function getToolErrorAttributes(error) {
10625
- if (!(error instanceof SlackActionError)) {
10626
- return {};
10661
+ // src/chat/credentials/unlink-provider.ts
10662
+ async function unlinkProvider(userId, provider, userTokenStore) {
10663
+ await Promise.all([
10664
+ userTokenStore.delete(userId, provider),
10665
+ deleteMcpStoredOAuthCredentials(userId, provider),
10666
+ deleteMcpServerSessionId(userId, provider),
10667
+ deleteMcpAuthSessionsForUserProvider(userId, provider)
10668
+ ]);
10669
+ }
10670
+
10671
+ // src/chat/state/turn-session-store.ts
10672
+ var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
10673
+ var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
10674
+ function agentTurnSessionKey(conversationId, sessionId) {
10675
+ return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
10676
+ }
10677
+ function parseAgentTurnSessionCheckpoint(value) {
10678
+ if (typeof value !== "string") {
10679
+ return void 0;
10627
10680
  }
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 } : {}
10681
+ try {
10682
+ const parsed = JSON.parse(value);
10683
+ if (!isRecord(parsed)) {
10684
+ return void 0;
10685
+ }
10686
+ const status = parsed.state;
10687
+ if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed" && status !== "superseded") {
10688
+ return void 0;
10689
+ }
10690
+ const conversationId = parsed.conversationId;
10691
+ const sessionId = parsed.sessionId;
10692
+ const sliceId = parsed.sliceId;
10693
+ const checkpointVersion = parsed.checkpointVersion;
10694
+ const updatedAtMs = parsed.updatedAtMs;
10695
+ if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
10696
+ return void 0;
10697
+ }
10698
+ return {
10699
+ checkpointVersion,
10700
+ conversationId,
10701
+ sessionId,
10702
+ sliceId,
10703
+ state: status,
10704
+ updatedAtMs,
10705
+ piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
10706
+ ...Array.isArray(parsed.loadedSkillNames) ? {
10707
+ loadedSkillNames: parsed.loadedSkillNames.filter(
10708
+ (value2) => typeof value2 === "string"
10709
+ )
10710
+ } : {},
10711
+ ...parsed.resumeReason === "timeout" || parsed.resumeReason === "auth" ? { resumeReason: parsed.resumeReason } : {},
10712
+ ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
10713
+ ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
10714
+ };
10715
+ } catch {
10716
+ return void 0;
10717
+ }
10718
+ }
10719
+ async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
10720
+ const stateAdapter = getStateAdapter();
10721
+ await stateAdapter.connect();
10722
+ const value = await stateAdapter.get(
10723
+ agentTurnSessionKey(conversationId, sessionId)
10724
+ );
10725
+ return parseAgentTurnSessionCheckpoint(value);
10726
+ }
10727
+ async function upsertAgentTurnSessionCheckpoint(args) {
10728
+ const stateAdapter = getStateAdapter();
10729
+ await stateAdapter.connect();
10730
+ const existing = await getAgentTurnSessionCheckpoint(
10731
+ args.conversationId,
10732
+ args.sessionId
10733
+ );
10734
+ const checkpoint = {
10735
+ checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
10736
+ conversationId: args.conversationId,
10737
+ sessionId: args.sessionId,
10738
+ sliceId: args.sliceId,
10739
+ state: args.state,
10740
+ updatedAtMs: Date.now(),
10741
+ piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
10742
+ ...Array.isArray(args.loadedSkillNames) ? {
10743
+ loadedSkillNames: args.loadedSkillNames.filter(
10744
+ (value) => typeof value === "string"
10745
+ )
10746
+ } : {},
10747
+ ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
10748
+ ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
10749
+ ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
10634
10750
  };
10751
+ const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
10752
+ await stateAdapter.set(
10753
+ agentTurnSessionKey(args.conversationId, args.sessionId),
10754
+ JSON.stringify(checkpoint),
10755
+ ttlMs
10756
+ );
10757
+ return checkpoint;
10635
10758
  }
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
10759
+ async function supersedeAgentTurnSessionCheckpoint(args) {
10760
+ const existing = await getAgentTurnSessionCheckpoint(
10761
+ args.conversationId,
10762
+ args.sessionId
10763
+ );
10764
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "superseded") {
10765
+ return void 0;
10766
+ }
10767
+ return await upsertAgentTurnSessionCheckpoint({
10768
+ conversationId: existing.conversationId,
10769
+ sessionId: existing.sessionId,
10770
+ sliceId: existing.sliceId,
10771
+ state: "superseded",
10772
+ piMessages: existing.piMessages,
10773
+ loadedSkillNames: existing.loadedSkillNames,
10774
+ resumeReason: existing.resumeReason,
10775
+ resumedFromSliceId: existing.resumedFromSliceId,
10776
+ errorMessage: args.errorMessage ?? existing.errorMessage
10641
10777
  });
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
- );
10778
+ }
10779
+
10780
+ // src/chat/services/pending-auth.ts
10781
+ var AUTH_LINK_REUSE_WINDOW_MS = 10 * 60 * 1e3;
10782
+ function canReusePendingAuthLink(args) {
10783
+ const { pendingAuth } = args;
10784
+ if (!pendingAuth) {
10785
+ return false;
10656
10786
  }
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
- );
10787
+ return pendingAuth.kind === args.kind && pendingAuth.provider === args.provider && pendingAuth.requesterId === args.requesterId && pendingAuth.linkSentAtMs + AUTH_LINK_REUSE_WINDOW_MS > (args.nowMs ?? Date.now());
10788
+ }
10789
+ function getConversationPendingAuth(args) {
10790
+ const pendingAuth = args.conversation.processing.pendingAuth;
10791
+ if (!pendingAuth) {
10792
+ return void 0;
10671
10793
  }
10672
- throw error;
10794
+ if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId) {
10795
+ return void 0;
10796
+ }
10797
+ return pendingAuth;
10798
+ }
10799
+ function clearPendingAuth(conversation, sessionId) {
10800
+ if (!conversation.processing.pendingAuth) {
10801
+ return;
10802
+ }
10803
+ if (sessionId && conversation.processing.pendingAuth.sessionId !== sessionId) {
10804
+ return;
10805
+ }
10806
+ conversation.processing.pendingAuth = void 0;
10807
+ }
10808
+ async function applyPendingAuthUpdate(args) {
10809
+ const previousPendingAuth = args.conversation.processing.pendingAuth;
10810
+ args.conversation.processing.pendingAuth = args.nextPendingAuth;
10811
+ if (previousPendingAuth && previousPendingAuth.sessionId !== args.nextPendingAuth.sessionId && args.conversationId) {
10812
+ await supersedeAgentTurnSessionCheckpoint({
10813
+ conversationId: args.conversationId,
10814
+ sessionId: previousPendingAuth.sessionId,
10815
+ errorMessage: "Superseded by a newer auth-blocked request in the same conversation."
10816
+ });
10817
+ }
10818
+ }
10819
+ function isPendingAuthLatestRequest(conversation, pendingAuth) {
10820
+ for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
10821
+ const message = conversation.messages[index];
10822
+ if (message?.role !== "user") {
10823
+ continue;
10824
+ }
10825
+ return buildDeterministicTurnId(message.id) === pendingAuth.sessionId;
10826
+ }
10827
+ return false;
10673
10828
  }
10674
10829
 
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;
10830
+ // src/chat/services/plugin-auth-orchestration.ts
10831
+ var PluginAuthorizationPauseError = class extends AuthorizationPauseError {
10832
+ constructor(provider, disposition) {
10833
+ super("plugin", provider, disposition);
10834
+ }
10835
+ };
10836
+ var PluginCredentialFailureError = class extends Error {
10837
+ provider;
10838
+ constructor(provider, message) {
10839
+ super(message);
10840
+ this.name = "PluginCredentialFailureError";
10841
+ this.provider = provider;
10842
+ }
10843
+ };
10844
+ function isCommandAuthFailure(details) {
10845
+ if (!details || typeof details !== "object") {
10846
+ return false;
10847
+ }
10848
+ const result = details;
10849
+ if (typeof result.exit_code !== "number" || result.exit_code === 0) {
10850
+ return false;
10851
+ }
10852
+ const text = `${typeof result.stdout === "string" ? result.stdout : ""}
10853
+ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
10854
+ if (!text.trim()) {
10855
+ return false;
10856
+ }
10857
+ return [
10858
+ /\bjunior-auth-required\b/,
10859
+ /\b401\b/,
10860
+ /\bunauthorized\b/,
10861
+ /\bbad credentials\b/,
10862
+ /\binvalid token\b/,
10863
+ /\bgithub_token\b.*\binvalid\b/,
10864
+ /\btoken (?:expired|revoked)\b/,
10865
+ /\bexpired token\b/,
10866
+ /\bmissing scopes?\b/,
10867
+ /\binsufficient scope\b/,
10868
+ /\binvalid grant\b/,
10869
+ /\breauthoriz/
10870
+ ].some((pattern) => pattern.test(text));
10871
+ }
10872
+ function commandText(details) {
10873
+ if (!details || typeof details !== "object") {
10874
+ return "";
10875
+ }
10876
+ const result = details;
10877
+ return `${typeof result.stdout === "string" ? result.stdout : ""}
10878
+ ${typeof result.stderr === "string" ? result.stderr : ""}`;
10879
+ }
10880
+ function isGitHubSmartHttpAuthFailure(provider, command, details) {
10881
+ if (provider !== "github" || !/^\s*(?:gh|git)\b/i.test(command)) {
10882
+ return false;
10883
+ }
10884
+ const text = commandText(details).toLowerCase();
10885
+ return /\bgzip:\s*invalid header\b/.test(text);
10886
+ }
10887
+ function explicitAuthRequiredProvider(details) {
10888
+ const match = /\bjunior-auth-required\s+provider=([a-z0-9-]+)\b/.exec(
10889
+ commandText(details).toLowerCase()
10890
+ );
10891
+ return match?.[1];
10892
+ }
10893
+ function registeredProviderNames() {
10894
+ const providers = /* @__PURE__ */ new Set();
10895
+ for (const plugin of getPluginProviders()) {
10896
+ const domains = [
10897
+ ...plugin.manifest.credentials?.domains ?? [],
10898
+ ...plugin.manifest.domains ?? []
10899
+ ];
10900
+ if (domains.length > 0) {
10901
+ providers.add(plugin.manifest.name);
10902
+ }
10903
+ }
10904
+ return [...providers].sort((left, right) => left.localeCompare(right));
10905
+ }
10906
+ function commandTargetsProvider(provider, command, details) {
10907
+ const normalizedCommand = command.trim().toLowerCase();
10908
+ if (!normalizedCommand) {
10909
+ return false;
10910
+ }
10911
+ if (provider === "github" && /^(gh|git)\b/.test(normalizedCommand)) {
10912
+ return true;
10913
+ }
10914
+ const plugin = getPluginDefinition(provider);
10915
+ const candidates = /* @__PURE__ */ new Set([provider.toLowerCase()]);
10916
+ const manifest = plugin?.manifest;
10917
+ const credentials = manifest?.credentials;
10918
+ if (credentials) {
10919
+ candidates.add(credentials.authTokenEnv.toLowerCase());
10920
+ for (const domain of credentials.domains) {
10921
+ candidates.add(domain.toLowerCase());
10922
+ }
10923
+ }
10924
+ for (const domain of manifest?.domains ?? []) {
10925
+ candidates.add(domain.toLowerCase());
10926
+ }
10927
+ const combinedText = `${normalizedCommand}
10928
+ ${commandText(details).toLowerCase()}`;
10929
+ return [...candidates].some((candidate) => combinedText.includes(candidate));
10930
+ }
10931
+ function formatCommand(command) {
10932
+ const collapsed = command.replace(/\s+/g, " ").trim();
10933
+ return collapsed.length > 160 ? `${collapsed.slice(0, 157)}...` : collapsed;
10934
+ }
10935
+ function buildCredentialFailureError(provider, command) {
10936
+ const providerLabel = provider === "github" ? "GitHub" : formatProviderLabel(provider);
10937
+ const plugin = getPluginDefinition(provider);
10938
+ const credentialType = plugin?.manifest.credentials?.type;
10939
+ const commandSummary = formatCommand(command);
10940
+ 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.`;
10941
+ return new PluginCredentialFailureError(
10942
+ provider,
10943
+ `${providerLabel} credentials were rejected while running \`${commandSummary}\`. ${remediation}`
10944
+ );
10945
+ }
10946
+ function createPluginAuthOrchestration(deps, abortAgent) {
10947
+ let pendingPause;
10948
+ const startAuthorizationPause = async (provider, activeSkill, options) => {
10949
+ if (pendingPause) {
10950
+ throw pendingPause;
10951
+ }
10952
+ if (!deps.requesterId || !getPluginOAuthConfig(provider)) {
10953
+ throw new Error(`Cannot start plugin authorization for ${provider}`);
10954
+ }
10955
+ const providerLabel = formatProviderLabel(provider);
10956
+ const reusingPendingLink = canReusePendingAuthLink({
10957
+ pendingAuth: deps.currentPendingAuth,
10958
+ kind: "plugin",
10959
+ provider,
10960
+ requesterId: deps.requesterId
10961
+ });
10962
+ if (!reusingPendingLink) {
10963
+ const oauthResult = await startOAuthFlow(provider, {
10964
+ requesterId: deps.requesterId,
10965
+ channelId: deps.channelId,
10966
+ threadTs: deps.threadTs,
10967
+ userMessage: deps.userMessage,
10968
+ channelConfiguration: deps.channelConfiguration,
10969
+ activeSkillName: activeSkill?.name ?? void 0,
10970
+ resumeConversationId: deps.conversationId,
10971
+ resumeSessionId: deps.sessionId
10972
+ });
10973
+ if (!oauthResult.ok) {
10974
+ throw new Error(oauthResult.error);
10975
+ }
10976
+ if (!oauthResult.delivery) {
10977
+ throw new Error(
10978
+ `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.`
10979
+ );
10980
+ }
10981
+ }
10982
+ if (options?.unlinkExistingProvider && deps.requesterId && deps.userTokenStore) {
10983
+ await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
10984
+ }
10985
+ if (deps.sessionId) {
10986
+ await deps.onPendingAuth?.({
10987
+ kind: "plugin",
10988
+ provider,
10989
+ requesterId: deps.requesterId,
10990
+ sessionId: deps.sessionId,
10991
+ linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
10992
+ });
10993
+ }
10994
+ pendingPause = new PluginAuthorizationPauseError(
10995
+ provider,
10996
+ reusingPendingLink ? "link_already_sent" : "link_sent"
10997
+ );
10998
+ abortAgent();
10999
+ throw pendingPause;
11000
+ };
11001
+ return {
11002
+ handleCommandFailure: async (input) => {
11003
+ const providers = registeredProviderNames();
11004
+ const explicitProvider = explicitAuthRequiredProvider(input.details);
11005
+ const provider = explicitProvider && providers.includes(explicitProvider) ? explicitProvider : providers.find(
11006
+ (availableProvider) => commandTargetsProvider(
11007
+ availableProvider,
11008
+ input.command,
11009
+ input.details
11010
+ )
11011
+ );
11012
+ if (!provider) {
11013
+ return;
11014
+ }
11015
+ const authFailure = isCommandAuthFailure(input.details) || isGitHubSmartHttpAuthFailure(provider, input.command, input.details);
11016
+ if (!authFailure) {
11017
+ return;
11018
+ }
11019
+ if (!deps.requesterId || !deps.userTokenStore) {
11020
+ throw buildCredentialFailureError(provider, input.command);
11021
+ }
11022
+ if (!getPluginOAuthConfig(provider)) {
11023
+ throw buildCredentialFailureError(provider, input.command);
11024
+ }
11025
+ await startAuthorizationPause(provider, input.activeSkill, {
11026
+ unlinkExistingProvider: true
11027
+ });
11028
+ },
11029
+ getPendingPause: () => pendingPause
11030
+ };
11031
+ }
11032
+
11033
+ // src/chat/tools/execution/tool-error-handler.ts
11034
+ function getToolErrorAttributes(error) {
11035
+ if (!(error instanceof SlackActionError)) {
11036
+ return {};
11037
+ }
11038
+ return {
11039
+ "app.slack.error_code": error.code,
11040
+ ...error.apiError ? { "app.slack.api_error": error.apiError } : {},
11041
+ ...error.detail ? { "app.slack.detail": error.detail } : {},
11042
+ ...error.detailLine !== void 0 ? { "app.slack.detail_line": error.detailLine } : {},
11043
+ ...error.detailRule ? { "app.slack.detail_rule": error.detailRule } : {}
11044
+ };
11045
+ }
11046
+ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, traceContext) {
11047
+ const errorType = getMcpAwareErrorType(error, "tool_execution_error");
11048
+ const errorMessage = getMcpAwareErrorMessage(error);
11049
+ setSpanAttributes({
11050
+ "error.type": errorType,
11051
+ ...error instanceof PluginCredentialFailureError ? { "app.credential.provider": error.provider } : {}
11052
+ });
11053
+ if (error instanceof PluginCredentialFailureError) {
11054
+ if (shouldTrace) {
11055
+ logInfo(
11056
+ "plugin_credential_rejected",
11057
+ traceContext,
11058
+ {
11059
+ "app.credential.provider": error.provider,
11060
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
11061
+ "gen_ai.operation.name": "execute_tool",
11062
+ "gen_ai.tool.name": toolName,
11063
+ ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
11064
+ "error.type": errorType
11065
+ },
11066
+ "Plugin credentials were rejected during tool execution"
11067
+ );
11068
+ }
11069
+ throw error;
11070
+ }
11071
+ if (shouldTrace) {
11072
+ logWarn(
11073
+ "agent_tool_call_failed",
11074
+ traceContext,
11075
+ {
11076
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
11077
+ "gen_ai.operation.name": "execute_tool",
11078
+ "gen_ai.tool.name": toolName,
11079
+ ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
11080
+ "error.type": errorType,
11081
+ "exception.message": errorMessage
11082
+ },
11083
+ "Agent tool call failed"
11084
+ );
11085
+ }
11086
+ if (!(error instanceof McpToolError)) {
11087
+ logException(
11088
+ error,
11089
+ "agent_tool_call_failed",
11090
+ {},
11091
+ {
11092
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
11093
+ "gen_ai.operation.name": "execute_tool",
11094
+ "gen_ai.tool.name": toolName,
11095
+ ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
11096
+ ...getToolErrorAttributes(error)
11097
+ },
11098
+ "Agent tool call failed"
11099
+ );
11100
+ }
11101
+ throw error;
11102
+ }
11103
+
11104
+ // src/chat/tools/agent-tools.ts
11105
+ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, pluginAuthOrchestration, onToolCall) {
11106
+ const shouldTrace = shouldEmitDevAgentTrace();
11107
+ return Object.entries(tools).map(([toolName, toolDef]) => ({
11108
+ name: toolName,
11109
+ label: toolName,
11110
+ description: toolDef.description,
11111
+ parameters: toolDef.inputSchema,
11112
+ prepareArguments: toolDef.prepareArguments,
11113
+ executionMode: toolDef.executionMode,
11114
+ execute: async (toolCallId, params) => {
11115
+ const normalizedToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : void 0;
10687
11116
  const toolArgumentsAttribute = serializeGenAiAttribute(params);
10688
11117
  if (toolName === "reportProgress") {
10689
11118
  const status = buildReportedProgressStatus(params);
@@ -10997,9 +11426,31 @@ var TURN_THINKING_LEVELS = [
10997
11426
  "high",
10998
11427
  "xhigh"
10999
11428
  ];
11000
- var turnExecutionProfileSchema = z.object({
11001
- thinking_level: z.enum(TURN_THINKING_LEVELS),
11002
- confidence: z.number().min(0).max(1),
11429
+ var CONFIDENCE_LABELS = {
11430
+ low: 0.5,
11431
+ medium: CLASSIFIER_CONFIDENCE_THRESHOLD,
11432
+ high: 0.9
11433
+ };
11434
+ function coerceClassifierConfidence(value) {
11435
+ if (typeof value !== "string") {
11436
+ return value;
11437
+ }
11438
+ const trimmed = value.trim().toLowerCase();
11439
+ if (!trimmed) {
11440
+ return value;
11441
+ }
11442
+ const numeric = Number.parseFloat(trimmed);
11443
+ if (Number.isFinite(numeric)) {
11444
+ return numeric;
11445
+ }
11446
+ return CONFIDENCE_LABELS[trimmed] ?? value;
11447
+ }
11448
+ var turnExecutionProfileSchema = z.object({
11449
+ thinking_level: z.enum(TURN_THINKING_LEVELS),
11450
+ confidence: z.preprocess(
11451
+ coerceClassifierConfidence,
11452
+ z.number().min(0).max(1)
11453
+ ),
11003
11454
  reason: z.string().min(1)
11004
11455
  });
11005
11456
  var DEFAULT_THINKING_LEVEL = "medium";
@@ -11044,7 +11495,8 @@ function buildClassifierSystemPrompt() {
11044
11495
  "",
11045
11496
  "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
11497
  "",
11047
- "Return JSON only with thinking_level, confidence, and reason."
11498
+ "Return JSON only with thinking_level, confidence, and reason.",
11499
+ "confidence must be a number from 0 to 1, not a word label."
11048
11500
  ].join("\n");
11049
11501
  }
11050
11502
  function buildClassifierPrompt(args) {
@@ -11186,115 +11638,6 @@ function toAgentThinkingLevel(level) {
11186
11638
  }
11187
11639
  }
11188
11640
 
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;
11284
- }
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
- }
11297
-
11298
11641
  // src/chat/services/turn-checkpoint.ts
11299
11642
  async function loadTurnCheckpoint(ctx) {
11300
11643
  const canUseTurnSession = Boolean(ctx.conversationId && ctx.sessionId);
@@ -11408,56 +11751,6 @@ async function persistTimeoutCheckpoint(args) {
11408
11751
  }
11409
11752
  }
11410
11753
 
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
11754
  // src/chat/services/mcp-auth-orchestration.ts
11462
11755
  var McpAuthorizationPauseError = class extends AuthorizationPauseError {
11463
11756
  constructor(provider, disposition) {
@@ -11492,236 +11785,60 @@ function createMcpAuthOrchestration(deps, abortAgent) {
11492
11785
  }
11493
11786
  const authSessionId = authSessionIdsByProvider.get(provider);
11494
11787
  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());
11643
- }
11644
- const combinedText = `${normalizedCommand}
11645
- ${commandText(details).toLowerCase()}`;
11646
- return [...candidates].some((candidate) => combinedText.includes(candidate));
11647
- }
11648
- function createPluginAuthOrchestration(deps, abortAgent) {
11649
- let pendingPause;
11650
- const startAuthorizationPause = async (provider, activeSkill, options) => {
11651
- if (pendingPause) {
11652
- throw pendingPause;
11788
+ throw new Error(
11789
+ `Missing MCP auth session context for plugin "${provider}"`
11790
+ );
11653
11791
  }
11654
- if (!deps.requesterId || !getPluginOAuthConfig(provider)) {
11655
- throw new Error(`Cannot start plugin authorization for ${provider}`);
11792
+ const latestArtifactState = deps.getMergedArtifactState();
11793
+ await patchMcpAuthSession(authSessionId, {
11794
+ configuration: { ...deps.getConfiguration() },
11795
+ artifactState: latestArtifactState,
11796
+ toolChannelId: deps.toolChannelId ?? latestArtifactState.assistantContextChannelId ?? deps.channelId
11797
+ });
11798
+ const authSession = await getMcpAuthSession(authSessionId);
11799
+ if (!authSession?.authorizationUrl) {
11800
+ throw new Error(`Missing MCP authorization URL for plugin "${provider}"`);
11656
11801
  }
11657
- const providerLabel = formatProviderLabel(provider);
11658
11802
  const reusingPendingLink = canReusePendingAuthLink({
11659
11803
  pendingAuth: deps.currentPendingAuth,
11660
- kind: "plugin",
11804
+ kind: "mcp",
11661
11805
  provider,
11662
11806
  requesterId: deps.requesterId
11663
11807
  });
11664
11808
  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
11809
+ const delivery = await deliverPrivateMessage({
11810
+ channelId: authSession.channelId,
11811
+ threadTs: authSession.threadTs,
11812
+ userId: authSession.userId,
11813
+ text: `<${authSession.authorizationUrl}|Click here to link your ${formatProviderLabel(provider)} MCP access>. Once you've authorized, this thread will continue automatically.`
11674
11814
  });
11675
- if (!oauthResult.ok) {
11676
- throw new Error(oauthResult.error);
11677
- }
11678
- if (!oauthResult.delivery) {
11815
+ if (!delivery) {
11679
11816
  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.`
11817
+ `Unable to deliver MCP authorization link for plugin "${provider}"`
11681
11818
  );
11682
11819
  }
11820
+ } else {
11821
+ await deleteMcpAuthSession(authSessionId);
11683
11822
  }
11684
- if (options?.unlinkExistingProvider && deps.requesterId && deps.userTokenStore) {
11685
- await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
11686
- }
11687
- if (deps.sessionId) {
11823
+ if (deps.sessionId && deps.requesterId) {
11688
11824
  await deps.onPendingAuth?.({
11689
- kind: "plugin",
11825
+ kind: "mcp",
11690
11826
  provider,
11691
11827
  requesterId: deps.requesterId,
11692
11828
  sessionId: deps.sessionId,
11693
11829
  linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
11694
11830
  });
11695
11831
  }
11696
- pendingPause = new PluginAuthorizationPauseError(
11832
+ pendingPause = new McpAuthorizationPauseError(
11697
11833
  provider,
11698
11834
  reusingPendingLink ? "link_already_sent" : "link_sent"
11699
11835
  );
11700
11836
  abortAgent();
11701
- throw pendingPause;
11837
+ return true;
11702
11838
  };
11703
11839
  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
- },
11840
+ authProviderFactory,
11841
+ onAuthorizationRequired,
11725
11842
  getPendingPause: () => pendingPause
11726
11843
  };
11727
11844
  }
@@ -11988,7 +12105,8 @@ async function generateAssistantReply(messageText, context = {}) {
11988
12105
  sandboxDependencyProfileHash: context.sandbox?.sandboxDependencyProfileHash,
11989
12106
  traceContext: spanContext,
11990
12107
  credentialEgress: requesterId ? {
11991
- requesterId
12108
+ requesterId,
12109
+ activeProvider: () => skillSandbox.getActiveSkill()?.pluginProvider
11992
12110
  } : void 0,
11993
12111
  onSandboxAcquired: async (sandbox2) => {
11994
12112
  lastKnownSandboxId = sandbox2.sandboxId;
@@ -12708,6 +12826,12 @@ function finalizeFailedTurnReply(args) {
12708
12826
  };
12709
12827
  }
12710
12828
 
12829
+ // src/chat/services/turn-continuation-response.ts
12830
+ var TURN_CONTINUATION_RESPONSE = "I'm still working on this in the background. I'll post the final response here when it finishes.";
12831
+ function buildTurnContinuationResponse() {
12832
+ return TURN_CONTINUATION_RESPONSE;
12833
+ }
12834
+
12711
12835
  // src/chat/slack/assistant-thread/status-render.ts
12712
12836
  var DEFAULT_STATUS_CONTEXTS = {
12713
12837
  thinking: "\u2026",
@@ -13360,8 +13484,249 @@ async function postSlackApiReplyPosts(args) {
13360
13484
  });
13361
13485
  throw error;
13362
13486
  }
13363
- }
13364
- return lastPostedMessageTs;
13487
+ }
13488
+ return lastPostedMessageTs;
13489
+ }
13490
+
13491
+ // src/chat/slack/errors.ts
13492
+ function getSlackApiErrorCode(error) {
13493
+ if (!error || typeof error !== "object") {
13494
+ return void 0;
13495
+ }
13496
+ const candidate = error;
13497
+ if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
13498
+ return candidate.data.error;
13499
+ }
13500
+ if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
13501
+ return candidate.code;
13502
+ }
13503
+ return void 0;
13504
+ }
13505
+ function getSlackErrorObservabilityAttributes(error) {
13506
+ if (!error || typeof error !== "object") {
13507
+ return {};
13508
+ }
13509
+ const candidate = error;
13510
+ const attributes = {};
13511
+ if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
13512
+ attributes["app.slack.error_code"] = candidate.code;
13513
+ }
13514
+ if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
13515
+ attributes["app.slack.api_error"] = candidate.data.error;
13516
+ }
13517
+ const requestId = getHeaderString(candidate.headers, "x-slack-req-id");
13518
+ if (requestId) {
13519
+ attributes["app.slack.request_id"] = requestId;
13520
+ }
13521
+ if (typeof candidate.statusCode === "number" && Number.isFinite(candidate.statusCode)) {
13522
+ attributes["http.response.status_code"] = candidate.statusCode;
13523
+ }
13524
+ return attributes;
13525
+ }
13526
+ function isSlackTitlePermissionError(error) {
13527
+ const code = getSlackApiErrorCode(error);
13528
+ return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
13529
+ }
13530
+
13531
+ // src/chat/slack/context.ts
13532
+ function toTrimmedSlackString(value) {
13533
+ const normalized = toOptionalString(value);
13534
+ return normalized?.trim() || void 0;
13535
+ }
13536
+ function parseSlackThreadId(threadId) {
13537
+ const normalizedThreadId = toTrimmedSlackString(threadId);
13538
+ if (!normalizedThreadId) {
13539
+ return void 0;
13540
+ }
13541
+ const parts = normalizedThreadId.split(":");
13542
+ if (parts.length !== 3 || parts[0] !== "slack") {
13543
+ return void 0;
13544
+ }
13545
+ const channelId = toTrimmedSlackString(parts[1]);
13546
+ const threadTs = toTrimmedSlackString(parts[2]);
13547
+ if (!channelId || !threadTs) {
13548
+ return void 0;
13549
+ }
13550
+ return { channelId, threadTs };
13551
+ }
13552
+ function resolveSlackChannelIdFromThreadId(threadId) {
13553
+ return parseSlackThreadId(threadId)?.channelId;
13554
+ }
13555
+ function resolveSlackChannelIdFromMessage(message) {
13556
+ const messageChannelId = toTrimmedSlackString(
13557
+ message.channelId
13558
+ );
13559
+ if (messageChannelId) {
13560
+ return messageChannelId;
13561
+ }
13562
+ const raw = message.raw;
13563
+ if (raw && typeof raw === "object") {
13564
+ const rawChannel = toTrimmedSlackString(
13565
+ raw.channel
13566
+ );
13567
+ if (rawChannel) {
13568
+ return rawChannel;
13569
+ }
13570
+ }
13571
+ const threadId = toTrimmedSlackString(
13572
+ message.threadId
13573
+ );
13574
+ return resolveSlackChannelIdFromThreadId(threadId);
13575
+ }
13576
+
13577
+ // src/chat/runtime/thread-context.ts
13578
+ function escapeRegExp2(value) {
13579
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13580
+ }
13581
+ function stripLeadingBotMention(text, options = {}) {
13582
+ if (!text.trim()) return text;
13583
+ let next = text;
13584
+ if (options.stripLeadingSlackMentionToken) {
13585
+ next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
13586
+ }
13587
+ const mentionByNameRe = new RegExp(
13588
+ `^\\s*@${escapeRegExp2(botConfig.userName)}\\b[\\s,:-]*`,
13589
+ "i"
13590
+ );
13591
+ next = next.replace(mentionByNameRe, "").trim();
13592
+ const mentionByLabeledEntityRe = new RegExp(
13593
+ `^\\s*<@[^>|]+\\|${escapeRegExp2(botConfig.userName)}>[\\s,:-]*`,
13594
+ "i"
13595
+ );
13596
+ next = next.replace(mentionByLabeledEntityRe, "").trim();
13597
+ return next;
13598
+ }
13599
+ function getThreadId(thread, _message) {
13600
+ return toOptionalString(thread.id);
13601
+ }
13602
+ function getRunId(thread, message) {
13603
+ return toOptionalString(thread.runId) ?? toOptionalString(message.runId);
13604
+ }
13605
+ function getChannelId(thread, message) {
13606
+ return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
13607
+ }
13608
+ function getThreadTs(threadId) {
13609
+ return parseSlackThreadId(threadId)?.threadTs;
13610
+ }
13611
+ function getAssistantThreadContext(message) {
13612
+ const raw = message.raw;
13613
+ const rawRecord = raw && typeof raw === "object" ? raw : void 0;
13614
+ const channelId = toOptionalString(rawRecord?.channel);
13615
+ if (channelId) {
13616
+ const rawThreadTs = toOptionalString(rawRecord?.thread_ts);
13617
+ const threadTs = isDmChannel(channelId) ? rawThreadTs : rawThreadTs ?? toOptionalString(rawRecord?.ts);
13618
+ if (threadTs) {
13619
+ return { channelId, threadTs };
13620
+ }
13621
+ }
13622
+ const parsedThreadId = parseSlackThreadId(
13623
+ toOptionalString(message.threadId)
13624
+ );
13625
+ if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) {
13626
+ return void 0;
13627
+ }
13628
+ return parsedThreadId;
13629
+ }
13630
+ function getMessageTs(message) {
13631
+ const directTs = toOptionalString(
13632
+ message.ts
13633
+ );
13634
+ if (directTs) {
13635
+ return directTs;
13636
+ }
13637
+ const raw = message.raw;
13638
+ if (!raw || typeof raw !== "object") {
13639
+ return void 0;
13640
+ }
13641
+ const rawRecord = raw;
13642
+ return toOptionalString(rawRecord.ts) ?? toOptionalString(rawRecord.event_ts) ?? toOptionalString(rawRecord.message?.ts);
13643
+ }
13644
+
13645
+ // src/chat/runtime/processing-reaction.ts
13646
+ var PROCESSING_REACTION_EMOJI = "eyes";
13647
+ var noProcessingReaction = {
13648
+ keep: () => void 0,
13649
+ stop: async () => void 0
13650
+ };
13651
+ function isProcessingReactionEmoji(value) {
13652
+ return typeof value === "string" && normalizeSlackEmojiName(value) === PROCESSING_REACTION_EMOJI;
13653
+ }
13654
+ function shouldKeepProcessingReactionForToolInvocation(input) {
13655
+ return input.toolName === "slackMessageAddReaction" && isProcessingReactionEmoji(input.params.emoji);
13656
+ }
13657
+ async function startSlackProcessingReaction(args) {
13658
+ if (args.message.author.isMe) {
13659
+ return noProcessingReaction;
13660
+ }
13661
+ const channelId = getChannelId(args.thread, args.message);
13662
+ const messageTs = getMessageTs(args.message);
13663
+ if (!channelId || !messageTs) {
13664
+ return noProcessingReaction;
13665
+ }
13666
+ return startSlackProcessingReactionForMessage({
13667
+ channelId,
13668
+ timestamp: messageTs,
13669
+ logException: args.logException,
13670
+ logContext: args.logContext
13671
+ });
13672
+ }
13673
+ async function startSlackProcessingReactionForMessage(args) {
13674
+ try {
13675
+ await addReactionToMessage({
13676
+ channelId: args.channelId,
13677
+ timestamp: args.timestamp,
13678
+ emoji: PROCESSING_REACTION_EMOJI
13679
+ });
13680
+ } catch (error) {
13681
+ args.logException(
13682
+ error,
13683
+ "slack_processing_reaction_add_failed",
13684
+ args.logContext,
13685
+ {
13686
+ "app.slack.action": "reactions.add",
13687
+ "messaging.message.id": args.timestamp,
13688
+ ...getSlackErrorObservabilityAttributes(error)
13689
+ },
13690
+ "Failed to add Slack processing reaction"
13691
+ );
13692
+ return noProcessingReaction;
13693
+ }
13694
+ let shouldRemove = true;
13695
+ return {
13696
+ keep: () => {
13697
+ shouldRemove = false;
13698
+ },
13699
+ stop: async () => {
13700
+ if (!shouldRemove) {
13701
+ return;
13702
+ }
13703
+ try {
13704
+ await removeReactionFromMessage({
13705
+ channelId: args.channelId,
13706
+ timestamp: args.timestamp,
13707
+ emoji: PROCESSING_REACTION_EMOJI
13708
+ });
13709
+ } catch (error) {
13710
+ args.logException(
13711
+ error,
13712
+ "slack_processing_reaction_remove_failed",
13713
+ args.logContext,
13714
+ {
13715
+ "app.slack.action": "reactions.remove",
13716
+ "messaging.message.id": args.timestamp,
13717
+ ...getSlackErrorObservabilityAttributes(error)
13718
+ },
13719
+ "Failed to remove Slack processing reaction"
13720
+ );
13721
+ }
13722
+ }
13723
+ };
13724
+ }
13725
+
13726
+ // src/chat/services/auth-pause-response.ts
13727
+ var AUTH_PAUSE_RESPONSE = "I need authorization to continue. Check your private link to connect.";
13728
+ function buildAuthPauseResponse() {
13729
+ return AUTH_PAUSE_RESPONSE;
13365
13730
  }
13366
13731
 
13367
13732
  // src/chat/runtime/slack-resume.ts
@@ -13453,6 +13818,25 @@ async function postResumeFailureReply(args) {
13453
13818
  throw error;
13454
13819
  }
13455
13820
  }
13821
+ async function postTurnContinuationNoticeBestEffort(args) {
13822
+ try {
13823
+ await postSlackMessage({
13824
+ channelId: args.resumeArgs.channelId,
13825
+ threadTs: args.resumeArgs.threadTs,
13826
+ text: buildTurnContinuationResponse()
13827
+ });
13828
+ } catch (error) {
13829
+ logException(
13830
+ error,
13831
+ "slack_turn_continuation_notice_post_failed",
13832
+ getResumeLogContext(args.resumeArgs, args.lockKey),
13833
+ {
13834
+ "app.slack.reply_stage": "thread_reply_turn_continuation_notice"
13835
+ },
13836
+ "Failed to post turn continuation notice"
13837
+ );
13838
+ }
13839
+ }
13456
13840
  async function handleResumeFailure(args) {
13457
13841
  const logContext = getResumeLogContext(args.resumeArgs, args.lockKey);
13458
13842
  const capturedEventId = logException(
@@ -13520,9 +13904,19 @@ async function resumeSlackTurn(args) {
13520
13904
  channelId: args.channelId,
13521
13905
  threadTs: args.threadTs
13522
13906
  });
13907
+ let processingReaction;
13908
+ let deferredPauseKind;
13523
13909
  let deferredPauseHandler;
13524
13910
  let deferredFailureHandler;
13525
13911
  try {
13912
+ if (args.messageTs) {
13913
+ processingReaction = await startSlackProcessingReactionForMessage({
13914
+ channelId: args.channelId,
13915
+ timestamp: args.messageTs,
13916
+ logException,
13917
+ logContext: { ...getResumeLogContext(args, lockKey) }
13918
+ });
13919
+ }
13526
13920
  if (args.initialText) {
13527
13921
  await postSlackMessageBestEffort(
13528
13922
  args.channelId,
@@ -13573,10 +13967,12 @@ async function resumeSlackTurn(args) {
13573
13967
  const onAuthPause = args.onAuthPause;
13574
13968
  const onTimeoutPause = args.onTimeoutPause;
13575
13969
  if ((isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) && onAuthPause) {
13970
+ deferredPauseKind = "auth";
13576
13971
  deferredPauseHandler = async () => {
13577
13972
  await onAuthPause(error);
13578
13973
  };
13579
13974
  } else if (isRetryableTurnError(error, "turn_timeout_resume") && onTimeoutPause) {
13975
+ deferredPauseKind = "timeout";
13580
13976
  deferredPauseHandler = async () => {
13581
13977
  await onTimeoutPause(error);
13582
13978
  };
@@ -13592,11 +13988,25 @@ async function resumeSlackTurn(args) {
13592
13988
  };
13593
13989
  }
13594
13990
  } finally {
13991
+ await processingReaction?.stop();
13595
13992
  await stateAdapter.releaseLock(lock);
13596
13993
  }
13597
13994
  if (deferredPauseHandler) {
13598
13995
  try {
13599
13996
  await deferredPauseHandler();
13997
+ if (deferredPauseKind === "auth") {
13998
+ await postSlackMessageBestEffort(
13999
+ args.channelId,
14000
+ args.threadTs,
14001
+ buildAuthPauseResponse()
14002
+ );
14003
+ }
14004
+ if (deferredPauseKind === "timeout") {
14005
+ await postTurnContinuationNoticeBestEffort({
14006
+ lockKey,
14007
+ resumeArgs: args
14008
+ });
14009
+ }
13600
14010
  return;
13601
14011
  } catch (pauseError) {
13602
14012
  await handleResumeFailure({
@@ -13618,6 +14028,7 @@ async function resumeAuthorizedRequest(args) {
13618
14028
  messageText: args.messageText,
13619
14029
  channelId: args.channelId,
13620
14030
  threadTs: args.threadTs,
14031
+ messageTs: args.messageTs,
13621
14032
  replyContext: args.replyContext,
13622
14033
  lockKey: args.lockKey,
13623
14034
  initialText: args.connectedText,
@@ -13668,6 +14079,20 @@ var MAX_TURN_TIMEOUT_RESUME_SLICE_ID = 5;
13668
14079
  function canScheduleTurnTimeoutResume(nextSliceId) {
13669
14080
  return typeof nextSliceId === "number" && nextSliceId > 1 && nextSliceId <= MAX_TURN_TIMEOUT_RESUME_SLICE_ID;
13670
14081
  }
14082
+ async function getAwaitingTurnContinuationRequest(args) {
14083
+ const checkpoint = await getAgentTurnSessionCheckpoint(
14084
+ args.conversationId,
14085
+ args.sessionId
14086
+ );
14087
+ if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "timeout" || !canScheduleTurnTimeoutResume(checkpoint.sliceId)) {
14088
+ return void 0;
14089
+ }
14090
+ return {
14091
+ conversationId: args.conversationId,
14092
+ sessionId: args.sessionId,
14093
+ expectedCheckpointVersion: checkpoint.checkpointVersion
14094
+ };
14095
+ }
13671
14096
  function getTurnTimeoutResumeSecret() {
13672
14097
  const explicit = process.env.JUNIOR_INTERNAL_RESUME_SECRET?.trim();
13673
14098
  if (explicit) {
@@ -13926,6 +14351,7 @@ async function resumeAuthorizedMcpTurn(args) {
13926
14351
  messageText: userMessage.text,
13927
14352
  channelId: authSession.channelId,
13928
14353
  threadTs: authSession.threadTs,
14354
+ messageTs: getTurnUserSlackMessageTs(userMessage),
13929
14355
  lockKey: authSession.conversationId,
13930
14356
  connectedText: "",
13931
14357
  replyContext: {
@@ -14367,6 +14793,7 @@ async function resumeCheckpointedOAuthTurn(stored) {
14367
14793
  messageText: stored.pendingMessage ?? userMessage.text,
14368
14794
  channelId: stored.channelId,
14369
14795
  threadTs: stored.threadTs,
14796
+ messageTs: getTurnUserSlackMessageTs(userMessage),
14370
14797
  lockKey: stored.resumeConversationId,
14371
14798
  initialText: "",
14372
14799
  replyContext: {
@@ -14459,14 +14886,15 @@ async function resumePendingOAuthMessage(stored) {
14459
14886
  const conversation = coerceThreadConversationState(
14460
14887
  await getPersistedThreadState(threadId)
14461
14888
  );
14462
- const latestUserMessageId = [...conversation.messages].reverse().find((message) => message.role === "user")?.id;
14889
+ const latestUserMessage = [...conversation.messages].reverse().find((message) => message.role === "user");
14463
14890
  const conversationContext = buildConversationContext(conversation, {
14464
- excludeMessageId: latestUserMessageId
14891
+ excludeMessageId: latestUserMessage?.id
14465
14892
  });
14466
14893
  await resumeAuthorizedRequest({
14467
14894
  messageText: stored.pendingMessage,
14468
14895
  channelId: stored.channelId,
14469
14896
  threadTs: stored.threadTs,
14897
+ messageTs: getTurnUserSlackMessageTs(latestUserMessage),
14470
14898
  connectedText: "",
14471
14899
  replyContext: {
14472
14900
  requester: { userId: stored.userId },
@@ -14723,12 +15151,7 @@ async function getJwks(issuer) {
14723
15151
  });
14724
15152
  return jwks;
14725
15153
  }
14726
- function validateSandboxClaim(payload, egressId) {
14727
- if (payload.sandbox_id !== egressId) {
14728
- throw new Error("Vercel OIDC token belongs to a different sandbox");
14729
- }
14730
- }
14731
- async function verifyVercelSandboxOidcToken(token, egressId) {
15154
+ async function verifyVercelSandboxOidcToken(token) {
14732
15155
  const unverified = decodeJwt(token);
14733
15156
  if (typeof unverified.iss !== "string") {
14734
15157
  throw new Error("Vercel OIDC token did not include an issuer");
@@ -14737,7 +15160,6 @@ async function verifyVercelSandboxOidcToken(token, egressId) {
14737
15160
  const verified = await jwtVerify(token, jwks, {
14738
15161
  issuer: unverified.iss
14739
15162
  });
14740
- validateSandboxClaim(verified.payload, egressId);
14741
15163
  return verified.payload;
14742
15164
  }
14743
15165
 
@@ -14746,7 +15168,7 @@ var OIDC_TOKEN_HEADER = "vercel-sandbox-oidc-token";
14746
15168
  var FORWARDED_HOST_HEADER = "vercel-forwarded-host";
14747
15169
  var FORWARDED_SCHEME_HEADER = "vercel-forwarded-scheme";
14748
15170
  var FORWARDED_PORT_HEADER = "vercel-forwarded-port";
14749
- var ROUTE_PREFIX = "/api/internal/sandbox-egress";
15171
+ var FORWARDED_PATH_HEADER = "vercel-forwarded-path";
14750
15172
  var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
14751
15173
  "connection",
14752
15174
  "host",
@@ -14762,12 +15184,63 @@ var PROXY_ONLY_HEADERS = /* @__PURE__ */ new Set([
14762
15184
  OIDC_TOKEN_HEADER,
14763
15185
  FORWARDED_HOST_HEADER,
14764
15186
  FORWARDED_SCHEME_HEADER,
14765
- FORWARDED_PORT_HEADER
15187
+ FORWARDED_PORT_HEADER,
15188
+ FORWARDED_PATH_HEADER
15189
+ ]);
15190
+ var DECODED_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
15191
+ "content-encoding",
15192
+ "content-length"
14766
15193
  ]);
14767
15194
  var AUTH_REJECTION_STATUS = /* @__PURE__ */ new Set([401, 403]);
14768
15195
  function jsonError(message, status) {
14769
15196
  return Response.json({ error: message }, { status });
14770
15197
  }
15198
+ function shouldLogSandboxEgressInfo() {
15199
+ const environment = (process.env.SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? "").trim().toLowerCase();
15200
+ return environment !== "production";
15201
+ }
15202
+ function egressAttributes(input) {
15203
+ return {
15204
+ ...input.egressId ? { "app.sandbox.egress_id": input.egressId } : {},
15205
+ ...input.provider ? { "app.credential.provider": input.provider } : {},
15206
+ ...input.host ? { "server.address": input.host } : {},
15207
+ ...input.method ? { "http.request.method": input.method } : {},
15208
+ ...input.path ? { "url.path": input.path } : {},
15209
+ ...input.status ? { "http.response.status_code": input.status } : {}
15210
+ };
15211
+ }
15212
+ function routingAttributes(request, upstreamUrl) {
15213
+ const proxyUrl = new URL(request.url);
15214
+ const attributes = {
15215
+ "app.sandbox.egress.proxy_path": proxyUrl.pathname
15216
+ };
15217
+ if (upstreamUrl) {
15218
+ attributes["app.sandbox.egress.upstream_path"] = upstreamUrl.pathname;
15219
+ }
15220
+ return attributes;
15221
+ }
15222
+ function logSandboxEgressUpstreamRequest(input) {
15223
+ if (!shouldLogSandboxEgressInfo()) {
15224
+ return;
15225
+ }
15226
+ logInfo(
15227
+ "sandbox_egress_upstream_request",
15228
+ {},
15229
+ {
15230
+ ...egressAttributes({
15231
+ egressId: input.egressId,
15232
+ host: input.upstreamUrl.hostname,
15233
+ method: input.request.method,
15234
+ path: input.upstreamUrl.pathname,
15235
+ provider: input.provider,
15236
+ status: input.upstream.status
15237
+ }),
15238
+ ...routingAttributes(input.request, input.upstreamUrl),
15239
+ "app.sandbox.egress.upstream_ok": input.upstream.ok
15240
+ },
15241
+ `Sandbox egress ${input.request.method} ${input.upstreamUrl.hostname}${input.upstreamUrl.pathname} -> ${input.upstream.status}`
15242
+ );
15243
+ }
14771
15244
  function normalizeHost(value) {
14772
15245
  const trimmed = value.trim().toLowerCase();
14773
15246
  if (!trimmed || trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes(":")) {
@@ -14789,18 +15262,27 @@ function normalizePort(value) {
14789
15262
  const port = Number.parseInt(trimmed, 10);
14790
15263
  return port >= 1 && port <= 65535 ? trimmed : void 0;
14791
15264
  }
14792
- function upstreamPath(request, egressId) {
14793
- const url = new URL(request.url);
14794
- const prefix = `${ROUTE_PREFIX}/${encodeURIComponent(egressId)}`;
14795
- if (url.pathname === prefix) {
14796
- return `/${url.search}`;
14797
- }
14798
- if (url.pathname.startsWith(`${prefix}/`)) {
14799
- return `${url.pathname.slice(prefix.length)}${url.search}`;
15265
+ function sandboxIdFromPayload(payload) {
15266
+ return typeof payload.sandbox_id === "string" ? payload.sandbox_id : void 0;
15267
+ }
15268
+ function upstreamPath(request) {
15269
+ const forwardedPath = request.headers.get(FORWARDED_PATH_HEADER);
15270
+ if (forwardedPath?.trim()) {
15271
+ const path11 = forwardedPath.trim();
15272
+ if (!path11.startsWith("/") || path11.startsWith("//") || path11.includes("#") || /[\r\n]/.test(path11)) {
15273
+ return { ok: false, error: "Invalid forwarded path" };
15274
+ }
15275
+ try {
15276
+ const url2 = new URL(path11, "https://sandbox-forwarded.local");
15277
+ return { ok: true, path: `${url2.pathname}${url2.search}` };
15278
+ } catch {
15279
+ return { ok: false, error: "Invalid forwarded path" };
15280
+ }
14800
15281
  }
14801
- return void 0;
15282
+ const url = new URL(request.url);
15283
+ return { ok: true, path: `${url.pathname}${url.search}` };
14802
15284
  }
14803
- function buildUpstreamUrl(request, egressId) {
15285
+ function buildUpstreamUrl(request) {
14804
15286
  const forwardedHost = request.headers.get(FORWARDED_HOST_HEADER);
14805
15287
  if (!forwardedHost?.trim()) {
14806
15288
  return { ok: false, error: "Missing forwarded host" };
@@ -14822,12 +15304,14 @@ function buildUpstreamUrl(request, egressId) {
14822
15304
  if (forwardedPort && !port) {
14823
15305
  return { ok: false, error: "Invalid forwarded port" };
14824
15306
  }
14825
- const path11 = upstreamPath(request, egressId);
14826
- if (!path11) {
14827
- return { ok: false, error: "Invalid egress route" };
15307
+ const path11 = upstreamPath(request);
15308
+ if (!path11.ok) {
15309
+ return { ok: false, error: path11.error };
14828
15310
  }
14829
15311
  try {
14830
- const url = new URL(`${scheme}://${host}${port ? `:${port}` : ""}${path11}`);
15312
+ const url = new URL(
15313
+ `${scheme}://${host}${port ? `:${port}` : ""}${path11.path}`
15314
+ );
14831
15315
  return { ok: true, url };
14832
15316
  } catch {
14833
15317
  return { ok: false, error: "Invalid forwarded URL" };
@@ -14862,7 +15346,7 @@ function responseHeaders(upstream) {
14862
15346
  const headers = new Headers();
14863
15347
  upstream.headers.forEach((value, key) => {
14864
15348
  const normalized = key.toLowerCase();
14865
- if (!HOP_BY_HOP_HEADERS.has(normalized)) {
15349
+ if (!HOP_BY_HOP_HEADERS.has(normalized) && !DECODED_RESPONSE_HEADERS.has(normalized)) {
14866
15350
  headers.append(key, value);
14867
15351
  }
14868
15352
  });
@@ -14901,15 +15385,20 @@ function hasTransformForHost(lease, host) {
14901
15385
  (transform) => matchesSandboxEgressDomain(host, transform.domain)
14902
15386
  );
14903
15387
  }
14904
- async function proxySandboxEgressRequest(request, egressId, deps = {}) {
15388
+ function isSandboxEgressForwardedRequest(request) {
15389
+ return Boolean(
15390
+ request.headers.get(OIDC_TOKEN_HEADER)?.trim() && request.headers.get(FORWARDED_HOST_HEADER)?.trim() && request.headers.get(FORWARDED_SCHEME_HEADER)?.trim()
15391
+ );
15392
+ }
15393
+ async function proxySandboxEgressRequest(request, deps = {}) {
14905
15394
  const oidcToken = request.headers.get(OIDC_TOKEN_HEADER)?.trim();
14906
15395
  if (!oidcToken) {
14907
15396
  return jsonError("Missing Vercel Sandbox OIDC token", 401);
14908
15397
  }
15398
+ let oidcPayload;
14909
15399
  try {
14910
- await (deps.verifyOidc ?? verifyVercelSandboxOidcToken)(
14911
- oidcToken,
14912
- egressId
15400
+ oidcPayload = await (deps.verifyOidc ?? verifyVercelSandboxOidcToken)(
15401
+ oidcToken
14913
15402
  );
14914
15403
  } catch (error) {
14915
15404
  logWarn(
@@ -14922,24 +15411,101 @@ async function proxySandboxEgressRequest(request, egressId, deps = {}) {
14922
15411
  );
14923
15412
  return jsonError("Invalid Vercel Sandbox OIDC token", 401);
14924
15413
  }
14925
- const upstreamResult = buildUpstreamUrl(request, egressId);
15414
+ const activeEgressId = sandboxIdFromPayload(oidcPayload);
15415
+ if (!activeEgressId) {
15416
+ logWarn(
15417
+ "sandbox_egress_oidc_session_missing",
15418
+ {},
15419
+ {
15420
+ "http.request.method": request.method,
15421
+ "url.path": new URL(request.url).pathname
15422
+ },
15423
+ "Sandbox egress OIDC payload did not include a VM session id"
15424
+ );
15425
+ return jsonError(
15426
+ "Vercel Sandbox OIDC token did not include sandbox_id",
15427
+ 401
15428
+ );
15429
+ }
15430
+ const upstreamResult = buildUpstreamUrl(request);
14926
15431
  if (!upstreamResult.ok) {
15432
+ logWarn(
15433
+ "sandbox_egress_upstream_url_invalid",
15434
+ {},
15435
+ {
15436
+ ...egressAttributes({
15437
+ egressId: activeEgressId,
15438
+ method: request.method,
15439
+ path: new URL(request.url).pathname,
15440
+ status: 400
15441
+ }),
15442
+ ...routingAttributes(request)
15443
+ },
15444
+ "Sandbox egress forwarded request had invalid upstream routing headers"
15445
+ );
14927
15446
  return jsonError(upstreamResult.error, 400);
14928
15447
  }
14929
15448
  const upstreamUrl = upstreamResult.url;
14930
15449
  const provider = resolveSandboxEgressProviderForHost(upstreamUrl.hostname);
14931
15450
  if (!provider) {
15451
+ logWarn(
15452
+ "sandbox_egress_provider_unresolved",
15453
+ {},
15454
+ {
15455
+ ...egressAttributes({
15456
+ egressId: activeEgressId,
15457
+ host: upstreamUrl.hostname,
15458
+ method: request.method,
15459
+ path: upstreamUrl.pathname,
15460
+ status: 403
15461
+ }),
15462
+ ...routingAttributes(request, upstreamUrl)
15463
+ },
15464
+ "Sandbox egress forwarded host is not owned by any credential provider"
15465
+ );
14932
15466
  return jsonError("No provider owns forwarded host", 403);
14933
15467
  }
14934
- const session = await getSandboxEgressSession(egressId);
15468
+ const session = await getSandboxEgressSession(activeEgressId);
14935
15469
  if (!session) {
15470
+ logWarn(
15471
+ "sandbox_egress_session_unauthorized",
15472
+ {},
15473
+ {
15474
+ ...egressAttributes({
15475
+ egressId: activeEgressId,
15476
+ host: upstreamUrl.hostname,
15477
+ method: request.method,
15478
+ path: upstreamUrl.pathname,
15479
+ provider,
15480
+ status: 403
15481
+ }),
15482
+ ...routingAttributes(request, upstreamUrl)
15483
+ },
15484
+ "Sandbox egress VM session is not authorized for requester credentials"
15485
+ );
14936
15486
  return jsonError("Sandbox egress session is not authorized", 403);
14937
15487
  }
14938
15488
  let lease;
14939
15489
  try {
14940
- lease = await credentialLease(egressId, provider, session);
15490
+ lease = await credentialLease(activeEgressId, provider, session);
14941
15491
  } catch (error) {
14942
15492
  if (error instanceof CredentialUnavailableError) {
15493
+ logWarn(
15494
+ "sandbox_egress_credential_unavailable",
15495
+ {},
15496
+ {
15497
+ ...egressAttributes({
15498
+ egressId: activeEgressId,
15499
+ host: upstreamUrl.hostname,
15500
+ method: request.method,
15501
+ path: upstreamUrl.pathname,
15502
+ provider,
15503
+ status: 401
15504
+ }),
15505
+ ...routingAttributes(request, upstreamUrl)
15506
+ },
15507
+ "Sandbox egress provider credential is unavailable"
15508
+ );
14943
15509
  return new Response(
14944
15510
  `junior-auth-required provider=${error.provider} 401 unauthorized
14945
15511
  ${error.message}`,
@@ -14952,28 +15518,80 @@ ${error.message}`,
14952
15518
  throw error;
14953
15519
  }
14954
15520
  if (!hasTransformForHost(lease, upstreamUrl.hostname)) {
15521
+ logWarn(
15522
+ "sandbox_egress_transform_missing",
15523
+ {},
15524
+ {
15525
+ ...egressAttributes({
15526
+ egressId: activeEgressId,
15527
+ host: upstreamUrl.hostname,
15528
+ method: request.method,
15529
+ path: upstreamUrl.pathname,
15530
+ provider,
15531
+ status: 403
15532
+ }),
15533
+ "app.sandbox.egress.transform_domains": lease.headerTransforms.map(
15534
+ (transform) => transform.domain
15535
+ ),
15536
+ ...routingAttributes(request, upstreamUrl)
15537
+ },
15538
+ "Sandbox egress credential lease does not cover forwarded host"
15539
+ );
14955
15540
  return jsonError("Credential lease does not cover forwarded host", 403);
14956
15541
  }
14957
15542
  const body = await requestBodyBytes(request);
14958
- const upstream = await (deps.fetch ?? fetch)(upstreamUrl, {
15543
+ const fetchImpl = deps.fetch ?? fetch;
15544
+ const headers = requestHeaders(request, lease, upstreamUrl.hostname);
15545
+ const upstream = await fetchImpl(upstreamUrl, {
14959
15546
  method: request.method,
14960
- headers: requestHeaders(request, lease, upstreamUrl.hostname),
14961
- ...body ? { body } : {},
15547
+ headers,
15548
+ ...body !== void 0 ? { body } : {},
14962
15549
  redirect: "manual"
14963
15550
  });
15551
+ logSandboxEgressUpstreamRequest({
15552
+ egressId: activeEgressId,
15553
+ provider,
15554
+ request,
15555
+ upstream,
15556
+ upstreamUrl
15557
+ });
15558
+ if (upstream.status >= 400) {
15559
+ logWarn(
15560
+ "sandbox_egress_upstream_error_response",
15561
+ {},
15562
+ {
15563
+ ...egressAttributes({
15564
+ egressId: activeEgressId,
15565
+ host: upstreamUrl.hostname,
15566
+ method: request.method,
15567
+ path: upstreamUrl.pathname,
15568
+ provider,
15569
+ status: upstream.status
15570
+ }),
15571
+ ...routingAttributes(request, upstreamUrl),
15572
+ "error.type": `http_${upstream.status}`
15573
+ },
15574
+ `Sandbox egress upstream returned HTTP ${upstream.status}`
15575
+ );
15576
+ }
14964
15577
  if (AUTH_REJECTION_STATUS.has(upstream.status)) {
14965
15578
  logWarn(
14966
15579
  "sandbox_egress_upstream_auth_rejected",
14967
15580
  {},
14968
15581
  {
14969
- "app.credential.provider": provider,
14970
- "http.request.method": request.method,
14971
- "http.response.status_code": upstream.status,
14972
- "server.address": upstreamUrl.hostname
15582
+ ...egressAttributes({
15583
+ egressId: activeEgressId,
15584
+ host: upstreamUrl.hostname,
15585
+ method: request.method,
15586
+ path: upstreamUrl.pathname,
15587
+ provider,
15588
+ status: upstream.status
15589
+ }),
15590
+ ...routingAttributes(request, upstreamUrl)
14973
15591
  },
14974
15592
  "Sandbox egress upstream auth rejected"
14975
15593
  );
14976
- await clearSandboxEgressCredentialLease(egressId, provider, session);
15594
+ await clearSandboxEgressCredentialLease(activeEgressId, provider, session);
14977
15595
  }
14978
15596
  return new Response(upstream.body, {
14979
15597
  status: upstream.status,
@@ -14983,57 +15601,18 @@ ${error.message}`,
14983
15601
  }
14984
15602
 
14985
15603
  // src/handlers/sandbox-egress-proxy.ts
14986
- async function ALL(request, egressId) {
14987
- return await proxySandboxEgressRequest(request, egressId);
14988
- }
14989
-
14990
- // src/chat/slack/context.ts
14991
- function toTrimmedSlackString(value) {
14992
- const normalized = toOptionalString(value);
14993
- return normalized?.trim() || void 0;
15604
+ async function ALL(request) {
15605
+ return await proxySandboxEgressRequest(request);
14994
15606
  }
14995
- function parseSlackThreadId(threadId) {
14996
- const normalizedThreadId = toTrimmedSlackString(threadId);
14997
- if (!normalizedThreadId) {
14998
- return void 0;
14999
- }
15000
- const parts = normalizedThreadId.split(":");
15001
- if (parts.length !== 3 || parts[0] !== "slack") {
15002
- return void 0;
15003
- }
15004
- const channelId = toTrimmedSlackString(parts[1]);
15005
- const threadTs = toTrimmedSlackString(parts[2]);
15006
- if (!channelId || !threadTs) {
15007
- return void 0;
15008
- }
15009
- return { channelId, threadTs };
15010
- }
15011
- function resolveSlackChannelIdFromThreadId(threadId) {
15012
- return parseSlackThreadId(threadId)?.channelId;
15013
- }
15014
- function resolveSlackChannelIdFromMessage(message) {
15015
- const messageChannelId = toTrimmedSlackString(
15016
- message.channelId
15017
- );
15018
- if (messageChannelId) {
15019
- return messageChannelId;
15020
- }
15021
- const raw = message.raw;
15022
- if (raw && typeof raw === "object") {
15023
- const rawChannel = toTrimmedSlackString(
15024
- raw.channel
15025
- );
15026
- if (rawChannel) {
15027
- return rawChannel;
15028
- }
15029
- }
15030
- const threadId = toTrimmedSlackString(
15031
- message.threadId
15032
- );
15033
- return resolveSlackChannelIdFromThreadId(threadId);
15607
+ function isSandboxEgressRequest(request) {
15608
+ return isSandboxEgressForwardedRequest(request);
15034
15609
  }
15035
15610
 
15036
15611
  // src/handlers/turn-resume.ts
15612
+ var TIMEOUT_RESUME_LOCK_RETRY_DELAYS_MS = [250, 1e3, 2e3];
15613
+ function sleep3(ms) {
15614
+ return new Promise((resolve) => setTimeout(resolve, ms));
15615
+ }
15037
15616
  async function persistCompletedReplyState2(args) {
15038
15617
  const currentState = await getPersistedThreadState(
15039
15618
  args.checkpoint.conversationId
@@ -15233,25 +15812,53 @@ async function resumeTimedOutTurn(payload) {
15233
15812
  }
15234
15813
  });
15235
15814
  }
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) {
15815
+ async function resumeTimedOutTurnWithLockRetry(payload) {
15816
+ for (const [attempt, delayMs] of [
15817
+ ...TIMEOUT_RESUME_LOCK_RETRY_DELAYS_MS,
15818
+ void 0
15819
+ ].entries()) {
15820
+ try {
15821
+ await resumeTimedOutTurn(payload);
15822
+ return;
15823
+ } catch (error) {
15824
+ if (!(error instanceof ResumeTurnBusyError)) {
15825
+ throw error;
15826
+ }
15827
+ if (typeof delayMs !== "number") {
15244
15828
  logWarn(
15245
15829
  "timeout_resume_lock_busy",
15246
15830
  {},
15247
15831
  {
15248
15832
  "app.ai.conversation_id": payload.conversationId,
15249
- "app.ai.session_id": payload.sessionId
15833
+ "app.ai.session_id": payload.sessionId,
15834
+ "app.ai.resume_lock_retry_count": attempt
15250
15835
  },
15251
- "Skipped timeout resume because another turn owns the thread lock"
15836
+ "Skipped timeout resume because another turn still owns the thread lock"
15252
15837
  );
15253
15838
  return;
15254
15839
  }
15840
+ logWarn(
15841
+ "timeout_resume_lock_busy_retrying",
15842
+ {},
15843
+ {
15844
+ "app.ai.conversation_id": payload.conversationId,
15845
+ "app.ai.session_id": payload.sessionId,
15846
+ "app.ai.resume_lock_retry_attempt": attempt + 1,
15847
+ "app.ai.resume_lock_retry_delay_ms": delayMs
15848
+ },
15849
+ "Timeout resume lock was busy; retrying"
15850
+ );
15851
+ await sleep3(delayMs);
15852
+ }
15853
+ }
15854
+ }
15855
+ async function POST(request, waitUntil) {
15856
+ const payload = await verifyTurnTimeoutResumeRequest(request);
15857
+ if (!payload) {
15858
+ return new Response("Unauthorized", { status: 401 });
15859
+ }
15860
+ waitUntil(
15861
+ () => resumeTimedOutTurnWithLockRetry(payload).catch((error) => {
15255
15862
  logException(
15256
15863
  error,
15257
15864
  "timeout_resume_handler_failed",
@@ -15293,11 +15900,11 @@ var DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|
15293
15900
  var TERSE_CLARIFICATION_RE = /^(?:which one|which ones|why|how so|what do you mean|what did you mean|say more|explain that|clarify that|expand on that|elaborate on that)\??$/i;
15294
15901
  var GENERIC_IMMEDIATE_SIDE_CONVERSATION_RE = /^(?:is that (?:the )?right (?:approach|call|move)|(?:can|could|would) you check on this)\??$/i;
15295
15902
  var RECENT_THREAD_WINDOW = 6;
15296
- function escapeRegExp2(value) {
15903
+ function escapeRegExp3(value) {
15297
15904
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15298
15905
  }
15299
15906
  function containsAssistantInvocation(text, botUserName) {
15300
- const escapedUserName = escapeRegExp2(botUserName);
15907
+ const escapedUserName = escapeRegExp3(botUserName);
15301
15908
  const plainNameMentionRe = new RegExp(`(^|\\s)@${escapedUserName}\\b`, "i");
15302
15909
  const labeledEntityMentionRe = new RegExp(
15303
15910
  `<@[^>|]+\\|${escapedUserName}>`,
@@ -15567,69 +16174,29 @@ async function decideSubscribedThreadReply(args) {
15567
16174
  if (!parsed.should_reply) {
15568
16175
  return {
15569
16176
  shouldReply: false,
15570
- reason: "side_conversation" /* SideConversation */,
15571
- reasonDetail: reason
15572
- };
15573
- }
15574
- if (parsed.confidence < replyConfidenceThreshold) {
15575
- return {
15576
- shouldReply: false,
15577
- reason: "low_confidence" /* LowConfidence */,
15578
- reasonDetail: `${parsed.confidence.toFixed(2)}: ${reason}`
15579
- };
15580
- }
15581
- return {
15582
- shouldReply: true,
15583
- reason: "llm_classifier" /* Classifier */,
15584
- reasonDetail: reason
15585
- };
15586
- } catch (error) {
15587
- args.logClassifierFailure(error, args.input);
15588
- return {
15589
- shouldReply: false,
15590
- reason: "classifier_error" /* ClassifierError */
15591
- };
15592
- }
15593
- }
15594
-
15595
- // src/chat/slack/errors.ts
15596
- function getSlackApiErrorCode(error) {
15597
- if (!error || typeof error !== "object") {
15598
- return void 0;
15599
- }
15600
- const candidate = error;
15601
- if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
15602
- return candidate.data.error;
15603
- }
15604
- if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
15605
- return candidate.code;
15606
- }
15607
- return void 0;
15608
- }
15609
- function getSlackErrorObservabilityAttributes(error) {
15610
- if (!error || typeof error !== "object") {
15611
- return {};
15612
- }
15613
- const candidate = error;
15614
- const attributes = {};
15615
- if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
15616
- attributes["app.slack.error_code"] = candidate.code;
15617
- }
15618
- if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
15619
- attributes["app.slack.api_error"] = candidate.data.error;
15620
- }
15621
- const requestId = getHeaderString(candidate.headers, "x-slack-req-id");
15622
- if (requestId) {
15623
- attributes["app.slack.request_id"] = requestId;
15624
- }
15625
- if (typeof candidate.statusCode === "number" && Number.isFinite(candidate.statusCode)) {
15626
- attributes["http.response.status_code"] = candidate.statusCode;
16177
+ reason: "side_conversation" /* SideConversation */,
16178
+ reasonDetail: reason
16179
+ };
16180
+ }
16181
+ if (parsed.confidence < replyConfidenceThreshold) {
16182
+ return {
16183
+ shouldReply: false,
16184
+ reason: "low_confidence" /* LowConfidence */,
16185
+ reasonDetail: `${parsed.confidence.toFixed(2)}: ${reason}`
16186
+ };
16187
+ }
16188
+ return {
16189
+ shouldReply: true,
16190
+ reason: "llm_classifier" /* Classifier */,
16191
+ reasonDetail: reason
16192
+ };
16193
+ } catch (error) {
16194
+ args.logClassifierFailure(error, args.input);
16195
+ return {
16196
+ shouldReply: false,
16197
+ reason: "classifier_error" /* ClassifierError */
16198
+ };
15627
16199
  }
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";
15633
16200
  }
15634
16201
 
15635
16202
  // src/chat/runtime/slack-runtime.ts
@@ -15657,6 +16224,14 @@ function buildLogContext(deps, args) {
15657
16224
  }
15658
16225
  function createSlackTurnRuntime(deps) {
15659
16226
  const logContext = (args) => buildLogContext(deps, args);
16227
+ const createToolInvocationHook = (processingReaction, hooks) => {
16228
+ return (invocation) => {
16229
+ if (shouldKeepProcessingReactionForToolInvocation(invocation)) {
16230
+ processingReaction.keep();
16231
+ }
16232
+ hooks?.onToolInvocation?.(invocation);
16233
+ };
16234
+ };
15660
16235
  const postFallbackErrorReplyWithLogging = async (args) => {
15661
16236
  try {
15662
16237
  await args.thread.post(buildTurnFailureResponse(args.eventId));
@@ -15710,6 +16285,7 @@ function createSlackTurnRuntime(deps) {
15710
16285
  };
15711
16286
  return {
15712
16287
  async handleNewMention(thread, message, hooks) {
16288
+ let processingReaction;
15713
16289
  try {
15714
16290
  const threadId = deps.getThreadId(thread, message);
15715
16291
  const channelId = deps.getChannelId(thread, message);
@@ -15721,11 +16297,22 @@ function createSlackTurnRuntime(deps) {
15721
16297
  requesterUserName: message.author.userName,
15722
16298
  runId
15723
16299
  });
16300
+ processingReaction = await startSlackProcessingReaction({
16301
+ thread,
16302
+ message,
16303
+ logException: deps.logException,
16304
+ logContext: context
16305
+ });
16306
+ const toolInvocationHook = createToolInvocationHook(
16307
+ processingReaction,
16308
+ hooks
16309
+ );
15724
16310
  await deps.withSpan("chat.turn", "chat.turn", context, async () => {
15725
16311
  await thread.subscribe();
15726
16312
  await deps.replyToThread(thread, message, {
15727
16313
  explicitMention: true,
15728
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost
16314
+ beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
16315
+ onToolInvocation: toolInvocationHook
15729
16316
  });
15730
16317
  });
15731
16318
  } catch (error) {
@@ -15766,113 +16353,123 @@ function createSlackTurnRuntime(deps) {
15766
16353
  postFailureEventName: "mention_handler_failure_reply_post_failed",
15767
16354
  postFailureBody: "Failed to post fallback error reply for mention handler"
15768
16355
  });
16356
+ } finally {
16357
+ await processingReaction?.stop();
15769
16358
  }
15770
16359
  },
15771
16360
  async handleSubscribedMessage(thread, message, hooks) {
16361
+ let processingReaction;
15772
16362
  try {
15773
16363
  const threadId = deps.getThreadId(thread, message);
15774
16364
  const channelId = deps.getChannelId(thread, message);
15775
16365
  const runId = deps.getRunId(thread, message);
15776
- await deps.withSpan(
15777
- "chat.turn",
15778
- "chat.turn",
15779
- logContext({
16366
+ const context = logContext({
16367
+ threadId,
16368
+ requesterId: message.author.userId,
16369
+ requesterUserName: message.author.userName,
16370
+ channelId,
16371
+ runId
16372
+ });
16373
+ processingReaction = await startSlackProcessingReaction({
16374
+ thread,
16375
+ message,
16376
+ logException: deps.logException,
16377
+ logContext: context
16378
+ });
16379
+ const toolInvocationHook = createToolInvocationHook(
16380
+ processingReaction,
16381
+ hooks
16382
+ );
16383
+ await deps.withSpan("chat.turn", "chat.turn", context, async () => {
16384
+ const legacyAttachmentText = renderSlackLegacyAttachmentText(
16385
+ message.raw
16386
+ );
16387
+ const rawUserText = appendSlackLegacyAttachmentText(
16388
+ message.text,
16389
+ message.raw
16390
+ );
16391
+ const strippedUserText = deps.stripLeadingBotMention(message.text, {
16392
+ stripLeadingSlackMentionToken: Boolean(message.isMention)
16393
+ });
16394
+ const userText = appendSlackLegacyAttachmentText(
16395
+ strippedUserText,
16396
+ message.raw
16397
+ );
16398
+ const context2 = {
15780
16399
  threadId,
15781
16400
  requesterId: message.author.userId,
15782
- requesterUserName: message.author.userName,
15783
16401
  channelId,
15784
16402
  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({
16403
+ };
16404
+ const preflightDecision = getSubscribedReplyPreflightDecision({
16405
+ botUserName: deps.assistantUserName,
16406
+ rawText: rawUserText,
16407
+ text: userText,
16408
+ isExplicitMention: Boolean(message.isMention)
16409
+ });
16410
+ if (preflightDecision && !preflightDecision.shouldReply) {
16411
+ const reason = preflightDecision.reasonDetail ? `${preflightDecision.reason}:${preflightDecision.reasonDetail}` : preflightDecision.reason;
16412
+ await skipSubscribedMessage({
15825
16413
  thread,
15826
16414
  message,
15827
- userText,
15828
- explicitMention: Boolean(message.isMention),
15829
- context
16415
+ decision: { shouldReply: false, reason },
16416
+ context: context2,
16417
+ userText
15830
16418
  });
15831
- await deps.persistPreparedState({
16419
+ return;
16420
+ }
16421
+ const preparedState = await deps.prepareTurnState({
16422
+ thread,
16423
+ message,
16424
+ userText,
16425
+ explicitMention: Boolean(message.isMention),
16426
+ context: context2
16427
+ });
16428
+ await deps.persistPreparedState({
16429
+ thread,
16430
+ preparedState
16431
+ });
16432
+ const decision = await deps.decideSubscribedReply({
16433
+ rawText: rawUserText,
16434
+ text: userText,
16435
+ conversationContext: deps.getPreparedConversationContext(preparedState),
16436
+ hasAttachments: message.attachments.length > 0 || legacyAttachmentText !== "",
16437
+ isExplicitMention: Boolean(message.isMention),
16438
+ context: context2
16439
+ });
16440
+ if (await maybeHandleThreadOptOutDecision({
16441
+ thread,
16442
+ decision,
16443
+ beforeFirstResponsePost: hooks?.beforeFirstResponsePost
16444
+ })) {
16445
+ await skipSubscribedMessage({
15832
16446
  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
16447
+ message,
16448
+ decision,
16449
+ context: context2,
16450
+ preparedState,
16451
+ userText
15842
16452
  });
15843
- if (await maybeHandleThreadOptOutDecision({
16453
+ return;
16454
+ }
16455
+ if (!decision.shouldReply) {
16456
+ await skipSubscribedMessage({
15844
16457
  thread,
16458
+ message,
15845
16459
  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),
16460
+ context: context2,
15871
16461
  preparedState,
15872
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost
16462
+ userText
15873
16463
  });
16464
+ return;
15874
16465
  }
15875
- );
16466
+ await deps.replyToThread(thread, message, {
16467
+ explicitMention: Boolean(message.isMention),
16468
+ preparedState,
16469
+ beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
16470
+ onToolInvocation: toolInvocationHook
16471
+ });
16472
+ });
15876
16473
  } catch (error) {
15877
16474
  const errorContext = logContext({
15878
16475
  threadId: deps.getThreadId(thread, message),
@@ -15911,6 +16508,8 @@ function createSlackTurnRuntime(deps) {
15911
16508
  postFailureEventName: "subscribed_message_handler_failure_reply_post_failed",
15912
16509
  postFailureBody: "Failed to post fallback error reply for subscribed message handler"
15913
16510
  });
16511
+ } finally {
16512
+ await processingReaction?.stop();
15914
16513
  }
15915
16514
  },
15916
16515
  async handleAssistantThreadStarted(event) {
@@ -16608,6 +17207,7 @@ function createJuniorRuntimeServices(overrides = {}) {
16608
17207
  conversationMemory,
16609
17208
  replyExecutor: {
16610
17209
  generateAssistantReply: overrides.replyExecutor?.generateAssistantReply ?? generateAssistantReply,
17210
+ getAwaitingTurnContinuationRequest: overrides.replyExecutor?.getAwaitingTurnContinuationRequest ?? getAwaitingTurnContinuationRequest,
16611
17211
  lookupSlackUser: overrides.replyExecutor?.lookupSlackUser ?? lookupSlackUser,
16612
17212
  scheduleTurnTimeoutResume: overrides.replyExecutor?.scheduleTurnTimeoutResume ?? scheduleTurnTimeoutResume,
16613
17213
  generateThreadTitle: conversationMemory.generateThreadTitle
@@ -16620,8 +17220,14 @@ function createJuniorRuntimeServices(overrides = {}) {
16620
17220
  }
16621
17221
 
16622
17222
  // src/chat/slack/message.ts
17223
+ function isSlackMessageTs(value) {
17224
+ return /^\d+(?:\.\d+)?$/.test(value.trim());
17225
+ }
16623
17226
  function getSlackMessageTs(message) {
16624
- if (message.id.endsWith(":message_changed_mention") && message.raw && typeof message.raw === "object") {
17227
+ if (isSlackMessageTs(message.id)) {
17228
+ return message.id;
17229
+ }
17230
+ if (message.raw && typeof message.raw === "object") {
16625
17231
  const ts = message.raw.ts;
16626
17232
  if (typeof ts === "string" && ts.length > 0) {
16627
17233
  return ts;
@@ -16630,74 +17236,6 @@ function getSlackMessageTs(message) {
16630
17236
  return message.id;
16631
17237
  }
16632
17238
 
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
17239
  // src/chat/slack/assistant-thread/title.ts
16702
17240
  function maybeUpdateAssistantTitle(args) {
16703
17241
  const assistantThreadContext = args.assistantThreadContext;
@@ -16814,11 +17352,6 @@ function createReplyToThread(deps) {
16814
17352
  });
16815
17353
  const slackMessageTs = getSlackMessageTs(message);
16816
17354
  const turnId = buildDeterministicTurnId(message.id);
16817
- startActiveTurn({
16818
- conversation: preparedState.conversation,
16819
- nextTurnId: turnId,
16820
- updateConversationStats
16821
- });
16822
17355
  const turnTraceContext = {
16823
17356
  conversationId,
16824
17357
  slackThreadId: threadId,
@@ -16828,6 +17361,98 @@ function createReplyToThread(deps) {
16828
17361
  assistantUserName: botConfig.userName,
16829
17362
  modelId: botConfig.modelId
16830
17363
  };
17364
+ let beforeFirstResponsePostCalled = false;
17365
+ const beforeFirstResponsePost = async () => {
17366
+ if (beforeFirstResponsePostCalled) {
17367
+ return;
17368
+ }
17369
+ beforeFirstResponsePostCalled = true;
17370
+ await options.beforeFirstResponsePost?.();
17371
+ };
17372
+ const postTurnContinuationNotice = async () => {
17373
+ try {
17374
+ await beforeFirstResponsePost();
17375
+ await thread.post(
17376
+ buildSlackOutputMessage(buildTurnContinuationResponse())
17377
+ );
17378
+ } catch (error) {
17379
+ logException(
17380
+ error,
17381
+ "slack_turn_continuation_notice_post_failed",
17382
+ turnTraceContext,
17383
+ {
17384
+ "app.slack.reply_stage": "thread_reply_turn_continuation_notice",
17385
+ ...messageTs ? { "messaging.message.id": messageTs } : {},
17386
+ ...getSlackErrorObservabilityAttributes(error)
17387
+ },
17388
+ "Failed to post turn continuation notice"
17389
+ );
17390
+ throw error;
17391
+ }
17392
+ };
17393
+ const postAuthPauseNotice = async () => {
17394
+ try {
17395
+ await beforeFirstResponsePost();
17396
+ await thread.post(
17397
+ buildSlackOutputMessage(buildAuthPauseResponse())
17398
+ );
17399
+ } catch (error) {
17400
+ logException(
17401
+ error,
17402
+ "slack_auth_pause_notice_post_failed",
17403
+ turnTraceContext,
17404
+ {
17405
+ "app.slack.reply_stage": "thread_reply_auth_pause_notice",
17406
+ ...messageTs ? { "messaging.message.id": messageTs } : {},
17407
+ ...getSlackErrorObservabilityAttributes(error)
17408
+ },
17409
+ "Failed to post auth pause notice"
17410
+ );
17411
+ }
17412
+ };
17413
+ const activeTurnId = preparedState.conversation.processing.activeTurnId;
17414
+ if (conversationId && activeTurnId) {
17415
+ const resumeRequest = await deps.services.getAwaitingTurnContinuationRequest({
17416
+ conversationId,
17417
+ sessionId: activeTurnId
17418
+ });
17419
+ if (resumeRequest) {
17420
+ try {
17421
+ await deps.services.scheduleTurnTimeoutResume(resumeRequest);
17422
+ } catch (error) {
17423
+ logException(
17424
+ error,
17425
+ "agent_turn_continuation_retry_schedule_failed",
17426
+ turnTraceContext,
17427
+ {
17428
+ "app.ai.resume_checkpoint_version": resumeRequest.expectedCheckpointVersion,
17429
+ "app.ai.resume_session_id": resumeRequest.sessionId,
17430
+ ...messageTs ? { "messaging.message.id": messageTs } : {}
17431
+ },
17432
+ "Failed to reschedule active turn continuation"
17433
+ );
17434
+ throw error;
17435
+ }
17436
+ await postTurnContinuationNotice();
17437
+ markConversationMessage(
17438
+ preparedState.conversation,
17439
+ preparedState.userMessageId,
17440
+ {
17441
+ replied: true,
17442
+ skippedReason: void 0
17443
+ }
17444
+ );
17445
+ await persistThreadState(thread, {
17446
+ conversation: preparedState.conversation
17447
+ });
17448
+ return;
17449
+ }
17450
+ }
17451
+ startActiveTurn({
17452
+ conversation: preparedState.conversation,
17453
+ nextTurnId: turnId,
17454
+ updateConversationStats
17455
+ });
16831
17456
  setTags({
16832
17457
  conversationId
16833
17458
  });
@@ -16869,14 +17494,6 @@ function createReplyToThread(deps) {
16869
17494
  threadTs: assistantThreadContext?.threadTs,
16870
17495
  getSlackAdapter: deps.getSlackAdapter
16871
17496
  });
16872
- let beforeFirstResponsePostCalled = false;
16873
- const beforeFirstResponsePost = async () => {
16874
- if (beforeFirstResponsePostCalled) {
16875
- return;
16876
- }
16877
- beforeFirstResponsePostCalled = true;
16878
- await options.beforeFirstResponsePost?.();
16879
- };
16880
17497
  const postThreadReply = async (payload, stage) => {
16881
17498
  await beforeFirstResponsePost();
16882
17499
  try {
@@ -16963,7 +17580,8 @@ function createReplyToThread(deps) {
16963
17580
  conversation: preparedState.conversation
16964
17581
  });
16965
17582
  },
16966
- onStatus: (nextStatus) => status.update(nextStatus)
17583
+ onStatus: (nextStatus) => status.update(nextStatus),
17584
+ onToolInvocation: options.onToolInvocation
16967
17585
  });
16968
17586
  const diagnosticsContext = {
16969
17587
  slackThreadId: threadId,
@@ -17106,6 +17724,7 @@ function createReplyToThread(deps) {
17106
17724
  }
17107
17725
  } catch (error) {
17108
17726
  if (isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) {
17727
+ await postAuthPauseNotice();
17109
17728
  completeAuthPauseTurn({
17110
17729
  conversation: preparedState.conversation,
17111
17730
  sessionId: error.metadata?.sessionId ?? turnId
@@ -17130,7 +17749,6 @@ function createReplyToThread(deps) {
17130
17749
  expectedCheckpointVersion: checkpointVersion
17131
17750
  });
17132
17751
  shouldPersistFailureState = false;
17133
- return;
17134
17752
  } catch (scheduleError) {
17135
17753
  logException(
17136
17754
  scheduleError,
@@ -17142,7 +17760,11 @@ function createReplyToThread(deps) {
17142
17760
  },
17143
17761
  "Failed to schedule timeout resume callback"
17144
17762
  );
17763
+ shouldPersistFailureState = true;
17764
+ throw scheduleError;
17145
17765
  }
17766
+ await postTurnContinuationNotice();
17767
+ return;
17146
17768
  } else if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
17147
17769
  logWarn(
17148
17770
  "agent_turn_timeout_resume_slice_limit_reached",
@@ -17599,75 +18221,53 @@ function enqueueBackgroundTask(options, task) {
17599
18221
  throw new Error("Chat background processing requires waitUntil");
17600
18222
  }
17601
18223
  options.waitUntil(task);
18224
+ return task;
17602
18225
  }
17603
18226
  var JuniorChat = class extends Chat {
17604
18227
  /**
17605
18228
  * Normalize Slack thread IDs before the SDK's concurrency queue.
17606
18229
  *
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.
18230
+ * Slack DM roots can arrive with an empty thread timestamp, while
18231
+ * later replies include the root timestamp. Resolve factories before
18232
+ * delegating so the lock/state/subscription key is canonicalized before
18233
+ * the SDK computes its per-thread queue key.
17625
18234
  */
17626
18235
  processMessage(adapter, threadId, messageOrFactory, options) {
17627
18236
  if (typeof messageOrFactory === "function") {
17628
18237
  const runtime = this;
17629
- enqueueBackgroundTask(
18238
+ return enqueueBackgroundTask(
17630
18239
  options,
17631
18240
  (async () => {
18241
+ let message2;
17632
18242
  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);
18243
+ message2 = await messageOrFactory();
17645
18244
  } catch (error) {
17646
18245
  runtime.logger?.error?.("Message factory resolution error", {
17647
18246
  error,
17648
18247
  threadId
17649
18248
  });
18249
+ return;
18250
+ }
18251
+ if (isExternalSlackUser(message2.raw)) {
18252
+ return;
17650
18253
  }
18254
+ const normalized2 = normalizeIncomingSlackThreadId(threadId, message2);
18255
+ if (normalized2 !== threadId && "threadId" in message2) {
18256
+ message2.threadId = normalized2;
18257
+ }
18258
+ await super.processMessage(adapter, normalized2, message2, options);
17651
18259
  })()
17652
18260
  );
17653
- return;
17654
18261
  }
17655
- if (isExternalSlackUser(messageOrFactory.raw)) {
17656
- return;
18262
+ const message = messageOrFactory;
18263
+ if (isExternalSlackUser(message.raw)) {
18264
+ return Promise.resolve();
17657
18265
  }
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
- );
18266
+ const normalized = normalizeIncomingSlackThreadId(threadId, message);
18267
+ if (normalized !== threadId && "threadId" in message) {
18268
+ message.threadId = normalized;
18269
+ }
18270
+ return super.processMessage(adapter, normalized, message, options);
17671
18271
  }
17672
18272
  processReaction(event, options) {
17673
18273
  const runtime = this;
@@ -18351,6 +18951,12 @@ async function createApp(options) {
18351
18951
  logException(err, "unhandled_route_error");
18352
18952
  return c.text("Internal Server Error", 500);
18353
18953
  });
18954
+ app.use("*", async (c, next) => {
18955
+ if (isSandboxEgressRequest(c.req.raw)) {
18956
+ return await ALL(c.req.raw);
18957
+ }
18958
+ await next();
18959
+ });
18354
18960
  app.get("/", () => GET3());
18355
18961
  app.get("/health", () => GET2());
18356
18962
  app.get("/api/info", () => GET());
@@ -18363,12 +18969,6 @@ async function createApp(options) {
18363
18969
  app.post("/api/internal/turn-resume", (c) => {
18364
18970
  return POST(c.req.raw, waitUntil);
18365
18971
  });
18366
- app.all("/api/internal/sandbox-egress/:egressId", (c) => {
18367
- return ALL(c.req.raw, c.req.param("egressId"));
18368
- });
18369
- app.all("/api/internal/sandbox-egress/:egressId/*", (c) => {
18370
- return ALL(c.req.raw, c.req.param("egressId"));
18371
- });
18372
18972
  app.post("/api/webhooks/:platform", (c) => {
18373
18973
  return POST2(c.req.raw, c.req.param("platform"), waitUntil);
18374
18974
  });