@sentry/junior 0.235.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-DS7k0ZWY.js → agent-hooks-Da_H0jHv.js} +8 -8
  2. package/dist/{agent-runner-BlDI1zV2.js → agent-runner-uP7QRc4c.js} +353 -117
  3. package/dist/api/schema.d.ts +223 -2
  4. package/dist/api/schema.js +5 -4
  5. package/dist/api.d.ts +8 -2
  6. package/dist/api.js +404 -47
  7. package/dist/{app-pGsR__aT.d.ts → app-MEd7wbF0.d.ts} +82 -48
  8. package/dist/app.d.ts +3 -3
  9. package/dist/app.js +162 -119
  10. package/dist/{types-Du-HbEun.js → automation-B9Kc1gcf.js} +304 -33
  11. package/dist/{briefs-C3jhszOz.js → briefs-DrjBPQFy.js} +4 -4
  12. package/dist/{catalog-runtime-B9u_3-GW.js → catalog-runtime-C7339Cfm.js} +1 -1
  13. package/dist/{checkpoint-DLcJ5mKL.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-BSnNHO0R.js → client-Ch9Hg9NU.js} +2 -2
  24. package/dist/{config-Bmw4xTPK.js → config-JR9wke1W.js} +57 -17
  25. package/dist/{conversation-C_aYrISI.js → conversation-9Fxa1Zbq.js} +13 -1
  26. package/dist/{conversation-privacy-BV2dtba2.js → conversation-privacy-au02dyZK.js} +1 -1
  27. package/dist/{credential-sync-CukzQJ1x.js → credential-sync-BWTA3LnL.js} +14 -14
  28. package/dist/{executor-B9ksWZCv.js → executor-BW9XWgUQ.js} +38 -6
  29. package/dist/{images-CEXcQ0Id.js → images-BdG7zcNf.js} +3 -3
  30. package/dist/instrumentation.js +2 -2
  31. package/dist/{job-queue-CY1L2YRn.js → job-queue-BqXd0ZPV.js} +1 -1
  32. package/dist/{logging-F--48Q7Q.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-LasSG3Qc.js → oauth-authorization-BjRN99dc.js} +1 -1
  36. package/dist/{oauth-callback-server-D07ZzVMs.js → oauth-callback-server-B_1p98Mb.js} +2 -2
  37. package/dist/{pending-auth-DrG9Mkje.js → pending-auth-WqF4Gh8Z.js} +1 -1
  38. package/dist/{plugins-BT2l4We3.d.ts → plugins-CjfUP5gJ.d.ts} +1 -0
  39. package/dist/{profile-DZ2DUdd1.js → profile-BamYnmhO.js} +4 -4
  40. package/dist/{prompt-BM_5mAU3.js → prompt-Ctwy6hSI.js} +1 -1
  41. package/dist/queue-B5SnqRln.d.ts +25 -0
  42. package/dist/{registry-_JLY0x7b.js → registry-CXNv9fDX.js} +38 -2
  43. package/dist/{resolve-Cuoj3MML.js → resolve-C9ETkt4T.js} +2 -2
  44. package/dist/{runner-BfelmbMv.js → runner-DnBGVRnW.js} +9 -9
  45. package/dist/{sandbox-egress-signals-D5QUIO7Z.js → sandbox-egress-signals-DwJ7Boi_.js} +1 -1
  46. package/dist/{schema-CrdDxGbx.js → schema-CJiqDc5r.js} +3 -2
  47. package/dist/{skills-DAegjk5Y.js → skills-Dd8O9Aav.js} +3 -3
  48. package/dist/{spawn-DCfV7zGK.js → spawn-DFdRspSD.js} +2 -2
  49. package/dist/{task-queue-GcuFtzor.js → task-queue-DGQEOk1A.js} +16 -10
  50. package/dist/{task-runner-B0-P_3aq.js → task-runner-C5XCIDSF.js} +9 -9
  51. package/dist/{thread-state-TezcEhuH.js → thread-state-D3E29wZk.js} +5 -5
  52. package/dist/{turn-failure-response-Drhj_-t4.js → turn-failure-response-TDoj0aZz.js} +8 -8
  53. package/dist/{turn-lifecycle-BpIW3zOx.js → turn-lifecycle-DOnU45p7.js} +1 -1
  54. package/dist/{turn-session-routing-Dg8GDgyc.js → turn-session-routing-DbpZZoin.js} +1 -1
  55. package/dist/{validation-DAkYeKDT.js → validation-B9lMbisx.js} +1 -1
  56. package/dist/{version-9CB4Z-J4.js → version-CWkIGcLU.js} +1 -1
  57. package/dist/version.js +1 -1
  58. package/dist/{web-authorization-Cu33voNg.js → web-authorization-PPaoj2ns.js} +6 -6
  59. package/dist/{web-input-B3k0J7WB.js → web-input-CfNxXtHm.js} +86 -34
  60. package/dist/web-message-id-xPI_EFSC.js +239 -0
  61. package/dist/{work-PmkvcY9l.js → work-DNaoFrEe.js} +7 -7
  62. package/dist/{worker-COcaq2J6.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 +4 -4
  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, bt as getEventCatalog, ct as automationRevision, et as eventAutomationBelongsToUser, g as readStats, k as readLiveAttachment, lt as requireAutomationRevision, nt as getEventAutomation, ot as setEventAutomationStatus, st as AutomationConflictError } from "./agent-runner-BlDI1zV2.js";
