@sentry/junior 0.266.0 → 0.267.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 (75) hide show
  1. package/dist/{actor-tlxzqaRe.js → actor-DZSokpwj.js} +8 -1
  2. package/dist/{adapter-eRFA_naf.js → adapter-BybMEezj.js} +2 -2
  3. package/dist/{admin-tILpP51p.js → admin-DOs1_EiW.js} +1 -1
  4. package/dist/{agent-runner-BtYK7bht.js → agent-runner-DgNATN4D.js} +872 -226
  5. package/dist/api/schema.d.ts +198 -23
  6. package/dist/api/schema.js +5 -4
  7. package/dist/api.d.ts +6 -1
  8. package/dist/api.js +130 -1205
  9. package/dist/{app-Csbi9dW7.d.ts → app-DAB3fzyp.d.ts} +21 -3
  10. package/dist/app.d.ts +2 -2
  11. package/dist/app.js +1610 -353
  12. package/dist/{assistant-reply-UyzAfZQo.js → assistant-reply-CT7VcXQe.js} +1 -1
  13. package/dist/{automation-BKpY9LPa.js → automation-CmeFBSEh.js} +42 -14
  14. package/dist/{briefs-DgfWMU7e.js → briefs-CWKjka7v.js} +4 -4
  15. package/dist/{catalog-runtime-DAJjAbSx.js → catalog-runtime-cMlAI5LY.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-BjgbMawj.js} +2 -2
  25. package/dist/{config-C0UwYqDK.js → config-CdB6yJj5.js} +32 -10
  26. package/dist/{connections-Bwu3VORi.js → connections-RE_65jzK.js} +13 -12
  27. package/dist/{conversation-DF4rUcjb.js → conversation-DSo310Ps.js} +8 -1
  28. package/dist/{conversation-privacy-CY7JVjxn.js → conversation-privacy-BzgJC5k0.js} +1 -1
  29. package/dist/{credential-sync-VGAMUTGp.js → credential-sync-DhEGEpgq.js} +15 -15
  30. package/dist/{credentialed-enF5Bf3y.js → credentialed-CIGwoLU2.js} +2905 -1024
  31. package/dist/{db-BnRqQ7G5.d.ts → db-Bg-zC3r5.d.ts} +1 -1
  32. package/dist/{executor-TnWbvL09.js → executor-VYrx_K4w.js} +7 -4
  33. package/dist/{images-BKQqSi5W.js → images-CecjXAAz.js} +3 -3
  34. package/dist/instrumentation.js +6 -2
  35. package/dist/{job-queue-DBpHmKnd.js → job-queue-C0BZSa7r.js} +128 -3
  36. package/dist/{web-input-B-bMjD1V.js → list-1XQP-o6K.js} +1374 -71
  37. package/dist/{logging-Ce0mXZ8J.js → logging-DuZ1A4ab.js} +11 -3
  38. package/dist/{message-VZ3DFpGb.js → message-NKSbbnA6.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-qAHLPtml.js} +2 -2
  42. package/dist/{prompt-BcCGlmg2.js → prompt-BW9jFsqZ.js} +1 -1
  43. package/dist/{registry-1BEIPdbT.js → registry-FQutO926.js} +2 -2
  44. package/dist/{resolve-BSHHkEGE.js → resolve-Bk4yeuHy.js} +3 -3
  45. package/dist/{result-CwOpzqdh.js → result-B9e9ATOU.js} +9 -9
  46. package/dist/{runner-CEaX04kz.js → runner-C3S2ObH-.js} +11 -11
  47. package/dist/{sandbox-egress-signals-BwtGHwqa.js → sandbox-egress-signals-DqpGzzMB.js} +1 -1
  48. package/dist/{schema-RsaBHxRH.js → schema-X7Fj4fv1.js} +3 -2
  49. package/dist/{service-account-Dg4lIbHm.js → service-account-BQasl-qq.js} +3 -3
  50. package/dist/{service-account-6wC76eBZ.js → service-account-Do30HUNv.js} +3 -3
  51. package/dist/{session-stop-tZOw9AJD.js → session-stop-DkOjt46_.js} +45 -128
  52. package/dist/{skills-DzyGN7wd.js → skills-CQHrwfOa.js} +2 -2
  53. package/dist/{spawn-6rokq7l_.js → spawn-BHuqN14f.js} +2 -2
  54. package/dist/{store-DcKkzrdt.js → store-6EetVP-m.js} +3 -3
  55. package/dist/{store-CG0VJyye.d.ts → store-B8CABbmR.d.ts} +10 -2
  56. package/dist/store-DvNZIsCr.js +295 -0
  57. package/dist/{task-queue-Bg4RS2Gy.js → task-queue-B0DLgd2P.js} +10 -5
  58. package/dist/{task-runner-8N5LIkSg.js → task-runner-B-PD_JOj.js} +25 -13
  59. package/dist/{tasks-DDT-J3Rf.js → tasks-0ilmuCux.js} +1438 -44
  60. package/dist/{thread-state-cprEaxgl.js → thread-state-DRmdJ2QN.js} +5 -5
  61. package/dist/{turn-failure-response-C8sMdsdX.js → turn-failure-response-G11cs2gE.js} +9 -9
  62. package/dist/{turn-lifecycle-DaBVoz7h.js → turn-lifecycle-OzLIwvi1.js} +1 -1
  63. package/dist/{turn-session-routing-a9CHX-w6.js → turn-session-routing-Dyut9l7Y.js} +1 -1
  64. package/dist/user-input-request-BDFBdF8r.js +116 -0
  65. package/dist/{validation-C2lllinq.js → validation-CxEmt1id.js} +1 -1
  66. package/dist/{version-oaLfxxnf.js → version-Bapedn4A.js} +1 -1
  67. package/dist/version.js +1 -1
  68. package/dist/{viewer-BpE6Z3qf.js → viewer-DrN3oC2X.js} +21 -10
  69. package/dist/{work-VX1SHWha.js → work-DUQoNcqP.js} +10 -10
  70. package/dist/{worker-hook-y_Y.js → worker-D1l4Ugk0.js} +30 -19
  71. package/migrations/0064_any_resource_event_triggers.sql +2 -0
  72. package/migrations/meta/0064_snapshot.json +3505 -0
  73. package/migrations/meta/_journal.json +7 -0
  74. package/package.json +20 -5
  75. package/dist/store-FALy-JQb.js +0 -1424
@@ -1,41 +1,43 @@
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 { $n as buildDeterministicTurnId, At as PluginCredentialFailureError, Bt as wasPendingAuthStopped, Ct as isHumanConversationMessage, Dt as turnHasReply, E as sameDefinitionValue, Et as recordDeliveredAssistantMessage, G as commitMessages, Gt as completeTurnRecord, Ht as loadTurnCheckpoint, Jn as trimTrailingAssistantMessages, Jt as listTurnSummaries, Kt as failTurnRecord, Ot as upsertConversationMessage, Pn as persistWithRetry, Pt as AuthorizationFlowDisabledError, Qn as buildDeterministicAssistantMessageId, Rt as clearPendingAuth, S as automationDefinition, St as getConversationMessageSlackTs, Tt as normalizeConversationText, Ut as saveTurnCheckpoint, W as commitAcceptedReply, Wt as abandonTurnRecord, X as loadTurnProjection, Y as loadProjection, Yt as recordTurnSummary, _ as parseEventAutomationRow, bt as createConversationMemoryService, c as withScheduledAutomationLock, cr as isExperimentalFeatureEnabled, dr as configValueSchema, dt as persistConversationMessages, f as deleteEventAutomation, gt as fallbackShortTitle, h as listDeletedEventAutomationsCreatedBy, i as readScheduledAutomation, j as automationEventCatalog, jn as buildTurnErrorResponse, mt as resolveConversationTitle, nr as getSubscribedReplyPreflightDecision, nt as recordAuthorizationCompleted, o as saveScheduledAutomationInLock, ot as recordSubscribedReplyRoute, pr as piMessageFromHistoryItem, pt as persistConversationMessageSummaries, q as loadConversationProjection, qt as getTurnRecord, r as parseScheduledAutomationRow, rr as getThreadStopDecision, tr as decideSubscribedThreadReply, ut as hydrateConversationMessages, vt as appendThreadContextMessages, w as listAutomationVersions, wt as markConversationMessage, xt as estimateConversationContextTokens, yt as buildConversationContext, z as registeredEventTriggerProblems } from "./tasks-0ilmuCux.js";
2
+ import { a as completeText, p as escapeXml, r as completeDecision, y as isProviderRetryError } from "./client-BjgbMawj.js";
3
+ import { c as getSlackClientSecret, i as getChatConfig, l as getSlackSigningSecret, n as botConfig, o as getSlackBotToken, s as getSlackClientId, v as normalizeSlackEmojiName, y as createS3AttachmentStorage } from "./config-CdB6yJj5.js";
4
+ import { B as isRecord, E as withSpan, H as toOptionalString, S as setTags, T as withLogContext, V as toOptionalNumber, b as setSpanAttributes, f as logError, h as logWarn, m as logInfo, n as createChatSdkLogger, o as getActiveTraceContext, p as logException, s as getActiveTraceId, t as buildTurnFailureResponse, y as setSentryUser } from "./logging-DuZ1A4ab.js";
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";
7
+ import { a as isUserActor, d as parseSlackTeamId, i as isActorUserId, l as parseSlackChannelId, n as createSlackActor, o as parseActorUserId, p as parseSlackMessageTs, r as createSlackResumeActor, t as createActor } from "./actor-DZSokpwj.js";
8
+ import { T as addAgentTurnUsage, V as requireSlackDestination, _ as loadConversationSlackTenantIds, b as locationFromRow, f as getConversationStore, g as withConversationEventLock, k as hasAgentTurnUsage, m as getSqlExecutor, p as getDb, u as getConversationEventStore, w as parseSessionSource, z as createSlackDestination } from "./viewer-DrN3oC2X.js";
9
+ import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, o as conversationReadColumns, s as juniorConversations } from "./agent-invocations-CxKldAeB.js";
10
+ import { a as juniorAutomationRuns, f as juniorConversationBriefs, h as juniorConversationEvents, s as juniorAutomations, v as juniorCodeChanges } from "./executor-VYrx_K4w.js";
11
+ import { d as contextProvenance, p as instructionProvenanceFor } from "./history-C84mlAwO.js";
12
+ import { d as actorLogContext, f as credentialContextForActor } from "./registry-FQutO926.js";
13
+ import { B as resolveSlackChannelIdFromThreadId, H as listThreadReplies, J as describeEventAutomationTrigger, L as parseSlackThreadId, R as readSlackRawMessageContext, V as getConversationInfo, Y as effectiveTaskOutcomes, at as conversationSubtreeColumns, b as renderBlockText, ct as buildSteeringPiMessage, i as buildAuthPauseResponse, it as conversationSubtree, lt as withoutSavedSteeringMessages, mt as shouldEmitDevAgentTrace, n as executeAgentRun, p as latestProgressStatus, r as createContextCompactor, t as createAgentRunner, ut as RetryableDeliveryError, w as storeAttachment, y as renderAttachmentText, z as resolveSlackChannelIdFromMessage } from "./agent-runner-DgNATN4D.js";
14
+ import { $ as postSlackMessage, $t as downloadPrivateSlackFile, Bn as conversationNotArchivedForEmail, Fn as conversationArchiveTimes, Ft as cancelSubscriptions, G as normalizeSlackReplyMarkdown, Hn as viewerConversationMembership, In as conversationArchivedForEmail, J as truncateStatusText, K as normalizeSlackStatusText, Kn as buildSentryConversationUrl, Ln as conversationArchivedForUser, Nn as createPluginLogger, Pn as readConversationAccessFromSql, Qt as SlackActionError, Rn as conversationHasParticipantUser, U as escapeSlackMrkdwnText, Vn as conversationNotArchivedForUser, W as formatSlackLink, Xn as recordSlackAttachment, Y as addReactionToMessage, Yn as readAttachmentVisionSummary, Zt as verifyTimerWatchCredentialSubject, an as normalizeSlackConversationId, ar as createPluginCache, dn as JUNIOR_THREAD_STATE_TTL_MS, et as removeReactionFromMessage, fn as getDashboardConversationLink, in as isDmChannel, ir as lookupSlackUser, l as getPluginSlackConversationLink, n as createUserTokenStore, nn as getSlackClient, on as runWithSlackInstallationToken, p as getPlugins, pn as getDashboardObjectIconLink, qn as getSlackFileId, sn as withSlackRetries, tn as getHeaderString, tr as listConversationAnnotationsById, zn as conversationIsMemberRoot } from "./credentialed-CIGwoLU2.js";
15
+ import { a as MUTATION_LOCK_TTL_MS, c as withLock, i as getStateAdapter, o as acquireActiveLock } from "./adapter-BybMEezj.js";
16
+ import { W as readMessageAttachments, g as conversationFeedSchema, q as isVisionImageMediaType } from "./conversation-DSo310Ps.js";
17
+ import { a as EVENT_SYSTEM_ACTOR, o as isEventConversationMessage } from "./prompt-BW9jFsqZ.js";
18
+ import { B as ConversationTurnBoundaryError, G as isCooperativeTurnYieldError, H as TurnInputCommitLostError, J as markTurnClosed, K as isTurnInputCommitLostError, U as TurnInputDeferredError, V as CooperativeTurnYieldError, W as getConversationTurnBoundaryError, X as markTurnFailed, Y as markTurnCompleted, Z as startActiveTurn, q as isTurnInputDeferredError } from "./store-6EetVP-m.js";
19
+ import { n as getAssistantReplyText } from "./assistant-reply-CT7VcXQe.js";
20
+ import { C as maybePostAutomatedTurnLimitNotice, M as runDispatchOutcome, O as deliversFinalText, c as isEventMailboxMetadata, d as getEventCatalog, i as skipInputBeforeConversationStop, k as finishedRunReply, l as renderEventBatchText, s as eventBatchMessageEvents, u as timerWatchCredentialSubject, w as recordFinishedTurnForAutomatedLimit, x as joinMailboxText } from "./session-stop-DkOjt46_.js";
21
+ import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline } from "./job-queue-C0BZSa7r.js";
22
+ import { D as stopConversationWork, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, n as appendAndEnqueueExclusiveInboundMessage, r as appendAndEnqueueInboundMessage, w as requestConversationWork } from "./store-DvNZIsCr.js";
23
+ import { j as eventAutomationTriggerSchema, l as automationListQuerySchema } from "./automation-CmeFBSEh.js";
23
24
  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";
