@sentry/junior 0.147.0 → 0.149.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 (162) hide show
  1. package/dist/{agent-hooks-HS6VGUPM.js → agent-hooks-CWMCXAMT.js} +10 -10
  2. package/dist/api/conversations/access.d.ts +2 -1
  3. package/dist/api/conversations/aggregate.d.ts +2 -0
  4. package/dist/api/conversations/archive.d.ts +3 -5
  5. package/dist/api/conversations/create.d.ts +6 -0
  6. package/dist/api/conversations/detail.d.ts +2 -296
  7. package/dist/api/conversations/event-list.d.ts +2 -193
  8. package/dist/api/conversations/list.d.ts +4 -66
  9. package/dist/api/conversations/pending-messages.d.ts +15 -0
  10. package/dist/api/conversations/routes.d.ts +1 -1
  11. package/dist/api/conversations/stats.d.ts +0 -54
  12. package/dist/api/http.d.ts +2 -6
  13. package/dist/api/locations/detail.d.ts +3 -111
  14. package/dist/api/locations/list.d.ts +0 -45
  15. package/dist/api/locations/query.d.ts +2 -1
  16. package/dist/api/locations/routes.d.ts +1 -1
  17. package/dist/api/people/list.d.ts +0 -29
  18. package/dist/api/people/profile.d.ts +2 -106
  19. package/dist/api/people/profile.query.d.ts +2 -1
  20. package/dist/api/people/routes.d.ts +1 -1
  21. package/dist/api/people/shared.d.ts +3 -1
  22. package/dist/api/people/spend.d.ts +3 -4
  23. package/dist/api/personal-tokens/routes.d.ts +2 -2
  24. package/dist/api/route.d.ts +8 -14
  25. package/dist/api/schema/conversation.d.ts +130 -25
  26. package/dist/api/schema/location.d.ts +4 -0
  27. package/dist/api/schema/person.d.ts +7 -3
  28. package/dist/api/schema.d.ts +2 -2
  29. package/dist/api/schema.js +13 -1
  30. package/dist/api/stats.d.ts +0 -13
  31. package/dist/api/validation.d.ts +7 -0
  32. package/dist/api/viewer.d.ts +3 -0
  33. package/dist/api.d.ts +2 -1
  34. package/dist/api.js +772 -561
  35. package/dist/app.js +241 -204
  36. package/dist/{catalog-runtime-M6ZTPYRV.js → catalog-runtime-GAW5APVI.js} +5 -5
  37. package/dist/chat/actor.d.ts +4 -1
  38. package/dist/chat/agent/request.d.ts +2 -0
  39. package/dist/chat/agent/resume.d.ts +1 -0
  40. package/dist/chat/agent-dispatch/context.d.ts +2 -2
  41. package/dist/chat/agent-invocations/types.d.ts +52 -0
  42. package/dist/chat/api-turns/work.d.ts +108 -0
  43. package/dist/chat/app/services.d.ts +1 -1
  44. package/dist/chat/conversations/history.d.ts +60 -0
  45. package/dist/chat/conversations/provenance.d.ts +6 -0
  46. package/dist/chat/conversations/sql/actor-identity.d.ts +19 -0
  47. package/dist/chat/conversations/store.d.ts +1 -1
  48. package/dist/chat/resource-events/store.d.ts +1 -1
  49. package/dist/chat/runtime/reply-executor.d.ts +1 -3
  50. package/dist/chat/runtime/slack-runtime.d.ts +2 -1
  51. package/dist/chat/runtime/turn-preparation.d.ts +9 -0
  52. package/dist/chat/scheduled-tasks/tool-support.d.ts +5 -5
  53. package/dist/chat/scheduled-tasks/tools/create.d.ts +2 -2
  54. package/dist/chat/scheduled-tasks/tools/delete.d.ts +2 -2
  55. package/dist/chat/scheduled-tasks/tools/list.d.ts +2 -2
  56. package/dist/chat/scheduled-tasks/tools/run-now.d.ts +2 -2
  57. package/dist/chat/scheduled-tasks/tools/update.d.ts +2 -2
  58. package/dist/chat/services/conversation-memory.d.ts +2 -2
  59. package/dist/chat/services/conversation-title.d.ts +20 -0
  60. package/dist/chat/slack/assistant-thread/title.d.ts +10 -27
  61. package/dist/chat/slack/channel.d.ts +26 -0
  62. package/dist/chat/slack/dispatch-turn.d.ts +1 -0
  63. package/dist/chat/slack/timestamp-param.d.ts +0 -2
  64. package/dist/chat/slack/{tools → tool-support}/canvas/api.d.ts +1 -1
  65. package/dist/chat/slack/{tools → tool-support}/canvas/context.d.ts +3 -0
  66. package/dist/chat/slack/tool-support/channel-access.d.ts +42 -0
  67. package/dist/chat/slack/tool-support/channel-target.d.ts +27 -0
  68. package/dist/chat/slack/tool-support/list/params.d.ts +5 -0
  69. package/dist/chat/slack/tools/canvas/create.d.ts +1 -1
  70. package/dist/chat/slack/tools/channel-join.d.ts +28 -0
  71. package/dist/chat/slack/tools/channel-list-messages.d.ts +3 -2
  72. package/dist/chat/slack/tools/conversation-message-search.d.ts +2 -8
  73. package/dist/chat/slack/tools/message-add-reaction.d.ts +1 -1
  74. package/dist/chat/slack/tools/public-search.d.ts +63 -2
  75. package/dist/chat/slack/tools/send-files.d.ts +1 -1
  76. package/dist/chat/slack/tools/thread-read.d.ts +2 -5
  77. package/dist/chat/source.d.ts +2 -0
  78. package/dist/chat/state/conversation.d.ts +1 -0
  79. package/dist/chat/task-execution/checkpoint.d.ts +1 -0
  80. package/dist/chat/task-execution/state.d.ts +9 -4
  81. package/dist/chat/task-execution/store.d.ts +3 -2
  82. package/dist/chat/task-execution/turn-cursor.d.ts +2 -0
  83. package/dist/chat/task-execution/worker.d.ts +2 -0
  84. package/dist/chat/tasks/read.d.ts +1 -1
  85. package/dist/chat/tools/search-conversation-events.d.ts +1 -1
  86. package/dist/chat/tools/types.d.ts +11 -4
  87. package/dist/chat/tools/watch-resource-events.d.ts +2 -2
  88. package/dist/{chunk-ORO3PWKQ.js → chunk-25SC7D6Z.js} +1 -1
  89. package/dist/{chunk-GGB4WL5A.js → chunk-2JHKISQA.js} +1 -1
  90. package/dist/{chunk-AVZDSUFB.js → chunk-2P6BQQER.js} +4 -4
  91. package/dist/{chunk-7NVJVJID.js → chunk-32CXPIZE.js} +2 -2
  92. package/dist/{chunk-YSB2N563.js → chunk-3GYZ5YEH.js} +121 -87
  93. package/dist/{chunk-DU74WPIC.js → chunk-3I2IVWSZ.js} +1 -1
  94. package/dist/{chunk-SVHUEHUL.js → chunk-3JVUWLR7.js} +65 -4
  95. package/dist/{chunk-RXBVB2JM.js → chunk-3KGQOKIG.js} +1 -1
  96. package/dist/{chunk-YBVXBNSV.js → chunk-4JGM7FXO.js} +2 -2
  97. package/dist/{chunk-AZ7EAOBE.js → chunk-4YKPOYRV.js} +5 -5
  98. package/dist/chunk-5CWUQXXT.js +806 -0
  99. package/dist/{chunk-DLDRQHG5.js → chunk-6G5RCDKP.js} +1 -1
  100. package/dist/{chunk-AYU72WO7.js → chunk-75DSYFFB.js} +47 -70
  101. package/dist/{chunk-YHDERDRO.js → chunk-7BH46NB4.js} +91 -75
  102. package/dist/{chunk-745QJ3ZA.js → chunk-ALJG73MQ.js} +2 -2
  103. package/dist/chunk-ART7P442.js +48 -0
  104. package/dist/{chunk-7A4M7PCV.js → chunk-DK5UT6PQ.js} +1180 -556
  105. package/dist/{chunk-AIOK6LAG.js → chunk-EW2LNCRL.js} +813 -44
  106. package/dist/chunk-EXW76GW4.js +36 -0
  107. package/dist/chunk-GDFF3BQG.js +136 -0
  108. package/dist/{chunk-7UQIVT6G.js → chunk-JMH3DBM5.js} +6 -6
  109. package/dist/{chunk-CWP5RHVX.js → chunk-JMSMXXED.js} +12 -5
  110. package/dist/{chunk-CSZINQJ2.js → chunk-MEMURSBG.js} +1 -1
  111. package/dist/{chunk-YNTTROJT.js → chunk-QIW7OQEO.js} +1 -1
  112. package/dist/{chunk-Q4R73I43.js → chunk-RB26T3LU.js} +58 -28
  113. package/dist/{chunk-4LJO56OJ.js → chunk-RLVZIWNQ.js} +1 -1
  114. package/dist/{chunk-SGZO7UWH.js → chunk-SWHPVZBV.js} +2 -2
  115. package/dist/{chunk-PVNG6UMU.js → chunk-TFBNBXPU.js} +22 -10
  116. package/dist/{chunk-EFLJ2WJH.js → chunk-URDCZAL4.js} +5 -5
  117. package/dist/{chunk-6H5SZR24.js → chunk-UZGASGOD.js} +300 -383
  118. package/dist/{chunk-YEO3VX72.js → chunk-W7UONUIZ.js} +8 -6
  119. package/dist/{chunk-JUUKWRBE.js → chunk-X22XXSZA.js} +1 -1
  120. package/dist/{chunk-X5UY34KT.js → chunk-XPYWWMHR.js} +8 -8
  121. package/dist/{chunk-IIOOO4FQ.js → chunk-ZYMYLLWG.js} +2 -1
  122. package/dist/cli/chat.js +32 -35
  123. package/dist/cli/check.js +6 -6
  124. package/dist/cli/plugins.js +13 -13
  125. package/dist/cli/snapshot-create.js +9 -9
  126. package/dist/cli/upgrade.js +7 -7
  127. package/dist/db/schema/agent-invocations.d.ts +20 -0
  128. package/dist/db/schema/conversation-annotations.d.ts +2 -2
  129. package/dist/db/schema.d.ts +22 -2
  130. package/dist/{db-CSEW4RII.js → db-JBZLRIVY.js} +7 -7
  131. package/dist/instrumentation.js +4 -4
  132. package/dist/nitro.js +2 -2
  133. package/dist/{oauth-callback-server-FUEV5HNG.js → oauth-callback-server-MNTVYY2I.js} +24 -26
  134. package/dist/{runner-U3XHSKSH.js → runner-K24BWZSZ.js} +23 -25
  135. package/dist/{skills-AKE6W3WI.js → skills-AU5ST2CG.js} +6 -6
  136. package/dist/spawn-6PO7CVJ6.js +38 -0
  137. package/dist/{validation-WDTAULUF.js → validation-IXGZCFRO.js} +6 -6
  138. package/dist/version.js +1 -1
  139. package/dist/{work-C3HAO2BG.js → work-WCZIYILX.js} +18 -19
  140. package/dist/{worker-LNP4YKBM.js → worker-U2AACBEY.js} +11 -10
  141. package/package.json +6 -5
  142. package/dist/api/health.d.ts +0 -7
  143. package/dist/api/personal-tokens/create.d.ts +0 -11
  144. package/dist/api/personal-tokens/list.d.ts +0 -12
  145. package/dist/api/personal-tokens/revoke.d.ts +0 -5
  146. package/dist/api/plugin-reports.d.ts +0 -63
  147. package/dist/api/plugins.d.ts +0 -8
  148. package/dist/api/runtime.d.ts +0 -24
  149. package/dist/api/skills.d.ts +0 -6
  150. package/dist/chat/slack/tools/channel-access.d.ts +0 -30
  151. package/dist/chunk-34GS7B6L.js +0 -115
  152. package/dist/chunk-AVOCH2Z5.js +0 -285
  153. package/dist/chunk-HLXXQI5Q.js +0 -238
  154. package/dist/chunk-K4SZAP3K.js +0 -67
  155. package/dist/chunk-R776HKPK.js +0 -81
  156. package/dist/chunk-Z4YU7Z4L.js +0 -76
  157. package/dist/spawn-B6GDTWJ4.js +0 -39
  158. /package/dist/chat/slack/{tools → tool-support}/canvas/markdown.d.ts +0 -0
  159. /package/dist/chat/slack/{tools → tool-support}/channel-capabilities.d.ts +0 -0
  160. /package/dist/chat/slack/{tools → tool-support}/context.d.ts +0 -0
  161. /package/dist/chat/slack/{tools → tool-support}/list/api.d.ts +0 -0
  162. /package/dist/chat/slack/{tools → tool-support}/slack-message-url.d.ts +0 -0
