@sentry/junior 0.267.0 → 0.269.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 (95) hide show
  1. package/README.md +2 -9
  2. package/dist/{actor-DZSokpwj.js → actor-CP9K2Idu.js} +2 -1
  3. package/dist/{adapter-BybMEezj.js → adapter-2-CG25be.js} +3 -51
  4. package/dist/{admin-DOs1_EiW.js → admin-DSs8S5Uy.js} +1 -1
  5. package/dist/{agent-invocations-CxKldAeB.js → agent-invocations-C1RNOCs8.js} +2 -0
  6. package/dist/{agent-runner-DgNATN4D.js → agent-runner-CrDrPUtY.js} +3411 -1640
  7. package/dist/api/schema.d.ts +2 -5630
  8. package/dist/api/schema.js +7 -7
  9. package/dist/api.d.ts +3 -1
  10. package/dist/api.js +268 -287
  11. package/dist/{app-DAB3fzyp.d.ts → app-BrMMzjYm.d.ts} +10 -3
  12. package/dist/app.d.ts +1 -1
  13. package/dist/app.js +371 -223
  14. package/dist/{automation-CmeFBSEh.js → automation-p87hg_AV.js} +74 -74
  15. package/dist/{briefs-CWKjka7v.js → briefs-PtODdLNu.js} +4 -4
  16. package/dist/{catalog-runtime-cMlAI5LY.js → catalog-runtime-BsoSmTM0.js} +1 -1
  17. package/dist/cli/chat.js +16 -26
  18. package/dist/cli/check.js +3 -3
  19. package/dist/cli/main.js +5 -3
  20. package/dist/cli/plugins.js +4 -4
  21. package/dist/cli/snapshot-create.js +4 -4
  22. package/dist/cli/upgrade.d.ts +1 -1
  23. package/dist/cli/upgrade.js +3 -3
  24. package/dist/{client-BjgbMawj.js → client-Cqf4TboL.js} +20 -15
  25. package/dist/{config-CdB6yJj5.js → config-CPsE4T5L.js} +2 -2
  26. package/dist/{connections-RE_65jzK.js → connections-upFXLmhG.js} +10 -11
  27. package/dist/{conversation-DSo310Ps.js → conversation-CTpD_PGk.js} +7 -84
  28. package/dist/{conversation-privacy-BzgJC5k0.js → conversation-privacy-D6zJc8ff.js} +1 -1
  29. package/dist/{credential-sync-DhEGEpgq.js → credential-sync-CS_6IC-f.js} +30 -34
  30. package/dist/{credentialed-CIGwoLU2.js → credentialed-BU-OW7HP.js} +577 -74
  31. package/dist/{store-B8CABbmR.d.ts → db-BzOCzVb1.d.ts} +17 -7
  32. package/dist/{executor-VYrx_K4w.js → executor-CNS7ONt-.js} +78 -4
  33. package/dist/instrumentation.js +2 -2
  34. package/dist/{assistant-reply-CT7VcXQe.js → interruption-marker-g6Xup3Fk.js} +8 -2
  35. package/dist/{job-queue-C0BZSa7r.js → job-queue-DI-k-Bbk.js} +2 -2
  36. package/dist/{list-1XQP-o6K.js → list-CGoQdXTp.js} +429 -1401
  37. package/dist/locks-D-0h111u.js +48 -0
  38. package/dist/{logging-DuZ1A4ab.js → logging-n7knse-O.js} +1 -1
  39. package/dist/{history-C84mlAwO.js → media-85PDJxO_.js} +83 -1
  40. package/dist/{message-NKSbbnA6.js → message-BpaWd8TT.js} +4 -4
  41. package/dist/nitro.d.ts +1 -1
  42. package/dist/nitro.js +3 -3
  43. package/dist/{oauth-callback-server-qAHLPtml.js → oauth-callback-server-CpGmEw_i.js} +2 -2
  44. package/dist/{prompt-BW9jFsqZ.js → prompt-BlQB0QQt.js} +1 -1
  45. package/dist/{registry-FQutO926.js → registry-By36_xsm.js} +2 -2
  46. package/dist/reporting-schema-CZREhYc-.js +104 -0
  47. package/dist/reporting-window-CVarfId9.js +255 -0
  48. package/dist/{resolve-Bk4yeuHy.js → resolve-CUIBcdPd.js} +3 -3
  49. package/dist/{result-B9e9ATOU.js → result-BemsOMOO.js} +31 -12
  50. package/dist/{runner-C3S2ObH-.js → runner-BVok7PAA.js} +12 -11
  51. package/dist/{sandbox-egress-signals-DqpGzzMB.js → sandbox-egress-signals-BSUWQI95.js} +1 -1
  52. package/dist/{schema-X7Fj4fv1.js → schema-ChnFo5f2.js} +98 -6
  53. package/dist/schema-jK4-1u_H.d.ts +5927 -0
  54. package/dist/{service-account-Do30HUNv.js → service-account-DHvlLnZw.js} +4 -3
  55. package/dist/{service-account-BQasl-qq.js → service-account-hMmESDcr.js} +3 -3
  56. package/dist/{session-stop-DkOjt46_.js → session-stop-BQW_iFcd.js} +499 -23
  57. package/dist/{skills-CQHrwfOa.js → skills-DmJJIuLF.js} +2 -2
  58. package/dist/{spawn-BHuqN14f.js → spawn-ChCm46i-.js} +2 -2
  59. package/dist/{store-6EetVP-m.js → store-1DVhoKOL.js} +4 -4
  60. package/dist/{store-DvNZIsCr.js → store-DCo2n5iJ.js} +3 -3
  61. package/dist/{task-queue-B0DLgd2P.js → task-queue-HMC4QjFm.js} +3 -3
  62. package/dist/task-runner-cA6yjn_E.js +269 -0
  63. package/dist/{thread-state-DRmdJ2QN.js → thread-state-CHy8LS0C.js} +8 -18
  64. package/dist/{turn-failure-response-G11cs2gE.js → turn-failure-response-Dr7erLWs.js} +11 -16
  65. package/dist/{turn-lifecycle-OzLIwvi1.js → turn-lifecycle-bf2fIYi_.js} +1 -1
  66. package/dist/{tasks-0ilmuCux.js → turn-session-routing-BqO0qXLk.js} +959 -44
  67. package/dist/{validation-CxEmt1id.js → validation-DHR1ivfh.js} +1 -1
  68. package/dist/{version-Bapedn4A.js → version-CXCHLgWm.js} +1 -1
  69. package/dist/version.js +1 -1
  70. package/dist/{viewer-DrN3oC2X.js → viewer-DSbD6DF6.js} +503 -241
  71. package/dist/{work-DUQoNcqP.js → work-BE_qTYGi.js} +17 -36
  72. package/dist/{worker-D1l4Ugk0.js → worker-Cm9_sWXg.js} +15 -7
  73. package/migrations/0065_user_customization.sql +1 -0
  74. package/migrations/0066_review_items.sql +53 -0
  75. package/migrations/0067_review_item_dedupe_tenant.sql +2 -0
  76. package/migrations/0068_review_item_slack_cards.sql +10 -0
  77. package/migrations/0069_system_identity.sql +25 -0
  78. package/migrations/0070_review_item_foreign_keys.sql +5 -0
  79. package/migrations/AGENTS.md +17 -0
  80. package/migrations/meta/0065_snapshot.json +3511 -0
  81. package/migrations/meta/0066_snapshot.json +4099 -0
  82. package/migrations/meta/0067_snapshot.json +3921 -0
  83. package/migrations/meta/0068_snapshot.json +4176 -0
  84. package/migrations/meta/0069_snapshot.json +3979 -0
  85. package/migrations/meta/0070_snapshot.json +3997 -0
  86. package/migrations/meta/_journal.json +42 -0
  87. package/package.json +5 -5
  88. package/dist/db-Bg-zC3r5.d.ts +0 -5
  89. package/dist/history-BgI33Kie.d.ts +0 -78
  90. package/dist/images-CecjXAAz.js +0 -64
  91. package/dist/task-runner-B-PD_JOj.js +0 -1074
  92. package/dist/turn-session-routing-Dyut9l7Y.js +0 -76
  93. package/dist/user-input-request-BDFBdF8r.js +0 -116
  94. package/dist/web-message-id-CJSUiksE.js +0 -236
  95. 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-BjgbMawj.js";
