@sentry/junior 0.84.1 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/{agent-hooks-TFPY6V3T.js → agent-hooks-NWOUB3UR.js} +7 -7
  2. package/dist/app.js +916 -363
  3. package/dist/chat/conversation-privacy.d.ts +12 -2
  4. package/dist/chat/conversations/sql/migrations.d.ts +1 -1
  5. package/dist/chat/conversations/sql/store.d.ts +9 -1
  6. package/dist/chat/conversations/store.d.ts +13 -0
  7. package/dist/chat/current-instruction.d.ts +6 -0
  8. package/dist/chat/ingress/message-changed.d.ts +2 -0
  9. package/dist/chat/respond-helpers.d.ts +1 -2
  10. package/dist/chat/respond.d.ts +7 -1
  11. package/dist/chat/runtime/reply-executor.d.ts +2 -10
  12. package/dist/chat/runtime/slack-runtime.d.ts +5 -4
  13. package/dist/chat/runtime/turn.d.ts +7 -0
  14. package/dist/chat/services/persist-retry.d.ts +2 -0
  15. package/dist/chat/services/turn-session-record.d.ts +31 -2
  16. package/dist/chat/slack/client.d.ts +33 -2
  17. package/dist/chat/slack/conversation-context.d.ts +15 -0
  18. package/dist/chat/state/turn-session.d.ts +9 -0
  19. package/dist/chat/task-execution/queue.d.ts +1 -1
  20. package/dist/chat/task-execution/slack-work.d.ts +10 -2
  21. package/dist/chat/task-execution/state.d.ts +44 -4
  22. package/dist/chat/task-execution/store.d.ts +21 -4
  23. package/dist/chat/task-execution/worker.d.ts +11 -4
  24. package/dist/chat/tools/slack/channel-access.d.ts +29 -0
  25. package/dist/chat/tools/slack/thread-read.d.ts +4 -1
  26. package/dist/{chunk-AL6ZFV7U.js → chunk-2NFV5FMB.js} +4 -4
  27. package/dist/{chunk-JK7376UT.js → chunk-37B2R2QJ.js} +17 -17
  28. package/dist/{chunk-QWKB6NJV.js → chunk-6I6HBOQM.js} +80 -15
  29. package/dist/{chunk-SBYMRDH7.js → chunk-6O5UI3RG.js} +1 -1
  30. package/dist/{chunk-CWMMGUWM.js → chunk-BRSQQRG6.js} +1 -1
  31. package/dist/{chunk-FPN7NYTE.js → chunk-ENPSU7L7.js} +165 -27
  32. package/dist/{chunk-T2YRJZ5A.js → chunk-FPHA6GCQ.js} +905 -711
  33. package/dist/{chunk-MJ4E2PK7.js → chunk-GB5DFM4D.js} +1 -1
  34. package/dist/{chunk-BAPRSWNW.js → chunk-GGD6WK6V.js} +124 -25
  35. package/dist/{chunk-SVFA43LT.js → chunk-JRXCSSSU.js} +19 -4
  36. package/dist/{chunk-DR75T7J3.js → chunk-L7OHKDOX.js} +9 -5
  37. package/dist/{chunk-LSG6PQF4.js → chunk-RIB3M6YA.js} +2 -2
  38. package/dist/{chunk-ALL7GG6U.js → chunk-ZU2ALUVQ.js} +45 -6
  39. package/dist/cli/chat.js +4 -4
  40. package/dist/cli/plugins.js +7 -7
  41. package/dist/cli/snapshot-warmup.js +4 -4
  42. package/dist/cli/upgrade.js +10 -7
  43. package/dist/{db-ELRCNB4A.js → db-SZVUU7RB.js} +4 -4
  44. package/dist/handlers/github-webhook/issue-comment.d.ts +3 -0
  45. package/dist/handlers/github-webhook/pull-request-review-comment.d.ts +3 -0
  46. package/dist/instrumentation.js +1 -1
  47. package/dist/nitro.js +4 -4
  48. package/dist/reporting/conversations.d.ts +2 -1
  49. package/dist/reporting.js +110 -34
  50. package/dist/{runner-2XU6QQND.js → runner-HEBRPNN2.js} +27 -13
  51. package/package.json +5 -5
package/dist/app.js CHANGED
@@ -3,14 +3,17 @@ import {
3
3
  PluginCredentialFailureError,
4
4
  SANDBOX_EGRESS_PROXY_PATH,
5
5
  TurnInputCommitLostError,
6
+ TurnInputDeferredError,
6
7
  addReactionToMessage,
7
8
  appendSlackLegacyAttachmentText,
8
9
  applyPendingAuthUpdate,
9
10
  buildConversationContext,
10
11
  buildDeliveredTurnStatePatch,
11
12
  buildDeterministicTurnId,
13
+ buildSteeringPiMessage,
12
14
  clearPendingAuth,
13
15
  coerceThreadArtifactsState,
16
+ completeDeliveredTurn,
14
17
  createConversationMemoryService,
15
18
  createUserTokenStore,
16
19
  deleteMcpAuthSession,
@@ -18,12 +21,14 @@ import {
18
21
  deliverResourceEventSubscription,
19
22
  estimateTextTokens,
20
23
  executeCredentialedEgressRequest,
24
+ finalizeFailedTurnReply,
21
25
  finalizeMcpAuthorization,
22
26
  findMatchingResourceEventSubscriptions,
23
27
  formatProviderLabel,
24
28
  generateAssistantReply,
25
29
  generateConversationId,
26
30
  getAgentContextCompactionTriggerTokens,
31
+ getAgentTurnDiagnosticsAttributes,
27
32
  getChannelConfigurationService,
28
33
  getChannelConfigurationServiceById,
29
34
  getConfigDefaults,
@@ -40,6 +45,7 @@ import {
40
45
  isRedundantReactionAckText,
41
46
  isRetryableTurnError,
42
47
  isTurnInputCommitLostError,
48
+ isTurnInputDeferredError,
43
49
  listThreadReplies,
44
50
  markConversationMessage,
45
51
  markTurnClosed,
@@ -49,12 +55,15 @@ import {
49
55
  normalizeSandboxEgressTracePropagationDomains,
50
56
  parseOAuthStatePayload,
51
57
  parseSandboxEgressCredentialToken,
58
+ persistCompletedSessionRecord,
52
59
  persistThreadState,
53
60
  persistThreadStateById,
61
+ persistYieldSessionRecord,
54
62
  postSlackMessage,
55
63
  processPluginTask,
56
64
  removeReactionFromMessage,
57
65
  renderSlackLegacyAttachmentText,
66
+ requireTurnFailureEventId,
58
67
  resolveBaseUrl,
59
68
  resolvePluginOAuthAccount,
60
69
  resolveSandboxEgressProviderForHost,
@@ -68,37 +77,41 @@ import {
68
77
  updateConversationStats,
69
78
  uploadFilesToThread,
70
79
  upsertConversationMessage
71
- } from "./chunk-T2YRJZ5A.js";
80
+ } from "./chunk-FPHA6GCQ.js";
72
81
  import {
73
82
  CONVERSATION_WORK_CHECK_IN_INTERVAL_MS,
74
83
  CONVERSATION_WORK_STALE_ENQUEUE_MS,
84
+ ackMessages,
75
85
  appendInboundMessage,
76
86
  checkInConversationWork,
77
87
  clearConsumedConversationWake,
78
88
  clearExpiredConversationLease,
79
89
  completeConversationWork,
90
+ deadLetterAttempt,
80
91
  drainConversationMailbox,
81
92
  getConversation,
82
93
  getConversationWorkState,
94
+ isFinalAttempt,
95
+ isInvalidConversationRecordError,
83
96
  listActiveConversationIds,
84
- markConversationMessagesInjected,
85
97
  markConversationWorkEnqueued,
98
+ recordAttemptFailure,
86
99
  releaseConversationWork,
87
100
  removeActiveConversation,
88
101
  requestConversationContinuation,
89
102
  requestConversationWork,
90
103
  startConversationWork
91
- } from "./chunk-FPN7NYTE.js";
104
+ } from "./chunk-ENPSU7L7.js";
92
105
  import {
93
106
  JUNIOR_THREAD_STATE_TTL_MS,
94
107
  coerceThreadConversationState
95
108
  } from "./chunk-66NX7MNW.js";
96
- import "./chunk-LSG6PQF4.js";
109
+ import "./chunk-RIB3M6YA.js";
97
110
  import {
98
111
  getVercelConversationWorkQueue,
99
112
  resolveConversationWorkQueueTopic,
100
113
  verifyConversationQueueMessage
101
- } from "./chunk-SBYMRDH7.js";
114
+ } from "./chunk-6O5UI3RG.js";
102
115
  import {
103
116
  PLUGIN_TASK_QUEUE_TOPIC,
104
117
  createVercelQueueClient,
@@ -107,19 +120,18 @@ import {
107
120
  import {
108
121
  GET,
109
122
  buildSentryConversationUrl,
123
+ conversationVisibilityFromSlackChannelType,
110
124
  initConversationContext,
111
125
  resolveSlackChannelTypeFromMessage,
112
126
  resolveSlackConversationContext,
113
127
  setConversationTitle
114
- } from "./chunk-SVFA43LT.js";
128
+ } from "./chunk-JRXCSSSU.js";
115
129
  import {
116
130
  abandonAgentTurnSessionRecord,
117
131
  buildSlackOutputMessage,
118
132
  commitMessages,
119
- escapeXml,
120
133
  failAgentTurnSessionRecord,
121
134
  getAgentTurnSessionRecord,
122
- getInterruptionMarker,
123
135
  listAgentTurnSessionSummariesForConversation,
124
136
  loadProjection,
125
137
  normalizeSlackStatusText,
@@ -127,7 +139,7 @@ import {
127
139
  recordAuthorizationCompleted,
128
140
  splitSlackReplyText,
129
141
  truncateStatusText
130
- } from "./chunk-JK7376UT.js";
142
+ } from "./chunk-37B2R2QJ.js";
131
143
  import {
132
144
  validatePluginEgressCredentialHooks,
133
145
  validatePluginRegistrations
@@ -147,20 +159,20 @@ import {
147
159
  setPlugins,
148
160
  validatePlugins,
149
161
  verifySlackDirectCredentialSubject
150
- } from "./chunk-AL6ZFV7U.js";
162
+ } from "./chunk-2NFV5FMB.js";
151
163
  import {
152
164
  createPluginLogger,
153
165
  createPluginState
154
- } from "./chunk-CWMMGUWM.js";
166
+ } from "./chunk-BRSQQRG6.js";
155
167
  import {
156
168
  getConversationStore,
157
169
  getDb
158
- } from "./chunk-QWKB6NJV.js";
170
+ } from "./chunk-6I6HBOQM.js";
159
171
  import "./chunk-G3E7SCME.js";
160
172
  import {
161
173
  acquireActiveLock,
162
174
  getStateAdapter
163
- } from "./chunk-MJ4E2PK7.js";
175
+ } from "./chunk-GB5DFM4D.js";
164
176
  import {
165
177
  SlackActionError,
166
178
  createSlackDestination,
@@ -168,18 +180,21 @@ import {
168
180
  downloadPrivateSlackFile,
169
181
  getHeaderString,
170
182
  getSlackClient,
183
+ getWorkspaceTeamId,
171
184
  isDmChannel,
172
185
  normalizeSlackConversationId,
173
186
  parseDestination,
174
187
  requireSlackDestination,
188
+ runWithSlackInstallationToken,
189
+ runWithWorkspaceTeamId,
175
190
  sameDestination
176
- } from "./chunk-BAPRSWNW.js";
191
+ } from "./chunk-GGD6WK6V.js";
177
192
  import {
178
193
  FUNCTION_TIMEOUT_BUFFER_SECONDS,
179
- GEN_AI_PROVIDER_NAME,
180
194
  botConfig,
181
195
  completeObject,
182
196
  completeText,
197
+ escapeXml,
183
198
  getChatConfig,
184
199
  getRuntimeMetadata,
185
200
  getSlackBotToken,
@@ -191,13 +206,14 @@ import {
191
206
  normalizeSlackEmojiName,
192
207
  setSlackReactionConfig,
193
208
  stripRuntimeTurnContext,
194
- trimTrailingAssistantMessages
195
- } from "./chunk-ALL7GG6U.js";
209
+ trimTrailingAssistantMessages,
210
+ unwrapCurrentInstruction
211
+ } from "./chunk-ZU2ALUVQ.js";
196
212
  import {
197
213
  parseSlackThreadId,
198
214
  resolveSlackChannelIdFromMessage,
199
215
  resolveSlackChannelIdFromThreadId
200
- } from "./chunk-DR75T7J3.js";
216
+ } from "./chunk-L7OHKDOX.js";
201
217
  import {
202
218
  discoverSkills
203
219
  } from "./chunk-64ACM6AP.js";
