@sentry/junior 0.223.0 → 0.225.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.
Files changed (57) hide show
  1. package/dist/{agent-hooks-DZU5nsd1.js → agent-hooks-CRuRcOg9.js} +9 -9
  2. package/dist/{agent-Bzbf92vv.js → agent-runner-Cad1RtWn.js} +1016 -144
  3. package/dist/api/schema.js +2 -2
  4. package/dist/api.js +16 -16
  5. package/dist/app.js +1538 -71
  6. package/dist/{briefs-BJPfCrlT.js → briefs-BtnkPWT5.js} +5 -5
  7. package/dist/{catalog-runtime-BthunUDD.js → catalog-runtime-pv4grNNb.js} +1 -1
  8. package/dist/{checkpoint-Deu-OVb3.js → checkpoint-6fkAdB3g.js} +8 -8
  9. package/dist/cli/chat.js +14 -15
  10. package/dist/cli/check.js +3 -3
  11. package/dist/cli/main.js +1 -1
  12. package/dist/cli/plugins.js +3 -3
  13. package/dist/cli/snapshot-create.js +3 -3
  14. package/dist/cli/upgrade.js +3 -3
  15. package/dist/{client-DjC6-6iU.js → client-ihmkx60d.js} +2 -2
  16. package/dist/{config-CtoUVs72.js → config-DMg679BW.js} +11 -13
  17. package/dist/{conversation-1HUoZr2l.js → conversation-Bygi39Xj.js} +1 -1
  18. package/dist/{conversation-privacy-xS1t92xa.js → conversation-privacy-ZYy05Y3q.js} +1 -1
  19. package/dist/credential-sync-DCB2SwnQ.js +940 -0
  20. package/dist/{execution-stats-CrXs9vSH.js → execution-stats-CR9CYpyj.js} +3 -3
  21. package/dist/{executor-AOhG9X1t.js → executor-B9R4cHte.js} +1 -1
  22. package/dist/instrumentation.js +2 -2
  23. package/dist/{job-queue-CNd9cpIy.js → job-queue-D2SiTrnt.js} +1 -1
  24. package/dist/{logging-DiN3o3nB.js → logging-CcTvYQ8A.js} +2 -2
  25. package/dist/{model-profile-CZmVoGdX.js → model-profile-BwU8mqwD.js} +14 -1
  26. package/dist/nitro.js +3 -3
  27. package/dist/{pending-auth-D7vmttn_.js → oauth-authorization-BS4vPJmu.js} +6 -48
  28. package/dist/{oauth-callback-server-Ca0VLUEw.js → oauth-callback-server-DnlQsGBJ.js} +4 -4
  29. package/dist/{profile-BsNrw8lj.js → profile-CV97YuiL.js} +5 -9
  30. package/dist/{prompt-90miy8VV.js → prompt-CL8zxsmm.js} +1 -1
  31. package/dist/{registry-Dankt-Pl.js → registry-C8I4IejN.js} +3 -3
  32. package/dist/{resolve-B38CpJe2.js → resolve-DXVAzYa3.js} +2 -2
  33. package/dist/{runner-Ck5rEjCm.js → runner-Die1RJ_z.js} +9 -10
  34. package/dist/{sandbox-egress-signals-BOfFkvly.js → sandbox-egress-signals-BZepyVh1.js} +1 -1
  35. package/dist/{schema-RpbW5Od2.js → schema-BE4uso4i.js} +1 -1
  36. package/dist/{short-title-BHd_jUFX.js → short-title-3wYH3ipl.js} +11 -5
  37. package/dist/{skills-DbP4TDft.js → skills-DUcDEgDI.js} +2 -2
  38. package/dist/{spawn-DzXc_PKO.js → spawn-o15i3tJu.js} +2 -2
  39. package/dist/{stats-pNGp3G5q.js → stats-DawHjcTw.js} +6 -6
  40. package/dist/{task-queue-Cy9y1foT.js → task-queue-D3PLY59C.js} +3 -3
  41. package/dist/{task-runner-DI1FBJFJ.js → task-runner-BMl2aM2v.js} +9 -84
  42. package/dist/{thread-state-DXgxOYpZ.js → thread-state-CAR6gdAm.js} +5 -5
  43. package/dist/{auth-pause-state-Bn2vX4PA.js → turn-failure-response-CZyoUMkD.js} +74 -73
  44. package/dist/{turn-lifecycle-Bjf6y2GO.js → turn-lifecycle-DPDHKi2U.js} +1 -1
  45. package/dist/turn-session-routing-Jkmmxpp-.js +77 -0
  46. package/dist/{validation-D5CIslEu.js → validation-BcGGxOa6.js} +1 -1
  47. package/dist/{version-_ZQFlslB.js → version-DYcuOFQb.js} +1 -1
  48. package/dist/version.js +1 -1
  49. package/dist/{web-authorization-6mmfS_o-.js → web-authorization-B09B8A8R.js} +1 -1
  50. package/dist/{web-input-BJpWm6go.js → web-input-CGE58TpK.js} +7 -7
  51. package/dist/{work-DvIlJR3C.js → work-Bm90pR9P.js} +52 -10
  52. package/dist/{worker-D1qidg-Y.js → worker-BvHr6JhE.js} +5 -5
  53. package/package.json +4 -4
  54. package/dist/agent-runner-2VQ9WhkG.js +0 -679
  55. package/dist/credential-sync-CKdXEUDO.js +0 -2811
  56. package/dist/experimental-CIXf7Uye.js +0 -46
  57. package/dist/turn-execution-BNOXrZ-M.js +0 -47
package/dist/app.js CHANGED
@@ -1,41 +1,39 @@
1
- import { $ as AuthorizationFlowDisabledError, B as recordFinishedTurnForAutomatedLimit, C as renderBlockText, D as getNextRunAtMs, E as scheduledAutomationRunAttributes, F as ingestEvent, G as clearSandboxEgressSignals, H as normalizeSandboxEgressTracePropagationDomains, I as EVENT_WAIT_MS, J as parseSandboxEgressCredentialToken, K as consumeSandboxEgressAuthRequiredSignal, L as isEventMailboxMetadata, P as processWorkspaceSnapshotJob, Q as isVisionImageMediaType, R as renderTaskInput, S as renderAttachmentText, T as logScheduledAutomationRunSkipped, U as shouldPropagateSandboxEgressTrace, V as resolveSandboxEgressProviderForHost, W as SANDBOX_EGRESS_PROXY_PATH, X as createUserTokenStore, Z as buildSteeringPiMessage, _ as readPublicArtifact, b as readAttachmentVisionSummary, c as deleteMcpStoredOAuthCredentials, et as shouldEmitDevAgentTrace, f as PluginCredentialFailureError, g as publicArtifactHeaders, m as latestProgressStatus, n as createContextCompactor, nt as setConfigDefaults, o as deleteMcpAuthSessionsForUserProvider, q as consumeSandboxEgressPermissionDeniedSignal, r as executeCredentialedEgressRequest, s as deleteMcpServerSessionId, t as executeAgentRun, tt as getConfigDefaults, v as unpublishedArtifactHeaders, w as listThreadReplies, x as recordSlackAttachment, z as maybePostAutomatedTurnLimitNotice } from "./agent-Bzbf92vv.js";
2
- import { b as isProviderRetryError, i as completeText, m as escapeXml, r as completeObject } from "./client-DjC6-6iU.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-DiN3o3nB.js";
1
+ import { $ as clearSandboxEgressSignals, A as renderAttachmentText, Dt as setConfigDefaults, E as unpublishedArtifactHeaders, Et as getConfigDefaults, 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, T as readPublicArtifact, Tt as shouldEmitDevAgentTrace, U as ingestEvent, W as EVENT_WAIT_MS, X as normalizeSandboxEgressTracePropagationDomains, Y as resolveSandboxEgressProviderForHost, Z as shouldPropagateSandboxEgressTrace, _t as slackExternalRefIdSchema, a as isExperimentalFeatureEnabled, bt as isVisionImageMediaType, c as createContextCompactor, ct as escapeSlackMrkdwnText, dt as normalizeSlackStatusText, et as consumeSandboxEgressAuthRequiredSignal, f as deleteMcpAuthSessionsForUserProvider, ft as truncateStatusText, gt as slackEntitySchema, 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 createUserTokenStore, w as publicArtifactHeaders, wt as AuthorizationFlowDisabledError, x as isPendingAuthLatestRequest, xt as RetryableDeliveryError, y as clearPendingAuth, yt as buildSteeringPiMessage } from "./agent-runner-Cad1RtWn.js";
2
+ import { b as isProviderRetryError, i as completeText, m as escapeXml, r as completeObject } from "./client-ihmkx60d.js";
3
+ import { C as withLogContext, I as isRecord$1, L as toOptionalNumber, O as JUNIOR_PLUGIN_TASK_CALLBACK_ROUTE, R as toOptionalString, S as withContext, _ as setSentryUser, b as setTags, d as logException, f as logInfo, j as JUNIOR_WORKSPACE_SNAPSHOT_JOB_CALLBACK_ROUTE, n as createChatSdkLogger, o as getActiveTraceId, p as logWarn, r as createRequestContext, t as buildTurnFailureResponse, u as logError, v as setSpanAttributes, w as withSpan, y as setSpanStatus } from "./logging-CcTvYQ8A.js";
4
4
  import { r as continueTrace } from "./sentry-CgWZM_kV.js";
