@sentry/junior 0.234.0 → 0.236.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.
Files changed (71) hide show
  1. package/dist/{agent-hooks-BcH6E48y.js → agent-hooks-Da_H0jHv.js} +17 -10
  2. package/dist/{agent-runner-DUkGlK89.js → agent-runner-uP7QRc4c.js} +484 -162
  3. package/dist/api/schema.d.ts +288 -2
  4. package/dist/api/schema.js +5 -4
  5. package/dist/api.d.ts +8 -2
  6. package/dist/api.js +524 -83
  7. package/dist/{app-xk2-Id1T.d.ts → app-MEd7wbF0.d.ts} +83 -49
  8. package/dist/app.d.ts +3 -3
  9. package/dist/app.js +164 -119
  10. package/dist/{types-Du-HbEun.js → automation-B9Kc1gcf.js} +304 -33
  11. package/dist/{briefs-lyAQbDxJ.js → briefs-DrjBPQFy.js} +4 -4
  12. package/dist/{catalog-runtime-BSgsPVnr.js → catalog-runtime-C7339Cfm.js} +1 -1
  13. package/dist/{checkpoint-BNfXlq_1.js → checkpoint-DaD7_Gsb.js} +16 -9
  14. package/dist/cli/chat.d.ts +1 -1
  15. package/dist/cli/chat.js +14 -14
  16. package/dist/cli/check.js +4 -4
  17. package/dist/cli/main.js +1 -1
  18. package/dist/cli/plugins.d.ts +1 -1
  19. package/dist/cli/plugins.js +4 -4
  20. package/dist/cli/snapshot-create.js +3 -3
  21. package/dist/cli/upgrade.d.ts +1 -1
  22. package/dist/cli/upgrade.js +5 -5
  23. package/dist/{client-BX5lJRqW.js → client-Ch9Hg9NU.js} +2 -2
  24. package/dist/{config-DuCzUoCo.js → config-JR9wke1W.js} +57 -17
  25. package/dist/{conversation-CPjHYOV5.js → conversation-9Fxa1Zbq.js} +14 -1
  26. package/dist/{conversation-privacy-CSxvdSoV.js → conversation-privacy-au02dyZK.js} +1 -1
  27. package/dist/{credential-sync-BO4DfH9a.js → credential-sync-BWTA3LnL.js} +14 -14
  28. package/dist/{executor--jWf-Nnm.js → executor-BW9XWgUQ.js} +70 -6
  29. package/dist/{images-BaL19wFC.js → images-BdG7zcNf.js} +3 -3
  30. package/dist/instrumentation.js +2 -2
  31. package/dist/{job-queue-cwHVOwVZ.js → job-queue-BqXd0ZPV.js} +1 -1
  32. package/dist/{logging-Cox1Mgcv.js → logging-B4sKwmCG.js} +1 -1
  33. package/dist/nitro.d.ts +2 -2
  34. package/dist/nitro.js +5 -5
  35. package/dist/{oauth-authorization-CC8a1JKP.js → oauth-authorization-BjRN99dc.js} +1 -1
  36. package/dist/{oauth-callback-server-Z7QSt416.js → oauth-callback-server-B_1p98Mb.js} +2 -2
  37. package/dist/{pending-auth-DQhC3eSR.js → pending-auth-WqF4Gh8Z.js} +1 -1
  38. package/dist/{plugins-BT2l4We3.d.ts → plugins-CjfUP5gJ.d.ts} +1 -0
  39. package/dist/{profile-BdApelHS.js → profile-BamYnmhO.js} +4 -4
  40. package/dist/{prompt-3D0sSbTC.js → prompt-Ctwy6hSI.js} +1 -1
  41. package/dist/queue-B5SnqRln.d.ts +25 -0
  42. package/dist/{registry-DiY6B8CN.js → registry-CXNv9fDX.js} +38 -2
  43. package/dist/{resolve-VpJoiDpc.js → resolve-C9ETkt4T.js} +2 -2
  44. package/dist/{runner-BIMegnPE.js → runner-DnBGVRnW.js} +9 -9
  45. package/dist/{sandbox-egress-signals-Cbtxe492.js → sandbox-egress-signals-DwJ7Boi_.js} +1 -1
  46. package/dist/{schema-CxwnlTDN.js → schema-CJiqDc5r.js} +26 -3
  47. package/dist/{skills-WttpPJUy.js → skills-Dd8O9Aav.js} +3 -3
  48. package/dist/{spawn-DhfMKt4y.js → spawn-DFdRspSD.js} +2 -2
  49. package/dist/{task-queue-CZqUNJD0.js → task-queue-DGQEOk1A.js} +16 -10
  50. package/dist/{task-runner-BZaqoUCT.js → task-runner-C5XCIDSF.js} +9 -9
  51. package/dist/{thread-state-8tXZhu0y.js → thread-state-D3E29wZk.js} +5 -5
  52. package/dist/{turn-failure-response-DFJ9L9NQ.js → turn-failure-response-TDoj0aZz.js} +8 -8
  53. package/dist/{turn-lifecycle-WbpTUQ_r.js → turn-lifecycle-DOnU45p7.js} +1 -1
  54. package/dist/{turn-session-routing--EhI76rH.js → turn-session-routing-DbpZZoin.js} +1 -1
  55. package/dist/{validation-0F9IgG3b.js → validation-B9lMbisx.js} +1 -1
  56. package/dist/{version-A7-RRBOE.js → version-CWkIGcLU.js} +1 -1
  57. package/dist/version.js +1 -1
  58. package/dist/{web-authorization-BJggXofy.js → web-authorization-PPaoj2ns.js} +6 -6
  59. package/dist/{web-input-B-siN5Dr.js → web-input-CfNxXtHm.js} +86 -34
  60. package/dist/web-message-id-xPI_EFSC.js +239 -0
  61. package/dist/{work-CNc0ejGG.js → work-DNaoFrEe.js} +7 -7
  62. package/dist/{worker-BHbqpUAu.js → worker-xeJLxYwm.js} +5 -5
  63. package/migrations/0045_conversation_forks.sql +3 -0
  64. package/migrations/0046_automation_versions.sql +71 -0
  65. package/migrations/meta/0045_snapshot.json +3503 -0
  66. package/migrations/meta/0046_snapshot.json +3707 -0
  67. package/migrations/meta/_journal.json +15 -1
  68. package/package.json +5 -5
  69. package/dist/web-message-id-CVVCTWUb.js +0 -464
  70. package/dist/{package-discovery-7a9YqLhE.js → package-discovery-BVvSnQBB.js} +1 -1
  71. package/dist/{plugin-module-BKGOXca2.js → plugin-module-CVvEUSMH.js} +1 -1
package/dist/api.js CHANGED
@@ -1,20 +1,21 @@
1
- import { Dt as cancelHumanFacingPendingMessages, J as editEventAutomation, L as editScheduledAutomation, Lt as getConversation, M as parseSlackThreadId, St as pluginEventCatalog, U as readScheduledAutomation, W as saveScheduledAutomation, X as eventAutomationTriggerAvailable, Y as AutomationEditError, at as saveEventAutomation, b as readStats, bt as getEventCatalog, ct as automationRevision, et as eventAutomationBelongsToUser, k as readLiveAttachment, lt as requireAutomationRevision, nt as getEventAutomation, ot as setEventAutomationStatus, st as AutomationConflictError } from "./agent-runner-DUkGlK89.js";
2
- import { d as logException, p as logWarn } from "./logging-Cox1Mgcv.js";
1
+ import { $ as resolveTaskOutcomes, Et as getEventCatalog, G as saveScheduledAutomation, M as parseSlackThreadId, Nt as cancelHumanFacingPendingMessages, Ot as pluginEventCatalog, R as editScheduledAutomation, Ut as getConversation, W as readScheduledAutomation, X as AutomationEditError, Y as editEventAutomation, Z as eventAutomationTriggerAvailable, ct as setEventAutomationStatus, dt as readAutomationVersion, ft as sameDefinitionValue, g as readStats, ht as requireAutomationRevision, it as getEventAutomation, k as readLiveAttachment, lt as automationDefinition, mt as automationRevision, nt as eventAutomationBelongsToUser, pt as AutomationConflictError, st as saveEventAutomation } from "./agent-runner-uP7QRc4c.js";
2
+ import { d as logException, p as logWarn } from "./logging-B4sKwmCG.js";
3
3
  import { b as parseSlackMessageTs, h as parseSlackTeamId, p as parseSlackChannelId } from "./context-0Et9iVSB.js";