@@ -0,0 +1,806 @@
1
+ import {
2
+ appendAndEnqueueInboundMessage
3
+ } from "./chunk-JMSMXXED.js";
4
+ import {
5
+ buildDeliveredTurnStatePatch,
6
+ finalizeFailedTurnReplyWithEvent,
7
+ getTurnUserMessage,
8
+ processPluginTask,
9
+ scheduleSessionCompletedPluginTasks
10
+ } from "./chunk-UZGASGOD.js";
11
+ import {
12
+ ConversationTurnLifecycleService,
13
+ coerceThreadConversationState,
14
+ getPersistedSandboxState,
15
+ getPersistedThreadState,
16
+ persistThreadStateById
17
+ } from "./chunk-4YKPOYRV.js";
18
+ import {
19
+ healthReportSchema
20
+ } from "./chunk-UTL3MHPC.js";
21
+ import {
22
+ parseSlackThreadId
23
+ } from "./chunk-MEMURSBG.js";
24
+ import {
25
+ TurnInputCommitLostError,
26
+ buildConversationContext,
27
+ buildDeterministicTurnId,
28
+ getAssistantReplyText,
29
+ getConversationTurnBoundaryError,
30
+ getTurnRecord,
31
+ hydrateConversationMessages,
32
+ isTurnInputCommitLostError,
33
+ listTurnSummaries,
34
+ loadProjection,
35
+ markConversationMessage,
36
+ markTurnFailed,
37
+ normalizeConversationText,
38
+ persistConversationMessages,
39
+ persistWithRetry,
40
+ recordDeliveredAssistantMessage,
41
+ saveTurnCheckpoint,
42
+ startActiveTurn,
43
+ upsertConversationMessage
44
+ } from "./chunk-EW2LNCRL.js";
45
+ import {
46
+ normalizeSlackConversationId
47
+ } from "./chunk-SWHPVZBV.js";
48
+ import {
49
+ getConversationEventStore,
50
+ getConversationStore
51
+ } from "./chunk-3GYZ5YEH.js";
52
+ import {
53
+ credentialContextForActor
54
+ } from "./chunk-RUIXF3YQ.js";
55
+ import {
56
+ logException,
57
+ setTags,
58
+ withLogContext
59
+ } from "./chunk-RLVZIWNQ.js";
60
+
61
+ // src/handlers/health.ts
62
+ function readHealthReport() {
63
+ return healthReportSchema.parse({
64
+ status: "ok",
65
+ service: "junior",
66
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
67
+ });
68
+ }
69
+ function GET() {
70
+ return Response.json(readHealthReport());
71
+ }
72
+
73
+ // src/chat/api-turns/work.ts
74
+ import { createHash } from "crypto";
75
+ import { z } from "zod";
76
+ import {
77
+ createWebSource,
78
+ localDestinationSchema
79
+ } from "@sentry/junior-plugin-api";
80
+ var apiTurnMailboxMetadataSchema = z.object({
81
+ authorEmail: z.string().email(),
82
+ authorFullName: z.string().min(1).optional(),
83
+ authorUserId: z.string().min(1),
84
+ authorUserName: z.string().min(1).optional(),
85
+ kind: z.literal("api_turn"),
86
+ messageId: z.string().min(1)
87
+ }).strict();
88
+ function normalizeEmail(email) {
89
+ return email.trim().toLowerCase();
90
+ }
91
+ function stableHex(...parts) {
92
+ return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 24);
93
+ }
94
+ function createApiConversationId(args) {
95
+ return `local:web:${stableHex(
96
+ normalizeEmail(args.actorEmail),
97
+ args.idempotencyKey
98
+ )}`;
99
+ }
100
+ function apiMessageId(args) {
101
+ return `api-msg:${stableHex(args.conversationId, args.idempotencyKey)}`;
102
+ }
103
+ function apiTurnIdForMessage(messageId) {
104
+ return buildDeterministicTurnId(messageId);
105
+ }
106
+ function requireLocalDestination(conversationId) {
107
+ const parsed = localDestinationSchema.safeParse({
108
+ platform: "local",
109
+ conversationId
110
+ });
111
+ if (!parsed.success) {
112
+ throw new Error(`Invalid local conversation id: ${conversationId}`);
113
+ }
114
+ return parsed.data;
115
+ }
116
+ function resolveApiTurnDestination(args) {
117
+ if (args.existing) {
118
+ return args.existing;
119
+ }
120
+ return requireLocalDestination(args.conversationId);
121
+ }
122
+ function webSourceForConversation(args) {
123
+ return createWebSource(
124
+ args.conversationId,
125
+ args.visibility === "private" ? "private" : "public"
126
+ );
127
+ }
128
+ function actorFromMetadata(metadata) {
129
+ return {
130
+ platform: "web",
131
+ userId: metadata.authorUserId,
132
+ email: normalizeEmail(metadata.authorEmail),
133
+ ...metadata.authorFullName ? { fullName: metadata.authorFullName } : {},
134
+ ...metadata.authorUserName ? { userName: metadata.authorUserName } : {}
135
+ };
136
+ }
137
+ function storedActorFromApi(actor) {
138
+ return {
139
+ ...actor.email ? { email: normalizeEmail(actor.email) } : {},
140
+ ...actor.fullName ? { fullName: actor.fullName } : {}
141
+ };
142
+ }
143
+ function webActorFromEmail(email, profile) {
144
+ const normalized = normalizeEmail(email);
145
+ return {
146
+ platform: "web",
147
+ userId: `dashboard:${stableHex(normalized)}`,
148
+ email: normalized,
149
+ ...profile?.fullName ? { fullName: profile.fullName } : {},
150
+ ...profile?.userName ? { userName: profile.userName } : {}
151
+ };
152
+ }
153
+ function actorFromStoredConversation(stored) {
154
+ const email = stored?.email?.trim().toLowerCase();
155
+ if (!email) {
156
+ return void 0;
157
+ }
158
+ return webActorFromEmail(
159
+ email,
160
+ stored?.fullName ? { fullName: stored.fullName } : void 0
161
+ );
162
+ }
163
+ function buildApiTurnInboundMessage(args) {
164
+ const text = args.message.trim();
165
+ if (!text) {
166
+ throw new Error("API conversation message must not be empty");
167
+ }
168
+ if (!args.actor.email) {
169
+ throw new Error("API conversation actor requires a verified email");
170
+ }
171
+ const destination = resolveApiTurnDestination({
172
+ conversationId: args.conversationId,
173
+ existing: args.destination
174
+ });
175
+ const nowMs = args.nowMs ?? Date.now();
176
+ return {
177
+ conversationId: args.conversationId,
178
+ createdAtMs: args.createdAtMs ?? nowMs,
179
+ delivery: "defer",
180
+ destination,
181
+ inboundMessageId: args.messageId,
182
+ input: {
183
+ authorId: args.actor.userId,
184
+ text,
185
+ metadata: {
186
+ authorEmail: normalizeEmail(args.actor.email),
187
+ ...args.actor.fullName ? { authorFullName: args.actor.fullName } : {},
188
+ authorUserId: args.actor.userId,
189
+ ...args.actor.userName ? { authorUserName: args.actor.userName } : {},
190
+ kind: "api_turn",
191
+ messageId: args.messageId
192
+ }
193
+ },
194
+ receivedAtMs: nowMs,
195
+ publishExternally: false,
196
+ source: "web"
197
+ };
198
+ }
199
+ async function recordApiConversationActivity(args) {
200
+ const store = args.conversationStore ?? getConversationStore();
201
+ const existing = await store.get({ conversationId: args.conversationId });
202
+ const destination = resolveApiTurnDestination({
203
+ conversationId: args.conversationId,
204
+ existing: existing?.destination
205
+ });
206
+ const isNewRoot = !existing;
207
+ const visibility = args.rootVisibility === "private" ? "private" : "public";
208
+ const source = isNewRoot ? webSourceForConversation({
209
+ conversationId: args.conversationId,
210
+ visibility
211
+ }) : void 0;
212
+ await store.recordActivity({
213
+ conversationId: args.conversationId,
214
+ destination,
215
+ nowMs: args.nowMs,
216
+ actor: storedActorFromApi(args.actor),
217
+ // Do not rewrite a Slack root's origin source when a dashboard participant
218
+ // continues it. Mailbox entries still carry source "web" per turn.
219
+ ...isNewRoot ? { source: "web" } : {},
220
+ ...source ? { sessionSource: source } : {},
221
+ ...isNewRoot ? { visibility } : {}
222
+ });
223
+ return destination;
224
+ }
225
+ async function createAndEnqueueApiConversation(input, options) {
226
+ if (!input.actor.email) {
227
+ throw new Error("API conversation actor requires a verified email");
228
+ }
229
+ const conversationId = createApiConversationId({
230
+ actorEmail: input.actor.email,
231
+ idempotencyKey: input.idempotencyKey
232
+ });
233
+ return await appendAndEnqueueApiConversationMessage(
234
+ {
235
+ actor: input.actor,
236
+ conversationId,
237
+ idempotencyKey: input.idempotencyKey,
238
+ message: input.message,
239
+ rootVisibility: input.visibility === "private" ? "private" : "public"
240
+ },
241
+ options
242
+ );
243
+ }
244
+ async function appendAndEnqueueApiConversationMessage(input, options) {
245
+ const text = input.message.trim();
246
+ if (!text) {
247
+ throw new Error("API conversation message must not be empty");
248
+ }
249
+ if (!input.actor.email) {
250
+ throw new Error("API conversation actor requires a verified email");
251
+ }
252
+ const nowMs = options.nowMs ?? Date.now();
253
+ const messageId = apiMessageId({
254
+ conversationId: input.conversationId,
255
+ idempotencyKey: input.idempotencyKey
256
+ });
257
+ const destination = await recordApiConversationActivity({
258
+ actor: input.actor,
259
+ conversationId: input.conversationId,
260
+ conversationStore: options.conversationStore,
261
+ nowMs,
262
+ ...input.rootVisibility ? { rootVisibility: input.rootVisibility } : {}
263
+ });
264
+ const result = await appendAndEnqueueInboundMessage({
265
+ message: buildApiTurnInboundMessage({
266
+ actor: input.actor,
267
+ conversationId: input.conversationId,
268
+ destination,
269
+ message: text,
270
+ messageId,
271
+ nowMs
272
+ }),
273
+ conversationStore: options.conversationStore,
274
+ nowMs,
275
+ queue: options.queue,
276
+ state: options.state
277
+ });
278
+ return {
279
+ conversationId: input.conversationId,
280
+ messageId,
281
+ status: result.status === "duplicate" ? "duplicate" : "accepted"
282
+ };
283
+ }
284
+ function parseApiTurnMessages(messages) {
285
+ if (messages.length === 0) {
286
+ return [];
287
+ }
288
+ const parsed = messages.map((message) => ({
289
+ message,
290
+ metadata: apiTurnMailboxMetadataSchema.safeParse(message.input.metadata)
291
+ }));
292
+ if (parsed.every((entry) => !entry.metadata.success)) {
293
+ return [];
294
+ }
295
+ if (parsed.some((entry) => !entry.metadata.success)) {
296
+ throw new Error("Conversation mailbox mixes web turns and other input");
297
+ }
298
+ return parsed.map((entry) => {
299
+ if (!entry.metadata.success) {
300
+ throw new Error("API turn mailbox metadata failed validation");
301
+ }
302
+ return { message: entry.message, metadata: entry.metadata.data };
303
+ });
304
+ }
305
+ async function resolveApiTurnWork(context) {
306
+ const batch = parseApiTurnMessages(context.attempt.messages);
307
+ if (batch.length > 0) {
308
+ return { kind: "mailbox", batch };
309
+ }
310
+ if (context.attempt.messages.length > 0) {
311
+ return void 0;
312
+ }
313
+ const summaries = await listTurnSummaries(context.conversationId);
314
+ const active = summaries.filter(
315
+ (summary) => summary.surface === "api" && !summary.dispatchId && (summary.state === "paused" || summary.state === "running")
316
+ );
317
+ if (active.length > 1) {
318
+ throw new Error(
319
+ `Conversation ${context.conversationId} has multiple active web turns`
320
+ );
321
+ }
322
+ const turnId = active[0]?.turnId;
323
+ if (!turnId) {
324
+ return void 0;
325
+ }
326
+ const record = await getTurnRecord(context.conversationId, turnId);
327
+ if (!record || record.surface !== "api" || Boolean(record.dispatchId) || record.state !== "paused" && record.state !== "running") {
328
+ return void 0;
329
+ }
330
+ return { kind: "resume", turnId };
331
+ }
332
+ async function isApiTurnWork(context) {
333
+ return await resolveApiTurnWork(context) !== void 0;
334
+ }
335
+ function captureApiBoundaryFailure(args) {
336
+ const eventId = logException(args.error, `api.turn.${args.failureCode}`, {
337
+ conversationId: args.conversationId,
338
+ ...args.runId ? { runId: args.runId } : {},
339
+ turnId: args.turnId
340
+ });
341
+ return typeof eventId === "string" ? eventId : void 0;
342
+ }
343
+ function createApiTurnWorker(options) {
344
+ return async (context) => {
345
+ const resolved = await resolveApiTurnWork(context);
346
+ if (!resolved) {
347
+ throw new Error(
348
+ `API turn worker received non-API work for ${context.conversationId}`
349
+ );
350
+ }
351
+ if (context.publishExternally) {
352
+ throw new Error(
353
+ `API turn work must not publish externally for ${context.conversationId}`
354
+ );
355
+ }
356
+ const lifecycle = options.turnLifecycle ?? new ConversationTurnLifecycleService(getConversationEventStore());
357
+ const isResume = resolved.kind === "resume";
358
+ let actor;
359
+ let text;
360
+ let turnId;
361
+ let userMessageId;
362
+ let startedAtMs;
363
+ let inputMessageIds;
364
+ const storedConversation = await getConversationStore().get({
365
+ conversationId: context.conversationId
366
+ });
367
+ if (resolved.kind === "mailbox") {
368
+ const first = resolved.batch[0];
369
+ text = resolved.batch.map((entry) => entry.message.input.text.trim()).filter(Boolean).join("\n\n");
370
+ actor = actorFromMetadata(first.metadata);
371
+ turnId = apiTurnIdForMessage(first.metadata.messageId);
372
+ userMessageId = first.metadata.messageId;
373
+ startedAtMs = first.message.createdAtMs;
374
+ inputMessageIds = resolved.batch.map((entry) => entry.metadata.messageId);
375
+ } else {
376
+ turnId = resolved.turnId;
377
+ const resumedActor = actorFromStoredConversation(
378
+ storedConversation?.actor
379
+ );
380
+ if (!resumedActor) {
381
+ throw new Error(
382
+ `API turn resume missing actor for ${context.conversationId}`
383
+ );
384
+ }
385
+ actor = resumedActor;
386
+ text = "";
387
+ userMessageId = "";
388
+ startedAtMs = Date.now();
389
+ inputMessageIds = [];
390
+ }
391
+ const destination = resolveApiTurnDestination({
392
+ conversationId: context.conversationId,
393
+ existing: context.destination ?? (resolved.kind === "mailbox" ? resolved.batch[0]?.message.destination : void 0) ?? storedConversation?.destination
394
+ });
395
+ const source = webSourceForConversation({
396
+ conversationId: context.conversationId,
397
+ visibility: storedConversation?.visibility
398
+ });
399
+ return await withLogContext(
400
+ {
401
+ conversationId: context.conversationId,
402
+ platform: "web",
403
+ userId: actor.userId,
404
+ ...actor.userName ? { userName: actor.userName } : {}
405
+ },
406
+ async () => {
407
+ let acknowledged = isResume || context.attempt.messages.length === 0;
408
+ const acknowledge = async () => {
409
+ if (acknowledged) {
410
+ return;
411
+ }
412
+ try {
413
+ await context.attempt.ack();
414
+ } catch {
415
+ throw new TurnInputCommitLostError(
416
+ `Conversation work lease lost before API turn inbox ack for ${context.conversationId}`
417
+ );
418
+ }
419
+ acknowledged = true;
420
+ };
421
+ const persisted = await getPersistedThreadState(context.conversationId);
422
+ const conversation = coerceThreadConversationState(persisted);
423
+ await hydrateConversationMessages({
424
+ conversation,
425
+ conversationId: context.conversationId
426
+ });
427
+ let sandboxRef = getPersistedSandboxState(persisted);
428
+ const initialSandboxRef = sandboxRef;
429
+ if (isResume) {
430
+ const userMessage = getTurnUserMessage(conversation, turnId);
431
+ if (!userMessage) {
432
+ throw new Error(
433
+ `Unable to locate the persisted user message for API turn "${turnId}"`
434
+ );
435
+ }
436
+ userMessageId = userMessage.id;
437
+ text = userMessage.text;
438
+ startedAtMs = userMessage.createdAtMs;
439
+ inputMessageIds = [userMessageId];
440
+ } else {
441
+ upsertConversationMessage(conversation, {
442
+ id: userMessageId,
443
+ role: "user",
444
+ text: normalizeConversationText(text),
445
+ createdAtMs: startedAtMs,
446
+ author: {
447
+ ...actor.fullName ? { fullName: actor.fullName } : {},
448
+ userId: actor.userId,
449
+ ...actor.userName ? { userName: actor.userName } : {}
450
+ },
451
+ meta: {
452
+ explicitMention: true,
453
+ replied: false,
454
+ source: "web"
455
+ }
456
+ });
457
+ await persistConversationMessages({
458
+ conversation,
459
+ conversationId: context.conversationId
460
+ });
461
+ await lifecycle.start({
462
+ conversationId: context.conversationId,
463
+ createdAtMs: Date.now(),
464
+ inputMessageIds,
465
+ surface: "api",
466
+ turnId
467
+ });
468
+ startActiveTurn({
469
+ conversation,
470
+ nextTurnId: turnId
471
+ });
472
+ }
473
+ let currentRunId;
474
+ let assistantMessageDelivered = false;
475
+ let failureCode = "persistence_failed";
476
+ let modelFailureEventId;
477
+ let modelFailureCaptureAttempted = false;
478
+ let reply;
479
+ const deliverAssistantMessage = async (value) => {
480
+ const replyText = typeof value === "string" ? value : getAssistantReplyText(value);
481
+ if (!replyText?.trim()) {
482
+ return;
483
+ }
484
+ failureCode = "delivery_failed";
485
+ assistantMessageDelivered = true;
486
+ recordDeliveredAssistantMessage({
487
+ conversation,
488
+ sessionId: turnId,
489
+ source: "web",
490
+ text: replyText,
491
+ userMessageId
492
+ });
493
+ try {
494
+ await persistWithRetry(
495
+ () => persistConversationMessages({
496
+ conversation,
497
+ conversationId: context.conversationId
498
+ })
499
+ );
500
+ } catch (error) {
501
+ logException(
502
+ new Error("Accepted assistant message persistence failed"),
503
+ "api.assistant.message_post_delivery_persist.failed",
504
+ {
505
+ "error.type": error instanceof Error ? error.name : typeof error
506
+ }
507
+ );
508
+ }
509
+ failureCode = "agent_run_failed";
510
+ };
511
+ try {
512
+ await persistThreadStateById(context.conversationId, {
513
+ conversation
514
+ });
515
+ const piMessages = await loadProjection({
516
+ conversationId: context.conversationId
517
+ });
518
+ failureCode = "agent_run_failed";
519
+ currentRunId = `api-run:${stableHex(turnId, String(startedAtMs))}`;
520
+ setTags({ runId: currentRunId });
521
+ const outcome = await options.agentRunner.run({
522
+ conversationId: context.conversationId,
523
+ turnId,
524
+ runId: currentRunId,
525
+ input: {
526
+ messageText: text,
527
+ conversationContext: buildConversationContext(conversation, {
528
+ excludeMessageId: userMessageId
529
+ }),
530
+ piMessages
531
+ },
532
+ routing: {
533
+ actor,
534
+ credentialContext: credentialContextForActor(actor),
535
+ destination,
536
+ publishExternally: false,
537
+ source,
538
+ surface: "api"
539
+ },
540
+ policy: {
541
+ disabledFeatures: ["interactive-auth"]
542
+ },
543
+ state: {
544
+ pendingAuth: conversation.processing.pendingAuth,
545
+ sandboxRef
546
+ },
547
+ delivery: deliverAssistantMessage,
548
+ durability: {
549
+ onInputCommitted: acknowledge,
550
+ shouldYield: context.shouldYield,
551
+ onSandboxRefChanged: async (nextSandboxRef) => {
552
+ sandboxRef = nextSandboxRef;
553
+ await persistThreadStateById(context.conversationId, {
554
+ conversation,
555
+ sandboxRef
556
+ });
557
+ }
558
+ }
559
+ });
560
+ if (outcome.status === "suspended") {
561
+ return { status: "yielded" };
562
+ }
563
+ if (outcome.status !== "completed") {
564
+ throw new Error(`API agent run ended with ${outcome.status}`);
565
+ }
566
+ reply = outcome.result;
567
+ modelFailureCaptureAttempted = reply.diagnostics.outcome !== "success";
568
+ const finalized = finalizeFailedTurnReplyWithEvent({
569
+ reply,
570
+ logException
571
+ });
572
+ reply = finalized.reply;
573
+ modelFailureEventId = finalized.eventId;
574
+ if (reply.diagnostics.outcome !== "success") {
575
+ await deliverAssistantMessage(reply.text);
576
+ }
577
+ const completedState = buildDeliveredTurnStatePatch({
578
+ conversation,
579
+ reply,
580
+ sessionId: turnId,
581
+ userMessageId
582
+ });
583
+ await persistThreadStateById(context.conversationId, {
584
+ conversation: completedState.conversation,
585
+ sandboxRef: reply.sandboxRef ?? sandboxRef
586
+ });
587
+ if (reply.piMessages?.length) {
588
+ const latest = await getTurnRecord(context.conversationId, turnId);
589
+ await saveTurnCheckpoint({
590
+ mode: "completed",
591
+ conversationId: context.conversationId,
592
+ turnId,
593
+ sliceId: latest?.sliceId ?? 1,
594
+ messages: reply.piMessages,
595
+ durationMs: reply.diagnostics.durationMs,
596
+ usage: reply.diagnostics.usage,
597
+ destination,
598
+ publishExternally: false,
599
+ source,
600
+ actor,
601
+ surface: "api"
602
+ });
603
+ }
604
+ if (reply.diagnostics.outcome === "success") {
605
+ await lifecycle.complete({
606
+ conversationId: context.conversationId,
607
+ createdAtMs: Date.now(),
608
+ outcome: assistantMessageDelivered ? "success" : "no_reply",
609
+ turnId
610
+ });
611
+ try {
612
+ await scheduleSessionCompletedPluginTasks(
613
+ {
614
+ conversationId: context.conversationId,
615
+ sessionId: turnId
616
+ },
617
+ {
618
+ send: async (message) => {
619
+ try {
620
+ await processPluginTask(message);
621
+ } catch (error) {
622
+ logException(
623
+ error,
624
+ "api.plugin.session_completion_task.failed",
625
+ {
626
+ conversationId: context.conversationId,
627
+ pluginName: message.plugin,
628
+ taskName: message.name,
629
+ turnId
630
+ }
631
+ );
632
+ }
633
+ }
634
+ }
635
+ );
636
+ } catch (error) {
637
+ logException(error, "api.plugin.session_completion_task.failed", {
638
+ conversationId: context.conversationId,
639
+ turnId
640
+ });
641
+ }
642
+ } else {
643
+ await lifecycle.fail({
644
+ conversationId: context.conversationId,
645
+ createdAtMs: Date.now(),
646
+ ...modelFailureEventId ? { eventId: modelFailureEventId } : {},
647
+ failureCode: "model_execution_failed",
648
+ turnId
649
+ });
650
+ }
651
+ await acknowledge();
652
+ return { status: "completed" };
653
+ } catch (error) {
654
+ const cause = getConversationTurnBoundaryError(error)?.cause ?? error;
655
+ if (isTurnInputCommitLostError(error) || isTurnInputCommitLostError(cause)) {
656
+ return { status: "lost_lease" };
657
+ }
658
+ if (!context.attempt.isFinalAttempt) {
659
+ throw error;
660
+ }
661
+ const failureEventId = modelFailureCaptureAttempted && failureCode === "agent_run_failed" ? modelFailureEventId : captureApiBoundaryFailure({
662
+ conversationId: context.conversationId,
663
+ error,
664
+ failureCode,
665
+ runId: currentRunId,
666
+ turnId
667
+ });
668
+ try {
669
+ markTurnFailed({
670
+ conversation,
671
+ nowMs: Date.now(),
672
+ sessionId: turnId,
673
+ userMessageId,
674
+ markConversationMessage
675
+ });
676
+ await persistThreadStateById(context.conversationId, {
677
+ conversation,
678
+ sandboxRef: initialSandboxRef ?? null
679
+ });
680
+ } catch (persistenceError) {
681
+ captureApiBoundaryFailure({
682
+ conversationId: context.conversationId,
683
+ error: persistenceError,
684
+ failureCode: "persistence_failed",
685
+ runId: currentRunId,
686
+ turnId
687
+ });
688
+ }
689
+ await lifecycle.fail({
690
+ conversationId: context.conversationId,
691
+ createdAtMs: Date.now(),
692
+ ...failureEventId ? { eventId: failureEventId } : {},
693
+ failureCode,
694
+ turnId
695
+ });
696
+ await acknowledge();
697
+ return { status: "completed" };
698
+ }
699
+ }
700
+ );
701
+ };
702
+ }
703
+ function routeApiTurnWork(options) {
704
+ return async (context) => {
705
+ return await isApiTurnWork(context) ? await options.apiTurnWorker(context) : await options.fallbackWorker(context);
706
+ };
707
+ }
708
+
709
+ // src/chat/slack/conversation-context.ts
710
+ function normalizeConversationName(type, channelName) {
711
+ const trimmed = channelName?.trim();
712
+ if (!trimmed) return void 0;
713
+ if (type === "public_channel" || type === "private_channel" || type === "private_channel_or_group_dm") {
714
+ return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
715
+ }
716
+ return trimmed;
717
+ }
718
+ function typeFromSlackChannelType(channelType) {
719
+ if (channelType === "channel") return "public_channel";
720
+ if (channelType === "group") return "private_channel";
721
+ if (channelType === "mpim") return "group_dm";
722
+ if (channelType === "im") return "direct_message";
723
+ return void 0;
724
+ }
725
+ function typeFromChannelId(channelId, channelName) {
726
+ const normalized = normalizeSlackConversationId(channelId);
727
+ if (!normalized) return void 0;
728
+ if (normalized.startsWith("C")) return "public_channel";
729
+ if (normalized.startsWith("D")) return "direct_message";
730
+ if (normalized.startsWith("G")) {
731
+ return channelName?.trim().startsWith("mpdm-") ? "group_dm" : "private_channel_or_group_dm";
732
+ }
733
+ return void 0;
734
+ }
735
+ function toSlackEventChannelType(channelType) {
736
+ if (channelType === "channel" || channelType === "group" || channelType === "mpim" || channelType === "im") {
737
+ return channelType;
738
+ }
739
+ return void 0;
740
+ }
741
+ function conversationVisibilityFromSlackChannelType(channelType) {
742
+ if (!channelType) return void 0;
743
+ return channelType === "channel" ? "public" : "private";
744
+ }
745
+ function resolveSlackChannelTypeFromMessage(message) {
746
+ const raw = message.raw;
747
+ if (!raw || typeof raw !== "object") {
748
+ return void 0;
749
+ }
750
+ const channelType = raw.channel_type;
751
+ return typeof channelType === "string" ? toSlackEventChannelType(channelType.trim()) : void 0;
752
+ }
753
+ function resolveSlackConversationContext(input) {
754
+ const type = typeFromSlackChannelType(input.channelType) ?? typeFromChannelId(input.channelId, input.channelName);
755
+ if (!type) return void 0;
756
+ const name = normalizeConversationName(type, input.channelName);
757
+ const visibility = conversationVisibilityFromSlackChannelType(
758
+ input.channelType
759
+ );
760
+ return {
761
+ type,
762
+ ...name ? { name } : {},
763
+ ...visibility ? { visibility } : {}
764
+ };
765
+ }
766
+ function resolveSlackConversationContextFromThreadId(input) {
767
+ const slackThread = parseSlackThreadId(input.threadId);
768
+ const context = resolveSlackConversationContext({
769
+ channelId: slackThread?.channelId,
770
+ channelName: input.channelName
771
+ });
772
+ if (!context || !input.visibility) {
773
+ return context;
774
+ }
775
+ return {
776
+ ...context,
777
+ type: input.visibility === "private" && context.type === "public_channel" ? "private_channel" : context.type,
778
+ visibility: input.visibility
779
+ };
780
+ }
781
+ function formatSlackConversationTypeLabel(type) {
782
+ if (type === "public_channel") return "Public Channel";
783
+ if (type === "private_channel") return "Private Channel";
784
+ if (type === "group_dm") return "Group DM";
785
+ if (type === "direct_message") return "Direct Message";
786
+ return "Private Channel or Group DM";
787
+ }
788
+ function formatSlackConversationRedactedLabel(context) {
789
+ if (!context) return void 0;
790
+ return formatSlackConversationTypeLabel(context.type);
791
+ }
792
+
793
+ export {
794
+ readHealthReport,
795
+ GET,
796
+ conversationVisibilityFromSlackChannelType,
797
+ resolveSlackChannelTypeFromMessage,
798
+ resolveSlackConversationContext,
799
+ resolveSlackConversationContextFromThreadId,
800
+ formatSlackConversationRedactedLabel,
801
+ webActorFromEmail,
802
+ createAndEnqueueApiConversation,
803
+ appendAndEnqueueApiConversationMessage,
804
+ createApiTurnWorker,
805
+ routeApiTurnWork
806
+ };