@sentry/junior 0.267.0 → 0.268.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{actor-DZSokpwj.js → actor-CP9K2Idu.js} +2 -1
- package/dist/{adapter-BybMEezj.js → adapter-04AcD2SO.js} +4 -6
- package/dist/{admin-DOs1_EiW.js → admin-DIU1QyG3.js} +1 -1
- package/dist/{agent-invocations-CxKldAeB.js → agent-invocations-C1RNOCs8.js} +2 -0
- package/dist/{agent-runner-DgNATN4D.js → agent-runner-CAYfFmU7.js} +4409 -3020
- package/dist/api/schema.d.ts +2 -5629
- package/dist/api/schema.js +4 -4
- package/dist/api.d.ts +1 -1
- package/dist/api.js +154 -32
- package/dist/{app-DAB3fzyp.d.ts → app-z6Uuyq3j.d.ts} +3 -2
- package/dist/app.d.ts +1 -1
- package/dist/app.js +332 -98
- package/dist/{briefs-CWKjka7v.js → briefs-DQbV2jNr.js} +2 -2
- package/dist/{catalog-runtime-cMlAI5LY.js → catalog-runtime-BD9GSpc6.js} +1 -1
- package/dist/cli/chat.js +15 -15
- package/dist/cli/check.js +3 -3
- package/dist/cli/main.js +3 -3
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +4 -4
- package/dist/cli/upgrade.d.ts +1 -1
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-BjgbMawj.js → client-C1yDYjoS.js} +2 -2
- package/dist/{config-CdB6yJj5.js → config-DrTeF80T.js} +2 -2
- package/dist/{connections-RE_65jzK.js → connections-D46f8vvi.js} +10 -11
- package/dist/{conversation-privacy-BzgJC5k0.js → conversation-privacy-CxlOCiUo.js} +1 -1
- package/dist/{credential-sync-DhEGEpgq.js → credential-sync-sm9fegLY.js} +29 -34
- package/dist/{credentialed-CIGwoLU2.js → credentialed-lukuD-cc.js} +39 -13
- package/dist/db-BU4LQD8s.d.ts +6 -0
- package/dist/{executor-VYrx_K4w.js → executor-4vhvsMZJ.js} +78 -4
- package/dist/instrumentation.js +2 -2
- package/dist/{assistant-reply-CT7VcXQe.js → interruption-marker-CkncDT8d.js} +8 -2
- package/dist/{job-queue-C0BZSa7r.js → job-queue-In9QKz-I.js} +2 -2
- package/dist/{list-1XQP-o6K.js → list-BkgnXEEB.js} +298 -807
- package/dist/{logging-DuZ1A4ab.js → logging-DLSpkSSr.js} +1 -1
- package/dist/{message-NKSbbnA6.js → message-BObyg59s.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-B-WbSdoH.js} +2 -2
- package/dist/{registry-FQutO926.js → registry-BgecKva0.js} +2 -2
- package/dist/{web-message-id-CJSUiksE.js → reporting-window-DVCD_10C.js} +1 -10
- package/dist/{resolve-Bk4yeuHy.js → resolve-Cpa4Bu1O.js} +3 -3
- package/dist/{result-B9e9ATOU.js → result-D3TSmZZ8.js} +8 -8
- package/dist/{runner-C3S2ObH-.js → runner-DS5OKhEp.js} +10 -10
- package/dist/{sandbox-egress-signals-DqpGzzMB.js → sandbox-egress-signals-CnDjY5rx.js} +1 -1
- package/dist/{schema-X7Fj4fv1.js → schema-0050HFnB.js} +80 -4
- package/dist/schema-ZngqcOWM.d.ts +5813 -0
- package/dist/{service-account-Do30HUNv.js → service-account-CKV2WTY7.js} +3 -3
- package/dist/{service-account-BQasl-qq.js → service-account-DGjnDw9y.js} +3 -3
- package/dist/{session-stop-DkOjt46_.js → session-stop-Dam1EI7B.js} +494 -18
- package/dist/{skills-CQHrwfOa.js → skills-CfAnfImD.js} +2 -2
- package/dist/{spawn-BHuqN14f.js → spawn-cmq9p4Ly.js} +2 -2
- package/dist/{store-6EetVP-m.js → store-C5PN02QO.js} +4 -4
- package/dist/{store-B8CABbmR.d.ts → store-xMuuXRE0.d.ts} +13 -7
- package/dist/{store-DvNZIsCr.js → store-zPEoJPBB.js} +3 -3
- package/dist/{task-queue-B0DLgd2P.js → task-queue-_mf28dSu.js} +3 -3
- package/dist/{task-runner-B-PD_JOj.js → task-runner-DMI2prDO.js} +11 -12
- package/dist/{tasks-0ilmuCux.js → tasks-SB9z-VTU.js} +124 -21
- package/dist/{thread-state-DRmdJ2QN.js → thread-state-BbbPh0Sn.js} +8 -18
- package/dist/{turn-failure-response-G11cs2gE.js → turn-failure-response-BKKmnMB5.js} +10 -15
- package/dist/{turn-lifecycle-OzLIwvi1.js → turn-lifecycle-oP0kVeGz.js} +1 -1
- package/dist/{validation-CxEmt1id.js → validation--JsAGLwc.js} +1 -1
- package/dist/{version-Bapedn4A.js → version-jHwJp4hC.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-DrN3oC2X.js → viewer-CpXTGlep.js} +323 -235
- package/dist/{user-input-request-BDFBdF8r.js → web-message-id-DlHNANtd.js} +17 -1
- package/dist/{work-DUQoNcqP.js → work-3N-GHY9m.js} +10 -10
- package/dist/{worker-D1l4Ugk0.js → worker-CCdbcItd.js} +6 -6
- package/migrations/0065_user_customization.sql +1 -0
- package/migrations/0066_review_items.sql +53 -0
- package/migrations/0067_review_item_dedupe_tenant.sql +2 -0
- package/migrations/0068_review_item_slack_cards.sql +10 -0
- package/migrations/meta/0065_snapshot.json +3511 -0
- package/migrations/meta/0066_snapshot.json +4099 -0
- package/migrations/meta/0067_snapshot.json +3921 -0
- package/migrations/meta/0068_snapshot.json +4176 -0
- package/migrations/meta/_journal.json +29 -1
- package/package.json +5 -5
- package/dist/db-Bg-zC3r5.d.ts +0 -5
- package/dist/images-CecjXAAz.js +0 -64
- package/dist/turn-session-routing-Dyut9l7Y.js +0 -76
|
@@ -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 { A as resolveConversationRouting, At as upsertConversationMessage, Bt as clearPendingAuth, Ct as estimateConversationContextTokens, Dt as normalizeConversationText, E as sameDefinitionValue, Et as markConversationMessage, Gt as saveTurnCheckpoint, Ht as wasPendingAuthStopped, In as persistWithRetry, It as AuthorizationFlowDisabledError, Jt as failTurnRecord, K as commitAcceptedReply, Kt as abandonTurnRecord, Mt as PluginCredentialFailureError, N as automationEventCatalog, Nn as buildTurnErrorResponse, Ot as recordDeliveredAssistantMessage, Q as loadTurnProjection, S as automationDefinition, St as createConversationMemoryService, Tt as isHumanConversationMessage, V as registeredEventTriggerProblems, Wt as loadTurnCheckpoint, Xn as trimTrailingAssistantMessages, Xt as listTurnSummaries, Y as loadConversationProjection, Yt as getTurnRecord, Z as loadProjection, Zt as recordTurnSummary, _ as parseEventAutomationRow, ar as getThreadStopDecision, bt as appendThreadContextMessages, c as withScheduledAutomationLock, ct as recordSubscribedReplyRoute, er as buildDeterministicAssistantMessageId, f as deleteEventAutomation, ft as hydrateConversationMessages, gt as resolveConversationTitle, h as listDeletedEventAutomationsCreatedBy, hr as piMessageFromHistoryItem, ht as persistConversationMessageSummaries, i as readScheduledAutomation, ir as getSubscribedReplyPreflightDecision, it as recordAuthorizationCompleted, j as resolveTurnSessionRouting, kt as turnHasReply, o as saveScheduledAutomationInLock, pr as configValueSchema, pt as persistConversationMessages, q as commitMessages, qt as completeTurnRecord, r as parseScheduledAutomationRow, rr as decideSubscribedThreadReply, tr as buildDeterministicTurnId, ur as isExperimentalFeatureEnabled, vt as fallbackShortTitle, w as listAutomationVersions, wt as getConversationMessageSlackTs, xt as buildConversationContext } from "./tasks-SB9z-VTU.js";
|
|
2
|
+
import { a as completeText, p as escapeXml, r as completeDecision, y as isProviderRetryError } from "./client-C1yDYjoS.js";
|
|
3
|
+
import { c as getSlackClientSecret, i as getChatConfig, l as getSlackSigningSecret, n as botConfig, o as getSlackBotToken, s as getSlackClientId, v as normalizeSlackEmojiName, y as createS3AttachmentStorage } from "./config-DrTeF80T.js";
|
|
4
|
+
import { B as isRecord, E as withSpan, H as toOptionalString, S as setTags, T as withLogContext, V as toOptionalNumber, b as setSpanAttributes, f as logError, h as logWarn, m as logInfo, n as createChatSdkLogger, o as getActiveTraceContext, p as logException, s as getActiveTraceId, t as buildTurnFailureResponse, y as setSentryUser } from "./logging-DLSpkSSr.js";
|
|
5
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 {
|
|
7
|
+
import { f as parseSlackTeamId, i as isActorUserId, m as parseSlackMessageTs, n as createSlackActor, o as isUserActor, r as createSlackResumeActor, s as parseActorUserId, t as createActor, u as parseSlackChannelId } from "./actor-CP9K2Idu.js";
|
|
8
|
+
import { L as hasAgentTurnUsage, M as parseSessionSource, N as addAgentTurnUsage, V as requireSlackDestination, _ as withConversationEventLock, d as getConversationEventStore, h as getSqlExecutor, m as getDb, p as getConversationStore, v as loadConversationSlackTenantIds, x as locationFromRow, z as createSlackDestination } from "./viewer-CpXTGlep.js";
|
|
9
|
+
import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, o as conversationReadColumns, s as juniorConversations } from "./agent-invocations-C1RNOCs8.js";
|
|
10
|
+
import { S as juniorCodeChanges, d as juniorAutomations, g as juniorConversationBriefs, l as juniorAutomationRuns, y as juniorConversationEvents } from "./executor-4vhvsMZJ.js";
|
|
11
11
|
import { d as contextProvenance, p as instructionProvenanceFor } from "./history-C84mlAwO.js";
|
|
12
|
-
import { d as actorLogContext, f as credentialContextForActor } from "./registry-
|
|
13
|
-
import {
|
|
14
|
-
import { $ as
|
|
15
|
-
import { a as MUTATION_LOCK_TTL_MS, c as withLock, i as getStateAdapter, o as acquireActiveLock } from "./adapter-
|
|
12
|
+
import { d as actorLogContext, f as credentialContextForActor } from "./registry-BgecKva0.js";
|
|
13
|
+
import { A as getConversationInfo, D as readSlackRawMessageContext, E as parseSlackThreadId, Ft as buildSteeringPiMessage, H as syncReviewItemSlackCards, It as withoutSavedSteeringMessages, L as describeEventAutomationTrigger, Mt as conversationSubtreeColumns, O as resolveSlackChannelIdFromMessage, R as effectiveTaskOutcomes, Ut as shouldEmitDevAgentTrace, _t as recordWebConversationActivity, at as getReviewItem, b as renderBlockText, dt as sendReviewItemMessages, et as runWithSlackInstallation, gt as createConversationId, i as buildAuthPauseResponse, j as listThreadReplies, jt as conversationSubtree, k as resolveSlackChannelIdFromThreadId, lt as takeReviewItem, n as executeAgentRun, nt as canReadReviewItem, p as latestProgressStatus, r as createContextCompactor, rt as closeReviewItem, st as reviewItemReaderForUser, t as createAgentRunner, vt as webActorFromEmail, wt as storeAttachment, y as renderAttachmentText, yt as resolveConversationDestination, zt as RetryableDeliveryError } from "./agent-runner-CAYfFmU7.js";
|
|
14
|
+
import { $n as recordSlackAttachment, $t as verifyTimerWatchCredentialSubject, Bn as conversationArchivedForUser, Gn as viewerConversationMembership, Hn as conversationIsMemberRoot, In as createPluginLogger, J as truncateStatusText, K as normalizeSlackStatusText, Ln as readConversationAccessFromSql, Lt as cancelSubscriptions, Qn as readAttachmentVisionSummary, Rn as conversationArchiveTimes, Un as conversationNotArchivedForEmail, Vn as conversationHasParticipantUser, Wn as conversationNotArchivedForUser, Xn as getSlackFileId, Y as addReactionToMessage, cr as createPluginCache, en as SlackActionError, in as getSlackClient, ir as listConversationAnnotationsById, ln as withSlackRetries, n as createUserTokenStore, nt as removeReactionFromMessage, on as isDmChannel, p as getPlugins, pn as JUNIOR_THREAD_STATE_TTL_MS, rn as getHeaderString, sn as normalizeSlackConversationId, sr as lookupSlackUser, tn as downloadPrivateSlackFile, zn as conversationArchivedForEmail } from "./credentialed-lukuD-cc.js";
|
|
15
|
+
import { a as MUTATION_LOCK_TTL_MS, c as withLock, i as getStateAdapter, o as acquireActiveLock } from "./adapter-04AcD2SO.js";
|
|
16
16
|
import { W as readMessageAttachments, g as conversationFeedSchema, q as isVisionImageMediaType } from "./conversation-DSo310Ps.js";
|
|
17
17
|
import { a as EVENT_SYSTEM_ACTOR, o as isEventConversationMessage } from "./prompt-BW9jFsqZ.js";
|
|
18
|
-
import { B as ConversationTurnBoundaryError, G as isCooperativeTurnYieldError, H as TurnInputCommitLostError, J as markTurnClosed, K as isTurnInputCommitLostError, U as TurnInputDeferredError, V as CooperativeTurnYieldError, W as getConversationTurnBoundaryError, X as markTurnFailed, Y as markTurnCompleted, Z as startActiveTurn, q as isTurnInputDeferredError } from "./store-
|
|
19
|
-
import {
|
|
20
|
-
import { C as maybePostAutomatedTurnLimitNotice,
|
|
21
|
-
import { a as
|
|
22
|
-
import {
|
|
18
|
+
import { B as ConversationTurnBoundaryError, G as isCooperativeTurnYieldError, H as TurnInputCommitLostError, J as markTurnClosed, K as isTurnInputCommitLostError, U as TurnInputDeferredError, V as CooperativeTurnYieldError, W as getConversationTurnBoundaryError, X as markTurnFailed, Y as markTurnCompleted, Z as startActiveTurn, q as isTurnInputDeferredError } from "./store-C5PN02QO.js";
|
|
19
|
+
import { r as getAssistantReplyText } from "./interruption-marker-CkncDT8d.js";
|
|
20
|
+
import { C as maybePostAutomatedTurnLimitNotice, D as recordFinishedTurnForAutomatedLimit, I as runDispatchOutcome, M as finishedRunReply, N as postsRunNotices, T as sendSlackReply, c as isEventMailboxMetadata, d as getEventCatalog, i as skipInputBeforeConversationStop, j as deliversFinalText, l as renderEventBatchText, s as eventBatchMessageEvents, u as timerWatchCredentialSubject, w as resolveSlackReplyTarget, x as joinMailboxText } from "./session-stop-Dam1EI7B.js";
|
|
21
|
+
import { a as isEmptyUserInputAnswer, o as normalizeUserInputAnswer } from "./web-message-id-DlHNANtd.js";
|
|
22
|
+
import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline } from "./job-queue-In9QKz-I.js";
|
|
23
|
+
import { D as stopConversationWork, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, r as appendAndEnqueueInboundMessage, w as requestConversationWork } from "./store-zPEoJPBB.js";
|
|
23
24
|
import { j as eventAutomationTriggerSchema, l as automationListQuerySchema } from "./automation-CmeFBSEh.js";
|
|
24
|
-
import {
|
|
25
|
-
import { A as readAutomationExecutions, D as readAutomationExecutionStatusDays, E as readAutomationExecutionHours, M as readAutomationTitlesByConversationId, O as readAutomationExecutionStatusHours, S as emptyAutomationRunWindows, T as readAutomationExecutionDays, _ as markDispatchBlocked, a as claimDispatchMailboxAppend, b as markDispatchRunning, c as getDispatchConversationId, d as getDispatchRecord, f as getDispatchTurnId, g as markDispatchAwaitingResume, j as readAutomationRuns, k as readAutomationExecutionSummaries, l as getDispatchIdForConversation, m as isTerminalDispatchStatus, n as scheduleSessionCompletedPluginTasks, o as confirmDispatchMailboxAppend, t as processPluginTask, u as getDispatchInputMessageId, v as markDispatchCompleted, w as readAutomationExecutionByConversationId, y as markDispatchFailed } from "./task-runner-
|
|
26
|
-
import { a as withAgentInvocationResultActors, c as resolveMailboxTurnWork, d as legacyWebMailboxMetadataSchema, i as startAgentInvocationsForChildInput, l as sourceFromTurnInput, o as getAuthPausedConversationTurnId, s as getEventBatchDelayMs, t as enqueueAgentInvocationResult, u as turnInputFactsFromConversationMessage } from "./result-
|
|
27
|
-
import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-
|
|
28
|
-
import { i as resolveAgentInvocationId, o as AgentRunError, s as executeTurn, t as createAgentInvocationWorker } from "./work-
|
|
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, d as loadPendingMessageCards, i as completeAuthPauseTurn, l as buildDeliveredTurnStatePatch, n as getAgentTurnDiagnosticsAttributes, o as getTurnUserMessage, r as requireTurnFailureEventId, s as getTurnUserReplyAttachmentContext, t as finalizeFailedTurnReplyWithEvent, u as getInterruptionMarker } from "./turn-failure-response-G11cs2gE.js";
|
|
35
|
-
import { r as storeInputImages } from "./images-CecjXAAz.js";
|
|
25
|
+
import { p as healthReportSchema, s as sumUtcHoursIntoSixHours } from "./reporting-window-DVCD_10C.js";
|
|
26
|
+
import { A as readAutomationExecutions, D as readAutomationExecutionStatusDays, E as readAutomationExecutionHours, M as readAutomationTitlesByConversationId, O as readAutomationExecutionStatusHours, S as emptyAutomationRunWindows, T as readAutomationExecutionDays, _ as markDispatchBlocked, a as claimDispatchMailboxAppend, b as markDispatchRunning, c as getDispatchConversationId, d as getDispatchRecord, f as getDispatchTurnId, g as markDispatchAwaitingResume, j as readAutomationRuns, k as readAutomationExecutionSummaries, l as getDispatchIdForConversation, m as isTerminalDispatchStatus, n as scheduleSessionCompletedPluginTasks, o as confirmDispatchMailboxAppend, t as processPluginTask, u as getDispatchInputMessageId, v as markDispatchCompleted, w as readAutomationExecutionByConversationId, y as markDispatchFailed } from "./task-runner-DMI2prDO.js";
|
|
27
|
+
import { a as withAgentInvocationResultActors, c as resolveMailboxTurnWork, d as legacyWebMailboxMetadataSchema, i as startAgentInvocationsForChildInput, l as sourceFromTurnInput, o as getAuthPausedConversationTurnId, s as getEventBatchDelayMs, t as enqueueAgentInvocationResult, u as turnInputFactsFromConversationMessage } from "./result-D3TSmZZ8.js";
|
|
28
|
+
import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-oP0kVeGz.js";
|
|
29
|
+
import { i as resolveAgentInvocationId, o as AgentRunError, s as executeTurn, t as createAgentInvocationWorker } from "./work-3N-GHY9m.js";
|
|
30
|
+
import { a as persistThreadStateById, i as persistThreadRuntimeState, n as getPersistedSandboxState, o as coerceThreadConversationState, r as getPersistedThreadState, t as getLocationConfigurationService } from "./thread-state-BbbPh0Sn.js";
|
|
31
|
+
import { c as deleteWebAuthorization, d as createPausedTurns, f as getPausedTurnRequest, h as getTurnAuthorization, m as wakePausedTurn, s as createWebAuthorization } from "./connections-D46f8vvi.js";
|
|
32
|
+
import { t as bindMessageAgent } from "./message-BObyg59s.js";
|
|
33
|
+
import { t as bindSpawnAgent } from "./spawn-cmq9p4Ly.js";
|
|
34
|
+
import { a as persistAuthPauseTurnState, c as getTurnUserSlackMessageTs, i as completeAuthPauseTurn, l as buildDeliveredTurnStatePatch, n as getAgentTurnDiagnosticsAttributes, o as getTurnUserMessage, r as requireTurnFailureEventId, s as getTurnUserReplyAttachmentContext, t as finalizeFailedTurnReplyWithEvent, u as loadPendingMessageCards } from "./turn-failure-response-BKKmnMB5.js";
|
|
36
35
|
import { z } from "zod";
|
|
37
36
|
import { createHash } from "node:crypto";
|
|
38
37
|
import { and, asc, count, desc, eq, exists, inArray, isNotNull, isNull, ne, notInArray, or, sql } from "drizzle-orm";
|
|
39
|
-
import { conversationSidebarAnnotationSchema, createSlackSource,
|
|
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";
|
|
@@ -380,207 +379,140 @@ function createAttachmentStorage(config) {
|
|
|
380
379
|
default: throw new Error(`Unsupported attachment storage provider: ${String(config.provider)}`);
|
|
381
380
|
}
|
|
382
381
|
}
|
|
383
|
-
// src/chat/
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
custom_fields: [{
|
|
423
|
-
key: "trigger",
|
|
424
|
-
label: "When",
|
|
425
|
-
type: "string",
|
|
426
|
-
value: trigger,
|
|
427
|
-
long: true
|
|
428
|
-
}, ...warning ? [{
|
|
429
|
-
key: "warning",
|
|
430
|
-
label: "Needs attention",
|
|
431
|
-
type: "string",
|
|
432
|
-
value: warning,
|
|
433
|
-
long: true
|
|
434
|
-
}] : []]
|
|
435
|
-
}
|
|
436
|
-
}
|
|
382
|
+
// src/chat/review-items/actions.ts
|
|
383
|
+
/** An expected failure of a person action, for the API to report. */
|
|
384
|
+
var ReviewItemActionError = class extends Error {
|
|
385
|
+
code;
|
|
386
|
+
constructor(code, message) {
|
|
387
|
+
super(message);
|
|
388
|
+
this.name = "ReviewItemActionError";
|
|
389
|
+
this.code = code;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* Return whether a User can act on an open Review item: an assigned User,
|
|
394
|
+
* anyone for an item assigned to Everyone, and anyone for an item with no
|
|
395
|
+
* assignees. The caller checks that the User can see the item.
|
|
396
|
+
*/
|
|
397
|
+
function canActOnReviewItem(item, userId) {
|
|
398
|
+
if (item.status !== "open") return false;
|
|
399
|
+
if (item.assignedToEveryone) return true;
|
|
400
|
+
if (item.assignees.length === 0) return true;
|
|
401
|
+
return item.assignees.some((assignee) => assignee.userId === userId);
|
|
402
|
+
}
|
|
403
|
+
/** Return whether a User is already the only assignee, so take does nothing. */
|
|
404
|
+
function isOnlyAssignee(item, userId) {
|
|
405
|
+
return !item.assignedToEveryone && item.assignees.length === 1 && item.assignees[0]?.userId === userId;
|
|
406
|
+
}
|
|
407
|
+
/** Read one Review item that a User can see, or fail as not found. */
|
|
408
|
+
async function readReviewItemForUser(user, id) {
|
|
409
|
+
const item = await getReviewItem(getDb(), id);
|
|
410
|
+
if (!item || !canReadReviewItem(item, reviewItemReaderForUser(user))) throw new ReviewItemActionError("not_found", "Review item not found.");
|
|
411
|
+
return item;
|
|
412
|
+
}
|
|
413
|
+
function closedStatus(action) {
|
|
414
|
+
return action === "dismiss" ? "dismissed" : "done";
|
|
415
|
+
}
|
|
416
|
+
/** The change that one person action makes, for messages and notes. */
|
|
417
|
+
function reviewItemChange(action, user) {
|
|
418
|
+
if (action === "take") return {
|
|
419
|
+
kind: "taken",
|
|
420
|
+
user
|
|
437
421
|
};
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const text = value?.replace(/\r\n/g, "\n").trim() ?? "";
|
|
442
|
-
const preview = text.split("\n").slice(0, 6).join("\n").slice(0, 500);
|
|
443
|
-
return preview.length < text.length ? `${preview.slice(0, 499).trimEnd()}…` : preview;
|
|
444
|
-
}
|
|
445
|
-
/** Render a compact Task or Item preview, or the saved facts for its detail panel. */
|
|
446
|
-
function renderSlackObjectCard(card, conversationId, surface = "preview") {
|
|
447
|
-
if (card.objectType === "automation" && card.plugin === "junior") return renderSlackAutomationCard({
|
|
448
|
-
id: card.key,
|
|
449
|
-
title: card.title,
|
|
450
|
-
url: card.url,
|
|
451
|
-
trigger: card.trigger ?? "",
|
|
452
|
-
warning: card.warning ?? null
|
|
453
|
-
});
|
|
454
|
-
const presentation = objectPresentation(card);
|
|
455
|
-
const type = card.displayType ?? presentation.label;
|
|
456
|
-
const iconUrl = getDashboardObjectIconLink(presentation.icon);
|
|
457
|
-
const title = card.title.slice(0, 160);
|
|
458
|
-
const details = surface === "details";
|
|
459
|
-
const deployment = card.objectType === "deployment" || card.objectType === "item" && card.facts?.type === "deployment";
|
|
460
|
-
const calendarEvent = card.objectType === "calendar_event";
|
|
461
|
-
const status = details || deployment || calendarEvent ? card.status : void 0;
|
|
462
|
-
const warning = details || deployment ? card.warning : void 0;
|
|
463
|
-
const facts = objectFactFields(card.facts).filter((field) => details || calendarEvent || deployment && field.key === "environment");
|
|
464
|
-
const description = details ? card.description?.trim() ?? "" : calendarEvent ? "" : descriptionPreview(card.description);
|
|
465
|
-
const text = [
|
|
466
|
-
escapeSlackMrkdwnText(type),
|
|
467
|
-
card.url ? formatSlackLink(card.url, title) : escapeSlackMrkdwnText(title),
|
|
468
|
-
escapeSlackMrkdwnText(card.label),
|
|
469
|
-
status ? escapeSlackMrkdwnText(status) : null,
|
|
470
|
-
description ? escapeSlackMrkdwnText(description) : null,
|
|
471
|
-
warning ? escapeSlackMrkdwnText(`Needs attention: ${warning}`) : null,
|
|
472
|
-
...facts.map((field) => escapeSlackMrkdwnText(`${field.label}: ${field.value}`))
|
|
473
|
-
].filter(Boolean).join("\n");
|
|
474
|
-
if (!card.url) return {
|
|
475
|
-
entity: null,
|
|
476
|
-
text
|
|
422
|
+
if (action === "submit") return {
|
|
423
|
+
kind: "submitted",
|
|
424
|
+
user
|
|
477
425
|
};
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
display_id: card.label,
|
|
482
|
-
display_type: type,
|
|
483
|
-
product_icon: iconUrl ? {
|
|
484
|
-
url: iconUrl,
|
|
485
|
-
alt_text: `${type}${status ? `: ${status}` : ""}`
|
|
486
|
-
} : void 0,
|
|
487
|
-
product_name: card.plugin
|
|
426
|
+
if (action === "complete") return {
|
|
427
|
+
kind: "completed",
|
|
428
|
+
user
|
|
488
429
|
};
|
|
489
|
-
const customFields = [
|
|
490
|
-
...description ? [{
|
|
491
|
-
key: "description",
|
|
492
|
-
label: "Description",
|
|
493
|
-
type: "string",
|
|
494
|
-
value: description,
|
|
495
|
-
long: true,
|
|
496
|
-
format: "markdown"
|
|
497
|
-
}] : [],
|
|
498
|
-
...facts.map((field) => ({
|
|
499
|
-
...field,
|
|
500
|
-
type: "string"
|
|
501
|
-
})),
|
|
502
|
-
...warning ? [{
|
|
503
|
-
key: "warning",
|
|
504
|
-
label: "Needs attention",
|
|
505
|
-
type: "string",
|
|
506
|
-
value: warning
|
|
507
|
-
}] : []
|
|
508
|
-
];
|
|
509
430
|
return {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
...task ? {
|
|
513
|
-
entity_type: "slack#/entities/task",
|
|
514
|
-
entity_payload: {
|
|
515
|
-
attributes,
|
|
516
|
-
display_order: [...status ? ["status"] : [], ...customFields.map((field) => field.key)],
|
|
517
|
-
fields: status ? { status: { value: status } } : void 0,
|
|
518
|
-
custom_fields: customFields
|
|
519
|
-
}
|
|
520
|
-
} : {
|
|
521
|
-
entity_type: "slack#/entities/item",
|
|
522
|
-
entity_payload: {
|
|
523
|
-
attributes,
|
|
524
|
-
display_order: [...status ? ["status"] : [], ...customFields.map((field) => field.key)],
|
|
525
|
-
custom_fields: [...status ? [{
|
|
526
|
-
key: "status",
|
|
527
|
-
label: "Status",
|
|
528
|
-
type: "string",
|
|
529
|
-
value: status
|
|
530
|
-
}] : [], ...customFields]
|
|
531
|
-
}
|
|
532
|
-
},
|
|
533
|
-
external_ref: {
|
|
534
|
-
id: Buffer.from(JSON.stringify([
|
|
535
|
-
conversationId,
|
|
536
|
-
card.plugin,
|
|
537
|
-
card.key
|
|
538
|
-
]), "utf8").toString("base64url"),
|
|
539
|
-
type: "annotation"
|
|
540
|
-
},
|
|
541
|
-
url: card.url
|
|
542
|
-
}
|
|
431
|
+
kind: "dismissed",
|
|
432
|
+
user
|
|
543
433
|
};
|
|
544
434
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
435
|
+
/** Return whether a closed item is the result of this same action. */
|
|
436
|
+
function isSameClose(item, action, user) {
|
|
437
|
+
return item.closedBy?.userId === user.id && item.status === closedStatus(action) && action === "submit" === Boolean(item.answer);
|
|
438
|
+
}
|
|
439
|
+
function submittedAnswer(item, answer) {
|
|
440
|
+
if (item.kind !== "action" || !item.request) throw new ReviewItemActionError("invalid", "Only an action item takes an answer.");
|
|
441
|
+
const normalized = normalizeUserInputAnswer(item.request, {
|
|
442
|
+
other: answer?.other,
|
|
443
|
+
requestId: item.id,
|
|
444
|
+
values: answer?.values ?? {}
|
|
445
|
+
});
|
|
446
|
+
if (isEmptyUserInputAnswer(normalized)) throw new ReviewItemActionError("invalid", "The answer is empty.");
|
|
447
|
+
return normalized;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Do one person action on a Review item and tell its linked Conversations.
|
|
451
|
+
* The first submit, complete, or dismiss closes the item for every
|
|
452
|
+
* assignee. A retry of the same close sends any missing messages again and
|
|
453
|
+
* succeeds, so a client can retry after a failure. With `firstCloseOnly`,
|
|
454
|
+
* only the call that closes the item succeeds, and every other call gets a
|
|
455
|
+
* conflict. Use it when the caller adds its own input for the action, such
|
|
456
|
+
* as a Slack card click, so one action adds one Turn. `exceptConversationId`
|
|
457
|
+
* is the Conversation where the person acted. It gets the person's input
|
|
458
|
+
* from its own surface, so it gets no message here. Slack cards of the item
|
|
459
|
+
* change in place; see `syncReviewItemSlackCards` for `getSlackAdapter`.
|
|
460
|
+
*/
|
|
461
|
+
async function actOnReviewItem(args) {
|
|
462
|
+
const { action, user } = args;
|
|
463
|
+
const nowMs = args.nowMs ?? Date.now();
|
|
464
|
+
const db = getDb();
|
|
465
|
+
const item = await readReviewItemForUser(user, args.id);
|
|
466
|
+
if (action === "take") {
|
|
467
|
+
if (item.status !== "open") throw new ReviewItemActionError("conflict", "Review item is closed.");
|
|
468
|
+
if (!canActOnReviewItem(item, user.id)) throw new ReviewItemActionError("forbidden", "Only an assignee can take this item.");
|
|
469
|
+
if (!isOnlyAssignee(item, user.id) && !await takeReviewItem(db, {
|
|
470
|
+
id: item.id,
|
|
471
|
+
nowMs,
|
|
472
|
+
userId: user.id
|
|
473
|
+
})) throw new ReviewItemActionError("conflict", "Review item is closed.");
|
|
474
|
+
const taken = await readReviewItemForUser(user, item.id);
|
|
475
|
+
await syncReviewItemSlackCards({
|
|
476
|
+
getSlackAdapter: args.getSlackAdapter,
|
|
477
|
+
itemId: taken.id
|
|
478
|
+
});
|
|
479
|
+
await sendReviewItemMessages({
|
|
480
|
+
change: reviewItemChange(action, user),
|
|
481
|
+
exceptConversationId: args.exceptConversationId,
|
|
482
|
+
item: taken,
|
|
483
|
+
nowMs,
|
|
484
|
+
queue: args.queue
|
|
485
|
+
});
|
|
486
|
+
return taken;
|
|
487
|
+
}
|
|
488
|
+
let closed = item;
|
|
489
|
+
let closedNow = false;
|
|
490
|
+
if (item.status === "open") {
|
|
491
|
+
if (!canActOnReviewItem(item, user.id)) throw new ReviewItemActionError("forbidden", "Only an assignee can act on this item.");
|
|
492
|
+
if (action === "complete" && item.kind === "action") throw new ReviewItemActionError("invalid", "An action item needs an answer. Submit it or dismiss it.");
|
|
493
|
+
const answer = action === "submit" ? submittedAnswer(item, args.answer) : void 0;
|
|
494
|
+
closedNow = await closeReviewItem(db, {
|
|
495
|
+
closedByUserId: user.id,
|
|
496
|
+
id: item.id,
|
|
497
|
+
nowMs,
|
|
498
|
+
status: closedStatus(action),
|
|
499
|
+
...answer ? { answer } : void 0
|
|
500
|
+
});
|
|
501
|
+
closed = await readReviewItemForUser(user, item.id);
|
|
502
|
+
}
|
|
503
|
+
if (args.firstCloseOnly ? !closedNow : !isSameClose(closed, action, user)) throw new ReviewItemActionError("conflict", "Review item is closed.");
|
|
504
|
+
await syncReviewItemSlackCards({
|
|
505
|
+
getSlackAdapter: args.getSlackAdapter,
|
|
506
|
+
itemId: closed.id
|
|
507
|
+
});
|
|
508
|
+
await sendReviewItemMessages({
|
|
509
|
+
change: reviewItemChange(action, user),
|
|
510
|
+
exceptConversationId: args.exceptConversationId,
|
|
511
|
+
item: closed,
|
|
512
|
+
nowMs,
|
|
513
|
+
queue: args.queue
|
|
571
514
|
});
|
|
572
|
-
|
|
573
|
-
if (internals.defaultBotTokenProvider) return await args.task();
|
|
574
|
-
const installationId = args.installation.isEnterpriseInstall ? args.installation.enterpriseId : args.installation.teamId;
|
|
575
|
-
if (!installationId) throw new Error("Slack installation context is missing team id");
|
|
576
|
-
if (!internals.resolveTokenForTeam || !internals.requestContext) throw new Error("Slack adapter cannot resolve workspace installations");
|
|
577
|
-
const tokenContext = await internals.resolveTokenForTeam.call(args.adapter, installationId, args.installation.isEnterpriseInstall);
|
|
578
|
-
if (!tokenContext) throw new Error(`Slack installation token was not found for ${installationId}`);
|
|
579
|
-
return await internals.requestContext.run({
|
|
580
|
-
...tokenContext,
|
|
581
|
-
enterpriseId: args.installation.enterpriseId,
|
|
582
|
-
isEnterpriseInstall: args.installation.isEnterpriseInstall
|
|
583
|
-
}, () => runWithSlackInstallationToken(tokenContext.token, args.task));
|
|
515
|
+
return closed;
|
|
584
516
|
}
|
|
585
517
|
// src/chat/slack/errors.ts
|
|
586
518
|
/** Extract Slack's stable API error code from Web API errors. */
|
|
@@ -1040,10 +972,8 @@ function createSlackTurnRuntime(deps) {
|
|
|
1040
972
|
return true;
|
|
1041
973
|
};
|
|
1042
974
|
const failConversationTurn = async (args) => {
|
|
1043
|
-
const conversationId = getThreadId(args.thread, args.message) ?? getRunId(args.thread, args.message);
|
|
1044
|
-
if (!conversationId) return;
|
|
1045
975
|
await turnLifecycle.fail({
|
|
1046
|
-
conversationId,
|
|
976
|
+
conversationId: args.conversationId,
|
|
1047
977
|
createdAtMs: deps.now(),
|
|
1048
978
|
eventId: args.eventId,
|
|
1049
979
|
failureCode: args.failureCode,
|
|
@@ -1088,7 +1018,15 @@ function createSlackTurnRuntime(deps) {
|
|
|
1088
1018
|
};
|
|
1089
1019
|
const postFallbackErrorReplyWithLogging = async (args) => {
|
|
1090
1020
|
try {
|
|
1091
|
-
|
|
1021
|
+
const channelId = getChannelId(args.thread, args.message);
|
|
1022
|
+
if (!channelId) throw new Error("Slack fallback error reply requires a channel id");
|
|
1023
|
+
const threadTs = getThreadTs(getThreadId(args.thread, args.message));
|
|
1024
|
+
await sendSlackReply({
|
|
1025
|
+
channelId,
|
|
1026
|
+
conversationId: args.conversationId,
|
|
1027
|
+
text: buildTurnErrorResponse(args.error, args.eventId),
|
|
1028
|
+
...threadTs ? { threadTs } : void 0
|
|
1029
|
+
});
|
|
1092
1030
|
} catch (postError) {
|
|
1093
1031
|
logException(postError, args.postFailureEventName, {
|
|
1094
1032
|
"app.slack.reply_stage": "error_fallback_post",
|
|
@@ -1144,14 +1082,14 @@ function createSlackTurnRuntime(deps) {
|
|
|
1144
1082
|
const completedAtMs = deps.now();
|
|
1145
1083
|
logSkippedSubscribedDecision(args);
|
|
1146
1084
|
if (args.preparedState) await deps.onSubscribedMessageSkipped({
|
|
1147
|
-
|
|
1085
|
+
conversationId: args.conversationId,
|
|
1148
1086
|
message: args.message,
|
|
1149
1087
|
decision: args.decision,
|
|
1150
1088
|
preparedState: args.preparedState,
|
|
1151
1089
|
completedAtMs
|
|
1152
1090
|
});
|
|
1153
1091
|
else await deps.recordSkippedSubscribedTurn({
|
|
1154
|
-
|
|
1092
|
+
conversationId: args.conversationId,
|
|
1155
1093
|
message: args.message,
|
|
1156
1094
|
decision: args.decision,
|
|
1157
1095
|
text: args.text,
|
|
@@ -1199,7 +1137,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1199
1137
|
});
|
|
1200
1138
|
logSkippedSubscribedDecision(skipped);
|
|
1201
1139
|
await deps.recordSkippedSteeringMessage({
|
|
1202
|
-
|
|
1140
|
+
conversationId: args.conversationId,
|
|
1203
1141
|
message: skipped.message,
|
|
1204
1142
|
decision: skipped.decision,
|
|
1205
1143
|
text: skipped.text
|
|
@@ -1213,7 +1151,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1213
1151
|
while (args.pending.length > 0) {
|
|
1214
1152
|
const skipped = args.pending.shift();
|
|
1215
1153
|
await deps.recordSkippedSteeringMessage({
|
|
1216
|
-
|
|
1154
|
+
conversationId: args.conversationId,
|
|
1217
1155
|
message: skipped.message,
|
|
1218
1156
|
decision: skipped.decision,
|
|
1219
1157
|
text: skipped.text
|
|
@@ -1227,6 +1165,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1227
1165
|
};
|
|
1228
1166
|
return {
|
|
1229
1167
|
async handleNewMention(thread, message, hooks) {
|
|
1168
|
+
const conversationId = hooks.conversationId ?? thread.id;
|
|
1230
1169
|
const processingReactions = createProcessingReactionTracker(thread);
|
|
1231
1170
|
let processingReaction;
|
|
1232
1171
|
const skippedSteeringMessages = [];
|
|
@@ -1235,7 +1174,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1235
1174
|
const onTurnCompleted = async () => {
|
|
1236
1175
|
completed = true;
|
|
1237
1176
|
await flushSkippedSteeringMessagesBestEffort({
|
|
1238
|
-
|
|
1177
|
+
conversationId,
|
|
1239
1178
|
pending: skippedSteeringMessages,
|
|
1240
1179
|
context: logContext({
|
|
1241
1180
|
threadId: getThreadId(thread, message),
|
|
@@ -1289,6 +1228,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1289
1228
|
},
|
|
1290
1229
|
onSkipped: (skipped) => handleSkippedSteeringMessages({
|
|
1291
1230
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
1231
|
+
conversationId,
|
|
1292
1232
|
pending: skippedSteeringMessages,
|
|
1293
1233
|
skipped,
|
|
1294
1234
|
thread
|
|
@@ -1302,7 +1242,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1302
1242
|
await deps.executeSlackTurn(thread, message, {
|
|
1303
1243
|
explicitMention: true,
|
|
1304
1244
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
1305
|
-
conversationId
|
|
1245
|
+
conversationId,
|
|
1306
1246
|
destination: hooks.destination,
|
|
1307
1247
|
queuedMessages,
|
|
1308
1248
|
ack,
|
|
@@ -1339,17 +1279,19 @@ function createSlackTurnRuntime(deps) {
|
|
|
1339
1279
|
let lifecycleError;
|
|
1340
1280
|
try {
|
|
1341
1281
|
await failConversationTurn({
|
|
1282
|
+
conversationId,
|
|
1342
1283
|
eventId,
|
|
1343
1284
|
failureCode,
|
|
1344
|
-
message
|
|
1345
|
-
thread
|
|
1285
|
+
message
|
|
1346
1286
|
});
|
|
1347
1287
|
} catch (error) {
|
|
1348
1288
|
lifecycleError = error;
|
|
1349
1289
|
}
|
|
1350
1290
|
await hooks.beforeFirstResponsePost?.();
|
|
1351
1291
|
await postFallbackErrorReplyWithLogging({
|
|
1292
|
+
conversationId,
|
|
1352
1293
|
thread,
|
|
1294
|
+
message,
|
|
1353
1295
|
eventId,
|
|
1354
1296
|
error: failureCause,
|
|
1355
1297
|
postFailureEventName: "mention.handler.failure_reply_post.failed"
|
|
@@ -1359,7 +1301,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1359
1301
|
if (completed) await processingReactions.completeAll();
|
|
1360
1302
|
else {
|
|
1361
1303
|
await flushSkippedSteeringMessagesBestEffort({
|
|
1362
|
-
|
|
1304
|
+
conversationId,
|
|
1363
1305
|
pending: skippedSteeringMessages,
|
|
1364
1306
|
context: logContext({
|
|
1365
1307
|
threadId: getThreadId(thread, message),
|
|
@@ -1374,6 +1316,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1374
1316
|
}
|
|
1375
1317
|
},
|
|
1376
1318
|
async handleSubscribedMessage(thread, message, hooks) {
|
|
1319
|
+
const conversationId = hooks.conversationId ?? thread.id;
|
|
1377
1320
|
const processingReactions = createProcessingReactionTracker(thread);
|
|
1378
1321
|
let processingReaction;
|
|
1379
1322
|
const skippedSteeringMessages = [];
|
|
@@ -1382,7 +1325,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1382
1325
|
const onTurnCompleted = async () => {
|
|
1383
1326
|
completed = true;
|
|
1384
1327
|
await flushSkippedSteeringMessagesBestEffort({
|
|
1385
|
-
|
|
1328
|
+
conversationId,
|
|
1386
1329
|
pending: skippedSteeringMessages,
|
|
1387
1330
|
context: logContext({
|
|
1388
1331
|
threadId: getThreadId(thread, message),
|
|
@@ -1418,6 +1361,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1418
1361
|
userText: replaceTopLevelText(content, strippedUserText)
|
|
1419
1362
|
};
|
|
1420
1363
|
const threadContext = {
|
|
1364
|
+
conversationId,
|
|
1421
1365
|
threadId,
|
|
1422
1366
|
actorId,
|
|
1423
1367
|
channelId,
|
|
@@ -1450,7 +1394,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1450
1394
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost
|
|
1451
1395
|
});
|
|
1452
1396
|
await skipSubscribedMessage({
|
|
1453
|
-
|
|
1397
|
+
conversationId,
|
|
1454
1398
|
message,
|
|
1455
1399
|
decision,
|
|
1456
1400
|
context: threadContext,
|
|
@@ -1468,7 +1412,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1468
1412
|
if (preflightDecision && !preflightDecision.shouldReply) {
|
|
1469
1413
|
const reason = preflightDecision.reasonDetail ? `${preflightDecision.reason}:${preflightDecision.reasonDetail}` : preflightDecision.reason;
|
|
1470
1414
|
await skipSubscribedMessage({
|
|
1471
|
-
|
|
1415
|
+
conversationId,
|
|
1472
1416
|
message,
|
|
1473
1417
|
decision: {
|
|
1474
1418
|
shouldReply: false,
|
|
@@ -1494,7 +1438,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1494
1438
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost
|
|
1495
1439
|
});
|
|
1496
1440
|
await skipSubscribedMessage({
|
|
1497
|
-
|
|
1441
|
+
conversationId,
|
|
1498
1442
|
message,
|
|
1499
1443
|
decision,
|
|
1500
1444
|
context: threadContext,
|
|
@@ -1513,7 +1457,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1513
1457
|
queuedMessages
|
|
1514
1458
|
});
|
|
1515
1459
|
await deps.persistPreparedState({
|
|
1516
|
-
|
|
1460
|
+
conversationId,
|
|
1517
1461
|
preparedState
|
|
1518
1462
|
});
|
|
1519
1463
|
const decision = await deps.decideSubscribedReply({
|
|
@@ -1530,7 +1474,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1530
1474
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost
|
|
1531
1475
|
})) {
|
|
1532
1476
|
await skipSubscribedMessage({
|
|
1533
|
-
|
|
1477
|
+
conversationId,
|
|
1534
1478
|
message,
|
|
1535
1479
|
decision,
|
|
1536
1480
|
context: threadContext,
|
|
@@ -1542,7 +1486,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1542
1486
|
}
|
|
1543
1487
|
if (!decision.shouldReply) {
|
|
1544
1488
|
await skipSubscribedMessage({
|
|
1545
|
-
|
|
1489
|
+
conversationId,
|
|
1546
1490
|
message,
|
|
1547
1491
|
decision,
|
|
1548
1492
|
context: threadContext,
|
|
@@ -1561,6 +1505,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1561
1505
|
},
|
|
1562
1506
|
onSkipped: (skipped) => handleSkippedSteeringMessages({
|
|
1563
1507
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
1508
|
+
conversationId,
|
|
1564
1509
|
pending: skippedSteeringMessages,
|
|
1565
1510
|
skipped,
|
|
1566
1511
|
thread
|
|
@@ -1586,7 +1531,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1586
1531
|
const toolInvocationHook = createToolInvocationHook(processingReaction, hooks);
|
|
1587
1532
|
await deps.executeSlackTurn(thread, message, {
|
|
1588
1533
|
explicitMention: Boolean(message.isMention),
|
|
1589
|
-
conversationId
|
|
1534
|
+
conversationId,
|
|
1590
1535
|
destination: hooks.destination,
|
|
1591
1536
|
preparedState,
|
|
1592
1537
|
beforeFirstResponsePost: hooks.beforeFirstResponsePost,
|
|
@@ -1625,17 +1570,19 @@ function createSlackTurnRuntime(deps) {
|
|
|
1625
1570
|
let lifecycleError;
|
|
1626
1571
|
try {
|
|
1627
1572
|
await failConversationTurn({
|
|
1573
|
+
conversationId,
|
|
1628
1574
|
eventId,
|
|
1629
1575
|
failureCode,
|
|
1630
|
-
message
|
|
1631
|
-
thread
|
|
1576
|
+
message
|
|
1632
1577
|
});
|
|
1633
1578
|
} catch (error) {
|
|
1634
1579
|
lifecycleError = error;
|
|
1635
1580
|
}
|
|
1636
1581
|
await hooks.beforeFirstResponsePost?.();
|
|
1637
1582
|
await postFallbackErrorReplyWithLogging({
|
|
1583
|
+
conversationId,
|
|
1638
1584
|
thread,
|
|
1585
|
+
message,
|
|
1639
1586
|
eventId,
|
|
1640
1587
|
error: failureCause,
|
|
1641
1588
|
postFailureEventName: "subscribed_message.handler.failure_reply_post.failed"
|
|
@@ -1645,7 +1592,7 @@ function createSlackTurnRuntime(deps) {
|
|
|
1645
1592
|
if (completed) await processingReactions.completeAll();
|
|
1646
1593
|
else {
|
|
1647
1594
|
await flushSkippedSteeringMessagesBestEffort({
|
|
1648
|
-
|
|
1595
|
+
conversationId,
|
|
1649
1596
|
pending: skippedSteeringMessages,
|
|
1650
1597
|
context: logContext({
|
|
1651
1598
|
threadId: getThreadId(thread, message),
|
|
@@ -3592,292 +3539,6 @@ function readSlackActionToken(message) {
|
|
|
3592
3539
|
const token = slackActionTokenSchema.safeParse(envelope.data.raw?.action_token);
|
|
3593
3540
|
return token.success ? token.data : void 0;
|
|
3594
3541
|
}
|
|
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
3542
|
// src/chat/slack/assistant-thread/status-render.ts
|
|
3882
3543
|
const DEFAULT_STATUS_CONTEXTS = {
|
|
3883
3544
|
thinking: "…",
|
|
@@ -4379,8 +4040,8 @@ async function saveSteeringMessages(args) {
|
|
|
4379
4040
|
* Persist post-delivery Redis scratch with a short retry after durable SQL
|
|
4380
4041
|
* completion has succeeded.
|
|
4381
4042
|
*/
|
|
4382
|
-
async function persistThreadRuntimeStateWithRetry(
|
|
4383
|
-
await persistWithRetry(() => persistThreadRuntimeState(
|
|
4043
|
+
async function persistThreadRuntimeStateWithRetry(conversationId, patch) {
|
|
4044
|
+
await persistWithRetry(() => persistThreadRuntimeState(conversationId, patch));
|
|
4384
4045
|
}
|
|
4385
4046
|
/**
|
|
4386
4047
|
* Resolve the Pi history for this Slack turn from the most precise durable
|
|
@@ -4432,6 +4093,10 @@ function createSlackTurn(deps) {
|
|
|
4432
4093
|
visibility: destinationVisibility
|
|
4433
4094
|
}) ?? "private"
|
|
4434
4095
|
});
|
|
4096
|
+
const replyTarget = resolveSlackReplyTarget({
|
|
4097
|
+
destination,
|
|
4098
|
+
source
|
|
4099
|
+
});
|
|
4435
4100
|
const slackActionToken = readSlackActionToken(message);
|
|
4436
4101
|
const runId = options.execution?.dispatch?.id ?? getRunId(thread, message);
|
|
4437
4102
|
const conversationId = options.conversationId ?? threadId ?? runId;
|
|
@@ -4486,6 +4151,7 @@ function createSlackTurn(deps) {
|
|
|
4486
4151
|
destination,
|
|
4487
4152
|
locationConfiguration: options.execution?.locationConfiguration,
|
|
4488
4153
|
context: {
|
|
4154
|
+
conversationId,
|
|
4489
4155
|
threadId,
|
|
4490
4156
|
actorId: slackActorId,
|
|
4491
4157
|
channelId,
|
|
@@ -4503,17 +4169,16 @@ function createSlackTurn(deps) {
|
|
|
4503
4169
|
};
|
|
4504
4170
|
const postAuthPauseNotice = async (providerDisplayName, requestText) => {
|
|
4505
4171
|
if (!actor) throw new Error("Slack auth pause notice requires an actor");
|
|
4172
|
+
if (!postsRunNotices(options.execution?.dispatch)) return;
|
|
4506
4173
|
const text = buildAuthPauseResponse(actor.userId, providerDisplayName, requestText);
|
|
4507
4174
|
try {
|
|
4508
4175
|
await beforeFirstResponsePost();
|
|
4509
|
-
|
|
4510
|
-
|
|
4176
|
+
await sendSlackReply({
|
|
4177
|
+
...replyTarget,
|
|
4511
4178
|
conversationId,
|
|
4512
4179
|
replyAttribution: options.execution?.dispatch?.replyAttribution,
|
|
4513
|
-
text
|
|
4514
|
-
threadTs
|
|
4180
|
+
text
|
|
4515
4181
|
});
|
|
4516
|
-
else await thread.post(buildSlackOutputMessage(text));
|
|
4517
4182
|
} catch (error) {
|
|
4518
4183
|
logException(error, "slack.auth_pause.notice_post.failed", {
|
|
4519
4184
|
"app.slack.reply_stage": "thread_reply_auth_pause_notice",
|
|
@@ -4567,7 +4232,7 @@ function createSlackTurn(deps) {
|
|
|
4567
4232
|
const deliveredMessage = [...preparedState.conversation.messages].reverse().find((candidate) => candidate.role === "assistant" && candidate.id.startsWith(`${turnId}:assistant:`) && candidate.meta?.replied === true);
|
|
4568
4233
|
options.onTurnDeliveryAccepted?.(deliveredMessage?.meta?.slackTs);
|
|
4569
4234
|
options.onTurnOutcome?.({ outcome: "completed" });
|
|
4570
|
-
await
|
|
4235
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4571
4236
|
await options.onTurnStatePersisted?.();
|
|
4572
4237
|
await options.ack?.();
|
|
4573
4238
|
await options.onTurnCompleted?.();
|
|
@@ -4590,7 +4255,7 @@ function createSlackTurn(deps) {
|
|
|
4590
4255
|
});
|
|
4591
4256
|
throw error;
|
|
4592
4257
|
}
|
|
4593
|
-
await
|
|
4258
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4594
4259
|
await options.onTurnStatePersisted?.();
|
|
4595
4260
|
await options.ack?.();
|
|
4596
4261
|
return;
|
|
@@ -4628,7 +4293,11 @@ function createSlackTurn(deps) {
|
|
|
4628
4293
|
});
|
|
4629
4294
|
if (configReply) {
|
|
4630
4295
|
await beforeFirstResponsePost();
|
|
4631
|
-
await
|
|
4296
|
+
await sendSlackReply({
|
|
4297
|
+
...replyTarget,
|
|
4298
|
+
conversationId,
|
|
4299
|
+
text: configReply.text
|
|
4300
|
+
});
|
|
4632
4301
|
markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
|
|
4633
4302
|
replied: true,
|
|
4634
4303
|
skippedReason: void 0
|
|
@@ -4647,7 +4316,7 @@ function createSlackTurn(deps) {
|
|
|
4647
4316
|
source: "slack"
|
|
4648
4317
|
}
|
|
4649
4318
|
});
|
|
4650
|
-
await
|
|
4319
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4651
4320
|
await options.onTurnStatePersisted?.();
|
|
4652
4321
|
await options.ack?.();
|
|
4653
4322
|
return;
|
|
@@ -4688,7 +4357,7 @@ function createSlackTurn(deps) {
|
|
|
4688
4357
|
"app.message.id": message.id,
|
|
4689
4358
|
...messageTs ? { "messaging.message.id": messageTs } : void 0
|
|
4690
4359
|
});
|
|
4691
|
-
await
|
|
4360
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4692
4361
|
await options.onTurnStatePersisted?.();
|
|
4693
4362
|
if (actor) setSentryUser({
|
|
4694
4363
|
id: actor.userId,
|
|
@@ -4775,16 +4444,15 @@ function createSlackTurn(deps) {
|
|
|
4775
4444
|
}
|
|
4776
4445
|
else {
|
|
4777
4446
|
slackMessageTs = await sendSlackReply({
|
|
4447
|
+
...replyTarget,
|
|
4778
4448
|
cards,
|
|
4779
|
-
channelId: destination.channelId,
|
|
4780
4449
|
conversationId,
|
|
4781
4450
|
replyAttribution: options.execution?.dispatch?.replyAttribution,
|
|
4782
|
-
text
|
|
4783
|
-
...threadTs ? { threadTs } : void 0
|
|
4451
|
+
text
|
|
4784
4452
|
});
|
|
4785
4453
|
messageDestinations.push(...slackMessageTs.map((messageId) => ({
|
|
4786
4454
|
destination,
|
|
4787
|
-
messageId: threadTs ?? messageId
|
|
4455
|
+
messageId: replyTarget.threadTs ?? messageId
|
|
4788
4456
|
})));
|
|
4789
4457
|
}
|
|
4790
4458
|
} catch (error) {
|
|
@@ -4874,7 +4542,7 @@ function createSlackTurn(deps) {
|
|
|
4874
4542
|
});
|
|
4875
4543
|
if (compaction.compacted) {
|
|
4876
4544
|
piMessages = compaction.piMessages;
|
|
4877
|
-
await
|
|
4545
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4878
4546
|
}
|
|
4879
4547
|
}
|
|
4880
4548
|
const hasDurablePromptHistory = Boolean(piMessages?.length);
|
|
@@ -4983,11 +4651,11 @@ function createSlackTurn(deps) {
|
|
|
4983
4651
|
drainSteeringMessages,
|
|
4984
4652
|
shouldYield: options.shouldYield,
|
|
4985
4653
|
onSandboxRefChanged: async (sandboxRef) => {
|
|
4986
|
-
await
|
|
4654
|
+
await persistThreadStateById(conversationId, { sandboxRef });
|
|
4987
4655
|
},
|
|
4988
4656
|
recordPendingAuth: async (pendingAuth) => {
|
|
4989
4657
|
preparedState.conversation.processing.pendingAuth = pendingAuth;
|
|
4990
|
-
await
|
|
4658
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
4991
4659
|
}
|
|
4992
4660
|
}
|
|
4993
4661
|
};
|
|
@@ -5066,7 +4734,7 @@ function createSlackTurn(deps) {
|
|
|
5066
4734
|
conversation: completedState.conversation,
|
|
5067
4735
|
conversationId
|
|
5068
4736
|
}));
|
|
5069
|
-
await persistThreadRuntimeStateWithRetry(
|
|
4737
|
+
await persistThreadRuntimeStateWithRetry(conversationId, completedState);
|
|
5070
4738
|
const automatedTurnLimit = await recordFinishedTurnForAutomatedLimit({
|
|
5071
4739
|
conversationId,
|
|
5072
4740
|
maxTurns: botConfig.maxConsecutiveAutomatedTurns,
|
|
@@ -5075,7 +4743,7 @@ function createSlackTurn(deps) {
|
|
|
5075
4743
|
await maybePostAutomatedTurnLimitNotice({
|
|
5076
4744
|
conversationId,
|
|
5077
4745
|
maxTurns: botConfig.maxConsecutiveAutomatedTurns,
|
|
5078
|
-
|
|
4746
|
+
source,
|
|
5079
4747
|
update: automatedTurnLimit
|
|
5080
4748
|
});
|
|
5081
4749
|
} catch (saveError) {
|
|
@@ -5111,7 +4779,7 @@ function createSlackTurn(deps) {
|
|
|
5111
4779
|
nowMs: Date.now(),
|
|
5112
4780
|
sessionId: turnId
|
|
5113
4781
|
});
|
|
5114
|
-
await
|
|
4782
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5115
4783
|
if (conversationId) await turnLifecycle.fail({
|
|
5116
4784
|
conversationId,
|
|
5117
4785
|
createdAtMs: Date.now(),
|
|
@@ -5128,7 +4796,7 @@ function createSlackTurn(deps) {
|
|
|
5128
4796
|
conversation: preparedState.conversation,
|
|
5129
4797
|
sessionId: turnId
|
|
5130
4798
|
});
|
|
5131
|
-
await
|
|
4799
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5132
4800
|
persistedAtLeastOnce = true;
|
|
5133
4801
|
shouldPersistFailureState = false;
|
|
5134
4802
|
return;
|
|
@@ -5212,7 +4880,7 @@ function createSlackTurn(deps) {
|
|
|
5212
4880
|
nowMs: Date.now(),
|
|
5213
4881
|
sessionId: turnId
|
|
5214
4882
|
});
|
|
5215
|
-
await
|
|
4883
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5216
4884
|
await turnLifecycle.complete({
|
|
5217
4885
|
conversationId,
|
|
5218
4886
|
createdAtMs: Date.now(),
|
|
@@ -5269,7 +4937,7 @@ function createSlackTurn(deps) {
|
|
|
5269
4937
|
} catch (recordError) {
|
|
5270
4938
|
logException(recordError, "agent.turn.failed_session_record_persist.failed");
|
|
5271
4939
|
}
|
|
5272
|
-
await
|
|
4940
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5273
4941
|
if (shouldEmitDevAgentTrace()) logWarn("agent.turn.failed");
|
|
5274
4942
|
}
|
|
5275
4943
|
await status.clear();
|
|
@@ -5408,10 +5076,10 @@ async function seedConversationBackfill(thread, conversation, currentTurn, resol
|
|
|
5408
5076
|
/** Build the turn-state preparer from injected conversation services. */
|
|
5409
5077
|
function createPrepareTurnState(deps) {
|
|
5410
5078
|
return async function prepareTurnState(args) {
|
|
5411
|
-
const
|
|
5079
|
+
const conversationId = args.context.conversationId ?? args.context.threadId ?? args.context.runId;
|
|
5080
|
+
const existingState = conversationId ? await getPersistedThreadState(conversationId) : await args.thread.state;
|
|
5412
5081
|
const sandboxRef = getPersistedSandboxState(existingState ?? {});
|
|
5413
5082
|
const conversation = coerceThreadConversationState(existingState);
|
|
5414
|
-
const conversationId = args.context.threadId ?? args.context.runId;
|
|
5415
5083
|
if (conversationId && isVisionEnabled()) conversation.vision = await loadConversationVisionCache(conversationId);
|
|
5416
5084
|
await hydrateConversationMessages({
|
|
5417
5085
|
conversation,
|
|
@@ -5596,8 +5264,8 @@ function createSlackRuntime(options) {
|
|
|
5596
5264
|
modelId: defaultModelId(botConfig),
|
|
5597
5265
|
now: options.now ?? (() => Date.now()),
|
|
5598
5266
|
prepareTurnState,
|
|
5599
|
-
persistPreparedState: async ({
|
|
5600
|
-
await
|
|
5267
|
+
persistPreparedState: async ({ conversationId, preparedState }) => {
|
|
5268
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5601
5269
|
},
|
|
5602
5270
|
getPreparedConversationContext: (preparedState) => preparedState.conversationContext,
|
|
5603
5271
|
decideSubscribedReply: async (args) => {
|
|
@@ -5612,18 +5280,18 @@ function createSlackRuntime(options) {
|
|
|
5612
5280
|
});
|
|
5613
5281
|
return decision;
|
|
5614
5282
|
},
|
|
5615
|
-
recordSkippedSteeringMessage: async ({
|
|
5616
|
-
const conversation = coerceThreadConversationState(await
|
|
5283
|
+
recordSkippedSteeringMessage: async ({ conversationId, message, decision, text }) => {
|
|
5284
|
+
const conversation = coerceThreadConversationState(await getPersistedThreadState(conversationId));
|
|
5617
5285
|
recordSkippedConversationMessage({
|
|
5618
5286
|
conversation,
|
|
5619
5287
|
message,
|
|
5620
5288
|
skippedReason: decision.reason,
|
|
5621
5289
|
text: text.userText
|
|
5622
5290
|
});
|
|
5623
|
-
await
|
|
5291
|
+
await persistThreadStateById(conversationId, { conversation });
|
|
5624
5292
|
},
|
|
5625
|
-
recordSkippedSubscribedTurn: async ({
|
|
5626
|
-
const conversation = coerceThreadConversationState(await
|
|
5293
|
+
recordSkippedSubscribedTurn: async ({ conversationId, message, decision, completedAtMs, text }) => {
|
|
5294
|
+
const conversation = coerceThreadConversationState(await getPersistedThreadState(conversationId));
|
|
5627
5295
|
recordSkippedConversationMessage({
|
|
5628
5296
|
conversation,
|
|
5629
5297
|
message,
|
|
@@ -5632,16 +5300,16 @@ function createSlackRuntime(options) {
|
|
|
5632
5300
|
});
|
|
5633
5301
|
clearSkippedTurnIfActive(conversation, message.id);
|
|
5634
5302
|
conversation.processing.lastCompletedAtMs = completedAtMs;
|
|
5635
|
-
await
|
|
5303
|
+
await persistThreadStateById(conversationId, { conversation });
|
|
5636
5304
|
},
|
|
5637
|
-
onSubscribedMessageSkipped: async ({
|
|
5305
|
+
onSubscribedMessageSkipped: async ({ conversationId, message, preparedState, decision, completedAtMs }) => {
|
|
5638
5306
|
markConversationMessage(preparedState.conversation, preparedState.userMessageId, {
|
|
5639
5307
|
replied: false,
|
|
5640
5308
|
skippedReason: decision.reason
|
|
5641
5309
|
});
|
|
5642
5310
|
clearSkippedTurnIfActive(preparedState.conversation, message.id);
|
|
5643
5311
|
preparedState.conversation.processing.lastCompletedAtMs = completedAtMs;
|
|
5644
|
-
await
|
|
5312
|
+
await persistThreadStateById(conversationId, { conversation: preparedState.conversation });
|
|
5645
5313
|
},
|
|
5646
5314
|
executeSlackTurn,
|
|
5647
5315
|
initializeAssistantThread: async ({ channelId, threadTs, sourceChannelId }) => {
|
|
@@ -5688,23 +5356,20 @@ function createJuniorSlackAdapter(config) {
|
|
|
5688
5356
|
* authorization pause notices use `sendSlackReply` so the footer stays
|
|
5689
5357
|
* consistent.
|
|
5690
5358
|
*/
|
|
5691
|
-
|
|
5359
|
+
/** Post a failure reply or status notice when the run's dispatch allows it. */
|
|
5360
|
+
async function postRunNotice(resume, text) {
|
|
5361
|
+
if (!postsRunNotices(resume.run?.dispatch)) return;
|
|
5362
|
+
await sendSlackReply({
|
|
5363
|
+
channelId: resume.channelId,
|
|
5364
|
+
conversationId: resume.conversationId,
|
|
5365
|
+
replyAttribution: resume.run?.dispatch?.replyAttribution,
|
|
5366
|
+
text,
|
|
5367
|
+
threadTs: resume.threadTs
|
|
5368
|
+
});
|
|
5369
|
+
}
|
|
5370
|
+
async function postRunNoticeBestEffort(resume, text) {
|
|
5692
5371
|
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
|
-
});
|
|
5372
|
+
await postRunNotice(resume, text);
|
|
5708
5373
|
} catch {}
|
|
5709
5374
|
}
|
|
5710
5375
|
/** Create a read-only configuration service from persisted values. */
|
|
@@ -5745,13 +5410,6 @@ function getResumeLogContext(args) {
|
|
|
5745
5410
|
modelId: defaultModelId(botConfig)
|
|
5746
5411
|
};
|
|
5747
5412
|
}
|
|
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
5413
|
async function handleResumeFailure(args) {
|
|
5756
5414
|
const turnLifecycle = getTurnLifecycle();
|
|
5757
5415
|
const capturedEventId = logException(args.error, args.eventName);
|
|
@@ -5775,12 +5433,7 @@ async function handleResumeFailure(args) {
|
|
|
5775
5433
|
failureStatePersistError = persistError;
|
|
5776
5434
|
}
|
|
5777
5435
|
try {
|
|
5778
|
-
await
|
|
5779
|
-
channelId: args.resume.channelId,
|
|
5780
|
-
threadTs: args.resume.threadTs,
|
|
5781
|
-
eventId,
|
|
5782
|
-
error: args.error
|
|
5783
|
-
});
|
|
5436
|
+
await postRunNotice(args.resume, buildTurnErrorResponse(args.error, eventId));
|
|
5784
5437
|
} catch (deliveryError) {
|
|
5785
5438
|
const deliveryEventId = logException(deliveryError, "slack.resume.failure_delivery.failed", { "app.error.original_event_id": eventId });
|
|
5786
5439
|
try {
|
|
@@ -5899,7 +5552,7 @@ async function resumeSlackTurnInContext(args) {
|
|
|
5899
5552
|
channelId: runArgs.channelId,
|
|
5900
5553
|
timestamp: runArgs.messageTs
|
|
5901
5554
|
});
|
|
5902
|
-
if (runArgs.initialText) await
|
|
5555
|
+
if (runArgs.initialText) await postRunNoticeBestEffort(runArgs, runArgs.initialText);
|
|
5903
5556
|
status.update(runArgs.initialStatus);
|
|
5904
5557
|
const conversationId = runArgs.conversationId;
|
|
5905
5558
|
const visibleConversationId = args.conversationId;
|
|
@@ -6154,7 +5807,7 @@ async function resumeSlackTurnInContext(args) {
|
|
|
6154
5807
|
}
|
|
6155
5808
|
if (deferredPauseHandler) try {
|
|
6156
5809
|
await deferredPauseHandler();
|
|
6157
|
-
if (deferredAuthInfo) await
|
|
5810
|
+
if (deferredAuthInfo) await postRunNoticeBestEffort(runArgs, buildAuthPauseResponse(deferredAuthInfo.actorId, deferredAuthInfo.providerDisplayName, deferredAuthInfo.requestText));
|
|
6158
5811
|
return true;
|
|
6159
5812
|
} catch (pauseError) {
|
|
6160
5813
|
await handleResumeFailure({
|
|
@@ -6347,16 +6000,22 @@ async function runPausedTurn(payload, options, runOptions = {}) {
|
|
|
6347
6000
|
return withLogContext({ conversationId: payload.conversationId }, () => runPausedTurnInContext(payload, options, runOptions));
|
|
6348
6001
|
}
|
|
6349
6002
|
async function runPausedTurnInContext(payload, options, runOptions) {
|
|
6350
|
-
const thread = parseSlackThreadId(payload.conversationId);
|
|
6351
6003
|
const destination = requireSlackDestination(payload.destination, "Paused turn");
|
|
6352
6004
|
const wakePausedTurn$1 = options.wakePausedTurn ?? wakePausedTurn;
|
|
6005
|
+
const routing = await resolveConversationRouting({ conversationId: payload.conversationId });
|
|
6006
|
+
const pausedTurn = await getTurnRecord(payload.conversationId, payload.turnId);
|
|
6007
|
+
const target = resolveSlackReplyTarget({
|
|
6008
|
+
destination,
|
|
6009
|
+
location: routing?.location,
|
|
6010
|
+
source: pausedTurn?.source
|
|
6011
|
+
});
|
|
6353
6012
|
return await resumeSlackTurn({
|
|
6354
6013
|
messageText: "",
|
|
6355
6014
|
conversationId: payload.conversationId,
|
|
6356
6015
|
turnId: payload.turnId,
|
|
6357
6016
|
traceContext: payload.traceContext,
|
|
6358
|
-
channelId:
|
|
6359
|
-
...
|
|
6017
|
+
channelId: target.channelId,
|
|
6018
|
+
...target.threadTs ? { threadTs: target.threadTs } : void 0,
|
|
6360
6019
|
executeTurn: async (run, saveResult) => await executeTurn(options.agentRunner, run, saveResult),
|
|
6361
6020
|
scheduleSessionCompletedPluginTasks: options.scheduleSessionCompletedPluginTasks,
|
|
6362
6021
|
beforeStart: async () => {
|
|
@@ -6569,6 +6228,23 @@ async function failStrandedTurnWithFallback(args) {
|
|
|
6569
6228
|
failureCode: "agent_run_failed",
|
|
6570
6229
|
turnId: failed.turnId
|
|
6571
6230
|
});
|
|
6231
|
+
let replyAttribution;
|
|
6232
|
+
if (failed.dispatchId) {
|
|
6233
|
+
const dispatch = await getDispatchRecord(failed.dispatchId);
|
|
6234
|
+
await recordTurnSummary({
|
|
6235
|
+
conversationId: args.conversationId,
|
|
6236
|
+
destination: dispatch?.destination,
|
|
6237
|
+
dispatchId: failed.dispatchId,
|
|
6238
|
+
dispatchOutcome: "failed",
|
|
6239
|
+
turnId: failed.turnId,
|
|
6240
|
+
sliceId: failed.sliceId,
|
|
6241
|
+
source: dispatch?.source,
|
|
6242
|
+
state: "failed",
|
|
6243
|
+
surface: failed.surface
|
|
6244
|
+
});
|
|
6245
|
+
if (!dispatch || !postsRunNotices(buildDispatchRoutingContext(dispatch).dispatch)) return true;
|
|
6246
|
+
replyAttribution = dispatch.replyAttribution;
|
|
6247
|
+
}
|
|
6572
6248
|
let routing;
|
|
6573
6249
|
try {
|
|
6574
6250
|
routing = await resolveTurnSessionRouting({ conversationId: args.conversationId });
|
|
@@ -6579,22 +6255,15 @@ async function failStrandedTurnWithFallback(args) {
|
|
|
6579
6255
|
});
|
|
6580
6256
|
return true;
|
|
6581
6257
|
}
|
|
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
|
|
6258
|
+
const target = resolveSlackReplyTarget({
|
|
6259
|
+
destination: requireSlackDestination(routing.destination, "Stranded paused turn"),
|
|
6260
|
+
location: routing.location,
|
|
6261
|
+
source: failed.source
|
|
6592
6262
|
});
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
...thread?.threadTs ? { threadTs: thread.threadTs } : void 0,
|
|
6263
|
+
await sendSlackReply({
|
|
6264
|
+
...target,
|
|
6265
|
+
conversationId: args.conversationId,
|
|
6266
|
+
replyAttribution,
|
|
6598
6267
|
text: buildTurnFailureResponse(requireTurnFailureEventId(eventId, eventName))
|
|
6599
6268
|
});
|
|
6600
6269
|
return true;
|
|
@@ -6797,28 +6466,9 @@ function createWebSteeringDrain(args) {
|
|
|
6797
6466
|
return accepted;
|
|
6798
6467
|
};
|
|
6799
6468
|
}
|
|
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
6469
|
// src/chat/task-execution/conversation-turn.ts
|
|
6820
6470
|
/** Run a mailbox Turn and store its assistant Messages. */
|
|
6821
|
-
function stableHex
|
|
6471
|
+
function stableHex(...parts) {
|
|
6822
6472
|
return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24);
|
|
6823
6473
|
}
|
|
6824
6474
|
function captureConversationTurnFailure(args) {
|
|
@@ -7097,7 +6747,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
|
7097
6747
|
if (stopSignal?.aborted) return await completeCancelledTurn();
|
|
7098
6748
|
const piMessages = await loadProjection({ conversationId: context.conversationId });
|
|
7099
6749
|
failureCode = "agent_run_failed";
|
|
7100
|
-
currentRunId = `conversation-run:${stableHex
|
|
6750
|
+
currentRunId = `conversation-run:${stableHex(turnId, String(startedAtMs))}`;
|
|
7101
6751
|
setTags({ runId: currentRunId });
|
|
7102
6752
|
const outcome = await executeTurn(agentRunner, {
|
|
7103
6753
|
conversationId: context.conversationId,
|
|
@@ -7195,7 +6845,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
|
7195
6845
|
await maybePostAutomatedTurnLimitNotice({
|
|
7196
6846
|
conversationId: context.conversationId,
|
|
7197
6847
|
maxTurns: botConfig.maxConsecutiveAutomatedTurns,
|
|
7198
|
-
|
|
6848
|
+
source,
|
|
7199
6849
|
update: automatedTurnLimit
|
|
7200
6850
|
});
|
|
7201
6851
|
completedSuccessfully = reply.diagnostics.outcome === "success";
|
|
@@ -7307,11 +6957,13 @@ function createSlackSystemTurnDelivery(args) {
|
|
|
7307
6957
|
installation: { teamId: location.teamId },
|
|
7308
6958
|
state: args.state,
|
|
7309
6959
|
task: async () => await sendSlackReply({
|
|
6960
|
+
...resolveSlackReplyTarget({
|
|
6961
|
+
destination: location,
|
|
6962
|
+
location
|
|
6963
|
+
}),
|
|
7310
6964
|
cards,
|
|
7311
|
-
channelId: location.channelId,
|
|
7312
6965
|
conversationId,
|
|
7313
|
-
text
|
|
7314
|
-
...location.threadTs ? { threadTs: location.threadTs } : void 0
|
|
6966
|
+
text
|
|
7315
6967
|
})
|
|
7316
6968
|
});
|
|
7317
6969
|
} catch (error) {
|
|
@@ -7471,167 +7123,6 @@ function createProductionConversationWorkOptions(options) {
|
|
|
7471
7123
|
waitUntil: options.waitUntil
|
|
7472
7124
|
});
|
|
7473
7125
|
}
|
|
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
7126
|
// src/chat/slack/location-url.ts
|
|
7636
7127
|
/** Parse a Slack workspace subdomain used in `https://{domain}.slack.com` URLs. */
|
|
7637
7128
|
function parseSlackTeamDomain(value) {
|
|
@@ -8784,4 +8275,4 @@ async function readConversationFeedFromSql(options = {}) {
|
|
|
8784
8275
|
async function readConversationFeed(options = {}) {
|
|
8785
8276
|
return conversationFeedSchema.parse(await readConversationFeedFromSql(options));
|
|
8786
8277
|
}
|
|
8787
|
-
export {
|
|
8278
|
+
export { AGENT_DISPATCH_MAX_AGE_MS as $, assertNoLegacyAllowedDomains as A, normalizeIncomingSlackThreadId as B, readConversationSourceTask as C, readViewerAutomationVersions as D, readViewerAutomationSummary as E, recordSkippedConversationMessage as F, parseContent as G, stopConversationTurn as H, buildSlackInboundMessage as I, canActOnReviewItem as J, ReviewItemActionError as K, clearSlackPendingReactions as L, isAllowedSignInEmail as M, readEnvList as N, readViewerAutomations as O, resolveAllowedDomains as P, getAttachmentStorage as Q, isSlackMessageStopped as R, deleteViewerTask as S, readViewerAutomationRuns as T, THREAD_OPTOUT_ACK as U, withNormalizedThreadId as V, startProcessingReaction as W, readReviewItemForUser as X, isOnlyAssignee as Y, reviewItemChange as Z, createProductionConversationWorkOptions as _, resolveMentions as a, createSlackWebApiStatusSender as b, readConversationAuxiliaryCostsFromSql as c, notCopiedByFork as d, enqueueAgentDispatch as et, childAgentTotalsReport as f, resolveSlackTeamDomains as g, conversationSummaryFromStoredConversation as h, attachConversationMentions as i, isAllowedDomain as j, resolveViewerTaskCandidate as k, ConversationForkError as l, conversationEventHistory as m, readConversationRecordFromSql as n, readHealthReport as nt, readFirstHumanMessageTexts as o, readConversationTreeMetricsFromSql as p, actOnReviewItem as q, readConversationParticipants as r, listConversationSidebarAnnotations as s, readConversationFeed as t, GET as tt, forkConversation as u, createProductionSlackWebhookServices as v, readViewerAutomationExecutions as w, ViewerTaskNotFoundError as x, getProductionSlackAdapter as y, stopSlackThread as z };
|