@sentry/junior 0.268.0 → 0.270.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 (83) hide show
  1. package/README.md +2 -9
  2. package/dist/{adapter-04AcD2SO.js → adapter-BQNTKVTF.js} +3 -49
  3. package/dist/{admin-DIU1QyG3.js → admin-CKVEoXa4.js} +1 -1
  4. package/dist/{agent-runner-CAYfFmU7.js → agent-runner-MSQSMFkq.js} +4538 -3251
  5. package/dist/api/schema.d.ts +2 -3
  6. package/dist/api/schema.js +7 -7
  7. package/dist/api.d.ts +3 -1
  8. package/dist/api.js +190 -301
  9. package/dist/{app-z6Uuyq3j.d.ts → app-Bz8t4P8p.d.ts} +9 -3
  10. package/dist/app.d.ts +1 -1
  11. package/dist/app.js +129 -209
  12. package/dist/{automation-CmeFBSEh.js → automation-Dwh_xcqM.js} +74 -74
  13. package/dist/{briefs-DQbV2jNr.js → briefs-DNFzFFVx.js} +5 -5
  14. package/dist/{catalog-runtime-BD9GSpc6.js → catalog-runtime-Bdym_wKy.js} +1 -1
  15. package/dist/cli/chat.js +16 -26
  16. package/dist/cli/check.js +3 -3
  17. package/dist/cli/main.js +5 -3
  18. package/dist/cli/plugins.js +4 -4
  19. package/dist/cli/snapshot-create.js +4 -4
  20. package/dist/cli/upgrade.d.ts +1 -1
  21. package/dist/cli/upgrade.js +3 -3
  22. package/dist/{client-C1yDYjoS.js → client-B2MjENki.js} +31 -16
  23. package/dist/{config-DrTeF80T.js → config-Dm3pWRqY.js} +3 -3
  24. package/dist/{connections-D46f8vvi.js → connections-0BBhiD-p.js} +10 -10
  25. package/dist/{conversation-DSo310Ps.js → conversation-AhOVzN_k.js} +11 -86
  26. package/dist/{conversation-privacy-CxlOCiUo.js → conversation-privacy-C1pJ89rs.js} +1 -1
  27. package/dist/{credential-sync-sm9fegLY.js → credential-sync-BRiWYs6_.js} +15 -14
  28. package/dist/{credentialed-lukuD-cc.js → credentialed-BeBaJMZ_.js} +565 -83
  29. package/dist/{store-xMuuXRE0.d.ts → db-BYOuu6v2.d.ts} +4 -0
  30. package/dist/{executor-4vhvsMZJ.js → executor-BB6eJkD4.js} +6 -6
  31. package/dist/instrumentation.js +2 -2
  32. package/dist/{interruption-marker-CkncDT8d.js → interruption-marker-CdblrIQK.js} +1 -1
  33. package/dist/{job-queue-In9QKz-I.js → job-queue-DiaXoSDg.js} +2 -2
  34. package/dist/{list-BkgnXEEB.js → list-BTDRxkOg.js} +331 -662
  35. package/dist/locks-D-0h111u.js +48 -0
  36. package/dist/{logging-DLSpkSSr.js → logging-C6EdT6PP.js} +1 -1
  37. package/dist/{history-C84mlAwO.js → media-U6Tuz6Mi.js} +84 -2
  38. package/dist/{message-BObyg59s.js → message-DsjiWRVg.js} +4 -4
  39. package/dist/{model-profile-Bg8EoCKD.js → model-profile-B998sjVd.js} +9 -2
  40. package/dist/nitro.d.ts +1 -1
  41. package/dist/nitro.js +5 -3
  42. package/dist/{oauth-callback-server-B-WbSdoH.js → oauth-callback-server-CjFFVGqa.js} +2 -2
  43. package/dist/{prompt-BW9jFsqZ.js → prompt-BPSdXYXV.js} +1 -1
  44. package/dist/{registry-BgecKva0.js → registry-CndmkP2P.js} +1 -1
  45. package/dist/reporting-schema-CZREhYc-.js +104 -0
  46. package/dist/{web-message-id-DlHNANtd.js → reporting-window-DRCi1Ls_.js} +139 -7
  47. package/dist/{resolve-Cpa4Bu1O.js → resolve-BkAA1wH7.js} +3 -3
  48. package/dist/{result-D3TSmZZ8.js → result-9TJTpuYP.js} +30 -11
  49. package/dist/{runner-DS5OKhEp.js → runner-BmKLSyJm.js} +12 -11
  50. package/dist/{sandbox-egress-signals-CnDjY5rx.js → sandbox-egress-signals-CjLaogMo.js} +1 -1
  51. package/dist/{schema-0050HFnB.js → schema-DNLJ_MzW.js} +36 -20
  52. package/dist/{schema-ZngqcOWM.d.ts → schema-WayPSHFu.d.ts} +149 -12
  53. package/dist/{service-account-CKV2WTY7.js → service-account-D6e9cVmH.js} +4 -3
  54. package/dist/{service-account-DGjnDw9y.js → service-account-DlhYl-Mu.js} +3 -3
  55. package/dist/{session-stop-Dam1EI7B.js → session-stop-MZRgYqYH.js} +24 -16
  56. package/dist/{skills-CfAnfImD.js → skills-BggTR_ue.js} +2 -2
  57. package/dist/{spawn-cmq9p4Ly.js → spawn-DmtlcAT_.js} +2 -2
  58. package/dist/{store-zPEoJPBB.js → store-BeWGgaMA.js} +3 -3
  59. package/dist/{store-C5PN02QO.js → store-Byarin7b.js} +4 -4
  60. package/dist/{task-queue-_mf28dSu.js → task-queue-Dt1_ZoI_.js} +3 -3
  61. package/dist/task-runner-DyKqAPpm.js +269 -0
  62. package/dist/{thread-state-BbbPh0Sn.js → thread-state-BHBLMvgb.js} +5 -5
  63. package/dist/{turn-failure-response-BKKmnMB5.js → turn-failure-response-DWXy_0oJ.js} +9 -9
  64. package/dist/{turn-lifecycle-oP0kVeGz.js → turn-lifecycle-AiXkVqCB.js} +1 -1
  65. package/dist/{tasks-SB9z-VTU.js → turn-session-routing-B9KLBhaT.js} +939 -120
  66. package/dist/{validation--JsAGLwc.js → validation-BW60Vtx_.js} +1 -1
  67. package/dist/{version-jHwJp4hC.js → version-CivPYmkH.js} +1 -1
  68. package/dist/version.js +1 -1
  69. package/dist/{viewer-CpXTGlep.js → viewer-BTgAfdUg.js} +216 -27
  70. package/dist/{work-3N-GHY9m.js → work-Bjm52fHo.js} +17 -36
  71. package/dist/{worker-CCdbcItd.js → worker-CMbgtHLu.js} +15 -7
  72. package/migrations/0069_system_identity.sql +25 -0
  73. package/migrations/0070_review_item_foreign_keys.sql +5 -0
  74. package/migrations/AGENTS.md +17 -0
  75. package/migrations/meta/0069_snapshot.json +3979 -0
  76. package/migrations/meta/0070_snapshot.json +3997 -0
  77. package/migrations/meta/_journal.json +15 -1
  78. package/package.json +5 -4
  79. package/dist/db-BU4LQD8s.d.ts +0 -6
  80. package/dist/history-BgI33Kie.d.ts +0 -78
  81. package/dist/reporting-window-DVCD_10C.js +0 -227
  82. package/dist/task-runner-DMI2prDO.js +0 -1073
  83. package/migrations/README.md +0 -30
@@ -1,22 +1,23 @@
1
- import { a as completeText, f as unwrapCurrentInstruction, p as escapeXml, s as resolveGatewayModel, y as isProviderRetryError } from "./client-C1yDYjoS.js";
2
- import { i as getChatConfig, n as botConfig } from "./config-DrTeF80T.js";
3
- import { B as isRecord, H as toOptionalString, V as toOptionalNumber, b as setSpanAttributes, h as logWarn, o as getActiveTraceContext, p as logException, t as buildTurnFailureResponse } from "./logging-DLSpkSSr.js";
1
+ import { a as completeText, f as unwrapCurrentInstruction, p as escapeXml, s as resolveGatewayModel, y as isProviderRetryError } from "./client-B2MjENki.js";
2
+ import { i as getChatConfig, n as botConfig } from "./config-Dm3pWRqY.js";
3
+ import { B as isRecord, E as withSpan, H as toOptionalString, V as toOptionalNumber, _ as runBestEffort, b as setSpanAttributes, h as logWarn, o as getActiveTraceContext, p as logException, t as buildTurnFailureResponse } from "./logging-C6EdT6PP.js";
4
4
  import { c as parseStoredSlackActor, l as toStoredSlackActor, m as parseSlackMessageTs } from "./actor-CP9K2Idu.js";
5
- import { B as parseDestination, H as sameDestination, N as addAgentTurnUsage, S as startLocationThread, _ as withConversationEventLock, d as getConversationEventStore, g as createSqlConversationEventStore, h as getSqlExecutor, j as bindProviderConversation, m as getDb, p as getConversationStore } from "./viewer-CpXTGlep.js";
6
- import { d as juniorDestinations } from "./agent-invocations-C1RNOCs8.js";
7
- import { d as juniorAutomations, l as juniorAutomationRuns, p as juniorLocationConfigurations, u as juniorAutomationVersions } from "./executor-4vhvsMZJ.js";
8
- import { d as contextProvenance, f as instructionActors, n as agentHistoryItemSchema, p as instructionProvenanceFor } from "./history-C84mlAwO.js";
9
- import { f as credentialContextForActor, h as credentialSubjectSchema } from "./registry-BgecKva0.js";
10
- import { n as pluginCatalogRuntime } from "./catalog-runtime-BD9GSpc6.js";
11
- import { H as startOAuthFlow, In as createPluginLogger, bt as parseSandboxEgressAuthRequiredSignal, cr as createPluginCache, dr as agentsInstructionsUpdatedEvent, fr as authenticationLinkedEvent, p as getPlugins, pn as JUNIOR_THREAD_STATE_TTL_MS, pr as authenticationUnlinkedEvent, t as executeCredentialedEgressRequest, un as sleep$1, ur as JUNIOR_NATIVE_EVENT_NAMESPACE, wt as resolveSandboxEgressProviderForHost, yn as ToolInputError, z as formatProviderLabel } from "./credentialed-lukuD-cc.js";
12
- import { a as MUTATION_LOCK_TTL_MS, c as withLock, i as getStateAdapter, r as getDefaultRedisStateAdapterFor, s as fenceLock } from "./adapter-04AcD2SO.js";
13
- import { L as messageCardRef } from "./conversation-DSo310Ps.js";
14
- import { A as eventAutomationSchema, E as scheduledAutomationSchema, S as automationVersionSchema } from "./automation-CmeFBSEh.js";
5
+ import { A as startLocationThread, C as getSqlExecutor, G as agentTurnTotalTokens, M as resolveActorIdentityId, Q as sameDestination, S as getDb, T as withConversationEventLock, U as addAgentTurnUsage, V as bindProviderConversation, W as agentTurnCostUsd, X as parseDestination, w as createSqlConversationEventStore, x as getConversationStore, y as getConversationEventStore } from "./viewer-BTgAfdUg.js";
6
+ import { d as juniorDestinations, s as juniorConversations } from "./agent-invocations-C1RNOCs8.js";
7
+ import { d as juniorAutomations, l as juniorAutomationRuns, p as juniorLocationConfigurations, u as juniorAutomationVersions } from "./executor-BB6eJkD4.js";
8
+ import { a as agentHistoryItemSchema, g as instructionProvenanceFor, h as instructionActors, m as contextProvenance } from "./media-U6Tuz6Mi.js";
9
+ import { f as credentialContextForActor, h as credentialSubjectSchema } from "./registry-CndmkP2P.js";
10
+ import { n as pluginCatalogRuntime } from "./catalog-runtime-Bdym_wKy.js";
11
+ import { Bn as getDashboardTaskLink, Ct as openSlackDirectMessage, Et as postSlackMessage, Fr as authenticationUnlinkedEvent, Gt as parseSandboxEgressAuthRequiredSignal, Hn as ToolInputError, In as JUNIOR_THREAD_STATE_TTL_MS, Mr as JUNIOR_NATIVE_EVENT_NAMESPACE, Nn as sleep$1, Nr as agentsInstructionsUpdatedEvent, Pr as authenticationLinkedEvent, Yt as resolveSandboxEgressProviderForHost, ar as createPluginLogger, ht as formatSlackLink, jr as createPluginCache, mt as escapeSlackMrkdwnText, p as getPlugins, pt as startOAuthFlow, t as executeCredentialedEgressRequest, ut as formatProviderLabel } from "./credentialed-BeBaJMZ_.js";
12
+ import { a as withLock, i as fenceLock, n as MUTATION_LOCK_TTL_MS } from "./locks-D-0h111u.js";
13
+ import { n as getDefaultRedisStateAdapterFor, r as getStateAdapter } from "./adapter-BQNTKVTF.js";
14
+ import { L as messageCardRef } from "./conversation-AhOVzN_k.js";
15
+ import { O as eventAutomationSchema, S as automationVersionSchema, j as scheduledAutomationSchema } from "./automation-Dwh_xcqM.js";
15
16
  import { isDeepStrictEqual } from "node:util";
