@sentry/junior 0.266.0 → 0.268.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 (87) hide show
  1. package/dist/{actor-tlxzqaRe.js → actor-CP9K2Idu.js} +9 -1
  2. package/dist/{adapter-eRFA_naf.js → adapter-04AcD2SO.js} +5 -7
  3. package/dist/{admin-tILpP51p.js → admin-DIU1QyG3.js} +1 -1
  4. package/dist/{agent-invocations-CxKldAeB.js → agent-invocations-C1RNOCs8.js} +2 -0
  5. package/dist/{agent-runner-BtYK7bht.js → agent-runner-CAYfFmU7.js} +2636 -601
  6. package/dist/api/schema.d.ts +2 -5454
  7. package/dist/api/schema.js +6 -5
  8. package/dist/api.d.ts +6 -1
  9. package/dist/api.js +255 -1208
  10. package/dist/{app-Csbi9dW7.d.ts → app-z6Uuyq3j.d.ts} +23 -4
  11. package/dist/app.d.ts +2 -2
  12. package/dist/app.js +1869 -378
  13. package/dist/{automation-BKpY9LPa.js → automation-CmeFBSEh.js} +42 -14
  14. package/dist/{briefs-DgfWMU7e.js → briefs-DQbV2jNr.js} +4 -4
  15. package/dist/{catalog-runtime-DAJjAbSx.js → catalog-runtime-BD9GSpc6.js} +1 -1
  16. package/dist/cli/chat.js +15 -15
  17. package/dist/cli/check.js +3 -3
  18. package/dist/cli/init.js +0 -1
  19. package/dist/cli/main.js +3 -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-B0YZfxNy.js → client-C1yDYjoS.js} +2 -2
  25. package/dist/{config-C0UwYqDK.js → config-DrTeF80T.js} +32 -10
  26. package/dist/{connections-Bwu3VORi.js → connections-D46f8vvi.js} +12 -12
  27. package/dist/{conversation-DF4rUcjb.js → conversation-DSo310Ps.js} +8 -1
  28. package/dist/{conversation-privacy-CY7JVjxn.js → conversation-privacy-CxlOCiUo.js} +1 -1
  29. package/dist/{credential-sync-VGAMUTGp.js → credential-sync-sm9fegLY.js} +29 -34
  30. package/dist/{credentialed-enF5Bf3y.js → credentialed-lukuD-cc.js} +2933 -1026
  31. package/dist/db-BU4LQD8s.d.ts +6 -0
  32. package/dist/{executor-TnWbvL09.js → executor-4vhvsMZJ.js} +83 -6
  33. package/dist/instrumentation.js +6 -2
  34. package/dist/{assistant-reply-UyzAfZQo.js → interruption-marker-CkncDT8d.js} +8 -2
  35. package/dist/{job-queue-DBpHmKnd.js → job-queue-In9QKz-I.js} +128 -3
  36. package/dist/{web-input-B-bMjD1V.js → list-BkgnXEEB.js} +1611 -817
  37. package/dist/{logging-Ce0mXZ8J.js → logging-DLSpkSSr.js} +11 -3
  38. package/dist/{message-VZ3DFpGb.js → message-BObyg59s.js} +4 -4
  39. package/dist/nitro.d.ts +1 -1
  40. package/dist/nitro.js +3 -3
  41. package/dist/{oauth-callback-server-CpqWLjVY.js → oauth-callback-server-B-WbSdoH.js} +2 -2
  42. package/dist/{prompt-BcCGlmg2.js → prompt-BW9jFsqZ.js} +1 -1
  43. package/dist/{registry-1BEIPdbT.js → registry-BgecKva0.js} +2 -2
  44. package/dist/{web-message-id-CJSUiksE.js → reporting-window-DVCD_10C.js} +1 -10
  45. package/dist/{resolve-BSHHkEGE.js → resolve-Cpa4Bu1O.js} +3 -3
  46. package/dist/{result-CwOpzqdh.js → result-D3TSmZZ8.js} +9 -9
  47. package/dist/{runner-CEaX04kz.js → runner-DS5OKhEp.js} +11 -11
  48. package/dist/{sandbox-egress-signals-BwtGHwqa.js → sandbox-egress-signals-CnDjY5rx.js} +1 -1
  49. package/dist/{schema-RsaBHxRH.js → schema-0050HFnB.js} +82 -5
  50. package/dist/schema-ZngqcOWM.d.ts +5813 -0
  51. package/dist/{service-account-6wC76eBZ.js → service-account-CKV2WTY7.js} +3 -3
  52. package/dist/{service-account-Dg4lIbHm.js → service-account-DGjnDw9y.js} +3 -3
  53. package/dist/{session-stop-tZOw9AJD.js → session-stop-Dam1EI7B.js} +527 -134
  54. package/dist/{skills-DzyGN7wd.js → skills-CfAnfImD.js} +2 -2
  55. package/dist/{spawn-6rokq7l_.js → spawn-cmq9p4Ly.js} +2 -2
  56. package/dist/{store-DcKkzrdt.js → store-C5PN02QO.js} +4 -4
  57. package/dist/{store-CG0VJyye.d.ts → store-xMuuXRE0.d.ts} +23 -9
  58. package/dist/store-zPEoJPBB.js +295 -0
  59. package/dist/{task-queue-Bg4RS2Gy.js → task-queue-_mf28dSu.js} +10 -5
  60. package/dist/{task-runner-8N5LIkSg.js → task-runner-DMI2prDO.js} +25 -14
  61. package/dist/{tasks-DDT-J3Rf.js → tasks-SB9z-VTU.js} +1551 -54
  62. package/dist/{thread-state-cprEaxgl.js → thread-state-BbbPh0Sn.js} +8 -18
  63. package/dist/{turn-failure-response-C8sMdsdX.js → turn-failure-response-BKKmnMB5.js} +11 -16
  64. package/dist/{turn-lifecycle-DaBVoz7h.js → turn-lifecycle-oP0kVeGz.js} +1 -1
  65. package/dist/{validation-C2lllinq.js → validation--JsAGLwc.js} +1 -1
  66. package/dist/{version-oaLfxxnf.js → version-jHwJp4hC.js} +1 -1
  67. package/dist/version.js +1 -1
  68. package/dist/{viewer-BpE6Z3qf.js → viewer-CpXTGlep.js} +329 -230
  69. package/dist/web-message-id-DlHNANtd.js +132 -0
  70. package/dist/{work-VX1SHWha.js → work-3N-GHY9m.js} +10 -10
  71. package/dist/{worker-hook-y_Y.js → worker-CCdbcItd.js} +30 -19
  72. package/migrations/0064_any_resource_event_triggers.sql +2 -0
  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/meta/0064_snapshot.json +3505 -0
  78. package/migrations/meta/0065_snapshot.json +3511 -0
  79. package/migrations/meta/0066_snapshot.json +4099 -0
  80. package/migrations/meta/0067_snapshot.json +3921 -0
  81. package/migrations/meta/0068_snapshot.json +4176 -0
  82. package/migrations/meta/_journal.json +36 -1
  83. package/package.json +19 -4
  84. package/dist/db-BnRqQ7G5.d.ts +0 -5
  85. package/dist/images-BKQqSi5W.js +0 -64
  86. package/dist/store-FALy-JQb.js +0 -1424
  87. package/dist/turn-session-routing-a9CHX-w6.js +0 -76
@@ -1,41 +1,42 @@
1
- import { $ as persistConversationMessageSummaries, A as commitAcceptedReply, At as abandonTurnRecord, Bt as buildTurnErrorResponse, E as sameDefinitionValue, Et as wasPendingAuthStopped, F as loadProjection, Ft as recordTurnSummary, H as recordAuthorizationCompleted, K as recordSubscribedReplyRoute, Mt as failTurnRecord, N as loadConversationProjection, Nt as getTurnRecord, Ot as loadTurnCheckpoint, Pt as listTurnSummaries, S as automationDefinition, Ut as persistWithRetry, X as hydrateConversationMessages, Z as persistConversationMessages, _ as parseEventAutomationRow, _n as isExperimentalFeatureEnabled, at as buildConversationContext, bn as configValueSchema, bt as AuthorizationFlowDisabledError, c as withScheduledAutomationLock, cn as buildDeterministicTurnId, ct as getConversationMessageSlackTs, dn as getSubscribedReplyPreflightDecision, dt as normalizeConversationText, et as resolveConversationTitle, f as deleteEventAutomation, fn as getThreadStopDecision, ft as recordDeliveredAssistantMessage, gt as PluginCredentialFailureError, h as listDeletedEventAutomationsCreatedBy, i as readScheduledAutomation, it as appendThreadContextMessages, j as commitMessages, jt as completeTurnRecord, kt as saveTurnCheckpoint, lt as isHumanConversationMessage, mt as upsertConversationMessage, nt as fallbackShortTitle, o as saveScheduledAutomationInLock, ot as createConversationMemoryService, pt as turnHasReply, r as parseScheduledAutomationRow, rn as trimTrailingAssistantMessages, sn as buildDeterministicAssistantMessageId, st as estimateConversationContextTokens, un as decideSubscribedThreadReply, ut as markConversationMessage, w as listAutomationVersions, wt as clearPendingAuth } from "./tasks-DDT-J3Rf.js";
2
- import { a as completeText, p as escapeXml, r as completeDecision, y as isProviderRetryError } from "./client-B0YZfxNy.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-C0UwYqDK.js";
4
- import { B as toOptionalNumber, T as withSpan, V as toOptionalString, d as logError, f as logException, m as logWarn, n as createChatSdkLogger, o as getActiveTraceId, p as logInfo, t as buildTurnFailureResponse, v as setSentryUser, w as withLogContext, x as setTags, y as setSpanAttributes, z as isRecord } from "./logging-Ce0mXZ8J.js";
1
+ import { A as resolveConversationRouting, At as upsertConversationMessage, Bt as clearPendingAuth, Ct as estimateConversationContextTokens, Dt as normalizeConversationText, E as sameDefinitionValue, Et as markConversationMessage, Gt as saveTurnCheckpoint, Ht as wasPendingAuthStopped, In as persistWithRetry, It as AuthorizationFlowDisabledError, Jt as failTurnRecord, K as commitAcceptedReply, Kt as abandonTurnRecord, Mt as PluginCredentialFailureError, N as automationEventCatalog, Nn as buildTurnErrorResponse, Ot as recordDeliveredAssistantMessage, Q as loadTurnProjection, S as automationDefinition, St as createConversationMemoryService, Tt as isHumanConversationMessage, V as registeredEventTriggerProblems, Wt as loadTurnCheckpoint, Xn as trimTrailingAssistantMessages, Xt as listTurnSummaries, Y as loadConversationProjection, Yt as getTurnRecord, Z as loadProjection, Zt as recordTurnSummary, _ as parseEventAutomationRow, ar as getThreadStopDecision, bt as appendThreadContextMessages, c as withScheduledAutomationLock, ct as recordSubscribedReplyRoute, er as buildDeterministicAssistantMessageId, f as deleteEventAutomation, ft as hydrateConversationMessages, gt as resolveConversationTitle, h as listDeletedEventAutomationsCreatedBy, hr as piMessageFromHistoryItem, ht as persistConversationMessageSummaries, i as readScheduledAutomation, ir as getSubscribedReplyPreflightDecision, it as recordAuthorizationCompleted, j as resolveTurnSessionRouting, kt as turnHasReply, o as saveScheduledAutomationInLock, pr as configValueSchema, pt as persistConversationMessages, q as commitMessages, qt as completeTurnRecord, r as parseScheduledAutomationRow, rr as decideSubscribedThreadReply, tr as buildDeterministicTurnId, ur as isExperimentalFeatureEnabled, vt as fallbackShortTitle, w as listAutomationVersions, wt as getConversationMessageSlackTs, xt as buildConversationContext } from "./tasks-SB9z-VTU.js";
2
+ import { a as completeText, p as escapeXml, r as completeDecision, y as isProviderRetryError } from "./client-C1yDYjoS.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-DrTeF80T.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-DLSpkSSr.js";
5
+ import { i as getActiveSpan, r as continueTrace } from "./sentry-MDkYAVPV.js";
5
6
  import { o as modelIdForProfile, r as defaultModelId } from "./model-profile-Bg8EoCKD.js";
6
- import { a as isUserActor, g as parseSlackMessageTs, i as isActorUserId, l as parseSlackChannelId, n as createSlackActor, o as parseActorUserId, r as createSlackResumeActor, t as createActor } from "./actor-tlxzqaRe.js";
7
- import { B as requireSlackDestination, R as createSlackDestination, d as getConversationStore, f as getDb, l as getConversationEventStore, p as getSqlExecutor } from "./viewer-BpE6Z3qf.js";
8
- import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers } from "./agent-invocations-CxKldAeB.js";
9
- import { a as juniorAutomationRuns, s as juniorAutomations } from "./executor-TnWbvL09.js";
10
- import { p as instructionProvenanceFor } from "./history-C84mlAwO.js";
11
- import { d as actorLogContext, f as credentialContextForActor } from "./registry-1BEIPdbT.js";
12
- import { A as listThreadReplies, D as resolveSlackChannelIdFromMessage, E as readSlackRawMessageContext, I as describeEventAutomationTrigger, L as effectiveTaskOutcomes, O as resolveSlackChannelIdFromThreadId, Q as RetryableDeliveryError, T as parseSlackThreadId, X as buildSteeringPiMessage, Z as withoutSavedSteeringMessages, b as renderBlockText, i as buildAuthPauseResponse, k as getConversationInfo, n as executeAgentRun, nt as shouldEmitDevAgentTrace, p as latestProgressStatus, r as createContextCompactor, t as createAgentRunner, w as storeAttachment, y as renderAttachmentText } from "./agent-runner-BtYK7bht.js";
13
- import { $t as getHeaderString, Kn as readAttachmentVisionSummary, Nt as cancelSubscriptions, Un as buildSentryConversationUrl, Wn as getSlackFileId, Xt as SlackActionError, Yt as verifyTimerWatchCredentialSubject, Zt as downloadPrivateSlackFile, dn as getDashboardObjectIconLink, dt as normalizeSlackReplyMarkdown, en as getSlackClient, ft as normalizeSlackStatusText, ht as addReactionToMessage, in as runWithSlackInstallationToken, l as getPluginSlackConversationLink, ln as JUNIOR_THREAD_STATE_TTL_MS, lt as escapeSlackMrkdwnText, mt as truncateStatusText, n as createUserTokenStore, nn as isDmChannel, qn as recordSlackAttachment, rn as normalizeSlackConversationId, tr as lookupSlackUser, un as getDashboardConversationLink, ut as formatSlackLink, vt as postSlackMessage, yt as removeReactionFromMessage } from "./credentialed-enF5Bf3y.js";
14
- import { a as acquireActiveLock, i as MUTATION_LOCK_TTL_MS, r as getStateAdapter, s as withLock } from "./adapter-eRFA_naf.js";
15
- import { B as readMessageAttachments, U as isVisionImageMediaType } from "./conversation-DF4rUcjb.js";
16
- import { a as EVENT_SYSTEM_ACTOR, o as isEventConversationMessage } from "./prompt-BcCGlmg2.js";
17
- 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-DcKkzrdt.js";
18
- import { n as getAssistantReplyText } from "./assistant-reply-UyzAfZQo.js";
19
- import { B as finishedRunReply, F as recordFinishedTurnForAutomatedLimit, M as joinMailboxText, P as maybePostAutomatedTurnLimitNotice, U as runDispatchOutcome, c as isEventMailboxMetadata, d as getEventCatalog, i as skipInputBeforeConversationStop, l as renderEventBatchText, s as eventBatchMessageEvents, u as timerWatchCredentialSubject, x as automationEventCatalog, z as deliversFinalText } from "./session-stop-tZOw9AJD.js";
20
- import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline } from "./job-queue-DBpHmKnd.js";
21
- import { E as stopConversationWork, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, n as appendAndEnqueueExclusiveInboundMessage, r as appendAndEnqueueInboundMessage, w as requestConversationWork } from "./store-FALy-JQb.js";
22
- import { l as automationListQuerySchema } from "./automation-BKpY9LPa.js";
23
- import { c as sumUtcHoursIntoSixHours, m as healthReportSchema, t as webMessageId } from "./web-message-id-CJSUiksE.js";
24
- import { A as readAutomationExecutions, D as readAutomationExecutionStatusDays, E as readAutomationExecutionHours, 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-8N5LIkSg.js";
25
- import { a as withAgentInvocationResultActors, c as resolveMailboxTurnWork, i as startAgentInvocationsForChildInput, l as sourceFromTurnInput, o as getAuthPausedConversationTurnId, s as getEventBatchDelayMs, t as enqueueAgentInvocationResult, u as turnInputFactsFromConversationMessage } from "./result-CwOpzqdh.js";
26
- import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-DaBVoz7h.js";
27
- import { i as resolveAgentInvocationId, o as AgentRunError, s as executeTurn, t as createAgentInvocationWorker } from "./work-VX1SHWha.js";
28
- 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-cprEaxgl.js";
29
- import { c as deleteWebAuthorization, d as createPausedTurns, f as getPausedTurnRequest, h as getTurnAuthorization, m as wakePausedTurn, s as createWebAuthorization } from "./connections-Bwu3VORi.js";
30
- import { n as resolveTurnSessionRouting } from "./turn-session-routing-a9CHX-w6.js";
31
- import { t as bindMessageAgent } from "./message-VZ3DFpGb.js";
32
- import { t as bindSpawnAgent } from "./spawn-6rokq7l_.js";
33
- 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-C8sMdsdX.js";
34
- import { r as storeInputImages } from "./images-BKQqSi5W.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 { L as hasAgentTurnUsage, M as parseSessionSource, N as addAgentTurnUsage, V as requireSlackDestination, _ as withConversationEventLock, d as getConversationEventStore, h as getSqlExecutor, m as getDb, p as getConversationStore, v as loadConversationSlackTenantIds, x as locationFromRow, z as createSlackDestination } from "./viewer-CpXTGlep.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, d as juniorAutomations, g as juniorConversationBriefs, l as juniorAutomationRuns, y as juniorConversationEvents } from "./executor-4vhvsMZJ.js";
11
+ import { d as contextProvenance, p as instructionProvenanceFor } from "./history-C84mlAwO.js";
12
+ import { d as actorLogContext, f as credentialContextForActor } from "./registry-BgecKva0.js";
13
+ import { A as getConversationInfo, D as readSlackRawMessageContext, E as parseSlackThreadId, Ft as buildSteeringPiMessage, H as syncReviewItemSlackCards, It as withoutSavedSteeringMessages, L as describeEventAutomationTrigger, Mt as conversationSubtreeColumns, O as resolveSlackChannelIdFromMessage, R as effectiveTaskOutcomes, Ut as shouldEmitDevAgentTrace, _t as recordWebConversationActivity, at as getReviewItem, b as renderBlockText, dt as sendReviewItemMessages, et as runWithSlackInstallation, gt as createConversationId, i as buildAuthPauseResponse, j as listThreadReplies, jt as conversationSubtree, k as resolveSlackChannelIdFromThreadId, lt as takeReviewItem, n as executeAgentRun, nt as canReadReviewItem, p as latestProgressStatus, r as createContextCompactor, rt as closeReviewItem, st as reviewItemReaderForUser, t as createAgentRunner, vt as webActorFromEmail, wt as storeAttachment, y as renderAttachmentText, yt as resolveConversationDestination, zt as RetryableDeliveryError } from "./agent-runner-CAYfFmU7.js";
14
+ import { $n as recordSlackAttachment, $t as verifyTimerWatchCredentialSubject, Bn as conversationArchivedForUser, Gn as viewerConversationMembership, Hn as conversationIsMemberRoot, In as createPluginLogger, J as truncateStatusText, K as normalizeSlackStatusText, Ln as readConversationAccessFromSql, Lt as cancelSubscriptions, Qn as readAttachmentVisionSummary, Rn as conversationArchiveTimes, Un as conversationNotArchivedForEmail, Vn as conversationHasParticipantUser, Wn as conversationNotArchivedForUser, Xn as getSlackFileId, Y as addReactionToMessage, cr as createPluginCache, en as SlackActionError, in as getSlackClient, ir as listConversationAnnotationsById, ln as withSlackRetries, n as createUserTokenStore, nt as removeReactionFromMessage, on as isDmChannel, p as getPlugins, pn as JUNIOR_THREAD_STATE_TTL_MS, rn as getHeaderString, sn as normalizeSlackConversationId, sr as lookupSlackUser, tn as downloadPrivateSlackFile, zn as conversationArchivedForEmail } from "./credentialed-lukuD-cc.js";
15
+ import { a as MUTATION_LOCK_TTL_MS, c as withLock, i as getStateAdapter, o as acquireActiveLock } from "./adapter-04AcD2SO.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-C5PN02QO.js";
19
+ import { r as getAssistantReplyText } from "./interruption-marker-CkncDT8d.js";
20
+ import { C as maybePostAutomatedTurnLimitNotice, D as recordFinishedTurnForAutomatedLimit, I as runDispatchOutcome, M as finishedRunReply, N as postsRunNotices, T as sendSlackReply, c as isEventMailboxMetadata, d as getEventCatalog, i as skipInputBeforeConversationStop, j as deliversFinalText, l as renderEventBatchText, s as eventBatchMessageEvents, u as timerWatchCredentialSubject, w as resolveSlackReplyTarget, x as joinMailboxText } from "./session-stop-Dam1EI7B.js";
21
+ import { a as isEmptyUserInputAnswer, o as normalizeUserInputAnswer } from "./web-message-id-DlHNANtd.js";
22
+ import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline } from "./job-queue-In9QKz-I.js";
23
+ import { D as stopConversationWork, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, r as appendAndEnqueueInboundMessage, w as requestConversationWork } from "./store-zPEoJPBB.js";
24
+ import { j as eventAutomationTriggerSchema, l as automationListQuerySchema } from "./automation-CmeFBSEh.js";
25
+ import { p as healthReportSchema, s as sumUtcHoursIntoSixHours } from "./reporting-window-DVCD_10C.js";
26
+ 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-DMI2prDO.js";
27
+ 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-D3TSmZZ8.js";
28
+ import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-oP0kVeGz.js";
29
+ import { i as resolveAgentInvocationId, o as AgentRunError, s as executeTurn, t as createAgentInvocationWorker } from "./work-3N-GHY9m.js";
30
+ import { a as persistThreadStateById, i as persistThreadRuntimeState, n as getPersistedSandboxState, o as coerceThreadConversationState, r as getPersistedThreadState, t as getLocationConfigurationService } from "./thread-state-BbbPh0Sn.js";
31
+ import { c as deleteWebAuthorization, d as createPausedTurns, f as getPausedTurnRequest, h as getTurnAuthorization, m as wakePausedTurn, s as createWebAuthorization } from "./connections-D46f8vvi.js";
32
+ import { t as bindMessageAgent } from "./message-BObyg59s.js";
33
+ import { t as bindSpawnAgent } from "./spawn-cmq9p4Ly.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-BKKmnMB5.js";
35
35
  import { z } from "zod";