5
- import { t as JUNIOR_VERSION } from "./version-_ZQFlslB.js";
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-CtoUVs72.js";
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-DZU5nsd1.js";
5
+ import { t as JUNIOR_VERSION } from "./version-DYcuOFQb.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-DMg679BW.js";
7
+ import { o as modelIdForProfile, r as defaultModelId } from "./model-profile-BwU8mqwD.js";
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-CRuRcOg9.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-AOhG9X1t.js";
12
- import { Y as instructionProvenanceFor, k as annotationCard } from "./conversation-1HUoZr2l.js";
13
- import { n as pluginCatalogRuntime } from "./catalog-runtime-BthunUDD.js";
14
- import { $ as resolveConversationTitle, F as loadProjection, G as recordSubscribedReplyRoute, J as appendConversationMessages, M as commitMessages, P as loadConversationProjection, Q as persistConversationMessageSummaries, X as persistConversationMessages, Y as hydrateConversationMessages, Z as projectConversationMessages, a as completeTurnRecord, at as isHumanConversationMessage, c as getTurnRecordForResume, ct as recordDeliveredAssistantMessage, d as persistWithRetry, dt as estimateTextTokens, et as appendThreadContextMessages, i as abandonTurnRecord, it as getConversationMessageSlackTs, j as commitAcceptedReply, k as trimTrailingAssistantMessages, l as listTurnSummaries, lt as turnHasReply, m as buildTurnErrorResponse, n as loadTurnCheckpoint, nt as createConversationMemoryService, o as failTurnRecord, ot as markConversationMessage, r as saveTurnCheckpoint, rt as estimateConversationContextTokens, s as getTurnRecord, st as normalizeConversationText, tt as buildConversationContext, u as recordTurnSummary, ut as upsertConversationMessage } from "./checkpoint-Deu-OVb3.js";
15
- import { E as acquireActiveLock, O as withActiveLock, T as MUTATION_LOCK_TTL_MS$1, k as withLock$1, w as getStateAdapter } from "./profile-BsNrw8lj.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, 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-pNGp3G5q.js";
17
- import { S as startActiveTurn, _ as isTurnInputCommitLostError, b as markTurnCompleted, d as ConversationTurnBoundaryError, f as CooperativeTurnYieldError, g as isCooperativeTurnYieldError, h as getConversationTurnBoundaryError, m as TurnInputDeferredError, p as TurnInputCommitLostError, r as clearPendingAuth, s as getAssistantReplyText, v as isTurnInputDeferredError, x as markTurnFailed, y as markTurnClosed } from "./pending-auth-D7vmttn_.js";
18
- import { h as postSlackMessage, i as formatProviderLabel, s as startOAuthFlow, t as createAgentRunner, v as RetryableDeliveryError } from "./agent-runner-2VQ9WhkG.js";
19
- 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-B38CpJe2.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-B9R4cHte.js";
12
+ import { Y as instructionProvenanceFor, k as annotationCard } from "./conversation-Bygi39Xj.js";
13
+ import { n as pluginCatalogRuntime } from "./catalog-runtime-pv4grNNb.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-6fkAdB3g.js";
15
+ import { E as acquireActiveLock, O as withLock$1, T as MUTATION_LOCK_TTL_MS$1, w as getStateAdapter } from "./profile-CV97YuiL.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-DawHjcTw.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-BS4vPJmu.js";
18
+ import { a as EVENT_SYSTEM_ACTOR, i as EVENT_MESSAGE_AUTHOR, o as isEventConversationMessage } from "./prompt-CL8zxsmm.js";
19
+ import { f as setSandboxResourceConfig, u as getSandboxResourceConfig } from "./resolve-DXVAzYa3.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-CNd9cpIy.js";
23
- import { i as setExperimentalFeatures, n as getExperimentalFeatures, r as isExperimentalFeatureEnabled } from "./experimental-CIXf7Uye.js";
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-D2SiTrnt.js";
22
+ import { n as validatePluginRegistrations, t as validatePluginEgressCredentialHooks } from "./validation-BcGGxOa6.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-BJpWm6go.js";
27
- import { u as recordAutomationExecution } from "./execution-stats-CrXs9vSH.js";
28
- import { _ as markDispatchCompleted, a as claimDispatchMailboxAppend, c as getDispatchConversationId, d as getDispatchTurnId, f as getPluginDispatchProjection, g as markDispatchBlocked, h as markDispatchAwaitingResume, i as resolveTurnSessionRouting, l as getDispatchInputMessageId, m as listPendingDispatchMailboxAppends, n as scheduleSessionCompletedPluginTasks, o as confirmDispatchMailboxAppend, p as isTerminalDispatchStatus, s as createOrGetDispatch, t as processPluginTask, u as getDispatchRecord, v as markDispatchFailed, y as markDispatchRunning } from "./task-runner-DI1FBJFJ.js";
29
- import { i as resolveAgentInvocationId, o as listPendingAgentInvocationMailboxAppends, r as enqueueAgentInvocation, t as createAgentInvocationWorker } from "./work-DvIlJR3C.js";
30
- import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-Bjf6y2GO.js";
31
- import { n as executeTurn, t as AgentRunError } from "./turn-execution-BNOXrZ-M.js";
32
- import { a as persistThreadState, i as persistThreadRuntimeState, n as getPersistedSandboxState, o as persistThreadStateById, r as getPersistedThreadState, s as coerceThreadConversationState, t as getLocationConfigurationService } from "./thread-state-DXgxOYpZ.js";
33
- import { a as requireTurnFailureEventId, c as getTurnUserMessage, d as getTurnUserSlackMessageTs, f as buildDeliveredTurnStatePatch, i as getAgentTurnDiagnosticsAttributes, n as persistAuthPauseTurnState, r as finalizeFailedTurnReplyWithEvent, s as loadPendingMessageCards, t as completeAuthPauseTurn, u as getTurnUserReplyAttachmentContext } from "./auth-pause-state-Bn2vX4PA.js";
34
- import { a as registerVercelPluginTaskDevConsumer, n as createVercelPluginTaskCallback, s as queueCallback } from "./task-queue-Cy9y1foT.js";
35
- import { C as isSlackTitlePermissionError, D as createSlackAdapterAssistantStatusSession, E as renderSlackObjectCard, S as isRetryableSlackPostError, T as buildSlackOutputMessage, _ as getThreadId, a as GET$4, b as getSlackApiErrorCode, c as wakePausedTurn, d as shouldKeepProcessingReactionForToolInvocation, f as startProcessingReaction, g as getRunId, h as getMessageTs, i as publishAppHomeView, l as resumeSlackTurn, m as getChannelId, o as createPausedTurns, p as getAssistantThreadContext, r as GET$5, s as getPausedTurnRequest, t as receiveLocalOAuthCredential, u as buildAuthPauseResponse, v as getThreadTs, w as sendSlackReply, x as getSlackErrorObservabilityAttributes, y as stripLeadingBotMention } from "./credential-sync-CKdXEUDO.js";
36
- import { n as deleteWebAuthorization, t as createWebAuthorization } from "./web-authorization-6mmfS_o-.js";
37
- import { t as processConversationWork } from "./worker-D1qidg-Y.js";
38
- import { t as bindSpawnAgent } from "./spawn-DzXc_PKO.js";
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-CGE58TpK.js";
25
+ import { u as recordAutomationExecution } from "./execution-stats-CR9CYpyj.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-BMl2aM2v.js";
27
+ import { c as listPendingAgentInvocationMailboxAppends, i as resolveAgentInvocationId, o as AgentRunError, r as enqueueAgentInvocation, s as executeTurn, t as createAgentInvocationWorker } from "./work-Bm90pR9P.js";
28
+ import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-DPDHKi2U.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-CAR6gdAm.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-DCB2SwnQ.js";
31
+ import { n as resolveTurnSessionRouting } from "./turn-session-routing-Jkmmxpp-.js";
32
+ import { n as deleteWebAuthorization, t as createWebAuthorization } from "./web-authorization-B09B8A8R.js";
33
+ import { a as registerVercelPluginTaskDevConsumer, n as createVercelPluginTaskCallback, s as queueCallback } from "./task-queue-D3PLY59C.js";
34
+ import { t as processConversationWork } from "./worker-BvHr6JhE.js";
35
+ import { t as bindSpawnAgent } from "./spawn-o15i3tJu.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-CZyoUMkD.js";
39
37
  import { createRequire } from "node:module";
40
38
  import { pathToFileURL } from "node:url";
41
39
  import { Hono } from "hono";
@@ -1920,13 +1918,126 @@ 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
+ const attributes = {
1994
+ title: { text: title },
1995
+ display_id: card.label,
1996
+ display_type: card.objectType === "code_change" ? "Pull request" : task ? "Issue" : "Item",
1997
+ product_name: card.plugin
1998
+ };
1999
+ return {
2000
+ text,
2001
+ entity: {
2002
+ ...task ? {
2003
+ entity_type: "slack#/entities/task",
2004
+ entity_payload: {
2005
+ attributes,
2006
+ fields: card.status ? { status: { value: card.status } } : void 0,
2007
+ custom_fields: []
2008
+ }
2009
+ } : {
2010
+ entity_type: "slack#/entities/item",
2011
+ entity_payload: {
2012
+ attributes,
2013
+ custom_fields: card.status ? [{
2014
+ key: "status",
2015
+ label: "Status",
2016
+ type: "string",
2017
+ value: card.status
2018
+ }] : []
2019
+ }
2020
+ },
2021
+ external_ref: {
2022
+ id: Buffer.from(JSON.stringify([
2023
+ conversationId,
2024
+ card.plugin,
2025
+ card.key
2026
+ ]), "utf8").toString("base64url"),
2027
+ type: "annotation"
2028
+ },
2029
+ url: card.url
2030
+ }
2031
+ };
2032
+ }
2033
+ //#endregion
1923
2034
  //#region src/chat/slack/annotation-details.ts
1924
2035
  const eventSchema$1 = z.object({
1925
2036
  trigger_id: z.string().min(1),
1926
2037
  user: z.string().min(1),
1927
2038
  external_ref: z.object({
1928
2039
  type: z.literal("annotation"),
1929
- id: z.string().max(4096)
2040
+ id: slackExternalRefIdSchema.max(4096)
1930
2041
  })
1931
2042
  });
