@sentry/junior 0.115.0 → 0.116.1

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 (111) hide show
  1. package/dist/{agent-hooks-4EUUXP2F.js → agent-hooks-GUAAS3XH.js} +9 -9
  2. package/dist/api/conversations/detail.d.ts +18 -0
  3. package/dist/api/conversations/event-page.d.ts +1 -1
  4. package/dist/api/conversations/events.d.ts +1 -1
  5. package/dist/api/people/profile.d.ts +2 -2
  6. package/dist/api/people/shared.d.ts +1 -1
  7. package/dist/api/personal-tokens/create.d.ts +11 -0
  8. package/dist/api/personal-tokens/list.d.ts +12 -0
  9. package/dist/api/personal-tokens/revoke.d.ts +5 -0
  10. package/dist/api/personal-tokens/routes.d.ts +4 -0
  11. package/dist/api/plugins.d.ts +0 -1
  12. package/dist/api/route.d.ts +1 -1
  13. package/dist/api/schema/conversation.d.ts +18 -0
  14. package/dist/api/schema/person.d.ts +3 -3
  15. package/dist/api/schema/personal-token.d.ts +38 -0
  16. package/dist/api/schema.d.ts +2 -0
  17. package/dist/api/schema.js +15 -3
  18. package/dist/api.d.ts +1 -0
  19. package/dist/api.js +179 -39
  20. package/dist/app.js +1035 -853
  21. package/dist/{catalog-runtime-Y32UEQLB.js → catalog-runtime-WMPC4J4U.js} +5 -5
  22. package/dist/chat/agent/request.d.ts +1 -0
  23. package/dist/chat/agent/resume.d.ts +1 -0
  24. package/dist/chat/agent/sandbox.d.ts +1 -0
  25. package/dist/chat/agent/tools.d.ts +1 -0
  26. package/dist/chat/agent-dispatch/context.d.ts +2 -0
  27. package/dist/chat/agent-dispatch/heartbeat.d.ts +11 -5
  28. package/dist/chat/agent-dispatch/signing.d.ts +14 -5
  29. package/dist/chat/agent-dispatch/store.d.ts +36 -15
  30. package/dist/chat/agent-dispatch/types.d.ts +23 -10
  31. package/dist/chat/agent-dispatch/work.d.ts +58 -0
  32. package/dist/chat/app/factory.d.ts +12 -3
  33. package/dist/chat/conversations/history.d.ts +285 -290
  34. package/dist/chat/conversations/projection.d.ts +2 -2
  35. package/dist/chat/mcp/tool-manager.d.ts +2 -1
  36. package/dist/chat/pi/conversation-events.d.ts +6 -2
  37. package/dist/chat/plugins/annotations.d.ts +8 -0
  38. package/dist/chat/plugins/registry.d.ts +0 -3
  39. package/dist/chat/plugins/types.d.ts +0 -2
  40. package/dist/chat/runtime/agent-continue-runner.d.ts +4 -0
  41. package/dist/chat/runtime/reply-executor.d.ts +6 -1
  42. package/dist/chat/runtime/slack-resume.d.ts +5 -1
  43. package/dist/chat/runtime/turn-input.d.ts +3 -0
  44. package/dist/chat/sandbox/sandbox.d.ts +2 -1
  45. package/dist/chat/sandbox/session.d.ts +2 -11
  46. package/dist/chat/sandbox/workspace.d.ts +10 -2
  47. package/dist/chat/services/turn-session-record.d.ts +14 -1
  48. package/dist/chat/slack/dispatch-turn.d.ts +25 -0
  49. package/dist/chat/state/turn-session.d.ts +11 -0
  50. package/dist/chat/tool-support/text-range-result.d.ts +3 -0
  51. package/dist/chat/tool-support/zod-tool.d.ts +1 -0
  52. package/dist/chat/tools/definition.d.ts +3 -10
  53. package/dist/chat/tools/query-conversation-events.d.ts +68 -0
  54. package/dist/chat/tools/sandbox/file-utils.d.ts +8 -1
  55. package/dist/chat/tools/sandbox/find-files.d.ts +2 -1
  56. package/dist/chat/tools/sandbox/grep.d.ts +2 -1
  57. package/dist/{chunk-6V762AB4.js → chunk-2YYPXQNQ.js} +1 -1
  58. package/dist/{chunk-MIAKQNYX.js → chunk-6ZK6FZIV.js} +1 -1
  59. package/dist/{chunk-67CVNDJ7.js → chunk-7CSVG54J.js} +1 -1
  60. package/dist/{chunk-HWCQRGLL.js → chunk-BFYVIOTS.js} +105 -37
  61. package/dist/{chunk-LYA2AB5G.js → chunk-CGXLCMWI.js} +8 -52
  62. package/dist/{chunk-SYJT7AKB.js → chunk-CY7NXM32.js} +1 -1
  63. package/dist/{chunk-2QG4MZQ7.js → chunk-DLLW5ITL.js} +2 -2
  64. package/dist/{chunk-NVUQSFWX.js → chunk-F3SWGSPG.js} +3 -15
  65. package/dist/{chunk-CWEZEPD4.js → chunk-FAEALXEH.js} +68 -2
  66. package/dist/{chunk-43RYMWJ2.js → chunk-GDAXWEIJ.js} +809 -213
  67. package/dist/{chunk-AGLQYELI.js → chunk-H35BDAUS.js} +170 -170
  68. package/dist/{chunk-YCLO7D7M.js → chunk-JPKGW6L6.js} +77 -26
  69. package/dist/{chunk-G5SX4UPR.js → chunk-KNCELO2M.js} +2 -2
  70. package/dist/{chunk-FHC4JUSF.js → chunk-N3C4XCRE.js} +1 -2
  71. package/dist/{chunk-6FKLPVF3.js → chunk-OY2PQQ4P.js} +1 -1
  72. package/dist/{chunk-6OPTF4BQ.js → chunk-PE44LDDQ.js} +0 -1
  73. package/dist/{chunk-JQFJO5WH.js → chunk-RXRQJF7Y.js} +74 -17
  74. package/dist/{chunk-CEYOL62C.js → chunk-RYFAVWP3.js} +120 -20
  75. package/dist/{chunk-G36V2YGB.js → chunk-TC4ZEM2D.js} +33 -1
  76. package/dist/{chunk-PVZHG2XZ.js → chunk-VBMMMVVQ.js} +112 -35
  77. package/dist/{chunk-SG5WAA7H.js → chunk-WKU7TY22.js} +0 -3
  78. package/dist/{chunk-TVE25LPW.js → chunk-WLT72MRP.js} +1 -1
  79. package/dist/{chunk-EW5MLKT5.js → chunk-XA3VLFPK.js} +1 -55
  80. package/dist/{chunk-RAVKCKXA.js → chunk-ZYHLNTC2.js} +20 -5
  81. package/dist/cli/chat.js +23 -23
  82. package/dist/cli/check.js +6 -6
  83. package/dist/cli/plugins.js +13 -13
  84. package/dist/cli/snapshot-warmup.js +8 -8
  85. package/dist/cli/upgrade.js +7 -7
  86. package/dist/db/schema/api-tokens.d.ts +160 -0
  87. package/dist/db/schema/conversation-annotations.d.ts +142 -0
  88. package/dist/db/schema.d.ts +305 -1
  89. package/dist/{db-KBSJHAZU.js → db-FH4R5ZQ4.js} +6 -6
  90. package/dist/handlers/agent-dispatch.d.ts +7 -3
  91. package/dist/instrumentation.js +2 -2
  92. package/dist/nitro.js +5 -5
  93. package/dist/{oauth-callback-server-EYRMJTCS.js → oauth-callback-server-C2SSJ3FQ.js} +18 -18
  94. package/dist/personal-tokens/store.d.ts +27 -0
  95. package/dist/{plugins-PZMDS7AT.js → plugins-IUJA4VBU.js} +1 -1
  96. package/dist/reporting-schema.d.ts +0 -2
  97. package/dist/{runner-RQGZ5Z2S.js → runner-NAALFIXM.js} +12 -12
  98. package/dist/{skills-JJQIE3BT.js → skills-UN2JXYYF.js} +6 -6
  99. package/dist/{validation-MZ47PZP5.js → validation-EEF4ROHR.js} +6 -6
  100. package/dist/version.js +1 -1
  101. package/migrations/0008_sticky_rage.sql +14 -0
  102. package/migrations/0009_slippery_harrier.sql +12 -0
  103. package/migrations/0010_native_conversation_history.sql +142 -0
  104. package/migrations/meta/0008_snapshot.json +1076 -0
  105. package/migrations/meta/0009_snapshot.json +1146 -0
  106. package/migrations/meta/0010_snapshot.json +1190 -0
  107. package/migrations/meta/_journal.json +22 -1
  108. package/package.json +5 -6
  109. package/dist/chat/agent-dispatch/runner.d.ts +0 -13
  110. package/dist/chat/capabilities/catalog.d.ts +0 -16
  111. package/dist/{chunk-KNFROR7R.js → chunk-5I6ORJUY.js} +1 -1
package/dist/app.js CHANGED
@@ -48,7 +48,7 @@ import {
48
48
  startConversationWork,
49
49
  startSlackProcessingReaction,
50
50
  stripLeadingBotMention
51
- } from "./chunk-YCLO7D7M.js";
51
+ } from "./chunk-JPKGW6L6.js";
52
52
  import "./chunk-FA6POGLH.js";
53
53
  import {
54
54
  ConversationTurnLifecycleService,
@@ -63,15 +63,15 @@ import {
63
63
  processPluginTask,
64
64
  requireTurnFailureEventId,
65
65
  scheduleSessionCompletedPluginTasks
66
- } from "./chunk-AGLQYELI.js";
66
+ } from "./chunk-H35BDAUS.js";
67
67
  import {
68
68
  GET,
69
69
  conversationVisibilityFromSlackChannelType,
70
70
  resolveSlackChannelTypeFromMessage,
71
71
  resolveSlackConversationContext
72
- } from "./chunk-G5SX4UPR.js";
72
+ } from "./chunk-KNCELO2M.js";
73
73
  import "./chunk-4CODA7P5.js";
74
- import "./chunk-6OPTF4BQ.js";
74
+ import "./chunk-PE44LDDQ.js";
75
75
  import {
76
76
  ConversationQueueMessageRejectedError,
77
77
  conversationQueueMessageSchema,
@@ -79,22 +79,22 @@ import {
79
79
  isConversationQueueMessageRejectedError,
80
80
  resolveConversationWorkQueueTopic,
81
81
  verifyConversationQueueMessage
82
- } from "./chunk-MIAKQNYX.js";
82
+ } from "./chunk-6ZK6FZIV.js";
83
83
  import {
84
84
  PLUGIN_TASK_QUEUE_TOPIC,
85
85
  createVercelQueueClient,
86
86
  verifyPluginTaskQueueMessage
87
- } from "./chunk-KNFROR7R.js";
87
+ } from "./chunk-5I6ORJUY.js";
88
88
  import {
89
89
  validatePluginEgressCredentialHooks,
90
90
  validatePluginRegistrations
91
- } from "./chunk-6V762AB4.js";
91
+ } from "./chunk-2YYPXQNQ.js";
92
92
  import {
93
93
  defineJuniorPlugins,
94
94
  pluginCatalogConfigFromEnv,
95
95
  pluginCatalogConfigFromPluginSet,
96
96
  pluginRuntimeRegistrationsFromPluginSet
97
- } from "./chunk-SG5WAA7H.js";
97
+ } from "./chunk-WKU7TY22.js";
98
98
  import {
99
99
  AuthorizationFlowDisabledError,
100
100
  PluginCredentialFailureError,
@@ -121,7 +121,7 @@ import {
121
121
  setConfigDefaults,
122
122
  shouldEmitDevAgentTrace,
123
123
  shouldPropagateSandboxEgressTrace
124
- } from "./chunk-43RYMWJ2.js";
124
+ } from "./chunk-GDAXWEIJ.js";
125
125
  import "./chunk-DIB4GX3G.js";
126
126
  import {
127
127
  RetryableDeliveryError,
@@ -132,10 +132,9 @@ import {
132
132
  deleteMcpStoredOAuthCredentials,
133
133
  formatProviderLabel,
134
134
  postSlackMessage,
135
- resolveBaseUrl,
136
135
  splitSlackReplyText,
137
136
  startOAuthFlow
138
- } from "./chunk-LYA2AB5G.js";
137
+ } from "./chunk-CGXLCMWI.js";
139
138
  import {
140
139
  ConversationTurnBoundaryError,
141
140
  CooperativeTurnYieldError,
@@ -187,12 +186,11 @@ import {
187
186
  startActiveTurn,
188
187
  stripRuntimeTurnContext,
189
188
  trimTrailingAssistantMessages,
190
- turnHasReply,
191
189
  updateConversationStats,
192
190
  upsertConversationMessage
193
- } from "./chunk-CEYOL62C.js";
191
+ } from "./chunk-RYFAVWP3.js";
194
192
  import "./chunk-HDSDCEPK.js";
195
- import "./chunk-PVZHG2XZ.js";
193
+ import "./chunk-VBMMMVVQ.js";
196
194
  import "./chunk-MU6HHZEN.js";
197
195
  import "./chunk-PDO5BLNM.js";
198
196
  import "./chunk-UIE3R5XU.js";
@@ -209,11 +207,11 @@ import {
209
207
  validatePlugins,
210
208
  verifyScheduledTaskCredentialSubject,
211
209
  verifySlackDirectCredentialSubject
212
- } from "./chunk-RAVKCKXA.js";
210
+ } from "./chunk-ZYHLNTC2.js";
213
211
  import {
214
212
  acquireActiveLock,
215
213
  getStateAdapter
216
- } from "./chunk-67CVNDJ7.js";
214
+ } from "./chunk-7CSVG54J.js";
217
215
  import {
218
216
  SlackActionError,
219
217
  downloadPrivateSlackFile,
@@ -223,11 +221,10 @@ import {
223
221
  normalizeSlackConversationId,
224
222
  runWithSlackInstallationToken,
225
223
  runWithWorkspaceTeamId
226
- } from "./chunk-CWEZEPD4.js";
224
+ } from "./chunk-FAEALXEH.js";
227
225
  import {
228
226
  contextProvenance,
229
227
  createSlackDestination,
230
- destinationKey,
231
228
  getConversationEventStore,
232
229
  getConversationStore,
233
230
  getDb,
@@ -235,11 +232,11 @@ import {
235
232
  instructionProvenanceFor,
236
233
  requireSlackDestination,
237
234
  withConversationEventLock
238
- } from "./chunk-HWCQRGLL.js";
235
+ } from "./chunk-BFYVIOTS.js";
239
236
  import {
240
237
  juniorConversationEvents,
241
238
  juniorConversations
242
- } from "./chunk-JQFJO5WH.js";
239
+ } from "./chunk-RXRQJF7Y.js";
243
240
  import {
244
241
  sleep
245
242
  } from "./chunk-4ZNGQH7C.js";
@@ -260,18 +257,18 @@ import {
260
257
  modelIdForProfile,
261
258
  setSlackReactionConfig,
262
259
  standardModelId
263
- } from "./chunk-SYJT7AKB.js";
260
+ } from "./chunk-CY7NXM32.js";
264
261
  import {
265
262
  juniorDestinations
266
263
  } from "./chunk-VFUK3X5B.js";
267
264
  import {
268
265
  parseSlackThreadId
269
266
  } from "./chunk-FXOYURO4.js";
270
- import "./chunk-2QG4MZQ7.js";
267
+ import "./chunk-DLLW5ITL.js";
271
268
  import {
272
269
  pluginCatalogRuntime
273
- } from "./chunk-6FKLPVF3.js";
274
- import "./chunk-EW5MLKT5.js";
270
+ } from "./chunk-OY2PQQ4P.js";
271
+ import "./chunk-XA3VLFPK.js";
275
272
  import {
276
273
  credentialSubjectSchema
277
274
  } from "./chunk-W3U3YQYI.js";
@@ -297,7 +294,7 @@ import {
297
294
  setTags,
298
295
  withContext,
299
296
  withSpan
300
- } from "./chunk-NVUQSFWX.js";
297
+ } from "./chunk-F3SWGSPG.js";
301
298
  import {
302
299
  toOptionalString
303
300
  } from "./chunk-OB42YVAE.js";
