@sentry/junior 0.266.0 → 0.268.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/{actor-tlxzqaRe.js → actor-CP9K2Idu.js} +9 -1
  2. package/dist/{adapter-eRFA_naf.js → adapter-04AcD2SO.js} +5 -7
  3. package/dist/{admin-tILpP51p.js → admin-DIU1QyG3.js} +1 -1
  4. package/dist/{agent-invocations-CxKldAeB.js → agent-invocations-C1RNOCs8.js} +2 -0
  5. package/dist/{agent-runner-BtYK7bht.js → agent-runner-CAYfFmU7.js} +2636 -601
  6. package/dist/api/schema.d.ts +2 -5454
  7. package/dist/api/schema.js +6 -5
  8. package/dist/api.d.ts +6 -1
  9. package/dist/api.js +255 -1208
  10. package/dist/{app-Csbi9dW7.d.ts → app-z6Uuyq3j.d.ts} +23 -4
  11. package/dist/app.d.ts +2 -2
  12. package/dist/app.js +1869 -378
  13. package/dist/{automation-BKpY9LPa.js → automation-CmeFBSEh.js} +42 -14
  14. package/dist/{briefs-DgfWMU7e.js → briefs-DQbV2jNr.js} +4 -4
  15. package/dist/{catalog-runtime-DAJjAbSx.js → catalog-runtime-BD9GSpc6.js} +1 -1
  16. package/dist/cli/chat.js +15 -15
  17. package/dist/cli/check.js +3 -3
  18. package/dist/cli/init.js +0 -1
  19. package/dist/cli/main.js +3 -3
  20. package/dist/cli/plugins.js +4 -4
  21. package/dist/cli/snapshot-create.js +4 -4
  22. package/dist/cli/upgrade.d.ts +1 -1
  23. package/dist/cli/upgrade.js +3 -3
  24. package/dist/{client-B0YZfxNy.js → client-C1yDYjoS.js} +2 -2
  25. package/dist/{config-C0UwYqDK.js → config-DrTeF80T.js} +32 -10
  26. package/dist/{connections-Bwu3VORi.js → connections-D46f8vvi.js} +12 -12
  27. package/dist/{conversation-DF4rUcjb.js → conversation-DSo310Ps.js} +8 -1
  28. package/dist/{conversation-privacy-CY7JVjxn.js → conversation-privacy-CxlOCiUo.js} +1 -1
  29. package/dist/{credential-sync-VGAMUTGp.js → credential-sync-sm9fegLY.js} +29 -34
  30. package/dist/{credentialed-enF5Bf3y.js → credentialed-lukuD-cc.js} +2933 -1026
  31. package/dist/db-BU4LQD8s.d.ts +6 -0
  32. package/dist/{executor-TnWbvL09.js → executor-4vhvsMZJ.js} +83 -6
  33. package/dist/instrumentation.js +6 -2
  34. package/dist/{assistant-reply-UyzAfZQo.js → interruption-marker-CkncDT8d.js} +8 -2
  35. package/dist/{job-queue-DBpHmKnd.js → job-queue-In9QKz-I.js} +128 -3
  36. package/dist/{web-input-B-bMjD1V.js → list-BkgnXEEB.js} +1611 -817
  37. package/dist/{logging-Ce0mXZ8J.js → logging-DLSpkSSr.js} +11 -3
  38. package/dist/{message-VZ3DFpGb.js → message-BObyg59s.js} +4 -4
  39. package/dist/nitro.d.ts +1 -1
  40. package/dist/nitro.js +3 -3
  41. package/dist/{oauth-callback-server-CpqWLjVY.js → oauth-callback-server-B-WbSdoH.js} +2 -2
  42. package/dist/{prompt-BcCGlmg2.js → prompt-BW9jFsqZ.js} +1 -1
  43. package/dist/{registry-1BEIPdbT.js → registry-BgecKva0.js} +2 -2
  44. package/dist/{web-message-id-CJSUiksE.js → reporting-window-DVCD_10C.js} +1 -10
  45. package/dist/{resolve-BSHHkEGE.js → resolve-Cpa4Bu1O.js} +3 -3
  46. package/dist/{result-CwOpzqdh.js → result-D3TSmZZ8.js} +9 -9
  47. package/dist/{runner-CEaX04kz.js → runner-DS5OKhEp.js} +11 -11
  48. package/dist/{sandbox-egress-signals-BwtGHwqa.js → sandbox-egress-signals-CnDjY5rx.js} +1 -1
  49. package/dist/{schema-RsaBHxRH.js → schema-0050HFnB.js} +82 -5
  50. package/dist/schema-ZngqcOWM.d.ts +5813 -0
  51. package/dist/{service-account-6wC76eBZ.js → service-account-CKV2WTY7.js} +3 -3
  52. package/dist/{service-account-Dg4lIbHm.js → service-account-DGjnDw9y.js} +3 -3
  53. package/dist/{session-stop-tZOw9AJD.js → session-stop-Dam1EI7B.js} +527 -134
  54. package/dist/{skills-DzyGN7wd.js → skills-CfAnfImD.js} +2 -2
  55. package/dist/{spawn-6rokq7l_.js → spawn-cmq9p4Ly.js} +2 -2
  56. package/dist/{store-DcKkzrdt.js → store-C5PN02QO.js} +4 -4
  57. package/dist/{store-CG0VJyye.d.ts → store-xMuuXRE0.d.ts} +23 -9
  58. package/dist/store-zPEoJPBB.js +295 -0
  59. package/dist/{task-queue-Bg4RS2Gy.js → task-queue-_mf28dSu.js} +10 -5
  60. package/dist/{task-runner-8N5LIkSg.js → task-runner-DMI2prDO.js} +25 -14
  61. package/dist/{tasks-DDT-J3Rf.js → tasks-SB9z-VTU.js} +1551 -54
  62. package/dist/{thread-state-cprEaxgl.js → thread-state-BbbPh0Sn.js} +8 -18
  63. package/dist/{turn-failure-response-C8sMdsdX.js → turn-failure-response-BKKmnMB5.js} +11 -16
  64. package/dist/{turn-lifecycle-DaBVoz7h.js → turn-lifecycle-oP0kVeGz.js} +1 -1
  65. package/dist/{validation-C2lllinq.js → validation--JsAGLwc.js} +1 -1
  66. package/dist/{version-oaLfxxnf.js → version-jHwJp4hC.js} +1 -1
  67. package/dist/version.js +1 -1
  68. package/dist/{viewer-BpE6Z3qf.js → viewer-CpXTGlep.js} +329 -230
  69. package/dist/web-message-id-DlHNANtd.js +132 -0
  70. package/dist/{work-VX1SHWha.js → work-3N-GHY9m.js} +10 -10
  71. package/dist/{worker-hook-y_Y.js → worker-CCdbcItd.js} +30 -19
  72. package/migrations/0064_any_resource_event_triggers.sql +2 -0
  73. package/migrations/0065_user_customization.sql +1 -0
  74. package/migrations/0066_review_items.sql +53 -0
  75. package/migrations/0067_review_item_dedupe_tenant.sql +2 -0
  76. package/migrations/0068_review_item_slack_cards.sql +10 -0
  77. package/migrations/meta/0064_snapshot.json +3505 -0
  78. package/migrations/meta/0065_snapshot.json +3511 -0
  79. package/migrations/meta/0066_snapshot.json +4099 -0
  80. package/migrations/meta/0067_snapshot.json +3921 -0
  81. package/migrations/meta/0068_snapshot.json +4176 -0
  82. package/migrations/meta/_journal.json +36 -1
  83. package/package.json +19 -4
  84. package/dist/db-BnRqQ7G5.d.ts +0 -5
  85. package/dist/images-BKQqSi5W.js +0 -64
  86. package/dist/store-FALy-JQb.js +0 -1424
  87. package/dist/turn-session-routing-a9CHX-w6.js +0 -76
package/dist/api.js CHANGED
@@ -1,40 +1,39 @@
1
- import { D as AutomationConflictError, E as sameDefinitionValue, I as loadTurnProjection, O as automationRevision, S as automationDefinition, Sn as piMessageFromHistoryItem, T as readAutomationVersion, a as saveScheduledAutomation, k as requireAutomationRevision, nt as fallbackShortTitle, v as saveEventAutomation, y as setEventAutomationStatus } from "./tasks-DDT-J3Rf.js";
2
- import { p as escapeXml } from "./client-B0YZfxNy.js";
3
- import { f as logException, m as logWarn, p as logInfo } from "./logging-Ce0mXZ8J.js";
4
- import { d as parseSlackTeamId, g as parseSlackMessageTs, l as parseSlackChannelId } from "./actor-tlxzqaRe.js";
5
- import { C as parseSessionSource, D as agentTurnUsageSchema, O as hasAgentTurnUsage, a as resolveViewerUser, d as getConversationStore, f as getDb, g as loadConversationSlackTenantIds, h as withConversationEventLock, k as resolveRootConversationId, l as getConversationEventStore, p as getSqlExecutor, s as updateViewerDisplayName, t as findUserByEmail, w as addAgentTurnUsage, y as locationFromRow } from "./viewer-BpE6Z3qf.js";
6
- import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, o as conversationReadColumns, s as juniorConversations } from "./agent-invocations-CxKldAeB.js";
7
- import { f as juniorConversationBriefs, g as juniorApiTokens, h as juniorConversationEvents, m as juniorConversationMetrics, u as juniorConversationParticipants, v as juniorCodeChanges, y as juniorCodeRepositories } from "./executor-TnWbvL09.js";
8
- import { d as contextProvenance } from "./history-C84mlAwO.js";
9
- import { n as pluginCatalogRuntime } from "./catalog-runtime-DAJjAbSx.js";
10
- import { F as AutomationEditError, K as conversationSubtree, P as editEventAutomation, R as resolveTaskOutcomes, S as readLiveAttachment, T as parseSlackThreadId, W as listChildAgents, j as editScheduledAutomation, m as readStats, q as conversationSubtreeColumns, z as listOwnedWatches } from "./agent-runner-BtYK7bht.js";
11
- import { An as createPluginLogger, C as listWorkspaces, E as WorkspaceValidationError, Fn as conversationHasParticipantUser, Hn as readConversationEventPage, In as conversationIsMemberRoot, Ln as conversationNotArchivedForEmail, M as create, Mn as conversationArchiveTimes, N as hash, Nn as conversationArchivedForEmail, Pn as conversationArchivedForUser, Qn as listConversationAnnotationsById, Rn as conversationNotArchivedForUser, T as updateWorkspace, Un as buildSentryConversationUrl, Vn as readLatestConversationBrief, Zn as listConversationAnnotations, an as withSlackRetries, b as deleteWorkspace, en as getSlackClient, j as SANDBOX_RUNTIME, jn as readConversationAccessFromSql, n as createUserTokenStore, nr as createPluginCache, p as getPlugins, tr as lookupSlackUser, w as listWorkspacesByRepository, wt as workspaceRepoCheckoutPath, x as getWorkspace, y as createWorkspace, zn as viewerConversationMembership } from "./credentialed-enF5Bf3y.js";
1
+ import { D as AutomationConflictError, E as sameDefinitionValue, N as automationEventCatalog, O as automationRevision, S as automationDefinition, T as readAutomationVersion, a as saveScheduledAutomation, k as requireAutomationRevision, ur as isExperimentalFeatureEnabled, v as saveEventAutomation, y as setEventAutomationStatus } from "./tasks-SB9z-VTU.js";
2
+ import { p as escapeXml } from "./client-C1yDYjoS.js";
3
+ import { m as logInfo, p as logException } from "./logging-DLSpkSSr.js";
4
+ import { I as agentTurnUsageSchema, L as hasAgentTurnUsage, R as resolveRootConversationId, c as updateViewerDisplayName, h as getSqlExecutor, m as getDb, o as resolveViewerUser, p as getConversationStore, t as findUserByEmail } from "./viewer-CpXTGlep.js";
5
+ import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, s as juniorConversations } from "./agent-invocations-C1RNOCs8.js";
6
+ import { C as juniorCodeRepositories, S as juniorCodeChanges, b as juniorApiTokens, m as juniorConversationParticipants, v as juniorConversationMetrics, y as juniorConversationEvents } from "./executor-4vhvsMZJ.js";
7
+ import { n as pluginCatalogRuntime } from "./catalog-runtime-BD9GSpc6.js";
8
+ import { $ as webUserInputAnswerNote, B as listOwnedWatches, F as editEventAutomation, I as AutomationEditError, Lt as readUserCustomization, M as editScheduledAutomation, Mt as conversationSubtreeColumns, Rt as saveUserCustomization, St as readLiveAttachment, bt as decodeInputImages, ct as searchReviewItems, ft as acceptsWebMessages, ht as createAndEnqueueConversation, it as countReviewItems, jt as conversationSubtree, kt as listChildAgents, m as readStats, ot as listWaitingConversationIds, pt as appendAndEnqueueWebMessage, st as reviewItemReaderForUser, vt as webActorFromEmail, w as persistSlackUserInputReply, z as resolveTaskOutcomes } from "./agent-runner-CAYfFmU7.js";
9
+ import { C as listWorkspaces, E as WorkspaceValidationError, Et as SANDBOX_RUNTIME, In as createPluginLogger, Jn as readConversationEventPage, Ln as readConversationAccessFromSql, M as hash, Ot as workspaceRepoCheckoutPath, T as updateWorkspace, Yn as buildSentryConversationUrl, b as deleteWorkspace, cr as createPluginCache, fn as runWithWorkspaceTeamId, j as create, n as createUserTokenStore, p as getPlugins, qn as readLatestConversationBrief, rr as listConversationAnnotations, sr as lookupSlackUser, w as listWorkspacesByRepository, x as getWorkspace, y as createWorkspace } from "./credentialed-lukuD-cc.js";
10
+ import { i as getStateAdapter } from "./adapter-04AcD2SO.js";
12
11
  import { t as getFirstRunAtMs } from "./cadence-R7Em4RjE.js";
