@sentry/junior 0.266.0 → 0.268.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{actor-tlxzqaRe.js → actor-CP9K2Idu.js} +9 -1
- package/dist/{adapter-eRFA_naf.js → adapter-04AcD2SO.js} +5 -7
- package/dist/{admin-tILpP51p.js → admin-DIU1QyG3.js} +1 -1
- package/dist/{agent-invocations-CxKldAeB.js → agent-invocations-C1RNOCs8.js} +2 -0
- package/dist/{agent-runner-BtYK7bht.js → agent-runner-CAYfFmU7.js} +2636 -601
- package/dist/api/schema.d.ts +2 -5454
- package/dist/api/schema.js +6 -5
- package/dist/api.d.ts +6 -1
- package/dist/api.js +255 -1208
- package/dist/{app-Csbi9dW7.d.ts → app-z6Uuyq3j.d.ts} +23 -4
- package/dist/app.d.ts +2 -2
- package/dist/app.js +1869 -378
- package/dist/{automation-BKpY9LPa.js → automation-CmeFBSEh.js} +42 -14
- package/dist/{briefs-DgfWMU7e.js → briefs-DQbV2jNr.js} +4 -4
- package/dist/{catalog-runtime-DAJjAbSx.js → catalog-runtime-BD9GSpc6.js} +1 -1
- package/dist/cli/chat.js +15 -15
- package/dist/cli/check.js +3 -3
- package/dist/cli/init.js +0 -1
- package/dist/cli/main.js +3 -3
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +4 -4
- package/dist/cli/upgrade.d.ts +1 -1
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-B0YZfxNy.js → client-C1yDYjoS.js} +2 -2
- package/dist/{config-C0UwYqDK.js → config-DrTeF80T.js} +32 -10
- package/dist/{connections-Bwu3VORi.js → connections-D46f8vvi.js} +12 -12
- package/dist/{conversation-DF4rUcjb.js → conversation-DSo310Ps.js} +8 -1
- package/dist/{conversation-privacy-CY7JVjxn.js → conversation-privacy-CxlOCiUo.js} +1 -1
- package/dist/{credential-sync-VGAMUTGp.js → credential-sync-sm9fegLY.js} +29 -34
- package/dist/{credentialed-enF5Bf3y.js → credentialed-lukuD-cc.js} +2933 -1026
- package/dist/db-BU4LQD8s.d.ts +6 -0
- package/dist/{executor-TnWbvL09.js → executor-4vhvsMZJ.js} +83 -6
- package/dist/instrumentation.js +6 -2
- package/dist/{assistant-reply-UyzAfZQo.js → interruption-marker-CkncDT8d.js} +8 -2
- package/dist/{job-queue-DBpHmKnd.js → job-queue-In9QKz-I.js} +128 -3
- package/dist/{web-input-B-bMjD1V.js → list-BkgnXEEB.js} +1611 -817
- package/dist/{logging-Ce0mXZ8J.js → logging-DLSpkSSr.js} +11 -3
- package/dist/{message-VZ3DFpGb.js → message-BObyg59s.js} +4 -4
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-callback-server-CpqWLjVY.js → oauth-callback-server-B-WbSdoH.js} +2 -2
- package/dist/{prompt-BcCGlmg2.js → prompt-BW9jFsqZ.js} +1 -1
- package/dist/{registry-1BEIPdbT.js → registry-BgecKva0.js} +2 -2
- package/dist/{web-message-id-CJSUiksE.js → reporting-window-DVCD_10C.js} +1 -10
- package/dist/{resolve-BSHHkEGE.js → resolve-Cpa4Bu1O.js} +3 -3
- package/dist/{result-CwOpzqdh.js → result-D3TSmZZ8.js} +9 -9
- package/dist/{runner-CEaX04kz.js → runner-DS5OKhEp.js} +11 -11
- package/dist/{sandbox-egress-signals-BwtGHwqa.js → sandbox-egress-signals-CnDjY5rx.js} +1 -1
- package/dist/{schema-RsaBHxRH.js → schema-0050HFnB.js} +82 -5
- package/dist/schema-ZngqcOWM.d.ts +5813 -0
- package/dist/{service-account-6wC76eBZ.js → service-account-CKV2WTY7.js} +3 -3
- package/dist/{service-account-Dg4lIbHm.js → service-account-DGjnDw9y.js} +3 -3
- package/dist/{session-stop-tZOw9AJD.js → session-stop-Dam1EI7B.js} +527 -134
- package/dist/{skills-DzyGN7wd.js → skills-CfAnfImD.js} +2 -2
- package/dist/{spawn-6rokq7l_.js → spawn-cmq9p4Ly.js} +2 -2
- package/dist/{store-DcKkzrdt.js → store-C5PN02QO.js} +4 -4
- package/dist/{store-CG0VJyye.d.ts → store-xMuuXRE0.d.ts} +23 -9
- package/dist/store-zPEoJPBB.js +295 -0
- package/dist/{task-queue-Bg4RS2Gy.js → task-queue-_mf28dSu.js} +10 -5
- package/dist/{task-runner-8N5LIkSg.js → task-runner-DMI2prDO.js} +25 -14
- package/dist/{tasks-DDT-J3Rf.js → tasks-SB9z-VTU.js} +1551 -54
- package/dist/{thread-state-cprEaxgl.js → thread-state-BbbPh0Sn.js} +8 -18
- package/dist/{turn-failure-response-C8sMdsdX.js → turn-failure-response-BKKmnMB5.js} +11 -16
- package/dist/{turn-lifecycle-DaBVoz7h.js → turn-lifecycle-oP0kVeGz.js} +1 -1
- package/dist/{validation-C2lllinq.js → validation--JsAGLwc.js} +1 -1
- package/dist/{version-oaLfxxnf.js → version-jHwJp4hC.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-BpE6Z3qf.js → viewer-CpXTGlep.js} +329 -230
- package/dist/web-message-id-DlHNANtd.js +132 -0
- package/dist/{work-VX1SHWha.js → work-3N-GHY9m.js} +10 -10
- package/dist/{worker-hook-y_Y.js → worker-CCdbcItd.js} +30 -19
- package/migrations/0064_any_resource_event_triggers.sql +2 -0
- package/migrations/0065_user_customization.sql +1 -0
- package/migrations/0066_review_items.sql +53 -0
- package/migrations/0067_review_item_dedupe_tenant.sql +2 -0
- package/migrations/0068_review_item_slack_cards.sql +10 -0
- package/migrations/meta/0064_snapshot.json +3505 -0
- package/migrations/meta/0065_snapshot.json +3511 -0
- package/migrations/meta/0066_snapshot.json +4099 -0
- package/migrations/meta/0067_snapshot.json +3921 -0
- package/migrations/meta/0068_snapshot.json +4176 -0
- package/migrations/meta/_journal.json +36 -1
- package/package.json +19 -4
- package/dist/db-BnRqQ7G5.d.ts +0 -5
- package/dist/images-BKQqSi5W.js +0 -64
- package/dist/store-FALy-JQb.js +0 -1424
- package/dist/turn-session-routing-a9CHX-w6.js +0 -76
|
@@ -1,41 +1,42 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as completeText, p as escapeXml, r as completeDecision, y as isProviderRetryError } from "./client-
|
|
3
|
-
import { c as getSlackClientSecret, i as getChatConfig, l as getSlackSigningSecret, n as botConfig, o as getSlackBotToken, s as getSlackClientId, v as normalizeSlackEmojiName, y as createS3AttachmentStorage } from "./config-
|
|
4
|
-
import { B as
|
|
1
|
+
import { A as resolveConversationRouting, At as upsertConversationMessage, Bt as clearPendingAuth, Ct as estimateConversationContextTokens, Dt as normalizeConversationText, E as sameDefinitionValue, Et as markConversationMessage, Gt as saveTurnCheckpoint, Ht as wasPendingAuthStopped, In as persistWithRetry, It as AuthorizationFlowDisabledError, Jt as failTurnRecord, K as commitAcceptedReply, Kt as abandonTurnRecord, Mt as PluginCredentialFailureError, N as automationEventCatalog, Nn as buildTurnErrorResponse, Ot as recordDeliveredAssistantMessage, Q as loadTurnProjection, S as automationDefinition, St as createConversationMemoryService, Tt as isHumanConversationMessage, V as registeredEventTriggerProblems, Wt as loadTurnCheckpoint, Xn as trimTrailingAssistantMessages, Xt as listTurnSummaries, Y as loadConversationProjection, Yt as getTurnRecord, Z as loadProjection, Zt as recordTurnSummary, _ as parseEventAutomationRow, ar as getThreadStopDecision, bt as appendThreadContextMessages, c as withScheduledAutomationLock, ct as recordSubscribedReplyRoute, er as buildDeterministicAssistantMessageId, f as deleteEventAutomation, ft as hydrateConversationMessages, gt as resolveConversationTitle, h as listDeletedEventAutomationsCreatedBy, hr as piMessageFromHistoryItem, ht as persistConversationMessageSummaries, i as readScheduledAutomation, ir as getSubscribedReplyPreflightDecision, it as recordAuthorizationCompleted, j as resolveTurnSessionRouting, kt as turnHasReply, o as saveScheduledAutomationInLock, pr as configValueSchema, pt as persistConversationMessages, q as commitMessages, qt as completeTurnRecord, r as parseScheduledAutomationRow, rr as decideSubscribedThreadReply, tr as buildDeterministicTurnId, ur as isExperimentalFeatureEnabled, vt as fallbackShortTitle, w as listAutomationVersions, wt as getConversationMessageSlackTs, xt as buildConversationContext } from "./tasks-SB9z-VTU.js";
|
|
2
|
+
import { a as completeText, p as escapeXml, r as completeDecision, y as isProviderRetryError } from "./client-C1yDYjoS.js";
|
|
3
|
+
import { c as getSlackClientSecret, i as getChatConfig, l as getSlackSigningSecret, n as botConfig, o as getSlackBotToken, s as getSlackClientId, v as normalizeSlackEmojiName, y as createS3AttachmentStorage } from "./config-DrTeF80T.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-DLSpkSSr.js";
|
|
5
|
+
import { i as getActiveSpan, r as continueTrace } from "./sentry-MDkYAVPV.js";
|
|
5
6
|
import { o as modelIdForProfile, r as defaultModelId } from "./model-profile-Bg8EoCKD.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers } from "./agent-invocations-
|
|
9
|
-
import {
|
|
10
|
-
import { p as instructionProvenanceFor } from "./history-C84mlAwO.js";
|
|
11
|
-
import { d as actorLogContext, f as credentialContextForActor } from "./registry-
|
|
12
|
-
import { A as
|
|
13
|
-
import { $t as
|
|
14
|
-
import { a as
|
|
15
|
-
import {
|
|
16
|
-
import { a as EVENT_SYSTEM_ACTOR, o as isEventConversationMessage } from "./prompt-
|
|
17
|
-
import { B as ConversationTurnBoundaryError, G as isCooperativeTurnYieldError, H as TurnInputCommitLostError, J as markTurnClosed, K as isTurnInputCommitLostError, U as TurnInputDeferredError, V as CooperativeTurnYieldError, W as getConversationTurnBoundaryError, X as markTurnFailed, Y as markTurnCompleted, Z as startActiveTurn, q as isTurnInputDeferredError } from "./store-
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { a as
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { a as
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { t as bindMessageAgent } from "./message-
|
|
32
|
-
import { t as bindSpawnAgent } from "./spawn-
|
|
33
|
-
import { a as persistAuthPauseTurnState, c as getTurnUserSlackMessageTs,
|
|
34
|
-
import { r as storeInputImages } from "./images-BKQqSi5W.js";
|
|
7
|
+
import { f as parseSlackTeamId, i as isActorUserId, m as parseSlackMessageTs, n as createSlackActor, o as isUserActor, r as createSlackResumeActor, s as parseActorUserId, t as createActor, u as parseSlackChannelId } from "./actor-CP9K2Idu.js";
|
|
8
|
+
import { L as hasAgentTurnUsage, M as parseSessionSource, N as addAgentTurnUsage, V as requireSlackDestination, _ as withConversationEventLock, d as getConversationEventStore, h as getSqlExecutor, m as getDb, p as getConversationStore, v as loadConversationSlackTenantIds, x as locationFromRow, z as createSlackDestination } from "./viewer-CpXTGlep.js";
|
|
9
|
+
import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, o as conversationReadColumns, s as juniorConversations } from "./agent-invocations-C1RNOCs8.js";
|
|
10
|
+
import { S as juniorCodeChanges, d as juniorAutomations, g as juniorConversationBriefs, l as juniorAutomationRuns, y as juniorConversationEvents } from "./executor-4vhvsMZJ.js";
|
|
11
|
+
import { d as contextProvenance, p as instructionProvenanceFor } from "./history-C84mlAwO.js";
|
|
12
|
+
import { d as actorLogContext, f as credentialContextForActor } from "./registry-BgecKva0.js";
|
|
13
|
+
import { A as getConversationInfo, D as readSlackRawMessageContext, E as parseSlackThreadId, Ft as buildSteeringPiMessage, H as syncReviewItemSlackCards, It as withoutSavedSteeringMessages, L as describeEventAutomationTrigger, Mt as conversationSubtreeColumns, O as resolveSlackChannelIdFromMessage, R as effectiveTaskOutcomes, Ut as shouldEmitDevAgentTrace, _t as recordWebConversationActivity, at as getReviewItem, b as renderBlockText, dt as sendReviewItemMessages, et as runWithSlackInstallation, gt as createConversationId, i as buildAuthPauseResponse, j as listThreadReplies, jt as conversationSubtree, k as resolveSlackChannelIdFromThreadId, lt as takeReviewItem, n as executeAgentRun, nt as canReadReviewItem, p as latestProgressStatus, r as createContextCompactor, rt as closeReviewItem, st as reviewItemReaderForUser, t as createAgentRunner, vt as webActorFromEmail, wt as storeAttachment, y as renderAttachmentText, yt as resolveConversationDestination, zt as RetryableDeliveryError } from "./agent-runner-CAYfFmU7.js";
|
|
14
|
+
import { $n as recordSlackAttachment, $t as verifyTimerWatchCredentialSubject, Bn as conversationArchivedForUser, Gn as viewerConversationMembership, Hn as conversationIsMemberRoot, In as createPluginLogger, J as truncateStatusText, K as normalizeSlackStatusText, Ln as readConversationAccessFromSql, Lt as cancelSubscriptions, Qn as readAttachmentVisionSummary, Rn as conversationArchiveTimes, Un as conversationNotArchivedForEmail, Vn as conversationHasParticipantUser, Wn as conversationNotArchivedForUser, Xn as getSlackFileId, Y as addReactionToMessage, cr as createPluginCache, en as SlackActionError, in as getSlackClient, ir as listConversationAnnotationsById, ln as withSlackRetries, n as createUserTokenStore, nt as removeReactionFromMessage, on as isDmChannel, p as getPlugins, pn as JUNIOR_THREAD_STATE_TTL_MS, rn as getHeaderString, sn as normalizeSlackConversationId, sr as lookupSlackUser, tn as downloadPrivateSlackFile, zn as conversationArchivedForEmail } from "./credentialed-lukuD-cc.js";
|
|
15
|
+
import { a as MUTATION_LOCK_TTL_MS, c as withLock, i as getStateAdapter, o as acquireActiveLock } from "./adapter-04AcD2SO.js";
|
|
16
|
+
import { W as readMessageAttachments, g as conversationFeedSchema, q as isVisionImageMediaType } from "./conversation-DSo310Ps.js";
|
|
17
|
+
import { a as EVENT_SYSTEM_ACTOR, o as isEventConversationMessage } from "./prompt-BW9jFsqZ.js";
|
|
18
|
+
import { B as ConversationTurnBoundaryError, G as isCooperativeTurnYieldError, H as TurnInputCommitLostError, J as markTurnClosed, K as isTurnInputCommitLostError, U as TurnInputDeferredError, V as CooperativeTurnYieldError, W as getConversationTurnBoundaryError, X as markTurnFailed, Y as markTurnCompleted, Z as startActiveTurn, q as isTurnInputDeferredError } from "./store-C5PN02QO.js";
|
|
19
|
+
import { r as getAssistantReplyText } from "./interruption-marker-CkncDT8d.js";
|
|
20
|
+
import { C as maybePostAutomatedTurnLimitNotice, D as recordFinishedTurnForAutomatedLimit, I as runDispatchOutcome, M as finishedRunReply, N as postsRunNotices, T as sendSlackReply, c as isEventMailboxMetadata, d as getEventCatalog, i as skipInputBeforeConversationStop, j as deliversFinalText, l as renderEventBatchText, s as eventBatchMessageEvents, u as timerWatchCredentialSubject, w as resolveSlackReplyTarget, x as joinMailboxText } from "./session-stop-Dam1EI7B.js";
|
|
21
|
+
import { a as isEmptyUserInputAnswer, o as normalizeUserInputAnswer } from "./web-message-id-DlHNANtd.js";
|
|
22
|
+
import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline } from "./job-queue-In9QKz-I.js";
|
|
23
|
+
import { D as stopConversationWork, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, r as appendAndEnqueueInboundMessage, w as requestConversationWork } from "./store-zPEoJPBB.js";
|
|
24
|
+
import { j as eventAutomationTriggerSchema, l as automationListQuerySchema } from "./automation-CmeFBSEh.js";
|
|
25
|
+
import { p as healthReportSchema, s as sumUtcHoursIntoSixHours } from "./reporting-window-DVCD_10C.js";
|
|
26
|
+
import { A as readAutomationExecutions, D as readAutomationExecutionStatusDays, E as readAutomationExecutionHours, M as readAutomationTitlesByConversationId, O as readAutomationExecutionStatusHours, S as emptyAutomationRunWindows, T as readAutomationExecutionDays, _ as markDispatchBlocked, a as claimDispatchMailboxAppend, b as markDispatchRunning, c as getDispatchConversationId, d as getDispatchRecord, f as getDispatchTurnId, g as markDispatchAwaitingResume, j as readAutomationRuns, k as readAutomationExecutionSummaries, l as getDispatchIdForConversation, m as isTerminalDispatchStatus, n as scheduleSessionCompletedPluginTasks, o as confirmDispatchMailboxAppend, t as processPluginTask, u as getDispatchInputMessageId, v as markDispatchCompleted, w as readAutomationExecutionByConversationId, y as markDispatchFailed } from "./task-runner-DMI2prDO.js";
|
|
27
|
+
import { a as withAgentInvocationResultActors, c as resolveMailboxTurnWork, d as legacyWebMailboxMetadataSchema, i as startAgentInvocationsForChildInput, l as sourceFromTurnInput, o as getAuthPausedConversationTurnId, s as getEventBatchDelayMs, t as enqueueAgentInvocationResult, u as turnInputFactsFromConversationMessage } from "./result-D3TSmZZ8.js";
|
|
28
|
+
import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-oP0kVeGz.js";
|
|
29
|
+
import { i as resolveAgentInvocationId, o as AgentRunError, s as executeTurn, t as createAgentInvocationWorker } from "./work-3N-GHY9m.js";
|
|
30
|
+
import { a as persistThreadStateById, i as persistThreadRuntimeState, n as getPersistedSandboxState, o as coerceThreadConversationState, r as getPersistedThreadState, t as getLocationConfigurationService } from "./thread-state-BbbPh0Sn.js";
|
|
31
|
+
import { c as deleteWebAuthorization, d as createPausedTurns, f as getPausedTurnRequest, h as getTurnAuthorization, m as wakePausedTurn, s as createWebAuthorization } from "./connections-D46f8vvi.js";
|
|
32
|
+
import { t as bindMessageAgent } from "./message-BObyg59s.js";
|
|
33
|
+
import { t as bindSpawnAgent } from "./spawn-cmq9p4Ly.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-BKKmnMB5.js";
|
|
35
35
|
import { z } from "zod";
|
|
36
36
|
import { createHash } from "node:crypto";
|
|
37
|
-
import { and, asc, count, desc, eq, exists, inArray, ne, or, sql } from "drizzle-orm";
|
|
38
|
-
import {
|
|
37
|
+
import { and, asc, count, desc, eq, exists, inArray, isNotNull, isNull, ne, notInArray, or, sql } from "drizzle-orm";
|
|
38
|
+
import { conversationSidebarAnnotationSchema, createSlackSource, objectTypeSchema } from "@sentry/junior-plugin-api";
|
|
39
|
+
import { alias } from "drizzle-orm/pg-core";
|
|
39
40
|
import { del, get, put } from "@vercel/blob";
|
|
40
41
|
import { Message, ThreadImpl, stringifyMarkdown } from "chat";
|
|
41
42
|
import { SlackFormatConverter, createSlackAdapter } from "@chat-adapter/slack";
|
|
@@ -107,8 +108,7 @@ async function enqueueAgentDispatch(dispatch, options) {
|
|
|
107
108
|
destination: claimedDispatch.destination,
|
|
108
109
|
nowMs,
|
|
109
110
|
source: "plugin",
|
|
110
|
-
sessionSource: claimedDispatch.source
|
|
111
|
-
visibility: claimedDispatch.destinationVisibility
|
|
111
|
+
sessionSource: claimedDispatch.source
|
|
112
112
|
});
|
|
113
113
|
await appendAndEnqueueInboundMessage({
|
|
114
114
|
message: buildAgentDispatchInboundMessage(claimedDispatch, nowMs),
|
|
@@ -213,7 +213,6 @@ async function persistBlockedDispatchTurn(dispatch, error) {
|
|
|
213
213
|
actor: dispatch.actor,
|
|
214
214
|
conversationId,
|
|
215
215
|
destination: dispatch.destination,
|
|
216
|
-
destinationVisibility: dispatch.destinationVisibility,
|
|
217
216
|
dispatchId: dispatch.id,
|
|
218
217
|
dispatchOutcome: "blocked",
|
|
219
218
|
turnId: sessionId,
|
|
@@ -380,207 +379,140 @@ function createAttachmentStorage(config) {
|
|
|
380
379
|
default: throw new Error(`Unsupported attachment storage provider: ${String(config.provider)}`);
|
|
381
380
|
}
|
|
382
381
|
}
|
|
383
|
-
// src/chat/
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
custom_fields: [{
|
|
423
|
-
key: "trigger",
|
|
424
|
-
label: "When",
|
|
425
|
-
type: "string",
|
|
426
|
-
value: trigger,
|
|
427
|
-
long: true
|
|
428
|
-
}, ...warning ? [{
|
|
429
|
-
key: "warning",
|
|
430
|
-
label: "Needs attention",
|
|
431
|
-
type: "string",
|
|
432
|
-
value: warning,
|
|
433
|
-
long: true
|
|
434
|
-
}] : []]
|
|
435
|
-
}
|
|
436
|
-
}
|
|
382
|
+
// src/chat/review-items/actions.ts
|
|
383
|
+
/** An expected failure of a person action, for the API to report. */
|
|
384
|
+
var ReviewItemActionError = class extends Error {
|
|
385
|
+
code;
|
|
386
|
+
constructor(code, message) {
|
|
387
|
+
super(message);
|
|
388
|
+
this.name = "ReviewItemActionError";
|
|
389
|
+
this.code = code;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* Return whether a User can act on an open Review item: an assigned User,
|
|
394
|
+
* anyone for an item assigned to Everyone, and anyone for an item with no
|
|
395
|
+
* assignees. The caller checks that the User can see the item.
|
|
396
|
+
*/
|
|
397
|
+
function canActOnReviewItem(item, userId) {
|
|
398
|
+
if (item.status !== "open") return false;
|
|
399
|
+
if (item.assignedToEveryone) return true;
|
|
400
|
+
if (item.assignees.length === 0) return true;
|
|
401
|
+
return item.assignees.some((assignee) => assignee.userId === userId);
|
|
402
|
+
}
|
|
403
|
+
/** Return whether a User is already the only assignee, so take does nothing. */
|
|
404
|
+
function isOnlyAssignee(item, userId) {
|
|
405
|
+
return !item.assignedToEveryone && item.assignees.length === 1 && item.assignees[0]?.userId === userId;
|
|
406
|
+
}
|
|
407
|
+
/** Read one Review item that a User can see, or fail as not found. */
|
|
408
|
+
async function readReviewItemForUser(user, id) {
|
|
409
|
+
const item = await getReviewItem(getDb(), id);
|
|
410
|
+
if (!item || !canReadReviewItem(item, reviewItemReaderForUser(user))) throw new ReviewItemActionError("not_found", "Review item not found.");
|
|
411
|
+
return item;
|
|
412
|
+
}
|
|
413
|
+
function closedStatus(action) {
|
|
414
|
+
return action === "dismiss" ? "dismissed" : "done";
|
|
415
|
+
}
|
|
416
|
+
/** The change that one person action makes, for messages and notes. */
|
|
417
|
+
function reviewItemChange(action, user) {
|
|
418
|
+
if (action === "take") return {
|
|
419
|
+
kind: "taken",
|
|
420
|
+
user
|
|
437
421
|
};
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const text = value?.replace(/\r\n/g, "\n").trim() ?? "";
|
|
442
|
-
const preview = text.split("\n").slice(0, 6).join("\n").slice(0, 500);
|
|
443
|
-
return preview.length < text.length ? `${preview.slice(0, 499).trimEnd()}…` : preview;
|
|
444
|
-
}
|
|
445
|
-
/** Render a compact Task or Item preview, or the saved facts for its detail panel. */
|
|
446
|
-
function renderSlackObjectCard(card, conversationId, surface = "preview") {
|
|
447
|
-
if (card.objectType === "automation" && card.plugin === "junior") return renderSlackAutomationCard({
|
|
448
|
-
id: card.key,
|
|
449
|
-
title: card.title,
|
|
450
|
-
url: card.url,
|
|
451
|
-
trigger: card.trigger ?? "",
|
|
452
|
-
warning: card.warning ?? null
|
|
453
|
-
});
|
|
454
|
-
const presentation = objectPresentation(card);
|
|
455
|
-
const type = card.displayType ?? presentation.label;
|
|
456
|
-
const iconUrl = getDashboardObjectIconLink(presentation.icon);
|
|
457
|
-
const title = card.title.slice(0, 160);
|
|
458
|
-
const details = surface === "details";
|
|
459
|
-
const deployment = card.objectType === "deployment" || card.objectType === "item" && card.facts?.type === "deployment";
|
|
460
|
-
const calendarEvent = card.objectType === "calendar_event";
|
|
461
|
-
const status = details || deployment || calendarEvent ? card.status : void 0;
|
|
462
|
-
const warning = details || deployment ? card.warning : void 0;
|
|
463
|
-
const facts = objectFactFields(card.facts).filter((field) => details || calendarEvent || deployment && field.key === "environment");
|
|
464
|
-
const description = details ? card.description?.trim() ?? "" : calendarEvent ? "" : descriptionPreview(card.description);
|
|
465
|
-
const text = [
|
|
466
|
-
escapeSlackMrkdwnText(type),
|
|
467
|
-
card.url ? formatSlackLink(card.url, title) : escapeSlackMrkdwnText(title),
|
|
468
|
-
escapeSlackMrkdwnText(card.label),
|
|
469
|
-
status ? escapeSlackMrkdwnText(status) : null,
|
|
470
|
-
description ? escapeSlackMrkdwnText(description) : null,
|
|
471
|
-
warning ? escapeSlackMrkdwnText(`Needs attention: ${warning}`) : null,
|
|
472
|
-
...facts.map((field) => escapeSlackMrkdwnText(`${field.label}: ${field.value}`))
|
|
473
|
-
].filter(Boolean).join("\n");
|
|
474
|
-
if (!card.url) return {
|
|
475
|
-
entity: null,
|
|
476
|
-
text
|
|
422
|
+
if (action === "submit") return {
|
|
423
|
+
kind: "submitted",
|
|
424
|
+
user
|
|
477
425
|
};
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
display_id: card.label,
|
|
482
|
-
display_type: type,
|
|
483
|
-
product_icon: iconUrl ? {
|
|
484
|
-
url: iconUrl,
|
|
485
|
-
alt_text: `${type}${status ? `: ${status}` : ""}`
|
|
486
|
-
} : void 0,
|
|
487
|
-
product_name: card.plugin
|
|
426
|
+
if (action === "complete") return {
|
|
427
|
+
kind: "completed",
|
|
428
|
+
user
|
|
488
429
|
};
|
|
489
|
-
const customFields = [
|
|
490
|
-
...description ? [{
|
|
491
|
-
key: "description",
|
|
492
|
-
label: "Description",
|
|
493
|
-
type: "string",
|
|
494
|
-
value: description,
|
|
495
|
-
long: true,
|
|
496
|
-
format: "markdown"
|
|
497
|
-
}] : [],
|
|
498
|
-
...facts.map((field) => ({
|
|
499
|
-
...field,
|
|
500
|
-
type: "string"
|
|
501
|
-
})),
|
|
502
|
-
...warning ? [{
|
|
503
|
-
key: "warning",
|
|
504
|
-
label: "Needs attention",
|
|
505
|
-
type: "string",
|
|
506
|
-
value: warning
|
|
507
|
-
}] : []
|
|
508
|
-
];
|
|
509
430
|
return {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
...task ? {
|
|
513
|
-
entity_type: "slack#/entities/task",
|
|
514
|
-
entity_payload: {
|
|
515
|
-
attributes,
|
|
516
|
-
display_order: [...status ? ["status"] : [], ...customFields.map((field) => field.key)],
|
|
517
|
-
fields: status ? { status: { value: status } } : void 0,
|
|
518
|
-
custom_fields: customFields
|
|
519
|
-
}
|
|
520
|
-
} : {
|
|
521
|
-
entity_type: "slack#/entities/item",
|
|
522
|
-
entity_payload: {
|
|
523
|
-
attributes,
|
|
524
|
-
display_order: [...status ? ["status"] : [], ...customFields.map((field) => field.key)],
|
|
525
|
-
custom_fields: [...status ? [{
|
|
526
|
-
key: "status",
|
|
527
|
-
label: "Status",
|
|
528
|
-
type: "string",
|
|
529
|
-
value: status
|
|
530
|
-
}] : [], ...customFields]
|
|
531
|
-
}
|
|
532
|
-
},
|
|
533
|
-
external_ref: {
|
|
534
|
-
id: Buffer.from(JSON.stringify([
|
|
535
|
-
conversationId,
|
|
536
|
-
card.plugin,
|
|
537
|
-
card.key
|
|
538
|
-
]), "utf8").toString("base64url"),
|
|
539
|
-
type: "annotation"
|
|
540
|
-
},
|
|
541
|
-
url: card.url
|
|
542
|
-
}
|
|
431
|
+
kind: "dismissed",
|
|
432
|
+
user
|
|
543
433
|
};
|
|
544
434
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const state = await getConnectedState$1(args.state);
|
|
556
|
-
await args.adapter.initialize({ getState: () => state });
|
|
557
|
-
if (args.adapter.defaultBotTokenProvider && !args.adapter.botUserId) throw new Error("Slack adapter initialized without a resolved bot user id");
|
|
558
|
-
initializedAdapters.add(args.adapter);
|
|
559
|
-
}
|
|
560
|
-
/** Verify a Slack request using the adapter's configured signing secret. */
|
|
561
|
-
function verifySlackSignature(args) {
|
|
562
|
-
const verifySignature = args.adapter.verifySignature;
|
|
563
|
-
if (!verifySignature) throw new Error("Slack adapter does not expose signature verification");
|
|
564
|
-
return verifySignature.call(args.adapter, args.body, args.request.headers.get("x-slack-request-timestamp"), args.request.headers.get("x-slack-signature"));
|
|
565
|
-
}
|
|
566
|
-
/** Run Slack work with the installation token that matches the inbound event. */
|
|
567
|
-
async function runWithSlackInstallation(args) {
|
|
568
|
-
await ensureSlackAdapterInitialized({
|
|
569
|
-
adapter: args.adapter,
|
|
570
|
-
state: args.state
|
|
435
|
+
/** Return whether a closed item is the result of this same action. */
|
|
436
|
+
function isSameClose(item, action, user) {
|
|
437
|
+
return item.closedBy?.userId === user.id && item.status === closedStatus(action) && action === "submit" === Boolean(item.answer);
|
|
438
|
+
}
|
|
439
|
+
function submittedAnswer(item, answer) {
|
|
440
|
+
if (item.kind !== "action" || !item.request) throw new ReviewItemActionError("invalid", "Only an action item takes an answer.");
|
|
441
|
+
const normalized = normalizeUserInputAnswer(item.request, {
|
|
442
|
+
other: answer?.other,
|
|
443
|
+
requestId: item.id,
|
|
444
|
+
values: answer?.values ?? {}
|
|
571
445
|
});
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
446
|
+
if (isEmptyUserInputAnswer(normalized)) throw new ReviewItemActionError("invalid", "The answer is empty.");
|
|
447
|
+
return normalized;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Do one person action on a Review item and tell its linked Conversations.
|
|
451
|
+
* The first submit, complete, or dismiss closes the item for every
|
|
452
|
+
* assignee. A retry of the same close sends any missing messages again and
|
|
453
|
+
* succeeds, so a client can retry after a failure. With `firstCloseOnly`,
|
|
454
|
+
* only the call that closes the item succeeds, and every other call gets a
|
|
455
|
+
* conflict. Use it when the caller adds its own input for the action, such
|
|
456
|
+
* as a Slack card click, so one action adds one Turn. `exceptConversationId`
|
|
457
|
+
* is the Conversation where the person acted. It gets the person's input
|
|
458
|
+
* from its own surface, so it gets no message here. Slack cards of the item
|
|
459
|
+
* change in place; see `syncReviewItemSlackCards` for `getSlackAdapter`.
|
|
460
|
+
*/
|
|
461
|
+
async function actOnReviewItem(args) {
|
|
462
|
+
const { action, user } = args;
|
|
463
|
+
const nowMs = args.nowMs ?? Date.now();
|
|
464
|
+
const db = getDb();
|
|
465
|
+
const item = await readReviewItemForUser(user, args.id);
|
|
466
|
+
if (action === "take") {
|
|
467
|
+
if (item.status !== "open") throw new ReviewItemActionError("conflict", "Review item is closed.");
|
|
468
|
+
if (!canActOnReviewItem(item, user.id)) throw new ReviewItemActionError("forbidden", "Only an assignee can take this item.");
|
|
469
|
+
if (!isOnlyAssignee(item, user.id) && !await takeReviewItem(db, {
|
|
470
|
+
id: item.id,
|
|
471
|
+
nowMs,
|
|
472
|
+
userId: user.id
|
|
473
|
+
})) throw new ReviewItemActionError("conflict", "Review item is closed.");
|
|
474
|
+
const taken = await readReviewItemForUser(user, item.id);
|
|
475
|
+
await syncReviewItemSlackCards({
|
|
476
|
+
getSlackAdapter: args.getSlackAdapter,
|
|
477
|
+
itemId: taken.id
|
|
478
|
+
});
|
|
479
|
+
await sendReviewItemMessages({
|
|
480
|
+
change: reviewItemChange(action, user),
|
|
481
|
+
exceptConversationId: args.exceptConversationId,
|
|
482
|
+
item: taken,
|
|
483
|
+
nowMs,
|
|
484
|
+
queue: args.queue
|
|
485
|
+
});
|
|
486
|
+
return taken;
|
|
487
|
+
}
|
|
488
|
+
let closed = item;
|
|
489
|
+
let closedNow = false;
|
|
490
|
+
if (item.status === "open") {
|
|
491
|
+
if (!canActOnReviewItem(item, user.id)) throw new ReviewItemActionError("forbidden", "Only an assignee can act on this item.");
|
|
492
|
+
if (action === "complete" && item.kind === "action") throw new ReviewItemActionError("invalid", "An action item needs an answer. Submit it or dismiss it.");
|
|
493
|
+
const answer = action === "submit" ? submittedAnswer(item, args.answer) : void 0;
|
|
494
|
+
closedNow = await closeReviewItem(db, {
|
|
495
|
+
closedByUserId: user.id,
|
|
496
|
+
id: item.id,
|
|
497
|
+
nowMs,
|
|
498
|
+
status: closedStatus(action),
|
|
499
|
+
...answer ? { answer } : void 0
|
|
500
|
+
});
|
|
501
|
+
closed = await readReviewItemForUser(user, item.id);
|
|
502
|
+
}
|
|
503
|
+
if (args.firstCloseOnly ? !closedNow : !isSameClose(closed, action, user)) throw new ReviewItemActionError("conflict", "Review item is closed.");
|
|
504
|
+
await syncReviewItemSlackCards({
|
|
505
|
+
getSlackAdapter: args.getSlackAdapter,
|
|
506
|
+
itemId: closed.id
|
|
507
|
+
});
|
|
508
|
+
await sendReviewItemMessages({
|
|
509
|
+
change: reviewItemChange(action, user),
|
|
510
|
+
exceptConversationId: args.exceptConversationId,
|
|
511
|
+
item: closed,
|
|
512
|
+
nowMs,
|
|
513
|
+
queue: args.queue
|
|
514
|
+
});
|
|
515
|
+
return closed;
|
|
584
516
|
}
|
|
585
517
|
// src/chat/slack/errors.ts
|
|
586
518
|
/** Extract Slack's stable API error code from Web API errors. */
|
|
@@ -850,6 +782,36 @@ async function startProcessingReactionForMessage(args) {
|
|
|
850
782
|
};
|
|
851
783
|
}
|
|
852
784
|
// src/chat/providers/slack/turn-input-telemetry.ts
|
|
785
|
+
const SENTRY_TRACE_PATTERN = /^([0-9a-f]{32})-([0-9a-f]{16})(?:-([01]))?$/;
|
|
786
|
+
function spanLinks({ sentryTrace }) {
|
|
787
|
+
const [, traceId, spanId, sampled] = SENTRY_TRACE_PATTERN.exec(sentryTrace) ?? [];
|
|
788
|
+
return traceId && spanId ? [{ context: {
|
|
789
|
+
traceId,
|
|
790
|
+
spanId,
|
|
791
|
+
traceFlags: sampled === "1" ? 1 : 0
|
|
792
|
+
} }] : [];
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Run a `chat.turn` span in the trace of the first trace context.
|
|
796
|
+
*
|
|
797
|
+
* The span links to the other trace contexts and to the span that was active,
|
|
798
|
+
* such as the worker span. Without a trace context, the span is a child of the
|
|
799
|
+
* active span. A malformed trace context starts a new trace.
|
|
800
|
+
*/
|
|
801
|
+
async function withTurnSpan(context, input, callback) {
|
|
802
|
+
const [parent, ...others] = input?.traceContexts ?? [];
|
|
803
|
+
if (!parent) return await withSpan("chat.turn", "chat.turn", context, callback);
|
|
804
|
+
const activeSpan = getActiveSpan();
|
|
805
|
+
const links = [...others.flatMap(spanLinks), ...activeSpan ? [{ context: activeSpan.spanContext() }] : []];
|
|
806
|
+
const { baggage, sentryTrace } = parent;
|
|
807
|
+
return await continueTrace({
|
|
808
|
+
baggage,
|
|
809
|
+
sentryTrace
|
|
810
|
+
}, () => withSpan("chat.turn", "chat.turn", context, async (setSpanAttributes) => {
|
|
811
|
+
getActiveSpan()?.addLinks(links);
|
|
812
|
+
return await callback(setSpanAttributes);
|
|
813
|
+
}));
|
|
814
|
+
}
|
|
853
815
|
/**
|
|
854
816
|
* Build the `chat.turn` timing attributes for the oldest input message.
|
|
855
817
|
*
|
|
@@ -1010,10 +972,8 @@ function createSlackTurnRuntime(deps) {
|
|
|
1010
972
|
return true;
|
|
1011
973
|
};
|
|
1012
974
|
const failConversationTurn = async (args) => {
|
|
1013
|
-
const conversationId = getThreadId(args.thread, args.message) ?? getRunId(args.thread, args.message);
|
|
1014
|
-
if (!conversationId) return;
|
|
1015
975
|
await turnLifecycle.fail({
|
|
1016
|
-
conversationId,
|
|
976
|
+
conversationId: args.conversationId,
|
|
1017
977
|
createdAtMs: deps.now(),
|
|
1018
978
|
eventId: args.eventId,
|
|
1019
979
|
failureCode: args.failureCode,
|
|
@@ -1058,7 +1018,15 @@ function createSlackTurnRuntime(deps) {
|
|
|
1058
1018
|
};
|
|
1059
1019
|
const postFallbackErrorReplyWithLogging = async (args) => {
|
|
1060
1020
|
try {
|
|
1061
|
-
|
|
1021
|
+
const channelId = getChannelId(args.thread, args.message);
|
|
1022
|
+
if (!channelId) throw new Error("Slack fallback error reply requires a channel id");
|
|
1023
|
+
const threadTs = getThreadTs(getThreadId(args.thread, args.message));
|
|
1024
|
+
await sendSlackReply({
|
|
1025
|
+
channelId,
|
|
1026
|
+
conversationId: args.conversationId,
|
|
1027
|
+
text: buildTurnErrorResponse(args.error, args.eventId),
|
|
1028
|
+
...threadTs ? { threadTs } : void 0
|
|
1029
|
+
});
|
|
1062
1030
|
} catch (postError) {
|
|
1063
1031
|
logException(postError, args.postFailureEventName, {
|
|
1064
1032
|
"app.slack.reply_stage": "error_fallback_post",
|
|
@@ -1114,14 +1082,14 @@ function createSlackTurnRuntime(deps) {
|
|
|
1114
1082
|
const completedAtMs = deps.now();
|
|
1115
1083
|
logSkippedSubscribedDecision(args);
|
|
1116
1084
|
if (args.preparedState) await deps.onSubscribedMessageSkipped({
|
|
1117
|
-
|
|
1085
|
+
conversationId: args.conversationId,
|
|
1118
1086
|
message: args.message,
|
|
1119
1087
|
decision: args.decision,
|
|
1120
1088
|
preparedState: args.preparedState,
|
|
1121
1089
|
completedAtMs
|
|
1122
1090
|
});
|
|
1123
1091
|
else await deps.recordSkippedSubscribedTurn({
|
|
1124
|
-
|
|
1092
|
+
conversationId: args.conversationId,
|
|
1125
1093
|
message: args.message,
|
|
1126
1094
|
decision: args.decision,
|
|
1127
1095
|
text: args.text,
|
|
@@ -1169,7 +1137,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1169
1137
|
});
|
|
1170
1138
|
logSkippedSubscribedDecision(skipped);
|
|
1171
1139
|
await deps.recordSkippedSteeringMessage({
|
|
1172
|
-
|
|
1140
|
+
conversationId: args.conversationId,
|
|
1173
1141
|
message: skipped.message,
|
|
1174
1142
|
decision: skipped.decision,
|
|
1175
1143
|
text: skipped.text
|
|
@@ -1183,7 +1151,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1183
1151
|
while (args.pending.length > 0) {
|
|
1184
1152
|
const skipped = args.pending.shift();
|
|
1185
1153
|
await deps.recordSkippedSteeringMessage({
|
|
1186
|
-
|
|
1154
|
+
conversationId: args.conversationId,
|
|
1187
1155
|
message: skipped.message,
|
|
1188
1156
|
decision: skipped.decision,
|
|
1189
1157
|
text: skipped.text
|
|
@@ -1197,6 +1165,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1197
1165
|
};
|
|
1198
1166
|
return {
|
|
1199
1167
|
async handleNewMention(thread, message, hooks) {
|
|
1168
|
+
const conversationId = hooks.conversationId ?? thread.id;
|
|
1200
1169
|
const processingReactions = createProcessingReactionTracker(thread);
|
|
1201
1170
|
let processingReaction;
|
|
1202
1171
|
const skippedSteeringMessages = [];
|
|
@@ -1205,7 +1174,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1205
1174
|
const onTurnCompleted = async () => {
|
|
1206
1175
|
completed = true;
|
|
1207
1176
|
await flushSkippedSteeringMessagesBestEffort({
|
|
1208
|
-
|
|
1177
|
+
conversationId,
|
|
1209
1178
|
pending: skippedSteeringMessages,
|
|
1210
1179
|
context: logContext({
|
|
1211
1180
|
threadId: getThreadId(thread, message),
|
|
@@ -1233,7 +1202,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1233
1202
|
}
|
|
1234
1203
|
processingReaction = await processingReactions.start(context, message);
|
|
1235
1204
|
const toolInvocationHook = createToolInvocationHook(processingReaction, hooks);
|
|
1236
|
-
await
|
|
1205
|
+
await withTurnSpan(context, hooks.telemetry, async (set) => {
|
|
1237
1206
|
const turnStartedAtMs = deps.now();
|
|
1238
1207
|
if (hooks.telemetry) set(getTurnInputSpanAttributes(hooks.telemetry, turnStartedAtMs));
|
|
1239
1208
|
const queuedMessages = getQueuedMessages(hooks.messageContext, {
|
|
@@ -1259,6 +1228,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1259
1228
|
},
|
|
1260
1229
|
onSkipped: (skipped) => handleSkippedSteeringMessages({
|
|
1261
1230
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
1231
|
+
conversationId,
|
|
1262
1232
|
pending: skippedSteeringMessages,
|
|
1263
1233
|
skipped,
|
|
1264
1234
|
thread
|
|
@@ -1272,7 +1242,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1272
1242
|
await deps.executeSlackTurn(thread, message, {
|
|
1273
1243
|
explicitMention: true,
|
|
1274
1244
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
1275
|
-
conversationId
|
|
1245
|
+
conversationId,
|
|
1276
1246
|
destination: hooks.destination,
|
|
1277
1247
|
queuedMessages,
|
|
1278
1248
|
ack,
|
|
@@ -1309,17 +1279,19 @@ function createSlackTurnRuntime(deps) {
|
|
|
1309
1279
|
let lifecycleError;
|
|
1310
1280
|
try {
|
|
1311
1281
|
await failConversationTurn({
|
|
1282
|
+
conversationId,
|
|
1312
1283
|
eventId,
|
|
1313
1284
|
failureCode,
|
|
1314
|
-
message
|
|
1315
|
-
thread
|
|
1285
|
+
message
|
|
1316
1286
|
});
|
|
1317
1287
|
} catch (error) {
|
|
1318
1288
|
lifecycleError = error;
|
|
1319
1289
|
}
|
|
1320
1290
|
await hooks.beforeFirstResponsePost?.();
|
|
1321
1291
|
await postFallbackErrorReplyWithLogging({
|
|
1292
|
+
conversationId,
|
|
1322
1293
|
thread,
|
|
1294
|
+
message,
|
|
1323
1295
|
eventId,
|
|
1324
1296
|
error: failureCause,
|
|
1325
1297
|
postFailureEventName: "mention.handler.failure_reply_post.failed"
|
|
@@ -1329,7 +1301,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1329
1301
|
if (completed) await processingReactions.completeAll();
|
|
1330
1302
|
else {
|
|
1331
1303
|
await flushSkippedSteeringMessagesBestEffort({
|
|
1332
|
-
|
|
1304
|
+
conversationId,
|
|
1333
1305
|
pending: skippedSteeringMessages,
|
|
1334
1306
|
context: logContext({
|
|
1335
1307
|
threadId: getThreadId(thread, message),
|
|
@@ -1344,6 +1316,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1344
1316
|
}
|
|
1345
1317
|
},
|
|
1346
1318
|
async handleSubscribedMessage(thread, message, hooks) {
|
|
1319
|
+
const conversationId = hooks.conversationId ?? thread.id;
|
|
1347
1320
|
const processingReactions = createProcessingReactionTracker(thread);
|
|
1348
1321
|
let processingReaction;
|
|
1349
1322
|
const skippedSteeringMessages = [];
|
|
@@ -1352,7 +1325,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1352
1325
|
const onTurnCompleted = async () => {
|
|
1353
1326
|
completed = true;
|
|
1354
1327
|
await flushSkippedSteeringMessagesBestEffort({
|
|
1355
|
-
|
|
1328
|
+
conversationId,
|
|
1356
1329
|
pending: skippedSteeringMessages,
|
|
1357
1330
|
context: logContext({
|
|
1358
1331
|
threadId: getThreadId(thread, message),
|
|
@@ -1375,7 +1348,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1375
1348
|
channelId,
|
|
1376
1349
|
runId
|
|
1377
1350
|
});
|
|
1378
|
-
await
|
|
1351
|
+
await withTurnSpan(turnContext, hooks.telemetry, async (set) => {
|
|
1379
1352
|
const turnStartedAtMs = deps.now();
|
|
1380
1353
|
if (hooks.telemetry) set(getTurnInputSpanAttributes(hooks.telemetry, turnStartedAtMs));
|
|
1381
1354
|
const content = parseContent(message);
|
|
@@ -1388,6 +1361,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1388
1361
|
userText: replaceTopLevelText(content, strippedUserText)
|
|
1389
1362
|
};
|
|
1390
1363
|
const threadContext = {
|
|
1364
|
+
conversationId,
|
|
1391
1365
|
threadId,
|
|
1392
1366
|
actorId,
|
|
1393
1367
|
channelId,
|
|
@@ -1420,7 +1394,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1420
1394
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost
|
|
1421
1395
|
});
|
|
1422
1396
|
await skipSubscribedMessage({
|
|
1423
|
-
|
|
1397
|
+
conversationId,
|
|
1424
1398
|
message,
|
|
1425
1399
|
decision,
|
|
1426
1400
|
context: threadContext,
|
|
@@ -1438,7 +1412,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1438
1412
|
if (preflightDecision && !preflightDecision.shouldReply) {
|
|
1439
1413
|
const reason = preflightDecision.reasonDetail ? `${preflightDecision.reason}:${preflightDecision.reasonDetail}` : preflightDecision.reason;
|
|
1440
1414
|
await skipSubscribedMessage({
|
|
1441
|
-
|
|
1415
|
+
conversationId,
|
|
1442
1416
|
message,
|
|
1443
1417
|
decision: {
|
|
1444
1418
|
shouldReply: false,
|
|
@@ -1464,7 +1438,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1464
1438
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost
|
|
1465
1439
|
});
|
|
1466
1440
|
await skipSubscribedMessage({
|
|
1467
|
-
|
|
1441
|
+
conversationId,
|
|
1468
1442
|
message,
|
|
1469
1443
|
decision,
|
|
1470
1444
|
context: threadContext,
|
|
@@ -1483,7 +1457,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1483
1457
|
queuedMessages
|
|
1484
1458
|
});
|
|
1485
1459
|
await deps.persistPreparedState({
|
|
1486
|
-
|
|
1460
|
+
conversationId,
|
|
1487
1461
|
preparedState
|
|
1488
1462
|
});
|
|
1489
1463
|
const decision = await deps.decideSubscribedReply({
|
|
@@ -1500,7 +1474,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1500
1474
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost
|
|
1501
1475
|
})) {
|
|
1502
1476
|
await skipSubscribedMessage({
|
|
1503
|
-
|
|
1477
|
+
conversationId,
|
|
1504
1478
|
message,
|
|
1505
1479
|
decision,
|
|
1506
1480
|
context: threadContext,
|
|
@@ -1512,7 +1486,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1512
1486
|
}
|
|
1513
1487
|
if (!decision.shouldReply) {
|
|
1514
1488
|
await skipSubscribedMessage({
|
|
1515
|
-
|
|
1489
|
+
conversationId,
|
|
1516
1490
|
message,
|
|
1517
1491
|
decision,
|
|
1518
1492
|
context: threadContext,
|
|
@@ -1531,6 +1505,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1531
1505
|
},
|
|
1532
1506
|
onSkipped: (skipped) => handleSkippedSteeringMessages({
|
|
1533
1507
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
1508
|
+
conversationId,
|
|
1534
1509
|
pending: skippedSteeringMessages,
|
|
1535
1510
|
skipped,
|
|
1536
1511
|
thread
|
|
@@ -1556,7 +1531,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1556
1531
|
const toolInvocationHook = createToolInvocationHook(processingReaction, hooks);
|
|
1557
1532
|
await deps.executeSlackTurn(thread, message, {
|
|
1558
1533
|
explicitMention: Boolean(message.isMention),
|
|
1559
|
-
conversationId
|
|
1534
|
+
conversationId,
|
|
1560
1535
|
destination: hooks.destination,
|
|
1561
1536
|
preparedState,
|
|
1562
1537
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
@@ -1595,17 +1570,19 @@ function createSlackTurnRuntime(deps) {
|
|
|
1595
1570
|
let lifecycleError;
|
|
1596
1571
|
try {
|
|
1597
1572
|
await failConversationTurn({
|
|
1573
|
+
conversationId,
|
|
1598
1574
|
eventId,
|
|
1599
1575
|
failureCode,
|
|
1600
|
-
message
|
|
1601
|
-
thread
|
|
1576
|
+
message
|
|
1602
1577
|
});
|
|
1603
1578
|
} catch (error) {
|
|
1604
1579
|
lifecycleError = error;
|
|
1605
1580
|
}
|
|
1606
1581
|
await hooks.beforeFirstResponsePost?.();
|
|
1607
1582
|
await postFallbackErrorReplyWithLogging({
|
|
1583
|
+
conversationId,
|
|
1608
1584
|
thread,
|
|
1585
|
+
message,
|
|
1609
1586
|
eventId,
|
|
1610
1587
|
error: failureCause,
|
|
1611
1588
|
postFailureEventName: "subscribed_message.handler.failure_reply_post.failed"
|
|
@@ -1615,7 +1592,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1615
1592
|
if (completed) await processingReactions.completeAll();
|
|
1616
1593
|
else {
|
|
1617
1594
|
await flushSkippedSteeringMessagesBestEffort({
|
|
1618
|
-
|
|
1595
|
+
conversationId,
|
|
1619
1596
|
pending: skippedSteeringMessages,
|
|
1620
1597
|
context: logContext({
|
|
1621
1598
|
threadId: getThreadId(thread, message),
|
|
@@ -2166,7 +2143,8 @@ function createSlackConversationWorker(options) {
|
|
|
2166
2143
|
const telemetry = {
|
|
2167
2144
|
count: records.length,
|
|
2168
2145
|
oldestCreatedAtMs: oldestRecord.createdAtMs,
|
|
2169
|
-
oldestReceivedAtMs: oldestRecord.receivedAtMs
|
|
2146
|
+
oldestReceivedAtMs: oldestRecord.receivedAtMs,
|
|
2147
|
+
traceContexts: records.flatMap((record) => record.traceContext ?? [])
|
|
2170
2148
|
};
|
|
2171
2149
|
const latestMetadata = parseSlackMetadata(latestRecord.input.metadata);
|
|
2172
2150
|
if (!latestMetadata) throw new Error("Latest conversation mailbox record is not Slack metadata");
|
|
@@ -2213,7 +2191,7 @@ function createSlackConversationWorker(options) {
|
|
|
2213
2191
|
};
|
|
2214
2192
|
const drainSteeringMessages = async (accept) => {
|
|
2215
2193
|
await context.attempt.drain(async (pendingRecords) => {
|
|
2216
|
-
const candidates = pendingRecords.map((record) => ({
|
|
2194
|
+
const candidates = pendingRecords.filter((record) => record.source === "slack").map((record) => ({
|
|
2217
2195
|
inboundMessageId: record.inboundMessageId,
|
|
2218
2196
|
message: restoreMessage({
|
|
2219
2197
|
adapter,
|
|
@@ -2263,7 +2241,7 @@ function createSlackConversationWorker(options) {
|
|
|
2263
2241
|
return { status: "completed" };
|
|
2264
2242
|
};
|
|
2265
2243
|
}
|
|
2266
|
-
/** Serialize a Slack message into the
|
|
2244
|
+
/** Serialize a Slack message and the active trace into the durable mailbox. */
|
|
2267
2245
|
function buildSlackInboundMessage(args) {
|
|
2268
2246
|
const authorId = requireSlackAuthorId(args.message);
|
|
2269
2247
|
const destination = createSlackDestination({
|
|
@@ -2296,7 +2274,8 @@ function buildSlackInboundMessage(args) {
|
|
|
2296
2274
|
thread: serializedThreadSchema.parse(args.thread.toJSON()),
|
|
2297
2275
|
message: args.message.toJSON()
|
|
2298
2276
|
}
|
|
2299
|
-
}
|
|
2277
|
+
},
|
|
2278
|
+
traceContext: getActiveTraceContext()
|
|
2300
2279
|
};
|
|
2301
2280
|
}
|
|
2302
2281
|
// src/chat/slack/message/identity.ts
|
|
@@ -2784,6 +2763,57 @@ function recordSkippedConversationMessage(args) {
|
|
|
2784
2763
|
}
|
|
2785
2764
|
});
|
|
2786
2765
|
}
|
|
2766
|
+
// src/chat/sign-in/allowlist.ts
|
|
2767
|
+
/**
|
|
2768
|
+
* Domains that may use Junior. Slack authors must have a Slack profile email
|
|
2769
|
+
* on one of them, and dashboard Google sign-ins must come from a Google
|
|
2770
|
+
* Workspace account whose `hd` claim is one of them. Slack and the dashboard
|
|
2771
|
+
* share one list.
|
|
2772
|
+
*/
|
|
2773
|
+
/** Read a list env var: a JSON string array or comma-separated values. */
|
|
2774
|
+
function readEnvList(name) {
|
|
2775
|
+
const value = process.env[name]?.trim();
|
|
2776
|
+
if (!value) return void 0;
|
|
2777
|
+
if (!value.startsWith("[")) return value.split(",");
|
|
2778
|
+
let parsed;
|
|
2779
|
+
try {
|
|
2780
|
+
parsed = JSON.parse(value);
|
|
2781
|
+
} catch (error) {
|
|
2782
|
+
throw new Error(`${name} must be a JSON string array`, { cause: error });
|
|
2783
|
+
}
|
|
2784
|
+
const list = z.array(z.string()).safeParse(parsed);
|
|
2785
|
+
if (!list.success) throw new Error(`${name} must be a JSON string array`);
|
|
2786
|
+
return list.data;
|
|
2787
|
+
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Resolve the allowed domains: the `createApp` option when set, else
|
|
2790
|
+
* `JUNIOR_ALLOWED_DOMAINS`. An empty list allows every domain.
|
|
2791
|
+
*/
|
|
2792
|
+
function resolveAllowedDomains(allowedDomains) {
|
|
2793
|
+
return [...new Set((allowedDomains ?? readEnvList("JUNIOR_ALLOWED_DOMAINS") ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean))];
|
|
2794
|
+
}
|
|
2795
|
+
/**
|
|
2796
|
+
* Fail startup on the removed Google-specific names. Ignoring them would
|
|
2797
|
+
* leave the list empty, which lets every Slack workspace member in.
|
|
2798
|
+
*/
|
|
2799
|
+
function assertNoLegacyAllowedDomains(dashboardOptionKeys) {
|
|
2800
|
+
if (dashboardOptionKeys.includes("allowedGoogleDomains")) throw new Error("dashboard.allowedGoogleDomains was removed; set createApp({ allowedDomains }) instead");
|
|
2801
|
+
if (process.env.JUNIOR_DASHBOARD_GOOGLE_DOMAINS?.trim()) throw new Error("JUNIOR_DASHBOARD_GOOGLE_DOMAINS was removed; set JUNIOR_ALLOWED_DOMAINS instead");
|
|
2802
|
+
}
|
|
2803
|
+
/** Return whether a verified domain may use Junior. An empty list allows all. */
|
|
2804
|
+
function isAllowedDomain(domain, allowedDomains) {
|
|
2805
|
+
if (allowedDomains.length === 0) return true;
|
|
2806
|
+
return Boolean(domain && allowedDomains.includes(domain.toLowerCase()));
|
|
2807
|
+
}
|
|
2808
|
+
/**
|
|
2809
|
+
* Return whether a verified sign-in email is on an allowed domain. Sign-in
|
|
2810
|
+
* needs a configured list, so an empty list allows no email.
|
|
2811
|
+
*/
|
|
2812
|
+
function isAllowedSignInEmail(email, allowedDomains) {
|
|
2813
|
+
const normalized = email.trim().toLowerCase();
|
|
2814
|
+
const separator = normalized.lastIndexOf("@");
|
|
2815
|
+
return allowedDomains.length > 0 && separator > 0 && isAllowedDomain(normalized.slice(separator + 1), allowedDomains);
|
|
2816
|
+
}
|
|
2787
2817
|
// src/chat/automations/collection.ts
|
|
2788
2818
|
/**
|
|
2789
2819
|
* The web collection applies access before counts, filter options, and paging.
|
|
@@ -2799,18 +2829,6 @@ function viewerAutomationCollection(user) {
|
|
|
2799
2829
|
const owned = inArray(automation.creatorIdentityId, user.identities.map((identity) => identity.id));
|
|
2800
2830
|
const publicDestination = sql`coalesce(${destination.visibility} = 'public', false)`;
|
|
2801
2831
|
const publicToViewer = and(inArray(automation.teamId, teamIds), publicDestination, ne(automation.status, "completed"));
|
|
2802
|
-
const trigger = sql`${automation.record}->'trigger'`;
|
|
2803
|
-
const match = sql`coalesce(${trigger}->'match', '{}'::jsonb)`;
|
|
2804
|
-
const json = (value) => sql`${JSON.stringify(value)}::jsonb`;
|
|
2805
|
-
const fieldWorks = (key, field) => {
|
|
2806
|
-
const value = sql`${match}->(${key}::text)`;
|
|
2807
|
-
const type = sql`jsonb_typeof(${value})`;
|
|
2808
|
-
return sql`(${value} is null or (${and(field.events ? sql`${trigger}->'events' <@ ${json(field.events)}` : void 0, field.kind === "boolean" ? sql`${type} = 'boolean'` : sql`(${type} = ${field.kind} or (${type} = 'array' and not exists (select 1 from jsonb_array_elements(${value}) entry where jsonb_typeof(entry) <> ${field.kind})))`, field.enum ? sql`${value} <@ ${json(field.enum)}` : void 0)}))`;
|
|
2809
|
-
};
|
|
2810
|
-
const available = or(isScheduled, ...Object.entries(automationEventCatalog(getEventCatalog())).flatMap(([namespace, registration]) => registration.resourceTypes.map((resourceType) => {
|
|
2811
|
-
const fields = Object.entries(resourceType.matchFields ?? {});
|
|
2812
|
-
return and(eq(automation.namespace, namespace), sql`${trigger}->>'resourceType' = ${resourceType.type}`, sql`${trigger}->'events' <@ ${json(resourceType.supportedEvents)}`, sql`not exists (select 1 from jsonb_object_keys(${match}) match_key where not (${json(fields.map(([key]) => key))} @> to_jsonb(match_key)))`, ...fields.map(([key, field]) => fieldWorks(key, field)));
|
|
2813
|
-
}))) ?? sql`false`;
|
|
2814
2832
|
const runs = juniorAutomationRuns;
|
|
2815
2833
|
const lastRunNeedsAttention = sql`coalesce((select ${runs.status} in ('failed', 'blocked') from ${runs}
|
|
2816
2834
|
where ${runs.automationId} = ${automation.id} and ${runs.status} in ('blocked', 'completed', 'failed') and ${runs.completedAtMs} is not null
|
|
@@ -2821,8 +2839,8 @@ function viewerAutomationCollection(user) {
|
|
|
2821
2839
|
title: automation.title,
|
|
2822
2840
|
createdAtMs: automation.createdAtMs,
|
|
2823
2841
|
state: sql`${automation.status}`.as("state"),
|
|
2824
|
-
|
|
2825
|
-
|
|
2842
|
+
lastRunNeedsAttention: lastRunNeedsAttention.as("last_run_needs_attention"),
|
|
2843
|
+
trigger: sql`${automation.record}->'trigger'`.as("trigger"),
|
|
2826
2844
|
owned: sql`${owned}`.as("owned"),
|
|
2827
2845
|
resource: sql`case when ${isScheduled} then '' else concat_ws(' ', ${automation.identifier}, ${automation.record}->'trigger'->>'label', ${automation.namespace}) end`.as("resource"),
|
|
2828
2846
|
creator: sql`${automation.teamId} || ':' || ${automation.creatorSlackUserId}`.as("creator"),
|
|
@@ -2833,11 +2851,29 @@ function viewerAutomationCollection(user) {
|
|
|
2833
2851
|
instruction: automation.instruction
|
|
2834
2852
|
}).from(automation).leftJoin(destination, and(eq(destination.provider, "slack"), eq(destination.providerTenantId, automation.teamId), eq(destination.providerDestinationId, automation.channelId))).where(and(ne(automation.status, "deleted"), or(owned, publicToViewer))).as("accessible_automations");
|
|
2835
2853
|
}
|
|
2854
|
+
/**
|
|
2855
|
+
* List the accessible event automations whose trigger cannot work with the
|
|
2856
|
+
* enabled plugins. `eventTriggerProblems` decides, the same check that the
|
|
2857
|
+
* tools and the dashboard use, so SQL does not keep its own copy of the rules.
|
|
2858
|
+
*/
|
|
2859
|
+
async function unavailableEventAutomationIds(collection) {
|
|
2860
|
+
const catalog = automationEventCatalog(getEventCatalog());
|
|
2861
|
+
return (await getDb().select({
|
|
2862
|
+
id: collection.id,
|
|
2863
|
+
trigger: collection.trigger
|
|
2864
|
+
}).from(collection).where(eq(collection.kind, "event"))).flatMap((row) => {
|
|
2865
|
+
const trigger = eventAutomationTriggerSchema.safeParse(row.trigger);
|
|
2866
|
+
return trigger.success && registeredEventTriggerProblems(catalog, trigger.data).length === 0 ? [] : [row.id];
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2836
2869
|
/** Read an SQL page and complete, access-scoped counts and filter options. */
|
|
2837
2870
|
async function readAutomationCollection(user, input) {
|
|
2838
2871
|
const db = getDb();
|
|
2839
2872
|
const collection = viewerAutomationCollection(user);
|
|
2840
|
-
const
|
|
2873
|
+
const unavailableIds = await unavailableEventAutomationIds(collection);
|
|
2874
|
+
const unavailable = unavailableIds.length > 0 ? inArray(collection.id, unavailableIds) : sql`false`;
|
|
2875
|
+
const attention = and(notInArray(collection.state, ["paused", "completed"]), or(eq(collection.state, "blocked"), unavailable, eq(collection.lastRunNeedsAttention, true)));
|
|
2876
|
+
const where = and(input.scope === "mine" ? eq(collection.owned, true) : input.scope === "public" ? eq(collection.isPublic, true) : input.scope === "attention" ? attention : void 0, input.type === "all" ? void 0 : eq(collection.kind, input.type), input.state === "all" ? void 0 : input.state === "unavailable" ? unavailable : eq(collection.state, input.state), input.creator ? eq(collection.creator, input.creator) : void 0, input.destination ? eq(collection.destination, input.destination) : void 0, input.q ? sql`strpos(lower(concat_ws(' ', ${collection.title}, ${collection.instruction}, ${collection.resource})), ${input.q.toLowerCase()}) > 0` : void 0);
|
|
2841
2877
|
const [totals, counts, creators, destinations] = await Promise.all([
|
|
2842
2878
|
db.select({ total: count() }).from(collection).where(where),
|
|
2843
2879
|
db.select({
|
|
@@ -2966,6 +3002,7 @@ function executionSummaryFields(stats) {
|
|
|
2966
3002
|
lastRunStatus: stats?.lastRunStatus,
|
|
2967
3003
|
...stats?.lastConversationId ? { lastConversationId: stats.lastConversationId } : void 0,
|
|
2968
3004
|
...stats?.lastExecutedAtMs ? { lastRunAt: new Date(stats.lastExecutedAtMs).toISOString() } : void 0,
|
|
3005
|
+
costUsd: stats?.costUsd ?? emptyAutomationRunWindows(),
|
|
2969
3006
|
runs: stats?.runs ?? emptyAutomationRunWindows(),
|
|
2970
3007
|
totalRuns: stats?.totalRuns ?? 0
|
|
2971
3008
|
};
|
|
@@ -3019,7 +3056,7 @@ function eventAutomationSummary(task, ownedByViewer, destination, stats, creator
|
|
|
3019
3056
|
kind: "event",
|
|
3020
3057
|
...executionSummaryFields(stats),
|
|
3021
3058
|
ownedByViewer,
|
|
3022
|
-
resource: `${task.trigger.label} · ${task.trigger.identifier}
|
|
3059
|
+
resource: task.trigger.identifier ? `${task.trigger.label} · ${task.trigger.identifier}` : task.trigger.label,
|
|
3023
3060
|
source: task.trigger.namespace,
|
|
3024
3061
|
credentialMode: task.credentialMode,
|
|
3025
3062
|
outcomes: effectiveTaskOutcomes(task.outcomes, task.destination),
|
|
@@ -3502,292 +3539,6 @@ function readSlackActionToken(message) {
|
|
|
3502
3539
|
const token = slackActionTokenSchema.safeParse(envelope.data.raw?.action_token);
|
|
3503
3540
|
return token.success ? token.data : void 0;
|
|
3504
3541
|
}
|
|
3505
|
-
// src/chat/slack/cards.ts
|
|
3506
|
-
/** Select the Slack object renderer for a built-in card. */
|
|
3507
|
-
function renderSlackCard(card, conversationId) {
|
|
3508
|
-
switch (card.kind) {
|
|
3509
|
-
case "object": return renderSlackObjectCard(card, conversationId);
|
|
3510
|
-
case "automation": return renderSlackAutomationCard(card);
|
|
3511
|
-
}
|
|
3512
|
-
}
|
|
3513
|
-
// src/chat/slack/footer.ts
|
|
3514
|
-
/** Render compact reply attribution for the Slack footer. */
|
|
3515
|
-
function formatReplyAttribution(attribution) {
|
|
3516
|
-
return attribution.detail ? `${attribution.label} · ${attribution.detail}` : attribution.label;
|
|
3517
|
-
}
|
|
3518
|
-
/**
|
|
3519
|
-
* Build the compact conversation footer for visible Slack reply surfaces.
|
|
3520
|
-
*
|
|
3521
|
-
* Detailed turn metrics stay in the dashboard instead of Slack-visible copy.
|
|
3522
|
-
*/
|
|
3523
|
-
function buildSlackReplyFooter(args) {
|
|
3524
|
-
const items = [];
|
|
3525
|
-
const conversationId = args.conversationId?.trim();
|
|
3526
|
-
if (conversationId) {
|
|
3527
|
-
const idItem = {
|
|
3528
|
-
label: "ID",
|
|
3529
|
-
value: conversationId
|
|
3530
|
-
};
|
|
3531
|
-
const conversationUrl = getPluginSlackConversationLink(conversationId)?.url ?? getDashboardConversationLink(conversationId) ?? buildSentryConversationUrl(conversationId);
|
|
3532
|
-
if (conversationUrl) idItem.url = conversationUrl;
|
|
3533
|
-
items.push(idItem);
|
|
3534
|
-
}
|
|
3535
|
-
return items.length > 0 || args.replyAttribution ? {
|
|
3536
|
-
...args.replyAttribution ? { attribution: args.replyAttribution } : void 0,
|
|
3537
|
-
items
|
|
3538
|
-
} : void 0;
|
|
3539
|
-
}
|
|
3540
|
-
/** Build Slack blocks for a reply chunk using the Slack-flavored markdown block for the body. */
|
|
3541
|
-
function buildSlackReplyBlocks(text, footer) {
|
|
3542
|
-
const blocks = text.trim() ? [{
|
|
3543
|
-
type: "markdown",
|
|
3544
|
-
text
|
|
3545
|
-
}] : [];
|
|
3546
|
-
if (footer && (footer.attribution || footer.items.length > 0)) {
|
|
3547
|
-
const attributionElements = footer.attribution ? [{
|
|
3548
|
-
type: "plain_text",
|
|
3549
|
-
text: formatReplyAttribution(footer.attribution)
|
|
3550
|
-
}] : [];
|
|
3551
|
-
blocks.push({
|
|
3552
|
-
type: "context",
|
|
3553
|
-
elements: [...attributionElements, ...footer.items.map((item) => ({
|
|
3554
|
-
type: "mrkdwn",
|
|
3555
|
-
text: item.url ? `*${escapeSlackMrkdwnText(item.label)}:* ${formatSlackLink(item.url, item.value)}` : `*${escapeSlackMrkdwnText(item.label)}:* ${escapeSlackMrkdwnText(item.value)}`
|
|
3556
|
-
}))]
|
|
3557
|
-
});
|
|
3558
|
-
}
|
|
3559
|
-
return blocks.length ? blocks : void 0;
|
|
3560
|
-
}
|
|
3561
|
-
// src/chat/slack/output.ts
|
|
3562
|
-
const MAX_INLINE_CHARS = 2200;
|
|
3563
|
-
const MAX_INLINE_LINES = 45;
|
|
3564
|
-
const CONTINUED_MARKER = "\n\n[Continued below]";
|
|
3565
|
-
function countSlackLines(text) {
|
|
3566
|
-
if (!text) return 0;
|
|
3567
|
-
return text.split("\n").length;
|
|
3568
|
-
}
|
|
3569
|
-
function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
3570
|
-
return text.length <= maxChars && countSlackLines(text) <= maxLines;
|
|
3571
|
-
}
|
|
3572
|
-
function findSplitIndex(text, maxChars) {
|
|
3573
|
-
if (text.length <= maxChars) return text.length;
|
|
3574
|
-
const bounded = text.slice(0, maxChars);
|
|
3575
|
-
const newlineIndex = bounded.lastIndexOf("\n");
|
|
3576
|
-
if (newlineIndex > 0) return newlineIndex;
|
|
3577
|
-
const spaceIndex = bounded.lastIndexOf(" ");
|
|
3578
|
-
if (spaceIndex > 0) return spaceIndex;
|
|
3579
|
-
return maxChars;
|
|
3580
|
-
}
|
|
3581
|
-
function splitByLineBudget(text, maxLines) {
|
|
3582
|
-
if (maxLines <= 0) return "";
|
|
3583
|
-
const lines = text.split("\n");
|
|
3584
|
-
if (lines.length <= maxLines) return text;
|
|
3585
|
-
return lines.slice(0, maxLines).join("\n");
|
|
3586
|
-
}
|
|
3587
|
-
function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
3588
|
-
return {
|
|
3589
|
-
maxChars: Math.max(1, maxChars - suffix.length),
|
|
3590
|
-
maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
|
|
3591
|
-
};
|
|
3592
|
-
}
|
|
3593
|
-
function forceSplitBudget(text, budget) {
|
|
3594
|
-
const lineCount = countSlackLines(text);
|
|
3595
|
-
return {
|
|
3596
|
-
maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
|
|
3597
|
-
maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
|
|
3598
|
-
};
|
|
3599
|
-
}
|
|
3600
|
-
function getFenceContinuation(text) {
|
|
3601
|
-
let open;
|
|
3602
|
-
for (const line of text.split("\n")) {
|
|
3603
|
-
const trimmed = line.trimStart();
|
|
3604
|
-
const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
|
|
3605
|
-
if (!openerMatch?.[1]) continue;
|
|
3606
|
-
if (!open) {
|
|
3607
|
-
open = {
|
|
3608
|
-
fence: openerMatch[1],
|
|
3609
|
-
openerLine: trimmed
|
|
3610
|
-
};
|
|
3611
|
-
continue;
|
|
3612
|
-
}
|
|
3613
|
-
if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) open = void 0;
|
|
3614
|
-
}
|
|
3615
|
-
if (!open) return null;
|
|
3616
|
-
return {
|
|
3617
|
-
closeSuffix: text.endsWith("\n") ? open.fence : `\n${open.fence}`,
|
|
3618
|
-
reopenPrefix: `${open.openerLine}\n`
|
|
3619
|
-
};
|
|
3620
|
-
}
|
|
3621
|
-
function appendSlackSuffix(text, marker) {
|
|
3622
|
-
return `${text}${getFenceContinuation(text)?.closeSuffix ?? ""}${marker}`;
|
|
3623
|
-
}
|
|
3624
|
-
function stripTrailingContinuationMarker(text) {
|
|
3625
|
-
return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -19) : text;
|
|
3626
|
-
}
|
|
3627
|
-
function takeSlackContinuationChunk(text, budget) {
|
|
3628
|
-
let { prefix, rest } = takeSlackInlinePrefix(text, budget);
|
|
3629
|
-
if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, budget)));
|
|
3630
|
-
let carryover = rest ? getFenceContinuation(prefix) : null;
|
|
3631
|
-
if (!carryover) return {
|
|
3632
|
-
prefix,
|
|
3633
|
-
rest
|
|
3634
|
-
};
|
|
3635
|
-
const carryoverBudget = reserveInlineBudgetForSuffix(`${carryover.closeSuffix}${CONTINUED_MARKER}`);
|
|
3636
|
-
({prefix, rest} = takeSlackInlinePrefix(text, carryoverBudget));
|
|
3637
|
-
if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, carryoverBudget)));
|
|
3638
|
-
carryover = rest ? getFenceContinuation(prefix) : null;
|
|
3639
|
-
if (!carryover) return {
|
|
3640
|
-
prefix,
|
|
3641
|
-
rest
|
|
3642
|
-
};
|
|
3643
|
-
return {
|
|
3644
|
-
prefix,
|
|
3645
|
-
rest: `${carryover.reopenPrefix}${rest}`
|
|
3646
|
-
};
|
|
3647
|
-
}
|
|
3648
|
-
/** Return the first continuation-safe chunk plus any remaining text. */
|
|
3649
|
-
function takeSlackContinuationPrefix(text, options) {
|
|
3650
|
-
const budget = {
|
|
3651
|
-
maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
|
|
3652
|
-
maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
|
|
3653
|
-
};
|
|
3654
|
-
const { prefix, rest } = (() => {
|
|
3655
|
-
if (options?.forceSplit) return takeSlackContinuationChunk(text, budget);
|
|
3656
|
-
const initial = takeSlackInlinePrefix(text, budget);
|
|
3657
|
-
return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
|
|
3658
|
-
})();
|
|
3659
|
-
return {
|
|
3660
|
-
prefix,
|
|
3661
|
-
renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
|
|
3662
|
-
rest
|
|
3663
|
-
};
|
|
3664
|
-
}
|
|
3665
|
-
/**
|
|
3666
|
-
* Take the largest Slack-safe inline prefix from `text` under the configured
|
|
3667
|
-
* character and line budgets. Returns the consumed prefix plus remaining text.
|
|
3668
|
-
*/
|
|
3669
|
-
function takeSlackInlinePrefix(text, options) {
|
|
3670
|
-
const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
|
|
3671
|
-
const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
|
|
3672
|
-
const normalized = text.replace(/\r\n?/g, "\n");
|
|
3673
|
-
if (!normalized) return {
|
|
3674
|
-
prefix: "",
|
|
3675
|
-
rest: ""
|
|
3676
|
-
};
|
|
3677
|
-
if (fitsInlineBudget(normalized, maxChars, maxLines)) return {
|
|
3678
|
-
prefix: normalized,
|
|
3679
|
-
rest: ""
|
|
3680
|
-
};
|
|
3681
|
-
const lineBounded = splitByLineBudget(normalized, maxLines);
|
|
3682
|
-
const cutIndex = findSplitIndex(lineBounded, maxChars);
|
|
3683
|
-
const prefix = lineBounded.slice(0, cutIndex).trimEnd();
|
|
3684
|
-
if (prefix) return {
|
|
3685
|
-
prefix,
|
|
3686
|
-
rest: normalized.slice(prefix.length).trimStart()
|
|
3687
|
-
};
|
|
3688
|
-
const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
|
|
3689
|
-
return {
|
|
3690
|
-
prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
|
|
3691
|
-
rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
|
|
3692
|
-
};
|
|
3693
|
-
}
|
|
3694
|
-
/**
|
|
3695
|
-
* Split a normalized Slack reply into multiple inline-safe thread messages.
|
|
3696
|
-
*
|
|
3697
|
-
* Replies split across more than two messages receive explicit continuation
|
|
3698
|
-
* markers on non-final chunks. When
|
|
3699
|
-
* `interrupted` is true, the final chunk receives an interruption marker.
|
|
3700
|
-
*/
|
|
3701
|
-
function splitSlackReplyText(text, options) {
|
|
3702
|
-
const normalized = normalizeSlackReplyMarkdown(text);
|
|
3703
|
-
if (!normalized) return [];
|
|
3704
|
-
const chunks = [];
|
|
3705
|
-
const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
3706
|
-
let remaining = normalized;
|
|
3707
|
-
while (remaining) {
|
|
3708
|
-
if (options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining)) {
|
|
3709
|
-
chunks.push(options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining);
|
|
3710
|
-
break;
|
|
3711
|
-
}
|
|
3712
|
-
const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
|
|
3713
|
-
...continuationBudget,
|
|
3714
|
-
forceSplit: true
|
|
3715
|
-
});
|
|
3716
|
-
chunks.push(renderedPrefix);
|
|
3717
|
-
remaining = rest;
|
|
3718
|
-
}
|
|
3719
|
-
if (chunks.length === 2) chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
|
|
3720
|
-
return chunks;
|
|
3721
|
-
}
|
|
3722
|
-
/**
|
|
3723
|
-
* Reserve enough inline budget for a continuation suffix on the current chunk.
|
|
3724
|
-
*/
|
|
3725
|
-
function getSlackContinuationBudget() {
|
|
3726
|
-
return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
3727
|
-
}
|
|
3728
|
-
/** Normalize text for Slack and wrap it as a PostableMessage. */
|
|
3729
|
-
function buildSlackOutputMessage(text) {
|
|
3730
|
-
const normalized = normalizeSlackReplyMarkdown(text);
|
|
3731
|
-
if (!normalized) throw new Error(`Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`);
|
|
3732
|
-
return { markdown: normalized };
|
|
3733
|
-
}
|
|
3734
|
-
// src/chat/slack/reply.ts
|
|
3735
|
-
/**
|
|
3736
|
-
* Send one destination-visible Slack reply.
|
|
3737
|
-
*
|
|
3738
|
-
* Chunks oversized text, builds compact attribution and conversation footer
|
|
3739
|
-
* context, and posts through the shared Slack outbound boundary.
|
|
3740
|
-
*/
|
|
3741
|
-
async function sendSlackReply(args) {
|
|
3742
|
-
const posts = splitSlackReplyText(args.text).map((text) => ({
|
|
3743
|
-
text,
|
|
3744
|
-
cards: []
|
|
3745
|
-
}));
|
|
3746
|
-
const cards = (args.cards ?? []).map((card) => renderSlackCard(card, args.conversationId));
|
|
3747
|
-
for (let index = 0; index < cards.length; index += 5) {
|
|
3748
|
-
const group = cards.slice(index, index + 5);
|
|
3749
|
-
const lastPost = posts.at(-1);
|
|
3750
|
-
if (index === 0 && lastPost) lastPost.cards = group;
|
|
3751
|
-
else posts.push({
|
|
3752
|
-
text: "",
|
|
3753
|
-
cards: group
|
|
3754
|
-
});
|
|
3755
|
-
}
|
|
3756
|
-
const footer = buildSlackReplyFooter({
|
|
3757
|
-
conversationId: args.conversationId,
|
|
3758
|
-
replyAttribution: args.replyAttribution
|
|
3759
|
-
});
|
|
3760
|
-
const messageTs = [];
|
|
3761
|
-
let threadTs = args.threadTs;
|
|
3762
|
-
for (const [index, post] of posts.entries()) {
|
|
3763
|
-
const { text, cards } = post;
|
|
3764
|
-
const isFinalChunk = index === posts.length - 1;
|
|
3765
|
-
const blocks = buildSlackReplyBlocks(text, void 0) ?? [];
|
|
3766
|
-
for (const card of cards) if (!card.entity) blocks.push({
|
|
3767
|
-
type: "section",
|
|
3768
|
-
text: {
|
|
3769
|
-
type: "mrkdwn",
|
|
3770
|
-
text: card.text
|
|
3771
|
-
}
|
|
3772
|
-
});
|
|
3773
|
-
if (isFinalChunk || !text.trim()) blocks.push(...buildSlackReplyBlocks("", footer) ?? []);
|
|
3774
|
-
const entities = cards.flatMap((card) => card.entity ? [card.entity] : []);
|
|
3775
|
-
const accessibleText = [text, ...cards.map((card) => card.text)].filter(Boolean).join("\n\n");
|
|
3776
|
-
const fallbackText = isFinalChunk && args.replyAttribution ? `${accessibleText}\n\n${escapeSlackMrkdwnText(formatReplyAttribution(args.replyAttribution))}` : accessibleText;
|
|
3777
|
-
const response = await postSlackMessage({
|
|
3778
|
-
channelId: args.channelId,
|
|
3779
|
-
threadTs,
|
|
3780
|
-
text: fallbackText,
|
|
3781
|
-
...blocks.length ? { blocks } : void 0,
|
|
3782
|
-
...entities.length ? { entities } : void 0
|
|
3783
|
-
});
|
|
3784
|
-
if (response.ts) {
|
|
3785
|
-
messageTs.push(response.ts);
|
|
3786
|
-
threadTs ??= response.ts;
|
|
3787
|
-
}
|
|
3788
|
-
}
|
|
3789
|
-
return messageTs;
|
|
3790
|
-
}
|
|
3791
3542
|
// src/chat/slack/assistant-thread/status-render.ts
|
|
3792
3543
|
const DEFAULT_STATUS_CONTEXTS = {
|
|
3793
3544
|
thinking: "…",
|
|
@@ -4289,8 +4040,8 @@ async function saveSteeringMessages(args) {
|
|
|
4289
4040
|
* Persist post-delivery Redis scratch with a short retry after durable SQL
|
|
4290
4041
|
* completion has succeeded.
|
|
4291
4042
|
*/
|
|
4292
|
-
async function persistThreadRuntimeStateWithRetry(
|
|
4293
|
-
await persistWithRetry(() => persistThreadRuntimeState(
|
|
4043
|
+
async function persistThreadRuntimeStateWithRetry(conversationId, patch) {
|
|
4044
|
+
await persistWithRetry(() => persistThreadRuntimeState(conversationId, patch));
|
|
4294
4045
|
}
|
|
4295
4046
|
/**
|
|
4296
4047
|
* Resolve the Pi history for this Slack turn from the most precise durable
|
|
@@ -4327,10 +4078,7 @@ function createSlackTurn(deps) {
|
|
|
4327
4078
|
channelName,
|
|
4328
4079
|
channelType: slackChannelType
|
|
4329
4080
|
});
|
|
4330
|
-
const destinationVisibility =
|
|
4331
|
-
destination,
|
|
4332
|
-
visibility: options.execution?.destinationVisibility ?? conversationVisibilityFromSlackChannelType(slackChannelType)
|
|
4333
|
-
});
|
|
4081
|
+
const destinationVisibility = conversationVisibilityFromSlackChannelType(slackChannelType);
|
|
4334
4082
|
const assistantThreadContext = getAssistantThreadContext(message);
|
|
4335
4083
|
const threadTs = getThreadTs(threadId) ?? assistantThreadContext?.threadTs;
|
|
4336
4084
|
const messageTs = getMessageTs(message);
|
|
@@ -4340,7 +4088,14 @@ function createSlackTurn(deps) {
|
|
|
4340
4088
|
messageTs,
|
|
4341
4089
|
teamId,
|
|
4342
4090
|
threadTs,
|
|
4343
|
-
visibility:
|
|
4091
|
+
visibility: await resolveDestinationVisibility({
|
|
4092
|
+
destination,
|
|
4093
|
+
visibility: destinationVisibility
|
|
4094
|
+
}) ?? "private"
|
|
4095
|
+
});
|
|
4096
|
+
const replyTarget = resolveSlackReplyTarget({
|
|
4097
|
+
destination,
|
|
4098
|
+
source
|
|
4344
4099
|
});
|
|
4345
4100
|
const slackActionToken = readSlackActionToken(message);
|
|
4346
4101
|
const runId = options.execution?.dispatch?.id ?? getRunId(thread, message);
|
|
@@ -4396,6 +4151,7 @@ function createSlackTurn(deps) {
|
|
|
4396
4151
|
destination,
|
|
4397
4152
|
locationConfiguration: options.execution?.locationConfiguration,
|
|
4398
4153
|
context: {
|
|
4154
|
+
conversationId,
|
|
4399
4155
|
threadId,
|
|
4400
4156
|
actorId: slackActorId,
|
|
4401
4157
|
channelId,
|
|
@@ -4413,17 +4169,16 @@ function createSlackTurn(deps) {
|
|
|
4413
4169
|
};
|
|
4414
4170
|
const postAuthPauseNotice = async (providerDisplayName, requestText) => {
|
|
4415
4171
|
if (!actor) throw new Error("Slack auth pause notice requires an actor");
|
|
4172
|
+
if (!postsRunNotices(options.execution?.dispatch)) return;
|
|
4416
4173
|
const text = buildAuthPauseResponse(actor.userId, providerDisplayName, requestText);
|
|
4417
4174
|
try {
|
|
4418
4175
|
await beforeFirstResponsePost();
|
|
4419
|
-
|
|
4420
|
-
|
|
4176
|
+
await sendSlackReply({
|
|
4177
|
+
...replyTarget,
|
|
4421
4178
|
conversationId,
|
|
4422
4179
|
replyAttribution: options.execution?.dispatch?.replyAttribution,
|
|
4423
|
-
text
|
|
4424
|
-
threadTs
|
|
4180
|
+
text
|
|
4425
4181
|
});
|
|
4426
|
-
else await thread.post(buildSlackOutputMessage(text));
|
|
4427
4182
|
} catch (error) {
|
|
4428
4183
|
logException(error, "slack.auth_pause.notice_post.failed", {
|
|
4429
4184
|
"app.slack.reply_stage": "thread_reply_auth_pause_notice",
|
|
@@ -4477,7 +4232,7 @@ function createSlackTurn(deps) {
|
|
|
4477
4232
|
const deliveredMessage = [...preparedState.conversation.messages].reverse().find((candidate) => candidate.role === "assistant" && candidate.id.startsWith(`${turnId}:assistant:`) && candidate.meta?.replied === true);
|
|
4478
4233
|
options.onTurnDeliveryAccepted?.(deliveredMessage?.meta?.slackTs);
|
|
4479
4234
|
options.onTurnOutcome?.({ outcome: "completed" });
|
|
4480
|
-
await
|
|
4235
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4481
4236
|
await options.onTurnStatePersisted?.();
|
|
4482
4237
|
await options.ack?.();
|
|
4483
4238
|
await options.onTurnCompleted?.();
|
|
@@ -4500,7 +4255,7 @@ function createSlackTurn(deps) {
|
|
|
4500
4255
|
});
|
|
4501
4256
|
throw error;
|
|
4502
4257
|
}
|
|
4503
|
-
await
|
|
4258
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4504
4259
|
await options.onTurnStatePersisted?.();
|
|
4505
4260
|
await options.ack?.();
|
|
4506
4261
|
return;
|
|
@@ -4538,7 +4293,11 @@ function createSlackTurn(deps) {
|
|
|
4538
4293
|
});
|
|
4539
4294
|
if (configReply) {
|
|
4540
4295
|
await beforeFirstResponsePost();
|
|
4541
|
-
await
|
|
4296
|
+
await sendSlackReply({
|
|
4297
|
+
...replyTarget,
|
|
4298
|
+
conversationId,
|
|
4299
|
+
text: configReply.text
|
|
4300
|
+
});
|
|
4542
4301
|
markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
|
|
4543
4302
|
replied: true,
|
|
4544
4303
|
skippedReason: void 0
|
|
@@ -4557,7 +4316,7 @@ function createSlackTurn(deps) {
|
|
|
4557
4316
|
source: "slack"
|
|
4558
4317
|
}
|
|
4559
4318
|
});
|
|
4560
|
-
await
|
|
4319
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4561
4320
|
await options.onTurnStatePersisted?.();
|
|
4562
4321
|
await options.ack?.();
|
|
4563
4322
|
return;
|
|
@@ -4598,7 +4357,7 @@ function createSlackTurn(deps) {
|
|
|
4598
4357
|
"app.message.id": message.id,
|
|
4599
4358
|
...messageTs ? { "messaging.message.id": messageTs } : void 0
|
|
4600
4359
|
});
|
|
4601
|
-
await
|
|
4360
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4602
4361
|
await options.onTurnStatePersisted?.();
|
|
4603
4362
|
if (actor) setSentryUser({
|
|
4604
4363
|
id: actor.userId,
|
|
@@ -4685,16 +4444,15 @@ function createSlackTurn(deps) {
|
|
|
4685
4444
|
}
|
|
4686
4445
|
else {
|
|
4687
4446
|
slackMessageTs = await sendSlackReply({
|
|
4447
|
+
...replyTarget,
|
|
4688
4448
|
cards,
|
|
4689
|
-
channelId: destination.channelId,
|
|
4690
4449
|
conversationId,
|
|
4691
4450
|
replyAttribution: options.execution?.dispatch?.replyAttribution,
|
|
4692
|
-
text
|
|
4693
|
-
...threadTs ? { threadTs } : void 0
|
|
4451
|
+
text
|
|
4694
4452
|
});
|
|
4695
4453
|
messageDestinations.push(...slackMessageTs.map((messageId) => ({
|
|
4696
4454
|
destination,
|
|
4697
|
-
messageId: threadTs ?? messageId
|
|
4455
|
+
messageId: replyTarget.threadTs ?? messageId
|
|
4698
4456
|
})));
|
|
4699
4457
|
}
|
|
4700
4458
|
} catch (error) {
|
|
@@ -4784,7 +4542,7 @@ function createSlackTurn(deps) {
|
|
|
4784
4542
|
});
|
|
4785
4543
|
if (compaction.compacted) {
|
|
4786
4544
|
piMessages = compaction.piMessages;
|
|
4787
|
-
await
|
|
4545
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4788
4546
|
}
|
|
4789
4547
|
}
|
|
4790
4548
|
const hasDurablePromptHistory = Boolean(piMessages?.length);
|
|
@@ -4893,11 +4651,11 @@ function createSlackTurn(deps) {
|
|
|
4893
4651
|
drainSteeringMessages,
|
|
4894
4652
|
shouldYield: options.shouldYield,
|
|
4895
4653
|
onSandboxRefChanged: async (sandboxRef) => {
|
|
4896
|
-
await
|
|
4654
|
+
await persistThreadStateById(conversationId, { sandboxRef });
|
|
4897
4655
|
},
|
|
4898
4656
|
recordPendingAuth: async (pendingAuth) => {
|
|
4899
4657
|
preparedState.conversation.processing.pendingAuth = pendingAuth;
|
|
4900
|
-
await
|
|
4658
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4901
4659
|
}
|
|
4902
4660
|
}
|
|
4903
4661
|
};
|
|
@@ -4976,7 +4734,7 @@ function createSlackTurn(deps) {
|
|
|
4976
4734
|
conversation: completedState.conversation,
|
|
4977
4735
|
conversationId
|
|
4978
4736
|
}));
|
|
4979
|
-
await persistThreadRuntimeStateWithRetry(
|
|
4737
|
+
await persistThreadRuntimeStateWithRetry(conversationId, completedState);
|
|
4980
4738
|
const automatedTurnLimit = await recordFinishedTurnForAutomatedLimit({
|
|
4981
4739
|
conversationId,
|
|
4982
4740
|
maxTurns: botConfig.maxConsecutiveAutomatedTurns,
|
|
@@ -4985,7 +4743,7 @@ function createSlackTurn(deps) {
|
|
|
4985
4743
|
await maybePostAutomatedTurnLimitNotice({
|
|
4986
4744
|
conversationId,
|
|
4987
4745
|
maxTurns: botConfig.maxConsecutiveAutomatedTurns,
|
|
4988
|
-
|
|
4746
|
+
source,
|
|
4989
4747
|
update: automatedTurnLimit
|
|
4990
4748
|
});
|
|
4991
4749
|
} catch (saveError) {
|
|
@@ -5021,7 +4779,7 @@ function createSlackTurn(deps) {
|
|
|
5021
4779
|
nowMs: Date.now(),
|
|
5022
4780
|
sessionId: turnId
|
|
5023
4781
|
});
|
|
5024
|
-
await
|
|
4782
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5025
4783
|
if (conversationId) await turnLifecycle.fail({
|
|
5026
4784
|
conversationId,
|
|
5027
4785
|
createdAtMs: Date.now(),
|
|
@@ -5038,7 +4796,7 @@ function createSlackTurn(deps) {
|
|
|
5038
4796
|
conversation: preparedState.conversation,
|
|
5039
4797
|
sessionId: turnId
|
|
5040
4798
|
});
|
|
5041
|
-
await
|
|
4799
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5042
4800
|
persistedAtLeastOnce = true;
|
|
5043
4801
|
shouldPersistFailureState = false;
|
|
5044
4802
|
return;
|
|
@@ -5122,7 +4880,7 @@ function createSlackTurn(deps) {
|
|
|
5122
4880
|
nowMs: Date.now(),
|
|
5123
4881
|
sessionId: turnId
|
|
5124
4882
|
});
|
|
5125
|
-
await
|
|
4883
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5126
4884
|
await turnLifecycle.complete({
|
|
5127
4885
|
conversationId,
|
|
5128
4886
|
createdAtMs: Date.now(),
|
|
@@ -5179,7 +4937,7 @@ function createSlackTurn(deps) {
|
|
|
5179
4937
|
} catch (recordError) {
|
|
5180
4938
|
logException(recordError, "agent.turn.failed_session_record_persist.failed");
|
|
5181
4939
|
}
|
|
5182
|
-
await
|
|
4940
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5183
4941
|
if (shouldEmitDevAgentTrace()) logWarn("agent.turn.failed");
|
|
5184
4942
|
}
|
|
5185
4943
|
await status.clear();
|
|
@@ -5318,10 +5076,10 @@ async function seedConversationBackfill(thread, conversation, currentTurn, resol
|
|
|
5318
5076
|
/** Build the turn-state preparer from injected conversation services. */
|
|
5319
5077
|
function createPrepareTurnState(deps) {
|
|
5320
5078
|
return async function prepareTurnState(args) {
|
|
5321
|
-
const
|
|
5079
|
+
const conversationId = args.context.conversationId ?? args.context.threadId ?? args.context.runId;
|
|
5080
|
+
const existingState = conversationId ? await getPersistedThreadState(conversationId) : await args.thread.state;
|
|
5322
5081
|
const sandboxRef = getPersistedSandboxState(existingState ?? {});
|
|
5323
5082
|
const conversation = coerceThreadConversationState(existingState);
|
|
5324
|
-
const conversationId = args.context.threadId ?? args.context.runId;
|
|
5325
5083
|
if (conversationId && isVisionEnabled()) conversation.vision = await loadConversationVisionCache(conversationId);
|
|
5326
5084
|
await hydrateConversationMessages({
|
|
5327
5085
|
conversation,
|
|
@@ -5448,7 +5206,6 @@ function createSlackDispatchTurnRunner(options) {
|
|
|
5448
5206
|
disabledFeatures: ["interactive-auth"],
|
|
5449
5207
|
locationConfiguration: options.getLocationConfiguration(dispatch.destination),
|
|
5450
5208
|
credentialContext: routing.credentialContext,
|
|
5451
|
-
destinationVisibility: dispatch.destinationVisibility,
|
|
5452
5209
|
dispatch: routing.dispatch,
|
|
5453
5210
|
skipProviderDefaultConfig: true,
|
|
5454
5211
|
source: dispatch.source,
|
|
@@ -5507,8 +5264,8 @@ function createSlackRuntime(options) {
|
|
|
5507
5264
|
modelId: defaultModelId(botConfig),
|
|
5508
5265
|
now: options.now ?? (() => Date.now()),
|
|
5509
5266
|
prepareTurnState,
|
|
5510
|
-
persistPreparedState: async ({
|
|
5511
|
-
await
|
|
5267
|
+
persistPreparedState: async ({ conversationId, preparedState }) => {
|
|
5268
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5512
5269
|
},
|
|
5513
5270
|
getPreparedConversationContext: (preparedState) => preparedState.conversationContext,
|
|
5514
5271
|
decideSubscribedReply: async (args) => {
|
|
@@ -5523,18 +5280,18 @@ function createSlackRuntime(options) {
|
|
|
5523
5280
|
});
|
|
5524
5281
|
return decision;
|
|
5525
5282
|
},
|
|
5526
|
-
recordSkippedSteeringMessage: async ({
|
|
5527
|
-
const conversation = coerceThreadConversationState(await
|
|
5283
|
+
recordSkippedSteeringMessage: async ({ conversationId, message, decision, text }) => {
|
|
5284
|
+
const conversation = coerceThreadConversationState(await getPersistedThreadState(conversationId));
|
|
5528
5285
|
recordSkippedConversationMessage({
|
|
5529
5286
|
conversation,
|
|
5530
5287
|
message,
|
|
5531
5288
|
skippedReason: decision.reason,
|
|
5532
5289
|
text: text.userText
|
|
5533
5290
|
});
|
|
5534
|
-
await
|
|
5291
|
+
await persistThreadStateById(conversationId, { conversation });
|
|
5535
5292
|
},
|
|
5536
|
-
recordSkippedSubscribedTurn: async ({
|
|
5537
|
-
const conversation = coerceThreadConversationState(await
|
|
5293
|
+
recordSkippedSubscribedTurn: async ({ conversationId, message, decision, completedAtMs, text }) => {
|
|
5294
|
+
const conversation = coerceThreadConversationState(await getPersistedThreadState(conversationId));
|
|
5538
5295
|
recordSkippedConversationMessage({
|
|
5539
5296
|
conversation,
|
|
5540
5297
|
message,
|
|
@@ -5543,16 +5300,16 @@ function createSlackRuntime(options) {
|
|
|
5543
5300
|
});
|
|
5544
5301
|
clearSkippedTurnIfActive(conversation, message.id);
|
|
5545
5302
|
conversation.processing.lastCompletedAtMs = completedAtMs;
|
|
5546
|
-
await
|
|
5303
|
+
await persistThreadStateById(conversationId, { conversation });
|
|
5547
5304
|
},
|
|
5548
|
-
onSubscribedMessageSkipped: async ({
|
|
5305
|
+
onSubscribedMessageSkipped: async ({ conversationId, message, preparedState, decision, completedAtMs }) => {
|
|
5549
5306
|
markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
|
|
5550
5307
|
replied: false,
|
|
5551
5308
|
skippedReason: decision.reason
|
|
5552
5309
|
});
|
|
5553
5310
|
clearSkippedTurnIfActive(preparedState.conversation, message.id);
|
|
5554
5311
|
preparedState.conversation.processing.lastCompletedAtMs = completedAtMs;
|
|
5555
|
-
await
|
|
5312
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5556
5313
|
},
|
|
5557
5314
|
executeSlackTurn,
|
|
5558
5315
|
initializeAssistantThread: async ({ channelId, threadTs, sourceChannelId }) => {
|
|
@@ -5599,23 +5356,20 @@ function createJuniorSlackAdapter(config) {
|
|
|
5599
5356
|
* authorization pause notices use `sendSlackReply` so the footer stays
|
|
5600
5357
|
* consistent.
|
|
5601
5358
|
*/
|
|
5602
|
-
|
|
5359
|
+
/** Post a failure reply or status notice when the run's dispatch allows it. */
|
|
5360
|
+
async function postRunNotice(resume, text) {
|
|
5361
|
+
if (!postsRunNotices(resume.run?.dispatch)) return;
|
|
5362
|
+
await sendSlackReply({
|
|
5363
|
+
channelId: resume.channelId,
|
|
5364
|
+
conversationId: resume.conversationId,
|
|
5365
|
+
replyAttribution: resume.run?.dispatch?.replyAttribution,
|
|
5366
|
+
text,
|
|
5367
|
+
threadTs: resume.threadTs
|
|
5368
|
+
});
|
|
5369
|
+
}
|
|
5370
|
+
async function postRunNoticeBestEffort(resume, text) {
|
|
5603
5371
|
try {
|
|
5604
|
-
|
|
5605
|
-
await sendSlackReply({
|
|
5606
|
-
channelId,
|
|
5607
|
-
conversationId,
|
|
5608
|
-
replyAttribution,
|
|
5609
|
-
text,
|
|
5610
|
-
threadTs
|
|
5611
|
-
});
|
|
5612
|
-
return;
|
|
5613
|
-
}
|
|
5614
|
-
await postSlackMessage({
|
|
5615
|
-
channelId,
|
|
5616
|
-
threadTs,
|
|
5617
|
-
text
|
|
5618
|
-
});
|
|
5372
|
+
await postRunNotice(resume, text);
|
|
5619
5373
|
} catch {}
|
|
5620
5374
|
}
|
|
5621
5375
|
/** Create a read-only configuration service from persisted values. */
|
|
@@ -5656,13 +5410,6 @@ function getResumeLogContext(args) {
|
|
|
5656
5410
|
modelId: defaultModelId(botConfig)
|
|
5657
5411
|
};
|
|
5658
5412
|
}
|
|
5659
|
-
async function postResumeFailureReply(args) {
|
|
5660
|
-
await postSlackMessage({
|
|
5661
|
-
channelId: args.channelId,
|
|
5662
|
-
threadTs: args.threadTs,
|
|
5663
|
-
text: buildTurnErrorResponse(args.error, args.eventId)
|
|
5664
|
-
});
|
|
5665
|
-
}
|
|
5666
5413
|
async function handleResumeFailure(args) {
|
|
5667
5414
|
const turnLifecycle = getTurnLifecycle();
|
|
5668
5415
|
const capturedEventId = logException(args.error, args.eventName);
|
|
@@ -5686,12 +5433,7 @@ async function handleResumeFailure(args) {
|
|
|
5686
5433
|
failureStatePersistError = persistError;
|
|
5687
5434
|
}
|
|
5688
5435
|
try {
|
|
5689
|
-
await
|
|
5690
|
-
channelId: args.resume.channelId,
|
|
5691
|
-
threadTs: args.resume.threadTs,
|
|
5692
|
-
eventId,
|
|
5693
|
-
error: args.error
|
|
5694
|
-
});
|
|
5436
|
+
await postRunNotice(args.resume, buildTurnErrorResponse(args.error, eventId));
|
|
5695
5437
|
} catch (deliveryError) {
|
|
5696
5438
|
const deliveryEventId = logException(deliveryError, "slack.resume.failure_delivery.failed", { "app.error.original_event_id": eventId });
|
|
5697
5439
|
try {
|
|
@@ -5767,7 +5509,7 @@ function buildResumedRun(args, statusSession, delivery) {
|
|
|
5767
5509
|
* generation begins.
|
|
5768
5510
|
*/
|
|
5769
5511
|
async function resumeSlackTurn(args) {
|
|
5770
|
-
return
|
|
5512
|
+
return await withTurnSpan(getResumeLogContext(args), args.traceContext && { traceContexts: [args.traceContext] }, () => resumeSlackTurnInContext(args));
|
|
5771
5513
|
}
|
|
5772
5514
|
async function resumeSlackTurnInContext(args) {
|
|
5773
5515
|
const status = createSlackWebApiAssistantStatusSession({
|
|
@@ -5810,10 +5552,9 @@ async function resumeSlackTurnInContext(args) {
|
|
|
5810
5552
|
channelId: runArgs.channelId,
|
|
5811
5553
|
timestamp: runArgs.messageTs
|
|
5812
5554
|
});
|
|
5813
|
-
if (runArgs.initialText) await
|
|
5555
|
+
if (runArgs.initialText) await postRunNoticeBestEffort(runArgs, runArgs.initialText);
|
|
5814
5556
|
status.update(runArgs.initialStatus);
|
|
5815
5557
|
const conversationId = runArgs.conversationId;
|
|
5816
|
-
const visibility = (await getConversationStore().get({ conversationId }))?.visibility;
|
|
5817
5558
|
const visibleConversationId = args.conversationId;
|
|
5818
5559
|
const sessionId = runArgs.turnId;
|
|
5819
5560
|
let deliveryConversation;
|
|
@@ -5908,7 +5649,6 @@ async function resumeSlackTurnInContext(args) {
|
|
|
5908
5649
|
await persistWithRetry(() => recordTurnSummary({
|
|
5909
5650
|
conversationId: runArgs.conversationId,
|
|
5910
5651
|
destination: savedRun.destination,
|
|
5911
|
-
destinationVisibility: visibility,
|
|
5912
5652
|
dispatchId,
|
|
5913
5653
|
resultMessageId: messageTs,
|
|
5914
5654
|
turnId: runArgs.turnId,
|
|
@@ -5954,7 +5694,6 @@ async function resumeSlackTurnInContext(args) {
|
|
|
5954
5694
|
durationMs: reply.diagnostics.durationMs,
|
|
5955
5695
|
usage: reply.diagnostics.usage,
|
|
5956
5696
|
destination: run.destination,
|
|
5957
|
-
destinationVisibility: visibility,
|
|
5958
5697
|
dispatchId: run.dispatch?.id,
|
|
5959
5698
|
dispatchOutcome: dispatchResult.outcome,
|
|
5960
5699
|
...dispatchErrorMessage ? { errorMessage: dispatchErrorMessage } : void 0,
|
|
@@ -5967,7 +5706,6 @@ async function resumeSlackTurnInContext(args) {
|
|
|
5967
5706
|
else if (run.dispatch?.id) await recordTurnSummary({
|
|
5968
5707
|
conversationId: runArgs.conversationId,
|
|
5969
5708
|
destination: run.destination,
|
|
5970
|
-
destinationVisibility: visibility,
|
|
5971
5709
|
dispatchId: run.dispatch?.id,
|
|
5972
5710
|
dispatchOutcome: dispatchResult.outcome,
|
|
5973
5711
|
...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : void 0,
|
|
@@ -6069,7 +5807,7 @@ async function resumeSlackTurnInContext(args) {
|
|
|
6069
5807
|
}
|
|
6070
5808
|
if (deferredPauseHandler) try {
|
|
6071
5809
|
await deferredPauseHandler();
|
|
6072
|
-
if (deferredAuthInfo) await
|
|
5810
|
+
if (deferredAuthInfo) await postRunNoticeBestEffort(runArgs, buildAuthPauseResponse(deferredAuthInfo.actorId, deferredAuthInfo.providerDisplayName, deferredAuthInfo.requestText));
|
|
6073
5811
|
return true;
|
|
6074
5812
|
} catch (pauseError) {
|
|
6075
5813
|
await handleResumeFailure({
|
|
@@ -6262,15 +6000,22 @@ async function runPausedTurn(payload, options, runOptions = {}) {
|
|
|
6262
6000
|
return withLogContext({ conversationId: payload.conversationId }, () => runPausedTurnInContext(payload, options, runOptions));
|
|
6263
6001
|
}
|
|
6264
6002
|
async function runPausedTurnInContext(payload, options, runOptions) {
|
|
6265
|
-
const thread = parseSlackThreadId(payload.conversationId);
|
|
6266
6003
|
const destination = requireSlackDestination(payload.destination, "Paused turn");
|
|
6267
6004
|
const wakePausedTurn$1 = options.wakePausedTurn ?? wakePausedTurn;
|
|
6005
|
+
const routing = await resolveConversationRouting({ conversationId: payload.conversationId });
|
|
6006
|
+
const pausedTurn = await getTurnRecord(payload.conversationId, payload.turnId);
|
|
6007
|
+
const target = resolveSlackReplyTarget({
|
|
6008
|
+
destination,
|
|
6009
|
+
location: routing?.location,
|
|
6010
|
+
source: pausedTurn?.source
|
|
6011
|
+
});
|
|
6268
6012
|
return await resumeSlackTurn({
|
|
6269
6013
|
messageText: "",
|
|
6270
6014
|
conversationId: payload.conversationId,
|
|
6271
6015
|
turnId: payload.turnId,
|
|
6272
|
-
|
|
6273
|
-
|
|
6016
|
+
traceContext: payload.traceContext,
|
|
6017
|
+
channelId: target.channelId,
|
|
6018
|
+
...target.threadTs ? { threadTs: target.threadTs } : void 0,
|
|
6274
6019
|
executeTurn: async (run, saveResult) => await executeTurn(options.agentRunner, run, saveResult),
|
|
6275
6020
|
scheduleSessionCompletedPluginTasks: options.scheduleSessionCompletedPluginTasks,
|
|
6276
6021
|
beforeStart: async () => {
|
|
@@ -6483,6 +6228,23 @@ async function failStrandedTurnWithFallback(args) {
|
|
|
6483
6228
|
failureCode: "agent_run_failed",
|
|
6484
6229
|
turnId: failed.turnId
|
|
6485
6230
|
});
|
|
6231
|
+
let replyAttribution;
|
|
6232
|
+
if (failed.dispatchId) {
|
|
6233
|
+
const dispatch = await getDispatchRecord(failed.dispatchId);
|
|
6234
|
+
await recordTurnSummary({
|
|
6235
|
+
conversationId: args.conversationId,
|
|
6236
|
+
destination: dispatch?.destination,
|
|
6237
|
+
dispatchId: failed.dispatchId,
|
|
6238
|
+
dispatchOutcome: "failed",
|
|
6239
|
+
turnId: failed.turnId,
|
|
6240
|
+
sliceId: failed.sliceId,
|
|
6241
|
+
source: dispatch?.source,
|
|
6242
|
+
state: "failed",
|
|
6243
|
+
surface: failed.surface
|
|
6244
|
+
});
|
|
6245
|
+
if (!dispatch || !postsRunNotices(buildDispatchRoutingContext(dispatch).dispatch)) return true;
|
|
6246
|
+
replyAttribution = dispatch.replyAttribution;
|
|
6247
|
+
}
|
|
6486
6248
|
let routing;
|
|
6487
6249
|
try {
|
|
6488
6250
|
routing = await resolveTurnSessionRouting({ conversationId: args.conversationId });
|
|
@@ -6493,22 +6255,15 @@ async function failStrandedTurnWithFallback(args) {
|
|
|
6493
6255
|
});
|
|
6494
6256
|
return true;
|
|
6495
6257
|
}
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
dispatchOutcome: "failed",
|
|
6501
|
-
turnId: failed.turnId,
|
|
6502
|
-
sliceId: failed.sliceId,
|
|
6503
|
-
source: routing.source,
|
|
6504
|
-
state: "failed",
|
|
6505
|
-
surface: failed.surface
|
|
6258
|
+
const target = resolveSlackReplyTarget({
|
|
6259
|
+
destination: requireSlackDestination(routing.destination, "Stranded paused turn"),
|
|
6260
|
+
location: routing.location,
|
|
6261
|
+
source: failed.source
|
|
6506
6262
|
});
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
...thread?.threadTs ? { threadTs: thread.threadTs } : void 0,
|
|
6263
|
+
await sendSlackReply({
|
|
6264
|
+
...target,
|
|
6265
|
+
conversationId: args.conversationId,
|
|
6266
|
+
replyAttribution,
|
|
6512
6267
|
text: buildTurnFailureResponse(requireTurnFailureEventId(eventId, eventName))
|
|
6513
6268
|
});
|
|
6514
6269
|
return true;
|
|
@@ -6653,28 +6408,67 @@ async function commitAssistantMessage(args) {
|
|
|
6653
6408
|
logException(error, "conversation.assistant.message_persist.failed");
|
|
6654
6409
|
}
|
|
6655
6410
|
}
|
|
6656
|
-
// src/chat/
|
|
6657
|
-
function
|
|
6658
|
-
const
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6411
|
+
// src/chat/task-execution/web-steering.ts
|
|
6412
|
+
function steeringMessageFromWeb(message, actor) {
|
|
6413
|
+
const storedAttachments = readMessageAttachments(message.input.attachments);
|
|
6414
|
+
return {
|
|
6415
|
+
actor: {
|
|
6416
|
+
authorId: actor.userId,
|
|
6417
|
+
...actor.fullName ? { authorName: actor.fullName } : void 0
|
|
6418
|
+
},
|
|
6419
|
+
provenance: instructionProvenanceFor(actor),
|
|
6420
|
+
text: message.input.text.trim(),
|
|
6421
|
+
timestampMs: message.createdAtMs,
|
|
6422
|
+
...storedAttachments.length ? { storedAttachments } : void 0
|
|
6423
|
+
};
|
|
6664
6424
|
}
|
|
6665
6425
|
/**
|
|
6666
|
-
*
|
|
6426
|
+
* Drain steered web messages into the active web Turn.
|
|
6667
6427
|
*
|
|
6668
|
-
*
|
|
6669
|
-
*
|
|
6670
|
-
*
|
|
6428
|
+
* Only messages from the Turn Actor join. Steered messages from another
|
|
6429
|
+
* participant stay pending and start their own Turn next, so they never act
|
|
6430
|
+
* with this Actor's credentials.
|
|
6671
6431
|
*/
|
|
6672
|
-
function
|
|
6673
|
-
|
|
6432
|
+
function createWebSteeringDrain(args) {
|
|
6433
|
+
const { actor, author, context, conversation, turnId } = args;
|
|
6434
|
+
return async (accept) => {
|
|
6435
|
+
const accepted = [];
|
|
6436
|
+
await context.attempt.drain(async (pending) => {
|
|
6437
|
+
const joining = pending.filter((message) => message.source === "web" && message.input.authorId === actor.userId && legacyWebMailboxMetadataSchema.safeParse(message.input.metadata).success);
|
|
6438
|
+
if (joining.length === 0) return [];
|
|
6439
|
+
for (const message of joining) {
|
|
6440
|
+
const attachments = readMessageAttachments(message.input.attachments);
|
|
6441
|
+
upsertConversationMessage(conversation, {
|
|
6442
|
+
id: message.inboundMessageId,
|
|
6443
|
+
role: "user",
|
|
6444
|
+
text: normalizeConversationText(message.input.text),
|
|
6445
|
+
createdAtMs: message.createdAtMs,
|
|
6446
|
+
author,
|
|
6447
|
+
meta: {
|
|
6448
|
+
...attachments.length ? { attachments } : void 0,
|
|
6449
|
+
explicitMention: true,
|
|
6450
|
+
source: "web"
|
|
6451
|
+
}
|
|
6452
|
+
});
|
|
6453
|
+
}
|
|
6454
|
+
await persistConversationMessages({
|
|
6455
|
+
conversation,
|
|
6456
|
+
conversationId: context.conversationId
|
|
6457
|
+
});
|
|
6458
|
+
const saved = (await getTurnRecord(context.conversationId, turnId))?.piMessages ?? [];
|
|
6459
|
+
const messages = withoutSavedSteeringMessages(saved, joining.map((message) => steeringMessageFromWeb(message, actor)), buildSteeringPiMessage);
|
|
6460
|
+
if (messages.length > 0) {
|
|
6461
|
+
await accept(messages);
|
|
6462
|
+
accepted.push(...messages);
|
|
6463
|
+
}
|
|
6464
|
+
return joining.map((message) => message.inboundMessageId);
|
|
6465
|
+
});
|
|
6466
|
+
return accepted;
|
|
6467
|
+
};
|
|
6674
6468
|
}
|
|
6675
6469
|
// src/chat/task-execution/conversation-turn.ts
|
|
6676
6470
|
/** Run a mailbox Turn and store its assistant Messages. */
|
|
6677
|
-
function stableHex
|
|
6471
|
+
function stableHex(...parts) {
|
|
6678
6472
|
return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24);
|
|
6679
6473
|
}
|
|
6680
6474
|
function captureConversationTurnFailure(args) {
|
|
@@ -6953,7 +6747,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
|
6953
6747
|
if (stopSignal?.aborted) return await completeCancelledTurn();
|
|
6954
6748
|
const piMessages = await loadProjection({ conversationId: context.conversationId });
|
|
6955
6749
|
failureCode = "agent_run_failed";
|
|
6956
|
-
currentRunId = `conversation-run:${stableHex
|
|
6750
|
+
currentRunId = `conversation-run:${stableHex(turnId, String(startedAtMs))}`;
|
|
6957
6751
|
setTags({ runId: currentRunId });
|
|
6958
6752
|
const outcome = await executeTurn(agentRunner, {
|
|
6959
6753
|
conversationId: context.conversationId,
|
|
@@ -6982,6 +6776,13 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
|
6982
6776
|
},
|
|
6983
6777
|
delivery: deliverAssistantMessage,
|
|
6984
6778
|
durability: {
|
|
6779
|
+
...webActor ? { drainSteeringMessages: createWebSteeringDrain({
|
|
6780
|
+
actor: webActor,
|
|
6781
|
+
author,
|
|
6782
|
+
context,
|
|
6783
|
+
conversation,
|
|
6784
|
+
turnId
|
|
6785
|
+
}) } : void 0,
|
|
6985
6786
|
onInputCommitted: acknowledge,
|
|
6986
6787
|
shouldYield: context.shouldYield,
|
|
6987
6788
|
onSandboxRefChanged: async (nextSandboxRef) => {
|
|
@@ -7044,7 +6845,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
|
7044
6845
|
await maybePostAutomatedTurnLimitNotice({
|
|
7045
6846
|
conversationId: context.conversationId,
|
|
7046
6847
|
maxTurns: botConfig.maxConsecutiveAutomatedTurns,
|
|
7047
|
-
|
|
6848
|
+
source,
|
|
7048
6849
|
update: automatedTurnLimit
|
|
7049
6850
|
});
|
|
7050
6851
|
completedSuccessfully = reply.diagnostics.outcome === "success";
|
|
@@ -7156,11 +6957,13 @@ function createSlackSystemTurnDelivery(args) {
|
|
|
7156
6957
|
installation: { teamId: location.teamId },
|
|
7157
6958
|
state: args.state,
|
|
7158
6959
|
task: async () => await sendSlackReply({
|
|
6960
|
+
...resolveSlackReplyTarget({
|
|
6961
|
+
destination: location,
|
|
6962
|
+
location
|
|
6963
|
+
}),
|
|
7159
6964
|
cards,
|
|
7160
|
-
channelId: location.channelId,
|
|
7161
6965
|
conversationId,
|
|
7162
|
-
text
|
|
7163
|
-
...location.threadTs ? { threadTs: location.threadTs } : void 0
|
|
6966
|
+
text
|
|
7164
6967
|
})
|
|
7165
6968
|
});
|
|
7166
6969
|
} catch (error) {
|
|
@@ -7320,165 +7123,1156 @@ function createProductionConversationWorkOptions(options) {
|
|
|
7320
7123
|
waitUntil: options.waitUntil
|
|
7321
7124
|
});
|
|
7322
7125
|
}
|
|
7323
|
-
// src/chat/
|
|
7324
|
-
|
|
7325
|
-
|
|
7126
|
+
// src/chat/slack/location-url.ts
|
|
7127
|
+
/** Parse a Slack workspace subdomain used in `https://{domain}.slack.com` URLs. */
|
|
7128
|
+
function parseSlackTeamDomain(value) {
|
|
7129
|
+
if (typeof value !== "string") return void 0;
|
|
7130
|
+
const domain = value.trim().toLowerCase();
|
|
7131
|
+
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(domain)) return;
|
|
7132
|
+
return domain;
|
|
7326
7133
|
}
|
|
7327
|
-
|
|
7328
|
-
|
|
7134
|
+
/** Convert a Slack message ts into the `p…` segment used by archive URLs. */
|
|
7135
|
+
function archivePathFromThreadTs(threadTs) {
|
|
7136
|
+
const [seconds, fraction = ""] = threadTs.split(".");
|
|
7137
|
+
return `p${seconds}${(fraction ?? "").padEnd(6, "0").slice(0, 6)}`;
|
|
7329
7138
|
}
|
|
7330
7139
|
/**
|
|
7331
|
-
* Build a
|
|
7332
|
-
*
|
|
7333
|
-
* Retries with the same key must address the same Conversation before the
|
|
7334
|
-
* mailbox Message id is derived.
|
|
7140
|
+
* Build a direct Slack archive URL for a thread when the workspace domain is
|
|
7141
|
+
* known. Omits a link rather than falling back to `app_redirect`.
|
|
7335
7142
|
*/
|
|
7336
|
-
function
|
|
7337
|
-
|
|
7143
|
+
function buildSlackLocationUrl(args) {
|
|
7144
|
+
const channelId = parseSlackChannelId(args.channelId);
|
|
7145
|
+
const teamDomain = parseSlackTeamDomain(args.teamDomain);
|
|
7146
|
+
const threadTs = parseSlackMessageTs(args.threadTs);
|
|
7147
|
+
if (!channelId || !teamDomain || !threadTs) return void 0;
|
|
7148
|
+
const url = new URL(`https://${teamDomain}.slack.com/archives/${channelId}/${archivePathFromThreadTs(threadTs)}`);
|
|
7149
|
+
url.searchParams.set("thread_ts", threadTs);
|
|
7150
|
+
url.searchParams.set("cid", channelId);
|
|
7151
|
+
return url.toString();
|
|
7152
|
+
}
|
|
7153
|
+
// src/chat/slack/team-domain.ts
|
|
7154
|
+
const TEAM_DOMAIN_CACHE_TTL_MS = 864e5;
|
|
7155
|
+
const TEAM_DOMAIN_NEGATIVE_CACHE_TTL_MS = 6e4;
|
|
7156
|
+
const teamDomainCache = /* @__PURE__ */ new Map();
|
|
7157
|
+
const teamDomainInflight = /* @__PURE__ */ new Map();
|
|
7158
|
+
function readTeamDomainCache(teamId) {
|
|
7159
|
+
const hit = teamDomainCache.get(teamId);
|
|
7160
|
+
if (!hit) return void 0;
|
|
7161
|
+
if (hit.expiresAt < Date.now()) {
|
|
7162
|
+
teamDomainCache.delete(teamId);
|
|
7163
|
+
return;
|
|
7164
|
+
}
|
|
7165
|
+
return hit.kind === "hit" ? hit.domain : null;
|
|
7166
|
+
}
|
|
7167
|
+
function writeTeamDomainCache(teamId, domain) {
|
|
7168
|
+
if (domain) {
|
|
7169
|
+
teamDomainCache.set(teamId, {
|
|
7170
|
+
kind: "hit",
|
|
7171
|
+
domain,
|
|
7172
|
+
expiresAt: Date.now() + TEAM_DOMAIN_CACHE_TTL_MS
|
|
7173
|
+
});
|
|
7174
|
+
return;
|
|
7175
|
+
}
|
|
7176
|
+
teamDomainCache.set(teamId, {
|
|
7177
|
+
kind: "miss",
|
|
7178
|
+
expiresAt: Date.now() + TEAM_DOMAIN_NEGATIVE_CACHE_TTL_MS
|
|
7179
|
+
});
|
|
7180
|
+
}
|
|
7181
|
+
async function fetchSlackTeamDomain(teamId) {
|
|
7182
|
+
try {
|
|
7183
|
+
const response = await withSlackRetries(() => getSlackClient().team.info({ team: teamId }), 3, {
|
|
7184
|
+
action: "team.info",
|
|
7185
|
+
idempotent: true,
|
|
7186
|
+
spanAttributes: { "app.slack.team_id": teamId }
|
|
7187
|
+
});
|
|
7188
|
+
const responseTeamId = parseSlackTeamId(response.team?.id);
|
|
7189
|
+
const domain = parseSlackTeamDomain(response.team?.domain);
|
|
7190
|
+
if (!domain || !responseTeamId || responseTeamId !== teamId) {
|
|
7191
|
+
writeTeamDomainCache(teamId, void 0);
|
|
7192
|
+
return;
|
|
7193
|
+
}
|
|
7194
|
+
writeTeamDomainCache(teamId, domain);
|
|
7195
|
+
return domain;
|
|
7196
|
+
} catch {
|
|
7197
|
+
writeTeamDomainCache(teamId, void 0);
|
|
7198
|
+
return;
|
|
7199
|
+
}
|
|
7338
7200
|
}
|
|
7339
|
-
/**
|
|
7340
|
-
function
|
|
7341
|
-
|
|
7201
|
+
/** Resolve a workspace subdomain via `team.info`, cached by team id. */
|
|
7202
|
+
async function resolveSlackTeamDomain(teamId) {
|
|
7203
|
+
const parsedTeamId = parseSlackTeamId(teamId);
|
|
7204
|
+
if (!parsedTeamId) return void 0;
|
|
7205
|
+
const cached = readTeamDomainCache(parsedTeamId);
|
|
7206
|
+
if (cached !== void 0) return cached ?? void 0;
|
|
7207
|
+
const pending = teamDomainInflight.get(parsedTeamId);
|
|
7208
|
+
if (pending) return pending;
|
|
7209
|
+
const lookup = fetchSlackTeamDomain(parsedTeamId).finally(() => {
|
|
7210
|
+
teamDomainInflight.delete(parsedTeamId);
|
|
7211
|
+
});
|
|
7212
|
+
teamDomainInflight.set(parsedTeamId, lookup);
|
|
7213
|
+
return lookup;
|
|
7214
|
+
}
|
|
7215
|
+
/** Resolve workspace subdomains for the distinct team ids in one API response. */
|
|
7216
|
+
async function resolveSlackTeamDomains(teamIds) {
|
|
7217
|
+
const uniqueTeamIds = [...new Set([...teamIds].map((teamId) => parseSlackTeamId(teamId)).filter((teamId) => teamId !== void 0))];
|
|
7218
|
+
const resolved = await Promise.all(uniqueTeamIds.map(async (teamId) => {
|
|
7219
|
+
const domain = await resolveSlackTeamDomain(teamId);
|
|
7220
|
+
return domain ? [teamId, domain] : void 0;
|
|
7221
|
+
}));
|
|
7222
|
+
return new Map(resolved.filter((entry) => entry !== void 0));
|
|
7223
|
+
}
|
|
7224
|
+
// src/api/conversations/shared.ts
|
|
7225
|
+
/** Format a Slack destination without exposing private channel names. */
|
|
7226
|
+
function slackStatsLocationLabel(input) {
|
|
7227
|
+
const channelId = input.channel;
|
|
7228
|
+
if (!channelId) return void 0;
|
|
7229
|
+
if (input.channelNameRedacted && input.channelName) return input.channelName;
|
|
7230
|
+
const name = input.channelName?.replace(/^#/, "");
|
|
7231
|
+
if (channelId.startsWith("D")) return "Direct Message";
|
|
7232
|
+
if (channelId.startsWith("C")) return name ? `#${name}` : "Public Channel";
|
|
7233
|
+
if (channelId.startsWith("G")) {
|
|
7234
|
+
if (name?.startsWith("mpdm-")) return "Group DM";
|
|
7235
|
+
return "Private Channel";
|
|
7236
|
+
}
|
|
7237
|
+
return name || channelId;
|
|
7238
|
+
}
|
|
7239
|
+
/** Return the generic display label for a conversation surface. */
|
|
7240
|
+
function surfaceFallbackLabel(surface) {
|
|
7241
|
+
if (surface === "scheduler") return "Scheduler";
|
|
7242
|
+
if (surface === "api") return "API";
|
|
7243
|
+
if (surface === "internal") return "Internal";
|
|
7244
|
+
return "Conversation";
|
|
7245
|
+
}
|
|
7246
|
+
// src/api/conversations/projection.ts
|
|
7247
|
+
const PRIVATE_CONVERSATION_LABEL = "Private Conversation";
|
|
7248
|
+
function privateConversationLabel(slackConversation) {
|
|
7249
|
+
if (!slackConversation) return PRIVATE_CONVERSATION_LABEL;
|
|
7250
|
+
return slackConversation.visibility === "private" ? formatSlackConversationRedactedLabel(slackConversation) ?? PRIVATE_CONVERSATION_LABEL : PRIVATE_CONVERSATION_LABEL;
|
|
7251
|
+
}
|
|
7252
|
+
function surfaceFromConversationId(conversationId) {
|
|
7253
|
+
if (parseSlackThreadId(conversationId)) return "slack";
|
|
7254
|
+
if (conversationId.startsWith("scheduler:")) return "scheduler";
|
|
7255
|
+
if (conversationId.startsWith("api:")) return "api";
|
|
7256
|
+
return "internal";
|
|
7257
|
+
}
|
|
7258
|
+
function surfaceFromSource(source, conversationId) {
|
|
7259
|
+
if (source === "slack" || source === "api" || source === "web" || source === "scheduler") return source === "web" ? "api" : source;
|
|
7260
|
+
return surfaceFromConversationId(conversationId);
|
|
7261
|
+
}
|
|
7262
|
+
function locationUrlFromConversation(args) {
|
|
7263
|
+
const { conversation } = args;
|
|
7264
|
+
if (!args.canViewPrivateContent || conversation.location?.provider !== "slack") return;
|
|
7265
|
+
const teamDomain = args.teamDomainByTeamId?.get(conversation.location.teamId);
|
|
7266
|
+
const threadTs = conversation.location.threadTs;
|
|
7267
|
+
if (!teamDomain || !threadTs) return void 0;
|
|
7268
|
+
return buildSlackLocationUrl({
|
|
7269
|
+
channelId: conversation.location.channelId,
|
|
7270
|
+
teamDomain,
|
|
7271
|
+
threadTs
|
|
7272
|
+
});
|
|
7342
7273
|
}
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7274
|
+
function actorIdentityReport(actor) {
|
|
7275
|
+
if (!actor) return void 0;
|
|
7276
|
+
const identity = {
|
|
7277
|
+
...actor.email !== void 0 ? { email: actor.email } : void 0,
|
|
7278
|
+
...actor.fullName !== void 0 ? { fullName: actor.fullName } : void 0,
|
|
7279
|
+
...actor.slackUserId !== void 0 ? { slackUserId: actor.slackUserId } : void 0,
|
|
7280
|
+
...actor.slackUserName !== void 0 ? { slackUserName: actor.slackUserName } : void 0
|
|
7281
|
+
};
|
|
7282
|
+
return Object.keys(identity).length > 0 ? identity : void 0;
|
|
7283
|
+
}
|
|
7284
|
+
function statusFromConversation(conversation) {
|
|
7285
|
+
if (conversation.execution.status === "idle") return "completed";
|
|
7286
|
+
if (conversation.execution.status === "failed") return "failed";
|
|
7287
|
+
return "active";
|
|
7288
|
+
}
|
|
7289
|
+
function titleFromConversation(args) {
|
|
7290
|
+
const slackThread = parseSlackThreadId(args.conversation.conversationId);
|
|
7291
|
+
const effectiveChannelName = args.conversation.channelName;
|
|
7292
|
+
const slackConversation = resolveSlackConversationContextFromThreadId({
|
|
7293
|
+
threadId: args.conversation.conversationId,
|
|
7294
|
+
channelName: effectiveChannelName,
|
|
7295
|
+
...args.visibility ? { visibility: args.visibility } : void 0
|
|
7296
|
+
});
|
|
7297
|
+
return (args.canViewPrivateContent ? void 0 : privateConversationLabel(slackConversation)) ?? args.conversation.title ?? args.automationTitle ?? slackStatsLocationLabel({
|
|
7298
|
+
channel: slackThread?.channelId,
|
|
7299
|
+
channelName: effectiveChannelName
|
|
7300
|
+
}) ?? (args.conversation.source === "plugin" ? "Automation run" : fallbackShortTitle(args.firstHumanMessageText ?? "", surfaceFallbackLabel(args.surface)));
|
|
7301
|
+
}
|
|
7302
|
+
function channelNameFromConversation(conversation, canViewPrivateContent, visibility) {
|
|
7303
|
+
const effectiveChannelName = conversation.channelName;
|
|
7304
|
+
const slackThread = parseSlackThreadId(conversation.conversationId);
|
|
7305
|
+
if (!effectiveChannelName && !slackThread) return;
|
|
7306
|
+
const slackConversation = resolveSlackConversationContextFromThreadId({
|
|
7307
|
+
threadId: conversation.conversationId,
|
|
7308
|
+
channelName: effectiveChannelName,
|
|
7309
|
+
...visibility ? { visibility } : void 0
|
|
7310
|
+
});
|
|
7311
|
+
if (!canViewPrivateContent) return privateConversationLabel(slackConversation);
|
|
7312
|
+
return effectiveChannelName;
|
|
7313
|
+
}
|
|
7314
|
+
function channelNameRedactedFromConversation(conversation, canViewPrivateContent) {
|
|
7315
|
+
const effectiveChannelName = conversation.channelName;
|
|
7316
|
+
const slackThread = parseSlackThreadId(conversation.conversationId);
|
|
7317
|
+
if (!effectiveChannelName && !slackThread) return false;
|
|
7318
|
+
return !canViewPrivateContent;
|
|
7319
|
+
}
|
|
7320
|
+
/** Describe whether a viewer can read one conversation's retained events. */
|
|
7321
|
+
function conversationEventHistory(args) {
|
|
7322
|
+
if (args.transcriptPurgedAtMs !== void 0) return {
|
|
7323
|
+
status: "expired",
|
|
7324
|
+
expiredAt: new Date(args.transcriptPurgedAtMs).toISOString()
|
|
7325
|
+
};
|
|
7326
|
+
return args.canExposePayload ? { status: "available" } : {
|
|
7327
|
+
status: "redacted",
|
|
7328
|
+
reason: "non_public_conversation"
|
|
7353
7329
|
};
|
|
7354
7330
|
}
|
|
7355
|
-
/**
|
|
7356
|
-
function
|
|
7357
|
-
const
|
|
7331
|
+
/** Project one durable conversation and its SQL metrics into the REST summary. */
|
|
7332
|
+
function conversationSummaryFromStoredConversation(args) {
|
|
7333
|
+
const { conversation, durationMs, usage } = args;
|
|
7334
|
+
const canViewPrivateContent = args.access?.canViewPrivateContent ?? false;
|
|
7335
|
+
const accessVisibility = args.access?.visibility;
|
|
7336
|
+
const visibility = accessVisibility === "public" || accessVisibility === "private" ? accessVisibility : void 0;
|
|
7337
|
+
const surface = surfaceFromSource(conversation.source, conversation.conversationId);
|
|
7338
|
+
const actorIdentity = actorIdentityReport(conversation.actor);
|
|
7339
|
+
const locationUrl = locationUrlFromConversation({
|
|
7340
|
+
canViewPrivateContent,
|
|
7341
|
+
conversation,
|
|
7342
|
+
...args.teamDomainByTeamId ? { teamDomainByTeamId: args.teamDomainByTeamId } : void 0
|
|
7343
|
+
});
|
|
7344
|
+
const slackThread = parseSlackThreadId(conversation.conversationId);
|
|
7345
|
+
const channelName = channelNameFromConversation(conversation, canViewPrivateContent, visibility);
|
|
7346
|
+
const channelNameRedacted = channelNameRedactedFromConversation(conversation, canViewPrivateContent);
|
|
7358
7347
|
return {
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7348
|
+
conversationId: conversation.conversationId,
|
|
7349
|
+
cumulativeDurationMs: durationMs,
|
|
7350
|
+
displayTitle: titleFromConversation({
|
|
7351
|
+
automationTitle: args.automationTitle,
|
|
7352
|
+
canViewPrivateContent,
|
|
7353
|
+
conversation,
|
|
7354
|
+
firstHumanMessageText: args.firstHumanMessageText,
|
|
7355
|
+
surface,
|
|
7356
|
+
visibility
|
|
7357
|
+
}),
|
|
7358
|
+
isParticipant: args.access?.isParticipant ?? false,
|
|
7359
|
+
...visibility ? { visibility } : void 0,
|
|
7360
|
+
lastProgressAt: new Date(conversation.execution.updatedAtMs ?? conversation.updatedAtMs).toISOString(),
|
|
7361
|
+
lastSeenAt: new Date(conversation.lastActivityAtMs).toISOString(),
|
|
7362
|
+
startedAt: new Date(conversation.createdAtMs).toISOString(),
|
|
7363
|
+
status: statusFromConversation(conversation),
|
|
7364
|
+
surface,
|
|
7365
|
+
...args.auxiliaryCosts ? { auxiliaryCosts: args.auxiliaryCosts } : void 0,
|
|
7366
|
+
...usage ? { cumulativeUsage: usage } : void 0,
|
|
7367
|
+
...args.childAgentTotals ? { childAgentTotals: args.childAgentTotals } : void 0,
|
|
7368
|
+
...actorIdentity ? { actorIdentity } : void 0,
|
|
7369
|
+
...locationUrl ? { locationUrl } : void 0,
|
|
7370
|
+
archivedAt: args.archivedAtMs === void 0 ? null : new Date(args.archivedAtMs).toISOString(),
|
|
7371
|
+
...slackThread ? { channel: slackThread.channelId } : void 0,
|
|
7372
|
+
...channelName ? { channelName } : void 0,
|
|
7373
|
+
...channelNameRedacted ? { channelNameRedacted: true } : void 0,
|
|
7374
|
+
...args.locationId && !channelNameRedacted ? { locationId: args.locationId } : void 0
|
|
7364
7375
|
};
|
|
7365
7376
|
}
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7377
|
+
// src/api/conversations/usage.ts
|
|
7378
|
+
const usage = juniorConversations.usage;
|
|
7379
|
+
const componentFields = [
|
|
7380
|
+
"inputTokens",
|
|
7381
|
+
"outputTokens",
|
|
7382
|
+
"cachedInputTokens",
|
|
7383
|
+
"cacheCreationTokens"
|
|
7384
|
+
];
|
|
7385
|
+
function summedUsageNumber(field) {
|
|
7386
|
+
return sql`sum((${usage}->>${field})::double precision)::double precision`;
|
|
7387
|
+
}
|
|
7388
|
+
function summedCostNumber(field) {
|
|
7389
|
+
return sql`round(
|
|
7390
|
+
sum((${usage}->'cost'->>${field})::numeric),
|
|
7391
|
+
12
|
|
7392
|
+
)::double precision`;
|
|
7393
|
+
}
|
|
7394
|
+
function usageFromAggregate(row) {
|
|
7395
|
+
const components = {};
|
|
7396
|
+
let componentTotal = 0;
|
|
7397
|
+
for (const field of componentFields) {
|
|
7398
|
+
const value = row[field];
|
|
7399
|
+
if (value === null) continue;
|
|
7400
|
+
components[field] = value;
|
|
7401
|
+
componentTotal += value;
|
|
7402
|
+
}
|
|
7403
|
+
const cost = {
|
|
7404
|
+
...row.costInput === null ? void 0 : { input: row.costInput },
|
|
7405
|
+
...row.costOutput === null ? void 0 : { output: row.costOutput },
|
|
7406
|
+
...row.costCacheRead === null ? void 0 : { cacheRead: row.costCacheRead },
|
|
7407
|
+
...row.costCacheWrite === null ? void 0 : { cacheWrite: row.costCacheWrite },
|
|
7408
|
+
...row.costTotal === null ? void 0 : { total: row.costTotal }
|
|
7409
|
+
};
|
|
7410
|
+
const result = {
|
|
7411
|
+
...row.totalOnlyTokens === null ? components : { totalTokens: row.totalOnlyTokens + componentTotal },
|
|
7412
|
+
...row.reasoningTokens === null ? void 0 : { reasoningTokens: row.reasoningTokens },
|
|
7413
|
+
...Object.keys(cost).length === 0 ? void 0 : { cost }
|
|
7414
|
+
};
|
|
7415
|
+
return hasAgentTurnUsage(result) ? result : void 0;
|
|
7416
|
+
}
|
|
7417
|
+
/**
|
|
7418
|
+
* Aggregate saved runtime and usage in PostgreSQL. The total for each selected
|
|
7419
|
+
* Conversation includes itself and all its child agents at any depth.
|
|
7420
|
+
*/
|
|
7421
|
+
async function readConversationTreeMetricsFromSql(db, conversationIds) {
|
|
7422
|
+
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
7423
|
+
const hasComponents = sql`coalesce(
|
|
7424
|
+
${usage}->>'inputTokens',
|
|
7425
|
+
${usage}->>'outputTokens',
|
|
7426
|
+
${usage}->>'cachedInputTokens',
|
|
7427
|
+
${usage}->>'cacheCreationTokens'
|
|
7428
|
+
) is not null`;
|
|
7429
|
+
const isChildAgent = sql`(${conversationSubtreeColumns.conversationId} <> ${conversationSubtreeColumns.scopeConversationId})`;
|
|
7430
|
+
const rows = await db.select({
|
|
7431
|
+
scopeConversationId: conversationSubtreeColumns.scopeConversationId,
|
|
7432
|
+
isChildAgent,
|
|
7433
|
+
conversations: sql`count(*)::integer`.mapWith(Number),
|
|
7434
|
+
durationMs: sql`coalesce(sum(${juniorConversations.durationMs}), 0)::double precision`,
|
|
7435
|
+
inputTokens: summedUsageNumber("inputTokens"),
|
|
7436
|
+
outputTokens: summedUsageNumber("outputTokens"),
|
|
7437
|
+
cachedInputTokens: summedUsageNumber("cachedInputTokens"),
|
|
7438
|
+
cacheCreationTokens: summedUsageNumber("cacheCreationTokens"),
|
|
7439
|
+
totalOnlyTokens: sql`sum(
|
|
7440
|
+
case
|
|
7441
|
+
when not (${hasComponents})
|
|
7442
|
+
then (${usage}->>'totalTokens')::double precision
|
|
7443
|
+
end
|
|
7444
|
+
)::double precision`,
|
|
7445
|
+
reasoningTokens: summedUsageNumber("reasoningTokens"),
|
|
7446
|
+
costInput: summedCostNumber("input"),
|
|
7447
|
+
costOutput: summedCostNumber("output"),
|
|
7448
|
+
costCacheRead: summedCostNumber("cacheRead"),
|
|
7449
|
+
costCacheWrite: summedCostNumber("cacheWrite"),
|
|
7450
|
+
costTotal: summedCostNumber("total")
|
|
7451
|
+
}).from(juniorConversations).innerJoin(conversationSubtree(conversationIds), eq(conversationSubtreeColumns.conversationId, juniorConversations.conversationId)).groupBy(conversationSubtreeColumns.scopeConversationId, isChildAgent);
|
|
7452
|
+
const ownRows = /* @__PURE__ */ new Map();
|
|
7453
|
+
const childRows = /* @__PURE__ */ new Map();
|
|
7454
|
+
for (const row of rows) (row.isChildAgent ? childRows : ownRows).set(row.scopeConversationId, row);
|
|
7455
|
+
const result = /* @__PURE__ */ new Map();
|
|
7456
|
+
for (const [conversationId, own] of ownRows) {
|
|
7457
|
+
const ownUsage = usageFromAggregate(own);
|
|
7458
|
+
const child = childRows.get(conversationId);
|
|
7459
|
+
const childUsage = child ? usageFromAggregate(child) : void 0;
|
|
7460
|
+
const usage = addAgentTurnUsage(ownUsage, childUsage);
|
|
7461
|
+
result.set(conversationId, {
|
|
7462
|
+
durationMs: own.durationMs + (child?.durationMs ?? 0),
|
|
7463
|
+
...usage ? { usage } : void 0,
|
|
7464
|
+
...child ? { childAgents: {
|
|
7465
|
+
count: child.conversations,
|
|
7466
|
+
durationMs: child.durationMs,
|
|
7467
|
+
...childUsage ? { usage: childUsage } : void 0
|
|
7468
|
+
} } : void 0
|
|
7469
|
+
});
|
|
7470
|
+
}
|
|
7471
|
+
return result;
|
|
7472
|
+
}
|
|
7473
|
+
/**
|
|
7474
|
+
* Project the child agent part of tree metrics into the API shape. Return
|
|
7475
|
+
* nothing when the Conversation has no child agents.
|
|
7476
|
+
*/
|
|
7477
|
+
function childAgentTotalsReport(metrics, auxiliaryCosts) {
|
|
7478
|
+
const childAgents = metrics?.childAgents;
|
|
7479
|
+
if (!childAgents) return void 0;
|
|
7376
7480
|
return {
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
inboundMessageId: args.messageId,
|
|
7382
|
-
input: {
|
|
7383
|
-
authorId: args.actor.userId,
|
|
7384
|
-
text,
|
|
7385
|
-
...args.attachments?.length ? { attachments: args.attachments } : void 0,
|
|
7386
|
-
metadata: {
|
|
7387
|
-
authorEmail: normalizeEmail(args.actor.email),
|
|
7388
|
-
...args.actor.fullName && { authorFullName: args.actor.fullName },
|
|
7389
|
-
authorUserId: args.actor.userId,
|
|
7390
|
-
...args.actor.userName && { authorUserName: args.actor.userName },
|
|
7391
|
-
kind: "api_turn",
|
|
7392
|
-
messageId: args.messageId
|
|
7393
|
-
}
|
|
7394
|
-
},
|
|
7395
|
-
receivedAtMs: nowMs,
|
|
7396
|
-
source: "web"
|
|
7481
|
+
...auxiliaryCosts?.childAgentCostUsd === void 0 ? void 0 : { auxiliaryCostUsd: auxiliaryCosts.childAgentCostUsd },
|
|
7482
|
+
count: childAgents.count,
|
|
7483
|
+
cumulativeDurationMs: childAgents.durationMs,
|
|
7484
|
+
...childAgents.usage ? { cumulativeUsage: childAgents.usage } : void 0
|
|
7397
7485
|
};
|
|
7398
7486
|
}
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
actor: storedActorFromWeb(args.actor),
|
|
7415
|
-
...isNewRoot ? { source: "web" } : void 0,
|
|
7416
|
-
...source ? { sessionSource: source } : void 0,
|
|
7417
|
-
...isNewRoot ? { visibility } : void 0
|
|
7418
|
-
});
|
|
7419
|
-
return destination;
|
|
7487
|
+
// src/chat/conversations/fork.ts
|
|
7488
|
+
/** Event key of the agent note that follows the copied events in a fork. */
|
|
7489
|
+
const FORK_NOTE_KEY = "fork:note";
|
|
7490
|
+
const forkNote = alias(juniorConversationEvents, "fork_note");
|
|
7491
|
+
/**
|
|
7492
|
+
* SQL filter for event rows that a fork did not copy from its source. Usage
|
|
7493
|
+
* and cost reports use it, so copied model calls count only in the source.
|
|
7494
|
+
*/
|
|
7495
|
+
function notCopiedByFork() {
|
|
7496
|
+
return sql`not exists (
|
|
7497
|
+
select 1 from ${juniorConversationEvents} as ${sql.identifier("fork_note")}
|
|
7498
|
+
where ${forkNote.conversationId} = ${juniorConversationEvents.conversationId}
|
|
7499
|
+
and ${forkNote.idempotencyKey} = ${FORK_NOTE_KEY}
|
|
7500
|
+
and ${forkNote.seq} > ${juniorConversationEvents.seq}
|
|
7501
|
+
)`;
|
|
7420
7502
|
}
|
|
7421
|
-
/**
|
|
7422
|
-
|
|
7423
|
-
|
|
7503
|
+
/**
|
|
7504
|
+
* Events that hold credentials, approvals, provider connections, or plugin
|
|
7505
|
+
* state of the source actor. A fork does not copy them.
|
|
7506
|
+
*/
|
|
7507
|
+
const UNCOPIED_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
7508
|
+
"authorization_completed",
|
|
7509
|
+
"authorization_requested",
|
|
7510
|
+
"guardian_action_reviewed",
|
|
7511
|
+
"mcp_provider_connected",
|
|
7512
|
+
"mcp_provider_connected_unowned",
|
|
7513
|
+
"structured_event",
|
|
7514
|
+
"unknown"
|
|
7515
|
+
]);
|
|
7516
|
+
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.";
|
|
7517
|
+
/** An expected fork request failure, such as a reply without saved history. */
|
|
7518
|
+
var ConversationForkError = class extends Error {};
|
|
7519
|
+
/**
|
|
7520
|
+
* Create a new web root Conversation from the agent history of a source
|
|
7521
|
+
* Conversation, through one completed assistant reply.
|
|
7522
|
+
*
|
|
7523
|
+
* Retries with the same requester and key return the same Conversation.
|
|
7524
|
+
*/
|
|
7525
|
+
async function forkConversation(input) {
|
|
7424
7526
|
const conversationId = createConversationId({
|
|
7425
|
-
actorEmail: input.
|
|
7426
|
-
idempotencyKey:
|
|
7527
|
+
actorEmail: input.actorEmail,
|
|
7528
|
+
idempotencyKey: JSON.stringify([
|
|
7529
|
+
"fork",
|
|
7530
|
+
input.sourceConversationId,
|
|
7531
|
+
input.idempotencyKey
|
|
7532
|
+
])
|
|
7427
7533
|
});
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7534
|
+
const executor = getSqlExecutor();
|
|
7535
|
+
const events = getConversationEventStore();
|
|
7536
|
+
const store = getConversationStore();
|
|
7537
|
+
return withConversationEventLock(executor, input.sourceConversationId, () => withConversationEventLock(executor, conversationId, async () => {
|
|
7538
|
+
if (await store.get({ conversationId })) return { conversationId };
|
|
7539
|
+
const source = await store.get({ conversationId: input.sourceConversationId });
|
|
7540
|
+
if (!source || source.transcriptPurgedAtMs !== void 0) throw new ConversationForkError("The source history is no longer available.");
|
|
7541
|
+
if (source.parentConversationId) throw new ConversationForkError("You cannot fork a child conversation.");
|
|
7542
|
+
const boundary = await events.loadByIdempotencyKey(input.sourceConversationId, `message:${input.messageId}:agent`);
|
|
7543
|
+
const reply = boundary?.data.type === "assistant_message" ? piMessageFromHistoryItem(boundary.data) : void 0;
|
|
7544
|
+
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.");
|
|
7545
|
+
const projection = await loadTurnProjection({
|
|
7546
|
+
conversationId: input.sourceConversationId,
|
|
7547
|
+
committedSeq: boundary.seq,
|
|
7548
|
+
includeTail: false
|
|
7549
|
+
});
|
|
7550
|
+
if (!projection?.messages.length) throw new ConversationForkError("The selected history is no longer available.");
|
|
7551
|
+
if (hasUnfinishedToolCalls(projection.messages)) throw new ConversationForkError("This history has unfinished tool calls. Choose another reply.");
|
|
7552
|
+
const nowMs = Date.now();
|
|
7553
|
+
await recordWebConversationActivity({
|
|
7554
|
+
actor: webActorFromEmail(input.actorEmail, { ...input.actorName ? { fullName: input.actorName } : void 0 }),
|
|
7555
|
+
conversationId,
|
|
7556
|
+
nowMs,
|
|
7557
|
+
rootVisibility: source.visibility === "public" ? "public" : "private"
|
|
7558
|
+
});
|
|
7559
|
+
await executor.db().update(juniorConversations).set({
|
|
7560
|
+
forkedFromConversationId: source.conversationId,
|
|
7561
|
+
title: source.title ? `Fork: ${source.title}` : "Forked conversation"
|
|
7562
|
+
}).where(eq(juniorConversations.conversationId, conversationId));
|
|
7563
|
+
await copyForkEvents({
|
|
7564
|
+
sourceConversationId: source.conversationId,
|
|
7565
|
+
conversationId,
|
|
7566
|
+
events: await events.loadHistory(source.conversationId),
|
|
7567
|
+
boundarySeq: boundary.seq,
|
|
7568
|
+
messageId: input.messageId
|
|
7569
|
+
});
|
|
7570
|
+
await events.append(conversationId, [{
|
|
7571
|
+
idempotencyKey: FORK_NOTE_KEY,
|
|
7572
|
+
createdAtMs: nowMs,
|
|
7573
|
+
data: {
|
|
7574
|
+
type: "user_message",
|
|
7575
|
+
provenance: contextProvenance,
|
|
7576
|
+
content: [{
|
|
7577
|
+
type: "text",
|
|
7578
|
+
text: FORK_NOTE
|
|
7579
|
+
}],
|
|
7580
|
+
timestamp: nowMs
|
|
7581
|
+
}
|
|
7582
|
+
}]);
|
|
7583
|
+
return { conversationId };
|
|
7584
|
+
}));
|
|
7436
7585
|
}
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7586
|
+
/**
|
|
7587
|
+
* Copy the source event rows through the selected reply. Copies keep their
|
|
7588
|
+
* sequence, author, timestamp, key, and payload, so the fork shows the same
|
|
7589
|
+
* transcript and event log, and seq references in compactions stay valid.
|
|
7590
|
+
* Later updates of copied Messages and the end of copied Turns are also
|
|
7591
|
+
* copied, so that no copied item stays open in the fork.
|
|
7592
|
+
*/
|
|
7593
|
+
async function copyForkEvents(args) {
|
|
7594
|
+
const reply = args.events.find((event) => event.data.type === "message" && event.data.role === "assistant" && event.data.messageId === args.messageId);
|
|
7595
|
+
const cutoffSeq = Math.max(args.boundarySeq, reply?.seq ?? -1);
|
|
7596
|
+
const messageIds = /* @__PURE__ */ new Set();
|
|
7597
|
+
const turnIds = /* @__PURE__ */ new Set();
|
|
7598
|
+
const seqs = [];
|
|
7599
|
+
let historyVersion = 0;
|
|
7600
|
+
for (const event of args.events) {
|
|
7601
|
+
const data = event.data;
|
|
7602
|
+
if (UNCOPIED_EVENT_TYPES.has(data.type)) continue;
|
|
7603
|
+
if (event.idempotencyKey === FORK_NOTE_KEY) continue;
|
|
7604
|
+
if (event.seq <= cutoffSeq) {
|
|
7605
|
+
if (data.type === "message") messageIds.add(data.messageId);
|
|
7606
|
+
if (data.type === "turn_started") turnIds.add(data.turnId);
|
|
7607
|
+
historyVersion = Math.max(historyVersion, event.historyVersion);
|
|
7608
|
+
seqs.push(event.seq);
|
|
7609
|
+
} 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);
|
|
7610
|
+
}
|
|
7611
|
+
if (!seqs.length) return;
|
|
7612
|
+
const e = juniorConversationEvents;
|
|
7613
|
+
await getSqlExecutor().db().execute(sql`insert into ${e} (${sql.join([
|
|
7614
|
+
e.conversationId,
|
|
7615
|
+
e.seq,
|
|
7616
|
+
e.historyVersion,
|
|
7617
|
+
e.schemaVersion,
|
|
7618
|
+
e.idempotencyKey,
|
|
7619
|
+
e.type,
|
|
7620
|
+
e.payload,
|
|
7621
|
+
e.actorIdentityId,
|
|
7622
|
+
e.createdAt
|
|
7623
|
+
].map((column) => sql.identifier(column.name)), sql`, `)})
|
|
7624
|
+
select ${args.conversationId}, ${e.seq}, least(${e.historyVersion}, ${historyVersion}), ${e.schemaVersion}, ${e.idempotencyKey}, ${e.type}, ${e.payload}, ${e.actorIdentityId}, ${e.createdAt}
|
|
7625
|
+
from ${e}
|
|
7626
|
+
where ${e.conversationId} = ${args.sourceConversationId}
|
|
7627
|
+
and ${inArray(e.seq, seqs)}`);
|
|
7628
|
+
}
|
|
7629
|
+
function hasUnfinishedToolCalls(messages) {
|
|
7630
|
+
const pending = /* @__PURE__ */ new Set();
|
|
7631
|
+
for (const message of messages) if (message.role === "assistant") {
|
|
7632
|
+
for (const part of message.content) if (part.type === "toolCall") pending.add(part.id);
|
|
7633
|
+
} else if (message.role === "toolResult") pending.delete(message.toolCallId);
|
|
7634
|
+
return pending.size > 0;
|
|
7635
|
+
}
|
|
7636
|
+
// src/api/conversations/auxiliary-costs.ts
|
|
7637
|
+
function eventCost() {
|
|
7638
|
+
return sql`CASE
|
|
7639
|
+
WHEN ${juniorConversationEvents.type} = 'structured_event'
|
|
7640
|
+
AND jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
|
|
7641
|
+
THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::numeric
|
|
7642
|
+
WHEN ${juniorConversationEvents.type} IN ('guardian_action_reviewed', 'turn_routed')
|
|
7643
|
+
AND jsonb_typeof(${juniorConversationEvents.payload}->'costUsd') = 'number'
|
|
7644
|
+
THEN (${juniorConversationEvents.payload}->>'costUsd')::numeric
|
|
7645
|
+
ELSE NULL
|
|
7646
|
+
END`;
|
|
7647
|
+
}
|
|
7648
|
+
function addCostUsd(left, right) {
|
|
7649
|
+
return Math.round((left + right) * 0xe8d4a51000) / 0xe8d4a51000;
|
|
7650
|
+
}
|
|
7651
|
+
/**
|
|
7652
|
+
* Aggregate additive event costs by namespaced operation. The total for each
|
|
7653
|
+
* selected Conversation includes itself and all its child agents at any depth.
|
|
7654
|
+
*/
|
|
7655
|
+
async function readConversationAuxiliaryCostsFromSql(db, conversationIds) {
|
|
7656
|
+
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
7657
|
+
const scopeConversationId = conversationSubtreeColumns.scopeConversationId;
|
|
7658
|
+
const isChildAgent = sql`(${conversationSubtreeColumns.conversationId} <> ${scopeConversationId})`;
|
|
7659
|
+
const namespace = sql`CASE
|
|
7660
|
+
WHEN ${juniorConversationEvents.type} = 'structured_event'
|
|
7661
|
+
THEN ${juniorConversationEvents.payload}->>'namespace'
|
|
7662
|
+
ELSE 'junior'
|
|
7663
|
+
END`;
|
|
7664
|
+
const name = sql`CASE
|
|
7665
|
+
WHEN ${juniorConversationEvents.type} = 'structured_event'
|
|
7666
|
+
THEN ${juniorConversationEvents.payload}->>'name'
|
|
7667
|
+
ELSE ${juniorConversationEvents.type}
|
|
7668
|
+
END`;
|
|
7669
|
+
const cost = eventCost();
|
|
7670
|
+
const rows = await db.select({
|
|
7671
|
+
conversationId: scopeConversationId,
|
|
7672
|
+
childAgentCostUsd: sql`coalesce(round(sum(case when ${isChildAgent} then ${cost} end), 12), 0)::double precision`.mapWith(Number),
|
|
7673
|
+
costUsd: sql`round(sum(${cost}), 12)::double precision`.mapWith(Number),
|
|
7674
|
+
events: sql`count(*)::integer`.mapWith(Number),
|
|
7675
|
+
name,
|
|
7676
|
+
namespace
|
|
7677
|
+
}).from(juniorConversationEvents).innerJoin(conversationSubtree(conversationIds), eq(conversationSubtreeColumns.conversationId, juniorConversationEvents.conversationId)).where(and(or(eq(juniorConversationEvents.type, "structured_event"), inArray(juniorConversationEvents.type, ["guardian_action_reviewed", "turn_routed"])), sql`${cost} >= 0`, notCopiedByFork())).groupBy(scopeConversationId, namespace, name).orderBy(scopeConversationId, namespace, name);
|
|
7678
|
+
const result = /* @__PURE__ */ new Map();
|
|
7679
|
+
for (const row of rows) {
|
|
7680
|
+
const current = result.get(row.conversationId) ?? { total: {
|
|
7681
|
+
costUsd: 0,
|
|
7682
|
+
operations: []
|
|
7683
|
+
} };
|
|
7684
|
+
current.total.costUsd = addCostUsd(current.total.costUsd, row.costUsd);
|
|
7685
|
+
if (row.childAgentCostUsd > 0) current.childAgentCostUsd = addCostUsd(current.childAgentCostUsd ?? 0, row.childAgentCostUsd);
|
|
7686
|
+
current.total.operations.push({
|
|
7687
|
+
costUsd: row.costUsd,
|
|
7688
|
+
events: row.events,
|
|
7689
|
+
name: row.name,
|
|
7690
|
+
namespace: row.namespace
|
|
7691
|
+
});
|
|
7692
|
+
result.set(row.conversationId, current);
|
|
7693
|
+
}
|
|
7694
|
+
return result;
|
|
7695
|
+
}
|
|
7696
|
+
// src/chat/plugins/conversation-sidebar.ts
|
|
7697
|
+
/** Show native objects by default and let plugins select compact sidebar labels. */
|
|
7698
|
+
async function listConversationSidebarAnnotations(conversationIds, annotationsByConversation) {
|
|
7699
|
+
const candidates = new Set(conversationIds);
|
|
7700
|
+
const selected = {};
|
|
7701
|
+
const plugins = getPlugins();
|
|
7702
|
+
const customOwners = new Set(plugins.filter((plugin) => plugin.hooks?.conversationSidebar).map((plugin) => plugin.manifest.name));
|
|
7703
|
+
for (const conversationId of conversationIds) {
|
|
7704
|
+
const objects = (annotationsByConversation.get(conversationId) ?? []).flatMap((annotation) => {
|
|
7705
|
+
if (customOwners.has(annotation.plugin) || !annotation.objectType) return [];
|
|
7706
|
+
return [{
|
|
7707
|
+
key: annotation.key,
|
|
7708
|
+
label: annotation.label,
|
|
7709
|
+
objectType: annotation.objectType === "item" && annotation.kind === "object" && annotation.facts?.type === "deployment" ? "deployment" : annotation.objectType,
|
|
7710
|
+
status: annotation.status
|
|
7711
|
+
}];
|
|
7712
|
+
});
|
|
7713
|
+
if (objects.length) selected[conversationId] = objects;
|
|
7714
|
+
}
|
|
7715
|
+
for (const plugin of plugins) {
|
|
7716
|
+
const hook = plugin.hooks?.conversationSidebar;
|
|
7717
|
+
if (!hook) continue;
|
|
7718
|
+
const annotationsByConversationId = Object.fromEntries(conversationIds.flatMap((conversationId) => {
|
|
7719
|
+
const annotations = (annotationsByConversation.get(conversationId) ?? []).filter((annotation) => annotation.plugin === plugin.manifest.name);
|
|
7720
|
+
return annotations.length > 0 ? [[conversationId, annotations]] : [];
|
|
7721
|
+
}));
|
|
7722
|
+
try {
|
|
7723
|
+
const result = await hook({
|
|
7724
|
+
annotationsByConversationId,
|
|
7725
|
+
conversationIds,
|
|
7726
|
+
db: getDb(),
|
|
7727
|
+
log: createPluginLogger(plugin.manifest.name),
|
|
7728
|
+
plugin: { name: plugin.manifest.name },
|
|
7729
|
+
cache: createPluginCache(plugin.manifest.name)
|
|
7730
|
+
});
|
|
7731
|
+
for (const [conversationId, annotations] of Object.entries(result.annotationsByConversationId)) {
|
|
7732
|
+
if (!candidates.has(conversationId)) continue;
|
|
7733
|
+
const parsed = conversationSidebarAnnotationSchema.array().safeParse(annotations);
|
|
7734
|
+
if (!parsed.success || parsed.data.length === 0) continue;
|
|
7735
|
+
selected[conversationId] = [...selected[conversationId] ?? [], ...parsed.data];
|
|
7736
|
+
}
|
|
7737
|
+
} catch (error) {
|
|
7738
|
+
logWarn("plugin.conversation_sidebar.hook.failed", {
|
|
7739
|
+
"app.plugin.name": plugin.manifest.name,
|
|
7740
|
+
"exception.message": error instanceof Error ? error.message : String(error)
|
|
7741
|
+
});
|
|
7742
|
+
}
|
|
7743
|
+
}
|
|
7744
|
+
return selected;
|
|
7745
|
+
}
|
|
7746
|
+
// src/chat/plugins/unfinished-work.ts
|
|
7747
|
+
/**
|
|
7748
|
+
* Return assigned and unfinished plugin work for the candidate conversations.
|
|
7749
|
+
* Feed Priority combines these signals with conversation activity on the host.
|
|
7750
|
+
*/
|
|
7751
|
+
async function listConversationWork(conversationIds) {
|
|
7752
|
+
if (conversationIds.length === 0) return {
|
|
7753
|
+
assignedIds: [],
|
|
7754
|
+
finishedAtById: {},
|
|
7755
|
+
unfinishedIds: []
|
|
7756
|
+
};
|
|
7757
|
+
const candidates = new Set(conversationIds);
|
|
7758
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
7759
|
+
const finishedAtById = /* @__PURE__ */ new Map();
|
|
7760
|
+
const unfinished = /* @__PURE__ */ new Set();
|
|
7761
|
+
for (const plugin of getPlugins()) {
|
|
7762
|
+
const hook = plugin.hooks?.unfinishedWork;
|
|
7763
|
+
if (!hook) continue;
|
|
7764
|
+
try {
|
|
7765
|
+
const result = await hook({
|
|
7766
|
+
conversationIds,
|
|
7767
|
+
db: getDb(),
|
|
7768
|
+
log: createPluginLogger(plugin.manifest.name),
|
|
7769
|
+
plugin: { name: plugin.manifest.name },
|
|
7770
|
+
cache: createPluginCache(plugin.manifest.name)
|
|
7771
|
+
});
|
|
7772
|
+
for (const conversationId of result.conversationIds) {
|
|
7773
|
+
if (!candidates.has(conversationId)) continue;
|
|
7774
|
+
unfinished.add(conversationId);
|
|
7775
|
+
assigned.add(conversationId);
|
|
7776
|
+
}
|
|
7777
|
+
for (const conversationId of result.assignedConversationIds ?? []) if (candidates.has(conversationId)) assigned.add(conversationId);
|
|
7778
|
+
for (const [conversationId, finishedAt] of Object.entries(result.finishedWorkAtByConversationId ?? {})) {
|
|
7779
|
+
if (!candidates.has(conversationId)) continue;
|
|
7780
|
+
const time = Date.parse(finishedAt);
|
|
7781
|
+
if (!Number.isFinite(time)) continue;
|
|
7782
|
+
const current = finishedAtById.get(conversationId);
|
|
7783
|
+
if (!current || time > Date.parse(current)) finishedAtById.set(conversationId, finishedAt);
|
|
7784
|
+
}
|
|
7785
|
+
} catch (error) {
|
|
7786
|
+
logWarn("plugin.unfinished_work.hook.failed", {
|
|
7787
|
+
"app.plugin.name": plugin.manifest.name,
|
|
7788
|
+
"exception.message": error instanceof Error ? error.message : String(error)
|
|
7789
|
+
});
|
|
7790
|
+
}
|
|
7791
|
+
}
|
|
7792
|
+
return {
|
|
7793
|
+
assignedIds: conversationIds.filter((conversationId) => assigned.has(conversationId)),
|
|
7794
|
+
finishedAtById: Object.fromEntries(conversationIds.flatMap((conversationId) => {
|
|
7795
|
+
const finishedAt = finishedAtById.get(conversationId);
|
|
7796
|
+
return finishedAt ? [[conversationId, finishedAt]] : [];
|
|
7797
|
+
})),
|
|
7798
|
+
unfinishedIds: conversationIds.filter((conversationId) => unfinished.has(conversationId))
|
|
7799
|
+
};
|
|
7800
|
+
}
|
|
7801
|
+
// src/api/conversations/priority.ts
|
|
7802
|
+
/** Conversations with no known work leave Priority after this idle window. */
|
|
7803
|
+
const UNASSIGNED_PRIORITY_WINDOW_MS = 108e5;
|
|
7804
|
+
/** Unfinished work leaves Priority after this idle window. */
|
|
7805
|
+
const UNFINISHED_PRIORITY_WINDOW_MS = 1728e5;
|
|
7806
|
+
/**
|
|
7807
|
+
* Decide whether a conversation belongs in the dashboard Priority section.
|
|
7808
|
+
*
|
|
7809
|
+
* Priority includes only:
|
|
7810
|
+
* - unfinished work last seen within 48 hours
|
|
7811
|
+
* - finished assigned work with a user message after the finish time
|
|
7812
|
+
* - no known work last seen within 3 hours
|
|
7813
|
+
*
|
|
7814
|
+
* Finished assigned work with no later activity stays out of Priority.
|
|
7815
|
+
*/
|
|
7816
|
+
function isConversationPriority(conversation, nowMs) {
|
|
7817
|
+
const lastSeenAt = Date.parse(conversation.lastSeenAt);
|
|
7818
|
+
if (!Number.isFinite(lastSeenAt)) return false;
|
|
7819
|
+
if (conversation.unfinishedWork) return nowMs - lastSeenAt <= UNFINISHED_PRIORITY_WINDOW_MS;
|
|
7820
|
+
if (conversation.assignedWork) {
|
|
7821
|
+
const finishedAt = Date.parse(conversation.finishedWorkAt ?? "");
|
|
7822
|
+
const lastUserMessageAt = Date.parse(conversation.lastUserMessageAt ?? "");
|
|
7823
|
+
return Number.isFinite(finishedAt) && Number.isFinite(lastUserMessageAt) && lastUserMessageAt > finishedAt;
|
|
7824
|
+
}
|
|
7825
|
+
return nowMs - lastSeenAt <= UNASSIGNED_PRIORITY_WINDOW_MS;
|
|
7826
|
+
}
|
|
7827
|
+
// src/api/conversations/user-message-activity.ts
|
|
7828
|
+
/** Read the latest true user message time for each selected conversation. */
|
|
7829
|
+
async function readLastUserMessageAtByConversation(db, conversationIds) {
|
|
7830
|
+
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
7831
|
+
const authority = sql`${juniorConversationEvents.payload}->'provenance'->>'authority'`;
|
|
7832
|
+
const actorPlatform = sql`${juniorConversationEvents.payload}->'provenance'->'actor'->>'platform'`;
|
|
7833
|
+
const role = sql`${juniorConversationEvents.payload}->>'role'`;
|
|
7834
|
+
const rows = await db.select({
|
|
7835
|
+
conversationId: juniorConversationEvents.conversationId,
|
|
7836
|
+
lastUserMessageAt: sql`max(${juniorConversationEvents.createdAt})`
|
|
7837
|
+
}).from(juniorConversationEvents).where(and(inArray(juniorConversationEvents.conversationId, conversationIds), or(and(eq(juniorConversationEvents.type, "user_message"), eq(authority, "instruction"), sql`${actorPlatform} in ('slack', 'local', 'web')`), and(eq(juniorConversationEvents.type, "message"), eq(role, "user"), isNotNull(juniorConversationEvents.actorIdentityId))))).groupBy(juniorConversationEvents.conversationId);
|
|
7838
|
+
return new Map(rows.map((row) => [row.conversationId, new Date(row.lastUserMessageAt).toISOString()]));
|
|
7839
|
+
}
|
|
7840
|
+
// src/api/conversations/activity-preview.ts
|
|
7841
|
+
/** Read one recent assistant message, or the latest user message, for each conversation. */
|
|
7842
|
+
async function readConversationActivityPreviews(db, conversationIds) {
|
|
7843
|
+
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
7844
|
+
const role = sql`${juniorConversationEvents.payload}->>'role'`;
|
|
7845
|
+
const text = sql`${juniorConversationEvents.payload}->>'text'`;
|
|
7846
|
+
const assistantFirst = sql`case when ${role} = 'assistant' then 0 else 1 end`;
|
|
7847
|
+
const rows = await db.selectDistinctOn([juniorConversationEvents.conversationId], {
|
|
7848
|
+
conversationId: juniorConversationEvents.conversationId,
|
|
7849
|
+
createdAt: juniorConversationEvents.createdAt,
|
|
7850
|
+
role: role.as("role"),
|
|
7851
|
+
text: text.as("text")
|
|
7852
|
+
}).from(juniorConversationEvents).where(and(inArray(juniorConversationEvents.conversationId, conversationIds), eq(juniorConversationEvents.type, "message"), sql`${role} in ('assistant', 'user')`, isNotNull(sql`nullif(btrim(${text}), '')`))).orderBy(juniorConversationEvents.conversationId, assistantFirst, desc(juniorConversationEvents.createdAt), desc(juniorConversationEvents.seq));
|
|
7853
|
+
return new Map(rows.map((row) => [row.conversationId, {
|
|
7854
|
+
createdAt: row.createdAt.toISOString(),
|
|
7855
|
+
role: row.role,
|
|
7856
|
+
text: row.text
|
|
7857
|
+
}]));
|
|
7858
|
+
}
|
|
7859
|
+
// src/api/conversations/first-message.ts
|
|
7860
|
+
/**
|
|
7861
|
+
* Read the text of the first human message for each untitled conversation.
|
|
7862
|
+
* Read-time titles use it while the generated title is still unset.
|
|
7863
|
+
*/
|
|
7864
|
+
async function readFirstHumanMessageTexts(db, conversations) {
|
|
7865
|
+
const untitledIds = conversations.flatMap((conversation) => conversation.title ? [] : [conversation.conversationId]);
|
|
7866
|
+
if (untitledIds.length === 0) return /* @__PURE__ */ new Map();
|
|
7867
|
+
const text = sql`${juniorConversationEvents.payload}->>'text'`;
|
|
7868
|
+
const rows = await db.selectDistinctOn([juniorConversationEvents.conversationId], {
|
|
7869
|
+
conversationId: juniorConversationEvents.conversationId,
|
|
7870
|
+
text: text.as("text")
|
|
7871
|
+
}).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));
|
|
7872
|
+
return new Map(rows.map((row) => [row.conversationId, row.text]));
|
|
7873
|
+
}
|
|
7874
|
+
// src/api/mentions.ts
|
|
7875
|
+
const SLACK_USER_MENTION_RE = /<@([UW][A-Z0-9]+)(?:\|[^<>]*)?>/g;
|
|
7876
|
+
function mentionedSlackUserIds(texts) {
|
|
7877
|
+
const ids = texts.flatMap((text) => text ? [...text.matchAll(SLACK_USER_MENTION_RE)].map((m) => m[1]) : []);
|
|
7878
|
+
return [...new Set(ids)];
|
|
7879
|
+
}
|
|
7880
|
+
/**
|
|
7881
|
+
* Resolve the people that each source mentions, in source order. Every API
|
|
7882
|
+
* that returns display text uses this, so the dashboard can show the same
|
|
7883
|
+
* names and People links everywhere. It does one identity query per Slack
|
|
7884
|
+
* workspace.
|
|
7885
|
+
*/
|
|
7886
|
+
async function resolveMentions(executor, sources) {
|
|
7887
|
+
const idsBySource = sources.map((source) => mentionedSlackUserIds(source.texts));
|
|
7888
|
+
const idsByTenant = /* @__PURE__ */ new Map();
|
|
7889
|
+
sources.forEach((source, index) => {
|
|
7890
|
+
if (idsBySource[index].length === 0) return;
|
|
7891
|
+
const ids = idsByTenant.get(source.tenantId) ?? /* @__PURE__ */ new Set();
|
|
7892
|
+
for (const id of idsBySource[index]) ids.add(id);
|
|
7893
|
+
idsByTenant.set(source.tenantId, ids);
|
|
7445
7894
|
});
|
|
7446
|
-
const
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7895
|
+
const peopleByTenant = new Map(await Promise.all([...idsByTenant].map(async ([tenantId, ids]) => [tenantId, await readMentionedPeople(executor, tenantId, [...ids])])));
|
|
7896
|
+
return sources.map((source, index) => {
|
|
7897
|
+
const people = peopleByTenant.get(source.tenantId);
|
|
7898
|
+
return idsBySource[index].flatMap((id) => {
|
|
7899
|
+
const person = people?.get(id);
|
|
7900
|
+
return person ? [person] : [];
|
|
7901
|
+
});
|
|
7452
7902
|
});
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7903
|
+
}
|
|
7904
|
+
/**
|
|
7905
|
+
* Read current names for mentioned Slack users. Known Junior identities come
|
|
7906
|
+
* first. The email is set only for a linked, verified Junior user, because
|
|
7907
|
+
* that email is the People profile route.
|
|
7908
|
+
*/
|
|
7909
|
+
async function readMentionedPeople(executor, tenantId, userIds) {
|
|
7910
|
+
const rows = await executor.db().select({
|
|
7911
|
+
displayName: juniorIdentities.displayName,
|
|
7912
|
+
emailVerified: juniorIdentities.emailVerified,
|
|
7913
|
+
handle: juniorIdentities.handle,
|
|
7914
|
+
providerSubjectId: juniorIdentities.providerSubjectId,
|
|
7915
|
+
userDisplayName: juniorUsers.displayName,
|
|
7916
|
+
userEmail: juniorUsers.primaryEmail
|
|
7917
|
+
}).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));
|
|
7918
|
+
const people = /* @__PURE__ */ new Map();
|
|
7919
|
+
for (const row of rows) {
|
|
7920
|
+
if (people.has(row.providerSubjectId)) continue;
|
|
7921
|
+
const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
|
|
7922
|
+
const email = row.emailVerified ? row.userEmail?.trim() : void 0;
|
|
7923
|
+
people.set(row.providerSubjectId, {
|
|
7924
|
+
slackUserId: row.providerSubjectId,
|
|
7925
|
+
...fullName ? { fullName } : void 0,
|
|
7926
|
+
...row.handle ? { slackUserName: row.handle } : void 0,
|
|
7927
|
+
...email ? { email } : void 0
|
|
7460
7928
|
});
|
|
7461
7929
|
}
|
|
7462
|
-
const
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7930
|
+
const unnamed = userIds.filter((id) => {
|
|
7931
|
+
const person = people.get(id);
|
|
7932
|
+
return !person?.fullName && !person?.slackUserName;
|
|
7933
|
+
});
|
|
7934
|
+
if (tenantId && unnamed.length > 0) {
|
|
7935
|
+
const profiles = await Promise.all(unnamed.map((id) => lookupSlackUser(tenantId, id)));
|
|
7936
|
+
unnamed.forEach((id, index) => {
|
|
7937
|
+
const profile = profiles[index];
|
|
7938
|
+
if (!profile?.fullName && !profile?.userName) return;
|
|
7939
|
+
people.set(id, {
|
|
7940
|
+
...people.get(id),
|
|
7941
|
+
slackUserId: id,
|
|
7942
|
+
...profile.fullName ? { fullName: profile.fullName } : void 0,
|
|
7943
|
+
...profile.userName ? { slackUserName: profile.userName } : void 0
|
|
7944
|
+
});
|
|
7945
|
+
});
|
|
7946
|
+
}
|
|
7947
|
+
return people;
|
|
7948
|
+
}
|
|
7949
|
+
// src/api/conversations/mentions.ts
|
|
7950
|
+
/** Text in one event that the dashboard renders with mentions. */
|
|
7951
|
+
function eventMentionTexts(data) {
|
|
7952
|
+
switch (data.type) {
|
|
7953
|
+
case "message": return data.redacted ? [] : [
|
|
7954
|
+
data.text,
|
|
7955
|
+
data.trustedSummary,
|
|
7956
|
+
...(data.events ?? []).map((event) => event.trustedSummary)
|
|
7957
|
+
];
|
|
7958
|
+
case "assistant_message": return data.parts.map((part) => part.text);
|
|
7959
|
+
case "compaction":
|
|
7960
|
+
case "handoff": return [data.summary];
|
|
7961
|
+
case "structured_event": return [data.presentation.preview, ...(data.presentation.details ?? []).map((detail) => detail.description)];
|
|
7962
|
+
default: return [];
|
|
7963
|
+
}
|
|
7964
|
+
}
|
|
7965
|
+
function briefMentionTexts(brief) {
|
|
7966
|
+
return [
|
|
7967
|
+
brief.summary,
|
|
7968
|
+
brief.intent,
|
|
7969
|
+
brief.outcome.text,
|
|
7970
|
+
...brief.decisions.flatMap((decision) => [decision.text, decision.by]),
|
|
7971
|
+
...brief.openDecisions.flatMap((decision) => [decision.text, decision.owner]),
|
|
7972
|
+
...brief.facts
|
|
7973
|
+
];
|
|
7974
|
+
}
|
|
7975
|
+
function withMentions(value, mentions) {
|
|
7976
|
+
return mentions.length > 0 ? {
|
|
7977
|
+
...value,
|
|
7978
|
+
mentions
|
|
7979
|
+
} : value;
|
|
7980
|
+
}
|
|
7981
|
+
/**
|
|
7982
|
+
* Attach the people that each event and the Brief of one conversation
|
|
7983
|
+
* mention, resolved at read time.
|
|
7984
|
+
*/
|
|
7985
|
+
async function attachConversationMentions(executor, conversationId, content) {
|
|
7986
|
+
const tenantId = (await loadConversationSlackTenantIds(executor, [conversationId])).get(conversationId);
|
|
7987
|
+
const sources = content.events.map((event) => ({
|
|
7988
|
+
tenantId,
|
|
7989
|
+
texts: eventMentionTexts(event.data)
|
|
7990
|
+
}));
|
|
7991
|
+
if (content.brief) sources.push({
|
|
7992
|
+
tenantId,
|
|
7993
|
+
texts: briefMentionTexts(content.brief)
|
|
7476
7994
|
});
|
|
7477
|
-
const
|
|
7995
|
+
const mentions = await resolveMentions(executor, sources);
|
|
7996
|
+
return {
|
|
7997
|
+
briefMentions: content.brief ? mentions.at(-1) : [],
|
|
7998
|
+
events: content.events.map((event, index) => withMentions(event, mentions[index]))
|
|
7999
|
+
};
|
|
8000
|
+
}
|
|
8001
|
+
/** Attach the people that each activity preview mentions, resolved at read time. */
|
|
8002
|
+
async function attachActivityPreviewMentions(executor, previews) {
|
|
8003
|
+
const entries = [...previews];
|
|
8004
|
+
const tenants = await loadConversationSlackTenantIds(executor, entries.map(([conversationId]) => conversationId));
|
|
8005
|
+
const mentions = await resolveMentions(executor, entries.map(([conversationId, preview]) => ({
|
|
8006
|
+
tenantId: tenants.get(conversationId),
|
|
8007
|
+
texts: [preview.text]
|
|
8008
|
+
})));
|
|
8009
|
+
return new Map(entries.map(([conversationId, preview], index) => [conversationId, withMentions(preview, mentions[index])]));
|
|
8010
|
+
}
|
|
8011
|
+
// src/api/conversations/participants.ts
|
|
8012
|
+
const identityColumns = {
|
|
8013
|
+
displayName: juniorIdentities.displayName,
|
|
8014
|
+
email: juniorIdentities.email,
|
|
8015
|
+
handle: juniorIdentities.handle,
|
|
8016
|
+
identityId: juniorIdentities.id,
|
|
8017
|
+
provider: juniorIdentities.provider,
|
|
8018
|
+
providerSubjectId: juniorIdentities.providerSubjectId,
|
|
8019
|
+
userDisplayName: juniorUsers.displayName,
|
|
8020
|
+
userEmail: juniorUsers.primaryEmail,
|
|
8021
|
+
userId: juniorIdentities.userId
|
|
8022
|
+
};
|
|
8023
|
+
function participantIdentity(row) {
|
|
8024
|
+
const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
|
|
8025
|
+
const email = row.email?.trim() || row.userEmail?.trim();
|
|
8026
|
+
const slack = row.provider === "slack";
|
|
8027
|
+
const participant = {
|
|
8028
|
+
...email ? { email } : void 0,
|
|
8029
|
+
...fullName ? { fullName } : void 0,
|
|
8030
|
+
...slack && row.providerSubjectId ? { slackUserId: row.providerSubjectId } : void 0,
|
|
8031
|
+
...slack && row.handle ? { slackUserName: row.handle } : void 0
|
|
8032
|
+
};
|
|
8033
|
+
return Object.keys(participant).length > 0 ? participant : void 0;
|
|
8034
|
+
}
|
|
8035
|
+
/** Read actor identities in first-appearance order for each Conversation. */
|
|
8036
|
+
async function readConversationParticipants(db, conversationIds) {
|
|
8037
|
+
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
8038
|
+
const [rootRows, eventRows] = await Promise.all([db.select({
|
|
8039
|
+
conversationId: juniorConversations.conversationId,
|
|
8040
|
+
...identityColumns
|
|
8041
|
+
}).from(juniorConversations).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(inArray(juniorConversations.conversationId, [...conversationIds])), db.selectDistinctOn([juniorConversationEvents.conversationId, juniorConversationEvents.actorIdentityId], {
|
|
8042
|
+
conversationId: juniorConversationEvents.conversationId,
|
|
8043
|
+
seq: juniorConversationEvents.seq,
|
|
8044
|
+
...identityColumns
|
|
8045
|
+
}).from(juniorConversationEvents).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(inArray(juniorConversationEvents.conversationId, [...conversationIds])).orderBy(asc(juniorConversationEvents.conversationId), asc(juniorConversationEvents.actorIdentityId), asc(juniorConversationEvents.seq))]);
|
|
8046
|
+
const ordered = /* @__PURE__ */ new Map();
|
|
8047
|
+
const add = (conversationId, row) => {
|
|
8048
|
+
const participant = participantIdentity(row);
|
|
8049
|
+
if (!participant) return;
|
|
8050
|
+
const group = ordered.get(conversationId) ?? {
|
|
8051
|
+
identities: [],
|
|
8052
|
+
keys: /* @__PURE__ */ new Set()
|
|
8053
|
+
};
|
|
8054
|
+
const key = row.userId ? `user:${row.userId}` : `identity:${row.identityId}`;
|
|
8055
|
+
if (!group.keys.has(key)) {
|
|
8056
|
+
group.keys.add(key);
|
|
8057
|
+
group.identities.push(participant);
|
|
8058
|
+
}
|
|
8059
|
+
ordered.set(conversationId, group);
|
|
8060
|
+
};
|
|
8061
|
+
for (const row of rootRows) add(row.conversationId, row);
|
|
8062
|
+
for (const row of eventRows.sort((left, right) => left.seq - right.seq)) add(row.conversationId, row);
|
|
8063
|
+
return new Map([...ordered].map(([conversationId, group]) => [conversationId, group.identities]));
|
|
8064
|
+
}
|
|
8065
|
+
// src/api/conversations/list.ts
|
|
8066
|
+
const CONVERSATION_FEED_LIMIT = 50;
|
|
8067
|
+
function conversationFeedMembershipFilter(status, filter) {
|
|
8068
|
+
if (!filter) return status === "archived" ? sql`false` : void 0;
|
|
8069
|
+
if (filter.kind === "viewer") return and(conversationIsMemberRoot({ userId: filter.userId }), status === "archived" ? conversationArchivedForUser(filter.userId) : status === "all" ? void 0 : conversationNotArchivedForUser(filter.userId));
|
|
8070
|
+
return and(conversationIsMemberRoot({ email: filter.email }), status === "archived" ? conversationArchivedForEmail(filter.email) : status === "all" ? void 0 : conversationNotArchivedForEmail(filter.email));
|
|
8071
|
+
}
|
|
8072
|
+
/**
|
|
8073
|
+
* Keep root conversations linked to one code repository. A code change can
|
|
8074
|
+
* link a child conversation, so linked ids resolve to their root. Private
|
|
8075
|
+
* roots stay visible only to their members.
|
|
8076
|
+
*/
|
|
8077
|
+
function conversationCodeRepositoryFilter(repositoryId, viewerUserId) {
|
|
8078
|
+
return and(sql`${juniorConversations.conversationId} IN (
|
|
8079
|
+
SELECT coalesce(linked.root_conversation_id, linked.conversation_id)
|
|
8080
|
+
FROM ${juniorConversations} AS linked
|
|
8081
|
+
WHERE linked.conversation_id IN (
|
|
8082
|
+
SELECT unnest(${juniorCodeChanges.conversationIds})
|
|
8083
|
+
FROM ${juniorCodeChanges}
|
|
8084
|
+
WHERE ${juniorCodeChanges.repositoryId} = ${repositoryId}
|
|
8085
|
+
)
|
|
8086
|
+
)`, or(eq(juniorDestinations.visibility, "public"), viewerUserId ? viewerConversationMembership({
|
|
8087
|
+
actorMatch: eq(juniorIdentities.userId, viewerUserId),
|
|
8088
|
+
participantMatch: conversationHasParticipantUser(viewerUserId)
|
|
8089
|
+
}) : void 0));
|
|
8090
|
+
}
|
|
8091
|
+
async function conversationRows(db, limit, status, filter, query, includePrivateBriefs = false, codeRepository) {
|
|
8092
|
+
return db.select({
|
|
8093
|
+
conversation: conversationReadColumns(),
|
|
8094
|
+
location: juniorDestinations,
|
|
8095
|
+
identityDisplayName: juniorIdentities.displayName,
|
|
8096
|
+
identityEmail: juniorIdentities.email,
|
|
8097
|
+
identityHandle: juniorIdentities.handle,
|
|
8098
|
+
identityProvider: juniorIdentities.provider,
|
|
8099
|
+
identitySubjectId: juniorIdentities.providerSubjectId,
|
|
8100
|
+
identityTenantId: juniorIdentities.providerTenantId,
|
|
8101
|
+
userDisplayName: juniorUsers.displayName
|
|
8102
|
+
}).from(juniorConversations).leftJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(isNull(juniorConversations.parentConversationId), conversationFeedMembershipFilter(status, filter), codeRepository ? conversationCodeRepositoryFilter(codeRepository.id, codeRepository.viewerUserId) : void 0, query ? or(sql`strpos(lower(coalesce(${juniorConversations.title}, '')), ${query}) > 0`, sql`exists (
|
|
8103
|
+
select 1
|
|
8104
|
+
from ${juniorConversationBriefs}
|
|
8105
|
+
where ${juniorConversationBriefs.conversationId} = ${juniorConversations.conversationId}
|
|
8106
|
+
and ${includePrivateBriefs ? sql`true` : eq(juniorDestinations.visibility, "public")}
|
|
8107
|
+
and ${juniorConversationBriefs.version} = (
|
|
8108
|
+
select max(${juniorConversationBriefs.version})
|
|
8109
|
+
from ${juniorConversationBriefs}
|
|
8110
|
+
where ${juniorConversationBriefs.conversationId} = ${juniorConversations.conversationId}
|
|
8111
|
+
)
|
|
8112
|
+
and to_tsvector('english', ${juniorConversationBriefs.searchText})
|
|
8113
|
+
@@ websearch_to_tsquery('english', ${query})
|
|
8114
|
+
)`) : void 0)).orderBy(desc(juniorConversations.lastActivityAt), asc(juniorConversations.conversationId)).limit(limit);
|
|
8115
|
+
}
|
|
8116
|
+
/** Decode a conversation row with the linked user name and identity-scoped provider fields. */
|
|
8117
|
+
function conversationFromRow(row) {
|
|
8118
|
+
const value = row.conversation;
|
|
8119
|
+
const legacySessionSource = value.sessionSource === null ? void 0 : parseSessionSource(value.sessionSource);
|
|
8120
|
+
if (value.sessionSource !== null && !legacySessionSource) throw new Error("Conversation legacy source_json is invalid");
|
|
8121
|
+
const actorFullName = row.userDisplayName?.trim() ? row.userDisplayName : row.identityDisplayName;
|
|
8122
|
+
const actor = row.identityProvider === "slack" ? {
|
|
8123
|
+
platform: "slack",
|
|
8124
|
+
...row.identityEmail ? { email: row.identityEmail } : void 0,
|
|
8125
|
+
...actorFullName ? { fullName: actorFullName } : void 0,
|
|
8126
|
+
...row.identitySubjectId ? { slackUserId: row.identitySubjectId } : void 0,
|
|
8127
|
+
...row.identityHandle ? { slackUserName: row.identityHandle } : void 0,
|
|
8128
|
+
...row.identityTenantId ? { teamId: row.identityTenantId } : void 0
|
|
8129
|
+
} : void 0;
|
|
8130
|
+
const location = locationFromRow(value.location, row.location, legacySessionSource);
|
|
8131
|
+
return {
|
|
8132
|
+
schemaVersion: 1,
|
|
8133
|
+
conversationId: value.conversationId,
|
|
8134
|
+
createdAtMs: value.createdAt.getTime(),
|
|
8135
|
+
lastActivityAtMs: value.lastActivityAt.getTime(),
|
|
8136
|
+
updatedAtMs: value.updatedAt.getTime(),
|
|
8137
|
+
execution: {
|
|
8138
|
+
status: value.executionStatus,
|
|
8139
|
+
...value.runId ? { runId: value.runId } : void 0,
|
|
8140
|
+
...value.executionUpdatedAt ? { updatedAtMs: value.executionUpdatedAt.getTime() } : void 0
|
|
8141
|
+
},
|
|
8142
|
+
...actor ? { actor } : void 0,
|
|
8143
|
+
...location ? { location } : void 0,
|
|
8144
|
+
...value.channelName ? { channelName: value.channelName } : void 0,
|
|
8145
|
+
...value.source ? { source: value.source } : void 0,
|
|
8146
|
+
...value.title ? { title: value.title } : void 0,
|
|
8147
|
+
...value.transcriptPurgedAt ? { transcriptPurgedAtMs: value.transcriptPurgedAt.getTime() } : void 0
|
|
8148
|
+
};
|
|
8149
|
+
}
|
|
8150
|
+
/** Read one normalized conversation record directly from its SQL row. */
|
|
8151
|
+
async function readConversationRecordFromSql(conversationId) {
|
|
8152
|
+
const row = (await getDb().select({
|
|
8153
|
+
conversation: conversationReadColumns(),
|
|
8154
|
+
location: juniorDestinations,
|
|
8155
|
+
identityDisplayName: juniorIdentities.displayName,
|
|
8156
|
+
identityEmail: juniorIdentities.email,
|
|
8157
|
+
identityHandle: juniorIdentities.handle,
|
|
8158
|
+
identityProvider: juniorIdentities.provider,
|
|
8159
|
+
identitySubjectId: juniorIdentities.providerSubjectId,
|
|
8160
|
+
identityTenantId: juniorIdentities.providerTenantId,
|
|
8161
|
+
userDisplayName: juniorUsers.displayName
|
|
8162
|
+
}).from(juniorConversations).leftJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(eq(juniorConversations.conversationId, conversationId)).limit(1))[0];
|
|
8163
|
+
return row ? {
|
|
8164
|
+
conversation: conversationFromRow(row),
|
|
8165
|
+
durationMs: row.conversation.durationMs,
|
|
8166
|
+
...row.location?.visibility === "public" ? { locationId: row.location.id } : void 0,
|
|
8167
|
+
usage: row.conversation.usage,
|
|
8168
|
+
rootConversationId: row.conversation.rootConversationId
|
|
8169
|
+
} : void 0;
|
|
8170
|
+
}
|
|
8171
|
+
/** Prefer the authenticated viewer; fall back to actor email membership. */
|
|
8172
|
+
function conversationFeedFilter(options) {
|
|
8173
|
+
if (options.viewer) return {
|
|
8174
|
+
kind: "viewer",
|
|
8175
|
+
userId: options.viewer.id
|
|
8176
|
+
};
|
|
8177
|
+
if (options.actorEmail) return {
|
|
8178
|
+
kind: "actorEmail",
|
|
8179
|
+
email: options.actorEmail
|
|
8180
|
+
};
|
|
8181
|
+
}
|
|
8182
|
+
/**
|
|
8183
|
+
* Build a bounded dashboard feed. Prefer the viewer user when present; otherwise
|
|
8184
|
+
* keep only roots linked to actorEmail before applying the limit. Membership is
|
|
8185
|
+
* root-actor ownership or a materialized participant row for that person.
|
|
8186
|
+
*
|
|
8187
|
+
* `codeRepositoryId` replaces membership with every root linked to that
|
|
8188
|
+
* repository's code changes. Private roots appear only for their members, and
|
|
8189
|
+
* per-person archive state does not hide rows.
|
|
8190
|
+
*/
|
|
8191
|
+
async function readConversationFeedFromSql(options = {}) {
|
|
8192
|
+
const nowMs = Date.now();
|
|
8193
|
+
const db = getDb();
|
|
8194
|
+
const filter = options.codeRepositoryId ? void 0 : conversationFeedFilter(options);
|
|
8195
|
+
const query = options.q?.trim().toLowerCase() || void 0;
|
|
8196
|
+
const rows = await conversationRows(db, options.limit ?? CONVERSATION_FEED_LIMIT, query || options.codeRepositoryId ? "all" : options.status ?? "active", filter, query, filter?.kind === "viewer", options.codeRepositoryId ? {
|
|
8197
|
+
id: options.codeRepositoryId,
|
|
8198
|
+
...options.viewer ? { viewerUserId: options.viewer.id } : void 0
|
|
8199
|
+
} : void 0);
|
|
8200
|
+
const conversations = rows.map((row) => conversationFromRow(row));
|
|
8201
|
+
const conversationIds = conversations.map((conversation) => conversation.conversationId);
|
|
8202
|
+
const [accessByConversation, archiveTimesByConversation, annotationsByConversation, auxiliaryCostsByRoot, metricsByRoot, participantsByConversation, teamDomainByTeamId, conversationWork, lastUserMessageAtByConversation, activityPreviewByConversation, automationTitleByConversation, firstHumanMessageTextByConversation] = await Promise.all([
|
|
8203
|
+
readConversationAccessFromSql(db, conversationIds, options.viewer),
|
|
8204
|
+
conversationArchiveTimes(db, conversationIds, filter?.kind === "viewer" ? { userId: filter.userId } : filter?.kind === "actorEmail" ? { email: filter.email } : void 0),
|
|
8205
|
+
listConversationAnnotationsById(db, conversationIds),
|
|
8206
|
+
readConversationAuxiliaryCostsFromSql(db, conversationIds),
|
|
8207
|
+
readConversationTreeMetricsFromSql(db, conversationIds),
|
|
8208
|
+
readConversationParticipants(db, conversationIds),
|
|
8209
|
+
resolveSlackTeamDomains(conversations.flatMap((conversation) => conversation.location?.provider === "slack" ? [conversation.location.teamId] : [])),
|
|
8210
|
+
listConversationWork(conversationIds),
|
|
8211
|
+
readLastUserMessageAtByConversation(db, conversationIds),
|
|
8212
|
+
readConversationActivityPreviews(db, conversationIds),
|
|
8213
|
+
readAutomationTitlesByConversationId(conversations),
|
|
8214
|
+
readFirstHumanMessageTexts(db, conversations)
|
|
8215
|
+
]);
|
|
8216
|
+
const visibleActivityPreviews = await attachActivityPreviewMentions(getSqlExecutor(), new Map([...activityPreviewByConversation].filter(([conversationId]) => accessByConversation.get(conversationId)?.canViewPrivateContent)));
|
|
8217
|
+
const sidebarAnnotationsByConversation = await listConversationSidebarAnnotations(conversationIds, new Map(conversationIds.map((conversationId) => [conversationId, accessByConversation.get(conversationId)?.canViewPrivateContent ? annotationsByConversation.get(conversationId) ?? [] : []])));
|
|
8218
|
+
const assignedWork = new Set(conversationWork.assignedIds);
|
|
8219
|
+
const unfinishedWork = new Set(conversationWork.unfinishedIds);
|
|
7478
8220
|
return {
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
8221
|
+
conversations: conversations.map((conversation, index) => {
|
|
8222
|
+
const row = rows[index];
|
|
8223
|
+
const access = accessByConversation.get(conversation.conversationId);
|
|
8224
|
+
const metrics = metricsByRoot.get(conversation.conversationId);
|
|
8225
|
+
const auxiliaryCosts = auxiliaryCostsByRoot.get(conversation.conversationId);
|
|
8226
|
+
const childAgentTotals = childAgentTotalsReport(metrics, auxiliaryCosts);
|
|
8227
|
+
const archivedAtMs = archiveTimesByConversation.get(conversation.conversationId);
|
|
8228
|
+
const summary = conversationSummaryFromStoredConversation({
|
|
8229
|
+
...archivedAtMs === void 0 ? void 0 : { archivedAtMs },
|
|
8230
|
+
automationTitle: automationTitleByConversation.get(conversation.conversationId),
|
|
8231
|
+
conversation,
|
|
8232
|
+
access,
|
|
8233
|
+
auxiliaryCosts: auxiliaryCosts?.total,
|
|
8234
|
+
...childAgentTotals ? { childAgentTotals } : void 0,
|
|
8235
|
+
durationMs: metrics?.durationMs ?? row.conversation.durationMs,
|
|
8236
|
+
firstHumanMessageText: firstHumanMessageTextByConversation.get(conversation.conversationId),
|
|
8237
|
+
teamDomainByTeamId,
|
|
8238
|
+
...row.location?.visibility === "public" ? { locationId: row.location.id } : void 0,
|
|
8239
|
+
usage: metrics?.usage ?? row.conversation.usage ?? void 0
|
|
8240
|
+
});
|
|
8241
|
+
const work = {
|
|
8242
|
+
...assignedWork.has(conversation.conversationId) ? { assignedWork: true } : void 0,
|
|
8243
|
+
...conversationWork.finishedAtById[conversation.conversationId] ? { finishedWorkAt: conversationWork.finishedAtById[conversation.conversationId] } : void 0,
|
|
8244
|
+
...unfinishedWork.has(conversation.conversationId) ? { unfinishedWork: true } : void 0
|
|
8245
|
+
};
|
|
8246
|
+
const annotations = access?.canViewPrivateContent ? annotationsByConversation.get(conversation.conversationId) ?? [] : [];
|
|
8247
|
+
const activityPreview = visibleActivityPreviews.get(conversation.conversationId);
|
|
8248
|
+
const participants = access?.canViewPrivateContent ? participantsByConversation.get(conversation.conversationId) : void 0;
|
|
8249
|
+
return {
|
|
8250
|
+
...summary,
|
|
8251
|
+
...work,
|
|
8252
|
+
...participants && participants.length > 0 ? { participants } : void 0,
|
|
8253
|
+
...activityPreview ? { activityPreview } : void 0,
|
|
8254
|
+
...annotations.length > 0 ? { annotations } : void 0,
|
|
8255
|
+
...sidebarAnnotationsByConversation[conversation.conversationId] ? { sidebarAnnotations: sidebarAnnotationsByConversation[conversation.conversationId] } : void 0,
|
|
8256
|
+
isPriority: isConversationPriority({
|
|
8257
|
+
lastSeenAt: summary.lastSeenAt,
|
|
8258
|
+
lastUserMessageAt: lastUserMessageAtByConversation.get(conversation.conversationId),
|
|
8259
|
+
...work
|
|
8260
|
+
}, nowMs)
|
|
8261
|
+
};
|
|
8262
|
+
}),
|
|
8263
|
+
generatedAt: new Date(nowMs).toISOString(),
|
|
8264
|
+
source: "conversation_index"
|
|
7482
8265
|
};
|
|
7483
8266
|
}
|
|
7484
|
-
|
|
8267
|
+
/**
|
|
8268
|
+
* Load a bounded feed with an optional viewer-primary linked-user filter.
|
|
8269
|
+
*
|
|
8270
|
+
* With `viewer` and without `codeRepositoryId`, the feed holds only root
|
|
8271
|
+
* Conversations that the viewer started or takes part in, for every `q` and
|
|
8272
|
+
* `status`. The `actorEmail` filter and `codeRepositoryId` give no such
|
|
8273
|
+
* guarantee, so they are not an authorization boundary.
|
|
8274
|
+
*/
|
|
8275
|
+
async function readConversationFeed(options = {}) {
|
|
8276
|
+
return conversationFeedSchema.parse(await readConversationFeedFromSql(options));
|
|
8277
|
+
}
|
|
8278
|
+
export { AGENT_DISPATCH_MAX_AGE_MS as $, assertNoLegacyAllowedDomains as A, normalizeIncomingSlackThreadId as B, readConversationSourceTask as C, readViewerAutomationVersions as D, readViewerAutomationSummary as E, recordSkippedConversationMessage as F, parseContent as G, stopConversationTurn as H, buildSlackInboundMessage as I, canActOnReviewItem as J, ReviewItemActionError as K, clearSlackPendingReactions as L, isAllowedSignInEmail as M, readEnvList as N, readViewerAutomations as O, resolveAllowedDomains as P, getAttachmentStorage as Q, isSlackMessageStopped as R, deleteViewerTask as S, readViewerAutomationRuns as T, THREAD_OPTOUT_ACK as U, withNormalizedThreadId as V, startProcessingReaction as W, readReviewItemForUser as X, isOnlyAssignee as Y, reviewItemChange as Z, createProductionConversationWorkOptions as _, resolveMentions as a, createSlackWebApiStatusSender as b, readConversationAuxiliaryCostsFromSql as c, notCopiedByFork as d, enqueueAgentDispatch as et, childAgentTotalsReport as f, resolveSlackTeamDomains as g, conversationSummaryFromStoredConversation as h, attachConversationMentions as i, isAllowedDomain as j, resolveViewerTaskCandidate as k, ConversationForkError as l, conversationEventHistory as m, readConversationRecordFromSql as n, readHealthReport as nt, readFirstHumanMessageTexts as o, readConversationTreeMetricsFromSql as p, actOnReviewItem as q, readConversationParticipants as r, listConversationSidebarAnnotations as s, readConversationFeed as t, GET as tt, forkConversation as u, createProductionSlackWebhookServices as v, readViewerAutomationExecutions as w, ViewerTaskNotFoundError as x, getProductionSlackAdapter as y, stopSlackThread as z };
|