36
36
  import { createHash } from "node:crypto";
37
- import { and, asc, count, desc, eq, exists, inArray, ne, or, sql } from "drizzle-orm";
38
- import { createSlackSource, createWebSource, localDestinationSchema, objectFactFields, objectPresentation, objectTypeSchema } from "@sentry/junior-plugin-api";
37
+ import { and, asc, count, desc, eq, exists, inArray, isNotNull, isNull, ne, notInArray, or, sql } from "drizzle-orm";
38
+ import { conversationSidebarAnnotationSchema, createSlackSource, objectTypeSchema } from "@sentry/junior-plugin-api";
39
+ import { alias } from "drizzle-orm/pg-core";
39
40
  import { del, get, put } from "@vercel/blob";
40
41
  import { Message, ThreadImpl, stringifyMarkdown } from "chat";
41
42
  import { SlackFormatConverter, createSlackAdapter } from "@chat-adapter/slack";
@@ -107,8 +108,7 @@ async function enqueueAgentDispatch(dispatch, options) {
107
108
  destination: claimedDispatch.destination,
108
109
  nowMs,
109
110
  source: "plugin",
110
- sessionSource: claimedDispatch.source,
111
- visibility: claimedDispatch.destinationVisibility
111
+ sessionSource: claimedDispatch.source
112
112
  });
113
113
  await appendAndEnqueueInboundMessage({
114
114
  message: buildAgentDispatchInboundMessage(claimedDispatch, nowMs),
@@ -213,7 +213,6 @@ async function persistBlockedDispatchTurn(dispatch, error) {
213
213
  actor: dispatch.actor,
214
214
  conversationId,
215
215
  destination: dispatch.destination,
216
- destinationVisibility: dispatch.destinationVisibility,
217
216
  dispatchId: dispatch.id,
218
217
  dispatchOutcome: "blocked",
219
218
  turnId: sessionId,
@@ -380,207 +379,140 @@ function createAttachmentStorage(config) {
380
379
  default: throw new Error(`Unsupported attachment storage provider: ${String(config.provider)}`);
381
380
  }
382
381
  }
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
- }
382
+ // src/chat/review-items/actions.ts
383
+ /** An expected failure of a person action, for the API to report. */
384
+ var ReviewItemActionError = class extends Error {
385
+ code;
386
+ constructor(code, message) {
387
+ super(message);
388
+ this.name = "ReviewItemActionError";
389
+ this.code = code;
390
+ }
391
+ };
392
+ /**
393
+ * Return whether a User can act on an open Review item: an assigned User,
394
+ * anyone for an item assigned to Everyone, and anyone for an item with no
395
+ * assignees. The caller checks that the User can see the item.
396
+ */
397
+ function canActOnReviewItem(item, userId) {
398
+ if (item.status !== "open") return false;
399
+ if (item.assignedToEveryone) return true;
400
+ if (item.assignees.length === 0) return true;
401
+ return item.assignees.some((assignee) => assignee.userId === userId);
402
+ }
403
+ /** Return whether a User is already the only assignee, so take does nothing. */
404
+ function isOnlyAssignee(item, userId) {
405
+ return !item.assignedToEveryone && item.assignees.length === 1 && item.assignees[0]?.userId === userId;
406
+ }
407
+ /** Read one Review item that a User can see, or fail as not found. */
408
+ async function readReviewItemForUser(user, id) {
409
+ const item = await getReviewItem(getDb(), id);
410
+ if (!item || !canReadReviewItem(item, reviewItemReaderForUser(user))) throw new ReviewItemActionError("not_found", "Review item not found.");
411
+ return item;
412
+ }
413
+ function closedStatus(action) {
414
+ return action === "dismiss" ? "dismissed" : "done";
415
+ }
416
+ /** The change that one person action makes, for messages and notes. */
417
+ function reviewItemChange(action, user) {
418
+ if (action === "take") return {
419
+ kind: "taken",
420
+ user
437
421
  };
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
422
+ if (action === "submit") return {
423
+ kind: "submitted",
424
+ user
477
425
  };
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
426
+ if (action === "complete") return {
427
+ kind: "completed",
428
+ user
488
429
  };
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
430
  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
- }
431
+ kind: "dismissed",
432
+ user
543
433
  };
544
434
  }
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
435
+ /** Return whether a closed item is the result of this same action. */
436
+ function isSameClose(item, action, user) {
437
+ return item.closedBy?.userId === user.id && item.status === closedStatus(action) && action === "submit" === Boolean(item.answer);
438
+ }
439
+ function submittedAnswer(item, answer) {
440
+ if (item.kind !== "action" || !item.request) throw new ReviewItemActionError("invalid", "Only an action item takes an answer.");
441
+ const normalized = normalizeUserInputAnswer(item.request, {
442
+ other: answer?.other,
443
+ requestId: item.id,
444
+ values: answer?.values ?? {}
571
445
  });
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));
446
+ if (isEmptyUserInputAnswer(normalized)) throw new ReviewItemActionError("invalid", "The answer is empty.");
447
+ return normalized;
448
+ }
449
+ /**
450
+ * Do one person action on a Review item and tell its linked Conversations.
451
+ * The first submit, complete, or dismiss closes the item for every
452
+ * assignee. A retry of the same close sends any missing messages again and
453
+ * succeeds, so a client can retry after a failure. With `firstCloseOnly`,
454
+ * only the call that closes the item succeeds, and every other call gets a
455
+ * conflict. Use it when the caller adds its own input for the action, such
456
+ * as a Slack card click, so one action adds one Turn. `exceptConversationId`
457
+ * is the Conversation where the person acted. It gets the person's input
458
+ * from its own surface, so it gets no message here. Slack cards of the item
459
+ * change in place; see `syncReviewItemSlackCards` for `getSlackAdapter`.
460
+ */
461
+ async function actOnReviewItem(args) {
462
+ const { action, user } = args;
463
+ const nowMs = args.nowMs ?? Date.now();
464
+ const db = getDb();
465
+ const item = await readReviewItemForUser(user, args.id);
466
+ if (action === "take") {
467
+ if (item.status !== "open") throw new ReviewItemActionError("conflict", "Review item is closed.");
468
+ if (!canActOnReviewItem(item, user.id)) throw new ReviewItemActionError("forbidden", "Only an assignee can take this item.");
469
+ if (!isOnlyAssignee(item, user.id) && !await takeReviewItem(db, {
470
+ id: item.id,
471
+ nowMs,
472
+ userId: user.id
473
+ })) throw new ReviewItemActionError("conflict", "Review item is closed.");
474
+ const taken = await readReviewItemForUser(user, item.id);
475
+ await syncReviewItemSlackCards({
476
+ getSlackAdapter: args.getSlackAdapter,
477
+ itemId: taken.id
478
+ });
479
+ await sendReviewItemMessages({
480
+ change: reviewItemChange(action, user),
481
+ exceptConversationId: args.exceptConversationId,
482
+ item: taken,
483
+ nowMs,
484
+ queue: args.queue
485
+ });
486
+ return taken;
487
+ }
488
+ let closed = item;
489
+ let closedNow = false;
490
+ if (item.status === "open") {
491
+ if (!canActOnReviewItem(item, user.id)) throw new ReviewItemActionError("forbidden", "Only an assignee can act on this item.");
492
+ if (action === "complete" && item.kind === "action") throw new ReviewItemActionError("invalid", "An action item needs an answer. Submit it or dismiss it.");
493
+ const answer = action === "submit" ? submittedAnswer(item, args.answer) : void 0;
494
+ closedNow = await closeReviewItem(db, {
495
+ closedByUserId: user.id,
496
+ id: item.id,
497
+ nowMs,
498
+ status: closedStatus(action),
499
+ ...answer ? { answer } : void 0
500
+ });
501
+ closed = await readReviewItemForUser(user, item.id);
502
+ }
503
+ if (args.firstCloseOnly ? !closedNow : !isSameClose(closed, action, user)) throw new ReviewItemActionError("conflict", "Review item is closed.");
504
+ await syncReviewItemSlackCards({
505
+ getSlackAdapter: args.getSlackAdapter,
506
+ itemId: closed.id
507
+ });
508
+ await sendReviewItemMessages({
509
+ change: reviewItemChange(action, user),
510
+ exceptConversationId: args.exceptConversationId,
511
+ item: closed,
512
+ nowMs,
513
+ queue: args.queue
514
+ });
515
+ return closed;
584
516
  }
585
517
  // src/chat/slack/errors.ts
586
518
  /** Extract Slack's stable API error code from Web API errors. */
@@ -850,6 +782,36 @@ async function startProcessingReactionForMessage(args) {
850
782
  };
851
783
  }
852
784
  // src/chat/providers/slack/turn-input-telemetry.ts
785
+ const SENTRY_TRACE_PATTERN = /^([0-9a-f]{32})-([0-9a-f]{16})(?:-([01]))?$/;
786
+ function spanLinks({ sentryTrace }) {
787
+ const [, traceId, spanId, sampled] = SENTRY_TRACE_PATTERN.exec(sentryTrace) ?? [];
788
+ return traceId && spanId ? [{ context: {
789
+ traceId,
790
+ spanId,
791
+ traceFlags: sampled === "1" ? 1 : 0
792
+ } }] : [];
793
+ }
794
+ /**
795
+ * Run a `chat.turn` span in the trace of the first trace context.
796
+ *
797
+ * The span links to the other trace contexts and to the span that was active,
798
+ * such as the worker span. Without a trace context, the span is a child of the
799
+ * active span. A malformed trace context starts a new trace.
800
+ */
801
+ async function withTurnSpan(context, input, callback) {
802
+ const [parent, ...others] = input?.traceContexts ?? [];
803
+ if (!parent) return await withSpan("chat.turn", "chat.turn", context, callback);
804
+ const activeSpan = getActiveSpan();
805
+ const links = [...others.flatMap(spanLinks), ...activeSpan ? [{ context: activeSpan.spanContext() }] : []];
806
+ const { baggage, sentryTrace } = parent;
807
+ return await continueTrace({
808
+ baggage,
809
+ sentryTrace
810
+ }, () => withSpan("chat.turn", "chat.turn", context, async (setSpanAttributes) => {
811
+ getActiveSpan()?.addLinks(links);
812
+ return await callback(setSpanAttributes);
813
+ }));
814
+ }
853
815
  /**
854
816
  * Build the `chat.turn` timing attributes for the oldest input message.
855
817
  *
@@ -1010,10 +972,8 @@ function createSlackTurnRuntime(deps) {
1010
972
  return true;
1011
973
  };
1012
974
  const failConversationTurn = async (args) => {
1013
- const conversationId = getThreadId(args.thread, args.message) ?? getRunId(args.thread, args.message);
1014
- if (!conversationId) return;
1015
975
  await turnLifecycle.fail({
1016
- conversationId,
976
+ conversationId: args.conversationId,
1017
977
  createdAtMs: deps.now(),
1018
978
  eventId: args.eventId,
1019
979
  failureCode: args.failureCode,
@@ -1058,7 +1018,15 @@ function createSlackTurnRuntime(deps) {
1058
1018
  };
1059
1019
  const postFallbackErrorReplyWithLogging = async (args) => {
1060
1020
  try {
1061
- await args.thread.post(buildTurnErrorResponse(args.error, args.eventId));
1021
+ const channelId = getChannelId(args.thread, args.message);
1022
+ if (!channelId) throw new Error("Slack fallback error reply requires a channel id");
1023
+ const threadTs = getThreadTs(getThreadId(args.thread, args.message));
1024
+ await sendSlackReply({
1025
+ channelId,
1026
+ conversationId: args.conversationId,
1027
+ text: buildTurnErrorResponse(args.error, args.eventId),
1028
+ ...threadTs ? { threadTs } : void 0
1029
+ });
1062
1030
  } catch (postError) {
1063
1031
  logException(postError, args.postFailureEventName, {
1064
1032
  "app.slack.reply_stage": "error_fallback_post",
@@ -1114,14 +1082,14 @@ function createSlackTurnRuntime(deps) {
1114
1082
  const completedAtMs = deps.now();
1115
1083
  logSkippedSubscribedDecision(args);
1116
1084
  if (args.preparedState) await deps.onSubscribedMessageSkipped({
1117
- thread: args.thread,
1085
+ conversationId: args.conversationId,
1118
1086
  message: args.message,
1119
1087
  decision: args.decision,
1120
1088
  preparedState: args.preparedState,
1121
1089
  completedAtMs
1122
1090
  });
1123
1091
  else await deps.recordSkippedSubscribedTurn({
1124
- thread: args.thread,
1092
+ conversationId: args.conversationId,
1125
1093
  message: args.message,
1126
1094
  decision: args.decision,
1127
1095
  text: args.text,
@@ -1169,7 +1137,7 @@ function createSlackTurnRuntime(deps) {
1169
1137
  });
1170
1138
  logSkippedSubscribedDecision(skipped);
1171
1139
  await deps.recordSkippedSteeringMessage({
1172
- thread: args.thread,
1140
+ conversationId: args.conversationId,
1173
1141
  message: skipped.message,
1174
1142
  decision: skipped.decision,
1175
1143
  text: skipped.text
@@ -1183,7 +1151,7 @@ function createSlackTurnRuntime(deps) {
1183
1151
  while (args.pending.length > 0) {
1184
1152
  const skipped = args.pending.shift();
1185
1153
  await deps.recordSkippedSteeringMessage({
1186
- thread: args.thread,
1154
+ conversationId: args.conversationId,
1187
1155
  message: skipped.message,
1188
1156
  decision: skipped.decision,
1189
1157
  text: skipped.text
@@ -1197,6 +1165,7 @@ function createSlackTurnRuntime(deps) {
1197
1165
  };
1198
1166
  return {
1199
1167
  async handleNewMention(thread, message, hooks) {
1168
+ const conversationId = hooks.conversationId ?? thread.id;
1200
1169
  const processingReactions = createProcessingReactionTracker(thread);
1201
1170
  let processingReaction;
1202
1171
  const skippedSteeringMessages = [];
@@ -1205,7 +1174,7 @@ function createSlackTurnRuntime(deps) {
1205
1174
  const onTurnCompleted = async () => {
1206
1175
  completed = true;
1207
1176
  await flushSkippedSteeringMessagesBestEffort({
1208
- thread,
1177
+ conversationId,
1209
1178
  pending: skippedSteeringMessages,
1210
1179
  context: logContext({
1211
1180
  threadId: getThreadId(thread, message),
@@ -1233,7 +1202,7 @@ function createSlackTurnRuntime(deps) {
1233
1202
  }
1234
1203
  processingReaction = await processingReactions.start(context, message);
1235
1204
  const toolInvocationHook = createToolInvocationHook(processingReaction, hooks);
1236
- await withSpan("chat.turn", "chat.turn", context, async (set) => {
1205
+ await withTurnSpan(context, hooks.telemetry, async (set) => {
1237
1206
  const turnStartedAtMs = deps.now();
1238
1207
  if (hooks.telemetry) set(getTurnInputSpanAttributes(hooks.telemetry, turnStartedAtMs));
1239
1208
  const queuedMessages = getQueuedMessages(hooks.messageContext, {
@@ -1259,6 +1228,7 @@ function createSlackTurnRuntime(deps) {
1259
1228
  },
1260
1229
  onSkipped: (skipped) => handleSkippedSteeringMessages({
1261
1230
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
1231
+ conversationId,
1262
1232
  pending: skippedSteeringMessages,
1263
1233
  skipped,
1264
1234
  thread
@@ -1272,7 +1242,7 @@ function createSlackTurnRuntime(deps) {
1272
1242
  await deps.executeSlackTurn(thread, message, {
1273
1243
  explicitMention: true,
1274
1244
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
1275
- conversationId: hooks.conversationId,
1245
+ conversationId,
1276
1246
  destination: hooks.destination,
1277
1247
  queuedMessages,
1278
1248
  ack,
@@ -1309,17 +1279,19 @@ function createSlackTurnRuntime(deps) {
1309
1279
  let lifecycleError;
1310
1280
  try {
1311
1281
  await failConversationTurn({
1282
+ conversationId,
1312
1283
  eventId,
1313
1284
  failureCode,
1314
- message,
1315
- thread
1285
+ message
1316
1286
  });
1317
1287
  } catch (error) {
1318
1288
  lifecycleError = error;
1319
1289
  }
1320
1290
  await hooks.beforeFirstResponsePost?.();
1321
1291
  await postFallbackErrorReplyWithLogging({
1292
+ conversationId,
1322
1293
  thread,
1294
+ message,
1323
1295
  eventId,
1324
1296
  error: failureCause,
1325
1297
  postFailureEventName: "mention.handler.failure_reply_post.failed"
@@ -1329,7 +1301,7 @@ function createSlackTurnRuntime(deps) {
1329
1301
  if (completed) await processingReactions.completeAll();
1330
1302
  else {
1331
1303
  await flushSkippedSteeringMessagesBestEffort({
1332
- thread,
1304
+ conversationId,
1333
1305
  pending: skippedSteeringMessages,
1334
1306
  context: logContext({
1335
1307
  threadId: getThreadId(thread, message),
@@ -1344,6 +1316,7 @@ function createSlackTurnRuntime(deps) {
1344
1316
  }
1345
1317
  },
1346
1318
  async handleSubscribedMessage(thread, message, hooks) {
1319
+ const conversationId = hooks.conversationId ?? thread.id;
1347
1320
  const processingReactions = createProcessingReactionTracker(thread);
1348
1321
  let processingReaction;
1349
1322
  const skippedSteeringMessages = [];
@@ -1352,7 +1325,7 @@ function createSlackTurnRuntime(deps) {
1352
1325
  const onTurnCompleted = async () => {
1353
1326
  completed = true;
1354
1327
  await flushSkippedSteeringMessagesBestEffort({
1355
- thread,
1328
+ conversationId,
1356
1329
  pending: skippedSteeringMessages,
1357
1330
  context: logContext({
1358
1331
  threadId: getThreadId(thread, message),
@@ -1375,7 +1348,7 @@ function createSlackTurnRuntime(deps) {
1375
1348
  channelId,
1376
1349
  runId
1377
1350
  });
1378
- await withSpan("chat.turn", "chat.turn", turnContext, async (set) => {
1351
+ await withTurnSpan(turnContext, hooks.telemetry, async (set) => {
1379
1352
  const turnStartedAtMs = deps.now();
1380
1353
  if (hooks.telemetry) set(getTurnInputSpanAttributes(hooks.telemetry, turnStartedAtMs));
1381
1354
  const content = parseContent(message);
@@ -1388,6 +1361,7 @@ function createSlackTurnRuntime(deps) {
1388
1361
  userText: replaceTopLevelText(content, strippedUserText)
1389
1362
  };
1390
1363
  const threadContext = {
1364
+ conversationId,
1391
1365
  threadId,
1392
1366
  actorId,
1393
1367
  channelId,
@@ -1420,7 +1394,7 @@ function createSlackTurnRuntime(deps) {
1420
1394
  beforeFirstResponsePost: hooks.beforeFirstResponsePost
1421
1395
  });
1422
1396
  await skipSubscribedMessage({
1423
- thread,
1397
+ conversationId,
1424
1398
  message,
1425
1399
  decision,
1426
1400
  context: threadContext,
@@ -1438,7 +1412,7 @@ function createSlackTurnRuntime(deps) {
1438
1412
  if (preflightDecision && !preflightDecision.shouldReply) {
1439
1413
  const reason = preflightDecision.reasonDetail ? `${preflightDecision.reason}:${preflightDecision.reasonDetail}` : preflightDecision.reason;
1440
1414
  await skipSubscribedMessage({
1441
- thread,
1415
+ conversationId,
1442
1416
  message,
1443
1417
  decision: {
1444
1418
  shouldReply: false,
@@ -1464,7 +1438,7 @@ function createSlackTurnRuntime(deps) {
1464
1438
  beforeFirstResponsePost: hooks.beforeFirstResponsePost
1465
1439
  });
1466
1440
  await skipSubscribedMessage({
1467
- thread,
1441
+ conversationId,
1468
1442
  message,
1469
1443
  decision,
1470
1444
  context: threadContext,
@@ -1483,7 +1457,7 @@ function createSlackTurnRuntime(deps) {
1483
1457
  queuedMessages
1484
1458
  });
1485
1459
  await deps.persistPreparedState({
1486
- thread,
1460
+ conversationId,
1487
1461
  preparedState
1488
1462
  });
1489
1463
  const decision = await deps.decideSubscribedReply({
@@ -1500,7 +1474,7 @@ function createSlackTurnRuntime(deps) {
1500
1474
  beforeFirstResponsePost: hooks.beforeFirstResponsePost
1501
1475
  })) {
1502
1476
  await skipSubscribedMessage({
1503
- thread,
1477
+ conversationId,
1504
1478
  message,
1505
1479
  decision,
1506
1480
  context: threadContext,
@@ -1512,7 +1486,7 @@ function createSlackTurnRuntime(deps) {
1512
1486
  }
1513
1487
  if (!decision.shouldReply) {
1514
1488
  await skipSubscribedMessage({
1515
- thread,
1489
+ conversationId,
1516
1490
  message,
1517
1491
  decision,
1518
1492
  context: threadContext,
@@ -1531,6 +1505,7 @@ function createSlackTurnRuntime(deps) {
1531
1505
  },
1532
1506
  onSkipped: (skipped) => handleSkippedSteeringMessages({
1533
1507
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
1508
+ conversationId,
1534
1509
  pending: skippedSteeringMessages,
1535
1510
  skipped,
1536
1511
  thread
@@ -1556,7 +1531,7 @@ function createSlackTurnRuntime(deps) {
1556
1531
  const toolInvocationHook = createToolInvocationHook(processingReaction, hooks);
1557
1532
  await deps.executeSlackTurn(thread, message, {
1558
1533
  explicitMention: Boolean(message.isMention),
1559
- conversationId: hooks.conversationId,
1534
+ conversationId,
1560
1535
  destination: hooks.destination,
1561
1536
  preparedState,
1562
1537
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
@@ -1595,17 +1570,19 @@ function createSlackTurnRuntime(deps) {
1595
1570
  let lifecycleError;
1596
1571
  try {
1597
1572
  await failConversationTurn({
1573
+ conversationId,
1598
1574
  eventId,
1599
1575
  failureCode,
1600
- message,
1601
- thread
1576
+ message
1602
1577
  });
1603
1578
  } catch (error) {
1604
1579
  lifecycleError = error;
1605
1580
  }
1606
1581
  await hooks.beforeFirstResponsePost?.();
1607
1582
  await postFallbackErrorReplyWithLogging({
1583
+ conversationId,
1608
1584
  thread,
1585
+ message,
1609
1586
  eventId,
1610
1587
  error: failureCause,
1611
1588
  postFailureEventName: "subscribed_message.handler.failure_reply_post.failed"
@@ -1615,7 +1592,7 @@ function createSlackTurnRuntime(deps) {
1615
1592
  if (completed) await processingReactions.completeAll();
1616
1593
  else {
1617
1594
  await flushSkippedSteeringMessagesBestEffort({
1618
- thread,
1595
+ conversationId,
1619
1596
  pending: skippedSteeringMessages,
1620
1597
  context: logContext({
1621
1598
  threadId: getThreadId(thread, message),
@@ -2166,7 +2143,8 @@ function createSlackConversationWorker(options) {
2166
2143
  const telemetry = {
2167
2144
  count: records.length,
2168
2145
  oldestCreatedAtMs: oldestRecord.createdAtMs,
2169
- oldestReceivedAtMs: oldestRecord.receivedAtMs
2146
+ oldestReceivedAtMs: oldestRecord.receivedAtMs,
2147
+ traceContexts: records.flatMap((record) => record.traceContext ?? [])
2170
2148
  };
2171
2149
  const latestMetadata = parseSlackMetadata(latestRecord.input.metadata);
2172
2150
  if (!latestMetadata) throw new Error("Latest conversation mailbox record is not Slack metadata");
@@ -2213,7 +2191,7 @@ function createSlackConversationWorker(options) {
2213
2191
  };
2214
2192
  const drainSteeringMessages = async (accept) => {
2215
2193
  await context.attempt.drain(async (pendingRecords) => {
2216
- const candidates = pendingRecords.map((record) => ({
2194
+ const candidates = pendingRecords.filter((record) => record.source === "slack").map((record) => ({
2217
2195
  inboundMessageId: record.inboundMessageId,
2218
2196
  message: restoreMessage({
2219
2197
  adapter,
@@ -2263,7 +2241,7 @@ function createSlackConversationWorker(options) {
2263
2241
  return { status: "completed" };
2264
2242
  };
2265
2243
  }
2266
- /** Serialize a Slack message into the generic durable conversation mailbox. */
2244
+ /** Serialize a Slack message and the active trace into the durable mailbox. */
2267
2245
  function buildSlackInboundMessage(args) {
2268
2246
  const authorId = requireSlackAuthorId(args.message);
2269
2247
  const destination = createSlackDestination({
@@ -2296,7 +2274,8 @@ function buildSlackInboundMessage(args) {
2296
2274
  thread: serializedThreadSchema.parse(args.thread.toJSON()),
2297
2275
  message: args.message.toJSON()
2298
2276
  }
2299
- }
2277
+ },
2278
+ traceContext: getActiveTraceContext()
2300
2279
  };