4
- import { $ as resolveViewerUser, $t as getConversationStore, At as conversationNotArchivedForEmail, Ct as createPluginLogger, D as SANDBOX_RUNTIME, Dt as conversationArchivedForUser, Et as conversationArchivedForEmail, Ft as readConversationEventPage, Gt as listConversationAnnotationsById, It as buildSentryConversationUrl, J as withSlackRetries, Mt as viewerConversationMembership, Ot as conversationHasParticipantEmail, Pt as readLatestConversationBrief, S as WorkspaceValidationError, Tt as conversationArchiveTimes, U as getSlackClient, Wt as listConversationAnnotations, Z as findUserByEmail, _ as deleteWorkspace, an as locationFromRow, b as listWorkspaces, dn as agentTurnUsageSchema, en as getDb, et as updateViewerDisplayName, fn as hasAgentTurnUsage, g as createWorkspace, j as workspaceRepoCheckoutPath, jt as conversationNotArchivedForUser, kt as conversationHasParticipantUser, pn as resolveRootConversationId, sn as parseSessionSource, tn as getSqlExecutor, u as getPlugins, v as getWorkspace, wt as readConversationAccessFromSql, x as updateWorkspace } from "./agent-hooks-BcH6E48y.js";
4
+ import { At as conversationHasParticipantUser, C as WorkspaceValidationError, Dt as conversationArchivedForEmail, Et as conversationArchiveTimes, Ft as readLatestConversationBrief, Gt as listConversationAnnotations, It as readConversationEventPage, Kt as listConversationAnnotationsById, Lt as buildSentryConversationUrl, M as workspaceRepoCheckoutPath, Mt as conversationNotArchivedForUser, Nt as viewerConversationMembership, O as SANDBOX_RUNTIME, Ot as conversationArchivedForUser, Q as findUserByEmail, Qt as getConversationEventStore, S as updateWorkspace, Tt as readConversationAccessFromSql, W as getSlackClient, Y as withSlackRetries, _ as deleteWorkspace, b as listWorkspaces, cn as parseSessionSource, en as getConversationStore, et as resolveViewerUser, fn as agentTurnUsageSchema, g as createWorkspace, in as withConversationEventLock, jt as conversationNotArchivedForEmail, kt as conversationHasParticipantEmail, mn as resolveRootConversationId, nn as getSqlExecutor, on as locationFromRow, pn as hasAgentTurnUsage, tn as getDb, tt as updateViewerDisplayName, u as getPlugins, v as getWorkspace, wt as createPluginLogger, x as listWorkspacesByRepository } from "./agent-hooks-Da_H0jHv.js";
5
5
  import { n as juniorDestinations } from "./destinations-BeBiJeEM.js";
6
- import { C as juniorCodeRepositories, D as juniorIdentities, E as juniorConversations, O as juniorUsers, S as juniorCodeChanges, T as conversationReadColumns, b as juniorApiTokens, g as juniorConversationBriefs, m as juniorConversationParticipants, v as juniorConversationMetrics, y as juniorConversationEvents } from "./executor--jWf-Nnm.js";
7
- import { D as createConversationBodySchema, M as INPUT_IMAGE_TYPES, O as createConversationMessageBodySchema, 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 readMessageAttachments, m as conversationEventsQuerySchema, o as cancelConversationPendingMessagesResponseSchema, p as conversationEventPageSchema, r as archiveConversationBodySchema, t as acceptedConversationMessageSchema, u as conversationDetailQuerySchema, x as conversationPendingMessagesReportSchema } from "./conversation-CPjHYOV5.js";
8
- import { n as hash, t as create } from "./profile-BdApelHS.js";
9
- import { t as getCachedSnapshot } from "./resolve-VpJoiDpc.js";
10
- import { a as getVercelConversationWorkQueue } from "./job-queue-cwHVOwVZ.js";
11
- import { c as ScheduleIntentError, d as getFirstRunAtMs, l as compileScheduleIntent } from "./types-Du-HbEun.js";
12
- import { A as automationSummarySchema, B as pluginsSchema, C as automationListSchema, F as pluginOperationalReportFeedSchema, O as automationScheduleIntentSchema, P as healthReportSchema, S as automationListQuerySchema, T as automationRunListSchema, U as skillReportsSchema, V as runtimeInfoReportSchema, _ as automationExecutionListSchema, a as fillUtcHours, c as sumUtcHoursIntoSixHours, d as trailingUtcSixHourWindow, f as utcDayKey, g as automationEventCatalogSchema, h as automationEditSchema, i as fillUtcDays, j as automationUpdateSchema, k as automationSchedulePreviewSchema, l as trailingUtcDayWindow, m as automationEditErrorSchema, n as DAY_MS$1, o as fillUtcSixHours, p as utcHourKey, r as HOUR_MS$1, s as startOfUtcHour, u as trailingUtcHourWindow, w as automationParamsSchema, x as automationLifecycleSchema } from "./web-message-id-CVVCTWUb.js";
13
- import { F as createVercelAttachmentStorage, _ as readViewerAutomations, a as webActorFromEmail, b as clearSlackPendingReactions, c as getProductionSlackAdapter, d as ViewerTaskNotFoundError, f as deleteViewerTask, g as readViewerAutomationSummary, h as readViewerAutomationRuns, k as legacyWebMailboxMetadataSchema, l as formatSlackConversationRedactedLabel, m as readViewerAutomationExecutions, p as readConversationSourceTask, r as createAndEnqueueConversation, t as appendAndEnqueueWebMessage, u as resolveSlackConversationContextFromThreadId, x as lookupSlackUser, z as readHealthReport } from "./web-input-B-siN5Dr.js";
14
- import { A as conversationAggregateColumns, j as conversationRangeColumns, k as conversationActiveDaysColumn } from "./task-runner-BZaqoUCT.js";
15
- import { r as getWebAuthorization } from "./web-authorization-BJggXofy.js";
16
- import { t as decodeInputImages } from "./images-BaL19wFC.js";
17
- import { C as locationParamsSchema, E as activitySixHours$1, M as codePersonReportSchema, P as apiErrorSchema, 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 } from "./schema-CxwnlTDN.js";
6
+ import { C as juniorCodeChanges, D as juniorConversations, E as conversationReadColumns, O as juniorIdentities, _ as juniorConversationBriefs, b as juniorConversationEvents, h as juniorConversationParticipants, k as juniorUsers, w as juniorCodeRepositories, x as juniorApiTokens, y as juniorConversationMetrics } from "./executor-BW9XWgUQ.js";
7
+ import { A as forkConversationResponseSchema, D as createConversationBodySchema, N as readMessageAttachments, O as createConversationMessageBodySchema, P as INPUT_IMAGE_TYPES, T as conversationStatsReportSchema, _ as conversationParamsSchema, a as cancelConversationPendingMessagesBodySchema, c as conversationAttachmentParamsSchema, d as conversationDetailReportSchema, g as conversationFeedSchema, h as conversationFeedQuerySchema, i as archiveConversationResponseSchema, k as forkConversationBodySchema, m as conversationEventsQuerySchema, nt as contextProvenance, o as cancelConversationPendingMessagesResponseSchema, p as conversationEventPageSchema, r as archiveConversationBodySchema, t as acceptedConversationMessageSchema, u as conversationDetailQuerySchema, x as conversationPendingMessagesReportSchema } from "./conversation-9Fxa1Zbq.js";
8
+ import { F as loadTurnProjection, vt as piMessageFromHistoryItem } from "./checkpoint-DaD7_Gsb.js";
9
+ import { n as hash, t as create } from "./profile-BamYnmhO.js";
10
+ import { t as getCachedSnapshot } from "./resolve-C9ETkt4T.js";
11
+ import { a as getVercelConversationWorkQueue } from "./job-queue-BqXd0ZPV.js";
12
+ import { F as getFirstRunAtMs, 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-B9Kc1gcf.js";
13
+ 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";
14
+ import { C as clearSlackPendingReactions, H as readHealthReport, M as legacyWebMailboxMetadataSchema, R as createVercelAttachmentStorage, _ as readViewerAutomationSummary, a as recordWebConversationActivity, b as resolveViewerTaskCandidate, d as resolveSlackConversationContextFromThreadId, f as ViewerTaskNotFoundError, g as readViewerAutomationRuns, h as readViewerAutomationExecutions, i as createConversationId, l as getProductionSlackAdapter, m as readConversationSourceTask, o as webActorFromEmail, p as deleteViewerTask, r as createAndEnqueueConversation, t as appendAndEnqueueWebMessage, u as formatSlackConversationRedactedLabel, v as readViewerAutomationVersions, w as lookupSlackUser, y as readViewerAutomations } from "./web-input-CfNxXtHm.js";
15
+ import { A as conversationAggregateColumns, j as conversationRangeColumns, k as conversationActiveDaysColumn } from "./task-runner-C5XCIDSF.js";
16
+ import { r as getWebAuthorization } from "./web-authorization-PPaoj2ns.js";
17
+ import { t as decodeInputImages } from "./images-BdG7zcNf.js";
18
+ import { C as locationParamsSchema, E as activitySixHours$1, I as apiErrorSchema, M as codePersonReportSchema, N as codeRepositoryParamsSchema, P as codeRepositoryReportSchema, 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 } from "./schema-CJiqDc5r.js";
18
19
  import { Hono } from "hono";
19
20
  import path from "node:path";
20
21
  import { z } from "zod";
@@ -186,7 +187,7 @@ function actorFromViewer(viewer) {
186
187
  });
