@sentry/junior 0.271.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 (75) hide show
  1. package/dist/{adapter-Bqbtr1RV.js → adapter-umap1cth.js} +1 -1
  2. package/dist/{admin-CWBjzyN0.js → admin-0o6E_3lo.js} +1 -1
  3. package/dist/{agent-runner-BiHsH8O_.js → agent-runner-D7Qz-lGO.js} +899 -631
  4. package/dist/api/schema.d.ts +2 -2
  5. package/dist/api/schema.js +5 -5
  6. package/dist/api.d.ts +1 -1
  7. package/dist/api.js +62 -537
  8. package/dist/{app-C81xfpNa.d.ts → app-BcyhJq8x.d.ts} +4 -9
  9. package/dist/app.d.ts +1 -1
  10. package/dist/app.js +673 -134
  11. package/dist/{assistant-reply-DgtX9DaJ.js → assistant-reply-wZ7i4OFa.js} +1 -1
  12. package/dist/{automation-Dwh_xcqM.js → automation-yffXcZt9.js} +1 -1
  13. package/dist/{briefs-0FeSJHQ3.js → briefs-CgMnlpIp.js} +4 -4
  14. package/dist/{catalog-runtime-Bw9kjGVu.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-B0jlQSgO.js → client-DhqbV0Fe.js} +63 -5
  24. package/dist/{reporting-schema-CZREhYc-.js → common-Eu6sx09c.js} +4 -1
  25. package/dist/{config-DrAeD8sr.js → config-C6FTd7PX.js} +7 -15
  26. package/dist/{connections-BzN2K0G9.js → connections-Dq72KajR.js} +11 -11
  27. package/dist/{conversation-AhOVzN_k.js → conversation-T4oabbBi.js} +22 -1
  28. package/dist/{conversation-privacy-D1NhUxyo.js → conversation-privacy-BeRgrsph.js} +1 -1
  29. package/dist/{credential-sync-DpNWl4yy.js → credential-sync-BmCbHAp4.js} +15 -16
  30. package/dist/{db-DSabI7jJ.d.ts → db-DOzBGLn5.d.ts} +11 -1
  31. package/dist/{executor-C2cRw0hZ.js → executor-BafgvQcc.js} +25 -7
  32. package/dist/instrumentation.js +114 -3
  33. package/dist/{job-queue-CEeLYkSj.js → job-queue-C4lphW9Y.js} +3 -3
  34. package/dist/{logging-DuwXUuIY.js → logging-CNs1e04n.js} +6 -1
  35. package/dist/message-CETC5lsl.js +186 -0
  36. package/dist/nitro.d.ts +1 -1
  37. package/dist/nitro.js +3 -3
  38. package/dist/{oauth-callback-server-oM5TJt34.js → oauth-callback-server-DcQZsKMm.js} +2 -2
  39. package/dist/{credentialed-mh7HBqey.js → projection-BOpi891B.js} +4576 -49
  40. package/dist/{prompt-BPSdXYXV.js → prompt-B1ySjUyf.js} +1 -1
  41. package/dist/{registry-BPtbAVFo.js → registry-B-yUrv3h.js} +2 -2
  42. package/dist/{resolve-D44G1jJm.js → resolve-DqVtd4Q9.js} +3 -3
  43. package/dist/{result-CvP8c5bb.js → result-DQwTiLTw.js} +16 -12
  44. package/dist/{runner-FIGIKvk2.js → runner-Dco5qMq6.js} +11 -11
  45. package/dist/{sandbox-egress-signals-C_JzucwP.js → sandbox-egress-signals-DFc_9Mcv.js} +1 -1
  46. package/dist/{schema-DKZgO-xf.js → schema-5xHDGlBW.js} +10 -9
  47. package/dist/{schema-DPCvePop.d.ts → schema-9OGdsaxF.d.ts} +28 -3
  48. package/dist/{service-account-1mJGRAvx.js → service-account-CWfk9Cs-.js} +3 -3
  49. package/dist/{service-account-CHO3YXpt.js → service-account-smdPFWNy.js} +3 -3
  50. package/dist/{session-stop-DuUB1rwQ.js → session-stop-DaT_7tYa.js} +50 -24
  51. package/dist/{skills-DHK7L9Eo.js → skills-BbtJSF2-.js} +2 -2
  52. package/dist/{spawn-CKl_ySIq.js → spawn-Ceikdjkp.js} +2 -2
  53. package/dist/{list-DjJJ5K0x.js → stop-dKUc-x2K.js} +1017 -469
  54. package/dist/{store-Di0E4EvU.js → store-C1xuZcLd.js} +48 -31
  55. package/dist/{store-BF2XEeQz.js → store-DaxUuOmD.js} +3 -3
  56. package/dist/{task-queue-CC7IFtbT.js → task-queue-ymAh_-P1.js} +4 -4
  57. package/dist/{task-runner-DKZtMFwa.js → task-runner-vlH6pEm3.js} +6 -6
  58. package/dist/{thread-state-BQ1T3eeF.js → thread-state-BkUpE9Ae.js} +4 -5
  59. package/dist/{turn-failure-response-B5eSFoHn.js → turn-failure-response-DIsD7ip_.js} +7 -8
  60. package/dist/{turn-lifecycle-CTLEIEOn.js → turn-lifecycle-BdPKdPS8.js} +1 -1
  61. package/dist/turn-session-routing-BdCTDq9m.js +1625 -0
  62. package/dist/{validation-Cjo-3yt3.js → validation-BQjCfh84.js} +1 -1
  63. package/dist/{version-D30MBLLw.js → version-BpiFGZZW.js} +1 -1
  64. package/dist/version.js +1 -1
  65. package/dist/{viewer-D3jhuP66.js → viewer-C6Y1aXjZ.js} +49 -21
  66. package/dist/{work-CL9QwQcl.js → work-ChPh_YEh.js} +27 -39
  67. package/dist/{worker-S2P7IJDI.js → worker-Be_sK_ou.js} +6 -6
  68. package/migrations/0072_conversation_read_positions.sql +10 -0
  69. package/migrations/meta/0072_snapshot.json +4064 -0
  70. package/migrations/meta/_journal.json +7 -0
  71. package/package.json +4 -4
  72. package/dist/message-CRe3WBKc.js +0 -59
  73. package/dist/turn-session-routing-BFVp8TbU.js +0 -6072
  74. package/migrations/AGENTS.md +0 -8
  75. 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-BFVp8TbU.js";