13
- import { A as forkConversationResponseSchema, B as readMessageAttachments, D as createConversationBodySchema, O as createConversationMessageBodySchema, V as INPUT_IMAGE_TYPES, _ as conversationParamsSchema, a as cancelConversationPendingMessagesBodySchema, c as conversationAttachmentParamsSchema, d as conversationDetailReportSchema, g as conversationFeedSchema, h as conversationFeedQuerySchema, i as archiveConversationResponseSchema, k as forkConversationBodySchema, m as conversationEventsQuerySchema, o as cancelConversationPendingMessagesResponseSchema, p as conversationEventPageSchema, r as archiveConversationBodySchema, t as acceptedConversationMessageSchema, u as conversationDetailQuerySchema, x as conversationPendingMessagesReportSchema } from "./conversation-DF4rUcjb.js";
14
- import { h as getChildAgentLabel } from "./store-DcKkzrdt.js";
15
- import { d as getEventCatalog, x as automationEventCatalog } from "./session-stop-tZOw9AJD.js";
16
- import { s as getSandboxResources, t as getCachedSnapshot } from "./resolve-BSHHkEGE.js";
17
- import { a as getVercelConversationWorkQueue } from "./job-queue-DBpHmKnd.js";
18
- import { a as cancelHumanFacingPendingMessages, h as getConversation } from "./store-FALy-JQb.js";
19
- import { n as finishServiceAccountSignIn, o as serviceAccountPlugins, r as getServiceAccountStatus, s as startServiceAccountSignIn, t as disconnectServiceAccount } from "./service-account-6wC76eBZ.js";
20
- import { M as compileScheduleIntent, P as weekdaySchema, _ as automationSummarySchema, b as automationVersionListSchema, c as automationLifecycleSchema, d as automationParamsSchema, f as automationRunListSchema, g as automationSchedulePreviewSchema, h as automationScheduleIntentSchema, i as automationExecutionListSchema, j as ScheduleIntentError, l as automationListQuerySchema, n as automationEditSchema, r as automationEventCatalogSchema, t as automationEditErrorSchema, u as automationListSchema, v as automationUpdateSchema, x as automationVersionParamsSchema, y as automationVersionActivateSchema } from "./automation-BKpY9LPa.js";
21
- import { C as skillReportsSchema, a as fillUtcHours, b as pluginsSchema, c as sumUtcHoursIntoSixHours, d as trailingUtcSixHourWindow, f as utcDayKey, h as pluginOperationalReportFeedSchema, i as fillUtcDays, l as trailingUtcDayWindow, m as healthReportSchema, n as DAY_MS$1, o as fillUtcSixHours, p as utcHourKey, r as HOUR_MS$1, s as startOfUtcHour, u as trailingUtcHourWindow, x as runtimeInfoReportSchema } from "./web-message-id-CJSUiksE.js";
22
- import { B as readHealthReport, I as getAttachmentStorage, _ as readViewerAutomationRuns, a as recordWebConversationActivity, b as readViewerAutomations, d as resolveSlackConversationContextFromThreadId, g as readViewerAutomationExecutions, h as readConversationSourceTask, i as createConversationId, k as stopConversationTurn, l as getProductionSlackAdapter, m as deleteViewerTask, o as webActorFromEmail, p as ViewerTaskNotFoundError, r as createAndEnqueueConversation, t as appendAndEnqueueWebMessage, u as formatSlackConversationRedactedLabel, v as readViewerAutomationSummary, w as clearSlackPendingReactions, x as resolveViewerTaskCandidate, y as readViewerAutomationVersions } from "./web-input-B-bMjD1V.js";
23
- import { F as conversationAggregateColumns, I as conversationRangeColumns, M as readAutomationTitlesByConversationId, P as conversationActiveDaysColumn } from "./task-runner-8N5LIkSg.js";
24
- import { d as legacyWebMailboxMetadataSchema } from "./result-CwOpzqdh.js";
25
- import { a as startConnection, l as getWebAuthorization, n as isConnectableProvider, r as listConnections, t as disconnectConnection, u as htmlCallbackResponse } from "./connections-Bwu3VORi.js";
26
- import { t as decodeInputImages } from "./images-BKQqSi5W.js";
27
- import { A as activityHours$1, C as personParamsSchema, D as locationDirectoryReportSchema, E as locationDetailReportSchema, H as codePersonReportSchema, I as disconnectConnectionResponseSchema, L as startConnectionResponseSchema, N as connectionListSchema, O as locationParamsSchema, P as connectionParamsSchema, Q as apiErrorSchema, S as actorProfileReportSchema, U as codeRepositoryParamsSchema, V as codeOverviewReportSchema, W as codeRepositoryReportSchema, X as stopConversationTurnResponseSchema, Y as conversationChildAgentsReportSchema, Z as conversationStatsReportSchema, _ as personalTokenListSchema, a as workspaceBodySchema, b as revokePersonalTokenResponseSchema, d as serviceAccountListSchema, f as serviceAccountParamsSchema, g as createdPersonalTokenSchema, h as createPersonalTokenBodySchema, i as deleteWorkspaceResponseSchema, j as activitySixHours$1, k as activityDays$1, l as workspaceSchema, m as startServiceAccountSignInResponseSchema, n as statsReportSchema, o as workspaceListSchema, q as conversationWatchesReportSchema, s as workspaceParamsSchema, u as disconnectServiceAccountResponseSchema, w as personalSpendReportSchema, x as actorDirectoryReportSchema, y as personalTokenParamsSchema } from "./schema-RsaBHxRH.js";
12
+ import { A as createConversationMessageBodySchema, G as INPUT_IMAGE_TYPES, M as forkConversationResponseSchema, N as steerConversationPendingMessagesBodySchema, P as steerConversationPendingMessagesResponseSchema, W as readMessageAttachments, _ as conversationParamsSchema, a as cancelConversationPendingMessagesBodySchema, c as conversationAttachmentParamsSchema, d as conversationDetailReportSchema, g as conversationFeedSchema, h as conversationFeedQuerySchema, i as archiveConversationResponseSchema, j as forkConversationBodySchema, k as createConversationBodySchema, m as conversationEventsQuerySchema, o as cancelConversationPendingMessagesResponseSchema, p as conversationEventPageSchema, r as archiveConversationBodySchema, t as acceptedConversationMessageSchema, u as conversationDetailQuerySchema, x as conversationPendingMessagesReportSchema } from "./conversation-DSo310Ps.js";
13
+ import { h as getChildAgentLabel } from "./store-C5PN02QO.js";
14
+ import { d as getEventCatalog } from "./session-stop-Dam1EI7B.js";
15
+ import { l as userCustomizationSchema, t as webMessageId } from "./web-message-id-DlHNANtd.js";
16
+ import { s as getSandboxResources, t as getCachedSnapshot } from "./resolve-Cpa4Bu1O.js";
17
+ import { a as getVercelConversationWorkQueue } from "./job-queue-In9QKz-I.js";
18
+ import { E as steerWebPendingMessages, a as cancelHumanFacingPendingMessages, g as getConversationWorkState, h as getConversation } from "./store-zPEoJPBB.js";
19
+ import { n as finishServiceAccountSignIn, o as serviceAccountPlugins, r as getServiceAccountStatus, s as startServiceAccountSignIn, t as disconnectServiceAccount } from "./service-account-CKV2WTY7.js";
20
+ import { F as weekdaySchema, M as ScheduleIntentError, N as compileScheduleIntent, _ as automationSummarySchema, b as automationVersionListSchema, c as automationLifecycleSchema, d as automationParamsSchema, f as automationRunListSchema, g as automationSchedulePreviewSchema, h as automationScheduleIntentSchema, i as automationExecutionListSchema, l as automationListQuerySchema, n as automationEditSchema, r as automationEventCatalogSchema, t as automationEditErrorSchema, u as automationListSchema, v as automationUpdateSchema, x as automationVersionParamsSchema, y as automationVersionActivateSchema } from "./automation-CmeFBSEh.js";
21
+ import { S as skillReportsSchema, a as fillUtcSixHours, b as runtimeInfoReportSchema, c as trailingUtcDayWindow, d as utcDayKey, f as utcHourKey, i as fillUtcHours, l as trailingUtcHourWindow, m as pluginOperationalReportFeedSchema, n as HOUR_MS$1, o as startOfUtcHour, p as healthReportSchema, r as fillUtcDays, s as sumUtcHoursIntoSixHours, t as DAY_MS$1, u as trailingUtcSixHourWindow, y as pluginsSchema } from "./reporting-window-DVCD_10C.js";
22
+ import { C as readConversationSourceTask, D as readViewerAutomationVersions, E as readViewerAutomationSummary, H as stopConversationTurn, J as canActOnReviewItem, K as ReviewItemActionError, L as clearSlackPendingReactions, M as isAllowedSignInEmail, O as readViewerAutomations, Q as getAttachmentStorage, S as deleteViewerTask, T as readViewerAutomationRuns, X as readReviewItemForUser, Y as isOnlyAssignee, a as resolveMentions, c as readConversationAuxiliaryCostsFromSql, d as notCopiedByFork, f as childAgentTotalsReport, g as resolveSlackTeamDomains, h as conversationSummaryFromStoredConversation, i as attachConversationMentions, k as resolveViewerTaskCandidate, l as ConversationForkError, m as conversationEventHistory, n as readConversationRecordFromSql, nt as readHealthReport, o as readFirstHumanMessageTexts, p as readConversationTreeMetricsFromSql, q as actOnReviewItem, r as readConversationParticipants, s as listConversationSidebarAnnotations, t as readConversationFeed, u as forkConversation, w as readViewerAutomationExecutions, x as ViewerTaskNotFoundError, y as getProductionSlackAdapter } from "./list-BkgnXEEB.js";
23
+ import { F as conversationAggregateColumns, I as conversationRangeColumns, M as readAutomationTitlesByConversationId, P as conversationActiveDaysColumn } from "./task-runner-DMI2prDO.js";
24
+ import { d as legacyWebMailboxMetadataSchema } from "./result-D3TSmZZ8.js";
25
+ import { a as startConnection, l as getWebAuthorization, n as isConnectableProvider, r as listConnections, t as disconnectConnection, u as htmlCallbackResponse } from "./connections-D46f8vvi.js";
26
+ import { A as personParamsSchema, B as connectionListSchema, C as createdPersonalTokenSchema, D as revokePersonalTokenResponseSchema, E as personalTokenParamsSchema, F as locationParamsSchema, I as activityDays$1, J as codeOverviewReportSchema, L as activityHours$1, N as locationDetailReportSchema, O as actorDirectoryReportSchema, P as locationDirectoryReportSchema, R as activitySixHours$1, S as createPersonalTokenBodySchema, U as disconnectConnectionResponseSchema, V as connectionParamsSchema, W as startConnectionResponseSchema, X as codeRepositoryParamsSchema, Y as codePersonReportSchema, Z as codeRepositoryReportSchema, _ as disconnectServiceAccountResponseSchema, a as workspaceBodySchema, at as apiErrorSchema, d as reviewItemCountSchema, et as conversationWatchesReportSchema, f as reviewItemListQuerySchema, g as reviewItemWaitingConversationsSchema, h as reviewItemSchema, i as deleteWorkspaceResponseSchema, it as conversationStatsReportSchema, j as personalSpendReportSchema, k as actorProfileReportSchema, l as workspaceSchema, m as reviewItemParamsSchema, n as statsReportSchema, nt as conversationChildAgentsReportSchema, o as workspaceListSchema, p as reviewItemListSchema, rt as stopConversationTurnResponseSchema, s as workspaceParamsSchema, u as reviewItemActionSchema, v as serviceAccountListSchema, w as personalTokenListSchema, x as startServiceAccountSignInResponseSchema, y as serviceAccountParamsSchema } from "./schema-0050HFnB.js";
28
27
  import { Hono } from "hono";
29
28
  import path from "node:path";
30
29
  import { z } from "zod";
31
30
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
32
- import { and, asc, desc, eq, gt, gte, inArray, isNotNull, isNull, lte, notExists, or, sql } from "drizzle-orm";
33
- import { conversationSidebarAnnotationSchema, pluginUserPageContentSchema, pluginUserPageCursorSchema, pluginUserPageFilterSchema, pluginUserPageLinksSchema, pluginUserPageQuerySchema } from "@sentry/junior-plugin-api";
31
+ import { and, asc, desc, eq, gt, gte, isNotNull, isNull, lte, notExists, or, sql } from "drizzle-orm";
32
+ import { pluginUserPageContentSchema, pluginUserPageCursorSchema, pluginUserPageFilterSchema, pluginUserPageLinksSchema, pluginUserPageQuerySchema } from "@sentry/junior-plugin-api";
34
33
  import { alias } from "drizzle-orm/pg-core";
35
34
  import { readFileSync } from "node:fs";
36
- import { HTTPException } from "hono/http-exception";
37
35
  import { bodyLimit } from "hono/body-limit";
36
+ import { HTTPException } from "hono/http-exception";
38
37
  import { zValidator } from "@hono/zod-validator";
39
38
  import { createMiddleware } from "hono/factory";
40
39
  // src/api/http.ts
@@ -140,325 +139,6 @@ async function archiveConversation(viewer, conversationId, body) {
140
139
  if (result.status === "missing") throwApiError(404, "Conversation not found.");
141
140
  throwApiError(409, "Conversation received new activity.");
142
141
  }
