@sentry/junior 0.272.0 → 0.273.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 (70) hide show
  1. package/dist/{adapter-BDYDi1F9.js → adapter-umap1cth.js} +1 -1
  2. package/dist/{admin-Dj7EFycG.js → admin-0o6E_3lo.js} +1 -1
  3. package/dist/{agent-runner-Cl6Usx2Y.js → agent-runner-D7Qz-lGO.js} +517 -298
  4. package/dist/api/schema.d.ts +1 -1
  5. package/dist/api/schema.js +4 -4
  6. package/dist/api.d.ts +1 -1
  7. package/dist/api.js +56 -573
  8. package/dist/{app-DjZblE2J.d.ts → app-BcyhJq8x.d.ts} +4 -9
  9. package/dist/app.d.ts +1 -1
  10. package/dist/app.js +673 -127
  11. package/dist/{assistant-reply-DN2w6JL_.js → assistant-reply-wZ7i4OFa.js} +1 -1
  12. package/dist/{automation-CV7g5h3A.js → automation-yffXcZt9.js} +1 -1
  13. package/dist/{briefs-PysAqnjV.js → briefs-CgMnlpIp.js} +4 -4
  14. package/dist/{catalog-runtime-BNeCWIaF.js → catalog-runtime-TygDsPOU.js} +1 -1
  15. package/dist/cli/chat.js +15 -15
  16. package/dist/cli/check.js +3 -3
  17. package/dist/cli/init.js +1 -1
  18. package/dist/cli/main.js +3 -3
  19. package/dist/cli/plugins.js +4 -4
  20. package/dist/cli/snapshot-create.js +4 -4
  21. package/dist/cli/upgrade.d.ts +1 -1
  22. package/dist/cli/upgrade.js +3 -3
  23. package/dist/{client-FRphcDWW.js → client-DhqbV0Fe.js} +63 -5
  24. package/dist/{reporting-schema-CZREhYc-.js → common-Eu6sx09c.js} +4 -1
  25. package/dist/{config-BUP9wjiR.js → config-C6FTd7PX.js} +6 -14
  26. package/dist/{connections-CesHau9u.js → connections-Dq72KajR.js} +11 -11
  27. package/dist/{conversation-BtQzLoK8.js → conversation-T4oabbBi.js} +10 -0
  28. package/dist/{conversation-privacy-DiGvA9a3.js → conversation-privacy-BeRgrsph.js} +1 -1
  29. package/dist/{credential-sync-D2odJC30.js → credential-sync-BmCbHAp4.js} +15 -16
  30. package/dist/{db-BPLmOwXf.d.ts → db-DOzBGLn5.d.ts} +1 -1
  31. package/dist/{executor-DGDpyegI.js → executor-BafgvQcc.js} +2 -2
  32. package/dist/instrumentation.js +114 -3
  33. package/dist/{job-queue-CjB97sii.js → job-queue-C4lphW9Y.js} +3 -3
  34. package/dist/{logging-CjkDr-jP.js → logging-CNs1e04n.js} +6 -1
  35. package/dist/{message-D9y8PKA1.js → message-CETC5lsl.js} +9 -9
  36. package/dist/nitro.d.ts +1 -1
  37. package/dist/nitro.js +3 -3
  38. package/dist/{oauth-callback-server-02Pej6hj.js → oauth-callback-server-DcQZsKMm.js} +2 -2
  39. package/dist/{credentialed-CgTulRDH.js → projection-BOpi891B.js} +4576 -49
  40. package/dist/{prompt-vfceH0c-.js → prompt-B1ySjUyf.js} +1 -1
  41. package/dist/{registry-7XgwNgQe.js → registry-B-yUrv3h.js} +2 -2
  42. package/dist/{resolve-CuuxNfWG.js → resolve-DqVtd4Q9.js} +3 -3
  43. package/dist/{result-BRhb6iN1.js → result-DQwTiLTw.js} +9 -9
  44. package/dist/{runner-DaWCofuS.js → runner-Dco5qMq6.js} +11 -11
  45. package/dist/{sandbox-egress-signals-Bn6FA1UC.js → sandbox-egress-signals-DFc_9Mcv.js} +1 -1
  46. package/dist/{schema-D5VLF4NT.js → schema-5xHDGlBW.js} +9 -7
  47. package/dist/{schema-CNjAzYOw.d.ts → schema-9OGdsaxF.d.ts} +13 -0
  48. package/dist/{service-account-D9vrsi8d.js → service-account-CWfk9Cs-.js} +3 -3
  49. package/dist/{service-account-H45LIAG2.js → service-account-smdPFWNy.js} +3 -3
  50. package/dist/{session-stop-HKl0ECX0.js → session-stop-DaT_7tYa.js} +11 -11
  51. package/dist/{skills-CsiKu3-g.js → skills-BbtJSF2-.js} +2 -2
  52. package/dist/{spawn-DqjBxqWh.js → spawn-Ceikdjkp.js} +2 -2
  53. package/dist/{list-JZUP1yog.js → stop-dKUc-x2K.js} +996 -432
  54. package/dist/{store-DyVKH0QB.js → store-C1xuZcLd.js} +4 -4
  55. package/dist/{store-CPF2kzbu.js → store-DaxUuOmD.js} +3 -3
  56. package/dist/{task-queue-BbQXCcNX.js → task-queue-ymAh_-P1.js} +4 -4
  57. package/dist/{task-runner-ChzW5E7R.js → task-runner-vlH6pEm3.js} +6 -6
  58. package/dist/{thread-state-Ds8JTsX6.js → thread-state-BkUpE9Ae.js} +4 -5
  59. package/dist/{turn-failure-response-DCMvjsA0.js → turn-failure-response-DIsD7ip_.js} +7 -8
  60. package/dist/{turn-lifecycle-DM_77q33.js → turn-lifecycle-BdPKdPS8.js} +1 -1
  61. package/dist/turn-session-routing-BdCTDq9m.js +1625 -0
  62. package/dist/{validation-DcGelbBi.js → validation-BQjCfh84.js} +1 -1
  63. package/dist/{version-C48azPKq.js → version-BpiFGZZW.js} +1 -1
  64. package/dist/version.js +1 -1
  65. package/dist/{viewer-BCRuO8Wg.js → viewer-C6Y1aXjZ.js} +27 -15
  66. package/dist/{work-zxkmbJAp.js → work-ChPh_YEh.js} +10 -38
  67. package/dist/{worker-CNNxHF1V.js → worker-Be_sK_ou.js} +6 -6
  68. package/package.json +4 -4
  69. package/dist/turn-session-routing-BV_gX7n3.js +0 -6072
  70. package/dist/{oauth-relay-CTygMkNx.js → oauth-relay-C6ZAyJgf.js} +1 -1
@@ -1,45 +1,46 @@
1
- import { $t as appendThreadContextMessages, An as recordTurnSummary, Ar as trimTrailingAssistantMessages, At as loadProjection, Cn as loadTurnCheckpoint, Dn as failTurnRecord, En as completeTurnRecord, Et as commitMessages, Fr as buildDeterministicTurnId, Gt as hydrateConversationMessages, Jt as persistConversationMessageSummaries, Kt as persistConversationMessages, Lr as decideSubscribedThreadReply, Lt as recordAuthorizationCompleted, On as getTurnRecord, Ot as loadConversationProjection, Pr as buildDeterministicAssistantMessageId, Rr as getSubscribedReplyPreflightDecision, Tn as abandonTurnRecord, Tt as commitAcceptedReply, Vt as recordSubscribedReplyRoute, Wr as isExperimentalFeatureEnabled, Yr as piMessageFromHistoryItem, Yt as resolveConversationTitle, Zt as fallbackShortTitle, _ as markDispatchCompleted, a as confirmDispatchMailboxAppend, an as markConversationMessage, cn as turnHasReply, d as getDispatchTurnId, dn as PluginCredentialFailureError, en as buildConversationContext, fr as buildTurnErrorResponse, g as markDispatchBlocked, gr as persistWithRetry, h as markDispatchAwaitingResume, hn as AuthorizationFlowDisabledError, i as claimDispatchMailboxAppend, in as isHumanConversationMessage, j as readAutomationTitlesByConversationId, jt as loadTurnProjection, kn as listTurnSummaries, l as getDispatchInputMessageId, ln as upsertConversationMessage, n as resolveTurnSessionRouting, nn as estimateConversationContextTokens, on as normalizeConversationText, p as isTerminalDispatchStatus, qr as configValueSchema, rn as getConversationMessageSlackTs, s as getDispatchConversationId, sn as recordDeliveredAssistantMessage, t as resolveConversationRouting, tn as createConversationMemoryService, u as getDispatchRecord, v as markDispatchFailed, wn as saveTurnCheckpoint, xn as wasPendingAuthStopped, y as markDispatchRunning, yn as clearPendingAuth, zr as getThreadStopDecision } from "./turn-session-routing-BV_gX7n3.js";
2
- import { a as completeText, p as escapeXml, r as completeDecision, y as isProviderRetryError } from "./client-FRphcDWW.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-BUP9wjiR.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-CjkDr-jP.js";
1
+ import { $ as loadTurnCheckpoint, $a as configValueSchema, A as buildConversationContext, Aa as conversationNotArchivedForEmail, Bi as verifyTimerWatchCredentialSubject, Bn as closeReviewItem, Br as addReactionToMessage, Ca as createPluginLogger, D as fallbackShortTitle, Da as conversationArchivedForUser, Ea as conversationArchivedForEmail, F as markConversationMessage, Fa as readConversationEventPage, G as AuthorizationFlowDisabledError, Gi as getSlackClient, Ha as recordSlackAttachment, Hi as downloadPrivateSlackFile, Ht as buildTurnErrorResponse, I as normalizeConversationText, Ia as buildSentryConversationUrl, Ji as normalizeSlackConversationId, Ka as listConversationAnnotations, Kn as listReplyActionItems, Kt as persistWithRetry, L as recordDeliveredAssistantMessage, Lr as normalizeSlackStatusText, M as estimateConversationContextTokens, Ma as viewerConversationMembership, N as getConversationMessageSlackTs, Nn as getPlugins, Oa as conversationHasParticipantUser, P as isHumanConversationMessage, Pa as readLatestConversationBrief, Qn as canActOnReviewItem, R as turnHasReply, Ra as notInheritedByForkChild, S as persistConversationMessages, Sn as createUserTokenStore, T as resolveConversationTitle, Ta as conversationArchiveTimes, V as PluginCredentialFailureError, Va as readAttachmentVisionSummary, Vi as SlackActionError, Wi as getHeaderString, Wn as getReviewItem, Xa as lookupSlackUser, Xi as withSlackRetries, Y as clearPendingAuth, Yn as takeReviewItem, Z as wasPendingAuthStopped, Za as createPluginCache, _ as recordSubscribedReplyRoute, ar as reviewItemReaderForUser, at as listTurnSummaries, bn as isExperimentalFeatureEnabled, dn as buildDeterministicTurnId, ea as JUNIOR_THREAD_STATE_TTL_MS, er as canReadReviewItem, et as saveTurnCheckpoint, hn as getThreadStopDecision, i as loadConversationProjection, it as getTurnRecord, j as createConversationMemoryService, ja as conversationNotArchivedForUser, k as appendThreadContextMessages, ka as conversationIsMemberRoot, mn as getSubscribedReplyPreflightDecision, n as commitMessages, nt as completeTurnRecord, o as loadProjection, on as trimTrailingAssistantMessages, ot as recordTurnSummary, p as recordAuthorizationCompleted, pn as decideSubscribedThreadReply, qa as listConversationAnnotationsById, qi as isDmChannel, qr as removeReactionFromMessage, rt as failTurnRecord, s as loadTurnProjection, t as commitAcceptedReply, to as piMessageFromHistoryItem, tr as isOnlyAssignee, tt as abandonTurnRecord, un as buildDeterministicAssistantMessageId, w as persistConversationMessageSummaries, wa as readConversationAccessFromSql, wi as cancelSubscriptions, x as hydrateConversationMessages, z as upsertConversationMessage, za as getSlackFileId, zr as truncateStatusText } from "./projection-BOpi891B.js";
2
+ import { a as completeText, b as isProviderRetryError, m as escapeXml, r as completeDecision } from "./client-DhqbV0Fe.js";
3
+ import { _ as normalizeSlackEmojiName, c as getSlackClientSecret, i as getChatConfig, l as getSlackSigningSecret, n as botConfig, o as getSlackBotToken, s as getSlackClientId, v as createS3AttachmentStorage } from "./config-C6FTd7PX.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-CNs1e04n.js";
5
5
  import { i as getActiveSpan, r as continueTrace } from "./sentry-MDkYAVPV.js";
6
6
  import { i as defaultModelId, r as boundProfile, s as modelIdForProfile } from "./model-profile-B998sjVd.js";
7
7
  import { f as parseSlackTeamId, h as parseSlackMessageTs, i as isActorUserId, n as createSlackActor, o as isUserActor, r as createSlackResumeActor, s as parseActorUserId, t as createActor, u as parseSlackChannelId } from "./actor-B4DYygOB.js";