1932
2043
  const refSchema = z.tuple([
@@ -1950,7 +2061,7 @@ async function presentSlackAnnotationDetails(event, teamId) {
1950
2061
  }
1951
2062
  let value;
1952
2063
  try {
1953
- value = JSON.parse(parsed.data.external_ref.id);
2064
+ value = JSON.parse(Buffer.from(parsed.data.external_ref.id, "base64url").toString("utf8"));
1954
2065
  } catch {
1955
2066
  await missing();
1956
2067
  return;
@@ -1995,7 +2106,7 @@ async function presentSlackAnnotationDetails(event, teamId) {
1995
2106
  }
1996
2107
  await client.entity.presentDetails({
1997
2108
  trigger_id: parsed.data.trigger_id,
1998
- metadata: entity
2109
+ metadata: slackEntitySchema.parse(entity)
1999
2110
  });
2000
2111
  }
2001
2112
  //#endregion
@@ -2076,11 +2187,11 @@ const BARE_STOP_RE = /^stop(?:\s*[.!…]+)?$/i;
2076
2187
  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
2188
  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
2189
  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$1(value) {
2190
+ function escapeRegExp$2(value) {
2080
2191
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2081
2192
  }
2082
2193
  function containsAssistantInvocation(text, botUserName) {
2083
- const escapedUserName = escapeRegExp$1(botUserName);
2194
+ const escapedUserName = escapeRegExp$2(botUserName);
2084
2195
  const plainNameMentionRe = new RegExp(`(^|\\s)@${escapedUserName}\\b`, "i");
2085
2196
  const labeledEntityMentionRe = new RegExp(`<@[^>|]+\\|${escapedUserName}>`, "i");
2086
2197
  return plainNameMentionRe.test(text) || labeledEntityMentionRe.test(text);
@@ -2108,10 +2219,11 @@ function isBareStop(rawText, text) {
2108
2219
  });
2109
2220
  }
2110
2221
  /**
2111
- * Return a stop decision for one message body.
2222
+ * Return a stop decision only when the message is not addressed to another party.
2112
2223
  * Deferred batches must call this on each body, not on joined text.
2113
2224
  */
2114
2225
  function getThreadStopDecision(args) {
2226
+ if (getSubscribedReplyPreflightDecision(args)) return;
2115
2227
  const text = args.text.trim();
2116
2228
  const rawText = args.rawText.trim();
2117
2229
  if (hasBangStop(rawText, text)) return {
@@ -2274,8 +2386,10 @@ async function decideSubscribedThreadReply(args) {
2274
2386
  const text = args.input.text.trim();
2275
2387
  const rawText = args.input.rawText.trim();
2276
2388
  const stopDecision = getThreadStopDecision({
2389
+ botUserName: args.botUserName,
2277
2390
  rawText,
2278
- text
2391
+ text,
2392
+ isExplicitMention: args.input.isExplicitMention
2279
2393
  });
2280
2394
  if (stopDecision) return stopDecision;
2281
2395
  const preflightDecision = getSubscribedReplyPreflightDecision({
@@ -2371,6 +2485,53 @@ async function decideSubscribedThreadReply(args) {
2371
2485
  }
2372
2486
  }
2373
2487
  //#endregion
2488
+ //#region src/chat/slack/errors.ts
2489
+ /** Extract Slack's stable API error code from Web API errors. */
2490
+ function getSlackApiErrorCode(error) {
2491
+ if (!error || typeof error !== "object") return;
2492
+ const candidate = error;
2493
+ if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) return candidate.data.error;
2494
+ if (typeof candidate.code === "string" && candidate.code.trim().length > 0) return candidate.code;
2495
+ }
2496
+ /** Convert Slack Web API error details into the repository's trace attributes. */
2497
+ function getSlackErrorObservabilityAttributes(error) {
2498
+ if (!error || typeof error !== "object") return {};
2499
+ const candidate = error;
2500
+ const attributes = {};
2501
+ if (typeof candidate.code === "string" && candidate.code.trim().length > 0) attributes["app.slack.error_code"] = candidate.code;
2502
+ if (typeof candidate.data?.error === "string" && candidate.data.error.trim().length > 0) attributes["app.slack.api_error"] = candidate.data.error;
2503
+ const requestId = getHeaderString(candidate.headers, "x-slack-req-id");
2504
+ if (requestId) attributes["app.slack.request_id"] = requestId;
2505
+ if (typeof candidate.statusCode === "number" && Number.isFinite(candidate.statusCode)) attributes["http.response.status_code"] = candidate.statusCode;
2506
+ return attributes;
2507
+ }
2508
+ const TRANSIENT_SLACK_API_ERRORS = /* @__PURE__ */ new Set([
2509
+ "fatal_error",
2510
+ "internal_error",
2511
+ "rate_limited",
2512
+ "ratelimited",
2513
+ "request_timeout",
2514
+ "service_unavailable"
2515
+ ]);
2516
+ /** Recover transient or ambiguous Slack failures, but stop on explicit rejection. */
2517
+ function isRetryableSlackPostError(error) {
2518
+ if (!error || typeof error !== "object") return true;
2519
+ const candidate = error;
2520
+ if (typeof candidate.statusCode === "number") {
2521
+ if (candidate.statusCode === 429 || candidate.statusCode >= 500) return true;
2522
+ if (candidate.statusCode >= 400) return false;
2523
+ }
2524
+ const apiError = error instanceof SlackActionError ? error.apiError : typeof candidate.data?.error === "string" ? candidate.data.error : void 0;
2525
+ if (apiError) return TRANSIENT_SLACK_API_ERRORS.has(apiError);
2526
+ if (error instanceof SlackActionError) return error.code === "rate_limited" || error.code === "internal_error";
2527
+ return true;
2528
+ }
2529
+ /** Report whether Slack rejected assistant title updates for stable auth reasons. */
2530
+ function isSlackTitlePermissionError(error) {
2531
+ const code = getSlackApiErrorCode(error);
2532
+ return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
2533
+ }
2534
+ //#endregion
2374
2535
  //#region src/chat/slack/message/content.ts
2375
2536
  /**
2376
2537
  * Slack message content projection.
@@ -2411,6 +2572,166 @@ function parseContent(message) {
2411
2572
  };
2412
2573
  }
2413
2574
  //#endregion
2575
+ //#region src/chat/runtime/thread-context.ts
2576
+ function escapeRegExp$1(value) {
2577
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2578
+ }
2579
+ function readOptionalStringField(value, key) {
2580
+ if (!(key in value)) return;
2581
+ return toOptionalString(value[key]);
2582
+ }
2583
+ function stripLeadingBotMention(text, options = {}) {
2584
+ if (!text.trim()) return text;
2585
+ let next = text;
2586
+ if (options.stripLeadingSlackMentionToken) {
2587
+ if (options.botUserId) {
2588
+ const botUserId = escapeRegExp$1(options.botUserId);
2589
+ const mentionByBotUserIdRe = new RegExp(`^\\s*(?:<@${botUserId}(?:\\|[^>]+)?>|@${botUserId})[\\s,:-]*`, "i");
2590
+ next = next.replace(mentionByBotUserIdRe, "").trim();
2591
+ } else next = next.replace(/^\s*<@[^>]+>[\s,:-]*/, "").trim();
2592
+ }
2593
+ const mentionByNameRe = new RegExp(`^\\s*@${escapeRegExp$1(botConfig.userName)}\\b[\\s,:-]*`, "i");
2594
+ next = next.replace(mentionByNameRe, "").trim();
2595
+ const mentionByLabeledEntityRe = new RegExp(`^\\s*<@[^>|]+\\|${escapeRegExp$1(botConfig.userName)}>[\\s,:-]*`, "i");
2596
+ next = next.replace(mentionByLabeledEntityRe, "").trim();
2597
+ return next;
2598
+ }
2599
+ function getThreadId(thread, _message) {
2600
+ return toOptionalString(thread.id);
2601
+ }
2602
+ function getRunId(thread, message) {
2603
+ return readOptionalStringField(thread, "runId") ?? readOptionalStringField(message, "runId");
2604
+ }
2605
+ function getChannelId(thread, message) {
2606
+ return resolveSlackChannelIdFromThreadId(toOptionalString(thread.id)) ?? normalizeSlackConversationId(toOptionalString(thread.channelId)) ?? resolveSlackChannelIdFromMessage(message);
2607
+ }
2608
+ function getThreadTs(threadId) {
2609
+ return parseSlackThreadId(threadId)?.threadTs;
2610
+ }
2611
+ /**
2612
+ * Resolve Slack assistant-thread API context for the current turn.
2613
+ *
2614
+ * Slack assistant-thread methods must use the live inbound thread context
2615
+ * Slack provided on the current message. Slack's assistant utilities build
2616
+ * `setStatus`/`setTitle` from `message.channel` plus `message.thread_ts ?? message.ts`
2617
+ * for non-DM message events, while `message.im` still requires an explicit
2618
+ * `thread_ts`. Do not synthesize assistant-thread roots from persisted state.
2619
+ */
2620
+ function getAssistantThreadContext(message) {
2621
+ const rawContext = readSlackRawMessageContext(message);
2622
+ const channelId = rawContext?.channelId;
2623
+ if (channelId) {
2624
+ const threadTs = isDmChannel(channelId) ? rawContext.threadTs : rawContext.threadTs ?? rawContext.messageTs;
2625
+ if (threadTs) return {
2626
+ channelId,
2627
+ threadTs
2628
+ };
2629
+ }
2630
+ const parsedThreadId = parseSlackThreadId(readOptionalStringField(message, "threadId"));
2631
+ if (!parsedThreadId || isDmChannel(parsedThreadId.channelId)) return;
2632
+ return parsedThreadId;
2633
+ }
2634
+ /** Resolve the native Slack timestamp for a message that can target Slack APIs. */
2635
+ function getMessageTs(message) {
2636
+ const directTs = readOptionalStringField(message, "ts");
2637
+ const parsedDirectTs = parseSlackMessageTs(directTs);
2638
+ if (parsedDirectTs) return parsedDirectTs;
2639
+ const rawContext = readSlackRawMessageContext(message);
2640
+ if (!rawContext) return;
2641
+ const candidates = [rawContext.messageTs, rawContext.nestedMessageTs];
2642
+ for (const candidate of candidates) {
2643
+ const ts = parseSlackMessageTs(candidate);
2644
+ if (ts) return ts;
2645
+ }
2646
+ }
2647
+ //#endregion
2648
+ //#region src/chat/providers/slack/processing-reaction.ts
2649
+ const noProcessingReaction = {
2650
+ complete: async () => void 0,
2651
+ keep: () => void 0,
2652
+ stop: async () => void 0
2653
+ };
2654
+ function isProcessingReactionEmoji(value) {
2655
+ return typeof value === "string" && normalizeSlackEmojiName(value) === getChatConfig().slack.processingReactionEmoji;
2656
+ }
2657
+ /** Return true when a Slack reaction tool invocation keeps the processing reaction. */
2658
+ function shouldKeepProcessingReactionForToolInvocation(input) {
2659
+ return input.toolName === "addReaction" && isProcessingReactionEmoji(input.params.emoji);
2660
+ }
2661
+ /** Start Junior's automatic Slack processing reaction for one inbound message. */
2662
+ async function startProcessingReaction(args) {
2663
+ if (args.message.author.isMe) return noProcessingReaction;
2664
+ const channelId = getChannelId(args.thread, args.message);
2665
+ const messageTs = getMessageTs(args.message);
2666
+ if (!channelId || !messageTs) return noProcessingReaction;
2667
+ return startProcessingReactionForMessage({
2668
+ channelId,
2669
+ timestamp: messageTs
2670
+ });
2671
+ }
2672
+ /** Start Junior's automatic Slack processing reaction for a known Slack message. */
2673
+ async function startProcessingReactionForMessage(args) {
2674
+ try {
2675
+ await addReactionToMessage({
2676
+ channelId: args.channelId,
2677
+ timestamp: args.timestamp,
2678
+ emoji: getChatConfig().slack.processingReactionEmoji
2679
+ });
2680
+ } catch (error) {
2681
+ logException(error, "slack.processing.reaction_add.failed", {
2682
+ "app.slack.action": "reactions.add",
2683
+ "messaging.message.id": args.timestamp,
2684
+ ...getSlackErrorObservabilityAttributes(error)
2685
+ });
2686
+ return noProcessingReaction;
2687
+ }
2688
+ let shouldRemove = true;
2689
+ const removeProcessingReaction = async () => {
2690
+ if (!shouldRemove) return false;
2691
+ try {
2692
+ await removeReactionFromMessage({
2693
+ channelId: args.channelId,
2694
+ timestamp: args.timestamp,
2695
+ emoji: getChatConfig().slack.processingReactionEmoji
2696
+ });
2697
+ return true;
2698
+ } catch (error) {
2699
+ logException(error, "slack.processing.reaction_remove.failed", {
2700
+ "app.slack.action": "reactions.remove",
2701
+ "messaging.message.id": args.timestamp,
2702
+ ...getSlackErrorObservabilityAttributes(error)
2703
+ });
2704
+ return false;
2705
+ }
2706
+ };
2707
+ return {
2708
+ complete: async () => {
2709
+ const shouldAddCompleted = shouldRemove;
2710
+ await removeProcessingReaction();
2711
+ if (!shouldAddCompleted) return;
2712
+ try {
2713
+ await addReactionToMessage({
2714
+ channelId: args.channelId,
2715
+ timestamp: args.timestamp,
2716
+ emoji: getChatConfig().slack.completedReactionEmoji
2717
+ });
2718
+ } catch (error) {
2719
+ logException(error, "slack.processing.reaction_complete.failed", {
2720
+ "app.slack.action": "reactions.add",
2721
+ "messaging.message.id": args.timestamp,
2722
+ ...getSlackErrorObservabilityAttributes(error)
2723
+ });
2724
+ }
2725
+ },
2726
+ keep: () => {
2727
+ shouldRemove = false;
2728
+ },
2729
+ stop: async () => {
2730
+ await removeProcessingReaction();
2731
+ }
2732
+ };
2733
+ }
2734
+ //#endregion
2414
2735
  //#region src/chat/slack/message-control.ts
2415
2736
  /** Leading Slack mention forms that may precede an explicit steering marker. */
2416
2737
  const LEADING_MENTION_BEFORE_STEERING_RE = /^(\s*(?:<@[^>]+>|@[A-Za-z0-9._-]+\b)[\s,:-]*)!!\s*/;
@@ -2945,9 +3266,14 @@ function createSlackTurnRuntime(deps) {
2945
3266
  });
2946
3267
  const combinedText = combineTurnText(queuedMessages, currentText);
2947
3268
  const turnIsExplicitMention = Boolean(message.isMention) || queuedMessages.some((queued) => queued.explicitMention);
2948
- const stopDecision = [...queuedMessages, currentText].map((entry) => getThreadStopDecision({
3269
+ const stopDecision = [...queuedMessages, {
3270
+ ...currentText,
3271
+ explicitMention: Boolean(message.isMention)
3272
+ }].map((entry) => getThreadStopDecision({
3273
+ botUserName: deps.assistantUserName,
2949
3274
  rawText: entry.rawText,
2950
- text: entry.userText
3275
+ text: entry.userText,
3276
+ isExplicitMention: entry.explicitMention
2951
3277
  })).find((decision) => decision?.shouldUnsubscribe);
2952
3278
  if (stopDecision) {
2953
3279
  const decision = {
@@ -4747,7 +5073,7 @@ async function presentSlackAutomationDetails(event, teamId) {
4747
5073
  }
4748
5074
  await client.entity.presentDetails({
4749
5075
  trigger_id: triggerId,
4750
- metadata: entity
5076
+ metadata: slackEntitySchema.parse(entity)
4751
5077
  });
4752
5078
  }
4753
5079
  //#endregion
@@ -4940,8 +5266,10 @@ async function routeParsedMessage(args) {
4940
5266
  const route = isDirectMessage || isMention ? "mention" : isSubscribed ? "subscribed" : void 0;
4941
5267
  if (!route) return;
4942
5268
  const stopDecision = getThreadStopDecision({
5269
+ botUserName: botConfig.userName,
4943
5270
  rawText: args.event.text ?? "",
4944
- text: args.event.text ?? ""
5271
+ text: args.event.text ?? "",
5272
+ isExplicitMention: isMention
4945
5273
  });
4946
5274
  if (stopDecision) {
4947
5275
  await handleSlackThreadStop({
@@ -5443,6 +5771,616 @@ function readSlackActionToken(message) {
5443
5771
  return token.success ? token.data : void 0;
5444
5772
  }
5445
5773
  //#endregion
5774
+ //#region src/chat/slack/cards.ts
5775
+ /** Select the Slack object renderer for a built-in card. */
5776
+ function renderSlackCard(card, conversationId) {
5777
+ switch (card.kind) {
5778
+ case "object": return renderSlackObjectCard(card, conversationId);
5779
+ case "automation": return renderSlackAutomationCard(card);
5780
+ }
5781
+ }
5782
+ //#endregion
5783
+ //#region src/chat/slack/footer.ts
5784
+ /** Render compact reply attribution for the Slack footer. */
5785
+ function formatReplyAttribution(attribution) {
5786
+ return attribution.detail ? `${attribution.label} · ${attribution.detail}` : attribution.label;
5787
+ }
5788
+ /**
5789
+ * Build the compact conversation footer for visible Slack reply surfaces.
5790
+ *
5791
+ * Detailed turn metrics stay in the dashboard instead of Slack-visible copy.
5792
+ */
5793
+ function buildSlackReplyFooter(args) {
5794
+ const items = [];
5795
+ const conversationId = args.conversationId?.trim();
5796
+ if (conversationId) {
5797
+ const idItem = {
5798
+ label: "ID",
5799
+ value: conversationId
5800
+ };
5801
+ const conversationUrl = getPluginSlackConversationLink(conversationId)?.url ?? getDashboardConversationLink(conversationId) ?? buildSentryConversationUrl(conversationId);
5802
+ if (conversationUrl) idItem.url = conversationUrl;
5803
+ items.push(idItem);
5804
+ }
5805
+ return items.length > 0 || args.replyAttribution ? {
5806
+ ...args.replyAttribution ? { attribution: args.replyAttribution } : void 0,
5807
+ items
5808
+ } : void 0;
5809
+ }
5810
+ /** Build Slack blocks for a reply chunk using the Slack-flavored markdown block for the body. */
5811
+ function buildSlackReplyBlocks(text, footer) {
5812
+ const blocks = text.trim() ? [{
5813
+ type: "markdown",
5814
+ text
5815
+ }] : [];
5816
+ if (footer && (footer.attribution || footer.items.length > 0)) {
5817
+ const attributionElements = footer.attribution ? [{
5818
+ type: "plain_text",
5819
+ text: formatReplyAttribution(footer.attribution)
5820
+ }] : [];
5821
+ blocks.push({
5822
+ type: "context",
5823
+ elements: [...attributionElements, ...footer.items.map((item) => ({
5824
+ type: "mrkdwn",
5825
+ text: item.url ? `*${escapeSlackMrkdwnText(item.label)}:* ${formatSlackLink(item.url, item.value)}` : `*${escapeSlackMrkdwnText(item.label)}:* ${escapeSlackMrkdwnText(item.value)}`
5826
+ }))]
5827
+ });
5828
+ }
5829
+ return blocks.length ? blocks : void 0;
5830
+ }
5831
+ //#endregion
5832
+ //#region src/chat/slack/output.ts
5833
+ const MAX_INLINE_CHARS = 2200;
5834
+ const MAX_INLINE_LINES = 45;
5835
+ const CONTINUED_MARKER = "\n\n[Continued below]";
5836
+ function countSlackLines(text) {
5837
+ if (!text) return 0;
5838
+ return text.split("\n").length;
5839
+ }
5840
+ function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
5841
+ return text.length <= maxChars && countSlackLines(text) <= maxLines;
5842
+ }
5843
+ function findSplitIndex(text, maxChars) {
5844
+ if (text.length <= maxChars) return text.length;
5845
+ const bounded = text.slice(0, maxChars);
5846
+ const newlineIndex = bounded.lastIndexOf("\n");
5847
+ if (newlineIndex > 0) return newlineIndex;
5848
+ const spaceIndex = bounded.lastIndexOf(" ");
5849
+ if (spaceIndex > 0) return spaceIndex;
5850
+ return maxChars;
5851
+ }
5852
+ function splitByLineBudget(text, maxLines) {
5853
+ if (maxLines <= 0) return "";
5854
+ const lines = text.split("\n");
5855
+ if (lines.length <= maxLines) return text;
5856
+ return lines.slice(0, maxLines).join("\n");
5857
+ }
5858
+ function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
5859
+ return {
5860
+ maxChars: Math.max(1, maxChars - suffix.length),
5861
+ maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
5862
+ };
5863
+ }
5864
+ function forceSplitBudget(text, budget) {
5865
+ const lineCount = countSlackLines(text);
5866
+ return {
5867
+ maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
5868
+ maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
5869
+ };
5870
+ }
5871
+ function getFenceContinuation(text) {
5872
+ let open;
5873
+ for (const line of text.split("\n")) {
5874
+ const trimmed = line.trimStart();
5875
+ const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
5876
+ if (!openerMatch) continue;
5877
+ if (!open) {
5878
+ open = {
5879
+ fence: openerMatch[1],
5880
+ openerLine: trimmed
5881
+ };
5882
+ continue;
5883
+ }
5884
+ if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) open = void 0;
5885
+ }
5886
+ if (!open) return null;
5887
+ return {
5888
+ closeSuffix: text.endsWith("\n") ? open.fence : `\n${open.fence}`,
5889
+ reopenPrefix: `${open.openerLine}\n`
5890
+ };
5891
+ }
5892
+ function appendSlackSuffix(text, marker) {
5893
+ return `${text}${getFenceContinuation(text)?.closeSuffix ?? ""}${marker}`;
5894
+ }
5895
+ function stripTrailingContinuationMarker(text) {
5896
+ return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -19) : text;
5897
+ }
5898
+ function takeSlackContinuationChunk(text, budget) {
5899
+ let { prefix, rest } = takeSlackInlinePrefix(text, budget);
5900
+ if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, budget)));
5901
+ let carryover = rest ? getFenceContinuation(prefix) : null;
5902
+ if (!carryover) return {
5903
+ prefix,
5904
+ rest
5905
+ };
5906
+ const carryoverBudget = reserveInlineBudgetForSuffix(`${carryover.closeSuffix}${CONTINUED_MARKER}`);
5907
+ ({prefix, rest} = takeSlackInlinePrefix(text, carryoverBudget));
5908
+ if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, carryoverBudget)));
5909
+ carryover = rest ? getFenceContinuation(prefix) : null;
5910
+ if (!carryover) return {
5911
+ prefix,
5912
+ rest
5913
+ };
5914
+ return {
5915
+ prefix,
5916
+ rest: `${carryover.reopenPrefix}${rest}`
5917
+ };
5918
+ }
5919
+ /** Return the first continuation-safe chunk plus any remaining text. */
5920
+ function takeSlackContinuationPrefix(text, options) {
5921
+ const budget = {
5922
+ maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
5923
+ maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
5924
+ };
5925
+ const { prefix, rest } = (() => {
5926
+ if (options?.forceSplit) return takeSlackContinuationChunk(text, budget);
5927
+ const initial = takeSlackInlinePrefix(text, budget);
5928
+ return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
5929
+ })();
5930
+ return {
5931
+ prefix,
5932
+ renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
5933
+ rest
5934
+ };
5935
+ }
5936
+ /**
5937
+ * Take the largest Slack-safe inline prefix from `text` under the configured
5938
+ * character and line budgets. Returns the consumed prefix plus remaining text.
5939
+ */
5940
+ function takeSlackInlinePrefix(text, options) {
5941
+ const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
5942
+ const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
5943
+ const normalized = text.replace(/\r\n?/g, "\n");
5944
+ if (!normalized) return {
5945
+ prefix: "",
5946
+ rest: ""
5947
+ };
5948
+ if (fitsInlineBudget(normalized, maxChars, maxLines)) return {
5949
+ prefix: normalized,
5950
+ rest: ""
5951
+ };
5952
+ const lineBounded = splitByLineBudget(normalized, maxLines);
5953
+ const cutIndex = findSplitIndex(lineBounded, maxChars);
5954
+ const prefix = lineBounded.slice(0, cutIndex).trimEnd();
5955
+ if (prefix) return {
5956
+ prefix,
5957
+ rest: normalized.slice(prefix.length).trimStart()
5958
+ };
5959
+ const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
5960
+ return {
5961
+ prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
5962
+ rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
5963
+ };
5964
+ }
5965
+ /**
5966
+ * Split a normalized Slack reply into multiple inline-safe thread messages.
5967
+ *
5968
+ * Replies split across more than two messages receive explicit continuation
5969
+ * markers on non-final chunks. When
5970
+ * `interrupted` is true, the final chunk receives an interruption marker.
5971
+ */
5972
+ function splitSlackReplyText(text, options) {
5973
+ const normalized = normalizeSlackReplyMarkdown(text);
5974
+ if (!normalized) return [];
5975
+ const chunks = [];
5976
+ const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
5977
+ let remaining = normalized;
5978
+ while (remaining) {
5979
+ if (options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining)) {
5980
+ chunks.push(options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining);
5981
+ break;
5982
+ }
5983
+ const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
5984
+ ...continuationBudget,
5985
+ forceSplit: true
5986
+ });
5987
+ chunks.push(renderedPrefix);
5988
+ remaining = rest;
5989
+ }
5990
+ if (chunks.length === 2) chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
5991
+ return chunks;
5992
+ }
5993
+ /**
5994
+ * Reserve enough inline budget for a continuation suffix on the current chunk.
5995
+ */
5996
+ function getSlackContinuationBudget() {
5997
+ return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
5998
+ }
5999
+ /** Normalize text for Slack and wrap it as a PostableMessage. */
6000
+ function buildSlackOutputMessage(text) {
6001
+ const normalized = normalizeSlackReplyMarkdown(text);
6002
+ if (!normalized) throw new Error(`Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`);
6003
+ return { markdown: normalized };
6004
+ }
6005
+ //#endregion
6006
+ //#region src/chat/slack/reply.ts
6007
+ /**
6008
+ * Send one destination-visible Slack reply.
6009
+ *
6010
+ * Chunks oversized text, builds compact attribution and conversation footer
6011
+ * context, and posts through the shared Slack outbound boundary.
6012
+ */
6013
+ async function sendSlackReply(args) {
6014
+ const posts = splitSlackReplyText(args.text).map((text) => ({
6015
+ text,
6016
+ cards: []
6017
+ }));
6018
+ const cards = (args.cards ?? []).map((card) => renderSlackCard(card, args.conversationId));
6019
+ for (let index = 0; index < cards.length; index += 5) {
6020
+ const group = cards.slice(index, index + 5);
6021
+ const lastPost = posts.at(-1);
6022
+ if (index === 0 && lastPost) lastPost.cards = group;
6023
+ else posts.push({
6024
+ text: "",
6025
+ cards: group
6026
+ });
6027
+ }
6028
+ const footer = buildSlackReplyFooter({
6029
+ conversationId: args.conversationId,
6030
+ replyAttribution: args.replyAttribution
6031
+ });
6032
+ const messageTs = [];
6033
+ let threadTs = args.threadTs;
6034
+ for (const [index, post] of posts.entries()) {
6035
+ const { text, cards } = post;
6036
+ const isFinalChunk = index === posts.length - 1;
6037
+ const blocks = buildSlackReplyBlocks(text, void 0) ?? [];
6038
+ for (const card of cards) if (!card.entity) blocks.push({
6039
+ type: "section",
6040
+ text: {
6041
+ type: "mrkdwn",
6042
+ text: card.text
6043
+ }
6044
+ });
6045
+ if (isFinalChunk || !text.trim()) blocks.push(...buildSlackReplyBlocks("", footer) ?? []);
6046
+ const entities = cards.flatMap((card) => card.entity ? [card.entity] : []);
6047
+ const accessibleText = [text, ...cards.map((card) => card.text)].filter(Boolean).join("\n\n");
6048
+ const fallbackText = isFinalChunk && args.replyAttribution ? `${accessibleText}\n\n${escapeSlackMrkdwnText(formatReplyAttribution(args.replyAttribution))}` : accessibleText;
6049
+ const response = await postSlackMessage({
6050
+ channelId: args.channelId,
6051
+ threadTs,
6052
+ text: fallbackText,
6053
+ ...blocks.length ? { blocks } : void 0,
6054
+ ...entities.length ? { entities } : void 0
6055
+ });
6056
+ if (response.ts) {
6057
+ messageTs.push(response.ts);
6058
+ threadTs ??= response.ts;
6059
+ }
6060
+ }
6061
+ return messageTs;
6062
+ }
6063
+ //#endregion
6064
+ //#region src/chat/slack/assistant-thread/status-render.ts
6065
+ const DEFAULT_STATUS_CONTEXTS = {
6066
+ thinking: "…",
6067
+ searching: "sources",
6068
+ reading: "task",
6069
+ reviewing: "results",
6070
+ drafting: "reply",
6071
+ running: "tasks"
6072
+ };
6073
+ function formatAssistantStatusText(verb, context) {
6074
+ const normalizedVerb = normalizeSlackStatusText(verb).trim().toLowerCase();
6075
+ const normalizedContext = normalizeSlackStatusText(context ?? "") || DEFAULT_STATUS_CONTEXTS[normalizedVerb] || "";
6076
+ if (!normalizedVerb) return truncateStatusText(normalizedContext || "Working");
6077
+ const displayVerb = `${normalizedVerb[0]?.toUpperCase() ?? ""}${normalizedVerb.slice(1)}`;
6078
+ return truncateStatusText(normalizedContext ? `${displayVerb} ${normalizedContext}` : displayVerb);
6079
+ }
6080
+ /** Build assistant progress text from a verb and optional context. */
6081
+ function makeAssistantStatus(verb, context) {
6082
+ return { text: formatAssistantStatusText(verb, context) };
6083
+ }
6084
+ /** Normalize a progress update into the visible Slack loading copy. */
6085
+ function renderAssistantStatus(args) {
6086
+ const visible = truncateStatusText(normalizeSlackStatusText(args.status.text));
6087
+ return {
6088
+ key: visible,
6089
+ visible
6090
+ };
6091
+ }
6092
+ /** Select and normalize the loading messages used for Slack status rotation. */
6093
+ function selectAssistantLoadingMessages(args) {
6094
+ const random = args.random ?? Math.random;
6095
+ const normalized = Array.from(new Set(args.messages.map((message) => truncateStatusText(normalizeSlackStatusText(message))).filter((message) => message.length > 0)));
6096
+ if (normalized.length === 0) return;
6097
+ const shuffled = [...normalized];
6098
+ for (let index = shuffled.length - 1; index > 0; index -= 1) {
6099
+ const otherIndex = Math.floor(random() * (index + 1));
6100
+ [shuffled[index], shuffled[otherIndex]] = [shuffled[otherIndex], shuffled[index]];
6101
+ }
6102
+ return shuffled.slice(0, 10);
6103
+ }
6104
+ //#endregion
6105
+ //#region src/chat/slack/assistant-thread/status-scheduler.ts
6106
+ const STATUS_UPDATE_DEBOUNCE_MS = 1e3;
6107
+ const STATUS_MIN_VISIBLE_MS = 1200;
6108
+ const STATUS_ROTATION_INTERVAL_MS = 3e4;
6109
+ /**
6110
+ * Pace assistant loading-state writes for a single turn.
6111
+ *
6112
+ * This layer owns only local scheduling policy: debounce, minimum visible
6113
+ * duration, refresh cadence, and write ordering. It deals in user-visible
6114
+ * progress copy; the transport decides how that maps onto Slack's `status`
6115
+ * versus `loading_messages` fields.
6116
+ */
6117
+ function createAssistantStatusScheduler(args) {
6118
+ const now = args.now ?? (() => Date.now());
6119
+ const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
6120
+ const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
6121
+ const random = args.random ?? Math.random;
6122
+ const loadingMessages = selectAssistantLoadingMessages({
6123
+ messages: args.loadingMessages ?? [],
6124
+ random
6125
+ });
6126
+ const defaultStatus = makeAssistantStatus("thinking");
6127
+ let active = false;
6128
+ let currentKey = "";
6129
+ let currentVisibleStatus = "";
6130
+ let currentLoadingMessages;
6131
+ let lastStatusAt = 0;
6132
+ let pendingStatus = null;
6133
+ let pendingKey = "";
6134
+ let pendingTimer = null;
6135
+ let rotationTimer = null;
6136
+ let inflightStatusUpdate = Promise.resolve();
6137
+ const enqueueStatusUpdate = (task) => {
6138
+ const request = inflightStatusUpdate.catch(() => void 0).then(async () => {
6139
+ await task();
6140
+ });
6141
+ inflightStatusUpdate = request.catch(() => void 0);
6142
+ return request;
6143
+ };
6144
+ const scheduleRotation = () => {
6145
+ if (rotationTimer) {
6146
+ clearTimer(rotationTimer);
6147
+ rotationTimer = null;
6148
+ }
6149
+ if (!active || !currentVisibleStatus) return;
6150
+ rotationTimer = setTimer(() => {
6151
+ rotationTimer = null;
6152
+ if (!active || !currentVisibleStatus) return;
6153
+ postStatus(currentVisibleStatus, currentLoadingMessages);
6154
+ }, STATUS_ROTATION_INTERVAL_MS);
6155
+ };
6156
+ const getLoadingMessagesForVisibleStatus = (visible) => visible ? [visible] : void 0;
6157
+ const getInitialStatusText = () => {
6158
+ if (loadingMessages?.length) return loadingMessages[0];
6159
+ return defaultStatus.text;
6160
+ };
6161
+ const haveSameLoadingMessages = (left, right) => {
6162
+ if (left === right) return true;
6163
+ if (!left || !right || left.length !== right.length) return false;
6164
+ return left.every((message, index) => message === right[index]);
6165
+ };
6166
+ const postStatus = async (text, nextLoadingMessages) => {
6167
+ if (!text && !currentVisibleStatus) return;
6168
+ currentVisibleStatus = text;
6169
+ currentLoadingMessages = nextLoadingMessages;
6170
+ lastStatusAt = now();
6171
+ scheduleRotation();
6172
+ await enqueueStatusUpdate(async () => {
6173
+ await args.sendStatus(text, nextLoadingMessages);
6174
+ });
6175
+ };
6176
+ const postRenderedStatus = async (status) => {
6177
+ const presentation = renderAssistantStatus({ status });
6178
+ const nextLoadingMessages = getLoadingMessagesForVisibleStatus(presentation.visible);
6179
+ currentKey = presentation.key;
6180
+ await postStatus(presentation.visible, nextLoadingMessages);
6181
+ };
6182
+ const clearPending = () => {
6183
+ if (pendingTimer) {
6184
+ clearTimer(pendingTimer);
6185
+ pendingTimer = null;
6186
+ }
6187
+ pendingStatus = null;
6188
+ pendingKey = "";
6189
+ };
6190
+ const flushPending = async () => {
6191
+ if (!active || !pendingStatus) {
6192
+ clearPending();
6193
+ return;
6194
+ }
6195
+ const next = pendingStatus;
6196
+ clearPending();
6197
+ if (renderAssistantStatus({ status: next }).key !== currentKey) await postRenderedStatus(next);
6198
+ };
6199
+ return {
6200
+ async clear() {
6201
+ active = false;
6202
+ clearPending();
6203
+ if (rotationTimer) {
6204
+ clearTimer(rotationTimer);
6205
+ rotationTimer = null;
6206
+ }
6207
+ currentKey = "";
6208
+ await postStatus("");
6209
+ },
6210
+ update(status) {
6211
+ if (!active) {
6212
+ active = true;
6213
+ clearPending();
6214
+ if (status) {
6215
+ postRenderedStatus(status);
6216
+ return;
6217
+ }
6218
+ currentKey = "initial";
6219
+ postStatus(getInitialStatusText(), loadingMessages);
6220
+ return;
6221
+ }
6222
+ if (!status) {
6223
+ clearPending();
6224
+ currentKey = "initial";
6225
+ postStatus(getInitialStatusText(), loadingMessages);
6226
+ return;
6227
+ }
6228
+ const presentation = renderAssistantStatus({ status });
6229
+ if (!presentation.visible) return;
6230
+ if (presentation.key === currentKey || presentation.key === pendingKey) return;
6231
+ if (presentation.visible === currentVisibleStatus) {
6232
+ clearPending();
6233
+ currentKey = presentation.key;
6234
+ const nextLoadingMessages = getLoadingMessagesForVisibleStatus(presentation.visible);
6235
+ if (!haveSameLoadingMessages(currentLoadingMessages, nextLoadingMessages)) postStatus(presentation.visible, nextLoadingMessages);
6236
+ return;
6237
+ }
6238
+ const elapsed = now() - lastStatusAt;
6239
+ const waitMs = Math.max(STATUS_UPDATE_DEBOUNCE_MS - elapsed, STATUS_MIN_VISIBLE_MS - elapsed, 0);
6240
+ if (waitMs <= 0) {
6241
+ clearPending();
6242
+ postRenderedStatus(status);
6243
+ return;
6244
+ }
6245
+ pendingStatus = status;
6246
+ pendingKey = presentation.key;
6247
+ if (pendingTimer) return;
6248
+ pendingTimer = setTimer(() => {
6249
+ pendingTimer = null;
6250
+ flushPending();
6251
+ }, Math.max(1, waitMs));
6252
+ }
6253
+ };
6254
+ }
6255
+ //#endregion
6256
+ //#region src/chat/slack/assistant-thread/status-send.ts
6257
+ /**
6258
+ * Slack's assistant loading UI accepts both `status` and `loading_messages`,
6259
+ * but product policy keeps `status` stable and generic. User-visible progress
6260
+ * copy belongs in `loading_messages`.
6261
+ */
6262
+ const SLACK_ASSISTANT_ACTIVE_STATUS = "is working on your request...";
6263
+ /**
6264
+ * Build a best-effort sender for Slack's assistant loading state.
6265
+ *
6266
+ * The `text` argument is internal progress copy. This transport maps it onto
6267
+ * Slack's loading surface and keeps the raw Slack `status` field fixed.
6268
+ */
6269
+ function createSlackAdapterStatusSender(args) {
6270
+ const adapter = args.getSlackAdapter();
6271
+ const boundToken = getSlackAdapterRequestToken(adapter);
6272
+ return async (text, loadingMessages) => {
6273
+ const channelId = args.channelId;
6274
+ const threadTs = args.threadTs;
6275
+ if (!channelId || !threadTs) return;
6276
+ const normalizedChannelId = normalizeSlackConversationId(channelId);
6277
+ if (!normalizedChannelId) return;
6278
+ const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
6279
+ try {
6280
+ await runWithBoundSlackToken(adapter, boundToken, () => adapter.setAssistantStatus(normalizedChannelId, threadTs, text ? SLACK_ASSISTANT_ACTIVE_STATUS : "", nextLoadingMessages));
6281
+ } catch (error) {
6282
+ logAssistantStatusFailure({
6283
+ status: text,
6284
+ error,
6285
+ channelId,
6286
+ normalizedChannelId,
6287
+ threadTs
6288
+ });
6289
+ }
6290
+ };
6291
+ }
6292
+ /**
6293
+ * Build a best-effort sender for Slack's assistant loading state over raw Web
6294
+ * API calls. As with the adapter-backed path, the dynamic copy goes to
6295
+ * `loading_messages` while `status` stays fixed and generic.
6296
+ */
6297
+ function createSlackWebApiStatusSender(args) {
6298
+ const getClient = args.getSlackClient ?? getSlackClient;
6299
+ return async (text, loadingMessages) => {
6300
+ const channelId = args.channelId;
6301
+ const threadTs = args.threadTs;
6302
+ if (!channelId || !threadTs) return;
6303
+ const normalizedChannelId = normalizeSlackConversationId(channelId);
6304
+ if (!normalizedChannelId) return;
6305
+ const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
6306
+ try {
6307
+ await getClient().assistant.threads.setStatus({
6308
+ channel_id: normalizedChannelId,
6309
+ thread_ts: threadTs,
6310
+ status: text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
6311
+ ...nextLoadingMessages ? { loading_messages: nextLoadingMessages } : void 0
6312
+ });
6313
+ } catch (error) {
6314
+ logAssistantStatusFailure({
6315
+ status: text,
6316
+ error,
6317
+ channelId,
6318
+ normalizedChannelId,
6319
+ threadTs
6320
+ });
6321
+ }
6322
+ };
6323
+ }
6324
+ function getSlackAdapterRequestToken(adapter) {
6325
+ const token = adapter.requestContext?.getStore()?.token;
6326
+ if (typeof token !== "string") return;
6327
+ return token.trim() || void 0;
6328
+ }
6329
+ async function runWithBoundSlackToken(adapter, token, task) {
6330
+ if (!token) return await task();
6331
+ return await adapter.withBotToken(token, task);
6332
+ }
6333
+ function logAssistantStatusFailure(args) {
6334
+ logWarn("assistant.status.update.failed", {
6335
+ "app.slack.status_text": args.status || "(clear)",
6336
+ "app.slack.channel_id_raw": args.channelId,
6337
+ "app.slack.channel_id": args.normalizedChannelId,
6338
+ "app.slack.thread_ts": args.threadTs,
6339
+ "exception.message": args.error instanceof Error ? args.error.message : String(args.error)
6340
+ });
6341
+ }
6342
+ //#endregion
6343
+ //#region src/chat/slack/assistant-thread/status.ts
6344
+ /**
6345
+ * Create a Slack adapter-backed session for Slack's assistant loading state.
6346
+ *
6347
+ * The session accepts internal progress updates and leaves it to the sender to
6348
+ * map them onto Slack's fixed generic `status` field plus dynamic
6349
+ * `loading_messages`.
6350
+ */
6351
+ function createSlackAdapterAssistantStatusSession(args) {
6352
+ return createAssistantStatusScheduler({
6353
+ sendStatus: createSlackAdapterStatusSender({
6354
+ channelId: args.channelId,
6355
+ threadTs: args.threadTs,
6356
+ getSlackAdapter: args.getSlackAdapter
6357
+ }),
6358
+ loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
6359
+ now: args.now,
6360
+ setTimer: args.setTimer,
6361
+ clearTimer: args.clearTimer,
6362
+ random: args.random
6363
+ });
6364
+ }
6365
+ /**
6366
+ * Create a Web API-backed session for Slack's assistant loading state in
6367
+ * resume/callback flows that do not use the adapter thread object.
6368
+ */
6369
+ function createSlackWebApiAssistantStatusSession(args) {
6370
+ return createAssistantStatusScheduler({
6371
+ sendStatus: createSlackWebApiStatusSender({
6372
+ channelId: args.channelId,
6373
+ threadTs: args.threadTs,
6374
+ getSlackClient: args.getSlackClient
6375
+ }),
6376
+ loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
6377
+ now: args.now,
6378
+ setTimer: args.setTimer,
6379
+ clearTimer: args.clearTimer,
6380
+ random: args.random
6381
+ });
6382
+ }
6383
+ //#endregion
5446
6384
  //#region src/chat/slack/assistant-thread/title.ts