2301
2280
  }
2302
2281
  // src/chat/slack/message/identity.ts
@@ -2784,6 +2763,57 @@ function recordSkippedConversationMessage(args) {
2784
2763
  }
2785
2764
  });
2786
2765
  }
2766
+ // src/chat/sign-in/allowlist.ts
2767
+ /**
2768
+ * Domains that may use Junior. Slack authors must have a Slack profile email
2769
+ * on one of them, and dashboard Google sign-ins must come from a Google
2770
+ * Workspace account whose `hd` claim is one of them. Slack and the dashboard
2771
+ * share one list.
2772
+ */
2773
+ /** Read a list env var: a JSON string array or comma-separated values. */
2774
+ function readEnvList(name) {
2775
+ const value = process.env[name]?.trim();
2776
+ if (!value) return void 0;
2777
+ if (!value.startsWith("[")) return value.split(",");
2778
+ let parsed;
2779
+ try {
2780
+ parsed = JSON.parse(value);
2781
+ } catch (error) {
2782
+ throw new Error(`${name} must be a JSON string array`, { cause: error });
2783
+ }
2784
+ const list = z.array(z.string()).safeParse(parsed);
2785
+ if (!list.success) throw new Error(`${name} must be a JSON string array`);
2786
+ return list.data;
2787
+ }
2788
+ /**
2789
+ * Resolve the allowed domains: the `createApp` option when set, else
2790
+ * `JUNIOR_ALLOWED_DOMAINS`. An empty list allows every domain.
2791
+ */
2792
+ function resolveAllowedDomains(allowedDomains) {
2793
+ return [...new Set((allowedDomains ?? readEnvList("JUNIOR_ALLOWED_DOMAINS") ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean))];
2794
+ }
2795
+ /**
2796
+ * Fail startup on the removed Google-specific names. Ignoring them would
2797
+ * leave the list empty, which lets every Slack workspace member in.
2798
+ */
2799
+ function assertNoLegacyAllowedDomains(dashboardOptionKeys) {
2800
+ if (dashboardOptionKeys.includes("allowedGoogleDomains")) throw new Error("dashboard.allowedGoogleDomains was removed; set createApp({ allowedDomains }) instead");
2801
+ if (process.env.JUNIOR_DASHBOARD_GOOGLE_DOMAINS?.trim()) throw new Error("JUNIOR_DASHBOARD_GOOGLE_DOMAINS was removed; set JUNIOR_ALLOWED_DOMAINS instead");
2802
+ }
2803
+ /** Return whether a verified domain may use Junior. An empty list allows all. */
2804
+ function isAllowedDomain(domain, allowedDomains) {
2805
+ if (allowedDomains.length === 0) return true;
2806
+ return Boolean(domain && allowedDomains.includes(domain.toLowerCase()));
2807
+ }
2808
+ /**
2809
+ * Return whether a verified sign-in email is on an allowed domain. Sign-in
2810
+ * needs a configured list, so an empty list allows no email.
2811
+ */
2812
+ function isAllowedSignInEmail(email, allowedDomains) {
2813
+ const normalized = email.trim().toLowerCase();
2814
+ const separator = normalized.lastIndexOf("@");
2815
+ return allowedDomains.length > 0 && separator > 0 && isAllowedDomain(normalized.slice(separator + 1), allowedDomains);
2816
+ }
2787
2817
  // src/chat/automations/collection.ts
2788
2818
  /**
2789
2819
  * The web collection applies access before counts, filter options, and paging.
@@ -2799,18 +2829,6 @@ function viewerAutomationCollection(user) {
2799
2829
  const owned = inArray(automation.creatorIdentityId, user.identities.map((identity) => identity.id));
2800
2830
  const publicDestination = sql`coalesce(${destination.visibility} = 'public', false)`;
2801
2831
  const publicToViewer = and(inArray(automation.teamId, teamIds), publicDestination, ne(automation.status, "completed"));
2802
- const trigger = sql`${automation.record}->'trigger'`;
2803
- const match = sql`coalesce(${trigger}->'match', '{}'::jsonb)`;
2804
- const json = (value) => sql`${JSON.stringify(value)}::jsonb`;
2805
- const fieldWorks = (key, field) => {
2806
- const value = sql`${match}->(${key}::text)`;
2807
- const type = sql`jsonb_typeof(${value})`;
2808
- return sql`(${value} is null or (${and(field.events ? sql`${trigger}->'events' <@ ${json(field.events)}` : void 0, field.kind === "boolean" ? sql`${type} = 'boolean'` : sql`(${type} = ${field.kind} or (${type} = 'array' and not exists (select 1 from jsonb_array_elements(${value}) entry where jsonb_typeof(entry) <> ${field.kind})))`, field.enum ? sql`${value} <@ ${json(field.enum)}` : void 0)}))`;
2809
- };
2810
- const available = or(isScheduled, ...Object.entries(automationEventCatalog(getEventCatalog())).flatMap(([namespace, registration]) => registration.resourceTypes.map((resourceType) => {
2811
- const fields = Object.entries(resourceType.matchFields ?? {});
2812
- return and(eq(automation.namespace, namespace), sql`${trigger}->>'resourceType' = ${resourceType.type}`, sql`${trigger}->'events' <@ ${json(resourceType.supportedEvents)}`, sql`not exists (select 1 from jsonb_object_keys(${match}) match_key where not (${json(fields.map(([key]) => key))} @> to_jsonb(match_key)))`, ...fields.map(([key, field]) => fieldWorks(key, field)));
2813
- }))) ?? sql`false`;
2814
2832
  const runs = juniorAutomationRuns;
2815
2833
  const lastRunNeedsAttention = sql`coalesce((select ${runs.status} in ('failed', 'blocked') from ${runs}
2816
2834
  where ${runs.automationId} = ${automation.id} and ${runs.status} in ('blocked', 'completed', 'failed') and ${runs.completedAtMs} is not null
@@ -2821,8 +2839,8 @@ function viewerAutomationCollection(user) {
2821
2839
  title: automation.title,
2822
2840
  createdAtMs: automation.createdAtMs,
2823
2841
  state: sql`${automation.status}`.as("state"),
2824
- unavailable: sql`not (${available})`.as("unavailable"),
2825
- attention: sql`${automation.status} not in ('paused', 'completed') and (${automation.status} = 'blocked' or not (${available}) or ${lastRunNeedsAttention})`.as("attention"),
2842
+ lastRunNeedsAttention: lastRunNeedsAttention.as("last_run_needs_attention"),
2843
+ trigger: sql`${automation.record}->'trigger'`.as("trigger"),
2826
2844
  owned: sql`${owned}`.as("owned"),
2827
2845
  resource: sql`case when ${isScheduled} then '' else concat_ws(' ', ${automation.identifier}, ${automation.record}->'trigger'->>'label', ${automation.namespace}) end`.as("resource"),
2828
2846
  creator: sql`${automation.teamId} || ':' || ${automation.creatorSlackUserId}`.as("creator"),
@@ -2833,11 +2851,29 @@ function viewerAutomationCollection(user) {
2833
2851
  instruction: automation.instruction
2834
2852
  }).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");
2835
2853
  }
2854
+ /**
2855
+ * List the accessible event automations whose trigger cannot work with the
2856
+ * enabled plugins. `eventTriggerProblems` decides, the same check that the
2857
+ * tools and the dashboard use, so SQL does not keep its own copy of the rules.
2858
+ */
2859
+ async function unavailableEventAutomationIds(collection) {
2860
+ const catalog = automationEventCatalog(getEventCatalog());
2861
+ return (await getDb().select({
2862
+ id: collection.id,
2863
+ trigger: collection.trigger
2864
+ }).from(collection).where(eq(collection.kind, "event"))).flatMap((row) => {
2865
+ const trigger = eventAutomationTriggerSchema.safeParse(row.trigger);
2866
+ return trigger.success && registeredEventTriggerProblems(catalog, trigger.data).length === 0 ? [] : [row.id];
2867
+ });
2868
+ }
2836
2869
  /** Read an SQL page and complete, access-scoped counts and filter options. */
2837
2870
  async function readAutomationCollection(user, input) {
2838
2871
  const db = getDb();
2839
2872
  const collection = viewerAutomationCollection(user);
2840
- const where = and(input.scope === "mine" ? eq(collection.owned, true) : input.scope === "public" ? eq(collection.isPublic, true) : input.scope === "attention" ? eq(collection.attention, true) : void 0, input.type === "all" ? void 0 : eq(collection.kind, input.type), input.state === "all" ? void 0 : input.state === "unavailable" ? eq(collection.unavailable, true) : 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);
2873
+ const unavailableIds = await unavailableEventAutomationIds(collection);
2874
+ const unavailable = unavailableIds.length > 0 ? inArray(collection.id, unavailableIds) : sql`false`;
2875
+ const attention = and(notInArray(collection.state, ["paused", "completed"]), or(eq(collection.state, "blocked"), unavailable, eq(collection.lastRunNeedsAttention, true)));
2876
+ 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);
2841
2877
  const [totals, counts, creators, destinations] = await Promise.all([
2842
2878
  db.select({ total: count() }).from(collection).where(where),
2843
2879
  db.select({
@@ -2966,6 +3002,7 @@ function executionSummaryFields(stats) {
2966
3002
  lastRunStatus: stats?.lastRunStatus,
2967
3003
  ...stats?.lastConversationId ? { lastConversationId: stats.lastConversationId } : void 0,
2968
3004
  ...stats?.lastExecutedAtMs ? { lastRunAt: new Date(stats.lastExecutedAtMs).toISOString() } : void 0,
3005
+ costUsd: stats?.costUsd ?? emptyAutomationRunWindows(),
2969
3006
  runs: stats?.runs ?? emptyAutomationRunWindows(),
2970
3007
  totalRuns: stats?.totalRuns ?? 0
2971
3008
  };
@@ -3019,7 +3056,7 @@ function eventAutomationSummary(task, ownedByViewer, destination, stats, creator
3019
3056
  kind: "event",
3020
3057
  ...executionSummaryFields(stats),
3021
3058
  ownedByViewer,
3022
- resource: `${task.trigger.label} · ${task.trigger.identifier}`,
3059
+ resource: task.trigger.identifier ? `${task.trigger.label} · ${task.trigger.identifier}` : task.trigger.label,
3023
3060
  source: task.trigger.namespace,
3024
3061
  credentialMode: task.credentialMode,
3025
3062
  outcomes: effectiveTaskOutcomes(task.outcomes, task.destination),
@@ -3502,292 +3539,6 @@ function readSlackActionToken(message) {
3502
3539
  const token = slackActionTokenSchema.safeParse(envelope.data.raw?.action_token);
3503
3540
  return token.success ? token.data : void 0;
3504
3541
  }
3505
- // src/chat/slack/cards.ts
3506
- /** Select the Slack object renderer for a built-in card. */
3507
- function renderSlackCard(card, conversationId) {
3508
- switch (card.kind) {
3509
- case "object": return renderSlackObjectCard(card, conversationId);
3510
- case "automation": return renderSlackAutomationCard(card);
3511
- }
3512
- }
3513
- // src/chat/slack/footer.ts
3514
- /** Render compact reply attribution for the Slack footer. */
3515
- function formatReplyAttribution(attribution) {
3516
- return attribution.detail ? `${attribution.label} · ${attribution.detail}` : attribution.label;
3517
- }
3518
- /**
3519
- * Build the compact conversation footer for visible Slack reply surfaces.
3520
- *
3521
- * Detailed turn metrics stay in the dashboard instead of Slack-visible copy.
3522
- */
3523
- function buildSlackReplyFooter(args) {
3524
- const items = [];
3525
- const conversationId = args.conversationId?.trim();
3526
- if (conversationId) {
3527
- const idItem = {
3528
- label: "ID",
3529
- value: conversationId
3530
- };
3531
- const conversationUrl = getPluginSlackConversationLink(conversationId)?.url ?? getDashboardConversationLink(conversationId) ?? buildSentryConversationUrl(conversationId);
3532
- if (conversationUrl) idItem.url = conversationUrl;
3533
- items.push(idItem);
3534
- }
3535
- return items.length > 0 || args.replyAttribution ? {
3536
- ...args.replyAttribution ? { attribution: args.replyAttribution } : void 0,
3537
- items
3538
- } : void 0;
3539
- }
3540
- /** Build Slack blocks for a reply chunk using the Slack-flavored markdown block for the body. */
3541
- function buildSlackReplyBlocks(text, footer) {
3542
- const blocks = text.trim() ? [{
3543
- type: "markdown",
3544
- text
3545
- }] : [];
3546
- if (footer && (footer.attribution || footer.items.length > 0)) {
3547
- const attributionElements = footer.attribution ? [{
3548
- type: "plain_text",
3549
- text: formatReplyAttribution(footer.attribution)
3550
- }] : [];
3551
- blocks.push({
3552
- type: "context",
3553
- elements: [...attributionElements, ...footer.items.map((item) => ({
3554
- type: "mrkdwn",
3555
- text: item.url ? `*${escapeSlackMrkdwnText(item.label)}:* ${formatSlackLink(item.url, item.value)}` : `*${escapeSlackMrkdwnText(item.label)}:* ${escapeSlackMrkdwnText(item.value)}`
3556
- }))]
3557
- });
3558
- }
3559
- return blocks.length ? blocks : void 0;
3560
- }
3561
- // src/chat/slack/output.ts
3562
- const MAX_INLINE_CHARS = 2200;
3563
- const MAX_INLINE_LINES = 45;
3564
- const CONTINUED_MARKER = "\n\n[Continued below]";
3565
- function countSlackLines(text) {
3566
- if (!text) return 0;
3567
- return text.split("\n").length;
3568
- }
3569
- function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
3570
- return text.length <= maxChars && countSlackLines(text) <= maxLines;
3571
- }
3572
- function findSplitIndex(text, maxChars) {
3573
- if (text.length <= maxChars) return text.length;
3574
- const bounded = text.slice(0, maxChars);
3575
- const newlineIndex = bounded.lastIndexOf("\n");
3576
- if (newlineIndex > 0) return newlineIndex;
3577
- const spaceIndex = bounded.lastIndexOf(" ");
3578
- if (spaceIndex > 0) return spaceIndex;
3579
- return maxChars;
3580
- }
3581
- function splitByLineBudget(text, maxLines) {
3582
- if (maxLines <= 0) return "";
3583
- const lines = text.split("\n");
3584
- if (lines.length <= maxLines) return text;
3585
- return lines.slice(0, maxLines).join("\n");
3586
- }
3587
- function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
3588
- return {
3589
- maxChars: Math.max(1, maxChars - suffix.length),
3590
- maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
3591
- };
3592
- }
3593
- function forceSplitBudget(text, budget) {
3594
- const lineCount = countSlackLines(text);
3595
- return {
3596
- maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
3597
- maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
3598
- };
3599
- }
3600
- function getFenceContinuation(text) {
3601
- let open;
3602
- for (const line of text.split("\n")) {
3603
- const trimmed = line.trimStart();
3604
- const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
3605
- if (!openerMatch?.[1]) continue;
3606
- if (!open) {
3607
- open = {
3608
- fence: openerMatch[1],
3609
- openerLine: trimmed
3610
- };
3611
- continue;
3612
- }
3613
- if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) open = void 0;
3614
- }
3615
- if (!open) return null;
3616
- return {
3617
- closeSuffix: text.endsWith("\n") ? open.fence : `\n${open.fence}`,
3618
- reopenPrefix: `${open.openerLine}\n`
3619
- };
3620
- }
3621
- function appendSlackSuffix(text, marker) {
3622
- return `${text}${getFenceContinuation(text)?.closeSuffix ?? ""}${marker}`;
3623
- }
3624
- function stripTrailingContinuationMarker(text) {
3625
- return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -19) : text;
3626
- }
3627
- function takeSlackContinuationChunk(text, budget) {
3628
- let { prefix, rest } = takeSlackInlinePrefix(text, budget);
3629
- if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, budget)));
3630
- let carryover = rest ? getFenceContinuation(prefix) : null;
3631
- if (!carryover) return {
3632
- prefix,
3633
- rest
3634
- };
3635
- const carryoverBudget = reserveInlineBudgetForSuffix(`${carryover.closeSuffix}${CONTINUED_MARKER}`);
3636
- ({prefix, rest} = takeSlackInlinePrefix(text, carryoverBudget));
3637
- if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, carryoverBudget)));
3638
- carryover = rest ? getFenceContinuation(prefix) : null;
3639
- if (!carryover) return {
3640
- prefix,
3641
- rest
3642
- };
3643
- return {
3644
- prefix,
3645
- rest: `${carryover.reopenPrefix}${rest}`
3646
- };
3647
- }
3648
- /** Return the first continuation-safe chunk plus any remaining text. */
3649
- function takeSlackContinuationPrefix(text, options) {
3650
- const budget = {
3651
- maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
3652
- maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
3653
- };
3654
- const { prefix, rest } = (() => {
3655
- if (options?.forceSplit) return takeSlackContinuationChunk(text, budget);
3656
- const initial = takeSlackInlinePrefix(text, budget);
3657
- return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
3658
- })();
3659
- return {
3660
- prefix,
3661
- renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
3662
- rest
3663
- };
3664
- }
3665
- /**
3666
- * Take the largest Slack-safe inline prefix from `text` under the configured
3667
- * character and line budgets. Returns the consumed prefix plus remaining text.
3668
- */
3669
- function takeSlackInlinePrefix(text, options) {
3670
- const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
3671
- const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
3672
- const normalized = text.replace(/\r\n?/g, "\n");
3673
- if (!normalized) return {
3674
- prefix: "",
3675
- rest: ""
3676
- };
3677
- if (fitsInlineBudget(normalized, maxChars, maxLines)) return {
3678
- prefix: normalized,
3679
- rest: ""
3680
- };
3681
- const lineBounded = splitByLineBudget(normalized, maxLines);
3682
- const cutIndex = findSplitIndex(lineBounded, maxChars);
3683
- const prefix = lineBounded.slice(0, cutIndex).trimEnd();
3684
- if (prefix) return {
3685
- prefix,
3686
- rest: normalized.slice(prefix.length).trimStart()
3687
- };
3688
- const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
3689
- return {
3690
- prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
3691
- rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
3692
- };
3693
- }
3694
- /**
3695
- * Split a normalized Slack reply into multiple inline-safe thread messages.
3696
- *
3697
- * Replies split across more than two messages receive explicit continuation
3698
- * markers on non-final chunks. When
3699
- * `interrupted` is true, the final chunk receives an interruption marker.
3700
- */
3701
- function splitSlackReplyText(text, options) {
3702
- const normalized = normalizeSlackReplyMarkdown(text);
3703
- if (!normalized) return [];
3704
- const chunks = [];
3705
- const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
3706
- let remaining = normalized;
3707
- while (remaining) {
3708
- if (options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining)) {
3709
- chunks.push(options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining);
3710
- break;
3711
- }
3712
- const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
3713
- ...continuationBudget,
3714
- forceSplit: true
3715
- });
3716
- chunks.push(renderedPrefix);
3717
- remaining = rest;
3718
- }
3719
- if (chunks.length === 2) chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
3720
- return chunks;
3721
- }
3722
- /**
3723
- * Reserve enough inline budget for a continuation suffix on the current chunk.
3724
- */
3725
- function getSlackContinuationBudget() {
3726
- return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
3727
- }
3728
- /** Normalize text for Slack and wrap it as a PostableMessage. */
3729
- function buildSlackOutputMessage(text) {
3730
- const normalized = normalizeSlackReplyMarkdown(text);
3731
- if (!normalized) throw new Error(`Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`);
3732
- return { markdown: normalized };
3733
- }
3734
- // src/chat/slack/reply.ts
3735
- /**
3736
- * Send one destination-visible Slack reply.
3737
- *
3738
- * Chunks oversized text, builds compact attribution and conversation footer
3739
- * context, and posts through the shared Slack outbound boundary.
3740
- */
3741
- async function sendSlackReply(args) {
3742
- const posts = splitSlackReplyText(args.text).map((text) => ({
3743
- text,
3744
- cards: []
3745
- }));
3746
- const cards = (args.cards ?? []).map((card) => renderSlackCard(card, args.conversationId));
3747
- for (let index = 0; index < cards.length; index += 5) {
3748
- const group = cards.slice(index, index + 5);
3749
- const lastPost = posts.at(-1);
3750
- if (index === 0 && lastPost) lastPost.cards = group;
3751
- else posts.push({
3752
- text: "",
3753
- cards: group
3754
- });
3755
- }
3756
- const footer = buildSlackReplyFooter({
3757
- conversationId: args.conversationId,
3758
- replyAttribution: args.replyAttribution
3759
- });
3760
- const messageTs = [];
3761
- let threadTs = args.threadTs;
3762
- for (const [index, post] of posts.entries()) {
3763
- const { text, cards } = post;
3764
- const isFinalChunk = index === posts.length - 1;
3765
- const blocks = buildSlackReplyBlocks(text, void 0) ?? [];
3766
- for (const card of cards) if (!card.entity) blocks.push({
3767
- type: "section",
3768
- text: {
3769
- type: "mrkdwn",
3770
- text: card.text
3771
- }
3772
- });
3773
- if (isFinalChunk || !text.trim()) blocks.push(...buildSlackReplyBlocks("", footer) ?? []);
3774
- const entities = cards.flatMap((card) => card.entity ? [card.entity] : []);
3775
- const accessibleText = [text, ...cards.map((card) => card.text)].filter(Boolean).join("\n\n");
3776
- const fallbackText = isFinalChunk && args.replyAttribution ? `${accessibleText}\n\n${escapeSlackMrkdwnText(formatReplyAttribution(args.replyAttribution))}` : accessibleText;
3777
- const response = await postSlackMessage({
3778
- channelId: args.channelId,
3779
- threadTs,
3780
- text: fallbackText,
3781
- ...blocks.length ? { blocks } : void 0,
3782
- ...entities.length ? { entities } : void 0
3783
- });
3784
- if (response.ts) {
3785
- messageTs.push(response.ts);
3786
- threadTs ??= response.ts;
3787
- }
3788
- }
3789
- return messageTs;
3790
- }
3791
3542
  // src/chat/slack/assistant-thread/status-render.ts
