@sentry/junior 0.85.0 → 0.87.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 (49) hide show
  1. package/dist/{agent-hooks-AV7CHLUO.js → agent-hooks-NWOUB3UR.js} +7 -7
  2. package/dist/app.js +779 -356
  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/ingress/message-changed.d.ts +2 -0
  8. package/dist/chat/no-reply.d.ts +7 -0
  9. package/dist/chat/respond.d.ts +7 -1
  10. package/dist/chat/runtime/reply-executor.d.ts +2 -10
  11. package/dist/chat/runtime/slack-runtime.d.ts +5 -4
  12. package/dist/chat/runtime/turn.d.ts +7 -0
  13. package/dist/chat/services/persist-retry.d.ts +2 -0
  14. package/dist/chat/services/reply-delivery-plan.d.ts +0 -4
  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-UVNV67EC.js → chunk-2NFV5FMB.js} +4 -4
  27. package/dist/{chunk-QTTTRBNB.js → chunk-6I6HBOQM.js} +80 -15
  28. package/dist/{chunk-ENEWQMRJ.js → chunk-6O5UI3RG.js} +1 -1
  29. package/dist/{chunk-GY7L6VCT.js → chunk-ACJJJEGG.js} +35 -17
  30. package/dist/{chunk-MHEV3T2Y.js → chunk-B6FQPS7A.js} +953 -795
  31. package/dist/{chunk-MTE5NRPJ.js → chunk-BRSQQRG6.js} +1 -1
  32. package/dist/{chunk-EENOFKDN.js → chunk-ENPSU7L7.js} +165 -27
  33. package/dist/{chunk-RUB77TGS.js → chunk-GB5DFM4D.js} +1 -1
  34. package/dist/{chunk-C2PSUWK3.js → chunk-GGD6WK6V.js} +124 -25
  35. package/dist/{chunk-SSUEWAVS.js → chunk-JRXCSSSU.js} +19 -4
  36. package/dist/{chunk-DR75T7J3.js → chunk-L7OHKDOX.js} +9 -5
  37. package/dist/{chunk-FYPO4K7C.js → chunk-RIB3M6YA.js} +2 -2
  38. package/dist/{chunk-BU2AWPEN.js → chunk-ZU2ALUVQ.js} +3 -2
  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-S3HYBLUS.js → db-SZVUU7RB.js} +4 -4
  44. package/dist/instrumentation.js +1 -1
  45. package/dist/nitro.js +4 -4
  46. package/dist/reporting/conversations.d.ts +2 -1
  47. package/dist/reporting.js +95 -30
  48. package/dist/{runner-IMQH6V2B.js → runner-DMP3IQNM.js} +27 -13
  49. package/package.json +6 -6
package/dist/app.js CHANGED
@@ -3,27 +3,31 @@ 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,
17
- deleteSlackMessage,
18
20
  deliverResourceEventSubscription,
19
21
  estimateTextTokens,
20
22
  executeCredentialedEgressRequest,
23
+ finalizeFailedTurnReply,
21
24
  finalizeMcpAuthorization,
22
25
  findMatchingResourceEventSubscriptions,
23
26
  formatProviderLabel,
24
27
  generateAssistantReply,
25
28
  generateConversationId,
26
29
  getAgentContextCompactionTriggerTokens,
30
+ getAgentTurnDiagnosticsAttributes,
27
31
  getChannelConfigurationService,
28
32
  getChannelConfigurationServiceById,
29
33
  getConfigDefaults,
@@ -37,9 +41,9 @@ import {
37
41
  isCooperativeTurnYieldError,
38
42
  isHumanConversationMessage,
39
43
  isPendingAuthLatestRequest,
40
- isRedundantReactionAckText,
41
44
  isRetryableTurnError,
42
45
  isTurnInputCommitLostError,
46
+ isTurnInputDeferredError,
43
47
  listThreadReplies,
44
48
  markConversationMessage,
45
49
  markTurnClosed,
@@ -49,12 +53,15 @@ import {
49
53
  normalizeSandboxEgressTracePropagationDomains,
50
54
  parseOAuthStatePayload,
51
55
  parseSandboxEgressCredentialToken,
56
+ persistCompletedSessionRecord,
52
57
  persistThreadState,
53
58
  persistThreadStateById,
59
+ persistYieldSessionRecord,
54
60
  postSlackMessage,
55
61
  processPluginTask,
56
62
  removeReactionFromMessage,
57
63
  renderSlackLegacyAttachmentText,
64
+ requireTurnFailureEventId,
58
65
  resolveBaseUrl,
59
66
  resolvePluginOAuthAccount,
60
67
  resolveSandboxEgressProviderForHost,
@@ -68,37 +75,41 @@ import {
68
75
  updateConversationStats,
69
76
  uploadFilesToThread,
70
77
  upsertConversationMessage
71
- } from "./chunk-MHEV3T2Y.js";
78
+ } from "./chunk-B6FQPS7A.js";
72
79
  import {
73
80
  CONVERSATION_WORK_CHECK_IN_INTERVAL_MS,
74
81
  CONVERSATION_WORK_STALE_ENQUEUE_MS,
82
+ ackMessages,
75
83
  appendInboundMessage,
76
84
  checkInConversationWork,
77
85
  clearConsumedConversationWake,
78
86
  clearExpiredConversationLease,
79
87
  completeConversationWork,
88
+ deadLetterAttempt,
80
89
  drainConversationMailbox,
81
90
  getConversation,
82
91
  getConversationWorkState,
92
+ isFinalAttempt,
93
+ isInvalidConversationRecordError,
83
94
  listActiveConversationIds,
84
- markConversationMessagesInjected,
85
95
  markConversationWorkEnqueued,
96
+ recordAttemptFailure,
86
97
  releaseConversationWork,
87
98
  removeActiveConversation,
88
99
  requestConversationContinuation,
89
100
  requestConversationWork,
90
101
  startConversationWork
91
- } from "./chunk-EENOFKDN.js";
102
+ } from "./chunk-ENPSU7L7.js";
92
103
  import {
93
104
  JUNIOR_THREAD_STATE_TTL_MS,
94
105
  coerceThreadConversationState
95
106
  } from "./chunk-66NX7MNW.js";
96
- import "./chunk-FYPO4K7C.js";
107
+ import "./chunk-RIB3M6YA.js";
97
108
  import {
98
109
  getVercelConversationWorkQueue,
99
110
  resolveConversationWorkQueueTopic,
100
111
  verifyConversationQueueMessage
101
- } from "./chunk-ENEWQMRJ.js";
112
+ } from "./chunk-6O5UI3RG.js";
102
113
  import {
103
114
  PLUGIN_TASK_QUEUE_TOPIC,
104
115
  createVercelQueueClient,
@@ -107,18 +118,18 @@ import {
107
118
  import {
108
119
  GET,
109
120
  buildSentryConversationUrl,
121
+ conversationVisibilityFromSlackChannelType,
110
122
  initConversationContext,
111
123
  resolveSlackChannelTypeFromMessage,
112
124
  resolveSlackConversationContext,
113
125
  setConversationTitle
114
- } from "./chunk-SSUEWAVS.js";
126
+ } from "./chunk-JRXCSSSU.js";
115
127
  import {
116
128
  abandonAgentTurnSessionRecord,
117
129
  buildSlackOutputMessage,
118
130
  commitMessages,
119
131
  failAgentTurnSessionRecord,
120
132
  getAgentTurnSessionRecord,
121
- getInterruptionMarker,
122
133
  listAgentTurnSessionSummariesForConversation,
123
134
  loadProjection,
124
135
  normalizeSlackStatusText,
@@ -126,7 +137,7 @@ import {
126
137
  recordAuthorizationCompleted,
127
138
  splitSlackReplyText,
128
139
  truncateStatusText
129
- } from "./chunk-GY7L6VCT.js";
140
+ } from "./chunk-ACJJJEGG.js";
130
141
  import {
131
142
  validatePluginEgressCredentialHooks,
132
143
  validatePluginRegistrations
@@ -146,20 +157,20 @@ import {
146
157
  setPlugins,
147
158
  validatePlugins,
148
159
  verifySlackDirectCredentialSubject
149
- } from "./chunk-UVNV67EC.js";
160
+ } from "./chunk-2NFV5FMB.js";
150
161
  import {
151
162
  createPluginLogger,
152
163
  createPluginState
153
- } from "./chunk-MTE5NRPJ.js";
164
+ } from "./chunk-BRSQQRG6.js";
154
165
  import {
155
166
  getConversationStore,
156
167
  getDb
157
- } from "./chunk-QTTTRBNB.js";
168
+ } from "./chunk-6I6HBOQM.js";
158
169
  import "./chunk-G3E7SCME.js";
159
170
  import {
160
171
  acquireActiveLock,
161
172
  getStateAdapter
162
- } from "./chunk-RUB77TGS.js";
173
+ } from "./chunk-GB5DFM4D.js";
163
174
  import {
164
175
  SlackActionError,
165
176
  createSlackDestination,
@@ -167,15 +178,17 @@ import {
167
178
  downloadPrivateSlackFile,
168
179
  getHeaderString,
169
180
  getSlackClient,
181
+ getWorkspaceTeamId,
170
182
  isDmChannel,
171
183
  normalizeSlackConversationId,
172
184
  parseDestination,
173
185
  requireSlackDestination,
186
+ runWithSlackInstallationToken,
187
+ runWithWorkspaceTeamId,
174
188
  sameDestination
175
- } from "./chunk-C2PSUWK3.js";
189
+ } from "./chunk-GGD6WK6V.js";
176
190
  import {
177
191
  FUNCTION_TIMEOUT_BUFFER_SECONDS,
178
- GEN_AI_PROVIDER_NAME,
179
192
  botConfig,
180
193
  completeObject,
181
194
  completeText,
@@ -193,12 +206,12 @@ import {
193
206
  stripRuntimeTurnContext,
194
207
  trimTrailingAssistantMessages,
195
208
  unwrapCurrentInstruction
196
- } from "./chunk-BU2AWPEN.js";
209
+ } from "./chunk-ZU2ALUVQ.js";
197
210
  import {
198
211
  parseSlackThreadId,
199
212
  resolveSlackChannelIdFromMessage,
200
213
  resolveSlackChannelIdFromThreadId
201
- } from "./chunk-DR75T7J3.js";
214
+ } from "./chunk-L7OHKDOX.js";
202
215
  import {
203
216
  discoverSkills
204
217
  } from "./chunk-64ACM6AP.js";