2
- import { a as completeText, p as escapeXml, r as completeDecision, y as isProviderRetryError } from "./client-B0jlQSgO.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-DrAeD8sr.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-DuwXUuIY.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-D3jhuP66.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 { S as juniorCodeChanges, g as juniorConversationBriefs, y as juniorConversationEvents } from "./executor-C2cRw0hZ.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-BPtbAVFo.js";
13
- import { At as RetryableDeliveryError, Ct as conversationSubtreeColumns, Dt as withoutSavedSteeringMessages, Et as buildSteeringPiMessage, F as getConversationInfo, L as listThreadReplies, M as readSlackRawMessageContext, N as resolveSlackChannelIdFromMessage, P as resolveSlackChannelIdFromThreadId, Pt as shouldEmitDevAgentTrace, St as conversationSubtree, b as renderBlockText, ct as recordWebConversationActivity, ht as storeAttachment, i as buildAuthPauseResponse, j as parseSlackThreadId, k as runWithSlackInstallation, lt as webActorFromEmail, n as executeAgentRun, p as latestProgressStatus, r as createContextCompactor, st as createConversationId, t as createAgentRunner, tt as deliverReviewItemChange, ut as resolveConversationDestination, y as renderAttachmentText } from "./agent-runner-BiHsH8O_.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-mh7HBqey.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-Bqbtr1RV.js";
17
- import { W as readMessageAttachments, g as conversationFeedSchema } from "./conversation-AhOVzN_k.js";
18
- import { a as EVENT_SYSTEM_ACTOR, o as isEventConversationMessage } from "./prompt-BPSdXYXV.js";
19
- import { B as ConversationTurnBoundaryError, G as isCooperativeTurnYieldError, H as TurnInputCommitLostError, J as markTurnClosed, K as isTurnInputCommitLostError, U as TurnInputDeferredError, V as CooperativeTurnYieldError, W as getConversationTurnBoundaryError, X as markTurnFailed, Y as markTurnCompleted, Z as startActiveTurn, q as isTurnInputDeferredError } from "./store-Di0E4EvU.js";
20
- import { n as getAssistantReplyText } from "./assistant-reply-DgtX9DaJ.js";
21
- import { C as maybePostAutomatedTurnLimitNotice, D as recordFinishedTurnForAutomatedLimit, I as runDispatchOutcome, M as finishedRunReply, N as postsRunNotices, T as sendSlackReply, c as isEventMailboxMetadata, i as skipInputBeforeConversationStop, j as deliversFinalText, l as renderEventBatchText, s as eventBatchMessageEvents, u as timerWatchCredentialSubject, w as resolveSlackReplyTarget, x as joinMailboxText } from "./session-stop-DuUB1rwQ.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-CEeLYkSj.js";
24
- import { D as stopConversationWork, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, r as appendAndEnqueueInboundMessage, w as requestConversationWork } from "./store-BF2XEeQz.js";
25
- import { t as healthReportSchema } from "./reporting-schema-CZREhYc-.js";
26
- import { a as withAgentInvocationResultActors, c as resolveMailboxTurnWork, d as legacyWebMailboxMetadataSchema, l as sourceFromTurnInput, n as enqueueAgentInvocationResult, o as getAuthPausedConversationTurnId, s as getEventBatchDelayMs, t as createChildConversationWorker, u as turnInputFactsFromConversationMessage } from "./result-CvP8c5bb.js";
27
- import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-CTLEIEOn.js";
28
- import { a as AgentRunError, o as executeTurn } from "./work-CL9QwQcl.js";
29
- import { a as persistThreadStateById, i as persistThreadRuntimeState, n as getPersistedSandboxState, o as coerceThreadConversationState, r as getPersistedThreadState, t as getLocationConfigurationService } from "./thread-state-BQ1T3eeF.js";
30
- import { c as deleteWebAuthorization, d as createPausedTurns, f as getPausedTurnRequest, h as getTurnAuthorization, m as wakePausedTurn, s as createWebAuthorization } from "./connections-BzN2K0G9.js";
31
- import { t as bindMessageAgent } from "./message-CRe3WBKc.js";
32
- import { t as bindSpawnAgent } from "./spawn-CKl_ySIq.js";
33
- import { n as scheduleSessionCompletedPluginTasks, t as processPluginTask } from "./task-runner-DKZtMFwa.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-B5eSFoHn.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, ne, 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() {
@@ -471,10 +472,6 @@ function reviewItemChange(action, user) {
471
472
  kind: "submitted",
472
473
  user
473
474
  };