2
- import { i as getChatConfig, n as botConfig } from "./config-CdB6yJj5.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-DuZ1A4ab.js";
4
- import { c as toStoredSlackActor, p as parseSlackMessageTs, s as parseStoredSlackActor } from "./actor-DZSokpwj.js";
5
- import { B as parseDestination, C as bindProviderConversation, H as sameDestination, T as addAgentTurnUsage, f as getConversationStore, g as withConversationEventLock, h as createSqlConversationEventStore, m as getSqlExecutor, p as getDb, u as getConversationEventStore, x as startLocationThread } from "./viewer-DrN3oC2X.js";
6
- import { d as juniorDestinations } from "./agent-invocations-CxKldAeB.js";
7
- import { a as juniorAutomationRuns, l as juniorLocationConfigurations, o as juniorAutomationVersions, s as juniorAutomations } from "./executor-VYrx_K4w.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-FQutO926.js";
10
- import { n as pluginCatalogRuntime } from "./catalog-runtime-cMlAI5LY.js";
11
- import { H as startOAuthFlow, Nn as createPluginLogger, St as resolveSandboxEgressProviderForHost, ar as createPluginCache, cn as sleep$1, cr as agentsInstructionsUpdatedEvent, dn as JUNIOR_THREAD_STATE_TTL_MS, gn as ToolInputError, lr as authenticationLinkedEvent, p as getPlugins, sr as JUNIOR_NATIVE_EVENT_NAMESPACE, t as executeCredentialedEgressRequest, ur as authenticationUnlinkedEvent, vt as parseSandboxEgressAuthRequiredSignal, z as formatProviderLabel } from "./credentialed-CIGwoLU2.js";
12
- import { a as MUTATION_LOCK_TTL_MS, c as withLock, i as getStateAdapter, r as getDefaultRedisStateAdapterFor, s as fenceLock } from "./adapter-BybMEezj.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";
1
+ import { a as completeText, f as unwrapCurrentInstruction, p as escapeXml, s as resolveGatewayModel, y as isProviderRetryError } from "./client-Cqf4TboL.js";
2
+ import { i as getChatConfig, n as botConfig } from "./config-CPsE4T5L.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-n7knse-O.js";
4
+ import { c as parseStoredSlackActor, l as toStoredSlackActor, m as parseSlackMessageTs } from "./actor-CP9K2Idu.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-DSbD6DF6.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-CNS7ONt-.js";
8
+ import { a as agentHistoryItemSchema, g as instructionProvenanceFor, h as instructionActors, m as contextProvenance } from "./media-85PDJxO_.js";
9
+ import { f as credentialContextForActor, h as credentialSubjectSchema } from "./registry-By36_xsm.js";
10
+ import { n as pluginCatalogRuntime } from "./catalog-runtime-BsoSmTM0.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-BU-OW7HP.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-2-CG25be.js";
14
+ import { L as messageCardRef } from "./conversation-CTpD_PGk.js";
15
+ import { O as eventAutomationSchema, S as automationVersionSchema, j as scheduledAutomationSchema } from "./automation-p87hg_AV.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) {
@@ -401,7 +402,11 @@ function createDurableLocationConfigurationService(args) {
401
402
  * Add new keys here as features graduate from private experiments; remove them
402
403
  * once they become stable defaults.
403
404
  */
404
- const EXPERIMENTAL_FEATURES = ["operator-tools", "passive-routing"];
405
+ const EXPERIMENTAL_FEATURES = [
406
+ "operator-tools",
407
+ "passive-routing",
408
+ "user-customization"
409
+ ];
405
410
  const EXPERIMENTAL_FEATURE_SET = new Set(EXPERIMENTAL_FEATURES);
406
411
  let configuredExperimental = {};
407
412
  function isExperimentalFeature(value) {
@@ -887,7 +892,8 @@ const SESSION_CONTEXT_SECTION_TAG = "runtime";
887
892
  const REQUEST_CONTEXT_SECTION_TAG = {
888
893
  pluginContext: "plugin-context",
889
894
  conversationNotes: "conversation-notes",
890
- disabledTools: "disabled-tools"
895
+ disabledTools: "disabled-tools",
896
+ userCustomization: "user-customization"
891
897
  };
892
898
  // src/chat/pi/transcript.ts
893
899
  const RUNTIME_TURN_CONTEXT_START = `<${TURN_CONTEXT_TAG}>`;
@@ -1158,6 +1164,8 @@ const inboundMessageSourceSchema = z.enum([
1158
1164
  const agentInputSchema = z.object({
1159
1165
  attachments: z.array(z.unknown()).optional(),
1160
1166
  authorId: z.string().min(1).optional(),
1167
+ /** Identity of the input author, resolved at ingress. */
1168
+ authorIdentityId: z.string().min(1).optional(),
1161
1169
  metadata: z.record(z.string(), z.unknown()).optional(),
1162
1170
  text: z.string()
1163
1171
  }).strict().refine((input) => input.text.trim() || input.attachments?.length, { message: "agent input requires text or attachments" });
@@ -2300,8 +2308,17 @@ async function withTurnCursorMutation(conversationId, turnId, run) {
2300
2308
  if (!result.acquired) throw new Error(`Could not acquire turn cursor lock for ${turnId}`);
2301
2309
  return result.value;
2302
2310
  }
2303
- function executionMetricsForTurn(conversation, turnId) {
2304
- return conversation?.executionMetrics?.runId === turnId ? conversation.executionMetrics : void 0;
2311
+ /**
2312
+ * Read this Turn's own totals. A newer Turn can own the latest Conversation
2313
+ * metrics while this Turn is paused, so fall back to this Turn's metric rows.
2314
+ */
2315
+ async function executionMetricsForTurn(conversationStore, conversation, turnId) {
2316
+ if (!conversation) return void 0;
2317
+ if (conversation.executionMetrics?.runId === turnId) return conversation.executionMetrics;
2318
+ return await conversationStore.getRunMetrics({
2319
+ conversationId: conversation.conversationId,
2320
+ runId: turnId
2321
+ });
2305
2322
  }
2306
2323
  /** Keep only keys whose value is defined. */
2307
2324
  function definedProps$1(values) {
@@ -2350,6 +2367,7 @@ const storedTurnSummarySchema = z.object({
2350
2367
  /** Full resume cursor stored for one turn. */
2351
2368
  const storedTurnRecordSchema = z.object({
2352
2369
  actor: actorSchema.optional(),
2370
+ actorIdentityId: z.string().min(1).optional(),
2353
2371
  schemaVersion: z.literal(TURN_CURSOR_SCHEMA_VERSION),
2354
2372
  version: z.number().int().nonnegative(),
2355
2373
  conversationId: z.string().min(1),
@@ -2472,6 +2490,7 @@ function materializeTurnRecord(stored, piProjection, turnStartMessageIndex, runt
2472
2490
  actors: instructionActors(piProjection.provenance),
2473
2491
  cumulativeDurationMs: runtimeMetrics?.cumulativeDurationMs ?? 0,
2474
2492
  ...definedProps$1({
2493
+ actorIdentityId: stored.actorIdentityId,
2475
2494
  channelName: runtimeMetrics?.channelName,
2476
2495
  credentialSubject: stored.credentialSubject,
2477
2496
  cumulativeToolCallCount: stored.cumulativeToolCallCount,
@@ -2513,8 +2532,9 @@ async function materializeStoredTurnRecord(conversationId, turnId, followCurrent
2513
2532
  const followsReplacement = followCurrentReplacement && (parsed.state === "running" || parsed.state === "paused") && parsed.historyVersion !== void 0 && parsed.historyVersion !== currentHistoryVersion;
2514
2533
  const piProjection = followsReplacement ? projectConversationEvents(await eventStore.loadCurrentHistory(conversationId), { defaultProfile: botConfig.defaultProfile }) : pinnedProjection;
2515
2534
  const turnStartMessageIndex = followsReplacement ? 0 : parsed.turnStartSeq === void 0 ? void 0 : piProjection.seqs.filter((seq) => seq <= parsed.turnStartSeq).length;
2516
- const conversation = await getConversationStore().get({ conversationId });
2517
- const executionMetrics = executionMetricsForTurn(conversation, turnId);
2535
+ const conversationStore = getConversationStore();
2536
+ const conversation = await conversationStore.get({ conversationId });
2537
+ const executionMetrics = await executionMetricsForTurn(conversationStore, conversation, turnId);
2518
2538
  return materializeTurnRecord(parsed, piProjection, turnStartMessageIndex, {
2519
2539
  channelName: conversation?.channelName,
2520
2540
  cumulativeDurationMs: executionMetrics?.durationMs,
@@ -2545,6 +2565,7 @@ function buildStoredRecord(args) {
2545
2565
  committedSeq: args.committedSeq,
2546
2566
  ...definedProps$1({
2547
2567
  actor: args.actor,
2568
+ actorIdentityId: args.actorIdentityId,
2548
2569
  credentialSubject: args.credentialSubject,
2549
2570
  cumulativeToolCallCount: args.cumulativeToolCallCount,
2550
2571
  turnLimitWrappedUp: args.turnLimitWrappedUp,
@@ -2623,6 +2644,7 @@ async function updateTurnState(args) {
2623
2644
  lastProgressAtMs: parsed.lastProgressAtMs,
2624
2645
  previousVersion: parsed.version,
2625
2646
  ...definedProps$1({
2647
+ actorIdentityId: args.existing.actorIdentityId,
2626
2648
  credentialSubject: args.existing.credentialSubject,
2627
2649
  cumulativeToolCallCount: args.existing.cumulativeToolCallCount,
2628
2650
  turnLimitWrappedUp: args.existing.turnLimitWrappedUp,
@@ -2649,8 +2671,9 @@ async function upsertTurnRecord(args) {
2649
2671
  }
2650
2672
  async function upsertTurnRecordLocked(args, fence) {
2651
2673
  const existingRecord = await getStoredTurnRecord(args.conversationId, args.turnId);
2652
- const conversation = await (args.conversationStore ?? getConversationStore()).get({ conversationId: args.conversationId });
2653
- const executionMetrics = executionMetricsForTurn(conversation, args.turnId);
2674
+ const conversationStore = args.conversationStore ?? getConversationStore();
2675
+ const conversation = await conversationStore.get({ conversationId: args.conversationId });
2676
+ const executionMetrics = await executionMetricsForTurn(conversationStore, conversation, args.turnId);
2654
2677
  const existingDispatchId = existingRecord?.dispatchId ?? (await listTurnSummaries(args.conversationId)).find((summary) => summary.turnId === args.turnId)?.dispatchId;
2655
2678
  if (existingDispatchId && args.dispatchId && existingDispatchId !== args.dispatchId) throw new Error(`Turn ${args.turnId} dispatchId cannot be changed`);
2656
2679
  const ttlMs = turnCursorTtlMs(args.state, args.ttlMs);
@@ -2672,8 +2695,10 @@ async function upsertTurnRecordLocked(args, fence) {
2672
2695
  const dispatchId = args.dispatchId ?? existingRecord?.dispatchId;
2673
2696
  const hasProviderLocation = Boolean(conversation?.location) || args.destination?.platform === "slack";
2674
2697
  const publishExternally = dispatchId || turnSource ? hasProviderLocation && (Boolean(dispatchId) || turnSource?.kind === "slack" || turnSource?.kind === "event") : existingRecord?.publishExternally ?? false;
2698
+ const turnActor = existingRecord?.actor ?? args.actor;
2699
+ const actorIdentityId = existingRecord?.actorIdentityId ?? (turnActor ? await resolveActorIdentityId(getSqlExecutor(), turnActor) : void 0);
2675
2700
  return await setStoredRecord({
2676
- actor: existingRecord?.actor ?? args.actor,
2701
+ actor: turnActor,
2677
2702
  fence,
2678
2703
  conversationStore: args.conversationStore,
2679
2704
  destination: args.destination,
@@ -2690,7 +2715,7 @@ async function upsertTurnRecordLocked(args, fence) {
2690
2715
  ...definedProps$1({ indexTtlMs: args.ttlMs }),
2691
2716
  turnStartMessageIndex,
2692
2717
  record: buildStoredRecord({
2693
- actor: existingRecord?.actor ?? args.actor,
2718
+ actor: turnActor,
2694
2719
  conversationId: args.conversationId,
2695
2720
  turnId: args.turnId,
2696
2721
  sliceId: args.sliceId,
@@ -2699,6 +2724,7 @@ async function upsertTurnRecordLocked(args, fence) {
2699
2724
  historyVersion: commit.historyVersion,
2700
2725
  previousVersion: existingRecord?.version,
2701
2726
  ...definedProps$1({
2727
+ actorIdentityId,
2702
2728
  credentialSubject: existingRecord?.credentialSubject ?? args.credentialSubject,
2703
2729
  cumulativeToolCallCount: args.cumulativeToolCallCount ?? existingRecord?.cumulativeToolCallCount,
2704
2730
  turnLimitWrappedUp: args.turnLimitWrappedUp ?? existingRecord?.turnLimitWrappedUp,
@@ -2728,7 +2754,7 @@ async function recordTurnSummaryOwned(args, fence) {
2728
2754
  const stored = await getStoredTurnRecord(args.conversationId, args.turnId);
2729
2755
  const conversationStore = args.conversationStore ?? getConversationStore();
2730
2756
  const conversation = await conversationStore.get({ conversationId: args.conversationId });
2731
- const executionMetrics = executionMetricsForTurn(conversation, args.turnId);
2757
+ const executionMetrics = await executionMetricsForTurn(conversationStore, conversation, args.turnId);
2732
2758
  const priorSummary = (await listTurnSummaries(args.conversationId)).find((summary) => summary.turnId === args.turnId);
2733
2759
  const terminalSummary = priorSummary && (priorSummary.state === "completed" || priorSummary.state === "failed" || priorSummary.state === "abandoned") ? priorSummary : void 0;
2734
2760
  const terminalRecord = stored && (stored.state === "completed" || stored.state === "failed" || stored.state === "abandoned") ? stored : void 0;
@@ -2866,11 +2892,14 @@ async function loadTurnCheckpoint(args) {
2866
2892
  };
2867
2893
  }
2868
2894
  async function saveTurnCheckpoint(args) {
2869
- if (args.mode === "running") return await saveRunning(args);
2870
- if (args.mode === "paused") return await savePaused(args);
2871
- await saveDone(args);
2872
- }
2873
- function sharedWrite(args, latest) {
2895
+ const traceContext = getActiveTraceContext();
2896
+ return await withSpan("turn.checkpoint.save", "turn.checkpoint.save", {}, async () => {
2897
+ if (args.mode === "running") return await saveRunning(args, traceContext);
2898
+ if (args.mode === "paused") return await savePaused(args, traceContext);
2899
+ await saveDone(args, traceContext);
2900
+ }, { "app.turn.checkpoint.mode": args.mode });
2901
+ }
2902
+ function sharedWrite(args, traceContext, latest) {
2874
2903
  return {
2875
2904
  conversationId: args.conversationId,
2876
2905
  turnId: args.turnId,
@@ -2883,7 +2912,7 @@ function sharedWrite(args, latest) {
2883
2912
  dispatchId: args.dispatchId ?? latest?.dispatchId,
2884
2913
  source: args.source,
2885
2914
  surface: args.surface ?? latest?.surface,
2886
- traceContext: latest?.traceContext ?? getActiveTraceContext(),
2915
+ traceContext: latest?.traceContext ?? traceContext,
2887
2916
  turnStartMessageIndex: args.turnStartMessageIndex ?? latest?.turnStartMessageIndex,
2888
2917
  cumulativeToolCallCount: args.cumulativeToolCallCount ?? latest?.cumulativeToolCallCount,
2889
2918
  turnLimitWrappedUp: args.turnLimitWrappedUp ?? latest?.turnLimitWrappedUp,
@@ -2891,12 +2920,12 @@ function sharedWrite(args, latest) {
2891
2920
  })
2892
2921
  };
2893
2922
  }
2894
- async function saveRunning(args) {
2923
+ async function saveRunning(args, traceContext) {
2895
2924
  if (args.messages.length === 0 || !isContinuablePiBoundary(args.messages)) return;
2896
2925
  try {
2897
2926
  const latest = await getTurnRecord(args.conversationId, args.turnId);
2898
2927
  return await upsertTurnRecord({
2899
- ...sharedWrite(args, latest),
2928
+ ...sharedWrite(args, traceContext, latest),
2900
2929
  ...definedProps({
2901
2930
  trailingMessageProvenance: args.trailingMessageProvenance,
2902
2931
  turnContexts: args.turnContexts
@@ -2916,7 +2945,7 @@ async function saveRunning(args) {
2916
2945
  return;
2917
2946
  }
2918
2947
  }
2919
- async function savePaused(args) {
2948
+ async function savePaused(args, traceContext) {
2920
2949
  const keepSlice = args.reason === "yield";
2921
2950
  const nextSliceId = keepSlice ? args.sliceId : args.sliceId + 1;
2922
2951
  try {
@@ -2926,7 +2955,7 @@ async function savePaused(args) {
2926
2955
  if (!args.beforePrompt && messages.length > 0 && !isContinuablePiBoundary(messages)) return;
2927
2956
  } else if (messages.length === 0 || !isContinuablePiBoundary(messages)) return;
2928
2957
  const base = {
2929
- ...sharedWrite(args, latest),
2958
+ ...sharedWrite(args, traceContext, latest),
2930
2959
  ...definedProps({
2931
2960
  cumulativeDurationMs: addDurationMs(latest?.cumulativeDurationMs, args.durationMs),
2932
2961
  cumulativeUsage: addAgentTurnUsage(latest?.cumulativeUsage, args.usage),
@@ -2964,7 +2993,7 @@ async function savePaused(args) {
2964
2993
  return;
2965
2994
  }
2966
2995
  }
2967
- async function saveDone(args) {
2996
+ async function saveDone(args, traceContext) {
2968
2997
  let latest;
2969
2998
  await persistWithRetry(async () => {
2970
2999
  latest = await getTurnRecord(args.conversationId, args.turnId);
@@ -2973,7 +3002,7 @@ async function saveDone(args) {
2973
3002
  if (sliceId === void 0) throw new Error("Completed turn checkpoint requires a slice id from the caller or the latest stored record");
2974
3003
  await persistWithRetry(async () => {
2975
3004
  await upsertTurnRecord({
2976
- ...sharedWrite(args, latest),
3005
+ ...sharedWrite(args, traceContext, latest),
2977
3006
  ...definedProps({
2978
3007
  cumulativeDurationMs: addDurationMs(latest?.cumulativeDurationMs, args.durationMs),
2979
3008
  cumulativeUsage: addAgentTurnUsage(latest?.cumulativeUsage, args.usage),
@@ -3828,6 +3857,7 @@ function projectConversationMessages(history) {
3828
3857
  role: data.role,
3829
3858
  text: data.text,
3830
3859
  createdAtMs: event.createdAtMs,
3860
+ ...data.authorIdentityId ? { authorIdentityId: data.authorIdentityId } : void 0,
3831
3861
  ...splitMeta(data.meta)
3832
3862
  };
3833
3863
  if (data.type === "message" && current) throw new Error(`Duplicate message event for ${data.messageId} at seq ${event.seq}`);
@@ -3876,6 +3906,7 @@ function toStoredConversationMessage(message) {
3876
3906
  messageId: message.id,
3877
3907
  role: message.role,
3878
3908
  text: message.text,
3909
+ ...message.authorIdentityId ? { authorIdentityId: message.authorIdentityId } : void 0,
3879
3910
  ...Object.keys(meta).length > 0 ? { meta } : void 0,
3880
3911
  createdAtMs: message.createdAtMs
3881
3912
  };
@@ -4868,10 +4899,6 @@ async function deleteEventAutomation(db, id) {
4868
4899
  const rows = await db.update(juniorAutomations).set({ status: "deleted" }).where(and(eq(juniorAutomations.id, id), activeEventAutomationWhere())).returning();
4869
4900
  return rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
4870
4901
  }
4871
- /** List live event automations in one Slack workspace. */
4872
- async function listEventAutomationsForTeam(db, teamId) {
4873
- return (await db.select().from(juniorAutomations).where(and(eq(juniorAutomations.teamId, teamId), activeEventAutomationWhere())).orderBy(asc(juniorAutomations.createdAtMs), asc(juniorAutomations.id))).map(parseEventAutomationRow);
4874
- }
4875
4902
  /**
4876
4903
  * List deleted event automations created by one user, newest first.
4877
4904
  * Used only to keep historical runs after the live automation list drops the row.
@@ -5147,4 +5174,892 @@ async function saveScheduledAutomation(db, task, expectedRevision, editedBy) {
5147
5174
  async function saveScheduledAutomationInLock(db, task, current) {
5148
5175
  await writeScheduledAutomation(db, requireStoredTask(task), current);
5149
5176
  }
5150
- export { recordAgentsInstructionsUpdated as $, buildDeterministicTurnId as $n, appendExclusiveInboundMessage as $t, CORE_EVENT_NAMESPACE as A, TurnToolCallLimitExceededError as An, PluginCredentialFailureError as At, registeredEventTypeSchema as B, instructionTextForProjection as Bn, wasPendingAuthStopped as Bt, automationVersionOf as C, requestConversationWork as Cn, isHumanConversationMessage as Ct, AutomationConflictError as D, TURN_LIMIT_DISABLED_REASON as Dn, turnHasReply as Dt, sameDefinitionValue as E, stopConversationWork as En, recordDeliveredAssistantMessage as Et, eventNamespaceSchema as F, extractAssistantText as Fn, AuthorizationPauseError as Ft, commitMessages as G, normalizeToolNameFromResult as Gn, completeTurnRecord as Gt, requireEventIdentifier as H, isContinuablePiBoundary as Hn, loadTurnCheckpoint as Ht, eventWatchUnavailable as I, getPiMessageRole as In, abandonReplacedPendingAuth as It, loadLastModelProfile as J, trimTrailingAssistantMessages as Jn, listTurnSummaries as Jt, loadConnectedMcpProviders as K, retainRuntimeTurnContext as Kn, failTurnRecord as Kt, hasEventCatalogEntries as L, getTerminalAssistantMessages as Ln, canReusePendingAuthLink as Lt, describeRegisteredEventTrigger as M, createTurnLimitGate as Mn, authorizationId as Mt, eventCatalogEntry as N, isTurnExecutionLimitExceededError as Nn, AuthPausePersistenceError as Nt, automationRevision as O, TurnBudgetExceededError as On, upsertConversationMessage as Ot, eventGuidance as P, persistWithRetry as Pn, AuthorizationFlowDisabledError as Pt, openConversationProjection as Q, buildDeterministicAssistantMessageId as Qn, ackMessages as Qt, registeredEventMatchSchema as R, getUserMessageInstructionText as Rn, clearPendingAuth as Rt, automationDefinition as S, requestAnotherSlice as Sn, getConversationMessageSlackTs as St, readAutomationVersion as T, steerWebPendingMessages as Tn, normalizeConversationText as Tt, requireValidEventTrigger as U, isToolResultError as Un, saveTurnCheckpoint as Ut, registeredResourceTypeSchema as V, isAssistantMessage as Vn, continuableMessages as Vt, commitAcceptedReply as W, isToolResultMessage as Wn, abandonTurnRecord as Wt, loadTurnProjection as X, SESSION_CONTEXT_SECTION_TAG as Xn, CONVERSATION_WORK_CHECK_IN_INTERVAL_MS as Xt, loadProjection as Y, REQUEST_CONTEXT_SECTION_TAG as Yn, recordTurnSummary as Yt, loadTurnRoute as Z, TURN_CONTEXT_TAG as Zn, CONVERSATION_WORK_STALE_ENQUEUE_MS as Zt, parseEventAutomationRow as _, markConversationWorkEnqueued as _n, resolveTaskTitle as _t, saveScheduledAutomation as a, clearExpiredConversationLease as an, getAgentContextCompactionTriggerTokens as ar, recordMcpProviderConnected as at, admitEventAutomationRun as b, releaseConversationWork as bn, createConversationMemoryService as bt, withScheduledAutomationLock as c, deadLetterAttempt as cn, isExperimentalFeatureEnabled as cr, recordTurnRoute as ct, createEventAutomation as d, getConversationWorkState as dn, configValueSchema as dr, persistConversationMessages as dt, appendInboundMessage as en, SubscribedReplyReason as er, recordAttachmentsDelivered as et, deleteEventAutomation as f, hasConversationStop as fn, historyItemFromPiMessage as fr, projectConversationMessages as ft, listEventAutomationsForTeam as g, listActiveConversationIds as gn, fallbackShortTitle as gt, listDeletedEventAutomationsCreatedBy as h, isInvalidConversationRecordError as hn, setConfigDefaults as hr, settleConversationTitleWork as ht, readScheduledAutomation as i, clearConsumedConversationWake as in, estimateTextTokens as ir, recordGuardianActionReviewed as it, automationEventCatalog as j, buildTurnErrorResponse as jn, createPluginAuthOrchestration as jt, requireAutomationRevision as k, TurnSliceLimitExceededError as kn, createPluginEgress as kt, blockEventAutomation as l, drainConversationMailbox as ln, setExperimentalFeatures as lr, appendConversationMessages as lt, getEventAutomation as m, isFinalAttempt as mn, getConfigDefaults as mr, resolveConversationTitle as mt, listScheduledAutomationsForTeam as n, cancelHumanFacingPendingMessages as nn, getSubscribedReplyPreflightDecision as nr, recordAuthorizationCompleted as nt, saveScheduledAutomationInLock as o, completeConversationStop as on, getAgentContextInputLimitTokens as or, recordSubscribedReplyRoute as ot, findMatchingEventAutomations as p, hasRunnableConversationWork as pn, piMessageFromHistoryItem as pr, persistConversationMessageSummaries as pt, loadConversationProjection as q, stripRuntimeTurnContext as qn, getTurnRecord as qt, parseScheduledAutomationRow as r, checkInConversationWork as rn, getThreadStopDecision as rr, recordAuthorizationRequested as rt, scheduledRunId as s, completeConversationWork as sn, getExperimentalFeatures as sr, recordToolExecutionStarted as st, createScheduledAutomation as t, beginConversationResume as tn, decideSubscribedThreadReply as tr, recordAuthenticationLinked as tt, collectEventAutomationMatchKeys as u, getConversation as un, createDurableLocationConfigurationService as ur, hydrateConversationMessages as ut, saveEventAutomation as v, recordAttemptFailure as vn, appendThreadContextMessages as vt, listAutomationVersions as w, startConversationWork as wn, markConversationMessage as wt, eventAutomationRunLimitReason as x, removeActiveConversation as xn, estimateConversationContextTokens as xt, setEventAutomationStatus as y, recordConversationRetry as yn, buildConversationContext as yt, registeredEventTriggerProblems as z, hasRuntimeTurnContext as zn, getConversationPendingAuth as zt };
5177
+ /** Mark one live scheduled automation deleted while keeping its history. */
5178
+ async function deleteScheduledAutomation(db, id, nowMs = Date.now()) {
5179
+ return await withScheduledAutomationLock(db, id, async (tx) => {
5180
+ const current = await readScheduledAutomation(tx, id);
5181
+ if (!current || current.status === "deleted") return void 0;
5182
+ const deleted = requireStoredTask({
5183
+ ...current,
5184
+ nextRunAtMs: void 0,
5185
+ runNowAtMs: void 0,
5186
+ status: "deleted",
5187
+ updatedAtMs: nowMs
5188
+ });
5189
+ await writeScheduledAutomation(tx, deleted, current);
5190
+ return deleted;
5191
+ });
5192
+ }
5193
+ // src/api/conversations/aggregate.ts
5194
+ function usageTokenValue(source, field) {
5195
+ return sql`CASE
5196
+ WHEN ${source.usage}->>${field} IS NOT NULL
5197
+ THEN (${source.usage}->>${field})::double precision
5198
+ ELSE NULL
5199
+ END`;
5200
+ }
5201
+ function tokenValue(source) {
5202
+ return sql`
5203
+ CASE
5204
+ WHEN COALESCE(
5205
+ ${source.usage}->>'inputTokens',
5206
+ ${source.usage}->>'outputTokens',
5207
+ ${source.usage}->>'cachedInputTokens',
5208
+ ${source.usage}->>'cacheCreationTokens'
5209
+ ) IS NOT NULL
5210
+ THEN COALESCE((${source.usage}->>'inputTokens')::double precision, 0)
5211
+ + COALESCE((${source.usage}->>'outputTokens')::double precision, 0)
5212
+ + COALESCE((${source.usage}->>'cachedInputTokens')::double precision, 0)
5213
+ + COALESCE((${source.usage}->>'cacheCreationTokens')::double precision, 0)
5214
+ WHEN ${source.usage}->>'totalTokens' IS NOT NULL
5215
+ THEN (${source.usage}->>'totalTokens')::double precision
5216
+ ELSE NULL
5217
+ END
5218
+ `;
5219
+ }
5220
+ function costValue(source) {
5221
+ return conversationUsageCostExpr(source.usage);
5222
+ }
5223
+ /** Read one conversation row's usage cost as a SQL expression. */
5224
+ function conversationUsageCostExpr(usage) {
5225
+ return sql`
5226
+ CASE
5227
+ WHEN ${usage}->'cost'->>'total' IS NOT NULL
5228
+ THEN (${usage}->'cost'->>'total')::double precision
5229
+ WHEN COALESCE(
5230
+ ${usage}->'cost'->>'input',
5231
+ ${usage}->'cost'->>'output',
5232
+ ${usage}->'cost'->>'cacheRead',
5233
+ ${usage}->'cost'->>'cacheWrite'
5234
+ ) IS NOT NULL
5235
+ THEN COALESCE((${usage}->'cost'->>'input')::double precision, 0)
5236
+ + COALESCE((${usage}->'cost'->>'output')::double precision, 0)
5237
+ + COALESCE((${usage}->'cost'->>'cacheRead')::double precision, 0)
5238
+ + COALESCE((${usage}->'cost'->>'cacheWrite')::double precision, 0)
5239
+ ELSE NULL
5240
+ END
5241
+ `;
5242
+ }
5243
+ /** Select complete conversation metrics inside the database instead of materializing source rows. */
5244
+ function conversationAggregateColumns(sources) {
5245
+ const metrics = sources?.metrics ?? juniorConversations;
5246
+ const roots = sources?.roots ?? juniorConversations;
5247
+ const conversationCount = sources ? sql`COUNT(DISTINCT ${roots.conversationId})` : sql`COUNT(*)`;
5248
+ return {
5249
+ active: sql`${conversationCount} FILTER (
5250
+ WHERE ${roots.executionStatus} NOT IN ('idle', 'failed')
5251
+ )::integer`,
5252
+ conversations: sql`${conversationCount}::integer`,
5253
+ cachedInputTokens: sql`SUM(${usageTokenValue(metrics, "cachedInputTokens")})::double precision`,
5254
+ cacheCreationTokens: sql`SUM(${usageTokenValue(metrics, "cacheCreationTokens")})::double precision`,
5255
+ costUsd: sql`SUM(${costValue(metrics)})::double precision`,
5256
+ durationMs: sql`COALESCE(SUM(${metrics.durationMs}), 0)::double precision`,
5257
+ failed: sql`${conversationCount} FILTER (
5258
+ WHERE ${roots.executionStatus} = 'failed'
5259
+ )::integer`,
5260
+ inputTokens: sql`SUM(${usageTokenValue(metrics, "inputTokens")})::double precision`,
5261
+ tokens: sql`SUM(${tokenValue(metrics)})::double precision`
5262
+ };
5263
+ }
5264
+ /** Select the complete first/last activity range for a grouped conversation aggregate. */
5265
+ function conversationRangeColumns() {
5266
+ return {
5267
+ firstSeenAt: sql`MIN(${juniorConversations.createdAt})`.mapWith(juniorConversations.createdAt),
5268
+ lastSeenAt: sql`MAX(${juniorConversations.lastActivityAt})`.mapWith(juniorConversations.lastActivityAt)
5269
+ };
5270
+ }
5271
+ /** Count distinct UTC activity dates without loading conversation timestamps into the app. */
5272
+ function conversationActiveDaysColumn() {
5273
+ return sql`COUNT(DISTINCT (${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC')::date)::integer`;
5274
+ }
5275
+ // src/chat/automations/execution-stats.ts
5276
+ const AUTOMATION_EXECUTION_STATUSES = [
5277
+ "blocked",
5278
+ "completed",
5279
+ "failed"
5280
+ ];
5281
+ /** An execution is a run with an outcome. Skipped runs are not executions. */
5282
+ function isExecution() {
5283
+ return and(inArray(juniorAutomationRuns.status, [...AUTOMATION_EXECUTION_STATUSES]), isNotNull(juniorAutomationRuns.completedAtMs));
5284
+ }
5285
+ function addUsd(current, next) {
5286
+ return Math.round((current + next) * 0xe8d4a51000) / 0xe8d4a51000;
5287
+ }
5288
+ const EMPTY_RUN_WINDOWS = {
5289
+ 1: 0,
5290
+ 7: 0,
5291
+ 30: 0,
5292
+ 90: 0
5293
+ };
5294
+ /** Empty run or cost windows for automations with no execution history. */
5295
+ function emptyAutomationRunWindows() {
5296
+ return { ...EMPTY_RUN_WINDOWS };
5297
+ }
5298
+ function utcDate(nowMs) {
5299
+ return new Date(nowMs).toISOString().slice(0, 10);
5300
+ }
5301
+ function emptyDay(date) {
5302
+ return {
5303
+ costUsd: 0,
5304
+ date,
5305
+ event: 0,
5306
+ scheduled: 0
5307
+ };
5308
+ }
5309
+ function emptyStatusDay(date) {
5310
+ return {
5311
+ blocked: 0,
5312
+ completed: 0,
5313
+ date,
5314
+ failed: 0
5315
+ };
5316
+ }
5317
+ /**
5318
+ * Return the Conversation id when that Conversation exists, else null. A
5319
+ * dispatch can end before it creates its Conversation, and the run's
5320
+ * `conversation_id` is a foreign key.
5321
+ */
5322
+ function existingConversationId(conversationId) {
5323
+ return sql`(select ${juniorConversations.conversationId} from ${juniorConversations} where ${juniorConversations.conversationId} = ${conversationId})`;
5324
+ }
5325
+ /**
5326
+ * Record the outcome of one Automation run. A run that already has an outcome
5327
+ * keeps it. A failed write is logged, and the heartbeat settles a started
5328
+ * event run later.
5329
+ */
5330
+ async function recordAutomationExecution(automationId, options) {
5331
+ const nowMs = options.nowMs ?? Date.now();
5332
+ const status = options.status ?? "completed";
5333
+ const outcome = {
5334
+ completedAtMs: nowMs,
5335
+ errorMessage: options.errorMessage ?? null,
5336
+ status
5337
+ };
5338
+ try {
5339
+ await getDb().insert(juniorAutomationRuns).values({
5340
+ id: options.executionId,
5341
+ automationId,
5342
+ conversationId: options.conversationId ? existingConversationId(options.conversationId) : null,
5343
+ ...outcome
5344
+ }).onConflictDoUpdate({
5345
+ target: juniorAutomationRuns.id,
5346
+ set: {
5347
+ ...outcome,
5348
+ conversationId: sql`coalesce(${juniorAutomationRuns.conversationId}, excluded.conversation_id)`
5349
+ },
5350
+ setWhere: inArray(juniorAutomationRuns.status, ["pending", "running"])
5351
+ });
5352
+ } catch (error) {
5353
+ logWarn("task.execution.stat_failed", {
5354
+ error,
5355
+ "app.task.execution.id": options.executionId,
5356
+ "app.task.execution.status": status,
5357
+ "app.task.execution.task_id": automationId
5358
+ });
5359
+ }
5360
+ }
5361
+ /** Load usage analytics for the given automations. */
5362
+ async function readAutomationExecutionSummaries(automationIds, options = {}) {
5363
+ if (automationIds.length === 0) return /* @__PURE__ */ new Map();
5364
+ const nowMs = options.nowMs ?? Date.now();
5365
+ const oneDayAgoMs = nowMs - 864e5;
5366
+ const sevenDaysAgoMs = nowMs - 6048e5;
5367
+ const thirtyDaysAgoMs = nowMs - 2592e6;
5368
+ const ninetyDaysAgoMs = nowMs - 7776e6;
5369
+ const db = getDb();
5370
+ const selected = and(isExecution(), inArray(juniorAutomationRuns.automationId, automationIds));
5371
+ const latest = db.selectDistinctOn([juniorAutomationRuns.automationId], {
5372
+ status: juniorAutomationRuns.status,
5373
+ conversationId: juniorAutomationRuns.conversationId,
5374
+ automationId: juniorAutomationRuns.automationId
5375
+ }).from(juniorAutomationRuns).where(selected).orderBy(juniorAutomationRuns.automationId, desc(juniorAutomationRuns.completedAtMs), desc(juniorAutomationRuns.id)).as("latest_task_execution");
5376
+ const conversationCost = conversationUsageCostExpr(juniorConversations.usage);
5377
+ const costSince = (sinceMs) => sql`coalesce(sum(${conversationCost}) filter (where ${juniorAutomationRuns.completedAtMs} >= ${sinceMs}), 0)::double precision`;
5378
+ const rows = await db.select({
5379
+ costLast1Day: costSince(oneDayAgoMs),
5380
+ costLast7Days: costSince(sevenDaysAgoMs),
5381
+ costLast30Days: costSince(thirtyDaysAgoMs),
5382
+ costLast90Days: costSince(ninetyDaysAgoMs),
5383
+ lastRunStatus: latest.status,
5384
+ lastConversationId: latest.conversationId,
5385
+ lastExecutedAtMs: max(juniorAutomationRuns.completedAtMs),
5386
+ runsLast1Day: sql`count(*) filter (where ${juniorAutomationRuns.completedAtMs} >= ${oneDayAgoMs})::int`,
5387
+ runsLast7Days: sql`count(*) filter (where ${juniorAutomationRuns.completedAtMs} >= ${sevenDaysAgoMs})::int`,
5388
+ runsLast30Days: sql`count(*) filter (where ${juniorAutomationRuns.completedAtMs} >= ${thirtyDaysAgoMs})::int`,
5389
+ runsLast90Days: sql`count(*) filter (where ${juniorAutomationRuns.completedAtMs} >= ${ninetyDaysAgoMs})::int`,
5390
+ automationId: juniorAutomationRuns.automationId,
5391
+ totalRuns: sql`count(*)::int`
5392
+ }).from(juniorAutomationRuns).innerJoin(latest, eq(latest.automationId, juniorAutomationRuns.automationId)).leftJoin(juniorConversations, executionConversation()).where(selected).groupBy(juniorAutomationRuns.automationId, latest.conversationId, latest.status);
5393
+ return new Map(rows.map((row) => [row.automationId, {
5394
+ costUsd: {
5395
+ 1: addUsd(0, row.costLast1Day),
5396
+ 7: addUsd(0, row.costLast7Days),
5397
+ 30: addUsd(0, row.costLast30Days),
5398
+ 90: addUsd(0, row.costLast90Days)
5399
+ },
5400
+ lastRunStatus: row.lastRunStatus,
5401
+ ...row.lastConversationId ? { lastConversationId: row.lastConversationId } : void 0,
5402
+ ...row.lastExecutedAtMs !== null ? { lastExecutedAtMs: row.lastExecutedAtMs } : void 0,
5403
+ runs: {
5404
+ 1: row.runsLast1Day,
5405
+ 7: row.runsLast7Days,
5406
+ 30: row.runsLast30Days,
5407
+ 90: row.runsLast90Days
5408
+ },
5409
+ totalRuns: row.totalRuns
5410
+ }]));
5411
+ }
5412
+ /** Load a fixed trailing window of completed executions stacked by task type. */
5413
+ async function readAutomationExecutionDays(dayCount = 90, options = {}) {
5414
+ const end = utcDate(options.nowMs ?? Date.now());
5415
+ const endMs = Date.parse(`${end}T23:59:59.999Z`);
5416
+ const startMs = Date.parse(`${end}T00:00:00.000Z`) - (dayCount - 1) * 864e5;
5417
+ const executionDate = sql`to_char(to_timestamp(${juniorAutomationRuns.completedAtMs} / 1000.0) at time zone 'UTC', 'YYYY-MM-DD')`;
5418
+ const rows = await readCompletedExecutionBuckets({
5419
+ access: options.access,
5420
+ bucket: executionDate,
5421
+ endMs,
5422
+ startMs
5423
+ });
5424
+ const byDate = /* @__PURE__ */ new Map();
5425
+ for (let offset = 0; offset < dayCount; offset += 1) {
5426
+ const date = utcDate(startMs + offset * 864e5);
5427
+ byDate.set(date, emptyDay(date));
5428
+ }
5429
+ for (const row of rows) {
5430
+ const day = byDate.get(row.date);
5431
+ if (!day) continue;
5432
+ if (row.kind === "scheduled") day.scheduled = row.count;
5433
+ else if (row.kind === "event") day.event = row.count;
5434
+ if (row.costUsd !== null && row.costUsd > 0) day.costUsd = addUsd(day.costUsd, row.costUsd);
5435
+ }
5436
+ return [...byDate.values()];
5437
+ }
5438
+ /** Load a fixed trailing window of completed executions stacked by hour. */
5439
+ async function readAutomationExecutionHours(hourCount = 168, options = {}) {
5440
+ const nowMs = options.nowMs ?? Date.now();
5441
+ const end = new Date(nowMs);
5442
+ end.setUTCMinutes(0, 0, 0);
5443
+ const endMs = end.getTime() + 3599999;
5444
+ const startMs = end.getTime() - (hourCount - 1) * 36e5;
5445
+ const executionHour = sql`to_char(to_timestamp(${juniorAutomationRuns.completedAtMs} / 1000.0) at time zone 'UTC', 'YYYY-MM-DD"T"HH24')`;
5446
+ const rows = await readCompletedExecutionBuckets({
5447
+ access: options.access,
5448
+ bucket: executionHour,
5449
+ endMs,
5450
+ startMs
5451
+ });
5452
+ const byHour = /* @__PURE__ */ new Map();
5453
+ for (let offset = 0; offset < hourCount; offset += 1) {
5454
+ const date = new Date(startMs + offset * 36e5).toISOString().slice(0, 13);
5455
+ byHour.set(date, emptyDay(date));
5456
+ }
5457
+ for (const row of rows) {
5458
+ const hour = byHour.get(row.date);
5459
+ if (!hour) continue;
5460
+ if (row.kind === "scheduled") hour.scheduled = row.count;
5461
+ else if (row.kind === "event") hour.event = row.count;
5462
+ if (row.costUsd !== null && row.costUsd > 0) hour.costUsd = addUsd(hour.costUsd, row.costUsd);
5463
+ }
5464
+ return [...byHour.values()];
5465
+ }
5466
+ /** Count completed executions and their cost per time bucket and kind. */
5467
+ async function readCompletedExecutionBuckets(args) {
5468
+ const conversationCost = conversationUsageCostExpr(juniorConversations.usage);
5469
+ return await getDb().select({
5470
+ count: count(),
5471
+ costUsd: sql`SUM(${conversationCost})::double precision`,
5472
+ date: args.bucket,
5473
+ kind: juniorAutomations.kind
5474
+ }).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));
5475
+ }
5476
+ /** Load the Automation run linked to one conversation, if any. */
5477
+ async function readAutomationExecutionByConversationId(args) {
5478
+ const [row] = await getDb().select({
5479
+ kind: juniorAutomations.kind,
5480
+ automationId: juniorAutomationRuns.automationId
5481
+ }).from(juniorAutomationRuns).innerJoin(juniorAutomations, eq(juniorAutomations.id, juniorAutomationRuns.automationId)).where(eq(juniorAutomationRuns.conversationId, args.conversationId)).limit(1);
5482
+ return row;
5483
+ }
5484
+ /**
5485
+ * Load the display name of the Automation linked to each untitled
5486
+ * conversation. Like the Automation list, the name falls back to the first
5487
+ * line of the instruction. Titled conversations and conversations without a
5488
+ * linked Automation are absent from the result.
5489
+ */
5490
+ async function readAutomationTitlesByConversationId(conversations) {
5491
+ const titles = /* @__PURE__ */ new Map();
5492
+ const untitledIds = conversations.flatMap((conversation) => conversation.title ? [] : [conversation.conversationId]);
5493
+ if (untitledIds.length === 0) return titles;
5494
+ const rows = await getDb().select({
5495
+ conversationId: juniorAutomationRuns.conversationId,
5496
+ instruction: juniorAutomations.instruction,
5497
+ title: juniorAutomations.title
5498
+ }).from(juniorAutomationRuns).innerJoin(juniorAutomations, eq(juniorAutomations.id, juniorAutomationRuns.automationId)).where(inArray(juniorAutomationRuns.conversationId, untitledIds));
5499
+ for (const row of rows) {
5500
+ const title = row.title?.trim() || fallbackShortTitle(row.instruction, "");
5501
+ if (row.conversationId && title) titles.set(row.conversationId, title);
5502
+ }
5503
+ return titles;
5504
+ }
5505
+ /** Load newest-first executions for one automation, with conversation titles when present. */
5506
+ async function readAutomationExecutions(args) {
5507
+ 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));
5508
+ }
5509
+ /** Load newest-first executions for the supplied viewer-visible automations. */
5510
+ async function readAutomationRuns(args) {
5511
+ if (args.automationIds.length === 0) return [];
5512
+ return (await getDb().select({
5513
+ ...executionColumns,
5514
+ kind: juniorAutomations.kind,
5515
+ automationId: juniorAutomationRuns.automationId
5516
+ }).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) => ({
5517
+ ...mapAutomationExecutionRecord(row),
5518
+ kind: row.kind,
5519
+ automationId: row.automationId
5520
+ }));
5521
+ }
5522
+ /** Columns that describe one execution, its Conversation, and its version. */
5523
+ const executionColumns = {
5524
+ automationVersion: juniorAutomationRuns.automationVersion,
5525
+ conversationId: juniorAutomationRuns.conversationId,
5526
+ credentialMode: sql`${juniorAutomationVersions.definition}->>'credentialMode'`,
5527
+ durationMs: juniorConversations.durationMs,
5528
+ errorMessage: juniorAutomationRuns.errorMessage,
5529
+ executedAtMs: juniorAutomationRuns.completedAtMs,
5530
+ executionId: juniorAutomationRuns.id,
5531
+ scheduledForMs: juniorAutomationRuns.scheduledForMs,
5532
+ startedAtMs: juniorAutomationRuns.startedAtMs,
5533
+ status: juniorAutomationRuns.status,
5534
+ title: juniorConversations.title,
5535
+ usage: juniorConversations.usage
5536
+ };
5537
+ function executionConversation() {
5538
+ return eq(juniorConversations.conversationId, juniorAutomationRuns.conversationId);
5539
+ }
5540
+ function executionVersion() {
5541
+ return and(eq(juniorAutomationVersions.automationId, juniorAutomationRuns.automationId), eq(juniorAutomationVersions.version, juniorAutomationRuns.automationVersion));
5542
+ }
5543
+ function isoTime(ms) {
5544
+ return ms === null ? void 0 : new Date(ms).toISOString();
5545
+ }
5546
+ function mapAutomationExecutionRecord(row) {
5547
+ const title = row.title?.trim();
5548
+ const costUsd = agentTurnCostUsd(row.usage ?? void 0);
5549
+ const totalTokens = agentTurnTotalTokens(row.usage ?? void 0);
5550
+ const durationMs = typeof row.durationMs === "number" && Number.isFinite(row.durationMs) && row.durationMs > 0 ? row.durationMs : void 0;
5551
+ const error = row.errorMessage?.trim();
5552
+ const scheduledFor = isoTime(row.scheduledForMs);
5553
+ const startedAt = isoTime(row.startedAtMs);
5554
+ return {
5555
+ ...row.automationVersion !== null ? { automationVersion: row.automationVersion } : void 0,
5556
+ ...row.conversationId ? { conversationId: row.conversationId } : void 0,
5557
+ ...costUsd !== void 0 ? { costUsd } : void 0,
5558
+ ...row.credentialMode === "creator" || row.credentialMode === "system" ? { credentialMode: row.credentialMode } : void 0,
5559
+ ...durationMs !== void 0 ? { durationMs } : void 0,
5560
+ ...error ? { error } : void 0,
5561
+ executedAt: new Date(row.executedAtMs).toISOString(),
5562
+ executionId: row.executionId,
5563
+ ...scheduledFor ? { scheduledFor } : void 0,
5564
+ ...startedAt ? { startedAt } : void 0,
5565
+ status: row.status,
5566
+ ...title ? { title } : void 0,
5567
+ ...totalTokens !== void 0 ? { totalTokens } : void 0
5568
+ };
5569
+ }
5570
+ /** Load a fixed trailing window of terminal executions for one automation by status. */
5571
+ async function readAutomationExecutionStatusDays(args) {
5572
+ const dayCount = args.dayCount ?? 90;
5573
+ const end = utcDate(args.nowMs ?? Date.now());
5574
+ const endMs = Date.parse(`${end}T23:59:59.999Z`);
5575
+ const startMs = Date.parse(`${end}T00:00:00.000Z`) - (dayCount - 1) * 864e5;
5576
+ const executionDate = sql`to_char(to_timestamp(${juniorAutomationRuns.completedAtMs} / 1000.0) at time zone 'UTC', 'YYYY-MM-DD')`;
5577
+ const rows = await readExecutionStatusBuckets({
5578
+ automationId: args.automationId,
5579
+ bucket: executionDate,
5580
+ endMs,
5581
+ startMs
5582
+ });
5583
+ const byDate = /* @__PURE__ */ new Map();
5584
+ for (let offset = 0; offset < dayCount; offset += 1) {
5585
+ const date = utcDate(startMs + offset * 864e5);
5586
+ byDate.set(date, emptyStatusDay(date));
5587
+ }
5588
+ for (const row of rows) {
5589
+ const day = byDate.get(row.date);
5590
+ if (!day) continue;
5591
+ if (row.status === "completed") day.completed = row.count;
5592
+ else if (row.status === "failed") day.failed = row.count;
5593
+ else if (row.status === "blocked") day.blocked = row.count;
5594
+ }
5595
+ return [...byDate.values()];
5596
+ }
5597
+ /** Load a fixed trailing window of terminal executions for one automation by hour. */
5598
+ async function readAutomationExecutionStatusHours(args) {
5599
+ const hourCount = args.hourCount ?? 168;
5600
+ const nowMs = args.nowMs ?? Date.now();
5601
+ const end = new Date(nowMs);
5602
+ end.setUTCMinutes(0, 0, 0);
5603
+ const endMs = end.getTime() + 3599999;
5604
+ const startMs = end.getTime() - (hourCount - 1) * 36e5;
5605
+ const executionHour = sql`to_char(to_timestamp(${juniorAutomationRuns.completedAtMs} / 1000.0) at time zone 'UTC', 'YYYY-MM-DD"T"HH24')`;
5606
+ const rows = await readExecutionStatusBuckets({
5607
+ automationId: args.automationId,
5608
+ bucket: executionHour,
5609
+ endMs,
5610
+ startMs
5611
+ });
5612
+ const byHour = /* @__PURE__ */ new Map();
5613
+ for (let offset = 0; offset < hourCount; offset += 1) {
5614
+ const date = new Date(startMs + offset * 36e5).toISOString().slice(0, 13);
5615
+ byHour.set(date, emptyStatusDay(date));
5616
+ }
5617
+ for (const row of rows) {
5618
+ const hour = byHour.get(row.date);
5619
+ if (!hour) continue;
5620
+ if (row.status === "completed") hour.completed = row.count;
5621
+ else if (row.status === "failed") hour.failed = row.count;
5622
+ else if (row.status === "blocked") hour.blocked = row.count;
5623
+ }
5624
+ return [...byHour.values()];
5625
+ }
5626
+ /** Count one automation's executions per time bucket and status. */
5627
+ async function readExecutionStatusBuckets(args) {
5628
+ return await getDb().select({
5629
+ count: count(),
5630
+ date: args.bucket,
5631
+ status: juniorAutomationRuns.status
5632
+ }).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));
5633
+ }
5634
+ // src/chat/automations/blocked-notice.ts
5635
+ function buildBlockedNoticeText(automationId, title, reason) {
5636
+ const url = getDashboardTaskLink(automationId);
5637
+ return [
5638
+ `Your automation *${escapeSlackMrkdwnText(title)}* is blocked. It won't run again until you resume it.`,
5639
+ `> ${escapeSlackMrkdwnText(reason.replace(/\s+/g, " ").trim())}`,
5640
+ url ? `Fix the problem, then ${formatSlackLink(url, "resume it")}.` : "Fix the problem, then ask me to resume it."
5641
+ ].join("\n");
5642
+ }
5643
+ /**
5644
+ * Tell an Automation's creator in a direct message that a run blocked it,
5645
+ * and why. Call this once, after the Automation itself is stored as blocked,
5646
+ * so the dashboard matches the notice. The notice is best-effort: the reason
5647
+ * also shows on the dashboard and in the Automation tools.
5648
+ */
5649
+ async function notifyAutomationBlocked(ref) {
5650
+ await runBestEffort(async () => {
5651
+ const task = ref.kind === "scheduled" ? await readScheduledAutomation(getDb(), ref.automationId) : await getEventAutomation(getDb(), ref.automationId);
5652
+ if (!task) return;
5653
+ const title = task.title ?? fallbackShortTitle(task.task.text, ref.kind === "scheduled" ? "Scheduled automation" : "Event automation");
5654
+ await postSlackMessage({
5655
+ channelId: await openSlackDirectMessage(task.createdBy.slackUserId),
5656
+ text: buildBlockedNoticeText(ref.automationId, title, ref.reason)
5657
+ });
5658
+ }, "automation.blocked_notice.failed", {
5659
+ "app.task.type": ref.kind,
5660
+ ...ref.dispatchId ? { "app.dispatch.id": ref.dispatchId } : void 0
5661
+ });
5662
+ }
5663
+ // src/chat/agent-dispatch/store.ts
5664
+ const DISPATCH_PREFIX = "junior:agent_dispatch";
5665
+ const DISPATCH_LOCK_TTL_MS = 6e5;
5666
+ const DISPATCH_MAILBOX_APPEND_INDEX_LOCK_TTL_MS = 1e4;
5667
+ const nonEmptyExactStringSchema = z.string().min(1).refine((value) => value === value.trim() && value.toLowerCase() !== "unknown");
5668
+ const pendingDispatchMailboxAppendIndexSchema = z.array(nonEmptyExactStringSchema);
5669
+ const dispatchStatusSchema = z.enum([
5670
+ "pending",
5671
+ "running",
5672
+ "awaiting_resume",
5673
+ "completed",
5674
+ "failed",
5675
+ "blocked"
5676
+ ]);
5677
+ const dispatchActorSchema = z.object({
5678
+ platform: z.literal("system"),
5679
+ name: nonEmptyExactStringSchema
5680
+ }).strict();
5681
+ const dispatchRecordSchema = z.object({
5682
+ actor: dispatchActorSchema,
5683
+ createdAtMs: z.number().finite(),
5684
+ credentialSubject: credentialSubjectSchema.optional(),
5685
+ destination: destinationSchema,
5686
+ destinationVisibility: destinationVisibilitySchema,
5687
+ errorMessage: z.string().optional(),
5688
+ id: nonEmptyExactStringSchema,
5689
+ idempotencyKey: z.string().min(1),
5690
+ input: z.string().min(1),
5691
+ metadata: z.record(z.string(), z.string()).optional(),
5692
+ plugin: nonEmptyExactStringSchema,
5693
+ replyAttribution: replyAttributionSchema.optional(),
5694
+ resultMessageTs: z.string().optional(),
5695
+ source: sourceSchema,
5696
+ outcomes: z.array(taskOutcomeSchema).max(5).optional(),
5697
+ status: dispatchStatusSchema,
5698
+ updatedAtMs: z.number().finite()
5699
+ }).strict().superRefine((record, ctx) => {
5700
+ if (!isSlackDestination(record.destination)) {
5701
+ ctx.addIssue({
5702
+ code: z.ZodIssueCode.custom,
5703
+ message: "Dispatch destination platform must be slack",
5704
+ path: ["destination"]
5705
+ });
5706
+ return;
5707
+ }
5708
+ const subject = record.credentialSubject;
5709
+ if (!subject) return;
5710
+ if (subject.allowedWhen === "private-direct-conversation") {
5711
+ if (!record.destination.channelId.startsWith("D")) {
5712
+ ctx.addIssue({
5713
+ code: z.ZodIssueCode.custom,
5714
+ message: "Dispatch credentialSubject requires a private direct Slack destination",
5715
+ path: ["credentialSubject"]
5716
+ });
5717
+ return;
5718
+ }
5719
+ if (subject.binding.type !== "slack-direct-conversation" || subject.binding.teamId !== record.destination.teamId || subject.binding.channelId !== record.destination.channelId) ctx.addIssue({
5720
+ code: z.ZodIssueCode.custom,
5721
+ message: "Dispatch credentialSubject binding must match destination",
5722
+ path: ["credentialSubject", "binding"]
5723
+ });
5724
+ return;
5725
+ }
5726
+ if (subject.allowedWhen === "timer-watch") {
5727
+ ctx.addIssue({
5728
+ code: z.ZodIssueCode.custom,
5729
+ message: "Dispatch credentialSubject cannot use a timer Watch binding",
5730
+ path: ["credentialSubject", "allowedWhen"]
5731
+ });
5732
+ return;
5733
+ }
5734
+ if (subject.binding.type !== subject.allowedWhen || subject.binding.plugin !== record.plugin || subject.binding.taskId !== subject.taskId) ctx.addIssue({
5735
+ code: z.ZodIssueCode.custom,
5736
+ message: "Dispatch credentialSubject binding must match task",
5737
+ path: ["credentialSubject", "binding"]
5738
+ });
5739
+ });
5740
+ /** Return the durable key for one plugin-facing dispatch projection. */
5741
+ function getDispatchStorageKey(id) {
5742
+ return `${DISPATCH_PREFIX}:record:${id}`;
5743
+ }
5744
+ function dispatchLockKey(id) {
5745
+ return `${DISPATCH_PREFIX}:lock:${id}`;
5746
+ }
5747
+ function pendingDispatchMailboxAppendIndexKey() {
5748
+ return `${DISPATCH_PREFIX}:incomplete`;
5749
+ }
5750
+ function pendingDispatchMailboxAppendIndexLockKey() {
5751
+ return `${DISPATCH_PREFIX}:incomplete:lock`;
5752
+ }
5753
+ function normalizeMetadata(metadata) {
5754
+ if (!metadata) return;
5755
+ const entries = Object.entries(metadata).filter((entry) => typeof entry[0] === "string" && typeof entry[1] === "string");
5756
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
5757
+ }
5758
+ const DISPATCH_ID_PREFIX = "agent-dispatch:";
5759
+ /** Return the stable dispatch id for one plugin idempotency key. */
5760
+ function buildDispatchId(plugin, idempotencyKey) {
5761
+ return `dispatch_${createHash("sha256").update(plugin).update("\0").update(idempotencyKey).digest("hex").slice(0, 32)}`;
5762
+ }
5763
+ /** Parse one durable dispatch projection record. */
5764
+ function parseDispatchRecord(value) {
5765
+ const parsed = dispatchRecordSchema.safeParse(value);
5766
+ return parsed.success ? parsed.data : void 0;
5767
+ }
5768
+ /** Return the isolated durable conversation id for one dispatch. */
5769
+ function getDispatchConversationId(dispatch) {
5770
+ return `${DISPATCH_ID_PREFIX}${dispatch.id}`;
5771
+ }
5772
+ /** Return the stable synthetic input message id for one dispatch turn. */
5773
+ function getDispatchInputMessageId(dispatchId) {
5774
+ return `${DISPATCH_ID_PREFIX}${dispatchId}`;
5775
+ }
5776
+ /**
5777
+ * Return the dispatch id of a dispatch Conversation, such as an Automation
5778
+ * run. Other Conversations have no dispatch.
5779
+ */
5780
+ function getDispatchIdForConversation(conversationId) {
5781
+ if (!conversationId.startsWith(DISPATCH_ID_PREFIX)) return void 0;
5782
+ return conversationId.slice(15) || void 0;
5783
+ }
5784
+ /** Return the stable turn id used by every run of one dispatch. */
5785
+ function getDispatchTurnId(dispatchId) {
5786
+ return `dispatch:${dispatchId}`;
5787
+ }
5788
+ function toDispatchProjection(record) {
5789
+ return {
5790
+ id: record.id,
5791
+ status: record.status,
5792
+ ...record.resultMessageTs ? { resultMessageTs: record.resultMessageTs } : void 0,
5793
+ ...record.errorMessage ? { errorMessage: record.errorMessage } : void 0
5794
+ };
5795
+ }
5796
+ /** Gate recovery to dispatches that can still make progress. */
5797
+ function isTerminalDispatchStatus(status) {
5798
+ return status === "completed" || status === "failed" || status === "blocked";
5799
+ }
5800
+ /** Serialize dispatch projection mutations. */
5801
+ async function withDispatchLock(dispatchId, task) {
5802
+ const state = getStateAdapter();
5803
+ await state.connect();
5804
+ const lock = await state.acquireLock(dispatchLockKey(dispatchId), DISPATCH_LOCK_TTL_MS);
5805
+ if (!lock) throw new Error(`Could not acquire dispatch lock for ${dispatchId}`);
5806
+ try {
5807
+ return await task(state);
5808
+ } finally {
5809
+ await state.releaseLock(lock);
5810
+ }
5811
+ }
5812
+ async function putRecord(state, record) {
5813
+ const parsed = dispatchRecordSchema.safeParse(record);
5814
+ if (!parsed.success) throw new Error(`Dispatch record is invalid: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"} ${issue.message}`).join("; ")}`);
5815
+ const next = parsed.data;
5816
+ await state.set(getDispatchStorageKey(next.id), next, JUNIOR_THREAD_STATE_TTL_MS);
5817
+ }
5818
+ async function updatePendingDispatchMailboxAppendIndex(state, update) {
5819
+ const lock = await state.acquireLock(pendingDispatchMailboxAppendIndexLockKey(), DISPATCH_MAILBOX_APPEND_INDEX_LOCK_TTL_MS);
5820
+ if (!lock) throw new Error("Could not acquire dispatch mailbox append index lock");
5821
+ try {
5822
+ const stored = await state.get(pendingDispatchMailboxAppendIndexKey());
5823
+ const ids = stored === void 0 || stored === null ? [] : pendingDispatchMailboxAppendIndexSchema.parse(stored);
5824
+ const next = [...new Set(update([...new Set(ids)]))].slice(-1e4);
5825
+ if (next.length === ids.length && next.every((id, index) => id === ids[index])) return;
5826
+ await state.set(pendingDispatchMailboxAppendIndexKey(), next, JUNIOR_THREAD_STATE_TTL_MS);
5827
+ } finally {
5828
+ await state.releaseLock(lock);
5829
+ }
5830
+ }
5831
+ /** Load dispatch state for conversation work and plugin projections. */
5832
+ async function getDispatchRecord(id) {
5833
+ const state = getStateAdapter();
5834
+ await state.connect();
5835
+ return parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
5836
+ }
5837
+ /** Create a dispatch idempotently within its owner namespace. */
5838
+ async function createOrGetDispatch(args) {
5839
+ const id = buildDispatchId(args.plugin, args.options.idempotencyKey);
5840
+ return await withDispatchLock(id, async (state) => {
5841
+ const existing = parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
5842
+ if (existing) return {
5843
+ record: existing,
5844
+ status: "already_exists"
5845
+ };
5846
+ const metadata = normalizeMetadata(args.options.metadata);
5847
+ const record = {
5848
+ actor: {
5849
+ platform: "system",
5850
+ name: args.plugin
5851
+ },
5852
+ createdAtMs: args.nowMs,
5853
+ ...args.options.credentialSubject ? { credentialSubject: args.options.credentialSubject } : void 0,
5854
+ destination: args.options.destination,
5855
+ destinationVisibility: args.options.destinationVisibility,
5856
+ id,
5857
+ idempotencyKey: args.options.idempotencyKey,
5858
+ input: args.options.input,
5859
+ ...metadata ? { metadata } : void 0,
5860
+ plugin: args.plugin,
5861
+ ...args.options.replyAttribution ? { replyAttribution: args.options.replyAttribution } : void 0,
5862
+ ...args.options.outcomes ? { outcomes: args.options.outcomes } : void 0,
5863
+ status: "pending",
5864
+ source: args.options.source,
5865
+ updatedAtMs: args.nowMs
5866
+ };
5867
+ await updatePendingDispatchMailboxAppendIndex(state, (ids) => ids.includes(id) ? ids : [...ids, id]);
5868
+ await putRecord(state, record);
5869
+ return {
5870
+ record,
5871
+ status: "created"
5872
+ };
5873
+ });
5874
+ }
5875
+ async function transitionDispatch(id, transition) {
5876
+ return await withDispatchLock(id, async (state) => {
5877
+ const current = parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
5878
+ if (!current) return;
5879
+ const transitioned = transition(current);
5880
+ if (transitioned === current) return current;
5881
+ const next = {
5882
+ ...transitioned,
5883
+ updatedAtMs: Date.now()
5884
+ };
5885
+ await putRecord(state, next);
5886
+ return next;
5887
+ });
5888
+ }
5889
+ /** Mark a dispatch projection as actively running. */
5890
+ async function markDispatchRunning(id) {
5891
+ return await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
5892
+ ...record,
5893
+ status: "running",
5894
+ errorMessage: void 0
5895
+ });
5896
+ }
5897
+ /** Project a durable awaiting-resume turn state to the plugin API. */
5898
+ async function markDispatchAwaitingResume(id) {
5899
+ return await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
5900
+ ...record,
5901
+ status: "awaiting_resume"
5902
+ });
5903
+ }
5904
+ async function recordEventAutomationExecution(previous, next, status) {
5905
+ if (!next || next.status !== status || previous?.status === status) return;
5906
+ if (next.plugin !== "junior") return;
5907
+ const eventAutomationId = next.metadata?.eventAutomationId;
5908
+ if (!eventAutomationId) return;
5909
+ await recordAutomationExecution(eventAutomationId, {
5910
+ conversationId: getDispatchConversationId(next),
5911
+ errorMessage: next.errorMessage,
5912
+ executionId: next.id,
5913
+ nowMs: next.updatedAtMs,
5914
+ status
5915
+ });
5916
+ }
5917
+ /**
5918
+ * Project a blocked turn to the plugin API. An Event automation is blocked
5919
+ * first, so a retry after a failed write still blocks it. Its creator is told
5920
+ * when the block is first stored. The heartbeat blocks a Scheduled automation
5921
+ * and tells its creator.
5922
+ */
5923
+ async function markDispatchBlocked(id, errorMessage, resultMessageTs) {
5924
+ const previous = await getDispatchRecord(id);
5925
+ const eventAutomationId = previous?.metadata?.eventAutomationId;
5926
+ if (previous?.plugin === "junior" && eventAutomationId && !isTerminalDispatchStatus(previous.status) && await blockEventAutomation(getDb(), eventAutomationId, errorMessage)) await notifyAutomationBlocked({
5927
+ automationId: eventAutomationId,
5928
+ dispatchId: id,
5929
+ kind: "event",
5930
+ reason: errorMessage
5931
+ });
5932
+ const next = await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
5933
+ ...record,
5934
+ errorMessage,
5935
+ ...resultMessageTs ? { resultMessageTs } : void 0,
5936
+ status: "blocked"
5937
+ });
5938
+ await recordEventAutomationExecution(previous, next, "blocked");
5939
+ return next;
5940
+ }
5941
+ /** Project a completed turn and its accepted Slack message to the plugin API. */
5942
+ async function markDispatchCompleted(id, resultMessageTs) {
5943
+ const previous = await getDispatchRecord(id);
5944
+ const next = await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
5945
+ ...record,
5946
+ errorMessage: void 0,
5947
+ ...resultMessageTs ? { resultMessageTs } : void 0,
5948
+ status: "completed"
5949
+ });
5950
+ await recordEventAutomationExecution(previous, next, "completed");
5951
+ return next;
5952
+ }
5953
+ /** Project a terminal conversation turn failure to the plugin API. */
5954
+ async function markDispatchFailed(id, errorMessage, resultMessageTs) {
5955
+ const previous = await getDispatchRecord(id);
5956
+ const next = await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
5957
+ ...record,
5958
+ errorMessage,
5959
+ ...resultMessageTs ? { resultMessageTs } : void 0,
5960
+ status: "failed"
5961
+ });
5962
+ await recordEventAutomationExecution(previous, next, "failed");
5963
+ return next;
5964
+ }
5965
+ /** Remove a dispatch after its durable mailbox append has succeeded. */
5966
+ async function confirmDispatchMailboxAppend(id) {
5967
+ const state = getStateAdapter();
5968
+ await state.connect();
5969
+ await updatePendingDispatchMailboxAppendIndex(state, (ids) => ids.filter((candidate) => candidate !== id));
5970
+ }
5971
+ /** List dispatches whose durable mailbox append may not have completed. */
5972
+ async function listPendingDispatchMailboxAppends() {
5973
+ const state = getStateAdapter();
5974
+ await state.connect();
5975
+ const stored = await state.get(pendingDispatchMailboxAppendIndexKey());
5976
+ return stored === void 0 || stored === null ? [] : [...new Set(pendingDispatchMailboxAppendIndexSchema.parse(stored))];
5977
+ }
5978
+ /** Load the dispatch under lock before its mailbox append becomes visible. */
5979
+ async function claimDispatchMailboxAppend(id) {
5980
+ return await withDispatchLock(id, async (state) => {
5981
+ return parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
5982
+ });
5983
+ }
5984
+ /** Return a plugin-scoped dispatch projection without exposing raw runtime state. */
5985
+ async function getPluginDispatchProjection(args) {
5986
+ const record = await getDispatchRecord(args.id);
5987
+ if (!record || record.plugin !== args.plugin) return;
5988
+ return toDispatchProjection(record);
5989
+ }
5990
+ // src/chat/services/turn-session-routing.ts
5991
+ async function loadConversationChain(conversationId, conversationStore) {
5992
+ const chain = [];
5993
+ const seen = /* @__PURE__ */ new Set();
5994
+ let cursor = conversationId;
5995
+ while (cursor && !seen.has(cursor)) {
5996
+ seen.add(cursor);
5997
+ const conversation = await conversationStore.get({ conversationId: cursor });
5998
+ if (!conversation) break;
5999
+ chain.push(conversation);
6000
+ cursor = conversation.parentConversationId;
6001
+ }
6002
+ return chain;
6003
+ }
6004
+ /**
6005
+ * Normalize a stored Slack session source against the conversation destination.
6006
+ *
6007
+ * Does not create channel or thread values from the conversation id.
6008
+ */
6009
+ function slackSourceForDestination(args) {
6010
+ if (args.source?.kind !== "slack" || args.source.channelId !== args.destination.channelId) return;
6011
+ const threadTs = args.source.threadTs?.trim();
6012
+ return {
6013
+ kind: "slack",
6014
+ visibility: args.source.visibility ?? "public",
6015
+ teamId: args.source.teamId?.trim() || args.destination.teamId,
6016
+ channelId: args.source.channelId,
6017
+ ...threadTs ? { threadTs } : void 0
6018
+ };
6019
+ }
6020
+ /**
6021
+ * Resolve the Conversation Location, Destination, and session Source.
6022
+ *
6023
+ * Reads parent Conversations when this Conversation has no Destination.
6024
+ */
6025
+ async function resolveConversationRouting(args) {
6026
+ const conversationStore = args.conversationStore ?? (await import("./viewer-DSbD6DF6.js").then((n) => n.v)).getConversationStore();
6027
+ const chain = await loadConversationChain(args.conversationId, conversationStore);
6028
+ let destination;
6029
+ let source;
6030
+ let location;
6031
+ for (const conversation of chain) {
6032
+ destination ??= conversation.destination;
6033
+ source ??= conversation.sessionSource;
6034
+ location ??= conversation.location;
6035
+ if (destination && source) break;
6036
+ }
6037
+ if (!destination) return;
6038
+ if (destination.platform === "slack") {
6039
+ const slackSource = slackSourceForDestination({
6040
+ destination,
6041
+ source
6042
+ });
6043
+ return {
6044
+ destination,
6045
+ ...location ? { location } : void 0,
6046
+ ...slackSource ? { source: slackSource } : void 0
6047
+ };
6048
+ }
6049
+ return {
6050
+ destination,
6051
+ ...location ? { location } : void 0,
6052
+ ...source ? { source } : void 0
6053
+ };
6054
+ }
6055
+ /** Require the Conversation Destination and Source from SQL. */
6056
+ async function resolveTurnSessionRouting(args) {
6057
+ const routing = await resolveConversationRouting(args);
6058
+ if (!routing?.destination || !routing.source) throw new Error(`Conversation ${args.conversationId} is missing durable routing metadata`);
6059
+ return {
6060
+ destination: routing.destination,
6061
+ ...routing.location ? { location: routing.location } : void 0,
6062
+ source: routing.source
6063
+ };
6064
+ }
6065
+ export { saveEventAutomation as $, recordAttemptFailure as $n, appendThreadContextMessages as $t, readAutomationRuns as A, recordTurnSummary as An, REQUEST_CONTEXT_SECTION_TAG as Ar, loadProjection as At, readScheduledAutomation as B, clearExpiredConversationLease as Bn, getAgentContextCompactionTriggerTokens as Br, recordMcpProviderConnected as Bt, readAutomationExecutionByConversationId as C, loadTurnCheckpoint as Cn, isContinuablePiBoundary as Cr, requireEventIdentifier as Ct, readAutomationExecutionStatusHours as D, failTurnRecord as Dn, retainRuntimeTurnContext as Dr, loadConnectedMcpProviders as Dt, readAutomationExecutionStatusDays as E, completeTurnRecord as En, normalizeToolNameFromResult as Er, commitMessages as Et, conversationRangeColumns as F, appendInboundMessage as Fn, SubscribedReplyReason as Fr, recordAttachmentsDelivered as Ft, blockEventAutomation as G, getConversation as Gn, createDurableLocationConfigurationService as Gr, hydrateConversationMessages as Gt, saveScheduledAutomationInLock as H, completeConversationWork as Hn, getExperimentalFeatures as Hr, recordToolExecutionStarted as Ht, createScheduledAutomation as I, beginConversationResume as In, decideSubscribedThreadReply as Ir, recordAuthenticationLinked as It, deleteEventAutomation as J, hasRunnableConversationWork as Jn, piMessageFromHistoryItem as Jr, persistConversationMessageSummaries as Jt, collectEventAutomationMatchKeys as K, getConversationWorkState as Kn, configValueSchema as Kr, persistConversationMessages as Kt, deleteScheduledAutomation as L, cancelHumanFacingPendingMessages as Ln, getSubscribedReplyPreflightDecision as Lr, recordAuthorizationCompleted as Lt, recordAutomationExecution as M, CONVERSATION_WORK_STALE_ENQUEUE_MS as Mn, TURN_CONTEXT_TAG as Mr, loadTurnRoute as Mt, conversationActiveDaysColumn as N, ackMessages as Nn, buildDeterministicAssistantMessageId as Nr, openConversationProjection as Nt, readAutomationExecutionSummaries as O, getTurnRecord as On, stripRuntimeTurnContext as Or, loadConversationProjection as Ot, conversationAggregateColumns as P, appendExclusiveInboundMessage as Pn, buildDeterministicTurnId as Pr, recordAgentsInstructionsUpdated as Pt, parseEventAutomationRow as Q, markConversationWorkEnqueued as Qn, resolveTaskTitle as Qt, listScheduledAutomationsForTeam as R, checkInConversationWork as Rn, getThreadStopDecision as Rr, recordAuthorizationRequested as Rt, existingConversationId as S, continuableMessages as Sn, isAssistantMessage as Sr, registeredResourceTypeSchema as St, readAutomationExecutionHours as T, abandonTurnRecord as Tn, isToolResultMessage as Tr, commitAcceptedReply as Tt, scheduledRunId as U, deadLetterAttempt as Un, isExperimentalFeatureEnabled as Ur, recordTurnRoute as Ut, saveScheduledAutomation as V, completeConversationStop as Vn, getAgentContextInputLimitTokens as Vr, recordSubscribedReplyRoute as Vt, withScheduledAutomationLock as W, drainConversationMailbox as Wn, setExperimentalFeatures as Wr, appendConversationMessages as Wt, getEventAutomation as X, isInvalidConversationRecordError as Xn, setConfigDefaults as Xr, settleConversationTitleWork as Xt, findMatchingEventAutomations as Y, isFinalAttempt as Yn, getConfigDefaults as Yr, resolveConversationTitle as Yt, listDeletedEventAutomationsCreatedBy as Z, listActiveConversationIds as Zn, fallbackShortTitle as Zt, markDispatchCompleted as _, abandonReplacedPendingAuth as _n, getPiMessageRole as _r, eventWatchUnavailable as _t, confirmDispatchMailboxAppend as a, markConversationMessage as an, startConversationWork as ar, listAutomationVersions as at, notifyAutomationBlocked as b, getConversationPendingAuth as bn, hasRuntimeTurnContext as br, registeredEventTriggerProblems as bt, getDispatchIdForConversation as c, turnHasReply as cn, TURN_LIMIT_DISABLED_REASON 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, extractAssistantText as gr, eventNamespaceSchema as gt, markDispatchAwaitingResume as h, AuthorizationFlowDisabledError as hn, persistWithRetry 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, SESSION_CONTEXT_SECTION_TAG as jr, loadTurnProjection as jt, readAutomationExecutions as k, listTurnSummaries as kn, trimTrailingAssistantMessages 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, historyItemFromPiMessage 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, getTerminalAssistantMessages as vr, hasEventCatalogEntries as vt, readAutomationExecutionDays as w, saveTurnCheckpoint as wn, isToolResultError as wr, requireValidEventTrigger as wt, emptyAutomationRunWindows as x, wasPendingAuthStopped as xn, instructionTextForProjection as xr, registeredEventTypeSchema as xt, markDispatchRunning as y, clearPendingAuth as yn, getUserMessageInstructionText as yr, registeredEventMatchSchema as yt, parseScheduledAutomationRow as z, clearConsumedConversationWake as zn, estimateTextTokens as zr, recordGuardianActionReviewed as zt };