25
+ import { A as readAutomationExecutions, D as readAutomationExecutionStatusDays, E as readAutomationExecutionHours, M as readAutomationTitlesByConversationId, O as readAutomationExecutionStatusHours, S as emptyAutomationRunWindows, T as readAutomationExecutionDays, _ as markDispatchBlocked, a as claimDispatchMailboxAppend, b as markDispatchRunning, c as getDispatchConversationId, d as getDispatchRecord, f as getDispatchTurnId, g as markDispatchAwaitingResume, j as readAutomationRuns, k as readAutomationExecutionSummaries, l as getDispatchIdForConversation, m as isTerminalDispatchStatus, n as scheduleSessionCompletedPluginTasks, o as confirmDispatchMailboxAppend, t as processPluginTask, u as getDispatchInputMessageId, v as markDispatchCompleted, w as readAutomationExecutionByConversationId, y as markDispatchFailed } from "./task-runner-B-PD_JOj.js";
26
+ import { a as withAgentInvocationResultActors, c as resolveMailboxTurnWork, d as legacyWebMailboxMetadataSchema, i as startAgentInvocationsForChildInput, l as sourceFromTurnInput, o as getAuthPausedConversationTurnId, s as getEventBatchDelayMs, t as enqueueAgentInvocationResult, u as turnInputFactsFromConversationMessage } from "./result-B9e9ATOU.js";
27
+ import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-OzLIwvi1.js";
28
+ import { i as resolveAgentInvocationId, o as AgentRunError, s as executeTurn, t as createAgentInvocationWorker } from "./work-DUQoNcqP.js";
29
+ import { a as persistThreadState, i as persistThreadRuntimeState, n as getPersistedSandboxState, o as persistThreadStateById, r as getPersistedThreadState, s as coerceThreadConversationState, t as getLocationConfigurationService } from "./thread-state-DRmdJ2QN.js";
30
+ import { c as deleteWebAuthorization, d as createPausedTurns, f as getPausedTurnRequest, h as getTurnAuthorization, m as wakePausedTurn, s as createWebAuthorization } from "./connections-RE_65jzK.js";
31
+ import { n as resolveTurnSessionRouting } from "./turn-session-routing-Dyut9l7Y.js";
32
+ import { t as bindMessageAgent } from "./message-NKSbbnA6.js";
33
+ import { t as bindSpawnAgent } from "./spawn-BHuqN14f.js";
34
+ import { a as persistAuthPauseTurnState, c as getTurnUserSlackMessageTs, d as loadPendingMessageCards, i as completeAuthPauseTurn, l as buildDeliveredTurnStatePatch, n as getAgentTurnDiagnosticsAttributes, o as getTurnUserMessage, r as requireTurnFailureEventId, s as getTurnUserReplyAttachmentContext, t as finalizeFailedTurnReplyWithEvent, u as getInterruptionMarker } from "./turn-failure-response-G11cs2gE.js";
35
+ import { r as storeInputImages } from "./images-CecjXAAz.js";
35
36
  import { z } from "zod";
36
37
  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";
38
+ import { and, asc, count, desc, eq, exists, inArray, isNotNull, isNull, ne, notInArray, or, sql } from "drizzle-orm";
39
+ import { conversationSidebarAnnotationSchema, createSlackSource, createWebSource, localDestinationSchema, objectFactFields, objectPresentation, objectTypeSchema } from "@sentry/junior-plugin-api";
40
+ import { alias } from "drizzle-orm/pg-core";
39
41
  import { del, get, put } from "@vercel/blob";
40
42
  import { Message, ThreadImpl, stringifyMarkdown } from "chat";
41
43
  import { SlackFormatConverter, createSlackAdapter } from "@chat-adapter/slack";
@@ -107,8 +109,7 @@ async function enqueueAgentDispatch(dispatch, options) {
107
109
  destination: claimedDispatch.destination,
108
110
  nowMs,
109
111
  source: "plugin",
110
- sessionSource: claimedDispatch.source,
111
- visibility: claimedDispatch.destinationVisibility
112
+ sessionSource: claimedDispatch.source
112
113
  });
113
114
  await appendAndEnqueueInboundMessage({
114
115
  message: buildAgentDispatchInboundMessage(claimedDispatch, nowMs),
@@ -213,7 +214,6 @@ async function persistBlockedDispatchTurn(dispatch, error) {
213
214
  actor: dispatch.actor,
214
215
  conversationId,
215
216
  destination: dispatch.destination,
216
- destinationVisibility: dispatch.destinationVisibility,
217
217
  dispatchId: dispatch.id,
218
218
  dispatchOutcome: "blocked",
219
219
  turnId: sessionId,
@@ -850,6 +850,36 @@ async function startProcessingReactionForMessage(args) {
850
850
  };
851
851
  }
852
852
  // src/chat/providers/slack/turn-input-telemetry.ts
853
+ const SENTRY_TRACE_PATTERN = /^([0-9a-f]{32})-([0-9a-f]{16})(?:-([01]))?$/;
854
+ function spanLinks({ sentryTrace }) {
855
+ const [, traceId, spanId, sampled] = SENTRY_TRACE_PATTERN.exec(sentryTrace) ?? [];
856
+ return traceId && spanId ? [{ context: {
857
+ traceId,
858
+ spanId,
859
+ traceFlags: sampled === "1" ? 1 : 0
860
+ } }] : [];
861
+ }
862
+ /**
863
+ * Run a `chat.turn` span in the trace of the first trace context.
864
+ *
865
+ * The span links to the other trace contexts and to the span that was active,
866
+ * such as the worker span. Without a trace context, the span is a child of the
867
+ * active span. A malformed trace context starts a new trace.
868
+ */
869
+ async function withTurnSpan(context, input, callback) {
870
+ const [parent, ...others] = input?.traceContexts ?? [];
871
+ if (!parent) return await withSpan("chat.turn", "chat.turn", context, callback);
872
+ const activeSpan = getActiveSpan();
873
+ const links = [...others.flatMap(spanLinks), ...activeSpan ? [{ context: activeSpan.spanContext() }] : []];
874
+ const { baggage, sentryTrace } = parent;
875
+ return await continueTrace({
876
+ baggage,
877
+ sentryTrace
878
+ }, () => withSpan("chat.turn", "chat.turn", context, async (setSpanAttributes) => {
879
+ getActiveSpan()?.addLinks(links);
880
+ return await callback(setSpanAttributes);
881
+ }));
882
+ }
853
883
  /**
854
884
  * Build the `chat.turn` timing attributes for the oldest input message.
855
885
  *
@@ -1233,7 +1263,7 @@ function createSlackTurnRuntime(deps) {
1233
1263
  }
1234
1264
  processingReaction = await processingReactions.start(context, message);
1235
1265
  const toolInvocationHook = createToolInvocationHook(processingReaction, hooks);
1236
- await withSpan("chat.turn", "chat.turn", context, async (set) => {
1266
+ await withTurnSpan(context, hooks.telemetry, async (set) => {
1237
1267
  const turnStartedAtMs = deps.now();
1238
1268
  if (hooks.telemetry) set(getTurnInputSpanAttributes(hooks.telemetry, turnStartedAtMs));
1239
1269
  const queuedMessages = getQueuedMessages(hooks.messageContext, {
@@ -1375,7 +1405,7 @@ function createSlackTurnRuntime(deps) {
1375
1405
  channelId,
1376
1406
  runId
1377
1407
  });
1378
- await withSpan("chat.turn", "chat.turn", turnContext, async (set) => {
1408
+ await withTurnSpan(turnContext, hooks.telemetry, async (set) => {
1379
1409
  const turnStartedAtMs = deps.now();
1380
1410
  if (hooks.telemetry) set(getTurnInputSpanAttributes(hooks.telemetry, turnStartedAtMs));
1381
1411
  const content = parseContent(message);
@@ -2166,7 +2196,8 @@ function createSlackConversationWorker(options) {
2166
2196
  const telemetry = {
2167
2197
  count: records.length,
2168
2198
  oldestCreatedAtMs: oldestRecord.createdAtMs,
2169
- oldestReceivedAtMs: oldestRecord.receivedAtMs
2199
+ oldestReceivedAtMs: oldestRecord.receivedAtMs,
2200
+ traceContexts: records.flatMap((record) => record.traceContext ?? [])
2170
2201
  };
2171
2202
  const latestMetadata = parseSlackMetadata(latestRecord.input.metadata);
2172
2203
  if (!latestMetadata) throw new Error("Latest conversation mailbox record is not Slack metadata");
@@ -2213,7 +2244,7 @@ function createSlackConversationWorker(options) {
2213
2244
  };
2214
2245
  const drainSteeringMessages = async (accept) => {
2215
2246
  await context.attempt.drain(async (pendingRecords) => {
2216
- const candidates = pendingRecords.map((record) => ({
2247
+ const candidates = pendingRecords.filter((record) => record.source === "slack").map((record) => ({
2217
2248
  inboundMessageId: record.inboundMessageId,
2218
2249
  message: restoreMessage({
2219
2250
  adapter,
@@ -2263,7 +2294,7 @@ function createSlackConversationWorker(options) {
2263
2294
  return { status: "completed" };
2264
2295
  };
2265
2296
  }
2266
- /** Serialize a Slack message into the generic durable conversation mailbox. */
2297
+ /** Serialize a Slack message and the active trace into the durable mailbox. */
2267
2298
  function buildSlackInboundMessage(args) {
2268
2299
  const authorId = requireSlackAuthorId(args.message);
2269
2300
  const destination = createSlackDestination({
@@ -2296,7 +2327,8 @@ function buildSlackInboundMessage(args) {
2296
2327
  thread: serializedThreadSchema.parse(args.thread.toJSON()),
2297
2328
  message: args.message.toJSON()
2298
2329
  }
2299
- }
2330
+ },
2331
+ traceContext: getActiveTraceContext()
2300
2332
  };
2301
2333
  }
2302
2334
  // src/chat/slack/message/identity.ts
@@ -2784,6 +2816,57 @@ function recordSkippedConversationMessage(args) {
2784
2816
  }
2785
2817
  });
2786
2818
  }
2819
+ // src/chat/sign-in/allowlist.ts
2820
+ /**
2821
+ * Domains that may use Junior. Slack authors must have a Slack profile email
2822
+ * on one of them, and dashboard Google sign-ins must come from a Google
2823
+ * Workspace account whose `hd` claim is one of them. Slack and the dashboard
2824
+ * share one list.
2825
+ */
2826
+ /** Read a list env var: a JSON string array or comma-separated values. */
2827
+ function readEnvList(name) {
2828
+ const value = process.env[name]?.trim();
2829
+ if (!value) return void 0;
2830
+ if (!value.startsWith("[")) return value.split(",");
2831
+ let parsed;
2832
+ try {
2833
+ parsed = JSON.parse(value);
2834
+ } catch (error) {
2835
+ throw new Error(`${name} must be a JSON string array`, { cause: error });
2836
+ }
2837
+ const list = z.array(z.string()).safeParse(parsed);
2838
+ if (!list.success) throw new Error(`${name} must be a JSON string array`);
2839
+ return list.data;
2840
+ }
2841
+ /**
2842
+ * Resolve the allowed domains: the `createApp` option when set, else
2843
+ * `JUNIOR_ALLOWED_DOMAINS`. An empty list allows every domain.
2844
+ */
2845
+ function resolveAllowedDomains(allowedDomains) {
2846
+ return [...new Set((allowedDomains ?? readEnvList("JUNIOR_ALLOWED_DOMAINS") ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean))];
2847
+ }
2848
+ /**
2849
+ * Fail startup on the removed Google-specific names. Ignoring them would
2850
+ * leave the list empty, which lets every Slack workspace member in.
2851
+ */
2852
+ function assertNoLegacyAllowedDomains(dashboardOptionKeys) {
2853
+ if (dashboardOptionKeys.includes("allowedGoogleDomains")) throw new Error("dashboard.allowedGoogleDomains was removed; set createApp({ allowedDomains }) instead");
2854
+ if (process.env.JUNIOR_DASHBOARD_GOOGLE_DOMAINS?.trim()) throw new Error("JUNIOR_DASHBOARD_GOOGLE_DOMAINS was removed; set JUNIOR_ALLOWED_DOMAINS instead");
2855
+ }
2856
+ /** Return whether a verified domain may use Junior. An empty list allows all. */
2857
+ function isAllowedDomain(domain, allowedDomains) {
2858
+ if (allowedDomains.length === 0) return true;
2859
+ return Boolean(domain && allowedDomains.includes(domain.toLowerCase()));
2860
+ }
2861
+ /**
2862
+ * Return whether a verified sign-in email is on an allowed domain. Sign-in
2863
+ * needs a configured list, so an empty list allows no email.
2864
+ */
2865
+ function isAllowedSignInEmail(email, allowedDomains) {
2866
+ const normalized = email.trim().toLowerCase();
2867
+ const separator = normalized.lastIndexOf("@");
2868
+ return allowedDomains.length > 0 && separator > 0 && isAllowedDomain(normalized.slice(separator + 1), allowedDomains);
2869
+ }
2787
2870
  // src/chat/automations/collection.ts