474
- if (action === "complete") return {
475
- kind: "completed",
476
- user
477
- };
478
475
  return {
479
476
  kind: "dismissed",
480
477
  user
@@ -499,7 +496,7 @@ function submittedAnswer(item, answer) {
499
496
  }
500
497
  /**
501
498
  * Do one person action on a Review item and tell its linked Conversations.
502
- * The first submit, complete, or dismiss closes the item for every
499
+ * The first submit or dismiss closes the item for every
503
500
  * assignee. A retry of the same close sends any missing messages again and
504
501
  * succeeds, so a client can retry after a failure. With `firstCloseOnly`,
505
502
  * only the call that closes the item succeeds, and every other call gets a
@@ -537,7 +534,6 @@ async function actOnReviewItem(args) {
537
534
  let closedNow = false;
538
535
  if (item.status === "open") {
539
536
  if (!canActOnReviewItem(item, user.id)) throw new ReviewItemActionError("forbidden", "Only an assignee can act on this item.");
540
- if (action === "complete" && item.kind === "action") throw new ReviewItemActionError("invalid", "An action item needs an answer. Submit it or dismiss it.");
541
537
  const answer = action === "submit" ? submittedAnswer(item, args.answer) : void 0;
542
538
  closedNow = await closeReviewItem(db, {
543
539
  closedByUserId: user.id,
@@ -2270,7 +2266,6 @@ function getPendingRecords(work) {
2270
2266
  }
2271
2267
  /** Build the worker run function for queued Slack conversation work. */
2272
2268
  function createSlackConversationWorker(options) {
2273
- const crossActorMidRunMode = options.crossActorMidRunMode ?? botConfig.crossActorMidRunMode;
2274
2269
  return async (context) => {
2275
2270
  const adapter = options.getSlackAdapter();
2276
2271
  const actorLookup = options.lookupSlackUser ?? lookupSlackUser;
@@ -2357,7 +2352,7 @@ function createSlackConversationWorker(options) {
2357
2352
  adapter,
2358
2353
  record
2359
2354
  })
2360
- })).filter((candidate) => crossActorMidRunMode === "steer" || requireSlackAuthorId(candidate.message) === activeAuthorId || hasSteeringOverride(candidate.message.text));
2355
+ })).filter((candidate) => requireSlackAuthorId(candidate.message) === activeAuthorId);
2361
2356
  return candidates.length > 0 ? await accept(candidates) : [];
2362
2357
  });
2363
2358
  };
@@ -3518,17 +3513,6 @@ async function maybeApplyProviderDefaultConfigRequest(args) {
3518
3513
  });
3519
3514
  return { text: `Default GitHub repo set to \`${repo}\`.` };
3520
3515
  }
3521
- // src/chat/conversations/destination-visibility.ts
3522
- /** Read confirmed visibility from the current signal or persisted destination. */
3523
- async function resolveDestinationVisibility(args) {
3524
- if (args.visibility) return args.visibility;
3525
- if (args.destination.platform === "local") return "private";
3526
- return await getConversationStore().getDestinationVisibility({
3527
- provider: "slack",
3528
- providerDestinationId: args.destination.channelId,
3529
- providerTenantId: args.destination.teamId
3530
- });
3531
- }
3532
3516
  // src/chat/providers/slack/input.ts