143
- // src/chat/conversations/fork.ts
144
- /** Event key of the agent note that follows the copied events in a fork. */
145
- const FORK_NOTE_KEY = "fork:note";
146
- const forkNote = alias(juniorConversationEvents, "fork_note");
147
- /**
148
- * SQL filter for event rows that a fork did not copy from its source. Usage
149
- * and cost reports use it, so copied model calls count only in the source.
150
- */
151
- function notCopiedByFork() {
152
- return sql`not exists (
153
- select 1 from ${juniorConversationEvents} as ${sql.identifier("fork_note")}
154
- where ${forkNote.conversationId} = ${juniorConversationEvents.conversationId}
155
- and ${forkNote.idempotencyKey} = ${FORK_NOTE_KEY}
156
- and ${forkNote.seq} > ${juniorConversationEvents.seq}
157
- )`;
158
- }
159
- /**
160
- * Events that hold credentials, approvals, provider connections, or plugin
161
- * state of the source actor. A fork does not copy them.
162
- */
163
- const UNCOPIED_EVENT_TYPES = /* @__PURE__ */ new Set([
164
- "authorization_completed",
165
- "authorization_requested",
166
- "guardian_action_reviewed",
167
- "mcp_provider_connected",
168
- "mcp_provider_connected_unowned",
169
- "structured_event",
170
- "unknown"
171
- ]);
172
- 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.";
173
- /** An expected fork request failure, such as a reply without saved history. */
174
- var ConversationForkError = class extends Error {};
175
- /**
176
- * Create a new web root Conversation from the agent history of a source
177
- * Conversation, through one completed assistant reply.
178
- *
179
- * Retries with the same requester and key return the same Conversation.
180
- */
181
- async function forkConversation(input) {
182
- const conversationId = createConversationId({
183
- actorEmail: input.actorEmail,
184
- idempotencyKey: JSON.stringify([
185
- "fork",
186
- input.sourceConversationId,
187
- input.idempotencyKey
188
- ])
189
- });
190
- const executor = getSqlExecutor();
191
- const events = getConversationEventStore();
192
- const store = getConversationStore();
193
- return withConversationEventLock(executor, input.sourceConversationId, () => withConversationEventLock(executor, conversationId, async () => {
194
- if (await store.get({ conversationId })) return { conversationId };
195
- const source = await store.get({ conversationId: input.sourceConversationId });
196
- if (!source || source.transcriptPurgedAtMs !== void 0) throw new ConversationForkError("The source history is no longer available.");
197
- if (source.parentConversationId) throw new ConversationForkError("You cannot fork a child conversation.");
198
- const boundary = await events.loadByIdempotencyKey(input.sourceConversationId, `message:${input.messageId}:agent`);
199
- const reply = boundary?.data.type === "assistant_message" ? piMessageFromHistoryItem(boundary.data) : void 0;
200
- 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.");
201
- const projection = await loadTurnProjection({
202
- conversationId: input.sourceConversationId,
203
- committedSeq: boundary.seq,
204
- includeTail: false
205
- });
206
- if (!projection?.messages.length) throw new ConversationForkError("The selected history is no longer available.");
207
- if (hasUnfinishedToolCalls(projection.messages)) throw new ConversationForkError("This history has unfinished tool calls. Choose another reply.");
208
- const nowMs = Date.now();
209
- await recordWebConversationActivity({
210
- actor: webActorFromEmail(input.actorEmail, { ...input.actorName ? { fullName: input.actorName } : void 0 }),
211
- conversationId,
212
- nowMs,
213
- rootVisibility: source.visibility === "public" ? "public" : "private"
214
- });
215
- await executor.db().update(juniorConversations).set({
216
- forkedFromConversationId: source.conversationId,
217
- title: source.title ? `Fork: ${source.title}` : "Forked conversation"
218
- }).where(eq(juniorConversations.conversationId, conversationId));
219
- await copyForkEvents({
220
- sourceConversationId: source.conversationId,
221
- conversationId,
222
- events: await events.loadHistory(source.conversationId),
223
- boundarySeq: boundary.seq,
224
- messageId: input.messageId
225
- });
226
- await events.append(conversationId, [{
227
- idempotencyKey: FORK_NOTE_KEY,
228
- createdAtMs: nowMs,
229
- data: {
230
- type: "user_message",
231
- provenance: contextProvenance,
232
- content: [{
233
- type: "text",
234
- text: FORK_NOTE
235
- }],
236
- timestamp: nowMs
237
- }
238
- }]);
239
- return { conversationId };
240
- }));
241
- }
242
- /**
243
- * Copy the source event rows through the selected reply. Copies keep their
244
- * sequence, author, timestamp, key, and payload, so the fork shows the same
245
- * transcript and event log, and seq references in compactions stay valid.
246
- * Later updates of copied Messages and the end of copied Turns are also
247
- * copied, so that no copied item stays open in the fork.
248
- */
249
- async function copyForkEvents(args) {
250
- const reply = args.events.find((event) => event.data.type === "message" && event.data.role === "assistant" && event.data.messageId === args.messageId);
251
- const cutoffSeq = Math.max(args.boundarySeq, reply?.seq ?? -1);
252
- const messageIds = /* @__PURE__ */ new Set();
253
- const turnIds = /* @__PURE__ */ new Set();
254
- const seqs = [];
255
- let historyVersion = 0;
256
- for (const event of args.events) {
257
- const data = event.data;
258
- if (UNCOPIED_EVENT_TYPES.has(data.type)) continue;
259
- if (event.idempotencyKey === FORK_NOTE_KEY) continue;
260
- if (event.seq <= cutoffSeq) {
261
- if (data.type === "message") messageIds.add(data.messageId);
262
- if (data.type === "turn_started") turnIds.add(data.turnId);
263
- historyVersion = Math.max(historyVersion, event.historyVersion);
264
- seqs.push(event.seq);
265
- } 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);
266
- }
267
- if (!seqs.length) return;
268
- const e = juniorConversationEvents;
269
- await getSqlExecutor().db().execute(sql`insert into ${e} (${sql.join([
270
- e.conversationId,
271
- e.seq,
272
- e.historyVersion,
273
- e.schemaVersion,
274
- e.idempotencyKey,
275
- e.type,
276
- e.payload,
277
- e.actorIdentityId,
278
- e.createdAt
279
- ].map((column) => sql.identifier(column.name)), sql`, `)})
280
- select ${args.conversationId}, ${e.seq}, least(${e.historyVersion}, ${historyVersion}), ${e.schemaVersion}, ${e.idempotencyKey}, ${e.type}, ${e.payload}, ${e.actorIdentityId}, ${e.createdAt}
281
- from ${e}
282
- where ${e.conversationId} = ${args.sourceConversationId}
283
- and ${inArray(e.seq, seqs)}`);
284
- }
285
- function hasUnfinishedToolCalls(messages) {
286
- const pending = /* @__PURE__ */ new Set();
287
- for (const message of messages) if (message.role === "assistant") {
288
- for (const part of message.content) if (part.type === "toolCall") pending.add(part.id);
289
- } else if (message.role === "toolResult") pending.delete(message.toolCallId);
290
- return pending.size > 0;
291
- }
292
- // src/api/conversations/auxiliary-costs.ts
293
- function eventCost() {
294
- return sql`CASE
295
- WHEN ${juniorConversationEvents.type} = 'structured_event'
296
- AND jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
297
- THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::numeric
298
- WHEN ${juniorConversationEvents.type} IN ('guardian_action_reviewed', 'turn_routed')
299
- AND jsonb_typeof(${juniorConversationEvents.payload}->'costUsd') = 'number'
300
- THEN (${juniorConversationEvents.payload}->>'costUsd')::numeric
301
- ELSE NULL
302
- END`;
303
- }
304
- function addCostUsd(left, right) {
305
- return Math.round((left + right) * 0xe8d4a51000) / 0xe8d4a51000;
306
- }
307
- /**
308
- * Aggregate additive event costs by namespaced operation. The total for each
309
- * selected Conversation includes itself and all its child agents at any depth.
310
- */
311
- async function readConversationAuxiliaryCostsFromSql(db, conversationIds) {
312
- if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
313
- const scopeConversationId = conversationSubtreeColumns.scopeConversationId;
314
- const isChildAgent = sql`(${conversationSubtreeColumns.conversationId} <> ${scopeConversationId})`;
315
- const namespace = sql`CASE
316
- WHEN ${juniorConversationEvents.type} = 'structured_event'
317
- THEN ${juniorConversationEvents.payload}->>'namespace'
318
- ELSE 'junior'
319
- END`;
320
- const name = sql`CASE
321
- WHEN ${juniorConversationEvents.type} = 'structured_event'
322
- THEN ${juniorConversationEvents.payload}->>'name'
323
- ELSE ${juniorConversationEvents.type}
324
- END`;
325
- const cost = eventCost();
326
- const rows = await db.select({
327
- conversationId: scopeConversationId,
328
- childAgentCostUsd: sql`coalesce(round(sum(case when ${isChildAgent} then ${cost} end), 12), 0)::double precision`.mapWith(Number),
329
- costUsd: sql`round(sum(${cost}), 12)::double precision`.mapWith(Number),
330
- events: sql`count(*)::integer`.mapWith(Number),
331
- name,
332
- namespace
333
- }).from(juniorConversationEvents).innerJoin(conversationSubtree(conversationIds), eq(conversationSubtreeColumns.conversationId, juniorConversationEvents.conversationId)).where(and(or(eq(juniorConversationEvents.type, "structured_event"), inArray(juniorConversationEvents.type, ["guardian_action_reviewed", "turn_routed"])), sql`${cost} >= 0`, notCopiedByFork())).groupBy(scopeConversationId, namespace, name).orderBy(scopeConversationId, namespace, name);
334
- const result = /* @__PURE__ */ new Map();
335
- for (const row of rows) {
336
- const current = result.get(row.conversationId) ?? { total: {
337
- costUsd: 0,
338
- operations: []
339
- } };
340
- current.total.costUsd = addCostUsd(current.total.costUsd, row.costUsd);
341
- if (row.childAgentCostUsd > 0) current.childAgentCostUsd = addCostUsd(current.childAgentCostUsd ?? 0, row.childAgentCostUsd);
342
- current.total.operations.push({
343
- costUsd: row.costUsd,
344
- events: row.events,
345
- name: row.name,
346
- namespace: row.namespace
347
- });
348
- result.set(row.conversationId, current);
349
- }
350
- return result;
351
- }
352
- // src/api/conversations/usage.ts
353
- const usage$1 = juniorConversations.usage;
354
- const componentFields = [
355
- "inputTokens",
356
- "outputTokens",
357
- "cachedInputTokens",
358
- "cacheCreationTokens"
359
- ];
360
- function summedUsageNumber(field) {
361
- return sql`sum((${usage$1}->>${field})::double precision)::double precision`;
362
- }
363
- function summedCostNumber(field) {
364
- return sql`round(
365
- sum((${usage$1}->'cost'->>${field})::numeric),
366
- 12
367
- )::double precision`;
368
- }
369
- function usageFromAggregate(row) {
370
- const components = {};
371
- let componentTotal = 0;
372
- for (const field of componentFields) {
373
- const value = row[field];
374
- if (value === null) continue;
375
- components[field] = value;
376
- componentTotal += value;
377
- }
378
- const cost = {
379
- ...row.costInput === null ? void 0 : { input: row.costInput },
380
- ...row.costOutput === null ? void 0 : { output: row.costOutput },
381
- ...row.costCacheRead === null ? void 0 : { cacheRead: row.costCacheRead },
382
- ...row.costCacheWrite === null ? void 0 : { cacheWrite: row.costCacheWrite },
383
- ...row.costTotal === null ? void 0 : { total: row.costTotal }
384
- };
385
- const result = {
386
- ...row.totalOnlyTokens === null ? components : { totalTokens: row.totalOnlyTokens + componentTotal },
387
- ...row.reasoningTokens === null ? void 0 : { reasoningTokens: row.reasoningTokens },
388
- ...Object.keys(cost).length === 0 ? void 0 : { cost }
389
- };
390
- return hasAgentTurnUsage(result) ? result : void 0;
391
- }
392
- /**
393
- * Aggregate saved runtime and usage in PostgreSQL. The total for each selected
394
- * Conversation includes itself and all its child agents at any depth.
395
- */
396
- async function readConversationTreeMetricsFromSql(db, conversationIds) {
397
- if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
398
- const hasComponents = sql`coalesce(
399
- ${usage$1}->>'inputTokens',
400
- ${usage$1}->>'outputTokens',
401
- ${usage$1}->>'cachedInputTokens',
402
- ${usage$1}->>'cacheCreationTokens'
403
- ) is not null`;
404
- const isChildAgent = sql`(${conversationSubtreeColumns.conversationId} <> ${conversationSubtreeColumns.scopeConversationId})`;
405
- const rows = await db.select({
406
- scopeConversationId: conversationSubtreeColumns.scopeConversationId,
407
- isChildAgent,
408
- conversations: sql`count(*)::integer`.mapWith(Number),
409
- durationMs: sql`coalesce(sum(${juniorConversations.durationMs}), 0)::double precision`,
410
- inputTokens: summedUsageNumber("inputTokens"),
411
- outputTokens: summedUsageNumber("outputTokens"),
412
- cachedInputTokens: summedUsageNumber("cachedInputTokens"),
413
- cacheCreationTokens: summedUsageNumber("cacheCreationTokens"),
414
- totalOnlyTokens: sql`sum(
415
- case
416
- when not (${hasComponents})
417
- then (${usage$1}->>'totalTokens')::double precision
418
- end
419
- )::double precision`,
420
- reasoningTokens: summedUsageNumber("reasoningTokens"),
421
- costInput: summedCostNumber("input"),
422
- costOutput: summedCostNumber("output"),
423
- costCacheRead: summedCostNumber("cacheRead"),
424
- costCacheWrite: summedCostNumber("cacheWrite"),
425
- costTotal: summedCostNumber("total")
426
- }).from(juniorConversations).innerJoin(conversationSubtree(conversationIds), eq(conversationSubtreeColumns.conversationId, juniorConversations.conversationId)).groupBy(conversationSubtreeColumns.scopeConversationId, isChildAgent);
427
- const ownRows = /* @__PURE__ */ new Map();
428
- const childRows = /* @__PURE__ */ new Map();
429
- for (const row of rows) (row.isChildAgent ? childRows : ownRows).set(row.scopeConversationId, row);
430
- const result = /* @__PURE__ */ new Map();
431
- for (const [conversationId, own] of ownRows) {
432
- const ownUsage = usageFromAggregate(own);
433
- const child = childRows.get(conversationId);
434
- const childUsage = child ? usageFromAggregate(child) : void 0;
435
- const usage = addAgentTurnUsage(ownUsage, childUsage);
436
- result.set(conversationId, {
437
- durationMs: own.durationMs + (child?.durationMs ?? 0),
438
- ...usage ? { usage } : void 0,
439
- ...child ? { childAgents: {
440
- count: child.conversations,
441
- durationMs: child.durationMs,
442
- ...childUsage ? { usage: childUsage } : void 0
443
- } } : void 0
444
- });
445
- }
446
- return result;
447
- }
448
- /**
449
- * Project the child agent part of tree metrics into the API shape. Return
450
- * nothing when the Conversation has no child agents.
451
- */
452
- function childAgentTotalsReport(metrics, auxiliaryCosts) {
453
- const childAgents = metrics?.childAgents;
454
- if (!childAgents) return void 0;
455
- return {
456
- ...auxiliaryCosts?.childAgentCostUsd === void 0 ? void 0 : { auxiliaryCostUsd: auxiliaryCosts.childAgentCostUsd },
457
- count: childAgents.count,
458
- cumulativeDurationMs: childAgents.durationMs,
459
- ...childAgents.usage ? { cumulativeUsage: childAgents.usage } : void 0
460
- };
461
- }
462
142
  // src/api/conversations/child-agents.ts