187
188
  }
188
189
  /** Create a dashboard root conversation and enqueue its first message. */
189
- async function createConversationForViewer(viewer, body, attachmentStorage) {
190
+ async function createConversationForViewer(viewer, body, services) {
190
191
  const images = parseImages(body.images);
191
192
  try {
192
193
  return await createAndEnqueueConversation({
@@ -197,15 +198,15 @@ async function createConversationForViewer(viewer, body, attachmentStorage) {
197
198
  ...body.visibility ? { visibility: body.visibility } : void 0
198
199
  }, {
199
200
  conversationStore: getConversationStore(),
200
- attachmentStorage,
201
- queue: getVercelConversationWorkQueue()
201
+ attachmentStorage: services.attachmentStorage,
202
+ queue: services.queue
202
203
  });
203
204
  } catch (error) {
204
205
  throwApiError(500, "Unable to create conversation.", error);
205
206
  }
206
207
  }
207
208
  /** Append one dashboard message to an existing conversation. */
208
- async function appendConversationMessageForViewer(viewer, conversationId, body, attachmentStorage) {
209
+ async function appendConversationMessageForViewer(viewer, conversationId, body, services) {
209
210
  const conversation = await getConversationStore().get({ conversationId });
210
211
  if (!conversation) throwApiError(404, "Conversation not found.");
211
212
  const destinationPlatform = conversation.destination?.platform;
@@ -221,8 +222,8 @@ async function appendConversationMessageForViewer(viewer, conversationId, body,
221
222
  images
222
223
  }, {
223
224
  conversationStore: getConversationStore(),
224
- attachmentStorage,
225
- queue: getVercelConversationWorkQueue()
225
+ attachmentStorage: services.attachmentStorage,
226
+ queue: services.queue
226
227
  });
227
228
  } catch (error) {
228
229
  throwApiError(500, "Unable to append message.", error);
@@ -379,6 +380,156 @@ async function resolveSlackTeamDomains(teamIds) {
379
380
  return new Map(resolved.filter((entry) => entry !== void 0));
380
381
  }
381
382
  //#endregion
383
+ //#region src/chat/conversations/fork.ts
384
+ /** Event key of the agent note that follows the copied events in a fork. */
385
+ const FORK_NOTE_KEY = "fork:note";
386
+ const forkNote = alias(juniorConversationEvents, "fork_note");
387
+ /**
388
+ * SQL filter for event rows that a fork did not copy from its source. Usage
389
+ * and cost reports use it, so copied model calls count only in the source.
390
+ */
391
+ function notCopiedByFork() {
392
+ return sql`not exists (
393
+ select 1 from ${juniorConversationEvents} as ${sql.identifier("fork_note")}
394
+ where ${forkNote.conversationId} = ${juniorConversationEvents.conversationId}
395
+ and ${forkNote.idempotencyKey} = ${FORK_NOTE_KEY}
396
+ and ${forkNote.seq} > ${juniorConversationEvents.seq}
397
+ )`;
398
+ }
399
+ /**
400
+ * Events that hold credentials, approvals, provider connections, or plugin
401
+ * state of the source actor. A fork does not copy them.
402
+ */
403
+ const UNCOPIED_EVENT_TYPES = /* @__PURE__ */ new Set([
404
+ "authorization_completed",
405
+ "authorization_requested",
406
+ "guardian_action_reviewed",
407
+ "mcp_provider_connected",
408
+ "mcp_provider_connected_unowned",
409
+ "structured_event",
410
+ "unknown"
411
+ ]);
412
+ 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.";
413
+ /** An expected fork request failure, such as a reply without saved history. */
414
+ var ConversationForkError = class extends Error {};
415
+ /**
416
+ * Create a new web root Conversation from the agent history of a source
417
+ * Conversation, through one completed assistant reply.
418
+ *
419
+ * Retries with the same requester and key return the same Conversation.
420
+ */
421
+ async function forkConversation(input) {
422
+ const conversationId = createConversationId({
423
+ actorEmail: input.actorEmail,
424
+ idempotencyKey: JSON.stringify([
425
+ "fork",
426
+ input.sourceConversationId,
427
+ input.idempotencyKey
428
+ ])
429
+ });
430
+ const executor = getSqlExecutor();
431
+ const events = getConversationEventStore();
432
+ const store = getConversationStore();
433
+ return withConversationEventLock(executor, input.sourceConversationId, () => withConversationEventLock(executor, conversationId, async () => {
434
+ if (await store.get({ conversationId })) return { conversationId };
435
+ const source = await store.get({ conversationId: input.sourceConversationId });
436
+ if (!source || source.transcriptPurgedAtMs !== void 0) throw new ConversationForkError("The source history is no longer available.");
437
+ if (source.parentConversationId) throw new ConversationForkError("You cannot fork a child conversation.");
438
+ const boundary = await events.loadByIdempotencyKey(input.sourceConversationId, `message:${input.messageId}:agent`);
439
+ const reply = boundary?.data.type === "assistant_message" ? piMessageFromHistoryItem(boundary.data) : void 0;
440
+ 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.");
441
+ const projection = await loadTurnProjection({
442
+ conversationId: input.sourceConversationId,
443
+ committedSeq: boundary.seq,
444
+ includeTail: false
445
+ });
446
+ if (!projection?.messages.length) throw new ConversationForkError("The selected history is no longer available.");
447
+ if (hasUnfinishedToolCalls(projection.messages)) throw new ConversationForkError("This history has unfinished tool calls. Choose another reply.");
448
+ const nowMs = Date.now();
449
+ await recordWebConversationActivity({
450
+ actor: webActorFromEmail(input.actorEmail, { ...input.actorName ? { fullName: input.actorName } : void 0 }),
451
+ conversationId,
452
+ nowMs,
453
+ rootVisibility: source.visibility === "public" ? "public" : "private"
454
+ });
455
+ await executor.db().update(juniorConversations).set({
456
+ forkedFromConversationId: source.conversationId,
457
+ title: source.title ? `Fork: ${source.title}` : "Forked conversation"
458
+ }).where(eq(juniorConversations.conversationId, conversationId));
459
+ await copyForkEvents({
460
+ sourceConversationId: source.conversationId,
461
+ conversationId,
462
+ events: await events.loadHistory(source.conversationId),
463
+ boundarySeq: boundary.seq,
464
+ messageId: input.messageId
465
+ });
466
+ await events.append(conversationId, [{
467
+ idempotencyKey: FORK_NOTE_KEY,
468
+ createdAtMs: nowMs,
469
+ data: {
470
+ type: "user_message",
471
+ provenance: contextProvenance,
472
+ content: [{
473
+ type: "text",
474
+ text: FORK_NOTE
475
+ }],
476
+ timestamp: nowMs
477
+ }
478
+ }]);
479
+ return { conversationId };
480
+ }));
481
+ }
482
+ /**
483
+ * Copy the source event rows through the selected reply. Copies keep their
484
+ * sequence, author, timestamp, key, and payload, so the fork shows the same
485
+ * transcript and event log, and seq references in compactions stay valid.
486
+ * Later updates of copied Messages and the end of copied Turns are also
487
+ * copied, so that no copied item stays open in the fork.
488
+ */
489
+ async function copyForkEvents(args) {
490
+ const reply = args.events.find((event) => event.data.type === "message" && event.data.role === "assistant" && event.data.messageId === args.messageId);
491
+ const cutoffSeq = Math.max(args.boundarySeq, reply?.seq ?? -1);
492
+ const messageIds = /* @__PURE__ */ new Set();
493
+ const turnIds = /* @__PURE__ */ new Set();
494
+ const seqs = [];
495
+ let historyVersion = 0;
496
+ for (const event of args.events) {
497
+ const data = event.data;
498
+ if (UNCOPIED_EVENT_TYPES.has(data.type)) continue;
499
+ if (event.idempotencyKey === FORK_NOTE_KEY) continue;
500
+ if (event.seq <= cutoffSeq) {
501
+ if (data.type === "message") messageIds.add(data.messageId);
502
+ if (data.type === "turn_started") turnIds.add(data.turnId);
503
+ historyVersion = Math.max(historyVersion, event.historyVersion);
504
+ seqs.push(event.seq);
505
+ } 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);
506
+ }
507
+ if (!seqs.length) return;
508
+ const e = juniorConversationEvents;
509
+ await getSqlExecutor().db().execute(sql`insert into ${e} (${sql.join([
510
+ e.conversationId,
511
+ e.seq,
512
+ e.historyVersion,
513
+ e.schemaVersion,
514
+ e.idempotencyKey,
515
+ e.type,
516
+ e.payload,
517
+ e.actorIdentityId,
518
+ e.createdAt
519
+ ].map((column) => sql.identifier(column.name)), sql`, `)})
520
+ select ${args.conversationId}, ${e.seq}, least(${e.historyVersion}, ${historyVersion}), ${e.schemaVersion}, ${e.idempotencyKey}, ${e.type}, ${e.payload}, ${e.actorIdentityId}, ${e.createdAt}
521
+ from ${e}
522
+ where ${e.conversationId} = ${args.sourceConversationId}
523
+ and ${inArray(e.seq, seqs)}`);
524
+ }
525
+ function hasUnfinishedToolCalls(messages) {
526
+ const pending = /* @__PURE__ */ new Set();
527
+ for (const message of messages) if (message.role === "assistant") {
528
+ for (const part of message.content) if (part.type === "toolCall") pending.add(part.id);
529
+ } else if (message.role === "toolResult") pending.delete(message.toolCallId);
530
+ return pending.size > 0;
531
+ }
532
+ //#endregion
382
533
  //#region src/chat/pi/sql-model-usage.ts
383
534
  /** SQL reporting adapter for usage stored on assistant history events. */
384
535
  const message = sql`${juniorConversationEvents.payload}`;
@@ -474,7 +625,7 @@ async function readConversationModelUsageFromSql(executor, options) {
474
625
  costCacheRead: summedCost(nonnegativeNumber(cost, "cacheRead")),
475
626
  costCacheWrite: summedCost(nonnegativeNumber(cost, "cacheWrite")),
476
627
  costTotal: summedCost(nonnegativeNumber(cost, "total"))
477
- }).from(juniorConversationEvents).innerJoin(juniorConversations, eq(juniorConversations.conversationId, juniorConversationEvents.conversationId)).where(and(options.includeDescendants ? eq(juniorConversations.rootConversationId, options.conversationId) : eq(juniorConversationEvents.conversationId, options.conversationId), eq(juniorConversationEvents.type, "assistant_message"), sql`jsonb_typeof(${message}->'provider') = 'string'`, sql`jsonb_typeof(${message}->'model') = 'string'`, sql`coalesce(${message}->>'provider', '') <> ''`, sql`coalesce(${message}->>'model', '') <> ''`)).groupBy(modelId).orderBy(modelId)).flatMap((row) => {
628
+ }).from(juniorConversationEvents).innerJoin(juniorConversations, eq(juniorConversations.conversationId, juniorConversationEvents.conversationId)).where(and(options.includeDescendants ? eq(juniorConversations.rootConversationId, options.conversationId) : eq(juniorConversationEvents.conversationId, options.conversationId), eq(juniorConversationEvents.type, "assistant_message"), notCopiedByFork(), sql`jsonb_typeof(${message}->'provider') = 'string'`, sql`jsonb_typeof(${message}->'model') = 'string'`, sql`coalesce(${message}->>'provider', '') <> ''`, sql`coalesce(${message}->>'model', '') <> ''`)).groupBy(modelId).orderBy(modelId)).flatMap((row) => {
478
629
  const modelUsage = usageFromRow(row);
479
630
  return modelUsage ? [{
480
631
  modelId: row.modelId,
@@ -794,7 +945,7 @@ async function readConversationAuxiliaryCostsFromSql(db, conversationIds, option
794
945
  events: sql`count(*)::integer`.mapWith(Number),
795
946
  name,
796
947
  namespace
797
- }).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`)).groupBy(conversationId, namespace, name).orderBy(conversationId, namespace, name);
948
+ }).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);
798
949
  const result = /* @__PURE__ */ new Map();
799
950
  for (const row of rows) {
800
951
  const current = result.get(row.conversationId) ?? {
@@ -997,7 +1148,26 @@ function conversationFeedMembershipFilter(status, filter) {
997
1148
  participantMatch: conversationHasParticipantEmail(filter.email)
998
1149
  }), status === "archived" ? conversationArchivedForEmail(filter.email) : status === "all" ? void 0 : conversationNotArchivedForEmail(filter.email));
999
1150
  }
1000
- async function conversationRows(db, limit, status, filter, query, includePrivateBriefs = false) {
1151
+ /**
1152
+ * Keep root conversations linked to one code repository. A code change can
1153
+ * link a child conversation, so linked ids resolve to their root. Private
1154
+ * roots stay visible only to their members.
1155
+ */
1156
+ function conversationCodeRepositoryFilter(repositoryId, viewerUserId) {
1157
+ return and(sql`${juniorConversations.conversationId} IN (
1158
+ SELECT coalesce(linked.root_conversation_id, linked.conversation_id)
1159
+ FROM ${juniorConversations} AS linked
1160
+ WHERE linked.conversation_id IN (
1161
+ SELECT unnest(${juniorCodeChanges.conversationIds})
1162
+ FROM ${juniorCodeChanges}
1163
+ WHERE ${juniorCodeChanges.repositoryId} = ${repositoryId}
1164
+ )
1165
+ )`, or(eq(juniorDestinations.visibility, "public"), viewerUserId ? viewerConversationMembership({
1166
+ actorMatch: eq(juniorIdentities.userId, viewerUserId),
1167
+ participantMatch: conversationHasParticipantUser(viewerUserId)
1168
+ }) : void 0));
1169
+ }
1170
+ async function conversationRows(db, limit, status, filter, query, includePrivateBriefs = false, codeRepository) {
1001
1171
  return db.select({
1002
1172
  conversation: conversationReadColumns(),
1003
1173
  location: juniorDestinations,
@@ -1008,7 +1178,7 @@ async function conversationRows(db, limit, status, filter, query, includePrivate
1008
1178
  identitySubjectId: juniorIdentities.providerSubjectId,
1009
1179
  identityTenantId: juniorIdentities.providerTenantId,
1010
1180
  userDisplayName: juniorUsers.displayName
1011
- }).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), query ? or(sql`strpos(lower(coalesce(${juniorConversations.title}, '')), ${query}) > 0`, sql`exists (
1181
+ }).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 (
1012
1182
  select 1
1013
1183
  from ${juniorConversationBriefs}
1014
1184
  where ${juniorConversationBriefs.conversationId} = ${juniorConversations.conversationId}
@@ -1092,13 +1262,20 @@ function conversationFeedFilter(options) {
1092
1262
  * Build a bounded dashboard feed. Prefer the viewer user when present; otherwise
1093
1263
  * keep only roots linked to actorEmail before applying the limit. Membership is
1094
1264
  * root-actor ownership or a materialized participant row for that person.
1265
+ *
1266
+ * `codeRepositoryId` replaces membership with every root linked to that
1267
+ * repository's code changes. Private roots appear only for their members, and
1268
+ * per-person archive state does not hide rows.
1095
1269
  */
1096
1270
  async function readConversationFeedFromSql(options = {}) {
1097
1271
  const nowMs = Date.now();
1098
1272
  const db = getDb();
1099
- const filter = conversationFeedFilter(options);
1273
+ const filter = options.codeRepositoryId ? void 0 : conversationFeedFilter(options);
1100
1274
  const query = options.q?.trim().toLowerCase() || void 0;
1101
- const rows = await conversationRows(db, options.limit ?? CONVERSATION_FEED_LIMIT, query ? "all" : options.status ?? "active", filter, query, filter?.kind === "viewer");
1275
+ const rows = await conversationRows(db, options.limit ?? CONVERSATION_FEED_LIMIT, query || options.codeRepositoryId ? "all" : options.status ?? "active", filter, query, filter?.kind === "viewer", options.codeRepositoryId ? {
1276
+ id: options.codeRepositoryId,
1277
+ ...options.viewer ? { viewerUserId: options.viewer.id } : void 0
1278
+ } : void 0);
1102
1279
  const conversations = rows.map((row) => conversationFromRow(row));
1103
1280
  const conversationIds = conversations.map((conversation) => conversation.conversationId);
1104
1281
  const [accessByConversation, archiveTimesByConversation, annotationsByConversation, auxiliaryCostsByRoot, metricsByRoot, participantsByConversation, teamDomainByTeamId, conversationWork, lastUserMessageAtByConversation, activityPreviewByConversation] = await Promise.all([
@@ -1166,6 +1343,59 @@ async function readConversationFeed(options = {}) {
1166
1343
  return conversationFeedSchema.parse(await readConversationFeedFromSql(options));
1167
1344
  }
1168
1345
  //#endregion
1346
+ //#region src/api/conversations/fork.ts
1347
+ /** Fork a Conversation that the viewer can read into a new root they own. */
1348
+ async function forkConversationForViewer(viewer, sourceConversationId, body) {
1349
+ if (!(await readConversationAccessFromSql(getDb(), [sourceConversationId], viewer)).get(sourceConversationId)?.canViewPrivateContent) throwApiError(403, "You do not have access to this conversation.");
1350
+ try {
1351
+ return await forkConversation({
1352
+ sourceConversationId,
1353
+ ...body,
1354
+ actorEmail: viewer.email,
1355
+ ...viewer.displayName ? { actorName: viewer.displayName } : void 0
1356
+ });
1357
+ } catch (error) {
1358
+ if (error instanceof ConversationForkError) throwApiError(409, error.message);
1359
+ throw error;
1360
+ }
1361
+ }
1362
+ /**
1363
+ * Read fork state for the viewer: whether they can fork, and the source and
1364
+ * fork links that they can open.
1365
+ */
1366
+ async function readConversationForks(conversationId, viewer) {
1367
+ const db = getDb();
1368
+ const [row] = await db.select({
1369
+ forkedFrom: juniorConversations.forkedFromConversationId,
1370
+ parent: juniorConversations.parentConversationId
1371
+ }).from(juniorConversations).where(eq(juniorConversations.conversationId, conversationId));
1372
+ const forks = await db.select({ conversationId: juniorConversations.conversationId }).from(juniorConversations).where(eq(juniorConversations.forkedFromConversationId, conversationId)).orderBy(asc(juniorConversations.createdAt)).limit(50);
1373
+ const forkedFrom = row?.forkedFrom ?? void 0;
1374
+ const ids = forks.map((fork) => fork.conversationId);
1375
+ const access = await readConversationAccessFromSql(db, [
1376
+ conversationId,
1377
+ ...ids,
1378
+ ...forkedFrom ? [forkedFrom] : []
1379
+ ], viewer);
1380
+ const canOpen = (id) => access.get(id)?.canViewPrivateContent;
1381
+ if (!canOpen(conversationId)) return {
1382
+ canFork: false,
1383
+ forks: []
1384
+ };
1385
+ const result = {
1386
+ canFork: !row?.parent,
1387
+ forks: ids.filter(canOpen)
1388
+ };
1389
+ if (forkedFrom && canOpen(forkedFrom)) {
1390
+ const [source] = await db.select({ title: juniorConversations.title }).from(juniorConversations).where(eq(juniorConversations.conversationId, forkedFrom));
1391
+ if (source) {
1392
+ result.forkedFromConversationId = forkedFrom;
1393
+ if (source.title) result.forkedFromTitle = source.title;
1394
+ }
1395
+ }
1396
+ return result;
1397
+ }
1398
+ //#endregion
1169
1399
  //#region src/api/conversations/detail.ts
1170
1400
  /** Project stored metadata and a bounded event page into a signed history cursor. */
1171
1401
  function projectConversationDetail(args) {
@@ -1264,7 +1494,12 @@ async function readConversationDetail(conversationId, options = {}) {
1264
1494
  ...options,
1265
1495
  limit: options.limit ?? 500
1266
1496
  });
1267
- return report ? conversationDetailReportSchema.parse(report) : void 0;
1497
+ if (!report) return void 0;
1498
+ const forks = await readConversationForks(conversationId, options.viewer);
1499
+ return conversationDetailReportSchema.parse({
1500
+ ...report,
1501
+ ...forks
1502
+ });
1268
1503
  }
1269
1504
  //#endregion
1270
1505
  //#region src/api/conversations/event-list.ts
@@ -1820,10 +2055,11 @@ function createConversationRoutes(options) {
1820
2055
  }
1821
2056
  });
1822
2057
  app.get("/", validateRequest("query", conversationFeedQuerySchema, "Invalid query parameters."), async (context) => {
1823
- const { actorEmail, q, status } = context.req.valid("query");
2058
+ const { actorEmail, codeRepositoryId, q, status } = context.req.valid("query");
1824
2059
  const viewer = context.get("viewer");
1825
2060
  return jsonResponse(conversationFeedSchema, await readConversationFeed({
1826
2061
  ...actorEmail ? { actorEmail } : void 0,
2062
+ ...codeRepositoryId ? { codeRepositoryId } : void 0,
1827
2063
  ...q ? { q } : void 0,
1828
2064
  status,
1829
2065
  ...viewer ? { viewer } : void 0
@@ -1833,14 +2069,15 @@ function createConversationRoutes(options) {
1833
2069
  app.post("/", requireViewer, messageBodyLimit, validateRequest("json", createConversationBodySchema, "Invalid request body."), async (context) => {
1834
2070
  const viewer = context.get("viewer");
1835
2071
  const body = context.req.valid("json");
1836
- return jsonResponse(acceptedConversationMessageSchema, await createConversationForViewer(viewer, body, options.attachmentStorage));
2072
+ return jsonResponse(acceptedConversationMessageSchema, await createConversationForViewer(viewer, body, options));
1837
2073
  });
1838
2074
  app.post("/:conversationId/messages", requireViewer, messageBodyLimit, validateRequest("param", conversationParamsSchema, "Invalid route parameters."), validateRequest("json", createConversationMessageBodySchema, "Invalid request body."), async (context) => {
1839
2075
  const viewer = context.get("viewer");
1840
2076
  const { conversationId } = context.req.valid("param");
1841
2077
  const body = context.req.valid("json");
1842
- return jsonResponse(acceptedConversationMessageSchema, await appendConversationMessageForViewer(viewer, conversationId, body, options.attachmentStorage));
2078
+ return jsonResponse(acceptedConversationMessageSchema, await appendConversationMessageForViewer(viewer, conversationId, body, options));
1843
2079
  });
2080
+ 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"))));
1844
2081
  app.patch("/:conversationId/archive", requireViewer, validateRequest("param", conversationParamsSchema, "Invalid route parameters."), validateRequest("json", archiveConversationBodySchema, "Invalid request body."), async (context) => {
1845
2082
  const viewer = context.get("viewer");
1846
2083
  const { conversationId } = context.req.valid("param");
@@ -1994,17 +2231,18 @@ function ownedByUserSql(conversationIds, userId) {
1994
2231
  WHERE identities.user_id = ${userId}
1995
2232
  )`;
1996
2233
  }
1997
- function ownershipFilter(userId) {
1998
- if (!userId) return void 0;
1999
- return ownedByUserSql(sql`${juniorCodeChanges.conversationIds}`, userId);
2234
+ /** Limit code changes to one person's conversations or one repository. */
2235
+ function changeScopeFilter(args) {
2236
+ return and(args.userId ? ownedByUserSql(sql`${juniorCodeChanges.conversationIds}`, args.userId) : void 0, args.repositoryId ? eq(juniorCodeChanges.repositoryId, args.repositoryId) : void 0);
2000
2237
  }
2238
+ /** Read windowed code summary and activity buckets for an optional scope. */
2001
2239
  async function readCodeWindows(args) {
2002
2240
  const db = getDb();
2003
2241
  const windowEnd = new Date(args.nowMs);
2004
2242
  const windowStart = /* @__PURE__ */ new Date(args.nowMs - WINDOW_DAYS * DAY_MS);
2005
2243
  const activityStart = startOfUtcDay(args.nowMs - 89 * DAY_MS);
2006
2244
  const changes = juniorCodeChanges;
2007
- const ownership = ownershipFilter(args.userId);
2245
+ const scope = changeScopeFilter(args);
2008
2246
  const conversationTreeCost = conversationTreeCostExpr();
2009
2247
  const activityHourEnd = new Date(args.nowMs);
2010
2248
  activityHourEnd.setUTCMinutes(0, 0, 0);
@@ -2020,7 +2258,7 @@ async function readCodeWindows(args) {
2020
2258
  ${changes.state},
2021
2259
  ${changes.conversationIds} AS conversation_ids
2022
2260
  FROM ${changes}
2023
- ${ownership ? sql`WHERE ${ownership}` : sql``}
2261
+ ${scope ? sql`WHERE ${scope}` : sql``}
2024
2262
  ), change_costs AS (
2025
2263
  SELECT
2026
2264
  recent_changes.id,
@@ -2086,18 +2324,18 @@ async function readCodeWindows(args) {
2086
2324
  SELECT ${changes.openedAt} AS event_at, 'created'::text AS kind
2087
2325
  FROM ${changes}
2088
2326
  WHERE ${changes.openedAt} >= ${activityStart}
2089
- ${ownership ? sql`AND ${ownership}` : sql``}
2327
+ ${scope ? sql`AND ${scope}` : sql``}
2090
2328
  UNION ALL
2091
2329
  SELECT ${changes.mergedAt} AS event_at, 'merged'::text AS kind
2092
2330
  FROM ${changes}
2093
2331
  WHERE ${changes.mergedAt} >= ${activityStart}
2094
- ${ownership ? sql`AND ${ownership}` : sql``}
2332
+ ${scope ? sql`AND ${scope}` : sql``}
2095
2333
  UNION ALL
2096
2334
  SELECT ${changes.closedAt} AS event_at, 'closed'::text AS kind
2097
2335
  FROM ${changes}
2098
2336
  WHERE ${changes.state} = 'closed'
2099
2337
  AND ${changes.closedAt} >= ${activityStart}
2100
- ${ownership ? sql`AND ${ownership}` : sql``}
2338
+ ${scope ? sql`AND ${scope}` : sql``}
2101
2339
  ) AS events
2102
2340
  GROUP BY date_trunc('day', event_at AT TIME ZONE 'UTC')
2103
2341
  )
@@ -2127,18 +2365,18 @@ async function readCodeWindows(args) {
2127
2365
  SELECT ${changes.openedAt} AS event_at, 'created'::text AS kind
2128
2366
  FROM ${changes}
2129
2367
  WHERE ${changes.openedAt} >= ${activityHourStart}
2130
- ${ownership ? sql`AND ${ownership}` : sql``}
2368
+ ${scope ? sql`AND ${scope}` : sql``}
2131
2369
  UNION ALL
2132
2370
  SELECT ${changes.mergedAt} AS event_at, 'merged'::text AS kind
2133
2371
  FROM ${changes}
2134
2372
  WHERE ${changes.mergedAt} >= ${activityHourStart}
2135
- ${ownership ? sql`AND ${ownership}` : sql``}
2373
+ ${scope ? sql`AND ${scope}` : sql``}
2136
2374
  UNION ALL
2137
2375
  SELECT ${changes.closedAt} AS event_at, 'closed'::text AS kind
2138
2376
  FROM ${changes}
2139
2377
  WHERE ${changes.state} = 'closed'
2140
2378
  AND ${changes.closedAt} >= ${activityHourStart}
2141
- ${ownership ? sql`AND ${ownership}` : sql``}
2379
+ ${scope ? sql`AND ${scope}` : sql``}
2142
2380
  ) AS events
2143
2381
  GROUP BY date_trunc('hour', event_at AT TIME ZONE 'UTC')
2144
2382
  )
@@ -2183,6 +2421,30 @@ async function readCodeWindows(args) {
2183
2421
  windowStart
2184
2422
  };
2185
2423
  }
2424
+ /** Columns read for one code change report row. */
2425
+ const codeChangeReportColumns = {
2426
+ closedAt: juniorCodeChanges.closedAt,
2427
+ id: juniorCodeChanges.id,
2428
+ mergedAt: juniorCodeChanges.mergedAt,
2429
+ number: juniorCodeChanges.number,
2430
+ openedAt: juniorCodeChanges.openedAt,
2431
+ provider: juniorCodeChanges.provider,
2432
+ repository: juniorCodeRepositories.name,
2433
+ state: juniorCodeChanges.state,
2434
+ title: juniorCodeChanges.title,
2435
+ url: juniorCodeChanges.url
2436
+ };
2437
+ /** Convert one code change row to its report shape. */
2438
+ function codeChangeReport(change) {
2439
+ return {
2440
+ ...change,
2441
+ closedAt: change.closedAt?.toISOString(),
2442
+ mergedAt: change.mergedAt?.toISOString(),
2443
+ openedAt: change.openedAt.toISOString(),
2444
+ title: change.title ?? void 0,
2445
+ url: change.url ?? void 0
2446
+ };
2447
+ }
2186
2448
  /** Read code activity created by Junior across installed code plugins. */
2187
2449
  async function readCodeOverview(nowMs = Date.now()) {
2188
2450
  const db = getDb();
@@ -2248,30 +2510,12 @@ async function readCodeOverview(nowMs = Date.now()) {
2248
2510
  GROUP BY ${repositories.id}
2249
2511
  ORDER BY "merged" DESC, "created" DESC, ${repositories.name} ASC
2250
2512
  LIMIT 25
2251
- `), db.select({
2252
- closedAt: changes.closedAt,
2253
- id: changes.id,
2254
- mergedAt: changes.mergedAt,
2255
- number: changes.number,
2256
- openedAt: changes.openedAt,
2257
- provider: changes.provider,
2258
- repository: repositories.name,
2259
- state: changes.state,
2260
- title: changes.title,
2261
- url: changes.url
2262
- }).from(changes).innerJoin(repositories, eq(changes.repositoryId, repositories.id)).where(or(gte(changes.openedAt, windows.windowStart), gte(changes.mergedAt, windows.windowStart), gte(changes.closedAt, windows.windowStart), eq(changes.state, "open"))).orderBy(desc(changes.updatedAt)).limit(25)]);
2513
+ `), db.select(codeChangeReportColumns).from(changes).innerJoin(repositories, eq(changes.repositoryId, repositories.id)).where(or(gte(changes.openedAt, windows.windowStart), gte(changes.mergedAt, windows.windowStart), gte(changes.closedAt, windows.windowStart), eq(changes.state, "open"))).orderBy(desc(changes.updatedAt)).limit(25)]);
2263
2514
  return codeOverviewReportSchema.parse({
2264
2515
  activityDays: windows.activityDays,
2265
2516
  activityHours: windows.activityHours,
2266
2517
  activitySixHours: windows.activitySixHours,
2267
- changes: recentChanges.map((change) => ({
2268
- ...change,
2269
- closedAt: change.closedAt?.toISOString(),
2270
- mergedAt: change.mergedAt?.toISOString(),
2271
- openedAt: change.openedAt.toISOString(),
2272
- title: change.title ?? void 0,
2273
- url: change.url ?? void 0
2274
- })),
2518
+ changes: recentChanges.map(codeChangeReport),
2275
2519
  generatedAt: windows.windowEnd.toISOString(),
2276
2520
  repositories: z.array(repositoryRowSchema).parse(queryRows(repositoryResult)).map((repository) => ({
2277
2521
  ...repository,
@@ -2303,11 +2547,61 @@ async function readPersonCodeOverview(args) {
2303
2547
  });
2304
2548
  }
2305
2549
  //#endregion
2550
+ //#region src/api/code/repository.ts
2551
+ /** Most recent code changes listed on one repository page. */
2552
+ const REPOSITORY_CHANGE_LIMIT = 100;
2553
+ /**
2554
+ * Read code activity for one repository: windowed summary, activity buckets,
2555
+ * recent code changes, and the Workspaces that include it.
2556
+ */
2557
+ async function readCodeRepository(repositoryId, nowMs = Date.now()) {
2558
+ const db = getDb();
2559
+ const [repository] = await db.select({
2560
+ id: juniorCodeRepositories.id,
2561
+ name: juniorCodeRepositories.name,
2562
+ provider: juniorCodeRepositories.provider,
2563
+ url: juniorCodeRepositories.url
2564
+ }).from(juniorCodeRepositories).where(eq(juniorCodeRepositories.id, repositoryId)).limit(1);
2565
+ if (!repository) return void 0;
2566
+ const [windows, changes, workspaces] = await Promise.all([
2567
+ readCodeWindows({
2568
+ nowMs,
2569
+ repositoryId
2570
+ }),
2571
+ db.select(codeChangeReportColumns).from(juniorCodeChanges).innerJoin(juniorCodeRepositories, eq(juniorCodeChanges.repositoryId, juniorCodeRepositories.id)).where(eq(juniorCodeChanges.repositoryId, repositoryId)).orderBy(desc(juniorCodeChanges.updatedAt)).limit(REPOSITORY_CHANGE_LIMIT),
2572
+ listWorkspacesByRepository(db, {
2573
+ provider: repository.provider,
2574
+ repo: repository.name
2575
+ })
2576
+ ]);
2577
+ return codeRepositoryReportSchema.parse({
2578
+ activityDays: windows.activityDays,
2579
+ activityHours: windows.activityHours,
2580
+ activitySixHours: windows.activitySixHours,
2581
+ changes: changes.map(codeChangeReport),
2582
+ generatedAt: windows.windowEnd.toISOString(),
2583
+ repository: {
2584
+ ...repository,
2585
+ url: repository.url ?? void 0
2586
+ },
2587
+ summary: windows.summary,
2588
+ windowEnd: windows.windowEnd.toISOString(),
2589
+ windowStart: windows.windowStart.toISOString(),
2590
+ workspaces
2591
+ });
2592
+ }
2593
+ //#endregion
2306
2594
  //#region src/api/code/routes.ts
2307
2595
  /** Create the code analytics API. */
2308
2596
  function createCodeRoutes() {
2309
2597
  const app = new Hono();
2310
2598
  app.get("/", async () => jsonResponse(codeOverviewReportSchema, await readCodeOverview()));
2599
+ app.get("/repositories/:repositoryId", validateRequest("param", codeRepositoryParamsSchema, "Invalid route parameters."), async (context) => {
2600
+ const { repositoryId } = context.req.valid("param");
2601
+ const report = await readCodeRepository(repositoryId);
2602
+ if (!report) throwApiError(404, "Repository not found.");
2603
+ return jsonResponse(codeRepositoryReportSchema, report);
2604
+ });
2311
2605
  return app;
2312
2606
  }
2313
2607
  //#endregion
@@ -3060,7 +3354,7 @@ async function readPeoplePluginReports(args) {
3060
3354
  reports: [],
3061
3355
  source: "plugins"
3062
3356
  });
3063
- const { getPluginProfileReports } = await import("./agent-hooks-BcH6E48y.js").then((n) => n.t);
3357
+ const { getPluginProfileReports } = await import("./agent-hooks-Da_H0jHv.js").then((n) => n.t);
3064
3358
  return pluginOperationalReportFeedSchema.parse({
3065
3359
  generatedAt: new Date(nowMs).toISOString(),
3066
3360
  reports: await getPluginProfileReports({
@@ -3509,7 +3803,10 @@ function createUserPageRoutes() {
3509
3803
  }
3510
3804
  //#endregion
3511
3805
  //#region src/chat/automations/edit.ts
3512
- /** Creator-only web edits. Public read access never grants write authority. */
3806
+ /**
3807
+ * Web edits. Owners and public Destination readers can edit. Creator-only
3808
+ * rules still apply, and lifecycle actions stay creator-only.
3809
+ */
3513
3810
  async function requireOwnedAutomation(user, kind, id) {
3514
3811
  if (kind === "scheduled") {
3515
3812
  const task = await readScheduledAutomation(getDb(), id);
@@ -3526,9 +3823,26 @@ async function requireOwnedAutomation(user, kind, id) {
3526
3823
  task
3527
3824
  };
3528
3825
  }
3529
- function editView(automation) {
3826
+ /** Owners and public Destination readers can edit. Others get not found. */
3827
+ async function requireEditableAutomation(user, kind, id) {
3828
+ const candidate = await resolveViewerTaskCandidate(user, kind, id);
3829
+ if (!candidate) throw new ViewerTaskNotFoundError();
3830
+ return candidate;
3831
+ }
3832
+ /** Record the viewer's Slack identity in the Automation workspace as the editor. */
3833
+ function viewerEditor(user, automation) {
3834
+ if (automation.ownedByViewer) return automation.task.createdBy;
3835
+ const identity = user.identities.find((candidate) => candidate.provider === "slack" && candidate.providerTenantId === automation.task.destination.teamId);
3836
+ if (!identity) return void 0;
3837
+ const editor = { slackUserId: identity.providerSubjectId };
3838
+ if (identity.displayName) editor.fullName = identity.displayName;
3839
+ if (identity.handle) editor.userName = identity.handle;
3840
+ return editor;
3841
+ }
3842
+ function editView(automation, ownedByViewer) {
3530
3843
  const { task } = automation;
3531
3844
  const common = {
3845
+ ownedByViewer,
3532
3846
  id: task.id,
3533
3847
  revision: automationRevision(task),
3534
3848
  title: task.title ?? null,
@@ -3559,27 +3873,131 @@ function editView(automation) {
3559
3873
  }
3560
3874
  /** Load exact editable values by ID, independent of list filters. */
3561
3875
  async function readViewerAutomationEdit(user, kind, id) {
3562
- return editView(await requireOwnedAutomation(user, kind, id));
3876
+ const automation = await requireEditableAutomation(user, kind, id);
3877
+ return editView(automation, automation.ownedByViewer);
3563
3878
  }
3564
3879
  /** Save a partial edit against the read revision, without dispatching work. */
3565
3880
  async function updateViewerAutomation(user, id, input) {
3566
- const current = await requireOwnedAutomation(user, input.kind, id);
3881
+ const current = await requireEditableAutomation(user, input.kind, id);
3567
3882
  requireAutomationRevision(current.task, input.revision);
3883
+ return saveViewerEdit(user, current, input);
3884
+ }
3885
+ /** `restored` carries version values that edit input cannot express, such as a cleared title. */
3886
+ async function saveViewerEdit(user, current, input, restored) {
3887
+ const isCreator = current.ownedByViewer;
3888
+ const editedBy = viewerEditor(user, current);
3568
3889
  if (input.kind === "scheduled" && current.kind === "scheduled") {
3569
- const next = await editScheduledAutomation(current.task, input, true, Date.now());
3890
+ const next = await editScheduledAutomation(current.task, input, isCreator, Date.now());
3570
3891
  return editView({
3571
3892
  kind: "scheduled",
3572
- task: await saveScheduledAutomation(getDb(), next, input.revision)
3573
- });
3893
+ task: await saveScheduledAutomation(getDb(), restored ? {
3894
+ ...next,
3895
+ ...restored
3896
+ } : next, input.revision, editedBy)
3897
+ }, isCreator);
3574
3898
  }
3575
3899
  if (input.kind === "event" && current.kind === "event") {
3576
- const next = await editEventAutomation(current.task, input, true, getEventCatalog());
3577
- const task = await saveEventAutomation(getDb(), next, input.revision);
3900
+ const next = await editEventAutomation(current.task, input, isCreator, getEventCatalog());
3901
+ const task = await saveEventAutomation(getDb(), restored ? {
3902
+ ...next,
3903
+ title: restored.title
3904
+ } : next, input.revision, editedBy);
3578
3905
  if (!task) throw new ViewerTaskNotFoundError();
3579
3906
  return editView({
3580
3907
  kind: "event",
3581
3908
  task
3582
- });
3909
+ }, isCreator);
3910
+ }
3911
+ throw new ViewerTaskNotFoundError();
3912
+ }
3913
+ function scheduleIntent(schedule) {
3914
+ const recurrence = schedule.recurrence;
3915
+ if (schedule.kind !== "recurring" || !recurrence) throw new AutomationEditError("A one-time Schedule cannot be restored. Set a new time in the editor.", "schedule");
3916
+ return {
3917
+ kind: "recurring",
3918
+ frequency: recurrence.frequency,
3919
+ interval: recurrence.interval,
3920
+ time: `${String(recurrence.time.hour).padStart(2, "0")}:${String(recurrence.time.minute).padStart(2, "0")}`,
3921
+ weekdays: recurrence.weekdays?.map((day) => weekdaySchema.options[day]),
3922
+ dayOfMonth: recurrence.dayOfMonth,
3923
+ month: recurrence.month,
3924
+ startDate: recurrence.startDate,
3925
+ timezone: schedule.timezone
3926
+ };
3927
+ }
3928
+ /**
3929
+ * Turn saved outcomes back into edit input. Messages to the Automation
3930
+ * Destination or the creator DM are always allowed again. Other stored
3931
+ * Destinations must still be on the Automation.
3932
+ */
3933
+ async function restorableOutcomes(current, outcomes) {
3934
+ const { task } = current;
3935
+ let creatorDm;
3936
+ const restored = [];
3937
+ for (const outcome of outcomes) {
3938
+ if (sameDefinitionValue(outcome.destination, task.destination)) {
3939
+ restored.push({
3940
+ action: outcome.action,
3941
+ destination: "current_conversation"
3942
+ });
3943
+ continue;
3944
+ }
3945
+ if (current.ownedByViewer && !task.outcomes.some((stored) => sameDefinitionValue(stored, outcome))) {
3946
+ creatorDm ??= (await resolveTaskOutcomes([{
3947
+ action: "send_message",
3948
+ destination: "task_creator"
3949
+ }], task.destination, task.createdBy.slackUserId))[0].destination;
3950
+ if (sameDefinitionValue(outcome.destination, creatorDm)) {
3951
+ restored.push({
3952
+ action: outcome.action,
3953
+ destination: "task_creator"
3954
+ });
3955
+ continue;
3956
+ }
3957
+ }
3958
+ restored.push(outcome);
3959
+ }
3960
+ return restored;
3961
+ }
3962
+ /**
3963
+ * Make a saved version active. This saves its definition as a new version
3964
+ * through the same edit rules, so history is never rewritten.
3965
+ */
3966
+ async function activateViewerAutomationVersion(user, kind, id, input) {
3967
+ const current = await requireEditableAutomation(user, kind, id);
3968
+ requireAutomationRevision(current.task, input.revision);
3969
+ const saved = await readAutomationVersion(getDb(), kind, id, input.version);
3970
+ if (!saved) throw new AutomationEditError("This version does not exist.", "version");
3971
+ const { definition } = saved;
3972
+ if (sameDefinitionValue(definition, automationDefinition(current.task))) return editView(current, current.ownedByViewer);
3973
+ if (!sameDefinitionValue(definition.destination, current.task.destination)) throw new AutomationEditError("This version delivers to another Destination. Move the automation from Slack instead.", "destination");
3974
+ const fields = {};
3975
+ if (definition.instruction !== current.task.task.text) fields.instruction = definition.instruction;
3976
+ if (definition.credentialMode !== current.task.credentialMode) fields.credentialMode = definition.credentialMode;
3977
+ if (!sameDefinitionValue(definition.outcomes, current.task.outcomes)) fields.outcomes = await restorableOutcomes(current, definition.outcomes);
3978
+ const title = definition.title ?? void 0;
3979
+ if (saved.kind === "scheduled" && current.kind === "scheduled") {
3980
+ const update = {
3981
+ kind: "scheduled",
3982
+ revision: input.revision,
3983
+ ...fields
3984
+ };
3985
+ const restored = { title };
3986
+ const { schedule } = saved.definition;
3987
+ if (!sameDefinitionValue(schedule, current.task.schedule)) {
3988
+ update.schedule = scheduleIntent(schedule);
3989
+ restored.schedule = schedule;
3990
+ }
3991
+ return saveViewerEdit(user, current, update, restored);
3992
+ }
3993
+ if (saved.kind === "event" && current.kind === "event") {
3994
+ const update = {
3995
+ kind: "event",
3996
+ revision: input.revision,
3997
+ ...fields
3998
+ };
3999
+ if (!sameDefinitionValue(saved.definition.trigger, current.task.trigger)) update.trigger = saved.definition.trigger;
4000
+ return saveViewerEdit(user, current, update, { title });
3583
4001
  }
3584
4002
  throw new ViewerTaskNotFoundError();
3585
4003
  }
@@ -3589,12 +4007,12 @@ async function changeViewerAutomationLifecycle(user, kind, id, input) {
3589
4007
  requireAutomationRevision(current.task, input.revision);
3590
4008
  const status = current.task.status;
3591
4009
  if (status === "completed") throw new AutomationEditError("Completed Automations cannot be restarted.");
3592
- if (input.action === "pause" && status === "paused") return editView(current);
4010
+ if (input.action === "pause" && status === "paused") return editView(current, true);
3593
4011
  if (input.action === "resume" && status !== "paused" && status !== "blocked") throw new AutomationEditError("Only paused or blocked Automations can be resumed.");
3594
4012
  if (current.kind === "event") return editView({
3595
4013
  kind: "event",
3596
4014
  task: await setEventAutomationStatus(getDb(), id, input.action === "pause" ? "paused" : "active", input.revision)
3597
- });
4015
+ }, true);
3598
4016
  const nowMs = Date.now();
3599
4017
  const next = {
3600
4018
  ...current.task,
@@ -3619,7 +4037,7 @@ async function changeViewerAutomationLifecycle(user, kind, id, input) {
3619
4037
  return editView({
3620
4038
  kind: "scheduled",
3621
4039
  task: await saveScheduledAutomation(getDb(), next, input.revision)
3622
- });
4040
+ }, true);
3623
4041
  }
3624
4042
  //#endregion
3625
4043
  //#region src/chat/automations/editor.ts
@@ -3698,6 +4116,26 @@ function createAutomationRoutes() {
3698
4116
  throw error;
3699
4117
  }
3700
4118
  });
4119
+ app.get("/:kind/:id/versions", requireViewer, validateRequest("param", automationParamsSchema, "Invalid Automation."), async (context) => {
4120
+ const { kind, id } = context.req.valid("param");
4121
+ try {
4122
+ return jsonResponse(automationVersionListSchema, await readViewerAutomationVersions(context.get("viewer"), kind, id));
4123
+ } catch (error) {
4124
+ if (error instanceof ViewerTaskNotFoundError) return jsonResponse(apiErrorSchema, { error: error.message }, { status: 404 });
4125
+ throw error;
4126
+ }
4127
+ });
4128
+ app.post("/:kind/:id/versions/:version/activate", requireViewer, validateRequest("param", automationVersionParamsSchema, "Invalid Automation version."), validateRequest("json", automationVersionActivateSchema, "Invalid version activation."), async (context) => {
4129
+ const { kind, id, version } = context.req.valid("param");
4130
+ try {
4131
+ return jsonResponse(automationEditSchema, await activateViewerAutomationVersion(context.get("viewer"), kind, id, {
4132
+ version,
4133
+ revision: context.req.valid("json").revision
4134
+ }));
4135
+ } catch (error) {
4136
+ return editErrorResponse(error);
4137
+ }
4138
+ });
3701
4139
  app.get("/:kind/:id/edit", requireViewer, validateRequest("param", automationParamsSchema, "Invalid Automation."), async (context) => {
3702
4140
  const { kind, id } = context.req.valid("param");
3703
4141
  try {
@@ -3879,7 +4317,7 @@ function readDescriptionText(home) {
3879
4317
  }
3880
4318
  /** Read discovered skill names for authenticated runtime diagnostics. */
3881
4319
  async function readSkillReports() {
3882
- const { discoverSkills } = await import("./skills-WttpPJUy.js").then((n) => n.o);
4320
+ const { discoverSkills } = await import("./skills-Dd8O9Aav.js").then((n) => n.o);
3883
4321
  const skills = await discoverSkills();
3884
4322
  return skillReportsSchema.parse(skills.map((skill) => ({
3885
4323
  name: skill.name,
@@ -3888,7 +4326,7 @@ async function readSkillReports() {
3888
4326
  }
3889
4327
  /** Read safe configured plugin metadata for authenticated runtime diagnostics. */
3890
4328
  async function readPlugins() {
3891
- const { pluginCatalogRuntime } = await import("./catalog-runtime-BSgsPVnr.js").then((n) => n.t);
4329
+ const { pluginCatalogRuntime } = await import("./catalog-runtime-C7339Cfm.js").then((n) => n.t);
3892
4330
  return pluginsSchema.parse(pluginCatalogRuntime.getProviders().map((plugin) => ({
3893
4331
  configKeys: plugin.manifest.configKeys ?? [],
3894
4332
  description: plugin.manifest.description,
@@ -3899,8 +4337,8 @@ async function readPlugins() {
3899
4337
  /** Read authenticated runtime discovery data. */
3900
4338
  async function readRuntimeInfoReport() {
3901
4339
  const [{ homeDir }, { pluginCatalogRuntime }, plugins, skills] = await Promise.all([
3902
- import("./package-discovery-7a9YqLhE.js").then((n) => n.r),
3903
- import("./catalog-runtime-BSgsPVnr.js").then((n) => n.t),
4340
+ import("./package-discovery-BVvSnQBB.js").then((n) => n.r),
4341
+ import("./catalog-runtime-C7339Cfm.js").then((n) => n.t),
3904
4342
  readPlugins(),
3905
4343
  readSkillReports()
3906
4344
  ]);
@@ -3917,7 +4355,7 @@ async function readRuntimeInfoReport() {
3917
4355
  /** Read sanitized operational summaries contributed by plugins. */
3918
4356
  async function readPluginOperationalReportFeed() {
3919
4357
  const nowMs = Date.now();
3920
- const { getPluginOperationalReports } = await import("./agent-hooks-BcH6E48y.js").then((n) => n.t);
4358
+ const { getPluginOperationalReports } = await import("./agent-hooks-Da_H0jHv.js").then((n) => n.t);
3921
4359
  return pluginOperationalReportFeedSchema.parse({
3922
4360
  source: "plugins",
3923
4361
  generatedAt: new Date(nowMs).toISOString(),
@@ -3927,7 +4365,7 @@ async function readPluginOperationalReportFeed() {
3927
4365
  //#endregion
3928
4366
  //#region src/api.ts
3929
4367
  /** Create Junior's production REST API for authenticated dashboard consumers. */
3930
- function createJuniorApi() {
4368
+ function createJuniorApi(options = {}) {
3931
4369
  const app = new Hono();
3932
4370
  app.get("/api/health", async () => jsonResponse(healthReportSchema, readHealthReport()));
3933
4371
  app.get("/api/runtime", async () => jsonResponse(runtimeInfoReportSchema, await readRuntimeInfoReport()));
@@ -3935,7 +4373,10 @@ function createJuniorApi() {
3935
4373
  app.get("/api/skills", async () => jsonResponse(skillReportsSchema, await readSkillReports()));
3936
4374
  app.get("/api/plugin-reports", async () => jsonResponse(pluginOperationalReportFeedSchema, await readPluginOperationalReportFeed()));
3937
4375
  app.get("/api/stats", async () => jsonResponse(statsReportSchema, await readStatsReport()));
3938
- app.route("/api/conversations", createConversationRoutes({ attachmentStorage: createVercelAttachmentStorage() }));
4376
+ app.route("/api/conversations", createConversationRoutes({
4377
+ attachmentStorage: createVercelAttachmentStorage(),
4378
+ queue: options.conversationWorkQueue ?? getVercelConversationWorkQueue()
4379
+ }));
3939
4380
  app.route("/api/code", createCodeRoutes());
3940
4381
  app.route("/api/personal-tokens", createPersonalTokenRoutes());
3941
4382
  app.route("/api/people", createPeopleRoutes());