@sentry/junior 0.267.0 → 0.269.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 (95) hide show
  1. package/README.md +2 -9
  2. package/dist/{actor-DZSokpwj.js → actor-CP9K2Idu.js} +2 -1
  3. package/dist/{adapter-BybMEezj.js → adapter-2-CG25be.js} +3 -51
  4. package/dist/{admin-DOs1_EiW.js → admin-DSs8S5Uy.js} +1 -1
  5. package/dist/{agent-invocations-CxKldAeB.js → agent-invocations-C1RNOCs8.js} +2 -0
  6. package/dist/{agent-runner-DgNATN4D.js → agent-runner-CrDrPUtY.js} +3411 -1640
  7. package/dist/api/schema.d.ts +2 -5630
  8. package/dist/api/schema.js +7 -7
  9. package/dist/api.d.ts +3 -1
  10. package/dist/api.js +268 -287
  11. package/dist/{app-DAB3fzyp.d.ts → app-BrMMzjYm.d.ts} +10 -3
  12. package/dist/app.d.ts +1 -1
  13. package/dist/app.js +371 -223
  14. package/dist/{automation-CmeFBSEh.js → automation-p87hg_AV.js} +74 -74
  15. package/dist/{briefs-CWKjka7v.js → briefs-PtODdLNu.js} +4 -4
  16. package/dist/{catalog-runtime-cMlAI5LY.js → catalog-runtime-BsoSmTM0.js} +1 -1
  17. package/dist/cli/chat.js +16 -26
  18. package/dist/cli/check.js +3 -3
  19. package/dist/cli/main.js +5 -3
  20. package/dist/cli/plugins.js +4 -4
  21. package/dist/cli/snapshot-create.js +4 -4
  22. package/dist/cli/upgrade.d.ts +1 -1
  23. package/dist/cli/upgrade.js +3 -3
  24. package/dist/{client-BjgbMawj.js → client-Cqf4TboL.js} +20 -15
  25. package/dist/{config-CdB6yJj5.js → config-CPsE4T5L.js} +2 -2
  26. package/dist/{connections-RE_65jzK.js → connections-upFXLmhG.js} +10 -11
  27. package/dist/{conversation-DSo310Ps.js → conversation-CTpD_PGk.js} +7 -84
  28. package/dist/{conversation-privacy-BzgJC5k0.js → conversation-privacy-D6zJc8ff.js} +1 -1
  29. package/dist/{credential-sync-DhEGEpgq.js → credential-sync-CS_6IC-f.js} +30 -34
  30. package/dist/{credentialed-CIGwoLU2.js → credentialed-BU-OW7HP.js} +577 -74
  31. package/dist/{store-B8CABbmR.d.ts → db-BzOCzVb1.d.ts} +17 -7
  32. package/dist/{executor-VYrx_K4w.js → executor-CNS7ONt-.js} +78 -4
  33. package/dist/instrumentation.js +2 -2
  34. package/dist/{assistant-reply-CT7VcXQe.js → interruption-marker-g6Xup3Fk.js} +8 -2
  35. package/dist/{job-queue-C0BZSa7r.js → job-queue-DI-k-Bbk.js} +2 -2
  36. package/dist/{list-1XQP-o6K.js → list-CGoQdXTp.js} +429 -1401
  37. package/dist/locks-D-0h111u.js +48 -0
  38. package/dist/{logging-DuZ1A4ab.js → logging-n7knse-O.js} +1 -1
  39. package/dist/{history-C84mlAwO.js → media-85PDJxO_.js} +83 -1
  40. package/dist/{message-NKSbbnA6.js → message-BpaWd8TT.js} +4 -4
  41. package/dist/nitro.d.ts +1 -1
  42. package/dist/nitro.js +3 -3
  43. package/dist/{oauth-callback-server-qAHLPtml.js → oauth-callback-server-CpGmEw_i.js} +2 -2
  44. package/dist/{prompt-BW9jFsqZ.js → prompt-BlQB0QQt.js} +1 -1
  45. package/dist/{registry-FQutO926.js → registry-By36_xsm.js} +2 -2
  46. package/dist/reporting-schema-CZREhYc-.js +104 -0
  47. package/dist/reporting-window-CVarfId9.js +255 -0
  48. package/dist/{resolve-Bk4yeuHy.js → resolve-CUIBcdPd.js} +3 -3
  49. package/dist/{result-B9e9ATOU.js → result-BemsOMOO.js} +31 -12
  50. package/dist/{runner-C3S2ObH-.js → runner-BVok7PAA.js} +12 -11
  51. package/dist/{sandbox-egress-signals-DqpGzzMB.js → sandbox-egress-signals-BSUWQI95.js} +1 -1
  52. package/dist/{schema-X7Fj4fv1.js → schema-ChnFo5f2.js} +98 -6
  53. package/dist/schema-jK4-1u_H.d.ts +5927 -0
  54. package/dist/{service-account-Do30HUNv.js → service-account-DHvlLnZw.js} +4 -3
  55. package/dist/{service-account-BQasl-qq.js → service-account-hMmESDcr.js} +3 -3
  56. package/dist/{session-stop-DkOjt46_.js → session-stop-BQW_iFcd.js} +499 -23
  57. package/dist/{skills-CQHrwfOa.js → skills-DmJJIuLF.js} +2 -2
  58. package/dist/{spawn-BHuqN14f.js → spawn-ChCm46i-.js} +2 -2
  59. package/dist/{store-6EetVP-m.js → store-1DVhoKOL.js} +4 -4
  60. package/dist/{store-DvNZIsCr.js → store-DCo2n5iJ.js} +3 -3
  61. package/dist/{task-queue-B0DLgd2P.js → task-queue-HMC4QjFm.js} +3 -3
  62. package/dist/task-runner-cA6yjn_E.js +269 -0
  63. package/dist/{thread-state-DRmdJ2QN.js → thread-state-CHy8LS0C.js} +8 -18
  64. package/dist/{turn-failure-response-G11cs2gE.js → turn-failure-response-Dr7erLWs.js} +11 -16
  65. package/dist/{turn-lifecycle-OzLIwvi1.js → turn-lifecycle-bf2fIYi_.js} +1 -1
  66. package/dist/{tasks-0ilmuCux.js → turn-session-routing-BqO0qXLk.js} +959 -44
  67. package/dist/{validation-CxEmt1id.js → validation-DHR1ivfh.js} +1 -1
  68. package/dist/{version-Bapedn4A.js → version-CXCHLgWm.js} +1 -1
  69. package/dist/version.js +1 -1
  70. package/dist/{viewer-DrN3oC2X.js → viewer-DSbD6DF6.js} +503 -241
  71. package/dist/{work-DUQoNcqP.js → work-BE_qTYGi.js} +17 -36
  72. package/dist/{worker-D1l4Ugk0.js → worker-Cm9_sWXg.js} +15 -7
  73. package/migrations/0065_user_customization.sql +1 -0
  74. package/migrations/0066_review_items.sql +53 -0
  75. package/migrations/0067_review_item_dedupe_tenant.sql +2 -0
  76. package/migrations/0068_review_item_slack_cards.sql +10 -0
  77. package/migrations/0069_system_identity.sql +25 -0
  78. package/migrations/0070_review_item_foreign_keys.sql +5 -0
  79. package/migrations/AGENTS.md +17 -0
  80. package/migrations/meta/0065_snapshot.json +3511 -0
  81. package/migrations/meta/0066_snapshot.json +4099 -0
  82. package/migrations/meta/0067_snapshot.json +3921 -0
  83. package/migrations/meta/0068_snapshot.json +4176 -0
  84. package/migrations/meta/0069_snapshot.json +3979 -0
  85. package/migrations/meta/0070_snapshot.json +3997 -0
  86. package/migrations/meta/_journal.json +42 -0
  87. package/package.json +5 -5
  88. package/dist/db-Bg-zC3r5.d.ts +0 -5
  89. package/dist/history-BgI33Kie.d.ts +0 -78
  90. package/dist/images-CecjXAAz.js +0 -64
  91. package/dist/task-runner-B-PD_JOj.js +0 -1074
  92. package/dist/turn-session-routing-Dyut9l7Y.js +0 -76
  93. package/dist/user-input-request-BDFBdF8r.js +0 -116
  94. package/dist/web-message-id-CJSUiksE.js +0 -236
  95. package/migrations/README.md +0 -30
@@ -1,42 +1,41 @@
1
- import { $n as buildDeterministicTurnId, At as PluginCredentialFailureError, Bt as wasPendingAuthStopped, Ct as isHumanConversationMessage, Dt as turnHasReply, E as sameDefinitionValue, Et as recordDeliveredAssistantMessage, G as commitMessages, Gt as completeTurnRecord, Ht as loadTurnCheckpoint, Jn as trimTrailingAssistantMessages, Jt as listTurnSummaries, Kt as failTurnRecord, Ot as upsertConversationMessage, Pn as persistWithRetry, Pt as AuthorizationFlowDisabledError, Qn as buildDeterministicAssistantMessageId, Rt as clearPendingAuth, S as automationDefinition, St as getConversationMessageSlackTs, Tt as normalizeConversationText, Ut as saveTurnCheckpoint, W as commitAcceptedReply, Wt as abandonTurnRecord, X as loadTurnProjection, Y as loadProjection, Yt as recordTurnSummary, _ as parseEventAutomationRow, bt as createConversationMemoryService, c as withScheduledAutomationLock, cr as isExperimentalFeatureEnabled, dr as configValueSchema, dt as persistConversationMessages, f as deleteEventAutomation, gt as fallbackShortTitle, h as listDeletedEventAutomationsCreatedBy, i as readScheduledAutomation, j as automationEventCatalog, jn as buildTurnErrorResponse, mt as resolveConversationTitle, nr as getSubscribedReplyPreflightDecision, nt as recordAuthorizationCompleted, o as saveScheduledAutomationInLock, ot as recordSubscribedReplyRoute, pr as piMessageFromHistoryItem, pt as persistConversationMessageSummaries, q as loadConversationProjection, qt as getTurnRecord, r as parseScheduledAutomationRow, rr as getThreadStopDecision, tr as decideSubscribedThreadReply, ut as hydrateConversationMessages, vt as appendThreadContextMessages, w as listAutomationVersions, wt as markConversationMessage, xt as estimateConversationContextTokens, yt as buildConversationContext, z as registeredEventTriggerProblems } from "./tasks-0ilmuCux.js";
2
- import { a as completeText, p as escapeXml, r as completeDecision, y as isProviderRetryError } from "./client-BjgbMawj.js";
3
- import { c as getSlackClientSecret, i as getChatConfig, l as getSlackSigningSecret, n as botConfig, o as getSlackBotToken, s as getSlackClientId, v as normalizeSlackEmojiName, y as createS3AttachmentStorage } from "./config-CdB6yJj5.js";
4
- import { B as isRecord, E as withSpan, H as toOptionalString, S as setTags, T as withLogContext, V as toOptionalNumber, b as setSpanAttributes, f as logError, h as logWarn, m as logInfo, n as createChatSdkLogger, o as getActiveTraceContext, p as logException, s as getActiveTraceId, t as buildTurnFailureResponse, y as setSentryUser } from "./logging-DuZ1A4ab.js";
1
+ import { $t as appendThreadContextMessages, An as recordTurnSummary, At as loadProjection, Cn as loadTurnCheckpoint, Dn as failTurnRecord, En as completeTurnRecord, Et as commitMessages, Gt as hydrateConversationMessages, Ir as decideSubscribedThreadReply, Jr as piMessageFromHistoryItem, Jt as persistConversationMessageSummaries, Kr as configValueSchema, Kt as persistConversationMessages, Lr as getSubscribedReplyPreflightDecision, Lt as recordAuthorizationCompleted, Nr as buildDeterministicAssistantMessageId, On as getTurnRecord, Ot as loadConversationProjection, Pr as buildDeterministicTurnId, Rr as getThreadStopDecision, Tn as abandonTurnRecord, Tt as commitAcceptedReply, Ur as isExperimentalFeatureEnabled, Vt as recordSubscribedReplyRoute, Yt as resolveConversationTitle, Zt as fallbackShortTitle, _ as markDispatchCompleted, a as confirmDispatchMailboxAppend, an as markConversationMessage, cn as turnHasReply, d as getDispatchTurnId, dn as PluginCredentialFailureError, en as buildConversationContext, fr as buildTurnErrorResponse, g as markDispatchBlocked, h as markDispatchAwaitingResume, hn as AuthorizationFlowDisabledError, hr as persistWithRetry, i as claimDispatchMailboxAppend, in as isHumanConversationMessage, j as readAutomationTitlesByConversationId, jt as loadTurnProjection, kn as listTurnSummaries, kr as trimTrailingAssistantMessages, l as getDispatchInputMessageId, ln as upsertConversationMessage, n as resolveTurnSessionRouting, nn as estimateConversationContextTokens, on as normalizeConversationText, p as isTerminalDispatchStatus, rn as getConversationMessageSlackTs, s as getDispatchConversationId, sn as recordDeliveredAssistantMessage, t as resolveConversationRouting, tn as createConversationMemoryService, u as getDispatchRecord, v as markDispatchFailed, wn as saveTurnCheckpoint, xn as wasPendingAuthStopped, y as markDispatchRunning, yn as clearPendingAuth } from "./turn-session-routing-BqO0qXLk.js";
2
+ import { a as completeText, p as escapeXml, r as completeDecision, y as isProviderRetryError } from "./client-Cqf4TboL.js";
3
+ import { c as getSlackClientSecret, i as getChatConfig, l as getSlackSigningSecret, n as botConfig, o as getSlackBotToken, s as getSlackClientId, v as normalizeSlackEmojiName, y as createS3AttachmentStorage } from "./config-CPsE4T5L.js";
4
+ import { B as isRecord, E as withSpan, H as toOptionalString, S as setTags, T as withLogContext, V as toOptionalNumber, b as setSpanAttributes, f as logError, h as logWarn, m as logInfo, n as createChatSdkLogger, o as getActiveTraceContext, p as logException, s as getActiveTraceId, t as buildTurnFailureResponse, y as setSentryUser } from "./logging-n7knse-O.js";
5
5
  import { i as getActiveSpan, r as continueTrace } from "./sentry-MDkYAVPV.js";
6
6
  import { o as modelIdForProfile, r as defaultModelId } from "./model-profile-Bg8EoCKD.js";