463
143
  /**
464
144
  * Load the direct child agents of one Conversation, or throw a standard API
@@ -577,174 +257,50 @@ async function createConversationForViewer(viewer, body, services) {
577
257
  async function appendConversationMessageForViewer(viewer, conversationId, body, services) {
578
258
  const conversation = await getConversationStore().get({ conversationId });
579
259
  if (!conversation) throwApiError(404, "Conversation not found.");
580
- const destinationPlatform = conversation.destination?.platform;
581
- if (!(destinationPlatform === "local" && conversationId.startsWith("local:web:") || destinationPlatform === "slack")) throwApiError(409, "Conversation does not accept web messages.");
260
+ if (!acceptsWebMessages(conversationId, conversation.destination)) throwApiError(409, "Conversation does not accept web messages.");
582
261
  if (!(await readConversationAccessFromSql(getDb(), [conversationId], viewer)).get(conversationId)?.isParticipant) throwApiError(403, "Only conversation participants can add messages.");
583
262
  const images = parseImages(body.images);
263
+ const actor = actorFromViewer(viewer);
264
+ const append = () => appendAndEnqueueWebMessage({
265
+ actor,
266
+ conversationId,
267
+ idempotencyKey: body.idempotencyKey,
268
+ message: body.message,
269
+ images
270
+ }, {
271
+ conversationStore: getConversationStore(),
272
+ attachmentStorage: services.attachmentStorage,
273
+ queue: services.queue
274
+ });
275
+ const destination = conversation.destination;
584
276
  try {
585
- return await appendAndEnqueueWebMessage({
586
- actor: actorFromViewer(viewer),
587
- conversationId,
588
- idempotencyKey: body.idempotencyKey,
589
- message: body.message,
590
- images
591
- }, {
592
- conversationStore: getConversationStore(),
593
- attachmentStorage: services.attachmentStorage,
594
- queue: services.queue
277
+ if (destination?.platform !== "slack") return await append();
278
+ return await runWithWorkspaceTeamId(destination.teamId, async () => {
279
+ const state = getStateAdapter();
280
+ await state.connect();
281
+ return persistSlackUserInputReply({
282
+ channelId: destination.channelId,
283
+ claimedBy: viewer.email,
284
+ isSaved: async () => {
285
+ const [messageId, work] = await Promise.all([webMessageId({
286
+ conversationId,
287
+ idempotencyKey: body.idempotencyKey
288
+ }), getConversationWorkState({
289
+ conversationId,
290
+ state
291
+ })]);
292
+ return work?.execution.inboundMessageIds.includes(messageId) === true;
293
+ },
294
+ note: () => webUserInputAnswerNote(viewer.displayName ?? viewer.email),
295
+ persist: append,
296
+ state,
297
+ threadTs: conversation.location?.threadTs ?? (conversation.sessionSource?.kind === "slack" ? conversation.sessionSource.threadTs : void 0)
298
+ });
595
299
  });
596
300
  } catch (error) {
597
301
  throwApiError(500, "Unable to append message.", error);
598
302
  }
599
303
  }
600
- // src/chat/plugins/conversation-sidebar.ts
601
- /** Show native objects by default and let plugins select compact sidebar labels. */
602
- async function listConversationSidebarAnnotations(conversationIds, annotationsByConversation) {
603
- const candidates = new Set(conversationIds);
604
- const selected = {};
605
- const plugins = getPlugins();
606
- const customOwners = new Set(plugins.filter((plugin) => plugin.hooks?.conversationSidebar).map((plugin) => plugin.manifest.name));
607
- for (const conversationId of conversationIds) {
608
- const objects = (annotationsByConversation.get(conversationId) ?? []).flatMap((annotation) => {
609
- if (customOwners.has(annotation.plugin) || !annotation.objectType) return [];
610
- return [{
611
- key: annotation.key,
612
- label: annotation.label,
613
- objectType: annotation.objectType === "item" && annotation.kind === "object" && annotation.facts?.type === "deployment" ? "deployment" : annotation.objectType,
614
- status: annotation.status
615
- }];
616
- });
617
- if (objects.length) selected[conversationId] = objects;
618
- }
619
- for (const plugin of plugins) {
620
- const hook = plugin.hooks?.conversationSidebar;
621
- if (!hook) continue;
622
- const annotationsByConversationId = Object.fromEntries(conversationIds.flatMap((conversationId) => {
623
- const annotations = (annotationsByConversation.get(conversationId) ?? []).filter((annotation) => annotation.plugin === plugin.manifest.name);
624
- return annotations.length > 0 ? [[conversationId, annotations]] : [];
625
- }));
626
- try {
627
- const result = await hook({
628
- annotationsByConversationId,
629
- conversationIds,
630
- db: getDb(),
631
- log: createPluginLogger(plugin.manifest.name),
632
- plugin: { name: plugin.manifest.name },
633
- cache: createPluginCache(plugin.manifest.name)
634
- });
635
- for (const [conversationId, annotations] of Object.entries(result.annotationsByConversationId)) {
636
- if (!candidates.has(conversationId)) continue;
637
- const parsed = conversationSidebarAnnotationSchema.array().safeParse(annotations);
638
- if (!parsed.success || parsed.data.length === 0) continue;
639
- selected[conversationId] = [...selected[conversationId] ?? [], ...parsed.data];
640
- }
641
- } catch (error) {
642
- logWarn("plugin.conversation_sidebar.hook.failed", {
643
- "app.plugin.name": plugin.manifest.name,
644
- "exception.message": error instanceof Error ? error.message : String(error)
645
- });
646
- }
647
- }
648
- return selected;
649
- }
650
- // src/chat/slack/location-url.ts
651
- /** Parse a Slack workspace subdomain used in `https://{domain}.slack.com` URLs. */
652
- function parseSlackTeamDomain(value) {
653
- if (typeof value !== "string") return void 0;
654
- const domain = value.trim().toLowerCase();
655
- if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(domain)) return;
656
- return domain;
657
- }
658
- /** Convert a Slack message ts into the `p…` segment used by archive URLs. */
659
- function archivePathFromThreadTs(threadTs) {
660
- const [seconds, fraction = ""] = threadTs.split(".");
661
- return `p${seconds}${(fraction ?? "").padEnd(6, "0").slice(0, 6)}`;
662
- }
663
- /**
664
- * Build a direct Slack archive URL for a thread when the workspace domain is
665
- * known. Omits a link rather than falling back to `app_redirect`.
666
- */
667
- function buildSlackLocationUrl(args) {
668
- const channelId = parseSlackChannelId(args.channelId);
669
- const teamDomain = parseSlackTeamDomain(args.teamDomain);
670
- const threadTs = parseSlackMessageTs(args.threadTs);
671
- if (!channelId || !teamDomain || !threadTs) return void 0;
672
- const url = new URL(`https://${teamDomain}.slack.com/archives/${channelId}/${archivePathFromThreadTs(threadTs)}`);
673
- url.searchParams.set("thread_ts", threadTs);
674
- url.searchParams.set("cid", channelId);
675
- return url.toString();
676
- }
677
- // src/chat/slack/team-domain.ts
678
- const TEAM_DOMAIN_CACHE_TTL_MS = 864e5;
679
- const TEAM_DOMAIN_NEGATIVE_CACHE_TTL_MS = 6e4;
680
- const teamDomainCache = /* @__PURE__ */ new Map();
681
- const teamDomainInflight = /* @__PURE__ */ new Map();
682
- function readTeamDomainCache(teamId) {
683
- const hit = teamDomainCache.get(teamId);
684
- if (!hit) return void 0;
685
- if (hit.expiresAt < Date.now()) {
686
- teamDomainCache.delete(teamId);
687
- return;
688
- }
689
- return hit.kind === "hit" ? hit.domain : null;
690
- }
691
- function writeTeamDomainCache(teamId, domain) {
692
- if (domain) {
693
- teamDomainCache.set(teamId, {
694
- kind: "hit",
695
- domain,
696
- expiresAt: Date.now() + TEAM_DOMAIN_CACHE_TTL_MS
697
- });
698
- return;
699
- }
700
- teamDomainCache.set(teamId, {
701
- kind: "miss",
702
- expiresAt: Date.now() + TEAM_DOMAIN_NEGATIVE_CACHE_TTL_MS
703
- });
704
- }
705
- async function fetchSlackTeamDomain(teamId) {
706
- try {
707
- const response = await withSlackRetries(() => getSlackClient().team.info({ team: teamId }), 3, {
708
- action: "team.info",
709
- idempotent: true,
710
- spanAttributes: { "app.slack.team_id": teamId }
711
- });
712
- const responseTeamId = parseSlackTeamId(response.team?.id);
713
- const domain = parseSlackTeamDomain(response.team?.domain);
714
- if (!domain || !responseTeamId || responseTeamId !== teamId) {
715
- writeTeamDomainCache(teamId, void 0);
716
- return;
717
- }
718
- writeTeamDomainCache(teamId, domain);
719
- return domain;
720
- } catch {
721
- writeTeamDomainCache(teamId, void 0);
722
- return;
723
- }
724
- }
725
- /** Resolve a workspace subdomain via `team.info`, cached by team id. */
726
- async function resolveSlackTeamDomain(teamId) {
727
- const parsedTeamId = parseSlackTeamId(teamId);
728
- if (!parsedTeamId) return void 0;
729
- const cached = readTeamDomainCache(parsedTeamId);
730
- if (cached !== void 0) return cached ?? void 0;
731
- const pending = teamDomainInflight.get(parsedTeamId);
732
- if (pending) return pending;
733
- const lookup = fetchSlackTeamDomain(parsedTeamId).finally(() => {
734
- teamDomainInflight.delete(parsedTeamId);
735
- });
736
- teamDomainInflight.set(parsedTeamId, lookup);
737
- return lookup;
738
- }
739
- /** Resolve workspace subdomains for the distinct team ids in one API response. */
740
- async function resolveSlackTeamDomains(teamIds) {
741
- const uniqueTeamIds = [...new Set([...teamIds].map((teamId) => parseSlackTeamId(teamId)).filter((teamId) => teamId !== void 0))];
742
- const resolved = await Promise.all(uniqueTeamIds.map(async (teamId) => {
743
- const domain = await resolveSlackTeamDomain(teamId);
744
- return domain ? [teamId, domain] : void 0;
745
- }));
746
- return new Map(resolved.filter((entry) => entry !== void 0));
747
- }
748
304
  // src/chat/pi/sql-model-usage.ts
749
305
  /** SQL reporting adapter for usage stored on assistant history events. */
750
306
  const message = sql`${juniorConversationEvents.payload}`;
@@ -888,687 +444,6 @@ function decodeConversationCursor(args) {
888
444
  return;
889
445
  }
890
446
  }
