@sentry/junior 0.85.0 → 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 (47) hide show
  1. package/dist/{agent-hooks-AV7CHLUO.js → agent-hooks-NWOUB3UR.js} +7 -7
  2. package/dist/app.js +788 -333
  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/respond.d.ts +7 -1
  9. package/dist/chat/runtime/reply-executor.d.ts +2 -10
  10. package/dist/chat/runtime/slack-runtime.d.ts +5 -4
  11. package/dist/chat/runtime/turn.d.ts +7 -0
  12. package/dist/chat/services/persist-retry.d.ts +2 -0
  13. package/dist/chat/services/turn-session-record.d.ts +31 -2
  14. package/dist/chat/slack/client.d.ts +33 -2
  15. package/dist/chat/slack/conversation-context.d.ts +15 -0
  16. package/dist/chat/state/turn-session.d.ts +9 -0
  17. package/dist/chat/task-execution/queue.d.ts +1 -1
  18. package/dist/chat/task-execution/slack-work.d.ts +10 -2
  19. package/dist/chat/task-execution/state.d.ts +44 -4
  20. package/dist/chat/task-execution/store.d.ts +21 -4
  21. package/dist/chat/task-execution/worker.d.ts +11 -4
  22. package/dist/chat/tools/slack/channel-access.d.ts +29 -0
  23. package/dist/chat/tools/slack/thread-read.d.ts +4 -1
  24. package/dist/{chunk-UVNV67EC.js → chunk-2NFV5FMB.js} +4 -4
  25. package/dist/{chunk-GY7L6VCT.js → chunk-37B2R2QJ.js} +15 -10
  26. package/dist/{chunk-QTTTRBNB.js → chunk-6I6HBOQM.js} +80 -15
  27. package/dist/{chunk-ENEWQMRJ.js → chunk-6O5UI3RG.js} +1 -1
  28. package/dist/{chunk-MTE5NRPJ.js → chunk-BRSQQRG6.js} +1 -1
  29. package/dist/{chunk-EENOFKDN.js → chunk-ENPSU7L7.js} +165 -27
  30. package/dist/{chunk-MHEV3T2Y.js → chunk-FPHA6GCQ.js} +876 -719
  31. package/dist/{chunk-RUB77TGS.js → chunk-GB5DFM4D.js} +1 -1
  32. package/dist/{chunk-C2PSUWK3.js → chunk-GGD6WK6V.js} +124 -25
  33. package/dist/{chunk-SSUEWAVS.js → chunk-JRXCSSSU.js} +19 -4
  34. package/dist/{chunk-DR75T7J3.js → chunk-L7OHKDOX.js} +9 -5
  35. package/dist/{chunk-FYPO4K7C.js → chunk-RIB3M6YA.js} +2 -2
  36. package/dist/{chunk-BU2AWPEN.js → chunk-ZU2ALUVQ.js} +3 -2
  37. package/dist/cli/chat.js +4 -4
  38. package/dist/cli/plugins.js +7 -7
  39. package/dist/cli/snapshot-warmup.js +4 -4
  40. package/dist/cli/upgrade.js +10 -7
  41. package/dist/{db-S3HYBLUS.js → db-SZVUU7RB.js} +4 -4
  42. package/dist/instrumentation.js +1 -1
  43. package/dist/nitro.js +4 -4
  44. package/dist/reporting/conversations.d.ts +2 -1
  45. package/dist/reporting.js +95 -30
  46. package/dist/{runner-IMQH6V2B.js → runner-HEBRPNN2.js} +27 -13
  47. package/package.json +6 -6
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-MHEV3T2Y.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-EENOFKDN.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-FYPO4K7C.js";
109
+ import "./chunk-RIB3M6YA.js";
97
110
  import {
98
111
  getVercelConversationWorkQueue,
99
112
  resolveConversationWorkQueueTopic,
100
113
  verifyConversationQueueMessage
101
- } from "./chunk-ENEWQMRJ.js";
114
+ } from "./chunk-6O5UI3RG.js";
102
115
  import {
103
116
  PLUGIN_TASK_QUEUE_TOPIC,
104
117
  createVercelQueueClient,
@@ -107,18 +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-SSUEWAVS.js";
128
+ } from "./chunk-JRXCSSSU.js";
115
129
  import {
116
130
  abandonAgentTurnSessionRecord,
117
131
  buildSlackOutputMessage,
118
132
  commitMessages,
119
133
  failAgentTurnSessionRecord,
120
134
  getAgentTurnSessionRecord,
121
- getInterruptionMarker,
122
135
  listAgentTurnSessionSummariesForConversation,
123
136
  loadProjection,
124
137
  normalizeSlackStatusText,
@@ -126,7 +139,7 @@ import {
126
139
  recordAuthorizationCompleted,
127
140
  splitSlackReplyText,
128
141
  truncateStatusText
129
- } from "./chunk-GY7L6VCT.js";
142
+ } from "./chunk-37B2R2QJ.js";
130
143
  import {
131
144
  validatePluginEgressCredentialHooks,
132
145
  validatePluginRegistrations
@@ -146,20 +159,20 @@ import {
146
159
  setPlugins,
147
160
  validatePlugins,
148
161
  verifySlackDirectCredentialSubject
149
- } from "./chunk-UVNV67EC.js";
162
+ } from "./chunk-2NFV5FMB.js";
150
163
  import {
151
164
  createPluginLogger,
152
165
  createPluginState
153
- } from "./chunk-MTE5NRPJ.js";
166
+ } from "./chunk-BRSQQRG6.js";
154
167
  import {
155
168
  getConversationStore,
156
169
  getDb
157
- } from "./chunk-QTTTRBNB.js";
170
+ } from "./chunk-6I6HBOQM.js";
158
171
  import "./chunk-G3E7SCME.js";