7
- import { a as isUserActor, d as parseSlackTeamId, i as isActorUserId, l as parseSlackChannelId, n as createSlackActor, o as parseActorUserId, p as parseSlackMessageTs, r as createSlackResumeActor, t as createActor } from "./actor-DZSokpwj.js";
8
- import { T as addAgentTurnUsage, V as requireSlackDestination, _ as loadConversationSlackTenantIds, b as locationFromRow, f as getConversationStore, g as withConversationEventLock, k as hasAgentTurnUsage, m as getSqlExecutor, p as getDb, u as getConversationEventStore, w as parseSessionSource, z as createSlackDestination } from "./viewer-DrN3oC2X.js";
9
- import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, o as conversationReadColumns, s as juniorConversations } from "./agent-invocations-CxKldAeB.js";
10
- import { a as juniorAutomationRuns, f as juniorConversationBriefs, h as juniorConversationEvents, s as juniorAutomations, v as juniorCodeChanges } from "./executor-VYrx_K4w.js";
11
- import { d as contextProvenance, p as instructionProvenanceFor } from "./history-C84mlAwO.js";
12
- import { d as actorLogContext, f as credentialContextForActor } from "./registry-FQutO926.js";
13
- import { B as resolveSlackChannelIdFromThreadId, H as listThreadReplies, J as describeEventAutomationTrigger, L as parseSlackThreadId, R as readSlackRawMessageContext, V as getConversationInfo, Y as effectiveTaskOutcomes, at as conversationSubtreeColumns, b as renderBlockText, ct as buildSteeringPiMessage, i as buildAuthPauseResponse, it as conversationSubtree, lt as withoutSavedSteeringMessages, mt as shouldEmitDevAgentTrace, n as executeAgentRun, p as latestProgressStatus, r as createContextCompactor, t as createAgentRunner, ut as RetryableDeliveryError, w as storeAttachment, y as renderAttachmentText, z as resolveSlackChannelIdFromMessage } from "./agent-runner-DgNATN4D.js";
14
- import { $ as postSlackMessage, $t as downloadPrivateSlackFile, Bn as conversationNotArchivedForEmail, Fn as conversationArchiveTimes, Ft as cancelSubscriptions, G as normalizeSlackReplyMarkdown, Hn as viewerConversationMembership, In as conversationArchivedForEmail, J as truncateStatusText, K as normalizeSlackStatusText, Kn as buildSentryConversationUrl, Ln as conversationArchivedForUser, Nn as createPluginLogger, Pn as readConversationAccessFromSql, Qt as SlackActionError, Rn as conversationHasParticipantUser, U as escapeSlackMrkdwnText, Vn as conversationNotArchivedForUser, W as formatSlackLink, Xn as recordSlackAttachment, Y as addReactionToMessage, Yn as readAttachmentVisionSummary, Zt as verifyTimerWatchCredentialSubject, an as normalizeSlackConversationId, ar as createPluginCache, dn as JUNIOR_THREAD_STATE_TTL_MS, et as removeReactionFromMessage, fn as getDashboardConversationLink, in as isDmChannel, ir as lookupSlackUser, l as getPluginSlackConversationLink, n as createUserTokenStore, nn as getSlackClient, on as runWithSlackInstallationToken, p as getPlugins, pn as getDashboardObjectIconLink, qn as getSlackFileId, sn as withSlackRetries, tn as getHeaderString, tr as listConversationAnnotationsById, zn as conversationIsMemberRoot } from "./credentialed-CIGwoLU2.js";
15
- import { a as MUTATION_LOCK_TTL_MS, c as withLock, i as getStateAdapter, o as acquireActiveLock } from "./adapter-BybMEezj.js";
16
- import { W as readMessageAttachments, g as conversationFeedSchema, q as isVisionImageMediaType } from "./conversation-DSo310Ps.js";
17
- import { a as EVENT_SYSTEM_ACTOR, o as isEventConversationMessage } from "./prompt-BW9jFsqZ.js";
18
- import { B as ConversationTurnBoundaryError, G as isCooperativeTurnYieldError, H as TurnInputCommitLostError, J as markTurnClosed, K as isTurnInputCommitLostError, U as TurnInputDeferredError, V as CooperativeTurnYieldError, W as getConversationTurnBoundaryError, X as markTurnFailed, Y as markTurnCompleted, Z as startActiveTurn, q as isTurnInputDeferredError } from "./store-6EetVP-m.js";
19
- import { n as getAssistantReplyText } from "./assistant-reply-CT7VcXQe.js";
20
- import { C as maybePostAutomatedTurnLimitNotice, M as runDispatchOutcome, O as deliversFinalText, c as isEventMailboxMetadata, d as getEventCatalog, i as skipInputBeforeConversationStop, k as finishedRunReply, l as renderEventBatchText, s as eventBatchMessageEvents, u as timerWatchCredentialSubject, w as recordFinishedTurnForAutomatedLimit, x as joinMailboxText } from "./session-stop-DkOjt46_.js";
21
- import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline } from "./job-queue-C0BZSa7r.js";
22
- import { D as stopConversationWork, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, n as appendAndEnqueueExclusiveInboundMessage, r as appendAndEnqueueInboundMessage, w as requestConversationWork } from "./store-DvNZIsCr.js";
23
- import { j as eventAutomationTriggerSchema, l as automationListQuerySchema } from "./automation-CmeFBSEh.js";
24
- import { c as sumUtcHoursIntoSixHours, m as healthReportSchema, t as webMessageId } from "./web-message-id-CJSUiksE.js";
25
- import { A as readAutomationExecutions, D as readAutomationExecutionStatusDays, E as readAutomationExecutionHours, M as readAutomationTitlesByConversationId, O as readAutomationExecutionStatusHours, S as emptyAutomationRunWindows, T as readAutomationExecutionDays, _ as markDispatchBlocked, a as claimDispatchMailboxAppend, b as markDispatchRunning, c as getDispatchConversationId, d as getDispatchRecord, f as getDispatchTurnId, g as markDispatchAwaitingResume, j as readAutomationRuns, k as readAutomationExecutionSummaries, l as getDispatchIdForConversation, m as isTerminalDispatchStatus, n as scheduleSessionCompletedPluginTasks, o as confirmDispatchMailboxAppend, t as processPluginTask, u as getDispatchInputMessageId, v as markDispatchCompleted, w as readAutomationExecutionByConversationId, y as markDispatchFailed } from "./task-runner-B-PD_JOj.js";
26
- import { a as withAgentInvocationResultActors, c as resolveMailboxTurnWork, d as legacyWebMailboxMetadataSchema, i as startAgentInvocationsForChildInput, l as sourceFromTurnInput, o as getAuthPausedConversationTurnId, s as getEventBatchDelayMs, t as enqueueAgentInvocationResult, u as turnInputFactsFromConversationMessage } from "./result-B9e9ATOU.js";
27
- import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-OzLIwvi1.js";
28
- import { i as resolveAgentInvocationId, o as AgentRunError, s as executeTurn, t as createAgentInvocationWorker } from "./work-DUQoNcqP.js";
29
- 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-DRmdJ2QN.js";
30
- import { c as deleteWebAuthorization, d as createPausedTurns, f as getPausedTurnRequest, h as getTurnAuthorization, m as wakePausedTurn, s as createWebAuthorization } from "./connections-RE_65jzK.js";
31
- import { n as resolveTurnSessionRouting } from "./turn-session-routing-Dyut9l7Y.js";
32
- import { t as bindMessageAgent } from "./message-NKSbbnA6.js";
33
- import { t as bindSpawnAgent } from "./spawn-BHuqN14f.js";
34
- 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-G11cs2gE.js";
35
- import { r as storeInputImages } from "./images-CecjXAAz.js";
7
+ import { f as parseSlackTeamId, i as isActorUserId, m as parseSlackMessageTs, n as createSlackActor, o as isUserActor, r as createSlackResumeActor, s as parseActorUserId, t as createActor, u as parseSlackChannelId } from "./actor-CP9K2Idu.js";
8
+ import { C as getSqlExecutor, E as loadConversationSlackTenantIds, H as parseSessionSource, M as resolveActorIdentityId, N as saveActorAvatar, S as getDb, T as withConversationEventLock, U as addAgentTurnUsage, Y as createSlackDestination, Z as requireSlackDestination, f as identityAvatarUrl, h as readUserAvatarUrls, k as locationFromRow, q as hasAgentTurnUsage, x as getConversationStore, y as getConversationEventStore } from "./viewer-DSbD6DF6.js";
9
+ import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, o as conversationReadColumns, s as juniorConversations } from "./agent-invocations-C1RNOCs8.js";
10
+ import { S as juniorCodeChanges, g as juniorConversationBriefs, y as juniorConversationEvents } from "./executor-CNS7ONt-.js";
11
+ import { g as instructionProvenanceFor, m as contextProvenance, r as isVisionImageMediaType } from "./media-85PDJxO_.js";
12
+ import { d as actorLogContext, f as credentialContextForActor } from "./registry-By36_xsm.js";
13
+ import { Et as withoutSavedSteeringMessages, F as getConversationInfo, I as listThreadReplies, M as readSlackRawMessageContext, N as resolveSlackChannelIdFromMessage, Nt as shouldEmitDevAgentTrace, P as resolveSlackChannelIdFromThreadId, St as conversationSubtreeColumns, Tt as buildSteeringPiMessage, b as renderBlockText, ct as webActorFromEmail, et as deliverReviewItemChange, i as buildAuthPauseResponse, j as parseSlackThreadId, k as runWithSlackInstallation, kt as RetryableDeliveryError, lt as resolveConversationDestination, mt as storeAttachment, n as executeAgentRun, ot as createConversationId, p as latestProgressStatus, r as createContextCompactor, st as recordWebConversationActivity, t as createAgentRunner, xt as conversationSubtree, y as renderAttachmentText } from "./agent-runner-CrDrPUtY.js";
14
+ import { An as normalizeSlackConversationId, Ar as lookupSlackUser, Cn as SlackActionError, Dn as getSlackClient, Dr as listConversationAnnotationsById, Dt as removeReactionFromMessage, E as listReplyActionItems, En as getHeaderString, F as isOnlyAssignee, In as JUNIOR_THREAD_STATE_TTL_MS, M as canActOnReviewItem, Mn as withSlackRetries, P as canReadReviewItem, Sn as verifyTimerWatchCredentialSubject, Sr as recordSlackAttachment, _t as normalizeSlackStatusText, ar as createPluginLogger, b as closeReviewItem, bt as addReactionToMessage, cr as conversationArchivedForEmail, dr as conversationIsMemberRoot, fr as conversationNotArchivedForEmail, jr as createPluginCache, k as takeReviewItem, kn as isDmChannel, lr as conversationArchivedForUser, mr as viewerConversationMembership, n as createUserTokenStore, on as cancelSubscriptions, or as readConversationAccessFromSql, p as getPlugins, pr as conversationNotArchivedForUser, sr as conversationArchiveTimes, ur as conversationHasParticipantUser, w as getReviewItem, wn as downloadPrivateSlackFile, xr as readAttachmentVisionSummary, yr as getSlackFileId, yt as truncateStatusText, z as reviewItemReaderForUser } from "./credentialed-BU-OW7HP.js";
15
+ import { a as withLock, n as MUTATION_LOCK_TTL_MS, r as acquireActiveLock } from "./locks-D-0h111u.js";
16
+ import { r as getStateAdapter } from "./adapter-2-CG25be.js";
17
+ import { W as readMessageAttachments, g as conversationFeedSchema } from "./conversation-CTpD_PGk.js";
18
+ import { a as EVENT_SYSTEM_ACTOR, o as isEventConversationMessage } from "./prompt-BlQB0QQt.js";
19
+ import { B as ConversationTurnBoundaryError, G as isCooperativeTurnYieldError, H as TurnInputCommitLostError, J as markTurnClosed, K as isTurnInputCommitLostError, U as TurnInputDeferredError, V as CooperativeTurnYieldError, W as getConversationTurnBoundaryError, X as markTurnFailed, Y as markTurnCompleted, Z as startActiveTurn, q as isTurnInputDeferredError } from "./store-1DVhoKOL.js";
20
+ import { r as getAssistantReplyText } from "./interruption-marker-g6Xup3Fk.js";
21
+ import { C as maybePostAutomatedTurnLimitNotice, D as recordFinishedTurnForAutomatedLimit, I as runDispatchOutcome, M as finishedRunReply, N as postsRunNotices, T as sendSlackReply, c as isEventMailboxMetadata, i as skipInputBeforeConversationStop, j as deliversFinalText, l as renderEventBatchText, s as eventBatchMessageEvents, u as timerWatchCredentialSubject, w as resolveSlackReplyTarget, x as joinMailboxText } from "./session-stop-BQW_iFcd.js";
22
+ import { _ as normalizeUserInputAnswer, g as isEmptyUserInputAnswer } from "./reporting-window-CVarfId9.js";
23
+ import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline } from "./job-queue-DI-k-Bbk.js";
24
+ import { D as stopConversationWork, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, r as appendAndEnqueueInboundMessage, w as requestConversationWork } from "./store-DCo2n5iJ.js";
25
+ import { t as healthReportSchema } from "./reporting-schema-CZREhYc-.js";
26
+ import { a as withAgentInvocationResultActors, c as resolveMailboxTurnWork, d as legacyWebMailboxMetadataSchema, l as sourceFromTurnInput, n as enqueueAgentInvocationResult, o as getAuthPausedConversationTurnId, s as getEventBatchDelayMs, t as createChildConversationWorker, u as turnInputFactsFromConversationMessage } from "./result-BemsOMOO.js";
27
+ import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-bf2fIYi_.js";
28
+ import { a as AgentRunError, o as executeTurn } from "./work-BE_qTYGi.js";
29
+ import { a as persistThreadStateById, i as persistThreadRuntimeState, n as getPersistedSandboxState, o as coerceThreadConversationState, r as getPersistedThreadState, t as getLocationConfigurationService } from "./thread-state-CHy8LS0C.js";
30
+ import { c as deleteWebAuthorization, d as createPausedTurns, f as getPausedTurnRequest, h as getTurnAuthorization, m as wakePausedTurn, s as createWebAuthorization } from "./connections-upFXLmhG.js";
31
+ import { t as bindMessageAgent } from "./message-BpaWd8TT.js";
32
+ import { t as bindSpawnAgent } from "./spawn-ChCm46i-.js";
33
+ import { n as scheduleSessionCompletedPluginTasks, t as processPluginTask } from "./task-runner-cA6yjn_E.js";
34
+ import { a as persistAuthPauseTurnState, c as getTurnUserSlackMessageTs, i as completeAuthPauseTurn, l as buildDeliveredTurnStatePatch, n as getAgentTurnDiagnosticsAttributes, o as getTurnUserMessage, r as requireTurnFailureEventId, s as getTurnUserReplyAttachmentContext, t as finalizeFailedTurnReplyWithEvent, u as loadPendingMessageCards } from "./turn-failure-response-Dr7erLWs.js";
36
35
  import { z } from "zod";
37
36
  import { createHash } from "node:crypto";
38
- import { and, asc, count, desc, eq, exists, inArray, isNotNull, isNull, ne, notInArray, or, sql } from "drizzle-orm";
39
- import { conversationSidebarAnnotationSchema, createSlackSource, createWebSource, localDestinationSchema, objectFactFields, objectPresentation, objectTypeSchema } from "@sentry/junior-plugin-api";
37
+ import { and, asc, desc, eq, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm";
38
+ import { conversationSidebarAnnotationSchema, createSlackSource, objectTypeSchema } from "@sentry/junior-plugin-api";
40
39
  import { alias } from "drizzle-orm/pg-core";
41
40
  import { del, get, put } from "@vercel/blob";
42
41
  import { Message, ThreadImpl, stringifyMarkdown } from "chat";
@@ -80,7 +79,7 @@ function buildDispatchRoutingContext(dispatch) {
80
79
  };
81
80
  }
82
81
  /** Build the stable mailbox work item for one agent dispatch. */