891
- // src/api/conversations/first-message.ts
892
- /**
893
- * Read the text of the first human message for each untitled conversation.
894
- * Read-time titles use it while the generated title is still unset.
895
- */
896
- async function readFirstHumanMessageTexts(db, conversations) {
897
- const untitledIds = conversations.flatMap((conversation) => conversation.title ? [] : [conversation.conversationId]);
898
- if (untitledIds.length === 0) return /* @__PURE__ */ new Map();
899
- const text = sql`${juniorConversationEvents.payload}->>'text'`;
900
- const rows = await db.selectDistinctOn([juniorConversationEvents.conversationId], {
901
- conversationId: juniorConversationEvents.conversationId,
902
- text: text.as("text")
903
- }).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));
904
- return new Map(rows.map((row) => [row.conversationId, row.text]));
905
- }
906
- // src/api/mentions.ts
907
- const SLACK_USER_MENTION_RE = /<@([UW][A-Z0-9]+)(?:\|[^<>]*)?>/g;
908
- function mentionedSlackUserIds(texts) {
909
- const ids = texts.flatMap((text) => text ? [...text.matchAll(SLACK_USER_MENTION_RE)].map((m) => m[1]) : []);
910
- return [...new Set(ids)];
911
- }
912
- /**
913
- * Resolve the people that each source mentions, in source order. Every API
914
- * that returns display text uses this, so the dashboard can show the same
915
- * names and People links everywhere. It does one identity query per Slack
916
- * workspace.
917
- */
918
- async function resolveMentions(executor, sources) {
919
- const idsBySource = sources.map((source) => mentionedSlackUserIds(source.texts));
920
- const idsByTenant = /* @__PURE__ */ new Map();
921
- sources.forEach((source, index) => {
922
- if (idsBySource[index].length === 0) return;
923
- const ids = idsByTenant.get(source.tenantId) ?? /* @__PURE__ */ new Set();
924
- for (const id of idsBySource[index]) ids.add(id);
925
- idsByTenant.set(source.tenantId, ids);
926
- });
927
- const peopleByTenant = new Map(await Promise.all([...idsByTenant].map(async ([tenantId, ids]) => [tenantId, await readMentionedPeople(executor, tenantId, [...ids])])));
928
- return sources.map((source, index) => {
929
- const people = peopleByTenant.get(source.tenantId);
930
- return idsBySource[index].flatMap((id) => {
931
- const person = people?.get(id);
932
- return person ? [person] : [];
933
- });
934
- });
935
- }
936
- /**
937
- * Read current names for mentioned Slack users. Known Junior identities come
938
- * first. The email is set only for a linked, verified Junior user, because
939
- * that email is the People profile route.
940
- */
941
- async function readMentionedPeople(executor, tenantId, userIds) {
942
- const rows = await executor.db().select({
943
- displayName: juniorIdentities.displayName,
944
- emailVerified: juniorIdentities.emailVerified,
945
- handle: juniorIdentities.handle,
946
- providerSubjectId: juniorIdentities.providerSubjectId,
947
- userDisplayName: juniorUsers.displayName,
948
- userEmail: juniorUsers.primaryEmail
949
- }).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));
950
- const people = /* @__PURE__ */ new Map();
951
- for (const row of rows) {
952
- if (people.has(row.providerSubjectId)) continue;
953
- const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
954
- const email = row.emailVerified ? row.userEmail?.trim() : void 0;
955
- people.set(row.providerSubjectId, {
956
- slackUserId: row.providerSubjectId,
957
- ...fullName ? { fullName } : void 0,
958
- ...row.handle ? { slackUserName: row.handle } : void 0,
959
- ...email ? { email } : void 0
960
- });
961
- }
962
- const unnamed = userIds.filter((id) => {
963
- const person = people.get(id);
964
- return !person?.fullName && !person?.slackUserName;
965
- });
966
- if (tenantId && unnamed.length > 0) {
967
- const profiles = await Promise.all(unnamed.map((id) => lookupSlackUser(tenantId, id)));
968
- unnamed.forEach((id, index) => {
969
- const profile = profiles[index];
970
- if (!profile?.fullName && !profile?.userName) return;
971
- people.set(id, {
972
- ...people.get(id),
973
- slackUserId: id,
974
- ...profile.fullName ? { fullName: profile.fullName } : void 0,
975
- ...profile.userName ? { slackUserName: profile.userName } : void 0
976
- });
977
- });
978
- }
979
- return people;
980
- }
981
- // src/api/conversations/mentions.ts
982
- /** Text in one event that the dashboard renders with mentions. */
983
- function eventMentionTexts(data) {
984
- switch (data.type) {
985
- case "message": return data.redacted ? [] : [
986
- data.text,
987
- data.trustedSummary,
988
- ...(data.events ?? []).map((event) => event.trustedSummary)
989
- ];
990
- case "assistant_message": return data.parts.map((part) => part.text);
991
- case "compaction":
992
- case "handoff": return [data.summary];
993
- case "structured_event": return [data.presentation.preview, ...(data.presentation.details ?? []).map((detail) => detail.description)];
994
- default: return [];
995
- }
996
- }
997
- function briefMentionTexts(brief) {
998
- return [
999
- brief.summary,
1000
- brief.intent,
1001
- brief.outcome.text,
1002
- ...brief.decisions.flatMap((decision) => [decision.text, decision.by]),
1003
- ...brief.openDecisions.flatMap((decision) => [decision.text, decision.owner]),
1004
- ...brief.facts
1005
- ];
1006
- }
1007
- function withMentions(value, mentions) {
1008
- return mentions.length > 0 ? {
1009
- ...value,
1010
- mentions
1011
- } : value;
1012
- }
1013
- /**
1014
- * Attach the people that each event and the Brief of one conversation
1015
- * mention, resolved at read time.
1016
- */
1017
- async function attachConversationMentions(executor, conversationId, content) {
1018
- const tenantId = (await loadConversationSlackTenantIds(executor, [conversationId])).get(conversationId);
1019
- const sources = content.events.map((event) => ({
1020
- tenantId,
1021
- texts: eventMentionTexts(event.data)
1022
- }));
1023
- if (content.brief) sources.push({
1024
- tenantId,
1025
- texts: briefMentionTexts(content.brief)
1026
- });
1027
- const mentions = await resolveMentions(executor, sources);
1028
- return {
1029
- briefMentions: content.brief ? mentions.at(-1) : [],
1030
- events: content.events.map((event, index) => withMentions(event, mentions[index]))
1031
- };
1032
- }
1033
- /** Attach the people that each activity preview mentions, resolved at read time. */
1034
- async function attachActivityPreviewMentions(executor, previews) {
1035
- const entries = [...previews];
1036
- const tenants = await loadConversationSlackTenantIds(executor, entries.map(([conversationId]) => conversationId));
1037
- const mentions = await resolveMentions(executor, entries.map(([conversationId, preview]) => ({
1038
- tenantId: tenants.get(conversationId),
1039
- texts: [preview.text]
1040
- })));
1041
- return new Map(entries.map(([conversationId, preview], index) => [conversationId, withMentions(preview, mentions[index])]));
1042
- }
1043
- // src/api/conversations/shared.ts
1044
- /** Format a Slack destination without exposing private channel names. */
1045
- function slackStatsLocationLabel(input) {
1046
- const channelId = input.channel;
1047
- if (!channelId) return void 0;
1048
- if (input.channelNameRedacted && input.channelName) return input.channelName;
1049
- const name = input.channelName?.replace(/^#/, "");
1050
- if (channelId.startsWith("D")) return "Direct Message";
1051
- if (channelId.startsWith("C")) return name ? `#${name}` : "Public Channel";
1052
- if (channelId.startsWith("G")) {
1053
- if (name?.startsWith("mpdm-")) return "Group DM";
1054
- return "Private Channel";
1055
- }
1056
- return name || channelId;
1057
- }
1058
- /** Return the generic display label for a conversation surface. */
1059
- function surfaceFallbackLabel(surface) {
1060
- if (surface === "scheduler") return "Scheduler";
1061
- if (surface === "api") return "API";
1062
- if (surface === "internal") return "Internal";
1063
- return "Conversation";
1064
- }
1065
- // src/api/conversations/projection.ts
1066
- const PRIVATE_CONVERSATION_LABEL = "Private Conversation";
1067
- function privateConversationLabel(slackConversation) {
1068
- if (!slackConversation) return PRIVATE_CONVERSATION_LABEL;
1069
- return slackConversation.visibility === "private" ? formatSlackConversationRedactedLabel(slackConversation) ?? PRIVATE_CONVERSATION_LABEL : PRIVATE_CONVERSATION_LABEL;
1070
- }
1071
- function surfaceFromConversationId(conversationId) {
1072
- if (parseSlackThreadId(conversationId)) return "slack";
1073
- if (conversationId.startsWith("scheduler:")) return "scheduler";
1074
- if (conversationId.startsWith("api:")) return "api";
1075
- return "internal";
1076
- }
1077
- function surfaceFromSource(source, conversationId) {
1078
- if (source === "slack" || source === "api" || source === "web" || source === "scheduler") return source === "web" ? "api" : source;
1079
- return surfaceFromConversationId(conversationId);
1080
- }
1081
- function locationUrlFromConversation(args) {
1082
- const { conversation } = args;
1083
- if (!args.canViewPrivateContent || conversation.location?.provider !== "slack") return;
1084
- const teamDomain = args.teamDomainByTeamId?.get(conversation.location.teamId);
1085
- const threadTs = conversation.location.threadTs;
1086
- if (!teamDomain || !threadTs) return void 0;
1087
- return buildSlackLocationUrl({
1088
- channelId: conversation.location.channelId,
1089
- teamDomain,
1090
- threadTs
1091
- });
1092
- }
1093
- function actorIdentityReport(actor) {
1094
- if (!actor) return void 0;
1095
- const identity = {
1096
- ...actor.email !== void 0 ? { email: actor.email } : void 0,
1097
- ...actor.fullName !== void 0 ? { fullName: actor.fullName } : void 0,
1098
- ...actor.slackUserId !== void 0 ? { slackUserId: actor.slackUserId } : void 0,
1099
- ...actor.slackUserName !== void 0 ? { slackUserName: actor.slackUserName } : void 0
1100
- };
1101
- return Object.keys(identity).length > 0 ? identity : void 0;
1102
- }
1103
- function statusFromConversation(conversation) {
1104
- if (conversation.execution.status === "idle") return "completed";
1105
- if (conversation.execution.status === "failed") return "failed";
1106
- return "active";
1107
- }
1108
- function titleFromConversation(args) {
1109
- const slackThread = parseSlackThreadId(args.conversation.conversationId);
1110
- const effectiveChannelName = args.conversation.channelName;
1111
- const slackConversation = resolveSlackConversationContextFromThreadId({
1112
- threadId: args.conversation.conversationId,
1113
- channelName: effectiveChannelName,
1114
- ...args.visibility ? { visibility: args.visibility } : void 0
1115
- });
1116
- return (args.canViewPrivateContent ? void 0 : privateConversationLabel(slackConversation)) ?? args.conversation.title ?? args.automationTitle ?? slackStatsLocationLabel({
1117
- channel: slackThread?.channelId,
1118
- channelName: effectiveChannelName
1119
- }) ?? (args.conversation.source === "plugin" ? "Automation run" : fallbackShortTitle(args.firstHumanMessageText ?? "", surfaceFallbackLabel(args.surface)));
1120
- }
1121
- function channelNameFromConversation(conversation, canViewPrivateContent, visibility) {
1122
- const effectiveChannelName = conversation.channelName;
1123
- const slackThread = parseSlackThreadId(conversation.conversationId);
1124
- if (!effectiveChannelName && !slackThread) return;
1125
- const slackConversation = resolveSlackConversationContextFromThreadId({
1126
- threadId: conversation.conversationId,
1127
- channelName: effectiveChannelName,
1128
- ...visibility ? { visibility } : void 0
1129
- });
1130
- if (!canViewPrivateContent) return privateConversationLabel(slackConversation);
1131
- return effectiveChannelName;
1132
- }
1133
- function channelNameRedactedFromConversation(conversation, canViewPrivateContent) {
1134
- const effectiveChannelName = conversation.channelName;
1135
- const slackThread = parseSlackThreadId(conversation.conversationId);
1136
- if (!effectiveChannelName && !slackThread) return false;
1137
- return !canViewPrivateContent;
1138
- }
1139
- /** Describe whether a viewer can read one conversation's retained events. */
1140
- function conversationEventHistory(args) {
1141
- if (args.transcriptPurgedAtMs !== void 0) return {
1142
- status: "expired",
1143
- expiredAt: new Date(args.transcriptPurgedAtMs).toISOString()
1144
- };
1145
- return args.canExposePayload ? { status: "available" } : {
1146
- status: "redacted",
1147
- reason: "non_public_conversation"
1148
- };
1149
- }
1150
- /** Project one durable conversation and its SQL metrics into the REST summary. */
1151
- function conversationSummaryFromStoredConversation(args) {
1152
- const { conversation, durationMs, usage } = args;
1153
- const canViewPrivateContent = args.access?.canViewPrivateContent ?? false;
1154
- const accessVisibility = args.access?.visibility;
1155
- const visibility = accessVisibility === "public" || accessVisibility === "private" ? accessVisibility : void 0;
1156
- const surface = surfaceFromSource(conversation.source, conversation.conversationId);
1157
- const actorIdentity = actorIdentityReport(conversation.actor);
1158
- const locationUrl = locationUrlFromConversation({
1159
- canViewPrivateContent,
1160
- conversation,
1161
- ...args.teamDomainByTeamId ? { teamDomainByTeamId: args.teamDomainByTeamId } : void 0
1162
- });
1163
- const slackThread = parseSlackThreadId(conversation.conversationId);
1164
- const channelName = channelNameFromConversation(conversation, canViewPrivateContent, visibility);
1165
- const channelNameRedacted = channelNameRedactedFromConversation(conversation, canViewPrivateContent);
1166
- return {
1167
- conversationId: conversation.conversationId,
1168
- cumulativeDurationMs: durationMs,
1169
- displayTitle: titleFromConversation({
1170
- automationTitle: args.automationTitle,
1171
- canViewPrivateContent,
1172
- conversation,
1173
- firstHumanMessageText: args.firstHumanMessageText,
1174
- surface,
1175
- visibility
1176
- }),
1177
- isParticipant: args.access?.isParticipant ?? false,
1178
- ...visibility ? { visibility } : void 0,
1179
- lastProgressAt: new Date(conversation.execution.updatedAtMs ?? conversation.updatedAtMs).toISOString(),
1180
- lastSeenAt: new Date(conversation.lastActivityAtMs).toISOString(),
1181
- startedAt: new Date(conversation.createdAtMs).toISOString(),
1182
- status: statusFromConversation(conversation),
1183
- surface,
1184
- ...args.auxiliaryCosts ? { auxiliaryCosts: args.auxiliaryCosts } : void 0,
1185
- ...usage ? { cumulativeUsage: usage } : void 0,
1186
- ...args.childAgentTotals ? { childAgentTotals: args.childAgentTotals } : void 0,
1187
- ...actorIdentity ? { actorIdentity } : void 0,
1188
- ...locationUrl ? { locationUrl } : void 0,
1189
- archivedAt: args.archivedAtMs === void 0 ? null : new Date(args.archivedAtMs).toISOString(),
1190
- ...slackThread ? { channel: slackThread.channelId } : void 0,
1191
- ...channelName ? { channelName } : void 0,
1192
- ...channelNameRedacted ? { channelNameRedacted: true } : void 0,
1193
- ...args.locationId && !channelNameRedacted ? { locationId: args.locationId } : void 0
1194
- };
1195
- }
1196
- // src/chat/plugins/unfinished-work.ts
1197
- /**
1198
- * Return assigned and unfinished plugin work for the candidate conversations.
1199
- * Feed Priority combines these signals with conversation activity on the host.
1200
- */
1201
- async function listConversationWork(conversationIds) {
1202
- if (conversationIds.length === 0) return {
1203
- assignedIds: [],
1204
- finishedAtById: {},
1205
- unfinishedIds: []
1206
- };
1207
- const candidates = new Set(conversationIds);
1208
- const assigned = /* @__PURE__ */ new Set();
1209
- const finishedAtById = /* @__PURE__ */ new Map();
1210
- const unfinished = /* @__PURE__ */ new Set();
1211
- for (const plugin of getPlugins()) {
1212
- const hook = plugin.hooks?.unfinishedWork;
1213
- if (!hook) continue;
1214
- try {
1215
- const result = await hook({
1216
- conversationIds,
1217
- db: getDb(),
1218
- log: createPluginLogger(plugin.manifest.name),
1219
- plugin: { name: plugin.manifest.name },
1220
- cache: createPluginCache(plugin.manifest.name)
1221
- });
1222
- for (const conversationId of result.conversationIds) {
1223
- if (!candidates.has(conversationId)) continue;
1224
- unfinished.add(conversationId);
1225
- assigned.add(conversationId);
1226
- }
1227
- for (const conversationId of result.assignedConversationIds ?? []) if (candidates.has(conversationId)) assigned.add(conversationId);
1228
- for (const [conversationId, finishedAt] of Object.entries(result.finishedWorkAtByConversationId ?? {})) {
1229
- if (!candidates.has(conversationId)) continue;
1230
- const time = Date.parse(finishedAt);
1231
- if (!Number.isFinite(time)) continue;
1232
- const current = finishedAtById.get(conversationId);
1233
- if (!current || time > Date.parse(current)) finishedAtById.set(conversationId, finishedAt);
1234
- }
1235
- } catch (error) {
1236
- logWarn("plugin.unfinished_work.hook.failed", {
1237
- "app.plugin.name": plugin.manifest.name,
1238
- "exception.message": error instanceof Error ? error.message : String(error)
1239
- });
1240
- }
1241
- }
1242
- return {
1243
- assignedIds: conversationIds.filter((conversationId) => assigned.has(conversationId)),
1244
- finishedAtById: Object.fromEntries(conversationIds.flatMap((conversationId) => {
1245
- const finishedAt = finishedAtById.get(conversationId);
1246
- return finishedAt ? [[conversationId, finishedAt]] : [];
1247
- })),
1248
- unfinishedIds: conversationIds.filter((conversationId) => unfinished.has(conversationId))
1249
- };
1250
- }
1251
- // src/api/conversations/priority.ts
1252
- /** Conversations with no known work leave Priority after this idle window. */
1253
- const UNASSIGNED_PRIORITY_WINDOW_MS = 108e5;
1254
- /** Unfinished work leaves Priority after this idle window. */
1255
- const UNFINISHED_PRIORITY_WINDOW_MS = 1728e5;
1256
- /**
1257
- * Decide whether a conversation belongs in the dashboard Priority section.
1258
- *
1259
- * Priority includes only:
1260
- * - unfinished work last seen within 48 hours
1261
- * - finished assigned work with a user message after the finish time
1262
- * - no known work last seen within 3 hours
1263
- *
1264
- * Finished assigned work with no later activity stays out of Priority.
1265
- */
1266
- function isConversationPriority(conversation, nowMs) {
1267
- const lastSeenAt = Date.parse(conversation.lastSeenAt);
1268
- if (!Number.isFinite(lastSeenAt)) return false;
1269
- if (conversation.unfinishedWork) return nowMs - lastSeenAt <= UNFINISHED_PRIORITY_WINDOW_MS;
1270
- if (conversation.assignedWork) {
1271
- const finishedAt = Date.parse(conversation.finishedWorkAt ?? "");
1272
- const lastUserMessageAt = Date.parse(conversation.lastUserMessageAt ?? "");
1273
- return Number.isFinite(finishedAt) && Number.isFinite(lastUserMessageAt) && lastUserMessageAt > finishedAt;
1274
- }
1275
- return nowMs - lastSeenAt <= UNASSIGNED_PRIORITY_WINDOW_MS;
1276
- }
1277
- // src/api/conversations/user-message-activity.ts
1278
- /** Read the latest true user message time for each selected conversation. */
1279
- async function readLastUserMessageAtByConversation(db, conversationIds) {
1280
- if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
1281
- const authority = sql`${juniorConversationEvents.payload}->'provenance'->>'authority'`;
1282
- const actorPlatform = sql`${juniorConversationEvents.payload}->'provenance'->'actor'->>'platform'`;
1283
- const role = sql`${juniorConversationEvents.payload}->>'role'`;
1284
- const rows = await db.select({
1285
- conversationId: juniorConversationEvents.conversationId,
1286
- lastUserMessageAt: sql`max(${juniorConversationEvents.createdAt})`
1287
- }).from(juniorConversationEvents).where(and(inArray(juniorConversationEvents.conversationId, conversationIds), or(and(eq(juniorConversationEvents.type, "user_message"), eq(authority, "instruction"), sql`${actorPlatform} in ('slack', 'local', 'web')`), and(eq(juniorConversationEvents.type, "message"), eq(role, "user"), isNotNull(juniorConversationEvents.actorIdentityId))))).groupBy(juniorConversationEvents.conversationId);
1288
- return new Map(rows.map((row) => [row.conversationId, new Date(row.lastUserMessageAt).toISOString()]));
1289
- }
1290
- // src/api/conversations/activity-preview.ts
1291
- /** Read one recent assistant message, or the latest user message, for each conversation. */
1292
- async function readConversationActivityPreviews(db, conversationIds) {
1293
- if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
1294
- const role = sql`${juniorConversationEvents.payload}->>'role'`;
1295
- const text = sql`${juniorConversationEvents.payload}->>'text'`;
1296
- const assistantFirst = sql`case when ${role} = 'assistant' then 0 else 1 end`;
1297
- const rows = await db.selectDistinctOn([juniorConversationEvents.conversationId], {
1298
- conversationId: juniorConversationEvents.conversationId,
1299
- createdAt: juniorConversationEvents.createdAt,
1300
- role: role.as("role"),
1301
- text: text.as("text")
1302
- }).from(juniorConversationEvents).where(and(inArray(juniorConversationEvents.conversationId, conversationIds), eq(juniorConversationEvents.type, "message"), sql`${role} in ('assistant', 'user')`, isNotNull(sql`nullif(btrim(${text}), '')`))).orderBy(juniorConversationEvents.conversationId, assistantFirst, desc(juniorConversationEvents.createdAt), desc(juniorConversationEvents.seq));
1303
- return new Map(rows.map((row) => [row.conversationId, {
1304
- createdAt: row.createdAt.toISOString(),
1305
- role: row.role,
1306
- text: row.text
1307
- }]));
1308
- }
1309
- // src/api/conversations/participants.ts
1310
- const identityColumns = {
1311
- displayName: juniorIdentities.displayName,
1312
- email: juniorIdentities.email,
1313
- handle: juniorIdentities.handle,
1314
- identityId: juniorIdentities.id,
1315
- provider: juniorIdentities.provider,
1316
- providerSubjectId: juniorIdentities.providerSubjectId,
1317
- userDisplayName: juniorUsers.displayName,
1318
- userEmail: juniorUsers.primaryEmail,
1319
- userId: juniorIdentities.userId
1320
- };
1321
- function participantIdentity(row) {
1322
- const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
1323
- const email = row.email?.trim() || row.userEmail?.trim();
1324
- const slack = row.provider === "slack";
1325
- const participant = {
1326
- ...email ? { email } : void 0,
1327
- ...fullName ? { fullName } : void 0,
1328
- ...slack && row.providerSubjectId ? { slackUserId: row.providerSubjectId } : void 0,
1329
- ...slack && row.handle ? { slackUserName: row.handle } : void 0
1330
- };
1331
- return Object.keys(participant).length > 0 ? participant : void 0;
1332
- }
1333
- /** Read actor identities in first-appearance order for each Conversation. */
1334
- async function readConversationParticipants(db, conversationIds) {
1335
- if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
1336
- const [rootRows, eventRows] = await Promise.all([db.select({
1337
- conversationId: juniorConversations.conversationId,
1338
- ...identityColumns
1339
- }).from(juniorConversations).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(inArray(juniorConversations.conversationId, [...conversationIds])), db.selectDistinctOn([juniorConversationEvents.conversationId, juniorConversationEvents.actorIdentityId], {
1340
- conversationId: juniorConversationEvents.conversationId,
1341
- seq: juniorConversationEvents.seq,
1342
- ...identityColumns
1343
- }).from(juniorConversationEvents).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(inArray(juniorConversationEvents.conversationId, [...conversationIds])).orderBy(asc(juniorConversationEvents.conversationId), asc(juniorConversationEvents.actorIdentityId), asc(juniorConversationEvents.seq))]);
1344
- const ordered = /* @__PURE__ */ new Map();
1345
- const add = (conversationId, row) => {
1346
- const participant = participantIdentity(row);
1347
- if (!participant) return;
1348
- const group = ordered.get(conversationId) ?? {
1349
- identities: [],
1350
- keys: /* @__PURE__ */ new Set()
1351
- };
1352
- const key = row.userId ? `user:${row.userId}` : `identity:${row.identityId}`;
1353
- if (!group.keys.has(key)) {
1354
- group.keys.add(key);
1355
- group.identities.push(participant);
1356
- }
1357
- ordered.set(conversationId, group);
1358
- };
1359
- for (const row of rootRows) add(row.conversationId, row);
1360
- for (const row of eventRows.sort((left, right) => left.seq - right.seq)) add(row.conversationId, row);
1361
- return new Map([...ordered].map(([conversationId, group]) => [conversationId, group.identities]));
1362
- }
1363
- // src/api/conversations/list.ts
1364
- const CONVERSATION_FEED_LIMIT = 50;
1365
- function conversationFeedMembershipFilter(status, filter) {
1366
- if (!filter) return status === "archived" ? sql`false` : void 0;
1367
- if (filter.kind === "viewer") return and(conversationIsMemberRoot({ userId: filter.userId }), status === "archived" ? conversationArchivedForUser(filter.userId) : status === "all" ? void 0 : conversationNotArchivedForUser(filter.userId));
1368
- return and(conversationIsMemberRoot({ email: filter.email }), status === "archived" ? conversationArchivedForEmail(filter.email) : status === "all" ? void 0 : conversationNotArchivedForEmail(filter.email));
1369
- }
1370
- /**
1371
- * Keep root conversations linked to one code repository. A code change can
1372
- * link a child conversation, so linked ids resolve to their root. Private
1373
- * roots stay visible only to their members.
1374
- */
1375
- function conversationCodeRepositoryFilter(repositoryId, viewerUserId) {
1376
- return and(sql`${juniorConversations.conversationId} IN (
1377
- SELECT coalesce(linked.root_conversation_id, linked.conversation_id)
1378
- FROM ${juniorConversations} AS linked
1379
- WHERE linked.conversation_id IN (
1380
- SELECT unnest(${juniorCodeChanges.conversationIds})
1381
- FROM ${juniorCodeChanges}
1382
- WHERE ${juniorCodeChanges.repositoryId} = ${repositoryId}
1383
- )
1384
- )`, or(eq(juniorDestinations.visibility, "public"), viewerUserId ? viewerConversationMembership({
1385
- actorMatch: eq(juniorIdentities.userId, viewerUserId),
1386
- participantMatch: conversationHasParticipantUser(viewerUserId)
1387
- }) : void 0));
1388
- }
1389
- async function conversationRows(db, limit, status, filter, query, includePrivateBriefs = false, codeRepository) {
1390
- return db.select({
1391
- conversation: conversationReadColumns(),
1392
- location: juniorDestinations,
1393
- identityDisplayName: juniorIdentities.displayName,
1394
- identityEmail: juniorIdentities.email,
1395
- identityHandle: juniorIdentities.handle,
1396
- identityProvider: juniorIdentities.provider,
1397
- identitySubjectId: juniorIdentities.providerSubjectId,
1398
- identityTenantId: juniorIdentities.providerTenantId,
1399
- userDisplayName: juniorUsers.displayName
1400
- }).from(juniorConversations).leftJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(isNull(juniorConversations.parentConversationId), conversationFeedMembershipFilter(status, filter), codeRepository ? conversationCodeRepositoryFilter(codeRepository.id, codeRepository.viewerUserId) : void 0, query ? or(sql`strpos(lower(coalesce(${juniorConversations.title}, '')), ${query}) > 0`, sql`exists (
1401
- select 1
1402
- from ${juniorConversationBriefs}
1403
- where ${juniorConversationBriefs.conversationId} = ${juniorConversations.conversationId}
1404
- and ${includePrivateBriefs ? sql`true` : eq(juniorDestinations.visibility, "public")}
1405
- and ${juniorConversationBriefs.version} = (
1406
- select max(${juniorConversationBriefs.version})
1407
- from ${juniorConversationBriefs}
1408
- where ${juniorConversationBriefs.conversationId} = ${juniorConversations.conversationId}
1409
- )
1410
- and to_tsvector('english', ${juniorConversationBriefs.searchText})
1411
- @@ websearch_to_tsquery('english', ${query})
1412
- )`) : void 0)).orderBy(desc(juniorConversations.lastActivityAt), asc(juniorConversations.conversationId)).limit(limit);
1413
- }
1414
- /** Decode a conversation row with the linked user name and identity-scoped provider fields. */
1415
- function conversationFromRow(row) {
1416
- const value = row.conversation;
1417
- const legacySessionSource = value.sessionSource === null ? void 0 : parseSessionSource(value.sessionSource);
1418
- if (value.sessionSource !== null && !legacySessionSource) throw new Error("Conversation legacy source_json is invalid");
1419
- const actorFullName = row.userDisplayName?.trim() ? row.userDisplayName : row.identityDisplayName;
1420
- const actor = row.identityProvider === "slack" ? {
1421
- platform: "slack",
1422
- ...row.identityEmail ? { email: row.identityEmail } : void 0,
1423
- ...actorFullName ? { fullName: actorFullName } : void 0,
1424
- ...row.identitySubjectId ? { slackUserId: row.identitySubjectId } : void 0,
1425
- ...row.identityHandle ? { slackUserName: row.identityHandle } : void 0,
1426
- ...row.identityTenantId ? { teamId: row.identityTenantId } : void 0
1427
- } : void 0;
1428
- const location = locationFromRow(value.location, row.location, legacySessionSource);
1429
- return {
1430
- schemaVersion: 1,
1431
- conversationId: value.conversationId,
1432
- createdAtMs: value.createdAt.getTime(),
1433
- lastActivityAtMs: value.lastActivityAt.getTime(),
1434
- updatedAtMs: value.updatedAt.getTime(),
1435
- execution: {
1436
- status: value.executionStatus,
1437
- ...value.runId ? { runId: value.runId } : void 0,
1438
- ...value.executionUpdatedAt ? { updatedAtMs: value.executionUpdatedAt.getTime() } : void 0
1439
- },
1440
- ...actor ? { actor } : void 0,
1441
- ...location ? { location } : void 0,
1442
- ...value.channelName ? { channelName: value.channelName } : void 0,
1443
- ...value.source ? { source: value.source } : void 0,
1444
- ...value.title ? { title: value.title } : void 0,
1445
- ...value.transcriptPurgedAt ? { transcriptPurgedAtMs: value.transcriptPurgedAt.getTime() } : void 0
1446
- };
1447
- }
1448
- /** Read one normalized conversation record directly from its SQL row. */
1449
- async function readConversationRecordFromSql(conversationId) {
1450
- const row = (await getDb().select({
1451
- conversation: conversationReadColumns(),
1452
- location: juniorDestinations,
1453
- identityDisplayName: juniorIdentities.displayName,
1454
- identityEmail: juniorIdentities.email,
1455
- identityHandle: juniorIdentities.handle,
1456
- identityProvider: juniorIdentities.provider,
1457
- identitySubjectId: juniorIdentities.providerSubjectId,
1458
- identityTenantId: juniorIdentities.providerTenantId,
1459
- userDisplayName: juniorUsers.displayName
1460
- }).from(juniorConversations).leftJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(eq(juniorConversations.conversationId, conversationId)).limit(1))[0];
1461
- return row ? {
1462
- conversation: conversationFromRow(row),
1463
- durationMs: row.conversation.durationMs,
1464
- ...row.location?.visibility === "public" ? { locationId: row.location.id } : void 0,
1465
- usage: row.conversation.usage,
1466
- rootConversationId: row.conversation.rootConversationId
1467
- } : void 0;
1468
- }
1469
- /** Prefer the authenticated viewer; fall back to actor email membership. */
1470
- function conversationFeedFilter(options) {
1471
- if (options.viewer) return {
1472
- kind: "viewer",
1473
- userId: options.viewer.id
1474
- };
1475
- if (options.actorEmail) return {
1476
- kind: "actorEmail",
1477
- email: options.actorEmail
1478
- };
1479
- }
1480
- /**
1481
- * Build a bounded dashboard feed. Prefer the viewer user when present; otherwise
1482
- * keep only roots linked to actorEmail before applying the limit. Membership is
1483
- * root-actor ownership or a materialized participant row for that person.
1484
- *
1485
- * `codeRepositoryId` replaces membership with every root linked to that
1486
- * repository's code changes. Private roots appear only for their members, and
1487
- * per-person archive state does not hide rows.
1488
- */
1489
- async function readConversationFeedFromSql(options = {}) {
1490
- const nowMs = Date.now();
1491
- const db = getDb();
1492
- const filter = options.codeRepositoryId ? void 0 : conversationFeedFilter(options);
1493
- const query = options.q?.trim().toLowerCase() || void 0;
1494
- const rows = await conversationRows(db, options.limit ?? CONVERSATION_FEED_LIMIT, query || options.codeRepositoryId ? "all" : options.status ?? "active", filter, query, filter?.kind === "viewer", options.codeRepositoryId ? {
1495
- id: options.codeRepositoryId,
1496
- ...options.viewer ? { viewerUserId: options.viewer.id } : void 0
1497
- } : void 0);
1498
- const conversations = rows.map((row) => conversationFromRow(row));
1499
- const conversationIds = conversations.map((conversation) => conversation.conversationId);
1500
- const [accessByConversation, archiveTimesByConversation, annotationsByConversation, auxiliaryCostsByRoot, metricsByRoot, participantsByConversation, teamDomainByTeamId, conversationWork, lastUserMessageAtByConversation, activityPreviewByConversation, automationTitleByConversation, firstHumanMessageTextByConversation] = await Promise.all([
1501
- readConversationAccessFromSql(db, conversationIds, options.viewer),
1502
- conversationArchiveTimes(db, conversationIds, filter?.kind === "viewer" ? { userId: filter.userId } : filter?.kind === "actorEmail" ? { email: filter.email } : void 0),
1503
- listConversationAnnotationsById(db, conversationIds),
1504
- readConversationAuxiliaryCostsFromSql(db, conversationIds),
1505
- readConversationTreeMetricsFromSql(db, conversationIds),
1506
- readConversationParticipants(db, conversationIds),
1507
- resolveSlackTeamDomains(conversations.flatMap((conversation) => conversation.location?.provider === "slack" ? [conversation.location.teamId] : [])),
1508
- listConversationWork(conversationIds),
1509
- readLastUserMessageAtByConversation(db, conversationIds),
1510
- readConversationActivityPreviews(db, conversationIds),
1511
- readAutomationTitlesByConversationId(conversations),
1512
- readFirstHumanMessageTexts(db, conversations)
1513
- ]);
1514
- const visibleActivityPreviews = await attachActivityPreviewMentions(getSqlExecutor(), new Map([...activityPreviewByConversation].filter(([conversationId]) => accessByConversation.get(conversationId)?.canViewPrivateContent)));
1515
- const sidebarAnnotationsByConversation = await listConversationSidebarAnnotations(conversationIds, new Map(conversationIds.map((conversationId) => [conversationId, accessByConversation.get(conversationId)?.canViewPrivateContent ? annotationsByConversation.get(conversationId) ?? [] : []])));
1516
- const assignedWork = new Set(conversationWork.assignedIds);
1517
- const unfinishedWork = new Set(conversationWork.unfinishedIds);
1518
- return {
1519
- conversations: conversations.map((conversation, index) => {
1520
- const row = rows[index];
1521
- const access = accessByConversation.get(conversation.conversationId);
1522
- const metrics = metricsByRoot.get(conversation.conversationId);
1523
- const auxiliaryCosts = auxiliaryCostsByRoot.get(conversation.conversationId);
1524
- const childAgentTotals = childAgentTotalsReport(metrics, auxiliaryCosts);
1525
- const archivedAtMs = archiveTimesByConversation.get(conversation.conversationId);
1526
- const summary = conversationSummaryFromStoredConversation({
1527
- ...archivedAtMs === void 0 ? void 0 : { archivedAtMs },
1528
- automationTitle: automationTitleByConversation.get(conversation.conversationId),
1529
- conversation,
1530
- access,
1531
- auxiliaryCosts: auxiliaryCosts?.total,
1532
- ...childAgentTotals ? { childAgentTotals } : void 0,
1533
- durationMs: metrics?.durationMs ?? row.conversation.durationMs,
1534
- firstHumanMessageText: firstHumanMessageTextByConversation.get(conversation.conversationId),
1535
- teamDomainByTeamId,
1536
- ...row.location?.visibility === "public" ? { locationId: row.location.id } : void 0,
1537
- usage: metrics?.usage ?? row.conversation.usage ?? void 0
1538
- });
1539
- const work = {
1540
- ...assignedWork.has(conversation.conversationId) ? { assignedWork: true } : void 0,
1541
- ...conversationWork.finishedAtById[conversation.conversationId] ? { finishedWorkAt: conversationWork.finishedAtById[conversation.conversationId] } : void 0,
1542
- ...unfinishedWork.has(conversation.conversationId) ? { unfinishedWork: true } : void 0
1543
- };
1544
- const annotations = access?.canViewPrivateContent ? annotationsByConversation.get(conversation.conversationId) ?? [] : [];
1545
- const activityPreview = visibleActivityPreviews.get(conversation.conversationId);
1546
- const participants = access?.canViewPrivateContent ? participantsByConversation.get(conversation.conversationId) : void 0;
1547
- return {
1548
- ...summary,
1549
- ...work,
1550
- ...participants && participants.length > 0 ? { participants } : void 0,
1551
- ...activityPreview ? { activityPreview } : void 0,
1552
- ...annotations.length > 0 ? { annotations } : void 0,
1553
- ...sidebarAnnotationsByConversation[conversation.conversationId] ? { sidebarAnnotations: sidebarAnnotationsByConversation[conversation.conversationId] } : void 0,
1554
- isPriority: isConversationPriority({
1555
- lastSeenAt: summary.lastSeenAt,
1556
- lastUserMessageAt: lastUserMessageAtByConversation.get(conversation.conversationId),
1557
- ...work
1558
- }, nowMs)
1559
- };
1560
- }),
1561
- generatedAt: new Date(nowMs).toISOString(),
1562
- source: "conversation_index"
1563
- };
1564
- }
1565
- /**
1566
- * Load a bounded feed with an optional viewer-primary linked-user filter.
1567
- * This filter is not an authorization boundary.
1568
- */
1569
- async function readConversationFeed(options = {}) {
1570
- return conversationFeedSchema.parse(await readConversationFeedFromSql(options));
1571
- }
1572
447
  // src/api/conversations/fork.ts