8
- import { C as getSqlExecutor, E as loadConversationSlackTenantIds, H as parseSessionSource, M as resolveActorIdentityId, N as saveActorAvatar, S as getDb, T as withConversationEventLock, U as addAgentTurnUsage, Y as createSlackDestination, Z as requireSlackDestination, f as identityAvatarUrl, h as readUserAvatarUrls, k as locationFromRow, q as hasAgentTurnUsage, x as getConversationStore, y as getConversationEventStore } from "./viewer-BCRuO8Wg.js";
8
+ import { A as locationFromRow, C as getSqlExecutor, D as loadConversationSlackTenantIds, E as withConversationEventLock, J as hasAgentTurnUsage, N as resolveActorIdentityId, P as saveActorAvatar, Q as requireSlackDestination, S as getDb, U as parseSessionSource, W as addAgentTurnUsage, X as createSlackDestination, Y as resolveRootConversationId, f as identityAvatarUrl, h as readUserAvatarUrls, q as agentTurnUsageSchema, x as getConversationStore, y as getConversationEventStore } from "./viewer-C6Y1aXjZ.js";
9
9
  import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, o as conversationReadColumns, s as juniorConversations } from "./agent-invocations-C1RNOCs8.js";
10
- import { C as juniorCodeChanges, _ as juniorConversationBriefs, b as juniorConversationEvents, h as juniorConversationParticipants } from "./executor-DGDpyegI.js";
10
+ import { C as juniorCodeChanges, _ as juniorConversationBriefs, b as juniorConversationEvents, h as juniorConversationParticipants, m as juniorConversationReadPositions } from "./executor-BafgvQcc.js";
11
11
  import { g as instructionProvenanceFor, m as contextProvenance, r as isVisionImageMediaType } from "./media-U6Tuz6Mi.js";
12
- import { d as actorLogContext, f as credentialContextForActor } from "./registry-7XgwNgQe.js";
13
- import { Ct as conversationSubtree, Dt as buildSteeringPiMessage, F as getConversationInfo, Ft as shouldEmitDevAgentTrace, L as listThreadReplies, M as readSlackRawMessageContext, N as resolveSlackChannelIdFromMessage, Ot as withoutSavedSteeringMessages, P as resolveSlackChannelIdFromThreadId, R as resolveDestinationVisibility, b as renderBlockText, ct as createConversationId, dt as resolveConversationDestination, gt as storeAttachment, i as buildAuthPauseResponse, j as parseSlackThreadId, jt as RetryableDeliveryError, k as runWithSlackInstallation, lt as recordWebConversationActivity, n as executeAgentRun, nt as deliverReviewItemChange, p as latestProgressStatus, r as createContextCompactor, t as createAgentRunner, ut as webActorFromEmail, wt as conversationSubtreeColumns, y as renderAttachmentText } from "./agent-runner-Cl6Usx2Y.js";
14
- import { An as normalizeSlackConversationId, Ar as lookupSlackUser, Cn as SlackActionError, Dn as getSlackClient, Dr as listConversationAnnotationsById, Dt as removeReactionFromMessage, E as listReplyActionItems, En as getHeaderString, F as isOnlyAssignee, In as JUNIOR_THREAD_STATE_TTL_MS, M as canActOnReviewItem, Mn as withSlackRetries, P as canReadReviewItem, Sn as verifyTimerWatchCredentialSubject, Sr as recordSlackAttachment, _t as normalizeSlackStatusText, ar as createPluginLogger, b as closeReviewItem, bt as addReactionToMessage, cr as conversationArchivedForEmail, dr as conversationIsMemberRoot, fr as conversationNotArchivedForEmail, jr as createPluginCache, k as takeReviewItem, kn as isDmChannel, lr as conversationArchivedForUser, mr as viewerConversationMembership, n as createUserTokenStore, on as cancelSubscriptions, or as readConversationAccessFromSql, p as getPlugins, pr as conversationNotArchivedForUser, sr as conversationArchiveTimes, ur as conversationHasParticipantUser, w as getReviewItem, wn as downloadPrivateSlackFile, xr as readAttachmentVisionSummary, yr as getSlackFileId, yt as truncateStatusText, z as reviewItemReaderForUser } from "./credentialed-CgTulRDH.js";
12
+ import { d as actorLogContext, f as credentialContextForActor } from "./registry-B-yUrv3h.js";
13
+ import { Ct as conversationSubtree, Dt as buildSteeringPiMessage, F as getConversationInfo, Ft as shouldEmitDevAgentTrace, L as listThreadReplies, M as readSlackRawMessageContext, N as resolveSlackChannelIdFromMessage, Ot as withoutSavedSteeringMessages, P as resolveSlackChannelIdFromThreadId, R as resolveDestinationVisibility, b as renderBlockText, ct as createConversationId, dt as resolveConversationDestination, gt as storeAttachment, i as buildAuthPauseResponse, j as parseSlackThreadId, jt as RetryableDeliveryError, k as runWithSlackInstallation, lt as recordWebConversationActivity, n as executeAgentRun, nt as deliverReviewItemChange, p as latestProgressStatus, q as readConversationSourceTask, r as createContextCompactor, t as createAgentRunner, ut as webActorFromEmail, wt as conversationSubtreeColumns, y as renderAttachmentText } from "./agent-runner-D7Qz-lGO.js";
15
14
  import { a as withLock, n as MUTATION_LOCK_TTL_MS, r as acquireActiveLock } from "./locks-D-0h111u.js";
16
- import { r as getStateAdapter } from "./adapter-BDYDi1F9.js";
17
- import { G as readMessageAttachments, g as conversationFeedSchema } from "./conversation-BtQzLoK8.js";
18
- import { a as EVENT_SYSTEM_ACTOR, o as isEventConversationMessage } from "./prompt-vfceH0c-.js";
19
- import { $ as startActiveTurn, G as TurnInputDeferredError, H as ConversationTurnBoundaryError, J as isTurnInputCommitLostError, K as getConversationTurnBoundaryError, Q as markTurnFailed, U as CooperativeTurnYieldError, W as TurnInputCommitLostError, X as markTurnClosed, Y as isTurnInputDeferredError, Z as markTurnCompleted, q as isCooperativeTurnYieldError } from "./store-DyVKH0QB.js";
20
- import { n as getAssistantReplyText } from "./assistant-reply-DN2w6JL_.js";
21
- import { A as recordFinishedTurnForAutomatedLimit, D as sendSlackReply, E as resolveSlackReplyTarget, F as finishedRunReply, I as postsRunNotices, P as deliversFinalText, S as joinMailboxText, c as isEventMailboxMetadata, i as skipInputBeforeConversationStop, l as renderEventBatchText, s as eventBatchMessageEvents, u as timerWatchCredentialSubject, w as maybePostAutomatedTurnLimitNotice, z as runDispatchOutcome } from "./session-stop-HKl0ECX0.js";
15
+ import { r as getStateAdapter } from "./adapter-umap1cth.js";
16
+ import { G as readMessageAttachments, d as conversationDetailReportSchema, g as conversationFeedSchema } from "./conversation-T4oabbBi.js";
17
+ import { a as EVENT_SYSTEM_ACTOR, o as isEventConversationMessage } from "./prompt-B1ySjUyf.js";
18
+ import { $ as startActiveTurn, G as TurnInputDeferredError, H as ConversationTurnBoundaryError, J as isTurnInputCommitLostError, K as getConversationTurnBoundaryError, Q as markTurnFailed, U as CooperativeTurnYieldError, W as TurnInputCommitLostError, X as markTurnClosed, Y as isTurnInputDeferredError, Z as markTurnCompleted, m as getChildAgentLabel, q as isCooperativeTurnYieldError } from "./store-C1xuZcLd.js";
19
+ import { n as getAssistantReplyText } from "./assistant-reply-wZ7i4OFa.js";
20
+ import { A as recordFinishedTurnForAutomatedLimit, D as sendSlackReply, E as resolveSlackReplyTarget, F as finishedRunReply, I as postsRunNotices, P as deliversFinalText, S as joinMailboxText, c as isEventMailboxMetadata, i as skipInputBeforeConversationStop, l as renderEventBatchText, s as eventBatchMessageEvents, u as timerWatchCredentialSubject, w as maybePostAutomatedTurnLimitNotice, z as runDispatchOutcome } from "./session-stop-DaT_7tYa.js";
22
21
  import { _ as isEmptyUserInputAnswer, v as normalizeUserInputAnswer } from "./reporting-window-DgHgwQB3.js";
23
- import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline } from "./job-queue-CjB97sii.js";
24
- import { D as stopConversationWork, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, r as appendAndEnqueueInboundMessage, w as requestConversationWork } from "./store-CPF2kzbu.js";
25
- import { t as healthReportSchema } from "./reporting-schema-CZREhYc-.js";
26
- import { a as withAgentInvocationResultActors, c as listActiveMailboxTurns, d as turnInputFactsFromConversationMessage, f as legacyWebMailboxMetadataSchema, l as resolveMailboxTurnWork, n as enqueueAgentInvocationResult, o as getAuthPausedConversationTurnId, s as getEventBatchDelayMs, t as createChildConversationWorker, u as sourceFromTurnInput } from "./result-BRhb6iN1.js";
27
- import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-DM_77q33.js";
28
- import { a as AgentRunError, o as executeTurn } from "./work-zxkmbJAp.js";
29
- import { a as persistThreadStateById, i as persistThreadRuntimeState, n as getPersistedSandboxState, o as coerceThreadConversationState, r as getPersistedThreadState, t as getLocationConfigurationService } from "./thread-state-Ds8JTsX6.js";
30
- import { c as deleteWebAuthorization, d as createPausedTurns, f as getPausedTurnRequest, h as getTurnAuthorization, m as wakePausedTurn, s as createWebAuthorization } from "./connections-CesHau9u.js";
31
- import { t as bindMessageAgent } from "./message-D9y8PKA1.js";
32
- import { t as bindSpawnAgent } from "./spawn-DqjBxqWh.js";
33
- import { n as scheduleSessionCompletedPluginTasks, t as processPluginTask } from "./task-runner-ChzW5E7R.js";
34
- import { a as persistAuthPauseTurnState, c as getTurnUserSlackMessageTs, i as completeAuthPauseTurn, l as buildDeliveredTurnStatePatch, n as getAgentTurnDiagnosticsAttributes, o as getTurnUserMessage, r as requireTurnFailureEventId, s as getTurnUserReplyAttachmentContext, t as finalizeFailedTurnReplyWithEvent, u as loadPendingMessageCards } from "./turn-failure-response-DCMvjsA0.js";
22
+ import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline } from "./job-queue-C4lphW9Y.js";
23
+ import { D as stopConversationWork, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, r as appendAndEnqueueInboundMessage, w as requestConversationWork } from "./store-DaxUuOmD.js";
24
+ import { _ as markDispatchCompleted, a as confirmDispatchMailboxAppend, d as getDispatchTurnId, g as markDispatchBlocked, h as markDispatchAwaitingResume, i as claimDispatchMailboxAppend, j as readAutomationTitlesByConversationId, l as getDispatchInputMessageId, n as resolveTurnSessionRouting, p as isTerminalDispatchStatus, s as getDispatchConversationId, t as resolveConversationRouting, u as getDispatchRecord, v as markDispatchFailed, y as markDispatchRunning } from "./turn-session-routing-BdCTDq9m.js";
25
+ import { n as healthReportSchema, t as apiErrorSchema } from "./common-Eu6sx09c.js";
26
+ import { a as withAgentInvocationResultActors, c as listActiveMailboxTurns, d as turnInputFactsFromConversationMessage, f as legacyWebMailboxMetadataSchema, l as resolveMailboxTurnWork, n as enqueueAgentInvocationResult, o as getAuthPausedConversationTurnId, s as getEventBatchDelayMs, t as createChildConversationWorker, u as sourceFromTurnInput } from "./result-DQwTiLTw.js";
27
+ import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-BdPKdPS8.js";
28
+ import { a as AgentRunError, o as executeTurn } from "./work-ChPh_YEh.js";
29
+ import { a as persistThreadStateById, i as persistThreadRuntimeState, n as getPersistedSandboxState, o as coerceThreadConversationState, r as getPersistedThreadState, t as getLocationConfigurationService } from "./thread-state-BkUpE9Ae.js";
30
+ import { c as deleteWebAuthorization, d as createPausedTurns, f as getPausedTurnRequest, h as getTurnAuthorization, m as wakePausedTurn, s as createWebAuthorization } from "./connections-Dq72KajR.js";
31
+ import { t as bindMessageAgent } from "./message-CETC5lsl.js";
32
+ import { t as bindSpawnAgent } from "./spawn-Ceikdjkp.js";
33
+ import { n as scheduleSessionCompletedPluginTasks, t as processPluginTask } from "./task-runner-vlH6pEm3.js";
34
+ import { a as persistAuthPauseTurnState, c as getTurnUserSlackMessageTs, i as completeAuthPauseTurn, l as buildDeliveredTurnStatePatch, n as getAgentTurnDiagnosticsAttributes, o as getTurnUserMessage, r as requireTurnFailureEventId, s as getTurnUserReplyAttachmentContext, t as finalizeFailedTurnReplyWithEvent, u as loadPendingMessageCards } from "./turn-failure-response-DIsD7ip_.js";
35
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
35
36
  import { z } from "zod";
36
- import { createHash } from "node:crypto";
37
- import { and, asc, desc, eq, inArray, isNotNull, isNull, or, sql } from "drizzle-orm";
37
+ import { and, asc, desc, eq, inArray, isNotNull, isNull, max, or, sql } from "drizzle-orm";
38
38
  import { conversationSidebarAnnotationSchema, createSlackSource, objectTypeSchema } from "@sentry/junior-plugin-api";
39
39
  import { alias } from "drizzle-orm/pg-core";
40
40
  import { del, get, put } from "@vercel/blob";
41
41
  import { Message, ThreadImpl, stringifyMarkdown } from "chat";
42
42
  import { SlackFormatConverter, createSlackAdapter } from "@chat-adapter/slack";
43
+ import { HTTPException } from "hono/http-exception";
43
44
  // src/handlers/health.ts