5447
6385
  /** Set-once DM title projection for the current process. */
5448
6386
  const projectedAssistantTitles = /* @__PURE__ */ new Set();
@@ -5481,6 +6419,21 @@ async function maybeSyncAssistantTitle(args) {
5481
6419
  }
5482
6420
  }
5483
6421
  //#endregion
6422
+ //#region src/chat/services/auth-pause-response.ts
6423
+ const MAX_AUTH_REQUEST_LENGTH = 200;
6424
+ function formatAuthRequest(requestText) {
6425
+ const normalized = requestText.replace(/\s+/g, " ").trim();
6426
+ if (!normalized) return;
6427
+ return (normalized.length > MAX_AUTH_REQUEST_LENGTH ? `${normalized.slice(0, 199).trimEnd()}…` : normalized).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
6428
+ }
6429
+ /** Build the visible Slack thread note for an auth-paused turn. */
6430
+ function buildAuthPauseResponse(slackUserId, providerDisplayName, requestText) {
6431
+ const mention = slackUserId ? `<@${slackUserId}> ` : "";
6432
+ const request = requestText ? formatAuthRequest(requestText) : void 0;
6433
+ if (!request) return `${mention}I'll need you to authorize ${providerDisplayName}. I sent you a link.`;
6434
+ return `${mention}I need access to ${providerDisplayName} to continue.\n\n*Why:* ${request}\n\nI sent you a link.`;
6435
+ }
6436
+ //#endregion
5484
6437
  //#region src/chat/services/provider-default-config.ts
