@sentry/junior 0.203.0 → 0.205.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/agent-D4nu8MQV.js +17521 -0
- package/dist/agent-hooks-DXHpTJ4m.js +4042 -0
- package/dist/agent-runner-C04colea.js +678 -0
- package/dist/api/schema.d.ts +2736 -2639
- package/dist/api/schema.js +4 -169
- package/dist/api.d.ts +23 -17
- package/dist/api.js +3881 -5326
- package/dist/app-Cb776lL8.d.ts +247 -0
- package/dist/app.d.ts +4 -520
- package/dist/app.js +9524 -11875
- package/dist/auth-pause-state-C2P3guS1.js +168 -0
- package/dist/catalog-runtime-CXw3asJD.js +7 -0
- package/dist/checkpoint-CrViJ8Ps.js +2450 -0
- package/dist/cli/chat.d.ts +20 -0
- package/dist/cli/chat.js +229 -336
- package/dist/cli/check.d.ts +8 -0
- package/dist/cli/check.js +513 -806
- package/dist/cli/env.d.ts +13 -0
- package/dist/cli/env.js +63 -12
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.js +90 -159
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.js +43 -64
- package/dist/cli/plugins.d.ts +13 -0
- package/dist/cli/plugins.js +148 -227
- package/dist/cli/run.d.ts +16 -0
- package/dist/cli/run.js +52 -8
- package/dist/cli/snapshot-create.d.ts +4 -0
- package/dist/cli/snapshot-create.js +71 -125
- package/dist/cli/upgrade.d.ts +15 -0
- package/dist/cli/upgrade.js +232 -335
- package/dist/config-BOtJQ7Pc.js +941 -0
- package/dist/context-B45_WqVX.js +298 -0
- package/dist/conversation-privacy-BZRA6Pes.js +182 -0
- package/dist/conversation-work-C6y3T5Hx.js +51 -0
- package/dist/credential-sync-Bep34IB_.js +2661 -0
- package/dist/db-BeyXKJOI.d.ts +5 -0
- package/dist/destinations-BeBiJeEM.js +29 -0
- package/dist/execution-stats-B6lgWxCK.js +373 -0
- package/dist/executor-BlUhFL7_.js +721 -0
- package/dist/experimental-CIXf7Uye.js +46 -0
- package/dist/history-ClE4toxe.d.ts +32 -0
- package/dist/history-CqTuSAdI.js +441 -0
- package/dist/instrumentation.d.ts +3 -3
- package/dist/instrumentation.js +130 -176
- package/dist/job-queue-DLMqIK74.js +63 -0
- package/dist/logging-BMJzdDlP.js +1210 -0
- package/dist/model-profile-CZmVoGdX.js +49 -0
- package/dist/nitro.d.ts +31 -36
- package/dist/nitro.js +275 -456
- package/dist/oauth-callback-server-WwG5_Zgm.js +130 -0
- package/dist/oauth-relay-CdCnu80W.js +62 -0
- package/dist/package-discovery-7a9YqLhE.js +455 -0
- package/dist/pending-auth-CUbXq06G.js +185 -0
- package/dist/plugin-module-BKGOXca2.js +81 -0
- package/dist/plugins-BI9A1vKA.d.ts +95 -0
- package/dist/plugins-egKWjgue.js +99 -0
- package/dist/private-trace-result-BLvBjy_e.js +16 -0
- package/dist/profile-T0tXE4zu.js +861 -0
- package/dist/registry-BA0__8ai.js +1419 -0
- package/dist/reporting-schema-DosTN__Q.js +105 -0
- package/dist/resolve-bvD1ORdv.js +546 -0
- package/dist/rolldown-runtime-B4FMCO8f.js +28 -0
- package/dist/runner-BXYxfIUL.js +392 -0
- package/dist/sandbox-egress-signals-ngEPjSEW.js +46 -0
- package/dist/schema-Rn8-XEaN.js +1140 -0
- package/dist/sentry-CD3SdJl-.js +26 -0
- package/dist/skills-DJOlYdBs.js +316 -0
- package/dist/spawn-h0R1bTEo.js +29 -0
- package/dist/stats-3YZn-Swi.js +2648 -0
- package/dist/store-C5qxF2ac.d.ts +208 -0
- package/dist/task-queue-CoXwd2eq.js +136 -0
- package/dist/task-runner-B2_mJZj0.js +639 -0
- package/dist/thread-state-CqZ1m-es.js +431 -0
- package/dist/turn-execution-fI6PZ0iI.js +47 -0
- package/dist/turn-lifecycle-CcnwrOcp.js +54 -0
- package/dist/usage-B5SfMb3H.js +296 -0
- package/dist/usage-schema-CKaDkH_j.js +20 -0
- package/dist/validation-DfWcvhuS.js +40 -0
- package/dist/vercel-queue-DIkVcfK4.js +167 -0
- package/dist/vercel.d.ts +6 -6
- package/dist/vercel.js +14 -6
- package/dist/version-CnXZ614E.d.ts +5 -0
- package/dist/version-oD-oPrbO.js +8 -0
- package/dist/version.d.ts +2 -4
- package/dist/version.js +2 -6
- package/dist/web-authorization-Crbzlmzo.js +76 -0
- package/dist/web-input-BOuq3oKK.js +494 -0
- package/dist/work-B91LQKHr.js +509 -0
- package/dist/worker-BcRRTe6k.js +625 -0
- package/package.json +9 -9
- package/dist/agent-hooks-GB2VPTNA.js +0 -51
- package/dist/catalog-runtime-UBLIWD3O.js +0 -14
- package/dist/chunk-2LYHWMR4.js +0 -46
- package/dist/chunk-352OWAZH.js +0 -320
- package/dist/chunk-3Y47VMHV.js +0 -6
- package/dist/chunk-3YJSKOWF.js +0 -164
- package/dist/chunk-4KSSKTQ5.js +0 -229
- package/dist/chunk-4UO6FK4G.js +0 -64
- package/dist/chunk-5AHX53NQ.js +0 -717
- package/dist/chunk-5J3E5FWA.js +0 -700
- package/dist/chunk-5PDAG6O3.js +0 -206
- package/dist/chunk-6Y5B5PEX.js +0 -80
- package/dist/chunk-7GWQ5S5R.js +0 -2086
- package/dist/chunk-7H2VZTWN.js +0 -103
- package/dist/chunk-7NL7T6VU.js +0 -406
- package/dist/chunk-A5L27LE4.js +0 -387
- package/dist/chunk-ADIAWL34.js +0 -81
- package/dist/chunk-ASTEMBKJ.js +0 -519
- package/dist/chunk-CEMMLV5L.js +0 -98
- package/dist/chunk-CM5HNV2W.js +0 -131
- package/dist/chunk-CRTRBBRI.js +0 -105
- package/dist/chunk-DA2KGZZI.js +0 -13
- package/dist/chunk-DIB4GX3G.js +0 -8
- package/dist/chunk-DKIJ2RP5.js +0 -68
- package/dist/chunk-DWPN6SCH.js +0 -172
- package/dist/chunk-EDKUFGPL.js +0 -64
- package/dist/chunk-ENY6SKIU.js +0 -1228
- package/dist/chunk-F4MGCZOX.js +0 -2943
- package/dist/chunk-FA6POGLH.js +0 -70
- package/dist/chunk-FSUR7GTG.js +0 -20475
- package/dist/chunk-GKEOHJL5.js +0 -1856
- package/dist/chunk-GMCB6W5Y.js +0 -333
- package/dist/chunk-IE2VPG7O.js +0 -1135
- package/dist/chunk-IZ5B5IHQ.js +0 -3656
- package/dist/chunk-JAL3CH4G.js +0 -27
- package/dist/chunk-KIIEUXD3.js +0 -1764
- package/dist/chunk-KXU4NIL7.js +0 -692
- package/dist/chunk-MTM5IFCY.js +0 -1190
- package/dist/chunk-N7TMSOZH.js +0 -235
- package/dist/chunk-NCEBK6PH.js +0 -36
- package/dist/chunk-NHJU2ZAL.js +0 -56
- package/dist/chunk-P6KTRNVC.js +0 -321
- package/dist/chunk-PDO5BLNM.js +0 -22
- package/dist/chunk-PDRNLLFT.js +0 -3295
- package/dist/chunk-PEQXYSSE.js +0 -45
- package/dist/chunk-PKEX2AQX.js +0 -198
- package/dist/chunk-PSJCGIGO.js +0 -1163
- package/dist/chunk-PZUSUL55.js +0 -506
- package/dist/chunk-R45M4TUS.js +0 -37
- package/dist/chunk-RC745GGZ.js +0 -122
- package/dist/chunk-SP3QKZXJ.js +0 -820
- package/dist/chunk-TXKUKMVO.js +0 -417
- package/dist/chunk-UE4GPWSD.js +0 -931
- package/dist/chunk-UIE3R5XU.js +0 -17
- package/dist/chunk-UXPG6ZIN.js +0 -84
- package/dist/chunk-VFUK3X5B.js +0 -50
- package/dist/chunk-VNMJWREK.js +0 -787
- package/dist/chunk-VZ7CSOX2.js +0 -706
- package/dist/chunk-WCNQJKFT.js +0 -10
- package/dist/chunk-WDZR3QQM.js +0 -135
- package/dist/chunk-WLYVJCHV.js +0 -1433
- package/dist/chunk-WS7NAFTM.js +0 -1520
- package/dist/chunk-X6GDDWQH.js +0 -60
- package/dist/chunk-XPDEYNQI.js +0 -102
- package/dist/chunk-XRW44VGM.js +0 -58
- package/dist/chunk-Y2CM7HXH.js +0 -111
- package/dist/chunk-Y7X25LFY.js +0 -15
- package/dist/conversation-work-VIOQ5ZF3.js +0 -54
- package/dist/db-NGDJV7YH.js +0 -26
- package/dist/discovery-DWVDM7UK.js +0 -44
- package/dist/experimental-P5FZM4CU.js +0 -12
- package/dist/oauth-callback-server-YFY7YEGK.js +0 -243
- package/dist/oauth-relay-5XPVRPYB.js +0 -8
- package/dist/plugins-YTYJMMJF.js +0 -14
- package/dist/runner-ORTNSB2Q.js +0 -522
- package/dist/sandbox-egress-signals-FIL7EGPR.js +0 -73
- package/dist/sentry-MB3NL3IR.js +0 -42
- package/dist/skills-H376WQOH.js +0 -27
- package/dist/spawn-T43WHLOS.js +0 -42
- package/dist/task-runner-LBZWF32V.js +0 -36
- package/dist/validation-REJLRKI6.js +0 -17
- package/dist/work-3KJ3RE76.js +0 -51
- package/dist/worker-QFEBU72E.js +0 -24
package/dist/chunk-PDRNLLFT.js
DELETED
|
@@ -1,3295 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
relayLocalOAuthCallback
|
|
3
|
-
} from "./chunk-FA6POGLH.js";
|
|
4
|
-
import {
|
|
5
|
-
AgentRunError,
|
|
6
|
-
executeTurn
|
|
7
|
-
} from "./chunk-XRW44VGM.js";
|
|
8
|
-
import {
|
|
9
|
-
buildDeliveredTurnStatePatch,
|
|
10
|
-
finalizeFailedTurnReplyWithEvent,
|
|
11
|
-
getInterruptionMarker,
|
|
12
|
-
getTurnUserMessage,
|
|
13
|
-
getTurnUserMessageId,
|
|
14
|
-
getTurnUserReplyAttachmentContext,
|
|
15
|
-
getTurnUserSlackMessageTs,
|
|
16
|
-
persistAuthPauseTurnState,
|
|
17
|
-
requireTurnFailureEventId
|
|
18
|
-
} from "./chunk-4KSSKTQ5.js";
|
|
19
|
-
import {
|
|
20
|
-
resolveTurnSessionRouting,
|
|
21
|
-
scheduleSessionCompletedPluginTasks
|
|
22
|
-
} from "./chunk-SP3QKZXJ.js";
|
|
23
|
-
import {
|
|
24
|
-
getTurnLifecycle
|
|
25
|
-
} from "./chunk-EDKUFGPL.js";
|
|
26
|
-
import {
|
|
27
|
-
coerceThreadConversationState,
|
|
28
|
-
configValueSchema,
|
|
29
|
-
getLocationConfigurationService,
|
|
30
|
-
getPersistedSandboxState,
|
|
31
|
-
getPersistedThreadState,
|
|
32
|
-
persistThreadStateById
|
|
33
|
-
} from "./chunk-ASTEMBKJ.js";
|
|
34
|
-
import {
|
|
35
|
-
buildSentryConversationUrl,
|
|
36
|
-
deleteWebAuthorization
|
|
37
|
-
} from "./chunk-CRTRBBRI.js";
|
|
38
|
-
import {
|
|
39
|
-
createUserTokenStore,
|
|
40
|
-
deleteMcpAuthSession,
|
|
41
|
-
finalizeMcpAuthorization,
|
|
42
|
-
getMcpAuthSession,
|
|
43
|
-
getMcpProviderErrorAttributes,
|
|
44
|
-
getMcpStoredOAuthCredentials,
|
|
45
|
-
lookupSlackUserProfile,
|
|
46
|
-
putMcpStoredOAuthCredentials,
|
|
47
|
-
resolvePluginOAuthAccount
|
|
48
|
-
} from "./chunk-PSJCGIGO.js";
|
|
49
|
-
import {
|
|
50
|
-
RetryableDeliveryError,
|
|
51
|
-
addReactionToMessage,
|
|
52
|
-
escapeSlackMrkdwnText,
|
|
53
|
-
formatProviderLabel,
|
|
54
|
-
formatSlackLink,
|
|
55
|
-
normalizeSlackReplyMarkdown,
|
|
56
|
-
normalizeSlackStatusText,
|
|
57
|
-
parseOAuthStatePayload,
|
|
58
|
-
postSlackMessage,
|
|
59
|
-
removeReactionFromMessage,
|
|
60
|
-
resolveBaseUrl,
|
|
61
|
-
truncateStatusText
|
|
62
|
-
} from "./chunk-UE4GPWSD.js";
|
|
63
|
-
import {
|
|
64
|
-
CooperativeTurnYieldError,
|
|
65
|
-
clearPendingAuth,
|
|
66
|
-
getAssistantReplyText,
|
|
67
|
-
getConversationPendingAuth,
|
|
68
|
-
isCooperativeTurnYieldError,
|
|
69
|
-
isPendingAuthLatestRequest,
|
|
70
|
-
markTurnFailed
|
|
71
|
-
} from "./chunk-5PDAG6O3.js";
|
|
72
|
-
import {
|
|
73
|
-
abandonTurnRecord,
|
|
74
|
-
buildConversationContext,
|
|
75
|
-
buildTurnErrorResponse,
|
|
76
|
-
commitAcceptedReply,
|
|
77
|
-
failTurnRecord,
|
|
78
|
-
getTurnRecord,
|
|
79
|
-
hydrateConversationMessages,
|
|
80
|
-
loadProjection,
|
|
81
|
-
loadTurnCheckpoint,
|
|
82
|
-
markConversationMessage,
|
|
83
|
-
persistWithRetry,
|
|
84
|
-
recordAuthenticationLinked,
|
|
85
|
-
recordAuthorizationCompleted,
|
|
86
|
-
recordDeliveredAssistantMessage,
|
|
87
|
-
recordTurnSummary,
|
|
88
|
-
saveTurnCheckpoint,
|
|
89
|
-
turnHasReply
|
|
90
|
-
} from "./chunk-F4MGCZOX.js";
|
|
91
|
-
import {
|
|
92
|
-
parseSlackThreadId,
|
|
93
|
-
readSlackRawMessageContext,
|
|
94
|
-
resolveSlackChannelIdFromMessage,
|
|
95
|
-
resolveSlackChannelIdFromThreadId
|
|
96
|
-
} from "./chunk-XPDEYNQI.js";
|
|
97
|
-
import {
|
|
98
|
-
ensureConversationWake,
|
|
99
|
-
requestConversationWork
|
|
100
|
-
} from "./chunk-KIIEUXD3.js";
|
|
101
|
-
import {
|
|
102
|
-
getTurnRequestDeadline
|
|
103
|
-
} from "./chunk-NCEBK6PH.js";
|
|
104
|
-
import {
|
|
105
|
-
getVercelConversationWorkQueue
|
|
106
|
-
} from "./chunk-DKIJ2RP5.js";
|
|
107
|
-
import {
|
|
108
|
-
SlackActionError,
|
|
109
|
-
getDashboardConversationLink,
|
|
110
|
-
getHeaderString,
|
|
111
|
-
getPluginSlackConversationLink,
|
|
112
|
-
getSlackClient,
|
|
113
|
-
isDmChannel,
|
|
114
|
-
normalizeSlackConversationId
|
|
115
|
-
} from "./chunk-IZ5B5IHQ.js";
|
|
116
|
-
import {
|
|
117
|
-
getConversationStore,
|
|
118
|
-
getSqlExecutor,
|
|
119
|
-
requireSlackDestination,
|
|
120
|
-
upsertIdentity,
|
|
121
|
-
upsertLinkedIdentity
|
|
122
|
-
} from "./chunk-GKEOHJL5.js";
|
|
123
|
-
import {
|
|
124
|
-
acquireActiveLock,
|
|
125
|
-
getStateAdapter
|
|
126
|
-
} from "./chunk-P6KTRNVC.js";
|
|
127
|
-
import {
|
|
128
|
-
botConfig,
|
|
129
|
-
escapeXml,
|
|
130
|
-
getChatConfig,
|
|
131
|
-
getRuntimeMetadata,
|
|
132
|
-
normalizeSlackEmojiName
|
|
133
|
-
} from "./chunk-ENY6SKIU.js";
|
|
134
|
-
import {
|
|
135
|
-
defaultModelId
|
|
136
|
-
} from "./chunk-NHJU2ZAL.js";
|
|
137
|
-
import {
|
|
138
|
-
discoverSkills
|
|
139
|
-
} from "./chunk-A5L27LE4.js";
|
|
140
|
-
import {
|
|
141
|
-
pluginCatalogRuntime
|
|
142
|
-
} from "./chunk-WCNQJKFT.js";
|
|
143
|
-
import {
|
|
144
|
-
buildOAuthTokenRequest,
|
|
145
|
-
hasRequiredOAuthScope,
|
|
146
|
-
parseOAuthTokenResponse
|
|
147
|
-
} from "./chunk-7GWQ5S5R.js";
|
|
148
|
-
import {
|
|
149
|
-
createSlackResumeActor,
|
|
150
|
-
isUserActor,
|
|
151
|
-
parseSlackMessageTs,
|
|
152
|
-
parseSlackUserId
|
|
153
|
-
} from "./chunk-N7TMSOZH.js";
|
|
154
|
-
import {
|
|
155
|
-
homeDir
|
|
156
|
-
} from "./chunk-TXKUKMVO.js";
|
|
157
|
-
import {
|
|
158
|
-
logException,
|
|
159
|
-
logInfo,
|
|
160
|
-
logWarn,
|
|
161
|
-
runBestEffort,
|
|
162
|
-
setTags,
|
|
163
|
-
toOptionalString,
|
|
164
|
-
withLogContext
|
|
165
|
-
} from "./chunk-WS7NAFTM.js";
|
|
166
|
-
|
|
167
|
-
// src/chat/slack/assistant-thread/status-render.ts
|
|
168
|
-
var DEFAULT_STATUS_CONTEXTS = {
|
|
169
|
-
thinking: "\u2026",
|
|
170
|
-
searching: "sources",
|
|
171
|
-
reading: "task",
|
|
172
|
-
reviewing: "results",
|
|
173
|
-
drafting: "reply",
|
|
174
|
-
running: "tasks"
|
|
175
|
-
};
|
|
176
|
-
function formatAssistantStatusText(verb, context) {
|
|
177
|
-
const normalizedVerb = normalizeSlackStatusText(verb).trim().toLowerCase();
|
|
178
|
-
const normalizedContext = normalizeSlackStatusText(context ?? "") || DEFAULT_STATUS_CONTEXTS[normalizedVerb] || "";
|
|
179
|
-
if (!normalizedVerb) {
|
|
180
|
-
return truncateStatusText(normalizedContext || "Working");
|
|
181
|
-
}
|
|
182
|
-
const displayVerb = `${normalizedVerb[0]?.toUpperCase() ?? ""}${normalizedVerb.slice(1)}`;
|
|
183
|
-
return truncateStatusText(
|
|
184
|
-
normalizedContext ? `${displayVerb} ${normalizedContext}` : displayVerb
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
function makeAssistantStatus(verb, context) {
|
|
188
|
-
return {
|
|
189
|
-
text: formatAssistantStatusText(verb, context)
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
function renderAssistantStatus(args) {
|
|
193
|
-
const visible = truncateStatusText(
|
|
194
|
-
normalizeSlackStatusText(args.status.text)
|
|
195
|
-
);
|
|
196
|
-
return {
|
|
197
|
-
key: visible,
|
|
198
|
-
visible
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
function selectAssistantLoadingMessages(args) {
|
|
202
|
-
const random = args.random ?? Math.random;
|
|
203
|
-
const normalized = Array.from(
|
|
204
|
-
new Set(
|
|
205
|
-
args.messages.map((message) => truncateStatusText(normalizeSlackStatusText(message))).filter((message) => message.length > 0)
|
|
206
|
-
)
|
|
207
|
-
);
|
|
208
|
-
if (normalized.length === 0) {
|
|
209
|
-
return void 0;
|
|
210
|
-
}
|
|
211
|
-
const shuffled = [...normalized];
|
|
212
|
-
for (let index = shuffled.length - 1; index > 0; index -= 1) {
|
|
213
|
-
const otherIndex = Math.floor(random() * (index + 1));
|
|
214
|
-
[shuffled[index], shuffled[otherIndex]] = [
|
|
215
|
-
shuffled[otherIndex],
|
|
216
|
-
shuffled[index]
|
|
217
|
-
];
|
|
218
|
-
}
|
|
219
|
-
return shuffled.slice(0, 10);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// src/chat/slack/assistant-thread/status-scheduler.ts
|
|
223
|
-
var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
|
|
224
|
-
var STATUS_MIN_VISIBLE_MS = 1200;
|
|
225
|
-
var STATUS_ROTATION_INTERVAL_MS = 3e4;
|
|
226
|
-
function createAssistantStatusScheduler(args) {
|
|
227
|
-
const now = args.now ?? (() => Date.now());
|
|
228
|
-
const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
229
|
-
const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
|
|
230
|
-
const random = args.random ?? Math.random;
|
|
231
|
-
const loadingMessages = selectAssistantLoadingMessages({
|
|
232
|
-
messages: args.loadingMessages ?? [],
|
|
233
|
-
random
|
|
234
|
-
});
|
|
235
|
-
const defaultStatus = makeAssistantStatus("thinking");
|
|
236
|
-
let active = false;
|
|
237
|
-
let currentKey = "";
|
|
238
|
-
let currentVisibleStatus = "";
|
|
239
|
-
let currentLoadingMessages;
|
|
240
|
-
let lastStatusAt = 0;
|
|
241
|
-
let pendingStatus = null;
|
|
242
|
-
let pendingKey = "";
|
|
243
|
-
let pendingTimer = null;
|
|
244
|
-
let rotationTimer = null;
|
|
245
|
-
let inflightStatusUpdate = Promise.resolve();
|
|
246
|
-
const enqueueStatusUpdate = (task) => {
|
|
247
|
-
const request = inflightStatusUpdate.catch(() => void 0).then(async () => {
|
|
248
|
-
await task();
|
|
249
|
-
});
|
|
250
|
-
inflightStatusUpdate = request.catch(() => void 0);
|
|
251
|
-
return request;
|
|
252
|
-
};
|
|
253
|
-
const scheduleRotation = () => {
|
|
254
|
-
if (rotationTimer) {
|
|
255
|
-
clearTimer(rotationTimer);
|
|
256
|
-
rotationTimer = null;
|
|
257
|
-
}
|
|
258
|
-
if (!active || !currentVisibleStatus) {
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
rotationTimer = setTimer(() => {
|
|
262
|
-
rotationTimer = null;
|
|
263
|
-
if (!active || !currentVisibleStatus) {
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
void postStatus(currentVisibleStatus, currentLoadingMessages);
|
|
267
|
-
}, STATUS_ROTATION_INTERVAL_MS);
|
|
268
|
-
};
|
|
269
|
-
const getLoadingMessagesForVisibleStatus = (visible) => visible ? [visible] : void 0;
|
|
270
|
-
const getInitialStatusText = () => {
|
|
271
|
-
if (loadingMessages?.length) {
|
|
272
|
-
return loadingMessages[0];
|
|
273
|
-
}
|
|
274
|
-
return defaultStatus.text;
|
|
275
|
-
};
|
|
276
|
-
const haveSameLoadingMessages = (left, right) => {
|
|
277
|
-
if (left === right) {
|
|
278
|
-
return true;
|
|
279
|
-
}
|
|
280
|
-
if (!left || !right || left.length !== right.length) {
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
return left.every((message, index) => message === right[index]);
|
|
284
|
-
};
|
|
285
|
-
const postStatus = async (text, nextLoadingMessages) => {
|
|
286
|
-
if (!text && !currentVisibleStatus) {
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
currentVisibleStatus = text;
|
|
290
|
-
currentLoadingMessages = nextLoadingMessages;
|
|
291
|
-
lastStatusAt = now();
|
|
292
|
-
scheduleRotation();
|
|
293
|
-
await enqueueStatusUpdate(async () => {
|
|
294
|
-
await args.sendStatus(text, nextLoadingMessages);
|
|
295
|
-
});
|
|
296
|
-
};
|
|
297
|
-
const postRenderedStatus = async (status) => {
|
|
298
|
-
const presentation = renderAssistantStatus({
|
|
299
|
-
status
|
|
300
|
-
});
|
|
301
|
-
const nextLoadingMessages = getLoadingMessagesForVisibleStatus(
|
|
302
|
-
presentation.visible
|
|
303
|
-
);
|
|
304
|
-
currentKey = presentation.key;
|
|
305
|
-
await postStatus(presentation.visible, nextLoadingMessages);
|
|
306
|
-
};
|
|
307
|
-
const clearPending = () => {
|
|
308
|
-
if (pendingTimer) {
|
|
309
|
-
clearTimer(pendingTimer);
|
|
310
|
-
pendingTimer = null;
|
|
311
|
-
}
|
|
312
|
-
pendingStatus = null;
|
|
313
|
-
pendingKey = "";
|
|
314
|
-
};
|
|
315
|
-
const flushPending = async () => {
|
|
316
|
-
if (!active || !pendingStatus) {
|
|
317
|
-
clearPending();
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
const next = pendingStatus;
|
|
321
|
-
clearPending();
|
|
322
|
-
const nextPresentation = renderAssistantStatus({
|
|
323
|
-
status: next
|
|
324
|
-
});
|
|
325
|
-
if (nextPresentation.key !== currentKey) {
|
|
326
|
-
await postRenderedStatus(next);
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
return {
|
|
330
|
-
async clear() {
|
|
331
|
-
active = false;
|
|
332
|
-
clearPending();
|
|
333
|
-
if (rotationTimer) {
|
|
334
|
-
clearTimer(rotationTimer);
|
|
335
|
-
rotationTimer = null;
|
|
336
|
-
}
|
|
337
|
-
currentKey = "";
|
|
338
|
-
await postStatus("");
|
|
339
|
-
},
|
|
340
|
-
update(status) {
|
|
341
|
-
if (!active) {
|
|
342
|
-
active = true;
|
|
343
|
-
clearPending();
|
|
344
|
-
if (status) {
|
|
345
|
-
void postRenderedStatus(status);
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
currentKey = "initial";
|
|
349
|
-
void postStatus(getInitialStatusText(), loadingMessages);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (!status) {
|
|
353
|
-
clearPending();
|
|
354
|
-
currentKey = "initial";
|
|
355
|
-
void postStatus(getInitialStatusText(), loadingMessages);
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
const presentation = renderAssistantStatus({
|
|
359
|
-
status
|
|
360
|
-
});
|
|
361
|
-
if (!presentation.visible) {
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
if (presentation.key === currentKey || presentation.key === pendingKey) {
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
if (presentation.visible === currentVisibleStatus) {
|
|
368
|
-
clearPending();
|
|
369
|
-
currentKey = presentation.key;
|
|
370
|
-
const nextLoadingMessages = getLoadingMessagesForVisibleStatus(
|
|
371
|
-
presentation.visible
|
|
372
|
-
);
|
|
373
|
-
if (!haveSameLoadingMessages(currentLoadingMessages, nextLoadingMessages)) {
|
|
374
|
-
void postStatus(presentation.visible, nextLoadingMessages);
|
|
375
|
-
}
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
const elapsed = now() - lastStatusAt;
|
|
379
|
-
const waitMs = Math.max(
|
|
380
|
-
STATUS_UPDATE_DEBOUNCE_MS - elapsed,
|
|
381
|
-
STATUS_MIN_VISIBLE_MS - elapsed,
|
|
382
|
-
0
|
|
383
|
-
);
|
|
384
|
-
if (waitMs <= 0) {
|
|
385
|
-
clearPending();
|
|
386
|
-
void postRenderedStatus(status);
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
pendingStatus = status;
|
|
390
|
-
pendingKey = presentation.key;
|
|
391
|
-
if (pendingTimer) {
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
pendingTimer = setTimer(
|
|
395
|
-
() => {
|
|
396
|
-
pendingTimer = null;
|
|
397
|
-
void flushPending();
|
|
398
|
-
},
|
|
399
|
-
Math.max(1, waitMs)
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// src/chat/slack/assistant-thread/status-send.ts
|
|
406
|
-
var SLACK_ASSISTANT_ACTIVE_STATUS = "is working on your request...";
|
|
407
|
-
function createSlackAdapterStatusSender(args) {
|
|
408
|
-
const adapter = args.getSlackAdapter();
|
|
409
|
-
const boundToken = getSlackAdapterRequestToken(adapter);
|
|
410
|
-
return async (text, loadingMessages) => {
|
|
411
|
-
const channelId = args.channelId;
|
|
412
|
-
const threadTs = args.threadTs;
|
|
413
|
-
if (!channelId || !threadTs) {
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
const normalizedChannelId = normalizeSlackConversationId(channelId);
|
|
417
|
-
if (!normalizedChannelId) {
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
|
|
421
|
-
try {
|
|
422
|
-
await runWithBoundSlackToken(
|
|
423
|
-
adapter,
|
|
424
|
-
boundToken,
|
|
425
|
-
() => adapter.setAssistantStatus(
|
|
426
|
-
normalizedChannelId,
|
|
427
|
-
threadTs,
|
|
428
|
-
text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
|
|
429
|
-
nextLoadingMessages
|
|
430
|
-
)
|
|
431
|
-
);
|
|
432
|
-
} catch (error) {
|
|
433
|
-
logAssistantStatusFailure({
|
|
434
|
-
status: text,
|
|
435
|
-
error,
|
|
436
|
-
channelId,
|
|
437
|
-
normalizedChannelId,
|
|
438
|
-
threadTs
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
function createSlackWebApiStatusSender(args) {
|
|
444
|
-
const getClient = args.getSlackClient ?? getSlackClient;
|
|
445
|
-
return async (text, loadingMessages) => {
|
|
446
|
-
const channelId = args.channelId;
|
|
447
|
-
const threadTs = args.threadTs;
|
|
448
|
-
if (!channelId || !threadTs) {
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
const normalizedChannelId = normalizeSlackConversationId(channelId);
|
|
452
|
-
if (!normalizedChannelId) {
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
|
|
456
|
-
try {
|
|
457
|
-
await getClient().assistant.threads.setStatus({
|
|
458
|
-
channel_id: normalizedChannelId,
|
|
459
|
-
thread_ts: threadTs,
|
|
460
|
-
status: text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
|
|
461
|
-
...nextLoadingMessages ? { loading_messages: nextLoadingMessages } : void 0
|
|
462
|
-
});
|
|
463
|
-
} catch (error) {
|
|
464
|
-
logAssistantStatusFailure({
|
|
465
|
-
status: text,
|
|
466
|
-
error,
|
|
467
|
-
channelId,
|
|
468
|
-
normalizedChannelId,
|
|
469
|
-
threadTs
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
function getSlackAdapterRequestToken(adapter) {
|
|
475
|
-
const token = adapter.requestContext?.getStore()?.token;
|
|
476
|
-
if (typeof token !== "string") {
|
|
477
|
-
return void 0;
|
|
478
|
-
}
|
|
479
|
-
const trimmed = token.trim();
|
|
480
|
-
return trimmed || void 0;
|
|
481
|
-
}
|
|
482
|
-
async function runWithBoundSlackToken(adapter, token, task) {
|
|
483
|
-
if (!token) {
|
|
484
|
-
return await task();
|
|
485
|
-
}
|
|
486
|
-
return await adapter.withBotToken(token, task);
|
|
487
|
-
}
|
|
488
|
-
function logAssistantStatusFailure(args) {
|
|
489
|
-
logWarn("assistant.status.update.failed", {
|
|
490
|
-
"app.slack.status_text": args.status || "(clear)",
|
|
491
|
-
"app.slack.channel_id_raw": args.channelId,
|
|
492
|
-
"app.slack.channel_id": args.normalizedChannelId,
|
|
493
|
-
"app.slack.thread_ts": args.threadTs,
|
|
494
|
-
"exception.message": args.error instanceof Error ? args.error.message : String(args.error)
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// src/chat/slack/assistant-thread/status.ts
|
|
499
|
-
function createSlackAdapterAssistantStatusSession(args) {
|
|
500
|
-
return createAssistantStatusScheduler({
|
|
501
|
-
sendStatus: createSlackAdapterStatusSender({
|
|
502
|
-
channelId: args.channelId,
|
|
503
|
-
threadTs: args.threadTs,
|
|
504
|
-
getSlackAdapter: args.getSlackAdapter
|
|
505
|
-
}),
|
|
506
|
-
loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
|
|
507
|
-
now: args.now,
|
|
508
|
-
setTimer: args.setTimer,
|
|
509
|
-
clearTimer: args.clearTimer,
|
|
510
|
-
random: args.random
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
function createSlackWebApiAssistantStatusSession(args) {
|
|
514
|
-
return createAssistantStatusScheduler({
|
|
515
|
-
sendStatus: createSlackWebApiStatusSender({
|
|
516
|
-
channelId: args.channelId,
|
|
517
|
-
threadTs: args.threadTs,
|
|
518
|
-
getSlackClient: args.getSlackClient
|
|
519
|
-
}),
|
|
520
|
-
loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
|
|
521
|
-
now: args.now,
|
|
522
|
-
setTimer: args.setTimer,
|
|
523
|
-
clearTimer: args.clearTimer,
|
|
524
|
-
random: args.random
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// src/chat/slack/footer.ts
|
|
529
|
-
function formatReplyAttribution(attribution) {
|
|
530
|
-
return attribution.detail ? `${attribution.label} \xB7 ${attribution.detail}` : attribution.label;
|
|
531
|
-
}
|
|
532
|
-
function buildSlackReplyFooter(args) {
|
|
533
|
-
const items = [];
|
|
534
|
-
const conversationId = args.conversationId?.trim();
|
|
535
|
-
if (conversationId) {
|
|
536
|
-
const idItem = {
|
|
537
|
-
label: "ID",
|
|
538
|
-
value: conversationId
|
|
539
|
-
};
|
|
540
|
-
const conversationUrl = getPluginSlackConversationLink(conversationId)?.url ?? getDashboardConversationLink(conversationId) ?? buildSentryConversationUrl(conversationId);
|
|
541
|
-
if (conversationUrl) {
|
|
542
|
-
idItem.url = conversationUrl;
|
|
543
|
-
}
|
|
544
|
-
items.push(idItem);
|
|
545
|
-
}
|
|
546
|
-
return items.length > 0 || args.replyAttribution ? {
|
|
547
|
-
...args.replyAttribution ? { attribution: args.replyAttribution } : void 0,
|
|
548
|
-
items
|
|
549
|
-
} : void 0;
|
|
550
|
-
}
|
|
551
|
-
function buildSlackReplyBlocks(text, footer) {
|
|
552
|
-
if (!text.trim()) {
|
|
553
|
-
return void 0;
|
|
554
|
-
}
|
|
555
|
-
const blocks = [
|
|
556
|
-
{
|
|
557
|
-
type: "markdown",
|
|
558
|
-
text
|
|
559
|
-
}
|
|
560
|
-
];
|
|
561
|
-
if (footer && (footer.attribution || footer.items.length > 0)) {
|
|
562
|
-
const attributionElements = footer.attribution ? [
|
|
563
|
-
{
|
|
564
|
-
type: "plain_text",
|
|
565
|
-
text: formatReplyAttribution(footer.attribution)
|
|
566
|
-
}
|
|
567
|
-
] : [];
|
|
568
|
-
blocks.push({
|
|
569
|
-
type: "context",
|
|
570
|
-
elements: [
|
|
571
|
-
...attributionElements,
|
|
572
|
-
...footer.items.map((item) => ({
|
|
573
|
-
type: "mrkdwn",
|
|
574
|
-
text: item.url ? `*${escapeSlackMrkdwnText(item.label)}:* ${formatSlackLink(item.url, item.value)}` : `*${escapeSlackMrkdwnText(item.label)}:* ${escapeSlackMrkdwnText(item.value)}`
|
|
575
|
-
}))
|
|
576
|
-
]
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
return blocks;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// src/chat/slack/output.ts
|
|
583
|
-
var MAX_INLINE_CHARS = 2200;
|
|
584
|
-
var MAX_INLINE_LINES = 45;
|
|
585
|
-
var CONTINUED_MARKER = "\n\n[Continued below]";
|
|
586
|
-
function countSlackLines(text) {
|
|
587
|
-
if (!text) {
|
|
588
|
-
return 0;
|
|
589
|
-
}
|
|
590
|
-
return text.split("\n").length;
|
|
591
|
-
}
|
|
592
|
-
function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
593
|
-
return text.length <= maxChars && countSlackLines(text) <= maxLines;
|
|
594
|
-
}
|
|
595
|
-
function findSplitIndex(text, maxChars) {
|
|
596
|
-
if (text.length <= maxChars) {
|
|
597
|
-
return text.length;
|
|
598
|
-
}
|
|
599
|
-
const bounded = text.slice(0, maxChars);
|
|
600
|
-
const newlineIndex = bounded.lastIndexOf("\n");
|
|
601
|
-
if (newlineIndex > 0) {
|
|
602
|
-
return newlineIndex;
|
|
603
|
-
}
|
|
604
|
-
const spaceIndex = bounded.lastIndexOf(" ");
|
|
605
|
-
if (spaceIndex > 0) {
|
|
606
|
-
return spaceIndex;
|
|
607
|
-
}
|
|
608
|
-
return maxChars;
|
|
609
|
-
}
|
|
610
|
-
function splitByLineBudget(text, maxLines) {
|
|
611
|
-
if (maxLines <= 0) {
|
|
612
|
-
return "";
|
|
613
|
-
}
|
|
614
|
-
const lines = text.split("\n");
|
|
615
|
-
if (lines.length <= maxLines) {
|
|
616
|
-
return text;
|
|
617
|
-
}
|
|
618
|
-
return lines.slice(0, maxLines).join("\n");
|
|
619
|
-
}
|
|
620
|
-
function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
621
|
-
return {
|
|
622
|
-
maxChars: Math.max(1, maxChars - suffix.length),
|
|
623
|
-
maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
function forceSplitBudget(text, budget) {
|
|
627
|
-
const lineCount = countSlackLines(text);
|
|
628
|
-
return {
|
|
629
|
-
maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
|
|
630
|
-
maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
function getFenceContinuation(text) {
|
|
634
|
-
let open;
|
|
635
|
-
for (const line of text.split("\n")) {
|
|
636
|
-
const trimmed = line.trimStart();
|
|
637
|
-
const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
|
|
638
|
-
if (!openerMatch) {
|
|
639
|
-
continue;
|
|
640
|
-
}
|
|
641
|
-
if (!open) {
|
|
642
|
-
open = {
|
|
643
|
-
fence: openerMatch[1],
|
|
644
|
-
openerLine: trimmed
|
|
645
|
-
};
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
|
|
649
|
-
open = void 0;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
if (!open) {
|
|
653
|
-
return null;
|
|
654
|
-
}
|
|
655
|
-
return {
|
|
656
|
-
closeSuffix: text.endsWith("\n") ? open.fence : `
|
|
657
|
-
${open.fence}`,
|
|
658
|
-
reopenPrefix: `${open.openerLine}
|
|
659
|
-
`
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
function appendSlackSuffix(text, marker) {
|
|
663
|
-
const carryover = getFenceContinuation(text);
|
|
664
|
-
return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
|
|
665
|
-
}
|
|
666
|
-
function stripTrailingContinuationMarker(text) {
|
|
667
|
-
return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
|
|
668
|
-
}
|
|
669
|
-
function takeSlackContinuationChunk(text, budget) {
|
|
670
|
-
let { prefix, rest } = takeSlackInlinePrefix(text, budget);
|
|
671
|
-
if (!rest) {
|
|
672
|
-
({ prefix, rest } = takeSlackInlinePrefix(
|
|
673
|
-
text,
|
|
674
|
-
forceSplitBudget(text, budget)
|
|
675
|
-
));
|
|
676
|
-
}
|
|
677
|
-
let carryover = rest ? getFenceContinuation(prefix) : null;
|
|
678
|
-
if (!carryover) {
|
|
679
|
-
return { prefix, rest };
|
|
680
|
-
}
|
|
681
|
-
const carryoverBudget = reserveInlineBudgetForSuffix(
|
|
682
|
-
`${carryover.closeSuffix}${CONTINUED_MARKER}`
|
|
683
|
-
);
|
|
684
|
-
({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
|
|
685
|
-
if (!rest) {
|
|
686
|
-
({ prefix, rest } = takeSlackInlinePrefix(
|
|
687
|
-
text,
|
|
688
|
-
forceSplitBudget(text, carryoverBudget)
|
|
689
|
-
));
|
|
690
|
-
}
|
|
691
|
-
carryover = rest ? getFenceContinuation(prefix) : null;
|
|
692
|
-
if (!carryover) {
|
|
693
|
-
return { prefix, rest };
|
|
694
|
-
}
|
|
695
|
-
return {
|
|
696
|
-
prefix,
|
|
697
|
-
rest: `${carryover.reopenPrefix}${rest}`
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
function takeSlackContinuationPrefix(text, options) {
|
|
701
|
-
const budget = {
|
|
702
|
-
maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
|
|
703
|
-
maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
|
|
704
|
-
};
|
|
705
|
-
const { prefix, rest } = (() => {
|
|
706
|
-
if (options?.forceSplit) {
|
|
707
|
-
return takeSlackContinuationChunk(text, budget);
|
|
708
|
-
}
|
|
709
|
-
const initial = takeSlackInlinePrefix(text, budget);
|
|
710
|
-
return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
|
|
711
|
-
})();
|
|
712
|
-
return {
|
|
713
|
-
prefix,
|
|
714
|
-
renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
|
|
715
|
-
rest
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
function takeSlackInlinePrefix(text, options) {
|
|
719
|
-
const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
|
|
720
|
-
const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
|
|
721
|
-
const normalized = text.replace(/\r\n?/g, "\n");
|
|
722
|
-
if (!normalized) {
|
|
723
|
-
return { prefix: "", rest: "" };
|
|
724
|
-
}
|
|
725
|
-
if (fitsInlineBudget(normalized, maxChars, maxLines)) {
|
|
726
|
-
return { prefix: normalized, rest: "" };
|
|
727
|
-
}
|
|
728
|
-
const lineBounded = splitByLineBudget(normalized, maxLines);
|
|
729
|
-
const cutIndex = findSplitIndex(lineBounded, maxChars);
|
|
730
|
-
const prefix = lineBounded.slice(0, cutIndex).trimEnd();
|
|
731
|
-
if (prefix) {
|
|
732
|
-
return {
|
|
733
|
-
prefix,
|
|
734
|
-
rest: normalized.slice(prefix.length).trimStart()
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
|
|
738
|
-
return {
|
|
739
|
-
prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
|
|
740
|
-
rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
function splitSlackReplyText(text, options) {
|
|
744
|
-
const normalized = normalizeSlackReplyMarkdown(text);
|
|
745
|
-
if (!normalized) {
|
|
746
|
-
return [];
|
|
747
|
-
}
|
|
748
|
-
const chunks = [];
|
|
749
|
-
const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
750
|
-
let remaining = normalized;
|
|
751
|
-
while (remaining) {
|
|
752
|
-
const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
|
|
753
|
-
if (fitsFinalChunk) {
|
|
754
|
-
chunks.push(
|
|
755
|
-
options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
|
|
756
|
-
);
|
|
757
|
-
break;
|
|
758
|
-
}
|
|
759
|
-
const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
|
|
760
|
-
...continuationBudget,
|
|
761
|
-
forceSplit: true
|
|
762
|
-
});
|
|
763
|
-
chunks.push(renderedPrefix);
|
|
764
|
-
remaining = rest;
|
|
765
|
-
}
|
|
766
|
-
if (chunks.length === 2) {
|
|
767
|
-
chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
|
|
768
|
-
}
|
|
769
|
-
return chunks;
|
|
770
|
-
}
|
|
771
|
-
function getSlackContinuationBudget() {
|
|
772
|
-
return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
773
|
-
}
|
|
774
|
-
function buildSlackOutputMessage(text) {
|
|
775
|
-
const normalized = normalizeSlackReplyMarkdown(text);
|
|
776
|
-
if (!normalized) {
|
|
777
|
-
throw new Error(
|
|
778
|
-
`Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
|
|
779
|
-
);
|
|
780
|
-
}
|
|
781
|
-
return {
|
|
782
|
-
markdown: normalized
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// src/chat/slack/reply.ts
|
|
787
|
-
async function sendSlackReply(args) {
|
|
788
|
-
const chunks = splitSlackReplyText(args.text);
|
|
789
|
-
const footer = buildSlackReplyFooter({
|
|
790
|
-
conversationId: args.conversationId,
|
|
791
|
-
replyAttribution: args.replyAttribution
|
|
792
|
-
});
|
|
793
|
-
const messageTs = [];
|
|
794
|
-
let threadTs = args.threadTs;
|
|
795
|
-
for (const [index, text] of chunks.entries()) {
|
|
796
|
-
const isFinalChunk = index === chunks.length - 1;
|
|
797
|
-
const blocks = buildSlackReplyBlocks(
|
|
798
|
-
text,
|
|
799
|
-
isFinalChunk ? footer : void 0
|
|
800
|
-
);
|
|
801
|
-
const fallbackText = isFinalChunk && args.replyAttribution ? `${text}
|
|
802
|
-
|
|
803
|
-
${escapeSlackMrkdwnText(formatReplyAttribution(args.replyAttribution))}` : text;
|
|
804
|
-
const response = await postSlackMessage({
|
|
805
|
-
channelId: args.channelId,
|
|
806
|
-
threadTs,
|
|
807
|
-
text: fallbackText,
|
|
808
|
-
...blocks ? { blocks } : void 0
|
|
809
|
-
});
|
|
810
|
-
if (response.ts) {
|
|
811
|
-
messageTs.push(response.ts);
|
|
812
|
-
threadTs ??= response.ts;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
return messageTs;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// src/chat/slack/errors.ts
|
|
819
|
-
function getSlackApiErrorCode(error) {
|
|
820
|
-
if (!error || typeof error !== "object") {
|
|
821
|
-
return void 0;
|
|
822
|
-
}
|
|
823
|
-
const candidate = error;
|
|
824
|
-
if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
|
|
825
|
-
return candidate.data.error;
|
|
826
|
-
}
|
|
827
|
-
if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
|
|
828
|
-
return candidate.code;
|
|
829
|
-
}
|
|
830
|
-
return void 0;
|
|
831
|
-
}
|
|
832
|
-
function getSlackErrorObservabilityAttributes(error) {
|
|
833
|
-
if (!error || typeof error !== "object") {
|
|
834
|
-
return {};
|
|
835
|
-
}
|
|
836
|
-
const candidate = error;
|
|
837
|
-
const attributes = {};
|
|
838
|
-
if (typeof candidate.code === "string" && candidate.code.trim().length > 0) {
|
|
839
|
-
attributes["app.slack.error_code"] = candidate.code;
|
|
840
|
-
}
|
|
841
|
-
if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) {
|
|
842
|
-
attributes["app.slack.api_error"] = candidate.data.error;
|
|
843
|
-
}
|
|
844
|
-
const requestId = getHeaderString(candidate.headers, "x-slack-req-id");
|
|
845
|
-
if (requestId) {
|
|
846
|
-
attributes["app.slack.request_id"] = requestId;
|
|
847
|
-
}
|
|
848
|
-
if (typeof candidate.statusCode === "number" && Number.isFinite(candidate.statusCode)) {
|
|
849
|
-
attributes["http.response.status_code"] = candidate.statusCode;
|
|
850
|
-
}
|
|
851
|
-
return attributes;
|
|
852
|
-
}
|
|
853
|
-
var TRANSIENT_SLACK_API_ERRORS = /* @__PURE__ */ new Set([
|
|
854
|
-
"fatal_error",
|
|
855
|
-
"internal_error",
|
|
856
|
-
// chat.postMessage documents both spellings.
|
|
857
|
-
"rate_limited",
|
|
858
|
-
"ratelimited",
|
|
859
|
-
"request_timeout",
|
|
860
|
-
"service_unavailable"
|
|
861
|
-
]);
|
|
862
|
-
function isRetryableSlackPostError(error) {
|
|
863
|
-
if (!error || typeof error !== "object") {
|
|
864
|
-
return true;
|
|
865
|
-
}
|
|
866
|
-
const candidate = error;
|
|
867
|
-
if (typeof candidate.statusCode === "number") {
|
|
868
|
-
if (candidate.statusCode === 429 || candidate.statusCode >= 500) {
|
|
869
|
-
return true;
|
|
870
|
-
}
|
|
871
|
-
if (candidate.statusCode >= 400) {
|
|
872
|
-
return false;
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
const apiError = error instanceof SlackActionError ? error.apiError : typeof candidate.data?.error === "string" ? candidate.data.error : void 0;
|
|
876
|
-
if (apiError) {
|
|
877
|
-
return TRANSIENT_SLACK_API_ERRORS.has(apiError);
|
|
878
|
-
}
|
|
879
|
-
if (error instanceof SlackActionError) {
|
|
880
|
-
return error.code === "rate_limited" || error.code === "internal_error";
|
|
881
|
-
}
|
|
882
|
-
return true;
|
|
883
|
-
}
|
|
884
|
-
function isSlackTitlePermissionError(error) {
|
|
885
|
-
const code = getSlackApiErrorCode(error);
|
|
886
|
-
return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// src/chat/runtime/thread-context.ts
|
|
890
|
-
function escapeRegExp(value) {
|
|
891
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
892
|
-
}
|
|
893
|
-
function readOptionalStringField(value, key) {
|
|
894
|
-
if (!(key in value)) {
|
|
895
|
-
return void 0;
|
|
896
|
-
}
|
|
897
|
-
return toOptionalString(value[key]);
|
|
898
|
-
}
|
|
899
|
-
function stripLeadingBotMention(text, options = {}) {
|
|
900
|
-
if (!text.trim()) return text;
|
|
901
|
-
let next = text;
|
|
902
|
-
if (options.stripLeadingSlackMentionToken) {
|
|
903
|
-
if (options.botUserId) {
|
|
904
|
-
const botUserId = escapeRegExp(options.botUserId);
|
|
905
|
-
const mentionByBotUserIdRe = new RegExp(
|
|
906
|
-
`^\\s*(?:<@${botUserId}(?:\\|[^>]+)?>|@${botUserId})[\\s,:-]*`,
|
|
907
|
-
"i"
|
|
908
|
-
);
|
|
909
|
-
next = next.replace(mentionByBotUserIdRe, "").trim();
|
|
910
|
-
} else {
|
|
911
|
-
next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
const mentionByNameRe = new RegExp(
|
|
915
|
-
`^\\s*@${escapeRegExp(botConfig.userName)}\\b[\\s,:-]*`,
|
|
916
|
-
"i"
|
|
917
|
-
);
|
|
918
|
-
next = next.replace(mentionByNameRe, "").trim();
|
|
919
|
-
const mentionByLabeledEntityRe = new RegExp(
|
|
920
|
-
`^\\s*<@[^>|]+\\|${escapeRegExp(botConfig.userName)}>[\\s,:-]*`,
|
|
921
|
-
"i"
|
|
922
|
-
);
|
|
923
|
-
next = next.replace(mentionByLabeledEntityRe, "").trim();
|
|
924
|
-
return next;
|
|
925
|
-
}
|
|
926
|
-
function getThreadId(thread, _message) {
|
|
927
|
-
return toOptionalString(thread.id);
|
|
928
|
-
}
|
|
929
|
-
function getRunId(thread, message) {
|
|
930
|
-
return readOptionalStringField(thread, "runId") ?? readOptionalStringField(message, "runId");
|
|
931
|
-
}
|
|
932
|
-
function getChannelId(thread, message) {
|
|
933
|
-
return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
|
|
934
|
-
}
|
|
935
|
-
function getThreadTs(threadId) {
|
|
936
|
-
return parseSlackThreadId(threadId)?.threadTs;
|
|
937
|
-
}
|
|
938
|
-
function getAssistantThreadContext(message) {
|
|
939
|
-
const rawContext = readSlackRawMessageContext(message);
|
|
940
|
-
const channelId = rawContext?.channelId;
|
|
941
|
-
if (channelId) {
|
|
942
|
-
const threadTs = isDmChannel(channelId) ? rawContext.threadTs : rawContext.threadTs ?? rawContext.messageTs;
|
|
943
|
-
if (threadTs) {
|
|
944
|
-
return { channelId, threadTs };
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
const parsedThreadId = parseSlackThreadId(
|
|
948
|
-
readOptionalStringField(message, "threadId")
|
|
949
|
-
);
|
|
950
|
-
if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) {
|
|
951
|
-
return void 0;
|
|
952
|
-
}
|
|
953
|
-
return parsedThreadId;
|
|
954
|
-
}
|
|
955
|
-
function getMessageTs(message) {
|
|
956
|
-
const directTs = readOptionalStringField(message, "ts");
|
|
957
|
-
const parsedDirectTs = parseSlackMessageTs(directTs);
|
|
958
|
-
if (parsedDirectTs) {
|
|
959
|
-
return parsedDirectTs;
|
|
960
|
-
}
|
|
961
|
-
const rawContext = readSlackRawMessageContext(message);
|
|
962
|
-
if (!rawContext) {
|
|
963
|
-
return void 0;
|
|
964
|
-
}
|
|
965
|
-
const candidates = [rawContext.messageTs, rawContext.nestedMessageTs];
|
|
966
|
-
for (const candidate of candidates) {
|
|
967
|
-
const ts = parseSlackMessageTs(candidate);
|
|
968
|
-
if (ts) {
|
|
969
|
-
return ts;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
return void 0;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// src/chat/providers/slack/processing-reaction.ts
|
|
976
|
-
var noProcessingReaction = {
|
|
977
|
-
complete: async () => void 0,
|
|
978
|
-
keep: () => void 0,
|
|
979
|
-
stop: async () => void 0
|
|
980
|
-
};
|
|
981
|
-
function isProcessingReactionEmoji(value) {
|
|
982
|
-
return typeof value === "string" && normalizeSlackEmojiName(value) === getChatConfig().slack.processingReactionEmoji;
|
|
983
|
-
}
|
|
984
|
-
function shouldKeepProcessingReactionForToolInvocation(input) {
|
|
985
|
-
return input.toolName === "addReaction" && isProcessingReactionEmoji(input.params.emoji);
|
|
986
|
-
}
|
|
987
|
-
async function startProcessingReaction(args) {
|
|
988
|
-
if (args.message.author.isMe) {
|
|
989
|
-
return noProcessingReaction;
|
|
990
|
-
}
|
|
991
|
-
const channelId = getChannelId(args.thread, args.message);
|
|
992
|
-
const messageTs = getMessageTs(args.message);
|
|
993
|
-
if (!channelId || !messageTs) {
|
|
994
|
-
return noProcessingReaction;
|
|
995
|
-
}
|
|
996
|
-
return startProcessingReactionForMessage({
|
|
997
|
-
channelId,
|
|
998
|
-
timestamp: messageTs
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
async function startProcessingReactionForMessage(args) {
|
|
1002
|
-
try {
|
|
1003
|
-
await addReactionToMessage({
|
|
1004
|
-
channelId: args.channelId,
|
|
1005
|
-
timestamp: args.timestamp,
|
|
1006
|
-
emoji: getChatConfig().slack.processingReactionEmoji
|
|
1007
|
-
});
|
|
1008
|
-
} catch (error) {
|
|
1009
|
-
logException(error, "slack.processing.reaction_add.failed", {
|
|
1010
|
-
"app.slack.action": "reactions.add",
|
|
1011
|
-
"messaging.message.id": args.timestamp,
|
|
1012
|
-
...getSlackErrorObservabilityAttributes(error)
|
|
1013
|
-
});
|
|
1014
|
-
return noProcessingReaction;
|
|
1015
|
-
}
|
|
1016
|
-
let shouldRemove = true;
|
|
1017
|
-
const removeProcessingReaction = async () => {
|
|
1018
|
-
if (!shouldRemove) {
|
|
1019
|
-
return false;
|
|
1020
|
-
}
|
|
1021
|
-
try {
|
|
1022
|
-
await removeReactionFromMessage({
|
|
1023
|
-
channelId: args.channelId,
|
|
1024
|
-
timestamp: args.timestamp,
|
|
1025
|
-
emoji: getChatConfig().slack.processingReactionEmoji
|
|
1026
|
-
});
|
|
1027
|
-
return true;
|
|
1028
|
-
} catch (error) {
|
|
1029
|
-
logException(error, "slack.processing.reaction_remove.failed", {
|
|
1030
|
-
"app.slack.action": "reactions.remove",
|
|
1031
|
-
"messaging.message.id": args.timestamp,
|
|
1032
|
-
...getSlackErrorObservabilityAttributes(error)
|
|
1033
|
-
});
|
|
1034
|
-
return false;
|
|
1035
|
-
}
|
|
1036
|
-
};
|
|
1037
|
-
return {
|
|
1038
|
-
complete: async () => {
|
|
1039
|
-
const shouldAddCompleted = shouldRemove;
|
|
1040
|
-
await removeProcessingReaction();
|
|
1041
|
-
if (!shouldAddCompleted) {
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
try {
|
|
1045
|
-
await addReactionToMessage({
|
|
1046
|
-
channelId: args.channelId,
|
|
1047
|
-
timestamp: args.timestamp,
|
|
1048
|
-
emoji: getChatConfig().slack.completedReactionEmoji
|
|
1049
|
-
});
|
|
1050
|
-
} catch (error) {
|
|
1051
|
-
logException(error, "slack.processing.reaction_complete.failed", {
|
|
1052
|
-
"app.slack.action": "reactions.add",
|
|
1053
|
-
"messaging.message.id": args.timestamp,
|
|
1054
|
-
...getSlackErrorObservabilityAttributes(error)
|
|
1055
|
-
});
|
|
1056
|
-
}
|
|
1057
|
-
},
|
|
1058
|
-
keep: () => {
|
|
1059
|
-
shouldRemove = false;
|
|
1060
|
-
},
|
|
1061
|
-
stop: async () => {
|
|
1062
|
-
await removeProcessingReaction();
|
|
1063
|
-
}
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
// src/chat/services/auth-pause-response.ts
|
|
1068
|
-
var MAX_AUTH_REQUEST_LENGTH = 200;
|
|
1069
|
-
function formatAuthRequest(requestText) {
|
|
1070
|
-
const normalized = requestText.replace(/\s+/g, " ").trim();
|
|
1071
|
-
if (!normalized) {
|
|
1072
|
-
return void 0;
|
|
1073
|
-
}
|
|
1074
|
-
const bounded = normalized.length > MAX_AUTH_REQUEST_LENGTH ? `${normalized.slice(0, MAX_AUTH_REQUEST_LENGTH - 1).trimEnd()}\u2026` : normalized;
|
|
1075
|
-
return bounded.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
1076
|
-
}
|
|
1077
|
-
function buildAuthPauseResponse(slackUserId, providerDisplayName, requestText) {
|
|
1078
|
-
const mention = slackUserId ? `<@${slackUserId}> ` : "";
|
|
1079
|
-
const request = requestText ? formatAuthRequest(requestText) : void 0;
|
|
1080
|
-
if (!request) {
|
|
1081
|
-
return `${mention}I'll need you to authorize ${providerDisplayName}. I sent you a link.`;
|
|
1082
|
-
}
|
|
1083
|
-
return `${mention}I need access to ${providerDisplayName} to continue.
|
|
1084
|
-
|
|
1085
|
-
*Why:* ${request}
|
|
1086
|
-
|
|
1087
|
-
I sent you a link.`;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// src/chat/providers/slack/resume.ts
|
|
1091
|
-
function resolveReplyTimeoutMs() {
|
|
1092
|
-
const raw = process.env.EVAL_AGENT_REPLY_TIMEOUT_MS?.trim();
|
|
1093
|
-
if (!raw) {
|
|
1094
|
-
return void 0;
|
|
1095
|
-
}
|
|
1096
|
-
const parsed = Number.parseInt(raw, 10);
|
|
1097
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
1098
|
-
}
|
|
1099
|
-
async function postSlackMessageBestEffort(channelId, threadTs, text, conversationId, replyAttribution) {
|
|
1100
|
-
try {
|
|
1101
|
-
if (conversationId) {
|
|
1102
|
-
await sendSlackReply({
|
|
1103
|
-
channelId,
|
|
1104
|
-
conversationId,
|
|
1105
|
-
replyAttribution,
|
|
1106
|
-
text,
|
|
1107
|
-
threadTs
|
|
1108
|
-
});
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
await postSlackMessage({ channelId, threadTs, text });
|
|
1112
|
-
} catch {
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
function createReadOnlyConfigService(rawValues) {
|
|
1116
|
-
const values = Object.fromEntries(
|
|
1117
|
-
Object.entries(rawValues).map(([key, value]) => [
|
|
1118
|
-
key,
|
|
1119
|
-
configValueSchema.parse(value)
|
|
1120
|
-
])
|
|
1121
|
-
);
|
|
1122
|
-
const entries = Object.entries(values).map(([key, value]) => ({
|
|
1123
|
-
key,
|
|
1124
|
-
value,
|
|
1125
|
-
scope: "location",
|
|
1126
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1127
|
-
}));
|
|
1128
|
-
return {
|
|
1129
|
-
get: async (key) => entries.find((entry) => entry.key === key),
|
|
1130
|
-
set: async () => {
|
|
1131
|
-
throw new Error("Configuration is read-only for a resumed Run");
|
|
1132
|
-
},
|
|
1133
|
-
unset: async () => false,
|
|
1134
|
-
list: async ({ prefix } = {}) => entries.filter((entry) => !prefix || entry.key.startsWith(prefix)),
|
|
1135
|
-
resolve: async (key) => values[key],
|
|
1136
|
-
resolveValues: async ({ keys, prefix } = {}) => {
|
|
1137
|
-
const filtered = {};
|
|
1138
|
-
for (const [key, value] of Object.entries(values)) {
|
|
1139
|
-
if (prefix && !key.startsWith(prefix)) continue;
|
|
1140
|
-
if (keys && !keys.includes(key)) continue;
|
|
1141
|
-
filtered[key] = value;
|
|
1142
|
-
}
|
|
1143
|
-
return filtered;
|
|
1144
|
-
}
|
|
1145
|
-
};
|
|
1146
|
-
}
|
|
1147
|
-
var ResumeTurnBusyError = class extends Error {
|
|
1148
|
-
constructor(lockKey) {
|
|
1149
|
-
super(`Another worker is already resuming Slack Turn "${lockKey}"`);
|
|
1150
|
-
this.name = "ResumeTurnBusyError";
|
|
1151
|
-
}
|
|
1152
|
-
};
|
|
1153
|
-
function getDefaultLockKey(channelId, threadTs) {
|
|
1154
|
-
return threadTs ? `slack:${channelId}:${threadTs}` : `slack:${channelId}`;
|
|
1155
|
-
}
|
|
1156
|
-
function getResumeLogContext(args, lockKey) {
|
|
1157
|
-
const actor = args.run?.actor;
|
|
1158
|
-
return {
|
|
1159
|
-
conversationId: args.conversationId,
|
|
1160
|
-
messageConversationId: lockKey,
|
|
1161
|
-
userId: isUserActor(actor) ? actor.userId : void 0,
|
|
1162
|
-
userName: isUserActor(actor) ? actor.userName : void 0,
|
|
1163
|
-
destinationName: args.channelId,
|
|
1164
|
-
assistantUserName: botConfig.userName,
|
|
1165
|
-
modelId: defaultModelId(botConfig)
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
async function postResumeFailureReply(args) {
|
|
1169
|
-
await postSlackMessage({
|
|
1170
|
-
channelId: args.channelId,
|
|
1171
|
-
threadTs: args.threadTs,
|
|
1172
|
-
text: buildTurnErrorResponse(args.error, args.eventId)
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
async function handleResumeFailure(args) {
|
|
1176
|
-
const turnLifecycle = getTurnLifecycle();
|
|
1177
|
-
const capturedEventId = logException(args.error, args.eventName);
|
|
1178
|
-
const eventId = requireTurnFailureEventId(capturedEventId, args.eventName);
|
|
1179
|
-
let failureStatePersistError;
|
|
1180
|
-
try {
|
|
1181
|
-
await args.resume.onFailure?.(args.error);
|
|
1182
|
-
} catch (persistError) {
|
|
1183
|
-
const persistEventId = logException(
|
|
1184
|
-
persistError,
|
|
1185
|
-
"slack.resume.failure_state_persist.failed",
|
|
1186
|
-
{ "app.error.original_event_id": eventId }
|
|
1187
|
-
);
|
|
1188
|
-
try {
|
|
1189
|
-
await turnLifecycle.fail({
|
|
1190
|
-
conversationId: args.resume.conversationId,
|
|
1191
|
-
turnId: args.resume.turnId,
|
|
1192
|
-
createdAtMs: Date.now(),
|
|
1193
|
-
failureCode: "persistence_failed",
|
|
1194
|
-
...persistEventId ? { eventId: persistEventId } : void 0
|
|
1195
|
-
});
|
|
1196
|
-
} catch (lifecycleError) {
|
|
1197
|
-
logException(
|
|
1198
|
-
lifecycleError,
|
|
1199
|
-
"slack.resume.failure_lifecycle_persist.failed",
|
|
1200
|
-
{ "app.error.original_event_id": eventId }
|
|
1201
|
-
);
|
|
1202
|
-
}
|
|
1203
|
-
failureStatePersistError = persistError;
|
|
1204
|
-
}
|
|
1205
|
-
try {
|
|
1206
|
-
await postResumeFailureReply({
|
|
1207
|
-
channelId: args.resume.channelId,
|
|
1208
|
-
threadTs: args.resume.threadTs,
|
|
1209
|
-
eventId,
|
|
1210
|
-
error: args.error
|
|
1211
|
-
});
|
|
1212
|
-
} catch (deliveryError) {
|
|
1213
|
-
const deliveryEventId = logException(
|
|
1214
|
-
deliveryError,
|
|
1215
|
-
"slack.resume.failure_delivery.failed",
|
|
1216
|
-
{ "app.error.original_event_id": eventId }
|
|
1217
|
-
);
|
|
1218
|
-
try {
|
|
1219
|
-
await turnLifecycle.fail({
|
|
1220
|
-
conversationId: args.resume.conversationId,
|
|
1221
|
-
turnId: args.resume.turnId,
|
|
1222
|
-
createdAtMs: Date.now(),
|
|
1223
|
-
failureCode: "delivery_failed",
|
|
1224
|
-
...deliveryEventId ? { eventId: deliveryEventId } : void 0
|
|
1225
|
-
});
|
|
1226
|
-
} catch (lifecycleError) {
|
|
1227
|
-
logException(
|
|
1228
|
-
lifecycleError,
|
|
1229
|
-
"slack.resume.failure_lifecycle_persist.failed",
|
|
1230
|
-
{ "app.error.original_event_id": eventId }
|
|
1231
|
-
);
|
|
1232
|
-
}
|
|
1233
|
-
throw deliveryError;
|
|
1234
|
-
}
|
|
1235
|
-
if (failureStatePersistError) {
|
|
1236
|
-
throw failureStatePersistError;
|
|
1237
|
-
}
|
|
1238
|
-
await turnLifecycle.fail({
|
|
1239
|
-
conversationId: args.resume.conversationId,
|
|
1240
|
-
turnId: args.resume.turnId,
|
|
1241
|
-
createdAtMs: Date.now(),
|
|
1242
|
-
failureCode: args.failureCode,
|
|
1243
|
-
eventId
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1246
|
-
function buildResumedRun(args, statusSession, delivery) {
|
|
1247
|
-
const savedRun = args.run;
|
|
1248
|
-
if (!savedRun) {
|
|
1249
|
-
throw new TypeError("Slack resume requires a saved Run");
|
|
1250
|
-
}
|
|
1251
|
-
if (!savedRun.source) {
|
|
1252
|
-
throw new TypeError("Slack resume requires a Run Source");
|
|
1253
|
-
}
|
|
1254
|
-
const source = savedRun.source;
|
|
1255
|
-
if (savedRun.destination.platform !== "slack") {
|
|
1256
|
-
throw new TypeError("Slack resume requires a Slack Destination");
|
|
1257
|
-
}
|
|
1258
|
-
const requestDeadline = getTurnRequestDeadline();
|
|
1259
|
-
const threadId = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
|
|
1260
|
-
const persistedLocationConfiguration = savedRun.environment?.locationConfiguration ?? (savedRun.environment?.configuration ? createReadOnlyConfigService(savedRun.environment.configuration) : void 0);
|
|
1261
|
-
const priorOnEvent = savedRun.onEvent;
|
|
1262
|
-
return {
|
|
1263
|
-
...savedRun,
|
|
1264
|
-
conversationId: args.conversationId,
|
|
1265
|
-
turnId: args.turnId,
|
|
1266
|
-
instruction: {
|
|
1267
|
-
...savedRun.instruction ?? { text: args.messageText },
|
|
1268
|
-
text: args.messageText
|
|
1269
|
-
},
|
|
1270
|
-
source: source.kind === "slack" ? {
|
|
1271
|
-
...source,
|
|
1272
|
-
channelId: args.channelId,
|
|
1273
|
-
...args.threadTs ? { threadTs: args.threadTs } : void 0
|
|
1274
|
-
} : source,
|
|
1275
|
-
deadlineAtMs: savedRun.deadlineAtMs ?? requestDeadline?.deadlineAtMs,
|
|
1276
|
-
environment: {
|
|
1277
|
-
...savedRun.environment,
|
|
1278
|
-
locationConfiguration: persistedLocationConfiguration
|
|
1279
|
-
},
|
|
1280
|
-
onEvent: async (event) => {
|
|
1281
|
-
if (event.type === "status") {
|
|
1282
|
-
statusSession.update({ text: event.text });
|
|
1283
|
-
}
|
|
1284
|
-
await priorOnEvent?.(event);
|
|
1285
|
-
},
|
|
1286
|
-
delivery,
|
|
1287
|
-
durability: {
|
|
1288
|
-
...savedRun.durability,
|
|
1289
|
-
onSandboxRefChanged: async (sandboxRef) => {
|
|
1290
|
-
await persistThreadStateById(threadId, {
|
|
1291
|
-
sandboxRef
|
|
1292
|
-
});
|
|
1293
|
-
await savedRun.durability?.onSandboxRefChanged?.(sandboxRef);
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
};
|
|
1297
|
-
}
|
|
1298
|
-
async function resumeSlackTurn(args) {
|
|
1299
|
-
const lockKey = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
|
|
1300
|
-
return withLogContext(
|
|
1301
|
-
getResumeLogContext(args, lockKey),
|
|
1302
|
-
() => resumeSlackTurnInContext(args)
|
|
1303
|
-
);
|
|
1304
|
-
}
|
|
1305
|
-
async function resumeSlackTurnInContext(args) {
|
|
1306
|
-
const stateAdapter = getStateAdapter();
|
|
1307
|
-
await stateAdapter.connect();
|
|
1308
|
-
const lockKey = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
|
|
1309
|
-
const lock = args.ownsConversationLease ? void 0 : await acquireActiveLock(stateAdapter, lockKey);
|
|
1310
|
-
if (!args.ownsConversationLease && !lock) {
|
|
1311
|
-
throw new ResumeTurnBusyError(lockKey);
|
|
1312
|
-
}
|
|
1313
|
-
const status = createSlackWebApiAssistantStatusSession({
|
|
1314
|
-
channelId: args.channelId,
|
|
1315
|
-
threadTs: args.threadTs
|
|
1316
|
-
});
|
|
1317
|
-
let processingReaction;
|
|
1318
|
-
let deferredAuthInfo;
|
|
1319
|
-
let deferredPauseHandler;
|
|
1320
|
-
let deferredFailureHandler;
|
|
1321
|
-
let runResultHandled = false;
|
|
1322
|
-
let assistantMessageDelivered = false;
|
|
1323
|
-
let shouldScheduleCompletedPluginTasks = false;
|
|
1324
|
-
let postDeliveryCommitError;
|
|
1325
|
-
const turnLifecycle = getTurnLifecycle();
|
|
1326
|
-
let failureCode = "agent_run_failed";
|
|
1327
|
-
let runArgs = args;
|
|
1328
|
-
try {
|
|
1329
|
-
const preparedArgs = await args.beforeStart?.();
|
|
1330
|
-
if (preparedArgs === false) {
|
|
1331
|
-
return false;
|
|
1332
|
-
}
|
|
1333
|
-
if (preparedArgs) {
|
|
1334
|
-
runArgs = { ...args, ...preparedArgs };
|
|
1335
|
-
}
|
|
1336
|
-
setTags(getResumeLogContext(runArgs, lockKey));
|
|
1337
|
-
const savedRun = runArgs.run;
|
|
1338
|
-
if (!savedRun) {
|
|
1339
|
-
throw new Error("Resumed Turn requires a saved Run");
|
|
1340
|
-
}
|
|
1341
|
-
const credentialContext = savedRun.credentialContext;
|
|
1342
|
-
if (!credentialContext) {
|
|
1343
|
-
throw new Error("Resumed Run requires a credential context");
|
|
1344
|
-
}
|
|
1345
|
-
const routingActor = savedRun.actor;
|
|
1346
|
-
let resumeActor;
|
|
1347
|
-
if ("type" in credentialContext.actor) {
|
|
1348
|
-
if (!isUserActor(routingActor) || credentialContext.actor.userId !== routingActor.userId) {
|
|
1349
|
-
throw new Error("Resumed Run credential actor must match its Actor");
|
|
1350
|
-
}
|
|
1351
|
-
resumeActor = routingActor;
|
|
1352
|
-
} else {
|
|
1353
|
-
if (routingActor && (routingActor.platform !== "system" || routingActor.name !== credentialContext.actor.name)) {
|
|
1354
|
-
throw new Error(
|
|
1355
|
-
"Resumed Run system credential actor must match its Actor"
|
|
1356
|
-
);
|
|
1357
|
-
}
|
|
1358
|
-
resumeActor = credentialContext.actor;
|
|
1359
|
-
}
|
|
1360
|
-
if (runArgs.messageTs) {
|
|
1361
|
-
processingReaction = await startProcessingReactionForMessage({
|
|
1362
|
-
channelId: runArgs.channelId,
|
|
1363
|
-
timestamp: runArgs.messageTs
|
|
1364
|
-
});
|
|
1365
|
-
}
|
|
1366
|
-
if (runArgs.initialText) {
|
|
1367
|
-
await postSlackMessageBestEffort(
|
|
1368
|
-
runArgs.channelId,
|
|
1369
|
-
runArgs.threadTs,
|
|
1370
|
-
runArgs.initialText
|
|
1371
|
-
);
|
|
1372
|
-
}
|
|
1373
|
-
status.update(runArgs.initialStatus);
|
|
1374
|
-
const conversationId = runArgs.conversationId;
|
|
1375
|
-
const visibility = (await getConversationStore().get({ conversationId }))?.visibility;
|
|
1376
|
-
const visibleConversationId = lockKey;
|
|
1377
|
-
const sessionId = runArgs.turnId;
|
|
1378
|
-
let deliveryConversation;
|
|
1379
|
-
let acceptedDeliveryId;
|
|
1380
|
-
const getDeliveryConversation = async () => {
|
|
1381
|
-
if (deliveryConversation) {
|
|
1382
|
-
const userMessageId2 = getTurnUserMessage(
|
|
1383
|
-
deliveryConversation,
|
|
1384
|
-
sessionId
|
|
1385
|
-
)?.id;
|
|
1386
|
-
return {
|
|
1387
|
-
conversation: deliveryConversation,
|
|
1388
|
-
sessionId,
|
|
1389
|
-
...userMessageId2 ? { userMessageId: userMessageId2 } : void 0
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
const persistedState = await getPersistedThreadState(
|
|
1393
|
-
visibleConversationId
|
|
1394
|
-
);
|
|
1395
|
-
const conversation = coerceThreadConversationState(persistedState);
|
|
1396
|
-
await hydrateConversationMessages({
|
|
1397
|
-
conversation,
|
|
1398
|
-
conversationId
|
|
1399
|
-
});
|
|
1400
|
-
deliveryConversation = conversation;
|
|
1401
|
-
const userMessageId = getTurnUserMessage(conversation, sessionId)?.id;
|
|
1402
|
-
return {
|
|
1403
|
-
conversation,
|
|
1404
|
-
sessionId,
|
|
1405
|
-
...userMessageId ? { userMessageId } : void 0
|
|
1406
|
-
};
|
|
1407
|
-
};
|
|
1408
|
-
const deliverAssistantMessage = async (reply) => {
|
|
1409
|
-
const message = typeof reply === "string" ? void 0 : reply;
|
|
1410
|
-
const text = typeof reply === "string" ? reply : getAssistantReplyText(reply);
|
|
1411
|
-
if (!text?.trim()) {
|
|
1412
|
-
return;
|
|
1413
|
-
}
|
|
1414
|
-
failureCode = "delivery_failed";
|
|
1415
|
-
const deliveryState2 = await getDeliveryConversation();
|
|
1416
|
-
let slackMessageTs = [];
|
|
1417
|
-
try {
|
|
1418
|
-
slackMessageTs = await sendSlackReply({
|
|
1419
|
-
channelId: runArgs.channelId,
|
|
1420
|
-
conversationId: runArgs.conversationId,
|
|
1421
|
-
replyAttribution: runArgs.run?.dispatch?.replyAttribution,
|
|
1422
|
-
text,
|
|
1423
|
-
threadTs: runArgs.threadTs
|
|
1424
|
-
});
|
|
1425
|
-
} catch (error) {
|
|
1426
|
-
if (isRetryableSlackPostError(error)) {
|
|
1427
|
-
throw new RetryableDeliveryError(error);
|
|
1428
|
-
}
|
|
1429
|
-
throw error;
|
|
1430
|
-
}
|
|
1431
|
-
const messageTs = slackMessageTs.at(-1);
|
|
1432
|
-
assistantMessageDelivered = true;
|
|
1433
|
-
acceptedDeliveryId = messageTs;
|
|
1434
|
-
const recordedMessageId = recordDeliveredAssistantMessage({
|
|
1435
|
-
conversation: deliveryState2.conversation,
|
|
1436
|
-
sessionId: deliveryState2.sessionId,
|
|
1437
|
-
source: "slack",
|
|
1438
|
-
text,
|
|
1439
|
-
userMessageId: deliveryState2.userMessageId
|
|
1440
|
-
});
|
|
1441
|
-
if (messageTs) {
|
|
1442
|
-
markConversationMessage(deliveryState2.conversation, recordedMessageId, {
|
|
1443
|
-
slackTs: messageTs
|
|
1444
|
-
});
|
|
1445
|
-
}
|
|
1446
|
-
try {
|
|
1447
|
-
const destination = runArgs.run?.destination;
|
|
1448
|
-
const providerConversationIds = runArgs.threadTs ? [runArgs.threadTs] : slackMessageTs;
|
|
1449
|
-
await persistWithRetry(
|
|
1450
|
-
() => commitAcceptedReply({
|
|
1451
|
-
...message ? { agentMessage: message } : void 0,
|
|
1452
|
-
conversation: deliveryState2.conversation,
|
|
1453
|
-
conversationMessageId: recordedMessageId,
|
|
1454
|
-
conversationId,
|
|
1455
|
-
...runArgs.run?.dispatch && destination?.platform === "slack" && providerConversationIds.length > 0 ? {
|
|
1456
|
-
providerConversationBindings: providerConversationIds.map(
|
|
1457
|
-
(providerConversationId) => ({
|
|
1458
|
-
provider: "slack",
|
|
1459
|
-
providerDestinationId: destination.channelId,
|
|
1460
|
-
providerTenantId: destination.teamId,
|
|
1461
|
-
providerConversationId
|
|
1462
|
-
})
|
|
1463
|
-
)
|
|
1464
|
-
} : void 0
|
|
1465
|
-
})
|
|
1466
|
-
);
|
|
1467
|
-
} catch (error) {
|
|
1468
|
-
logException(
|
|
1469
|
-
new Error("Accepted assistant message persistence failed"),
|
|
1470
|
-
"slack.resume.assistant_message_post_delivery_persist.failed",
|
|
1471
|
-
{ "error.type": error instanceof Error ? error.name : typeof error }
|
|
1472
|
-
);
|
|
1473
|
-
}
|
|
1474
|
-
const dispatchId = savedRun.dispatch?.id;
|
|
1475
|
-
if (messageTs && dispatchId) {
|
|
1476
|
-
try {
|
|
1477
|
-
await persistWithRetry(
|
|
1478
|
-
() => recordTurnSummary({
|
|
1479
|
-
conversationId: runArgs.conversationId,
|
|
1480
|
-
destination: savedRun.destination,
|
|
1481
|
-
destinationVisibility: visibility,
|
|
1482
|
-
dispatchId,
|
|
1483
|
-
resultMessageId: messageTs,
|
|
1484
|
-
turnId: runArgs.turnId,
|
|
1485
|
-
sliceId: runArgs.sliceId ?? 1,
|
|
1486
|
-
source: savedRun.source,
|
|
1487
|
-
state: "running",
|
|
1488
|
-
surface: savedRun.surface ?? "slack"
|
|
1489
|
-
})
|
|
1490
|
-
);
|
|
1491
|
-
} catch (error) {
|
|
1492
|
-
logException(error, "agent.turn.delivery_receipt_persist.failed");
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
failureCode = "agent_run_failed";
|
|
1496
|
-
};
|
|
1497
|
-
const deliveryState = await getDeliveryConversation();
|
|
1498
|
-
assistantMessageDelivered = turnHasReply(
|
|
1499
|
-
deliveryState.conversation,
|
|
1500
|
-
sessionId
|
|
1501
|
-
);
|
|
1502
|
-
const run = buildResumedRun(runArgs, status, deliverAssistantMessage);
|
|
1503
|
-
if (runArgs.inputMessageIds?.length) {
|
|
1504
|
-
await turnLifecycle.start({
|
|
1505
|
-
conversationId: runArgs.conversationId,
|
|
1506
|
-
turnId: runArgs.turnId,
|
|
1507
|
-
createdAtMs: Date.now(),
|
|
1508
|
-
inputMessageIds: [...new Set(runArgs.inputMessageIds)],
|
|
1509
|
-
surface: "slack"
|
|
1510
|
-
});
|
|
1511
|
-
}
|
|
1512
|
-
const replyTimeoutMs = resolveReplyTimeoutMs();
|
|
1513
|
-
const outcome = await runArgs.executeTurn(
|
|
1514
|
-
run,
|
|
1515
|
-
async (result) => {
|
|
1516
|
-
const finalized = finalizeFailedTurnReplyWithEvent({
|
|
1517
|
-
reply: result,
|
|
1518
|
-
logException
|
|
1519
|
-
});
|
|
1520
|
-
const reply = finalized.reply;
|
|
1521
|
-
const dispatchErrorMessage = run.dispatch && reply.diagnostics.outcome !== "success" ? reply.diagnostics.errorMessage ?? `Agent turn ended with ${reply.diagnostics.outcome}.` : void 0;
|
|
1522
|
-
if (reply.diagnostics.outcome !== "success") {
|
|
1523
|
-
await deliverAssistantMessage(reply.text);
|
|
1524
|
-
}
|
|
1525
|
-
runResultHandled = true;
|
|
1526
|
-
await status.clear();
|
|
1527
|
-
failureCode = "persistence_failed";
|
|
1528
|
-
if (reply.piMessages?.length) {
|
|
1529
|
-
await saveTurnCheckpoint({
|
|
1530
|
-
mode: "completed",
|
|
1531
|
-
conversationId: runArgs.conversationId,
|
|
1532
|
-
turnId: runArgs.turnId,
|
|
1533
|
-
messages: reply.piMessages,
|
|
1534
|
-
durationMs: reply.diagnostics.durationMs,
|
|
1535
|
-
usage: reply.diagnostics.usage,
|
|
1536
|
-
destination: run.destination,
|
|
1537
|
-
destinationVisibility: visibility,
|
|
1538
|
-
dispatchId: run.dispatch?.id,
|
|
1539
|
-
dispatchOutcome: reply.diagnostics.outcome === "success" ? "completed" : "failed",
|
|
1540
|
-
...dispatchErrorMessage ? { errorMessage: dispatchErrorMessage } : void 0,
|
|
1541
|
-
...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : void 0,
|
|
1542
|
-
source: run.source,
|
|
1543
|
-
actor: resumeActor,
|
|
1544
|
-
surface: run.surface ?? "slack",
|
|
1545
|
-
sliceId: runArgs.sliceId
|
|
1546
|
-
});
|
|
1547
|
-
} else if (run.dispatch?.id) {
|
|
1548
|
-
await recordTurnSummary({
|
|
1549
|
-
conversationId: runArgs.conversationId,
|
|
1550
|
-
destination: run.destination,
|
|
1551
|
-
destinationVisibility: visibility,
|
|
1552
|
-
dispatchId: run.dispatch?.id,
|
|
1553
|
-
dispatchOutcome: reply.diagnostics.outcome === "success" ? "completed" : "failed",
|
|
1554
|
-
...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : void 0,
|
|
1555
|
-
turnId: runArgs.turnId,
|
|
1556
|
-
sliceId: runArgs.sliceId ?? 1,
|
|
1557
|
-
source: run.source,
|
|
1558
|
-
state: reply.diagnostics.outcome === "success" ? "completed" : "failed",
|
|
1559
|
-
surface: run.surface ?? "slack"
|
|
1560
|
-
});
|
|
1561
|
-
}
|
|
1562
|
-
await runArgs.commitResult?.(reply);
|
|
1563
|
-
if (reply.diagnostics.outcome === "success") {
|
|
1564
|
-
shouldScheduleCompletedPluginTasks = true;
|
|
1565
|
-
return {
|
|
1566
|
-
outcome: assistantMessageDelivered ? "success" : "no_reply"
|
|
1567
|
-
};
|
|
1568
|
-
}
|
|
1569
|
-
return {
|
|
1570
|
-
outcome: "failed",
|
|
1571
|
-
failureCode: "model_execution_failed",
|
|
1572
|
-
...finalized.eventId ? { eventId: finalized.eventId } : void 0,
|
|
1573
|
-
...finalized.failureReason ? { failureReason: finalized.failureReason } : void 0
|
|
1574
|
-
};
|
|
1575
|
-
},
|
|
1576
|
-
replyTimeoutMs
|
|
1577
|
-
);
|
|
1578
|
-
if (outcome.status !== "completed") {
|
|
1579
|
-
await status.clear();
|
|
1580
|
-
const onAuthPause = runArgs.onAuthPause;
|
|
1581
|
-
const onSuspend = runArgs.onSuspend;
|
|
1582
|
-
if (outcome.status === "awaiting_auth" && onAuthPause) {
|
|
1583
|
-
deferredAuthInfo = {
|
|
1584
|
-
providerDisplayName: outcome.providerDisplayName,
|
|
1585
|
-
actorId: isUserActor(resumeActor) ? resumeActor.userId : void 0,
|
|
1586
|
-
...outcome.requestText ? { requestText: outcome.requestText } : void 0
|
|
1587
|
-
};
|
|
1588
|
-
deferredPauseHandler = async () => {
|
|
1589
|
-
await onAuthPause({
|
|
1590
|
-
providerDisplayName: outcome.providerDisplayName
|
|
1591
|
-
});
|
|
1592
|
-
};
|
|
1593
|
-
} else if (outcome.status === "suspended") {
|
|
1594
|
-
if (runArgs.ownsConversationLease && (outcome.reason === "timeout" || savedRun.durability?.shouldYield?.())) {
|
|
1595
|
-
throw new CooperativeTurnYieldError();
|
|
1596
|
-
}
|
|
1597
|
-
if (onSuspend) {
|
|
1598
|
-
deferredPauseHandler = async () => {
|
|
1599
|
-
await onSuspend(outcome.resumeVersion);
|
|
1600
|
-
};
|
|
1601
|
-
} else {
|
|
1602
|
-
deferredFailureHandler = async () => {
|
|
1603
|
-
await handleResumeFailure({
|
|
1604
|
-
error: new Error("Resumed Run ended suspended without onSuspend"),
|
|
1605
|
-
eventName: "slack.resume.turn.failed",
|
|
1606
|
-
failureCode: "agent_run_failed",
|
|
1607
|
-
resume: runArgs
|
|
1608
|
-
});
|
|
1609
|
-
};
|
|
1610
|
-
}
|
|
1611
|
-
} else {
|
|
1612
|
-
deferredFailureHandler = async () => {
|
|
1613
|
-
await handleResumeFailure({
|
|
1614
|
-
error: new Error(
|
|
1615
|
-
`Resumed Run ended ${outcome.status} without its required callback`
|
|
1616
|
-
),
|
|
1617
|
-
eventName: "slack.resume.turn.failed",
|
|
1618
|
-
failureCode: "agent_run_failed",
|
|
1619
|
-
resume: runArgs
|
|
1620
|
-
});
|
|
1621
|
-
};
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
if (shouldScheduleCompletedPluginTasks) {
|
|
1625
|
-
try {
|
|
1626
|
-
const params = {
|
|
1627
|
-
conversationId: runArgs.conversationId,
|
|
1628
|
-
sessionId: runArgs.turnId
|
|
1629
|
-
};
|
|
1630
|
-
await (runArgs.scheduleSessionCompletedPluginTasks ?? scheduleSessionCompletedPluginTasks)(params);
|
|
1631
|
-
} catch (scheduleError) {
|
|
1632
|
-
logException(
|
|
1633
|
-
scheduleError,
|
|
1634
|
-
"plugin.session.completed_task_schedule.failed"
|
|
1635
|
-
);
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
} catch (error) {
|
|
1639
|
-
await status.clear();
|
|
1640
|
-
const runError = error instanceof AgentRunError ? error.cause : error;
|
|
1641
|
-
if (isCooperativeTurnYieldError(runError)) {
|
|
1642
|
-
if (lock) {
|
|
1643
|
-
await stateAdapter.releaseLock(lock);
|
|
1644
|
-
}
|
|
1645
|
-
await processingReaction?.stop();
|
|
1646
|
-
throw runError;
|
|
1647
|
-
}
|
|
1648
|
-
if (runResultHandled) {
|
|
1649
|
-
postDeliveryCommitError = error;
|
|
1650
|
-
try {
|
|
1651
|
-
await runArgs.onPostDeliveryCommitFailure?.(error);
|
|
1652
|
-
} catch (terminalizeError) {
|
|
1653
|
-
logException(
|
|
1654
|
-
terminalizeError,
|
|
1655
|
-
"slack.resume.post_delivery_terminalize.failed"
|
|
1656
|
-
);
|
|
1657
|
-
}
|
|
1658
|
-
} else {
|
|
1659
|
-
deferredFailureHandler = async () => {
|
|
1660
|
-
await handleResumeFailure({
|
|
1661
|
-
error: runError,
|
|
1662
|
-
eventName: "slack.resume.turn.failed",
|
|
1663
|
-
failureCode,
|
|
1664
|
-
resume: runArgs
|
|
1665
|
-
});
|
|
1666
|
-
};
|
|
1667
|
-
}
|
|
1668
|
-
} finally {
|
|
1669
|
-
if (runResultHandled) {
|
|
1670
|
-
await processingReaction?.complete();
|
|
1671
|
-
} else {
|
|
1672
|
-
await processingReaction?.stop();
|
|
1673
|
-
}
|
|
1674
|
-
if (lock) {
|
|
1675
|
-
await stateAdapter.releaseLock(lock);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
if (postDeliveryCommitError) {
|
|
1679
|
-
const eventId = logException(
|
|
1680
|
-
postDeliveryCommitError,
|
|
1681
|
-
"slack.resume.success_handler.failed"
|
|
1682
|
-
);
|
|
1683
|
-
await turnLifecycle.fail({
|
|
1684
|
-
conversationId: runArgs.conversationId,
|
|
1685
|
-
turnId: runArgs.turnId,
|
|
1686
|
-
createdAtMs: Date.now(),
|
|
1687
|
-
failureCode: "persistence_failed",
|
|
1688
|
-
...eventId ? { eventId } : void 0
|
|
1689
|
-
});
|
|
1690
|
-
throw postDeliveryCommitError;
|
|
1691
|
-
}
|
|
1692
|
-
if (deferredPauseHandler) {
|
|
1693
|
-
try {
|
|
1694
|
-
await deferredPauseHandler();
|
|
1695
|
-
if (deferredAuthInfo) {
|
|
1696
|
-
await postSlackMessageBestEffort(
|
|
1697
|
-
runArgs.channelId,
|
|
1698
|
-
runArgs.threadTs,
|
|
1699
|
-
buildAuthPauseResponse(
|
|
1700
|
-
deferredAuthInfo.actorId,
|
|
1701
|
-
deferredAuthInfo.providerDisplayName,
|
|
1702
|
-
deferredAuthInfo.requestText
|
|
1703
|
-
),
|
|
1704
|
-
runArgs.conversationId,
|
|
1705
|
-
runArgs.run?.dispatch?.replyAttribution
|
|
1706
|
-
);
|
|
1707
|
-
}
|
|
1708
|
-
return true;
|
|
1709
|
-
} catch (pauseError) {
|
|
1710
|
-
await handleResumeFailure({
|
|
1711
|
-
error: pauseError,
|
|
1712
|
-
eventName: "slack.resume.pause_handler.failed",
|
|
1713
|
-
failureCode: "persistence_failed",
|
|
1714
|
-
resume: runArgs
|
|
1715
|
-
});
|
|
1716
|
-
return true;
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
if (deferredFailureHandler) {
|
|
1720
|
-
await deferredFailureHandler();
|
|
1721
|
-
}
|
|
1722
|
-
return true;
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
// src/chat/task-execution/turn-wake.ts
|
|
1726
|
-
async function getPausedTurnRequest(args) {
|
|
1727
|
-
const checkpoint = await loadTurnCheckpoint({
|
|
1728
|
-
conversationId: args.conversationId,
|
|
1729
|
-
turnId: args.turnId
|
|
1730
|
-
});
|
|
1731
|
-
const turn = checkpoint.record;
|
|
1732
|
-
if (!turn || turn.state !== "paused" || turn.resumeReason !== "timeout" && turn.resumeReason !== "yield" && turn.resumeReason !== "retry" || turn.resumeReason === "timeout" && turn.sliceId < 2) {
|
|
1733
|
-
return void 0;
|
|
1734
|
-
}
|
|
1735
|
-
let routing;
|
|
1736
|
-
try {
|
|
1737
|
-
routing = await resolveTurnSessionRouting({
|
|
1738
|
-
conversationId: args.conversationId,
|
|
1739
|
-
conversationStore: args.conversationStore
|
|
1740
|
-
});
|
|
1741
|
-
} catch (error) {
|
|
1742
|
-
await failTurnRecord({
|
|
1743
|
-
conversationId: args.conversationId,
|
|
1744
|
-
expectedVersion: turn.version,
|
|
1745
|
-
turnId: args.turnId,
|
|
1746
|
-
errorMessage: error instanceof Error ? error.message : String(error)
|
|
1747
|
-
});
|
|
1748
|
-
return void 0;
|
|
1749
|
-
}
|
|
1750
|
-
return {
|
|
1751
|
-
conversationId: args.conversationId,
|
|
1752
|
-
destination: routing.destination,
|
|
1753
|
-
turnId: args.turnId,
|
|
1754
|
-
expectedVersion: turn.version
|
|
1755
|
-
};
|
|
1756
|
-
}
|
|
1757
|
-
async function wakePausedTurn(request, options = {}) {
|
|
1758
|
-
const nowMs = options.nowMs ?? Date.now();
|
|
1759
|
-
await requestConversationWork({
|
|
1760
|
-
conversationId: request.conversationId,
|
|
1761
|
-
destination: request.destination,
|
|
1762
|
-
nowMs,
|
|
1763
|
-
state: options.state
|
|
1764
|
-
});
|
|
1765
|
-
const queue = options.queue ?? getVercelConversationWorkQueue();
|
|
1766
|
-
await ensureConversationWake({
|
|
1767
|
-
conversationId: request.conversationId,
|
|
1768
|
-
idempotencyKey: [
|
|
1769
|
-
"agent-continue",
|
|
1770
|
-
request.conversationId,
|
|
1771
|
-
request.turnId,
|
|
1772
|
-
request.expectedVersion,
|
|
1773
|
-
nowMs
|
|
1774
|
-
].join(":"),
|
|
1775
|
-
nowMs,
|
|
1776
|
-
queue,
|
|
1777
|
-
state: options.state
|
|
1778
|
-
});
|
|
1779
|
-
}
|
|
1780
|
-
function createPausedTurns(options = {}) {
|
|
1781
|
-
return {
|
|
1782
|
-
get: (args) => getPausedTurnRequest({
|
|
1783
|
-
...args,
|
|
1784
|
-
conversationStore: options.conversationStore
|
|
1785
|
-
}),
|
|
1786
|
-
wake: (request) => wakePausedTurn(request, options)
|
|
1787
|
-
};
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
// src/handlers/oauth-html.ts
|
|
1791
|
-
function htmlCallbackResponse(title, message, status, options = {}) {
|
|
1792
|
-
const footerMessage = options.footerMessage ?? `You can close this tab and return to ${botConfig.userName}.`;
|
|
1793
|
-
const html = `<!DOCTYPE html>
|
|
1794
|
-
<html>
|
|
1795
|
-
<head><title>${title}</title></head>
|
|
1796
|
-
<body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
|
|
1797
|
-
<div style="text-align: center; max-width: 480px;">
|
|
1798
|
-
<h1>${title}</h1>
|
|
1799
|
-
<p>${message}</p>
|
|
1800
|
-
<p style="margin-top: 2rem; color: #666; font-size: 0.9em;">${footerMessage}</p>
|
|
1801
|
-
</div>
|
|
1802
|
-
</body>
|
|
1803
|
-
</html>`;
|
|
1804
|
-
return new Response(html, {
|
|
1805
|
-
status,
|
|
1806
|
-
headers: {
|
|
1807
|
-
"Cache-Control": "no-store",
|
|
1808
|
-
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'",
|
|
1809
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
1810
|
-
"Referrer-Policy": "no-referrer",
|
|
1811
|
-
"X-Content-Type-Options": "nosniff"
|
|
1812
|
-
}
|
|
1813
|
-
});
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
// src/handlers/mcp-oauth-callback.ts
|
|
1817
|
-
function callbackPages(botName) {
|
|
1818
|
-
return {
|
|
1819
|
-
missing_state: {
|
|
1820
|
-
title: "Authorization failed",
|
|
1821
|
-
message: "Missing state parameter.",
|
|
1822
|
-
status: 400
|
|
1823
|
-
},
|
|
1824
|
-
provider_error: {
|
|
1825
|
-
title: "Authorization failed",
|
|
1826
|
-
message: "The provider returned an authorization error.",
|
|
1827
|
-
status: 400
|
|
1828
|
-
},
|
|
1829
|
-
missing_code: {
|
|
1830
|
-
title: "Authorization failed",
|
|
1831
|
-
message: "Missing code parameter.",
|
|
1832
|
-
status: 400
|
|
1833
|
-
},
|
|
1834
|
-
expired: {
|
|
1835
|
-
title: "Authorization expired",
|
|
1836
|
-
message: `This authorization link is no longer active. Return to ${botName} and retry the original request.`,
|
|
1837
|
-
status: 400
|
|
1838
|
-
},
|
|
1839
|
-
success: {
|
|
1840
|
-
title: "Authorization complete",
|
|
1841
|
-
message: `Your MCP access is connected. ${botName} will continue the paused request.`,
|
|
1842
|
-
status: 200
|
|
1843
|
-
},
|
|
1844
|
-
failure: {
|
|
1845
|
-
title: "Authorization failed",
|
|
1846
|
-
message: `${botName} could not finish the authorization callback. Return to ${botName} and retry the original request.`,
|
|
1847
|
-
status: 500
|
|
1848
|
-
}
|
|
1849
|
-
};
|
|
1850
|
-
}
|
|
1851
|
-
var McpOAuthAttemptExpiredError = class extends Error {
|
|
1852
|
-
constructor() {
|
|
1853
|
-
super("MCP OAuth authorization attempt is no longer current");
|
|
1854
|
-
this.name = "McpOAuthAttemptExpiredError";
|
|
1855
|
-
}
|
|
1856
|
-
};
|
|
1857
|
-
function mcpAuthorizationId(args) {
|
|
1858
|
-
return `${args.sessionId}:mcp:${args.provider}`;
|
|
1859
|
-
}
|
|
1860
|
-
function htmlResponse(kind, options) {
|
|
1861
|
-
const botName = botConfig.userName;
|
|
1862
|
-
const pages = callbackPages(botName);
|
|
1863
|
-
const page = kind === "success" && options?.local ? {
|
|
1864
|
-
...pages.success,
|
|
1865
|
-
message: `Your MCP access is connected. ${botName} will continue the paused request in the local client.`
|
|
1866
|
-
} : pages[kind];
|
|
1867
|
-
const footerMessage = kind === "success" && !options?.local ? `You can close this tab and return to ${botName}.` : void 0;
|
|
1868
|
-
return htmlCallbackResponse(page.title, page.message, page.status, {
|
|
1869
|
-
footerMessage
|
|
1870
|
-
});
|
|
1871
|
-
}
|
|
1872
|
-
async function persistCompletedReplyState(channelId, threadTs, sessionId, reply) {
|
|
1873
|
-
const threadId = `slack:${channelId}:${threadTs}`;
|
|
1874
|
-
const currentState = await getPersistedThreadState(threadId);
|
|
1875
|
-
const conversation = coerceThreadConversationState(currentState);
|
|
1876
|
-
await hydrateConversationMessages({ conversation, conversationId: threadId });
|
|
1877
|
-
const userMessage = getTurnUserMessage(conversation, sessionId);
|
|
1878
|
-
const statePatch = buildDeliveredTurnStatePatch({
|
|
1879
|
-
conversation,
|
|
1880
|
-
reply,
|
|
1881
|
-
sessionId,
|
|
1882
|
-
userMessageId: userMessage?.id
|
|
1883
|
-
});
|
|
1884
|
-
await persistThreadStateById(threadId, {
|
|
1885
|
-
...statePatch
|
|
1886
|
-
});
|
|
1887
|
-
}
|
|
1888
|
-
async function failSessionRecordBestEffort(args) {
|
|
1889
|
-
try {
|
|
1890
|
-
await failTurnRecord({
|
|
1891
|
-
conversationId: args.conversationId,
|
|
1892
|
-
turnId: args.turnId,
|
|
1893
|
-
errorMessage: args.errorMessage,
|
|
1894
|
-
expectedVersion: args.expectedVersion
|
|
1895
|
-
});
|
|
1896
|
-
} catch (error) {
|
|
1897
|
-
logException(
|
|
1898
|
-
error,
|
|
1899
|
-
"mcp.oauth_callback.session_record.failure_persistence.failed",
|
|
1900
|
-
{
|
|
1901
|
-
"app.ai.conversation_id": args.conversationId,
|
|
1902
|
-
"app.ai.session_id": args.turnId
|
|
1903
|
-
}
|
|
1904
|
-
);
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
async function persistFailedReplyState(channelId, threadTs, sessionId, expectedVersion) {
|
|
1908
|
-
const threadId = `slack:${channelId}:${threadTs}`;
|
|
1909
|
-
const currentState = await getPersistedThreadState(threadId);
|
|
1910
|
-
const conversation = coerceThreadConversationState(currentState);
|
|
1911
|
-
await hydrateConversationMessages({ conversation, conversationId: threadId });
|
|
1912
|
-
clearPendingAuth(conversation, sessionId);
|
|
1913
|
-
markTurnFailed({
|
|
1914
|
-
conversation,
|
|
1915
|
-
nowMs: Date.now(),
|
|
1916
|
-
sessionId,
|
|
1917
|
-
userMessageId: getTurnUserMessageId(conversation, sessionId),
|
|
1918
|
-
markConversationMessage
|
|
1919
|
-
});
|
|
1920
|
-
await failSessionRecordBestEffort({
|
|
1921
|
-
conversationId: threadId,
|
|
1922
|
-
turnId: sessionId,
|
|
1923
|
-
errorMessage: "OAuth-resumed MCP turn failed",
|
|
1924
|
-
expectedVersion
|
|
1925
|
-
});
|
|
1926
|
-
await persistThreadStateById(threadId, {
|
|
1927
|
-
conversation
|
|
1928
|
-
});
|
|
1929
|
-
}
|
|
1930
|
-
async function resumeAuthorizedMcpTurn(args) {
|
|
1931
|
-
const { authSession, agentRunner, provider } = args;
|
|
1932
|
-
if (!authSession.channelId || !authSession.destination || !authSession.threadTs) {
|
|
1933
|
-
return;
|
|
1934
|
-
}
|
|
1935
|
-
const destination = requireSlackDestination(
|
|
1936
|
-
authSession.destination,
|
|
1937
|
-
"MCP OAuth resume"
|
|
1938
|
-
);
|
|
1939
|
-
const threadId = `slack:${authSession.channelId}:${authSession.threadTs}`;
|
|
1940
|
-
const currentState = await getPersistedThreadState(threadId);
|
|
1941
|
-
const conversation = coerceThreadConversationState(currentState);
|
|
1942
|
-
await hydrateConversationMessages({ conversation, conversationId: threadId });
|
|
1943
|
-
const pendingAuth = getConversationPendingAuth({
|
|
1944
|
-
conversation,
|
|
1945
|
-
kind: "mcp",
|
|
1946
|
-
provider,
|
|
1947
|
-
actorId: authSession.userId
|
|
1948
|
-
});
|
|
1949
|
-
if (pendingAuth?.authSessionId !== authSession.authSessionId) {
|
|
1950
|
-
return;
|
|
1951
|
-
}
|
|
1952
|
-
const resolvedSessionId = pendingAuth.sessionId;
|
|
1953
|
-
const userMessage = getTurnUserMessage(conversation, resolvedSessionId);
|
|
1954
|
-
if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
|
|
1955
|
-
clearPendingAuth(conversation, pendingAuth.sessionId);
|
|
1956
|
-
await persistThreadStateById(threadId, { conversation });
|
|
1957
|
-
await abandonTurnRecord({
|
|
1958
|
-
conversationId: authSession.conversationId,
|
|
1959
|
-
turnId: pendingAuth.sessionId,
|
|
1960
|
-
errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
|
|
1961
|
-
});
|
|
1962
|
-
return;
|
|
1963
|
-
}
|
|
1964
|
-
if (!userMessage) {
|
|
1965
|
-
return;
|
|
1966
|
-
}
|
|
1967
|
-
await resumeSlackTurn({
|
|
1968
|
-
messageText: userMessage.text,
|
|
1969
|
-
conversationId: authSession.conversationId,
|
|
1970
|
-
turnId: resolvedSessionId,
|
|
1971
|
-
channelId: authSession.channelId,
|
|
1972
|
-
threadTs: authSession.threadTs,
|
|
1973
|
-
messageTs: getTurnUserSlackMessageTs(userMessage),
|
|
1974
|
-
lockKey: threadId,
|
|
1975
|
-
initialText: "",
|
|
1976
|
-
executeTurn: async (run, saveResult, timeoutMs) => await executeTurn(agentRunner, run, saveResult, timeoutMs),
|
|
1977
|
-
beforeStart: async () => {
|
|
1978
|
-
const lockedState = await getPersistedThreadState(threadId);
|
|
1979
|
-
const lockedConversation = coerceThreadConversationState(lockedState);
|
|
1980
|
-
await hydrateConversationMessages({
|
|
1981
|
-
conversation: lockedConversation,
|
|
1982
|
-
conversationId: threadId
|
|
1983
|
-
});
|
|
1984
|
-
const lockedPendingAuth = getConversationPendingAuth({
|
|
1985
|
-
conversation: lockedConversation,
|
|
1986
|
-
kind: "mcp",
|
|
1987
|
-
provider,
|
|
1988
|
-
actorId: authSession.userId
|
|
1989
|
-
});
|
|
1990
|
-
if (lockedPendingAuth?.authSessionId !== authSession.authSessionId) {
|
|
1991
|
-
return false;
|
|
1992
|
-
}
|
|
1993
|
-
const lockedSessionId = lockedPendingAuth.sessionId;
|
|
1994
|
-
if (lockedSessionId !== resolvedSessionId) {
|
|
1995
|
-
return false;
|
|
1996
|
-
}
|
|
1997
|
-
if (!isPendingAuthLatestRequest(lockedConversation, lockedPendingAuth)) {
|
|
1998
|
-
clearPendingAuth(lockedConversation, lockedPendingAuth.sessionId);
|
|
1999
|
-
await persistThreadStateById(threadId, {
|
|
2000
|
-
conversation: lockedConversation
|
|
2001
|
-
});
|
|
2002
|
-
await abandonTurnRecord({
|
|
2003
|
-
conversationId: authSession.conversationId,
|
|
2004
|
-
turnId: lockedPendingAuth.sessionId,
|
|
2005
|
-
errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
|
|
2006
|
-
});
|
|
2007
|
-
return false;
|
|
2008
|
-
}
|
|
2009
|
-
const lockedUserMessage = getTurnUserMessage(
|
|
2010
|
-
lockedConversation,
|
|
2011
|
-
lockedSessionId
|
|
2012
|
-
);
|
|
2013
|
-
if (!lockedUserMessage) {
|
|
2014
|
-
return false;
|
|
2015
|
-
}
|
|
2016
|
-
const lockedSessionRecord = await getTurnRecord(
|
|
2017
|
-
authSession.conversationId,
|
|
2018
|
-
lockedSessionId
|
|
2019
|
-
);
|
|
2020
|
-
if (!lockedSessionRecord || lockedSessionRecord.state !== "paused" || lockedSessionRecord.resumeReason !== "auth") {
|
|
2021
|
-
return false;
|
|
2022
|
-
}
|
|
2023
|
-
const lockedConversationContext = buildConversationContext(
|
|
2024
|
-
lockedConversation,
|
|
2025
|
-
{
|
|
2026
|
-
excludeMessageId: lockedUserMessage.id
|
|
2027
|
-
}
|
|
2028
|
-
);
|
|
2029
|
-
const lockedLocationConfiguration = getLocationConfigurationService(destination);
|
|
2030
|
-
let actor;
|
|
2031
|
-
try {
|
|
2032
|
-
actor = createSlackResumeActor({
|
|
2033
|
-
actor: lockedSessionRecord.actor,
|
|
2034
|
-
teamId: destination.teamId,
|
|
2035
|
-
userId: authSession.userId
|
|
2036
|
-
});
|
|
2037
|
-
} catch {
|
|
2038
|
-
await failTurnRecord({
|
|
2039
|
-
conversationId: authSession.conversationId,
|
|
2040
|
-
expectedVersion: lockedSessionRecord.version,
|
|
2041
|
-
turnId: lockedSessionId,
|
|
2042
|
-
errorMessage: "Unable to rebuild Slack actor for OAuth resume"
|
|
2043
|
-
});
|
|
2044
|
-
return false;
|
|
2045
|
-
}
|
|
2046
|
-
let routing;
|
|
2047
|
-
try {
|
|
2048
|
-
routing = await resolveTurnSessionRouting({
|
|
2049
|
-
conversationId: authSession.conversationId
|
|
2050
|
-
});
|
|
2051
|
-
} catch (error) {
|
|
2052
|
-
await failTurnRecord({
|
|
2053
|
-
conversationId: authSession.conversationId,
|
|
2054
|
-
expectedVersion: lockedSessionRecord.version,
|
|
2055
|
-
turnId: lockedSessionId,
|
|
2056
|
-
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2057
|
-
});
|
|
2058
|
-
return false;
|
|
2059
|
-
}
|
|
2060
|
-
await recordAuthorizationCompleted({
|
|
2061
|
-
conversationId: authSession.conversationId,
|
|
2062
|
-
kind: "mcp",
|
|
2063
|
-
provider,
|
|
2064
|
-
actorId: authSession.userId,
|
|
2065
|
-
authorizationId: mcpAuthorizationId({
|
|
2066
|
-
provider,
|
|
2067
|
-
sessionId: lockedSessionId
|
|
2068
|
-
})
|
|
2069
|
-
});
|
|
2070
|
-
const lockedMessageTs = getTurnUserSlackMessageTs(lockedUserMessage);
|
|
2071
|
-
return {
|
|
2072
|
-
messageText: lockedUserMessage.text,
|
|
2073
|
-
sliceId: lockedSessionRecord.sliceId,
|
|
2074
|
-
messageTs: lockedMessageTs,
|
|
2075
|
-
inputMessageIds: [lockedUserMessage.id],
|
|
2076
|
-
run: {
|
|
2077
|
-
instruction: {
|
|
2078
|
-
text: lockedUserMessage.text,
|
|
2079
|
-
context: lockedConversationContext,
|
|
2080
|
-
...getTurnUserReplyAttachmentContext(lockedUserMessage)
|
|
2081
|
-
},
|
|
2082
|
-
// Pi history is SQL-authoritative: the resumed run reads its
|
|
2083
|
-
// session record first and falls back to the step projection.
|
|
2084
|
-
history: await loadProjection({ conversationId: threadId }),
|
|
2085
|
-
credentialContext: {
|
|
2086
|
-
actor: { type: "user", userId: actor.userId }
|
|
2087
|
-
},
|
|
2088
|
-
actor,
|
|
2089
|
-
destination,
|
|
2090
|
-
...routing.location ? { location: routing.location } : void 0,
|
|
2091
|
-
source: routing.source,
|
|
2092
|
-
toolChannelId: authSession.toolChannelId ?? authSession.channelId,
|
|
2093
|
-
environment: {
|
|
2094
|
-
configuration: authSession.configuration,
|
|
2095
|
-
locationConfiguration: lockedLocationConfiguration
|
|
2096
|
-
},
|
|
2097
|
-
state: {
|
|
2098
|
-
pendingAuth: lockedPendingAuth,
|
|
2099
|
-
sandboxRef: getPersistedSandboxState(lockedState)
|
|
2100
|
-
},
|
|
2101
|
-
durability: {
|
|
2102
|
-
recordPendingAuth: async (nextPendingAuth) => {
|
|
2103
|
-
lockedConversation.processing.pendingAuth = nextPendingAuth;
|
|
2104
|
-
await persistThreadStateById(threadId, {
|
|
2105
|
-
conversation: lockedConversation
|
|
2106
|
-
});
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
},
|
|
2110
|
-
commitResult: async (reply) => {
|
|
2111
|
-
await persistCompletedReplyState(
|
|
2112
|
-
authSession.channelId,
|
|
2113
|
-
authSession.threadTs,
|
|
2114
|
-
lockedSessionId,
|
|
2115
|
-
reply
|
|
2116
|
-
);
|
|
2117
|
-
},
|
|
2118
|
-
onPostDeliveryCommitFailure: async () => {
|
|
2119
|
-
await failTurnRecord({
|
|
2120
|
-
conversationId: authSession.conversationId,
|
|
2121
|
-
expectedVersion: lockedSessionRecord.version,
|
|
2122
|
-
turnId: lockedSessionId,
|
|
2123
|
-
errorMessage: "OAuth-resumed MCP reply was delivered but completion state did not persist"
|
|
2124
|
-
});
|
|
2125
|
-
},
|
|
2126
|
-
onFailure: async () => {
|
|
2127
|
-
try {
|
|
2128
|
-
await persistFailedReplyState(
|
|
2129
|
-
authSession.channelId,
|
|
2130
|
-
authSession.threadTs,
|
|
2131
|
-
lockedSessionId,
|
|
2132
|
-
lockedSessionRecord.version
|
|
2133
|
-
);
|
|
2134
|
-
} catch (persistError) {
|
|
2135
|
-
logException(
|
|
2136
|
-
persistError,
|
|
2137
|
-
"mcp.oauth_callback.resume_failure_persist.failed",
|
|
2138
|
-
{ "app.credential.provider": provider }
|
|
2139
|
-
);
|
|
2140
|
-
}
|
|
2141
|
-
},
|
|
2142
|
-
onAuthPause: async () => {
|
|
2143
|
-
await persistAuthPauseTurnState({
|
|
2144
|
-
sessionId: lockedSessionId,
|
|
2145
|
-
threadStateId: threadId
|
|
2146
|
-
});
|
|
2147
|
-
logWarn("mcp.oauth_callback.resume.reparked_for_auth", {
|
|
2148
|
-
"app.credential.provider": provider
|
|
2149
|
-
});
|
|
2150
|
-
},
|
|
2151
|
-
onSuspend: async (resumeVersion) => {
|
|
2152
|
-
await wakePausedTurn({
|
|
2153
|
-
conversationId: authSession.conversationId,
|
|
2154
|
-
destination,
|
|
2155
|
-
turnId: lockedSessionId,
|
|
2156
|
-
expectedVersion: resumeVersion
|
|
2157
|
-
});
|
|
2158
|
-
}
|
|
2159
|
-
};
|
|
2160
|
-
}
|
|
2161
|
-
});
|
|
2162
|
-
}
|
|
2163
|
-
async function isCurrentMcpAuthorizationAttempt(authSession, provider) {
|
|
2164
|
-
const conversationId = mcpConversationId(authSession);
|
|
2165
|
-
if (!conversationId) {
|
|
2166
|
-
return false;
|
|
2167
|
-
}
|
|
2168
|
-
const currentState = await getPersistedThreadState(conversationId);
|
|
2169
|
-
const conversation = coerceThreadConversationState(currentState);
|
|
2170
|
-
const pendingAuth = getConversationPendingAuth({
|
|
2171
|
-
conversation,
|
|
2172
|
-
kind: "mcp",
|
|
2173
|
-
provider,
|
|
2174
|
-
actorId: authSession.userId
|
|
2175
|
-
});
|
|
2176
|
-
return pendingAuth?.authSessionId === authSession.authSessionId && pendingAuth.sessionId === authSession.sessionId;
|
|
2177
|
-
}
|
|
2178
|
-
function mcpConversationId(authSession) {
|
|
2179
|
-
if (authSession.destination?.platform === "local" || authSession.source?.kind === "web") {
|
|
2180
|
-
return authSession.conversationId;
|
|
2181
|
-
}
|
|
2182
|
-
if (authSession.channelId && authSession.threadTs) {
|
|
2183
|
-
return `slack:${authSession.channelId}:${authSession.threadTs}`;
|
|
2184
|
-
}
|
|
2185
|
-
return void 0;
|
|
2186
|
-
}
|
|
2187
|
-
async function runCurrentMcpCredentialMutation(authSession, provider, mutation) {
|
|
2188
|
-
const conversationId = mcpConversationId(authSession);
|
|
2189
|
-
if (!conversationId) {
|
|
2190
|
-
throw new McpOAuthAttemptExpiredError();
|
|
2191
|
-
}
|
|
2192
|
-
const stateAdapter = getStateAdapter();
|
|
2193
|
-
await stateAdapter.connect();
|
|
2194
|
-
const lock = await acquireActiveLock(stateAdapter, conversationId);
|
|
2195
|
-
if (!lock) {
|
|
2196
|
-
throw new Error(
|
|
2197
|
-
`Could not acquire MCP OAuth callback lock for ${conversationId}`
|
|
2198
|
-
);
|
|
2199
|
-
}
|
|
2200
|
-
try {
|
|
2201
|
-
if (!await isCurrentMcpAuthorizationAttempt(authSession, provider)) {
|
|
2202
|
-
throw new McpOAuthAttemptExpiredError();
|
|
2203
|
-
}
|
|
2204
|
-
return await mutation();
|
|
2205
|
-
} finally {
|
|
2206
|
-
await stateAdapter.releaseLock(lock);
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
async function GET(request, provider, waitUntil, options) {
|
|
2210
|
-
const localRelay = await relayLocalOAuthCallback(request);
|
|
2211
|
-
if (localRelay) {
|
|
2212
|
-
return localRelay;
|
|
2213
|
-
}
|
|
2214
|
-
const url = new URL(request.url);
|
|
2215
|
-
const state = url.searchParams.get("state")?.trim();
|
|
2216
|
-
const code = url.searchParams.get("code")?.trim();
|
|
2217
|
-
const error = url.searchParams.get("error")?.trim();
|
|
2218
|
-
if (!state) {
|
|
2219
|
-
return htmlResponse("missing_state");
|
|
2220
|
-
}
|
|
2221
|
-
if (error) {
|
|
2222
|
-
try {
|
|
2223
|
-
const pendingSession = await getMcpAuthSession(state);
|
|
2224
|
-
if (pendingSession && pendingSession.provider === provider) {
|
|
2225
|
-
try {
|
|
2226
|
-
await runCurrentMcpCredentialMutation(
|
|
2227
|
-
pendingSession,
|
|
2228
|
-
provider,
|
|
2229
|
-
async () => {
|
|
2230
|
-
const credentials = await getMcpStoredOAuthCredentials(
|
|
2231
|
-
pendingSession.userId,
|
|
2232
|
-
provider
|
|
2233
|
-
);
|
|
2234
|
-
if (!credentials?.clientInformation && !credentials?.discoveryState) {
|
|
2235
|
-
return;
|
|
2236
|
-
}
|
|
2237
|
-
const nextCredentials = credentials.tokens ? { tokens: credentials.tokens } : {};
|
|
2238
|
-
await putMcpStoredOAuthCredentials(
|
|
2239
|
-
pendingSession.userId,
|
|
2240
|
-
provider,
|
|
2241
|
-
nextCredentials
|
|
2242
|
-
);
|
|
2243
|
-
}
|
|
2244
|
-
);
|
|
2245
|
-
} catch (cleanupError) {
|
|
2246
|
-
if (!(cleanupError instanceof McpOAuthAttemptExpiredError)) {
|
|
2247
|
-
logException(
|
|
2248
|
-
cleanupError,
|
|
2249
|
-
"mcp.oauth_callback.provider_error_cleanup.failed",
|
|
2250
|
-
{ "app.credential.provider": provider }
|
|
2251
|
-
);
|
|
2252
|
-
}
|
|
2253
|
-
}
|
|
2254
|
-
await deleteMcpAuthSession(pendingSession.authSessionId);
|
|
2255
|
-
}
|
|
2256
|
-
} catch (cleanupError) {
|
|
2257
|
-
logException(cleanupError, "mcp.oauth_callback.provider_error_cleanup.failed", {
|
|
2258
|
-
"app.credential.provider": provider
|
|
2259
|
-
});
|
|
2260
|
-
}
|
|
2261
|
-
return htmlResponse("provider_error");
|
|
2262
|
-
}
|
|
2263
|
-
if (!code) {
|
|
2264
|
-
return htmlResponse("missing_code");
|
|
2265
|
-
}
|
|
2266
|
-
try {
|
|
2267
|
-
const pendingSession = await getMcpAuthSession(state);
|
|
2268
|
-
if (!pendingSession || pendingSession.provider !== provider || !await isCurrentMcpAuthorizationAttempt(pendingSession, provider)) {
|
|
2269
|
-
if (pendingSession) {
|
|
2270
|
-
await deleteMcpAuthSession(pendingSession.authSessionId);
|
|
2271
|
-
}
|
|
2272
|
-
return htmlResponse("expired");
|
|
2273
|
-
}
|
|
2274
|
-
const authSession = await finalizeMcpAuthorization(
|
|
2275
|
-
provider,
|
|
2276
|
-
state,
|
|
2277
|
-
code,
|
|
2278
|
-
async (mutation) => await runCurrentMcpCredentialMutation(
|
|
2279
|
-
pendingSession,
|
|
2280
|
-
provider,
|
|
2281
|
-
mutation
|
|
2282
|
-
)
|
|
2283
|
-
);
|
|
2284
|
-
let providerLabel;
|
|
2285
|
-
try {
|
|
2286
|
-
providerLabel = formatProviderLabel(provider);
|
|
2287
|
-
} catch {
|
|
2288
|
-
providerLabel = void 0;
|
|
2289
|
-
}
|
|
2290
|
-
try {
|
|
2291
|
-
await recordAuthenticationLinked({
|
|
2292
|
-
conversationId: authSession.conversationId,
|
|
2293
|
-
provider,
|
|
2294
|
-
actorId: authSession.userId,
|
|
2295
|
-
authorizationId: mcpAuthorizationId({
|
|
2296
|
-
provider,
|
|
2297
|
-
sessionId: authSession.sessionId
|
|
2298
|
-
}),
|
|
2299
|
-
turnId: authSession.sessionId,
|
|
2300
|
-
...providerLabel ? { providerLabel } : void 0
|
|
2301
|
-
});
|
|
2302
|
-
} catch (error2) {
|
|
2303
|
-
logException(error2, "mcp.oauth_callback.authentication_event.failed", {
|
|
2304
|
-
"app.credential.provider": provider
|
|
2305
|
-
});
|
|
2306
|
-
}
|
|
2307
|
-
try {
|
|
2308
|
-
await deleteMcpAuthSession(authSession.authSessionId);
|
|
2309
|
-
} catch (cleanupError) {
|
|
2310
|
-
logException(cleanupError, "mcp.oauth_callback.session_cleanup.failed", {
|
|
2311
|
-
"app.credential.provider": provider
|
|
2312
|
-
});
|
|
2313
|
-
}
|
|
2314
|
-
if (authSession.source?.kind === "web" && authSession.destination) {
|
|
2315
|
-
waitUntil(async () => {
|
|
2316
|
-
const turn = await getTurnRecord(
|
|
2317
|
-
authSession.conversationId,
|
|
2318
|
-
authSession.sessionId
|
|
2319
|
-
);
|
|
2320
|
-
await deleteWebAuthorization({
|
|
2321
|
-
actorId: authSession.userId,
|
|
2322
|
-
conversationId: authSession.conversationId
|
|
2323
|
-
});
|
|
2324
|
-
if (!turn || turn.state !== "paused" || turn.resumeReason !== "auth") {
|
|
2325
|
-
return;
|
|
2326
|
-
}
|
|
2327
|
-
await recordAuthorizationCompleted({
|
|
2328
|
-
conversationId: authSession.conversationId,
|
|
2329
|
-
kind: "mcp",
|
|
2330
|
-
provider,
|
|
2331
|
-
actorId: authSession.userId,
|
|
2332
|
-
authorizationId: mcpAuthorizationId({
|
|
2333
|
-
provider,
|
|
2334
|
-
sessionId: authSession.sessionId
|
|
2335
|
-
})
|
|
2336
|
-
});
|
|
2337
|
-
await wakePausedTurn(
|
|
2338
|
-
{
|
|
2339
|
-
conversationId: authSession.conversationId,
|
|
2340
|
-
destination: authSession.destination,
|
|
2341
|
-
turnId: authSession.sessionId,
|
|
2342
|
-
expectedVersion: turn.version
|
|
2343
|
-
},
|
|
2344
|
-
options.conversationWorkQueue ? { queue: options.conversationWorkQueue } : void 0
|
|
2345
|
-
);
|
|
2346
|
-
});
|
|
2347
|
-
} else if (authSession.destination?.platform !== "local") {
|
|
2348
|
-
waitUntil(
|
|
2349
|
-
() => resumeAuthorizedMcpTurn({
|
|
2350
|
-
authSession,
|
|
2351
|
-
agentRunner: options.agentRunner,
|
|
2352
|
-
provider
|
|
2353
|
-
})
|
|
2354
|
-
);
|
|
2355
|
-
}
|
|
2356
|
-
return htmlResponse("success", {
|
|
2357
|
-
// Web roots keep a local destination for conversation-only delivery.
|
|
2358
|
-
// Only the CLI path should get the local-client success copy.
|
|
2359
|
-
local: authSession.destination?.platform === "local" && authSession.source?.kind !== "web"
|
|
2360
|
-
});
|
|
2361
|
-
} catch (callbackError) {
|
|
2362
|
-
if (callbackError instanceof McpOAuthAttemptExpiredError) {
|
|
2363
|
-
await deleteMcpAuthSession(state);
|
|
2364
|
-
return htmlResponse("expired");
|
|
2365
|
-
}
|
|
2366
|
-
logException(callbackError, "mcp.oauth_callback.failed", {
|
|
2367
|
-
"app.credential.provider": provider,
|
|
2368
|
-
...getMcpProviderErrorAttributes(callbackError)
|
|
2369
|
-
});
|
|
2370
|
-
return htmlResponse("failure");
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
|
-
// src/chat/slack/app-home.ts
|
|
2375
|
-
import fs from "fs";
|
|
2376
|
-
import path from "path";
|
|
2377
|
-
var DEFAULT_DESCRIPTION_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
|
|
2378
|
-
var MAX_HOME_SKILLS = 6;
|
|
2379
|
-
var MAX_SECTION_TEXT_CHARS = 3e3;
|
|
2380
|
-
var HIDDEN_HOME_SKILLS = /* @__PURE__ */ new Set(["jr-rpc"]);
|
|
2381
|
-
function clampSectionText(text) {
|
|
2382
|
-
if (text.length <= MAX_SECTION_TEXT_CHARS) {
|
|
2383
|
-
return text;
|
|
2384
|
-
}
|
|
2385
|
-
return `${text.slice(0, MAX_SECTION_TEXT_CHARS - 1)}\u2026`;
|
|
2386
|
-
}
|
|
2387
|
-
function loadDescriptionText() {
|
|
2388
|
-
const descriptionPath = path.join(homeDir(), "DESCRIPTION.md");
|
|
2389
|
-
try {
|
|
2390
|
-
const raw = fs.readFileSync(descriptionPath, "utf8").trim();
|
|
2391
|
-
if (raw.length > 0) {
|
|
2392
|
-
return clampSectionText(raw);
|
|
2393
|
-
}
|
|
2394
|
-
} catch {
|
|
2395
|
-
}
|
|
2396
|
-
return DEFAULT_DESCRIPTION_TEXT;
|
|
2397
|
-
}
|
|
2398
|
-
async function buildSkillsSummaryText() {
|
|
2399
|
-
const skills = (await discoverSkills()).filter(
|
|
2400
|
-
(skill) => !HIDDEN_HOME_SKILLS.has(skill.name)
|
|
2401
|
-
);
|
|
2402
|
-
if (skills.length === 0) {
|
|
2403
|
-
return "No skills installed.";
|
|
2404
|
-
}
|
|
2405
|
-
const visible = skills.slice(0, MAX_HOME_SKILLS);
|
|
2406
|
-
const lines = visible.map(
|
|
2407
|
-
(skill) => `\u2022 *${skill.name}* \u2014 ${skill.description}`
|
|
2408
|
-
);
|
|
2409
|
-
if (skills.length > visible.length) {
|
|
2410
|
-
lines.push(`\u2022 \u2026and ${skills.length - visible.length} more`);
|
|
2411
|
-
}
|
|
2412
|
-
return lines.join("\n");
|
|
2413
|
-
}
|
|
2414
|
-
function accountLabel(account) {
|
|
2415
|
-
const label = account.label ?? account.id;
|
|
2416
|
-
return account.url ? `<${account.url}|${label}>` : label;
|
|
2417
|
-
}
|
|
2418
|
-
function connectedAccountText(plugin, account) {
|
|
2419
|
-
return account ? `*${plugin.manifest.name}*
|
|
2420
|
-
Connected as ${accountLabel(account)}` : `*${plugin.manifest.name}*
|
|
2421
|
-
${plugin.manifest.description}`;
|
|
2422
|
-
}
|
|
2423
|
-
async function connectedOAuthTokens(userId, plugin, userTokenStore) {
|
|
2424
|
-
if (plugin.manifest.oauth || plugin.manifest.credentials) {
|
|
2425
|
-
const stored = await userTokenStore.get(userId, plugin.manifest.name);
|
|
2426
|
-
return stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope) ? stored : void 0;
|
|
2427
|
-
}
|
|
2428
|
-
return void 0;
|
|
2429
|
-
}
|
|
2430
|
-
async function hasConnectedMcpAccount(userId, plugin) {
|
|
2431
|
-
if (!plugin.manifest.mcp) {
|
|
2432
|
-
return false;
|
|
2433
|
-
}
|
|
2434
|
-
const credentials = await getMcpStoredOAuthCredentials(
|
|
2435
|
-
userId,
|
|
2436
|
-
plugin.manifest.name
|
|
2437
|
-
);
|
|
2438
|
-
return Boolean(
|
|
2439
|
-
credentials?.tokens || credentials?.clientInformation || credentials?.discoveryState
|
|
2440
|
-
);
|
|
2441
|
-
}
|
|
2442
|
-
async function buildHomeView(userId, userTokenStore) {
|
|
2443
|
-
const runtimeMetadata = getRuntimeMetadata();
|
|
2444
|
-
const descriptionText = loadDescriptionText();
|
|
2445
|
-
const skillsSummaryText = await buildSkillsSummaryText();
|
|
2446
|
-
const providers = pluginCatalogRuntime.getProviders();
|
|
2447
|
-
const connectedSections = [];
|
|
2448
|
-
for (const plugin of providers) {
|
|
2449
|
-
const tokens = await connectedOAuthTokens(userId, plugin, userTokenStore);
|
|
2450
|
-
if (!tokens && !await hasConnectedMcpAccount(userId, plugin)) continue;
|
|
2451
|
-
connectedSections.push({
|
|
2452
|
-
type: "section",
|
|
2453
|
-
text: {
|
|
2454
|
-
type: "mrkdwn",
|
|
2455
|
-
text: connectedAccountText(plugin, tokens?.account)
|
|
2456
|
-
},
|
|
2457
|
-
accessory: {
|
|
2458
|
-
type: "button",
|
|
2459
|
-
text: { type: "plain_text", text: "Unlink" },
|
|
2460
|
-
action_id: "app_home_disconnect",
|
|
2461
|
-
value: plugin.manifest.name,
|
|
2462
|
-
style: "danger"
|
|
2463
|
-
}
|
|
2464
|
-
});
|
|
2465
|
-
}
|
|
2466
|
-
const accountBlocks = connectedSections.length > 0 ? connectedSections : [
|
|
2467
|
-
{
|
|
2468
|
-
type: "section",
|
|
2469
|
-
text: {
|
|
2470
|
-
type: "mrkdwn",
|
|
2471
|
-
text: "No connected accounts"
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
];
|
|
2475
|
-
return {
|
|
2476
|
-
type: "home",
|
|
2477
|
-
blocks: [
|
|
2478
|
-
{
|
|
2479
|
-
type: "header",
|
|
2480
|
-
text: {
|
|
2481
|
-
type: "plain_text",
|
|
2482
|
-
text: "Junior"
|
|
2483
|
-
}
|
|
2484
|
-
},
|
|
2485
|
-
{
|
|
2486
|
-
type: "section",
|
|
2487
|
-
text: {
|
|
2488
|
-
type: "mrkdwn",
|
|
2489
|
-
text: descriptionText
|
|
2490
|
-
}
|
|
2491
|
-
},
|
|
2492
|
-
{ type: "divider" },
|
|
2493
|
-
{
|
|
2494
|
-
type: "header",
|
|
2495
|
-
text: {
|
|
2496
|
-
type: "plain_text",
|
|
2497
|
-
text: "What I can help with"
|
|
2498
|
-
}
|
|
2499
|
-
},
|
|
2500
|
-
{
|
|
2501
|
-
type: "section",
|
|
2502
|
-
text: {
|
|
2503
|
-
type: "mrkdwn",
|
|
2504
|
-
text: skillsSummaryText
|
|
2505
|
-
}
|
|
2506
|
-
},
|
|
2507
|
-
{ type: "divider" },
|
|
2508
|
-
{
|
|
2509
|
-
type: "header",
|
|
2510
|
-
text: {
|
|
2511
|
-
type: "plain_text",
|
|
2512
|
-
text: "Connected accounts"
|
|
2513
|
-
}
|
|
2514
|
-
},
|
|
2515
|
-
...accountBlocks,
|
|
2516
|
-
{
|
|
2517
|
-
type: "context",
|
|
2518
|
-
elements: [
|
|
2519
|
-
{
|
|
2520
|
-
type: "mrkdwn",
|
|
2521
|
-
text: `*junior version:* \`${runtimeMetadata.version ?? "unknown"}\``
|
|
2522
|
-
}
|
|
2523
|
-
]
|
|
2524
|
-
}
|
|
2525
|
-
]
|
|
2526
|
-
};
|
|
2527
|
-
}
|
|
2528
|
-
async function publishAppHomeView(slackClient, userId, userTokenStore) {
|
|
2529
|
-
const view = await buildHomeView(userId, userTokenStore);
|
|
2530
|
-
await slackClient.views.publish({ user_id: userId, view });
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
|
-
// src/handlers/oauth-callback.ts
|
|
2534
|
-
function htmlErrorResponse(title, message, status) {
|
|
2535
|
-
return htmlCallbackResponse(escapeXml(title), escapeXml(message), status);
|
|
2536
|
-
}
|
|
2537
|
-
function oauthTokenErrorAttributes(provider, endpoint, status) {
|
|
2538
|
-
return {
|
|
2539
|
-
"app.credential.provider": provider,
|
|
2540
|
-
"app.oauth.error.phase": "token_exchange",
|
|
2541
|
-
"server.address": new URL(endpoint).hostname,
|
|
2542
|
-
...status !== void 0 ? { "http.response.status_code": status } : void 0
|
|
2543
|
-
};
|
|
2544
|
-
}
|
|
2545
|
-
async function persistCompletedOAuthReplyState(args) {
|
|
2546
|
-
const currentState = await getPersistedThreadState(args.conversationId);
|
|
2547
|
-
const conversation = coerceThreadConversationState(currentState);
|
|
2548
|
-
await hydrateConversationMessages({
|
|
2549
|
-
conversation,
|
|
2550
|
-
conversationId: args.conversationId
|
|
2551
|
-
});
|
|
2552
|
-
const userMessage = getTurnUserMessage(conversation, args.sessionId);
|
|
2553
|
-
const statePatch = buildDeliveredTurnStatePatch({
|
|
2554
|
-
conversation,
|
|
2555
|
-
reply: args.reply,
|
|
2556
|
-
sessionId: args.sessionId,
|
|
2557
|
-
userMessageId: userMessage?.id
|
|
2558
|
-
});
|
|
2559
|
-
await persistThreadStateById(args.conversationId, {
|
|
2560
|
-
...statePatch
|
|
2561
|
-
});
|
|
2562
|
-
}
|
|
2563
|
-
function pluginAuthorizationId(args) {
|
|
2564
|
-
return `${args.sessionId}:plugin:${args.provider}`;
|
|
2565
|
-
}
|
|
2566
|
-
async function failSessionRecordBestEffort2(args) {
|
|
2567
|
-
try {
|
|
2568
|
-
await failTurnRecord({
|
|
2569
|
-
conversationId: args.conversationId,
|
|
2570
|
-
expectedVersion: args.expectedVersion,
|
|
2571
|
-
turnId: args.turnId,
|
|
2572
|
-
errorMessage: args.errorMessage
|
|
2573
|
-
});
|
|
2574
|
-
} catch (error) {
|
|
2575
|
-
logException(
|
|
2576
|
-
error,
|
|
2577
|
-
"oauth.callback.session_record.failure_persistence.failed",
|
|
2578
|
-
{
|
|
2579
|
-
"app.ai.conversation_id": args.conversationId,
|
|
2580
|
-
"app.ai.session_id": args.turnId
|
|
2581
|
-
}
|
|
2582
|
-
);
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
async function persistFailedOAuthReplyState(args) {
|
|
2586
|
-
const currentState = await getPersistedThreadState(args.conversationId);
|
|
2587
|
-
const conversation = coerceThreadConversationState(currentState);
|
|
2588
|
-
await hydrateConversationMessages({
|
|
2589
|
-
conversation,
|
|
2590
|
-
conversationId: args.conversationId
|
|
2591
|
-
});
|
|
2592
|
-
clearPendingAuth(conversation, args.sessionId);
|
|
2593
|
-
markTurnFailed({
|
|
2594
|
-
conversation,
|
|
2595
|
-
nowMs: Date.now(),
|
|
2596
|
-
sessionId: args.sessionId,
|
|
2597
|
-
userMessageId: getTurnUserMessage(conversation, args.sessionId)?.id,
|
|
2598
|
-
markConversationMessage
|
|
2599
|
-
});
|
|
2600
|
-
await failSessionRecordBestEffort2({
|
|
2601
|
-
conversationId: args.conversationId,
|
|
2602
|
-
expectedVersion: args.expectedVersion,
|
|
2603
|
-
turnId: args.sessionId,
|
|
2604
|
-
errorMessage: "OAuth-resumed turn failed"
|
|
2605
|
-
});
|
|
2606
|
-
await persistThreadStateById(args.conversationId, {
|
|
2607
|
-
conversation
|
|
2608
|
-
});
|
|
2609
|
-
}
|
|
2610
|
-
async function resumeOAuthSessionRecordTurn(stored, options) {
|
|
2611
|
-
if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.channelId || !stored.destination || !stored.source || !stored.threadTs) {
|
|
2612
|
-
return;
|
|
2613
|
-
}
|
|
2614
|
-
const destination = requireSlackDestination(
|
|
2615
|
-
stored.destination,
|
|
2616
|
-
"OAuth resume"
|
|
2617
|
-
);
|
|
2618
|
-
const currentState = await getPersistedThreadState(
|
|
2619
|
-
stored.resumeConversationId
|
|
2620
|
-
);
|
|
2621
|
-
const conversation = coerceThreadConversationState(currentState);
|
|
2622
|
-
await hydrateConversationMessages({
|
|
2623
|
-
conversation,
|
|
2624
|
-
conversationId: stored.resumeConversationId
|
|
2625
|
-
});
|
|
2626
|
-
const pendingAuth = getConversationPendingAuth({
|
|
2627
|
-
conversation,
|
|
2628
|
-
kind: "plugin",
|
|
2629
|
-
provider: stored.provider,
|
|
2630
|
-
actorId: stored.userId,
|
|
2631
|
-
...stored.scope ? { scope: stored.scope } : void 0
|
|
2632
|
-
});
|
|
2633
|
-
const resolvedSessionId = pendingAuth?.sessionId ?? stored.resumeSessionId;
|
|
2634
|
-
const userMessage = resolvedSessionId ? getTurnUserMessage(conversation, resolvedSessionId) : void 0;
|
|
2635
|
-
if (pendingAuth) {
|
|
2636
|
-
if (!isPendingAuthLatestRequest(conversation, pendingAuth)) {
|
|
2637
|
-
clearPendingAuth(conversation, pendingAuth.sessionId);
|
|
2638
|
-
await persistThreadStateById(stored.resumeConversationId, {
|
|
2639
|
-
conversation
|
|
2640
|
-
});
|
|
2641
|
-
await abandonTurnRecord({
|
|
2642
|
-
conversationId: stored.resumeConversationId,
|
|
2643
|
-
turnId: pendingAuth.sessionId,
|
|
2644
|
-
errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
|
|
2645
|
-
});
|
|
2646
|
-
return;
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
const sessionRecord = await getTurnRecord(
|
|
2650
|
-
stored.resumeConversationId,
|
|
2651
|
-
resolvedSessionId
|
|
2652
|
-
);
|
|
2653
|
-
if (!sessionRecord) {
|
|
2654
|
-
return;
|
|
2655
|
-
}
|
|
2656
|
-
if (sessionRecord.state === "completed" || sessionRecord.state === "failed" || sessionRecord.state === "abandoned") {
|
|
2657
|
-
return;
|
|
2658
|
-
}
|
|
2659
|
-
if (sessionRecord.state !== "paused" || sessionRecord.resumeReason !== "auth") {
|
|
2660
|
-
return;
|
|
2661
|
-
}
|
|
2662
|
-
if (!userMessage?.author?.userId) {
|
|
2663
|
-
return;
|
|
2664
|
-
}
|
|
2665
|
-
if (!pendingAuth && conversation.processing.activeTurnId !== stored.resumeSessionId) {
|
|
2666
|
-
return;
|
|
2667
|
-
}
|
|
2668
|
-
await resumeSlackTurn({
|
|
2669
|
-
messageText: userMessage.text,
|
|
2670
|
-
conversationId: stored.resumeConversationId,
|
|
2671
|
-
turnId: resolvedSessionId,
|
|
2672
|
-
channelId: stored.channelId,
|
|
2673
|
-
threadTs: stored.threadTs,
|
|
2674
|
-
messageTs: getTurnUserSlackMessageTs(userMessage),
|
|
2675
|
-
lockKey: stored.resumeConversationId,
|
|
2676
|
-
initialText: "",
|
|
2677
|
-
executeTurn: async (run, saveResult, timeoutMs) => await executeTurn(options.agentRunner, run, saveResult, timeoutMs),
|
|
2678
|
-
beforeStart: async () => {
|
|
2679
|
-
const lockedState = await getPersistedThreadState(
|
|
2680
|
-
stored.resumeConversationId
|
|
2681
|
-
);
|
|
2682
|
-
const lockedConversation = coerceThreadConversationState(lockedState);
|
|
2683
|
-
await hydrateConversationMessages({
|
|
2684
|
-
conversation: lockedConversation,
|
|
2685
|
-
conversationId: stored.resumeConversationId
|
|
2686
|
-
});
|
|
2687
|
-
const lockedPendingAuth = getConversationPendingAuth({
|
|
2688
|
-
conversation: lockedConversation,
|
|
2689
|
-
kind: "plugin",
|
|
2690
|
-
provider: stored.provider,
|
|
2691
|
-
actorId: stored.userId,
|
|
2692
|
-
...stored.scope ? { scope: stored.scope } : void 0
|
|
2693
|
-
});
|
|
2694
|
-
const lockedSessionId = lockedPendingAuth?.sessionId ?? stored.resumeSessionId;
|
|
2695
|
-
if (lockedSessionId !== resolvedSessionId) {
|
|
2696
|
-
return false;
|
|
2697
|
-
}
|
|
2698
|
-
const lockedSessionRecord = await getTurnRecord(
|
|
2699
|
-
stored.resumeConversationId,
|
|
2700
|
-
lockedSessionId
|
|
2701
|
-
);
|
|
2702
|
-
if (!lockedSessionRecord || lockedSessionRecord.state !== "paused" || lockedSessionRecord.resumeReason !== "auth") {
|
|
2703
|
-
return false;
|
|
2704
|
-
}
|
|
2705
|
-
if (lockedPendingAuth) {
|
|
2706
|
-
if (!isPendingAuthLatestRequest(lockedConversation, lockedPendingAuth)) {
|
|
2707
|
-
clearPendingAuth(lockedConversation, lockedPendingAuth.sessionId);
|
|
2708
|
-
await persistThreadStateById(stored.resumeConversationId, {
|
|
2709
|
-
conversation: lockedConversation
|
|
2710
|
-
});
|
|
2711
|
-
await abandonTurnRecord({
|
|
2712
|
-
conversationId: stored.resumeConversationId,
|
|
2713
|
-
turnId: lockedPendingAuth.sessionId,
|
|
2714
|
-
errorMessage: "Auth completed after a newer thread message abandoned this blocked request."
|
|
2715
|
-
});
|
|
2716
|
-
return false;
|
|
2717
|
-
}
|
|
2718
|
-
} else if (lockedConversation.processing.activeTurnId !== stored.resumeSessionId) {
|
|
2719
|
-
return false;
|
|
2720
|
-
}
|
|
2721
|
-
const lockedUserMessage = getTurnUserMessage(
|
|
2722
|
-
lockedConversation,
|
|
2723
|
-
lockedSessionId
|
|
2724
|
-
);
|
|
2725
|
-
if (!lockedUserMessage?.author?.userId) {
|
|
2726
|
-
return false;
|
|
2727
|
-
}
|
|
2728
|
-
const lockedConversationContext = buildConversationContext(
|
|
2729
|
-
lockedConversation,
|
|
2730
|
-
{
|
|
2731
|
-
excludeMessageId: lockedUserMessage.id
|
|
2732
|
-
}
|
|
2733
|
-
);
|
|
2734
|
-
const lockedLocationConfiguration = getLocationConfigurationService(destination);
|
|
2735
|
-
let actor;
|
|
2736
|
-
try {
|
|
2737
|
-
actor = createSlackResumeActor({
|
|
2738
|
-
actor: lockedSessionRecord.actor,
|
|
2739
|
-
teamId: destination.teamId,
|
|
2740
|
-
userId: lockedUserMessage.author.userId
|
|
2741
|
-
});
|
|
2742
|
-
} catch {
|
|
2743
|
-
await failTurnRecord({
|
|
2744
|
-
conversationId: stored.resumeConversationId,
|
|
2745
|
-
expectedVersion: lockedSessionRecord.version,
|
|
2746
|
-
turnId: lockedSessionId,
|
|
2747
|
-
errorMessage: "Unable to rebuild Slack actor for OAuth resume"
|
|
2748
|
-
});
|
|
2749
|
-
return false;
|
|
2750
|
-
}
|
|
2751
|
-
let routing;
|
|
2752
|
-
try {
|
|
2753
|
-
routing = await resolveTurnSessionRouting({
|
|
2754
|
-
conversationId: stored.resumeConversationId
|
|
2755
|
-
});
|
|
2756
|
-
} catch (error) {
|
|
2757
|
-
await failTurnRecord({
|
|
2758
|
-
conversationId: stored.resumeConversationId,
|
|
2759
|
-
expectedVersion: lockedSessionRecord.version,
|
|
2760
|
-
turnId: lockedSessionId,
|
|
2761
|
-
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2762
|
-
});
|
|
2763
|
-
return false;
|
|
2764
|
-
}
|
|
2765
|
-
await recordAuthorizationCompleted({
|
|
2766
|
-
conversationId: stored.resumeConversationId,
|
|
2767
|
-
kind: "plugin",
|
|
2768
|
-
provider: stored.provider,
|
|
2769
|
-
actorId: stored.userId,
|
|
2770
|
-
authorizationId: pluginAuthorizationId({
|
|
2771
|
-
provider: stored.provider,
|
|
2772
|
-
sessionId: lockedSessionId
|
|
2773
|
-
})
|
|
2774
|
-
});
|
|
2775
|
-
const lockedMessageTs = getTurnUserSlackMessageTs(lockedUserMessage);
|
|
2776
|
-
return {
|
|
2777
|
-
messageText: lockedUserMessage.text,
|
|
2778
|
-
sliceId: lockedSessionRecord.sliceId,
|
|
2779
|
-
messageTs: lockedMessageTs,
|
|
2780
|
-
inputMessageIds: [lockedUserMessage.id],
|
|
2781
|
-
run: {
|
|
2782
|
-
instruction: {
|
|
2783
|
-
text: lockedUserMessage.text,
|
|
2784
|
-
context: lockedConversationContext,
|
|
2785
|
-
...getTurnUserReplyAttachmentContext(lockedUserMessage)
|
|
2786
|
-
},
|
|
2787
|
-
// Pi history is SQL-authoritative: the resumed run reads its
|
|
2788
|
-
// session record first and falls back to the step projection.
|
|
2789
|
-
history: await loadProjection({
|
|
2790
|
-
conversationId: stored.resumeConversationId
|
|
2791
|
-
}),
|
|
2792
|
-
credentialContext: {
|
|
2793
|
-
actor: {
|
|
2794
|
-
type: "user",
|
|
2795
|
-
userId: actor.userId
|
|
2796
|
-
}
|
|
2797
|
-
},
|
|
2798
|
-
actor,
|
|
2799
|
-
destination,
|
|
2800
|
-
...routing.location ? { location: routing.location } : void 0,
|
|
2801
|
-
source: routing.source,
|
|
2802
|
-
toolChannelId: stored.channelId,
|
|
2803
|
-
environment: {
|
|
2804
|
-
locationConfiguration: lockedLocationConfiguration
|
|
2805
|
-
},
|
|
2806
|
-
state: {
|
|
2807
|
-
pendingAuth: lockedPendingAuth,
|
|
2808
|
-
sandboxRef: getPersistedSandboxState(lockedState)
|
|
2809
|
-
},
|
|
2810
|
-
durability: {
|
|
2811
|
-
recordPendingAuth: async (nextPendingAuth) => {
|
|
2812
|
-
lockedConversation.processing.pendingAuth = nextPendingAuth;
|
|
2813
|
-
await persistThreadStateById(stored.resumeConversationId, {
|
|
2814
|
-
conversation: lockedConversation
|
|
2815
|
-
});
|
|
2816
|
-
}
|
|
2817
|
-
}
|
|
2818
|
-
},
|
|
2819
|
-
commitResult: async (reply) => {
|
|
2820
|
-
logInfo("oauth.callback.resume.completed", {
|
|
2821
|
-
"app.credential.provider": stored.provider,
|
|
2822
|
-
"app.ai.outcome": reply.diagnostics.outcome,
|
|
2823
|
-
"app.ai.tool_calls": reply.diagnostics.toolCalls.length
|
|
2824
|
-
});
|
|
2825
|
-
await persistCompletedOAuthReplyState({
|
|
2826
|
-
conversationId: stored.resumeConversationId,
|
|
2827
|
-
sessionId: lockedSessionId,
|
|
2828
|
-
reply
|
|
2829
|
-
});
|
|
2830
|
-
},
|
|
2831
|
-
onPostDeliveryCommitFailure: async () => {
|
|
2832
|
-
await failTurnRecord({
|
|
2833
|
-
conversationId: stored.resumeConversationId,
|
|
2834
|
-
expectedVersion: lockedSessionRecord.version,
|
|
2835
|
-
turnId: lockedSessionId,
|
|
2836
|
-
errorMessage: "OAuth-resumed reply was delivered but completion state did not persist"
|
|
2837
|
-
});
|
|
2838
|
-
},
|
|
2839
|
-
onFailure: async () => {
|
|
2840
|
-
await persistFailedOAuthReplyState({
|
|
2841
|
-
conversationId: stored.resumeConversationId,
|
|
2842
|
-
expectedVersion: lockedSessionRecord.version,
|
|
2843
|
-
sessionId: lockedSessionId
|
|
2844
|
-
});
|
|
2845
|
-
},
|
|
2846
|
-
onAuthPause: async () => {
|
|
2847
|
-
await persistAuthPauseTurnState({
|
|
2848
|
-
sessionId: lockedSessionId,
|
|
2849
|
-
threadStateId: stored.resumeConversationId
|
|
2850
|
-
});
|
|
2851
|
-
},
|
|
2852
|
-
onSuspend: async (resumeVersion) => {
|
|
2853
|
-
await wakePausedTurn({
|
|
2854
|
-
conversationId: stored.resumeConversationId,
|
|
2855
|
-
destination,
|
|
2856
|
-
turnId: lockedSessionId,
|
|
2857
|
-
expectedVersion: resumeVersion
|
|
2858
|
-
});
|
|
2859
|
-
}
|
|
2860
|
-
};
|
|
2861
|
-
}
|
|
2862
|
-
});
|
|
2863
|
-
}
|
|
2864
|
-
async function GET2(request, provider, waitUntil, options) {
|
|
2865
|
-
const localRelay = await relayLocalOAuthCallback(request);
|
|
2866
|
-
if (localRelay) {
|
|
2867
|
-
return localRelay;
|
|
2868
|
-
}
|
|
2869
|
-
const providerConfig = pluginCatalogRuntime.getOAuthConfig(provider);
|
|
2870
|
-
if (!providerConfig) {
|
|
2871
|
-
return htmlErrorResponse(
|
|
2872
|
-
"Unknown provider",
|
|
2873
|
-
"The OAuth provider in this link is not recognized.",
|
|
2874
|
-
404
|
|
2875
|
-
);
|
|
2876
|
-
}
|
|
2877
|
-
const providerLabel = formatProviderLabel(provider);
|
|
2878
|
-
const url = new URL(request.url);
|
|
2879
|
-
const errorParam = url.searchParams.get("error");
|
|
2880
|
-
const code = url.searchParams.get("code");
|
|
2881
|
-
const state = url.searchParams.get("state");
|
|
2882
|
-
if (errorParam) {
|
|
2883
|
-
if (state) {
|
|
2884
|
-
const cleanupAdapter = getStateAdapter();
|
|
2885
|
-
await cleanupAdapter.delete(`oauth-state:${state}`);
|
|
2886
|
-
}
|
|
2887
|
-
if (errorParam === "access_denied") {
|
|
2888
|
-
return htmlErrorResponse(
|
|
2889
|
-
"Authorization declined",
|
|
2890
|
-
`You declined the ${providerLabel} authorization request. Return to Junior and connect your ${providerLabel} account again if you change your mind.`,
|
|
2891
|
-
400
|
|
2892
|
-
);
|
|
2893
|
-
}
|
|
2894
|
-
return htmlErrorResponse(
|
|
2895
|
-
"Authorization failed",
|
|
2896
|
-
`${providerLabel} returned an error: ${errorParam}. Return to Junior and try again.`,
|
|
2897
|
-
400
|
|
2898
|
-
);
|
|
2899
|
-
}
|
|
2900
|
-
if (!code || !state) {
|
|
2901
|
-
return htmlErrorResponse(
|
|
2902
|
-
"Invalid request",
|
|
2903
|
-
"This authorization link is missing required parameters.",
|
|
2904
|
-
400
|
|
2905
|
-
);
|
|
2906
|
-
}
|
|
2907
|
-
const stateAdapter = getStateAdapter();
|
|
2908
|
-
const stateKey = `oauth-state:${state}`;
|
|
2909
|
-
const stored = parseOAuthStatePayload(await stateAdapter.get(stateKey));
|
|
2910
|
-
if (!stored) {
|
|
2911
|
-
return htmlErrorResponse(
|
|
2912
|
-
"Link expired",
|
|
2913
|
-
`This authorization link has expired (links are valid for 10 minutes). Return to Junior and connect your ${providerLabel} account again to get a new link.`,
|
|
2914
|
-
400
|
|
2915
|
-
);
|
|
2916
|
-
}
|
|
2917
|
-
if (stored.provider !== provider) {
|
|
2918
|
-
return htmlErrorResponse(
|
|
2919
|
-
"Provider mismatch",
|
|
2920
|
-
"This authorization link does not match the expected provider.",
|
|
2921
|
-
400
|
|
2922
|
-
);
|
|
2923
|
-
}
|
|
2924
|
-
await stateAdapter.delete(stateKey);
|
|
2925
|
-
const clientId = process.env[providerConfig.clientIdEnv]?.trim();
|
|
2926
|
-
const clientSecret = process.env[providerConfig.clientSecretEnv]?.trim();
|
|
2927
|
-
if (!clientId || !clientSecret) {
|
|
2928
|
-
return htmlErrorResponse(
|
|
2929
|
-
"Configuration error",
|
|
2930
|
-
"OAuth client credentials are not configured on the server.",
|
|
2931
|
-
500
|
|
2932
|
-
);
|
|
2933
|
-
}
|
|
2934
|
-
const baseUrl = resolveBaseUrl();
|
|
2935
|
-
if (!baseUrl) {
|
|
2936
|
-
return htmlErrorResponse(
|
|
2937
|
-
"Configuration error",
|
|
2938
|
-
"The server cannot determine its base URL.",
|
|
2939
|
-
500
|
|
2940
|
-
);
|
|
2941
|
-
}
|
|
2942
|
-
const redirectUri = `${baseUrl}${providerConfig.callbackPath}`;
|
|
2943
|
-
const requestedScope = stored.scope ?? providerConfig.scope;
|
|
2944
|
-
let tokenResponse;
|
|
2945
|
-
try {
|
|
2946
|
-
const tokenRequest = buildOAuthTokenRequest({
|
|
2947
|
-
clientId,
|
|
2948
|
-
clientSecret,
|
|
2949
|
-
payload: {
|
|
2950
|
-
grant_type: "authorization_code",
|
|
2951
|
-
code,
|
|
2952
|
-
redirect_uri: redirectUri
|
|
2953
|
-
},
|
|
2954
|
-
tokenAuthMethod: providerConfig.tokenAuthMethod,
|
|
2955
|
-
tokenExtraHeaders: providerConfig.tokenExtraHeaders
|
|
2956
|
-
});
|
|
2957
|
-
tokenResponse = await fetch(providerConfig.tokenEndpoint, {
|
|
2958
|
-
method: "POST",
|
|
2959
|
-
headers: tokenRequest.headers,
|
|
2960
|
-
body: tokenRequest.body
|
|
2961
|
-
});
|
|
2962
|
-
} catch {
|
|
2963
|
-
logWarn(
|
|
2964
|
-
"oauth.token_exchange.failed",
|
|
2965
|
-
oauthTokenErrorAttributes(provider, providerConfig.tokenEndpoint)
|
|
2966
|
-
);
|
|
2967
|
-
return htmlErrorResponse(
|
|
2968
|
-
"Connection failed",
|
|
2969
|
-
"Failed to exchange the authorization code. Please try again.",
|
|
2970
|
-
500
|
|
2971
|
-
);
|
|
2972
|
-
}
|
|
2973
|
-
if (!tokenResponse.ok) {
|
|
2974
|
-
logWarn(
|
|
2975
|
-
"oauth.token_exchange.failed",
|
|
2976
|
-
oauthTokenErrorAttributes(
|
|
2977
|
-
provider,
|
|
2978
|
-
providerConfig.tokenEndpoint,
|
|
2979
|
-
tokenResponse.status
|
|
2980
|
-
)
|
|
2981
|
-
);
|
|
2982
|
-
return htmlErrorResponse(
|
|
2983
|
-
"Connection failed",
|
|
2984
|
-
"The token exchange with the provider failed. Please try again.",
|
|
2985
|
-
500
|
|
2986
|
-
);
|
|
2987
|
-
}
|
|
2988
|
-
let parsedTokenResponse;
|
|
2989
|
-
try {
|
|
2990
|
-
const tokenData = await tokenResponse.json();
|
|
2991
|
-
parsedTokenResponse = parseOAuthTokenResponse(tokenData, requestedScope, {
|
|
2992
|
-
treatEmptyScopeAsUnreported: providerConfig.treatEmptyScopeAsUnreported
|
|
2993
|
-
});
|
|
2994
|
-
} catch {
|
|
2995
|
-
logWarn(
|
|
2996
|
-
"oauth.token_exchange.failed",
|
|
2997
|
-
oauthTokenErrorAttributes(
|
|
2998
|
-
provider,
|
|
2999
|
-
providerConfig.tokenEndpoint,
|
|
3000
|
-
tokenResponse.status
|
|
3001
|
-
)
|
|
3002
|
-
);
|
|
3003
|
-
return htmlErrorResponse(
|
|
3004
|
-
"Connection failed",
|
|
3005
|
-
"The provider returned an incomplete token response. Please try again.",
|
|
3006
|
-
500
|
|
3007
|
-
);
|
|
3008
|
-
}
|
|
3009
|
-
if (!hasRequiredOAuthScope(parsedTokenResponse.scope, requestedScope)) {
|
|
3010
|
-
return htmlErrorResponse(
|
|
3011
|
-
"Connection failed",
|
|
3012
|
-
`The ${providerLabel} authorization did not grant the access Junior requires. Return to Junior and connect your ${providerLabel} account again.`,
|
|
3013
|
-
400
|
|
3014
|
-
);
|
|
3015
|
-
}
|
|
3016
|
-
const userTokenStore = createUserTokenStore();
|
|
3017
|
-
let account;
|
|
3018
|
-
try {
|
|
3019
|
-
account = await resolvePluginOAuthAccount({
|
|
3020
|
-
provider,
|
|
3021
|
-
tokens: parsedTokenResponse
|
|
3022
|
-
});
|
|
3023
|
-
} catch {
|
|
3024
|
-
return htmlErrorResponse(
|
|
3025
|
-
"Connection failed",
|
|
3026
|
-
`Junior could not verify the connected ${providerLabel} account. Please try again.`,
|
|
3027
|
-
500
|
|
3028
|
-
);
|
|
3029
|
-
}
|
|
3030
|
-
await userTokenStore.set(stored.userId, provider, {
|
|
3031
|
-
...parsedTokenResponse,
|
|
3032
|
-
...account ? { account } : void 0
|
|
3033
|
-
});
|
|
3034
|
-
const slackActor = stored.actor?.platform === "slack" ? stored.actor : void 0;
|
|
3035
|
-
if (account && slackActor) {
|
|
3036
|
-
await runBestEffort(
|
|
3037
|
-
async () => {
|
|
3038
|
-
const slackUserId = parseSlackUserId(slackActor.userId);
|
|
3039
|
-
if (!slackUserId) {
|
|
3040
|
-
throw new Error("OAuth Slack actor user id is invalid");
|
|
3041
|
-
}
|
|
3042
|
-
const profile = await lookupSlackUserProfile(slackUserId);
|
|
3043
|
-
const slackIdentity = await upsertIdentity(getSqlExecutor(), {
|
|
3044
|
-
kind: "user",
|
|
3045
|
-
provider: "slack",
|
|
3046
|
-
providerTenantId: slackActor.teamId,
|
|
3047
|
-
providerSubjectId: profile.id,
|
|
3048
|
-
displayName: profile.real_name || profile.display_name,
|
|
3049
|
-
handle: profile.name,
|
|
3050
|
-
...profile.email ? { email: profile.email, emailVerified: true } : void 0
|
|
3051
|
-
});
|
|
3052
|
-
if (!slackIdentity.userId) {
|
|
3053
|
-
throw new Error("OAuth Slack identity is not linked to a user");
|
|
3054
|
-
}
|
|
3055
|
-
await upsertLinkedIdentity(getSqlExecutor(), slackIdentity.userId, {
|
|
3056
|
-
kind: "user",
|
|
3057
|
-
provider,
|
|
3058
|
-
providerSubjectId: account.id,
|
|
3059
|
-
displayName: account.displayName,
|
|
3060
|
-
handle: account.handle ?? account.label
|
|
3061
|
-
});
|
|
3062
|
-
},
|
|
3063
|
-
"oauth.callback.identity_link.failed",
|
|
3064
|
-
{ "app.credential.provider": provider }
|
|
3065
|
-
);
|
|
3066
|
-
}
|
|
3067
|
-
const resumeConversationId = stored.resumeConversationId;
|
|
3068
|
-
if (resumeConversationId) {
|
|
3069
|
-
await runBestEffort(
|
|
3070
|
-
async () => {
|
|
3071
|
-
await recordAuthenticationLinked({
|
|
3072
|
-
conversationId: resumeConversationId,
|
|
3073
|
-
provider,
|
|
3074
|
-
actorId: stored.userId,
|
|
3075
|
-
providerLabel,
|
|
3076
|
-
...account?.label ? { accountLabel: account.label } : void 0,
|
|
3077
|
-
...stored.resumeSessionId ? {
|
|
3078
|
-
authorizationId: pluginAuthorizationId({
|
|
3079
|
-
provider,
|
|
3080
|
-
sessionId: stored.resumeSessionId
|
|
3081
|
-
}),
|
|
3082
|
-
turnId: stored.resumeSessionId
|
|
3083
|
-
} : void 0
|
|
3084
|
-
});
|
|
3085
|
-
},
|
|
3086
|
-
"oauth.callback.authentication_event.failed",
|
|
3087
|
-
{ "app.credential.provider": provider }
|
|
3088
|
-
);
|
|
3089
|
-
}
|
|
3090
|
-
if (stored.destination?.platform !== "local" && stored.source?.kind !== "web") {
|
|
3091
|
-
waitUntil(async () => {
|
|
3092
|
-
try {
|
|
3093
|
-
await publishAppHomeView(
|
|
3094
|
-
getSlackClient(),
|
|
3095
|
-
stored.userId,
|
|
3096
|
-
userTokenStore
|
|
3097
|
-
);
|
|
3098
|
-
} catch {
|
|
3099
|
-
}
|
|
3100
|
-
});
|
|
3101
|
-
}
|
|
3102
|
-
const resumesWebTurn = Boolean(
|
|
3103
|
-
stored.source?.kind === "web" && stored.destination && stored.resumeConversationId && stored.resumeSessionId
|
|
3104
|
-
);
|
|
3105
|
-
const resumesAgentTurn = Boolean(
|
|
3106
|
-
stored.destination?.platform !== "local" && stored.resumeConversationId && stored.resumeSessionId
|
|
3107
|
-
);
|
|
3108
|
-
if (resumesWebTurn) {
|
|
3109
|
-
waitUntil(async () => {
|
|
3110
|
-
const turn = await getTurnRecord(
|
|
3111
|
-
stored.resumeConversationId,
|
|
3112
|
-
stored.resumeSessionId
|
|
3113
|
-
);
|
|
3114
|
-
await deleteWebAuthorization({
|
|
3115
|
-
actorId: stored.userId,
|
|
3116
|
-
conversationId: stored.resumeConversationId
|
|
3117
|
-
});
|
|
3118
|
-
if (!turn || turn.state !== "paused" || turn.resumeReason !== "auth") {
|
|
3119
|
-
return;
|
|
3120
|
-
}
|
|
3121
|
-
await recordAuthorizationCompleted({
|
|
3122
|
-
conversationId: stored.resumeConversationId,
|
|
3123
|
-
kind: "plugin",
|
|
3124
|
-
provider: stored.provider,
|
|
3125
|
-
actorId: stored.userId,
|
|
3126
|
-
authorizationId: pluginAuthorizationId({
|
|
3127
|
-
provider: stored.provider,
|
|
3128
|
-
sessionId: stored.resumeSessionId
|
|
3129
|
-
})
|
|
3130
|
-
});
|
|
3131
|
-
await wakePausedTurn(
|
|
3132
|
-
{
|
|
3133
|
-
conversationId: stored.resumeConversationId,
|
|
3134
|
-
destination: stored.destination,
|
|
3135
|
-
turnId: stored.resumeSessionId,
|
|
3136
|
-
expectedVersion: turn.version
|
|
3137
|
-
},
|
|
3138
|
-
options.conversationWorkQueue ? { queue: options.conversationWorkQueue } : void 0
|
|
3139
|
-
);
|
|
3140
|
-
});
|
|
3141
|
-
} else if (resumesAgentTurn) {
|
|
3142
|
-
waitUntil(
|
|
3143
|
-
() => withLogContext(
|
|
3144
|
-
{ conversationId: stored.resumeConversationId },
|
|
3145
|
-
async () => {
|
|
3146
|
-
try {
|
|
3147
|
-
await resumeOAuthSessionRecordTurn(stored, options);
|
|
3148
|
-
} catch (error) {
|
|
3149
|
-
if (error instanceof ResumeTurnBusyError) {
|
|
3150
|
-
logWarn("oauth.callback.resume.busy", {
|
|
3151
|
-
"app.credential.provider": stored.provider,
|
|
3152
|
-
...stored.resumeSessionId && {
|
|
3153
|
-
"app.ai.session_id": stored.resumeSessionId
|
|
3154
|
-
}
|
|
3155
|
-
});
|
|
3156
|
-
return;
|
|
3157
|
-
}
|
|
3158
|
-
throw error;
|
|
3159
|
-
}
|
|
3160
|
-
}
|
|
3161
|
-
)
|
|
3162
|
-
);
|
|
3163
|
-
} else if (stored.channelId && stored.threadTs) {
|
|
3164
|
-
const { channelId, threadTs } = stored;
|
|
3165
|
-
waitUntil(
|
|
3166
|
-
() => postSlackMessage({
|
|
3167
|
-
channelId,
|
|
3168
|
-
threadTs,
|
|
3169
|
-
text: `Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
|
|
3170
|
-
})
|
|
3171
|
-
);
|
|
3172
|
-
}
|
|
3173
|
-
const botName = botConfig.userName;
|
|
3174
|
-
const statusMessage = stored.destination?.platform === "local" && !resumesWebTurn ? `Your request is continuing in the local ${botName} client.` : resumesWebTurn ? `Your request is continuing in ${botName}.` : resumesAgentTurn ? "Your request is being processed in Slack." : `Your ${providerLabel} account is connected.`;
|
|
3175
|
-
const footerMessage = stored.destination?.platform === "local" || resumesWebTurn ? `You can close this tab and return to ${botName}.` : "You can close this tab and return to Slack.";
|
|
3176
|
-
return htmlCallbackResponse(
|
|
3177
|
-
escapeXml(`${providerLabel} account connected`),
|
|
3178
|
-
escapeXml(statusMessage),
|
|
3179
|
-
200,
|
|
3180
|
-
{ footerMessage: escapeXml(footerMessage) }
|
|
3181
|
-
);
|
|
3182
|
-
}
|
|
3183
|
-
|
|
3184
|
-
// src/chat/local/credential-sync.ts
|
|
3185
|
-
import { createHmac, createSecretKey, timingSafeEqual } from "crypto";
|
|
3186
|
-
import {
|
|
3187
|
-
pluginStoredTokensSchema
|
|
3188
|
-
} from "@sentry/junior-plugin-api";
|
|
3189
|
-
import { z } from "zod";
|
|
3190
|
-
var LOCAL_CREDENTIAL_SYNC_CONTEXT = "junior.local-credential-sync.v1";
|
|
3191
|
-
var LOCAL_CREDENTIAL_SYNC_MAX_AGE_MS = 6e4;
|
|
3192
|
-
var localCredentialSyncSchema = z.object({
|
|
3193
|
-
createdAtMs: z.number().finite(),
|
|
3194
|
-
provider: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
3195
|
-
tokens: pluginStoredTokensSchema
|
|
3196
|
-
}).strict();
|
|
3197
|
-
function syncSecret() {
|
|
3198
|
-
const secret = process.env.JUNIOR_SECRET?.trim();
|
|
3199
|
-
if (!secret) {
|
|
3200
|
-
throw new Error("Local credential sync requires JUNIOR_SECRET");
|
|
3201
|
-
}
|
|
3202
|
-
return secret;
|
|
3203
|
-
}
|
|
3204
|
-
function signature(body) {
|
|
3205
|
-
const signingKey = createSecretKey(Buffer.from(syncSecret(), "utf8"));
|
|
3206
|
-
return createHmac("sha256", signingKey).update(`${LOCAL_CREDENTIAL_SYNC_CONTEXT}:${body}`).digest("base64url");
|
|
3207
|
-
}
|
|
3208
|
-
function signaturesMatch(expected, actual) {
|
|
3209
|
-
const expectedBuffer = Buffer.from(expected);
|
|
3210
|
-
const actualBuffer = Buffer.from(actual);
|
|
3211
|
-
return expectedBuffer.length === actualBuffer.length && timingSafeEqual(expectedBuffer, actualBuffer);
|
|
3212
|
-
}
|
|
3213
|
-
async function syncLocalOAuthCredential(provider, tokens) {
|
|
3214
|
-
const body = JSON.stringify({
|
|
3215
|
-
createdAtMs: Date.now(),
|
|
3216
|
-
provider,
|
|
3217
|
-
tokens
|
|
3218
|
-
});
|
|
3219
|
-
const port = process.env.PORT?.trim() || "3000";
|
|
3220
|
-
const response = await fetch(
|
|
3221
|
-
`http://127.0.0.1:${port}/api/internal/local-oauth-credentials`,
|
|
3222
|
-
{
|
|
3223
|
-
method: "POST",
|
|
3224
|
-
headers: {
|
|
3225
|
-
"content-type": "application/json",
|
|
3226
|
-
"x-junior-local-credential-signature": signature(body)
|
|
3227
|
-
},
|
|
3228
|
-
body
|
|
3229
|
-
}
|
|
3230
|
-
);
|
|
3231
|
-
if (!response.ok) {
|
|
3232
|
-
throw new Error(
|
|
3233
|
-
`Could not sync local OAuth credential: HTTP ${response.status}`
|
|
3234
|
-
);
|
|
3235
|
-
}
|
|
3236
|
-
}
|
|
3237
|
-
async function receiveLocalOAuthCredential(request) {
|
|
3238
|
-
const hostname = new URL(request.url).hostname;
|
|
3239
|
-
if (process.env.NODE_ENV !== "development" || hostname !== "127.0.0.1" && hostname !== "localhost") {
|
|
3240
|
-
return new Response(null, { status: 404 });
|
|
3241
|
-
}
|
|
3242
|
-
const body = await request.text();
|
|
3243
|
-
const actualSignature = request.headers.get(
|
|
3244
|
-
"x-junior-local-credential-signature"
|
|
3245
|
-
);
|
|
3246
|
-
if (!actualSignature || !signaturesMatch(signature(body), actualSignature)) {
|
|
3247
|
-
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
3248
|
-
}
|
|
3249
|
-
let value;
|
|
3250
|
-
try {
|
|
3251
|
-
value = JSON.parse(body);
|
|
3252
|
-
} catch {
|
|
3253
|
-
return Response.json({ error: "Invalid request" }, { status: 400 });
|
|
3254
|
-
}
|
|
3255
|
-
const payload = localCredentialSyncSchema.safeParse(value);
|
|
3256
|
-
if (!payload.success || Math.abs(Date.now() - payload.data.createdAtMs) > LOCAL_CREDENTIAL_SYNC_MAX_AGE_MS) {
|
|
3257
|
-
return Response.json({ error: "Invalid request" }, { status: 400 });
|
|
3258
|
-
}
|
|
3259
|
-
await createUserTokenStore().set(
|
|
3260
|
-
"local-cli",
|
|
3261
|
-
payload.data.provider,
|
|
3262
|
-
payload.data.tokens
|
|
3263
|
-
);
|
|
3264
|
-
return new Response(null, { status: 204 });
|
|
3265
|
-
}
|
|
3266
|
-
|
|
3267
|
-
export {
|
|
3268
|
-
createSlackAdapterAssistantStatusSession,
|
|
3269
|
-
splitSlackReplyText,
|
|
3270
|
-
buildSlackOutputMessage,
|
|
3271
|
-
sendSlackReply,
|
|
3272
|
-
getSlackApiErrorCode,
|
|
3273
|
-
getSlackErrorObservabilityAttributes,
|
|
3274
|
-
isRetryableSlackPostError,
|
|
3275
|
-
isSlackTitlePermissionError,
|
|
3276
|
-
stripLeadingBotMention,
|
|
3277
|
-
getThreadId,
|
|
3278
|
-
getRunId,
|
|
3279
|
-
getChannelId,
|
|
3280
|
-
getThreadTs,
|
|
3281
|
-
getAssistantThreadContext,
|
|
3282
|
-
getMessageTs,
|
|
3283
|
-
shouldKeepProcessingReactionForToolInvocation,
|
|
3284
|
-
startProcessingReaction,
|
|
3285
|
-
buildAuthPauseResponse,
|
|
3286
|
-
resumeSlackTurn,
|
|
3287
|
-
getPausedTurnRequest,
|
|
3288
|
-
wakePausedTurn,
|
|
3289
|
-
createPausedTurns,
|
|
3290
|
-
GET,
|
|
3291
|
-
publishAppHomeView,
|
|
3292
|
-
GET2,
|
|
3293
|
-
syncLocalOAuthCredential,
|
|
3294
|
-
receiveLocalOAuthCredential
|
|
3295
|
-
};
|