@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,1074 +0,0 @@
1
- import { t as __exportAll } from "./rolldown-runtime-nwUcYXjP.js";
2
- import { Bn as instructionTextForProjection, Gn as normalizeToolNameFromResult, In as getPiMessageRole, Un as isToolResultError, Wn as isToolResultMessage, gt as fallbackShortTitle, i as readScheduledAutomation, l as blockEventAutomation, m as getEventAutomation, qn as stripRuntimeTurnContext, qt as getTurnRecord, ut as hydrateConversationMessages } from "./tasks-0ilmuCux.js";
3
- import { _ as runBestEffort, h as logWarn } from "./logging-DuZ1A4ab.js";
4
- import { p as parseSlackMessageTs } from "./actor-DZSokpwj.js";
5
- import { D as agentTurnTotalTokens, E as agentTurnCostUsd, f as getConversationStore, p as getDb, r as readActorIdentity } from "./viewer-DrN3oC2X.js";
6
- import { s as juniorConversations } from "./agent-invocations-CxKldAeB.js";
7
- import { a as juniorAutomationRuns, o as juniorAutomationVersions, s as juniorAutomations } from "./executor-VYrx_K4w.js";
8
- import { m as sameActorIdentity } from "./history-C84mlAwO.js";
9
- import { h as credentialSubjectSchema } from "./registry-FQutO926.js";
10
- import { $ as postSlackMessage, An as createPluginState, Mn as createPluginModel, Nn as createPluginLogger, U as escapeSlackMrkdwnText, Un as createPluginConversationEvents, W as formatSlackLink, Z as openSlackDirectMessage, ar as createPluginCache, dn as JUNIOR_THREAD_STATE_TTL_MS, g as coreTaskRegistrations, jn as createPluginEmbedder, mn as getDashboardTaskLink, p as getPlugins } from "./credentialed-CIGwoLU2.js";
11
- import { i as getStateAdapter } from "./adapter-BybMEezj.js";
12
- import { r as getPersistedThreadState, s as coerceThreadConversationState } from "./thread-state-DRmdJ2QN.js";
13
- import { t as resolveConversationRouting } from "./turn-session-routing-Dyut9l7Y.js";
14
- import { i as pluginTaskParamsSchema, r as pluginTaskId, s as sendVercelPluginTask } from "./task-queue-B0DLgd2P.js";
15
- import { z } from "zod";
16
- import { createHash } from "node:crypto";
17
- import { and, asc, count, desc, eq, gte, inArray, isNotNull, lte, max, sql } from "drizzle-orm";
18
- import { destinationSchema, destinationVisibilitySchema, isSlackDestination, pluginRunContextSchema, replyAttributionSchema, sourceSchema, taskOutcomeSchema } from "@sentry/junior-plugin-api";
19
- // src/api/conversations/aggregate.ts
20
- function usageTokenValue(source, field) {
21
- return sql`CASE
22
- WHEN ${source.usage}->>${field} IS NOT NULL
23
- THEN (${source.usage}->>${field})::double precision
24
- ELSE NULL
25
- END`;
26
- }
27
- function tokenValue(source) {
28
- return sql`
29
- CASE
30
- WHEN COALESCE(
31
- ${source.usage}->>'inputTokens',
32
- ${source.usage}->>'outputTokens',
33
- ${source.usage}->>'cachedInputTokens',
34
- ${source.usage}->>'cacheCreationTokens'
35
- ) IS NOT NULL
36
- THEN COALESCE((${source.usage}->>'inputTokens')::double precision, 0)
37
- + COALESCE((${source.usage}->>'outputTokens')::double precision, 0)
38
- + COALESCE((${source.usage}->>'cachedInputTokens')::double precision, 0)
39
- + COALESCE((${source.usage}->>'cacheCreationTokens')::double precision, 0)
40
- WHEN ${source.usage}->>'totalTokens' IS NOT NULL
41
- THEN (${source.usage}->>'totalTokens')::double precision
42
- ELSE NULL
43
- END
44
- `;
45
- }
46
- function costValue(source) {
47
- return conversationUsageCostExpr(source.usage);
48
- }
49
- /** Read one conversation row's usage cost as a SQL expression. */
50
- function conversationUsageCostExpr(usage) {
51
- return sql`
52
- CASE
53
- WHEN ${usage}->'cost'->>'total' IS NOT NULL
54
- THEN (${usage}->'cost'->>'total')::double precision
55
- WHEN COALESCE(
56
- ${usage}->'cost'->>'input',
57
- ${usage}->'cost'->>'output',
58
- ${usage}->'cost'->>'cacheRead',
59
- ${usage}->'cost'->>'cacheWrite'
60
- ) IS NOT NULL
61
- THEN COALESCE((${usage}->'cost'->>'input')::double precision, 0)
62
- + COALESCE((${usage}->'cost'->>'output')::double precision, 0)
63
- + COALESCE((${usage}->'cost'->>'cacheRead')::double precision, 0)
64
- + COALESCE((${usage}->'cost'->>'cacheWrite')::double precision, 0)
65
- ELSE NULL
66
- END
67
- `;
68
- }
69
- /** Select complete conversation metrics inside the database instead of materializing source rows. */
70
- function conversationAggregateColumns(sources) {
71
- const metrics = sources?.metrics ?? juniorConversations;
72
- const roots = sources?.roots ?? juniorConversations;
73
- const conversationCount = sources ? sql`COUNT(DISTINCT ${roots.conversationId})` : sql`COUNT(*)`;
74
- return {
75
- active: sql`${conversationCount} FILTER (
76
- WHERE ${roots.executionStatus} NOT IN ('idle', 'failed')
77
- )::integer`,
78
- conversations: sql`${conversationCount}::integer`,
79
- cachedInputTokens: sql`SUM(${usageTokenValue(metrics, "cachedInputTokens")})::double precision`,
80
- cacheCreationTokens: sql`SUM(${usageTokenValue(metrics, "cacheCreationTokens")})::double precision`,
81
- costUsd: sql`SUM(${costValue(metrics)})::double precision`,
82
- durationMs: sql`COALESCE(SUM(${metrics.durationMs}), 0)::double precision`,
83
- failed: sql`${conversationCount} FILTER (
84
- WHERE ${roots.executionStatus} = 'failed'
85
- )::integer`,
86
- inputTokens: sql`SUM(${usageTokenValue(metrics, "inputTokens")})::double precision`,
87
- tokens: sql`SUM(${tokenValue(metrics)})::double precision`
88
- };
89
- }
90
- /** Select the complete first/last activity range for a grouped conversation aggregate. */
91
- function conversationRangeColumns() {
92
- return {
93
- firstSeenAt: sql`MIN(${juniorConversations.createdAt})`.mapWith(juniorConversations.createdAt),
94
- lastSeenAt: sql`MAX(${juniorConversations.lastActivityAt})`.mapWith(juniorConversations.lastActivityAt)
95
- };
96
- }
97
- /** Count distinct UTC activity dates without loading conversation timestamps into the app. */
98
- function conversationActiveDaysColumn() {
99
- return sql`COUNT(DISTINCT (${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC')::date)::integer`;
100
- }
101
- // src/chat/automations/execution-stats.ts
102
- const AUTOMATION_EXECUTION_STATUSES = [
103
- "blocked",
104
- "completed",
105
- "failed"
106
- ];
107
- /** An execution is a run with an outcome. Skipped runs are not executions. */
108
- function isExecution() {
109
- return and(inArray(juniorAutomationRuns.status, [...AUTOMATION_EXECUTION_STATUSES]), isNotNull(juniorAutomationRuns.completedAtMs));
110
- }
111
- function addUsd(current, next) {
112
- return Math.round((current + next) * 0xe8d4a51000) / 0xe8d4a51000;
113
- }
114
- const EMPTY_RUN_WINDOWS = {
115
- 1: 0,
116
- 7: 0,
117
- 30: 0,
118
- 90: 0
119
- };
120
- /** Empty run or cost windows for automations with no execution history. */
121
- function emptyAutomationRunWindows() {
122
- return { ...EMPTY_RUN_WINDOWS };
123
- }
124
- function utcDate(nowMs) {
125
- return new Date(nowMs).toISOString().slice(0, 10);
126
- }
127
- function emptyDay(date) {
128
- return {
129
- costUsd: 0,
130
- date,
131
- event: 0,
132
- scheduled: 0
133
- };
134
- }
135
- function emptyStatusDay(date) {
136
- return {
137
- blocked: 0,
138
- completed: 0,
139
- date,
140
- failed: 0
141
- };
142
- }
143
- /**
144
- * Return the Conversation id when that Conversation exists, else null. A
145
- * dispatch can end before it creates its Conversation, and the run's
146
- * `conversation_id` is a foreign key.
147
- */
148
- function existingConversationId(conversationId) {
149
- return sql`(select ${juniorConversations.conversationId} from ${juniorConversations} where ${juniorConversations.conversationId} = ${conversationId})`;
150
- }
151
- /**
152
- * Record the outcome of one Automation run. A run that already has an outcome
153
- * keeps it. A failed write is logged, and the heartbeat settles a started
154
- * event run later.
155
- */
156
- async function recordAutomationExecution(automationId, options) {
157
- const nowMs = options.nowMs ?? Date.now();
158
- const status = options.status ?? "completed";
159
- const outcome = {
160
- completedAtMs: nowMs,
161
- errorMessage: options.errorMessage ?? null,
162
- status
163
- };
164
- try {
165
- await getDb().insert(juniorAutomationRuns).values({
166
- id: options.executionId,
167
- automationId,
168
- conversationId: options.conversationId ? existingConversationId(options.conversationId) : null,
169
- ...outcome
170
- }).onConflictDoUpdate({
171
- target: juniorAutomationRuns.id,
172
- set: {
173
- ...outcome,
174
- conversationId: sql`coalesce(${juniorAutomationRuns.conversationId}, excluded.conversation_id)`
175
- },
176
- setWhere: inArray(juniorAutomationRuns.status, ["pending", "running"])
177
- });
178
- } catch (error) {
179
- logWarn("task.execution.stat_failed", {
180
- error,
181
- "app.task.execution.id": options.executionId,
182
- "app.task.execution.status": status,
183
- "app.task.execution.task_id": automationId
184
- });
185
- }
186
- }
187
- /** Load usage analytics for the given automations. */
188
- async function readAutomationExecutionSummaries(automationIds, options = {}) {
189
- if (automationIds.length === 0) return /* @__PURE__ */ new Map();
190
- const nowMs = options.nowMs ?? Date.now();
191
- const oneDayAgoMs = nowMs - 864e5;
192
- const sevenDaysAgoMs = nowMs - 6048e5;
193
- const thirtyDaysAgoMs = nowMs - 2592e6;
194
- const ninetyDaysAgoMs = nowMs - 7776e6;
195
- const db = getDb();
196
- const selected = and(isExecution(), inArray(juniorAutomationRuns.automationId, automationIds));
197
- const latest = db.selectDistinctOn([juniorAutomationRuns.automationId], {
198
- status: juniorAutomationRuns.status,
199
- conversationId: juniorAutomationRuns.conversationId,
200
- automationId: juniorAutomationRuns.automationId
201
- }).from(juniorAutomationRuns).where(selected).orderBy(juniorAutomationRuns.automationId, desc(juniorAutomationRuns.completedAtMs), desc(juniorAutomationRuns.id)).as("latest_task_execution");
202
- const conversationCost = conversationUsageCostExpr(juniorConversations.usage);
203
- const costSince = (sinceMs) => sql`coalesce(sum(${conversationCost}) filter (where ${juniorAutomationRuns.completedAtMs} >= ${sinceMs}), 0)::double precision`;
204
- const rows = await db.select({
205
- costLast1Day: costSince(oneDayAgoMs),
206
- costLast7Days: costSince(sevenDaysAgoMs),
207
- costLast30Days: costSince(thirtyDaysAgoMs),
208
- costLast90Days: costSince(ninetyDaysAgoMs),
209
- lastRunStatus: latest.status,
210
- lastConversationId: latest.conversationId,
211
- lastExecutedAtMs: max(juniorAutomationRuns.completedAtMs),
212
- runsLast1Day: sql`count(*) filter (where ${juniorAutomationRuns.completedAtMs} >= ${oneDayAgoMs})::int`,
213
- runsLast7Days: sql`count(*) filter (where ${juniorAutomationRuns.completedAtMs} >= ${sevenDaysAgoMs})::int`,
214
- runsLast30Days: sql`count(*) filter (where ${juniorAutomationRuns.completedAtMs} >= ${thirtyDaysAgoMs})::int`,
215
- runsLast90Days: sql`count(*) filter (where ${juniorAutomationRuns.completedAtMs} >= ${ninetyDaysAgoMs})::int`,
216
- automationId: juniorAutomationRuns.automationId,
217
- totalRuns: sql`count(*)::int`
218
- }).from(juniorAutomationRuns).innerJoin(latest, eq(latest.automationId, juniorAutomationRuns.automationId)).leftJoin(juniorConversations, executionConversation()).where(selected).groupBy(juniorAutomationRuns.automationId, latest.conversationId, latest.status);
219
- return new Map(rows.map((row) => [row.automationId, {
220
- costUsd: {
221
- 1: addUsd(0, row.costLast1Day),
222
- 7: addUsd(0, row.costLast7Days),
223
- 30: addUsd(0, row.costLast30Days),
224
- 90: addUsd(0, row.costLast90Days)
225
- },
226
- lastRunStatus: row.lastRunStatus,
227
- ...row.lastConversationId ? { lastConversationId: row.lastConversationId } : void 0,
228
- ...row.lastExecutedAtMs !== null ? { lastExecutedAtMs: row.lastExecutedAtMs } : void 0,
229
- runs: {
230
- 1: row.runsLast1Day,
231
- 7: row.runsLast7Days,
232
- 30: row.runsLast30Days,
233
- 90: row.runsLast90Days
234
- },
235
- totalRuns: row.totalRuns
236
- }]));
237
- }
238
- /** Load a fixed trailing window of completed executions stacked by task type. */
239
- async function readAutomationExecutionDays(dayCount = 90, options = {}) {
240
- const end = utcDate(options.nowMs ?? Date.now());
241
- const endMs = Date.parse(`${end}T23:59:59.999Z`);
242
- const startMs = Date.parse(`${end}T00:00:00.000Z`) - (dayCount - 1) * 864e5;
243
- const executionDate = sql`to_char(to_timestamp(${juniorAutomationRuns.completedAtMs} / 1000.0) at time zone 'UTC', 'YYYY-MM-DD')`;
244
- const rows = await readCompletedExecutionBuckets({
245
- access: options.access,
246
- bucket: executionDate,
247
- endMs,
248
- startMs
249
- });
250
- const byDate = /* @__PURE__ */ new Map();
251
- for (let offset = 0; offset < dayCount; offset += 1) {
252
- const date = utcDate(startMs + offset * 864e5);
253
- byDate.set(date, emptyDay(date));
254
- }
255
- for (const row of rows) {
256
- const day = byDate.get(row.date);
257
- if (!day) continue;
258
- if (row.kind === "scheduled") day.scheduled = row.count;
259
- else if (row.kind === "event") day.event = row.count;
260
- if (row.costUsd !== null && row.costUsd > 0) day.costUsd = addUsd(day.costUsd, row.costUsd);
261
- }
262
- return [...byDate.values()];
263
- }
264
- /** Load a fixed trailing window of completed executions stacked by hour. */
265
- async function readAutomationExecutionHours(hourCount = 168, options = {}) {
266
- const nowMs = options.nowMs ?? Date.now();
267
- const end = new Date(nowMs);
268
- end.setUTCMinutes(0, 0, 0);
269
- const endMs = end.getTime() + 3599999;
270
- const startMs = end.getTime() - (hourCount - 1) * 36e5;
271
- const executionHour = sql`to_char(to_timestamp(${juniorAutomationRuns.completedAtMs} / 1000.0) at time zone 'UTC', 'YYYY-MM-DD"T"HH24')`;
272
- const rows = await readCompletedExecutionBuckets({
273
- access: options.access,
274
- bucket: executionHour,
275
- endMs,
276
- startMs
277
- });
278
- const byHour = /* @__PURE__ */ new Map();
279
- for (let offset = 0; offset < hourCount; offset += 1) {
280
- const date = new Date(startMs + offset * 36e5).toISOString().slice(0, 13);
281
- byHour.set(date, emptyDay(date));
282
- }
283
- for (const row of rows) {
284
- const hour = byHour.get(row.date);
285
- if (!hour) continue;
286
- if (row.kind === "scheduled") hour.scheduled = row.count;
287
- else if (row.kind === "event") hour.event = row.count;
288
- if (row.costUsd !== null && row.costUsd > 0) hour.costUsd = addUsd(hour.costUsd, row.costUsd);
289
- }
290
- return [...byHour.values()];
291
- }
292
- /** Count completed executions and their cost per time bucket and kind. */
293
- async function readCompletedExecutionBuckets(args) {
294
- const conversationCost = conversationUsageCostExpr(juniorConversations.usage);
295
- return await getDb().select({
296
- count: count(),
297
- costUsd: sql`SUM(${conversationCost})::double precision`,
298
- date: args.bucket,
299
- kind: juniorAutomations.kind
300
- }).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));
301
- }
302
- /** Load the Automation run linked to one conversation, if any. */
303
- async function readAutomationExecutionByConversationId(args) {
304
- const [row] = await getDb().select({
305
- kind: juniorAutomations.kind,
306
- automationId: juniorAutomationRuns.automationId
307
- }).from(juniorAutomationRuns).innerJoin(juniorAutomations, eq(juniorAutomations.id, juniorAutomationRuns.automationId)).where(eq(juniorAutomationRuns.conversationId, args.conversationId)).limit(1);
308
- return row;
309
- }
310
- /**
311
- * Load the display name of the Automation linked to each untitled
312
- * conversation. Like the Automation list, the name falls back to the first
313
- * line of the instruction. Titled conversations and conversations without a
314
- * linked Automation are absent from the result.
315
- */
316
- async function readAutomationTitlesByConversationId(conversations) {
317
- const titles = /* @__PURE__ */ new Map();
318
- const untitledIds = conversations.flatMap((conversation) => conversation.title ? [] : [conversation.conversationId]);
319
- if (untitledIds.length === 0) return titles;
320
- const rows = await getDb().select({
321
- conversationId: juniorAutomationRuns.conversationId,
322
- instruction: juniorAutomations.instruction,
323
- title: juniorAutomations.title
324
- }).from(juniorAutomationRuns).innerJoin(juniorAutomations, eq(juniorAutomations.id, juniorAutomationRuns.automationId)).where(inArray(juniorAutomationRuns.conversationId, untitledIds));
325
- for (const row of rows) {
326
- const title = row.title?.trim() || fallbackShortTitle(row.instruction, "");
327
- if (row.conversationId && title) titles.set(row.conversationId, title);
328
- }
329
- return titles;
330
- }
331
- /** Load newest-first executions for one automation, with conversation titles when present. */
332
- async function readAutomationExecutions(args) {
333
- 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));
334
- }
335
- /** Load newest-first executions for the supplied viewer-visible automations. */
336
- async function readAutomationRuns(args) {
337
- if (args.automationIds.length === 0) return [];
338
- return (await getDb().select({
339
- ...executionColumns,
340
- kind: juniorAutomations.kind,
341
- automationId: juniorAutomationRuns.automationId
342
- }).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) => ({
343
- ...mapAutomationExecutionRecord(row),
344
- kind: row.kind,
345
- automationId: row.automationId
346
- }));
347
- }
348
- /** Columns that describe one execution, its Conversation, and its version. */
349
- const executionColumns = {
350
- automationVersion: juniorAutomationRuns.automationVersion,
351
- conversationId: juniorAutomationRuns.conversationId,
352
- credentialMode: sql`${juniorAutomationVersions.definition}->>'credentialMode'`,
353
- durationMs: juniorConversations.durationMs,
354
- errorMessage: juniorAutomationRuns.errorMessage,
355
- executedAtMs: juniorAutomationRuns.completedAtMs,
356
- executionId: juniorAutomationRuns.id,
357
- scheduledForMs: juniorAutomationRuns.scheduledForMs,
358
- startedAtMs: juniorAutomationRuns.startedAtMs,
359
- status: juniorAutomationRuns.status,
360
- title: juniorConversations.title,
361
- usage: juniorConversations.usage
362
- };
363
- function executionConversation() {
364
- return eq(juniorConversations.conversationId, juniorAutomationRuns.conversationId);
365
- }
366
- function executionVersion() {
367
- return and(eq(juniorAutomationVersions.automationId, juniorAutomationRuns.automationId), eq(juniorAutomationVersions.version, juniorAutomationRuns.automationVersion));
368
- }
369
- function isoTime(ms) {
370
- return ms === null ? void 0 : new Date(ms).toISOString();
371
- }
372
- function mapAutomationExecutionRecord(row) {
373
- const title = row.title?.trim();
374
- const costUsd = agentTurnCostUsd(row.usage ?? void 0);
375
- const totalTokens = agentTurnTotalTokens(row.usage ?? void 0);
376
- const durationMs = typeof row.durationMs === "number" && Number.isFinite(row.durationMs) && row.durationMs > 0 ? row.durationMs : void 0;
377
- const error = row.errorMessage?.trim();
378
- const scheduledFor = isoTime(row.scheduledForMs);
379
- const startedAt = isoTime(row.startedAtMs);
380
- return {
381
- ...row.automationVersion !== null ? { automationVersion: row.automationVersion } : void 0,
382
- ...row.conversationId ? { conversationId: row.conversationId } : void 0,
383
- ...costUsd !== void 0 ? { costUsd } : void 0,
384
- ...row.credentialMode === "creator" || row.credentialMode === "system" ? { credentialMode: row.credentialMode } : void 0,
385
- ...durationMs !== void 0 ? { durationMs } : void 0,
386
- ...error ? { error } : void 0,
387
- executedAt: new Date(row.executedAtMs).toISOString(),
388
- executionId: row.executionId,
389
- ...scheduledFor ? { scheduledFor } : void 0,
390
- ...startedAt ? { startedAt } : void 0,
391
- status: row.status,
392
- ...title ? { title } : void 0,
393
- ...totalTokens !== void 0 ? { totalTokens } : void 0
394
- };
395
- }
396
- /** Load a fixed trailing window of terminal executions for one automation by status. */
397
- async function readAutomationExecutionStatusDays(args) {
398
- const dayCount = args.dayCount ?? 90;
399
- const end = utcDate(args.nowMs ?? Date.now());
400
- const endMs = Date.parse(`${end}T23:59:59.999Z`);
401
- const startMs = Date.parse(`${end}T00:00:00.000Z`) - (dayCount - 1) * 864e5;
402
- const executionDate = sql`to_char(to_timestamp(${juniorAutomationRuns.completedAtMs} / 1000.0) at time zone 'UTC', 'YYYY-MM-DD')`;
403
- const rows = await readExecutionStatusBuckets({
404
- automationId: args.automationId,
405
- bucket: executionDate,
406
- endMs,
407
- startMs
408
- });
409
- const byDate = /* @__PURE__ */ new Map();
410
- for (let offset = 0; offset < dayCount; offset += 1) {
411
- const date = utcDate(startMs + offset * 864e5);
412
- byDate.set(date, emptyStatusDay(date));
413
- }
414
- for (const row of rows) {
415
- const day = byDate.get(row.date);
416
- if (!day) continue;
417
- if (row.status === "completed") day.completed = row.count;
418
- else if (row.status === "failed") day.failed = row.count;
419
- else if (row.status === "blocked") day.blocked = row.count;
420
- }
421
- return [...byDate.values()];
422
- }
423
- /** Load a fixed trailing window of terminal executions for one automation by hour. */
424
- async function readAutomationExecutionStatusHours(args) {
425
- const hourCount = args.hourCount ?? 168;
426
- const nowMs = args.nowMs ?? Date.now();
427
- const end = new Date(nowMs);
428
- end.setUTCMinutes(0, 0, 0);
429
- const endMs = end.getTime() + 3599999;
430
- const startMs = end.getTime() - (hourCount - 1) * 36e5;
431
- const executionHour = sql`to_char(to_timestamp(${juniorAutomationRuns.completedAtMs} / 1000.0) at time zone 'UTC', 'YYYY-MM-DD"T"HH24')`;
432
- const rows = await readExecutionStatusBuckets({
433
- automationId: args.automationId,
434
- bucket: executionHour,
435
- endMs,
436
- startMs
437
- });
438
- const byHour = /* @__PURE__ */ new Map();
439
- for (let offset = 0; offset < hourCount; offset += 1) {
440
- const date = new Date(startMs + offset * 36e5).toISOString().slice(0, 13);
441
- byHour.set(date, emptyStatusDay(date));
442
- }
443
- for (const row of rows) {
444
- const hour = byHour.get(row.date);
445
- if (!hour) continue;
446
- if (row.status === "completed") hour.completed = row.count;
447
- else if (row.status === "failed") hour.failed = row.count;
448
- else if (row.status === "blocked") hour.blocked = row.count;
449
- }
450
- return [...byHour.values()];
451
- }
452
- /** Count one automation's executions per time bucket and status. */
453
- async function readExecutionStatusBuckets(args) {
454
- return await getDb().select({
455
- count: count(),
456
- date: args.bucket,
457
- status: juniorAutomationRuns.status
458
- }).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));
459
- }
460
- // src/chat/automations/blocked-notice.ts
461
- function buildBlockedNoticeText(automationId, title, reason) {
462
- const url = getDashboardTaskLink(automationId);
463
- return [
464
- `Your automation *${escapeSlackMrkdwnText(title)}* is blocked. It won't run again until you resume it.`,
465
- `> ${escapeSlackMrkdwnText(reason.replace(/\s+/g, " ").trim())}`,
466
- url ? `Fix the problem, then ${formatSlackLink(url, "resume it")}.` : "Fix the problem, then ask me to resume it."
467
- ].join("\n");
468
- }
469
- /**
470
- * Tell an Automation's creator in a direct message that a run blocked it,
471
- * and why. Call this once, after the Automation itself is stored as blocked,
472
- * so the dashboard matches the notice. The notice is best-effort: the reason
473
- * also shows on the dashboard and in the Automation tools.
474
- */
475
- async function notifyAutomationBlocked(ref) {
476
- await runBestEffort(async () => {
477
- const task = ref.kind === "scheduled" ? await readScheduledAutomation(getDb(), ref.automationId) : await getEventAutomation(getDb(), ref.automationId);
478
- if (!task) return;
479
- const title = task.title ?? fallbackShortTitle(task.task.text, ref.kind === "scheduled" ? "Scheduled automation" : "Event automation");
480
- await postSlackMessage({
481
- channelId: await openSlackDirectMessage(task.createdBy.slackUserId),
482
- text: buildBlockedNoticeText(ref.automationId, title, ref.reason)
483
- });
484
- }, "automation.blocked_notice.failed", {
485
- "app.task.type": ref.kind,
486
- ...ref.dispatchId ? { "app.dispatch.id": ref.dispatchId } : void 0
487
- });
488
- }
489
- // src/chat/agent-dispatch/store.ts
490
- const DISPATCH_PREFIX = "junior:agent_dispatch";
491
- const DISPATCH_LOCK_TTL_MS = 6e5;
492
- const DISPATCH_MAILBOX_APPEND_INDEX_LOCK_TTL_MS = 1e4;
493
- const nonEmptyExactStringSchema = z.string().min(1).refine((value) => value === value.trim() && value.toLowerCase() !== "unknown");
494
- const pendingDispatchMailboxAppendIndexSchema = z.array(nonEmptyExactStringSchema);
495
- const dispatchStatusSchema = z.enum([
496
- "pending",
497
- "running",
498
- "awaiting_resume",
499
- "completed",
500
- "failed",
501
- "blocked"
502
- ]);
503
- const dispatchActorSchema = z.object({
504
- platform: z.literal("system"),
505
- name: nonEmptyExactStringSchema
506
- }).strict();
507
- const dispatchRecordSchema = z.object({
508
- actor: dispatchActorSchema,
509
- createdAtMs: z.number().finite(),
510
- credentialSubject: credentialSubjectSchema.optional(),
511
- destination: destinationSchema,
512
- destinationVisibility: destinationVisibilitySchema,
513
- errorMessage: z.string().optional(),
514
- id: nonEmptyExactStringSchema,
515
- idempotencyKey: z.string().min(1),
516
- input: z.string().min(1),
517
- metadata: z.record(z.string(), z.string()).optional(),
518
- plugin: nonEmptyExactStringSchema,
519
- replyAttribution: replyAttributionSchema.optional(),
520
- resultMessageTs: z.string().optional(),
521
- source: sourceSchema,
522
- outcomes: z.array(taskOutcomeSchema).max(5).optional(),
523
- status: dispatchStatusSchema,
524
- updatedAtMs: z.number().finite()
525
- }).strict().superRefine((record, ctx) => {
526
- if (!isSlackDestination(record.destination)) {
527
- ctx.addIssue({
528
- code: z.ZodIssueCode.custom,
529
- message: "Dispatch destination platform must be slack",
530
- path: ["destination"]
531
- });
532
- return;
533
- }
534
- const subject = record.credentialSubject;
535
- if (!subject) return;
536
- if (subject.allowedWhen === "private-direct-conversation") {
537
- if (!record.destination.channelId.startsWith("D")) {
538
- ctx.addIssue({
539
- code: z.ZodIssueCode.custom,
540
- message: "Dispatch credentialSubject requires a private direct Slack destination",
541
- path: ["credentialSubject"]
542
- });
543
- return;
544
- }
545
- if (subject.binding.type !== "slack-direct-conversation" || subject.binding.teamId !== record.destination.teamId || subject.binding.channelId !== record.destination.channelId) ctx.addIssue({
546
- code: z.ZodIssueCode.custom,
547
- message: "Dispatch credentialSubject binding must match destination",
548
- path: ["credentialSubject", "binding"]
549
- });
550
- return;
551
- }
552
- if (subject.allowedWhen === "timer-watch") {
553
- ctx.addIssue({
554
- code: z.ZodIssueCode.custom,
555
- message: "Dispatch credentialSubject cannot use a timer Watch binding",
556
- path: ["credentialSubject", "allowedWhen"]
557
- });
558
- return;
559
- }
560
- if (subject.binding.type !== subject.allowedWhen || subject.binding.plugin !== record.plugin || subject.binding.taskId !== subject.taskId) ctx.addIssue({
561
- code: z.ZodIssueCode.custom,
562
- message: "Dispatch credentialSubject binding must match task",
563
- path: ["credentialSubject", "binding"]
564
- });
565
- });
566
- /** Return the durable key for one plugin-facing dispatch projection. */
567
- function getDispatchStorageKey(id) {
568
- return `${DISPATCH_PREFIX}:record:${id}`;
569
- }
570
- function dispatchLockKey(id) {
571
- return `${DISPATCH_PREFIX}:lock:${id}`;
572
- }
573
- function pendingDispatchMailboxAppendIndexKey() {
574
- return `${DISPATCH_PREFIX}:incomplete`;
575
- }
576
- function pendingDispatchMailboxAppendIndexLockKey() {
577
- return `${DISPATCH_PREFIX}:incomplete:lock`;
578
- }
579
- function normalizeMetadata(metadata) {
580
- if (!metadata) return;
581
- const entries = Object.entries(metadata).filter((entry) => typeof entry[0] === "string" && typeof entry[1] === "string");
582
- return entries.length > 0 ? Object.fromEntries(entries) : void 0;
583
- }
584
- const DISPATCH_ID_PREFIX = "agent-dispatch:";
585
- /** Return the stable dispatch id for one plugin idempotency key. */
586
- function buildDispatchId(plugin, idempotencyKey) {
587
- return `dispatch_${createHash("sha256").update(plugin).update("\0").update(idempotencyKey).digest("hex").slice(0, 32)}`;
588
- }
589
- /** Parse one durable dispatch projection record. */
590
- function parseDispatchRecord(value) {
591
- const parsed = dispatchRecordSchema.safeParse(value);
592
- return parsed.success ? parsed.data : void 0;
593
- }
594
- /** Return the isolated durable conversation id for one dispatch. */
595
- function getDispatchConversationId(dispatch) {
596
- return `${DISPATCH_ID_PREFIX}${dispatch.id}`;
597
- }
598
- /** Return the stable synthetic input message id for one dispatch turn. */
599
- function getDispatchInputMessageId(dispatchId) {
600
- return `${DISPATCH_ID_PREFIX}${dispatchId}`;
601
- }
602
- /**
603
- * Return the dispatch id of a dispatch Conversation, such as an Automation
604
- * run. Other Conversations have no dispatch.
605
- */
606
- function getDispatchIdForConversation(conversationId) {
607
- if (!conversationId.startsWith(DISPATCH_ID_PREFIX)) return void 0;
608
- return conversationId.slice(15) || void 0;
609
- }
610
- /** Return the stable turn id used by every run of one dispatch. */
611
- function getDispatchTurnId(dispatchId) {
612
- return `dispatch:${dispatchId}`;
613
- }
614
- function toDispatchProjection(record) {
615
- return {
616
- id: record.id,
617
- status: record.status,
618
- ...record.resultMessageTs ? { resultMessageTs: record.resultMessageTs } : void 0,
619
- ...record.errorMessage ? { errorMessage: record.errorMessage } : void 0
620
- };
621
- }
622
- /** Gate recovery to dispatches that can still make progress. */
623
- function isTerminalDispatchStatus(status) {
624
- return status === "completed" || status === "failed" || status === "blocked";
625
- }
626
- /** Serialize dispatch projection mutations. */
627
- async function withDispatchLock(dispatchId, task) {
628
- const state = getStateAdapter();
629
- await state.connect();
630
- const lock = await state.acquireLock(dispatchLockKey(dispatchId), DISPATCH_LOCK_TTL_MS);
631
- if (!lock) throw new Error(`Could not acquire dispatch lock for ${dispatchId}`);
632
- try {
633
- return await task(state);
634
- } finally {
635
- await state.releaseLock(lock);
636
- }
637
- }
638
- async function putRecord(state, record) {
639
- const parsed = dispatchRecordSchema.safeParse(record);
640
- if (!parsed.success) throw new Error(`Dispatch record is invalid: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"} ${issue.message}`).join("; ")}`);
641
- const next = parsed.data;
642
- await state.set(getDispatchStorageKey(next.id), next, JUNIOR_THREAD_STATE_TTL_MS);
643
- }
644
- async function updatePendingDispatchMailboxAppendIndex(state, update) {
645
- const lock = await state.acquireLock(pendingDispatchMailboxAppendIndexLockKey(), DISPATCH_MAILBOX_APPEND_INDEX_LOCK_TTL_MS);
646
- if (!lock) throw new Error("Could not acquire dispatch mailbox append index lock");
647
- try {
648
- const stored = await state.get(pendingDispatchMailboxAppendIndexKey());
649
- const ids = stored === void 0 || stored === null ? [] : pendingDispatchMailboxAppendIndexSchema.parse(stored);
650
- const next = [...new Set(update([...new Set(ids)]))].slice(-1e4);
651
- if (next.length === ids.length && next.every((id, index) => id === ids[index])) return;
652
- await state.set(pendingDispatchMailboxAppendIndexKey(), next, JUNIOR_THREAD_STATE_TTL_MS);
653
- } finally {
654
- await state.releaseLock(lock);
655
- }
656
- }
657
- /** Load dispatch state for conversation work and plugin projections. */
658
- async function getDispatchRecord(id) {
659
- const state = getStateAdapter();
660
- await state.connect();
661
- return parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
662
- }
663
- /** Create a dispatch idempotently within its owner namespace. */
664
- async function createOrGetDispatch(args) {
665
- const id = buildDispatchId(args.plugin, args.options.idempotencyKey);
666
- return await withDispatchLock(id, async (state) => {
667
- const existing = parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
668
- if (existing) return {
669
- record: existing,
670
- status: "already_exists"
671
- };
672
- const metadata = normalizeMetadata(args.options.metadata);
673
- const record = {
674
- actor: {
675
- platform: "system",
676
- name: args.plugin
677
- },
678
- createdAtMs: args.nowMs,
679
- ...args.options.credentialSubject ? { credentialSubject: args.options.credentialSubject } : void 0,
680
- destination: args.options.destination,
681
- destinationVisibility: args.options.destinationVisibility,
682
- id,
683
- idempotencyKey: args.options.idempotencyKey,
684
- input: args.options.input,
685
- ...metadata ? { metadata } : void 0,
686
- plugin: args.plugin,
687
- ...args.options.replyAttribution ? { replyAttribution: args.options.replyAttribution } : void 0,
688
- ...args.options.outcomes ? { outcomes: args.options.outcomes } : void 0,
689
- status: "pending",
690
- source: args.options.source,
691
- updatedAtMs: args.nowMs
692
- };
693
- await updatePendingDispatchMailboxAppendIndex(state, (ids) => ids.includes(id) ? ids : [...ids, id]);
694
- await putRecord(state, record);
695
- return {
696
- record,
697
- status: "created"
698
- };
699
- });
700
- }
701
- async function transitionDispatch(id, transition) {
702
- return await withDispatchLock(id, async (state) => {
703
- const current = parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
704
- if (!current) return;
705
- const transitioned = transition(current);
706
- if (transitioned === current) return current;
707
- const next = {
708
- ...transitioned,
709
- updatedAtMs: Date.now()
710
- };
711
- await putRecord(state, next);
712
- return next;
713
- });
714
- }
715
- /** Mark a dispatch projection as actively running. */
716
- async function markDispatchRunning(id) {
717
- return await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
718
- ...record,
719
- status: "running",
720
- errorMessage: void 0
721
- });
722
- }
723
- /** Project a durable awaiting-resume turn state to the plugin API. */
724
- async function markDispatchAwaitingResume(id) {
725
- return await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
726
- ...record,
727
- status: "awaiting_resume"
728
- });
729
- }
730
- async function recordEventAutomationExecution(previous, next, status) {
731
- if (!next || next.status !== status || previous?.status === status) return;
732
- if (next.plugin !== "junior") return;
733
- const eventAutomationId = next.metadata?.eventAutomationId;
734
- if (!eventAutomationId) return;
735
- await recordAutomationExecution(eventAutomationId, {
736
- conversationId: getDispatchConversationId(next),
737
- errorMessage: next.errorMessage,
738
- executionId: next.id,
739
- nowMs: next.updatedAtMs,
740
- status
741
- });
742
- }
743
- /**
744
- * Project a blocked turn to the plugin API. An Event automation is blocked
745
- * first, so a retry after a failed write still blocks it. Its creator is told
746
- * when the block is first stored. The heartbeat blocks a Scheduled automation
747
- * and tells its creator.
748
- */
749
- async function markDispatchBlocked(id, errorMessage, resultMessageTs) {
750
- const previous = await getDispatchRecord(id);
751
- const eventAutomationId = previous?.metadata?.eventAutomationId;
752
- if (previous?.plugin === "junior" && eventAutomationId && !isTerminalDispatchStatus(previous.status) && await blockEventAutomation(getDb(), eventAutomationId, errorMessage)) await notifyAutomationBlocked({
753
- automationId: eventAutomationId,
754
- dispatchId: id,
755
- kind: "event",
756
- reason: errorMessage
757
- });
758
- const next = await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
759
- ...record,
760
- errorMessage,
761
- ...resultMessageTs ? { resultMessageTs } : void 0,
762
- status: "blocked"
763
- });
764
- await recordEventAutomationExecution(previous, next, "blocked");
765
- return next;
766
- }
767
- /** Project a completed turn and its accepted Slack message to the plugin API. */
768
- async function markDispatchCompleted(id, resultMessageTs) {
769
- const previous = await getDispatchRecord(id);
770
- const next = await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
771
- ...record,
772
- errorMessage: void 0,
773
- ...resultMessageTs ? { resultMessageTs } : void 0,
774
- status: "completed"
775
- });
776
- await recordEventAutomationExecution(previous, next, "completed");
777
- return next;
778
- }
779
- /** Project a terminal conversation turn failure to the plugin API. */
780
- async function markDispatchFailed(id, errorMessage, resultMessageTs) {
781
- const previous = await getDispatchRecord(id);
782
- const next = await transitionDispatch(id, (record) => isTerminalDispatchStatus(record.status) ? record : {
783
- ...record,
784
- errorMessage,
785
- ...resultMessageTs ? { resultMessageTs } : void 0,
786
- status: "failed"
787
- });
788
- await recordEventAutomationExecution(previous, next, "failed");
789
- return next;
790
- }
791
- /** Remove a dispatch after its durable mailbox append has succeeded. */
792
- async function confirmDispatchMailboxAppend(id) {
793
- const state = getStateAdapter();
794
- await state.connect();
795
- await updatePendingDispatchMailboxAppendIndex(state, (ids) => ids.filter((candidate) => candidate !== id));
796
- }
797
- /** List dispatches whose durable mailbox append may not have completed. */
798
- async function listPendingDispatchMailboxAppends() {
799
- const state = getStateAdapter();
800
- await state.connect();
801
- const stored = await state.get(pendingDispatchMailboxAppendIndexKey());
802
- return stored === void 0 || stored === null ? [] : [...new Set(pendingDispatchMailboxAppendIndexSchema.parse(stored))];
803
- }
804
- /** Load the dispatch under lock before its mailbox append becomes visible. */
805
- async function claimDispatchMailboxAppend(id) {
806
- return await withDispatchLock(id, async (state) => {
807
- return parseDispatchRecord(await state.get(getDispatchStorageKey(id)));
808
- });
809
- }
810
- /** Return a plugin-scoped dispatch projection without exposing raw runtime state. */
811
- async function getPluginDispatchProjection(args) {
812
- const record = await getDispatchRecord(args.id);
813
- if (!record || record.plugin !== args.plugin) return;
814
- return toDispatchProjection(record);
815
- }
816
- // src/chat/plugins/task-runner.ts
817
- /**
818
- * Plugin background-task orchestration.
819
- *
820
- * Core schedules tasks from completed sessions and exposes plugins only a
821
- * bounded run projection rather than live runtime internals or queue
822
- * payloads.
823
- */
824
- var task_runner_exports = /* @__PURE__ */ __exportAll({
825
- processPluginTask: () => processPluginTask,
826
- scheduleSessionCompletedPluginTasks: () => scheduleSessionCompletedPluginTasks
827
- });
828
- const PLUGIN_TASK_LOCK_TTL_MS = 3e5;
829
- function isRecord(value) {
830
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
831
- }
832
- function textPart(value) {
833
- if (isRecord(value) && value.type === "text" && typeof value.text === "string") return value.text;
834
- }
835
- function messageText(message) {
836
- const content = message.content;
837
- if (typeof content === "string") return sanitizeText(content);
838
- if (!Array.isArray(content)) return "";
839
- return sanitizeText(content.map(textPart).filter(Boolean).join("\n"));
840
- }
841
- function toolResultText(message) {
842
- const record = message;
843
- return sanitizeText([
844
- messageText(message),
845
- record.output,
846
- record.result,
847
- record.stdout,
848
- record.stderr,
849
- record.toolResult
850
- ].filter((value) => typeof value === "string" && value.length > 0).join("\n"));
851
- }
852
- function sanitizeText(text) {
853
- return text.replace(/<data_base64>[\s\S]*?<\/data_base64>/g, "<data_base64>[omitted]</data_base64>").replace(/data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=]+/gi, "[image data omitted]").replaceAll("\0", " ").trim();
854
- }
855
- /** Build the transcript provenance for a user message from its Pi provenance. */
856
- function messageProvenance(provenance) {
857
- return {
858
- authority: provenance.authority,
859
- ...provenance.actor ? { actor: provenance.actor } : void 0
860
- };
861
- }
862
- function runTranscriptEntry(message, provenance, runActor) {
863
- const role = getPiMessageRole(message);
864
- if (role === "user" || role === "assistant") {
865
- const text = role === "user" ? instructionTextForProjection(messageText(message)) : messageText(message);
866
- if (!text) return;
867
- if (role === "assistant") return {
868
- type: "message",
869
- role,
870
- text
871
- };
872
- return {
873
- type: "message",
874
- role,
875
- text,
876
- provenance: messageProvenance(provenance),
877
- isRunActor: sameActorIdentity(provenance.actor, runActor)
878
- };
879
- }
880
- if (!isToolResultMessage(message)) return;
881
- const toolName = normalizeToolNameFromResult(message);
882
- if (!toolName) return;
883
- const text = toolResultText(message);
884
- return {
885
- type: "toolResult",
886
- toolName,
887
- isError: isToolResultError(message),
888
- ...text ? { text } : void 0
889
- };
890
- }
891
- /**
892
- * Slice the current turn's Pi messages with their aligned provenance and strip
893
- * runtime turn context, keeping each surviving message paired with the exact
894
- * provenance recorded for it. Stripping can drop or rewrite messages, so the
895
- * pairing is applied per message rather than by post-strip index.
896
- */
897
- function turnMessagesWithProvenance(record) {
898
- const startIndex = record.turnStartMessageIndex ?? 0;
899
- const messages = record.piMessages.slice(startIndex);
900
- const provenance = record.piMessageProvenance.slice(startIndex);
901
- const paired = [];
902
- for (const [index, message] of messages.entries()) for (const stripped of stripRuntimeTurnContext([message])) paired.push({
903
- message: stripped,
904
- provenance: provenance[index] ?? { authority: "context" }
905
- });
906
- return paired;
907
- }
908
- /** Recover the Slack context author identity from a persisted thread message. */
909
- function slackContextAuthor(location, message) {
910
- const userId = message.author?.userId?.trim();
911
- if (!userId) return;
912
- return {
913
- platform: "slack",
914
- teamId: location.teamId,
915
- userId,
916
- ...message.author?.userName ? { userName: message.author.userName } : void 0,
917
- ...message.author?.fullName ? { fullName: message.author.fullName } : void 0
918
- };
919
- }
920
- function slackTimestampMs(value) {
921
- const timestamp = parseSlackMessageTs(value);
922
- if (!timestamp) return;
923
- const timestampMs = Number(timestamp) * 1e3;
924
- return Number.isFinite(timestampMs) ? timestampMs : void 0;
925
- }
926
- function conversationMessageTimestampMs(message) {
927
- if (message.meta?.slackTs !== void 0) return slackTimestampMs(message.meta.slackTs);
928
- return Number.isFinite(message.createdAtMs) ? message.createdAtMs : void 0;
929
- }
930
- function messageExistedAtRunCompletion(message, completedAtMs) {
931
- const messageTimestampMs = conversationMessageTimestampMs(message);
932
- return messageTimestampMs !== void 0 && messageTimestampMs <= completedAtMs;
933
- }
934
- /**
935
- * Project bounded public-thread context into the run transcript.
936
- *
937
- * Prior public Slack messages from the current Conversation are durable
938
- * evidence a completed run may have acted on, so passive consumers can cite
939
- * them. They are always context authority (never instruction), and private or
940
- * local Conversations add nothing here.
941
- */
942
- async function loadConversationContextTranscriptEntries(record, location, visibility, runActor) {
943
- if (location?.provider !== "slack" || visibility !== "public") return [];
944
- const state = await getPersistedThreadState(record.conversationId);
945
- const conversation = coerceThreadConversationState(state);
946
- await hydrateConversationMessages({
947
- conversation,
948
- conversationId: record.conversationId
949
- });
950
- const entries = [];
951
- for (const message of conversation.messages) {
952
- if (message.role !== "user") continue;
953
- if (!messageExistedAtRunCompletion(message, record.updatedAtMs)) continue;
954
- const text = sanitizeText(message.text);
955
- if (!text) continue;
956
- const author = slackContextAuthor(location, message);
957
- entries.push({
958
- type: "message",
959
- role: "user",
960
- text,
961
- provenance: {
962
- authority: "context",
963
- ...author ? { actor: author } : void 0
964
- },
965
- isRunActor: sameActorIdentity(author, runActor)
966
- });
967
- }
968
- return entries;
969
- }
970
- async function withPluginTaskLock(taskId, callback) {
971
- const state = getStateAdapter();
972
- await state.connect();
973
- const lock = await state.acquireLock(`plugin:task:${taskId}`, PLUGIN_TASK_LOCK_TTL_MS);
974
- if (!lock) throw new Error(`Could not acquire plugin task lock for ${taskId}`);
975
- try {
976
- return await callback();
977
- } finally {
978
- await state.releaseLock(lock);
979
- }
980
- }
981
- /** Load the bounded completed-run projection exposed to plugin tasks. */
982
- async function loadPluginRun(params) {
983
- const record = await getTurnRecord(params.conversationId, params.sessionId);
984
- if (!record) throw new Error("Completed plugin task session record is unavailable");
985
- if (record.state !== "completed") throw new Error("Completed plugin task session record is not completed");
986
- const conversationStore = getConversationStore();
987
- const routing = await resolveConversationRouting({
988
- conversationId: params.conversationId,
989
- conversationStore
990
- });
991
- if (!routing) throw new Error(`Conversation ${params.conversationId} is missing durable routing metadata`);
992
- const source = record.source ?? routing.source;
993
- if (!source) throw new Error("Completed plugin task session record is missing Source");
994
- const conversation = await conversationStore.get({ conversationId: params.conversationId });
995
- const runActor = record.actor ?? record.actors[0] ?? (record.dispatchId ? (await getDispatchRecord(record.dispatchId))?.actor : void 0);
996
- const actorUser = runActor ? (await readActorIdentity(runActor))?.user : void 0;
997
- const runEntries = turnMessagesWithProvenance(record).map(({ message, provenance }) => runTranscriptEntry(message, provenance, runActor)).filter((entry) => Boolean(entry));
998
- const runMessageTexts = new Set(runEntries.filter((entry) => entry.type === "message").map((entry) => entry.text));
999
- const contextEntries = (await loadConversationContextTranscriptEntries(record, conversation?.location, conversation?.visibility, runActor)).filter((entry) => entry.type !== "message" || !runMessageTexts.has(entry.text));
1000
- return pluginRunContextSchema.parse({
1001
- ...actorUser ? { actorUserId: actorUser.id } : void 0,
1002
- completedAtMs: record.updatedAtMs,
1003
- conversationId: record.conversationId,
1004
- destination: routing.destination,
1005
- ...routing.location ? { locationId: routing.location.id } : void 0,
1006
- actors: record.actors,
1007
- ...runActor ? { actor: runActor } : void 0,
1008
- runId: record.turnId,
1009
- source,
1010
- transcript: [...contextEntries, ...runEntries]
1011
- });
1012
- }
1013
- /** Build the plugin-facing context for one claimed task attempt. */
1014
- function taskPluginContext(plugin, message, options = {}) {
1015
- const pluginName = plugin.manifest.name;
1016
- const sessionParams = pluginTaskParamsSchema.parse(message.params);
1017
- return {
1018
- db: getDb(),
1019
- embedder: createPluginEmbedder(pluginName, { signal: options.signal }),
1020
- events: createPluginConversationEvents({
1021
- conversationId: sessionParams.conversationId,
1022
- operationId: pluginTaskId(message),
1023
- plugin,
1024
- turnId: sessionParams.sessionId
1025
- }),
1026
- id: pluginTaskId(message),
1027
- log: createPluginLogger(pluginName),
1028
- model: createPluginModel(pluginName, plugin.model, { signal: options.signal }),
1029
- name: message.name,
1030
- plugin: { name: pluginName },
1031
- cache: createPluginCache(pluginName),
1032
- run: { async load() {
1033
- return await loadPluginRun(sessionParams);
1034
- } },
1035
- state: createPluginState(pluginName)
1036
- };
1037
- }
1038
- function findPluginTask(message) {
1039
- const plugin = [...coreTaskRegistrations(), ...getPlugins()].find((candidate) => candidate.manifest.name === message.plugin);
1040
- if (!plugin?.tasks || !Object.hasOwn(plugin.tasks, message.name)) return;
1041
- return {
1042
- plugin,
1043
- task: plugin.tasks[message.name]
1044
- };
1045
- }
1046
- /** Schedule all plugin tasks interested in a completed agent-run session. */
1047
- async function scheduleSessionCompletedPluginTasks(params, options = {}) {
1048
- const coreParams = pluginTaskParamsSchema.parse(params);
1049
- const taskRegistrations = [...getPlugins(), ...coreTaskRegistrations()].flatMap((plugin) => Object.keys(plugin.tasks ?? {}).map((name) => ({
1050
- name,
1051
- plugin
1052
- })));
1053
- if (taskRegistrations.length === 0) return;
1054
- const record = await getTurnRecord(coreParams.conversationId, coreParams.sessionId);
1055
- if (!record || record.state !== "completed") throw new Error("Completed plugin task session record is not ready");
1056
- const send = options.send ?? sendVercelPluginTask;
1057
- const messages = taskRegistrations.map(({ name, plugin }) => ({
1058
- name,
1059
- params: coreParams,
1060
- plugin: plugin.manifest.name
1061
- }));
1062
- await Promise.all(messages.map(async (message) => {
1063
- await send(message);
1064
- }));
1065
- }
1066
- /** Execute one parsed plugin task request. */
1067
- async function processPluginTask(message, options = {}) {
1068
- await withPluginTaskLock(pluginTaskId(message), async () => {
1069
- const resolved = findPluginTask(message);
1070
- if (!resolved?.task) throw new Error(`Plugin task "${message.plugin}.${message.name}" is not registered`);
1071
- await resolved.task.run(taskPluginContext(resolved.plugin, message, options));
1072
- });
1073
- }
1074
- export { readAutomationExecutions as A, existingConversationId as C, readAutomationExecutionStatusDays as D, readAutomationExecutionHours as E, conversationAggregateColumns as F, conversationRangeColumns as I, readAutomationTitlesByConversationId as M, recordAutomationExecution as N, readAutomationExecutionStatusHours as O, conversationActiveDaysColumn as P, emptyAutomationRunWindows as S, readAutomationExecutionDays as T, markDispatchBlocked as _, claimDispatchMailboxAppend as a, markDispatchRunning as b, getDispatchConversationId as c, getDispatchRecord as d, getDispatchTurnId as f, markDispatchAwaitingResume as g, listPendingDispatchMailboxAppends as h, buildDispatchId as i, readAutomationRuns as j, readAutomationExecutionSummaries as k, getDispatchIdForConversation as l, isTerminalDispatchStatus as m, scheduleSessionCompletedPluginTasks as n, confirmDispatchMailboxAppend as o, getPluginDispatchProjection as p, task_runner_exports as r, createOrGetDispatch as s, processPluginTask as t, getDispatchInputMessageId as u, markDispatchCompleted as v, readAutomationExecutionByConversationId as w, notifyAutomationBlocked as x, markDispatchFailed as y };