5485
6438
  const GITHUB_REPO_PART = String.raw`[A-Za-z0-9_.-]*[A-Za-z0-9_-]`;
5486
6439
  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 +7883,506 @@ function createJuniorSlackAdapter(config) {
6930
7883
  return createSlackAdapter(config);
6931
7884
  }
6932
7885
  //#endregion
7886
+ //#region src/chat/providers/slack/resume.ts
7887
+ /**
7888
+ * Resume a Slack Turn from saved Run state.
7889
+ *
7890
+ * Resumed Turns load their saved Run while holding the Slack thread lock.
7891
+ * Status notices are best effort. Completed assistant replies and
7892
+ * authorization pause notices use `sendSlackReply` so the footer stays
7893
+ * consistent.
7894
+ */
7895
+ function resolveReplyTimeoutMs() {
7896
+ const raw = process.env.EVAL_AGENT_REPLY_TIMEOUT_MS?.trim();
7897
+ if (!raw) return;
7898
+ const parsed = Number.parseInt(raw, 10);
7899
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
7900
+ }
7901
+ async function postSlackMessageBestEffort(channelId, threadTs, text, conversationId, replyAttribution) {
7902
+ try {
7903
+ if (conversationId) {
7904
+ await sendSlackReply({
7905
+ channelId,
7906
+ conversationId,
7907
+ replyAttribution,
7908
+ text,
7909
+ threadTs
7910
+ });
7911
+ return;
7912
+ }
7913
+ await postSlackMessage({
7914
+ channelId,
7915
+ threadTs,
7916
+ text
7917
+ });
7918
+ } catch {}
7919
+ }
7920
+ /** Create a read-only configuration service from persisted values. */
7921
+ function createReadOnlyConfigService(rawValues) {
7922
+ const values = Object.fromEntries(Object.entries(rawValues).map(([key, value]) => [key, configValueSchema.parse(value)]));
7923
+ const entries = Object.entries(values).map(([key, value]) => ({
7924
+ key,
7925
+ value,
7926
+ scope: "location",
7927
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7928
+ }));
7929
+ return {
7930
+ get: async (key) => entries.find((entry) => entry.key === key),
7931
+ set: async () => {
7932
+ throw new Error("Configuration is read-only for a resumed Run");
7933
+ },
7934
+ unset: async () => false,
7935
+ list: async ({ prefix } = {}) => entries.filter((entry) => !prefix || entry.key.startsWith(prefix)),
7936
+ resolve: async (key) => values[key],
7937
+ resolveValues: async ({ keys, prefix } = {}) => {
7938
+ const filtered = {};
7939
+ for (const [key, value] of Object.entries(values)) {
7940
+ if (prefix && !key.startsWith(prefix)) continue;
7941
+ if (keys && !keys.includes(key)) continue;
7942
+ filtered[key] = value;
7943
+ }
7944
+ return filtered;
7945
+ }
7946
+ };
7947
+ }
7948
+ function getResumeLogContext(args) {
7949
+ const actor = args.run?.actor;
7950
+ return {
7951
+ conversationId: args.conversationId,
7952
+ messageConversationId: args.conversationId,
7953
+ userId: isUserActor(actor) ? actor.userId : void 0,
7954
+ userName: isUserActor(actor) ? actor.userName : void 0,
7955
+ destinationName: args.channelId,
7956
+ assistantUserName: botConfig.userName,
7957
+ modelId: defaultModelId(botConfig)
7958
+ };
7959
+ }
7960
+ async function postResumeFailureReply(args) {
7961
+ await postSlackMessage({
7962
+ channelId: args.channelId,
7963
+ threadTs: args.threadTs,
7964
+ text: buildTurnErrorResponse(args.error, args.eventId)
7965
+ });
7966
+ }
7967
+ async function handleResumeFailure(args) {
7968
+ const turnLifecycle = getTurnLifecycle();
7969
+ const capturedEventId = logException(args.error, args.eventName);
7970
+ const eventId = requireTurnFailureEventId(capturedEventId, args.eventName);
7971
+ let failureStatePersistError;
7972
+ try {
7973
+ await args.resume.onFailure?.(args.error);
7974
+ } catch (persistError) {
7975
+ const persistEventId = logException(persistError, "slack.resume.failure_state_persist.failed", { "app.error.original_event_id": eventId });
7976
+ try {
7977
+ await turnLifecycle.fail({
7978
+ conversationId: args.resume.conversationId,
7979
+ turnId: args.resume.turnId,
7980
+ createdAtMs: Date.now(),
7981
+ failureCode: "persistence_failed",
7982
+ ...persistEventId ? { eventId: persistEventId } : void 0
7983
+ });
7984
+ } catch (lifecycleError) {
7985
+ logException(lifecycleError, "slack.resume.failure_lifecycle_persist.failed", { "app.error.original_event_id": eventId });
7986
+ }
7987
+ failureStatePersistError = persistError;
7988
+ }
7989
+ try {
7990
+ await postResumeFailureReply({
7991
+ channelId: args.resume.channelId,
7992
+ threadTs: args.resume.threadTs,
7993
+ eventId,
7994
+ error: args.error
7995
+ });
7996
+ } catch (deliveryError) {
7997
+ const deliveryEventId = logException(deliveryError, "slack.resume.failure_delivery.failed", { "app.error.original_event_id": eventId });
7998
+ try {
7999
+ await turnLifecycle.fail({
8000
+ conversationId: args.resume.conversationId,
8001
+ turnId: args.resume.turnId,
8002
+ createdAtMs: Date.now(),
8003
+ failureCode: "delivery_failed",
8004
+ ...deliveryEventId ? { eventId: deliveryEventId } : void 0
8005
+ });
8006
+ } catch (lifecycleError) {
8007
+ logException(lifecycleError, "slack.resume.failure_lifecycle_persist.failed", { "app.error.original_event_id": eventId });
8008
+ }
8009
+ throw deliveryError;
8010
+ }
8011
+ if (failureStatePersistError) throw failureStatePersistError;
8012
+ await turnLifecycle.fail({
8013
+ conversationId: args.resume.conversationId,
8014
+ turnId: args.resume.turnId,
8015
+ createdAtMs: Date.now(),
8016
+ failureCode: args.failureCode,
8017
+ eventId
8018
+ });
8019
+ }
8020
+ function buildResumedRun(args, statusSession, delivery) {
8021
+ const savedRun = args.run;
8022
+ if (!savedRun) throw new TypeError("Slack resume requires a saved Run");
8023
+ if (!savedRun.source) throw new TypeError("Slack resume requires a Run Source");
8024
+ const source = savedRun.source;
8025
+ if (savedRun.destination.platform !== "slack") throw new TypeError("Slack resume requires a Slack Destination");
8026
+ const requestDeadline = getTurnRequestDeadline();
8027
+ const threadId = args.conversationId;
8028
+ const persistedLocationConfiguration = savedRun.environment?.locationConfiguration ?? (savedRun.environment?.configuration ? createReadOnlyConfigService(savedRun.environment.configuration) : void 0);
8029
+ const priorOnEvent = savedRun.onEvent;
8030
+ return {
8031
+ ...savedRun,
8032
+ conversationId: args.conversationId,
8033
+ turnId: args.turnId,
8034
+ instruction: {
8035
+ ...savedRun.instruction ?? { text: args.messageText },
8036
+ text: args.messageText
8037
+ },
8038
+ source: source.kind === "slack" ? {
8039
+ ...source,
8040
+ channelId: args.channelId,
8041
+ ...args.threadTs ? { threadTs: args.threadTs } : void 0
8042
+ } : source,
8043
+ deadlineAtMs: savedRun.deadlineAtMs ?? requestDeadline?.deadlineAtMs,
8044
+ environment: {
8045
+ ...savedRun.environment,
8046
+ locationConfiguration: persistedLocationConfiguration
8047
+ },
8048
+ onEvent: async (event) => {
8049
+ if (event.type === "status") statusSession.update({ text: event.text });
8050
+ await priorOnEvent?.(event);
8051
+ },
8052
+ ...savedRun.dispatch?.outcomes?.length === 0 ? void 0 : { delivery },
8053
+ durability: {
8054
+ ...savedRun.durability,
8055
+ onSandboxRefChanged: async (sandboxRef) => {
8056
+ await persistThreadStateById(threadId, { sandboxRef });
8057
+ await savedRun.durability?.onSandboxRefChanged?.(sandboxRef);
8058
+ }
8059
+ }
8060
+ };
8061
+ }
8062
+ /**
8063
+ * Resume a paused Slack Turn.
8064
+ *
8065
+ * The caller must hold the conversation work lease. This function does not
8066
+ * acquire another execution lock. A started resume owns its completion work.
8067
+ * Returns false only when `beforeStart` proves the resume is stale before
8068
+ * generation begins.
8069
+ */
8070
+ async function resumeSlackTurn(args) {
8071
+ return withLogContext(getResumeLogContext(args), () => resumeSlackTurnInContext(args));
8072
+ }
8073
+ async function resumeSlackTurnInContext(args) {
8074
+ const status = createSlackWebApiAssistantStatusSession({
8075
+ channelId: args.channelId,
8076
+ threadTs: args.threadTs
8077
+ });
8078
+ let processingReaction;
8079
+ let deferredAuthInfo;
8080
+ let deferredPauseHandler;
8081
+ let deferredFailureHandler;
8082
+ let runResultHandled = false;
8083
+ let assistantMessageDelivered = false;
8084
+ let shouldScheduleCompletedPluginTasks = false;
8085
+ let postDeliveryCommitError;
8086
+ const turnLifecycle = getTurnLifecycle();
8087
+ let failureCode = "agent_run_failed";
8088
+ let runArgs = args;
8089
+ try {
8090
+ const preparedArgs = await args.beforeStart?.();
8091
+ if (preparedArgs === false) return false;
8092
+ if (preparedArgs) runArgs = {
8093
+ ...args,
8094
+ ...preparedArgs
8095
+ };
8096
+ setTags(getResumeLogContext(runArgs));
8097
+ const savedRun = runArgs.run;
8098
+ if (!savedRun) throw new Error("Resumed Turn requires a saved Run");
8099
+ const credentialContext = savedRun.credentialContext;
8100
+ if (!credentialContext) throw new Error("Resumed Run requires a credential context");
8101
+ const routingActor = savedRun.actor;
8102
+ let resumeActor;
8103
+ if ("type" in credentialContext.actor) {
8104
+ if (!isUserActor(routingActor) || credentialContext.actor.userId !== routingActor.userId) throw new Error("Resumed Run credential actor must match its Actor");
8105
+ resumeActor = routingActor;
8106
+ } else {
8107
+ if (routingActor && (routingActor.platform !== "system" || routingActor.name !== credentialContext.actor.name)) throw new Error("Resumed Run system credential actor must match its Actor");
8108
+ resumeActor = credentialContext.actor;
8109
+ }
8110
+ if (runArgs.messageTs) processingReaction = await startProcessingReactionForMessage({
8111
+ channelId: runArgs.channelId,
8112
+ timestamp: runArgs.messageTs
8113
+ });
8114
+ if (runArgs.initialText) await postSlackMessageBestEffort(runArgs.channelId, runArgs.threadTs, runArgs.initialText);
8115
+ status.update(runArgs.initialStatus);
8116
+ const conversationId = runArgs.conversationId;
8117
+ const visibility = (await getConversationStore().get({ conversationId }))?.visibility;
8118
+ const visibleConversationId = args.conversationId;
8119
+ const sessionId = runArgs.turnId;
8120
+ let deliveryConversation;
8121
+ let acceptedDeliveryId;
8122
+ /** Load the resumed conversation once for ordered delivery recording. */
8123
+ const getDeliveryConversation = async () => {
8124
+ if (deliveryConversation) {
8125
+ const userMessageId = getTurnUserMessage(deliveryConversation, sessionId)?.id;
8126
+ return {
8127
+ conversation: deliveryConversation,
8128
+ sessionId,
8129
+ ...userMessageId ? { userMessageId } : void 0
8130
+ };
8131
+ }
8132
+ const persistedState = await getPersistedThreadState(visibleConversationId);
8133
+ const conversation = coerceThreadConversationState(persistedState);
8134
+ await hydrateConversationMessages({
8135
+ conversation,
8136
+ conversationId
8137
+ });
8138
+ deliveryConversation = conversation;
8139
+ const userMessageId = getTurnUserMessage(conversation, sessionId)?.id;
8140
+ return {
8141
+ conversation,
8142
+ sessionId,
8143
+ ...userMessageId ? { userMessageId } : void 0
8144
+ };
8145
+ };
8146
+ /** Post and record one completed assistant message for the resumed turn. */
8147
+ const deliverAssistantMessage = async (reply) => {
8148
+ const message = typeof reply === "string" ? void 0 : reply;
8149
+ const text = typeof reply === "string" ? reply : getAssistantReplyText(reply);
8150
+ if (!text?.trim()) return;
8151
+ failureCode = "delivery_failed";
8152
+ const cards = await loadPendingMessageCards(conversationId);
8153
+ const deliveryState = await getDeliveryConversation();
8154
+ let slackMessageTs = [];
8155
+ try {
8156
+ const outcomes = runArgs.run?.dispatch?.outcomes;
8157
+ if (outcomes) for (const outcome of outcomes) slackMessageTs.push(...await sendSlackReply({
8158
+ cards,
8159
+ channelId: outcome.destination.channelId,
8160
+ conversationId: runArgs.conversationId,
8161
+ replyAttribution: runArgs.run?.dispatch?.replyAttribution,
8162
+ text,
8163
+ threadTs: outcome.destination.threadTs
8164
+ }));
8165
+ else slackMessageTs = await sendSlackReply({
8166
+ cards,
8167
+ channelId: runArgs.channelId,
8168
+ conversationId: runArgs.conversationId,
8169
+ replyAttribution: runArgs.run?.dispatch?.replyAttribution,
8170
+ text,
8171
+ threadTs: runArgs.threadTs
8172
+ });
8173
+ } catch (error) {
8174
+ if (isRetryableSlackPostError(error)) throw new RetryableDeliveryError(error);
8175
+ throw error;
8176
+ }
8177
+ const messageTs = slackMessageTs.at(-1);
8178
+ assistantMessageDelivered = true;
8179
+ acceptedDeliveryId = messageTs;
8180
+ const recordedMessageId = recordDeliveredAssistantMessage({
8181
+ cards,
8182
+ conversation: deliveryState.conversation,
8183
+ sessionId: deliveryState.sessionId,
8184
+ source: "slack",
8185
+ text,
8186
+ userMessageId: deliveryState.userMessageId
8187
+ });
8188
+ if (messageTs) markConversationMessage(deliveryState.conversation, recordedMessageId, { slackTs: messageTs });
8189
+ try {
8190
+ const destination = runArgs.run?.destination;
8191
+ const providerConversationIds = runArgs.threadTs ? [runArgs.threadTs] : slackMessageTs;
8192
+ await persistWithRetry(() => commitAcceptedReply({
8193
+ ...message ? { agentMessage: message } : void 0,
8194
+ conversation: deliveryState.conversation,
8195
+ conversationMessageId: recordedMessageId,
8196
+ conversationId,
8197
+ ...runArgs.run?.dispatch && destination?.platform === "slack" && providerConversationIds.length > 0 ? { providerConversationBindings: providerConversationIds.map((providerConversationId) => ({
8198
+ provider: "slack",
8199
+ providerDestinationId: destination.channelId,
8200
+ providerTenantId: destination.teamId,
8201
+ providerConversationId
8202
+ })) } : void 0
8203
+ }));
8204
+ } catch (error) {
8205
+ logException(error, "slack.resume.assistant_message_post_delivery_persist.failed");
8206
+ }
8207
+ const dispatchId = savedRun.dispatch?.id;
8208
+ if (messageTs && dispatchId) try {
8209
+ await persistWithRetry(() => recordTurnSummary({
8210
+ conversationId: runArgs.conversationId,
8211
+ destination: savedRun.destination,
8212
+ destinationVisibility: visibility,
8213
+ dispatchId,
8214
+ resultMessageId: messageTs,
8215
+ turnId: runArgs.turnId,
8216
+ sliceId: runArgs.sliceId ?? 1,
8217
+ source: savedRun.source,
8218
+ state: "running",
8219
+ surface: savedRun.surface ?? "slack"
8220
+ }));
8221
+ } catch (error) {
8222
+ logException(error, "agent.turn.delivery_receipt_persist.failed");
8223
+ }
8224
+ failureCode = "agent_run_failed";
8225
+ };
8226
+ const deliveryState = await getDeliveryConversation();
8227
+ assistantMessageDelivered = turnHasReply(deliveryState.conversation, sessionId);
8228
+ const run = buildResumedRun(runArgs, status, deliverAssistantMessage);
8229
+ if (runArgs.inputMessageIds?.length) await turnLifecycle.start({
8230
+ conversationId: runArgs.conversationId,
8231
+ turnId: runArgs.turnId,
8232
+ createdAtMs: Date.now(),
8233
+ inputMessageIds: [...new Set(runArgs.inputMessageIds)],
8234
+ surface: "slack"
8235
+ });
8236
+ const replyTimeoutMs = resolveReplyTimeoutMs();
8237
+ const outcome = await runArgs.executeTurn(run, async (result) => {
8238
+ const finalized = finalizeFailedTurnReplyWithEvent({
8239
+ reply: result,
8240
+ logException
8241
+ });
8242
+ const reply = finalized.reply;
8243
+ const dispatchErrorMessage = run.dispatch && reply.diagnostics.outcome !== "success" ? reply.diagnostics.errorMessage ?? `Agent turn ended with ${reply.diagnostics.outcome}.` : void 0;
8244
+ if (reply.diagnostics.outcome !== "success") await deliverAssistantMessage(reply.text);
8245
+ runResultHandled = true;
8246
+ await status.clear();
8247
+ failureCode = "persistence_failed";
8248
+ if (reply.piMessages?.length) await saveTurnCheckpoint({
8249
+ mode: "completed",
8250
+ conversationId: runArgs.conversationId,
8251
+ turnId: runArgs.turnId,
8252
+ messages: reply.piMessages,
8253
+ durationMs: reply.diagnostics.durationMs,
8254
+ usage: reply.diagnostics.usage,
8255
+ destination: run.destination,
8256
+ destinationVisibility: visibility,
8257
+ dispatchId: run.dispatch?.id,
8258
+ dispatchOutcome: reply.diagnostics.outcome === "success" ? "completed" : "failed",
8259
+ ...dispatchErrorMessage ? { errorMessage: dispatchErrorMessage } : void 0,
8260
+ ...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : void 0,
8261
+ source: run.source,
8262
+ actor: resumeActor,
8263
+ surface: run.surface ?? "slack",
8264
+ sliceId: runArgs.sliceId
8265
+ });
8266
+ else if (run.dispatch?.id) await recordTurnSummary({
8267
+ conversationId: runArgs.conversationId,
8268
+ destination: run.destination,
8269
+ destinationVisibility: visibility,
8270
+ dispatchId: run.dispatch?.id,
8271
+ dispatchOutcome: reply.diagnostics.outcome === "success" ? "completed" : "failed",
8272
+ ...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : void 0,
8273
+ turnId: runArgs.turnId,
8274
+ sliceId: runArgs.sliceId ?? 1,
8275
+ source: run.source,
8276
+ state: reply.diagnostics.outcome === "success" ? "completed" : "failed",
8277
+ surface: run.surface ?? "slack"
8278
+ });
8279
+ await runArgs.commitResult?.(reply);
8280
+ if (reply.diagnostics.outcome === "success") {
8281
+ shouldScheduleCompletedPluginTasks = true;
8282
+ return { outcome: assistantMessageDelivered ? "success" : "no_reply" };
8283
+ }
8284
+ return {
8285
+ outcome: "failed",
8286
+ failureCode: "model_execution_failed",
8287
+ ...finalized.eventId ? { eventId: finalized.eventId } : void 0,
8288
+ ...finalized.failureReason ? { failureReason: finalized.failureReason } : void 0
8289
+ };
8290
+ }, replyTimeoutMs);
8291
+ if (outcome.status !== "completed") {
8292
+ await status.clear();
8293
+ const onAuthPause = runArgs.onAuthPause;
8294
+ const onSuspend = runArgs.onSuspend;
8295
+ if (outcome.status === "awaiting_auth" && onAuthPause) {
8296
+ deferredAuthInfo = {
8297
+ providerDisplayName: outcome.providerDisplayName,
8298
+ actorId: isUserActor(resumeActor) ? resumeActor.userId : void 0,
8299
+ ...outcome.requestText ? { requestText: outcome.requestText } : void 0
8300
+ };
8301
+ deferredPauseHandler = async () => {
8302
+ await onAuthPause({ providerDisplayName: outcome.providerDisplayName });
8303
+ };
8304
+ } else if (outcome.status === "suspended") {
8305
+ if (outcome.reason === "timeout" || savedRun.durability?.shouldYield?.()) throw new CooperativeTurnYieldError();
8306
+ if (onSuspend) deferredPauseHandler = async () => {
8307
+ await onSuspend(outcome.resumeVersion);
8308
+ };
8309
+ else deferredFailureHandler = async () => {
8310
+ await handleResumeFailure({
8311
+ error: /* @__PURE__ */ new Error("Resumed Run ended suspended without onSuspend"),
8312
+ eventName: "slack.resume.turn.failed",
8313
+ failureCode: "agent_run_failed",
8314
+ resume: runArgs
8315
+ });
8316
+ };
8317
+ } else deferredFailureHandler = async () => {
8318
+ await handleResumeFailure({
8319
+ error: /* @__PURE__ */ new Error(`Resumed Run ended ${outcome.status} without its required callback`),
8320
+ eventName: "slack.resume.turn.failed",
8321
+ failureCode: "agent_run_failed",
8322
+ resume: runArgs
8323
+ });
8324
+ };
8325
+ }
8326
+ if (shouldScheduleCompletedPluginTasks) try {
8327
+ const params = {
8328
+ conversationId: runArgs.conversationId,
8329
+ sessionId: runArgs.turnId
8330
+ };
8331
+ await (runArgs.scheduleSessionCompletedPluginTasks ?? scheduleSessionCompletedPluginTasks)(params);
8332
+ } catch (scheduleError) {
8333
+ logException(scheduleError, "plugin.session.completed_task_schedule.failed");
8334
+ }
8335
+ } catch (error) {
8336
+ await status.clear();
8337
+ const runError = error instanceof AgentRunError ? error.cause : error;
8338
+ if (isCooperativeTurnYieldError(runError)) throw runError;
8339
+ if (runResultHandled) {
8340
+ postDeliveryCommitError = error;
8341
+ try {
8342
+ await runArgs.onPostDeliveryCommitFailure?.(error);
8343
+ } catch (terminalizeError) {
8344
+ logException(terminalizeError, "slack.resume.post_delivery_terminalize.failed");
8345
+ }
8346
+ } else deferredFailureHandler = async () => {
8347
+ await handleResumeFailure({
8348
+ error: runError,
8349
+ eventName: "slack.resume.turn.failed",
8350
+ failureCode,
8351
+ resume: runArgs
8352
+ });
8353
+ };
8354
+ } finally {
8355
+ if (runResultHandled) await processingReaction?.complete();
8356
+ else await processingReaction?.stop();
8357
+ }
8358
+ if (postDeliveryCommitError) {
8359
+ const eventId = logException(postDeliveryCommitError, "slack.resume.success_handler.failed");
8360
+ await turnLifecycle.fail({
8361
+ conversationId: runArgs.conversationId,
8362
+ turnId: runArgs.turnId,
8363
+ createdAtMs: Date.now(),
8364
+ failureCode: "persistence_failed",
8365
+ ...eventId ? { eventId } : void 0
8366
+ });
8367
+ throw postDeliveryCommitError;
8368
+ }
8369
+ if (deferredPauseHandler) try {
8370
+ await deferredPauseHandler();
8371
+ if (deferredAuthInfo) await postSlackMessageBestEffort(runArgs.channelId, runArgs.threadTs, buildAuthPauseResponse(deferredAuthInfo.actorId, deferredAuthInfo.providerDisplayName, deferredAuthInfo.requestText), runArgs.conversationId, runArgs.run?.dispatch?.replyAttribution);
8372
+ return true;
8373
+ } catch (pauseError) {
8374
+ await handleResumeFailure({
8375
+ error: pauseError,
8376
+ eventName: "slack.resume.pause_handler.failed",
8377
+ failureCode: "persistence_failed",
8378
+ resume: runArgs
8379
+ });
8380
+ return true;
8381
+ }
8382
+ if (deferredFailureHandler) await deferredFailureHandler();
8383
+ return true;
8384
+ }
8385
+ //#endregion
6933
8386
  //#region src/chat/task-execution/paused-turn.ts