3533
3517
  function renderRecentThreadMessageLines(conversationContext, messages) {
3534
3518
  const messagesForContext = messages.filter((queued) => {
@@ -6393,21 +6377,21 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
6393
6377
  if (stopSignal?.aborted) return await completeCancelledTurn();
6394
6378
  if (!isResume) {
6395
6379
  const pendingAuthorizationActorId = conversation.processing.pendingAuth?.actorId;
6396
- const authParked = (await listTurnSummaries(context.conversationId)).filter((summary) => summary.surface === "api" && !summary.dispatchId && summary.state === "paused" && summary.resumeReason === "auth");
6397
- for (const parked of authParked) {
6380
+ const superseded = (await listActiveMailboxTurns(context.conversationId)).filter((record) => record.turnId !== turnId);
6381
+ for (const stale of superseded) {
6398
6382
  if (stopSignal?.aborted) return await completeCancelledTurn();
6399
6383
  await abandonTurnRecord({
6400
6384
  conversationId: context.conversationId,
6401
- turnId: parked.turnId,
6402
- errorMessage: "Auth-paused Turn superseded by new input"
6385
+ turnId: stale.turnId,
6386
+ errorMessage: "Turn superseded by new input"
6403
6387
  });
6404
6388
  markTurnClosed({
6405
6389
  conversation,
6406
6390
  nowMs: Date.now(),
6407
- sessionId: parked.turnId
6391
+ sessionId: stale.turnId
6408
6392
  });
6409
6393
  }
6410
- if (authParked.length > 0 && pendingAuthorizationActorId) await deleteWebAuthorization({
6394
+ if (superseded.some((record) => record.state === "paused" && record.resumeReason === "auth") && pendingAuthorizationActorId) await deleteWebAuthorization({
6411
6395
  actorId: pendingAuthorizationActorId,
6412
6396
  conversationId: context.conversationId
6413
6397
  });
@@ -6788,6 +6772,139 @@ function createProductionConversationWorkOptions(options) {
6788
6772
  waitUntil: options.waitUntil
6789
6773
  });
6790
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
+ }
6791
6908
  // src/chat/slack/location-url.ts
6792
6909
  /** Parse a Slack workspace subdomain used in `https://{domain}.slack.com` URLs. */
6793
6910
  function parseSlackTeamDomain(value) {
@@ -6886,43 +7003,487 @@ async function resolveSlackTeamDomains(teamIds) {
6886
7003
  }));
6887
7004
  return new Map(resolved.filter((entry) => entry !== void 0));
6888
7005
  }
6889
- // src/api/conversations/shared.ts
6890
- /** Format a Slack destination without exposing private channel names. */
6891
- function slackStatsLocationLabel(input) {
6892
- const channelId = input.channel;
6893
- if (!channelId) return void 0;
6894
- if (input.channelNameRedacted && input.channelName) return input.channelName;
6895
- const name = input.channelName?.replace(/^#/, "");
6896
- if (channelId.startsWith("D")) return "Direct Message";
6897
- if (channelId.startsWith("C")) return name ? `#${name}` : "Public Channel";
6898
- if (channelId.startsWith("G")) {
6899
- if (name?.startsWith("mpdm-")) return "Group DM";
6900
- return "Private Channel";
6901
- }
6902
- return name || channelId;
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
+ )`;
6903
7021
  }
6904
- /** Return the generic display label for a conversation surface. */
6905
- function surfaceFallbackLabel(surface) {
6906
- if (surface === "scheduler") return "Scheduler";
6907
- if (surface === "api") return "API";
6908
- if (surface === "internal") return "Internal";
6909
- return "Conversation";
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
+ }));
6910
7104
  }
6911
- // src/api/conversations/projection.ts
6912
- const PRIVATE_CONVERSATION_LABEL = "Private Conversation";
6913
- function privateConversationLabel(slackConversation) {
6914
- if (!slackConversation) return PRIVATE_CONVERSATION_LABEL;
6915
- return slackConversation.visibility === "private" ? formatSlackConversationRedactedLabel(slackConversation) ?? PRIVATE_CONVERSATION_LABEL : PRIVATE_CONVERSATION_LABEL;
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)}`);
6916
7147
  }
6917
- function surfaceFromConversationId(conversationId) {
6918
- if (parseSlackThreadId(conversationId)) return "slack";
6919
- if (conversationId.startsWith("scheduler:")) return "scheduler";
6920
- if (conversationId.startsWith("api:")) return "api";
6921
- return "internal";
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;
6922
7154
  }
6923
- function surfaceFromSource(source, conversationId) {
6924
- if (source === "slack" || source === "api" || source === "web" || source === "scheduler") return source === "web" ? "api" : source;
6925
- return surfaceFromConversationId(conversationId);
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)
7433
+ });
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
+ };
7439
+ }
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])]));
7449
+ }
7450
+ // src/api/conversations/shared.ts
7451
+ /** Format a Slack destination without exposing private channel names. */
7452
+ function slackStatsLocationLabel(input) {
7453
+ const channelId = input.channel;
7454
+ if (!channelId) return void 0;
7455
+ if (input.channelNameRedacted && input.channelName) return input.channelName;
7456
+ const name = input.channelName?.replace(/^#/, "");
7457
+ if (channelId.startsWith("D")) return "Direct Message";
7458
+ if (channelId.startsWith("C")) return name ? `#${name}` : "Public Channel";
7459
+ if (channelId.startsWith("G")) {
7460
+ if (name?.startsWith("mpdm-")) return "Group DM";
7461
+ return "Private Channel";
7462
+ }
7463
+ return name || channelId;
7464
+ }
7465
+ /** Return the generic display label for a conversation surface. */
7466
+ function surfaceFallbackLabel(surface) {
7467
+ if (surface === "scheduler") return "Scheduler";
7468
+ if (surface === "api") return "API";
7469
+ if (surface === "internal") return "Internal";
7470
+ return "Conversation";
7471
+ }
7472
+ // src/api/conversations/projection.ts
7473
+ const PRIVATE_CONVERSATION_LABEL = "Private Conversation";
7474
+ function privateConversationLabel(slackConversation) {
7475
+ if (!slackConversation) return PRIVATE_CONVERSATION_LABEL;
7476
+ return slackConversation.visibility === "private" ? formatSlackConversationRedactedLabel(slackConversation) ?? PRIVATE_CONVERSATION_LABEL : PRIVATE_CONVERSATION_LABEL;
7477
+ }
7478
+ function surfaceFromConversationId(conversationId) {
7479
+ if (parseSlackThreadId(conversationId)) return "slack";
7480
+ if (conversationId.startsWith("scheduler:")) return "scheduler";
7481
+ if (conversationId.startsWith("api:")) return "api";
7482
+ return "internal";
7483
+ }
7484
+ function surfaceFromSource(source, conversationId) {
7485
+ if (source === "slack" || source === "api" || source === "web" || source === "scheduler") return source === "web" ? "api" : source;
7486
+ return surfaceFromConversationId(conversationId);
6926
7487
  }
6927
7488
  function locationUrlFromConversation(args) {
6928
7489
  const { conversation } = args;
@@ -7149,155 +7710,6 @@ function childAgentTotalsReport(metrics, auxiliaryCosts) {
7149
7710
  ...childAgents.usage ? { cumulativeUsage: childAgents.usage } : void 0
7150
7711
  };
7151
7712
  }
7152
- // src/chat/conversations/fork.ts
7153
- /** Event key of the agent note that follows the copied events in a fork. */
7154
- const FORK_NOTE_KEY = "fork:note";
7155
- const forkNote = alias(juniorConversationEvents, "fork_note");
7156
- /**
7157
- * SQL filter for event rows that a fork did not copy from its source. Usage
7158
- * and cost reports use it, so copied model calls count only in the source.
7159
- */
7160
- function notCopiedByFork() {
7161
- return sql`not exists (
7162
- select 1 from ${juniorConversationEvents} as ${sql.identifier("fork_note")}
7163
- where ${forkNote.conversationId} = ${juniorConversationEvents.conversationId}
7164
- and ${forkNote.idempotencyKey} = ${FORK_NOTE_KEY}
7165
- and ${forkNote.seq} > ${juniorConversationEvents.seq}
7166
- )`;
7167
- }
7168
- /**
7169
- * Events that hold credentials, approvals, provider connections, or plugin
7170
- * state of the source actor. A fork does not copy them.
7171
- */
7172
- const UNCOPIED_EVENT_TYPES = /* @__PURE__ */ new Set([
7173
- "authorization_completed",
7174
- "authorization_requested",
7175
- "guardian_action_reviewed",
7176
- "mcp_provider_connected",
7177
- "mcp_provider_connected_unowned",
7178
- "structured_event",
7179
- "unknown"
7180
- ]);
7181
- 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.";
7182
- /** An expected fork request failure, such as a reply without saved history. */
7183
- var ConversationForkError = class extends Error {};
7184
- /**
7185
- * Create a new web root Conversation from the agent history of a source
7186
- * Conversation, through one completed assistant reply.
7187
- *
7188
- * Retries with the same requester and key return the same Conversation.
7189
- */
7190
- async function forkConversation(input) {
7191
- const conversationId = createConversationId({
7192
- actorEmail: input.actorEmail,
7193
- idempotencyKey: JSON.stringify([
7194
- "fork",
7195
- input.sourceConversationId,
7196
- input.idempotencyKey
7197
- ])
7198
- });
7199
- const executor = getSqlExecutor();
7200
- const events = getConversationEventStore();
7201
- const store = getConversationStore();
7202
- return withConversationEventLock(executor, input.sourceConversationId, () => withConversationEventLock(executor, conversationId, async () => {
7203
- if (await store.get({ conversationId })) return { conversationId };
7204
- const source = await store.get({ conversationId: input.sourceConversationId });
7205
- if (!source || source.transcriptPurgedAtMs !== void 0) throw new ConversationForkError("The source history is no longer available.");
7206
- if (source.parentConversationId) throw new ConversationForkError("You cannot fork a child conversation.");
7207
- const boundary = await events.loadByIdempotencyKey(input.sourceConversationId, `message:${input.messageId}:agent`);
7208
- const reply = boundary?.data.type === "assistant_message" ? piMessageFromHistoryItem(boundary.data) : void 0;
7209
- 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.");
7210
- const projection = await loadTurnProjection({
7211
- conversationId: input.sourceConversationId,
7212
- committedSeq: boundary.seq,
7213
- includeTail: false
7214
- });
7215
- if (!projection?.messages.length) throw new ConversationForkError("The selected history is no longer available.");
7216
- if (hasUnfinishedToolCalls(projection.messages)) throw new ConversationForkError("This history has unfinished tool calls. Choose another reply.");
7217
- const nowMs = Date.now();
7218
- await recordWebConversationActivity({
7219
- actor: webActorFromEmail(input.actorEmail, { ...input.actorName ? { fullName: input.actorName } : void 0 }),
7220
- conversationId,
7221
- nowMs,
7222
- rootVisibility: source.visibility === "public" ? "public" : "private"
7223
- });
7224
- await executor.db().update(juniorConversations).set({
7225
- forkedFromConversationId: source.conversationId,
7226
- title: source.title ? `Fork: ${source.title}` : "Forked conversation"
7227
- }).where(eq(juniorConversations.conversationId, conversationId));
7228
- await copyForkEvents({
7229
- sourceConversationId: source.conversationId,
7230
- conversationId,
7231
- events: await events.loadHistory(source.conversationId),
7232
- boundarySeq: boundary.seq,
7233
- messageId: input.messageId
7234
- });
7235
- await events.append(conversationId, [{
7236
- idempotencyKey: FORK_NOTE_KEY,
7237
- createdAtMs: nowMs,
7238
- data: {
7239
- type: "user_message",
7240
- provenance: contextProvenance,
7241
- content: [{
7242
- type: "text",
7243
- text: FORK_NOTE
7244
- }],
7245
- timestamp: nowMs
7246
- }
7247
- }]);
7248
- return { conversationId };
7249
- }));
7250
- }
7251
- /**
7252
- * Copy the source event rows through the selected reply. Copies keep their
7253
- * sequence, author, timestamp, key, and payload, so the fork shows the same
7254
- * transcript and event log, and seq references in compactions stay valid.
7255
- * Later updates of copied Messages and the end of copied Turns are also
7256
- * copied, so that no copied item stays open in the fork.
7257
- */
7258
- async function copyForkEvents(args) {
7259
- const reply = args.events.find((event) => event.data.type === "message" && event.data.role === "assistant" && event.data.messageId === args.messageId);
7260
- const cutoffSeq = Math.max(args.boundarySeq, reply?.seq ?? -1);
7261
- const messageIds = /* @__PURE__ */ new Set();
7262
- const turnIds = /* @__PURE__ */ new Set();
7263
- const seqs = [];
7264
- let historyVersion = 0;
7265
- for (const event of args.events) {
7266
- const data = event.data;
7267
- if (UNCOPIED_EVENT_TYPES.has(data.type)) continue;
7268
- if (event.idempotencyKey === FORK_NOTE_KEY) continue;
7269
- if (event.seq <= cutoffSeq) {
7270
- if (data.type === "message") messageIds.add(data.messageId);
7271
- if (data.type === "turn_started") turnIds.add(data.turnId);
7272
- historyVersion = Math.max(historyVersion, event.historyVersion);
7273
- seqs.push(event.seq);
7274
- } 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);
7275
- }
7276
- if (!seqs.length) return;
7277
- const e = juniorConversationEvents;
7278
- await getSqlExecutor().db().execute(sql`insert into ${e} (${sql.join([
7279
- e.conversationId,
7280
- e.seq,
7281
- e.historyVersion,
7282
- e.schemaVersion,
7283
- e.idempotencyKey,
7284
- e.type,
7285
- e.payload,
7286
- e.actorIdentityId,
7287
- e.createdAt
7288
- ].map((column) => sql.identifier(column.name)), sql`, `)})
7289
- select ${args.conversationId}, ${e.seq}, least(${e.historyVersion}, ${historyVersion}), ${e.schemaVersion}, ${e.idempotencyKey}, ${e.type}, ${e.payload}, ${e.actorIdentityId}, ${e.createdAt}
7290
- from ${e}
7291
- where ${e.conversationId} = ${args.sourceConversationId}
7292
- and ${inArray(e.seq, seqs)}`);
7293
- }
7294
- function hasUnfinishedToolCalls(messages) {
7295
- const pending = /* @__PURE__ */ new Set();
7296
- for (const message of messages) if (message.role === "assistant") {
7297
- for (const part of message.content) if (part.type === "toolCall") pending.add(part.id);
7298
- } else if (message.role === "toolResult") pending.delete(message.toolCallId);
7299
- return pending.size > 0;
7300
- }
7301
7713
  // src/api/conversations/auxiliary-costs.ts