3792
3543
  const DEFAULT_STATUS_CONTEXTS = {
3793
3544
  thinking: "…",
@@ -4289,8 +4040,8 @@ async function saveSteeringMessages(args) {
4289
4040
  * Persist post-delivery Redis scratch with a short retry after durable SQL
4290
4041
  * completion has succeeded.
4291
4042
  */
4292
- async function persistThreadRuntimeStateWithRetry(thread, patch) {
4293
- await persistWithRetry(() => persistThreadRuntimeState(thread, patch));
4043
+ async function persistThreadRuntimeStateWithRetry(conversationId, patch) {
4044
+ await persistWithRetry(() => persistThreadRuntimeState(conversationId, patch));
4294
4045
  }
4295
4046
  /**
4296
4047
  * Resolve the Pi history for this Slack turn from the most precise durable
@@ -4327,10 +4078,7 @@ function createSlackTurn(deps) {
4327
4078
  channelName,
4328
4079
  channelType: slackChannelType
4329
4080
  });
4330
- const destinationVisibility = await resolveDestinationVisibility({
4331
- destination,
4332
- visibility: options.execution?.destinationVisibility ?? conversationVisibilityFromSlackChannelType(slackChannelType)
4333
- });
4081
+ const destinationVisibility = conversationVisibilityFromSlackChannelType(slackChannelType);
4334
4082
  const assistantThreadContext = getAssistantThreadContext(message);
4335
4083
  const threadTs = getThreadTs(threadId) ?? assistantThreadContext?.threadTs;
4336
4084
  const messageTs = getMessageTs(message);
@@ -4340,7 +4088,14 @@ function createSlackTurn(deps) {
4340
4088
  messageTs,
4341
4089
  teamId,
4342
4090
  threadTs,
4343
- visibility: destinationVisibility ?? "private"
4091
+ visibility: await resolveDestinationVisibility({
4092
+ destination,
4093
+ visibility: destinationVisibility
4094
+ }) ?? "private"
4095
+ });
4096
+ const replyTarget = resolveSlackReplyTarget({
4097
+ destination,
4098
+ source
4344
4099
  });
4345
4100
  const slackActionToken = readSlackActionToken(message);
4346
4101
  const runId = options.execution?.dispatch?.id ?? getRunId(thread, message);
@@ -4396,6 +4151,7 @@ function createSlackTurn(deps) {
4396
4151
  destination,
4397
4152
  locationConfiguration: options.execution?.locationConfiguration,
4398
4153
  context: {
4154
+ conversationId,
4399
4155
  threadId,
4400
4156
  actorId: slackActorId,
4401
4157
  channelId,
@@ -4413,17 +4169,16 @@ function createSlackTurn(deps) {
4413
4169
  };
4414
4170
  const postAuthPauseNotice = async (providerDisplayName, requestText) => {
4415
4171
  if (!actor) throw new Error("Slack auth pause notice requires an actor");
4172
+ if (!postsRunNotices(options.execution?.dispatch)) return;
4416
4173
  const text = buildAuthPauseResponse(actor.userId, providerDisplayName, requestText);
4417
4174
  try {
4418
4175
  await beforeFirstResponsePost();
4419
- if (channelId && threadTs) await sendSlackReply({
4420
- channelId,
4176
+ await sendSlackReply({
4177
+ ...replyTarget,
4421
4178
  conversationId,
4422
4179
  replyAttribution: options.execution?.dispatch?.replyAttribution,
4423
- text,
4424
- threadTs
4180
+ text
4425
4181
  });
4426
- else await thread.post(buildSlackOutputMessage(text));
4427
4182
  } catch (error) {
4428
4183
  logException(error, "slack.auth_pause.notice_post.failed", {
4429
4184
  "app.slack.reply_stage": "thread_reply_auth_pause_notice",
@@ -4477,7 +4232,7 @@ function createSlackTurn(deps) {
4477
4232
  const deliveredMessage = [...preparedState.conversation.messages].reverse().find((candidate) => candidate.role === "assistant" && candidate.id.startsWith(`${turnId}:assistant:`) && candidate.meta?.replied === true);
4478
4233
  options.onTurnDeliveryAccepted?.(deliveredMessage?.meta?.slackTs);
4479
4234
  options.onTurnOutcome?.({ outcome: "completed" });
4480
- await persistThreadState(thread, { conversation: preparedState.conversation });
4235
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
4481
4236
  await options.onTurnStatePersisted?.();
4482
4237
  await options.ack?.();
4483
4238
  await options.onTurnCompleted?.();
@@ -4500,7 +4255,7 @@ function createSlackTurn(deps) {
4500
4255
  });
4501
4256
  throw error;
4502
4257
  }
4503
- await persistThreadState(thread, { conversation: preparedState.conversation });
4258
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
4504
4259
  await options.onTurnStatePersisted?.();
4505
4260
  await options.ack?.();
4506
4261
  return;
@@ -4538,7 +4293,11 @@ function createSlackTurn(deps) {
4538
4293
  });
4539
4294
  if (configReply) {
4540
4295
  await beforeFirstResponsePost();
4541
- await thread.post(buildSlackOutputMessage(configReply.text));
4296
+ await sendSlackReply({
4297
+ ...replyTarget,
4298
+ conversationId,
4299
+ text: configReply.text
4300
+ });
4542
4301
  markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
4543
4302
  replied: true,
4544
4303
  skippedReason: void 0
@@ -4557,7 +4316,7 @@ function createSlackTurn(deps) {
4557
4316
  source: "slack"
4558
4317
  }
4559
4318
  });
4560
- await persistThreadState(thread, { conversation: preparedState.conversation });
4319
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
4561
4320
  await options.onTurnStatePersisted?.();
4562
4321
  await options.ack?.();
4563
4322
  return;
@@ -4598,7 +4357,7 @@ function createSlackTurn(deps) {
4598
4357
  "app.message.id": message.id,
4599
4358
  ...messageTs ? { "messaging.message.id": messageTs } : void 0
4600
4359
  });
4601
- await persistThreadState(thread, { conversation: preparedState.conversation });
4360
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
4602
4361
  await options.onTurnStatePersisted?.();
4603
4362
  if (actor) setSentryUser({
4604
4363
  id: actor.userId,
@@ -4685,16 +4444,15 @@ function createSlackTurn(deps) {
4685
4444
  }
4686
4445
  else {
4687
4446
  slackMessageTs = await sendSlackReply({
4447
+ ...replyTarget,
4688
4448
  cards,
4689
- channelId: destination.channelId,
4690
4449
  conversationId,
4691
4450
  replyAttribution: options.execution?.dispatch?.replyAttribution,
4692
- text,
4693
- ...threadTs ? { threadTs } : void 0
4451
+ text
4694
4452
  });
4695
4453
  messageDestinations.push(...slackMessageTs.map((messageId) => ({
4696
4454
  destination,
4697
- messageId: threadTs ?? messageId
4455
+ messageId: replyTarget.threadTs ?? messageId
4698
4456
  })));
4699
4457
  }
4700
4458
  } catch (error) {
@@ -4784,7 +4542,7 @@ function createSlackTurn(deps) {
4784
4542
  });
4785
4543
  if (compaction.compacted) {
4786
4544
  piMessages = compaction.piMessages;
4787
- await persistThreadState(thread, { conversation: preparedState.conversation });
4545
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
4788
4546
  }
4789
4547
  }
4790
4548
  const hasDurablePromptHistory = Boolean(piMessages?.length);
@@ -4893,11 +4651,11 @@ function createSlackTurn(deps) {
4893
4651
  drainSteeringMessages,
4894
4652
  shouldYield: options.shouldYield,
4895
4653
  onSandboxRefChanged: async (sandboxRef) => {
4896
- await persistThreadState(thread, { sandboxRef });
4654
+ await persistThreadStateById(conversationId, { sandboxRef });
4897
4655
  },
4898
4656
  recordPendingAuth: async (pendingAuth) => {
4899
4657
  preparedState.conversation.processing.pendingAuth = pendingAuth;
4900
- await persistThreadState(thread, { conversation: preparedState.conversation });
4658
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
4901
4659
  }
4902
4660
  }
4903
4661
  };
@@ -4976,7 +4734,7 @@ function createSlackTurn(deps) {
4976
4734
  conversation: completedState.conversation,
4977
4735
  conversationId
4978
4736
  }));
4979
- await persistThreadRuntimeStateWithRetry(thread, completedState);
4737
+ await persistThreadRuntimeStateWithRetry(conversationId, completedState);
4980
4738
  const automatedTurnLimit = await recordFinishedTurnForAutomatedLimit({
4981
4739
  conversationId,
4982
4740
  maxTurns: botConfig.maxConsecutiveAutomatedTurns,
@@ -4985,7 +4743,7 @@ function createSlackTurn(deps) {
4985
4743
  await maybePostAutomatedTurnLimitNotice({
4986
4744
  conversationId,
4987
4745
  maxTurns: botConfig.maxConsecutiveAutomatedTurns,
4988
- threadTs,
4746
+ source,
4989
4747
  update: automatedTurnLimit
4990
4748
  });
4991
4749
  } catch (saveError) {
@@ -5021,7 +4779,7 @@ function createSlackTurn(deps) {
5021
4779
  nowMs: Date.now(),
5022
4780
  sessionId: turnId
5023
4781
  });
5024
- await persistThreadState(thread, { conversation: preparedState.conversation });
4782
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
5025
4783
  if (conversationId) await turnLifecycle.fail({
5026
4784
  conversationId,
5027
4785
  createdAtMs: Date.now(),
@@ -5038,7 +4796,7 @@ function createSlackTurn(deps) {
5038
4796
  conversation: preparedState.conversation,
5039
4797
  sessionId: turnId
5040
4798
  });
5041
- await persistThreadState(thread, { conversation: preparedState.conversation });
4799
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
5042
4800
  persistedAtLeastOnce = true;
5043
4801
  shouldPersistFailureState = false;
5044
4802
  return;
@@ -5122,7 +4880,7 @@ function createSlackTurn(deps) {
5122
4880
  nowMs: Date.now(),
5123
4881
  sessionId: turnId
5124
4882
  });
5125
- await persistThreadState(thread, { conversation: preparedState.conversation });
4883
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
5126
4884
  await turnLifecycle.complete({
5127
4885
  conversationId,
5128
4886
  createdAtMs: Date.now(),
@@ -5179,7 +4937,7 @@ function createSlackTurn(deps) {
5179
4937
  } catch (recordError) {
5180
4938
  logException(recordError, "agent.turn.failed_session_record_persist.failed");
5181
4939
  }
5182
- await persistThreadState(thread, { conversation: preparedState.conversation });
4940
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
5183
4941
  if (shouldEmitDevAgentTrace()) logWarn("agent.turn.failed");
5184
4942
  }
5185
4943
  await status.clear();
@@ -5318,10 +5076,10 @@ async function seedConversationBackfill(thread, conversation, currentTurn, resol
5318
5076
  /** Build the turn-state preparer from injected conversation services. */
5319
5077
  function createPrepareTurnState(deps) {
5320
5078
  return async function prepareTurnState(args) {
5321
- const existingState = await args.thread.state;
5079
+ const conversationId = args.context.conversationId ?? args.context.threadId ?? args.context.runId;
5080
+ const existingState = conversationId ? await getPersistedThreadState(conversationId) : await args.thread.state;
5322
5081
  const sandboxRef = getPersistedSandboxState(existingState ?? {});
5323
5082
  const conversation = coerceThreadConversationState(existingState);
5324
- const conversationId = args.context.threadId ?? args.context.runId;
5325
5083
  if (conversationId && isVisionEnabled()) conversation.vision = await loadConversationVisionCache(conversationId);
5326
5084
  await hydrateConversationMessages({
5327
5085
  conversation,
@@ -5448,7 +5206,6 @@ function createSlackDispatchTurnRunner(options) {
5448
5206
  disabledFeatures: ["interactive-auth"],
5449
5207
  locationConfiguration: options.getLocationConfiguration(dispatch.destination),
5450
5208
  credentialContext: routing.credentialContext,
5451
- destinationVisibility: dispatch.destinationVisibility,
5452
5209
  dispatch: routing.dispatch,
5453
5210
  skipProviderDefaultConfig: true,
5454
5211
  source: dispatch.source,
@@ -5507,8 +5264,8 @@ function createSlackRuntime(options) {
5507
5264
  modelId: defaultModelId(botConfig),
5508
5265
  now: options.now ?? (() => Date.now()),
5509
5266
  prepareTurnState,
5510
- persistPreparedState: async ({ thread, preparedState }) => {
5511
- await persistThreadState(thread, { conversation: preparedState.conversation });
5267
+ persistPreparedState: async ({ conversationId, preparedState }) => {
5268
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
5512
5269
  },
5513
5270
  getPreparedConversationContext: (preparedState) => preparedState.conversationContext,
5514
5271
  decideSubscribedReply: async (args) => {
@@ -5523,18 +5280,18 @@ function createSlackRuntime(options) {
5523
5280
  });
5524
5281
  return decision;
5525
5282
  },
5526
- recordSkippedSteeringMessage: async ({ thread, message, decision, text }) => {
5527
- const conversation = coerceThreadConversationState(await thread.state);
5283
+ recordSkippedSteeringMessage: async ({ conversationId, message, decision, text }) => {
5284
+ const conversation = coerceThreadConversationState(await getPersistedThreadState(conversationId));
5528
5285
  recordSkippedConversationMessage({
5529
5286
  conversation,
5530
5287
  message,
5531
5288
  skippedReason: decision.reason,
5532
5289
  text: text.userText
5533
5290
  });
5534
- await persistThreadState(thread, { conversation });
5291
+ await persistThreadStateById(conversationId, { conversation });
5535
5292
  },
5536
- recordSkippedSubscribedTurn: async ({ thread, message, decision, completedAtMs, text }) => {
5537
- const conversation = coerceThreadConversationState(await thread.state);
5293
+ recordSkippedSubscribedTurn: async ({ conversationId, message, decision, completedAtMs, text }) => {
5294
+ const conversation = coerceThreadConversationState(await getPersistedThreadState(conversationId));
5538
5295
  recordSkippedConversationMessage({
5539
5296
  conversation,
5540
5297
  message,
@@ -5543,16 +5300,16 @@ function createSlackRuntime(options) {
5543
5300
  });
5544
5301
  clearSkippedTurnIfActive(conversation, message.id);
5545
5302
  conversation.processing.lastCompletedAtMs = completedAtMs;
5546
- await persistThreadState(thread, { conversation });
5303
+ await persistThreadStateById(conversationId, { conversation });
5547
5304
  },
5548
- onSubscribedMessageSkipped: async ({ thread, message, preparedState, decision, completedAtMs }) => {
5305
+ onSubscribedMessageSkipped: async ({ conversationId, message, preparedState, decision, completedAtMs }) => {
5549
5306
  markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
5550
5307
  replied: false,
5551
5308
  skippedReason: decision.reason
5552
5309
  });
5553
5310
  clearSkippedTurnIfActive(preparedState.conversation, message.id);
5554
5311
  preparedState.conversation.processing.lastCompletedAtMs = completedAtMs;
5555
- await persistThreadState(thread, { conversation: preparedState.conversation });
5312
+ await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
5556
5313
  },
5557
5314
  executeSlackTurn,
5558
5315
  initializeAssistantThread: async ({ channelId, threadTs, sourceChannelId }) => {
@@ -5599,23 +5356,20 @@ function createJuniorSlackAdapter(config) {
5599
5356
  * authorization pause notices use `sendSlackReply` so the footer stays
5600
5357
  * consistent.
5601
5358
  */
5602
- async function postSlackMessageBestEffort(channelId, threadTs, text, conversationId, replyAttribution) {
5359
+ /** Post a failure reply or status notice when the run's dispatch allows it. */
5360
+ async function postRunNotice(resume, text) {
5361
+ if (!postsRunNotices(resume.run?.dispatch)) return;
5362
+ await sendSlackReply({
5363
+ channelId: resume.channelId,
5364
+ conversationId: resume.conversationId,
5365
+ replyAttribution: resume.run?.dispatch?.replyAttribution,
5366
+ text,
5367
+ threadTs: resume.threadTs
5368
+ });
5369
+ }
5370
+ async function postRunNoticeBestEffort(resume, text) {
5603
5371
  try {
5604
- if (conversationId) {
5605
- await sendSlackReply({
5606
- channelId,
5607
- conversationId,
5608
- replyAttribution,
5609
- text,
5610
- threadTs
5611
- });
5612
- return;
5613
- }
5614
- await postSlackMessage({
5615
- channelId,
5616
- threadTs,
5617
- text
5618
- });
5372
+ await postRunNotice(resume, text);
5619
5373
  } catch {}
5620
5374
  }
5621
5375
  /** Create a read-only configuration service from persisted values. */
@@ -5656,13 +5410,6 @@ function getResumeLogContext(args) {
5656
5410
  modelId: defaultModelId(botConfig)
5657
5411
  };
5658
5412
  }
5659
- async function postResumeFailureReply(args) {
5660
- await postSlackMessage({
5661
- channelId: args.channelId,
5662
- threadTs: args.threadTs,
5663
- text: buildTurnErrorResponse(args.error, args.eventId)
5664
- });
5665
- }
5666
5413
  async function handleResumeFailure(args) {
5667
5414
  const turnLifecycle = getTurnLifecycle();
5668
5415
  const capturedEventId = logException(args.error, args.eventName);
@@ -5686,12 +5433,7 @@ async function handleResumeFailure(args) {
5686
5433
  failureStatePersistError = persistError;
5687
5434
  }
5688
5435
  try {
5689
- await postResumeFailureReply({
5690
- channelId: args.resume.channelId,
5691
- threadTs: args.resume.threadTs,
5692
- eventId,
5693
- error: args.error
5694
- });
5436
+ await postRunNotice(args.resume, buildTurnErrorResponse(args.error, eventId));
5695
5437
  } catch (deliveryError) {
5696
5438
  const deliveryEventId = logException(deliveryError, "slack.resume.failure_delivery.failed", { "app.error.original_event_id": eventId });
5697
5439
  try {
@@ -5767,7 +5509,7 @@ function buildResumedRun(args, statusSession, delivery) {
5767
5509
  * generation begins.
5768
5510
  */
5769
5511
  async function resumeSlackTurn(args) {
5770
- return withLogContext(getResumeLogContext(args), () => resumeSlackTurnInContext(args));
5512
+ return await withTurnSpan(getResumeLogContext(args), args.traceContext && { traceContexts: [args.traceContext] }, () => resumeSlackTurnInContext(args));
5771
5513
  }
5772
5514
  async function resumeSlackTurnInContext(args) {
5773
5515
  const status = createSlackWebApiAssistantStatusSession({
@@ -5810,10 +5552,9 @@ async function resumeSlackTurnInContext(args) {
5810
5552
  channelId: runArgs.channelId,
5811
5553
  timestamp: runArgs.messageTs
5812
5554
  });
5813
- if (runArgs.initialText) await postSlackMessageBestEffort(runArgs.channelId, runArgs.threadTs, runArgs.initialText);
5555
+ if (runArgs.initialText) await postRunNoticeBestEffort(runArgs, runArgs.initialText);
5814
5556
  status.update(runArgs.initialStatus);
5815
5557
  const conversationId = runArgs.conversationId;
5816
- const visibility = (await getConversationStore().get({ conversationId }))?.visibility;
5817
5558
  const visibleConversationId = args.conversationId;
5818
5559
  const sessionId = runArgs.turnId;
5819
5560
  let deliveryConversation;
@@ -5908,7 +5649,6 @@ async function resumeSlackTurnInContext(args) {
5908
5649
  await persistWithRetry(() => recordTurnSummary({
5909
5650
  conversationId: runArgs.conversationId,
5910
5651
  destination: savedRun.destination,
5911
- destinationVisibility: visibility,
5912
5652
  dispatchId,
5913
5653
  resultMessageId: messageTs,
5914
5654
  turnId: runArgs.turnId,
@@ -5954,7 +5694,6 @@ async function resumeSlackTurnInContext(args) {
5954
5694
  durationMs: reply.diagnostics.durationMs,
5955
5695
  usage: reply.diagnostics.usage,
5956
5696
  destination: run.destination,
5957
- destinationVisibility: visibility,
5958
5697
  dispatchId: run.dispatch?.id,
5959
5698
  dispatchOutcome: dispatchResult.outcome,
5960
5699
  ...dispatchErrorMessage ? { errorMessage: dispatchErrorMessage } : void 0,
@@ -5967,7 +5706,6 @@ async function resumeSlackTurnInContext(args) {
5967
5706
  else if (run.dispatch?.id) await recordTurnSummary({
5968
5707
  conversationId: runArgs.conversationId,
5969
5708
  destination: run.destination,
5970
- destinationVisibility: visibility,
5971
5709
  dispatchId: run.dispatch?.id,
5972
5710
  dispatchOutcome: dispatchResult.outcome,
5973
5711
  ...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : void 0,
@@ -6069,7 +5807,7 @@ async function resumeSlackTurnInContext(args) {
6069
5807
  }
6070
5808
  if (deferredPauseHandler) try {
6071
5809
  await deferredPauseHandler();
6072
- if (deferredAuthInfo) await postSlackMessageBestEffort(runArgs.channelId, runArgs.threadTs, buildAuthPauseResponse(deferredAuthInfo.actorId, deferredAuthInfo.providerDisplayName, deferredAuthInfo.requestText), runArgs.conversationId, runArgs.run?.dispatch?.replyAttribution);
5810
+ if (deferredAuthInfo) await postRunNoticeBestEffort(runArgs, buildAuthPauseResponse(deferredAuthInfo.actorId, deferredAuthInfo.providerDisplayName, deferredAuthInfo.requestText));
6073
5811
  return true;
6074
5812
  } catch (pauseError) {
6075
5813
  await handleResumeFailure({
@@ -6262,15 +6000,22 @@ async function runPausedTurn(payload, options, runOptions = {}) {
6262
6000
  return withLogContext({ conversationId: payload.conversationId }, () => runPausedTurnInContext(payload, options, runOptions));
6263
6001
  }
6264
6002
  async function runPausedTurnInContext(payload, options, runOptions) {
6265
- const thread = parseSlackThreadId(payload.conversationId);
6266
6003
  const destination = requireSlackDestination(payload.destination, "Paused turn");
6267
6004
  const wakePausedTurn$1 = options.wakePausedTurn ?? wakePausedTurn;
6005
+ const routing = await resolveConversationRouting({ conversationId: payload.conversationId });
6006
+ const pausedTurn = await getTurnRecord(payload.conversationId, payload.turnId);
6007
+ const target = resolveSlackReplyTarget({
6008
+ destination,
6009
+ location: routing?.location,
6010
+ source: pausedTurn?.source
6011
+ });
6268
6012
  return await resumeSlackTurn({
6269
6013
  messageText: "",
6270
6014
  conversationId: payload.conversationId,
6271
6015
  turnId: payload.turnId,
6272
- channelId: thread?.channelId ?? destination.channelId,
6273
- ...thread?.threadTs ? { threadTs: thread.threadTs } : void 0,
6016
+ traceContext: payload.traceContext,
6017
+ channelId: target.channelId,
6018
+ ...target.threadTs ? { threadTs: target.threadTs } : void 0,
6274
6019
  executeTurn: async (run, saveResult) => await executeTurn(options.agentRunner, run, saveResult),
6275
6020
  scheduleSessionCompletedPluginTasks: options.scheduleSessionCompletedPluginTasks,
6276
6021
  beforeStart: async () => {
@@ -6483,6 +6228,23 @@ async function failStrandedTurnWithFallback(args) {
6483
6228
  failureCode: "agent_run_failed",
6484
6229
  turnId: failed.turnId
6485
6230
  });
6231
+ let replyAttribution;
6232
+ if (failed.dispatchId) {
6233
+ const dispatch = await getDispatchRecord(failed.dispatchId);
6234
+ await recordTurnSummary({
6235
+ conversationId: args.conversationId,
6236
+ destination: dispatch?.destination,
6237
+ dispatchId: failed.dispatchId,
6238
+ dispatchOutcome: "failed",
6239
+ turnId: failed.turnId,
6240
+ sliceId: failed.sliceId,
6241
+ source: dispatch?.source,
6242
+ state: "failed",
6243
+ surface: failed.surface
6244
+ });
6245
+ if (!dispatch || !postsRunNotices(buildDispatchRoutingContext(dispatch).dispatch)) return true;
6246
+ replyAttribution = dispatch.replyAttribution;
6247
+ }
6486
6248
  let routing;
6487
6249
  try {
6488
6250
  routing = await resolveTurnSessionRouting({ conversationId: args.conversationId });
@@ -6493,22 +6255,15 @@ async function failStrandedTurnWithFallback(args) {
6493
6255
  });
6494
6256
  return true;
6495
6257
  }
6496
- if (failed.dispatchId) await recordTurnSummary({
6497
- conversationId: args.conversationId,
6498
- destination: routing.destination,
6499
- dispatchId: failed.dispatchId,
6500
- dispatchOutcome: "failed",
6501
- turnId: failed.turnId,
6502
- sliceId: failed.sliceId,
6503
- source: routing.source,
6504
- state: "failed",
6505
- surface: failed.surface
6258
+ const target = resolveSlackReplyTarget({
6259
+ destination: requireSlackDestination(routing.destination, "Stranded paused turn"),
6260
+ location: routing.location,
6261
+ source: failed.source
6506
6262
  });
6507
- const thread = parseSlackThreadId(args.conversationId);
6508
- const channelId = thread?.channelId ?? requireSlackDestination(routing.destination, "Stranded paused turn").channelId;
6509
- await postSlackMessage({
6510
- channelId,
6511
- ...thread?.threadTs ? { threadTs: thread.threadTs } : void 0,
6263
+ await sendSlackReply({
6264
+ ...target,
6265
+ conversationId: args.conversationId,
6266
+ replyAttribution,
6512
6267
  text: buildTurnFailureResponse(requireTurnFailureEventId(eventId, eventName))
6513
6268
  });
6514
6269
  return true;
@@ -6653,28 +6408,67 @@ async function commitAssistantMessage(args) {
6653
6408
  logException(error, "conversation.assistant.message_persist.failed");
6654
6409
  }
6655
6410
  }
6656
- // src/chat/conversations/destination.ts
6657
- function requireLocalDestination(conversationId) {
6658
- const parsed = localDestinationSchema.safeParse({
6659
- platform: "local",
6660
- conversationId
6661
- });
6662
- if (!parsed.success) throw new Error(`Invalid local conversation id: ${conversationId}`);
6663
- return parsed.data;
6411
+ // src/chat/task-execution/web-steering.ts
6412
+ function steeringMessageFromWeb(message, actor) {
6413
+ const storedAttachments = readMessageAttachments(message.input.attachments);
6414
+ return {
6415
+ actor: {
6416
+ authorId: actor.userId,
6417
+ ...actor.fullName ? { authorName: actor.fullName } : void 0
6418
+ },
6419
+ provenance: instructionProvenanceFor(actor),
6420
+ text: message.input.text.trim(),
6421
+ timestampMs: message.createdAtMs,
6422
+ ...storedAttachments.length ? { storedAttachments } : void 0
6423
+ };
6664
6424
  }
6665
6425
  /**
6666
- * Keep a stored Destination, or create the temporary local Destination.
6426
+ * Drain steered web messages into the active web Turn.
6667
6427
  *
6668
- * TODO(dcramer): Delete this module after Conversation and AgentRun use
6669
- * Location, and web and event Turns no longer need a local
6670
- * Destination.
6428
+ * Only messages from the Turn Actor join. Steered messages from another
6429
+ * participant stay pending and start their own Turn next, so they never act
6430
+ * with this Actor's credentials.
6671
6431
  */
6672
- function resolveConversationDestination(args) {
6673
- return args.existing ?? requireLocalDestination(args.conversationId);
6432
+ function createWebSteeringDrain(args) {
6433
+ const { actor, author, context, conversation, turnId } = args;
6434
+ return async (accept) => {
6435
+ const accepted = [];
6436
+ await context.attempt.drain(async (pending) => {
6437
+ const joining = pending.filter((message) => message.source === "web" && message.input.authorId === actor.userId && legacyWebMailboxMetadataSchema.safeParse(message.input.metadata).success);
6438
+ if (joining.length === 0) return [];
6439
+ for (const message of joining) {
6440
+ const attachments = readMessageAttachments(message.input.attachments);
6441
+ upsertConversationMessage(conversation, {
6442
+ id: message.inboundMessageId,
6443
+ role: "user",
6444
+ text: normalizeConversationText(message.input.text),
6445
+ createdAtMs: message.createdAtMs,
6446
+ author,
6447
+ meta: {
6448
+ ...attachments.length ? { attachments } : void 0,
6449
+ explicitMention: true,
6450
+ source: "web"
6451
+ }
6452
+ });
6453
+ }
6454
+ await persistConversationMessages({
6455
+ conversation,
6456
+ conversationId: context.conversationId
6457
+ });
6458
+ const saved = (await getTurnRecord(context.conversationId, turnId))?.piMessages ?? [];
6459
+ const messages = withoutSavedSteeringMessages(saved, joining.map((message) => steeringMessageFromWeb(message, actor)), buildSteeringPiMessage);
6460
+ if (messages.length > 0) {
6461
+ await accept(messages);
6462
+ accepted.push(...messages);
6463
+ }
6464
+ return joining.map((message) => message.inboundMessageId);
6465
+ });
6466
+ return accepted;
6467
+ };
6674
6468
  }
6675
6469
  // src/chat/task-execution/conversation-turn.ts
6676
6470
  /** Run a mailbox Turn and store its assistant Messages. */
6677
- function stableHex$1(...parts) {
6471
+ function stableHex(...parts) {
6678
6472
  return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24);
6679
6473
  }
6680
6474
  function captureConversationTurnFailure(args) {
@@ -6953,7 +6747,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
6953
6747
  if (stopSignal?.aborted) return await completeCancelledTurn();
6954
6748
  const piMessages = await loadProjection({ conversationId: context.conversationId });
6955
6749
  failureCode = "agent_run_failed";
6956
- currentRunId = `conversation-run:${stableHex$1(turnId, String(startedAtMs))}`;
6750
+ currentRunId = `conversation-run:${stableHex(turnId, String(startedAtMs))}`;
6957
6751
  setTags({ runId: currentRunId });
6958
6752
  const outcome = await executeTurn(agentRunner, {
6959
6753
  conversationId: context.conversationId,
@@ -6982,6 +6776,13 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
6982
6776
  },
6983
6777
  delivery: deliverAssistantMessage,
6984
6778
  durability: {
6779
+ ...webActor ? { drainSteeringMessages: createWebSteeringDrain({
6780
+ actor: webActor,
6781
+ author,
6782
+ context,
6783
+ conversation,
6784
+ turnId
6785
+ }) } : void 0,
6985
6786
  onInputCommitted: acknowledge,
6986
6787
  shouldYield: context.shouldYield,
6987
6788
  onSandboxRefChanged: async (nextSandboxRef) => {
@@ -7044,7 +6845,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
7044
6845
  await maybePostAutomatedTurnLimitNotice({
7045
6846
  conversationId: context.conversationId,
7046
6847
  maxTurns: botConfig.maxConsecutiveAutomatedTurns,
7047
- threadTs: conversationLocation?.provider === "slack" ? conversationLocation.threadTs : void 0,
6848
+ source,
7048
6849
  update: automatedTurnLimit
7049
6850
  });
7050
6851
  completedSuccessfully = reply.diagnostics.outcome === "success";
@@ -7156,11 +6957,13 @@ function createSlackSystemTurnDelivery(args) {
7156
6957
  installation: { teamId: location.teamId },
7157
6958
  state: args.state,
7158
6959
  task: async () => await sendSlackReply({
6960
+ ...resolveSlackReplyTarget({
6961
+ destination: location,
6962
+ location
6963
+ }),
7159
6964
  cards,
7160
- channelId: location.channelId,
7161
6965
  conversationId,
7162
- text,
7163
- ...location.threadTs ? { threadTs: location.threadTs } : void 0
6966
+ text
7164
6967
  })
7165
6968
  });
7166
6969
  } catch (error) {
@@ -7320,165 +7123,1156 @@ function createProductionConversationWorkOptions(options) {
7320
7123
  waitUntil: options.waitUntil
7321
7124
  });
7322
7125
  }
7323
- // src/chat/conversations/web-input.ts
7324
- function normalizeEmail(email) {
7325
- return email.trim().toLowerCase();
7126
+ // src/chat/slack/location-url.ts
7127
+ /** Parse a Slack workspace subdomain used in `https://{domain}.slack.com` URLs. */
7128
+ function parseSlackTeamDomain(value) {
7129
+ if (typeof value !== "string") return void 0;
7130
+ const domain = value.trim().toLowerCase();
7131
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(domain)) return;
7132
+ return domain;
7326
7133
  }
7327
- function stableHex(...parts) {
7328
- return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24);
7134
+ /** Convert a Slack message ts into the `p…` segment used by archive URLs. */
7135
+ function archivePathFromThreadTs(threadTs) {
7136
+ const [seconds, fraction = ""] = threadTs.split(".");
7137
+ return `p${seconds}${(fraction ?? "").padEnd(6, "0").slice(0, 6)}`;
7329
7138
  }
7330
7139
  /**
7331
- * Build a durable Conversation id for one web Actor and create key.
7332
- *
7333
- * Retries with the same key must address the same Conversation before the
7334
- * mailbox Message id is derived.
7140
+ * Build a direct Slack archive URL for a thread when the workspace domain is
7141
+ * known. Omits a link rather than falling back to `app_redirect`.
7335
7142
  */
7336
- function createConversationId(args) {
7337
- return `local:web:${stableHex(normalizeEmail(args.actorEmail), args.idempotencyKey)}`;
7143
+ function buildSlackLocationUrl(args) {
7144
+ const channelId = parseSlackChannelId(args.channelId);
7145
+ const teamDomain = parseSlackTeamDomain(args.teamDomain);
7146
+ const threadTs = parseSlackMessageTs(args.threadTs);
7147
+ if (!channelId || !teamDomain || !threadTs) return void 0;
7148
+ const url = new URL(`https://${teamDomain}.slack.com/archives/${channelId}/${archivePathFromThreadTs(threadTs)}`);
7149
+ url.searchParams.set("thread_ts", threadTs);
7150
+ url.searchParams.set("cid", channelId);
7151
+ return url.toString();
7152
+ }
7153
+ // src/chat/slack/team-domain.ts
7154
+ const TEAM_DOMAIN_CACHE_TTL_MS = 864e5;
7155
+ const TEAM_DOMAIN_NEGATIVE_CACHE_TTL_MS = 6e4;
7156
+ const teamDomainCache = /* @__PURE__ */ new Map();
7157
+ const teamDomainInflight = /* @__PURE__ */ new Map();
7158
+ function readTeamDomainCache(teamId) {
7159
+ const hit = teamDomainCache.get(teamId);
7160
+ if (!hit) return void 0;
7161
+ if (hit.expiresAt < Date.now()) {
7162
+ teamDomainCache.delete(teamId);
7163
+ return;
7164
+ }
7165
+ return hit.kind === "hit" ? hit.domain : null;
7166
+ }
7167
+ function writeTeamDomainCache(teamId, domain) {
7168
+ if (domain) {
7169
+ teamDomainCache.set(teamId, {
7170
+ kind: "hit",
7171
+ domain,
7172
+ expiresAt: Date.now() + TEAM_DOMAIN_CACHE_TTL_MS
7173
+ });
7174
+ return;
7175
+ }
7176
+ teamDomainCache.set(teamId, {
7177
+ kind: "miss",
7178
+ expiresAt: Date.now() + TEAM_DOMAIN_NEGATIVE_CACHE_TTL_MS
7179
+ });
7180
+ }
7181
+ async function fetchSlackTeamDomain(teamId) {
7182
+ try {
7183
+ const response = await withSlackRetries(() => getSlackClient().team.info({ team: teamId }), 3, {
7184
+ action: "team.info",
7185
+ idempotent: true,
7186
+ spanAttributes: { "app.slack.team_id": teamId }
7187
+ });
7188
+ const responseTeamId = parseSlackTeamId(response.team?.id);
7189
+ const domain = parseSlackTeamDomain(response.team?.domain);
7190
+ if (!domain || !responseTeamId || responseTeamId !== teamId) {
7191
+ writeTeamDomainCache(teamId, void 0);
7192
+ return;
7193
+ }
7194
+ writeTeamDomainCache(teamId, domain);
7195
+ return domain;
7196
+ } catch {
7197
+ writeTeamDomainCache(teamId, void 0);
7198
+ return;
7199
+ }
7338
7200
  }
7339
- /** Return the stable Turn id for one mailbox Message. */
7340
- function conversationTurnIdForMessage(messageId) {
7341
- return buildDeterministicTurnId(messageId);
7201
+ /** Resolve a workspace subdomain via `team.info`, cached by team id. */
7202
+ async function resolveSlackTeamDomain(teamId) {
7203
+ const parsedTeamId = parseSlackTeamId(teamId);
7204
+ if (!parsedTeamId) return void 0;
7205
+ const cached = readTeamDomainCache(parsedTeamId);
7206
+ if (cached !== void 0) return cached ?? void 0;
7207
+ const pending = teamDomainInflight.get(parsedTeamId);
7208
+ if (pending) return pending;
7209
+ const lookup = fetchSlackTeamDomain(parsedTeamId).finally(() => {
7210
+ teamDomainInflight.delete(parsedTeamId);
7211
+ });
7212
+ teamDomainInflight.set(parsedTeamId, lookup);
7213
+ return lookup;
7214
+ }
7215
+ /** Resolve workspace subdomains for the distinct team ids in one API response. */
7216
+ async function resolveSlackTeamDomains(teamIds) {
7217
+ const uniqueTeamIds = [...new Set([...teamIds].map((teamId) => parseSlackTeamId(teamId)).filter((teamId) => teamId !== void 0))];
7218
+ const resolved = await Promise.all(uniqueTeamIds.map(async (teamId) => {
7219
+ const domain = await resolveSlackTeamDomain(teamId);
7220
+ return domain ? [teamId, domain] : void 0;
7221
+ }));
7222
+ return new Map(resolved.filter((entry) => entry !== void 0));
7223
+ }
7224
+ // src/api/conversations/shared.ts
7225
+ /** Format a Slack destination without exposing private channel names. */
7226
+ function slackStatsLocationLabel(input) {
7227
+ const channelId = input.channel;
7228
+ if (!channelId) return void 0;
7229
+ if (input.channelNameRedacted && input.channelName) return input.channelName;
7230
+ const name = input.channelName?.replace(/^#/, "");
7231
+ if (channelId.startsWith("D")) return "Direct Message";
7232
+ if (channelId.startsWith("C")) return name ? `#${name}` : "Public Channel";
7233
+ if (channelId.startsWith("G")) {
7234
+ if (name?.startsWith("mpdm-")) return "Group DM";
7235
+ return "Private Channel";
7236
+ }
7237
+ return name || channelId;
7238
+ }
7239
+ /** Return the generic display label for a conversation surface. */
7240
+ function surfaceFallbackLabel(surface) {
7241
+ if (surface === "scheduler") return "Scheduler";
7242
+ if (surface === "api") return "API";
7243
+ if (surface === "internal") return "Internal";
7244
+ return "Conversation";
7245
+ }
7246
+ // src/api/conversations/projection.ts
7247
+ const PRIVATE_CONVERSATION_LABEL = "Private Conversation";
7248
+ function privateConversationLabel(slackConversation) {
7249
+ if (!slackConversation) return PRIVATE_CONVERSATION_LABEL;
7250
+ return slackConversation.visibility === "private" ? formatSlackConversationRedactedLabel(slackConversation) ?? PRIVATE_CONVERSATION_LABEL : PRIVATE_CONVERSATION_LABEL;
7251
+ }
7252
+ function surfaceFromConversationId(conversationId) {
7253
+ if (parseSlackThreadId(conversationId)) return "slack";
7254
+ if (conversationId.startsWith("scheduler:")) return "scheduler";
7255
+ if (conversationId.startsWith("api:")) return "api";
7256
+ return "internal";
7257
+ }
7258
+ function surfaceFromSource(source, conversationId) {
7259
+ if (source === "slack" || source === "api" || source === "web" || source === "scheduler") return source === "web" ? "api" : source;
7260
+ return surfaceFromConversationId(conversationId);
7261
+ }
7262
+ function locationUrlFromConversation(args) {
7263
+ const { conversation } = args;
7264
+ if (!args.canViewPrivateContent || conversation.location?.provider !== "slack") return;
7265
+ const teamDomain = args.teamDomainByTeamId?.get(conversation.location.teamId);
7266
+ const threadTs = conversation.location.threadTs;
7267
+ if (!teamDomain || !threadTs) return void 0;
7268
+ return buildSlackLocationUrl({
7269
+ channelId: conversation.location.channelId,
7270
+ teamDomain,
7271
+ threadTs
7272
+ });
7342
7273
  }
7343
- /**
7344
- * Store the dashboard participant in the current Conversation actor field.
7345
- *
7346
- * TODO(dcramer): Remove this StoredSlackActor conversion after Conversation
7347
- * actor storage accepts web Actors directly.
7348
- */
7349
- function storedActorFromWeb(actor) {
7350
- return {
7351
- ...actor.email ? { email: normalizeEmail(actor.email) } : void 0,
7352
- ...actor.fullName ? { fullName: actor.fullName } : void 0
7274
+ function actorIdentityReport(actor) {
7275
+ if (!actor) return void 0;
7276
+ const identity = {
7277
+ ...actor.email !== void 0 ? { email: actor.email } : void 0,
7278
+ ...actor.fullName !== void 0 ? { fullName: actor.fullName } : void 0,
7279
+ ...actor.slackUserId !== void 0 ? { slackUserId: actor.slackUserId } : void 0,
7280
+ ...actor.slackUserName !== void 0 ? { slackUserName: actor.slackUserName } : void 0
7281
+ };
7282
+ return Object.keys(identity).length > 0 ? identity : void 0;
7283
+ }
7284
+ function statusFromConversation(conversation) {
7285
+ if (conversation.execution.status === "idle") return "completed";
7286
+ if (conversation.execution.status === "failed") return "failed";
7287
+ return "active";
7288
+ }
7289
+ function titleFromConversation(args) {
7290
+ const slackThread = parseSlackThreadId(args.conversation.conversationId);
7291
+ const effectiveChannelName = args.conversation.channelName;
7292
+ const slackConversation = resolveSlackConversationContextFromThreadId({
7293
+ threadId: args.conversation.conversationId,
7294
+ channelName: effectiveChannelName,
7295
+ ...args.visibility ? { visibility: args.visibility } : void 0
7296
+ });
7297
+ return (args.canViewPrivateContent ? void 0 : privateConversationLabel(slackConversation)) ?? args.conversation.title ?? args.automationTitle ?? slackStatsLocationLabel({
7298
+ channel: slackThread?.channelId,
7299
+ channelName: effectiveChannelName
7300
+ }) ?? (args.conversation.source === "plugin" ? "Automation run" : fallbackShortTitle(args.firstHumanMessageText ?? "", surfaceFallbackLabel(args.surface)));
7301
+ }
7302
+ function channelNameFromConversation(conversation, canViewPrivateContent, visibility) {
7303
+ const effectiveChannelName = conversation.channelName;
7304
+ const slackThread = parseSlackThreadId(conversation.conversationId);
7305
+ if (!effectiveChannelName && !slackThread) return;
7306
+ const slackConversation = resolveSlackConversationContextFromThreadId({
7307
+ threadId: conversation.conversationId,
7308
+ channelName: effectiveChannelName,
7309
+ ...visibility ? { visibility } : void 0
7310
+ });
7311
+ if (!canViewPrivateContent) return privateConversationLabel(slackConversation);
7312
+ return effectiveChannelName;
7313
+ }
7314
+ function channelNameRedactedFromConversation(conversation, canViewPrivateContent) {
7315
+ const effectiveChannelName = conversation.channelName;
7316
+ const slackThread = parseSlackThreadId(conversation.conversationId);
7317
+ if (!effectiveChannelName && !slackThread) return false;
7318
+ return !canViewPrivateContent;
7319
+ }
7320
+ /** Describe whether a viewer can read one conversation's retained events. */
7321
+ function conversationEventHistory(args) {
7322
+ if (args.transcriptPurgedAtMs !== void 0) return {
7323
+ status: "expired",
7324
+ expiredAt: new Date(args.transcriptPurgedAtMs).toISOString()
7325
+ };
7326
+ return args.canExposePayload ? { status: "available" } : {
7327
+ status: "redacted",
7328
+ reason: "non_public_conversation"
7353
7329
  };
7354
7330
  }
7355
- /** Rebuild the web Actor from durable Conversation identity. */
7356
- function webActorFromEmail(email, profile) {
7357
- const normalized = normalizeEmail(email);
7331
+ /** Project one durable conversation and its SQL metrics into the REST summary. */
7332
+ function conversationSummaryFromStoredConversation(args) {
7333
+ const { conversation, durationMs, usage } = args;
7334
+ const canViewPrivateContent = args.access?.canViewPrivateContent ?? false;
7335
+ const accessVisibility = args.access?.visibility;
7336
+ const visibility = accessVisibility === "public" || accessVisibility === "private" ? accessVisibility : void 0;
7337
+ const surface = surfaceFromSource(conversation.source, conversation.conversationId);
7338
+ const actorIdentity = actorIdentityReport(conversation.actor);
7339
+ const locationUrl = locationUrlFromConversation({
7340
+ canViewPrivateContent,
7341
+ conversation,
7342
+ ...args.teamDomainByTeamId ? { teamDomainByTeamId: args.teamDomainByTeamId } : void 0
7343
+ });
7344
+ const slackThread = parseSlackThreadId(conversation.conversationId);
7345
+ const channelName = channelNameFromConversation(conversation, canViewPrivateContent, visibility);
7346
+ const channelNameRedacted = channelNameRedactedFromConversation(conversation, canViewPrivateContent);
7358
7347
  return {
7359
- platform: "web",
7360
- userId: `dashboard:${stableHex(normalized)}`,
7361
- email: normalized,
7362
- ...profile?.fullName ? { fullName: profile.fullName } : void 0,
7363
- ...profile?.userName ? { userName: profile.userName } : void 0
7348
+ conversationId: conversation.conversationId,
7349
+ cumulativeDurationMs: durationMs,
7350
+ displayTitle: titleFromConversation({
7351
+ automationTitle: args.automationTitle,
7352
+ canViewPrivateContent,
7353
+ conversation,
7354
+ firstHumanMessageText: args.firstHumanMessageText,
7355
+ surface,
7356
+ visibility
7357
+ }),
7358
+ isParticipant: args.access?.isParticipant ?? false,
7359
+ ...visibility ? { visibility } : void 0,
7360
+ lastProgressAt: new Date(conversation.execution.updatedAtMs ?? conversation.updatedAtMs).toISOString(),
7361
+ lastSeenAt: new Date(conversation.lastActivityAtMs).toISOString(),
7362
+ startedAt: new Date(conversation.createdAtMs).toISOString(),
7363
+ status: statusFromConversation(conversation),
7364
+ surface,
7365
+ ...args.auxiliaryCosts ? { auxiliaryCosts: args.auxiliaryCosts } : void 0,
7366
+ ...usage ? { cumulativeUsage: usage } : void 0,
7367
+ ...args.childAgentTotals ? { childAgentTotals: args.childAgentTotals } : void 0,
7368
+ ...actorIdentity ? { actorIdentity } : void 0,
7369
+ ...locationUrl ? { locationUrl } : void 0,
7370
+ archivedAt: args.archivedAtMs === void 0 ? null : new Date(args.archivedAtMs).toISOString(),
7371
+ ...slackThread ? { channel: slackThread.channelId } : void 0,
7372
+ ...channelName ? { channelName } : void 0,
7373
+ ...channelNameRedacted ? { channelNameRedacted: true } : void 0,
7374
+ ...args.locationId && !channelNameRedacted ? { locationId: args.locationId } : void 0
7364
7375
  };
7365
7376
  }
7366
- /** Build one web mailbox entry. */
7367
- function buildWebInboundMessage(args) {
7368
- const text = args.message.trim();
7369
- if (!text && !args.attachments?.length) throw new Error("Web Message must not be empty");
7370
- if (!args.actor.email) throw new Error("Web Actor requires a verified email");
7371
- const destination = resolveConversationDestination({
7372
- conversationId: args.conversationId,
7373
- existing: args.destination
7374
- });
7375
- const nowMs = args.nowMs ?? Date.now();
7377
+ // src/api/conversations/usage.ts
7378
+ const usage = juniorConversations.usage;
7379
+ const componentFields = [
7380
+ "inputTokens",
7381
+ "outputTokens",
7382
+ "cachedInputTokens",
7383
+ "cacheCreationTokens"
7384
+ ];
7385
+ function summedUsageNumber(field) {
7386
+ return sql`sum((${usage}->>${field})::double precision)::double precision`;
7387
+ }
7388
+ function summedCostNumber(field) {
7389
+ return sql`round(
7390
+ sum((${usage}->'cost'->>${field})::numeric),
7391
+ 12
7392
+ )::double precision`;
7393
+ }
7394
+ function usageFromAggregate(row) {
7395
+ const components = {};
7396
+ let componentTotal = 0;
7397
+ for (const field of componentFields) {
7398
+ const value = row[field];
7399
+ if (value === null) continue;
7400
+ components[field] = value;
7401
+ componentTotal += value;
7402
+ }
7403
+ const cost = {
7404
+ ...row.costInput === null ? void 0 : { input: row.costInput },
7405
+ ...row.costOutput === null ? void 0 : { output: row.costOutput },
7406
+ ...row.costCacheRead === null ? void 0 : { cacheRead: row.costCacheRead },
7407
+ ...row.costCacheWrite === null ? void 0 : { cacheWrite: row.costCacheWrite },
7408
+ ...row.costTotal === null ? void 0 : { total: row.costTotal }
7409
+ };
7410
+ const result = {
7411
+ ...row.totalOnlyTokens === null ? components : { totalTokens: row.totalOnlyTokens + componentTotal },
7412
+ ...row.reasoningTokens === null ? void 0 : { reasoningTokens: row.reasoningTokens },
7413
+ ...Object.keys(cost).length === 0 ? void 0 : { cost }
7414
+ };
7415
+ return hasAgentTurnUsage(result) ? result : void 0;
7416
+ }
7417
+ /**
7418
+ * Aggregate saved runtime and usage in PostgreSQL. The total for each selected
7419
+ * Conversation includes itself and all its child agents at any depth.
7420
+ */
7421
+ async function readConversationTreeMetricsFromSql(db, conversationIds) {
7422
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
7423
+ const hasComponents = sql`coalesce(
7424
+ ${usage}->>'inputTokens',
7425
+ ${usage}->>'outputTokens',
7426
+ ${usage}->>'cachedInputTokens',
7427
+ ${usage}->>'cacheCreationTokens'
7428
+ ) is not null`;
7429
+ const isChildAgent = sql`(${conversationSubtreeColumns.conversationId} <> ${conversationSubtreeColumns.scopeConversationId})`;
7430
+ const rows = await db.select({
7431
+ scopeConversationId: conversationSubtreeColumns.scopeConversationId,
7432
+ isChildAgent,
7433
+ conversations: sql`count(*)::integer`.mapWith(Number),
7434
+ durationMs: sql`coalesce(sum(${juniorConversations.durationMs}), 0)::double precision`,
7435
+ inputTokens: summedUsageNumber("inputTokens"),
7436
+ outputTokens: summedUsageNumber("outputTokens"),
7437
+ cachedInputTokens: summedUsageNumber("cachedInputTokens"),
7438
+ cacheCreationTokens: summedUsageNumber("cacheCreationTokens"),
7439
+ totalOnlyTokens: sql`sum(
7440
+ case
7441
+ when not (${hasComponents})
7442
+ then (${usage}->>'totalTokens')::double precision
7443
+ end
7444
+ )::double precision`,
7445
+ reasoningTokens: summedUsageNumber("reasoningTokens"),
7446
+ costInput: summedCostNumber("input"),
7447
+ costOutput: summedCostNumber("output"),
7448
+ costCacheRead: summedCostNumber("cacheRead"),
7449
+ costCacheWrite: summedCostNumber("cacheWrite"),
7450
+ costTotal: summedCostNumber("total")
7451
+ }).from(juniorConversations).innerJoin(conversationSubtree(conversationIds), eq(conversationSubtreeColumns.conversationId, juniorConversations.conversationId)).groupBy(conversationSubtreeColumns.scopeConversationId, isChildAgent);
7452
+ const ownRows = /* @__PURE__ */ new Map();
7453
+ const childRows = /* @__PURE__ */ new Map();
7454
+ for (const row of rows) (row.isChildAgent ? childRows : ownRows).set(row.scopeConversationId, row);
7455
+ const result = /* @__PURE__ */ new Map();
7456
+ for (const [conversationId, own] of ownRows) {
7457
+ const ownUsage = usageFromAggregate(own);
7458
+ const child = childRows.get(conversationId);
7459
+ const childUsage = child ? usageFromAggregate(child) : void 0;
7460
+ const usage = addAgentTurnUsage(ownUsage, childUsage);
7461
+ result.set(conversationId, {
7462
+ durationMs: own.durationMs + (child?.durationMs ?? 0),
7463
+ ...usage ? { usage } : void 0,
7464
+ ...child ? { childAgents: {
7465
+ count: child.conversations,
7466
+ durationMs: child.durationMs,
7467
+ ...childUsage ? { usage: childUsage } : void 0
7468
+ } } : void 0
7469
+ });
7470
+ }
7471
+ return result;
7472
+ }
7473
+ /**
7474
+ * Project the child agent part of tree metrics into the API shape. Return
7475
+ * nothing when the Conversation has no child agents.
7476
+ */
7477
+ function childAgentTotalsReport(metrics, auxiliaryCosts) {
7478
+ const childAgents = metrics?.childAgents;
7479
+ if (!childAgents) return void 0;
7376
7480
  return {
7377
- conversationId: args.conversationId,
7378
- createdAtMs: args.createdAtMs ?? nowMs,
7379
- delivery: "defer",
7380
- destination,
7381
- inboundMessageId: args.messageId,
7382
- input: {
7383
- authorId: args.actor.userId,
7384
- text,
7385
- ...args.attachments?.length ? { attachments: args.attachments } : void 0,
7386
- metadata: {
7387
- authorEmail: normalizeEmail(args.actor.email),
7388
- ...args.actor.fullName && { authorFullName: args.actor.fullName },
7389
- authorUserId: args.actor.userId,
7390
- ...args.actor.userName && { authorUserName: args.actor.userName },
7391
- kind: "api_turn",
7392
- messageId: args.messageId
7393
- }
7394
- },
7395
- receivedAtMs: nowMs,
7396
- source: "web"
7481
+ ...auxiliaryCosts?.childAgentCostUsd === void 0 ? void 0 : { auxiliaryCostUsd: auxiliaryCosts.childAgentCostUsd },
7482
+ count: childAgents.count,
7483
+ cumulativeDurationMs: childAgents.durationMs,
7484
+ ...childAgents.usage ? { cumulativeUsage: childAgents.usage } : void 0
7397
7485
  };
7398
7486
  }
7399
- /** Record web activity and create a Conversation root when needed. */
7400
- async function recordWebConversationActivity(args) {
7401
- const store = args.conversationStore ?? getConversationStore();
7402
- const existing = await store.get({ conversationId: args.conversationId });
7403
- const destination = resolveConversationDestination({
7404
- conversationId: args.conversationId,
7405
- existing: existing?.destination
7406
- });
7407
- const isNewRoot = !existing;
7408
- const visibility = args.rootVisibility === "private" ? "private" : "public";
7409
- const source = isNewRoot ? createWebSource(args.conversationId, visibility) : void 0;
7410
- await store.recordActivity({
7411
- conversationId: args.conversationId,
7412
- destination,
7413
- nowMs: args.nowMs,
7414
- actor: storedActorFromWeb(args.actor),
7415
- ...isNewRoot ? { source: "web" } : void 0,
7416
- ...source ? { sessionSource: source } : void 0,
7417
- ...isNewRoot ? { visibility } : void 0
7418
- });
7419
- return destination;
7487
+ // src/chat/conversations/fork.ts
7488
+ /** Event key of the agent note that follows the copied events in a fork. */
7489
+ const FORK_NOTE_KEY = "fork:note";
7490
+ const forkNote = alias(juniorConversationEvents, "fork_note");
7491
+ /**
7492
+ * SQL filter for event rows that a fork did not copy from its source. Usage
7493
+ * and cost reports use it, so copied model calls count only in the source.
7494
+ */
7495
+ function notCopiedByFork() {
7496
+ return sql`not exists (
7497
+ select 1 from ${juniorConversationEvents} as ${sql.identifier("fork_note")}
7498
+ where ${forkNote.conversationId} = ${juniorConversationEvents.conversationId}
7499
+ and ${forkNote.idempotencyKey} = ${FORK_NOTE_KEY}
7500
+ and ${forkNote.seq} > ${juniorConversationEvents.seq}
7501
+ )`;
7420
7502
  }
7421
- /** Create a Conversation from web input and enqueue its first Message. */
7422
- async function createAndEnqueueConversation(input, options) {
7423
- if (!input.actor.email) throw new Error("Web Actor requires a verified email");
7503
+ /**
7504
+ * Events that hold credentials, approvals, provider connections, or plugin
7505
+ * state of the source actor. A fork does not copy them.
7506
+ */
7507
+ const UNCOPIED_EVENT_TYPES = /* @__PURE__ */ new Set([
7508
+ "authorization_completed",
7509
+ "authorization_requested",
7510
+ "guardian_action_reviewed",
7511
+ "mcp_provider_connected",
7512
+ "mcp_provider_connected_unowned",
7513
+ "structured_event",
7514
+ "unknown"
7515
+ ]);
7516
+ const FORK_NOTE = "This Conversation is a fork. The earlier messages are copied from another Conversation. Sandbox files, active work, approvals, credentials, Watches, Automations, and the delivery location were not copied. Earlier file paths and runtime context belong to the source Conversation. Verify files in the current Sandbox before you use them.";
7517
+ /** An expected fork request failure, such as a reply without saved history. */
7518
+ var ConversationForkError = class extends Error {};
7519
+ /**
7520
+ * Create a new web root Conversation from the agent history of a source
7521
+ * Conversation, through one completed assistant reply.
7522
+ *
7523
+ * Retries with the same requester and key return the same Conversation.
7524
+ */
7525
+ async function forkConversation(input) {
7424
7526
  const conversationId = createConversationId({
7425
- actorEmail: input.actor.email,
7426
- idempotencyKey: input.idempotencyKey
7527
+ actorEmail: input.actorEmail,
7528
+ idempotencyKey: JSON.stringify([
7529
+ "fork",
7530
+ input.sourceConversationId,
7531
+ input.idempotencyKey
7532
+ ])
7427
7533
  });
7428
- return await appendAndEnqueueWebMessage({
7429
- actor: input.actor,
7430
- conversationId,
7431
- idempotencyKey: input.idempotencyKey,
7432
- message: input.message,
7433
- images: input.images,
7434
- rootVisibility: input.visibility === "private" ? "private" : "public"
7435
- }, options);
7534
+ const executor = getSqlExecutor();
7535
+ const events = getConversationEventStore();
7536
+ const store = getConversationStore();
7537
+ return withConversationEventLock(executor, input.sourceConversationId, () => withConversationEventLock(executor, conversationId, async () => {
7538
+ if (await store.get({ conversationId })) return { conversationId };
7539
+ const source = await store.get({ conversationId: input.sourceConversationId });
7540
+ if (!source || source.transcriptPurgedAtMs !== void 0) throw new ConversationForkError("The source history is no longer available.");
7541
+ if (source.parentConversationId) throw new ConversationForkError("You cannot fork a child conversation.");
7542
+ const boundary = await events.loadByIdempotencyKey(input.sourceConversationId, `message:${input.messageId}:agent`);
7543
+ const reply = boundary?.data.type === "assistant_message" ? piMessageFromHistoryItem(boundary.data) : void 0;
7544
+ if (!boundary || reply?.role !== "assistant" || reply.stopReason !== "stop" || reply.content.some((part) => part.type === "toolCall")) throw new ConversationForkError("Choose a completed assistant reply with saved agent history.");
7545
+ const projection = await loadTurnProjection({
7546
+ conversationId: input.sourceConversationId,
7547
+ committedSeq: boundary.seq,
7548
+ includeTail: false
7549
+ });
7550
+ if (!projection?.messages.length) throw new ConversationForkError("The selected history is no longer available.");
7551
+ if (hasUnfinishedToolCalls(projection.messages)) throw new ConversationForkError("This history has unfinished tool calls. Choose another reply.");
7552
+ const nowMs = Date.now();
7553
+ await recordWebConversationActivity({
7554
+ actor: webActorFromEmail(input.actorEmail, { ...input.actorName ? { fullName: input.actorName } : void 0 }),
7555
+ conversationId,
7556
+ nowMs,
7557
+ rootVisibility: source.visibility === "public" ? "public" : "private"
7558
+ });
7559
+ await executor.db().update(juniorConversations).set({
7560
+ forkedFromConversationId: source.conversationId,
7561
+ title: source.title ? `Fork: ${source.title}` : "Forked conversation"
7562
+ }).where(eq(juniorConversations.conversationId, conversationId));
7563
+ await copyForkEvents({
7564
+ sourceConversationId: source.conversationId,
7565
+ conversationId,
7566
+ events: await events.loadHistory(source.conversationId),
7567
+ boundarySeq: boundary.seq,
7568
+ messageId: input.messageId
7569
+ });
7570
+ await events.append(conversationId, [{
7571
+ idempotencyKey: FORK_NOTE_KEY,
7572
+ createdAtMs: nowMs,
7573
+ data: {
7574
+ type: "user_message",
7575
+ provenance: contextProvenance,
7576
+ content: [{
7577
+ type: "text",
7578
+ text: FORK_NOTE
7579
+ }],
7580
+ timestamp: nowMs
7581
+ }
7582
+ }]);
7583
+ return { conversationId };
7584
+ }));
7436
7585
  }
7437
- async function appendAndEnqueueWebMessage(input, options) {
7438
- const text = input.message.trim();
7439
- if (!text && !input.images?.length) throw new Error("Web Message must not be empty");
7440
- if (!input.actor.email) throw new Error("Web Actor requires a verified email");
7441
- const nowMs = options.nowMs ?? Date.now();
7442
- const messageId = await webMessageId({
7443
- conversationId: input.conversationId,
7444
- idempotencyKey: input.idempotencyKey
7586
+ /**
7587
+ * Copy the source event rows through the selected reply. Copies keep their
7588
+ * sequence, author, timestamp, key, and payload, so the fork shows the same
7589
+ * transcript and event log, and seq references in compactions stay valid.
7590
+ * Later updates of copied Messages and the end of copied Turns are also
7591
+ * copied, so that no copied item stays open in the fork.
7592
+ */
7593
+ async function copyForkEvents(args) {
7594
+ const reply = args.events.find((event) => event.data.type === "message" && event.data.role === "assistant" && event.data.messageId === args.messageId);
7595
+ const cutoffSeq = Math.max(args.boundarySeq, reply?.seq ?? -1);
7596
+ const messageIds = /* @__PURE__ */ new Set();
7597
+ const turnIds = /* @__PURE__ */ new Set();
7598
+ const seqs = [];
7599
+ let historyVersion = 0;
7600
+ for (const event of args.events) {
7601
+ const data = event.data;
7602
+ if (UNCOPIED_EVENT_TYPES.has(data.type)) continue;
7603
+ if (event.idempotencyKey === FORK_NOTE_KEY) continue;
7604
+ if (event.seq <= cutoffSeq) {
7605
+ if (data.type === "message") messageIds.add(data.messageId);
7606
+ if (data.type === "turn_started") turnIds.add(data.turnId);
7607
+ historyVersion = Math.max(historyVersion, event.historyVersion);
7608
+ seqs.push(event.seq);
7609
+ } else if ((data.type === "message_updated" || data.type === "message_handled") && messageIds.has(data.messageId) || (data.type === "turn_completed" || data.type === "turn_failed") && turnIds.has(data.turnId)) seqs.push(event.seq);
7610
+ }
7611
+ if (!seqs.length) return;
7612
+ const e = juniorConversationEvents;
7613
+ await getSqlExecutor().db().execute(sql`insert into ${e} (${sql.join([
7614
+ e.conversationId,
7615
+ e.seq,
7616
+ e.historyVersion,
7617
+ e.schemaVersion,
7618
+ e.idempotencyKey,
7619
+ e.type,
7620
+ e.payload,
7621
+ e.actorIdentityId,
7622
+ e.createdAt
7623
+ ].map((column) => sql.identifier(column.name)), sql`, `)})
7624
+ select ${args.conversationId}, ${e.seq}, least(${e.historyVersion}, ${historyVersion}), ${e.schemaVersion}, ${e.idempotencyKey}, ${e.type}, ${e.payload}, ${e.actorIdentityId}, ${e.createdAt}
7625
+ from ${e}
7626
+ where ${e.conversationId} = ${args.sourceConversationId}
7627
+ and ${inArray(e.seq, seqs)}`);
7628
+ }
7629
+ function hasUnfinishedToolCalls(messages) {
7630
+ const pending = /* @__PURE__ */ new Set();
7631
+ for (const message of messages) if (message.role === "assistant") {
7632
+ for (const part of message.content) if (part.type === "toolCall") pending.add(part.id);
7633
+ } else if (message.role === "toolResult") pending.delete(message.toolCallId);
7634
+ return pending.size > 0;
7635
+ }
7636
+ // src/api/conversations/auxiliary-costs.ts
7637
+ function eventCost() {
7638
+ return sql`CASE
7639
+ WHEN ${juniorConversationEvents.type} = 'structured_event'
7640
+ AND jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
7641
+ THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::numeric
7642
+ WHEN ${juniorConversationEvents.type} IN ('guardian_action_reviewed', 'turn_routed')
7643
+ AND jsonb_typeof(${juniorConversationEvents.payload}->'costUsd') = 'number'
7644
+ THEN (${juniorConversationEvents.payload}->>'costUsd')::numeric
7645
+ ELSE NULL
7646
+ END`;
7647
+ }
7648
+ function addCostUsd(left, right) {
7649
+ return Math.round((left + right) * 0xe8d4a51000) / 0xe8d4a51000;
7650
+ }
7651
+ /**
7652
+ * Aggregate additive event costs by namespaced operation. The total for each
7653
+ * selected Conversation includes itself and all its child agents at any depth.
7654
+ */
7655
+ async function readConversationAuxiliaryCostsFromSql(db, conversationIds) {
7656
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
7657
+ const scopeConversationId = conversationSubtreeColumns.scopeConversationId;
7658
+ const isChildAgent = sql`(${conversationSubtreeColumns.conversationId} <> ${scopeConversationId})`;
7659
+ const namespace = sql`CASE
7660
+ WHEN ${juniorConversationEvents.type} = 'structured_event'
7661
+ THEN ${juniorConversationEvents.payload}->>'namespace'
7662
+ ELSE 'junior'
7663
+ END`;
7664
+ const name = sql`CASE
7665
+ WHEN ${juniorConversationEvents.type} = 'structured_event'
7666
+ THEN ${juniorConversationEvents.payload}->>'name'
7667
+ ELSE ${juniorConversationEvents.type}
7668
+ END`;
7669
+ const cost = eventCost();
7670
+ const rows = await db.select({
7671
+ conversationId: scopeConversationId,
7672
+ childAgentCostUsd: sql`coalesce(round(sum(case when ${isChildAgent} then ${cost} end), 12), 0)::double precision`.mapWith(Number),
7673
+ costUsd: sql`round(sum(${cost}), 12)::double precision`.mapWith(Number),
7674
+ events: sql`count(*)::integer`.mapWith(Number),
7675
+ name,
7676
+ namespace
7677
+ }).from(juniorConversationEvents).innerJoin(conversationSubtree(conversationIds), eq(conversationSubtreeColumns.conversationId, juniorConversationEvents.conversationId)).where(and(or(eq(juniorConversationEvents.type, "structured_event"), inArray(juniorConversationEvents.type, ["guardian_action_reviewed", "turn_routed"])), sql`${cost} >= 0`, notCopiedByFork())).groupBy(scopeConversationId, namespace, name).orderBy(scopeConversationId, namespace, name);
7678
+ const result = /* @__PURE__ */ new Map();
7679
+ for (const row of rows) {
7680
+ const current = result.get(row.conversationId) ?? { total: {
7681
+ costUsd: 0,
7682
+ operations: []
7683
+ } };
7684
+ current.total.costUsd = addCostUsd(current.total.costUsd, row.costUsd);
7685
+ if (row.childAgentCostUsd > 0) current.childAgentCostUsd = addCostUsd(current.childAgentCostUsd ?? 0, row.childAgentCostUsd);
7686
+ current.total.operations.push({
7687
+ costUsd: row.costUsd,
7688
+ events: row.events,
7689
+ name: row.name,
7690
+ namespace: row.namespace
7691
+ });
7692
+ result.set(row.conversationId, current);
7693
+ }
7694
+ return result;
7695
+ }
7696
+ // src/chat/plugins/conversation-sidebar.ts
7697
+ /** Show native objects by default and let plugins select compact sidebar labels. */
7698
+ async function listConversationSidebarAnnotations(conversationIds, annotationsByConversation) {
7699
+ const candidates = new Set(conversationIds);
7700
+ const selected = {};
7701
+ const plugins = getPlugins();
7702
+ const customOwners = new Set(plugins.filter((plugin) => plugin.hooks?.conversationSidebar).map((plugin) => plugin.manifest.name));
7703
+ for (const conversationId of conversationIds) {
7704
+ const objects = (annotationsByConversation.get(conversationId) ?? []).flatMap((annotation) => {
7705
+ if (customOwners.has(annotation.plugin) || !annotation.objectType) return [];
7706
+ return [{
7707
+ key: annotation.key,
7708
+ label: annotation.label,
7709
+ objectType: annotation.objectType === "item" && annotation.kind === "object" && annotation.facts?.type === "deployment" ? "deployment" : annotation.objectType,
7710
+ status: annotation.status
7711
+ }];
7712
+ });
7713
+ if (objects.length) selected[conversationId] = objects;
7714
+ }
7715
+ for (const plugin of plugins) {
7716
+ const hook = plugin.hooks?.conversationSidebar;
7717
+ if (!hook) continue;
7718
+ const annotationsByConversationId = Object.fromEntries(conversationIds.flatMap((conversationId) => {
7719
+ const annotations = (annotationsByConversation.get(conversationId) ?? []).filter((annotation) => annotation.plugin === plugin.manifest.name);
7720
+ return annotations.length > 0 ? [[conversationId, annotations]] : [];
7721
+ }));
7722
+ try {
7723
+ const result = await hook({
7724
+ annotationsByConversationId,
7725
+ conversationIds,
7726
+ db: getDb(),
7727
+ log: createPluginLogger(plugin.manifest.name),
7728
+ plugin: { name: plugin.manifest.name },
7729
+ cache: createPluginCache(plugin.manifest.name)
7730
+ });
7731
+ for (const [conversationId, annotations] of Object.entries(result.annotationsByConversationId)) {
7732
+ if (!candidates.has(conversationId)) continue;
7733
+ const parsed = conversationSidebarAnnotationSchema.array().safeParse(annotations);
7734
+ if (!parsed.success || parsed.data.length === 0) continue;
7735
+ selected[conversationId] = [...selected[conversationId] ?? [], ...parsed.data];
7736
+ }
7737
+ } catch (error) {
7738
+ logWarn("plugin.conversation_sidebar.hook.failed", {
7739
+ "app.plugin.name": plugin.manifest.name,
7740
+ "exception.message": error instanceof Error ? error.message : String(error)
7741
+ });
7742
+ }
7743
+ }
7744
+ return selected;
7745
+ }
7746
+ // src/chat/plugins/unfinished-work.ts
7747
+ /**
7748
+ * Return assigned and unfinished plugin work for the candidate conversations.
7749
+ * Feed Priority combines these signals with conversation activity on the host.
7750
+ */
7751
+ async function listConversationWork(conversationIds) {
7752
+ if (conversationIds.length === 0) return {
7753
+ assignedIds: [],
7754
+ finishedAtById: {},
7755
+ unfinishedIds: []
7756
+ };
7757
+ const candidates = new Set(conversationIds);
7758
+ const assigned = /* @__PURE__ */ new Set();
7759
+ const finishedAtById = /* @__PURE__ */ new Map();
7760
+ const unfinished = /* @__PURE__ */ new Set();
7761
+ for (const plugin of getPlugins()) {
7762
+ const hook = plugin.hooks?.unfinishedWork;
7763
+ if (!hook) continue;
7764
+ try {
7765
+ const result = await hook({
7766
+ conversationIds,
7767
+ db: getDb(),
7768
+ log: createPluginLogger(plugin.manifest.name),
7769
+ plugin: { name: plugin.manifest.name },
7770
+ cache: createPluginCache(plugin.manifest.name)
7771
+ });
7772
+ for (const conversationId of result.conversationIds) {
7773
+ if (!candidates.has(conversationId)) continue;
7774
+ unfinished.add(conversationId);
7775
+ assigned.add(conversationId);
7776
+ }
7777
+ for (const conversationId of result.assignedConversationIds ?? []) if (candidates.has(conversationId)) assigned.add(conversationId);
7778
+ for (const [conversationId, finishedAt] of Object.entries(result.finishedWorkAtByConversationId ?? {})) {
7779
+ if (!candidates.has(conversationId)) continue;
7780
+ const time = Date.parse(finishedAt);
7781
+ if (!Number.isFinite(time)) continue;
7782
+ const current = finishedAtById.get(conversationId);
7783
+ if (!current || time > Date.parse(current)) finishedAtById.set(conversationId, finishedAt);
7784
+ }
7785
+ } catch (error) {
7786
+ logWarn("plugin.unfinished_work.hook.failed", {
7787
+ "app.plugin.name": plugin.manifest.name,
7788
+ "exception.message": error instanceof Error ? error.message : String(error)
7789
+ });
7790
+ }
7791
+ }
7792
+ return {
7793
+ assignedIds: conversationIds.filter((conversationId) => assigned.has(conversationId)),
7794
+ finishedAtById: Object.fromEntries(conversationIds.flatMap((conversationId) => {
7795
+ const finishedAt = finishedAtById.get(conversationId);
7796
+ return finishedAt ? [[conversationId, finishedAt]] : [];
7797
+ })),
7798
+ unfinishedIds: conversationIds.filter((conversationId) => unfinished.has(conversationId))
7799
+ };
7800
+ }
7801
+ // src/api/conversations/priority.ts
7802
+ /** Conversations with no known work leave Priority after this idle window. */
7803
+ const UNASSIGNED_PRIORITY_WINDOW_MS = 108e5;
7804
+ /** Unfinished work leaves Priority after this idle window. */
7805
+ const UNFINISHED_PRIORITY_WINDOW_MS = 1728e5;
7806
+ /**
7807
+ * Decide whether a conversation belongs in the dashboard Priority section.
7808
+ *
7809
+ * Priority includes only:
7810
+ * - unfinished work last seen within 48 hours
7811
+ * - finished assigned work with a user message after the finish time
7812
+ * - no known work last seen within 3 hours
7813
+ *
7814
+ * Finished assigned work with no later activity stays out of Priority.
7815
+ */
7816
+ function isConversationPriority(conversation, nowMs) {
7817
+ const lastSeenAt = Date.parse(conversation.lastSeenAt);
7818
+ if (!Number.isFinite(lastSeenAt)) return false;
7819
+ if (conversation.unfinishedWork) return nowMs - lastSeenAt <= UNFINISHED_PRIORITY_WINDOW_MS;
7820
+ if (conversation.assignedWork) {
7821
+ const finishedAt = Date.parse(conversation.finishedWorkAt ?? "");
7822
+ const lastUserMessageAt = Date.parse(conversation.lastUserMessageAt ?? "");
7823
+ return Number.isFinite(finishedAt) && Number.isFinite(lastUserMessageAt) && lastUserMessageAt > finishedAt;
7824
+ }
7825
+ return nowMs - lastSeenAt <= UNASSIGNED_PRIORITY_WINDOW_MS;
7826
+ }
7827
+ // src/api/conversations/user-message-activity.ts
7828
+ /** Read the latest true user message time for each selected conversation. */
7829
+ async function readLastUserMessageAtByConversation(db, conversationIds) {
7830
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
7831
+ const authority = sql`${juniorConversationEvents.payload}->'provenance'->>'authority'`;
7832
+ const actorPlatform = sql`${juniorConversationEvents.payload}->'provenance'->'actor'->>'platform'`;
7833
+ const role = sql`${juniorConversationEvents.payload}->>'role'`;
7834
+ const rows = await db.select({
7835
+ conversationId: juniorConversationEvents.conversationId,
7836
+ lastUserMessageAt: sql`max(${juniorConversationEvents.createdAt})`
7837
+ }).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);
7838
+ return new Map(rows.map((row) => [row.conversationId, new Date(row.lastUserMessageAt).toISOString()]));
7839
+ }
7840
+ // src/api/conversations/activity-preview.ts
7841
+ /** Read one recent assistant message, or the latest user message, for each conversation. */
7842
+ async function readConversationActivityPreviews(db, conversationIds) {
7843
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
7844
+ const role = sql`${juniorConversationEvents.payload}->>'role'`;
7845
+ const text = sql`${juniorConversationEvents.payload}->>'text'`;
7846
+ const assistantFirst = sql`case when ${role} = 'assistant' then 0 else 1 end`;
7847
+ const rows = await db.selectDistinctOn([juniorConversationEvents.conversationId], {
7848
+ conversationId: juniorConversationEvents.conversationId,
7849
+ createdAt: juniorConversationEvents.createdAt,
7850
+ role: role.as("role"),
7851
+ text: text.as("text")
7852
+ }).from(juniorConversationEvents).where(and(inArray(juniorConversationEvents.conversationId, conversationIds), eq(juniorConversationEvents.type, "message"), sql`${role} in ('assistant', 'user')`, isNotNull(sql`nullif(btrim(${text}), '')`))).orderBy(juniorConversationEvents.conversationId, assistantFirst, desc(juniorConversationEvents.createdAt), desc(juniorConversationEvents.seq));
7853
+ return new Map(rows.map((row) => [row.conversationId, {
7854
+ createdAt: row.createdAt.toISOString(),
7855
+ role: row.role,
7856
+ text: row.text
7857
+ }]));
7858
+ }
7859
+ // src/api/conversations/first-message.ts
7860
+ /**
7861
+ * Read the text of the first human message for each untitled conversation.
7862
+ * Read-time titles use it while the generated title is still unset.
7863
+ */
7864
+ async function readFirstHumanMessageTexts(db, conversations) {
7865
+ const untitledIds = conversations.flatMap((conversation) => conversation.title ? [] : [conversation.conversationId]);
7866
+ if (untitledIds.length === 0) return /* @__PURE__ */ new Map();
7867
+ const text = sql`${juniorConversationEvents.payload}->>'text'`;
7868
+ const rows = await db.selectDistinctOn([juniorConversationEvents.conversationId], {
7869
+ conversationId: juniorConversationEvents.conversationId,
7870
+ text: text.as("text")
7871
+ }).from(juniorConversationEvents).where(and(inArray(juniorConversationEvents.conversationId, untitledIds), eq(juniorConversationEvents.type, "message"), sql`${juniorConversationEvents.payload}->>'role' = 'user'`, sql`coalesce(${juniorConversationEvents.payload}->'meta'->'author'->>'isBot', 'false') <> 'true'`, isNotNull(sql`nullif(btrim(${text}), '')`))).orderBy(juniorConversationEvents.conversationId, asc(juniorConversationEvents.createdAt), asc(juniorConversationEvents.seq));
7872
+ return new Map(rows.map((row) => [row.conversationId, row.text]));
7873
+ }
7874
+ // src/api/mentions.ts
7875
+ const SLACK_USER_MENTION_RE = /<@([UW][A-Z0-9]+)(?:\|[^<>]*)?>/g;
7876
+ function mentionedSlackUserIds(texts) {
7877
+ const ids = texts.flatMap((text) => text ? [...text.matchAll(SLACK_USER_MENTION_RE)].map((m) => m[1]) : []);
7878
+ return [...new Set(ids)];
7879
+ }
7880
+ /**
7881
+ * Resolve the people that each source mentions, in source order. Every API
7882
+ * that returns display text uses this, so the dashboard can show the same
7883
+ * names and People links everywhere. It does one identity query per Slack
7884
+ * workspace.
7885
+ */
7886
+ async function resolveMentions(executor, sources) {
7887
+ const idsBySource = sources.map((source) => mentionedSlackUserIds(source.texts));
7888
+ const idsByTenant = /* @__PURE__ */ new Map();
7889
+ sources.forEach((source, index) => {
7890
+ if (idsBySource[index].length === 0) return;
7891
+ const ids = idsByTenant.get(source.tenantId) ?? /* @__PURE__ */ new Set();
7892
+ for (const id of idsBySource[index]) ids.add(id);
7893
+ idsByTenant.set(source.tenantId, ids);
7445
7894
  });
7446
- const destination = await recordWebConversationActivity({
7447
- actor: input.actor,
7448
- conversationId: input.conversationId,
7449
- conversationStore: options.conversationStore,
7450
- nowMs,
7451
- ...input.rootVisibility && { rootVisibility: input.rootVisibility }
7895
+ const peopleByTenant = new Map(await Promise.all([...idsByTenant].map(async ([tenantId, ids]) => [tenantId, await readMentionedPeople(executor, tenantId, [...ids])])));
7896
+ return sources.map((source, index) => {
7897
+ const people = peopleByTenant.get(source.tenantId);
7898
+ return idsBySource[index].flatMap((id) => {
7899
+ const person = people?.get(id);
7900
+ return person ? [person] : [];
7901
+ });
7452
7902
  });
7453
- let attachments = [];
7454
- if (input.images?.length) {
7455
- if (!options.attachmentStorage) throw new Error("Attachment storage is unavailable.");
7456
- attachments = await storeInputImages({
7457
- conversationId: input.conversationId,
7458
- files: input.images,
7459
- storage: options.attachmentStorage
7903
+ }
7904
+ /**
7905
+ * Read current names for mentioned Slack users. Known Junior identities come
7906
+ * first. The email is set only for a linked, verified Junior user, because
7907
+ * that email is the People profile route.
7908
+ */
7909
+ async function readMentionedPeople(executor, tenantId, userIds) {
7910
+ const rows = await executor.db().select({
7911
+ displayName: juniorIdentities.displayName,
7912
+ emailVerified: juniorIdentities.emailVerified,
7913
+ handle: juniorIdentities.handle,
7914
+ providerSubjectId: juniorIdentities.providerSubjectId,
7915
+ userDisplayName: juniorUsers.displayName,
7916
+ userEmail: juniorUsers.primaryEmail
7917
+ }).from(juniorIdentities).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(eq(juniorIdentities.provider, "slack"), inArray(juniorIdentities.providerSubjectId, userIds), tenantId ? eq(juniorIdentities.providerTenantId, tenantId) : void 0));
7918
+ const people = /* @__PURE__ */ new Map();
7919
+ for (const row of rows) {
7920
+ if (people.has(row.providerSubjectId)) continue;
7921
+ const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
7922
+ const email = row.emailVerified ? row.userEmail?.trim() : void 0;
7923
+ people.set(row.providerSubjectId, {
7924
+ slackUserId: row.providerSubjectId,
7925
+ ...fullName ? { fullName } : void 0,
7926
+ ...row.handle ? { slackUserName: row.handle } : void 0,
7927
+ ...email ? { email } : void 0
7460
7928
  });
7461
7929
  }
7462
- const result = await (options.exclusive ? appendAndEnqueueExclusiveInboundMessage : appendAndEnqueueInboundMessage)({
7463
- message: buildWebInboundMessage({
7464
- actor: input.actor,
7465
- conversationId: input.conversationId,
7466
- destination,
7467
- message: text,
7468
- attachments,
7469
- messageId,
7470
- nowMs
7471
- }),
7472
- conversationStore: options.conversationStore,
7473
- nowMs,
7474
- queue: options.queue,
7475
- state: options.state
7930
+ const unnamed = userIds.filter((id) => {
7931
+ const person = people.get(id);
7932
+ return !person?.fullName && !person?.slackUserName;
7933
+ });
7934
+ if (tenantId && unnamed.length > 0) {
7935
+ const profiles = await Promise.all(unnamed.map((id) => lookupSlackUser(tenantId, id)));
7936
+ unnamed.forEach((id, index) => {
7937
+ const profile = profiles[index];
7938
+ if (!profile?.fullName && !profile?.userName) return;
7939
+ people.set(id, {
7940
+ ...people.get(id),
7941
+ slackUserId: id,
7942
+ ...profile.fullName ? { fullName: profile.fullName } : void 0,
7943
+ ...profile.userName ? { slackUserName: profile.userName } : void 0
7944
+ });
7945
+ });
7946
+ }
7947
+ return people;
7948
+ }
7949
+ // src/api/conversations/mentions.ts
7950
+ /** Text in one event that the dashboard renders with mentions. */
7951
+ function eventMentionTexts(data) {
7952
+ switch (data.type) {
7953
+ case "message": return data.redacted ? [] : [
7954
+ data.text,
7955
+ data.trustedSummary,
7956
+ ...(data.events ?? []).map((event) => event.trustedSummary)
7957
+ ];
7958
+ case "assistant_message": return data.parts.map((part) => part.text);
7959
+ case "compaction":
7960
+ case "handoff": return [data.summary];
7961
+ case "structured_event": return [data.presentation.preview, ...(data.presentation.details ?? []).map((detail) => detail.description)];
7962
+ default: return [];
7963
+ }
7964
+ }
7965
+ function briefMentionTexts(brief) {
7966
+ return [
7967
+ brief.summary,
7968
+ brief.intent,
7969
+ brief.outcome.text,
7970
+ ...brief.decisions.flatMap((decision) => [decision.text, decision.by]),
7971
+ ...brief.openDecisions.flatMap((decision) => [decision.text, decision.owner]),
7972
+ ...brief.facts
7973
+ ];
7974
+ }
7975
+ function withMentions(value, mentions) {
7976
+ return mentions.length > 0 ? {
7977
+ ...value,
7978
+ mentions
7979
+ } : value;
7980
+ }
7981
+ /**
7982
+ * Attach the people that each event and the Brief of one conversation
7983
+ * mention, resolved at read time.
7984
+ */
7985
+ async function attachConversationMentions(executor, conversationId, content) {
7986
+ const tenantId = (await loadConversationSlackTenantIds(executor, [conversationId])).get(conversationId);
7987
+ const sources = content.events.map((event) => ({
7988
+ tenantId,
7989
+ texts: eventMentionTexts(event.data)
7990
+ }));
7991
+ if (content.brief) sources.push({
7992
+ tenantId,
7993
+ texts: briefMentionTexts(content.brief)
7476
7994
  });
7477
- const status = result.status === "appended" ? "accepted" : result.status;
7995
+ const mentions = await resolveMentions(executor, sources);
7996
+ return {
7997
+ briefMentions: content.brief ? mentions.at(-1) : [],
7998
+ events: content.events.map((event, index) => withMentions(event, mentions[index]))
7999
+ };
8000
+ }
8001
+ /** Attach the people that each activity preview mentions, resolved at read time. */
8002
+ async function attachActivityPreviewMentions(executor, previews) {
8003
+ const entries = [...previews];
8004
+ const tenants = await loadConversationSlackTenantIds(executor, entries.map(([conversationId]) => conversationId));
8005
+ const mentions = await resolveMentions(executor, entries.map(([conversationId, preview]) => ({
8006
+ tenantId: tenants.get(conversationId),
8007
+ texts: [preview.text]
8008
+ })));
8009
+ return new Map(entries.map(([conversationId, preview], index) => [conversationId, withMentions(preview, mentions[index])]));
8010
+ }
8011
+ // src/api/conversations/participants.ts
8012
+ const identityColumns = {
8013
+ displayName: juniorIdentities.displayName,
8014
+ email: juniorIdentities.email,
8015
+ handle: juniorIdentities.handle,
8016
+ identityId: juniorIdentities.id,
8017
+ provider: juniorIdentities.provider,
8018
+ providerSubjectId: juniorIdentities.providerSubjectId,
8019
+ userDisplayName: juniorUsers.displayName,
8020
+ userEmail: juniorUsers.primaryEmail,
8021
+ userId: juniorIdentities.userId
8022
+ };
8023
+ function participantIdentity(row) {
8024
+ const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
8025
+ const email = row.email?.trim() || row.userEmail?.trim();
8026
+ const slack = row.provider === "slack";
8027
+ const participant = {
8028
+ ...email ? { email } : void 0,
8029
+ ...fullName ? { fullName } : void 0,
8030
+ ...slack && row.providerSubjectId ? { slackUserId: row.providerSubjectId } : void 0,
8031
+ ...slack && row.handle ? { slackUserName: row.handle } : void 0
8032
+ };
8033
+ return Object.keys(participant).length > 0 ? participant : void 0;
8034
+ }
8035
+ /** Read actor identities in first-appearance order for each Conversation. */
8036
+ async function readConversationParticipants(db, conversationIds) {
8037
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
8038
+ const [rootRows, eventRows] = await Promise.all([db.select({
8039
+ conversationId: juniorConversations.conversationId,
8040
+ ...identityColumns
8041
+ }).from(juniorConversations).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(inArray(juniorConversations.conversationId, [...conversationIds])), db.selectDistinctOn([juniorConversationEvents.conversationId, juniorConversationEvents.actorIdentityId], {
8042
+ conversationId: juniorConversationEvents.conversationId,
8043
+ seq: juniorConversationEvents.seq,
8044
+ ...identityColumns
8045
+ }).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))]);
8046
+ const ordered = /* @__PURE__ */ new Map();
8047
+ const add = (conversationId, row) => {
8048
+ const participant = participantIdentity(row);
8049
+ if (!participant) return;
8050
+ const group = ordered.get(conversationId) ?? {
8051
+ identities: [],
8052
+ keys: /* @__PURE__ */ new Set()
8053
+ };
8054
+ const key = row.userId ? `user:${row.userId}` : `identity:${row.identityId}`;
8055
+ if (!group.keys.has(key)) {
8056
+ group.keys.add(key);
8057
+ group.identities.push(participant);
8058
+ }
8059
+ ordered.set(conversationId, group);
8060
+ };
8061
+ for (const row of rootRows) add(row.conversationId, row);
8062
+ for (const row of eventRows.sort((left, right) => left.seq - right.seq)) add(row.conversationId, row);
8063
+ return new Map([...ordered].map(([conversationId, group]) => [conversationId, group.identities]));
8064
+ }
8065
+ // src/api/conversations/list.ts
8066
+ const CONVERSATION_FEED_LIMIT = 50;
8067
+ function conversationFeedMembershipFilter(status, filter) {
8068
+ if (!filter) return status === "archived" ? sql`false` : void 0;
8069
+ if (filter.kind === "viewer") return and(conversationIsMemberRoot({ userId: filter.userId }), status === "archived" ? conversationArchivedForUser(filter.userId) : status === "all" ? void 0 : conversationNotArchivedForUser(filter.userId));
8070
+ return and(conversationIsMemberRoot({ email: filter.email }), status === "archived" ? conversationArchivedForEmail(filter.email) : status === "all" ? void 0 : conversationNotArchivedForEmail(filter.email));
8071
+ }
8072
+ /**
8073
+ * Keep root conversations linked to one code repository. A code change can
8074
+ * link a child conversation, so linked ids resolve to their root. Private
8075
+ * roots stay visible only to their members.
8076
+ */
8077
+ function conversationCodeRepositoryFilter(repositoryId, viewerUserId) {
8078
+ return and(sql`${juniorConversations.conversationId} IN (
8079
+ SELECT coalesce(linked.root_conversation_id, linked.conversation_id)
8080
+ FROM ${juniorConversations} AS linked
8081
+ WHERE linked.conversation_id IN (
8082
+ SELECT unnest(${juniorCodeChanges.conversationIds})
8083
+ FROM ${juniorCodeChanges}
8084
+ WHERE ${juniorCodeChanges.repositoryId} = ${repositoryId}
8085
+ )
8086
+ )`, or(eq(juniorDestinations.visibility, "public"), viewerUserId ? viewerConversationMembership({
8087
+ actorMatch: eq(juniorIdentities.userId, viewerUserId),
8088
+ participantMatch: conversationHasParticipantUser(viewerUserId)
8089
+ }) : void 0));
8090
+ }
8091
+ async function conversationRows(db, limit, status, filter, query, includePrivateBriefs = false, codeRepository) {
8092
+ return db.select({
8093
+ conversation: conversationReadColumns(),
8094
+ location: juniorDestinations,
8095
+ identityDisplayName: juniorIdentities.displayName,
8096
+ identityEmail: juniorIdentities.email,
8097
+ identityHandle: juniorIdentities.handle,
8098
+ identityProvider: juniorIdentities.provider,
8099
+ identitySubjectId: juniorIdentities.providerSubjectId,
8100
+ identityTenantId: juniorIdentities.providerTenantId,
8101
+ userDisplayName: juniorUsers.displayName
8102
+ }).from(juniorConversations).leftJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(isNull(juniorConversations.parentConversationId), conversationFeedMembershipFilter(status, filter), codeRepository ? conversationCodeRepositoryFilter(codeRepository.id, codeRepository.viewerUserId) : void 0, query ? or(sql`strpos(lower(coalesce(${juniorConversations.title}, '')), ${query}) > 0`, sql`exists (
8103
+ select 1
8104
+ from ${juniorConversationBriefs}
8105
+ where ${juniorConversationBriefs.conversationId} = ${juniorConversations.conversationId}
8106
+ and ${includePrivateBriefs ? sql`true` : eq(juniorDestinations.visibility, "public")}
8107
+ and ${juniorConversationBriefs.version} = (
8108
+ select max(${juniorConversationBriefs.version})
8109
+ from ${juniorConversationBriefs}
8110
+ where ${juniorConversationBriefs.conversationId} = ${juniorConversations.conversationId}
8111
+ )
8112
+ and to_tsvector('english', ${juniorConversationBriefs.searchText})
8113
+ @@ websearch_to_tsquery('english', ${query})
8114
+ )`) : void 0)).orderBy(desc(juniorConversations.lastActivityAt), asc(juniorConversations.conversationId)).limit(limit);
8115
+ }
8116
+ /** Decode a conversation row with the linked user name and identity-scoped provider fields. */
8117
+ function conversationFromRow(row) {
8118
+ const value = row.conversation;
8119
+ const legacySessionSource = value.sessionSource === null ? void 0 : parseSessionSource(value.sessionSource);
8120
+ if (value.sessionSource !== null && !legacySessionSource) throw new Error("Conversation legacy source_json is invalid");
8121
+ const actorFullName = row.userDisplayName?.trim() ? row.userDisplayName : row.identityDisplayName;
8122
+ const actor = row.identityProvider === "slack" ? {
8123
+ platform: "slack",
8124
+ ...row.identityEmail ? { email: row.identityEmail } : void 0,
8125
+ ...actorFullName ? { fullName: actorFullName } : void 0,
8126
+ ...row.identitySubjectId ? { slackUserId: row.identitySubjectId } : void 0,
8127
+ ...row.identityHandle ? { slackUserName: row.identityHandle } : void 0,
8128
+ ...row.identityTenantId ? { teamId: row.identityTenantId } : void 0
8129
+ } : void 0;
8130
+ const location = locationFromRow(value.location, row.location, legacySessionSource);
8131
+ return {
8132
+ schemaVersion: 1,
8133
+ conversationId: value.conversationId,
8134
+ createdAtMs: value.createdAt.getTime(),
8135
+ lastActivityAtMs: value.lastActivityAt.getTime(),
8136
+ updatedAtMs: value.updatedAt.getTime(),
8137
+ execution: {
8138
+ status: value.executionStatus,
8139
+ ...value.runId ? { runId: value.runId } : void 0,
8140
+ ...value.executionUpdatedAt ? { updatedAtMs: value.executionUpdatedAt.getTime() } : void 0
8141
+ },
8142
+ ...actor ? { actor } : void 0,
8143
+ ...location ? { location } : void 0,
8144
+ ...value.channelName ? { channelName: value.channelName } : void 0,
8145
+ ...value.source ? { source: value.source } : void 0,
8146
+ ...value.title ? { title: value.title } : void 0,
8147
+ ...value.transcriptPurgedAt ? { transcriptPurgedAtMs: value.transcriptPurgedAt.getTime() } : void 0
8148
+ };
8149
+ }
8150
+ /** Read one normalized conversation record directly from its SQL row. */
8151
+ async function readConversationRecordFromSql(conversationId) {
8152
+ const row = (await getDb().select({
8153
+ conversation: conversationReadColumns(),
8154
+ location: juniorDestinations,
8155
+ identityDisplayName: juniorIdentities.displayName,
8156
+ identityEmail: juniorIdentities.email,
8157
+ identityHandle: juniorIdentities.handle,
8158
+ identityProvider: juniorIdentities.provider,
8159
+ identitySubjectId: juniorIdentities.providerSubjectId,
8160
+ identityTenantId: juniorIdentities.providerTenantId,
8161
+ userDisplayName: juniorUsers.displayName
8162
+ }).from(juniorConversations).leftJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(eq(juniorConversations.conversationId, conversationId)).limit(1))[0];
8163
+ return row ? {
8164
+ conversation: conversationFromRow(row),
8165
+ durationMs: row.conversation.durationMs,
8166
+ ...row.location?.visibility === "public" ? { locationId: row.location.id } : void 0,
8167
+ usage: row.conversation.usage,
8168
+ rootConversationId: row.conversation.rootConversationId
8169
+ } : void 0;
8170
+ }
8171
+ /** Prefer the authenticated viewer; fall back to actor email membership. */
8172
+ function conversationFeedFilter(options) {
8173
+ if (options.viewer) return {
8174
+ kind: "viewer",
8175
+ userId: options.viewer.id
8176
+ };
8177
+ if (options.actorEmail) return {
8178
+ kind: "actorEmail",
8179
+ email: options.actorEmail
8180
+ };
8181
+ }
8182
+ /**
8183
+ * Build a bounded dashboard feed. Prefer the viewer user when present; otherwise
8184
+ * keep only roots linked to actorEmail before applying the limit. Membership is
8185
+ * root-actor ownership or a materialized participant row for that person.
8186
+ *
8187
+ * `codeRepositoryId` replaces membership with every root linked to that
8188
+ * repository's code changes. Private roots appear only for their members, and
8189
+ * per-person archive state does not hide rows.
8190
+ */
8191
+ async function readConversationFeedFromSql(options = {}) {
8192
+ const nowMs = Date.now();
8193
+ const db = getDb();
8194
+ const filter = options.codeRepositoryId ? void 0 : conversationFeedFilter(options);
8195
+ const query = options.q?.trim().toLowerCase() || void 0;
8196
+ const rows = await conversationRows(db, options.limit ?? CONVERSATION_FEED_LIMIT, query || options.codeRepositoryId ? "all" : options.status ?? "active", filter, query, filter?.kind === "viewer", options.codeRepositoryId ? {
8197
+ id: options.codeRepositoryId,
8198
+ ...options.viewer ? { viewerUserId: options.viewer.id } : void 0
8199
+ } : void 0);
8200
+ const conversations = rows.map((row) => conversationFromRow(row));
8201
+ const conversationIds = conversations.map((conversation) => conversation.conversationId);
8202
+ const [accessByConversation, archiveTimesByConversation, annotationsByConversation, auxiliaryCostsByRoot, metricsByRoot, participantsByConversation, teamDomainByTeamId, conversationWork, lastUserMessageAtByConversation, activityPreviewByConversation, automationTitleByConversation, firstHumanMessageTextByConversation] = await Promise.all([
8203
+ readConversationAccessFromSql(db, conversationIds, options.viewer),
8204
+ conversationArchiveTimes(db, conversationIds, filter?.kind === "viewer" ? { userId: filter.userId } : filter?.kind === "actorEmail" ? { email: filter.email } : void 0),
8205
+ listConversationAnnotationsById(db, conversationIds),
8206
+ readConversationAuxiliaryCostsFromSql(db, conversationIds),
8207
+ readConversationTreeMetricsFromSql(db, conversationIds),
8208
+ readConversationParticipants(db, conversationIds),
8209
+ resolveSlackTeamDomains(conversations.flatMap((conversation) => conversation.location?.provider === "slack" ? [conversation.location.teamId] : [])),
8210
+ listConversationWork(conversationIds),
8211
+ readLastUserMessageAtByConversation(db, conversationIds),
8212
+ readConversationActivityPreviews(db, conversationIds),
8213
+ readAutomationTitlesByConversationId(conversations),
8214
+ readFirstHumanMessageTexts(db, conversations)
8215
+ ]);
8216
+ const visibleActivityPreviews = await attachActivityPreviewMentions(getSqlExecutor(), new Map([...activityPreviewByConversation].filter(([conversationId]) => accessByConversation.get(conversationId)?.canViewPrivateContent)));
8217
+ const sidebarAnnotationsByConversation = await listConversationSidebarAnnotations(conversationIds, new Map(conversationIds.map((conversationId) => [conversationId, accessByConversation.get(conversationId)?.canViewPrivateContent ? annotationsByConversation.get(conversationId) ?? [] : []])));
8218
+ const assignedWork = new Set(conversationWork.assignedIds);
8219
+ const unfinishedWork = new Set(conversationWork.unfinishedIds);
7478
8220
  return {
7479
- conversationId: input.conversationId,
7480
- messageId,
7481
- status
8221
+ conversations: conversations.map((conversation, index) => {
8222
+ const row = rows[index];
8223
+ const access = accessByConversation.get(conversation.conversationId);
8224
+ const metrics = metricsByRoot.get(conversation.conversationId);
8225
+ const auxiliaryCosts = auxiliaryCostsByRoot.get(conversation.conversationId);
8226
+ const childAgentTotals = childAgentTotalsReport(metrics, auxiliaryCosts);
8227
+ const archivedAtMs = archiveTimesByConversation.get(conversation.conversationId);
8228
+ const summary = conversationSummaryFromStoredConversation({
8229
+ ...archivedAtMs === void 0 ? void 0 : { archivedAtMs },
8230
+ automationTitle: automationTitleByConversation.get(conversation.conversationId),
8231
+ conversation,
8232
+ access,
8233
+ auxiliaryCosts: auxiliaryCosts?.total,
8234
+ ...childAgentTotals ? { childAgentTotals } : void 0,
8235
+ durationMs: metrics?.durationMs ?? row.conversation.durationMs,
8236
+ firstHumanMessageText: firstHumanMessageTextByConversation.get(conversation.conversationId),
8237
+ teamDomainByTeamId,
8238
+ ...row.location?.visibility === "public" ? { locationId: row.location.id } : void 0,
8239
+ usage: metrics?.usage ?? row.conversation.usage ?? void 0
8240
+ });
8241
+ const work = {
8242
+ ...assignedWork.has(conversation.conversationId) ? { assignedWork: true } : void 0,
8243
+ ...conversationWork.finishedAtById[conversation.conversationId] ? { finishedWorkAt: conversationWork.finishedAtById[conversation.conversationId] } : void 0,
8244
+ ...unfinishedWork.has(conversation.conversationId) ? { unfinishedWork: true } : void 0
8245
+ };
8246
+ const annotations = access?.canViewPrivateContent ? annotationsByConversation.get(conversation.conversationId) ?? [] : [];
8247
+ const activityPreview = visibleActivityPreviews.get(conversation.conversationId);
8248
+ const participants = access?.canViewPrivateContent ? participantsByConversation.get(conversation.conversationId) : void 0;
8249
+ return {
8250
+ ...summary,
8251
+ ...work,
8252
+ ...participants && participants.length > 0 ? { participants } : void 0,
8253
+ ...activityPreview ? { activityPreview } : void 0,
8254
+ ...annotations.length > 0 ? { annotations } : void 0,
8255
+ ...sidebarAnnotationsByConversation[conversation.conversationId] ? { sidebarAnnotations: sidebarAnnotationsByConversation[conversation.conversationId] } : void 0,
8256
+ isPriority: isConversationPriority({
8257
+ lastSeenAt: summary.lastSeenAt,
8258
+ lastUserMessageAt: lastUserMessageAtByConversation.get(conversation.conversationId),
8259
+ ...work
8260
+ }, nowMs)
8261
+ };
8262
+ }),
8263
+ generatedAt: new Date(nowMs).toISOString(),
8264
+ source: "conversation_index"
7482
8265
  };
7483
8266
  }
7484
- export { THREAD_OPTOUT_ACK as A, readHealthReport as B, buildSlackInboundMessage as C, normalizeIncomingSlackThreadId as D, stopSlackThread as E, renderSlackObjectCard as F, getAttachmentStorage as I, AGENT_DISPATCH_MAX_AGE_MS as L, parseContent as M, runWithSlackInstallation as N, withNormalizedThreadId as O, verifySlackSignature as P, enqueueAgentDispatch as R, recordSkippedConversationMessage as S, isSlackMessageStopped as T, readViewerAutomationRuns as _, recordWebConversationActivity as a, readViewerAutomations as b, createProductionSlackWebhookServices as c, resolveSlackConversationContextFromThreadId as d, createSlackWebApiStatusSender as f, readViewerAutomationExecutions as g, readConversationSourceTask as h, createConversationId as i, startProcessingReaction as j, stopConversationTurn as k, getProductionSlackAdapter as l, deleteViewerTask as m, conversationTurnIdForMessage as n, webActorFromEmail as o, ViewerTaskNotFoundError as p, createAndEnqueueConversation as r, createProductionConversationWorkOptions as s, appendAndEnqueueWebMessage as t, formatSlackConversationRedactedLabel as u, readViewerAutomationSummary as v, clearSlackPendingReactions as w, resolveViewerTaskCandidate as x, readViewerAutomationVersions as y, GET as z };
8267
+ /**
8268
+ * Load a bounded feed with an optional viewer-primary linked-user filter.
8269
+ *
8270
+ * With `viewer` and without `codeRepositoryId`, the feed holds only root
8271
+ * Conversations that the viewer started or takes part in, for every `q` and
8272
+ * `status`. The `actorEmail` filter and `codeRepositoryId` give no such
8273
+ * guarantee, so they are not an authorization boundary.
8274
+ */
8275
+ async function readConversationFeed(options = {}) {
8276
+ return conversationFeedSchema.parse(await readConversationFeedFromSql(options));
8277
+ }
8278
+ export { AGENT_DISPATCH_MAX_AGE_MS as $, assertNoLegacyAllowedDomains as A, normalizeIncomingSlackThreadId as B, readConversationSourceTask as C, readViewerAutomationVersions as D, readViewerAutomationSummary as E, recordSkippedConversationMessage as F, parseContent as G, stopConversationTurn as H, buildSlackInboundMessage as I, canActOnReviewItem as J, ReviewItemActionError as K, clearSlackPendingReactions as L, isAllowedSignInEmail as M, readEnvList as N, readViewerAutomations as O, resolveAllowedDomains as P, getAttachmentStorage as Q, isSlackMessageStopped as R, deleteViewerTask as S, readViewerAutomationRuns as T, THREAD_OPTOUT_ACK as U, withNormalizedThreadId as V, startProcessingReaction as W, readReviewItemForUser as X, isOnlyAssignee as Y, reviewItemChange as Z, createProductionConversationWorkOptions as _, resolveMentions as a, createSlackWebApiStatusSender as b, readConversationAuxiliaryCostsFromSql as c, notCopiedByFork as d, enqueueAgentDispatch as et, childAgentTotalsReport as f, resolveSlackTeamDomains as g, conversationSummaryFromStoredConversation as h, attachConversationMentions as i, isAllowedDomain as j, resolveViewerTaskCandidate as k, ConversationForkError as l, conversationEventHistory as m, readConversationRecordFromSql as n, readHealthReport as nt, readFirstHumanMessageTexts as o, readConversationTreeMetricsFromSql as p, actOnReviewItem as q, readConversationParticipants as r, listConversationSidebarAnnotations as s, readConversationFeed as t, GET as tt, forkConversation as u, createProductionSlackWebhookServices as v, readViewerAutomationExecutions as w, ViewerTaskNotFoundError as x, getProductionSlackAdapter as y, stopSlackThread as z };