@@ -495,96 +508,25 @@ async function postSlackApiReplyPosts(args) {
495
508
  return lastPostedMessageTs;
496
509
  }
497
510
 
498
- // src/chat/services/turn-failure-response.ts
499
- function requireTurnFailureEventId(eventId, eventName) {
500
- if (!eventId) {
501
- throw new Error(`Sentry did not return an event ID for ${eventName}`);
502
- }
503
- return eventId;
504
- }
505
- function getExecutionFailureReason(reply) {
506
- const errorMessage = reply.diagnostics.errorMessage?.trim();
507
- if (errorMessage) {
508
- return errorMessage;
509
- }
510
- if (reply.diagnostics.toolErrorCount > 0) {
511
- return `${reply.diagnostics.toolErrorCount} tool result error(s)`;
512
- }
513
- if (reply.diagnostics.assistantMessageCount > 0) {
514
- return "assistant returned no text";
515
- }
516
- return "empty assistant turn";
517
- }
518
- function getFailureCapture(reply) {
519
- if (reply.diagnostics.outcome === "provider_error") {
520
- return {
521
- eventName: "agent_turn_provider_error",
522
- error: reply.diagnostics.providerError ?? new Error(
523
- reply.diagnostics.errorMessage ?? "Provider error without explicit message"
524
- ),
525
- attributes: {},
526
- body: "Agent turn failed with provider error"
527
- };
528
- }
529
- const failureReason = getExecutionFailureReason(reply);
530
- return {
531
- eventName: "agent_turn_execution_failure",
532
- error: new Error(`Agent turn execution failure: ${failureReason}`),
533
- attributes: {
534
- "app.ai.execution_failure_reason": failureReason
535
- },
536
- body: "Agent turn completed with execution failure"
537
- };
538
- }
539
- function getAgentTurnDiagnosticsAttributes(reply) {
540
- return {
541
- "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
542
- "gen_ai.operation.name": "invoke_agent",
543
- "app.ai.outcome": reply.diagnostics.outcome,
544
- "app.ai.assistant_messages": reply.diagnostics.assistantMessageCount,
545
- "app.ai.tool_results": reply.diagnostics.toolResultCount,
546
- "app.ai.tool_error_results": reply.diagnostics.toolErrorCount,
547
- "app.ai.tool_call_count": reply.diagnostics.toolCalls.length,
548
- "app.ai.used_primary_text": reply.diagnostics.usedPrimaryText,
549
- ...reply.diagnostics.thinkingLevel ? {
550
- "app.ai.reasoning_effort": reply.diagnostics.thinkingLevel
551
- } : {},
552
- ...reply.diagnostics.stopReason ? {
553
- "gen_ai.response.finish_reasons": [reply.diagnostics.stopReason]
554
- } : {},
555
- ...reply.diagnostics.errorMessage ? { "exception.message": reply.diagnostics.errorMessage } : {}
556
- };
511
+ // src/chat/services/persist-retry.ts
512
+ var PERSIST_ATTEMPTS = 3;
513
+ function sleep(ms) {
514
+ return new Promise((resolve) => setTimeout(resolve, ms));
557
515
  }
558
- function finalizeFailedTurnReply(args) {
559
- if (args.reply.diagnostics.outcome === "success") {
560
- return args.reply;
561
- }
562
- const capture = getFailureCapture(args.reply);
563
- const eventId = requireTurnFailureEventId(
564
- args.logException(
565
- capture.error,
566
- capture.eventName,
567
- args.context,
568
- {
569
- ...getAgentTurnDiagnosticsAttributes(args.reply),
570
- ...args.attributes,
571
- ...capture.attributes
572
- },
573
- capture.body
574
- ),
575
- capture.eventName
576
- );
577
- const providerPartialText = args.reply.diagnostics.outcome === "provider_error" ? args.reply.text.trim() : "";
578
- return {
579
- ...args.reply,
580
- text: providerPartialText ? `${providerPartialText}${getInterruptionMarker()}` : buildTurnFailureResponse(eventId),
581
- deliveryMode: "thread",
582
- deliveryPlan: {
583
- mode: "thread",
584
- postThreadText: true,
585
- attachFiles: args.reply.files && args.reply.files.length > 0 ? "inline" : "none"
516
+ async function persistWithRetry(persist) {
517
+ let lastError;
518
+ for (let attempt = 1; attempt <= PERSIST_ATTEMPTS; attempt += 1) {
519
+ try {
520
+ await persist();
521
+ return;
522
+ } catch (error) {
523
+ lastError = error;
524
+ if (attempt < PERSIST_ATTEMPTS) {
525
+ await sleep(attempt * 100);
526
+ }
586
527
  }
587
- };
528
+ }
529
+ throw lastError;
588
530
  }
589
531
 
590
532
  // src/chat/agent-dispatch/signing.ts
@@ -1035,6 +977,16 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1035
977
  }
1036
978
  let dispatch = claimedDispatch;
1037
979
  const conversationId = getDispatchConversationId(dispatch);
980
+ const turnId = getDispatchTurnId(dispatch.id);
981
+ const logContext = {
982
+ conversationId,
983
+ slackThreadId: conversationId,
984
+ slackChannelId: dispatch.destination.channelId,
985
+ runId: dispatch.id,
986
+ actorType: dispatch.actor.type,
987
+ actorId: dispatch.actor.id,
988
+ assistantUserName: botConfig.userName
989
+ };
1038
990
  const destinationLockId = getDispatchDestinationLockId(dispatch.destination);
1039
991
  const stateAdapter = getStateAdapter();
1040
992
  await stateAdapter.connect();
@@ -1119,7 +1071,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1119
1071
  correlation: {
1120
1072
  conversationId,
1121
1073
  threadId: conversationId,
1122
- turnId: getDispatchTurnId(dispatch.id),
1074
+ turnId,
1123
1075
  runId: dispatch.id,
1124
1076
  channelId: dispatch.destination.channelId,
1125
1077
  teamId: dispatch.destination.teamId
@@ -1159,13 +1111,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1159
1111
  reply,
1160
1112
  logException,
1161
1113
  context: {
1162
- conversationId,
1163
- slackThreadId: conversationId,
1164
- slackChannelId: dispatch.destination.channelId,
1165
- runId: dispatch.id,
1166
- actorType: dispatch.actor.type,
1167
- actorId: dispatch.actor.id,
1168
- assistantUserName: botConfig.userName,
1114
+ ...logContext,
1169
1115
  modelId: reply.diagnostics.modelId
1170
1116
  }
1171
1117
  });
@@ -1199,13 +1145,45 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1199
1145
  });
1200
1146
  updateConversationStats(conversation);
1201
1147
  const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : artifacts;