@@ -312,10 +309,10 @@ import "./chunk-B3IT5END.js";
312
309
  import "./chunk-3V7OFJZC.js";
313
310
  import {
314
311
  JUNIOR_PLUGIN_TASK_CALLBACK_ROUTE
315
- } from "./chunk-TVE25LPW.js";
312
+ } from "./chunk-WLT72MRP.js";
316
313
  import {
317
314
  JUNIOR_VERSION
318
- } from "./chunk-FHC4JUSF.js";
315
+ } from "./chunk-N3C4XCRE.js";
319
316
  import "./chunk-MLKGABMK.js";
320
317
 
321
318
  // src/app.ts
@@ -325,13 +322,16 @@ import { Hono } from "hono";
325
322
 
326
323
  // src/chat/agent-dispatch/signing.ts
327
324
  import { createHmac, timingSafeEqual } from "crypto";
328
- var DISPATCH_CALLBACK_PATH = "/api/internal/agent-dispatch";
325
+ import { z } from "zod";
329
326
  var DISPATCH_HMAC_CONTEXT = "junior.agent_dispatch.v1";
330
327
  var DISPATCH_SIGNATURE_VERSION = "v1";
331
328
  var DISPATCH_MAX_SKEW_MS = 5 * 60 * 1e3;
332
- var DISPATCH_CALLBACK_TIMEOUT_MS = 1e4;
333
329
  var DISPATCH_TIMESTAMP_HEADER = "x-junior-dispatch-timestamp";
334
330
  var DISPATCH_SIGNATURE_HEADER = "x-junior-dispatch-signature";
331
+ var legacyDispatchCallbackSchema = z.object({
332
+ expectedVersion: z.number().int().positive(),
333
+ id: z.string().min(1)
334
+ }).strict();
335
335
  function getDispatchSecret() {
336
336
  return process.env.JUNIOR_SECRET?.trim() || void 0;
337
337
  }
@@ -350,50 +350,6 @@ function timingSafeMatch(expected, actual) {
350
350
  }
351
351
  return timingSafeEqual(expectedBuffer, actualBuffer);
352
352
  }