6934
8387
  /**
6935
8388
  * Worker continue for paused turns under the conversation lease.
@@ -7041,7 +8494,7 @@ async function resolveSlackResumeUserActor(args) {
7041
8494
  *
7042
8495
  * Sources, in order:
7043
8496
  * 1. Actor saved on the Turn.
7044
- * 2. Actor and credentials supplied by dispatch or OAuth.
8497
+ * 2. Actor and credentials supplied by dispatch.
7045
8498
  * 3. Legacy Event or Slack Message data.
7046
8499
  *
7047
8500
  * TODO(dcramer): Remove the routing and Message Actor fallbacks after no
@@ -7070,7 +8523,7 @@ async function resolveResumeExecutionIdentity(args) {
7070
8523
  };
7071
8524
  }
7072
8525
  function isPausedTurn(summary) {
7073
- return summary.state === "paused" && (summary.resumeReason === "timeout" || summary.resumeReason === "yield" || summary.resumeReason === "retry");
8526
+ return summary.state === "paused" && (summary.resumeReason === "timeout" || summary.resumeReason === "yield" || summary.resumeReason === "retry" || summary.resumeReason === "auth");
7074
8527
  }
7075
8528
  async function failPausedTurn(args) {
7076
8529
  const failed = await failTurnRecord({
@@ -7118,8 +8571,6 @@ async function runPausedTurnInContext(payload, options, runOptions) {
7118
8571
  turnId: payload.turnId,
7119
8572
  channelId: thread?.channelId ?? destination.channelId,
7120
8573
  ...thread?.threadTs ? { threadTs: thread.threadTs } : void 0,
7121
- lockKey: payload.conversationId,
7122
- ownsConversationLease: true,
7123
8574
  executeTurn: async (run, saveResult, timeoutMs) => await executeTurn(options.agentRunner, run, saveResult, timeoutMs),
7124
8575
  scheduleSessionCompletedPluginTasks: options.scheduleSessionCompletedPluginTasks,
7125
8576
  beforeStart: async () => {
@@ -7129,7 +8580,7 @@ async function runPausedTurnInContext(payload, options, runOptions) {
7129
8580
  conversationId: payload.conversationId,
7130
8581
  turnId: payload.turnId
7131
8582
  })).record;
7132
- if (!turn || turn.state !== "paused" || turn.resumeReason !== "timeout" && turn.resumeReason !== "yield" && turn.resumeReason !== "retry" || turn.version !== payload.expectedVersion) return false;
8583
+ if (!turn || turn.state !== "paused" || turn.resumeReason !== "timeout" && turn.resumeReason !== "yield" && turn.resumeReason !== "retry" && turn.resumeReason !== "auth" || turn.version !== payload.expectedVersion) return false;
7133
8584
  const activeTurn = turn;
7134
8585
  const { dispatch, surface } = options.routingContext ?? {};
7135
8586
  const currentState = await getPersistedThreadState(payload.conversationId);
@@ -7149,7 +8600,22 @@ async function runPausedTurnInContext(payload, options, runOptions) {
7149
8600
  });
7150
8601
  return false;
7151
8602
  }
7152
- if (conversation.processing.activeTurnId !== payload.turnId) return false;
8603
+ const authorization = turn.resumeReason === "auth" ? await getTurnAuthorization(turn) : void 0;
8604
+ if (turn.resumeReason === "auth") {
8605
+ if (!authorization) return false;
8606
+ const pendingAuth = conversation.processing.pendingAuth;
8607
+ 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))) {
8608
+ await abandonTurnRecord({
8609
+ conversationId: payload.conversationId,
8610
+ turnId: turn.turnId,
8611
+ errorMessage: "Authorization no longer belongs to the latest request"
8612
+ });
8613
+ clearPendingAuth(conversation, turn.turnId);
8614
+ await persistThreadStateById(payload.conversationId, { conversation });
8615
+ return false;
8616
+ }
8617
+ if (!pendingAuth && conversation.processing.activeTurnId !== turn.turnId) return false;
8618
+ } else if (conversation.processing.activeTurnId !== payload.turnId) return false;
7153
8619
  const identity = await resolveResumeExecutionIdentity({
7154
8620
  actor: activeTurn.actor,
7155
8621
  conversationId: payload.conversationId,
@@ -7166,6 +8632,15 @@ async function runPausedTurnInContext(payload, options, runOptions) {
7166
8632
  return false;
7167
8633
  }
7168
8634
  const { actor, credentialContext } = identity;
8635
+ if (authorization && (!("userId" in actor) || actor.userId !== authorization.actorId)) throw new Error("Authorization actor does not match the paused Turn");
8636
+ if (authorization) {
8637
+ await recordAuthorizationCompleted({
8638
+ conversationId: payload.conversationId,
8639
+ ...authorization
8640
+ });
8641
+ conversation.processing.activeTurnId = turn.turnId;
8642
+ await persistThreadStateById(payload.conversationId, { conversation });
8643
+ }
7169
8644
  const locationConfiguration = getLocationConfigurationService(destination);
7170
8645
  const conversationContext = dispatchId ? void 0 : buildConversationContext(conversation, { excludeMessageId: userMessage.id });
7171
8646
  const sandboxRef = getPersistedSandboxState(currentState);
@@ -7207,8 +8682,11 @@ async function runPausedTurnInContext(payload, options, runOptions) {
7207
8682
  ...routing.location ? { location: routing.location } : void 0,
7208
8683
  source,
7209
8684
  ...surface ? { surface } : void 0,
7210
- toolChannelId: destination.channelId,
7211
- environment: { locationConfiguration },
8685
+ toolChannelId: authorization?.toolChannelId ?? destination.channelId,
8686
+ environment: {
8687
+ configuration: authorization?.configuration,
8688
+ locationConfiguration
8689
+ },
7212
8690
  state: {
7213
8691
  pendingAuth: conversation.processing.pendingAuth,
7214
8692
  sandboxRef
@@ -7359,16 +8837,10 @@ async function runNextPausedTurnInContext(conversationId, options, runOptions) {
7359
8837
  await persistThreadStateById(conversationId, { conversation });
7360
8838
  return false;
7361
8839
  }
7362
- const state = getStateAdapter();
7363
- await state.connect();
7364
- await withActiveLock(state, conversationId, async () => {
7365
- const record = await getTurnRecordForResume(conversationId, running.turnId);
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
- });
8840
+ await failStrandedTurnWithFallback({
8841
+ conversationId,
8842
+ errorMessage: "Turn lost its worker before reaching a safe boundary",
8843
+ turn: running
7372
8844
  });
7373
8845
  return false;
7374
8846
  }
@@ -7379,6 +8851,7 @@ async function runNextPausedTurnInContext(conversationId, options, runOptions) {
7379
8851
  turnId: summary.turnId
7380
8852
  });
7381
8853
  if (!request) {
8854
+ if (summary.resumeReason === "auth") continue;
7382
8855
  await failPausedTurn({
7383
8856
  conversationId,
7384
8857
  summary,
@@ -10362,16 +11835,10 @@ async function createApp(options) {
10362
11835
  storage: attachmentStorage
10363
11836
  }));
10364
11837
  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
- });
11838
+ return GET$4(c.req.raw, c.req.param("provider"), waitUntil, { conversationWorkQueue: getVercelConversationWorkQueue() });
10369
11839
  });
10370
11840
  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
- });
11841
+ return GET$5(c.req.raw, c.req.param("provider"), waitUntil, { conversationWorkQueue: getVercelConversationWorkQueue() });
10375
11842
  });
10376
11843
  app.get("/api/internal/sandbox-egress/signals", (c) => {
10377
11844
  return GET(c.req.raw);