2788
2871
  /**
2789
2872
  * The web collection applies access before counts, filter options, and paging.
@@ -2799,18 +2882,6 @@ function viewerAutomationCollection(user) {
2799
2882
  const owned = inArray(automation.creatorIdentityId, user.identities.map((identity) => identity.id));
2800
2883
  const publicDestination = sql`coalesce(${destination.visibility} = 'public', false)`;
2801
2884
  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
2885
  const runs = juniorAutomationRuns;
2815
2886
  const lastRunNeedsAttention = sql`coalesce((select ${runs.status} in ('failed', 'blocked') from ${runs}
2816
2887
  where ${runs.automationId} = ${automation.id} and ${runs.status} in ('blocked', 'completed', 'failed') and ${runs.completedAtMs} is not null
@@ -2821,8 +2892,8 @@ function viewerAutomationCollection(user) {
2821
2892
  title: automation.title,
2822
2893
  createdAtMs: automation.createdAtMs,
2823
2894
  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"),
2895
+ lastRunNeedsAttention: lastRunNeedsAttention.as("last_run_needs_attention"),
2896
+ trigger: sql`${automation.record}->'trigger'`.as("trigger"),
2826
2897
  owned: sql`${owned}`.as("owned"),
2827
2898
  resource: sql`case when ${isScheduled} then '' else concat_ws(' ', ${automation.identifier}, ${automation.record}->'trigger'->>'label', ${automation.namespace}) end`.as("resource"),
2828
2899
  creator: sql`${automation.teamId} || ':' || ${automation.creatorSlackUserId}`.as("creator"),
@@ -2833,11 +2904,29 @@ function viewerAutomationCollection(user) {
2833
2904
  instruction: automation.instruction
2834
2905
  }).from(automation).leftJoin(destination, and(eq(destination.provider, "slack"), eq(destination.providerTenantId, automation.teamId), eq(destination.providerDestinationId, automation.channelId))).where(and(ne(automation.status, "deleted"), or(owned, publicToViewer))).as("accessible_automations");
2835
2906
  }
2907
+ /**
2908
+ * List the accessible event automations whose trigger cannot work with the
2909
+ * enabled plugins. `eventTriggerProblems` decides, the same check that the
2910
+ * tools and the dashboard use, so SQL does not keep its own copy of the rules.
2911
+ */
2912
+ async function unavailableEventAutomationIds(collection) {
2913
+ const catalog = automationEventCatalog(getEventCatalog());
2914
+ return (await getDb().select({
2915
+ id: collection.id,
2916
+ trigger: collection.trigger
2917
+ }).from(collection).where(eq(collection.kind, "event"))).flatMap((row) => {
2918
+ const trigger = eventAutomationTriggerSchema.safeParse(row.trigger);
2919
+ return trigger.success && registeredEventTriggerProblems(catalog, trigger.data).length === 0 ? [] : [row.id];
2920
+ });
2921
+ }
2836
2922
  /** Read an SQL page and complete, access-scoped counts and filter options. */
2837
2923
  async function readAutomationCollection(user, input) {
2838
2924
  const db = getDb();
2839
2925
  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);