44
45
  /** Build Junior's process health value for HTTP and reporting consumers. */
45
46
  function readHealthReport() {
@@ -2265,7 +2266,6 @@ function getPendingRecords(work) {
2265
2266
  }
2266
2267
  /** Build the worker run function for queued Slack conversation work. */
2267
2268
  function createSlackConversationWorker(options) {
2268
- const crossActorMidRunMode = options.crossActorMidRunMode ?? botConfig.crossActorMidRunMode;
2269
2269
  return async (context) => {
2270
2270
  const adapter = options.getSlackAdapter();
2271
2271
  const actorLookup = options.lookupSlackUser ?? lookupSlackUser;
@@ -2352,7 +2352,7 @@ function createSlackConversationWorker(options) {
2352
2352
  adapter,
2353
2353
  record
2354
2354
  })
2355
- })).filter((candidate) => crossActorMidRunMode === "steer" || requireSlackAuthorId(candidate.message) === activeAuthorId || hasSteeringOverride(candidate.message.text));
2355
+ })).filter((candidate) => requireSlackAuthorId(candidate.message) === activeAuthorId);
2356
2356
  return candidates.length > 0 ? await accept(candidates) : [];
2357
2357
  });
2358
2358
  };
@@ -6772,6 +6772,139 @@ function createProductionConversationWorkOptions(options) {
6772
6772
  waitUntil: options.waitUntil
6773
6773
  });
6774
6774
  }
6775
+ // src/api/http.ts
6776
+ const NO_STORE_HEADERS = { "cache-control": "no-store" };
6777
+ /** Build response init that keeps personalized API payloads out of shared caches. */
6778
+ function noStoreInit(init) {
6779
+ if (!init) return { headers: NO_STORE_HEADERS };
6780
+ const headers = new Headers(init.headers);
6781
+ if (!headers.has("cache-control")) headers.set("cache-control", "no-store");
6782
+ return {
6783
+ ...init,
6784
+ headers
6785
+ };
6786
+ }
6787
+ /** Serialize a REST response only after it satisfies its declared schema. */
6788
+ function jsonResponse(schema, value, init) {
6789
+ return Response.json(schema.parse(value), noStoreInit(init));
6790
+ }
6791
+ /** Return an empty personalized API response with no-store caching. */
6792
+ function emptyResponse(status = 204) {
6793
+ return new Response(null, noStoreInit({ status }));
6794
+ }
6795
+ /** Stop a request with Junior's stable JSON error contract. */
6796
+ function throwApiError(status, message, cause) {
6797
+ throw new HTTPException(status, {
6798
+ ...cause === void 0 ? void 0 : { cause },
6799
+ message,
6800
+ res: jsonResponse(apiErrorSchema, { error: message }, { status })
6801
+ });
6802
+ }
6803
+ // src/api/conversations/archive.ts
6804
+ async function setViewerArchive(args) {
6805
+ const executor = getSqlExecutor();
6806
+ return await executor.transaction(async () => {
6807
+ const db = executor.db();
6808
+ const [root] = await db.select({ lastActivityAt: juniorConversations.lastActivityAt }).from(juniorConversations).where(eq(juniorConversations.conversationId, args.rootConversationId)).limit(1).for("update");
6809
+ if (!root) return { status: "missing" };
6810
+ if (args.archived) {
6811
+ if (root.lastActivityAt.getTime() !== Date.parse(args.lastSeenAt)) return { status: "conflict" };
6812
+ const archivedAt = /* @__PURE__ */ new Date();
6813
+ const lastMessageAt = new Date(args.lastSeenAt);
6814
+ await db.insert(juniorConversationParticipants).values({
6815
+ archivedAt,
6816
+ lastMessageAt,
6817
+ rootConversationId: args.rootConversationId,
6818
+ userId: args.userId
6819
+ }).onConflictDoUpdate({
6820
+ target: [juniorConversationParticipants.userId, juniorConversationParticipants.rootConversationId],
6821
+ set: {
6822
+ archivedAt,
6823
+ lastMessageAt: sql`greatest(${juniorConversationParticipants.lastMessageAt}, excluded.last_message_at)`
6824
+ }
6825
+ });
6826
+ return {
6827
+ archivedAt: archivedAt.toISOString(),
6828
+ status: "updated"
6829
+ };
6830
+ }
6831
+ await db.update(juniorConversationParticipants).set({ archivedAt: null }).where(and(eq(juniorConversationParticipants.userId, args.userId), eq(juniorConversationParticipants.rootConversationId, args.rootConversationId), isNotNull(juniorConversationParticipants.archivedAt)));
6832
+ return {
6833
+ archivedAt: null,
6834
+ status: "updated"
6835
+ };
6836
+ });
6837
+ }
6838
+ /** Read one viewer's archive timestamp for a conversation root. */
6839
+ async function readConversationArchivedAt(viewer, conversationId) {
6840
+ const rootConversationId = await resolveRootConversationId(getSqlExecutor(), conversationId);
6841
+ const [row] = await getDb().select({ archivedAt: juniorConversationParticipants.archivedAt }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.userId, viewer.id), eq(juniorConversationParticipants.rootConversationId, rootConversationId))).limit(1);
6842
+ return row?.archivedAt?.getTime();
6843
+ }
6844
+ /** Archive or restore one conversation for the signed-in viewer. */
6845
+ async function archiveConversation(viewer, conversationId, body) {
6846
+ if (!(await readConversationAccessFromSql(getDb(), [conversationId], viewer)).get(conversationId)?.isParticipant) throwApiError(404, "Conversation not found.");
6847
+ const rootConversationId = await resolveRootConversationId(getSqlExecutor(), conversationId);
6848
+ const result = await setViewerArchive({
6849
+ archived: body.archived,
6850
+ lastSeenAt: body.lastSeenAt,
6851
+ rootConversationId,
6852
+ userId: viewer.id
6853
+ });
6854
+ if (result.status === "updated") return { archivedAt: result.archivedAt };
6855
+ if (result.status === "missing") throwApiError(404, "Conversation not found.");
6856
+ throwApiError(409, "Conversation received new activity.");
6857
+ }
6858
+ // src/chat/plugins/conversation-sidebar.ts
6859
+ /** Show native objects by default and let plugins select compact sidebar labels. */
6860
+ async function listConversationSidebarAnnotations(conversationIds, annotationsByConversation) {
6861
+ const candidates = new Set(conversationIds);
6862
+ const selected = {};
6863
+ const plugins = getPlugins();
6864
+ const customOwners = new Set(plugins.filter((plugin) => plugin.hooks?.conversationSidebar).map((plugin) => plugin.manifest.name));
6865
+ for (const conversationId of conversationIds) {
6866
+ const objects = (annotationsByConversation.get(conversationId) ?? []).flatMap((annotation) => {
6867
+ if (customOwners.has(annotation.plugin) || !annotation.objectType) return [];
6868
+ return [{
6869
+ key: annotation.key,
6870
+ label: annotation.label,
6871
+ objectType: annotation.objectType === "item" && annotation.kind === "object" && annotation.facts?.type === "deployment" ? "deployment" : annotation.objectType,
6872
+ status: annotation.status
6873
+ }];
6874
+ });
6875
+ if (objects.length) selected[conversationId] = objects;
6876
+ }
6877
+ for (const plugin of plugins) {
6878
+ const hook = plugin.hooks?.conversationSidebar;
6879
+ if (!hook) continue;
6880
+ const annotationsByConversationId = Object.fromEntries(conversationIds.flatMap((conversationId) => {
6881
+ const annotations = (annotationsByConversation.get(conversationId) ?? []).filter((annotation) => annotation.plugin === plugin.manifest.name);
6882
+ return annotations.length > 0 ? [[conversationId, annotations]] : [];
6883
+ }));
6884
+ try {
6885
+ const result = await hook({
6886
+ annotationsByConversationId,
6887
+ conversationIds,
6888
+ db: getDb(),
6889
+ log: createPluginLogger(plugin.manifest.name),
6890
+ plugin: { name: plugin.manifest.name },
6891
+ cache: createPluginCache(plugin.manifest.name)
6892
+ });
6893
+ for (const [conversationId, annotations] of Object.entries(result.annotationsByConversationId)) {
6894
+ if (!candidates.has(conversationId)) continue;
6895
+ const parsed = conversationSidebarAnnotationSchema.array().safeParse(annotations);
6896
+ if (!parsed.success || parsed.data.length === 0) continue;
6897
+ selected[conversationId] = [...selected[conversationId] ?? [], ...parsed.data];
6898
+ }
6899
+ } catch (error) {
6900
+ logWarn("plugin.conversation_sidebar.hook.failed", {
6901
+ "app.plugin.name": plugin.manifest.name,
6902
+ "exception.message": error instanceof Error ? error.message : String(error)
6903
+ });
6904
+ }
6905
+ }
6906
+ return selected;
6907
+ }
6775
6908
  // src/chat/slack/location-url.ts
6776
6909
  /** Parse a Slack workspace subdomain used in `https://{domain}.slack.com` URLs. */
6777
6910
  function parseSlackTeamDomain(value) {
@@ -6847,28 +6980,472 @@ async function fetchSlackTeamDomain(teamId) {
6847
6980
  return;
6848
6981
  }
6849
6982
  }
