@sentry/junior 0.107.1 → 0.109.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 (68) hide show
  1. package/dist/{agent-hooks-ICPIJAFY.js → agent-hooks-FHHVLNPA.js} +12 -12
  2. package/dist/api/conversations/events.d.ts +1 -1
  3. package/dist/api/conversations/list.d.ts +1 -0
  4. package/dist/api/conversations/schema.d.ts +57 -6
  5. package/dist/api/schema.js +1 -1
  6. package/dist/api.js +116 -27
  7. package/dist/app.d.ts +1 -0
  8. package/dist/app.js +1741 -458
  9. package/dist/{catalog-runtime-FOFNPR3P.js → catalog-runtime-DMGJSLV2.js} +6 -4
  10. package/dist/chat/conversations/history.d.ts +16 -5
  11. package/dist/chat/conversations/message-projection.d.ts +3 -5
  12. package/dist/chat/conversations/sql/store.d.ts +0 -7
  13. package/dist/chat/log-context.d.ts +40 -0
  14. package/dist/chat/logging.d.ts +3 -26
  15. package/dist/chat/resource-events/store.d.ts +6 -0
  16. package/dist/chat/runtime/slack-resume.d.ts +2 -2
  17. package/dist/chat/runtime/slack-runtime.d.ts +3 -0
  18. package/dist/chat/services/turn-session-record.d.ts +0 -2
  19. package/dist/chat/slack/reply.d.ts +7 -19
  20. package/dist/chat/state/conversation.d.ts +1 -0
  21. package/dist/chat/task-execution/slack-work.d.ts +237 -10
  22. package/dist/chat/tool-support/turn-deadline-result.d.ts +3 -0
  23. package/dist/chunk-3SXUV423.js +10 -0
  24. package/dist/{chunk-YFQ7CQDE.js → chunk-72OMGPDS.js} +1017 -39
  25. package/dist/{chunk-2XXEKR44.js → chunk-7Q6WCISF.js} +1 -1
  26. package/dist/{chunk-NVOTGWYX.js → chunk-FENLIFJD.js} +97 -601
  27. package/dist/{chunk-IGHMVDWI.js → chunk-FWJRXN4F.js} +3 -3
  28. package/dist/{chunk-PUAMO4PI.js → chunk-HHVY4SMP.js} +41 -7
  29. package/dist/{chunk-SPUAJVVH.js → chunk-HO5NT6OE.js} +5 -7
  30. package/dist/{chunk-O24R4DCK.js → chunk-ISMWX44P.js} +93 -55
  31. package/dist/{chunk-7FBGKXPE.js → chunk-IVVGFRQQ.js} +5 -0
  32. package/dist/{chunk-EDLNHZH3.js → chunk-IXKSQGIO.js} +226 -191
  33. package/dist/{chunk-SS67LUOK.js → chunk-JNAVF45E.js} +6 -7
  34. package/dist/{chunk-DY7TRYB5.js → chunk-MGUMR35T.js} +1 -1
  35. package/dist/{chunk-CQ7KSO2B.js → chunk-MXW4GL53.js} +11 -15
  36. package/dist/{chunk-RT5BUHR5.js → chunk-O3JMSZLI.js} +1 -1
  37. package/dist/chunk-OCD3OZRK.js +125 -0
  38. package/dist/{chunk-B2Z2H66D.js → chunk-OUHNWH46.js} +60 -5
  39. package/dist/{chunk-VIXFG2VC.js → chunk-PLB7MRCQ.js} +1 -2
  40. package/dist/{chunk-RMVOAJRL.js → chunk-QY636HZU.js} +114 -12
  41. package/dist/{chunk-VH6KWKG2.js → chunk-RGX4V75M.js} +3 -3
  42. package/dist/{chunk-B7PUBQQM.js → chunk-SYAQIR5Z.js} +1 -1
  43. package/dist/chunk-TMBRFULF.js +471 -0
  44. package/dist/{chunk-YIPD3L6L.js → chunk-TT6GMXNA.js} +8 -10
  45. package/dist/{chunk-YT72TOEK.js → chunk-WHMOBKT7.js} +2 -2
  46. package/dist/cli/chat.js +23 -23
  47. package/dist/cli/check.js +8 -6
  48. package/dist/cli/plugins.js +16 -16
  49. package/dist/cli/snapshot-warmup.js +10 -9
  50. package/dist/cli/upgrade.js +314 -3505
  51. package/dist/db/db.d.ts +1 -1
  52. package/dist/{db-DIGO4TGW.js → db-V3NU72O2.js} +8 -6
  53. package/dist/instrumentation.js +3 -2
  54. package/dist/nitro.js +5 -4
  55. package/dist/{runner-ACR2HAIC.js → runner-JYBCQBLK.js} +17 -20
  56. package/dist/{skills-ITNHBARB.js → skills-U6MDAST7.js} +7 -5
  57. package/dist/{validation-AGQ46ZGV.js → validation-72N3JVRU.js} +7 -5
  58. package/dist/version.d.ts +2 -0
  59. package/dist/version.js +7 -0
  60. package/migrations/0006_drop_legacy_conversation_messages.sql +1 -0
  61. package/migrations/README.md +12 -9
  62. package/migrations/meta/0006_snapshot.json +948 -0
  63. package/migrations/meta/_journal.json +7 -0
  64. package/package.json +11 -7
  65. package/dist/chunk-4YF7Z6IA.js +0 -1061
  66. package/dist/chunk-AUUOHQAT.js +0 -115
  67. package/dist/chunk-G3E7SCME.js +0 -28
  68. package/dist/chunk-YNP2ATQX.js +0 -1282
package/dist/app.js CHANGED
@@ -6,31 +6,7 @@ import {
6
6
  processPluginTask,
7
7
  requireTurnFailureEventId,
8
8
  scheduleSessionCompletedPluginTasks
9
- } from "./chunk-CQ7KSO2B.js";
10
- import {
11
- CONVERSATION_WORK_CHECK_IN_INTERVAL_MS,
12
- CONVERSATION_WORK_STALE_ENQUEUE_MS,
13
- ackMessages,
14
- appendInboundMessage,
15
- checkInConversationWork,
16
- clearConsumedConversationWake,
17
- clearExpiredConversationLease,
18
- completeConversationWork,
19
- deadLetterAttempt,
20
- drainConversationMailbox,
21
- getConversation,
22
- getConversationWorkState,
23
- isFinalAttempt,
24
- isInvalidConversationRecordError,
25
- listActiveConversationIds,
26
- markConversationWorkEnqueued,
27
- recordAttemptFailure,
28
- releaseConversationWork,
29
- removeActiveConversation,
30
- requestConversationContinuation,
31
- requestConversationWork,
32
- startConversationWork
33
- } from "./chunk-YNP2ATQX.js";
9
+ } from "./chunk-MXW4GL53.js";
34
10
  import {
35
11
  GET,
36
12
  buildSentryConversationUrl,
@@ -38,13 +14,13 @@ import {
38
14
  resolveRootVisibility,
39
15
  resolveSlackChannelTypeFromMessage,
40
16
  resolveSlackConversationContext
41
- } from "./chunk-IGHMVDWI.js";
17
+ } from "./chunk-FWJRXN4F.js";
42
18
  import "./chunk-CEA3A3UA.js";
43
19
  import {
44
20
  getVercelConversationWorkQueue,
45
21
  resolveConversationWorkQueueTopic,
46
22
  verifyConversationQueueMessage
47
- } from "./chunk-2XXEKR44.js";
23
+ } from "./chunk-7Q6WCISF.js";
48
24
  import {
49
25
  PLUGIN_TASK_QUEUE_TOPIC,
50
26
  createVercelQueueClient,
@@ -53,7 +29,7 @@ import {
53
29
  import {
54
30
  validatePluginEgressCredentialHooks,
55
31
  validatePluginRegistrations
56
- } from "./chunk-DY7TRYB5.js";
32
+ } from "./chunk-MGUMR35T.js";
57
33
  import {
58
34
  defineJuniorPlugins,
59
35
  pluginCatalogConfigFromEnv,
@@ -69,6 +45,7 @@ import {
69
45
  appendSlackLegacyAttachmentText,
70
46
  buildSlackOutputMessage,
71
47
  buildSteeringPiMessage,
48
+ cancelSubscriptions,
72
49
  createAgentRunner,
73
50
  createContextCompactor,
74
51
  createUserTokenStore,
@@ -105,21 +82,28 @@ import {
105
82
  splitSlackReplyText,
106
83
  startOAuthFlow,
107
84
  truncateStatusText
108
- } from "./chunk-EDLNHZH3.js";
85
+ } from "./chunk-IXKSQGIO.js";
109
86
  import {
110
87
  ConversationTurnBoundaryError,
111
88
  CooperativeTurnYieldError,
89
+ JUNIOR_THREAD_STATE_TTL_MS,
112
90
  TurnInputCommitLostError,
113
91
  TurnInputDeferredError,
114
92
  TurnSliceLimitExceededError,
93
+ abandonAgentTurnSessionRecord,
115
94
  buildConversationContext,
116
95
  buildDeterministicAssistantMessageId,
117
96
  buildDeterministicTurnId,
118
97
  buildTurnLimitResponse,
119
98
  clearPendingAuth,
120
99
  coerceThreadArtifactsState,
100
+ coerceThreadConversationState,
101
+ commitMessages,
121
102
  completeDeliveredTurn,
122
103
  createConversationMemoryService,
104
+ failAgentTurnSessionRecord,
105
+ getAgentTurnSessionRecord,
106
+ getAgentTurnSessionRecordForResume,
123
107
  getChannelConfigurationService,
124
108
  getChannelConfigurationServiceById,
125
109
  getConversationMessageSlackTs,
@@ -134,6 +118,9 @@ import {
134
118
  isPendingAuthLatestRequest,
135
119
  isTurnInputCommitLostError,
136
120
  isTurnInputDeferredError,
121
+ listAgentTurnSessionSummariesForConversation,
122
+ loadConversationProjection,
123
+ loadProjection,
137
124
  markConversationMessage,
138
125
  markTurnClosed,
139
126
  markTurnFailed,
@@ -147,33 +134,23 @@ import {
147
134
  persistThreadStateById,
148
135
  persistWithRetry,
149
136
  persistYieldSessionRecord,
137
+ recordAgentTurnSessionSummary,
138
+ recordAuthorizationCompleted,
150
139
  recordDeliveredAssistantMessage,
151
140
  startActiveTurn,
152
141
  turnHasReply,
153
142
  updateConversationStats,
154
143
  upsertConversationMessage
155
- } from "./chunk-YFQ7CQDE.js";
156
- import {
157
- JUNIOR_THREAD_STATE_TTL_MS,
158
- abandonAgentTurnSessionRecord,
159
- coerceThreadConversationState,
160
- commitMessages,
161
- failAgentTurnSessionRecord,
162
- getAgentTurnSessionRecord,
163
- getAgentTurnSessionRecordForResume,
164
- listAgentTurnSessionSummariesForConversation,
165
- loadConversationProjection,
166
- loadProjection,
167
- recordAgentTurnSessionSummary,
168
- recordAuthorizationCompleted
169
- } from "./chunk-4YF7Z6IA.js";
170
- import "./chunk-SPUAJVVH.js";
144
+ } from "./chunk-72OMGPDS.js";
145
+ import "./chunk-HO5NT6OE.js";
171
146
  import "./chunk-MU6HHZEN.js";
172
147
  import "./chunk-PDO5BLNM.js";
173
148
  import "./chunk-UIE3R5XU.js";
174
149
  import {
175
150
  bindScheduledTaskCredentialSubject,
176
151
  bindSlackDirectCredentialSubject,
152
+ createPluginLogger,
153
+ createPluginState,
177
154
  getDashboardConversationLink,
178
155
  getPluginApiRoutes,
179
156
  getPluginRoutes,
@@ -184,16 +161,12 @@ import {
184
161
  validatePlugins,
185
162
  verifyScheduledTaskCredentialSubject,
186
163
  verifySlackDirectCredentialSubject
187
- } from "./chunk-RMVOAJRL.js";
188
- import {
189
- createPluginLogger,
190
- createPluginState
191
- } from "./chunk-AUUOHQAT.js";
192
- import "./chunk-G3E7SCME.js";
164
+ } from "./chunk-QY636HZU.js";
193
165
  import {
194
166
  acquireActiveLock,
167
+ getDefaultRedisStateAdapterFor,
195
168
  getStateAdapter
196
- } from "./chunk-B2Z2H66D.js";
169
+ } from "./chunk-OUHNWH46.js";
197
170
  import {
198
171
  SlackActionError,
199
172
  downloadPrivateSlackFile,
@@ -204,7 +177,7 @@ import {
204
177
  normalizeSlackConversationId,
205
178
  runWithSlackInstallationToken,
206
179
  runWithWorkspaceTeamId
207
- } from "./chunk-VH6KWKG2.js";
180
+ } from "./chunk-RGX4V75M.js";
208
181
  import {
209
182
  contextProvenance,
210
183
  getConversationEventStore,
@@ -212,10 +185,12 @@ import {
212
185
  getDb,
213
186
  getSqlExecutor,
214
187
  instructionProvenanceFor,
215
- juniorConversationEvents,
216
- juniorConversations,
217
188
  withConversationEventLock
218
- } from "./chunk-NVOTGWYX.js";
189
+ } from "./chunk-FENLIFJD.js";
190
+ import {
191
+ juniorConversationEvents,
192
+ juniorConversations
193
+ } from "./chunk-TMBRFULF.js";
219
194
  import {
220
195
  sleep
221
196
  } from "./chunk-4ZNGQH7C.js";
@@ -239,7 +214,7 @@ import {
239
214
  standardModelId,
240
215
  stripRuntimeTurnContext,
241
216
  trimTrailingAssistantMessages
242
- } from "./chunk-SS67LUOK.js";
217
+ } from "./chunk-JNAVF45E.js";
243
218
  import {
244
219
  juniorDestinations
245
220
  } from "./chunk-VFUK3X5B.js";
@@ -248,32 +223,35 @@ import {
248
223
  readSlackRawMessageContext,
249
224
  resolveSlackChannelIdFromMessage,
250
225
  resolveSlackChannelIdFromThreadId
251
- } from "./chunk-RT5BUHR5.js";
226
+ } from "./chunk-O3JMSZLI.js";
252
227
  import {
253
228
  createSlackDestination,
254
229
  destinationKey,
255
230
  parseDestination,
256
231
  requireSlackDestination,
257
232
  sameDestination
258
- } from "./chunk-B7PUBQQM.js";
233
+ } from "./chunk-SYAQIR5Z.js";
259
234
  import {
260
235
  discoverSkills
261
- } from "./chunk-YT72TOEK.js";
236
+ } from "./chunk-WHMOBKT7.js";
237
+ import {
238
+ pluginCatalogRuntime
239
+ } from "./chunk-3SXUV423.js";
262
240
  import {
263
241
  buildOAuthTokenRequest,
264
242
  credentialSubjectSchema,
265
243
  hasRequiredOAuthScope,
266
- parseOAuthTokenResponse,
267
- pluginCatalogRuntime
268
- } from "./chunk-YIPD3L6L.js";
244
+ parseOAuthTokenResponse
245
+ } from "./chunk-TT6GMXNA.js";
269
246
  import {
270
247
  createActor,
271
248
  createSlackActor,
272
249
  createSlackResumeActor,
273
250
  isActorUserId,
274
251
  isUserActor,
275
- parseActorUserId
276
- } from "./chunk-VIXFG2VC.js";
252
+ parseActorUserId,
253
+ parseStoredSlackActor
254
+ } from "./chunk-PLB7MRCQ.js";
277
255
  import {
278
256
  buildTurnFailureResponse,
279
257
  createChatSdkLogger,
@@ -289,22 +267,27 @@ import {
289
267
  setTags,
290
268
  withContext,
291
269
  withSpan
292
- } from "./chunk-O24R4DCK.js";
270
+ } from "./chunk-ISMWX44P.js";
293
271
  import {
272
+ isRecord,
273
+ toOptionalNumber,
294
274
  toOptionalString
295
275
  } from "./chunk-OB42YVAE.js";
296
276
  import {
297
277
  continueTrace
298
278
  } from "./chunk-6GWA276C.js";
299
279
  import "./chunk-B3IT5END.js";
280
+ import {
281
+ homeDir
282
+ } from "./chunk-3V7OFJZC.js";
300
283
  import {
301
284
  JUNIOR_PLUGIN_TASK_CALLBACK_ROUTE,
302
285
  parseSlackChannelId,
303
286
  parseSlackMessageTs
304
- } from "./chunk-7FBGKXPE.js";
287
+ } from "./chunk-IVVGFRQQ.js";
305
288
  import {
306
- homeDir
307
- } from "./chunk-3V7OFJZC.js";
289
+ JUNIOR_VERSION
290
+ } from "./chunk-OCD3OZRK.js";
308
291
  import "./chunk-MLKGABMK.js";
309
292
 
310
293
  // src/app.ts
@@ -352,53 +335,26 @@ function buildSlackReplyBlocks(text, footer) {
352
335
  }
353
336
 
354
337
  // src/chat/slack/reply.ts