@@ -494,96 +510,25 @@ async function postSlackApiReplyPosts(args) {
494
510
  return lastPostedMessageTs;
495
511
  }
496
512
 
497
- // src/chat/services/turn-failure-response.ts
498
- function requireTurnFailureEventId(eventId, eventName) {
499
- if (!eventId) {
500
- throw new Error(`Sentry did not return an event ID for ${eventName}`);
501
- }
502
- return eventId;
503
- }
504
- function getExecutionFailureReason(reply) {
505
- const errorMessage = reply.diagnostics.errorMessage?.trim();
506
- if (errorMessage) {
507
- return errorMessage;
508
- }
509
- if (reply.diagnostics.toolErrorCount > 0) {
510
- return `${reply.diagnostics.toolErrorCount} tool result error(s)`;
511
- }
512
- if (reply.diagnostics.assistantMessageCount > 0) {
513
- return "assistant returned no text";
514
- }
515
- return "empty assistant turn";
516
- }
517
- function getFailureCapture(reply) {
518
- if (reply.diagnostics.outcome === "provider_error") {
519
- return {
520
- eventName: "agent_turn_provider_error",
521
- error: reply.diagnostics.providerError ?? new Error(
522
- reply.diagnostics.errorMessage ?? "Provider error without explicit message"
523
- ),
524
- attributes: {},
525
- body: "Agent turn failed with provider error"
526
- };
527
- }
528
- const failureReason = getExecutionFailureReason(reply);
529
- return {
530
- eventName: "agent_turn_execution_failure",
531
- error: new Error(`Agent turn execution failure: ${failureReason}`),
532
- attributes: {
533
- "app.ai.execution_failure_reason": failureReason
534
- },
535
- body: "Agent turn completed with execution failure"
536
- };
537
- }
538
- function getAgentTurnDiagnosticsAttributes(reply) {
539
- return {
540
- "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
541
- "gen_ai.operation.name": "invoke_agent",
542
- "app.ai.outcome": reply.diagnostics.outcome,
543
- "app.ai.assistant_messages": reply.diagnostics.assistantMessageCount,
544
- "app.ai.tool_results": reply.diagnostics.toolResultCount,
545
- "app.ai.tool_error_results": reply.diagnostics.toolErrorCount,
546
- "app.ai.tool_call_count": reply.diagnostics.toolCalls.length,
547
- "app.ai.used_primary_text": reply.diagnostics.usedPrimaryText,
548
- ...reply.diagnostics.thinkingLevel ? {
549
- "app.ai.reasoning_effort": reply.diagnostics.thinkingLevel
550
- } : {},
551
- ...reply.diagnostics.stopReason ? {
552
- "gen_ai.response.finish_reasons": [reply.diagnostics.stopReason]
553
- } : {},
554
- ...reply.diagnostics.errorMessage ? { "exception.message": reply.diagnostics.errorMessage } : {}
555
- };
513
+ // src/chat/services/persist-retry.ts
514
+ var PERSIST_ATTEMPTS = 3;
515
+ function sleep(ms) {
516
+ return new Promise((resolve) => setTimeout(resolve, ms));
556
517
  }
557
- function finalizeFailedTurnReply(args) {
558
- if (args.reply.diagnostics.outcome === "success") {
559
- return args.reply;
560
- }
561
- const capture = getFailureCapture(args.reply);
562
- const eventId = requireTurnFailureEventId(
563
- args.logException(
564
- capture.error,
565
- capture.eventName,
566
- args.context,
567
- {
568
- ...getAgentTurnDiagnosticsAttributes(args.reply),
569
- ...args.attributes,
570
- ...capture.attributes
571
- },
572
- capture.body
573
- ),
574
- capture.eventName
575
- );
576
- const providerPartialText = args.reply.diagnostics.outcome === "provider_error" ? args.reply.text.trim() : "";
577
- return {
578
- ...args.reply,
579
- text: providerPartialText ? `${providerPartialText}${getInterruptionMarker()}` : buildTurnFailureResponse(eventId),
580
- deliveryMode: "thread",
581
- deliveryPlan: {
582
- mode: "thread",
583
- postThreadText: true,
584
- attachFiles: args.reply.files && args.reply.files.length > 0 ? "inline" : "none"
518
+ async function persistWithRetry(persist) {
519
+ let lastError;
520
+ for (let attempt = 1; attempt <= PERSIST_ATTEMPTS; attempt += 1) {
521
+ try {
522
+ await persist();
523
+ return;
524
+ } catch (error) {
525
+ lastError = error;
526
+ if (attempt < PERSIST_ATTEMPTS) {
527
+ await sleep(attempt * 100);
528
+ }
585
529
  }
586
- };
530
+ }
531
+ throw lastError;
587
532
  }
588
533
 
589
534
  // src/chat/agent-dispatch/signing.ts
@@ -1034,6 +979,16 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1034
979
  }
1035
980
  let dispatch = claimedDispatch;
1036
981
  const conversationId = getDispatchConversationId(dispatch);
982
+ const turnId = getDispatchTurnId(dispatch.id);
983
+ const logContext = {
984
+ conversationId,
985
+ slackThreadId: conversationId,
986
+ slackChannelId: dispatch.destination.channelId,
987
+ runId: dispatch.id,
988
+ actorType: dispatch.actor.type,
989
+ actorId: dispatch.actor.id,
990
+ assistantUserName: botConfig.userName
991
+ };
1037
992
  const destinationLockId = getDispatchDestinationLockId(dispatch.destination);
1038
993
  const stateAdapter = getStateAdapter();
1039
994
  await stateAdapter.connect();
@@ -1118,7 +1073,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1118
1073
  correlation: {
1119
1074
  conversationId,
1120
1075
  threadId: conversationId,
1121
- turnId: getDispatchTurnId(dispatch.id),
1076
+ turnId,
1122
1077
  runId: dispatch.id,
1123
1078
  channelId: dispatch.destination.channelId,
1124
1079
  teamId: dispatch.destination.teamId
@@ -1158,13 +1113,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1158
1113
  reply,
1159
1114
  logException,
1160
1115
  context: {
1161
- conversationId,
1162
- slackThreadId: conversationId,
1163
- slackChannelId: dispatch.destination.channelId,
1164
- runId: dispatch.id,
1165
- actorType: dispatch.actor.type,
1166
- actorId: dispatch.actor.id,
1167
- assistantUserName: botConfig.userName,
1116
+ ...logContext,
1168
1117
  modelId: reply.diagnostics.modelId
1169
1118
  }
1170
1119
  });
@@ -1198,13 +1147,45 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1198
1147
  });
1199
1148
  updateConversationStats(conversation);
1200
1149
  const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : artifacts;