83
- function buildAgentDispatchInboundMessage(dispatch, nowMs = Date.now()) {
82
+ function buildAgentDispatchInboundMessage(dispatch, nowMs = Date.now(), authorIdentityId) {
84
83
  return {
85
84
  conversationId: getDispatchConversationId(dispatch),
86
85
  createdAtMs: dispatch.createdAtMs,
@@ -89,6 +88,7 @@ function buildAgentDispatchInboundMessage(dispatch, nowMs = Date.now()) {
89
88
  inboundMessageId: getDispatchInputMessageId(dispatch.id),
90
89
  input: {
91
90
  text: "[plugin dispatch]",
91
+ ...authorIdentityId ? { authorIdentityId } : void 0,
92
92
  metadata: {
93
93
  dispatchId: dispatch.id,
94
94
  kind: "agent_dispatch"
@@ -112,7 +112,7 @@ async function enqueueAgentDispatch(dispatch, options) {
112
112
  sessionSource: claimedDispatch.source
113
113
  });
114
114
  await appendAndEnqueueInboundMessage({
115
- message: buildAgentDispatchInboundMessage(claimedDispatch, nowMs),
115
+ message: buildAgentDispatchInboundMessage(claimedDispatch, nowMs, await resolveActorIdentityId(getSqlExecutor(), claimedDispatch.actor)),
116
116
  conversationStore: options.conversationStore,
117
117
  nowMs,
118
118
  queue: options.queue,
@@ -380,207 +380,198 @@ function createAttachmentStorage(config) {
380
380
  default: throw new Error(`Unsupported attachment storage provider: ${String(config.provider)}`);
381
381
  }
382
382
  }
383
- // src/chat/slack/automation-card.ts
384
- function preview(text, length) {
385
- const compact = text.replace(/\s+/g, " ").trim();
386
- return compact.length > length ? `${compact.slice(0, length - 1)}…` : compact;
387
- }
388
- /** Show an Automation as a native Work Object, not an operation receipt. */
389
- function renderSlackAutomationCard(card) {
390
- const title = preview(card.title, 160);
391
- const iconUrl = getDashboardObjectIconLink("workflow");
392
- const trigger = preview(card.trigger, 500);
393
- const warning = card.warning ? preview(card.warning, 500) : null;
394
- const text = [
395
- "Automation",
396
- card.url ? formatSlackLink(card.url, title) : escapeSlackMrkdwnText(title),
397
- escapeSlackMrkdwnText(trigger),
398
- warning ? escapeSlackMrkdwnText(warning) : null
399
- ].filter(Boolean).join("\n");
400
- if (!card.url) return {
401
- entity: null,
402
- text
403
- };
404
- return {
405
- text,
406
- entity: {
407
- entity_type: "slack#/entities/item",
408
- external_ref: {
409
- id: card.id,
410
- type: "automation"
411
- },
412
- url: card.url,
413
- entity_payload: {
414
- attributes: {
415
- title: { text: title },
416
- display_type: "Automation",
417
- product_icon: iconUrl ? {
418
- url: iconUrl,
419
- alt_text: "Automation"
420
- } : void 0
421
- },
422
- custom_fields: [{
423
- key: "trigger",
424
- label: "When",
425
- type: "string",
426
- value: trigger,
427
- long: true
428
- }, ...warning ? [{
429
- key: "warning",
430
- label: "Needs attention",
431
- type: "string",
432
- value: warning,
433
- long: true
434
- }] : []]
435
- }
436
- }
383
+ // src/chat/review-items/actions.ts
384
+ /** An expected failure of a person action, for the API to report. */
385
+ var ReviewItemActionError = class extends Error {
386
+ code;
387
+ constructor(code, message) {
388
+ super(message);
389
+ this.name = "ReviewItemActionError";
390
+ this.code = code;
391
+ }
392
+ };
393
+ /** Read one Review item that a User can see, or fail as not found. */
394
+ async function readReviewItemForUser(user, id) {
395
+ const item = await getReviewItem(getDb(), id);
396
+ if (!item || !canReadReviewItem(item, reviewItemReaderForUser(user))) throw new ReviewItemActionError("not_found", "Review item not found.");
397
+ return item;
398
+ }
399
+ function closedStatus(action) {
400
+ return action === "dismiss" ? "dismissed" : "done";
401
+ }
402
+ /** The change that one person action makes, for messages and notes. */
403
+ function reviewItemChange(action, user) {
404
+ if (action === "take") return {
405
+ kind: "taken",
406
+ user
437
407
  };
438
- }
439
- // src/chat/slack/object-card.ts
440
- function descriptionPreview(value) {
441
- const text = value?.replace(/\r\n/g, "\n").trim() ?? "";
442
- const preview = text.split("\n").slice(0, 6).join("\n").slice(0, 500);
443
- return preview.length < text.length ? `${preview.slice(0, 499).trimEnd()}…` : preview;
444
- }
445
- /** Render a compact Task or Item preview, or the saved facts for its detail panel. */
446
- function renderSlackObjectCard(card, conversationId, surface = "preview") {
447
- if (card.objectType === "automation" && card.plugin === "junior") return renderSlackAutomationCard({
448
- id: card.key,
449
- title: card.title,
450
- url: card.url,
451
- trigger: card.trigger ?? "",
452
- warning: card.warning ?? null
453
- });
454
- const presentation = objectPresentation(card);
455
- const type = card.displayType ?? presentation.label;
456
- const iconUrl = getDashboardObjectIconLink(presentation.icon);
457
- const title = card.title.slice(0, 160);
458
- const details = surface === "details";
459
- const deployment = card.objectType === "deployment" || card.objectType === "item" && card.facts?.type === "deployment";
460
- const calendarEvent = card.objectType === "calendar_event";
461
- const status = details || deployment || calendarEvent ? card.status : void 0;
462
- const warning = details || deployment ? card.warning : void 0;
463
- const facts = objectFactFields(card.facts).filter((field) => details || calendarEvent || deployment && field.key === "environment");
464
- const description = details ? card.description?.trim() ?? "" : calendarEvent ? "" : descriptionPreview(card.description);
465
- const text = [
466
- escapeSlackMrkdwnText(type),
467
- card.url ? formatSlackLink(card.url, title) : escapeSlackMrkdwnText(title),
468
- escapeSlackMrkdwnText(card.label),
469
- status ? escapeSlackMrkdwnText(status) : null,
470
- description ? escapeSlackMrkdwnText(description) : null,
471
- warning ? escapeSlackMrkdwnText(`Needs attention: ${warning}`) : null,
472
- ...facts.map((field) => escapeSlackMrkdwnText(`${field.label}: ${field.value}`))
473
- ].filter(Boolean).join("\n");
474
- if (!card.url) return {
475
- entity: null,
476
- text
408
+ if (action === "submit") return {
409
+ kind: "submitted",
410
+ user
477
411
  };
478
- const task = card.objectType === "task";
479
- const attributes = {
480
- title: { text: title },
481
- display_id: card.label,
482
- display_type: type,
483
- product_icon: iconUrl ? {
484
- url: iconUrl,
485
- alt_text: `${type}${status ? `: ${status}` : ""}`
486
- } : void 0,
487
- product_name: card.plugin
412
+ if (action === "complete") return {
413
+ kind: "completed",
414
+ user
488
415
  };
489
- const customFields = [
490
- ...description ? [{
491
- key: "description",
492
- label: "Description",
493
- type: "string",
494
- value: description,
495
- long: true,
496
- format: "markdown"
497
- }] : [],
498
- ...facts.map((field) => ({
499
- ...field,
500
- type: "string"
501
- })),
502
- ...warning ? [{
503
- key: "warning",
504
- label: "Needs attention",
505
- type: "string",
506
- value: warning
507
- }] : []
508
- ];
509
416
  return {
510
- text,
511
- entity: {
512
- ...task ? {
513
- entity_type: "slack#/entities/task",
514
- entity_payload: {
515
- attributes,
516
- display_order: [...status ? ["status"] : [], ...customFields.map((field) => field.key)],
517
- fields: status ? { status: { value: status } } : void 0,
518
- custom_fields: customFields
519
- }
520
- } : {
521
- entity_type: "slack#/entities/item",
522
- entity_payload: {
523
- attributes,
524
- display_order: [...status ? ["status"] : [], ...customFields.map((field) => field.key)],
525
- custom_fields: [...status ? [{
526
- key: "status",
527
- label: "Status",
528
- type: "string",
529
- value: status
530
- }] : [], ...customFields]
531
- }
532
- },
533
- external_ref: {
534
- id: Buffer.from(JSON.stringify([
535
- conversationId,
536
- card.plugin,
537
- card.key
538
- ]), "utf8").toString("base64url"),
539
- type: "annotation"
540
- },
541
- url: card.url
542
- }
417
+ kind: "dismissed",
418
+ user
543
419
  };
544
420
  }
545
- // src/chat/slack/adapter-context.ts
546
- const initializedAdapters = /* @__PURE__ */ new WeakSet();
547
- async function getConnectedState$1(stateAdapter) {
548
- const state = stateAdapter ?? getStateAdapter();
549
- await state.connect();
550
- return state;
551
- }
552
- /** Initialize the Slack adapter against the repository state adapter. */
553
- async function ensureSlackAdapterInitialized(args) {
554
- if (initializedAdapters.has(args.adapter)) return;
555
- const state = await getConnectedState$1(args.state);
556
- await args.adapter.initialize({ getState: () => state });
557
- if (args.adapter.defaultBotTokenProvider && !args.adapter.botUserId) throw new Error("Slack adapter initialized without a resolved bot user id");
558
- initializedAdapters.add(args.adapter);
559
- }
560
- /** Verify a Slack request using the adapter's configured signing secret. */
561
- function verifySlackSignature(args) {
562
- const verifySignature = args.adapter.verifySignature;
563
- if (!verifySignature) throw new Error("Slack adapter does not expose signature verification");
564
- return verifySignature.call(args.adapter, args.body, args.request.headers.get("x-slack-request-timestamp"), args.request.headers.get("x-slack-signature"));
565
- }
566
- /** Run Slack work with the installation token that matches the inbound event. */
567
- async function runWithSlackInstallation(args) {
568
- await ensureSlackAdapterInitialized({
569
- adapter: args.adapter,
570
- state: args.state
421
+ /**
422
+ * Return whether a closed item is the result of this same action. An item
423
+ * that a person's message closed never matches a card or desk action.
424
+ */
425
+ function isSameClose(item, action, user) {
426
+ return item.closeReason !== "Replied in thread" && item.closedBy?.userId === user.id && item.status === closedStatus(action) && action === "submit" === Boolean(item.answer);
427
+ }
428
+ function submittedAnswer(item, answer) {
429
+ if (item.kind !== "action" || !item.request) throw new ReviewItemActionError("invalid", "Only an action item takes an answer.");
430
+ const normalized = normalizeUserInputAnswer(item.request, {
431
+ other: answer?.other,
432
+ requestId: item.id,
433
+ values: answer?.values ?? {}
571
434
  });
572
- const internals = args.adapter;
573
- if (internals.defaultBotTokenProvider) return await args.task();
574
- const installationId = args.installation.isEnterpriseInstall ? args.installation.enterpriseId : args.installation.teamId;
575
- if (!installationId) throw new Error("Slack installation context is missing team id");
576
- if (!internals.resolveTokenForTeam || !internals.requestContext) throw new Error("Slack adapter cannot resolve workspace installations");
577
- const tokenContext = await internals.resolveTokenForTeam.call(args.adapter, installationId, args.installation.isEnterpriseInstall);
578
- if (!tokenContext) throw new Error(`Slack installation token was not found for ${installationId}`);
579
- return await internals.requestContext.run({
580
- ...tokenContext,
581
- enterpriseId: args.installation.enterpriseId,
582
- isEnterpriseInstall: args.installation.isEnterpriseInstall
583
- }, () => runWithSlackInstallationToken(tokenContext.token, args.task));
435
+ if (isEmptyUserInputAnswer(normalized)) throw new ReviewItemActionError("invalid", "The answer is empty.");
436
+ return normalized;
437
+ }
438
+ /**
439
+ * Do one person action on a Review item and tell its linked Conversations.
440
+ * The first submit, complete, or dismiss closes the item for every
441
+ * assignee. A retry of the same close sends any missing messages again and
442
+ * succeeds, so a client can retry after a failure. With `firstCloseOnly`,
443
+ * only the call that closes the item succeeds, and every other call gets a
444
+ * conflict. Use it when the caller adds its own input for the action, such
445
+ * as a card click in a chat thread, so one action adds one Turn.
446
+ * `exceptConversationId` is the Conversation where the person acted. It gets
447
+ * the person's input from its own surface, so it gets no message here.
448
+ * `delivery` shows the change and sends the messages; see
449
+ * `deliverReviewItemChange`.
450
+ */
451
+ async function actOnReviewItem(args) {
452
+ const { action, user } = args;
453
+ const nowMs = args.nowMs ?? Date.now();
454
+ const db = getDb();
455
+ const item = await readReviewItemForUser(user, args.id);
456
+ if (action === "take") {
457
+ if (item.status !== "open") throw new ReviewItemActionError("conflict", "Review item is closed.");
458
+ if (!canActOnReviewItem(item, user.id)) throw new ReviewItemActionError("forbidden", "Only an assignee can take this item.");
459
+ if (!isOnlyAssignee(item, user.id) && !await takeReviewItem(db, {
460
+ id: item.id,
461
+ nowMs,
462
+ userId: user.id
463
+ })) throw new ReviewItemActionError("conflict", "Review item is closed.");
464
+ const taken = await readReviewItemForUser(user, item.id);
465
+ await deliverReviewItemChange({
466
+ change: reviewItemChange(action, user),
467
+ delivery: args.delivery,
468
+ exceptConversationId: args.exceptConversationId,
469
+ item: taken,
470
+ nowMs
471
+ });
472
+ return taken;
473
+ }
474
+ let closed = item;
475
+ let closedNow = false;
476
+ if (item.status === "open") {
477
+ if (!canActOnReviewItem(item, user.id)) throw new ReviewItemActionError("forbidden", "Only an assignee can act on this item.");
478
+ if (action === "complete" && item.kind === "action") throw new ReviewItemActionError("invalid", "An action item needs an answer. Submit it or dismiss it.");
479
+ const answer = action === "submit" ? submittedAnswer(item, args.answer) : void 0;
480
+ closedNow = await closeReviewItem(db, {
481
+ closedByUserId: user.id,
482
+ id: item.id,
483
+ nowMs,
484
+ status: closedStatus(action),
485
+ ...answer ? { answer } : void 0
486
+ });
487
+ closed = await readReviewItemForUser(user, item.id);
488
+ }
489
+ if (args.firstCloseOnly ? !closedNow : !isSameClose(closed, action, user)) throw new ReviewItemActionError("conflict", "Review item is closed.");
490
+ await deliverReviewItemChange({
491
+ change: reviewItemChange(action, user),
492
+ delivery: args.delivery,
493
+ exceptConversationId: args.exceptConversationId,
494
+ item: closed,
495
+ nowMs
496
+ });
497
+ return closed;
498
+ }
499
+ /**
500
+ * Return whether a reply from this User answers an open action item, or is
501
+ * a retry of the reply from this User that closed it.
502
+ */
503
+ function replyAnswers(item, user) {
504
+ if (item.status !== "open") return item.closedBy?.userId === user?.id;
505
+ if (item.assignedToEveryone) return false;
506
+ if (item.assignees.length === 0) return true;
507
+ return user !== void 0 && item.assignees.some((assignee) => assignee.userId === user.id);
508
+ }
509
+ /** Return the answer that the text of a person's message gives to an item. */
510
+ function messageAnswer(item, text) {
511
+ if (!item.request) return void 0;
512
+ const answer = normalizeUserInputAnswer(item.request, {
513
+ other: text,
514
+ requestId: item.id,
515
+ values: {}
516
+ });
517
+ return isEmptyUserInputAnswer(answer) ? void 0 : answer;
518
+ }
519
+ /**
520
+ * Close the open action items that a person answered with a reply in their
521
+ * thread. Call it after the reply is saved to the Conversation, so the
522
+ * reply always reaches the agent. Each item that it closes saves `text` as
523
+ * its answer, so the item keeps what the person said.
524
+ *
525
+ * A reply closes an item only when all of these are true:
526
+ * - The item is an open `action` item.
527
+ * - The agent added it in this Conversation (`addedByConversationId`).
528
+ * - The replier is an assigned User, or the item has no assignees. A Slack
529
+ * replier without a Junior User closes only items with no assignees.
530
+ *
531
+ * Items assigned to Everyone, or only to other people, stay open. They need
532
+ * an answer on the card or the review desk. Each item closes as `done` with
533
+ * `REVIEW_ITEM_REPLIED_REASON`, and the replier closes it. The reply itself
534
+ * is the answer in this Conversation, so only the other linked
535
+ * Conversations get a message. A click that closes the item first wins.
536
+ * A retry of the reply, or a later reply from the same person, delivers
537
+ * the items that it closed again: the cards show the close, and only the
538
+ * missing messages go. Each message has a key, so a message that was sent
539
+ * does not go again. `delivery` shows the change and sends the messages;
540
+ * see `deliverReviewItemChange`.
541
+ */
542
+ async function closeReviewItemsAnsweredByReply(args) {
543
+ const db = getDb();
544
+ const items = await listReplyActionItems(db, args.conversationId);
545
+ if (items.length === 0) return;
546
+ if (args.canAnswer && !await args.canAnswer()) return;
547
+ const user = await args.resolveUser();
548
+ const nowMs = args.nowMs ?? Date.now();
549
+ for (const item of items.filter((candidate) => replyAnswers(candidate, user))) {
550
+ let closedItem = item;
551
+ if (item.status === "open") {
552
+ const answer = messageAnswer(item, args.text);
553
+ if (!await closeReviewItem(db, {
554
+ ...answer ? { answer } : void 0,
555
+ id: item.id,
556
+ nowMs,
557
+ reason: "Replied in thread",
558
+ status: "done",
559
+ ...user ? { closedByUserId: user.id } : void 0
560
+ })) continue;
561
+ closedItem = await getReviewItem(db, item.id);
562
+ if (!closedItem) continue;
563
+ }
564
+ await deliverReviewItemChange({
565
+ change: {
566
+ kind: "replied",
567
+ who: user ? user.displayName ?? user.email : "A person"
568
+ },
569
+ delivery: args.delivery,
570
+ exceptConversationId: args.conversationId,
571
+ item: closedItem,
572
+ nowMs
573
+ });
574
+ }
584
575
  }
585
576
  // src/chat/slack/errors.ts
586
577
  /** Extract Slack's stable API error code from Web API errors. */
@@ -915,6 +906,7 @@ function combineTurnText(queuedMessages, latestText) {
915
906
  }
916
907
  // src/chat/services/message-actor-identity.ts
917
908
  const messageActors = /* @__PURE__ */ new WeakMap();
909
+ const messageAuthorIdentityIds = /* @__PURE__ */ new WeakMap();
918
910
  function canonicalUserId(author, actor) {
919
911
  const authorUserId = parseActorUserId(author.userId);
920
912
  if (authorUserId && authorUserId !== actor.userId) throw new Error("Message actor user id mismatch");
@@ -945,6 +937,17 @@ function bindMessageActorIdentity(message, actor) {
945
937
  applyActorToAuthor(message.author, currentActor);
946
938
  return currentActor;
947
939
  }
940
+ /**
941
+ * Keep the author Identity id that ingress resolved, next to the Actor.
942
+ * The Message write reads it, so the write does not guess the author again.
943
+ */
944
+ function bindMessageAuthorIdentityId(message, authorIdentityId) {
945
+ if (authorIdentityId) messageAuthorIdentityIds.set(message, authorIdentityId);
946
+ }
947
+ /** Read the author Identity id that ingress bound to a Chat SDK message. */
948
+ function getMessageAuthorIdentityId(message) {
949
+ return messageAuthorIdentityIds.get(message);
950
+ }
948
951
  /** Read message identity without promoting adapter display fallbacks. */
949
952
  function getMessageActorIdentity(message) {
950
953
  return messageActors.get(message) ?? actorFromAuthor(message.author);
@@ -1040,10 +1043,8 @@ function createSlackTurnRuntime(deps) {
1040
1043
  return true;
1041
1044
  };
1042
1045
  const failConversationTurn = async (args) => {
1043
- const conversationId = getThreadId(args.thread, args.message) ?? getRunId(args.thread, args.message);
1044
- if (!conversationId) return;
1045
1046
  await turnLifecycle.fail({
1046
- conversationId,
1047
+ conversationId: args.conversationId,
1047
1048
  createdAtMs: deps.now(),
1048
1049
  eventId: args.eventId,
1049
1050
  failureCode: args.failureCode,
@@ -1088,7 +1089,15 @@ function createSlackTurnRuntime(deps) {
1088
1089
  };
1089
1090
  const postFallbackErrorReplyWithLogging = async (args) => {
1090
1091
  try {
1091
- await args.thread.post(buildTurnErrorResponse(args.error, args.eventId));
1092
+ const channelId = getChannelId(args.thread, args.message);
1093
+ if (!channelId) throw new Error("Slack fallback error reply requires a channel id");
1094
+ const threadTs = getThreadTs(getThreadId(args.thread, args.message));
1095
+ await sendSlackReply({
1096
+ channelId,
1097
+ conversationId: args.conversationId,
1098
+ text: buildTurnErrorResponse(args.error, args.eventId),
1099
+ ...threadTs ? { threadTs } : void 0
1100
+ });
1092
1101
  } catch (postError) {
1093
1102
  logException(postError, args.postFailureEventName, {
1094
1103
  "app.slack.reply_stage": "error_fallback_post",
@@ -1144,14 +1153,14 @@ function createSlackTurnRuntime(deps) {
1144
1153
  const completedAtMs = deps.now();
1145
1154
  logSkippedSubscribedDecision(args);
1146
1155
  if (args.preparedState) await deps.onSubscribedMessageSkipped({
1147
- thread: args.thread,
1156
+ conversationId: args.conversationId,
1148
1157
  message: args.message,
1149
1158
  decision: args.decision,
1150
1159
  preparedState: args.preparedState,
1151
1160
  completedAtMs
1152
1161
  });
1153
1162
  else await deps.recordSkippedSubscribedTurn({
1154
- thread: args.thread,
1163
+ conversationId: args.conversationId,
1155
1164
  message: args.message,
1156
1165
  decision: args.decision,
1157
1166
  text: args.text,
@@ -1199,7 +1208,7 @@ function createSlackTurnRuntime(deps) {
1199
1208
  });
1200
1209
  logSkippedSubscribedDecision(skipped);
1201
1210
  await deps.recordSkippedSteeringMessage({
1202
- thread: args.thread,
1211
+ conversationId: args.conversationId,
1203
1212
  message: skipped.message,
1204
1213
  decision: skipped.decision,
1205
1214
  text: skipped.text
@@ -1213,7 +1222,7 @@ function createSlackTurnRuntime(deps) {
1213
1222
  while (args.pending.length > 0) {
1214
1223
  const skipped = args.pending.shift();
1215
1224
  await deps.recordSkippedSteeringMessage({
1216
- thread: args.thread,
1225
+ conversationId: args.conversationId,
1217
1226
  message: skipped.message,
1218
1227
  decision: skipped.decision,
1219
1228
  text: skipped.text
@@ -1227,6 +1236,7 @@ function createSlackTurnRuntime(deps) {
1227
1236
  };
1228
1237
  return {
1229
1238
  async handleNewMention(thread, message, hooks) {
1239
+ const conversationId = hooks.conversationId ?? thread.id;
1230
1240
  const processingReactions = createProcessingReactionTracker(thread);
1231
1241
  let processingReaction;
1232
1242
  const skippedSteeringMessages = [];
@@ -1235,7 +1245,7 @@ function createSlackTurnRuntime(deps) {
1235
1245
  const onTurnCompleted = async () => {
1236
1246
  completed = true;
1237
1247
  await flushSkippedSteeringMessagesBestEffort({
1238
- thread,
1248
+ conversationId,
1239
1249
  pending: skippedSteeringMessages,
1240
1250
  context: logContext({
1241
1251
  threadId: getThreadId(thread, message),
@@ -1289,6 +1299,7 @@ function createSlackTurnRuntime(deps) {
1289
1299
  },
1290
1300
  onSkipped: (skipped) => handleSkippedSteeringMessages({
1291
1301
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
1302
+ conversationId,
1292
1303
  pending: skippedSteeringMessages,
1293
1304
  skipped,
1294
1305
  thread
@@ -1302,7 +1313,7 @@ function createSlackTurnRuntime(deps) {
1302
1313
  await deps.executeSlackTurn(thread, message, {
1303
1314
  explicitMention: true,
1304
1315
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
1305
- conversationId: hooks.conversationId,
1316
+ conversationId,
1306
1317
  destination: hooks.destination,
1307
1318
  queuedMessages,
1308
1319
  ack,
@@ -1339,17 +1350,19 @@ function createSlackTurnRuntime(deps) {
1339
1350
  let lifecycleError;
1340
1351
  try {
1341
1352
  await failConversationTurn({
1353
+ conversationId,
1342
1354
  eventId,
1343
1355
  failureCode,
1344
- message,
1345
- thread
1356
+ message
1346
1357
  });
1347
1358
  } catch (error) {
1348
1359
  lifecycleError = error;
1349
1360
  }
1350
1361
  await hooks.beforeFirstResponsePost?.();
1351
1362
  await postFallbackErrorReplyWithLogging({
1363
+ conversationId,
1352
1364
  thread,
1365
+ message,
1353
1366
  eventId,
1354
1367
  error: failureCause,
1355
1368
  postFailureEventName: "mention.handler.failure_reply_post.failed"
@@ -1359,7 +1372,7 @@ function createSlackTurnRuntime(deps) {
1359
1372
  if (completed) await processingReactions.completeAll();
1360
1373
  else {
1361
1374
  await flushSkippedSteeringMessagesBestEffort({
1362
- thread,
1375
+ conversationId,
1363
1376
  pending: skippedSteeringMessages,
1364
1377
  context: logContext({
1365
1378
  threadId: getThreadId(thread, message),
@@ -1374,6 +1387,7 @@ function createSlackTurnRuntime(deps) {
1374
1387
  }
1375
1388
  },
1376
1389
  async handleSubscribedMessage(thread, message, hooks) {
1390
+ const conversationId = hooks.conversationId ?? thread.id;
1377
1391
  const processingReactions = createProcessingReactionTracker(thread);
1378
1392
  let processingReaction;
1379
1393
  const skippedSteeringMessages = [];
@@ -1382,7 +1396,7 @@ function createSlackTurnRuntime(deps) {
1382
1396
  const onTurnCompleted = async () => {
1383
1397
  completed = true;
1384
1398
  await flushSkippedSteeringMessagesBestEffort({
1385
- thread,
1399
+ conversationId,
1386
1400
  pending: skippedSteeringMessages,
1387
1401
  context: logContext({
1388
1402
  threadId: getThreadId(thread, message),
@@ -1418,6 +1432,7 @@ function createSlackTurnRuntime(deps) {
1418
1432
  userText: replaceTopLevelText(content, strippedUserText)
1419
1433
  };
1420
1434
  const threadContext = {
1435
+ conversationId,
1421
1436
  threadId,
1422
1437
  actorId,
1423
1438
  channelId,
@@ -1450,7 +1465,7 @@ function createSlackTurnRuntime(deps) {
1450
1465
  beforeFirstResponsePost: hooks.beforeFirstResponsePost
1451
1466
  });
1452
1467
  await skipSubscribedMessage({
1453
- thread,
1468
+ conversationId,
1454
1469
  message,
1455
1470
  decision,
1456
1471
  context: threadContext,
@@ -1468,7 +1483,7 @@ function createSlackTurnRuntime(deps) {
1468
1483
  if (preflightDecision && !preflightDecision.shouldReply) {
1469
1484
  const reason = preflightDecision.reasonDetail ? `${preflightDecision.reason}:${preflightDecision.reasonDetail}` : preflightDecision.reason;
1470
1485
  await skipSubscribedMessage({
1471
- thread,
1486
+ conversationId,
1472
1487
  message,
1473
1488
  decision: {
1474
1489
  shouldReply: false,
@@ -1494,7 +1509,7 @@ function createSlackTurnRuntime(deps) {
1494
1509
  beforeFirstResponsePost: hooks.beforeFirstResponsePost
1495
1510
  });
1496
1511
  await skipSubscribedMessage({
1497
- thread,
1512
+ conversationId,
1498
1513
  message,
1499
1514
  decision,
1500
1515
  context: threadContext,
@@ -1513,7 +1528,7 @@ function createSlackTurnRuntime(deps) {
1513
1528
  queuedMessages
1514
1529
  });
1515
1530
  await deps.persistPreparedState({
1516
- thread,
1531
+ conversationId,
1517
1532
  preparedState
1518
1533
  });
1519
1534
  const decision = await deps.decideSubscribedReply({
@@ -1530,7 +1545,7 @@ function createSlackTurnRuntime(deps) {
1530
1545
  beforeFirstResponsePost: hooks.beforeFirstResponsePost
1531
1546
  })) {
1532
1547
  await skipSubscribedMessage({
1533
- thread,
1548
+ conversationId,
1534
1549
  message,
1535
1550
  decision,
1536
1551
  context: threadContext,
@@ -1542,7 +1557,7 @@ function createSlackTurnRuntime(deps) {
1542
1557
  }
1543
1558
  if (!decision.shouldReply) {
1544
1559
  await skipSubscribedMessage({
1545
- thread,
1560
+ conversationId,
1546
1561
  message,
1547
1562
  decision,
1548
1563
  context: threadContext,
@@ -1561,6 +1576,7 @@ function createSlackTurnRuntime(deps) {
1561
1576
  },
1562
1577
  onSkipped: (skipped) => handleSkippedSteeringMessages({
1563
1578
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
1579
+ conversationId,
1564
1580
  pending: skippedSteeringMessages,
1565
1581
  skipped,
1566
1582
  thread
@@ -1586,7 +1602,7 @@ function createSlackTurnRuntime(deps) {
1586
1602
  const toolInvocationHook = createToolInvocationHook(processingReaction, hooks);
1587
1603
  await deps.executeSlackTurn(thread, message, {
1588
1604
  explicitMention: Boolean(message.isMention),
1589
- conversationId: hooks.conversationId,
1605
+ conversationId,
1590
1606
  destination: hooks.destination,
1591
1607
  preparedState,
1592
1608
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
@@ -1625,17 +1641,19 @@ function createSlackTurnRuntime(deps) {
1625
1641
  let lifecycleError;
1626
1642
  try {
1627
1643
  await failConversationTurn({
1644
+ conversationId,
1628
1645
  eventId,
1629
1646
  failureCode,
1630
- message,
1631
- thread
1647
+ message
1632
1648
  });
1633
1649
  } catch (error) {
1634
1650
  lifecycleError = error;
1635
1651
  }
1636
1652
  await hooks.beforeFirstResponsePost?.();
1637
1653
  await postFallbackErrorReplyWithLogging({
1654
+ conversationId,
1638
1655
  thread,
1656
+ message,
1639
1657
  eventId,
1640
1658
  error: failureCause,
1641
1659
  postFailureEventName: "subscribed_message.handler.failure_reply_post.failed"
@@ -1645,7 +1663,7 @@ function createSlackTurnRuntime(deps) {
1645
1663
  if (completed) await processingReactions.completeAll();
1646
1664
  else {
1647
1665
  await flushSkippedSteeringMessagesBestEffort({
1648
- thread,
1666
+ conversationId,
1649
1667
  pending: skippedSteeringMessages,
1650
1668
  context: logContext({
1651
1669
  threadId: getThreadId(thread, message),
@@ -2125,6 +2143,7 @@ function restoreMessage(args) {
2125
2143
  const message = Message.fromJSON(metadata.message);
2126
2144
  message.attachments = message.attachments.map((attachment) => args.adapter.rehydrateAttachment(attachment));
2127
2145
  rehydrateAttachmentFetchers(message);
2146
+ bindMessageAuthorIdentityId(message, args.record.input.authorIdentityId);
2128
2147
  return message;
2129
2148
  }
2130
2149
  async function bindSlackActorIdentities(args) {
@@ -2136,6 +2155,10 @@ async function bindSlackActorIdentities(args) {
2136
2155
  await Promise.all([...byAuthorId].map(async ([authorId, messages]) => {
2137
2156
  const profile = await args.lookupSlackUser(args.teamId, authorId);
2138
2157
  await Promise.all(messages.map((message) => ensureSlackMessageActorIdentity(message, args.teamId, async () => profile)));
2158
+ if (profile && messages.some(getMessageAuthorIdentityId)) await resolveActorIdentityId(getSqlExecutor(), createSlackActor(args.teamId, authorId, {
2159
+ fullName: profile.fullName,
2160
+ userName: profile.userName
2161
+ }));
2139
2162
  }));
2140
2163
  }
2141
2164
  function restoreThread(args) {
@@ -2297,6 +2320,7 @@ function createSlackConversationWorker(options) {
2297
2320
  /** Serialize a Slack message and the active trace into the durable mailbox. */
2298
2321
  function buildSlackInboundMessage(args) {
2299
2322
  const authorId = requireSlackAuthorId(args.message);
2323
+ const authorIdentityId = getMessageAuthorIdentityId(args.message);
2300
2324
  const destination = createSlackDestination({
2301
2325
  channelId: args.thread.channelId,
2302
2326
  teamId: args.installation?.teamId
@@ -2319,6 +2343,7 @@ function buildSlackInboundMessage(args) {
2319
2343
  input: {
2320
2344
  text: args.message.text || " ",
2321
2345
  authorId,
2346
+ ...authorIdentityId ? { authorIdentityId } : void 0,
2322
2347
  attachments: args.message.attachments,
2323
2348
  metadata: {
2324
2349
  platform: "slack",
@@ -2775,9 +2800,11 @@ function toConversationMessage(args) {
2775
2800
  const slackTs = getMessageTimestamp(args.entry);
2776
2801
  const messageHasPotentialImageAttachment = hasPotentialImageAttachment(args.entry.attachments);
2777
2802
  const imageAttachmentCount = messageHasPotentialImageAttachment ? countPotentialImageAttachments(args.entry.attachments) : 0;
2803
+ const authorIdentityId = args.entry.author.isMe ? void 0 : getMessageAuthorIdentityId(args.entry);
2778
2804
  return {
2779
2805
  id: args.entry.id,
2780
2806
  role: args.entry.author.isMe ? "assistant" : "user",
2807
+ ...authorIdentityId ? { authorIdentityId } : void 0,
2781
2808
  text: resolveMessageText(args),
2782
2809
  createdAtMs: args.entry.metadata.dateSent.getTime(),
2783
2810
  author: {
@@ -2867,578 +2894,6 @@ function isAllowedSignInEmail(email, allowedDomains) {
2867
2894
  const separator = normalized.lastIndexOf("@");
2868
2895
  return allowedDomains.length > 0 && separator > 0 && isAllowedDomain(normalized.slice(separator + 1), allowedDomains);
2869
2896
  }
2870
- // src/chat/automations/collection.ts
2871
- /**
2872
- * The web collection applies access before counts, filter options, and paging.
2873
- * Keep its ownership rules aligned with the direct Automation reader. A public
2874
- * Destination only grants access inside a viewer's linked Slack workspaces.
2875
- */
2876
- function viewerAutomationCollection(user) {
2877
- const db = getDb();
2878
- const teamIds = user.identities.filter((identity) => identity.provider === "slack").flatMap((identity) => identity.providerTenantId ? [identity.providerTenantId] : []);
2879
- const automation = juniorAutomations;
2880
- const destination = juniorDestinations;
2881
- const isScheduled = eq(automation.kind, "scheduled");
2882
- const owned = inArray(automation.creatorIdentityId, user.identities.map((identity) => identity.id));
2883
- const publicDestination = sql`coalesce(${destination.visibility} = 'public', false)`;
2884
- const publicToViewer = and(inArray(automation.teamId, teamIds), publicDestination, ne(automation.status, "completed"));
2885
- const runs = juniorAutomationRuns;
2886
- const lastRunNeedsAttention = sql`coalesce((select ${runs.status} in ('failed', 'blocked') from ${runs}
2887
- where ${runs.automationId} = ${automation.id} and ${runs.status} in ('blocked', 'completed', 'failed') and ${runs.completedAtMs} is not null
2888
- order by ${runs.completedAtMs} desc, ${runs.id} desc limit 1), false)`;
2889
- return db.select({
2890
- kind: automation.kind,
2891
- id: automation.id,
2892
- title: automation.title,
2893
- createdAtMs: automation.createdAtMs,
2894
- state: sql`${automation.status}`.as("state"),
2895
- lastRunNeedsAttention: lastRunNeedsAttention.as("last_run_needs_attention"),
2896
- trigger: sql`${automation.record}->'trigger'`.as("trigger"),
2897
- owned: sql`${owned}`.as("owned"),
2898
- resource: sql`case when ${isScheduled} then '' else concat_ws(' ', ${automation.identifier}, ${automation.record}->'trigger'->>'label', ${automation.namespace}) end`.as("resource"),
2899
- creator: sql`${automation.teamId} || ':' || ${automation.creatorSlackUserId}`.as("creator"),
2900
- creatorLabel: sql`coalesce(nullif(trim(${automation.record}->'createdBy'->>'fullName'), ''), nullif(trim(${automation.record}->'createdBy'->>'userName'), ''), ${automation.creatorSlackUserId})`.as("creator_label"),
2901
- destination: sql`${automation.teamId} || ':' || ${automation.channelId}`.as("destination"),
2902
- destinationLabel: sql`coalesce(nullif(${destination.displayName}, ''), ${automation.channelId})`.as("destination_label"),
2903
- isPublic: publicDestination.as("is_public"),
2904
- instruction: automation.instruction
2905
- }).from(automation).leftJoin(destination, and(eq(destination.provider, "slack"), eq(destination.providerTenantId, automation.teamId), eq(destination.providerDestinationId, automation.channelId))).where(and(ne(automation.status, "deleted"), or(owned, publicToViewer))).as("accessible_automations");
2906
- }
2907
- /**
2908
- * List the accessible event automations whose trigger cannot work with the
2909
- * enabled plugins. `eventTriggerProblems` decides, the same check that the
2910
- * tools and the dashboard use, so SQL does not keep its own copy of the rules.
2911
- */
2912
- async function unavailableEventAutomationIds(collection) {
2913
- const catalog = automationEventCatalog(getEventCatalog());
2914
- return (await getDb().select({
2915
- id: collection.id,
2916
- trigger: collection.trigger
2917
- }).from(collection).where(eq(collection.kind, "event"))).flatMap((row) => {
2918
- const trigger = eventAutomationTriggerSchema.safeParse(row.trigger);
2919
- return trigger.success && registeredEventTriggerProblems(catalog, trigger.data).length === 0 ? [] : [row.id];
2920
- });
2921
- }
2922
- /** Read an SQL page and complete, access-scoped counts and filter options. */
2923
- async function readAutomationCollection(user, input) {
2924
- const db = getDb();
2925
- const collection = viewerAutomationCollection(user);
2926
- const unavailableIds = await unavailableEventAutomationIds(collection);
2927
- const unavailable = unavailableIds.length > 0 ? inArray(collection.id, unavailableIds) : sql`false`;
2928
- const attention = and(notInArray(collection.state, ["paused", "completed"]), or(eq(collection.state, "blocked"), unavailable, eq(collection.lastRunNeedsAttention, true)));
2929
- const where = and(input.scope === "mine" ? eq(collection.owned, true) : input.scope === "public" ? eq(collection.isPublic, true) : input.scope === "attention" ? attention : void 0, input.type === "all" ? void 0 : eq(collection.kind, input.type), input.state === "all" ? void 0 : input.state === "unavailable" ? unavailable : eq(collection.state, input.state), input.creator ? eq(collection.creator, input.creator) : void 0, input.destination ? eq(collection.destination, input.destination) : void 0, input.q ? sql`strpos(lower(concat_ws(' ', ${collection.title}, ${collection.instruction}, ${collection.resource})), ${input.q.toLowerCase()}) > 0` : void 0);
2930
- const [totals, counts, creators, destinations] = await Promise.all([
2931
- db.select({ total: count() }).from(collection).where(where),
2932
- db.select({
2933
- all: count(),
2934
- mine: sql`count(*) filter (where ${collection.owned})::int`,
2935
- public: sql`count(*) filter (where ${collection.isPublic})::int`,
2936
- private: sql`count(*) filter (where not ${collection.isPublic})::int`
2937
- }).from(collection),
2938
- db.select({
2939
- value: collection.creator,
2940
- label: sql`min(${collection.creatorLabel})`
2941
- }).from(collection).groupBy(collection.creator).orderBy(sql`min(${collection.creatorLabel})`, collection.creator),
2942
- db.select({
2943
- value: collection.destination,
2944
- label: sql`min(${collection.destinationLabel})`
2945
- }).from(collection).groupBy(collection.destination).orderBy(sql`min(${collection.destinationLabel})`, collection.destination)
2946
- ]);
2947
- const total = totals[0].total;
2948
- const page = Math.min(input.page, Math.max(1, Math.ceil(total / input.pageSize)));
2949
- const order = input.sort === "oldest" ? asc(collection.createdAtMs) : input.sort === "title" ? asc(sql`lower(coalesce(nullif(trim(${collection.title}), ''), ${collection.instruction}))`) : desc(collection.createdAtMs);
2950
- return {
2951
- rows: await db.select({
2952
- id: collection.id,
2953
- kind: collection.kind
2954
- }).from(collection).where(where).orderBy(order, desc(collection.id)).limit(input.pageSize).offset((page - 1) * input.pageSize),
2955
- total,
2956
- page,
2957
- pageSize: input.pageSize,
2958
- counts: counts[0],
2959
- creators,
2960
- destinations
2961
- };
2962
- }
2963
- // src/chat/automations/creator.ts
2964
- /**
2965
- * Return whether the user created the Automation. Both kinds store the
2966
- * creating Junior identity, so one rule decides ownership.
2967
- */
2968
- function isAutomationCreator(user, automation) {
2969
- return user.identities.some((identity) => identity.id === automation.creatorIdentityId);
2970
- }
2971
- // src/chat/scheduled-automations/personal.ts
2972
- var PersonalScheduledAutomationNotFoundError = class extends Error {
2973
- constructor() {
2974
- super("Scheduled automation was not found.");
2975
- this.name = "PersonalScheduledAutomationNotFoundError";
2976
- }
2977
- };
2978
- /** Delete one scheduled automation created by the viewer. */
2979
- async function deleteViewerScheduledAutomation(db, user, id, nowMs = Date.now()) {
2980
- await withScheduledAutomationLock(db, id, async (tx) => {
2981
- const task = await readScheduledAutomation(tx, id);
2982
- if (!task || task.status === "deleted" || !isAutomationCreator(user, task)) throw new PersonalScheduledAutomationNotFoundError();
2983
- await saveScheduledAutomationInLock(tx, {
2984
- ...task,
2985
- nextRunAtMs: void 0,
2986
- runNowAtMs: void 0,
2987
- status: "deleted",
2988
- updatedAtMs: nowMs
2989
- }, task);
2990
- });
2991
- }
2992
- // src/chat/automations/read.ts
2993
- const TASK_EXECUTION_LIST_LIMIT = 100;
2994
- const AUTOMATION_VERSION_LIST_LIMIT = 100;
2995
- function creatorLabel(creator) {
2996
- return creator.fullName?.trim() || (creator.userName?.trim() ? `@${creator.userName.trim()}` : "") || creator.slackUserId;
2997
- }
2998
- function creatorKey(teamId, slackUserId) {
2999
- return `${teamId}:${slackUserId}`;
3000
- }
3001
- async function creatorProfiles(candidates) {
3002
- const selectors = new Map(candidates.map(({ task }) => {
3003
- const teamId = task.destination.teamId;
3004
- const slackUserId = task.createdBy.slackUserId;
3005
- return [creatorKey(teamId, slackUserId), {
3006
- slackUserId,
3007
- teamId
3008
- }];
3009
- }));
3010
- if (selectors.size === 0) return /* @__PURE__ */ new Map();
3011
- const rows = await getDb().select({
3012
- avatarUrl: juniorIdentities.avatarUrl,
3013
- email: juniorUsers.primaryEmailNormalized,
3014
- emailVerified: juniorIdentities.emailVerified,
3015
- slackUserId: juniorIdentities.providerSubjectId,
3016
- teamId: juniorIdentities.providerTenantId
3017
- }).from(juniorIdentities).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(eq(juniorIdentities.kind, "user"), eq(juniorIdentities.provider, "slack"), or(...[...selectors.values()].map((selector) => and(eq(juniorIdentities.providerTenantId, selector.teamId), eq(juniorIdentities.providerSubjectId, selector.slackUserId))))));
3018
- return new Map(rows.map((row) => [creatorKey(row.teamId, row.slackUserId), {
3019
- email: row.emailVerified ? row.email ?? void 0 : void 0,
3020
- avatarUrl: row.avatarUrl ?? void 0
3021
- }]));
3022
- }
3023
- function destinationKey(destination) {
3024
- return `${destination.teamId}:${destination.channelId}`;
3025
- }
3026
- function displayText(value, fallback) {
3027
- return value.trim() || fallback;
3028
- }
3029
- function taskDisplayTitle(title, instruction, fallback) {
3030
- const stored = title?.trim();
3031
- if (stored) return stored;
3032
- return fallbackShortTitle(instruction, fallback);
3033
- }
3034
- async function destinationDetails(destinations) {
3035
- const selectors = new Map(destinations.map((destination) => [destinationKey(destination), destination]));
3036
- if (selectors.size === 0) return /* @__PURE__ */ new Map();
3037
- const rows = await getDb().select({
3038
- displayName: juniorDestinations.displayName,
3039
- kind: juniorDestinations.kind,
3040
- providerDestinationId: juniorDestinations.providerDestinationId,
3041
- providerTenantId: juniorDestinations.providerTenantId,
3042
- visibility: juniorDestinations.visibility
3043
- }).from(juniorDestinations).where(or(...[...selectors.values()].map((destination) => and(eq(juniorDestinations.provider, "slack"), eq(juniorDestinations.providerTenantId, destination.teamId), eq(juniorDestinations.providerDestinationId, destination.channelId)))));
3044
- return new Map(rows.map((row) => {
3045
- const name = row.displayName?.trim();
3046
- const label = name ? row.kind === "channel" ? `#${name.replace(/^#/, "")}` : name : row.kind === "dm" ? "Direct message" : row.kind === "group" ? "Group message" : `Channel ${row.providerDestinationId}`;
3047
- return [`${row.providerTenantId}:${row.providerDestinationId}`, {
3048
- label,
3049
- visibility: row.visibility === "public" ? "public" : "private"
3050
- }];
3051
- }));
3052
- }
3053
- function executionSummaryFields(stats) {
3054
- return {
3055
- lastRunStatus: stats?.lastRunStatus,
3056
- ...stats?.lastConversationId ? { lastConversationId: stats.lastConversationId } : void 0,
3057
- ...stats?.lastExecutedAtMs ? { lastRunAt: new Date(stats.lastExecutedAtMs).toISOString() } : void 0,
3058
- costUsd: stats?.costUsd ?? emptyAutomationRunWindows(),
3059
- runs: stats?.runs ?? emptyAutomationRunWindows(),
3060
- totalRuns: stats?.totalRuns ?? 0
3061
- };
3062
- }
3063
- function scheduledAutomationSummary(task, ownedByViewer, destination, stats, creator) {
3064
- if (task.status === "deleted") throw new Error("Deleted scheduled automations cannot enter the Automations view");
3065
- const nextRunAtMs = task.status === "active" ? task.runNowAtMs ?? task.nextRunAtMs : void 0;
3066
- const instruction = displayText(task.task.text, "Untitled scheduled automation");
3067
- return {
3068
- createdAt: new Date(task.createdAtMs).toISOString(),
3069
- createdBy: creatorLabel(task.createdBy),
3070
- createdByEmail: creator?.email,
3071
- createdByAvatarUrl: creator?.avatarUrl,
3072
- destination: {
3073
- channelId: task.destination.channelId,
3074
- label: destination.label,
3075
- teamId: task.destination.teamId,
3076
- visibility: destination.visibility
3077
- },
3078
- id: task.id,
3079
- instruction,
3080
- kind: "scheduled",
3081
- ...executionSummaryFields(stats),
3082
- ...nextRunAtMs !== void 0 ? { nextRunAt: new Date(nextRunAtMs).toISOString() } : void 0,
3083
- ownedByViewer,
3084
- schedule: displayText(task.schedule.description, "Schedule unavailable"),
3085
- status: task.status,
3086
- statusReason: task.statusReason,
3087
- timezone: task.schedule.timezone,
3088
- credentialMode: task.credentialMode,
3089
- outcomes: effectiveTaskOutcomes(task.outcomes, task.destination),
3090
- title: taskDisplayTitle(task.title, instruction, "Untitled scheduled automation")
3091
- };
3092
- }
3093
- function eventAutomationSummary(task, ownedByViewer, destination, stats, creator) {
3094
- const instruction = task.task.text;
3095
- const catalog = getEventCatalog();
3096
- return {
3097
- createdAt: new Date(task.createdAtMs).toISOString(),
3098
- createdBy: creatorLabel(task.createdBy),
3099
- createdByEmail: creator?.email,
3100
- createdByAvatarUrl: creator?.avatarUrl,
3101
- destination: {
3102
- channelId: task.destination.channelId,
3103
- label: destination.label,
3104
- teamId: task.destination.teamId,
3105
- visibility: destination.visibility
3106
- },
3107
- id: task.id,
3108
- instruction,
3109
- kind: "event",
3110
- ...executionSummaryFields(stats),
3111
- ownedByViewer,
3112
- resource: task.trigger.identifier ? `${task.trigger.label} · ${task.trigger.identifier}` : task.trigger.label,
3113
- source: task.trigger.namespace,
3114
- credentialMode: task.credentialMode,
3115
- outcomes: effectiveTaskOutcomes(task.outcomes, task.destination),
3116
- title: taskDisplayTitle(task.title, instruction, "Untitled event automation"),
3117
- status: task.status === "deleted" ? "active" : task.status,
3118
- statusReason: task.statusReason,
3119
- trigger: describeEventAutomationTrigger(task.trigger, catalog)
3120
- };
3121
- }
3122
- function viewerTeamIds(user) {
3123
- return [...new Set(user.identities.filter((identity) => identity.provider === "slack").map((identity) => identity.providerTenantId).filter((teamId) => Boolean(teamId)))];
3124
- }
3125
- /** Resolve an Automation the viewer owns or can read through a public Destination. */
3126
- async function resolveViewerTaskCandidate(user, id) {
3127
- const [row] = await getDb().select().from(juniorAutomations).where(eq(juniorAutomations.id, id)).limit(1);
3128
- if (!row) return void 0;
3129
- let candidate;
3130
- if (row.kind === "scheduled") {
3131
- const task = parseScheduledAutomationRow(row);
3132
- if (!task) return void 0;
3133
- candidate = {
3134
- kind: "scheduled",
3135
- ownedByViewer: isAutomationCreator(user, task),
3136
- task
3137
- };
3138
- } else {
3139
- const task = parseEventAutomationRow(row);
3140
- candidate = {
3141
- kind: "event",
3142
- ownedByViewer: isAutomationCreator(user, task),
3143
- task
3144
- };
3145
- }
3146
- const { task } = candidate;
3147
- if (task.status === "deleted") return void 0;
3148
- if (candidate.ownedByViewer) return candidate;
3149
- const destination = (await destinationDetails([task.destination])).get(destinationKey(task.destination));
3150
- return viewerTeamIds(user).includes(task.destination.teamId) && destination?.visibility === "public" ? candidate : void 0;
3151
- }
3152
- /** Read one current Automation summary with the Automations view access rules. */
3153
- async function readViewerAutomationSummary(user, id) {
3154
- const candidate = await resolveViewerTaskCandidate(user, id);
3155
- if (!candidate) return void 0;
3156
- return automationSummaryForCandidate(candidate);
3157
- }
3158
- async function automationSummaryForCandidate(candidate) {
3159
- const [destinations, creators, stats] = await Promise.all([
3160
- destinationDetails([candidate.task.destination]),
3161
- creatorProfiles([candidate]),
3162
- readAutomationExecutionSummaries([candidate.task.id])
3163
- ]);
3164
- const destination = destinations.get(destinationKey(candidate.task.destination)) ?? {
3165
- label: `Channel ${candidate.task.destination.channelId}`,
3166
- visibility: "private"
3167
- };
3168
- const creator = creators.get(creatorKey(candidate.task.destination.teamId, candidate.task.createdBy.slackUserId));
3169
- if (candidate.kind === "scheduled") return scheduledAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), creator);
3170
- return eventAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), creator);
3171
- }
3172
- function emptyAutomationExecutionDay(date) {
3173
- return {
3174
- costUsd: 0,
3175
- date,
3176
- event: 0,
3177
- scheduled: 0
3178
- };
3179
- }
3180
- function emptyAutomationExecutionStatusDay(date) {
3181
- return {
3182
- blocked: 0,
3183
- completed: 0,
3184
- date,
3185
- failed: 0
3186
- };
3187
- }
3188
- function automationExecutionSixHours(hours, nowMs = Date.now()) {
3189
- return sumUtcHoursIntoSixHours({
3190
- empty: emptyAutomationExecutionDay,
3191
- hours,
3192
- nowMs
3193
- });
3194
- }
3195
- function automationExecutionStatusSixHours(hours, nowMs = Date.now()) {
3196
- return sumUtcHoursIntoSixHours({
3197
- empty: emptyAutomationExecutionStatusDay,
3198
- hours,
3199
- nowMs
3200
- });
3201
- }
3202
- /** Read a page from the full accessible Automation collection. */
3203
- async function readViewerAutomations(user, input = automationListQuerySchema.parse({})) {
3204
- const { rows, ...page } = await readAutomationCollection(user, input);
3205
- const db = getDb();
3206
- const ids = rows.map((row) => row.id);
3207
- const stored = ids.length ? await db.select().from(juniorAutomations).where(inArray(juniorAutomations.id, ids)) : [];
3208
- const candidates = /* @__PURE__ */ new Map();
3209
- for (const row of stored) {
3210
- if (row.kind === "scheduled") {
3211
- const task = parseScheduledAutomationRow(row);
3212
- if (task && task.status !== "deleted") candidates.set(task.id, {
3213
- kind: "scheduled",
3214
- task,
3215
- ownedByViewer: isAutomationCreator(user, task)
3216
- });
3217
- continue;
3218
- }
3219
- const task = parseEventAutomationRow(row);
3220
- if (task.status !== "deleted") candidates.set(task.id, {
3221
- kind: "event",
3222
- task,
3223
- ownedByViewer: isAutomationCreator(user, task)
3224
- });
3225
- }
3226
- const selected = rows.flatMap((row) => {
3227
- const candidate = candidates.get(row.id);
3228
- return candidate ? [candidate] : [];
3229
- });
3230
- const [destinations, creators] = await Promise.all([destinationDetails(selected.map(({ task }) => task.destination)), creatorProfiles(selected)]);
3231
- const visible = selected.filter((candidate) => candidate.ownedByViewer || destinations.get(destinationKey(candidate.task.destination))?.visibility === "public");
3232
- const collection = viewerAutomationCollection(user);
3233
- const access = exists(getDb().select({ id: collection.id }).from(collection).where(eq(collection.id, juniorAutomationRuns.automationId)));
3234
- const [executionDays, executionHours, stats] = await Promise.all([
3235
- readAutomationExecutionDays(90, { access }),
3236
- readAutomationExecutionHours(168, { access }),
3237
- readAutomationExecutionSummaries(ids)
3238
- ]);
3239
- const automations = visible.map((candidate) => {
3240
- const destination = destinations.get(destinationKey(candidate.task.destination)) ?? {
3241
- label: `Channel ${candidate.task.destination.channelId}`,
3242
- visibility: "private"
3243
- };
3244
- const creator = creators.get(creatorKey(candidate.task.destination.teamId, candidate.task.createdBy.slackUserId));
3245
- if (candidate.kind === "scheduled") return scheduledAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), creator);
3246
- return eventAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), creator);
3247
- });
3248
- return {
3249
- executionDays,
3250
- executionHours,
3251
- executionSixHours: automationExecutionSixHours(executionHours),
3252
- automations,
3253
- ...page
3254
- };
3255
- }
3256
- /** Load deleted scheduled automations the viewer still owns, newest first. */
3257
- async function readDeletedOwnedScheduledAutomations(user) {
3258
- const identityIds = user.identities.map((identity) => identity.id);
3259
- if (identityIds.length === 0) return [];
3260
- return (await getDb().select().from(juniorAutomations).where(and(eq(juniorAutomations.kind, "scheduled"), eq(juniorAutomations.status, "deleted"), inArray(juniorAutomations.creatorIdentityId, identityIds))).orderBy(desc(juniorAutomations.createdAtMs), desc(juniorAutomations.id)).limit(TASK_EXECUTION_LIST_LIMIT)).map(parseScheduledAutomationRow).filter((task) => task?.status === "deleted");
3261
- }
3262
- /** Load deleted event automations the viewer still owns, newest first. */
3263
- async function readDeletedOwnedEventAutomations(user) {
3264
- return await listDeletedEventAutomationsCreatedBy(getDb(), user, TASK_EXECUTION_LIST_LIMIT);
3265
- }
3266
- function automationTitleForRun(run, automationTitles) {
3267
- return automationTitles.get(`${run.kind}:${run.automationId}`) ?? run.title?.trim() ?? "Untitled task";
3268
- }
3269
- function addDeletedAutomationRun(args, automationTitles, automations) {
3270
- const key = `${args.kind}:${args.automationId}`;
3271
- if (automationTitles.has(key)) return;
3272
- automationTitles.set(key, args.title);
3273
- automations.push({
3274
- kind: args.kind,
3275
- automationId: args.automationId
3276
- });
3277
- }
3278
- /**
3279
- * Read newest runs across live viewer-visible automations and deleted automations the
3280
- * viewer owns. Deleted task rows stay available so history does not depend on
3281
- * conversation actor membership.
3282
- */
3283
- async function readViewerAutomationRuns(user) {
3284
- const [taskList, deletedScheduled, deletedEvent] = await Promise.all([
3285
- getDb().select().from(viewerAutomationCollection(user)),
3286
- readDeletedOwnedScheduledAutomations(user),
3287
- readDeletedOwnedEventAutomations(user)
3288
- ]);
3289
- const automationTitles = /* @__PURE__ */ new Map();
3290
- const automations = [];
3291
- for (const task of taskList) {
3292
- automationTitles.set(`${task.kind}:${task.id}`, taskDisplayTitle(task.title ?? void 0, task.instruction, `Untitled ${task.kind} automation`));
3293
- automations.push({
3294
- kind: task.kind,
3295
- automationId: task.id
3296
- });
3297
- }
3298
- for (const task of deletedScheduled) {
3299
- const instruction = displayText(task.task.text, "Untitled scheduled automation");
3300
- addDeletedAutomationRun({
3301
- kind: "scheduled",
3302
- automationId: task.id,
3303
- title: taskDisplayTitle(task.title, instruction, "Untitled scheduled automation")
3304
- }, automationTitles, automations);
3305
- }
3306
- for (const task of deletedEvent) {
3307
- const instruction = displayText(task.task.text, "Untitled event automation");
3308
- addDeletedAutomationRun({
3309
- kind: "event",
3310
- automationId: task.id,
3311
- title: taskDisplayTitle(task.title, instruction, "Untitled event automation")
3312
- }, automationTitles, automations);
3313
- }
3314
- const runs = await readAutomationRuns({
3315
- automationIds: automations.map((automation) => automation.automationId),
3316
- limit: 101
3317
- });
3318
- return {
3319
- runs: runs.slice(0, TASK_EXECUTION_LIST_LIMIT).map((run) => ({
3320
- ...run,
3321
- automationTitle: automationTitleForRun(run, automationTitles)
3322
- })),
3323
- truncated: runs.length > TASK_EXECUTION_LIST_LIMIT
3324
- };
3325
- }
3326
- var ViewerTaskNotFoundError = class extends Error {
3327
- constructor() {
3328
- super("Task was not found.");
3329
- this.name = "ViewerTaskNotFoundError";
3330
- }
3331
- };
3332
- /** Read one viewer-visible task and its newest terminal executions. */
3333
- async function readViewerAutomationExecutions(user, id) {
3334
- const candidate = await resolveViewerTaskCandidate(user, id);
3335
- if (!candidate) throw new ViewerTaskNotFoundError();
3336
- const [automation, executions, executionDays, executionHours] = await Promise.all([
3337
- automationSummaryForCandidate(candidate),
3338
- readAutomationExecutions({
3339
- automationId: id,
3340
- limit: 101
3341
- }),
3342
- readAutomationExecutionStatusDays({ automationId: id }),
3343
- readAutomationExecutionStatusHours({ automationId: id })
3344
- ]);
3345
- return {
3346
- executionDays,
3347
- executionHours,
3348
- executionSixHours: automationExecutionStatusSixHours(executionHours),
3349
- executions: executions.slice(0, TASK_EXECUTION_LIST_LIMIT),
3350
- automation,
3351
- truncated: executions.length > TASK_EXECUTION_LIST_LIMIT
3352
- };
3353
- }
3354
- /** Read saved definitions for one viewer-visible Automation, newest first. */
3355
- async function readViewerAutomationVersions(user, id) {
3356
- const candidate = await resolveViewerTaskCandidate(user, id);
3357
- if (!candidate) throw new ViewerTaskNotFoundError();
3358
- const versions = await listAutomationVersions(getDb(), id, 101);
3359
- const current = automationDefinition(candidate.task);
3360
- const listed = versions.slice(0, AUTOMATION_VERSION_LIST_LIMIT);
3361
- const catalog = getEventCatalog();
3362
- return {
3363
- versions: listed.map((version) => version.kind === "event" ? {
3364
- ...version,
3365
- trigger: describeEventAutomationTrigger(version.definition.trigger, catalog)
3366
- } : version),
3367
- activeVersion: listed.find(({ definition }) => sameDefinitionValue(definition, current))?.version ?? null,
3368
- truncated: versions.length > AUTOMATION_VERSION_LIST_LIMIT
3369
- };
3370
- }
3371
- /** Name the event in a trigger, such as "opened" for `pull_request.opened`. */
3372
- function eventActionLabel(eventName) {
3373
- const [, ...action] = eventName.split(".");
3374
- return (action.length > 0 ? action : [eventName]).join(" ").replaceAll("_", " ");
3375
- }
3376
- /**
3377
- * Read whose access one run used from its dispatch record. Return nothing when
3378
- * the record is gone, because the Automation's current setting can differ.
3379
- */
3380
- async function readRunCredential(dispatchId, creator) {
3381
- if (!dispatchId) return {};
3382
- const dispatch = await getDispatchRecord(dispatchId);
3383
- if (!dispatch) return {};
3384
- const subject = dispatch.credentialSubject;
3385
- if (!subject) return { credentialMode: "system" };
3386
- return {
3387
- credentialMode: "creator",
3388
- credentialUser: subject.userId === creator.slackUserId ? creatorLabel(creator) : subject.userId
3389
- };
3390
- }
3391
- /** Resolve the source task for one conversation when an Automation run links it. */
3392
- async function readConversationSourceTask(args) {
3393
- const execution = await readAutomationExecutionByConversationId({ conversationId: args.conversationId });
3394
- if (!execution) return void 0;
3395
- const dispatchId = getDispatchIdForConversation(args.conversationId);
3396
- const base = {
3397
- kind: execution.kind,
3398
- ...dispatchId ? { triggerMessageId: getDispatchInputMessageId(dispatchId) } : void 0
3399
- };
3400
- if (!args.viewer) return base;
3401
- const candidate = await resolveViewerTaskCandidate(args.viewer, execution.automationId);
3402
- if (!candidate) return base;
3403
- const credential = await readRunCredential(dispatchId, candidate.task.createdBy);
3404
- if (candidate.kind === "scheduled") {
3405
- const instruction = displayText(candidate.task.task.text, "Untitled scheduled automation");
3406
- const schedule = candidate.task.schedule.description.trim();
3407
- return {
3408
- ...base,
3409
- ...credential,
3410
- id: candidate.task.id,
3411
- kind: "scheduled",
3412
- label: instruction,
3413
- title: taskDisplayTitle(candidate.task.title, instruction, "Untitled scheduled automation"),
3414
- ...schedule ? { triggerLabel: schedule } : void 0
3415
- };
3416
- }
3417
- const instruction = displayText(candidate.task.task.text, "Untitled event automation");
3418
- const { events, label } = candidate.task.trigger;
3419
- return {
3420
- ...base,
3421
- ...credential,
3422
- id: candidate.task.id,
3423
- kind: "event",
3424
- label: instruction,
3425
- title: taskDisplayTitle(candidate.task.title, instruction, "Untitled event automation"),
3426
- triggerLabel: events.length === 1 ? `${label} ${eventActionLabel(events[0])}` : label
3427
- };
3428
- }
3429
- /** Delete one viewer-owned scheduled or event automation. */
3430
- async function deleteViewerTask(user, id) {
3431
- const candidate = await resolveViewerTaskCandidate(user, id);
3432
- if (!candidate?.ownedByViewer) throw new ViewerTaskNotFoundError();
3433
- if (candidate.kind === "scheduled") try {
3434
- await deleteViewerScheduledAutomation(getDb(), user, id);
3435
- return;
3436
- } catch (error) {
3437
- if (error instanceof PersonalScheduledAutomationNotFoundError) throw new ViewerTaskNotFoundError();
3438
- throw error;
3439
- }
3440
- await deleteEventAutomation(getDb(), id);
3441
- }
3442
2897
  // src/chat/slack/assistant-thread/status-send.ts
3443
2898
  /**
3444
2899
  * Slack's assistant loading UI accepts both `status` and `loading_messages`,
@@ -3592,292 +3047,6 @@ function readSlackActionToken(message) {
3592
3047
  const token = slackActionTokenSchema.safeParse(envelope.data.raw?.action_token);
3593
3048
  return token.success ? token.data : void 0;
3594
3049
  }
3595
- // src/chat/slack/cards.ts
3596
- /** Select the Slack object renderer for a built-in card. */
3597
- function renderSlackCard(card, conversationId) {
3598
- switch (card.kind) {
3599
- case "object": return renderSlackObjectCard(card, conversationId);
3600
- case "automation": return renderSlackAutomationCard(card);
3601
- }
3602
- }
3603
- // src/chat/slack/footer.ts
3604
- /** Render compact reply attribution for the Slack footer. */
3605
- function formatReplyAttribution(attribution) {
3606
- return attribution.detail ? `${attribution.label} · ${attribution.detail}` : attribution.label;
3607
- }
3608
- /**
3609
- * Build the compact conversation footer for visible Slack reply surfaces.
3610
- *
3611
- * Detailed turn metrics stay in the dashboard instead of Slack-visible copy.
3612
- */
3613
- function buildSlackReplyFooter(args) {
3614
- const items = [];
3615
- const conversationId = args.conversationId?.trim();
3616
- if (conversationId) {
3617
- const idItem = {
3618
- label: "ID",
3619
- value: conversationId
3620
- };
3621
- const conversationUrl = getPluginSlackConversationLink(conversationId)?.url ?? getDashboardConversationLink(conversationId) ?? buildSentryConversationUrl(conversationId);
3622
- if (conversationUrl) idItem.url = conversationUrl;
3623
- items.push(idItem);
3624
- }
3625
- return items.length > 0 || args.replyAttribution ? {
3626
- ...args.replyAttribution ? { attribution: args.replyAttribution } : void 0,
3627
- items
3628
- } : void 0;
3629
- }
3630
- /** Build Slack blocks for a reply chunk using the Slack-flavored markdown block for the body. */
3631
- function buildSlackReplyBlocks(text, footer) {
3632
- const blocks = text.trim() ? [{
3633
- type: "markdown",
3634
- text
3635
- }] : [];
3636
- if (footer && (footer.attribution || footer.items.length > 0)) {
3637
- const attributionElements = footer.attribution ? [{
3638
- type: "plain_text",
3639
- text: formatReplyAttribution(footer.attribution)
3640
- }] : [];
3641
- blocks.push({
3642
- type: "context",
3643
- elements: [...attributionElements, ...footer.items.map((item) => ({
3644
- type: "mrkdwn",
3645
- text: item.url ? `*${escapeSlackMrkdwnText(item.label)}:* ${formatSlackLink(item.url, item.value)}` : `*${escapeSlackMrkdwnText(item.label)}:* ${escapeSlackMrkdwnText(item.value)}`
3646
- }))]
3647
- });
3648
- }
3649
- return blocks.length ? blocks : void 0;
3650
- }
3651
- // src/chat/slack/output.ts
3652
- const MAX_INLINE_CHARS = 2200;
3653
- const MAX_INLINE_LINES = 45;
3654
- const CONTINUED_MARKER = "\n\n[Continued below]";
3655
- function countSlackLines(text) {
3656
- if (!text) return 0;
3657
- return text.split("\n").length;
3658
- }
3659
- function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
3660
- return text.length <= maxChars && countSlackLines(text) <= maxLines;
3661
- }
3662
- function findSplitIndex(text, maxChars) {
3663
- if (text.length <= maxChars) return text.length;
3664
- const bounded = text.slice(0, maxChars);
3665
- const newlineIndex = bounded.lastIndexOf("\n");
3666
- if (newlineIndex > 0) return newlineIndex;
3667
- const spaceIndex = bounded.lastIndexOf(" ");
3668
- if (spaceIndex > 0) return spaceIndex;
3669
- return maxChars;
3670
- }
3671
- function splitByLineBudget(text, maxLines) {
3672
- if (maxLines <= 0) return "";
3673
- const lines = text.split("\n");
3674
- if (lines.length <= maxLines) return text;
3675
- return lines.slice(0, maxLines).join("\n");
3676
- }
3677
- function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
3678
- return {
3679
- maxChars: Math.max(1, maxChars - suffix.length),
3680
- maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
3681
- };
3682
- }
3683
- function forceSplitBudget(text, budget) {
3684
- const lineCount = countSlackLines(text);
3685
- return {
3686
- maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
3687
- maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
3688
- };
3689
- }
3690
- function getFenceContinuation(text) {
3691
- let open;
3692
- for (const line of text.split("\n")) {
3693
- const trimmed = line.trimStart();
3694
- const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
3695
- if (!openerMatch?.[1]) continue;
3696
- if (!open) {
3697
- open = {
3698
- fence: openerMatch[1],
3699
- openerLine: trimmed
3700
- };
3701
- continue;
3702
- }
3703
- if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) open = void 0;
3704
- }
3705
- if (!open) return null;
3706
- return {
3707
- closeSuffix: text.endsWith("\n") ? open.fence : `\n${open.fence}`,
3708
- reopenPrefix: `${open.openerLine}\n`
3709
- };
3710
- }
3711
- function appendSlackSuffix(text, marker) {
3712
- return `${text}${getFenceContinuation(text)?.closeSuffix ?? ""}${marker}`;
3713
- }
3714
- function stripTrailingContinuationMarker(text) {
3715
- return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -19) : text;
3716
- }
3717
- function takeSlackContinuationChunk(text, budget) {
3718
- let { prefix, rest } = takeSlackInlinePrefix(text, budget);
3719
- if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, budget)));
3720
- let carryover = rest ? getFenceContinuation(prefix) : null;
3721
- if (!carryover) return {
3722
- prefix,
3723
- rest
3724
- };
3725
- const carryoverBudget = reserveInlineBudgetForSuffix(`${carryover.closeSuffix}${CONTINUED_MARKER}`);
3726
- ({prefix, rest} = takeSlackInlinePrefix(text, carryoverBudget));
3727
- if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, carryoverBudget)));
3728
- carryover = rest ? getFenceContinuation(prefix) : null;
3729
- if (!carryover) return {
3730
- prefix,
3731
- rest
3732
- };
3733
- return {
3734
- prefix,
3735
- rest: `${carryover.reopenPrefix}${rest}`
3736
- };
3737
- }
3738
- /** Return the first continuation-safe chunk plus any remaining text. */
3739
- function takeSlackContinuationPrefix(text, options) {
3740
- const budget = {
3741
- maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
3742
- maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
3743
- };
3744
- const { prefix, rest } = (() => {
3745
- if (options?.forceSplit) return takeSlackContinuationChunk(text, budget);
3746
- const initial = takeSlackInlinePrefix(text, budget);
3747
- return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
3748
- })();
3749
- return {
3750
- prefix,
3751
- renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
3752
- rest
3753
- };
3754
- }
3755
- /**
3756
- * Take the largest Slack-safe inline prefix from `text` under the configured
3757
- * character and line budgets. Returns the consumed prefix plus remaining text.
3758
- */
3759
- function takeSlackInlinePrefix(text, options) {
3760
- const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
3761
- const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
3762
- const normalized = text.replace(/\r\n?/g, "\n");
3763
- if (!normalized) return {
3764
- prefix: "",
3765
- rest: ""
3766
- };
3767
- if (fitsInlineBudget(normalized, maxChars, maxLines)) return {
3768
- prefix: normalized,
3769
- rest: ""
3770
- };
3771
- const lineBounded = splitByLineBudget(normalized, maxLines);
3772
- const cutIndex = findSplitIndex(lineBounded, maxChars);
3773
- const prefix = lineBounded.slice(0, cutIndex).trimEnd();
3774
- if (prefix) return {
3775
- prefix,
3776
- rest: normalized.slice(prefix.length).trimStart()
3777
- };
3778
- const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
3779
- return {
3780
- prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
3781
- rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
3782
- };
3783
- }
3784
- /**
3785
- * Split a normalized Slack reply into multiple inline-safe thread messages.
3786
- *
3787
- * Replies split across more than two messages receive explicit continuation
3788
- * markers on non-final chunks. When
3789
- * `interrupted` is true, the final chunk receives an interruption marker.
3790
- */
3791
- function splitSlackReplyText(text, options) {
3792
- const normalized = normalizeSlackReplyMarkdown(text);
3793
- if (!normalized) return [];
3794
- const chunks = [];
3795
- const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
3796
- let remaining = normalized;
3797
- while (remaining) {
3798
- if (options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining)) {
3799
- chunks.push(options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining);
3800
- break;
3801
- }
3802
- const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
3803
- ...continuationBudget,
3804
- forceSplit: true
3805
- });
3806
- chunks.push(renderedPrefix);
3807
- remaining = rest;
3808
- }
3809
- if (chunks.length === 2) chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
3810
- return chunks;
3811
- }
3812
- /**
3813
- * Reserve enough inline budget for a continuation suffix on the current chunk.
3814
- */
3815
- function getSlackContinuationBudget() {
3816
- return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
3817
- }
3818
- /** Normalize text for Slack and wrap it as a PostableMessage. */
3819
- function buildSlackOutputMessage(text) {
3820
- const normalized = normalizeSlackReplyMarkdown(text);
3821
- if (!normalized) throw new Error(`Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`);
3822
- return { markdown: normalized };
3823
- }
3824
- // src/chat/slack/reply.ts
3825
- /**
3826
- * Send one destination-visible Slack reply.
3827
- *
3828
- * Chunks oversized text, builds compact attribution and conversation footer
3829
- * context, and posts through the shared Slack outbound boundary.
3830
- */
3831
- async function sendSlackReply(args) {
3832
- const posts = splitSlackReplyText(args.text).map((text) => ({
3833
- text,
3834
- cards: []
3835
- }));
3836
- const cards = (args.cards ?? []).map((card) => renderSlackCard(card, args.conversationId));
3837
- for (let index = 0; index < cards.length; index += 5) {
3838
- const group = cards.slice(index, index + 5);
3839
- const lastPost = posts.at(-1);
3840
- if (index === 0 && lastPost) lastPost.cards = group;
3841
- else posts.push({
3842
- text: "",
3843
- cards: group
3844
- });
3845
- }
3846
- const footer = buildSlackReplyFooter({
3847
- conversationId: args.conversationId,
3848
- replyAttribution: args.replyAttribution
3849
- });
3850
- const messageTs = [];
3851
- let threadTs = args.threadTs;
3852
- for (const [index, post] of posts.entries()) {
3853
- const { text, cards } = post;
3854
- const isFinalChunk = index === posts.length - 1;
3855
- const blocks = buildSlackReplyBlocks(text, void 0) ?? [];
3856
- for (const card of cards) if (!card.entity) blocks.push({
3857
- type: "section",
3858
- text: {
3859
- type: "mrkdwn",
3860
- text: card.text
3861
- }
3862
- });
3863
- if (isFinalChunk || !text.trim()) blocks.push(...buildSlackReplyBlocks("", footer) ?? []);
3864
- const entities = cards.flatMap((card) => card.entity ? [card.entity] : []);
3865
- const accessibleText = [text, ...cards.map((card) => card.text)].filter(Boolean).join("\n\n");
3866
- const fallbackText = isFinalChunk && args.replyAttribution ? `${accessibleText}\n\n${escapeSlackMrkdwnText(formatReplyAttribution(args.replyAttribution))}` : accessibleText;
3867
- const response = await postSlackMessage({
3868
- channelId: args.channelId,
3869
- threadTs,
3870
- text: fallbackText,
3871
- ...blocks.length ? { blocks } : void 0,
3872
- ...entities.length ? { entities } : void 0
3873
- });
3874
- if (response.ts) {
3875
- messageTs.push(response.ts);
3876
- threadTs ??= response.ts;
3877
- }
3878
- }
3879
- return messageTs;
3880
- }
3881
3050
  // src/chat/slack/assistant-thread/status-render.ts
3882
3051
  const DEFAULT_STATUS_CONTEXTS = {
3883
3052
  thinking: "…",
@@ -4235,6 +3404,21 @@ function formatSlackConversationRedactedLabel(context) {
4235
3404
  if (!context) return void 0;
4236
3405
  return formatSlackConversationTypeLabel(context.type);
4237
3406
  }
3407
+ // src/chat/slack/avatar.ts
3408
+ const AVATAR_SAVE_INTERVAL_MS = 864e5;
3409
+ /**
3410
+ * Save the Slack photo of one Slack Actor on their Slack Identity. Slack
3411
+ * Turns call this, so the saved photo follows profile changes within a day.
3412
+ */
3413
+ async function saveSlackActorAvatar(actor) {
3414
+ const state = getStateAdapter();
3415
+ const savedKey = `junior:slack:avatar-saved:${actor.teamId}:${actor.userId}`;
3416
+ if (await state.get(savedKey)) return;
3417
+ const profile = await lookupSlackUser(actor.teamId, actor.userId);
3418
+ if (!profile) return;
3419
+ await saveActorAvatar(getSqlExecutor(), actor, profile.avatarUrl ?? null);
3420
+ await state.set(savedKey, true, AVATAR_SAVE_INTERVAL_MS);
3421
+ }
4238
3422
  // src/chat/services/provider-default-config.ts
4239
3423
  const GITHUB_REPO_PART = String.raw`[A-Za-z0-9_.-]*[A-Za-z0-9_-]`;
4240
3424
  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");
@@ -4379,8 +3563,8 @@ async function saveSteeringMessages(args) {
4379
3563
  * Persist post-delivery Redis scratch with a short retry after durable SQL
4380
3564
  * completion has succeeded.
4381
3565
  */
4382
- async function persistThreadRuntimeStateWithRetry(thread, patch) {
4383
- await persistWithRetry(() => persistThreadRuntimeState(thread, patch));
3566
+ async function persistThreadRuntimeStateWithRetry(conversationId, patch) {
3567
+ await persistWithRetry(() => persistThreadRuntimeState(conversationId, patch));
4384
3568
  }
4385
3569
  /**
4386
3570
  * Resolve the Pi history for this Slack turn from the most precise durable
@@ -4432,6 +3616,10 @@ function createSlackTurn(deps) {
4432
3616
  visibility: destinationVisibility
4433
3617
  }) ?? "private"
4434
3618
  });
3619
+ const replyTarget = resolveSlackReplyTarget({
3620
+ destination,
3621
+ source
3622
+ });
4435
3623
  const slackActionToken = readSlackActionToken(message);
4436
3624
  const runId = options.execution?.dispatch?.id ?? getRunId(thread, message);
4437
3625
  const conversationId = options.conversationId ?? threadId ?? runId;
@@ -4471,8 +3659,10 @@ function createSlackTurn(deps) {
4471
3659
  executionActor = options.execution.dispatch.actor;
4472
3660
  credentialContext = options.execution.credentialContext;
4473
3661
  } else {
4474
- executionActor = await ensureSlackMessageActorIdentity(message, teamId, lookupSlackUser);
4475
- if (executionActor) credentialContext = credentialContextForActor(executionActor);
3662
+ const slackActor = await ensureSlackMessageActorIdentity(message, teamId, lookupSlackUser);
3663
+ await saveSlackActorAvatar(slackActor);
3664
+ executionActor = slackActor;
3665
+ credentialContext = credentialContextForActor(slackActor);
4476
3666
  }
4477
3667
  if (!executionActor || !credentialContext) throw new Error("Slack reply execution requires an actor");
4478
3668
  const actor = "userId" in executionActor ? executionActor : void 0;
@@ -4486,6 +3676,7 @@ function createSlackTurn(deps) {
4486
3676
  destination,
4487
3677
  locationConfiguration: options.execution?.locationConfiguration,
4488
3678
  context: {
3679
+ conversationId,
4489
3680
  threadId,
4490
3681
  actorId: slackActorId,
4491
3682
  channelId,
@@ -4503,17 +3694,16 @@ function createSlackTurn(deps) {
4503
3694
  };
4504
3695
  const postAuthPauseNotice = async (providerDisplayName, requestText) => {
4505
3696
  if (!actor) throw new Error("Slack auth pause notice requires an actor");
3697
+ if (!postsRunNotices(options.execution?.dispatch)) return;
4506
3698
  const text = buildAuthPauseResponse(actor.userId, providerDisplayName, requestText);
4507
3699
  try {
4508
3700
  await beforeFirstResponsePost();
4509
- if (channelId && threadTs) await sendSlackReply({
4510
- channelId,
3701
+ await sendSlackReply({
3702
+ ...replyTarget,
4511
3703
  conversationId,
4512
3704
  replyAttribution: options.execution?.dispatch?.replyAttribution,
4513
- text,
4514
- threadTs
3705
+ text
4515
3706
  });
4516
- else await thread.post(buildSlackOutputMessage(text));
4517
3707
  } catch (error) {
4518
3708
  logException(error, "slack.auth_pause.notice_post.failed", {
4519
3709
  "app.slack.reply_stage": "thread_reply_auth_pause_notice",
@@ -4567,7 +3757,7 @@ function createSlackTurn(deps) {
4567
3757
  const deliveredMessage = [...preparedState.conversation.messages].reverse().find((candidate) => candidate.role === "assistant" && candidate.id.startsWith(`${turnId}:assistant:`) && candidate.meta?.replied === true);
4568
3758
  options.onTurnDeliveryAccepted?.(deliveredMessage?.meta?.slackTs);
4569
3759
  options.onTurnOutcome?.({ outcome: "completed" });
4570
- await persistThreadState(thread, { conversation: preparedState.conversation });
3760
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
4571
3761
  await options.onTurnStatePersisted?.();
4572
3762
  await options.ack?.();
4573
3763
  await options.onTurnCompleted?.();
@@ -4590,7 +3780,7 @@ function createSlackTurn(deps) {
4590
3780
  });
4591
3781
  throw error;
4592
3782
  }
4593
- await persistThreadState(thread, { conversation: preparedState.conversation });
3783
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
4594
3784
  await options.onTurnStatePersisted?.();
4595
3785
  await options.ack?.();
4596
3786
  return;
@@ -4628,7 +3818,11 @@ function createSlackTurn(deps) {
4628
3818
  });
4629
3819
  if (configReply) {
4630
3820
  await beforeFirstResponsePost();
4631
- await thread.post(buildSlackOutputMessage(configReply.text));
3821
+ await sendSlackReply({
3822
+ ...replyTarget,
3823
+ conversationId,
3824
+ text: configReply.text
3825
+ });
4632
3826
  markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
4633
3827
  replied: true,
4634
3828
  skippedReason: void 0
@@ -4647,7 +3841,7 @@ function createSlackTurn(deps) {
4647
3841
  source: "slack"
4648
3842
  }
4649
3843
  });
4650
- await persistThreadState(thread, { conversation: preparedState.conversation });
3844
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
4651
3845
  await options.onTurnStatePersisted?.();
4652
3846
  await options.ack?.();
4653
3847
  return;
@@ -4688,7 +3882,7 @@ function createSlackTurn(deps) {
4688
3882
  "app.message.id": message.id,
4689
3883
  ...messageTs ? { "messaging.message.id": messageTs } : void 0
4690
3884
  });
4691
- await persistThreadState(thread, { conversation: preparedState.conversation });
3885
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
4692
3886
  await options.onTurnStatePersisted?.();
4693
3887
  if (actor) setSentryUser({
4694
3888
  id: actor.userId,
@@ -4775,16 +3969,15 @@ function createSlackTurn(deps) {
4775
3969
  }
4776
3970
  else {
4777
3971
  slackMessageTs = await sendSlackReply({
3972
+ ...replyTarget,
4778
3973
  cards,
4779
- channelId: destination.channelId,
4780
3974
  conversationId,
4781
3975
  replyAttribution: options.execution?.dispatch?.replyAttribution,
4782
- text,
4783
- ...threadTs ? { threadTs } : void 0
3976
+ text
4784
3977
  });
4785
3978
  messageDestinations.push(...slackMessageTs.map((messageId) => ({
4786
3979
  destination,
4787
- messageId: threadTs ?? messageId
3980
+ messageId: replyTarget.threadTs ?? messageId
4788
3981
  })));
4789
3982
  }
4790
3983
  } catch (error) {
@@ -4874,7 +4067,7 @@ function createSlackTurn(deps) {
4874
4067
  });
4875
4068
  if (compaction.compacted) {
4876
4069
  piMessages = compaction.piMessages;
4877
- await persistThreadState(thread, { conversation: preparedState.conversation });
4070
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
4878
4071
  }
4879
4072
  }
4880
4073
  const hasDurablePromptHistory = Boolean(piMessages?.length);
@@ -4983,11 +4176,11 @@ function createSlackTurn(deps) {
4983
4176
  drainSteeringMessages,
4984
4177
  shouldYield: options.shouldYield,
4985
4178
  onSandboxRefChanged: async (sandboxRef) => {
4986
- await persistThreadState(thread, { sandboxRef });
4179
+ await persistThreadStateById(conversationId, { sandboxRef });
4987
4180
  },
4988
4181
  recordPendingAuth: async (pendingAuth) => {
4989
4182
  preparedState.conversation.processing.pendingAuth = pendingAuth;
4990
- await persistThreadState(thread, { conversation: preparedState.conversation });
4183
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
4991
4184
  }
4992
4185
  }
4993
4186
  };
@@ -5066,7 +4259,7 @@ function createSlackTurn(deps) {
5066
4259
  conversation: completedState.conversation,
5067
4260
  conversationId
5068
4261
  }));
5069
- await persistThreadRuntimeStateWithRetry(thread, completedState);
4262
+ await persistThreadRuntimeStateWithRetry(conversationId, completedState);
5070
4263
  const automatedTurnLimit = await recordFinishedTurnForAutomatedLimit({
5071
4264
  conversationId,
5072
4265
  maxTurns: botConfig.maxConsecutiveAutomatedTurns,
@@ -5075,7 +4268,7 @@ function createSlackTurn(deps) {
5075
4268
  await maybePostAutomatedTurnLimitNotice({
5076
4269
  conversationId,
5077
4270
  maxTurns: botConfig.maxConsecutiveAutomatedTurns,
5078
- threadTs,
4271
+ source,
5079
4272
  update: automatedTurnLimit
5080
4273
  });
5081
4274
  } catch (saveError) {
@@ -5111,7 +4304,7 @@ function createSlackTurn(deps) {
5111
4304
  nowMs: Date.now(),
5112
4305
  sessionId: turnId
5113
4306
  });
5114
- await persistThreadState(thread, { conversation: preparedState.conversation });
4307
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
5115
4308
  if (conversationId) await turnLifecycle.fail({
5116
4309
  conversationId,
5117
4310
  createdAtMs: Date.now(),
@@ -5128,7 +4321,7 @@ function createSlackTurn(deps) {
5128
4321
  conversation: preparedState.conversation,
5129
4322
  sessionId: turnId
5130
4323
  });
5131
- await persistThreadState(thread, { conversation: preparedState.conversation });
4324
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
5132
4325
  persistedAtLeastOnce = true;
5133
4326
  shouldPersistFailureState = false;
5134
4327
  return;
@@ -5212,7 +4405,7 @@ function createSlackTurn(deps) {
5212
4405
  nowMs: Date.now(),
5213
4406
  sessionId: turnId
5214
4407
  });
5215
- await persistThreadState(thread, { conversation: preparedState.conversation });
4408
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
5216
4409
  await turnLifecycle.complete({
5217
4410
  conversationId,
5218
4411
  createdAtMs: Date.now(),
@@ -5269,7 +4462,7 @@ function createSlackTurn(deps) {
5269
4462
  } catch (recordError) {
5270
4463
  logException(recordError, "agent.turn.failed_session_record_persist.failed");
5271
4464
  }
5272
- await persistThreadState(thread, { conversation: preparedState.conversation });
4465
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
5273
4466
  if (shouldEmitDevAgentTrace()) logWarn("agent.turn.failed");
5274
4467
  }
5275
4468
  await status.clear();
@@ -5408,10 +4601,10 @@ async function seedConversationBackfill(thread, conversation, currentTurn, resol
5408
4601
  /** Build the turn-state preparer from injected conversation services. */
5409
4602
  function createPrepareTurnState(deps) {
5410
4603
  return async function prepareTurnState(args) {
5411
- const existingState = await args.thread.state;
4604
+ const conversationId = args.context.conversationId ?? args.context.threadId ?? args.context.runId;
4605
+ const existingState = conversationId ? await getPersistedThreadState(conversationId) : await args.thread.state;
5412
4606
  const sandboxRef = getPersistedSandboxState(existingState ?? {});
5413
4607
  const conversation = coerceThreadConversationState(existingState);
5414
- const conversationId = args.context.threadId ?? args.context.runId;
5415
4608
  if (conversationId && isVisionEnabled()) conversation.vision = await loadConversationVisionCache(conversationId);
5416
4609
  await hydrateConversationMessages({
5417
4610
  conversation,
@@ -5596,8 +4789,8 @@ function createSlackRuntime(options) {
5596
4789
  modelId: defaultModelId(botConfig),
5597
4790
  now: options.now ?? (() => Date.now()),
5598
4791
  prepareTurnState,
5599
- persistPreparedState: async ({ thread, preparedState }) => {
5600
- await persistThreadState(thread, { conversation: preparedState.conversation });
4792
+ persistPreparedState: async ({ conversationId, preparedState }) => {
4793
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
5601
4794
  },
5602
4795
  getPreparedConversationContext: (preparedState) => preparedState.conversationContext,
5603
4796
  decideSubscribedReply: async (args) => {
@@ -5612,18 +4805,18 @@ function createSlackRuntime(options) {
5612
4805
  });
5613
4806
  return decision;
5614
4807
  },
5615
- recordSkippedSteeringMessage: async ({ thread, message, decision, text }) => {
5616
- const conversation = coerceThreadConversationState(await thread.state);
4808
+ recordSkippedSteeringMessage: async ({ conversationId, message, decision, text }) => {
4809
+ const conversation = coerceThreadConversationState(await getPersistedThreadState(conversationId));
5617
4810
  recordSkippedConversationMessage({
5618
4811
  conversation,
5619
4812
  message,
5620
4813
  skippedReason: decision.reason,
5621
4814
  text: text.userText
5622
4815
  });
5623
- await persistThreadState(thread, { conversation });
4816
+ await persistThreadStateById(conversationId, { conversation });
5624
4817
  },
5625
- recordSkippedSubscribedTurn: async ({ thread, message, decision, completedAtMs, text }) => {
5626
- const conversation = coerceThreadConversationState(await thread.state);
4818
+ recordSkippedSubscribedTurn: async ({ conversationId, message, decision, completedAtMs, text }) => {
4819
+ const conversation = coerceThreadConversationState(await getPersistedThreadState(conversationId));
5627
4820
  recordSkippedConversationMessage({
5628
4821
  conversation,
5629
4822
  message,
@@ -5632,16 +4825,16 @@ function createSlackRuntime(options) {
5632
4825
  });
5633
4826
  clearSkippedTurnIfActive(conversation, message.id);
5634
4827
  conversation.processing.lastCompletedAtMs = completedAtMs;
5635
- await persistThreadState(thread, { conversation });
4828
+ await persistThreadStateById(conversationId, { conversation });
5636
4829
  },
5637
- onSubscribedMessageSkipped: async ({ thread, message, preparedState, decision, completedAtMs }) => {
4830
+ onSubscribedMessageSkipped: async ({ conversationId, message, preparedState, decision, completedAtMs }) => {
5638
4831
  markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
5639
4832
  replied: false,
5640
4833
  skippedReason: decision.reason
5641
4834
  });
5642
4835
  clearSkippedTurnIfActive(preparedState.conversation, message.id);
5643
4836
  preparedState.conversation.processing.lastCompletedAtMs = completedAtMs;
5644
- await persistThreadState(thread, { conversation: preparedState.conversation });
4837
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
5645
4838
  },
5646
4839
  executeSlackTurn,
5647
4840
  initializeAssistantThread: async ({ channelId, threadTs, sourceChannelId }) => {
@@ -5688,23 +4881,20 @@ function createJuniorSlackAdapter(config) {
5688
4881
  * authorization pause notices use `sendSlackReply` so the footer stays
5689
4882
  * consistent.
5690
4883
  */
5691
- async function postSlackMessageBestEffort(channelId, threadTs, text, conversationId, replyAttribution) {
4884
+ /** Post a failure reply or status notice when the run's dispatch allows it. */
4885
+ async function postRunNotice(resume, text) {
4886
+ if (!postsRunNotices(resume.run?.dispatch)) return;
4887
+ await sendSlackReply({
4888
+ channelId: resume.channelId,
4889
+ conversationId: resume.conversationId,
4890
+ replyAttribution: resume.run?.dispatch?.replyAttribution,
4891
+ text,
4892
+ threadTs: resume.threadTs
4893
+ });
4894
+ }
4895
+ async function postRunNoticeBestEffort(resume, text) {
5692
4896
  try {
5693
- if (conversationId) {
5694
- await sendSlackReply({
5695
- channelId,
5696
- conversationId,
5697
- replyAttribution,
5698
- text,
5699
- threadTs
5700
- });
5701
- return;
5702
- }
5703
- await postSlackMessage({
5704
- channelId,
5705
- threadTs,
5706
- text
5707
- });
4897
+ await postRunNotice(resume, text);
5708
4898
  } catch {}
5709
4899
  }
5710
4900
  /** Create a read-only configuration service from persisted values. */
@@ -5745,13 +4935,6 @@ function getResumeLogContext(args) {
5745
4935
  modelId: defaultModelId(botConfig)
5746
4936
  };
5747
4937
  }
5748
- async function postResumeFailureReply(args) {
5749
- await postSlackMessage({
5750
- channelId: args.channelId,
5751
- threadTs: args.threadTs,
5752
- text: buildTurnErrorResponse(args.error, args.eventId)
5753
- });
5754
- }
5755
4938
  async function handleResumeFailure(args) {
5756
4939
  const turnLifecycle = getTurnLifecycle();
5757
4940
  const capturedEventId = logException(args.error, args.eventName);
@@ -5775,12 +4958,7 @@ async function handleResumeFailure(args) {
5775
4958
  failureStatePersistError = persistError;
5776
4959
  }
5777
4960
  try {
5778
- await postResumeFailureReply({
5779
- channelId: args.resume.channelId,
5780
- threadTs: args.resume.threadTs,
5781
- eventId,
5782
- error: args.error
5783
- });
4961
+ await postRunNotice(args.resume, buildTurnErrorResponse(args.error, eventId));
5784
4962
  } catch (deliveryError) {
5785
4963
  const deliveryEventId = logException(deliveryError, "slack.resume.failure_delivery.failed", { "app.error.original_event_id": eventId });
5786
4964
  try {
@@ -5899,7 +5077,7 @@ async function resumeSlackTurnInContext(args) {
5899
5077
  channelId: runArgs.channelId,
5900
5078
  timestamp: runArgs.messageTs
5901
5079
  });
5902
- if (runArgs.initialText) await postSlackMessageBestEffort(runArgs.channelId, runArgs.threadTs, runArgs.initialText);
5080
+ if (runArgs.initialText) await postRunNoticeBestEffort(runArgs, runArgs.initialText);
5903
5081
  status.update(runArgs.initialStatus);
5904
5082
  const conversationId = runArgs.conversationId;
5905
5083
  const visibleConversationId = args.conversationId;
@@ -6154,7 +5332,7 @@ async function resumeSlackTurnInContext(args) {
6154
5332
  }
6155
5333
  if (deferredPauseHandler) try {
6156
5334
  await deferredPauseHandler();
6157
- if (deferredAuthInfo) await postSlackMessageBestEffort(runArgs.channelId, runArgs.threadTs, buildAuthPauseResponse(deferredAuthInfo.actorId, deferredAuthInfo.providerDisplayName, deferredAuthInfo.requestText), runArgs.conversationId, runArgs.run?.dispatch?.replyAttribution);
5335
+ if (deferredAuthInfo) await postRunNoticeBestEffort(runArgs, buildAuthPauseResponse(deferredAuthInfo.actorId, deferredAuthInfo.providerDisplayName, deferredAuthInfo.requestText));
6158
5336
  return true;
6159
5337
  } catch (pauseError) {
6160
5338
  await handleResumeFailure({
@@ -6347,16 +5525,22 @@ async function runPausedTurn(payload, options, runOptions = {}) {
6347
5525
  return withLogContext({ conversationId: payload.conversationId }, () => runPausedTurnInContext(payload, options, runOptions));
6348
5526
  }
6349
5527
  async function runPausedTurnInContext(payload, options, runOptions) {
6350
- const thread = parseSlackThreadId(payload.conversationId);
6351
5528
  const destination = requireSlackDestination(payload.destination, "Paused turn");
6352
5529
  const wakePausedTurn$1 = options.wakePausedTurn ?? wakePausedTurn;
5530
+ const routing = await resolveConversationRouting({ conversationId: payload.conversationId });
5531
+ const pausedTurn = await getTurnRecord(payload.conversationId, payload.turnId);
5532
+ const target = resolveSlackReplyTarget({
5533
+ destination,
5534
+ location: routing?.location,
5535
+ source: pausedTurn?.source
5536
+ });
6353
5537
  return await resumeSlackTurn({
6354
5538
  messageText: "",
6355
5539
  conversationId: payload.conversationId,
6356
5540
  turnId: payload.turnId,
6357
5541
  traceContext: payload.traceContext,
6358
- channelId: thread?.channelId ?? destination.channelId,
6359
- ...thread?.threadTs ? { threadTs: thread.threadTs } : void 0,
5542
+ channelId: target.channelId,
5543
+ ...target.threadTs ? { threadTs: target.threadTs } : void 0,
6360
5544
  executeTurn: async (run, saveResult) => await executeTurn(options.agentRunner, run, saveResult),
6361
5545
  scheduleSessionCompletedPluginTasks: options.scheduleSessionCompletedPluginTasks,
6362
5546
  beforeStart: async () => {
@@ -6569,6 +5753,23 @@ async function failStrandedTurnWithFallback(args) {
6569
5753
  failureCode: "agent_run_failed",
6570
5754
  turnId: failed.turnId
6571
5755
  });
5756
+ let replyAttribution;
5757
+ if (failed.dispatchId) {
5758
+ const dispatch = await getDispatchRecord(failed.dispatchId);
5759
+ await recordTurnSummary({
5760
+ conversationId: args.conversationId,
5761
+ destination: dispatch?.destination,
5762
+ dispatchId: failed.dispatchId,
5763
+ dispatchOutcome: "failed",
5764
+ turnId: failed.turnId,
5765
+ sliceId: failed.sliceId,
5766
+ source: dispatch?.source,
5767
+ state: "failed",
5768
+ surface: failed.surface
5769
+ });
5770
+ if (!dispatch || !postsRunNotices(buildDispatchRoutingContext(dispatch).dispatch)) return true;
5771
+ replyAttribution = dispatch.replyAttribution;
5772
+ }
6572
5773
  let routing;
6573
5774
  try {
6574
5775
  routing = await resolveTurnSessionRouting({ conversationId: args.conversationId });
@@ -6579,22 +5780,15 @@ async function failStrandedTurnWithFallback(args) {
6579
5780
  });
6580
5781
  return true;
6581
5782
  }
6582
- if (failed.dispatchId) await recordTurnSummary({
6583
- conversationId: args.conversationId,
6584
- destination: routing.destination,
6585
- dispatchId: failed.dispatchId,
6586
- dispatchOutcome: "failed",
6587
- turnId: failed.turnId,
6588
- sliceId: failed.sliceId,
6589
- source: routing.source,
6590
- state: "failed",
6591
- surface: failed.surface
5783
+ const target = resolveSlackReplyTarget({
5784
+ destination: requireSlackDestination(routing.destination, "Stranded paused turn"),
5785
+ location: routing.location,
5786
+ source: failed.source
6592
5787
  });
6593
- const thread = parseSlackThreadId(args.conversationId);
6594
- const channelId = thread?.channelId ?? requireSlackDestination(routing.destination, "Stranded paused turn").channelId;
6595
- await postSlackMessage({
6596
- channelId,
6597
- ...thread?.threadTs ? { threadTs: thread.threadTs } : void 0,
5788
+ await sendSlackReply({
5789
+ ...target,
5790
+ conversationId: args.conversationId,
5791
+ replyAttribution,
6598
5792
  text: buildTurnFailureResponse(requireTurnFailureEventId(eventId, eventName))
6599
5793
  });
6600
5794
  return true;
@@ -6775,6 +5969,7 @@ function createWebSteeringDrain(args) {
6775
5969
  text: normalizeConversationText(message.input.text),
6776
5970
  createdAtMs: message.createdAtMs,
6777
5971
  author,
5972
+ ...message.input.authorIdentityId ? { authorIdentityId: message.input.authorIdentityId } : void 0,
6778
5973
  meta: {
6779
5974
  ...attachments.length ? { attachments } : void 0,
6780
5975
  explicitMention: true,
@@ -6797,28 +5992,9 @@ function createWebSteeringDrain(args) {
6797
5992
  return accepted;
6798
5993
  };
6799
5994
  }
6800
- // src/chat/conversations/destination.ts
6801
- function requireLocalDestination(conversationId) {
6802
- const parsed = localDestinationSchema.safeParse({
6803
- platform: "local",
6804
- conversationId
6805
- });
6806
- if (!parsed.success) throw new Error(`Invalid local conversation id: ${conversationId}`);
6807
- return parsed.data;
6808
- }
6809
- /**
6810
- * Keep a stored Destination, or create the temporary local Destination.
6811
- *
6812
- * TODO(dcramer): Delete this module after Conversation and AgentRun use
6813
- * Location, and web and event Turns no longer need a local
6814
- * Destination.
6815
- */
6816
- function resolveConversationDestination(args) {
6817
- return args.existing ?? requireLocalDestination(args.conversationId);
6818
- }
6819
5995
  // src/chat/task-execution/conversation-turn.ts
6820
5996
  /** Run a mailbox Turn and store its assistant Messages. */
6821
- function stableHex$1(...parts) {
5997
+ function stableHex(...parts) {
6822
5998
  return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24);
6823
5999
  }
6824
6000
  function captureConversationTurnFailure(args) {
@@ -6876,6 +6052,14 @@ async function completeCancelledConversationTurn(args) {
6876
6052
  await args.acknowledge();
6877
6053
  }
6878
6054
  /**
6055
+ * Return the author Identity id that every batched input shares.
6056
+ * A batch with more than one author has no single author yet.
6057
+ */
6058
+ function sharedAuthorIdentityId(messages) {
6059
+ const ids = new Set(messages.map((message) => message.input.authorIdentityId));
6060
+ return ids.size === 1 ? [...ids][0] : void 0;
6061
+ }
6062
+ /**
6879
6063
  * Create the shared mailbox worker for web and event Turns.
6880
6064
  */
6881
6065
  function createConversationTurnWorker(agentRunner, deliverMessage) {
@@ -6898,6 +6082,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
6898
6082
  let inputMessageIds = [];
6899
6083
  let event;
6900
6084
  let batchEvents = [];
6085
+ let authorIdentityId;
6901
6086
  const storedConversation = await getConversationStore().get({ conversationId: context.conversationId });
6902
6087
  const mailboxDestination = resolved.kind === "mailbox" ? resolved.batch[0]?.message.destination : void 0;
6903
6088
  const destination = resolveConversationDestination({
@@ -6918,6 +6103,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
6918
6103
  userMessageId = first.message.inboundMessageId;
6919
6104
  turnId = buildDeterministicTurnId(userMessageId);
6920
6105
  inputMessageIds = resolved.batch.map((entry) => entry.message.inboundMessageId);
6106
+ authorIdentityId = sharedAuthorIdentityId(messages);
6921
6107
  turnInputFacts = first;
6922
6108
  } else turnId = resolved.turnId;
6923
6109
  const persisted = await getPersistedThreadState(context.conversationId);
@@ -7004,6 +6190,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
7004
6190
  text: normalizeConversationText(text),
7005
6191
  createdAtMs: startedAtMs,
7006
6192
  author,
6193
+ ...authorIdentityId ? { authorIdentityId } : void 0,
7007
6194
  meta: {
7008
6195
  ...attachments.length ? { attachments } : void 0,
7009
6196
  explicitMention: true,
@@ -7097,7 +6284,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
7097
6284
  if (stopSignal?.aborted) return await completeCancelledTurn();
7098
6285
  const piMessages = await loadProjection({ conversationId: context.conversationId });
7099
6286
  failureCode = "agent_run_failed";
7100
- currentRunId = `conversation-run:${stableHex$1(turnId, String(startedAtMs))}`;
6287
+ currentRunId = `conversation-run:${stableHex(turnId, String(startedAtMs))}`;
7101
6288
  setTags({ runId: currentRunId });
7102
6289
  const outcome = await executeTurn(agentRunner, {
7103
6290
  conversationId: context.conversationId,
@@ -7195,7 +6382,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
7195
6382
  await maybePostAutomatedTurnLimitNotice({
7196
6383
  conversationId: context.conversationId,
7197
6384
  maxTurns: botConfig.maxConsecutiveAutomatedTurns,
7198
- threadTs: conversationLocation?.provider === "slack" ? conversationLocation.threadTs : void 0,
6385
+ source,
7199
6386
  update: automatedTurnLimit
7200
6387
  });
7201
6388
  completedSuccessfully = reply.diagnostics.outcome === "success";
@@ -7307,11 +6494,13 @@ function createSlackSystemTurnDelivery(args) {
7307
6494
  installation: { teamId: location.teamId },
7308
6495
  state: args.state,
7309
6496
  task: async () => await sendSlackReply({
6497
+ ...resolveSlackReplyTarget({
6498
+ destination: location,
6499
+ location
6500
+ }),
7310
6501
  cards,
7311
- channelId: location.channelId,
7312
6502
  conversationId,
7313
- text,
7314
- ...location.threadTs ? { threadTs: location.threadTs } : void 0
6503
+ text
7315
6504
  })
7316
6505
  });
7317
6506
  } catch (error) {
@@ -7376,12 +6565,14 @@ function createConversationWork(options) {
7376
6565
  },
7377
6566
  runTurn: runtime.runDispatchTurn
7378
6567
  });
7379
- const invocationWorker = createAgentInvocationWorker(options.agentRunner, {
6568
+ const childWorker = createChildConversationWorker(options.agentRunner, {
7380
6569
  enqueueResult: async (invocation) => await enqueueAgentInvocationResult(invocation, {
7381
6570
  queue: options.queue,
7382
6571
  state: options.state
7383
6572
  }),
7384
- maxAgentDepth: botConfig.maxAgentDepth
6573
+ maxAgentDepth: botConfig.maxAgentDepth,
6574
+ queue: options.queue,
6575
+ state: options.state
7385
6576
  });
7386
6577
  const conversationTurnWorker = createConversationTurnWorker(options.agentRunner, createSlackSystemTurnDelivery({
7387
6578
  getSlackAdapter: options.getSlackAdapter,
@@ -7396,15 +6587,9 @@ function createConversationWork(options) {
7396
6587
  const run = async (context) => {
7397
6588
  const skipped = await skipInputBeforeConversationStop(context);
7398
6589
  if (skipped) return skipped;
7399
- const childInput = await startAgentInvocationsForChildInput(context, {
7400
- queue: options.queue,
7401
- state: options.state
7402
- });
7403
- if (childInput) return childInput;
6590
+ if ((await options.conversationStore.get({ conversationId: context.conversationId }))?.parentConversationId) return await childWorker(context);
7404
6591
  const mailboxTurn = await resolveMailboxTurnWork(context);
7405
6592
  if (mailboxTurn) return await conversationTurnWorker(context, await withAgentInvocationResultActors(mailboxTurn));
7406
- const invocationId = await resolveAgentInvocationId(context);
7407
- if (invocationId) return await invocationWorker(context, invocationId);
7408
6593
  const dispatchId = await resolveAgentDispatchId(context);
7409
6594
  if (dispatchId) return await dispatchWorker(context, dispatchId);
7410
6595
  return await destinationWorker(context);
@@ -7471,167 +6656,6 @@ function createProductionConversationWorkOptions(options) {
7471
6656
  waitUntil: options.waitUntil
7472
6657
  });
7473
6658
  }
7474
- // src/chat/conversations/web-input.ts
7475
- function normalizeEmail(email) {
7476
- return email.trim().toLowerCase();
7477
- }
7478
- function stableHex(...parts) {
7479
- return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24);
7480
- }
7481
- /**
7482
- * Build a durable Conversation id for one web Actor and create key.
7483
- *
7484
- * Retries with the same key must address the same Conversation before the
7485
- * mailbox Message id is derived.
7486
- */
7487
- function createConversationId(args) {
7488
- return `local:web:${stableHex(normalizeEmail(args.actorEmail), args.idempotencyKey)}`;
7489
- }
7490
- /** Return the stable Turn id for one mailbox Message. */
7491
- function conversationTurnIdForMessage(messageId) {
7492
- return buildDeterministicTurnId(messageId);
7493
- }
7494
- /**
7495
- * Store the dashboard participant in the current Conversation actor field.
7496
- *
7497
- * TODO(dcramer): Remove this StoredSlackActor conversion after Conversation
7498
- * actor storage accepts web Actors directly.
7499
- */
7500
- function storedActorFromWeb(actor) {
7501
- return {
7502
- ...actor.email ? { email: normalizeEmail(actor.email) } : void 0,
7503
- ...actor.fullName ? { fullName: actor.fullName } : void 0
7504
- };
7505
- }
7506
- /** Rebuild the web Actor from durable Conversation identity. */
7507
- function webActorFromEmail(email, profile) {
7508
- const normalized = normalizeEmail(email);
7509
- return {
7510
- platform: "web",
7511
- userId: `dashboard:${stableHex(normalized)}`,
7512
- email: normalized,
7513
- ...profile?.fullName ? { fullName: profile.fullName } : void 0,
7514
- ...profile?.userName ? { userName: profile.userName } : void 0
7515
- };
7516
- }
7517
- /** Build one web mailbox entry. */
7518
- function buildWebInboundMessage(args) {
7519
- const text = args.message.trim();
7520
- if (!text && !args.attachments?.length) throw new Error("Web Message must not be empty");
7521
- if (!args.actor.email) throw new Error("Web Actor requires a verified email");
7522
- const destination = resolveConversationDestination({
7523
- conversationId: args.conversationId,
7524
- existing: args.destination
7525
- });
7526
- const nowMs = args.nowMs ?? Date.now();
7527
- return {
7528
- conversationId: args.conversationId,
7529
- createdAtMs: args.createdAtMs ?? nowMs,
7530
- delivery: "defer",
7531
- destination,
7532
- inboundMessageId: args.messageId,
7533
- input: {
7534
- authorId: args.actor.userId,
7535
- text,
7536
- ...args.attachments?.length ? { attachments: args.attachments } : void 0,
7537
- metadata: {
7538
- authorEmail: normalizeEmail(args.actor.email),
7539
- ...args.actor.fullName && { authorFullName: args.actor.fullName },
7540
- authorUserId: args.actor.userId,
7541
- ...args.actor.userName && { authorUserName: args.actor.userName },
7542
- kind: "api_turn",
7543
- messageId: args.messageId
7544
- }
7545
- },
7546
- receivedAtMs: nowMs,
7547
- source: "web"
7548
- };
7549
- }
7550
- /** Record web activity and create a Conversation root when needed. */
7551
- async function recordWebConversationActivity(args) {
7552
- const store = args.conversationStore ?? getConversationStore();
7553
- const existing = await store.get({ conversationId: args.conversationId });
7554
- const destination = resolveConversationDestination({
7555
- conversationId: args.conversationId,
7556
- existing: existing?.destination
7557
- });
7558
- const isNewRoot = !existing;
7559
- const visibility = args.rootVisibility === "private" ? "private" : "public";
7560
- const source = isNewRoot ? createWebSource(args.conversationId, visibility) : void 0;
7561
- await store.recordActivity({
7562
- conversationId: args.conversationId,
7563
- destination,
7564
- nowMs: args.nowMs,
7565
- actor: storedActorFromWeb(args.actor),
7566
- ...isNewRoot ? { source: "web" } : void 0,
7567
- ...source ? { sessionSource: source } : void 0,
7568
- ...isNewRoot ? { visibility } : void 0
7569
- });
7570
- return destination;
7571
- }
7572
- /** Create a Conversation from web input and enqueue its first Message. */
7573
- async function createAndEnqueueConversation(input, options) {
7574
- if (!input.actor.email) throw new Error("Web Actor requires a verified email");
7575
- const conversationId = createConversationId({
7576
- actorEmail: input.actor.email,
7577
- idempotencyKey: input.idempotencyKey
7578
- });
7579
- return await appendAndEnqueueWebMessage({
7580
- actor: input.actor,
7581
- conversationId,
7582
- idempotencyKey: input.idempotencyKey,
7583
- message: input.message,
7584
- images: input.images,
7585
- rootVisibility: input.visibility === "private" ? "private" : "public"
7586
- }, options);
7587
- }
7588
- async function appendAndEnqueueWebMessage(input, options) {
7589
- const text = input.message.trim();
7590
- if (!text && !input.images?.length) throw new Error("Web Message must not be empty");
7591
- if (!input.actor.email) throw new Error("Web Actor requires a verified email");
7592
- const nowMs = options.nowMs ?? Date.now();
7593
- const messageId = await webMessageId({
7594
- conversationId: input.conversationId,
7595
- idempotencyKey: input.idempotencyKey
7596
- });
7597
- const destination = await recordWebConversationActivity({
7598
- actor: input.actor,
7599
- conversationId: input.conversationId,
7600
- conversationStore: options.conversationStore,
7601
- nowMs,
7602
- ...input.rootVisibility && { rootVisibility: input.rootVisibility }
7603
- });
7604
- let attachments = [];
7605
- if (input.images?.length) {
7606
- if (!options.attachmentStorage) throw new Error("Attachment storage is unavailable.");
7607
- attachments = await storeInputImages({
7608
- conversationId: input.conversationId,
7609
- files: input.images,
7610
- storage: options.attachmentStorage
7611
- });
7612
- }
7613
- const result = await (options.exclusive ? appendAndEnqueueExclusiveInboundMessage : appendAndEnqueueInboundMessage)({
7614
- message: buildWebInboundMessage({
7615
- actor: input.actor,
7616
- conversationId: input.conversationId,
7617
- destination,
7618
- message: text,
7619
- attachments,
7620
- messageId,
7621
- nowMs
7622
- }),
7623
- conversationStore: options.conversationStore,
7624
- nowMs,
7625
- queue: options.queue,
7626
- state: options.state
7627
- });
7628
- const status = result.status === "appended" ? "accepted" : result.status;
7629
- return {
7630
- conversationId: input.conversationId,
7631
- messageId,
7632
- status
7633
- };
7634
- }
7635
6659
  // src/chat/slack/location-url.ts
7636
6660
  /** Parse a Slack workspace subdomain used in `https://{domain}.slack.com` URLs. */
7637
6661
  function parseSlackTeamDomain(value) {
@@ -8343,7 +7367,7 @@ async function readLastUserMessageAtByConversation(db, conversationIds) {
8343
7367
  const rows = await db.select({
8344
7368
  conversationId: juniorConversationEvents.conversationId,
8345
7369
  lastUserMessageAt: sql`max(${juniorConversationEvents.createdAt})`
8346
- }).from(juniorConversationEvents).where(and(inArray(juniorConversationEvents.conversationId, conversationIds), or(and(eq(juniorConversationEvents.type, "user_message"), eq(authority, "instruction"), sql`${actorPlatform} in ('slack', 'local', 'web')`), and(eq(juniorConversationEvents.type, "message"), eq(role, "user"), isNotNull(juniorConversationEvents.actorIdentityId))))).groupBy(juniorConversationEvents.conversationId);
7370
+ }).from(juniorConversationEvents).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).where(and(inArray(juniorConversationEvents.conversationId, conversationIds), or(and(eq(juniorConversationEvents.type, "user_message"), eq(authority, "instruction"), sql`${actorPlatform} in ('slack', 'local', 'web')`), and(eq(juniorConversationEvents.type, "message"), eq(role, "user"), eq(juniorIdentities.kind, "user"))))).groupBy(juniorConversationEvents.conversationId);
8347
7371
  return new Map(rows.map((row) => [row.conversationId, new Date(row.lastUserMessageAt).toISOString()]));
8348
7372
  }
8349
7373
  // src/api/conversations/activity-preview.ts
@@ -8519,6 +7543,7 @@ async function attachActivityPreviewMentions(executor, previews) {
8519
7543
  }
8520
7544
  // src/api/conversations/participants.ts
8521
7545
  const identityColumns = {
7546
+ avatarUrl: juniorIdentities.avatarUrl,
8522
7547
  displayName: juniorIdentities.displayName,
8523
7548
  email: juniorIdentities.email,
8524
7549
  handle: juniorIdentities.handle,
@@ -8529,11 +7554,13 @@ const identityColumns = {
8529
7554
  userEmail: juniorUsers.primaryEmail,
8530
7555
  userId: juniorIdentities.userId
8531
7556
  };
8532
- function participantIdentity(row) {
7557
+ function participantIdentity(row, userAvatarUrls) {
7558
+ const avatarUrl = identityAvatarUrl(row, userAvatarUrls);
8533
7559
  const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
8534
7560
  const email = row.email?.trim() || row.userEmail?.trim();
8535
7561
  const slack = row.provider === "slack";
8536
7562
  const participant = {
7563
+ ...avatarUrl ? { avatarUrl } : void 0,
8537
7564
  ...email ? { email } : void 0,
8538
7565
  ...fullName ? { fullName } : void 0,
8539
7566
  ...slack && row.providerSubjectId ? { slackUserId: row.providerSubjectId } : void 0,
@@ -8551,10 +7578,11 @@ async function readConversationParticipants(db, conversationIds) {
8551
7578
  conversationId: juniorConversationEvents.conversationId,
8552
7579
  seq: juniorConversationEvents.seq,
8553
7580
  ...identityColumns
8554
- }).from(juniorConversationEvents).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(inArray(juniorConversationEvents.conversationId, [...conversationIds])).orderBy(asc(juniorConversationEvents.conversationId), asc(juniorConversationEvents.actorIdentityId), asc(juniorConversationEvents.seq))]);
7581
+ }).from(juniorConversationEvents).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(inArray(juniorConversationEvents.conversationId, [...conversationIds]), ne(juniorIdentities.kind, "system"))).orderBy(asc(juniorConversationEvents.conversationId), asc(juniorConversationEvents.actorIdentityId), asc(juniorConversationEvents.seq))]);
7582
+ const userAvatarUrls = await readUserAvatarUrls(db, [...rootRows, ...eventRows].flatMap((row) => row.userId ? [row.userId] : []));
8555
7583
  const ordered = /* @__PURE__ */ new Map();
8556
7584
  const add = (conversationId, row) => {
8557
- const participant = participantIdentity(row);
7585
+ const participant = participantIdentity(row, userAvatarUrls);
8558
7586
  if (!participant) return;
8559
7587
  const group = ordered.get(conversationId) ?? {
8560
7588
  identities: [],
@@ -8784,4 +7812,4 @@ async function readConversationFeedFromSql(options = {}) {
8784
7812
  async function readConversationFeed(options = {}) {
8785
7813
  return conversationFeedSchema.parse(await readConversationFeedFromSql(options));
8786
7814
  }
8787
- export { renderSlackObjectCard as $, readViewerAutomationRuns as A, recordSkippedConversationMessage as B, createProductionSlackWebhookServices as C, deleteViewerTask as D, ViewerTaskNotFoundError as E, assertNoLegacyAllowedDomains as F, normalizeIncomingSlackThreadId as G, clearSlackPendingReactions as H, isAllowedDomain as I, THREAD_OPTOUT_ACK as J, withNormalizedThreadId as K, isAllowedSignInEmail as L, readViewerAutomationVersions as M, readViewerAutomations as N, readConversationSourceTask as O, resolveViewerTaskCandidate as P, verifySlackSignature as Q, readEnvList as R, createProductionConversationWorkOptions as S, createSlackWebApiStatusSender as T, isSlackMessageStopped as U, buildSlackInboundMessage as V, stopSlackThread as W, parseContent as X, startProcessingReaction as Y, runWithSlackInstallation as Z, appendAndEnqueueWebMessage as _, resolveMentions as a, recordWebConversationActivity as b, readConversationAuxiliaryCostsFromSql as c, notCopiedByFork as d, getAttachmentStorage as et, childAgentTotalsReport as f, resolveSlackTeamDomains as g, conversationSummaryFromStoredConversation as h, attachConversationMentions as i, readHealthReport as it, readViewerAutomationSummary as j, readViewerAutomationExecutions as k, ConversationForkError as l, conversationEventHistory as m, readConversationRecordFromSql as n, enqueueAgentDispatch as nt, readFirstHumanMessageTexts as o, readConversationTreeMetricsFromSql as p, stopConversationTurn as q, readConversationParticipants as r, GET as rt, listConversationSidebarAnnotations as s, readConversationFeed as t, AGENT_DISPATCH_MAX_AGE_MS as tt, forkConversation as u, conversationTurnIdForMessage as v, getProductionSlackAdapter as w, webActorFromEmail as x, createAndEnqueueConversation as y, resolveAllowedDomains as z };
7815
+ export { stopSlackThread as A, actOnReviewItem as B, isAllowedSignInEmail as C, buildSlackInboundMessage as D, recordSkippedConversationMessage as E, bindMessageAuthorIdentityId as F, AGENT_DISPATCH_MAX_AGE_MS as G, readReviewItemForUser as H, startProcessingReaction as I, readHealthReport as J, enqueueAgentDispatch as K, stripLeadingBotMention as L, withNormalizedThreadId as M, stopConversationTurn as N, clearSlackPendingReactions as O, THREAD_OPTOUT_ACK as P, parseContent as R, isAllowedDomain as S, resolveAllowedDomains as T, reviewItemChange as U, closeReviewItemsAnsweredByReply as V, getAttachmentStorage as W, createProductionConversationWorkOptions as _, resolveMentions as a, createSlackWebApiStatusSender as b, readConversationAuxiliaryCostsFromSql as c, notCopiedByFork as d, childAgentTotalsReport as f, resolveSlackTeamDomains as g, conversationSummaryFromStoredConversation as h, attachConversationMentions as i, normalizeIncomingSlackThreadId as j, isSlackMessageStopped as k, ConversationForkError as l, conversationEventHistory as m, readConversationRecordFromSql as n, readFirstHumanMessageTexts as o, readConversationTreeMetricsFromSql as p, GET as q, readConversationParticipants as r, listConversationSidebarAnnotations as s, readConversationFeed as t, forkConversation as u, createProductionSlackWebhookServices as v, readEnvList as w, assertNoLegacyAllowedDomains as x, getProductionSlackAdapter as y, ReviewItemActionError as z };