16
17
  import { z } from "zod";
17
18
  import { createHash, randomUUID } from "node:crypto";
18
- import { and, asc, desc, eq, inArray, isNull, ne, or, sql } from "drizzle-orm";
19
- import { actorSchema, anyResourceMatchFields, describeEventTrigger, destinationSchema, eventMatchSchema, eventMatches, eventTriggerChoices, eventTriggerProblems, normalizeEventIdentifier, normalizeEventMatch, sourceSchema } from "@sentry/junior-plugin-api";
19
+ import { and, asc, count, desc, eq, gte, inArray, isNotNull, isNull, lte, max, ne, or, sql } from "drizzle-orm";
20
+ import { actorSchema, anyResourceMatchFields, describeEventTrigger, destinationSchema, destinationVisibilitySchema, eventMatchSchema, eventMatches, eventTriggerChoices, eventTriggerProblems, isSlackDestination, normalizeEventIdentifier, normalizeEventMatch, replyAttributionSchema, sourceSchema, taskOutcomeSchema } from "@sentry/junior-plugin-api";
20
21
  // src/chat/configuration/defaults.ts
21
22
  let installDefaults = {};
22
23
  function cloneDefaults(defaults) {
@@ -1047,6 +1048,12 @@ async function persistWithRetry(persist) {
1047
1048
  }
1048
1049
  throw lastError;
1049
1050
  }
1051
+ // src/chat/codemode/result.ts
1052
+ /**
1053
+ * Name of the tool that runs a Code Mode script. Nested calls use it as
1054
+ * their dispatcher name.
1055
+ */
1056
+ const CODEMODE_TOOL_NAME = "codemode";
1050
1057
  // src/chat/services/turn-limit.ts
1051
1058
  /** Terminal failure when one turn uses too many execution slices. */