7302
7714
  function eventCost() {
7303
7715
  return sql`CASE
@@ -7347,66 +7759,16 @@ async function readConversationAuxiliaryCostsFromSql(db, conversationIds) {
7347
7759
  operations: []
7348
7760
  } };
7349
7761
  current.total.costUsd = addCostUsd(current.total.costUsd, row.costUsd);
7350
- if (row.childAgentCostUsd > 0) current.childAgentCostUsd = addCostUsd(current.childAgentCostUsd ?? 0, row.childAgentCostUsd);
7351
- current.total.operations.push({
7352
- costUsd: row.costUsd,
7353
- events: row.events,
7354
- name: row.name,
7355
- namespace: row.namespace
7356
- });
7357
- result.set(row.conversationId, current);
7358
- }
7359
- return result;
7360
- }
7361
- // src/chat/plugins/conversation-sidebar.ts
7362
- /** Show native objects by default and let plugins select compact sidebar labels. */
7363
- async function listConversationSidebarAnnotations(conversationIds, annotationsByConversation) {
7364
- const candidates = new Set(conversationIds);
7365
- const selected = {};
7366
- const plugins = getPlugins();
7367
- const customOwners = new Set(plugins.filter((plugin) => plugin.hooks?.conversationSidebar).map((plugin) => plugin.manifest.name));
7368
- for (const conversationId of conversationIds) {
7369
- const objects = (annotationsByConversation.get(conversationId) ?? []).flatMap((annotation) => {
7370
- if (customOwners.has(annotation.plugin) || !annotation.objectType) return [];
7371
- return [{
7372
- key: annotation.key,
7373
- label: annotation.label,
7374
- objectType: annotation.objectType === "item" && annotation.kind === "object" && annotation.facts?.type === "deployment" ? "deployment" : annotation.objectType,
7375
- status: annotation.status
7376
- }];
7377
- });
7378
- if (objects.length) selected[conversationId] = objects;
7379
- }
7380
- for (const plugin of plugins) {
7381
- const hook = plugin.hooks?.conversationSidebar;
7382
- if (!hook) continue;
7383
- const annotationsByConversationId = Object.fromEntries(conversationIds.flatMap((conversationId) => {
7384
- const annotations = (annotationsByConversation.get(conversationId) ?? []).filter((annotation) => annotation.plugin === plugin.manifest.name);
7385
- return annotations.length > 0 ? [[conversationId, annotations]] : [];
7386
- }));
7387
- try {
7388
- const result = await hook({
7389
- annotationsByConversationId,
7390
- conversationIds,
7391
- db: getDb(),
7392
- log: createPluginLogger(plugin.manifest.name),
7393
- plugin: { name: plugin.manifest.name },
7394
- cache: createPluginCache(plugin.manifest.name)
7395
- });
7396
- for (const [conversationId, annotations] of Object.entries(result.annotationsByConversationId)) {
7397
- if (!candidates.has(conversationId)) continue;
7398
- const parsed = conversationSidebarAnnotationSchema.array().safeParse(annotations);
7399
- if (!parsed.success || parsed.data.length === 0) continue;
7400
- selected[conversationId] = [...selected[conversationId] ?? [], ...parsed.data];
7401
- }
7402
- } catch (error) {
7403
- logWarn("plugin.conversation_sidebar.hook.failed", {
7404
- "app.plugin.name": plugin.manifest.name,
7405
- "exception.message": error instanceof Error ? error.message : String(error)
7406
- });
7407
- }
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);
7408
7770
  }
7409
- return selected;
7771
+ return result;
7410
7772
  }
7411
7773
  // src/chat/plugins/unfinished-work.ts
7412
7774
  /**
@@ -7521,158 +7883,6 @@ async function readConversationActivityPreviews(db, conversationIds) {
7521
7883
  text: row.text
7522
7884
  }]));
7523
7885
  }
7524
- // src/api/conversations/first-message.ts
7525
- /**
7526
- * Read the text of the first human message for each untitled conversation.
7527
- * Read-time titles use it while the generated title is still unset.
7528
- */
7529
- async function readFirstHumanMessageTexts(db, conversations) {
7530
- const untitledIds = conversations.flatMap((conversation) => conversation.title ? [] : [conversation.conversationId]);
7531
- if (untitledIds.length === 0) return /* @__PURE__ */ new Map();
7532
- const text = sql`${juniorConversationEvents.payload}->>'text'`;
7533
- const rows = await db.selectDistinctOn([juniorConversationEvents.conversationId], {
7534
- conversationId: juniorConversationEvents.conversationId,
7535
- text: text.as("text")
7536
- }).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));
7537
- return new Map(rows.map((row) => [row.conversationId, row.text]));
7538
- }
7539
- // src/api/mentions.ts
7540
- const SLACK_USER_MENTION_RE = /<@([UW][A-Z0-9]+)(?:\|[^<>]*)?>/g;
7541
- function mentionedSlackUserIds(texts) {
7542
- const ids = texts.flatMap((text) => text ? [...text.matchAll(SLACK_USER_MENTION_RE)].map((m) => m[1]) : []);
7543
- return [...new Set(ids)];
7544
- }
7545
- /**
7546
- * Resolve the people that each source mentions, in source order. Every API
7547
- * that returns display text uses this, so the dashboard can show the same
7548
- * names and People links everywhere. It does one identity query per Slack
7549
- * workspace.
7550
- */
7551
- async function resolveMentions(executor, sources) {
7552
- const idsBySource = sources.map((source) => mentionedSlackUserIds(source.texts));
7553
- const idsByTenant = /* @__PURE__ */ new Map();
7554
- sources.forEach((source, index) => {
7555
- if (idsBySource[index].length === 0) return;
7556
- const ids = idsByTenant.get(source.tenantId) ?? /* @__PURE__ */ new Set();
7557
- for (const id of idsBySource[index]) ids.add(id);
7558
- idsByTenant.set(source.tenantId, ids);
7559
- });
7560
- const peopleByTenant = new Map(await Promise.all([...idsByTenant].map(async ([tenantId, ids]) => [tenantId, await readMentionedPeople(executor, tenantId, [...ids])])));
7561
- return sources.map((source, index) => {
7562
- const people = peopleByTenant.get(source.tenantId);
7563
- return idsBySource[index].flatMap((id) => {
7564
- const person = people?.get(id);
7565
- return person ? [person] : [];
7566
- });
7567
- });
7568
- }
7569
- /**
7570
- * Read current names for mentioned Slack users. Known Junior identities come
7571
- * first. The email is set only for a linked, verified Junior user, because
7572
- * that email is the People profile route.
7573
- */
7574
- async function readMentionedPeople(executor, tenantId, userIds) {
7575
- const rows = await executor.db().select({
7576
- displayName: juniorIdentities.displayName,
7577
- emailVerified: juniorIdentities.emailVerified,
7578
- handle: juniorIdentities.handle,
7579
- providerSubjectId: juniorIdentities.providerSubjectId,
7580
- userDisplayName: juniorUsers.displayName,
7581
- userEmail: juniorUsers.primaryEmail
7582
- }).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));
7583
- const people = /* @__PURE__ */ new Map();
7584
- for (const row of rows) {
7585
- if (people.has(row.providerSubjectId)) continue;
7586
- const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
7587
- const email = row.emailVerified ? row.userEmail?.trim() : void 0;
7588
- people.set(row.providerSubjectId, {
7589
- slackUserId: row.providerSubjectId,
7590
- ...fullName ? { fullName } : void 0,
7591
- ...row.handle ? { slackUserName: row.handle } : void 0,
7592
- ...email ? { email } : void 0
7593
- });
7594
- }
7595
- const unnamed = userIds.filter((id) => {
7596
- const person = people.get(id);
7597
- return !person?.fullName && !person?.slackUserName;
7598
- });
7599
- if (tenantId && unnamed.length > 0) {
7600
- const profiles = await Promise.all(unnamed.map((id) => lookupSlackUser(tenantId, id)));
7601
- unnamed.forEach((id, index) => {
7602
- const profile = profiles[index];
7603
- if (!profile?.fullName && !profile?.userName) return;
7604
- people.set(id, {
7605
- ...people.get(id),
7606
- slackUserId: id,
7607
- ...profile.fullName ? { fullName: profile.fullName } : void 0,
7608
- ...profile.userName ? { slackUserName: profile.userName } : void 0
7609
- });
7610
- });
7611
- }
7612
- return people;
7613
- }
7614
- // src/api/conversations/mentions.ts
7615
- /** Text in one event that the dashboard renders with mentions. */
7616
- function eventMentionTexts(data) {
7617
- switch (data.type) {
7618
- case "message": return data.redacted ? [] : [
7619
- data.text,
7620
- data.trustedSummary,
7621
- ...(data.events ?? []).map((event) => event.trustedSummary)
7622
- ];
7623
- case "assistant_message": return data.parts.map((part) => part.text);
7624
- case "compaction":
7625
- case "handoff": return [data.summary];
7626
- case "structured_event": return [data.presentation.preview, ...(data.presentation.details ?? []).map((detail) => detail.description)];
7627
- default: return [];
7628
- }
7629
- }
7630
- function briefMentionTexts(brief) {
7631
- return [
7632
- brief.summary,
7633
- brief.intent,
7634
- brief.outcome.text,
7635
- ...brief.decisions.flatMap((decision) => [decision.text, decision.by]),
7636
- ...brief.openDecisions.flatMap((decision) => [decision.text, decision.owner]),
7637
- ...brief.facts
7638
- ];
7639
- }
7640
- function withMentions(value, mentions) {
7641
- return mentions.length > 0 ? {
7642
- ...value,
7643
- mentions
7644
- } : value;
7645
- }
7646
- /**
7647
- * Attach the people that each event and the Brief of one conversation
7648
- * mention, resolved at read time.
7649
- */
7650
- async function attachConversationMentions(executor, conversationId, content) {
7651
- const tenantId = (await loadConversationSlackTenantIds(executor, [conversationId])).get(conversationId);
7652
- const sources = content.events.map((event) => ({
7653
- tenantId,
7654
- texts: eventMentionTexts(event.data)
7655
- }));
7656
- if (content.brief) sources.push({
7657
- tenantId,
7658
- texts: briefMentionTexts(content.brief)
7659
- });
7660
- const mentions = await resolveMentions(executor, sources);
7661
- return {
7662
- briefMentions: content.brief ? mentions.at(-1) : [],
7663
- events: content.events.map((event, index) => withMentions(event, mentions[index]))
7664
- };
7665
- }
7666
- /** Attach the people that each activity preview mentions, resolved at read time. */
7667
- async function attachActivityPreviewMentions(executor, previews) {
7668
- const entries = [...previews];
7669
- const tenants = await loadConversationSlackTenantIds(executor, entries.map(([conversationId]) => conversationId));
7670
- const mentions = await resolveMentions(executor, entries.map(([conversationId, preview]) => ({
7671
- tenantId: tenants.get(conversationId),
7672
- texts: [preview.text]
7673
- })));
7674
- return new Map(entries.map(([conversationId, preview], index) => [conversationId, withMentions(preview, mentions[index])]));
7675
- }
7676
7886
  // src/api/conversations/participants.ts
7677
7887
  const identityColumns = {
7678
7888
  avatarUrl: juniorIdentities.avatarUrl,
@@ -7700,9 +7910,10 @@ function participantIdentity(row, userAvatarUrls) {
7700
7910
  };
7701
7911
  return Object.keys(participant).length > 0 ? participant : void 0;
7702
7912
  }
7703
- /** Read actor identities in first-appearance order for each Conversation. */
7704
- async function readConversationParticipants(db, conversationIds) {
7705
- 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) {
7706
7917
  const [rootRows, eventRows] = await Promise.all([db.select({
7707
7918
  conversationId: juniorConversations.conversationId,
7708
7919
  ...identityColumns
@@ -7710,26 +7921,50 @@ async function readConversationParticipants(db, conversationIds) {
7710
7921
  conversationId: juniorConversationEvents.conversationId,
7711
7922
  seq: juniorConversationEvents.seq,
7712
7923
  ...identityColumns
7713
- }).from(juniorConversationEvents).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(inArray(juniorConversationEvents.conversationId, [...conversationIds]), ne(juniorIdentities.kind, "system"))).orderBy(asc(juniorConversationEvents.conversationId), asc(juniorConversationEvents.actorIdentityId), asc(juniorConversationEvents.seq))]);
7714
- const userAvatarUrls = await readUserAvatarUrls(db, [...rootRows, ...eventRows].flatMap((row) => row.userId ? [row.userId] : []));
7715
- const ordered = /* @__PURE__ */ new Map();
7716
- const add = (conversationId, row) => {
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))]);
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) {
7717
7931
  const participant = participantIdentity(row, userAvatarUrls);
7718
- if (!participant) return;
7719
- const group = ordered.get(conversationId) ?? {
7720
- identities: [],
7721
- keys: /* @__PURE__ */ new Set()
7722
- };
7723
- const key = row.userId ? `user:${row.userId}` : `identity:${row.identityId}`;
7724
- if (!group.keys.has(key)) {
7725
- group.keys.add(key);
7726
- group.identities.push(participant);
7727
- }
7728
- 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()]
7729
7967
  };
7730
- for (const row of rootRows) add(row.conversationId, row);
7731
- for (const row of eventRows.sort((left, right) => left.seq - right.seq)) add(row.conversationId, row);
7732
- return new Map([...ordered].map(([conversationId, group]) => [conversationId, group.identities]));
7733
7968
  }
7734
7969
  // src/api/conversations/list.ts
7735
7970
  const CONVERSATION_FEED_LIMIT = 50;
@@ -7944,4 +8179,317 @@ async function readConversationFeedFromSql(options = {}) {
7944
8179
  async function readConversationFeed(options = {}) {
7945
8180
  return conversationFeedSchema.parse(await readConversationFeedFromSql(options));
7946
8181
  }
7947
- 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 };