2
- import { d as logException, p as logWarn } from "./logging-F--48Q7Q.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 { 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, 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, 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-DS7k0ZWY.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-B9ksWZCv.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-C_aYrISI.js";
8
- import { n as hash, t as create } from "./profile-DZ2DUdd1.js";
9
- import { t as getCachedSnapshot } from "./resolve-Cuoj3MML.js";
10
- import { a as getVercelConversationWorkQueue } from "./job-queue-CY1L2YRn.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-B3k0J7WB.js";
14
- import { A as conversationAggregateColumns, j as conversationRangeColumns, k as conversationActiveDaysColumn } from "./task-runner-B0-P_3aq.js";
15
- import { r as getWebAuthorization } from "./web-authorization-Cu33voNg.js";
16
- import { t as decodeInputImages } from "./images-CEXcQ0Id.js";
17
- 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-CrdDxGbx.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) ?? {
@@ -1192,6 +1343,59 @@ async function readConversationFeed(options = {}) {
1192
1343
  return conversationFeedSchema.parse(await readConversationFeedFromSql(options));
1193
1344
  }
1194
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
1195
1399
  //#region src/api/conversations/detail.ts
1196
1400
  /** Project stored metadata and a bounded event page into a signed history cursor. */
1197
1401
  function projectConversationDetail(args) {
@@ -1290,7 +1494,12 @@ async function readConversationDetail(conversationId, options = {}) {
1290
1494
  ...options,
1291
1495
  limit: options.limit ?? 500
1292
1496
  });
1293
- 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
+ });
1294
1503
  }
1295
1504
  //#endregion
1296
1505
  //#region src/api/conversations/event-list.ts