159
172
  import {
160
173
  acquireActiveLock,
161
174
  getStateAdapter
162
- } from "./chunk-RUB77TGS.js";
175
+ } from "./chunk-GB5DFM4D.js";
163
176
  import {
164
177
  SlackActionError,
165
178
  createSlackDestination,
@@ -167,15 +180,17 @@ import {
167
180
  downloadPrivateSlackFile,
168
181
  getHeaderString,
169
182
  getSlackClient,
183
+ getWorkspaceTeamId,
170
184
  isDmChannel,
171
185
  normalizeSlackConversationId,
172
186
  parseDestination,
173
187
  requireSlackDestination,
188
+ runWithSlackInstallationToken,
189
+ runWithWorkspaceTeamId,
174
190
  sameDestination
175
- } from "./chunk-C2PSUWK3.js";
191
+ } from "./chunk-GGD6WK6V.js";
176
192
  import {
177
193
  FUNCTION_TIMEOUT_BUFFER_SECONDS,
178
- GEN_AI_PROVIDER_NAME,
179
194
  botConfig,
180
195
  completeObject,
181
196
  completeText,
@@ -193,12 +208,12 @@ import {
193
208
  stripRuntimeTurnContext,
194
209
  trimTrailingAssistantMessages,
195
210
  unwrapCurrentInstruction
196
- } from "./chunk-BU2AWPEN.js";
211
+ } from "./chunk-ZU2ALUVQ.js";
197
212
  import {
198
213
  parseSlackThreadId,
199
214
  resolveSlackChannelIdFromMessage,
200
215
  resolveSlackChannelIdFromThreadId
201
- } from "./chunk-DR75T7J3.js";
216
+ } from "./chunk-L7OHKDOX.js";
202
217
  import {
203
218
  discoverSkills
204
219
  } from "./chunk-64ACM6AP.js";
@@ -495,96 +510,25 @@ async function postSlackApiReplyPosts(args) {
495
510
  return lastPostedMessageTs;
496
511
  }
497
512
 
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
- };
513
+ // src/chat/services/persist-retry.ts
514
+ var PERSIST_ATTEMPTS = 3;
515
+ function sleep(ms) {
516
+ return new Promise((resolve) => setTimeout(resolve, ms));
557
517
  }
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"
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
+ }
586
529
  }
587
- };
530
+ }
531
+ throw lastError;
588
532
  }
589
533
 
590
534
  // src/chat/agent-dispatch/signing.ts
@@ -1035,6 +979,16 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1035
979
  }
1036
980
  let dispatch = claimedDispatch;
1037
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
+ };
1038
992
  const destinationLockId = getDispatchDestinationLockId(dispatch.destination);
1039
993
  const stateAdapter = getStateAdapter();
1040
994
  await stateAdapter.connect();
@@ -1119,7 +1073,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1119
1073
  correlation: {
1120
1074
  conversationId,
1121
1075
  threadId: conversationId,
1122
- turnId: getDispatchTurnId(dispatch.id),
1076
+ turnId,
1123
1077
  runId: dispatch.id,
1124
1078
  channelId: dispatch.destination.channelId,
1125
1079
  teamId: dispatch.destination.teamId
@@ -1159,13 +1113,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1159
1113
  reply,
1160
1114
  logException,
1161
1115
  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,
1116
+ ...logContext,
1169
1117
  modelId: reply.diagnostics.modelId
1170
1118
  }
1171
1119
  });
@@ -1199,13 +1147,45 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1199
1147
  });
1200
1148
  updateConversationStats(conversation);
1201
1149
  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
