@sentry/junior 0.223.0 → 0.224.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-hooks-DZU5nsd1.js → agent-hooks-DNLBwmsh.js} +6 -6
- package/dist/{agent-Bzbf92vv.js → agent-runner-9g6TQk9y.js} +809 -34
- package/dist/api.js +14 -14
- package/dist/app.js +1507 -59
- package/dist/{briefs-BJPfCrlT.js → briefs-CjXnGKzZ.js} +2 -2
- package/dist/{catalog-runtime-BthunUDD.js → catalog-runtime-CVZ9TEuK.js} +1 -1
- package/dist/{checkpoint-Deu-OVb3.js → checkpoint-Ceck_fLO.js} +7 -7
- package/dist/cli/chat.js +14 -15
- package/dist/cli/check.js +3 -3
- package/dist/cli/main.js +1 -1
- package/dist/cli/plugins.js +3 -3
- package/dist/cli/snapshot-create.js +3 -3
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-DjC6-6iU.js → client-B27ufhzl.js} +2 -2
- package/dist/{config-CtoUVs72.js → config-BAhl7-RC.js} +2 -2
- package/dist/{conversation-privacy-xS1t92xa.js → conversation-privacy-C-4JQzKD.js} +1 -1
- package/dist/credential-sync-BXHIxbtS.js +940 -0
- package/dist/{execution-stats-CrXs9vSH.js → execution-stats-DikPOxJ1.js} +3 -3
- package/dist/{executor-AOhG9X1t.js → executor-BG2Armby.js} +1 -1
- package/dist/instrumentation.js +2 -2
- package/dist/{job-queue-CNd9cpIy.js → job-queue-D4oAVrL9.js} +1 -1
- package/dist/{logging-DiN3o3nB.js → logging-iY0ssa2q.js} +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{pending-auth-D7vmttn_.js → oauth-authorization-BIijVS2N.js} +6 -48
- package/dist/{oauth-callback-server-Ca0VLUEw.js → oauth-callback-server-DPkK3kzi.js} +4 -4
- package/dist/{profile-BsNrw8lj.js → profile-6KWhg_-U.js} +5 -9
- package/dist/{registry-Dankt-Pl.js → registry-Ctgdjt-W.js} +1 -1
- package/dist/{resolve-B38CpJe2.js → resolve-CB45ZukQ.js} +2 -2
- package/dist/{runner-Ck5rEjCm.js → runner-BwKCqoF3.js} +8 -9
- package/dist/{sandbox-egress-signals-BOfFkvly.js → sandbox-egress-signals-Cmy8ODat.js} +1 -1
- package/dist/{short-title-BHd_jUFX.js → short-title-xEkZwkcV.js} +2 -2
- package/dist/{skills-DbP4TDft.js → skills-c2bMDVRS.js} +2 -2
- package/dist/{spawn-DzXc_PKO.js → spawn-Cg6lyjE7.js} +2 -2
- package/dist/{stats-pNGp3G5q.js → stats-DP9zl3Jg.js} +6 -6
- package/dist/{task-queue-Cy9y1foT.js → task-queue-SL4uLfBB.js} +3 -3
- package/dist/{task-runner-DI1FBJFJ.js → task-runner-fntmnqzx.js} +8 -83
- package/dist/{thread-state-DXgxOYpZ.js → thread-state-Wfr0GdKS.js} +5 -5
- package/dist/{auth-pause-state-Bn2vX4PA.js → turn-failure-response-DLjSEm-R.js} +73 -72
- package/dist/{turn-lifecycle-Bjf6y2GO.js → turn-lifecycle-5_jvLqRz.js} +1 -1
- package/dist/turn-session-routing-CQaysLoe.js +77 -0
- package/dist/{validation-D5CIslEu.js → validation-CnZkCN7o.js} +1 -1
- package/dist/{version-_ZQFlslB.js → version-C6lA-h_o.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{web-authorization-6mmfS_o-.js → web-authorization-Bbx999_q.js} +1 -1
- package/dist/{web-input-BJpWm6go.js → web-input-BoE1xE-K.js} +7 -7
- package/dist/{work-DvIlJR3C.js → work-4Nhz9mVv.js} +52 -10
- package/dist/{worker-D1qidg-Y.js → worker-CdWPsNL2.js} +5 -5
- package/package.json +4 -4
- package/dist/agent-runner-2VQ9WhkG.js +0 -679
- package/dist/credential-sync-CKdXEUDO.js +0 -2811
- package/dist/experimental-CIXf7Uye.js +0 -46
- package/dist/turn-execution-BNOXrZ-M.js +0 -47
package/dist/app.js
CHANGED
|
@@ -1,41 +1,39 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { b as isProviderRetryError, i as completeText, m as escapeXml, r as completeObject } from "./client-
|
|
3
|
-
import { A as JUNIOR_WORKSPACE_SNAPSHOT_JOB_CALLBACK_ROUTE, C as withSpan, D as JUNIOR_PLUGIN_TASK_CALLBACK_ROUTE, F as isRecord$1, I as toOptionalNumber, L as toOptionalString, S as withLogContext, _ as setSpanAttributes, d as logInfo, f as logWarn, g as setSentryUser, l as logError, n as createChatSdkLogger, o as getActiveTraceId, r as createRequestContext, t as buildTurnFailureResponse, u as logException, v as setSpanStatus, x as withContext, y as setTags } from "./logging-
|
|
1
|
+
import { $ as clearSandboxEgressSignals, A as renderAttachmentText, Ct as shouldEmitDevAgentTrace, E as unpublishedArtifactHeaders, F as getNextRunAtMs, G as isEventMailboxMetadata, H as processWorkspaceSnapshotJob, J as recordFinishedTurnForAutomatedLimit, K as renderTaskInput, M as listThreadReplies, N as logScheduledAutomationRunSkipped, O as readAttachmentVisionSummary, P as scheduledAutomationRunAttributes, Q as SANDBOX_EGRESS_PROXY_PATH, S as latestProgressStatus, St as AuthorizationFlowDisabledError, T as readPublicArtifact, Tt as setConfigDefaults, U as ingestEvent, W as EVENT_WAIT_MS, X as normalizeSandboxEgressTracePropagationDomains, Y as resolveSandboxEgressProviderForHost, Z as shouldPropagateSandboxEgressTrace, _t as buildSteeringPiMessage, a as isExperimentalFeatureEnabled, c as createContextCompactor, ct as escapeSlackMrkdwnText, dt as normalizeSlackStatusText, et as consumeSandboxEgressAuthRequiredSignal, f as deleteMcpAuthSessionsForUserProvider, ft as truncateStatusText, gt as createUserTokenStore, ht as removeReactionFromMessage, i as getExperimentalFeatures, it as formatProviderLabel, j as renderBlockText, k as recordSlackAttachment, l as executeCredentialedEgressRequest, lt as formatSlackLink, m as deleteMcpStoredOAuthCredentials, mt as postSlackMessage, nt as parseSandboxEgressCredentialToken, o as setExperimentalFeatures, p as deleteMcpServerSessionId, pt as addReactionToMessage, q as maybePostAutomatedTurnLimitNotice, s as executeAgentRun, st as startOAuthFlow, t as createAgentRunner, tt as consumeSandboxEgressPermissionDeniedSignal, ut as normalizeSlackReplyMarkdown, v as PluginCredentialFailureError, vt as isVisionImageMediaType, w as publicArtifactHeaders, wt as getConfigDefaults, x as isPendingAuthLatestRequest, y as clearPendingAuth, yt as RetryableDeliveryError } from "./agent-runner-9g6TQk9y.js";
|
|
2
|
+
import { b as isProviderRetryError, i as completeText, m as escapeXml, r as completeObject } from "./client-B27ufhzl.js";
|
|
3
|
+
import { A as JUNIOR_WORKSPACE_SNAPSHOT_JOB_CALLBACK_ROUTE, C as withSpan, D as JUNIOR_PLUGIN_TASK_CALLBACK_ROUTE, F as isRecord$1, I as toOptionalNumber, L as toOptionalString, S as withLogContext, _ as setSpanAttributes, d as logInfo, f as logWarn, g as setSentryUser, l as logError, n as createChatSdkLogger, o as getActiveTraceId, r as createRequestContext, t as buildTurnFailureResponse, u as logException, v as setSpanStatus, x as withContext, y as setTags } from "./logging-iY0ssa2q.js";
|
|
4
4
|
import { r as continueTrace } from "./sentry-CgWZM_kV.js";
|
|
5
|
-
import { t as JUNIOR_VERSION } from "./version-
|
|
6
|
-
import { a as getChatConfig, c as getSlackClientId, d as getSlackSigningSecret, f as setBotModelConfig, i as configureFunctionMaxDurationSeconds, l as getSlackClientSecret, m as setSlackReactionConfig, n as botConfig, p as setProfiles, s as getSlackBotToken, u as getSlackReactionConfig } from "./config-
|
|
5
|
+
import { t as JUNIOR_VERSION } from "./version-C6lA-h_o.js";
|
|
6
|
+
import { a as getChatConfig, c as getSlackClientId, d as getSlackSigningSecret, f as setBotModelConfig, h as normalizeSlackEmojiName, i as configureFunctionMaxDurationSeconds, l as getSlackClientSecret, m as setSlackReactionConfig, n as botConfig, p as setProfiles, s as getSlackBotToken, u as getSlackReactionConfig } from "./config-BAhl7-RC.js";
|
|
7
7
|
import { a as modelIdForProfile, n as defaultModelId } from "./model-profile-CZmVoGdX.js";
|
|
8
|
-
import { _ as parseSlackMessageTs, a as createActor, c as isActorUserId, o as createSlackActor, p as parseSlackChannelId, s as createSlackResumeActor, t as credentialContextForActor, u as parseActorUserId } from "./context-Bplcgypl.js";
|
|
9
|
-
import { B as downloadPrivateSlackFile, F as bindSlackDirectCredentialSubject, G as isDmChannel, I as verifyEventAutomationCredentialSubject, Jt as getSqlExecutor, K as normalizeSlackConversationId, Kt as getConversationStore, L as verifyScheduledAutomationCredentialSubject, Lt as listConversationAnnotations, N as bindEventAutomationCredentialSubject, P as bindScheduledAutomationCredentialSubject, Pt as resolveRootVisibility, Q as readActorIdentity, Qt as withConversationMutationLock, R as verifySlackDirectCredentialSubject, U as getSlackClient, Wt as getConversationEventStore, X as runWithWorkspaceTeamId, Xt as withConversationEventLock, Y as getWorkspaceTeamId, _t as createPluginState, a as getPluginRoutes, bt as createPluginLogger, cn as deleteProviderIdentityForSlackUser, ct as collectEventMatchKeys, d as setPlugins, dn as createSlackDestination, dt as findMatchingWatches, f as validatePlugins, gt as setDashboardConversationLinkOptions, h as setBriefsConfig, ht as getDashboardTaskLink, i as getPluginApiRoutes, mt as getDashboardConversationLink, ot as cancelSubscriptions, pn as requireSlackDestination, pt as JUNIOR_THREAD_STATE_TTL_MS, q as runWithSlackInstallationToken, qt as getDb, u as getPlugins, xt as readConversationAccessFromSql, z as SlackActionError } from "./agent-hooks-
|
|
8
|
+
import { _ as parseSlackMessageTs, a as createActor, c as isActorUserId, l as isUserActor, o as createSlackActor, p as parseSlackChannelId, s as createSlackResumeActor, t as credentialContextForActor, u as parseActorUserId } from "./context-Bplcgypl.js";
|
|
9
|
+
import { B as downloadPrivateSlackFile, F as bindSlackDirectCredentialSubject, G as isDmChannel, H as getHeaderString, I as verifyEventAutomationCredentialSubject, Jt as getSqlExecutor, K as normalizeSlackConversationId, Kt as getConversationStore, L as verifyScheduledAutomationCredentialSubject, Lt as listConversationAnnotations, N as bindEventAutomationCredentialSubject, Nt as buildSentryConversationUrl, P as bindScheduledAutomationCredentialSubject, Pt as resolveRootVisibility, Q as readActorIdentity, Qt as withConversationMutationLock, R as verifySlackDirectCredentialSubject, U as getSlackClient, Wt as getConversationEventStore, X as runWithWorkspaceTeamId, Xt as withConversationEventLock, Y as getWorkspaceTeamId, _t as createPluginState, a as getPluginRoutes, bt as createPluginLogger, cn as deleteProviderIdentityForSlackUser, ct as collectEventMatchKeys, d as setPlugins, dn as createSlackDestination, dt as findMatchingWatches, f as validatePlugins, gt as setDashboardConversationLinkOptions, h as setBriefsConfig, ht as getDashboardTaskLink, i as getPluginApiRoutes, mt as getDashboardConversationLink, o as getPluginSlackConversationLink, ot as cancelSubscriptions, pn as requireSlackDestination, pt as JUNIOR_THREAD_STATE_TTL_MS, q as runWithSlackInstallationToken, qt as getDb, u as getPlugins, xt as readConversationAccessFromSql, z as SlackActionError } from "./agent-hooks-DNLBwmsh.js";
|
|
10
10
|
import { n as juniorDestinations } from "./destinations-BeBiJeEM.js";
|
|
11
|
-
import { E as juniorConversations, a as juniorSchedulerRuns, f as juniorAgentBindings, g as juniorConversationBriefs, o as juniorSchedulerTasks, p as juniorAgentInvocations, y as juniorConversationEvents } from "./executor-
|
|
11
|
+
import { E as juniorConversations, a as juniorSchedulerRuns, f as juniorAgentBindings, g as juniorConversationBriefs, o as juniorSchedulerTasks, p as juniorAgentInvocations, y as juniorConversationEvents } from "./executor-BG2Armby.js";
|
|
12
12
|
import { Y as instructionProvenanceFor, k as annotationCard } from "./conversation-1HUoZr2l.js";
|
|
13
|
-
import { n as pluginCatalogRuntime } from "./catalog-runtime-
|
|
14
|
-
import { $ as
|
|
15
|
-
import { E as acquireActiveLock, O as
|
|
16
|
-
import { Bt as hasRunnableConversationWork$1, Dt as hasRunnableConversationWork, Et as getConversationWorkState, F as effectiveTaskOutcomes, Ht as isInvalidConversationRecordError, It as stopConversationWork, Kt as buildDeterministicAssistantMessageId, Mt as removeActiveConversation, Ot as listActiveConversationIds, Pt as requestConversationWork, Rt as CONVERSATION_WORK_STALE_ENQUEUE_MS, S as withScheduledAutomationLock, Tt as getConversation, U as findMatchingEventAutomations, X as getEventCatalog, a as requestAttachmentDeletion, b as saveScheduledAutomationInLock, c as parseSlackThreadId, g as listScheduledAutomationsForTeam, h as listScheduledAutomations, o as storeAttachment, pt as appendAndEnqueueInboundMessage, qt as buildDeterministicTurnId, r as collectAttachmentGarbage, tt as eventGuidance, v as readScheduledAutomation, vt as clearExpiredConversationLease, w as scheduledRunSchema, wt as ensureConversationWake, x as scheduledRunId, z as collectEventAutomationMatchKeys } from "./stats-
|
|
17
|
-
import {
|
|
18
|
-
import { h as postSlackMessage, i as formatProviderLabel, s as startOAuthFlow, t as createAgentRunner, v as RetryableDeliveryError } from "./agent-runner-2VQ9WhkG.js";
|
|
13
|
+
import { n as pluginCatalogRuntime } from "./catalog-runtime-CVZ9TEuK.js";
|
|
14
|
+
import { $ as appendThreadContextMessages, A as commitAcceptedReply, B as recordAuthorizationCompleted, J as hydrateConversationMessages, N as loadConversationProjection, O as trimTrailingAssistantMessages, P as loadProjection, Q as resolveConversationTitle, W as recordSubscribedReplyRoute, X as projectConversationMessages, Y as persistConversationMessages, Z as persistConversationMessageSummaries, a as completeTurnRecord, at as markConversationMessage, c as listTurnSummaries, ct as turnHasReply, et as buildConversationContext, i as abandonTurnRecord, it as isHumanConversationMessage, j as commitMessages, l as recordTurnSummary, lt as upsertConversationMessage, n as loadTurnCheckpoint, nt as estimateConversationContextTokens, o as failTurnRecord, ot as normalizeConversationText, p as buildTurnErrorResponse, q as appendConversationMessages, r as saveTurnCheckpoint, rt as getConversationMessageSlackTs, s as getTurnRecord, st as recordDeliveredAssistantMessage, tt as createConversationMemoryService, u as persistWithRetry, ut as estimateTextTokens } from "./checkpoint-Ceck_fLO.js";
|
|
15
|
+
import { E as acquireActiveLock, O as withLock$1, T as MUTATION_LOCK_TTL_MS$1, w as getStateAdapter } from "./profile-6KWhg_-U.js";
|
|
16
|
+
import { Bt as hasRunnableConversationWork$1, Dt as hasRunnableConversationWork, Et as getConversationWorkState, F as effectiveTaskOutcomes, Ht as isInvalidConversationRecordError, It as stopConversationWork, Kt as buildDeterministicAssistantMessageId, Mt as removeActiveConversation, Ot as listActiveConversationIds, Pt as requestConversationWork, Rt as CONVERSATION_WORK_STALE_ENQUEUE_MS, S as withScheduledAutomationLock, Tt as getConversation, U as findMatchingEventAutomations, X as getEventCatalog, a as requestAttachmentDeletion, b as saveScheduledAutomationInLock, c as parseSlackThreadId, d as resolveSlackChannelIdFromThreadId, g as listScheduledAutomationsForTeam, h as listScheduledAutomations, l as readSlackRawMessageContext, o as storeAttachment, pt as appendAndEnqueueInboundMessage, qt as buildDeterministicTurnId, r as collectAttachmentGarbage, tt as eventGuidance, u as resolveSlackChannelIdFromMessage, v as readScheduledAutomation, vt as clearExpiredConversationLease, w as scheduledRunSchema, wt as ensureConversationWake, x as scheduledRunId, z as collectEventAutomationMatchKeys } from "./stats-DP9zl3Jg.js";
|
|
17
|
+
import { _ as markTurnFailed, c as CooperativeTurnYieldError, d as getConversationTurnBoundaryError, f as isCooperativeTurnYieldError, g as markTurnCompleted, h as markTurnClosed, l as TurnInputCommitLostError, m as isTurnInputDeferredError, p as isTurnInputCommitLostError, r as getAssistantReplyText, s as ConversationTurnBoundaryError, u as TurnInputDeferredError, v as startActiveTurn } from "./oauth-authorization-BIijVS2N.js";
|
|
19
18
|
import { a as EVENT_SYSTEM_ACTOR, i as EVENT_MESSAGE_AUTHOR, o as isEventConversationMessage } from "./prompt-90miy8VV.js";
|
|
20
|
-
import { f as setSandboxResourceConfig, u as getSandboxResourceConfig } from "./resolve-
|
|
19
|
+
import { f as setSandboxResourceConfig, u as getSandboxResourceConfig } from "./resolve-CB45ZukQ.js";
|
|
21
20
|
import { l as conversationQueueMessageSchema, n as resolveConversationWorkQueueTopic, r as verifyConversationQueueMessage, t as getVercelConversationWorkQueue, u as isConversationQueueMessageRejectedError } from "./vercel-queue-DIkVcfK4.js";
|
|
22
|
-
import { a as getTurnRequestDeadline, i as verifyWorkspaceSnapshotJobMessage, t as WORKSPACE_SNAPSHOT_JOB_QUEUE_TOPIC } from "./job-queue-
|
|
23
|
-
import {
|
|
24
|
-
import { n as validatePluginRegistrations, t as validatePluginEgressCredentialHooks } from "./validation-D5CIslEu.js";
|
|
21
|
+
import { a as getTurnRequestDeadline, i as verifyWorkspaceSnapshotJobMessage, t as WORKSPACE_SNAPSHOT_JOB_QUEUE_TOPIC } from "./job-queue-D4oAVrL9.js";
|
|
22
|
+
import { n as validatePluginRegistrations, t as validatePluginEgressCredentialHooks } from "./validation-CnZkCN7o.js";
|
|
25
23
|
import { a as pluginRuntimeRegistrationsFromPluginSet, n as pluginCatalogConfigFromEnv, r as pluginCatalogConfigFromPluginSet, t as defineJuniorPlugins } from "./plugins-egKWjgue.js";
|
|
26
|
-
import { S as GET$3, _ as readViewerAutomationSummary, a as webActorFromEmail, b as legacyWebMailboxMetadataSchema, i as recordWebConversationActivity, l as resolveSlackChannelTypeFromMessage, n as conversationTurnIdForMessage, o as resolveConversationDestination, s as conversationVisibilityFromSlackChannelType, t as appendAndEnqueueWebMessage, u as resolveSlackConversationContext, x as createVercelAttachmentStorage, y as lookupSlackUser } from "./web-input-
|
|
27
|
-
import { u as recordAutomationExecution } from "./execution-stats-
|
|
28
|
-
import { _ as
|
|
29
|
-
import { i as resolveAgentInvocationId, o as
|
|
30
|
-
import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-
|
|
31
|
-
import { n as
|
|
32
|
-
import { a as
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import { t as
|
|
38
|
-
import { t as
|
|
24
|
+
import { S as GET$3, _ as readViewerAutomationSummary, a as webActorFromEmail, b as legacyWebMailboxMetadataSchema, i as recordWebConversationActivity, l as resolveSlackChannelTypeFromMessage, n as conversationTurnIdForMessage, o as resolveConversationDestination, s as conversationVisibilityFromSlackChannelType, t as appendAndEnqueueWebMessage, u as resolveSlackConversationContext, x as createVercelAttachmentStorage, y as lookupSlackUser } from "./web-input-BoE1xE-K.js";
|
|
25
|
+
import { u as recordAutomationExecution } from "./execution-stats-DikPOxJ1.js";
|
|
26
|
+
import { _ as markDispatchFailed, a as confirmDispatchMailboxAppend, c as getDispatchInputMessageId, d as getPluginDispatchProjection, f as isTerminalDispatchStatus, g as markDispatchCompleted, h as markDispatchBlocked, i as claimDispatchMailboxAppend, l as getDispatchRecord, m as markDispatchAwaitingResume, n as scheduleSessionCompletedPluginTasks, o as createOrGetDispatch, p as listPendingDispatchMailboxAppends, s as getDispatchConversationId, t as processPluginTask, u as getDispatchTurnId, v as markDispatchRunning } from "./task-runner-fntmnqzx.js";
|
|
27
|
+
import { c as listPendingAgentInvocationMailboxAppends, i as resolveAgentInvocationId, o as AgentRunError, r as enqueueAgentInvocation, s as executeTurn, t as createAgentInvocationWorker } from "./work-4Nhz9mVv.js";
|
|
28
|
+
import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-5_jvLqRz.js";
|
|
29
|
+
import { a as persistThreadState, c as configValueSchema, i as persistThreadRuntimeState, n as getPersistedSandboxState, o as persistThreadStateById, r as getPersistedThreadState, s as coerceThreadConversationState, t as getLocationConfigurationService } from "./thread-state-Wfr0GdKS.js";
|
|
30
|
+
import { a as GET$4, c as wakePausedTurn, i as publishAppHomeView, l as getTurnAuthorization, o as createPausedTurns, r as GET$5, s as getPausedTurnRequest, t as receiveLocalOAuthCredential } from "./credential-sync-BXHIxbtS.js";
|
|
31
|
+
import { n as resolveTurnSessionRouting } from "./turn-session-routing-CQaysLoe.js";
|
|
32
|
+
import { n as deleteWebAuthorization, t as createWebAuthorization } from "./web-authorization-Bbx999_q.js";
|
|
33
|
+
import { a as registerVercelPluginTaskDevConsumer, n as createVercelPluginTaskCallback, s as queueCallback } from "./task-queue-SL4uLfBB.js";
|
|
34
|
+
import { t as processConversationWork } from "./worker-CdWPsNL2.js";
|
|
35
|
+
import { t as bindSpawnAgent } from "./spawn-Cg6lyjE7.js";
|
|
36
|
+
import { a as persistAuthPauseTurnState, c as getTurnUserSlackMessageTs, d as loadPendingMessageCards, i as completeAuthPauseTurn, l as buildDeliveredTurnStatePatch, n as getAgentTurnDiagnosticsAttributes, o as getTurnUserMessage, r as requireTurnFailureEventId, s as getTurnUserReplyAttachmentContext, t as finalizeFailedTurnReplyWithEvent, u as getInterruptionMarker } from "./turn-failure-response-DLjSEm-R.js";
|
|
39
37
|
import { createRequire } from "node:module";
|
|
40
38
|
import { pathToFileURL } from "node:url";
|
|
41
39
|
import { Hono } from "hono";
|
|
@@ -1920,6 +1918,110 @@ async function GET(request) {
|
|
|
1920
1918
|
});
|
|
1921
1919
|
}
|
|
1922
1920
|
//#endregion
|
|
1921
|
+
//#region src/chat/slack/automation-card.ts
|
|
1922
|
+
function preview(text, length) {
|
|
1923
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
1924
|
+
return compact.length > length ? `${compact.slice(0, length - 1)}…` : compact;
|
|
1925
|
+
}
|
|
1926
|
+
/** Show an Automation as a native Work Object, not an operation receipt. */
|
|
1927
|
+
function renderSlackAutomationCard(card) {
|
|
1928
|
+
const title = preview(card.title, 160);
|
|
1929
|
+
const trigger = preview(card.trigger, 500);
|
|
1930
|
+
const warning = card.warning ? preview(card.warning, 500) : null;
|
|
1931
|
+
const text = [
|
|
1932
|
+
card.url ? formatSlackLink(card.url, title) : escapeSlackMrkdwnText(title),
|
|
1933
|
+
escapeSlackMrkdwnText(trigger),
|
|
1934
|
+
warning ? escapeSlackMrkdwnText(warning) : null
|
|
1935
|
+
].filter(Boolean).join("\n");
|
|
1936
|
+
if (!card.url) return {
|
|
1937
|
+
entity: null,
|
|
1938
|
+
text
|
|
1939
|
+
};
|
|
1940
|
+
return {
|
|
1941
|
+
text,
|
|
1942
|
+
entity: {
|
|
1943
|
+
entity_type: "slack#/entities/item",
|
|
1944
|
+
external_ref: {
|
|
1945
|
+
id: card.id,
|
|
1946
|
+
type: "automation"
|
|
1947
|
+
},
|
|
1948
|
+
url: card.url,
|
|
1949
|
+
entity_payload: {
|
|
1950
|
+
attributes: {
|
|
1951
|
+
title: { text: title },
|
|
1952
|
+
display_type: "Automation"
|
|
1953
|
+
},
|
|
1954
|
+
custom_fields: [{
|
|
1955
|
+
key: "trigger",
|
|
1956
|
+
label: "When",
|
|
1957
|
+
type: "string",
|
|
1958
|
+
value: trigger,
|
|
1959
|
+
long: true
|
|
1960
|
+
}, ...warning ? [{
|
|
1961
|
+
key: "warning",
|
|
1962
|
+
label: "Needs attention",
|
|
1963
|
+
type: "string",
|
|
1964
|
+
value: warning,
|
|
1965
|
+
long: true
|
|
1966
|
+
}] : []]
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
//#endregion
|
|
1972
|
+
//#region src/chat/slack/object-card.ts
|
|
1973
|
+
/** Render verified annotation facts as a native Task or Item preview. */
|
|
1974
|
+
function renderSlackObjectCard(card, conversationId) {
|
|
1975
|
+
if (card.objectType === "automation" && card.plugin === "junior") return renderSlackAutomationCard({
|
|
1976
|
+
id: card.key,
|
|
1977
|
+
title: card.title,
|
|
1978
|
+
url: card.url,
|
|
1979
|
+
trigger: card.trigger ?? "",
|
|
1980
|
+
warning: card.warning ?? null
|
|
1981
|
+
});
|
|
1982
|
+
const title = card.title.slice(0, 160);
|
|
1983
|
+
const text = [
|
|
1984
|
+
card.url ? formatSlackLink(card.url, title) : escapeSlackMrkdwnText(title),
|
|
1985
|
+
escapeSlackMrkdwnText(card.label),
|
|
1986
|
+
card.status ? escapeSlackMrkdwnText(card.status) : null
|
|
1987
|
+
].filter(Boolean).join("\n");
|
|
1988
|
+
if (!card.url) return {
|
|
1989
|
+
entity: null,
|
|
1990
|
+
text
|
|
1991
|
+
};
|
|
1992
|
+
const task = card.objectType === "task";
|
|
1993
|
+
return {
|
|
1994
|
+
text,
|
|
1995
|
+
entity: {
|
|
1996
|
+
entity_type: task ? "slack#/entities/task" : "slack#/entities/item",
|
|
1997
|
+
external_ref: {
|
|
1998
|
+
id: JSON.stringify([
|
|
1999
|
+
conversationId,
|
|
2000
|
+
card.plugin,
|
|
2001
|
+
card.key
|
|
2002
|
+
]),
|
|
2003
|
+
type: "annotation"
|
|
2004
|
+
},
|
|
2005
|
+
url: card.url,
|
|
2006
|
+
entity_payload: {
|
|
2007
|
+
attributes: {
|
|
2008
|
+
title: { text: title },
|
|
2009
|
+
display_id: card.label,
|
|
2010
|
+
display_type: card.objectType === "code_change" ? "Pull request" : task ? "Issue" : "Item",
|
|
2011
|
+
product_name: card.plugin
|
|
2012
|
+
},
|
|
2013
|
+
fields: task && card.status ? { status: { value: card.status } } : void 0,
|
|
2014
|
+
custom_fields: !task && card.status ? [{
|
|
2015
|
+
key: "status",
|
|
2016
|
+
label: "Status",
|
|
2017
|
+
type: "string",
|
|
2018
|
+
value: card.status
|
|
2019
|
+
}] : []
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
//#endregion
|
|
1923
2025
|
//#region src/chat/slack/annotation-details.ts
|
|
1924
2026
|
const eventSchema$1 = z.object({
|
|
1925
2027
|
trigger_id: z.string().min(1),
|
|
@@ -2076,11 +2178,11 @@ const BARE_STOP_RE = /^stop(?:\s*[.!…]+)?$/i;
|
|
|
2076
2178
|
const ACKNOWLEDGMENT_ONLY_RE = /^(?:thanks(?: you)?|thank you|thx|ty|got it|sounds good|sgtm|lgtm|ok(?:ay)?|cool|nice|perfect|awesome|great|makes sense|understood|roger|yep|yup|kk|on it|will do)(?:[.!]+)?$/i;
|
|
2077
2179
|
const DIRECTED_FOLLOW_UP_CUE_RE = /\b(?:you said|you just said|your last response|your last answer|what did you just say|what do you mean|what did you mean|explain(?: that| this| it| more)?|clarify(?: that| this| it)?|expand(?: on)?(?: that| this| it)?|elaborate(?: on)?(?: that| this| it)?|say more)\b/i;
|
|
2078
2180
|
const TERSE_CLARIFICATION_RE = /^(?:which one|which ones|why|how so|what do you mean|what did you mean|say more|explain that|clarify that|expand on that|elaborate on that)\??$/i;
|
|
2079
|
-
function escapeRegExp$
|
|
2181
|
+
function escapeRegExp$2(value) {
|
|
2080
2182
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2081
2183
|
}
|
|
2082
2184
|
function containsAssistantInvocation(text, botUserName) {
|
|
2083
|
-
const escapedUserName = escapeRegExp$
|
|
2185
|
+
const escapedUserName = escapeRegExp$2(botUserName);
|
|
2084
2186
|
const plainNameMentionRe = new RegExp(`(^|\\s)@${escapedUserName}\\b`, "i");
|
|
2085
2187
|
const labeledEntityMentionRe = new RegExp(`<@[^>|]+\\|${escapedUserName}>`, "i");
|
|
2086
2188
|
return plainNameMentionRe.test(text) || labeledEntityMentionRe.test(text);
|
|
@@ -2371,6 +2473,53 @@ async function decideSubscribedThreadReply(args) {
|
|
|
2371
2473
|
}
|
|
2372
2474
|
}
|
|
2373
2475
|
//#endregion
|
|
2476
|
+
//#region src/chat/slack/errors.ts
|
|
2477
|
+
/** Extract Slack's stable API error code from Web API errors. */
|
|
2478
|
+
function getSlackApiErrorCode(error) {
|
|
2479
|
+
if (!error || typeof error !== "object") return;
|
|
2480
|
+
const candidate = error;
|
|
2481
|
+
if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) return candidate.data.error;
|
|
2482
|
+
if (typeof candidate.code === "string" && candidate.code.trim().length > 0) return candidate.code;
|
|
2483
|
+
}
|
|
2484
|
+
/** Convert Slack Web API error details into the repository's trace attributes. */
|
|
2485
|
+
function getSlackErrorObservabilityAttributes(error) {
|
|
2486
|
+
if (!error || typeof error !== "object") return {};
|
|
2487
|
+
const candidate = error;
|
|
2488
|
+
const attributes = {};
|
|
2489
|
+
if (typeof candidate.code === "string" && candidate.code.trim().length > 0) attributes["app.slack.error_code"] = candidate.code;
|
|
2490
|
+
if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) attributes["app.slack.api_error"] = candidate.data.error;
|
|
2491
|
+
const requestId = getHeaderString(candidate.headers, "x-slack-req-id");
|
|
2492
|
+
if (requestId) attributes["app.slack.request_id"] = requestId;
|
|
2493
|
+
if (typeof candidate.statusCode === "number" && Number.isFinite(candidate.statusCode)) attributes["http.response.status_code"] = candidate.statusCode;
|
|
2494
|
+
return attributes;
|
|
2495
|
+
}
|
|
2496
|
+
const TRANSIENT_SLACK_API_ERRORS = /* @__PURE__ */ new Set([
|
|
2497
|
+
"fatal_error",
|
|
2498
|
+
"internal_error",
|
|
2499
|
+
"rate_limited",
|
|
2500
|
+
"ratelimited",
|
|
2501
|
+
"request_timeout",
|
|
2502
|
+
"service_unavailable"
|
|
2503
|
+
]);
|
|
2504
|
+
/** Recover transient or ambiguous Slack failures, but stop on explicit rejection. */
|
|
2505
|
+
function isRetryableSlackPostError(error) {
|
|
2506
|
+
if (!error || typeof error !== "object") return true;
|
|
2507
|
+
const candidate = error;
|
|
2508
|
+
if (typeof candidate.statusCode === "number") {
|
|
2509
|
+
if (candidate.statusCode === 429 || candidate.statusCode >= 500) return true;
|
|
2510
|
+
if (candidate.statusCode >= 400) return false;
|
|
2511
|
+
}
|
|
2512
|
+
const apiError = error instanceof SlackActionError ? error.apiError : typeof candidate.data?.error === "string" ? candidate.data.error : void 0;
|
|
2513
|
+
if (apiError) return TRANSIENT_SLACK_API_ERRORS.has(apiError);
|
|
2514
|
+
if (error instanceof SlackActionError) return error.code === "rate_limited" || error.code === "internal_error";
|
|
2515
|
+
return true;
|
|
2516
|
+
}
|
|
2517
|
+
/** Report whether Slack rejected assistant title updates for stable auth reasons. */
|
|
2518
|
+
function isSlackTitlePermissionError(error) {
|
|
2519
|
+
const code = getSlackApiErrorCode(error);
|
|
2520
|
+
return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
|
|
2521
|
+
}
|
|
2522
|
+
//#endregion
|
|
2374
2523
|
//#region src/chat/slack/message/content.ts
|
|
2375
2524
|
/**
|
|
2376
2525
|
* Slack message content projection.
|
|
@@ -2411,6 +2560,166 @@ function parseContent(message) {
|
|
|
2411
2560
|
};
|
|
2412
2561
|
}
|
|
2413
2562
|
//#endregion
|
|
2563
|
+
//#region src/chat/runtime/thread-context.ts
|
|
2564
|
+
function escapeRegExp$1(value) {
|
|
2565
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2566
|
+
}
|
|
2567
|
+
function readOptionalStringField(value, key) {
|
|
2568
|
+
if (!(key in value)) return;
|
|
2569
|
+
return toOptionalString(value[key]);
|
|
2570
|
+
}
|
|
2571
|
+
function stripLeadingBotMention(text, options = {}) {
|
|
2572
|
+
if (!text.trim()) return text;
|
|
2573
|
+
let next = text;
|
|
2574
|
+
if (options.stripLeadingSlackMentionToken) {
|
|
2575
|
+
if (options.botUserId) {
|
|
2576
|
+
const botUserId = escapeRegExp$1(options.botUserId);
|
|
2577
|
+
const mentionByBotUserIdRe = new RegExp(`^\\s*(?:<@${botUserId}(?:\\|[^>]+)?>|@${botUserId})[\\s,:-]*`, "i");
|
|
2578
|
+
next = next.replace(mentionByBotUserIdRe, "").trim();
|
|
2579
|
+
} else next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
|
|
2580
|
+
}
|
|
2581
|
+
const mentionByNameRe = new RegExp(`^\\s*@${escapeRegExp$1(botConfig.userName)}\\b[\\s,:-]*`, "i");
|
|
2582
|
+
next = next.replace(mentionByNameRe, "").trim();
|
|
2583
|
+
const mentionByLabeledEntityRe = new RegExp(`^\\s*<@[^>|]+\\|${escapeRegExp$1(botConfig.userName)}>[\\s,:-]*`, "i");
|
|
2584
|
+
next = next.replace(mentionByLabeledEntityRe, "").trim();
|
|
2585
|
+
return next;
|
|
2586
|
+
}
|
|
2587
|
+
function getThreadId(thread, _message) {
|
|
2588
|
+
return toOptionalString(thread.id);
|
|
2589
|
+
}
|
|
2590
|
+
function getRunId(thread, message) {
|
|
2591
|
+
return readOptionalStringField(thread, "runId") ?? readOptionalStringField(message, "runId");
|
|
2592
|
+
}
|
|
2593
|
+
function getChannelId(thread, message) {
|
|
2594
|
+
return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
|
|
2595
|
+
}
|
|
2596
|
+
function getThreadTs(threadId) {
|
|
2597
|
+
return parseSlackThreadId(threadId)?.threadTs;
|
|
2598
|
+
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Resolve Slack assistant-thread API context for the current turn.
|
|
2601
|
+
*
|
|
2602
|
+
* Slack assistant-thread methods must use the live inbound thread context
|
|
2603
|
+
* Slack provided on the current message. Slack's assistant utilities build
|
|
2604
|
+
* `setStatus`/`setTitle` from `message.channel` plus `message.thread_ts ?? message.ts`
|
|
2605
|
+
* for non-DM message events, while `message.im` still requires an explicit
|
|
2606
|
+
* `thread_ts`. Do not synthesize assistant-thread roots from persisted state.
|
|
2607
|
+
*/
|
|
2608
|
+
function getAssistantThreadContext(message) {
|
|
2609
|
+
const rawContext = readSlackRawMessageContext(message);
|
|
2610
|
+
const channelId = rawContext?.channelId;
|
|
2611
|
+
if (channelId) {
|
|
2612
|
+
const threadTs = isDmChannel(channelId) ? rawContext.threadTs : rawContext.threadTs ?? rawContext.messageTs;
|
|
2613
|
+
if (threadTs) return {
|
|
2614
|
+
channelId,
|
|
2615
|
+
threadTs
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
const parsedThreadId = parseSlackThreadId(readOptionalStringField(message, "threadId"));
|
|
2619
|
+
if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) return;
|
|
2620
|
+
return parsedThreadId;
|
|
2621
|
+
}
|
|
2622
|
+
/** Resolve the native Slack timestamp for a message that can target Slack APIs. */
|
|
2623
|
+
function getMessageTs(message) {
|
|
2624
|
+
const directTs = readOptionalStringField(message, "ts");
|
|
2625
|
+
const parsedDirectTs = parseSlackMessageTs(directTs);
|
|
2626
|
+
if (parsedDirectTs) return parsedDirectTs;
|
|
2627
|
+
const rawContext = readSlackRawMessageContext(message);
|
|
2628
|
+
if (!rawContext) return;
|
|
2629
|
+
const candidates = [rawContext.messageTs, rawContext.nestedMessageTs];
|
|
2630
|
+
for (const candidate of candidates) {
|
|
2631
|
+
const ts = parseSlackMessageTs(candidate);
|
|
2632
|
+
if (ts) return ts;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
//#endregion
|
|
2636
|
+
//#region src/chat/providers/slack/processing-reaction.ts
|
|
2637
|
+
const noProcessingReaction = {
|
|
2638
|
+
complete: async () => void 0,
|
|
2639
|
+
keep: () => void 0,
|
|
2640
|
+
stop: async () => void 0
|
|
2641
|
+
};
|
|
2642
|
+
function isProcessingReactionEmoji(value) {
|
|
2643
|
+
return typeof value === "string" && normalizeSlackEmojiName(value) === getChatConfig().slack.processingReactionEmoji;
|
|
2644
|
+
}
|
|
2645
|
+
/** Return true when a Slack reaction tool invocation keeps the processing reaction. */
|
|
2646
|
+
function shouldKeepProcessingReactionForToolInvocation(input) {
|
|
2647
|
+
return input.toolName === "addReaction" && isProcessingReactionEmoji(input.params.emoji);
|
|
2648
|
+
}
|
|
2649
|
+
/** Start Junior's automatic Slack processing reaction for one inbound message. */
|
|
2650
|
+
async function startProcessingReaction(args) {
|
|
2651
|
+
if (args.message.author.isMe) return noProcessingReaction;
|
|
2652
|
+
const channelId = getChannelId(args.thread, args.message);
|
|
2653
|
+
const messageTs = getMessageTs(args.message);
|
|
2654
|
+
if (!channelId || !messageTs) return noProcessingReaction;
|
|
2655
|
+
return startProcessingReactionForMessage({
|
|
2656
|
+
channelId,
|
|
2657
|
+
timestamp: messageTs
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
/** Start Junior's automatic Slack processing reaction for a known Slack message. */
|
|
2661
|
+
async function startProcessingReactionForMessage(args) {
|
|
2662
|
+
try {
|
|
2663
|
+
await addReactionToMessage({
|
|
2664
|
+
channelId: args.channelId,
|
|
2665
|
+
timestamp: args.timestamp,
|
|
2666
|
+
emoji: getChatConfig().slack.processingReactionEmoji
|
|
2667
|
+
});
|
|
2668
|
+
} catch (error) {
|
|
2669
|
+
logException(error, "slack.processing.reaction_add.failed", {
|
|
2670
|
+
"app.slack.action": "reactions.add",
|
|
2671
|
+
"messaging.message.id": args.timestamp,
|
|
2672
|
+
...getSlackErrorObservabilityAttributes(error)
|
|
2673
|
+
});
|
|
2674
|
+
return noProcessingReaction;
|
|
2675
|
+
}
|
|
2676
|
+
let shouldRemove = true;
|
|
2677
|
+
const removeProcessingReaction = async () => {
|
|
2678
|
+
if (!shouldRemove) return false;
|
|
2679
|
+
try {
|
|
2680
|
+
await removeReactionFromMessage({
|
|
2681
|
+
channelId: args.channelId,
|
|
2682
|
+
timestamp: args.timestamp,
|
|
2683
|
+
emoji: getChatConfig().slack.processingReactionEmoji
|
|
2684
|
+
});
|
|
2685
|
+
return true;
|
|
2686
|
+
} catch (error) {
|
|
2687
|
+
logException(error, "slack.processing.reaction_remove.failed", {
|
|
2688
|
+
"app.slack.action": "reactions.remove",
|
|
2689
|
+
"messaging.message.id": args.timestamp,
|
|
2690
|
+
...getSlackErrorObservabilityAttributes(error)
|
|
2691
|
+
});
|
|
2692
|
+
return false;
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
return {
|
|
2696
|
+
complete: async () => {
|
|
2697
|
+
const shouldAddCompleted = shouldRemove;
|
|
2698
|
+
await removeProcessingReaction();
|
|
2699
|
+
if (!shouldAddCompleted) return;
|
|
2700
|
+
try {
|
|
2701
|
+
await addReactionToMessage({
|
|
2702
|
+
channelId: args.channelId,
|
|
2703
|
+
timestamp: args.timestamp,
|
|
2704
|
+
emoji: getChatConfig().slack.completedReactionEmoji
|
|
2705
|
+
});
|
|
2706
|
+
} catch (error) {
|
|
2707
|
+
logException(error, "slack.processing.reaction_complete.failed", {
|
|
2708
|
+
"app.slack.action": "reactions.add",
|
|
2709
|
+
"messaging.message.id": args.timestamp,
|
|
2710
|
+
...getSlackErrorObservabilityAttributes(error)
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
},
|
|
2714
|
+
keep: () => {
|
|
2715
|
+
shouldRemove = false;
|
|
2716
|
+
},
|
|
2717
|
+
stop: async () => {
|
|
2718
|
+
await removeProcessingReaction();
|
|
2719
|
+
}
|
|
2720
|
+
};
|
|
2721
|
+
}
|
|
2722
|
+
//#endregion
|
|
2414
2723
|
//#region src/chat/slack/message-control.ts
|
|
2415
2724
|
/** Leading Slack mention forms that may precede an explicit steering marker. */
|
|
2416
2725
|
const LEADING_MENTION_BEFORE_STEERING_RE = /^(\s*(?:<@[^>]+>|@[A-Za-z0-9._-]+\b)[\s,:-]*)!!\s*/;
|
|
@@ -5443,6 +5752,616 @@ function readSlackActionToken(message) {
|
|
|
5443
5752
|
return token.success ? token.data : void 0;
|
|
5444
5753
|
}
|
|
5445
5754
|
//#endregion
|
|
5755
|
+
//#region src/chat/slack/cards.ts
|
|
5756
|
+
/** Select the Slack object renderer for a built-in card. */
|
|
5757
|
+
function renderSlackCard(card, conversationId) {
|
|
5758
|
+
switch (card.kind) {
|
|
5759
|
+
case "object": return renderSlackObjectCard(card, conversationId);
|
|
5760
|
+
case "automation": return renderSlackAutomationCard(card);
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
//#endregion
|
|
5764
|
+
//#region src/chat/slack/footer.ts
|
|
5765
|
+
/** Render compact reply attribution for the Slack footer. */
|
|
5766
|
+
function formatReplyAttribution(attribution) {
|
|
5767
|
+
return attribution.detail ? `${attribution.label} · ${attribution.detail}` : attribution.label;
|
|
5768
|
+
}
|
|
5769
|
+
/**
|
|
5770
|
+
* Build the compact conversation footer for visible Slack reply surfaces.
|
|
5771
|
+
*
|
|
5772
|
+
* Detailed turn metrics stay in the dashboard instead of Slack-visible copy.
|
|
5773
|
+
*/
|
|
5774
|
+
function buildSlackReplyFooter(args) {
|
|
5775
|
+
const items = [];
|
|
5776
|
+
const conversationId = args.conversationId?.trim();
|
|
5777
|
+
if (conversationId) {
|
|
5778
|
+
const idItem = {
|
|
5779
|
+
label: "ID",
|
|
5780
|
+
value: conversationId
|
|
5781
|
+
};
|
|
5782
|
+
const conversationUrl = getPluginSlackConversationLink(conversationId)?.url ?? getDashboardConversationLink(conversationId) ?? buildSentryConversationUrl(conversationId);
|
|
5783
|
+
if (conversationUrl) idItem.url = conversationUrl;
|
|
5784
|
+
items.push(idItem);
|
|
5785
|
+
}
|
|
5786
|
+
return items.length > 0 || args.replyAttribution ? {
|
|
5787
|
+
...args.replyAttribution ? { attribution: args.replyAttribution } : void 0,
|
|
5788
|
+
items
|
|
5789
|
+
} : void 0;
|
|
5790
|
+
}
|
|
5791
|
+
/** Build Slack blocks for a reply chunk using the Slack-flavored markdown block for the body. */
|
|
5792
|
+
function buildSlackReplyBlocks(text, footer) {
|
|
5793
|
+
const blocks = text.trim() ? [{
|
|
5794
|
+
type: "markdown",
|
|
5795
|
+
text
|
|
5796
|
+
}] : [];
|
|
5797
|
+
if (footer && (footer.attribution || footer.items.length > 0)) {
|
|
5798
|
+
const attributionElements = footer.attribution ? [{
|
|
5799
|
+
type: "plain_text",
|
|
5800
|
+
text: formatReplyAttribution(footer.attribution)
|
|
5801
|
+
}] : [];
|
|
5802
|
+
blocks.push({
|
|
5803
|
+
type: "context",
|
|
5804
|
+
elements: [...attributionElements, ...footer.items.map((item) => ({
|
|
5805
|
+
type: "mrkdwn",
|
|
5806
|
+
text: item.url ? `*${escapeSlackMrkdwnText(item.label)}:* ${formatSlackLink(item.url, item.value)}` : `*${escapeSlackMrkdwnText(item.label)}:* ${escapeSlackMrkdwnText(item.value)}`
|
|
5807
|
+
}))]
|
|
5808
|
+
});
|
|
5809
|
+
}
|
|
5810
|
+
return blocks.length ? blocks : void 0;
|
|
5811
|
+
}
|
|
5812
|
+
//#endregion
|
|
5813
|
+
//#region src/chat/slack/output.ts
|
|
5814
|
+
const MAX_INLINE_CHARS = 2200;
|
|
5815
|
+
const MAX_INLINE_LINES = 45;
|
|
5816
|
+
const CONTINUED_MARKER = "\n\n[Continued below]";
|
|
5817
|
+
function countSlackLines(text) {
|
|
5818
|
+
if (!text) return 0;
|
|
5819
|
+
return text.split("\n").length;
|
|
5820
|
+
}
|
|
5821
|
+
function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
5822
|
+
return text.length <= maxChars && countSlackLines(text) <= maxLines;
|
|
5823
|
+
}
|
|
5824
|
+
function findSplitIndex(text, maxChars) {
|
|
5825
|
+
if (text.length <= maxChars) return text.length;
|
|
5826
|
+
const bounded = text.slice(0, maxChars);
|
|
5827
|
+
const newlineIndex = bounded.lastIndexOf("\n");
|
|
5828
|
+
if (newlineIndex > 0) return newlineIndex;
|
|
5829
|
+
const spaceIndex = bounded.lastIndexOf(" ");
|
|
5830
|
+
if (spaceIndex > 0) return spaceIndex;
|
|
5831
|
+
return maxChars;
|
|
5832
|
+
}
|
|
5833
|
+
function splitByLineBudget(text, maxLines) {
|
|
5834
|
+
if (maxLines <= 0) return "";
|
|
5835
|
+
const lines = text.split("\n");
|
|
5836
|
+
if (lines.length <= maxLines) return text;
|
|
5837
|
+
return lines.slice(0, maxLines).join("\n");
|
|
5838
|
+
}
|
|
5839
|
+
function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
5840
|
+
return {
|
|
5841
|
+
maxChars: Math.max(1, maxChars - suffix.length),
|
|
5842
|
+
maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
|
|
5843
|
+
};
|
|
5844
|
+
}
|
|
5845
|
+
function forceSplitBudget(text, budget) {
|
|
5846
|
+
const lineCount = countSlackLines(text);
|
|
5847
|
+
return {
|
|
5848
|
+
maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
|
|
5849
|
+
maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
|
|
5850
|
+
};
|
|
5851
|
+
}
|
|
5852
|
+
function getFenceContinuation(text) {
|
|
5853
|
+
let open;
|
|
5854
|
+
for (const line of text.split("\n")) {
|
|
5855
|
+
const trimmed = line.trimStart();
|
|
5856
|
+
const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
|
|
5857
|
+
if (!openerMatch) continue;
|
|
5858
|
+
if (!open) {
|
|
5859
|
+
open = {
|
|
5860
|
+
fence: openerMatch[1],
|
|
5861
|
+
openerLine: trimmed
|
|
5862
|
+
};
|
|
5863
|
+
continue;
|
|
5864
|
+
}
|
|
5865
|
+
if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) open = void 0;
|
|
5866
|
+
}
|
|
5867
|
+
if (!open) return null;
|
|
5868
|
+
return {
|
|
5869
|
+
closeSuffix: text.endsWith("\n") ? open.fence : `\n${open.fence}`,
|
|
5870
|
+
reopenPrefix: `${open.openerLine}\n`
|
|
5871
|
+
};
|
|
5872
|
+
}
|
|
5873
|
+
function appendSlackSuffix(text, marker) {
|
|
5874
|
+
return `${text}${getFenceContinuation(text)?.closeSuffix ?? ""}${marker}`;
|
|
5875
|
+
}
|
|
5876
|
+
function stripTrailingContinuationMarker(text) {
|
|
5877
|
+
return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -19) : text;
|
|
5878
|
+
}
|
|
5879
|
+
function takeSlackContinuationChunk(text, budget) {
|
|
5880
|
+
let { prefix, rest } = takeSlackInlinePrefix(text, budget);
|
|
5881
|
+
if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, budget)));
|
|
5882
|
+
let carryover = rest ? getFenceContinuation(prefix) : null;
|
|
5883
|
+
if (!carryover) return {
|
|
5884
|
+
prefix,
|
|
5885
|
+
rest
|
|
5886
|
+
};
|
|
5887
|
+
const carryoverBudget = reserveInlineBudgetForSuffix(`${carryover.closeSuffix}${CONTINUED_MARKER}`);
|
|
5888
|
+
({prefix, rest} = takeSlackInlinePrefix(text, carryoverBudget));
|
|
5889
|
+
if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, carryoverBudget)));
|
|
5890
|
+
carryover = rest ? getFenceContinuation(prefix) : null;
|
|
5891
|
+
if (!carryover) return {
|
|
5892
|
+
prefix,
|
|
5893
|
+
rest
|
|
5894
|
+
};
|
|
5895
|
+
return {
|
|
5896
|
+
prefix,
|
|
5897
|
+
rest: `${carryover.reopenPrefix}${rest}`
|
|
5898
|
+
};
|
|
5899
|
+
}
|
|
5900
|
+
/** Return the first continuation-safe chunk plus any remaining text. */
|
|
5901
|
+
function takeSlackContinuationPrefix(text, options) {
|
|
5902
|
+
const budget = {
|
|
5903
|
+
maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
|
|
5904
|
+
maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
|
|
5905
|
+
};
|
|
5906
|
+
const { prefix, rest } = (() => {
|
|
5907
|
+
if (options?.forceSplit) return takeSlackContinuationChunk(text, budget);
|
|
5908
|
+
const initial = takeSlackInlinePrefix(text, budget);
|
|
5909
|
+
return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
|
|
5910
|
+
})();
|
|
5911
|
+
return {
|
|
5912
|
+
prefix,
|
|
5913
|
+
renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
|
|
5914
|
+
rest
|
|
5915
|
+
};
|
|
5916
|
+
}
|
|
5917
|
+
/**
|
|
5918
|
+
* Take the largest Slack-safe inline prefix from `text` under the configured
|
|
5919
|
+
* character and line budgets. Returns the consumed prefix plus remaining text.
|
|
5920
|
+
*/
|
|
5921
|
+
function takeSlackInlinePrefix(text, options) {
|
|
5922
|
+
const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
|
|
5923
|
+
const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
|
|
5924
|
+
const normalized = text.replace(/\r\n?/g, "\n");
|
|
5925
|
+
if (!normalized) return {
|
|
5926
|
+
prefix: "",
|
|
5927
|
+
rest: ""
|
|
5928
|
+
};
|
|
5929
|
+
if (fitsInlineBudget(normalized, maxChars, maxLines)) return {
|
|
5930
|
+
prefix: normalized,
|
|
5931
|
+
rest: ""
|
|
5932
|
+
};
|
|
5933
|
+
const lineBounded = splitByLineBudget(normalized, maxLines);
|
|
5934
|
+
const cutIndex = findSplitIndex(lineBounded, maxChars);
|
|
5935
|
+
const prefix = lineBounded.slice(0, cutIndex).trimEnd();
|
|
5936
|
+
if (prefix) return {
|
|
5937
|
+
prefix,
|
|
5938
|
+
rest: normalized.slice(prefix.length).trimStart()
|
|
5939
|
+
};
|
|
5940
|
+
const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
|
|
5941
|
+
return {
|
|
5942
|
+
prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
|
|
5943
|
+
rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
|
|
5944
|
+
};
|
|
5945
|
+
}
|
|
5946
|
+
/**
|
|
5947
|
+
* Split a normalized Slack reply into multiple inline-safe thread messages.
|
|
5948
|
+
*
|
|
5949
|
+
* Replies split across more than two messages receive explicit continuation
|
|
5950
|
+
* markers on non-final chunks. When
|
|
5951
|
+
* `interrupted` is true, the final chunk receives an interruption marker.
|
|
5952
|
+
*/
|
|
5953
|
+
function splitSlackReplyText(text, options) {
|
|
5954
|
+
const normalized = normalizeSlackReplyMarkdown(text);
|
|
5955
|
+
if (!normalized) return [];
|
|
5956
|
+
const chunks = [];
|
|
5957
|
+
const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
5958
|
+
let remaining = normalized;
|
|
5959
|
+
while (remaining) {
|
|
5960
|
+
if (options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining)) {
|
|
5961
|
+
chunks.push(options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining);
|
|
5962
|
+
break;
|
|
5963
|
+
}
|
|
5964
|
+
const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
|
|
5965
|
+
...continuationBudget,
|
|
5966
|
+
forceSplit: true
|
|
5967
|
+
});
|
|
5968
|
+
chunks.push(renderedPrefix);
|
|
5969
|
+
remaining = rest;
|
|
5970
|
+
}
|
|
5971
|
+
if (chunks.length === 2) chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
|
|
5972
|
+
return chunks;
|
|
5973
|
+
}
|
|
5974
|
+
/**
|
|
5975
|
+
* Reserve enough inline budget for a continuation suffix on the current chunk.
|
|
5976
|
+
*/
|
|
5977
|
+
function getSlackContinuationBudget() {
|
|
5978
|
+
return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
5979
|
+
}
|
|
5980
|
+
/** Normalize text for Slack and wrap it as a PostableMessage. */
|
|
5981
|
+
function buildSlackOutputMessage(text) {
|
|
5982
|
+
const normalized = normalizeSlackReplyMarkdown(text);
|
|
5983
|
+
if (!normalized) throw new Error(`Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`);
|
|
5984
|
+
return { markdown: normalized };
|
|
5985
|
+
}
|
|
5986
|
+
//#endregion
|
|
5987
|
+
//#region src/chat/slack/reply.ts
|
|
5988
|
+
/**
|
|
5989
|
+
* Send one destination-visible Slack reply.
|
|
5990
|
+
*
|
|
5991
|
+
* Chunks oversized text, builds compact attribution and conversation footer
|
|
5992
|
+
* context, and posts through the shared Slack outbound boundary.
|
|
5993
|
+
*/
|
|
5994
|
+
async function sendSlackReply(args) {
|
|
5995
|
+
const posts = splitSlackReplyText(args.text).map((text) => ({
|
|
5996
|
+
text,
|
|
5997
|
+
cards: []
|
|
5998
|
+
}));
|
|
5999
|
+
const cards = (args.cards ?? []).map((card) => renderSlackCard(card, args.conversationId));
|
|
6000
|
+
for (let index = 0; index < cards.length; index += 5) {
|
|
6001
|
+
const group = cards.slice(index, index + 5);
|
|
6002
|
+
const lastPost = posts.at(-1);
|
|
6003
|
+
if (index === 0 && lastPost) lastPost.cards = group;
|
|
6004
|
+
else posts.push({
|
|
6005
|
+
text: "",
|
|
6006
|
+
cards: group
|
|
6007
|
+
});
|
|
6008
|
+
}
|
|
6009
|
+
const footer = buildSlackReplyFooter({
|
|
6010
|
+
conversationId: args.conversationId,
|
|
6011
|
+
replyAttribution: args.replyAttribution
|
|
6012
|
+
});
|
|
6013
|
+
const messageTs = [];
|
|
6014
|
+
let threadTs = args.threadTs;
|
|
6015
|
+
for (const [index, post] of posts.entries()) {
|
|
6016
|
+
const { text, cards } = post;
|
|
6017
|
+
const isFinalChunk = index === posts.length - 1;
|
|
6018
|
+
const blocks = buildSlackReplyBlocks(text, void 0) ?? [];
|
|
6019
|
+
for (const card of cards) if (!card.entity) blocks.push({
|
|
6020
|
+
type: "section",
|
|
6021
|
+
text: {
|
|
6022
|
+
type: "mrkdwn",
|
|
6023
|
+
text: card.text
|
|
6024
|
+
}
|
|
6025
|
+
});
|
|
6026
|
+
if (isFinalChunk || !text.trim()) blocks.push(...buildSlackReplyBlocks("", footer) ?? []);
|
|
6027
|
+
const entities = cards.flatMap((card) => card.entity ? [card.entity] : []);
|
|
6028
|
+
const accessibleText = [text, ...cards.map((card) => card.text)].filter(Boolean).join("\n\n");
|
|
6029
|
+
const fallbackText = isFinalChunk && args.replyAttribution ? `${accessibleText}\n\n${escapeSlackMrkdwnText(formatReplyAttribution(args.replyAttribution))}` : accessibleText;
|
|
6030
|
+
const response = await postSlackMessage({
|
|
6031
|
+
channelId: args.channelId,
|
|
6032
|
+
threadTs,
|
|
6033
|
+
text: fallbackText,
|
|
6034
|
+
...blocks.length ? { blocks } : void 0,
|
|
6035
|
+
...entities.length ? { entities } : void 0
|
|
6036
|
+
});
|
|
6037
|
+
if (response.ts) {
|
|
6038
|
+
messageTs.push(response.ts);
|
|
6039
|
+
threadTs ??= response.ts;
|
|
6040
|
+
}
|
|
6041
|
+
}
|
|
6042
|
+
return messageTs;
|
|
6043
|
+
}
|
|
6044
|
+
//#endregion
|
|
6045
|
+
//#region src/chat/slack/assistant-thread/status-render.ts
|
|
6046
|
+
const DEFAULT_STATUS_CONTEXTS = {
|
|
6047
|
+
thinking: "…",
|
|
6048
|
+
searching: "sources",
|
|
6049
|
+
reading: "task",
|
|
6050
|
+
reviewing: "results",
|
|
6051
|
+
drafting: "reply",
|
|
6052
|
+
running: "tasks"
|
|
6053
|
+
};
|
|
6054
|
+
function formatAssistantStatusText(verb, context) {
|
|
6055
|
+
const normalizedVerb = normalizeSlackStatusText(verb).trim().toLowerCase();
|
|
6056
|
+
const normalizedContext = normalizeSlackStatusText(context ?? "") || DEFAULT_STATUS_CONTEXTS[normalizedVerb] || "";
|
|
6057
|
+
if (!normalizedVerb) return truncateStatusText(normalizedContext || "Working");
|
|
6058
|
+
const displayVerb = `${normalizedVerb[0]?.toUpperCase() ?? ""}${normalizedVerb.slice(1)}`;
|
|
6059
|
+
return truncateStatusText(normalizedContext ? `${displayVerb} ${normalizedContext}` : displayVerb);
|
|
6060
|
+
}
|
|
6061
|
+
/** Build assistant progress text from a verb and optional context. */
|
|
6062
|
+
function makeAssistantStatus(verb, context) {
|
|
6063
|
+
return { text: formatAssistantStatusText(verb, context) };
|
|
6064
|
+
}
|
|
6065
|
+
/** Normalize a progress update into the visible Slack loading copy. */
|
|
6066
|
+
function renderAssistantStatus(args) {
|
|
6067
|
+
const visible = truncateStatusText(normalizeSlackStatusText(args.status.text));
|
|
6068
|
+
return {
|
|
6069
|
+
key: visible,
|
|
6070
|
+
visible
|
|
6071
|
+
};
|
|
6072
|
+
}
|
|
6073
|
+
/** Select and normalize the loading messages used for Slack status rotation. */
|
|
6074
|
+
function selectAssistantLoadingMessages(args) {
|
|
6075
|
+
const random = args.random ?? Math.random;
|
|
6076
|
+
const normalized = Array.from(new Set(args.messages.map((message) => truncateStatusText(normalizeSlackStatusText(message))).filter((message) => message.length > 0)));
|
|
6077
|
+
if (normalized.length === 0) return;
|
|
6078
|
+
const shuffled = [...normalized];
|
|
6079
|
+
for (let index = shuffled.length - 1; index > 0; index -= 1) {
|
|
6080
|
+
const otherIndex = Math.floor(random() * (index + 1));
|
|
6081
|
+
[shuffled[index], shuffled[otherIndex]] = [shuffled[otherIndex], shuffled[index]];
|
|
6082
|
+
}
|
|
6083
|
+
return shuffled.slice(0, 10);
|
|
6084
|
+
}
|
|
6085
|
+
//#endregion
|
|
6086
|
+
//#region src/chat/slack/assistant-thread/status-scheduler.ts
|
|
6087
|
+
const STATUS_UPDATE_DEBOUNCE_MS = 1e3;
|
|
6088
|
+
const STATUS_MIN_VISIBLE_MS = 1200;
|
|
6089
|
+
const STATUS_ROTATION_INTERVAL_MS = 3e4;
|
|
6090
|
+
/**
|
|
6091
|
+
* Pace assistant loading-state writes for a single turn.
|
|
6092
|
+
*
|
|
6093
|
+
* This layer owns only local scheduling policy: debounce, minimum visible
|
|
6094
|
+
* duration, refresh cadence, and write ordering. It deals in user-visible
|
|
6095
|
+
* progress copy; the transport decides how that maps onto Slack's `status`
|
|
6096
|
+
* versus `loading_messages` fields.
|
|
6097
|
+
*/
|
|
6098
|
+
function createAssistantStatusScheduler(args) {
|
|
6099
|
+
const now = args.now ?? (() => Date.now());
|
|
6100
|
+
const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
6101
|
+
const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
|
|
6102
|
+
const random = args.random ?? Math.random;
|
|
6103
|
+
const loadingMessages = selectAssistantLoadingMessages({
|
|
6104
|
+
messages: args.loadingMessages ?? [],
|
|
6105
|
+
random
|
|
6106
|
+
});
|
|
6107
|
+
const defaultStatus = makeAssistantStatus("thinking");
|
|
6108
|
+
let active = false;
|
|
6109
|
+
let currentKey = "";
|
|
6110
|
+
let currentVisibleStatus = "";
|
|
6111
|
+
let currentLoadingMessages;
|
|
6112
|
+
let lastStatusAt = 0;
|
|
6113
|
+
let pendingStatus = null;
|
|
6114
|
+
let pendingKey = "";
|
|
6115
|
+
let pendingTimer = null;
|
|
6116
|
+
let rotationTimer = null;
|
|
6117
|
+
let inflightStatusUpdate = Promise.resolve();
|
|
6118
|
+
const enqueueStatusUpdate = (task) => {
|
|
6119
|
+
const request = inflightStatusUpdate.catch(() => void 0).then(async () => {
|
|
6120
|
+
await task();
|
|
6121
|
+
});
|
|
6122
|
+
inflightStatusUpdate = request.catch(() => void 0);
|
|
6123
|
+
return request;
|
|
6124
|
+
};
|
|
6125
|
+
const scheduleRotation = () => {
|
|
6126
|
+
if (rotationTimer) {
|
|
6127
|
+
clearTimer(rotationTimer);
|
|
6128
|
+
rotationTimer = null;
|
|
6129
|
+
}
|
|
6130
|
+
if (!active || !currentVisibleStatus) return;
|
|
6131
|
+
rotationTimer = setTimer(() => {
|
|
6132
|
+
rotationTimer = null;
|
|
6133
|
+
if (!active || !currentVisibleStatus) return;
|
|
6134
|
+
postStatus(currentVisibleStatus, currentLoadingMessages);
|
|
6135
|
+
}, STATUS_ROTATION_INTERVAL_MS);
|
|
6136
|
+
};
|
|
6137
|
+
const getLoadingMessagesForVisibleStatus = (visible) => visible ? [visible] : void 0;
|
|
6138
|
+
const getInitialStatusText = () => {
|
|
6139
|
+
if (loadingMessages?.length) return loadingMessages[0];
|
|
6140
|
+
return defaultStatus.text;
|
|
6141
|
+
};
|
|
6142
|
+
const haveSameLoadingMessages = (left, right) => {
|
|
6143
|
+
if (left === right) return true;
|
|
6144
|
+
if (!left || !right || left.length !== right.length) return false;
|
|
6145
|
+
return left.every((message, index) => message === right[index]);
|
|
6146
|
+
};
|
|
6147
|
+
const postStatus = async (text, nextLoadingMessages) => {
|
|
6148
|
+
if (!text && !currentVisibleStatus) return;
|
|
6149
|
+
currentVisibleStatus = text;
|
|
6150
|
+
currentLoadingMessages = nextLoadingMessages;
|
|
6151
|
+
lastStatusAt = now();
|
|
6152
|
+
scheduleRotation();
|
|
6153
|
+
await enqueueStatusUpdate(async () => {
|
|
6154
|
+
await args.sendStatus(text, nextLoadingMessages);
|
|
6155
|
+
});
|
|
6156
|
+
};
|
|
6157
|
+
const postRenderedStatus = async (status) => {
|
|
6158
|
+
const presentation = renderAssistantStatus({ status });
|
|
6159
|
+
const nextLoadingMessages = getLoadingMessagesForVisibleStatus(presentation.visible);
|
|
6160
|
+
currentKey = presentation.key;
|
|
6161
|
+
await postStatus(presentation.visible, nextLoadingMessages);
|
|
6162
|
+
};
|
|
6163
|
+
const clearPending = () => {
|
|
6164
|
+
if (pendingTimer) {
|
|
6165
|
+
clearTimer(pendingTimer);
|
|
6166
|
+
pendingTimer = null;
|
|
6167
|
+
}
|
|
6168
|
+
pendingStatus = null;
|
|
6169
|
+
pendingKey = "";
|
|
6170
|
+
};
|
|
6171
|
+
const flushPending = async () => {
|
|
6172
|
+
if (!active || !pendingStatus) {
|
|
6173
|
+
clearPending();
|
|
6174
|
+
return;
|
|
6175
|
+
}
|
|
6176
|
+
const next = pendingStatus;
|
|
6177
|
+
clearPending();
|
|
6178
|
+
if (renderAssistantStatus({ status: next }).key !== currentKey) await postRenderedStatus(next);
|
|
6179
|
+
};
|
|
6180
|
+
return {
|
|
6181
|
+
async clear() {
|
|
6182
|
+
active = false;
|
|
6183
|
+
clearPending();
|
|
6184
|
+
if (rotationTimer) {
|
|
6185
|
+
clearTimer(rotationTimer);
|
|
6186
|
+
rotationTimer = null;
|
|
6187
|
+
}
|
|
6188
|
+
currentKey = "";
|
|
6189
|
+
await postStatus("");
|
|
6190
|
+
},
|
|
6191
|
+
update(status) {
|
|
6192
|
+
if (!active) {
|
|
6193
|
+
active = true;
|
|
6194
|
+
clearPending();
|
|
6195
|
+
if (status) {
|
|
6196
|
+
postRenderedStatus(status);
|
|
6197
|
+
return;
|
|
6198
|
+
}
|
|
6199
|
+
currentKey = "initial";
|
|
6200
|
+
postStatus(getInitialStatusText(), loadingMessages);
|
|
6201
|
+
return;
|
|
6202
|
+
}
|
|
6203
|
+
if (!status) {
|
|
6204
|
+
clearPending();
|
|
6205
|
+
currentKey = "initial";
|
|
6206
|
+
postStatus(getInitialStatusText(), loadingMessages);
|
|
6207
|
+
return;
|
|
6208
|
+
}
|
|
6209
|
+
const presentation = renderAssistantStatus({ status });
|
|
6210
|
+
if (!presentation.visible) return;
|
|
6211
|
+
if (presentation.key === currentKey || presentation.key === pendingKey) return;
|
|
6212
|
+
if (presentation.visible === currentVisibleStatus) {
|
|
6213
|
+
clearPending();
|
|
6214
|
+
currentKey = presentation.key;
|
|
6215
|
+
const nextLoadingMessages = getLoadingMessagesForVisibleStatus(presentation.visible);
|
|
6216
|
+
if (!haveSameLoadingMessages(currentLoadingMessages, nextLoadingMessages)) postStatus(presentation.visible, nextLoadingMessages);
|
|
6217
|
+
return;
|
|
6218
|
+
}
|
|
6219
|
+
const elapsed = now() - lastStatusAt;
|
|
6220
|
+
const waitMs = Math.max(STATUS_UPDATE_DEBOUNCE_MS - elapsed, STATUS_MIN_VISIBLE_MS - elapsed, 0);
|
|
6221
|
+
if (waitMs <= 0) {
|
|
6222
|
+
clearPending();
|
|
6223
|
+
postRenderedStatus(status);
|
|
6224
|
+
return;
|
|
6225
|
+
}
|
|
6226
|
+
pendingStatus = status;
|
|
6227
|
+
pendingKey = presentation.key;
|
|
6228
|
+
if (pendingTimer) return;
|
|
6229
|
+
pendingTimer = setTimer(() => {
|
|
6230
|
+
pendingTimer = null;
|
|
6231
|
+
flushPending();
|
|
6232
|
+
}, Math.max(1, waitMs));
|
|
6233
|
+
}
|
|
6234
|
+
};
|
|
6235
|
+
}
|
|
6236
|
+
//#endregion
|
|
6237
|
+
//#region src/chat/slack/assistant-thread/status-send.ts
|
|
6238
|
+
/**
|
|
6239
|
+
* Slack's assistant loading UI accepts both `status` and `loading_messages`,
|
|
6240
|
+
* but product policy keeps `status` stable and generic. User-visible progress
|
|
6241
|
+
* copy belongs in `loading_messages`.
|
|
6242
|
+
*/
|
|
6243
|
+
const SLACK_ASSISTANT_ACTIVE_STATUS = "is working on your request...";
|
|
6244
|
+
/**
|
|
6245
|
+
* Build a best-effort sender for Slack's assistant loading state.
|
|
6246
|
+
*
|
|
6247
|
+
* The `text` argument is internal progress copy. This transport maps it onto
|
|
6248
|
+
* Slack's loading surface and keeps the raw Slack `status` field fixed.
|
|
6249
|
+
*/
|
|
6250
|
+
function createSlackAdapterStatusSender(args) {
|
|
6251
|
+
const adapter = args.getSlackAdapter();
|
|
6252
|
+
const boundToken = getSlackAdapterRequestToken(adapter);
|
|
6253
|
+
return async (text, loadingMessages) => {
|
|
6254
|
+
const channelId = args.channelId;
|
|
6255
|
+
const threadTs = args.threadTs;
|
|
6256
|
+
if (!channelId || !threadTs) return;
|
|
6257
|
+
const normalizedChannelId = normalizeSlackConversationId(channelId);
|
|
6258
|
+
if (!normalizedChannelId) return;
|
|
6259
|
+
const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
|
|
6260
|
+
try {
|
|
6261
|
+
await runWithBoundSlackToken(adapter, boundToken, () => adapter.setAssistantStatus(normalizedChannelId, threadTs, text ? SLACK_ASSISTANT_ACTIVE_STATUS : "", nextLoadingMessages));
|
|
6262
|
+
} catch (error) {
|
|
6263
|
+
logAssistantStatusFailure({
|
|
6264
|
+
status: text,
|
|
6265
|
+
error,
|
|
6266
|
+
channelId,
|
|
6267
|
+
normalizedChannelId,
|
|
6268
|
+
threadTs
|
|
6269
|
+
});
|
|
6270
|
+
}
|
|
6271
|
+
};
|
|
6272
|
+
}
|
|
6273
|
+
/**
|
|
6274
|
+
* Build a best-effort sender for Slack's assistant loading state over raw Web
|
|
6275
|
+
* API calls. As with the adapter-backed path, the dynamic copy goes to
|
|
6276
|
+
* `loading_messages` while `status` stays fixed and generic.
|
|
6277
|
+
*/
|
|
6278
|
+
function createSlackWebApiStatusSender(args) {
|
|
6279
|
+
const getClient = args.getSlackClient ?? getSlackClient;
|
|
6280
|
+
return async (text, loadingMessages) => {
|
|
6281
|
+
const channelId = args.channelId;
|
|
6282
|
+
const threadTs = args.threadTs;
|
|
6283
|
+
if (!channelId || !threadTs) return;
|
|
6284
|
+
const normalizedChannelId = normalizeSlackConversationId(channelId);
|
|
6285
|
+
if (!normalizedChannelId) return;
|
|
6286
|
+
const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
|
|
6287
|
+
try {
|
|
6288
|
+
await getClient().assistant.threads.setStatus({
|
|
6289
|
+
channel_id: normalizedChannelId,
|
|
6290
|
+
thread_ts: threadTs,
|
|
6291
|
+
status: text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
|
|
6292
|
+
...nextLoadingMessages ? { loading_messages: nextLoadingMessages } : void 0
|
|
6293
|
+
});
|
|
6294
|
+
} catch (error) {
|
|
6295
|
+
logAssistantStatusFailure({
|
|
6296
|
+
status: text,
|
|
6297
|
+
error,
|
|
6298
|
+
channelId,
|
|
6299
|
+
normalizedChannelId,
|
|
6300
|
+
threadTs
|
|
6301
|
+
});
|
|
6302
|
+
}
|
|
6303
|
+
};
|
|
6304
|
+
}
|
|
6305
|
+
function getSlackAdapterRequestToken(adapter) {
|
|
6306
|
+
const token = adapter.requestContext?.getStore()?.token;
|
|
6307
|
+
if (typeof token !== "string") return;
|
|
6308
|
+
return token.trim() || void 0;
|
|
6309
|
+
}
|
|
6310
|
+
async function runWithBoundSlackToken(adapter, token, task) {
|
|
6311
|
+
if (!token) return await task();
|
|
6312
|
+
return await adapter.withBotToken(token, task);
|
|
6313
|
+
}
|
|
6314
|
+
function logAssistantStatusFailure(args) {
|
|
6315
|
+
logWarn("assistant.status.update.failed", {
|
|
6316
|
+
"app.slack.status_text": args.status || "(clear)",
|
|
6317
|
+
"app.slack.channel_id_raw": args.channelId,
|
|
6318
|
+
"app.slack.channel_id": args.normalizedChannelId,
|
|
6319
|
+
"app.slack.thread_ts": args.threadTs,
|
|
6320
|
+
"exception.message": args.error instanceof Error ? args.error.message : String(args.error)
|
|
6321
|
+
});
|
|
6322
|
+
}
|
|
6323
|
+
//#endregion
|
|
6324
|
+
//#region src/chat/slack/assistant-thread/status.ts
|
|
6325
|
+
/**
|
|
6326
|
+
* Create a Slack adapter-backed session for Slack's assistant loading state.
|
|
6327
|
+
*
|
|
6328
|
+
* The session accepts internal progress updates and leaves it to the sender to
|
|
6329
|
+
* map them onto Slack's fixed generic `status` field plus dynamic
|
|
6330
|
+
* `loading_messages`.
|
|
6331
|
+
*/
|
|
6332
|
+
function createSlackAdapterAssistantStatusSession(args) {
|
|
6333
|
+
return createAssistantStatusScheduler({
|
|
6334
|
+
sendStatus: createSlackAdapterStatusSender({
|
|
6335
|
+
channelId: args.channelId,
|
|
6336
|
+
threadTs: args.threadTs,
|
|
6337
|
+
getSlackAdapter: args.getSlackAdapter
|
|
6338
|
+
}),
|
|
6339
|
+
loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
|
|
6340
|
+
now: args.now,
|
|
6341
|
+
setTimer: args.setTimer,
|
|
6342
|
+
clearTimer: args.clearTimer,
|
|
6343
|
+
random: args.random
|
|
6344
|
+
});
|
|
6345
|
+
}
|
|
6346
|
+
/**
|
|
6347
|
+
* Create a Web API-backed session for Slack's assistant loading state in
|
|
6348
|
+
* resume/callback flows that do not use the adapter thread object.
|
|
6349
|
+
*/
|
|
6350
|
+
function createSlackWebApiAssistantStatusSession(args) {
|
|
6351
|
+
return createAssistantStatusScheduler({
|
|
6352
|
+
sendStatus: createSlackWebApiStatusSender({
|
|
6353
|
+
channelId: args.channelId,
|
|
6354
|
+
threadTs: args.threadTs,
|
|
6355
|
+
getSlackClient: args.getSlackClient
|
|
6356
|
+
}),
|
|
6357
|
+
loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
|
|
6358
|
+
now: args.now,
|
|
6359
|
+
setTimer: args.setTimer,
|
|
6360
|
+
clearTimer: args.clearTimer,
|
|
6361
|
+
random: args.random
|
|
6362
|
+
});
|
|
6363
|
+
}
|
|
6364
|
+
//#endregion
|
|
5446
6365
|
//#region src/chat/slack/assistant-thread/title.ts
|
|
5447
6366
|
/** Set-once DM title projection for the current process. */
|
|
5448
6367
|
const projectedAssistantTitles = /* @__PURE__ */ new Set();
|
|
@@ -5481,6 +6400,21 @@ async function maybeSyncAssistantTitle(args) {
|
|
|
5481
6400
|
}
|
|
5482
6401
|
}
|
|
5483
6402
|
//#endregion
|
|
6403
|
+
//#region src/chat/services/auth-pause-response.ts
|
|
6404
|
+
const MAX_AUTH_REQUEST_LENGTH = 200;
|
|
6405
|
+
function formatAuthRequest(requestText) {
|
|
6406
|
+
const normalized = requestText.replace(/\s+/g, " ").trim();
|
|
6407
|
+
if (!normalized) return;
|
|
6408
|
+
return (normalized.length > MAX_AUTH_REQUEST_LENGTH ? `${normalized.slice(0, 199).trimEnd()}…` : normalized).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
6409
|
+
}
|
|
6410
|
+
/** Build the visible Slack thread note for an auth-paused turn. */
|
|
6411
|
+
function buildAuthPauseResponse(slackUserId, providerDisplayName, requestText) {
|
|
6412
|
+
const mention = slackUserId ? `<@${slackUserId}> ` : "";
|
|
6413
|
+
const request = requestText ? formatAuthRequest(requestText) : void 0;
|
|
6414
|
+
if (!request) return `${mention}I'll need you to authorize ${providerDisplayName}. I sent you a link.`;
|
|
6415
|
+
return `${mention}I need access to ${providerDisplayName} to continue.\n\n*Why:* ${request}\n\nI sent you a link.`;
|
|
6416
|
+
}
|
|
6417
|
+
//#endregion
|
|
5484
6418
|
//#region src/chat/services/provider-default-config.ts
|
|
5485
6419
|
const GITHUB_REPO_PART = String.raw`[A-Za-z0-9_.-]*[A-Za-z0-9_-]`;
|
|
5486
6420
|
const GITHUB_REPO_RE = new RegExp(String.raw`^\s*(?:set|use)\s+(?:the\s+)?default\s+(?:github\s+)?repo(?:sitory)?\s+(?:to|as)\s+(${GITHUB_REPO_PART}/${GITHUB_REPO_PART})(?:\s+for\s+this\s+channel)?[.!?]?\s*$`, "i");
|
|
@@ -6930,6 +7864,506 @@ function createJuniorSlackAdapter(config) {
|
|
|
6930
7864
|
return createSlackAdapter(config);
|
|
6931
7865
|
}
|
|
6932
7866
|
//#endregion
|
|
7867
|
+
//#region src/chat/providers/slack/resume.ts
|
|
7868
|
+
/**
|
|
7869
|
+
* Resume a Slack Turn from saved Run state.
|
|
7870
|
+
*
|
|
7871
|
+
* Resumed Turns load their saved Run while holding the Slack thread lock.
|
|
7872
|
+
* Status notices are best effort. Completed assistant replies and
|
|
7873
|
+
* authorization pause notices use `sendSlackReply` so the footer stays
|
|
7874
|
+
* consistent.
|
|
7875
|
+
*/
|
|
7876
|
+
function resolveReplyTimeoutMs() {
|
|
7877
|
+
const raw = process.env.EVAL_AGENT_REPLY_TIMEOUT_MS?.trim();
|
|
7878
|
+
if (!raw) return;
|
|
7879
|
+
const parsed = Number.parseInt(raw, 10);
|
|
7880
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
7881
|
+
}
|
|
7882
|
+
async function postSlackMessageBestEffort(channelId, threadTs, text, conversationId, replyAttribution) {
|
|
7883
|
+
try {
|
|
7884
|
+
if (conversationId) {
|
|
7885
|
+
await sendSlackReply({
|
|
7886
|
+
channelId,
|
|
7887
|
+
conversationId,
|
|
7888
|
+
replyAttribution,
|
|
7889
|
+
text,
|
|
7890
|
+
threadTs
|
|
7891
|
+
});
|
|
7892
|
+
return;
|
|
7893
|
+
}
|
|
7894
|
+
await postSlackMessage({
|
|
7895
|
+
channelId,
|
|
7896
|
+
threadTs,
|
|
7897
|
+
text
|
|
7898
|
+
});
|
|
7899
|
+
} catch {}
|
|
7900
|
+
}
|
|
7901
|
+
/** Create a read-only configuration service from persisted values. */
|
|
7902
|
+
function createReadOnlyConfigService(rawValues) {
|
|
7903
|
+
const values = Object.fromEntries(Object.entries(rawValues).map(([key, value]) => [key, configValueSchema.parse(value)]));
|
|
7904
|
+
const entries = Object.entries(values).map(([key, value]) => ({
|
|
7905
|
+
key,
|
|
7906
|
+
value,
|
|
7907
|
+
scope: "location",
|
|
7908
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7909
|
+
}));
|
|
7910
|
+
return {
|
|
7911
|
+
get: async (key) => entries.find((entry) => entry.key === key),
|
|
7912
|
+
set: async () => {
|
|
7913
|
+
throw new Error("Configuration is read-only for a resumed Run");
|
|
7914
|
+
},
|
|
7915
|
+
unset: async () => false,
|
|
7916
|
+
list: async ({ prefix } = {}) => entries.filter((entry) => !prefix || entry.key.startsWith(prefix)),
|
|
7917
|
+
resolve: async (key) => values[key],
|
|
7918
|
+
resolveValues: async ({ keys, prefix } = {}) => {
|
|
7919
|
+
const filtered = {};
|
|
7920
|
+
for (const [key, value] of Object.entries(values)) {
|
|
7921
|
+
if (prefix && !key.startsWith(prefix)) continue;
|
|
7922
|
+
if (keys && !keys.includes(key)) continue;
|
|
7923
|
+
filtered[key] = value;
|
|
7924
|
+
}
|
|
7925
|
+
return filtered;
|
|
7926
|
+
}
|
|
7927
|
+
};
|
|
7928
|
+
}
|
|
7929
|
+
function getResumeLogContext(args) {
|
|
7930
|
+
const actor = args.run?.actor;
|
|
7931
|
+
return {
|
|
7932
|
+
conversationId: args.conversationId,
|
|
7933
|
+
messageConversationId: args.conversationId,
|
|
7934
|
+
userId: isUserActor(actor) ? actor.userId : void 0,
|
|
7935
|
+
userName: isUserActor(actor) ? actor.userName : void 0,
|
|
7936
|
+
destinationName: args.channelId,
|
|
7937
|
+
assistantUserName: botConfig.userName,
|
|
7938
|
+
modelId: defaultModelId(botConfig)
|
|
7939
|
+
};
|
|
7940
|
+
}
|
|
7941
|
+
async function postResumeFailureReply(args) {
|
|
7942
|
+
await postSlackMessage({
|
|
7943
|
+
channelId: args.channelId,
|
|
7944
|
+
threadTs: args.threadTs,
|
|
7945
|
+
text: buildTurnErrorResponse(args.error, args.eventId)
|
|
7946
|
+
});
|
|
7947
|
+
}
|
|
7948
|
+
async function handleResumeFailure(args) {
|
|
7949
|
+
const turnLifecycle = getTurnLifecycle();
|
|
7950
|
+
const capturedEventId = logException(args.error, args.eventName);
|
|
7951
|
+
const eventId = requireTurnFailureEventId(capturedEventId, args.eventName);
|
|
7952
|
+
let failureStatePersistError;
|
|
7953
|
+
try {
|
|
7954
|
+
await args.resume.onFailure?.(args.error);
|
|
7955
|
+
} catch (persistError) {
|
|
7956
|
+
const persistEventId = logException(persistError, "slack.resume.failure_state_persist.failed", { "app.error.original_event_id": eventId });
|
|
7957
|
+
try {
|
|
7958
|
+
await turnLifecycle.fail({
|
|
7959
|
+
conversationId: args.resume.conversationId,
|
|
7960
|
+
turnId: args.resume.turnId,
|
|
7961
|
+
createdAtMs: Date.now(),
|
|
7962
|
+
failureCode: "persistence_failed",
|
|
7963
|
+
...persistEventId ? { eventId: persistEventId } : void 0
|
|
7964
|
+
});
|
|
7965
|
+
} catch (lifecycleError) {
|
|
7966
|
+
logException(lifecycleError, "slack.resume.failure_lifecycle_persist.failed", { "app.error.original_event_id": eventId });
|
|
7967
|
+
}
|
|
7968
|
+
failureStatePersistError = persistError;
|
|
7969
|
+
}
|
|
7970
|
+
try {
|
|
7971
|
+
await postResumeFailureReply({
|
|
7972
|
+
channelId: args.resume.channelId,
|
|
7973
|
+
threadTs: args.resume.threadTs,
|
|
7974
|
+
eventId,
|
|
7975
|
+
error: args.error
|
|
7976
|
+
});
|
|
7977
|
+
} catch (deliveryError) {
|
|
7978
|
+
const deliveryEventId = logException(deliveryError, "slack.resume.failure_delivery.failed", { "app.error.original_event_id": eventId });
|
|
7979
|
+
try {
|
|
7980
|
+
await turnLifecycle.fail({
|
|
7981
|
+
conversationId: args.resume.conversationId,
|
|
7982
|
+
turnId: args.resume.turnId,
|
|
7983
|
+
createdAtMs: Date.now(),
|
|
7984
|
+
failureCode: "delivery_failed",
|
|
7985
|
+
...deliveryEventId ? { eventId: deliveryEventId } : void 0
|
|
7986
|
+
});
|
|
7987
|
+
} catch (lifecycleError) {
|
|
7988
|
+
logException(lifecycleError, "slack.resume.failure_lifecycle_persist.failed", { "app.error.original_event_id": eventId });
|
|
7989
|
+
}
|
|
7990
|
+
throw deliveryError;
|
|
7991
|
+
}
|
|
7992
|
+
if (failureStatePersistError) throw failureStatePersistError;
|
|
7993
|
+
await turnLifecycle.fail({
|
|
7994
|
+
conversationId: args.resume.conversationId,
|
|
7995
|
+
turnId: args.resume.turnId,
|
|
7996
|
+
createdAtMs: Date.now(),
|
|
7997
|
+
failureCode: args.failureCode,
|
|
7998
|
+
eventId
|
|
7999
|
+
});
|
|
8000
|
+
}
|
|
8001
|
+
function buildResumedRun(args, statusSession, delivery) {
|
|
8002
|
+
const savedRun = args.run;
|
|
8003
|
+
if (!savedRun) throw new TypeError("Slack resume requires a saved Run");
|
|
8004
|
+
if (!savedRun.source) throw new TypeError("Slack resume requires a Run Source");
|
|
8005
|
+
const source = savedRun.source;
|
|
8006
|
+
if (savedRun.destination.platform !== "slack") throw new TypeError("Slack resume requires a Slack Destination");
|
|
8007
|
+
const requestDeadline = getTurnRequestDeadline();
|
|
8008
|
+
const threadId = args.conversationId;
|
|
8009
|
+
const persistedLocationConfiguration = savedRun.environment?.locationConfiguration ?? (savedRun.environment?.configuration ? createReadOnlyConfigService(savedRun.environment.configuration) : void 0);
|
|
8010
|
+
const priorOnEvent = savedRun.onEvent;
|
|
8011
|
+
return {
|
|
8012
|
+
...savedRun,
|
|
8013
|
+
conversationId: args.conversationId,
|
|
8014
|
+
turnId: args.turnId,
|
|
8015
|
+
instruction: {
|
|
8016
|
+
...savedRun.instruction ?? { text: args.messageText },
|
|
8017
|
+
text: args.messageText
|
|
8018
|
+
},
|
|
8019
|
+
source: source.kind === "slack" ? {
|
|
8020
|
+
...source,
|
|
8021
|
+
channelId: args.channelId,
|
|
8022
|
+
...args.threadTs ? { threadTs: args.threadTs } : void 0
|
|
8023
|
+
} : source,
|
|
8024
|
+
deadlineAtMs: savedRun.deadlineAtMs ?? requestDeadline?.deadlineAtMs,
|
|
8025
|
+
environment: {
|
|
8026
|
+
...savedRun.environment,
|
|
8027
|
+
locationConfiguration: persistedLocationConfiguration
|
|
8028
|
+
},
|
|
8029
|
+
onEvent: async (event) => {
|
|
8030
|
+
if (event.type === "status") statusSession.update({ text: event.text });
|
|
8031
|
+
await priorOnEvent?.(event);
|
|
8032
|
+
},
|
|
8033
|
+
...savedRun.dispatch?.outcomes?.length === 0 ? void 0 : { delivery },
|
|
8034
|
+
durability: {
|
|
8035
|
+
...savedRun.durability,
|
|
8036
|
+
onSandboxRefChanged: async (sandboxRef) => {
|
|
8037
|
+
await persistThreadStateById(threadId, { sandboxRef });
|
|
8038
|
+
await savedRun.durability?.onSandboxRefChanged?.(sandboxRef);
|
|
8039
|
+
}
|
|
8040
|
+
}
|
|
8041
|
+
};
|
|
8042
|
+
}
|
|
8043
|
+
/**
|
|
8044
|
+
* Resume a paused Slack Turn.
|
|
8045
|
+
*
|
|
8046
|
+
* The caller must hold the conversation work lease. This function does not
|
|
8047
|
+
* acquire another execution lock. A started resume owns its completion work.
|
|
8048
|
+
* Returns false only when `beforeStart` proves the resume is stale before
|
|
8049
|
+
* generation begins.
|
|
8050
|
+
*/
|
|
8051
|
+
async function resumeSlackTurn(args) {
|
|
8052
|
+
return withLogContext(getResumeLogContext(args), () => resumeSlackTurnInContext(args));
|
|
8053
|
+
}
|
|
8054
|
+
async function resumeSlackTurnInContext(args) {
|
|
8055
|
+
const status = createSlackWebApiAssistantStatusSession({
|
|
8056
|
+
channelId: args.channelId,
|
|
8057
|
+
threadTs: args.threadTs
|
|
8058
|
+
});
|
|
8059
|
+
let processingReaction;
|
|
8060
|
+
let deferredAuthInfo;
|
|
8061
|
+
let deferredPauseHandler;
|
|
8062
|
+
let deferredFailureHandler;
|
|
8063
|
+
let runResultHandled = false;
|
|
8064
|
+
let assistantMessageDelivered = false;
|
|
8065
|
+
let shouldScheduleCompletedPluginTasks = false;
|
|
8066
|
+
let postDeliveryCommitError;
|
|
8067
|
+
const turnLifecycle = getTurnLifecycle();
|
|
8068
|
+
let failureCode = "agent_run_failed";
|
|
8069
|
+
let runArgs = args;
|
|
8070
|
+
try {
|
|
8071
|
+
const preparedArgs = await args.beforeStart?.();
|
|
8072
|
+
if (preparedArgs === false) return false;
|
|
8073
|
+
if (preparedArgs) runArgs = {
|
|
8074
|
+
...args,
|
|
8075
|
+
...preparedArgs
|
|
8076
|
+
};
|
|
8077
|
+
setTags(getResumeLogContext(runArgs));
|
|
8078
|
+
const savedRun = runArgs.run;
|
|
8079
|
+
if (!savedRun) throw new Error("Resumed Turn requires a saved Run");
|
|
8080
|
+
const credentialContext = savedRun.credentialContext;
|
|
8081
|
+
if (!credentialContext) throw new Error("Resumed Run requires a credential context");
|
|
8082
|
+
const routingActor = savedRun.actor;
|
|
8083
|
+
let resumeActor;
|
|
8084
|
+
if ("type" in credentialContext.actor) {
|
|
8085
|
+
if (!isUserActor(routingActor) || credentialContext.actor.userId !== routingActor.userId) throw new Error("Resumed Run credential actor must match its Actor");
|
|
8086
|
+
resumeActor = routingActor;
|
|
8087
|
+
} else {
|
|
8088
|
+
if (routingActor && (routingActor.platform !== "system" || routingActor.name !== credentialContext.actor.name)) throw new Error("Resumed Run system credential actor must match its Actor");
|
|
8089
|
+
resumeActor = credentialContext.actor;
|
|
8090
|
+
}
|
|
8091
|
+
if (runArgs.messageTs) processingReaction = await startProcessingReactionForMessage({
|
|
8092
|
+
channelId: runArgs.channelId,
|
|
8093
|
+
timestamp: runArgs.messageTs
|
|
8094
|
+
});
|
|
8095
|
+
if (runArgs.initialText) await postSlackMessageBestEffort(runArgs.channelId, runArgs.threadTs, runArgs.initialText);
|
|
8096
|
+
status.update(runArgs.initialStatus);
|
|
8097
|
+
const conversationId = runArgs.conversationId;
|
|
8098
|
+
const visibility = (await getConversationStore().get({ conversationId }))?.visibility;
|
|
8099
|
+
const visibleConversationId = args.conversationId;
|
|
8100
|
+
const sessionId = runArgs.turnId;
|
|
8101
|
+
let deliveryConversation;
|
|
8102
|
+
let acceptedDeliveryId;
|
|
8103
|
+
/** Load the resumed conversation once for ordered delivery recording. */
|
|
8104
|
+
const getDeliveryConversation = async () => {
|
|
8105
|
+
if (deliveryConversation) {
|
|
8106
|
+
const userMessageId = getTurnUserMessage(deliveryConversation, sessionId)?.id;
|
|
8107
|
+
return {
|
|
8108
|
+
conversation: deliveryConversation,
|
|
8109
|
+
sessionId,
|
|
8110
|
+
...userMessageId ? { userMessageId } : void 0
|
|
8111
|
+
};
|
|
8112
|
+
}
|
|
8113
|
+
const persistedState = await getPersistedThreadState(visibleConversationId);
|
|
8114
|
+
const conversation = coerceThreadConversationState(persistedState);
|
|
8115
|
+
await hydrateConversationMessages({
|
|
8116
|
+
conversation,
|
|
8117
|
+
conversationId
|
|
8118
|
+
});
|
|
8119
|
+
deliveryConversation = conversation;
|
|
8120
|
+
const userMessageId = getTurnUserMessage(conversation, sessionId)?.id;
|
|
8121
|
+
return {
|
|
8122
|
+
conversation,
|
|
8123
|
+
sessionId,
|
|
8124
|
+
...userMessageId ? { userMessageId } : void 0
|
|
8125
|
+
};
|
|
8126
|
+
};
|
|
8127
|
+
/** Post and record one completed assistant message for the resumed turn. */
|
|
8128
|
+
const deliverAssistantMessage = async (reply) => {
|
|
8129
|
+
const message = typeof reply === "string" ? void 0 : reply;
|
|
8130
|
+
const text = typeof reply === "string" ? reply : getAssistantReplyText(reply);
|
|
8131
|
+
if (!text?.trim()) return;
|
|
8132
|
+
failureCode = "delivery_failed";
|
|
8133
|
+
const cards = await loadPendingMessageCards(conversationId);
|
|
8134
|
+
const deliveryState = await getDeliveryConversation();
|
|
8135
|
+
let slackMessageTs = [];
|
|
8136
|
+
try {
|
|
8137
|
+
const outcomes = runArgs.run?.dispatch?.outcomes;
|
|
8138
|
+
if (outcomes) for (const outcome of outcomes) slackMessageTs.push(...await sendSlackReply({
|
|
8139
|
+
cards,
|
|
8140
|
+
channelId: outcome.destination.channelId,
|
|
8141
|
+
conversationId: runArgs.conversationId,
|
|
8142
|
+
replyAttribution: runArgs.run?.dispatch?.replyAttribution,
|
|
8143
|
+
text,
|
|
8144
|
+
threadTs: outcome.destination.threadTs
|
|
8145
|
+
}));
|
|
8146
|
+
else slackMessageTs = await sendSlackReply({
|
|
8147
|
+
cards,
|
|
8148
|
+
channelId: runArgs.channelId,
|
|
8149
|
+
conversationId: runArgs.conversationId,
|
|
8150
|
+
replyAttribution: runArgs.run?.dispatch?.replyAttribution,
|
|
8151
|
+
text,
|
|
8152
|
+
threadTs: runArgs.threadTs
|
|
8153
|
+
});
|
|
8154
|
+
} catch (error) {
|
|
8155
|
+
if (isRetryableSlackPostError(error)) throw new RetryableDeliveryError(error);
|
|
8156
|
+
throw error;
|
|
8157
|
+
}
|
|
8158
|
+
const messageTs = slackMessageTs.at(-1);
|
|
8159
|
+
assistantMessageDelivered = true;
|
|
8160
|
+
acceptedDeliveryId = messageTs;
|
|
8161
|
+
const recordedMessageId = recordDeliveredAssistantMessage({
|
|
8162
|
+
cards,
|
|
8163
|
+
conversation: deliveryState.conversation,
|
|
8164
|
+
sessionId: deliveryState.sessionId,
|
|
8165
|
+
source: "slack",
|
|
8166
|
+
text,
|
|
8167
|
+
userMessageId: deliveryState.userMessageId
|
|
8168
|
+
});
|
|
8169
|
+
if (messageTs) markConversationMessage(deliveryState.conversation, recordedMessageId, { slackTs: messageTs });
|
|
8170
|
+
try {
|
|
8171
|
+
const destination = runArgs.run?.destination;
|
|
8172
|
+
const providerConversationIds = runArgs.threadTs ? [runArgs.threadTs] : slackMessageTs;
|
|
8173
|
+
await persistWithRetry(() => commitAcceptedReply({
|
|
8174
|
+
...message ? { agentMessage: message } : void 0,
|
|
8175
|
+
conversation: deliveryState.conversation,
|
|
8176
|
+
conversationMessageId: recordedMessageId,
|
|
8177
|
+
conversationId,
|
|
8178
|
+
...runArgs.run?.dispatch && destination?.platform === "slack" && providerConversationIds.length > 0 ? { providerConversationBindings: providerConversationIds.map((providerConversationId) => ({
|
|
8179
|
+
provider: "slack",
|
|
8180
|
+
providerDestinationId: destination.channelId,
|
|
8181
|
+
providerTenantId: destination.teamId,
|
|
8182
|
+
providerConversationId
|
|
8183
|
+
})) } : void 0
|
|
8184
|
+
}));
|
|
8185
|
+
} catch (error) {
|
|
8186
|
+
logException(error, "slack.resume.assistant_message_post_delivery_persist.failed");
|
|
8187
|
+
}
|
|
8188
|
+
const dispatchId = savedRun.dispatch?.id;
|
|
8189
|
+
if (messageTs && dispatchId) try {
|
|
8190
|
+
await persistWithRetry(() => recordTurnSummary({
|
|
8191
|
+
conversationId: runArgs.conversationId,
|
|
8192
|
+
destination: savedRun.destination,
|
|
8193
|
+
destinationVisibility: visibility,
|
|
8194
|
+
dispatchId,
|
|
8195
|
+
resultMessageId: messageTs,
|
|
8196
|
+
turnId: runArgs.turnId,
|
|
8197
|
+
sliceId: runArgs.sliceId ?? 1,
|
|
8198
|
+
source: savedRun.source,
|
|
8199
|
+
state: "running",
|
|
8200
|
+
surface: savedRun.surface ?? "slack"
|
|
8201
|
+
}));
|
|
8202
|
+
} catch (error) {
|
|
8203
|
+
logException(error, "agent.turn.delivery_receipt_persist.failed");
|
|
8204
|
+
}
|
|
8205
|
+
failureCode = "agent_run_failed";
|
|
8206
|
+
};
|
|
8207
|
+
const deliveryState = await getDeliveryConversation();
|
|
8208
|
+
assistantMessageDelivered = turnHasReply(deliveryState.conversation, sessionId);
|
|
8209
|
+
const run = buildResumedRun(runArgs, status, deliverAssistantMessage);
|
|
8210
|
+
if (runArgs.inputMessageIds?.length) await turnLifecycle.start({
|
|
8211
|
+
conversationId: runArgs.conversationId,
|
|
8212
|
+
turnId: runArgs.turnId,
|
|
8213
|
+
createdAtMs: Date.now(),
|
|
8214
|
+
inputMessageIds: [...new Set(runArgs.inputMessageIds)],
|
|
8215
|
+
surface: "slack"
|
|
8216
|
+
});
|
|
8217
|
+
const replyTimeoutMs = resolveReplyTimeoutMs();
|
|
8218
|
+
const outcome = await runArgs.executeTurn(run, async (result) => {
|
|
8219
|
+
const finalized = finalizeFailedTurnReplyWithEvent({
|
|
8220
|
+
reply: result,
|
|
8221
|
+
logException
|
|
8222
|
+
});
|
|
8223
|
+
const reply = finalized.reply;
|
|
8224
|
+
const dispatchErrorMessage = run.dispatch && reply.diagnostics.outcome !== "success" ? reply.diagnostics.errorMessage ?? `Agent turn ended with ${reply.diagnostics.outcome}.` : void 0;
|
|
8225
|
+
if (reply.diagnostics.outcome !== "success") await deliverAssistantMessage(reply.text);
|
|
8226
|
+
runResultHandled = true;
|
|
8227
|
+
await status.clear();
|
|
8228
|
+
failureCode = "persistence_failed";
|
|
8229
|
+
if (reply.piMessages?.length) await saveTurnCheckpoint({
|
|
8230
|
+
mode: "completed",
|
|
8231
|
+
conversationId: runArgs.conversationId,
|
|
8232
|
+
turnId: runArgs.turnId,
|
|
8233
|
+
messages: reply.piMessages,
|
|
8234
|
+
durationMs: reply.diagnostics.durationMs,
|
|
8235
|
+
usage: reply.diagnostics.usage,
|
|
8236
|
+
destination: run.destination,
|
|
8237
|
+
destinationVisibility: visibility,
|
|
8238
|
+
dispatchId: run.dispatch?.id,
|
|
8239
|
+
dispatchOutcome: reply.diagnostics.outcome === "success" ? "completed" : "failed",
|
|
8240
|
+
...dispatchErrorMessage ? { errorMessage: dispatchErrorMessage } : void 0,
|
|
8241
|
+
...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : void 0,
|
|
8242
|
+
source: run.source,
|
|
8243
|
+
actor: resumeActor,
|
|
8244
|
+
surface: run.surface ?? "slack",
|
|
8245
|
+
sliceId: runArgs.sliceId
|
|
8246
|
+
});
|
|
8247
|
+
else if (run.dispatch?.id) await recordTurnSummary({
|
|
8248
|
+
conversationId: runArgs.conversationId,
|
|
8249
|
+
destination: run.destination,
|
|
8250
|
+
destinationVisibility: visibility,
|
|
8251
|
+
dispatchId: run.dispatch?.id,
|
|
8252
|
+
dispatchOutcome: reply.diagnostics.outcome === "success" ? "completed" : "failed",
|
|
8253
|
+
...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : void 0,
|
|
8254
|
+
turnId: runArgs.turnId,
|
|
8255
|
+
sliceId: runArgs.sliceId ?? 1,
|
|
8256
|
+
source: run.source,
|
|
8257
|
+
state: reply.diagnostics.outcome === "success" ? "completed" : "failed",
|
|
8258
|
+
surface: run.surface ?? "slack"
|
|
8259
|
+
});
|
|
8260
|
+
await runArgs.commitResult?.(reply);
|
|
8261
|
+
if (reply.diagnostics.outcome === "success") {
|
|
8262
|
+
shouldScheduleCompletedPluginTasks = true;
|
|
8263
|
+
return { outcome: assistantMessageDelivered ? "success" : "no_reply" };
|
|
8264
|
+
}
|
|
8265
|
+
return {
|
|
8266
|
+
outcome: "failed",
|
|
8267
|
+
failureCode: "model_execution_failed",
|
|
8268
|
+
...finalized.eventId ? { eventId: finalized.eventId } : void 0,
|
|
8269
|
+
...finalized.failureReason ? { failureReason: finalized.failureReason } : void 0
|
|
8270
|
+
};
|
|
8271
|
+
}, replyTimeoutMs);
|
|
8272
|
+
if (outcome.status !== "completed") {
|
|
8273
|
+
await status.clear();
|
|
8274
|
+
const onAuthPause = runArgs.onAuthPause;
|
|
8275
|
+
const onSuspend = runArgs.onSuspend;
|
|
8276
|
+
if (outcome.status === "awaiting_auth" && onAuthPause) {
|
|
8277
|
+
deferredAuthInfo = {
|
|
8278
|
+
providerDisplayName: outcome.providerDisplayName,
|
|
8279
|
+
actorId: isUserActor(resumeActor) ? resumeActor.userId : void 0,
|
|
8280
|
+
...outcome.requestText ? { requestText: outcome.requestText } : void 0
|
|
8281
|
+
};
|
|
8282
|
+
deferredPauseHandler = async () => {
|
|
8283
|
+
await onAuthPause({ providerDisplayName: outcome.providerDisplayName });
|
|
8284
|
+
};
|
|
8285
|
+
} else if (outcome.status === "suspended") {
|
|
8286
|
+
if (outcome.reason === "timeout" || savedRun.durability?.shouldYield?.()) throw new CooperativeTurnYieldError();
|
|
8287
|
+
if (onSuspend) deferredPauseHandler = async () => {
|
|
8288
|
+
await onSuspend(outcome.resumeVersion);
|
|
8289
|
+
};
|
|
8290
|
+
else deferredFailureHandler = async () => {
|
|
8291
|
+
await handleResumeFailure({
|
|
8292
|
+
error: /* @__PURE__ */ new Error("Resumed Run ended suspended without onSuspend"),
|
|
8293
|
+
eventName: "slack.resume.turn.failed",
|
|
8294
|
+
failureCode: "agent_run_failed",
|
|
8295
|
+
resume: runArgs
|
|
8296
|
+
});
|
|
8297
|
+
};
|
|
8298
|
+
} else deferredFailureHandler = async () => {
|
|
8299
|
+
await handleResumeFailure({
|
|
8300
|
+
error: /* @__PURE__ */ new Error(`Resumed Run ended ${outcome.status} without its required callback`),
|
|
8301
|
+
eventName: "slack.resume.turn.failed",
|
|
8302
|
+
failureCode: "agent_run_failed",
|
|
8303
|
+
resume: runArgs
|
|
8304
|
+
});
|
|
8305
|
+
};
|
|
8306
|
+
}
|
|
8307
|
+
if (shouldScheduleCompletedPluginTasks) try {
|
|
8308
|
+
const params = {
|
|
8309
|
+
conversationId: runArgs.conversationId,
|
|
8310
|
+
sessionId: runArgs.turnId
|
|
8311
|
+
};
|
|
8312
|
+
await (runArgs.scheduleSessionCompletedPluginTasks ?? scheduleSessionCompletedPluginTasks)(params);
|
|
8313
|
+
} catch (scheduleError) {
|
|
8314
|
+
logException(scheduleError, "plugin.session.completed_task_schedule.failed");
|
|
8315
|
+
}
|
|
8316
|
+
} catch (error) {
|
|
8317
|
+
await status.clear();
|
|
8318
|
+
const runError = error instanceof AgentRunError ? error.cause : error;
|
|
8319
|
+
if (isCooperativeTurnYieldError(runError)) throw runError;
|
|
8320
|
+
if (runResultHandled) {
|
|
8321
|
+
postDeliveryCommitError = error;
|
|
8322
|
+
try {
|
|
8323
|
+
await runArgs.onPostDeliveryCommitFailure?.(error);
|
|
8324
|
+
} catch (terminalizeError) {
|
|
8325
|
+
logException(terminalizeError, "slack.resume.post_delivery_terminalize.failed");
|
|
8326
|
+
}
|
|
8327
|
+
} else deferredFailureHandler = async () => {
|
|
8328
|
+
await handleResumeFailure({
|
|
8329
|
+
error: runError,
|
|
8330
|
+
eventName: "slack.resume.turn.failed",
|
|
8331
|
+
failureCode,
|
|
8332
|
+
resume: runArgs
|
|
8333
|
+
});
|
|
8334
|
+
};
|
|
8335
|
+
} finally {
|
|
8336
|
+
if (runResultHandled) await processingReaction?.complete();
|
|
8337
|
+
else await processingReaction?.stop();
|
|
8338
|
+
}
|
|
8339
|
+
if (postDeliveryCommitError) {
|
|
8340
|
+
const eventId = logException(postDeliveryCommitError, "slack.resume.success_handler.failed");
|
|
8341
|
+
await turnLifecycle.fail({
|
|
8342
|
+
conversationId: runArgs.conversationId,
|
|
8343
|
+
turnId: runArgs.turnId,
|
|
8344
|
+
createdAtMs: Date.now(),
|
|
8345
|
+
failureCode: "persistence_failed",
|
|
8346
|
+
...eventId ? { eventId } : void 0
|
|
8347
|
+
});
|
|
8348
|
+
throw postDeliveryCommitError;
|
|
8349
|
+
}
|
|
8350
|
+
if (deferredPauseHandler) try {
|
|
8351
|
+
await deferredPauseHandler();
|
|
8352
|
+
if (deferredAuthInfo) await postSlackMessageBestEffort(runArgs.channelId, runArgs.threadTs, buildAuthPauseResponse(deferredAuthInfo.actorId, deferredAuthInfo.providerDisplayName, deferredAuthInfo.requestText), runArgs.conversationId, runArgs.run?.dispatch?.replyAttribution);
|
|
8353
|
+
return true;
|
|
8354
|
+
} catch (pauseError) {
|
|
8355
|
+
await handleResumeFailure({
|
|
8356
|
+
error: pauseError,
|
|
8357
|
+
eventName: "slack.resume.pause_handler.failed",
|
|
8358
|
+
failureCode: "persistence_failed",
|
|
8359
|
+
resume: runArgs
|
|
8360
|
+
});
|
|
8361
|
+
return true;
|
|
8362
|
+
}
|
|
8363
|
+
if (deferredFailureHandler) await deferredFailureHandler();
|
|
8364
|
+
return true;
|
|
8365
|
+
}
|
|
8366
|
+
//#endregion
|
|
6933
8367
|
//#region src/chat/task-execution/paused-turn.ts
|
|
6934
8368
|
/**
|
|
6935
8369
|
* Worker continue for paused turns under the conversation lease.
|
|
@@ -7041,7 +8475,7 @@ async function resolveSlackResumeUserActor(args) {
|
|
|
7041
8475
|
*
|
|
7042
8476
|
* Sources, in order:
|
|
7043
8477
|
* 1. Actor saved on the Turn.
|
|
7044
|
-
* 2. Actor and credentials supplied by dispatch
|
|
8478
|
+
* 2. Actor and credentials supplied by dispatch.
|
|
7045
8479
|
* 3. Legacy Event or Slack Message data.
|
|
7046
8480
|
*
|
|
7047
8481
|
* TODO(dcramer): Remove the routing and Message Actor fallbacks after no
|
|
@@ -7070,7 +8504,7 @@ async function resolveResumeExecutionIdentity(args) {
|
|
|
7070
8504
|
};
|
|
7071
8505
|
}
|
|
7072
8506
|
function isPausedTurn(summary) {
|
|
7073
|
-
return summary.state === "paused" && (summary.resumeReason === "timeout" || summary.resumeReason === "yield" || summary.resumeReason === "retry");
|
|
8507
|
+
return summary.state === "paused" && (summary.resumeReason === "timeout" || summary.resumeReason === "yield" || summary.resumeReason === "retry" || summary.resumeReason === "auth");
|
|
7074
8508
|
}
|
|
7075
8509
|
async function failPausedTurn(args) {
|
|
7076
8510
|
const failed = await failTurnRecord({
|
|
@@ -7118,8 +8552,6 @@ async function runPausedTurnInContext(payload, options, runOptions) {
|
|
|
7118
8552
|
turnId: payload.turnId,
|
|
7119
8553
|
channelId: thread?.channelId ?? destination.channelId,
|
|
7120
8554
|
...thread?.threadTs ? { threadTs: thread.threadTs } : void 0,
|
|
7121
|
-
lockKey: payload.conversationId,
|
|
7122
|
-
ownsConversationLease: true,
|
|
7123
8555
|
executeTurn: async (run, saveResult, timeoutMs) => await executeTurn(options.agentRunner, run, saveResult, timeoutMs),
|
|
7124
8556
|
scheduleSessionCompletedPluginTasks: options.scheduleSessionCompletedPluginTasks,
|
|
7125
8557
|
beforeStart: async () => {
|
|
@@ -7129,7 +8561,7 @@ async function runPausedTurnInContext(payload, options, runOptions) {
|
|
|
7129
8561
|
conversationId: payload.conversationId,
|
|
7130
8562
|
turnId: payload.turnId
|
|
7131
8563
|
})).record;
|
|
7132
|
-
if (!turn || turn.state !== "paused" || turn.resumeReason !== "timeout" && turn.resumeReason !== "yield" && turn.resumeReason !== "retry" || turn.version !== payload.expectedVersion) return false;
|
|
8564
|
+
if (!turn || turn.state !== "paused" || turn.resumeReason !== "timeout" && turn.resumeReason !== "yield" && turn.resumeReason !== "retry" && turn.resumeReason !== "auth" || turn.version !== payload.expectedVersion) return false;
|
|
7133
8565
|
const activeTurn = turn;
|
|
7134
8566
|
const { dispatch, surface } = options.routingContext ?? {};
|
|
7135
8567
|
const currentState = await getPersistedThreadState(payload.conversationId);
|
|
@@ -7149,7 +8581,22 @@ async function runPausedTurnInContext(payload, options, runOptions) {
|
|
|
7149
8581
|
});
|
|
7150
8582
|
return false;
|
|
7151
8583
|
}
|
|
7152
|
-
|
|
8584
|
+
const authorization = turn.resumeReason === "auth" ? await getTurnAuthorization(turn) : void 0;
|
|
8585
|
+
if (turn.resumeReason === "auth") {
|
|
8586
|
+
if (!authorization) return false;
|
|
8587
|
+
const pendingAuth = conversation.processing.pendingAuth;
|
|
8588
|
+
if (pendingAuth && (pendingAuth.sessionId !== turn.turnId || pendingAuth.actorId !== authorization.actorId || pendingAuth.provider !== authorization.provider || pendingAuth.kind !== authorization.kind || pendingAuth.scope !== authorization.scope || pendingAuth.kind === "mcp" && pendingAuth.authSessionId !== authorization.authSessionId || !isPendingAuthLatestRequest(conversation, pendingAuth))) {
|
|
8589
|
+
await abandonTurnRecord({
|
|
8590
|
+
conversationId: payload.conversationId,
|
|
8591
|
+
turnId: turn.turnId,
|
|
8592
|
+
errorMessage: "Authorization no longer belongs to the latest request"
|
|
8593
|
+
});
|
|
8594
|
+
clearPendingAuth(conversation, turn.turnId);
|
|
8595
|
+
await persistThreadStateById(payload.conversationId, { conversation });
|
|
8596
|
+
return false;
|
|
8597
|
+
}
|
|
8598
|
+
if (!pendingAuth && conversation.processing.activeTurnId !== turn.turnId) return false;
|
|
8599
|
+
} else if (conversation.processing.activeTurnId !== payload.turnId) return false;
|
|
7153
8600
|
const identity = await resolveResumeExecutionIdentity({
|
|
7154
8601
|
actor: activeTurn.actor,
|
|
7155
8602
|
conversationId: payload.conversationId,
|
|
@@ -7166,6 +8613,15 @@ async function runPausedTurnInContext(payload, options, runOptions) {
|
|
|
7166
8613
|
return false;
|
|
7167
8614
|
}
|
|
7168
8615
|
const { actor, credentialContext } = identity;
|
|
8616
|
+
if (authorization && (!("userId" in actor) || actor.userId !== authorization.actorId)) throw new Error("Authorization actor does not match the paused Turn");
|
|
8617
|
+
if (authorization) {
|
|
8618
|
+
await recordAuthorizationCompleted({
|
|
8619
|
+
conversationId: payload.conversationId,
|
|
8620
|
+
...authorization
|
|
8621
|
+
});
|
|
8622
|
+
conversation.processing.activeTurnId = turn.turnId;
|
|
8623
|
+
await persistThreadStateById(payload.conversationId, { conversation });
|
|
8624
|
+
}
|
|
7169
8625
|
const locationConfiguration = getLocationConfigurationService(destination);
|
|
7170
8626
|
const conversationContext = dispatchId ? void 0 : buildConversationContext(conversation, { excludeMessageId: userMessage.id });
|
|
7171
8627
|
const sandboxRef = getPersistedSandboxState(currentState);
|
|
@@ -7207,8 +8663,11 @@ async function runPausedTurnInContext(payload, options, runOptions) {
|
|
|
7207
8663
|
...routing.location ? { location: routing.location } : void 0,
|
|
7208
8664
|
source,
|
|
7209
8665
|
...surface ? { surface } : void 0,
|
|
7210
|
-
toolChannelId: destination.channelId,
|
|
7211
|
-
environment: {
|
|
8666
|
+
toolChannelId: authorization?.toolChannelId ?? destination.channelId,
|
|
8667
|
+
environment: {
|
|
8668
|
+
configuration: authorization?.configuration,
|
|
8669
|
+
locationConfiguration
|
|
8670
|
+
},
|
|
7212
8671
|
state: {
|
|
7213
8672
|
pendingAuth: conversation.processing.pendingAuth,
|
|
7214
8673
|
sandboxRef
|
|
@@ -7359,16 +8818,10 @@ async function runNextPausedTurnInContext(conversationId, options, runOptions) {
|
|
|
7359
8818
|
await persistThreadStateById(conversationId, { conversation });
|
|
7360
8819
|
return false;
|
|
7361
8820
|
}
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
if (!record || record.state !== "running") return;
|
|
7367
|
-
await failStrandedTurnWithFallback({
|
|
7368
|
-
conversationId,
|
|
7369
|
-
errorMessage: "Turn lost its worker before reaching a safe boundary",
|
|
7370
|
-
turn: record
|
|
7371
|
-
});
|
|
8821
|
+
await failStrandedTurnWithFallback({
|
|
8822
|
+
conversationId,
|
|
8823
|
+
errorMessage: "Turn lost its worker before reaching a safe boundary",
|
|
8824
|
+
turn: running
|
|
7372
8825
|
});
|
|
7373
8826
|
return false;
|
|
7374
8827
|
}
|
|
@@ -7379,6 +8832,7 @@ async function runNextPausedTurnInContext(conversationId, options, runOptions) {
|
|
|
7379
8832
|
turnId: summary.turnId
|
|
7380
8833
|
});
|
|
7381
8834
|
if (!request) {
|
|
8835
|
+
if (summary.resumeReason === "auth") continue;
|
|
7382
8836
|
await failPausedTurn({
|
|
7383
8837
|
conversationId,
|
|
7384
8838
|
summary,
|
|
@@ -10362,16 +11816,10 @@ async function createApp(options) {
|
|
|
10362
11816
|
storage: attachmentStorage
|
|
10363
11817
|
}));
|
|
10364
11818
|
app.get("/api/oauth/callback/mcp/:provider", (c) => {
|
|
10365
|
-
return GET$4(c.req.raw, c.req.param("provider"), waitUntil, {
|
|
10366
|
-
agentRunner,
|
|
10367
|
-
conversationWorkQueue: getVercelConversationWorkQueue()
|
|
10368
|
-
});
|
|
11819
|
+
return GET$4(c.req.raw, c.req.param("provider"), waitUntil, { conversationWorkQueue: getVercelConversationWorkQueue() });
|
|
10369
11820
|
});
|
|
10370
11821
|
app.get("/api/oauth/callback/:provider", (c) => {
|
|
10371
|
-
return GET$5(c.req.raw, c.req.param("provider"), waitUntil, {
|
|
10372
|
-
agentRunner,
|
|
10373
|
-
conversationWorkQueue: getVercelConversationWorkQueue()
|
|
10374
|
-
});
|
|
11822
|
+
return GET$5(c.req.raw, c.req.param("provider"), waitUntil, { conversationWorkQueue: getVercelConversationWorkQueue() });
|
|
10375
11823
|
});
|
|
10376
11824
|
app.get("/api/internal/sandbox-egress/signals", (c) => {
|
|
10377
11825
|
return GET(c.req.raw);
|