@sentry/junior 0.268.0 → 0.269.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -9
- package/dist/{adapter-04AcD2SO.js → adapter-2-CG25be.js} +3 -49
- package/dist/{admin-DIU1QyG3.js → admin-DSs8S5Uy.js} +1 -1
- package/dist/{agent-runner-CAYfFmU7.js → agent-runner-CrDrPUtY.js} +3208 -2826
- package/dist/api/schema.d.ts +2 -3
- package/dist/api/schema.js +7 -7
- package/dist/api.d.ts +3 -1
- package/dist/api.js +157 -298
- package/dist/{app-z6Uuyq3j.d.ts → app-BrMMzjYm.d.ts} +8 -2
- package/dist/app.d.ts +1 -1
- package/dist/app.js +121 -207
- package/dist/{automation-CmeFBSEh.js → automation-p87hg_AV.js} +74 -74
- package/dist/{briefs-DQbV2jNr.js → briefs-PtODdLNu.js} +4 -4
- package/dist/{catalog-runtime-BD9GSpc6.js → catalog-runtime-BsoSmTM0.js} +1 -1
- package/dist/cli/chat.js +16 -26
- package/dist/cli/check.js +3 -3
- package/dist/cli/main.js +5 -3
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +4 -4
- package/dist/cli/upgrade.d.ts +1 -1
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-C1yDYjoS.js → client-Cqf4TboL.js} +20 -15
- package/dist/{config-DrTeF80T.js → config-CPsE4T5L.js} +2 -2
- package/dist/{connections-D46f8vvi.js → connections-upFXLmhG.js} +10 -10
- package/dist/{conversation-DSo310Ps.js → conversation-CTpD_PGk.js} +7 -84
- package/dist/{conversation-privacy-CxlOCiUo.js → conversation-privacy-D6zJc8ff.js} +1 -1
- package/dist/{credential-sync-sm9fegLY.js → credential-sync-CS_6IC-f.js} +15 -14
- package/dist/{credentialed-lukuD-cc.js → credentialed-BU-OW7HP.js} +548 -71
- package/dist/{store-xMuuXRE0.d.ts → db-BzOCzVb1.d.ts} +4 -0
- package/dist/{executor-4vhvsMZJ.js → executor-CNS7ONt-.js} +6 -6
- package/dist/instrumentation.js +2 -2
- package/dist/{interruption-marker-CkncDT8d.js → interruption-marker-g6Xup3Fk.js} +1 -1
- package/dist/{job-queue-In9QKz-I.js → job-queue-DI-k-Bbk.js} +2 -2
- package/dist/{list-BkgnXEEB.js → list-CGoQdXTp.js} +193 -656
- package/dist/locks-D-0h111u.js +48 -0
- package/dist/{logging-DLSpkSSr.js → logging-n7knse-O.js} +1 -1
- package/dist/{history-C84mlAwO.js → media-85PDJxO_.js} +83 -1
- package/dist/{message-BObyg59s.js → message-BpaWd8TT.js} +4 -4
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-callback-server-B-WbSdoH.js → oauth-callback-server-CpGmEw_i.js} +2 -2
- package/dist/{prompt-BW9jFsqZ.js → prompt-BlQB0QQt.js} +1 -1
- package/dist/{registry-BgecKva0.js → registry-By36_xsm.js} +1 -1
- package/dist/reporting-schema-CZREhYc-.js +104 -0
- package/dist/{web-message-id-DlHNANtd.js → reporting-window-CVarfId9.js} +130 -7
- package/dist/{resolve-Cpa4Bu1O.js → resolve-CUIBcdPd.js} +3 -3
- package/dist/{result-D3TSmZZ8.js → result-BemsOMOO.js} +30 -11
- package/dist/{runner-DS5OKhEp.js → runner-BVok7PAA.js} +12 -11
- package/dist/{sandbox-egress-signals-CnDjY5rx.js → sandbox-egress-signals-BSUWQI95.js} +1 -1
- package/dist/{schema-0050HFnB.js → schema-ChnFo5f2.js} +36 -20
- package/dist/{schema-ZngqcOWM.d.ts → schema-jK4-1u_H.d.ts} +126 -12
- package/dist/{service-account-CKV2WTY7.js → service-account-DHvlLnZw.js} +4 -3
- package/dist/{service-account-DGjnDw9y.js → service-account-hMmESDcr.js} +3 -3
- package/dist/{session-stop-Dam1EI7B.js → session-stop-BQW_iFcd.js} +15 -15
- package/dist/{skills-CfAnfImD.js → skills-DmJJIuLF.js} +2 -2
- package/dist/{spawn-cmq9p4Ly.js → spawn-ChCm46i-.js} +2 -2
- package/dist/{store-C5PN02QO.js → store-1DVhoKOL.js} +3 -3
- package/dist/{store-zPEoJPBB.js → store-DCo2n5iJ.js} +3 -3
- package/dist/{task-queue-_mf28dSu.js → task-queue-HMC4QjFm.js} +3 -3
- package/dist/task-runner-cA6yjn_E.js +269 -0
- package/dist/{thread-state-BbbPh0Sn.js → thread-state-CHy8LS0C.js} +5 -5
- package/dist/{turn-failure-response-BKKmnMB5.js → turn-failure-response-Dr7erLWs.js} +9 -9
- package/dist/{turn-lifecycle-oP0kVeGz.js → turn-lifecycle-bf2fIYi_.js} +1 -1
- package/dist/{tasks-SB9z-VTU.js → turn-session-routing-BqO0qXLk.js} +928 -116
- package/dist/{validation--JsAGLwc.js → validation-DHR1ivfh.js} +1 -1
- package/dist/{version-jHwJp4hC.js → version-CXCHLgWm.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-CpXTGlep.js → viewer-DSbD6DF6.js} +201 -27
- package/dist/{work-3N-GHY9m.js → work-BE_qTYGi.js} +17 -36
- package/dist/{worker-CCdbcItd.js → worker-Cm9_sWXg.js} +15 -7
- package/migrations/0069_system_identity.sql +25 -0
- package/migrations/0070_review_item_foreign_keys.sql +5 -0
- package/migrations/AGENTS.md +17 -0
- package/migrations/meta/0069_snapshot.json +3979 -0
- package/migrations/meta/0070_snapshot.json +3997 -0
- package/migrations/meta/_journal.json +15 -1
- package/package.json +4 -4
- package/dist/db-BU4LQD8s.d.ts +0 -6
- package/dist/history-BgI33Kie.d.ts +0 -78
- package/dist/reporting-window-DVCD_10C.js +0 -227
- package/dist/task-runner-DMI2prDO.js +0 -1073
- package/migrations/README.md +0 -30
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as completeText, p as escapeXml, r as completeDecision, y as isProviderRetryError } from "./client-
|
|
3
|
-
import { c as getSlackClientSecret, i as getChatConfig, l as getSlackSigningSecret, n as botConfig, o as getSlackBotToken, s as getSlackClientId, v as normalizeSlackEmojiName, y as createS3AttachmentStorage } from "./config-
|
|
4
|
-
import { B as isRecord, E as withSpan, H as toOptionalString, S as setTags, T as withLogContext, V as toOptionalNumber, b as setSpanAttributes, f as logError, h as logWarn, m as logInfo, n as createChatSdkLogger, o as getActiveTraceContext, p as logException, s as getActiveTraceId, t as buildTurnFailureResponse, y as setSentryUser } from "./logging-
|
|
1
|
+
import { $t as appendThreadContextMessages, An as recordTurnSummary, At as loadProjection, Cn as loadTurnCheckpoint, Dn as failTurnRecord, En as completeTurnRecord, Et as commitMessages, Gt as hydrateConversationMessages, Ir as decideSubscribedThreadReply, Jr as piMessageFromHistoryItem, Jt as persistConversationMessageSummaries, Kr as configValueSchema, Kt as persistConversationMessages, Lr as getSubscribedReplyPreflightDecision, Lt as recordAuthorizationCompleted, Nr as buildDeterministicAssistantMessageId, On as getTurnRecord, Ot as loadConversationProjection, Pr as buildDeterministicTurnId, Rr as getThreadStopDecision, Tn as abandonTurnRecord, Tt as commitAcceptedReply, Ur as isExperimentalFeatureEnabled, Vt as recordSubscribedReplyRoute, Yt as resolveConversationTitle, Zt as fallbackShortTitle, _ as markDispatchCompleted, a as confirmDispatchMailboxAppend, an as markConversationMessage, cn as turnHasReply, d as getDispatchTurnId, dn as PluginCredentialFailureError, en as buildConversationContext, fr as buildTurnErrorResponse, g as markDispatchBlocked, h as markDispatchAwaitingResume, hn as AuthorizationFlowDisabledError, hr as persistWithRetry, i as claimDispatchMailboxAppend, in as isHumanConversationMessage, j as readAutomationTitlesByConversationId, jt as loadTurnProjection, kn as listTurnSummaries, kr as trimTrailingAssistantMessages, l as getDispatchInputMessageId, ln as upsertConversationMessage, n as resolveTurnSessionRouting, nn as estimateConversationContextTokens, on as normalizeConversationText, p as isTerminalDispatchStatus, rn as getConversationMessageSlackTs, s as getDispatchConversationId, sn as recordDeliveredAssistantMessage, t as resolveConversationRouting, tn as createConversationMemoryService, u as getDispatchRecord, v as markDispatchFailed, wn as saveTurnCheckpoint, xn as wasPendingAuthStopped, y as markDispatchRunning, yn as clearPendingAuth } from "./turn-session-routing-BqO0qXLk.js";
|
|
2
|
+
import { a as completeText, p as escapeXml, r as completeDecision, y as isProviderRetryError } from "./client-Cqf4TboL.js";
|
|
3
|
+
import { c as getSlackClientSecret, i as getChatConfig, l as getSlackSigningSecret, n as botConfig, o as getSlackBotToken, s as getSlackClientId, v as normalizeSlackEmojiName, y as createS3AttachmentStorage } from "./config-CPsE4T5L.js";
|
|
4
|
+
import { B as isRecord, E as withSpan, H as toOptionalString, S as setTags, T as withLogContext, V as toOptionalNumber, b as setSpanAttributes, f as logError, h as logWarn, m as logInfo, n as createChatSdkLogger, o as getActiveTraceContext, p as logException, s as getActiveTraceId, t as buildTurnFailureResponse, y as setSentryUser } from "./logging-n7knse-O.js";
|
|
5
5
|
import { i as getActiveSpan, r as continueTrace } from "./sentry-MDkYAVPV.js";
|
|
6
6
|
import { o as modelIdForProfile, r as defaultModelId } from "./model-profile-Bg8EoCKD.js";
|
|
7
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 {
|
|
8
|
+
import { C as getSqlExecutor, E as loadConversationSlackTenantIds, H as parseSessionSource, M as resolveActorIdentityId, N as saveActorAvatar, S as getDb, T as withConversationEventLock, U as addAgentTurnUsage, Y as createSlackDestination, Z as requireSlackDestination, f as identityAvatarUrl, h as readUserAvatarUrls, k as locationFromRow, q as hasAgentTurnUsage, x as getConversationStore, y as getConversationEventStore } from "./viewer-DSbD6DF6.js";
|
|
9
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,
|
|
11
|
-
import {
|
|
12
|
-
import { d as actorLogContext, f as credentialContextForActor } from "./registry-
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { a as
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import { i as
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import { t as
|
|
33
|
-
import { t as
|
|
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-
|
|
10
|
+
import { S as juniorCodeChanges, g as juniorConversationBriefs, y as juniorConversationEvents } from "./executor-CNS7ONt-.js";
|
|
11
|
+
import { g as instructionProvenanceFor, m as contextProvenance, r as isVisionImageMediaType } from "./media-85PDJxO_.js";
|
|
12
|
+
import { d as actorLogContext, f as credentialContextForActor } from "./registry-By36_xsm.js";
|
|
13
|
+
import { Et as withoutSavedSteeringMessages, F as getConversationInfo, I as listThreadReplies, M as readSlackRawMessageContext, N as resolveSlackChannelIdFromMessage, Nt as shouldEmitDevAgentTrace, P as resolveSlackChannelIdFromThreadId, St as conversationSubtreeColumns, Tt as buildSteeringPiMessage, b as renderBlockText, ct as webActorFromEmail, et as deliverReviewItemChange, i as buildAuthPauseResponse, j as parseSlackThreadId, k as runWithSlackInstallation, kt as RetryableDeliveryError, lt as resolveConversationDestination, mt as storeAttachment, n as executeAgentRun, ot as createConversationId, p as latestProgressStatus, r as createContextCompactor, st as recordWebConversationActivity, t as createAgentRunner, xt as conversationSubtree, y as renderAttachmentText } from "./agent-runner-CrDrPUtY.js";
|
|
14
|
+
import { An as normalizeSlackConversationId, Ar as lookupSlackUser, Cn as SlackActionError, Dn as getSlackClient, Dr as listConversationAnnotationsById, Dt as removeReactionFromMessage, E as listReplyActionItems, En as getHeaderString, F as isOnlyAssignee, In as JUNIOR_THREAD_STATE_TTL_MS, M as canActOnReviewItem, Mn as withSlackRetries, P as canReadReviewItem, Sn as verifyTimerWatchCredentialSubject, Sr as recordSlackAttachment, _t as normalizeSlackStatusText, ar as createPluginLogger, b as closeReviewItem, bt as addReactionToMessage, cr as conversationArchivedForEmail, dr as conversationIsMemberRoot, fr as conversationNotArchivedForEmail, jr as createPluginCache, k as takeReviewItem, kn as isDmChannel, lr as conversationArchivedForUser, mr as viewerConversationMembership, n as createUserTokenStore, on as cancelSubscriptions, or as readConversationAccessFromSql, p as getPlugins, pr as conversationNotArchivedForUser, sr as conversationArchiveTimes, ur as conversationHasParticipantUser, w as getReviewItem, wn as downloadPrivateSlackFile, xr as readAttachmentVisionSummary, yr as getSlackFileId, yt as truncateStatusText, z as reviewItemReaderForUser } from "./credentialed-BU-OW7HP.js";
|
|
15
|
+
import { a as withLock, n as MUTATION_LOCK_TTL_MS, r as acquireActiveLock } from "./locks-D-0h111u.js";
|
|
16
|
+
import { r as getStateAdapter } from "./adapter-2-CG25be.js";
|
|
17
|
+
import { W as readMessageAttachments, g as conversationFeedSchema } from "./conversation-CTpD_PGk.js";
|
|
18
|
+
import { a as EVENT_SYSTEM_ACTOR, o as isEventConversationMessage } from "./prompt-BlQB0QQt.js";
|
|
19
|
+
import { B as ConversationTurnBoundaryError, G as isCooperativeTurnYieldError, H as TurnInputCommitLostError, J as markTurnClosed, K as isTurnInputCommitLostError, U as TurnInputDeferredError, V as CooperativeTurnYieldError, W as getConversationTurnBoundaryError, X as markTurnFailed, Y as markTurnCompleted, Z as startActiveTurn, q as isTurnInputDeferredError } from "./store-1DVhoKOL.js";
|
|
20
|
+
import { r as getAssistantReplyText } from "./interruption-marker-g6Xup3Fk.js";
|
|
21
|
+
import { C as maybePostAutomatedTurnLimitNotice, D as recordFinishedTurnForAutomatedLimit, I as runDispatchOutcome, M as finishedRunReply, N as postsRunNotices, T as sendSlackReply, c as isEventMailboxMetadata, i as skipInputBeforeConversationStop, j as deliversFinalText, l as renderEventBatchText, s as eventBatchMessageEvents, u as timerWatchCredentialSubject, w as resolveSlackReplyTarget, x as joinMailboxText } from "./session-stop-BQW_iFcd.js";
|
|
22
|
+
import { _ as normalizeUserInputAnswer, g as isEmptyUserInputAnswer } from "./reporting-window-CVarfId9.js";
|
|
23
|
+
import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline } from "./job-queue-DI-k-Bbk.js";
|
|
24
|
+
import { D as stopConversationWork, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, r as appendAndEnqueueInboundMessage, w as requestConversationWork } from "./store-DCo2n5iJ.js";
|
|
25
|
+
import { t as healthReportSchema } from "./reporting-schema-CZREhYc-.js";
|
|
26
|
+
import { a as withAgentInvocationResultActors, c as resolveMailboxTurnWork, d as legacyWebMailboxMetadataSchema, l as sourceFromTurnInput, n as enqueueAgentInvocationResult, o as getAuthPausedConversationTurnId, s as getEventBatchDelayMs, t as createChildConversationWorker, u as turnInputFactsFromConversationMessage } from "./result-BemsOMOO.js";
|
|
27
|
+
import { n as getTurnLifecycle, t as ConversationTurnLifecycleService } from "./turn-lifecycle-bf2fIYi_.js";
|
|
28
|
+
import { a as AgentRunError, o as executeTurn } from "./work-BE_qTYGi.js";
|
|
29
|
+
import { a as persistThreadStateById, i as persistThreadRuntimeState, n as getPersistedSandboxState, o as coerceThreadConversationState, r as getPersistedThreadState, t as getLocationConfigurationService } from "./thread-state-CHy8LS0C.js";
|
|
30
|
+
import { c as deleteWebAuthorization, d as createPausedTurns, f as getPausedTurnRequest, h as getTurnAuthorization, m as wakePausedTurn, s as createWebAuthorization } from "./connections-upFXLmhG.js";
|
|
31
|
+
import { t as bindMessageAgent } from "./message-BpaWd8TT.js";
|
|
32
|
+
import { t as bindSpawnAgent } from "./spawn-ChCm46i-.js";
|
|
33
|
+
import { n as scheduleSessionCompletedPluginTasks, t as processPluginTask } from "./task-runner-cA6yjn_E.js";
|
|
34
|
+
import { a as persistAuthPauseTurnState, c as getTurnUserSlackMessageTs, i as completeAuthPauseTurn, l as buildDeliveredTurnStatePatch, n as getAgentTurnDiagnosticsAttributes, o as getTurnUserMessage, r as requireTurnFailureEventId, s as getTurnUserReplyAttachmentContext, t as finalizeFailedTurnReplyWithEvent, u as loadPendingMessageCards } from "./turn-failure-response-Dr7erLWs.js";
|
|
35
35
|
import { z } from "zod";
|
|
36
36
|
import { createHash } from "node:crypto";
|
|
37
|
-
import { and, asc,
|
|
37
|
+
import { and, asc, desc, eq, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm";
|
|
38
38
|
import { conversationSidebarAnnotationSchema, createSlackSource, objectTypeSchema } from "@sentry/junior-plugin-api";
|
|
39
39
|
import { alias } from "drizzle-orm/pg-core";
|
|
40
40
|
import { del, get, put } from "@vercel/blob";
|
|
@@ -79,7 +79,7 @@ function buildDispatchRoutingContext(dispatch) {
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
/** Build the stable mailbox work item for one agent dispatch. */
|
|
82
|
-
function buildAgentDispatchInboundMessage(dispatch, nowMs = Date.now()) {
|
|
82
|
+
function buildAgentDispatchInboundMessage(dispatch, nowMs = Date.now(), authorIdentityId) {
|
|
83
83
|
return {
|
|
84
84
|
conversationId: getDispatchConversationId(dispatch),
|
|
85
85
|
createdAtMs: dispatch.createdAtMs,
|
|
@@ -88,6 +88,7 @@ function buildAgentDispatchInboundMessage(dispatch, nowMs = Date.now()) {
|
|
|
88
88
|
inboundMessageId: getDispatchInputMessageId(dispatch.id),
|
|
89
89
|
input: {
|
|
90
90
|
text: "[plugin dispatch]",
|
|
91
|
+
...authorIdentityId ? { authorIdentityId } : void 0,
|
|
91
92
|
metadata: {
|
|
92
93
|
dispatchId: dispatch.id,
|
|
93
94
|
kind: "agent_dispatch"
|
|
@@ -111,7 +112,7 @@ async function enqueueAgentDispatch(dispatch, options) {
|
|
|
111
112
|
sessionSource: claimedDispatch.source
|
|
112
113
|
});
|
|
113
114
|
await appendAndEnqueueInboundMessage({
|
|
114
|
-
message: buildAgentDispatchInboundMessage(claimedDispatch, nowMs),
|
|
115
|
+
message: buildAgentDispatchInboundMessage(claimedDispatch, nowMs, await resolveActorIdentityId(getSqlExecutor(), claimedDispatch.actor)),
|
|
115
116
|
conversationStore: options.conversationStore,
|
|
116
117
|
nowMs,
|
|
117
118
|
queue: options.queue,
|
|
@@ -389,21 +390,6 @@ var ReviewItemActionError = class extends Error {
|
|
|
389
390
|
this.code = code;
|
|
390
391
|
}
|
|
391
392
|
};
|
|
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
393
|
/** Read one Review item that a User can see, or fail as not found. */
|
|
408
394
|
async function readReviewItemForUser(user, id) {
|
|
409
395
|
const item = await getReviewItem(getDb(), id);
|
|
@@ -432,9 +418,12 @@ function reviewItemChange(action, user) {
|
|
|
432
418
|
user
|
|
433
419
|
};
|
|
434
420
|
}
|
|
435
|
-
/**
|
|
421
|
+
/**
|
|
422
|
+
* Return whether a closed item is the result of this same action. An item
|
|
423
|
+
* that a person's message closed never matches a card or desk action.
|
|
424
|
+
*/
|
|
436
425
|
function isSameClose(item, action, user) {
|
|
437
|
-
return item.closedBy?.userId === user.id && item.status === closedStatus(action) && action === "submit" === Boolean(item.answer);
|
|
426
|
+
return item.closeReason !== "Replied in thread" && item.closedBy?.userId === user.id && item.status === closedStatus(action) && action === "submit" === Boolean(item.answer);
|
|
438
427
|
}
|
|
439
428
|
function submittedAnswer(item, answer) {
|
|
440
429
|
if (item.kind !== "action" || !item.request) throw new ReviewItemActionError("invalid", "Only an action item takes an answer.");
|
|
@@ -453,10 +442,11 @@ function submittedAnswer(item, answer) {
|
|
|
453
442
|
* succeeds, so a client can retry after a failure. With `firstCloseOnly`,
|
|
454
443
|
* only the call that closes the item succeeds, and every other call gets a
|
|
455
444
|
* conflict. Use it when the caller adds its own input for the action, such
|
|
456
|
-
* as a
|
|
457
|
-
* is the Conversation where the person acted. It gets
|
|
458
|
-
* from its own surface, so it gets no message here.
|
|
459
|
-
* change
|
|
445
|
+
* as a card click in a chat thread, so one action adds one Turn.
|
|
446
|
+
* `exceptConversationId` is the Conversation where the person acted. It gets
|
|
447
|
+
* the person's input from its own surface, so it gets no message here.
|
|
448
|
+
* `delivery` shows the change and sends the messages; see
|
|
449
|
+
* `deliverReviewItemChange`.
|
|
460
450
|
*/
|
|
461
451
|
async function actOnReviewItem(args) {
|
|
462
452
|
const { action, user } = args;
|
|
@@ -472,16 +462,12 @@ async function actOnReviewItem(args) {
|
|
|
472
462
|
userId: user.id
|
|
473
463
|
})) throw new ReviewItemActionError("conflict", "Review item is closed.");
|
|
474
464
|
const taken = await readReviewItemForUser(user, item.id);
|
|
475
|
-
await
|
|
476
|
-
getSlackAdapter: args.getSlackAdapter,
|
|
477
|
-
itemId: taken.id
|
|
478
|
-
});
|
|
479
|
-
await sendReviewItemMessages({
|
|
465
|
+
await deliverReviewItemChange({
|
|
480
466
|
change: reviewItemChange(action, user),
|
|
467
|
+
delivery: args.delivery,
|
|
481
468
|
exceptConversationId: args.exceptConversationId,
|
|
482
469
|
item: taken,
|
|
483
|
-
nowMs
|
|
484
|
-
queue: args.queue
|
|
470
|
+
nowMs
|
|
485
471
|
});
|
|
486
472
|
return taken;
|
|
487
473
|
}
|
|
@@ -501,19 +487,92 @@ async function actOnReviewItem(args) {
|
|
|
501
487
|
closed = await readReviewItemForUser(user, item.id);
|
|
502
488
|
}
|
|
503
489
|
if (args.firstCloseOnly ? !closedNow : !isSameClose(closed, action, user)) throw new ReviewItemActionError("conflict", "Review item is closed.");
|
|
504
|
-
await
|
|
505
|
-
getSlackAdapter: args.getSlackAdapter,
|
|
506
|
-
itemId: closed.id
|
|
507
|
-
});
|
|
508
|
-
await sendReviewItemMessages({
|
|
490
|
+
await deliverReviewItemChange({
|
|
509
491
|
change: reviewItemChange(action, user),
|
|
492
|
+
delivery: args.delivery,
|
|
510
493
|
exceptConversationId: args.exceptConversationId,
|
|
511
494
|
item: closed,
|
|
512
|
-
nowMs
|
|
513
|
-
queue: args.queue
|
|
495
|
+
nowMs
|
|
514
496
|
});
|
|
515
497
|
return closed;
|
|
516
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Return whether a reply from this User answers an open action item, or is
|
|
501
|
+
* a retry of the reply from this User that closed it.
|
|
502
|
+
*/
|
|
503
|
+
function replyAnswers(item, user) {
|
|
504
|
+
if (item.status !== "open") return item.closedBy?.userId === user?.id;
|
|
505
|
+
if (item.assignedToEveryone) return false;
|
|
506
|
+
if (item.assignees.length === 0) return true;
|
|
507
|
+
return user !== void 0 && item.assignees.some((assignee) => assignee.userId === user.id);
|
|
508
|
+
}
|
|
509
|
+
/** Return the answer that the text of a person's message gives to an item. */
|
|
510
|
+
function messageAnswer(item, text) {
|
|
511
|
+
if (!item.request) return void 0;
|
|
512
|
+
const answer = normalizeUserInputAnswer(item.request, {
|
|
513
|
+
other: text,
|
|
514
|
+
requestId: item.id,
|
|
515
|
+
values: {}
|
|
516
|
+
});
|
|
517
|
+
return isEmptyUserInputAnswer(answer) ? void 0 : answer;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Close the open action items that a person answered with a reply in their
|
|
521
|
+
* thread. Call it after the reply is saved to the Conversation, so the
|
|
522
|
+
* reply always reaches the agent. Each item that it closes saves `text` as
|
|
523
|
+
* its answer, so the item keeps what the person said.
|
|
524
|
+
*
|
|
525
|
+
* A reply closes an item only when all of these are true:
|
|
526
|
+
* - The item is an open `action` item.
|
|
527
|
+
* - The agent added it in this Conversation (`addedByConversationId`).
|
|
528
|
+
* - The replier is an assigned User, or the item has no assignees. A Slack
|
|
529
|
+
* replier without a Junior User closes only items with no assignees.
|
|
530
|
+
*
|
|
531
|
+
* Items assigned to Everyone, or only to other people, stay open. They need
|
|
532
|
+
* an answer on the card or the review desk. Each item closes as `done` with
|
|
533
|
+
* `REVIEW_ITEM_REPLIED_REASON`, and the replier closes it. The reply itself
|
|
534
|
+
* is the answer in this Conversation, so only the other linked
|
|
535
|
+
* Conversations get a message. A click that closes the item first wins.
|
|
536
|
+
* A retry of the reply, or a later reply from the same person, delivers
|
|
537
|
+
* the items that it closed again: the cards show the close, and only the
|
|
538
|
+
* missing messages go. Each message has a key, so a message that was sent
|
|
539
|
+
* does not go again. `delivery` shows the change and sends the messages;
|
|
540
|
+
* see `deliverReviewItemChange`.
|
|
541
|
+
*/
|
|
542
|
+
async function closeReviewItemsAnsweredByReply(args) {
|
|
543
|
+
const db = getDb();
|
|
544
|
+
const items = await listReplyActionItems(db, args.conversationId);
|
|
545
|
+
if (items.length === 0) return;
|
|
546
|
+
if (args.canAnswer && !await args.canAnswer()) return;
|
|
547
|
+
const user = await args.resolveUser();
|
|
548
|
+
const nowMs = args.nowMs ?? Date.now();
|
|
549
|
+
for (const item of items.filter((candidate) => replyAnswers(candidate, user))) {
|
|
550
|
+
let closedItem = item;
|
|
551
|
+
if (item.status === "open") {
|
|
552
|
+
const answer = messageAnswer(item, args.text);
|
|
553
|
+
if (!await closeReviewItem(db, {
|
|
554
|
+
...answer ? { answer } : void 0,
|
|
555
|
+
id: item.id,
|
|
556
|
+
nowMs,
|
|
557
|
+
reason: "Replied in thread",
|
|
558
|
+
status: "done",
|
|
559
|
+
...user ? { closedByUserId: user.id } : void 0
|
|
560
|
+
})) continue;
|
|
561
|
+
closedItem = await getReviewItem(db, item.id);
|
|
562
|
+
if (!closedItem) continue;
|
|
563
|
+
}
|
|
564
|
+
await deliverReviewItemChange({
|
|
565
|
+
change: {
|
|
566
|
+
kind: "replied",
|
|
567
|
+
who: user ? user.displayName ?? user.email : "A person"
|
|
568
|
+
},
|
|
569
|
+
delivery: args.delivery,
|
|
570
|
+
exceptConversationId: args.conversationId,
|
|
571
|
+
item: closedItem,
|
|
572
|
+
nowMs
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
517
576
|
// src/chat/slack/errors.ts
|
|
518
577
|
/** Extract Slack's stable API error code from Web API errors. */
|
|
519
578
|
function getSlackApiErrorCode(error) {
|
|
@@ -847,6 +906,7 @@ function combineTurnText(queuedMessages, latestText) {
|
|
|
847
906
|
}
|
|
848
907
|
// src/chat/services/message-actor-identity.ts
|
|
849
908
|
const messageActors = /* @__PURE__ */ new WeakMap();
|
|
909
|
+
const messageAuthorIdentityIds = /* @__PURE__ */ new WeakMap();
|
|
850
910
|
function canonicalUserId(author, actor) {
|
|
851
911
|
const authorUserId = parseActorUserId(author.userId);
|
|
852
912
|
if (authorUserId && authorUserId !== actor.userId) throw new Error("Message actor user id mismatch");
|
|
@@ -877,6 +937,17 @@ function bindMessageActorIdentity(message, actor) {
|
|
|
877
937
|
applyActorToAuthor(message.author, currentActor);
|
|
878
938
|
return currentActor;
|
|
879
939
|
}
|
|
940
|
+
/**
|
|
941
|
+
* Keep the author Identity id that ingress resolved, next to the Actor.
|
|
942
|
+
* The Message write reads it, so the write does not guess the author again.
|
|
943
|
+
*/
|
|
944
|
+
function bindMessageAuthorIdentityId(message, authorIdentityId) {
|
|
945
|
+
if (authorIdentityId) messageAuthorIdentityIds.set(message, authorIdentityId);
|
|
946
|
+
}
|
|
947
|
+
/** Read the author Identity id that ingress bound to a Chat SDK message. */
|
|
948
|
+
function getMessageAuthorIdentityId(message) {
|
|
949
|
+
return messageAuthorIdentityIds.get(message);
|
|
950
|
+
}
|
|
880
951
|
/** Read message identity without promoting adapter display fallbacks. */
|
|
881
952
|
function getMessageActorIdentity(message) {
|
|
882
953
|
return messageActors.get(message) ?? actorFromAuthor(message.author);
|
|
@@ -2072,6 +2143,7 @@ function restoreMessage(args) {
|
|
|
2072
2143
|
const message = Message.fromJSON(metadata.message);
|
|
2073
2144
|
message.attachments = message.attachments.map((attachment) => args.adapter.rehydrateAttachment(attachment));
|
|
2074
2145
|
rehydrateAttachmentFetchers(message);
|
|
2146
|
+
bindMessageAuthorIdentityId(message, args.record.input.authorIdentityId);
|
|
2075
2147
|
return message;
|
|
2076
2148
|
}
|
|
2077
2149
|
async function bindSlackActorIdentities(args) {
|
|
@@ -2083,6 +2155,10 @@ async function bindSlackActorIdentities(args) {
|
|
|
2083
2155
|
await Promise.all([...byAuthorId].map(async ([authorId, messages]) => {
|
|
2084
2156
|
const profile = await args.lookupSlackUser(args.teamId, authorId);
|
|
2085
2157
|
await Promise.all(messages.map((message) => ensureSlackMessageActorIdentity(message, args.teamId, async () => profile)));
|
|
2158
|
+
if (profile && messages.some(getMessageAuthorIdentityId)) await resolveActorIdentityId(getSqlExecutor(), createSlackActor(args.teamId, authorId, {
|
|
2159
|
+
fullName: profile.fullName,
|
|
2160
|
+
userName: profile.userName
|
|
2161
|
+
}));
|
|
2086
2162
|
}));
|
|
2087
2163
|
}
|
|
2088
2164
|
function restoreThread(args) {
|
|
@@ -2244,6 +2320,7 @@ function createSlackConversationWorker(options) {
|
|
|
2244
2320
|
/** Serialize a Slack message and the active trace into the durable mailbox. */
|
|
2245
2321
|
function buildSlackInboundMessage(args) {
|
|
2246
2322
|
const authorId = requireSlackAuthorId(args.message);
|
|
2323
|
+
const authorIdentityId = getMessageAuthorIdentityId(args.message);
|
|
2247
2324
|
const destination = createSlackDestination({
|
|
2248
2325
|
channelId: args.thread.channelId,
|
|
2249
2326
|
teamId: args.installation?.teamId
|
|
@@ -2266,6 +2343,7 @@ function buildSlackInboundMessage(args) {
|
|
|
2266
2343
|
input: {
|
|
2267
2344
|
text: args.message.text || " ",
|
|
2268
2345
|
authorId,
|
|
2346
|
+
...authorIdentityId ? { authorIdentityId } : void 0,
|
|
2269
2347
|
attachments: args.message.attachments,
|
|
2270
2348
|
metadata: {
|
|
2271
2349
|
platform: "slack",
|
|
@@ -2722,9 +2800,11 @@ function toConversationMessage(args) {
|
|
|
2722
2800
|
const slackTs = getMessageTimestamp(args.entry);
|
|
2723
2801
|
const messageHasPotentialImageAttachment = hasPotentialImageAttachment(args.entry.attachments);
|
|
2724
2802
|
const imageAttachmentCount = messageHasPotentialImageAttachment ? countPotentialImageAttachments(args.entry.attachments) : 0;
|
|
2803
|
+
const authorIdentityId = args.entry.author.isMe ? void 0 : getMessageAuthorIdentityId(args.entry);
|
|
2725
2804
|
return {
|
|
2726
2805
|
id: args.entry.id,
|
|
2727
2806
|
role: args.entry.author.isMe ? "assistant" : "user",
|
|
2807
|
+
...authorIdentityId ? { authorIdentityId } : void 0,
|
|
2728
2808
|
text: resolveMessageText(args),
|
|
2729
2809
|
createdAtMs: args.entry.metadata.dateSent.getTime(),
|
|
2730
2810
|
author: {
|
|
@@ -2814,578 +2894,6 @@ function isAllowedSignInEmail(email, allowedDomains) {
|
|
|
2814
2894
|
const separator = normalized.lastIndexOf("@");
|
|
2815
2895
|
return allowedDomains.length > 0 && separator > 0 && isAllowedDomain(normalized.slice(separator + 1), allowedDomains);
|
|
2816
2896
|
}
|
|
2817
|
-
// src/chat/automations/collection.ts
|
|
2818
|
-
/**
|
|
2819
|
-
* The web collection applies access before counts, filter options, and paging.
|
|
2820
|
-
* Keep its ownership rules aligned with the direct Automation reader. A public
|
|
2821
|
-
* Destination only grants access inside a viewer's linked Slack workspaces.
|
|
2822
|
-
*/
|
|
2823
|
-
function viewerAutomationCollection(user) {
|
|
2824
|
-
const db = getDb();
|
|
2825
|
-
const teamIds = user.identities.filter((identity) => identity.provider === "slack").flatMap((identity) => identity.providerTenantId ? [identity.providerTenantId] : []);
|
|
2826
|
-
const automation = juniorAutomations;
|
|
2827
|
-
const destination = juniorDestinations;
|
|
2828
|
-
const isScheduled = eq(automation.kind, "scheduled");
|
|
2829
|
-
const owned = inArray(automation.creatorIdentityId, user.identities.map((identity) => identity.id));
|
|
2830
|
-
const publicDestination = sql`coalesce(${destination.visibility} = 'public', false)`;
|
|
2831
|
-
const publicToViewer = and(inArray(automation.teamId, teamIds), publicDestination, ne(automation.status, "completed"));
|
|
2832
|
-
const runs = juniorAutomationRuns;
|
|
2833
|
-
const lastRunNeedsAttention = sql`coalesce((select ${runs.status} in ('failed', 'blocked') from ${runs}
|
|
2834
|
-
where ${runs.automationId} = ${automation.id} and ${runs.status} in ('blocked', 'completed', 'failed') and ${runs.completedAtMs} is not null
|
|
2835
|
-
order by ${runs.completedAtMs} desc, ${runs.id} desc limit 1), false)`;
|
|
2836
|
-
return db.select({
|
|
2837
|
-
kind: automation.kind,
|
|
2838
|
-
id: automation.id,
|
|
2839
|
-
title: automation.title,
|
|
2840
|
-
createdAtMs: automation.createdAtMs,
|
|
2841
|
-
state: sql`${automation.status}`.as("state"),
|
|
2842
|
-
lastRunNeedsAttention: lastRunNeedsAttention.as("last_run_needs_attention"),
|
|
2843
|
-
trigger: sql`${automation.record}->'trigger'`.as("trigger"),
|
|
2844
|
-
owned: sql`${owned}`.as("owned"),
|
|
2845
|
-
resource: sql`case when ${isScheduled} then '' else concat_ws(' ', ${automation.identifier}, ${automation.record}->'trigger'->>'label', ${automation.namespace}) end`.as("resource"),
|
|
2846
|
-
creator: sql`${automation.teamId} || ':' || ${automation.creatorSlackUserId}`.as("creator"),
|
|
2847
|
-
creatorLabel: sql`coalesce(nullif(trim(${automation.record}->'createdBy'->>'fullName'), ''), nullif(trim(${automation.record}->'createdBy'->>'userName'), ''), ${automation.creatorSlackUserId})`.as("creator_label"),
|
|
2848
|
-
destination: sql`${automation.teamId} || ':' || ${automation.channelId}`.as("destination"),
|
|
2849
|
-
destinationLabel: sql`coalesce(nullif(${destination.displayName}, ''), ${automation.channelId})`.as("destination_label"),
|
|
2850
|
-
isPublic: publicDestination.as("is_public"),
|
|
2851
|
-
instruction: automation.instruction
|
|
2852
|
-
}).from(automation).leftJoin(destination, and(eq(destination.provider, "slack"), eq(destination.providerTenantId, automation.teamId), eq(destination.providerDestinationId, automation.channelId))).where(and(ne(automation.status, "deleted"), or(owned, publicToViewer))).as("accessible_automations");
|
|
2853
|
-
}
|
|
2854
|
-
/**
|
|
2855
|
-
* List the accessible event automations whose trigger cannot work with the
|
|
2856
|
-
* enabled plugins. `eventTriggerProblems` decides, the same check that the
|
|
2857
|
-
* tools and the dashboard use, so SQL does not keep its own copy of the rules.
|
|
2858
|
-
*/
|
|
2859
|
-
async function unavailableEventAutomationIds(collection) {
|
|
2860
|
-
const catalog = automationEventCatalog(getEventCatalog());
|
|
2861
|
-
return (await getDb().select({
|
|
2862
|
-
id: collection.id,
|
|
2863
|
-
trigger: collection.trigger
|
|
2864
|
-
}).from(collection).where(eq(collection.kind, "event"))).flatMap((row) => {
|
|
2865
|
-
const trigger = eventAutomationTriggerSchema.safeParse(row.trigger);
|
|
2866
|
-
return trigger.success && registeredEventTriggerProblems(catalog, trigger.data).length === 0 ? [] : [row.id];
|
|
2867
|
-
});
|
|
2868
|
-
}
|
|
2869
|
-
/** Read an SQL page and complete, access-scoped counts and filter options. */
|
|
2870
|
-
async function readAutomationCollection(user, input) {
|
|
2871
|
-
const db = getDb();
|
|
2872
|
-
const collection = viewerAutomationCollection(user);
|
|
2873
|
-
const unavailableIds = await unavailableEventAutomationIds(collection);
|
|
2874
|
-
const unavailable = unavailableIds.length > 0 ? inArray(collection.id, unavailableIds) : sql`false`;
|
|
2875
|
-
const attention = and(notInArray(collection.state, ["paused", "completed"]), or(eq(collection.state, "blocked"), unavailable, eq(collection.lastRunNeedsAttention, true)));
|
|
2876
|
-
const where = and(input.scope === "mine" ? eq(collection.owned, true) : input.scope === "public" ? eq(collection.isPublic, true) : input.scope === "attention" ? attention : void 0, input.type === "all" ? void 0 : eq(collection.kind, input.type), input.state === "all" ? void 0 : input.state === "unavailable" ? unavailable : eq(collection.state, input.state), input.creator ? eq(collection.creator, input.creator) : void 0, input.destination ? eq(collection.destination, input.destination) : void 0, input.q ? sql`strpos(lower(concat_ws(' ', ${collection.title}, ${collection.instruction}, ${collection.resource})), ${input.q.toLowerCase()}) > 0` : void 0);
|
|
2877
|
-
const [totals, counts, creators, destinations] = await Promise.all([
|
|
2878
|
-
db.select({ total: count() }).from(collection).where(where),
|
|
2879
|
-
db.select({
|
|
2880
|
-
all: count(),
|
|
2881
|
-
mine: sql`count(*) filter (where ${collection.owned})::int`,
|
|
2882
|
-
public: sql`count(*) filter (where ${collection.isPublic})::int`,
|
|
2883
|
-
private: sql`count(*) filter (where not ${collection.isPublic})::int`
|
|
2884
|
-
}).from(collection),
|
|
2885
|
-
db.select({
|
|
2886
|
-
value: collection.creator,
|
|
2887
|
-
label: sql`min(${collection.creatorLabel})`
|
|
2888
|
-
}).from(collection).groupBy(collection.creator).orderBy(sql`min(${collection.creatorLabel})`, collection.creator),
|
|
2889
|
-
db.select({
|
|
2890
|
-
value: collection.destination,
|
|
2891
|
-
label: sql`min(${collection.destinationLabel})`
|
|
2892
|
-
}).from(collection).groupBy(collection.destination).orderBy(sql`min(${collection.destinationLabel})`, collection.destination)
|
|
2893
|
-
]);
|
|
2894
|
-
const total = totals[0].total;
|
|
2895
|
-
const page = Math.min(input.page, Math.max(1, Math.ceil(total / input.pageSize)));
|
|
2896
|
-
const order = input.sort === "oldest" ? asc(collection.createdAtMs) : input.sort === "title" ? asc(sql`lower(coalesce(nullif(trim(${collection.title}), ''), ${collection.instruction}))`) : desc(collection.createdAtMs);
|
|
2897
|
-
return {
|
|
2898
|
-
rows: await db.select({
|
|
2899
|
-
id: collection.id,
|
|
2900
|
-
kind: collection.kind
|
|
2901
|
-
}).from(collection).where(where).orderBy(order, desc(collection.id)).limit(input.pageSize).offset((page - 1) * input.pageSize),
|
|
2902
|
-
total,
|
|
2903
|
-
page,
|
|
2904
|
-
pageSize: input.pageSize,
|
|
2905
|
-
counts: counts[0],
|
|
2906
|
-
creators,
|
|
2907
|
-
destinations
|
|
2908
|
-
};
|
|
2909
|
-
}
|
|
2910
|
-
// src/chat/automations/creator.ts
|
|
2911
|
-
/**
|
|
2912
|
-
* Return whether the user created the Automation. Both kinds store the
|
|
2913
|
-
* creating Junior identity, so one rule decides ownership.
|
|
2914
|
-
*/
|
|
2915
|
-
function isAutomationCreator(user, automation) {
|
|
2916
|
-
return user.identities.some((identity) => identity.id === automation.creatorIdentityId);
|
|
2917
|
-
}
|
|
2918
|
-
// src/chat/scheduled-automations/personal.ts
|
|
2919
|
-
var PersonalScheduledAutomationNotFoundError = class extends Error {
|
|
2920
|
-
constructor() {
|
|
2921
|
-
super("Scheduled automation was not found.");
|
|
2922
|
-
this.name = "PersonalScheduledAutomationNotFoundError";
|
|
2923
|
-
}
|
|
2924
|
-
};
|
|
2925
|
-
/** Delete one scheduled automation created by the viewer. */
|
|
2926
|
-
async function deleteViewerScheduledAutomation(db, user, id, nowMs = Date.now()) {
|
|
2927
|
-
await withScheduledAutomationLock(db, id, async (tx) => {
|
|
2928
|
-
const task = await readScheduledAutomation(tx, id);
|
|
2929
|
-
if (!task || task.status === "deleted" || !isAutomationCreator(user, task)) throw new PersonalScheduledAutomationNotFoundError();
|
|
2930
|
-
await saveScheduledAutomationInLock(tx, {
|
|
2931
|
-
...task,
|
|
2932
|
-
nextRunAtMs: void 0,
|
|
2933
|
-
runNowAtMs: void 0,
|
|
2934
|
-
status: "deleted",
|
|
2935
|
-
updatedAtMs: nowMs
|
|
2936
|
-
}, task);
|
|
2937
|
-
});
|
|
2938
|
-
}
|
|
2939
|
-
// src/chat/automations/read.ts
|
|
2940
|
-
const TASK_EXECUTION_LIST_LIMIT = 100;
|
|
2941
|
-
const AUTOMATION_VERSION_LIST_LIMIT = 100;
|
|
2942
|
-
function creatorLabel(creator) {
|
|
2943
|
-
return creator.fullName?.trim() || (creator.userName?.trim() ? `@${creator.userName.trim()}` : "") || creator.slackUserId;
|
|
2944
|
-
}
|
|
2945
|
-
function creatorKey(teamId, slackUserId) {
|
|
2946
|
-
return `${teamId}:${slackUserId}`;
|
|
2947
|
-
}
|
|
2948
|
-
async function creatorProfiles(candidates) {
|
|
2949
|
-
const selectors = new Map(candidates.map(({ task }) => {
|
|
2950
|
-
const teamId = task.destination.teamId;
|
|
2951
|
-
const slackUserId = task.createdBy.slackUserId;
|
|
2952
|
-
return [creatorKey(teamId, slackUserId), {
|
|
2953
|
-
slackUserId,
|
|
2954
|
-
teamId
|
|
2955
|
-
}];
|
|
2956
|
-
}));
|
|
2957
|
-
if (selectors.size === 0) return /* @__PURE__ */ new Map();
|
|
2958
|
-
const rows = await getDb().select({
|
|
2959
|
-
avatarUrl: juniorIdentities.avatarUrl,
|
|
2960
|
-
email: juniorUsers.primaryEmailNormalized,
|
|
2961
|
-
emailVerified: juniorIdentities.emailVerified,
|
|
2962
|
-
slackUserId: juniorIdentities.providerSubjectId,
|
|
2963
|
-
teamId: juniorIdentities.providerTenantId
|
|
2964
|
-
}).from(juniorIdentities).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(eq(juniorIdentities.kind, "user"), eq(juniorIdentities.provider, "slack"), or(...[...selectors.values()].map((selector) => and(eq(juniorIdentities.providerTenantId, selector.teamId), eq(juniorIdentities.providerSubjectId, selector.slackUserId))))));
|
|
2965
|
-
return new Map(rows.map((row) => [creatorKey(row.teamId, row.slackUserId), {
|
|
2966
|
-
email: row.emailVerified ? row.email ?? void 0 : void 0,
|
|
2967
|
-
avatarUrl: row.avatarUrl ?? void 0
|
|
2968
|
-
}]));
|
|
2969
|
-
}
|
|
2970
|
-
function destinationKey(destination) {
|
|
2971
|
-
return `${destination.teamId}:${destination.channelId}`;
|
|
2972
|
-
}
|
|
2973
|
-
function displayText(value, fallback) {
|
|
2974
|
-
return value.trim() || fallback;
|
|
2975
|
-
}
|
|
2976
|
-
function taskDisplayTitle(title, instruction, fallback) {
|
|
2977
|
-
const stored = title?.trim();
|
|
2978
|
-
if (stored) return stored;
|
|
2979
|
-
return fallbackShortTitle(instruction, fallback);
|
|
2980
|
-
}
|
|
2981
|
-
async function destinationDetails(destinations) {
|
|
2982
|
-
const selectors = new Map(destinations.map((destination) => [destinationKey(destination), destination]));
|
|
2983
|
-
if (selectors.size === 0) return /* @__PURE__ */ new Map();
|
|
2984
|
-
const rows = await getDb().select({
|
|
2985
|
-
displayName: juniorDestinations.displayName,
|
|
2986
|
-
kind: juniorDestinations.kind,
|
|
2987
|
-
providerDestinationId: juniorDestinations.providerDestinationId,
|
|
2988
|
-
providerTenantId: juniorDestinations.providerTenantId,
|
|
2989
|
-
visibility: juniorDestinations.visibility
|
|
2990
|
-
}).from(juniorDestinations).where(or(...[...selectors.values()].map((destination) => and(eq(juniorDestinations.provider, "slack"), eq(juniorDestinations.providerTenantId, destination.teamId), eq(juniorDestinations.providerDestinationId, destination.channelId)))));
|
|
2991
|
-
return new Map(rows.map((row) => {
|
|
2992
|
-
const name = row.displayName?.trim();
|
|
2993
|
-
const label = name ? row.kind === "channel" ? `#${name.replace(/^#/, "")}` : name : row.kind === "dm" ? "Direct message" : row.kind === "group" ? "Group message" : `Channel ${row.providerDestinationId}`;
|
|
2994
|
-
return [`${row.providerTenantId}:${row.providerDestinationId}`, {
|
|
2995
|
-
label,
|
|
2996
|
-
visibility: row.visibility === "public" ? "public" : "private"
|
|
2997
|
-
}];
|
|
2998
|
-
}));
|
|
2999
|
-
}
|
|
3000
|
-
function executionSummaryFields(stats) {
|
|
3001
|
-
return {
|
|
3002
|
-
lastRunStatus: stats?.lastRunStatus,
|
|
3003
|
-
...stats?.lastConversationId ? { lastConversationId: stats.lastConversationId } : void 0,
|
|
3004
|
-
...stats?.lastExecutedAtMs ? { lastRunAt: new Date(stats.lastExecutedAtMs).toISOString() } : void 0,
|
|
3005
|
-
costUsd: stats?.costUsd ?? emptyAutomationRunWindows(),
|
|
3006
|
-
runs: stats?.runs ?? emptyAutomationRunWindows(),
|
|
3007
|
-
totalRuns: stats?.totalRuns ?? 0
|
|
3008
|
-
};
|
|
3009
|
-
}
|
|
3010
|
-
function scheduledAutomationSummary(task, ownedByViewer, destination, stats, creator) {
|
|
3011
|
-
if (task.status === "deleted") throw new Error("Deleted scheduled automations cannot enter the Automations view");
|
|
3012
|
-
const nextRunAtMs = task.status === "active" ? task.runNowAtMs ?? task.nextRunAtMs : void 0;
|
|
3013
|
-
const instruction = displayText(task.task.text, "Untitled scheduled automation");
|
|
3014
|
-
return {
|
|
3015
|
-
createdAt: new Date(task.createdAtMs).toISOString(),
|
|
3016
|
-
createdBy: creatorLabel(task.createdBy),
|
|
3017
|
-
createdByEmail: creator?.email,
|
|
3018
|
-
createdByAvatarUrl: creator?.avatarUrl,
|
|
3019
|
-
destination: {
|
|
3020
|
-
channelId: task.destination.channelId,
|
|
3021
|
-
label: destination.label,
|
|
3022
|
-
teamId: task.destination.teamId,
|
|
3023
|
-
visibility: destination.visibility
|
|
3024
|
-
},
|
|
3025
|
-
id: task.id,
|
|
3026
|
-
instruction,
|
|
3027
|
-
kind: "scheduled",
|
|
3028
|
-
...executionSummaryFields(stats),
|
|
3029
|
-
...nextRunAtMs !== void 0 ? { nextRunAt: new Date(nextRunAtMs).toISOString() } : void 0,
|
|
3030
|
-
ownedByViewer,
|
|
3031
|
-
schedule: displayText(task.schedule.description, "Schedule unavailable"),
|
|
3032
|
-
status: task.status,
|
|
3033
|
-
statusReason: task.statusReason,
|
|
3034
|
-
timezone: task.schedule.timezone,
|
|
3035
|
-
credentialMode: task.credentialMode,
|
|
3036
|
-
outcomes: effectiveTaskOutcomes(task.outcomes, task.destination),
|
|
3037
|
-
title: taskDisplayTitle(task.title, instruction, "Untitled scheduled automation")
|
|
3038
|
-
};
|
|
3039
|
-
}
|
|
3040
|
-
function eventAutomationSummary(task, ownedByViewer, destination, stats, creator) {
|
|
3041
|
-
const instruction = task.task.text;
|
|
3042
|
-
const catalog = getEventCatalog();
|
|
3043
|
-
return {
|
|
3044
|
-
createdAt: new Date(task.createdAtMs).toISOString(),
|
|
3045
|
-
createdBy: creatorLabel(task.createdBy),
|
|
3046
|
-
createdByEmail: creator?.email,
|
|
3047
|
-
createdByAvatarUrl: creator?.avatarUrl,
|
|
3048
|
-
destination: {
|
|
3049
|
-
channelId: task.destination.channelId,
|
|
3050
|
-
label: destination.label,
|
|
3051
|
-
teamId: task.destination.teamId,
|
|
3052
|
-
visibility: destination.visibility
|
|
3053
|
-
},
|
|
3054
|
-
id: task.id,
|
|
3055
|
-
instruction,
|
|
3056
|
-
kind: "event",
|
|
3057
|
-
...executionSummaryFields(stats),
|
|
3058
|
-
ownedByViewer,
|
|
3059
|
-
resource: task.trigger.identifier ? `${task.trigger.label} · ${task.trigger.identifier}` : task.trigger.label,
|
|
3060
|
-
source: task.trigger.namespace,
|
|
3061
|
-
credentialMode: task.credentialMode,
|
|
3062
|
-
outcomes: effectiveTaskOutcomes(task.outcomes, task.destination),
|
|
3063
|
-
title: taskDisplayTitle(task.title, instruction, "Untitled event automation"),
|
|
3064
|
-
status: task.status === "deleted" ? "active" : task.status,
|
|
3065
|
-
statusReason: task.statusReason,
|
|
3066
|
-
trigger: describeEventAutomationTrigger(task.trigger, catalog)
|
|
3067
|
-
};
|
|
3068
|
-
}
|
|
3069
|
-
function viewerTeamIds(user) {
|
|
3070
|
-
return [...new Set(user.identities.filter((identity) => identity.provider === "slack").map((identity) => identity.providerTenantId).filter((teamId) => Boolean(teamId)))];
|
|
3071
|
-
}
|
|
3072
|
-
/** Resolve an Automation the viewer owns or can read through a public Destination. */
|
|
3073
|
-
async function resolveViewerTaskCandidate(user, id) {
|
|
3074
|
-
const [row] = await getDb().select().from(juniorAutomations).where(eq(juniorAutomations.id, id)).limit(1);
|
|
3075
|
-
if (!row) return void 0;
|
|
3076
|
-
let candidate;
|
|
3077
|
-
if (row.kind === "scheduled") {
|
|
3078
|
-
const task = parseScheduledAutomationRow(row);
|
|
3079
|
-
if (!task) return void 0;
|
|
3080
|
-
candidate = {
|
|
3081
|
-
kind: "scheduled",
|
|
3082
|
-
ownedByViewer: isAutomationCreator(user, task),
|
|
3083
|
-
task
|
|
3084
|
-
};
|
|
3085
|
-
} else {
|
|
3086
|
-
const task = parseEventAutomationRow(row);
|
|
3087
|
-
candidate = {
|
|
3088
|
-
kind: "event",
|
|
3089
|
-
ownedByViewer: isAutomationCreator(user, task),
|
|
3090
|
-
task
|
|
3091
|
-
};
|
|
3092
|
-
}
|
|
3093
|
-
const { task } = candidate;
|
|
3094
|
-
if (task.status === "deleted") return void 0;
|
|
3095
|
-
if (candidate.ownedByViewer) return candidate;
|
|
3096
|
-
const destination = (await destinationDetails([task.destination])).get(destinationKey(task.destination));
|
|
3097
|
-
return viewerTeamIds(user).includes(task.destination.teamId) && destination?.visibility === "public" ? candidate : void 0;
|
|
3098
|
-
}
|
|
3099
|
-
/** Read one current Automation summary with the Automations view access rules. */
|
|
3100
|
-
async function readViewerAutomationSummary(user, id) {
|
|
3101
|
-
const candidate = await resolveViewerTaskCandidate(user, id);
|
|
3102
|
-
if (!candidate) return void 0;
|
|
3103
|
-
return automationSummaryForCandidate(candidate);
|
|
3104
|
-
}
|
|
3105
|
-
async function automationSummaryForCandidate(candidate) {
|
|
3106
|
-
const [destinations, creators, stats] = await Promise.all([
|
|
3107
|
-
destinationDetails([candidate.task.destination]),
|
|
3108
|
-
creatorProfiles([candidate]),
|
|
3109
|
-
readAutomationExecutionSummaries([candidate.task.id])
|
|
3110
|
-
]);
|
|
3111
|
-
const destination = destinations.get(destinationKey(candidate.task.destination)) ?? {
|
|
3112
|
-
label: `Channel ${candidate.task.destination.channelId}`,
|
|
3113
|
-
visibility: "private"
|
|
3114
|
-
};
|
|
3115
|
-
const creator = creators.get(creatorKey(candidate.task.destination.teamId, candidate.task.createdBy.slackUserId));
|
|
3116
|
-
if (candidate.kind === "scheduled") return scheduledAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), creator);
|
|
3117
|
-
return eventAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), creator);
|
|
3118
|
-
}
|
|
3119
|
-
function emptyAutomationExecutionDay(date) {
|
|
3120
|
-
return {
|
|
3121
|
-
costUsd: 0,
|
|
3122
|
-
date,
|
|
3123
|
-
event: 0,
|
|
3124
|
-
scheduled: 0
|
|
3125
|
-
};
|
|
3126
|
-
}
|
|
3127
|
-
function emptyAutomationExecutionStatusDay(date) {
|
|
3128
|
-
return {
|
|
3129
|
-
blocked: 0,
|
|
3130
|
-
completed: 0,
|
|
3131
|
-
date,
|
|
3132
|
-
failed: 0
|
|
3133
|
-
};
|
|
3134
|
-
}
|
|
3135
|
-
function automationExecutionSixHours(hours, nowMs = Date.now()) {
|
|
3136
|
-
return sumUtcHoursIntoSixHours({
|
|
3137
|
-
empty: emptyAutomationExecutionDay,
|
|
3138
|
-
hours,
|
|
3139
|
-
nowMs
|
|
3140
|
-
});
|
|
3141
|
-
}
|
|
3142
|
-
function automationExecutionStatusSixHours(hours, nowMs = Date.now()) {
|
|
3143
|
-
return sumUtcHoursIntoSixHours({
|
|
3144
|
-
empty: emptyAutomationExecutionStatusDay,
|
|
3145
|
-
hours,
|
|
3146
|
-
nowMs
|
|
3147
|
-
});
|
|
3148
|
-
}
|
|
3149
|
-
/** Read a page from the full accessible Automation collection. */
|
|
3150
|
-
async function readViewerAutomations(user, input = automationListQuerySchema.parse({})) {
|
|
3151
|
-
const { rows, ...page } = await readAutomationCollection(user, input);
|
|
3152
|
-
const db = getDb();
|
|
3153
|
-
const ids = rows.map((row) => row.id);
|
|
3154
|
-
const stored = ids.length ? await db.select().from(juniorAutomations).where(inArray(juniorAutomations.id, ids)) : [];
|
|
3155
|
-
const candidates = /* @__PURE__ */ new Map();
|
|
3156
|
-
for (const row of stored) {
|
|
3157
|
-
if (row.kind === "scheduled") {
|
|
3158
|
-
const task = parseScheduledAutomationRow(row);
|
|
3159
|
-
if (task && task.status !== "deleted") candidates.set(task.id, {
|
|
3160
|
-
kind: "scheduled",
|
|
3161
|
-
task,
|
|
3162
|
-
ownedByViewer: isAutomationCreator(user, task)
|
|
3163
|
-
});
|
|
3164
|
-
continue;
|
|
3165
|
-
}
|
|
3166
|
-
const task = parseEventAutomationRow(row);
|
|
3167
|
-
if (task.status !== "deleted") candidates.set(task.id, {
|
|
3168
|
-
kind: "event",
|
|
3169
|
-
task,
|
|
3170
|
-
ownedByViewer: isAutomationCreator(user, task)
|
|
3171
|
-
});
|
|
3172
|
-
}
|
|
3173
|
-
const selected = rows.flatMap((row) => {
|
|
3174
|
-
const candidate = candidates.get(row.id);
|
|
3175
|
-
return candidate ? [candidate] : [];
|
|
3176
|
-
});
|
|
3177
|
-
const [destinations, creators] = await Promise.all([destinationDetails(selected.map(({ task }) => task.destination)), creatorProfiles(selected)]);
|
|
3178
|
-
const visible = selected.filter((candidate) => candidate.ownedByViewer || destinations.get(destinationKey(candidate.task.destination))?.visibility === "public");
|
|
3179
|
-
const collection = viewerAutomationCollection(user);
|
|
3180
|
-
const access = exists(getDb().select({ id: collection.id }).from(collection).where(eq(collection.id, juniorAutomationRuns.automationId)));
|
|
3181
|
-
const [executionDays, executionHours, stats] = await Promise.all([
|
|
3182
|
-
readAutomationExecutionDays(90, { access }),
|
|
3183
|
-
readAutomationExecutionHours(168, { access }),
|
|
3184
|
-
readAutomationExecutionSummaries(ids)
|
|
3185
|
-
]);
|
|
3186
|
-
const automations = visible.map((candidate) => {
|
|
3187
|
-
const destination = destinations.get(destinationKey(candidate.task.destination)) ?? {
|
|
3188
|
-
label: `Channel ${candidate.task.destination.channelId}`,
|
|
3189
|
-
visibility: "private"
|
|
3190
|
-
};
|
|
3191
|
-
const creator = creators.get(creatorKey(candidate.task.destination.teamId, candidate.task.createdBy.slackUserId));
|
|
3192
|
-
if (candidate.kind === "scheduled") return scheduledAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), creator);
|
|
3193
|
-
return eventAutomationSummary(candidate.task, candidate.ownedByViewer, destination, stats.get(candidate.task.id), creator);
|
|
3194
|
-
});
|
|
3195
|
-
return {
|
|
3196
|
-
executionDays,
|
|
3197
|
-
executionHours,
|
|
3198
|
-
executionSixHours: automationExecutionSixHours(executionHours),
|
|
3199
|
-
automations,
|
|
3200
|
-
...page
|
|
3201
|
-
};
|
|
3202
|
-
}
|
|
3203
|
-
/** Load deleted scheduled automations the viewer still owns, newest first. */
|
|
3204
|
-
async function readDeletedOwnedScheduledAutomations(user) {
|
|
3205
|
-
const identityIds = user.identities.map((identity) => identity.id);
|
|
3206
|
-
if (identityIds.length === 0) return [];
|
|
3207
|
-
return (await getDb().select().from(juniorAutomations).where(and(eq(juniorAutomations.kind, "scheduled"), eq(juniorAutomations.status, "deleted"), inArray(juniorAutomations.creatorIdentityId, identityIds))).orderBy(desc(juniorAutomations.createdAtMs), desc(juniorAutomations.id)).limit(TASK_EXECUTION_LIST_LIMIT)).map(parseScheduledAutomationRow).filter((task) => task?.status === "deleted");
|
|
3208
|
-
}
|
|
3209
|
-
/** Load deleted event automations the viewer still owns, newest first. */
|
|
3210
|
-
async function readDeletedOwnedEventAutomations(user) {
|
|
3211
|
-
return await listDeletedEventAutomationsCreatedBy(getDb(), user, TASK_EXECUTION_LIST_LIMIT);
|
|
3212
|
-
}
|
|
3213
|
-
function automationTitleForRun(run, automationTitles) {
|
|
3214
|
-
return automationTitles.get(`${run.kind}:${run.automationId}`) ?? run.title?.trim() ?? "Untitled task";
|
|
3215
|
-
}
|
|
3216
|
-
function addDeletedAutomationRun(args, automationTitles, automations) {
|
|
3217
|
-
const key = `${args.kind}:${args.automationId}`;
|
|
3218
|
-
if (automationTitles.has(key)) return;
|
|
3219
|
-
automationTitles.set(key, args.title);
|
|
3220
|
-
automations.push({
|
|
3221
|
-
kind: args.kind,
|
|
3222
|
-
automationId: args.automationId
|
|
3223
|
-
});
|
|
3224
|
-
}
|
|
3225
|
-
/**
|
|
3226
|
-
* Read newest runs across live viewer-visible automations and deleted automations the
|
|
3227
|
-
* viewer owns. Deleted task rows stay available so history does not depend on
|
|
3228
|
-
* conversation actor membership.
|
|
3229
|
-
*/
|
|
3230
|
-
async function readViewerAutomationRuns(user) {
|
|
3231
|
-
const [taskList, deletedScheduled, deletedEvent] = await Promise.all([
|
|
3232
|
-
getDb().select().from(viewerAutomationCollection(user)),
|
|
3233
|
-
readDeletedOwnedScheduledAutomations(user),
|
|
3234
|
-
readDeletedOwnedEventAutomations(user)
|
|
3235
|
-
]);
|
|
3236
|
-
const automationTitles = /* @__PURE__ */ new Map();
|
|
3237
|
-
const automations = [];
|
|
3238
|
-
for (const task of taskList) {
|
|
3239
|
-
automationTitles.set(`${task.kind}:${task.id}`, taskDisplayTitle(task.title ?? void 0, task.instruction, `Untitled ${task.kind} automation`));
|
|
3240
|
-
automations.push({
|
|
3241
|
-
kind: task.kind,
|
|
3242
|
-
automationId: task.id
|
|
3243
|
-
});
|
|
3244
|
-
}
|
|
3245
|
-
for (const task of deletedScheduled) {
|
|
3246
|
-
const instruction = displayText(task.task.text, "Untitled scheduled automation");
|
|
3247
|
-
addDeletedAutomationRun({
|
|
3248
|
-
kind: "scheduled",
|
|
3249
|
-
automationId: task.id,
|
|
3250
|
-
title: taskDisplayTitle(task.title, instruction, "Untitled scheduled automation")
|
|
3251
|
-
}, automationTitles, automations);
|
|
3252
|
-
}
|
|
3253
|
-
for (const task of deletedEvent) {
|
|
3254
|
-
const instruction = displayText(task.task.text, "Untitled event automation");
|
|
3255
|
-
addDeletedAutomationRun({
|
|
3256
|
-
kind: "event",
|
|
3257
|
-
automationId: task.id,
|
|
3258
|
-
title: taskDisplayTitle(task.title, instruction, "Untitled event automation")
|
|
3259
|
-
}, automationTitles, automations);
|
|
3260
|
-
}
|
|
3261
|
-
const runs = await readAutomationRuns({
|
|
3262
|
-
automationIds: automations.map((automation) => automation.automationId),
|
|
3263
|
-
limit: 101
|
|
3264
|
-
});
|
|
3265
|
-
return {
|
|
3266
|
-
runs: runs.slice(0, TASK_EXECUTION_LIST_LIMIT).map((run) => ({
|
|
3267
|
-
...run,
|
|
3268
|
-
automationTitle: automationTitleForRun(run, automationTitles)
|
|
3269
|
-
})),
|
|
3270
|
-
truncated: runs.length > TASK_EXECUTION_LIST_LIMIT
|
|
3271
|
-
};
|
|
3272
|
-
}
|
|
3273
|
-
var ViewerTaskNotFoundError = class extends Error {
|
|
3274
|
-
constructor() {
|
|
3275
|
-
super("Task was not found.");
|
|
3276
|
-
this.name = "ViewerTaskNotFoundError";
|
|
3277
|
-
}
|
|
3278
|
-
};
|
|
3279
|
-
/** Read one viewer-visible task and its newest terminal executions. */
|
|
3280
|
-
async function readViewerAutomationExecutions(user, id) {
|
|
3281
|
-
const candidate = await resolveViewerTaskCandidate(user, id);
|
|
3282
|
-
if (!candidate) throw new ViewerTaskNotFoundError();
|
|
3283
|
-
const [automation, executions, executionDays, executionHours] = await Promise.all([
|
|
3284
|
-
automationSummaryForCandidate(candidate),
|
|
3285
|
-
readAutomationExecutions({
|
|
3286
|
-
automationId: id,
|
|
3287
|
-
limit: 101
|
|
3288
|
-
}),
|
|
3289
|
-
readAutomationExecutionStatusDays({ automationId: id }),
|
|
3290
|
-
readAutomationExecutionStatusHours({ automationId: id })
|
|
3291
|
-
]);
|
|
3292
|
-
return {
|
|
3293
|
-
executionDays,
|
|
3294
|
-
executionHours,
|
|
3295
|
-
executionSixHours: automationExecutionStatusSixHours(executionHours),
|
|
3296
|
-
executions: executions.slice(0, TASK_EXECUTION_LIST_LIMIT),
|
|
3297
|
-
automation,
|
|
3298
|
-
truncated: executions.length > TASK_EXECUTION_LIST_LIMIT
|
|
3299
|
-
};
|
|
3300
|
-
}
|
|
3301
|
-
/** Read saved definitions for one viewer-visible Automation, newest first. */
|
|
3302
|
-
async function readViewerAutomationVersions(user, id) {
|
|
3303
|
-
const candidate = await resolveViewerTaskCandidate(user, id);
|
|
3304
|
-
if (!candidate) throw new ViewerTaskNotFoundError();
|
|
3305
|
-
const versions = await listAutomationVersions(getDb(), id, 101);
|
|
3306
|
-
const current = automationDefinition(candidate.task);
|
|
3307
|
-
const listed = versions.slice(0, AUTOMATION_VERSION_LIST_LIMIT);
|
|
3308
|
-
const catalog = getEventCatalog();
|
|
3309
|
-
return {
|
|
3310
|
-
versions: listed.map((version) => version.kind === "event" ? {
|
|
3311
|
-
...version,
|
|
3312
|
-
trigger: describeEventAutomationTrigger(version.definition.trigger, catalog)
|
|
3313
|
-
} : version),
|
|
3314
|
-
activeVersion: listed.find(({ definition }) => sameDefinitionValue(definition, current))?.version ?? null,
|
|
3315
|
-
truncated: versions.length > AUTOMATION_VERSION_LIST_LIMIT
|
|
3316
|
-
};
|
|
3317
|
-
}
|
|
3318
|
-
/** Name the event in a trigger, such as "opened" for `pull_request.opened`. */
|
|
3319
|
-
function eventActionLabel(eventName) {
|
|
3320
|
-
const [, ...action] = eventName.split(".");
|
|
3321
|
-
return (action.length > 0 ? action : [eventName]).join(" ").replaceAll("_", " ");
|
|
3322
|
-
}
|
|
3323
|
-
/**
|
|
3324
|
-
* Read whose access one run used from its dispatch record. Return nothing when
|
|
3325
|
-
* the record is gone, because the Automation's current setting can differ.
|
|
3326
|
-
*/
|
|
3327
|
-
async function readRunCredential(dispatchId, creator) {
|
|
3328
|
-
if (!dispatchId) return {};
|
|
3329
|
-
const dispatch = await getDispatchRecord(dispatchId);
|
|
3330
|
-
if (!dispatch) return {};
|
|
3331
|
-
const subject = dispatch.credentialSubject;
|
|
3332
|
-
if (!subject) return { credentialMode: "system" };
|
|
3333
|
-
return {
|
|
3334
|
-
credentialMode: "creator",
|
|
3335
|
-
credentialUser: subject.userId === creator.slackUserId ? creatorLabel(creator) : subject.userId
|
|
3336
|
-
};
|
|
3337
|
-
}
|
|
3338
|
-
/** Resolve the source task for one conversation when an Automation run links it. */
|
|
3339
|
-
async function readConversationSourceTask(args) {
|
|
3340
|
-
const execution = await readAutomationExecutionByConversationId({ conversationId: args.conversationId });
|
|
3341
|
-
if (!execution) return void 0;
|
|
3342
|
-
const dispatchId = getDispatchIdForConversation(args.conversationId);
|
|
3343
|
-
const base = {
|
|
3344
|
-
kind: execution.kind,
|
|
3345
|
-
...dispatchId ? { triggerMessageId: getDispatchInputMessageId(dispatchId) } : void 0
|
|
3346
|
-
};
|
|
3347
|
-
if (!args.viewer) return base;
|
|
3348
|
-
const candidate = await resolveViewerTaskCandidate(args.viewer, execution.automationId);
|
|
3349
|
-
if (!candidate) return base;
|
|
3350
|
-
const credential = await readRunCredential(dispatchId, candidate.task.createdBy);
|
|
3351
|
-
if (candidate.kind === "scheduled") {
|
|
3352
|
-
const instruction = displayText(candidate.task.task.text, "Untitled scheduled automation");
|
|
3353
|
-
const schedule = candidate.task.schedule.description.trim();
|
|
3354
|
-
return {
|
|
3355
|
-
...base,
|
|
3356
|
-
...credential,
|
|
3357
|
-
id: candidate.task.id,
|
|
3358
|
-
kind: "scheduled",
|
|
3359
|
-
label: instruction,
|
|
3360
|
-
title: taskDisplayTitle(candidate.task.title, instruction, "Untitled scheduled automation"),
|
|
3361
|
-
...schedule ? { triggerLabel: schedule } : void 0
|
|
3362
|
-
};
|
|
3363
|
-
}
|
|
3364
|
-
const instruction = displayText(candidate.task.task.text, "Untitled event automation");
|
|
3365
|
-
const { events, label } = candidate.task.trigger;
|
|
3366
|
-
return {
|
|
3367
|
-
...base,
|
|
3368
|
-
...credential,
|
|
3369
|
-
id: candidate.task.id,
|
|
3370
|
-
kind: "event",
|
|
3371
|
-
label: instruction,
|
|
3372
|
-
title: taskDisplayTitle(candidate.task.title, instruction, "Untitled event automation"),
|
|
3373
|
-
triggerLabel: events.length === 1 ? `${label} ${eventActionLabel(events[0])}` : label
|
|
3374
|
-
};
|
|
3375
|
-
}
|
|
3376
|
-
/** Delete one viewer-owned scheduled or event automation. */
|
|
3377
|
-
async function deleteViewerTask(user, id) {
|
|
3378
|
-
const candidate = await resolveViewerTaskCandidate(user, id);
|
|
3379
|
-
if (!candidate?.ownedByViewer) throw new ViewerTaskNotFoundError();
|
|
3380
|
-
if (candidate.kind === "scheduled") try {
|
|
3381
|
-
await deleteViewerScheduledAutomation(getDb(), user, id);
|
|
3382
|
-
return;
|
|
3383
|
-
} catch (error) {
|
|
3384
|
-
if (error instanceof PersonalScheduledAutomationNotFoundError) throw new ViewerTaskNotFoundError();
|
|
3385
|
-
throw error;
|
|
3386
|
-
}
|
|
3387
|
-
await deleteEventAutomation(getDb(), id);
|
|
3388
|
-
}
|
|
3389
2897
|
// src/chat/slack/assistant-thread/status-send.ts
|
|
3390
2898
|
/**
|
|
3391
2899
|
* Slack's assistant loading UI accepts both `status` and `loading_messages`,
|
|
@@ -3896,6 +3404,21 @@ function formatSlackConversationRedactedLabel(context) {
|
|
|
3896
3404
|
if (!context) return void 0;
|
|
3897
3405
|
return formatSlackConversationTypeLabel(context.type);
|
|
3898
3406
|
}
|
|
3407
|
+
// src/chat/slack/avatar.ts
|
|
3408
|
+
const AVATAR_SAVE_INTERVAL_MS = 864e5;
|
|
3409
|
+
/**
|
|
3410
|
+
* Save the Slack photo of one Slack Actor on their Slack Identity. Slack
|
|
3411
|
+
* Turns call this, so the saved photo follows profile changes within a day.
|
|
3412
|
+
*/
|
|
3413
|
+
async function saveSlackActorAvatar(actor) {
|
|
3414
|
+
const state = getStateAdapter();
|
|
3415
|
+
const savedKey = `junior:slack:avatar-saved:${actor.teamId}:${actor.userId}`;
|
|
3416
|
+
if (await state.get(savedKey)) return;
|
|
3417
|
+
const profile = await lookupSlackUser(actor.teamId, actor.userId);
|
|
3418
|
+
if (!profile) return;
|
|
3419
|
+
await saveActorAvatar(getSqlExecutor(), actor, profile.avatarUrl ?? null);
|
|
3420
|
+
await state.set(savedKey, true, AVATAR_SAVE_INTERVAL_MS);
|
|
3421
|
+
}
|
|
3899
3422
|
// src/chat/services/provider-default-config.ts
|
|
3900
3423
|
const GITHUB_REPO_PART = String.raw`[A-Za-z0-9_.-]*[A-Za-z0-9_-]`;
|
|
3901
3424
|
const GITHUB_REPO_RE = new RegExp(String.raw`^\s*(?:set|use)\s+(?:the\s+)?default\s+(?:github\s+)?repo(?:sitory)?\s+(?:to|as)\s+(${GITHUB_REPO_PART}/${GITHUB_REPO_PART})(?:\s+for\s+this\s+channel)?[.!?]?\s*$`, "i");
|
|
@@ -4136,8 +3659,10 @@ function createSlackTurn(deps) {
|
|
|
4136
3659
|
executionActor = options.execution.dispatch.actor;
|
|
4137
3660
|
credentialContext = options.execution.credentialContext;
|
|
4138
3661
|
} else {
|
|
4139
|
-
|
|
4140
|
-
|
|
3662
|
+
const slackActor = await ensureSlackMessageActorIdentity(message, teamId, lookupSlackUser);
|
|
3663
|
+
await saveSlackActorAvatar(slackActor);
|
|
3664
|
+
executionActor = slackActor;
|
|
3665
|
+
credentialContext = credentialContextForActor(slackActor);
|
|
4141
3666
|
}
|
|
4142
3667
|
if (!executionActor || !credentialContext) throw new Error("Slack reply execution requires an actor");
|
|
4143
3668
|
const actor = "userId" in executionActor ? executionActor : void 0;
|
|
@@ -6444,6 +5969,7 @@ function createWebSteeringDrain(args) {
|
|
|
6444
5969
|
text: normalizeConversationText(message.input.text),
|
|
6445
5970
|
createdAtMs: message.createdAtMs,
|
|
6446
5971
|
author,
|
|
5972
|
+
...message.input.authorIdentityId ? { authorIdentityId: message.input.authorIdentityId } : void 0,
|
|
6447
5973
|
meta: {
|
|
6448
5974
|
...attachments.length ? { attachments } : void 0,
|
|
6449
5975
|
explicitMention: true,
|
|
@@ -6526,6 +6052,14 @@ async function completeCancelledConversationTurn(args) {
|
|
|
6526
6052
|
await args.acknowledge();
|
|
6527
6053
|
}
|
|
6528
6054
|
/**
|
|
6055
|
+
* Return the author Identity id that every batched input shares.
|
|
6056
|
+
* A batch with more than one author has no single author yet.
|
|
6057
|
+
*/
|
|
6058
|
+
function sharedAuthorIdentityId(messages) {
|
|
6059
|
+
const ids = new Set(messages.map((message) => message.input.authorIdentityId));
|
|
6060
|
+
return ids.size === 1 ? [...ids][0] : void 0;
|
|
6061
|
+
}
|
|
6062
|
+
/**
|
|
6529
6063
|
* Create the shared mailbox worker for web and event Turns.
|
|
6530
6064
|
*/
|
|
6531
6065
|
function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
@@ -6548,6 +6082,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
|
6548
6082
|
let inputMessageIds = [];
|
|
6549
6083
|
let event;
|
|
6550
6084
|
let batchEvents = [];
|
|
6085
|
+
let authorIdentityId;
|
|
6551
6086
|
const storedConversation = await getConversationStore().get({ conversationId: context.conversationId });
|
|
6552
6087
|
const mailboxDestination = resolved.kind === "mailbox" ? resolved.batch[0]?.message.destination : void 0;
|
|
6553
6088
|
const destination = resolveConversationDestination({
|
|
@@ -6568,6 +6103,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
|
6568
6103
|
userMessageId = first.message.inboundMessageId;
|
|
6569
6104
|
turnId = buildDeterministicTurnId(userMessageId);
|
|
6570
6105
|
inputMessageIds = resolved.batch.map((entry) => entry.message.inboundMessageId);
|
|
6106
|
+
authorIdentityId = sharedAuthorIdentityId(messages);
|
|
6571
6107
|
turnInputFacts = first;
|
|
6572
6108
|
} else turnId = resolved.turnId;
|
|
6573
6109
|
const persisted = await getPersistedThreadState(context.conversationId);
|
|
@@ -6654,6 +6190,7 @@ function createConversationTurnWorker(agentRunner, deliverMessage) {
|
|
|
6654
6190
|
text: normalizeConversationText(text),
|
|
6655
6191
|
createdAtMs: startedAtMs,
|
|
6656
6192
|
author,
|
|
6193
|
+
...authorIdentityId ? { authorIdentityId } : void 0,
|
|
6657
6194
|
meta: {
|
|
6658
6195
|
...attachments.length ? { attachments } : void 0,
|
|
6659
6196
|
explicitMention: true,
|
|
@@ -7028,12 +6565,14 @@ function createConversationWork(options) {
|
|
|
7028
6565
|
},
|
|
7029
6566
|
runTurn: runtime.runDispatchTurn
|
|
7030
6567
|
});
|
|
7031
|
-
const
|
|
6568
|
+
const childWorker = createChildConversationWorker(options.agentRunner, {
|
|
7032
6569
|
enqueueResult: async (invocation) => await enqueueAgentInvocationResult(invocation, {
|
|
7033
6570
|
queue: options.queue,
|
|
7034
6571
|
state: options.state
|
|
7035
6572
|
}),
|
|
7036
|
-
maxAgentDepth: botConfig.maxAgentDepth
|
|
6573
|
+
maxAgentDepth: botConfig.maxAgentDepth,
|
|
6574
|
+
queue: options.queue,
|
|
6575
|
+
state: options.state
|
|
7037
6576
|
});
|
|
7038
6577
|
const conversationTurnWorker = createConversationTurnWorker(options.agentRunner, createSlackSystemTurnDelivery({
|
|
7039
6578
|
getSlackAdapter: options.getSlackAdapter,
|
|
@@ -7048,15 +6587,9 @@ function createConversationWork(options) {
|
|
|
7048
6587
|
const run = async (context) => {
|
|
7049
6588
|
const skipped = await skipInputBeforeConversationStop(context);
|
|
7050
6589
|
if (skipped) return skipped;
|
|
7051
|
-
|
|
7052
|
-
queue: options.queue,
|
|
7053
|
-
state: options.state
|
|
7054
|
-
});
|
|
7055
|
-
if (childInput) return childInput;
|
|
6590
|
+
if ((await options.conversationStore.get({ conversationId: context.conversationId }))?.parentConversationId) return await childWorker(context);
|
|
7056
6591
|
const mailboxTurn = await resolveMailboxTurnWork(context);
|
|
7057
6592
|
if (mailboxTurn) return await conversationTurnWorker(context, await withAgentInvocationResultActors(mailboxTurn));
|
|
7058
|
-
const invocationId = await resolveAgentInvocationId(context);
|
|
7059
|
-
if (invocationId) return await invocationWorker(context, invocationId);
|
|
7060
6593
|
const dispatchId = await resolveAgentDispatchId(context);
|
|
7061
6594
|
if (dispatchId) return await dispatchWorker(context, dispatchId);
|
|
7062
6595
|
return await destinationWorker(context);
|
|
@@ -7834,7 +7367,7 @@ async function readLastUserMessageAtByConversation(db, conversationIds) {
|
|
|
7834
7367
|
const rows = await db.select({
|
|
7835
7368
|
conversationId: juniorConversationEvents.conversationId,
|
|
7836
7369
|
lastUserMessageAt: sql`max(${juniorConversationEvents.createdAt})`
|
|
7837
|
-
}).from(juniorConversationEvents).where(and(inArray(juniorConversationEvents.conversationId, conversationIds), or(and(eq(juniorConversationEvents.type, "user_message"), eq(authority, "instruction"), sql`${actorPlatform} in ('slack', 'local', 'web')`), and(eq(juniorConversationEvents.type, "message"), eq(role, "user"),
|
|
7370
|
+
}).from(juniorConversationEvents).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).where(and(inArray(juniorConversationEvents.conversationId, conversationIds), or(and(eq(juniorConversationEvents.type, "user_message"), eq(authority, "instruction"), sql`${actorPlatform} in ('slack', 'local', 'web')`), and(eq(juniorConversationEvents.type, "message"), eq(role, "user"), eq(juniorIdentities.kind, "user"))))).groupBy(juniorConversationEvents.conversationId);
|
|
7838
7371
|
return new Map(rows.map((row) => [row.conversationId, new Date(row.lastUserMessageAt).toISOString()]));
|
|
7839
7372
|
}
|
|
7840
7373
|
// src/api/conversations/activity-preview.ts
|
|
@@ -8010,6 +7543,7 @@ async function attachActivityPreviewMentions(executor, previews) {
|
|
|
8010
7543
|
}
|
|
8011
7544
|
// src/api/conversations/participants.ts
|
|
8012
7545
|
const identityColumns = {
|
|
7546
|
+
avatarUrl: juniorIdentities.avatarUrl,
|
|
8013
7547
|
displayName: juniorIdentities.displayName,
|
|
8014
7548
|
email: juniorIdentities.email,
|
|
8015
7549
|
handle: juniorIdentities.handle,
|
|
@@ -8020,11 +7554,13 @@ const identityColumns = {
|
|
|
8020
7554
|
userEmail: juniorUsers.primaryEmail,
|
|
8021
7555
|
userId: juniorIdentities.userId
|
|
8022
7556
|
};
|
|
8023
|
-
function participantIdentity(row) {
|
|
7557
|
+
function participantIdentity(row, userAvatarUrls) {
|
|
7558
|
+
const avatarUrl = identityAvatarUrl(row, userAvatarUrls);
|
|
8024
7559
|
const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
|
|
8025
7560
|
const email = row.email?.trim() || row.userEmail?.trim();
|
|
8026
7561
|
const slack = row.provider === "slack";
|
|
8027
7562
|
const participant = {
|
|
7563
|
+
...avatarUrl ? { avatarUrl } : void 0,
|
|
8028
7564
|
...email ? { email } : void 0,
|
|
8029
7565
|
...fullName ? { fullName } : void 0,
|
|
8030
7566
|
...slack && row.providerSubjectId ? { slackUserId: row.providerSubjectId } : void 0,
|
|
@@ -8042,10 +7578,11 @@ async function readConversationParticipants(db, conversationIds) {
|
|
|
8042
7578
|
conversationId: juniorConversationEvents.conversationId,
|
|
8043
7579
|
seq: juniorConversationEvents.seq,
|
|
8044
7580
|
...identityColumns
|
|
8045
|
-
}).from(juniorConversationEvents).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(inArray(juniorConversationEvents.conversationId, [...conversationIds])).orderBy(asc(juniorConversationEvents.conversationId), asc(juniorConversationEvents.actorIdentityId), asc(juniorConversationEvents.seq))]);
|
|
7581
|
+
}).from(juniorConversationEvents).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversationEvents.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(inArray(juniorConversationEvents.conversationId, [...conversationIds]), ne(juniorIdentities.kind, "system"))).orderBy(asc(juniorConversationEvents.conversationId), asc(juniorConversationEvents.actorIdentityId), asc(juniorConversationEvents.seq))]);
|
|
7582
|
+
const userAvatarUrls = await readUserAvatarUrls(db, [...rootRows, ...eventRows].flatMap((row) => row.userId ? [row.userId] : []));
|
|
8046
7583
|
const ordered = /* @__PURE__ */ new Map();
|
|
8047
7584
|
const add = (conversationId, row) => {
|
|
8048
|
-
const participant = participantIdentity(row);
|
|
7585
|
+
const participant = participantIdentity(row, userAvatarUrls);
|
|
8049
7586
|
if (!participant) return;
|
|
8050
7587
|
const group = ordered.get(conversationId) ?? {
|
|
8051
7588
|
identities: [],
|
|
@@ -8275,4 +7812,4 @@ async function readConversationFeedFromSql(options = {}) {
|
|
|
8275
7812
|
async function readConversationFeed(options = {}) {
|
|
8276
7813
|
return conversationFeedSchema.parse(await readConversationFeedFromSql(options));
|
|
8277
7814
|
}
|
|
8278
|
-
export {
|
|
7815
|
+
export { stopSlackThread as A, actOnReviewItem as B, isAllowedSignInEmail as C, buildSlackInboundMessage as D, recordSkippedConversationMessage as E, bindMessageAuthorIdentityId as F, AGENT_DISPATCH_MAX_AGE_MS as G, readReviewItemForUser as H, startProcessingReaction as I, readHealthReport as J, enqueueAgentDispatch as K, stripLeadingBotMention as L, withNormalizedThreadId as M, stopConversationTurn as N, clearSlackPendingReactions as O, THREAD_OPTOUT_ACK as P, parseContent as R, isAllowedDomain as S, resolveAllowedDomains as T, reviewItemChange as U, closeReviewItemsAnsweredByReply as V, getAttachmentStorage as W, createProductionConversationWorkOptions as _, resolveMentions as a, createSlackWebApiStatusSender as b, readConversationAuxiliaryCostsFromSql as c, notCopiedByFork as d, childAgentTotalsReport as f, resolveSlackTeamDomains as g, conversationSummaryFromStoredConversation as h, attachConversationMentions as i, normalizeIncomingSlackThreadId as j, isSlackMessageStopped as k, ConversationForkError as l, conversationEventHistory as m, readConversationRecordFromSql as n, readFirstHumanMessageTexts as o, readConversationTreeMetricsFromSql as p, GET as q, readConversationParticipants as r, listConversationSidebarAnnotations as s, readConversationFeed as t, forkConversation as u, createProductionSlackWebhookServices as v, readEnvList as w, assertNoLegacyAllowedDomains as x, getProductionSlackAdapter as y, ReviewItemActionError as z };
|