- });
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
+ }
1209
1189
  dispatch = await markDispatch({
1210
1190
  dispatch,
1211
1191
  status: failure ? "failed" : "completed",
@@ -1216,21 +1196,13 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1216
1196
  try {
1217
1197
  await scheduleCompletedTasks({
1218
1198
  conversationId,
1219
- sessionId: getDispatchTurnId(dispatch.id)
1199
+ sessionId: turnId
1220
1200
  });
1221
1201
  } catch (error) {
1222
1202
  logException(
1223
1203
  error,
1224
1204
  "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
- },
1205
+ logContext,
1234
1206
  {},
1235
1207
  "Plugin session.completed task scheduling failed"
1236
1208
  );
@@ -1279,13 +1251,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
1279
1251
  error,
1280
1252
  "agent_dispatch_run_failed",
1281
1253
  {
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,
1254
+ ...logContext,
1289
1255
  modelId: botConfig.modelId
1290
1256
  },
1291
1257
  {},
@@ -1506,8 +1472,8 @@ async function drainConversationMailbox2(args) {
1506
1472
  }
1507
1473
  return result;
1508
1474
  }
1509
- async function markConversationMessagesInjected2(args) {
1510
- const result = await markConversationMessagesInjected(args);
1475
+ async function ackMessages2(args) {
1476
+ const result = await ackMessages(args);
1511
1477
  await recordExecutionMetadata(args);
1512
1478
  return result;
1513
1479
  }
@@ -1526,6 +1492,18 @@ async function completeConversationWork2(args) {
1526
1492
  await recordExecutionMetadata(args);
1527
1493
  return result;
1528
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
+ }
1529
1507
  async function clearExpiredConversationLease2(args) {
1530
1508
  const result = await clearExpiredConversationLease(args);
1531
1509
  await recordExecutionMetadata(args);
@@ -1593,6 +1571,10 @@ async function ensureSlackAdapterInitialized(args) {
1593
1571
  await args.adapter.initialize({
1594
1572
  getState: () => state
1595
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
+ }
1596
1578
  initializedAdapters.add(args.adapter);
1597
1579
  }
1598
1580
  function verifySlackSignature(args) {
@@ -1640,7 +1622,7 @@ async function runWithSlackInstallation(args) {
1640
1622
  enterpriseId: args.installation.enterpriseId,
1641
1623
  isEnterpriseInstall: args.installation.isEnterpriseInstall
1642
1624
  },
1643
- args.task
1625
+ () => runWithSlackInstallationToken(tokenContext.token, args.task)
1644
1626
  );
1645
1627
  }
1646
1628
 
@@ -2014,14 +1996,22 @@ function createSlackConversationWorker(options) {
2014
1996
  const actorLookup = options.lookupSlackUser ?? lookupSlackUser;
2015
1997
  const state = getConnectedState2(options.state);
2016
1998
  await state.connect();
2017
- const records = getPendingRecords(
2018
- await getConversationWorkState2({
2019
- conversationId: context.conversationId,
2020
- state
2021
- })
2022
- );
1999
+ const records = getPendingRecords({
2000
+ execution: { pendingMessages: [...context.attempt.messages] }
2001
+ });
2023
2002
  if (records.length === 0) {
2024
- 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
+ });
2025
2015
  return { status: "completed" };
2026
2016
  }
2027
2017
  const latestRecord = records[records.length - 1];
@@ -2071,33 +2061,22 @@ function createSlackConversationWorker(options) {
2071
2061
  skipped,
2072
2062
  totalSinceLastHandler: messages.length
2073
2063
  };
2074
- const initialInboundMessageIds = records.map(
2075
- (record) => record.inboundMessageId
2076
- );
2077
- let initialMessagesPersisted = false;
2078
- const markInitialMessagesInjected = async () => {
2079
- if (initialMessagesPersisted) {
2080
- return true;
2064
+ let initialMessagesAcked = false;
2065
+ const ack = async () => {
2066
+ if (initialMessagesAcked) {
2067
+ return;
2081
2068
  }
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()) {
2069
+ try {
2070
+ await context.attempt.ack();
2071
+ initialMessagesAcked = true;
2072
+ } catch {
2094
2073
  throw new TurnInputCommitLostError(
2095
- `Conversation work lease lost before Slack input commit for ${context.conversationId}`
2074
+ `Conversation work lease lost before Slack inbox ack for ${context.conversationId}`
2096
2075
  );
2097
2076
  }
2098
2077
  };
2099
- const drainSteeringMessages = async (inject) => {
2100
- await context.drainMailbox(async (pendingRecords) => {
2078
+ const drainSteeringMessages = async (accept) => {
2079
+ await context.attempt.drain(async (pendingRecords) => {
2101
2080
  const messages2 = pendingRecords.map((record) => {
2102
2081
  const metadata = record.input.metadata;
2103
2082
  if (!isSlackMetadata(metadata)) {
@@ -2112,7 +2091,7 @@ function createSlackConversationWorker(options) {
2112
2091
  message
2113
2092
  };
2114
2093
  });
2115
- return await inject(messages2);
2094
+ return await accept(messages2);
2116
2095
  });
2117
2096
  };
2118
2097
  try {
@@ -2121,7 +2100,8 @@ function createSlackConversationWorker(options) {
2121
2100
  destination: context.destination,
2122
2101
  messageContext,
2123
2102
  drainSteeringMessages,
2124
- onInputCommitted,
2103
+ ack,
2104
+ isFinalAttempt: context.attempt.isFinalAttempt,
2125
2105
  shouldYield: context.shouldYield
2126
2106
  });
2127
2107
  return;
@@ -2130,10 +2110,14 @@ function createSlackConversationWorker(options) {
2130
2110
  destination: context.destination,
2131
2111
  messageContext,
2132
2112
  drainSteeringMessages,
2133
- onInputCommitted,
2113
+ ack,
2114
+ isFinalAttempt: context.attempt.isFinalAttempt,
2134
2115
  shouldYield: context.shouldYield
2135
2116
  });
2136
2117
  } catch (error) {
2118
+ if (isTurnInputDeferredError(error)) {
2119
+ return { status: "deferred" };
2120
+ }
2137
2121
  if (isCooperativeTurnYieldError(error)) {
2138
2122
  return { status: "yielded" };
2139
2123
  }
@@ -2144,7 +2128,7 @@ function createSlackConversationWorker(options) {
2144
2128
  }
2145
2129
  }
2146
2130
  });
2147
- if (turnResult?.status === "yielded" || turnResult?.status === "lost_lease") {
2131
+ if (turnResult?.status === "deferred" || turnResult?.status === "yielded" || turnResult?.status === "lost_lease") {
2148
2132
  return turnResult;
2149
2133
  }
2150
2134
  return { status: "completed" };
@@ -2616,11 +2600,29 @@ async function recoverConversationWork(args) {
2616
2600
  state: args.state
2617
2601
  });
2618
2602
  for (const conversationId of ids) {
2603
+ let work;
2619
2604
  try {
2620
- const work = await getConversationWorkState2({
2605
+ work = await getConversationWorkState2({
2621
2606
  conversationId,
2622
2607
  state: args.state
2623
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 {
2624
2626
  if (!work) {
2625
2627
  await removeActiveConversation2({
2626
2628
  conversationId,
@@ -3529,17 +3531,6 @@ function isSlackTitlePermissionError(error) {
3529
3531
  return code === "no_permission" || code === "missing_scope" || code === "not_allowed_token_type";
3530
3532
  }
3531
3533
 
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
3534
  // src/chat/runtime/thread-context.ts
3544
3535
  function escapeRegExp(value) {
3545
3536
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3734,8 +3725,8 @@ function buildAuthPauseResponse(slackUserId, providerDisplayName) {
3734
3725
  }
3735
3726
 
3736
3727
  // src/chat/runtime/request-deadline.ts
3737
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
3738
- var requestDeadlineStorage = new AsyncLocalStorage2();
3728
+ import { AsyncLocalStorage } from "async_hooks";
3729
+ var requestDeadlineStorage = new AsyncLocalStorage();
3739
3730
  function createTurnRequestDeadline(startedAtMs) {
3740
3731
  const requestBudgetMs = Math.max(
3741
3732
  1,
@@ -4014,6 +4005,27 @@ async function resumeSlackTurn(args) {
4014
4005
  footer
4015
4006
  });
4016
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
+ }
4017
4029
  await runArgs.onSuccess?.(reply);
4018
4030
  if (reply.diagnostics.outcome === "success" && replyContext.correlation?.conversationId && replyContext.correlation.turnId) {
4019
4031
  try {
@@ -5775,14 +5787,18 @@ function extractMessageChangedMention(body, botUserId, adapter) {
5775
5787
  const teamId = typeof body.team_id === "string" ? body.team_id : void 0;
5776
5788
  const userTeam = typeof event.message.user_team === "string" ? event.message.user_team : void 0;
5777
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;
5778
5792
  const raw = {
5779
5793
  channel: channelId,
5794
+ ...channelType ? { channel_type: channelType } : {},
5780
5795
  ts: messageTs,
5781
5796
  thread_ts: threadTs,
5782
5797
  user: userId,
5783
5798
  ...teamId ? { team_id: teamId } : {},
5784
5799
  ...userTeam ? { user_team: userTeam } : {},
5785
- ...sourceTeam ? { source_team: sourceTeam } : {}
5800
+ ...sourceTeam ? { source_team: sourceTeam } : {},
5801
+ ...botId ? { bot_id: botId } : {}
5786
5802
  };
5787
5803
  const message = new Message2({
5788
5804
  id: getEditedMentionMessageId(messageTs),
@@ -5798,8 +5814,11 @@ function extractMessageChangedMention(body, botUserId, adapter) {
5798
5814
  // Raw message_changed payloads do not include profile fields.
5799
5815
  userName: "",
5800
5816
  fullName: "",
5801
- isBot: false,
5802
- 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
5803
5822
  }
5804
5823
  });
5805
5824
  Object.defineProperty(message, "adapter", {
@@ -6055,14 +6074,16 @@ async function handleMessageChanged(args) {
6055
6074
  }
6056
6075
  const botUserId = args.adapter.botUserId;
6057
6076
  if (!botUserId) {
6058
- return false;
6077
+ throw new Error(
6078
+ "Slack bot identity is unresolved; cannot classify message_changed event"
6079
+ );
6059
6080
  }
6060
6081
  const result = extractMessageChangedMention(
6061
6082
  args.body,
6062
6083
  botUserId,
6063
6084
  args.adapter
6064
6085
  );
6065
- if (!result) {
6086
+ if (!result || shouldIgnoreMessage(result.message)) {
6066
6087
  return true;
6067
6088
  }
6068
6089
  await persistSlackMessage({
@@ -6374,12 +6395,12 @@ async function handleSlackWebhook(args) {
6374
6395
  try {
6375
6396
  await eventTask;
6376
6397
  } catch (error) {
6377
- if (!(error instanceof SlackEventPersistenceError)) {
6378
- logException(error, "slack_event_enqueue_failed");
6379
- 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");
6380
6402
  }
6381
- logException(error.cause, "slack_event_persist_failed");
6382
- return new Response("Slack event persistence failed", { status: 503 });
6403
+ return new Response("Slack event handling failed", { status: 503 });
6383
6404
  }
6384
6405
  } else {
6385
6406
  enqueue(
@@ -6529,6 +6550,33 @@ async function requestLostLeaseRecovery(args) {
6529
6550
  state: args.options.state
6530
6551
  });
6531
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
+ }
6532
6580
  function startLeaseCheckIn(args) {
6533
6581
  const timer = setInterval(() => {
6534
6582
  const nowMs = now2(args.options);
@@ -6566,10 +6614,22 @@ function startLeaseCheckIn(args) {
6566
6614
  }
6567
6615
  async function processConversationWork(message, options) {
6568
6616
  const conversationId = message.conversationId;
6569
- const initial = await getConversationWorkState2({
6570
- conversationId,
6571
- state: options.state
6572
- });
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
+ }
6573
6633
  if (!initial || countPendingConversationMessages(initial) === 0 && initial.execution.status === "idle" && !initial.execution.lease) {
6574
6634
  if (initial) {
6575
6635
  await clearConsumedConversationWake2({
@@ -6628,6 +6688,14 @@ async function processConversationWork(message, options) {
6628
6688
  }
6629
6689
  const startedAtMs = now2(options);
6630
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
+ );
6631
6699
  let leaseLost = false;
6632
6700
  const markLeaseLost = () => {
6633
6701
  leaseLost = true;
@@ -6647,10 +6715,43 @@ async function processConversationWork(message, options) {
6647
6715
  },
6648
6716
  "Conversation work lease acquired"
6649
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
+ };
6650
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
+ },
6651
6753
  conversationId,
6652
6754
  destination,
6653
- leaseToken: lease.leaseToken,
6654
6755
  shouldYield: () => leaseLost || now2(options) >= softYieldDeadlineMs,
6655
6756
  checkIn: async () => {
6656
6757
  const checkedIn = await checkInConversationWork2({
@@ -6664,15 +6765,7 @@ async function processConversationWork(message, options) {
6664
6765
  markLeaseLost();
6665
6766
  }
6666
6767
  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
- })
6768
+ }
6676
6769
  };
6677
6770
  try {
6678
6771
  const result = await options.run(workerContext);
@@ -6739,6 +6832,51 @@ async function processConversationWork(message, options) {
6739
6832
  );
6740
6833
  return { status: "yielded" };
6741
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
+ }
6742
6880
  const completion = await completeConversationWork2({
6743
6881
  conversationId,
6744
6882
  leaseToken: lease.leaseToken,
@@ -6776,53 +6914,62 @@ async function processConversationWork(message, options) {
6776
6914
  return { status: "completed" };
6777
6915
  } catch (error) {
6778
6916
  const errorNowMs = now2(options);
6917
+ let recoveryRecorded = false;
6779
6918
  try {
6780
- const continuationMarked = await requestConversationContinuation2({
6919
+ const failure = attemptMessageIds.length > 0 ? await recordFailedDeliveryAttempt({
6781
6920
  conversationId,
6782
- destination,
6783
6921
  leaseToken: lease.leaseToken,
6784
- conversationStore: options.conversationStore,
6785
6922
  nowMs: errorNowMs,
6786
- state: options.state
6787
- });
6788
- if (continuationMarked) {
6789
- await ensureConversationWake({
6923
+ messageIds: attemptMessageIds,
6924
+ options
6925
+ }) : void 0;
6926
+ if (failure && isTerminalFailure(failure)) {
6927
+ await deadLetterAttempt2({
6928
+ conversationId,
6929
+ leaseToken: lease.leaseToken,
6930
+ conversationStore: options.conversationStore,
6931
+ nowMs: errorNowMs,
6932
+ state: options.state
6933
+ });
6934
+ } else {
6935
+ const continuationMarked = await requestConversationContinuation2({
6790
6936
  conversationId,
6937
+ destination,
6938
+ leaseToken: lease.leaseToken,
6791
6939
  conversationStore: options.conversationStore,
6792
- idempotencyKey: nudgeIdempotencyKey(
6793
- "error",
6940
+ nowMs: errorNowMs,
6941
+ state: options.state
6942
+ });
6943
+ if (continuationMarked) {
6944
+ await ensureConversationWake({
6794
6945
  conversationId,
6795
- errorNowMs
6796
- ),
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,
6797
6961
  nowMs: errorNowMs,
6798
- queue: options.queue,
6799
6962
  state: options.state
6800
6963
  });
6801
6964
  }
6802
- } catch (requeueError) {
6965
+ recoveryRecorded = true;
6966
+ } catch (recoveryError) {
6803
6967
  logException(
6804
- requeueError,
6968
+ recoveryError,
6805
6969
  "conversation_work_requeue_failed",
6806
6970
  { conversationId },
6807
6971
  {},
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"
6972
+ "Conversation work recovery failed after runner error"
6826
6973
  );
6827
6974
  }
6828
6975
  if (!isProviderRetryError(error)) {
@@ -6836,7 +6983,10 @@ async function processConversationWork(message, options) {
6836
6983
  "Conversation work failed"
6837
6984
  );
6838
6985
  }
6839
- throw error;
6986
+ if (!recoveryRecorded) {
6987
+ throw error;
6988
+ }
6989
+ return { status: "failed" };
6840
6990
  } finally {
6841
6991
  clearInterval(timer);
6842
6992
  }
@@ -7360,7 +7510,7 @@ function combineTurnText(queuedMessages, latestText) {
7360
7510
  // src/chat/runtime/slack-runtime.ts
7361
7511
  var THREAD_OPTOUT_ACK = "Understood. I'll stay out of this thread unless someone @mentions me again.";
7362
7512
  function shouldRethrowTurnControlError(error) {
7363
- return isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error) || isProviderRetryError(error);
7513
+ return isCooperativeTurnYieldError(error) || isTurnInputCommitLostError(error) || isTurnInputDeferredError(error) || isProviderRetryError(error);
7364
7514
  }
7365
7515
  async function maybeHandleThreadOptOutDecision(args) {
7366
7516
  if (!args.decision?.shouldUnsubscribe) {
@@ -7394,13 +7544,13 @@ function createAcceptedSteeringDrain(hooks, options) {
7394
7544
  if (!hooks.drainSteeringMessages) {
7395
7545
  return void 0;
7396
7546
  }
7397
- return async (inject, context) => {
7547
+ return async (accept, context) => {
7398
7548
  let interruptedMessages;
7399
7549
  await hooks.drainSteeringMessages(async (messages) => {
7400
7550
  const selection = await options.selectMessages(messages, context);
7401
7551
  await options.onSkipped?.(selection.skipped);
7402
7552
  const interrupted = selection.accepted.filter((accepted) => accepted.mode === "interrupt").map((accepted) => accepted.message);
7403
- await inject(getQueuedMessagesFromSlackMessages(interrupted, options));
7553
+ await accept(getQueuedMessagesFromSlackMessages(interrupted, options));
7404
7554
  interruptedMessages = interrupted;
7405
7555
  await options.onAcceptedForProcessing?.(interrupted);
7406
7556
  return [
@@ -7567,7 +7717,7 @@ function createSlackTurnRuntime(deps) {
7567
7717
  completedAtMs
7568
7718
  });
7569
7719
  }
7570
- await args.onInputCommitted?.();
7720
+ await args.ack?.();
7571
7721
  };
7572
7722
  const selectAcceptedSteeringMessages = async (args) => {
7573
7723
  const selected = [];
@@ -7653,6 +7803,7 @@ function createSlackTurnRuntime(deps) {
7653
7803
  let processingReaction;
7654
7804
  const skippedSteeringMessages = [];
7655
7805
  let completed = false;
7806
+ let acked = false;
7656
7807
  const onTurnCompleted = async () => {
7657
7808
  completed = true;
7658
7809
  await flushSkippedSteeringMessagesBestEffort({
@@ -7701,8 +7852,9 @@ function createSlackTurnRuntime(deps) {
7701
7852
  )
7702
7853
  );
7703
7854
  };
7704
- const onInputCommitted = async () => {
7705
- await hooks.onInputCommitted?.();
7855
+ const ack = async () => {
7856
+ await hooks.ack?.();
7857
+ acked = true;
7706
7858
  await startQueuedProcessingReactions();
7707
7859
  };
7708
7860
  const drainSteeringMessages = createAcceptedSteeringDrain(hooks, {
@@ -7732,7 +7884,7 @@ function createSlackTurnRuntime(deps) {
7732
7884
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
7733
7885
  destination: hooks.destination,
7734
7886
  queuedMessages,
7735
- onInputCommitted,
7887
+ ack,
7736
7888
  onToolInvocation: toolInvocationHook,
7737
7889
  onTurnCompleted,
7738
7890
  drainSteeringMessages,
@@ -7786,6 +7938,9 @@ function createSlackTurnRuntime(deps) {
7786
7938
  {},
7787
7939
  "onNewMention failed"
7788
7940
  );
7941
+ if (!acked && hooks.isFinalAttempt === false) {
7942
+ return;
7943
+ }
7789
7944
  if (!eventId) {
7790
7945
  throw new Error(
7791
7946
  "Sentry did not return an event ID for mention_handler_failed"
@@ -7823,6 +7978,7 @@ function createSlackTurnRuntime(deps) {
7823
7978
  let processingReaction;
7824
7979
  const skippedSteeringMessages = [];
7825
7980
  let completed = false;
7981
+ let acked = false;
7826
7982
  const onTurnCompleted = async () => {
7827
7983
  completed = true;
7828
7984
  await flushSkippedSteeringMessagesBestEffort({
@@ -7889,7 +8045,7 @@ function createSlackTurnRuntime(deps) {
7889
8045
  message,
7890
8046
  decision: { shouldReply: false, reason },
7891
8047
  context: threadContext,
7892
- onInputCommitted: hooks.onInputCommitted,
8048
+ ack: hooks.ack,
7893
8049
  text: combinedText
7894
8050
  });
7895
8051
  return;
@@ -7926,7 +8082,7 @@ function createSlackTurnRuntime(deps) {
7926
8082
  message,
7927
8083
  decision,
7928
8084
  context: threadContext,
7929
- onInputCommitted: hooks.onInputCommitted,
8085
+ ack: hooks.ack,
7930
8086
  preparedState,
7931
8087
  text: combinedText
7932
8088
  });
@@ -7938,7 +8094,7 @@ function createSlackTurnRuntime(deps) {
7938
8094
  message,
7939
8095
  decision,
7940
8096
  context: threadContext,
7941
- onInputCommitted: hooks.onInputCommitted,
8097
+ ack: hooks.ack,
7942
8098
  preparedState,
7943
8099
  text: combinedText
7944
8100
  });
@@ -7983,8 +8139,9 @@ function createSlackTurnRuntime(deps) {
7983
8139
  )
7984
8140
  );
7985
8141
  };
7986
- const onInputCommitted = async () => {
7987
- await hooks.onInputCommitted?.();
8142
+ const ack = async () => {
8143
+ await hooks.ack?.();
8144
+ acked = true;
7988
8145
  await startQueuedProcessingReactions();
7989
8146
  };
7990
8147
  const toolInvocationHook = createToolInvocationHook(
@@ -7997,7 +8154,7 @@ function createSlackTurnRuntime(deps) {
7997
8154
  preparedState,
7998
8155
  beforeFirstResponsePost: hooks.beforeFirstResponsePost,
7999
8156
  queuedMessages,
8000
- onInputCommitted,
8157
+ ack,
8001
8158
  onToolInvocation: toolInvocationHook,
8002
8159
  onTurnCompleted,
8003
8160
  drainSteeringMessages,
@@ -8051,6 +8208,9 @@ function createSlackTurnRuntime(deps) {
8051
8208
  {},
8052
8209
  "onSubscribedMessage failed"
8053
8210
  );
8211
+ if (!acked && hooks.isFinalAttempt === false) {
8212
+ return;
8213
+ }
8054
8214
  if (!eventId) {
8055
8215
  throw new Error(
8056
8216
  "Sentry did not return an event ID for subscribed_message_handler_failed"
@@ -9002,6 +9162,7 @@ function createJuniorRuntimeServices(overrides = {}) {
9002
9162
  }
9003
9163
 
9004
9164
  // src/chat/runtime/reply-executor.ts
9165
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS4 } from "chat";
9005
9166
  import { createSlackSource } from "@sentry/junior-plugin-api";
9006
9167
 
9007
9168
  // src/chat/slack/message.ts
@@ -9134,6 +9295,9 @@ async function maybeApplyProviderDefaultConfigRequest(args) {
9134
9295
  }
9135
9296
 
9136
9297
  // src/chat/runtime/reply-executor.ts
9298
+ async function persistThreadStateWithRetry(thread, patch) {
9299
+ await persistWithRetry(() => persistThreadState(thread, patch));
9300
+ }
9137
9301
  function collectCanvasUrls(artifacts) {
9138
9302
  return new Set(
9139
9303
  [
@@ -9145,6 +9309,14 @@ function collectCanvasUrls(artifacts) {
9145
9309
  function turnRequester(requester) {
9146
9310
  return toStoredSlackRequester(requester);
9147
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
+ }
9148
9320
  function isResourceEventMessage(message) {
9149
9321
  const raw = message.raw && typeof message.raw === "object" ? message.raw : void 0;
9150
9322
  return raw?.event_type === "resource_event";
@@ -9226,6 +9398,7 @@ function createReplyToThread(deps) {
9226
9398
  channelName,
9227
9399
  channelType: slackChannelType
9228
9400
  });
9401
+ const destinationVisibility = conversationVisibilityFromSlackChannelType(slackChannelType);
9229
9402
  const threadTs = getThreadTs(threadId);
9230
9403
  const assistantThreadContext = getAssistantThreadContext(message);
9231
9404
  const messageTs = getMessageTs(message);
@@ -9238,7 +9411,8 @@ function createReplyToThread(deps) {
9238
9411
  channelId: channelId ?? destination.channelId,
9239
9412
  messageTs,
9240
9413
  teamId,
9241
- threadTs
9414
+ threadTs,
9415
+ type: destinationVisibility === "public" ? "pub" : "priv"
9242
9416
  });
9243
9417
  const runId = getRunId(thread, message);
9244
9418
  const conversationId = threadId ?? runId;
@@ -9365,12 +9539,87 @@ function createReplyToThread(deps) {
9365
9539
  }
9366
9540
  };
9367
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
+ };
9368
9617
  if (preparedState.userMessageAlreadyReplied) {
9369
9618
  await persistThreadState(thread, {
9370
9619
  conversation: preparedState.conversation
9371
9620
  });
9372
9621
  await options.onTurnStatePersisted?.();
9373
- await options.onInputCommitted?.();
9622
+ await options.ack?.();
9374
9623
  await options.onTurnCompleted?.();
9375
9624
  return;
9376
9625
  }
@@ -9380,6 +9629,9 @@ function createReplyToThread(deps) {
9380
9629
  sessionId: activeTurnId
9381
9630
  });
9382
9631
  if (resumeRequest) {
9632
+ if (!await appendParkedTurnInput(resumeRequest.sessionId)) {
9633
+ throw new TurnInputDeferredError();
9634
+ }
9383
9635
  try {
9384
9636
  await deps.services.scheduleAgentContinue(resumeRequest);
9385
9637
  } catch (error) {
@@ -9400,7 +9652,7 @@ function createReplyToThread(deps) {
9400
9652
  conversation: preparedState.conversation
9401
9653
  });
9402
9654
  await options.onTurnStatePersisted?.();
9403
- await options.onInputCommitted?.();
9655
+ await options.ack?.();
9404
9656
  return;
9405
9657
  }
9406
9658
  const sessionRecord = await getAgentTurnSessionRecord(
@@ -9409,27 +9661,34 @@ function createReplyToThread(deps) {
9409
9661
  );
9410
9662
  if (sessionRecord?.state === "awaiting_resume") {
9411
9663
  if (sessionRecord.resumeReason === "auth") {
9412
- await persistThreadState(thread, {
9413
- conversation: preparedState.conversation
9664
+ await abandonAgentTurnSessionRecord({
9665
+ conversationId,
9666
+ sessionId: activeTurnId,
9667
+ errorMessage: "Auth-parked session superseded by a new user message"
9414
9668
  });
9415
- await options.onTurnStatePersisted?.();
9416
- await options.onInputCommitted?.();
9417
- 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;
9418
9691
  }
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
9692
  }
9434
9693
  }
9435
9694
  const configReply = await maybeApplyProviderDefaultConfigRequest({
@@ -9465,7 +9724,7 @@ function createReplyToThread(deps) {
9465
9724
  conversation: preparedState.conversation
9466
9725
  });
9467
9726
  await options.onTurnStatePersisted?.();
9468
- await options.onInputCommitted?.();
9727
+ await options.ack?.();
9469
9728
  return;
9470
9729
  }
9471
9730
  startActiveTurn({
@@ -9485,6 +9744,7 @@ function createReplyToThread(deps) {
9485
9744
  surface: "slack",
9486
9745
  requester,
9487
9746
  destination,
9747
+ destinationVisibility,
9488
9748
  source,
9489
9749
  traceId: getActiveTraceId()
9490
9750
  }).catch((error) => {
@@ -9601,8 +9861,10 @@ function createReplyToThread(deps) {
9601
9861
  };
9602
9862
  let persistedAtLeastOnce = false;
9603
9863
  let shouldPersistFailureState = true;
9864
+ let finalReplyDelivered = false;
9604
9865
  let latestArtifacts = preparedState.artifacts;
9605
9866
  let assistantTitleArtifacts = {};
9867
+ const hasVisibleSlackDelivery = (post) => post.text.trim().length > 0 || Boolean(post.files?.length);
9606
9868
  try {
9607
9869
  const loadedPiMessages = await loadPiMessagesForTurn({
9608
9870
  conversationId,
@@ -9694,39 +9956,16 @@ function createReplyToThread(deps) {
9694
9956
  );
9695
9957
  });
9696
9958
  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;
9959
+ const drainSteeringMessages = options.drainSteeringMessages ? async (accept) => {
9960
+ let acceptedMessages;
9722
9961
  const drained = await options.drainSteeringMessages(
9723
9962
  async (queuedMessages) => {
9724
- injectedMessages = await resolveSteeringMessages(queuedMessages);
9725
- await inject(injectedMessages);
9963
+ acceptedMessages = await resolveSteeringMessages(queuedMessages);
9964
+ await accept(acceptedMessages);
9726
9965
  },
9727
9966
  { conversationContext: preparedState.conversationContext }
9728
9967
  );
9729
- return injectedMessages ?? await resolveSteeringMessages(drained);
9968
+ return acceptedMessages ?? await resolveSteeringMessages(drained);
9730
9969
  } : void 0;
9731
9970
  let reply = await deps.services.generateAssistantReply(
9732
9971
  effectiveUserText,
@@ -9792,7 +10031,7 @@ function createReplyToThread(deps) {
9792
10031
  },
9793
10032
  onStatus: (nextStatus) => status.update(nextStatus),
9794
10033
  onToolInvocation: options.onToolInvocation,
9795
- onInputCommitted: options.onInputCommitted,
10034
+ onInputCommitted: options.ack,
9796
10035
  drainSteeringMessages,
9797
10036
  shouldYield: options.shouldYield
9798
10037
  }
@@ -9825,6 +10064,14 @@ function createReplyToThread(deps) {
9825
10064
  const shouldUseSlackFooter = Boolean(replyFooter) && Boolean(channelId && threadTs) && thread.adapter?.name === "slack";
9826
10065
  if (plannedPosts.length > 0) {
9827
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
+ }
9828
10075
  if (shouldUseSlackFooter) {
9829
10076
  const slackChannelId = channelId;
9830
10077
  const slackThreadTs = threadTs;
@@ -9868,16 +10115,34 @@ function createReplyToThread(deps) {
9868
10115
  }
9869
10116
  } else {
9870
10117
  for (const post of plannedPosts) {
10118
+ if (!hasVisibleSlackDelivery(post)) {
10119
+ continue;
10120
+ }
9871
10121
  sent = await postThreadReply(
9872
10122
  buildSlackOutputMessage(post.text, post.files),
9873
10123
  post.stage
9874
10124
  );
9875
10125
  }
9876
10126
  }
10127
+ finalReplyDelivered = true;
10128
+ shouldPersistFailureState = false;
9877
10129
  const firstPlannedMessageHasFiles = (plannedPosts[0]?.files?.length ?? 0) > 0;
9878
10130
  if (sent && reactionPerformed && plannedPosts.length === 1 && !firstPlannedMessageHasFiles && isRedundantReactionAckText(reply.text)) {
9879
- 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
+ }
9880
10142
  }
10143
+ } else {
10144
+ finalReplyDelivered = true;
10145
+ shouldPersistFailureState = false;
9881
10146
  }
9882
10147
  const completedState = buildDeliveredTurnStatePatch({
9883
10148
  artifactStatePatch: {
@@ -9893,27 +10158,63 @@ function createReplyToThread(deps) {
9893
10158
  if (completedState.artifacts) {
9894
10159
  latestArtifacts = completedState.artifacts;
9895
10160
  }
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()
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
9916
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
+ );
9917
10218
  }
9918
10219
  preparedState.conversation = completedState.conversation;
9919
10220
  persistedAtLeastOnce = true;
@@ -9947,6 +10248,17 @@ function createReplyToThread(deps) {
9947
10248
  }
9948
10249
  }
9949
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
+ }
9950
10262
  if (isCooperativeTurnYieldError(error)) {
9951
10263
  shouldPersistFailureState = false;
9952
10264
  throw error;
@@ -10121,6 +10433,7 @@ function createReplyToThread(deps) {
10121
10433
  state: "failed",
10122
10434
  requester,
10123
10435
  destination,
10436
+ destinationVisibility,
10124
10437
  source,
10125
10438
  traceId: getActiveTraceId()
10126
10439
  });
@@ -10558,7 +10871,7 @@ function createJuniorSlackAdapter(config) {
10558
10871
 
10559
10872
  // src/chat/runtime/agent-continue-runner.ts
10560
10873
  var AGENT_CONTINUE_LOCK_RETRY_DELAYS_MS = [250, 1e3, 2e3];
10561
- function sleep(ms) {
10874
+ function sleep2(ms) {
10562
10875
  return new Promise((resolve) => setTimeout(resolve, ms));
10563
10876
  }
10564
10877
  async function persistCompletedReplyState2(args) {
@@ -10645,6 +10958,28 @@ async function failContinuationStartup(args) {
10645
10958
  );
10646
10959
  }
10647
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
+ }
10648
10983
  function isContinuationResume(summary) {
10649
10984
  return summary.state === "awaiting_resume" && (summary.resumeReason === "timeout" || summary.resumeReason === "yield");
10650
10985
  }
@@ -10708,11 +11043,20 @@ async function continueSlackAgentRun(payload, options = {}) {
10708
11043
  payload.destination,
10709
11044
  "Slack continuation"
10710
11045
  );
10711
- const requester = createSlackResumeRequester({
10712
- requester: activeSessionRecord.requester,
11046
+ const requester = await resolveContinuationRequester({
11047
+ conversationId: payload.conversationId,
11048
+ sessionRecordRequester: activeSessionRecord.requester,
10713
11049
  teamId: destination.teamId,
10714
11050
  userId: userMessage2.author.userId
10715
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
+ }
10716
11060
  if (!activeSessionRecord.source) {
10717
11061
  await failAgentTurnSessionRecord({
10718
11062
  conversationId: payload.conversationId,
@@ -10822,8 +11166,119 @@ async function continueSlackAgentRun(payload, options = {}) {
10822
11166
  }
10823
11167
  });
10824
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
+ }
10825
11272
  async function resumeAwaitingSlackContinuation(conversationId, options = {}) {
10826
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
+ }
10827
11282
  for (const summary of summaries) {
10828
11283
  if (!isContinuationResume(summary)) {
10829
11284
  continue;
@@ -10889,7 +11344,7 @@ async function continueSlackAgentRunWithLockRetry(payload, options = {}) {
10889
11344
  },
10890
11345
  "Agent continuation lock was busy; retrying"
10891
11346
  );
10892
- await sleep(delayMs);
11347
+ await sleep2(delayMs);
10893
11348
  }
10894
11349
  }
10895
11350
  return true;