6850
- /** Resolve a workspace subdomain via `team.info`, cached by team id. */
6851
- async function resolveSlackTeamDomain(teamId) {
6852
- const parsedTeamId = parseSlackTeamId(teamId);
6853
- if (!parsedTeamId) return void 0;
6854
- const cached = readTeamDomainCache(parsedTeamId);
6855
- if (cached !== void 0) return cached ?? void 0;
6856
- const pending = teamDomainInflight.get(parsedTeamId);
6857
- if (pending) return pending;
6858
- const lookup = fetchSlackTeamDomain(parsedTeamId).finally(() => {
6859
- teamDomainInflight.delete(parsedTeamId);
6983
+ /** Resolve a workspace subdomain via `team.info`, cached by team id. */
6984
+ async function resolveSlackTeamDomain(teamId) {
6985
+ const parsedTeamId = parseSlackTeamId(teamId);
6986
+ if (!parsedTeamId) return void 0;
6987
+ const cached = readTeamDomainCache(parsedTeamId);
6988
+ if (cached !== void 0) return cached ?? void 0;
6989
+ const pending = teamDomainInflight.get(parsedTeamId);
6990
+ if (pending) return pending;
6991
+ const lookup = fetchSlackTeamDomain(parsedTeamId).finally(() => {
6992
+ teamDomainInflight.delete(parsedTeamId);
6993
+ });
6994
+ teamDomainInflight.set(parsedTeamId, lookup);
6995
+ return lookup;
6996
+ }
6997
+ /** Resolve workspace subdomains for the distinct team ids in one API response. */
6998
+ async function resolveSlackTeamDomains(teamIds) {
6999
+ const uniqueTeamIds = [...new Set([...teamIds].map((teamId) => parseSlackTeamId(teamId)).filter((teamId) => teamId !== void 0))];
7000
+ const resolved = await Promise.all(uniqueTeamIds.map(async (teamId) => {
7001
+ const domain = await resolveSlackTeamDomain(teamId);
7002
+ return domain ? [teamId, domain] : void 0;
7003
+ }));
7004
+ return new Map(resolved.filter((entry) => entry !== void 0));
7005
+ }
7006
+ // src/chat/conversations/fork.ts
7007
+ /** Event key of the agent note that follows the copied events in a fork. */
7008
+ const FORK_NOTE_KEY = "fork:note";
7009
+ const forkNote = alias(juniorConversationEvents, "fork_note");
7010
+ /**
7011
+ * SQL filter for event rows that a fork did not copy from its source. Usage
7012
+ * and cost reports use it, so copied model calls count only in the source.
7013
+ */
7014
+ function notCopiedByFork() {
7015
+ return sql`not exists (
7016
+ select 1 from ${juniorConversationEvents} as ${sql.identifier("fork_note")}
7017
+ where ${forkNote.conversationId} = ${juniorConversationEvents.conversationId}
7018
+ and ${forkNote.idempotencyKey} = ${FORK_NOTE_KEY}
7019
+ and ${forkNote.seq} > ${juniorConversationEvents.seq}
7020
+ )`;
7021
+ }
7022
+ /**
7023
+ * Events that hold credentials, approvals, provider connections, or plugin
7024
+ * state of the source actor. A fork does not copy them.
7025
+ */
7026
+ const UNCOPIED_EVENT_TYPES = /* @__PURE__ */ new Set([
7027
+ "authorization_completed",
7028
+ "authorization_requested",
7029
+ "guardian_action_reviewed",
7030
+ "mcp_provider_connected",
7031
+ "mcp_provider_connected_unowned",
7032
+ "structured_event",
7033
+ "unknown"
7034
+ ]);
7035
+ 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.";
7036
+ /** An expected fork request failure, such as a reply without saved history. */
7037
+ var ConversationForkError = class extends Error {};
7038
+ /**
7039
+ * Create a new web root Conversation from the agent history of a source
7040
+ * Conversation, through one completed assistant reply.
7041
+ *
7042
+ * Retries with the same requester and key return the same Conversation.
7043
+ */
7044
+ async function forkConversation(input) {
7045
+ const conversationId = createConversationId({
7046
+ actorEmail: input.actorEmail,
7047
+ idempotencyKey: JSON.stringify([
7048
+ "fork",
7049
+ input.sourceConversationId,
7050
+ input.idempotencyKey
7051
+ ])
7052
+ });
7053
+ const executor = getSqlExecutor();
7054
+ const events = getConversationEventStore();
7055
+ const store = getConversationStore();
7056
+ return withConversationEventLock(executor, input.sourceConversationId, () => withConversationEventLock(executor, conversationId, async () => {
7057
+ if (await store.get({ conversationId })) return { conversationId };
7058
+ const source = await store.get({ conversationId: input.sourceConversationId });
7059
+ if (!source || source.transcriptPurgedAtMs !== void 0) throw new ConversationForkError("The source history is no longer available.");
7060
+ if (source.parentConversationId) throw new ConversationForkError("You cannot fork a child conversation.");
7061
+ const boundary = await events.loadByIdempotencyKey(input.sourceConversationId, `message:${input.messageId}:agent`);
7062
+ const reply = boundary?.data.type === "assistant_message" ? piMessageFromHistoryItem(boundary.data) : void 0;
7063
+ 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.");
7064
+ const projection = await loadTurnProjection({
7065
+ conversationId: input.sourceConversationId,
7066
+ committedSeq: boundary.seq,
7067
+ includeTail: false
7068
+ });
7069
+ if (!projection?.messages.length) throw new ConversationForkError("The selected history is no longer available.");
7070
+ if (hasUnfinishedToolCalls(projection.messages)) throw new ConversationForkError("This history has unfinished tool calls. Choose another reply.");
7071
+ const nowMs = Date.now();
7072
+ await recordWebConversationActivity({
7073
+ actor: webActorFromEmail(input.actorEmail, { ...input.actorName ? { fullName: input.actorName } : void 0 }),
7074
+ conversationId,
7075
+ nowMs,
7076
+ rootVisibility: source.visibility === "public" ? "public" : "private"
7077
+ });
7078
+ await executor.db().update(juniorConversations).set({
7079
+ forkedFromConversationId: source.conversationId,
7080
+ title: source.title ? `Fork: ${source.title}` : "Forked conversation"
7081
+ }).where(eq(juniorConversations.conversationId, conversationId));
7082
+ await copyForkEvents({
7083
+ sourceConversationId: source.conversationId,
7084
+ conversationId,
7085
+ events: await events.loadHistory(source.conversationId),
7086
+ boundarySeq: boundary.seq,
7087
+ messageId: input.messageId
7088
+ });
7089
+ await events.append(conversationId, [{
7090
+ idempotencyKey: FORK_NOTE_KEY,
7091
+ createdAtMs: nowMs,
7092
+ data: {
7093
+ type: "user_message",
7094
+ provenance: contextProvenance,
7095
+ content: [{
7096
+ type: "text",
7097
+ text: FORK_NOTE
7098
+ }],
7099
+ timestamp: nowMs
7100
+ }
7101
+ }]);
7102
+ return { conversationId };
7103
+ }));
7104
+ }
7105
+ /**
7106
+ * Copy the source event rows through the selected reply. Copies keep their
7107
+ * sequence, author, timestamp, key, and payload, so the fork shows the same
7108
+ * transcript and event log, and seq references in compactions stay valid.
7109
+ * Later updates of copied Messages and the end of copied Turns are also
7110
+ * copied, so that no copied item stays open in the fork.
7111
+ */
7112
+ async function copyForkEvents(args) {
7113
+ const reply = args.events.find((event) => event.data.type === "message" && event.data.role === "assistant" && event.data.messageId === args.messageId);
7114
+ const cutoffSeq = Math.max(args.boundarySeq, reply?.seq ?? -1);
7115
+ const messageIds = /* @__PURE__ */ new Set();
7116
+ const turnIds = /* @__PURE__ */ new Set();
7117
+ const seqs = [];
7118
+ let historyVersion = 0;
7119
+ for (const event of args.events) {
7120
+ const data = event.data;
7121
+ if (UNCOPIED_EVENT_TYPES.has(data.type)) continue;
7122
+ if (event.idempotencyKey === FORK_NOTE_KEY) continue;
7123
+ if (event.seq <= cutoffSeq) {
7124
+ if (data.type === "message") messageIds.add(data.messageId);
7125
+ if (data.type === "turn_started") turnIds.add(data.turnId);
7126
+ historyVersion = Math.max(historyVersion, event.historyVersion);
7127
+ seqs.push(event.seq);
7128
+ } 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);
7129
+ }
7130
+ if (!seqs.length) return;
7131
+ const e = juniorConversationEvents;
7132
+ await getSqlExecutor().db().execute(sql`insert into ${e} (${sql.join([
7133
+ e.conversationId,
7134
+ e.seq,
7135
+ e.historyVersion,
7136
+ e.schemaVersion,
7137
+ e.idempotencyKey,
7138
+ e.type,
7139
+ e.payload,
7140
+ e.actorIdentityId,
7141
+ e.createdAt
7142
+ ].map((column) => sql.identifier(column.name)), sql`, `)})
7143
+ select ${args.conversationId}, ${e.seq}, least(${e.historyVersion}, ${historyVersion}), ${e.schemaVersion}, ${e.idempotencyKey}, ${e.type}, ${e.payload}, ${e.actorIdentityId}, ${e.createdAt}
7144
+ from ${e}
7145
+ where ${e.conversationId} = ${args.sourceConversationId}
7146
+ and ${inArray(e.seq, seqs)}`);
7147
+ }
7148
+ function hasUnfinishedToolCalls(messages) {
7149
+ const pending = /* @__PURE__ */ new Set();
7150
+ for (const message of messages) if (message.role === "assistant") {
7151
+ for (const part of message.content) if (part.type === "toolCall") pending.add(part.id);
7152
+ } else if (message.role === "toolResult") pending.delete(message.toolCallId);
7153
+ return pending.size > 0;
7154
+ }
7155
+ // src/chat/pi/sql-model-usage.ts
7156
+ /** SQL reporting adapter for usage stored on assistant history events. */
7157
+ const message = sql`${juniorConversationEvents.payload}`;
7158
+ const usage$1 = sql`${message}->'usage'`;
7159
+ const cost = sql`${usage$1}->'cost'`;
7160
+ function nonnegativeNumber(source, field, fallbackField, floor = false) {
7161
+ const value = (name) => sql`case
7162
+ when jsonb_typeof(${source}->${name}) = 'number'
7163
+ and (${source}->>${name})::numeric >= 0
7164
+ then ${floor ? sql`floor((${source}->>${name})::numeric)` : sql`(${source}->>${name})::numeric`}
7165
+ end`;
7166
+ const primary = value(field);
7167
+ return fallbackField ? sql`coalesce(${primary}, ${value(fallbackField)})` : sql`${primary}`;
7168
+ }
7169
+ function token(field, fallbackField) {
7170
+ return nonnegativeNumber(usage$1, field, fallbackField, true);
7171
+ }
7172
+ function summed(value) {
7173
+ return sql`sum(${value})::double precision`;
7174
+ }
7175
+ function summedCost(value) {
7176
+ return sql`round(sum(${value}), 12)::double precision`;
7177
+ }
7178
+ function messageTotalTokens(args) {
7179
+ const { input, output, cacheRead, cacheWrite } = args;
7180
+ return sql`case
7181
+ when ${input} is not null
7182
+ or ${output} is not null
7183
+ or ${cacheRead} is not null
7184
+ or ${cacheWrite} is not null
7185
+ then coalesce(${input}, 0)
7186
+ + coalesce(${output}, 0)
7187
+ + coalesce(${cacheRead}, 0)
7188
+ + coalesce(${cacheWrite}, 0)
7189
+ else ${token("totalTokens")}
7190
+ end`;
7191
+ }
7192
+ function definedNumber(value) {
7193
+ return value === null ? void 0 : value;
7194
+ }
7195
+ function usageFromRow(row) {
7196
+ const components = {
7197
+ inputTokens: definedNumber(row.inputTokens),
7198
+ outputTokens: definedNumber(row.outputTokens),
7199
+ cachedInputTokens: definedNumber(row.cachedInputTokens),
7200
+ cacheCreationTokens: definedNumber(row.cacheCreationTokens)
7201
+ };
7202
+ const costFields = {
7203
+ input: definedNumber(row.costInput),
7204
+ output: definedNumber(row.costOutput),
7205
+ cacheRead: definedNumber(row.costCacheRead),
7206
+ cacheWrite: definedNumber(row.costCacheWrite),
7207
+ total: definedNumber(row.costTotal)
7208
+ };
7209
+ const definedCost = Object.fromEntries(Object.entries(costFields).filter(([, value]) => value !== void 0));
7210
+ const result = {
7211
+ ...Object.fromEntries(Object.entries(components).filter(([, value]) => value !== void 0)),
7212
+ ...row.reasoningTokens === null ? void 0 : { reasoningTokens: row.reasoningTokens },
7213
+ ...row.totalTokens === null ? void 0 : { totalTokens: row.totalTokens },
7214
+ ...Object.keys(definedCost).length > 0 ? { cost: definedCost } : void 0
7215
+ };
7216
+ return hasAgentTurnUsage(result) ? agentTurnUsageSchema.parse(result) : void 0;
7217
+ }
7218
+ /**
7219
+ * Aggregate real assistant calls by provider/model in SQL, for one
7220
+ * Conversation and all its child agents at any depth. Replayed history is not
7221
+ * another call; its tokens appear in the next call's input usage.
7222
+ */
7223
+ async function readConversationModelUsageFromSql(executor, options) {
7224
+ const modelId = sql`case
7225
+ when position('/' in coalesce(${message}->>'model', '')) > 0
7226
+ then ${message}->>'model'
7227
+ else concat(${message}->>'provider', '/', ${message}->>'model')
7228
+ end`;
7229
+ const inputTokens = token("input", "inputTokens");
7230
+ const outputTokens = token("output", "outputTokens");
7231
+ const cachedInputTokens = token("cacheRead", "cachedInputTokens");
7232
+ const cacheCreationTokens = token("cacheWrite", "cacheCreationTokens");
7233
+ return (await executor.db().select({
7234
+ modelId,
7235
+ inputTokens: summed(inputTokens),
7236
+ outputTokens: summed(outputTokens),
7237
+ cachedInputTokens: summed(cachedInputTokens),
7238
+ cacheCreationTokens: summed(cacheCreationTokens),
7239
+ reasoningTokens: summed(token("reasoning", "reasoningTokens")),
7240
+ totalTokens: summed(messageTotalTokens({
7241
+ input: inputTokens,
7242
+ output: outputTokens,
7243
+ cacheRead: cachedInputTokens,
7244
+ cacheWrite: cacheCreationTokens
7245
+ })),
7246
+ costInput: summedCost(nonnegativeNumber(cost, "input")),
7247
+ costOutput: summedCost(nonnegativeNumber(cost, "output")),
7248
+ costCacheRead: summedCost(nonnegativeNumber(cost, "cacheRead")),
7249
+ costCacheWrite: summedCost(nonnegativeNumber(cost, "cacheWrite")),
7250
+ costTotal: summedCost(nonnegativeNumber(cost, "total"))
7251
+ }).from(juniorConversationEvents).innerJoin(conversationSubtree([options.conversationId]), eq(conversationSubtreeColumns.conversationId, juniorConversationEvents.conversationId)).where(and(eq(juniorConversationEvents.type, "assistant_message"), notCopiedByFork(), notInheritedByForkChild(), sql`jsonb_typeof(${message}->'provider') = 'string'`, sql`jsonb_typeof(${message}->'model') = 'string'`, sql`coalesce(${message}->>'provider', '') <> ''`, sql`coalesce(${message}->>'model', '') <> ''`)).groupBy(modelId).orderBy(modelId)).flatMap((row) => {
7252
+ const modelUsage = usageFromRow(row);
7253
+ return modelUsage ? [{
7254
+ modelId: row.modelId,
7255
+ usage: modelUsage
7256
+ }] : [];
7257
+ });
7258
+ }
7259
+ // src/api/conversations/cursor.ts
7260
+ const CURSOR_CONTEXT = "junior-conversation-report-cursor";
7261
+ const conversationCursorPayloadSchema = z.object({
7262
+ conversationId: z.string().min(1),
7263
+ seq: z.number().int().min(-1),
7264
+ version: z.literal(1)
7265
+ }).strict();
7266
+ function cursorSecret() {
7267
+ const secret = process.env.JUNIOR_SECRET?.trim();
7268
+ if (!secret) throw new Error("Cannot sign conversation cursor without JUNIOR_SECRET");
7269
+ return secret;
7270
+ }
7271
+ /** Sign one encoded cursor with the domain-separated server secret. */
7272
+ function signature(payload) {
7273
+ return createHmac("sha256", cursorSecret()).update(`${CURSOR_CONTEXT}:${payload}`).digest("base64url");
7274
+ }
7275
+ /** Encode an authenticated, conversation-bound event position for an API cursor. */
7276
+ function encodeConversationCursor(payload) {
7277
+ const encoded = Buffer.from(JSON.stringify({
7278
+ ...payload,
7279
+ version: 1
7280
+ }), "utf8").toString("base64url");
7281
+ return `${encoded}.${signature(encoded)}`;
7282
+ }
7283
+ /** Decode an authenticated reporting cursor for the expected conversation. */
7284
+ function decodeConversationCursor(args) {
7285
+ const [encoded, actualSignature, extra] = args.cursor.split(".");
7286
+ if (!encoded || !actualSignature || extra !== void 0) return void 0;
7287
+ const expected = Buffer.from(signature(encoded));
7288
+ const actual = Buffer.from(actualSignature);
7289
+ if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) return;
7290
+ try {
7291
+ const parsed = conversationCursorPayloadSchema.parse(JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")));
7292
+ if (parsed.conversationId !== args.conversationId) return;
7293
+ return { seq: parsed.seq };
7294
+ } catch {
7295
+ return;
7296
+ }
7297
+ }
7298
+ // src/api/conversations/first-message.ts
7299
+ /**
7300
+ * Read the text of the first human message for each untitled conversation.
7301
+ * Read-time titles use it while the generated title is still unset.
7302
+ */
7303
+ async function readFirstHumanMessageTexts(db, conversations) {
7304
+ const untitledIds = conversations.flatMap((conversation) => conversation.title ? [] : [conversation.conversationId]);
7305
+ if (untitledIds.length === 0) return /* @__PURE__ */ new Map();
7306
+ const text = sql`${juniorConversationEvents.payload}->>'text'`;
7307
+ const rows = await db.selectDistinctOn([juniorConversationEvents.conversationId], {
7308
+ conversationId: juniorConversationEvents.conversationId,
7309
+ text: text.as("text")
7310
+ }).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));
7311
+ return new Map(rows.map((row) => [row.conversationId, row.text]));
7312
+ }
7313
+ // src/api/mentions.ts
7314
+ const SLACK_USER_MENTION_RE = /<@([UW][A-Z0-9]+)(?:\|[^<>]*)?>/g;
7315
+ function mentionedSlackUserIds(texts) {
7316
+ const ids = texts.flatMap((text) => text ? [...text.matchAll(SLACK_USER_MENTION_RE)].map((m) => m[1]) : []);
7317
+ return [...new Set(ids)];
7318
+ }
7319
+ /**
7320
+ * Resolve the people that each source mentions, in source order. Every API
7321
+ * that returns display text uses this, so the dashboard can show the same
7322
+ * names and People links everywhere. It does one identity query per Slack
7323
+ * workspace.
7324
+ */
7325
+ async function resolveMentions(executor, sources) {
7326
+ const idsBySource = sources.map((source) => mentionedSlackUserIds(source.texts));
7327
+ const idsByTenant = /* @__PURE__ */ new Map();
7328
+ sources.forEach((source, index) => {
7329
+ if (idsBySource[index].length === 0) return;
7330
+ const ids = idsByTenant.get(source.tenantId) ?? /* @__PURE__ */ new Set();
7331
+ for (const id of idsBySource[index]) ids.add(id);
7332
+ idsByTenant.set(source.tenantId, ids);
7333
+ });
7334
+ const peopleByTenant = new Map(await Promise.all([...idsByTenant].map(async ([tenantId, ids]) => [tenantId, await readMentionedPeople(executor, tenantId, [...ids])])));
7335
+ return sources.map((source, index) => {
7336
+ const people = peopleByTenant.get(source.tenantId);
7337
+ return idsBySource[index].flatMap((id) => {
7338
+ const person = people?.get(id);
7339
+ return person ? [person] : [];
7340
+ });
7341
+ });
7342
+ }
7343
+ /**
7344
+ * Read current names for mentioned Slack users. Known Junior identities come
7345
+ * first. The email is set only for a linked, verified Junior user, because
7346
+ * that email is the People profile route.
7347
+ */
7348
+ async function readMentionedPeople(executor, tenantId, userIds) {
7349
+ const rows = await executor.db().select({
7350
+ displayName: juniorIdentities.displayName,
7351
+ emailVerified: juniorIdentities.emailVerified,
7352
+ handle: juniorIdentities.handle,
7353
+ providerSubjectId: juniorIdentities.providerSubjectId,
7354
+ userDisplayName: juniorUsers.displayName,
7355
+ userEmail: juniorUsers.primaryEmail
7356
+ }).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));
7357
+ const people = /* @__PURE__ */ new Map();
7358
+ for (const row of rows) {
7359
+ if (people.has(row.providerSubjectId)) continue;
7360
+ const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
7361
+ const email = row.emailVerified ? row.userEmail?.trim() : void 0;
7362
+ people.set(row.providerSubjectId, {
7363
+ slackUserId: row.providerSubjectId,
7364
+ ...fullName ? { fullName } : void 0,
7365
+ ...row.handle ? { slackUserName: row.handle } : void 0,
7366
+ ...email ? { email } : void 0
7367
+ });
7368
+ }
7369
+ const unnamed = userIds.filter((id) => {
7370
+ const person = people.get(id);
7371
+ return !person?.fullName && !person?.slackUserName;
7372
+ });
7373
+ if (tenantId && unnamed.length > 0) {
7374
+ const profiles = await Promise.all(unnamed.map((id) => lookupSlackUser(tenantId, id)));
7375
+ unnamed.forEach((id, index) => {
7376
+ const profile = profiles[index];
7377
+ if (!profile?.fullName && !profile?.userName) return;
7378
+ people.set(id, {
7379
+ ...people.get(id),
7380
+ slackUserId: id,
7381
+ ...profile.fullName ? { fullName: profile.fullName } : void 0,
7382
+ ...profile.userName ? { slackUserName: profile.userName } : void 0
7383
+ });
7384
+ });
7385
+ }
7386
+ return people;
7387
+ }
7388
+ // src/api/conversations/mentions.ts
7389
+ /** Text in one event that the dashboard renders with mentions. */
7390
+ function eventMentionTexts(data) {
7391
+ switch (data.type) {
7392
+ case "message": return data.redacted ? [] : [
7393
+ data.text,
7394
+ data.trustedSummary,
7395
+ ...(data.events ?? []).map((event) => event.trustedSummary)
7396
+ ];
7397
+ case "assistant_message": return data.parts.map((part) => part.text);
7398
+ case "compaction":
7399
+ case "handoff": return [data.summary];
7400
+ case "structured_event": return [data.presentation.preview, ...(data.presentation.details ?? []).map((detail) => detail.description)];
7401
+ default: return [];
7402
+ }
7403
+ }
7404
+ function briefMentionTexts(brief) {
7405
+ return [
7406
+ brief.summary,
7407
+ brief.intent,
7408
+ brief.outcome.text,
7409
+ ...brief.decisions.flatMap((decision) => [decision.text, decision.by]),
7410
+ ...brief.openDecisions.flatMap((decision) => [decision.text, decision.owner]),
7411
+ ...brief.facts
7412
+ ];
7413
+ }
7414
+ function withMentions(value, mentions) {
7415
+ return mentions.length > 0 ? {
7416
+ ...value,
7417
+ mentions
7418
+ } : value;
7419
+ }
7420
+ /**
7421
+ * Attach the people that each event and the Brief of one conversation
7422
+ * mention, resolved at read time.
7423
+ */
7424
+ async function attachConversationMentions(executor, conversationId, content) {
7425
+ const tenantId = (await loadConversationSlackTenantIds(executor, [conversationId])).get(conversationId);
7426
+ const sources = content.events.map((event) => ({
7427
+ tenantId,
7428
+ texts: eventMentionTexts(event.data)
7429
+ }));
7430
+ if (content.brief) sources.push({
7431
+ tenantId,
7432
+ texts: briefMentionTexts(content.brief)
6860
7433
  });
6861
- teamDomainInflight.set(parsedTeamId, lookup);
6862
- return lookup;
7434
+ const mentions = await resolveMentions(executor, sources);
7435
+ return {
7436
+ briefMentions: content.brief ? mentions.at(-1) : [],
7437
+ events: content.events.map((event, index) => withMentions(event, mentions[index]))
7438
+ };
6863
7439
  }
