@sentry/junior 0.266.0 → 0.267.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{actor-tlxzqaRe.js → actor-DZSokpwj.js} +8 -1
- package/dist/{adapter-eRFA_naf.js → adapter-BybMEezj.js} +2 -2
- package/dist/{admin-tILpP51p.js → admin-DOs1_EiW.js} +1 -1
- package/dist/{agent-runner-BtYK7bht.js → agent-runner-DgNATN4D.js} +872 -226
- package/dist/api/schema.d.ts +198 -23
- package/dist/api/schema.js +5 -4
- package/dist/api.d.ts +6 -1
- package/dist/api.js +130 -1205
- package/dist/{app-Csbi9dW7.d.ts → app-DAB3fzyp.d.ts} +21 -3
- package/dist/app.d.ts +2 -2
- package/dist/app.js +1610 -353
- package/dist/{assistant-reply-UyzAfZQo.js → assistant-reply-CT7VcXQe.js} +1 -1
- package/dist/{automation-BKpY9LPa.js → automation-CmeFBSEh.js} +42 -14
- package/dist/{briefs-DgfWMU7e.js → briefs-CWKjka7v.js} +4 -4
- package/dist/{catalog-runtime-DAJjAbSx.js → catalog-runtime-cMlAI5LY.js} +1 -1
- package/dist/cli/chat.js +15 -15
- package/dist/cli/check.js +3 -3
- package/dist/cli/init.js +0 -1
- package/dist/cli/main.js +3 -3
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +4 -4
- package/dist/cli/upgrade.d.ts +1 -1
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-B0YZfxNy.js → client-BjgbMawj.js} +2 -2
- package/dist/{config-C0UwYqDK.js → config-CdB6yJj5.js} +32 -10
- package/dist/{connections-Bwu3VORi.js → connections-RE_65jzK.js} +13 -12
- package/dist/{conversation-DF4rUcjb.js → conversation-DSo310Ps.js} +8 -1
- package/dist/{conversation-privacy-CY7JVjxn.js → conversation-privacy-BzgJC5k0.js} +1 -1
- package/dist/{credential-sync-VGAMUTGp.js → credential-sync-DhEGEpgq.js} +15 -15
- package/dist/{credentialed-enF5Bf3y.js → credentialed-CIGwoLU2.js} +2905 -1024
- package/dist/{db-BnRqQ7G5.d.ts → db-Bg-zC3r5.d.ts} +1 -1
- package/dist/{executor-TnWbvL09.js → executor-VYrx_K4w.js} +7 -4
- package/dist/{images-BKQqSi5W.js → images-CecjXAAz.js} +3 -3
- package/dist/instrumentation.js +6 -2
- package/dist/{job-queue-DBpHmKnd.js → job-queue-C0BZSa7r.js} +128 -3
- package/dist/{web-input-B-bMjD1V.js → list-1XQP-o6K.js} +1374 -71
- package/dist/{logging-Ce0mXZ8J.js → logging-DuZ1A4ab.js} +11 -3
- package/dist/{message-VZ3DFpGb.js → message-NKSbbnA6.js} +4 -4
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-callback-server-CpqWLjVY.js → oauth-callback-server-qAHLPtml.js} +2 -2
- package/dist/{prompt-BcCGlmg2.js → prompt-BW9jFsqZ.js} +1 -1
- package/dist/{registry-1BEIPdbT.js → registry-FQutO926.js} +2 -2
- package/dist/{resolve-BSHHkEGE.js → resolve-Bk4yeuHy.js} +3 -3
- package/dist/{result-CwOpzqdh.js → result-B9e9ATOU.js} +9 -9
- package/dist/{runner-CEaX04kz.js → runner-C3S2ObH-.js} +11 -11
- package/dist/{sandbox-egress-signals-BwtGHwqa.js → sandbox-egress-signals-DqpGzzMB.js} +1 -1
- package/dist/{schema-RsaBHxRH.js → schema-X7Fj4fv1.js} +3 -2
- package/dist/{service-account-Dg4lIbHm.js → service-account-BQasl-qq.js} +3 -3
- package/dist/{service-account-6wC76eBZ.js → service-account-Do30HUNv.js} +3 -3
- package/dist/{session-stop-tZOw9AJD.js → session-stop-DkOjt46_.js} +45 -128
- package/dist/{skills-DzyGN7wd.js → skills-CQHrwfOa.js} +2 -2
- package/dist/{spawn-6rokq7l_.js → spawn-BHuqN14f.js} +2 -2
- package/dist/{store-DcKkzrdt.js → store-6EetVP-m.js} +3 -3
- package/dist/{store-CG0VJyye.d.ts → store-B8CABbmR.d.ts} +10 -2
- package/dist/store-DvNZIsCr.js +295 -0
- package/dist/{task-queue-Bg4RS2Gy.js → task-queue-B0DLgd2P.js} +10 -5
- package/dist/{task-runner-8N5LIkSg.js → task-runner-B-PD_JOj.js} +25 -13
- package/dist/{tasks-DDT-J3Rf.js → tasks-0ilmuCux.js} +1438 -44
- package/dist/{thread-state-cprEaxgl.js → thread-state-DRmdJ2QN.js} +5 -5
- package/dist/{turn-failure-response-C8sMdsdX.js → turn-failure-response-G11cs2gE.js} +9 -9
- package/dist/{turn-lifecycle-DaBVoz7h.js → turn-lifecycle-OzLIwvi1.js} +1 -1
- package/dist/{turn-session-routing-a9CHX-w6.js → turn-session-routing-Dyut9l7Y.js} +1 -1
- package/dist/user-input-request-BDFBdF8r.js +116 -0
- package/dist/{validation-C2lllinq.js → validation-CxEmt1id.js} +1 -1
- package/dist/{version-oaLfxxnf.js → version-Bapedn4A.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-BpE6Z3qf.js → viewer-DrN3oC2X.js} +21 -10
- package/dist/{work-VX1SHWha.js → work-DUQoNcqP.js} +10 -10
- package/dist/{worker-hook-y_Y.js → worker-D1l4Ugk0.js} +30 -19
- package/migrations/0064_any_resource_event_triggers.sql +2 -0
- package/migrations/meta/0064_snapshot.json +3505 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +20 -5
- package/dist/store-FALy-JQb.js +0 -1424
package/dist/api.js
CHANGED
|
@@ -1,40 +1,39 @@
|
|
|
1
|
-
import { D as AutomationConflictError, E as sameDefinitionValue,
|
|
2
|
-
import { p as escapeXml } from "./client-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { n as
|
|
10
|
-
import {
|
|
11
|
-
import { An as createPluginLogger, C as listWorkspaces, E as WorkspaceValidationError, Fn as conversationHasParticipantUser, Hn as readConversationEventPage, In as conversationIsMemberRoot, Ln as conversationNotArchivedForEmail, M as create, Mn as conversationArchiveTimes, N as hash, Nn as conversationArchivedForEmail, Pn as conversationArchivedForUser, Qn as listConversationAnnotationsById, Rn as conversationNotArchivedForUser, T as updateWorkspace, Un as buildSentryConversationUrl, Vn as readLatestConversationBrief, Zn as listConversationAnnotations, an as withSlackRetries, b as deleteWorkspace, en as getSlackClient, j as SANDBOX_RUNTIME, jn as readConversationAccessFromSql, n as createUserTokenStore, nr as createPluginCache, p as getPlugins, tr as lookupSlackUser, w as listWorkspacesByRepository, wt as workspaceRepoCheckoutPath, x as getWorkspace, y as createWorkspace, zn as viewerConversationMembership } from "./credentialed-enF5Bf3y.js";
|
|
1
|
+
import { D as AutomationConflictError, E as sameDefinitionValue, O as automationRevision, S as automationDefinition, T as readAutomationVersion, a as saveScheduledAutomation, j as automationEventCatalog, k as requireAutomationRevision, v as saveEventAutomation, y as setEventAutomationStatus } from "./tasks-0ilmuCux.js";
|
|
2
|
+
import { p as escapeXml } from "./client-BjgbMawj.js";
|
|
3
|
+
import { m as logInfo, p as logException } from "./logging-DuZ1A4ab.js";
|
|
4
|
+
import { A as resolveRootConversationId, O as agentTurnUsageSchema, a as resolveViewerUser, f as getConversationStore, k as hasAgentTurnUsage, m as getSqlExecutor, p as getDb, s as updateViewerDisplayName, t as findUserByEmail } from "./viewer-DrN3oC2X.js";
|
|
5
|
+
import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, s as juniorConversations } from "./agent-invocations-CxKldAeB.js";
|
|
6
|
+
import { g as juniorApiTokens, h as juniorConversationEvents, m as juniorConversationMetrics, u as juniorConversationParticipants, v as juniorCodeChanges, y as juniorCodeRepositories } from "./executor-VYrx_K4w.js";
|
|
7
|
+
import { n as pluginCatalogRuntime } from "./catalog-runtime-cMlAI5LY.js";
|
|
8
|
+
import { I as webUserInputAnswerNote, K as editEventAutomation, L as parseSlackThreadId, O as persistSlackUserInputReply, S as readLiveAttachment, U as editScheduledAutomation, X as resolveTaskOutcomes, Z as listOwnedWatches, at as conversationSubtreeColumns, it as conversationSubtree, m as readStats, nt as listChildAgents, q as AutomationEditError } from "./agent-runner-DgNATN4D.js";
|
|
9
|
+
import { C as listWorkspaces, E as WorkspaceValidationError, Et as workspaceRepoCheckoutPath, Gn as readConversationEventPage, Kn as buildSentryConversationUrl, M as hash, Nn as createPluginLogger, Pn as readConversationAccessFromSql, T as updateWorkspace, Wn as readLatestConversationBrief, ar as createPluginCache, b as deleteWorkspace, er as listConversationAnnotations, ir as lookupSlackUser, j as create, n as createUserTokenStore, p as getPlugins, un as runWithWorkspaceTeamId, w as listWorkspacesByRepository, wt as SANDBOX_RUNTIME, x as getWorkspace, y as createWorkspace } from "./credentialed-CIGwoLU2.js";
|
|
10
|
+
import { i as getStateAdapter } from "./adapter-BybMEezj.js";
|
|
12
11
|
import { t as getFirstRunAtMs } from "./cadence-R7Em4RjE.js";
|
|
13
|
-
import { A as
|
|
14
|
-
import { h as getChildAgentLabel } from "./store-
|
|
15
|
-
import { d as getEventCatalog
|
|
16
|
-
import { s as getSandboxResources, t as getCachedSnapshot } from "./resolve-
|
|
17
|
-
import { a as getVercelConversationWorkQueue } from "./job-queue-
|
|
18
|
-
import { a as cancelHumanFacingPendingMessages, h as getConversation } from "./store-
|
|
19
|
-
import { n as finishServiceAccountSignIn, o as serviceAccountPlugins, r as getServiceAccountStatus, s as startServiceAccountSignIn, t as disconnectServiceAccount } from "./service-account-
|
|
20
|
-
import { M as
|
|
21
|
-
import { C as skillReportsSchema, a as fillUtcHours, b as pluginsSchema, c as sumUtcHoursIntoSixHours, d as trailingUtcSixHourWindow, f as utcDayKey, h as pluginOperationalReportFeedSchema, i as fillUtcDays, l as trailingUtcDayWindow, m as healthReportSchema, n as DAY_MS$1, o as fillUtcSixHours, p as utcHourKey, r as HOUR_MS$1, s as startOfUtcHour, u as trailingUtcHourWindow, x as runtimeInfoReportSchema } from "./web-message-id-CJSUiksE.js";
|
|
22
|
-
import {
|
|
23
|
-
import { F as conversationAggregateColumns, I as conversationRangeColumns, M as readAutomationTitlesByConversationId, P as conversationActiveDaysColumn } from "./task-runner-
|
|
24
|
-
import { d as legacyWebMailboxMetadataSchema } from "./result-
|
|
25
|
-
import { a as startConnection, l as getWebAuthorization, n as isConnectableProvider, r as listConnections, t as disconnectConnection, u as htmlCallbackResponse } from "./connections-
|
|
26
|
-
import { t as decodeInputImages } from "./images-
|
|
27
|
-
import { A as activityHours$1, C as personParamsSchema, D as locationDirectoryReportSchema, E as locationDetailReportSchema, H as codePersonReportSchema, I as disconnectConnectionResponseSchema, L as startConnectionResponseSchema, N as connectionListSchema, O as locationParamsSchema, P as connectionParamsSchema, Q as apiErrorSchema, S as actorProfileReportSchema, U as codeRepositoryParamsSchema, V as codeOverviewReportSchema, W as codeRepositoryReportSchema, X as stopConversationTurnResponseSchema, Y as conversationChildAgentsReportSchema, Z as conversationStatsReportSchema, _ as personalTokenListSchema, a as workspaceBodySchema, b as revokePersonalTokenResponseSchema, d as serviceAccountListSchema, f as serviceAccountParamsSchema, g as createdPersonalTokenSchema, h as createPersonalTokenBodySchema, i as deleteWorkspaceResponseSchema, j as activitySixHours$1, k as activityDays$1, l as workspaceSchema, m as startServiceAccountSignInResponseSchema, n as statsReportSchema, o as workspaceListSchema, q as conversationWatchesReportSchema, s as workspaceParamsSchema, u as disconnectServiceAccountResponseSchema, w as personalSpendReportSchema, x as actorDirectoryReportSchema, y as personalTokenParamsSchema } from "./schema-
|
|
12
|
+
import { A as createConversationMessageBodySchema, G as INPUT_IMAGE_TYPES, M as forkConversationResponseSchema, N as steerConversationPendingMessagesBodySchema, P as steerConversationPendingMessagesResponseSchema, W as readMessageAttachments, _ as conversationParamsSchema, a as cancelConversationPendingMessagesBodySchema, c as conversationAttachmentParamsSchema, d as conversationDetailReportSchema, g as conversationFeedSchema, h as conversationFeedQuerySchema, i as archiveConversationResponseSchema, j as forkConversationBodySchema, k as createConversationBodySchema, m as conversationEventsQuerySchema, o as cancelConversationPendingMessagesResponseSchema, p as conversationEventPageSchema, r as archiveConversationBodySchema, t as acceptedConversationMessageSchema, u as conversationDetailQuerySchema, x as conversationPendingMessagesReportSchema } from "./conversation-DSo310Ps.js";
|
|
13
|
+
import { h as getChildAgentLabel } from "./store-6EetVP-m.js";
|
|
14
|
+
import { d as getEventCatalog } from "./session-stop-DkOjt46_.js";
|
|
15
|
+
import { s as getSandboxResources, t as getCachedSnapshot } from "./resolve-Bk4yeuHy.js";
|
|
16
|
+
import { a as getVercelConversationWorkQueue } from "./job-queue-C0BZSa7r.js";
|
|
17
|
+
import { E as steerWebPendingMessages, a as cancelHumanFacingPendingMessages, g as getConversationWorkState, h as getConversation } from "./store-DvNZIsCr.js";
|
|
18
|
+
import { n as finishServiceAccountSignIn, o as serviceAccountPlugins, r as getServiceAccountStatus, s as startServiceAccountSignIn, t as disconnectServiceAccount } from "./service-account-Do30HUNv.js";
|
|
19
|
+
import { F as weekdaySchema, M as ScheduleIntentError, N as compileScheduleIntent, _ as automationSummarySchema, b as automationVersionListSchema, c as automationLifecycleSchema, d as automationParamsSchema, f as automationRunListSchema, g as automationSchedulePreviewSchema, h as automationScheduleIntentSchema, i as automationExecutionListSchema, l as automationListQuerySchema, n as automationEditSchema, r as automationEventCatalogSchema, t as automationEditErrorSchema, u as automationListSchema, v as automationUpdateSchema, x as automationVersionParamsSchema, y as automationVersionActivateSchema } from "./automation-CmeFBSEh.js";
|
|
20
|
+
import { C as skillReportsSchema, a as fillUtcHours, b as pluginsSchema, c as sumUtcHoursIntoSixHours, d as trailingUtcSixHourWindow, f as utcDayKey, h as pluginOperationalReportFeedSchema, i as fillUtcDays, l as trailingUtcDayWindow, m as healthReportSchema, n as DAY_MS$1, o as fillUtcSixHours, p as utcHourKey, r as HOUR_MS$1, s as startOfUtcHour, t as webMessageId, u as trailingUtcHourWindow, x as runtimeInfoReportSchema } from "./web-message-id-CJSUiksE.js";
|
|
21
|
+
import { A as readViewerAutomationRuns, D as deleteViewerTask, E as ViewerTaskNotFoundError, H as clearSlackPendingReactions, L as isAllowedSignInEmail, M as readViewerAutomationVersions, N as readViewerAutomations, O as readConversationSourceTask, P as resolveViewerTaskCandidate, _ as appendAndEnqueueWebMessage, a as resolveMentions, c as readConversationAuxiliaryCostsFromSql, d as notCopiedByFork, et as getAttachmentStorage, f as childAgentTotalsReport, g as resolveSlackTeamDomains, h as conversationSummaryFromStoredConversation, i as attachConversationMentions, it as readHealthReport, j as readViewerAutomationSummary, k as readViewerAutomationExecutions, l as ConversationForkError, m as conversationEventHistory, n as readConversationRecordFromSql, o as readFirstHumanMessageTexts, p as readConversationTreeMetricsFromSql, q as stopConversationTurn, r as readConversationParticipants, s as listConversationSidebarAnnotations, t as readConversationFeed, u as forkConversation, w as getProductionSlackAdapter, x as webActorFromEmail, y as createAndEnqueueConversation } from "./list-1XQP-o6K.js";
|
|
22
|
+
import { F as conversationAggregateColumns, I as conversationRangeColumns, M as readAutomationTitlesByConversationId, P as conversationActiveDaysColumn } from "./task-runner-B-PD_JOj.js";
|
|
23
|
+
import { d as legacyWebMailboxMetadataSchema } from "./result-B9e9ATOU.js";
|
|
24
|
+
import { a as startConnection, l as getWebAuthorization, n as isConnectableProvider, r as listConnections, t as disconnectConnection, u as htmlCallbackResponse } from "./connections-RE_65jzK.js";
|
|
25
|
+
import { t as decodeInputImages } from "./images-CecjXAAz.js";
|
|
26
|
+
import { A as activityHours$1, C as personParamsSchema, D as locationDirectoryReportSchema, E as locationDetailReportSchema, H as codePersonReportSchema, I as disconnectConnectionResponseSchema, L as startConnectionResponseSchema, N as connectionListSchema, O as locationParamsSchema, P as connectionParamsSchema, Q as apiErrorSchema, S as actorProfileReportSchema, U as codeRepositoryParamsSchema, V as codeOverviewReportSchema, W as codeRepositoryReportSchema, X as stopConversationTurnResponseSchema, Y as conversationChildAgentsReportSchema, Z as conversationStatsReportSchema, _ as personalTokenListSchema, a as workspaceBodySchema, b as revokePersonalTokenResponseSchema, d as serviceAccountListSchema, f as serviceAccountParamsSchema, g as createdPersonalTokenSchema, h as createPersonalTokenBodySchema, i as deleteWorkspaceResponseSchema, j as activitySixHours$1, k as activityDays$1, l as workspaceSchema, m as startServiceAccountSignInResponseSchema, n as statsReportSchema, o as workspaceListSchema, q as conversationWatchesReportSchema, s as workspaceParamsSchema, u as disconnectServiceAccountResponseSchema, w as personalSpendReportSchema, x as actorDirectoryReportSchema, y as personalTokenParamsSchema } from "./schema-X7Fj4fv1.js";
|
|
28
27
|
import { Hono } from "hono";
|
|
29
28
|
import path from "node:path";
|
|
30
29
|
import { z } from "zod";
|
|
31
30
|
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
32
|
-
import { and, asc, desc, eq, gt, gte,
|
|
33
|
-
import {
|
|
31
|
+
import { and, asc, desc, eq, gt, gte, isNotNull, isNull, lte, notExists, or, sql } from "drizzle-orm";
|
|
32
|
+
import { pluginUserPageContentSchema, pluginUserPageCursorSchema, pluginUserPageFilterSchema, pluginUserPageLinksSchema, pluginUserPageQuerySchema } from "@sentry/junior-plugin-api";
|
|
34
33
|
import { alias } from "drizzle-orm/pg-core";
|
|
35
34
|
import { readFileSync } from "node:fs";
|
|
36
|
-
import { HTTPException } from "hono/http-exception";
|
|
37
35
|
import { bodyLimit } from "hono/body-limit";
|
|
36
|
+
import { HTTPException } from "hono/http-exception";
|
|
38
37
|
import { zValidator } from "@hono/zod-validator";
|
|
39
38
|
import { createMiddleware } from "hono/factory";
|
|
40
39
|
// src/api/http.ts
|
|
@@ -140,325 +139,6 @@ async function archiveConversation(viewer, conversationId, body) {
|
|
|
140
139
|
if (result.status === "missing") throwApiError(404, "Conversation not found.");
|
|
141
140
|
throwApiError(409, "Conversation received new activity.");
|
|
142
141
|
}
|
|
143
|
-
// src/chat/conversations/fork.ts
|
|
144
|
-
/** Event key of the agent note that follows the copied events in a fork. */
|
|
145
|
-
const FORK_NOTE_KEY = "fork:note";
|
|
146
|
-
const forkNote = alias(juniorConversationEvents, "fork_note");
|
|
147
|
-
/**
|
|
148
|
-
* SQL filter for event rows that a fork did not copy from its source. Usage
|
|
149
|
-
* and cost reports use it, so copied model calls count only in the source.
|
|
150
|
-
*/
|
|
151
|
-
function notCopiedByFork() {
|
|
152
|
-
return sql`not exists (
|
|
153
|
-
select 1 from ${juniorConversationEvents} as ${sql.identifier("fork_note")}
|
|
154
|
-
where ${forkNote.conversationId} = ${juniorConversationEvents.conversationId}
|
|
155
|
-
and ${forkNote.idempotencyKey} = ${FORK_NOTE_KEY}
|
|
156
|
-
and ${forkNote.seq} > ${juniorConversationEvents.seq}
|
|
157
|
-
)`;
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Events that hold credentials, approvals, provider connections, or plugin
|
|
161
|
-
* state of the source actor. A fork does not copy them.
|
|
162
|
-
*/
|
|
163
|
-
const UNCOPIED_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
164
|
-
"authorization_completed",
|
|
165
|
-
"authorization_requested",
|
|
166
|
-
"guardian_action_reviewed",
|
|
167
|
-
"mcp_provider_connected",
|
|
168
|
-
"mcp_provider_connected_unowned",
|
|
169
|
-
"structured_event",
|
|
170
|
-
"unknown"
|
|
171
|
-
]);
|
|
172
|
-
const FORK_NOTE = "This Conversation is a fork. The earlier messages are copied from another Conversation. Sandbox files, active work, approvals, credentials, Watches, Automations, and the delivery location were not copied. Earlier file paths and runtime context belong to the source Conversation. Verify files in the current Sandbox before you use them.";
|
|
173
|
-
/** An expected fork request failure, such as a reply without saved history. */
|
|
174
|
-
var ConversationForkError = class extends Error {};
|
|
175
|
-
/**
|
|
176
|
-
* Create a new web root Conversation from the agent history of a source
|
|
177
|
-
* Conversation, through one completed assistant reply.
|
|
178
|
-
*
|
|
179
|
-
* Retries with the same requester and key return the same Conversation.
|
|
180
|
-
*/
|
|
181
|
-
async function forkConversation(input) {
|
|
182
|
-
const conversationId = createConversationId({
|
|
183
|
-
actorEmail: input.actorEmail,
|
|
184
|
-
idempotencyKey: JSON.stringify([
|
|
185
|
-
"fork",
|
|
186
|
-
input.sourceConversationId,
|
|
187
|
-
input.idempotencyKey
|
|
188
|
-
])
|
|
189
|
-
});
|
|
190
|
-
const executor = getSqlExecutor();
|
|
191
|
-
const events = getConversationEventStore();
|
|
192
|
-
const store = getConversationStore();
|
|
193
|
-
return withConversationEventLock(executor, input.sourceConversationId, () => withConversationEventLock(executor, conversationId, async () => {
|
|
194
|
-
if (await store.get({ conversationId })) return { conversationId };
|
|
195
|
-
const source = await store.get({ conversationId: input.sourceConversationId });
|
|
196
|
-
if (!source || source.transcriptPurgedAtMs !== void 0) throw new ConversationForkError("The source history is no longer available.");
|
|
197
|
-
if (source.parentConversationId) throw new ConversationForkError("You cannot fork a child conversation.");
|
|
198
|
-
const boundary = await events.loadByIdempotencyKey(input.sourceConversationId, `message:${input.messageId}:agent`);
|
|
199
|
-
const reply = boundary?.data.type === "assistant_message" ? piMessageFromHistoryItem(boundary.data) : void 0;
|
|
200
|
-
if (!boundary || reply?.role !== "assistant" || reply.stopReason !== "stop" || reply.content.some((part) => part.type === "toolCall")) throw new ConversationForkError("Choose a completed assistant reply with saved agent history.");
|
|
201
|
-
const projection = await loadTurnProjection({
|
|
202
|
-
conversationId: input.sourceConversationId,
|
|
203
|
-
committedSeq: boundary.seq,
|
|
204
|
-
includeTail: false
|
|
205
|
-
});
|
|
206
|
-
if (!projection?.messages.length) throw new ConversationForkError("The selected history is no longer available.");
|
|
207
|
-
if (hasUnfinishedToolCalls(projection.messages)) throw new ConversationForkError("This history has unfinished tool calls. Choose another reply.");
|
|
208
|
-
const nowMs = Date.now();
|
|
209
|
-
await recordWebConversationActivity({
|
|
210
|
-
actor: webActorFromEmail(input.actorEmail, { ...input.actorName ? { fullName: input.actorName } : void 0 }),
|
|
211
|
-
conversationId,
|
|
212
|
-
nowMs,
|
|
213
|
-
rootVisibility: source.visibility === "public" ? "public" : "private"
|
|
214
|
-
});
|
|
215
|
-
await executor.db().update(juniorConversations).set({
|
|
216
|
-
forkedFromConversationId: source.conversationId,
|
|
217
|
-
title: source.title ? `Fork: ${source.title}` : "Forked conversation"
|
|
218
|
-
}).where(eq(juniorConversations.conversationId, conversationId));
|
|
219
|
-
await copyForkEvents({
|
|
220
|
-
sourceConversationId: source.conversationId,
|
|
221
|
-
conversationId,
|
|
222
|
-
events: await events.loadHistory(source.conversationId),
|
|
223
|
-
boundarySeq: boundary.seq,
|
|
224
|
-
messageId: input.messageId
|
|
225
|
-
});
|
|
226
|
-
await events.append(conversationId, [{
|
|
227
|
-
idempotencyKey: FORK_NOTE_KEY,
|
|
228
|
-
createdAtMs: nowMs,
|
|
229
|
-
data: {
|
|
230
|
-
type: "user_message",
|
|
231
|
-
provenance: contextProvenance,
|
|
232
|
-
content: [{
|
|
233
|
-
type: "text",
|
|
234
|
-
text: FORK_NOTE
|
|
235
|
-
}],
|
|
236
|
-
timestamp: nowMs
|
|
237
|
-
}
|
|
238
|
-
}]);
|
|
239
|
-
return { conversationId };
|
|
240
|
-
}));
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Copy the source event rows through the selected reply. Copies keep their
|
|
244
|
-
* sequence, author, timestamp, key, and payload, so the fork shows the same
|
|
245
|
-
* transcript and event log, and seq references in compactions stay valid.
|
|
246
|
-
* Later updates of copied Messages and the end of copied Turns are also
|
|
247
|
-
* copied, so that no copied item stays open in the fork.
|
|
248
|
-
*/
|
|
249
|
-
async function copyForkEvents(args) {
|
|
250
|
-
const reply = args.events.find((event) => event.data.type === "message" && event.data.role === "assistant" && event.data.messageId === args.messageId);
|
|
251
|
-
const cutoffSeq = Math.max(args.boundarySeq, reply?.seq ?? -1);
|
|
252
|
-
const messageIds = /* @__PURE__ */ new Set();
|
|
253
|
-
const turnIds = /* @__PURE__ */ new Set();
|
|
254
|
-
const seqs = [];
|
|
255
|
-
let historyVersion = 0;
|
|
256
|
-
for (const event of args.events) {
|
|
257
|
-
const data = event.data;
|
|
258
|
-
if (UNCOPIED_EVENT_TYPES.has(data.type)) continue;
|
|
259
|
-
if (event.idempotencyKey === FORK_NOTE_KEY) continue;
|
|
260
|
-
if (event.seq <= cutoffSeq) {
|
|
261
|
-
if (data.type === "message") messageIds.add(data.messageId);
|
|
262
|
-
if (data.type === "turn_started") turnIds.add(data.turnId);
|
|
263
|
-
historyVersion = Math.max(historyVersion, event.historyVersion);
|
|
264
|
-
seqs.push(event.seq);
|
|
265
|
-
} else if ((data.type === "message_updated" || data.type === "message_handled") && messageIds.has(data.messageId) || (data.type === "turn_completed" || data.type === "turn_failed") && turnIds.has(data.turnId)) seqs.push(event.seq);
|
|
266
|
-
}
|
|
267
|
-
if (!seqs.length) return;
|
|
268
|
-
const e = juniorConversationEvents;
|
|
269
|
-
await getSqlExecutor().db().execute(sql`insert into ${e} (${sql.join([
|
|
270
|
-
e.conversationId,
|
|
271
|
-
e.seq,
|
|
272
|
-
e.historyVersion,
|
|
273
|
-
e.schemaVersion,
|
|
274
|
-
e.idempotencyKey,
|
|
275
|
-
e.type,
|
|
276
|
-
e.payload,
|
|
277
|
-
e.actorIdentityId,
|
|
278
|
-
e.createdAt
|
|
279
|
-
].map((column) => sql.identifier(column.name)), sql`, `)})
|
|
280
|
-
select ${args.conversationId}, ${e.seq}, least(${e.historyVersion}, ${historyVersion}), ${e.schemaVersion}, ${e.idempotencyKey}, ${e.type}, ${e.payload}, ${e.actorIdentityId}, ${e.createdAt}
|
|
281
|
-
from ${e}
|
|
282
|
-
where ${e.conversationId} = ${args.sourceConversationId}
|
|
283
|
-
and ${inArray(e.seq, seqs)}`);
|
|
284
|
-
}
|
|
285
|
-
function hasUnfinishedToolCalls(messages) {
|
|
286
|
-
const pending = /* @__PURE__ */ new Set();
|
|
287
|
-
for (const message of messages) if (message.role === "assistant") {
|
|
288
|
-
for (const part of message.content) if (part.type === "toolCall") pending.add(part.id);
|
|
289
|
-
} else if (message.role === "toolResult") pending.delete(message.toolCallId);
|
|
290
|
-
return pending.size > 0;
|
|
291
|
-
}
|
|
292
|
-
// src/api/conversations/auxiliary-costs.ts
|
|
293
|
-
function eventCost() {
|
|
294
|
-
return sql`CASE
|
|
295
|
-
WHEN ${juniorConversationEvents.type} = 'structured_event'
|
|
296
|
-
AND jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
|
|
297
|
-
THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::numeric
|
|
298
|
-
WHEN ${juniorConversationEvents.type} IN ('guardian_action_reviewed', 'turn_routed')
|
|
299
|
-
AND jsonb_typeof(${juniorConversationEvents.payload}->'costUsd') = 'number'
|
|
300
|
-
THEN (${juniorConversationEvents.payload}->>'costUsd')::numeric
|
|
301
|
-
ELSE NULL
|
|
302
|
-
END`;
|
|
303
|
-
}
|
|
304
|
-
function addCostUsd(left, right) {
|
|
305
|
-
return Math.round((left + right) * 0xe8d4a51000) / 0xe8d4a51000;
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Aggregate additive event costs by namespaced operation. The total for each
|
|
309
|
-
* selected Conversation includes itself and all its child agents at any depth.
|
|
310
|
-
*/
|
|
311
|
-
async function readConversationAuxiliaryCostsFromSql(db, conversationIds) {
|
|
312
|
-
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
313
|
-
const scopeConversationId = conversationSubtreeColumns.scopeConversationId;
|
|
314
|
-
const isChildAgent = sql`(${conversationSubtreeColumns.conversationId} <> ${scopeConversationId})`;
|
|
315
|
-
const namespace = sql`CASE
|
|
316
|
-
WHEN ${juniorConversationEvents.type} = 'structured_event'
|
|
317
|
-
THEN ${juniorConversationEvents.payload}->>'namespace'
|
|
318
|
-
ELSE 'junior'
|
|
319
|
-
END`;
|
|
320
|
-
const name = sql`CASE
|
|
321
|
-
WHEN ${juniorConversationEvents.type} = 'structured_event'
|
|
322
|
-
THEN ${juniorConversationEvents.payload}->>'name'
|
|
323
|
-
ELSE ${juniorConversationEvents.type}
|
|
324
|
-
END`;
|
|
325
|
-
const cost = eventCost();
|
|
326
|
-
const rows = await db.select({
|
|
327
|
-
conversationId: scopeConversationId,
|
|
328
|
-
childAgentCostUsd: sql`coalesce(round(sum(case when ${isChildAgent} then ${cost} end), 12), 0)::double precision`.mapWith(Number),
|
|
329
|
-
costUsd: sql`round(sum(${cost}), 12)::double precision`.mapWith(Number),
|
|
330
|
-
events: sql`count(*)::integer`.mapWith(Number),
|
|
331
|
-
name,
|
|
332
|
-
namespace
|
|
333
|
-
}).from(juniorConversationEvents).innerJoin(conversationSubtree(conversationIds), eq(conversationSubtreeColumns.conversationId, juniorConversationEvents.conversationId)).where(and(or(eq(juniorConversationEvents.type, "structured_event"), inArray(juniorConversationEvents.type, ["guardian_action_reviewed", "turn_routed"])), sql`${cost} >= 0`, notCopiedByFork())).groupBy(scopeConversationId, namespace, name).orderBy(scopeConversationId, namespace, name);
|
|
334
|
-
const result = /* @__PURE__ */ new Map();
|
|
335
|
-
for (const row of rows) {
|
|
336
|
-
const current = result.get(row.conversationId) ?? { total: {
|
|
337
|
-
costUsd: 0,
|
|
338
|
-
operations: []
|
|
339
|
-
} };
|
|
340
|
-
current.total.costUsd = addCostUsd(current.total.costUsd, row.costUsd);
|
|
341
|
-
if (row.childAgentCostUsd > 0) current.childAgentCostUsd = addCostUsd(current.childAgentCostUsd ?? 0, row.childAgentCostUsd);
|
|
342
|
-
current.total.operations.push({
|
|
343
|
-
costUsd: row.costUsd,
|
|
344
|
-
events: row.events,
|
|
345
|
-
name: row.name,
|
|
346
|
-
namespace: row.namespace
|
|
347
|
-
});
|
|
348
|
-
result.set(row.conversationId, current);
|
|
349
|
-
}
|
|
350
|
-
return result;
|
|
351
|
-
}
|
|
352
|
-
// src/api/conversations/usage.ts
|
|
353
|
-
const usage$1 = juniorConversations.usage;
|
|
354
|
-
const componentFields = [
|
|
355
|
-
"inputTokens",
|
|
356
|
-
"outputTokens",
|
|
357
|
-
"cachedInputTokens",
|
|
358
|
-
"cacheCreationTokens"
|
|
359
|
-
];
|
|
360
|
-
function summedUsageNumber(field) {
|
|
361
|
-
return sql`sum((${usage$1}->>${field})::double precision)::double precision`;
|
|
362
|
-
}
|
|
363
|
-
function summedCostNumber(field) {
|
|
364
|
-
return sql`round(
|
|
365
|
-
sum((${usage$1}->'cost'->>${field})::numeric),
|
|
366
|
-
12
|
|
367
|
-
)::double precision`;
|
|
368
|
-
}
|
|
369
|
-
function usageFromAggregate(row) {
|
|
370
|
-
const components = {};
|
|
371
|
-
let componentTotal = 0;
|
|
372
|
-
for (const field of componentFields) {
|
|
373
|
-
const value = row[field];
|
|
374
|
-
if (value === null) continue;
|
|
375
|
-
components[field] = value;
|
|
376
|
-
componentTotal += value;
|
|
377
|
-
}
|
|
378
|
-
const cost = {
|
|
379
|
-
...row.costInput === null ? void 0 : { input: row.costInput },
|
|
380
|
-
...row.costOutput === null ? void 0 : { output: row.costOutput },
|
|
381
|
-
...row.costCacheRead === null ? void 0 : { cacheRead: row.costCacheRead },
|
|
382
|
-
...row.costCacheWrite === null ? void 0 : { cacheWrite: row.costCacheWrite },
|
|
383
|
-
...row.costTotal === null ? void 0 : { total: row.costTotal }
|
|
384
|
-
};
|
|
385
|
-
const result = {
|
|
386
|
-
...row.totalOnlyTokens === null ? components : { totalTokens: row.totalOnlyTokens + componentTotal },
|
|
387
|
-
...row.reasoningTokens === null ? void 0 : { reasoningTokens: row.reasoningTokens },
|
|
388
|
-
...Object.keys(cost).length === 0 ? void 0 : { cost }
|
|
389
|
-
};
|
|
390
|
-
return hasAgentTurnUsage(result) ? result : void 0;
|
|
391
|
-
}
|
|
392
|
-
/**
|
|
393
|
-
* Aggregate saved runtime and usage in PostgreSQL. The total for each selected
|
|
394
|
-
* Conversation includes itself and all its child agents at any depth.
|
|
395
|
-
*/
|
|
396
|
-
async function readConversationTreeMetricsFromSql(db, conversationIds) {
|
|
397
|
-
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
398
|
-
const hasComponents = sql`coalesce(
|
|
399
|
-
${usage$1}->>'inputTokens',
|
|
400
|
-
${usage$1}->>'outputTokens',
|
|
401
|
-
${usage$1}->>'cachedInputTokens',
|
|
402
|
-
${usage$1}->>'cacheCreationTokens'
|
|
403
|
-
) is not null`;
|
|
404
|
-
const isChildAgent = sql`(${conversationSubtreeColumns.conversationId} <> ${conversationSubtreeColumns.scopeConversationId})`;
|
|
405
|
-
const rows = await db.select({
|
|
406
|
-
scopeConversationId: conversationSubtreeColumns.scopeConversationId,
|
|
407
|
-
isChildAgent,
|
|
408
|
-
conversations: sql`count(*)::integer`.mapWith(Number),
|
|
409
|
-
durationMs: sql`coalesce(sum(${juniorConversations.durationMs}), 0)::double precision`,
|
|
410
|
-
inputTokens: summedUsageNumber("inputTokens"),
|
|
411
|
-
outputTokens: summedUsageNumber("outputTokens"),
|
|
412
|
-
cachedInputTokens: summedUsageNumber("cachedInputTokens"),
|
|
413
|
-
cacheCreationTokens: summedUsageNumber("cacheCreationTokens"),
|
|
414
|
-
totalOnlyTokens: sql`sum(
|
|
415
|
-
case
|
|
416
|
-
when not (${hasComponents})
|
|
417
|
-
then (${usage$1}->>'totalTokens')::double precision
|
|
418
|
-
end
|
|
419
|
-
)::double precision`,
|
|
420
|
-
reasoningTokens: summedUsageNumber("reasoningTokens"),
|
|
421
|
-
costInput: summedCostNumber("input"),
|
|
422
|
-
costOutput: summedCostNumber("output"),
|
|
423
|
-
costCacheRead: summedCostNumber("cacheRead"),
|
|
424
|
-
costCacheWrite: summedCostNumber("cacheWrite"),
|
|
425
|
-
costTotal: summedCostNumber("total")
|
|
426
|
-
}).from(juniorConversations).innerJoin(conversationSubtree(conversationIds), eq(conversationSubtreeColumns.conversationId, juniorConversations.conversationId)).groupBy(conversationSubtreeColumns.scopeConversationId, isChildAgent);
|
|
427
|
-
const ownRows = /* @__PURE__ */ new Map();
|
|
428
|
-
const childRows = /* @__PURE__ */ new Map();
|
|
429
|
-
for (const row of rows) (row.isChildAgent ? childRows : ownRows).set(row.scopeConversationId, row);
|
|
430
|
-
const result = /* @__PURE__ */ new Map();
|
|
431
|
-
for (const [conversationId, own] of ownRows) {
|
|
432
|
-
const ownUsage = usageFromAggregate(own);
|
|
433
|
-
const child = childRows.get(conversationId);
|
|
434
|
-
const childUsage = child ? usageFromAggregate(child) : void 0;
|
|
435
|
-
const usage = addAgentTurnUsage(ownUsage, childUsage);
|
|
436
|
-
result.set(conversationId, {
|
|
437
|
-
durationMs: own.durationMs + (child?.durationMs ?? 0),
|
|
438
|
-
...usage ? { usage } : void 0,
|
|
439
|
-
...child ? { childAgents: {
|
|
440
|
-
count: child.conversations,
|
|
441
|
-
durationMs: child.durationMs,
|
|
442
|
-
...childUsage ? { usage: childUsage } : void 0
|
|
443
|
-
} } : void 0
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
return result;
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* Project the child agent part of tree metrics into the API shape. Return
|
|
450
|
-
* nothing when the Conversation has no child agents.
|
|
451
|
-
*/
|
|
452
|
-
function childAgentTotalsReport(metrics, auxiliaryCosts) {
|
|
453
|
-
const childAgents = metrics?.childAgents;
|
|
454
|
-
if (!childAgents) return void 0;
|
|
455
|
-
return {
|
|
456
|
-
...auxiliaryCosts?.childAgentCostUsd === void 0 ? void 0 : { auxiliaryCostUsd: auxiliaryCosts.childAgentCostUsd },
|
|
457
|
-
count: childAgents.count,
|
|
458
|
-
cumulativeDurationMs: childAgents.durationMs,
|
|
459
|
-
...childAgents.usage ? { cumulativeUsage: childAgents.usage } : void 0
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
142
|
// src/api/conversations/child-agents.ts
|
|
463
143
|
/**
|
|
464
144
|
* Load the direct child agents of one Conversation, or throw a standard API
|
|
@@ -581,170 +261,47 @@ async function appendConversationMessageForViewer(viewer, conversationId, body,
|
|
|
581
261
|
if (!(destinationPlatform === "local" && conversationId.startsWith("local:web:") || destinationPlatform === "slack")) throwApiError(409, "Conversation does not accept web messages.");
|
|
582
262
|
if (!(await readConversationAccessFromSql(getDb(), [conversationId], viewer)).get(conversationId)?.isParticipant) throwApiError(403, "Only conversation participants can add messages.");
|
|
583
263
|
const images = parseImages(body.images);
|
|
264
|
+
const actor = actorFromViewer(viewer);
|
|
265
|
+
const append = () => appendAndEnqueueWebMessage({
|
|
266
|
+
actor,
|
|
267
|
+
conversationId,
|
|
268
|
+
idempotencyKey: body.idempotencyKey,
|
|
269
|
+
message: body.message,
|
|
270
|
+
images
|
|
271
|
+
}, {
|
|
272
|
+
conversationStore: getConversationStore(),
|
|
273
|
+
attachmentStorage: services.attachmentStorage,
|
|
274
|
+
queue: services.queue
|
|
275
|
+
});
|
|
276
|
+
const destination = conversation.destination;
|
|
584
277
|
try {
|
|
585
|
-
return await
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
278
|
+
if (destination?.platform !== "slack") return await append();
|
|
279
|
+
return await runWithWorkspaceTeamId(destination.teamId, async () => {
|
|
280
|
+
const state = getStateAdapter();
|
|
281
|
+
await state.connect();
|
|
282
|
+
return persistSlackUserInputReply({
|
|
283
|
+
channelId: destination.channelId,
|
|
284
|
+
claimedBy: viewer.email,
|
|
285
|
+
isSaved: async () => {
|
|
286
|
+
const [messageId, work] = await Promise.all([webMessageId({
|
|
287
|
+
conversationId,
|
|
288
|
+
idempotencyKey: body.idempotencyKey
|
|
289
|
+
}), getConversationWorkState({
|
|
290
|
+
conversationId,
|
|
291
|
+
state
|
|
292
|
+
})]);
|
|
293
|
+
return work?.execution.inboundMessageIds.includes(messageId) === true;
|
|
294
|
+
},
|
|
295
|
+
note: () => webUserInputAnswerNote(viewer.displayName ?? viewer.email),
|
|
296
|
+
persist: append,
|
|
297
|
+
state,
|
|
298
|
+
threadTs: conversation.location?.threadTs ?? parseSlackThreadId(conversationId)?.threadTs
|
|
299
|
+
});
|
|
595
300
|
});
|
|
596
301
|
} catch (error) {
|
|
597
302
|
throwApiError(500, "Unable to append message.", error);
|
|
598
303
|
}
|
|
599
304
|
}
|
|
600
|
-
// src/chat/plugins/conversation-sidebar.ts
|
|
601
|
-
/** Show native objects by default and let plugins select compact sidebar labels. */
|
|
602
|
-
async function listConversationSidebarAnnotations(conversationIds, annotationsByConversation) {
|
|
603
|
-
const candidates = new Set(conversationIds);
|
|
604
|
-
const selected = {};
|
|
605
|
-
const plugins = getPlugins();
|
|
606
|
-
const customOwners = new Set(plugins.filter((plugin) => plugin.hooks?.conversationSidebar).map((plugin) => plugin.manifest.name));
|
|
607
|
-
for (const conversationId of conversationIds) {
|
|
608
|
-
const objects = (annotationsByConversation.get(conversationId) ?? []).flatMap((annotation) => {
|
|
609
|
-
if (customOwners.has(annotation.plugin) || !annotation.objectType) return [];
|
|
610
|
-
return [{
|
|
611
|
-
key: annotation.key,
|
|
612
|
-
label: annotation.label,
|
|
613
|
-
objectType: annotation.objectType === "item" && annotation.kind === "object" && annotation.facts?.type === "deployment" ? "deployment" : annotation.objectType,
|
|
614
|
-
status: annotation.status
|
|
615
|
-
}];
|
|
616
|
-
});
|
|
617
|
-
if (objects.length) selected[conversationId] = objects;
|
|
618
|
-
}
|
|
619
|
-
for (const plugin of plugins) {
|
|
620
|
-
const hook = plugin.hooks?.conversationSidebar;
|
|
621
|
-
if (!hook) continue;
|
|
622
|
-
const annotationsByConversationId = Object.fromEntries(conversationIds.flatMap((conversationId) => {
|
|
623
|
-
const annotations = (annotationsByConversation.get(conversationId) ?? []).filter((annotation) => annotation.plugin === plugin.manifest.name);
|
|
624
|
-
return annotations.length > 0 ? [[conversationId, annotations]] : [];
|
|
625
|
-
}));
|
|
626
|
-
try {
|
|
627
|
-
const result = await hook({
|
|
628
|
-
annotationsByConversationId,
|
|
629
|
-
conversationIds,
|
|
630
|
-
db: getDb(),
|
|
631
|
-
log: createPluginLogger(plugin.manifest.name),
|
|
632
|
-
plugin: { name: plugin.manifest.name },
|
|
633
|
-
cache: createPluginCache(plugin.manifest.name)
|
|
634
|
-
});
|
|
635
|
-
for (const [conversationId, annotations] of Object.entries(result.annotationsByConversationId)) {
|
|
636
|
-
if (!candidates.has(conversationId)) continue;
|
|
637
|
-
const parsed = conversationSidebarAnnotationSchema.array().safeParse(annotations);
|
|
638
|
-
if (!parsed.success || parsed.data.length === 0) continue;
|
|
639
|
-
selected[conversationId] = [...selected[conversationId] ?? [], ...parsed.data];
|
|
640
|
-
}
|
|
641
|
-
} catch (error) {
|
|
642
|
-
logWarn("plugin.conversation_sidebar.hook.failed", {
|
|
643
|
-
"app.plugin.name": plugin.manifest.name,
|
|
644
|
-
"exception.message": error instanceof Error ? error.message : String(error)
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
return selected;
|
|
649
|
-
}
|
|
650
|
-
// src/chat/slack/location-url.ts
|
|
651
|
-
/** Parse a Slack workspace subdomain used in `https://{domain}.slack.com` URLs. */
|
|
652
|
-
function parseSlackTeamDomain(value) {
|
|
653
|
-
if (typeof value !== "string") return void 0;
|
|
654
|
-
const domain = value.trim().toLowerCase();
|
|
655
|
-
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(domain)) return;
|
|
656
|
-
return domain;
|
|
657
|
-
}
|
|
658
|
-
/** Convert a Slack message ts into the `p…` segment used by archive URLs. */
|
|
659
|
-
function archivePathFromThreadTs(threadTs) {
|
|
660
|
-
const [seconds, fraction = ""] = threadTs.split(".");
|
|
661
|
-
return `p${seconds}${(fraction ?? "").padEnd(6, "0").slice(0, 6)}`;
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* Build a direct Slack archive URL for a thread when the workspace domain is
|
|
665
|
-
* known. Omits a link rather than falling back to `app_redirect`.
|
|
666
|
-
*/
|
|
667
|
-
function buildSlackLocationUrl(args) {
|
|
668
|
-
const channelId = parseSlackChannelId(args.channelId);
|
|
669
|
-
const teamDomain = parseSlackTeamDomain(args.teamDomain);
|
|
670
|
-
const threadTs = parseSlackMessageTs(args.threadTs);
|
|
671
|
-
if (!channelId || !teamDomain || !threadTs) return void 0;
|
|
672
|
-
const url = new URL(`https://${teamDomain}.slack.com/archives/${channelId}/${archivePathFromThreadTs(threadTs)}`);
|
|
673
|
-
url.searchParams.set("thread_ts", threadTs);
|
|
674
|
-
url.searchParams.set("cid", channelId);
|
|
675
|
-
return url.toString();
|
|
676
|
-
}
|
|
677
|
-
// src/chat/slack/team-domain.ts
|
|
678
|
-
const TEAM_DOMAIN_CACHE_TTL_MS = 864e5;
|
|
679
|
-
const TEAM_DOMAIN_NEGATIVE_CACHE_TTL_MS = 6e4;
|
|
680
|
-
const teamDomainCache = /* @__PURE__ */ new Map();
|
|
681
|
-
const teamDomainInflight = /* @__PURE__ */ new Map();
|
|
682
|
-
function readTeamDomainCache(teamId) {
|
|
683
|
-
const hit = teamDomainCache.get(teamId);
|
|
684
|
-
if (!hit) return void 0;
|
|
685
|
-
if (hit.expiresAt < Date.now()) {
|
|
686
|
-
teamDomainCache.delete(teamId);
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
return hit.kind === "hit" ? hit.domain : null;
|
|
690
|
-
}
|
|
691
|
-
function writeTeamDomainCache(teamId, domain) {
|
|
692
|
-
if (domain) {
|
|
693
|
-
teamDomainCache.set(teamId, {
|
|
694
|
-
kind: "hit",
|
|
695
|
-
domain,
|
|
696
|
-
expiresAt: Date.now() + TEAM_DOMAIN_CACHE_TTL_MS
|
|
697
|
-
});
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
teamDomainCache.set(teamId, {
|
|
701
|
-
kind: "miss",
|
|
702
|
-
expiresAt: Date.now() + TEAM_DOMAIN_NEGATIVE_CACHE_TTL_MS
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
async function fetchSlackTeamDomain(teamId) {
|
|
706
|
-
try {
|
|
707
|
-
const response = await withSlackRetries(() => getSlackClient().team.info({ team: teamId }), 3, {
|
|
708
|
-
action: "team.info",
|
|
709
|
-
idempotent: true,
|
|
710
|
-
spanAttributes: { "app.slack.team_id": teamId }
|
|
711
|
-
});
|
|
712
|
-
const responseTeamId = parseSlackTeamId(response.team?.id);
|
|
713
|
-
const domain = parseSlackTeamDomain(response.team?.domain);
|
|
714
|
-
if (!domain || !responseTeamId || responseTeamId !== teamId) {
|
|
715
|
-
writeTeamDomainCache(teamId, void 0);
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
writeTeamDomainCache(teamId, domain);
|
|
719
|
-
return domain;
|
|
720
|
-
} catch {
|
|
721
|
-
writeTeamDomainCache(teamId, void 0);
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
/** Resolve a workspace subdomain via `team.info`, cached by team id. */
|
|
726
|
-
async function resolveSlackTeamDomain(teamId) {
|
|
727
|
-
const parsedTeamId = parseSlackTeamId(teamId);
|
|
728
|
-
if (!parsedTeamId) return void 0;
|
|
729
|
-
const cached = readTeamDomainCache(parsedTeamId);
|
|
730
|
-
if (cached !== void 0) return cached ?? void 0;
|
|
731
|
-
const pending = teamDomainInflight.get(parsedTeamId);
|
|
732
|
-
if (pending) return pending;
|
|
733
|
-
const lookup = fetchSlackTeamDomain(parsedTeamId).finally(() => {
|
|
734
|
-
teamDomainInflight.delete(parsedTeamId);
|
|
735
|
-
});
|
|
736
|
-
teamDomainInflight.set(parsedTeamId, lookup);
|
|
737
|
-
return lookup;
|
|
738
|
-
}
|
|
739
|
-
/** Resolve workspace subdomains for the distinct team ids in one API response. */
|
|
740
|
-
async function resolveSlackTeamDomains(teamIds) {
|
|
741
|
-
const uniqueTeamIds = [...new Set([...teamIds].map((teamId) => parseSlackTeamId(teamId)).filter((teamId) => teamId !== void 0))];
|
|
742
|
-
const resolved = await Promise.all(uniqueTeamIds.map(async (teamId) => {
|
|
743
|
-
const domain = await resolveSlackTeamDomain(teamId);
|
|
744
|
-
return domain ? [teamId, domain] : void 0;
|
|
745
|
-
}));
|
|
746
|
-
return new Map(resolved.filter((entry) => entry !== void 0));
|
|
747
|
-
}
|
|
748
305
|
// src/chat/pi/sql-model-usage.ts
|
|
749
306
|
/** SQL reporting adapter for usage stored on assistant history events. */
|
|
750
307
|
const message = sql`${juniorConversationEvents.payload}`;
|
|
@@ -888,687 +445,6 @@ function decodeConversationCursor(args) {
|
|
|
888
445
|
return;
|
|
889
446
|
}
|
|
890
447
|
}
|
|
891
|
-
// src/api/conversations/first-message.ts
|
|
892
|
-
/**
|
|
893
|
-
* Read the text of the first human message for each untitled conversation.
|
|
894
|
-
* Read-time titles use it while the generated title is still unset.
|
|
895
|
-
*/
|
|
896
|
-
async function readFirstHumanMessageTexts(db, conversations) {
|
|
897
|
-
const untitledIds = conversations.flatMap((conversation) => conversation.title ? [] : [conversation.conversationId]);
|
|
898
|
-
if (untitledIds.length === 0) return /* @__PURE__ */ new Map();
|
|
899
|
-
const text = sql`${juniorConversationEvents.payload}->>'text'`;
|
|
900
|
-
const rows = await db.selectDistinctOn([juniorConversationEvents.conversationId], {
|
|
901
|
-
conversationId: juniorConversationEvents.conversationId,
|
|
902
|
-
text: text.as("text")
|
|
903
|
-
}).from(juniorConversationEvents).where(and(inArray(juniorConversationEvents.conversationId, untitledIds), eq(juniorConversationEvents.type, "message"), sql`${juniorConversationEvents.payload}->>'role' = 'user'`, sql`coalesce(${juniorConversationEvents.payload}->'meta'->'author'->>'isBot', 'false') <> 'true'`, isNotNull(sql`nullif(btrim(${text}), '')`))).orderBy(juniorConversationEvents.conversationId, asc(juniorConversationEvents.createdAt), asc(juniorConversationEvents.seq));
|
|
904
|
-
return new Map(rows.map((row) => [row.conversationId, row.text]));
|
|
905
|
-
}
|
|
906
|
-
// src/api/mentions.ts
|
|
907
|
-
const SLACK_USER_MENTION_RE = /<@([UW][A-Z0-9]+)(?:\|[^<>]*)?>/g;
|
|
908
|
-
function mentionedSlackUserIds(texts) {
|
|
909
|
-
const ids = texts.flatMap((text) => text ? [...text.matchAll(SLACK_USER_MENTION_RE)].map((m) => m[1]) : []);
|
|
910
|
-
return [...new Set(ids)];
|
|
911
|
-
}
|
|
912
|
-
/**
|
|
913
|
-
* Resolve the people that each source mentions, in source order. Every API
|
|
914
|
-
* that returns display text uses this, so the dashboard can show the same
|
|
915
|
-
* names and People links everywhere. It does one identity query per Slack
|
|
916
|
-
* workspace.
|
|
917
|
-
*/
|
|
918
|
-
async function resolveMentions(executor, sources) {
|
|
919
|
-
const idsBySource = sources.map((source) => mentionedSlackUserIds(source.texts));
|
|
920
|
-
const idsByTenant = /* @__PURE__ */ new Map();
|
|
921
|
-
sources.forEach((source, index) => {
|
|
922
|
-
if (idsBySource[index].length === 0) return;
|
|
923
|
-
const ids = idsByTenant.get(source.tenantId) ?? /* @__PURE__ */ new Set();
|
|
924
|
-
for (const id of idsBySource[index]) ids.add(id);
|
|
925
|
-
idsByTenant.set(source.tenantId, ids);
|
|
926
|
-
});
|
|
927
|
-
const peopleByTenant = new Map(await Promise.all([...idsByTenant].map(async ([tenantId, ids]) => [tenantId, await readMentionedPeople(executor, tenantId, [...ids])])));
|
|
928
|
-
return sources.map((source, index) => {
|
|
929
|
-
const people = peopleByTenant.get(source.tenantId);
|
|
930
|
-
return idsBySource[index].flatMap((id) => {
|
|
931
|
-
const person = people?.get(id);
|
|
932
|
-
return person ? [person] : [];
|
|
933
|
-
});
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
/**
|
|
937
|
-
* Read current names for mentioned Slack users. Known Junior identities come
|
|
938
|
-
* first. The email is set only for a linked, verified Junior user, because
|
|
939
|
-
* that email is the People profile route.
|
|
940
|
-
*/
|
|
941
|
-
async function readMentionedPeople(executor, tenantId, userIds) {
|
|
942
|
-
const rows = await executor.db().select({
|
|
943
|
-
displayName: juniorIdentities.displayName,
|
|
944
|
-
emailVerified: juniorIdentities.emailVerified,
|
|
945
|
-
handle: juniorIdentities.handle,
|
|
946
|
-
providerSubjectId: juniorIdentities.providerSubjectId,
|
|
947
|
-
userDisplayName: juniorUsers.displayName,
|
|
948
|
-
userEmail: juniorUsers.primaryEmail
|
|
949
|
-
}).from(juniorIdentities).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(eq(juniorIdentities.provider, "slack"), inArray(juniorIdentities.providerSubjectId, userIds), tenantId ? eq(juniorIdentities.providerTenantId, tenantId) : void 0));
|
|
950
|
-
const people = /* @__PURE__ */ new Map();
|
|
951
|
-
for (const row of rows) {
|
|
952
|
-
if (people.has(row.providerSubjectId)) continue;
|
|
953
|
-
const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
|
|
954
|
-
const email = row.emailVerified ? row.userEmail?.trim() : void 0;
|
|
955
|
-
people.set(row.providerSubjectId, {
|
|
956
|
-
slackUserId: row.providerSubjectId,
|
|
957
|
-
...fullName ? { fullName } : void 0,
|
|
958
|
-
...row.handle ? { slackUserName: row.handle } : void 0,
|
|
959
|
-
...email ? { email } : void 0
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
const unnamed = userIds.filter((id) => {
|
|
963
|
-
const person = people.get(id);
|
|
964
|
-
return !person?.fullName && !person?.slackUserName;
|
|
965
|
-
});
|
|
966
|
-
if (tenantId && unnamed.length > 0) {
|
|
967
|
-
const profiles = await Promise.all(unnamed.map((id) => lookupSlackUser(tenantId, id)));
|
|
968
|
-
unnamed.forEach((id, index) => {
|
|
969
|
-
const profile = profiles[index];
|
|
970
|
-
if (!profile?.fullName && !profile?.userName) return;
|
|
971
|
-
people.set(id, {
|
|
972
|
-
...people.get(id),
|
|
973
|
-
slackUserId: id,
|
|
974
|
-
...profile.fullName ? { fullName: profile.fullName } : void 0,
|
|
975
|
-
...profile.userName ? { slackUserName: profile.userName } : void 0
|
|
976
|
-
});
|
|
977
|
-
});
|
|
978
|
-
}
|
|
979
|
-
return people;
|
|
980
|
-
}
|
|
981
|
-
// src/api/conversations/mentions.ts
|
|
982
|
-
/** Text in one event that the dashboard renders with mentions. */
|
|
983
|
-
function eventMentionTexts(data) {
|
|
984
|
-
switch (data.type) {
|
|
985
|
-
case "message": return data.redacted ? [] : [
|
|
986
|
-
data.text,
|
|
987
|
-
data.trustedSummary,
|
|
988
|
-
...(data.events ?? []).map((event) => event.trustedSummary)
|
|
989
|
-
];
|
|
990
|
-
case "assistant_message": return data.parts.map((part) => part.text);
|
|
991
|
-
case "compaction":
|
|
992
|
-
case "handoff": return [data.summary];
|
|
993
|
-
case "structured_event": return [data.presentation.preview, ...(data.presentation.details ?? []).map((detail) => detail.description)];
|
|
994
|
-
default: return [];
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
function briefMentionTexts(brief) {
|
|
998
|
-
return [
|
|
999
|
-
brief.summary,
|
|
1000
|
-
brief.intent,
|
|
1001
|
-
brief.outcome.text,
|
|
1002
|
-
...brief.decisions.flatMap((decision) => [decision.text, decision.by]),
|
|
1003
|
-
...brief.openDecisions.flatMap((decision) => [decision.text, decision.owner]),
|
|
1004
|
-
...brief.facts
|
|
1005
|
-
];
|
|
1006
|
-
}
|
|
1007
|
-
function withMentions(value, mentions) {
|
|
1008
|
-
return mentions.length > 0 ? {
|
|
1009
|
-
...value,
|
|
1010
|
-
mentions
|
|
1011
|
-
} : value;
|
|
1012
|
-
}
|
|
1013
|
-
/**
|
|
1014
|
-
* Attach the people that each event and the Brief of one conversation
|
|
1015
|
-
* mention, resolved at read time.
|
|
1016
|
-
*/
|
|
1017
|
-
async function attachConversationMentions(executor, conversationId, content) {
|
|
1018
|
-
const tenantId = (await loadConversationSlackTenantIds(executor, [conversationId])).get(conversationId);
|
|
1019
|
-
const sources = content.events.map((event) => ({
|
|
1020
|
-
tenantId,
|
|
1021
|
-
texts: eventMentionTexts(event.data)
|
|
1022
|
-
}));
|
|
1023
|
-
if (content.brief) sources.push({
|
|
1024
|
-
tenantId,
|
|
1025
|
-
texts: briefMentionTexts(content.brief)
|
|
1026
|
-
});
|
|
1027
|
-
const mentions = await resolveMentions(executor, sources);
|
|
1028
|
-
return {
|
|
1029
|
-
briefMentions: content.brief ? mentions.at(-1) : [],
|
|
1030
|
-
events: content.events.map((event, index) => withMentions(event, mentions[index]))
|
|
1031
|
-
};
|
|
1032
|
-
}
|
|
1033
|
-
/** Attach the people that each activity preview mentions, resolved at read time. */
|
|
1034
|
-
async function attachActivityPreviewMentions(executor, previews) {
|
|
1035
|
-
const entries = [...previews];
|
|
1036
|
-
const tenants = await loadConversationSlackTenantIds(executor, entries.map(([conversationId]) => conversationId));
|
|
1037
|
-
const mentions = await resolveMentions(executor, entries.map(([conversationId, preview]) => ({
|
|
1038
|
-
tenantId: tenants.get(conversationId),
|
|
1039
|
-
texts: [preview.text]
|
|
1040
|
-
})));
|
|
1041
|
-
return new Map(entries.map(([conversationId, preview], index) => [conversationId, withMentions(preview, mentions[index])]));
|
|
1042
|
-
}
|
|
1043
|
-
// src/api/conversations/shared.ts
|
|
1044
|
-
/** Format a Slack destination without exposing private channel names. */
|
|
1045
|
-
function slackStatsLocationLabel(input) {
|
|
1046
|
-
const channelId = input.channel;
|
|
1047
|
-
if (!channelId) return void 0;
|
|
1048
|
-
if (input.channelNameRedacted && input.channelName) return input.channelName;
|
|
1049
|
-
const name = input.channelName?.replace(/^#/, "");
|
|
1050
|
-
if (channelId.startsWith("D")) return "Direct Message";
|
|
1051
|
-
if (channelId.startsWith("C")) return name ? `#${name}` : "Public Channel";
|
|
1052
|
-
if (channelId.startsWith("G")) {
|
|
1053
|
-
if (name?.startsWith("mpdm-")) return "Group DM";
|
|
1054
|
-
return "Private Channel";
|
|
1055
|
-
}
|
|
1056
|
-
return name || channelId;
|
|
1057
|
-
}
|
|
1058
|
-
/** Return the generic display label for a conversation surface. */
|
|
1059
|
-
function surfaceFallbackLabel(surface) {
|
|
1060
|
-
if (surface === "scheduler") return "Scheduler";
|
|
1061
|
-
if (surface === "api") return "API";
|
|
1062
|
-
if (surface === "internal") return "Internal";
|
|
1063
|
-
return "Conversation";
|
|
1064
|
-
}
|
|
1065
|
-
// src/api/conversations/projection.ts
|
|
1066
|
-
const PRIVATE_CONVERSATION_LABEL = "Private Conversation";
|
|
1067
|
-
function privateConversationLabel(slackConversation) {
|
|
1068
|
-
if (!slackConversation) return PRIVATE_CONVERSATION_LABEL;
|
|
1069
|
-
return slackConversation.visibility === "private" ? formatSlackConversationRedactedLabel(slackConversation) ?? PRIVATE_CONVERSATION_LABEL : PRIVATE_CONVERSATION_LABEL;
|
|
1070
|
-
}
|
|
1071
|
-
function surfaceFromConversationId(conversationId) {
|
|
1072
|
-
if (parseSlackThreadId(conversationId)) return "slack";
|
|
1073
|
-
if (conversationId.startsWith("scheduler:")) return "scheduler";
|
|
1074
|
-
if (conversationId.startsWith("api:")) return "api";
|
|
1075
|
-
return "internal";
|
|
1076
|
-
}
|
|
1077
|
-
function surfaceFromSource(source, conversationId) {
|
|
1078
|
-
if (source === "slack" || source === "api" || source === "web" || source === "scheduler") return source === "web" ? "api" : source;
|
|
1079
|
-
return surfaceFromConversationId(conversationId);
|
|
1080
|
-
}
|
|
1081
|
-
function locationUrlFromConversation(args) {
|
|
1082
|
-
const { conversation } = args;
|
|
1083
|
-
if (!args.canViewPrivateContent || conversation.location?.provider !== "slack") return;
|
|
1084
|
-
const teamDomain = args.teamDomainByTeamId?.get(conversation.location.teamId);
|
|
1085
|
-
const threadTs = conversation.location.threadTs;
|
|
1086
|
-
if (!teamDomain || !threadTs) return void 0;
|
|
1087
|
-
return buildSlackLocationUrl({
|
|
1088
|
-
channelId: conversation.location.channelId,
|
|
1089
|
-
teamDomain,
|
|
1090
|
-
threadTs
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
function actorIdentityReport(actor) {
|
|
1094
|
-
if (!actor) return void 0;
|
|
1095
|
-
const identity = {
|
|
1096
|
-
...actor.email !== void 0 ? { email: actor.email } : void 0,
|
|
1097
|
-
...actor.fullName !== void 0 ? { fullName: actor.fullName } : void 0,
|
|
1098
|
-
...actor.slackUserId !== void 0 ? { slackUserId: actor.slackUserId } : void 0,
|
|
1099
|
-
...actor.slackUserName !== void 0 ? { slackUserName: actor.slackUserName } : void 0
|
|
1100
|
-
};
|
|
1101
|
-
return Object.keys(identity).length > 0 ? identity : void 0;
|
|
1102
|
-
}
|
|
1103
|
-
function statusFromConversation(conversation) {
|
|
1104
|
-
if (conversation.execution.status === "idle") return "completed";
|
|
1105
|
-
if (conversation.execution.status === "failed") return "failed";
|
|
1106
|
-
return "active";
|
|
1107
|
-
}
|
|
1108
|
-
function titleFromConversation(args) {
|
|
1109
|
-
const slackThread = parseSlackThreadId(args.conversation.conversationId);
|
|
1110
|
-
const effectiveChannelName = args.conversation.channelName;
|
|
1111
|
-
const slackConversation = resolveSlackConversationContextFromThreadId({
|
|
1112
|
-
threadId: args.conversation.conversationId,
|
|
1113
|
-
channelName: effectiveChannelName,
|
|
1114
|
-
...args.visibility ? { visibility: args.visibility } : void 0
|
|
1115
|
-
});
|
|
1116
|
-
return (args.canViewPrivateContent ? void 0 : privateConversationLabel(slackConversation)) ?? args.conversation.title ?? args.automationTitle ?? slackStatsLocationLabel({
|
|
1117
|
-
channel: slackThread?.channelId,
|
|
1118
|
-
channelName: effectiveChannelName
|
|
1119
|
-
}) ?? (args.conversation.source === "plugin" ? "Automation run" : fallbackShortTitle(args.firstHumanMessageText ?? "", surfaceFallbackLabel(args.surface)));
|
|
1120
|
-
}
|
|
1121
|
-
function channelNameFromConversation(conversation, canViewPrivateContent, visibility) {
|
|
1122
|
-
const effectiveChannelName = conversation.channelName;
|
|
1123
|
-
const slackThread = parseSlackThreadId(conversation.conversationId);
|
|
1124
|
-
if (!effectiveChannelName && !slackThread) return;
|
|
1125
|
-
const slackConversation = resolveSlackConversationContextFromThreadId({
|
|
1126
|
-
threadId: conversation.conversationId,
|
|
1127
|
-
channelName: effectiveChannelName,
|
|
1128
|
-
...visibility ? { visibility } : void 0
|
|
1129
|
-
});
|
|
1130
|
-
if (!canViewPrivateContent) return privateConversationLabel(slackConversation);
|
|
1131
|
-
return effectiveChannelName;
|
|
1132
|
-
}
|
|
1133
|
-
function channelNameRedactedFromConversation(conversation, canViewPrivateContent) {
|
|
1134
|
-
const effectiveChannelName = conversation.channelName;
|
|
1135
|
-
const slackThread = parseSlackThreadId(conversation.conversationId);
|
|
1136
|
-
if (!effectiveChannelName && !slackThread) return false;
|
|
1137
|
-
return !canViewPrivateContent;
|
|
1138
|
-
}
|
|
1139
|
-
/** Describe whether a viewer can read one conversation's retained events. */
|
|
1140
|
-
function conversationEventHistory(args) {
|
|
1141
|
-
if (args.transcriptPurgedAtMs !== void 0) return {
|
|
1142
|
-
status: "expired",
|
|
1143
|
-
expiredAt: new Date(args.transcriptPurgedAtMs).toISOString()
|
|
1144
|
-
};
|
|
1145
|
-
return args.canExposePayload ? { status: "available" } : {
|
|
1146
|
-
status: "redacted",
|
|
1147
|
-
reason: "non_public_conversation"
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1150
|
-
/** Project one durable conversation and its SQL metrics into the REST summary. */
|
|
1151
|
-
function conversationSummaryFromStoredConversation(args) {
|
|
1152
|
-
const { conversation, durationMs, usage } = args;
|
|
1153
|
-
const canViewPrivateContent = args.access?.canViewPrivateContent ?? false;
|
|
1154
|
-
const accessVisibility = args.access?.visibility;
|
|
1155
|
-
const visibility = accessVisibility === "public" || accessVisibility === "private" ? accessVisibility : void 0;
|
|
1156
|
-
const surface = surfaceFromSource(conversation.source, conversation.conversationId);
|
|
1157
|
-
const actorIdentity = actorIdentityReport(conversation.actor);
|
|
1158
|
-
const locationUrl = locationUrlFromConversation({
|
|
1159
|
-
canViewPrivateContent,
|
|
1160
|
-
conversation,
|
|
1161
|
-
...args.teamDomainByTeamId ? { teamDomainByTeamId: args.teamDomainByTeamId } : void 0
|
|
1162
|
-
});
|
|
1163
|
-
const slackThread = parseSlackThreadId(conversation.conversationId);
|
|
1164
|
-
const channelName = channelNameFromConversation(conversation, canViewPrivateContent, visibility);
|
|
1165
|
-
const channelNameRedacted = channelNameRedactedFromConversation(conversation, canViewPrivateContent);
|
|
1166
|
-
return {
|
|
1167
|
-
conversationId: conversation.conversationId,
|
|
1168
|
-
cumulativeDurationMs: durationMs,
|
|
1169
|
-
displayTitle: titleFromConversation({
|
|
1170
|
-
automationTitle: args.automationTitle,
|
|
1171
|
-
canViewPrivateContent,
|
|
1172
|
-
conversation,
|
|
1173
|
-
firstHumanMessageText: args.firstHumanMessageText,
|
|
1174
|
-
surface,
|
|
1175
|
-
visibility
|
|
1176
|
-
}),
|
|
1177
|
-
isParticipant: args.access?.isParticipant ?? false,
|
|
1178
|
-
...visibility ? { visibility } : void 0,
|
|
1179
|
-
lastProgressAt: new Date(conversation.execution.updatedAtMs ?? conversation.updatedAtMs).toISOString(),
|
|
1180
|
-
lastSeenAt: new Date(conversation.lastActivityAtMs).toISOString(),
|
|
1181
|
-
startedAt: new Date(conversation.createdAtMs).toISOString(),
|
|
1182
|
-
status: statusFromConversation(conversation),
|
|
1183
|
-
surface,
|
|
1184
|
-
...args.auxiliaryCosts ? { auxiliaryCosts: args.auxiliaryCosts } : void 0,
|
|
1185
|
-
...usage ? { cumulativeUsage: usage } : void 0,
|
|
1186
|
-
...args.childAgentTotals ? { childAgentTotals: args.childAgentTotals } : void 0,
|
|
1187
|
-
...actorIdentity ? { actorIdentity } : void 0,
|
|
1188
|
-
...locationUrl ? { locationUrl } : void 0,
|
|
1189
|
-
archivedAt: args.archivedAtMs === void 0 ? null : new Date(args.archivedAtMs).toISOString(),
|
|
1190
|
-
...slackThread ? { channel: slackThread.channelId } : void 0,
|
|
1191
|
-
...channelName ? { channelName } : void 0,
|
|
1192
|
-
...channelNameRedacted ? { channelNameRedacted: true } : void 0,
|
|
1193
|
-
...args.locationId && !channelNameRedacted ? { locationId: args.locationId } : void 0
|
|
1194
|
-
};
|
|
1195
|
-
}
|
|
1196
|
-
// src/chat/plugins/unfinished-work.ts
|
|
1197
|
-
/**
|
|
1198
|
-
* Return assigned and unfinished plugin work for the candidate conversations.
|
|
1199
|
-
* Feed Priority combines these signals with conversation activity on the host.
|
|
1200
|
-
*/
|
|
1201
|
-
async function listConversationWork(conversationIds) {
|
|
1202
|
-
if (conversationIds.length === 0) return {
|
|
1203
|
-
assignedIds: [],
|
|
1204
|
-
finishedAtById: {},
|
|
1205
|
-
unfinishedIds: []
|
|
1206
|
-
};
|
|
1207
|
-
const candidates = new Set(conversationIds);
|
|
1208
|
-
const assigned = /* @__PURE__ */ new Set();
|
|
1209
|
-
const finishedAtById = /* @__PURE__ */ new Map();
|
|
1210
|
-
const unfinished = /* @__PURE__ */ new Set();
|
|
1211
|
-
for (const plugin of getPlugins()) {
|
|
1212
|
-
const hook = plugin.hooks?.unfinishedWork;
|
|
1213
|
-
if (!hook) continue;
|
|
1214
|
-
try {
|
|
1215
|
-
const result = await hook({
|
|
1216
|
-
conversationIds,
|
|
1217
|
-
db: getDb(),
|
|
1218
|
-
log: createPluginLogger(plugin.manifest.name),
|
|
1219
|
-
plugin: { name: plugin.manifest.name },
|
|
1220
|
-
cache: createPluginCache(plugin.manifest.name)
|
|
1221
|
-
});
|
|
1222
|
-
for (const conversationId of result.conversationIds) {
|
|
1223
|
-
if (!candidates.has(conversationId)) continue;
|
|
1224
|
-
unfinished.add(conversationId);
|
|
1225
|
-
assigned.add(conversationId);
|
|
1226
|
-
}
|
|
1227
|
-
for (const conversationId of result.assignedConversationIds ?? []) if (candidates.has(conversationId)) assigned.add(conversationId);
|
|
1228
|
-
for (const [conversationId, finishedAt] of Object.entries(result.finishedWorkAtByConversationId ?? {})) {
|
|
1229
|
-
if (!candidates.has(conversationId)) continue;
|
|
1230
|
-
const time = Date.parse(finishedAt);
|
|
1231
|
-
if (!Number.isFinite(time)) continue;
|
|
1232
|
-
const current = finishedAtById.get(conversationId);
|
|
1233
|
-
if (!current || time > Date.parse(current)) finishedAtById.set(conversationId, finishedAt);
|
|
1234
|
-
}
|
|
1235
|
-
} catch (error) {
|
|
1236
|
-
logWarn("plugin.unfinished_work.hook.failed", {
|
|
1237
|
-
"app.plugin.name": plugin.manifest.name,
|
|
1238
|
-
"exception.message": error instanceof Error ? error.message : String(error)
|
|
1239
|
-
});
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
return {
|
|
1243
|
-
assignedIds: conversationIds.filter((conversationId) => assigned.has(conversationId)),
|
|
1244
|
-
finishedAtById: Object.fromEntries(conversationIds.flatMap((conversationId) => {
|
|
1245
|
-
const finishedAt = finishedAtById.get(conversationId);
|
|
1246
|
-
return finishedAt ? [[conversationId, finishedAt]] : [];
|
|
1247
|
-
})),
|
|
1248
|
-
unfinishedIds: conversationIds.filter((conversationId) => unfinished.has(conversationId))
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
// src/api/conversations/priority.ts
|
|
1252
|
-
/** Conversations with no known work leave Priority after this idle window. */
|
|
1253
|
-
const UNASSIGNED_PRIORITY_WINDOW_MS = 108e5;
|
|
1254
|
-
/** Unfinished work leaves Priority after this idle window. */
|
|
1255
|
-
const UNFINISHED_PRIORITY_WINDOW_MS = 1728e5;
|
|
1256
|
-
/**
|
|
1257
|
-
* Decide whether a conversation belongs in the dashboard Priority section.
|
|
1258
|
-
*
|
|
1259
|
-
* Priority includes only:
|
|
1260
|
-
* - unfinished work last seen within 48 hours
|
|
1261
|
-
* - finished assigned work with a user message after the finish time
|
|
1262
|
-
* - no known work last seen within 3 hours
|
|
1263
|
-
*
|
|
1264
|
-
* Finished assigned work with no later activity stays out of Priority.
|
|
1265
|
-
*/
|
|
1266
|
-
function isConversationPriority(conversation, nowMs) {
|
|
1267
|
-
const lastSeenAt = Date.parse(conversation.lastSeenAt);
|
|
1268
|
-
if (!Number.isFinite(lastSeenAt)) return false;
|
|
1269
|
-
if (conversation.unfinishedWork) return nowMs - lastSeenAt <= UNFINISHED_PRIORITY_WINDOW_MS;
|
|
1270
|
-
if (conversation.assignedWork) {
|
|
1271
|
-
const finishedAt = Date.parse(conversation.finishedWorkAt ?? "");
|
|
1272
|
-
const lastUserMessageAt = Date.parse(conversation.lastUserMessageAt ?? "");
|
|
1273
|
-
return Number.isFinite(finishedAt) && Number.isFinite(lastUserMessageAt) && lastUserMessageAt > finishedAt;
|
|
1274
|
-
}
|
|
1275
|
-
return nowMs - lastSeenAt <= UNASSIGNED_PRIORITY_WINDOW_MS;
|
|
1276
|
-
}
|
|
1277
|
-
// src/api/conversations/user-message-activity.ts
|
|
1278
|
-
/** Read the latest true user message time for each selected conversation. */
|
|
1279
|
-
async function readLastUserMessageAtByConversation(db, conversationIds) {
|
|
1280
|
-
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1281
|
-
const authority = sql`${juniorConversationEvents.payload}->'provenance'->>'authority'`;
|
|
1282
|
-
const actorPlatform = sql`${juniorConversationEvents.payload}->'provenance'->'actor'->>'platform'`;
|
|
1283
|
-
const role = sql`${juniorConversationEvents.payload}->>'role'`;
|
|
1284
|
-
const rows = await db.select({
|
|
1285
|
-
conversationId: juniorConversationEvents.conversationId,
|
|
1286
|
-
lastUserMessageAt: sql`max(${juniorConversationEvents.createdAt})`
|
|
1287
|
-
}).from(juniorConversationEvents).where(and(inArray(juniorConversationEvents.conversationId, conversationIds), or(and(eq(juniorConversationEvents.type, "user_message"), eq(authority, "instruction"), sql`${actorPlatform} in ('slack', 'local', 'web')`), and(eq(juniorConversationEvents.type, "message"), eq(role, "user"), isNotNull(juniorConversationEvents.actorIdentityId))))).groupBy(juniorConversationEvents.conversationId);
|
|
1288
|
-
return new Map(rows.map((row) => [row.conversationId, new Date(row.lastUserMessageAt).toISOString()]));
|
|
1289
|
-
}
|
|
1290
|
-
// src/api/conversations/activity-preview.ts
|
|
1291
|
-
/** Read one recent assistant message, or the latest user message, for each conversation. */
|
|
1292
|
-
async function readConversationActivityPreviews(db, conversationIds) {
|
|
1293
|
-
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1294
|
-
const role = sql`${juniorConversationEvents.payload}->>'role'`;
|
|
1295
|
-
const text = sql`${juniorConversationEvents.payload}->>'text'`;
|
|
1296
|
-
const assistantFirst = sql`case when ${role} = 'assistant' then 0 else 1 end`;
|
|
1297
|
-
const rows = await db.selectDistinctOn([juniorConversationEvents.conversationId], {
|
|
1298
|
-
conversationId: juniorConversationEvents.conversationId,
|
|
1299
|
-
createdAt: juniorConversationEvents.createdAt,
|
|
1300
|
-
role: role.as("role"),
|
|
1301
|
-
text: text.as("text")
|
|
1302
|
-
}).from(juniorConversationEvents).where(and(inArray(juniorConversationEvents.conversationId, conversationIds), eq(juniorConversationEvents.type, "message"), sql`${role} in ('assistant', 'user')`, isNotNull(sql`nullif(btrim(${text}), '')`))).orderBy(juniorConversationEvents.conversationId, assistantFirst, desc(juniorConversationEvents.createdAt), desc(juniorConversationEvents.seq));
|
|
1303
|
-
return new Map(rows.map((row) => [row.conversationId, {
|
|
1304
|
-
createdAt: row.createdAt.toISOString(),
|
|
1305
|
-
role: row.role,
|
|
1306
|
-
text: row.text
|
|
1307
|
-
}]));
|
|
1308
|
-
}
|
|
1309
|
-
// src/api/conversations/participants.ts
|
|
1310
|
-
const identityColumns = {
|
|
1311
|
-
displayName: juniorIdentities.displayName,
|
|
1312
|
-
email: juniorIdentities.email,
|
|
1313
|
-
handle: juniorIdentities.handle,
|
|
1314
|
-
identityId: juniorIdentities.id,
|
|
1315
|
-
provider: juniorIdentities.provider,
|
|
1316
|
-
providerSubjectId: juniorIdentities.providerSubjectId,
|
|
1317
|
-
userDisplayName: juniorUsers.displayName,
|
|
1318
|
-
userEmail: juniorUsers.primaryEmail,
|
|
1319
|
-
userId: juniorIdentities.userId
|
|
1320
|
-
};
|
|
1321
|
-
function participantIdentity(row) {
|
|
1322
|
-
const fullName = row.userDisplayName?.trim() || row.displayName?.trim();
|
|
1323
|
-
const email = row.email?.trim() || row.userEmail?.trim();
|
|
1324
|
-
const slack = row.provider === "slack";
|
|
1325
|
-
const participant = {
|
|
1326
|
-
...email ? { email } : void 0,
|
|
1327
|
-
...fullName ? { fullName } : void 0,
|
|
1328
|
-
...slack && row.providerSubjectId ? { slackUserId: row.providerSubjectId } : void 0,
|
|
1329
|
-
...slack && row.handle ? { slackUserName: row.handle } : void 0
|
|
1330
|
-
};
|
|
1331
|
-
return Object.keys(participant).length > 0 ? participant : void 0;
|
|
1332
|
-
}
|
|
1333
|
-
/** Read actor identities in first-appearance order for each Conversation. */
|
|
1334
|
-
async function readConversationParticipants(db, conversationIds) {
|
|
1335
|
-
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
1336
|
-
const [rootRows, eventRows] = await Promise.all([db.select({
|
|
1337
|
-
conversationId: juniorConversations.conversationId,
|
|
1338
|
-
...identityColumns
|
|
1339
|
-
}).from(juniorConversations).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(inArray(juniorConversations.conversationId, [...conversationIds])), db.selectDistinctOn([juniorConversationEvents.conversationId, juniorConversationEvents.actorIdentityId], {
|
|
1340
|
-
conversationId: juniorConversationEvents.conversationId,
|
|
1341
|
-
seq: juniorConversationEvents.seq,
|
|
1342
|
-
...identityColumns
|
|
1343
|
-
}).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))]);
|
|
1344
|
-
const ordered = /* @__PURE__ */ new Map();
|
|
1345
|
-
const add = (conversationId, row) => {
|
|
1346
|
-
const participant = participantIdentity(row);
|
|
1347
|
-
if (!participant) return;
|
|
1348
|
-
const group = ordered.get(conversationId) ?? {
|
|
1349
|
-
identities: [],
|
|
1350
|
-
keys: /* @__PURE__ */ new Set()
|
|
1351
|
-
};
|
|
1352
|
-
const key = row.userId ? `user:${row.userId}` : `identity:${row.identityId}`;
|
|
1353
|
-
if (!group.keys.has(key)) {
|
|
1354
|
-
group.keys.add(key);
|
|
1355
|
-
group.identities.push(participant);
|
|
1356
|
-
}
|
|
1357
|
-
ordered.set(conversationId, group);
|
|
1358
|
-
};
|
|
1359
|
-
for (const row of rootRows) add(row.conversationId, row);
|
|
1360
|
-
for (const row of eventRows.sort((left, right) => left.seq - right.seq)) add(row.conversationId, row);
|
|
1361
|
-
return new Map([...ordered].map(([conversationId, group]) => [conversationId, group.identities]));
|
|
1362
|
-
}
|
|
1363
|
-
// src/api/conversations/list.ts
|
|
1364
|
-
const CONVERSATION_FEED_LIMIT = 50;
|
|
1365
|
-
function conversationFeedMembershipFilter(status, filter) {
|
|
1366
|
-
if (!filter) return status === "archived" ? sql`false` : void 0;
|
|
1367
|
-
if (filter.kind === "viewer") return and(conversationIsMemberRoot({ userId: filter.userId }), status === "archived" ? conversationArchivedForUser(filter.userId) : status === "all" ? void 0 : conversationNotArchivedForUser(filter.userId));
|
|
1368
|
-
return and(conversationIsMemberRoot({ email: filter.email }), status === "archived" ? conversationArchivedForEmail(filter.email) : status === "all" ? void 0 : conversationNotArchivedForEmail(filter.email));
|
|
1369
|
-
}
|
|
1370
|
-
/**
|
|
1371
|
-
* Keep root conversations linked to one code repository. A code change can
|
|
1372
|
-
* link a child conversation, so linked ids resolve to their root. Private
|
|
1373
|
-
* roots stay visible only to their members.
|
|
1374
|
-
*/
|
|
1375
|
-
function conversationCodeRepositoryFilter(repositoryId, viewerUserId) {
|
|
1376
|
-
return and(sql`${juniorConversations.conversationId} IN (
|
|
1377
|
-
SELECT coalesce(linked.root_conversation_id, linked.conversation_id)
|
|
1378
|
-
FROM ${juniorConversations} AS linked
|
|
1379
|
-
WHERE linked.conversation_id IN (
|
|
1380
|
-
SELECT unnest(${juniorCodeChanges.conversationIds})
|
|
1381
|
-
FROM ${juniorCodeChanges}
|
|
1382
|
-
WHERE ${juniorCodeChanges.repositoryId} = ${repositoryId}
|
|
1383
|
-
)
|
|
1384
|
-
)`, or(eq(juniorDestinations.visibility, "public"), viewerUserId ? viewerConversationMembership({
|
|
1385
|
-
actorMatch: eq(juniorIdentities.userId, viewerUserId),
|
|
1386
|
-
participantMatch: conversationHasParticipantUser(viewerUserId)
|
|
1387
|
-
}) : void 0));
|
|
1388
|
-
}
|
|
1389
|
-
async function conversationRows(db, limit, status, filter, query, includePrivateBriefs = false, codeRepository) {
|
|
1390
|
-
return db.select({
|
|
1391
|
-
conversation: conversationReadColumns(),
|
|
1392
|
-
location: juniorDestinations,
|
|
1393
|
-
identityDisplayName: juniorIdentities.displayName,
|
|
1394
|
-
identityEmail: juniorIdentities.email,
|
|
1395
|
-
identityHandle: juniorIdentities.handle,
|
|
1396
|
-
identityProvider: juniorIdentities.provider,
|
|
1397
|
-
identitySubjectId: juniorIdentities.providerSubjectId,
|
|
1398
|
-
identityTenantId: juniorIdentities.providerTenantId,
|
|
1399
|
-
userDisplayName: juniorUsers.displayName
|
|
1400
|
-
}).from(juniorConversations).leftJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(and(isNull(juniorConversations.parentConversationId), conversationFeedMembershipFilter(status, filter), codeRepository ? conversationCodeRepositoryFilter(codeRepository.id, codeRepository.viewerUserId) : void 0, query ? or(sql`strpos(lower(coalesce(${juniorConversations.title}, '')), ${query}) > 0`, sql`exists (
|
|
1401
|
-
select 1
|
|
1402
|
-
from ${juniorConversationBriefs}
|
|
1403
|
-
where ${juniorConversationBriefs.conversationId} = ${juniorConversations.conversationId}
|
|
1404
|
-
and ${includePrivateBriefs ? sql`true` : eq(juniorDestinations.visibility, "public")}
|
|
1405
|
-
and ${juniorConversationBriefs.version} = (
|
|
1406
|
-
select max(${juniorConversationBriefs.version})
|
|
1407
|
-
from ${juniorConversationBriefs}
|
|
1408
|
-
where ${juniorConversationBriefs.conversationId} = ${juniorConversations.conversationId}
|
|
1409
|
-
)
|
|
1410
|
-
and to_tsvector('english', ${juniorConversationBriefs.searchText})
|
|
1411
|
-
@@ websearch_to_tsquery('english', ${query})
|
|
1412
|
-
)`) : void 0)).orderBy(desc(juniorConversations.lastActivityAt), asc(juniorConversations.conversationId)).limit(limit);
|
|
1413
|
-
}
|
|
1414
|
-
/** Decode a conversation row with the linked user name and identity-scoped provider fields. */
|
|
1415
|
-
function conversationFromRow(row) {
|
|
1416
|
-
const value = row.conversation;
|
|
1417
|
-
const legacySessionSource = value.sessionSource === null ? void 0 : parseSessionSource(value.sessionSource);
|
|
1418
|
-
if (value.sessionSource !== null && !legacySessionSource) throw new Error("Conversation legacy source_json is invalid");
|
|
1419
|
-
const actorFullName = row.userDisplayName?.trim() ? row.userDisplayName : row.identityDisplayName;
|
|
1420
|
-
const actor = row.identityProvider === "slack" ? {
|
|
1421
|
-
platform: "slack",
|
|
1422
|
-
...row.identityEmail ? { email: row.identityEmail } : void 0,
|
|
1423
|
-
...actorFullName ? { fullName: actorFullName } : void 0,
|
|
1424
|
-
...row.identitySubjectId ? { slackUserId: row.identitySubjectId } : void 0,
|
|
1425
|
-
...row.identityHandle ? { slackUserName: row.identityHandle } : void 0,
|
|
1426
|
-
...row.identityTenantId ? { teamId: row.identityTenantId } : void 0
|
|
1427
|
-
} : void 0;
|
|
1428
|
-
const location = locationFromRow(value.location, row.location, legacySessionSource);
|
|
1429
|
-
return {
|
|
1430
|
-
schemaVersion: 1,
|
|
1431
|
-
conversationId: value.conversationId,
|
|
1432
|
-
createdAtMs: value.createdAt.getTime(),
|
|
1433
|
-
lastActivityAtMs: value.lastActivityAt.getTime(),
|
|
1434
|
-
updatedAtMs: value.updatedAt.getTime(),
|
|
1435
|
-
execution: {
|
|
1436
|
-
status: value.executionStatus,
|
|
1437
|
-
...value.runId ? { runId: value.runId } : void 0,
|
|
1438
|
-
...value.executionUpdatedAt ? { updatedAtMs: value.executionUpdatedAt.getTime() } : void 0
|
|
1439
|
-
},
|
|
1440
|
-
...actor ? { actor } : void 0,
|
|
1441
|
-
...location ? { location } : void 0,
|
|
1442
|
-
...value.channelName ? { channelName: value.channelName } : void 0,
|
|
1443
|
-
...value.source ? { source: value.source } : void 0,
|
|
1444
|
-
...value.title ? { title: value.title } : void 0,
|
|
1445
|
-
...value.transcriptPurgedAt ? { transcriptPurgedAtMs: value.transcriptPurgedAt.getTime() } : void 0
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1448
|
-
/** Read one normalized conversation record directly from its SQL row. */
|
|
1449
|
-
async function readConversationRecordFromSql(conversationId) {
|
|
1450
|
-
const row = (await getDb().select({
|
|
1451
|
-
conversation: conversationReadColumns(),
|
|
1452
|
-
location: juniorDestinations,
|
|
1453
|
-
identityDisplayName: juniorIdentities.displayName,
|
|
1454
|
-
identityEmail: juniorIdentities.email,
|
|
1455
|
-
identityHandle: juniorIdentities.handle,
|
|
1456
|
-
identityProvider: juniorIdentities.provider,
|
|
1457
|
-
identitySubjectId: juniorIdentities.providerSubjectId,
|
|
1458
|
-
identityTenantId: juniorIdentities.providerTenantId,
|
|
1459
|
-
userDisplayName: juniorUsers.displayName
|
|
1460
|
-
}).from(juniorConversations).leftJoin(juniorDestinations, eq(juniorDestinations.id, juniorConversations.destinationId)).leftJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).leftJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(eq(juniorConversations.conversationId, conversationId)).limit(1))[0];
|
|
1461
|
-
return row ? {
|
|
1462
|
-
conversation: conversationFromRow(row),
|
|
1463
|
-
durationMs: row.conversation.durationMs,
|
|
1464
|
-
...row.location?.visibility === "public" ? { locationId: row.location.id } : void 0,
|
|
1465
|
-
usage: row.conversation.usage,
|
|
1466
|
-
rootConversationId: row.conversation.rootConversationId
|
|
1467
|
-
} : void 0;
|
|
1468
|
-
}
|
|
1469
|
-
/** Prefer the authenticated viewer; fall back to actor email membership. */
|
|
1470
|
-
function conversationFeedFilter(options) {
|
|
1471
|
-
if (options.viewer) return {
|
|
1472
|
-
kind: "viewer",
|
|
1473
|
-
userId: options.viewer.id
|
|
1474
|
-
};
|
|
1475
|
-
if (options.actorEmail) return {
|
|
1476
|
-
kind: "actorEmail",
|
|
1477
|
-
email: options.actorEmail
|
|
1478
|
-
};
|
|
1479
|
-
}
|
|
1480
|
-
/**
|
|
1481
|
-
* Build a bounded dashboard feed. Prefer the viewer user when present; otherwise
|
|
1482
|
-
* keep only roots linked to actorEmail before applying the limit. Membership is
|
|
1483
|
-
* root-actor ownership or a materialized participant row for that person.
|
|
1484
|
-
*
|
|
1485
|
-
* `codeRepositoryId` replaces membership with every root linked to that
|
|
1486
|
-
* repository's code changes. Private roots appear only for their members, and
|
|
1487
|
-
* per-person archive state does not hide rows.
|
|
1488
|
-
*/
|
|
1489
|
-
async function readConversationFeedFromSql(options = {}) {
|
|
1490
|
-
const nowMs = Date.now();
|
|
1491
|
-
const db = getDb();
|
|
1492
|
-
const filter = options.codeRepositoryId ? void 0 : conversationFeedFilter(options);
|
|
1493
|
-
const query = options.q?.trim().toLowerCase() || void 0;
|
|
1494
|
-
const rows = await conversationRows(db, options.limit ?? CONVERSATION_FEED_LIMIT, query || options.codeRepositoryId ? "all" : options.status ?? "active", filter, query, filter?.kind === "viewer", options.codeRepositoryId ? {
|
|
1495
|
-
id: options.codeRepositoryId,
|
|
1496
|
-
...options.viewer ? { viewerUserId: options.viewer.id } : void 0
|
|
1497
|
-
} : void 0);
|
|
1498
|
-
const conversations = rows.map((row) => conversationFromRow(row));
|
|
1499
|
-
const conversationIds = conversations.map((conversation) => conversation.conversationId);
|
|
1500
|
-
const [accessByConversation, archiveTimesByConversation, annotationsByConversation, auxiliaryCostsByRoot, metricsByRoot, participantsByConversation, teamDomainByTeamId, conversationWork, lastUserMessageAtByConversation, activityPreviewByConversation, automationTitleByConversation, firstHumanMessageTextByConversation] = await Promise.all([
|
|
1501
|
-
readConversationAccessFromSql(db, conversationIds, options.viewer),
|
|
1502
|
-
conversationArchiveTimes(db, conversationIds, filter?.kind === "viewer" ? { userId: filter.userId } : filter?.kind === "actorEmail" ? { email: filter.email } : void 0),
|
|
1503
|
-
listConversationAnnotationsById(db, conversationIds),
|
|
1504
|
-
readConversationAuxiliaryCostsFromSql(db, conversationIds),
|
|
1505
|
-
readConversationTreeMetricsFromSql(db, conversationIds),
|
|
1506
|
-
readConversationParticipants(db, conversationIds),
|
|
1507
|
-
resolveSlackTeamDomains(conversations.flatMap((conversation) => conversation.location?.provider === "slack" ? [conversation.location.teamId] : [])),
|
|
1508
|
-
listConversationWork(conversationIds),
|
|
1509
|
-
readLastUserMessageAtByConversation(db, conversationIds),
|
|
1510
|
-
readConversationActivityPreviews(db, conversationIds),
|
|
1511
|
-
readAutomationTitlesByConversationId(conversations),
|
|
1512
|
-
readFirstHumanMessageTexts(db, conversations)
|
|
1513
|
-
]);
|
|
1514
|
-
const visibleActivityPreviews = await attachActivityPreviewMentions(getSqlExecutor(), new Map([...activityPreviewByConversation].filter(([conversationId]) => accessByConversation.get(conversationId)?.canViewPrivateContent)));
|
|
1515
|
-
const sidebarAnnotationsByConversation = await listConversationSidebarAnnotations(conversationIds, new Map(conversationIds.map((conversationId) => [conversationId, accessByConversation.get(conversationId)?.canViewPrivateContent ? annotationsByConversation.get(conversationId) ?? [] : []])));
|
|
1516
|
-
const assignedWork = new Set(conversationWork.assignedIds);
|
|
1517
|
-
const unfinishedWork = new Set(conversationWork.unfinishedIds);
|
|
1518
|
-
return {
|
|
1519
|
-
conversations: conversations.map((conversation, index) => {
|
|
1520
|
-
const row = rows[index];
|
|
1521
|
-
const access = accessByConversation.get(conversation.conversationId);
|
|
1522
|
-
const metrics = metricsByRoot.get(conversation.conversationId);
|
|
1523
|
-
const auxiliaryCosts = auxiliaryCostsByRoot.get(conversation.conversationId);
|
|
1524
|
-
const childAgentTotals = childAgentTotalsReport(metrics, auxiliaryCosts);
|
|
1525
|
-
const archivedAtMs = archiveTimesByConversation.get(conversation.conversationId);
|
|
1526
|
-
const summary = conversationSummaryFromStoredConversation({
|
|
1527
|
-
...archivedAtMs === void 0 ? void 0 : { archivedAtMs },
|
|
1528
|
-
automationTitle: automationTitleByConversation.get(conversation.conversationId),
|
|
1529
|
-
conversation,
|
|
1530
|
-
access,
|
|
1531
|
-
auxiliaryCosts: auxiliaryCosts?.total,
|
|
1532
|
-
...childAgentTotals ? { childAgentTotals } : void 0,
|
|
1533
|
-
durationMs: metrics?.durationMs ?? row.conversation.durationMs,
|
|
1534
|
-
firstHumanMessageText: firstHumanMessageTextByConversation.get(conversation.conversationId),
|
|
1535
|
-
teamDomainByTeamId,
|
|
1536
|
-
...row.location?.visibility === "public" ? { locationId: row.location.id } : void 0,
|
|
1537
|
-
usage: metrics?.usage ?? row.conversation.usage ?? void 0
|
|
1538
|
-
});
|
|
1539
|
-
const work = {
|
|
1540
|
-
...assignedWork.has(conversation.conversationId) ? { assignedWork: true } : void 0,
|
|
1541
|
-
...conversationWork.finishedAtById[conversation.conversationId] ? { finishedWorkAt: conversationWork.finishedAtById[conversation.conversationId] } : void 0,
|
|
1542
|
-
...unfinishedWork.has(conversation.conversationId) ? { unfinishedWork: true } : void 0
|
|
1543
|
-
};
|
|
1544
|
-
const annotations = access?.canViewPrivateContent ? annotationsByConversation.get(conversation.conversationId) ?? [] : [];
|
|
1545
|
-
const activityPreview = visibleActivityPreviews.get(conversation.conversationId);
|
|
1546
|
-
const participants = access?.canViewPrivateContent ? participantsByConversation.get(conversation.conversationId) : void 0;
|
|
1547
|
-
return {
|
|
1548
|
-
...summary,
|
|
1549
|
-
...work,
|
|
1550
|
-
...participants && participants.length > 0 ? { participants } : void 0,
|
|
1551
|
-
...activityPreview ? { activityPreview } : void 0,
|
|
1552
|
-
...annotations.length > 0 ? { annotations } : void 0,
|
|
1553
|
-
...sidebarAnnotationsByConversation[conversation.conversationId] ? { sidebarAnnotations: sidebarAnnotationsByConversation[conversation.conversationId] } : void 0,
|
|
1554
|
-
isPriority: isConversationPriority({
|
|
1555
|
-
lastSeenAt: summary.lastSeenAt,
|
|
1556
|
-
lastUserMessageAt: lastUserMessageAtByConversation.get(conversation.conversationId),
|
|
1557
|
-
...work
|
|
1558
|
-
}, nowMs)
|
|
1559
|
-
};
|
|
1560
|
-
}),
|
|
1561
|
-
generatedAt: new Date(nowMs).toISOString(),
|
|
1562
|
-
source: "conversation_index"
|
|
1563
|
-
};
|
|
1564
|
-
}
|
|
1565
|
-
/**
|
|
1566
|
-
* Load a bounded feed with an optional viewer-primary linked-user filter.
|
|
1567
|
-
* This filter is not an authorization boundary.
|
|
1568
|
-
*/
|
|
1569
|
-
async function readConversationFeed(options = {}) {
|
|
1570
|
-
return conversationFeedSchema.parse(await readConversationFeedFromSql(options));
|
|
1571
|
-
}
|
|
1572
448
|
// src/api/conversations/fork.ts
|
|
1573
449
|
/** Fork a Conversation that the viewer can read into a new root they own. */
|
|
1574
450
|
async function forkConversationForViewer(viewer, sourceConversationId, body) {
|
|
@@ -1846,6 +722,25 @@ async function cancelConversationPendingMessagesForViewer(viewer, conversationId
|
|
|
1846
722
|
throwApiError(500, "Unable to cancel queued messages.", error);
|
|
1847
723
|
}
|
|
1848
724
|
}
|
|
725
|
+
// src/api/conversations/steer-pending-messages.ts
|
|
726
|
+
/**
|
|
727
|
+
* Steer the viewer's own queued web messages into the conversation now.
|
|
728
|
+
*
|
|
729
|
+
* Only the author can steer a message. Ids that are not the viewer's queued
|
|
730
|
+
* web messages, or that Junior already read, are left out of the result.
|
|
731
|
+
*/
|
|
732
|
+
async function steerConversationPendingMessagesForViewer(viewer, conversationId, body) {
|
|
733
|
+
if (!await getConversationStore().get({ conversationId })) throwApiError(404, "Conversation not found.");
|
|
734
|
+
if (!(await readConversationAccessFromSql(getDb(), [conversationId], viewer)).get(conversationId)?.isParticipant) throwApiError(403, "Only conversation participants can steer queued messages.");
|
|
735
|
+
return {
|
|
736
|
+
conversationId,
|
|
737
|
+
steeredInboundMessageIds: (await steerWebPendingMessages({
|
|
738
|
+
authorId: webActorFromEmail(viewer.email).userId,
|
|
739
|
+
conversationId,
|
|
740
|
+
inboundMessageIds: body.inboundMessageIds
|
|
741
|
+
})).steeredInboundMessageIds
|
|
742
|
+
};
|
|
743
|
+
}
|
|
1849
744
|
// src/api/conversations/pending-messages.ts
|
|
1850
745
|
const slackMailboxAuthorSchema = z.object({
|
|
1851
746
|
userId: z.string(),
|
|
@@ -2486,6 +1381,12 @@ function createConversationRoutes(options) {
|
|
|
2486
1381
|
const viewer = context.get("viewer");
|
|
2487
1382
|
return jsonResponse(conversationPendingMessagesReportSchema, await requireConversationPendingMessages(conversationId, { viewer }));
|
|
2488
1383
|
});
|
|
1384
|
+
app.post("/:conversationId/pending-messages/steer", requireViewer, validateRequest("param", conversationParamsSchema, "Invalid route parameters."), validateRequest("json", steerConversationPendingMessagesBodySchema, "Invalid request body."), async (context) => {
|
|
1385
|
+
const viewer = context.get("viewer");
|
|
1386
|
+
const { conversationId } = context.req.valid("param");
|
|
1387
|
+
const body = context.req.valid("json");
|
|
1388
|
+
return jsonResponse(steerConversationPendingMessagesResponseSchema, await steerConversationPendingMessagesForViewer(viewer, conversationId, body));
|
|
1389
|
+
});
|
|
2489
1390
|
app.delete("/:conversationId/pending-messages", requireViewer, validateRequest("param", conversationParamsSchema, "Invalid route parameters."), validateRequest("json", cancelConversationPendingMessagesBodySchema, "Invalid request body."), async (context) => {
|
|
2490
1391
|
const viewer = context.get("viewer");
|
|
2491
1392
|
const { conversationId } = context.req.valid("param");
|
|
@@ -3528,10 +2429,28 @@ function activitySixHours(hours, nowMs) {
|
|
|
3528
2429
|
function statsItems(map) {
|
|
3529
2430
|
return [...map.values()].sort((left, right) => right.conversations - left.conversations || right.durationMs - left.durationMs || left.label.localeCompare(right.label));
|
|
3530
2431
|
}
|
|
3531
|
-
/**
|
|
2432
|
+
/**
|
|
2433
|
+
* Build the verified person actor predicate shared by People aggregate and
|
|
2434
|
+
* recent-row queries.
|
|
2435
|
+
*
|
|
2436
|
+
* Any provider counts, so Slack and dashboard (web) Conversations roll up to
|
|
2437
|
+
* the same person. System identities never count.
|
|
2438
|
+
*/
|
|
3532
2439
|
function verifiedActorWhere(email) {
|
|
3533
2440
|
const normalizedEmail = normalizeEmail$1(email);
|
|
3534
|
-
return and(eq(juniorIdentities.
|
|
2441
|
+
return and(eq(juniorIdentities.kind, "user"), eq(juniorIdentities.emailVerified, true), sql`${juniorUsers.primaryEmailNormalized} IS NOT NULL`, normalizedEmail ? eq(juniorUsers.primaryEmailNormalized, normalizedEmail) : void 0);
|
|
2442
|
+
}
|
|
2443
|
+
/**
|
|
2444
|
+
* Read the Slack user fields for a grouped person row.
|
|
2445
|
+
*
|
|
2446
|
+
* Web identities use the email as the provider subject, so only Slack
|
|
2447
|
+
* identities can supply these fields.
|
|
2448
|
+
*/
|
|
2449
|
+
function slackActorColumns() {
|
|
2450
|
+
return {
|
|
2451
|
+
slackUserId: sql`MAX(CASE WHEN ${juniorIdentities.provider} = 'slack' THEN ${juniorIdentities.providerSubjectId} END)`,
|
|
2452
|
+
slackUserName: sql`MAX(CASE WHEN ${juniorIdentities.provider} = 'slack' THEN ${juniorIdentities.handle} END)`
|
|
2453
|
+
};
|
|
3535
2454
|
}
|
|
3536
2455
|
/** Read only the recent conversation rows required by a People profile. */
|
|
3537
2456
|
async function recentActorRows(email) {
|
|
@@ -3545,9 +2464,9 @@ async function recentActorRows(email) {
|
|
|
3545
2464
|
executionStatus: juniorConversations.executionStatus,
|
|
3546
2465
|
executionUpdatedAt: juniorConversations.executionUpdatedAt,
|
|
3547
2466
|
fullName: juniorUsers.displayName,
|
|
3548
|
-
handle: juniorIdentities.handle
|
|
2467
|
+
handle: sql`CASE WHEN ${juniorIdentities.provider} = 'slack' THEN ${juniorIdentities.handle} END`,
|
|
3549
2468
|
lastActivityAt: juniorConversations.lastActivityAt,
|
|
3550
|
-
providerSubjectId: juniorIdentities.providerSubjectId
|
|
2469
|
+
providerSubjectId: sql`CASE WHEN ${juniorIdentities.provider} = 'slack' THEN ${juniorIdentities.providerSubjectId} END`,
|
|
3551
2470
|
source: juniorConversations.source,
|
|
3552
2471
|
title: juniorConversations.title,
|
|
3553
2472
|
updatedAt: juniorConversations.updatedAt,
|
|
@@ -3671,8 +2590,7 @@ async function readPeopleListFromSql() {
|
|
|
3671
2590
|
getDb().select({
|
|
3672
2591
|
email: juniorUsers.primaryEmailNormalized,
|
|
3673
2592
|
fullName: juniorUsers.displayName,
|
|
3674
|
-
|
|
3675
|
-
slackUserName: sql`MAX(${juniorIdentities.handle})`,
|
|
2593
|
+
...slackActorColumns(),
|
|
3676
2594
|
activeDays: conversationActiveDaysColumn(),
|
|
3677
2595
|
...peopleTreeAggregateColumns,
|
|
3678
2596
|
...conversationRangeColumns()
|
|
@@ -3777,7 +2695,7 @@ async function readPeoplePluginReports(args) {
|
|
|
3777
2695
|
reports: [],
|
|
3778
2696
|
source: "plugins"
|
|
3779
2697
|
});
|
|
3780
|
-
const { getPluginProfileReports } = await import("./credentialed-
|
|
2698
|
+
const { getPluginProfileReports } = await import("./credentialed-CIGwoLU2.js").then((n) => n.a);
|
|
3781
2699
|
return pluginOperationalReportFeedSchema.parse({
|
|
3782
2700
|
generatedAt: new Date(nowMs).toISOString(),
|
|
3783
2701
|
reports: await getPluginProfileReports({
|
|
@@ -3828,6 +2746,8 @@ function surfaceExpression() {
|
|
|
3828
2746
|
return sql`CASE
|
|
3829
2747
|
WHEN ${juniorConversations.source} IN ('api', 'scheduler', 'slack')
|
|
3830
2748
|
THEN ${juniorConversations.source}
|
|
2749
|
+
-- Dashboard web conversations share the product api surface label.
|
|
2750
|
+
WHEN ${juniorConversations.source} = 'web' THEN 'api'
|
|
3831
2751
|
WHEN ${juniorConversations.conversationId} LIKE 'slack:%' THEN 'slack'
|
|
3832
2752
|
WHEN ${juniorConversations.conversationId} LIKE 'scheduler:%' THEN 'scheduler'
|
|
3833
2753
|
WHEN ${juniorConversations.conversationId} LIKE 'api:%' THEN 'api'
|
|
@@ -3868,8 +2788,7 @@ async function readPeopleProfileFromSql(email, options = {}) {
|
|
|
3868
2788
|
getDb().select({
|
|
3869
2789
|
email: juniorUsers.primaryEmailNormalized,
|
|
3870
2790
|
fullName: juniorUsers.displayName,
|
|
3871
|
-
|
|
3872
|
-
slackUserName: sql`MAX(${juniorIdentities.handle})`,
|
|
2791
|
+
...slackActorColumns(),
|
|
3873
2792
|
activeDays: conversationActiveDaysColumn(),
|
|
3874
2793
|
...peopleTreeAggregateColumns
|
|
3875
2794
|
}).from(juniorConversations).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId)).innerJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(where).groupBy(juniorUsers.primaryEmailNormalized, juniorUsers.displayName),
|
|
@@ -4517,12 +3436,18 @@ async function changeViewerAutomationLifecycle(user, id, input) {
|
|
|
4517
3436
|
}
|
|
4518
3437
|
// src/chat/automations/editor.ts
|
|
4519
3438
|
/** Read-only editor support. Core owns schedules; plugins own Event choices. */
|
|
4520
|
-
/**
|
|
3439
|
+
/**
|
|
3440
|
+
* Return only Event types that can start a durable Automation, one entry for
|
|
3441
|
+
* each namespace, with the reach of each event.
|
|
3442
|
+
*/
|
|
4521
3443
|
function readAutomationEventCatalog() {
|
|
4522
|
-
return Object.entries(automationEventCatalog(getEventCatalog())).
|
|
3444
|
+
return Object.entries(automationEventCatalog(getEventCatalog())).map(([namespace, registration]) => ({
|
|
4523
3445
|
namespace,
|
|
4524
|
-
|
|
4525
|
-
|
|
3446
|
+
displayName: registration.displayName ?? namespace,
|
|
3447
|
+
events: Object.fromEntries(Object.entries(registration.events).map(([eventType, declaration]) => [eventType, { reach: declaration.reach }])),
|
|
3448
|
+
anyResourceFields: registration.anyResourceFields,
|
|
3449
|
+
resourceTypes: registration.resourceTypes
|
|
3450
|
+
}));
|
|
4526
3451
|
}
|
|
4527
3452
|
/** Preview a creator's draft with the same clock and compiler used by saving. */
|
|
4528
3453
|
async function previewAutomationSchedule(user, id, intent) {
|
|
@@ -4810,7 +3735,7 @@ function readDescriptionText(home) {
|
|
|
4810
3735
|
}
|
|
4811
3736
|
/** Read discovered skill names for authenticated runtime diagnostics. */
|
|
4812
3737
|
async function readSkillReports() {
|
|
4813
|
-
const { discoverSkills } = await import("./skills-
|
|
3738
|
+
const { discoverSkills } = await import("./skills-CQHrwfOa.js").then((n) => n.o);
|
|
4814
3739
|
const skills = await discoverSkills();
|
|
4815
3740
|
return skillReportsSchema.parse(skills.map((skill) => ({
|
|
4816
3741
|
name: skill.name,
|
|
@@ -4819,7 +3744,7 @@ async function readSkillReports() {
|
|
|
4819
3744
|
}
|
|
4820
3745
|
/** Read safe configured plugin metadata for authenticated runtime diagnostics. */
|
|
4821
3746
|
async function readPlugins() {
|
|
4822
|
-
const { pluginCatalogRuntime } = await import("./catalog-runtime-
|
|
3747
|
+
const { pluginCatalogRuntime } = await import("./catalog-runtime-cMlAI5LY.js").then((n) => n.t);
|
|
4823
3748
|
return pluginsSchema.parse(pluginCatalogRuntime.getProviders().map((plugin) => ({
|
|
4824
3749
|
configKeys: plugin.manifest.configKeys ?? [],
|
|
4825
3750
|
description: plugin.manifest.description,
|
|
@@ -4831,7 +3756,7 @@ async function readPlugins() {
|
|
|
4831
3756
|
async function readRuntimeInfoReport() {
|
|
4832
3757
|
const [{ homeDir }, { pluginCatalogRuntime }, plugins, skills] = await Promise.all([
|
|
4833
3758
|
import("./package-discovery-DJBompdd.js").then((n) => n.r),
|
|
4834
|
-
import("./catalog-runtime-
|
|
3759
|
+
import("./catalog-runtime-cMlAI5LY.js").then((n) => n.t),
|
|
4835
3760
|
readPlugins(),
|
|
4836
3761
|
readSkillReports()
|
|
4837
3762
|
]);
|
|
@@ -4848,7 +3773,7 @@ async function readRuntimeInfoReport() {
|
|
|
4848
3773
|
/** Read sanitized operational summaries contributed by plugins. */
|
|
4849
3774
|
async function readPluginOperationalReportFeed() {
|
|
4850
3775
|
const nowMs = Date.now();
|
|
4851
|
-
const { getPluginOperationalReports } = await import("./credentialed-
|
|
3776
|
+
const { getPluginOperationalReports } = await import("./credentialed-CIGwoLU2.js").then((n) => n.a);
|
|
4852
3777
|
return pluginOperationalReportFeedSchema.parse({
|
|
4853
3778
|
source: "plugins",
|
|
4854
3779
|
generatedAt: new Date(nowMs).toISOString(),
|
|
@@ -4889,4 +3814,4 @@ function createJuniorApi(options = {}) {
|
|
|
4889
3814
|
});
|
|
4890
3815
|
return app;
|
|
4891
3816
|
}
|
|
4892
|
-
export { authenticatePersonalToken, createJuniorApi, jsonResponse, resolveViewerUser, updateViewerDisplayName };
|
|
3817
|
+
export { authenticatePersonalToken, createJuniorApi, isAllowedSignInEmail, jsonResponse, resolveViewerUser, updateViewerDisplayName };
|