@sentry/junior 0.244.0 → 0.246.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/{admin-CF9lQg0M.js → admin-Bh-MYw-j.js} +1 -1
- package/dist/{agent-hooks-Cj_rSeWd.js → agent-hooks-DpklSWZg.js} +10 -10
- package/dist/{agent-runner-DtEtbaOb.js → agent-runner-DSKpHjHW.js} +568 -191
- package/dist/api/schema.d.ts +86 -3
- package/dist/api/schema.js +3 -3
- package/dist/api.js +372 -245
- package/dist/{app-DRLh-8zs.d.ts → app-BYB7szGb.d.ts} +2 -0
- package/dist/app.d.ts +1 -1
- package/dist/app.js +179 -146
- package/dist/{briefs-BMzTiGJO.js → briefs-cZ5vWqP_.js} +4 -4
- package/dist/{catalog-runtime-CpxhLwZG.js → catalog-runtime-CTaQXYqS.js} +1 -1
- package/dist/cli/chat.js +17 -13
- package/dist/cli/check.js +3 -3
- package/dist/cli/main.js +2 -2
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +3 -3
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-BVaGEb1-.js → client--xtsAxKP.js} +2 -2
- package/dist/{config-q9xXVN_5.js → config-BK53Ip5C.js} +11 -2
- package/dist/{conversation-BxvgH64Q.js → conversation-CvBG57Io.js} +11 -1
- package/dist/{conversation-privacy-Dyh_63Ee.js → conversation-privacy-BDBFw6Ws.js} +1 -1
- package/dist/{credential-sync-Bsn-6m16.js → credential-sync-BiuhC9Ds.js} +14 -14
- package/dist/{executor-DA7eHhmC.js → executor-bmmuRY-w.js} +2 -2
- package/dist/{images-CvMreau5.js → images-DZliDtSH.js} +3 -3
- package/dist/instrumentation.js +2 -2
- package/dist/{job-queue-mVyF9jgv.js → job-queue-Cz3M7gg4.js} +1 -1
- package/dist/{logging-z7_w-KN_.js → logging-DVtMYOdD.js} +1 -1
- package/dist/message-BSN-Ma5f.js +317 -0
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-authorization-Db1yGVvG.js → oauth-authorization-By_MVRDC.js} +1 -1
- package/dist/{oauth-callback-server-_MfOtSxS.js → oauth-callback-server-BrUo6w1e.js} +2 -2
- package/dist/{pending-auth-OndKPW4M.js → pending-auth-BuQLZ6dK.js} +2 -2
- package/dist/{profile-yeKVNECl.js → profile-BevMNSlr.js} +4 -4
- package/dist/{prompt-CX36ZlbV.js → prompt-C9Q9cjD3.js} +1 -1
- package/dist/{registry-DvzzL8-v.js → registry-C2rSEio1.js} +1 -1
- package/dist/{resolve-4u5gEU04.js → resolve-qPhrznTf.js} +2 -2
- package/dist/{runner-Y6A6pww4.js → runner-CNh5eMWx.js} +10 -10
- package/dist/{sandbox-egress-signals-D7AOkuoe.js → sandbox-egress-signals-BnDEKu8d.js} +1 -1
- package/dist/{schema-Dyyi9oRc.js → schema-BWMRg8JS.js} +28 -4
- package/dist/{skills-DFduSvuO.js → skills-C6zJbudq.js} +2 -2
- package/dist/{spawn-DYg7jhhR.js → spawn-C3efZSx3.js} +2 -2
- package/dist/{task-queue-BQ0vnumm.js → task-queue-BX4vG6uA.js} +3 -3
- package/dist/{task-runner-D4jvUpFV.js → task-runner-T51kll0U.js} +10 -10
- package/dist/{tasks-DBZAgTx7.js → tasks-B9OIjl2K.js} +82 -11
- package/dist/{thread-state-hj70P2Cp.js → thread-state-AGZtVyXp.js} +6 -6
- package/dist/{turn-failure-response-S2pwtR8b.js → turn-failure-response-DJ8KU2bo.js} +9 -9
- package/dist/{turn-lifecycle-CNFOMAJq.js → turn-lifecycle-DZJkJ95q.js} +1 -1
- package/dist/{turn-session-routing-1eTgv6ns.js → turn-session-routing-mRqLbMZW.js} +1 -1
- package/dist/{validation-DcsBjzdi.js → validation-Dg0Ihxzo.js} +1 -1
- package/dist/{version-DVCHJYcY.js → version-Deub4try.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-BBnCMR1g.js → viewer-D5hbx6TE.js} +3 -3
- package/dist/{web-authorization-CTltRLsB.js → web-authorization-DCxixz5S.js} +6 -6
- package/dist/{web-input-CIsRGF07.js → web-input-0sorYVLh.js} +74 -187
- package/dist/{work-fje9VXgQ.js → work-rm2X7jj4.js} +13 -6
- package/dist/{worker-Byd2Et9r.js → worker-bnGsAOP3.js} +5 -5
- package/package.json +4 -4
package/dist/api.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { L as editEventAutomation, Mt as
|
|
2
|
-
import { d as logException, p as logWarn } from "./logging-
|
|
1
|
+
import { Et as automationEventCatalog, G as listChildAgents, Ht as getConversation, L as editEventAutomation, Mt as cancelHumanFacingPendingMessages, O as parseSlackThreadId, P as editScheduledAutomation, R as AutomationEditError, St as getEventCatalog, T as readLiveAttachment, V as resolveTaskOutcomes, g as readStats, it as getChildAgentLabel, z as eventAutomationTriggerAvailable } from "./agent-runner-DSKpHjHW.js";
|
|
2
|
+
import { d as logException, p as logWarn } from "./logging-DVtMYOdD.js";
|
|
3
3
|
import { d as parseSlackTeamId, g as parseSlackMessageTs, l as parseSlackChannelId } from "./actor-BTtLaeWy.js";
|
|
4
|
-
import { E as resolveRootConversationId, T as hasAgentTurnUsage, _ as locationFromRow, b as parseSessionSource, c as getConversationEventStore, d as getDb, f as getSqlExecutor, i as resolveViewerUser, m as withConversationEventLock, o as updateViewerDisplayName, t as findUserByEmail, u as getConversationStore, w as agentTurnUsageSchema } from "./viewer-
|
|
4
|
+
import { E as resolveRootConversationId, T as hasAgentTurnUsage, _ as locationFromRow, b as parseSessionSource, c as getConversationEventStore, d as getDb, f as getSqlExecutor, i as resolveViewerUser, m as withConversationEventLock, o as updateViewerDisplayName, t as findUserByEmail, u as getConversationStore, w as agentTurnUsageSchema } from "./viewer-D5hbx6TE.js";
|
|
5
5
|
import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, o as conversationReadColumns, s as juniorConversations } from "./agent-invocations-BO6ddZax.js";
|
|
6
|
-
import { _ as juniorConversationEvents, b as juniorCodeChanges, f as juniorConversationParticipants, g as juniorConversationMetrics, m as juniorConversationBriefs, v as juniorApiTokens, x as juniorCodeRepositories } from "./executor-
|
|
6
|
+
import { _ as juniorConversationEvents, b as juniorCodeChanges, f as juniorConversationParticipants, g as juniorConversationMetrics, m as juniorConversationBriefs, v as juniorApiTokens, x as juniorCodeRepositories } from "./executor-bmmuRY-w.js";
|
|
7
7
|
import { u as contextProvenance } from "./history-Nte1oVed.js";
|
|
8
|
-
import {
|
|
9
|
-
import { At as listConversationAnnotations, C as WorkspaceValidationError, M as workspaceRepoCheckoutPath, O as SANDBOX_RUNTIME, Pt as lookupSlackUser, S as updateWorkspace, St as buildSentryConversationUrl, Wt as withSlackRetries, _ as deleteWorkspace, _t as conversationNotArchivedForUser, b as listWorkspaces, bt as readLatestConversationBrief, dt as conversationArchiveTimes, ft as conversationArchivedForEmail, g as createWorkspace, gt as conversationNotArchivedForEmail, ht as conversationHasParticipantUser, jt as listConversationAnnotationsById, lt as createPluginLogger, mt as conversationHasParticipantEmail, pt as conversationArchivedForUser, u as getPlugins, ut as readConversationAccessFromSql, v as getWorkspace, vt as viewerConversationMembership, x as listWorkspacesByRepository, xt as readConversationEventPage, zt as getSlackClient } from "./agent-hooks-
|
|
10
|
-
import { A as
|
|
11
|
-
import { n as hash, t as create } from "./profile-
|
|
8
|
+
import { A as requireAutomationRevision, D as sameDefinitionValue, E as readAutomationVersion, O as AutomationConflictError, Ot as loadTurnProjection, a as readScheduledAutomation, b as saveEventAutomation, dn as piMessageFromHistoryItem, g as getEventAutomation, k as automationRevision, m as eventAutomationBelongsToUser, o as saveScheduledAutomation, w as automationDefinition, x as setEventAutomationStatus } from "./tasks-B9OIjl2K.js";
|
|
9
|
+
import { At as listConversationAnnotations, C as WorkspaceValidationError, M as workspaceRepoCheckoutPath, O as SANDBOX_RUNTIME, Pt as lookupSlackUser, S as updateWorkspace, St as buildSentryConversationUrl, Wt as withSlackRetries, _ as deleteWorkspace, _t as conversationNotArchivedForUser, b as listWorkspaces, bt as readLatestConversationBrief, dt as conversationArchiveTimes, ft as conversationArchivedForEmail, g as createWorkspace, gt as conversationNotArchivedForEmail, ht as conversationHasParticipantUser, jt as listConversationAnnotationsById, lt as createPluginLogger, mt as conversationHasParticipantEmail, pt as conversationArchivedForUser, u as getPlugins, ut as readConversationAccessFromSql, v as getWorkspace, vt as viewerConversationMembership, x as listWorkspacesByRepository, xt as readConversationEventPage, zt as getSlackClient } from "./agent-hooks-DpklSWZg.js";
|
|
10
|
+
import { A as forkConversationBodySchema, F as INPUT_IMAGE_TYPES, O as createConversationBodySchema, P as readMessageAttachments, T as conversationStatsReportSchema, _ as conversationParamsSchema, a as cancelConversationPendingMessagesBodySchema, c as conversationAttachmentParamsSchema, d as conversationDetailReportSchema, g as conversationFeedSchema, h as conversationFeedQuerySchema, i as archiveConversationResponseSchema, j as forkConversationResponseSchema, k as createConversationMessageBodySchema, m as conversationEventsQuerySchema, o as cancelConversationPendingMessagesResponseSchema, p as conversationEventPageSchema, r as archiveConversationBodySchema, t as acceptedConversationMessageSchema, u as conversationDetailQuerySchema, x as conversationPendingMessagesReportSchema } from "./conversation-CvBG57Io.js";
|
|
11
|
+
import { n as hash, t as create } from "./profile-BevMNSlr.js";
|
|
12
12
|
import { t as getFirstRunAtMs } from "./cadence-D00SCzT1.js";
|
|
13
|
-
import { t as getCachedSnapshot } from "./resolve-
|
|
14
|
-
import { a as getVercelConversationWorkQueue } from "./job-queue-
|
|
13
|
+
import { t as getCachedSnapshot } from "./resolve-qPhrznTf.js";
|
|
14
|
+
import { a as getVercelConversationWorkQueue } from "./job-queue-Cz3M7gg4.js";
|
|
15
15
|
import { M as compileScheduleIntent, P as weekdaySchema, _ as automationSummarySchema, b as automationVersionListSchema, c as automationLifecycleSchema, d as automationParamsSchema, f as automationRunListSchema, g as automationSchedulePreviewSchema, h as automationScheduleIntentSchema, i as automationExecutionListSchema, j as ScheduleIntentError, 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-gznOMaQ2.js";
|
|
16
16
|
import { C as skillReportsSchema, a as fillUtcHours, b as pluginsSchema, c as sumUtcHoursIntoSixHours, d as trailingUtcSixHourWindow, f as utcDayKey, h as pluginOperationalReportFeedSchema, i as fillUtcDays, l as trailingUtcDayWindow, m as healthReportSchema, n as DAY_MS$1, o as fillUtcSixHours, p as utcHourKey, r as HOUR_MS$1, s as startOfUtcHour, u as trailingUtcHourWindow, x as runtimeInfoReportSchema } from "./web-message-id-xPI_EFSC.js";
|
|
17
|
-
import { H as readHealthReport,
|
|
18
|
-
import { A as conversationActiveDaysColumn, M as conversationRangeColumns, j as conversationAggregateColumns } from "./task-runner-
|
|
19
|
-
import { r as getWebAuthorization } from "./web-authorization-
|
|
20
|
-
import { t as decodeInputImages } from "./images-
|
|
21
|
-
import { C as locationParamsSchema, E as activitySixHours$1, L as conversationChildAgentsReportSchema, M as codePersonReportSchema, N as codeRepositoryParamsSchema, P as codeRepositoryReportSchema, R as
|
|
17
|
+
import { H as readHealthReport, N as legacyWebMailboxMetadataSchema, R as createVercelAttachmentStorage, _ as readViewerAutomationRuns, a as recordWebConversationActivity, b as readViewerAutomations, d as resolveSlackConversationContextFromThreadId, g as readViewerAutomationExecutions, h as readConversationSourceTask, i as createConversationId, j as stopConversationTurn, l as getProductionSlackAdapter, m as deleteViewerTask, o as webActorFromEmail, p as ViewerTaskNotFoundError, r as createAndEnqueueConversation, t as appendAndEnqueueWebMessage, u as formatSlackConversationRedactedLabel, v as readViewerAutomationSummary, w as clearSlackPendingReactions, x as resolveViewerTaskCandidate, y as readViewerAutomationVersions } from "./web-input-0sorYVLh.js";
|
|
18
|
+
import { A as conversationActiveDaysColumn, M as conversationRangeColumns, j as conversationAggregateColumns } from "./task-runner-T51kll0U.js";
|
|
19
|
+
import { r as getWebAuthorization } from "./web-authorization-DCxixz5S.js";
|
|
20
|
+
import { t as decodeInputImages } from "./images-DZliDtSH.js";
|
|
21
|
+
import { C as locationParamsSchema, E as activitySixHours$1, L as conversationChildAgentsReportSchema, M as codePersonReportSchema, N as codeRepositoryParamsSchema, P as codeRepositoryReportSchema, R as stopConversationTurnResponseSchema, S as locationDirectoryReportSchema, T as activityHours$1, _ as actorProfileReportSchema, a as workspaceBodySchema, d as createdPersonalTokenSchema, f as personalTokenListSchema, g as actorDirectoryReportSchema, h as revokePersonalTokenResponseSchema, i as deleteWorkspaceResponseSchema, j as codeOverviewReportSchema, l as workspaceSchema, m as personalTokenParamsSchema, n as statsReportSchema, o as workspaceListSchema, s as workspaceParamsSchema, u as createPersonalTokenBodySchema, v as personParamsSchema, w as activityDays$1, x as locationDetailReportSchema, y as personalSpendReportSchema, z as apiErrorSchema } from "./schema-BWMRg8JS.js";
|
|
22
22
|
import { Hono } from "hono";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { z } from "zod";
|
|
@@ -131,26 +131,256 @@ async function archiveConversation(viewer, conversationId, body) {
|
|
|
131
131
|
throwApiError(409, "Conversation received new activity.");
|
|
132
132
|
}
|
|
133
133
|
//#endregion
|
|
134
|
+
//#region src/chat/conversations/fork.ts
|
|
135
|
+
/** Event key of the agent note that follows the copied events in a fork. */
|
|
136
|
+
const FORK_NOTE_KEY = "fork:note";
|
|
137
|
+
const forkNote = alias(juniorConversationEvents, "fork_note");
|
|
138
|
+
/**
|
|
139
|
+
* SQL filter for event rows that a fork did not copy from its source. Usage
|
|
140
|
+
* and cost reports use it, so copied model calls count only in the source.
|
|
141
|
+
*/
|
|
142
|
+
function notCopiedByFork() {
|
|
143
|
+
return sql`not exists (
|
|
144
|
+
select 1 from ${juniorConversationEvents} as ${sql.identifier("fork_note")}
|
|
145
|
+
where ${forkNote.conversationId} = ${juniorConversationEvents.conversationId}
|
|
146
|
+
and ${forkNote.idempotencyKey} = ${FORK_NOTE_KEY}
|
|
147
|
+
and ${forkNote.seq} > ${juniorConversationEvents.seq}
|
|
148
|
+
)`;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Events that hold credentials, approvals, provider connections, or plugin
|
|
152
|
+
* state of the source actor. A fork does not copy them.
|
|
153
|
+
*/
|
|
154
|
+
const UNCOPIED_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
155
|
+
"authorization_completed",
|
|
156
|
+
"authorization_requested",
|
|
157
|
+
"guardian_action_reviewed",
|
|
158
|
+
"mcp_provider_connected",
|
|
159
|
+
"mcp_provider_connected_unowned",
|
|
160
|
+
"structured_event",
|
|
161
|
+
"unknown"
|
|
162
|
+
]);
|
|
163
|
+
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.";
|
|
164
|
+
/** An expected fork request failure, such as a reply without saved history. */
|
|
165
|
+
var ConversationForkError = class extends Error {};
|
|
166
|
+
/**
|
|
167
|
+
* Create a new web root Conversation from the agent history of a source
|
|
168
|
+
* Conversation, through one completed assistant reply.
|
|
169
|
+
*
|
|
170
|
+
* Retries with the same requester and key return the same Conversation.
|
|
171
|
+
*/
|
|
172
|
+
async function forkConversation(input) {
|
|
173
|
+
const conversationId = createConversationId({
|
|
174
|
+
actorEmail: input.actorEmail,
|
|
175
|
+
idempotencyKey: JSON.stringify([
|
|
176
|
+
"fork",
|
|
177
|
+
input.sourceConversationId,
|
|
178
|
+
input.idempotencyKey
|
|
179
|
+
])
|
|
180
|
+
});
|
|
181
|
+
const executor = getSqlExecutor();
|
|
182
|
+
const events = getConversationEventStore();
|
|
183
|
+
const store = getConversationStore();
|
|
184
|
+
return withConversationEventLock(executor, input.sourceConversationId, () => withConversationEventLock(executor, conversationId, async () => {
|
|
185
|
+
if (await store.get({ conversationId })) return { conversationId };
|
|
186
|
+
const source = await store.get({ conversationId: input.sourceConversationId });
|
|
187
|
+
if (!source || source.transcriptPurgedAtMs !== void 0) throw new ConversationForkError("The source history is no longer available.");
|
|
188
|
+
if (source.parentConversationId) throw new ConversationForkError("You cannot fork a child conversation.");
|
|
189
|
+
const boundary = await events.loadByIdempotencyKey(input.sourceConversationId, `message:${input.messageId}:agent`);
|
|
190
|
+
const reply = boundary?.data.type === "assistant_message" ? piMessageFromHistoryItem(boundary.data) : void 0;
|
|
191
|
+
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.");
|
|
192
|
+
const projection = await loadTurnProjection({
|
|
193
|
+
conversationId: input.sourceConversationId,
|
|
194
|
+
committedSeq: boundary.seq,
|
|
195
|
+
includeTail: false
|
|
196
|
+
});
|
|
197
|
+
if (!projection?.messages.length) throw new ConversationForkError("The selected history is no longer available.");
|
|
198
|
+
if (hasUnfinishedToolCalls(projection.messages)) throw new ConversationForkError("This history has unfinished tool calls. Choose another reply.");
|
|
199
|
+
const nowMs = Date.now();
|
|
200
|
+
await recordWebConversationActivity({
|
|
201
|
+
actor: webActorFromEmail(input.actorEmail, { ...input.actorName ? { fullName: input.actorName } : void 0 }),
|
|
202
|
+
conversationId,
|
|
203
|
+
nowMs,
|
|
204
|
+
rootVisibility: source.visibility === "public" ? "public" : "private"
|
|
205
|
+
});
|
|
206
|
+
await executor.db().update(juniorConversations).set({
|
|
207
|
+
forkedFromConversationId: source.conversationId,
|
|
208
|
+
title: source.title ? `Fork: ${source.title}` : "Forked conversation"
|
|
209
|
+
}).where(eq(juniorConversations.conversationId, conversationId));
|
|
210
|
+
await copyForkEvents({
|
|
211
|
+
sourceConversationId: source.conversationId,
|
|
212
|
+
conversationId,
|
|
213
|
+
events: await events.loadHistory(source.conversationId),
|
|
214
|
+
boundarySeq: boundary.seq,
|
|
215
|
+
messageId: input.messageId
|
|
216
|
+
});
|
|
217
|
+
await events.append(conversationId, [{
|
|
218
|
+
idempotencyKey: FORK_NOTE_KEY,
|
|
219
|
+
createdAtMs: nowMs,
|
|
220
|
+
data: {
|
|
221
|
+
type: "user_message",
|
|
222
|
+
provenance: contextProvenance,
|
|
223
|
+
content: [{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: FORK_NOTE
|
|
226
|
+
}],
|
|
227
|
+
timestamp: nowMs
|
|
228
|
+
}
|
|
229
|
+
}]);
|
|
230
|
+
return { conversationId };
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Copy the source event rows through the selected reply. Copies keep their
|
|
235
|
+
* sequence, author, timestamp, key, and payload, so the fork shows the same
|
|
236
|
+
* transcript and event log, and seq references in compactions stay valid.
|
|
237
|
+
* Later updates of copied Messages and the end of copied Turns are also
|
|
238
|
+
* copied, so that no copied item stays open in the fork.
|
|
239
|
+
*/
|
|
240
|
+
async function copyForkEvents(args) {
|
|
241
|
+
const reply = args.events.find((event) => event.data.type === "message" && event.data.role === "assistant" && event.data.messageId === args.messageId);
|
|
242
|
+
const cutoffSeq = Math.max(args.boundarySeq, reply?.seq ?? -1);
|
|
243
|
+
const messageIds = /* @__PURE__ */ new Set();
|
|
244
|
+
const turnIds = /* @__PURE__ */ new Set();
|
|
245
|
+
const seqs = [];
|
|
246
|
+
let historyVersion = 0;
|
|
247
|
+
for (const event of args.events) {
|
|
248
|
+
const data = event.data;
|
|
249
|
+
if (UNCOPIED_EVENT_TYPES.has(data.type)) continue;
|
|
250
|
+
if (event.idempotencyKey === FORK_NOTE_KEY) continue;
|
|
251
|
+
if (event.seq <= cutoffSeq) {
|
|
252
|
+
if (data.type === "message") messageIds.add(data.messageId);
|
|
253
|
+
if (data.type === "turn_started") turnIds.add(data.turnId);
|
|
254
|
+
historyVersion = Math.max(historyVersion, event.historyVersion);
|
|
255
|
+
seqs.push(event.seq);
|
|
256
|
+
} 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);
|
|
257
|
+
}
|
|
258
|
+
if (!seqs.length) return;
|
|
259
|
+
const e = juniorConversationEvents;
|
|
260
|
+
await getSqlExecutor().db().execute(sql`insert into ${e} (${sql.join([
|
|
261
|
+
e.conversationId,
|
|
262
|
+
e.seq,
|
|
263
|
+
e.historyVersion,
|
|
264
|
+
e.schemaVersion,
|
|
265
|
+
e.idempotencyKey,
|
|
266
|
+
e.type,
|
|
267
|
+
e.payload,
|
|
268
|
+
e.actorIdentityId,
|
|
269
|
+
e.createdAt
|
|
270
|
+
].map((column) => sql.identifier(column.name)), sql`, `)})
|
|
271
|
+
select ${args.conversationId}, ${e.seq}, least(${e.historyVersion}, ${historyVersion}), ${e.schemaVersion}, ${e.idempotencyKey}, ${e.type}, ${e.payload}, ${e.actorIdentityId}, ${e.createdAt}
|
|
272
|
+
from ${e}
|
|
273
|
+
where ${e.conversationId} = ${args.sourceConversationId}
|
|
274
|
+
and ${inArray(e.seq, seqs)}`);
|
|
275
|
+
}
|
|
276
|
+
function hasUnfinishedToolCalls(messages) {
|
|
277
|
+
const pending = /* @__PURE__ */ new Set();
|
|
278
|
+
for (const message of messages) if (message.role === "assistant") {
|
|
279
|
+
for (const part of message.content) if (part.type === "toolCall") pending.add(part.id);
|
|
280
|
+
} else if (message.role === "toolResult") pending.delete(message.toolCallId);
|
|
281
|
+
return pending.size > 0;
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/api/conversations/auxiliary-costs.ts
|
|
285
|
+
const auxiliaryRootConversation = alias(juniorConversations, "auxiliary_root_conversation");
|
|
286
|
+
function eventCost() {
|
|
287
|
+
return sql`CASE
|
|
288
|
+
WHEN ${juniorConversationEvents.type} = 'structured_event'
|
|
289
|
+
AND jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
|
|
290
|
+
THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::numeric
|
|
291
|
+
WHEN ${juniorConversationEvents.type} IN ('guardian_action_reviewed', 'turn_routed')
|
|
292
|
+
AND jsonb_typeof(${juniorConversationEvents.payload}->'costUsd') = 'number'
|
|
293
|
+
THEN (${juniorConversationEvents.payload}->>'costUsd')::numeric
|
|
294
|
+
ELSE NULL
|
|
295
|
+
END`;
|
|
296
|
+
}
|
|
297
|
+
/** Aggregate additive event costs by namespaced operation for conversations. */
|
|
298
|
+
async function readConversationAuxiliaryCostsFromSql(db, conversationIds, options) {
|
|
299
|
+
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
300
|
+
const selectedIds = [...conversationIds];
|
|
301
|
+
const conversationId = options.includeDescendants ? sql`coalesce(
|
|
302
|
+
${auxiliaryRootConversation.conversationId},
|
|
303
|
+
${juniorConversations.conversationId}
|
|
304
|
+
)` : sql`${juniorConversations.conversationId}`;
|
|
305
|
+
const namespace = sql`CASE
|
|
306
|
+
WHEN ${juniorConversationEvents.type} = 'structured_event'
|
|
307
|
+
THEN ${juniorConversationEvents.payload}->>'namespace'
|
|
308
|
+
ELSE 'junior'
|
|
309
|
+
END`;
|
|
310
|
+
const name = sql`CASE
|
|
311
|
+
WHEN ${juniorConversationEvents.type} = 'structured_event'
|
|
312
|
+
THEN ${juniorConversationEvents.payload}->>'name'
|
|
313
|
+
ELSE ${juniorConversationEvents.type}
|
|
314
|
+
END`;
|
|
315
|
+
const cost = eventCost();
|
|
316
|
+
const selectedConversation = options.includeDescendants ? or(and(inArray(juniorConversations.rootConversationId, selectedIds), isNotNull(auxiliaryRootConversation.conversationId)), inArray(juniorConversations.conversationId, selectedIds)) : inArray(juniorConversations.conversationId, selectedIds);
|
|
317
|
+
const rows = await db.select({
|
|
318
|
+
conversationId,
|
|
319
|
+
costUsd: sql`round(sum(${cost}), 12)::double precision`.mapWith(Number),
|
|
320
|
+
events: sql`count(*)::integer`.mapWith(Number),
|
|
321
|
+
name,
|
|
322
|
+
namespace
|
|
323
|
+
}).from(juniorConversationEvents).innerJoin(juniorConversations, eq(juniorConversations.conversationId, juniorConversationEvents.conversationId)).leftJoin(auxiliaryRootConversation, and(eq(auxiliaryRootConversation.conversationId, juniorConversations.rootConversationId), isNull(auxiliaryRootConversation.parentConversationId), eq(auxiliaryRootConversation.rootConversationId, auxiliaryRootConversation.conversationId))).where(and(selectedConversation, or(eq(juniorConversationEvents.type, "structured_event"), inArray(juniorConversationEvents.type, ["guardian_action_reviewed", "turn_routed"])), sql`${cost} >= 0`, notCopiedByFork())).groupBy(conversationId, namespace, name).orderBy(conversationId, namespace, name);
|
|
324
|
+
const result = /* @__PURE__ */ new Map();
|
|
325
|
+
for (const row of rows) {
|
|
326
|
+
const current = result.get(row.conversationId) ?? {
|
|
327
|
+
costUsd: 0,
|
|
328
|
+
operations: []
|
|
329
|
+
};
|
|
330
|
+
current.costUsd = Math.round((current.costUsd + row.costUsd) * 0xe8d4a51000) / 0xe8d4a51000;
|
|
331
|
+
current.operations.push({
|
|
332
|
+
costUsd: row.costUsd,
|
|
333
|
+
events: row.events,
|
|
334
|
+
name: row.name,
|
|
335
|
+
namespace: row.namespace
|
|
336
|
+
});
|
|
337
|
+
result.set(row.conversationId, current);
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
//#endregion
|
|
134
342
|
//#region src/api/conversations/child-agents.ts
|
|
343
|
+
/** Read the runtime and usage saved on each child Conversation row. */
|
|
344
|
+
async function readChildConversationMetrics(db, conversationIds) {
|
|
345
|
+
const rows = conversationIds.length === 0 ? [] : await db.select({
|
|
346
|
+
conversationId: juniorConversations.conversationId,
|
|
347
|
+
durationMs: juniorConversations.durationMs,
|
|
348
|
+
usage: juniorConversations.usage
|
|
349
|
+
}).from(juniorConversations).where(inArray(juniorConversations.conversationId, [...conversationIds]));
|
|
350
|
+
return new Map(rows.map((row) => [row.conversationId, {
|
|
351
|
+
cumulativeDurationMs: row.durationMs,
|
|
352
|
+
...row.usage ? { cumulativeUsage: row.usage } : void 0
|
|
353
|
+
}]));
|
|
354
|
+
}
|
|
135
355
|
/**
|
|
136
356
|
* Load the direct child agents of one Conversation, or throw a standard API
|
|
137
|
-
* not-found error. Viewers who cannot see private content get only the status
|
|
138
|
-
* and
|
|
357
|
+
* not-found error. Viewers who cannot see private content get only the status,
|
|
358
|
+
* time, and usage of each child agent.
|
|
139
359
|
*/
|
|
140
360
|
async function requireConversationChildAgents(conversationId, options = {}) {
|
|
141
361
|
if (!await getConversationStore().get({ conversationId })) throwApiError(404, "Conversation not found.");
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
362
|
+
const db = getDb();
|
|
363
|
+
const [accessByConversation, children] = await Promise.all([readConversationAccessFromSql(db, [conversationId], options.viewer), listChildAgents(conversationId)]);
|
|
364
|
+
const canExposePayload = accessByConversation.get(conversationId)?.canViewPrivateContent ?? false;
|
|
365
|
+
const childIds = children.map((agent) => agent.childConversationId);
|
|
366
|
+
const [metricsByChild, auxiliaryCostsByChild] = await Promise.all([readChildConversationMetrics(db, childIds), readConversationAuxiliaryCostsFromSql(db, childIds, { includeDescendants: false })]);
|
|
367
|
+
const agents = children.map((agent) => {
|
|
368
|
+
const auxiliaryCosts = auxiliaryCostsByChild.get(agent.childConversationId);
|
|
369
|
+
return {
|
|
370
|
+
conversationId: agent.childConversationId,
|
|
371
|
+
cumulativeDurationMs: 0,
|
|
372
|
+
...metricsByChild.get(agent.childConversationId),
|
|
373
|
+
...auxiliaryCosts ? { auxiliaryCosts } : void 0,
|
|
374
|
+
status: agent.status,
|
|
375
|
+
updatedAt: new Date(agent.updatedAtMs).toISOString(),
|
|
376
|
+
...canExposePayload ? {
|
|
377
|
+
...agent.agentName ? { name: agent.agentName } : void 0,
|
|
378
|
+
task: agent.task,
|
|
379
|
+
...agent.title ? { title: agent.title } : void 0,
|
|
380
|
+
...agent.errorMessage ? { error: agent.errorMessage } : void 0
|
|
381
|
+
} : { redacted: true }
|
|
382
|
+
};
|
|
383
|
+
});
|
|
154
384
|
return conversationChildAgentsReportSchema.parse({
|
|
155
385
|
agents,
|
|
156
386
|
conversationId
|
|
@@ -409,156 +639,6 @@ async function resolveSlackTeamDomains(teamIds) {
|
|
|
409
639
|
return new Map(resolved.filter((entry) => entry !== void 0));
|
|
410
640
|
}
|
|
411
641
|
//#endregion
|
|
412
|
-
//#region src/chat/conversations/fork.ts
|
|
413
|
-
/** Event key of the agent note that follows the copied events in a fork. */
|
|
414
|
-
const FORK_NOTE_KEY = "fork:note";
|
|
415
|
-
const forkNote = alias(juniorConversationEvents, "fork_note");
|
|
416
|
-
/**
|
|
417
|
-
* SQL filter for event rows that a fork did not copy from its source. Usage
|
|
418
|
-
* and cost reports use it, so copied model calls count only in the source.
|
|
419
|
-
*/
|
|
420
|
-
function notCopiedByFork() {
|
|
421
|
-
return sql`not exists (
|
|
422
|
-
select 1 from ${juniorConversationEvents} as ${sql.identifier("fork_note")}
|
|
423
|
-
where ${forkNote.conversationId} = ${juniorConversationEvents.conversationId}
|
|
424
|
-
and ${forkNote.idempotencyKey} = ${FORK_NOTE_KEY}
|
|
425
|
-
and ${forkNote.seq} > ${juniorConversationEvents.seq}
|
|
426
|
-
)`;
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Events that hold credentials, approvals, provider connections, or plugin
|
|
430
|
-
* state of the source actor. A fork does not copy them.
|
|
431
|
-
*/
|
|
432
|
-
const UNCOPIED_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
433
|
-
"authorization_completed",
|
|
434
|
-
"authorization_requested",
|
|
435
|
-
"guardian_action_reviewed",
|
|
436
|
-
"mcp_provider_connected",
|
|
437
|
-
"mcp_provider_connected_unowned",
|
|
438
|
-
"structured_event",
|
|
439
|
-
"unknown"
|
|
440
|
-
]);
|
|
441
|
-
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.";
|
|
442
|
-
/** An expected fork request failure, such as a reply without saved history. */
|
|
443
|
-
var ConversationForkError = class extends Error {};
|
|
444
|
-
/**
|
|
445
|
-
* Create a new web root Conversation from the agent history of a source
|
|
446
|
-
* Conversation, through one completed assistant reply.
|
|
447
|
-
*
|
|
448
|
-
* Retries with the same requester and key return the same Conversation.
|
|
449
|
-
*/
|
|
450
|
-
async function forkConversation(input) {
|
|
451
|
-
const conversationId = createConversationId({
|
|
452
|
-
actorEmail: input.actorEmail,
|
|
453
|
-
idempotencyKey: JSON.stringify([
|
|
454
|
-
"fork",
|
|
455
|
-
input.sourceConversationId,
|
|
456
|
-
input.idempotencyKey
|
|
457
|
-
])
|
|
458
|
-
});
|
|
459
|
-
const executor = getSqlExecutor();
|
|
460
|
-
const events = getConversationEventStore();
|
|
461
|
-
const store = getConversationStore();
|
|
462
|
-
return withConversationEventLock(executor, input.sourceConversationId, () => withConversationEventLock(executor, conversationId, async () => {
|
|
463
|
-
if (await store.get({ conversationId })) return { conversationId };
|
|
464
|
-
const source = await store.get({ conversationId: input.sourceConversationId });
|
|
465
|
-
if (!source || source.transcriptPurgedAtMs !== void 0) throw new ConversationForkError("The source history is no longer available.");
|
|
466
|
-
if (source.parentConversationId) throw new ConversationForkError("You cannot fork a child conversation.");
|
|
467
|
-
const boundary = await events.loadByIdempotencyKey(input.sourceConversationId, `message:${input.messageId}:agent`);
|
|
468
|
-
const reply = boundary?.data.type === "assistant_message" ? piMessageFromHistoryItem(boundary.data) : void 0;
|
|
469
|
-
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.");
|
|
470
|
-
const projection = await loadTurnProjection({
|
|
471
|
-
conversationId: input.sourceConversationId,
|
|
472
|
-
committedSeq: boundary.seq,
|
|
473
|
-
includeTail: false
|
|
474
|
-
});
|
|
475
|
-
if (!projection?.messages.length) throw new ConversationForkError("The selected history is no longer available.");
|
|
476
|
-
if (hasUnfinishedToolCalls(projection.messages)) throw new ConversationForkError("This history has unfinished tool calls. Choose another reply.");
|
|
477
|
-
const nowMs = Date.now();
|
|
478
|
-
await recordWebConversationActivity({
|
|
479
|
-
actor: webActorFromEmail(input.actorEmail, { ...input.actorName ? { fullName: input.actorName } : void 0 }),
|
|
480
|
-
conversationId,
|
|
481
|
-
nowMs,
|
|
482
|
-
rootVisibility: source.visibility === "public" ? "public" : "private"
|
|
483
|
-
});
|
|
484
|
-
await executor.db().update(juniorConversations).set({
|
|
485
|
-
forkedFromConversationId: source.conversationId,
|
|
486
|
-
title: source.title ? `Fork: ${source.title}` : "Forked conversation"
|
|
487
|
-
}).where(eq(juniorConversations.conversationId, conversationId));
|
|
488
|
-
await copyForkEvents({
|
|
489
|
-
sourceConversationId: source.conversationId,
|
|
490
|
-
conversationId,
|
|
491
|
-
events: await events.loadHistory(source.conversationId),
|
|
492
|
-
boundarySeq: boundary.seq,
|
|
493
|
-
messageId: input.messageId
|
|
494
|
-
});
|
|
495
|
-
await events.append(conversationId, [{
|
|
496
|
-
idempotencyKey: FORK_NOTE_KEY,
|
|
497
|
-
createdAtMs: nowMs,
|
|
498
|
-
data: {
|
|
499
|
-
type: "user_message",
|
|
500
|
-
provenance: contextProvenance,
|
|
501
|
-
content: [{
|
|
502
|
-
type: "text",
|
|
503
|
-
text: FORK_NOTE
|
|
504
|
-
}],
|
|
505
|
-
timestamp: nowMs
|
|
506
|
-
}
|
|
507
|
-
}]);
|
|
508
|
-
return { conversationId };
|
|
509
|
-
}));
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Copy the source event rows through the selected reply. Copies keep their
|
|
513
|
-
* sequence, author, timestamp, key, and payload, so the fork shows the same
|
|
514
|
-
* transcript and event log, and seq references in compactions stay valid.
|
|
515
|
-
* Later updates of copied Messages and the end of copied Turns are also
|
|
516
|
-
* copied, so that no copied item stays open in the fork.
|
|
517
|
-
*/
|
|
518
|
-
async function copyForkEvents(args) {
|
|
519
|
-
const reply = args.events.find((event) => event.data.type === "message" && event.data.role === "assistant" && event.data.messageId === args.messageId);
|
|
520
|
-
const cutoffSeq = Math.max(args.boundarySeq, reply?.seq ?? -1);
|
|
521
|
-
const messageIds = /* @__PURE__ */ new Set();
|
|
522
|
-
const turnIds = /* @__PURE__ */ new Set();
|
|
523
|
-
const seqs = [];
|
|
524
|
-
let historyVersion = 0;
|
|
525
|
-
for (const event of args.events) {
|
|
526
|
-
const data = event.data;
|
|
527
|
-
if (UNCOPIED_EVENT_TYPES.has(data.type)) continue;
|
|
528
|
-
if (event.idempotencyKey === FORK_NOTE_KEY) continue;
|
|
529
|
-
if (event.seq <= cutoffSeq) {
|
|
530
|
-
if (data.type === "message") messageIds.add(data.messageId);
|
|
531
|
-
if (data.type === "turn_started") turnIds.add(data.turnId);
|
|
532
|
-
historyVersion = Math.max(historyVersion, event.historyVersion);
|
|
533
|
-
seqs.push(event.seq);
|
|
534
|
-
} 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);
|
|
535
|
-
}
|
|
536
|
-
if (!seqs.length) return;
|
|
537
|
-
const e = juniorConversationEvents;
|
|
538
|
-
await getSqlExecutor().db().execute(sql`insert into ${e} (${sql.join([
|
|
539
|
-
e.conversationId,
|
|
540
|
-
e.seq,
|
|
541
|
-
e.historyVersion,
|
|
542
|
-
e.schemaVersion,
|
|
543
|
-
e.idempotencyKey,
|
|
544
|
-
e.type,
|
|
545
|
-
e.payload,
|
|
546
|
-
e.actorIdentityId,
|
|
547
|
-
e.createdAt
|
|
548
|
-
].map((column) => sql.identifier(column.name)), sql`, `)})
|
|
549
|
-
select ${args.conversationId}, ${e.seq}, least(${e.historyVersion}, ${historyVersion}), ${e.schemaVersion}, ${e.idempotencyKey}, ${e.type}, ${e.payload}, ${e.actorIdentityId}, ${e.createdAt}
|
|
550
|
-
from ${e}
|
|
551
|
-
where ${e.conversationId} = ${args.sourceConversationId}
|
|
552
|
-
and ${inArray(e.seq, seqs)}`);
|
|
553
|
-
}
|
|
554
|
-
function hasUnfinishedToolCalls(messages) {
|
|
555
|
-
const pending = /* @__PURE__ */ new Set();
|
|
556
|
-
for (const message of messages) if (message.role === "assistant") {
|
|
557
|
-
for (const part of message.content) if (part.type === "toolCall") pending.add(part.id);
|
|
558
|
-
} else if (message.role === "toolResult") pending.delete(message.toolCallId);
|
|
559
|
-
return pending.size > 0;
|
|
560
|
-
}
|
|
561
|
-
//#endregion
|
|
562
642
|
//#region src/chat/pi/sql-model-usage.ts
|
|
563
643
|
/** SQL reporting adapter for usage stored on assistant history events. */
|
|
564
644
|
const message = sql`${juniorConversationEvents.payload}`;
|
|
@@ -935,64 +1015,6 @@ async function readRootConversationMetricsFromSql(db, rootConversationIds) {
|
|
|
935
1015
|
}));
|
|
936
1016
|
}
|
|
937
1017
|
//#endregion
|
|
938
|
-
//#region src/api/conversations/auxiliary-costs.ts
|
|
939
|
-
const auxiliaryRootConversation = alias(juniorConversations, "auxiliary_root_conversation");
|
|
940
|
-
function eventCost() {
|
|
941
|
-
return sql`CASE
|
|
942
|
-
WHEN ${juniorConversationEvents.type} = 'structured_event'
|
|
943
|
-
AND jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
|
|
944
|
-
THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::numeric
|
|
945
|
-
WHEN ${juniorConversationEvents.type} IN ('guardian_action_reviewed', 'turn_routed')
|
|
946
|
-
AND jsonb_typeof(${juniorConversationEvents.payload}->'costUsd') = 'number'
|
|
947
|
-
THEN (${juniorConversationEvents.payload}->>'costUsd')::numeric
|
|
948
|
-
ELSE NULL
|
|
949
|
-
END`;
|
|
950
|
-
}
|
|
951
|
-
/** Aggregate additive event costs by namespaced operation for conversations. */
|
|
952
|
-
async function readConversationAuxiliaryCostsFromSql(db, conversationIds, options) {
|
|
953
|
-
if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
|
|
954
|
-
const selectedIds = [...conversationIds];
|
|
955
|
-
const conversationId = options.includeDescendants ? sql`coalesce(
|
|
956
|
-
${auxiliaryRootConversation.conversationId},
|
|
957
|
-
${juniorConversations.conversationId}
|
|
958
|
-
)` : sql`${juniorConversations.conversationId}`;
|
|
959
|
-
const namespace = sql`CASE
|
|
960
|
-
WHEN ${juniorConversationEvents.type} = 'structured_event'
|
|
961
|
-
THEN ${juniorConversationEvents.payload}->>'namespace'
|
|
962
|
-
ELSE 'junior'
|
|
963
|
-
END`;
|
|
964
|
-
const name = sql`CASE
|
|
965
|
-
WHEN ${juniorConversationEvents.type} = 'structured_event'
|
|
966
|
-
THEN ${juniorConversationEvents.payload}->>'name'
|
|
967
|
-
ELSE ${juniorConversationEvents.type}
|
|
968
|
-
END`;
|
|
969
|
-
const cost = eventCost();
|
|
970
|
-
const selectedConversation = options.includeDescendants ? or(and(inArray(juniorConversations.rootConversationId, selectedIds), isNotNull(auxiliaryRootConversation.conversationId)), inArray(juniorConversations.conversationId, selectedIds)) : inArray(juniorConversations.conversationId, selectedIds);
|
|
971
|
-
const rows = await db.select({
|
|
972
|
-
conversationId,
|
|
973
|
-
costUsd: sql`round(sum(${cost}), 12)::double precision`.mapWith(Number),
|
|
974
|
-
events: sql`count(*)::integer`.mapWith(Number),
|
|
975
|
-
name,
|
|
976
|
-
namespace
|
|
977
|
-
}).from(juniorConversationEvents).innerJoin(juniorConversations, eq(juniorConversations.conversationId, juniorConversationEvents.conversationId)).leftJoin(auxiliaryRootConversation, and(eq(auxiliaryRootConversation.conversationId, juniorConversations.rootConversationId), isNull(auxiliaryRootConversation.parentConversationId), eq(auxiliaryRootConversation.rootConversationId, auxiliaryRootConversation.conversationId))).where(and(selectedConversation, or(eq(juniorConversationEvents.type, "structured_event"), inArray(juniorConversationEvents.type, ["guardian_action_reviewed", "turn_routed"])), sql`${cost} >= 0`, notCopiedByFork())).groupBy(conversationId, namespace, name).orderBy(conversationId, namespace, name);
|
|
978
|
-
const result = /* @__PURE__ */ new Map();
|
|
979
|
-
for (const row of rows) {
|
|
980
|
-
const current = result.get(row.conversationId) ?? {
|
|
981
|
-
costUsd: 0,
|
|
982
|
-
operations: []
|
|
983
|
-
};
|
|
984
|
-
current.costUsd = Math.round((current.costUsd + row.costUsd) * 0xe8d4a51000) / 0xe8d4a51000;
|
|
985
|
-
current.operations.push({
|
|
986
|
-
costUsd: row.costUsd,
|
|
987
|
-
events: row.events,
|
|
988
|
-
name: row.name,
|
|
989
|
-
namespace: row.namespace
|
|
990
|
-
});
|
|
991
|
-
result.set(row.conversationId, current);
|
|
992
|
-
}
|
|
993
|
-
return result;
|
|
994
|
-
}
|
|
995
|
-
//#endregion
|
|
996
1018
|
//#region src/chat/plugins/unfinished-work.ts
|
|
997
1019
|
/**
|
|
998
1020
|
* Return assigned and unfinished plugin work for the candidate conversations.
|
|
@@ -1425,6 +1447,52 @@ async function readConversationForks(conversationId, viewer) {
|
|
|
1425
1447
|
return result;
|
|
1426
1448
|
}
|
|
1427
1449
|
//#endregion
|
|
1450
|
+
//#region src/api/conversations/agent-tree.ts
|
|
1451
|
+
/** Bound the parent chain that one agent tree reads. */
|
|
1452
|
+
const MAX_AGENT_TREE_DEPTH = 16;
|
|
1453
|
+
const CHILD_LABEL = "Subagent";
|
|
1454
|
+
async function readParentConversationId(conversationId) {
|
|
1455
|
+
const [row] = await getDb().select({ parentId: juniorConversations.parentConversationId }).from(juniorConversations).where(eq(juniorConversations.conversationId, conversationId));
|
|
1456
|
+
return row?.parentId ?? void 0;
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Read the agent tree of a child Conversation: its parent chain, root first,
|
|
1460
|
+
* with the child last. Returns undefined for a root Conversation. Each
|
|
1461
|
+
* Conversation has its own access check, so a viewer sees a label only where
|
|
1462
|
+
* they can see private content.
|
|
1463
|
+
*/
|
|
1464
|
+
async function readConversationAgentTree(conversationId, viewer) {
|
|
1465
|
+
const ids = [conversationId];
|
|
1466
|
+
let parentId = await readParentConversationId(conversationId);
|
|
1467
|
+
while (parentId && !ids.includes(parentId) && ids.length < MAX_AGENT_TREE_DEPTH) {
|
|
1468
|
+
ids.unshift(parentId);
|
|
1469
|
+
parentId = await readParentConversationId(parentId);
|
|
1470
|
+
}
|
|
1471
|
+
if (ids.length < 2) return;
|
|
1472
|
+
const root = parentId ? void 0 : await getConversationStore().get({ conversationId: ids[0] });
|
|
1473
|
+
const access = await readConversationAccessFromSql(getDb(), ids, viewer);
|
|
1474
|
+
return await Promise.all(ids.map(async (id) => {
|
|
1475
|
+
const itemAccess = access.get(id);
|
|
1476
|
+
if (root?.conversationId === id) return {
|
|
1477
|
+
conversationId: id,
|
|
1478
|
+
label: conversationSummaryFromStoredConversation({
|
|
1479
|
+
access: itemAccess,
|
|
1480
|
+
conversation: root,
|
|
1481
|
+
durationMs: 0
|
|
1482
|
+
}).displayTitle
|
|
1483
|
+
};
|
|
1484
|
+
if (!itemAccess?.canViewPrivateContent) return {
|
|
1485
|
+
conversationId: id,
|
|
1486
|
+
label: CHILD_LABEL
|
|
1487
|
+
};
|
|
1488
|
+
const agent = await getChildAgentLabel(id);
|
|
1489
|
+
return {
|
|
1490
|
+
conversationId: id,
|
|
1491
|
+
label: agent?.title ?? agent?.agentName ?? CHILD_LABEL
|
|
1492
|
+
};
|
|
1493
|
+
}));
|
|
1494
|
+
}
|
|
1495
|
+
//#endregion
|
|
1428
1496
|
//#region src/api/conversations/detail.ts
|
|
1429
1497
|
/** Project stored metadata and a bounded event page into a signed history cursor. */
|
|
1430
1498
|
function projectConversationDetail(args) {
|
|
@@ -1524,10 +1592,11 @@ async function readConversationDetail(conversationId, options = {}) {
|
|
|
1524
1592
|
limit: options.limit ?? 500
|
|
1525
1593
|
});
|
|
1526
1594
|
if (!report) return void 0;
|
|
1527
|
-
const forks = await readConversationForks(conversationId, options.viewer);
|
|
1595
|
+
const [forks, agentTree] = await Promise.all([readConversationForks(conversationId, options.viewer), readConversationAgentTree(conversationId, options.viewer)]);
|
|
1528
1596
|
return conversationDetailReportSchema.parse({
|
|
1529
1597
|
...report,
|
|
1530
|
-
...forks
|
|
1598
|
+
...forks,
|
|
1599
|
+
...agentTree ? { agentTree } : void 0
|
|
1531
1600
|
});
|
|
1532
1601
|
}
|
|
1533
1602
|
//#endregion
|
|
@@ -2073,6 +2142,59 @@ async function readConversationStats() {
|
|
|
2073
2142
|
return conversationStatsReportSchema.parse(await readConversationStatsFromSql());
|
|
2074
2143
|
}
|
|
2075
2144
|
//#endregion
|
|
2145
|
+
//#region src/api/conversations/stop.ts
|
|
2146
|
+
/**
|
|
2147
|
+
* Stop the active Turn for one conversation participant.
|
|
2148
|
+
*
|
|
2149
|
+
* The stop also removes the human messages queued before it. The viewer's own
|
|
2150
|
+
* web text comes back in the response so the dashboard can restore it to the
|
|
2151
|
+
* composer instead of losing it.
|
|
2152
|
+
*/
|
|
2153
|
+
async function stopConversationTurnForViewer(viewer, conversationId, services) {
|
|
2154
|
+
const conversationStore = getConversationStore();
|
|
2155
|
+
if (!await conversationStore.get({ conversationId })) throwApiError(404, "Conversation not found.");
|
|
2156
|
+
if (!(await readConversationAccessFromSql(getDb(), [conversationId], viewer)).get(conversationId)?.isParticipant) throwApiError(403, "Only conversation participants can stop this turn.");
|
|
2157
|
+
try {
|
|
2158
|
+
const result = await stopConversationTurn({
|
|
2159
|
+
conversationId,
|
|
2160
|
+
conversationStore,
|
|
2161
|
+
queue: services.queue
|
|
2162
|
+
});
|
|
2163
|
+
if (result.status === "no_work") return {
|
|
2164
|
+
conversationId,
|
|
2165
|
+
status: "no_work",
|
|
2166
|
+
stoppedInboundMessageIds: [],
|
|
2167
|
+
stoppedMessages: []
|
|
2168
|
+
};
|
|
2169
|
+
await clearSlackPendingReactions({
|
|
2170
|
+
getSlackAdapter: getProductionSlackAdapter,
|
|
2171
|
+
messages: result.pendingMessages
|
|
2172
|
+
});
|
|
2173
|
+
return {
|
|
2174
|
+
conversationId,
|
|
2175
|
+
status: "requested",
|
|
2176
|
+
stoppedInboundMessageIds: result.pendingMessages.map((message) => message.inboundMessageId),
|
|
2177
|
+
stoppedMessages: viewerWebMessages(result.pendingMessages, viewer)
|
|
2178
|
+
};
|
|
2179
|
+
} catch (error) {
|
|
2180
|
+
throwApiError(500, "Unable to stop the active turn.", error);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
function viewerWebMessages(messages, viewer) {
|
|
2184
|
+
const viewerUserId = webActorFromEmail(viewer.email).userId;
|
|
2185
|
+
return messages.flatMap((message) => {
|
|
2186
|
+
if (message.source !== "web") return [];
|
|
2187
|
+
const metadata = legacyWebMailboxMetadataSchema.safeParse(message.input.metadata);
|
|
2188
|
+
if (!metadata.success) return [];
|
|
2189
|
+
if (metadata.data.authorUserId !== viewerUserId) return [];
|
|
2190
|
+
const text = message.input.text.trim();
|
|
2191
|
+
return text ? [{
|
|
2192
|
+
inboundMessageId: message.inboundMessageId,
|
|
2193
|
+
text
|
|
2194
|
+
}] : [];
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
//#endregion
|
|
2076
2198
|
//#region src/api/conversations/routes.ts
|
|
2077
2199
|
/** Create the HTTP routes owned by the conversations API. */
|
|
2078
2200
|
function createConversationRoutes(options) {
|
|
@@ -2106,6 +2228,11 @@ function createConversationRoutes(options) {
|
|
|
2106
2228
|
const body = context.req.valid("json");
|
|
2107
2229
|
return jsonResponse(acceptedConversationMessageSchema, await appendConversationMessageForViewer(viewer, conversationId, body, options));
|
|
2108
2230
|
});
|
|
2231
|
+
app.post("/:conversationId/stop", requireViewer, validateRequest("param", conversationParamsSchema, "Invalid route parameters."), async (context) => {
|
|
2232
|
+
const viewer = context.get("viewer");
|
|
2233
|
+
const { conversationId } = context.req.valid("param");
|
|
2234
|
+
return jsonResponse(stopConversationTurnResponseSchema, await stopConversationTurnForViewer(viewer, conversationId, options));
|
|
2235
|
+
});
|
|
2109
2236
|
app.post("/:conversationId/forks", requireViewer, bodyLimit({ maxSize: 4096 }), validateRequest("param", conversationParamsSchema, "Invalid route parameters."), validateRequest("json", forkConversationBodySchema, "Invalid request body."), async (context) => jsonResponse(forkConversationResponseSchema, await forkConversationForViewer(context.get("viewer"), context.req.valid("param").conversationId, context.req.valid("json"))));
|
|
2110
2237
|
app.patch("/:conversationId/archive", requireViewer, validateRequest("param", conversationParamsSchema, "Invalid route parameters."), validateRequest("json", archiveConversationBodySchema, "Invalid request body."), async (context) => {
|
|
2111
2238
|
const viewer = context.get("viewer");
|
|
@@ -3388,7 +3515,7 @@ async function readPeoplePluginReports(args) {
|
|
|
3388
3515
|
reports: [],
|
|
3389
3516
|
source: "plugins"
|
|
3390
3517
|
});
|
|
3391
|
-
const { getPluginProfileReports } = await import("./agent-hooks-
|
|
3518
|
+
const { getPluginProfileReports } = await import("./agent-hooks-DpklSWZg.js").then((n) => n.t);
|
|
3392
3519
|
return pluginOperationalReportFeedSchema.parse({
|
|
3393
3520
|
generatedAt: new Date(nowMs).toISOString(),
|
|
3394
3521
|
reports: await getPluginProfileReports({
|
|
@@ -4083,7 +4210,7 @@ async function changeViewerAutomationLifecycle(user, kind, id, input) {
|
|
|
4083
4210
|
/** Read-only editor support. Core owns schedules; plugins own Event choices. */
|
|
4084
4211
|
/** Return only Event types that can start a durable Automation. */
|
|
4085
4212
|
function readAutomationEventCatalog() {
|
|
4086
|
-
return Object.entries(
|
|
4213
|
+
return Object.entries(automationEventCatalog(getEventCatalog())).flatMap(([namespace, registration]) => registration.resourceTypes.map((resource) => ({
|
|
4087
4214
|
namespace,
|
|
4088
4215
|
...resource
|
|
4089
4216
|
})));
|
|
@@ -4356,7 +4483,7 @@ function readDescriptionText(home) {
|
|
|
4356
4483
|
}
|
|
4357
4484
|
/** Read discovered skill names for authenticated runtime diagnostics. */
|
|
4358
4485
|
async function readSkillReports() {
|
|
4359
|
-
const { discoverSkills } = await import("./skills-
|
|
4486
|
+
const { discoverSkills } = await import("./skills-C6zJbudq.js").then((n) => n.o);
|
|
4360
4487
|
const skills = await discoverSkills();
|
|
4361
4488
|
return skillReportsSchema.parse(skills.map((skill) => ({
|
|
4362
4489
|
name: skill.name,
|
|
@@ -4365,7 +4492,7 @@ async function readSkillReports() {
|
|
|
4365
4492
|
}
|
|
4366
4493
|
/** Read safe configured plugin metadata for authenticated runtime diagnostics. */
|
|
4367
4494
|
async function readPlugins() {
|
|
4368
|
-
const { pluginCatalogRuntime } = await import("./catalog-runtime-
|
|
4495
|
+
const { pluginCatalogRuntime } = await import("./catalog-runtime-CTaQXYqS.js").then((n) => n.t);
|
|
4369
4496
|
return pluginsSchema.parse(pluginCatalogRuntime.getProviders().map((plugin) => ({
|
|
4370
4497
|
configKeys: plugin.manifest.configKeys ?? [],
|
|
4371
4498
|
description: plugin.manifest.description,
|
|
@@ -4377,7 +4504,7 @@ async function readPlugins() {
|
|
|
4377
4504
|
async function readRuntimeInfoReport() {
|
|
4378
4505
|
const [{ homeDir }, { pluginCatalogRuntime }, plugins, skills] = await Promise.all([
|
|
4379
4506
|
import("./package-discovery-BVvSnQBB.js").then((n) => n.r),
|
|
4380
|
-
import("./catalog-runtime-
|
|
4507
|
+
import("./catalog-runtime-CTaQXYqS.js").then((n) => n.t),
|
|
4381
4508
|
readPlugins(),
|
|
4382
4509
|
readSkillReports()
|
|
4383
4510
|
]);
|
|
@@ -4394,7 +4521,7 @@ async function readRuntimeInfoReport() {
|
|
|
4394
4521
|
/** Read sanitized operational summaries contributed by plugins. */
|
|
4395
4522
|
async function readPluginOperationalReportFeed() {
|
|
4396
4523
|
const nowMs = Date.now();
|
|
4397
|
-
const { getPluginOperationalReports } = await import("./agent-hooks-
|
|
4524
|
+
const { getPluginOperationalReports } = await import("./agent-hooks-DpklSWZg.js").then((n) => n.t);
|
|
4398
4525
|
return pluginOperationalReportFeedSchema.parse({
|
|
4399
4526
|
source: "plugins",
|
|
4400
4527
|
generatedAt: new Date(nowMs).toISOString(),
|