@sentry/junior 0.267.0 → 0.269.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/README.md +2 -9
- package/dist/{actor-DZSokpwj.js → actor-CP9K2Idu.js} +2 -1
- package/dist/{adapter-BybMEezj.js → adapter-2-CG25be.js} +3 -51
- package/dist/{admin-DOs1_EiW.js → admin-DSs8S5Uy.js} +1 -1
- package/dist/{agent-invocations-CxKldAeB.js → agent-invocations-C1RNOCs8.js} +2 -0
- package/dist/{agent-runner-DgNATN4D.js → agent-runner-CrDrPUtY.js} +3411 -1640
- package/dist/api/schema.d.ts +2 -5630
- package/dist/api/schema.js +7 -7
- package/dist/api.d.ts +3 -1
- package/dist/api.js +268 -287
- package/dist/{app-DAB3fzyp.d.ts → app-BrMMzjYm.d.ts} +10 -3
- package/dist/app.d.ts +1 -1
- package/dist/app.js +371 -223
- package/dist/{automation-CmeFBSEh.js → automation-p87hg_AV.js} +74 -74
- package/dist/{briefs-CWKjka7v.js → briefs-PtODdLNu.js} +4 -4
- package/dist/{catalog-runtime-cMlAI5LY.js → catalog-runtime-BsoSmTM0.js} +1 -1
- package/dist/cli/chat.js +16 -26
- package/dist/cli/check.js +3 -3
- package/dist/cli/main.js +5 -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-BjgbMawj.js → client-Cqf4TboL.js} +20 -15
- package/dist/{config-CdB6yJj5.js → config-CPsE4T5L.js} +2 -2
- package/dist/{connections-RE_65jzK.js → connections-upFXLmhG.js} +10 -11
- package/dist/{conversation-DSo310Ps.js → conversation-CTpD_PGk.js} +7 -84
- package/dist/{conversation-privacy-BzgJC5k0.js → conversation-privacy-D6zJc8ff.js} +1 -1
- package/dist/{credential-sync-DhEGEpgq.js → credential-sync-CS_6IC-f.js} +30 -34
- package/dist/{credentialed-CIGwoLU2.js → credentialed-BU-OW7HP.js} +577 -74
- package/dist/{store-B8CABbmR.d.ts → db-BzOCzVb1.d.ts} +17 -7
- package/dist/{executor-VYrx_K4w.js → executor-CNS7ONt-.js} +78 -4
- package/dist/instrumentation.js +2 -2
- package/dist/{assistant-reply-CT7VcXQe.js → interruption-marker-g6Xup3Fk.js} +8 -2
- package/dist/{job-queue-C0BZSa7r.js → job-queue-DI-k-Bbk.js} +2 -2
- package/dist/{list-1XQP-o6K.js → list-CGoQdXTp.js} +429 -1401
- package/dist/locks-D-0h111u.js +48 -0
- package/dist/{logging-DuZ1A4ab.js → logging-n7knse-O.js} +1 -1
- package/dist/{history-C84mlAwO.js → media-85PDJxO_.js} +83 -1
- package/dist/{message-NKSbbnA6.js → message-BpaWd8TT.js} +4 -4
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-callback-server-qAHLPtml.js → oauth-callback-server-CpGmEw_i.js} +2 -2
- package/dist/{prompt-BW9jFsqZ.js → prompt-BlQB0QQt.js} +1 -1
- package/dist/{registry-FQutO926.js → registry-By36_xsm.js} +2 -2
- package/dist/reporting-schema-CZREhYc-.js +104 -0
- package/dist/reporting-window-CVarfId9.js +255 -0
- package/dist/{resolve-Bk4yeuHy.js → resolve-CUIBcdPd.js} +3 -3
- package/dist/{result-B9e9ATOU.js → result-BemsOMOO.js} +31 -12
- package/dist/{runner-C3S2ObH-.js → runner-BVok7PAA.js} +12 -11
- package/dist/{sandbox-egress-signals-DqpGzzMB.js → sandbox-egress-signals-BSUWQI95.js} +1 -1
- package/dist/{schema-X7Fj4fv1.js → schema-ChnFo5f2.js} +98 -6
- package/dist/schema-jK4-1u_H.d.ts +5927 -0
- package/dist/{service-account-Do30HUNv.js → service-account-DHvlLnZw.js} +4 -3
- package/dist/{service-account-BQasl-qq.js → service-account-hMmESDcr.js} +3 -3
- package/dist/{session-stop-DkOjt46_.js → session-stop-BQW_iFcd.js} +499 -23
- package/dist/{skills-CQHrwfOa.js → skills-DmJJIuLF.js} +2 -2
- package/dist/{spawn-BHuqN14f.js → spawn-ChCm46i-.js} +2 -2
- package/dist/{store-6EetVP-m.js → store-1DVhoKOL.js} +4 -4
- package/dist/{store-DvNZIsCr.js → store-DCo2n5iJ.js} +3 -3
- package/dist/{task-queue-B0DLgd2P.js → task-queue-HMC4QjFm.js} +3 -3
- package/dist/task-runner-cA6yjn_E.js +269 -0
- package/dist/{thread-state-DRmdJ2QN.js → thread-state-CHy8LS0C.js} +8 -18
- package/dist/{turn-failure-response-G11cs2gE.js → turn-failure-response-Dr7erLWs.js} +11 -16
- package/dist/{turn-lifecycle-OzLIwvi1.js → turn-lifecycle-bf2fIYi_.js} +1 -1
- package/dist/{tasks-0ilmuCux.js → turn-session-routing-BqO0qXLk.js} +959 -44
- package/dist/{validation-CxEmt1id.js → validation-DHR1ivfh.js} +1 -1
- package/dist/{version-Bapedn4A.js → version-CXCHLgWm.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-DrN3oC2X.js → viewer-DSbD6DF6.js} +503 -241
- package/dist/{work-DUQoNcqP.js → work-BE_qTYGi.js} +17 -36
- package/dist/{worker-D1l4Ugk0.js → worker-Cm9_sWXg.js} +15 -7
- 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/0069_system_identity.sql +25 -0
- package/migrations/0070_review_item_foreign_keys.sql +5 -0
- package/migrations/AGENTS.md +17 -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/0069_snapshot.json +3979 -0
- package/migrations/meta/0070_snapshot.json +3997 -0
- package/migrations/meta/_journal.json +42 -0
- package/package.json +5 -5
- package/dist/db-Bg-zC3r5.d.ts +0 -5
- package/dist/history-BgI33Kie.d.ts +0 -78
- package/dist/images-CecjXAAz.js +0 -64
- package/dist/task-runner-B-PD_JOj.js +0 -1074
- package/dist/turn-session-routing-Dyut9l7Y.js +0 -76
- package/dist/user-input-request-BDFBdF8r.js +0 -116
- package/dist/web-message-id-CJSUiksE.js +0 -236
- package/migrations/README.md +0 -30
|
@@ -1,42 +1,41 @@
|
|
|
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 isRecord, E as withSpan, H as toOptionalString, S as setTags, T as withLogContext, V as toOptionalNumber, b as setSpanAttributes, f as logError, h as logWarn, m as logInfo, n as createChatSdkLogger, o as getActiveTraceContext, p as logException, s as getActiveTraceId, t as buildTurnFailureResponse, y as setSentryUser } from "./logging-
|
|
1
|
+
import { $t as appendThreadContextMessages, An as recordTurnSummary, At as loadProjection, Cn as loadTurnCheckpoint, Dn as failTurnRecord, En as completeTurnRecord, Et as commitMessages, Gt as hydrateConversationMessages, Ir as decideSubscribedThreadReply, Jr as piMessageFromHistoryItem, Jt as persistConversationMessageSummaries, Kr as configValueSchema, Kt as persistConversationMessages, Lr as getSubscribedReplyPreflightDecision, Lt as recordAuthorizationCompleted, Nr as buildDeterministicAssistantMessageId, On as getTurnRecord, Ot as loadConversationProjection, Pr as buildDeterministicTurnId, Rr as getThreadStopDecision, Tn as abandonTurnRecord, Tt as commitAcceptedReply, Ur as isExperimentalFeatureEnabled, Vt as recordSubscribedReplyRoute, Yt as resolveConversationTitle, Zt as fallbackShortTitle, _ as markDispatchCompleted, a as confirmDispatchMailboxAppend, an as markConversationMessage, cn as turnHasReply, d as getDispatchTurnId, dn as PluginCredentialFailureError, en as buildConversationContext, fr as buildTurnErrorResponse, g as markDispatchBlocked, h as markDispatchAwaitingResume, hn as AuthorizationFlowDisabledError, hr as persistWithRetry, i as claimDispatchMailboxAppend, in as isHumanConversationMessage, j as readAutomationTitlesByConversationId, jt as loadTurnProjection, kn as listTurnSummaries, kr as trimTrailingAssistantMessages, l as getDispatchInputMessageId, ln as upsertConversationMessage, n as resolveTurnSessionRouting, nn as estimateConversationContextTokens, on as normalizeConversationText, p as isTerminalDispatchStatus, rn as getConversationMessageSlackTs, s as getDispatchConversationId, sn as recordDeliveredAssistantMessage, t as resolveConversationRouting, tn as createConversationMemoryService, u as getDispatchRecord, v as markDispatchFailed, wn as saveTurnCheckpoint, xn as wasPendingAuthStopped, y as markDispatchRunning, yn as clearPendingAuth } from "./turn-session-routing-BqO0qXLk.js";
|
|
2
|
+
import { a as completeText, p as escapeXml, r as completeDecision, y as isProviderRetryError } from "./client-Cqf4TboL.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-CPsE4T5L.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-n7knse-O.js";
|
|
5
5
|
import { i as getActiveSpan, r as continueTrace } from "./sentry-MDkYAVPV.js";
|
|
6
6
|
import { o as modelIdForProfile, r as defaultModelId } from "./model-profile-Bg8EoCKD.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, o as conversationReadColumns, s as juniorConversations } from "./agent-invocations-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { d as actorLogContext, f as credentialContextForActor } from "./registry-
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { a as
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import { a as withAgentInvocationResultActors, c as resolveMailboxTurnWork, d as legacyWebMailboxMetadataSchema,
|
|
27
|
-
import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-
|
|
28
|
-
import {
|
|
29
|
-
import { a as
|
|
30
|
-
import { c as deleteWebAuthorization, d as createPausedTurns, f as getPausedTurnRequest, h as getTurnAuthorization, m as wakePausedTurn, s as createWebAuthorization } from "./connections-
|
|
31
|
-
import {
|
|
32
|
-
import { t as
|
|
33
|
-
import { t as
|
|
34
|
-
import { a as persistAuthPauseTurnState, c as getTurnUserSlackMessageTs,
|
|
35
|
-
import { r as storeInputImages } from "./images-CecjXAAz.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 { C as getSqlExecutor, E as loadConversationSlackTenantIds, H as parseSessionSource, M as resolveActorIdentityId, N as saveActorAvatar, S as getDb, T as withConversationEventLock, U as addAgentTurnUsage, Y as createSlackDestination, Z as requireSlackDestination, f as identityAvatarUrl, h as readUserAvatarUrls, k as locationFromRow, q as hasAgentTurnUsage, x as getConversationStore, y as getConversationEventStore } from "./viewer-DSbD6DF6.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, g as juniorConversationBriefs, y as juniorConversationEvents } from "./executor-CNS7ONt-.js";
|
|
11
|
+
import { g as instructionProvenanceFor, m as contextProvenance, r as isVisionImageMediaType } from "./media-85PDJxO_.js";
|
|
12
|
+
import { d as actorLogContext, f as credentialContextForActor } from "./registry-By36_xsm.js";
|
|
13
|
+
import { Et as withoutSavedSteeringMessages, F as getConversationInfo, I as listThreadReplies, M as readSlackRawMessageContext, N as resolveSlackChannelIdFromMessage, Nt as shouldEmitDevAgentTrace, P as resolveSlackChannelIdFromThreadId, St as conversationSubtreeColumns, Tt as buildSteeringPiMessage, b as renderBlockText, ct as webActorFromEmail, et as deliverReviewItemChange, i as buildAuthPauseResponse, j as parseSlackThreadId, k as runWithSlackInstallation, kt as RetryableDeliveryError, lt as resolveConversationDestination, mt as storeAttachment, n as executeAgentRun, ot as createConversationId, p as latestProgressStatus, r as createContextCompactor, st as recordWebConversationActivity, t as createAgentRunner, xt as conversationSubtree, y as renderAttachmentText } from "./agent-runner-CrDrPUtY.js";
|
|
14
|
+
import { An as normalizeSlackConversationId, Ar as lookupSlackUser, Cn as SlackActionError, Dn as getSlackClient, Dr as listConversationAnnotationsById, Dt as removeReactionFromMessage, E as listReplyActionItems, En as getHeaderString, F as isOnlyAssignee, In as JUNIOR_THREAD_STATE_TTL_MS, M as canActOnReviewItem, Mn as withSlackRetries, P as canReadReviewItem, Sn as verifyTimerWatchCredentialSubject, Sr as recordSlackAttachment, _t as normalizeSlackStatusText, ar as createPluginLogger, b as closeReviewItem, bt as addReactionToMessage, cr as conversationArchivedForEmail, dr as conversationIsMemberRoot, fr as conversationNotArchivedForEmail, jr as createPluginCache, k as takeReviewItem, kn as isDmChannel, lr as conversationArchivedForUser, mr as viewerConversationMembership, n as createUserTokenStore, on as cancelSubscriptions, or as readConversationAccessFromSql, p as getPlugins, pr as conversationNotArchivedForUser, sr as conversationArchiveTimes, ur as conversationHasParticipantUser, w as getReviewItem, wn as downloadPrivateSlackFile, xr as readAttachmentVisionSummary, yr as getSlackFileId, yt as truncateStatusText, z as reviewItemReaderForUser } from "./credentialed-BU-OW7HP.js";
|
|
15
|
+
import { a as withLock, n as MUTATION_LOCK_TTL_MS, r as acquireActiveLock } from "./locks-D-0h111u.js";
|
|
16
|
+
import { r as getStateAdapter } from "./adapter-2-CG25be.js";
|
|
17
|
+
import { W as readMessageAttachments, g as conversationFeedSchema } from "./conversation-CTpD_PGk.js";
|
|
18
|
+
import { a as EVENT_SYSTEM_ACTOR, o as isEventConversationMessage } from "./prompt-BlQB0QQt.js";
|
|
19
|
+
import { B as ConversationTurnBoundaryError, G as isCooperativeTurnYieldError, H as TurnInputCommitLostError, J as markTurnClosed, K as isTurnInputCommitLostError, U as TurnInputDeferredError, V as CooperativeTurnYieldError, W as getConversationTurnBoundaryError, X as markTurnFailed, Y as markTurnCompleted, Z as startActiveTurn, q as isTurnInputDeferredError } from "./store-1DVhoKOL.js";
|
|
20
|
+
import { r as getAssistantReplyText } from "./interruption-marker-g6Xup3Fk.js";
|
|
21
|
+
import { C as maybePostAutomatedTurnLimitNotice, D as recordFinishedTurnForAutomatedLimit, I as runDispatchOutcome, M as finishedRunReply, N as postsRunNotices, T as sendSlackReply, c as isEventMailboxMetadata, i as skipInputBeforeConversationStop, j as deliversFinalText, l as renderEventBatchText, s as eventBatchMessageEvents, u as timerWatchCredentialSubject, w as resolveSlackReplyTarget, x as joinMailboxText } from "./session-stop-BQW_iFcd.js";
|
|
22
|
+
import { _ as normalizeUserInputAnswer, g as isEmptyUserInputAnswer } from "./reporting-window-CVarfId9.js";
|
|
23
|
+
import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline } from "./job-queue-DI-k-Bbk.js";
|
|
24
|
+
import { D as stopConversationWork, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, r as appendAndEnqueueInboundMessage, w as requestConversationWork } from "./store-DCo2n5iJ.js";
|
|
25
|
+
import { t as healthReportSchema } from "./reporting-schema-CZREhYc-.js";
|
|
26
|
+
import { a as withAgentInvocationResultActors, c as resolveMailboxTurnWork, d as legacyWebMailboxMetadataSchema, l as sourceFromTurnInput, n as enqueueAgentInvocationResult, o as getAuthPausedConversationTurnId, s as getEventBatchDelayMs, t as createChildConversationWorker, u as turnInputFactsFromConversationMessage } from "./result-BemsOMOO.js";
|
|
27
|
+
import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-bf2fIYi_.js";
|
|
28
|
+
import { a as AgentRunError, o as executeTurn } from "./work-BE_qTYGi.js";
|
|
29
|
+
import { a as persistThreadStateById, i as persistThreadRuntimeState, n as getPersistedSandboxState, o as coerceThreadConversationState, r as getPersistedThreadState, t as getLocationConfigurationService } from "./thread-state-CHy8LS0C.js";
|
|
30
|
+
import { c as deleteWebAuthorization, d as createPausedTurns, f as getPausedTurnRequest, h as getTurnAuthorization, m as wakePausedTurn, s as createWebAuthorization } from "./connections-upFXLmhG.js";
|
|
31
|
+
import { t as bindMessageAgent } from "./message-BpaWd8TT.js";
|
|
32
|
+
import { t as bindSpawnAgent } from "./spawn-ChCm46i-.js";
|
|
33
|
+
import { n as scheduleSessionCompletedPluginTasks, t as processPluginTask } from "./task-runner-cA6yjn_E.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-Dr7erLWs.js";
|
|
36
35
|
import { z } from "zod";
|
|
37
36
|
import { createHash } from "node:crypto";
|
|
38
|
-
import { and, asc,
|
|
39
|
-
import { conversationSidebarAnnotationSchema, createSlackSource,
|
|
37
|
+
import { and, asc, desc, eq, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm";
|
|
38
|
+
import { conversationSidebarAnnotationSchema, createSlackSource, objectTypeSchema } from "@sentry/junior-plugin-api";
|
|
40
39
|
import { alias } from "drizzle-orm/pg-core";
|
|
41
40
|
import { del, get, put } from "@vercel/blob";
|
|
42
41
|
import { Message, ThreadImpl, stringifyMarkdown } from "chat";
|
|
@@ -80,7 +79,7 @@ function buildDispatchRoutingContext(dispatch) {
|
|
|
80
79
|
};
|
|
81
80
|
}
|
|
82
81
|
/** Build the stable mailbox work item for one agent dispatch. */
|
|
83
|
-
function buildAgentDispatchInboundMessage(dispatch, nowMs = Date.now()) {
|
|
82
|
+
function buildAgentDispatchInboundMessage(dispatch, nowMs = Date.now(), authorIdentityId) {
|
|
84
83
|
return {
|
|
85
84
|
conversationId: getDispatchConversationId(dispatch),
|
|
86
85
|
createdAtMs: dispatch.createdAtMs,
|
|
@@ -89,6 +88,7 @@ function buildAgentDispatchInboundMessage(dispatch, nowMs = Date.now()) {
|
|
|
89
88
|
inboundMessageId: getDispatchInputMessageId(dispatch.id),
|
|
90
89
|
input: {
|
|
91
90
|
text: "[plugin dispatch]",
|
|
91
|
+
...authorIdentityId ? { authorIdentityId } : void 0,
|
|
92
92
|
metadata: {
|
|
93
93
|
dispatchId: dispatch.id,
|
|
94
94
|
kind: "agent_dispatch"
|
|
@@ -112,7 +112,7 @@ async function enqueueAgentDispatch(dispatch, options) {
|
|
|
112
112
|
sessionSource: claimedDispatch.source
|
|
113
113
|
});
|
|
114
114
|
await appendAndEnqueueInboundMessage({
|
|
115
|
-
message: buildAgentDispatchInboundMessage(claimedDispatch, nowMs),
|
|
115
|
+
message: buildAgentDispatchInboundMessage(claimedDispatch, nowMs, await resolveActorIdentityId(getSqlExecutor(), claimedDispatch.actor)),
|
|
116
116
|
conversationStore: options.conversationStore,
|
|
117
117
|
nowMs,
|
|
118
118
|
queue: options.queue,
|
|
@@ -380,207 +380,198 @@ function createAttachmentStorage(config) {
|
|
|
380
380
|
default: throw new Error(`Unsupported attachment storage provider: ${String(config.provider)}`);
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
|
-
// src/chat/
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
entity_type: "slack#/entities/item",
|
|
408
|
-
external_ref: {
|
|
409
|
-
id: card.id,
|
|
410
|
-
type: "automation"
|
|
411
|
-
},
|
|
412
|
-
url: card.url,
|
|
413
|
-
entity_payload: {
|
|
414
|
-
attributes: {
|
|
415
|
-
title: { text: title },
|
|
416
|
-
display_type: "Automation",
|
|
417
|
-
product_icon: iconUrl ? {
|
|
418
|
-
url: iconUrl,
|
|
419
|
-
alt_text: "Automation"
|
|
420
|
-
} : void 0
|
|
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
|
-
}
|
|
383
|
+
// src/chat/review-items/actions.ts
|
|
384
|
+
/** An expected failure of a person action, for the API to report. */
|
|
385
|
+
var ReviewItemActionError = class extends Error {
|
|
386
|
+
code;
|
|
387
|
+
constructor(code, message) {
|
|
388
|
+
super(message);
|
|
389
|
+
this.name = "ReviewItemActionError";
|
|
390
|
+
this.code = code;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
/** Read one Review item that a User can see, or fail as not found. */
|
|
394
|
+
async function readReviewItemForUser(user, id) {
|
|
395
|
+
const item = await getReviewItem(getDb(), id);
|
|
396
|
+
if (!item || !canReadReviewItem(item, reviewItemReaderForUser(user))) throw new ReviewItemActionError("not_found", "Review item not found.");
|
|
397
|
+
return item;
|
|
398
|
+
}
|
|
399
|
+
function closedStatus(action) {
|
|
400
|
+
return action === "dismiss" ? "dismissed" : "done";
|
|
401
|
+
}
|
|
402
|
+
/** The change that one person action makes, for messages and notes. */
|
|
403
|
+
function reviewItemChange(action, user) {
|
|
404
|
+
if (action === "take") return {
|
|
405
|
+
kind: "taken",
|
|
406
|
+
user
|
|
437
407
|
};
|
|
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
|
|
408
|
+
if (action === "submit") return {
|
|
409
|
+
kind: "submitted",
|
|
410
|
+
user
|
|
477
411
|
};
|
|
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
|
|
412
|
+
if (action === "complete") return {
|
|
413
|
+
kind: "completed",
|
|
414
|
+
user
|
|
488
415
|
};
|
|
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
416
|
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
|
-
}
|
|
417
|
+
kind: "dismissed",
|
|
418
|
+
user
|
|
543
419
|
};
|
|
544
420
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
return
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
|
421
|
+
/**
|
|
422
|
+
* Return whether a closed item is the result of this same action. An item
|
|
423
|
+
* that a person's message closed never matches a card or desk action.
|
|
424
|
+
*/
|
|
425
|
+
function isSameClose(item, action, user) {
|
|
426
|
+
return item.closeReason !== "Replied in thread" && item.closedBy?.userId === user.id && item.status === closedStatus(action) && action === "submit" === Boolean(item.answer);
|
|
427
|
+
}
|
|
428
|
+
function submittedAnswer(item, answer) {
|
|
429
|
+
if (item.kind !== "action" || !item.request) throw new ReviewItemActionError("invalid", "Only an action item takes an answer.");
|
|
430
|
+
const normalized = normalizeUserInputAnswer(item.request, {
|
|
431
|
+
other: answer?.other,
|
|
432
|
+
requestId: item.id,
|
|
433
|
+
values: answer?.values ?? {}
|
|
571
434
|
});
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
435
|
+
if (isEmptyUserInputAnswer(normalized)) throw new ReviewItemActionError("invalid", "The answer is empty.");
|
|
436
|
+
return normalized;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Do one person action on a Review item and tell its linked Conversations.
|
|
440
|
+
* The first submit, complete, or dismiss closes the item for every
|
|
441
|
+
* assignee. A retry of the same close sends any missing messages again and
|
|
442
|
+
* succeeds, so a client can retry after a failure. With `firstCloseOnly`,
|
|
443
|
+
* only the call that closes the item succeeds, and every other call gets a
|
|
444
|
+
* conflict. Use it when the caller adds its own input for the action, such
|
|
445
|
+
* as a card click in a chat thread, so one action adds one Turn.
|
|
446
|
+
* `exceptConversationId` is the Conversation where the person acted. It gets
|
|
447
|
+
* the person's input from its own surface, so it gets no message here.
|
|
448
|
+
* `delivery` shows the change and sends the messages; see
|
|
449
|
+
* `deliverReviewItemChange`.
|
|
450
|
+
*/
|
|
451
|
+
async function actOnReviewItem(args) {
|
|
452
|
+
const { action, user } = args;
|
|
453
|
+
const nowMs = args.nowMs ?? Date.now();
|
|
454
|
+
const db = getDb();
|
|
455
|
+
const item = await readReviewItemForUser(user, args.id);
|
|
456
|
+
if (action === "take") {
|
|
457
|
+
if (item.status !== "open") throw new ReviewItemActionError("conflict", "Review item is closed.");
|
|
458
|
+
if (!canActOnReviewItem(item, user.id)) throw new ReviewItemActionError("forbidden", "Only an assignee can take this item.");
|
|
459
|
+
if (!isOnlyAssignee(item, user.id) && !await takeReviewItem(db, {
|
|
460
|
+
id: item.id,
|
|
461
|
+
nowMs,
|
|
462
|
+
userId: user.id
|
|
463
|
+
})) throw new ReviewItemActionError("conflict", "Review item is closed.");
|
|
464
|
+
const taken = await readReviewItemForUser(user, item.id);
|
|
465
|
+
await deliverReviewItemChange({
|
|
466
|
+
change: reviewItemChange(action, user),
|
|
467
|
+
delivery: args.delivery,
|
|
468
|
+
exceptConversationId: args.exceptConversationId,
|
|
469
|
+
item: taken,
|
|
470
|
+
nowMs
|
|
471
|
+
});
|
|
472
|
+
return taken;
|
|
473
|
+
}
|
|
474
|
+
let closed = item;
|
|
475
|
+
let closedNow = false;
|
|
476
|
+
if (item.status === "open") {
|
|
477
|
+
if (!canActOnReviewItem(item, user.id)) throw new ReviewItemActionError("forbidden", "Only an assignee can act on this item.");
|
|
478
|
+
if (action === "complete" && item.kind === "action") throw new ReviewItemActionError("invalid", "An action item needs an answer. Submit it or dismiss it.");
|
|
479
|
+
const answer = action === "submit" ? submittedAnswer(item, args.answer) : void 0;
|
|
480
|
+
closedNow = await closeReviewItem(db, {
|
|
481
|
+
closedByUserId: user.id,
|
|
482
|
+
id: item.id,
|
|
483
|
+
nowMs,
|
|
484
|
+
status: closedStatus(action),
|
|
485
|
+
...answer ? { answer } : void 0
|
|
486
|
+
});
|
|
487
|
+
closed = await readReviewItemForUser(user, item.id);
|
|
488
|
+
}
|
|
489
|
+
if (args.firstCloseOnly ? !closedNow : !isSameClose(closed, action, user)) throw new ReviewItemActionError("conflict", "Review item is closed.");
|
|
490
|
+
await deliverReviewItemChange({
|
|
491
|
+
change: reviewItemChange(action, user),
|
|
492
|
+
delivery: args.delivery,
|
|
493
|
+
exceptConversationId: args.exceptConversationId,
|
|
494
|
+
item: closed,
|
|
495
|
+
nowMs
|
|
496
|
+
});
|
|
497
|
+
return closed;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Return whether a reply from this User answers an open action item, or is
|
|
501
|
+
* a retry of the reply from this User that closed it.
|
|
502
|
+
*/
|
|
503
|
+
function replyAnswers(item, user) {
|
|
504
|
+
if (item.status !== "open") return item.closedBy?.userId === user?.id;
|
|
505
|
+
if (item.assignedToEveryone) return false;
|
|
506
|
+
if (item.assignees.length === 0) return true;
|
|
507
|
+
return user !== void 0 && item.assignees.some((assignee) => assignee.userId === user.id);
|
|
508
|
+
}
|
|
509
|
+
/** Return the answer that the text of a person's message gives to an item. */
|
|
510
|
+
function messageAnswer(item, text) {
|
|
511
|
+
if (!item.request) return void 0;
|
|
512
|
+
const answer = normalizeUserInputAnswer(item.request, {
|
|
513
|
+
other: text,
|
|
514
|
+
requestId: item.id,
|
|
515
|
+
values: {}
|
|
516
|
+
});
|
|
517
|
+
return isEmptyUserInputAnswer(answer) ? void 0 : answer;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Close the open action items that a person answered with a reply in their
|
|
521
|
+
* thread. Call it after the reply is saved to the Conversation, so the
|
|
522
|
+
* reply always reaches the agent. Each item that it closes saves `text` as
|
|
523
|
+
* its answer, so the item keeps what the person said.
|
|
524
|
+
*
|
|
525
|
+
* A reply closes an item only when all of these are true:
|
|
526
|
+
* - The item is an open `action` item.
|
|
527
|
+
* - The agent added it in this Conversation (`addedByConversationId`).
|
|
528
|
+
* - The replier is an assigned User, or the item has no assignees. A Slack
|
|
529
|
+
* replier without a Junior User closes only items with no assignees.
|
|
530
|
+
*
|
|
531
|
+
* Items assigned to Everyone, or only to other people, stay open. They need
|
|
532
|
+
* an answer on the card or the review desk. Each item closes as `done` with
|
|
533
|
+
* `REVIEW_ITEM_REPLIED_REASON`, and the replier closes it. The reply itself
|
|
534
|
+
* is the answer in this Conversation, so only the other linked
|
|
535
|
+
* Conversations get a message. A click that closes the item first wins.
|
|
536
|
+
* A retry of the reply, or a later reply from the same person, delivers
|
|
537
|
+
* the items that it closed again: the cards show the close, and only the
|
|
538
|
+
* missing messages go. Each message has a key, so a message that was sent
|
|
539
|
+
* does not go again. `delivery` shows the change and sends the messages;
|
|
540
|
+
* see `deliverReviewItemChange`.
|
|
541
|
+
*/
|
|
542
|
+
async function closeReviewItemsAnsweredByReply(args) {
|
|
543
|
+
const db = getDb();
|
|
544
|
+
const items = await listReplyActionItems(db, args.conversationId);
|
|
545
|
+
if (items.length === 0) return;
|
|
546
|
+
if (args.canAnswer && !await args.canAnswer()) return;
|
|
547
|
+
const user = await args.resolveUser();
|
|
548
|
+
const nowMs = args.nowMs ?? Date.now();
|
|
549
|
+
for (const item of items.filter((candidate) => replyAnswers(candidate, user))) {
|
|
550
|
+
let closedItem = item;
|
|
551
|
+
if (item.status === "open") {
|
|
552
|
+
const answer = messageAnswer(item, args.text);
|
|
553
|
+
if (!await closeReviewItem(db, {
|
|
554
|
+
...answer ? { answer } : void 0,
|
|
555
|
+
id: item.id,
|
|
556
|
+
nowMs,
|
|
557
|
+
reason: "Replied in thread",
|
|
558
|
+
status: "done",
|
|
559
|
+
...user ? { closedByUserId: user.id } : void 0
|
|
560
|
+
})) continue;
|
|
561
|
+
closedItem = await getReviewItem(db, item.id);
|
|
562
|
+
if (!closedItem) continue;
|
|
563
|
+
}
|
|
564
|
+
await deliverReviewItemChange({
|
|
565
|
+
change: {
|
|
566
|
+
kind: "replied",
|
|
567
|
+
who: user ? user.displayName ?? user.email : "A person"
|
|
568
|
+
},
|
|
569
|
+
delivery: args.delivery,
|
|
570
|
+
exceptConversationId: args.conversationId,
|
|
571
|
+
item: closedItem,
|
|
572
|
+
nowMs
|
|
573
|
+
});
|
|
574
|
+
}
|
|
584
575
|
}
|
|
585
576
|
// src/chat/slack/errors.ts
|
|
586
577
|
/** Extract Slack's stable API error code from Web API errors. */
|
|
@@ -915,6 +906,7 @@ function combineTurnText(queuedMessages, latestText) {
|
|
|
915
906
|
}
|
|
916
907
|
// src/chat/services/message-actor-identity.ts
|
|
917
908
|
const messageActors = /* @__PURE__ */ new WeakMap();
|
|
909
|
+
const messageAuthorIdentityIds = /* @__PURE__ */ new WeakMap();
|
|
918
910
|
function canonicalUserId(author, actor) {
|
|
919
911
|
const authorUserId = parseActorUserId(author.userId);
|
|
920
912
|
if (authorUserId && authorUserId !== actor.userId) throw new Error("Message actor user id mismatch");
|
|
@@ -945,6 +937,17 @@ function bindMessageActorIdentity(message, actor) {
|
|
|
945
937
|
applyActorToAuthor(message.author, currentActor);
|
|
946
938
|
return currentActor;
|
|
947
939
|
}
|
|
940
|
+
/**
|
|
941
|
+
* Keep the author Identity id that ingress resolved, next to the Actor.
|
|
942
|
+
* The Message write reads it, so the write does not guess the author again.
|
|
943
|
+
*/
|
|
944
|
+
function bindMessageAuthorIdentityId(message, authorIdentityId) {
|
|
945
|
+
if (authorIdentityId) messageAuthorIdentityIds.set(message, authorIdentityId);
|
|
946
|
+
}
|
|
947
|
+
/** Read the author Identity id that ingress bound to a Chat SDK message. */
|
|
948
|
+
function getMessageAuthorIdentityId(message) {
|
|
949
|
+
return messageAuthorIdentityIds.get(message);
|
|
950
|
+
}
|
|
948
951
|
/** Read message identity without promoting adapter display fallbacks. */
|
|
949
952
|
function getMessageActorIdentity(message) {
|
|
950
953
|
return messageActors.get(message) ?? actorFromAuthor(message.author);
|
|
@@ -1040,10 +1043,8 @@ function createSlackTurnRuntime(deps) {
|
|
|
1040
1043
|
return true;
|
|
1041
1044
|
};
|
|
1042
1045
|
const failConversationTurn = async (args) => {
|
|
1043
|
-
const conversationId = getThreadId(args.thread, args.message) ?? getRunId(args.thread, args.message);
|
|
1044
|
-
if (!conversationId) return;
|
|
1045
1046
|
await turnLifecycle.fail({
|
|
1046
|
-
conversationId,
|
|
1047
|
+
conversationId: args.conversationId,
|
|
1047
1048
|
createdAtMs: deps.now(),
|
|
1048
1049
|
eventId: args.eventId,
|
|
1049
1050
|
failureCode: args.failureCode,
|
|
@@ -1088,7 +1089,15 @@ function createSlackTurnRuntime(deps) {
|
|
|
1088
1089
|
};
|
|
1089
1090
|
const postFallbackErrorReplyWithLogging = async (args) => {
|
|
1090
1091
|
try {
|
|
1091
|
-
|
|
1092
|
+
const channelId = getChannelId(args.thread, args.message);
|
|
1093
|
+
if (!channelId) throw new Error("Slack fallback error reply requires a channel id");
|
|
1094
|
+
const threadTs = getThreadTs(getThreadId(args.thread, args.message));
|
|
1095
|
+
await sendSlackReply({
|
|
1096
|
+
channelId,
|
|
1097
|
+
conversationId: args.conversationId,
|
|
1098
|
+
text: buildTurnErrorResponse(args.error, args.eventId),
|
|
1099
|
+
...threadTs ? { threadTs } : void 0
|
|
1100
|
+
});
|
|
1092
1101
|
} catch (postError) {
|
|
1093
1102
|
logException(postError, args.postFailureEventName, {
|
|
1094
1103
|
"app.slack.reply_stage": "error_fallback_post",
|
|
@@ -1144,14 +1153,14 @@ function createSlackTurnRuntime(deps) {
|
|
|
1144
1153
|
const completedAtMs = deps.now();
|
|
1145
1154
|
logSkippedSubscribedDecision(args);
|
|
1146
1155
|
if (args.preparedState) await deps.onSubscribedMessageSkipped({
|
|
1147
|
-
|
|
1156
|
+
conversationId: args.conversationId,
|
|
1148
1157
|
message: args.message,
|
|
1149
1158
|
decision: args.decision,
|
|
1150
1159
|
preparedState: args.preparedState,
|
|
1151
1160
|
completedAtMs
|
|
1152
1161
|
});
|
|
1153
1162
|
else await deps.recordSkippedSubscribedTurn({
|
|
1154
|
-
|
|
1163
|
+
conversationId: args.conversationId,
|
|
1155
1164
|
message: args.message,
|
|
1156
1165
|
decision: args.decision,
|
|
1157
1166
|
text: args.text,
|
|
@@ -1199,7 +1208,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1199
1208
|
});
|
|
1200
1209
|
logSkippedSubscribedDecision(skipped);
|
|
1201
1210
|
await deps.recordSkippedSteeringMessage({
|
|
1202
|
-
|
|
1211
|
+
conversationId: args.conversationId,
|
|
1203
1212
|
message: skipped.message,
|
|
1204
1213
|
decision: skipped.decision,
|
|
1205
1214
|
text: skipped.text
|
|
@@ -1213,7 +1222,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1213
1222
|
while (args.pending.length > 0) {
|
|
1214
1223
|
const skipped = args.pending.shift();
|
|
1215
1224
|
await deps.recordSkippedSteeringMessage({
|
|
1216
|
-
|
|
1225
|
+
conversationId: args.conversationId,
|
|
1217
1226
|
message: skipped.message,
|
|
1218
1227
|
decision: skipped.decision,
|
|
1219
1228
|
text: skipped.text
|
|
@@ -1227,6 +1236,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1227
1236
|
};
|
|
1228
1237
|
return {
|
|
1229
1238
|
async handleNewMention(thread, message, hooks) {
|
|
1239
|
+
const conversationId = hooks.conversationId ?? thread.id;
|
|
1230
1240
|
const processingReactions = createProcessingReactionTracker(thread);
|
|
1231
1241
|
let processingReaction;
|
|
1232
1242
|
const skippedSteeringMessages = [];
|
|
@@ -1235,7 +1245,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1235
1245
|
const onTurnCompleted = async () => {
|
|
1236
1246
|
completed = true;
|
|
1237
1247
|
await flushSkippedSteeringMessagesBestEffort({
|
|
1238
|
-
|
|
1248
|
+
conversationId,
|
|
1239
1249
|
pending: skippedSteeringMessages,
|
|
1240
1250
|
context: logContext({
|
|
1241
1251
|
threadId: getThreadId(thread, message),
|
|
@@ -1289,6 +1299,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1289
1299
|
},
|
|
1290
1300
|
onSkipped: (skipped) => handleSkippedSteeringMessages({
|
|
1291
1301
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
1302
|
+
conversationId,
|
|
1292
1303
|
pending: skippedSteeringMessages,
|
|
1293
1304
|
skipped,
|
|
1294
1305
|
thread
|
|
@@ -1302,7 +1313,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1302
1313
|
await deps.executeSlackTurn(thread, message, {
|
|
1303
1314
|
explicitMention: true,
|
|
1304
1315
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
1305
|
-
conversationId
|
|
1316
|
+
conversationId,
|
|
1306
1317
|
destination: hooks.destination,
|
|
1307
1318
|
queuedMessages,
|
|
1308
1319
|
ack,
|
|
@@ -1339,17 +1350,19 @@ function createSlackTurnRuntime(deps) {
|
|
|
1339
1350
|
let lifecycleError;
|
|
1340
1351
|
try {
|
|
1341
1352
|
await failConversationTurn({
|
|
1353
|
+
conversationId,
|
|
1342
1354
|
eventId,
|
|
1343
1355
|
failureCode,
|
|
1344
|
-
message
|
|
1345
|
-
thread
|
|
1356
|
+
message
|
|
1346
1357
|
});
|
|
1347
1358
|
} catch (error) {
|
|
1348
1359
|
lifecycleError = error;
|
|
1349
1360
|
}
|
|
1350
1361
|
await hooks.beforeFirstResponsePost?.();
|
|
1351
1362
|
await postFallbackErrorReplyWithLogging({
|
|
1363
|
+
conversationId,
|
|
1352
1364
|
thread,
|
|
1365
|
+
message,
|
|
1353
1366
|
eventId,
|
|
1354
1367
|
error: failureCause,
|
|
1355
1368
|
postFailureEventName: "mention.handler.failure_reply_post.failed"
|
|
@@ -1359,7 +1372,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1359
1372
|
if (completed) await processingReactions.completeAll();
|
|
1360
1373
|
else {
|
|
1361
1374
|
await flushSkippedSteeringMessagesBestEffort({
|
|
1362
|
-
|
|
1375
|
+
conversationId,
|
|
1363
1376
|
pending: skippedSteeringMessages,
|
|
1364
1377
|
context: logContext({
|
|
1365
1378
|
threadId: getThreadId(thread, message),
|
|
@@ -1374,6 +1387,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1374
1387
|
}
|
|
1375
1388
|
},
|
|
1376
1389
|
async handleSubscribedMessage(thread, message, hooks) {
|
|
1390
|
+
const conversationId = hooks.conversationId ?? thread.id;
|
|
1377
1391
|
const processingReactions = createProcessingReactionTracker(thread);
|
|
1378
1392
|
let processingReaction;
|
|
1379
1393
|
const skippedSteeringMessages = [];
|
|
@@ -1382,7 +1396,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1382
1396
|
const onTurnCompleted = async () => {
|
|
1383
1397
|
completed = true;
|
|
1384
1398
|
await flushSkippedSteeringMessagesBestEffort({
|
|
1385
|
-
|
|
1399
|
+
conversationId,
|
|
1386
1400
|
pending: skippedSteeringMessages,
|
|
1387
1401
|
context: logContext({
|
|
1388
1402
|
threadId: getThreadId(thread, message),
|
|
@@ -1418,6 +1432,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1418
1432
|
userText: replaceTopLevelText(content, strippedUserText)
|
|
1419
1433
|
};
|
|
1420
1434
|
const threadContext = {
|
|
1435
|
+
conversationId,
|
|
1421
1436
|
threadId,
|
|
1422
1437
|
actorId,
|
|
1423
1438
|
channelId,
|
|
@@ -1450,7 +1465,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1450
1465
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost
|
|
1451
1466
|
});
|
|
1452
1467
|
await skipSubscribedMessage({
|
|
1453
|
-
|
|
1468
|
+
conversationId,
|
|
1454
1469
|
message,
|
|
1455
1470
|
decision,
|
|
1456
1471
|
context: threadContext,
|
|
@@ -1468,7 +1483,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1468
1483
|
if (preflightDecision && !preflightDecision.shouldReply) {
|
|
1469
1484
|
const reason = preflightDecision.reasonDetail ? `${preflightDecision.reason}:${preflightDecision.reasonDetail}` : preflightDecision.reason;
|
|
1470
1485
|
await skipSubscribedMessage({
|
|
1471
|
-
|
|
1486
|
+
conversationId,
|
|
1472
1487
|
message,
|
|
1473
1488
|
decision: {
|
|
1474
1489
|
shouldReply: false,
|
|
@@ -1494,7 +1509,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1494
1509
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost
|
|
1495
1510
|
});
|
|
1496
1511
|
await skipSubscribedMessage({
|
|
1497
|
-
|
|
1512
|
+
conversationId,
|
|
1498
1513
|
message,
|
|
1499
1514
|
decision,
|
|
1500
1515
|
context: threadContext,
|
|
@@ -1513,7 +1528,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1513
1528
|
queuedMessages
|
|
1514
1529
|
});
|
|
1515
1530
|
await deps.persistPreparedState({
|
|
1516
|
-
|
|
1531
|
+
conversationId,
|
|
1517
1532
|
preparedState
|
|
1518
1533
|
});
|
|
1519
1534
|
const decision = await deps.decideSubscribedReply({
|
|
@@ -1530,7 +1545,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1530
1545
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost
|
|
1531
1546
|
})) {
|
|
1532
1547
|
await skipSubscribedMessage({
|
|
1533
|
-
|
|
1548
|
+
conversationId,
|
|
1534
1549
|
message,
|
|
1535
1550
|
decision,
|
|
1536
1551
|
context: threadContext,
|
|
@@ -1542,7 +1557,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1542
1557
|
}
|
|
1543
1558
|
if (!decision.shouldReply) {
|
|
1544
1559
|
await skipSubscribedMessage({
|
|
1545
|
-
|
|
1560
|
+
conversationId,
|
|
1546
1561
|
message,
|
|
1547
1562
|
decision,
|
|
1548
1563
|
context: threadContext,
|
|
@@ -1561,6 +1576,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1561
1576
|
},
|
|
1562
1577
|
onSkipped: (skipped) => handleSkippedSteeringMessages({
|
|
1563
1578
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
1579
|
+
conversationId,
|
|
1564
1580
|
pending: skippedSteeringMessages,
|
|
1565
1581
|
skipped,
|
|
1566
1582
|
thread
|
|
@@ -1586,7 +1602,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1586
1602
|
const toolInvocationHook = createToolInvocationHook(processingReaction, hooks);
|
|
1587
1603
|
await deps.executeSlackTurn(thread, message, {
|
|
1588
1604
|
explicitMention: Boolean(message.isMention),
|
|
1589
|
-
conversationId
|
|
1605
|
+
conversationId,
|
|
1590
1606
|
destination: hooks.destination,
|
|
1591
1607
|
preparedState,
|
|
1592
1608
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
@@ -1625,17 +1641,19 @@ function createSlackTurnRuntime(deps) {
|
|
|
1625
1641
|
let lifecycleError;
|
|
1626
1642
|
try {
|
|
1627
1643
|
await failConversationTurn({
|
|
1644
|
+
conversationId,
|
|
1628
1645
|
eventId,
|
|
1629
1646
|
failureCode,
|
|
1630
|
-
message
|
|
1631
|
-
thread
|
|
1647
|
+
message
|
|
1632
1648
|
});
|
|
1633
1649
|
} catch (error) {
|
|
1634
1650
|
lifecycleError = error;
|
|
1635
1651
|
}
|
|
1636
1652
|
await hooks.beforeFirstResponsePost?.();
|
|
1637
1653
|
await postFallbackErrorReplyWithLogging({
|
|
1654
|
+
conversationId,
|
|
1638
1655
|
thread,
|
|
1656
|
+
message,
|
|
1639
1657
|
eventId,
|
|
1640
1658
|
error: failureCause,
|
|
1641
1659
|
postFailureEventName: "subscribed_message.handler.failure_reply_post.failed"
|
|
@@ -1645,7 +1663,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1645
1663
|
if (completed) await processingReactions.completeAll();
|
|
1646
1664
|
else {
|
|
1647
1665
|
await flushSkippedSteeringMessagesBestEffort({
|
|
1648
|
-
|
|
1666
|
+
conversationId,
|
|
1649
1667
|
pending: skippedSteeringMessages,
|
|
1650
1668
|
context: logContext({
|
|
1651
1669
|
threadId: getThreadId(thread, message),
|
|
@@ -2125,6 +2143,7 @@ function restoreMessage(args) {
|
|
|
2125
2143
|
const message = Message.fromJSON(metadata.message);
|
|
2126
2144
|
message.attachments = message.attachments.map((attachment) => args.adapter.rehydrateAttachment(attachment));
|
|
2127
2145
|
rehydrateAttachmentFetchers(message);
|
|
2146
|
+
bindMessageAuthorIdentityId(message, args.record.input.authorIdentityId);
|
|
2128
2147
|
return message;
|
|
2129
2148
|
}
|
|
2130
2149
|
async function bindSlackActorIdentities(args) {
|
|
@@ -2136,6 +2155,10 @@ async function bindSlackActorIdentities(args) {
|
|
|
2136
2155
|
await Promise.all([...byAuthorId].map(async ([authorId, messages]) => {
|
|
2137
2156
|
const profile = await args.lookupSlackUser(args.teamId, authorId);
|
|
2138
2157
|
await Promise.all(messages.map((message) => ensureSlackMessageActorIdentity(message, args.teamId, async () => profile)));
|
|
2158
|
+
if (profile && messages.some(getMessageAuthorIdentityId)) await resolveActorIdentityId(getSqlExecutor(), createSlackActor(args.teamId, authorId, {
|
|
2159
|
+
fullName: profile.fullName,
|
|
2160
|
+
userName: profile.userName
|
|
2161
|
+
}));
|
|
2139
2162
|
}));
|
|
2140
2163
|
}
|
|
2141
2164
|
function restoreThread(args) {
|
|
@@ -2297,6 +2320,7 @@ function createSlackConversationWorker(options) {
|
|
|
2297
2320
|
/** Serialize a Slack message and the active trace into the durable mailbox. */
|
|
2298
2321
|
function buildSlackInboundMessage(args) {
|
|
2299
2322
|
const authorId = requireSlackAuthorId(args.message);
|
|
2323
|
+
const authorIdentityId = getMessageAuthorIdentityId(args.message);
|
|
2300
2324
|
const destination = createSlackDestination({
|
|
2301
2325
|
channelId: args.thread.channelId,
|
|
2302
2326
|
teamId: args.installation?.teamId
|
|
@@ -2319,6 +2343,7 @@ function buildSlackInboundMessage(args) {
|
|
|
2319
2343
|
input: {
|
|
2320
2344
|
text: args.message.text || " ",
|
|
2321
2345
|
authorId,
|
|
2346
|
+
...authorIdentityId ? { authorIdentityId } : void 0,
|
|
2322
2347
|
attachments: args.message.attachments,
|
|
2323
2348
|
metadata: {
|
|
2324
2349
|
platform: "slack",
|
|
@@ -2775,9 +2800,11 @@ function toConversationMessage(args) {
|
|
|
2775
2800
|
const slackTs = getMessageTimestamp(args.entry);
|
|
2776
2801
|
const messageHasPotentialImageAttachment = hasPotentialImageAttachment(args.entry.attachments);
|
|
2777
2802
|
const imageAttachmentCount = messageHasPotentialImageAttachment ? countPotentialImageAttachments(args.entry.attachments) : 0;
|
|
2803
|
+
const authorIdentityId = args.entry.author.isMe ? void 0 : getMessageAuthorIdentityId(args.entry);
|
|
2778
2804
|
return {
|
|
2779
2805
|
id: args.entry.id,
|
|
2780
2806
|
role: args.entry.author.isMe ? "assistant" : "user",
|
|
2807
|
+
...authorIdentityId ? { authorIdentityId } : void 0,
|
|
2781
2808
|
text: resolveMessageText(args),
|
|
2782
2809
|
createdAtMs: args.entry.metadata.dateSent.getTime(),
|
|
2783
2810
|
author: {
|
|
@@ -2867,578 +2894,6 @@ function isAllowedSignInEmail(email, allowedDomains) {
|
|
|
2867
2894
|
const separator = normalized.lastIndexOf("@");
|
|
2868
2895
|
return allowedDomains.length > 0 && separator > 0 && isAllowedDomain(normalized.slice(separator + 1), allowedDomains);
|
|
2869
2896
|
}
|
|
2870
|
-
// src/chat/automations/collection.ts
|
|
2871
|
-
/**
|
|
2872
|
-
* The web collection applies access before counts, filter options, and paging.
|
|
2873
|
-
* Keep its ownership rules aligned with the direct Automation reader. A public
|
|
2874
|
-
* Destination only grants access inside a viewer's linked Slack workspaces.
|
|
2875
|
-
*/
|
|
2876
|
-
function viewerAutomationCollection(user) {
|
|
2877
|
-
const db = getDb();
|
|
2878
|
-
const teamIds = user.identities.filter((identity) => identity.provider === "slack").flatMap((identity) => identity.providerTenantId ? [identity.providerTenantId] : []);
|
|
2879
|
-
const automation = juniorAutomations;
|
|
2880
|
-
const destination = juniorDestinations;
|
|
2881
|
-
const isScheduled = eq(automation.kind, "scheduled");
|
|
2882
|
-
const owned = inArray(automation.creatorIdentityId, user.identities.map((identity) => identity.id));
|
|
2883
|
-
const publicDestination = sql`coalesce(${destination.visibility} = 'public', false)`;
|
|
2884
|
-
const publicToViewer = and(inArray(automation.teamId, teamIds), publicDestination, ne(automation.status, "completed"));
|
|
2885
|
-
const runs = juniorAutomationRuns;
|
|
2886
|
-
const lastRunNeedsAttention = sql`coalesce((select ${runs.status} in ('failed', 'blocked') from ${runs}
|
|
2887
|
-
where ${runs.automationId} = ${automation.id} and ${runs.status} in ('blocked', 'completed', 'failed') and ${runs.completedAtMs} is not null
|
|
2888
|
-
order by ${runs.completedAtMs} desc, ${runs.id} desc limit 1), false)`;
|
|
2889
|
-
return db.select({
|
|
2890
|
-
kind: automation.kind,
|
|
2891
|
-
id: automation.id,
|
|
2892
|
-
title: automation.title,
|
|
2893
|
-
createdAtMs: automation.createdAtMs,
|
|
2894
|
-
state: sql`${automation.status}`.as("state"),
|
|
2895
|
-
lastRunNeedsAttention: lastRunNeedsAttention.as("last_run_needs_attention"),
|
|
2896
|
-
trigger: sql`${automation.record}->'trigger'`.as("trigger"),
|
|
2897
|
-
owned: sql`${owned}`.as("owned"),
|
|
2898
|
-
resource: sql`case when ${isScheduled} then '' else concat_ws(' ', ${automation.identifier}, ${automation.record}->'trigger'->>'label', ${automation.namespace}) end`.as("resource"),
|
|
2899
|
-
creator: sql`${automation.teamId} || ':' || ${automation.creatorSlackUserId}`.as("creator"),
|
|
2900
|
-
creatorLabel: sql`coalesce(nullif(trim(${automation.record}->'createdBy'->>'fullName'), ''), nullif(trim(${automation.record}->'createdBy'->>'userName'), ''), ${automation.creatorSlackUserId})`.as("creator_label"),
|
|
2901
|
-
destination: sql`${automation.teamId} || ':' || ${automation.channelId}`.as("destination"),
|
|
2902
|
-
destinationLabel: sql`coalesce(nullif(${destination.displayName}, ''), ${automation.channelId})`.as("destination_label"),
|
|
2903
|
-
isPublic: publicDestination.as("is_public"),
|
|
2904
|
-
instruction: automation.instruction
|
|
2905
|
-
}).from(automation).leftJoin(destination, and(eq(destination.provider, "slack"), eq(destination.providerTenantId, automation.teamId), eq(destination.providerDestinationId, automation.channelId))).where(and(ne(automation.status, "deleted"), or(owned, publicToViewer))).as("accessible_automations");
|
|
2906
|
-
}
|
|
2907
|
-
/**
|
|
2908
|
-
* List the accessible event automations whose trigger cannot work with the
|
|
2909
|
-
* enabled plugins. `eventTriggerProblems` decides, the same check that the
|
|
2910
|
-
* tools and the dashboard use, so SQL does not keep its own copy of the rules.
|
|
2911
|
-
*/
|
|
2912
|
-
async function unavailableEventAutomationIds(collection) {
|
|
2913
|
-
const catalog = automationEventCatalog(getEventCatalog());
|
|
2914
|
-
return (await getDb().select({
|
|
2915
|
-
id: collection.id,
|
|
2916
|
-
trigger: collection.trigger
|
|
2917
|
-
}).from(collection).where(eq(collection.kind, "event"))).flatMap((row) => {
|
|
2918
|
-
const trigger = eventAutomationTriggerSchema.safeParse(row.trigger);
|
|
2919
|
-
return trigger.success && registeredEventTriggerProblems(catalog, trigger.data).length === 0 ? [] : [row.id];
|
|
2920
|
-
});
|
|
2921
|
-
}
|
|
2922
|
-
/** Read an SQL page and complete, access-scoped counts and filter options. */
|
|
2923
|
-
async function readAutomationCollection(user, input) {
|
|
2924
|
-
const db = getDb();
|
|
2925
|
-
const collection = viewerAutomationCollection(user);
|
|
2926
|
-
const unavailableIds = await unavailableEventAutomationIds(collection);
|
|
2927
|
-
const unavailable = unavailableIds.length > 0 ? inArray(collection.id, unavailableIds) : sql`false`;
|
|
2928
|
-
const attention = and(notInArray(collection.state, ["paused", "completed"]), or(eq(collection.state, "blocked"), unavailable, eq(collection.lastRunNeedsAttention, true)));
|
|
2929
|
-
const where = and(input.scope === "mine" ? eq(collection.owned, true) : input.scope === "public" ? eq(collection.isPublic, true) : input.scope === "attention" ? attention : void 0, input.type === "all" ? void 0 : eq(collection.kind, input.type), input.state === "all" ? void 0 : input.state === "unavailable" ? unavailable : eq(collection.state, input.state), input.creator ? eq(collection.creator, input.creator) : void 0, input.destination ? eq(collection.destination, input.destination) : void 0, input.q ? sql`strpos(lower(concat_ws(' ', ${collection.title}, ${collection.instruction}, ${collection.resource})), ${input.q.toLowerCase()}) > 0` : void 0);
|
|
2930
|
-
const [totals, counts, creators, destinations] = await Promise.all([
|
|
2931
|
-
db.select({ total: count() }).from(collection).where(where),
|
|
2932
|
-
db.select({
|
|
2933
|
-
all: count(),
|
|
2934
|
-
mine: sql`count(*) filter (where ${collection.owned})::int`,
|
|
2935
|
-
public: sql`count(*) filter (where ${collection.isPublic})::int`,
|
|
2936
|
-
private: sql`count(*) filter (where not ${collection.isPublic})::int`
|
|
2937
|
-
}).from(collection),
|
|
2938
|
-
db.select({
|
|
2939
|
-
value: collection.creator,
|
|
2940
|
-
label: sql`min(${collection.creatorLabel})`
|
|
2941
|
-
}).from(collection).groupBy(collection.creator).orderBy(sql`min(${collection.creatorLabel})`, collection.creator),
|
|
2942
|
-
db.select({
|
|
2943
|
-
value: collection.destination,
|
|
2944
|
-
label: sql`min(${collection.destinationLabel})`
|
|
2945
|
-
}).from(collection).groupBy(collection.destination).orderBy(sql`min(${collection.destinationLabel})`, collection.destination)
|
|
2946
|
-
]);
|
|
2947
|
-
const total = totals[0].total;
|
|
2948
|
-
const page = Math.min(input.page, Math.max(1, Math.ceil(total / input.pageSize)));
|
|
2949
|
-
const order = input.sort === "oldest" ? asc(collection.createdAtMs) : input.sort === "title" ? asc(sql`lower(coalesce(nullif(trim(${collection.title}), ''), ${collection.instruction}))`) : desc(collection.createdAtMs);
|
|
2950
|
-
return {
|
|
2951
|
-
rows: await db.select({
|
|
2952
|
-
id: collection.id,
|
|
2953
|
-
kind: collection.kind
|
|
2954
|
-
}).from(collection).where(where).orderBy(order, desc(collection.id)).limit(input.pageSize).offset((page - 1) * input.pageSize),
|
|
2955
|
-
total,
|
|
2956
|
-
page,
|
|
2957
|
-
pageSize: input.pageSize,
|
|
2958
|
-
counts: counts[0],
|
|
2959
|
-
creators,
|
|
2960
|
-
destinations
|
|
2961
|
-
};
|
|
2962
|
-
}
|
|
2963
|
-
// src/chat/automations/creator.ts
|
|
2964
|
-
/**
|
|
2965
|
-
* Return whether the user created the Automation. Both kinds store the
|
|
2966
|
-
* creating Junior identity, so one rule decides ownership.
|
|
2967
|
-
*/
|
|
2968
|
-
function isAutomationCreator(user, automation) {
|
|
2969
|
-
return user.identities.some((identity) => identity.id === automation.creatorIdentityId);
|
|
2970
|
-
}
|
|
2971
|
-
// src/chat/scheduled-automations/personal.ts
|
|
2972
|
-
var PersonalScheduledAutomationNotFoundError = class extends Error {
|
|
2973
|
-
constructor() {
|
|
2974
|
-
super("Scheduled automation was not found.");
|
|
2975
|
-
this.name = "PersonalScheduledAutomationNotFoundError";
|
|
2976
|
-
}
|
|
2977
|
-
};
|
|
2978
|
-
/** Delete one scheduled automation created by the viewer. */
|
|
2979
|
-
async function deleteViewerScheduledAutomation(db, user, id, nowMs = Date.now()) {
|
|
2980
|
-
await withScheduledAutomationLock(db, id, async (tx) => {
|
|
2981
|
-
const task = await readScheduledAutomation(tx, id);
|
|
2982
|
-
if (!task || task.status === "deleted" || !isAutomationCreator(user, task)) throw new PersonalScheduledAutomationNotFoundError();
|
|
2983
|
-
await saveScheduledAutomationInLock(tx, {
|
|
2984
|
-
...task,
|
|
2985
|
-
nextRunAtMs: void 0,
|
|
2986
|
-
runNowAtMs: void 0,
|
|
2987
|
-
status: "deleted",
|
|
2988
|
-
updatedAtMs: nowMs
|
|
2989
|
-
}, task);
|
|
2990
|
-
});
|
|
2991
|
-
}
|
|
2992
|
-
// src/chat/automations/read.ts
|
|
2993
|
-
const TASK_EXECUTION_LIST_LIMIT = 100;
|
|
2994
|
-
const AUTOMATION_VERSION_LIST_LIMIT = 100;
|
|
2995
|
-
function creatorLabel(creator) {
|
|
2996
|
-
return creator.fullName?.trim() || (creator.userName?.trim() ? `@${creator.userName.trim()}` : "") || creator.slackUserId;
|
|
2997
|
-
}
|
|
2998
|
-
function creatorKey(teamId, slackUserId) {
|
|
2999
|
-
return `${teamId}:${slackUserId}`;
|
|
3000
|
-
}
|
|
3001
|
-
async function creatorProfiles(candidates) {
|
|
3002
|
-
const selectors = new Map(candidates.map(({ task }) => {
|
|
3003
|
-
const teamId = task.destination.teamId;
|
|
3004
|
-
const slackUserId = task.createdBy.slackUserId;
|
|
3005
|
-
return [creatorKey(teamId, slackUserId), {
|
|
3006
|
-
slackUserId,
|
|
3007
|
-
teamId
|
|
3008
|
-
}];
|
|
3009
|
-
}));
|
|
3010
|
-
if (selectors.size === 0) return /* @__PURE__ */ new Map();
|
|
3011
|
-
const rows = await getDb().select({
|
|
3012
|
-
avatarUrl: juniorIdentities.avatarUrl,
|
|
3013
|
-
email: juniorUsers.primaryEmailNormalized,
|
|
3014
|
-
emailVerified: juniorIdentities.emailVerified,
|
|
3015
|
-
slackUserId: juniorIdentities.providerSubjectId,
|
|
3016
|
-
teamId: juniorIdentities.providerTenantId
|
|
3017
|
-
}).from(juniorIdentities).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(eq(juniorIdentities.kind, "user"), eq(juniorIdentities.provider, "slack"), or(...[...selectors.values()].map((selector) => and(eq(juniorIdentities.providerTenantId, selector.teamId), eq(juniorIdentities.providerSubjectId, selector.slackUserId))))));
|
|
3018
|
-
return new Map(rows.map((row) => [creatorKey(row.teamId, row.slackUserId), {
|
|
3019
|
-
email: row.emailVerified ? row.email ?? void 0 : void 0,
|
|
3020
|
-
avatarUrl: row.avatarUrl ?? void 0
|
|
3021
|
-
}]));
|
|
3022
|
-
}
|
|
3023
|
-
function destinationKey(destination) {
|
|
3024
|
-
return `${destination.teamId}:${destination.channelId}`;
|
|
3025
|
-
}
|
|
3026
|
-
function displayText(value, fallback) {
|
|
3027
|
-
return value.trim() || fallback;
|
|
3028
|
-
}
|
|
3029
|
-
function taskDisplayTitle(title, instruction, fallback) {
|
|
3030
|
-
const stored = title?.trim();
|
|
3031
|
-
if (stored) return stored;
|
|
3032
|
-
return fallbackShortTitle(instruction, fallback);
|
|
3033
|
-
}
|
|
3034
|
-
async function destinationDetails(destinations) {
|
|
3035
|
-
const selectors = new Map(destinations.map((destination) => [destinationKey(destination), destination]));
|
|
3036
|
-
if (selectors.size === 0) return /* @__PURE__ */ new Map();
|
|
3037
|
-
const rows = await getDb().select({
|
|
3038
|
-
displayName: juniorDestinations.displayName,
|
|
3039
|
-
kind: juniorDestinations.kind,
|
|
3040
|
-
providerDestinationId: juniorDestinations.providerDestinationId,
|
|
3041
|
-
providerTenantId: juniorDestinations.providerTenantId,
|
|
3042
|
-
visibility: juniorDestinations.visibility
|
|
3043
|
-
}).from(juniorDestinations).where(or(...[...selectors.values()].map((destination) => and(eq(juniorDestinations.provider, "slack"), eq(juniorDestinations.providerTenantId, destination.teamId), eq(juniorDestinations.providerDestinationId, destination.channelId)))));
|
|
3044
|
-
return new Map(rows.map((row) => {
|
|
3045
|
-
const name = row.displayName?.trim();
|
|
3046
|
-
const label = name ? row.kind === "channel" ? `#${name.replace(/^#/, "")}` : name : row.kind === "dm" ? "Direct message" : row.kind === "group" ? "Group message" : `Channel ${row.providerDestinationId}`;
|
|
3047
|
-
return [`${row.providerTenantId}:${row.providerDestinationId}`, {
|
|
3048
|
-
label,
|
|
3049
|
-
visibility: row.visibility === "public" ? "public" : "private"
|
|
3050
|
-
}];
|
|
3051
|
-
}));
|
|
3052
|
-
}
|
|
3053
|
-
function executionSummaryFields(stats) {
|
|
3054
|
-
return {
|
|
3055
|
-
lastRunStatus: stats?.lastRunStatus,
|
|
3056
|
-
...stats?.lastConversationId ? { lastConversationId: stats.lastConversationId } : void 0,
|
|
3057
|
-
...stats?.lastExecutedAtMs ? { lastRunAt: new Date(stats.lastExecutedAtMs).toISOString() } : void 0,
|
|
3058
|
-
costUsd: stats?.costUsd ?? emptyAutomationRunWindows(),
|
|
3059
|
-
runs: stats?.runs ?? emptyAutomationRunWindows(),
|
|
3060
|
-
totalRuns: stats?.totalRuns ?? 0
|
|
3061
|
-
};
|
|
3062
|
-
}
|
|
3063
|
-
function scheduledAutomationSummary(task, ownedByViewer, destination, stats, creator) {
|
|
3064
|
-
if (task.status === "deleted") throw new Error("Deleted scheduled automations cannot enter the Automations view");
|
|
3065
|
-
const nextRunAtMs = task.status === "active" ? task.runNowAtMs ?? task.nextRunAtMs : void 0;
|
|
3066
|
-
const instruction = displayText(task.task.text, "Untitled scheduled automation");
|
|
3067
|
-
return {
|
|
3068
|
-
createdAt: new Date(task.createdAtMs).toISOString(),
|
|
3069
|
-
createdBy: creatorLabel(task.createdBy),
|
|
3070
|
-
createdByEmail: creator?.email,
|
|
3071
|
-
createdByAvatarUrl: creator?.avatarUrl,
|
|
3072
|
-
destination: {
|
|
3073
|
-
channelId: task.destination.channelId,
|
|
3074
|
-
label: destination.label,
|
|
3075
|
-
teamId: task.destination.teamId,
|
|
3076
|
-
visibility: destination.visibility
|
|
3077
|
-
},
|
|
3078
|
-
id: task.id,
|
|
3079
|
-
instruction,
|
|
3080
|
-
kind: "scheduled",
|
|
3081
|
-
...executionSummaryFields(stats),
|
|
3082
|
-
...nextRunAtMs !== void 0 ? { nextRunAt: new Date(nextRunAtMs).toISOString() } : void 0,
|
|
3083
|
-
ownedByViewer,
|
|
3084
|
-
schedule: displayText(task.schedule.description, "Schedule unavailable"),
|
|
3085
|
-
status: task.status,
|
|
3086
|
-
statusReason: task.statusReason,
|
|
3087
|
-
timezone: task.schedule.timezone,
|
|
3088
|
-
credentialMode: task.credentialMode,
|
|
3089
|
-
outcomes: effectiveTaskOutcomes(task.outcomes, task.destination),
|
|
3090
|
-
title: taskDisplayTitle(task.title, instruction, "Untitled scheduled automation")
|
|
3091
|
-
};
|
|
3092
|
-
}
|
|
3093
|
-
function eventAutomationSummary(task, ownedByViewer, destination, stats, creator) {
|
|
3094
|
-
const instruction = task.task.text;
|
|
3095
|
-
const catalog = getEventCatalog();
|
|
3096
|
-
return {
|
|
3097
|
-
createdAt: new Date(task.createdAtMs).toISOString(),
|
|
3098
|
-
createdBy: creatorLabel(task.createdBy),
|
|
3099
|
-
createdByEmail: creator?.email,
|
|
3100
|
-
createdByAvatarUrl: creator?.avatarUrl,
|
|
3101
|
-
destination: {
|
|
3102
|
-
channelId: task.destination.channelId,
|
|
3103
|
-
label: destination.label,
|
|
3104
|
-
teamId: task.destination.teamId,
|
|
3105
|
-
visibility: destination.visibility
|
|
3106
|
-
},
|
|
3107
|
-
id: task.id,
|
|
3108
|
-
instruction,
|
|
3109
|
-
kind: "event",
|
|
3110
|
-
...executionSummaryFields(stats),
|
|
3111
|
-
ownedByViewer,
|
|
3112
|
-
resource: task.trigger.identifier ? `${task.trigger.label} · ${task.trigger.identifier}` : task.trigger.label,
|
|
3113
|
-
source: task.trigger.namespace,
|
|
3114
|
-
credentialMode: task.credentialMode,
|
|
3115
|
-
outcomes: effectiveTaskOutcomes(task.outcomes, task.destination),
|
|
3116
|
-
title: taskDisplayTitle(task.title, instruction, "Untitled event automation"),
|
|
3117
|
-
status: task.status === "deleted" ? "active" : task.status,
|
|
3118
|
-
statusReason: task.statusReason,
|
|
3119
|
-
trigger: describeEventAutomationTrigger(task.trigger, catalog)
|
|
3120
|
-
};
|
|
3121
|
-
}
|
|
3122
|
-
function viewerTeamIds(user) {
|
|
3123
|
-
return [...new Set(user.identities.filter((identity) => identity.provider === "slack").map((identity) => identity.providerTenantId).filter((teamId) => Boolean(teamId)))];
|
|
3124
|
-
}
|
|
3125
|
-
/** Resolve an Automation the viewer owns or can read through a public Destination. */
|
|
3126
|
-
async function resolveViewerTaskCandidate(user, id) {
|
|
3127
|
-
const [row] = await getDb().select().from(juniorAutomations).where(eq(juniorAutomations.id, id)).limit(1);
|
|
3128
|
-
if (!row) return void 0;
|
|
3129
|
-
let candidate;
|
|
3130
|
-
if (row.kind === "scheduled") {
|
|
3131
|
-
const task = parseScheduledAutomationRow(row);
|
|
3132
|
-
if (!task) return void 0;
|
|
3133
|
-
candidate = {
|
|
3134
|
-
kind: "scheduled",
|
|
3135
|
-
ownedByViewer: isAutomationCreator(user, task),
|
|
3136
|
-
task
|
|
3137
|
-
};
|
|
3138
|
-
} else {
|
|
3139
|
-
const task = parseEventAutomationRow(row);
|
|
3140
|
-
candidate = {
|
|
3141
|
-
kind: "event",
|
|
3142
|
-
ownedByViewer: isAutomationCreator(user, task),
|
|
3143
|
-
task
|
|
3144
|
-
};
|
|
3145
|
-
}
|
|
3146
|
-
const { task } = candidate;
|
|
3147
|
-
if (task.status === "deleted") return void 0;
|
|
3148
|
-
if (candidate.ownedByViewer) return candidate;
|
|
3149
|
-
const destination = (await destinationDetails([task.destination])).get(destinationKey(task.destination));
|
|
3150
|
-
return viewerTeamIds(user).includes(task.destination.teamId) && destination?.visibility === "public" ? candidate : void 0;
|
|
3151
|
-
}
|
|
3152
|
-
/** Read one current Automation summary with the Automations view access rules. */
|
|
3153
|
-
async function readViewerAutomationSummary(user, id) {
|
|
3154
|
-
const candidate = await resolveViewerTaskCandidate(user, id);
|
|
3155
|
-
if (!candidate) return void 0;
|
|
3156
|
-
return automationSummaryForCandidate(candidate);
|
|
3157
|
-
}
|
|
3158
|
-
async function automationSummaryForCandidate(candidate) {
|
|
3159
|
-
const [destinations, creators, stats] = await Promise.all([
|
|
3160
|
-
destinationDetails([candidate.task.destination]),
|
|
3161
|
-
creatorProfiles([candidate]),
|
|
3162
|
-
readAutomationExecutionSummaries([candidate.task.id])
|
|
3163
|
-
]);
|
|
3164
|
-
const destination = destinations.get(destinationKey(candidate.task.destination)) ?? {
|
|
3165
|
-
label: `Channel ${candidate.task.destination.channelId}`,
|
|
3166
|
-
visibility: "private"
|
|
3167
|
-
};
|
|
3168
|
-
const creator = creators.get(creatorKey(candidate.task.destination.teamId, candidate.task.createdBy.slackUserId));
|
|
3169
|
-
if (candidate.kind === "scheduled") return scheduledAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), creator);
|
|
3170
|
-
return eventAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), creator);
|
|
3171
|
-
}
|
|
3172
|
-
function emptyAutomationExecutionDay(date) {
|
|
3173
|
-
return {
|
|
3174
|
-
costUsd: 0,
|
|
3175
|
-
date,
|
|
3176
|
-
event: 0,
|
|
3177
|
-
scheduled: 0
|
|
3178
|
-
};
|
|
3179
|
-
}
|
|
3180
|
-
function emptyAutomationExecutionStatusDay(date) {
|
|
3181
|
-
return {
|
|
3182
|
-
blocked: 0,
|
|
3183
|
-
completed: 0,
|
|
3184
|
-
date,
|
|
3185
|
-
failed: 0
|
|
3186
|
-
};
|
|
3187
|
-
}
|
|
3188
|
-
function automationExecutionSixHours(hours, nowMs = Date.now()) {
|
|
3189
|
-
return sumUtcHoursIntoSixHours({
|
|
3190
|
-
empty: emptyAutomationExecutionDay,
|
|
3191
|
-
hours,
|
|
3192
|
-
nowMs
|
|
3193
|
-
});
|
|
3194
|
-
}
|
|
3195
|
-
function automationExecutionStatusSixHours(hours, nowMs = Date.now()) {
|
|
3196
|
-
return sumUtcHoursIntoSixHours({
|
|
3197
|
-
empty: emptyAutomationExecutionStatusDay,
|
|
3198
|
-
hours,
|
|
3199
|
-
nowMs
|
|
3200
|
-
});
|
|
3201
|
-
}
|
|
3202
|
-
/** Read a page from the full accessible Automation collection. */
|
|
3203
|
-
async function readViewerAutomations(user, input = automationListQuerySchema.parse({})) {
|
|
3204
|
-
const { rows, ...page } = await readAutomationCollection(user, input);
|
|
3205
|
-
const db = getDb();
|
|
3206
|
-
const ids = rows.map((row) => row.id);
|
|
3207
|
-
const stored = ids.length ? await db.select().from(juniorAutomations).where(inArray(juniorAutomations.id, ids)) : [];
|
|
3208
|
-
const candidates = /* @__PURE__ */ new Map();
|
|
3209
|
-
for (const row of stored) {
|
|
3210
|
-
if (row.kind === "scheduled") {
|
|
3211
|
-
const task = parseScheduledAutomationRow(row);
|
|
3212
|
-
if (task && task.status !== "deleted") candidates.set(task.id, {
|
|
3213
|
-
kind: "scheduled",
|
|
3214
|
-
task,
|
|
3215
|
-
ownedByViewer: isAutomationCreator(user, task)
|
|
3216
|
-
});
|
|
3217
|
-
continue;
|
|
3218
|
-
}
|
|
3219
|
-
const task = parseEventAutomationRow(row);
|
|
3220
|
-
if (task.status !== "deleted") candidates.set(task.id, {
|
|
3221
|
-
kind: "event",
|
|
3222
|
-
task,
|
|
3223
|
-
ownedByViewer: isAutomationCreator(user, task)
|
|
3224
|
-
});
|
|
3225
|
-
}
|
|
3226
|
-
const selected = rows.flatMap((row) => {
|
|
3227
|
-
const candidate = candidates.get(row.id);
|
|
3228
|
-
return candidate ? [candidate] : [];
|
|
3229
|
-
});
|
|
3230
|
-
const [destinations, creators] = await Promise.all([destinationDetails(selected.map(({ task }) => task.destination)), creatorProfiles(selected)]);
|
|
3231
|
-
const visible = selected.filter((candidate) => candidate.ownedByViewer || destinations.get(destinationKey(candidate.task.destination))?.visibility === "public");
|
|
3232
|
-
const collection = viewerAutomationCollection(user);
|
|
3233
|
-
const access = exists(getDb().select({ id: collection.id }).from(collection).where(eq(collection.id, juniorAutomationRuns.automationId)));
|
|
3234
|
-
const [executionDays, executionHours, stats] = await Promise.all([
|
|
3235
|
-
readAutomationExecutionDays(90, { access }),
|
|
3236
|
-
readAutomationExecutionHours(168, { access }),
|
|
3237
|
-
readAutomationExecutionSummaries(ids)
|
|
3238
|
-
]);
|
|
3239
|
-
const automations = visible.map((candidate) => {
|
|
3240
|
-
const destination = destinations.get(destinationKey(candidate.task.destination)) ?? {
|
|
3241
|
-
label: `Channel ${candidate.task.destination.channelId}`,
|
|
3242
|
-
visibility: "private"
|
|
3243
|
-
};
|
|
3244
|
-
const creator = creators.get(creatorKey(candidate.task.destination.teamId, candidate.task.createdBy.slackUserId));
|
|
3245
|
-
if (candidate.kind === "scheduled") return scheduledAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), creator);
|
|
3246
|
-
return eventAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), creator);
|
|
3247
|
-
});
|
|
3248
|
-
return {
|
|
3249
|
-
executionDays,
|
|
3250
|
-
executionHours,
|
|
3251
|
-
executionSixHours: automationExecutionSixHours(executionHours),
|
|
3252
|
-
automations,
|
|
3253
|
-
...page
|
|
3254
|
-
};
|
|
3255
|
-
}
|
|
3256
|
-
/** Load deleted scheduled automations the viewer still owns, newest first. */
|
|
3257
|
-
async function readDeletedOwnedScheduledAutomations(user) {
|
|
3258
|
-
const identityIds = user.identities.map((identity) => identity.id);
|
|
3259
|
-
if (identityIds.length === 0) return [];
|
|
3260
|
-
return (await getDb().select().from(juniorAutomations).where(and(eq(juniorAutomations.kind, "scheduled"), eq(juniorAutomations.status, "deleted"), inArray(juniorAutomations.creatorIdentityId, identityIds))).orderBy(desc(juniorAutomations.createdAtMs), desc(juniorAutomations.id)).limit(TASK_EXECUTION_LIST_LIMIT)).map(parseScheduledAutomationRow).filter((task) => task?.status === "deleted");
|
|
3261
|
-
}
|
|
3262
|
-
/** Load deleted event automations the viewer still owns, newest first. */
|
|
3263
|
-
async function readDeletedOwnedEventAutomations(user) {
|
|
3264
|
-
return await listDeletedEventAutomationsCreatedBy(getDb(), user, TASK_EXECUTION_LIST_LIMIT);
|
|
3265
|
-
}
|
|
3266
|
-
function automationTitleForRun(run, automationTitles) {
|
|
3267
|
-
return automationTitles.get(`${run.kind}:${run.automationId}`) ?? run.title?.trim() ?? "Untitled task";
|
|
3268
|
-
}
|
|
3269
|
-
function addDeletedAutomationRun(args, automationTitles, automations) {
|
|
3270
|
-
const key = `${args.kind}:${args.automationId}`;
|
|
3271
|
-
if (automationTitles.has(key)) return;
|
|
3272
|
-
automationTitles.set(key, args.title);
|
|
3273
|
-
automations.push({
|
|
3274
|
-
kind: args.kind,
|
|
3275
|
-
automationId: args.automationId
|
|
3276
|
-
});
|
|
3277
|
-
}
|
|
3278
|
-
/**
|
|
3279
|
-
* Read newest runs across live viewer-visible automations and deleted automations the
|
|
3280
|
-
* viewer owns. Deleted task rows stay available so history does not depend on
|
|
3281
|
-
* conversation actor membership.
|
|
3282
|
-
*/
|
|
3283
|
-
async function readViewerAutomationRuns(user) {
|
|
3284
|
-
const [taskList, deletedScheduled, deletedEvent] = await Promise.all([
|
|
3285
|
-
getDb().select().from(viewerAutomationCollection(user)),
|
|
3286
|
-
readDeletedOwnedScheduledAutomations(user),
|
|
3287
|
-
readDeletedOwnedEventAutomations(user)
|
|
3288
|
-
]);
|
|
3289
|
-
const automationTitles = /* @__PURE__ */ new Map();
|
|
3290
|
-
const automations = [];
|
|
3291
|
-
for (const task of taskList) {
|
|
3292
|
-
automationTitles.set(`${task.kind}:${task.id}`, taskDisplayTitle(task.title ?? void 0, task.instruction, `Untitled ${task.kind} automation`));
|
|
3293
|
-
automations.push({
|
|
3294
|
-
kind: task.kind,
|
|
3295
|
-
automationId: task.id
|
|
3296
|
-
});
|
|
3297
|
-
}
|
|
3298
|
-
for (const task of deletedScheduled) {
|
|
3299
|
-
const instruction = displayText(task.task.text, "Untitled scheduled automation");
|
|
3300
|
-
addDeletedAutomationRun({
|
|
3301
|
-
kind: "scheduled",
|
|
3302
|
-
automationId: task.id,
|
|
3303
|
-
title: taskDisplayTitle(task.title, instruction, "Untitled scheduled automation")
|
|
3304
|
-
}, automationTitles, automations);
|
|
3305
|
-
}
|
|
3306
|
-
for (const task of deletedEvent) {
|
|
3307
|
-
const instruction = displayText(task.task.text, "Untitled event automation");
|
|
3308
|
-
addDeletedAutomationRun({
|
|
3309
|
-
kind: "event",
|
|
3310
|
-
automationId: task.id,
|
|
3311
|
-
title: taskDisplayTitle(task.title, instruction, "Untitled event automation")
|
|
3312
|
-
}, automationTitles, automations);
|
|
3313
|
-
}
|
|
3314
|
-
const runs = await readAutomationRuns({
|
|
3315
|
-
automationIds: automations.map((automation) => automation.automationId),
|
|
3316
|
-
limit: 101
|
|
3317
|
-
});
|
|
3318
|
-
return {
|
|
3319
|
-
runs: runs.slice(0, TASK_EXECUTION_LIST_LIMIT).map((run) => ({
|
|
3320
|
-
...run,
|
|
3321
|
-
automationTitle: automationTitleForRun(run, automationTitles)
|
|
3322
|
-
})),
|
|
3323
|
-
truncated: runs.length > TASK_EXECUTION_LIST_LIMIT
|
|
3324
|
-
};
|
|
3325
|
-
}
|
|
3326
|
-
var ViewerTaskNotFoundError = class extends Error {
|
|
3327
|
-
constructor() {
|
|
3328
|
-
super("Task was not found.");
|
|
3329
|
-
this.name = "ViewerTaskNotFoundError";
|
|
3330
|
-
}
|
|
3331
|
-
};
|
|
3332
|
-
/** Read one viewer-visible task and its newest terminal executions. */
|
|
3333
|
-
async function readViewerAutomationExecutions(user, id) {
|
|
3334
|
-
const candidate = await resolveViewerTaskCandidate(user, id);
|
|
3335
|
-
if (!candidate) throw new ViewerTaskNotFoundError();
|
|
3336
|
-
const [automation, executions, executionDays, executionHours] = await Promise.all([
|
|
3337
|
-
automationSummaryForCandidate(candidate),
|
|
3338
|
-
readAutomationExecutions({
|
|
3339
|
-
automationId: id,
|
|
3340
|
-
limit: 101
|
|
3341
|
-
}),
|
|
3342
|
-
readAutomationExecutionStatusDays({ automationId: id }),
|
|
3343
|
-
readAutomationExecutionStatusHours({ automationId: id })
|
|
3344
|
-
]);
|
|
3345
|
-
return {
|
|
3346
|
-
executionDays,
|
|
3347
|
-
executionHours,
|
|
3348
|
-
executionSixHours: automationExecutionStatusSixHours(executionHours),
|
|
3349
|
-
executions: executions.slice(0, TASK_EXECUTION_LIST_LIMIT),
|
|
3350
|
-
automation,
|
|
3351
|
-
truncated: executions.length > TASK_EXECUTION_LIST_LIMIT
|
|
3352
|
-
};
|
|
3353
|
-
}
|
|
3354
|
-
/** Read saved definitions for one viewer-visible Automation, newest first. */
|
|
3355
|
-
async function readViewerAutomationVersions(user, id) {
|
|
3356
|
-
const candidate = await resolveViewerTaskCandidate(user, id);
|
|
3357
|
-
if (!candidate) throw new ViewerTaskNotFoundError();
|
|
3358
|
-
const versions = await listAutomationVersions(getDb(), id, 101);
|
|
3359
|
-
const current = automationDefinition(candidate.task);
|
|
3360
|
-
const listed = versions.slice(0, AUTOMATION_VERSION_LIST_LIMIT);
|
|
3361
|
-
const catalog = getEventCatalog();
|
|
3362
|
-
return {
|
|
3363
|
-
versions: listed.map((version) => version.kind === "event" ? {
|
|
3364
|
-
...version,
|
|
3365
|
-
trigger: describeEventAutomationTrigger(version.definition.trigger, catalog)
|
|
3366
|
-
} : version),
|
|
3367
|
-
activeVersion: listed.find(({ definition }) => sameDefinitionValue(definition, current))?.version ?? null,
|
|
3368
|
-
truncated: versions.length > AUTOMATION_VERSION_LIST_LIMIT
|
|
3369
|
-
};
|
|
3370
|
-
}
|
|
3371
|
-
/** Name the event in a trigger, such as "opened" for `pull_request.opened`. */
|
|
3372
|
-
function eventActionLabel(eventName) {
|
|
3373
|
-
const [, ...action] = eventName.split(".");
|
|
3374
|
-
return (action.length > 0 ? action : [eventName]).join(" ").replaceAll("_", " ");
|
|
3375
|
-
}
|
|
3376
|
-
/**
|
|
3377
|
-
* Read whose access one run used from its dispatch record. Return nothing when
|
|
3378
|
-
* the record is gone, because the Automation's current setting can differ.
|
|
3379
|
-
*/
|
|
3380
|
-
async function readRunCredential(dispatchId, creator) {
|
|
3381
|
-
if (!dispatchId) return {};
|
|
3382
|
-
const dispatch = await getDispatchRecord(dispatchId);
|
|
3383
|
-
if (!dispatch) return {};
|
|
3384
|
-
const subject = dispatch.credentialSubject;
|
|
3385
|
-
if (!subject) return { credentialMode: "system" };
|
|
3386
|
-
return {
|
|
3387
|
-
credentialMode: "creator",
|
|
3388
|
-
credentialUser: subject.userId === creator.slackUserId ? creatorLabel(creator) : subject.userId
|
|
3389
|
-
};
|
|
3390
|
-
}
|
|
3391
|
-
/** Resolve the source task for one conversation when an Automation run links it. */
|
|
3392
|
-
async function readConversationSourceTask(args) {
|
|
3393
|
-
const execution = await readAutomationExecutionByConversationId({ conversationId: args.conversationId });
|
|
3394
|
-
if (!execution) return void 0;
|
|
3395
|
-
const dispatchId = getDispatchIdForConversation(args.conversationId);
|
|
3396
|
-
const base = {
|
|
3397
|
-
kind: execution.kind,
|
|
3398
|
-
...dispatchId ? { triggerMessageId: getDispatchInputMessageId(dispatchId) } : void 0
|
|
3399
|
-
};
|
|
3400
|
-
if (!args.viewer) return base;
|
|
3401
|
-
const candidate = await resolveViewerTaskCandidate(args.viewer, execution.automationId);
|
|
3402
|
-
if (!candidate) return base;
|
|
3403
|
-
const credential = await readRunCredential(dispatchId, candidate.task.createdBy);
|
|
3404
|
-
if (candidate.kind === "scheduled") {
|
|
3405
|
-
const instruction = displayText(candidate.task.task.text, "Untitled scheduled automation");
|
|
3406
|
-
const schedule = candidate.task.schedule.description.trim();
|
|
3407
|
-
return {
|
|
3408
|
-
...base,
|
|
3409
|
-
...credential,
|
|
3410
|
-
id: candidate.task.id,
|
|
3411
|
-
kind: "scheduled",
|
|
3412
|
-
label: instruction,
|
|
3413
|
-
title: taskDisplayTitle(candidate.task.title, instruction, "Untitled scheduled automation"),
|
|
3414
|
-
...schedule ? { triggerLabel: schedule } : void 0
|
|
3415
|
-
};
|
|
3416
|
-
}
|
|
3417
|
-
const instruction = displayText(candidate.task.task.text, "Untitled event automation");
|
|
3418
|
-
const { events, label } = candidate.task.trigger;
|
|
3419
|
-
return {
|
|
3420
|
-
...base,
|
|
3421
|
-
...credential,
|
|
3422
|
-
id: candidate.task.id,
|
|
3423
|
-
kind: "event",
|
|
3424
|
-
label: instruction,
|
|
3425
|
-
title: taskDisplayTitle(candidate.task.title, instruction, "Untitled event automation"),
|
|
3426
|
-
triggerLabel: events.length === 1 ? `${label} ${eventActionLabel(events[0])}` : label
|
|
3427
|
-
};
|
|
3428
|
-
}
|
|
3429
|
-
/** Delete one viewer-owned scheduled or event automation. */
|
|
3430
|
-
async function deleteViewerTask(user, id) {
|
|
3431
|
-
const candidate = await resolveViewerTaskCandidate(user, id);
|
|
3432
|
-
if (!candidate?.ownedByViewer) throw new ViewerTaskNotFoundError();
|
|
3433
|
-
if (candidate.kind === "scheduled") try {
|
|
3434
|
-
await deleteViewerScheduledAutomation(getDb(), user, id);
|
|
3435
|
-
return;
|
|
3436
|
-
} catch (error) {
|
|
3437
|
-
if (error instanceof PersonalScheduledAutomationNotFoundError) throw new ViewerTaskNotFoundError();
|
|
3438
|
-
throw error;
|
|
3439
|
-
}
|
|
3440
|
-
await deleteEventAutomation(getDb(), id);
|
|
3441
|
-
}
|
|
3442
2897
|
// src/chat/slack/assistant-thread/status-send.ts
|
|
3443
2898
|
/**
|
|
3444
2899
|
* Slack's assistant loading UI accepts both `status` and `loading_messages`,
|
|
@@ -3592,292 +3047,6 @@ function readSlackActionToken(message) {
|
|
|
3592
3047
|
const token = slackActionTokenSchema.safeParse(envelope.data.raw?.action_token);
|
|
3593
3048
|
return token.success ? token.data : void 0;
|
|
3594
3049
|
}
|
|
3595
|
-
// src/chat/slack/cards.ts
|
|
3596
|
-
/** Select the Slack object renderer for a built-in card. */
|
|
3597
|
-
function renderSlackCard(card, conversationId) {
|
|
3598
|
-
switch (card.kind) {
|
|
3599
|
-
case "object": return renderSlackObjectCard(card, conversationId);
|
|
3600
|
-
case "automation": return renderSlackAutomationCard(card);
|
|
3601
|
-
}
|
|
3602
|
-
}
|
|
3603
|
-
// src/chat/slack/footer.ts
|
|
3604
|
-
/** Render compact reply attribution for the Slack footer. */
|
|
3605
|
-
function formatReplyAttribution(attribution) {
|
|
3606
|
-
return attribution.detail ? `${attribution.label} · ${attribution.detail}` : attribution.label;
|
|
3607
|
-
}
|
|
3608
|
-
/**
|
|
3609
|
-
* Build the compact conversation footer for visible Slack reply surfaces.
|
|
3610
|
-
*
|
|
3611
|
-
* Detailed turn metrics stay in the dashboard instead of Slack-visible copy.
|
|
3612
|
-
*/
|
|
3613
|
-
function buildSlackReplyFooter(args) {
|
|
3614
|
-
const items = [];
|
|
3615
|
-
const conversationId = args.conversationId?.trim();
|
|
3616
|
-
if (conversationId) {
|
|
3617
|
-
const idItem = {
|
|
3618
|
-
label: "ID",
|
|
3619
|
-
value: conversationId
|
|
3620
|
-
};
|
|
3621
|
-
const conversationUrl = getPluginSlackConversationLink(conversationId)?.url ?? getDashboardConversationLink(conversationId) ?? buildSentryConversationUrl(conversationId);
|
|
3622
|
-
if (conversationUrl) idItem.url = conversationUrl;
|
|
3623
|
-
items.push(idItem);
|
|
3624
|
-
}
|
|
3625
|
-
return items.length > 0 || args.replyAttribution ? {
|
|
3626
|
-
...args.replyAttribution ? { attribution: args.replyAttribution } : void 0,
|
|
3627
|
-
items
|
|
3628
|
-
} : void 0;
|
|
3629
|
-
}
|
|
3630
|
-
/** Build Slack blocks for a reply chunk using the Slack-flavored markdown block for the body. */
|
|
3631
|
-
function buildSlackReplyBlocks(text, footer) {
|
|
3632
|
-
const blocks = text.trim() ? [{
|
|
3633
|
-
type: "markdown",
|
|
3634
|
-
text
|
|
3635
|
-
}] : [];
|
|
3636
|
-
if (footer && (footer.attribution || footer.items.length > 0)) {
|
|
3637
|
-
const attributionElements = footer.attribution ? [{
|
|
3638
|
-
type: "plain_text",
|
|
3639
|
-
text: formatReplyAttribution(footer.attribution)
|
|
3640
|
-
}] : [];
|
|
3641
|
-
blocks.push({
|
|
3642
|
-
type: "context",
|
|
3643
|
-
elements: [...attributionElements, ...footer.items.map((item) => ({
|
|
3644
|
-
type: "mrkdwn",
|
|
3645
|
-
text: item.url ? `*${escapeSlackMrkdwnText(item.label)}:* ${formatSlackLink(item.url, item.value)}` : `*${escapeSlackMrkdwnText(item.label)}:* ${escapeSlackMrkdwnText(item.value)}`
|
|
3646
|
-
}))]
|
|
3647
|
-
});
|
|
3648
|
-
}
|
|
3649
|
-
return blocks.length ? blocks : void 0;
|
|
3650
|
-
}
|
|
3651
|
-
// src/chat/slack/output.ts
|
|
3652
|
-
const MAX_INLINE_CHARS = 2200;
|
|
3653
|
-
const MAX_INLINE_LINES = 45;
|
|
3654
|
-
const CONTINUED_MARKER = "\n\n[Continued below]";
|
|
3655
|
-
function countSlackLines(text) {
|
|
3656
|
-
if (!text) return 0;
|
|
3657
|
-
return text.split("\n").length;
|
|
3658
|
-
}
|
|
3659
|
-
function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
3660
|
-
return text.length <= maxChars && countSlackLines(text) <= maxLines;
|
|
3661
|
-
}
|
|
3662
|
-
function findSplitIndex(text, maxChars) {
|
|
3663
|
-
if (text.length <= maxChars) return text.length;
|
|
3664
|
-
const bounded = text.slice(0, maxChars);
|
|
3665
|
-
const newlineIndex = bounded.lastIndexOf("\n");
|
|
3666
|
-
if (newlineIndex > 0) return newlineIndex;
|
|
3667
|
-
const spaceIndex = bounded.lastIndexOf(" ");
|
|
3668
|
-
if (spaceIndex > 0) return spaceIndex;
|
|
3669
|
-
return maxChars;
|
|
3670
|
-
}
|
|
3671
|
-
function splitByLineBudget(text, maxLines) {
|
|
3672
|
-
if (maxLines <= 0) return "";
|
|
3673
|
-
const lines = text.split("\n");
|
|
3674
|
-
if (lines.length <= maxLines) return text;
|
|
3675
|
-
return lines.slice(0, maxLines).join("\n");
|
|
3676
|
-
}
|
|
3677
|
-
function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
|
|
3678
|
-
return {
|
|
3679
|
-
maxChars: Math.max(1, maxChars - suffix.length),
|
|
3680
|
-
maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
|
|
3681
|
-
};
|
|
3682
|
-
}
|
|
3683
|
-
function forceSplitBudget(text, budget) {
|
|
3684
|
-
const lineCount = countSlackLines(text);
|
|
3685
|
-
return {
|
|
3686
|
-
maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
|
|
3687
|
-
maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
|
|
3688
|
-
};
|
|
3689
|
-
}
|
|
3690
|
-
function getFenceContinuation(text) {
|
|
3691
|
-
let open;
|
|
3692
|
-
for (const line of text.split("\n")) {
|
|
3693
|
-
const trimmed = line.trimStart();
|
|
3694
|
-
const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
|
|
3695
|
-
if (!openerMatch?.[1]) continue;
|
|
3696
|
-
if (!open) {
|
|
3697
|
-
open = {
|
|
3698
|
-
fence: openerMatch[1],
|
|
3699
|
-
openerLine: trimmed
|
|
3700
|
-
};
|
|
3701
|
-
continue;
|
|
3702
|
-
}
|
|
3703
|
-
if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) open = void 0;
|
|
3704
|
-
}
|
|
3705
|
-
if (!open) return null;
|
|
3706
|
-
return {
|
|
3707
|
-
closeSuffix: text.endsWith("\n") ? open.fence : `\n${open.fence}`,
|
|
3708
|
-
reopenPrefix: `${open.openerLine}\n`
|
|
3709
|
-
};
|
|
3710
|
-
}
|
|
3711
|
-
function appendSlackSuffix(text, marker) {
|
|
3712
|
-
return `${text}${getFenceContinuation(text)?.closeSuffix ?? ""}${marker}`;
|
|
3713
|
-
}
|
|
3714
|
-
function stripTrailingContinuationMarker(text) {
|
|
3715
|
-
return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -19) : text;
|
|
3716
|
-
}
|
|
3717
|
-
function takeSlackContinuationChunk(text, budget) {
|
|
3718
|
-
let { prefix, rest } = takeSlackInlinePrefix(text, budget);
|
|
3719
|
-
if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, budget)));
|
|
3720
|
-
let carryover = rest ? getFenceContinuation(prefix) : null;
|
|
3721
|
-
if (!carryover) return {
|
|
3722
|
-
prefix,
|
|
3723
|
-
rest
|
|
3724
|
-
};
|
|
3725
|
-
const carryoverBudget = reserveInlineBudgetForSuffix(`${carryover.closeSuffix}${CONTINUED_MARKER}`);
|
|
3726
|
-
({prefix, rest} = takeSlackInlinePrefix(text, carryoverBudget));
|
|
3727
|
-
if (!rest) ({prefix, rest} = takeSlackInlinePrefix(text, forceSplitBudget(text, carryoverBudget)));
|
|
3728
|
-
carryover = rest ? getFenceContinuation(prefix) : null;
|
|
3729
|
-
if (!carryover) return {
|
|
3730
|
-
prefix,
|
|
3731
|
-
rest
|
|
3732
|
-
};
|
|
3733
|
-
return {
|
|
3734
|
-
prefix,
|
|
3735
|
-
rest: `${carryover.reopenPrefix}${rest}`
|
|
3736
|
-
};
|
|
3737
|
-
}
|
|
3738
|
-
/** Return the first continuation-safe chunk plus any remaining text. */
|
|
3739
|
-
function takeSlackContinuationPrefix(text, options) {
|
|
3740
|
-
const budget = {
|
|
3741
|
-
maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
|
|
3742
|
-
maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
|
|
3743
|
-
};
|
|
3744
|
-
const { prefix, rest } = (() => {
|
|
3745
|
-
if (options?.forceSplit) return takeSlackContinuationChunk(text, budget);
|
|
3746
|
-
const initial = takeSlackInlinePrefix(text, budget);
|
|
3747
|
-
return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
|
|
3748
|
-
})();
|
|
3749
|
-
return {
|
|
3750
|
-
prefix,
|
|
3751
|
-
renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
|
|
3752
|
-
rest
|
|
3753
|
-
};
|
|
3754
|
-
}
|
|
3755
|
-
/**
|
|
3756
|
-
* Take the largest Slack-safe inline prefix from `text` under the configured
|
|
3757
|
-
* character and line budgets. Returns the consumed prefix plus remaining text.
|
|
3758
|
-
*/
|
|
3759
|
-
function takeSlackInlinePrefix(text, options) {
|
|
3760
|
-
const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
|
|
3761
|
-
const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
|
|
3762
|
-
const normalized = text.replace(/\r\n?/g, "\n");
|
|
3763
|
-
if (!normalized) return {
|
|
3764
|
-
prefix: "",
|
|
3765
|
-
rest: ""
|
|
3766
|
-
};
|
|
3767
|
-
if (fitsInlineBudget(normalized, maxChars, maxLines)) return {
|
|
3768
|
-
prefix: normalized,
|
|
3769
|
-
rest: ""
|
|
3770
|
-
};
|
|
3771
|
-
const lineBounded = splitByLineBudget(normalized, maxLines);
|
|
3772
|
-
const cutIndex = findSplitIndex(lineBounded, maxChars);
|
|
3773
|
-
const prefix = lineBounded.slice(0, cutIndex).trimEnd();
|
|
3774
|
-
if (prefix) return {
|
|
3775
|
-
prefix,
|
|
3776
|
-
rest: normalized.slice(prefix.length).trimStart()
|
|
3777
|
-
};
|
|
3778
|
-
const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
|
|
3779
|
-
return {
|
|
3780
|
-
prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
|
|
3781
|
-
rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
|
|
3782
|
-
};
|
|
3783
|
-
}
|
|
3784
|
-
/**
|
|
3785
|
-
* Split a normalized Slack reply into multiple inline-safe thread messages.
|
|
3786
|
-
*
|
|
3787
|
-
* Replies split across more than two messages receive explicit continuation
|
|
3788
|
-
* markers on non-final chunks. When
|
|
3789
|
-
* `interrupted` is true, the final chunk receives an interruption marker.
|
|
3790
|
-
*/
|
|
3791
|
-
function splitSlackReplyText(text, options) {
|
|
3792
|
-
const normalized = normalizeSlackReplyMarkdown(text);
|
|
3793
|
-
if (!normalized) return [];
|
|
3794
|
-
const chunks = [];
|
|
3795
|
-
const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
3796
|
-
let remaining = normalized;
|
|
3797
|
-
while (remaining) {
|
|
3798
|
-
if (options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining)) {
|
|
3799
|
-
chunks.push(options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining);
|
|
3800
|
-
break;
|
|
3801
|
-
}
|
|
3802
|
-
const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
|
|
3803
|
-
...continuationBudget,
|
|
3804
|
-
forceSplit: true
|
|
3805
|
-
});
|
|
3806
|
-
chunks.push(renderedPrefix);
|
|
3807
|
-
remaining = rest;
|
|
3808
|
-
}
|
|
3809
|
-
if (chunks.length === 2) chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
|
|
3810
|
-
return chunks;
|
|
3811
|
-
}
|
|
3812
|
-
/**
|
|
3813
|
-
* Reserve enough inline budget for a continuation suffix on the current chunk.
|
|
3814
|
-
*/
|
|
3815
|
-
function getSlackContinuationBudget() {
|
|
3816
|
-
return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
|
|
3817
|
-
}
|
|
3818
|
-
/** Normalize text for Slack and wrap it as a PostableMessage. */
|
|
3819
|
-
function buildSlackOutputMessage(text) {
|
|
3820
|
-
const normalized = normalizeSlackReplyMarkdown(text);
|
|
3821
|
-
if (!normalized) throw new Error(`Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`);
|
|
3822
|
-
return { markdown: normalized };
|
|
3823
|
-
}
|
|
3824
|
-
// src/chat/slack/reply.ts
|
|
3825
|
-
/**
|
|
3826
|
-
* Send one destination-visible Slack reply.
|
|
3827
|
-
*
|
|
3828
|
-
* Chunks oversized text, builds compact attribution and conversation footer
|
|
3829
|
-
* context, and posts through the shared Slack outbound boundary.
|
|
3830
|
-
*/
|
|
3831
|
-
async function sendSlackReply(args) {
|
|
3832
|
-
const posts = splitSlackReplyText(args.text).map((text) => ({
|
|
3833
|
-
text,
|
|
3834
|
-
cards: []
|
|
3835
|
-
}));
|
|
3836
|
-
const cards = (args.cards ?? []).map((card) => renderSlackCard(card, args.conversationId));
|
|
3837
|
-
for (let index = 0; index < cards.length; index += 5) {
|
|
3838
|
-
const group = cards.slice(index, index + 5);
|
|
3839
|
-
const lastPost = posts.at(-1);
|
|
3840
|
-
if (index === 0 && lastPost) lastPost.cards = group;
|
|
3841
|
-
else posts.push({
|
|
3842
|
-
text: "",
|
|
3843
|
-
cards: group
|
|
3844
|
-
});
|
|
3845
|
-
}
|
|
3846
|
-
const footer = buildSlackReplyFooter({
|
|
3847
|
-
conversationId: args.conversationId,
|
|
3848
|
-
replyAttribution: args.replyAttribution
|
|
3849
|
-
});
|
|
3850
|
-
const messageTs = [];
|
|
3851
|
-
let threadTs = args.threadTs;
|
|
3852
|
-
for (const [index, post] of posts.entries()) {
|
|
3853
|
-
const { text, cards } = post;
|
|
3854
|
-
const isFinalChunk = index === posts.length - 1;
|
|
3855
|
-
const blocks = buildSlackReplyBlocks(text, void 0) ?? [];
|
|
3856
|
-
for (const card of cards) if (!card.entity) blocks.push({
|
|
3857
|
-
type: "section",
|
|
3858
|
-
text: {
|
|
3859
|
-
type: "mrkdwn",
|
|
3860
|
-
text: card.text
|
|
3861
|
-
}
|
|
3862
|
-
});
|
|
3863
|
-
if (isFinalChunk || !text.trim()) blocks.push(...buildSlackReplyBlocks("", footer) ?? []);
|
|
3864
|
-
const entities = cards.flatMap((card) => card.entity ? [card.entity] : []);
|
|
3865
|
-
const accessibleText = [text, ...cards.map((card) => card.text)].filter(Boolean).join("\n\n");
|
|
3866
|
-
const fallbackText = isFinalChunk && args.replyAttribution ? `${accessibleText}\n\n${escapeSlackMrkdwnText(formatReplyAttribution(args.replyAttribution))}` : accessibleText;
|
|
3867
|
-
const response = await postSlackMessage({
|
|
3868
|
-
channelId: args.channelId,
|
|
3869
|
-
threadTs,
|
|
3870
|
-
text: fallbackText,
|
|
3871
|
-
...blocks.length ? { blocks } : void 0,
|
|
3872
|
-
...entities.length ? { entities } : void 0
|
|
3873
|
-
});
|
|
3874
|
-
if (response.ts) {
|
|
3875
|
-
messageTs.push(response.ts);
|
|
3876
|
-
threadTs ??= response.ts;
|
|
3877
|
-
}
|
|
3878
|
-
}
|
|
3879
|
-
return messageTs;
|
|
3880
|
-
}
|
|
3881
3050
|
// src/chat/slack/assistant-thread/status-render.ts
|
|
3882
3051
|
const DEFAULT_STATUS_CONTEXTS = {
|
|
3883
3052
|
thinking: "…",
|
|
@@ -4235,6 +3404,21 @@ function formatSlackConversationRedactedLabel(context) {
|
|
|
4235
3404
|
if (!context) return void 0;
|
|
4236
3405
|
return formatSlackConversationTypeLabel(context.type);
|
|
4237
3406
|
}
|
|
3407
|
+
// src/chat/slack/avatar.ts
|
|
3408
|
+
const AVATAR_SAVE_INTERVAL_MS = 864e5;
|
|
3409
|
+
/**
|
|
3410
|
+
* Save the Slack photo of one Slack Actor on their Slack Identity. Slack
|
|
3411
|
+
* Turns call this, so the saved photo follows profile changes within a day.
|
|
3412
|
+
*/
|
|
3413
|
+
async function saveSlackActorAvatar(actor) {
|
|
3414
|
+
const state = getStateAdapter();
|
|
3415
|
+
const savedKey = `junior:slack:avatar-saved:${actor.teamId}:${actor.userId}`;
|
|
3416
|
+
if (await state.get(savedKey)) return;
|
|
3417
|
+
const profile = await lookupSlackUser(actor.teamId, actor.userId);
|
|
3418
|
+
if (!profile) return;
|
|
3419
|
+
await saveActorAvatar(getSqlExecutor(), actor, profile.avatarUrl ?? null);
|
|
3420
|
+
await state.set(savedKey, true, AVATAR_SAVE_INTERVAL_MS);
|
|
3421
|
+
}
|
|
4238
3422
|
// src/chat/services/provider-default-config.ts
|
|
4239
3423
|
const GITHUB_REPO_PART = String.raw`[A-Za-z0-9_.-]*[A-Za-z0-9_-]`;
|
|
4240
3424
|
const GITHUB_REPO_RE = new RegExp(String.raw`^\s*(?:set|use)\s+(?:the\s+)?default\s+(?:github\s+)?repo(?:sitory)?\s+(?:to|as)\s+(${GITHUB_REPO_PART}/${GITHUB_REPO_PART})(?:\s+for\s+this\s+channel)?[.!?]?\s*$`, "i");
|
|
@@ -4379,8 +3563,8 @@ async function saveSteeringMessages(args) {
|
|
|
4379
3563
|
* Persist post-delivery Redis scratch with a short retry after durable SQL
|
|
4380
3564
|
* completion has succeeded.
|
|
4381
3565
|
*/
|
|
4382
|
-
async function persistThreadRuntimeStateWithRetry(
|
|
4383
|
-
await persistWithRetry(() => persistThreadRuntimeState(
|
|
3566
|
+
async function persistThreadRuntimeStateWithRetry(conversationId, patch) {
|
|
3567
|
+
await persistWithRetry(() => persistThreadRuntimeState(conversationId, patch));
|
|
4384
3568
|
}
|
|
4385
3569
|
/**
|
|
4386
3570
|
* Resolve the Pi history for this Slack turn from the most precise durable
|
|
@@ -4432,6 +3616,10 @@ function createSlackTurn(deps) {
|
|
|
4432
3616
|
visibility: destinationVisibility
|
|
4433
3617
|
}) ?? "private"
|
|
4434
3618
|
});
|
|
3619
|
+
const replyTarget = resolveSlackReplyTarget({
|
|
3620
|
+
destination,
|
|
3621
|
+
source
|
|
3622
|
+
});
|
|
4435
3623
|
const slackActionToken = readSlackActionToken(message);
|
|
4436
3624
|
const runId = options.execution?.dispatch?.id ?? getRunId(thread, message);
|
|
4437
3625
|
const conversationId = options.conversationId ?? threadId ?? runId;
|
|
@@ -4471,8 +3659,10 @@ function createSlackTurn(deps) {
|
|
|
4471
3659
|
executionActor = options.execution.dispatch.actor;
|
|
4472
3660
|
credentialContext = options.execution.credentialContext;
|
|
4473
3661
|
} else {
|
|
4474
|
-
|
|
4475
|
-
|
|
3662
|
+
const slackActor = await ensureSlackMessageActorIdentity(message, teamId, lookupSlackUser);
|
|
3663
|
+
await saveSlackActorAvatar(slackActor);
|
|
3664
|
+
executionActor = slackActor;
|
|
3665
|
+
credentialContext = credentialContextForActor(slackActor);
|
|
4476
3666
|
}
|
|
4477
3667
|
if (!executionActor || !credentialContext) throw new Error("Slack reply execution requires an actor");
|
|
4478
3668
|
const actor = "userId" in executionActor ? executionActor : void 0;
|
|
@@ -4486,6 +3676,7 @@ function createSlackTurn(deps) {
|
|
|
4486
3676
|
destination,
|
|
4487
3677
|
locationConfiguration: options.execution?.locationConfiguration,
|
|
4488
3678
|
context: {
|
|
3679
|
+
conversationId,
|
|
4489
3680
|
threadId,
|
|
4490
3681
|
actorId: slackActorId,
|
|
4491
3682
|
channelId,
|
|
@@ -4503,17 +3694,16 @@ function createSlackTurn(deps) {
|
|
|
4503
3694
|
};
|
|
4504
3695
|
const postAuthPauseNotice = async (providerDisplayName, requestText) => {
|
|
4505
3696
|
if (!actor) throw new Error("Slack auth pause notice requires an actor");
|
|
3697
|
+
if (!postsRunNotices(options.execution?.dispatch)) return;
|
|
4506
3698
|
const text = buildAuthPauseResponse(actor.userId, providerDisplayName, requestText);
|
|
4507
3699
|
try {
|
|
4508
3700
|
await beforeFirstResponsePost();
|
|
4509
|
-
|
|
4510
|
-
|
|
3701
|
+
await sendSlackReply({
|
|
3702
|
+
...replyTarget,
|
|
4511
3703
|
conversationId,
|
|
4512
3704
|
replyAttribution: options.execution?.dispatch?.replyAttribution,
|
|
4513
|
-
text
|
|
4514
|
-
threadTs
|
|
3705
|
+
text
|
|
4515
3706
|
});
|
|
4516
|
-
else await thread.post(buildSlackOutputMessage(text));
|
|
4517
3707
|
} catch (error) {
|
|
4518
3708
|
logException(error, "slack.auth_pause.notice_post.failed", {
|
|
4519
3709
|
"app.slack.reply_stage": "thread_reply_auth_pause_notice",
|
|
@@ -4567,7 +3757,7 @@ function createSlackTurn(deps) {
|
|
|
4567
3757
|
const deliveredMessage = [...preparedState.conversation.messages].reverse().find((candidate) => candidate.role === "assistant" && candidate.id.startsWith(`${turnId}:assistant:`) && candidate.meta?.replied === true);
|
|
4568
3758
|
options.onTurnDeliveryAccepted?.(deliveredMessage?.meta?.slackTs);
|
|
4569
3759
|
options.onTurnOutcome?.({ outcome: "completed" });
|
|
4570
|
-
await
|
|
3760
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4571
3761
|
await options.onTurnStatePersisted?.();
|
|
4572
3762
|
await options.ack?.();
|
|
4573
3763
|
await options.onTurnCompleted?.();
|
|
@@ -4590,7 +3780,7 @@ function createSlackTurn(deps) {
|
|
|
4590
3780
|
});
|
|
4591
3781
|
throw error;
|
|
4592
3782
|
}
|
|
4593
|
-
await
|
|
3783
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4594
3784
|
await options.onTurnStatePersisted?.();
|
|
4595
3785
|
await options.ack?.();
|
|
4596
3786
|
return;
|
|
@@ -4628,7 +3818,11 @@ function createSlackTurn(deps) {
|
|
|
4628
3818
|
});
|
|
4629
3819
|
if (configReply) {
|
|
4630
3820
|
await beforeFirstResponsePost();
|
|
4631
|
-
await
|
|
3821
|
+
await sendSlackReply({
|
|
3822
|
+
...replyTarget,
|
|
3823
|
+
conversationId,
|
|
3824
|
+
text: configReply.text
|
|
3825
|
+
});
|
|
4632
3826
|
markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
|
|
4633
3827
|
replied: true,
|
|
4634
3828
|
skippedReason: void 0
|
|
@@ -4647,7 +3841,7 @@ function createSlackTurn(deps) {
|
|
|
4647
3841
|
source: "slack"
|
|
4648
3842
|
}
|
|
4649
3843
|
});
|
|
4650
|
-
await
|
|
3844
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4651
3845
|
await options.onTurnStatePersisted?.();
|
|
4652
3846
|
await options.ack?.();
|
|
4653
3847
|
return;
|
|
@@ -4688,7 +3882,7 @@ function createSlackTurn(deps) {
|
|
|
4688
3882
|
"app.message.id": message.id,
|
|
4689
3883
|
...messageTs ? { "messaging.message.id": messageTs } : void 0
|
|
4690
3884
|
});
|
|
4691
|
-
await
|
|
3885
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4692
3886
|
await options.onTurnStatePersisted?.();
|
|
4693
3887
|
if (actor) setSentryUser({
|
|
4694
3888
|
id: actor.userId,
|
|
@@ -4775,16 +3969,15 @@ function createSlackTurn(deps) {
|
|
|
4775
3969
|
}
|
|
4776
3970
|
else {
|
|
4777
3971
|
slackMessageTs = await sendSlackReply({
|
|
3972
|
+
...replyTarget,
|
|
4778
3973
|
cards,
|
|
4779
|
-
channelId: destination.channelId,
|
|
4780
3974
|
conversationId,
|
|
4781
3975
|
replyAttribution: options.execution?.dispatch?.replyAttribution,
|
|
4782
|
-
text
|
|
4783
|
-
...threadTs ? { threadTs } : void 0
|
|
3976
|
+
text
|
|
4784
3977
|
});
|
|
4785
3978
|
messageDestinations.push(...slackMessageTs.map((messageId) => ({
|
|
4786
3979
|
destination,
|
|
4787
|
-
messageId: threadTs ?? messageId
|
|
3980
|
+
messageId: replyTarget.threadTs ?? messageId
|
|
4788
3981
|
})));
|
|
4789
3982
|
}
|
|
4790
3983
|
} catch (error) {
|
|
@@ -4874,7 +4067,7 @@ function createSlackTurn(deps) {
|
|
|
4874
4067
|
});
|
|
4875
4068
|
if (compaction.compacted) {
|
|
4876
4069
|
piMessages = compaction.piMessages;
|
|
4877
|
-
await
|
|
4070
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4878
4071
|
}
|
|
4879
4072
|
}
|
|
4880
4073
|
const hasDurablePromptHistory = Boolean(piMessages?.length);
|
|
@@ -4983,11 +4176,11 @@ function createSlackTurn(deps) {
|
|
|
4983
4176
|
drainSteeringMessages,
|
|
4984
4177
|
shouldYield: options.shouldYield,
|
|
4985
4178
|
onSandboxRefChanged: async (sandboxRef) => {
|
|
4986
|
-
await
|
|
4179
|
+
await persistThreadStateById(conversationId, { sandboxRef });
|
|
4987
4180
|
},
|
|
4988
4181
|
recordPendingAuth: async (pendingAuth) => {
|
|
4989
4182
|
preparedState.conversation.processing.pendingAuth = pendingAuth;
|
|
4990
|
-
await
|
|
4183
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4991
4184
|
}
|
|
4992
4185
|
}
|
|
4993
4186
|
};
|
|
@@ -5066,7 +4259,7 @@ function createSlackTurn(deps) {
|
|
|
5066
4259
|
conversation: completedState.conversation,
|
|
5067
4260
|
conversationId
|
|
5068
4261
|
}));
|
|
5069
|
-
await persistThreadRuntimeStateWithRetry(
|
|
4262
|
+
await persistThreadRuntimeStateWithRetry(conversationId, completedState);
|
|
5070
4263
|
const automatedTurnLimit = await recordFinishedTurnForAutomatedLimit({
|
|
5071
4264
|
conversationId,
|
|
5072
4265
|
maxTurns: botConfig.maxConsecutiveAutomatedTurns,
|
|
@@ -5075,7 +4268,7 @@ function createSlackTurn(deps) {
|
|
|
5075
4268
|
await maybePostAutomatedTurnLimitNotice({
|
|
5076
4269
|
conversationId,
|
|
5077
4270
|
maxTurns: botConfig.maxConsecutiveAutomatedTurns,
|
|
5078
|
-
|
|
4271
|
+
source,
|
|
5079
4272
|
update: automatedTurnLimit
|
|
5080
4273
|
});
|
|
5081
4274
|
} catch (saveError) {
|
|
@@ -5111,7 +4304,7 @@ function createSlackTurn(deps) {
|
|
|
5111
4304
|
nowMs: Date.now(),
|
|
5112
4305
|
sessionId: turnId
|
|
5113
4306
|
});
|
|
5114
|
-
await
|
|
4307
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5115
4308
|
if (conversationId) await turnLifecycle.fail({
|
|
5116
4309
|
conversationId,
|
|
5117
4310
|
createdAtMs: Date.now(),
|
|
@@ -5128,7 +4321,7 @@ function createSlackTurn(deps) {
|
|
|
5128
4321
|
conversation: preparedState.conversation,
|
|
5129
4322
|
sessionId: turnId
|
|
5130
4323
|
});
|
|
5131
|
-
await
|
|
4324
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5132
4325
|
persistedAtLeastOnce = true;
|
|
5133
4326
|
shouldPersistFailureState = false;
|
|
5134
4327
|
return;
|
|
@@ -5212,7 +4405,7 @@ function createSlackTurn(deps) {
|
|
|
5212
4405
|
nowMs: Date.now(),
|
|
5213
4406
|
sessionId: turnId
|
|
5214
4407
|
});
|
|
5215
|
-
await
|
|
4408
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5216
4409
|
await turnLifecycle.complete({
|
|
5217
4410
|
conversationId,
|
|
5218
4411
|
createdAtMs: Date.now(),
|
|
@@ -5269,7 +4462,7 @@ function createSlackTurn(deps) {
|
|
|
5269
4462
|
} catch (recordError) {
|
|
5270
4463
|
logException(recordError, "agent.turn.failed_session_record_persist.failed");
|
|
5271
4464
|
}
|
|
5272
|
-
await
|
|
4465
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5273
4466
|
if (shouldEmitDevAgentTrace()) logWarn("agent.turn.failed");
|
|
5274
4467
|
}
|
|
5275
4468
|
await status.clear();
|
|
@@ -5408,10 +4601,10 @@ async function seedConversationBackfill(thread, conversation, currentTurn, resol
|
|
|
5408
4601
|
/** Build the turn-state preparer from injected conversation services. */
|
|
5409
4602
|
function createPrepareTurnState(deps) {
|
|
5410
4603
|
return async function prepareTurnState(args) {
|
|
5411
|
-
const
|
|
4604
|
+
const conversationId = args.context.conversationId ?? args.context.threadId ?? args.context.runId;
|
|
4605
|
+
const existingState = conversationId ? await getPersistedThreadState(conversationId) : await args.thread.state;
|
|
5412
4606
|
const sandboxRef = getPersistedSandboxState(existingState ?? {});
|
|
5413
4607
|
const conversation = coerceThreadConversationState(existingState);
|
|
5414
|
-
const conversationId = args.context.threadId ?? args.context.runId;
|
|
5415
4608
|
if (conversationId && isVisionEnabled()) conversation.vision = await loadConversationVisionCache(conversationId);
|
|
5416
4609
|
await hydrateConversationMessages({
|
|
5417
4610
|
conversation,
|
|
@@ -5596,8 +4789,8 @@ function createSlackRuntime(options) {
|
|
|
5596
4789
|
modelId: defaultModelId(botConfig),
|
|
5597
4790
|
now: options.now ?? (() => Date.now()),
|
|
5598
4791
|
prepareTurnState,
|
|
5599
|
-
persistPreparedState: async ({
|
|
5600
|
-
await
|
|
4792
|
+
persistPreparedState: async ({ conversationId, preparedState }) => {
|
|
4793
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5601
4794
|
},
|
|
5602
4795
|
getPreparedConversationContext: (preparedState) => preparedState.conversationContext,
|
|
5603
4796
|
decideSubscribedReply: async (args) => {
|
|
@@ -5612,18 +4805,18 @@ function createSlackRuntime(options) {
|
|
|
5612
4805
|
});
|
|
5613
4806
|
return decision;
|
|
5614
4807
|
},
|
|
5615
|
-
recordSkippedSteeringMessage: async ({
|
|
5616
|
-
const conversation = coerceThreadConversationState(await
|
|
4808
|
+
recordSkippedSteeringMessage: async ({ conversationId, message, decision, text }) => {
|
|
4809
|
+
const conversation = coerceThreadConversationState(await getPersistedThreadState(conversationId));
|
|
5617
4810
|
recordSkippedConversationMessage({
|
|
5618
4811
|
conversation,
|
|
5619
4812
|
message,
|
|
5620
4813
|
skippedReason: decision.reason,
|
|
5621
4814
|
text: text.userText
|
|
5622
4815
|
});
|
|
5623
|
-
await
|
|
4816
|
+
await persistThreadStateById(conversationId, { conversation });
|
|
5624
4817
|
},
|
|
5625
|
-
recordSkippedSubscribedTurn: async ({
|
|
5626
|
-
const conversation = coerceThreadConversationState(await
|
|
4818
|
+
recordSkippedSubscribedTurn: async ({ conversationId, message, decision, completedAtMs, text }) => {
|
|
4819
|
+
const conversation = coerceThreadConversationState(await getPersistedThreadState(conversationId));
|
|
5627
4820
|
recordSkippedConversationMessage({
|
|
5628
4821
|
conversation,
|
|
5629
4822
|
message,
|
|
@@ -5632,16 +4825,16 @@ function createSlackRuntime(options) {
|
|
|
5632
4825
|
});
|
|
5633
4826
|
clearSkippedTurnIfActive(conversation, message.id);
|
|
5634
4827
|
conversation.processing.lastCompletedAtMs = completedAtMs;
|
|
5635
|
-
await
|
|
4828
|
+
await persistThreadStateById(conversationId, { conversation });
|
|
5636
4829
|
},
|
|
5637
|
-
onSubscribedMessageSkipped: async ({
|
|
4830
|
+
onSubscribedMessageSkipped: async ({ conversationId, message, preparedState, decision, completedAtMs }) => {
|
|
5638
4831
|
markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
|
|
5639
4832
|
replied: false,
|
|
5640
4833
|
skippedReason: decision.reason
|
|
5641
4834
|
});
|
|
5642
4835
|
clearSkippedTurnIfActive(preparedState.conversation, message.id);
|
|
5643
4836
|
preparedState.conversation.processing.lastCompletedAtMs = completedAtMs;
|
|
5644
|
-
await
|
|
4837
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5645
4838
|
},
|
|
5646
4839
|
executeSlackTurn,
|
|
5647
4840
|
initializeAssistantThread: async ({ channelId, threadTs, sourceChannelId }) => {
|
|
@@ -5688,23 +4881,20 @@ function createJuniorSlackAdapter(config) {
|
|
|
5688
4881
|
* authorization pause notices use `sendSlackReply` so the footer stays
|
|
5689
4882
|
* consistent.
|
|
5690
4883
|
*/
|
|
5691
|
-
|
|
4884
|
+
/** Post a failure reply or status notice when the run's dispatch allows it. */
|
|
4885
|
+
async function postRunNotice(resume, text) {
|
|
4886
|
+
if (!postsRunNotices(resume.run?.dispatch)) return;
|
|
4887
|
+
await sendSlackReply({
|
|
4888
|
+
channelId: resume.channelId,
|
|
4889
|
+
conversationId: resume.conversationId,
|
|
4890
|
+
replyAttribution: resume.run?.dispatch?.replyAttribution,
|
|
4891
|
+
text,
|
|
4892
|
+
threadTs: resume.threadTs
|
|
4893
|
+
});
|
|
4894
|
+
}
|
|
4895
|
+
async function postRunNoticeBestEffort(resume, text) {
|
|
5692
4896
|
try {
|
|
5693
|
-
|
|
5694
|
-
await sendSlackReply({
|
|
5695
|
-
channelId,
|
|
5696
|
-
conversationId,
|
|
5697
|
-
replyAttribution,
|
|
5698
|
-
text,
|
|
5699
|
-
threadTs
|
|
5700
|
-
});
|
|
5701
|
-
return;
|
|
5702
|
-
}
|
|
5703
|
-
await postSlackMessage({
|
|
5704
|
-
channelId,
|
|
5705
|
-
threadTs,
|
|
5706
|
-
text
|
|
5707
|
-
});
|
|
4897
|
+
await postRunNotice(resume, text);
|
|
5708
4898
|
} catch {}
|
|
5709
4899
|
}
|
|
5710
4900
|
/** Create a read-only configuration service from persisted values. */
|
|
@@ -5745,13 +4935,6 @@ function getResumeLogContext(args) {
|
|
|
5745
4935
|
modelId: defaultModelId(botConfig)
|
|
5746
4936
|
};
|
|
5747
4937
|
}
|
|
5748
|
-
async function postResumeFailureReply(args) {
|
|
5749
|
-
await postSlackMessage({
|
|
5750
|
-
channelId: args.channelId,
|
|
5751
|
-
threadTs: args.threadTs,
|
|
5752
|
-
text: buildTurnErrorResponse(args.error, args.eventId)
|
|
5753
|
-
});
|
|
5754
|
-
}
|
|
5755
4938
|
async function handleResumeFailure(args) {
|
|
5756
4939
|
const turnLifecycle = getTurnLifecycle();
|
|
5757
4940
|
const capturedEventId = logException(args.error, args.eventName);
|
|
@@ -5775,12 +4958,7 @@ async function handleResumeFailure(args) {
|
|
|
5775
4958
|
failureStatePersistError = persistError;
|
|
5776
4959
|
}
|
|
5777
4960
|
try {
|
|
5778
|
-
await
|
|
5779
|
-
channelId: args.resume.channelId,
|
|
5780
|
-
threadTs: args.resume.threadTs,
|
|
5781
|
-
eventId,
|
|
5782
|
-
error: args.error
|
|
5783
|
-
});
|
|
4961
|
+
await postRunNotice(args.resume, buildTurnErrorResponse(args.error, eventId));
|
|
5784
4962
|
} catch (deliveryError) {
|
|
5785
4963
|
const deliveryEventId = logException(deliveryError, "slack.resume.failure_delivery.failed", { "app.error.original_event_id": eventId });
|
|
5786
4964
|
try {
|
|
@@ -5899,7 +5077,7 @@ async function resumeSlackTurnInContext(args) {
|
|
|
5899
5077
|
channelId: runArgs.channelId,
|
|
5900
5078
|
timestamp: runArgs.messageTs
|
|
5901
5079
|
});
|
|
5902
|
-
if (runArgs.initialText) await
|
|
5080
|
+
if (runArgs.initialText) await postRunNoticeBestEffort(runArgs, runArgs.initialText);
|
|
5903
5081
|
status.update(runArgs.initialStatus);
|
|
5904
5082
|
const conversationId = runArgs.conversationId;
|
|
5905
5083
|
const visibleConversationId = args.conversationId;
|
|
@@ -6154,7 +5332,7 @@ async function resumeSlackTurnInContext(args) {
|
|
|
6154
5332
|
}
|
|
6155
5333
|
if (deferredPauseHandler) try {
|
|
6156
5334
|
await deferredPauseHandler();
|
|
6157
|
-
if (deferredAuthInfo) await
|
|
5335
|
+
if (deferredAuthInfo) await postRunNoticeBestEffort(runArgs, buildAuthPauseResponse(deferredAuthInfo.actorId, deferredAuthInfo.providerDisplayName, deferredAuthInfo.requestText));
|
|
6158
5336
|
return true;
|
|
6159
5337
|
} catch (pauseError) {
|
|
6160
5338
|
await handleResumeFailure({
|
|
@@ -6347,16 +5525,22 @@ async function runPausedTurn(payload, options, runOptions = {}) {
|
|
|
6347
5525
|
return withLogContext({ conversationId: payload.conversationId }, () => runPausedTurnInContext(payload, options, runOptions));
|
|
6348
5526
|
}
|
|
6349
5527
|
async function runPausedTurnInContext(payload, options, runOptions) {
|
|
6350
|
-
const thread = parseSlackThreadId(payload.conversationId);
|
|
6351
5528
|
const destination = requireSlackDestination(payload.destination, "Paused turn");
|
|
6352
5529
|
const wakePausedTurn$1 = options.wakePausedTurn ?? wakePausedTurn;
|
|
5530
|
+
const routing = await resolveConversationRouting({ conversationId: payload.conversationId });
|
|
5531
|
+
const pausedTurn = await getTurnRecord(payload.conversationId, payload.turnId);
|
|
5532
|
+
const target = resolveSlackReplyTarget({
|
|
5533
|
+
destination,
|
|
5534
|
+
location: routing?.location,
|
|
5535
|
+
source: pausedTurn?.source
|
|
5536
|
+
});
|
|
6353
5537
|
return await resumeSlackTurn({
|
|
6354
5538
|
messageText: "",
|
|
6355
5539
|
conversationId: payload.conversationId,
|
|
6356
5540
|
turnId: payload.turnId,
|
|
6357
5541
|
traceContext: payload.traceContext,
|
|
6358
|
-
channelId:
|
|
6359
|
-
...
|
|
5542
|
+
channelId: target.channelId,
|
|
5543
|
+
...target.threadTs ? { threadTs: target.threadTs } : void 0,
|
|
6360
5544
|
executeTurn: async (run, saveResult) => await executeTurn(options.agentRunner, run, saveResult),
|
|
6361
5545
|
scheduleSessionCompletedPluginTasks: options.scheduleSessionCompletedPluginTasks,
|
|
6362
5546
|
beforeStart: async () => {
|
|
@@ -6569,6 +5753,23 @@ async function failStrandedTurnWithFallback(args) {
|
|
|
6569
5753
|
failureCode: "agent_run_failed",
|
|
6570
5754
|
turnId: failed.turnId
|
|
6571
5755
|
});
|
|
5756
|
+
let replyAttribution;
|
|
5757
|
+
if (failed.dispatchId) {
|
|
5758
|
+
const dispatch = await getDispatchRecord(failed.dispatchId);
|
|
5759
|
+
await recordTurnSummary({
|
|
5760
|
+
conversationId: args.conversationId,
|
|
5761
|
+
destination: dispatch?.destination,
|
|
5762
|
+
dispatchId: failed.dispatchId,
|
|
5763
|
+
dispatchOutcome: "failed",
|
|
5764
|
+
turnId: failed.turnId,
|
|
5765
|
+
sliceId: failed.sliceId,
|
|
5766
|
+
source: dispatch?.source,
|
|
5767
|
+
state: "failed",
|
|
5768
|
+
surface: failed.surface
|
|
5769
|
+
});
|
|
5770
|
+
if (!dispatch || !postsRunNotices(buildDispatchRoutingContext(dispatch).dispatch)) return true;
|
|
5771
|
+
replyAttribution = dispatch.replyAttribution;
|
|
5772
|
+
}
|
|
6572
5773
|
let routing;
|
|
6573
5774
|
try {
|
|
6574
5775
|
routing = await resolveTurnSessionRouting({ conversationId: args.conversationId });
|
|
@@ -6579,22 +5780,15 @@ async function failStrandedTurnWithFallback(args) {
|
|
|
6579
5780
|
});
|
|
6580
5781
|
return true;
|
|
6581
5782
|
}
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
dispatchOutcome: "failed",
|
|
6587
|
-
turnId: failed.turnId,
|
|
6588
|
-
sliceId: failed.sliceId,
|
|
6589
|
-
source: routing.source,
|
|
6590
|
-
state: "failed",
|
|
6591
|
-
surface: failed.surface
|
|
5783
|
+
const target = resolveSlackReplyTarget({
|
|
5784
|
+
destination: requireSlackDestination(routing.destination, "Stranded paused turn"),
|
|
5785
|
+
location: routing.location,
|
|
5786
|
+
source: failed.source
|
|
6592
5787
|
});
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
...thread?.threadTs ? { threadTs: thread.threadTs } : void 0,
|
|
5788
|
+
await sendSlackReply({
|
|
5789
|
+
...target,
|
|
5790
|
+
conversationId: args.conversationId,
|
|
5791
|
+
replyAttribution,
|
|
6598
5792
|
text: buildTurnFailureResponse(requireTurnFailureEventId(eventId, eventName))
|
|
6599
5793
|
});
|
|
6600
5794
|
return true;
|
|
@@ -6775,6 +5969,7 @@ function createWebSteeringDrain(args) {
|
|
|
6775
5969
|
text: normalizeConversationText(message.input.text),
|
|
6776
5970
|
createdAtMs: message.createdAtMs,
|
|
6777
5971
|
author,
|
|
5972
|
+
...message.input.authorIdentityId ? { authorIdentityId: message.input.authorIdentityId } : void 0,
|
|
6778
5973
|
meta: {
|
|
6779
5974
|
...attachments.length ? { attachments } : void 0,
|
|
6780
5975
|
explicitMention: true,
|
|
@@ -6797,28 +5992,9 @@ function createWebSteeringDrain(args) {
|
|
|
6797
5992
|
return accepted;
|
|
6798
5993
|
};
|
|
6799
5994
|
}
|
|
6800
|
-
// src/chat/conversations/destination.ts
|
|
6801
|
-
function requireLocalDestination(conversationId) {
|
|
6802
|
-
const parsed = localDestinationSchema.safeParse({
|
|
6803
|
-
platform: "local",
|
|
6804
|
-
conversationId
|
|
6805
|
-
});
|
|
6806
|
-
if (!parsed.success) throw new Error(`Invalid local conversation id: ${conversationId}`);
|
|
6807
|
-
return parsed.data;
|
|
6808
|
-
}
|
|
6809
|
-
/**
|
|
6810
|
-
* Keep a stored Destination, or create the temporary local Destination.
|
|
6811
|
-
*
|
|
6812
|
-
* TODO(dcramer): Delete this module after Conversation and AgentRun use
|
|
6813
|
-
* Location, and web and event Turns no longer need a local
|
|
6814
|
-
* Destination.
|
|
6815
|
-
*/
|
|
6816
|
-
function resolveConversationDestination(args) {
|
|
6817
|
-
return args.existing ?? requireLocalDestination(args.conversationId);
|
|
6818
|
-
}
|
|
6819
5995
|
// src/chat/task-execution/conversation-turn.ts
|
|
6820
5996
|
/** Run a mailbox Turn and store its assistant Messages. */
|
|
6821
|
-
function stableHex
|
|
5997
|
+
function stableHex(...parts) {
|
|
6822
5998
|
return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24);
|
|
6823
5999
|
}
|
|
6824
6000
|
function captureConversationTurnFailure(args) {
|
|
@@ -6876,6 +6052,14 @@ async function completeCancelledConversationTurn(args) {
|
|
|
6876
6052
|
await args.acknowledge();
|
|
6877
6053
|
}
|
|
6878
6054
|
/**
|
|
6055
|
+
* Return the author Identity id that every batched input shares.
|
|
6056
|
+
* A batch with more than one author has no single author yet.
|
|
6057
|
+
*/
|
|
6058
|
+
function sharedAuthorIdentityId(messages) {
|
|
6059
|
+
const ids = new Set(messages.map((message) => message.input.authorIdentityId));
|
|
6060
|
+
return ids.size === 1 ? [...ids][0] : void 0;
|
|
6061
|
+
}
|
|
6062
|
+
/**
|
|
6879
6063
|
* Create the shared mailbox worker for web and event Turns.
|
|
6880
6064
|
*/
|
|
6881
6065
|
function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
@@ -6898,6 +6082,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
|
6898
6082
|
let inputMessageIds = [];
|
|
6899
6083
|
let event;
|
|
6900
6084
|
let batchEvents = [];
|
|
6085
|
+
let authorIdentityId;
|
|
6901
6086
|
const storedConversation = await getConversationStore().get({ conversationId: context.conversationId });
|
|
6902
6087
|
const mailboxDestination = resolved.kind === "mailbox" ? resolved.batch[0]?.message.destination : void 0;
|
|
6903
6088
|
const destination = resolveConversationDestination({
|
|
@@ -6918,6 +6103,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
|
6918
6103
|
userMessageId = first.message.inboundMessageId;
|
|
6919
6104
|
turnId = buildDeterministicTurnId(userMessageId);
|
|
6920
6105
|
inputMessageIds = resolved.batch.map((entry) => entry.message.inboundMessageId);
|
|
6106
|
+
authorIdentityId = sharedAuthorIdentityId(messages);
|
|
6921
6107
|
turnInputFacts = first;
|
|
6922
6108
|
} else turnId = resolved.turnId;
|
|
6923
6109
|
const persisted = await getPersistedThreadState(context.conversationId);
|
|
@@ -7004,6 +6190,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
|
7004
6190
|
text: normalizeConversationText(text),
|
|
7005
6191
|
createdAtMs: startedAtMs,
|
|
7006
6192
|
author,
|
|
6193
|
+
...authorIdentityId ? { authorIdentityId } : void 0,
|
|
7007
6194
|
meta: {
|
|
7008
6195
|
...attachments.length ? { attachments } : void 0,
|
|
7009
6196
|
explicitMention: true,
|
|
@@ -7097,7 +6284,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
|
7097
6284
|
if (stopSignal?.aborted) return await completeCancelledTurn();
|
|
7098
6285
|
const piMessages = await loadProjection({ conversationId: context.conversationId });
|
|
7099
6286
|
failureCode = "agent_run_failed";
|
|
7100
|
-
currentRunId = `conversation-run:${stableHex
|
|
6287
|
+
currentRunId = `conversation-run:${stableHex(turnId, String(startedAtMs))}`;
|
|
7101
6288
|
setTags({ runId: currentRunId });
|
|
7102
6289
|
const outcome = await executeTurn(agentRunner, {
|
|
7103
6290
|
conversationId: context.conversationId,
|
|
@@ -7195,7 +6382,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
|
7195
6382
|
await maybePostAutomatedTurnLimitNotice({
|
|
7196
6383
|
conversationId: context.conversationId,
|
|
7197
6384
|
maxTurns: botConfig.maxConsecutiveAutomatedTurns,
|
|
7198
|
-
|
|
6385
|
+
source,
|
|
7199
6386
|
update: automatedTurnLimit
|
|
7200
6387
|
});
|
|
7201
6388
|
completedSuccessfully = reply.diagnostics.outcome === "success";
|
|
@@ -7307,11 +6494,13 @@ function createSlackSystemTurnDelivery(args) {
|
|
|
7307
6494
|
installation: { teamId: location.teamId },
|
|
7308
6495
|
state: args.state,
|
|
7309
6496
|
task: async () => await sendSlackReply({
|
|
6497
|
+
...resolveSlackReplyTarget({
|
|
6498
|
+
destination: location,
|
|
6499
|
+
location
|
|
6500
|
+
}),
|
|
7310
6501
|
cards,
|
|
7311
|
-
channelId: location.channelId,
|
|
7312
6502
|
conversationId,
|
|
7313
|
-
text
|
|
7314
|
-
...location.threadTs ? { threadTs: location.threadTs } : void 0
|
|
6503
|
+
text
|
|
7315
6504
|
})
|
|
7316
6505
|
});
|
|
7317
6506
|
} catch (error) {
|
|
@@ -7376,12 +6565,14 @@ function createConversationWork(options) {
|
|
|
7376
6565
|
},
|
|
7377
6566
|
runTurn: runtime.runDispatchTurn
|
|
7378
6567
|
});
|
|
7379
|
-
const
|
|
6568
|
+
const childWorker = createChildConversationWorker(options.agentRunner, {
|
|
7380
6569
|
enqueueResult: async (invocation) => await enqueueAgentInvocationResult(invocation, {
|
|
7381
6570
|
queue: options.queue,
|
|
7382
6571
|
state: options.state
|
|
7383
6572
|
}),
|
|
7384
|
-
maxAgentDepth: botConfig.maxAgentDepth
|
|
6573
|
+
maxAgentDepth: botConfig.maxAgentDepth,
|
|
6574
|
+
queue: options.queue,
|
|
6575
|
+
state: options.state
|
|
7385
6576
|
});
|
|
7386
6577
|
const conversationTurnWorker = createConversationTurnWorker(options.agentRunner, createSlackSystemTurnDelivery({
|
|
7387
6578
|
getSlackAdapter: options.getSlackAdapter,
|
|
@@ -7396,15 +6587,9 @@ function createConversationWork(options) {
|
|
|
7396
6587
|
const run = async (context) => {
|
|
7397
6588
|
const skipped = await skipInputBeforeConversationStop(context);
|
|
7398
6589
|
if (skipped) return skipped;
|
|
7399
|
-
|
|
7400
|
-
queue: options.queue,
|
|
7401
|
-
state: options.state
|
|
7402
|
-
});
|
|
7403
|
-
if (childInput) return childInput;
|
|
6590
|
+
if ((await options.conversationStore.get({ conversationId: context.conversationId }))?.parentConversationId) return await childWorker(context);
|
|
7404
6591
|
const mailboxTurn = await resolveMailboxTurnWork(context);
|
|
7405
6592
|
if (mailboxTurn) return await conversationTurnWorker(context, await withAgentInvocationResultActors(mailboxTurn));
|
|
7406
|
-
const invocationId = await resolveAgentInvocationId(context);
|
|
7407
|
-
if (invocationId) return await invocationWorker(context, invocationId);
|
|
7408
6593
|
const dispatchId = await resolveAgentDispatchId(context);
|
|
7409
6594
|
if (dispatchId) return await dispatchWorker(context, dispatchId);
|
|
7410
6595
|
return await destinationWorker(context);
|
|
@@ -7471,167 +6656,6 @@ function createProductionConversationWorkOptions(options) {
|
|
|
7471
6656
|
waitUntil: options.waitUntil
|
|
7472
6657
|
});
|
|
7473
6658
|
}
|
|
7474
|
-
// src/chat/conversations/web-input.ts
|
|
7475
|
-
function normalizeEmail(email) {
|
|
7476
|
-
return email.trim().toLowerCase();
|
|
7477
|
-
}
|
|
7478
|
-
function stableHex(...parts) {
|
|
7479
|
-
return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24);
|
|
7480
|
-
}
|
|
7481
|
-
/**
|
|
7482
|
-
* Build a durable Conversation id for one web Actor and create key.
|
|
7483
|
-
*
|
|
7484
|
-
* Retries with the same key must address the same Conversation before the
|
|
7485
|
-
* mailbox Message id is derived.
|
|
7486
|
-
*/
|
|
7487
|
-
function createConversationId(args) {
|
|
7488
|
-
return `local:web:${stableHex(normalizeEmail(args.actorEmail), args.idempotencyKey)}`;
|
|
7489
|
-
}
|
|
7490
|
-
/** Return the stable Turn id for one mailbox Message. */
|
|
7491
|
-
function conversationTurnIdForMessage(messageId) {
|
|
7492
|
-
return buildDeterministicTurnId(messageId);
|
|
7493
|
-
}
|
|
7494
|
-
/**
|
|
7495
|
-
* Store the dashboard participant in the current Conversation actor field.
|
|
7496
|
-
*
|
|
7497
|
-
* TODO(dcramer): Remove this StoredSlackActor conversion after Conversation
|
|
7498
|
-
* actor storage accepts web Actors directly.
|
|
7499
|
-
*/
|
|
7500
|
-
function storedActorFromWeb(actor) {
|
|
7501
|
-
return {
|
|
7502
|
-
...actor.email ? { email: normalizeEmail(actor.email) } : void 0,
|
|
7503
|
-
...actor.fullName ? { fullName: actor.fullName } : void 0
|
|
7504
|
-
};
|
|
7505
|
-
}
|
|
7506
|
-
/** Rebuild the web Actor from durable Conversation identity. */
|
|
7507
|
-
function webActorFromEmail(email, profile) {
|
|
7508
|
-
const normalized = normalizeEmail(email);
|
|
7509
|
-
return {
|
|
7510
|
-
platform: "web",
|
|
7511
|
-
userId: `dashboard:${stableHex(normalized)}`,
|
|
7512
|
-
email: normalized,
|
|
7513
|
-
...profile?.fullName ? { fullName: profile.fullName } : void 0,
|
|
7514
|
-
...profile?.userName ? { userName: profile.userName } : void 0
|
|
7515
|
-
};
|
|
7516
|
-
}
|
|
7517
|
-
/** Build one web mailbox entry. */
|
|
7518
|
-
function buildWebInboundMessage(args) {
|
|
7519
|
-
const text = args.message.trim();
|
|
7520
|
-
if (!text && !args.attachments?.length) throw new Error("Web Message must not be empty");
|
|
7521
|
-
if (!args.actor.email) throw new Error("Web Actor requires a verified email");
|
|
7522
|
-
const destination = resolveConversationDestination({
|
|
7523
|
-
conversationId: args.conversationId,
|
|
7524
|
-
existing: args.destination
|
|
7525
|
-
});
|
|
7526
|
-
const nowMs = args.nowMs ?? Date.now();
|
|
7527
|
-
return {
|
|
7528
|
-
conversationId: args.conversationId,
|
|
7529
|
-
createdAtMs: args.createdAtMs ?? nowMs,
|
|
7530
|
-
delivery: "defer",
|
|
7531
|
-
destination,
|
|
7532
|
-
inboundMessageId: args.messageId,
|
|
7533
|
-
input: {
|
|
7534
|
-
authorId: args.actor.userId,
|
|
7535
|
-
text,
|
|
7536
|
-
...args.attachments?.length ? { attachments: args.attachments } : void 0,
|
|
7537
|
-
metadata: {
|
|
7538
|
-
authorEmail: normalizeEmail(args.actor.email),
|
|
7539
|
-
...args.actor.fullName && { authorFullName: args.actor.fullName },
|
|
7540
|
-
authorUserId: args.actor.userId,
|
|
7541
|
-
...args.actor.userName && { authorUserName: args.actor.userName },
|
|
7542
|
-
kind: "api_turn",
|
|
7543
|
-
messageId: args.messageId
|
|
7544
|
-
}
|
|
7545
|
-
},
|
|
7546
|
-
receivedAtMs: nowMs,
|
|
7547
|
-
source: "web"
|
|
7548
|
-
};
|
|
7549
|
-
}
|
|
7550
|
-
/** Record web activity and create a Conversation root when needed. */
|
|
7551
|
-
async function recordWebConversationActivity(args) {
|
|
7552
|
-
const store = args.conversationStore ?? getConversationStore();
|
|
7553
|
-
const existing = await store.get({ conversationId: args.conversationId });
|
|
7554
|
-
const destination = resolveConversationDestination({
|
|
7555
|
-
conversationId: args.conversationId,
|
|
7556
|
-
existing: existing?.destination
|
|
7557
|
-
});
|
|
7558
|
-
const isNewRoot = !existing;
|
|
7559
|
-
const visibility = args.rootVisibility === "private" ? "private" : "public";
|
|
7560
|
-
const source = isNewRoot ? createWebSource(args.conversationId, visibility) : void 0;
|
|
7561
|
-
await store.recordActivity({
|
|
7562
|
-
conversationId: args.conversationId,
|
|
7563
|
-
destination,
|
|
7564
|
-
nowMs: args.nowMs,
|
|
7565
|
-
actor: storedActorFromWeb(args.actor),
|
|
7566
|
-
...isNewRoot ? { source: "web" } : void 0,
|
|
7567
|
-
...source ? { sessionSource: source } : void 0,
|
|
7568
|
-
...isNewRoot ? { visibility } : void 0
|
|
7569
|
-
});
|
|
7570
|
-
return destination;
|
|
7571
|
-
}
|
|
7572
|
-
/** Create a Conversation from web input and enqueue its first Message. */
|
|
7573
|
-
async function createAndEnqueueConversation(input, options) {
|
|
7574
|
-
if (!input.actor.email) throw new Error("Web Actor requires a verified email");
|
|
7575
|
-
const conversationId = createConversationId({
|
|
7576
|
-
actorEmail: input.actor.email,
|
|
7577
|
-
idempotencyKey: input.idempotencyKey
|
|
7578
|
-
});
|
|
7579
|
-
return await appendAndEnqueueWebMessage({
|
|
7580
|
-
actor: input.actor,
|
|
7581
|
-
conversationId,
|
|
7582
|
-
idempotencyKey: input.idempotencyKey,
|
|
7583
|
-
message: input.message,
|
|
7584
|
-
images: input.images,
|
|
7585
|
-
rootVisibility: input.visibility === "private" ? "private" : "public"
|
|
7586
|
-
}, options);
|
|
7587
|
-
}
|
|
7588
|
-
async function appendAndEnqueueWebMessage(input, options) {
|
|
7589
|
-
const text = input.message.trim();
|
|
7590
|
-
if (!text && !input.images?.length) throw new Error("Web Message must not be empty");
|
|
7591
|
-
if (!input.actor.email) throw new Error("Web Actor requires a verified email");
|
|
7592
|
-
const nowMs = options.nowMs ?? Date.now();
|
|
7593
|
-
const messageId = await webMessageId({
|
|
7594
|
-
conversationId: input.conversationId,
|
|
7595
|
-
idempotencyKey: input.idempotencyKey
|
|
7596
|
-
});
|
|
7597
|
-
const destination = await recordWebConversationActivity({
|
|
7598
|
-
actor: input.actor,
|
|
7599
|
-
conversationId: input.conversationId,
|
|
7600
|
-
conversationStore: options.conversationStore,
|
|
7601
|
-
nowMs,
|
|
7602
|
-
...input.rootVisibility && { rootVisibility: input.rootVisibility }
|
|
7603
|
-
});
|
|
7604
|
-
let attachments = [];
|
|
7605
|
-
if (input.images?.length) {
|
|
7606
|
-
if (!options.attachmentStorage) throw new Error("Attachment storage is unavailable.");
|
|
7607
|
-
attachments = await storeInputImages({
|
|
7608
|
-
conversationId: input.conversationId,
|
|
7609
|
-
files: input.images,
|
|
7610
|
-
storage: options.attachmentStorage
|
|
7611
|
-
});
|
|
7612
|
-
}
|
|
7613
|
-
const result = await (options.exclusive ? appendAndEnqueueExclusiveInboundMessage : appendAndEnqueueInboundMessage)({
|
|
7614
|
-
message: buildWebInboundMessage({
|
|
7615
|
-
actor: input.actor,
|
|
7616
|
-
conversationId: input.conversationId,
|
|
7617
|
-
destination,
|
|
7618
|
-
message: text,
|
|
7619
|
-
attachments,
|
|
7620
|
-
messageId,
|
|
7621
|
-
nowMs
|
|
7622
|
-
}),
|
|
7623
|
-
conversationStore: options.conversationStore,
|
|
7624
|
-
nowMs,
|
|
7625
|
-
queue: options.queue,
|
|
7626
|
-
state: options.state
|
|
7627
|
-
});
|
|
7628
|
-
const status = result.status === "appended" ? "accepted" : result.status;
|
|
7629
|
-
return {
|
|
7630
|
-
conversationId: input.conversationId,
|
|
7631
|
-
messageId,
|
|
7632
|
-
status
|
|
7633
|
-
};
|
|
7634
|
-
}
|
|
7635
6659
|
// src/chat/slack/location-url.ts
|
|
7636
6660
|
/** Parse a Slack workspace subdomain used in `https://{domain}.slack.com` URLs. */
|
|
7637
6661
|
function parseSlackTeamDomain(value) {
|
|
@@ -8343,7 +7367,7 @@ async function readLastUserMessageAtByConversation(db, conversationIds) {
|
|
|
8343
7367
|
const rows = await db.select({
|
|
8344
7368
|
conversationId: juniorConversationEvents.conversationId,
|
|
8345
7369
|
lastUserMessageAt: sql`max(${juniorConversationEvents.createdAt})`
|
|
8346
|
-
}).from(juniorConversationEvents).where(and(inArray(juniorConversationEvents.conversationId, conversationIds), or(and(eq(juniorConversationEvents.type, "user_message"), eq(authority, "instruction"), sql`${actorPlatform} in ('slack', 'local', 'web')`), and(eq(juniorConversationEvents.type, "message"), eq(role, "user"),
|
|
7370
|
+
}).from(juniorConversationEvents).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).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"), eq(juniorIdentities.kind, "user"))))).groupBy(juniorConversationEvents.conversationId);
|
|
8347
7371
|
return new Map(rows.map((row) => [row.conversationId, new Date(row.lastUserMessageAt).toISOString()]));
|
|
8348
7372
|
}
|
|
8349
7373
|
// src/api/conversations/activity-preview.ts
|
|
@@ -8519,6 +7543,7 @@ async function attachActivityPreviewMentions(executor, previews) {
|
|
|
8519
7543
|
}
|
|
8520
7544
|
// src/api/conversations/participants.ts
|
|
8521
7545
|
const identityColumns = {
|
|
7546
|
+
avatarUrl: juniorIdentities.avatarUrl,
|
|
8522
7547
|
displayName: juniorIdentities.displayName,
|
|
8523
7548
|
email: juniorIdentities.email,
|
|
8524
7549
|
handle: juniorIdentities.handle,
|
|
@@ -8529,11 +7554,13 @@ const identityColumns = {
|
|
|
8529
7554
|
userEmail: juniorUsers.primaryEmail,
|
|
8530
7555
|
userId: juniorIdentities.userId
|
|
8531
7556
|
};
|
|
8532
|
-
function participantIdentity(row) {
|
|
7557
|
+
function participantIdentity(row, userAvatarUrls) {
|
|
7558
|
+
const avatarUrl = identityAvatarUrl(row, userAvatarUrls);
|
|
8533
7559
|
const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
|
|
8534
7560
|
const email = row.email?.trim() || row.userEmail?.trim();
|
|
8535
7561
|
const slack = row.provider === "slack";
|
|
8536
7562
|
const participant = {
|
|
7563
|
+
...avatarUrl ? { avatarUrl } : void 0,
|
|
8537
7564
|
...email ? { email } : void 0,
|
|
8538
7565
|
...fullName ? { fullName } : void 0,
|
|
8539
7566
|
...slack && row.providerSubjectId ? { slackUserId: row.providerSubjectId } : void 0,
|
|
@@ -8551,10 +7578,11 @@ async function readConversationParticipants(db, conversationIds) {
|
|
|
8551
7578
|
conversationId: juniorConversationEvents.conversationId,
|
|
8552
7579
|
seq: juniorConversationEvents.seq,
|
|
8553
7580
|
...identityColumns
|
|
8554
|
-
}).from(juniorConversationEvents).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(inArray(juniorConversationEvents.conversationId, [...conversationIds])).orderBy(asc(juniorConversationEvents.conversationId), asc(juniorConversationEvents.actorIdentityId), asc(juniorConversationEvents.seq))]);
|
|
7581
|
+
}).from(juniorConversationEvents).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(inArray(juniorConversationEvents.conversationId, [...conversationIds]), ne(juniorIdentities.kind, "system"))).orderBy(asc(juniorConversationEvents.conversationId), asc(juniorConversationEvents.actorIdentityId), asc(juniorConversationEvents.seq))]);
|
|
7582
|
+
const userAvatarUrls = await readUserAvatarUrls(db, [...rootRows, ...eventRows].flatMap((row) => row.userId ? [row.userId] : []));
|
|
8555
7583
|
const ordered = /* @__PURE__ */ new Map();
|
|
8556
7584
|
const add = (conversationId, row) => {
|
|
8557
|
-
const participant = participantIdentity(row);
|
|
7585
|
+
const participant = participantIdentity(row, userAvatarUrls);
|
|
8558
7586
|
if (!participant) return;
|
|
8559
7587
|
const group = ordered.get(conversationId) ?? {
|
|
8560
7588
|
identities: [],
|
|
@@ -8784,4 +7812,4 @@ async function readConversationFeedFromSql(options = {}) {
|
|
|
8784
7812
|
async function readConversationFeed(options = {}) {
|
|
8785
7813
|
return conversationFeedSchema.parse(await readConversationFeedFromSql(options));
|
|
8786
7814
|
}
|
|
8787
|
-
export {
|
|
7815
|
+
export { stopSlackThread as A, actOnReviewItem as B, isAllowedSignInEmail as C, buildSlackInboundMessage as D, recordSkippedConversationMessage as E, bindMessageAuthorIdentityId as F, AGENT_DISPATCH_MAX_AGE_MS as G, readReviewItemForUser as H, startProcessingReaction as I, readHealthReport as J, enqueueAgentDispatch as K, stripLeadingBotMention as L, withNormalizedThreadId as M, stopConversationTurn as N, clearSlackPendingReactions as O, THREAD_OPTOUT_ACK as P, parseContent as R, isAllowedDomain as S, resolveAllowedDomains as T, reviewItemChange as U, closeReviewItemsAnsweredByReply as V, getAttachmentStorage as W, createProductionConversationWorkOptions as _, resolveMentions as a, createSlackWebApiStatusSender as b, readConversationAuxiliaryCostsFromSql as c, notCopiedByFork as d, childAgentTotalsReport as f, resolveSlackTeamDomains as g, conversationSummaryFromStoredConversation as h, attachConversationMentions as i, normalizeIncomingSlackThreadId as j, isSlackMessageStopped as k, ConversationForkError as l, conversationEventHistory as m, readConversationRecordFromSql as n, readFirstHumanMessageTexts as o, readConversationTreeMetricsFromSql as p, GET as q, readConversationParticipants as r, listConversationSidebarAnnotations as s, readConversationFeed as t, forkConversation as u, createProductionSlackWebhookServices as v, readEnvList as w, assertNoLegacyAllowedDomains as x, getProductionSlackAdapter as y, ReviewItemActionError as z };
|