6864
- /** Resolve workspace subdomains for the distinct team ids in one API response. */
6865
- async function resolveSlackTeamDomains(teamIds) {
6866
- const uniqueTeamIds = [...new Set([...teamIds].map((teamId) => parseSlackTeamId(teamId)).filter((teamId) => teamId !== void 0))];
6867
- const resolved = await Promise.all(uniqueTeamIds.map(async (teamId) => {
6868
- const domain = await resolveSlackTeamDomain(teamId);
6869
- return domain ? [teamId, domain] : void 0;
6870
- }));
6871
- return new Map(resolved.filter((entry) => entry !== void 0));
7440
+ /** Attach the people that each activity preview mentions, resolved at read time. */
7441
+ async function attachActivityPreviewMentions(executor, previews) {
7442
+ const entries = [...previews];
7443
+ const tenants = await loadConversationSlackTenantIds(executor, entries.map(([conversationId]) => conversationId));
7444
+ const mentions = await resolveMentions(executor, entries.map(([conversationId, preview]) => ({
7445
+ tenantId: tenants.get(conversationId),
7446
+ texts: [preview.text]
7447
+ })));
7448
+ return new Map(entries.map(([conversationId, preview], index) => [conversationId, withMentions(preview, mentions[index])]));
6872
7449
  }
6873
7450
  // src/api/conversations/shared.ts
6874
7451
  /** Format a Slack destination without exposing private channel names. */
@@ -7133,155 +7710,6 @@ function childAgentTotalsReport(metrics, auxiliaryCosts) {
7133
7710
  ...childAgents.usage ? { cumulativeUsage: childAgents.usage } : void 0
7134
7711
  };
7135
7712
  }
7136
- // src/chat/conversations/fork.ts
7137
- /** Event key of the agent note that follows the copied events in a fork. */
7138
- const FORK_NOTE_KEY = "fork:note";
7139
- const forkNote = alias(juniorConversationEvents, "fork_note");
7140
- /**
7141
- * SQL filter for event rows that a fork did not copy from its source. Usage
7142
- * and cost reports use it, so copied model calls count only in the source.
7143
- */
7144
- function notCopiedByFork() {
7145
- return sql`not exists (
7146
- select 1 from ${juniorConversationEvents} as ${sql.identifier("fork_note")}
7147
- where ${forkNote.conversationId} = ${juniorConversationEvents.conversationId}
7148
- and ${forkNote.idempotencyKey} = ${FORK_NOTE_KEY}
7149
- and ${forkNote.seq} > ${juniorConversationEvents.seq}
7150
- )`;
7151
- }
7152
- /**
7153
- * Events that hold credentials, approvals, provider connections, or plugin
7154
- * state of the source actor. A fork does not copy them.
7155
- */
7156
- const UNCOPIED_EVENT_TYPES = /* @__PURE__ */ new Set([
7157
- "authorization_completed",
7158
- "authorization_requested",
7159
- "guardian_action_reviewed",
7160
- "mcp_provider_connected",
7161
- "mcp_provider_connected_unowned",
7162
- "structured_event",
7163
- "unknown"
7164
- ]);
7165
- 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.";
7166
- /** An expected fork request failure, such as a reply without saved history. */
7167
- var ConversationForkError = class extends Error {};
7168
- /**
7169
- * Create a new web root Conversation from the agent history of a source
7170
- * Conversation, through one completed assistant reply.
7171
- *
7172
- * Retries with the same requester and key return the same Conversation.
7173
- */
7174
- async function forkConversation(input) {
7175
- const conversationId = createConversationId({
7176
- actorEmail: input.actorEmail,
7177
- idempotencyKey: JSON.stringify([
7178
- "fork",
7179
- input.sourceConversationId,
7180
- input.idempotencyKey
7181
- ])
7182
- });
7183
- const executor = getSqlExecutor();
7184
- const events = getConversationEventStore();
7185
- const store = getConversationStore();
7186
- return withConversationEventLock(executor, input.sourceConversationId, () => withConversationEventLock(executor, conversationId, async () => {
7187
- if (await store.get({ conversationId })) return { conversationId };
7188
- const source = await store.get({ conversationId: input.sourceConversationId });
7189
- if (!source || source.transcriptPurgedAtMs !== void 0) throw new ConversationForkError("The source history is no longer available.");
7190
- if (source.parentConversationId) throw new ConversationForkError("You cannot fork a child conversation.");
7191
- const boundary = await events.loadByIdempotencyKey(input.sourceConversationId, `message:${input.messageId}:agent`);
7192
- const reply = boundary?.data.type === "assistant_message" ? piMessageFromHistoryItem(boundary.data) : void 0;
7193
- 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.");
7194
- const projection = await loadTurnProjection({
7195
- conversationId: input.sourceConversationId,
7196
- committedSeq: boundary.seq,
7197
- includeTail: false
7198
- });
7199
- if (!projection?.messages.length) throw new ConversationForkError("The selected history is no longer available.");
7200
- if (hasUnfinishedToolCalls(projection.messages)) throw new ConversationForkError("This history has unfinished tool calls. Choose another reply.");
7201
- const nowMs = Date.now();
7202
- await recordWebConversationActivity({
7203
- actor: webActorFromEmail(input.actorEmail, { ...input.actorName ? { fullName: input.actorName } : void 0 }),
7204
- conversationId,
7205
- nowMs,
7206
- rootVisibility: source.visibility === "public" ? "public" : "private"
7207
- });
7208
- await executor.db().update(juniorConversations).set({
7209
- forkedFromConversationId: source.conversationId,
7210
- title: source.title ? `Fork: ${source.title}` : "Forked conversation"
7211
- }).where(eq(juniorConversations.conversationId, conversationId));
7212
- await copyForkEvents({
7213
- sourceConversationId: source.conversationId,
7214
- conversationId,
7215
- events: await events.loadHistory(source.conversationId),
7216
- boundarySeq: boundary.seq,
7217
- messageId: input.messageId
7218
- });
7219
- await events.append(conversationId, [{
7220
- idempotencyKey: FORK_NOTE_KEY,
7221
- createdAtMs: nowMs,
7222
- data: {
7223
- type: "user_message",
7224
- provenance: contextProvenance,
7225
- content: [{
7226
- type: "text",
7227
- text: FORK_NOTE
7228
- }],
7229
- timestamp: nowMs
7230
- }
7231
- }]);
7232
- return { conversationId };
7233
- }));
7234
- }
7235
- /**
7236
- * Copy the source event rows through the selected reply. Copies keep their
7237
- * sequence, author, timestamp, key, and payload, so the fork shows the same
7238
- * transcript and event log, and seq references in compactions stay valid.
7239
- * Later updates of copied Messages and the end of copied Turns are also
7240
- * copied, so that no copied item stays open in the fork.
7241
- */
7242
- async function copyForkEvents(args) {
7243
- const reply = args.events.find((event) => event.data.type === "message" && event.data.role === "assistant" && event.data.messageId === args.messageId);
7244
- const cutoffSeq = Math.max(args.boundarySeq, reply?.seq ?? -1);
7245
- const messageIds = /* @__PURE__ */ new Set();
7246
- const turnIds = /* @__PURE__ */ new Set();
7247
- const seqs = [];
7248
- let historyVersion = 0;
7249
- for (const event of args.events) {
7250
- const data = event.data;
7251
- if (UNCOPIED_EVENT_TYPES.has(data.type)) continue;
7252
- if (event.idempotencyKey === FORK_NOTE_KEY) continue;
7253
- if (event.seq <= cutoffSeq) {
7254
- if (data.type === "message") messageIds.add(data.messageId);
7255
- if (data.type === "turn_started") turnIds.add(data.turnId);
7256
- historyVersion = Math.max(historyVersion, event.historyVersion);
7257
- seqs.push(event.seq);
7258
- } 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);
7259
- }
7260
- if (!seqs.length) return;
7261
- const e = juniorConversationEvents;
7262
- await getSqlExecutor().db().execute(sql`insert into ${e} (${sql.join([
7263
- e.conversationId,
7264
- e.seq,
7265
- e.historyVersion,
7266
- e.schemaVersion,
7267
- e.idempotencyKey,
7268
- e.type,
7269
- e.payload,
7270
- e.actorIdentityId,
7271
- e.createdAt
7272
- ].map((column) => sql.identifier(column.name)), sql`, `)})
7273
- select ${args.conversationId}, ${e.seq}, least(${e.historyVersion}, ${historyVersion}), ${e.schemaVersion}, ${e.idempotencyKey}, ${e.type}, ${e.payload}, ${e.actorIdentityId}, ${e.createdAt}
7274
- from ${e}
7275
- where ${e.conversationId} = ${args.sourceConversationId}
7276
- and ${inArray(e.seq, seqs)}`);
7277
- }
7278
- function hasUnfinishedToolCalls(messages) {
7279
- const pending = /* @__PURE__ */ new Set();
7280
- for (const message of messages) if (message.role === "assistant") {
7281
- for (const part of message.content) if (part.type === "toolCall") pending.add(part.id);
7282
- } else if (message.role === "toolResult") pending.delete(message.toolCallId);
7283
- return pending.size > 0;
7284
- }
7285
7713
  // src/api/conversations/auxiliary-costs.ts
