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