1202
- await persistRuntimePatch({
1203
- threadId: conversationId,
1204
- conversation,
1205
- artifacts: nextArtifacts,
1206
- sandboxId: reply.sandboxId ?? sandboxId,
1207
- sandboxDependencyProfileHash: reply.sandboxDependencyProfileHash ?? sandboxDependencyProfileHash
1208
- });
1148
+ try {
1149
+ await persistWithRetry(
1150
+ () => persistRuntimePatch({
1151
+ threadId: conversationId,
1152
+ conversation,
1153
+ artifacts: nextArtifacts,
1154
+ sandboxId: reply.sandboxId ?? sandboxId,
1155
+ sandboxDependencyProfileHash: reply.sandboxDependencyProfileHash ?? sandboxDependencyProfileHash
1156
+ })
1157
+ );
1158
+ } catch (persistError) {
1159
+ logException(
1160
+ persistError,
1161
+ "agent_dispatch_post_delivery_persist_failed",
1162
+ logContext,
1163
+ {},
1164
+ "Failed to persist delivered dispatch state after Slack accepted the reply"
1165
+ );
1166
+ }
1167
+ if (reply.piMessages?.length) {
1168
+ await completeDeliveredTurn({
1169
+ conversationId,
1170
+ sessionId: turnId,
1171
+ sliceId: 1,
1172
+ messages: reply.piMessages,
1173
+ durationMs: reply.diagnostics.durationMs,
1174
+ usage: reply.diagnostics.usage,
1175
+ destination: dispatch.destination,
1176
+ source: dispatch.source,
1177
+ surface: "api",
1178
+ logContext: {
1179
+ threadId: conversationId,
1180
+ channelId: dispatch.destination.channelId,
1181
+ runId: dispatch.id,
1182
+ assistantUserName: botConfig.userName,
1183
+ modelId: reply.diagnostics.modelId
1184
+ }
1185
+ });
1186
+ }
1209
1187
  dispatch = await markDispatch({
1210
1188
  dispatch,
1211
1189
  status: failure ? "failed" : "completed",
@@ -1216,21 +1194,13 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1216
1194
  try {
1217
1195
  await scheduleCompletedTasks({
1218
1196
  conversationId,
1219
- sessionId: getDispatchTurnId(dispatch.id)
1197
+ sessionId: turnId
1220
1198
  });
1221
1199
  } catch (error) {
1222
1200
  logException(
1223
1201
  error,
1224
1202
  "plugin_session_completed_task_schedule_failed",
1225
- {
1226
- conversationId,
1227
- slackThreadId: conversationId,
1228
- slackChannelId: dispatch.destination.channelId,
1229
- runId: dispatch.id,
1230
- actorType: dispatch.actor.type,
1231
- actorId: dispatch.actor.id,
1232
- assistantUserName: botConfig.userName
1233
- },
1203
+ logContext,
1234
1204
  {},
1235
1205
  "Plugin session.completed task scheduling failed"
1236
1206
  );
@@ -1279,13 +1249,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1279
1249
  error,
1280
1250
  "agent_dispatch_run_failed",
1281
1251
  {
1282
- conversationId,
1283
- slackThreadId: conversationId,
1284
- slackChannelId: dispatch.destination.channelId,
1285
- runId: dispatch.id,
1286
- actorType: dispatch.actor.type,
1287
- actorId: dispatch.actor.id,
1288
- assistantUserName: botConfig.userName,
1252
+ ...logContext,
1289
1253
  modelId: botConfig.modelId
1290
1254
  },
1291
1255
  {},
@@ -1506,8 +1470,8 @@ async function drainConversationMailbox2(args) {
1506
1470
  }
1507
1471
  return result;
1508
1472
  }
1509
- async function markConversationMessagesInjected2(args) {
1510
- const result = await markConversationMessagesInjected(args);
1473
+ async function ackMessages2(args) {
1474
+ const result = await ackMessages(args);
1511
1475
  await recordExecutionMetadata(args);
1512
1476
  return result;
1513
1477
  }
@@ -1526,6 +1490,18 @@ async function completeConversationWork2(args) {
1526
1490
  await recordExecutionMetadata(args);
1527
1491
  return result;
1528
1492
  }
1493
+ async function recordAttemptFailure2(args) {
1494
+ const result = await recordAttemptFailure(args);
1495
+ if (result.status === "recorded") {
1496
+ await recordExecutionMetadata(args);
1497
+ }
1498
+ return result;
1499
+ }
1500
+ async function deadLetterAttempt2(args) {
1501
+ const result = await deadLetterAttempt(args);
1502
+ await recordExecutionMetadata(args);
1503
+ return result;
1504
+ }
1529
1505
  async function clearExpiredConversationLease2(args) {
1530
1506
  const result = await clearExpiredConversationLease(args);
1531
1507
  await recordExecutionMetadata(args);
@@ -1593,6 +1569,10 @@ async function ensureSlackAdapterInitialized(args) {
1593
1569
  await args.adapter.initialize({
1594
1570
  getState: () => state
1595
1571
  });
1572
+ const internals = args.adapter;
1573
+ if (internals.defaultBotTokenProvider && !args.adapter.botUserId) {
1574
+ throw new Error("Slack adapter initialized without a resolved bot user id");
1575
+ }
1596
1576
  initializedAdapters.add(args.adapter);
1597
1577
  }
1598
1578
  function verifySlackSignature(args) {
@@ -1640,7 +1620,7 @@ async function runWithSlackInstallation(args) {
1640
1620
  enterpriseId: args.installation.enterpriseId,
1641
1621
  isEnterpriseInstall: args.installation.isEnterpriseInstall
1642
1622
  },
1643
- args.task
1623
+ () => runWithSlackInstallationToken(tokenContext.token, args.task)
1644
1624
  );
1645
1625
  }
1646
1626
 
@@ -2014,14 +1994,22 @@ function createSlackConversationWorker(options) {
2014
1994
  const actorLookup = options.lookupSlackUser ?? lookupSlackUser;
2015
1995
  const state = getConnectedState2(options.state);
2016
1996
  await state.connect();
2017
- const records = getPendingRecords(
2018
- await getConversationWorkState2({
2019
- conversationId: context.conversationId,
2020
- state
2021
- })
2022
- );
1997
+ const records = getPendingRecords({
1998
+ execution: { pendingMessages: [...context.attempt.messages] }
1999
+ });
2023
2000
  if (records.length === 0) {
2024
- await options.resumeAwaitingContinuation(context.conversationId);
2001
+ const destination = requireSlackDestination(
2002
+ context.destination,
2003
+ "Slack continuation recovery"
2004
+ );
2005
+ await runWithSlackInstallation({
2006
+ adapter,
2007
+ installation: { teamId: destination.teamId },
2008
+ state,
2009
+ task: async () => {
2010
+ await options.resumeAwaitingContinuation(context.conversationId);
2011
+ }
2012
+ });
2025
2013
  return { status: "completed" };
2026
2014
  }
2027
2015
  const latestRecord = records[records.length - 1];
@@ -2071,33 +2059,22 @@ function createSlackConversationWorker(options) {
2071
2059
  skipped,
2072
2060
  totalSinceLastHandler: messages.length
2073
2061
  };
2074
- const initialInboundMessageIds = records.map(
2075
- (record) => record.inboundMessageId
2076
- );
2077
- let initialMessagesPersisted = false;
2078
- const markInitialMessagesInjected = async () => {
2079
- if (initialMessagesPersisted) {
2080
- return true;
2062
+ let initialMessagesAcked = false;
2063
+ const ack = async () => {
2064
+ if (initialMessagesAcked) {
2065
+ return;
2081
2066
  }
2082
- const marked = await markConversationMessagesInjected2({
2083
- conversationId: context.conversationId,
2084
- inboundMessageIds: initialInboundMessageIds,
2085
- leaseToken: context.leaseToken,
2086
- conversationStore: options.conversationStore,
2087
- state
2088
- });
2089
- initialMessagesPersisted = marked;
2090
- return marked;
2091
- };
2092
- const onInputCommitted = async () => {
2093
- if (!await markInitialMessagesInjected()) {
2067
+ try {
2068
+ await context.attempt.ack();
2069
+ initialMessagesAcked = true;
2070
+ } catch {
2094
2071
  throw new TurnInputCommitLostError(
2095
- `Conversation work lease lost before Slack input commit for ${context.conversationId}`
2072
+ `Conversation work lease lost before Slack inbox ack for ${context.conversationId}`
2096
2073
  );
2097
2074
  }
2098
2075
  };
2099
- const drainSteeringMessages = async (inject) => {
2100
- await context.drainMailbox(async (pendingRecords) => {
2076
+ const drainSteeringMessages = async (accept) => {
2077
+ await context.attempt.drain(async (pendingRecords) => {
2101
2078
  const messages2 = pendingRecords.map((record) => {
2102
2079
  const metadata = record.input.metadata;
2103
2080
  if (!isSlackMetadata(metadata)) {
@@ -2112,7 +2089,7 @@ function createSlackConversationWorker(options) {
2112
2089
  message
2113
2090
  };
2114
2091
  });
2115
- return await inject(messages2);
2092
+ return await accept(messages2);
2116
2093
  });
2117
2094
  };
2118
2095
  try {
@@ -2121,7 +2098,8 @@ function createSlackConversationWorker(options) {
2121
2098
  destination: context.destination,
2122
2099
  messageContext,
2123
2100
  drainSteeringMessages,
2124
- onInputCommitted,
2101
+ ack,
2102
+ isFinalAttempt: context.attempt.isFinalAttempt,
2125
2103
  shouldYield: context.shouldYield
2126
2104
  });
2127
2105
  return;
@@ -2130,10 +2108,14 @@ function createSlackConversationWorker(options) {
2130
2108
  destination: context.destination,
2131
2109
  messageContext,
2132
2110
  drainSteeringMessages,
2133
- onInputCommitted,
2111
+ ack,
2112
+ isFinalAttempt: context.attempt.isFinalAttempt,
2134
2113
  shouldYield: context.shouldYield
2135
2114
  });
2136
2115
  } catch (error) {
2116
+ if (isTurnInputDeferredError(error)) {
2117
+ return { status: "deferred" };
2118
+ }
2137
2119
  if (isCooperativeTurnYieldError(error)) {
2138
2120
  return { status: "yielded" };
2139
2121
  }
@@ -2144,7 +2126,7 @@ function createSlackConversationWorker(options) {
2144
2126
  }
2145
2127
  }
2146
2128
  });
2147
- if (turnResult?.status === "yielded" || turnResult?.status === "lost_lease") {
2129
+ if (turnResult?.status === "deferred" || turnResult?.status === "yielded" || turnResult?.status === "lost_lease") {
2148
2130
  return turnResult;
2149
2131
  }
2150
2132
  return { status: "completed" };
@@ -2616,11 +2598,29 @@ async function recoverConversationWork(args) {
2616
2598
  state: args.state
2617
2599
  });
2618
2600
  for (const conversationId of ids) {
2601
+ let work;
2619
2602
  try {
2620
- const work = await getConversationWorkState2({
2603
+ work = await getConversationWorkState2({
2621
2604
  conversationId,
2622
2605
  state: args.state
2623
2606
  });
2607
+ } catch (error) {
2608
+ logException(
2609
+ error,
2610
+ "conversation_work_recovery_failed",
2611
+ { conversationId },
2612
+ {},
2613
+ "Conversation work heartbeat recovery failed"
2614
+ );
2615
+ if (isInvalidConversationRecordError(error)) {
2616
+ await removeActiveConversation2({
2617
+ conversationId,
2618
+ state: args.state
2619
+ });
2620
+ }
2621
+ continue;
2622
+ }
2623
+ try {
2624
2624
  if (!work) {
2625
2625
  await removeActiveConversation2({
2626
2626
  conversationId,
@@ -3529,17 +3529,6 @@ function isSlackTitlePermissionError(error) {
3529
3529
  return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
3530
3530
  }
3531
3531
 
3532
- // src/chat/slack/workspace-context.ts
3533
- import { AsyncLocalStorage } from "async_hooks";
3534
- var workspaceTeamIdStorage = new AsyncLocalStorage();
3535
- function runWithWorkspaceTeamId(teamId, fn) {
3536
- if (!teamId) return fn();
3537
- return workspaceTeamIdStorage.run(teamId, fn);
3538
- }
3539
- function getWorkspaceTeamId() {
3540
- return workspaceTeamIdStorage.getStore();
3541
- }
3542
-
3543
3532
  // src/chat/runtime/thread-context.ts
3544
3533
  function escapeRegExp(value) {
3545
3534
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3734,8 +3723,8 @@ function buildAuthPauseResponse(slackUserId, providerDisplayName) {
3734
3723
  }
3735
3724
 
3736
3725
  // src/chat/runtime/request-deadline.ts
3737
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
3738
- var requestDeadlineStorage = new AsyncLocalStorage2();
3726
+ import { AsyncLocalStorage } from "async_hooks";
3727
+ var requestDeadlineStorage = new AsyncLocalStorage();
3739
3728
  function createTurnRequestDeadline(startedAtMs) {
3740
3729
  const requestBudgetMs = Math.max(
3741
3730
  1,
@@ -4014,6 +4003,27 @@ async function resumeSlackTurn(args) {
4014
4003
  footer
4015
4004
  });
4016
4005
  finalReplyDelivered = true;
4006
+ if (replyContext.correlation?.conversationId && replyContext.correlation.turnId && reply.piMessages?.length) {
4007
+ await persistCompletedSessionRecord({
4008
+ conversationId: replyContext.correlation.conversationId,
4009
+ sessionId: replyContext.correlation.turnId,
4010
+ allMessages: reply.piMessages,
4011
+ currentDurationMs: reply.diagnostics.durationMs,
4012
+ currentUsage: reply.diagnostics.usage,
4013
+ destination: replyContext.destination,
4014
+ source: replyContext.source,
4015
+ requester: replyContext.requester,
4016
+ surface: "slack",
4017
+ logContext: {
4018
+ threadId: replyContext.correlation.threadId,
4019
+ requesterId: replyContext.requester?.userId,
4020
+ channelId: runArgs.channelId,
4021
+ runId: replyContext.correlation.runId,
4022
+ assistantUserName: botConfig.userName,
4023
+ modelId: reply.diagnostics.modelId
4024
+ }
4025
+ });
4026
+ }
4017
4027
  await runArgs.onSuccess?.(reply);
4018
4028
  if (reply.diagnostics.outcome === "success" && replyContext.correlation?.conversationId && replyContext.correlation.turnId) {
4019
4029
  try {
@@ -5775,14 +5785,18 @@ function extractMessageChangedMention(body, botUserId, adapter) {
5775
5785
  const teamId = typeof body.team_id === "string" ? body.team_id : void 0;
5776
5786
  const userTeam = typeof event.message.user_team === "string" ? event.message.user_team : void 0;
5777
5787
  const sourceTeam = typeof event.message.source_team === "string" ? event.message.source_team : void 0;
5788
+ const botId = typeof event.message.bot_id === "string" ? event.message.bot_id : void 0;
5789
+ const channelType = typeof event.channel_type === "string" ? event.channel_type : void 0;
5778
5790
  const raw = {
5779
5791
  channel: channelId,
5792
+ ...channelType ? { channel_type: channelType } : {},
5780
5793
  ts: messageTs,
5781
5794
  thread_ts: threadTs,
5782
5795
  user: userId,
5783
5796
  ...teamId ? { team_id: teamId } : {},
5784
5797
  ...userTeam ? { user_team: userTeam } : {},
5785
- ...sourceTeam ? { source_team: sourceTeam } : {}
5798
+ ...sourceTeam ? { source_team: sourceTeam } : {},
5799
+ ...botId ? { bot_id: botId } : {}
5786
5800
  };
5787
5801
  const message = new Message2({
5788
5802
  id: getEditedMentionMessageId(messageTs),
@@ -5798,8 +5812,11 @@ function extractMessageChangedMention(body, botUserId, adapter) {
5798
5812
  // Raw message_changed payloads do not include profile fields.
5799
5813
  userName: "",
5800
5814
  fullName: "",
5801
- isBot: false,
5802
- isMe: false
5815
+ // Mirror the fresh-message parse so the shared ingress author gate
5816
+ // applies: synthesized flags must never launder a bot-authored or
5817
+ // self-authored edit into a routable user message.
5818
+ isBot: Boolean(botId),
5819
+ isMe: userId === botUserId
5803
5820
  }
5804
5821
  });
5805
5822
  Object.defineProperty(message, "adapter", {
@@ -6055,14 +6072,16 @@ async function handleMessageChanged(args) {
6055
6072
  }
6056
6073
  const botUserId = args.adapter.botUserId;
6057
6074
  if (!botUserId) {
6058
- return false;
6075
+ throw new Error(
6076
+ "Slack bot identity is unresolved; cannot classify message_changed event"
6077
+ );
6059
6078
  }
6060
6079
  const result = extractMessageChangedMention(
6061
6080
  args.body,
6062
6081
  botUserId,
6063
6082
  args.adapter
6064
6083
  );
6065
- if (!result) {
6084
+ if (!result || shouldIgnoreMessage(result.message)) {
6066
6085
  return true;
6067
6086
  }
6068
6087
  await persistSlackMessage({
@@ -6374,12 +6393,12 @@ async function handleSlackWebhook(args) {
6374
6393
  try {
6375
6394
  await eventTask;
6376
6395
  } catch (error) {
6377
- if (!(error instanceof SlackEventPersistenceError)) {
6378
- logException(error, "slack_event_enqueue_failed");
6379
- return new Response("ok", { status: 200 });
6396
+ if (error instanceof SlackEventPersistenceError) {
6397
+ logException(error.cause, "slack_event_persist_failed");
6398
+ } else {
6399
+ logException(error, "slack_event_routing_failed");
6380
6400
  }
6381
- logException(error.cause, "slack_event_persist_failed");
6382
- return new Response("Slack event persistence failed", { status: 503 });
6401
+ return new Response("Slack event handling failed", { status: 503 });
6383
6402
  }
6384
6403
  } else {
6385
6404
  enqueue(
@@ -6529,6 +6548,33 @@ async function requestLostLeaseRecovery(args) {
6529
6548
  state: args.options.state
6530
6549
  });
6531
6550
  }
6551
+ async function recordFailedDeliveryAttempt(args) {
6552
+ const failure = await recordAttemptFailure2({
6553
+ conversationId: args.conversationId,
6554
+ inboundMessageIds: args.messageIds,
6555
+ leaseToken: args.leaseToken,
6556
+ conversationStore: args.options.conversationStore,
6557
+ nowMs: args.nowMs,
6558
+ state: args.options.state
6559
+ });
6560
+ for (const message of failure.deadLetteredMessages) {
6561
+ logWarn(
6562
+ "conversation_work_dead_lettered",
6563
+ { conversationId: args.conversationId },
6564
+ {
6565
+ "app.conversation.source": message.source,
6566
+ "app.inbound.attempt_count": message.attemptCount ?? 0,
6567
+ "app.inbound.message_id": message.inboundMessageId,
6568
+ "app.inbound.pending_count": failure.pendingCount
6569
+ },
6570
+ "Conversation work message consumed after exceeding the delivery attempt limit"
6571
+ );
6572
+ }
6573
+ return failure;
6574
+ }
6575
+ function isTerminalFailure(failure) {
6576
+ return failure.status === "recorded" && failure.deadLetteredMessages.length > 0 && failure.pendingCount === 0;
6577
+ }
6532
6578
  function startLeaseCheckIn(args) {
6533
6579
  const timer = setInterval(() => {
6534
6580
  const nowMs = now2(args.options);
@@ -6566,10 +6612,22 @@ function startLeaseCheckIn(args) {
6566
6612
  }
6567
6613
  async function processConversationWork(message, options) {
6568
6614
  const conversationId = message.conversationId;
6569
- const initial = await getConversationWorkState2({
6570
- conversationId,
6571
- state: options.state
6572
- });
6615
+ let initial;
6616
+ try {
6617
+ initial = await getConversationWorkState2({
6618
+ conversationId,
6619
+ state: options.state
6620
+ });
6621
+ } catch (error) {
6622
+ if (isInvalidConversationRecordError(error)) {
6623
+ throw new ConversationQueueMessageRejectedError(
6624
+ "invalid_record",
6625
+ `Conversation record failed validation for ${conversationId}`,
6626
+ { conversationId }
6627
+ );
6628
+ }
6629
+ throw error;
6630
+ }
6573
6631
  if (!initial || countPendingConversationMessages(initial) === 0 && initial.execution.status === "idle" && !initial.execution.lease) {
6574
6632
  if (initial) {
6575
6633
  await clearConsumedConversationWake2({
@@ -6628,6 +6686,14 @@ async function processConversationWork(message, options) {
6628
6686
  }
6629
6687
  const startedAtMs = now2(options);
6630
6688
  const softYieldDeadlineMs = startedAtMs + (options.softYieldAfterMs ?? CONVERSATION_WORK_SOFT_YIELD_AFTER_MS);
6689
+ const leasedWork = await getConversationWorkState2({
6690
+ conversationId,
6691
+ state: options.state
6692
+ });
6693
+ const attemptMessages = leasedWork?.messages ?? initial.messages;
6694
+ const attemptMessageIds = attemptMessages.map(
6695
+ (message2) => message2.inboundMessageId
6696
+ );
6631
6697
  let leaseLost = false;
6632
6698
  const markLeaseLost = () => {
6633
6699
  leaseLost = true;
@@ -6647,10 +6713,43 @@ async function processConversationWork(message, options) {
6647
6713
  },
6648
6714
  "Conversation work lease acquired"
6649
6715
  );
6716
+ const drainInbox = (handle) => drainConversationMailbox2({
6717
+ conversationId,
6718
+ leaseToken: lease.leaseToken,
6719
+ conversationStore: options.conversationStore,
6720
+ handle,
6721
+ nowMs: now2(options),
6722
+ state: options.state
6723
+ });
6724
+ const ack = async () => {
6725
+ const acknowledged = await ackMessages2({
6726
+ conversationId,
6727
+ inboundMessageIds: attemptMessageIds,
6728
+ leaseToken: lease.leaseToken,
6729
+ conversationStore: options.conversationStore,
6730
+ nowMs: now2(options),
6731
+ state: options.state
6732
+ });
6733
+ if (!acknowledged) {
6734
+ markLeaseLost();
6735
+ throw new Error(
6736
+ `Conversation work lease lost before inbox ack for ${conversationId}`
6737
+ );
6738
+ }
6739
+ };
6650
6740
  const workerContext = {
6741
+ attempt: {
6742
+ ack,
6743
+ conversationId,
6744
+ destination,
6745
+ drain: drainInbox,
6746
+ isFinalAttempt: attemptMessages.some(
6747
+ (message2) => isFinalAttempt(message2)
6748
+ ),
6749
+ messages: attemptMessages
6750
+ },
6651
6751
  conversationId,
6652
6752
  destination,
6653
- leaseToken: lease.leaseToken,
6654
6753
  shouldYield: () => leaseLost || now2(options) >= softYieldDeadlineMs,
6655
6754
  checkIn: async () => {
6656
6755
  const checkedIn = await checkInConversationWork2({
@@ -6664,15 +6763,7 @@ async function processConversationWork(message, options) {
6664
6763
  markLeaseLost();
6665
6764
  }
6666
6765
  return checkedIn;
6667
- },
6668
- drainMailbox: (inject) => drainConversationMailbox2({
6669
- conversationId,
6670
- leaseToken: lease.leaseToken,
6671
- conversationStore: options.conversationStore,
6672
- inject,
6673
- nowMs: now2(options),
6674
- state: options.state
6675
- })
6766
+ }
6676
6767
  };
6677
6768
  try {
6678
6769
  const result = await options.run(workerContext);
@@ -6739,6 +6830,51 @@ async function processConversationWork(message, options) {
6739
6830
  );
6740
6831
  return { status: "yielded" };
6741
6832
  }
6833
+ if (result.status === "deferred") {
6834
+ const deferredNowMs = now2(options);
6835
+ const released = await releaseConversationWork2({
6836
+ conversationId,
6837
+ leaseToken: lease.leaseToken,
6838
+ conversationStore: options.conversationStore,
6839
+ nowMs: deferredNowMs,
6840
+ state: options.state
6841
+ });
6842
+ if (!released) {
6843
+ return { status: "lost_lease" };
6844
+ }
6845
+ const wake = await ensureConversationWake({
6846
+ conversationId,
6847
+ conversationStore: options.conversationStore,
6848
+ idempotencyKey: nudgeIdempotencyKey(
6849
+ "deferred",
6850
+ conversationId,
6851
+ deferredNowMs
6852
+ ),
6853
+ nowMs: deferredNowMs,
6854
+ queue: options.queue,
6855
+ state: options.state
6856
+ });
6857
+ return wake.status === "enqueued" ? { status: "pending_requeued" } : { status: "completed" };
6858
+ }
6859
+ if (attemptMessageIds.length > 0) {
6860
+ const failure = await recordFailedDeliveryAttempt({
6861
+ conversationId,
6862
+ leaseToken: lease.leaseToken,
6863
+ nowMs: now2(options),
6864
+ messageIds: attemptMessageIds,
6865
+ options
6866
+ });
6867
+ if (isTerminalFailure(failure)) {
6868
+ await deadLetterAttempt2({
6869
+ conversationId,
6870
+ leaseToken: lease.leaseToken,
6871
+ conversationStore: options.conversationStore,
6872
+ nowMs: now2(options),
6873
+ state: options.state
6874
+ });
6875
+ return { status: "failed" };
6876
+ }
6877
+ }
6742
6878
  const completion = await completeConversationWork2({
6743
6879
  conversationId,
6744
6880
  leaseToken: lease.leaseToken,
@@ -6776,53 +6912,62 @@ async function processConversationWork(message, options) {
6776
6912
  return { status: "completed" };
6777
6913
  } catch (error) {
6778
6914
  const errorNowMs = now2(options);
6915
+ let recoveryRecorded = false;
6779
6916
  try {
6780
- const continuationMarked = await requestConversationContinuation2({
6917
+ const failure = attemptMessageIds.length > 0 ? await recordFailedDeliveryAttempt({
6781
6918
  conversationId,
6782
- destination,
6783
6919
  leaseToken: lease.leaseToken,
6784
- conversationStore: options.conversationStore,
6785
6920
  nowMs: errorNowMs,
6786
- state: options.state
6787
- });
6788
- if (continuationMarked) {
6789
- await ensureConversationWake({
6921
+ messageIds: attemptMessageIds,
6922
+ options
6923
+ }) : void 0;
6924
+ if (failure && isTerminalFailure(failure)) {
6925
+ await deadLetterAttempt2({
6790
6926
  conversationId,
6927
+ leaseToken: lease.leaseToken,
6791
6928
  conversationStore: options.conversationStore,
6792
- idempotencyKey: nudgeIdempotencyKey(
6793
- "error",
6929
+ nowMs: errorNowMs,
6930
+ state: options.state
6931
+ });
6932
+ } else {
6933
+ const continuationMarked = await requestConversationContinuation2({
6934
+ conversationId,
6935
+ destination,
6936
+ leaseToken: lease.leaseToken,
6937
+ conversationStore: options.conversationStore,
6938
+ nowMs: errorNowMs,
6939
+ state: options.state
6940
+ });
6941
+ if (continuationMarked) {
6942
+ await ensureConversationWake({
6794
6943
  conversationId,
6795
- errorNowMs
6796
- ),
6944
+ conversationStore: options.conversationStore,
6945
+ idempotencyKey: nudgeIdempotencyKey(
6946
+ "error",
6947
+ conversationId,
6948
+ errorNowMs
6949
+ ),
6950
+ nowMs: errorNowMs,
6951
+ queue: options.queue,
6952
+ state: options.state
6953
+ });
6954
+ }
6955
+ await releaseConversationWork2({
6956
+ conversationId,
6957
+ leaseToken: lease.leaseToken,
6958
+ conversationStore: options.conversationStore,
6797
6959
  nowMs: errorNowMs,
6798
- queue: options.queue,
6799
6960
  state: options.state
6800
6961
  });
6801
6962
  }
6802
- } catch (requeueError) {
6963
+ recoveryRecorded = true;
6964
+ } catch (recoveryError) {
6803
6965
  logException(
6804
- requeueError,
6966
+ recoveryError,
6805
6967
  "conversation_work_requeue_failed",
6806
6968
  { conversationId },
6807
6969
  {},
6808
- "Conversation work requeue failed after runner error"
6809
- );
6810
- }
6811
- try {
6812
- await releaseConversationWork2({
6813
- conversationId,
6814
- leaseToken: lease.leaseToken,
6815
- conversationStore: options.conversationStore,
6816
- nowMs: errorNowMs,
6817
- state: options.state
6818
- });
6819
- } catch (releaseError) {
6820
- logException(
6821
- releaseError,
6822
- "conversation_work_release_failed",
6823
- { conversationId },
6824
- {},
6825
- "Conversation work release failed after runner error"
6970
+ "Conversation work recovery failed after runner error"
6826
6971
  );
6827
6972
  }
6828
6973
  if (!isProviderRetryError(error)) {
@@ -6836,7 +6981,10 @@ async function processConversationWork(message, options) {
6836
6981
  "Conversation work failed"
6837
6982
  );
6838
6983
  }
6839
- throw error;
6984
+ if (!recoveryRecorded) {
6985
+ throw error;
6986
+ }
6987
+ return { status: "failed" };
6840
6988
  } finally {
6841
6989
  clearInterval(timer);
6842
6990
  }
@@ -7360,7 +7508,7 @@ function combineTurnText(queuedMessages, latestText) {
7360
7508
  // src/chat/runtime/slack-runtime.ts
7361
7509
  var THREAD_OPTOUT_ACK = "Understood. I'll stay out of this thread unless someone @mentions me again.";
7362
7510
  function shouldRethrowTurnControlError(error) {
7363
- return isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error) || isProviderRetryError(error);
7511
+ return isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error) || isTurnInputDeferredError(error) || isProviderRetryError(error);
7364
7512
  }
7365
7513
  async function maybeHandleThreadOptOutDecision(args) {
7366
7514
  if (!args.decision?.shouldUnsubscribe) {
@@ -7394,13 +7542,13 @@ function createAcceptedSteeringDrain(hooks, options) {
7394
7542
  if (!hooks.drainSteeringMessages) {
7395
7543
  return void 0;
7396
7544
  }
7397
- return async (inject, context) => {
7545
+ return async (accept, context) => {
7398
7546
  let interruptedMessages;
7399
7547
  await hooks.drainSteeringMessages(async (messages) => {
7400
7548
  const selection = await options.selectMessages(messages, context);
7401
7549
  await options.onSkipped?.(selection.skipped);
7402
7550
  const interrupted = selection.accepted.filter((accepted) => accepted.mode === "interrupt").map((accepted) => accepted.message);
7403
- await inject(getQueuedMessagesFromSlackMessages(interrupted, options));
7551
+ await accept(getQueuedMessagesFromSlackMessages(interrupted, options));
7404
7552
  interruptedMessages = interrupted;
7405
7553
  await options.onAcceptedForProcessing?.(interrupted);
7406
7554
  return [
@@ -7567,7 +7715,7 @@ function createSlackTurnRuntime(deps) {
7567
7715
  completedAtMs
7568
7716
  });
7569
7717
  }
7570
- await args.onInputCommitted?.();
7718
+ await args.ack?.();
7571
7719
  };
7572
7720
  const selectAcceptedSteeringMessages = async (args) => {
7573
7721
  const selected = [];
@@ -7653,6 +7801,7 @@ function createSlackTurnRuntime(deps) {
7653
7801
  let processingReaction;
7654
7802
  const skippedSteeringMessages = [];
7655
7803
  let completed = false;
7804
+ let acked = false;
7656
7805
  const onTurnCompleted = async () => {
7657
7806
  completed = true;
7658
7807
  await flushSkippedSteeringMessagesBestEffort({
@@ -7701,8 +7850,9 @@ function createSlackTurnRuntime(deps) {
7701
7850
  )
7702
7851
  );
7703
7852
  };
7704
- const onInputCommitted = async () => {
7705
- await hooks.onInputCommitted?.();
7853
+ const ack = async () => {
7854
+ await hooks.ack?.();
7855
+ acked = true;
7706
7856
  await startQueuedProcessingReactions();
7707
7857
  };
7708
7858
  const drainSteeringMessages = createAcceptedSteeringDrain(hooks, {
@@ -7732,7 +7882,7 @@ function createSlackTurnRuntime(deps) {
7732
7882
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
7733
7883
  destination: hooks.destination,
7734
7884
  queuedMessages,
7735
- onInputCommitted,
7885
+ ack,
7736
7886
  onToolInvocation: toolInvocationHook,
7737
7887
  onTurnCompleted,
7738
7888
  drainSteeringMessages,
@@ -7786,6 +7936,9 @@ function createSlackTurnRuntime(deps) {
7786
7936
  {},
7787
7937
  "onNewMention failed"
7788
7938
  );
7939
+ if (!acked && hooks.isFinalAttempt === false) {
7940
+ return;
7941
+ }
7789
7942
  if (!eventId) {
7790
7943
  throw new Error(
7791
7944
  "Sentry did not return an event ID for mention_handler_failed"
@@ -7823,6 +7976,7 @@ function createSlackTurnRuntime(deps) {
7823
7976
  let processingReaction;
7824
7977
  const skippedSteeringMessages = [];
7825
7978
  let completed = false;
7979
+ let acked = false;
7826
7980
  const onTurnCompleted = async () => {
7827
7981
  completed = true;
7828
7982
  await flushSkippedSteeringMessagesBestEffort({
@@ -7889,7 +8043,7 @@ function createSlackTurnRuntime(deps) {
7889
8043
  message,
7890
8044
  decision: { shouldReply: false, reason },
7891
8045
  context: threadContext,
7892
- onInputCommitted: hooks.onInputCommitted,
8046
+ ack: hooks.ack,
7893
8047
  text: combinedText
7894
8048
  });
7895
8049
  return;
@@ -7926,7 +8080,7 @@ function createSlackTurnRuntime(deps) {
7926
8080
  message,
7927
8081
  decision,
7928
8082
  context: threadContext,
7929
- onInputCommitted: hooks.onInputCommitted,
8083
+ ack: hooks.ack,
7930
8084
  preparedState,
7931
8085
  text: combinedText
7932
8086
  });
@@ -7938,7 +8092,7 @@ function createSlackTurnRuntime(deps) {
7938
8092
  message,
7939
8093
  decision,
7940
8094
  context: threadContext,
7941
- onInputCommitted: hooks.onInputCommitted,
8095
+ ack: hooks.ack,
7942
8096
  preparedState,
7943
8097
  text: combinedText
7944
8098
  });
@@ -7983,8 +8137,9 @@ function createSlackTurnRuntime(deps) {
7983
8137
  )
7984
8138
  );
7985
8139
  };
7986
- const onInputCommitted = async () => {
7987
- await hooks.onInputCommitted?.();
8140
+ const ack = async () => {
8141
+ await hooks.ack?.();
8142
+ acked = true;
7988
8143
  await startQueuedProcessingReactions();
7989
8144
  };
7990
8145
  const toolInvocationHook = createToolInvocationHook(
@@ -7997,7 +8152,7 @@ function createSlackTurnRuntime(deps) {
7997
8152
  preparedState,
7998
8153
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
7999
8154
  queuedMessages,
8000
- onInputCommitted,
8155
+ ack,
8001
8156
  onToolInvocation: toolInvocationHook,
8002
8157
  onTurnCompleted,
8003
8158
  drainSteeringMessages,
@@ -8051,6 +8206,9 @@ function createSlackTurnRuntime(deps) {
8051
8206
  {},
8052
8207
  "onSubscribedMessage failed"
8053
8208
  );
8209
+ if (!acked && hooks.isFinalAttempt === false) {
8210
+ return;
8211
+ }
8054
8212
  if (!eventId) {
8055
8213
  throw new Error(
8056
8214
  "Sentry did not return an event ID for subscribed_message_handler_failed"
@@ -9002,6 +9160,7 @@ function createJuniorRuntimeServices(overrides = {}) {
9002
9160
  }
9003
9161
 
9004
9162
  // src/chat/runtime/reply-executor.ts
9163
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS4 } from "chat";
9005
9164
  import { createSlackSource } from "@sentry/junior-plugin-api";
9006
9165
 
9007
9166
  // src/chat/slack/message.ts
@@ -9134,6 +9293,9 @@ async function maybeApplyProviderDefaultConfigRequest(args) {
9134
9293
  }
9135
9294
 
9136
9295
  // src/chat/runtime/reply-executor.ts
9296
+ async function persistThreadStateWithRetry(thread, patch) {
9297
+ await persistWithRetry(() => persistThreadState(thread, patch));
9298
+ }
9137
9299
  function collectCanvasUrls(artifacts) {
9138
9300
  return new Set(
9139
9301
  [
@@ -9145,6 +9307,14 @@ function collectCanvasUrls(artifacts) {
9145
9307
  function turnRequester(requester) {
9146
9308
  return toStoredSlackRequester(requester);
9147
9309
  }
9310
+ function parkedInputKey(message) {
9311
+ if (message.role !== "user") {
9312
+ return void 0;
9313
+ }
9314
+ const first = Array.isArray(message.content) ? message.content[0] : void 0;
9315
+ const text = first && typeof first === "object" && "text" in first ? String(first.text ?? "") : "";
9316
+ return `${message.timestamp}:${text}`;
9317
+ }
9148
9318
  function isResourceEventMessage(message) {
9149
9319
  const raw = message.raw && typeof message.raw === "object" ? message.raw : void 0;
9150
9320
  return raw?.event_type === "resource_event";
@@ -9226,6 +9396,7 @@ function createReplyToThread(deps) {
9226
9396
  channelName,
9227
9397
  channelType: slackChannelType
9228
9398
  });
9399
+ const destinationVisibility = conversationVisibilityFromSlackChannelType(slackChannelType);
9229
9400
  const threadTs = getThreadTs(threadId);
9230
9401
  const assistantThreadContext = getAssistantThreadContext(message);
9231
9402
  const messageTs = getMessageTs(message);
@@ -9238,7 +9409,8 @@ function createReplyToThread(deps) {
9238
9409
  channelId: channelId ?? destination.channelId,
9239
9410
  messageTs,
9240
9411
  teamId,
9241
- threadTs
9412
+ threadTs,
9413
+ type: destinationVisibility === "public" ? "pub" : "priv"
9242
9414
  });
9243
9415
  const runId = getRunId(thread, message);
9244
9416
  const conversationId = threadId ?? runId;
@@ -9365,12 +9537,87 @@ function createReplyToThread(deps) {
9365
9537
  }
9366
9538
  };
9367
9539
  let activeTurnId = preparedState.conversation.processing.activeTurnId;
9540
+ const resolveSteeringMessages = async (queuedMessages) => {
9541
+ return await Promise.all(
9542
+ queuedMessages.map(async (queued) => {
9543
+ const attachments = queued.message.attachments;
9544
+ return {
9545
+ text: queued.userText,
9546
+ timestampMs: queued.message.metadata.dateSent.getTime(),
9547
+ omittedImageAttachmentCount: !isVisionEnabled() && hasPotentialImageAttachment(attachments) ? countPotentialImageAttachments(attachments) : 0,
9548
+ userAttachments: await deps.resolveUserAttachments(
9549
+ attachments,
9550
+ {
9551
+ threadId,
9552
+ requesterId: isResourceEventMessage(queued.message) ? void 0 : queued.message.author.userId,
9553
+ channelId,
9554
+ runId,
9555
+ conversation: preparedState.conversation,
9556
+ messageTs: getSlackMessageTs(queued.message)
9557
+ }
9558
+ )
9559
+ };
9560
+ })
9561
+ );
9562
+ };
9563
+ const appendParkedTurnInput = async (parkedSessionId) => {
9564
+ if (!conversationId) {
9565
+ return true;
9566
+ }
9567
+ const parkedMessages = [
9568
+ ...options.queuedMessages ?? [],
9569
+ {
9570
+ explicitMention: Boolean(
9571
+ options.explicitMention || message.isMention
9572
+ ),
9573
+ message,
9574
+ rawText: currentText.rawText,
9575
+ userText: currentText.userText
9576
+ }
9577
+ ].filter(
9578
+ // Redelivery of the parked turn's own message must not duplicate
9579
+ // the prompt that already started the session.
9580
+ (queued) => buildDeterministicTurnId(queued.message.id) !== parkedSessionId
9581
+ );
9582
+ if (parkedMessages.length === 0) {
9583
+ return true;
9584
+ }
9585
+ const stateAdapter = getStateAdapter();
9586
+ await stateAdapter.connect();
9587
+ const lock = await acquireActiveLock(stateAdapter, conversationId);
9588
+ if (!lock) {
9589
+ return false;
9590
+ }
9591
+ try {
9592
+ const piMessages = (await resolveSteeringMessages(parkedMessages)).map(buildSteeringPiMessage);
9593
+ const projection = await loadProjection({ conversationId });
9594
+ const appendedKeys = new Set(
9595
+ projection.map(parkedInputKey).filter((key) => key !== void 0)
9596
+ );
9597
+ const missing = piMessages.filter((piMessage) => {
9598
+ const key = parkedInputKey(piMessage);
9599
+ return key === void 0 || !appendedKeys.has(key);
9600
+ });
9601
+ if (missing.length === 0) {
9602
+ return true;
9603
+ }
9604
+ await commitMessages({
9605
+ conversationId,
9606
+ messages: [...projection, ...missing],
9607
+ requester: storedRequester,
9608
+ ttlMs: THREAD_STATE_TTL_MS4
9609
+ });
9610
+ return true;
9611
+ } finally {
9612
+ await stateAdapter.releaseLock(lock);
9613
+ }
9614
+ };
9368
9615
  if (preparedState.userMessageAlreadyReplied) {
9369
9616
  await persistThreadState(thread, {
9370
9617
  conversation: preparedState.conversation
9371
9618
  });
9372
9619
  await options.onTurnStatePersisted?.();
9373
- await options.onInputCommitted?.();
9620
+ await options.ack?.();
9374
9621
  await options.onTurnCompleted?.();
9375
9622
  return;
9376
9623
  }
@@ -9380,6 +9627,9 @@ function createReplyToThread(deps) {
9380
9627
  sessionId: activeTurnId
9381
9628
  });
9382
9629
  if (resumeRequest) {
9630
+ if (!await appendParkedTurnInput(resumeRequest.sessionId)) {
9631
+ throw new TurnInputDeferredError();
9632
+ }
9383
9633
  try {
9384
9634
  await deps.services.scheduleAgentContinue(resumeRequest);
9385
9635
  } catch (error) {
@@ -9400,7 +9650,7 @@ function createReplyToThread(deps) {
9400
9650
  conversation: preparedState.conversation
9401
9651
  });
9402
9652
  await options.onTurnStatePersisted?.();
9403
- await options.onInputCommitted?.();
9653
+ await options.ack?.();
9404
9654
  return;
9405
9655
  }
9406
9656
  const sessionRecord = await getAgentTurnSessionRecord(
@@ -9409,27 +9659,34 @@ function createReplyToThread(deps) {
9409
9659
  );
9410
9660
  if (sessionRecord?.state === "awaiting_resume") {
9411
9661
  if (sessionRecord.resumeReason === "auth") {
9412
- await persistThreadState(thread, {
9413
- conversation: preparedState.conversation
9662
+ await abandonAgentTurnSessionRecord({
9663
+ conversationId,
9664
+ sessionId: activeTurnId,
9665
+ errorMessage: "Auth-parked session superseded by a new user message"
9414
9666
  });
9415
- await options.onTurnStatePersisted?.();
9416
- await options.onInputCommitted?.();
9417
- return;
9667
+ markTurnClosed({
9668
+ conversation: preparedState.conversation,
9669
+ nowMs: Date.now(),
9670
+ sessionId: activeTurnId,
9671
+ updateConversationStats
9672
+ });
9673
+ activeTurnId = void 0;
9674
+ } else {
9675
+ await failAgentTurnSessionRecord({
9676
+ conversationId,
9677
+ expectedVersion: sessionRecord.version,
9678
+ sessionId: activeTurnId,
9679
+ errorMessage: "Awaiting agent continuation metadata could not be materialized"
9680
+ });
9681
+ markTurnFailed({
9682
+ conversation: preparedState.conversation,
9683
+ nowMs: Date.now(),
9684
+ sessionId: activeTurnId,
9685
+ markConversationMessage,
9686
+ updateConversationStats
9687
+ });
9688
+ activeTurnId = void 0;
9418
9689
  }
9419
- await failAgentTurnSessionRecord({
9420
- conversationId,
9421
- expectedVersion: sessionRecord.version,
9422
- sessionId: activeTurnId,
9423
- errorMessage: "Awaiting agent continuation metadata could not be materialized"
9424
- });
9425
- markTurnFailed({
9426
- conversation: preparedState.conversation,
9427
- nowMs: Date.now(),
9428
- sessionId: activeTurnId,
9429
- markConversationMessage,
9430
- updateConversationStats
9431
- });
9432
- activeTurnId = void 0;
9433
9690
  }
9434
9691
  }
9435
9692
  const configReply = await maybeApplyProviderDefaultConfigRequest({
@@ -9465,7 +9722,7 @@ function createReplyToThread(deps) {
9465
9722
  conversation: preparedState.conversation
9466
9723
  });
9467
9724
  await options.onTurnStatePersisted?.();
9468
- await options.onInputCommitted?.();
9725
+ await options.ack?.();
9469
9726
  return;
9470
9727
  }
9471
9728
  startActiveTurn({
@@ -9485,6 +9742,7 @@ function createReplyToThread(deps) {
9485
9742
  surface: "slack",
9486
9743
  requester,
9487
9744
  destination,
9745
+ destinationVisibility,
9488
9746
  source,
9489
9747
  traceId: getActiveTraceId()
9490
9748
  }).catch((error) => {
@@ -9601,8 +9859,10 @@ function createReplyToThread(deps) {
9601
9859
  };
9602
9860
  let persistedAtLeastOnce = false;
9603
9861
  let shouldPersistFailureState = true;
9862
+ let finalReplyDelivered = false;
9604
9863
  let latestArtifacts = preparedState.artifacts;
9605
9864
  let assistantTitleArtifacts = {};
9865
+ const hasVisibleSlackDelivery = (post) => post.text.trim().length > 0 || Boolean(post.files?.length);
9606
9866
  try {
9607
9867
  const loadedPiMessages = await loadPiMessagesForTurn({
9608
9868
  conversationId,
@@ -9694,39 +9954,16 @@ function createReplyToThread(deps) {
9694
9954
  );
9695
9955
  });
9696
9956
  const toolChannelId = preparedState.artifacts.assistantContextChannelId ?? channelId;
9697
- const resolveSteeringMessages = async (queuedMessages) => {
9698
- return await Promise.all(
9699
- queuedMessages.map(async (queued) => {
9700
- const attachments = queued.message.attachments;
9701
- return {
9702
- text: queued.userText,
9703
- timestampMs: queued.message.metadata.dateSent.getTime(),
9704
- omittedImageAttachmentCount: !isVisionEnabled() && hasPotentialImageAttachment(attachments) ? countPotentialImageAttachments(attachments) : 0,
9705
- userAttachments: await deps.resolveUserAttachments(
9706
- attachments,
9707
- {
9708
- threadId,
9709
- requesterId: isResourceEventMessage(queued.message) ? void 0 : queued.message.author.userId,
9710
- channelId,
9711
- runId,
9712
- conversation: preparedState.conversation,
9713
- messageTs: getSlackMessageTs(queued.message)
9714
- }
9715
- )
9716
- };
9717
- })
9718
- );
9719
- };
9720
- const drainSteeringMessages = options.drainSteeringMessages ? async (inject) => {
9721
- let injectedMessages;
9957
+ const drainSteeringMessages = options.drainSteeringMessages ? async (accept) => {
9958
+ let acceptedMessages;
9722
9959
  const drained = await options.drainSteeringMessages(
9723
9960
  async (queuedMessages) => {
9724
- injectedMessages = await resolveSteeringMessages(queuedMessages);
9725
- await inject(injectedMessages);
9961
+ acceptedMessages = await resolveSteeringMessages(queuedMessages);
9962
+ await accept(acceptedMessages);
9726
9963
  },
9727
9964
  { conversationContext: preparedState.conversationContext }
9728
9965
  );
9729
- return injectedMessages ?? await resolveSteeringMessages(drained);
9966
+ return acceptedMessages ?? await resolveSteeringMessages(drained);
9730
9967
  } : void 0;
9731
9968
  let reply = await deps.services.generateAssistantReply(
9732
9969
  effectiveUserText,
@@ -9792,7 +10029,7 @@ function createReplyToThread(deps) {
9792
10029
  },
9793
10030
  onStatus: (nextStatus) => status.update(nextStatus),
9794
10031
  onToolInvocation: options.onToolInvocation,
9795
- onInputCommitted: options.onInputCommitted,
10032
+ onInputCommitted: options.ack,
9796
10033
  drainSteeringMessages,
9797
10034
  shouldYield: options.shouldYield
9798
10035
  }
@@ -9815,16 +10052,20 @@ function createReplyToThread(deps) {
9815
10052
  });
9816
10053
  }
9817
10054
  const artifactStatePatch = reply.artifactStatePatch ? { ...reply.artifactStatePatch } : {};
9818
- const reactionPerformed = reply.diagnostics.toolCalls.includes(
9819
- "slackMessageAddReaction"
9820
- );
9821
10055
  const plannedPosts = planSlackReplyPosts({ reply });
9822
10056
  const replyFooter = buildSlackReplyFooter({
9823
10057
  conversationId
9824
10058
  });
9825
10059
  const shouldUseSlackFooter = Boolean(replyFooter) && Boolean(channelId && threadTs) && thread.adapter?.name === "slack";
9826
10060
  if (plannedPosts.length > 0) {
9827
- let sent;
10061
+ const hasVisibleDelivery = plannedPosts.some(
10062
+ hasVisibleSlackDelivery
10063
+ );
10064
+ if (!hasVisibleDelivery) {
10065
+ throw new Error(
10066
+ "Slack final reply plan did not contain visible delivery"
10067
+ );
10068
+ }
9828
10069
  if (shouldUseSlackFooter) {
9829
10070
  const slackChannelId = channelId;
9830
10071
  const slackThreadTs = threadTs;
@@ -9833,7 +10074,7 @@ function createReplyToThread(deps) {
9833
10074
  "Slack footer delivery requires a concrete channel and thread timestamp"
9834
10075
  );
9835
10076
  }
9836
- const sentMessageTs = await postSlackApiReplyPosts({
10077
+ await postSlackApiReplyPosts({
9837
10078
  beforePost: beforeFirstResponsePost,
9838
10079
  channelId: slackChannelId,
9839
10080
  threadTs: slackThreadTs,
@@ -9854,30 +10095,22 @@ function createReplyToThread(deps) {
9854
10095
  );
9855
10096
  }
9856
10097
  });
9857
- if (sentMessageTs) {
9858
- sent = {
9859
- id: sentMessageTs,
9860
- text: reply.text,
9861
- delete: async () => {
9862
- await deleteSlackMessage({
9863
- channelId: slackChannelId,
9864
- timestamp: sentMessageTs
9865
- });
9866
- }
9867
- };
9868
- }
9869
10098
  } else {
9870
10099
  for (const post of plannedPosts) {
9871
- sent = await postThreadReply(
10100
+ if (!hasVisibleSlackDelivery(post)) {
10101
+ continue;
10102
+ }
10103
+ await postThreadReply(
9872
10104
  buildSlackOutputMessage(post.text, post.files),
9873
10105
  post.stage
9874
10106
  );
9875
10107
  }
9876
10108
  }
9877
- const firstPlannedMessageHasFiles = (plannedPosts[0]?.files?.length ?? 0) > 0;
9878
- if (sent && reactionPerformed && plannedPosts.length === 1 && !firstPlannedMessageHasFiles && isRedundantReactionAckText(reply.text)) {
9879
- await sent.delete();
9880
- }
10109
+ finalReplyDelivered = true;
10110
+ shouldPersistFailureState = false;
10111
+ } else {
10112
+ finalReplyDelivered = true;
10113
+ shouldPersistFailureState = false;
9881
10114
  }
9882
10115
  const completedState = buildDeliveredTurnStatePatch({
9883
10116
  artifactStatePatch: {
@@ -9893,27 +10126,63 @@ function createReplyToThread(deps) {
9893
10126
  if (completedState.artifacts) {
9894
10127
  latestArtifacts = completedState.artifacts;
9895
10128
  }
9896
- await persistThreadState(thread, {
9897
- ...completedState
9898
- });
9899
- if (completedState.artifacts && (assistantTitleArtifacts.assistantTitle !== void 0 || assistantTitleArtifacts.assistantTitleSourceMessageId !== void 0) && (completedState.artifacts.assistantTitle !== assistantTitleArtifacts.assistantTitle || completedState.artifacts.assistantTitleSourceMessageId !== assistantTitleArtifacts.assistantTitleSourceMessageId)) {
9900
- await persistThreadState(thread, { artifacts: latestArtifacts });
9901
- }
9902
- if (conversationId) {
9903
- await recordAgentTurnSessionSummary({
9904
- channelName,
9905
- conversationId,
9906
- cumulativeDurationMs: reply.diagnostics.durationMs,
9907
- cumulativeUsage: reply.diagnostics.usage,
9908
- sessionId: turnId,
9909
- sliceId: 1,
9910
- startedAtMs: message.metadata.dateSent.getTime(),
9911
- state: "completed",
9912
- requester,
9913
- destination,
9914
- source,
9915
- traceId: getActiveTraceId()
10129
+ try {
10130
+ if (conversationId && reply.piMessages?.length) {
10131
+ await completeDeliveredTurn({
10132
+ channelName,
10133
+ conversationId,
10134
+ durationMs: reply.diagnostics.durationMs,
10135
+ usage: reply.diagnostics.usage,
10136
+ destination,
10137
+ destinationVisibility,
10138
+ source,
10139
+ sessionId: turnId,
10140
+ sliceId: 1,
10141
+ messages: reply.piMessages,
10142
+ logContext: {
10143
+ threadId,
10144
+ requesterId: slackRequesterId,
10145
+ channelId,
10146
+ runId,
10147
+ assistantUserName: botConfig.userName,
10148
+ modelId: reply.diagnostics.modelId
10149
+ },
10150
+ requester,
10151
+ surface: "slack"
10152
+ });
10153
+ } else if (conversationId) {
10154
+ await recordAgentTurnSessionSummary({
10155
+ channelName,
10156
+ conversationId,
10157
+ cumulativeDurationMs: reply.diagnostics.durationMs,
10158
+ cumulativeUsage: reply.diagnostics.usage,
10159
+ sessionId: turnId,
10160
+ sliceId: 1,
10161
+ startedAtMs: message.metadata.dateSent.getTime(),
10162
+ state: "completed",
10163
+ requester,
10164
+ destination,
10165
+ destinationVisibility,
10166
+ source,
10167
+ traceId: getActiveTraceId()
10168
+ });
10169
+ }
10170
+ await persistThreadStateWithRetry(thread, {
10171
+ ...completedState
9916
10172
  });
10173
+ if (completedState.artifacts && (assistantTitleArtifacts.assistantTitle !== void 0 || assistantTitleArtifacts.assistantTitleSourceMessageId !== void 0) && (completedState.artifacts.assistantTitle !== assistantTitleArtifacts.assistantTitle || completedState.artifacts.assistantTitleSourceMessageId !== assistantTitleArtifacts.assistantTitleSourceMessageId)) {
10174
+ await persistThreadStateWithRetry(thread, {
10175
+ artifacts: latestArtifacts
10176
+ });
10177
+ }
10178
+ } catch (commitError) {
10179
+ logException(
10180
+ commitError,
10181
+ "slack_reply_post_delivery_commit_failed",
10182
+ turnTraceContext,
10183
+ messageTs ? { "messaging.message.id": messageTs } : {},
10184
+ "Post-delivery turn state persistence failed after Slack accepted the reply"
10185
+ );
9917
10186
  }
9918
10187
  preparedState.conversation = completedState.conversation;
9919
10188
  persistedAtLeastOnce = true;
@@ -9947,6 +10216,17 @@ function createReplyToThread(deps) {
9947
10216
  }
9948
10217
  }
9949
10218
  } catch (error) {
10219
+ if (finalReplyDelivered) {
10220
+ shouldPersistFailureState = false;
10221
+ logException(
10222
+ error,
10223
+ "slack_reply_post_delivery_commit_failed",
10224
+ turnTraceContext,
10225
+ messageTs ? { "messaging.message.id": messageTs } : {},
10226
+ "Post-delivery turn work failed after Slack accepted the reply"
10227
+ );
10228
+ return;
10229
+ }
9950
10230
  if (isCooperativeTurnYieldError(error)) {
9951
10231
  shouldPersistFailureState = false;
9952
10232
  throw error;
@@ -10121,6 +10401,7 @@ function createReplyToThread(deps) {
10121
10401
  state: "failed",
10122
10402
  requester,
10123
10403
  destination,
10404
+ destinationVisibility,
10124
10405
  source,
10125
10406
  traceId: getActiveTraceId()
10126
10407
  });
@@ -10558,7 +10839,7 @@ function createJuniorSlackAdapter(config) {
10558
10839
 
10559
10840
  // src/chat/runtime/agent-continue-runner.ts
10560
10841
  var AGENT_CONTINUE_LOCK_RETRY_DELAYS_MS = [250, 1e3, 2e3];
10561
- function sleep(ms) {
10842
+ function sleep2(ms) {
10562
10843
  return new Promise((resolve) => setTimeout(resolve, ms));
10563
10844
  }
10564
10845
  async function persistCompletedReplyState2(args) {
@@ -10645,6 +10926,28 @@ async function failContinuationStartup(args) {
10645
10926
  );
10646
10927
  }
10647
10928
  }
10929
+ async function resolveContinuationRequester(args) {
10930
+ const stored = args.sessionRecordRequester;
10931
+ if (stored?.platform === "slack" && stored.teamId === args.teamId && stored.userId === args.userId) {
10932
+ return createSlackResumeRequester({
10933
+ requester: stored,
10934
+ teamId: args.teamId,
10935
+ userId: args.userId
10936
+ });
10937
+ }
10938
+ const work = await getConversationWorkState2({
10939
+ conversationId: args.conversationId
10940
+ });
10941
+ const workRequester = work?.requester;
10942
+ if (workRequester && workRequester.teamId === args.teamId && workRequester.slackUserId === args.userId) {
10943
+ return createSlackRequester(args.teamId, args.userId, {
10944
+ email: workRequester.email,
10945
+ fullName: workRequester.fullName,
10946
+ userName: workRequester.slackUserName
10947
+ });
10948
+ }
10949
+ return void 0;
10950
+ }
10648
10951
  function isContinuationResume(summary) {
10649
10952
  return summary.state === "awaiting_resume" && (summary.resumeReason === "timeout" || summary.resumeReason === "yield");
10650
10953
  }
@@ -10708,11 +11011,20 @@ async function continueSlackAgentRun(payload, options = {}) {
10708
11011
  payload.destination,
10709
11012
  "Slack continuation"
10710
11013
  );
10711
- const requester = createSlackResumeRequester({
10712
- requester: activeSessionRecord.requester,
11014
+ const requester = await resolveContinuationRequester({
11015
+ conversationId: payload.conversationId,
11016
+ sessionRecordRequester: activeSessionRecord.requester,
10713
11017
  teamId: destination.teamId,
10714
11018
  userId: userMessage2.author.userId
10715
11019
  });
11020
+ if (!requester) {
11021
+ await failStrandedSessionWithFallback({
11022
+ conversationId: payload.conversationId,
11023
+ errorMessage: "Stored Slack requester missing for continuation",
11024
+ sessionRecord: activeSessionRecord
11025
+ });
11026
+ return false;
11027
+ }
10716
11028
  if (!activeSessionRecord.source) {
10717
11029
  await failAgentTurnSessionRecord({
10718
11030
  conversationId: payload.conversationId,
@@ -10822,8 +11134,119 @@ async function continueSlackAgentRun(payload, options = {}) {
10822
11134
  }
10823
11135
  });
10824
11136
  }
11137
+ async function failStrandedSessionWithFallback(args) {
11138
+ await failAgentTurnSessionRecord({
11139
+ conversationId: args.conversationId,
11140
+ expectedVersion: args.sessionRecord.version,
11141
+ sessionId: args.sessionRecord.sessionId,
11142
+ errorMessage: args.errorMessage
11143
+ });
11144
+ const currentState = await getPersistedThreadState(args.conversationId);
11145
+ const conversation = coerceThreadConversationState(currentState);
11146
+ markTurnFailed({
11147
+ conversation,
11148
+ nowMs: Date.now(),
11149
+ sessionId: args.sessionRecord.sessionId,
11150
+ userMessageId: getTurnUserMessage(
11151
+ conversation,
11152
+ args.sessionRecord.sessionId
11153
+ )?.id,
11154
+ markConversationMessage,
11155
+ updateConversationStats
11156
+ });
11157
+ await persistThreadStateById(args.conversationId, { conversation });
11158
+ const thread = parseSlackThreadId(args.conversationId);
11159
+ if (!thread) {
11160
+ return;
11161
+ }
11162
+ const eventName = "agent_turn_stranded_session_failed";
11163
+ const eventId = logException(
11164
+ new Error(args.errorMessage),
11165
+ eventName,
11166
+ { conversationId: args.conversationId },
11167
+ {
11168
+ "app.ai.conversation_id": args.conversationId,
11169
+ "app.ai.session_id": args.sessionRecord.sessionId
11170
+ },
11171
+ "Stranded running agent session terminally failed"
11172
+ );
11173
+ await postSlackMessage({
11174
+ channelId: thread.channelId,
11175
+ threadTs: thread.threadTs,
11176
+ text: buildTurnFailureResponse(
11177
+ requireTurnFailureEventId(eventId, eventName)
11178
+ )
11179
+ });
11180
+ }
11181
+ async function recoverStrandedRunningSession(args) {
11182
+ const stateAdapter = getStateAdapter();
11183
+ await stateAdapter.connect();
11184
+ const probe = await acquireActiveLock(stateAdapter, args.conversationId);
11185
+ if (!probe) {
11186
+ return false;
11187
+ }
11188
+ await stateAdapter.releaseLock(probe);
11189
+ const sessionRecord = await getAgentTurnSessionRecord(
11190
+ args.conversationId,
11191
+ args.summary.sessionId
11192
+ );
11193
+ if (!sessionRecord || sessionRecord.state !== "running") {
11194
+ return false;
11195
+ }
11196
+ const parked = await persistYieldSessionRecord({
11197
+ channelName: sessionRecord.channelName,
11198
+ conversationId: args.conversationId,
11199
+ sessionId: sessionRecord.sessionId,
11200
+ currentSliceId: sessionRecord.sliceId,
11201
+ destination: sessionRecord.destination,
11202
+ source: sessionRecord.source,
11203
+ messages: sessionRecord.piMessages,
11204
+ errorMessage: "Recovered running session after hard worker death",
11205
+ logContext: { modelId: botConfig.modelId },
11206
+ requester: sessionRecord.requester,
11207
+ surface: sessionRecord.surface
11208
+ });
11209
+ if (!parked) {
11210
+ await failStrandedSessionWithFallback({
11211
+ conversationId: args.conversationId,
11212
+ errorMessage: "Stranded running session had no resumable boundary after worker death",
11213
+ sessionRecord
11214
+ });
11215
+ return false;
11216
+ }
11217
+ const request = await getAwaitingAgentContinueRequest({
11218
+ conversationId: args.conversationId,
11219
+ sessionId: sessionRecord.sessionId
11220
+ });
11221
+ if (!request) {
11222
+ await failStrandedSessionWithFallback({
11223
+ conversationId: args.conversationId,
11224
+ errorMessage: "Stranded running session could not materialize continuation metadata",
11225
+ sessionRecord: parked
11226
+ });
11227
+ return false;
11228
+ }
11229
+ if (await continueSlackAgentRunWithLockRetry(request, args.options)) {
11230
+ return true;
11231
+ }
11232
+ await failUnresumableContinuation({
11233
+ conversationId: args.conversationId,
11234
+ expectedVersion: request.expectedVersion,
11235
+ summary: args.summary,
11236
+ errorMessage: "Awaiting agent continuation was stale before it could run"
11237
+ });
11238
+ return false;
11239
+ }
10825
11240
  async function resumeAwaitingSlackContinuation(conversationId, options = {}) {
10826
11241
  const summaries = await listAgentTurnSessionSummariesForConversation(conversationId);
11242
+ const newest = summaries[0];
11243
+ if (newest?.state === "running") {
11244
+ return await recoverStrandedRunningSession({
11245
+ conversationId,
11246
+ options,
11247
+ summary: newest
11248
+ });
11249
+ }
10827
11250
  for (const summary of summaries) {
10828
11251
  if (!isContinuationResume(summary)) {
10829
11252
  continue;
@@ -10889,7 +11312,7 @@ async function continueSlackAgentRunWithLockRetry(payload, options = {}) {
10889
11312
  },
10890
11313
  "Agent continuation lock was busy; retrying"
10891
11314
  );
10892
- await sleep(delayMs);
11315
+ await sleep2(delayMs);
10893
11316
  }
10894
11317
  }
10895
11318
  return true;