7286
7714
  function eventCost() {
7287
7715
  return sql`CASE
@@ -7331,66 +7759,16 @@ async function readConversationAuxiliaryCostsFromSql(db, conversationIds) {
7331
7759
  operations: []
7332
7760
  } };
7333
7761
  current.total.costUsd = addCostUsd(current.total.costUsd, row.costUsd);
7334
- if (row.childAgentCostUsd > 0) current.childAgentCostUsd = addCostUsd(current.childAgentCostUsd ?? 0, row.childAgentCostUsd);
7335
- current.total.operations.push({
7336
- costUsd: row.costUsd,
7337
- events: row.events,
7338
- name: row.name,
7339
- namespace: row.namespace
7340
- });
7341
- result.set(row.conversationId, current);
7342
- }
7343
- return result;
7344
- }
7345
- // src/chat/plugins/conversation-sidebar.ts
7346
- /** Show native objects by default and let plugins select compact sidebar labels. */
7347
- async function listConversationSidebarAnnotations(conversationIds, annotationsByConversation) {
7348
- const candidates = new Set(conversationIds);
7349
- const selected = {};
7350
- const plugins = getPlugins();
7351
- const customOwners = new Set(plugins.filter((plugin) => plugin.hooks?.conversationSidebar).map((plugin) => plugin.manifest.name));
7352
- for (const conversationId of conversationIds) {
7353
- const objects = (annotationsByConversation.get(conversationId) ?? []).flatMap((annotation) => {
7354
- if (customOwners.has(annotation.plugin) || !annotation.objectType) return [];
7355
- return [{
7356
- key: annotation.key,
7357
- label: annotation.label,
7358
- objectType: annotation.objectType === "item" && annotation.kind === "object" && annotation.facts?.type === "deployment" ? "deployment" : annotation.objectType,
7359
- status: annotation.status
7360
- }];
7361
- });
7362
- if (objects.length) selected[conversationId] = objects;
7363
- }
7364
- for (const plugin of plugins) {
7365
- const hook = plugin.hooks?.conversationSidebar;
7366
- if (!hook) continue;
7367
- const annotationsByConversationId = Object.fromEntries(conversationIds.flatMap((conversationId) => {
7368
- const annotations = (annotationsByConversation.get(conversationId) ?? []).filter((annotation) => annotation.plugin === plugin.manifest.name);
7369
- return annotations.length > 0 ? [[conversationId, annotations]] : [];
7370
- }));
7371
- try {
7372
- const result = await hook({
7373
- annotationsByConversationId,
7374
- conversationIds,
7375
- db: getDb(),
7376
- log: createPluginLogger(plugin.manifest.name),
7377
- plugin: { name: plugin.manifest.name },
7378
- cache: createPluginCache(plugin.manifest.name)
7379
- });
7380
- for (const [conversationId, annotations] of Object.entries(result.annotationsByConversationId)) {
7381
- if (!candidates.has(conversationId)) continue;
7382
- const parsed = conversationSidebarAnnotationSchema.array().safeParse(annotations);
7383
- if (!parsed.success || parsed.data.length === 0) continue;
7384
- selected[conversationId] = [...selected[conversationId] ?? [], ...parsed.data];
7385
- }
7386
- } catch (error) {
7387
- logWarn("plugin.conversation_sidebar.hook.failed", {
7388
- "app.plugin.name": plugin.manifest.name,
7389
- "exception.message": error instanceof Error ? error.message : String(error)
7390
- });
7391
- }
7762
+ if (row.childAgentCostUsd > 0) current.childAgentCostUsd = addCostUsd(current.childAgentCostUsd ?? 0, row.childAgentCostUsd);
7763
+ current.total.operations.push({
7764
+ costUsd: row.costUsd,
7765
+ events: row.events,
7766
+ name: row.name,
7767
+ namespace: row.namespace
7768
+ });
7769
+ result.set(row.conversationId, current);
7392
7770
  }
7393
- return selected;
7771
+ return result;
7394
7772
  }
7395
7773
  // src/chat/plugins/unfinished-work.ts
7396
7774
  /**
@@ -7505,158 +7883,6 @@ async function readConversationActivityPreviews(db, conversationIds) {
7505
7883
  text: row.text
7506
7884
  }]));
7507
7885
  }
7508
- // src/api/conversations/first-message.ts
7509
- /**
7510
- * Read the text of the first human message for each untitled conversation.
7511
- * Read-time titles use it while the generated title is still unset.
7512
- */
7513
- async function readFirstHumanMessageTexts(db, conversations) {
7514
- const untitledIds = conversations.flatMap((conversation) => conversation.title ? [] : [conversation.conversationId]);
7515
- if (untitledIds.length === 0) return /* @__PURE__ */ new Map();
7516
- const text = sql`${juniorConversationEvents.payload}->>'text'`;
7517
- const rows = await db.selectDistinctOn([juniorConversationEvents.conversationId], {
7518
- conversationId: juniorConversationEvents.conversationId,
7519
- text: text.as("text")
7520
- }).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));
7521
- return new Map(rows.map((row) => [row.conversationId, row.text]));
7522
- }
7523
- // src/api/mentions.ts
7524
- const SLACK_USER_MENTION_RE = /<@([UW][A-Z0-9]+)(?:\|[^<>]*)?>/g;
7525
- function mentionedSlackUserIds(texts) {
7526
- const ids = texts.flatMap((text) => text ? [...text.matchAll(SLACK_USER_MENTION_RE)].map((m) => m[1]) : []);
7527
- return [...new Set(ids)];
7528
- }
7529
- /**
7530
- * Resolve the people that each source mentions, in source order. Every API
7531
- * that returns display text uses this, so the dashboard can show the same
7532
- * names and People links everywhere. It does one identity query per Slack
7533
- * workspace.
7534
- */
7535
- async function resolveMentions(executor, sources) {
7536
- const idsBySource = sources.map((source) => mentionedSlackUserIds(source.texts));
7537
- const idsByTenant = /* @__PURE__ */ new Map();
7538
- sources.forEach((source, index) => {
7539
- if (idsBySource[index].length === 0) return;
7540
- const ids = idsByTenant.get(source.tenantId) ?? /* @__PURE__ */ new Set();
7541
- for (const id of idsBySource[index]) ids.add(id);
7542
- idsByTenant.set(source.tenantId, ids);
7543
- });
7544
- const peopleByTenant = new Map(await Promise.all([...idsByTenant].map(async ([tenantId, ids]) => [tenantId, await readMentionedPeople(executor, tenantId, [...ids])])));
7545
- return sources.map((source, index) => {
7546
- const people = peopleByTenant.get(source.tenantId);
7547
- return idsBySource[index].flatMap((id) => {
7548
- const person = people?.get(id);
7549
- return person ? [person] : [];
7550
- });
7551
- });
7552
- }
7553
- /**
7554
- * Read current names for mentioned Slack users. Known Junior identities come
7555
- * first. The email is set only for a linked, verified Junior user, because
7556
- * that email is the People profile route.
7557
- */
7558
- async function readMentionedPeople(executor, tenantId, userIds) {
7559
- const rows = await executor.db().select({
7560
- displayName: juniorIdentities.displayName,
7561
- emailVerified: juniorIdentities.emailVerified,
7562
- handle: juniorIdentities.handle,
7563
- providerSubjectId: juniorIdentities.providerSubjectId,
7564
- userDisplayName: juniorUsers.displayName,
7565
- userEmail: juniorUsers.primaryEmail
7566
- }).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));
7567
- const people = /* @__PURE__ */ new Map();
7568
- for (const row of rows) {
7569
- if (people.has(row.providerSubjectId)) continue;
7570
- const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
7571
- const email = row.emailVerified ? row.userEmail?.trim() : void 0;
7572
- people.set(row.providerSubjectId, {
7573
- slackUserId: row.providerSubjectId,
7574
- ...fullName ? { fullName } : void 0,
7575
- ...row.handle ? { slackUserName: row.handle } : void 0,
7576
- ...email ? { email } : void 0
7577
- });
7578
- }
7579
- const unnamed = userIds.filter((id) => {
7580
- const person = people.get(id);
7581
- return !person?.fullName && !person?.slackUserName;
7582
- });
7583
- if (tenantId && unnamed.length > 0) {
7584
- const profiles = await Promise.all(unnamed.map((id) => lookupSlackUser(tenantId, id)));
7585
- unnamed.forEach((id, index) => {
7586
- const profile = profiles[index];
7587
- if (!profile?.fullName && !profile?.userName) return;
7588
- people.set(id, {
7589
- ...people.get(id),
7590
- slackUserId: id,
7591
- ...profile.fullName ? { fullName: profile.fullName } : void 0,
7592
- ...profile.userName ? { slackUserName: profile.userName } : void 0
7593
- });
7594
- });
7595
- }
7596
- return people;
7597
- }
7598
- // src/api/conversations/mentions.ts
7599
- /** Text in one event that the dashboard renders with mentions. */
7600
- function eventMentionTexts(data) {
7601
- switch (data.type) {
7602
- case "message": return data.redacted ? [] : [
7603
- data.text,
7604
- data.trustedSummary,
7605
- ...(data.events ?? []).map((event) => event.trustedSummary)
7606
- ];
7607
- case "assistant_message": return data.parts.map((part) => part.text);
7608
- case "compaction":
7609
- case "handoff": return [data.summary];
7610
- case "structured_event": return [data.presentation.preview, ...(data.presentation.details ?? []).map((detail) => detail.description)];
7611
- default: return [];
7612
- }
7613
- }
7614
- function briefMentionTexts(brief) {
7615
- return [
7616
- brief.summary,
7617
- brief.intent,
7618
- brief.outcome.text,
7619
- ...brief.decisions.flatMap((decision) => [decision.text, decision.by]),
7620
- ...brief.openDecisions.flatMap((decision) => [decision.text, decision.owner]),
7621
- ...brief.facts
7622
- ];
7623
- }
7624
- function withMentions(value, mentions) {
7625
- return mentions.length > 0 ? {
7626
- ...value,
7627
- mentions
7628
- } : value;
7629
- }
7630
- /**
7631
- * Attach the people that each event and the Brief of one conversation
7632
- * mention, resolved at read time.
7633
- */
7634
- async function attachConversationMentions(executor, conversationId, content) {
7635
- const tenantId = (await loadConversationSlackTenantIds(executor, [conversationId])).get(conversationId);
7636
- const sources = content.events.map((event) => ({
7637
- tenantId,
7638
- texts: eventMentionTexts(event.data)
7639
- }));
7640
- if (content.brief) sources.push({
7641
- tenantId,
7642
- texts: briefMentionTexts(content.brief)
7643
- });
7644
- const mentions = await resolveMentions(executor, sources);
7645
- return {
7646
- briefMentions: content.brief ? mentions.at(-1) : [],
7647
- events: content.events.map((event, index) => withMentions(event, mentions[index]))
7648
- };
7649
- }
7650
- /** Attach the people that each activity preview mentions, resolved at read time. */
7651
- async function attachActivityPreviewMentions(executor, previews) {
7652
- const entries = [...previews];
7653
- const tenants = await loadConversationSlackTenantIds(executor, entries.map(([conversationId]) => conversationId));
7654
- const mentions = await resolveMentions(executor, entries.map(([conversationId, preview]) => ({
7655
- tenantId: tenants.get(conversationId),
7656
- texts: [preview.text]
7657
- })));
7658
- return new Map(entries.map(([conversationId, preview], index) => [conversationId, withMentions(preview, mentions[index])]));
7659
- }
7660
7886
  // src/api/conversations/participants.ts
7661
7887
  const identityColumns = {
7662
7888
  avatarUrl: juniorIdentities.avatarUrl,
@@ -7684,9 +7910,10 @@ function participantIdentity(row, userAvatarUrls) {
7684
7910
  };
7685
7911
  return Object.keys(participant).length > 0 ? participant : void 0;
7686
7912
  }
7687
- /** Read actor identities in first-appearance order for each Conversation. */
7688
- async function readConversationParticipants(db, conversationIds) {
7689
- if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
7913
+ function participantKey(row) {
7914
+ return row.userId ? `user:${row.userId}` : `identity:${row.identityId}`;
7915
+ }
7916
+ async function readParticipantRows(db, conversationIds) {
7690
7917
  const [rootRows, eventRows] = await Promise.all([db.select({
7691
7918
  conversationId: juniorConversations.conversationId,
7692
7919
  ...identityColumns
@@ -7695,25 +7922,49 @@ async function readConversationParticipants(db, conversationIds) {
7695
7922
  seq: juniorConversationEvents.seq,
7696
7923
  ...identityColumns
7697
7924
  }).from(juniorConversationEvents).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).innerJoin(juniorConversations, eq(juniorConversations.conversationId, juniorConversationEvents.conversationId)).innerJoin(juniorConversationParticipants, and(eq(juniorConversationParticipants.userId, juniorIdentities.userId), eq(juniorConversationParticipants.rootConversationId, sql`coalesce(${juniorConversations.rootConversationId}, ${juniorConversations.conversationId})`))).where(inArray(juniorConversationEvents.conversationId, [...conversationIds])).orderBy(asc(juniorConversationEvents.conversationId), asc(juniorConversationEvents.actorIdentityId), asc(juniorConversationEvents.seq))]);
7698
- const userAvatarUrls = await readUserAvatarUrls(db, [...rootRows, ...eventRows].flatMap((row) => row.userId ? [row.userId] : []));
7699
- const ordered = /* @__PURE__ */ new Map();
7700
- const add = (conversationId, row) => {
7925
+ return [...rootRows, ...eventRows.sort((left, right) => left.seq - right.seq)];
7926
+ }
7927
+ async function groupParticipants(db, rows) {
7928
+ const userAvatarUrls = await readUserAvatarUrls(db, rows.flatMap((row) => row.userId ? [row.userId] : []));
7929
+ const groups = /* @__PURE__ */ new Map();
7930
+ for (const row of rows) {
7701
7931
  const participant = participantIdentity(row, userAvatarUrls);
7702
- if (!participant) return;
7703
- const group = ordered.get(conversationId) ?? {
7704
- identities: [],
7705
- keys: /* @__PURE__ */ new Set()
7706
- };
7707
- const key = row.userId ? `user:${row.userId}` : `identity:${row.identityId}`;
7708
- if (!group.keys.has(key)) {
7709
- group.keys.add(key);
7710
- group.identities.push(participant);
7711
- }
7712
- ordered.set(conversationId, group);
7932
+ if (!participant) continue;
7933
+ const group = groups.get(row.conversationId) ?? /* @__PURE__ */ new Map();
7934
+ const key = participantKey(row);
7935
+ if (!group.has(key)) group.set(key, participant);
7936
+ groups.set(row.conversationId, group);
7937
+ }
7938
+ return groups;
7939
+ }
7940
+ /** Read actor identities in first-appearance order for each Conversation. */
7941
+ async function readConversationParticipants(db, conversationIds) {
7942
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
7943
+ const groups = await groupParticipants(db, await readParticipantRows(db, conversationIds));
7944
+ return new Map([...groups].map(([conversationId, group]) => [conversationId, [...group.values()]]));
7945
+ }
7946
+ /**
7947
+ * Read the participants of one Conversation, and each of them by each of
7948
+ * their Identity ids. A person is listed under every Identity of their
7949
+ * user, so a pending Message from another provider resolves too.
7950
+ */
7951
+ async function readConversationAuthors(db, conversationId) {
7952
+ const rows = await readParticipantRows(db, [conversationId]);
7953
+ const userIds = [...new Set(rows.flatMap((row) => row.userId ? [row.userId] : []))];
7954
+ const [groups, userIdentities] = await Promise.all([groupParticipants(db, rows), userIds.length > 0 ? db.select({
7955
+ identityId: juniorIdentities.id,
7956
+ userId: juniorIdentities.userId
7957
+ }).from(juniorIdentities).where(inArray(juniorIdentities.userId, userIds)) : []]);
7958
+ const group = groups.get(conversationId) ?? /* @__PURE__ */ new Map();
7959
+ const authors = {};
7960
+ for (const row of [...rows, ...userIdentities]) {
7961
+ const participant = group.get(participantKey(row));
7962
+ if (participant) authors[row.identityId] = participant;
7963
+ }
7964
+ return {
7965
+ authors,
7966
+ participants: [...group.values()]
7713
7967
  };
7714
- for (const row of rootRows) add(row.conversationId, row);
7715
- for (const row of eventRows.sort((left, right) => left.seq - right.seq)) add(row.conversationId, row);
7716
- return new Map([...ordered].map(([conversationId, group]) => [conversationId, group.identities]));
7717
7968
  }
7718
7969
  // src/api/conversations/list.ts
7719
7970
  const CONVERSATION_FEED_LIMIT = 50;
@@ -7928,4 +8179,317 @@ async function readConversationFeedFromSql(options = {}) {
7928
8179
  async function readConversationFeed(options = {}) {
7929
8180
  return conversationFeedSchema.parse(await readConversationFeedFromSql(options));
7930
8181
  }
7931
- export { stopSlackThread as A, actOnReviewItem as B, isAllowedSignInEmail as C, buildSlackInboundMessage as D, recordSkippedConversationMessage as E, bindMessageAuthorIdentityId as F, AGENT_DISPATCH_MAX_AGE_MS as G, readReviewItemForUser as H, startProcessingReaction as I, readHealthReport as J, enqueueAgentDispatch as K, stripLeadingBotMention as L, withNormalizedThreadId as M, stopConversationTurn as N, clearSlackPendingReactions as O, THREAD_OPTOUT_ACK as P, parseContent as R, isAllowedDomain as S, resolveAllowedDomains as T, reviewItemChange as U, closeReviewItemsAnsweredByReply as V, getAttachmentStorage as W, createProductionConversationWorkOptions as _, resolveMentions as a, createSlackWebApiStatusSender as b, readConversationAuxiliaryCostsFromSql as c, notCopiedByFork as d, childAgentTotalsReport as f, resolveSlackTeamDomains as g, conversationSummaryFromStoredConversation as h, attachConversationMentions as i, normalizeIncomingSlackThreadId as j, isSlackMessageStopped as k, ConversationForkError as l, conversationEventHistory as m, readConversationRecordFromSql as n, readFirstHumanMessageTexts as o, readConversationTreeMetricsFromSql as p, GET as q, readConversationParticipants as r, listConversationSidebarAnnotations as s, readConversationFeed as t, forkConversation as u, createProductionSlackWebhookServices as v, readEnvList as w, assertNoLegacyAllowedDomains as x, getProductionSlackAdapter as y, ReviewItemActionError as z };
8182
+ // src/api/conversations/fork.ts
8183
+ /** Fork a Conversation that the viewer can read into a new root they own. */
8184
+ async function forkConversationForViewer(viewer, sourceConversationId, body) {
8185
+ if (!(await readConversationAccessFromSql(getDb(), [sourceConversationId], viewer)).get(sourceConversationId)?.canViewPrivateContent) throwApiError(403, "You do not have access to this conversation.");
8186
+ try {
8187
+ return await forkConversation({
8188
+ sourceConversationId,
8189
+ ...body,
8190
+ actorEmail: viewer.email,
8191
+ ...viewer.displayName ? { actorName: viewer.displayName } : void 0
8192
+ });
8193
+ } catch (error) {
8194
+ if (error instanceof ConversationForkError) throwApiError(409, error.message);
8195
+ throw error;
8196
+ }
8197
+ }
8198
+ /**
8199
+ * Read fork state for the viewer: whether they can fork, and the source and
8200
+ * fork links that they can open.
8201
+ */
8202
+ async function readConversationForks(conversationId, viewer) {
8203
+ const db = getDb();
8204
+ const [row] = await db.select({
8205
+ forkedFrom: juniorConversations.forkedFromConversationId,
8206
+ parent: juniorConversations.parentConversationId
8207
+ }).from(juniorConversations).where(eq(juniorConversations.conversationId, conversationId));
8208
+ const forks = await db.select({ conversationId: juniorConversations.conversationId }).from(juniorConversations).where(eq(juniorConversations.forkedFromConversationId, conversationId)).orderBy(asc(juniorConversations.createdAt)).limit(50);
8209
+ const forkedFrom = row?.forkedFrom ?? void 0;
8210
+ const ids = forks.map((fork) => fork.conversationId);
8211
+ const access = await readConversationAccessFromSql(db, [
8212
+ conversationId,
8213
+ ...ids,
8214
+ ...forkedFrom ? [forkedFrom] : []
8215
+ ], viewer);
8216
+ const canOpen = (id) => access.get(id)?.canViewPrivateContent;
8217
+ if (!canOpen(conversationId)) return {
8218
+ canFork: false,
8219
+ forks: []
8220
+ };
8221
+ const result = {
8222
+ canFork: !row?.parent,
8223
+ forks: ids.filter(canOpen)
8224
+ };
8225
+ if (forkedFrom && canOpen(forkedFrom)) {
8226
+ const [source] = await db.select({ title: juniorConversations.title }).from(juniorConversations).where(eq(juniorConversations.conversationId, forkedFrom));
8227
+ if (source) {
8228
+ result.forkedFromConversationId = forkedFrom;
8229
+ if (source.title) result.forkedFromTitle = source.title;
8230
+ }
8231
+ }
8232
+ return result;
8233
+ }
8234
+ // src/api/conversations/agent-tree.ts
8235
+ /** Bound the parent chain that one agent tree reads. */
8236
+ const MAX_AGENT_TREE_DEPTH = 16;
8237
+ const CHILD_LABEL = "Subagent";
8238
+ async function readParentConversationId(conversationId) {
8239
+ const [row] = await getDb().select({ parentId: juniorConversations.parentConversationId }).from(juniorConversations).where(eq(juniorConversations.conversationId, conversationId));
8240
+ return row?.parentId ?? void 0;
8241
+ }
8242
+ /**
8243
+ * Read the agent tree of a child Conversation: its parent chain, root first,
8244
+ * with the child last. Returns undefined for a root Conversation. Each
8245
+ * Conversation has its own access check, so a viewer sees a label only where
8246
+ * they can see private content.
8247
+ */
8248
+ async function readConversationAgentTree(conversationId, viewer) {
8249
+ const ids = [conversationId];
8250
+ let parentId = await readParentConversationId(conversationId);
8251
+ while (parentId && !ids.includes(parentId) && ids.length < MAX_AGENT_TREE_DEPTH) {
8252
+ ids.unshift(parentId);
8253
+ parentId = await readParentConversationId(parentId);
8254
+ }
8255
+ if (ids.length < 2) return;
8256
+ const root = parentId ? void 0 : await getConversationStore().get({ conversationId: ids[0] });
8257
+ const [access, automationTitleByConversation, firstHumanMessageTextByConversation] = await Promise.all([
8258
+ readConversationAccessFromSql(getDb(), ids, viewer),
8259
+ readAutomationTitlesByConversationId(root ? [root] : []),
8260
+ readFirstHumanMessageTexts(getDb(), root ? [root] : [])
8261
+ ]);
8262
+ return await Promise.all(ids.map(async (id) => {
8263
+ const itemAccess = access.get(id);
8264
+ if (root?.conversationId === id) return {
8265
+ conversationId: id,
8266
+ label: conversationSummaryFromStoredConversation({
8267
+ access: itemAccess,
8268
+ automationTitle: automationTitleByConversation.get(id),
8269
+ conversation: root,
8270
+ durationMs: 0,
8271
+ firstHumanMessageText: firstHumanMessageTextByConversation.get(id)
8272
+ }).displayTitle
8273
+ };
8274
+ if (!itemAccess?.canViewPrivateContent) return {
8275
+ conversationId: id,
8276
+ label: CHILD_LABEL
8277
+ };
8278
+ const agent = await getChildAgentLabel(id);
8279
+ return {
8280
+ conversationId: id,
8281
+ label: agent?.title ?? agent?.agentName ?? CHILD_LABEL
8282
+ };
8283
+ }));
8284
+ }
8285
+ // src/api/conversations/read-position.ts
8286
+ /** Read how far the viewer has read one Conversation, if they have read it. */
8287
+ async function readConversationReadPosition(viewer, conversationId) {
8288
+ const [row] = await getDb().select({ lastReadSeq: juniorConversationReadPositions.lastReadSeq }).from(juniorConversationReadPositions).where(and(eq(juniorConversationReadPositions.userId, viewer.id), eq(juniorConversationReadPositions.conversationId, conversationId))).limit(1);
8289
+ return row;
8290
+ }
8291
+ /**
8292
+ * Move the viewer's read position forward.
8293
+ *
8294
+ * The stored value never goes back, so devices can report in any order. A
8295
+ * value past the last event is cut to the last event, so a bad client cannot
8296
+ * hide messages that arrive later.
8297
+ */
8298
+ async function markConversationRead(viewer, conversationId, body) {
8299
+ const db = getDb();
8300
+ const [conversation] = await db.select({ lastSeq: max(juniorConversationEvents.seq) }).from(juniorConversations).leftJoin(juniorConversationEvents, eq(juniorConversationEvents.conversationId, juniorConversations.conversationId)).where(eq(juniorConversations.conversationId, conversationId)).groupBy(juniorConversations.conversationId);
8301
+ if (!conversation) throwApiError(404, "Conversation not found.");
8302
+ const [row] = await db.insert(juniorConversationReadPositions).values({
8303
+ conversationId,
8304
+ lastReadSeq: Math.min(body.lastReadSeq, conversation.lastSeq ?? 0),
8305
+ updatedAt: /* @__PURE__ */ new Date(),
8306
+ userId: viewer.id
8307
+ }).onConflictDoUpdate({
8308
+ target: [juniorConversationReadPositions.userId, juniorConversationReadPositions.conversationId],
8309
+ set: {
8310
+ lastReadSeq: sql`greatest(${juniorConversationReadPositions.lastReadSeq}, excluded.last_read_seq)`,
8311
+ updatedAt: sql`excluded.updated_at`
8312
+ }
8313
+ }).returning({ lastReadSeq: juniorConversationReadPositions.lastReadSeq });
8314
+ return row;
8315
+ }
8316
+ // src/api/conversations/detail.ts
8317
+ /** Project stored metadata and a bounded event page into a signed history cursor. */
8318
+ function projectConversationDetail(args) {
8319
+ const { conversation } = args;
8320
+ const conversationId = conversation.conversationId;
8321
+ const transcriptPurgedAtMs = conversation.transcriptPurgedAtMs;
8322
+ const canExposePayload = args.access?.canViewPrivateContent ?? false;
8323
+ const modelUsage = transcriptPurgedAtMs === void 0 ? args.modelUsage : [];
8324
+ const sentryConversationUrl = buildSentryConversationUrl(conversationId);
8325
+ return {
8326
+ ...conversationSummaryFromStoredConversation({
8327
+ access: args.access,
8328
+ ...args.archivedAtMs === void 0 ? void 0 : { archivedAtMs: args.archivedAtMs },
8329
+ automationTitle: args.automationTitle,
8330
+ auxiliaryCosts: args.auxiliaryCosts,
8331
+ ...args.childAgentTotals ? { childAgentTotals: args.childAgentTotals } : void 0,
8332
+ conversation,
8333
+ durationMs: args.durationMs,
8334
+ firstHumanMessageText: args.firstHumanMessageText,
8335
+ ...args.locationId ? { locationId: args.locationId } : void 0,
8336
+ ...args.teamDomainByTeamId ? { teamDomainByTeamId: args.teamDomainByTeamId } : void 0,
8337
+ usage: args.usage
8338
+ }),
8339
+ annotations: canExposePayload ? args.annotations : [],
8340
+ sidebarAnnotations: canExposePayload ? args.sidebarAnnotations : void 0,
8341
+ ...canExposePayload && args.brief ? { brief: args.brief } : void 0,
8342
+ events: args.events,
8343
+ ...canExposePayload && args.participants.length > 0 ? {
8344
+ authors: args.authors,
8345
+ participants: args.participants
8346
+ } : void 0,
8347
+ ...args.previousSeq !== void 0 ? { previousCursor: encodeConversationCursor({
8348
+ conversationId,
8349
+ seq: args.previousSeq
8350
+ }) } : void 0,
8351
+ ...modelUsage.length > 0 ? { modelUsage } : void 0,
8352
+ eventHistory: conversationEventHistory({
8353
+ canExposePayload,
8354
+ ...transcriptPurgedAtMs === void 0 ? void 0 : { transcriptPurgedAtMs }
8355
+ }),
8356
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
8357
+ ...sentryConversationUrl ? { sentryConversationUrl } : void 0,
8358
+ ...args.sourceTask ? { sourceTask: args.sourceTask } : void 0
8359
+ };
8360
+ }
8361
+ async function readConversationDetailFromSql(conversationId, options) {
8362
+ const record = await readConversationRecordFromSql(conversationId);
8363
+ if (!record) return void 0;
8364
+ const archivedAtMs = options.viewer ? await readConversationArchivedAt(options.viewer, conversationId) : void 0;
8365
+ const executor = getSqlExecutor();
8366
+ const [accessByConversation, annotations, automationTitleByConversation, auxiliaryCostsByConversation, briefVersion, firstHumanMessageTextByConversation, modelUsage, metricsByConversation, people, sourceTask, teamDomainByTeamId] = await Promise.all([
8367
+ readConversationAccessFromSql(getDb(), [conversationId], options.viewer),
8368
+ listConversationAnnotations(getDb(), conversationId),
8369
+ readAutomationTitlesByConversationId([record.conversation]),
8370
+ readConversationAuxiliaryCostsFromSql(getDb(), [conversationId]),
8371
+ readLatestConversationBrief(getDb(), conversationId),
8372
+ readFirstHumanMessageTexts(getDb(), [record.conversation]),
8373
+ record.conversation.transcriptPurgedAtMs === void 0 ? readConversationModelUsageFromSql(executor, { conversationId }) : Promise.resolve([]),
8374
+ readConversationTreeMetricsFromSql(getDb(), [conversationId]),
8375
+ readConversationAuthors(getDb(), conversationId),
8376
+ readConversationSourceTask({
8377
+ conversationId,
8378
+ ...options.viewer ? { viewer: options.viewer } : void 0
8379
+ }),
8380
+ resolveSlackTeamDomains(record.conversation.location?.provider === "slack" ? [record.conversation.location.teamId] : [])
8381
+ ]);
8382
+ const access = accessByConversation.get(conversationId);
8383
+ const sidebarAnnotations = access?.canViewPrivateContent ? (await listConversationSidebarAnnotations([conversationId], /* @__PURE__ */ new Map([[conversationId, annotations]])))[conversationId] : void 0;
8384
+ const page = record.conversation.transcriptPurgedAtMs === void 0 ? await readConversationEventPage(executor, {
8385
+ canExposePayload: access?.canViewPrivateContent ?? false,
8386
+ conversationId,
8387
+ limit: options.limit
8388
+ }) : { events: [] };
8389
+ const brief = access?.canViewPrivateContent ? briefVersion?.content : void 0;
8390
+ const { briefMentions, events } = await attachConversationMentions(executor, conversationId, {
8391
+ ...brief ? { brief } : void 0,
8392
+ events: page.events
8393
+ });
8394
+ const metrics = metricsByConversation.get(conversationId);
8395
+ const auxiliaryCosts = auxiliaryCostsByConversation.get(conversationId);
8396
+ const childAgentTotals = childAgentTotalsReport(metrics, auxiliaryCosts);
8397
+ return projectConversationDetail({
8398
+ ...record,
8399
+ access,
8400
+ ...archivedAtMs === void 0 ? void 0 : { archivedAtMs },
8401
+ automationTitle: automationTitleByConversation.get(conversationId),
8402
+ annotations,
8403
+ sidebarAnnotations,
8404
+ auxiliaryCosts: auxiliaryCosts?.total,
8405
+ ...childAgentTotals ? { childAgentTotals } : void 0,
8406
+ ...briefVersion ? { brief: {
8407
+ content: briefVersion.content,
8408
+ ...briefMentions.length > 0 ? { mentions: briefMentions } : void 0,
8409
+ updatedAt: briefVersion.createdAt.toISOString(),
8410
+ version: briefVersion.version
8411
+ } } : void 0,
8412
+ durationMs: metrics?.durationMs ?? record.durationMs,
8413
+ events,
8414
+ firstHumanMessageText: firstHumanMessageTextByConversation.get(conversationId),
8415
+ modelUsage,
8416
+ authors: people.authors,
8417
+ participants: people.participants,
8418
+ ...sourceTask ? { sourceTask } : void 0,
8419
+ teamDomainByTeamId,
8420
+ ...page.previousSeq === void 0 ? void 0 : { previousSeq: page.previousSeq },
8421
+ usage: metrics?.usage ?? record.usage ?? void 0
8422
+ });
8423
+ }
8424
+ /** Load one conversation from its canonical event history. */
8425
+ async function readConversationDetail(conversationId, options = {}) {
8426
+ const report = await readConversationDetailFromSql(conversationId, {
8427
+ ...options,
8428
+ limit: options.limit ?? 500
8429
+ });
8430
+ if (!report) return void 0;
8431
+ const [forks, agentTree, readPosition] = await Promise.all([
8432
+ readConversationForks(conversationId, options.viewer),
8433
+ readConversationAgentTree(conversationId, options.viewer),
8434
+ options.viewer ? readConversationReadPosition(options.viewer, conversationId) : void 0
8435
+ ]);
8436
+ return conversationDetailReportSchema.parse({
8437
+ ...report,
8438
+ ...forks,
8439
+ ...agentTree ? { agentTree } : void 0,
8440
+ ...readPosition ? { readPosition } : void 0
8441
+ });
8442
+ }
8443
+ // src/api/conversations/stop.ts
8444
+ /**
8445
+ * Stop the active Turn for one conversation participant.
8446
+ *
8447
+ * The stop also removes the human messages queued before it. The viewer's own
8448
+ * web text comes back in the response so the dashboard can restore it to the
8449
+ * composer instead of losing it.
8450
+ */
8451
+ async function stopConversationTurnForViewer(viewer, conversationId, services) {
8452
+ const conversationStore = getConversationStore();
8453
+ if (!await conversationStore.get({ conversationId })) throwApiError(404, "Conversation not found.");
8454
+ if (!(await readConversationAccessFromSql(getDb(), [conversationId], viewer)).get(conversationId)?.isParticipant) throwApiError(403, "Only conversation participants can stop this turn.");
8455
+ try {
8456
+ const result = await stopConversationTurn({
8457
+ conversationId,
8458
+ conversationStore,
8459
+ queue: services.queue
8460
+ });
8461
+ if (result.status === "no_work") return {
8462
+ conversationId,
8463
+ status: "no_work",
8464
+ stoppedInboundMessageIds: [],
8465
+ stoppedMessages: []
8466
+ };
8467
+ await clearSlackPendingReactions({
8468
+ getSlackAdapter: getProductionSlackAdapter,
8469
+ messages: result.pendingMessages
8470
+ });
8471
+ return {
8472
+ conversationId,
8473
+ status: "requested",
8474
+ stoppedInboundMessageIds: result.pendingMessages.map((message) => message.inboundMessageId),
8475
+ stoppedMessages: viewerWebMessages(result.pendingMessages, viewer)
8476
+ };
8477
+ } catch (error) {
8478
+ throwApiError(500, "Unable to stop the active turn.", error);
8479
+ }
8480
+ }
8481
+ function viewerWebMessages(messages, viewer) {
8482
+ const viewerUserId = webActorFromEmail(viewer.email).userId;
8483
+ return messages.flatMap((message) => {
8484
+ if (message.source !== "web") return [];
8485
+ const metadata = legacyWebMailboxMetadataSchema.safeParse(message.input.metadata);
8486
+ if (!metadata.success) return [];
8487
+ if (metadata.data.authorUserId !== viewerUserId) return [];
8488
+ const text = message.input.text.trim();
8489
+ return text ? [{
8490
+ inboundMessageId: message.inboundMessageId,
8491
+ text
8492
+ }] : [];
8493
+ });
8494
+ }
8495
+ export { recordSkippedConversationMessage as A, startProcessingReaction as B, getProductionSlackAdapter as C, isAllowedSignInEmail as D, isAllowedDomain as E, normalizeIncomingSlackThreadId as F, closeReviewItemsAnsweredByReply as G, parseContent as H, withNormalizedThreadId as I, getAttachmentStorage as J, readReviewItemForUser as K, stopConversationTurn as L, clearSlackPendingReactions as M, isSlackMessageStopped as N, readEnvList as O, stopSlackThread as P, readHealthReport as Q, THREAD_OPTOUT_ACK as R, createProductionSlackWebhookServices as S, assertNoLegacyAllowedDomains as T, ReviewItemActionError as U, stripLeadingBotMention as V, actOnReviewItem as W, enqueueAgentDispatch as X, AGENT_DISPATCH_MAX_AGE_MS as Y, GET as Z, archiveConversation as _, readConversationFeed as a, throwApiError as b, childAgentTotalsReport as c, conversationSummaryFromStoredConversation as d, attachConversationMentions as f, encodeConversationCursor as g, decodeConversationCursor as h, forkConversationForViewer as i, buildSlackInboundMessage as j, resolveAllowedDomains as k, readConversationTreeMetricsFromSql as l, readFirstHumanMessageTexts as m, readConversationDetail as n, readConversationRecordFromSql as o, resolveMentions as p, reviewItemChange as q, markConversationRead as r, readConversationAuxiliaryCostsFromSql as s, stopConversationTurnForViewer as t, conversationEventHistory as u, emptyResponse as v, createSlackWebApiStatusSender as w, createProductionConversationWorkOptions as x, jsonResponse as y, bindMessageAuthorIdentityId as z };