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