@sentry/junior 0.43.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  runNonInteractiveCommand,
32
32
  sandboxSkillDir,
33
33
  sandboxSkillFile
34
- } from "./chunk-YSXHRIWR.js";
34
+ } from "./chunk-QAMTCT2R.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;
@@ -8622,26 +8653,28 @@ function resolveSandboxEgressProviderForHost(host) {
8622
8653
  (entry) => entry.domains.some((domain) => matchesSandboxEgressDomain(host, domain))
8623
8654
  )?.provider;
8624
8655
  }
8625
- function proxyUrl(sandboxId) {
8656
+ function proxyUrl(egressId) {
8626
8657
  const baseUrl = resolveBaseUrl();
8627
8658
  if (!baseUrl) {
8628
8659
  return void 0;
8629
8660
  }
8630
8661
  const url = new URL(
8631
- `${SANDBOX_EGRESS_PROXY_PATH}/${encodeURIComponent(sandboxId)}`,
8662
+ `${SANDBOX_EGRESS_PROXY_PATH}/${encodeURIComponent(egressId)}`,
8632
8663
  baseUrl
8633
8664
  );
8634
8665
  return url.toString();
8635
8666
  }
8636
- function buildSandboxEgressNetworkPolicy(sandboxId) {
8637
- const forwardURL = proxyUrl(sandboxId);
8638
- if (!forwardURL) {
8639
- return void 0;
8640
- }
8667
+ function buildSandboxEgressNetworkPolicy(egressId) {
8641
8668
  const entries = providerEntries();
8642
8669
  if (entries.length === 0) {
8643
8670
  return void 0;
8644
8671
  }
8672
+ const forwardURL = proxyUrl(egressId);
8673
+ if (!forwardURL) {
8674
+ throw new Error(
8675
+ "Cannot determine base URL for sandbox credential egress (set JUNIOR_BASE_URL or deploy to Vercel)"
8676
+ );
8677
+ }
8645
8678
  const allow = {
8646
8679
  "*": []
8647
8680
  };
@@ -8671,11 +8704,11 @@ import { randomUUID as randomUUID3 } from "crypto";
8671
8704
  var SANDBOX_EGRESS_SESSION_PREFIX = "sandbox-egress-session";
8672
8705
  var SANDBOX_EGRESS_LEASE_PREFIX = "sandbox-egress-lease";
8673
8706
  var DEFAULT_SESSION_TTL_MS = 30 * 60 * 1e3;
8674
- function sessionKey2(sandboxId) {
8675
- return `${SANDBOX_EGRESS_SESSION_PREFIX}:${sandboxId}`;
8707
+ function sessionKey2(egressId) {
8708
+ return `${SANDBOX_EGRESS_SESSION_PREFIX}:${egressId}`;
8676
8709
  }
8677
- function leaseKey(sandboxId, provider, session) {
8678
- return `${SANDBOX_EGRESS_LEASE_PREFIX}:${sandboxId}:${provider}:${session.requesterId}:${session.activationId}`;
8710
+ function leaseKey(egressId, provider, session) {
8711
+ return `${SANDBOX_EGRESS_LEASE_PREFIX}:${egressId}:${provider}:${session.requesterId}:${session.activationId}`;
8679
8712
  }
8680
8713
  function parseSession(value) {
8681
8714
  if (!value || typeof value !== "object") {
@@ -8730,19 +8763,19 @@ async function upsertSandboxEgressSession(input) {
8730
8763
  expiresAtMs: now + ttlMs,
8731
8764
  activationId: randomUUID3()
8732
8765
  };
8733
- await state.set(sessionKey2(input.sandboxId), session, ttlMs);
8766
+ await state.set(sessionKey2(input.egressId), session, ttlMs);
8734
8767
  }
8735
- async function clearSandboxEgressSession(sandboxId) {
8768
+ async function clearSandboxEgressSession(egressId) {
8736
8769
  const state = getStateAdapter();
8737
8770
  await state.connect();
8738
- await state.delete(sessionKey2(sandboxId));
8771
+ await state.delete(sessionKey2(egressId));
8739
8772
  }
8740
- async function getSandboxEgressSession(sandboxId) {
8773
+ async function getSandboxEgressSession(egressId) {
8741
8774
  const state = getStateAdapter();
8742
8775
  await state.connect();
8743
- return parseSession(await state.get(sessionKey2(sandboxId)));
8776
+ return parseSession(await state.get(sessionKey2(egressId)));
8744
8777
  }
8745
- async function setSandboxEgressCredentialLease(sandboxId, session, lease) {
8778
+ async function setSandboxEgressCredentialLease(egressId, session, lease) {
8746
8779
  const leaseExpiresAtMs = Date.parse(lease.expiresAt);
8747
8780
  if (!Number.isFinite(leaseExpiresAtMs) || leaseExpiresAtMs <= Date.now()) {
8748
8781
  return;
@@ -8753,17 +8786,17 @@ async function setSandboxEgressCredentialLease(sandboxId, session, lease) {
8753
8786
  );
8754
8787
  const state = getStateAdapter();
8755
8788
  await state.connect();
8756
- await state.set(leaseKey(sandboxId, lease.provider, session), lease, ttlMs);
8789
+ await state.set(leaseKey(egressId, lease.provider, session), lease, ttlMs);
8757
8790
  }
8758
- async function getSandboxEgressCredentialLease(sandboxId, provider, session) {
8791
+ async function getSandboxEgressCredentialLease(egressId, provider, session) {
8759
8792
  const state = getStateAdapter();
8760
8793
  await state.connect();
8761
- return parseLease(await state.get(leaseKey(sandboxId, provider, session)));
8794
+ return parseLease(await state.get(leaseKey(egressId, provider, session)));
8762
8795
  }
8763
- async function clearSandboxEgressCredentialLease(sandboxId, provider, session) {
8796
+ async function clearSandboxEgressCredentialLease(egressId, provider, session) {
8764
8797
  const state = getStateAdapter();
8765
8798
  await state.connect();
8766
- await state.delete(leaseKey(sandboxId, provider, session));
8799
+ await state.delete(leaseKey(egressId, provider, session));
8767
8800
  }
8768
8801
 
8769
8802
  // src/chat/sandbox/http-error-details.ts
@@ -9620,10 +9653,10 @@ function createSandboxSessionManager(options) {
9620
9653
  appliedNetworkPolicyKey = void 0;
9621
9654
  };
9622
9655
  const createSandboxName = () => `${SANDBOX_NAME_PREFIX}${randomUUID4()}`;
9623
- const rememberNetworkPolicy = (networkPolicy) => {
9624
- appliedNetworkPolicyKey = networkPolicy ? JSON.stringify(networkPolicy) : void 0;
9656
+ const preflightNetworkPolicy = (sandboxName) => {
9657
+ return options?.createNetworkPolicy?.(sandboxName);
9625
9658
  };
9626
- const rememberSandbox = async (nextSandbox, rememberOptions) => {
9659
+ const rememberSandbox = async (nextSandbox) => {
9627
9660
  sandbox = nextSandbox;
9628
9661
  sandboxIdHint = nextSandbox.sandboxId;
9629
9662
  toolExecutors = void 0;
@@ -9632,11 +9665,6 @@ function createSandboxSessionManager(options) {
9632
9665
  ...dependencyProfileHash ? { sandboxDependencyProfileHash: dependencyProfileHash } : {}
9633
9666
  };
9634
9667
  await options?.onSandboxAcquired?.(acquired);
9635
- if (rememberOptions?.recordNetworkPolicy) {
9636
- rememberNetworkPolicy(
9637
- options?.createNetworkPolicy?.(nextSandbox.sandboxId)
9638
- );
9639
- }
9640
9668
  return nextSandbox;
9641
9669
  };
9642
9670
  const failSetup = (error) => {
@@ -9653,7 +9681,7 @@ function createSandboxSessionManager(options) {
9653
9681
  };
9654
9682
  const refreshNetworkPolicy = async (targetSandbox) => {
9655
9683
  const networkPolicy = options?.createNetworkPolicy?.(
9656
- targetSandbox.sandboxId
9684
+ targetSandbox.sandboxEgressId
9657
9685
  );
9658
9686
  if (!networkPolicy) {
9659
9687
  return;
@@ -9709,7 +9737,7 @@ function createSandboxSessionManager(options) {
9709
9737
  const createSandboxFromSnapshot = async (snapshotId, sandboxCredentials, initialSandboxName) => {
9710
9738
  for (let attempt = 0; attempt < SNAPSHOT_BOOT_RETRY_COUNT; attempt += 1) {
9711
9739
  const sandboxName = attempt === 0 ? initialSandboxName : createSandboxName();
9712
- const networkPolicy = options?.createNetworkPolicy?.(sandboxName);
9740
+ const networkPolicy = preflightNetworkPolicy(sandboxName);
9713
9741
  try {
9714
9742
  return createSandboxInstance(
9715
9743
  await Sandbox.create({
@@ -9748,7 +9776,7 @@ function createSandboxSessionManager(options) {
9748
9776
  const createSandboxFromResolvedSnapshot = async (params) => {
9749
9777
  const { runtime, snapshot, sandboxCredentials, sandboxName } = params;
9750
9778
  if (!snapshot.snapshotId) {
9751
- const networkPolicy = options?.createNetworkPolicy?.(sandboxName);
9779
+ const networkPolicy = preflightNetworkPolicy(sandboxName);
9752
9780
  return createSandboxInstance(
9753
9781
  await Sandbox.create({
9754
9782
  timeout: timeoutMs,
@@ -9819,11 +9847,12 @@ function createSandboxSessionManager(options) {
9819
9847
  return failSetup(error);
9820
9848
  }
9821
9849
  try {
9850
+ await refreshNetworkPolicy(createdSandbox);
9822
9851
  await syncSkills(createdSandbox);
9823
9852
  } catch (error) {
9824
9853
  return failSetup(error);
9825
9854
  }
9826
- return await rememberSandbox(createdSandbox, { recordNetworkPolicy: true });
9855
+ return await rememberSandbox(createdSandbox);
9827
9856
  };
9828
9857
  const discardHintIfProfileChanged = () => {
9829
9858
  if (sandbox || !sandboxIdHint || dependencyProfileHash === options?.sandboxDependencyProfileHash) {
@@ -9976,7 +10005,8 @@ function createSandboxSessionManager(options) {
9976
10005
  }
9977
10006
  return {
9978
10007
  bash: async (input) => {
9979
- await options?.beforeCommand?.(activeSandboxId);
10008
+ const commandEgressId = sandboxInstance.sandboxEgressId;
10009
+ await options?.beforeCommand?.(commandEgressId);
9980
10010
  let timedOut = false;
9981
10011
  let timeoutId;
9982
10012
  let commandFinished = false;
@@ -9985,7 +10015,7 @@ function createSandboxSessionManager(options) {
9985
10015
  return;
9986
10016
  }
9987
10017
  commandFinished = true;
9988
- await options?.afterCommand?.(activeSandboxId);
10018
+ await options?.afterCommand?.(commandEgressId);
9989
10019
  };
9990
10020
  const finishCommandBestEffort = async () => {
9991
10021
  try {
@@ -10126,15 +10156,15 @@ function createSandboxExecutor(options) {
10126
10156
  let referenceFiles = [];
10127
10157
  const traceContext = options?.traceContext ?? {};
10128
10158
  const credentialEgress = options?.credentialEgress;
10129
- const syncSandboxEgressSession = credentialEgress ? async (sandboxId) => {
10159
+ const syncSandboxEgressSession = credentialEgress ? async (egressId) => {
10130
10160
  await upsertSandboxEgressSession({
10131
- sandboxId,
10161
+ egressId,
10132
10162
  requesterId: credentialEgress.requesterId,
10133
10163
  ttlMs: options?.timeoutMs
10134
10164
  });
10135
10165
  } : void 0;
10136
- const clearSandboxEgressSessionForCommand = credentialEgress ? async (sandboxId) => {
10137
- await clearSandboxEgressSession(sandboxId);
10166
+ const clearSandboxEgressSessionForCommand = credentialEgress ? async (egressId) => {
10167
+ await clearSandboxEgressSession(egressId);
10138
10168
  } : void 0;
10139
10169
  const sessionManager = createSandboxSessionManager({
10140
10170
  sandboxId: options?.sandboxId,
@@ -10621,77 +10651,468 @@ function normalizeToolResult(result, isSandboxResult) {
10621
10651
  };
10622
10652
  }
10623
10653
 
10624
- // src/chat/tools/execution/tool-error-handler.ts
10625
- function getToolErrorAttributes(error) {
10626
- if (!(error instanceof SlackActionError)) {
10627
- return {};
10654
+ // src/chat/credentials/unlink-provider.ts
10655
+ async function unlinkProvider(userId, provider, userTokenStore) {
10656
+ await Promise.all([
10657
+ userTokenStore.delete(userId, provider),
10658
+ deleteMcpStoredOAuthCredentials(userId, provider),
10659
+ deleteMcpServerSessionId(userId, provider),
10660
+ deleteMcpAuthSessionsForUserProvider(userId, provider)
10661
+ ]);
10662
+ }
10663
+
10664
+ // src/chat/state/turn-session-store.ts
10665
+ var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
10666
+ var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
10667
+ function agentTurnSessionKey(conversationId, sessionId) {
10668
+ return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
10669
+ }
10670
+ function parseAgentTurnSessionCheckpoint(value) {
10671
+ if (typeof value !== "string") {
10672
+ return void 0;
10628
10673
  }
10629
- return {
10630
- "app.slack.error_code": error.code,
10631
- ...error.apiError ? { "app.slack.api_error": error.apiError } : {},
10632
- ...error.detail ? { "app.slack.detail": error.detail } : {},
10633
- ...error.detailLine !== void 0 ? { "app.slack.detail_line": error.detailLine } : {},
10634
- ...error.detailRule ? { "app.slack.detail_rule": error.detailRule } : {}
10674
+ try {
10675
+ const parsed = JSON.parse(value);
10676
+ if (!isRecord(parsed)) {
10677
+ return void 0;
10678
+ }
10679
+ const status = parsed.state;
10680
+ if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed" && status !== "superseded") {
10681
+ return void 0;
10682
+ }
10683
+ const conversationId = parsed.conversationId;
10684
+ const sessionId = parsed.sessionId;
10685
+ const sliceId = parsed.sliceId;
10686
+ const checkpointVersion = parsed.checkpointVersion;
10687
+ const updatedAtMs = parsed.updatedAtMs;
10688
+ if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
10689
+ return void 0;
10690
+ }
10691
+ return {
10692
+ checkpointVersion,
10693
+ conversationId,
10694
+ sessionId,
10695
+ sliceId,
10696
+ state: status,
10697
+ updatedAtMs,
10698
+ piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
10699
+ ...Array.isArray(parsed.loadedSkillNames) ? {
10700
+ loadedSkillNames: parsed.loadedSkillNames.filter(
10701
+ (value2) => typeof value2 === "string"
10702
+ )
10703
+ } : {},
10704
+ ...parsed.resumeReason === "timeout" || parsed.resumeReason === "auth" ? { resumeReason: parsed.resumeReason } : {},
10705
+ ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
10706
+ ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
10707
+ };
10708
+ } catch {
10709
+ return void 0;
10710
+ }
10711
+ }
10712
+ async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
10713
+ const stateAdapter = getStateAdapter();
10714
+ await stateAdapter.connect();
10715
+ const value = await stateAdapter.get(
10716
+ agentTurnSessionKey(conversationId, sessionId)
10717
+ );
10718
+ return parseAgentTurnSessionCheckpoint(value);
10719
+ }
10720
+ async function upsertAgentTurnSessionCheckpoint(args) {
10721
+ const stateAdapter = getStateAdapter();
10722
+ await stateAdapter.connect();
10723
+ const existing = await getAgentTurnSessionCheckpoint(
10724
+ args.conversationId,
10725
+ args.sessionId
10726
+ );
10727
+ const checkpoint = {
10728
+ checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
10729
+ conversationId: args.conversationId,
10730
+ sessionId: args.sessionId,
10731
+ sliceId: args.sliceId,
10732
+ state: args.state,
10733
+ updatedAtMs: Date.now(),
10734
+ piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
10735
+ ...Array.isArray(args.loadedSkillNames) ? {
10736
+ loadedSkillNames: args.loadedSkillNames.filter(
10737
+ (value) => typeof value === "string"
10738
+ )
10739
+ } : {},
10740
+ ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
10741
+ ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
10742
+ ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
10635
10743
  };
10744
+ const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
10745
+ await stateAdapter.set(
10746
+ agentTurnSessionKey(args.conversationId, args.sessionId),
10747
+ JSON.stringify(checkpoint),
10748
+ ttlMs
10749
+ );
10750
+ return checkpoint;
10636
10751
  }
10637
- function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, traceContext) {
10638
- const errorType = getMcpAwareErrorType(error, "tool_execution_error");
10639
- const errorMessage = getMcpAwareErrorMessage(error);
10640
- setSpanAttributes({
10641
- "error.type": errorType
10752
+ async function supersedeAgentTurnSessionCheckpoint(args) {
10753
+ const existing = await getAgentTurnSessionCheckpoint(
10754
+ args.conversationId,
10755
+ args.sessionId
10756
+ );
10757
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "superseded") {
10758
+ return void 0;
10759
+ }
10760
+ return await upsertAgentTurnSessionCheckpoint({
10761
+ conversationId: existing.conversationId,
10762
+ sessionId: existing.sessionId,
10763
+ sliceId: existing.sliceId,
10764
+ state: "superseded",
10765
+ piMessages: existing.piMessages,
10766
+ loadedSkillNames: existing.loadedSkillNames,
10767
+ resumeReason: existing.resumeReason,
10768
+ resumedFromSliceId: existing.resumedFromSliceId,
10769
+ errorMessage: args.errorMessage ?? existing.errorMessage
10642
10770
  });
10643
- if (shouldTrace) {
10644
- logWarn(
10645
- "agent_tool_call_failed",
10646
- traceContext,
10647
- {
10648
- "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
10649
- "gen_ai.operation.name": "execute_tool",
10650
- "gen_ai.tool.name": toolName,
10651
- ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
10652
- "error.type": errorType,
10653
- "exception.message": errorMessage
10654
- },
10655
- "Agent tool call failed"
10656
- );
10771
+ }
10772
+
10773
+ // src/chat/services/pending-auth.ts
10774
+ var AUTH_LINK_REUSE_WINDOW_MS = 10 * 60 * 1e3;
10775
+ function canReusePendingAuthLink(args) {
10776
+ const { pendingAuth } = args;
10777
+ if (!pendingAuth) {
10778
+ return false;
10657
10779
  }
10658
- if (!(error instanceof McpToolError)) {
10659
- logException(
10660
- error,
10661
- "agent_tool_call_failed",
10662
- {},
10663
- {
10664
- "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
10665
- "gen_ai.operation.name": "execute_tool",
10666
- "gen_ai.tool.name": toolName,
10667
- ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
10668
- ...getToolErrorAttributes(error)
10669
- },
10670
- "Agent tool call failed"
10671
- );
10780
+ return pendingAuth.kind === args.kind && pendingAuth.provider === args.provider && pendingAuth.requesterId === args.requesterId && pendingAuth.linkSentAtMs + AUTH_LINK_REUSE_WINDOW_MS > (args.nowMs ?? Date.now());
10781
+ }
10782
+ function getConversationPendingAuth(args) {
10783
+ const pendingAuth = args.conversation.processing.pendingAuth;
10784
+ if (!pendingAuth) {
10785
+ return void 0;
10672
10786
  }
10673
- throw error;
10787
+ if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId) {
10788
+ return void 0;
10789
+ }
10790
+ return pendingAuth;
10791
+ }
10792
+ function clearPendingAuth(conversation, sessionId) {
10793
+ if (!conversation.processing.pendingAuth) {
10794
+ return;
10795
+ }
10796
+ if (sessionId && conversation.processing.pendingAuth.sessionId !== sessionId) {
10797
+ return;
10798
+ }
10799
+ conversation.processing.pendingAuth = void 0;
10800
+ }
10801
+ async function applyPendingAuthUpdate(args) {
10802
+ const previousPendingAuth = args.conversation.processing.pendingAuth;
10803
+ args.conversation.processing.pendingAuth = args.nextPendingAuth;
10804
+ if (previousPendingAuth && previousPendingAuth.sessionId !== args.nextPendingAuth.sessionId && args.conversationId) {
10805
+ await supersedeAgentTurnSessionCheckpoint({
10806
+ conversationId: args.conversationId,
10807
+ sessionId: previousPendingAuth.sessionId,
10808
+ errorMessage: "Superseded by a newer auth-blocked request in the same conversation."
10809
+ });
10810
+ }
10811
+ }
10812
+ function isPendingAuthLatestRequest(conversation, pendingAuth) {
10813
+ for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
10814
+ const message = conversation.messages[index];
10815
+ if (message?.role !== "user") {
10816
+ continue;
10817
+ }
10818
+ return buildDeterministicTurnId(message.id) === pendingAuth.sessionId;
10819
+ }
10820
+ return false;
10674
10821
  }
10675
10822
 
10676
- // src/chat/tools/agent-tools.ts
10677
- function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, pluginAuthOrchestration, onToolCall) {
10678
- const shouldTrace = shouldEmitDevAgentTrace();
10679
- return Object.entries(tools).map(([toolName, toolDef]) => ({
10680
- name: toolName,
10681
- label: toolName,
10682
- description: toolDef.description,
10683
- parameters: toolDef.inputSchema,
10684
- prepareArguments: toolDef.prepareArguments,
10685
- executionMode: toolDef.executionMode,
10686
- execute: async (toolCallId, params) => {
10687
- const normalizedToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : void 0;
10688
- const toolArgumentsAttribute = serializeGenAiAttribute(params);
10689
- if (toolName === "reportProgress") {
10690
- const status = buildReportedProgressStatus(params);
10691
- if (status) {
10692
- await onStatus?.(status);
10693
- }
10694
- }
10823
+ // src/chat/services/plugin-auth-orchestration.ts
10824
+ var PluginAuthorizationPauseError = class extends AuthorizationPauseError {
10825
+ constructor(provider, disposition) {
10826
+ super("plugin", provider, disposition);
10827
+ }
10828
+ };
10829
+ var PluginCredentialFailureError = class extends Error {
10830
+ provider;
10831
+ constructor(provider, message) {
10832
+ super(message);
10833
+ this.name = "PluginCredentialFailureError";
10834
+ this.provider = provider;
10835
+ }
10836
+ };
10837
+ function isCommandAuthFailure(details) {
10838
+ if (!details || typeof details !== "object") {
10839
+ return false;
10840
+ }
10841
+ const result = details;
10842
+ if (typeof result.exit_code !== "number" || result.exit_code === 0) {
10843
+ return false;
10844
+ }
10845
+ const text = `${typeof result.stdout === "string" ? result.stdout : ""}
10846
+ ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
10847
+ if (!text.trim()) {
10848
+ return false;
10849
+ }
10850
+ return [
10851
+ /\bjunior-auth-required\b/,
10852
+ /\b401\b/,
10853
+ /\bunauthorized\b/,
10854
+ /\bbad credentials\b/,
10855
+ /\binvalid token\b/,
10856
+ /\bgithub_token\b.*\binvalid\b/,
10857
+ /\btoken (?:expired|revoked)\b/,
10858
+ /\bexpired token\b/,
10859
+ /\bmissing scopes?\b/,
10860
+ /\binsufficient scope\b/,
10861
+ /\binvalid grant\b/,
10862
+ /\breauthoriz/
10863
+ ].some((pattern) => pattern.test(text));
10864
+ }
10865
+ function commandText(details) {
10866
+ if (!details || typeof details !== "object") {
10867
+ return "";
10868
+ }
10869
+ const result = details;
10870
+ return `${typeof result.stdout === "string" ? result.stdout : ""}
10871
+ ${typeof result.stderr === "string" ? result.stderr : ""}`;
10872
+ }
10873
+ function isGitHubSmartHttpAuthFailure(provider, command, details) {
10874
+ if (provider !== "github" || !/^\s*(?:gh|git)\b/i.test(command)) {
10875
+ return false;
10876
+ }
10877
+ const text = commandText(details).toLowerCase();
10878
+ return /\bgzip:\s*invalid header\b/.test(text);
10879
+ }
10880
+ function explicitAuthRequiredProvider(details) {
10881
+ const match = /\bjunior-auth-required\s+provider=([a-z0-9-]+)\b/.exec(
10882
+ commandText(details).toLowerCase()
10883
+ );
10884
+ return match?.[1];
10885
+ }
10886
+ function registeredProviderNames() {
10887
+ const providers = /* @__PURE__ */ new Set();
10888
+ for (const plugin of getPluginProviders()) {
10889
+ const domains = [
10890
+ ...plugin.manifest.credentials?.domains ?? [],
10891
+ ...plugin.manifest.domains ?? []
10892
+ ];
10893
+ if (domains.length > 0) {
10894
+ providers.add(plugin.manifest.name);
10895
+ }
10896
+ }
10897
+ return [...providers].sort((left, right) => left.localeCompare(right));
10898
+ }
10899
+ function commandTargetsProvider(provider, command, details) {
10900
+ const normalizedCommand = command.trim().toLowerCase();
10901
+ if (!normalizedCommand) {
10902
+ return false;
10903
+ }
10904
+ if (provider === "github" && /^(gh|git)\b/.test(normalizedCommand)) {
10905
+ return true;
10906
+ }
10907
+ const plugin = getPluginDefinition(provider);
10908
+ const candidates = /* @__PURE__ */ new Set([provider.toLowerCase()]);
10909
+ const manifest = plugin?.manifest;
10910
+ const credentials = manifest?.credentials;
10911
+ if (credentials) {
10912
+ candidates.add(credentials.authTokenEnv.toLowerCase());
10913
+ for (const domain of credentials.domains) {
10914
+ candidates.add(domain.toLowerCase());
10915
+ }
10916
+ }
10917
+ for (const domain of manifest?.domains ?? []) {
10918
+ candidates.add(domain.toLowerCase());
10919
+ }
10920
+ const combinedText = `${normalizedCommand}
10921
+ ${commandText(details).toLowerCase()}`;
10922
+ return [...candidates].some((candidate) => combinedText.includes(candidate));
10923
+ }
10924
+ function formatCommand(command) {
10925
+ const collapsed = command.replace(/\s+/g, " ").trim();
10926
+ return collapsed.length > 160 ? `${collapsed.slice(0, 157)}...` : collapsed;
10927
+ }
10928
+ function buildCredentialFailureError(provider, command) {
10929
+ const providerLabel = provider === "github" ? "GitHub" : formatProviderLabel(provider);
10930
+ const plugin = getPluginDefinition(provider);
10931
+ const credentialType = plugin?.manifest.credentials?.type;
10932
+ const commandSummary = formatCommand(command);
10933
+ const remediation = provider === "github" && credentialType === "github-app" ? "Verify the GitHub App installation covers the target repository and the host GitHub App environment variables are current." : `Verify the ${providerLabel} provider credentials before retrying.`;
10934
+ return new PluginCredentialFailureError(
10935
+ provider,
10936
+ `${providerLabel} credentials were rejected while running \`${commandSummary}\`. ${remediation}`
10937
+ );
10938
+ }
10939
+ function createPluginAuthOrchestration(deps, abortAgent) {
10940
+ let pendingPause;
10941
+ const startAuthorizationPause = async (provider, activeSkill, options) => {
10942
+ if (pendingPause) {
10943
+ throw pendingPause;
10944
+ }
10945
+ if (!deps.requesterId || !getPluginOAuthConfig(provider)) {
10946
+ throw new Error(`Cannot start plugin authorization for ${provider}`);
10947
+ }
10948
+ const providerLabel = formatProviderLabel(provider);
10949
+ const reusingPendingLink = canReusePendingAuthLink({
10950
+ pendingAuth: deps.currentPendingAuth,
10951
+ kind: "plugin",
10952
+ provider,
10953
+ requesterId: deps.requesterId
10954
+ });
10955
+ if (!reusingPendingLink) {
10956
+ const oauthResult = await startOAuthFlow(provider, {
10957
+ requesterId: deps.requesterId,
10958
+ channelId: deps.channelId,
10959
+ threadTs: deps.threadTs,
10960
+ userMessage: deps.userMessage,
10961
+ channelConfiguration: deps.channelConfiguration,
10962
+ activeSkillName: activeSkill?.name ?? void 0,
10963
+ resumeConversationId: deps.conversationId,
10964
+ resumeSessionId: deps.sessionId
10965
+ });
10966
+ if (!oauthResult.ok) {
10967
+ throw new Error(oauthResult.error);
10968
+ }
10969
+ if (!oauthResult.delivery) {
10970
+ throw new Error(
10971
+ `I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try again.`
10972
+ );
10973
+ }
10974
+ }
10975
+ if (options?.unlinkExistingProvider && deps.requesterId && deps.userTokenStore) {
10976
+ await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
10977
+ }
10978
+ if (deps.sessionId) {
10979
+ await deps.onPendingAuth?.({
10980
+ kind: "plugin",
10981
+ provider,
10982
+ requesterId: deps.requesterId,
10983
+ sessionId: deps.sessionId,
10984
+ linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
10985
+ });
10986
+ }
10987
+ pendingPause = new PluginAuthorizationPauseError(
10988
+ provider,
10989
+ reusingPendingLink ? "link_already_sent" : "link_sent"
10990
+ );
10991
+ abortAgent();
10992
+ throw pendingPause;
10993
+ };
10994
+ return {
10995
+ handleCommandFailure: async (input) => {
10996
+ const providers = registeredProviderNames();
10997
+ const explicitProvider = explicitAuthRequiredProvider(input.details);
10998
+ const provider = explicitProvider && providers.includes(explicitProvider) ? explicitProvider : providers.find(
10999
+ (availableProvider) => commandTargetsProvider(
11000
+ availableProvider,
11001
+ input.command,
11002
+ input.details
11003
+ )
11004
+ );
11005
+ if (!provider) {
11006
+ return;
11007
+ }
11008
+ const authFailure = isCommandAuthFailure(input.details) || isGitHubSmartHttpAuthFailure(provider, input.command, input.details);
11009
+ if (!authFailure) {
11010
+ return;
11011
+ }
11012
+ if (!deps.requesterId || !deps.userTokenStore) {
11013
+ throw buildCredentialFailureError(provider, input.command);
11014
+ }
11015
+ if (!getPluginOAuthConfig(provider)) {
11016
+ throw buildCredentialFailureError(provider, input.command);
11017
+ }
11018
+ await startAuthorizationPause(provider, input.activeSkill, {
11019
+ unlinkExistingProvider: true
11020
+ });
11021
+ },
11022
+ getPendingPause: () => pendingPause
11023
+ };
11024
+ }
11025
+
11026
+ // src/chat/tools/execution/tool-error-handler.ts
11027
+ function getToolErrorAttributes(error) {
11028
+ if (!(error instanceof SlackActionError)) {
11029
+ return {};
11030
+ }
11031
+ return {
11032
+ "app.slack.error_code": error.code,
11033
+ ...error.apiError ? { "app.slack.api_error": error.apiError } : {},
11034
+ ...error.detail ? { "app.slack.detail": error.detail } : {},
11035
+ ...error.detailLine !== void 0 ? { "app.slack.detail_line": error.detailLine } : {},
11036
+ ...error.detailRule ? { "app.slack.detail_rule": error.detailRule } : {}
11037
+ };
11038
+ }
11039
+ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, traceContext) {
11040
+ const errorType = getMcpAwareErrorType(error, "tool_execution_error");
11041
+ const errorMessage = getMcpAwareErrorMessage(error);
11042
+ setSpanAttributes({
11043
+ "error.type": errorType,
11044
+ ...error instanceof PluginCredentialFailureError ? { "app.credential.provider": error.provider } : {}
11045
+ });
11046
+ if (error instanceof PluginCredentialFailureError) {
11047
+ if (shouldTrace) {
11048
+ logInfo(
11049
+ "plugin_credential_rejected",
11050
+ traceContext,
11051
+ {
11052
+ "app.credential.provider": error.provider,
11053
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
11054
+ "gen_ai.operation.name": "execute_tool",
11055
+ "gen_ai.tool.name": toolName,
11056
+ ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
11057
+ "error.type": errorType
11058
+ },
11059
+ "Plugin credentials were rejected during tool execution"
11060
+ );
11061
+ }
11062
+ throw error;
11063
+ }
11064
+ if (shouldTrace) {
11065
+ logWarn(
11066
+ "agent_tool_call_failed",
11067
+ traceContext,
11068
+ {
11069
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
11070
+ "gen_ai.operation.name": "execute_tool",
11071
+ "gen_ai.tool.name": toolName,
11072
+ ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
11073
+ "error.type": errorType,
11074
+ "exception.message": errorMessage
11075
+ },
11076
+ "Agent tool call failed"
11077
+ );
11078
+ }
11079
+ if (!(error instanceof McpToolError)) {
11080
+ logException(
11081
+ error,
11082
+ "agent_tool_call_failed",
11083
+ {},
11084
+ {
11085
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
11086
+ "gen_ai.operation.name": "execute_tool",
11087
+ "gen_ai.tool.name": toolName,
11088
+ ...toolCallId ? { "gen_ai.tool.call.id": toolCallId } : {},
11089
+ ...getToolErrorAttributes(error)
11090
+ },
11091
+ "Agent tool call failed"
11092
+ );
11093
+ }
11094
+ throw error;
11095
+ }
11096
+
11097
+ // src/chat/tools/agent-tools.ts
11098
+ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor, pluginAuthOrchestration, onToolCall) {
11099
+ const shouldTrace = shouldEmitDevAgentTrace();
11100
+ return Object.entries(tools).map(([toolName, toolDef]) => ({
11101
+ name: toolName,
11102
+ label: toolName,
11103
+ description: toolDef.description,
11104
+ parameters: toolDef.inputSchema,
11105
+ prepareArguments: toolDef.prepareArguments,
11106
+ executionMode: toolDef.executionMode,
11107
+ execute: async (toolCallId, params) => {
11108
+ const normalizedToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : void 0;
11109
+ const toolArgumentsAttribute = serializeGenAiAttribute(params);
11110
+ if (toolName === "reportProgress") {
11111
+ const status = buildReportedProgressStatus(params);
11112
+ if (status) {
11113
+ await onStatus?.(status);
11114
+ }
11115
+ }
10695
11116
  return withSpan(
10696
11117
  `execute_tool ${toolName}`,
10697
11118
  "gen_ai.execute_tool",
@@ -10998,9 +11419,31 @@ var TURN_THINKING_LEVELS = [
10998
11419
  "high",
10999
11420
  "xhigh"
11000
11421
  ];
11422
+ var CONFIDENCE_LABELS = {
11423
+ low: 0.5,
11424
+ medium: CLASSIFIER_CONFIDENCE_THRESHOLD,
11425
+ high: 0.9
11426
+ };
11427
+ function coerceClassifierConfidence(value) {
11428
+ if (typeof value !== "string") {
11429
+ return value;
11430
+ }
11431
+ const trimmed = value.trim().toLowerCase();
11432
+ if (!trimmed) {
11433
+ return value;
11434
+ }
11435
+ const numeric = Number.parseFloat(trimmed);
11436
+ if (Number.isFinite(numeric)) {
11437
+ return numeric;
11438
+ }
11439
+ return CONFIDENCE_LABELS[trimmed] ?? value;
11440
+ }
11001
11441
  var turnExecutionProfileSchema = z.object({
11002
11442
  thinking_level: z.enum(TURN_THINKING_LEVELS),
11003
- confidence: z.number().min(0).max(1),
11443
+ confidence: z.preprocess(
11444
+ coerceClassifierConfidence,
11445
+ z.number().min(0).max(1)
11446
+ ),
11004
11447
  reason: z.string().min(1)
11005
11448
  });
11006
11449
  var DEFAULT_THINKING_LEVEL = "medium";
@@ -11045,7 +11488,8 @@ function buildClassifierSystemPrompt() {
11045
11488
  "",
11046
11489
  "Classify based on the substance of the task, not the length of the current message. When the current instruction is a short affirmation (for example: 'go', 'do it', 'yes please', 'proceed') and the thread-background contains a pending task, classify the pending task \u2014 not the affirmation.",
11047
11490
  "",
11048
- "Return JSON only with thinking_level, confidence, and reason."
11491
+ "Return JSON only with thinking_level, confidence, and reason.",
11492
+ "confidence must be a number from 0 to 1, not a word label."
11049
11493
  ].join("\n");
11050
11494
  }
11051
11495
  function buildClassifierPrompt(args) {
@@ -11167,133 +11611,24 @@ async function classifyTurn(args) {
11167
11611
  };
11168
11612
  } catch {
11169
11613
  return {
11170
- thinkingLevel: DEFAULT_THINKING_LEVEL,
11171
- reason: "classifier_error_default"
11172
- };
11173
- }
11174
- }
11175
- function toAgentThinkingLevel(level) {
11176
- switch (level) {
11177
- case "none":
11178
- return "off";
11179
- case "low":
11180
- return "low";
11181
- case "medium":
11182
- return "medium";
11183
- case "high":
11184
- return "high";
11185
- case "xhigh":
11186
- return "xhigh";
11187
- }
11188
- }
11189
-
11190
- // src/chat/state/turn-session-store.ts
11191
- var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
11192
- var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
11193
- function agentTurnSessionKey(conversationId, sessionId) {
11194
- return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
11195
- }
11196
- function parseAgentTurnSessionCheckpoint(value) {
11197
- if (typeof value !== "string") {
11198
- return void 0;
11199
- }
11200
- try {
11201
- const parsed = JSON.parse(value);
11202
- if (!isRecord(parsed)) {
11203
- return void 0;
11204
- }
11205
- const status = parsed.state;
11206
- if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed" && status !== "superseded") {
11207
- return void 0;
11208
- }
11209
- const conversationId = parsed.conversationId;
11210
- const sessionId = parsed.sessionId;
11211
- const sliceId = parsed.sliceId;
11212
- const checkpointVersion = parsed.checkpointVersion;
11213
- const updatedAtMs = parsed.updatedAtMs;
11214
- if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
11215
- return void 0;
11216
- }
11217
- return {
11218
- checkpointVersion,
11219
- conversationId,
11220
- sessionId,
11221
- sliceId,
11222
- state: status,
11223
- updatedAtMs,
11224
- piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
11225
- ...Array.isArray(parsed.loadedSkillNames) ? {
11226
- loadedSkillNames: parsed.loadedSkillNames.filter(
11227
- (value2) => typeof value2 === "string"
11228
- )
11229
- } : {},
11230
- ...parsed.resumeReason === "timeout" || parsed.resumeReason === "auth" ? { resumeReason: parsed.resumeReason } : {},
11231
- ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
11232
- ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
11233
- };
11234
- } catch {
11235
- return void 0;
11236
- }
11237
- }
11238
- async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
11239
- const stateAdapter = getStateAdapter();
11240
- await stateAdapter.connect();
11241
- const value = await stateAdapter.get(
11242
- agentTurnSessionKey(conversationId, sessionId)
11243
- );
11244
- return parseAgentTurnSessionCheckpoint(value);
11245
- }
11246
- async function upsertAgentTurnSessionCheckpoint(args) {
11247
- const stateAdapter = getStateAdapter();
11248
- await stateAdapter.connect();
11249
- const existing = await getAgentTurnSessionCheckpoint(
11250
- args.conversationId,
11251
- args.sessionId
11252
- );
11253
- const checkpoint = {
11254
- checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
11255
- conversationId: args.conversationId,
11256
- sessionId: args.sessionId,
11257
- sliceId: args.sliceId,
11258
- state: args.state,
11259
- updatedAtMs: Date.now(),
11260
- piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
11261
- ...Array.isArray(args.loadedSkillNames) ? {
11262
- loadedSkillNames: args.loadedSkillNames.filter(
11263
- (value) => typeof value === "string"
11264
- )
11265
- } : {},
11266
- ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
11267
- ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
11268
- ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
11269
- };
11270
- const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
11271
- await stateAdapter.set(
11272
- agentTurnSessionKey(args.conversationId, args.sessionId),
11273
- JSON.stringify(checkpoint),
11274
- ttlMs
11275
- );
11276
- return checkpoint;
11277
- }
11278
- async function supersedeAgentTurnSessionCheckpoint(args) {
11279
- const existing = await getAgentTurnSessionCheckpoint(
11280
- args.conversationId,
11281
- args.sessionId
11282
- );
11283
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "superseded") {
11284
- return void 0;
11614
+ thinkingLevel: DEFAULT_THINKING_LEVEL,
11615
+ reason: "classifier_error_default"
11616
+ };
11617
+ }
11618
+ }
11619
+ function toAgentThinkingLevel(level) {
11620
+ switch (level) {
11621
+ case "none":
11622
+ return "off";
11623
+ case "low":
11624
+ return "low";
11625
+ case "medium":
11626
+ return "medium";
11627
+ case "high":
11628
+ return "high";
11629
+ case "xhigh":
11630
+ return "xhigh";
11285
11631
  }
11286
- return await upsertAgentTurnSessionCheckpoint({
11287
- conversationId: existing.conversationId,
11288
- sessionId: existing.sessionId,
11289
- sliceId: existing.sliceId,
11290
- state: "superseded",
11291
- piMessages: existing.piMessages,
11292
- loadedSkillNames: existing.loadedSkillNames,
11293
- resumeReason: existing.resumeReason,
11294
- resumedFromSliceId: existing.resumedFromSliceId,
11295
- errorMessage: args.errorMessage ?? existing.errorMessage
11296
- });
11297
11632
  }
11298
11633
 
11299
11634
  // src/chat/services/turn-checkpoint.ts
@@ -11409,320 +11744,94 @@ async function persistTimeoutCheckpoint(args) {
11409
11744
  }
11410
11745
  }
11411
11746
 
11412
- // src/chat/services/pending-auth.ts
11413
- var AUTH_LINK_REUSE_WINDOW_MS = 10 * 60 * 1e3;
11414
- function canReusePendingAuthLink(args) {
11415
- const { pendingAuth } = args;
11416
- if (!pendingAuth) {
11417
- return false;
11418
- }
11419
- return pendingAuth.kind === args.kind && pendingAuth.provider === args.provider && pendingAuth.requesterId === args.requesterId && pendingAuth.linkSentAtMs + AUTH_LINK_REUSE_WINDOW_MS > (args.nowMs ?? Date.now());
11420
- }
11421
- function getConversationPendingAuth(args) {
11422
- const pendingAuth = args.conversation.processing.pendingAuth;
11423
- if (!pendingAuth) {
11424
- return void 0;
11425
- }
11426
- if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId) {
11427
- return void 0;
11428
- }
11429
- return pendingAuth;
11430
- }
11431
- function clearPendingAuth(conversation, sessionId) {
11432
- if (!conversation.processing.pendingAuth) {
11433
- return;
11434
- }
11435
- if (sessionId && conversation.processing.pendingAuth.sessionId !== sessionId) {
11436
- return;
11437
- }
11438
- conversation.processing.pendingAuth = void 0;
11439
- }
11440
- async function applyPendingAuthUpdate(args) {
11441
- const previousPendingAuth = args.conversation.processing.pendingAuth;
11442
- args.conversation.processing.pendingAuth = args.nextPendingAuth;
11443
- if (previousPendingAuth && previousPendingAuth.sessionId !== args.nextPendingAuth.sessionId && args.conversationId) {
11444
- await supersedeAgentTurnSessionCheckpoint({
11445
- conversationId: args.conversationId,
11446
- sessionId: previousPendingAuth.sessionId,
11447
- errorMessage: "Superseded by a newer auth-blocked request in the same conversation."
11448
- });
11449
- }
11450
- }
11451
- function isPendingAuthLatestRequest(conversation, pendingAuth) {
11452
- for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
11453
- const message = conversation.messages[index];
11454
- if (message?.role !== "user") {
11455
- continue;
11456
- }
11457
- return buildDeterministicTurnId(message.id) === pendingAuth.sessionId;
11458
- }
11459
- return false;
11460
- }
11461
-
11462
11747
  // src/chat/services/mcp-auth-orchestration.ts
11463
11748
  var McpAuthorizationPauseError = class extends AuthorizationPauseError {
11464
11749
  constructor(provider, disposition) {
11465
11750
  super("mcp", provider, disposition);
11466
11751
  }
11467
- };
11468
- function createMcpAuthOrchestration(deps, abortAgent) {
11469
- let pendingPause;
11470
- const authSessionIdsByProvider = /* @__PURE__ */ new Map();
11471
- const authProviderFactory = async (plugin) => {
11472
- if (!deps.conversationId || !deps.sessionId || !deps.requesterId) {
11473
- return void 0;
11474
- }
11475
- const provider = await createMcpOAuthClientProvider({
11476
- provider: plugin.manifest.name,
11477
- conversationId: deps.conversationId,
11478
- sessionId: deps.sessionId,
11479
- userId: deps.requesterId,
11480
- userMessage: deps.userMessage,
11481
- ...deps.channelId ? { channelId: deps.channelId } : {},
11482
- ...deps.threadTs ? { threadTs: deps.threadTs } : {},
11483
- ...deps.toolChannelId ? { toolChannelId: deps.toolChannelId } : {},
11484
- configuration: deps.getConfiguration(),
11485
- artifactState: deps.getArtifactState()
11486
- });
11487
- authSessionIdsByProvider.set(plugin.manifest.name, provider.authSessionId);
11488
- return provider;
11489
- };
11490
- const onAuthorizationRequired = async (provider) => {
11491
- if (pendingPause) {
11492
- return true;
11493
- }
11494
- const authSessionId = authSessionIdsByProvider.get(provider);
11495
- if (!authSessionId || !deps.requesterId) {
11496
- throw new Error(
11497
- `Missing MCP auth session context for plugin "${provider}"`
11498
- );
11499
- }
11500
- const latestArtifactState = deps.getMergedArtifactState();
11501
- await patchMcpAuthSession(authSessionId, {
11502
- configuration: { ...deps.getConfiguration() },
11503
- artifactState: latestArtifactState,
11504
- toolChannelId: deps.toolChannelId ?? latestArtifactState.assistantContextChannelId ?? deps.channelId
11505
- });
11506
- const authSession = await getMcpAuthSession(authSessionId);
11507
- if (!authSession?.authorizationUrl) {
11508
- throw new Error(`Missing MCP authorization URL for plugin "${provider}"`);
11509
- }
11510
- const reusingPendingLink = canReusePendingAuthLink({
11511
- pendingAuth: deps.currentPendingAuth,
11512
- kind: "mcp",
11513
- provider,
11514
- requesterId: deps.requesterId
11515
- });
11516
- if (!reusingPendingLink) {
11517
- const delivery = await deliverPrivateMessage({
11518
- channelId: authSession.channelId,
11519
- threadTs: authSession.threadTs,
11520
- userId: authSession.userId,
11521
- text: `<${authSession.authorizationUrl}|Click here to link your ${formatProviderLabel(provider)} MCP access>. Once you've authorized, this thread will continue automatically.`
11522
- });
11523
- if (!delivery) {
11524
- throw new Error(
11525
- `Unable to deliver MCP authorization link for plugin "${provider}"`
11526
- );
11527
- }
11528
- } else {
11529
- await deleteMcpAuthSession(authSessionId);
11530
- }
11531
- if (deps.sessionId && deps.requesterId) {
11532
- await deps.onPendingAuth?.({
11533
- kind: "mcp",
11534
- provider,
11535
- requesterId: deps.requesterId,
11536
- sessionId: deps.sessionId,
11537
- linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
11538
- });
11539
- }
11540
- pendingPause = new McpAuthorizationPauseError(
11541
- provider,
11542
- reusingPendingLink ? "link_already_sent" : "link_sent"
11543
- );
11544
- abortAgent();
11545
- return true;
11546
- };
11547
- return {
11548
- authProviderFactory,
11549
- onAuthorizationRequired,
11550
- getPendingPause: () => pendingPause
11551
- };
11552
- }
11553
-
11554
- // src/chat/credentials/unlink-provider.ts
11555
- async function unlinkProvider(userId, provider, userTokenStore) {
11556
- await Promise.all([
11557
- userTokenStore.delete(userId, provider),
11558
- deleteMcpStoredOAuthCredentials(userId, provider),
11559
- deleteMcpServerSessionId(userId, provider),
11560
- deleteMcpAuthSessionsForUserProvider(userId, provider)
11561
- ]);
11562
- }
11563
-
11564
- // src/chat/services/plugin-auth-orchestration.ts
11565
- var PluginAuthorizationPauseError = class extends AuthorizationPauseError {
11566
- constructor(provider, disposition) {
11567
- super("plugin", provider, disposition);
11568
- }
11569
- };
11570
- function isCommandAuthFailure(details) {
11571
- if (!details || typeof details !== "object") {
11572
- return false;
11573
- }
11574
- const result = details;
11575
- if (typeof result.exit_code !== "number" || result.exit_code === 0) {
11576
- return false;
11577
- }
11578
- const text = `${typeof result.stdout === "string" ? result.stdout : ""}
11579
- ${typeof result.stderr === "string" ? result.stderr : ""}`.toLowerCase();
11580
- if (!text.trim()) {
11581
- return false;
11582
- }
11583
- return [
11584
- /\bjunior-auth-required\b/,
11585
- /\b401\b/,
11586
- /\bunauthorized\b/,
11587
- /\bbad credentials\b/,
11588
- /\binvalid token\b/,
11589
- /\btoken (?:expired|revoked)\b/,
11590
- /\bexpired token\b/,
11591
- /\bmissing scopes?\b/,
11592
- /\binsufficient scope\b/,
11593
- /\binvalid grant\b/,
11594
- /\breauthoriz/
11595
- ].some((pattern) => pattern.test(text));
11596
- }
11597
- function commandText(details) {
11598
- if (!details || typeof details !== "object") {
11599
- return "";
11600
- }
11601
- const result = details;
11602
- return `${typeof result.stdout === "string" ? result.stdout : ""}
11603
- ${typeof result.stderr === "string" ? result.stderr : ""}`;
11604
- }
11605
- function explicitAuthRequiredProvider(details) {
11606
- const match = /\bjunior-auth-required\s+provider=([a-z0-9-]+)\b/.exec(
11607
- commandText(details).toLowerCase()
11608
- );
11609
- return match?.[1];
11610
- }
11611
- function registeredProviderNames() {
11612
- const providers = /* @__PURE__ */ new Set();
11613
- for (const plugin of getPluginProviders()) {
11614
- const domains = [
11615
- ...plugin.manifest.credentials?.domains ?? [],
11616
- ...plugin.manifest.domains ?? []
11617
- ];
11618
- if (domains.length > 0) {
11619
- providers.add(plugin.manifest.name);
11620
- }
11621
- }
11622
- return [...providers].sort((left, right) => left.localeCompare(right));
11623
- }
11624
- function commandTargetsProvider(provider, command, details) {
11625
- const normalizedCommand = command.trim().toLowerCase();
11626
- if (!normalizedCommand) {
11627
- return false;
11628
- }
11629
- if (provider === "github" && /^(gh|git)\b/.test(normalizedCommand)) {
11630
- return true;
11631
- }
11632
- const plugin = getPluginDefinition(provider);
11633
- const candidates = /* @__PURE__ */ new Set([provider.toLowerCase()]);
11634
- const manifest = plugin?.manifest;
11635
- const credentials = manifest?.credentials;
11636
- if (credentials) {
11637
- candidates.add(credentials.authTokenEnv.toLowerCase());
11638
- for (const domain of credentials.domains) {
11639
- candidates.add(domain.toLowerCase());
11640
- }
11641
- }
11642
- for (const domain of manifest?.domains ?? []) {
11643
- candidates.add(domain.toLowerCase());
11644
- }
11645
- const combinedText = `${normalizedCommand}
11646
- ${commandText(details).toLowerCase()}`;
11647
- return [...candidates].some((candidate) => combinedText.includes(candidate));
11648
- }
11649
- function createPluginAuthOrchestration(deps, abortAgent) {
11752
+ };
11753
+ function createMcpAuthOrchestration(deps, abortAgent) {
11650
11754
  let pendingPause;
11651
- const startAuthorizationPause = async (provider, activeSkill, options) => {
11755
+ const authSessionIdsByProvider = /* @__PURE__ */ new Map();
11756
+ const authProviderFactory = async (plugin) => {
11757
+ if (!deps.conversationId || !deps.sessionId || !deps.requesterId) {
11758
+ return void 0;
11759
+ }
11760
+ const provider = await createMcpOAuthClientProvider({
11761
+ provider: plugin.manifest.name,
11762
+ conversationId: deps.conversationId,
11763
+ sessionId: deps.sessionId,
11764
+ userId: deps.requesterId,
11765
+ userMessage: deps.userMessage,
11766
+ ...deps.channelId ? { channelId: deps.channelId } : {},
11767
+ ...deps.threadTs ? { threadTs: deps.threadTs } : {},
11768
+ ...deps.toolChannelId ? { toolChannelId: deps.toolChannelId } : {},
11769
+ configuration: deps.getConfiguration(),
11770
+ artifactState: deps.getArtifactState()
11771
+ });
11772
+ authSessionIdsByProvider.set(plugin.manifest.name, provider.authSessionId);
11773
+ return provider;
11774
+ };
11775
+ const onAuthorizationRequired = async (provider) => {
11652
11776
  if (pendingPause) {
11653
- throw pendingPause;
11777
+ return true;
11654
11778
  }
11655
- if (!deps.requesterId || !getPluginOAuthConfig(provider)) {
11656
- throw new Error(`Cannot start plugin authorization for ${provider}`);
11779
+ const authSessionId = authSessionIdsByProvider.get(provider);
11780
+ if (!authSessionId || !deps.requesterId) {
11781
+ throw new Error(
11782
+ `Missing MCP auth session context for plugin "${provider}"`
11783
+ );
11784
+ }
11785
+ const latestArtifactState = deps.getMergedArtifactState();
11786
+ await patchMcpAuthSession(authSessionId, {
11787
+ configuration: { ...deps.getConfiguration() },
11788
+ artifactState: latestArtifactState,
11789
+ toolChannelId: deps.toolChannelId ?? latestArtifactState.assistantContextChannelId ?? deps.channelId
11790
+ });
11791
+ const authSession = await getMcpAuthSession(authSessionId);
11792
+ if (!authSession?.authorizationUrl) {
11793
+ throw new Error(`Missing MCP authorization URL for plugin "${provider}"`);
11657
11794
  }
11658
- const providerLabel = formatProviderLabel(provider);
11659
11795
  const reusingPendingLink = canReusePendingAuthLink({
11660
11796
  pendingAuth: deps.currentPendingAuth,
11661
- kind: "plugin",
11797
+ kind: "mcp",
11662
11798
  provider,
11663
11799
  requesterId: deps.requesterId
11664
11800
  });
11665
11801
  if (!reusingPendingLink) {
11666
- const oauthResult = await startOAuthFlow(provider, {
11667
- requesterId: deps.requesterId,
11668
- channelId: deps.channelId,
11669
- threadTs: deps.threadTs,
11670
- userMessage: deps.userMessage,
11671
- channelConfiguration: deps.channelConfiguration,
11672
- activeSkillName: activeSkill?.name ?? void 0,
11673
- resumeConversationId: deps.conversationId,
11674
- resumeSessionId: deps.sessionId
11802
+ const delivery = await deliverPrivateMessage({
11803
+ channelId: authSession.channelId,
11804
+ threadTs: authSession.threadTs,
11805
+ userId: authSession.userId,
11806
+ text: `<${authSession.authorizationUrl}|Click here to link your ${formatProviderLabel(provider)} MCP access>. Once you've authorized, this thread will continue automatically.`
11675
11807
  });
11676
- if (!oauthResult.ok) {
11677
- throw new Error(oauthResult.error);
11678
- }
11679
- if (!oauthResult.delivery) {
11808
+ if (!delivery) {
11680
11809
  throw new Error(
11681
- `I need to connect your ${providerLabel} account first, but I wasn't able to send you a private authorization link. Please send me a direct message and try again.`
11810
+ `Unable to deliver MCP authorization link for plugin "${provider}"`
11682
11811
  );
11683
11812
  }
11813
+ } else {
11814
+ await deleteMcpAuthSession(authSessionId);
11684
11815
  }
11685
- if (options?.unlinkExistingProvider && deps.requesterId && deps.userTokenStore) {
11686
- await unlinkProvider(deps.requesterId, provider, deps.userTokenStore);
11687
- }
11688
- if (deps.sessionId) {
11816
+ if (deps.sessionId && deps.requesterId) {
11689
11817
  await deps.onPendingAuth?.({
11690
- kind: "plugin",
11818
+ kind: "mcp",
11691
11819
  provider,
11692
11820
  requesterId: deps.requesterId,
11693
11821
  sessionId: deps.sessionId,
11694
11822
  linkSentAtMs: reusingPendingLink ? deps.currentPendingAuth.linkSentAtMs : Date.now()
11695
11823
  });
11696
11824
  }
11697
- pendingPause = new PluginAuthorizationPauseError(
11825
+ pendingPause = new McpAuthorizationPauseError(
11698
11826
  provider,
11699
11827
  reusingPendingLink ? "link_already_sent" : "link_sent"
11700
11828
  );
11701
11829
  abortAgent();
11702
- throw pendingPause;
11830
+ return true;
11703
11831
  };
11704
11832
  return {
11705
- handleCommandFailure: async (input) => {
11706
- const providers = registeredProviderNames();
11707
- const authFailure = isCommandAuthFailure(input.details);
11708
- if (!authFailure) {
11709
- return;
11710
- }
11711
- const explicitProvider = explicitAuthRequiredProvider(input.details);
11712
- const provider = explicitProvider && providers.includes(explicitProvider) ? explicitProvider : providers.find(
11713
- (availableProvider) => commandTargetsProvider(
11714
- availableProvider,
11715
- input.command,
11716
- input.details
11717
- )
11718
- );
11719
- if (!provider || !deps.requesterId || !deps.userTokenStore || !getPluginOAuthConfig(provider)) {
11720
- return;
11721
- }
11722
- await startAuthorizationPause(provider, input.activeSkill, {
11723
- unlinkExistingProvider: true
11724
- });
11725
- },
11833
+ authProviderFactory,
11834
+ onAuthorizationRequired,
11726
11835
  getPendingPause: () => pendingPause
11727
11836
  };
11728
11837
  }
@@ -12709,6 +12818,12 @@ function finalizeFailedTurnReply(args) {
12709
12818
  };
12710
12819
  }
12711
12820
 
12821
+ // src/chat/services/turn-continuation-response.ts
12822
+ var TURN_CONTINUATION_RESPONSE = "I'm still working on this in the background. I'll post the final response here when it finishes.";
12823
+ function buildTurnContinuationResponse() {
12824
+ return TURN_CONTINUATION_RESPONSE;
12825
+ }
12826
+
12712
12827
  // src/chat/slack/assistant-thread/status-render.ts
12713
12828
  var DEFAULT_STATUS_CONTEXTS = {
12714
12829
  thinking: "\u2026",
@@ -13454,6 +13569,25 @@ async function postResumeFailureReply(args) {
13454
13569
  throw error;
13455
13570
  }
13456
13571
  }
13572
+ async function postTurnContinuationNoticeBestEffort(args) {
13573
+ try {
13574
+ await postSlackMessage({
13575
+ channelId: args.resumeArgs.channelId,
13576
+ threadTs: args.resumeArgs.threadTs,
13577
+ text: buildTurnContinuationResponse()
13578
+ });
13579
+ } catch (error) {
13580
+ logException(
13581
+ error,
13582
+ "slack_turn_continuation_notice_post_failed",
13583
+ getResumeLogContext(args.resumeArgs, args.lockKey),
13584
+ {
13585
+ "app.slack.reply_stage": "thread_reply_turn_continuation_notice"
13586
+ },
13587
+ "Failed to post turn continuation notice"
13588
+ );
13589
+ }
13590
+ }
13457
13591
  async function handleResumeFailure(args) {
13458
13592
  const logContext = getResumeLogContext(args.resumeArgs, args.lockKey);
13459
13593
  const capturedEventId = logException(
@@ -13521,6 +13655,7 @@ async function resumeSlackTurn(args) {
13521
13655
  channelId: args.channelId,
13522
13656
  threadTs: args.threadTs
13523
13657
  });
13658
+ let deferredPauseKind;
13524
13659
  let deferredPauseHandler;
13525
13660
  let deferredFailureHandler;
13526
13661
  try {
@@ -13574,10 +13709,12 @@ async function resumeSlackTurn(args) {
13574
13709
  const onAuthPause = args.onAuthPause;
13575
13710
  const onTimeoutPause = args.onTimeoutPause;
13576
13711
  if ((isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) && onAuthPause) {
13712
+ deferredPauseKind = "auth";
13577
13713
  deferredPauseHandler = async () => {
13578
13714
  await onAuthPause(error);
13579
13715
  };
13580
13716
  } else if (isRetryableTurnError(error, "turn_timeout_resume") && onTimeoutPause) {
13717
+ deferredPauseKind = "timeout";
13581
13718
  deferredPauseHandler = async () => {
13582
13719
  await onTimeoutPause(error);
13583
13720
  };
@@ -13598,6 +13735,12 @@ async function resumeSlackTurn(args) {
13598
13735
  if (deferredPauseHandler) {
13599
13736
  try {
13600
13737
  await deferredPauseHandler();
13738
+ if (deferredPauseKind === "timeout") {
13739
+ await postTurnContinuationNoticeBestEffort({
13740
+ lockKey,
13741
+ resumeArgs: args
13742
+ });
13743
+ }
13601
13744
  return;
13602
13745
  } catch (pauseError) {
13603
13746
  await handleResumeFailure({
@@ -13669,6 +13812,20 @@ var MAX_TURN_TIMEOUT_RESUME_SLICE_ID = 5;
13669
13812
  function canScheduleTurnTimeoutResume(nextSliceId) {
13670
13813
  return typeof nextSliceId === "number" && nextSliceId > 1 && nextSliceId <= MAX_TURN_TIMEOUT_RESUME_SLICE_ID;
13671
13814
  }
13815
+ async function getAwaitingTurnContinuationRequest(args) {
13816
+ const checkpoint = await getAgentTurnSessionCheckpoint(
13817
+ args.conversationId,
13818
+ args.sessionId
13819
+ );
13820
+ if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "timeout" || !canScheduleTurnTimeoutResume(checkpoint.sliceId)) {
13821
+ return void 0;
13822
+ }
13823
+ return {
13824
+ conversationId: args.conversationId,
13825
+ sessionId: args.sessionId,
13826
+ expectedCheckpointVersion: checkpoint.checkpointVersion
13827
+ };
13828
+ }
13672
13829
  function getTurnTimeoutResumeSecret() {
13673
13830
  const explicit = process.env.JUNIOR_INTERNAL_RESUME_SECRET?.trim();
13674
13831
  if (explicit) {
@@ -14724,41 +14881,21 @@ async function getJwks(issuer) {
14724
14881
  });
14725
14882
  return jwks;
14726
14883
  }
14727
- function expectedVercelOidcAudience() {
14728
- const audience = process.env.VERCEL_OIDC_AUDIENCE?.trim();
14729
- if (!audience) {
14730
- throw new Error("VERCEL_OIDC_AUDIENCE is required for sandbox egress OIDC");
14731
- }
14732
- return audience;
14733
- }
14734
- function validateVercelSandboxOidcClaims(payload, sandboxId) {
14735
- const expectedTeamId = process.env.VERCEL_TEAM_ID?.trim();
14736
- const expectedProjectId = process.env.VERCEL_PROJECT_ID?.trim();
14737
- if (!expectedProjectId) {
14738
- throw new Error("VERCEL_PROJECT_ID is required for sandbox egress OIDC");
14739
- }
14740
- if (expectedTeamId && (typeof payload.owner_id !== "string" || payload.owner_id !== expectedTeamId)) {
14741
- throw new Error("Vercel OIDC token belongs to a different team");
14742
- }
14743
- if (typeof payload.project_id !== "string" || payload.project_id !== expectedProjectId) {
14744
- throw new Error("Vercel OIDC token belongs to a different project");
14745
- }
14746
- if (payload.sandbox_id !== sandboxId) {
14884
+ function validateSandboxClaim(payload, egressId) {
14885
+ if (payload.sandbox_id !== egressId) {
14747
14886
  throw new Error("Vercel OIDC token belongs to a different sandbox");
14748
14887
  }
14749
14888
  }
14750
- async function verifyVercelSandboxOidcToken(token, sandboxId) {
14889
+ async function verifyVercelSandboxOidcToken(token, egressId) {
14751
14890
  const unverified = decodeJwt(token);
14752
14891
  if (typeof unverified.iss !== "string") {
14753
14892
  throw new Error("Vercel OIDC token did not include an issuer");
14754
14893
  }
14755
- const audience = expectedVercelOidcAudience();
14756
14894
  const jwks = await getJwks(unverified.iss);
14757
14895
  const verified = await jwtVerify(token, jwks, {
14758
- issuer: unverified.iss,
14759
- audience
14896
+ issuer: unverified.iss
14760
14897
  });
14761
- validateVercelSandboxOidcClaims(verified.payload, sandboxId);
14898
+ validateSandboxClaim(verified.payload, egressId);
14762
14899
  return verified.payload;
14763
14900
  }
14764
14901
 
@@ -14785,6 +14922,10 @@ var PROXY_ONLY_HEADERS = /* @__PURE__ */ new Set([
14785
14922
  FORWARDED_SCHEME_HEADER,
14786
14923
  FORWARDED_PORT_HEADER
14787
14924
  ]);
14925
+ var DECODED_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
14926
+ "content-encoding",
14927
+ "content-length"
14928
+ ]);
14788
14929
  var AUTH_REJECTION_STATUS = /* @__PURE__ */ new Set([401, 403]);
14789
14930
  function jsonError(message, status) {
14790
14931
  return Response.json({ error: message }, { status });
@@ -14810,9 +14951,9 @@ function normalizePort(value) {
14810
14951
  const port = Number.parseInt(trimmed, 10);
14811
14952
  return port >= 1 && port <= 65535 ? trimmed : void 0;
14812
14953
  }
14813
- function upstreamPath(request, sandboxId) {
14954
+ function upstreamPath(request, egressId) {
14814
14955
  const url = new URL(request.url);
14815
- const prefix = `${ROUTE_PREFIX}/${encodeURIComponent(sandboxId)}`;
14956
+ const prefix = `${ROUTE_PREFIX}/${encodeURIComponent(egressId)}`;
14816
14957
  if (url.pathname === prefix) {
14817
14958
  return `/${url.search}`;
14818
14959
  }
@@ -14821,7 +14962,7 @@ function upstreamPath(request, sandboxId) {
14821
14962
  }
14822
14963
  return void 0;
14823
14964
  }
14824
- function buildUpstreamUrl(request, sandboxId) {
14965
+ function buildUpstreamUrl(request, egressId) {
14825
14966
  const forwardedHost = request.headers.get(FORWARDED_HOST_HEADER);
14826
14967
  if (!forwardedHost?.trim()) {
14827
14968
  return { ok: false, error: "Missing forwarded host" };
@@ -14843,7 +14984,7 @@ function buildUpstreamUrl(request, sandboxId) {
14843
14984
  if (forwardedPort && !port) {
14844
14985
  return { ok: false, error: "Invalid forwarded port" };
14845
14986
  }
14846
- const path11 = upstreamPath(request, sandboxId);
14987
+ const path11 = upstreamPath(request, egressId);
14847
14988
  if (!path11) {
14848
14989
  return { ok: false, error: "Invalid egress route" };
14849
14990
  }
@@ -14883,15 +15024,15 @@ function responseHeaders(upstream) {
14883
15024
  const headers = new Headers();
14884
15025
  upstream.headers.forEach((value, key) => {
14885
15026
  const normalized = key.toLowerCase();
14886
- if (!HOP_BY_HOP_HEADERS.has(normalized)) {
15027
+ if (!HOP_BY_HOP_HEADERS.has(normalized) && !DECODED_RESPONSE_HEADERS.has(normalized)) {
14887
15028
  headers.append(key, value);
14888
15029
  }
14889
15030
  });
14890
15031
  return headers;
14891
15032
  }
14892
- async function credentialLease(sandboxId, provider, session) {
15033
+ async function credentialLease(egressId, provider, session) {
14893
15034
  const cached = await getSandboxEgressCredentialLease(
14894
- sandboxId,
15035
+ egressId,
14895
15036
  provider,
14896
15037
  session
14897
15038
  );
@@ -14914,7 +15055,7 @@ async function credentialLease(sandboxId, provider, session) {
14914
15055
  expiresAt: lease.expiresAt,
14915
15056
  headerTransforms
14916
15057
  };
14917
- await setSandboxEgressCredentialLease(sandboxId, session, cachedLease);
15058
+ await setSandboxEgressCredentialLease(egressId, session, cachedLease);
14918
15059
  return cachedLease;
14919
15060
  }
14920
15061
  function hasTransformForHost(lease, host) {
@@ -14922,7 +15063,7 @@ function hasTransformForHost(lease, host) {
14922
15063
  (transform) => matchesSandboxEgressDomain(host, transform.domain)
14923
15064
  );
14924
15065
  }
14925
- async function proxySandboxEgressRequest(request, sandboxId, deps = {}) {
15066
+ async function proxySandboxEgressRequest(request, egressId, deps = {}) {
14926
15067
  const oidcToken = request.headers.get(OIDC_TOKEN_HEADER)?.trim();
14927
15068
  if (!oidcToken) {
14928
15069
  return jsonError("Missing Vercel Sandbox OIDC token", 401);
@@ -14930,7 +15071,7 @@ async function proxySandboxEgressRequest(request, sandboxId, deps = {}) {
14930
15071
  try {
14931
15072
  await (deps.verifyOidc ?? verifyVercelSandboxOidcToken)(
14932
15073
  oidcToken,
14933
- sandboxId
15074
+ egressId
14934
15075
  );
14935
15076
  } catch (error) {
14936
15077
  logWarn(
@@ -14943,7 +15084,7 @@ async function proxySandboxEgressRequest(request, sandboxId, deps = {}) {
14943
15084
  );
14944
15085
  return jsonError("Invalid Vercel Sandbox OIDC token", 401);
14945
15086
  }
14946
- const upstreamResult = buildUpstreamUrl(request, sandboxId);
15087
+ const upstreamResult = buildUpstreamUrl(request, egressId);
14947
15088
  if (!upstreamResult.ok) {
14948
15089
  return jsonError(upstreamResult.error, 400);
14949
15090
  }
@@ -14952,13 +15093,13 @@ async function proxySandboxEgressRequest(request, sandboxId, deps = {}) {
14952
15093
  if (!provider) {
14953
15094
  return jsonError("No provider owns forwarded host", 403);
14954
15095
  }
14955
- const session = await getSandboxEgressSession(sandboxId);
15096
+ const session = await getSandboxEgressSession(egressId);
14956
15097
  if (!session) {
14957
15098
  return jsonError("Sandbox egress session is not authorized", 403);
14958
15099
  }
14959
15100
  let lease;
14960
15101
  try {
14961
- lease = await credentialLease(sandboxId, provider, session);
15102
+ lease = await credentialLease(egressId, provider, session);
14962
15103
  } catch (error) {
14963
15104
  if (error instanceof CredentialUnavailableError) {
14964
15105
  return new Response(
@@ -14983,7 +15124,18 @@ ${error.message}`,
14983
15124
  redirect: "manual"
14984
15125
  });
14985
15126
  if (AUTH_REJECTION_STATUS.has(upstream.status)) {
14986
- await clearSandboxEgressCredentialLease(sandboxId, provider, session);
15127
+ logWarn(
15128
+ "sandbox_egress_upstream_auth_rejected",
15129
+ {},
15130
+ {
15131
+ "app.credential.provider": provider,
15132
+ "http.request.method": request.method,
15133
+ "http.response.status_code": upstream.status,
15134
+ "server.address": upstreamUrl.hostname
15135
+ },
15136
+ "Sandbox egress upstream auth rejected"
15137
+ );
15138
+ await clearSandboxEgressCredentialLease(egressId, provider, session);
14987
15139
  }
14988
15140
  return new Response(upstream.body, {
14989
15141
  status: upstream.status,
@@ -14993,8 +15145,8 @@ ${error.message}`,
14993
15145
  }
14994
15146
 
14995
15147
  // src/handlers/sandbox-egress-proxy.ts
14996
- async function ALL(request, sandboxId) {
14997
- return await proxySandboxEgressRequest(request, sandboxId);
15148
+ async function ALL(request, egressId) {
15149
+ return await proxySandboxEgressRequest(request, egressId);
14998
15150
  }
14999
15151
 
15000
15152
  // src/chat/slack/context.ts
@@ -15044,6 +15196,10 @@ function resolveSlackChannelIdFromMessage(message) {
15044
15196
  }
15045
15197
 
15046
15198
  // src/handlers/turn-resume.ts
15199
+ var TIMEOUT_RESUME_LOCK_RETRY_DELAYS_MS = [250, 1e3, 2e3];
15200
+ function sleep3(ms) {
15201
+ return new Promise((resolve) => setTimeout(resolve, ms));
15202
+ }
15047
15203
  async function persistCompletedReplyState2(args) {
15048
15204
  const currentState = await getPersistedThreadState(
15049
15205
  args.checkpoint.conversationId
@@ -15243,25 +15399,53 @@ async function resumeTimedOutTurn(payload) {
15243
15399
  }
15244
15400
  });
15245
15401
  }
15246
- async function POST(request, waitUntil) {
15247
- const payload = await verifyTurnTimeoutResumeRequest(request);
15248
- if (!payload) {
15249
- return new Response("Unauthorized", { status: 401 });
15250
- }
15251
- waitUntil(
15252
- () => resumeTimedOutTurn(payload).catch((error) => {
15253
- if (error instanceof ResumeTurnBusyError) {
15402
+ async function resumeTimedOutTurnWithLockRetry(payload) {
15403
+ for (const [attempt, delayMs] of [
15404
+ ...TIMEOUT_RESUME_LOCK_RETRY_DELAYS_MS,
15405
+ void 0
15406
+ ].entries()) {
15407
+ try {
15408
+ await resumeTimedOutTurn(payload);
15409
+ return;
15410
+ } catch (error) {
15411
+ if (!(error instanceof ResumeTurnBusyError)) {
15412
+ throw error;
15413
+ }
15414
+ if (typeof delayMs !== "number") {
15254
15415
  logWarn(
15255
15416
  "timeout_resume_lock_busy",
15256
15417
  {},
15257
15418
  {
15258
15419
  "app.ai.conversation_id": payload.conversationId,
15259
- "app.ai.session_id": payload.sessionId
15420
+ "app.ai.session_id": payload.sessionId,
15421
+ "app.ai.resume_lock_retry_count": attempt
15260
15422
  },
15261
- "Skipped timeout resume because another turn owns the thread lock"
15423
+ "Skipped timeout resume because another turn still owns the thread lock"
15262
15424
  );
15263
15425
  return;
15264
15426
  }
15427
+ logWarn(
15428
+ "timeout_resume_lock_busy_retrying",
15429
+ {},
15430
+ {
15431
+ "app.ai.conversation_id": payload.conversationId,
15432
+ "app.ai.session_id": payload.sessionId,
15433
+ "app.ai.resume_lock_retry_attempt": attempt + 1,
15434
+ "app.ai.resume_lock_retry_delay_ms": delayMs
15435
+ },
15436
+ "Timeout resume lock was busy; retrying"
15437
+ );
15438
+ await sleep3(delayMs);
15439
+ }
15440
+ }
15441
+ }
15442
+ async function POST(request, waitUntil) {
15443
+ const payload = await verifyTurnTimeoutResumeRequest(request);
15444
+ if (!payload) {
15445
+ return new Response("Unauthorized", { status: 401 });
15446
+ }
15447
+ waitUntil(
15448
+ () => resumeTimedOutTurnWithLockRetry(payload).catch((error) => {
15265
15449
  logException(
15266
15450
  error,
15267
15451
  "timeout_resume_handler_failed",
@@ -15625,21 +15809,162 @@ function getSlackErrorObservabilityAttributes(error) {
15625
15809
  if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
15626
15810
  attributes["app.slack.error_code"] = candidate.code;
15627
15811
  }
15628
- if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
15629
- attributes["app.slack.api_error"] = candidate.data.error;
15812
+ if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
15813
+ attributes["app.slack.api_error"] = candidate.data.error;
15814
+ }
15815
+ const requestId = getHeaderString(candidate.headers, "x-slack-req-id");
15816
+ if (requestId) {
15817
+ attributes["app.slack.request_id"] = requestId;
15818
+ }
15819
+ if (typeof candidate.statusCode === "number" && Number.isFinite(candidate.statusCode)) {
15820
+ attributes["http.response.status_code"] = candidate.statusCode;
15821
+ }
15822
+ return attributes;
15823
+ }
15824
+ function isSlackTitlePermissionError(error) {
15825
+ const code = getSlackApiErrorCode(error);
15826
+ return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
15827
+ }
15828
+
15829
+ // src/chat/runtime/thread-context.ts
15830
+ function escapeRegExp3(value) {
15831
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15832
+ }
15833
+ function stripLeadingBotMention(text, options = {}) {
15834
+ if (!text.trim()) return text;
15835
+ let next = text;
15836
+ if (options.stripLeadingSlackMentionToken) {
15837
+ next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
15838
+ }
15839
+ const mentionByNameRe = new RegExp(
15840
+ `^\\s*@${escapeRegExp3(botConfig.userName)}\\b[\\s,:-]*`,
15841
+ "i"
15842
+ );
15843
+ next = next.replace(mentionByNameRe, "").trim();
15844
+ const mentionByLabeledEntityRe = new RegExp(
15845
+ `^\\s*<@[^>|]+\\|${escapeRegExp3(botConfig.userName)}>[\\s,:-]*`,
15846
+ "i"
15847
+ );
15848
+ next = next.replace(mentionByLabeledEntityRe, "").trim();
15849
+ return next;
15850
+ }
15851
+ function getThreadId(thread, _message) {
15852
+ return toOptionalString(thread.id);
15853
+ }
15854
+ function getRunId(thread, message) {
15855
+ return toOptionalString(thread.runId) ?? toOptionalString(message.runId);
15856
+ }
15857
+ function getChannelId(thread, message) {
15858
+ return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
15859
+ }
15860
+ function getThreadTs(threadId) {
15861
+ return parseSlackThreadId(threadId)?.threadTs;
15862
+ }
15863
+ function getAssistantThreadContext(message) {
15864
+ const raw = message.raw;
15865
+ const rawRecord = raw && typeof raw === "object" ? raw : void 0;
15866
+ const channelId = toOptionalString(rawRecord?.channel);
15867
+ if (channelId) {
15868
+ const rawThreadTs = toOptionalString(rawRecord?.thread_ts);
15869
+ const threadTs = isDmChannel(channelId) ? rawThreadTs : rawThreadTs ?? toOptionalString(rawRecord?.ts);
15870
+ if (threadTs) {
15871
+ return { channelId, threadTs };
15872
+ }
15873
+ }
15874
+ const parsedThreadId = parseSlackThreadId(
15875
+ toOptionalString(message.threadId)
15876
+ );
15877
+ if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) {
15878
+ return void 0;
15879
+ }
15880
+ return parsedThreadId;
15881
+ }
15882
+ function getMessageTs(message) {
15883
+ const directTs = toOptionalString(
15884
+ message.ts
15885
+ );
15886
+ if (directTs) {
15887
+ return directTs;
15888
+ }
15889
+ const raw = message.raw;
15890
+ if (!raw || typeof raw !== "object") {
15891
+ return void 0;
15892
+ }
15893
+ const rawRecord = raw;
15894
+ return toOptionalString(rawRecord.ts) ?? toOptionalString(rawRecord.event_ts) ?? toOptionalString(rawRecord.message?.ts);
15895
+ }
15896
+
15897
+ // src/chat/runtime/processing-reaction.ts
15898
+ var PROCESSING_REACTION_EMOJI = "eyes";
15899
+ var noProcessingReaction = {
15900
+ keep: () => void 0,
15901
+ stop: async () => void 0
15902
+ };
15903
+ function isProcessingReactionEmoji(value) {
15904
+ return typeof value === "string" && normalizeSlackEmojiName(value) === PROCESSING_REACTION_EMOJI;
15905
+ }
15906
+ function shouldKeepProcessingReactionForToolInvocation(input) {
15907
+ return input.toolName === "slackMessageAddReaction" && isProcessingReactionEmoji(input.params.emoji);
15908
+ }
15909
+ async function startSlackProcessingReaction(args) {
15910
+ if (args.message.author.isMe) {
15911
+ return noProcessingReaction;
15630
15912
  }
15631
- const requestId = getHeaderString(candidate.headers, "x-slack-req-id");
15632
- if (requestId) {
15633
- attributes["app.slack.request_id"] = requestId;
15913
+ const channelId = getChannelId(args.thread, args.message);
15914
+ const messageTs = getMessageTs(args.message);
15915
+ if (!channelId || !messageTs) {
15916
+ return noProcessingReaction;
15634
15917
  }
15635
- if (typeof candidate.statusCode === "number" && Number.isFinite(candidate.statusCode)) {
15636
- attributes["http.response.status_code"] = candidate.statusCode;
15918
+ try {
15919
+ await addReactionToMessage({
15920
+ channelId,
15921
+ timestamp: messageTs,
15922
+ emoji: PROCESSING_REACTION_EMOJI
15923
+ });
15924
+ } catch (error) {
15925
+ args.logException(
15926
+ error,
15927
+ "slack_processing_reaction_add_failed",
15928
+ args.logContext,
15929
+ {
15930
+ "app.slack.action": "reactions.add",
15931
+ "messaging.message.id": messageTs,
15932
+ ...getSlackErrorObservabilityAttributes(error)
15933
+ },
15934
+ "Failed to add Slack processing reaction"
15935
+ );
15936
+ return noProcessingReaction;
15637
15937
  }
15638
- return attributes;
15639
- }
15640
- function isSlackTitlePermissionError(error) {
15641
- const code = getSlackApiErrorCode(error);
15642
- return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
15938
+ let shouldRemove = true;
15939
+ return {
15940
+ keep: () => {
15941
+ shouldRemove = false;
15942
+ },
15943
+ stop: async () => {
15944
+ if (!shouldRemove) {
15945
+ return;
15946
+ }
15947
+ try {
15948
+ await removeReactionFromMessage({
15949
+ channelId,
15950
+ timestamp: messageTs,
15951
+ emoji: PROCESSING_REACTION_EMOJI
15952
+ });
15953
+ } catch (error) {
15954
+ args.logException(
15955
+ error,
15956
+ "slack_processing_reaction_remove_failed",
15957
+ args.logContext,
15958
+ {
15959
+ "app.slack.action": "reactions.remove",
15960
+ "messaging.message.id": messageTs,
15961
+ ...getSlackErrorObservabilityAttributes(error)
15962
+ },
15963
+ "Failed to remove Slack processing reaction"
15964
+ );
15965
+ }
15966
+ }
15967
+ };
15643
15968
  }
15644
15969
 
15645
15970
  // src/chat/runtime/slack-runtime.ts
@@ -15667,6 +15992,14 @@ function buildLogContext(deps, args) {
15667
15992
  }
15668
15993
  function createSlackTurnRuntime(deps) {
15669
15994
  const logContext = (args) => buildLogContext(deps, args);
15995
+ const createToolInvocationHook = (processingReaction, hooks) => {
15996
+ return (invocation) => {
15997
+ if (shouldKeepProcessingReactionForToolInvocation(invocation)) {
15998
+ processingReaction.keep();
15999
+ }
16000
+ hooks?.onToolInvocation?.(invocation);
16001
+ };
16002
+ };
15670
16003
  const postFallbackErrorReplyWithLogging = async (args) => {
15671
16004
  try {
15672
16005
  await args.thread.post(buildTurnFailureResponse(args.eventId));
@@ -15720,6 +16053,7 @@ function createSlackTurnRuntime(deps) {
15720
16053
  };
15721
16054
  return {
15722
16055
  async handleNewMention(thread, message, hooks) {
16056
+ let processingReaction;
15723
16057
  try {
15724
16058
  const threadId = deps.getThreadId(thread, message);
15725
16059
  const channelId = deps.getChannelId(thread, message);
@@ -15731,11 +16065,22 @@ function createSlackTurnRuntime(deps) {
15731
16065
  requesterUserName: message.author.userName,
15732
16066
  runId
15733
16067
  });
16068
+ processingReaction = await startSlackProcessingReaction({
16069
+ thread,
16070
+ message,
16071
+ logException: deps.logException,
16072
+ logContext: context
16073
+ });
16074
+ const toolInvocationHook = createToolInvocationHook(
16075
+ processingReaction,
16076
+ hooks
16077
+ );
15734
16078
  await deps.withSpan("chat.turn", "chat.turn", context, async () => {
15735
16079
  await thread.subscribe();
15736
16080
  await deps.replyToThread(thread, message, {
15737
16081
  explicitMention: true,
15738
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost
16082
+ beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
16083
+ onToolInvocation: toolInvocationHook
15739
16084
  });
15740
16085
  });
15741
16086
  } catch (error) {
@@ -15776,113 +16121,123 @@ function createSlackTurnRuntime(deps) {
15776
16121
  postFailureEventName: "mention_handler_failure_reply_post_failed",
15777
16122
  postFailureBody: "Failed to post fallback error reply for mention handler"
15778
16123
  });
16124
+ } finally {
16125
+ await processingReaction?.stop();
15779
16126
  }
15780
16127
  },
15781
16128
  async handleSubscribedMessage(thread, message, hooks) {
16129
+ let processingReaction;
15782
16130
  try {
15783
16131
  const threadId = deps.getThreadId(thread, message);
15784
16132
  const channelId = deps.getChannelId(thread, message);
15785
16133
  const runId = deps.getRunId(thread, message);
15786
- await deps.withSpan(
15787
- "chat.turn",
15788
- "chat.turn",
15789
- logContext({
16134
+ const context = logContext({
16135
+ threadId,
16136
+ requesterId: message.author.userId,
16137
+ requesterUserName: message.author.userName,
16138
+ channelId,
16139
+ runId
16140
+ });
16141
+ processingReaction = await startSlackProcessingReaction({
16142
+ thread,
16143
+ message,
16144
+ logException: deps.logException,
16145
+ logContext: context
16146
+ });
16147
+ const toolInvocationHook = createToolInvocationHook(
16148
+ processingReaction,
16149
+ hooks
16150
+ );
16151
+ await deps.withSpan("chat.turn", "chat.turn", context, async () => {
16152
+ const legacyAttachmentText = renderSlackLegacyAttachmentText(
16153
+ message.raw
16154
+ );
16155
+ const rawUserText = appendSlackLegacyAttachmentText(
16156
+ message.text,
16157
+ message.raw
16158
+ );
16159
+ const strippedUserText = deps.stripLeadingBotMention(message.text, {
16160
+ stripLeadingSlackMentionToken: Boolean(message.isMention)
16161
+ });
16162
+ const userText = appendSlackLegacyAttachmentText(
16163
+ strippedUserText,
16164
+ message.raw
16165
+ );
16166
+ const context2 = {
15790
16167
  threadId,
15791
16168
  requesterId: message.author.userId,
15792
- requesterUserName: message.author.userName,
15793
16169
  channelId,
15794
16170
  runId
15795
- }),
15796
- async () => {
15797
- const legacyAttachmentText = renderSlackLegacyAttachmentText(
15798
- message.raw
15799
- );
15800
- const rawUserText = appendSlackLegacyAttachmentText(
15801
- message.text,
15802
- message.raw
15803
- );
15804
- const strippedUserText = deps.stripLeadingBotMention(message.text, {
15805
- stripLeadingSlackMentionToken: Boolean(message.isMention)
15806
- });
15807
- const userText = appendSlackLegacyAttachmentText(
15808
- strippedUserText,
15809
- message.raw
15810
- );
15811
- const context = {
15812
- threadId,
15813
- requesterId: message.author.userId,
15814
- channelId,
15815
- runId
15816
- };
15817
- const preflightDecision = getSubscribedReplyPreflightDecision({
15818
- botUserName: deps.assistantUserName,
15819
- rawText: rawUserText,
15820
- text: userText,
15821
- isExplicitMention: Boolean(message.isMention)
15822
- });
15823
- if (preflightDecision && !preflightDecision.shouldReply) {
15824
- const reason = preflightDecision.reasonDetail ? `${preflightDecision.reason}:${preflightDecision.reasonDetail}` : preflightDecision.reason;
15825
- await skipSubscribedMessage({
15826
- thread,
15827
- message,
15828
- decision: { shouldReply: false, reason },
15829
- context,
15830
- userText
15831
- });
15832
- return;
15833
- }
15834
- const preparedState = await deps.prepareTurnState({
16171
+ };
16172
+ const preflightDecision = getSubscribedReplyPreflightDecision({
16173
+ botUserName: deps.assistantUserName,
16174
+ rawText: rawUserText,
16175
+ text: userText,
16176
+ isExplicitMention: Boolean(message.isMention)
16177
+ });
16178
+ if (preflightDecision && !preflightDecision.shouldReply) {
16179
+ const reason = preflightDecision.reasonDetail ? `${preflightDecision.reason}:${preflightDecision.reasonDetail}` : preflightDecision.reason;
16180
+ await skipSubscribedMessage({
15835
16181
  thread,
15836
16182
  message,
15837
- userText,
15838
- explicitMention: Boolean(message.isMention),
15839
- context
16183
+ decision: { shouldReply: false, reason },
16184
+ context: context2,
16185
+ userText
15840
16186
  });
15841
- await deps.persistPreparedState({
16187
+ return;
16188
+ }
16189
+ const preparedState = await deps.prepareTurnState({
16190
+ thread,
16191
+ message,
16192
+ userText,
16193
+ explicitMention: Boolean(message.isMention),
16194
+ context: context2
16195
+ });
16196
+ await deps.persistPreparedState({
16197
+ thread,
16198
+ preparedState
16199
+ });
16200
+ const decision = await deps.decideSubscribedReply({
16201
+ rawText: rawUserText,
16202
+ text: userText,
16203
+ conversationContext: deps.getPreparedConversationContext(preparedState),
16204
+ hasAttachments: message.attachments.length > 0 || legacyAttachmentText !== "",
16205
+ isExplicitMention: Boolean(message.isMention),
16206
+ context: context2
16207
+ });
16208
+ if (await maybeHandleThreadOptOutDecision({
16209
+ thread,
16210
+ decision,
16211
+ beforeFirstResponsePost: hooks?.beforeFirstResponsePost
16212
+ })) {
16213
+ await skipSubscribedMessage({
15842
16214
  thread,
15843
- preparedState
15844
- });
15845
- const decision = await deps.decideSubscribedReply({
15846
- rawText: rawUserText,
15847
- text: userText,
15848
- conversationContext: deps.getPreparedConversationContext(preparedState),
15849
- hasAttachments: message.attachments.length > 0 || legacyAttachmentText !== "",
15850
- isExplicitMention: Boolean(message.isMention),
15851
- context
16215
+ message,
16216
+ decision,
16217
+ context: context2,
16218
+ preparedState,
16219
+ userText
15852
16220
  });
15853
- if (await maybeHandleThreadOptOutDecision({
16221
+ return;
16222
+ }
16223
+ if (!decision.shouldReply) {
16224
+ await skipSubscribedMessage({
15854
16225
  thread,
16226
+ message,
15855
16227
  decision,
15856
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost
15857
- })) {
15858
- await skipSubscribedMessage({
15859
- thread,
15860
- message,
15861
- decision,
15862
- context,
15863
- preparedState,
15864
- userText
15865
- });
15866
- return;
15867
- }
15868
- if (!decision.shouldReply) {
15869
- await skipSubscribedMessage({
15870
- thread,
15871
- message,
15872
- decision,
15873
- context,
15874
- preparedState,
15875
- userText
15876
- });
15877
- return;
15878
- }
15879
- await deps.replyToThread(thread, message, {
15880
- explicitMention: Boolean(message.isMention),
16228
+ context: context2,
15881
16229
  preparedState,
15882
- beforeFirstResponsePost: hooks?.beforeFirstResponsePost
16230
+ userText
15883
16231
  });
16232
+ return;
15884
16233
  }
15885
- );
16234
+ await deps.replyToThread(thread, message, {
16235
+ explicitMention: Boolean(message.isMention),
16236
+ preparedState,
16237
+ beforeFirstResponsePost: hooks?.beforeFirstResponsePost,
16238
+ onToolInvocation: toolInvocationHook
16239
+ });
16240
+ });
15886
16241
  } catch (error) {
15887
16242
  const errorContext = logContext({
15888
16243
  threadId: deps.getThreadId(thread, message),
@@ -15921,6 +16276,8 @@ function createSlackTurnRuntime(deps) {
15921
16276
  postFailureEventName: "subscribed_message_handler_failure_reply_post_failed",
15922
16277
  postFailureBody: "Failed to post fallback error reply for subscribed message handler"
15923
16278
  });
16279
+ } finally {
16280
+ await processingReaction?.stop();
15924
16281
  }
15925
16282
  },
15926
16283
  async handleAssistantThreadStarted(event) {
@@ -16618,6 +16975,7 @@ function createJuniorRuntimeServices(overrides = {}) {
16618
16975
  conversationMemory,
16619
16976
  replyExecutor: {
16620
16977
  generateAssistantReply: overrides.replyExecutor?.generateAssistantReply ?? generateAssistantReply,
16978
+ getAwaitingTurnContinuationRequest: overrides.replyExecutor?.getAwaitingTurnContinuationRequest ?? getAwaitingTurnContinuationRequest,
16621
16979
  lookupSlackUser: overrides.replyExecutor?.lookupSlackUser ?? lookupSlackUser,
16622
16980
  scheduleTurnTimeoutResume: overrides.replyExecutor?.scheduleTurnTimeoutResume ?? scheduleTurnTimeoutResume,
16623
16981
  generateThreadTitle: conversationMemory.generateThreadTitle
@@ -16640,74 +16998,6 @@ function getSlackMessageTs(message) {
16640
16998
  return message.id;
16641
16999
  }
16642
17000
 
16643
- // src/chat/runtime/thread-context.ts
16644
- function escapeRegExp3(value) {
16645
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16646
- }
16647
- function stripLeadingBotMention(text, options = {}) {
16648
- if (!text.trim()) return text;
16649
- let next = text;
16650
- if (options.stripLeadingSlackMentionToken) {
16651
- next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
16652
- }
16653
- const mentionByNameRe = new RegExp(
16654
- `^\\s*@${escapeRegExp3(botConfig.userName)}\\b[\\s,:-]*`,
16655
- "i"
16656
- );
16657
- next = next.replace(mentionByNameRe, "").trim();
16658
- const mentionByLabeledEntityRe = new RegExp(
16659
- `^\\s*<@[^>|]+\\|${escapeRegExp3(botConfig.userName)}>[\\s,:-]*`,
16660
- "i"
16661
- );
16662
- next = next.replace(mentionByLabeledEntityRe, "").trim();
16663
- return next;
16664
- }
16665
- function getThreadId(thread, _message) {
16666
- return toOptionalString(thread.id);
16667
- }
16668
- function getRunId(thread, message) {
16669
- return toOptionalString(thread.runId) ?? toOptionalString(message.runId);
16670
- }
16671
- function getChannelId(thread, message) {
16672
- return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
16673
- }
16674
- function getThreadTs(threadId) {
16675
- return parseSlackThreadId(threadId)?.threadTs;
16676
- }
16677
- function getAssistantThreadContext(message) {
16678
- const raw = message.raw;
16679
- const rawRecord = raw && typeof raw === "object" ? raw : void 0;
16680
- const channelId = toOptionalString(rawRecord?.channel);
16681
- if (channelId) {
16682
- const rawThreadTs = toOptionalString(rawRecord?.thread_ts);
16683
- const threadTs = isDmChannel(channelId) ? rawThreadTs : rawThreadTs ?? toOptionalString(rawRecord?.ts);
16684
- if (threadTs) {
16685
- return { channelId, threadTs };
16686
- }
16687
- }
16688
- const parsedThreadId = parseSlackThreadId(
16689
- toOptionalString(message.threadId)
16690
- );
16691
- if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) {
16692
- return void 0;
16693
- }
16694
- return parsedThreadId;
16695
- }
16696
- function getMessageTs(message) {
16697
- const directTs = toOptionalString(
16698
- message.ts
16699
- );
16700
- if (directTs) {
16701
- return directTs;
16702
- }
16703
- const raw = message.raw;
16704
- if (!raw || typeof raw !== "object") {
16705
- return void 0;
16706
- }
16707
- const rawRecord = raw;
16708
- return toOptionalString(rawRecord.ts) ?? toOptionalString(rawRecord.event_ts) ?? toOptionalString(rawRecord.message?.ts);
16709
- }
16710
-
16711
17001
  // src/chat/slack/assistant-thread/title.ts
16712
17002
  function maybeUpdateAssistantTitle(args) {
16713
17003
  const assistantThreadContext = args.assistantThreadContext;
@@ -16824,11 +17114,6 @@ function createReplyToThread(deps) {
16824
17114
  });
16825
17115
  const slackMessageTs = getSlackMessageTs(message);
16826
17116
  const turnId = buildDeterministicTurnId(message.id);
16827
- startActiveTurn({
16828
- conversation: preparedState.conversation,
16829
- nextTurnId: turnId,
16830
- updateConversationStats
16831
- });
16832
17117
  const turnTraceContext = {
16833
17118
  conversationId,
16834
17119
  slackThreadId: threadId,
@@ -16838,6 +17123,78 @@ function createReplyToThread(deps) {
16838
17123
  assistantUserName: botConfig.userName,
16839
17124
  modelId: botConfig.modelId
16840
17125
  };
17126
+ let beforeFirstResponsePostCalled = false;
17127
+ const beforeFirstResponsePost = async () => {
17128
+ if (beforeFirstResponsePostCalled) {
17129
+ return;
17130
+ }
17131
+ beforeFirstResponsePostCalled = true;
17132
+ await options.beforeFirstResponsePost?.();
17133
+ };
17134
+ const postTurnContinuationNotice = async () => {
17135
+ try {
17136
+ await beforeFirstResponsePost();
17137
+ await thread.post(
17138
+ buildSlackOutputMessage(buildTurnContinuationResponse())
17139
+ );
17140
+ } catch (error) {
17141
+ logException(
17142
+ error,
17143
+ "slack_turn_continuation_notice_post_failed",
17144
+ turnTraceContext,
17145
+ {
17146
+ "app.slack.reply_stage": "thread_reply_turn_continuation_notice",
17147
+ ...messageTs ? { "messaging.message.id": messageTs } : {},
17148
+ ...getSlackErrorObservabilityAttributes(error)
17149
+ },
17150
+ "Failed to post turn continuation notice"
17151
+ );
17152
+ throw error;
17153
+ }
17154
+ };
17155
+ const activeTurnId = preparedState.conversation.processing.activeTurnId;
17156
+ if (conversationId && activeTurnId) {
17157
+ const resumeRequest = await deps.services.getAwaitingTurnContinuationRequest({
17158
+ conversationId,
17159
+ sessionId: activeTurnId
17160
+ });
17161
+ if (resumeRequest) {
17162
+ try {
17163
+ await deps.services.scheduleTurnTimeoutResume(resumeRequest);
17164
+ } catch (error) {
17165
+ logException(
17166
+ error,
17167
+ "agent_turn_continuation_retry_schedule_failed",
17168
+ turnTraceContext,
17169
+ {
17170
+ "app.ai.resume_checkpoint_version": resumeRequest.expectedCheckpointVersion,
17171
+ "app.ai.resume_session_id": resumeRequest.sessionId,
17172
+ ...messageTs ? { "messaging.message.id": messageTs } : {}
17173
+ },
17174
+ "Failed to reschedule active turn continuation"
17175
+ );
17176
+ throw error;
17177
+ }
17178
+ await postTurnContinuationNotice();
17179
+ markConversationMessage(
17180
+ preparedState.conversation,
17181
+ preparedState.userMessageId,
17182
+ {
17183
+ replied: true,
17184
+ skippedReason: void 0
17185
+ }
17186
+ );
17187
+ await persistThreadState(thread, {
17188
+ conversation: preparedState.conversation
17189
+ });
17190
+ return;
17191
+ }
17192
+ }
17193
+ startActiveTurn({
17194
+ conversation: preparedState.conversation,
17195
+ nextTurnId: turnId,
17196
+ updateConversationStats
17197
+ });
16841
17198
  setTags({
16842
17199
  conversationId
16843
17200
  });
@@ -16879,14 +17236,6 @@ function createReplyToThread(deps) {
16879
17236
  threadTs: assistantThreadContext?.threadTs,
16880
17237
  getSlackAdapter: deps.getSlackAdapter
16881
17238
  });
16882
- let beforeFirstResponsePostCalled = false;
16883
- const beforeFirstResponsePost = async () => {
16884
- if (beforeFirstResponsePostCalled) {
16885
- return;
16886
- }
16887
- beforeFirstResponsePostCalled = true;
16888
- await options.beforeFirstResponsePost?.();
16889
- };
16890
17239
  const postThreadReply = async (payload, stage) => {
16891
17240
  await beforeFirstResponsePost();
16892
17241
  try {
@@ -16973,7 +17322,8 @@ function createReplyToThread(deps) {
16973
17322
  conversation: preparedState.conversation
16974
17323
  });
16975
17324
  },
16976
- onStatus: (nextStatus) => status.update(nextStatus)
17325
+ onStatus: (nextStatus) => status.update(nextStatus),
17326
+ onToolInvocation: options.onToolInvocation
16977
17327
  });
16978
17328
  const diagnosticsContext = {
16979
17329
  slackThreadId: threadId,
@@ -17140,7 +17490,6 @@ function createReplyToThread(deps) {
17140
17490
  expectedCheckpointVersion: checkpointVersion
17141
17491
  });
17142
17492
  shouldPersistFailureState = false;
17143
- return;
17144
17493
  } catch (scheduleError) {
17145
17494
  logException(
17146
17495
  scheduleError,
@@ -17152,7 +17501,11 @@ function createReplyToThread(deps) {
17152
17501
  },
17153
17502
  "Failed to schedule timeout resume callback"
17154
17503
  );
17504
+ shouldPersistFailureState = true;
17505
+ throw scheduleError;
17155
17506
  }
17507
+ await postTurnContinuationNotice();
17508
+ return;
17156
17509
  } else if (conversationIdForResume && sessionIdForResume && typeof checkpointVersion === "number") {
17157
17510
  logWarn(
17158
17511
  "agent_turn_timeout_resume_slice_limit_reached",
@@ -17609,75 +17962,53 @@ function enqueueBackgroundTask(options, task) {
17609
17962
  throw new Error("Chat background processing requires waitUntil");
17610
17963
  }
17611
17964
  options.waitUntil(task);
17965
+ return task;
17612
17966
  }
17613
17967
  var JuniorChat = class extends Chat {
17614
17968
  /**
17615
17969
  * Normalize Slack thread IDs before the SDK's concurrency queue.
17616
17970
  *
17617
- * The SDK uses the `threadId` parameter as the lock/queue key
17618
- * (Chat.handleIncomingMessage getLockKey). @chat-adapter/slack
17619
- * (as of 4.22.0) builds DM thread IDs as `slack:<channel>:` (empty
17620
- * thread_ts) when the Slack event has no `thread_ts` field — it uses
17621
- * `event.thread_ts || ""` instead of falling back to `event.ts`.
17622
- * See @chat-adapter/slack/dist/index.js:1466.
17623
- *
17624
- * A DM root event arrives as `slack:D123:` while a reply in the same
17625
- * thread carries `slack:D123:<ts>`, splitting the lock/state/subscription
17626
- * keys and breaking conversation continuity.
17627
- *
17628
- * We fix this by resolving the message eagerly (even when the adapter
17629
- * provides a factory), deriving the canonical thread ID from
17630
- * `raw.channel` + `raw.thread_ts ?? raw.ts`, and passing both the
17631
- * normalized threadId and concrete message to super.processMessage.
17632
- *
17633
- * Remove this override when @chat-adapter/slack uses `event.ts` as
17634
- * the DM thread_ts fallback.
17971
+ * Slack DM roots can arrive with an empty thread timestamp, while
17972
+ * later replies include the root timestamp. Resolve factories before
17973
+ * delegating so the lock/state/subscription key is canonicalized before
17974
+ * the SDK computes its per-thread queue key.
17635
17975
  */
17636
17976
  processMessage(adapter, threadId, messageOrFactory, options) {
17637
17977
  if (typeof messageOrFactory === "function") {
17638
17978
  const runtime = this;
17639
- enqueueBackgroundTask(
17979
+ return enqueueBackgroundTask(
17640
17980
  options,
17641
17981
  (async () => {
17982
+ let message2;
17642
17983
  try {
17643
- const message = await messageOrFactory();
17644
- if (isExternalSlackUser(message.raw)) {
17645
- return;
17646
- }
17647
- const normalized = normalizeIncomingSlackThreadId(
17648
- threadId,
17649
- message
17650
- );
17651
- if (normalized !== threadId && "threadId" in message) {
17652
- message.threadId = normalized;
17653
- }
17654
- super.processMessage(adapter, normalized, message, options);
17984
+ message2 = await messageOrFactory();
17655
17985
  } catch (error) {
17656
17986
  runtime.logger?.error?.("Message factory resolution error", {
17657
17987
  error,
17658
17988
  threadId
17659
17989
  });
17990
+ return;
17991
+ }
17992
+ if (isExternalSlackUser(message2.raw)) {
17993
+ return;
17660
17994
  }
17995
+ const normalized2 = normalizeIncomingSlackThreadId(threadId, message2);
17996
+ if (normalized2 !== threadId && "threadId" in message2) {
17997
+ message2.threadId = normalized2;
17998
+ }
17999
+ await super.processMessage(adapter, normalized2, message2, options);
17661
18000
  })()
17662
18001
  );
17663
- return;
17664
18002
  }
17665
- if (isExternalSlackUser(messageOrFactory.raw)) {
17666
- return;
18003
+ const message = messageOrFactory;
18004
+ if (isExternalSlackUser(message.raw)) {
18005
+ return Promise.resolve();
17667
18006
  }
17668
- enqueueBackgroundTask(
17669
- options,
17670
- (async () => {
17671
- const normalized = normalizeIncomingSlackThreadId(
17672
- threadId,
17673
- messageOrFactory
17674
- );
17675
- if (normalized !== threadId && "threadId" in messageOrFactory) {
17676
- messageOrFactory.threadId = normalized;
17677
- }
17678
- super.processMessage(adapter, normalized, messageOrFactory, options);
17679
- })()
17680
- );
18007
+ const normalized = normalizeIncomingSlackThreadId(threadId, message);
18008
+ if (normalized !== threadId && "threadId" in message) {
18009
+ message.threadId = normalized;
18010
+ }
18011
+ return super.processMessage(adapter, normalized, message, options);
17681
18012
  }
17682
18013
  processReaction(event, options) {
17683
18014
  const runtime = this;
@@ -18373,11 +18704,11 @@ async function createApp(options) {
18373
18704
  app.post("/api/internal/turn-resume", (c) => {
18374
18705
  return POST(c.req.raw, waitUntil);
18375
18706
  });
18376
- app.all("/api/internal/sandbox-egress/:sandboxId", (c) => {
18377
- return ALL(c.req.raw, c.req.param("sandboxId"));
18707
+ app.all("/api/internal/sandbox-egress/:egressId", (c) => {
18708
+ return ALL(c.req.raw, c.req.param("egressId"));
18378
18709
  });
18379
- app.all("/api/internal/sandbox-egress/:sandboxId/*", (c) => {
18380
- return ALL(c.req.raw, c.req.param("sandboxId"));
18710
+ app.all("/api/internal/sandbox-egress/:egressId/*", (c) => {
18711
+ return ALL(c.req.raw, c.req.param("egressId"));
18381
18712
  });
18382
18713
  app.post("/api/webhooks/:platform", (c) => {
18383
18714
  return POST2(c.req.raw, c.req.param("platform"), waitUntil);