@sentry/junior 0.266.0 → 0.268.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{actor-tlxzqaRe.js → actor-CP9K2Idu.js} +9 -1
- package/dist/{adapter-eRFA_naf.js → adapter-04AcD2SO.js} +5 -7
- package/dist/{admin-tILpP51p.js → admin-DIU1QyG3.js} +1 -1
- package/dist/{agent-invocations-CxKldAeB.js → agent-invocations-C1RNOCs8.js} +2 -0
- package/dist/{agent-runner-BtYK7bht.js → agent-runner-CAYfFmU7.js} +2636 -601
- package/dist/api/schema.d.ts +2 -5454
- package/dist/api/schema.js +6 -5
- package/dist/api.d.ts +6 -1
- package/dist/api.js +255 -1208
- package/dist/{app-Csbi9dW7.d.ts → app-z6Uuyq3j.d.ts} +23 -4
- package/dist/app.d.ts +2 -2
- package/dist/app.js +1869 -378
- package/dist/{automation-BKpY9LPa.js → automation-CmeFBSEh.js} +42 -14
- package/dist/{briefs-DgfWMU7e.js → briefs-DQbV2jNr.js} +4 -4
- package/dist/{catalog-runtime-DAJjAbSx.js → catalog-runtime-BD9GSpc6.js} +1 -1
- package/dist/cli/chat.js +15 -15
- package/dist/cli/check.js +3 -3
- package/dist/cli/init.js +0 -1
- package/dist/cli/main.js +3 -3
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +4 -4
- package/dist/cli/upgrade.d.ts +1 -1
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-B0YZfxNy.js → client-C1yDYjoS.js} +2 -2
- package/dist/{config-C0UwYqDK.js → config-DrTeF80T.js} +32 -10
- package/dist/{connections-Bwu3VORi.js → connections-D46f8vvi.js} +12 -12
- package/dist/{conversation-DF4rUcjb.js → conversation-DSo310Ps.js} +8 -1
- package/dist/{conversation-privacy-CY7JVjxn.js → conversation-privacy-CxlOCiUo.js} +1 -1
- package/dist/{credential-sync-VGAMUTGp.js → credential-sync-sm9fegLY.js} +29 -34
- package/dist/{credentialed-enF5Bf3y.js → credentialed-lukuD-cc.js} +2933 -1026
- package/dist/db-BU4LQD8s.d.ts +6 -0
- package/dist/{executor-TnWbvL09.js → executor-4vhvsMZJ.js} +83 -6
- package/dist/instrumentation.js +6 -2
- package/dist/{assistant-reply-UyzAfZQo.js → interruption-marker-CkncDT8d.js} +8 -2
- package/dist/{job-queue-DBpHmKnd.js → job-queue-In9QKz-I.js} +128 -3
- package/dist/{web-input-B-bMjD1V.js → list-BkgnXEEB.js} +1611 -817
- package/dist/{logging-Ce0mXZ8J.js → logging-DLSpkSSr.js} +11 -3
- package/dist/{message-VZ3DFpGb.js → message-BObyg59s.js} +4 -4
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-callback-server-CpqWLjVY.js → oauth-callback-server-B-WbSdoH.js} +2 -2
- package/dist/{prompt-BcCGlmg2.js → prompt-BW9jFsqZ.js} +1 -1
- package/dist/{registry-1BEIPdbT.js → registry-BgecKva0.js} +2 -2
- package/dist/{web-message-id-CJSUiksE.js → reporting-window-DVCD_10C.js} +1 -10
- package/dist/{resolve-BSHHkEGE.js → resolve-Cpa4Bu1O.js} +3 -3
- package/dist/{result-CwOpzqdh.js → result-D3TSmZZ8.js} +9 -9
- package/dist/{runner-CEaX04kz.js → runner-DS5OKhEp.js} +11 -11
- package/dist/{sandbox-egress-signals-BwtGHwqa.js → sandbox-egress-signals-CnDjY5rx.js} +1 -1
- package/dist/{schema-RsaBHxRH.js → schema-0050HFnB.js} +82 -5
- package/dist/schema-ZngqcOWM.d.ts +5813 -0
- package/dist/{service-account-6wC76eBZ.js → service-account-CKV2WTY7.js} +3 -3
- package/dist/{service-account-Dg4lIbHm.js → service-account-DGjnDw9y.js} +3 -3
- package/dist/{session-stop-tZOw9AJD.js → session-stop-Dam1EI7B.js} +527 -134
- package/dist/{skills-DzyGN7wd.js → skills-CfAnfImD.js} +2 -2
- package/dist/{spawn-6rokq7l_.js → spawn-cmq9p4Ly.js} +2 -2
- package/dist/{store-DcKkzrdt.js → store-C5PN02QO.js} +4 -4
- package/dist/{store-CG0VJyye.d.ts → store-xMuuXRE0.d.ts} +23 -9
- package/dist/store-zPEoJPBB.js +295 -0
- package/dist/{task-queue-Bg4RS2Gy.js → task-queue-_mf28dSu.js} +10 -5
- package/dist/{task-runner-8N5LIkSg.js → task-runner-DMI2prDO.js} +25 -14
- package/dist/{tasks-DDT-J3Rf.js → tasks-SB9z-VTU.js} +1551 -54
- package/dist/{thread-state-cprEaxgl.js → thread-state-BbbPh0Sn.js} +8 -18
- package/dist/{turn-failure-response-C8sMdsdX.js → turn-failure-response-BKKmnMB5.js} +11 -16
- package/dist/{turn-lifecycle-DaBVoz7h.js → turn-lifecycle-oP0kVeGz.js} +1 -1
- package/dist/{validation-C2lllinq.js → validation--JsAGLwc.js} +1 -1
- package/dist/{version-oaLfxxnf.js → version-jHwJp4hC.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-BpE6Z3qf.js → viewer-CpXTGlep.js} +329 -230
- package/dist/web-message-id-DlHNANtd.js +132 -0
- package/dist/{work-VX1SHWha.js → work-3N-GHY9m.js} +10 -10
- package/dist/{worker-hook-y_Y.js → worker-CCdbcItd.js} +30 -19
- package/migrations/0064_any_resource_event_triggers.sql +2 -0
- package/migrations/0065_user_customization.sql +1 -0
- package/migrations/0066_review_items.sql +53 -0
- package/migrations/0067_review_item_dedupe_tenant.sql +2 -0
- package/migrations/0068_review_item_slack_cards.sql +10 -0
- package/migrations/meta/0064_snapshot.json +3505 -0
- package/migrations/meta/0065_snapshot.json +3511 -0
- package/migrations/meta/0066_snapshot.json +4099 -0
- package/migrations/meta/0067_snapshot.json +3921 -0
- package/migrations/meta/0068_snapshot.json +4176 -0
- package/migrations/meta/_journal.json +36 -1
- package/package.json +19 -4
- package/dist/db-BnRqQ7G5.d.ts +0 -5
- package/dist/images-BKQqSi5W.js +0 -64
- package/dist/store-FALy-JQb.js +0 -1424
- package/dist/turn-session-routing-a9CHX-w6.js +0 -76
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, N as automationEventCatalog, O as automationRevision, S as automationDefinition, T as readAutomationVersion, a as saveScheduledAutomation, k as requireAutomationRevision, ur as isExperimentalFeatureEnabled, v as saveEventAutomation, y as setEventAutomationStatus } from "./tasks-SB9z-VTU.js";
|
|
2
|
+
import { p as escapeXml } from "./client-C1yDYjoS.js";
|
|
3
|
+
import { m as logInfo, p as logException } from "./logging-DLSpkSSr.js";
|
|
4
|
+
import { I as agentTurnUsageSchema, L as hasAgentTurnUsage, R as resolveRootConversationId, c as updateViewerDisplayName, h as getSqlExecutor, m as getDb, o as resolveViewerUser, p as getConversationStore, t as findUserByEmail } from "./viewer-CpXTGlep.js";
|
|
5
|
+
import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, s as juniorConversations } from "./agent-invocations-C1RNOCs8.js";
|
|
6
|
+
import { C as juniorCodeRepositories, S as juniorCodeChanges, b as juniorApiTokens, m as juniorConversationParticipants, v as juniorConversationMetrics, y as juniorConversationEvents } from "./executor-4vhvsMZJ.js";
|
|
7
|
+
import { n as pluginCatalogRuntime } from "./catalog-runtime-BD9GSpc6.js";
|
|
8
|
+
import { $ as webUserInputAnswerNote, B as listOwnedWatches, F as editEventAutomation, I as AutomationEditError, Lt as readUserCustomization, M as editScheduledAutomation, Mt as conversationSubtreeColumns, Rt as saveUserCustomization, St as readLiveAttachment, bt as decodeInputImages, ct as searchReviewItems, ft as acceptsWebMessages, ht as createAndEnqueueConversation, it as countReviewItems, jt as conversationSubtree, kt as listChildAgents, m as readStats, ot as listWaitingConversationIds, pt as appendAndEnqueueWebMessage, st as reviewItemReaderForUser, vt as webActorFromEmail, w as persistSlackUserInputReply, z as resolveTaskOutcomes } from "./agent-runner-CAYfFmU7.js";
|
|
9
|
+
import { C as listWorkspaces, E as WorkspaceValidationError, Et as SANDBOX_RUNTIME, In as createPluginLogger, Jn as readConversationEventPage, Ln as readConversationAccessFromSql, M as hash, Ot as workspaceRepoCheckoutPath, T as updateWorkspace, Yn as buildSentryConversationUrl, b as deleteWorkspace, cr as createPluginCache, fn as runWithWorkspaceTeamId, j as create, n as createUserTokenStore, p as getPlugins, qn as readLatestConversationBrief, rr as listConversationAnnotations, sr as lookupSlackUser, w as listWorkspacesByRepository, x as getWorkspace, y as createWorkspace } from "./credentialed-lukuD-cc.js";
|
|
10
|
+
import { i as getStateAdapter } from "./adapter-04AcD2SO.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 {
|
|
17
|
-
import {
|
|
18
|
-
import { a as
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import { t as
|
|
27
|
-
import { A as
|
|
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-C5PN02QO.js";
|
|
14
|
+
import { d as getEventCatalog } from "./session-stop-Dam1EI7B.js";
|
|
15
|
+
import { l as userCustomizationSchema, t as webMessageId } from "./web-message-id-DlHNANtd.js";
|
|
16
|
+
import { s as getSandboxResources, t as getCachedSnapshot } from "./resolve-Cpa4Bu1O.js";
|
|
17
|
+
import { a as getVercelConversationWorkQueue } from "./job-queue-In9QKz-I.js";
|
|
18
|
+
import { E as steerWebPendingMessages, a as cancelHumanFacingPendingMessages, g as getConversationWorkState, h as getConversation } from "./store-zPEoJPBB.js";
|
|
19
|
+
import { n as finishServiceAccountSignIn, o as serviceAccountPlugins, r as getServiceAccountStatus, s as startServiceAccountSignIn, t as disconnectServiceAccount } from "./service-account-CKV2WTY7.js";
|
|
20
|
+
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";
|
|
21
|
+
import { S as skillReportsSchema, a as fillUtcSixHours, b as runtimeInfoReportSchema, c as trailingUtcDayWindow, d as utcDayKey, f as utcHourKey, i as fillUtcHours, l as trailingUtcHourWindow, m as pluginOperationalReportFeedSchema, n as HOUR_MS$1, o as startOfUtcHour, p as healthReportSchema, r as fillUtcDays, s as sumUtcHoursIntoSixHours, t as DAY_MS$1, u as trailingUtcSixHourWindow, y as pluginsSchema } from "./reporting-window-DVCD_10C.js";
|
|
22
|
+
import { C as readConversationSourceTask, D as readViewerAutomationVersions, E as readViewerAutomationSummary, H as stopConversationTurn, J as canActOnReviewItem, K as ReviewItemActionError, L as clearSlackPendingReactions, M as isAllowedSignInEmail, O as readViewerAutomations, Q as getAttachmentStorage, S as deleteViewerTask, T as readViewerAutomationRuns, X as readReviewItemForUser, Y as isOnlyAssignee, a as resolveMentions, c as readConversationAuxiliaryCostsFromSql, d as notCopiedByFork, f as childAgentTotalsReport, g as resolveSlackTeamDomains, h as conversationSummaryFromStoredConversation, i as attachConversationMentions, k as resolveViewerTaskCandidate, l as ConversationForkError, m as conversationEventHistory, n as readConversationRecordFromSql, nt as readHealthReport, o as readFirstHumanMessageTexts, p as readConversationTreeMetricsFromSql, q as actOnReviewItem, r as readConversationParticipants, s as listConversationSidebarAnnotations, t as readConversationFeed, u as forkConversation, w as readViewerAutomationExecutions, x as ViewerTaskNotFoundError, y as getProductionSlackAdapter } from "./list-BkgnXEEB.js";
|
|
23
|
+
import { F as conversationAggregateColumns, I as conversationRangeColumns, M as readAutomationTitlesByConversationId, P as conversationActiveDaysColumn } from "./task-runner-DMI2prDO.js";
|
|
24
|
+
import { d as legacyWebMailboxMetadataSchema } from "./result-D3TSmZZ8.js";
|
|
25
|
+
import { a as startConnection, l as getWebAuthorization, n as isConnectableProvider, r as listConnections, t as disconnectConnection, u as htmlCallbackResponse } from "./connections-D46f8vvi.js";
|
|
26
|
+
import { A as personParamsSchema, B as connectionListSchema, C as createdPersonalTokenSchema, D as revokePersonalTokenResponseSchema, E as personalTokenParamsSchema, F as locationParamsSchema, I as activityDays$1, J as codeOverviewReportSchema, L as activityHours$1, N as locationDetailReportSchema, O as actorDirectoryReportSchema, P as locationDirectoryReportSchema, R as activitySixHours$1, S as createPersonalTokenBodySchema, U as disconnectConnectionResponseSchema, V as connectionParamsSchema, W as startConnectionResponseSchema, X as codeRepositoryParamsSchema, Y as codePersonReportSchema, Z as codeRepositoryReportSchema, _ as disconnectServiceAccountResponseSchema, a as workspaceBodySchema, at as apiErrorSchema, d as reviewItemCountSchema, et as conversationWatchesReportSchema, f as reviewItemListQuerySchema, g as reviewItemWaitingConversationsSchema, h as reviewItemSchema, i as deleteWorkspaceResponseSchema, it as conversationStatsReportSchema, j as personalSpendReportSchema, k as actorProfileReportSchema, l as workspaceSchema, m as reviewItemParamsSchema, n as statsReportSchema, nt as conversationChildAgentsReportSchema, o as workspaceListSchema, p as reviewItemListSchema, rt as stopConversationTurnResponseSchema, s as workspaceParamsSchema, u as reviewItemActionSchema, v as serviceAccountListSchema, w as personalTokenListSchema, x as startServiceAccountSignInResponseSchema, y as serviceAccountParamsSchema } from "./schema-0050HFnB.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
|
|
@@ -577,174 +257,50 @@ async function createConversationForViewer(viewer, body, services) {
|
|
|
577
257
|
async function appendConversationMessageForViewer(viewer, conversationId, body, services) {
|
|
578
258
|
const conversation = await getConversationStore().get({ conversationId });
|
|
579
259
|
if (!conversation) throwApiError(404, "Conversation not found.");
|
|
580
|
-
|
|
581
|
-
if (!(destinationPlatform === "local" && conversationId.startsWith("local:web:") || destinationPlatform === "slack")) throwApiError(409, "Conversation does not accept web messages.");
|
|
260
|
+
if (!acceptsWebMessages(conversationId, conversation.destination)) throwApiError(409, "Conversation does not accept web messages.");
|
|
582
261
|
if (!(await readConversationAccessFromSql(getDb(), [conversationId], viewer)).get(conversationId)?.isParticipant) throwApiError(403, "Only conversation participants can add messages.");
|
|
583
262
|
const images = parseImages(body.images);
|
|
263
|
+
const actor = actorFromViewer(viewer);
|
|
264
|
+
const append = () => appendAndEnqueueWebMessage({
|
|
265
|
+
actor,
|
|
266
|
+
conversationId,
|
|
267
|
+
idempotencyKey: body.idempotencyKey,
|
|
268
|
+
message: body.message,
|
|
269
|
+
images
|
|
270
|
+
}, {
|
|
271
|
+
conversationStore: getConversationStore(),
|
|
272
|
+
attachmentStorage: services.attachmentStorage,
|
|
273
|
+
queue: services.queue
|
|
274
|
+
});
|
|
275
|
+
const destination = conversation.destination;
|
|
584
276
|
try {
|
|
585
|
-
return await
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
277
|
+
if (destination?.platform !== "slack") return await append();
|
|
278
|
+
return await runWithWorkspaceTeamId(destination.teamId, async () => {
|
|
279
|
+
const state = getStateAdapter();
|
|
280
|
+
await state.connect();
|
|
281
|
+
return persistSlackUserInputReply({
|
|
282
|
+
channelId: destination.channelId,
|
|
283
|
+
claimedBy: viewer.email,
|
|
284
|
+
isSaved: async () => {
|
|
285
|
+
const [messageId, work] = await Promise.all([webMessageId({
|
|
286
|
+
conversationId,
|
|
287
|
+
idempotencyKey: body.idempotencyKey
|
|
288
|
+
}), getConversationWorkState({
|
|
289
|
+
conversationId,
|
|
290
|
+
state
|
|
291
|
+
})]);
|
|
292
|
+
return work?.execution.inboundMessageIds.includes(messageId) === true;
|
|
293
|
+
},
|
|
294
|
+
note: () => webUserInputAnswerNote(viewer.displayName ?? viewer.email),
|
|
295
|
+
persist: append,
|
|
296
|
+
state,
|
|
297
|
+
threadTs: conversation.location?.threadTs ?? (conversation.sessionSource?.kind === "slack" ? conversation.sessionSource.threadTs : void 0)
|
|
298
|
+
});
|
|
595
299
|
});
|
|
596
300
|
} catch (error) {
|
|
597
301
|
throwApiError(500, "Unable to append message.", error);
|
|
598
302
|
}
|
|
599
303
|
}
|
|
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
304
|
// src/chat/pi/sql-model-usage.ts
|
|
749
305
|
/** SQL reporting adapter for usage stored on assistant history events. */
|
|
750
306
|
const message = sql`${juniorConversationEvents.payload}`;
|
|
@@ -888,687 +444,6 @@ function decodeConversationCursor(args) {
|
|
|
888
444
|
return;
|
|
889
445
|
}
|
|
890
446
|
}
|
|
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
447
|
// src/api/conversations/fork.ts
|
|
1573
448
|
/** Fork a Conversation that the viewer can read into a new root they own. */
|
|
1574
449
|
async function forkConversationForViewer(viewer, sourceConversationId, body) {
|
|
@@ -1846,6 +721,25 @@ async function cancelConversationPendingMessagesForViewer(viewer, conversationId
|
|
|
1846
721
|
throwApiError(500, "Unable to cancel queued messages.", error);
|
|
1847
722
|
}
|
|
1848
723
|
}
|
|
724
|
+
// src/api/conversations/steer-pending-messages.ts
|
|
725
|
+
/**
|
|
726
|
+
* Steer the viewer's own queued web messages into the conversation now.
|
|
727
|
+
*
|
|
728
|
+
* Only the author can steer a message. Ids that are not the viewer's queued
|
|
729
|
+
* web messages, or that Junior already read, are left out of the result.
|
|
730
|
+
*/
|
|
731
|
+
async function steerConversationPendingMessagesForViewer(viewer, conversationId, body) {
|
|
732
|
+
if (!await getConversationStore().get({ conversationId })) throwApiError(404, "Conversation not found.");
|
|
733
|
+
if (!(await readConversationAccessFromSql(getDb(), [conversationId], viewer)).get(conversationId)?.isParticipant) throwApiError(403, "Only conversation participants can steer queued messages.");
|
|
734
|
+
return {
|
|
735
|
+
conversationId,
|
|
736
|
+
steeredInboundMessageIds: (await steerWebPendingMessages({
|
|
737
|
+
authorId: webActorFromEmail(viewer.email).userId,
|
|
738
|
+
conversationId,
|
|
739
|
+
inboundMessageIds: body.inboundMessageIds
|
|
740
|
+
})).steeredInboundMessageIds
|
|
741
|
+
};
|
|
742
|
+
}
|
|
1849
743
|
// src/api/conversations/pending-messages.ts
|
|
1850
744
|
const slackMailboxAuthorSchema = z.object({
|
|
1851
745
|
userId: z.string(),
|
|
@@ -2486,6 +1380,12 @@ function createConversationRoutes(options) {
|
|
|
2486
1380
|
const viewer = context.get("viewer");
|
|
2487
1381
|
return jsonResponse(conversationPendingMessagesReportSchema, await requireConversationPendingMessages(conversationId, { viewer }));
|
|
2488
1382
|
});
|
|
1383
|
+
app.post("/:conversationId/pending-messages/steer", requireViewer, validateRequest("param", conversationParamsSchema, "Invalid route parameters."), validateRequest("json", steerConversationPendingMessagesBodySchema, "Invalid request body."), async (context) => {
|
|
1384
|
+
const viewer = context.get("viewer");
|
|
1385
|
+
const { conversationId } = context.req.valid("param");
|
|
1386
|
+
const body = context.req.valid("json");
|
|
1387
|
+
return jsonResponse(steerConversationPendingMessagesResponseSchema, await steerConversationPendingMessagesForViewer(viewer, conversationId, body));
|
|
1388
|
+
});
|
|
2489
1389
|
app.delete("/:conversationId/pending-messages", requireViewer, validateRequest("param", conversationParamsSchema, "Invalid route parameters."), validateRequest("json", cancelConversationPendingMessagesBodySchema, "Invalid request body."), async (context) => {
|
|
2490
1390
|
const viewer = context.get("viewer");
|
|
2491
1391
|
const { conversationId } = context.req.valid("param");
|
|
@@ -3528,10 +2428,28 @@ function activitySixHours(hours, nowMs) {
|
|
|
3528
2428
|
function statsItems(map) {
|
|
3529
2429
|
return [...map.values()].sort((left, right) => right.conversations - left.conversations || right.durationMs - left.durationMs || left.label.localeCompare(right.label));
|
|
3530
2430
|
}
|
|
3531
|
-
/**
|
|
2431
|
+
/**
|
|
2432
|
+
* Build the verified person actor predicate shared by People aggregate and
|
|
2433
|
+
* recent-row queries.
|
|
2434
|
+
*
|
|
2435
|
+
* Any provider counts, so Slack and dashboard (web) Conversations roll up to
|
|
2436
|
+
* the same person. System identities never count.
|
|
2437
|
+
*/
|
|
3532
2438
|
function verifiedActorWhere(email) {
|
|
3533
2439
|
const normalizedEmail = normalizeEmail$1(email);
|
|
3534
|
-
return and(eq(juniorIdentities.
|
|
2440
|
+
return and(eq(juniorIdentities.kind, "user"), eq(juniorIdentities.emailVerified, true), sql`${juniorUsers.primaryEmailNormalized} IS NOT NULL`, normalizedEmail ? eq(juniorUsers.primaryEmailNormalized, normalizedEmail) : void 0);
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Read the Slack user fields for a grouped person row.
|
|
2444
|
+
*
|
|
2445
|
+
* Web identities use the email as the provider subject, so only Slack
|
|
2446
|
+
* identities can supply these fields.
|
|
2447
|
+
*/
|
|
2448
|
+
function slackActorColumns() {
|
|
2449
|
+
return {
|
|
2450
|
+
slackUserId: sql`MAX(CASE WHEN ${juniorIdentities.provider} = 'slack' THEN ${juniorIdentities.providerSubjectId} END)`,
|
|
2451
|
+
slackUserName: sql`MAX(CASE WHEN ${juniorIdentities.provider} = 'slack' THEN ${juniorIdentities.handle} END)`
|
|
2452
|
+
};
|
|
3535
2453
|
}
|
|
3536
2454
|
/** Read only the recent conversation rows required by a People profile. */
|
|
3537
2455
|
async function recentActorRows(email) {
|
|
@@ -3545,9 +2463,9 @@ async function recentActorRows(email) {
|
|
|
3545
2463
|
executionStatus: juniorConversations.executionStatus,
|
|
3546
2464
|
executionUpdatedAt: juniorConversations.executionUpdatedAt,
|
|
3547
2465
|
fullName: juniorUsers.displayName,
|
|
3548
|
-
handle: juniorIdentities.handle
|
|
2466
|
+
handle: sql`CASE WHEN ${juniorIdentities.provider} = 'slack' THEN ${juniorIdentities.handle} END`,
|
|
3549
2467
|
lastActivityAt: juniorConversations.lastActivityAt,
|
|
3550
|
-
providerSubjectId: juniorIdentities.providerSubjectId
|
|
2468
|
+
providerSubjectId: sql`CASE WHEN ${juniorIdentities.provider} = 'slack' THEN ${juniorIdentities.providerSubjectId} END`,
|
|
3551
2469
|
source: juniorConversations.source,
|
|
3552
2470
|
title: juniorConversations.title,
|
|
3553
2471
|
updatedAt: juniorConversations.updatedAt,
|
|
@@ -3671,8 +2589,7 @@ async function readPeopleListFromSql() {
|
|
|
3671
2589
|
getDb().select({
|
|
3672
2590
|
email: juniorUsers.primaryEmailNormalized,
|
|
3673
2591
|
fullName: juniorUsers.displayName,
|
|
3674
|
-
|
|
3675
|
-
slackUserName: sql`MAX(${juniorIdentities.handle})`,
|
|
2592
|
+
...slackActorColumns(),
|
|
3676
2593
|
activeDays: conversationActiveDaysColumn(),
|
|
3677
2594
|
...peopleTreeAggregateColumns,
|
|
3678
2595
|
...conversationRangeColumns()
|
|
@@ -3777,7 +2694,7 @@ async function readPeoplePluginReports(args) {
|
|
|
3777
2694
|
reports: [],
|
|
3778
2695
|
source: "plugins"
|
|
3779
2696
|
});
|
|
3780
|
-
const { getPluginProfileReports } = await import("./credentialed-
|
|
2697
|
+
const { getPluginProfileReports } = await import("./credentialed-lukuD-cc.js").then((n) => n.a);
|
|
3781
2698
|
return pluginOperationalReportFeedSchema.parse({
|
|
3782
2699
|
generatedAt: new Date(nowMs).toISOString(),
|
|
3783
2700
|
reports: await getPluginProfileReports({
|
|
@@ -3828,6 +2745,8 @@ function surfaceExpression() {
|
|
|
3828
2745
|
return sql`CASE
|
|
3829
2746
|
WHEN ${juniorConversations.source} IN ('api', 'scheduler', 'slack')
|
|
3830
2747
|
THEN ${juniorConversations.source}
|
|
2748
|
+
-- Dashboard web conversations share the product api surface label.
|
|
2749
|
+
WHEN ${juniorConversations.source} = 'web' THEN 'api'
|
|
3831
2750
|
WHEN ${juniorConversations.conversationId} LIKE 'slack:%' THEN 'slack'
|
|
3832
2751
|
WHEN ${juniorConversations.conversationId} LIKE 'scheduler:%' THEN 'scheduler'
|
|
3833
2752
|
WHEN ${juniorConversations.conversationId} LIKE 'api:%' THEN 'api'
|
|
@@ -3868,8 +2787,7 @@ async function readPeopleProfileFromSql(email, options = {}) {
|
|
|
3868
2787
|
getDb().select({
|
|
3869
2788
|
email: juniorUsers.primaryEmailNormalized,
|
|
3870
2789
|
fullName: juniorUsers.displayName,
|
|
3871
|
-
|
|
3872
|
-
slackUserName: sql`MAX(${juniorIdentities.handle})`,
|
|
2790
|
+
...slackActorColumns(),
|
|
3873
2791
|
activeDays: conversationActiveDaysColumn(),
|
|
3874
2792
|
...peopleTreeAggregateColumns
|
|
3875
2793
|
}).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 +3435,18 @@ async function changeViewerAutomationLifecycle(user, id, input) {
|
|
|
4517
3435
|
}
|
|
4518
3436
|
// src/chat/automations/editor.ts
|
|
4519
3437
|
/** Read-only editor support. Core owns schedules; plugins own Event choices. */
|
|
4520
|
-
/**
|
|
3438
|
+
/**
|
|
3439
|
+
* Return only Event types that can start a durable Automation, one entry for
|
|
3440
|
+
* each namespace, with the reach of each event.
|
|
3441
|
+
*/
|
|
4521
3442
|
function readAutomationEventCatalog() {
|
|
4522
|
-
return Object.entries(automationEventCatalog(getEventCatalog())).
|
|
3443
|
+
return Object.entries(automationEventCatalog(getEventCatalog())).map(([namespace, registration]) => ({
|
|
4523
3444
|
namespace,
|
|
4524
|
-
|
|
4525
|
-
|
|
3445
|
+
displayName: registration.displayName ?? namespace,
|
|
3446
|
+
events: Object.fromEntries(Object.entries(registration.events).map(([eventType, declaration]) => [eventType, { reach: declaration.reach }])),
|
|
3447
|
+
anyResourceFields: registration.anyResourceFields,
|
|
3448
|
+
resourceTypes: registration.resourceTypes
|
|
3449
|
+
}));
|
|
4526
3450
|
}
|
|
4527
3451
|
/** Preview a creator's draft with the same clock and compiler used by saving. */
|
|
4528
3452
|
async function previewAutomationSchedule(user, id, intent) {
|
|
@@ -4686,6 +3610,126 @@ function createAutomationRoutes() {
|
|
|
4686
3610
|
});
|
|
4687
3611
|
return app;
|
|
4688
3612
|
}
|
|
3613
|
+
// src/api/user-customization/routes.ts
|
|
3614
|
+
/**
|
|
3615
|
+
* Create routes that read and replace the viewer's customization. They return
|
|
3616
|
+
* 404 unless the app enables the `user-customization` experiment.
|
|
3617
|
+
*/
|
|
3618
|
+
function createUserCustomizationRoutes() {
|
|
3619
|
+
const app = new Hono();
|
|
3620
|
+
app.use("*", async (_context, next) => {
|
|
3621
|
+
if (!isExperimentalFeatureEnabled("user-customization")) throwApiError(404, "Resource not found.");
|
|
3622
|
+
await next();
|
|
3623
|
+
});
|
|
3624
|
+
app.get("/", requireViewer, async (context) => {
|
|
3625
|
+
const viewer = context.get("viewer");
|
|
3626
|
+
return jsonResponse(userCustomizationSchema, { customization: await readUserCustomization(viewer.id) ?? "" });
|
|
3627
|
+
});
|
|
3628
|
+
app.put("/", requireViewer, validateRequest("json", userCustomizationSchema, "Invalid request body."), async (context) => {
|
|
3629
|
+
const viewer = context.get("viewer");
|
|
3630
|
+
const { customization } = context.req.valid("json");
|
|
3631
|
+
if (!await saveUserCustomization(viewer.id, customization)) throwApiError(404, "User not found.");
|
|
3632
|
+
return jsonResponse(userCustomizationSchema, { customization });
|
|
3633
|
+
});
|
|
3634
|
+
return app;
|
|
3635
|
+
}
|
|
3636
|
+
// src/api/review-items/routes.ts
|
|
3637
|
+
const LIST_LIMIT = 100;
|
|
3638
|
+
const ERROR_STATUS = {
|
|
3639
|
+
conflict: 409,
|
|
3640
|
+
forbidden: 403,
|
|
3641
|
+
invalid: 400,
|
|
3642
|
+
not_found: 404
|
|
3643
|
+
};
|
|
3644
|
+
/** Build the API payload of one Review item for its viewer. */
|
|
3645
|
+
function reviewItemPayload(item, viewer) {
|
|
3646
|
+
return {
|
|
3647
|
+
addedByConversationId: item.addedByConversationId,
|
|
3648
|
+
assignedToEveryone: item.assignedToEveryone,
|
|
3649
|
+
assignees: item.assignees,
|
|
3650
|
+
body: item.body,
|
|
3651
|
+
canAct: canActOnReviewItem(item, viewer.id),
|
|
3652
|
+
canTake: canActOnReviewItem(item, viewer.id) && !isOnlyAssignee(item, viewer.id),
|
|
3653
|
+
conversationIds: item.conversationIds,
|
|
3654
|
+
createdAt: item.createdAt.toISOString(),
|
|
3655
|
+
id: item.id,
|
|
3656
|
+
kind: item.kind,
|
|
3657
|
+
status: item.status,
|
|
3658
|
+
title: item.title,
|
|
3659
|
+
updatedAt: item.updatedAt.toISOString(),
|
|
3660
|
+
visibility: item.visibility,
|
|
3661
|
+
...item.answer ? { answer: item.answer } : void 0,
|
|
3662
|
+
...item.automationId ? { automationId: item.automationId } : void 0,
|
|
3663
|
+
...item.closeReason ? { closeReason: item.closeReason } : void 0,
|
|
3664
|
+
...item.closedAt ? { closedAt: item.closedAt.toISOString() } : void 0,
|
|
3665
|
+
...item.closedBy ? { closedBy: item.closedBy } : void 0,
|
|
3666
|
+
...item.request ? { request: item.request } : void 0,
|
|
3667
|
+
...item.subject ? { subject: item.subject } : void 0
|
|
3668
|
+
};
|
|
3669
|
+
}
|
|
3670
|
+
function actionErrorResponse(error) {
|
|
3671
|
+
if (error instanceof ReviewItemActionError) return jsonResponse(apiErrorSchema, { error: error.message }, { status: ERROR_STATUS[error.code] });
|
|
3672
|
+
throw error;
|
|
3673
|
+
}
|
|
3674
|
+
/** Create the authenticated review desk routes. */
|
|
3675
|
+
function createReviewItemRoutes(services) {
|
|
3676
|
+
const app = new Hono();
|
|
3677
|
+
app.get("/", requireViewer, validateRequest("query", reviewItemListQuerySchema, "Invalid review item query."), async (context) => {
|
|
3678
|
+
const viewer = context.get("viewer");
|
|
3679
|
+
const query = context.req.valid("query");
|
|
3680
|
+
const view = query.view ?? "all";
|
|
3681
|
+
const items = await searchReviewItems(getDb(), reviewItemReaderForUser(viewer), {
|
|
3682
|
+
limit: LIST_LIMIT,
|
|
3683
|
+
statuses: query.archived === "true" ? ["done", "dismissed"] : ["open"],
|
|
3684
|
+
...view === "mine" ? { assignee: { userId: viewer.id } } : view === "everyone" ? { assignee: { everyone: true } } : void 0,
|
|
3685
|
+
...query.conversationId ? { conversationId: query.conversationId } : void 0,
|
|
3686
|
+
...query.q ? { query: query.q } : void 0
|
|
3687
|
+
});
|
|
3688
|
+
return jsonResponse(reviewItemListSchema, { items: items.map((item) => reviewItemPayload(item, viewer)) });
|
|
3689
|
+
});
|
|
3690
|
+
app.get("/count", requireViewer, async (context) => {
|
|
3691
|
+
const viewer = context.get("viewer");
|
|
3692
|
+
return jsonResponse(reviewItemCountSchema, { mine: await countReviewItems(getDb(), reviewItemReaderForUser(viewer), {
|
|
3693
|
+
assignee: { userId: viewer.id },
|
|
3694
|
+
statuses: ["open"]
|
|
3695
|
+
}) });
|
|
3696
|
+
});
|
|
3697
|
+
app.get("/waiting-conversations", requireViewer, async (context) => {
|
|
3698
|
+
const viewer = context.get("viewer");
|
|
3699
|
+
return jsonResponse(reviewItemWaitingConversationsSchema, { conversationIds: await listWaitingConversationIds(getDb(), viewer) });
|
|
3700
|
+
});
|
|
3701
|
+
app.get("/:id", requireViewer, validateRequest("param", reviewItemParamsSchema, "Invalid review item."), async (context) => {
|
|
3702
|
+
const viewer = context.get("viewer");
|
|
3703
|
+
try {
|
|
3704
|
+
const item = await readReviewItemForUser(viewer, context.req.valid("param").id);
|
|
3705
|
+
return jsonResponse(reviewItemSchema, reviewItemPayload(item, viewer));
|
|
3706
|
+
} catch (error) {
|
|
3707
|
+
return actionErrorResponse(error);
|
|
3708
|
+
}
|
|
3709
|
+
});
|
|
3710
|
+
app.post("/:id/actions", requireViewer, validateRequest("param", reviewItemParamsSchema, "Invalid review item."), validateRequest("json", reviewItemActionSchema, "Invalid action."), async (context) => {
|
|
3711
|
+
const viewer = context.get("viewer");
|
|
3712
|
+
const body = context.req.valid("json");
|
|
3713
|
+
const { id } = context.req.valid("param");
|
|
3714
|
+
try {
|
|
3715
|
+
const item = await actOnReviewItem({
|
|
3716
|
+
action: body.action,
|
|
3717
|
+
getSlackAdapter: getProductionSlackAdapter,
|
|
3718
|
+
id,
|
|
3719
|
+
queue: services.queue,
|
|
3720
|
+
user: viewer,
|
|
3721
|
+
...body.action === "submit" ? { answer: {
|
|
3722
|
+
...body.answer,
|
|
3723
|
+
requestId: id
|
|
3724
|
+
} } : void 0
|
|
3725
|
+
});
|
|
3726
|
+
return jsonResponse(reviewItemSchema, reviewItemPayload(item, viewer));
|
|
3727
|
+
} catch (error) {
|
|
3728
|
+
return actionErrorResponse(error);
|
|
3729
|
+
}
|
|
3730
|
+
});
|
|
3731
|
+
return app;
|
|
3732
|
+
}
|
|
4689
3733
|
// src/api/workspaces/routes.ts
|
|
4690
3734
|
function snapshotView(workspace) {
|
|
4691
3735
|
const recorded = workspace.snapshot;
|
|
@@ -4810,7 +3854,7 @@ function readDescriptionText(home) {
|
|
|
4810
3854
|
}
|
|
4811
3855
|
/** Read discovered skill names for authenticated runtime diagnostics. */
|
|
4812
3856
|
async function readSkillReports() {
|
|
4813
|
-
const { discoverSkills } = await import("./skills-
|
|
3857
|
+
const { discoverSkills } = await import("./skills-CfAnfImD.js").then((n) => n.o);
|
|
4814
3858
|
const skills = await discoverSkills();
|
|
4815
3859
|
return skillReportsSchema.parse(skills.map((skill) => ({
|
|
4816
3860
|
name: skill.name,
|
|
@@ -4819,7 +3863,7 @@ async function readSkillReports() {
|
|
|
4819
3863
|
}
|
|
4820
3864
|
/** Read safe configured plugin metadata for authenticated runtime diagnostics. */
|
|
4821
3865
|
async function readPlugins() {
|
|
4822
|
-
const { pluginCatalogRuntime } = await import("./catalog-runtime-
|
|
3866
|
+
const { pluginCatalogRuntime } = await import("./catalog-runtime-BD9GSpc6.js").then((n) => n.t);
|
|
4823
3867
|
return pluginsSchema.parse(pluginCatalogRuntime.getProviders().map((plugin) => ({
|
|
4824
3868
|
configKeys: plugin.manifest.configKeys ?? [],
|
|
4825
3869
|
description: plugin.manifest.description,
|
|
@@ -4831,7 +3875,7 @@ async function readPlugins() {
|
|
|
4831
3875
|
async function readRuntimeInfoReport() {
|
|
4832
3876
|
const [{ homeDir }, { pluginCatalogRuntime }, plugins, skills] = await Promise.all([
|
|
4833
3877
|
import("./package-discovery-DJBompdd.js").then((n) => n.r),
|
|
4834
|
-
import("./catalog-runtime-
|
|
3878
|
+
import("./catalog-runtime-BD9GSpc6.js").then((n) => n.t),
|
|
4835
3879
|
readPlugins(),
|
|
4836
3880
|
readSkillReports()
|
|
4837
3881
|
]);
|
|
@@ -4848,7 +3892,7 @@ async function readRuntimeInfoReport() {
|
|
|
4848
3892
|
/** Read sanitized operational summaries contributed by plugins. */
|
|
4849
3893
|
async function readPluginOperationalReportFeed() {
|
|
4850
3894
|
const nowMs = Date.now();
|
|
4851
|
-
const { getPluginOperationalReports } = await import("./credentialed-
|
|
3895
|
+
const { getPluginOperationalReports } = await import("./credentialed-lukuD-cc.js").then((n) => n.a);
|
|
4852
3896
|
return pluginOperationalReportFeedSchema.parse({
|
|
4853
3897
|
source: "plugins",
|
|
4854
3898
|
generatedAt: new Date(nowMs).toISOString(),
|
|
@@ -4865,10 +3909,12 @@ function createJuniorApi(options = {}) {
|
|
|
4865
3909
|
app.get("/api/skills", async () => jsonResponse(skillReportsSchema, await readSkillReports()));
|
|
4866
3910
|
app.get("/api/plugin-reports", async () => jsonResponse(pluginOperationalReportFeedSchema, await readPluginOperationalReportFeed()));
|
|
4867
3911
|
app.get("/api/stats", async () => jsonResponse(statsReportSchema, await readStatsReport()));
|
|
3912
|
+
const queue = options.conversationWorkQueue ?? getVercelConversationWorkQueue();
|
|
4868
3913
|
app.route("/api/conversations", createConversationRoutes({
|
|
4869
3914
|
attachmentStorage: getAttachmentStorage(),
|
|
4870
|
-
queue
|
|
3915
|
+
queue
|
|
4871
3916
|
}));
|
|
3917
|
+
app.route("/api/review-items", createReviewItemRoutes({ queue }));
|
|
4872
3918
|
app.route("/api/code", createCodeRoutes());
|
|
4873
3919
|
app.route("/api/connections", createConnectionRoutes());
|
|
4874
3920
|
app.route("/api/personal-tokens", createPersonalTokenRoutes());
|
|
@@ -4876,6 +3922,7 @@ function createJuniorApi(options = {}) {
|
|
|
4876
3922
|
app.route("/api/people", createPeopleRoutes());
|
|
4877
3923
|
app.route("/api/locations", createLocationRoutes());
|
|
4878
3924
|
app.route("/api/user-pages", createUserPageRoutes());
|
|
3925
|
+
app.route("/api/user-customization", createUserCustomizationRoutes());
|
|
4879
3926
|
app.route("/api/automations", createAutomationRoutes());
|
|
4880
3927
|
app.route("/api/workspaces", createWorkspaceRoutes());
|
|
4881
3928
|
app.notFound(() => jsonResponse(apiErrorSchema, { error: "Resource not found." }, { status: 404 }));
|
|
@@ -4889,4 +3936,4 @@ function createJuniorApi(options = {}) {
|
|
|
4889
3936
|
});
|
|
4890
3937
|
return app;
|
|
4891
3938
|
}
|
|
4892
|
-
export { authenticatePersonalToken, createJuniorApi, jsonResponse, resolveViewerUser, updateViewerDisplayName };
|
|
3939
|
+
export { authenticatePersonalToken, createJuniorApi, isAllowedSignInEmail, jsonResponse, resolveViewerUser, updateViewerDisplayName };
|