@@ -1860,14 +2069,15 @@ function createConversationRoutes(options) {
1860
2069
  app.post("/", requireViewer, messageBodyLimit, validateRequest("json", createConversationBodySchema, "Invalid request body."), async (context) => {
1861
2070
  const viewer = context.get("viewer");
1862
2071
  const body = context.req.valid("json");
1863
- return jsonResponse(acceptedConversationMessageSchema, await createConversationForViewer(viewer, body, options.attachmentStorage));
2072
+ return jsonResponse(acceptedConversationMessageSchema, await createConversationForViewer(viewer, body, options));
1864
2073
  });
1865
2074
  app.post("/:conversationId/messages", requireViewer, messageBodyLimit, validateRequest("param", conversationParamsSchema, "Invalid route parameters."), validateRequest("json", createConversationMessageBodySchema, "Invalid request body."), async (context) => {
1866
2075
  const viewer = context.get("viewer");
1867
2076
  const { conversationId } = context.req.valid("param");
1868
2077
  const body = context.req.valid("json");
1869
- return jsonResponse(acceptedConversationMessageSchema, await appendConversationMessageForViewer(viewer, conversationId, body, options.attachmentStorage));
2078
+ return jsonResponse(acceptedConversationMessageSchema, await appendConversationMessageForViewer(viewer, conversationId, body, options));
1870
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"))));
1871
2081
  app.patch("/:conversationId/archive", requireViewer, validateRequest("param", conversationParamsSchema, "Invalid route parameters."), validateRequest("json", archiveConversationBodySchema, "Invalid request body."), async (context) => {
1872
2082
  const viewer = context.get("viewer");
1873
2083
  const { conversationId } = context.req.valid("param");
@@ -3144,7 +3354,7 @@ async function readPeoplePluginReports(args) {
3144
3354
  reports: [],
3145
3355
  source: "plugins"
3146
3356
  });
3147
- const { getPluginProfileReports } = await import("./agent-hooks-DS7k0ZWY.js").then((n) => n.t);
3357
+ const { getPluginProfileReports } = await import("./agent-hooks-Da_H0jHv.js").then((n) => n.t);
3148
3358
  return pluginOperationalReportFeedSchema.parse({
3149
3359
  generatedAt: new Date(nowMs).toISOString(),
3150
3360
  reports: await getPluginProfileReports({
@@ -3593,7 +3803,10 @@ function createUserPageRoutes() {
3593
3803
  }
3594
3804
  //#endregion
3595
3805
  //#region src/chat/automations/edit.ts
3596
- /** 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
+ */
3597
3810
  async function requireOwnedAutomation(user, kind, id) {
3598
3811
  if (kind === "scheduled") {
3599
3812
  const task = await readScheduledAutomation(getDb(), id);
@@ -3610,9 +3823,26 @@ async function requireOwnedAutomation(user, kind, id) {
3610
3823
  task
3611
3824
  };
3612
3825
  }
3613
- 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) {
3614
3843
  const { task } = automation;
3615
3844
  const common = {
3845
+ ownedByViewer,
3616
3846
  id: task.id,
3617
3847
  revision: automationRevision(task),
3618
3848
  title: task.title ?? null,
@@ -3643,27 +3873,131 @@ function editView(automation) {
3643
3873
  }
3644
3874
  /** Load exact editable values by ID, independent of list filters. */
3645
3875
  async function readViewerAutomationEdit(user, kind, id) {
3646
- return editView(await requireOwnedAutomation(user, kind, id));
3876
+ const automation = await requireEditableAutomation(user, kind, id);
3877
+ return editView(automation, automation.ownedByViewer);
3647
3878
  }
3648
3879
  /** Save a partial edit against the read revision, without dispatching work. */
3649
3880
  async function updateViewerAutomation(user, id, input) {
3650
- const current = await requireOwnedAutomation(user, input.kind, id);
3881
+ const current = await requireEditableAutomation(user, input.kind, id);
3651
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);
3652
3889
  if (input.kind === "scheduled" && current.kind === "scheduled") {
3653
- const next = await editScheduledAutomation(current.task, input, true, Date.now());
3890
+ const next = await editScheduledAutomation(current.task, input, isCreator, Date.now());
3654
3891
  return editView({
3655
3892
  kind: "scheduled",
3656
- task: await saveScheduledAutomation(getDb(), next, input.revision)
3657
- });
3893
+ task: await saveScheduledAutomation(getDb(), restored ? {
3894
+ ...next,
3895
+ ...restored
3896
+ } : next, input.revision, editedBy)
3897
+ }, isCreator);
3658
3898
  }
3659
3899
  if (input.kind === "event" && current.kind === "event") {
3660
- const next = await editEventAutomation(current.task, input, true, getEventCatalog());
3661
- 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);
3662
3905
  if (!task) throw new ViewerTaskNotFoundError();
3663
3906
  return editView({
3664
3907
  kind: "event",
3665
3908
  task
3666
- });
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 });
3667
4001
  }
3668
4002
  throw new ViewerTaskNotFoundError();
3669
4003
  }
@@ -3673,12 +4007,12 @@ async function changeViewerAutomationLifecycle(user, kind, id, input) {
3673
4007
  requireAutomationRevision(current.task, input.revision);
3674
4008
  const status = current.task.status;
3675
4009
  if (status === "completed") throw new AutomationEditError("Completed Automations cannot be restarted.");
3676
- if (input.action === "pause" && status === "paused") return editView(current);
4010
+ if (input.action === "pause" && status === "paused") return editView(current, true);
3677
4011
  if (input.action === "resume" && status !== "paused" && status !== "blocked") throw new AutomationEditError("Only paused or blocked Automations can be resumed.");
3678
4012
  if (current.kind === "event") return editView({
3679
4013
  kind: "event",
3680
4014
  task: await setEventAutomationStatus(getDb(), id, input.action === "pause" ? "paused" : "active", input.revision)
3681
- });
4015
+ }, true);
3682
4016
  const nowMs = Date.now();
3683
4017
  const next = {
3684
4018
  ...current.task,
@@ -3703,7 +4037,7 @@ async function changeViewerAutomationLifecycle(user, kind, id, input) {
3703
4037
  return editView({
3704
4038
  kind: "scheduled",
3705
4039
  task: await saveScheduledAutomation(getDb(), next, input.revision)
3706
- });
4040
+ }, true);
3707
4041
  }
3708
4042
  //#endregion
3709
4043
  //#region src/chat/automations/editor.ts
@@ -3782,6 +4116,26 @@ function createAutomationRoutes() {
3782
4116
  throw error;
3783
4117
  }
3784
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
+ });
3785
4139
  app.get("/:kind/:id/edit", requireViewer, validateRequest("param", automationParamsSchema, "Invalid Automation."), async (context) => {
3786
4140
  const { kind, id } = context.req.valid("param");
3787
4141
  try {
@@ -3963,7 +4317,7 @@ function readDescriptionText(home) {
3963
4317
  }
3964
4318
  /** Read discovered skill names for authenticated runtime diagnostics. */
3965
4319
  async function readSkillReports() {
3966
- const { discoverSkills } = await import("./skills-DAegjk5Y.js").then((n) => n.o);
4320
+ const { discoverSkills } = await import("./skills-Dd8O9Aav.js").then((n) => n.o);
3967
4321
  const skills = await discoverSkills();
3968
4322
  return skillReportsSchema.parse(skills.map((skill) => ({
3969
4323
  name: skill.name,
@@ -3972,7 +4326,7 @@ async function readSkillReports() {
3972
4326
  }
3973
4327
  /** Read safe configured plugin metadata for authenticated runtime diagnostics. */
3974
4328
  async function readPlugins() {
3975
- const { pluginCatalogRuntime } = await import("./catalog-runtime-B9u_3-GW.js").then((n) => n.t);
4329
+ const { pluginCatalogRuntime } = await import("./catalog-runtime-C7339Cfm.js").then((n) => n.t);
3976
4330
  return pluginsSchema.parse(pluginCatalogRuntime.getProviders().map((plugin) => ({
3977
4331
  configKeys: plugin.manifest.configKeys ?? [],
3978
4332
  description: plugin.manifest.description,
@@ -3983,8 +4337,8 @@ async function readPlugins() {
3983
4337
  /** Read authenticated runtime discovery data. */
3984
4338
  async function readRuntimeInfoReport() {
3985
4339
  const [{ homeDir }, { pluginCatalogRuntime }, plugins, skills] = await Promise.all([
3986
- import("./package-discovery-7a9YqLhE.js").then((n) => n.r),
3987
- import("./catalog-runtime-B9u_3-GW.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),
3988
4342
  readPlugins(),
3989
4343
  readSkillReports()
3990
4344
  ]);
@@ -4001,7 +4355,7 @@ async function readRuntimeInfoReport() {
4001
4355
  /** Read sanitized operational summaries contributed by plugins. */
4002
4356
  async function readPluginOperationalReportFeed() {
4003
4357
  const nowMs = Date.now();
4004
- const { getPluginOperationalReports } = await import("./agent-hooks-DS7k0ZWY.js").then((n) => n.t);
4358
+ const { getPluginOperationalReports } = await import("./agent-hooks-Da_H0jHv.js").then((n) => n.t);
4005
4359
  return pluginOperationalReportFeedSchema.parse({
4006
4360
  source: "plugins",
4007
4361
  generatedAt: new Date(nowMs).toISOString(),
@@ -4011,7 +4365,7 @@ async function readPluginOperationalReportFeed() {
4011
4365
  //#endregion
4012
4366
  //#region src/api.ts
4013
4367
  /** Create Junior's production REST API for authenticated dashboard consumers. */
4014
- function createJuniorApi() {
4368
+ function createJuniorApi(options = {}) {
4015
4369
  const app = new Hono();
4016
4370
  app.get("/api/health", async () => jsonResponse(healthReportSchema, readHealthReport()));
4017
4371
  app.get("/api/runtime", async () => jsonResponse(runtimeInfoReportSchema, await readRuntimeInfoReport()));
@@ -4019,7 +4373,10 @@ function createJuniorApi() {
4019
4373
  app.get("/api/skills", async () => jsonResponse(skillReportsSchema, await readSkillReports()));
4020
4374
  app.get("/api/plugin-reports", async () => jsonResponse(pluginOperationalReportFeedSchema, await readPluginOperationalReportFeed()));
4021
4375
  app.get("/api/stats", async () => jsonResponse(statsReportSchema, await readStatsReport()));
4022
- app.route("/api/conversations", createConversationRoutes({ attachmentStorage: createVercelAttachmentStorage() }));
4376
+ app.route("/api/conversations", createConversationRoutes({
4377
+ attachmentStorage: createVercelAttachmentStorage(),
4378
+ queue: options.conversationWorkQueue ?? getVercelConversationWorkQueue()
4379
+ }));
4023
4380
  app.route("/api/code", createCodeRoutes());
4024
4381
  app.route("/api/personal-tokens", createPersonalTokenRoutes());
4025
4382
  app.route("/api/people", createPeopleRoutes());