2926
+ const unavailableIds = await unavailableEventAutomationIds(collection);
2927
+ const unavailable = unavailableIds.length > 0 ? inArray(collection.id, unavailableIds) : sql`false`;
2928
+ const attention = and(notInArray(collection.state, ["paused", "completed"]), or(eq(collection.state, "blocked"), unavailable, eq(collection.lastRunNeedsAttention, true)));
2929
+ const where = and(input.scope === "mine" ? eq(collection.owned, true) : input.scope === "public" ? eq(collection.isPublic, true) : input.scope === "attention" ? attention : void 0, input.type === "all" ? void 0 : eq(collection.kind, input.type), input.state === "all" ? void 0 : input.state === "unavailable" ? unavailable : eq(collection.state, input.state), input.creator ? eq(collection.creator, input.creator) : void 0, input.destination ? eq(collection.destination, input.destination) : void 0, input.q ? sql`strpos(lower(concat_ws(' ', ${collection.title}, ${collection.instruction}, ${collection.resource})), ${input.q.toLowerCase()}) > 0` : void 0);
2841
2930
  const [totals, counts, creators, destinations] = await Promise.all([
2842
2931
  db.select({ total: count() }).from(collection).where(where),
2843
2932
  db.select({
@@ -2966,6 +3055,7 @@ function executionSummaryFields(stats) {
2966
3055
  lastRunStatus: stats?.lastRunStatus,
2967
3056
  ...stats?.lastConversationId ? { lastConversationId: stats.lastConversationId } : void 0,
2968
3057
  ...stats?.lastExecutedAtMs ? { lastRunAt: new Date(stats.lastExecutedAtMs).toISOString() } : void 0,
3058
+ costUsd: stats?.costUsd ?? emptyAutomationRunWindows(),
2969
3059
  runs: stats?.runs ?? emptyAutomationRunWindows(),
2970
3060
  totalRuns: stats?.totalRuns ?? 0
2971
3061
  };
@@ -3019,7 +3109,7 @@ function eventAutomationSummary(task, ownedByViewer, destination, stats, creator
3019
3109
  kind: "event",
3020
3110
  ...executionSummaryFields(stats),
3021
3111
  ownedByViewer,
3022
- resource: `${task.trigger.label} · ${task.trigger.identifier}`,
3112
+ resource: task.trigger.identifier ? `${task.trigger.label} · ${task.trigger.identifier}` : task.trigger.label,
3023
3113
  source: task.trigger.namespace,
3024
3114
  credentialMode: task.credentialMode,
3025
3115
  outcomes: effectiveTaskOutcomes(task.outcomes, task.destination),
@@ -4327,10 +4417,7 @@ function createSlackTurn(deps) {
4327
4417
  channelName,
4328
4418
  channelType: slackChannelType
4329
4419
  });
4330
- const destinationVisibility = await resolveDestinationVisibility({
4331
- destination,
4332
- visibility: options.execution?.destinationVisibility ?? conversationVisibilityFromSlackChannelType(slackChannelType)
4333
- });
4420
+ const destinationVisibility = conversationVisibilityFromSlackChannelType(slackChannelType);
4334
4421
  const assistantThreadContext = getAssistantThreadContext(message);
4335
4422
  const threadTs = getThreadTs(threadId) ?? assistantThreadContext?.threadTs;
4336
4423
  const messageTs = getMessageTs(message);
@@ -4340,7 +4427,10 @@ function createSlackTurn(deps) {
4340
4427
  messageTs,
4341
4428
  teamId,
4342
4429
  threadTs,
4343
- visibility: destinationVisibility ?? "private"
4430
+ visibility: await resolveDestinationVisibility({
4431
+ destination,
4432
+ visibility: destinationVisibility
4433
+ }) ?? "private"
4344
4434
  });
4345
4435
  const slackActionToken = readSlackActionToken(message);
4346
4436
  const runId = options.execution?.dispatch?.id ?? getRunId(thread, message);
@@ -5448,7 +5538,6 @@ function createSlackDispatchTurnRunner(options) {
5448
5538
  disabledFeatures: ["interactive-auth"],
5449
5539
  locationConfiguration: options.getLocationConfiguration(dispatch.destination),
5450
5540
  credentialContext: routing.credentialContext,
5451
- destinationVisibility: dispatch.destinationVisibility,
5452
5541
  dispatch: routing.dispatch,
5453
5542
  skipProviderDefaultConfig: true,
5454
5543
  source: dispatch.source,
@@ -5767,7 +5856,7 @@ function buildResumedRun(args, statusSession, delivery) {
5767
5856
  * generation begins.
5768
5857
  */
5769
5858
  async function resumeSlackTurn(args) {
5770
- return withLogContext(getResumeLogContext(args), () => resumeSlackTurnInContext(args));
5859
+ return await withTurnSpan(getResumeLogContext(args), args.traceContext && { traceContexts: [args.traceContext] }, () => resumeSlackTurnInContext(args));
5771
5860
  }
5772
5861
  async function resumeSlackTurnInContext(args) {
5773
5862
  const status = createSlackWebApiAssistantStatusSession({
@@ -5813,7 +5902,6 @@ async function resumeSlackTurnInContext(args) {
5813
5902
  if (runArgs.initialText) await postSlackMessageBestEffort(runArgs.channelId, runArgs.threadTs, runArgs.initialText);
5814
5903
  status.update(runArgs.initialStatus);
5815
5904
  const conversationId = runArgs.conversationId;
5816
- const visibility = (await getConversationStore().get({ conversationId }))?.visibility;
5817
5905
  const visibleConversationId = args.conversationId;
5818
5906
  const sessionId = runArgs.turnId;
5819
5907
  let deliveryConversation;
@@ -5908,7 +5996,6 @@ async function resumeSlackTurnInContext(args) {
5908
5996
  await persistWithRetry(() => recordTurnSummary({
5909
5997
  conversationId: runArgs.conversationId,
5910
5998
  destination: savedRun.destination,
5911
- destinationVisibility: visibility,
5912
5999
  dispatchId,
5913
6000
  resultMessageId: messageTs,
5914
6001
  turnId: runArgs.turnId,
@@ -5954,7 +6041,6 @@ async function resumeSlackTurnInContext(args) {
5954
6041
  durationMs: reply.diagnostics.durationMs,
5955
6042
  usage: reply.diagnostics.usage,
5956
6043
  destination: run.destination,
5957
- destinationVisibility: visibility,
5958
6044
  dispatchId: run.dispatch?.id,
5959
6045
  dispatchOutcome: dispatchResult.outcome,
5960
6046
  ...dispatchErrorMessage ? { errorMessage: dispatchErrorMessage } : void 0,
@@ -5967,7 +6053,6 @@ async function resumeSlackTurnInContext(args) {
5967
6053
  else if (run.dispatch?.id) await recordTurnSummary({
5968
6054
  conversationId: runArgs.conversationId,
5969
6055
  destination: run.destination,
5970
- destinationVisibility: visibility,
5971
6056
  dispatchId: run.dispatch?.id,
5972
6057
  dispatchOutcome: dispatchResult.outcome,
5973
6058
  ...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : void 0,
@@ -6269,6 +6354,7 @@ async function runPausedTurnInContext(payload, options, runOptions) {
6269
6354
  messageText: "",
6270
6355
  conversationId: payload.conversationId,
6271
6356
  turnId: payload.turnId,
6357
+ traceContext: payload.traceContext,
6272
6358
  channelId: thread?.channelId ?? destination.channelId,
6273
6359
  ...thread?.threadTs ? { threadTs: thread.threadTs } : void 0,
6274
6360
  executeTurn: async (run, saveResult) => await executeTurn(options.agentRunner, run, saveResult),
@@ -6653,6 +6739,64 @@ async function commitAssistantMessage(args) {
6653
6739
  logException(error, "conversation.assistant.message_persist.failed");
6654
6740
  }
6655
6741
  }
6742
+ // src/chat/task-execution/web-steering.ts
6743
+ function steeringMessageFromWeb(message, actor) {
6744
+ const storedAttachments = readMessageAttachments(message.input.attachments);
6745
+ return {
6746
+ actor: {
6747
+ authorId: actor.userId,
6748
+ ...actor.fullName ? { authorName: actor.fullName } : void 0
6749
+ },
6750
+ provenance: instructionProvenanceFor(actor),
6751
+ text: message.input.text.trim(),
6752
+ timestampMs: message.createdAtMs,
6753
+ ...storedAttachments.length ? { storedAttachments } : void 0
6754
+ };
6755
+ }
6756
+ /**
6757
+ * Drain steered web messages into the active web Turn.
6758
+ *
6759
+ * Only messages from the Turn Actor join. Steered messages from another
6760
+ * participant stay pending and start their own Turn next, so they never act
6761
+ * with this Actor's credentials.
6762
+ */
6763
+ function createWebSteeringDrain(args) {
6764
+ const { actor, author, context, conversation, turnId } = args;
6765
+ return async (accept) => {
6766
+ const accepted = [];
6767
+ await context.attempt.drain(async (pending) => {
6768
+ const joining = pending.filter((message) => message.source === "web" && message.input.authorId === actor.userId && legacyWebMailboxMetadataSchema.safeParse(message.input.metadata).success);
6769
+ if (joining.length === 0) return [];
6770
+ for (const message of joining) {
6771
+ const attachments = readMessageAttachments(message.input.attachments);
6772
+ upsertConversationMessage(conversation, {
6773
+ id: message.inboundMessageId,
6774
+ role: "user",
6775
+ text: normalizeConversationText(message.input.text),
6776
+ createdAtMs: message.createdAtMs,
6777
+ author,
6778
+ meta: {
6779
+ ...attachments.length ? { attachments } : void 0,
6780
+ explicitMention: true,
6781
+ source: "web"
6782
+ }
6783
+ });
6784
+ }
6785
+ await persistConversationMessages({
6786
+ conversation,
6787
+ conversationId: context.conversationId
6788
+ });
6789
+ const saved = (await getTurnRecord(context.conversationId, turnId))?.piMessages ?? [];
6790
+ const messages = withoutSavedSteeringMessages(saved, joining.map((message) => steeringMessageFromWeb(message, actor)), buildSteeringPiMessage);
6791
+ if (messages.length > 0) {
6792
+ await accept(messages);
6793
+ accepted.push(...messages);
6794
+ }
6795
+ return joining.map((message) => message.inboundMessageId);
6796
+ });
6797
+ return accepted;
6798
+ };
6799
+ }
6656
6800
  // src/chat/conversations/destination.ts
6657
6801
  function requireLocalDestination(conversationId) {
6658
6802
  const parsed = localDestinationSchema.safeParse({
@@ -6982,6 +7126,13 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
6982
7126
  },
6983
7127
  delivery: deliverAssistantMessage,
6984
7128
  durability: {
7129
+ ...webActor ? { drainSteeringMessages: createWebSteeringDrain({
7130
+ actor: webActor,
7131
+ author,
7132
+ context,
7133
+ conversation,
7134
+ turnId
7135
+ }) } : void 0,
6985
7136
  onInputCommitted: acknowledge,
6986
7137
  shouldYield: context.shouldYield,
6987
7138
  onSandboxRefChanged: async (nextSandboxRef) => {
@@ -7481,4 +7632,1156 @@ async function appendAndEnqueueWebMessage(input, options) {
7481
7632
  status
7482
7633
  };
7483
7634
  }
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 };
7635
+ // src/chat/slack/location-url.ts
7636
+ /** Parse a Slack workspace subdomain used in `https://{domain}.slack.com` URLs. */
7637
+ function parseSlackTeamDomain(value) {
7638
+ if (typeof value !== "string") return void 0;
7639
+ const domain = value.trim().toLowerCase();
7640
+ if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(domain)) return;
7641
+ return domain;
7642
+ }
7643
+ /** Convert a Slack message ts into the `p…` segment used by archive URLs. */
7644
+ function archivePathFromThreadTs(threadTs) {
7645
+ const [seconds, fraction = ""] = threadTs.split(".");
7646
+ return `p${seconds}${(fraction ?? "").padEnd(6, "0").slice(0, 6)}`;
7647
+ }
7648
+ /**
7649
+ * Build a direct Slack archive URL for a thread when the workspace domain is
7650
+ * known. Omits a link rather than falling back to `app_redirect`.
7651
+ */
7652
+ function buildSlackLocationUrl(args) {
7653
+ const channelId = parseSlackChannelId(args.channelId);
7654
+ const teamDomain = parseSlackTeamDomain(args.teamDomain);
7655
+ const threadTs = parseSlackMessageTs(args.threadTs);
7656
+ if (!channelId || !teamDomain || !threadTs) return void 0;
7657
+ const url = new URL(`https://${teamDomain}.slack.com/archives/${channelId}/${archivePathFromThreadTs(threadTs)}`);
7658
+ url.searchParams.set("thread_ts", threadTs);
7659
+ url.searchParams.set("cid", channelId);
7660
+ return url.toString();
7661
+ }
7662
+ // src/chat/slack/team-domain.ts
7663
+ const TEAM_DOMAIN_CACHE_TTL_MS = 864e5;
7664
+ const TEAM_DOMAIN_NEGATIVE_CACHE_TTL_MS = 6e4;
7665
+ const teamDomainCache = /* @__PURE__ */ new Map();
7666
+ const teamDomainInflight = /* @__PURE__ */ new Map();
7667
+ function readTeamDomainCache(teamId) {
7668
+ const hit = teamDomainCache.get(teamId);
7669
+ if (!hit) return void 0;
7670
+ if (hit.expiresAt < Date.now()) {
7671
+ teamDomainCache.delete(teamId);
7672
+ return;
7673
+ }
7674
+ return hit.kind === "hit" ? hit.domain : null;
7675
+ }
7676
+ function writeTeamDomainCache(teamId, domain) {
7677
+ if (domain) {
7678
+ teamDomainCache.set(teamId, {
7679
+ kind: "hit",
7680
+ domain,
7681
+ expiresAt: Date.now() + TEAM_DOMAIN_CACHE_TTL_MS
7682
+ });
7683
+ return;
7684
+ }
7685
+ teamDomainCache.set(teamId, {
7686
+ kind: "miss",
7687
+ expiresAt: Date.now() + TEAM_DOMAIN_NEGATIVE_CACHE_TTL_MS
7688
+ });
7689
+ }
7690
+ async function fetchSlackTeamDomain(teamId) {
7691
+ try {
7692
+ const response = await withSlackRetries(() => getSlackClient().team.info({ team: teamId }), 3, {
7693
+ action: "team.info",
7694
+ idempotent: true,
7695
+ spanAttributes: { "app.slack.team_id": teamId }
7696
+ });
7697
+ const responseTeamId = parseSlackTeamId(response.team?.id);
7698
+ const domain = parseSlackTeamDomain(response.team?.domain);
7699
+ if (!domain || !responseTeamId || responseTeamId !== teamId) {
7700
+ writeTeamDomainCache(teamId, void 0);
7701
+ return;
7702
+ }
7703
+ writeTeamDomainCache(teamId, domain);
7704
+ return domain;
7705
+ } catch {
7706
+ writeTeamDomainCache(teamId, void 0);
7707
+ return;
7708
+ }
7709
+ }
7710
+ /** Resolve a workspace subdomain via `team.info`, cached by team id. */
7711
+ async function resolveSlackTeamDomain(teamId) {
7712
+ const parsedTeamId = parseSlackTeamId(teamId);
7713
+ if (!parsedTeamId) return void 0;
7714
+ const cached = readTeamDomainCache(parsedTeamId);
7715
+ if (cached !== void 0) return cached ?? void 0;
7716
+ const pending = teamDomainInflight.get(parsedTeamId);
7717
+ if (pending) return pending;
7718
+ const lookup = fetchSlackTeamDomain(parsedTeamId).finally(() => {
7719
+ teamDomainInflight.delete(parsedTeamId);
7720
+ });
7721
+ teamDomainInflight.set(parsedTeamId, lookup);
7722
+ return lookup;
7723
+ }
7724
+ /** Resolve workspace subdomains for the distinct team ids in one API response. */
7725
+ async function resolveSlackTeamDomains(teamIds) {
7726
+ const uniqueTeamIds = [...new Set([...teamIds].map((teamId) => parseSlackTeamId(teamId)).filter((teamId) => teamId !== void 0))];
7727
+ const resolved = await Promise.all(uniqueTeamIds.map(async (teamId) => {
7728
+ const domain = await resolveSlackTeamDomain(teamId);
7729
+ return domain ? [teamId, domain] : void 0;
7730
+ }));
7731
+ return new Map(resolved.filter((entry) => entry !== void 0));
7732
+ }
7733
+ // src/api/conversations/shared.ts
7734
+ /** Format a Slack destination without exposing private channel names. */
7735
+ function slackStatsLocationLabel(input) {
7736
+ const channelId = input.channel;
7737
+ if (!channelId) return void 0;
7738
+ if (input.channelNameRedacted && input.channelName) return input.channelName;
7739
+ const name = input.channelName?.replace(/^#/, "");
7740
+ if (channelId.startsWith("D")) return "Direct Message";
7741
+ if (channelId.startsWith("C")) return name ? `#${name}` : "Public Channel";
7742
+ if (channelId.startsWith("G")) {
7743
+ if (name?.startsWith("mpdm-")) return "Group DM";
7744
+ return "Private Channel";
7745
+ }
7746
+ return name || channelId;
7747
+ }
7748
+ /** Return the generic display label for a conversation surface. */
7749
+ function surfaceFallbackLabel(surface) {
7750
+ if (surface === "scheduler") return "Scheduler";
7751
+ if (surface === "api") return "API";
7752
+ if (surface === "internal") return "Internal";
7753
+ return "Conversation";
7754
+ }
7755
+ // src/api/conversations/projection.ts
7756
+ const PRIVATE_CONVERSATION_LABEL = "Private Conversation";
7757
+ function privateConversationLabel(slackConversation) {
7758
+ if (!slackConversation) return PRIVATE_CONVERSATION_LABEL;
7759
+ return slackConversation.visibility === "private" ? formatSlackConversationRedactedLabel(slackConversation) ?? PRIVATE_CONVERSATION_LABEL : PRIVATE_CONVERSATION_LABEL;
7760
+ }
7761
+ function surfaceFromConversationId(conversationId) {
7762
+ if (parseSlackThreadId(conversationId)) return "slack";
7763
+ if (conversationId.startsWith("scheduler:")) return "scheduler";
7764
+ if (conversationId.startsWith("api:")) return "api";
7765
+ return "internal";
7766
+ }
7767
+ function surfaceFromSource(source, conversationId) {
7768
+ if (source === "slack" || source === "api" || source === "web" || source === "scheduler") return source === "web" ? "api" : source;
7769
+ return surfaceFromConversationId(conversationId);
7770
+ }
7771
+ function locationUrlFromConversation(args) {
7772
+ const { conversation } = args;
7773
+ if (!args.canViewPrivateContent || conversation.location?.provider !== "slack") return;
7774
+ const teamDomain = args.teamDomainByTeamId?.get(conversation.location.teamId);
7775
+ const threadTs = conversation.location.threadTs;
7776
+ if (!teamDomain || !threadTs) return void 0;
7777
+ return buildSlackLocationUrl({
7778
+ channelId: conversation.location.channelId,
7779
+ teamDomain,
7780
+ threadTs
7781
+ });
7782
+ }
7783
+ function actorIdentityReport(actor) {
7784
+ if (!actor) return void 0;
7785
+ const identity = {
7786
+ ...actor.email !== void 0 ? { email: actor.email } : void 0,
7787
+ ...actor.fullName !== void 0 ? { fullName: actor.fullName } : void 0,
7788
+ ...actor.slackUserId !== void 0 ? { slackUserId: actor.slackUserId } : void 0,
7789
+ ...actor.slackUserName !== void 0 ? { slackUserName: actor.slackUserName } : void 0
7790
+ };
7791
+ return Object.keys(identity).length > 0 ? identity : void 0;
7792
+ }
7793
+ function statusFromConversation(conversation) {
7794
+ if (conversation.execution.status === "idle") return "completed";
7795
+ if (conversation.execution.status === "failed") return "failed";
7796
+ return "active";
7797
+ }
7798
+ function titleFromConversation(args) {
7799
+ const slackThread = parseSlackThreadId(args.conversation.conversationId);
7800
+ const effectiveChannelName = args.conversation.channelName;
7801
+ const slackConversation = resolveSlackConversationContextFromThreadId({
7802
+ threadId: args.conversation.conversationId,
7803
+ channelName: effectiveChannelName,
7804
+ ...args.visibility ? { visibility: args.visibility } : void 0
7805
+ });
7806
+ return (args.canViewPrivateContent ? void 0 : privateConversationLabel(slackConversation)) ?? args.conversation.title ?? args.automationTitle ?? slackStatsLocationLabel({
7807
+ channel: slackThread?.channelId,
7808
+ channelName: effectiveChannelName
7809
+ }) ?? (args.conversation.source === "plugin" ? "Automation run" : fallbackShortTitle(args.firstHumanMessageText ?? "", surfaceFallbackLabel(args.surface)));
7810
+ }
7811
+ function channelNameFromConversation(conversation, canViewPrivateContent, visibility) {
7812
+ const effectiveChannelName = conversation.channelName;
7813
+ const slackThread = parseSlackThreadId(conversation.conversationId);
7814
+ if (!effectiveChannelName && !slackThread) return;
7815
+ const slackConversation = resolveSlackConversationContextFromThreadId({
7816
+ threadId: conversation.conversationId,
7817
+ channelName: effectiveChannelName,
7818
+ ...visibility ? { visibility } : void 0
7819
+ });
7820
+ if (!canViewPrivateContent) return privateConversationLabel(slackConversation);
7821
+ return effectiveChannelName;
7822
+ }
7823
+ function channelNameRedactedFromConversation(conversation, canViewPrivateContent) {
7824
+ const effectiveChannelName = conversation.channelName;
7825
+ const slackThread = parseSlackThreadId(conversation.conversationId);
7826
+ if (!effectiveChannelName && !slackThread) return false;
7827
+ return !canViewPrivateContent;
7828
+ }
7829
+ /** Describe whether a viewer can read one conversation's retained events. */
7830
+ function conversationEventHistory(args) {
7831
+ if (args.transcriptPurgedAtMs !== void 0) return {
7832
+ status: "expired",
7833
+ expiredAt: new Date(args.transcriptPurgedAtMs).toISOString()
7834
+ };
7835
+ return args.canExposePayload ? { status: "available" } : {
7836
+ status: "redacted",
7837
+ reason: "non_public_conversation"
7838
+ };
7839
+ }
7840
+ /** Project one durable conversation and its SQL metrics into the REST summary. */
7841
+ function conversationSummaryFromStoredConversation(args) {
7842
+ const { conversation, durationMs, usage } = args;
7843
+ const canViewPrivateContent = args.access?.canViewPrivateContent ?? false;
7844
+ const accessVisibility = args.access?.visibility;
7845
+ const visibility = accessVisibility === "public" || accessVisibility === "private" ? accessVisibility : void 0;
7846
+ const surface = surfaceFromSource(conversation.source, conversation.conversationId);
7847
+ const actorIdentity = actorIdentityReport(conversation.actor);
7848
+ const locationUrl = locationUrlFromConversation({
7849
+ canViewPrivateContent,
7850
+ conversation,
7851
+ ...args.teamDomainByTeamId ? { teamDomainByTeamId: args.teamDomainByTeamId } : void 0
7852
+ });
7853
+ const slackThread = parseSlackThreadId(conversation.conversationId);
7854
+ const channelName = channelNameFromConversation(conversation, canViewPrivateContent, visibility);
7855
+ const channelNameRedacted = channelNameRedactedFromConversation(conversation, canViewPrivateContent);
7856
+ return {
7857
+ conversationId: conversation.conversationId,
7858
+ cumulativeDurationMs: durationMs,
7859
+ displayTitle: titleFromConversation({
7860
+ automationTitle: args.automationTitle,
7861
+ canViewPrivateContent,
7862
+ conversation,
7863
+ firstHumanMessageText: args.firstHumanMessageText,
7864
+ surface,
7865
+ visibility
7866
+ }),
7867
+ isParticipant: args.access?.isParticipant ?? false,
7868
+ ...visibility ? { visibility } : void 0,
7869
+ lastProgressAt: new Date(conversation.execution.updatedAtMs ?? conversation.updatedAtMs).toISOString(),
7870
+ lastSeenAt: new Date(conversation.lastActivityAtMs).toISOString(),
7871
+ startedAt: new Date(conversation.createdAtMs).toISOString(),
7872
+ status: statusFromConversation(conversation),
7873
+ surface,
7874
+ ...args.auxiliaryCosts ? { auxiliaryCosts: args.auxiliaryCosts } : void 0,
7875
+ ...usage ? { cumulativeUsage: usage } : void 0,
7876
+ ...args.childAgentTotals ? { childAgentTotals: args.childAgentTotals } : void 0,
7877
+ ...actorIdentity ? { actorIdentity } : void 0,
7878
+ ...locationUrl ? { locationUrl } : void 0,
7879
+ archivedAt: args.archivedAtMs === void 0 ? null : new Date(args.archivedAtMs).toISOString(),
7880
+ ...slackThread ? { channel: slackThread.channelId } : void 0,
7881
+ ...channelName ? { channelName } : void 0,
7882
+ ...channelNameRedacted ? { channelNameRedacted: true } : void 0,
7883
+ ...args.locationId && !channelNameRedacted ? { locationId: args.locationId } : void 0
7884
+ };
7885
+ }
7886
+ // src/api/conversations/usage.ts
7887
+ const usage = juniorConversations.usage;
7888
+ const componentFields = [
7889
+ "inputTokens",
7890
+ "outputTokens",
7891
+ "cachedInputTokens",
7892
+ "cacheCreationTokens"
7893
+ ];
7894
+ function summedUsageNumber(field) {
7895
+ return sql`sum((${usage}->>${field})::double precision)::double precision`;
7896
+ }
7897
+ function summedCostNumber(field) {
7898
+ return sql`round(
7899
+ sum((${usage}->'cost'->>${field})::numeric),
7900
+ 12
7901
+ )::double precision`;
7902
+ }
7903
+ function usageFromAggregate(row) {
7904
+ const components = {};
7905
+ let componentTotal = 0;
7906
+ for (const field of componentFields) {
7907
+ const value = row[field];
7908
+ if (value === null) continue;
7909
+ components[field] = value;
7910
+ componentTotal += value;
7911
+ }
7912
+ const cost = {
7913
+ ...row.costInput === null ? void 0 : { input: row.costInput },
7914
+ ...row.costOutput === null ? void 0 : { output: row.costOutput },
7915
+ ...row.costCacheRead === null ? void 0 : { cacheRead: row.costCacheRead },
7916
+ ...row.costCacheWrite === null ? void 0 : { cacheWrite: row.costCacheWrite },
7917
+ ...row.costTotal === null ? void 0 : { total: row.costTotal }
7918
+ };
7919
+ const result = {
7920
+ ...row.totalOnlyTokens === null ? components : { totalTokens: row.totalOnlyTokens + componentTotal },
7921
+ ...row.reasoningTokens === null ? void 0 : { reasoningTokens: row.reasoningTokens },
7922
+ ...Object.keys(cost).length === 0 ? void 0 : { cost }
7923
+ };
7924
+ return hasAgentTurnUsage(result) ? result : void 0;
7925
+ }
7926
+ /**
7927
+ * Aggregate saved runtime and usage in PostgreSQL. The total for each selected
7928
+ * Conversation includes itself and all its child agents at any depth.
7929
+ */
7930
+ async function readConversationTreeMetricsFromSql(db, conversationIds) {
7931
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
7932
+ const hasComponents = sql`coalesce(
7933
+ ${usage}->>'inputTokens',
7934
+ ${usage}->>'outputTokens',
7935
+ ${usage}->>'cachedInputTokens',
7936
+ ${usage}->>'cacheCreationTokens'
7937
+ ) is not null`;
7938
+ const isChildAgent = sql`(${conversationSubtreeColumns.conversationId} <> ${conversationSubtreeColumns.scopeConversationId})`;
7939
+ const rows = await db.select({
7940
+ scopeConversationId: conversationSubtreeColumns.scopeConversationId,
7941
+ isChildAgent,
7942
+ conversations: sql`count(*)::integer`.mapWith(Number),
7943
+ durationMs: sql`coalesce(sum(${juniorConversations.durationMs}), 0)::double precision`,
7944
+ inputTokens: summedUsageNumber("inputTokens"),
7945
+ outputTokens: summedUsageNumber("outputTokens"),
7946
+ cachedInputTokens: summedUsageNumber("cachedInputTokens"),
7947
+ cacheCreationTokens: summedUsageNumber("cacheCreationTokens"),
7948
+ totalOnlyTokens: sql`sum(
7949
+ case
7950
+ when not (${hasComponents})
7951
+ then (${usage}->>'totalTokens')::double precision
7952
+ end
7953
+ )::double precision`,
7954
+ reasoningTokens: summedUsageNumber("reasoningTokens"),
7955
+ costInput: summedCostNumber("input"),
7956
+ costOutput: summedCostNumber("output"),
7957
+ costCacheRead: summedCostNumber("cacheRead"),
7958
+ costCacheWrite: summedCostNumber("cacheWrite"),
7959
+ costTotal: summedCostNumber("total")
7960
+ }).from(juniorConversations).innerJoin(conversationSubtree(conversationIds), eq(conversationSubtreeColumns.conversationId, juniorConversations.conversationId)).groupBy(conversationSubtreeColumns.scopeConversationId, isChildAgent);
7961
+ const ownRows = /* @__PURE__ */ new Map();
7962
+ const childRows = /* @__PURE__ */ new Map();
7963
+ for (const row of rows) (row.isChildAgent ? childRows : ownRows).set(row.scopeConversationId, row);
7964
+ const result = /* @__PURE__ */ new Map();
7965
+ for (const [conversationId, own] of ownRows) {
7966
+ const ownUsage = usageFromAggregate(own);
7967
+ const child = childRows.get(conversationId);
7968
+ const childUsage = child ? usageFromAggregate(child) : void 0;
7969
+ const usage = addAgentTurnUsage(ownUsage, childUsage);
7970
+ result.set(conversationId, {
7971
+ durationMs: own.durationMs + (child?.durationMs ?? 0),
7972
+ ...usage ? { usage } : void 0,
7973
+ ...child ? { childAgents: {
7974
+ count: child.conversations,
7975
+ durationMs: child.durationMs,
7976
+ ...childUsage ? { usage: childUsage } : void 0
7977
+ } } : void 0
7978
+ });
7979
+ }
7980
+ return result;
7981
+ }
7982
+ /**
7983
+ * Project the child agent part of tree metrics into the API shape. Return
7984
+ * nothing when the Conversation has no child agents.
7985
+ */
7986
+ function childAgentTotalsReport(metrics, auxiliaryCosts) {
7987
+ const childAgents = metrics?.childAgents;
7988
+ if (!childAgents) return void 0;
7989
+ return {
7990
+ ...auxiliaryCosts?.childAgentCostUsd === void 0 ? void 0 : { auxiliaryCostUsd: auxiliaryCosts.childAgentCostUsd },
7991
+ count: childAgents.count,
7992
+ cumulativeDurationMs: childAgents.durationMs,
7993
+ ...childAgents.usage ? { cumulativeUsage: childAgents.usage } : void 0
7994
+ };
7995
+ }
7996
+ // src/chat/conversations/fork.ts
7997
+ /** Event key of the agent note that follows the copied events in a fork. */
7998
+ const FORK_NOTE_KEY = "fork:note";
7999
+ const forkNote = alias(juniorConversationEvents, "fork_note");
8000
+ /**
8001
+ * SQL filter for event rows that a fork did not copy from its source. Usage
8002
+ * and cost reports use it, so copied model calls count only in the source.
8003
+ */
8004
+ function notCopiedByFork() {
8005
+ return sql`not exists (
8006
+ select 1 from ${juniorConversationEvents} as ${sql.identifier("fork_note")}
8007
+ where ${forkNote.conversationId} = ${juniorConversationEvents.conversationId}
8008
+ and ${forkNote.idempotencyKey} = ${FORK_NOTE_KEY}
8009
+ and ${forkNote.seq} > ${juniorConversationEvents.seq}
8010
+ )`;
8011
+ }
8012
+ /**
8013
+ * Events that hold credentials, approvals, provider connections, or plugin
8014
+ * state of the source actor. A fork does not copy them.
8015
+ */
8016
+ const UNCOPIED_EVENT_TYPES = /* @__PURE__ */ new Set([
8017
+ "authorization_completed",
8018
+ "authorization_requested",
8019
+ "guardian_action_reviewed",
8020
+ "mcp_provider_connected",
8021
+ "mcp_provider_connected_unowned",
8022
+ "structured_event",
8023
+ "unknown"
8024
+ ]);
8025
+ 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.";
8026
+ /** An expected fork request failure, such as a reply without saved history. */
8027
+ var ConversationForkError = class extends Error {};
8028
+ /**
8029
+ * Create a new web root Conversation from the agent history of a source
8030
+ * Conversation, through one completed assistant reply.
8031
+ *
8032
+ * Retries with the same requester and key return the same Conversation.
8033
+ */
8034
+ async function forkConversation(input) {
8035
+ const conversationId = createConversationId({
8036
+ actorEmail: input.actorEmail,
8037
+ idempotencyKey: JSON.stringify([
8038
+ "fork",
8039
+ input.sourceConversationId,
8040
+ input.idempotencyKey
8041
+ ])
8042
+ });
8043
+ const executor = getSqlExecutor();
8044
+ const events = getConversationEventStore();
8045
+ const store = getConversationStore();
8046
+ return withConversationEventLock(executor, input.sourceConversationId, () => withConversationEventLock(executor, conversationId, async () => {
8047
+ if (await store.get({ conversationId })) return { conversationId };
8048
+ const source = await store.get({ conversationId: input.sourceConversationId });
8049
+ if (!source || source.transcriptPurgedAtMs !== void 0) throw new ConversationForkError("The source history is no longer available.");
8050
+ if (source.parentConversationId) throw new ConversationForkError("You cannot fork a child conversation.");
8051
+ const boundary = await events.loadByIdempotencyKey(input.sourceConversationId, `message:${input.messageId}:agent`);
8052
+ const reply = boundary?.data.type === "assistant_message" ? piMessageFromHistoryItem(boundary.data) : void 0;
8053
+ 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.");
8054
+ const projection = await loadTurnProjection({
8055
+ conversationId: input.sourceConversationId,
8056
+ committedSeq: boundary.seq,
8057
+ includeTail: false
8058
+ });
8059
+ if (!projection?.messages.length) throw new ConversationForkError("The selected history is no longer available.");
8060
+ if (hasUnfinishedToolCalls(projection.messages)) throw new ConversationForkError("This history has unfinished tool calls. Choose another reply.");
8061
+ const nowMs = Date.now();
8062
+ await recordWebConversationActivity({
8063
+ actor: webActorFromEmail(input.actorEmail, { ...input.actorName ? { fullName: input.actorName } : void 0 }),
8064
+ conversationId,
8065
+ nowMs,
8066
+ rootVisibility: source.visibility === "public" ? "public" : "private"
8067
+ });
8068
+ await executor.db().update(juniorConversations).set({
8069
+ forkedFromConversationId: source.conversationId,
8070
+ title: source.title ? `Fork: ${source.title}` : "Forked conversation"
8071
+ }).where(eq(juniorConversations.conversationId, conversationId));
8072
+ await copyForkEvents({
8073
+ sourceConversationId: source.conversationId,
8074
+ conversationId,
8075
+ events: await events.loadHistory(source.conversationId),
8076
+ boundarySeq: boundary.seq,
8077
+ messageId: input.messageId
8078
+ });
8079
+ await events.append(conversationId, [{
8080
+ idempotencyKey: FORK_NOTE_KEY,
8081
+ createdAtMs: nowMs,
8082
+ data: {
8083
+ type: "user_message",
8084
+ provenance: contextProvenance,
8085
+ content: [{
8086
+ type: "text",
8087
+ text: FORK_NOTE
8088
+ }],
8089
+ timestamp: nowMs
8090
+ }
8091
+ }]);
8092
+ return { conversationId };
8093
+ }));
8094
+ }
8095
+ /**
8096
+ * Copy the source event rows through the selected reply. Copies keep their
8097
+ * sequence, author, timestamp, key, and payload, so the fork shows the same
8098
+ * transcript and event log, and seq references in compactions stay valid.
8099
+ * Later updates of copied Messages and the end of copied Turns are also
8100
+ * copied, so that no copied item stays open in the fork.
8101
+ */
8102
+ async function copyForkEvents(args) {
8103
+ const reply = args.events.find((event) => event.data.type === "message" && event.data.role === "assistant" && event.data.messageId === args.messageId);
8104
+ const cutoffSeq = Math.max(args.boundarySeq, reply?.seq ?? -1);
8105
+ const messageIds = /* @__PURE__ */ new Set();
8106
+ const turnIds = /* @__PURE__ */ new Set();
8107
+ const seqs = [];
8108
+ let historyVersion = 0;
8109
+ for (const event of args.events) {
8110
+ const data = event.data;
8111
+ if (UNCOPIED_EVENT_TYPES.has(data.type)) continue;
8112
+ if (event.idempotencyKey === FORK_NOTE_KEY) continue;
8113
+ if (event.seq <= cutoffSeq) {
8114
+ if (data.type === "message") messageIds.add(data.messageId);
8115
+ if (data.type === "turn_started") turnIds.add(data.turnId);
8116
+ historyVersion = Math.max(historyVersion, event.historyVersion);
8117
+ seqs.push(event.seq);
8118
+ } 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);
8119
+ }
8120
+ if (!seqs.length) return;
8121
+ const e = juniorConversationEvents;
8122
+ await getSqlExecutor().db().execute(sql`insert into ${e} (${sql.join([
8123
+ e.conversationId,
8124
+ e.seq,
8125
+ e.historyVersion,
8126
+ e.schemaVersion,
8127
+ e.idempotencyKey,
8128
+ e.type,
8129
+ e.payload,
8130
+ e.actorIdentityId,
8131
+ e.createdAt
8132
+ ].map((column) => sql.identifier(column.name)), sql`, `)})
8133
+ select ${args.conversationId}, ${e.seq}, least(${e.historyVersion}, ${historyVersion}), ${e.schemaVersion}, ${e.idempotencyKey}, ${e.type}, ${e.payload}, ${e.actorIdentityId}, ${e.createdAt}
8134
+ from ${e}
8135
+ where ${e.conversationId} = ${args.sourceConversationId}
8136
+ and ${inArray(e.seq, seqs)}`);
8137
+ }
8138
+ function hasUnfinishedToolCalls(messages) {
8139
+ const pending = /* @__PURE__ */ new Set();
8140
+ for (const message of messages) if (message.role === "assistant") {
8141
+ for (const part of message.content) if (part.type === "toolCall") pending.add(part.id);
8142
+ } else if (message.role === "toolResult") pending.delete(message.toolCallId);
8143
+ return pending.size > 0;
8144
+ }
8145
+ // src/api/conversations/auxiliary-costs.ts
8146
+ function eventCost() {
8147
+ return sql`CASE
8148
+ WHEN ${juniorConversationEvents.type} = 'structured_event'
8149
+ AND jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
8150
+ THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::numeric
8151
+ WHEN ${juniorConversationEvents.type} IN ('guardian_action_reviewed', 'turn_routed')
8152
+ AND jsonb_typeof(${juniorConversationEvents.payload}->'costUsd') = 'number'
8153
+ THEN (${juniorConversationEvents.payload}->>'costUsd')::numeric
8154
+ ELSE NULL
8155
+ END`;
8156
+ }
8157
+ function addCostUsd(left, right) {
8158
+ return Math.round((left + right) * 0xe8d4a51000) / 0xe8d4a51000;
8159
+ }
8160
+ /**
8161
+ * Aggregate additive event costs by namespaced operation. The total for each
8162
+ * selected Conversation includes itself and all its child agents at any depth.
8163
+ */
8164
+ async function readConversationAuxiliaryCostsFromSql(db, conversationIds) {
8165
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
8166
+ const scopeConversationId = conversationSubtreeColumns.scopeConversationId;
8167
+ const isChildAgent = sql`(${conversationSubtreeColumns.conversationId} <> ${scopeConversationId})`;
8168
+ const namespace = sql`CASE
8169
+ WHEN ${juniorConversationEvents.type} = 'structured_event'
8170
+ THEN ${juniorConversationEvents.payload}->>'namespace'
8171
+ ELSE 'junior'
8172
+ END`;
8173
+ const name = sql`CASE
8174
+ WHEN ${juniorConversationEvents.type} = 'structured_event'
8175
+ THEN ${juniorConversationEvents.payload}->>'name'
8176
+ ELSE ${juniorConversationEvents.type}
8177
+ END`;
8178
+ const cost = eventCost();
8179
+ const rows = await db.select({
8180
+ conversationId: scopeConversationId,
8181
+ childAgentCostUsd: sql`coalesce(round(sum(case when ${isChildAgent} then ${cost} end), 12), 0)::double precision`.mapWith(Number),
8182
+ costUsd: sql`round(sum(${cost}), 12)::double precision`.mapWith(Number),
8183
+ events: sql`count(*)::integer`.mapWith(Number),
8184
+ name,
8185
+ namespace
8186
+ }).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);
8187
+ const result = /* @__PURE__ */ new Map();
8188
+ for (const row of rows) {
8189
+ const current = result.get(row.conversationId) ?? { total: {
8190
+ costUsd: 0,
8191
+ operations: []
8192
+ } };
8193
+ current.total.costUsd = addCostUsd(current.total.costUsd, row.costUsd);
8194
+ if (row.childAgentCostUsd > 0) current.childAgentCostUsd = addCostUsd(current.childAgentCostUsd ?? 0, row.childAgentCostUsd);
8195
+ current.total.operations.push({
8196
+ costUsd: row.costUsd,
8197
+ events: row.events,
8198
+ name: row.name,
8199
+ namespace: row.namespace
8200
+ });
8201
+ result.set(row.conversationId, current);
8202
+ }
8203
+ return result;
8204
+ }
8205
+ // src/chat/plugins/conversation-sidebar.ts
8206
+ /** Show native objects by default and let plugins select compact sidebar labels. */
8207
+ async function listConversationSidebarAnnotations(conversationIds, annotationsByConversation) {
8208
+ const candidates = new Set(conversationIds);
8209
+ const selected = {};
8210
+ const plugins = getPlugins();
8211
+ const customOwners = new Set(plugins.filter((plugin) => plugin.hooks?.conversationSidebar).map((plugin) => plugin.manifest.name));
8212
+ for (const conversationId of conversationIds) {
8213
+ const objects = (annotationsByConversation.get(conversationId) ?? []).flatMap((annotation) => {
8214
+ if (customOwners.has(annotation.plugin) || !annotation.objectType) return [];
8215
+ return [{
8216
+ key: annotation.key,
8217
+ label: annotation.label,
8218
+ objectType: annotation.objectType === "item" && annotation.kind === "object" && annotation.facts?.type === "deployment" ? "deployment" : annotation.objectType,
8219
+ status: annotation.status
8220
+ }];
8221
+ });
8222
+ if (objects.length) selected[conversationId] = objects;
8223
+ }
8224
+ for (const plugin of plugins) {
8225
+ const hook = plugin.hooks?.conversationSidebar;
8226
+ if (!hook) continue;
8227
+ const annotationsByConversationId = Object.fromEntries(conversationIds.flatMap((conversationId) => {
8228
+ const annotations = (annotationsByConversation.get(conversationId) ?? []).filter((annotation) => annotation.plugin === plugin.manifest.name);
8229
+ return annotations.length > 0 ? [[conversationId, annotations]] : [];
8230
+ }));
8231
+ try {
8232
+ const result = await hook({
8233
+ annotationsByConversationId,
8234
+ conversationIds,
8235
+ db: getDb(),
8236
+ log: createPluginLogger(plugin.manifest.name),
8237
+ plugin: { name: plugin.manifest.name },
8238
+ cache: createPluginCache(plugin.manifest.name)
8239
+ });
8240
+ for (const [conversationId, annotations] of Object.entries(result.annotationsByConversationId)) {
8241
+ if (!candidates.has(conversationId)) continue;
8242
+ const parsed = conversationSidebarAnnotationSchema.array().safeParse(annotations);
8243
+ if (!parsed.success || parsed.data.length === 0) continue;
8244
+ selected[conversationId] = [...selected[conversationId] ?? [], ...parsed.data];
8245
+ }
8246
+ } catch (error) {
8247
+ logWarn("plugin.conversation_sidebar.hook.failed", {
8248
+ "app.plugin.name": plugin.manifest.name,
8249
+ "exception.message": error instanceof Error ? error.message : String(error)
8250
+ });
8251
+ }
8252
+ }
8253
+ return selected;
8254
+ }
8255
+ // src/chat/plugins/unfinished-work.ts
8256
+ /**
8257
+ * Return assigned and unfinished plugin work for the candidate conversations.
8258
+ * Feed Priority combines these signals with conversation activity on the host.
8259
+ */
8260
+ async function listConversationWork(conversationIds) {
8261
+ if (conversationIds.length === 0) return {
8262
+ assignedIds: [],
8263
+ finishedAtById: {},
8264
+ unfinishedIds: []
8265
+ };
8266
+ const candidates = new Set(conversationIds);
8267
+ const assigned = /* @__PURE__ */ new Set();
8268
+ const finishedAtById = /* @__PURE__ */ new Map();
8269
+ const unfinished = /* @__PURE__ */ new Set();
8270
+ for (const plugin of getPlugins()) {
8271
+ const hook = plugin.hooks?.unfinishedWork;
8272
+ if (!hook) continue;
8273
+ try {
8274
+ const result = await hook({
8275
+ conversationIds,
8276
+ db: getDb(),
8277
+ log: createPluginLogger(plugin.manifest.name),
8278
+ plugin: { name: plugin.manifest.name },
8279
+ cache: createPluginCache(plugin.manifest.name)
8280
+ });
8281
+ for (const conversationId of result.conversationIds) {
8282
+ if (!candidates.has(conversationId)) continue;
8283
+ unfinished.add(conversationId);
8284
+ assigned.add(conversationId);
8285
+ }
8286
+ for (const conversationId of result.assignedConversationIds ?? []) if (candidates.has(conversationId)) assigned.add(conversationId);
8287
+ for (const [conversationId, finishedAt] of Object.entries(result.finishedWorkAtByConversationId ?? {})) {
8288
+ if (!candidates.has(conversationId)) continue;
8289
+ const time = Date.parse(finishedAt);
8290
+ if (!Number.isFinite(time)) continue;
8291
+ const current = finishedAtById.get(conversationId);
8292
+ if (!current || time > Date.parse(current)) finishedAtById.set(conversationId, finishedAt);
8293
+ }
8294
+ } catch (error) {
8295
+ logWarn("plugin.unfinished_work.hook.failed", {
8296
+ "app.plugin.name": plugin.manifest.name,
8297
+ "exception.message": error instanceof Error ? error.message : String(error)
8298
+ });
8299
+ }
8300
+ }
8301
+ return {
8302
+ assignedIds: conversationIds.filter((conversationId) => assigned.has(conversationId)),
8303
+ finishedAtById: Object.fromEntries(conversationIds.flatMap((conversationId) => {
8304
+ const finishedAt = finishedAtById.get(conversationId);
8305
+ return finishedAt ? [[conversationId, finishedAt]] : [];
8306
+ })),
8307
+ unfinishedIds: conversationIds.filter((conversationId) => unfinished.has(conversationId))
8308
+ };
8309
+ }
8310
+ // src/api/conversations/priority.ts
8311
+ /** Conversations with no known work leave Priority after this idle window. */
8312
+ const UNASSIGNED_PRIORITY_WINDOW_MS = 108e5;
8313
+ /** Unfinished work leaves Priority after this idle window. */
8314
+ const UNFINISHED_PRIORITY_WINDOW_MS = 1728e5;
8315
+ /**
8316
+ * Decide whether a conversation belongs in the dashboard Priority section.
8317
+ *
8318
+ * Priority includes only:
8319
+ * - unfinished work last seen within 48 hours
8320
+ * - finished assigned work with a user message after the finish time
8321
+ * - no known work last seen within 3 hours
8322
+ *
8323
+ * Finished assigned work with no later activity stays out of Priority.
8324
+ */
8325
+ function isConversationPriority(conversation, nowMs) {
8326
+ const lastSeenAt = Date.parse(conversation.lastSeenAt);
8327
+ if (!Number.isFinite(lastSeenAt)) return false;
8328
+ if (conversation.unfinishedWork) return nowMs - lastSeenAt <= UNFINISHED_PRIORITY_WINDOW_MS;
8329
+ if (conversation.assignedWork) {
8330
+ const finishedAt = Date.parse(conversation.finishedWorkAt ?? "");
8331
+ const lastUserMessageAt = Date.parse(conversation.lastUserMessageAt ?? "");
8332
+ return Number.isFinite(finishedAt) && Number.isFinite(lastUserMessageAt) && lastUserMessageAt > finishedAt;
8333
+ }
8334
+ return nowMs - lastSeenAt <= UNASSIGNED_PRIORITY_WINDOW_MS;
8335
+ }
8336
+ // src/api/conversations/user-message-activity.ts
8337
+ /** Read the latest true user message time for each selected conversation. */
8338
+ async function readLastUserMessageAtByConversation(db, conversationIds) {
8339
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
8340
+ const authority = sql`${juniorConversationEvents.payload}->'provenance'->>'authority'`;
8341
+ const actorPlatform = sql`${juniorConversationEvents.payload}->'provenance'->'actor'->>'platform'`;
8342
+ const role = sql`${juniorConversationEvents.payload}->>'role'`;
8343
+ const rows = await db.select({
8344
+ conversationId: juniorConversationEvents.conversationId,
8345
+ lastUserMessageAt: sql`max(${juniorConversationEvents.createdAt})`
8346
+ }).from(juniorConversationEvents).where(and(inArray(juniorConversationEvents.conversationId, conversationIds), or(and(eq(juniorConversationEvents.type, "user_message"), eq(authority, "instruction"), sql`${actorPlatform} in ('slack', 'local', 'web')`), and(eq(juniorConversationEvents.type, "message"), eq(role, "user"), isNotNull(juniorConversationEvents.actorIdentityId))))).groupBy(juniorConversationEvents.conversationId);
8347
+ return new Map(rows.map((row) => [row.conversationId, new Date(row.lastUserMessageAt).toISOString()]));
8348
+ }
8349
+ // src/api/conversations/activity-preview.ts
8350
+ /** Read one recent assistant message, or the latest user message, for each conversation. */
8351
+ async function readConversationActivityPreviews(db, conversationIds) {
8352
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
8353
+ const role = sql`${juniorConversationEvents.payload}->>'role'`;
8354
+ const text = sql`${juniorConversationEvents.payload}->>'text'`;
8355
+ const assistantFirst = sql`case when ${role} = 'assistant' then 0 else 1 end`;
8356
+ const rows = await db.selectDistinctOn([juniorConversationEvents.conversationId], {
8357
+ conversationId: juniorConversationEvents.conversationId,
8358
+ createdAt: juniorConversationEvents.createdAt,
8359
+ role: role.as("role"),
8360
+ text: text.as("text")
8361
+ }).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));
8362
+ return new Map(rows.map((row) => [row.conversationId, {
8363
+ createdAt: row.createdAt.toISOString(),
8364
+ role: row.role,
8365
+ text: row.text
8366
+ }]));
8367
+ }
8368
+ // src/api/conversations/first-message.ts
8369
+ /**
8370
+ * Read the text of the first human message for each untitled conversation.
8371
+ * Read-time titles use it while the generated title is still unset.
8372
+ */
8373
+ async function readFirstHumanMessageTexts(db, conversations) {
8374
+ const untitledIds = conversations.flatMap((conversation) => conversation.title ? [] : [conversation.conversationId]);
8375
+ if (untitledIds.length === 0) return /* @__PURE__ */ new Map();
8376
+ const text = sql`${juniorConversationEvents.payload}->>'text'`;
8377
+ const rows = await db.selectDistinctOn([juniorConversationEvents.conversationId], {
8378
+ conversationId: juniorConversationEvents.conversationId,
8379
+ text: text.as("text")
8380
+ }).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));
8381
+ return new Map(rows.map((row) => [row.conversationId, row.text]));
8382
+ }
8383
+ // src/api/mentions.ts
8384
+ const SLACK_USER_MENTION_RE = /<@([UW][A-Z0-9]+)(?:\|[^<>]*)?>/g;
8385
+ function mentionedSlackUserIds(texts) {
8386
+ const ids = texts.flatMap((text) => text ? [...text.matchAll(SLACK_USER_MENTION_RE)].map((m) => m[1]) : []);
8387
+ return [...new Set(ids)];
8388
+ }
8389
+ /**
8390
+ * Resolve the people that each source mentions, in source order. Every API
8391
+ * that returns display text uses this, so the dashboard can show the same
8392
+ * names and People links everywhere. It does one identity query per Slack
8393
+ * workspace.
8394
+ */
8395
+ async function resolveMentions(executor, sources) {
8396
+ const idsBySource = sources.map((source) => mentionedSlackUserIds(source.texts));
8397
+ const idsByTenant = /* @__PURE__ */ new Map();
8398
+ sources.forEach((source, index) => {
8399
+ if (idsBySource[index].length === 0) return;
8400
+ const ids = idsByTenant.get(source.tenantId) ?? /* @__PURE__ */ new Set();
8401
+ for (const id of idsBySource[index]) ids.add(id);
8402
+ idsByTenant.set(source.tenantId, ids);
8403
+ });
8404
+ const peopleByTenant = new Map(await Promise.all([...idsByTenant].map(async ([tenantId, ids]) => [tenantId, await readMentionedPeople(executor, tenantId, [...ids])])));
8405
+ return sources.map((source, index) => {
8406
+ const people = peopleByTenant.get(source.tenantId);
8407
+ return idsBySource[index].flatMap((id) => {
8408
+ const person = people?.get(id);
8409
+ return person ? [person] : [];
8410
+ });
8411
+ });
8412
+ }
8413
+ /**
8414
+ * Read current names for mentioned Slack users. Known Junior identities come
8415
+ * first. The email is set only for a linked, verified Junior user, because
8416
+ * that email is the People profile route.
8417
+ */
8418
+ async function readMentionedPeople(executor, tenantId, userIds) {
8419
+ const rows = await executor.db().select({
8420
+ displayName: juniorIdentities.displayName,
8421
+ emailVerified: juniorIdentities.emailVerified,
8422
+ handle: juniorIdentities.handle,
8423
+ providerSubjectId: juniorIdentities.providerSubjectId,
8424
+ userDisplayName: juniorUsers.displayName,
8425
+ userEmail: juniorUsers.primaryEmail
8426
+ }).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));
8427
+ const people = /* @__PURE__ */ new Map();
8428
+ for (const row of rows) {
8429
+ if (people.has(row.providerSubjectId)) continue;
8430
+ const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
8431
+ const email = row.emailVerified ? row.userEmail?.trim() : void 0;
8432
+ people.set(row.providerSubjectId, {
8433
+ slackUserId: row.providerSubjectId,
8434
+ ...fullName ? { fullName } : void 0,
8435
+ ...row.handle ? { slackUserName: row.handle } : void 0,
8436
+ ...email ? { email } : void 0
8437
+ });
8438
+ }
8439
+ const unnamed = userIds.filter((id) => {
8440
+ const person = people.get(id);
8441
+ return !person?.fullName && !person?.slackUserName;
8442
+ });
8443
+ if (tenantId && unnamed.length > 0) {
8444
+ const profiles = await Promise.all(unnamed.map((id) => lookupSlackUser(tenantId, id)));
8445
+ unnamed.forEach((id, index) => {
8446
+ const profile = profiles[index];
8447
+ if (!profile?.fullName && !profile?.userName) return;
8448
+ people.set(id, {
8449
+ ...people.get(id),
8450
+ slackUserId: id,
8451
+ ...profile.fullName ? { fullName: profile.fullName } : void 0,
8452
+ ...profile.userName ? { slackUserName: profile.userName } : void 0
8453
+ });
8454
+ });
8455
+ }
8456
+ return people;
8457
+ }
8458
+ // src/api/conversations/mentions.ts
8459
+ /** Text in one event that the dashboard renders with mentions. */
8460
+ function eventMentionTexts(data) {
8461
+ switch (data.type) {
8462
+ case "message": return data.redacted ? [] : [
8463
+ data.text,
8464
+ data.trustedSummary,
8465
+ ...(data.events ?? []).map((event) => event.trustedSummary)
8466
+ ];
8467
+ case "assistant_message": return data.parts.map((part) => part.text);
8468
+ case "compaction":
8469
+ case "handoff": return [data.summary];
8470
+ case "structured_event": return [data.presentation.preview, ...(data.presentation.details ?? []).map((detail) => detail.description)];
8471
+ default: return [];
8472
+ }
8473
+ }
8474
+ function briefMentionTexts(brief) {
8475
+ return [
8476
+ brief.summary,
8477
+ brief.intent,
8478
+ brief.outcome.text,
8479
+ ...brief.decisions.flatMap((decision) => [decision.text, decision.by]),
8480
+ ...brief.openDecisions.flatMap((decision) => [decision.text, decision.owner]),
8481
+ ...brief.facts
8482
+ ];
8483
+ }
8484
+ function withMentions(value, mentions) {
8485
+ return mentions.length > 0 ? {
8486
+ ...value,
8487
+ mentions
8488
+ } : value;
8489
+ }
8490
+ /**
8491
+ * Attach the people that each event and the Brief of one conversation
8492
+ * mention, resolved at read time.
8493
+ */
8494
+ async function attachConversationMentions(executor, conversationId, content) {
8495
+ const tenantId = (await loadConversationSlackTenantIds(executor, [conversationId])).get(conversationId);
8496
+ const sources = content.events.map((event) => ({
8497
+ tenantId,
8498
+ texts: eventMentionTexts(event.data)
8499
+ }));
8500
+ if (content.brief) sources.push({
8501
+ tenantId,
8502
+ texts: briefMentionTexts(content.brief)
8503
+ });
8504
+ const mentions = await resolveMentions(executor, sources);
8505
+ return {
8506
+ briefMentions: content.brief ? mentions.at(-1) : [],
8507
+ events: content.events.map((event, index) => withMentions(event, mentions[index]))
8508
+ };
8509
+ }
8510
+ /** Attach the people that each activity preview mentions, resolved at read time. */
8511
+ async function attachActivityPreviewMentions(executor, previews) {
8512
+ const entries = [...previews];
8513
+ const tenants = await loadConversationSlackTenantIds(executor, entries.map(([conversationId]) => conversationId));
8514
+ const mentions = await resolveMentions(executor, entries.map(([conversationId, preview]) => ({
8515
+ tenantId: tenants.get(conversationId),
8516
+ texts: [preview.text]
8517
+ })));
8518
+ return new Map(entries.map(([conversationId, preview], index) => [conversationId, withMentions(preview, mentions[index])]));
8519
+ }
8520
+ // src/api/conversations/participants.ts
8521
+ const identityColumns = {
8522
+ displayName: juniorIdentities.displayName,
8523
+ email: juniorIdentities.email,
8524
+ handle: juniorIdentities.handle,
8525
+ identityId: juniorIdentities.id,
8526
+ provider: juniorIdentities.provider,
8527
+ providerSubjectId: juniorIdentities.providerSubjectId,
8528
+ userDisplayName: juniorUsers.displayName,
8529
+ userEmail: juniorUsers.primaryEmail,
8530
+ userId: juniorIdentities.userId
8531
+ };
8532
+ function participantIdentity(row) {
8533
+ const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
8534
+ const email = row.email?.trim() || row.userEmail?.trim();
8535
+ const slack = row.provider === "slack";
8536
+ const participant = {
8537
+ ...email ? { email } : void 0,
8538
+ ...fullName ? { fullName } : void 0,
8539
+ ...slack && row.providerSubjectId ? { slackUserId: row.providerSubjectId } : void 0,
8540
+ ...slack && row.handle ? { slackUserName: row.handle } : void 0
8541
+ };
8542
+ return Object.keys(participant).length > 0 ? participant : void 0;
8543
+ }
8544
+ /** Read actor identities in first-appearance order for each Conversation. */
8545
+ async function readConversationParticipants(db, conversationIds) {
8546
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
8547
+ const [rootRows, eventRows] = await Promise.all([db.select({
8548
+ conversationId: juniorConversations.conversationId,
8549
+ ...identityColumns
8550
+ }).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], {
8551
+ conversationId: juniorConversationEvents.conversationId,
8552
+ seq: juniorConversationEvents.seq,
8553
+ ...identityColumns
8554
+ }).from(juniorConversationEvents).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(inArray(juniorConversationEvents.conversationId, [...conversationIds])).orderBy(asc(juniorConversationEvents.conversationId), asc(juniorConversationEvents.actorIdentityId), asc(juniorConversationEvents.seq))]);
8555
+ const ordered = /* @__PURE__ */ new Map();
8556
+ const add = (conversationId, row) => {
8557
+ const participant = participantIdentity(row);
8558
+ if (!participant) return;
8559
+ const group = ordered.get(conversationId) ?? {
8560
+ identities: [],
8561
+ keys: /* @__PURE__ */ new Set()
8562
+ };
8563
+ const key = row.userId ? `user:${row.userId}` : `identity:${row.identityId}`;
8564
+ if (!group.keys.has(key)) {
8565
+ group.keys.add(key);
8566
+ group.identities.push(participant);
8567
+ }
8568
+ ordered.set(conversationId, group);
8569
+ };
8570
+ for (const row of rootRows) add(row.conversationId, row);
8571
+ for (const row of eventRows.sort((left, right) => left.seq - right.seq)) add(row.conversationId, row);
8572
+ return new Map([...ordered].map(([conversationId, group]) => [conversationId, group.identities]));
8573
+ }
8574
+ // src/api/conversations/list.ts
8575
+ const CONVERSATION_FEED_LIMIT = 50;
8576
+ function conversationFeedMembershipFilter(status, filter) {
8577
+ if (!filter) return status === "archived" ? sql`false` : void 0;
8578
+ if (filter.kind === "viewer") return and(conversationIsMemberRoot({ userId: filter.userId }), status === "archived" ? conversationArchivedForUser(filter.userId) : status === "all" ? void 0 : conversationNotArchivedForUser(filter.userId));
8579
+ return and(conversationIsMemberRoot({ email: filter.email }), status === "archived" ? conversationArchivedForEmail(filter.email) : status === "all" ? void 0 : conversationNotArchivedForEmail(filter.email));
8580
+ }
8581
+ /**
8582
+ * Keep root conversations linked to one code repository. A code change can
8583
+ * link a child conversation, so linked ids resolve to their root. Private
8584
+ * roots stay visible only to their members.
8585
+ */
8586
+ function conversationCodeRepositoryFilter(repositoryId, viewerUserId) {
8587
+ return and(sql`${juniorConversations.conversationId} IN (
8588
+ SELECT coalesce(linked.root_conversation_id, linked.conversation_id)
8589
+ FROM ${juniorConversations} AS linked
8590
+ WHERE linked.conversation_id IN (
8591
+ SELECT unnest(${juniorCodeChanges.conversationIds})
8592
+ FROM ${juniorCodeChanges}
8593
+ WHERE ${juniorCodeChanges.repositoryId} = ${repositoryId}
8594
+ )
8595
+ )`, or(eq(juniorDestinations.visibility, "public"), viewerUserId ? viewerConversationMembership({
8596
+ actorMatch: eq(juniorIdentities.userId, viewerUserId),
8597
+ participantMatch: conversationHasParticipantUser(viewerUserId)
8598
+ }) : void 0));
8599
+ }
8600
+ async function conversationRows(db, limit, status, filter, query, includePrivateBriefs = false, codeRepository) {
8601
+ return db.select({
8602
+ conversation: conversationReadColumns(),
8603
+ location: juniorDestinations,
8604
+ identityDisplayName: juniorIdentities.displayName,
8605
+ identityEmail: juniorIdentities.email,
8606
+ identityHandle: juniorIdentities.handle,
8607
+ identityProvider: juniorIdentities.provider,
8608
+ identitySubjectId: juniorIdentities.providerSubjectId,
8609
+ identityTenantId: juniorIdentities.providerTenantId,
8610
+ userDisplayName: juniorUsers.displayName
8611
+ }).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 (
8612
+ select 1
8613
+ from ${juniorConversationBriefs}
8614
+ where ${juniorConversationBriefs.conversationId} = ${juniorConversations.conversationId}
8615
+ and ${includePrivateBriefs ? sql`true` : eq(juniorDestinations.visibility, "public")}
8616
+ and ${juniorConversationBriefs.version} = (
8617
+ select max(${juniorConversationBriefs.version})
8618
+ from ${juniorConversationBriefs}
8619
+ where ${juniorConversationBriefs.conversationId} = ${juniorConversations.conversationId}
8620
+ )
8621
+ and to_tsvector('english', ${juniorConversationBriefs.searchText})
8622
+ @@ websearch_to_tsquery('english', ${query})
8623
+ )`) : void 0)).orderBy(desc(juniorConversations.lastActivityAt), asc(juniorConversations.conversationId)).limit(limit);
8624
+ }
8625
+ /** Decode a conversation row with the linked user name and identity-scoped provider fields. */
8626
+ function conversationFromRow(row) {
8627
+ const value = row.conversation;
8628
+ const legacySessionSource = value.sessionSource === null ? void 0 : parseSessionSource(value.sessionSource);
8629
+ if (value.sessionSource !== null && !legacySessionSource) throw new Error("Conversation legacy source_json is invalid");
8630
+ const actorFullName = row.userDisplayName?.trim() ? row.userDisplayName : row.identityDisplayName;
8631
+ const actor = row.identityProvider === "slack" ? {
8632
+ platform: "slack",
8633
+ ...row.identityEmail ? { email: row.identityEmail } : void 0,
8634
+ ...actorFullName ? { fullName: actorFullName } : void 0,
8635
+ ...row.identitySubjectId ? { slackUserId: row.identitySubjectId } : void 0,
8636
+ ...row.identityHandle ? { slackUserName: row.identityHandle } : void 0,
8637
+ ...row.identityTenantId ? { teamId: row.identityTenantId } : void 0
8638
+ } : void 0;
8639
+ const location = locationFromRow(value.location, row.location, legacySessionSource);
8640
+ return {
8641
+ schemaVersion: 1,
8642
+ conversationId: value.conversationId,
8643
+ createdAtMs: value.createdAt.getTime(),
8644
+ lastActivityAtMs: value.lastActivityAt.getTime(),
8645
+ updatedAtMs: value.updatedAt.getTime(),
8646
+ execution: {
8647
+ status: value.executionStatus,
8648
+ ...value.runId ? { runId: value.runId } : void 0,
8649
+ ...value.executionUpdatedAt ? { updatedAtMs: value.executionUpdatedAt.getTime() } : void 0
8650
+ },
8651
+ ...actor ? { actor } : void 0,
8652
+ ...location ? { location } : void 0,
8653
+ ...value.channelName ? { channelName: value.channelName } : void 0,
8654
+ ...value.source ? { source: value.source } : void 0,
8655
+ ...value.title ? { title: value.title } : void 0,
8656
+ ...value.transcriptPurgedAt ? { transcriptPurgedAtMs: value.transcriptPurgedAt.getTime() } : void 0
8657
+ };
8658
+ }
8659
+ /** Read one normalized conversation record directly from its SQL row. */
8660
+ async function readConversationRecordFromSql(conversationId) {
8661
+ const row = (await getDb().select({
8662
+ conversation: conversationReadColumns(),
8663
+ location: juniorDestinations,
8664
+ identityDisplayName: juniorIdentities.displayName,
8665
+ identityEmail: juniorIdentities.email,
8666
+ identityHandle: juniorIdentities.handle,
8667
+ identityProvider: juniorIdentities.provider,
8668
+ identitySubjectId: juniorIdentities.providerSubjectId,
8669
+ identityTenantId: juniorIdentities.providerTenantId,
8670
+ userDisplayName: juniorUsers.displayName
8671
+ }).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];
8672
+ return row ? {
8673
+ conversation: conversationFromRow(row),
8674
+ durationMs: row.conversation.durationMs,
8675
+ ...row.location?.visibility === "public" ? { locationId: row.location.id } : void 0,
8676
+ usage: row.conversation.usage,
8677
+ rootConversationId: row.conversation.rootConversationId
8678
+ } : void 0;
8679
+ }
8680
+ /** Prefer the authenticated viewer; fall back to actor email membership. */
8681
+ function conversationFeedFilter(options) {
8682
+ if (options.viewer) return {
8683
+ kind: "viewer",
8684
+ userId: options.viewer.id
8685
+ };
8686
+ if (options.actorEmail) return {
8687
+ kind: "actorEmail",
8688
+ email: options.actorEmail
8689
+ };
8690
+ }
8691
+ /**
8692
+ * Build a bounded dashboard feed. Prefer the viewer user when present; otherwise
8693
+ * keep only roots linked to actorEmail before applying the limit. Membership is
8694
+ * root-actor ownership or a materialized participant row for that person.
8695
+ *
8696
+ * `codeRepositoryId` replaces membership with every root linked to that
8697
+ * repository's code changes. Private roots appear only for their members, and
8698
+ * per-person archive state does not hide rows.
8699
+ */
8700
+ async function readConversationFeedFromSql(options = {}) {
8701
+ const nowMs = Date.now();
8702
+ const db = getDb();
8703
+ const filter = options.codeRepositoryId ? void 0 : conversationFeedFilter(options);
8704
+ const query = options.q?.trim().toLowerCase() || void 0;
8705
+ const rows = await conversationRows(db, options.limit ?? CONVERSATION_FEED_LIMIT, query || options.codeRepositoryId ? "all" : options.status ?? "active", filter, query, filter?.kind === "viewer", options.codeRepositoryId ? {
8706
+ id: options.codeRepositoryId,
8707
+ ...options.viewer ? { viewerUserId: options.viewer.id } : void 0
8708
+ } : void 0);
8709
+ const conversations = rows.map((row) => conversationFromRow(row));
8710
+ const conversationIds = conversations.map((conversation) => conversation.conversationId);
8711
+ const [accessByConversation, archiveTimesByConversation, annotationsByConversation, auxiliaryCostsByRoot, metricsByRoot, participantsByConversation, teamDomainByTeamId, conversationWork, lastUserMessageAtByConversation, activityPreviewByConversation, automationTitleByConversation, firstHumanMessageTextByConversation] = await Promise.all([
8712
+ readConversationAccessFromSql(db, conversationIds, options.viewer),
8713
+ conversationArchiveTimes(db, conversationIds, filter?.kind === "viewer" ? { userId: filter.userId } : filter?.kind === "actorEmail" ? { email: filter.email } : void 0),
8714
+ listConversationAnnotationsById(db, conversationIds),
8715
+ readConversationAuxiliaryCostsFromSql(db, conversationIds),
8716
+ readConversationTreeMetricsFromSql(db, conversationIds),
8717
+ readConversationParticipants(db, conversationIds),
8718
+ resolveSlackTeamDomains(conversations.flatMap((conversation) => conversation.location?.provider === "slack" ? [conversation.location.teamId] : [])),
8719
+ listConversationWork(conversationIds),
8720
+ readLastUserMessageAtByConversation(db, conversationIds),
8721
+ readConversationActivityPreviews(db, conversationIds),
8722
+ readAutomationTitlesByConversationId(conversations),
8723
+ readFirstHumanMessageTexts(db, conversations)
8724
+ ]);
8725
+ const visibleActivityPreviews = await attachActivityPreviewMentions(getSqlExecutor(), new Map([...activityPreviewByConversation].filter(([conversationId]) => accessByConversation.get(conversationId)?.canViewPrivateContent)));
8726
+ const sidebarAnnotationsByConversation = await listConversationSidebarAnnotations(conversationIds, new Map(conversationIds.map((conversationId) => [conversationId, accessByConversation.get(conversationId)?.canViewPrivateContent ? annotationsByConversation.get(conversationId) ?? [] : []])));
8727
+ const assignedWork = new Set(conversationWork.assignedIds);
8728
+ const unfinishedWork = new Set(conversationWork.unfinishedIds);
8729
+ return {
8730
+ conversations: conversations.map((conversation, index) => {
8731
+ const row = rows[index];
8732
+ const access = accessByConversation.get(conversation.conversationId);
8733
+ const metrics = metricsByRoot.get(conversation.conversationId);
8734
+ const auxiliaryCosts = auxiliaryCostsByRoot.get(conversation.conversationId);
8735
+ const childAgentTotals = childAgentTotalsReport(metrics, auxiliaryCosts);
8736
+ const archivedAtMs = archiveTimesByConversation.get(conversation.conversationId);
8737
+ const summary = conversationSummaryFromStoredConversation({
8738
+ ...archivedAtMs === void 0 ? void 0 : { archivedAtMs },
8739
+ automationTitle: automationTitleByConversation.get(conversation.conversationId),
8740
+ conversation,
8741
+ access,
8742
+ auxiliaryCosts: auxiliaryCosts?.total,
8743
+ ...childAgentTotals ? { childAgentTotals } : void 0,
8744
+ durationMs: metrics?.durationMs ?? row.conversation.durationMs,
8745
+ firstHumanMessageText: firstHumanMessageTextByConversation.get(conversation.conversationId),
8746
+ teamDomainByTeamId,
8747
+ ...row.location?.visibility === "public" ? { locationId: row.location.id } : void 0,
8748
+ usage: metrics?.usage ?? row.conversation.usage ?? void 0
8749
+ });
8750
+ const work = {
8751
+ ...assignedWork.has(conversation.conversationId) ? { assignedWork: true } : void 0,
8752
+ ...conversationWork.finishedAtById[conversation.conversationId] ? { finishedWorkAt: conversationWork.finishedAtById[conversation.conversationId] } : void 0,
8753
+ ...unfinishedWork.has(conversation.conversationId) ? { unfinishedWork: true } : void 0
8754
+ };
8755
+ const annotations = access?.canViewPrivateContent ? annotationsByConversation.get(conversation.conversationId) ?? [] : [];
8756
+ const activityPreview = visibleActivityPreviews.get(conversation.conversationId);
8757
+ const participants = access?.canViewPrivateContent ? participantsByConversation.get(conversation.conversationId) : void 0;
8758
+ return {
8759
+ ...summary,
8760
+ ...work,
8761
+ ...participants && participants.length > 0 ? { participants } : void 0,
8762
+ ...activityPreview ? { activityPreview } : void 0,
8763
+ ...annotations.length > 0 ? { annotations } : void 0,
8764
+ ...sidebarAnnotationsByConversation[conversation.conversationId] ? { sidebarAnnotations: sidebarAnnotationsByConversation[conversation.conversationId] } : void 0,
8765
+ isPriority: isConversationPriority({
8766
+ lastSeenAt: summary.lastSeenAt,
8767
+ lastUserMessageAt: lastUserMessageAtByConversation.get(conversation.conversationId),
8768
+ ...work
8769
+ }, nowMs)
8770
+ };
8771
+ }),
8772
+ generatedAt: new Date(nowMs).toISOString(),
8773
+ source: "conversation_index"
8774
+ };
8775
+ }
8776
+ /**
8777
+ * Load a bounded feed with an optional viewer-primary linked-user filter.
8778
+ *
8779
+ * With `viewer` and without `codeRepositoryId`, the feed holds only root
8780
+ * Conversations that the viewer started or takes part in, for every `q` and
8781
+ * `status`. The `actorEmail` filter and `codeRepositoryId` give no such
8782
+ * guarantee, so they are not an authorization boundary.
8783
+ */
8784
+ async function readConversationFeed(options = {}) {
8785
+ return conversationFeedSchema.parse(await readConversationFeedFromSql(options));
8786
+ }
8787
+ export { renderSlackObjectCard as $, readViewerAutomationRuns as A, recordSkippedConversationMessage as B, createProductionSlackWebhookServices as C, deleteViewerTask as D, ViewerTaskNotFoundError as E, assertNoLegacyAllowedDomains as F, normalizeIncomingSlackThreadId as G, clearSlackPendingReactions as H, isAllowedDomain as I, THREAD_OPTOUT_ACK as J, withNormalizedThreadId as K, isAllowedSignInEmail as L, readViewerAutomationVersions as M, readViewerAutomations as N, readConversationSourceTask as O, resolveViewerTaskCandidate as P, verifySlackSignature as Q, readEnvList as R, createProductionConversationWorkOptions as S, createSlackWebApiStatusSender as T, isSlackMessageStopped as U, buildSlackInboundMessage as V, stopSlackThread as W, parseContent as X, startProcessingReaction as Y, runWithSlackInstallation as Z, appendAndEnqueueWebMessage as _, resolveMentions as a, recordWebConversationActivity as b, readConversationAuxiliaryCostsFromSql as c, notCopiedByFork as d, getAttachmentStorage as et, childAgentTotalsReport as f, resolveSlackTeamDomains as g, conversationSummaryFromStoredConversation as h, attachConversationMentions as i, readHealthReport as it, readViewerAutomationSummary as j, readViewerAutomationExecutions as k, ConversationForkError as l, conversationEventHistory as m, readConversationRecordFromSql as n, enqueueAgentDispatch as nt, readFirstHumanMessageTexts as o, readConversationTreeMetricsFromSql as p, stopConversationTurn as q, readConversationParticipants as r, GET as rt, listConversationSidebarAnnotations as s, readConversationFeed as t, AGENT_DISPATCH_MAX_AGE_MS as tt, forkConversation as u, conversationTurnIdForMessage as v, getProductionSlackAdapter as w, webActorFromEmail as x, createAndEnqueueConversation as y, resolveAllowedDomains as z };