1573
448
  /** Fork a Conversation that the viewer can read into a new root they own. */
1574
449
  async function forkConversationForViewer(viewer, sourceConversationId, body) {
@@ -1846,6 +721,25 @@ async function cancelConversationPendingMessagesForViewer(viewer, conversationId
1846
721
  throwApiError(500, "Unable to cancel queued messages.", error);
1847
722
  }
1848
723
  }
724
+ // src/api/conversations/steer-pending-messages.ts
725
+ /**
726
+ * Steer the viewer's own queued web messages into the conversation now.
727
+ *
728
+ * Only the author can steer a message. Ids that are not the viewer's queued
729
+ * web messages, or that Junior already read, are left out of the result.
730
+ */
731
+ async function steerConversationPendingMessagesForViewer(viewer, conversationId, body) {
732
+ if (!await getConversationStore().get({ conversationId })) throwApiError(404, "Conversation not found.");
733
+ if (!(await readConversationAccessFromSql(getDb(), [conversationId], viewer)).get(conversationId)?.isParticipant) throwApiError(403, "Only conversation participants can steer queued messages.");
734
+ return {
735
+ conversationId,
736
+ steeredInboundMessageIds: (await steerWebPendingMessages({
737
+ authorId: webActorFromEmail(viewer.email).userId,
738
+ conversationId,
739
+ inboundMessageIds: body.inboundMessageIds
740
+ })).steeredInboundMessageIds
741
+ };
742
+ }
1849
743
  // src/api/conversations/pending-messages.ts