1201
- await persistRuntimePatch({
1202
- threadId: conversationId,
1203
- conversation,
1204
- artifacts: nextArtifacts,
1205
- sandboxId: reply.sandboxId ?? sandboxId,
1206
- sandboxDependencyProfileHash: reply.sandboxDependencyProfileHash ?? sandboxDependencyProfileHash
1207
- });
1150
+ try {
1151
+ await persistWithRetry(
1152
+ () => persistRuntimePatch({
1153
+ threadId: conversationId,
1154
+ conversation,
1155
+ artifacts: nextArtifacts,
1156
+ sandboxId: reply.sandboxId ?? sandboxId,
1157
+ sandboxDependencyProfileHash: reply.sandboxDependencyProfileHash ?? sandboxDependencyProfileHash
1158
+ })
1159
+ );
1160
+ } catch (persistError) {
1161
+ logException(
1162
+ persistError,
1163
+ "agent_dispatch_post_delivery_persist_failed",
1164
+ logContext,
1165
+ {},
1166
+ "Failed to persist delivered dispatch state after Slack accepted the reply"
1167
+ );
1168
+ }
1169
+ if (reply.piMessages?.length) {
1170
+ await completeDeliveredTurn({
1171
+ conversationId,
1172
+ sessionId: turnId,
1173
+ sliceId: 1,
1174
+ messages: reply.piMessages,
1175
+ durationMs: reply.diagnostics.durationMs,
1176
+ usage: reply.diagnostics.usage,
1177
+ destination: dispatch.destination,
1178
+ source: dispatch.source,
1179
+ surface: "api",
1180
+ logContext: {
1181
+ threadId: conversationId,
1182
+ channelId: dispatch.destination.channelId,
1183
+ runId: dispatch.id,
1184
+ assistantUserName: botConfig.userName,
1185
+ modelId: reply.diagnostics.modelId
1186
+ }
1187
+ });
1188
+ }
1208
1189
  dispatch = await markDispatch({
1209
1190
  dispatch,
1210
1191
  status: failure ? "failed" : "completed",
@@ -1215,21 +1196,13 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1215
1196
  try {
1216
1197
  await scheduleCompletedTasks({
1217
1198
  conversationId,
1218
- sessionId: getDispatchTurnId(dispatch.id)
1199
+ sessionId: turnId
1219
1200
  });
1220
1201
  } catch (error) {
1221
1202
  logException(
1222
1203
  error,
1223
1204
  "plugin_session_completed_task_schedule_failed",
1224
- {
1225
- conversationId,
1226
- slackThreadId: conversationId,
1227
- slackChannelId: dispatch.destination.channelId,
1228
- runId: dispatch.id,
1229
- actorType: dispatch.actor.type,
1230
- actorId: dispatch.actor.id,
1231
- assistantUserName: botConfig.userName
1232
- },
1205
+ logContext,
1233
1206
  {},
1234
1207
  "Plugin session.completed task scheduling failed"
1235
1208
  );
@@ -1278,13 +1251,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1278
1251
  error,
1279
1252
  "agent_dispatch_run_failed",
1280
1253
  {
1281
- conversationId,
1282
- slackThreadId: conversationId,
1283
- slackChannelId: dispatch.destination.channelId,
1284
- runId: dispatch.id,
1285
- actorType: dispatch.actor.type,
1286
- actorId: dispatch.actor.id,
1287
- assistantUserName: botConfig.userName,
1254
+ ...logContext,
1288
1255
  modelId: botConfig.modelId
1289
1256
  },
1290
1257
  {},
@@ -1505,8 +1472,8 @@ async function drainConversationMailbox2(args) {
1505
1472
  }
1506
1473
  return result;
1507
1474
  }
1508
- async function markConversationMessagesInjected2(args) {
1509
- const result = await markConversationMessagesInjected(args);
1475
+ async function ackMessages2(args) {
1476
+ const result = await ackMessages(args);
1510
1477
  await recordExecutionMetadata(args);
1511
1478
  return result;
1512
1479
  }
@@ -1525,6 +1492,18 @@ async function completeConversationWork2(args) {
1525
1492
  await recordExecutionMetadata(args);
1526
1493
  return result;
1527
1494
  }
1495
+ async function recordAttemptFailure2(args) {
1496
+ const result = await recordAttemptFailure(args);
1497
+ if (result.status === "recorded") {
1498
+ await recordExecutionMetadata(args);
1499
+ }
1500
+ return result;
1501
+ }
1502
+ async function deadLetterAttempt2(args) {
1503
+ const result = await deadLetterAttempt(args);
1504
+ await recordExecutionMetadata(args);
1505
+ return result;
1506
+ }
1528
1507
  async function clearExpiredConversationLease2(args) {
1529
1508
  const result = await clearExpiredConversationLease(args);
1530
1509
  await recordExecutionMetadata(args);
@@ -1592,6 +1571,10 @@ async function ensureSlackAdapterInitialized(args) {
1592
1571
  await args.adapter.initialize({
1593
1572
  getState: () => state
1594
1573
  });
1574
+ const internals = args.adapter;
1575
+ if (internals.defaultBotTokenProvider && !args.adapter.botUserId) {
1576
+ throw new Error("Slack adapter initialized without a resolved bot user id");
1577
+ }
1595
1578
  initializedAdapters.add(args.adapter);
1596
1579
  }
1597
1580
  function verifySlackSignature(args) {
@@ -1639,7 +1622,7 @@ async function runWithSlackInstallation(args) {
1639
1622
  enterpriseId: args.installation.enterpriseId,
1640
1623
  isEnterpriseInstall: args.installation.isEnterpriseInstall
1641
1624
  },
1642
- args.task
1625
+ () => runWithSlackInstallationToken(tokenContext.token, args.task)
1643
1626
  );
1644
1627
  }
1645
1628
 
@@ -2013,14 +1996,22 @@ function createSlackConversationWorker(options) {
2013
1996
  const actorLookup = options.lookupSlackUser ?? lookupSlackUser;
2014
1997
  const state = getConnectedState2(options.state);
2015
1998
  await state.connect();
2016
- const records = getPendingRecords(
2017
- await getConversationWorkState2({
2018
- conversationId: context.conversationId,
2019
- state
2020
- })
2021
- );
1999
+ const records = getPendingRecords({
2000
+ execution: { pendingMessages: [...context.attempt.messages] }
2001
+ });
2022
2002
  if (records.length === 0) {
2023
- await options.resumeAwaitingContinuation(context.conversationId);
2003
+ const destination = requireSlackDestination(
2004
+ context.destination,
2005
+ "Slack continuation recovery"
2006
+ );
2007
+ await runWithSlackInstallation({
2008
+ adapter,
2009
+ installation: { teamId: destination.teamId },
2010
+ state,
2011
+ task: async () => {
2012
+ await options.resumeAwaitingContinuation(context.conversationId);
2013
+ }
2014
+ });
2024
2015
  return { status: "completed" };
2025
2016
  }
2026
2017
  const latestRecord = records[records.length - 1];
@@ -2070,33 +2061,22 @@ function createSlackConversationWorker(options) {
2070
2061
  skipped,
2071
2062
  totalSinceLastHandler: messages.length
2072
2063
  };
2073
- const initialInboundMessageIds = records.map(
2074
- (record) => record.inboundMessageId
2075
- );
2076
- let initialMessagesPersisted = false;
2077
- const markInitialMessagesInjected = async () => {
2078
- if (initialMessagesPersisted) {
2079
- return true;
2064
+ let initialMessagesAcked = false;
2065
+ const ack = async () => {
2066
+ if (initialMessagesAcked) {
2067
+ return;
2080
2068
  }
2081
- const marked = await markConversationMessagesInjected2({
2082
- conversationId: context.conversationId,
2083
- inboundMessageIds: initialInboundMessageIds,
2084
- leaseToken: context.leaseToken,
2085
- conversationStore: options.conversationStore,
2086
- state
2087
- });
2088
- initialMessagesPersisted = marked;
2089
- return marked;
2090
- };
2091
- const onInputCommitted = async () => {
2092
- if (!await markInitialMessagesInjected()) {
2069
+ try {
2070
+ await context.attempt.ack();
2071
+ initialMessagesAcked = true;
2072
+ } catch {
2093
2073
  throw new TurnInputCommitLostError(
2094
- `Conversation work lease lost before Slack input commit for ${context.conversationId}`
2074
+ `Conversation work lease lost before Slack inbox ack for ${context.conversationId}`
2095
2075
  );
2096
2076
  }
2097
2077
  };
2098
- const drainSteeringMessages = async (inject) => {
2099
- await context.drainMailbox(async (pendingRecords) => {
2078
+ const drainSteeringMessages = async (accept) => {
2079
+ await context.attempt.drain(async (pendingRecords) => {
2100
2080
  const messages2 = pendingRecords.map((record) => {
2101
2081
  const metadata = record.input.metadata;
2102
2082
  if (!isSlackMetadata(metadata)) {
@@ -2111,7 +2091,7 @@ function createSlackConversationWorker(options) {
2111
2091
  message
2112
2092
  };
2113
2093
  });
2114
- return await inject(messages2);
2094
+ return await accept(messages2);
2115
2095
  });
2116
2096
  };
2117
2097
  try {
@@ -2120,7 +2100,8 @@ function createSlackConversationWorker(options) {
2120
2100
  destination: context.destination,
2121
2101
  messageContext,
2122
2102
  drainSteeringMessages,
2123
- onInputCommitted,
2103
+ ack,
2104
+ isFinalAttempt: context.attempt.isFinalAttempt,
2124
2105
  shouldYield: context.shouldYield
2125
2106
  });
2126
2107
  return;
@@ -2129,10 +2110,14 @@ function createSlackConversationWorker(options) {
2129
2110
  destination: context.destination,
2130
2111
  messageContext,
2131
2112
  drainSteeringMessages,
2132
- onInputCommitted,
2113
+ ack,
2114
+ isFinalAttempt: context.attempt.isFinalAttempt,
2133
2115
  shouldYield: context.shouldYield
2134
2116
  });
2135
2117
  } catch (error) {
2118
+ if (isTurnInputDeferredError(error)) {
2119
+ return { status: "deferred" };
2120
+ }
2136
2121
  if (isCooperativeTurnYieldError(error)) {
2137
2122
  return { status: "yielded" };
2138
2123
  }
@@ -2143,7 +2128,7 @@ function createSlackConversationWorker(options) {
2143
2128
  }
2144
2129
  }
2145
2130
  });
2146
- if (turnResult?.status === "yielded" || turnResult?.status === "lost_lease") {
2131
+ if (turnResult?.status === "deferred" || turnResult?.status === "yielded" || turnResult?.status === "lost_lease") {
2147
2132
  return turnResult;
2148
2133
  }
2149
2134
  return { status: "completed" };
@@ -2349,16 +2334,58 @@ function normalizeGitHubCheckSuiteEvents(deliveryId, body) {
2349
2334
  });
2350
2335
  }
2351
2336
 
2352
- // src/handlers/github-webhook/pull-request.ts
2337
+ // src/handlers/github-webhook/issue-comment.ts
2353
2338
  import { z as z4 } from "zod";
2354
- var pullRequestWebhookSchema = z4.object({
2339
+ var issueCommentWebhookSchema = z4.object({
2355
2340
  action: z4.string(),
2356
- pull_request: z4.object({
2357
- merged: z4.boolean().optional(),
2358
- number: z4.number()
2359
- }),
2341
+ comment: z4.object({
2342
+ body: z4.string(),
2343
+ user: z4.object({
2344
+ login: z4.string().optional()
2345
+ }).strip().optional()
2346
+ }).strip(),
2347
+ issue: z4.object({
2348
+ number: z4.number(),
2349
+ pull_request: z4.object({
2350
+ url: z4.string().min(1)
2351
+ }).strip().optional()
2352
+ }).strip(),
2360
2353
  repository: z4.object({
2361
2354
  full_name: z4.string().min(1)
2355
+ }).strip()
2356
+ }).strip();
2357
+ function normalizeGitHubIssueCommentEvent(deliveryId, body) {
2358
+ const parsed = issueCommentWebhookSchema.safeParse(body);
2359
+ if (!parsed.success || parsed.data.action !== "created" || !parsed.data.issue.pull_request) {
2360
+ return void 0;
2361
+ }
2362
+ const eventType = "comment.created";
2363
+ const resource = gitHubPullRequestResource({
2364
+ pullRequestNumber: parsed.data.issue.number,
2365
+ repositoryFullName: parsed.data.repository.full_name
2366
+ });
2367
+ const author = parsed.data.comment.user?.login;
2368
+ return {
2369
+ eventKey: gitHubEventKey(deliveryId, eventType),
2370
+ eventType,
2371
+ occurredAtMs: Date.now(),
2372
+ provider: "github",
2373
+ resourceRef: resource.resourceRef,
2374
+ trustedSummary: `${resource.label} received a comment${author ? ` from ${author}` : ""}.`,
2375
+ untrustedText: parsed.data.comment.body
2376
+ };
2377
+ }
2378
+
2379
+ // src/handlers/github-webhook/pull-request.ts
2380
+ import { z as z5 } from "zod";
2381
+ var pullRequestWebhookSchema = z5.object({
2382
+ action: z5.string(),
2383
+ pull_request: z5.object({
2384
+ merged: z5.boolean().optional(),
2385
+ number: z5.number()
2386
+ }),
2387
+ repository: z5.object({
2388
+ full_name: z5.string().min(1)
2362
2389
  })
2363
2390
  });
2364
2391
  function normalizeGitHubPullRequestEvent(deliveryId, body) {
@@ -2382,21 +2409,60 @@ function normalizeGitHubPullRequestEvent(deliveryId, body) {
2382
2409
  };
2383
2410
  }
2384
2411
 
2412
+ // src/handlers/github-webhook/pull-request-review-comment.ts
2413
+ import { z as z6 } from "zod";
2414
+ var pullRequestReviewCommentWebhookSchema = z6.object({
2415
+ action: z6.string(),
2416
+ comment: z6.object({
2417
+ body: z6.string(),
2418
+ user: z6.object({
2419
+ login: z6.string().optional()
2420
+ }).strip().optional()
2421
+ }).strip(),
2422
+ pull_request: z6.object({
2423
+ number: z6.number()
2424
+ }).strip(),
2425
+ repository: z6.object({
2426
+ full_name: z6.string().min(1)
2427
+ }).strip()
2428
+ }).strip();
2429
+ function normalizeGitHubPullRequestReviewCommentEvent(deliveryId, body) {
2430
+ const parsed = pullRequestReviewCommentWebhookSchema.safeParse(body);
2431
+ if (!parsed.success || parsed.data.action !== "created") {
2432
+ return void 0;
2433
+ }
2434
+ const eventType = "review_comment.created";
2435
+ const resource = gitHubPullRequestResource({
2436
+ pullRequestNumber: parsed.data.pull_request.number,
2437
+ repositoryFullName: parsed.data.repository.full_name
2438
+ });
2439
+ const author = parsed.data.comment.user?.login;
2440
+ return {
2441
+ eventKey: gitHubEventKey(deliveryId, eventType),
2442
+ eventType,
2443
+ occurredAtMs: Date.now(),
2444
+ provider: "github",
2445
+ resourceRef: resource.resourceRef,
2446
+ trustedSummary: `${resource.label} received an inline review comment${author ? ` from ${author}` : ""}.`,
2447
+ untrustedText: parsed.data.comment.body
2448
+ };
2449
+ }
2450
+
2385
2451
  // src/handlers/github-webhook/pull-request-review.ts
2386
- import { z as z5 } from "zod";
2387
- var pullRequestReviewWebhookSchema = z5.object({
2388
- action: z5.string(),
2389
- pull_request: z5.object({
2390
- number: z5.number()
2452
+ import { z as z7 } from "zod";
2453
+ var pullRequestReviewWebhookSchema = z7.object({
2454
+ action: z7.string(),
2455
+ pull_request: z7.object({
2456
+ number: z7.number()
2391
2457
  }),
2392
- repository: z5.object({
2393
- full_name: z5.string().min(1)
2458
+ repository: z7.object({
2459
+ full_name: z7.string().min(1)
2394
2460
  }),
2395
- review: z5.object({
2396
- body: z5.string().optional().nullable(),
2397
- state: z5.string(),
2398
- user: z5.object({
2399
- login: z5.string().optional()
2461
+ review: z7.object({
2462
+ body: z7.string().optional().nullable(),
2463
+ state: z7.string(),
2464
+ user: z7.object({
2465
+ login: z7.string().optional()
2400
2466
  }).optional()
2401
2467
  })
2402
2468
  });
@@ -2406,7 +2472,7 @@ function normalizeGitHubPullRequestReviewEvent(deliveryId, body) {
2406
2472
  return void 0;
2407
2473
  }
2408
2474
  const reviewState = parsed.data.review.state.toUpperCase();
2409
- const eventType = reviewState === "APPROVED" ? "review.approved" : reviewState === "CHANGES_REQUESTED" ? "review.changes_requested" : void 0;
2475
+ const eventType = reviewState === "APPROVED" ? "review.approved" : reviewState === "CHANGES_REQUESTED" ? "review.changes_requested" : reviewState === "COMMENTED" ? "review.commented" : void 0;
2410
2476
  if (!eventType) {
2411
2477
  return void 0;
2412
2478
  }
@@ -2421,7 +2487,7 @@ function normalizeGitHubPullRequestReviewEvent(deliveryId, body) {
2421
2487
  occurredAtMs: Date.now(),
2422
2488
  provider: "github",
2423
2489
  resourceRef: resource.resourceRef,
2424
- trustedSummary: eventType === "review.approved" ? `${resource.label} was approved${reviewer ? ` by ${reviewer}` : ""}.` : `${resource.label} received requested changes${reviewer ? ` from ${reviewer}` : ""}.`,
2490
+ trustedSummary: eventType === "review.approved" ? `${resource.label} was approved${reviewer ? ` by ${reviewer}` : ""}.` : eventType === "review.changes_requested" ? `${resource.label} received requested changes${reviewer ? ` from ${reviewer}` : ""}.` : `${resource.label} received a review comment${reviewer ? ` from ${reviewer}` : ""}.`,
2425
2491
  untrustedText: parsed.data.review.body ?? void 0
2426
2492
  };
2427
2493
  }
@@ -2457,6 +2523,20 @@ function normalizeGitHubResourceEvents(args) {
2457
2523
  );
2458
2524
  return event ? [event] : [];
2459
2525
  }
2526
+ case "issue_comment": {
2527
+ const event = normalizeGitHubIssueCommentEvent(
2528
+ args.deliveryId,
2529
+ args.body
2530
+ );
2531
+ return event ? [event] : [];
2532
+ }
2533
+ case "pull_request_review_comment": {
2534
+ const event = normalizeGitHubPullRequestReviewCommentEvent(
2535
+ args.deliveryId,
2536
+ args.body
2537
+ );
2538
+ return event ? [event] : [];
2539
+ }
2460
2540
  case "check_suite":
2461
2541
  return normalizeGitHubCheckSuiteEvents(args.deliveryId, args.body);
2462
2542
  default:
@@ -2520,11 +2600,29 @@ async function recoverConversationWork(args) {
2520
2600
  state: args.state
2521
2601
  });
2522
2602
  for (const conversationId of ids) {
2603
+ let work;
2523
2604
  try {
2524
- const work = await getConversationWorkState2({
2605
+ work = await getConversationWorkState2({
2525
2606
  conversationId,
2526
2607
  state: args.state
2527
2608
  });
2609
+ } catch (error) {
2610
+ logException(
2611
+ error,
2612
+ "conversation_work_recovery_failed",
2613
+ { conversationId },
2614
+ {},
2615
+ "Conversation work heartbeat recovery failed"
2616
+ );
2617
+ if (isInvalidConversationRecordError(error)) {
2618
+ await removeActiveConversation2({
2619
+ conversationId,
2620
+ state: args.state
2621
+ });
2622
+ }
2623
+ continue;
2624
+ }
2625
+ try {
2528
2626
  if (!work) {
2529
2627
  await removeActiveConversation2({
2530
2628
  conversationId,
@@ -3433,17 +3531,6 @@ function isSlackTitlePermissionError(error) {
3433
3531
  return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
3434
3532
  }
3435
3533
 
3436
- // src/chat/slack/workspace-context.ts
3437
- import { AsyncLocalStorage } from "async_hooks";
3438
- var workspaceTeamIdStorage = new AsyncLocalStorage();
3439
- function runWithWorkspaceTeamId(teamId, fn) {
3440
- if (!teamId) return fn();
3441
- return workspaceTeamIdStorage.run(teamId, fn);
3442
- }
3443
- function getWorkspaceTeamId() {
3444
- return workspaceTeamIdStorage.getStore();
3445
- }
3446
-
3447
3534
  // src/chat/runtime/thread-context.ts
3448
3535
  function escapeRegExp(value) {
3449
3536
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3638,8 +3725,8 @@ function buildAuthPauseResponse(slackUserId, providerDisplayName) {
3638
3725
  }
3639
3726
 
3640
3727
  // src/chat/runtime/request-deadline.ts
3641
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
3642
- var requestDeadlineStorage = new AsyncLocalStorage2();
3728
+ import { AsyncLocalStorage } from "async_hooks";
3729
+ var requestDeadlineStorage = new AsyncLocalStorage();
3643
3730
  function createTurnRequestDeadline(startedAtMs) {
3644
3731
  const requestBudgetMs = Math.max(
3645
3732
  1,
@@ -3918,6 +4005,27 @@ async function resumeSlackTurn(args) {
3918
4005
  footer
3919
4006
  });
3920
4007
  finalReplyDelivered = true;
4008
+ if (replyContext.correlation?.conversationId && replyContext.correlation.turnId && reply.piMessages?.length) {
4009
+ await persistCompletedSessionRecord({
4010
+ conversationId: replyContext.correlation.conversationId,
4011
+ sessionId: replyContext.correlation.turnId,
4012
+ allMessages: reply.piMessages,
4013
+ currentDurationMs: reply.diagnostics.durationMs,
4014
+ currentUsage: reply.diagnostics.usage,
4015
+ destination: replyContext.destination,
4016
+ source: replyContext.source,
4017
+ requester: replyContext.requester,
4018
+ surface: "slack",
4019
+ logContext: {
4020
+ threadId: replyContext.correlation.threadId,
4021
+ requesterId: replyContext.requester?.userId,
4022
+ channelId: runArgs.channelId,
4023
+ runId: replyContext.correlation.runId,
4024
+ assistantUserName: botConfig.userName,
4025
+ modelId: reply.diagnostics.modelId
4026
+ }
4027
+ });
4028
+ }
3921
4029
  await runArgs.onSuccess?.(reply);
3922
4030
  if (reply.diagnostics.outcome === "success" && replyContext.correlation?.conversationId && replyContext.correlation.turnId) {
3923
4031
  try {
@@ -5679,14 +5787,18 @@ function extractMessageChangedMention(body, botUserId, adapter) {
5679
5787
  const teamId = typeof body.team_id === "string" ? body.team_id : void 0;
5680
5788
  const userTeam = typeof event.message.user_team === "string" ? event.message.user_team : void 0;
5681
5789
  const sourceTeam = typeof event.message.source_team === "string" ? event.message.source_team : void 0;
5790
+ const botId = typeof event.message.bot_id === "string" ? event.message.bot_id : void 0;
5791
+ const channelType = typeof event.channel_type === "string" ? event.channel_type : void 0;
5682
5792
  const raw = {
5683
5793
  channel: channelId,
5794
+ ...channelType ? { channel_type: channelType } : {},
5684
5795
  ts: messageTs,
5685
5796
  thread_ts: threadTs,
5686
5797
  user: userId,
5687
5798
  ...teamId ? { team_id: teamId } : {},
5688
5799
  ...userTeam ? { user_team: userTeam } : {},
5689
- ...sourceTeam ? { source_team: sourceTeam } : {}
5800
+ ...sourceTeam ? { source_team: sourceTeam } : {},
5801
+ ...botId ? { bot_id: botId } : {}
5690
5802
  };
5691
5803
  const message = new Message2({
5692
5804
  id: getEditedMentionMessageId(messageTs),
@@ -5702,8 +5814,11 @@ function extractMessageChangedMention(body, botUserId, adapter) {
5702
5814
  // Raw message_changed payloads do not include profile fields.
5703
5815
  userName: "",
5704
5816
  fullName: "",
5705
- isBot: false,
5706
- isMe: false
5817
+ // Mirror the fresh-message parse so the shared ingress author gate
5818
+ // applies: synthesized flags must never launder a bot-authored or
5819
+ // self-authored edit into a routable user message.
5820
+ isBot: Boolean(botId),
5821
+ isMe: userId === botUserId
5707
5822
  }
5708
5823
  });
5709
5824
  Object.defineProperty(message, "adapter", {
@@ -5959,14 +6074,16 @@ async function handleMessageChanged(args) {
5959
6074
  }
5960
6075
  const botUserId = args.adapter.botUserId;
5961
6076
  if (!botUserId) {
5962
- return false;
6077
+ throw new Error(
6078
+ "Slack bot identity is unresolved; cannot classify message_changed event"
6079
+ );
5963
6080
  }
5964
6081
  const result = extractMessageChangedMention(
5965
6082
  args.body,
5966
6083
  botUserId,
5967
6084
  args.adapter
5968
6085
  );
5969
- if (!result) {
6086
+ if (!result || shouldIgnoreMessage(result.message)) {
5970
6087
  return true;
5971
6088
  }
5972
6089
  await persistSlackMessage({
@@ -6278,12 +6395,12 @@ async function handleSlackWebhook(args) {
6278
6395
  try {
6279
6396
  await eventTask;
6280
6397
  } catch (error) {
6281
- if (!(error instanceof SlackEventPersistenceError)) {
6282
- logException(error, "slack_event_enqueue_failed");
6283
- return new Response("ok", { status: 200 });
6398
+ if (error instanceof SlackEventPersistenceError) {
6399
+ logException(error.cause, "slack_event_persist_failed");
6400
+ } else {
6401
+ logException(error, "slack_event_routing_failed");
6284
6402
  }
6285
- logException(error.cause, "slack_event_persist_failed");
6286
- return new Response("Slack event persistence failed", { status: 503 });
6403
+ return new Response("Slack event handling failed", { status: 503 });
6287
6404
  }
6288
6405
  } else {
6289
6406
  enqueue(
@@ -6433,6 +6550,33 @@ async function requestLostLeaseRecovery(args) {
6433
6550
  state: args.options.state
6434
6551
  });
6435
6552
  }
6553
+ async function recordFailedDeliveryAttempt(args) {
6554
+ const failure = await recordAttemptFailure2({
6555
+ conversationId: args.conversationId,
6556
+ inboundMessageIds: args.messageIds,
6557
+ leaseToken: args.leaseToken,
6558
+ conversationStore: args.options.conversationStore,
6559
+ nowMs: args.nowMs,
6560
+ state: args.options.state
6561
+ });
6562
+ for (const message of failure.deadLetteredMessages) {
6563
+ logWarn(
6564
+ "conversation_work_dead_lettered",
6565
+ { conversationId: args.conversationId },
6566
+ {
6567
+ "app.conversation.source": message.source,
6568
+ "app.inbound.attempt_count": message.attemptCount ?? 0,
6569
+ "app.inbound.message_id": message.inboundMessageId,
6570
+ "app.inbound.pending_count": failure.pendingCount
6571
+ },
6572
+ "Conversation work message consumed after exceeding the delivery attempt limit"
6573
+ );
6574
+ }
6575
+ return failure;
6576
+ }
6577
+ function isTerminalFailure(failure) {
6578
+ return failure.status === "recorded" && failure.deadLetteredMessages.length > 0 && failure.pendingCount === 0;
6579
+ }
6436
6580
  function startLeaseCheckIn(args) {
6437
6581
  const timer = setInterval(() => {
6438
6582
  const nowMs = now2(args.options);
@@ -6470,10 +6614,22 @@ function startLeaseCheckIn(args) {
6470
6614
  }
6471
6615
  async function processConversationWork(message, options) {
6472
6616
  const conversationId = message.conversationId;
6473
- const initial = await getConversationWorkState2({
6474
- conversationId,
6475
- state: options.state
6476
- });
6617
+ let initial;
6618
+ try {
6619
+ initial = await getConversationWorkState2({
6620
+ conversationId,
6621
+ state: options.state
6622
+ });
6623
+ } catch (error) {
6624
+ if (isInvalidConversationRecordError(error)) {
6625
+ throw new ConversationQueueMessageRejectedError(
6626
+ "invalid_record",
6627
+ `Conversation record failed validation for ${conversationId}`,
6628
+ { conversationId }
6629
+ );
6630
+ }
6631
+ throw error;
6632
+ }
6477
6633
  if (!initial || countPendingConversationMessages(initial) === 0 && initial.execution.status === "idle" && !initial.execution.lease) {
6478
6634
  if (initial) {
6479
6635
  await clearConsumedConversationWake2({
@@ -6532,6 +6688,14 @@ async function processConversationWork(message, options) {
6532
6688
  }
6533
6689
  const startedAtMs = now2(options);
6534
6690
  const softYieldDeadlineMs = startedAtMs + (options.softYieldAfterMs ?? CONVERSATION_WORK_SOFT_YIELD_AFTER_MS);
6691
+ const leasedWork = await getConversationWorkState2({
6692
+ conversationId,
6693
+ state: options.state
6694
+ });
6695
+ const attemptMessages = leasedWork?.messages ?? initial.messages;
6696
+ const attemptMessageIds = attemptMessages.map(
6697
+ (message2) => message2.inboundMessageId
6698
+ );
6535
6699
  let leaseLost = false;
6536
6700
  const markLeaseLost = () => {
6537
6701
  leaseLost = true;
@@ -6551,10 +6715,43 @@ async function processConversationWork(message, options) {
6551
6715
  },
6552
6716
  "Conversation work lease acquired"
6553
6717
  );
6718
+ const drainInbox = (handle) => drainConversationMailbox2({
6719
+ conversationId,
6720
+ leaseToken: lease.leaseToken,
6721
+ conversationStore: options.conversationStore,
6722
+ handle,
6723
+ nowMs: now2(options),
6724
+ state: options.state
6725
+ });
6726
+ const ack = async () => {
6727
+ const acknowledged = await ackMessages2({
6728
+ conversationId,
6729
+ inboundMessageIds: attemptMessageIds,
6730
+ leaseToken: lease.leaseToken,
6731
+ conversationStore: options.conversationStore,
6732
+ nowMs: now2(options),
6733
+ state: options.state
6734
+ });
6735
+ if (!acknowledged) {
6736
+ markLeaseLost();
6737
+ throw new Error(
6738
+ `Conversation work lease lost before inbox ack for ${conversationId}`
6739
+ );
6740
+ }
6741
+ };
6554
6742
  const workerContext = {
6743
+ attempt: {
6744
+ ack,
6745
+ conversationId,
6746
+ destination,
6747
+ drain: drainInbox,
6748
+ isFinalAttempt: attemptMessages.some(
6749
+ (message2) => isFinalAttempt(message2)
6750
+ ),
6751
+ messages: attemptMessages
6752
+ },
6555
6753
  conversationId,
6556
6754
  destination,
6557
- leaseToken: lease.leaseToken,
6558
6755
  shouldYield: () => leaseLost || now2(options) >= softYieldDeadlineMs,
6559
6756
  checkIn: async () => {
6560
6757
  const checkedIn = await checkInConversationWork2({
@@ -6568,15 +6765,7 @@ async function processConversationWork(message, options) {
6568
6765
  markLeaseLost();
6569
6766
  }
6570
6767
  return checkedIn;
6571
- },
6572
- drainMailbox: (inject) => drainConversationMailbox2({
6573
- conversationId,
6574
- leaseToken: lease.leaseToken,
6575
- conversationStore: options.conversationStore,
6576
- inject,
6577
- nowMs: now2(options),
6578
- state: options.state
6579
- })
6768
+ }
6580
6769
  };
6581
6770
  try {
6582
6771
  const result = await options.run(workerContext);
@@ -6643,6 +6832,51 @@ async function processConversationWork(message, options) {
6643
6832
  );
6644
6833
  return { status: "yielded" };
6645
6834
  }
6835
+ if (result.status === "deferred") {
6836
+ const deferredNowMs = now2(options);
6837
+ const released = await releaseConversationWork2({
6838
+ conversationId,
6839
+ leaseToken: lease.leaseToken,
6840
+ conversationStore: options.conversationStore,
6841
+ nowMs: deferredNowMs,
6842
+ state: options.state
6843
+ });
6844
+ if (!released) {
6845
+ return { status: "lost_lease" };
6846
+ }
6847
+ const wake = await ensureConversationWake({
6848
+ conversationId,
6849
+ conversationStore: options.conversationStore,
6850
+ idempotencyKey: nudgeIdempotencyKey(
6851
+ "deferred",
6852
+ conversationId,
6853
+ deferredNowMs
6854
+ ),
6855
+ nowMs: deferredNowMs,
6856
+ queue: options.queue,
6857
+ state: options.state
6858
+ });
6859
+ return wake.status === "enqueued" ? { status: "pending_requeued" } : { status: "completed" };
6860
+ }
6861
+ if (attemptMessageIds.length > 0) {
6862
+ const failure = await recordFailedDeliveryAttempt({
6863
+ conversationId,
6864
+ leaseToken: lease.leaseToken,
6865
+ nowMs: now2(options),
6866
+ messageIds: attemptMessageIds,
6867
+ options
6868
+ });
6869
+ if (isTerminalFailure(failure)) {
6870
+ await deadLetterAttempt2({
6871
+ conversationId,
6872
+ leaseToken: lease.leaseToken,
6873
+ conversationStore: options.conversationStore,
6874
+ nowMs: now2(options),
6875
+ state: options.state
6876
+ });
6877
+ return { status: "failed" };
6878
+ }
6879
+ }
6646
6880
  const completion = await completeConversationWork2({
6647
6881
  conversationId,
6648
6882
  leaseToken: lease.leaseToken,
@@ -6680,53 +6914,62 @@ async function processConversationWork(message, options) {
6680
6914
  return { status: "completed" };
6681
6915
  } catch (error) {
6682
6916
  const errorNowMs = now2(options);
6917
+ let recoveryRecorded = false;
6683
6918
  try {
6684
- const continuationMarked = await requestConversationContinuation2({
6919
+ const failure = attemptMessageIds.length > 0 ? await recordFailedDeliveryAttempt({
6685
6920
  conversationId,
6686
- destination,
6687
6921
  leaseToken: lease.leaseToken,
6688
- conversationStore: options.conversationStore,
6689
6922
  nowMs: errorNowMs,
6690
- state: options.state
6691
- });
6692
- if (continuationMarked) {
6693
- await ensureConversationWake({
6923
+ messageIds: attemptMessageIds,
6924
+ options
6925
+ }) : void 0;
6926
+ if (failure && isTerminalFailure(failure)) {
6927
+ await deadLetterAttempt2({
6694
6928
  conversationId,
6929
+ leaseToken: lease.leaseToken,
6695
6930
  conversationStore: options.conversationStore,
6696
- idempotencyKey: nudgeIdempotencyKey(
6697
- "error",
6931
+ nowMs: errorNowMs,
6932
+ state: options.state
6933
+ });
6934
+ } else {
6935
+ const continuationMarked = await requestConversationContinuation2({
6936
+ conversationId,
6937
+ destination,
6938
+ leaseToken: lease.leaseToken,
6939
+ conversationStore: options.conversationStore,
6940
+ nowMs: errorNowMs,
6941
+ state: options.state
6942
+ });
6943
+ if (continuationMarked) {
6944
+ await ensureConversationWake({
6698
6945
  conversationId,
6699
- errorNowMs
6700
- ),
6946
+ conversationStore: options.conversationStore,
6947
+ idempotencyKey: nudgeIdempotencyKey(
6948
+ "error",
6949
+ conversationId,
6950
+ errorNowMs
6951
+ ),
6952
+ nowMs: errorNowMs,
6953
+ queue: options.queue,
6954
+ state: options.state
6955
+ });
6956
+ }
6957
+ await releaseConversationWork2({
6958
+ conversationId,
6959
+ leaseToken: lease.leaseToken,
6960
+ conversationStore: options.conversationStore,
6701
6961
  nowMs: errorNowMs,
6702
- queue: options.queue,
6703
6962
  state: options.state
6704
6963
  });
6705
6964
  }
6706
- } catch (requeueError) {
6965
+ recoveryRecorded = true;
6966
+ } catch (recoveryError) {
6707
6967
  logException(
6708
- requeueError,
6968
+ recoveryError,
6709
6969
  "conversation_work_requeue_failed",
6710
6970
  { conversationId },
6711
6971
  {},
6712
- "Conversation work requeue failed after runner error"
6713
- );
6714
- }
6715
- try {
6716
- await releaseConversationWork2({
6717
- conversationId,
6718
- leaseToken: lease.leaseToken,
6719
- conversationStore: options.conversationStore,
6720
- nowMs: errorNowMs,
6721
- state: options.state
6722
- });
6723
- } catch (releaseError) {
6724
- logException(
6725
- releaseError,
6726
- "conversation_work_release_failed",
6727
- { conversationId },
6728
- {},
6729
- "Conversation work release failed after runner error"
6972
+ "Conversation work recovery failed after runner error"
6730
6973
  );
6731
6974
  }
6732
6975
  if (!isProviderRetryError(error)) {
@@ -6740,7 +6983,10 @@ async function processConversationWork(message, options) {
6740
6983
  "Conversation work failed"
6741
6984
  );
6742
6985
  }
6743
- throw error;
6986
+ if (!recoveryRecorded) {
6987
+ throw error;
6988
+ }
6989
+ return { status: "failed" };
6744
6990
  } finally {
6745
6991
  clearInterval(timer);
6746
6992
  }
@@ -6913,14 +7159,14 @@ function registerVercelPluginTaskDevConsumer() {
6913
7159
  }
6914
7160
 
6915
7161
  // src/chat/services/subscribed-decision.ts
6916
- import { z as z6 } from "zod";
6917
- var replyDecisionSchema = z6.object({
6918
- should_reply: z6.boolean().describe("Whether Junior should respond to this thread message."),
6919
- should_unsubscribe: z6.boolean().describe(
7162
+ import { z as z8 } from "zod";
7163
+ var replyDecisionSchema = z8.object({
7164
+ should_reply: z8.boolean().describe("Whether Junior should respond to this thread message."),
7165
+ should_unsubscribe: z8.boolean().describe(
6920
7166
  "Whether Junior should unsubscribe from this thread because the user clearly asked it to stop participating."
6921
7167
  ),
6922
- confidence: z6.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
6923
- reason: z6.string().optional().describe("Short reason for the decision.")
7168
+ confidence: z8.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
7169
+ reason: z8.string().optional().describe("Short reason for the decision.")
6924
7170
  }).strict();
6925
7171
  var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
6926
7172
  var ROUTER_CLASSIFIER_MAX_TOKENS = 240;
@@ -7264,7 +7510,7 @@ function combineTurnText(queuedMessages, latestText) {
7264
7510
  // src/chat/runtime/slack-runtime.ts
7265
7511
  var THREAD_OPTOUT_ACK = "Understood. I'll stay out of this thread unless someone @mentions me again.";
7266
7512
  function shouldRethrowTurnControlError(error) {
7267
- return isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error) || isProviderRetryError(error);
7513
+ return isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error) || isTurnInputDeferredError(error) || isProviderRetryError(error);
7268
7514
  }
7269
7515
  async function maybeHandleThreadOptOutDecision(args) {
7270
7516
  if (!args.decision?.shouldUnsubscribe) {
@@ -7298,13 +7544,13 @@ function createAcceptedSteeringDrain(hooks, options) {
7298
7544
  if (!hooks.drainSteeringMessages) {
7299
7545
  return void 0;
7300
7546
  }
7301
- return async (inject, context) => {
7547
+ return async (accept, context) => {
7302
7548
  let interruptedMessages;
7303
7549
  await hooks.drainSteeringMessages(async (messages) => {
7304
7550
  const selection = await options.selectMessages(messages, context);
7305
7551
  await options.onSkipped?.(selection.skipped);
7306
7552
  const interrupted = selection.accepted.filter((accepted) => accepted.mode === "interrupt").map((accepted) => accepted.message);
7307
- await inject(getQueuedMessagesFromSlackMessages(interrupted, options));
7553
+ await accept(getQueuedMessagesFromSlackMessages(interrupted, options));
7308
7554
  interruptedMessages = interrupted;
7309
7555
  await options.onAcceptedForProcessing?.(interrupted);
7310
7556
  return [
@@ -7471,7 +7717,7 @@ function createSlackTurnRuntime(deps) {
7471
7717
  completedAtMs
7472
7718
  });
7473
7719
  }
7474
- await args.onInputCommitted?.();
7720
+ await args.ack?.();
7475
7721
  };
7476
7722
  const selectAcceptedSteeringMessages = async (args) => {
7477
7723
  const selected = [];
@@ -7557,6 +7803,7 @@ function createSlackTurnRuntime(deps) {
7557
7803
  let processingReaction;
7558
7804
  const skippedSteeringMessages = [];
7559
7805
  let completed = false;
7806
+ let acked = false;
7560
7807
  const onTurnCompleted = async () => {
7561
7808
  completed = true;
7562
7809
  await flushSkippedSteeringMessagesBestEffort({
@@ -7605,8 +7852,9 @@ function createSlackTurnRuntime(deps) {
7605
7852
  )
7606
7853
  );
7607
7854
  };
7608
- const onInputCommitted = async () => {
7609
- await hooks.onInputCommitted?.();
7855
+ const ack = async () => {
7856
+ await hooks.ack?.();
7857
+ acked = true;
7610
7858
  await startQueuedProcessingReactions();
7611
7859
  };
7612
7860
  const drainSteeringMessages = createAcceptedSteeringDrain(hooks, {
@@ -7636,7 +7884,7 @@ function createSlackTurnRuntime(deps) {
7636
7884
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
7637
7885
  destination: hooks.destination,
7638
7886
  queuedMessages,
7639
- onInputCommitted,
7887
+ ack,
7640
7888
  onToolInvocation: toolInvocationHook,
7641
7889
  onTurnCompleted,
7642
7890
  drainSteeringMessages,
@@ -7690,6 +7938,9 @@ function createSlackTurnRuntime(deps) {
7690
7938
  {},
7691
7939
  "onNewMention failed"
7692
7940
  );
7941
+ if (!acked && hooks.isFinalAttempt === false) {
7942
+ return;
7943
+ }
7693
7944
  if (!eventId) {
7694
7945
  throw new Error(
7695
7946
  "Sentry did not return an event ID for mention_handler_failed"
@@ -7727,6 +7978,7 @@ function createSlackTurnRuntime(deps) {
7727
7978
  let processingReaction;
7728
7979
  const skippedSteeringMessages = [];
7729
7980
  let completed = false;
7981
+ let acked = false;
7730
7982
  const onTurnCompleted = async () => {
7731
7983
  completed = true;
7732
7984
  await flushSkippedSteeringMessagesBestEffort({
@@ -7793,7 +8045,7 @@ function createSlackTurnRuntime(deps) {
7793
8045
  message,
7794
8046
  decision: { shouldReply: false, reason },
7795
8047
  context: threadContext,
7796
- onInputCommitted: hooks.onInputCommitted,
8048
+ ack: hooks.ack,
7797
8049
  text: combinedText
7798
8050
  });
7799
8051
  return;
@@ -7830,7 +8082,7 @@ function createSlackTurnRuntime(deps) {
7830
8082
  message,
7831
8083
  decision,
7832
8084
  context: threadContext,
7833
- onInputCommitted: hooks.onInputCommitted,
8085
+ ack: hooks.ack,
7834
8086
  preparedState,
7835
8087
  text: combinedText
7836
8088
  });
@@ -7842,7 +8094,7 @@ function createSlackTurnRuntime(deps) {
7842
8094
  message,
7843
8095
  decision,
7844
8096
  context: threadContext,
7845
- onInputCommitted: hooks.onInputCommitted,
8097
+ ack: hooks.ack,
7846
8098
  preparedState,
7847
8099
  text: combinedText
7848
8100
  });
@@ -7887,8 +8139,9 @@ function createSlackTurnRuntime(deps) {
7887
8139
  )
7888
8140
  );
7889
8141
  };
7890
- const onInputCommitted = async () => {
7891
- await hooks.onInputCommitted?.();
8142
+ const ack = async () => {
8143
+ await hooks.ack?.();
8144
+ acked = true;
7892
8145
  await startQueuedProcessingReactions();
7893
8146
  };
7894
8147
  const toolInvocationHook = createToolInvocationHook(
@@ -7901,7 +8154,7 @@ function createSlackTurnRuntime(deps) {
7901
8154
  preparedState,
7902
8155
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
7903
8156
  queuedMessages,
7904
- onInputCommitted,
8157
+ ack,
7905
8158
  onToolInvocation: toolInvocationHook,
7906
8159
  onTurnCompleted,
7907
8160
  drainSteeringMessages,
@@ -7955,6 +8208,9 @@ function createSlackTurnRuntime(deps) {
7955
8208
  {},
7956
8209
  "onSubscribedMessage failed"
7957
8210
  );
8211
+ if (!acked && hooks.isFinalAttempt === false) {
8212
+ return;
8213
+ }
7958
8214
  if (!eventId) {
7959
8215
  throw new Error(
7960
8216
  "Sentry did not return an event ID for subscribed_message_handler_failed"
@@ -8059,10 +8315,12 @@ function textPart(value) {
8059
8315
  }
8060
8316
  function messageText(message) {
8061
8317
  const content = message.content;
8318
+ const unwrapTask = message.role === "user";
8319
+ const displayText = (text) => unwrapTask ? unwrapCurrentInstruction(text) ?? text : text;
8062
8320
  if (!Array.isArray(content)) {
8063
- return typeof content === "string" ? content : "";
8321
+ return typeof content === "string" ? displayText(content) : "";
8064
8322
  }
8065
- return content.map(textPart).filter(Boolean).join("\n").trim();
8323
+ return content.map(textPart).filter((text) => Boolean(text)).map(displayText).join("\n").trim();
8066
8324
  }
8067
8325
  function sanitizeText(text) {
8068
8326
  return text.replace(
@@ -8904,6 +9162,7 @@ function createJuniorRuntimeServices(overrides = {}) {
8904
9162
  }
8905
9163
 
8906
9164
  // src/chat/runtime/reply-executor.ts
9165
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS4 } from "chat";
8907
9166
  import { createSlackSource } from "@sentry/junior-plugin-api";
8908
9167
 
8909
9168
  // src/chat/slack/message.ts
@@ -9036,6 +9295,9 @@ async function maybeApplyProviderDefaultConfigRequest(args) {
9036
9295
  }
9037
9296
 
9038
9297
  // src/chat/runtime/reply-executor.ts
9298
+ async function persistThreadStateWithRetry(thread, patch) {
9299
+ await persistWithRetry(() => persistThreadState(thread, patch));
9300
+ }
9039
9301
  function collectCanvasUrls(artifacts) {
9040
9302
  return new Set(
9041
9303
  [
@@ -9047,6 +9309,14 @@ function collectCanvasUrls(artifacts) {
9047
9309
  function turnRequester(requester) {
9048
9310
  return toStoredSlackRequester(requester);
9049
9311
  }
9312
+ function parkedInputKey(message) {
9313
+ if (message.role !== "user") {
9314
+ return void 0;
9315
+ }
9316
+ const first = Array.isArray(message.content) ? message.content[0] : void 0;
9317
+ const text = first && typeof first === "object" && "text" in first ? String(first.text ?? "") : "";
9318
+ return `${message.timestamp}:${text}`;
9319
+ }
9050
9320
  function isResourceEventMessage(message) {
9051
9321
  const raw = message.raw && typeof message.raw === "object" ? message.raw : void 0;
9052
9322
  return raw?.event_type === "resource_event";
@@ -9128,6 +9398,7 @@ function createReplyToThread(deps) {
9128
9398
  channelName,
9129
9399
  channelType: slackChannelType
9130
9400
  });
9401
+ const destinationVisibility = conversationVisibilityFromSlackChannelType(slackChannelType);
9131
9402
  const threadTs = getThreadTs(threadId);
9132
9403
  const assistantThreadContext = getAssistantThreadContext(message);
9133
9404
  const messageTs = getMessageTs(message);
@@ -9140,7 +9411,8 @@ function createReplyToThread(deps) {
9140
9411
  channelId: channelId ?? destination.channelId,
9141
9412
  messageTs,
9142
9413
  teamId,
9143
- threadTs
9414
+ threadTs,
9415
+ type: destinationVisibility === "public" ? "pub" : "priv"
9144
9416
  });
9145
9417
  const runId = getRunId(thread, message);
9146
9418
  const conversationId = threadId ?? runId;
@@ -9267,12 +9539,87 @@ function createReplyToThread(deps) {
9267
9539
  }
9268
9540
  };
9269
9541
  let activeTurnId = preparedState.conversation.processing.activeTurnId;
9542
+ const resolveSteeringMessages = async (queuedMessages) => {
9543
+ return await Promise.all(
9544
+ queuedMessages.map(async (queued) => {
9545
+ const attachments = queued.message.attachments;
9546
+ return {
9547
+ text: queued.userText,
9548
+ timestampMs: queued.message.metadata.dateSent.getTime(),
9549
+ omittedImageAttachmentCount: !isVisionEnabled() && hasPotentialImageAttachment(attachments) ? countPotentialImageAttachments(attachments) : 0,
9550
+ userAttachments: await deps.resolveUserAttachments(
9551
+ attachments,
9552
+ {
9553
+ threadId,
9554
+ requesterId: isResourceEventMessage(queued.message) ? void 0 : queued.message.author.userId,
9555
+ channelId,
9556
+ runId,
9557
+ conversation: preparedState.conversation,
9558
+ messageTs: getSlackMessageTs(queued.message)
9559
+ }
9560
+ )
9561
+ };
9562
+ })
9563
+ );
9564
+ };
9565
+ const appendParkedTurnInput = async (parkedSessionId) => {
9566
+ if (!conversationId) {
9567
+ return true;
9568
+ }
9569
+ const parkedMessages = [
9570
+ ...options.queuedMessages ?? [],
9571
+ {
9572
+ explicitMention: Boolean(
9573
+ options.explicitMention || message.isMention
9574
+ ),
9575
+ message,
9576
+ rawText: currentText.rawText,
9577
+ userText: currentText.userText
9578
+ }
9579
+ ].filter(
9580
+ // Redelivery of the parked turn's own message must not duplicate
9581
+ // the prompt that already started the session.
9582
+ (queued) => buildDeterministicTurnId(queued.message.id) !== parkedSessionId
9583
+ );
9584
+ if (parkedMessages.length === 0) {
9585
+ return true;
9586
+ }
9587
+ const stateAdapter = getStateAdapter();
9588
+ await stateAdapter.connect();
9589
+ const lock = await acquireActiveLock(stateAdapter, conversationId);
9590
+ if (!lock) {
9591
+ return false;
9592
+ }
9593
+ try {
9594
+ const piMessages = (await resolveSteeringMessages(parkedMessages)).map(buildSteeringPiMessage);
9595
+ const projection = await loadProjection({ conversationId });
9596
+ const appendedKeys = new Set(
9597
+ projection.map(parkedInputKey).filter((key) => key !== void 0)
9598
+ );
9599
+ const missing = piMessages.filter((piMessage) => {
9600
+ const key = parkedInputKey(piMessage);
9601
+ return key === void 0 || !appendedKeys.has(key);
9602
+ });
9603
+ if (missing.length === 0) {
9604
+ return true;
9605
+ }
9606
+ await commitMessages({
9607
+ conversationId,
9608
+ messages: [...projection, ...missing],
9609
+ requester: storedRequester,
9610
+ ttlMs: THREAD_STATE_TTL_MS4
9611
+ });
9612
+ return true;
9613
+ } finally {
9614
+ await stateAdapter.releaseLock(lock);
9615
+ }
9616
+ };
9270
9617
  if (preparedState.userMessageAlreadyReplied) {
9271
9618
  await persistThreadState(thread, {
9272
9619
  conversation: preparedState.conversation
9273
9620
  });
9274
9621
  await options.onTurnStatePersisted?.();
9275
- await options.onInputCommitted?.();
9622
+ await options.ack?.();
9276
9623
  await options.onTurnCompleted?.();
9277
9624
  return;
9278
9625
  }
@@ -9282,6 +9629,9 @@ function createReplyToThread(deps) {
9282
9629
  sessionId: activeTurnId
9283
9630
  });
9284
9631
  if (resumeRequest) {
9632
+ if (!await appendParkedTurnInput(resumeRequest.sessionId)) {
9633
+ throw new TurnInputDeferredError();
9634
+ }
9285
9635
  try {
9286
9636
  await deps.services.scheduleAgentContinue(resumeRequest);
9287
9637
  } catch (error) {
@@ -9302,7 +9652,7 @@ function createReplyToThread(deps) {
9302
9652
  conversation: preparedState.conversation
9303
9653
  });
9304
9654
  await options.onTurnStatePersisted?.();
9305
- await options.onInputCommitted?.();
9655
+ await options.ack?.();
9306
9656
  return;
9307
9657
  }
9308
9658
  const sessionRecord = await getAgentTurnSessionRecord(
@@ -9311,27 +9661,34 @@ function createReplyToThread(deps) {
9311
9661
  );
9312
9662
  if (sessionRecord?.state === "awaiting_resume") {
9313
9663
  if (sessionRecord.resumeReason === "auth") {
9314
- await persistThreadState(thread, {
9315
- conversation: preparedState.conversation
9664
+ await abandonAgentTurnSessionRecord({
9665
+ conversationId,
9666
+ sessionId: activeTurnId,
9667
+ errorMessage: "Auth-parked session superseded by a new user message"
9316
9668
  });
9317
- await options.onTurnStatePersisted?.();
9318
- await options.onInputCommitted?.();
9319
- return;
9669
+ markTurnClosed({
9670
+ conversation: preparedState.conversation,
9671
+ nowMs: Date.now(),
9672
+ sessionId: activeTurnId,
9673
+ updateConversationStats
9674
+ });
9675
+ activeTurnId = void 0;
9676
+ } else {
9677
+ await failAgentTurnSessionRecord({
9678
+ conversationId,
9679
+ expectedVersion: sessionRecord.version,
9680
+ sessionId: activeTurnId,
9681
+ errorMessage: "Awaiting agent continuation metadata could not be materialized"
9682
+ });
9683
+ markTurnFailed({
9684
+ conversation: preparedState.conversation,
9685
+ nowMs: Date.now(),
9686
+ sessionId: activeTurnId,
9687
+ markConversationMessage,
9688
+ updateConversationStats
9689
+ });
9690
+ activeTurnId = void 0;
9320
9691
  }
9321
- await failAgentTurnSessionRecord({
9322
- conversationId,
9323
- expectedVersion: sessionRecord.version,
9324
- sessionId: activeTurnId,
9325
- errorMessage: "Awaiting agent continuation metadata could not be materialized"
9326
- });
9327
- markTurnFailed({
9328
- conversation: preparedState.conversation,
9329
- nowMs: Date.now(),
9330
- sessionId: activeTurnId,
9331
- markConversationMessage,
9332
- updateConversationStats
9333
- });
9334
- activeTurnId = void 0;
9335
9692
  }
9336
9693
  }
9337
9694
  const configReply = await maybeApplyProviderDefaultConfigRequest({
@@ -9367,7 +9724,7 @@ function createReplyToThread(deps) {
9367
9724
  conversation: preparedState.conversation
9368
9725
  });
9369
9726
  await options.onTurnStatePersisted?.();
9370
- await options.onInputCommitted?.();
9727
+ await options.ack?.();
9371
9728
  return;
9372
9729
  }
9373
9730
  startActiveTurn({
@@ -9387,6 +9744,7 @@ function createReplyToThread(deps) {
9387
9744
  surface: "slack",
9388
9745
  requester,
9389
9746
  destination,
9747
+ destinationVisibility,
9390
9748
  source,
9391
9749
  traceId: getActiveTraceId()
9392
9750
  }).catch((error) => {
@@ -9503,8 +9861,10 @@ function createReplyToThread(deps) {
9503
9861
  };
9504
9862
  let persistedAtLeastOnce = false;
9505
9863
  let shouldPersistFailureState = true;
9864
+ let finalReplyDelivered = false;
9506
9865
  let latestArtifacts = preparedState.artifacts;
9507
9866
  let assistantTitleArtifacts = {};
9867
+ const hasVisibleSlackDelivery = (post) => post.text.trim().length > 0 || Boolean(post.files?.length);
9508
9868
  try {
9509
9869
  const loadedPiMessages = await loadPiMessagesForTurn({
9510
9870
  conversationId,
@@ -9596,39 +9956,16 @@ function createReplyToThread(deps) {
9596
9956
  );
9597
9957
  });
9598
9958
  const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
9599
- const resolveSteeringMessages = async (queuedMessages) => {
9600
- return await Promise.all(
9601
- queuedMessages.map(async (queued) => {
9602
- const attachments = queued.message.attachments;
9603
- return {
9604
- text: queued.userText,
9605
- timestampMs: queued.message.metadata.dateSent.getTime(),
9606
- omittedImageAttachmentCount: !isVisionEnabled() && hasPotentialImageAttachment(attachments) ? countPotentialImageAttachments(attachments) : 0,
9607
- userAttachments: await deps.resolveUserAttachments(
9608
- attachments,
9609
- {
9610
- threadId,
9611
- requesterId: isResourceEventMessage(queued.message) ? void 0 : queued.message.author.userId,
9612
- channelId,
9613
- runId,
9614
- conversation: preparedState.conversation,
9615
- messageTs: getSlackMessageTs(queued.message)
9616
- }
9617
- )
9618
- };
9619
- })
9620
- );
9621
- };
9622
- const drainSteeringMessages = options.drainSteeringMessages ? async (inject) => {
9623
- let injectedMessages;
9959
+ const drainSteeringMessages = options.drainSteeringMessages ? async (accept) => {
9960
+ let acceptedMessages;
9624
9961
  const drained = await options.drainSteeringMessages(
9625
9962
  async (queuedMessages) => {
9626
- injectedMessages = await resolveSteeringMessages(queuedMessages);
9627
- await inject(injectedMessages);
9963
+ acceptedMessages = await resolveSteeringMessages(queuedMessages);
9964
+ await accept(acceptedMessages);
9628
9965
  },
9629
9966
  { conversationContext: preparedState.conversationContext }
9630
9967
  );
9631
- return injectedMessages ?? await resolveSteeringMessages(drained);
9968
+ return acceptedMessages ?? await resolveSteeringMessages(drained);
9632
9969
  } : void 0;
9633
9970
  let reply = await deps.services.generateAssistantReply(
9634
9971
  effectiveUserText,
@@ -9694,7 +10031,7 @@ function createReplyToThread(deps) {
9694
10031
  },
9695
10032
  onStatus: (nextStatus) => status.update(nextStatus),
9696
10033
  onToolInvocation: options.onToolInvocation,
9697
- onInputCommitted: options.onInputCommitted,
10034
+ onInputCommitted: options.ack,
9698
10035
  drainSteeringMessages,
9699
10036
  shouldYield: options.shouldYield
9700
10037
  }
@@ -9727,6 +10064,14 @@ function createReplyToThread(deps) {
9727
10064
  const shouldUseSlackFooter = Boolean(replyFooter) && Boolean(channelId && threadTs) && thread.adapter?.name === "slack";
9728
10065
  if (plannedPosts.length > 0) {
9729
10066
  let sent;
10067
+ const hasVisibleDelivery = plannedPosts.some(
10068
+ hasVisibleSlackDelivery
10069
+ );
10070
+ if (!hasVisibleDelivery) {
10071
+ throw new Error(
10072
+ "Slack final reply plan did not contain visible delivery"
10073
+ );
10074
+ }
9730
10075
  if (shouldUseSlackFooter) {
9731
10076
  const slackChannelId = channelId;
9732
10077
  const slackThreadTs = threadTs;
@@ -9770,16 +10115,34 @@ function createReplyToThread(deps) {
9770
10115
  }
9771
10116
  } else {
9772
10117
  for (const post of plannedPosts) {
10118
+ if (!hasVisibleSlackDelivery(post)) {
10119
+ continue;
10120
+ }
9773
10121
  sent = await postThreadReply(
9774
10122
  buildSlackOutputMessage(post.text, post.files),
9775
10123
  post.stage
9776
10124
  );
9777
10125
  }
9778
10126
  }
10127
+ finalReplyDelivered = true;
10128
+ shouldPersistFailureState = false;
9779
10129
  const firstPlannedMessageHasFiles = (plannedPosts[0]?.files?.length ?? 0) > 0;
9780
10130
  if (sent && reactionPerformed && plannedPosts.length === 1 && !firstPlannedMessageHasFiles && isRedundantReactionAckText(reply.text)) {
9781
- await sent.delete();
10131
+ try {
10132
+ await sent.delete();
10133
+ } catch (error) {
10134
+ logException(
10135
+ error,
10136
+ "slack_redundant_ack_delete_failed",
10137
+ turnTraceContext,
10138
+ messageTs ? { "messaging.message.id": messageTs } : {},
10139
+ "Failed to delete redundant reaction-ack reply"
10140
+ );
10141
+ }
9782
10142
  }
10143
+ } else {
10144
+ finalReplyDelivered = true;
10145
+ shouldPersistFailureState = false;
9783
10146
  }
9784
10147
  const completedState = buildDeliveredTurnStatePatch({
9785
10148
  artifactStatePatch: {
@@ -9795,27 +10158,63 @@ function createReplyToThread(deps) {
9795
10158
  if (completedState.artifacts) {
9796
10159
  latestArtifacts = completedState.artifacts;
9797
10160
  }
9798
- await persistThreadState(thread, {
9799
- ...completedState
9800
- });
9801
- if (completedState.artifacts && (assistantTitleArtifacts.assistantTitle !== void 0 || assistantTitleArtifacts.assistantTitleSourceMessageId !== void 0) && (completedState.artifacts.assistantTitle !== assistantTitleArtifacts.assistantTitle || completedState.artifacts.assistantTitleSourceMessageId !== assistantTitleArtifacts.assistantTitleSourceMessageId)) {
9802
- await persistThreadState(thread, { artifacts: latestArtifacts });
9803
- }
9804
- if (conversationId) {
9805
- await recordAgentTurnSessionSummary({
9806
- channelName,
9807
- conversationId,
9808
- cumulativeDurationMs: reply.diagnostics.durationMs,
9809
- cumulativeUsage: reply.diagnostics.usage,
9810
- sessionId: turnId,
9811
- sliceId: 1,
9812
- startedAtMs: message.metadata.dateSent.getTime(),
9813
- state: "completed",
9814
- requester,
9815
- destination,
9816
- source,
9817
- traceId: getActiveTraceId()
10161
+ try {
10162
+ if (conversationId && reply.piMessages?.length) {
10163
+ await completeDeliveredTurn({
10164
+ channelName,
10165
+ conversationId,
10166
+ durationMs: reply.diagnostics.durationMs,
10167
+ usage: reply.diagnostics.usage,
10168
+ destination,
10169
+ destinationVisibility,
10170
+ source,
10171
+ sessionId: turnId,
10172
+ sliceId: 1,
10173
+ messages: reply.piMessages,
10174
+ logContext: {
10175
+ threadId,
10176
+ requesterId: slackRequesterId,
10177
+ channelId,
10178
+ runId,
10179
+ assistantUserName: botConfig.userName,
10180
+ modelId: reply.diagnostics.modelId
10181
+ },
10182
+ requester,
10183
+ surface: "slack"
10184
+ });
10185
+ } else if (conversationId) {
10186
+ await recordAgentTurnSessionSummary({
10187
+ channelName,
10188
+ conversationId,
10189
+ cumulativeDurationMs: reply.diagnostics.durationMs,
10190
+ cumulativeUsage: reply.diagnostics.usage,
10191
+ sessionId: turnId,
10192
+ sliceId: 1,
10193
+ startedAtMs: message.metadata.dateSent.getTime(),
10194
+ state: "completed",
10195
+ requester,
10196
+ destination,
10197
+ destinationVisibility,
10198
+ source,
10199
+ traceId: getActiveTraceId()
10200
+ });
10201
+ }
10202
+ await persistThreadStateWithRetry(thread, {
10203
+ ...completedState
9818
10204
  });
10205
+ if (completedState.artifacts && (assistantTitleArtifacts.assistantTitle !== void 0 || assistantTitleArtifacts.assistantTitleSourceMessageId !== void 0) && (completedState.artifacts.assistantTitle !== assistantTitleArtifacts.assistantTitle || completedState.artifacts.assistantTitleSourceMessageId !== assistantTitleArtifacts.assistantTitleSourceMessageId)) {
10206
+ await persistThreadStateWithRetry(thread, {
10207
+ artifacts: latestArtifacts
10208
+ });
10209
+ }
10210
+ } catch (commitError) {
10211
+ logException(
10212
+ commitError,
10213
+ "slack_reply_post_delivery_commit_failed",
10214
+ turnTraceContext,
10215
+ messageTs ? { "messaging.message.id": messageTs } : {},
10216
+ "Post-delivery turn state persistence failed after Slack accepted the reply"
10217
+ );
9819
10218
  }
9820
10219
  preparedState.conversation = completedState.conversation;
9821
10220
  persistedAtLeastOnce = true;
@@ -9849,6 +10248,17 @@ function createReplyToThread(deps) {
9849
10248
  }
9850
10249
  }
9851
10250
  } catch (error) {
10251
+ if (finalReplyDelivered) {
10252
+ shouldPersistFailureState = false;
10253
+ logException(
10254
+ error,
10255
+ "slack_reply_post_delivery_commit_failed",
10256
+ turnTraceContext,
10257
+ messageTs ? { "messaging.message.id": messageTs } : {},
10258
+ "Post-delivery turn work failed after Slack accepted the reply"
10259
+ );
10260
+ return;
10261
+ }
9852
10262
  if (isCooperativeTurnYieldError(error)) {
9853
10263
  shouldPersistFailureState = false;
9854
10264
  throw error;
@@ -10023,6 +10433,7 @@ function createReplyToThread(deps) {
10023
10433
  state: "failed",
10024
10434
  requester,
10025
10435
  destination,
10436
+ destinationVisibility,
10026
10437
  source,
10027
10438
  traceId: getActiveTraceId()
10028
10439
  });
@@ -10460,7 +10871,7 @@ function createJuniorSlackAdapter(config) {
10460
10871
 
10461
10872
  // src/chat/runtime/agent-continue-runner.ts
10462
10873
  var AGENT_CONTINUE_LOCK_RETRY_DELAYS_MS = [250, 1e3, 2e3];
10463
- function sleep(ms) {
10874
+ function sleep2(ms) {
10464
10875
  return new Promise((resolve) => setTimeout(resolve, ms));
10465
10876
  }
10466
10877
  async function persistCompletedReplyState2(args) {
@@ -10547,6 +10958,28 @@ async function failContinuationStartup(args) {
10547
10958
  );
10548
10959
  }
10549
10960
  }
10961
+ async function resolveContinuationRequester(args) {
10962
+ const stored = args.sessionRecordRequester;
10963
+ if (stored?.platform === "slack" && stored.teamId === args.teamId && stored.userId === args.userId) {
10964
+ return createSlackResumeRequester({
10965
+ requester: stored,
10966
+ teamId: args.teamId,
10967
+ userId: args.userId
10968
+ });
10969
+ }
10970
+ const work = await getConversationWorkState2({
10971
+ conversationId: args.conversationId
10972
+ });
10973
+ const workRequester = work?.requester;
10974
+ if (workRequester && workRequester.teamId === args.teamId && workRequester.slackUserId === args.userId) {
10975
+ return createSlackRequester(args.teamId, args.userId, {
10976
+ email: workRequester.email,
10977
+ fullName: workRequester.fullName,
10978
+ userName: workRequester.slackUserName
10979
+ });
10980
+ }
10981
+ return void 0;
10982
+ }
10550
10983
  function isContinuationResume(summary) {
10551
10984
  return summary.state === "awaiting_resume" && (summary.resumeReason === "timeout" || summary.resumeReason === "yield");
10552
10985
  }
@@ -10610,11 +11043,20 @@ async function continueSlackAgentRun(payload, options = {}) {
10610
11043
  payload.destination,
10611
11044
  "Slack continuation"
10612
11045
  );
10613
- const requester = createSlackResumeRequester({
10614
- requester: activeSessionRecord.requester,
11046
+ const requester = await resolveContinuationRequester({
11047
+ conversationId: payload.conversationId,
11048
+ sessionRecordRequester: activeSessionRecord.requester,
10615
11049
  teamId: destination.teamId,
10616
11050
  userId: userMessage2.author.userId
10617
11051
  });
11052
+ if (!requester) {
11053
+ await failStrandedSessionWithFallback({
11054
+ conversationId: payload.conversationId,
11055
+ errorMessage: "Stored Slack requester missing for continuation",
11056
+ sessionRecord: activeSessionRecord
11057
+ });
11058
+ return false;
11059
+ }
10618
11060
  if (!activeSessionRecord.source) {
10619
11061
  await failAgentTurnSessionRecord({
10620
11062
  conversationId: payload.conversationId,
@@ -10724,8 +11166,119 @@ async function continueSlackAgentRun(payload, options = {}) {
10724
11166
  }
10725
11167
  });
10726
11168
  }
11169
+ async function failStrandedSessionWithFallback(args) {
11170
+ await failAgentTurnSessionRecord({
11171
+ conversationId: args.conversationId,
11172
+ expectedVersion: args.sessionRecord.version,
11173
+ sessionId: args.sessionRecord.sessionId,
11174
+ errorMessage: args.errorMessage
11175
+ });
11176
+ const currentState = await getPersistedThreadState(args.conversationId);
11177
+ const conversation = coerceThreadConversationState(currentState);
11178
+ markTurnFailed({
11179
+ conversation,
11180
+ nowMs: Date.now(),
11181
+ sessionId: args.sessionRecord.sessionId,
11182
+ userMessageId: getTurnUserMessage(
11183
+ conversation,
11184
+ args.sessionRecord.sessionId
11185
+ )?.id,
11186
+ markConversationMessage,
11187
+ updateConversationStats
11188
+ });
11189
+ await persistThreadStateById(args.conversationId, { conversation });
11190
+ const thread = parseSlackThreadId(args.conversationId);
11191
+ if (!thread) {
11192
+ return;
11193
+ }
11194
+ const eventName = "agent_turn_stranded_session_failed";
11195
+ const eventId = logException(
11196
+ new Error(args.errorMessage),
11197
+ eventName,
11198
+ { conversationId: args.conversationId },
11199
+ {
11200
+ "app.ai.conversation_id": args.conversationId,
11201
+ "app.ai.session_id": args.sessionRecord.sessionId
11202
+ },
11203
+ "Stranded running agent session terminally failed"
11204
+ );
11205
+ await postSlackMessage({
11206
+ channelId: thread.channelId,
11207
+ threadTs: thread.threadTs,
11208
+ text: buildTurnFailureResponse(
11209
+ requireTurnFailureEventId(eventId, eventName)
11210
+ )
11211
+ });
11212
+ }
11213
+ async function recoverStrandedRunningSession(args) {
11214
+ const stateAdapter = getStateAdapter();
11215
+ await stateAdapter.connect();
11216
+ const probe = await acquireActiveLock(stateAdapter, args.conversationId);
11217
+ if (!probe) {
11218
+ return false;
11219
+ }
11220
+ await stateAdapter.releaseLock(probe);
11221
+ const sessionRecord = await getAgentTurnSessionRecord(
11222
+ args.conversationId,
11223
+ args.summary.sessionId
11224
+ );
11225
+ if (!sessionRecord || sessionRecord.state !== "running") {
11226
+ return false;
11227
+ }
11228
+ const parked = await persistYieldSessionRecord({
11229
+ channelName: sessionRecord.channelName,
11230
+ conversationId: args.conversationId,
11231
+ sessionId: sessionRecord.sessionId,
11232
+ currentSliceId: sessionRecord.sliceId,
11233
+ destination: sessionRecord.destination,
11234
+ source: sessionRecord.source,
11235
+ messages: sessionRecord.piMessages,
11236
+ errorMessage: "Recovered running session after hard worker death",
11237
+ logContext: { modelId: botConfig.modelId },
11238
+ requester: sessionRecord.requester,
11239
+ surface: sessionRecord.surface
11240
+ });
11241
+ if (!parked) {
11242
+ await failStrandedSessionWithFallback({
11243
+ conversationId: args.conversationId,
11244
+ errorMessage: "Stranded running session had no resumable boundary after worker death",
11245
+ sessionRecord
11246
+ });
11247
+ return false;
11248
+ }
11249
+ const request = await getAwaitingAgentContinueRequest({
11250
+ conversationId: args.conversationId,
11251
+ sessionId: sessionRecord.sessionId
11252
+ });
11253
+ if (!request) {
11254
+ await failStrandedSessionWithFallback({
11255
+ conversationId: args.conversationId,
11256
+ errorMessage: "Stranded running session could not materialize continuation metadata",
11257
+ sessionRecord: parked
11258
+ });
11259
+ return false;
11260
+ }
11261
+ if (await continueSlackAgentRunWithLockRetry(request, args.options)) {
11262
+ return true;
11263
+ }
11264
+ await failUnresumableContinuation({
11265
+ conversationId: args.conversationId,
11266
+ expectedVersion: request.expectedVersion,
11267
+ summary: args.summary,
11268
+ errorMessage: "Awaiting agent continuation was stale before it could run"
11269
+ });
11270
+ return false;
11271
+ }
10727
11272
  async function resumeAwaitingSlackContinuation(conversationId, options = {}) {
10728
11273
  const summaries = await listAgentTurnSessionSummariesForConversation(conversationId);
11274
+ const newest = summaries[0];
11275
+ if (newest?.state === "running") {
11276
+ return await recoverStrandedRunningSession({
11277
+ conversationId,
11278
+ options,
11279
+ summary: newest
11280
+ });
11281
+ }
10729
11282
  for (const summary of summaries) {
10730
11283
  if (!isContinuationResume(summary)) {
10731
11284
  continue;
@@ -10791,7 +11344,7 @@ async function continueSlackAgentRunWithLockRetry(payload, options = {}) {
10791
11344
  },
10792
11345
  "Agent continuation lock was busy; retrying"
10793
11346
  );
10794
- await sleep(delayMs);
11347
+ await sleep2(delayMs);
10795
11348
  }
10796
11349
  }
10797
11350
  return true;