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