355
- function planSlackAssistantMessagePosts(text) {
356
- return splitSlackReplyText(text).map((chunk, index) => ({
357
- text: chunk,
358
- stage: index === 0 ? "thread_reply" : "thread_reply_continuation"
359
- }));
360
- }
361
- function findLastTextPostIndex(posts) {
362
- for (let index = posts.length - 1; index >= 0; index -= 1) {
363
- if (posts[index]?.text.trim().length) {
364
- return index;
365
- }
366
- }
367
- return -1;
368
- }
369
- async function postSlackApiReplyPosts(args) {
370
- const lastTextPostIndex = findLastTextPostIndex(args.posts);
371
- let lastPostedMessageTs;
372
- for (const [index, post] of args.posts.entries()) {
373
- const hasVisibleDelivery = post.text.trim().length > 0;
374
- if (hasVisibleDelivery) {
375
- await args.beforePost?.();
376
- }
377
- let messageTs;
378
- try {
379
- if (post.text.trim().length > 0) {
380
- const footer = index === lastTextPostIndex ? args.footer : void 0;
381
- const blocks = buildSlackReplyBlocks(post.text, footer);
382
- const response = await postSlackMessage({
383
- channelId: args.channelId,
384
- threadTs: args.threadTs,
385
- text: post.text,
386
- ...blocks ? { blocks } : {}
387
- });
388
- messageTs = response.ts;
389
- lastPostedMessageTs = response.ts;
390
- }
391
- continue;
392
- } catch (error) {
393
- await args.onPostError?.({
394
- error,
395
- messageTs,
396
- stage: post.stage
397
- });
398
- throw error;
399
- }
338
+ async function sendSlackReply(args) {
339
+ const chunks = splitSlackReplyText(args.text);
340
+ const footer = buildSlackReplyFooter({
341
+ conversationId: args.conversationId
342
+ });
343
+ let lastMessageTs;
344
+ for (const [index, text] of chunks.entries()) {
345
+ const blocks = buildSlackReplyBlocks(
346
+ text,
347
+ index === chunks.length - 1 ? footer : void 0
348
+ );
349
+ const response = await postSlackMessage({
350
+ channelId: args.channelId,
351
+ threadTs: args.threadTs,
352
+ text,
353
+ ...blocks ? { blocks } : {}
354
+ });
355
+ lastMessageTs = response.ts;
400
356
  }
401
- return lastPostedMessageTs;
357
+ return lastMessageTs;
402
358
  }
403
359
 
404
360
  // src/chat/slack/errors.ts
@@ -583,6 +539,7 @@ var DEFAULT_MAX_ATTEMPTS = 5;
583
539
  var nonEmptyExactStringSchema = z.string().min(1).refine(
584
540
  (value) => value === value.trim() && value.toLowerCase() !== "unknown"
585
541
  );
542
+ var incompleteDispatchIndexSchema = z.array(nonEmptyExactStringSchema);
586
543
  var dispatchStatusSchema = z.enum([
587
544
  "pending",
588
545
  "running",
@@ -664,6 +621,9 @@ function incompleteDispatchIndexKey() {
664
621
  function incompleteDispatchIndexLockKey() {
665
622
  return `${DISPATCH_PREFIX}:incomplete:lock`;
666
623
  }
624
+ function parseIncompleteDispatchIndex(value) {
625
+ return value === void 0 || value === null ? [] : incompleteDispatchIndexSchema.parse(value);
626
+ }
667
627
  function dispatchLockKey(id) {
668
628
  return `${DISPATCH_PREFIX}:lock:${id}`;
669
629
  }
@@ -736,9 +696,12 @@ async function withIncompleteDispatchIndexLock(state, callback) {
736
696
  }
737
697
  async function syncIncompleteDispatchIndex(state, record) {
738
698
  await withIncompleteDispatchIndexLock(state, async () => {
739
- const existing = await state.get(incompleteDispatchIndexKey()) ?? [];
740
699
  const ids = [
741
- ...new Set(existing.filter((id) => typeof id === "string"))
700
+ ...new Set(
701
+ parseIncompleteDispatchIndex(
702
+ await state.get(incompleteDispatchIndexKey())
703
+ )
704
+ )
742
705
  ];
743
706
  const next = isTerminalDispatchStatus(record.status) ? ids.filter((id) => id !== record.id) : ids.includes(record.id) ? ids : [...ids, record.id];
744
707
  if (next.length === ids.length && next.every((id, index) => id === ids[index])) {
@@ -815,8 +778,13 @@ async function updateDispatchRecord(state, record) {
815
778
  async function listIncompleteDispatchIds() {
816
779
  const state = getStateAdapter();
817
780
  await state.connect();
818
- const ids = await state.get(incompleteDispatchIndexKey()) ?? [];
819
- return [...new Set(ids.filter((id) => typeof id === "string"))];
781
+ return [
782
+ ...new Set(
783
+ parseIncompleteDispatchIndex(
784
+ await state.get(incompleteDispatchIndexKey())
785
+ )
786
+ )
787
+ ];
820
788
  }
821
789
  async function getPluginDispatchProjection(args) {
822
790
  const record = await getDispatchRecord(args.id);
@@ -914,8 +882,8 @@ async function runAgentDispatchSlice(callback, deps) {
914
882
  const turnId = getDispatchTurnId(dispatch.id);
915
883
  const logContext = {
916
884
  conversationId,
917
- slackThreadId: conversationId,
918
- slackChannelId: dispatch.destination.channelId,
885
+ messageConversationId: conversationId,
886
+ destinationName: dispatch.destination.channelId,
919
887
  runId: dispatch.id,
920
888
  actorType: dispatch.actor.platform,
921
889
  actorId: dispatch.actor.name,
@@ -1005,15 +973,15 @@ async function runAgentDispatchSlice(callback, deps) {
1005
973
  let resultMessageTs;
1006
974
  let assistantMessageDelivered = turnHasReply(conversation, turnId);
1007
975
  const deliverAssistantMessage = async (assistantMessage) => {
1008
- const posts = planSlackAssistantMessagePosts(assistantMessage.text);
1009
- if (posts.length === 0) {
976
+ if (!assistantMessage.text.trim()) {
1010
977
  return;
1011
978
  }
1012
979
  failureCode = "delivery_failed";
1013
980
  try {
1014
- resultMessageTs = await postSlackApiReplyPosts({
981
+ resultMessageTs = await sendSlackReply({
1015
982
  channelId: dispatch.destination.channelId,
1016
- posts
983
+ conversationId,
984
+ text: assistantMessage.text
1017
985
  });
1018
986
  } catch (error) {
1019
987
  if (isRetryableSlackPostError(error)) {
@@ -1209,13 +1177,7 @@ async function runAgentDispatchSlice(callback, deps) {
1209
1177
  destinationVisibility: dispatch.destinationVisibility,
1210
1178
  source: dispatch.source,
1211
1179
  actor: dispatch.actor,
1212
- surface: "api",
1213
- logContext: {
1214
- threadId: conversationId,
1215
- channelId: dispatch.destination.channelId,
1216
- runId: dispatch.id,
1217
- assistantUserName: botConfig.userName
1218
- }
1180
+ surface: "api"
1219
1181
  });
1220
1182
  }
1221
1183
  if (statePersisted) {
@@ -1237,117 +1199,1244 @@ async function runAgentDispatchSlice(callback, deps) {
1237
1199
  }
1238
1200
  lifecycleTerminalized = true;
1239
1201
  }
1240
- dispatch = await markDispatch({
1241
- dispatch,
1242
- status: failure ? "failed" : "completed",
1243
- ...failure ? { errorMessage: failure } : {},
1244
- resultMessageTs
1245
- });
1246
- if (!failure) {
1247
- try {
1248
- await scheduleCompletedTasks({
1249
- conversationId,
1250
- sessionId: turnId
1251
- });
1252
- } catch (error) {
1253
- logException(
1254
- error,
1255
- "plugin_session_completed_task_schedule_failed",
1256
- logContext,
1257
- {},
1258
- "Plugin session.completed task scheduling failed"
1259
- );
1260
- }
1202
+ dispatch = await markDispatch({
1203
+ dispatch,
1204
+ status: failure ? "failed" : "completed",
1205
+ ...failure ? { errorMessage: failure } : {},
1206
+ resultMessageTs
1207
+ });
1208
+ if (!failure) {
1209
+ try {
1210
+ await scheduleCompletedTasks({
1211
+ conversationId,
1212
+ sessionId: turnId
1213
+ });
1214
+ } catch (error) {
1215
+ logException(
1216
+ error,
1217
+ "plugin_session_completed_task_schedule_failed",
1218
+ logContext,
1219
+ {},
1220
+ "Plugin session.completed task scheduling failed"
1221
+ );
1222
+ }
1223
+ }
1224
+ } catch (error) {
1225
+ if (error instanceof AuthorizationFlowDisabledError) {
1226
+ if (lifecycleStarted && !lifecycleTerminalized) {
1227
+ await turnLifecycle.fail({
1228
+ conversationId,
1229
+ turnId,
1230
+ createdAtMs: Date.now(),
1231
+ failureCode: "agent_run_failed"
1232
+ });
1233
+ lifecycleTerminalized = true;
1234
+ }
1235
+ await markDispatch({
1236
+ dispatch,
1237
+ status: "blocked",
1238
+ errorMessage: `Dispatch requires ${error.provider} authorization.`
1239
+ });
1240
+ return;
1241
+ }
1242
+ if (error instanceof PluginCredentialFailureError) {
1243
+ if (lifecycleStarted && !lifecycleTerminalized) {
1244
+ await turnLifecycle.fail({
1245
+ conversationId,
1246
+ turnId,
1247
+ createdAtMs: Date.now(),
1248
+ failureCode: "agent_run_failed"
1249
+ });
1250
+ lifecycleTerminalized = true;
1251
+ }
1252
+ await markDispatch({
1253
+ dispatch,
1254
+ status: "blocked",
1255
+ errorMessage: error.message
1256
+ });
1257
+ return;
1258
+ }
1259
+ const eventId = logException(
1260
+ error,
1261
+ "agent_dispatch_run_failed",
1262
+ {
1263
+ ...logContext,
1264
+ modelId: standardModelId(botConfig)
1265
+ },
1266
+ {},
1267
+ "Agent dispatch failed"
1268
+ );
1269
+ if (lifecycleStarted && !lifecycleTerminalized) {
1270
+ await turnLifecycle.fail({
1271
+ conversationId,
1272
+ turnId,
1273
+ createdAtMs: Date.now(),
1274
+ failureCode,
1275
+ ...eventId ? { eventId } : {}
1276
+ });
1277
+ lifecycleTerminalized = true;
1278
+ }
1279
+ await markDispatch({
1280
+ dispatch,
1281
+ status: "failed",
1282
+ errorMessage: error instanceof Error ? error.message : String(error)
1283
+ });
1284
+ } finally {
1285
+ await stateAdapter.releaseLock(destinationLock);
1286
+ }
1287
+ }
1288
+
1289
+ // src/handlers/agent-dispatch.ts
1290
+ async function POST(request, waitUntil, options) {
1291
+ const payload = await verifyDispatchCallbackRequest(request);
1292
+ if (!payload) {
1293
+ return new Response("Unauthorized", { status: 401 });
1294
+ }
1295
+ waitUntil(
1296
+ () => runAgentDispatchSlice(payload, {
1297
+ agentRunner: options.agentRunner
1298
+ }).catch((error) => {
1299
+ logException(
1300
+ error,
1301
+ "agent_dispatch_handler_failed",
1302
+ {},
1303
+ { "app.dispatch.id": payload.id },
1304
+ "Agent dispatch handler failed"
1305
+ );
1306
+ })
1307
+ );
1308
+ return new Response("Accepted", { status: 202 });
1309
+ }
1310
+
1311
+ // src/handlers/heartbeat.ts
1312
+ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
1313
+
1314
+ // src/chat/task-execution/state.ts
1315
+ import { randomUUID } from "crypto";
1316
+ var CONVERSATION_PREFIX = "junior:conversation";
1317
+ var CONVERSATION_SCHEMA_VERSION = 1;
1318
+ var CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH = 1e4;
1319
+ var CONVERSATION_INDEX_LOCK_TTL_MS = 1e4;
1320
+ var CONVERSATION_INDEX_LOCK_WAIT_MS = 2e3;
1321
+ var CONVERSATION_INDEX_LOCK_RETRY_MS = 25;
1322
+ var CONVERSATION_MUTATION_LOCK_TTL_MS = 1e4;
1323
+ var CONVERSATION_MUTATION_WAIT_MS = 1e4;
1324
+ var CONVERSATION_MUTATION_RETRY_MS = 25;
1325
+ var InvalidConversationRecordError = class extends Error {
1326
+ constructor(conversationId) {
1327
+ super(`Conversation record is invalid for ${conversationId}`);
1328
+ this.name = "InvalidConversationRecordError";
1329
+ }
1330
+ };
1331
+ function isInvalidConversationRecordError(error) {
1332
+ return error instanceof InvalidConversationRecordError;
1333
+ }
1334
+ var ConversationMutationFencedError = class extends Error {
1335
+ constructor(conversationId) {
1336
+ super(
1337
+ `Conversation mutation lock was lost before write for ${conversationId}`
1338
+ );
1339
+ this.name = "ConversationMutationFencedError";
1340
+ }
1341
+ };
1342
+ var CONVERSATION_BY_ACTIVITY_INDEX_KEY = `${CONVERSATION_PREFIX}:by-activity`;
1343
+ var CONVERSATION_ACTIVE_INDEX_KEY = `${CONVERSATION_PREFIX}:active`;
1344
+ var CONVERSATION_WORK_LEASE_TTL_MS = 9e4;
1345
+ var CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15e3;
1346
+ var CONVERSATION_WORK_STALE_ENQUEUE_MS = 6e4;
1347
+ var CONVERSATION_WORK_MAX_DELIVERY_ATTEMPTS = 5;
1348
+ function conversationKey(conversationId) {
1349
+ return `${CONVERSATION_PREFIX}:${conversationId}`;
1350
+ }
1351
+ function indexLockKey(indexKey) {
1352
+ return `${indexKey}:lock`;
1353
+ }
1354
+ function mutationLockKey(conversationId) {
1355
+ return `${CONVERSATION_PREFIX}:mutation:${conversationId}`;
1356
+ }
1357
+ function now() {
1358
+ return Date.now();
1359
+ }
1360
+ function compareMessages(left, right) {
1361
+ return left.createdAtMs - right.createdAtMs || left.receivedAtMs - right.receivedAtMs || left.inboundMessageId.localeCompare(right.inboundMessageId);
1362
+ }
1363
+ function inputHasAttachments(input) {
1364
+ return Array.isArray(input.attachments) && input.attachments.length > 0;
1365
+ }
1366
+ function upgradedPendingMessage(stored, duplicate) {
1367
+ if (!inputHasAttachments(duplicate.input) || inputHasAttachments(stored.input)) {
1368
+ return stored;
1369
+ }
1370
+ return { ...stored, input: duplicate.input };
1371
+ }
1372
+ function compareIndexDescending(left, right) {
1373
+ return right.score - left.score || right.conversationId.localeCompare(left.conversationId);
1374
+ }
1375
+ function compareIndexAscending(left, right) {
1376
+ return left.score - right.score || left.conversationId.localeCompare(right.conversationId);
1377
+ }
1378
+ function uniqueStrings(values) {
1379
+ return [...new Set(values)];
1380
+ }
1381
+ function normalizeSource(value) {
1382
+ if (value === "api" || value === "internal" || value === "local" || value === "plugin" || value === "resource_event" || value === "scheduler" || value === "slack") {
1383
+ return value;
1384
+ }
1385
+ return void 0;
1386
+ }
1387
+ function normalizeExecutionStatus(value) {
1388
+ if (value === "awaiting_resume" || value === "failed" || value === "idle" || value === "pending" || value === "running") {
1389
+ return value;
1390
+ }
1391
+ return void 0;
1392
+ }
1393
+ function normalizeMetadata2(value) {
1394
+ if (!isRecord(value)) {
1395
+ return void 0;
1396
+ }
1397
+ return value;
1398
+ }
1399
+ function normalizeInput(value) {
1400
+ if (!isRecord(value)) {
1401
+ return void 0;
1402
+ }
1403
+ const text = typeof value.text === "string" ? value.text : void 0;
1404
+ const attachments = Array.isArray(value.attachments) ? [...value.attachments] : void 0;
1405
+ if (text === void 0 || !text.trim() && !attachments?.length) {
1406
+ return void 0;
1407
+ }
1408
+ return {
1409
+ text,
1410
+ authorId: toOptionalString(value.authorId),
1411
+ attachments,
1412
+ metadata: normalizeMetadata2(value.metadata)
1413
+ };
1414
+ }
1415
+ function normalizeMessage(value) {
1416
+ if (!isRecord(value)) {
1417
+ return void 0;
1418
+ }
1419
+ const conversationId = toOptionalString(value.conversationId);
1420
+ const inboundMessageId = toOptionalString(value.inboundMessageId);
1421
+ const source = normalizeSource(value.source);
1422
+ const destination = parseDestination(value.destination);
1423
+ const createdAtMs = toOptionalNumber(value.createdAtMs);
1424
+ const receivedAtMs = toOptionalNumber(value.receivedAtMs);
1425
+ const input = normalizeInput(value.input);
1426
+ if (!conversationId || !destination || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
1427
+ return void 0;
1428
+ }
1429
+ return {
1430
+ conversationId,
1431
+ destination,
1432
+ inboundMessageId,
1433
+ source,
1434
+ createdAtMs,
1435
+ receivedAtMs,
1436
+ input,
1437
+ injectedAtMs: toOptionalNumber(value.injectedAtMs),
1438
+ attemptCount: toOptionalNumber(value.attemptCount)
1439
+ };
1440
+ }
1441
+ function isFinalAttempt(message) {
1442
+ return (message.attemptCount ?? 0) >= CONVERSATION_WORK_MAX_DELIVERY_ATTEMPTS - 1;
1443
+ }
1444
+ function normalizeActor(value) {
1445
+ return parseStoredSlackActor(value);
1446
+ }
1447
+ function normalizeLease(value) {
1448
+ if (!isRecord(value)) {
1449
+ return void 0;
1450
+ }
1451
+ const token = toOptionalString(value.token);
1452
+ const acquiredAtMs = toOptionalNumber(value.acquiredAtMs);
1453
+ const lastCheckInAtMs = toOptionalNumber(value.lastCheckInAtMs);
1454
+ const expiresAtMs = toOptionalNumber(value.expiresAtMs);
1455
+ if (!token || typeof acquiredAtMs !== "number" || typeof lastCheckInAtMs !== "number" || typeof expiresAtMs !== "number") {
1456
+ return void 0;
1457
+ }
1458
+ return {
1459
+ token,
1460
+ acquiredAtMs,
1461
+ lastCheckInAtMs,
1462
+ expiresAtMs
1463
+ };
1464
+ }
1465
+ function normalizeExecution(conversationId, value) {
1466
+ if (!isRecord(value)) {
1467
+ return void 0;
1468
+ }
1469
+ const status = normalizeExecutionStatus(value.status);
1470
+ if (!status) {
1471
+ return void 0;
1472
+ }
1473
+ const pendingMessages2 = Array.isArray(value.pendingMessages) ? value.pendingMessages.map(normalizeMessage).filter((message) => Boolean(message)).filter((message) => message.conversationId === conversationId).filter((message) => message.injectedAtMs === void 0).sort(compareMessages) : [];
1474
+ const inboundMessageIds = Array.isArray(value.inboundMessageIds) ? uniqueStrings(
1475
+ value.inboundMessageIds.map((id) => typeof id === "string" ? id : void 0).filter((id) => Boolean(id))
1476
+ ) : [];
1477
+ const lease = normalizeLease(value.lease);
1478
+ const normalizedStatus = status === "idle" && lease ? "running" : status === "idle" && pendingMessages2.length > 0 ? "pending" : status;
1479
+ return {
1480
+ status: normalizedStatus,
1481
+ inboundMessageIds: uniqueStrings([
1482
+ ...inboundMessageIds,
1483
+ ...pendingMessages2.map((message) => message.inboundMessageId)
1484
+ ]),
1485
+ pendingCount: pendingMessages2.length,
1486
+ pendingMessages: pendingMessages2,
1487
+ lease,
1488
+ lastCheckpointAtMs: toOptionalNumber(value.lastCheckpointAtMs),
1489
+ lastEnqueuedAtMs: toOptionalNumber(value.lastEnqueuedAtMs),
1490
+ runId: toOptionalString(value.runId),
1491
+ updatedAtMs: toOptionalNumber(value.updatedAtMs)
1492
+ };
1493
+ }
1494
+ function normalizeConversation(conversationId, value) {
1495
+ if (!isRecord(value) || value.schemaVersion !== CONVERSATION_SCHEMA_VERSION) {
1496
+ return void 0;
1497
+ }
1498
+ const storedConversationId = toOptionalString(value.conversationId);
1499
+ const createdAtMs = toOptionalNumber(value.createdAtMs);
1500
+ const lastActivityAtMs = toOptionalNumber(value.lastActivityAtMs);
1501
+ const updatedAtMs = toOptionalNumber(value.updatedAtMs);
1502
+ const execution = normalizeExecution(conversationId, value.execution);
1503
+ const destination = value.destination === void 0 ? void 0 : parseDestination(value.destination);
1504
+ if (storedConversationId !== conversationId || typeof createdAtMs !== "number" || typeof lastActivityAtMs !== "number" || typeof updatedAtMs !== "number" || !execution || value.destination !== void 0 && !destination) {
1505
+ return void 0;
1506
+ }
1507
+ if (execution.pendingMessages.length > 0 && (!destination || execution.pendingMessages.some(
1508
+ (message) => !sameDestination(message.destination, destination)
1509
+ ))) {
1510
+ return void 0;
1511
+ }
1512
+ return {
1513
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1514
+ conversationId,
1515
+ createdAtMs,
1516
+ lastActivityAtMs,
1517
+ updatedAtMs,
1518
+ execution,
1519
+ ...destination ? { destination } : {},
1520
+ ...toOptionalString(value.title) ? { title: toOptionalString(value.title) } : {},
1521
+ ...toOptionalString(value.channelName) ? { channelName: toOptionalString(value.channelName) } : {},
1522
+ ...normalizeActor(value.actor) ? { actor: normalizeActor(value.actor) } : {},
1523
+ ...normalizeSource(value.source) ? { source: normalizeSource(value.source) } : {}
1524
+ };
1525
+ }
1526
+ function emptyConversation(args) {
1527
+ return {
1528
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1529
+ conversationId: args.conversationId,
1530
+ createdAtMs: args.nowMs,
1531
+ lastActivityAtMs: args.nowMs,
1532
+ updatedAtMs: args.nowMs,
1533
+ ...args.destination ? { destination: args.destination } : {},
1534
+ ...args.source ? { source: args.source } : {},
1535
+ execution: {
1536
+ status: "idle",
1537
+ inboundMessageIds: [],
1538
+ pendingCount: 0,
1539
+ pendingMessages: [],
1540
+ updatedAtMs: args.nowMs
1541
+ }
1542
+ };
1543
+ }
1544
+ function isLeaseActive(lease, nowMs) {
1545
+ return Boolean(lease && lease.expiresAtMs > nowMs);
1546
+ }
1547
+ function pendingMessages(conversation) {
1548
+ return [...conversation.execution.pendingMessages].sort(compareMessages);
1549
+ }
1550
+ function isRunnableStatus(status) {
1551
+ return status !== "failed" && status !== "idle";
1552
+ }
1553
+ function hasRunnableWork(conversation) {
1554
+ return isRunnableStatus(conversation.execution.status) || pendingMessages(conversation).length > 0;
1555
+ }
1556
+ function executionWithPendingMessages(execution, pending) {
1557
+ const pendingMessages2 = [...pending].sort(compareMessages);
1558
+ const status = execution.status === "idle" && execution.lease ? "running" : execution.status === "idle" && pendingMessages2.length > 0 ? "pending" : execution.status;
1559
+ return {
1560
+ ...execution,
1561
+ status,
1562
+ inboundMessageIds: uniqueStrings([
1563
+ ...execution.inboundMessageIds,
1564
+ ...pendingMessages2.map((message) => message.inboundMessageId)
1565
+ ]),
1566
+ pendingMessages: pendingMessages2,
1567
+ pendingCount: pendingMessages2.length
1568
+ };
1569
+ }
1570
+ function withExecutionUpdate(conversation, execution, nowMs) {
1571
+ return {
1572
+ ...conversation,
1573
+ updatedAtMs: nowMs,
1574
+ execution: {
1575
+ ...executionWithPendingMessages(execution, execution.pendingMessages),
1576
+ updatedAtMs: nowMs
1577
+ }
1578
+ };
1579
+ }
1580
+ async function getConnectedState(stateAdapter) {
1581
+ const state = stateAdapter ?? getStateAdapter();
1582
+ await state.connect();
1583
+ return state;
1584
+ }
1585
+ async function sleep2(ms) {
1586
+ await new Promise((resolve) => {
1587
+ const timer = setTimeout(resolve, ms);
1588
+ timer.unref?.();
1589
+ });
1590
+ }
1591
+ async function withIndexLock(state, indexKey, callback) {
1592
+ const startedAtMs = now();
1593
+ let lock;
1594
+ while (true) {
1595
+ lock = await state.acquireLock(
1596
+ indexLockKey(indexKey),
1597
+ CONVERSATION_INDEX_LOCK_TTL_MS
1598
+ );
1599
+ if (lock) {
1600
+ break;
1601
+ }
1602
+ if (now() - startedAtMs >= CONVERSATION_INDEX_LOCK_WAIT_MS) {
1603
+ throw new Error(
1604
+ `Could not acquire conversation index lock for ${indexKey}`
1605
+ );
1606
+ }
1607
+ await sleep2(CONVERSATION_INDEX_LOCK_RETRY_MS);
1608
+ }
1609
+ try {
1610
+ return await callback();
1611
+ } finally {
1612
+ await state.releaseLock(lock);
1613
+ }
1614
+ }
1615
+ function normalizeIndexEntry(value) {
1616
+ if (!isRecord(value)) {
1617
+ return void 0;
1618
+ }
1619
+ const conversationId = toOptionalString(value.conversationId);
1620
+ const score = toOptionalNumber(value.score);
1621
+ if (!conversationId || typeof score !== "number") {
1622
+ return void 0;
1623
+ }
1624
+ return { conversationId, score };
1625
+ }
1626
+ function uniqueIndexEntries(value) {
1627
+ if (!Array.isArray(value)) {
1628
+ return [];
1629
+ }
1630
+ const entries = /* @__PURE__ */ new Map();
1631
+ for (const item of value) {
1632
+ const entry = normalizeIndexEntry(item);
1633
+ if (!entry) {
1634
+ continue;
1635
+ }
1636
+ const existing = entries.get(entry.conversationId);
1637
+ if (!existing || entry.score > existing.score) {
1638
+ entries.set(entry.conversationId, entry);
1639
+ }
1640
+ }
1641
+ return [...entries.values()];
1642
+ }
1643
+ function retainedIndexEntries(indexKey, entries) {
1644
+ if (indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
1645
+ return entries.sort(compareIndexDescending).slice(0, CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH);
1646
+ }
1647
+ if (indexKey === CONVERSATION_ACTIVE_INDEX_KEY) {
1648
+ return entries.sort(compareIndexAscending);
1649
+ }
1650
+ throw new Error(`Unknown conversation index ${indexKey}`);
1651
+ }
1652
+ function redisIndexKey(indexKey) {
1653
+ const prefix = getChatConfig().state.keyPrefix;
1654
+ return [...prefix ? [prefix] : [], indexKey].join(":");
1655
+ }
1656
+ function parseRedisIndexEntries(values) {
1657
+ if (!Array.isArray(values)) {
1658
+ return [];
1659
+ }
1660
+ const entries = [];
1661
+ for (let index = 0; index < values.length; index += 2) {
1662
+ const conversationId = toOptionalString(values[index]);
1663
+ const score = typeof values[index + 1] === "number" ? values[index + 1] : Number(values[index + 1]);
1664
+ if (!conversationId || !Number.isFinite(score)) {
1665
+ continue;
1666
+ }
1667
+ entries.push({ conversationId, score });
1668
+ }
1669
+ return entries;
1670
+ }
1671
+ function redisConversationIndexStore(client) {
1672
+ const upsertBoundedActivityScript = `
1673
+ redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
1674
+ redis.call("PEXPIRE", KEYS[1], ARGV[3])
1675
+ local extra = redis.call("ZCARD", KEYS[1]) - tonumber(ARGV[4])
1676
+ if extra > 0 then
1677
+ redis.call("ZREMRANGEBYRANK", KEYS[1], 0, extra - 1)
1678
+ end
1679
+ return 1
1680
+ `;
1681
+ return {
1682
+ async list(args) {
1683
+ const key = redisIndexKey(args.indexKey);
1684
+ const limit = args.limit;
1685
+ const offset = Math.max(0, args.offset ?? 0);
1686
+ if (limit === 0) {
1687
+ return [];
1688
+ }
1689
+ const values = args.scoreMax !== void 0 ? await client.sendCommand([
1690
+ "ZRANGEBYSCORE",
1691
+ key,
1692
+ "-inf",
1693
+ String(args.scoreMax),
1694
+ "WITHSCORES",
1695
+ ...limit !== void 0 || offset > 0 ? ["LIMIT", String(offset), String(limit ?? 1e9)] : []
1696
+ ]) : await client.sendCommand([
1697
+ args.order === "asc" ? "ZRANGE" : "ZREVRANGE",
1698
+ key,
1699
+ String(offset),
1700
+ String(
1701
+ limit === void 0 ? -1 : offset + Math.max(0, limit - 1)
1702
+ ),
1703
+ "WITHSCORES"
1704
+ ]);
1705
+ return parseRedisIndexEntries(values);
1706
+ },
1707
+ async remove(args) {
1708
+ await client.sendCommand([
1709
+ "ZREM",
1710
+ redisIndexKey(args.indexKey),
1711
+ args.conversationId
1712
+ ]);
1713
+ },
1714
+ async upsert(args) {
1715
+ const key = redisIndexKey(args.indexKey);
1716
+ if (args.indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
1717
+ await client.sendCommand([
1718
+ "EVAL",
1719
+ upsertBoundedActivityScript,
1720
+ "1",
1721
+ key,
1722
+ String(args.score),
1723
+ args.conversationId,
1724
+ String(JUNIOR_THREAD_STATE_TTL_MS),
1725
+ String(CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH)
1726
+ ]);
1727
+ return;
1728
+ }
1729
+ if (args.indexKey === CONVERSATION_ACTIVE_INDEX_KEY) {
1730
+ await client.sendCommand([
1731
+ "ZADD",
1732
+ key,
1733
+ String(args.score),
1734
+ args.conversationId
1735
+ ]);
1736
+ await client.sendCommand([
1737
+ "PEXPIRE",
1738
+ key,
1739
+ String(JUNIOR_THREAD_STATE_TTL_MS)
1740
+ ]);
1741
+ return;
1742
+ }
1743
+ throw new Error(`Unknown conversation index ${args.indexKey}`);
1744
+ }
1745
+ };
1746
+ }
1747
+ function emulatedConversationIndexStore(state) {
1748
+ const readIndex = async (indexKey) => uniqueIndexEntries(await state.get(indexKey));
1749
+ const writeIndex = async (indexKey, entries) => {
1750
+ await state.set(indexKey, entries, JUNIOR_THREAD_STATE_TTL_MS);
1751
+ };
1752
+ return {
1753
+ async list(args) {
1754
+ const entries = (await readIndex(args.indexKey)).filter(
1755
+ (entry) => args.scoreMax === void 0 ? true : entry.score <= args.scoreMax
1756
+ ).sort(
1757
+ args.order === "asc" ? compareIndexAscending : compareIndexDescending
1758
+ );
1759
+ const offset = Math.max(0, args.offset ?? 0);
1760
+ return entries.slice(
1761
+ offset,
1762
+ args.limit === void 0 ? entries.length : offset + args.limit
1763
+ );
1764
+ },
1765
+ async remove(args) {
1766
+ await withIndexLock(state, args.indexKey, async () => {
1767
+ const entries = await readIndex(args.indexKey);
1768
+ const next = entries.filter(
1769
+ (entry) => entry.conversationId !== args.conversationId
1770
+ );
1771
+ if (next.length === entries.length) {
1772
+ return;
1773
+ }
1774
+ await writeIndex(args.indexKey, next);
1775
+ });
1776
+ },
1777
+ async upsert(args) {
1778
+ await withIndexLock(state, args.indexKey, async () => {
1779
+ const entries = await readIndex(args.indexKey);
1780
+ const withoutCurrent = entries.filter(
1781
+ (entry) => entry.conversationId !== args.conversationId
1782
+ );
1783
+ const next = retainedIndexEntries(args.indexKey, [
1784
+ ...withoutCurrent,
1785
+ { conversationId: args.conversationId, score: args.score }
1786
+ ]);
1787
+ await writeIndex(args.indexKey, next);
1788
+ });
1789
+ }
1790
+ };
1791
+ }
1792
+ async function getConversationIndexStore(state) {
1793
+ const redisStateAdapter = await getDefaultRedisStateAdapterFor(state);
1794
+ if (redisStateAdapter) {
1795
+ return redisConversationIndexStore(redisStateAdapter.getClient());
1796
+ }
1797
+ return emulatedConversationIndexStore(state);
1798
+ }
1799
+ async function upsertIndexEntry(args) {
1800
+ const index = await getConversationIndexStore(args.state);
1801
+ await index.upsert({
1802
+ conversationId: args.conversationId,
1803
+ indexKey: args.indexKey,
1804
+ score: args.score
1805
+ });
1806
+ }
1807
+ async function removeIndexEntry(args) {
1808
+ const index = await getConversationIndexStore(args.state);
1809
+ await index.remove({
1810
+ conversationId: args.conversationId,
1811
+ indexKey: args.indexKey
1812
+ });
1813
+ }
1814
+ async function acquireMutationLock(state, conversationId) {
1815
+ const startedAtMs = now();
1816
+ while (true) {
1817
+ const lock = await state.acquireLock(
1818
+ mutationLockKey(conversationId),
1819
+ CONVERSATION_MUTATION_LOCK_TTL_MS
1820
+ );
1821
+ if (lock) {
1822
+ return lock;
1823
+ }
1824
+ if (now() - startedAtMs >= CONVERSATION_MUTATION_WAIT_MS) {
1825
+ throw new Error(
1826
+ `Could not acquire conversation mutation lock for ${conversationId}`
1827
+ );
1828
+ }
1829
+ await sleep2(CONVERSATION_MUTATION_RETRY_MS);
1830
+ }
1831
+ }
1832
+ async function withConversationMutation(args, callback) {
1833
+ const state = await getConnectedState(args.state);
1834
+ const lock = await acquireMutationLock(state, args.conversationId);
1835
+ try {
1836
+ return await callback(state, lock);
1837
+ } finally {
1838
+ await state.releaseLock(lock);
1839
+ }
1840
+ }
1841
+ async function readConversation(state, conversationId) {
1842
+ const raw = await state.get(conversationKey(conversationId));
1843
+ if (raw == null) {
1844
+ return void 0;
1845
+ }
1846
+ const conversation = normalizeConversation(conversationId, raw);
1847
+ if (!conversation) {
1848
+ throw new InvalidConversationRecordError(conversationId);
1849
+ }
1850
+ return conversation;
1851
+ }
1852
+ async function writeConversation(state, lock, conversation) {
1853
+ const execution = executionWithPendingMessages(
1854
+ conversation.execution,
1855
+ conversation.execution.pendingMessages
1856
+ );
1857
+ const next = {
1858
+ ...conversation,
1859
+ execution
1860
+ };
1861
+ const fenced = await state.extendLock(
1862
+ lock,
1863
+ CONVERSATION_MUTATION_LOCK_TTL_MS
1864
+ );
1865
+ if (!fenced) {
1866
+ throw new ConversationMutationFencedError(next.conversationId);
1867
+ }
1868
+ await state.set(
1869
+ conversationKey(next.conversationId),
1870
+ next,
1871
+ JUNIOR_THREAD_STATE_TTL_MS
1872
+ );
1873
+ await upsertIndexEntry({
1874
+ state,
1875
+ indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
1876
+ conversationId: next.conversationId,
1877
+ score: next.lastActivityAtMs
1878
+ });
1879
+ if (!hasRunnableWork(next)) {
1880
+ await removeIndexEntry({
1881
+ state,
1882
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
1883
+ conversationId: next.conversationId
1884
+ });
1885
+ return;
1886
+ }
1887
+ await upsertIndexEntry({
1888
+ state,
1889
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
1890
+ conversationId: next.conversationId,
1891
+ score: next.execution.updatedAtMs ?? next.updatedAtMs
1892
+ });
1893
+ }
1894
+ function assertSameConversationDestination(args) {
1895
+ if (!args.current || sameDestination(args.current, args.next)) {
1896
+ return;
1897
+ }
1898
+ throw new Error(
1899
+ `Conversation destination changed for ${args.conversationId}`
1900
+ );
1901
+ }
1902
+ function conversationWorkState(conversation) {
1903
+ const lease = conversation.execution.lease;
1904
+ return {
1905
+ ...conversation,
1906
+ lastEnqueuedAtMs: conversation.execution.lastEnqueuedAtMs,
1907
+ ...lease ? {
1908
+ lease: {
1909
+ acquiredAtMs: lease.acquiredAtMs,
1910
+ lastCheckInAtMs: lease.lastCheckInAtMs,
1911
+ leaseExpiresAtMs: lease.expiresAtMs,
1912
+ leaseToken: lease.token
1913
+ }
1914
+ } : {},
1915
+ messages: pendingMessages(conversation),
1916
+ needsRun: hasRunnableWork(conversation)
1917
+ };
1918
+ }
1919
+ async function getConversation(args) {
1920
+ const state = await getConnectedState(args.state);
1921
+ return await readConversation(state, args.conversationId);
1922
+ }
1923
+ async function getConversationWorkState(args) {
1924
+ const conversation = await getConversation(args);
1925
+ return conversation ? conversationWorkState(conversation) : void 0;
1926
+ }
1927
+ async function appendInboundMessage(args) {
1928
+ const nowMs = args.nowMs ?? now();
1929
+ return await withConversationMutation(
1930
+ { conversationId: args.message.conversationId, state: args.state },
1931
+ async (state, lock) => {
1932
+ const current = await readConversation(state, args.message.conversationId) ?? emptyConversation({
1933
+ conversationId: args.message.conversationId,
1934
+ destination: args.message.destination,
1935
+ nowMs,
1936
+ source: args.message.source
1937
+ });
1938
+ assertSameConversationDestination({
1939
+ conversationId: args.message.conversationId,
1940
+ current: current.destination,
1941
+ next: args.message.destination
1942
+ });
1943
+ const existingPending = current.execution.pendingMessages.some(
1944
+ (message) => message.inboundMessageId === args.message.inboundMessageId
1945
+ );
1946
+ const existing = current.execution.inboundMessageIds.includes(
1947
+ args.message.inboundMessageId
1948
+ );
1949
+ if (existing) {
1950
+ if (!existingPending) {
1951
+ return { status: "duplicate" };
1952
+ }
1953
+ const nextStatus = current.execution.status === "idle" ? "pending" : current.execution.status;
1954
+ await writeConversation(
1955
+ state,
1956
+ lock,
1957
+ withExecutionUpdate(
1958
+ current,
1959
+ {
1960
+ ...current.execution,
1961
+ status: nextStatus,
1962
+ pendingMessages: current.execution.pendingMessages.map(
1963
+ (message) => message.inboundMessageId === args.message.inboundMessageId ? upgradedPendingMessage(message, args.message) : message
1964
+ )
1965
+ },
1966
+ nowMs
1967
+ )
1968
+ );
1969
+ return { status: "duplicate" };
1970
+ }
1971
+ const status = current.execution.lease && current.execution.status === "running" ? "running" : current.execution.lease ? "awaiting_resume" : "pending";
1972
+ const next = {
1973
+ ...current,
1974
+ destination: current.destination ?? args.message.destination,
1975
+ source: current.source ?? args.message.source,
1976
+ lastActivityAtMs: nowMs
1977
+ };
1978
+ await writeConversation(
1979
+ state,
1980
+ lock,
1981
+ withExecutionUpdate(
1982
+ next,
1983
+ {
1984
+ ...current.execution,
1985
+ status,
1986
+ inboundMessageIds: [
1987
+ ...current.execution.inboundMessageIds,
1988
+ args.message.inboundMessageId
1989
+ ],
1990
+ pendingMessages: [
1991
+ ...current.execution.pendingMessages,
1992
+ args.message
1993
+ ].sort(compareMessages)
1994
+ },
1995
+ nowMs
1996
+ )
1997
+ );
1998
+ return { status: "appended" };
1999
+ }
2000
+ );
2001
+ }
2002
+ async function requestConversationWork(args) {
2003
+ const nowMs = args.nowMs ?? now();
2004
+ return await withConversationMutation(args, async (state, lock) => {
2005
+ const existing = await readConversation(state, args.conversationId);
2006
+ if (existing) {
2007
+ assertSameConversationDestination({
2008
+ conversationId: args.conversationId,
2009
+ current: existing.destination,
2010
+ next: args.destination
2011
+ });
2012
+ }
2013
+ const current = existing ?? emptyConversation({
2014
+ conversationId: args.conversationId,
2015
+ destination: args.destination,
2016
+ nowMs
2017
+ });
2018
+ const status = current.execution.lease ? "awaiting_resume" : "pending";
2019
+ await writeConversation(
2020
+ state,
2021
+ lock,
2022
+ withExecutionUpdate(
2023
+ {
2024
+ ...current,
2025
+ destination: current.destination ?? args.destination
2026
+ },
2027
+ {
2028
+ ...current.execution,
2029
+ status
2030
+ },
2031
+ nowMs
2032
+ )
2033
+ );
2034
+ return { status: existing === void 0 ? "created" : "updated" };
2035
+ });
2036
+ }
2037
+ async function markConversationWorkEnqueued(args) {
2038
+ const nowMs = args.nowMs ?? now();
2039
+ await withConversationMutation(args, async (state, lock) => {
2040
+ const current = await readConversation(state, args.conversationId);
2041
+ if (!current) {
2042
+ return;
2043
+ }
2044
+ await writeConversation(
2045
+ state,
2046
+ lock,
2047
+ withExecutionUpdate(
2048
+ current,
2049
+ {
2050
+ ...current.execution,
2051
+ lastEnqueuedAtMs: nowMs
2052
+ },
2053
+ nowMs
2054
+ )
2055
+ );
2056
+ });
2057
+ }
2058
+ async function clearConsumedConversationWake(args) {
2059
+ const nowMs = args.nowMs ?? now();
2060
+ return await withConversationMutation(args, async (state, lock) => {
2061
+ const current = await readConversation(state, args.conversationId);
2062
+ if (!current || hasRunnableWork(current) || current.execution.lastEnqueuedAtMs === void 0) {
2063
+ return false;
2064
+ }
2065
+ await writeConversation(
2066
+ state,
2067
+ lock,
2068
+ withExecutionUpdate(
2069
+ current,
2070
+ {
2071
+ ...current.execution,
2072
+ lastEnqueuedAtMs: void 0
2073
+ },
2074
+ nowMs
2075
+ )
2076
+ );
2077
+ return true;
2078
+ });
2079
+ }
2080
+ async function startConversationWork(args) {
2081
+ const nowMs = args.nowMs ?? now();
2082
+ return await withConversationMutation(args, async (state, lock) => {
2083
+ const current = await readConversation(state, args.conversationId);
2084
+ if (!current) {
2085
+ return { status: "no_work" };
2086
+ }
2087
+ if (isLeaseActive(current.execution.lease, nowMs)) {
2088
+ return {
2089
+ status: "active",
2090
+ leaseExpiresAtMs: current.execution.lease.expiresAtMs
2091
+ };
2092
+ }
2093
+ if (!hasRunnableWork(current)) {
2094
+ return { status: "no_work" };
2095
+ }
2096
+ const lease = {
2097
+ token: randomUUID(),
2098
+ acquiredAtMs: nowMs,
2099
+ lastCheckInAtMs: nowMs,
2100
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
2101
+ };
2102
+ await writeConversation(
2103
+ state,
2104
+ lock,
2105
+ withExecutionUpdate(
2106
+ current,
2107
+ {
2108
+ ...current.execution,
2109
+ lease,
2110
+ status: "running",
2111
+ runId: current.execution.runId ?? randomUUID(),
2112
+ lastEnqueuedAtMs: void 0
2113
+ },
2114
+ nowMs
2115
+ )
2116
+ );
2117
+ return {
2118
+ status: "acquired",
2119
+ leaseToken: lease.token,
2120
+ leaseExpiresAtMs: lease.expiresAtMs
2121
+ };
2122
+ });
2123
+ }
2124
+ async function checkInConversationWork(args) {
2125
+ const nowMs = args.nowMs ?? now();
2126
+ return await withConversationMutation(args, async (state, lock) => {
2127
+ const current = await readConversation(state, args.conversationId);
2128
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2129
+ return false;
2130
+ }
2131
+ await writeConversation(
2132
+ state,
2133
+ lock,
2134
+ withExecutionUpdate(
2135
+ current,
2136
+ {
2137
+ ...current.execution,
2138
+ lease: {
2139
+ ...current.execution.lease,
2140
+ lastCheckInAtMs: nowMs,
2141
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
2142
+ }
2143
+ },
2144
+ nowMs
2145
+ )
2146
+ );
2147
+ return true;
2148
+ });
2149
+ }
2150
+ async function drainConversationMailbox(args) {
2151
+ const nowMs = args.nowMs ?? now();
2152
+ const pending = await withConversationMutation(args, async (state) => {
2153
+ const current = await readConversation(state, args.conversationId);
2154
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2155
+ throw new Error(
2156
+ `Conversation lease is not held for ${args.conversationId}`
2157
+ );
2158
+ }
2159
+ return pendingMessages(current);
2160
+ });
2161
+ if (pending.length === 0) {
2162
+ return [];
2163
+ }
2164
+ const acknowledgedIds = await args.handle(pending);
2165
+ const pendingIds = new Set(
2166
+ pending.map((message) => message.inboundMessageId)
2167
+ );
2168
+ for (const inboundMessageId of acknowledgedIds ?? []) {
2169
+ if (!pendingIds.has(inboundMessageId)) {
2170
+ throw new Error(
2171
+ `Conversation mailbox acknowledgement is not pending for ${args.conversationId}`
2172
+ );
2173
+ }
2174
+ }
2175
+ const drainedIds = new Set(
2176
+ acknowledgedIds ?? pending.map((message) => message.inboundMessageId)
2177
+ );
2178
+ await withConversationMutation(args, async (state, lock) => {
2179
+ const current = await readConversation(state, args.conversationId);
2180
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2181
+ throw new Error(
2182
+ `Conversation lease is not held for ${args.conversationId}`
2183
+ );
2184
+ }
2185
+ const pendingMessages2 = current.execution.pendingMessages.filter(
2186
+ (message) => !drainedIds.has(message.inboundMessageId)
2187
+ );
2188
+ await writeConversation(
2189
+ state,
2190
+ lock,
2191
+ withExecutionUpdate(
2192
+ current,
2193
+ {
2194
+ ...current.execution,
2195
+ status: current.execution.status === "pending" && pendingMessages2.length === 0 ? "running" : current.execution.status,
2196
+ pendingMessages: pendingMessages2
2197
+ },
2198
+ nowMs
2199
+ )
2200
+ );
2201
+ });
2202
+ return pending.filter((message) => drainedIds.has(message.inboundMessageId));
2203
+ }
2204
+ async function ackMessages(args) {
2205
+ const nowMs = args.nowMs ?? now();
2206
+ const inboundMessageIds = new Set(args.inboundMessageIds);
2207
+ return await withConversationMutation(args, async (state, lock) => {
2208
+ const current = await readConversation(state, args.conversationId);
2209
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2210
+ return false;
2211
+ }
2212
+ if (inboundMessageIds.size === 0) {
2213
+ return true;
2214
+ }
2215
+ const pendingMessages2 = current.execution.pendingMessages.filter(
2216
+ (message) => !inboundMessageIds.has(message.inboundMessageId)
2217
+ );
2218
+ if (pendingMessages2.length === current.execution.pendingMessages.length) {
2219
+ return true;
2220
+ }
2221
+ await writeConversation(
2222
+ state,
2223
+ lock,
2224
+ withExecutionUpdate(
2225
+ current,
2226
+ {
2227
+ ...current.execution,
2228
+ pendingMessages: pendingMessages2
2229
+ },
2230
+ nowMs
2231
+ )
2232
+ );
2233
+ return true;
2234
+ });
2235
+ }
2236
+ async function requestConversationContinuation(args) {
2237
+ const nowMs = args.nowMs ?? now();
2238
+ return await withConversationMutation(args, async (state, lock) => {
2239
+ const current = await readConversation(state, args.conversationId);
2240
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2241
+ return false;
2242
+ }
2243
+ assertSameConversationDestination({
2244
+ conversationId: args.conversationId,
2245
+ current: current.destination,
2246
+ next: args.destination
2247
+ });
2248
+ await writeConversation(
2249
+ state,
2250
+ lock,
2251
+ withExecutionUpdate(
2252
+ current,
2253
+ {
2254
+ ...current.execution,
2255
+ status: "awaiting_resume"
2256
+ },
2257
+ nowMs
2258
+ )
2259
+ );
2260
+ return true;
2261
+ });
2262
+ }
2263
+ async function releaseConversationWork(args) {
2264
+ const nowMs = args.nowMs ?? now();
2265
+ return await withConversationMutation(args, async (state, lock) => {
2266
+ const current = await readConversation(state, args.conversationId);
2267
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2268
+ return false;
2269
+ }
2270
+ await writeConversation(
2271
+ state,
2272
+ lock,
2273
+ withExecutionUpdate(
2274
+ current,
2275
+ {
2276
+ ...current.execution,
2277
+ lease: void 0,
2278
+ status: current.execution.status === "running" ? "pending" : current.execution.status
2279
+ },
2280
+ nowMs
2281
+ )
2282
+ );
2283
+ return true;
2284
+ });
2285
+ }
2286
+ async function completeConversationWork(args) {
2287
+ const nowMs = args.nowMs ?? now();
2288
+ return await withConversationMutation(args, async (state, lock) => {
2289
+ const current = await readConversation(state, args.conversationId);
2290
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2291
+ return "lost_lease";
2292
+ }
2293
+ const hasPending = pendingMessages(current).length > 0;
2294
+ const needsRun = current.execution.status === "awaiting_resume";
2295
+ const runnable = needsRun || hasPending;
2296
+ await writeConversation(
2297
+ state,
2298
+ lock,
2299
+ withExecutionUpdate(
2300
+ current,
2301
+ {
2302
+ ...current.execution,
2303
+ lease: void 0,
2304
+ status: runnable ? "pending" : "idle",
2305
+ runId: runnable ? current.execution.runId : void 0
2306
+ },
2307
+ nowMs
2308
+ )
2309
+ );
2310
+ return runnable ? "pending" : "completed";
2311
+ });
2312
+ }
2313
+ async function recordAttemptFailure(args) {
2314
+ const nowMs = args.nowMs ?? now();
2315
+ return await withConversationMutation(args, async (state, lock) => {
2316
+ const current = await readConversation(state, args.conversationId);
2317
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2318
+ return {
2319
+ status: "lost_lease",
2320
+ pendingCount: 0,
2321
+ deadLetteredMessages: []
2322
+ };
2323
+ }
2324
+ const pendingIds = new Set(
2325
+ current.execution.pendingMessages.map(
2326
+ (message) => message.inboundMessageId
2327
+ )
2328
+ );
2329
+ if (args.inboundMessageIds.length === 0 || args.inboundMessageIds.some((id) => !pendingIds.has(id))) {
2330
+ return {
2331
+ status: "skipped",
2332
+ pendingCount: current.execution.pendingMessages.length,
2333
+ deadLetteredMessages: []
2334
+ };
1261
2335
  }
1262
- } catch (error) {
1263
- if (error instanceof AuthorizationFlowDisabledError) {
1264
- if (lifecycleStarted && !lifecycleTerminalized) {
1265
- await turnLifecycle.fail({
1266
- conversationId,
1267
- turnId,
1268
- createdAtMs: Date.now(),
1269
- failureCode: "agent_run_failed"
1270
- });
1271
- lifecycleTerminalized = true;
2336
+ const attemptedIds = new Set(args.inboundMessageIds);
2337
+ const deadLetteredMessages = [];
2338
+ const pendingMessages2 = [];
2339
+ for (const message of current.execution.pendingMessages) {
2340
+ if (!attemptedIds.has(message.inboundMessageId)) {
2341
+ pendingMessages2.push(message);
2342
+ continue;
1272
2343
  }
1273
- await markDispatch({
1274
- dispatch,
1275
- status: "blocked",
1276
- errorMessage: `Dispatch requires ${error.provider} authorization.`
1277
- });
1278
- return;
1279
- }
1280
- if (error instanceof PluginCredentialFailureError) {
1281
- if (lifecycleStarted && !lifecycleTerminalized) {
1282
- await turnLifecycle.fail({
1283
- conversationId,
1284
- turnId,
1285
- createdAtMs: Date.now(),
1286
- failureCode: "agent_run_failed"
1287
- });
1288
- lifecycleTerminalized = true;
2344
+ const attempted = {
2345
+ ...message,
2346
+ attemptCount: (message.attemptCount ?? 0) + 1
2347
+ };
2348
+ if (attempted.attemptCount >= CONVERSATION_WORK_MAX_DELIVERY_ATTEMPTS) {
2349
+ deadLetteredMessages.push(attempted);
2350
+ continue;
1289
2351
  }
1290
- await markDispatch({
1291
- dispatch,
1292
- status: "blocked",
1293
- errorMessage: error.message
1294
- });
1295
- return;
2352
+ pendingMessages2.push(attempted);
1296
2353
  }
1297
- const eventId = logException(
1298
- error,
1299
- "agent_dispatch_run_failed",
1300
- {
1301
- ...logContext,
1302
- modelId: standardModelId(botConfig)
1303
- },
1304
- {},
1305
- "Agent dispatch failed"
2354
+ await writeConversation(
2355
+ state,
2356
+ lock,
2357
+ withExecutionUpdate(
2358
+ current,
2359
+ {
2360
+ ...current.execution,
2361
+ pendingMessages: pendingMessages2
2362
+ },
2363
+ nowMs
2364
+ )
1306
2365
  );
1307
- if (lifecycleStarted && !lifecycleTerminalized) {
1308
- await turnLifecycle.fail({
1309
- conversationId,
1310
- turnId,
1311
- createdAtMs: Date.now(),
1312
- failureCode,
1313
- ...eventId ? { eventId } : {}
1314
- });
1315
- lifecycleTerminalized = true;
2366
+ return {
2367
+ status: "recorded",
2368
+ pendingCount: pendingMessages2.length,
2369
+ deadLetteredMessages
2370
+ };
2371
+ });
2372
+ }
2373
+ async function deadLetterAttempt(args) {
2374
+ const nowMs = args.nowMs ?? now();
2375
+ return await withConversationMutation(args, async (state, lock) => {
2376
+ const current = await readConversation(state, args.conversationId);
2377
+ if (!current || current.execution.lease?.token !== args.leaseToken) {
2378
+ return "lost_lease";
1316
2379
  }
1317
- await markDispatch({
1318
- dispatch,
1319
- status: "failed",
1320
- errorMessage: error instanceof Error ? error.message : String(error)
1321
- });
1322
- } finally {
1323
- await stateAdapter.releaseLock(destinationLock);
1324
- }
2380
+ const runnable = pendingMessages(current).length > 0;
2381
+ await writeConversation(
2382
+ state,
2383
+ lock,
2384
+ withExecutionUpdate(
2385
+ current,
2386
+ {
2387
+ ...current.execution,
2388
+ lease: void 0,
2389
+ status: runnable ? "pending" : "failed",
2390
+ runId: runnable ? current.execution.runId : void 0
2391
+ },
2392
+ nowMs
2393
+ )
2394
+ );
2395
+ return runnable ? "pending" : "failed";
2396
+ });
1325
2397
  }
1326
-
1327
- // src/handlers/agent-dispatch.ts
1328
- async function POST(request, waitUntil, options) {
1329
- const payload = await verifyDispatchCallbackRequest(request);
1330
- if (!payload) {
1331
- return new Response("Unauthorized", { status: 401 });
1332
- }
1333
- waitUntil(
1334
- () => runAgentDispatchSlice(payload, {
1335
- agentRunner: options.agentRunner
1336
- }).catch((error) => {
1337
- logException(
1338
- error,
1339
- "agent_dispatch_handler_failed",
1340
- {},
1341
- { "app.dispatch.id": payload.id },
1342
- "Agent dispatch handler failed"
1343
- );
1344
- })
1345
- );
1346
- return new Response("Accepted", { status: 202 });
2398
+ async function clearExpiredConversationLease(args) {
2399
+ const nowMs = args.nowMs ?? now();
2400
+ return await withConversationMutation(args, async (state, lock) => {
2401
+ const current = await readConversation(state, args.conversationId);
2402
+ if (!current?.execution.lease || current.execution.lease.expiresAtMs > nowMs) {
2403
+ return false;
2404
+ }
2405
+ await writeConversation(
2406
+ state,
2407
+ lock,
2408
+ withExecutionUpdate(
2409
+ current,
2410
+ {
2411
+ ...current.execution,
2412
+ lease: void 0,
2413
+ status: "pending"
2414
+ },
2415
+ nowMs
2416
+ )
2417
+ );
2418
+ return true;
2419
+ });
2420
+ }
2421
+ async function removeActiveConversation(args) {
2422
+ const state = await getConnectedState(args.state);
2423
+ await removeIndexEntry({
2424
+ state,
2425
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
2426
+ conversationId: args.conversationId
2427
+ });
2428
+ }
2429
+ async function listActiveConversationIds(args = {}) {
2430
+ const state = await getConnectedState(args.state);
2431
+ const index = await getConversationIndexStore(state);
2432
+ const entries = await index.list({
2433
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
2434
+ limit: args.limit,
2435
+ order: "asc",
2436
+ scoreMax: args.staleBeforeMs
2437
+ });
2438
+ return entries.map((entry) => entry.conversationId);
1347
2439
  }
1348
-
1349
- // src/handlers/heartbeat.ts
1350
- import { timingSafeEqual as timingSafeEqual2 } from "crypto";
1351
2440
 
1352
2441
  // src/chat/task-execution/store.ts
1353
2442
  function metadataStore(options) {
@@ -1360,7 +2449,7 @@ function hasRecentEnqueueMarker(conversation, nowMs) {
1360
2449
  const lastEnqueuedAtMs = conversation.execution.lastEnqueuedAtMs;
1361
2450
  return typeof lastEnqueuedAtMs === "number" && lastEnqueuedAtMs + CONVERSATION_WORK_STALE_ENQUEUE_MS > nowMs;
1362
2451
  }
1363
- function now() {
2452
+ function now2() {
1364
2453
  return Date.now();
1365
2454
  }
1366
2455
  async function recordExecutionMetadata(args) {
@@ -1401,7 +2490,7 @@ function hasRunnableConversationWork(conversation) {
1401
2490
  return conversation.execution.status !== "idle" || countPendingConversationMessages(conversation) > 0;
1402
2491
  }
1403
2492
  async function ensureConversationWake(args) {
1404
- const nowMs = args.nowMs ?? now();
2493
+ const nowMs = args.nowMs ?? now2();
1405
2494
  const conversation = await getConversation({
1406
2495
  conversationId: args.conversationId,
1407
2496
  state: args.state
@@ -1434,7 +2523,7 @@ async function ensureConversationWake(args) {
1434
2523
  return { status: "enqueued", queueMessageId: queueResult?.messageId };
1435
2524
  }
1436
2525
  async function appendAndEnqueueInboundMessage(args) {
1437
- const nowMs = args.nowMs ?? now();
2526
+ const nowMs = args.nowMs ?? now2();
1438
2527
  const appendResult = await appendInboundMessage({
1439
2528
  message: args.message,
1440
2529
  nowMs,
@@ -2432,7 +3521,7 @@ var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
2432
3521
  var STATUS_MIN_VISIBLE_MS = 1200;
2433
3522
  var STATUS_ROTATION_INTERVAL_MS = 3e4;
2434
3523
  function createAssistantStatusScheduler(args) {
2435
- const now3 = args.now ?? (() => Date.now());
3524
+ const now4 = args.now ?? (() => Date.now());
2436
3525
  const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
2437
3526
  const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
2438
3527
  const random = args.random ?? Math.random;
@@ -2496,7 +3585,7 @@ function createAssistantStatusScheduler(args) {
2496
3585
  }
2497
3586
  currentVisibleStatus = text;
2498
3587
  currentLoadingMessages = nextLoadingMessages;
2499
- lastStatusAt = now3();
3588
+ lastStatusAt = now4();
2500
3589
  scheduleRotation();
2501
3590
  await enqueueStatusUpdate(async () => {
2502
3591
  await args.sendStatus(text, nextLoadingMessages);
@@ -2583,7 +3672,7 @@ function createAssistantStatusScheduler(args) {
2583
3672
  }
2584
3673
  return;
2585
3674
  }
2586
- const elapsed = now3() - lastStatusAt;
3675
+ const elapsed = now4() - lastStatusAt;
2587
3676
  const waitMs = Math.max(
2588
3677
  STATUS_UPDATE_DEBOUNCE_MS - elapsed,
2589
3678
  STATUS_MIN_VISIBLE_MS - elapsed,
@@ -2981,19 +4070,14 @@ function resolveReplyTimeoutMs(explicitTimeoutMs) {
2981
4070
  const parsed = Number.parseInt(raw, 10);
2982
4071
  return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
2983
4072
  }
2984
- async function postSlackMessageBestEffort(channelId, threadTs, text, footer) {
4073
+ async function postSlackMessageBestEffort(channelId, threadTs, text, conversationId) {
2985
4074
  try {
2986
- if (footer) {
2987
- await postSlackApiReplyPosts({
4075
+ if (conversationId) {
4076
+ await sendSlackReply({
2988
4077
  channelId,
2989
- threadTs,
2990
- posts: [
2991
- {
2992
- text,
2993
- stage: "thread_reply"
2994
- }
2995
- ],
2996
- footer
4078
+ conversationId,
4079
+ text,
4080
+ threadTs
2997
4081
  });
2998
4082
  return;
2999
4083
  }
@@ -3041,10 +4125,10 @@ function getResumeLogContext(args, lockKey) {
3041
4125
  const actor = routing?.actor;
3042
4126
  return {
3043
4127
  conversationId: args.conversationId,
3044
- slackThreadId: lockKey,
3045
- slackUserId: isUserActor(actor) ? actor.userId : void 0,
3046
- slackUserName: isUserActor(actor) ? actor.userName : void 0,
3047
- slackChannelId: args.channelId,
4128
+ messageConversationId: lockKey,
4129
+ userId: isUserActor(actor) ? actor.userId : void 0,
4130
+ userName: isUserActor(actor) ? actor.userName : void 0,
4131
+ destinationName: args.channelId,
3048
4132
  assistantUserName: botConfig.userName,
3049
4133
  modelId: standardModelId(botConfig)
3050
4134
  };
@@ -3308,18 +4392,18 @@ async function resumeSlackTurn(args) {
3308
4392
  };
3309
4393
  };
3310
4394
  const deliverAssistantMessage = async (assistantMessage) => {
3311
- const posts = planSlackAssistantMessagePosts(assistantMessage.text);
3312
- if (posts.length === 0) {
4395
+ if (!assistantMessage.text.trim()) {
3313
4396
  return;
3314
4397
  }
3315
4398
  failureCode = "delivery_failed";
3316
4399
  const deliveryState2 = await getDeliveryConversation();
3317
4400
  let messageTs;
3318
4401
  try {
3319
- messageTs = await postSlackApiReplyPosts({
4402
+ messageTs = await sendSlackReply({
3320
4403
  channelId: runArgs.channelId,
3321
- threadTs: runArgs.threadTs,
3322
- posts
4404
+ conversationId: runArgs.conversationId,
4405
+ text: assistantMessage.text,
4406
+ threadTs: runArgs.threadTs
3323
4407
  });
3324
4408
  } catch (error) {
3325
4409
  if (isRetryableSlackPostError(error)) {
@@ -3446,16 +4530,10 @@ async function resumeSlackTurn(args) {
3446
4530
  destination: replyContext.routing.destination,
3447
4531
  source: replyContext.routing.source,
3448
4532
  actor: resumeActor,
3449
- surface: "slack",
3450
- logContext: {
3451
- threadId: runArgs.conversationId,
3452
- actorId: isUserActor(replyContext.routing.actor) ? replyContext.routing.actor.userId : void 0,
3453
- channelId: runArgs.channelId,
3454
- assistantUserName: botConfig.userName
3455
- }
4533
+ surface: "slack"
3456
4534
  });
3457
4535
  }
3458
- await runArgs.onSuccess?.(reply);
4536
+ await runArgs.commitResult?.(reply);
3459
4537
  if (reply.diagnostics.outcome === "success") {
3460
4538
  await turnLifecycle.complete({
3461
4539
  conversationId: runArgs.conversationId,
@@ -3551,9 +4629,6 @@ async function resumeSlackTurn(args) {
3551
4629
  try {
3552
4630
  await deferredPauseHandler();
3553
4631
  if (deferredAuthInfo) {
3554
- const footer = buildSlackReplyFooter({
3555
- conversationId: runArgs.conversationId
3556
- });
3557
4632
  await postSlackMessageBestEffort(
3558
4633
  runArgs.channelId,
3559
4634
  runArgs.threadTs,
@@ -3561,7 +4636,7 @@ async function resumeSlackTurn(args) {
3561
4636
  deferredAuthInfo.actorId,
3562
4637
  deferredAuthInfo.providerDisplayName
3563
4638
  ),
3564
- footer
4639
+ runArgs.conversationId
3565
4640
  );
3566
4641
  }
3567
4642
  return true;
@@ -3964,7 +5039,7 @@ async function resumeAuthorizedMcpTurn(args) {
3964
5039
  }
3965
5040
  }
3966
5041
  },
3967
- onSuccess: async (reply) => {
5042
+ commitResult: async (reply) => {
3968
5043
  await persistCompletedReplyState(
3969
5044
  authSession.channelId,
3970
5045
  authSession.threadTs,
@@ -4562,7 +5637,7 @@ async function resumeOAuthSessionRecordTurn(stored, options) {
4562
5637
  }
4563
5638
  }
4564
5639
  },
4565
- onSuccess: async (reply) => {
5640
+ commitResult: async (reply) => {
4566
5641
  logInfo(
4567
5642
  "oauth_callback_resume_complete",
4568
5643
  {},
@@ -4849,9 +5924,9 @@ function buildJwksUrl(value) {
4849
5924
  return url;
4850
5925
  }
4851
5926
  async function getJwks(issuer) {
4852
- const now3 = Date.now();
5927
+ const now4 = Date.now();
4853
5928
  const cached = jwksByIssuer.get(issuer);
4854
- if (cached && cached.expiresAtMs > now3) {
5929
+ if (cached && cached.expiresAtMs > now4) {
4855
5930
  return cached.jwks;
4856
5931
  }
4857
5932
  if (cached) {
@@ -4876,7 +5951,7 @@ async function getJwks(issuer) {
4876
5951
  }
4877
5952
  jwksByIssuer.set(issuer, {
4878
5953
  jwks,
4879
- expiresAtMs: now3 + OIDC_DISCOVERY_CACHE_TTL_MS
5954
+ expiresAtMs: now4 + OIDC_DISCOVERY_CACHE_TTL_MS
4880
5955
  });
4881
5956
  return jwks;
4882
5957
  }
@@ -5188,6 +6263,7 @@ import {
5188
6263
  Message,
5189
6264
  ThreadImpl
5190
6265
  } from "chat";
6266
+ import { z as z3 } from "zod";
5191
6267
 
5192
6268
  // src/chat/ingress/message-router.ts
5193
6269
  function normalizeIncomingSlackThreadId(threadId, message) {
@@ -5225,7 +6301,7 @@ function rehydrateAttachmentFetchers(message, downloadPrivateSlackFile2 = downlo
5225
6301
 
5226
6302
  // src/chat/slack/adapter-context.ts
5227
6303
  var initializedAdapters = /* @__PURE__ */ new WeakSet();
5228
- async function getConnectedState(stateAdapter) {
6304
+ async function getConnectedState2(stateAdapter) {
5229
6305
  const state = stateAdapter ?? getStateAdapter();
5230
6306
  await state.connect();
5231
6307
  return state;
@@ -5234,7 +6310,7 @@ async function ensureSlackAdapterInitialized(args) {
5234
6310
  if (initializedAdapters.has(args.adapter)) {
5235
6311
  return;
5236
6312
  }
5237
- const state = await getConnectedState(args.state);
6313
+ const state = await getConnectedState2(args.state);
5238
6314
  await args.adapter.initialize({
5239
6315
  getState: () => state
5240
6316
  });
@@ -5453,6 +6529,211 @@ async function lookupSlackUser(teamId, userId) {
5453
6529
  }
5454
6530
 
5455
6531
  // src/chat/task-execution/slack-work.ts
6532
+ var slackConversationRouteSchema = z3.enum(["mention", "subscribed"]);
6533
+ var serializedDateSchema = z3.iso.datetime();
6534
+ var mdastPointSchema = z3.object({
6535
+ column: z3.number().int().positive(),
6536
+ line: z3.number().int().positive(),
6537
+ offset: z3.number().int().nonnegative().optional()
6538
+ }).strict();
6539
+ var mdastPositionSchema = z3.object({
6540
+ end: mdastPointSchema,
6541
+ start: mdastPointSchema
6542
+ }).strict();
6543
+ function isOptionalNullableString(value) {
6544
+ return value === void 0 || value === null || typeof value === "string";
6545
+ }
6546
+ function isOptionalNullableBoolean(value) {
6547
+ return value === void 0 || value === null || typeof value === "boolean";
6548
+ }
6549
+ function hasValidMdastBase(node, fields) {
6550
+ const allowed = /* @__PURE__ */ new Set(["data", "position", "type", ...fields]);
6551
+ return Object.keys(node).every((key) => allowed.has(key)) && (node.data === void 0 || typeof node.data === "object" && node.data !== null && !Array.isArray(node.data)) && (node.position === void 0 || mdastPositionSchema.safeParse(node.position).success);
6552
+ }
6553
+ var mdastPhrasingTypes = /* @__PURE__ */ new Set([
6554
+ "break",
6555
+ "delete",
6556
+ "emphasis",
6557
+ "footnoteReference",
6558
+ "html",
6559
+ "image",
6560
+ "imageReference",
6561
+ "inlineCode",
6562
+ "link",
6563
+ "linkReference",
6564
+ "strong",
6565
+ "text"
6566
+ ]);
6567
+ var mdastBlockOrDefinitionTypes = /* @__PURE__ */ new Set([
6568
+ "blockquote",
6569
+ "code",
6570
+ "definition",
6571
+ "footnoteDefinition",
6572
+ "heading",
6573
+ "html",
6574
+ "list",
6575
+ "paragraph",
6576
+ "table",
6577
+ "thematicBreak"
6578
+ ]);
6579
+ var mdastListItemTypes = /* @__PURE__ */ new Set(["listItem"]);
6580
+ var mdastTableCellTypes = /* @__PURE__ */ new Set(["tableCell"]);
6581
+ var mdastTableRowTypes = /* @__PURE__ */ new Set(["tableRow"]);
6582
+ function hasMdastChildren(node, allowedTypes) {
6583
+ return Array.isArray(node.children) && node.children.every(
6584
+ (child) => isFormattedContentNode(child) && (!allowedTypes || typeof child === "object" && child !== null && !Array.isArray(child) && allowedTypes.has(String(child.type)))
6585
+ );
6586
+ }
6587
+ function isFormattedContentNode(value) {
6588
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
6589
+ return false;
6590
+ }
6591
+ const node = value;
6592
+ switch (node.type) {
6593
+ case "break":
6594
+ case "thematicBreak":
6595
+ return hasValidMdastBase(node, []);
6596
+ case "html":
6597
+ case "inlineCode":
6598
+ case "text":
6599
+ case "yaml":
6600
+ return hasValidMdastBase(node, ["value"]) && typeof node.value === "string";
6601
+ case "code":
6602
+ return hasValidMdastBase(node, ["lang", "meta", "value"]) && typeof node.value === "string" && isOptionalNullableString(node.lang) && isOptionalNullableString(node.meta);
6603
+ case "delete":
6604
+ case "emphasis":
6605
+ case "paragraph":
6606
+ case "strong":
6607
+ case "tableCell":
6608
+ return hasValidMdastBase(node, ["children"]) && hasMdastChildren(node, mdastPhrasingTypes);
6609
+ case "blockquote":
6610
+ return hasValidMdastBase(node, ["children"]) && hasMdastChildren(node, mdastBlockOrDefinitionTypes);
6611
+ case "tableRow":
6612
+ return hasValidMdastBase(node, ["children"]) && hasMdastChildren(node, mdastTableCellTypes);
6613
+ case "heading":
6614
+ return hasValidMdastBase(node, ["children", "depth"]) && hasMdastChildren(node, mdastPhrasingTypes) && Number.isInteger(node.depth) && Number(node.depth) >= 1 && Number(node.depth) <= 6;
6615
+ case "list":
6616
+ return hasValidMdastBase(node, ["children", "ordered", "spread", "start"]) && hasMdastChildren(node, mdastListItemTypes) && isOptionalNullableBoolean(node.ordered) && isOptionalNullableBoolean(node.spread) && (node.start === void 0 || node.start === null || typeof node.start === "number" && Number.isFinite(node.start));
6617
+ case "listItem":
6618
+ return hasValidMdastBase(node, ["checked", "children", "spread"]) && hasMdastChildren(node, mdastBlockOrDefinitionTypes) && isOptionalNullableBoolean(node.checked) && isOptionalNullableBoolean(node.spread);
6619
+ case "table":
6620
+ return hasValidMdastBase(node, ["align", "children"]) && hasMdastChildren(node, mdastTableRowTypes) && (node.align === void 0 || node.align === null || Array.isArray(node.align) && node.align.every(
6621
+ (align) => align === null || align === "center" || align === "left" || align === "right"
6622
+ ));
6623
+ case "definition":
6624
+ return hasValidMdastBase(node, ["identifier", "label", "title", "url"]) && typeof node.identifier === "string" && typeof node.url === "string" && isOptionalNullableString(node.label) && isOptionalNullableString(node.title);
6625
+ case "footnoteDefinition":
6626
+ return hasValidMdastBase(node, ["children", "identifier", "label"]) && hasMdastChildren(node, mdastBlockOrDefinitionTypes) && typeof node.identifier === "string" && isOptionalNullableString(node.label);
6627
+ case "footnoteReference":
6628
+ return hasValidMdastBase(node, ["identifier", "label"]) && typeof node.identifier === "string" && isOptionalNullableString(node.label);
6629
+ case "image":
6630
+ return hasValidMdastBase(node, ["alt", "title", "url"]) && typeof node.url === "string" && isOptionalNullableString(node.alt) && isOptionalNullableString(node.title);
6631
+ case "imageReference":
6632
+ return hasValidMdastBase(node, [
6633
+ "alt",
6634
+ "identifier",
6635
+ "label",
6636
+ "referenceType"
6637
+ ]) && typeof node.identifier === "string" && isOptionalNullableString(node.alt) && isOptionalNullableString(node.label) && (node.referenceType === "collapsed" || node.referenceType === "full" || node.referenceType === "shortcut");
6638
+ case "link":
6639
+ return hasValidMdastBase(node, ["children", "title", "url"]) && hasMdastChildren(node, mdastPhrasingTypes) && typeof node.url === "string" && isOptionalNullableString(node.title);
6640
+ case "linkReference":
6641
+ return hasValidMdastBase(node, [
6642
+ "children",
6643
+ "identifier",
6644
+ "label",
6645
+ "referenceType"
6646
+ ]) && hasMdastChildren(node, mdastPhrasingTypes) && typeof node.identifier === "string" && isOptionalNullableString(node.label) && (node.referenceType === "collapsed" || node.referenceType === "full" || node.referenceType === "shortcut");
6647
+ default:
6648
+ return false;
6649
+ }
6650
+ }
6651
+ var formattedContentSchema = z3.custom(
6652
+ (value) => {
6653
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
6654
+ return false;
6655
+ }
6656
+ const root = value;
6657
+ return root.type === "root" && hasValidMdastBase(root, ["children"]) && hasMdastChildren(root);
6658
+ },
6659
+ "must be a valid formatted-content root"
6660
+ );
6661
+ var serializedMessageSchema = z3.object({
6662
+ _type: z3.literal("chat:Message"),
6663
+ attachments: z3.array(
6664
+ z3.object({
6665
+ type: z3.enum(["image", "file", "video", "audio"]),
6666
+ url: z3.string().optional(),
6667
+ name: z3.string().optional(),
6668
+ mimeType: z3.string().optional(),
6669
+ size: z3.number().finite().optional(),
6670
+ width: z3.number().finite().optional(),
6671
+ height: z3.number().finite().optional(),
6672
+ fetchMetadata: z3.record(z3.string(), z3.string()).optional()
6673
+ }).strict()
6674
+ ),
6675
+ author: z3.object({
6676
+ userId: z3.string(),
6677
+ userName: z3.string(),
6678
+ fullName: z3.string(),
6679
+ isBot: z3.union([z3.boolean(), z3.literal("unknown")]),
6680
+ isMe: z3.boolean()
6681
+ }).strict(),
6682
+ formatted: formattedContentSchema,
6683
+ id: z3.string().min(1),
6684
+ isMention: z3.boolean().optional(),
6685
+ links: z3.array(
6686
+ z3.object({
6687
+ url: z3.string(),
6688
+ title: z3.string().optional(),
6689
+ description: z3.string().optional(),
6690
+ imageUrl: z3.string().optional(),
6691
+ siteName: z3.string().optional()
6692
+ }).strict()
6693
+ ).optional(),
6694
+ metadata: z3.object({
6695
+ dateSent: serializedDateSchema,
6696
+ edited: z3.boolean(),
6697
+ editedAt: serializedDateSchema.optional()
6698
+ }).strict(),
6699
+ raw: z3.unknown(),
6700
+ text: z3.string(),
6701
+ threadId: z3.string().min(1)
6702
+ }).strict();
6703
+ var serializedThreadSchema = z3.object({
6704
+ _type: z3.literal("chat:Thread"),
6705
+ adapterName: z3.literal("slack"),
6706
+ channelId: z3.string().min(1),
6707
+ channelVisibility: z3.enum(["private", "workspace", "external", "unknown"]).optional(),
6708
+ currentMessage: serializedMessageSchema.optional(),
6709
+ id: z3.string().min(1),
6710
+ isDM: z3.boolean()
6711
+ }).strict();
6712
+ var slackInstallationSchema = z3.object({
6713
+ enterpriseId: z3.string().optional(),
6714
+ isEnterpriseInstall: z3.boolean().optional(),
6715
+ teamId: z3.string().optional()
6716
+ }).strict();
6717
+ var slackConversationMessageMetadataBaseSchema = z3.object({
6718
+ installation: slackInstallationSchema.optional(),
6719
+ message: serializedMessageSchema,
6720
+ platform: z3.literal("slack"),
6721
+ route: slackConversationRouteSchema,
6722
+ thread: serializedThreadSchema
6723
+ });
6724
+ var slackConversationMessageMetadataSchema = z3.union([
6725
+ slackConversationMessageMetadataBaseSchema.strict(),
6726
+ slackConversationMessageMetadataBaseSchema.extend({
6727
+ kind: z3.literal("resource_event"),
6728
+ resourceEvent: z3.object({
6729
+ eventKey: z3.string(),
6730
+ eventType: z3.string(),
6731
+ provider: z3.string(),
6732
+ resourceRef: z3.string(),
6733
+ subscriptionId: z3.string()
6734
+ }).strict()
6735
+ }).strict()
6736
+ ]);
5456
6737
  function requireSlackAuthorId(message) {
5457
6738
  const authorId = parseActorUserId(message.author.userId);
5458
6739
  if (!authorId) {
@@ -5497,6 +6778,7 @@ function slackSerializedResourceEventMessage(input) {
5497
6778
  raw: {
5498
6779
  channel: input.channelId,
5499
6780
  event_type: "resource_event",
6781
+ resource_event_type: input.eventType,
5500
6782
  thread_ts: input.threadTs,
5501
6783
  type: "message",
5502
6784
  user: "UJRNEVENT"
@@ -5522,6 +6804,7 @@ function createSlackResourceEventInboundMessage(input) {
5522
6804
  const timestampIso = new Date(input.event.occurredAtMs).toISOString();
5523
6805
  const message = slackSerializedResourceEventMessage({
5524
6806
  channelId: slack.channelId,
6807
+ eventType: input.event.eventType,
5525
6808
  id: messageId,
5526
6809
  text: input.text,
5527
6810
  threadTs: slack.threadTs,
@@ -5562,19 +6845,20 @@ function createSlackResourceEventInboundMessage(input) {
5562
6845
  }
5563
6846
  };
5564
6847
  }
5565
- function getConnectedState2(stateAdapter) {
6848
+ function getConnectedState3(stateAdapter) {
5566
6849
  return stateAdapter ?? getStateAdapter();
5567
6850
  }
5568
- function isSlackMetadata(value) {
5569
- return Boolean(value) && value?.platform === "slack" && (value.route === "mention" || value.route === "subscribed") && Boolean(value.thread) && Boolean(value.message);
6851
+ function parseSlackMetadata(value) {
6852
+ const parsed = slackConversationMessageMetadataSchema.safeParse(value);
6853
+ return parsed.success ? parsed.data : void 0;
5570
6854
  }
5571
6855
  function compareInboundMessages(left, right) {
5572
6856
  return left.createdAtMs - right.createdAtMs || left.receivedAtMs - right.receivedAtMs || left.inboundMessageId.localeCompare(right.inboundMessageId);
5573
6857
  }
5574
6858
  function routeForRecords(records) {
5575
6859
  return records.some((record) => {
5576
- const metadata = record.input.metadata;
5577
- if (!isSlackMetadata(metadata)) {
6860
+ const metadata = parseSlackMetadata(record.input.metadata);
6861
+ if (!metadata) {
5578
6862
  throw new Error("Conversation mailbox record is not Slack metadata");
5579
6863
  }
5580
6864
  return metadata.route === "mention";
@@ -5589,8 +6873,8 @@ function isResourceEventNotificationMessage(message) {
5589
6873
  return raw?.event_type === "resource_event";
5590
6874
  }
5591
6875
  function restoreMessage(args) {
5592
- const metadata = args.record.input.metadata;
5593
- if (!isSlackMetadata(metadata)) {
6876
+ const metadata = parseSlackMetadata(args.record.input.metadata);
6877
+ if (!metadata) {
5594
6878
  throw new Error("Conversation mailbox record is not a Slack message");
5595
6879
  }
5596
6880
  const message = Message.fromJSON(metadata.message);
@@ -5646,8 +6930,8 @@ function restoreThread(args) {
5646
6930
  }
5647
6931
  function getInstallation(records) {
5648
6932
  for (let index = records.length - 1; index >= 0; index -= 1) {
5649
- const metadata = records[index]?.input.metadata;
5650
- if (isSlackMetadata(metadata) && metadata.installation) {
6933
+ const metadata = parseSlackMetadata(records[index]?.input.metadata);
6934
+ if (metadata?.installation) {
5651
6935
  return metadata.installation;
5652
6936
  }
5653
6937
  }
@@ -5663,7 +6947,7 @@ function createSlackConversationWorker(options) {
5663
6947
  return async (context) => {
5664
6948
  const adapter = options.getSlackAdapter();
5665
6949
  const actorLookup = options.lookupSlackUser ?? lookupSlackUser;
5666
- const state = getConnectedState2(options.state);
6950
+ const state = getConnectedState3(options.state);
5667
6951
  await state.connect();
5668
6952
  const records = getPendingRecords({
5669
6953
  execution: { pendingMessages: [...context.attempt.messages] }
@@ -5687,8 +6971,8 @@ function createSlackConversationWorker(options) {
5687
6971
  if (!latestRecord) {
5688
6972
  return { status: "completed" };
5689
6973
  }
5690
- const latestMetadata = latestRecord.input.metadata;
5691
- if (!isSlackMetadata(latestMetadata)) {
6974
+ const latestMetadata = parseSlackMetadata(latestRecord.input.metadata);
6975
+ if (!latestMetadata) {
5692
6976
  throw new Error(
5693
6977
  "Latest conversation mailbox record is not Slack metadata"
5694
6978
  );
@@ -5747,8 +7031,8 @@ function createSlackConversationWorker(options) {
5747
7031
  const drainSteeringMessages = async (accept) => {
5748
7032
  await context.attempt.drain(async (pendingRecords) => {
5749
7033
  const messages2 = pendingRecords.map((record) => {
5750
- const metadata = record.input.metadata;
5751
- if (!isSlackMetadata(metadata)) {
7034
+ const metadata = parseSlackMetadata(record.input.metadata);
7035
+ if (!metadata) {
5752
7036
  throw new Error(
5753
7037
  "Conversation mailbox record is not Slack metadata"
5754
7038
  );
@@ -5832,7 +7116,7 @@ function buildSlackInboundMessage(args) {
5832
7116
  platform: "slack",
5833
7117
  route: args.route,
5834
7118
  installation: args.installation,
5835
- thread: args.thread.toJSON(),
7119
+ thread: serializedThreadSchema.parse(args.thread.toJSON()),
5836
7120
  message: args.message.toJSON()
5837
7121
  }
5838
7122
  }
@@ -6015,7 +7299,7 @@ async function handleUnlink(event, actorId, provider) {
6015
7299
  await tokenStore.delete(actorId, provider);
6016
7300
  logInfo(
6017
7301
  "slash_command_unlink",
6018
- { slackUserId: actorId },
7302
+ { userId: actorId },
6019
7303
  { "app.credential.provider": provider },
6020
7304
  `Unlinked ${formatProviderLabel(provider)} account via ${getCommandName()} slash command`
6021
7305
  );
@@ -6237,7 +7521,7 @@ async function handleSlackEvent(args) {
6237
7521
  );
6238
7522
  } catch (error) {
6239
7523
  logException(error, "slack_app_home_publish_failed", {
6240
- slackUserId: userId
7524
+ userId
6241
7525
  });
6242
7526
  }
6243
7527
  }
@@ -6348,7 +7632,7 @@ async function handleSlashCommandForm(args) {
6348
7632
  await withSpan(
6349
7633
  "chat.slash_command",
6350
7634
  "chat.slash_command",
6351
- { slackUserId: userId },
7635
+ { userId },
6352
7636
  async () => {
6353
7637
  await handleSlashCommand({
6354
7638
  adapter: args.adapter,
@@ -6387,7 +7671,7 @@ async function handleInteractivePayload(args) {
6387
7671
  await withSpan(
6388
7672
  "chat.app_home_disconnect",
6389
7673
  "chat.app_home_disconnect",
6390
- { slackUserId: userId },
7674
+ { userId },
6391
7675
  async () => {
6392
7676
  try {
6393
7677
  await unlinkProvider(userId, provider, args.userTokenStore);
@@ -6395,7 +7679,7 @@ async function handleInteractivePayload(args) {
6395
7679
  logException(
6396
7680
  error,
6397
7681
  "app_home_disconnect_unlink_failed",
6398
- { slackUserId: userId },
7682
+ { userId },
6399
7683
  { "app.credential.provider": provider }
6400
7684
  );
6401
7685
  }
@@ -6405,7 +7689,7 @@ async function handleInteractivePayload(args) {
6405
7689
  logException(
6406
7690
  error,
6407
7691
  "app_home_disconnect_publish_failed",
6408
- { slackUserId: userId },
7692
+ { userId },
6409
7693
  { "app.credential.provider": provider }
6410
7694
  );
6411
7695
  }
@@ -6448,7 +7732,7 @@ async function handleSlackForm(args) {
6448
7732
  })
6449
7733
  ).catch((error) => {
6450
7734
  logException(error, "slash_command_failed", {
6451
- slackUserId: params.get("user_id") ?? void 0
7735
+ userId: params.get("user_id") ?? void 0
6452
7736
  });
6453
7737
  })
6454
7738
  );
@@ -6478,7 +7762,7 @@ async function handleSlackForm(args) {
6478
7762
  })
6479
7763
  ).catch((error) => {
6480
7764
  logException(error, "slack_interactive_payload_failed", {
6481
- slackUserId: payload.user?.id?.trim() || void 0
7765
+ userId: payload.user?.id?.trim() || void 0
6482
7766
  });
6483
7767
  })
6484
7768
  );
@@ -6628,7 +7912,7 @@ function isConversationQueueMessageRejectedError(error) {
6628
7912
  // src/chat/task-execution/worker.ts
6629
7913
  var CONVERSATION_WORK_DEFER_DELAY_MS = 15e3;
6630
7914
  var CONVERSATION_WORK_SOFT_YIELD_AFTER_MS = 24e4;
6631
- function now2(options) {
7915
+ function now3(options) {
6632
7916
  return options.nowMs?.() ?? Date.now();
6633
7917
  }
6634
7918
  function nudgeIdempotencyKey(reason, conversationId, nowMs) {
@@ -6699,7 +7983,7 @@ function isTerminalFailure(failure) {
6699
7983
  }
6700
7984
  function startLeaseCheckIn(args) {
6701
7985
  const timer = setInterval(() => {
6702
- const nowMs = now2(args.options);
7986
+ const nowMs = now3(args.options);
6703
7987
  void checkInConversationWork2({
6704
7988
  conversationId: args.conversationId,
6705
7989
  leaseToken: args.leaseToken,
@@ -6755,7 +8039,7 @@ async function processConversationWork(message, options) {
6755
8039
  await clearConsumedConversationWake2({
6756
8040
  conversationId,
6757
8041
  conversationStore: options.conversationStore,
6758
- nowMs: now2(options),
8042
+ nowMs: now3(options),
6759
8043
  state: options.state
6760
8044
  });
6761
8045
  }
@@ -6772,20 +8056,20 @@ async function processConversationWork(message, options) {
6772
8056
  const lease = await startConversationWork2({
6773
8057
  conversationId,
6774
8058
  conversationStore: options.conversationStore,
6775
- nowMs: now2(options),
8059
+ nowMs: now3(options),
6776
8060
  state: options.state
6777
8061
  });
6778
8062
  if (lease.status === "no_work") {
6779
8063
  await clearConsumedConversationWake2({
6780
8064
  conversationId,
6781
8065
  conversationStore: options.conversationStore,
6782
- nowMs: now2(options),
8066
+ nowMs: now3(options),
6783
8067
  state: options.state
6784
8068
  });
6785
8069
  return { status: "no_work" };
6786
8070
  }
6787
8071
  if (lease.status === "active") {
6788
- const nudgeNowMs = now2(options);
8072
+ const nudgeNowMs = now3(options);
6789
8073
  await ensureConversationWake({
6790
8074
  conversationId,
6791
8075
  conversationStore: options.conversationStore,
@@ -6806,7 +8090,7 @@ async function processConversationWork(message, options) {
6806
8090
  );
6807
8091
  return { status: "active" };
6808
8092
  }
6809
- const startedAtMs = now2(options);
8093
+ const startedAtMs = now3(options);
6810
8094
  const softYieldDeadlineMs = startedAtMs + (options.softYieldAfterMs ?? CONVERSATION_WORK_SOFT_YIELD_AFTER_MS);
6811
8095
  const leasedWork = await getConversationWorkState2({
6812
8096
  conversationId,
@@ -6840,7 +8124,7 @@ async function processConversationWork(message, options) {
6840
8124
  leaseToken: lease.leaseToken,
6841
8125
  conversationStore: options.conversationStore,
6842
8126
  handle,
6843
- nowMs: now2(options),
8127
+ nowMs: now3(options),
6844
8128
  state: options.state
6845
8129
  });
6846
8130
  const ack = async () => {
@@ -6849,7 +8133,7 @@ async function processConversationWork(message, options) {
6849
8133
  inboundMessageIds: attemptMessageIds,
6850
8134
  leaseToken: lease.leaseToken,
6851
8135
  conversationStore: options.conversationStore,
6852
- nowMs: now2(options),
8136
+ nowMs: now3(options),
6853
8137
  state: options.state
6854
8138
  });
6855
8139
  if (!acknowledged) {
@@ -6872,13 +8156,13 @@ async function processConversationWork(message, options) {
6872
8156
  },
6873
8157
  conversationId,
6874
8158
  destination,
6875
- shouldYield: () => leaseLost || now2(options) >= softYieldDeadlineMs,
8159
+ shouldYield: () => leaseLost || now3(options) >= softYieldDeadlineMs,
6876
8160
  checkIn: async () => {
6877
8161
  const checkedIn = await checkInConversationWork2({
6878
8162
  conversationId,
6879
8163
  leaseToken: lease.leaseToken,
6880
8164
  conversationStore: options.conversationStore,
6881
- nowMs: now2(options),
8165
+ nowMs: now3(options),
6882
8166
  state: options.state
6883
8167
  });
6884
8168
  if (!checkedIn) {
@@ -6894,7 +8178,7 @@ async function processConversationWork(message, options) {
6894
8178
  conversationId,
6895
8179
  destination,
6896
8180
  leaseToken: lease.leaseToken,
6897
- nowMs: now2(options),
8181
+ nowMs: now3(options),
6898
8182
  options
6899
8183
  });
6900
8184
  return { status: "lost_lease" };
@@ -6904,13 +8188,13 @@ async function processConversationWork(message, options) {
6904
8188
  conversationId,
6905
8189
  destination,
6906
8190
  leaseToken: lease.leaseToken,
6907
- nowMs: now2(options),
8191
+ nowMs: now3(options),
6908
8192
  options
6909
8193
  });
6910
8194
  return { status: "lost_lease" };
6911
8195
  }
6912
8196
  if (result.status === "yielded") {
6913
- const yieldNowMs = now2(options);
8197
+ const yieldNowMs = now3(options);
6914
8198
  const continuationMarked = await requestConversationContinuation2({
6915
8199
  conversationId,
6916
8200
  destination,
@@ -6945,7 +8229,7 @@ async function processConversationWork(message, options) {
6945
8229
  "conversation_work_cooperative_yield",
6946
8230
  { conversationId },
6947
8231
  {
6948
- "app.worker.elapsed_ms": now2(options) - startedAtMs,
8232
+ "app.worker.elapsed_ms": now3(options) - startedAtMs,
6949
8233
  "app.worker.soft_yield_deadline_ms": softYieldDeadlineMs
6950
8234
  },
6951
8235
  "Conversation work yielded cooperatively"
@@ -6953,7 +8237,7 @@ async function processConversationWork(message, options) {
6953
8237
  return { status: "yielded" };
6954
8238
  }
6955
8239
  if (result.status === "deferred") {
6956
- const deferredNowMs = now2(options);
8240
+ const deferredNowMs = now3(options);
6957
8241
  const released = await releaseConversationWork2({
6958
8242
  conversationId,
6959
8243
  leaseToken: lease.leaseToken,
@@ -6982,7 +8266,7 @@ async function processConversationWork(message, options) {
6982
8266
  const failure = await recordFailedDeliveryAttempt({
6983
8267
  conversationId,
6984
8268
  leaseToken: lease.leaseToken,
6985
- nowMs: now2(options),
8269
+ nowMs: now3(options),
6986
8270
  messageIds: attemptMessageIds,
6987
8271
  options
6988
8272
  });
@@ -6991,7 +8275,7 @@ async function processConversationWork(message, options) {
6991
8275
  conversationId,
6992
8276
  leaseToken: lease.leaseToken,
6993
8277
  conversationStore: options.conversationStore,
6994
- nowMs: now2(options),
8278
+ nowMs: now3(options),
6995
8279
  state: options.state
6996
8280
  });
6997
8281
  return { status: "failed" };
@@ -7001,14 +8285,14 @@ async function processConversationWork(message, options) {
7001
8285
  conversationId,
7002
8286
  leaseToken: lease.leaseToken,
7003
8287
  conversationStore: options.conversationStore,
7004
- nowMs: now2(options),
8288
+ nowMs: now3(options),
7005
8289
  state: options.state
7006
8290
  });
7007
8291
  if (completion === "lost_lease") {
7008
8292
  return { status: "lost_lease" };
7009
8293
  }
7010
8294
  if (completion === "pending") {
7011
- const nudgeNowMs = now2(options);
8295
+ const nudgeNowMs = now3(options);
7012
8296
  const wake = await ensureConversationWake({
7013
8297
  conversationId,
7014
8298
  conversationStore: options.conversationStore,
@@ -7027,13 +8311,13 @@ async function processConversationWork(message, options) {
7027
8311
  "conversation_work_completed",
7028
8312
  { conversationId },
7029
8313
  {
7030
- "app.worker.elapsed_ms": now2(options) - startedAtMs
8314
+ "app.worker.elapsed_ms": now3(options) - startedAtMs
7031
8315
  },
7032
8316
  "Conversation work completed"
7033
8317
  );
7034
8318
  return { status: "completed" };
7035
8319
  } catch (error) {
7036
- const errorNowMs = now2(options);
8320
+ const errorNowMs = now3(options);
7037
8321
  let recoveryRecorded = false;
7038
8322
  try {
7039
8323
  const failure = attemptMessageIds.length > 0 ? await recordFailedDeliveryAttempt({
@@ -7098,7 +8382,7 @@ async function processConversationWork(message, options) {
7098
8382
  "conversation_work_failed",
7099
8383
  { conversationId },
7100
8384
  {
7101
- "app.worker.elapsed_ms": now2(options) - startedAtMs
8385
+ "app.worker.elapsed_ms": now3(options) - startedAtMs
7102
8386
  },
7103
8387
  "Conversation work failed"
7104
8388
  );
@@ -7279,14 +8563,14 @@ function registerVercelPluginTaskDevConsumer() {
7279
8563
  }
7280
8564
 
7281
8565
  // src/chat/services/subscribed-decision.ts
7282
- import { z as z3 } from "zod";
7283
- var replyDecisionSchema = z3.object({
7284
- should_reply: z3.boolean().describe("Whether Junior should respond to this thread message."),
7285
- should_unsubscribe: z3.boolean().describe(
8566
+ import { z as z4 } from "zod";
8567
+ var replyDecisionSchema = z4.object({
8568
+ should_reply: z4.boolean().describe("Whether Junior should respond to this thread message."),
8569
+ should_unsubscribe: z4.boolean().describe(
7286
8570
  "Whether Junior should unsubscribe from this thread because the user clearly asked it to stop participating."
7287
8571
  ),
7288
- confidence: z3.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
7289
- reason: z3.string().describe("Short reason for the decision; use an empty string if none.")
8572
+ confidence: z4.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
8573
+ reason: z4.string().describe("Short reason for the decision; use an empty string if none.")
7290
8574
  }).strict();
7291
8575
  var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
7292
8576
  var ROUTER_CLASSIFIER_MAX_TOKENS = 240;
@@ -7632,15 +8916,6 @@ var THREAD_OPTOUT_ACK = "Understood. I'll stay out of this thread unless someone
7632
8916
  function shouldRethrowTurnControlError(error) {
7633
8917
  return isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error) || isTurnInputDeferredError(error) || isProviderRetryError(error);
7634
8918
  }
7635
- async function maybeHandleThreadOptOutDecision(args) {
7636
- if (!args.decision?.shouldUnsubscribe) {
7637
- return false;
7638
- }
7639
- await args.thread.unsubscribe();
7640
- await args.beforeFirstResponsePost?.();
7641
- await args.thread.post(THREAD_OPTOUT_ACK);
7642
- return true;
7643
- }
7644
8919
  function getQueuedMessages(context, options) {
7645
8920
  return (context?.skipped ?? []).map((message) => {
7646
8921
  const stripped = options.stripLeadingBotMention(message.text, {
@@ -7687,10 +8962,10 @@ function createAcceptedSteeringDrain(hooks, options) {
7687
8962
  function buildLogContext(deps, args) {
7688
8963
  return {
7689
8964
  conversationId: args.threadId ?? args.runId,
7690
- slackThreadId: args.threadId,
7691
- slackUserId: args.actorId,
7692
- slackUserName: args.actorUserName,
7693
- slackChannelId: args.channelId,
8965
+ messageConversationId: args.threadId,
8966
+ userId: args.actorId,
8967
+ userName: args.actorUserName,
8968
+ destinationName: args.channelId,
7694
8969
  runId: args.runId,
7695
8970
  assistantUserName: deps.assistantUserName,
7696
8971
  modelId: deps.modelId
@@ -7705,6 +8980,16 @@ function isResourceEventNotificationMessage2(message) {
7705
8980
  }
7706
8981
  function createSlackTurnRuntime(deps) {
7707
8982
  const logContext = (args) => buildLogContext(deps, args);
8983
+ const maybeHandleThreadOptOutDecision = async (args) => {
8984
+ if (!args.decision?.shouldUnsubscribe) {
8985
+ return false;
8986
+ }
8987
+ await deps.cancelEventSubscriptions({ conversationId: args.thread.id });
8988
+ await args.thread.unsubscribe();
8989
+ await args.beforeFirstResponsePost?.();
8990
+ await args.thread.post(THREAD_OPTOUT_ACK);
8991
+ return true;
8992
+ };
7708
8993
  const failConversationTurn = async (args) => {
7709
8994
  const conversationId = deps.getThreadId(args.thread, args.message) ?? deps.getRunId(args.thread, args.message);
7710
8995
  if (!conversationId) {
@@ -8399,9 +9684,9 @@ function createSlackTurnRuntime(deps) {
8399
9684
  error,
8400
9685
  "assistant_thread_started_handler_failed",
8401
9686
  {
8402
- slackThreadId: event.threadId,
8403
- slackUserId: event.userId,
8404
- slackChannelId: event.channelId,
9687
+ messageConversationId: event.threadId,
9688
+ userId: event.userId,
9689
+ destinationName: event.channelId,
8405
9690
  assistantUserName: deps.assistantUserName,
8406
9691
  modelId: deps.modelId
8407
9692
  },
@@ -8423,9 +9708,9 @@ function createSlackTurnRuntime(deps) {
8423
9708
  error,
8424
9709
  "assistant_context_changed_handler_failed",
8425
9710
  {
8426
- slackThreadId: event.threadId,
8427
- slackUserId: event.userId,
8428
- slackChannelId: event.channelId,
9711
+ messageConversationId: event.threadId,
9712
+ userId: event.userId,
9713
+ destinationName: event.channelId,
8429
9714
  assistantUserName: deps.assistantUserName,
8430
9715
  modelId: deps.modelId
8431
9716
  },
@@ -8449,9 +9734,9 @@ function createSubscribedReplyPolicy(deps) {
8449
9734
  logWarn(
8450
9735
  "subscribed_message_classifier_failed",
8451
9736
  {
8452
- slackThreadId: input.context.threadId,
8453
- slackUserId: input.context.actorId,
8454
- slackChannelId: input.context.channelId,
9737
+ messageConversationId: input.context.threadId,
9738
+ userId: input.context.actorId,
9739
+ destinationName: input.context.channelId,
8455
9740
  runId: input.context.runId,
8456
9741
  assistantUserName: botConfig.userName,
8457
9742
  modelId: botConfig.fastModelId
@@ -8660,9 +9945,9 @@ async function resolveUserAttachmentsWithDeps(attachments, context, deps) {
8660
9945
  logWarn(
8661
9946
  "attachment_skipped_size_limit",
8662
9947
  {
8663
- slackThreadId: context.threadId,
8664
- slackUserId: context.actorId,
8665
- slackChannelId: context.channelId,
9948
+ messageConversationId: context.threadId,
9949
+ userId: context.actorId,
9950
+ destinationName: context.channelId,
8666
9951
  runId: context.runId,
8667
9952
  assistantUserName: botConfig.userName,
8668
9953
  modelId: standardModelId(botConfig)
@@ -8685,9 +9970,9 @@ async function resolveUserAttachmentsWithDeps(attachments, context, deps) {
8685
9970
  logWarn(
8686
9971
  "image_attachment_processing_failed",
8687
9972
  {
8688
- slackThreadId: context.threadId,
8689
- slackUserId: context.actorId,
8690
- slackChannelId: context.channelId,
9973
+ messageConversationId: context.threadId,
9974
+ userId: context.actorId,
9975
+ destinationName: context.channelId,
8691
9976
  runId: context.runId,
8692
9977
  assistantUserName: botConfig.userName,
8693
9978
  modelId: botConfig.visionModelId ?? standardModelId(botConfig)
@@ -8704,9 +9989,9 @@ async function resolveUserAttachmentsWithDeps(attachments, context, deps) {
8704
9989
  logWarn(
8705
9990
  "attachment_resolution_failed",
8706
9991
  {
8707
- slackThreadId: context.threadId,
8708
- slackUserId: context.actorId,
8709
- slackChannelId: context.channelId,
9992
+ messageConversationId: context.threadId,
9993
+ userId: context.actorId,
9994
+ destinationName: context.channelId,
8710
9995
  runId: context.runId,
8711
9996
  assistantUserName: botConfig.userName,
8712
9997
  modelId: standardModelId(botConfig)
@@ -8754,9 +10039,9 @@ async function summarizeConversationImage(args, deps) {
8754
10039
  logWarn(
8755
10040
  "conversation_image_vision_failed",
8756
10041
  {
8757
- slackThreadId: args.context.threadId,
8758
- slackUserId: args.context.actorId,
8759
- slackChannelId: args.context.channelId,
10042
+ messageConversationId: args.context.threadId,
10043
+ userId: args.context.actorId,
10044
+ destinationName: args.context.channelId,
8760
10045
  runId: args.context.runId,
8761
10046
  assistantUserName: botConfig.userName,
8762
10047
  modelId: visionModelId
@@ -8810,9 +10095,9 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
8810
10095
  logWarn(
8811
10096
  "conversation_image_replies_fetch_failed",
8812
10097
  {
8813
- slackThreadId: context.threadId,
8814
- slackUserId: context.actorId,
8815
- slackChannelId: context.channelId,
10098
+ messageConversationId: context.threadId,
10099
+ userId: context.actorId,
10100
+ destinationName: context.channelId,
8816
10101
  runId: context.runId,
8817
10102
  assistantUserName: botConfig.userName,
8818
10103
  modelId: standardModelId(botConfig)
@@ -8877,9 +10162,9 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
8877
10162
  logWarn(
8878
10163
  "conversation_image_skipped_size_limit",
8879
10164
  {
8880
- slackThreadId: context.threadId,
8881
- slackUserId: context.actorId,
8882
- slackChannelId: context.channelId,
10165
+ messageConversationId: context.threadId,
10166
+ userId: context.actorId,
10167
+ destinationName: context.channelId,
8883
10168
  runId: context.runId,
8884
10169
  assistantUserName: botConfig.userName,
8885
10170
  modelId: standardModelId(botConfig)
@@ -8904,9 +10189,9 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
8904
10189
  logWarn(
8905
10190
  "conversation_image_download_failed",
8906
10191
  {
8907
- slackThreadId: context.threadId,
8908
- slackUserId: context.actorId,
8909
- slackChannelId: context.channelId,
10192
+ messageConversationId: context.threadId,
10193
+ userId: context.actorId,
10194
+ destinationName: context.channelId,
8910
10195
  runId: context.runId,
8911
10196
  assistantUserName: botConfig.userName,
8912
10197
  modelId: standardModelId(botConfig)
@@ -8924,9 +10209,9 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
8924
10209
  logWarn(
8925
10210
  "conversation_image_skipped_size_limit",
8926
10211
  {
8927
- slackThreadId: context.threadId,
8928
- slackUserId: context.actorId,
8929
- slackChannelId: context.channelId,
10212
+ messageConversationId: context.threadId,
10213
+ userId: context.actorId,
10214
+ destinationName: context.channelId,
8930
10215
  runId: context.runId,
8931
10216
  assistantUserName: botConfig.userName,
8932
10217
  modelId: standardModelId(botConfig)
@@ -8967,9 +10252,9 @@ async function hydrateConversationVisionContextWithDeps(conversation, context, d
8967
10252
  logInfo(
8968
10253
  "conversation_image_context_hydrated",
8969
10254
  {
8970
- slackThreadId: context.threadId,
8971
- slackUserId: context.actorId,
8972
- slackChannelId: context.channelId,
10255
+ messageConversationId: context.threadId,
10256
+ userId: context.actorId,
10257
+ destinationName: context.channelId,
8973
10258
  runId: context.runId,
8974
10259
  assistantUserName: botConfig.userName,
8975
10260
  modelId: standardModelId(botConfig)
@@ -9052,11 +10337,11 @@ function getSlackMessageTs(message) {
9052
10337
  }
9053
10338
 
9054
10339
  // src/chat/slack/action-token.ts
9055
- import { z as z4 } from "zod";
9056
- var slackActionTokenSchema = z4.string().trim().min(1).brand();
9057
- var slackMessageEnvelopeSchema = z4.object({
9058
- raw: z4.object({
9059
- action_token: z4.unknown().optional()
10340
+ import { z as z5 } from "zod";
10341
+ var slackActionTokenSchema = z5.string().trim().min(1).brand();
10342
+ var slackMessageEnvelopeSchema = z5.object({
10343
+ raw: z5.object({
10344
+ action_token: z5.unknown().optional()
9060
10345
  }).optional()
9061
10346
  });
9062
10347
  function readSlackActionToken(message) {
@@ -9092,9 +10377,9 @@ function maybeUpdateAssistantTitle(args) {
9092
10377
  logWarn(
9093
10378
  "thread_title_generation_failed",
9094
10379
  {
9095
- slackThreadId: args.threadId,
9096
- slackUserId: args.actorId,
9097
- slackChannelId: args.channelId,
10380
+ messageConversationId: args.threadId,
10381
+ userId: args.actorId,
10382
+ destinationName: args.channelId,
9098
10383
  runId: args.runId,
9099
10384
  assistantUserName: args.assistantUserName,
9100
10385
  modelId: args.modelId
@@ -9126,9 +10411,9 @@ function maybeUpdateAssistantTitle(args) {
9126
10411
  logError(
9127
10412
  "thread_title_generation_permission_denied",
9128
10413
  {
9129
- slackThreadId: args.threadId,
9130
- slackUserId: args.actorId,
9131
- slackChannelId: args.channelId,
10414
+ messageConversationId: args.threadId,
10415
+ userId: args.actorId,
10416
+ destinationName: args.channelId,
9132
10417
  runId: args.runId,
9133
10418
  assistantUserName: args.assistantUserName,
9134
10419
  modelId: args.modelId
@@ -9140,9 +10425,9 @@ function maybeUpdateAssistantTitle(args) {
9140
10425
  logWarn(
9141
10426
  "thread_title_slack_update_failed",
9142
10427
  {
9143
- slackThreadId: args.threadId,
9144
- slackUserId: args.actorId,
9145
- slackChannelId: args.channelId,
10428
+ messageConversationId: args.threadId,
10429
+ userId: args.actorId,
10430
+ destinationName: args.channelId,
9146
10431
  runId: args.runId,
9147
10432
  assistantUserName: args.assistantUserName,
9148
10433
  modelId: args.modelId
@@ -9374,9 +10659,9 @@ function createReplyToThread(deps) {
9374
10659
  "chat.reply",
9375
10660
  {
9376
10661
  conversationId,
9377
- slackThreadId: threadId,
9378
- slackUserId: message.author.userId,
9379
- slackChannelId: channelId,
10662
+ messageConversationId: threadId,
10663
+ userId: message.author.userId,
10664
+ destinationName: channelId,
9380
10665
  runId,
9381
10666
  assistantUserName: botConfig.userName,
9382
10667
  modelId: standardModelId(botConfig)
@@ -9432,9 +10717,9 @@ function createReplyToThread(deps) {
9432
10717
  const turnId = buildDeterministicTurnId(message.id);
9433
10718
  const turnTraceContext = {
9434
10719
  conversationId,
9435
- slackThreadId: threadId,
9436
- slackUserId: message.author.userId,
9437
- slackChannelId: channelId,
10720
+ messageConversationId: threadId,
10721
+ userId: message.author.userId,
10722
+ destinationName: channelId,
9438
10723
  runId,
9439
10724
  assistantUserName: botConfig.userName,
9440
10725
  modelId: standardModelId(botConfig)
@@ -9455,23 +10740,16 @@ function createReplyToThread(deps) {
9455
10740
  actor.userId,
9456
10741
  providerDisplayName
9457
10742
  );
9458
- const footer = buildSlackReplyFooter({ conversationId });
9459
10743
  try {
10744
+ await beforeFirstResponsePost();
9460
10745
  if (channelId && threadTs) {
9461
- await postSlackApiReplyPosts({
9462
- beforePost: beforeFirstResponsePost,
10746
+ await sendSlackReply({
9463
10747
  channelId,
9464
- threadTs,
9465
- posts: [
9466
- {
9467
- text,
9468
- stage: "thread_reply"
9469
- }
9470
- ],
9471
- footer
10748
+ conversationId,
10749
+ text,
10750
+ threadTs
9472
10751
  });
9473
10752
  } else {
9474
- await beforeFirstResponsePost();
9475
10753
  await thread.post(buildSlackOutputMessage(text));
9476
10754
  }
9477
10755
  } catch (error) {
@@ -9767,7 +11045,7 @@ function createReplyToThread(deps) {
9767
11045
  });
9768
11046
  }
9769
11047
  if (actor?.userName) {
9770
- setTags({ slackUserName: actor.userName });
11048
+ setTags({ userName: actor.userName });
9771
11049
  }
9772
11050
  const turnAttachments = collectTurnAttachments(
9773
11051
  message,
@@ -9793,32 +11071,6 @@ function createReplyToThread(deps) {
9793
11071
  const compactingStatus = {
9794
11072
  text: "Compacting context"
9795
11073
  };
9796
- const postThreadReply = async (payload, stage) => {
9797
- await beforeFirstResponsePost();
9798
- try {
9799
- return await thread.post(payload);
9800
- } catch (error) {
9801
- if (isRetryableSlackPostError(error)) {
9802
- throw new RetryableDeliveryError(error);
9803
- }
9804
- const eventId = logException(
9805
- error,
9806
- "slack_thread_post_failed",
9807
- turnTraceContext,
9808
- {
9809
- "app.slack.reply_stage": stage,
9810
- ...messageTs ? { "messaging.message.id": messageTs } : {},
9811
- ...getSlackErrorObservabilityAttributes(error)
9812
- },
9813
- "Failed to post Slack thread reply"
9814
- );
9815
- throw new ConversationTurnBoundaryError({
9816
- cause: error,
9817
- ...eventId ? { eventId } : {},
9818
- failureCode: "delivery_failed"
9819
- });
9820
- }
9821
- };
9822
11074
  let persistedAtLeastOnce = false;
9823
11075
  let shouldPersistFailureState = true;
9824
11076
  let runResultHandled = false;
@@ -9838,18 +11090,45 @@ function createReplyToThread(deps) {
9838
11090
  turnCompletionNotified = true;
9839
11091
  };
9840
11092
  const deliverAssistantMessage = async (assistantMessage) => {
9841
- const posts = planSlackAssistantMessagePosts(assistantMessage.text);
9842
- if (posts.length === 0) {
11093
+ if (!assistantMessage.text.trim()) {
9843
11094
  return;
9844
11095
  }
9845
11096
  boundaryFailureCode = "delivery_failed";
9846
11097
  let slackTs;
9847
- for (const post of posts) {
9848
- const sentMessage = await postThreadReply(
9849
- buildSlackOutputMessage(post.text),
9850
- post.stage
11098
+ await beforeFirstResponsePost();
11099
+ try {
11100
+ if (channelId && threadTs && thread.adapter.name === "slack") {
11101
+ slackTs = await sendSlackReply({
11102
+ channelId,
11103
+ conversationId,
11104
+ text: assistantMessage.text,
11105
+ threadTs
11106
+ });
11107
+ } else {
11108
+ for (const text of splitSlackReplyText(assistantMessage.text)) {
11109
+ slackTs = (await thread.post(buildSlackOutputMessage(text))).id;
11110
+ }
11111
+ }
11112
+ } catch (error) {
11113
+ if (isRetryableSlackPostError(error)) {
11114
+ throw new RetryableDeliveryError(error);
11115
+ }
11116
+ const eventId = logException(
11117
+ error,
11118
+ "slack_thread_post_failed",
11119
+ turnTraceContext,
11120
+ {
11121
+ "app.slack.reply_stage": "thread_reply",
11122
+ ...messageTs ? { "messaging.message.id": messageTs } : {},
11123
+ ...getSlackErrorObservabilityAttributes(error)
11124
+ },
11125
+ "Failed to post Slack thread reply"
9851
11126
  );
9852
- slackTs = sentMessage.id;
11127
+ throw new ConversationTurnBoundaryError({
11128
+ cause: error,
11129
+ ...eventId ? { eventId } : {},
11130
+ failureCode: "delivery_failed"
11131
+ });
9853
11132
  }
9854
11133
  assistantMessageDelivered = true;
9855
11134
  const recordedMessageId = recordDeliveredAssistantMessage({
@@ -10181,9 +11460,9 @@ function createReplyToThread(deps) {
10181
11460
  }
10182
11461
  let reply = outcome.result;
10183
11462
  const diagnosticsContext = {
10184
- slackThreadId: threadId,
10185
- slackUserId: message.author.userId,
10186
- slackChannelId: channelId,
11463
+ messageConversationId: threadId,
11464
+ userId: message.author.userId,
11465
+ destinationName: channelId,
10187
11466
  runId,
10188
11467
  assistantUserName: botConfig.userName,
10189
11468
  modelId: reply.diagnostics.modelId
@@ -10233,13 +11512,6 @@ function createReplyToThread(deps) {
10233
11512
  sliceId: 1,
10234
11513
  messages: reply.piMessages,
10235
11514
  modelId: reply.diagnostics.modelId,
10236
- logContext: {
10237
- threadId,
10238
- actorId: slackActorId,
10239
- channelId,
10240
- runId,
10241
- assistantUserName: botConfig.userName
10242
- },
10243
11515
  actor: executionActor,
10244
11516
  surface: "slack"
10245
11517
  });
@@ -10526,6 +11798,11 @@ async function refreshAssistantThreadContext(event) {
10526
11798
 
10527
11799
  // src/chat/runtime/conversation-message.ts
10528
11800
  var NON_TEXT_MESSAGE_TEXT = "[non-text message]";
11801
+ function resourceEventType(entry) {
11802
+ if (!entry.raw || typeof entry.raw !== "object") return void 0;
11803
+ const raw = entry.raw;
11804
+ return raw.event_type === "resource_event" && typeof raw.resource_event_type === "string" ? raw.resource_event_type : void 0;
11805
+ }
10529
11806
  function resolveMessageText(args) {
10530
11807
  const text = normalizeConversationText(args.text);
10531
11808
  return text || NON_TEXT_MESSAGE_TEXT;
@@ -10550,6 +11827,7 @@ function toConversationMessage(args) {
10550
11827
  },
10551
11828
  meta: {
10552
11829
  attachmentCount: args.entry.attachments.length,
11830
+ eventType: resourceEventType(args.entry),
10553
11831
  explicitMention: args.explicitMention,
10554
11832
  imageAttachmentCount: imageAttachmentCount > 0 ? imageAttachmentCount : void 0,
10555
11833
  imagesHydrated: !messageHasPotentialImageAttachment,
@@ -10766,6 +12044,7 @@ function createSlackRuntime(options) {
10766
12044
  });
10767
12045
  return createSlackTurnRuntime({
10768
12046
  assistantUserName: botConfig.userName,
12047
+ cancelEventSubscriptions: cancelSubscriptions,
10769
12048
  modelId: standardModelId(botConfig),
10770
12049
  now: options.now ?? (() => Date.now()),
10771
12050
  getThreadId,
@@ -11155,7 +12434,7 @@ async function continueSlackAgentRun(payload, options) {
11155
12434
  }
11156
12435
  }
11157
12436
  },
11158
- onSuccess: async (reply) => {
12437
+ commitResult: async (reply) => {
11159
12438
  await persistCompletedReplyState2({
11160
12439
  sessionRecord: activeSessionRecord,
11161
12440
  reply
@@ -11482,6 +12761,9 @@ function renderResourceEventNotificationText(subscription, event) {
11482
12761
  "Handling:",
11483
12762
  "- This is a subscribed conversation update, not a user-authored command.",
11484
12763
  "- Use the subscription intent to decide whether this event warrants action or a visible reply. Otherwise, stay silent.",
12764
+ "- Treat the trusted summary as sufficient evidence for the facts it reports. Do not verify or expand those facts with tools.",
12765
+ "- Use tools only when the subscription intent explicitly requires missing details or an action.",
12766
+ "- When replying, state what changed and the useful next step, if any.",
11485
12767
  "",
11486
12768
  "Subscription:",
11487
12769
  `- resource: ${subscription.label}`,
@@ -11988,6 +13270,7 @@ async function createApp(options) {
11988
13270
  return app;
11989
13271
  }
11990
13272
  export {
13273
+ JUNIOR_VERSION,
11991
13274
  createApp,
11992
13275
  defineJuniorPlugins
11993
13276
  };