1850
744
  const slackMailboxAuthorSchema = z.object({
1851
745
  userId: z.string(),
@@ -2486,6 +1380,12 @@ function createConversationRoutes(options) {
2486
1380
  const viewer = context.get("viewer");
2487
1381
  return jsonResponse(conversationPendingMessagesReportSchema, await requireConversationPendingMessages(conversationId, { viewer }));
2488
1382
  });
1383
+ app.post("/:conversationId/pending-messages/steer", requireViewer, validateRequest("param", conversationParamsSchema, "Invalid route parameters."), validateRequest("json", steerConversationPendingMessagesBodySchema, "Invalid request body."), async (context) => {
1384
+ const viewer = context.get("viewer");
1385
+ const { conversationId } = context.req.valid("param");
1386
+ const body = context.req.valid("json");
1387
+ return jsonResponse(steerConversationPendingMessagesResponseSchema, await steerConversationPendingMessagesForViewer(viewer, conversationId, body));
1388
+ });
2489
1389
  app.delete("/:conversationId/pending-messages", requireViewer, validateRequest("param", conversationParamsSchema, "Invalid route parameters."), validateRequest("json", cancelConversationPendingMessagesBodySchema, "Invalid request body."), async (context) => {
2490
1390
  const viewer = context.get("viewer");
2491
1391
  const { conversationId } = context.req.valid("param");
@@ -3528,10 +2428,28 @@ function activitySixHours(hours, nowMs) {
3528
2428
  function statsItems(map) {
3529
2429
  return [...map.values()].sort((left, right) => right.conversations - left.conversations || right.durationMs - left.durationMs || left.label.localeCompare(right.label));
3530
2430
  }
3531
- /** Build the verified Slack actor predicate shared by People aggregate and recent-row queries. */
2431
+ /**
2432
+ * Build the verified person actor predicate shared by People aggregate and
2433
+ * recent-row queries.
2434
+ *
2435
+ * Any provider counts, so Slack and dashboard (web) Conversations roll up to
2436
+ * the same person. System identities never count.
2437
+ */
3532
2438
  function verifiedActorWhere(email) {
3533
2439
  const normalizedEmail = normalizeEmail$1(email);
3534
- return and(eq(juniorIdentities.provider, "slack"), eq(juniorIdentities.emailVerified, true), sql`${juniorUsers.primaryEmailNormalized} IS NOT NULL`, normalizedEmail ? eq(juniorUsers.primaryEmailNormalized, normalizedEmail) : void 0);
2440
+ return and(eq(juniorIdentities.kind, "user"), eq(juniorIdentities.emailVerified, true), sql`${juniorUsers.primaryEmailNormalized} IS NOT NULL`, normalizedEmail ? eq(juniorUsers.primaryEmailNormalized, normalizedEmail) : void 0);
2441
+ }
2442
+ /**
2443
+ * Read the Slack user fields for a grouped person row.
2444
+ *
2445
+ * Web identities use the email as the provider subject, so only Slack
2446
+ * identities can supply these fields.
2447
+ */
2448
+ function slackActorColumns() {
2449
+ return {
2450
+ slackUserId: sql`MAX(CASE WHEN ${juniorIdentities.provider} = 'slack' THEN ${juniorIdentities.providerSubjectId} END)`,
2451
+ slackUserName: sql`MAX(CASE WHEN ${juniorIdentities.provider} = 'slack' THEN ${juniorIdentities.handle} END)`
2452
+ };
3535
2453
  }
3536
2454
  /** Read only the recent conversation rows required by a People profile. */
3537
2455
  async function recentActorRows(email) {
@@ -3545,9 +2463,9 @@ async function recentActorRows(email) {
3545
2463
  executionStatus: juniorConversations.executionStatus,
3546
2464
  executionUpdatedAt: juniorConversations.executionUpdatedAt,
3547
2465
  fullName: juniorUsers.displayName,
3548
- handle: juniorIdentities.handle,
2466
+ handle: sql`CASE WHEN ${juniorIdentities.provider} = 'slack' THEN ${juniorIdentities.handle} END`,
3549
2467
  lastActivityAt: juniorConversations.lastActivityAt,
3550
- providerSubjectId: juniorIdentities.providerSubjectId,
2468
+ providerSubjectId: sql`CASE WHEN ${juniorIdentities.provider} = 'slack' THEN ${juniorIdentities.providerSubjectId} END`,
3551
2469
  source: juniorConversations.source,
3552
2470
  title: juniorConversations.title,
3553
2471
  updatedAt: juniorConversations.updatedAt,
@@ -3671,8 +2589,7 @@ async function readPeopleListFromSql() {
3671
2589
  getDb().select({
3672
2590
  email: juniorUsers.primaryEmailNormalized,
3673
2591
  fullName: juniorUsers.displayName,
3674
- slackUserId: sql`MAX(${juniorIdentities.providerSubjectId})`,
3675
- slackUserName: sql`MAX(${juniorIdentities.handle})`,
2592
+ ...slackActorColumns(),
3676
2593
  activeDays: conversationActiveDaysColumn(),
3677
2594
  ...peopleTreeAggregateColumns,
3678
2595
  ...conversationRangeColumns()
@@ -3777,7 +2694,7 @@ async function readPeoplePluginReports(args) {
3777
2694
  reports: [],
3778
2695
  source: "plugins"
3779
2696
  });
3780
- const { getPluginProfileReports } = await import("./credentialed-enF5Bf3y.js").then((n) => n.a);
2697
+ const { getPluginProfileReports } = await import("./credentialed-lukuD-cc.js").then((n) => n.a);
3781
2698
  return pluginOperationalReportFeedSchema.parse({
3782
2699
  generatedAt: new Date(nowMs).toISOString(),
3783
2700
  reports: await getPluginProfileReports({
@@ -3828,6 +2745,8 @@ function surfaceExpression() {
3828
2745
  return sql`CASE
3829
2746
  WHEN ${juniorConversations.source} IN ('api', 'scheduler', 'slack')
3830
2747
  THEN ${juniorConversations.source}
2748
+ -- Dashboard web conversations share the product api surface label.
2749
+ WHEN ${juniorConversations.source} = 'web' THEN 'api'
3831
2750
  WHEN ${juniorConversations.conversationId} LIKE 'slack:%' THEN 'slack'
3832
2751
  WHEN ${juniorConversations.conversationId} LIKE 'scheduler:%' THEN 'scheduler'
3833
2752
  WHEN ${juniorConversations.conversationId} LIKE 'api:%' THEN 'api'
@@ -3868,8 +2787,7 @@ async function readPeopleProfileFromSql(email, options = {}) {
3868
2787
  getDb().select({
3869
2788
  email: juniorUsers.primaryEmailNormalized,
3870
2789
  fullName: juniorUsers.displayName,
3871
- slackUserId: sql`MAX(${juniorIdentities.providerSubjectId})`,
3872
- slackUserName: sql`MAX(${juniorIdentities.handle})`,
2790
+ ...slackActorColumns(),
3873
2791
  activeDays: conversationActiveDaysColumn(),
3874
2792
  ...peopleTreeAggregateColumns
3875
2793
  }).from(juniorConversations).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).innerJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(where).groupBy(juniorUsers.primaryEmailNormalized, juniorUsers.displayName),
@@ -4517,12 +3435,18 @@ async function changeViewerAutomationLifecycle(user, id, input) {
4517
3435
  }
4518
3436
  // src/chat/automations/editor.ts
4519
3437
  /** Read-only editor support. Core owns schedules; plugins own Event choices. */
4520
- /** Return only Event types that can start a durable Automation. */
3438
+ /**
3439
+ * Return only Event types that can start a durable Automation, one entry for
3440
+ * each namespace, with the reach of each event.
3441
+ */
4521
3442
  function readAutomationEventCatalog() {
4522
- return Object.entries(automationEventCatalog(getEventCatalog())).flatMap(([namespace, registration]) => registration.resourceTypes.map((resource) => ({
3443
+ return Object.entries(automationEventCatalog(getEventCatalog())).map(([namespace, registration]) => ({
4523
3444
  namespace,
4524
- ...resource
4525
- })));
3445
+ displayName: registration.displayName ?? namespace,
3446
+ events: Object.fromEntries(Object.entries(registration.events).map(([eventType, declaration]) => [eventType, { reach: declaration.reach }])),
3447
+ anyResourceFields: registration.anyResourceFields,
3448
+ resourceTypes: registration.resourceTypes
3449
+ }));
4526
3450
  }
4527
3451
  /** Preview a creator's draft with the same clock and compiler used by saving. */
4528
3452
  async function previewAutomationSchedule(user, id, intent) {
@@ -4686,6 +3610,126 @@ function createAutomationRoutes() {
4686
3610
  });
4687
3611
  return app;
4688
3612
  }
3613
+ // src/api/user-customization/routes.ts
3614
+ /**
3615
+ * Create routes that read and replace the viewer's customization. They return
3616
+ * 404 unless the app enables the `user-customization` experiment.
3617
+ */
3618
+ function createUserCustomizationRoutes() {
3619
+ const app = new Hono();
3620
+ app.use("*", async (_context, next) => {
3621
+ if (!isExperimentalFeatureEnabled("user-customization")) throwApiError(404, "Resource not found.");
3622
+ await next();
3623
+ });
3624
+ app.get("/", requireViewer, async (context) => {
3625
+ const viewer = context.get("viewer");
3626
+ return jsonResponse(userCustomizationSchema, { customization: await readUserCustomization(viewer.id) ?? "" });
3627
+ });
3628
+ app.put("/", requireViewer, validateRequest("json", userCustomizationSchema, "Invalid request body."), async (context) => {
3629
+ const viewer = context.get("viewer");
3630
+ const { customization } = context.req.valid("json");
3631
+ if (!await saveUserCustomization(viewer.id, customization)) throwApiError(404, "User not found.");
3632
+ return jsonResponse(userCustomizationSchema, { customization });
3633
+ });
3634
+ return app;
3635
+ }
3636
+ // src/api/review-items/routes.ts
3637
+ const LIST_LIMIT = 100;
3638
+ const ERROR_STATUS = {
3639
+ conflict: 409,
3640
+ forbidden: 403,
3641
+ invalid: 400,
3642
+ not_found: 404
3643
+ };
3644
+ /** Build the API payload of one Review item for its viewer. */
3645
+ function reviewItemPayload(item, viewer) {
3646
+ return {
3647
+ addedByConversationId: item.addedByConversationId,
3648
+ assignedToEveryone: item.assignedToEveryone,
3649
+ assignees: item.assignees,
3650
+ body: item.body,
3651
+ canAct: canActOnReviewItem(item, viewer.id),
3652
+ canTake: canActOnReviewItem(item, viewer.id) && !isOnlyAssignee(item, viewer.id),
3653
+ conversationIds: item.conversationIds,
3654
+ createdAt: item.createdAt.toISOString(),
3655
+ id: item.id,
3656
+ kind: item.kind,
3657
+ status: item.status,
3658
+ title: item.title,
3659
+ updatedAt: item.updatedAt.toISOString(),
3660
+ visibility: item.visibility,
3661
+ ...item.answer ? { answer: item.answer } : void 0,
3662
+ ...item.automationId ? { automationId: item.automationId } : void 0,
3663
+ ...item.closeReason ? { closeReason: item.closeReason } : void 0,
3664
+ ...item.closedAt ? { closedAt: item.closedAt.toISOString() } : void 0,
3665
+ ...item.closedBy ? { closedBy: item.closedBy } : void 0,
3666
+ ...item.request ? { request: item.request } : void 0,
3667
+ ...item.subject ? { subject: item.subject } : void 0
3668
+ };
3669
+ }
3670
+ function actionErrorResponse(error) {
3671
+ if (error instanceof ReviewItemActionError) return jsonResponse(apiErrorSchema, { error: error.message }, { status: ERROR_STATUS[error.code] });
3672
+ throw error;
3673
+ }
3674
+ /** Create the authenticated review desk routes. */
3675
+ function createReviewItemRoutes(services) {
3676
+ const app = new Hono();
3677
+ app.get("/", requireViewer, validateRequest("query", reviewItemListQuerySchema, "Invalid review item query."), async (context) => {
3678
+ const viewer = context.get("viewer");
3679
+ const query = context.req.valid("query");
3680
+ const view = query.view ?? "all";
3681
+ const items = await searchReviewItems(getDb(), reviewItemReaderForUser(viewer), {
3682
+ limit: LIST_LIMIT,
3683
+ statuses: query.archived === "true" ? ["done", "dismissed"] : ["open"],
3684
+ ...view === "mine" ? { assignee: { userId: viewer.id } } : view === "everyone" ? { assignee: { everyone: true } } : void 0,
3685
+ ...query.conversationId ? { conversationId: query.conversationId } : void 0,
3686
+ ...query.q ? { query: query.q } : void 0
3687
+ });
3688
+ return jsonResponse(reviewItemListSchema, { items: items.map((item) => reviewItemPayload(item, viewer)) });
3689
+ });
3690
+ app.get("/count", requireViewer, async (context) => {
3691
+ const viewer = context.get("viewer");
3692
+ return jsonResponse(reviewItemCountSchema, { mine: await countReviewItems(getDb(), reviewItemReaderForUser(viewer), {
3693
+ assignee: { userId: viewer.id },
3694
+ statuses: ["open"]
3695
+ }) });
3696
+ });
3697
+ app.get("/waiting-conversations", requireViewer, async (context) => {
3698
+ const viewer = context.get("viewer");
3699
+ return jsonResponse(reviewItemWaitingConversationsSchema, { conversationIds: await listWaitingConversationIds(getDb(), viewer) });
3700
+ });
3701
+ app.get("/:id", requireViewer, validateRequest("param", reviewItemParamsSchema, "Invalid review item."), async (context) => {
3702
+ const viewer = context.get("viewer");
3703
+ try {
3704
+ const item = await readReviewItemForUser(viewer, context.req.valid("param").id);
3705
+ return jsonResponse(reviewItemSchema, reviewItemPayload(item, viewer));
3706
+ } catch (error) {
3707
+ return actionErrorResponse(error);
3708
+ }
3709
+ });
3710
+ app.post("/:id/actions", requireViewer, validateRequest("param", reviewItemParamsSchema, "Invalid review item."), validateRequest("json", reviewItemActionSchema, "Invalid action."), async (context) => {
3711
+ const viewer = context.get("viewer");
3712
+ const body = context.req.valid("json");
3713
+ const { id } = context.req.valid("param");
3714
+ try {
3715
+ const item = await actOnReviewItem({
3716
+ action: body.action,
3717
+ getSlackAdapter: getProductionSlackAdapter,
3718
+ id,
3719
+ queue: services.queue,
3720
+ user: viewer,
3721
+ ...body.action === "submit" ? { answer: {
3722
+ ...body.answer,
3723
+ requestId: id
3724
+ } } : void 0
3725
+ });
3726
+ return jsonResponse(reviewItemSchema, reviewItemPayload(item, viewer));
3727
+ } catch (error) {
3728
+ return actionErrorResponse(error);
3729
+ }
3730
+ });
3731
+ return app;
3732
+ }
4689
3733
  // src/api/workspaces/routes.ts
4690
3734
  function snapshotView(workspace) {
4691
3735
  const recorded = workspace.snapshot;
@@ -4810,7 +3854,7 @@ function readDescriptionText(home) {
4810
3854
  }
4811
3855
  /** Read discovered skill names for authenticated runtime diagnostics. */
4812
3856
  async function readSkillReports() {
4813
- const { discoverSkills } = await import("./skills-DzyGN7wd.js").then((n) => n.o);
3857
+ const { discoverSkills } = await import("./skills-CfAnfImD.js").then((n) => n.o);
4814
3858
  const skills = await discoverSkills();
4815
3859
  return skillReportsSchema.parse(skills.map((skill) => ({
4816
3860
  name: skill.name,
@@ -4819,7 +3863,7 @@ async function readSkillReports() {
4819
3863
  }
4820
3864
  /** Read safe configured plugin metadata for authenticated runtime diagnostics. */
4821
3865
  async function readPlugins() {
4822
- const { pluginCatalogRuntime } = await import("./catalog-runtime-DAJjAbSx.js").then((n) => n.t);
3866
+ const { pluginCatalogRuntime } = await import("./catalog-runtime-BD9GSpc6.js").then((n) => n.t);
4823
3867
  return pluginsSchema.parse(pluginCatalogRuntime.getProviders().map((plugin) => ({
4824
3868
  configKeys: plugin.manifest.configKeys ?? [],
4825
3869
  description: plugin.manifest.description,
@@ -4831,7 +3875,7 @@ async function readPlugins() {
4831
3875
  async function readRuntimeInfoReport() {
4832
3876
  const [{ homeDir }, { pluginCatalogRuntime }, plugins, skills] = await Promise.all([
4833
3877
  import("./package-discovery-DJBompdd.js").then((n) => n.r),
4834
- import("./catalog-runtime-DAJjAbSx.js").then((n) => n.t),
3878
+ import("./catalog-runtime-BD9GSpc6.js").then((n) => n.t),
4835
3879
  readPlugins(),
4836
3880
  readSkillReports()
4837
3881
  ]);
@@ -4848,7 +3892,7 @@ async function readRuntimeInfoReport() {
4848
3892
  /** Read sanitized operational summaries contributed by plugins. */
4849
3893
  async function readPluginOperationalReportFeed() {
4850
3894
  const nowMs = Date.now();
4851
- const { getPluginOperationalReports } = await import("./credentialed-enF5Bf3y.js").then((n) => n.a);
3895
+ const { getPluginOperationalReports } = await import("./credentialed-lukuD-cc.js").then((n) => n.a);
4852
3896
  return pluginOperationalReportFeedSchema.parse({
4853
3897
  source: "plugins",
4854
3898
  generatedAt: new Date(nowMs).toISOString(),
@@ -4865,10 +3909,12 @@ function createJuniorApi(options = {}) {
4865
3909
  app.get("/api/skills", async () => jsonResponse(skillReportsSchema, await readSkillReports()));
4866
3910
  app.get("/api/plugin-reports", async () => jsonResponse(pluginOperationalReportFeedSchema, await readPluginOperationalReportFeed()));
4867
3911
  app.get("/api/stats", async () => jsonResponse(statsReportSchema, await readStatsReport()));
3912
+ const queue = options.conversationWorkQueue ?? getVercelConversationWorkQueue();
4868
3913
  app.route("/api/conversations", createConversationRoutes({
4869
3914
  attachmentStorage: getAttachmentStorage(),
4870
- queue: options.conversationWorkQueue ?? getVercelConversationWorkQueue()
3915
+ queue
4871
3916
  }));
3917
+ app.route("/api/review-items", createReviewItemRoutes({ queue }));
4872
3918
  app.route("/api/code", createCodeRoutes());
4873
3919
  app.route("/api/connections", createConnectionRoutes());
4874
3920
  app.route("/api/personal-tokens", createPersonalTokenRoutes());
@@ -4876,6 +3922,7 @@ function createJuniorApi(options = {}) {
4876
3922
  app.route("/api/people", createPeopleRoutes());
4877
3923
  app.route("/api/locations", createLocationRoutes());
4878
3924
  app.route("/api/user-pages", createUserPageRoutes());
3925
+ app.route("/api/user-customization", createUserCustomizationRoutes());
4879
3926
  app.route("/api/automations", createAutomationRoutes());
4880
3927
  app.route("/api/workspaces", createWorkspaceRoutes());
4881
3928
  app.notFound(() => jsonResponse(apiErrorSchema, { error: "Resource not found." }, { status: 404 }));
@@ -4889,4 +3936,4 @@ function createJuniorApi(options = {}) {
4889
3936
  });
4890
3937
  return app;
4891
3938
  }
4892
- export { authenticatePersonalToken, createJuniorApi, jsonResponse, resolveViewerUser, updateViewerDisplayName };
3939
+ export { authenticatePersonalToken, createJuniorApi, isAllowedSignInEmail, jsonResponse, resolveViewerUser, updateViewerDisplayName };