1052
1059
  var TurnSliceLimitExceededError = class extends Error {
@@ -1078,11 +1085,12 @@ function isTurnExecutionLimitExceededError(error) {
1078
1085
  return error instanceof Error && isDirectTurnExecutionLimitError(error.cause);
1079
1086
  }
1080
1087
  /**
1081
- * Why tools are disabled once a turn is out of budget. Junior blocks the call
1082
- * with the same message as a tool that `disableTools` disabled. The tool list
1083
- * stays the same, so the provider cache stays valid.
1088
+ * The result of a tool call that Junior blocks once a turn is out of budget,
1089
+ * as a direct call or as a call in a script. It names no single tool: the
1090
+ * model can reach a tool directly or through a script, so the message closes
1091
+ * both paths. The tool list stays the same, so the provider cache stays valid.
1084
1092
  */
1085
- const TURN_LIMIT_DISABLED_REASON = "This Turn used its execution budget, so all tools are disabled. Reply now with what you finished, what is left, and how to continue.";
1093
+ const TURN_LIMIT_WRAP_UP_MESSAGE = `This Turn used its execution budget, so all tools are disabled, also in ${CODEMODE_TOOL_NAME} scripts. Do not call a tool again in this Turn. Reply now with what you finished, what is left, and how to continue.`;
1086
1094
  /**
1087
1095
  * Check tool calls against the turn limits for one slice.
1088
1096
  *
@@ -1163,12 +1171,7 @@ const inboundMessageSourceSchema = z.enum([
1163
1171
  const agentInputSchema = z.object({
1164
1172
  attachments: z.array(z.unknown()).optional(),
1165
1173
  authorId: z.string().min(1).optional(),
1166
- /**
1167
- * Identity of the input author, resolved at ingress.
1168
- *
1169
- * TODO(dcramer): Set this at every ingress after every deployed worker
1170
- * reads it. This schema is strict, so an older worker rejects the entry.
1171
- */
1174
+ /** Identity of the input author, resolved at ingress. */
1172
1175
  authorIdentityId: z.string().min(1).optional(),
1173
1176
  metadata: z.record(z.string(), z.unknown()).optional(),
1174
1177
  text: z.string()
@@ -2699,8 +2702,10 @@ async function upsertTurnRecordLocked(args, fence) {
2699
2702
  const dispatchId = args.dispatchId ?? existingRecord?.dispatchId;
2700
2703
  const hasProviderLocation = Boolean(conversation?.location) || args.destination?.platform === "slack";
2701
2704
  const publishExternally = dispatchId || turnSource ? hasProviderLocation && (Boolean(dispatchId) || turnSource?.kind === "slack" || turnSource?.kind === "event") : existingRecord?.publishExternally ?? false;
2705
+ const turnActor = existingRecord?.actor ?? args.actor;
2706
+ const actorIdentityId = existingRecord?.actorIdentityId ?? (turnActor ? await resolveActorIdentityId(getSqlExecutor(), turnActor) : void 0);
2702
2707
  return await setStoredRecord({
2703
- actor: existingRecord?.actor ?? args.actor,
2708
+ actor: turnActor,
2704
2709
  fence,
2705
2710
  conversationStore: args.conversationStore,
2706
2711
  destination: args.destination,
@@ -2717,7 +2722,7 @@ async function upsertTurnRecordLocked(args, fence) {
2717
2722
  ...definedProps$1({ indexTtlMs: args.ttlMs }),
2718
2723
  turnStartMessageIndex,
2719
2724
  record: buildStoredRecord({
2720
- actor: existingRecord?.actor ?? args.actor,
2725
+ actor: turnActor,
2721
2726
  conversationId: args.conversationId,
2722
2727
  turnId: args.turnId,
2723
2728
  sliceId: args.sliceId,
@@ -2726,7 +2731,7 @@ async function upsertTurnRecordLocked(args, fence) {
2726
2731
  historyVersion: commit.historyVersion,
2727
2732
  previousVersion: existingRecord?.version,
2728
2733
  ...definedProps$1({
2729
- actorIdentityId: existingRecord?.actorIdentityId,
2734
+ actorIdentityId,
2730
2735
  credentialSubject: existingRecord?.credentialSubject ?? args.credentialSubject,
2731
2736
  cumulativeToolCallCount: args.cumulativeToolCallCount ?? existingRecord?.cumulativeToolCallCount,
2732
2737
  turnLimitWrappedUp: args.turnLimitWrappedUp ?? existingRecord?.turnLimitWrappedUp,
@@ -2894,11 +2899,14 @@ async function loadTurnCheckpoint(args) {
2894
2899
  };
2895
2900
  }
2896
2901
  async function saveTurnCheckpoint(args) {
2897
- if (args.mode === "running") return await saveRunning(args);
2898
- if (args.mode === "paused") return await savePaused(args);
2899
- await saveDone(args);
2900
- }
2901
- function sharedWrite(args, latest) {
2902
+ const traceContext = getActiveTraceContext();
2903
+ return await withSpan("turn.checkpoint.save", "turn.checkpoint.save", {}, async () => {
2904
+ if (args.mode === "running") return await saveRunning(args, traceContext);
2905
+ if (args.mode === "paused") return await savePaused(args, traceContext);
2906
+ await saveDone(args, traceContext);
2907
+ }, { "app.turn.checkpoint.mode": args.mode });
2908
+ }
2909
+ function sharedWrite(args, traceContext, latest) {
2902
2910
  return {
2903
2911
  conversationId: args.conversationId,
2904
2912
  turnId: args.turnId,
@@ -2911,7 +2919,7 @@ function sharedWrite(args, latest) {
2911
2919
  dispatchId: args.dispatchId ?? latest?.dispatchId,
2912
2920
  source: args.source,
2913
2921
  surface: args.surface ?? latest?.surface,
2914
- traceContext: latest?.traceContext ?? getActiveTraceContext(),
2922
+ traceContext: latest?.traceContext ?? traceContext,
2915
2923
  turnStartMessageIndex: args.turnStartMessageIndex ?? latest?.turnStartMessageIndex,
2916
2924
  cumulativeToolCallCount: args.cumulativeToolCallCount ?? latest?.cumulativeToolCallCount,
2917
2925
  turnLimitWrappedUp: args.turnLimitWrappedUp ?? latest?.turnLimitWrappedUp,
@@ -2919,12 +2927,12 @@ function sharedWrite(args, latest) {
2919
2927
  })
2920
2928
  };
2921
2929
  }
2922
- async function saveRunning(args) {
2930
+ async function saveRunning(args, traceContext) {
2923
2931
  if (args.messages.length === 0 || !isContinuablePiBoundary(args.messages)) return;
2924
2932
  try {
2925
2933
  const latest = await getTurnRecord(args.conversationId, args.turnId);
2926
2934
  return await upsertTurnRecord({
2927
- ...sharedWrite(args, latest),
2935
+ ...sharedWrite(args, traceContext, latest),
2928
2936
  ...definedProps({
2929
2937
  trailingMessageProvenance: args.trailingMessageProvenance,
2930
2938
  turnContexts: args.turnContexts
@@ -2944,7 +2952,7 @@ async function saveRunning(args) {
2944
2952
  return;
2945
2953
  }
2946
2954
  }
2947
- async function savePaused(args) {
2955
+ async function savePaused(args, traceContext) {
2948
2956
  const keepSlice = args.reason === "yield";
2949
2957
  const nextSliceId = keepSlice ? args.sliceId : args.sliceId + 1;
2950
2958
  try {
@@ -2954,7 +2962,7 @@ async function savePaused(args) {
2954
2962
  if (!args.beforePrompt && messages.length > 0 && !isContinuablePiBoundary(messages)) return;
2955
2963
  } else if (messages.length === 0 || !isContinuablePiBoundary(messages)) return;
2956
2964
  const base = {
2957
- ...sharedWrite(args, latest),
2965
+ ...sharedWrite(args, traceContext, latest),
2958
2966
  ...definedProps({
2959
2967
  cumulativeDurationMs: addDurationMs(latest?.cumulativeDurationMs, args.durationMs),
2960
2968
  cumulativeUsage: addAgentTurnUsage(latest?.cumulativeUsage, args.usage),
@@ -2992,7 +3000,7 @@ async function savePaused(args) {
2992
3000
  return;
2993
3001
  }
2994
3002
  }
2995
- async function saveDone(args) {
3003
+ async function saveDone(args, traceContext) {
2996
3004
  let latest;
2997
3005
  await persistWithRetry(async () => {
2998
3006
  latest = await getTurnRecord(args.conversationId, args.turnId);
@@ -3001,7 +3009,7 @@ async function saveDone(args) {
3001
3009
  if (sliceId === void 0) throw new Error("Completed turn checkpoint requires a slice id from the caller or the latest stored record");
3002
3010
  await persistWithRetry(async () => {
3003
3011
  await upsertTurnRecord({
3004
- ...sharedWrite(args, latest),
3012
+ ...sharedWrite(args, traceContext, latest),
3005
3013
  ...definedProps({
3006
3014
  cumulativeDurationMs: addDurationMs(latest?.cumulativeDurationMs, args.durationMs),
3007
3015
  cumulativeUsage: addAgentTurnUsage(latest?.cumulativeUsage, args.usage),
@@ -3856,6 +3864,7 @@ function projectConversationMessages(history) {
3856
3864
  role: data.role,
3857
3865
  text: data.text,
3858
3866
  createdAtMs: event.createdAtMs,
3867
+ ...data.authorIdentityId ? { authorIdentityId: data.authorIdentityId } : void 0,
3859
3868
  ...splitMeta(data.meta)
3860
3869
  };
3861
3870
  if (data.type === "message" && current) throw new Error(`Duplicate message event for ${data.messageId} at seq ${event.seq}`);
@@ -3904,6 +3913,7 @@ function toStoredConversationMessage(message) {
3904
3913
  messageId: message.id,
3905
3914
  role: message.role,
3906
3915
  text: message.text,
3916
+ ...message.authorIdentityId ? { authorIdentityId: message.authorIdentityId } : void 0,
3907
3917
  ...Object.keys(meta).length > 0 ? { meta } : void 0,
3908
3918
  createdAtMs: message.createdAtMs
3909
3919
  };
@@ -4580,81 +4590,6 @@ function requireEventIdentifier(catalog, input) {
4580
4590
  const fitting = resourceTypes.filter(fits).map((candidate) => candidate.type);
4581
4591
  throw new ToolInputError([`Identifier "${identifier}" does not fit resourceType "${input.resourceType}" in namespace "${input.namespace}". Expected format: "${resourceType.identifier.format}".`, ...fitting.length > 0 ? [`If the identifier is correct, use one of these resourceType values: ${fitting.map((type) => `"${type}"`).join(", ")}.`] : []].join(" "));
4582
4592
  }
4583
- // src/chat/services/turn-session-routing.ts
4584
- async function loadConversationChain(conversationId, conversationStore) {
4585
- const chain = [];
4586
- const seen = /* @__PURE__ */ new Set();
4587
- let cursor = conversationId;
4588
- while (cursor && !seen.has(cursor)) {
4589
- seen.add(cursor);
4590
- const conversation = await conversationStore.get({ conversationId: cursor });
4591
- if (!conversation) break;
4592
- chain.push(conversation);
4593
- cursor = conversation.parentConversationId;
4594
- }
4595
- return chain;
4596
- }
4597
- /**
4598
- * Normalize a stored Slack session source against the conversation destination.
4599
- *
4600
- * Does not create channel or thread values from the conversation id.
4601
- */
4602
- function slackSourceForDestination(args) {
4603
- if (args.source?.kind !== "slack" || args.source.channelId !== args.destination.channelId) return;
4604
- const threadTs = args.source.threadTs?.trim();
4605
- return {
4606
- kind: "slack",
4607
- visibility: args.source.visibility ?? "public",
4608
- teamId: args.source.teamId?.trim() || args.destination.teamId,
4609
- channelId: args.source.channelId,
4610
- ...threadTs ? { threadTs } : void 0
4611
- };
4612
- }
4613
- /**
4614
- * Resolve the Conversation Location, Destination, and session Source.
4615
- *
4616
- * Reads parent Conversations when this Conversation has no Destination.
4617
- */
4618
- async function resolveConversationRouting(args) {
4619
- const conversationStore = args.conversationStore ?? (await import("./viewer-CpXTGlep.js").then((n) => n.u)).getConversationStore();
4620
- const chain = await loadConversationChain(args.conversationId, conversationStore);
4621
- let destination;
4622
- let source;
4623
- let location;
4624
- for (const conversation of chain) {
4625
- destination ??= conversation.destination;
4626
- source ??= conversation.sessionSource;
4627
- location ??= conversation.location;
4628
- if (destination && source) break;
4629
- }
4630
- if (!destination) return;
4631
- if (destination.platform === "slack") {
4632
- const slackSource = slackSourceForDestination({
4633
- destination,
4634
- source
4635
- });
4636
- return {
4637
- destination,
4638
- ...location ? { location } : void 0,
4639
- ...slackSource ? { source: slackSource } : void 0
4640
- };
4641
- }
4642
- return {
4643
- destination,
4644
- ...location ? { location } : void 0,
4645
- ...source ? { source } : void 0
4646
- };
4647
- }
4648
- /** Require the Conversation Destination and Source from SQL. */
4649
- async function resolveTurnSessionRouting(args) {
4650
- const routing = await resolveConversationRouting(args);
4651
- if (!routing?.destination || !routing.source) throw new Error(`Conversation ${args.conversationId} is missing durable routing metadata`);
4652
- return {
4653
- destination: routing.destination,
4654
- ...routing.location ? { location: routing.location } : void 0,
4655
- source: routing.source
4656
- };
4657
- }
4658
4593
  // src/chat/automations/revision.ts
4659
4594
  /** Storage revisions prevent stale edits from replacing newer values. */
4660
4595
  var AutomationConflictError = class extends ToolInputError {
@@ -4971,10 +4906,6 @@ async function deleteEventAutomation(db, id) {
4971
4906
  const rows = await db.update(juniorAutomations).set({ status: "deleted" }).where(and(eq(juniorAutomations.id, id), activeEventAutomationWhere())).returning();
4972
4907
  return rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
4973
4908
  }
4974
- /** List live event automations in one Slack workspace. */
4975
- async function listEventAutomationsForTeam(db, teamId) {
4976
- return (await db.select().from(juniorAutomations).where(and(eq(juniorAutomations.teamId, teamId), activeEventAutomationWhere())).orderBy(asc(juniorAutomations.createdAtMs), asc(juniorAutomations.id))).map(parseEventAutomationRow);
4977
- }
4978
4909
  /**
4979
4910
  * List deleted event automations created by one user, newest first.
4980
4911
  * Used only to keep historical runs after the live automation list drops the row.
@@ -5250,4 +5181,892 @@ async function saveScheduledAutomation(db, task, expectedRevision, editedBy) {
5250
5181
  async function saveScheduledAutomationInLock(db, task, current) {
5251
5182
  await writeScheduledAutomation(db, requireStoredTask(task), current);
5252
5183
  }
5253
- export { loadTurnRoute as $, TURN_CONTEXT_TAG as $n, CONVERSATION_WORK_STALE_ENQUEUE_MS as $t, resolveConversationRouting as A, TurnBudgetExceededError as An, upsertConversationMessage as At, registeredEventMatchSchema as B, getUserMessageInstructionText as Bn, clearPendingAuth as Bt, automationVersionOf as C, removeActiveConversation as Cn, estimateConversationContextTokens as Ct, AutomationConflictError as D, steerWebPendingMessages as Dn, normalizeConversationText as Dt, sameDefinitionValue as E, startConversationWork as En, markConversationMessage as Et, eventCatalogEntry as F, isTurnExecutionLimitExceededError as Fn, AuthPausePersistenceError as Ft, requireValidEventTrigger as G, isToolResultError as Gn, saveTurnCheckpoint as Gt, registeredEventTypeSchema as H, instructionTextForProjection as Hn, wasPendingAuthStopped as Ht, eventGuidance as I, persistWithRetry as In, AuthorizationFlowDisabledError as It, loadConnectedMcpProviders as J, retainRuntimeTurnContext as Jn, failTurnRecord as Jt, commitAcceptedReply as K, isToolResultMessage as Kn, abandonTurnRecord as Kt, eventNamespaceSchema as L, extractAssistantText as Ln, AuthorizationPauseError as Lt, CORE_EVENT_NAMESPACE as M, TurnToolCallLimitExceededError as Mn, PluginCredentialFailureError as Mt, automationEventCatalog as N, buildTurnErrorResponse as Nn, createPluginAuthOrchestration as Nt, automationRevision as O, stopConversationWork as On, recordDeliveredAssistantMessage as Ot, describeRegisteredEventTrigger as P, createTurnLimitGate as Pn, authorizationId as Pt, loadTurnProjection as Q, SESSION_CONTEXT_SECTION_TAG as Qn, CONVERSATION_WORK_CHECK_IN_INTERVAL_MS as Qt, eventWatchUnavailable as R, getPiMessageRole as Rn, abandonReplacedPendingAuth as Rt, automationDefinition as S, releaseConversationWork as Sn, createConversationMemoryService as St, readAutomationVersion as T, requestConversationWork as Tn, isHumanConversationMessage as Tt, registeredResourceTypeSchema as U, isAssistantMessage as Un, continuableMessages as Ut, registeredEventTriggerProblems as V, hasRuntimeTurnContext as Vn, getConversationPendingAuth as Vt, requireEventIdentifier as W, isContinuablePiBoundary as Wn, loadTurnCheckpoint as Wt, loadLastModelProfile as X, trimTrailingAssistantMessages as Xn, listTurnSummaries as Xt, loadConversationProjection as Y, stripRuntimeTurnContext as Yn, getTurnRecord as Yt, loadProjection as Z, REQUEST_CONTEXT_SECTION_TAG as Zn, recordTurnSummary as Zt, parseEventAutomationRow as _, isInvalidConversationRecordError as _n, setConfigDefaults as _r, settleConversationTitleWork as _t, saveScheduledAutomation as a, checkInConversationWork as an, getThreadStopDecision as ar, recordAuthorizationRequested as at, admitEventAutomationRun as b, recordAttemptFailure as bn, appendThreadContextMessages as bt, withScheduledAutomationLock as c, completeConversationStop as cn, getAgentContextInputLimitTokens as cr, recordSubscribedReplyRoute as ct, createEventAutomation as d, drainConversationMailbox as dn, setExperimentalFeatures as dr, appendConversationMessages as dt, ackMessages as en, buildDeterministicAssistantMessageId as er, openConversationProjection as et, deleteEventAutomation as f, getConversation as fn, createDurableLocationConfigurationService as fr, hydrateConversationMessages as ft, listEventAutomationsForTeam as g, isFinalAttempt as gn, getConfigDefaults as gr, resolveConversationTitle as gt, listDeletedEventAutomationsCreatedBy as h, hasRunnableConversationWork as hn, piMessageFromHistoryItem as hr, persistConversationMessageSummaries as ht, readScheduledAutomation as i, cancelHumanFacingPendingMessages as in, getSubscribedReplyPreflightDecision as ir, recordAuthorizationCompleted as it, resolveTurnSessionRouting as j, TurnSliceLimitExceededError as jn, createPluginEgress as jt, requireAutomationRevision as k, TURN_LIMIT_DISABLED_REASON as kn, turnHasReply as kt, blockEventAutomation as l, completeConversationWork as ln, getExperimentalFeatures as lr, recordToolExecutionStarted as lt, getEventAutomation as m, hasConversationStop as mn, historyItemFromPiMessage as mr, projectConversationMessages as mt, listScheduledAutomationsForTeam as n, appendInboundMessage as nn, SubscribedReplyReason as nr, recordAttachmentsDelivered as nt, saveScheduledAutomationInLock as o, clearConsumedConversationWake as on, estimateTextTokens as or, recordGuardianActionReviewed as ot, findMatchingEventAutomations as p, getConversationWorkState as pn, configValueSchema as pr, persistConversationMessages as pt, commitMessages as q, normalizeToolNameFromResult as qn, completeTurnRecord as qt, parseScheduledAutomationRow as r, beginConversationResume as rn, decideSubscribedThreadReply as rr, recordAuthenticationLinked as rt, scheduledRunId as s, clearExpiredConversationLease as sn, getAgentContextCompactionTriggerTokens as sr, recordMcpProviderConnected as st, createScheduledAutomation as t, appendExclusiveInboundMessage as tn, buildDeterministicTurnId as tr, recordAgentsInstructionsUpdated as tt, collectEventAutomationMatchKeys as u, deadLetterAttempt as un, isExperimentalFeatureEnabled as ur, recordTurnRoute as ut, saveEventAutomation as v, listActiveConversationIds as vn, fallbackShortTitle as vt, listAutomationVersions as w, requestAnotherSlice as wn, getConversationMessageSlackTs as wt, eventAutomationRunLimitReason as x, recordConversationRetry as xn, buildConversationContext as xt, setEventAutomationStatus as y, markConversationWorkEnqueued as yn, resolveTaskTitle as yt, hasEventCatalogEntries as z, getTerminalAssistantMessages as zn, canReusePendingAuthLink as zt };
5184
+ /** Mark one live scheduled automation deleted while keeping its history. */
5185
+ async function deleteScheduledAutomation(db, id, nowMs = Date.now()) {
5186
+ return await withScheduledAutomationLock(db, id, async (tx) => {
5187
+ const current = await readScheduledAutomation(tx, id);
5188
+ if (!current || current.status === "deleted") return void 0;
5189
+ const deleted = requireStoredTask({
5190
+ ...current,
5191
+ nextRunAtMs: void 0,
5192
+ runNowAtMs: void 0,
5193
+ status: "deleted",
5194
+ updatedAtMs: nowMs
5195
+ });
5196
+ await writeScheduledAutomation(tx, deleted, current);
5197
+ return deleted;
5198
+ });
5199
+ }
5200
+ // src/api/conversations/aggregate.ts
5201
+ function usageTokenValue(source, field) {
5202
+ return sql`CASE
5203
+ WHEN ${source.usage}->>${field} IS NOT NULL
5204
+ THEN (${source.usage}->>${field})::double precision
5205
+ ELSE NULL
5206
+ END`;
5207
+ }
5208
+ function tokenValue(source) {
5209
+ return sql`
5210
+ CASE
5211
+ WHEN COALESCE(
5212
+ ${source.usage}->>'inputTokens',
5213
+ ${source.usage}->>'outputTokens',
5214
+ ${source.usage}->>'cachedInputTokens',
5215
+ ${source.usage}->>'cacheCreationTokens'
5216
+ ) IS NOT NULL
5217
+ THEN COALESCE((${source.usage}->>'inputTokens')::double precision, 0)
5218
+ + COALESCE((${source.usage}->>'outputTokens')::double precision, 0)
5219
+ + COALESCE((${source.usage}->>'cachedInputTokens')::double precision, 0)
5220
+ + COALESCE((${source.usage}->>'cacheCreationTokens')::double precision, 0)
5221
+ WHEN ${source.usage}->>'totalTokens' IS NOT NULL
5222
+ THEN (${source.usage}->>'totalTokens')::double precision
5223
+ ELSE NULL
5224
+ END
5225
+ `;
5226
+ }
5227
+ function costValue(source) {
5228
+ return conversationUsageCostExpr(source.usage);
5229
+ }
5230
+ /** Read one conversation row's usage cost as a SQL expression. */
5231
+ function conversationUsageCostExpr(usage) {
5232
+ return sql`
5233
+ CASE
5234
+ WHEN ${usage}->'cost'->>'total' IS NOT NULL
5235
+ THEN (${usage}->'cost'->>'total')::double precision
5236
+ WHEN COALESCE(
5237
+ ${usage}->'cost'->>'input',
5238
+ ${usage}->'cost'->>'output',
5239
+ ${usage}->'cost'->>'cacheRead',
5240
+ ${usage}->'cost'->>'cacheWrite'
5241
+ ) IS NOT NULL
5242
+ THEN COALESCE((${usage}->'cost'->>'input')::double precision, 0)
5243
+ + COALESCE((${usage}->'cost'->>'output')::double precision, 0)
5244
+ + COALESCE((${usage}->'cost'->>'cacheRead')::double precision, 0)
5245
+ + COALESCE((${usage}->'cost'->>'cacheWrite')::double precision, 0)
5246
+ ELSE NULL
5247
+ END
5248
+ `;
5249
+ }
5250
+ /** Select complete conversation metrics inside the database instead of materializing source rows. */
5251
+ function conversationAggregateColumns(sources) {
5252
+ const metrics = sources?.metrics ?? juniorConversations;
5253
+ const roots = sources?.roots ?? juniorConversations;
5254
+ const conversationCount = sources ? sql`COUNT(DISTINCT ${roots.conversationId})` : sql`COUNT(*)`;
5255
+ return {
5256
+ active: sql`${conversationCount} FILTER (
5257
+ WHERE ${roots.executionStatus} NOT IN ('idle', 'failed')
5258
+ )::integer`,
5259
+ conversations: sql`${conversationCount}::integer`,
5260
+ cachedInputTokens: sql`SUM(${usageTokenValue(metrics, "cachedInputTokens")})::double precision`,
5261
+ cacheCreationTokens: sql`SUM(${usageTokenValue(metrics, "cacheCreationTokens")})::double precision`,
5262
+ costUsd: sql`SUM(${costValue(metrics)})::double precision`,
5263
+ durationMs: sql`COALESCE(SUM(${metrics.durationMs}), 0)::double precision`,
5264
+ failed: sql`${conversationCount} FILTER (
5265
+ WHERE ${roots.executionStatus} = 'failed'
5266
+ )::integer`,
5267
+ inputTokens: sql`SUM(${usageTokenValue(metrics, "inputTokens")})::double precision`,
5268
+ tokens: sql`SUM(${tokenValue(metrics)})::double precision`
5269
+ };
5270
+ }
5271
+ /** Select the complete first/last activity range for a grouped conversation aggregate. */
5272
+ function conversationRangeColumns() {
5273
+ return {
5274
+ firstSeenAt: sql`MIN(${juniorConversations.createdAt})`.mapWith(juniorConversations.createdAt),
5275
+ lastSeenAt: sql`MAX(${juniorConversations.lastActivityAt})`.mapWith(juniorConversations.lastActivityAt)
5276
+ };
5277
+ }
5278
+ /** Count distinct UTC activity dates without loading conversation timestamps into the app. */
5279
+ function conversationActiveDaysColumn() {
5280
+ return sql`COUNT(DISTINCT (${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC')::date)::integer`;
5281
+ }
5282
+ // src/chat/automations/execution-stats.ts
5283
+ const AUTOMATION_EXECUTION_STATUSES = [
5284
+ "blocked",
5285
+ "completed",
5286
+ "failed"
5287
+ ];
5288
+ /** An execution is a run with an outcome. Skipped runs are not executions. */
5289
+ function isExecution() {
5290
+ return and(inArray(juniorAutomationRuns.status, [...AUTOMATION_EXECUTION_STATUSES]), isNotNull(juniorAutomationRuns.completedAtMs));
5291
+ }
5292
+ function addUsd(current, next) {
5293
+ return Math.round((current + next) * 0xe8d4a51000) / 0xe8d4a51000;
5294
+ }
5295
+ const EMPTY_RUN_WINDOWS = {
5296
+ 1: 0,
5297
+ 7: 0,
5298
+ 30: 0,
5299
+ 90: 0
5300
+ };
5301
+ /** Empty run or cost windows for automations with no execution history. */
5302
+ function emptyAutomationRunWindows() {
5303
+ return { ...EMPTY_RUN_WINDOWS };
5304
+ }
5305
+ function utcDate(nowMs) {
5306
+ return new Date(nowMs).toISOString().slice(0, 10);
5307
+ }
5308
+ function emptyDay(date) {
5309
+ return {
5310
+ costUsd: 0,
5311
+ date,
5312
+ event: 0,
5313
+ scheduled: 0
5314
+ };
5315
+ }
5316
+ function emptyStatusDay(date) {
5317
+ return {
5318
+ blocked: 0,
5319
+ completed: 0,
5320
+ date,
5321
+ failed: 0
5322
+ };
5323
+ }
5324
+ /**
5325
+ * Return the Conversation id when that Conversation exists, else null. A
5326
+ * dispatch can end before it creates its Conversation, and the run's
5327
+ * `conversation_id` is a foreign key.
5328
+ */
5329
+ function existingConversationId(conversationId) {
5330
+ return sql`(select ${juniorConversations.conversationId} from ${juniorConversations} where ${juniorConversations.conversationId} = ${conversationId})`;
5331
+ }
5332
+ /**
5333
+ * Record the outcome of one Automation run. A run that already has an outcome
5334
+ * keeps it. A failed write is logged, and the heartbeat settles a started
5335
+ * event run later.
5336
+ */
5337
+ async function recordAutomationExecution(automationId, options) {
5338
+ const nowMs = options.nowMs ?? Date.now();
5339
+ const status = options.status ?? "completed";
5340
+ const outcome = {
5341
+ completedAtMs: nowMs,
5342
+ errorMessage: options.errorMessage ?? null,
5343
+ status
5344
+ };
5345
+ try {
5346
+ await getDb().insert(juniorAutomationRuns).values({
5347
+ id: options.executionId,
5348
+ automationId,
5349
+ conversationId: options.conversationId ? existingConversationId(options.conversationId) : null,
5350
+ ...outcome
5351
+ }).onConflictDoUpdate({
5352
+ target: juniorAutomationRuns.id,
5353
+ set: {
5354
+ ...outcome,
5355
+ conversationId: sql`coalesce(${juniorAutomationRuns.conversationId}, excluded.conversation_id)`
5356
+ },
5357
+ setWhere: inArray(juniorAutomationRuns.status, ["pending", "running"])
5358
+ });
5359
+ } catch (error) {
5360
+ logWarn("task.execution.stat_failed", {
5361
+ error,
5362
+ "app.task.execution.id": options.executionId,
5363
+ "app.task.execution.status": status,
5364
+ "app.task.execution.task_id": automationId
5365
+ });
5366
+ }
5367
+ }
5368
+ /** Load usage analytics for the given automations. */
5369
+ async function readAutomationExecutionSummaries(automationIds, options = {}) {
5370
+ if (automationIds.length === 0) return /* @__PURE__ */ new Map();
5371
+ const nowMs = options.nowMs ?? Date.now();
5372
+ const oneDayAgoMs = nowMs - 864e5;
5373
+ const sevenDaysAgoMs = nowMs - 6048e5;
5374
+ const thirtyDaysAgoMs = nowMs - 2592e6;
5375
+ const ninetyDaysAgoMs = nowMs - 7776e6;
5376
+ const db = getDb();
5377
+ const selected = and(isExecution(), inArray(juniorAutomationRuns.automationId, automationIds));
5378
+ const latest = db.selectDistinctOn([juniorAutomationRuns.automationId], {
5379
+ status: juniorAutomationRuns.status,
5380
+ conversationId: juniorAutomationRuns.conversationId,
5381
+ automationId: juniorAutomationRuns.automationId
5382
+ }).from(juniorAutomationRuns).where(selected).orderBy(juniorAutomationRuns.automationId, desc(juniorAutomationRuns.completedAtMs), desc(juniorAutomationRuns.id)).as("latest_task_execution");
5383
+ const conversationCost = conversationUsageCostExpr(juniorConversations.usage);
5384
+ const costSince = (sinceMs) => sql`coalesce(sum(${conversationCost}) filter (where ${juniorAutomationRuns.completedAtMs} >= ${sinceMs}), 0)::double precision`;
5385
+ const rows = await db.select({
5386
+ costLast1Day: costSince(oneDayAgoMs),
5387
+ costLast7Days: costSince(sevenDaysAgoMs),
5388
+ costLast30Days: costSince(thirtyDaysAgoMs),
5389
+ costLast90Days: costSince(ninetyDaysAgoMs),
5390
+ lastRunStatus: latest.status,
5391
+ lastConversationId: latest.conversationId,
5392
+ lastExecutedAtMs: max(juniorAutomationRuns.completedAtMs),
5393
+ runsLast1Day: sql`count(*) filter (where ${juniorAutomationRuns.completedAtMs} >= ${oneDayAgoMs})::int`,
5394
+ runsLast7Days: sql`count(*) filter (where ${juniorAutomationRuns.completedAtMs} >= ${sevenDaysAgoMs})::int`,
5395
+ runsLast30Days: sql`count(*) filter (where ${juniorAutomationRuns.completedAtMs} >= ${thirtyDaysAgoMs})::int`,
5396
+ runsLast90Days: sql`count(*) filter (where ${juniorAutomationRuns.completedAtMs} >= ${ninetyDaysAgoMs})::int`,
5397
+ automationId: juniorAutomationRuns.automationId,
5398
+ totalRuns: sql`count(*)::int`
5399
+ }).from(juniorAutomationRuns).innerJoin(latest, eq(latest.automationId, juniorAutomationRuns.automationId)).leftJoin(juniorConversations, executionConversation()).where(selected).groupBy(juniorAutomationRuns.automationId, latest.conversationId, latest.status);
5400
+ return new Map(rows.map((row) => [row.automationId, {
5401
+ costUsd: {
5402
+ 1: addUsd(0, row.costLast1Day),
5403
+ 7: addUsd(0, row.costLast7Days),
5404
+ 30: addUsd(0, row.costLast30Days),
5405
+ 90: addUsd(0, row.costLast90Days)
5406
+ },
5407
+ lastRunStatus: row.lastRunStatus,
5408
+ ...row.lastConversationId ? { lastConversationId: row.lastConversationId } : void 0,
5409
+ ...row.lastExecutedAtMs !== null ? { lastExecutedAtMs: row.lastExecutedAtMs } : void 0,
5410
+ runs: {
5411
+ 1: row.runsLast1Day,
5412
+ 7: row.runsLast7Days,
5413
+ 30: row.runsLast30Days,
5414
+ 90: row.runsLast90Days
5415
+ },
5416
+ totalRuns: row.totalRuns
5417
+ }]));
5418
+ }
5419
+ /** Load a fixed trailing window of completed executions stacked by task type. */
5420
+ async function readAutomationExecutionDays(dayCount = 90, options = {}) {
5421
+ const end = utcDate(options.nowMs ?? Date.now());
5422
+ const endMs = Date.parse(`${end}T23:59:59.999Z`);
5423
+ const startMs = Date.parse(`${end}T00:00:00.000Z`) - (dayCount - 1) * 864e5;
5424
+ const executionDate = sql`to_char(to_timestamp(${juniorAutomationRuns.completedAtMs} / 1000.0) at time zone 'UTC', 'YYYY-MM-DD')`;
5425
+ const rows = await readCompletedExecutionBuckets({
5426
+ access: options.access,
5427
+ bucket: executionDate,
5428
+ endMs,
5429
+ startMs
5430
+ });
5431
+ const byDate = /* @__PURE__ */ new Map();
5432
+ for (let offset = 0; offset < dayCount; offset += 1) {
5433
+ const date = utcDate(startMs + offset * 864e5);
5434
+ byDate.set(date, emptyDay(date));
5435
+ }
5436
+ for (const row of rows) {
5437
+ const day = byDate.get(row.date);
5438
+ if (!day) continue;
5439
+ if (row.kind === "scheduled") day.scheduled = row.count;
5440
+ else if (row.kind === "event") day.event = row.count;
5441
+ if (row.costUsd !== null && row.costUsd > 0) day.costUsd = addUsd(day.costUsd, row.costUsd);
5442
+ }
5443
+ return [...byDate.values()];
5444
+ }
5445
+ /** Load a fixed trailing window of completed executions stacked by hour. */
5446
+ async function readAutomationExecutionHours(hourCount = 168, options = {}) {
5447
+ const nowMs = options.nowMs ?? Date.now();
5448
+ const end = new Date(nowMs);
5449
+ end.setUTCMinutes(0, 0, 0);
5450
+ const endMs = end.getTime() + 3599999;
5451
+ const startMs = end.getTime() - (hourCount - 1) * 36e5;
5452
+ const executionHour = sql`to_char(to_timestamp(${juniorAutomationRuns.completedAtMs} / 1000.0) at time zone 'UTC', 'YYYY-MM-DD"T"HH24')`;
5453
+ const rows = await readCompletedExecutionBuckets({
5454
+ access: options.access,
5455
+ bucket: executionHour,
5456
+ endMs,
5457
+ startMs
5458
+ });
5459
+ const byHour = /* @__PURE__ */ new Map();
5460
+ for (let offset = 0; offset < hourCount; offset += 1) {
5461
+ const date = new Date(startMs + offset * 36e5).toISOString().slice(0, 13);
5462
+ byHour.set(date, emptyDay(date));
5463
+ }
5464
+ for (const row of rows) {
5465
+ const hour = byHour.get(row.date);
5466
+ if (!hour) continue;
5467
+ if (row.kind === "scheduled") hour.scheduled = row.count;
5468
+ else if (row.kind === "event") hour.event = row.count;
5469
+ if (row.costUsd !== null && row.costUsd > 0) hour.costUsd = addUsd(hour.costUsd, row.costUsd);
5470
+ }
5471
+ return [...byHour.values()];
5472
+ }
5473
+ /** Count completed executions and their cost per time bucket and kind. */
5474
+ async function readCompletedExecutionBuckets(args) {
5475
+ const conversationCost = conversationUsageCostExpr(juniorConversations.usage);
5476
+ return await getDb().select({
5477
+ count: count(),
5478
+ costUsd: sql`SUM(${conversationCost})::double precision`,
5479
+ date: args.bucket,
5480
+ kind: juniorAutomations.kind
5481
+ }).from(juniorAutomationRuns).innerJoin(juniorAutomations, eq(juniorAutomations.id, juniorAutomationRuns.automationId)).leftJoin(juniorConversations, eq(juniorConversations.conversationId, juniorAutomationRuns.conversationId)).where(and(gte(juniorAutomationRuns.completedAtMs, args.startMs), lte(juniorAutomationRuns.completedAtMs, args.endMs), eq(juniorAutomationRuns.status, "completed"), args.access)).groupBy(args.bucket, juniorAutomations.kind).orderBy(asc(args.bucket), asc(juniorAutomations.kind));
5482
+ }
5483
+ /** Load the Automation run linked to one conversation, if any. */
5484
+ async function readAutomationExecutionByConversationId(args) {
5485
+ const [row] = await getDb().select({
5486
+ kind: juniorAutomations.kind,
5487
+ automationId: juniorAutomationRuns.automationId
5488
+ }).from(juniorAutomationRuns).innerJoin(juniorAutomations, eq(juniorAutomations.id, juniorAutomationRuns.automationId)).where(eq(juniorAutomationRuns.conversationId, args.conversationId)).limit(1);
5489
+ return row;
5490
+ }
5491
+ /**
5492
+ * Load the display name of the Automation linked to each untitled
5493
+ * conversation. Like the Automation list, the name falls back to the first
5494
+ * line of the instruction. Titled conversations and conversations without a
5495
+ * linked Automation are absent from the result.
5496
+ */
5497
+ async function readAutomationTitlesByConversationId(conversations) {
5498
+ const titles = /* @__PURE__ */ new Map();
5499
+ const untitledIds = conversations.flatMap((conversation) => conversation.title ? [] : [conversation.conversationId]);
5500
+ if (untitledIds.length === 0) return titles;
5501
+ const rows = await getDb().select({
5502
+ conversationId: juniorAutomationRuns.conversationId,
5503
+ instruction: juniorAutomations.instruction,
5504
+ title: juniorAutomations.title
5505
+ }).from(juniorAutomationRuns).innerJoin(juniorAutomations, eq(juniorAutomations.id, juniorAutomationRuns.automationId)).where(inArray(juniorAutomationRuns.conversationId, untitledIds));
5506
+ for (const row of rows) {
5507
+ const title = row.title?.trim() || fallbackShortTitle(row.instruction, "");
5508
+ if (row.conversationId && title) titles.set(row.conversationId, title);
5509
+ }
5510
+ return titles;
5511
+ }
5512
+ /** Load newest-first executions for one automation, with conversation titles when present. */
5513
+ async function readAutomationExecutions(args) {
5514
+ return (await getDb().select(executionColumns).from(juniorAutomationRuns).leftJoin(juniorConversations, executionConversation()).leftJoin(juniorAutomationVersions, executionVersion()).where(and(isExecution(), eq(juniorAutomationRuns.automationId, args.automationId))).orderBy(desc(juniorAutomationRuns.completedAtMs), desc(juniorAutomationRuns.id)).limit(args.limit)).map((row) => mapAutomationExecutionRecord(row));
5515
+ }
5516
+ /** Load newest-first executions for the supplied viewer-visible automations. */
5517
+ async function readAutomationRuns(args) {
5518
+ if (args.automationIds.length === 0) return [];
5519
+ return (await getDb().select({
5520
+ ...executionColumns,
5521
+ kind: juniorAutomations.kind,
5522
+ automationId: juniorAutomationRuns.automationId
5523
+ }).from(juniorAutomationRuns).innerJoin(juniorAutomations, eq(juniorAutomations.id, juniorAutomationRuns.automationId)).leftJoin(juniorConversations, executionConversation()).leftJoin(juniorAutomationVersions, executionVersion()).where(and(isExecution(), inArray(juniorAutomationRuns.automationId, args.automationIds))).orderBy(desc(juniorAutomationRuns.completedAtMs), desc(juniorAutomationRuns.id)).limit(args.limit)).map((row) => ({
5524
+ ...mapAutomationExecutionRecord(row),
5525
+ kind: row.kind,
5526
+ automationId: row.automationId
5527
+ }));
5528
+ }
5529
+ /** Columns that describe one execution, its Conversation, and its version. */
5530
+ const executionColumns = {
5531
+ automationVersion: juniorAutomationRuns.automationVersion,
5532
+ conversationId: juniorAutomationRuns.conversationId,
5533
+ credentialMode: sql`${juniorAutomationVersions.definition}->>'credentialMode'`,
5534
+ durationMs: juniorConversations.durationMs,
5535
+ errorMessage: juniorAutomationRuns.errorMessage,
5536
+ executedAtMs: juniorAutomationRuns.completedAtMs,
5537
+ executionId: juniorAutomationRuns.id,
5538
+ scheduledForMs: juniorAutomationRuns.scheduledForMs,
5539
+ startedAtMs: juniorAutomationRuns.startedAtMs,
5540
+ status: juniorAutomationRuns.status,
5541
+ title: juniorConversations.title,
5542
+ usage: juniorConversations.usage
5543
+ };
5544
+ function executionConversation() {
5545
+ return eq(juniorConversations.conversationId, juniorAutomationRuns.conversationId);
5546
+ }
5547
+ function executionVersion() {
5548
+ return and(eq(juniorAutomationVersions.automationId, juniorAutomationRuns.automationId), eq(juniorAutomationVersions.version, juniorAutomationRuns.automationVersion));
5549
+ }
5550
+ function isoTime(ms) {
5551
+ return ms === null ? void 0 : new Date(ms).toISOString();
5552
+ }
5553
+ function mapAutomationExecutionRecord(row) {
5554
+ const title = row.title?.trim();
5555
+ const costUsd = agentTurnCostUsd(row.usage ?? void 0);
5556
+ const totalTokens = agentTurnTotalTokens(row.usage ?? void 0);
5557
+ const durationMs = typeof row.durationMs === "number" && Number.isFinite(row.durationMs) && row.durationMs > 0 ? row.durationMs : void 0;
5558
+ const error = row.errorMessage?.trim();
5559
+ const scheduledFor = isoTime(row.scheduledForMs);
5560
+ const startedAt = isoTime(row.startedAtMs);
5561
+ return {
5562
+ ...row.automationVersion !== null ? { automationVersion: row.automationVersion } : void 0,
5563
+ ...row.conversationId ? { conversationId: row.conversationId } : void 0,
5564
+ ...costUsd !== void 0 ? { costUsd } : void 0,
5565
+ ...row.credentialMode === "creator" || row.credentialMode === "system" ? { credentialMode: row.credentialMode } : void 0,
5566
+ ...durationMs !== void 0 ? { durationMs } : void 0,
5567
+ ...error ? { error } : void 0,
5568
+ executedAt: new Date(row.executedAtMs).toISOString(),
5569
+ executionId: row.executionId,
5570
+ ...scheduledFor ? { scheduledFor } : void 0,
5571
+ ...startedAt ? { startedAt } : void 0,
5572
+ status: row.status,
5573
+ ...title ? { title } : void 0,
5574
+ ...totalTokens !== void 0 ? { totalTokens } : void 0
5575
+ };
5576
+ }
5577
+ /** Load a fixed trailing window of terminal executions for one automation by status. */
5578
+ async function readAutomationExecutionStatusDays(args) {
5579
+ const dayCount = args.dayCount ?? 90;
5580
+ const end = utcDate(args.nowMs ?? Date.now());
5581
+ const endMs = Date.parse(`${end}T23:59:59.999Z`);
5582
+ const startMs = Date.parse(`${end}T00:00:00.000Z`) - (dayCount - 1) * 864e5;
5583
+ const executionDate = sql`to_char(to_timestamp(${juniorAutomationRuns.completedAtMs} / 1000.0) at time zone 'UTC', 'YYYY-MM-DD')`;
5584
+ const rows = await readExecutionStatusBuckets({
5585
+ automationId: args.automationId,
5586
+ bucket: executionDate,
5587
+ endMs,
5588
+ startMs
5589
+ });
5590
+ const byDate = /* @__PURE__ */ new Map();
5591
+ for (let offset = 0; offset < dayCount; offset += 1) {
5592
+ const date = utcDate(startMs + offset * 864e5);
5593
+ byDate.set(date, emptyStatusDay(date));
5594
+ }
5595
+ for (const row of rows) {
5596
+ const day = byDate.get(row.date);
5597
+ if (!day) continue;
5598
+ if (row.status === "completed") day.completed = row.count;
5599
+ else if (row.status === "failed") day.failed = row.count;
5600
+ else if (row.status === "blocked") day.blocked = row.count;
5601
+ }
5602
+ return [...byDate.values()];
5603
+ }
5604
+ /** Load a fixed trailing window of terminal executions for one automation by hour. */
5605
+ async function readAutomationExecutionStatusHours(args) {
5606
+ const hourCount = args.hourCount ?? 168;
5607
+ const nowMs = args.nowMs ?? Date.now();
5608
+ const end = new Date(nowMs);
5609
+ end.setUTCMinutes(0, 0, 0);
5610
+ const endMs = end.getTime() + 3599999;
5611
+ const startMs = end.getTime() - (hourCount - 1) * 36e5;
5612
+ const executionHour = sql`to_char(to_timestamp(${juniorAutomationRuns.completedAtMs} / 1000.0) at time zone 'UTC', 'YYYY-MM-DD"T"HH24')`;
5613
+ const rows = await readExecutionStatusBuckets({
5614
+ automationId: args.automationId,
5615
+ bucket: executionHour,
5616
+ endMs,
5617
+ startMs
5618
+ });
5619
+ const byHour = /* @__PURE__ */ new Map();
5620
+ for (let offset = 0; offset < hourCount; offset += 1) {
5621
+ const date = new Date(startMs + offset * 36e5).toISOString().slice(0, 13);
5622
+ byHour.set(date, emptyStatusDay(date));
5623
+ }
5624
+ for (const row of rows) {
5625
+ const hour = byHour.get(row.date);
5626
+ if (!hour) continue;
5627
+ if (row.status === "completed") hour.completed = row.count;
5628
+ else if (row.status === "failed") hour.failed = row.count;
5629
+ else if (row.status === "blocked") hour.blocked = row.count;
5630
+ }
5631
+ return [...byHour.values()];
5632
+ }
5633
+ /** Count one automation's executions per time bucket and status. */
5634
+ async function readExecutionStatusBuckets(args) {
5635
+ return await getDb().select({
5636
+ count: count(),
5637
+ date: args.bucket,
5638
+ status: juniorAutomationRuns.status
5639
+ }).from(juniorAutomationRuns).where(and(isExecution(), eq(juniorAutomationRuns.automationId, args.automationId), gte(juniorAutomationRuns.completedAtMs, args.startMs), lte(juniorAutomationRuns.completedAtMs, args.endMs))).groupBy(args.bucket, juniorAutomationRuns.status).orderBy(asc(args.bucket), asc(juniorAutomationRuns.status));
5640
+ }
5641
+ // src/chat/automations/blocked-notice.ts
5642
+ function buildBlockedNoticeText(automationId, title, reason) {
5643
+ const url = getDashboardTaskLink(automationId);
5644
+ return [
5645
+ `Your automation *${escapeSlackMrkdwnText(title)}* is blocked. It won't run again until you resume it.`,
5646
+ `> ${escapeSlackMrkdwnText(reason.replace(/\s+/g, " ").trim())}`,
5647
+ url ? `Fix the problem, then ${formatSlackLink(url, "resume it")}.` : "Fix the problem, then ask me to resume it."
5648
+ ].join("\n");
5649
+ }
5650
+ /**
5651
+ * Tell an Automation's creator in a direct message that a run blocked it,
5652
+ * and why. Call this once, after the Automation itself is stored as blocked,
5653
+ * so the dashboard matches the notice. The notice is best-effort: the reason
5654
+ * also shows on the dashboard and in the Automation tools.
5655
+ */
5656
+ async function notifyAutomationBlocked(ref) {
5657
+ await runBestEffort(async () => {
5658
+ const task = ref.kind === "scheduled" ? await readScheduledAutomation(getDb(), ref.automationId) : await getEventAutomation(getDb(), ref.automationId);
5659
+ if (!task) return;
5660
+ const title = task.title ?? fallbackShortTitle(task.task.text, ref.kind === "scheduled" ? "Scheduled automation" : "Event automation");
5661
+ await postSlackMessage({
5662
+ channelId: await openSlackDirectMessage(task.createdBy.slackUserId),
5663
+ text: buildBlockedNoticeText(ref.automationId, title, ref.reason)
5664
+ });
5665
+ }, "automation.blocked_notice.failed", {
5666
+ "app.task.type": ref.kind,
5667
+ ...ref.dispatchId ? { "app.dispatch.id": ref.dispatchId } : void 0
5668
+ });
5669
+ }
5670
+ // src/chat/agent-dispatch/store.ts
5671
+ const DISPATCH_PREFIX = "junior:agent_dispatch";
5672
+ const DISPATCH_LOCK_TTL_MS = 6e5;
5673
+ const DISPATCH_MAILBOX_APPEND_INDEX_LOCK_TTL_MS = 1e4;
5674
+ const nonEmptyExactStringSchema = z.string().min(1).refine((value) => value === value.trim() && value.toLowerCase() !== "unknown");
5675
+ const pendingDispatchMailboxAppendIndexSchema = z.array(nonEmptyExactStringSchema);
5676
+ const dispatchStatusSchema = z.enum([
5677
+ "pending",
5678
+ "running",
5679
+ "awaiting_resume",
5680
+ "completed",
5681
+ "failed",
5682
+ "blocked"
5683
+ ]);
5684
+ const dispatchActorSchema = z.object({
5685
+ platform: z.literal("system"),
5686
+ name: nonEmptyExactStringSchema
5687
+ }).strict();
5688
+ const dispatchRecordSchema = z.object({
5689
+ actor: dispatchActorSchema,
5690
+ createdAtMs: z.number().finite(),
5691
+ credentialSubject: credentialSubjectSchema.optional(),
5692
+ destination: destinationSchema,
5693
+ destinationVisibility: destinationVisibilitySchema,
5694
+ errorMessage: z.string().optional(),
5695
+ id: nonEmptyExactStringSchema,
5696
+ idempotencyKey: z.string().min(1),
5697
+ input: z.string().min(1),
5698
+ metadata: z.record(z.string(), z.string()).optional(),
5699
+ plugin: nonEmptyExactStringSchema,
5700
+ replyAttribution: replyAttributionSchema.optional(),
5701
+ resultMessageTs: z.string().optional(),
5702
+ source: sourceSchema,
5703
+ outcomes: z.array(taskOutcomeSchema).max(5).optional(),
5704
+ status: dispatchStatusSchema,
5705
+ updatedAtMs: z.number().finite()
5706
+ }).strict().superRefine((record, ctx) => {
5707
+ if (!isSlackDestination(record.destination)) {
5708
+ ctx.addIssue({
5709
+ code: z.ZodIssueCode.custom,
5710
+ message: "Dispatch destination platform must be slack",
5711
+ path: ["destination"]
5712
+ });
5713
+ return;
5714
+ }
5715
+ const subject = record.credentialSubject;
5716
+ if (!subject) return;
5717
+ if (subject.allowedWhen === "private-direct-conversation") {
5718
+ if (!record.destination.channelId.startsWith("D")) {
5719
+ ctx.addIssue({
5720
+ code: z.ZodIssueCode.custom,
5721
+ message: "Dispatch credentialSubject requires a private direct Slack destination",
5722
+ path: ["credentialSubject"]
5723
+ });
5724
+ return;
5725
+ }
5726
+ if (subject.binding.type !== "slack-direct-conversation" || subject.binding.teamId !== record.destination.teamId || subject.binding.channelId !== record.destination.channelId) ctx.addIssue({
5727
+ code: z.ZodIssueCode.custom,
5728
+ message: "Dispatch credentialSubject binding must match destination",
5729
+ path: ["credentialSubject", "binding"]
5730
+ });
5731
+ return;
5732
+ }
5733
+ if (subject.allowedWhen === "timer-watch") {
5734
+ ctx.addIssue({
5735
+ code: z.ZodIssueCode.custom,
5736
+ message: "Dispatch credentialSubject cannot use a timer Watch binding",
5737
+ path: ["credentialSubject", "allowedWhen"]
5738
+ });
5739
+ return;
5740
+ }
5741
+ if (subject.binding.type !== subject.allowedWhen || subject.binding.plugin !== record.plugin || subject.binding.taskId !== subject.taskId) ctx.addIssue({
5742
+ code: z.ZodIssueCode.custom,
5743
+ message: "Dispatch credentialSubject binding must match task",
5744
+ path: ["credentialSubject", "binding"]
5745
+ });
5746
+ });
5747
+ /** Return the durable key for one plugin-facing dispatch projection. */
5748
+ function getDispatchStorageKey(id) {
5749
+ return `${DISPATCH_PREFIX}:record:${id}`;
5750
+ }
5751
+ function dispatchLockKey(id) {
5752
+ return `${DISPATCH_PREFIX}:lock:${id}`;
5753
+ }
5754
+ function pendingDispatchMailboxAppendIndexKey() {
5755
+ return `${DISPATCH_PREFIX}:incomplete`;
5756
+ }
5757
+ function pendingDispatchMailboxAppendIndexLockKey() {
5758
+ return `${DISPATCH_PREFIX}:incomplete:lock`;
5759
+ }
5760
+ function normalizeMetadata(metadata) {
5761
+ if (!metadata) return;
5762
+ const entries = Object.entries(metadata).filter((entry) => typeof entry[0] === "string" && typeof entry[1] === "string");
5763
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
5764
+ }
5765
+ const DISPATCH_ID_PREFIX = "agent-dispatch:";
5766
+ /** Return the stable dispatch id for one plugin idempotency key. */
5767
+ function buildDispatchId(plugin, idempotencyKey) {
5768
+ return `dispatch_${createHash("sha256").update(plugin).update("\0").update(idempotencyKey).digest("hex").slice(0, 32)}`;
5769
+ }
5770
+ /** Parse one durable dispatch projection record. */
5771
+ function parseDispatchRecord(value) {
5772
+ const parsed = dispatchRecordSchema.safeParse(value);
5773
+ return parsed.success ? parsed.data : void 0;
5774
+ }
5775
+ /** Return the isolated durable conversation id for one dispatch. */
5776
+ function getDispatchConversationId(dispatch) {
5777
+ return `${DISPATCH_ID_PREFIX}${dispatch.id}`;
5778
+ }
5779
+ /** Return the stable synthetic input message id for one dispatch turn. */
5780
+ function getDispatchInputMessageId(dispatchId) {
5781
+ return `${DISPATCH_ID_PREFIX}${dispatchId}`;
5782
+ }
5783
+ /**
5784
+ * Return the dispatch id of a dispatch Conversation, such as an Automation
5785
+ * run. Other Conversations have no dispatch.
5786
+ */
5787
+ function getDispatchIdForConversation(conversationId) {
5788
+ if (!conversationId.startsWith(DISPATCH_ID_PREFIX)) return void 0;
5789
+ return conversationId.slice(15) || void 0;
5790
+ }
5791
+ /** Return the stable turn id used by every run of one dispatch. */
5792
+ function getDispatchTurnId(dispatchId) {
5793
+ return `dispatch:${dispatchId}`;
5794
+ }
5795
+ function toDispatchProjection(record) {
5796
+ return {
5797
+ id: record.id,
5798
+ status: record.status,
5799
+ ...record.resultMessageTs ? { resultMessageTs: record.resultMessageTs } : void 0,
5800
+ ...record.errorMessage ? { errorMessage: record.errorMessage } : void 0
5801
+ };
5802
+ }
5803
+ /** Gate recovery to dispatches that can still make progress. */
5804
+ function isTerminalDispatchStatus(status) {
5805
+ return status === "completed" || status === "failed" || status === "blocked";
5806
+ }
5807
+ /** Serialize dispatch projection mutations. */
5808
+ async function withDispatchLock(dispatchId, task) {
5809
+ const state = getStateAdapter();
5810
+ await state.connect();
5811
+ const lock = await state.acquireLock(dispatchLockKey(dispatchId), DISPATCH_LOCK_TTL_MS);
5812
+ if (!lock) throw new Error(`Could not acquire dispatch lock for ${dispatchId}`);
5813
+ try {
5814
+ return await task(state);
5815
+ } finally {
5816
+ await state.releaseLock(lock);
5817
+ }
5818
+ }
5819
+ async function putRecord(state, record) {
5820
+ const parsed = dispatchRecordSchema.safeParse(record);
5821
+ if (!parsed.success) throw new Error(`Dispatch record is invalid: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"} ${issue.message}`).join("; ")}`);
5822
+ const next = parsed.data;
5823
+ await state.set(getDispatchStorageKey(next.id), next, JUNIOR_THREAD_STATE_TTL_MS);
5824
+ }
5825
+ async function updatePendingDispatchMailboxAppendIndex(state, update) {
5826
+ const lock = await state.acquireLock(pendingDispatchMailboxAppendIndexLockKey(), DISPATCH_MAILBOX_APPEND_INDEX_LOCK_TTL_MS);
5827
+ if (!lock) throw new Error("Could not acquire dispatch mailbox append index lock");
5828
+ try {
5829
+ const stored = await state.get(pendingDispatchMailboxAppendIndexKey());
5830
+ const ids = stored === void 0 || stored === null ? [] : pendingDispatchMailboxAppendIndexSchema.parse(stored);
5831
+ const next = [...new Set(update([...new Set(ids)]))].slice(-1e4);
5832
+ if (next.length === ids.length && next.every((id, index) => id === ids[index])) return;
5833
+ await state.set(pendingDispatchMailboxAppendIndexKey(), next, JUNIOR_THREAD_STATE_TTL_MS);
5834
+ } finally {
5835
+ await state.releaseLock(lock);
5836
+ }
5837
+ }
5838
+ /** Load dispatch state for conversation work and plugin projections. */
5839
+ async function getDispatchRecord(id) {
5840
+ const state = getStateAdapter();
5841
+ await state.connect();
5842
+ return parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
5843
+ }
5844
+ /** Create a dispatch idempotently within its owner namespace. */
5845
+ async function createOrGetDispatch(args) {
5846
+ const id = buildDispatchId(args.plugin, args.options.idempotencyKey);
5847
+ return await withDispatchLock(id, async (state) => {
5848
+ const existing = parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
5849
+ if (existing) return {
5850
+ record: existing,
5851
+ status: "already_exists"
5852
+ };
5853
+ const metadata = normalizeMetadata(args.options.metadata);
5854
+ const record = {
5855
+ actor: {
5856
+ platform: "system",
5857
+ name: args.plugin
5858
+ },
5859
+ createdAtMs: args.nowMs,
5860
+ ...args.options.credentialSubject ? { credentialSubject: args.options.credentialSubject } : void 0,
5861
+ destination: args.options.destination,
5862
+ destinationVisibility: args.options.destinationVisibility,
5863
+ id,
5864
+ idempotencyKey: args.options.idempotencyKey,
5865
+ input: args.options.input,
5866
+ ...metadata ? { metadata } : void 0,
5867
+ plugin: args.plugin,
5868
+ ...args.options.replyAttribution ? { replyAttribution: args.options.replyAttribution } : void 0,
5869
+ ...args.options.outcomes ? { outcomes: args.options.outcomes } : void 0,
5870
+ status: "pending",
5871
+ source: args.options.source,
5872
+ updatedAtMs: args.nowMs
5873
+ };
5874
+ await updatePendingDispatchMailboxAppendIndex(state, (ids) => ids.includes(id) ? ids : [...ids, id]);
5875
+ await putRecord(state, record);
5876
+ return {
5877
+ record,
5878
+ status: "created"
5879
+ };
5880
+ });
5881
+ }
5882
+ async function transitionDispatch(id, transition) {
5883
+ return await withDispatchLock(id, async (state) => {
5884
+ const current = parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
5885
+ if (!current) return;
5886
+ const transitioned = transition(current);
5887
+ if (transitioned === current) return current;
5888
+ const next = {
5889
+ ...transitioned,
5890
+ updatedAtMs: Date.now()
5891
+ };
5892
+ await putRecord(state, next);
5893
+ return next;
5894
+ });
5895
+ }
5896
+ /** Mark a dispatch projection as actively running. */
5897
+ async function markDispatchRunning(id) {
5898
+ return await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
5899
+ ...record,
5900
+ status: "running",
5901
+ errorMessage: void 0
5902
+ });
5903
+ }
5904
+ /** Project a durable awaiting-resume turn state to the plugin API. */
5905
+ async function markDispatchAwaitingResume(id) {
5906
+ return await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
5907
+ ...record,
5908
+ status: "awaiting_resume"
5909
+ });
5910
+ }
5911
+ async function recordEventAutomationExecution(previous, next, status) {
5912
+ if (!next || next.status !== status || previous?.status === status) return;
5913
+ if (next.plugin !== "junior") return;
5914
+ const eventAutomationId = next.metadata?.eventAutomationId;
5915
+ if (!eventAutomationId) return;
5916
+ await recordAutomationExecution(eventAutomationId, {
5917
+ conversationId: getDispatchConversationId(next),
5918
+ errorMessage: next.errorMessage,
5919
+ executionId: next.id,
5920
+ nowMs: next.updatedAtMs,
5921
+ status
5922
+ });
5923
+ }
5924
+ /**
5925
+ * Project a blocked turn to the plugin API. An Event automation is blocked
5926
+ * first, so a retry after a failed write still blocks it. Its creator is told
5927
+ * when the block is first stored. The heartbeat blocks a Scheduled automation
5928
+ * and tells its creator.
5929
+ */
5930
+ async function markDispatchBlocked(id, errorMessage, resultMessageTs) {
5931
+ const previous = await getDispatchRecord(id);
5932
+ const eventAutomationId = previous?.metadata?.eventAutomationId;
5933
+ if (previous?.plugin === "junior" && eventAutomationId && !isTerminalDispatchStatus(previous.status) && await blockEventAutomation(getDb(), eventAutomationId, errorMessage)) await notifyAutomationBlocked({
5934
+ automationId: eventAutomationId,
5935
+ dispatchId: id,
5936
+ kind: "event",
5937
+ reason: errorMessage
5938
+ });
5939
+ const next = await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
5940
+ ...record,
5941
+ errorMessage,
5942
+ ...resultMessageTs ? { resultMessageTs } : void 0,
5943
+ status: "blocked"
5944
+ });
5945
+ await recordEventAutomationExecution(previous, next, "blocked");
5946
+ return next;
5947
+ }
5948
+ /** Project a completed turn and its accepted Slack message to the plugin API. */
5949
+ async function markDispatchCompleted(id, resultMessageTs) {
5950
+ const previous = await getDispatchRecord(id);
5951
+ const next = await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
5952
+ ...record,
5953
+ errorMessage: void 0,
5954
+ ...resultMessageTs ? { resultMessageTs } : void 0,
5955
+ status: "completed"
5956
+ });
5957
+ await recordEventAutomationExecution(previous, next, "completed");
5958
+ return next;
5959
+ }
5960
+ /** Project a terminal conversation turn failure to the plugin API. */
5961
+ async function markDispatchFailed(id, errorMessage, resultMessageTs) {
5962
+ const previous = await getDispatchRecord(id);
5963
+ const next = await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
5964
+ ...record,
5965
+ errorMessage,
5966
+ ...resultMessageTs ? { resultMessageTs } : void 0,
5967
+ status: "failed"
5968
+ });
5969
+ await recordEventAutomationExecution(previous, next, "failed");
5970
+ return next;
5971
+ }
5972
+ /** Remove a dispatch after its durable mailbox append has succeeded. */
5973
+ async function confirmDispatchMailboxAppend(id) {
5974
+ const state = getStateAdapter();
5975
+ await state.connect();
5976
+ await updatePendingDispatchMailboxAppendIndex(state, (ids) => ids.filter((candidate) => candidate !== id));
5977
+ }
5978
+ /** List dispatches whose durable mailbox append may not have completed. */
5979
+ async function listPendingDispatchMailboxAppends() {
5980
+ const state = getStateAdapter();
5981
+ await state.connect();
5982
+ const stored = await state.get(pendingDispatchMailboxAppendIndexKey());
5983
+ return stored === void 0 || stored === null ? [] : [...new Set(pendingDispatchMailboxAppendIndexSchema.parse(stored))];
5984
+ }
5985
+ /** Load the dispatch under lock before its mailbox append becomes visible. */
5986
+ async function claimDispatchMailboxAppend(id) {
5987
+ return await withDispatchLock(id, async (state) => {
5988
+ return parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
5989
+ });
5990
+ }
5991
+ /** Return a plugin-scoped dispatch projection without exposing raw runtime state. */
5992
+ async function getPluginDispatchProjection(args) {
5993
+ const record = await getDispatchRecord(args.id);
5994
+ if (!record || record.plugin !== args.plugin) return;
5995
+ return toDispatchProjection(record);
5996
+ }
5997
+ // src/chat/services/turn-session-routing.ts
5998
+ async function loadConversationChain(conversationId, conversationStore) {
5999
+ const chain = [];
6000
+ const seen = /* @__PURE__ */ new Set();
6001
+ let cursor = conversationId;
6002
+ while (cursor && !seen.has(cursor)) {
6003
+ seen.add(cursor);
6004
+ const conversation = await conversationStore.get({ conversationId: cursor });
6005
+ if (!conversation) break;
6006
+ chain.push(conversation);
6007
+ cursor = conversation.parentConversationId;
6008
+ }
6009
+ return chain;
6010
+ }
6011
+ /**
6012
+ * Normalize a stored Slack session source against the conversation destination.
6013
+ *
6014
+ * Does not create channel or thread values from the conversation id.
6015
+ */
6016
+ function slackSourceForDestination(args) {
6017
+ if (args.source?.kind !== "slack" || args.source.channelId !== args.destination.channelId) return;
6018
+ const threadTs = args.source.threadTs?.trim();
6019
+ return {
6020
+ kind: "slack",
6021
+ visibility: args.source.visibility ?? "public",
6022
+ teamId: args.source.teamId?.trim() || args.destination.teamId,
6023
+ channelId: args.source.channelId,
6024
+ ...threadTs ? { threadTs } : void 0
6025
+ };
6026
+ }
6027
+ /**
6028
+ * Resolve the Conversation Location, Destination, and session Source.
6029
+ *
6030
+ * Reads parent Conversations when this Conversation has no Destination.
6031
+ */
6032
+ async function resolveConversationRouting(args) {
6033
+ const conversationStore = args.conversationStore ?? (await import("./viewer-BTgAfdUg.js").then((n) => n.v)).getConversationStore();
6034
+ const chain = await loadConversationChain(args.conversationId, conversationStore);
6035
+ let destination;
6036
+ let source;
6037
+ let location;
6038
+ for (const conversation of chain) {
6039
+ destination ??= conversation.destination;
6040
+ source ??= conversation.sessionSource;
6041
+ location ??= conversation.location;
6042
+ if (destination && source) break;
6043
+ }
6044
+ if (!destination) return;
6045
+ if (destination.platform === "slack") {
6046
+ const slackSource = slackSourceForDestination({
6047
+ destination,
6048
+ source
6049
+ });
6050
+ return {
6051
+ destination,
6052
+ ...location ? { location } : void 0,
6053
+ ...slackSource ? { source: slackSource } : void 0
6054
+ };
6055
+ }
6056
+ return {
6057
+ destination,
6058
+ ...location ? { location } : void 0,
6059
+ ...source ? { source } : void 0
6060
+ };
6061
+ }
6062
+ /** Require the Conversation Destination and Source from SQL. */
6063
+ async function resolveTurnSessionRouting(args) {
6064
+ const routing = await resolveConversationRouting(args);
6065
+ if (!routing?.destination || !routing.source) throw new Error(`Conversation ${args.conversationId} is missing durable routing metadata`);
6066
+ return {
6067
+ destination: routing.destination,
6068
+ ...routing.location ? { location: routing.location } : void 0,
6069
+ source: routing.source
6070
+ };
6071
+ }
6072
+ export { saveEventAutomation as $, recordAttemptFailure as $n, appendThreadContextMessages as $t, readAutomationRuns as A, recordTurnSummary as An, trimTrailingAssistantMessages as Ar, loadProjection as At, readScheduledAutomation as B, clearExpiredConversationLease as Bn, estimateTextTokens as Br, recordMcpProviderConnected as Bt, readAutomationExecutionByConversationId as C, loadTurnCheckpoint as Cn, isAssistantMessage as Cr, requireEventIdentifier as Ct, readAutomationExecutionStatusHours as D, failTurnRecord as Dn, normalizeToolNameFromResult as Dr, loadConnectedMcpProviders as Dt, readAutomationExecutionStatusDays as E, completeTurnRecord as En, isToolResultMessage as Er, commitMessages as Et, conversationRangeColumns as F, appendInboundMessage as Fn, buildDeterministicTurnId as Fr, recordAttachmentsDelivered as Ft, blockEventAutomation as G, getConversation as Gn, setExperimentalFeatures as Gr, hydrateConversationMessages as Gt, saveScheduledAutomationInLock as H, completeConversationWork as Hn, getAgentContextInputLimitTokens as Hr, recordToolExecutionStarted as Ht, createScheduledAutomation as I, beginConversationResume as In, SubscribedReplyReason as Ir, recordAuthenticationLinked as It, deleteEventAutomation as J, hasRunnableConversationWork as Jn, historyItemFromPiMessage as Jr, persistConversationMessageSummaries as Jt, collectEventAutomationMatchKeys as K, getConversationWorkState as Kn, createDurableLocationConfigurationService as Kr, persistConversationMessages as Kt, deleteScheduledAutomation as L, cancelHumanFacingPendingMessages as Ln, decideSubscribedThreadReply as Lr, recordAuthorizationCompleted as Lt, recordAutomationExecution as M, CONVERSATION_WORK_STALE_ENQUEUE_MS as Mn, SESSION_CONTEXT_SECTION_TAG as Mr, loadTurnRoute as Mt, conversationActiveDaysColumn as N, ackMessages as Nn, TURN_CONTEXT_TAG as Nr, openConversationProjection as Nt, readAutomationExecutionSummaries as O, getTurnRecord as On, retainRuntimeTurnContext as Or, loadConversationProjection as Ot, conversationAggregateColumns as P, appendExclusiveInboundMessage as Pn, buildDeterministicAssistantMessageId as Pr, recordAgentsInstructionsUpdated as Pt, parseEventAutomationRow as Q, markConversationWorkEnqueued as Qn, resolveTaskTitle as Qt, listScheduledAutomationsForTeam as R, checkInConversationWork as Rn, getSubscribedReplyPreflightDecision as Rr, recordAuthorizationRequested as Rt, existingConversationId as S, continuableMessages as Sn, instructionTextForProjection as Sr, registeredResourceTypeSchema as St, readAutomationExecutionHours as T, abandonTurnRecord as Tn, isToolResultError as Tr, commitAcceptedReply as Tt, scheduledRunId as U, deadLetterAttempt as Un, getExperimentalFeatures as Ur, recordTurnRoute as Ut, saveScheduledAutomation as V, completeConversationStop as Vn, getAgentContextCompactionTriggerTokens as Vr, recordSubscribedReplyRoute as Vt, withScheduledAutomationLock as W, drainConversationMailbox as Wn, isExperimentalFeatureEnabled as Wr, appendConversationMessages as Wt, getEventAutomation as X, isInvalidConversationRecordError as Xn, getConfigDefaults as Xr, settleConversationTitleWork as Xt, findMatchingEventAutomations as Y, isFinalAttempt as Yn, piMessageFromHistoryItem as Yr, resolveConversationTitle as Yt, listDeletedEventAutomationsCreatedBy as Z, listActiveConversationIds as Zn, setConfigDefaults as Zr, fallbackShortTitle as Zt, markDispatchCompleted as _, abandonReplacedPendingAuth as _n, extractAssistantText as _r, eventWatchUnavailable as _t, confirmDispatchMailboxAppend as a, markConversationMessage as an, startConversationWork as ar, listAutomationVersions as at, notifyAutomationBlocked as b, getConversationPendingAuth as bn, getUserMessageInstructionText as br, registeredEventTriggerProblems as bt, getDispatchIdForConversation as c, turnHasReply as cn, TURN_LIMIT_WRAP_UP_MESSAGE as cr, AutomationConflictError as ct, getDispatchTurnId as d, PluginCredentialFailureError as dn, TurnToolCallLimitExceededError as dr, CORE_EVENT_NAMESPACE as dt, buildConversationContext as en, recordConversationRetry as er, setEventAutomationStatus as et, getPluginDispatchProjection as f, createPluginAuthOrchestration as fn, buildTurnErrorResponse as fr, automationEventCatalog as ft, markDispatchBlocked as g, AuthorizationPauseError as gn, persistWithRetry as gr, eventNamespaceSchema as gt, markDispatchAwaitingResume as h, AuthorizationFlowDisabledError as hn, CODEMODE_TOOL_NAME as hr, eventGuidance as ht, claimDispatchMailboxAppend as i, isHumanConversationMessage as in, requestConversationWork as ir, automationVersionOf as it, readAutomationTitlesByConversationId as j, CONVERSATION_WORK_CHECK_IN_INTERVAL_MS as jn, REQUEST_CONTEXT_SECTION_TAG as jr, loadTurnProjection as jt, readAutomationExecutions as k, listTurnSummaries as kn, stripRuntimeTurnContext as kr, loadLastModelProfile as kt, getDispatchInputMessageId as l, upsertConversationMessage as ln, TurnBudgetExceededError as lr, automationRevision as lt, listPendingDispatchMailboxAppends as m, AuthPausePersistenceError as mn, isTurnExecutionLimitExceededError as mr, eventCatalogEntry as mt, resolveTurnSessionRouting as n, estimateConversationContextTokens as nn, removeActiveConversation as nr, eventAutomationRunLimitReason as nt, createOrGetDispatch as o, normalizeConversationText as on, steerWebPendingMessages as or, readAutomationVersion as ot, isTerminalDispatchStatus as p, authorizationId as pn, createTurnLimitGate as pr, describeRegisteredEventTrigger as pt, createEventAutomation as q, hasConversationStop as qn, configValueSchema as qr, projectConversationMessages as qt, buildDispatchId as r, getConversationMessageSlackTs as rn, requestAnotherSlice as rr, automationDefinition as rt, getDispatchConversationId as s, recordDeliveredAssistantMessage as sn, stopConversationWork as sr, sameDefinitionValue as st, resolveConversationRouting as t, createConversationMemoryService as tn, releaseConversationWork as tr, admitEventAutomationRun as tt, getDispatchRecord as u, createPluginEgress as un, TurnSliceLimitExceededError as ur, requireAutomationRevision as ut, markDispatchFailed as v, canReusePendingAuthLink as vn, getPiMessageRole as vr, hasEventCatalogEntries as vt, readAutomationExecutionDays as w, saveTurnCheckpoint as wn, isContinuablePiBoundary as wr, requireValidEventTrigger as wt, emptyAutomationRunWindows as x, wasPendingAuthStopped as xn, hasRuntimeTurnContext as xr, registeredEventTypeSchema as xt, markDispatchRunning as y, clearPendingAuth as yn, getTerminalAssistantMessages as yr, registeredEventMatchSchema as yt, parseScheduledAutomationRow as z, clearConsumedConversationWake as zn, getThreadStopDecision as zr, recordGuardianActionReviewed as zt };