353
- function parseDispatchCallback(value) {
354
- if (!value || typeof value !== "object") {
355
- return void 0;
356
- }
357
- const record = value;
358
- if (typeof record.id !== "string" || typeof record.expectedVersion !== "number") {
359
- return void 0;
360
- }
361
- return {
362
- id: record.id,
363
- expectedVersion: record.expectedVersion
364
- };
365
- }
366
- async function scheduleDispatchCallback(callback) {
367
- const baseUrl = resolveBaseUrl();
368
- if (!baseUrl) {
369
- throw new Error(
370
- "Cannot determine base URL for agent dispatch callback (set JUNIOR_BASE_URL or deploy to Vercel)"
371
- );
372
- }
373
- const secret = getDispatchSecret();
374
- if (!secret) {
375
- throw new Error(
376
- "Cannot determine agent dispatch secret (set JUNIOR_SECRET)"
377
- );
378
- }
379
- const body = JSON.stringify(callback);
380
- const timestamp = Date.now().toString();
381
- const response = await fetch(`${baseUrl}${DISPATCH_CALLBACK_PATH}`, {
382
- method: "POST",
383
- headers: {
384
- "content-type": "application/json",
385
- [DISPATCH_TIMESTAMP_HEADER]: timestamp,
386
- [DISPATCH_SIGNATURE_HEADER]: signBody(secret, timestamp, body)
387
- },
388
- signal: AbortSignal.timeout(DISPATCH_CALLBACK_TIMEOUT_MS),
389
- body
390
- });
391
- if (!response.ok) {
392
- throw new Error(
393
- `Agent dispatch callback failed with status ${response.status}`
394
- );
395
- }
396
- }
397
353
  async function verifyDispatchCallbackRequest(request) {
398
354
  const timestamp = request.headers.get(DISPATCH_TIMESTAMP_HEADER)?.trim() ?? "";
399
355
  const signature = request.headers.get(DISPATCH_SIGNATURE_HEADER)?.trim() ?? "";
@@ -411,7 +367,8 @@ async function verifyDispatchCallbackRequest(request) {
411
367
  return void 0;
412
368
  }
413
369
  try {
414
- return parseDispatchCallback(JSON.parse(body));
370
+ const parsed = legacyDispatchCallbackSchema.safeParse(JSON.parse(body));
371
+ return parsed.success ? parsed.data : void 0;
415
372
  } catch {
416
373
  return void 0;
417
374
  }
@@ -425,17 +382,18 @@ import {
425
382
  isSlackDestination,
426
383
  sourceSchema
427
384
  } from "@sentry/junior-plugin-api";
428
- import { z } from "zod";
385
+ import { z as z2 } from "zod";
429
386
  var DISPATCH_PREFIX = "junior:agent_dispatch";
430
387
  var DISPATCH_LOCK_TTL_MS = 10 * 60 * 1e3;
431
- var DISPATCH_INDEX_LOCK_TTL_MS = 1e4;
432
- var DISPATCH_INDEX_MAX_LENGTH = 1e4;
433
- var DEFAULT_MAX_ATTEMPTS = 5;
434
- var nonEmptyExactStringSchema = z.string().min(1).refine(
388
+ var DISPATCH_MAILBOX_APPEND_INDEX_LOCK_TTL_MS = 1e4;
389
+ var DISPATCH_MAILBOX_APPEND_INDEX_MAX_LENGTH = 1e4;
390
+ var nonEmptyExactStringSchema = z2.string().min(1).refine(
435
391
  (value) => value === value.trim() && value.toLowerCase() !== "unknown"
436
392
  );
437
- var incompleteDispatchIndexSchema = z.array(nonEmptyExactStringSchema);
438
- var dispatchStatusSchema = z.enum([
393
+ var pendingDispatchMailboxAppendIndexSchema = z2.array(
394
+ nonEmptyExactStringSchema
395
+ );
396
+ var dispatchStatusSchema = z2.enum([
439
397
  "pending",
440
398
  "running",
441
399
  "awaiting_resume",
@@ -443,35 +401,30 @@ var dispatchStatusSchema = z.enum([
443
401
  "failed",
444
402
  "blocked"
445
403
  ]);
446
- var dispatchActorSchema = z.object({
447
- platform: z.literal("system"),
404
+ var dispatchActorSchema = z2.object({
405
+ platform: z2.literal("system"),
448
406
  name: nonEmptyExactStringSchema
449
407
  }).strict();
450
- var dispatchRecordSchema = z.object({
408
+ var dispatchRecordSchema = z2.object({
451
409
  actor: dispatchActorSchema,
452
- attempt: z.number().int().nonnegative(),
453
- createdAtMs: z.number().finite(),
410
+ createdAtMs: z2.number().finite(),
454
411
  credentialSubject: credentialSubjectSchema.optional(),
455
412
  destination: destinationSchema,
456
413
  destinationVisibility: destinationVisibilitySchema,
457
- errorMessage: z.string().optional(),
414
+ errorMessage: z2.string().optional(),
458
415
  id: nonEmptyExactStringSchema,
459
- idempotencyKey: z.string().min(1),
460
- input: z.string().min(1),
461
- lastCallbackAtMs: z.number().finite().optional(),
462
- leaseExpiresAtMs: z.number().finite().optional(),
463
- maxAttempts: z.number().int().positive(),
464
- metadata: z.record(z.string(), z.string()).optional(),
416
+ idempotencyKey: z2.string().min(1),
417
+ input: z2.string().min(1),
418
+ metadata: z2.record(z2.string(), z2.string()).optional(),
465
419
  plugin: nonEmptyExactStringSchema,
466
- resultMessageTs: z.string().optional(),
420
+ resultMessageTs: z2.string().optional(),
467
421
  source: sourceSchema,
468
422
  status: dispatchStatusSchema,
469
- updatedAtMs: z.number().finite(),
470
- version: z.number().int().positive()
423
+ updatedAtMs: z2.number().finite()
471
424
  }).strict().superRefine((record, ctx) => {
472
425
  if (!isSlackDestination(record.destination)) {
473
426
  ctx.addIssue({
474
- code: z.ZodIssueCode.custom,
427
+ code: z2.ZodIssueCode.custom,
475
428
  message: "Dispatch destination platform must be slack",
476
429
  path: ["destination"]
477
430
  });
@@ -484,7 +437,7 @@ var dispatchRecordSchema = z.object({
484
437
  if (subject.allowedWhen === "private-direct-conversation") {
485
438
  if (!record.destination.channelId.startsWith("D")) {
486
439
  ctx.addIssue({
487
- code: z.ZodIssueCode.custom,
440
+ code: z2.ZodIssueCode.custom,
488
441
  message: "Dispatch credentialSubject requires a private direct Slack destination",
489
442
  path: ["credentialSubject"]
490
443
  });
@@ -492,7 +445,7 @@ var dispatchRecordSchema = z.object({
492
445
  }
493
446
  if (subject.binding.type !== "slack-direct-conversation" || subject.binding.teamId !== record.destination.teamId || subject.binding.channelId !== record.destination.channelId) {
494
447
  ctx.addIssue({
495
- code: z.ZodIssueCode.custom,
448
+ code: z2.ZodIssueCode.custom,
496
449
  message: "Dispatch credentialSubject binding must match destination",
497
450
  path: ["credentialSubject", "binding"]
498
451
  });
@@ -501,27 +454,31 @@ var dispatchRecordSchema = z.object({
501
454
  }
502
455
  if (subject.binding.type !== "scheduled-task" || subject.binding.plugin !== record.plugin || subject.binding.taskId !== subject.taskId) {
503
456
  ctx.addIssue({
504
- code: z.ZodIssueCode.custom,
457
+ code: z2.ZodIssueCode.custom,
505
458
  message: "Dispatch credentialSubject binding must match task",
506
459
  path: ["credentialSubject", "binding"]
507
460
  });
508
461
  }
509
462
  });
463
+ var storedDispatchRecordSchema = dispatchRecordSchema.extend({
464
+ attempt: z2.number().int().nonnegative().optional(),
465
+ lastCallbackAtMs: z2.number().finite().optional(),
466
+ leaseExpiresAtMs: z2.number().finite().optional(),
467
+ maxAttempts: z2.number().int().positive().optional(),
468
+ version: z2.number().int().positive().optional()
469
+ });
510
470
  function getDispatchStorageKey(id) {
511
471
  return `${DISPATCH_PREFIX}:record:${id}`;
512
472
  }
513
- function incompleteDispatchIndexKey() {
473
+ function dispatchLockKey(id) {
474
+ return `${DISPATCH_PREFIX}:lock:${id}`;
475
+ }
476
+ function pendingDispatchMailboxAppendIndexKey() {
514
477
  return `${DISPATCH_PREFIX}:incomplete`;
515
478
  }
516
- function incompleteDispatchIndexLockKey() {
479
+ function pendingDispatchMailboxAppendIndexLockKey() {
517
480
  return `${DISPATCH_PREFIX}:incomplete:lock`;
518
481
  }
519
- function parseIncompleteDispatchIndex(value) {
520
- return value === void 0 || value === null ? [] : incompleteDispatchIndexSchema.parse(value);
521
- }
522
- function dispatchLockKey(id) {
523
- return `${DISPATCH_PREFIX}:lock:${id}`;
524
- }
525
482
  function normalizeMetadata(metadata) {
526
483
  if (!metadata) {
527
484
  return void 0;
@@ -536,15 +493,42 @@ function buildDispatchId(plugin, idempotencyKey) {
536
493
  return `dispatch_${digest}`;
537
494
  }
538
495
  function parseDispatchRecord(value) {
539
- const parsed = dispatchRecordSchema.safeParse(value);
540
- return parsed.success ? parsed.data : void 0;
496
+ const parsed = parseStoredDispatchRecord(value);
497
+ return parsed?.record;
541
498
  }
542
- function getDispatchDestinationLockId(destination) {
543
- return destinationKey(destination);
499
+ function parseStoredDispatchRecord(value) {
500
+ const parsed = storedDispatchRecordSchema.safeParse(value);
501
+ if (!parsed.success) {
502
+ return void 0;
503
+ }
504
+ const {
505
+ attempt: _attempt,
506
+ lastCallbackAtMs: _lastCallbackAtMs,
507
+ leaseExpiresAtMs: _leaseExpiresAtMs,
508
+ maxAttempts: _maxAttempts,
509
+ version: _version,
510
+ ...record
511
+ } = parsed.data;
512
+ return {
513
+ ...typeof parsed.data.leaseExpiresAtMs === "number" ? { legacyLeaseExpiresAtMs: parsed.data.leaseExpiresAtMs } : {},
514
+ record
515
+ };
544
516
  }
545
517
  function getDispatchConversationId(dispatch) {
546
518
  return `agent-dispatch:${dispatch.id}`;
547
519
  }
520
+ function getDispatchInputMessageId(dispatchId) {
521
+ return `agent-dispatch:${dispatchId}`;
522
+ }
523
+ function getLegacyDispatchInputMessageId(dispatchId) {
524
+ return `dispatch:${dispatchId}:user`;
525
+ }
526
+ function getDispatchInputMessageIds(dispatchId) {
527
+ return [
528
+ getDispatchInputMessageId(dispatchId),
529
+ getLegacyDispatchInputMessageId(dispatchId)
530
+ ];
531
+ }
548
532
  function getDispatchTurnId(dispatchId) {
549
533
  return `dispatch:${dispatchId}`;
550
534
  }
@@ -559,7 +543,7 @@ function toDispatchProjection(record) {
559
543
  function isTerminalDispatchStatus(status) {
560
544
  return status === "completed" || status === "failed" || status === "blocked";
561
545
  }
562
- async function withDispatchLock(dispatchId, callback) {
546
+ async function withDispatchLock(dispatchId, task) {
563
547
  const state = getStateAdapter();
564
548
  await state.connect();
565
549
  const lock = await state.acquireLock(
@@ -570,45 +554,11 @@ async function withDispatchLock(dispatchId, callback) {
570
554
  throw new Error(`Could not acquire dispatch lock for ${dispatchId}`);
571
555
  }
572
556
  try {
573
- return await callback(state);
557
+ return await task(state);
574
558
  } finally {
575
559
  await state.releaseLock(lock);
576
560
  }
577
561
  }
578
- async function withIncompleteDispatchIndexLock(state, callback) {
579
- const lock = await state.acquireLock(
580
- incompleteDispatchIndexLockKey(),
581
- DISPATCH_INDEX_LOCK_TTL_MS
582
- );
583
- if (!lock) {
584
- throw new Error("Could not acquire incomplete dispatch index lock");
585
- }
586
- try {
587
- return await callback();
588
- } finally {
589
- await state.releaseLock(lock);
590
- }
591
- }
592
- async function syncIncompleteDispatchIndex(state, record) {
593
- await withIncompleteDispatchIndexLock(state, async () => {
594
- const ids = [
595
- ...new Set(
596
- parseIncompleteDispatchIndex(
597
- await state.get(incompleteDispatchIndexKey())
598
- )
599
- )
600
- ];
601
- const next = isTerminalDispatchStatus(record.status) ? ids.filter((id) => id !== record.id) : ids.includes(record.id) ? ids : [...ids, record.id];
602
- if (next.length === ids.length && next.every((id, index) => id === ids[index])) {
603
- return;
604
- }
605
- await state.set(
606
- incompleteDispatchIndexKey(),
607
- next.slice(-DISPATCH_INDEX_MAX_LENGTH),
608
- JUNIOR_THREAD_STATE_TTL_MS
609
- );
610
- });
611
- }
612
562
  async function putRecord(state, record) {
613
563
  const parsed = dispatchRecordSchema.safeParse(record);
614
564
  if (!parsed.success) {
@@ -622,7 +572,32 @@ async function putRecord(state, record) {
622
572
  next,
623
573
  JUNIOR_THREAD_STATE_TTL_MS
624
574
  );
625
- await syncIncompleteDispatchIndex(state, next);
575
+ }
576
+ async function updatePendingDispatchMailboxAppendIndex(state, update) {
577
+ const lock = await state.acquireLock(
578
+ pendingDispatchMailboxAppendIndexLockKey(),
579
+ DISPATCH_MAILBOX_APPEND_INDEX_LOCK_TTL_MS
580
+ );
581
+ if (!lock) {
582
+ throw new Error("Could not acquire dispatch mailbox append index lock");
583
+ }
584
+ try {
585
+ const stored = await state.get(pendingDispatchMailboxAppendIndexKey());
586
+ const ids = stored === void 0 || stored === null ? [] : pendingDispatchMailboxAppendIndexSchema.parse(stored);
587
+ const next = [...new Set(update([...new Set(ids)]))].slice(
588
+ -DISPATCH_MAILBOX_APPEND_INDEX_MAX_LENGTH
589
+ );
590
+ if (next.length === ids.length && next.every((id, index) => id === ids[index])) {
591
+ return;
592
+ }
593
+ await state.set(
594
+ pendingDispatchMailboxAppendIndexKey(),
595
+ next,
596
+ JUNIOR_THREAD_STATE_TTL_MS
597
+ );
598
+ } finally {
599
+ await state.releaseLock(lock);
600
+ }
626
601
  }
627
602
  async function getDispatchRecord(id) {
628
603
  const state = getStateAdapter();
@@ -641,7 +616,6 @@ async function createOrGetDispatch(args) {
641
616
  const metadata = normalizeMetadata(args.options.metadata);
642
617
  const record = {
643
618
  actor: { platform: "system", name: args.plugin },
644
- attempt: 0,
645
619
  createdAtMs: args.nowMs,
646
620
  ...args.options.credentialSubject ? { credentialSubject: args.options.credentialSubject } : {},
647
621
  destination: args.options.destination,
@@ -649,37 +623,113 @@ async function createOrGetDispatch(args) {
649
623
  id,
650
624
  idempotencyKey: args.options.idempotencyKey,
651
625
  input: args.options.input,
652
- maxAttempts: DEFAULT_MAX_ATTEMPTS,
653
626
  ...metadata ? { metadata } : {},
654
627
  plugin: args.plugin,
655
628
  status: "pending",
656
629
  source: args.options.source,
657
- updatedAtMs: args.nowMs,
658
- version: 1
630
+ updatedAtMs: args.nowMs
659
631
  };
632
+ await updatePendingDispatchMailboxAppendIndex(
633
+ state,
634
+ (ids) => ids.includes(id) ? ids : [...ids, id]
635
+ );
660
636
  await putRecord(state, record);
661
637
  return { record, status: "created" };
662
638
  });
663
639
  }
664
- async function updateDispatchRecord(state, record) {
665
- const next = {
666
- ...record,
667
- updatedAtMs: Date.now(),
668
- version: record.version + 1
669
- };
670
- await putRecord(state, next);
671
- return next;
640
+ async function transitionDispatch(id, transition) {
641
+ return await withDispatchLock(id, async (state) => {
642
+ const current = parseDispatchRecord(
643
+ await state.get(getDispatchStorageKey(id))
644
+ );
645
+ if (!current) {
646
+ return void 0;
647
+ }
648
+ const transitioned = transition(current);
649
+ if (transitioned === current) {
650
+ return current;
651
+ }
652
+ const next = {
653
+ ...transitioned,
654
+ updatedAtMs: Date.now()
655
+ };
656
+ await putRecord(state, next);
657
+ return next;
658
+ });
659
+ }
660
+ async function markDispatchRunning(id) {
661
+ return await transitionDispatch(
662
+ id,
663
+ (record) => isTerminalDispatchStatus(record.status) ? record : { ...record, status: "running", errorMessage: void 0 }
664
+ );
665
+ }
666
+ async function markDispatchAwaitingResume(id) {
667
+ return await transitionDispatch(
668
+ id,
669
+ (record) => isTerminalDispatchStatus(record.status) ? record : {
670
+ ...record,
671
+ status: "awaiting_resume"
672
+ }
673
+ );
672
674
  }
673
- async function listIncompleteDispatchIds() {
675
+ async function markDispatchBlocked(id, errorMessage, resultMessageTs) {
676
+ return await transitionDispatch(
677
+ id,
678
+ (record) => isTerminalDispatchStatus(record.status) ? record : {
679
+ ...record,
680
+ errorMessage,
681
+ ...resultMessageTs ? { resultMessageTs } : {},
682
+ status: "blocked"
683
+ }
684
+ );
685
+ }
686
+ async function markDispatchCompleted(id, resultMessageTs) {
687
+ return await transitionDispatch(
688
+ id,
689
+ (record) => isTerminalDispatchStatus(record.status) ? record : {
690
+ ...record,
691
+ errorMessage: void 0,
692
+ ...resultMessageTs ? { resultMessageTs } : {},
693
+ status: "completed"
694
+ }
695
+ );
696
+ }
697
+ async function markDispatchFailed(id, errorMessage, resultMessageTs) {
698
+ return await transitionDispatch(
699
+ id,
700
+ (record) => isTerminalDispatchStatus(record.status) ? record : {
701
+ ...record,
702
+ errorMessage,
703
+ ...resultMessageTs ? { resultMessageTs } : {},
704
+ status: "failed"
705
+ }
706
+ );
707
+ }
708
+ async function confirmDispatchMailboxAppend(id) {
674
709
  const state = getStateAdapter();
675
710
  await state.connect();
676
- return [
677
- ...new Set(
678
- parseIncompleteDispatchIndex(
679
- await state.get(incompleteDispatchIndexKey())
680
- )
681
- )
682
- ];
711
+ await updatePendingDispatchMailboxAppendIndex(
712
+ state,
713
+ (ids) => ids.filter((candidate) => candidate !== id)
714
+ );
715
+ }
716
+ async function listPendingDispatchMailboxAppends() {
717
+ const state = getStateAdapter();
718
+ await state.connect();
719
+ const stored = await state.get(pendingDispatchMailboxAppendIndexKey());
720
+ return stored === void 0 || stored === null ? [] : [...new Set(pendingDispatchMailboxAppendIndexSchema.parse(stored))];
721
+ }
722
+ async function claimDispatchMailboxAppend(id, nowMs) {
723
+ return await withDispatchLock(id, async (state) => {
724
+ const stored = parseStoredDispatchRecord(
725
+ await state.get(getDispatchStorageKey(id))
726
+ );
727
+ if (!stored || stored.legacyLeaseExpiresAtMs && stored.legacyLeaseExpiresAtMs > nowMs) {
728
+ return void 0;
729
+ }
730
+ await putRecord(state, stored.record);
731
+ return stored.record;
732
+ });
683
733
  }
684
734
  async function getPluginDispatchProjection(args) {
685
735
  const record = await getDispatchRecord(args.id);
@@ -689,487 +739,358 @@ async function getPluginDispatchProjection(args) {
689
739
  return toDispatchProjection(record);
690
740
  }
691
741
 
692
- // src/chat/agent-dispatch/runner.ts
693
- var DISPATCH_SLICE_LEASE_MS = 5 * 60 * 1e3;
694
- function getUserMessageId(dispatch) {
695
- return `dispatch:${dispatch.id}:user`;
696
- }
697
- function buildDispatchConversationText(dispatch) {
698
- return `[dispatched task] ${dispatch.input}`;
699
- }
700
- function upsertDispatchUserMessage(args) {
701
- return upsertConversationMessage(args.conversation, {
702
- id: getUserMessageId(args.dispatch),
703
- role: "user",
704
- text: normalizeConversationText(
705
- buildDispatchConversationText(args.dispatch)
706
- ),
707
- createdAtMs: args.nowMs,
708
- author: {
709
- userName: `system:${args.dispatch.actor.name}`,
710
- isBot: true
742
+ // src/chat/agent-dispatch/work.ts
743
+ import { z as z3 } from "zod";
744
+ var agentDispatchMailboxMetadataSchema = z3.object({
745
+ dispatchId: z3.string().min(1),
746
+ kind: z3.literal("agent_dispatch")
747
+ }).strict();
748
+ var AGENT_DISPATCH_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
749
+ function buildDispatchRoutingContext(dispatch) {
750
+ return {
751
+ actor: dispatch.actor,
752
+ credentialContext: {
753
+ actor: dispatch.actor,
754
+ ...dispatch.credentialSubject ? { subject: dispatch.credentialSubject } : {}
711
755
  },
712
- meta: {
713
- explicitMention: true
714
- }
715
- });
756
+ destinationVisibility: dispatch.destinationVisibility,
757
+ dispatch: {
758
+ actor: dispatch.actor,
759
+ id: dispatch.id,
760
+ metadata: dispatch.metadata,
761
+ plugin: dispatch.plugin
762
+ },
763
+ surface: "api"
764
+ };
716
765
  }
717
- async function persistRuntimePatch(args) {
718
- await persistThreadStateById(args.threadId, {
719
- artifacts: args.artifacts,
720
- conversation: args.conversation,
721
- sandboxRef: args.sandboxRef
722
- });
766
+ function buildAgentDispatchInboundMessage(dispatch, nowMs = Date.now()) {
767
+ return {
768
+ conversationId: getDispatchConversationId(dispatch),
769
+ createdAtMs: dispatch.createdAtMs,
770
+ delivery: "defer",
771
+ destination: dispatch.destination,
772
+ inboundMessageId: getDispatchInputMessageId(dispatch.id),
773
+ input: {
774
+ // The dispatch record is the idempotent request authority. The mailbox
775
+ // only signals which request this conversation lease may advance.
776
+ text: "[plugin dispatch]",
777
+ metadata: {
778
+ dispatchId: dispatch.id,
779
+ kind: "agent_dispatch"
780
+ }
781
+ },
782
+ receivedAtMs: nowMs,
783
+ source: "plugin"
784
+ };
723
785
  }
724
- async function markDispatch(args) {
725
- return await withDispatchLock(args.dispatch.id, async (state) => {
726
- const current = parseDispatchRecord(
727
- await state.get(getDispatchStorageKey(args.dispatch.id))
728
- );
729
- if (!current) {
730
- throw new Error("Dispatch record is missing or invalid.");
731
- }
732
- return await updateDispatchRecord(state, {
733
- ...current,
734
- status: args.status,
735
- ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
736
- ...args.resultMessageTs ? { resultMessageTs: args.resultMessageTs } : {}
737
- });
786
+ async function enqueueAgentDispatch(dispatch, options) {
787
+ const nowMs = options.nowMs ?? Date.now();
788
+ const claimedDispatch = await claimDispatchMailboxAppend(dispatch.id, nowMs);
789
+ if (!claimedDispatch) {
790
+ return;
791
+ }
792
+ await (options.conversationStore ?? getConversationStore()).recordActivity({
793
+ activityAtMs: claimedDispatch.createdAtMs,
794
+ conversationId: getDispatchConversationId(claimedDispatch),
795
+ destination: claimedDispatch.destination,
796
+ nowMs,
797
+ source: "plugin",
798
+ visibility: claimedDispatch.destinationVisibility
799
+ });
800
+ await appendAndEnqueueInboundMessage({
801
+ message: buildAgentDispatchInboundMessage(claimedDispatch, nowMs),
802
+ conversationStore: options.conversationStore,
803
+ nowMs,
804
+ queue: options.queue,
805
+ state: options.state
738
806
  });
807
+ await confirmDispatchMailboxAppend(claimedDispatch.id);
739
808
  }
740
- function canClaimDispatch(record, nowMs) {
741
- if (isTerminalDispatchStatus(record.status)) {
742
- return false;
809
+ function dispatchIdFromMessages(messages) {
810
+ if (messages.length === 0) {
811
+ return void 0;
743
812
  }
744
- if (record.attempt >= record.maxAttempts) {
745
- return false;
813
+ const parsed = messages.map(
814
+ (message) => agentDispatchMailboxMetadataSchema.safeParse(message.input.metadata)
815
+ );
816
+ if (parsed.every((result) => !result.success)) {
817
+ return void 0;
746
818
  }
747
- if (record.status === "running" && typeof record.leaseExpiresAtMs === "number" && record.leaseExpiresAtMs > nowMs) {
748
- return false;
819
+ if (parsed.some((result) => !result.success)) {
820
+ throw new Error("Conversation mailbox mixes dispatch and provider input");
749
821
  }
750
- return true;
822
+ const ids = new Set(
823
+ parsed.map((result) => {
824
+ if (!result.success) {
825
+ throw new Error("Dispatch mailbox metadata failed validation");
826
+ }
827
+ return result.data.dispatchId;
828
+ })
829
+ );
830
+ if (ids.size !== 1) {
831
+ throw new Error("Conversation mailbox contains multiple dispatches");
832
+ }
833
+ return ids.values().next().value;
751
834
  }
752
- async function runAgentDispatchSlice(callback, deps) {
753
- const scheduleCallback = deps.scheduleCallback ?? scheduleDispatchCallback;
754
- const scheduleCompletedTasks = deps.scheduleSessionCompletedPluginTasks ?? scheduleSessionCompletedPluginTasks;
755
- const turnLifecycle = deps.turnLifecycle ?? new ConversationTurnLifecycleService(getConversationEventStore());
756
- const nowMs = Date.now();
757
- const claimedDispatch = await withDispatchLock(callback.id, async (state) => {
758
- const current = parseDispatchRecord(
759
- await state.get(getDispatchStorageKey(callback.id))
835
+ async function resolveAgentDispatchId(context) {
836
+ const mailboxDispatchId = dispatchIdFromMessages(context.attempt.messages);
837
+ if (mailboxDispatchId) {
838
+ return mailboxDispatchId;
839
+ }
840
+ if (context.attempt.messages.length > 0) {
841
+ return void 0;
842
+ }
843
+ const summaries = await listAgentTurnSessionSummariesForConversation(
844
+ context.conversationId
845
+ );
846
+ const activeDispatchIds = new Set(
847
+ summaries.filter(
848
+ (summary) => (summary.state === "awaiting_resume" || summary.state === "running") && Boolean(summary.dispatchId)
849
+ ).map((summary) => summary.dispatchId).filter((id) => Boolean(id))
850
+ );
851
+ if (activeDispatchIds.size > 1) {
852
+ throw new Error(
853
+ `Conversation ${context.conversationId} has multiple active dispatches`
760
854
  );
761
- if (!current || !canClaimDispatch(current, nowMs) || current.version !== callback.expectedVersion) {
762
- return void 0;
855
+ }
856
+ const activeDispatchId = activeDispatchIds.values().next().value;
857
+ if (activeDispatchId) {
858
+ return activeDispatchId;
859
+ }
860
+ const durableDispatchIds = new Set(
861
+ summaries.map((summary) => summary.dispatchId).filter((id) => Boolean(id))
862
+ );
863
+ if (durableDispatchIds.size > 1) {
864
+ throw new Error(
865
+ `Conversation ${context.conversationId} has multiple dispatch sessions`
866
+ );
867
+ }
868
+ const durableDispatchId = durableDispatchIds.values().next().value;
869
+ if (durableDispatchId) {
870
+ const dispatch2 = await getDispatchRecord(durableDispatchId);
871
+ if (dispatch2 && !isTerminalDispatchStatus(dispatch2.status)) {
872
+ return durableDispatchId;
763
873
  }
764
- return await updateDispatchRecord(state, {
765
- ...current,
766
- lastCallbackAtMs: nowMs,
767
- leaseExpiresAtMs: nowMs + DISPATCH_SLICE_LEASE_MS,
768
- status: "running"
769
- });
770
- });
771
- if (!claimedDispatch) {
772
- return;
773
874
  }
774
- let dispatch = claimedDispatch;
875
+ const prefix = "agent-dispatch:";
876
+ if (!context.conversationId.startsWith(prefix)) {
877
+ return void 0;
878
+ }
879
+ const dispatchId = context.conversationId.slice(prefix.length);
880
+ const dispatch = await getDispatchRecord(dispatchId);
881
+ if (!dispatch) {
882
+ return void 0;
883
+ }
884
+ return isTerminalDispatchStatus(dispatch.status) ? void 0 : dispatch.id;
885
+ }
886
+ function createAgentDispatchWorkRouter(options) {
887
+ return async (context) => {
888
+ const dispatchId = await resolveAgentDispatchId(context);
889
+ return dispatchId ? await options.dispatchWorker(context, dispatchId) : await options.fallbackWorker(context);
890
+ };
891
+ }
892
+ async function readDispatchTurnResult(dispatch) {
775
893
  const conversationId = getDispatchConversationId(dispatch);
776
894
  const turnId = getDispatchTurnId(dispatch.id);
777
- const logContext = {
778
- conversationId,
779
- messageConversationId: conversationId,
780
- destinationName: dispatch.destination.channelId,
781
- runId: dispatch.id,
782
- actorType: dispatch.actor.platform,
783
- actorId: dispatch.actor.name,
784
- assistantUserName: botConfig.userName
895
+ const storedSession = await getAgentTurnSessionRecord(conversationId, turnId);
896
+ const summary = (await listAgentTurnSessionSummariesForConversation(conversationId)).find((candidate) => candidate.sessionId === turnId);
897
+ const session = storedSession ?? summary;
898
+ const dispatchOutcome = summary?.dispatchOutcome ?? storedSession?.dispatchOutcome;
899
+ const resultMessageTs = summary?.resultMessageId ?? storedSession?.resultMessageId;
900
+ const errorMessage = storedSession?.errorMessage;
901
+ if (dispatchOutcome) {
902
+ return {
903
+ ...errorMessage ? { errorMessage } : {},
904
+ outcome: dispatchOutcome,
905
+ ...resultMessageTs ? { resultMessageTs } : {}
906
+ };
907
+ }
908
+ if (resultMessageTs) {
909
+ return {
910
+ outcome: "completed",
911
+ resultMessageTs
912
+ };
913
+ }
914
+ if (!session) {
915
+ return {};
916
+ }
917
+ if (session.state === "awaiting_resume") {
918
+ return { hasResumableRun: true, outcome: "awaiting_resume" };
919
+ }
920
+ if (session.state === "running") {
921
+ return { hasResumableRun: true };
922
+ }
923
+ if (session.state !== "completed") {
924
+ return {};
925
+ }
926
+ return {
927
+ outcome: "completed",
928
+ ...resultMessageTs ? { resultMessageTs } : {}
785
929
  };
786
- const destinationLockId = getDispatchDestinationLockId(dispatch.destination);
787
- const stateAdapter = getStateAdapter();
788
- let lifecycleStarted = false;
789
- let lifecycleTerminalized = false;
790
- let failureCode = "persistence_failed";
791
- await stateAdapter.connect();
792
- const destinationLock = await stateAdapter.acquireLock(
793
- destinationLockId,
794
- DISPATCH_SLICE_LEASE_MS
795
- );
796
- if (!destinationLock) {
797
- await markDispatch({
798
- dispatch,
799
- status: "pending",
800
- errorMessage: "Destination conversation is busy"
801
- });
802
- return;
930
+ }
931
+ async function projectDispatchTurnResult(dispatchId, result) {
932
+ switch (result.outcome) {
933
+ case "awaiting_resume":
934
+ await markDispatchAwaitingResume(dispatchId);
935
+ break;
936
+ case "blocked":
937
+ await markDispatchBlocked(
938
+ dispatchId,
939
+ "Dispatch requires authorization that is unavailable for background work",
940
+ result.resultMessageTs
941
+ );
942
+ break;
943
+ case "failed":
944
+ await markDispatchFailed(
945
+ dispatchId,
946
+ result.errorMessage ?? "Agent turn failed",
947
+ result.resultMessageTs
948
+ );
949
+ break;
950
+ case "completed":
951
+ await markDispatchCompleted(dispatchId, result.resultMessageTs);
952
+ break;
803
953
  }
804
- try {
805
- const startedDispatch = await withDispatchLock(
806
- dispatch.id,
807
- async (state) => {
808
- const current = parseDispatchRecord(
809
- await state.get(getDispatchStorageKey(dispatch.id))
810
- );
811
- if (!current || current.status !== "running" || current.version !== dispatch.version || current.attempt >= current.maxAttempts) {
812
- return void 0;
813
- }
814
- return await updateDispatchRecord(state, {
815
- ...current,
816
- attempt: current.attempt + 1
817
- });
818
- }
819
- );
820
- if (!startedDispatch) {
821
- return;
954
+ }
955
+ function getDispatchBlockingError(error) {
956
+ const cause = getConversationTurnBoundaryError(error)?.cause ?? error;
957
+ return cause instanceof AuthorizationFlowDisabledError || cause instanceof PluginCredentialFailureError ? cause : void 0;
958
+ }
959
+ async function persistBlockedDispatchTurn(dispatch, error) {
960
+ const conversationId = getDispatchConversationId(dispatch);
961
+ const sessionId = getDispatchTurnId(dispatch.id);
962
+ const session = await getAgentTurnSessionRecord(conversationId, sessionId);
963
+ await recordAgentTurnSessionSummary({
964
+ actor: dispatch.actor,
965
+ conversationId,
966
+ destination: dispatch.destination,
967
+ destinationVisibility: dispatch.destinationVisibility,
968
+ dispatchId: dispatch.id,
969
+ dispatchOutcome: "blocked",
970
+ sessionId,
971
+ sliceId: session?.sliceId ?? 1,
972
+ source: dispatch.source,
973
+ state: "failed",
974
+ surface: "api"
975
+ });
976
+ await markDispatchBlocked(
977
+ dispatch.id,
978
+ error instanceof AuthorizationFlowDisabledError ? `Dispatch requires ${error.provider} authorization.` : error.message
979
+ );
980
+ }
981
+ function createAgentDispatchConversationWorker(options) {
982
+ return async (context, dispatchId) => {
983
+ const dispatch = await getDispatchRecord(dispatchId);
984
+ if (!dispatch) {
985
+ throw new Error(`Dispatch record is missing for ${dispatchId}`);
822
986
  }
823
- dispatch = startedDispatch;
824
- const persisted = await getPersistedThreadState(conversationId);
825
- const conversation = coerceThreadConversationState(persisted);
826
- await hydrateConversationMessages({ conversation, conversationId });
827
- const completedSession = await getAgentTurnSessionRecord(
828
- conversationId,
829
- turnId
830
- );
831
- if (completedSession?.state === "completed") {
832
- const deliveredMessage = [...conversation.messages].reverse().find(
833
- (message) => message.id.startsWith(`${turnId}:assistant:`) && typeof message.meta?.slackTs === "string"
987
+ const expectedConversationId = getDispatchConversationId(dispatch);
988
+ if (context.conversationId !== expectedConversationId) {
989
+ throw new Error(
990
+ `Dispatch ${dispatch.id} belongs to ${expectedConversationId}, not ${context.conversationId}`
834
991
  );
835
- await markDispatch({
836
- dispatch,
837
- status: "completed",
838
- ...deliveredMessage?.meta?.slackTs ? { resultMessageTs: deliveredMessage.meta.slackTs } : {}
839
- });
840
- return;
841
992
  }
842
- let artifacts = coerceThreadArtifactsState(persisted);
843
- let sandboxRef = getPersistedSandboxState(persisted);
844
- const channelConfiguration = getChannelConfigurationServiceById(
845
- dispatch.destination.channelId
846
- );
847
- const configuration = await channelConfiguration.resolveValues();
848
- const userMessageId = upsertDispatchUserMessage({
849
- conversation,
850
- dispatch,
851
- nowMs
852
- });
853
- await persistConversationMessages({ conversation, conversationId });
854
- await turnLifecycle.start({
855
- conversationId,
856
- turnId,
857
- createdAtMs: nowMs,
858
- inputMessageIds: [userMessageId],
859
- surface: "api"
860
- });
861
- lifecycleStarted = true;
862
- failureCode = "agent_run_failed";
863
- const conversationContext = buildConversationContext(conversation, {
864
- excludeMessageId: userMessageId
865
- });
866
- let resultMessageTs;
867
- let assistantMessageDelivered = turnHasReply(conversation, turnId);
868
- const deliverAssistantMessage = async (assistantMessage) => {
869
- if (!assistantMessage.text.trim()) {
993
+ if (context.destination.platform !== dispatch.destination.platform || context.destination.teamId !== dispatch.destination.teamId || context.destination.channelId !== dispatch.destination.channelId) {
994
+ throw new Error(
995
+ `Dispatch ${dispatch.id} destination does not match its conversation lease`
996
+ );
997
+ }
998
+ let acknowledged = context.attempt.messages.length === 0;
999
+ const acknowledge = async () => {
1000
+ if (acknowledged) {
870
1001
  return;
871
1002
  }
872
- failureCode = "delivery_failed";
873
1003
  try {
874
- resultMessageTs = await sendSlackReply({
875
- channelId: dispatch.destination.channelId,
876
- conversationId,
877
- text: assistantMessage.text
878
- });
879
- } catch (error) {
880
- if (isRetryableSlackPostError(error)) {
881
- throw new RetryableDeliveryError(error);
882
- }
883
- throw error;
884
- }
885
- assistantMessageDelivered = true;
886
- const recordedMessageId = recordDeliveredAssistantMessage({
887
- conversation,
888
- sessionId: turnId,
889
- text: assistantMessage.text,
890
- userMessageId
891
- });
892
- if (resultMessageTs) {
893
- markConversationMessage(conversation, recordedMessageId, {
894
- slackTs: resultMessageTs
895
- });
896
- }
897
- try {
898
- await persistWithRetry(
899
- () => persistConversationMessages({ conversation, conversationId })
900
- );
901
- } catch (persistError) {
902
- logException(
903
- new Error("Accepted assistant message persistence failed"),
904
- "agent_dispatch_assistant_message_post_delivery_persist_failed",
905
- logContext,
906
- {
907
- "error.type": persistError instanceof Error ? persistError.name : typeof persistError
908
- },
909
- "Failed to persist an accepted dispatch assistant message"
1004
+ await context.attempt.ack();
1005
+ } catch {
1006
+ throw new TurnInputCommitLostError(
1007
+ `Conversation work lease lost before dispatch inbox ack for ${context.conversationId}`
910
1008
  );
911
1009
  }
912
- failureCode = "agent_run_failed";
1010
+ acknowledged = true;
913
1011
  };
914
- const outcome = await deps.agentRunner.run({
915
- conversationId,
916
- turnId,
917
- runId: dispatch.id,
918
- input: {
919
- messageText: dispatch.input,
920
- conversationContext,
921
- // Pi history for redelivered dispatch slices comes from the SQL
922
- // event-store projection, not a thread-state mirror.
923
- piMessages: await loadProjection({ conversationId })
924
- },
925
- routing: {
926
- credentialContext: {
927
- actor: dispatch.actor,
928
- ...dispatch.credentialSubject ? { subject: dispatch.credentialSubject } : {}
929
- },
930
- destination: dispatch.destination,
931
- destinationVisibility: dispatch.destinationVisibility,
932
- source: dispatch.source,
933
- dispatch: {
934
- actor: dispatch.actor,
935
- metadata: dispatch.metadata,
936
- plugin: dispatch.plugin
937
- },
938
- surface: "api",
939
- toolChannelId: dispatch.destination.channelId
940
- },
941
- policy: {
942
- authorizationFlowMode: "disabled",
943
- configuration,
944
- channelConfiguration
945
- },
946
- state: {
947
- artifactState: artifacts,
948
- sandboxRef
949
- },
950
- delivery: {
951
- onAssistantMessage: deliverAssistantMessage
952
- },
953
- durability: {
954
- onSandboxRefChanged: async (nextSandboxRef) => {
955
- sandboxRef = nextSandboxRef;
956
- await persistRuntimePatch({
957
- threadId: conversationId,
958
- conversation,
959
- artifacts,
960
- sandboxRef
961
- });
962
- },
963
- onArtifactStateUpdated: async (nextArtifacts2) => {
964
- artifacts = nextArtifacts2;
965
- await persistRuntimePatch({
966
- threadId: conversationId,
967
- conversation,
968
- artifacts,
969
- sandboxRef
970
- });
971
- }
972
- }
973
- });
974
- if (outcome.status === "awaiting_auth") {
975
- await turnLifecycle.fail({
976
- conversationId,
977
- turnId,
978
- createdAtMs: Date.now(),
979
- failureCode: "agent_run_failed"
980
- });
981
- lifecycleTerminalized = true;
982
- await markDispatch({
983
- dispatch,
984
- status: "blocked",
985
- errorMessage: "Dispatch requires authorization from an interactive user turn."
986
- });
987
- return;
1012
+ if (isTerminalDispatchStatus(dispatch.status)) {
1013
+ await acknowledge();
1014
+ return { status: "completed" };
988
1015
  }
989
- if (outcome.status === "suspended") {
990
- const awaiting = await markDispatch({
991
- dispatch,
992
- status: "awaiting_resume"
993
- });
994
- await scheduleCallback({
995
- id: awaiting.id,
996
- expectedVersion: awaiting.version
997
- });
998
- return;
1016
+ const durableResult = await readDispatchTurnResult(dispatch);
1017
+ if (durableResult.outcome === "blocked" || durableResult.outcome === "completed" || durableResult.outcome === "failed") {
1018
+ await projectDispatchTurnResult(dispatch.id, durableResult);
1019
+ await acknowledge();
1020
+ return { status: "completed" };
999
1021
  }
1000
- let reply = outcome.result;
1001
- const failure = reply.diagnostics.outcome === "success" ? void 0 : reply.diagnostics.errorMessage ?? `Agent turn ended with ${reply.diagnostics.outcome}.`;
1002
- let modelFailureEventId;
1003
- if (failure) {
1004
- const finalized = finalizeFailedTurnReplyWithEvent({
1005
- reply,
1006
- logException,
1007
- context: {
1008
- ...logContext,
1009
- modelId: reply.diagnostics.modelId
1010
- }
1011
- });
1012
- reply = finalized.reply;
1013
- modelFailureEventId = finalized.eventId;
1014
- await deliverAssistantMessage({ text: reply.text });
1015
- }
1016
- failureCode = "persistence_failed";
1017
- markConversationMessage(conversation, userMessageId, {
1018
- replied: true,
1019
- skippedReason: void 0
1020
- });
1021
- updateConversationStats(conversation);
1022
- const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : artifacts;
1023
- let statePersisted = false;
1024
- try {
1025
- await persistWithRetry(async () => {
1026
- await persistConversationMessages({ conversation, conversationId });
1027
- await persistRuntimePatch({
1028
- threadId: conversationId,
1029
- conversation,
1030
- artifacts: nextArtifacts,
1031
- sandboxRef: reply.sandboxRef ?? sandboxRef
1032
- });
1033
- });
1034
- statePersisted = true;
1035
- } catch (persistError) {
1036
- const eventId = logException(
1037
- persistError,
1038
- "agent_dispatch_post_delivery_persist_failed",
1039
- logContext,
1040
- {},
1041
- "Failed to persist delivered dispatch state after Slack accepted the reply"
1022
+ if (Date.now() - dispatch.createdAtMs > AGENT_DISPATCH_MAX_AGE_MS) {
1023
+ await markDispatchFailed(
1024
+ dispatch.id,
1025
+ "Dispatch exceeded its maximum processing age"
1042
1026
  );
1043
- await turnLifecycle.fail({
1044
- conversationId,
1045
- turnId,
1046
- createdAtMs: Date.now(),
1047
- failureCode: "persistence_failed",
1048
- ...eventId ? { eventId } : {}
1049
- });
1050
- lifecycleTerminalized = true;
1027
+ await acknowledge();
1028
+ return { status: "completed" };
1051
1029
  }
1052
- if (reply.piMessages?.length) {
1053
- await completeDeliveredTurn({
1054
- conversationId,
1055
- sessionId: turnId,
1056
- sliceId: 1,
1057
- messages: reply.piMessages,
1058
- modelId: reply.diagnostics.modelId,
1059
- durationMs: reply.diagnostics.durationMs,
1060
- usage: reply.diagnostics.usage,
1061
- reasoningLevel: reply.diagnostics.reasoningLevel,
1062
- destination: dispatch.destination,
1063
- destinationVisibility: dispatch.destinationVisibility,
1064
- source: dispatch.source,
1065
- actor: dispatch.actor,
1066
- surface: "api"
1067
- });
1030
+ const runningDispatch = await markDispatchRunning(dispatch.id);
1031
+ if (!runningDispatch) {
1032
+ throw new Error(`Dispatch record disappeared for ${dispatch.id}`);
1068
1033
  }
1069
- if (statePersisted) {
1070
- if (failure) {
1071
- await turnLifecycle.fail({
1072
- conversationId,
1073
- turnId,
1074
- createdAtMs: Date.now(),
1075
- failureCode: "model_execution_failed",
1076
- ...modelFailureEventId ? { eventId: modelFailureEventId } : {}
1034
+ if (isTerminalDispatchStatus(runningDispatch.status)) {
1035
+ await acknowledge();
1036
+ return { status: "completed" };
1037
+ }
1038
+ try {
1039
+ const resumesDurableTurn = durableResult.hasResumableRun === true;
1040
+ if (resumesDurableTurn && context.attempt.messages.length > 0) {
1041
+ await acknowledge();
1042
+ }
1043
+ let result;
1044
+ if (resumesDurableTurn) {
1045
+ await options.resumeTurn(dispatch, {
1046
+ shouldYield: context.shouldYield
1077
1047
  });
1048
+ result = await readDispatchTurnResult(dispatch);
1078
1049
  } else {
1079
- await turnLifecycle.complete({
1080
- conversationId,
1081
- turnId,
1082
- createdAtMs: Date.now(),
1083
- outcome: assistantMessageDelivered ? "success" : "no_reply"
1050
+ const runtimeResult = await options.runTurn(dispatch, {
1051
+ ack: acknowledge,
1052
+ shouldYield: context.shouldYield
1084
1053
  });
1054
+ result = runtimeResult.outcome ? runtimeResult : {
1055
+ ...runtimeResult,
1056
+ ...await readDispatchTurnResult(dispatch)
1057
+ };
1085
1058
  }
1086
- lifecycleTerminalized = true;
1087
- }
1088
- dispatch = await markDispatch({
1089
- dispatch,
1090
- status: failure ? "failed" : "completed",
1091
- ...failure ? { errorMessage: failure } : {},
1092
- resultMessageTs
1093
- });
1094
- if (!failure) {
1095
- try {
1096
- await scheduleCompletedTasks({
1097
- conversationId,
1098
- sessionId: turnId
1099
- });
1100
- } catch (error) {
1101
- logException(
1102
- error,
1103
- "plugin_session_completed_task_schedule_failed",
1104
- logContext,
1105
- {},
1106
- "Plugin session.completed task scheduling failed"
1059
+ if (!result.outcome) {
1060
+ throw new Error(
1061
+ `Dispatch turn ${dispatch.id} returned without a durable outcome`
1107
1062
  );
1108
1063
  }
1109
- }
1110
- } catch (error) {
1111
- if (error instanceof AuthorizationFlowDisabledError) {
1112
- if (lifecycleStarted && !lifecycleTerminalized) {
1113
- await turnLifecycle.fail({
1114
- conversationId,
1115
- turnId,
1116
- createdAtMs: Date.now(),
1117
- failureCode: "agent_run_failed"
1118
- });
1119
- lifecycleTerminalized = true;
1064
+ await projectDispatchTurnResult(dispatch.id, result);
1065
+ if (result.outcome && !acknowledged) {
1066
+ await acknowledge();
1120
1067
  }
1121
- await markDispatch({
1122
- dispatch,
1123
- status: "blocked",
1124
- errorMessage: `Dispatch requires ${error.provider} authorization.`
1125
- });
1126
- return;
1127
- }
1128
- if (error instanceof PluginCredentialFailureError) {
1129
- if (lifecycleStarted && !lifecycleTerminalized) {
1130
- await turnLifecycle.fail({
1131
- conversationId,
1132
- turnId,
1133
- createdAtMs: Date.now(),
1134
- failureCode: "agent_run_failed"
1135
- });
1136
- lifecycleTerminalized = true;
1068
+ return result.outcome === "awaiting_resume" && context.shouldYield() ? { status: "yielded" } : { status: "completed" };
1069
+ } catch (error) {
1070
+ if (isCooperativeTurnYieldError(error)) {
1071
+ await markDispatchAwaitingResume(dispatch.id);
1072
+ return { status: "yielded" };
1137
1073
  }
1138
- await markDispatch({
1139
- dispatch,
1140
- status: "blocked",
1141
- errorMessage: error.message
1142
- });
1143
- return;
1144
- }
1145
- const eventId = logException(
1146
- error,
1147
- "agent_dispatch_run_failed",
1148
- {
1149
- ...logContext,
1150
- modelId: standardModelId(botConfig)
1151
- },
1152
- {},
1153
- "Agent dispatch failed"
1154
- );
1155
- if (lifecycleStarted && !lifecycleTerminalized) {
1156
- await turnLifecycle.fail({
1157
- conversationId,
1158
- turnId,
1159
- createdAtMs: Date.now(),
1160
- failureCode,
1161
- ...eventId ? { eventId } : {}
1162
- });
1163
- lifecycleTerminalized = true;
1074
+ if (isTurnInputCommitLostError(error)) {
1075
+ return { status: "lost_lease" };
1076
+ }
1077
+ const blockingError = getDispatchBlockingError(error);
1078
+ if (blockingError) {
1079
+ await persistBlockedDispatchTurn(dispatch, blockingError);
1080
+ await acknowledge();
1081
+ return { status: "completed" };
1082
+ }
1083
+ if (!context.attempt.isFinalAttempt) {
1084
+ throw error;
1085
+ }
1086
+ await markDispatchFailed(
1087
+ dispatch.id,
1088
+ error instanceof Error ? error.message : "Dispatch turn failed"
1089
+ );
1090
+ await acknowledge();
1091
+ return { status: "completed" };
1164
1092
  }
1165
- await markDispatch({
1166
- dispatch,
1167
- status: "failed",
1168
- errorMessage: error instanceof Error ? error.message : String(error)
1169
- });
1170
- } finally {
1171
- await stateAdapter.releaseLock(destinationLock);
1172
- }
1093
+ };
1173
1094
  }
1174
1095
 
1175
1096
  // src/handlers/agent-dispatch.ts
@@ -1179,15 +1100,24 @@ async function POST(request, waitUntil, options) {
1179
1100
  return new Response("Unauthorized", { status: 401 });
1180
1101
  }
1181
1102
  waitUntil(
1182
- () => runAgentDispatchSlice(payload, {
1183
- agentRunner: options.agentRunner
1184
- }).catch((error) => {
1103
+ (async () => {
1104
+ const dispatch = await getDispatchRecord(payload.id);
1105
+ if (!dispatch) {
1106
+ throw new Error(`Dispatch record is missing for ${payload.id}`);
1107
+ }
1108
+ if (isTerminalDispatchStatus(dispatch.status)) {
1109
+ return;
1110
+ }
1111
+ await enqueueAgentDispatch(dispatch, {
1112
+ queue: options.conversationWorkQueue
1113
+ });
1114
+ })().catch((error) => {
1185
1115
  logException(
1186
1116
  error,
1187
- "agent_dispatch_handler_failed",
1117
+ "agent_dispatch_callback_failed",
1188
1118
  {},
1189
1119
  { "app.dispatch.id": payload.id },
1190
- "Agent dispatch handler failed"
1120
+ "Legacy dispatch callback failed"
1191
1121
  );
1192
1122
  })
1193
1123
  );
@@ -1446,12 +1376,6 @@ async function verifyDispatchCredentialSubjectAccess(options, plugin) {
1446
1376
 
1447
1377
  // src/chat/agent-dispatch/context.ts
1448
1378
  var MAX_DISPATCHES_PER_HEARTBEAT = 25;
1449
- function shouldScheduleDispatch(record, nowMs) {
1450
- if (isTerminalDispatchStatus(record.status)) {
1451
- return false;
1452
- }
1453
- return record.status !== "running" || typeof record.leaseExpiresAtMs !== "number" || record.leaseExpiresAtMs <= nowMs;
1454
- }
1455
1379
  function bindDispatchCredentialSubject(options, plugin) {
1456
1380
  const { credentialSubject, ...baseOptions } = options;
1457
1381
  if (!credentialSubject) {
@@ -1505,10 +1429,10 @@ function createHeartbeatContext(args) {
1505
1429
  nowMs: args.nowMs
1506
1430
  });
1507
1431
  dispatchCount += 1;
1508
- if (shouldScheduleDispatch(result.record, args.nowMs)) {
1509
- await scheduleDispatchCallback({
1510
- id: result.record.id,
1511
- expectedVersion: result.record.version
1432
+ if (!isTerminalDispatchStatus(result.record.status)) {
1433
+ await enqueueAgentDispatch(result.record, {
1434
+ queue: args.conversationWorkQueue,
1435
+ nowMs: args.nowMs
1512
1436
  });
1513
1437
  }
1514
1438
  return {
@@ -1527,37 +1451,9 @@ function createHeartbeatContext(args) {
1527
1451
  }
1528
1452
 
1529
1453
  // src/chat/agent-dispatch/heartbeat.ts
1530
- var DEFAULT_RECOVERY_LIMIT2 = 25;
1531
1454
  var DEFAULT_PLUGIN_LIMIT = 25;
1532
- var DISPATCH_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
1533
1455
  var PLUGIN_HEARTBEAT_TIMEOUT_MS = 25e3;
1534
- function isStaleDispatch(args) {
1535
- if (args.record.status === "running") {
1536
- return typeof args.record.leaseExpiresAtMs === "number" && args.record.leaseExpiresAtMs <= args.nowMs;
1537
- }
1538
- if (args.record.status === "awaiting_resume") {
1539
- return typeof args.record.leaseExpiresAtMs !== "number" || args.record.leaseExpiresAtMs <= args.nowMs;
1540
- }
1541
- if (args.record.status === "pending") {
1542
- return typeof args.record.lastCallbackAtMs !== "number" || args.record.lastCallbackAtMs + 6e4 <= args.nowMs;
1543
- }
1544
- return false;
1545
- }
1546
- async function failDispatch(args) {
1547
- await withDispatchLock(args.record.id, async (state) => {
1548
- const current = parseDispatchRecord(
1549
- await state.get(getDispatchStorageKey(args.record.id))
1550
- ) ?? args.record;
1551
- if (isTerminalDispatchStatus(current.status)) {
1552
- return;
1553
- }
1554
- await updateDispatchRecord(state, {
1555
- ...current,
1556
- errorMessage: args.errorMessage,
1557
- status: "failed"
1558
- });
1559
- });
1560
- }
1456
+ var DISPATCH_MAILBOX_APPEND_LIMIT = 100;
1561
1457
  async function runWithTimeout(promise, timeoutMs) {
1562
1458
  let timeout;
1563
1459
  try {
@@ -1575,52 +1471,6 @@ async function runWithTimeout(promise, timeoutMs) {
1575
1471
  }
1576
1472
  }
1577
1473
  }
1578
- async function recoverStaleDispatches(args) {
1579
- const ids = await listIncompleteDispatchIds();
1580
- let recovered = 0;
1581
- for (const id of ids) {
1582
- if (recovered >= (args.limit ?? DEFAULT_RECOVERY_LIMIT2)) {
1583
- break;
1584
- }
1585
- const record = await getDispatchRecord(id);
1586
- if (!record || isTerminalDispatchStatus(record.status)) {
1587
- continue;
1588
- }
1589
- try {
1590
- if (!isStaleDispatch({ record, nowMs: args.nowMs })) {
1591
- continue;
1592
- }
1593
- if (record.createdAtMs + DISPATCH_MAX_AGE_MS <= args.nowMs) {
1594
- await failDispatch({
1595
- record,
1596
- errorMessage: "Dispatch expired before completion."
1597
- });
1598
- continue;
1599
- }
1600
- if (record.attempt >= record.maxAttempts) {
1601
- await failDispatch({
1602
- record,
1603
- errorMessage: "Dispatch exceeded retry attempts."
1604
- });
1605
- continue;
1606
- }
1607
- await scheduleDispatchCallback({
1608
- id: record.id,
1609
- expectedVersion: record.version
1610
- });
1611
- recovered += 1;
1612
- } catch (error) {
1613
- logException(
1614
- error,
1615
- "agent_dispatch_recovery_failed",
1616
- { runId: record.id },
1617
- { "app.plugin.name": record.plugin },
1618
- "Agent dispatch recovery failed"
1619
- );
1620
- }
1621
- }
1622
- return recovered;
1623
- }
1624
1474
  async function runPluginHeartbeats(args) {
1625
1475
  let count = 0;
1626
1476
  for (const plugin of getPlugins()) {
@@ -1638,6 +1488,7 @@ async function runPluginHeartbeats(args) {
1638
1488
  Promise.resolve(
1639
1489
  heartbeat(
1640
1490
  createHeartbeatContext({
1491
+ conversationWorkQueue: args.conversationWorkQueue,
1641
1492
  plugin,
1642
1493
  nowMs: args.nowMs
1643
1494
  })
@@ -1667,13 +1518,55 @@ async function runPluginHeartbeats(args) {
1667
1518
  }
1668
1519
  }
1669
1520
  }
1521
+ async function recoverPendingDispatchMailboxAppends(args) {
1522
+ const ids = (await listPendingDispatchMailboxAppends()).slice(
1523
+ 0,
1524
+ DISPATCH_MAILBOX_APPEND_LIMIT
1525
+ );
1526
+ for (const id of ids) {
1527
+ try {
1528
+ const dispatch = await getDispatchRecord(id);
1529
+ if (!dispatch || isTerminalDispatchStatus(dispatch.status)) {
1530
+ await confirmDispatchMailboxAppend(id);
1531
+ continue;
1532
+ }
1533
+ if (args.nowMs - dispatch.createdAtMs > AGENT_DISPATCH_MAX_AGE_MS) {
1534
+ await markDispatchFailed(
1535
+ id,
1536
+ "Dispatch expired before its conversation mailbox append completed"
1537
+ );
1538
+ await confirmDispatchMailboxAppend(id);
1539
+ continue;
1540
+ }
1541
+ await enqueueAgentDispatch(dispatch, {
1542
+ nowMs: args.nowMs,
1543
+ queue: args.conversationWorkQueue
1544
+ });
1545
+ } catch (error) {
1546
+ logException(
1547
+ error,
1548
+ "dispatch_mailbox_append_recovery_failed",
1549
+ {},
1550
+ { "app.dispatch.id": id },
1551
+ "Pending dispatch mailbox append recovery failed"
1552
+ );
1553
+ }
1554
+ }
1555
+ }
1670
1556
  async function runHeartbeat(args) {
1557
+ const queue = args.conversationWorkQueue ?? getVercelConversationWorkQueue();
1671
1558
  await recoverConversationWork({
1672
1559
  nowMs: args.nowMs,
1673
- queue: args.conversationWorkQueue ?? getVercelConversationWorkQueue()
1560
+ queue
1561
+ });
1562
+ await recoverPendingDispatchMailboxAppends({
1563
+ conversationWorkQueue: queue,
1564
+ nowMs: args.nowMs
1565
+ });
1566
+ await runPluginHeartbeats({
1567
+ conversationWorkQueue: queue,
1568
+ nowMs: args.nowMs
1674
1569
  });
1675
- await recoverStaleDispatches({ nowMs: args.nowMs });
1676
- await runPluginHeartbeats({ nowMs: args.nowMs });
1677
1570
  }
1678
1571
 
1679
1572
  // src/handlers/heartbeat.ts
@@ -2412,7 +2305,7 @@ import {
2412
2305
  Message,
2413
2306
  ThreadImpl
2414
2307
  } from "chat";
2415
- import { z as z3 } from "zod";
2308
+ import { z as z5 } from "zod";
2416
2309
 
2417
2310
  // src/chat/ingress/message-router.ts
2418
2311
  function normalizeIncomingSlackThreadId(threadId, message) {
@@ -2583,14 +2476,14 @@ async function ensureSlackMessageActorIdentity(message, teamId, lookupSlackUser2
2583
2476
  }
2584
2477
 
2585
2478
  // src/chat/slack/user.ts
2586
- import { z as z2 } from "zod";
2587
- var slackUserInfoSchema = z2.object({
2588
- ok: z2.literal(true),
2589
- user: z2.object({
2590
- name: z2.string().nullish(),
2591
- profile: z2.object({
2592
- email: z2.string().nullish(),
2593
- real_name: z2.string().nullish()
2479
+ import { z as z4 } from "zod";
2480
+ var slackUserInfoSchema = z4.object({
2481
+ ok: z4.literal(true),
2482
+ user: z4.object({
2483
+ name: z4.string().nullish(),
2484
+ profile: z4.object({
2485
+ email: z4.string().nullish(),
2486
+ real_name: z4.string().nullish()
2594
2487
  }).optional()
2595
2488
  })
2596
2489
  });
@@ -2683,17 +2576,17 @@ function stripLeadingSteeringOverride(text) {
2683
2576
  }
2684
2577
 
2685
2578
  // src/chat/task-execution/slack-work.ts
2686
- var slackConversationRouteSchema = z3.enum(["mention", "subscribed"]);
2579
+ var slackConversationRouteSchema = z5.enum(["mention", "subscribed"]);
2687
2580
  function hasSteeringOverride(text) {
2688
2581
  return stripLeadingSteeringOverride(text) !== text;
2689
2582
  }
2690
- var serializedDateSchema = z3.iso.datetime();
2691
- var mdastPointSchema = z3.object({
2692
- column: z3.number().int().positive(),
2693
- line: z3.number().int().positive(),
2694
- offset: z3.number().int().nonnegative().optional()
2583
+ var serializedDateSchema = z5.iso.datetime();
2584
+ var mdastPointSchema = z5.object({
2585
+ column: z5.number().int().positive(),
2586
+ line: z5.number().int().positive(),
2587
+ offset: z5.number().int().nonnegative().optional()
2695
2588
  }).strict();
2696
- var mdastPositionSchema = z3.object({
2589
+ var mdastPositionSchema = z5.object({
2697
2590
  end: mdastPointSchema,
2698
2591
  start: mdastPointSchema
2699
2592
  }).strict();
@@ -2805,7 +2698,7 @@ function isFormattedContentNode(value) {
2805
2698
  return false;
2806
2699
  }
2807
2700
  }
2808
- var formattedContentSchema = z3.custom(
2701
+ var formattedContentSchema = z5.custom(
2809
2702
  (value) => {
2810
2703
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
2811
2704
  return false;
@@ -2815,79 +2708,79 @@ var formattedContentSchema = z3.custom(
2815
2708
  },
2816
2709
  "must be a valid formatted-content root"
2817
2710
  );
2818
- var serializedMessageSchema = z3.object({
2819
- _type: z3.literal("chat:Message"),
2820
- attachments: z3.array(
2821
- z3.object({
2822
- type: z3.enum(["image", "file", "video", "audio"]),
2823
- url: z3.string().optional(),
2824
- name: z3.string().optional(),
2825
- mimeType: z3.string().optional(),
2826
- size: z3.number().finite().optional(),
2827
- width: z3.number().finite().optional(),
2828
- height: z3.number().finite().optional(),
2829
- fetchMetadata: z3.record(z3.string(), z3.string()).optional()
2711
+ var serializedMessageSchema = z5.object({
2712
+ _type: z5.literal("chat:Message"),
2713
+ attachments: z5.array(
2714
+ z5.object({
2715
+ type: z5.enum(["image", "file", "video", "audio"]),
2716
+ url: z5.string().optional(),
2717
+ name: z5.string().optional(),
2718
+ mimeType: z5.string().optional(),
2719
+ size: z5.number().finite().optional(),
2720
+ width: z5.number().finite().optional(),
2721
+ height: z5.number().finite().optional(),
2722
+ fetchMetadata: z5.record(z5.string(), z5.string()).optional()
2830
2723
  }).strict()
2831
2724
  ),
2832
- author: z3.object({
2833
- userId: z3.string(),
2834
- userName: z3.string(),
2835
- fullName: z3.string(),
2836
- isBot: z3.union([z3.boolean(), z3.literal("unknown")]),
2837
- isMe: z3.boolean()
2725
+ author: z5.object({
2726
+ userId: z5.string(),
2727
+ userName: z5.string(),
2728
+ fullName: z5.string(),
2729
+ isBot: z5.union([z5.boolean(), z5.literal("unknown")]),
2730
+ isMe: z5.boolean()
2838
2731
  }).strict(),
2839
2732
  formatted: formattedContentSchema,
2840
- id: z3.string().min(1),
2841
- isMention: z3.boolean().optional(),
2842
- links: z3.array(
2843
- z3.object({
2844
- url: z3.string(),
2845
- title: z3.string().optional(),
2846
- description: z3.string().optional(),
2847
- imageUrl: z3.string().optional(),
2848
- siteName: z3.string().optional()
2733
+ id: z5.string().min(1),
2734
+ isMention: z5.boolean().optional(),
2735
+ links: z5.array(
2736
+ z5.object({
2737
+ url: z5.string(),
2738
+ title: z5.string().optional(),
2739
+ description: z5.string().optional(),
2740
+ imageUrl: z5.string().optional(),
2741
+ siteName: z5.string().optional()
2849
2742
  }).strict()
2850
2743
  ).optional(),
2851
- metadata: z3.object({
2744
+ metadata: z5.object({
2852
2745
  dateSent: serializedDateSchema,
2853
- edited: z3.boolean(),
2746
+ edited: z5.boolean(),
2854
2747
  editedAt: serializedDateSchema.optional()
2855
2748
  }).strict(),
2856
- raw: z3.unknown(),
2857
- text: z3.string(),
2858
- threadId: z3.string().min(1)
2749
+ raw: z5.unknown(),
2750
+ text: z5.string(),
2751
+ threadId: z5.string().min(1)
2859
2752
  }).strict();
2860
- var serializedThreadSchema = z3.object({
2861
- _type: z3.literal("chat:Thread"),
2862
- adapterName: z3.literal("slack"),
2863
- channelId: z3.string().min(1),
2864
- channelVisibility: z3.enum(["private", "workspace", "external", "unknown"]).optional(),
2753
+ var serializedThreadSchema = z5.object({
2754
+ _type: z5.literal("chat:Thread"),
2755
+ adapterName: z5.literal("slack"),
2756
+ channelId: z5.string().min(1),
2757
+ channelVisibility: z5.enum(["private", "workspace", "external", "unknown"]).optional(),
2865
2758
  currentMessage: serializedMessageSchema.optional(),
2866
- id: z3.string().min(1),
2867
- isDM: z3.boolean()
2759
+ id: z5.string().min(1),
2760
+ isDM: z5.boolean()
2868
2761
  }).strict();
2869
- var slackInstallationSchema = z3.object({
2870
- enterpriseId: z3.string().optional(),
2871
- isEnterpriseInstall: z3.boolean().optional(),
2872
- teamId: z3.string().optional()
2762
+ var slackInstallationSchema = z5.object({
2763
+ enterpriseId: z5.string().optional(),
2764
+ isEnterpriseInstall: z5.boolean().optional(),
2765
+ teamId: z5.string().optional()
2873
2766
  }).strict();
2874
- var slackConversationMessageMetadataBaseSchema = z3.object({
2767
+ var slackConversationMessageMetadataBaseSchema = z5.object({
2875
2768
  installation: slackInstallationSchema.optional(),
2876
2769
  message: serializedMessageSchema,
2877
- platform: z3.literal("slack"),
2770
+ platform: z5.literal("slack"),
2878
2771
  route: slackConversationRouteSchema,
2879
2772
  thread: serializedThreadSchema
2880
2773
  });
2881
- var slackConversationMessageMetadataSchema = z3.union([
2774
+ var slackConversationMessageMetadataSchema = z5.union([
2882
2775
  slackConversationMessageMetadataBaseSchema.strict(),
2883
2776
  slackConversationMessageMetadataBaseSchema.extend({
2884
- kind: z3.literal("resource_event"),
2885
- resourceEvent: z3.object({
2886
- eventKey: z3.string(),
2887
- eventType: z3.string(),
2888
- provider: z3.string(),
2889
- resourceRef: z3.string(),
2890
- subscriptionId: z3.string()
2777
+ kind: z5.literal("resource_event"),
2778
+ resourceEvent: z5.object({
2779
+ eventKey: z5.string(),
2780
+ eventType: z5.string(),
2781
+ provider: z5.string(),
2782
+ resourceRef: z5.string(),
2783
+ subscriptionId: z5.string()
2891
2784
  }).strict()
2892
2785
  }).strict()
2893
2786
  ]);
@@ -4799,14 +4692,14 @@ function registerVercelPluginTaskDevConsumer() {
4799
4692
  }
4800
4693
 
4801
4694
  // src/chat/services/subscribed-decision.ts
4802
- import { z as z4 } from "zod";
4803
- var replyDecisionSchema = z4.object({
4804
- should_reply: z4.boolean().describe("Whether Junior should respond to this thread message."),
4805
- should_unsubscribe: z4.boolean().describe(
4695
+ import { z as z6 } from "zod";
4696
+ var replyDecisionSchema = z6.object({
4697
+ should_reply: z6.boolean().describe("Whether Junior should respond to this thread message."),
4698
+ should_unsubscribe: z6.boolean().describe(
4806
4699
  "Whether Junior should unsubscribe from this thread because the user clearly asked it to stop participating."
4807
4700
  ),
4808
- confidence: z4.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
4809
- reason: z4.string().describe("Short reason for the decision; use an empty string if none.")
4701
+ confidence: z6.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
4702
+ reason: z6.string().describe("Short reason for the decision; use an empty string if none.")
4810
4703
  }).strict();
4811
4704
  var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
4812
4705
  var ROUTER_CLASSIFIER_MAX_TOKENS = 240;
@@ -6580,11 +6473,11 @@ function getSlackMessageTs(message) {
6580
6473
  }
6581
6474
 
6582
6475
  // src/chat/slack/action-token.ts
6583
- import { z as z5 } from "zod";
6584
- var slackActionTokenSchema = z5.string().trim().min(1).brand();
6585
- var slackMessageEnvelopeSchema = z5.object({
6586
- raw: z5.object({
6587
- action_token: z5.unknown().optional()
6476
+ import { z as z7 } from "zod";
6477
+ var slackActionTokenSchema = z7.string().trim().min(1).brand();
6478
+ var slackMessageEnvelopeSchema = z7.object({
6479
+ raw: z7.object({
6480
+ action_token: z7.unknown().optional()
6588
6481
  }).optional()
6589
6482
  });
6590
6483
  function readSlackActionToken(message) {
@@ -6869,14 +6762,14 @@ function createReplyToThread(deps) {
6869
6762
  }
6870
6763
  const threadId = getThreadId(thread, message);
6871
6764
  const channelId = getChannelId(thread, message);
6872
- const channelName = channelId ? await resolveChannelName(thread) : void 0;
6765
+ const channelName = !options.execution && channelId ? await resolveChannelName(thread) : void 0;
6873
6766
  const slackChannelType = resolveSlackChannelTypeFromMessage(message);
6874
6767
  const slackConversation = resolveSlackConversationContext({
6875
6768
  channelId,
6876
6769
  channelName,
6877
6770
  channelType: slackChannelType
6878
6771
  });
6879
- const destinationVisibility = conversationVisibilityFromSlackChannelType(slackChannelType);
6772
+ const destinationVisibility = options.execution?.destinationVisibility ?? conversationVisibilityFromSlackChannelType(slackChannelType);
6880
6773
  const threadTs = getThreadTs(threadId);
6881
6774
  const assistantThreadContext = getAssistantThreadContext(message);
6882
6775
  const messageTs = getMessageTs(message);
@@ -6885,7 +6778,7 @@ function createReplyToThread(deps) {
6885
6778
  "Slack reply execution"
6886
6779
  );
6887
6780
  const teamId = destination.teamId;
6888
- const source = createSlackSource({
6781
+ const source = options.execution?.source ?? createSlackSource({
6889
6782
  channelId: channelId ?? destination.channelId,
6890
6783
  messageTs,
6891
6784
  teamId,
@@ -6893,7 +6786,7 @@ function createReplyToThread(deps) {
6893
6786
  type: destinationVisibility === "public" ? "pub" : "priv"
6894
6787
  });
6895
6788
  const slackActionToken = readSlackActionToken(message);
6896
- const runId = getRunId(thread, message);
6789
+ const runId = options.execution?.dispatch?.id ?? getRunId(thread, message);
6897
6790
  const conversationId = threadId ?? runId;
6898
6791
  if (!conversationId) {
6899
6792
  throw new Error("Slack reply execution requires a conversation id");
@@ -6935,7 +6828,7 @@ function createReplyToThread(deps) {
6935
6828
  )
6936
6829
  );
6937
6830
  const effectiveUserText = currentText.userText;
6938
- const credentialContext = resourceEventCredentialContext(message) ?? {
6831
+ const credentialContext = options.execution?.credentialContext ?? resourceEventCredentialContext(message) ?? {
6939
6832
  actor: { type: "user", userId: message.author.userId }
6940
6833
  };
6941
6834
  const actor = "type" in credentialContext.actor ? await ensureSlackMessageActorIdentity(
@@ -6953,15 +6846,17 @@ function createReplyToThread(deps) {
6953
6846
  options.explicitMention || message.isMention
6954
6847
  ),
6955
6848
  queuedMessages: options.queuedMessages,
6849
+ channelConfiguration: options.execution?.channelConfiguration,
6956
6850
  context: {
6957
6851
  threadId,
6958
6852
  actorId: slackActorId,
6959
6853
  channelId,
6960
6854
  runId
6961
- }
6855
+ },
6856
+ skipBackfill: options.skipBackfill
6962
6857
  });
6963
6858
  const slackMessageTs = getSlackMessageTs(message);
6964
- const turnId = buildDeterministicTurnId(message.id);
6859
+ const turnId = options.execution?.turnId ?? buildDeterministicTurnId(message.id);
6965
6860
  const turnTraceContext = {
6966
6861
  conversationId,
6967
6862
  messageConversationId: threadId,
@@ -7109,6 +7004,11 @@ function createReplyToThread(deps) {
7109
7004
  return drainParkedInputToEventLog(parkedPairs);
7110
7005
  };
7111
7006
  if (preparedState.userMessageAlreadyReplied) {
7007
+ const deliveredMessage = [...preparedState.conversation.messages].reverse().find(
7008
+ (candidate) => candidate.role === "assistant" && candidate.id.startsWith(`${turnId}:assistant:`) && candidate.meta?.replied === true
7009
+ );
7010
+ options.onTurnDeliveryAccepted?.(deliveredMessage?.meta?.slackTs);
7011
+ options.onTurnOutcome?.({ outcome: "completed" });
7112
7012
  await persistThreadState(thread, {
7113
7013
  conversation: preparedState.conversation
7114
7014
  });
@@ -7185,7 +7085,7 @@ function createReplyToThread(deps) {
7185
7085
  }
7186
7086
  }
7187
7087
  }
7188
- const configReply = await maybeApplyProviderDefaultConfigRequest({
7088
+ const configReply = options.execution?.skipProviderDefaultConfig ? void 0 : await maybeApplyProviderDefaultConfigRequest({
7189
7089
  channelConfiguration: preparedState.channelConfiguration,
7190
7090
  actorId: actor?.userId,
7191
7091
  text: effectiveUserText
@@ -7238,7 +7138,7 @@ function createReplyToThread(deps) {
7238
7138
  preparedState.userMessageId
7239
7139
  ])
7240
7140
  ],
7241
- surface: "slack",
7141
+ surface: options.execution?.surface ?? "slack",
7242
7142
  turnId
7243
7143
  });
7244
7144
  }
@@ -7251,7 +7151,8 @@ function createReplyToThread(deps) {
7251
7151
  sliceId: 1,
7252
7152
  startedAtMs: turnStartedAtMs,
7253
7153
  state: "running",
7254
- surface: "slack",
7154
+ surface: options.execution?.surface ?? "slack",
7155
+ dispatchId: options.execution?.dispatch?.id,
7255
7156
  actor: executionActor,
7256
7157
  destination,
7257
7158
  destinationVisibility,
@@ -7323,13 +7224,36 @@ function createReplyToThread(deps) {
7323
7224
  let shouldPersistFailureState = true;
7324
7225
  let runResultHandled = false;
7325
7226
  let assistantMessageDelivered = false;
7227
+ let acceptedDeliveryId;
7326
7228
  let lifecycleTerminalized = false;
7327
7229
  let turnCompletionNotified = false;
7230
+ const recordDispatchOutcome = async (dispatchOutcome, state) => {
7231
+ const dispatchId = options.execution?.dispatch?.id;
7232
+ if (!dispatchId) {
7233
+ return;
7234
+ }
7235
+ await recordAgentTurnSessionSummary({
7236
+ channelName,
7237
+ conversationId,
7238
+ destination,
7239
+ destinationVisibility,
7240
+ dispatchId,
7241
+ dispatchOutcome,
7242
+ ...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : {},
7243
+ sessionId: turnId,
7244
+ sliceId: 1,
7245
+ source,
7246
+ startedAtMs: message.metadata.dateSent.getTime(),
7247
+ state,
7248
+ surface: options.execution?.surface ?? "slack"
7249
+ });
7250
+ };
7328
7251
  let latestArtifacts = preparedState.artifacts;
7329
7252
  let assistantTitleArtifacts = {};
7330
7253
  let agentContinueScheduleError;
7331
7254
  let boundaryFailureCode = "agent_run_failed";
7332
7255
  let finalizedFailureEventId;
7256
+ let terminalDispatchFailureOutcome;
7333
7257
  const notifyTurnCompleted = async () => {
7334
7258
  if (turnCompletionNotified) {
7335
7259
  return;
@@ -7337,7 +7261,7 @@ function createReplyToThread(deps) {
7337
7261
  await options.onTurnCompleted?.();
7338
7262
  turnCompletionNotified = true;
7339
7263
  };
7340
- const deliverAssistantMessage = async (assistantMessage) => {
7264
+ const deliverAssistantMessage = async (assistantMessage, terminalDispatchOutcome) => {
7341
7265
  if (!assistantMessage.text.trim()) {
7342
7266
  return;
7343
7267
  }
@@ -7345,12 +7269,12 @@ function createReplyToThread(deps) {
7345
7269
  let slackTs;
7346
7270
  await beforeFirstResponsePost();
7347
7271
  try {
7348
- if (channelId && threadTs && thread.adapter.name === "slack") {
7272
+ if (channelId && thread.adapter.name === "slack") {
7349
7273
  slackTs = await sendSlackReply({
7350
7274
  channelId,
7351
7275
  conversationId,
7352
7276
  text: assistantMessage.text,
7353
- threadTs
7277
+ ...threadTs ? { threadTs } : {}
7354
7278
  });
7355
7279
  } else {
7356
7280
  for (const text of splitSlackReplyText(assistantMessage.text)) {
@@ -7379,6 +7303,8 @@ function createReplyToThread(deps) {
7379
7303
  });
7380
7304
  }
7381
7305
  assistantMessageDelivered = true;
7306
+ acceptedDeliveryId = slackTs;
7307
+ options.onTurnDeliveryAccepted?.(slackTs);
7382
7308
  const recordedMessageId = recordDeliveredAssistantMessage({
7383
7309
  conversation: preparedState.conversation,
7384
7310
  sessionId: turnId,
@@ -7410,6 +7336,35 @@ function createReplyToThread(deps) {
7410
7336
  "Failed to persist an accepted Slack assistant message"
7411
7337
  );
7412
7338
  }
7339
+ if (slackTs && options.execution?.dispatch?.id) {
7340
+ try {
7341
+ await persistWithRetry(
7342
+ () => recordAgentTurnSessionSummary({
7343
+ channelName,
7344
+ conversationId,
7345
+ destination,
7346
+ destinationVisibility,
7347
+ dispatchId: options.execution?.dispatch?.id,
7348
+ resultMessageId: slackTs,
7349
+ sessionId: turnId,
7350
+ sliceId: 1,
7351
+ source,
7352
+ startedAtMs: message.metadata.dateSent.getTime(),
7353
+ ...terminalDispatchOutcome ? { dispatchOutcome: terminalDispatchOutcome } : {},
7354
+ state: terminalDispatchOutcome ? "failed" : "running",
7355
+ surface: options.execution?.surface ?? "slack"
7356
+ })
7357
+ );
7358
+ } catch (error) {
7359
+ logException(
7360
+ error,
7361
+ "agent_turn_delivery_receipt_persist_failed",
7362
+ turnTraceContext,
7363
+ {},
7364
+ "Failed to persist accepted turn delivery receipt"
7365
+ );
7366
+ }
7367
+ }
7413
7368
  boundaryFailureCode = "agent_run_failed";
7414
7369
  };
7415
7370
  try {
@@ -7570,19 +7525,20 @@ function createReplyToThread(deps) {
7570
7525
  },
7571
7526
  routing: {
7572
7527
  credentialContext,
7573
- actor,
7528
+ actor: options.execution ? executionActor : actor,
7574
7529
  slackConversation,
7575
7530
  source,
7576
7531
  destination,
7577
7532
  destinationVisibility,
7578
- surface: "slack",
7533
+ surface: options.execution?.surface ?? "slack",
7534
+ dispatch: options.execution?.dispatch,
7579
7535
  toolChannelId,
7580
7536
  slackActionToken
7581
7537
  },
7582
7538
  policy: {
7583
7539
  configuration: preparedState.configuration,
7584
7540
  channelConfiguration: preparedState.channelConfiguration,
7585
- authorizationFlowMode: message.author.isBot === true ? "disabled" : void 0,
7541
+ authorizationFlowMode: options.execution?.authorizationFlowMode ?? (message.author.isBot === true ? "disabled" : void 0),
7586
7542
  turnDeadlineAtMs: getTurnRequestDeadline()?.deadlineAtMs
7587
7543
  },
7588
7544
  state: {
@@ -7624,6 +7580,8 @@ function createReplyToThread(deps) {
7624
7580
  }
7625
7581
  });
7626
7582
  if (outcome.status === "awaiting_auth") {
7583
+ await recordDispatchOutcome("blocked", "failed");
7584
+ options.onTurnOutcome?.({ outcome: "blocked" });
7627
7585
  if (!actor) {
7628
7586
  const authFailureEventId = logException(
7629
7587
  new Error(
@@ -7675,6 +7633,7 @@ function createReplyToThread(deps) {
7675
7633
  return;
7676
7634
  }
7677
7635
  if (outcome.status === "suspended") {
7636
+ options.onTurnOutcome?.({ outcome: "awaiting_resume" });
7678
7637
  if (options.shouldYield?.()) {
7679
7638
  shouldPersistFailureState = false;
7680
7639
  throw new CooperativeTurnYieldError();
@@ -7730,6 +7689,10 @@ function createReplyToThread(deps) {
7730
7689
  finalizedFailureEventId = finalized.eventId;
7731
7690
  await deliverAssistantMessage({ text: reply.text });
7732
7691
  }
7692
+ const turnResult = reply.diagnostics.outcome === "success" ? { outcome: "completed" } : {
7693
+ errorMessage: reply.diagnostics.errorMessage ?? `Agent turn ended with ${reply.diagnostics.outcome}.`,
7694
+ outcome: "failed"
7695
+ };
7733
7696
  runResultHandled = true;
7734
7697
  shouldPersistFailureState = false;
7735
7698
  boundaryFailureCode = "agent_run_failed";
@@ -7761,10 +7724,14 @@ function createReplyToThread(deps) {
7761
7724
  source,
7762
7725
  sessionId: turnId,
7763
7726
  sliceId: 1,
7727
+ dispatchOutcome: reply.diagnostics.outcome === "success" ? "completed" : "failed",
7728
+ ...options.execution?.dispatch && turnResult.errorMessage ? { errorMessage: turnResult.errorMessage } : {},
7729
+ ...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : {},
7764
7730
  messages: reply.piMessages,
7765
7731
  modelId: reply.diagnostics.modelId,
7766
7732
  actor: executionActor,
7767
- surface: "slack"
7733
+ surface: options.execution?.surface ?? "slack",
7734
+ dispatchId: options.execution?.dispatch?.id
7768
7735
  });
7769
7736
  } else if (conversationId) {
7770
7737
  await recordAgentTurnSessionSummary({
@@ -7780,6 +7747,10 @@ function createReplyToThread(deps) {
7780
7747
  destination,
7781
7748
  destinationVisibility,
7782
7749
  source,
7750
+ surface: options.execution?.surface ?? "slack",
7751
+ dispatchId: options.execution?.dispatch?.id,
7752
+ dispatchOutcome: reply.diagnostics.outcome === "success" ? "completed" : "failed",
7753
+ ...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : {},
7783
7754
  traceId: getActiveTraceId()
7784
7755
  });
7785
7756
  }
@@ -7816,6 +7787,7 @@ function createReplyToThread(deps) {
7816
7787
  }
7817
7788
  preparedState.conversation = completedState.conversation;
7818
7789
  persistedAtLeastOnce = true;
7790
+ options.onTurnOutcome?.(turnResult);
7819
7791
  if (!lifecycleTerminalized && conversationId) {
7820
7792
  if (reply.diagnostics.outcome === "success") {
7821
7793
  await deps.services.turnLifecycle.complete({
@@ -7901,6 +7873,9 @@ function createReplyToThread(deps) {
7901
7873
  shouldPersistFailureState = true;
7902
7874
  const classifiedFailure = getConversationTurnBoundaryError(error);
7903
7875
  const failureCause = classifiedFailure?.cause ?? error;
7876
+ if (failureCause instanceof AuthorizationFlowDisabledError || failureCause instanceof PluginCredentialFailureError) {
7877
+ terminalDispatchFailureOutcome = "blocked";
7878
+ }
7904
7879
  const failureCode = classifiedFailure?.failureCode ?? boundaryFailureCode;
7905
7880
  const failureEventId = classifiedFailure?.eventId ?? logException(
7906
7881
  failureCause,
@@ -7914,8 +7889,31 @@ function createReplyToThread(deps) {
7914
7889
  after: latestArtifacts
7915
7890
  });
7916
7891
  if (createdCanvasUrl) {
7892
+ const dispatchOutcome = terminalDispatchFailureOutcome ?? "failed";
7893
+ const errorMessage = failureCause instanceof Error ? failureCause.message : "Agent turn failed after creating a canvas";
7917
7894
  const recoveryText = buildCanvasRecoveryReply(createdCanvasUrl);
7918
- await deliverAssistantMessage({ text: recoveryText });
7895
+ await deliverAssistantMessage(
7896
+ { text: recoveryText },
7897
+ dispatchOutcome
7898
+ );
7899
+ if (conversationId) {
7900
+ const sessionRecord = await getAgentTurnSessionRecord(
7901
+ conversationId,
7902
+ turnId
7903
+ );
7904
+ if (sessionRecord) {
7905
+ await failAgentTurnSessionRecord({
7906
+ conversationId,
7907
+ expectedVersion: sessionRecord.version,
7908
+ sessionId: turnId,
7909
+ errorMessage
7910
+ });
7911
+ }
7912
+ }
7913
+ options.onTurnOutcome?.({
7914
+ errorMessage,
7915
+ outcome: dispatchOutcome
7916
+ });
7919
7917
  markTurnClosed({
7920
7918
  conversation: preparedState.conversation,
7921
7919
  nowMs: Date.now(),
@@ -7970,6 +7968,11 @@ function createReplyToThread(deps) {
7970
7968
  destination,
7971
7969
  destinationVisibility,
7972
7970
  source,
7971
+ surface: options.execution?.surface ?? "slack",
7972
+ dispatchId: options.execution?.dispatch?.id,
7973
+ ...options.execution?.dispatch && terminalDispatchFailureOutcome ? {
7974
+ dispatchOutcome: terminalDispatchFailureOutcome
7975
+ } : {},
7973
7976
  traceId: getActiveTraceId()
7974
7977
  });
7975
7978
  const sessionRecord = await getAgentTurnSessionRecord(
@@ -8171,12 +8174,14 @@ function createPrepareTurnState(deps) {
8171
8174
  const conversation = coerceThreadConversationState(existingState);
8172
8175
  const conversationId = args.context.threadId ?? args.context.runId;
8173
8176
  await hydrateConversationMessages({ conversation, conversationId });
8174
- const channelConfiguration = getChannelConfigurationService(args.thread);
8177
+ const channelConfiguration = args.channelConfiguration ?? getChannelConfigurationService(args.thread);
8175
8178
  const configuration = await channelConfiguration.resolveValues();
8176
- await seedConversationBackfill(args.thread, conversation, {
8177
- messageId: args.message.id,
8178
- messageCreatedAtMs: args.message.metadata.dateSent.getTime()
8179
- });
8179
+ if (!args.skipBackfill) {
8180
+ await seedConversationBackfill(args.thread, conversation, {
8181
+ messageId: args.message.id,
8182
+ messageCreatedAtMs: args.message.metadata.dateSent.getTime()
8183
+ });
8184
+ }
8180
8185
  for (const queued of args.queuedMessages ?? []) {
8181
8186
  const queuedMessage = toConversationMessage({
8182
8187
  entry: queued.message,
@@ -8243,6 +8248,94 @@ function createPrepareTurnState(deps) {
8243
8248
  };
8244
8249
  }
8245
8250
 
8251
+ // src/chat/slack/dispatch-turn.ts
8252
+ import { Message as Message3, ThreadImpl as ThreadImpl3 } from "chat";
8253
+ function createSlackDispatchTurnRunner(options) {
8254
+ return async function runSlackDispatchTurn(dispatch, hooks) {
8255
+ const state = getStateAdapter();
8256
+ await state.connect();
8257
+ const conversationId = getDispatchConversationId(dispatch);
8258
+ const adapter = options.getSlackAdapter();
8259
+ const message = new Message3({
8260
+ id: getDispatchInputMessageId(dispatch.id),
8261
+ threadId: conversationId,
8262
+ text: dispatch.input,
8263
+ attachments: [],
8264
+ formatted: {
8265
+ type: "root",
8266
+ children: [
8267
+ {
8268
+ type: "paragraph",
8269
+ children: [{ type: "text", value: dispatch.input }]
8270
+ }
8271
+ ]
8272
+ },
8273
+ metadata: {
8274
+ dateSent: new Date(dispatch.createdAtMs),
8275
+ edited: false
8276
+ },
8277
+ raw: {
8278
+ channel: dispatch.destination.channelId,
8279
+ team: dispatch.destination.teamId
8280
+ },
8281
+ author: {
8282
+ userId: dispatch.actor.name,
8283
+ userName: dispatch.actor.name,
8284
+ fullName: dispatch.actor.name,
8285
+ isBot: true,
8286
+ isMe: false
8287
+ }
8288
+ });
8289
+ message.isMention = true;
8290
+ const thread = new ThreadImpl3({
8291
+ adapter,
8292
+ stateAdapter: state,
8293
+ id: conversationId,
8294
+ channelId: dispatch.destination.channelId,
8295
+ channelVisibility: dispatch.destinationVisibility === "public" ? "workspace" : "private",
8296
+ currentMessage: message,
8297
+ initialMessage: message,
8298
+ isDM: dispatch.destination.channelId.startsWith("D"),
8299
+ isSubscribedContext: true
8300
+ });
8301
+ let outcome;
8302
+ let errorMessage;
8303
+ let resultMessageTs;
8304
+ const routing = buildDispatchRoutingContext(dispatch);
8305
+ await options.replyToThread(thread, message, {
8306
+ ack: hooks.ack,
8307
+ destination: dispatch.destination,
8308
+ execution: {
8309
+ authorizationFlowMode: "disabled",
8310
+ channelConfiguration: options.getChannelConfiguration(
8311
+ dispatch.destination.channelId
8312
+ ),
8313
+ credentialContext: routing.credentialContext,
8314
+ destinationVisibility: dispatch.destinationVisibility,
8315
+ dispatch: routing.dispatch,
8316
+ skipProviderDefaultConfig: true,
8317
+ source: dispatch.source,
8318
+ surface: routing.surface,
8319
+ turnId: getDispatchTurnId(dispatch.id)
8320
+ },
8321
+ onTurnDeliveryAccepted: (messageId) => {
8322
+ resultMessageTs = messageId;
8323
+ },
8324
+ onTurnOutcome: (result) => {
8325
+ outcome = result.outcome;
8326
+ errorMessage = result.errorMessage;
8327
+ },
8328
+ shouldYield: hooks.shouldYield,
8329
+ skipBackfill: true
8330
+ });
8331
+ return {
8332
+ ...errorMessage ? { errorMessage } : {},
8333
+ ...outcome ? { outcome } : {},
8334
+ ...resultMessageTs ? { resultMessageTs } : {}
8335
+ };
8336
+ };
8337
+ }
8338
+
8246
8339
  // src/chat/app/factory.ts
8247
8340
  async function persistAssistantContextChannelId(args) {
8248
8341
  const currentArtifacts = coerceThreadArtifactsState(
@@ -8287,7 +8380,7 @@ function createSlackRuntime(options) {
8287
8380
  resolveUserAttachments: services.visionContext.resolveUserAttachments,
8288
8381
  services: services.replyExecutor
8289
8382
  });
8290
- return createSlackTurnRuntime({
8383
+ const runtime = createSlackTurnRuntime({
8291
8384
  assistantUserName: botConfig.userName,
8292
8385
  cancelEventSubscriptions: cancelSubscriptions,
8293
8386
  modelId: standardModelId(botConfig),
@@ -8406,6 +8499,14 @@ function createSlackRuntime(options) {
8406
8499
  });
8407
8500
  }
8408
8501
  });
8502
+ return {
8503
+ ...runtime,
8504
+ runDispatchTurn: createSlackDispatchTurnRunner({
8505
+ getChannelConfiguration: getChannelConfigurationServiceById,
8506
+ getSlackAdapter: options.getSlackAdapter,
8507
+ replyToThread
8508
+ })
8509
+ };
8409
8510
  }
8410
8511
 
8411
8512
  // src/chat/slack/adapter.ts
@@ -8464,7 +8565,7 @@ async function failSessionRecordBestEffort(args) {
8464
8565
  );
8465
8566
  }
8466
8567
  }
8467
- async function persistFailedReplyState(sessionRecord) {
8568
+ async function persistFailedReplyState(sessionRecord, errorMessage = "Paused agent run failed while continuing") {
8468
8569
  const currentState = await getPersistedThreadState(
8469
8570
  sessionRecord.conversationId
8470
8571
  );
@@ -8484,7 +8585,7 @@ async function persistFailedReplyState(sessionRecord) {
8484
8585
  });
8485
8586
  await failSessionRecordBestEffort({
8486
8587
  sessionRecord,
8487
- errorMessage: "Paused agent run failed while continuing"
8588
+ errorMessage
8488
8589
  });
8489
8590
  await persistThreadStateById(sessionRecord.conversationId, {
8490
8591
  conversation
@@ -8542,22 +8643,35 @@ async function failUnresumableContinuation(args) {
8542
8643
  sessionId: args.summary.sessionId,
8543
8644
  errorMessage: args.errorMessage
8544
8645
  });
8646
+ if (args.summary.dispatchId) {
8647
+ await recordAgentTurnSessionSummary({
8648
+ actor: args.summary.actor,
8649
+ conversationId: args.conversationId,
8650
+ destination: args.summary.destination,
8651
+ dispatchId: args.summary.dispatchId,
8652
+ dispatchOutcome: "failed",
8653
+ sessionId: args.summary.sessionId,
8654
+ sliceId: args.summary.sliceId,
8655
+ source: args.summary.source,
8656
+ state: "failed",
8657
+ surface: args.summary.surface
8658
+ });
8659
+ }
8545
8660
  }
8546
8661
  async function continueSlackAgentRun(payload, options, runOptions = {}) {
8547
8662
  const thread = parseSlackThreadId(payload.conversationId);
8548
- if (!thread) {
8549
- throw new Error(
8550
- `Agent continuation requires a Slack thread conversation id, got "${payload.conversationId}"`
8551
- );
8552
- }
8663
+ const destination = requireSlackDestination(
8664
+ payload.destination,
8665
+ "Agent continuation"
8666
+ );
8553
8667
  const scheduleAgentContinue2 = options.scheduleAgentContinue ?? scheduleAgentContinue;
8554
8668
  const resumeTurn = options.resumeTurn ?? resumeSlackTurn;
8555
8669
  return await resumeTurn({
8556
8670
  messageText: "",
8557
8671
  conversationId: payload.conversationId,
8558
8672
  turnId: payload.sessionId,
8559
- channelId: thread.channelId,
8560
- threadTs: thread.threadTs,
8673
+ channelId: thread?.channelId ?? destination.channelId,
8674
+ ...thread?.threadTs ? { threadTs: thread.threadTs } : {},
8561
8675
  lockKey: payload.conversationId,
8562
8676
  agentRunner: options.agentRunner,
8563
8677
  scheduleSessionCompletedPluginTasks: options.scheduleSessionCompletedPluginTasks,
@@ -8581,8 +8695,14 @@ async function continueSlackAgentRun(payload, options, runOptions = {}) {
8581
8695
  conversationId: payload.conversationId
8582
8696
  });
8583
8697
  const artifacts = coerceThreadArtifactsState(currentState);
8584
- const userMessage = getTurnUserMessage(conversation, payload.sessionId);
8585
- if (!userMessage?.author?.userId) {
8698
+ const dispatchId = activeSessionRecord.dispatchId ?? options.routingContext?.dispatch?.id;
8699
+ const dispatchUserMessage = dispatchId ? conversation.messages.find(
8700
+ (message) => message.role === "user" && options.inputMessageIds?.includes(message.id)
8701
+ ) : void 0;
8702
+ const userMessage = getTurnUserMessage(conversation, payload.sessionId) ?? dispatchUserMessage;
8703
+ const systemActor = activeSessionRecord.actor?.platform === "system" ? activeSessionRecord.actor : void 0;
8704
+ const userActorId = userMessage?.author?.userId;
8705
+ if (!userMessage || !systemActor && !userActorId) {
8586
8706
  throw new Error(
8587
8707
  `Unable to locate the persisted user message for agent continuation session "${payload.sessionId}"`
8588
8708
  );
@@ -8591,27 +8711,24 @@ async function continueSlackAgentRun(payload, options, runOptions = {}) {
8591
8711
  return false;
8592
8712
  }
8593
8713
  const channelConfiguration = getChannelConfigurationServiceById(
8594
- thread.channelId
8714
+ destination.channelId
8595
8715
  );
8596
- const conversationContext = buildConversationContext(conversation, {
8716
+ const conversationContext = dispatchId ? void 0 : buildConversationContext(conversation, {
8597
8717
  excludeMessageId: userMessage.id
8598
8718
  });
8599
8719
  const sandboxRef = getPersistedSandboxState(currentState);
8600
- const destination = requireSlackDestination(
8601
- payload.destination,
8602
- "Slack continuation"
8603
- );
8604
- const systemActor = activeSessionRecord.actor?.platform === "system" ? activeSessionRecord.actor : void 0;
8605
8720
  let actor;
8606
8721
  let credentialContext;
8607
- if (systemActor) {
8722
+ if (options.routingContext?.credentialContext) {
8723
+ credentialContext = options.routingContext.credentialContext;
8724
+ } else if (systemActor) {
8608
8725
  credentialContext = { actor: systemActor };
8609
8726
  } else {
8610
8727
  actor = await resolveContinuationActor({
8611
8728
  conversationId: payload.conversationId,
8612
8729
  sessionRecordActor: activeSessionRecord.actor,
8613
8730
  teamId: destination.teamId,
8614
- userId: userMessage.author.userId
8731
+ userId: userActorId
8615
8732
  });
8616
8733
  if (!actor) {
8617
8734
  await failStrandedSessionWithFallback({
@@ -8640,27 +8757,49 @@ async function continueSlackAgentRun(payload, options, runOptions = {}) {
8640
8757
  const turnMessages = activeSessionRecord.turnStartMessageIndex === void 0 ? [] : activeSessionRecord.piMessages.slice(
8641
8758
  activeSessionRecord.turnStartMessageIndex
8642
8759
  );
8760
+ const recordDispatchOutcome = async (dispatchOutcome) => {
8761
+ const dispatchId2 = options.routingContext?.dispatch?.id;
8762
+ if (!dispatchId2) {
8763
+ return;
8764
+ }
8765
+ await recordAgentTurnSessionSummary({
8766
+ conversationId: payload.conversationId,
8767
+ destination: payload.destination,
8768
+ destinationVisibility: options.routingContext?.destinationVisibility,
8769
+ dispatchId: dispatchId2,
8770
+ dispatchOutcome,
8771
+ sessionId: payload.sessionId,
8772
+ sliceId: activeSessionRecord.sliceId,
8773
+ source: activeSessionRecord.source,
8774
+ state: "failed",
8775
+ surface: options.routingContext?.surface ?? "slack"
8776
+ });
8777
+ };
8643
8778
  return {
8644
8779
  messageText: userMessage.text,
8780
+ sliceId: activeSessionRecord.sliceId,
8645
8781
  messageTs: getTurnUserSlackMessageTs(userMessage),
8646
8782
  inputMessageIds: [userMessage.id],
8647
8783
  initialStatus: latestReportedProgress(turnMessages),
8648
8784
  replyContext: {
8649
8785
  input: {
8650
- conversationContext,
8786
+ ...conversationContext ? { conversationContext } : {},
8651
8787
  // Pi history is SQL-authoritative: the resumed run reads its
8652
- // session record first and falls back to the step projection.
8653
- piMessages: await loadProjection({
8788
+ // exact dispatch session so unrelated conversation input cannot
8789
+ // gain system authority. Interactive turns retain their merged
8790
+ // projection so queued steering remains visible.
8791
+ piMessages: dispatchId ? activeSessionRecord.piMessages : await loadProjection({
8654
8792
  conversationId: payload.conversationId
8655
8793
  }),
8656
8794
  ...getTurnUserReplyAttachmentContext(userMessage)
8657
8795
  },
8658
8796
  routing: {
8797
+ ...options.routingContext,
8659
8798
  credentialContext,
8660
- ...actor ? { actor } : {},
8799
+ ...options.routingContext?.actor ?? actor ? { actor: options.routingContext?.actor ?? actor } : {},
8661
8800
  destination: payload.destination,
8662
8801
  source: activeSessionRecord.source,
8663
- toolChannelId: artifacts.assistantContextChannelId ?? thread.channelId
8802
+ toolChannelId: artifacts.assistantContextChannelId ?? destination.channelId
8664
8803
  },
8665
8804
  policy: {
8666
8805
  channelConfiguration
@@ -8686,8 +8825,12 @@ async function continueSlackAgentRun(payload, options, runOptions = {}) {
8686
8825
  reply
8687
8826
  });
8688
8827
  },
8689
- onFailure: async () => {
8690
- await persistFailedReplyState(activeSessionRecord);
8828
+ onFailure: async (error) => {
8829
+ await persistFailedReplyState(
8830
+ activeSessionRecord,
8831
+ error instanceof Error ? error.message : String(error)
8832
+ );
8833
+ await recordDispatchOutcome("failed");
8691
8834
  },
8692
8835
  onPostDeliveryCommitFailure: async () => {
8693
8836
  await failAgentTurnSessionRecord({
@@ -8696,12 +8839,14 @@ async function continueSlackAgentRun(payload, options, runOptions = {}) {
8696
8839
  sessionId: activeSessionRecord.sessionId,
8697
8840
  errorMessage: "Continued agent reply was delivered but completion state did not persist"
8698
8841
  });
8842
+ await recordDispatchOutcome("failed");
8699
8843
  },
8700
8844
  onAuthPause: async () => {
8701
8845
  await persistAuthPauseTurnState({
8702
8846
  sessionId: payload.sessionId,
8703
8847
  threadStateId: payload.conversationId
8704
8848
  });
8849
+ await recordDispatchOutcome("blocked");
8705
8850
  logWarn(
8706
8851
  "agent_continue_reparked_for_auth",
8707
8852
  {},
@@ -8739,6 +8884,20 @@ async function failStrandedSessionWithFallback(args) {
8739
8884
  sessionId: args.sessionRecord.sessionId,
8740
8885
  errorMessage: args.errorMessage
8741
8886
  });
8887
+ if (args.sessionRecord.dispatchId) {
8888
+ await recordAgentTurnSessionSummary({
8889
+ actor: args.sessionRecord.actor,
8890
+ conversationId: args.conversationId,
8891
+ destination: args.sessionRecord.destination,
8892
+ dispatchId: args.sessionRecord.dispatchId,
8893
+ dispatchOutcome: "failed",
8894
+ sessionId: args.sessionRecord.sessionId,
8895
+ sliceId: args.sessionRecord.sliceId,
8896
+ source: args.sessionRecord.source,
8897
+ state: "failed",
8898
+ surface: args.sessionRecord.surface
8899
+ });
8900
+ }
8742
8901
  const currentState = await getPersistedThreadState(args.conversationId);
8743
8902
  const conversation = coerceThreadConversationState(currentState);
8744
8903
  await hydrateConversationMessages({
@@ -8757,10 +8916,6 @@ async function failStrandedSessionWithFallback(args) {
8757
8916
  updateConversationStats
8758
8917
  });
8759
8918
  await persistThreadStateById(args.conversationId, { conversation });
8760
- const thread = parseSlackThreadId(args.conversationId);
8761
- if (!thread) {
8762
- return;
8763
- }
8764
8919
  const eventName = "agent_turn_stranded_session_failed";
8765
8920
  const eventId = logException(
8766
8921
  new Error(args.errorMessage),
@@ -8772,9 +8927,14 @@ async function failStrandedSessionWithFallback(args) {
8772
8927
  },
8773
8928
  "Stranded running agent session terminally failed"
8774
8929
  );
8930
+ const thread = parseSlackThreadId(args.conversationId);
8931
+ const channelId = thread?.channelId ?? requireSlackDestination(
8932
+ args.sessionRecord.destination,
8933
+ "Stranded agent continuation"
8934
+ ).channelId;
8775
8935
  await postSlackMessage({
8776
- channelId: thread.channelId,
8777
- threadTs: thread.threadTs,
8936
+ channelId,
8937
+ ...thread?.threadTs ? { threadTs: thread.threadTs } : {},
8778
8938
  text: buildTurnFailureResponse(
8779
8939
  requireTurnFailureEventId(eventId, eventName)
8780
8940
  )
@@ -8984,21 +9144,41 @@ function createProductionConversationWorkOptions(options) {
8984
9144
  getSlackAdapter: getProductionSlackAdapter,
8985
9145
  services
8986
9146
  });
8987
- return {
9147
+ const queue = getVercelConversationWorkQueue();
9148
+ const slackWorker = createSlackConversationWorker({
9149
+ getSlackAdapter: getProductionSlackAdapter,
8988
9150
  conversationStore,
8989
- queue: getVercelConversationWorkQueue(),
8990
- run: createSlackConversationWorker({
8991
- getSlackAdapter: getProductionSlackAdapter,
8992
- conversationStore,
8993
- resumeAwaitingContinuation: async (conversationId, runOptions) => await resumeAwaitingSlackContinuation(
8994
- conversationId,
9151
+ resumeAwaitingContinuation: async (conversationId, runOptions) => await resumeAwaitingSlackContinuation(
9152
+ conversationId,
9153
+ {
9154
+ agentRunner,
9155
+ scheduleSessionCompletedPluginTasks: services.replyExecutor?.scheduleSessionCompletedPluginTasks
9156
+ },
9157
+ runOptions
9158
+ ),
9159
+ runtime
9160
+ });
9161
+ const dispatchWorker = createAgentDispatchConversationWorker({
9162
+ resumeTurn: async (dispatch, hooks) => {
9163
+ await resumeAwaitingSlackContinuation(
9164
+ getDispatchConversationId(dispatch),
8995
9165
  {
8996
9166
  agentRunner,
9167
+ inputMessageIds: getDispatchInputMessageIds(dispatch.id),
9168
+ routingContext: buildDispatchRoutingContext(dispatch),
8997
9169
  scheduleSessionCompletedPluginTasks: services.replyExecutor?.scheduleSessionCompletedPluginTasks
8998
9170
  },
8999
- runOptions
9000
- ),
9001
- runtime
9171
+ { shouldYield: hooks.shouldYield }
9172
+ );
9173
+ },
9174
+ runTurn: runtime.runDispatchTurn
9175
+ });
9176
+ return {
9177
+ conversationStore,
9178
+ queue,
9179
+ run: createAgentDispatchWorkRouter({
9180
+ dispatchWorker,
9181
+ fallbackWorker: slackWorker
9002
9182
  })
9003
9183
  };
9004
9184
  }
@@ -9501,7 +9681,9 @@ async function createApp(options) {
9501
9681
  return receiveLocalOAuthCredential(c.req.raw);
9502
9682
  });
9503
9683
  app.post("/api/internal/agent-dispatch", (c) => {
9504
- return POST(c.req.raw, waitUntil, { agentRunner });
9684
+ return POST(c.req.raw, waitUntil, {
9685
+ conversationWorkQueue: getVercelConversationWorkQueue()
9686
+ });
9505
9687
  });
9506
9688
  let agentContinuePOST;
9507
9689
  let pluginTaskPOST;