@sentry/junior 0.71.3 → 0.72.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 (56) hide show
  1. package/bin/junior.mjs +10 -0
  2. package/dist/api-reference.d.ts +2 -0
  3. package/dist/app.d.ts +5 -5
  4. package/dist/app.js +1039 -1971
  5. package/dist/chat/agent-dispatch/heartbeat.d.ts +0 -6
  6. package/dist/chat/mcp/errors.d.ts +3 -0
  7. package/dist/chat/mcp/tool-manager.d.ts +5 -1
  8. package/dist/chat/plugins/agent-hooks.d.ts +3 -2
  9. package/dist/chat/requester.d.ts +60 -0
  10. package/dist/chat/respond.d.ts +2 -6
  11. package/dist/chat/runtime/agent-continue-runner.d.ts +25 -0
  12. package/dist/chat/runtime/reply-executor.d.ts +4 -4
  13. package/dist/chat/runtime/turn.d.ts +2 -2
  14. package/dist/chat/services/agent-continue.d.ts +27 -0
  15. package/dist/chat/services/message-actor-identity.d.ts +12 -4
  16. package/dist/chat/services/turn-session-record.d.ts +10 -7
  17. package/dist/chat/slack/user.d.ts +4 -4
  18. package/dist/chat/state/adapter.d.ts +2 -0
  19. package/dist/chat/state/conversation-details.d.ts +4 -3
  20. package/dist/chat/state/session-log.d.ts +43 -0
  21. package/dist/chat/state/turn-session.d.ts +7 -10
  22. package/dist/chat/task-execution/slack-work.d.ts +5 -5
  23. package/dist/chat/task-execution/store.d.ts +83 -48
  24. package/dist/chat/task-execution/worker.d.ts +3 -3
  25. package/dist/chat/tools/definition.d.ts +3 -0
  26. package/dist/chat/tools/execution/tool-error-handler.d.ts +2 -1
  27. package/dist/chat/tools/types.d.ts +2 -5
  28. package/dist/{chunk-R62YWUNO.js → chunk-3FYPXHPL.js} +10 -28
  29. package/dist/chunk-4JXCSGSA.js +212 -0
  30. package/dist/{chunk-GT67ZWZQ.js → chunk-55XEZFGD.js} +5 -3
  31. package/dist/{chunk-BBXYXOJW.js → chunk-6GEYPE6T.js} +18 -523
  32. package/dist/chunk-G3E7SCME.js +28 -0
  33. package/dist/{chunk-UXG6TU2U.js → chunk-GB3AL54K.js} +8 -93
  34. package/dist/chunk-HNMUVGSR.js +1119 -0
  35. package/dist/{chunk-XE2VFQQN.js → chunk-ICKIDP7G.js} +1 -1
  36. package/dist/chunk-KVZL5NZS.js +519 -0
  37. package/dist/chunk-PP7AGSBU.js +185 -0
  38. package/dist/{chunk-B5HKWWQB.js → chunk-VLIO6RQR.js} +8 -6
  39. package/dist/{chunk-HOGQL2H6.js → chunk-VSNA5KAB.js} +177 -101
  40. package/dist/{chunk-76YMBKW7.js → chunk-XC33FJZN.js} +4 -12
  41. package/dist/{chunk-JS4HURDT.js → chunk-ZJQPA67D.js} +25 -25
  42. package/dist/cli/check.js +10 -8
  43. package/dist/cli/run.js +9 -1
  44. package/dist/cli/snapshot-warmup.js +10 -7
  45. package/dist/cli/upgrade.js +599 -0
  46. package/dist/nitro.d.ts +1 -1
  47. package/dist/nitro.js +5 -4
  48. package/dist/plugins.d.ts +1 -1
  49. package/dist/reporting/conversations.d.ts +116 -0
  50. package/dist/reporting.d.ts +24 -129
  51. package/dist/reporting.js +310 -158
  52. package/package.json +3 -3
  53. package/dist/chat/runtime/timeout-resume-runner.d.ts +0 -19
  54. package/dist/chat/services/requester-identity.d.ts +0 -19
  55. package/dist/chat/services/timeout-resume.d.ts +0 -23
  56. package/dist/handlers/turn-resume.d.ts +0 -4
package/dist/app.js CHANGED
@@ -1,3 +1,11 @@
1
+ import {
2
+ defineJuniorPlugins,
3
+ getVercelConversationWorkQueue,
4
+ pluginCatalogConfigFromPluginSet,
5
+ pluginHookRegistrationsFromPluginSet,
6
+ resolveConversationWorkQueueTopic,
7
+ verifyConversationQueueMessage
8
+ } from "./chunk-ICKIDP7G.js";
1
9
  import {
2
10
  GET,
3
11
  JUNIOR_PERSONALITY,
@@ -20,7 +28,6 @@ import {
20
28
  getAgentTurnSessionRecord,
21
29
  getInterruptionMarker,
22
30
  initConversationContext,
23
- listAgentTurnSessionSummaries,
24
31
  listAgentTurnSessionSummariesForConversation,
25
32
  loadConnectedMcpProviders,
26
33
  loadProjection,
@@ -39,13 +46,53 @@ import {
39
46
  upsertAgentTurnSessionRecord,
40
47
  validateAgentPlugins,
41
48
  verifySlackDirectCredentialSubject
42
- } from "./chunk-HOGQL2H6.js";
49
+ } from "./chunk-VSNA5KAB.js";
43
50
  import {
44
51
  discoverSkills,
45
52
  findSkillByName,
46
53
  loadSkillsByName,
47
54
  parseSkillInvocation
48
- } from "./chunk-GT67ZWZQ.js";
55
+ } from "./chunk-55XEZFGD.js";
56
+ import {
57
+ buildConversationStatePatch,
58
+ coerceThreadConversationState
59
+ } from "./chunk-4JXCSGSA.js";
60
+ import {
61
+ CONVERSATION_WORK_CHECK_IN_INTERVAL_MS,
62
+ CONVERSATION_WORK_STALE_ENQUEUE_MS,
63
+ JUNIOR_THREAD_STATE_TTL_MS,
64
+ appendAndEnqueueInboundMessage,
65
+ checkInConversationWork,
66
+ clearExpiredConversationLease,
67
+ completeConversationWork,
68
+ countPendingConversationMessages,
69
+ drainConversationMailbox,
70
+ getConversationWorkState,
71
+ hasRunnableConversationWork,
72
+ listActiveConversationIds,
73
+ markConversationMessagesInjected,
74
+ markConversationWorkEnqueued,
75
+ releaseConversationWork,
76
+ removeActiveConversation,
77
+ requestConversationContinuation,
78
+ requestConversationWork,
79
+ startConversationWork
80
+ } from "./chunk-HNMUVGSR.js";
81
+ import {
82
+ SlackActionError,
83
+ createSlackDestination,
84
+ destinationKey,
85
+ downloadPrivateSlackFile,
86
+ getFilePermalink,
87
+ getHeaderString,
88
+ getSlackClient,
89
+ isConversationScopedChannel,
90
+ isDmChannel,
91
+ normalizeSlackConversationId,
92
+ parseDestination,
93
+ sameDestination,
94
+ withSlackRetries
95
+ } from "./chunk-XC33FJZN.js";
49
96
  import {
50
97
  buildNonInteractiveShellScript,
51
98
  createSandboxInstance,
@@ -54,19 +101,16 @@ import {
54
101
  isSnapshotMissingError,
55
102
  resolveRuntimeDependencySnapshot,
56
103
  runNonInteractiveCommand
57
- } from "./chunk-B5HKWWQB.js";
104
+ } from "./chunk-VLIO6RQR.js";
58
105
  import {
59
- ACTIVE_LOCK_TTL_MS,
60
106
  SANDBOX_DATA_ROOT,
61
107
  SANDBOX_SKILLS_ROOT,
62
108
  SANDBOX_WORKSPACE_ROOT,
63
- getStateAdapter,
64
109
  sandboxSkillDir,
65
110
  sandboxSkillFile
66
- } from "./chunk-R62YWUNO.js";
111
+ } from "./chunk-G3E7SCME.js";
67
112
  import {
68
113
  CredentialUnavailableError,
69
- buildActorIdentity,
70
114
  buildOAuthTokenRequest,
71
115
  createPluginBroker,
72
116
  credentialContextSchema,
@@ -77,39 +121,21 @@ import {
77
121
  getPluginOAuthConfig,
78
122
  getPluginProviders,
79
123
  hasRequiredOAuthScope,
80
- isActorUserId,
81
124
  isPluginConfigKey,
82
125
  isPluginProvider,
83
- parseActorUserId,
84
126
  parseOAuthTokenResponse,
85
127
  resolveAuthTokenPlaceholder,
86
128
  resolvePluginCommandEnv,
87
- setPluginCatalogConfig,
88
- slackActorIdentity
89
- } from "./chunk-UXG6TU2U.js";
129
+ setPluginCatalogConfig
130
+ } from "./chunk-GB3AL54K.js";
90
131
  import {
91
- defineJuniorPlugins,
92
- getVercelConversationWorkQueue,
93
- pluginCatalogConfigFromPluginSet,
94
- pluginHookRegistrationsFromPluginSet,
95
- resolveConversationWorkQueueTopic,
96
- verifyConversationQueueMessage
97
- } from "./chunk-XE2VFQQN.js";
132
+ homeDir,
133
+ listReferenceFiles
134
+ } from "./chunk-KVZL5NZS.js";
98
135
  import {
99
- SlackActionError,
100
- createSlackDestination,
101
- destinationKey,
102
- downloadPrivateSlackFile,
103
- getFilePermalink,
104
- getHeaderString,
105
- getSlackClient,
106
- isConversationScopedChannel,
107
- isDmChannel,
108
- normalizeSlackConversationId,
109
- parseDestination,
110
- sameDestination,
111
- withSlackRetries
112
- } from "./chunk-76YMBKW7.js";
136
+ ACTIVE_LOCK_TTL_MS,
137
+ getStateAdapter
138
+ } from "./chunk-3FYPXHPL.js";
113
139
  import {
114
140
  FUNCTION_TIMEOUT_BUFFER_SECONDS,
115
141
  GEN_AI_PROVIDER_NAME,
@@ -161,7 +187,15 @@ import {
161
187
  toObservablePromptPart,
162
188
  trimTrailingAssistantMessages,
163
189
  upsertActiveSkill
164
- } from "./chunk-JS4HURDT.js";
190
+ } from "./chunk-ZJQPA67D.js";
191
+ import {
192
+ createRequester,
193
+ createRequesterFromStoredSlackRequester,
194
+ createSlackRequester,
195
+ isActorUserId,
196
+ parseActorUserId,
197
+ toStoredSlackRequester
198
+ } from "./chunk-PP7AGSBU.js";
165
199
  import {
166
200
  buildTurnFailureResponse,
167
201
  createChatSdkLogger,
@@ -170,9 +204,7 @@ import {
170
204
  extractGenAiUsageSummary,
171
205
  getActiveTraceId,
172
206
  getLogContextAttributes,
173
- homeDir,
174
207
  isRecord,
175
- listReferenceFiles,
176
208
  logError,
177
209
  logException,
178
210
  logInfo,
@@ -183,11 +215,10 @@ import {
183
215
  setSpanAttributes,
184
216
  setSpanStatus,
185
217
  setTags,
186
- toOptionalNumber,
187
218
  toOptionalString,
188
219
  withContext,
189
220
  withSpan
190
- } from "./chunk-BBXYXOJW.js";
221
+ } from "./chunk-6GEYPE6T.js";
191
222
  import {
192
223
  sentry_exports
193
224
  } from "./chunk-Z3YD6NHK.js";
@@ -1051,6 +1082,12 @@ function getMcpAwareErrorType(error, fallback) {
1051
1082
  function getMcpAwareErrorMessage(error) {
1052
1083
  return error instanceof Error ? error.message : String(error);
1053
1084
  }
1085
+ function getMcpAwareTelemetryMessage(error, privacy) {
1086
+ if (privacy === "private" && error instanceof McpToolError) {
1087
+ return "MCP tool call failed";
1088
+ }
1089
+ return getMcpAwareErrorMessage(error);
1090
+ }
1054
1091
 
1055
1092
  // src/chat/mcp/tool-manager.ts
1056
1093
  function normalizeMcpToolName(provider, toolName) {
@@ -1270,63 +1307,104 @@ var McpToolManager = class {
1270
1307
  ...tool2.title?.trim() ? { title: tool2.title.trim() } : {},
1271
1308
  ...outputSchema ? { outputSchema } : {},
1272
1309
  ...annotations ? { annotations } : {},
1273
- execute: async (args) => {
1310
+ execute: async (args, options) => {
1274
1311
  const resolvedArgs = typeof args === "object" && args !== null ? args : {};
1312
+ const conversationPrivacy = options?.conversationPrivacy ?? "private";
1313
+ const managedToolName = normalizeMcpToolName(
1314
+ plugin.manifest.name,
1315
+ tool2.name
1316
+ );
1275
1317
  const baseAttributes = {
1276
- "mcp.method.name": "tools/call"
1277
- };
1278
- setSpanAttributes(baseAttributes);
1279
- try {
1280
- const result = await client.callTool(tool2.name, resolvedArgs);
1281
- if ("isError" in result && result.isError) {
1282
- throw new McpToolError(extractMcpErrorMessage(result));
1283
- }
1284
- return {
1285
- content: toAgentToolContent(result),
1286
- details: {
1287
- provider: plugin.manifest.name,
1288
- tool: tool2.name,
1289
- rawResult: result
1290
- }
1291
- };
1292
- } catch (error) {
1293
- if (error instanceof McpAuthorizationRequiredError && await this.handleAuthorizationRequired(
1318
+ "mcp.method.name": "tools/call",
1319
+ "gen_ai.operation.name": "execute_tool",
1320
+ "gen_ai.tool.name": managedToolName,
1321
+ "gen_ai.tool.description": describeMcpTool(
1294
1322
  plugin.manifest.name,
1295
- error
1296
- )) {
1297
- return {
1298
- // Pi turns thrown tool errors into toolResult isError frames.
1299
- // Once auth pause has been requested, return a placeholder result
1300
- // and let the aborted turn park cleanly instead of surfacing a
1301
- // spurious tool failure to the model.
1302
- content: [{ type: "text", text: "Authorization pending." }],
1303
- details: {
1304
- provider: plugin.manifest.name,
1305
- tool: tool2.name,
1306
- rawResult: {
1323
+ tool2
1324
+ ),
1325
+ "gen_ai.tool.type": "extension",
1326
+ "app.plugin.name": plugin.manifest.name,
1327
+ ...options?.toolCallId ? { "gen_ai.tool.call.id": options.toolCallId } : {}
1328
+ };
1329
+ const argumentAttribute = serializeMcpPayload(
1330
+ resolvedArgs,
1331
+ conversationPrivacy
1332
+ );
1333
+ return await withSpan(
1334
+ `execute_tool ${managedToolName}`,
1335
+ "gen_ai.execute_tool",
1336
+ {},
1337
+ async () => {
1338
+ try {
1339
+ const result = await client.callTool(tool2.name, resolvedArgs);
1340
+ if ("isError" in result && result.isError) {
1341
+ throw new McpToolError(extractMcpErrorMessage(result));
1342
+ }
1343
+ const resultAttribute = serializeMcpPayload(
1344
+ result,
1345
+ conversationPrivacy
1346
+ );
1347
+ if (resultAttribute) {
1348
+ setSpanAttributes({
1349
+ "gen_ai.tool.call.result": resultAttribute
1350
+ });
1351
+ }
1352
+ return {
1353
+ content: toAgentToolContent(result),
1354
+ details: {
1355
+ provider: plugin.manifest.name,
1356
+ tool: tool2.name,
1357
+ rawResult: result
1358
+ }
1359
+ };
1360
+ } catch (error) {
1361
+ if (error instanceof McpAuthorizationRequiredError && await this.handleAuthorizationRequired(
1362
+ plugin.manifest.name,
1363
+ error
1364
+ )) {
1365
+ const parkedResult = {
1307
1366
  toolResult: {
1308
1367
  authorizationPending: true
1309
1368
  }
1310
- }
1369
+ };
1370
+ return {
1371
+ // Pi turns thrown tool errors into toolResult isError frames.
1372
+ // Once auth pause has been requested, return a placeholder result
1373
+ // and let the aborted turn park cleanly instead of surfacing a
1374
+ // spurious tool failure to the model.
1375
+ content: [{ type: "text", text: "Authorization pending." }],
1376
+ details: {
1377
+ provider: plugin.manifest.name,
1378
+ tool: tool2.name,
1379
+ rawResult: parkedResult
1380
+ }
1381
+ };
1311
1382
  }
1312
- };
1313
- }
1314
- const errorAttributes = {
1383
+ const errorAttributes = {
1384
+ ...baseAttributes,
1385
+ "error.type": getMcpAwareErrorType(error, "mcp_tool_error"),
1386
+ "exception.message": getMcpAwareTelemetryMessage(
1387
+ error,
1388
+ conversationPrivacy
1389
+ )
1390
+ };
1391
+ setSpanAttributes(errorAttributes);
1392
+ if (error instanceof McpToolError) {
1393
+ logWarn(
1394
+ "mcp_tool_call_failed",
1395
+ {},
1396
+ errorAttributes,
1397
+ "MCP tool call failed"
1398
+ );
1399
+ }
1400
+ throw error;
1401
+ }
1402
+ },
1403
+ {
1315
1404
  ...baseAttributes,
1316
- "error.type": getMcpAwareErrorType(error, "mcp_tool_error"),
1317
- "exception.message": getMcpAwareErrorMessage(error)
1318
- };
1319
- setSpanAttributes(errorAttributes);
1320
- if (error instanceof McpToolError) {
1321
- logWarn(
1322
- "mcp_tool_call_failed",
1323
- {},
1324
- errorAttributes,
1325
- "MCP tool call failed"
1326
- );
1405
+ ...argumentAttribute ? { "gen_ai.tool.call.arguments": argumentAttribute } : {}
1327
1406
  }
1328
- throw error;
1329
- }
1407
+ );
1330
1408
  }
1331
1409
  };
1332
1410
  }
@@ -1371,6 +1449,11 @@ var McpToolManager = class {
1371
1449
  function toOptionalRecord(value) {
1372
1450
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1373
1451
  }
1452
+ function serializeMcpPayload(payload, privacy) {
1453
+ return serializeGenAiAttribute(
1454
+ privacy === "private" ? toGenAiPayloadMetadata(payload) : payload
1455
+ );
1456
+ }
1374
1457
 
1375
1458
  // src/chat/mcp/tool-name.ts
1376
1459
  function parseMcpProviderFromToolName(toolName) {
@@ -2713,7 +2796,7 @@ function createCallMcpToolTool(mcpToolManager) {
2713
2796
  },
2714
2797
  { additionalProperties: false }
2715
2798
  ),
2716
- execute: async (input) => {
2799
+ execute: async (input, options) => {
2717
2800
  const { tool_name } = input;
2718
2801
  const provider = parseMcpProviderFromToolName(tool_name);
2719
2802
  if (provider) {
@@ -2738,7 +2821,11 @@ function createCallMcpToolTool(mcpToolManager) {
2738
2821
  throw new McpToolError(missingToolMessage(tool_name, provider));
2739
2822
  }
2740
2823
  return await mcpTool.execute(
2741
- resolveMcpArguments(input)
2824
+ resolveMcpArguments(input),
2825
+ {
2826
+ conversationPrivacy: options?.conversationPrivacy ?? "private",
2827
+ ...options?.toolCallId ? { toolCallId: options.toolCallId } : {}
2828
+ }
2742
2829
  );
2743
2830
  }
2744
2831
  });
@@ -5226,13 +5313,13 @@ function createSystemTimeTool() {
5226
5313
  annotations: { readOnlyHint: true, destructiveHint: false },
5227
5314
  inputSchema: Type20.Object({}),
5228
5315
  execute: async () => {
5229
- const now3 = /* @__PURE__ */ new Date();
5316
+ const now2 = /* @__PURE__ */ new Date();
5230
5317
  return {
5231
5318
  ok: true,
5232
- unix_ms: now3.getTime(),
5233
- iso_utc: now3.toISOString(),
5234
- iso_local: new Date(now3.getTime() - now3.getTimezoneOffset() * 6e4).toISOString().replace("Z", ""),
5235
- timezone_offset_minutes: now3.getTimezoneOffset()
5319
+ unix_ms: now2.getTime(),
5320
+ iso_utc: now2.toISOString(),
5321
+ iso_local: new Date(now2.getTime() - now2.getTimezoneOffset() * 6e4).toISOString().replace("Z", ""),
5322
+ timezone_offset_minutes: now2.getTimezoneOffset()
5236
5323
  };
5237
5324
  }
5238
5325
  });
@@ -5244,9 +5331,6 @@ import {
5244
5331
  } from "@earendil-works/pi-agent-core";
5245
5332
  import { Type as Type21 } from "@sinclair/typebox";
5246
5333
 
5247
- // src/chat/state/ttl.ts
5248
- var JUNIOR_THREAD_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
5249
-
5250
5334
  // src/chat/tools/advisor/session-store.ts
5251
5335
  var ADVISOR_SESSION_TTL_MS = JUNIOR_THREAD_STATE_TTL_MS;
5252
5336
  function cloneMessages(messages) {
@@ -6667,11 +6751,11 @@ function getSandboxEgressSecret() {
6667
6751
  }
6668
6752
  function createSandboxEgressCredentialToken(input) {
6669
6753
  const ttlMs = Math.max(1, input.ttlMs ?? DEFAULT_SESSION_TTL_MS);
6670
- const now3 = Date.now();
6754
+ const now2 = Date.now();
6671
6755
  const context = {
6672
6756
  credentials: input.credentials,
6673
6757
  egressId: input.egressId,
6674
- expiresAtMs: now3 + ttlMs,
6758
+ expiresAtMs: now2 + ttlMs,
6675
6759
  contextId: randomUUID()
6676
6760
  };
6677
6761
  const payload = `${SANDBOX_EGRESS_TOKEN_VERSION}.${base64Url(
@@ -7953,14 +8037,14 @@ function createSandboxExecutor(options) {
7953
8037
  if (!credentialEgress) {
7954
8038
  throw new Error("Sandbox credential egress is not configured");
7955
8039
  }
7956
- const now3 = Date.now();
8040
+ const now2 = Date.now();
7957
8041
  const token = createSandboxEgressCredentialToken({
7958
8042
  credentials: credentialEgress,
7959
8043
  egressId,
7960
8044
  ttlMs: sandboxEgressTokenTtlMs
7961
8045
  });
7962
8046
  sandboxEgressCredentialTokens.set(egressId, {
7963
- expiresAtMs: now3 + sandboxEgressTokenTtlMs,
8047
+ expiresAtMs: now2 + sandboxEgressTokenTtlMs,
7964
8048
  token
7965
8049
  });
7966
8050
  return token;
@@ -8763,12 +8847,12 @@ async function deleteMcpAuthSession(authSessionId) {
8763
8847
  }
8764
8848
  async function deleteMcpAuthSessionsForUserProvider(userId, provider) {
8765
8849
  const stateAdapter = await getConnectedStateAdapter();
8766
- const indexKey2 = sessionIndexKey(userId, provider);
8767
- const authSessionIds = parseSessionIndex(await stateAdapter.get(indexKey2));
8850
+ const indexKey = sessionIndexKey(userId, provider);
8851
+ const authSessionIds = parseSessionIndex(await stateAdapter.get(indexKey));
8768
8852
  for (const authSessionId of authSessionIds) {
8769
8853
  await stateAdapter.delete(sessionKey(authSessionId));
8770
8854
  }
8771
- await stateAdapter.delete(indexKey2);
8855
+ await stateAdapter.delete(indexKey);
8772
8856
  }
8773
8857
  async function getLatestMcpAuthSessionForUserProvider(userId, provider) {
8774
8858
  const stateAdapter = await getConnectedStateAdapter();
@@ -9328,9 +9412,9 @@ function getToolErrorAttributes(error) {
9328
9412
  ...error.detailRule ? { "app.slack.detail_rule": error.detailRule } : {}
9329
9413
  };
9330
9414
  }
9331
- function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, traceContext) {
9415
+ function handleToolExecutionError(error, toolName, toolCallId, shouldTrace, traceContext, conversationPrivacy) {
9332
9416
  const errorType = getToolErrorType(error);
9333
- const errorMessage = getMcpAwareErrorMessage(error);
9417
+ const errorMessage = getMcpAwareTelemetryMessage(error, conversationPrivacy);
9334
9418
  setSpanAttributes({
9335
9419
  "error.type": errorType,
9336
9420
  ...error instanceof PluginCredentialFailureError ? { "app.credential.provider": error.provider } : {}
@@ -9453,7 +9537,9 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
9453
9537
  ...signal ? { signal } : {}
9454
9538
  }) : await toolDef.execute(toolInput, {
9455
9539
  experimental_context: sandbox,
9456
- ...signal ? { signal } : {}
9540
+ ...signal ? { signal } : {},
9541
+ conversationPrivacy: effectiveConversationPrivacy,
9542
+ ...normalizedToolCallId ? { toolCallId: normalizedToolCallId } : {}
9457
9543
  });
9458
9544
  const normalized = normalizeToolResult(result, isSandbox);
9459
9545
  if (bashCommand && pluginAuthOrchestration) {
@@ -9484,16 +9570,17 @@ function createAgentTools(tools, sandbox, spanContext, onStatus, sandboxExecutor
9484
9570
  toolName,
9485
9571
  normalizedToolCallId,
9486
9572
  shouldTrace,
9487
- spanContext
9573
+ spanContext,
9574
+ effectiveConversationPrivacy
9488
9575
  );
9489
9576
  }
9490
9577
  },
9491
9578
  {
9492
9579
  "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
9493
9580
  "gen_ai.operation.name": "execute_tool",
9494
- "app.conversation.privacy": effectiveConversationPrivacy,
9495
9581
  "gen_ai.tool.name": toolName,
9496
9582
  "gen_ai.tool.description": toolDef.description,
9583
+ "gen_ai.tool.type": "extension",
9497
9584
  ...toolArgumentsMetadata,
9498
9585
  ...normalizedToolCallId ? { "gen_ai.tool.call.id": normalizedToolCallId } : {},
9499
9586
  ...toolArgumentsAttribute ? { "gen_ai.tool.call.arguments": toolArgumentsAttribute } : {}
@@ -9699,208 +9786,6 @@ function createChannelConfigurationService(storage) {
9699
9786
  };
9700
9787
  }
9701
9788
 
9702
- // src/chat/state/conversation.ts
9703
- function coerceRole(value) {
9704
- return value === "assistant" || value === "system" || value === "user" ? value : "user";
9705
- }
9706
- function coerceAuthor(value) {
9707
- if (!isRecord(value)) return void 0;
9708
- const author = {
9709
- fullName: toOptionalString(value.fullName),
9710
- userId: toOptionalString(value.userId),
9711
- userName: toOptionalString(value.userName)
9712
- };
9713
- if (typeof value.isBot === "boolean") {
9714
- author.isBot = value.isBot;
9715
- }
9716
- if (!author.fullName && !author.userId && !author.userName && author.isBot === void 0) {
9717
- return void 0;
9718
- }
9719
- return author;
9720
- }
9721
- function coerceMessageMeta(value) {
9722
- if (!isRecord(value)) return void 0;
9723
- const meta = {};
9724
- const attachmentCount = toOptionalNumber(value.attachmentCount);
9725
- if (typeof attachmentCount === "number" && attachmentCount > 0) {
9726
- meta.attachmentCount = attachmentCount;
9727
- }
9728
- if (typeof value.explicitMention === "boolean") {
9729
- meta.explicitMention = value.explicitMention;
9730
- }
9731
- const imageAttachmentCount = toOptionalNumber(value.imageAttachmentCount);
9732
- if (typeof imageAttachmentCount === "number" && imageAttachmentCount > 0) {
9733
- meta.imageAttachmentCount = imageAttachmentCount;
9734
- }
9735
- if (typeof value.replied === "boolean") {
9736
- meta.replied = value.replied;
9737
- }
9738
- if (typeof value.skippedReason === "string" && value.skippedReason.trim().length > 0) {
9739
- meta.skippedReason = value.skippedReason;
9740
- }
9741
- if (typeof value.slackTs === "string" && value.slackTs.trim().length > 0) {
9742
- meta.slackTs = value.slackTs;
9743
- }
9744
- if (Array.isArray(value.imageFileIds)) {
9745
- const imageFileIds = value.imageFileIds.filter(
9746
- (entry) => typeof entry === "string" && entry.trim().length > 0
9747
- );
9748
- if (imageFileIds.length > 0) {
9749
- meta.imageFileIds = imageFileIds;
9750
- }
9751
- }
9752
- if (typeof value.imagesHydrated === "boolean") {
9753
- meta.imagesHydrated = value.imagesHydrated;
9754
- }
9755
- if (meta.attachmentCount === void 0 && meta.explicitMention === void 0 && meta.imageAttachmentCount === void 0 && meta.replied === void 0 && meta.skippedReason === void 0 && meta.slackTs === void 0 && meta.imageFileIds === void 0 && meta.imagesHydrated === void 0) {
9756
- return void 0;
9757
- }
9758
- return meta;
9759
- }
9760
- function defaultConversationState() {
9761
- const nowMs = Date.now();
9762
- return {
9763
- schemaVersion: 1,
9764
- messages: [],
9765
- piMessages: [],
9766
- compactions: [],
9767
- backfill: {},
9768
- processing: {},
9769
- stats: {
9770
- estimatedContextTokens: 0,
9771
- totalMessageCount: 0,
9772
- compactedMessageCount: 0,
9773
- updatedAtMs: nowMs
9774
- },
9775
- vision: {
9776
- byFileId: {}
9777
- }
9778
- };
9779
- }
9780
- function coercePendingAuthState(value) {
9781
- if (!isRecord(value)) {
9782
- return void 0;
9783
- }
9784
- const kind = value.kind;
9785
- const provider = toOptionalString(value.provider);
9786
- const requesterId = toOptionalString(value.requesterId);
9787
- const scope = toOptionalString(value.scope);
9788
- const sessionId = toOptionalString(value.sessionId);
9789
- const linkSentAtMs = toOptionalNumber(value.linkSentAtMs);
9790
- if (kind !== "mcp" && kind !== "plugin" || !provider || !requesterId || !sessionId || typeof linkSentAtMs !== "number") {
9791
- return void 0;
9792
- }
9793
- return {
9794
- kind,
9795
- provider,
9796
- requesterId,
9797
- ...scope ? { scope } : {},
9798
- sessionId,
9799
- linkSentAtMs
9800
- };
9801
- }
9802
- function coerceThreadConversationState(value) {
9803
- if (!isRecord(value)) {
9804
- return defaultConversationState();
9805
- }
9806
- const root = value;
9807
- const rawConversation = isRecord(root.conversation) ? root.conversation : {};
9808
- const base = defaultConversationState();
9809
- const rawMessages = Array.isArray(rawConversation.messages) ? rawConversation.messages : [];
9810
- const messages = [];
9811
- for (const item of rawMessages) {
9812
- if (!isRecord(item)) continue;
9813
- const id = toOptionalString(item.id);
9814
- const text = toOptionalString(item.text);
9815
- const createdAtMs = toOptionalNumber(item.createdAtMs);
9816
- if (!id || !text || !createdAtMs) continue;
9817
- messages.push({
9818
- id,
9819
- role: coerceRole(item.role),
9820
- text,
9821
- createdAtMs,
9822
- author: coerceAuthor(item.author),
9823
- meta: coerceMessageMeta(item.meta)
9824
- });
9825
- }
9826
- const rawCompactions = Array.isArray(rawConversation.compactions) ? rawConversation.compactions : [];
9827
- const compactions = [];
9828
- for (const item of rawCompactions) {
9829
- if (!isRecord(item)) continue;
9830
- const id = toOptionalString(item.id);
9831
- const summary = toOptionalString(item.summary);
9832
- const createdAtMs = toOptionalNumber(item.createdAtMs);
9833
- if (!id || !summary || !createdAtMs) continue;
9834
- const coveredMessageIds = Array.isArray(item.coveredMessageIds) ? item.coveredMessageIds.filter(
9835
- (entry) => typeof entry === "string" && entry.length > 0
9836
- ) : [];
9837
- compactions.push({
9838
- id,
9839
- summary,
9840
- createdAtMs,
9841
- coveredMessageIds
9842
- });
9843
- }
9844
- const rawBackfill = isRecord(rawConversation.backfill) ? rawConversation.backfill : {};
9845
- const backfill = {
9846
- completedAtMs: toOptionalNumber(rawBackfill.completedAtMs),
9847
- source: rawBackfill.source === "recent_messages" || rawBackfill.source === "thread_fetch" ? rawBackfill.source : void 0
9848
- };
9849
- const rawProcessing = isRecord(rawConversation.processing) ? rawConversation.processing : {};
9850
- const processing = {
9851
- activeTurnId: toOptionalString(rawProcessing.activeTurnId),
9852
- lastCompletedAtMs: toOptionalNumber(rawProcessing.lastCompletedAtMs),
9853
- pendingAuth: coercePendingAuthState(rawProcessing.pendingAuth)
9854
- };
9855
- const rawStats = isRecord(rawConversation.stats) ? rawConversation.stats : {};
9856
- const stats = {
9857
- estimatedContextTokens: toOptionalNumber(rawStats.estimatedContextTokens) ?? base.stats.estimatedContextTokens,
9858
- totalMessageCount: toOptionalNumber(rawStats.totalMessageCount) ?? messages.length,
9859
- compactedMessageCount: toOptionalNumber(rawStats.compactedMessageCount) ?? 0,
9860
- updatedAtMs: toOptionalNumber(rawStats.updatedAtMs) ?? base.stats.updatedAtMs
9861
- };
9862
- const rawVision = isRecord(rawConversation.vision) ? rawConversation.vision : {};
9863
- const rawVisionByFileId = isRecord(rawVision.byFileId) ? rawVision.byFileId : {};
9864
- const byFileId = {};
9865
- for (const [fileId, value2] of Object.entries(rawVisionByFileId)) {
9866
- if (typeof fileId !== "string" || fileId.trim().length === 0) continue;
9867
- if (!isRecord(value2)) continue;
9868
- const summary = toOptionalString(value2.summary);
9869
- const analyzedAtMs = toOptionalNumber(value2.analyzedAtMs);
9870
- if (!summary || !analyzedAtMs) continue;
9871
- byFileId[fileId] = {
9872
- summary,
9873
- analyzedAtMs
9874
- };
9875
- }
9876
- return {
9877
- schemaVersion: 1,
9878
- messages,
9879
- piMessages: Array.isArray(rawConversation.piMessages) ? rawConversation.piMessages : [],
9880
- compactions,
9881
- backfill,
9882
- processing,
9883
- stats,
9884
- vision: {
9885
- backfillCompletedAtMs: toOptionalNumber(rawVision.backfillCompletedAtMs),
9886
- byFileId
9887
- }
9888
- };
9889
- }
9890
- function buildConversationStatePatch(conversation) {
9891
- return {
9892
- conversation: {
9893
- ...conversation,
9894
- schemaVersion: 1,
9895
- stats: {
9896
- ...conversation.stats,
9897
- totalMessageCount: conversation.messages.length,
9898
- updatedAtMs: Date.now()
9899
- }
9900
- }
9901
- };
9902
- }
9903
-
9904
9789
  // src/chat/state/artifacts.ts
9905
9790
  function coerceThreadArtifactsState(value) {
9906
9791
  if (!value || typeof value !== "object") {
@@ -10664,7 +10549,7 @@ function addAgentTurnUsage(...usages) {
10664
10549
  }
10665
10550
 
10666
10551
  // src/chat/services/turn-session-record.ts
10667
- var AGENT_TURN_TIMEOUT_RESUME_MAX_SLICES = 48;
10552
+ var AGENT_CONTINUE_MAX_SLICES = 48;
10668
10553
  function logSessionRecordError(error, eventName, args, attributes, message) {
10669
10554
  logException(
10670
10555
  error,
@@ -10740,7 +10625,10 @@ async function persistRunningSessionRecord(args) {
10740
10625
  ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
10741
10626
  ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
10742
10627
  ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
10743
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
10628
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
10629
+ ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
10630
+ turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
10631
+ } : {}
10744
10632
  });
10745
10633
  return true;
10746
10634
  } catch (recordError) {
@@ -10781,7 +10669,10 @@ async function persistCompletedSessionRecord(args) {
10781
10669
  ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
10782
10670
  ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
10783
10671
  ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
10784
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
10672
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
10673
+ ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
10674
+ turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
10675
+ } : {}
10785
10676
  });
10786
10677
  } catch (recordError) {
10787
10678
  logSessionRecordError(
@@ -10869,7 +10760,7 @@ async function persistTimeoutSessionRecord(args) {
10869
10760
  latestSessionRecord?.cumulativeUsage,
10870
10761
  args.currentUsage
10871
10762
  );
10872
- if (nextSliceId > AGENT_TURN_TIMEOUT_RESUME_MAX_SLICES) {
10763
+ if (nextSliceId > AGENT_CONTINUE_MAX_SLICES) {
10873
10764
  return await upsertAgentTurnSessionRecord({
10874
10765
  ...args.channelName ?? latestSessionRecord?.channelName ? {
10875
10766
  channelName: args.channelName ?? latestSessionRecord?.channelName
@@ -10888,7 +10779,7 @@ async function persistTimeoutSessionRecord(args) {
10888
10779
  ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
10889
10780
  resumeReason: "timeout",
10890
10781
  resumedFromSliceId: latestSessionRecord?.resumedFromSliceId,
10891
- errorMessage: `Turn exceeded timeout resume slice limit (${AGENT_TURN_TIMEOUT_RESUME_MAX_SLICES})`,
10782
+ errorMessage: `Agent continuation exceeded slice limit (${AGENT_CONTINUE_MAX_SLICES})`,
10892
10783
  ...args.requester ?? latestSessionRecord?.requester ? { requester: args.requester ?? latestSessionRecord?.requester } : {},
10893
10784
  ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
10894
10785
  });
@@ -10914,13 +10805,13 @@ async function persistTimeoutSessionRecord(args) {
10914
10805
  } catch (recordError) {
10915
10806
  logSessionRecordError(
10916
10807
  recordError,
10917
- "agent_turn_timeout_resume_session_record_failed",
10808
+ "agent_continue_session_record_failed",
10918
10809
  args,
10919
10810
  {
10920
10811
  "app.ai.resume_from_slice_id": args.currentSliceId,
10921
10812
  "app.ai.resume_next_slice_id": nextSliceId
10922
10813
  },
10923
- "Failed to persist timeout session record before scheduling resume"
10814
+ "Failed to persist session record before scheduling agent continuation"
10924
10815
  );
10925
10816
  return void 0;
10926
10817
  }
@@ -11114,13 +11005,13 @@ var StateBackedMcpOAuthClientProvider = class {
11114
11005
  if (!this.sessionContext) {
11115
11006
  throw new Error(`Unknown MCP auth session: ${this.authSessionId}`);
11116
11007
  }
11117
- const now3 = Date.now();
11008
+ const now2 = Date.now();
11118
11009
  const nextSession = {
11119
11010
  authSessionId: this.authSessionId,
11120
11011
  ...this.sessionContext,
11121
11012
  ...patch,
11122
- createdAtMs: now3,
11123
- updatedAtMs: now3
11013
+ createdAtMs: now2,
11014
+ updatedAtMs: now2
11124
11015
  };
11125
11016
  await putMcpAuthSession(nextSession);
11126
11017
  return nextSession;
@@ -11158,7 +11049,7 @@ async function createMcpOAuthClientProvider(input) {
11158
11049
  input.provider
11159
11050
  );
11160
11051
  const reusableSession = existingSession && existingSession.conversationId === input.conversationId && existingSession.sessionId === input.sessionId ? existingSession : void 0;
11161
- const now3 = Date.now();
11052
+ const now2 = Date.now();
11162
11053
  const authSessionId = reusableSession?.authSessionId ?? randomUUID3();
11163
11054
  await putMcpAuthSession({
11164
11055
  authSessionId,
@@ -11175,8 +11066,8 @@ async function createMcpOAuthClientProvider(input) {
11175
11066
  ...input.artifactState ? { artifactState: input.artifactState } : {},
11176
11067
  ...reusableSession?.authorizationUrl ? { authorizationUrl: reusableSession.authorizationUrl } : {},
11177
11068
  ...reusableSession?.codeVerifier ? { codeVerifier: reusableSession.codeVerifier } : {},
11178
- createdAtMs: reusableSession?.createdAtMs ?? now3,
11179
- updatedAtMs: now3
11069
+ createdAtMs: reusableSession?.createdAtMs ?? now2,
11070
+ updatedAtMs: now2
11180
11071
  });
11181
11072
  return new StateBackedMcpOAuthClientProvider(
11182
11073
  authSessionId,
@@ -11415,18 +11306,15 @@ function extractSliceUsage(messages, beforeMessageCount) {
11415
11306
  );
11416
11307
  return hasAgentTurnUsage(usage) ? usage : void 0;
11417
11308
  }
11418
- function requesterFromContext(requester, requesterId) {
11419
- const identity = actorRequesterFromContext(requester, requesterId);
11420
- const agentRequester = {
11421
- ...identity?.email ? { email: identity.email } : {},
11422
- ...identity?.fullName ? { fullName: identity.fullName } : {},
11423
- ...identity?.userId ? { slackUserId: identity.userId } : {},
11424
- ...identity?.userName ? { slackUserName: identity.userName } : {}
11425
- };
11426
- return Object.keys(agentRequester).length > 0 ? agentRequester : void 0;
11309
+ function requesterFromContext(context) {
11310
+ const identity = actorRequesterFromContext(context);
11311
+ return identity ? toStoredSlackRequester(identity) : void 0;
11427
11312
  }
11428
- function actorRequesterFromContext(requester, requesterId) {
11429
- return buildActorIdentity(requester, requesterId);
11313
+ function actorRequesterFromContext(context) {
11314
+ return createRequester(context.requester, {
11315
+ teamId: context.destination?.teamId ?? context.correlation?.teamId ?? context.requester?.teamId,
11316
+ userId: context.correlation?.requesterId
11317
+ });
11430
11318
  }
11431
11319
  function surfaceFromContext(context) {
11432
11320
  if (context.surface) {
@@ -11544,6 +11432,7 @@ async function generateAssistantReply(messageText2, context = {}) {
11544
11432
  let timeoutResumeSliceId = 1;
11545
11433
  let timeoutResumeMessages = [];
11546
11434
  let beforeMessageCount = 0;
11435
+ let turnStartMessageIndex;
11547
11436
  let lastKnownSandboxId = context.sandbox?.sandboxId;
11548
11437
  let lastKnownSandboxDependencyProfileHash = context.sandbox?.sandboxDependencyProfileHash;
11549
11438
  let loadedSkillNamesForResume = [];
@@ -11556,14 +11445,8 @@ async function generateAssistantReply(messageText2, context = {}) {
11556
11445
  let inputCommitted = false;
11557
11446
  let turnUsage;
11558
11447
  let thinkingSelection;
11559
- const requester = requesterFromContext(
11560
- context.requester,
11561
- context.correlation?.requesterId
11562
- );
11563
- const actorRequester = actorRequesterFromContext(
11564
- context.requester,
11565
- context.correlation?.requesterId
11566
- );
11448
+ const requester = requesterFromContext(context);
11449
+ const actorRequester = actorRequesterFromContext(context);
11567
11450
  const surface = surfaceFromContext(context);
11568
11451
  const credentialActor = context.credentialContext?.actor;
11569
11452
  const credentialActorLogContext = credentialActor ? {
@@ -12094,7 +11977,8 @@ async function generateAssistantReply(messageText2, context = {}) {
12094
11977
  loadedSkillNames: loadedSkillNamesForResume,
12095
11978
  logContext: sessionRecordLogContext,
12096
11979
  requester,
12097
- ...surface ? { surface } : {}
11980
+ ...surface ? { surface } : {},
11981
+ ...turnStartMessageIndex !== void 0 ? { turnStartMessageIndex } : {}
12098
11982
  });
12099
11983
  if (!persisted) {
12100
11984
  return false;
@@ -12229,10 +12113,14 @@ async function generateAssistantReply(messageText2, context = {}) {
12229
12113
  existingSessionRecord.piMessages,
12230
12114
  turnContextPrompt
12231
12115
  ) : existingSessionRecord.piMessages;
12116
+ turnStartMessageIndex = existingSessionRecord.turnStartMessageIndex;
12232
12117
  } else if (context.piMessages && context.piMessages.length > 0) {
12233
12118
  agent.state.messages = [...context.piMessages];
12234
12119
  }
12235
12120
  beforeMessageCount = agent.state.messages.length;
12121
+ if (!resumedFromSessionRecord) {
12122
+ turnStartMessageIndex = beforeMessageCount;
12123
+ }
12236
12124
  await withSpan(
12237
12125
  `invoke_agent ${botConfig.modelId}`,
12238
12126
  "gen_ai.invoke_agent",
@@ -12407,7 +12295,8 @@ async function generateAssistantReply(messageText2, context = {}) {
12407
12295
  loadedSkillNames: loadedSkillNamesForResume,
12408
12296
  logContext: sessionRecordLogContext,
12409
12297
  requester,
12410
- ...surface ? { surface } : {}
12298
+ ...surface ? { surface } : {},
12299
+ ...turnStartMessageIndex !== void 0 ? { turnStartMessageIndex } : {}
12411
12300
  });
12412
12301
  }
12413
12302
  return buildTurnResult({
@@ -12478,7 +12367,7 @@ async function generateAssistantReply(messageText2, context = {}) {
12478
12367
  }
12479
12368
  if (sessionRecord.state === "awaiting_resume") {
12480
12369
  throw new RetryableTurnError(
12481
- "turn_timeout_resume",
12370
+ "agent_continue",
12482
12371
  `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${sessionRecord.sliceId} version=${sessionRecord.version}`,
12483
12372
  {
12484
12373
  conversationId: timeoutResumeConversationId,
@@ -13871,7 +13760,7 @@ async function runAgentDispatchSlice(callback, deps = {}) {
13871
13760
  });
13872
13761
  return;
13873
13762
  }
13874
- if (isRetryableTurnError(error, "turn_timeout_resume")) {
13763
+ if (isRetryableTurnError(error, "agent_continue")) {
13875
13764
  const version = error.metadata?.version;
13876
13765
  if (typeof version === "number") {
13877
13766
  const awaiting = await markDispatch({
@@ -13932,828 +13821,36 @@ async function POST(request, waitUntil) {
13932
13821
  }
13933
13822
 
13934
13823
  // src/handlers/heartbeat.ts
13935
- import { timingSafeEqual as timingSafeEqual4 } from "crypto";
13824
+ import { timingSafeEqual as timingSafeEqual3 } from "crypto";
13936
13825
 
13937
- // src/chat/services/timeout-resume.ts
13938
- import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "crypto";
13939
-
13940
- // src/chat/task-execution/store.ts
13941
- import { randomUUID as randomUUID4 } from "crypto";
13942
- var CONVERSATION_WORK_PREFIX = "junior:conversation-work";
13943
- var CONVERSATION_WORK_SCHEMA_VERSION = 1;
13944
- var CONVERSATION_WORK_INDEX_MAX_LENGTH = 1e4;
13945
- var CONVERSATION_WORK_INDEX_LOCK_TTL_MS = 1e4;
13946
- var CONVERSATION_WORK_INDEX_LOCK_WAIT_MS = 2e3;
13947
- var CONVERSATION_WORK_INDEX_LOCK_RETRY_MS = 25;
13948
- var CONVERSATION_WORK_MUTATION_LOCK_TTL_MS = 1e4;
13949
- var CONVERSATION_WORK_MUTATION_WAIT_MS = 1e4;
13950
- var CONVERSATION_WORK_MUTATION_RETRY_MS = 25;
13951
- var CONVERSATION_WORK_LEASE_TTL_MS = 9e4;
13952
- var CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15e3;
13953
- var CONVERSATION_WORK_STALE_ENQUEUE_MS = 6e4;
13954
- var CONVERSATION_WORK_MAX_CONSECUTIVE_FAILURES = 5;
13955
- function duplicateInboundNudgeIdempotencyKey(message, nowMs) {
13956
- return `duplicate:${message.conversationId}:${message.inboundMessageId}:${nowMs}`;
13957
- }
13958
- function hasRecentEnqueueMarker(state, nowMs) {
13959
- return typeof state.lastEnqueuedAtMs === "number" && state.lastEnqueuedAtMs + CONVERSATION_WORK_STALE_ENQUEUE_MS > nowMs;
13960
- }
13961
- function stateKey(conversationId) {
13962
- return `${CONVERSATION_WORK_PREFIX}:state:${conversationId}`;
13963
- }
13964
- function indexKey() {
13965
- return `${CONVERSATION_WORK_PREFIX}:index`;
13966
- }
13967
- function indexLockKey() {
13968
- return `${CONVERSATION_WORK_PREFIX}:index:lock`;
13969
- }
13970
- function mutationLockKey(conversationId) {
13971
- return `${CONVERSATION_WORK_PREFIX}:mutation:${conversationId}`;
13972
- }
13973
- function now() {
13974
- return Date.now();
13975
- }
13976
- function uniqueStrings(values) {
13977
- return [
13978
- ...new Set(
13979
- values.filter((value) => {
13980
- return typeof value === "string" && value.trim().length > 0;
13981
- })
13982
- )
13983
- ];
13984
- }
13985
- function compareMessages(left, right) {
13986
- return left.createdAtMs - right.createdAtMs || left.receivedAtMs - right.receivedAtMs || left.inboundMessageId.localeCompare(right.inboundMessageId);
13987
- }
13988
- function normalizeSource(value) {
13989
- if (value === "plugin" || value === "scheduler" || value === "slack") {
13990
- return value;
13991
- }
13992
- return void 0;
13993
- }
13994
- function normalizeMetadata2(value) {
13995
- if (!isRecord(value)) {
13996
- return void 0;
13997
- }
13998
- return value;
13999
- }
14000
- function normalizeInput(value) {
14001
- if (!isRecord(value)) {
14002
- return void 0;
14003
- }
14004
- const text = toOptionalString(value.text);
14005
- if (!text) {
14006
- return void 0;
14007
- }
14008
- return {
14009
- text,
14010
- authorId: toOptionalString(value.authorId),
14011
- attachments: Array.isArray(value.attachments) ? [...value.attachments] : void 0,
14012
- metadata: normalizeMetadata2(value.metadata)
14013
- };
13826
+ // src/chat/task-execution/heartbeat.ts
13827
+ var DEFAULT_RECOVERY_LIMIT = 25;
13828
+ function heartbeatIdempotencyKey(reason, conversationId, nowMs) {
13829
+ return `heartbeat:${reason}:${conversationId}:${nowMs}`;
14014
13830
  }
14015
- function normalizeMessage(value) {
14016
- if (!isRecord(value)) {
14017
- return void 0;
14018
- }
14019
- const conversationId = toOptionalString(value.conversationId);
14020
- const inboundMessageId = toOptionalString(value.inboundMessageId);
14021
- const source = normalizeSource(value.source);
14022
- const destination = parseDestination(value.destination);
14023
- const createdAtMs = toOptionalNumber(value.createdAtMs);
14024
- const receivedAtMs = toOptionalNumber(value.receivedAtMs);
14025
- const input = normalizeInput(value.input);
14026
- if (!conversationId || !destination || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
14027
- return void 0;
14028
- }
14029
- return {
14030
- conversationId,
14031
- destination,
14032
- inboundMessageId,
14033
- source,
14034
- createdAtMs,
14035
- receivedAtMs,
14036
- input,
14037
- injectedAtMs: toOptionalNumber(value.injectedAtMs)
14038
- };
13831
+ async function sendRecoveryNudge(args) {
13832
+ await args.queue.send(
13833
+ {
13834
+ conversationId: args.conversationId,
13835
+ destination: args.destination
13836
+ },
13837
+ { idempotencyKey: args.idempotencyKey }
13838
+ );
13839
+ await markConversationWorkEnqueued({
13840
+ conversationId: args.conversationId,
13841
+ nowMs: args.nowMs,
13842
+ state: args.state
13843
+ });
14039
13844
  }
14040
- function normalizeLease(value) {
14041
- if (!isRecord(value)) {
14042
- return void 0;
14043
- }
14044
- const leaseToken = toOptionalString(value.leaseToken);
14045
- const acquiredAtMs = toOptionalNumber(value.acquiredAtMs);
14046
- const lastCheckInAtMs = toOptionalNumber(value.lastCheckInAtMs);
14047
- const leaseExpiresAtMs = toOptionalNumber(value.leaseExpiresAtMs);
14048
- if (!leaseToken || typeof acquiredAtMs !== "number" || typeof lastCheckInAtMs !== "number" || typeof leaseExpiresAtMs !== "number") {
14049
- return void 0;
14050
- }
14051
- return {
14052
- leaseToken,
14053
- acquiredAtMs,
14054
- lastCheckInAtMs,
14055
- leaseExpiresAtMs
13845
+ async function recoverConversationWork(args) {
13846
+ const result = {
13847
+ expiredLeaseCount: 0,
13848
+ pendingCount: 0
14056
13849
  };
14057
- }
14058
- function normalizeWorkState(conversationId, value) {
14059
- if (!isRecord(value) || value.schemaVersion !== CONVERSATION_WORK_SCHEMA_VERSION) {
14060
- return void 0;
14061
- }
14062
- const storedConversationId = toOptionalString(value.conversationId);
14063
- const destination = parseDestination(value.destination);
14064
- const updatedAtMs = toOptionalNumber(value.updatedAtMs);
14065
- if (storedConversationId !== conversationId || !destination || typeof updatedAtMs !== "number") {
14066
- return void 0;
14067
- }
14068
- const messages = Array.isArray(value.messages) ? value.messages.map(normalizeMessage).filter((message) => Boolean(message)).filter((message) => message.conversationId === conversationId).sort(compareMessages) : [];
14069
- return {
14070
- schemaVersion: CONVERSATION_WORK_SCHEMA_VERSION,
14071
- conversationId,
14072
- destination,
14073
- messages,
14074
- needsRun: value.needsRun === true,
14075
- updatedAtMs,
14076
- consecutiveFailureCount: toOptionalNumber(value.consecutiveFailureCount) ?? 0,
14077
- lastEnqueuedAtMs: toOptionalNumber(value.lastEnqueuedAtMs),
14078
- lastFailureAtMs: toOptionalNumber(value.lastFailureAtMs),
14079
- lease: normalizeLease(value.lease),
14080
- terminallyFailedAtMs: toOptionalNumber(value.terminallyFailedAtMs)
14081
- };
14082
- }
14083
- function emptyWorkState(args) {
14084
- return {
14085
- schemaVersion: CONVERSATION_WORK_SCHEMA_VERSION,
14086
- conversationId: args.conversationId,
14087
- consecutiveFailureCount: 0,
14088
- destination: args.destination,
14089
- messages: [],
14090
- needsRun: false,
14091
- updatedAtMs: args.nowMs
14092
- };
14093
- }
14094
- function isLeaseActive(lease, nowMs) {
14095
- return Boolean(lease && lease.leaseExpiresAtMs > nowMs);
14096
- }
14097
- function pendingMessages(state) {
14098
- return state.messages.filter((message) => message.injectedAtMs === void 0).sort(compareMessages);
14099
- }
14100
- function shouldKeepIndexed(state) {
14101
- if (state.terminallyFailedAtMs !== void 0) {
14102
- return false;
14103
- }
14104
- return state.needsRun || Boolean(state.lease) || pendingMessages(state).length > 0;
14105
- }
14106
- async function getConnectedState(stateAdapter) {
14107
- const state = stateAdapter ?? getStateAdapter();
14108
- await state.connect();
14109
- return state;
14110
- }
14111
- async function sleep3(ms) {
14112
- await new Promise((resolve) => {
14113
- const timer = setTimeout(resolve, ms);
14114
- timer.unref?.();
14115
- });
14116
- }
14117
- async function withIndexLock(state, callback) {
14118
- const startedAtMs = now();
14119
- let lock;
14120
- while (true) {
14121
- lock = await state.acquireLock(
14122
- indexLockKey(),
14123
- CONVERSATION_WORK_INDEX_LOCK_TTL_MS
14124
- );
14125
- if (lock) {
14126
- break;
14127
- }
14128
- if (now() - startedAtMs >= CONVERSATION_WORK_INDEX_LOCK_WAIT_MS) {
14129
- throw new Error("Could not acquire conversation work index lock");
14130
- }
14131
- await sleep3(CONVERSATION_WORK_INDEX_LOCK_RETRY_MS);
14132
- }
14133
- try {
14134
- return await callback();
14135
- } finally {
14136
- await state.releaseLock(lock);
14137
- }
14138
- }
14139
- async function addToIndex(state, conversationId) {
14140
- await withIndexLock(state, async () => {
14141
- const existing = uniqueStrings(
14142
- await state.get(indexKey()) ?? []
14143
- );
14144
- if (existing.includes(conversationId)) {
14145
- return;
14146
- }
14147
- const indexed = [...existing, conversationId];
14148
- const remove = /* @__PURE__ */ new Set();
14149
- for (const id of indexed) {
14150
- if (indexed.length - remove.size <= CONVERSATION_WORK_INDEX_MAX_LENGTH) {
14151
- break;
14152
- }
14153
- const work = await readWorkState(state, id);
14154
- if (!work || !shouldKeepIndexed(work)) {
14155
- remove.add(id);
14156
- }
14157
- }
14158
- await state.set(
14159
- indexKey(),
14160
- indexed.filter((id) => !remove.has(id)),
14161
- JUNIOR_THREAD_STATE_TTL_MS
14162
- );
14163
- });
14164
- }
14165
- async function removeFromIndex(state, conversationId) {
14166
- await withIndexLock(state, async () => {
14167
- const existing = uniqueStrings(
14168
- await state.get(indexKey()) ?? []
14169
- );
14170
- const next = existing.filter((id) => id !== conversationId);
14171
- if (next.length === existing.length) {
14172
- return;
14173
- }
14174
- await state.set(indexKey(), next, JUNIOR_THREAD_STATE_TTL_MS);
14175
- });
14176
- }
14177
- async function acquireMutationLock(state, conversationId) {
14178
- const startedAtMs = now();
14179
- while (true) {
14180
- const lock = await state.acquireLock(
14181
- mutationLockKey(conversationId),
14182
- CONVERSATION_WORK_MUTATION_LOCK_TTL_MS
14183
- );
14184
- if (lock) {
14185
- return lock;
14186
- }
14187
- if (now() - startedAtMs >= CONVERSATION_WORK_MUTATION_WAIT_MS) {
14188
- throw new Error(
14189
- `Could not acquire conversation work mutation lock for ${conversationId}`
14190
- );
14191
- }
14192
- await sleep3(CONVERSATION_WORK_MUTATION_RETRY_MS);
14193
- }
14194
- }
14195
- async function withConversationMutation(args, callback) {
14196
- const state = await getConnectedState(args.state);
14197
- const lock = await acquireMutationLock(state, args.conversationId);
14198
- try {
14199
- return await callback(state);
14200
- } finally {
14201
- await state.releaseLock(lock);
14202
- }
14203
- }
14204
- async function readWorkState(state, conversationId) {
14205
- const raw = await state.get(stateKey(conversationId));
14206
- if (raw == null) {
14207
- return void 0;
14208
- }
14209
- const work = normalizeWorkState(conversationId, raw);
14210
- if (!work) {
14211
- throw new Error(`Conversation work state is invalid for ${conversationId}`);
14212
- }
14213
- return work;
14214
- }
14215
- async function writeWorkState(state, work) {
14216
- await state.set(
14217
- stateKey(work.conversationId),
14218
- work,
14219
- JUNIOR_THREAD_STATE_TTL_MS
14220
- );
14221
- if (shouldKeepIndexed(work)) {
14222
- await addToIndex(state, work.conversationId);
14223
- } else {
14224
- await removeFromIndex(state, work.conversationId);
14225
- }
14226
- }
14227
- function hasRunnableWork(state) {
14228
- if (state.terminallyFailedAtMs !== void 0) {
14229
- return false;
14230
- }
14231
- return state.needsRun || pendingMessages(state).length > 0;
14232
- }
14233
- function assertSameConversationDestination(args) {
14234
- if (sameDestination(args.current, args.next)) {
14235
- return;
14236
- }
14237
- throw new Error(
14238
- `Conversation work destination changed for ${args.conversationId}`
14239
- );
14240
- }
14241
- async function getConversationWorkState(args) {
14242
- const state = await getConnectedState(args.state);
14243
- return await readWorkState(state, args.conversationId);
14244
- }
14245
- function countPendingConversationMessages(state) {
14246
- return pendingMessages(state).length;
14247
- }
14248
- function hasRunnableConversationWork(state) {
14249
- return hasRunnableWork(state);
14250
- }
14251
- async function appendInboundMessage(args) {
14252
- const nowMs = args.nowMs ?? now();
14253
- return await withConversationMutation(
14254
- { conversationId: args.message.conversationId, state: args.state },
14255
- async (state) => {
14256
- const current = await readWorkState(state, args.message.conversationId) ?? emptyWorkState({
14257
- conversationId: args.message.conversationId,
14258
- destination: args.message.destination,
14259
- nowMs
14260
- });
14261
- assertSameConversationDestination({
14262
- conversationId: args.message.conversationId,
14263
- current: current.destination,
14264
- next: args.message.destination
14265
- });
14266
- const existing = current.messages.find(
14267
- (message) => message.inboundMessageId === args.message.inboundMessageId
14268
- );
14269
- if (existing) {
14270
- const next2 = {
14271
- ...current,
14272
- needsRun: current.needsRun || existing.injectedAtMs === void 0,
14273
- updatedAtMs: nowMs
14274
- };
14275
- await writeWorkState(state, next2);
14276
- return { status: "duplicate" };
14277
- }
14278
- const next = {
14279
- ...current,
14280
- consecutiveFailureCount: 0,
14281
- lastFailureAtMs: void 0,
14282
- messages: [...current.messages, args.message].sort(compareMessages),
14283
- needsRun: true,
14284
- terminallyFailedAtMs: void 0,
14285
- updatedAtMs: nowMs
14286
- };
14287
- await writeWorkState(state, next);
14288
- return { status: "appended" };
14289
- }
14290
- );
14291
- }
14292
- async function appendAndEnqueueInboundMessage(args) {
14293
- const nowMs = args.nowMs ?? now();
14294
- const appendResult = await appendInboundMessage({
14295
- message: args.message,
14296
- nowMs,
14297
- state: args.state
14298
- });
14299
- let idempotencyKey = args.message.inboundMessageId;
14300
- if (appendResult.status === "duplicate") {
14301
- const work = await getConversationWorkState({
14302
- conversationId: args.message.conversationId,
14303
- state: args.state
14304
- });
14305
- if (!work || hasRecentEnqueueMarker(work, nowMs)) {
14306
- return appendResult;
14307
- }
14308
- idempotencyKey = duplicateInboundNudgeIdempotencyKey(args.message, nowMs);
14309
- }
14310
- const queueResult = await args.queue.send(
14311
- {
14312
- conversationId: args.message.conversationId,
14313
- destination: args.message.destination
14314
- },
14315
- { idempotencyKey }
14316
- );
14317
- await markConversationWorkEnqueued({
14318
- conversationId: args.message.conversationId,
14319
- nowMs,
14320
- state: args.state
14321
- });
14322
- return {
14323
- ...appendResult,
14324
- queueMessageId: queueResult?.messageId
14325
- };
14326
- }
14327
- async function requestConversationWork(args) {
14328
- const nowMs = args.nowMs ?? now();
14329
- return await withConversationMutation(args, async (state) => {
14330
- const existing = await readWorkState(state, args.conversationId);
14331
- if (existing) {
14332
- assertSameConversationDestination({
14333
- conversationId: args.conversationId,
14334
- current: existing.destination,
14335
- next: args.destination
14336
- });
14337
- }
14338
- const current = existing ?? emptyWorkState({
14339
- conversationId: args.conversationId,
14340
- destination: args.destination,
14341
- nowMs
14342
- });
14343
- await writeWorkState(state, {
14344
- ...current,
14345
- needsRun: true,
14346
- updatedAtMs: nowMs
14347
- });
14348
- return { status: existing === void 0 ? "created" : "updated" };
14349
- });
14350
- }
14351
- async function markConversationWorkEnqueued(args) {
14352
- const nowMs = args.nowMs ?? now();
14353
- await withConversationMutation(args, async (state) => {
14354
- const current = await readWorkState(state, args.conversationId);
14355
- if (!current) {
14356
- return;
14357
- }
14358
- await writeWorkState(state, {
14359
- ...current,
14360
- lastEnqueuedAtMs: nowMs,
14361
- updatedAtMs: nowMs
14362
- });
14363
- });
14364
- }
14365
- async function startConversationWork(args) {
14366
- const nowMs = args.nowMs ?? now();
14367
- return await withConversationMutation(args, async (state) => {
14368
- const current = await readWorkState(state, args.conversationId);
14369
- if (!current) {
14370
- return { status: "no_work" };
14371
- }
14372
- if (isLeaseActive(current.lease, nowMs)) {
14373
- return {
14374
- status: "active",
14375
- leaseExpiresAtMs: current.lease.leaseExpiresAtMs
14376
- };
14377
- }
14378
- if (!hasRunnableWork(current)) {
14379
- return { status: "no_work" };
14380
- }
14381
- const lease = {
14382
- leaseToken: randomUUID4(),
14383
- acquiredAtMs: nowMs,
14384
- lastCheckInAtMs: nowMs,
14385
- leaseExpiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
14386
- };
14387
- await writeWorkState(state, {
14388
- ...current,
14389
- lease,
14390
- needsRun: false,
14391
- updatedAtMs: nowMs
14392
- });
14393
- return {
14394
- status: "acquired",
14395
- leaseToken: lease.leaseToken,
14396
- leaseExpiresAtMs: lease.leaseExpiresAtMs
14397
- };
14398
- });
14399
- }
14400
- async function checkInConversationWork(args) {
14401
- const nowMs = args.nowMs ?? now();
14402
- return await withConversationMutation(args, async (state) => {
14403
- const current = await readWorkState(state, args.conversationId);
14404
- if (!current || current.lease?.leaseToken !== args.leaseToken) {
14405
- return false;
14406
- }
14407
- await writeWorkState(state, {
14408
- ...current,
14409
- lease: {
14410
- ...current.lease,
14411
- lastCheckInAtMs: nowMs,
14412
- leaseExpiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
14413
- },
14414
- updatedAtMs: nowMs
14415
- });
14416
- return true;
14417
- });
14418
- }
14419
- async function drainConversationMailbox(args) {
14420
- const nowMs = args.nowMs ?? now();
14421
- const pending = await withConversationMutation(args, async (state) => {
14422
- const current = await readWorkState(state, args.conversationId);
14423
- if (!current || current.lease?.leaseToken !== args.leaseToken) {
14424
- throw new Error(
14425
- `Conversation work lease is not held for ${args.conversationId}`
14426
- );
14427
- }
14428
- return pendingMessages(current);
14429
- });
14430
- if (pending.length === 0) {
14431
- return [];
14432
- }
14433
- await args.inject(pending);
14434
- await withConversationMutation(args, async (state) => {
14435
- const current = await readWorkState(state, args.conversationId);
14436
- if (!current || current.lease?.leaseToken !== args.leaseToken) {
14437
- throw new Error(
14438
- `Conversation work lease is not held for ${args.conversationId}`
14439
- );
14440
- }
14441
- const drainedIds = new Set(
14442
- pending.map((message) => message.inboundMessageId)
14443
- );
14444
- const messages = current.messages.map(
14445
- (message) => drainedIds.has(message.inboundMessageId) ? { ...message, injectedAtMs: nowMs } : message
14446
- );
14447
- const hasPending = messages.some(
14448
- (message) => message.injectedAtMs === void 0
14449
- );
14450
- await writeWorkState(state, {
14451
- ...current,
14452
- consecutiveFailureCount: 0,
14453
- lastFailureAtMs: void 0,
14454
- messages,
14455
- needsRun: hasPending,
14456
- updatedAtMs: nowMs
14457
- });
14458
- });
14459
- return pending;
14460
- }
14461
- async function markConversationMessagesInjected(args) {
14462
- const nowMs = args.nowMs ?? now();
14463
- const inboundMessageIds = new Set(args.inboundMessageIds);
14464
- return await withConversationMutation(args, async (state) => {
14465
- const current = await readWorkState(state, args.conversationId);
14466
- if (!current || current.lease?.leaseToken !== args.leaseToken) {
14467
- return false;
14468
- }
14469
- if (inboundMessageIds.size === 0) {
14470
- return true;
14471
- }
14472
- let changed = false;
14473
- const messages = current.messages.map((message) => {
14474
- if (!inboundMessageIds.has(message.inboundMessageId) || message.injectedAtMs !== void 0) {
14475
- return message;
14476
- }
14477
- changed = true;
14478
- return { ...message, injectedAtMs: nowMs };
14479
- });
14480
- if (!changed) {
14481
- return true;
14482
- }
14483
- await writeWorkState(state, {
14484
- ...current,
14485
- consecutiveFailureCount: 0,
14486
- lastFailureAtMs: void 0,
14487
- messages,
14488
- updatedAtMs: nowMs
14489
- });
14490
- return true;
14491
- });
14492
- }
14493
- async function requestConversationContinuation(args) {
14494
- const nowMs = args.nowMs ?? now();
14495
- return await withConversationMutation(args, async (state) => {
14496
- const current = await readWorkState(state, args.conversationId);
14497
- if (!current || current.lease?.leaseToken !== args.leaseToken) {
14498
- return false;
14499
- }
14500
- assertSameConversationDestination({
14501
- conversationId: args.conversationId,
14502
- current: current.destination,
14503
- next: args.destination
14504
- });
14505
- await writeWorkState(state, {
14506
- ...current,
14507
- needsRun: true,
14508
- updatedAtMs: nowMs
14509
- });
14510
- return true;
14511
- });
14512
- }
14513
- async function releaseConversationWork(args) {
14514
- const nowMs = args.nowMs ?? now();
14515
- return await withConversationMutation(args, async (state) => {
14516
- const current = await readWorkState(state, args.conversationId);
14517
- if (!current || current.lease?.leaseToken !== args.leaseToken) {
14518
- return false;
14519
- }
14520
- await writeWorkState(state, {
14521
- ...current,
14522
- lease: void 0,
14523
- updatedAtMs: nowMs
14524
- });
14525
- return true;
14526
- });
14527
- }
14528
- async function completeConversationWork(args) {
14529
- const nowMs = args.nowMs ?? now();
14530
- return await withConversationMutation(args, async (state) => {
14531
- const current = await readWorkState(state, args.conversationId);
14532
- if (!current || current.lease?.leaseToken !== args.leaseToken) {
14533
- return "lost_lease";
14534
- }
14535
- const hasPending = pendingMessages(current).length > 0;
14536
- const hasRunnableWork2 = current.needsRun || hasPending;
14537
- await writeWorkState(state, {
14538
- ...current,
14539
- consecutiveFailureCount: 0,
14540
- lastFailureAtMs: void 0,
14541
- lease: void 0,
14542
- needsRun: hasRunnableWork2,
14543
- updatedAtMs: nowMs
14544
- });
14545
- return hasRunnableWork2 ? "pending" : "completed";
14546
- });
14547
- }
14548
- async function clearExpiredConversationLease(args) {
14549
- const nowMs = args.nowMs ?? now();
14550
- return await withConversationMutation(args, async (state) => {
14551
- const current = await readWorkState(state, args.conversationId);
14552
- if (!current?.lease || current.lease.leaseExpiresAtMs > nowMs) {
14553
- return false;
14554
- }
14555
- await writeWorkState(state, {
14556
- ...current,
14557
- lease: void 0,
14558
- needsRun: true,
14559
- updatedAtMs: nowMs
14560
- });
14561
- return true;
14562
- });
14563
- }
14564
- async function recordConversationWorkFailure(args) {
14565
- const nowMs = args.nowMs ?? now();
14566
- return await withConversationMutation(args, async (state) => {
14567
- const current = await readWorkState(state, args.conversationId);
14568
- if (!current) {
14569
- return {
14570
- abandoned: false,
14571
- consecutiveFailureCount: 0,
14572
- releasedLease: false
14573
- };
14574
- }
14575
- const consecutiveFailureCount = current.consecutiveFailureCount + 1;
14576
- const abandoned = consecutiveFailureCount >= CONVERSATION_WORK_MAX_CONSECUTIVE_FAILURES;
14577
- if (!abandoned) {
14578
- await writeWorkState(state, {
14579
- ...current,
14580
- consecutiveFailureCount,
14581
- lastFailureAtMs: nowMs,
14582
- updatedAtMs: nowMs
14583
- });
14584
- return {
14585
- abandoned: false,
14586
- consecutiveFailureCount,
14587
- releasedLease: false
14588
- };
14589
- }
14590
- const releasedLease = Boolean(current.lease);
14591
- const drainedMessages = current.messages.filter(
14592
- (message) => message.injectedAtMs !== void 0
14593
- );
14594
- await writeWorkState(state, {
14595
- ...current,
14596
- consecutiveFailureCount,
14597
- lastFailureAtMs: nowMs,
14598
- lease: void 0,
14599
- messages: drainedMessages,
14600
- needsRun: false,
14601
- terminallyFailedAtMs: nowMs,
14602
- updatedAtMs: nowMs
14603
- });
14604
- return {
14605
- abandoned: true,
14606
- consecutiveFailureCount,
14607
- releasedLease
14608
- };
14609
- });
14610
- }
14611
- async function listConversationWorkIds(args = {}) {
14612
- const state = await getConnectedState(args.state);
14613
- const ids = uniqueStrings(await state.get(indexKey()) ?? []);
14614
- return ids.slice(0, args.limit ?? ids.length);
14615
- }
14616
-
14617
- // src/chat/services/timeout-resume.ts
14618
- var TURN_TIMEOUT_RESUME_HMAC_CONTEXT = "junior.turn_timeout_resume.v1";
14619
- var TURN_TIMEOUT_RESUME_SIGNATURE_VERSION = "v1";
14620
- var TURN_TIMEOUT_RESUME_MAX_SKEW_MS = 5 * 60 * 1e3;
14621
- var TURN_TIMEOUT_RESUME_TIMESTAMP_HEADER = "x-junior-resume-timestamp";
14622
- var TURN_TIMEOUT_RESUME_SIGNATURE_HEADER = "x-junior-resume-signature";
14623
- async function getAwaitingTurnContinuationRequest(args) {
14624
- const sessionRecord = await getAgentTurnSessionRecord(
14625
- args.conversationId,
14626
- args.sessionId
14627
- );
14628
- if (!sessionRecord || sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "timeout" && sessionRecord.resumeReason !== "yield" || sessionRecord.resumeReason === "timeout" && sessionRecord.sliceId < 2) {
14629
- return void 0;
14630
- }
14631
- if (!sessionRecord.destination) {
14632
- return void 0;
14633
- }
14634
- return {
14635
- conversationId: args.conversationId,
14636
- destination: sessionRecord.destination,
14637
- sessionId: args.sessionId,
14638
- expectedVersion: sessionRecord.version
14639
- };
14640
- }
14641
- function getTurnTimeoutResumeSecret() {
14642
- return process.env.JUNIOR_SECRET?.trim() || void 0;
14643
- }
14644
- function buildSignedPayload2(timestamp, body) {
14645
- return `${TURN_TIMEOUT_RESUME_HMAC_CONTEXT}:${timestamp}:${body}`;
14646
- }
14647
- function signTurnTimeoutResumeBody(secret, timestamp, body) {
14648
- const digest = createHmac3("sha256", secret).update(buildSignedPayload2(timestamp, body)).digest("hex");
14649
- return `${TURN_TIMEOUT_RESUME_SIGNATURE_VERSION}=${digest}`;
14650
- }
14651
- function timingSafeMatch3(expected, actual) {
14652
- const expectedBuffer = Buffer.from(expected);
14653
- const actualBuffer = Buffer.from(actual);
14654
- if (expectedBuffer.length !== actualBuffer.length) {
14655
- return false;
14656
- }
14657
- return timingSafeEqual3(expectedBuffer, actualBuffer);
14658
- }
14659
- function parseTurnTimeoutResumeRequest(value) {
14660
- if (!value || typeof value !== "object") {
14661
- return void 0;
14662
- }
14663
- const record = value;
14664
- const destination = parseDestination(record.destination);
14665
- let expectedVersion = record.expectedVersion;
14666
- if (typeof expectedVersion !== "number") {
14667
- expectedVersion = record.expectedCheckpointVersion;
14668
- }
14669
- if (typeof record.conversationId !== "string" || typeof record.sessionId !== "string" || typeof expectedVersion !== "number" || !destination) {
14670
- return void 0;
14671
- }
14672
- return {
14673
- conversationId: record.conversationId,
14674
- destination,
14675
- sessionId: record.sessionId,
14676
- expectedVersion
14677
- };
14678
- }
14679
- async function scheduleTurnTimeoutResume(request, options = {}) {
14680
- const nowMs = options.nowMs ?? Date.now();
14681
- await requestConversationWork({
14682
- conversationId: request.conversationId,
14683
- destination: request.destination,
14684
- nowMs,
14685
- state: options.state
14686
- });
14687
- const queue = options.queue ?? getVercelConversationWorkQueue();
14688
- await queue.send(
14689
- {
14690
- conversationId: request.conversationId,
14691
- destination: request.destination
14692
- },
14693
- {
14694
- idempotencyKey: [
14695
- "timeout",
14696
- request.conversationId,
14697
- request.sessionId,
14698
- request.expectedVersion
14699
- ].join(":")
14700
- }
14701
- );
14702
- await markConversationWorkEnqueued({
14703
- conversationId: request.conversationId,
14704
- nowMs,
14705
- state: options.state
14706
- });
14707
- }
14708
- async function verifyTurnTimeoutResumeRequest(request) {
14709
- const timestamp = request.headers.get(TURN_TIMEOUT_RESUME_TIMESTAMP_HEADER)?.trim() ?? "";
14710
- const signature = request.headers.get(TURN_TIMEOUT_RESUME_SIGNATURE_HEADER)?.trim() ?? "";
14711
- const secret = getTurnTimeoutResumeSecret();
14712
- if (!timestamp || !signature || !secret) {
14713
- return void 0;
14714
- }
14715
- const parsedTimestamp = Number.parseInt(timestamp, 10);
14716
- if (!Number.isFinite(parsedTimestamp) || Math.abs(Date.now() - parsedTimestamp) > TURN_TIMEOUT_RESUME_MAX_SKEW_MS) {
14717
- return void 0;
14718
- }
14719
- const body = await request.text();
14720
- const expectedSignature = signTurnTimeoutResumeBody(secret, timestamp, body);
14721
- if (!timingSafeMatch3(expectedSignature, signature)) {
14722
- return void 0;
14723
- }
14724
- try {
14725
- return parseTurnTimeoutResumeRequest(JSON.parse(body));
14726
- } catch {
14727
- return void 0;
14728
- }
14729
- }
14730
-
14731
- // src/chat/task-execution/heartbeat.ts
14732
- var DEFAULT_RECOVERY_LIMIT = 25;
14733
- function heartbeatIdempotencyKey(reason, conversationId, nowMs) {
14734
- return `heartbeat:${reason}:${conversationId}:${nowMs}`;
14735
- }
14736
- async function sendRecoveryNudge(args) {
14737
- await args.queue.send(
14738
- {
14739
- conversationId: args.conversationId,
14740
- destination: args.destination
14741
- },
14742
- { idempotencyKey: args.idempotencyKey }
14743
- );
14744
- await markConversationWorkEnqueued({
14745
- conversationId: args.conversationId,
14746
- nowMs: args.nowMs,
14747
- state: args.state
14748
- });
14749
- }
14750
- async function recoverConversationWork(args) {
14751
- const result = {
14752
- expiredLeaseCount: 0,
14753
- pendingCount: 0
14754
- };
14755
- const ids = await listConversationWorkIds({
13850
+ const staleBeforeMs = args.nowMs - CONVERSATION_WORK_STALE_ENQUEUE_MS;
13851
+ const ids = await listActiveConversationIds({
14756
13852
  limit: args.limit ?? DEFAULT_RECOVERY_LIMIT,
13853
+ staleBeforeMs,
14757
13854
  state: args.state
14758
13855
  });
14759
13856
  for (const conversationId of ids) {
@@ -14763,9 +13860,24 @@ async function recoverConversationWork(args) {
14763
13860
  state: args.state
14764
13861
  });
14765
13862
  if (!work) {
13863
+ await removeActiveConversation({
13864
+ conversationId,
13865
+ state: args.state
13866
+ });
13867
+ continue;
13868
+ }
13869
+ if (work.execution.status === "idle") {
13870
+ await removeActiveConversation({
13871
+ conversationId,
13872
+ state: args.state
13873
+ });
13874
+ continue;
13875
+ }
13876
+ const destination = work.destination;
13877
+ if (!destination) {
14766
13878
  continue;
14767
13879
  }
14768
- if (work.lease && work.lease.leaseExpiresAtMs <= args.nowMs) {
13880
+ if (work.execution.lease && work.execution.lease.expiresAtMs <= args.nowMs) {
14769
13881
  const cleared = await clearExpiredConversationLease({
14770
13882
  conversationId,
14771
13883
  nowMs: args.nowMs,
@@ -14776,7 +13888,7 @@ async function recoverConversationWork(args) {
14776
13888
  }
14777
13889
  await sendRecoveryNudge({
14778
13890
  conversationId,
14779
- destination: work.destination,
13891
+ destination,
14780
13892
  idempotencyKey: heartbeatIdempotencyKey(
14781
13893
  "lease",
14782
13894
  conversationId,
@@ -14795,15 +13907,15 @@ async function recoverConversationWork(args) {
14795
13907
  );
14796
13908
  continue;
14797
13909
  }
14798
- if (work.lease || !hasRunnableConversationWork(work)) {
13910
+ if (work.execution.lease || !hasRunnableConversationWork(work)) {
14799
13911
  continue;
14800
13912
  }
14801
- if (typeof work.lastEnqueuedAtMs === "number" && work.lastEnqueuedAtMs + CONVERSATION_WORK_STALE_ENQUEUE_MS > args.nowMs) {
13913
+ if (typeof work.execution.lastEnqueuedAtMs === "number" && work.execution.lastEnqueuedAtMs + CONVERSATION_WORK_STALE_ENQUEUE_MS > args.nowMs) {
14802
13914
  continue;
14803
13915
  }
14804
13916
  await sendRecoveryNudge({
14805
13917
  conversationId,
14806
- destination: work.destination,
13918
+ destination,
14807
13919
  idempotencyKey: heartbeatIdempotencyKey(
14808
13920
  "pending",
14809
13921
  conversationId,
@@ -15016,8 +14128,6 @@ var DEFAULT_RECOVERY_LIMIT2 = 25;
15016
14128
  var DEFAULT_PLUGIN_LIMIT = 25;
15017
14129
  var DISPATCH_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
15018
14130
  var PLUGIN_HEARTBEAT_TIMEOUT_MS = 25e3;
15019
- var TIMEOUT_RESUME_STALE_MS = 2 * 60 * 1e3;
15020
- var TIMEOUT_RESUME_RECOVERY_SCAN_LIMIT = 500;
15021
14131
  function isStaleDispatch(args) {
15022
14132
  if (args.record.status === "running") {
15023
14133
  return typeof args.record.leaseExpiresAtMs === "number" && args.record.leaseExpiresAtMs <= args.nowMs;
@@ -15062,63 +14172,6 @@ async function runWithTimeout(promise, timeoutMs) {
15062
14172
  }
15063
14173
  }
15064
14174
  }
15065
- async function recoverStaleTimeoutResumes(args) {
15066
- const summaries = await listAgentTurnSessionSummaries(
15067
- TIMEOUT_RESUME_RECOVERY_SCAN_LIMIT
15068
- );
15069
- let recovered = 0;
15070
- for (const summary of summaries) {
15071
- if (recovered >= (args.limit ?? DEFAULT_RECOVERY_LIMIT2)) {
15072
- break;
15073
- }
15074
- if (summary.state !== "awaiting_resume" || summary.resumeReason !== "timeout" && summary.resumeReason !== "yield" || summary.updatedAtMs + TIMEOUT_RESUME_STALE_MS > args.nowMs) {
15075
- continue;
15076
- }
15077
- try {
15078
- const persistedState = await getPersistedThreadState(
15079
- summary.conversationId
15080
- );
15081
- const conversation = coerceThreadConversationState(persistedState);
15082
- if (conversation.processing.activeTurnId !== summary.sessionId) {
15083
- continue;
15084
- }
15085
- const request = await getAwaitingTurnContinuationRequest({
15086
- conversationId: summary.conversationId,
15087
- sessionId: summary.sessionId
15088
- });
15089
- if (!request) {
15090
- continue;
15091
- }
15092
- await scheduleTurnTimeoutResume(request, {
15093
- queue: args.conversationWorkQueue
15094
- });
15095
- recovered += 1;
15096
- logInfo(
15097
- "agent_turn_timeout_resume_recovery_scheduled",
15098
- {},
15099
- {
15100
- "app.ai.conversation_id": summary.conversationId,
15101
- "app.ai.session_id": summary.sessionId,
15102
- "app.ai.resume_session_version": request.expectedVersion,
15103
- "app.ai.resume_slice_id": summary.sliceId
15104
- },
15105
- "Heartbeat rescheduled stale timeout resume"
15106
- );
15107
- } catch (error) {
15108
- logException(
15109
- error,
15110
- "agent_turn_timeout_resume_recovery_failed",
15111
- {},
15112
- {
15113
- "app.ai.conversation_id": summary.conversationId,
15114
- "app.ai.session_id": summary.sessionId
15115
- },
15116
- "Heartbeat timeout resume recovery failed"
15117
- );
15118
- }
15119
- }
15120
- return recovered;
15121
- }
15122
14175
  async function recoverStaleDispatches(args) {
15123
14176
  const ids = await listIncompleteDispatchIds();
15124
14177
  let recovered = 0;
@@ -15216,10 +14269,6 @@ async function runHeartbeat(args) {
15216
14269
  nowMs: args.nowMs,
15217
14270
  queue: args.conversationWorkQueue ?? getVercelConversationWorkQueue()
15218
14271
  });
15219
- await recoverStaleTimeoutResumes({
15220
- conversationWorkQueue: args.conversationWorkQueue,
15221
- nowMs: args.nowMs
15222
- });
15223
14272
  await recoverStaleDispatches({ nowMs: args.nowMs });
15224
14273
  await runPluginHeartbeats({ nowMs: args.nowMs });
15225
14274
  }
@@ -15239,7 +14288,7 @@ function verifyHeartbeatRequest(request) {
15239
14288
  }
15240
14289
  const actual = Buffer.from(authorization.slice("Bearer ".length));
15241
14290
  const expected = Buffer.from(secret);
15242
- return actual.length === expected.length && timingSafeEqual4(actual, expected);
14291
+ return actual.length === expected.length && timingSafeEqual3(actual, expected);
15243
14292
  }
15244
14293
  async function GET2(request, waitUntil, options = {}) {
15245
14294
  if (!verifyHeartbeatRequest(request)) {
@@ -15399,7 +14448,7 @@ var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
15399
14448
  var STATUS_MIN_VISIBLE_MS = 1200;
15400
14449
  var STATUS_ROTATION_INTERVAL_MS = 3e4;
15401
14450
  function createAssistantStatusScheduler(args) {
15402
- const now3 = args.now ?? (() => Date.now());
14451
+ const now2 = args.now ?? (() => Date.now());
15403
14452
  const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
15404
14453
  const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
15405
14454
  const random = args.random ?? Math.random;
@@ -15463,7 +14512,7 @@ function createAssistantStatusScheduler(args) {
15463
14512
  }
15464
14513
  currentVisibleStatus = text;
15465
14514
  currentLoadingMessages = nextLoadingMessages;
15466
- lastStatusAt = now3();
14515
+ lastStatusAt = now2();
15467
14516
  scheduleRotation();
15468
14517
  await enqueueStatusUpdate(async () => {
15469
14518
  await args.sendStatus(text, nextLoadingMessages);
@@ -15546,7 +14595,7 @@ function createAssistantStatusScheduler(args) {
15546
14595
  }
15547
14596
  return;
15548
14597
  }
15549
- const elapsed = now3() - lastStatusAt;
14598
+ const elapsed = now2() - lastStatusAt;
15550
14599
  const waitMs = Math.max(
15551
14600
  STATUS_UPDATE_DEBOUNCE_MS - elapsed,
15552
14601
  STATUS_MIN_VISIBLE_MS - elapsed,
@@ -16224,7 +15273,7 @@ async function resumeSlackTurn(args) {
16224
15273
  deferredPauseHandler = async () => {
16225
15274
  await onAuthPause(error);
16226
15275
  };
16227
- } else if (isRetryableTurnError(error, "turn_timeout_resume") && onTimeoutPause) {
15276
+ } else if (isRetryableTurnError(error, "agent_continue") && onTimeoutPause) {
16228
15277
  deferredPauseKind = "timeout";
16229
15278
  deferredPauseHandler = async () => {
16230
15279
  await onTimeoutPause(error);
@@ -16332,6 +15381,56 @@ async function persistAuthPauseTurnState(args) {
16332
15381
  await persistThreadStateById(args.threadStateId, { conversation });
16333
15382
  }
16334
15383
 
15384
+ // src/chat/services/agent-continue.ts
15385
+ async function getAwaitingAgentContinueRequest(args) {
15386
+ const sessionRecord = await getAgentTurnSessionRecord(
15387
+ args.conversationId,
15388
+ args.sessionId
15389
+ );
15390
+ if (!sessionRecord || sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "timeout" && sessionRecord.resumeReason !== "yield" || sessionRecord.resumeReason === "timeout" && sessionRecord.sliceId < 2) {
15391
+ return void 0;
15392
+ }
15393
+ if (!sessionRecord.destination) {
15394
+ return void 0;
15395
+ }
15396
+ return {
15397
+ conversationId: args.conversationId,
15398
+ destination: sessionRecord.destination,
15399
+ sessionId: args.sessionId,
15400
+ expectedVersion: sessionRecord.version
15401
+ };
15402
+ }
15403
+ async function scheduleAgentContinue(request, options = {}) {
15404
+ const nowMs = options.nowMs ?? Date.now();
15405
+ await requestConversationWork({
15406
+ conversationId: request.conversationId,
15407
+ destination: request.destination,
15408
+ nowMs,
15409
+ state: options.state
15410
+ });
15411
+ const queue = options.queue ?? getVercelConversationWorkQueue();
15412
+ await queue.send(
15413
+ {
15414
+ conversationId: request.conversationId,
15415
+ destination: request.destination
15416
+ },
15417
+ {
15418
+ idempotencyKey: [
15419
+ "agent-continue",
15420
+ request.conversationId,
15421
+ request.sessionId,
15422
+ request.expectedVersion,
15423
+ nowMs
15424
+ ].join(":")
15425
+ }
15426
+ );
15427
+ await markConversationWorkEnqueued({
15428
+ conversationId: request.conversationId,
15429
+ nowMs,
15430
+ state: options.state
15431
+ });
15432
+ }
15433
+
16335
15434
  // src/handlers/oauth-html.ts
16336
15435
  function htmlCallbackResponse(title, message, status) {
16337
15436
  const html = `<!DOCTYPE html>
@@ -16351,87 +15450,6 @@ function htmlCallbackResponse(title, message, status) {
16351
15450
  });
16352
15451
  }
16353
15452
 
16354
- // src/chat/slack/user.ts
16355
- var USER_CACHE_TTL_MS = 5 * 60 * 1e3;
16356
- var userCache = /* @__PURE__ */ new Map();
16357
- function readFromCache(userId) {
16358
- const hit = userCache.get(userId);
16359
- if (!hit) return null;
16360
- if (hit.expiresAt < Date.now()) {
16361
- userCache.delete(userId);
16362
- return null;
16363
- }
16364
- return hit.value;
16365
- }
16366
- function writeToCache(userId, value) {
16367
- userCache.set(userId, {
16368
- value,
16369
- expiresAt: Date.now() + USER_CACHE_TTL_MS
16370
- });
16371
- }
16372
- async function lookupSlackUser(userId) {
16373
- if (!userId) {
16374
- return null;
16375
- }
16376
- const cached = readFromCache(userId);
16377
- if (cached) {
16378
- return cached;
16379
- }
16380
- const token = getSlackBotToken();
16381
- if (!token) {
16382
- return null;
16383
- }
16384
- try {
16385
- const response = await fetch(
16386
- `https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`,
16387
- {
16388
- headers: {
16389
- authorization: `Bearer ${token}`
16390
- }
16391
- }
16392
- );
16393
- if (!response.ok) {
16394
- logWarn(
16395
- "slack_user_lookup_failed",
16396
- {},
16397
- {
16398
- "enduser.id": userId,
16399
- "http.response.status_code": response.status
16400
- },
16401
- "Slack user lookup request failed"
16402
- );
16403
- return null;
16404
- }
16405
- const payload = await response.json();
16406
- if (!payload.ok || !payload.user) {
16407
- return null;
16408
- }
16409
- const userName = payload.user.name?.trim() || void 0;
16410
- const fullName = payload.user.profile?.display_name?.trim() || payload.user.profile?.real_name?.trim() || payload.user.real_name?.trim() || void 0;
16411
- const result = {
16412
- userName,
16413
- fullName,
16414
- email: payload.user.profile?.email?.trim() || void 0
16415
- };
16416
- writeToCache(userId, result);
16417
- return result;
16418
- } catch (error) {
16419
- logWarn(
16420
- "slack_user_lookup_failed",
16421
- {},
16422
- {
16423
- "enduser.id": userId,
16424
- "exception.message": error instanceof Error ? error.message : String(error)
16425
- },
16426
- "Slack user lookup failed with exception"
16427
- );
16428
- return null;
16429
- }
16430
- }
16431
- async function lookupSlackActorIdentity(userId) {
16432
- return slackActorIdentity(userId, await lookupSlackUser(userId));
16433
- }
16434
-
16435
15453
  // src/handlers/mcp-oauth-callback.ts
16436
15454
  var CALLBACK_PAGES = {
16437
15455
  missing_state: {
@@ -16622,6 +15640,22 @@ async function resumeAuthorizedMcpTurn(args) {
16622
15640
  const lockedChannelConfiguration = getChannelConfigurationServiceById(
16623
15641
  authSession.channelId
16624
15642
  );
15643
+ let requester;
15644
+ try {
15645
+ requester = createRequesterFromStoredSlackRequester({
15646
+ requester: lockedSessionRecord.requester,
15647
+ teamId: destination.teamId,
15648
+ userId: authSession.userId
15649
+ });
15650
+ } catch {
15651
+ await failAgentTurnSessionRecord({
15652
+ conversationId: authSession.conversationId,
15653
+ expectedVersion: lockedSessionRecord.version,
15654
+ sessionId: lockedSessionId,
15655
+ errorMessage: "Stored Slack requester identity did not match OAuth requester"
15656
+ });
15657
+ return false;
15658
+ }
16625
15659
  await recordAuthorizationCompleted({
16626
15660
  conversationId: authSession.conversationId,
16627
15661
  kind: "mcp",
@@ -16633,13 +15667,12 @@ async function resumeAuthorizedMcpTurn(args) {
16633
15667
  }),
16634
15668
  ttlMs: THREAD_STATE_TTL_MS4
16635
15669
  });
16636
- const requester = await lookupSlackActorIdentity(authSession.userId);
16637
15670
  return {
16638
15671
  messageText: lockedUserMessage.text,
16639
15672
  messageTs: getTurnUserSlackMessageTs(lockedUserMessage),
16640
15673
  replyContext: {
16641
15674
  credentialContext: {
16642
- actor: { type: "user", userId: authSession.userId }
15675
+ actor: { type: "user", userId: requester.userId }
16643
15676
  },
16644
15677
  requester,
16645
15678
  destination,
@@ -16648,7 +15681,7 @@ async function resumeAuthorizedMcpTurn(args) {
16648
15681
  turnId: lockedSessionId,
16649
15682
  channelId: authSession.channelId,
16650
15683
  threadTs: authSession.threadTs,
16651
- requesterId: authSession.userId
15684
+ requesterId: requester.userId
16652
15685
  },
16653
15686
  toolChannelId: authSession.toolChannelId ?? lockedArtifacts.assistantContextChannelId ?? authSession.channelId,
16654
15687
  conversationContext: lockedConversationContext,
@@ -16720,16 +15753,16 @@ async function resumeAuthorizedMcpTurn(args) {
16720
15753
  );
16721
15754
  },
16722
15755
  onTimeoutPause: async (error) => {
16723
- if (!isRetryableTurnError(error, "turn_timeout_resume")) {
15756
+ if (!isRetryableTurnError(error, "agent_continue")) {
16724
15757
  throw error;
16725
15758
  }
16726
15759
  const version = error.metadata?.version;
16727
15760
  if (typeof version !== "number") {
16728
15761
  throw new Error(
16729
- "Timed-out MCP resume did not include a turn-session version"
15762
+ "MCP OAuth agent continuation did not include a session record version"
16730
15763
  );
16731
15764
  }
16732
- await scheduleTurnTimeoutResume({
15765
+ await scheduleAgentContinue({
16733
15766
  conversationId: authSession.conversationId,
16734
15767
  destination,
16735
15768
  sessionId: lockedSessionId,
@@ -16920,28 +15953,118 @@ async function buildHomeView(userId, userTokenStore) {
16920
15953
  },
16921
15954
  { type: "divider" },
16922
15955
  {
16923
- type: "header",
16924
- text: {
16925
- type: "plain_text",
16926
- text: "Connected accounts"
16927
- }
15956
+ type: "header",
15957
+ text: {
15958
+ type: "plain_text",
15959
+ text: "Connected accounts"
15960
+ }
15961
+ },
15962
+ ...accountBlocks,
15963
+ {
15964
+ type: "context",
15965
+ elements: [
15966
+ {
15967
+ type: "mrkdwn",
15968
+ text: `*junior version:* \`${runtimeMetadata.version ?? "unknown"}\``
15969
+ }
15970
+ ]
15971
+ }
15972
+ ]
15973
+ };
15974
+ }
15975
+ async function publishAppHomeView(slackClient, userId, userTokenStore) {
15976
+ const view = await buildHomeView(userId, userTokenStore);
15977
+ await slackClient.views.publish({ user_id: userId, view });
15978
+ }
15979
+
15980
+ // src/chat/slack/user.ts
15981
+ var USER_CACHE_TTL_MS = 5 * 60 * 1e3;
15982
+ var userCache = /* @__PURE__ */ new Map();
15983
+ function userCacheKey(teamId, userId) {
15984
+ return `${teamId}:${userId}`;
15985
+ }
15986
+ function readFromCache(teamId, userId) {
15987
+ const hit = userCache.get(userCacheKey(teamId, userId));
15988
+ if (!hit) return null;
15989
+ if (hit.expiresAt < Date.now()) {
15990
+ userCache.delete(userCacheKey(teamId, userId));
15991
+ return null;
15992
+ }
15993
+ return hit.value;
15994
+ }
15995
+ function writeToCache(teamId, userId, value) {
15996
+ userCache.set(userCacheKey(teamId, userId), {
15997
+ value,
15998
+ expiresAt: Date.now() + USER_CACHE_TTL_MS
15999
+ });
16000
+ }
16001
+ async function lookupSlackUser(teamId, userId) {
16002
+ if (!teamId || !userId) {
16003
+ return null;
16004
+ }
16005
+ const cached = readFromCache(teamId, userId);
16006
+ if (cached) {
16007
+ return cached;
16008
+ }
16009
+ const token = getSlackBotToken();
16010
+ if (!token) {
16011
+ return null;
16012
+ }
16013
+ try {
16014
+ const response = await fetch(
16015
+ `https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`,
16016
+ {
16017
+ headers: {
16018
+ authorization: `Bearer ${token}`
16019
+ }
16020
+ }
16021
+ );
16022
+ if (!response.ok) {
16023
+ logWarn(
16024
+ "slack_user_lookup_failed",
16025
+ {},
16026
+ {
16027
+ "enduser.id": userId,
16028
+ "app.slack.team_id": teamId,
16029
+ "http.response.status_code": response.status
16030
+ },
16031
+ "Slack user lookup request failed"
16032
+ );
16033
+ return null;
16034
+ }
16035
+ const payload = await response.json();
16036
+ if (!payload.ok || !payload.user) {
16037
+ return null;
16038
+ }
16039
+ const userName = payload.user.name?.trim() || void 0;
16040
+ const fullName = payload.user.profile?.display_name?.trim() || payload.user.profile?.real_name?.trim() || payload.user.real_name?.trim() || void 0;
16041
+ const result = {
16042
+ userName,
16043
+ fullName,
16044
+ email: payload.user.profile?.email?.trim() || void 0
16045
+ };
16046
+ writeToCache(teamId, userId, result);
16047
+ return result;
16048
+ } catch (error) {
16049
+ logWarn(
16050
+ "slack_user_lookup_failed",
16051
+ {},
16052
+ {
16053
+ "enduser.id": userId,
16054
+ "app.slack.team_id": teamId,
16055
+ "exception.message": error instanceof Error ? error.message : String(error)
16928
16056
  },
16929
- ...accountBlocks,
16930
- {
16931
- type: "context",
16932
- elements: [
16933
- {
16934
- type: "mrkdwn",
16935
- text: `*junior version:* \`${runtimeMetadata.version ?? "unknown"}\``
16936
- }
16937
- ]
16938
- }
16939
- ]
16940
- };
16057
+ "Slack user lookup failed with exception"
16058
+ );
16059
+ return null;
16060
+ }
16941
16061
  }
16942
- async function publishAppHomeView(slackClient, userId, userTokenStore) {
16943
- const view = await buildHomeView(userId, userTokenStore);
16944
- await slackClient.views.publish({ user_id: userId, view });
16062
+ async function lookupSlackRequester(teamId, userId) {
16063
+ return createSlackRequester(
16064
+ teamId,
16065
+ userId,
16066
+ await lookupSlackUser(teamId, userId)
16067
+ );
16945
16068
  }
16946
16069
 
16947
16070
  // src/handlers/oauth-callback.ts
@@ -17124,6 +16247,22 @@ async function resumeOAuthSessionRecordTurn(stored) {
17124
16247
  const lockedChannelConfiguration = getChannelConfigurationServiceById(
17125
16248
  stored.channelId
17126
16249
  );
16250
+ let requester;
16251
+ try {
16252
+ requester = createRequesterFromStoredSlackRequester({
16253
+ requester: lockedSessionRecord.requester,
16254
+ teamId: destination.teamId,
16255
+ userId: lockedUserMessage.author.userId
16256
+ });
16257
+ } catch {
16258
+ await failAgentTurnSessionRecord({
16259
+ conversationId: stored.resumeConversationId,
16260
+ expectedVersion: lockedSessionRecord.version,
16261
+ sessionId: lockedSessionId,
16262
+ errorMessage: "Stored Slack requester identity did not match OAuth requester"
16263
+ });
16264
+ return false;
16265
+ }
17127
16266
  await recordAuthorizationCompleted({
17128
16267
  conversationId: stored.resumeConversationId,
17129
16268
  kind: "plugin",
@@ -17135,9 +16274,6 @@ async function resumeOAuthSessionRecordTurn(stored) {
17135
16274
  }),
17136
16275
  ttlMs: THREAD_STATE_TTL_MS5
17137
16276
  });
17138
- const requester = await lookupSlackActorIdentity(
17139
- lockedUserMessage.author.userId
17140
- );
17141
16277
  return {
17142
16278
  messageText: lockedPendingAuth ? lockedUserMessage.text : stored.pendingMessage ?? lockedUserMessage.text,
17143
16279
  messageTs: getTurnUserSlackMessageTs(lockedUserMessage),
@@ -17145,7 +16281,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
17145
16281
  credentialContext: {
17146
16282
  actor: {
17147
16283
  type: "user",
17148
- userId: lockedUserMessage.author.userId
16284
+ userId: requester.userId
17149
16285
  }
17150
16286
  },
17151
16287
  requester,
@@ -17155,7 +16291,7 @@ async function resumeOAuthSessionRecordTurn(stored) {
17155
16291
  turnId: lockedSessionId,
17156
16292
  channelId: stored.channelId,
17157
16293
  threadTs: stored.threadTs,
17158
- requesterId: lockedUserMessage.author.userId
16294
+ requesterId: requester.userId
17159
16295
  },
17160
16296
  toolChannelId: lockedArtifacts.assistantContextChannelId ?? stored.channelId,
17161
16297
  artifactState: lockedArtifacts,
@@ -17215,16 +16351,16 @@ async function resumeOAuthSessionRecordTurn(stored) {
17215
16351
  });
17216
16352
  },
17217
16353
  onTimeoutPause: async (error) => {
17218
- if (!isRetryableTurnError(error, "turn_timeout_resume")) {
16354
+ if (!isRetryableTurnError(error, "agent_continue")) {
17219
16355
  throw error;
17220
16356
  }
17221
16357
  const version = error.metadata?.version;
17222
16358
  if (typeof version !== "number") {
17223
16359
  throw new Error(
17224
- "Timed-out OAuth resume did not include a turn-session version"
16360
+ "OAuth agent continuation did not include a session record version"
17225
16361
  );
17226
16362
  }
17227
- await scheduleTurnTimeoutResume({
16363
+ await scheduleAgentContinue({
17228
16364
  conversationId: stored.resumeConversationId,
17229
16365
  destination,
17230
16366
  sessionId: lockedSessionId,
@@ -17248,7 +16384,10 @@ async function resumePendingOAuthMessage(stored) {
17248
16384
  const conversationContext = buildConversationContext(conversation, {
17249
16385
  excludeMessageId: latestUserMessage?.id
17250
16386
  });
17251
- const requester = await lookupSlackActorIdentity(stored.userId);
16387
+ const requester = await lookupSlackRequester(
16388
+ stored.destination.teamId,
16389
+ stored.userId
16390
+ );
17252
16391
  await resumeAuthorizedRequest({
17253
16392
  messageText: stored.pendingMessage,
17254
16393
  channelId: stored.channelId,
@@ -17325,8 +16464,8 @@ async function GET4(request, provider, waitUntil) {
17325
16464
  );
17326
16465
  }
17327
16466
  const stateAdapter = getStateAdapter();
17328
- const stateKey2 = `oauth-state:${state}`;
17329
- const stored = parseOAuthStatePayload(await stateAdapter.get(stateKey2));
16467
+ const stateKey = `oauth-state:${state}`;
16468
+ const stored = parseOAuthStatePayload(await stateAdapter.get(stateKey));
17330
16469
  if (!stored) {
17331
16470
  return htmlErrorResponse(
17332
16471
  "Link expired",
@@ -17341,7 +16480,7 @@ async function GET4(request, provider, waitUntil) {
17341
16480
  400
17342
16481
  );
17343
16482
  }
17344
- await stateAdapter.delete(stateKey2);
16483
+ await stateAdapter.delete(stateKey);
17345
16484
  const clientId = process.env[providerConfig.clientIdEnv]?.trim();
17346
16485
  const clientSecret = process.env[providerConfig.clientSecretEnv]?.trim();
17347
16486
  if (!clientId || !clientSecret) {
@@ -17648,9 +16787,9 @@ function buildJwksUrl(value) {
17648
16787
  return url;
17649
16788
  }
17650
16789
  async function getJwks(issuer) {
17651
- const now3 = Date.now();
16790
+ const now2 = Date.now();
17652
16791
  const cached = jwksByIssuer.get(issuer);
17653
- if (cached && cached.expiresAtMs > now3) {
16792
+ if (cached && cached.expiresAtMs > now2) {
17654
16793
  return cached.jwks;
17655
16794
  }
17656
16795
  if (cached) {
@@ -17675,7 +16814,7 @@ async function getJwks(issuer) {
17675
16814
  }
17676
16815
  jwksByIssuer.set(issuer, {
17677
16816
  jwks,
17678
- expiresAtMs: now3 + OIDC_DISCOVERY_CACHE_TTL_MS
16817
+ expiresAtMs: now2 + OIDC_DISCOVERY_CACHE_TTL_MS
17679
16818
  });
17680
16819
  return jwks;
17681
16820
  }
@@ -18459,292 +17598,22 @@ async function proxySandboxEgressRequest(request, deps = {}) {
18459
17598
  ...provider === "github" ? githubPermissionHeaders(upstream) : {}
18460
17599
  });
18461
17600
  }
18462
- }
18463
- return new Response(upstream.body, {
18464
- status: upstream.status,
18465
- statusText: upstream.statusText,
18466
- headers: responseHeaders(upstream)
18467
- });
18468
- }
18469
-
18470
- // src/handlers/sandbox-egress-proxy.ts
18471
- async function ALL(request, options = {}) {
18472
- return await proxySandboxEgressRequest(request, {
18473
- interceptHttp: options.interceptHttp
18474
- });
18475
- }
18476
- function isSandboxEgressRequest(request) {
18477
- return isSandboxEgressForwardedRequest(request);
18478
- }
18479
-
18480
- // src/chat/runtime/timeout-resume-runner.ts
18481
- var TIMEOUT_RESUME_LOCK_RETRY_DELAYS_MS = [250, 1e3, 2e3];
18482
- function sleep4(ms) {
18483
- return new Promise((resolve) => setTimeout(resolve, ms));
18484
- }
18485
- async function persistCompletedReplyState2(args) {
18486
- const currentState = await getPersistedThreadState(
18487
- args.sessionRecord.conversationId
18488
- );
18489
- const conversation = coerceThreadConversationState(currentState);
18490
- const artifacts = coerceThreadArtifactsState(currentState);
18491
- const userMessage2 = getTurnUserMessage(
18492
- conversation,
18493
- args.sessionRecord.sessionId
18494
- );
18495
- const statePatch = buildDeliveredTurnStatePatch({
18496
- artifacts,
18497
- conversation,
18498
- reply: args.reply,
18499
- sessionId: args.sessionRecord.sessionId,
18500
- userMessageId: userMessage2?.id
18501
- });
18502
- await persistThreadStateById(args.sessionRecord.conversationId, {
18503
- ...statePatch
18504
- });
18505
- }
18506
- async function failSessionRecordBestEffort3(args) {
18507
- try {
18508
- await failAgentTurnSessionRecord({
18509
- conversationId: args.sessionRecord.conversationId,
18510
- expectedVersion: args.sessionRecord.version,
18511
- sessionId: args.sessionRecord.sessionId,
18512
- errorMessage: args.errorMessage
18513
- });
18514
- } catch (error) {
18515
- logException(
18516
- error,
18517
- "timeout_resume_session_record_fail_persist_failed",
18518
- {},
18519
- {
18520
- "app.ai.conversation_id": args.sessionRecord.conversationId,
18521
- "app.ai.session_id": args.sessionRecord.sessionId
18522
- },
18523
- "Failed to mark timed-out turn session record failed"
18524
- );
18525
- }
18526
- }
18527
- async function persistFailedReplyState2(sessionRecord) {
18528
- const currentState = await getPersistedThreadState(
18529
- sessionRecord.conversationId
18530
- );
18531
- const conversation = coerceThreadConversationState(currentState);
18532
- clearPendingAuth(conversation, sessionRecord.sessionId);
18533
- markTurnFailed({
18534
- conversation,
18535
- nowMs: Date.now(),
18536
- sessionId: sessionRecord.sessionId,
18537
- userMessageId: getTurnUserMessage(conversation, sessionRecord.sessionId)?.id,
18538
- markConversationMessage,
18539
- updateConversationStats
18540
- });
18541
- await failSessionRecordBestEffort3({
18542
- sessionRecord,
18543
- errorMessage: "Timed-out turn failed while resuming"
18544
- });
18545
- await persistThreadStateById(sessionRecord.conversationId, {
18546
- conversation
18547
- });
18548
- }
18549
- async function resumeTimedOutTurn(payload, options = {}) {
18550
- const thread = parseSlackThreadId(payload.conversationId);
18551
- if (!thread) {
18552
- throw new Error(
18553
- `Timeout resume requires a Slack thread conversation id, got "${payload.conversationId}"`
18554
- );
18555
- }
18556
- const scheduleTurnTimeoutResume2 = options.scheduleTurnTimeoutResume ?? scheduleTurnTimeoutResume;
18557
- return await resumeSlackTurn({
18558
- messageText: "",
18559
- channelId: thread.channelId,
18560
- threadTs: thread.threadTs,
18561
- lockKey: payload.conversationId,
18562
- beforeStart: async () => {
18563
- const sessionRecord = await getAgentTurnSessionRecord(
18564
- payload.conversationId,
18565
- payload.sessionId
18566
- );
18567
- if (!sessionRecord || sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "timeout" && sessionRecord.resumeReason !== "yield" || sessionRecord.version !== payload.expectedVersion) {
18568
- return false;
18569
- }
18570
- const currentState = await getPersistedThreadState(
18571
- payload.conversationId
18572
- );
18573
- const conversation = coerceThreadConversationState(currentState);
18574
- const artifacts = coerceThreadArtifactsState(currentState);
18575
- const userMessage2 = getTurnUserMessage(conversation, payload.sessionId);
18576
- if (!userMessage2?.author?.userId) {
18577
- throw new Error(
18578
- `Unable to locate the persisted user message for timeout resume session "${payload.sessionId}"`
18579
- );
18580
- }
18581
- if (conversation.processing.activeTurnId !== payload.sessionId) {
18582
- return false;
18583
- }
18584
- const channelConfiguration = getChannelConfigurationServiceById(
18585
- thread.channelId
18586
- );
18587
- const conversationContext = buildConversationContext(conversation, {
18588
- excludeMessageId: userMessage2.id
18589
- });
18590
- const sandbox = getPersistedSandboxState(currentState);
18591
- const requester = await lookupSlackActorIdentity(
18592
- userMessage2.author.userId
18593
- );
18594
- return {
18595
- messageText: userMessage2.text,
18596
- messageTs: getTurnUserSlackMessageTs(userMessage2),
18597
- replyContext: {
18598
- credentialContext: {
18599
- actor: {
18600
- type: "user",
18601
- userId: userMessage2.author.userId
18602
- }
18603
- },
18604
- requester,
18605
- destination: payload.destination,
18606
- correlation: {
18607
- conversationId: payload.conversationId,
18608
- turnId: payload.sessionId,
18609
- channelId: thread.channelId,
18610
- threadTs: thread.threadTs,
18611
- requesterId: userMessage2.author.userId
18612
- },
18613
- toolChannelId: artifacts.assistantContextChannelId ?? thread.channelId,
18614
- artifactState: artifacts,
18615
- pendingAuth: conversation.processing.pendingAuth,
18616
- conversationContext,
18617
- channelConfiguration,
18618
- piMessages: conversation.piMessages,
18619
- sandbox,
18620
- onAuthPending: async (nextPendingAuth) => {
18621
- await applyPendingAuthUpdate({
18622
- conversation,
18623
- conversationId: payload.conversationId,
18624
- nextPendingAuth
18625
- });
18626
- await persistThreadStateById(payload.conversationId, {
18627
- conversation
18628
- });
18629
- },
18630
- ...getTurnUserReplyAttachmentContext(userMessage2)
18631
- },
18632
- onSuccess: async (reply) => {
18633
- await persistCompletedReplyState2({ sessionRecord, reply });
18634
- },
18635
- onFailure: async () => {
18636
- await persistFailedReplyState2(sessionRecord);
18637
- },
18638
- onPostDeliveryCommitFailure: async () => {
18639
- await failAgentTurnSessionRecord({
18640
- conversationId: sessionRecord.conversationId,
18641
- expectedVersion: sessionRecord.version,
18642
- sessionId: sessionRecord.sessionId,
18643
- errorMessage: "Timed-out turn reply was delivered but completion state did not persist"
18644
- });
18645
- },
18646
- onAuthPause: async () => {
18647
- await persistAuthPauseTurnState({
18648
- sessionId: payload.sessionId,
18649
- threadStateId: payload.conversationId
18650
- });
18651
- logWarn(
18652
- "timeout_resume_reparked_for_auth",
18653
- {},
18654
- {
18655
- "app.ai.conversation_id": payload.conversationId,
18656
- "app.ai.session_id": payload.sessionId
18657
- },
18658
- "Resumed timed-out turn parked for auth"
18659
- );
18660
- },
18661
- onTimeoutPause: async (error) => {
18662
- if (!isRetryableTurnError(error, "turn_timeout_resume")) {
18663
- throw error;
18664
- }
18665
- const version = error.metadata?.version;
18666
- if (typeof version !== "number") {
18667
- throw new Error(
18668
- "Timed-out resume turn did not include a turn-session version"
18669
- );
18670
- }
18671
- await scheduleTurnTimeoutResume2({
18672
- conversationId: payload.conversationId,
18673
- destination: payload.destination,
18674
- sessionId: payload.sessionId,
18675
- expectedVersion: version
18676
- });
18677
- }
18678
- };
18679
- }
18680
- });
18681
- }
18682
- async function resumeTimedOutTurnWithLockRetry(payload, options = {}) {
18683
- const scheduleTurnTimeoutResume2 = options.scheduleTurnTimeoutResume ?? scheduleTurnTimeoutResume;
18684
- for (const [attempt, delayMs] of [
18685
- ...TIMEOUT_RESUME_LOCK_RETRY_DELAYS_MS,
18686
- void 0
18687
- ].entries()) {
18688
- try {
18689
- return await resumeTimedOutTurn(payload, options);
18690
- } catch (error) {
18691
- if (!(error instanceof ResumeTurnBusyError)) {
18692
- throw error;
18693
- }
18694
- if (typeof delayMs !== "number") {
18695
- logWarn(
18696
- "timeout_resume_lock_busy",
18697
- {},
18698
- {
18699
- "app.ai.conversation_id": payload.conversationId,
18700
- "app.ai.session_id": payload.sessionId,
18701
- "app.ai.resume_lock_retry_count": attempt
18702
- },
18703
- "Rescheduling timeout resume because another turn still owns the thread lock"
18704
- );
18705
- await scheduleTurnTimeoutResume2(payload);
18706
- return true;
18707
- }
18708
- logWarn(
18709
- "timeout_resume_lock_busy_retrying",
18710
- {},
18711
- {
18712
- "app.ai.conversation_id": payload.conversationId,
18713
- "app.ai.session_id": payload.sessionId,
18714
- "app.ai.resume_lock_retry_attempt": attempt + 1,
18715
- "app.ai.resume_lock_retry_delay_ms": delayMs
18716
- },
18717
- "Timeout resume lock was busy; retrying"
18718
- );
18719
- await sleep4(delayMs);
18720
- }
18721
- }
18722
- return true;
18723
- }
18724
-
18725
- // src/handlers/turn-resume.ts
18726
- async function POST2(request, waitUntil, options = {}) {
18727
- const payload = await verifyTurnTimeoutResumeRequest(request);
18728
- if (!payload) {
18729
- return new Response("Unauthorized", { status: 401 });
18730
- }
18731
- waitUntil(
18732
- () => runWithTurnRequestDeadline(
18733
- () => resumeTimedOutTurnWithLockRetry(payload, options).catch((error) => {
18734
- logException(
18735
- error,
18736
- "timeout_resume_handler_failed",
18737
- {},
18738
- {
18739
- "app.ai.conversation_id": payload.conversationId,
18740
- "app.ai.session_id": payload.sessionId
18741
- },
18742
- "Timeout resume handler failed"
18743
- );
18744
- })
18745
- )
18746
- );
18747
- return new Response("Accepted", { status: 202 });
17601
+ }
17602
+ return new Response(upstream.body, {
17603
+ status: upstream.status,
17604
+ statusText: upstream.statusText,
17605
+ headers: responseHeaders(upstream)
17606
+ });
17607
+ }
17608
+
17609
+ // src/handlers/sandbox-egress-proxy.ts
17610
+ async function ALL(request, options = {}) {
17611
+ return await proxySandboxEgressRequest(request, {
17612
+ interceptHttp: options.interceptHttp
17613
+ });
17614
+ }
17615
+ function isSandboxEgressRequest(request) {
17616
+ return isSandboxEgressForwardedRequest(request);
18748
17617
  }
18749
17618
 
18750
17619
  // src/chat/services/subscribed-decision.ts
@@ -19098,44 +17967,46 @@ function combineTurnText(queuedMessages, latestText) {
19098
17967
 
19099
17968
  // src/chat/services/message-actor-identity.ts
19100
17969
  var messageActors = /* @__PURE__ */ new WeakMap();
19101
- function canonicalUserId(author, identity) {
17970
+ function canonicalUserId(author, requester) {
19102
17971
  const authorUserId = parseActorUserId(author.userId);
19103
- const identityUserId = parseActorUserId(identity.userId);
19104
- if (authorUserId && identityUserId && authorUserId !== identityUserId) {
19105
- throw new Error("Message actor identity user id mismatch");
17972
+ if (authorUserId && authorUserId !== requester.userId) {
17973
+ throw new Error("Message requester user id mismatch");
19106
17974
  }
19107
- const userId = authorUserId ?? identityUserId;
17975
+ const userId = authorUserId ?? requester.userId;
19108
17976
  if (!userId) {
19109
- throw new Error("Message actor identity requires a user id");
17977
+ throw new Error("Message requester requires a user id");
19110
17978
  }
19111
17979
  return userId;
19112
17980
  }
19113
- function actorIdentityFromAuthor(author) {
17981
+ function requesterFromAuthor(author) {
19114
17982
  const userId = parseActorUserId(author.userId);
19115
17983
  return userId ? { userId } : void 0;
19116
17984
  }
19117
- function applyIdentityToAuthor(author, identity) {
19118
- if (!isActorUserId(identity.userId)) {
19119
- throw new Error("Message actor identity requires a user id");
17985
+ function applyRequesterToAuthor(author, requester) {
17986
+ if (!isActorUserId(requester.userId)) {
17987
+ throw new Error("Message requester requires a user id");
19120
17988
  }
19121
- author.userId = identity.userId;
19122
- author.userName = identity.userName ?? "";
19123
- author.fullName = identity.fullName ?? "";
17989
+ author.userId = requester.userId;
17990
+ author.userName = requester.userName ?? "";
17991
+ author.fullName = requester.fullName ?? "";
19124
17992
  }
19125
- function bindMessageActorIdentity(message, identity) {
19126
- const userId = canonicalUserId(message.author, identity);
19127
- const actorIdentity = buildActorIdentity(identity, userId);
19128
- if (!actorIdentity?.userId) {
19129
- throw new Error("Message actor identity requires a user id");
17993
+ function bindMessageActorIdentity(message, requester) {
17994
+ const userId = canonicalUserId(message.author, requester);
17995
+ const actorRequester = createRequester(requester, {
17996
+ teamId: requester.teamId,
17997
+ userId
17998
+ });
17999
+ if (!actorRequester) {
18000
+ throw new Error("Message requester requires a user id");
19130
18001
  }
19131
- messageActors.set(message, actorIdentity);
19132
- applyIdentityToAuthor(message.author, actorIdentity);
19133
- return actorIdentity;
18002
+ messageActors.set(message, actorRequester);
18003
+ applyRequesterToAuthor(message.author, actorRequester);
18004
+ return actorRequester;
19134
18005
  }
19135
18006
  function getMessageActorIdentity(message) {
19136
- return messageActors.get(message) ?? actorIdentityFromAuthor(message.author);
18007
+ return messageActors.get(message) ?? requesterFromAuthor(message.author);
19137
18008
  }
19138
- async function ensureSlackMessageActorIdentity(message, lookupSlackUser2) {
18009
+ async function ensureSlackMessageActorIdentity(message, teamId, lookupSlackUser2) {
19139
18010
  const existing = messageActors.get(message);
19140
18011
  if (existing) {
19141
18012
  return existing;
@@ -19146,7 +18017,7 @@ async function ensureSlackMessageActorIdentity(message, lookupSlackUser2) {
19146
18017
  }
19147
18018
  return bindMessageActorIdentity(
19148
18019
  message,
19149
- slackActorIdentity(userId, await lookupSlackUser2(userId))
18020
+ createSlackRequester(teamId, userId, await lookupSlackUser2(teamId, userId))
19150
18021
  );
19151
18022
  }
19152
18023
 
@@ -20533,9 +19404,9 @@ function createJuniorRuntimeServices(overrides = {}) {
20533
19404
  replyExecutor: {
20534
19405
  contextCompactor: overrides.replyExecutor?.contextCompactor ?? contextCompactor,
20535
19406
  generateAssistantReply: overrides.replyExecutor?.generateAssistantReply ?? generateAssistantReply,
20536
- getAwaitingTurnContinuationRequest: overrides.replyExecutor?.getAwaitingTurnContinuationRequest ?? getAwaitingTurnContinuationRequest,
19407
+ getAwaitingAgentContinueRequest: overrides.replyExecutor?.getAwaitingAgentContinueRequest ?? getAwaitingAgentContinueRequest,
20537
19408
  lookupSlackUser: overrides.replyExecutor?.lookupSlackUser ?? lookupSlackUser,
20538
- scheduleTurnTimeoutResume: overrides.replyExecutor?.scheduleTurnTimeoutResume ?? scheduleTurnTimeoutResume,
19409
+ scheduleAgentContinue: overrides.replyExecutor?.scheduleAgentContinue ?? scheduleAgentContinue,
20539
19410
  generateThreadTitle: conversationMemory.generateThreadTitle
20540
19411
  },
20541
19412
  subscribedReplyPolicy: createSubscribedReplyPolicy({
@@ -20683,14 +19554,8 @@ function collectCanvasUrls(artifacts) {
20683
19554
  ].filter((url) => typeof url === "string" && url !== "")
20684
19555
  );
20685
19556
  }
20686
- function turnRequester(identity) {
20687
- const requester = {
20688
- ...identity.email ? { email: identity.email } : {},
20689
- ...identity.fullName ? { fullName: identity.fullName } : {},
20690
- ...identity.userId ? { slackUserId: identity.userId } : {},
20691
- ...identity.userName ? { slackUserName: identity.userName } : {}
20692
- };
20693
- return requester;
19557
+ function turnRequester(requester) {
19558
+ return toStoredSlackRequester(requester);
20694
19559
  }
20695
19560
  async function resolveChannelName(thread) {
20696
19561
  const existingName = thread.channel.name?.trim();
@@ -20804,12 +19669,14 @@ function createReplyToThread(deps) {
20804
19669
  (options.queuedMessages ?? []).map(
20805
19670
  (queued) => ensureSlackMessageActorIdentity(
20806
19671
  queued.message,
19672
+ teamId,
20807
19673
  deps.services.lookupSlackUser
20808
19674
  )
20809
19675
  )
20810
19676
  );
20811
19677
  const requesterIdentity = await ensureSlackMessageActorIdentity(
20812
19678
  message,
19679
+ teamId,
20813
19680
  deps.services.lookupSlackUser
20814
19681
  );
20815
19682
  const requester = turnRequester(requesterIdentity);
@@ -20878,24 +19745,24 @@ function createReplyToThread(deps) {
20878
19745
  return;
20879
19746
  }
20880
19747
  if (conversationId && activeTurnId) {
20881
- const resumeRequest = await deps.services.getAwaitingTurnContinuationRequest({
19748
+ const resumeRequest = await deps.services.getAwaitingAgentContinueRequest({
20882
19749
  conversationId,
20883
19750
  sessionId: activeTurnId
20884
19751
  });
20885
19752
  if (resumeRequest) {
20886
19753
  try {
20887
- await deps.services.scheduleTurnTimeoutResume(resumeRequest);
19754
+ await deps.services.scheduleAgentContinue(resumeRequest);
20888
19755
  } catch (error) {
20889
19756
  logException(
20890
19757
  error,
20891
- "agent_turn_continuation_retry_schedule_failed",
19758
+ "agent_continue_schedule_failed",
20892
19759
  turnTraceContext,
20893
19760
  {
20894
19761
  "app.ai.resume_session_version": resumeRequest.expectedVersion,
20895
19762
  "app.ai.resume_session_id": resumeRequest.sessionId,
20896
19763
  ...messageTs ? { "messaging.message.id": messageTs } : {}
20897
19764
  },
20898
- "Failed to reschedule active turn continuation"
19765
+ "Failed to reschedule active agent continuation"
20899
19766
  );
20900
19767
  throw error;
20901
19768
  }
@@ -20923,7 +19790,7 @@ function createReplyToThread(deps) {
20923
19790
  conversationId,
20924
19791
  expectedVersion: sessionRecord.version,
20925
19792
  sessionId: activeTurnId,
20926
- errorMessage: "Awaiting turn continuation metadata could not be materialized"
19793
+ errorMessage: "Awaiting agent continuation metadata could not be materialized"
20927
19794
  });
20928
19795
  markTurnFailed({
20929
19796
  conversation: preparedState.conversation,
@@ -21448,13 +20315,13 @@ function createReplyToThread(deps) {
21448
20315
  shouldPersistFailureState = false;
21449
20316
  return;
21450
20317
  }
21451
- if (isRetryableTurnError(error, "turn_timeout_resume")) {
20318
+ if (isRetryableTurnError(error, "agent_continue")) {
21452
20319
  const conversationIdForResume = error.metadata?.conversationId;
21453
20320
  const sessionIdForResume = error.metadata?.sessionId;
21454
20321
  const version = error.metadata?.version;
21455
20322
  if (conversationIdForResume && sessionIdForResume && typeof version === "number" && destination) {
21456
20323
  try {
21457
- await deps.services.scheduleTurnTimeoutResume({
20324
+ await deps.services.scheduleAgentContinue({
21458
20325
  conversationId: conversationIdForResume,
21459
20326
  destination,
21460
20327
  sessionId: sessionIdForResume,
@@ -21464,13 +20331,13 @@ function createReplyToThread(deps) {
21464
20331
  } catch (scheduleError) {
21465
20332
  logException(
21466
20333
  scheduleError,
21467
- "agent_turn_timeout_resume_schedule_failed",
20334
+ "agent_continue_schedule_failed",
21468
20335
  turnTraceContext,
21469
20336
  {
21470
20337
  ...messageTs ? { "messaging.message.id": messageTs } : {},
21471
20338
  "app.ai.resume_session_version": version
21472
20339
  },
21473
- "Failed to schedule timeout resume callback"
20340
+ "Failed to schedule agent continuation"
21474
20341
  );
21475
20342
  shouldPersistFailureState = true;
21476
20343
  throw scheduleError;
@@ -21478,10 +20345,10 @@ function createReplyToThread(deps) {
21478
20345
  return;
21479
20346
  } else {
21480
20347
  logWarn(
21481
- "agent_turn_timeout_resume_metadata_missing",
20348
+ "agent_continue_metadata_missing",
21482
20349
  turnTraceContext,
21483
20350
  messageTs ? { "messaging.message.id": messageTs } : {},
21484
- "Timed-out turn could not be scheduled for resume because retry metadata was incomplete"
20351
+ "Agent continuation could not be scheduled because retry metadata was incomplete"
21485
20352
  );
21486
20353
  }
21487
20354
  }
@@ -21840,6 +20707,11 @@ async function persistAssistantContextChannelId(args) {
21840
20707
  artifacts: nextArtifacts
21841
20708
  });
21842
20709
  }
20710
+ function clearSkippedTurnIfActive(conversation, messageId) {
20711
+ if (conversation.processing.activeTurnId === buildDeterministicTurnId(messageId)) {
20712
+ conversation.processing.activeTurnId = void 0;
20713
+ }
20714
+ }
21843
20715
  function createSlackRuntime(options) {
21844
20716
  const services = createJuniorRuntimeServices(options.services);
21845
20717
  const prepareTurnState = createPrepareTurnState({
@@ -21895,7 +20767,7 @@ function createSlackRuntime(options) {
21895
20767
  skippedReason: decision.reason
21896
20768
  }
21897
20769
  });
21898
- conversation.processing.activeTurnId = void 0;
20770
+ clearSkippedTurnIfActive(conversation, message.id);
21899
20771
  conversation.processing.lastCompletedAtMs = completedAtMs;
21900
20772
  updateConversationStats(conversation);
21901
20773
  await persistThreadState(thread, {
@@ -21904,6 +20776,7 @@ function createSlackRuntime(options) {
21904
20776
  },
21905
20777
  onSubscribedMessageSkipped: async ({
21906
20778
  thread,
20779
+ message,
21907
20780
  preparedState,
21908
20781
  decision,
21909
20782
  completedAtMs
@@ -21919,7 +20792,7 @@ function createSlackRuntime(options) {
21919
20792
  skippedReason: decision.reason
21920
20793
  }
21921
20794
  );
21922
- preparedState.conversation.processing.activeTurnId = void 0;
20795
+ clearSkippedTurnIfActive(preparedState.conversation, message.id);
21923
20796
  preparedState.conversation.processing.lastCompletedAtMs = completedAtMs;
21924
20797
  updateConversationStats(preparedState.conversation);
21925
20798
  await persistThreadState(thread, {
@@ -22014,7 +20887,7 @@ function rehydrateAttachmentFetchers(message, downloadPrivateSlackFile2 = downlo
22014
20887
 
22015
20888
  // src/chat/slack/adapter-context.ts
22016
20889
  var initializedAdapters = /* @__PURE__ */ new WeakSet();
22017
- async function getConnectedState2(stateAdapter) {
20890
+ async function getConnectedState(stateAdapter) {
22018
20891
  const state = stateAdapter ?? getStateAdapter();
22019
20892
  await state.connect();
22020
20893
  return state;
@@ -22023,7 +20896,7 @@ async function ensureSlackAdapterInitialized(args) {
22023
20896
  if (initializedAdapters.has(args.adapter)) {
22024
20897
  return;
22025
20898
  }
22026
- const state = await getConnectedState2(args.state);
20899
+ const state = await getConnectedState(args.state);
22027
20900
  await args.adapter.initialize({
22028
20901
  getState: () => state
22029
20902
  });
@@ -22086,7 +20959,7 @@ function requireSlackAuthorId(message) {
22086
20959
  }
22087
20960
  return authorId;
22088
20961
  }
22089
- function getConnectedState3(stateAdapter) {
20962
+ function getConnectedState2(stateAdapter) {
22090
20963
  return stateAdapter ?? getStateAdapter();
22091
20964
  }
22092
20965
  function isSlackMetadata(value) {
@@ -22118,10 +20991,14 @@ async function bindSlackActorIdentities(args) {
22118
20991
  }
22119
20992
  await Promise.all(
22120
20993
  [...byAuthorId].map(async ([authorId, messages]) => {
22121
- const profile = await args.lookupSlackUser(authorId);
20994
+ const profile = await args.lookupSlackUser(args.teamId, authorId);
22122
20995
  await Promise.all(
22123
20996
  messages.map(
22124
- (message) => ensureSlackMessageActorIdentity(message, async () => profile)
20997
+ (message) => ensureSlackMessageActorIdentity(
20998
+ message,
20999
+ args.teamId,
21000
+ async () => profile
21001
+ )
22125
21002
  )
22126
21003
  );
22127
21004
  })
@@ -22147,47 +21024,6 @@ function restoreThread(args) {
22147
21024
  isSubscribedContext: args.isSubscribedContext
22148
21025
  });
22149
21026
  }
22150
- function isContinuationResume(summary) {
22151
- return summary.state === "awaiting_resume" && (summary.resumeReason === "timeout" || summary.resumeReason === "yield");
22152
- }
22153
- async function failUnresumableContinuation(args) {
22154
- await failAgentTurnSessionRecord({
22155
- conversationId: args.conversationId,
22156
- expectedVersion: args.expectedVersion ?? args.summary.version,
22157
- sessionId: args.summary.sessionId,
22158
- errorMessage: args.errorMessage
22159
- });
22160
- }
22161
- async function resumeAwaitingContinuation(conversationId) {
22162
- const summaries = await listAgentTurnSessionSummariesForConversation(conversationId);
22163
- for (const summary of summaries) {
22164
- if (!isContinuationResume(summary)) {
22165
- continue;
22166
- }
22167
- const request = await getAwaitingTurnContinuationRequest({
22168
- conversationId,
22169
- sessionId: summary.sessionId
22170
- });
22171
- if (!request) {
22172
- await failUnresumableContinuation({
22173
- conversationId,
22174
- summary,
22175
- errorMessage: "Awaiting turn continuation metadata could not be materialized"
22176
- });
22177
- continue;
22178
- }
22179
- if (await resumeTimedOutTurnWithLockRetry(request)) {
22180
- return true;
22181
- }
22182
- await failUnresumableContinuation({
22183
- conversationId,
22184
- expectedVersion: request.expectedVersion,
22185
- summary,
22186
- errorMessage: "Awaiting turn continuation was stale before resuming"
22187
- });
22188
- }
22189
- return false;
22190
- }
22191
21027
  function getInstallation(records) {
22192
21028
  for (let index = records.length - 1; index >= 0; index -= 1) {
22193
21029
  const metadata = records[index]?.input.metadata;
@@ -22201,18 +21037,14 @@ function getPendingRecords(work) {
22201
21037
  if (!work) {
22202
21038
  return [];
22203
21039
  }
22204
- return work.messages.filter((message) => message.injectedAtMs === void 0).sort(compareInboundMessages);
21040
+ return work.execution.pendingMessages.sort(compareInboundMessages);
22205
21041
  }
22206
21042
  function createSlackConversationWorker(options) {
22207
21043
  return async (context) => {
22208
21044
  const adapter = options.getSlackAdapter();
22209
21045
  const actorLookup = options.lookupSlackUser ?? lookupSlackUser;
22210
- const state = getConnectedState3(options.state);
21046
+ const state = getConnectedState2(options.state);
22211
21047
  await state.connect();
22212
- const resumeContinuation = options.resumeAwaitingContinuation ?? resumeAwaitingContinuation;
22213
- if (await resumeContinuation(context.conversationId)) {
22214
- return { status: "completed" };
22215
- }
22216
21048
  const records = getPendingRecords(
22217
21049
  await getConversationWorkState({
22218
21050
  conversationId: context.conversationId,
@@ -22220,7 +21052,7 @@ function createSlackConversationWorker(options) {
22220
21052
  })
22221
21053
  );
22222
21054
  if (records.length === 0) {
22223
- await resumeContinuation(context.conversationId);
21055
+ await options.resumeAwaitingContinuation(context.conversationId);
22224
21056
  return { status: "completed" };
22225
21057
  }
22226
21058
  const latestRecord = records[records.length - 1];
@@ -22246,7 +21078,8 @@ function createSlackConversationWorker(options) {
22246
21078
  );
22247
21079
  await bindSlackActorIdentities({
22248
21080
  lookupSlackUser: actorLookup,
22249
- messages
21081
+ messages,
21082
+ teamId: context.destination.teamId
22250
21083
  });
22251
21084
  const latestMessage = messages[messages.length - 1];
22252
21085
  if (!latestMessage) {
@@ -22295,80 +21128,404 @@ function createSlackConversationWorker(options) {
22295
21128
  const messages2 = pendingRecords.map(
22296
21129
  (record) => restoreMessage({ adapter, record })
22297
21130
  );
22298
- restoredMessages = messages2;
22299
- await inject(messages2);
22300
- });
22301
- return restoredMessages ?? drained.map((record) => restoreMessage({ adapter, record }));
22302
- };
22303
- try {
22304
- if (route === "mention") {
22305
- await options.runtime.handleNewMention(thread, latestMessage, {
22306
- destination: context.destination,
22307
- messageContext,
22308
- drainSteeringMessages,
22309
- onInputCommitted,
22310
- shouldYield: context.shouldYield
21131
+ restoredMessages = messages2;
21132
+ await inject(messages2);
21133
+ });
21134
+ return restoredMessages ?? drained.map((record) => restoreMessage({ adapter, record }));
21135
+ };
21136
+ try {
21137
+ if (route === "mention") {
21138
+ await options.runtime.handleNewMention(thread, latestMessage, {
21139
+ destination: context.destination,
21140
+ messageContext,
21141
+ drainSteeringMessages,
21142
+ onInputCommitted,
21143
+ shouldYield: context.shouldYield
21144
+ });
21145
+ return;
21146
+ }
21147
+ await options.runtime.handleSubscribedMessage(thread, latestMessage, {
21148
+ destination: context.destination,
21149
+ messageContext,
21150
+ drainSteeringMessages,
21151
+ onInputCommitted,
21152
+ shouldYield: context.shouldYield
21153
+ });
21154
+ } catch (error) {
21155
+ if (isCooperativeTurnYieldError(error)) {
21156
+ return { status: "yielded" };
21157
+ }
21158
+ if (isTurnInputCommitLostError(error)) {
21159
+ return { status: "lost_lease" };
21160
+ }
21161
+ throw error;
21162
+ }
21163
+ }
21164
+ });
21165
+ if (turnResult?.status === "yielded" || turnResult?.status === "lost_lease") {
21166
+ return turnResult;
21167
+ }
21168
+ return { status: "completed" };
21169
+ };
21170
+ }
21171
+ function buildSlackInboundMessage(args) {
21172
+ const authorId = requireSlackAuthorId(args.message);
21173
+ const destination = createSlackDestination({
21174
+ channelId: args.thread.channelId,
21175
+ teamId: args.installation?.teamId
21176
+ });
21177
+ if (!destination) {
21178
+ throw new Error("Slack inbound message requires destination context");
21179
+ }
21180
+ return {
21181
+ conversationId: args.conversationId,
21182
+ destination,
21183
+ inboundMessageId: [
21184
+ "slack",
21185
+ args.installation?.teamId ?? args.installation?.enterpriseId ?? "unknown",
21186
+ args.conversationId,
21187
+ args.message.id
21188
+ ].join(":"),
21189
+ source: "slack",
21190
+ createdAtMs: args.message.metadata.dateSent.getTime(),
21191
+ receivedAtMs: args.receivedAtMs,
21192
+ input: {
21193
+ text: args.message.text || " ",
21194
+ authorId,
21195
+ attachments: args.message.attachments,
21196
+ metadata: {
21197
+ platform: "slack",
21198
+ route: args.route,
21199
+ installation: args.installation,
21200
+ thread: args.thread.toJSON(),
21201
+ message: args.message.toJSON()
21202
+ }
21203
+ }
21204
+ };
21205
+ }
21206
+
21207
+ // src/chat/runtime/agent-continue-runner.ts
21208
+ var AGENT_CONTINUE_LOCK_RETRY_DELAYS_MS = [250, 1e3, 2e3];
21209
+ function sleep3(ms) {
21210
+ return new Promise((resolve) => setTimeout(resolve, ms));
21211
+ }
21212
+ async function persistCompletedReplyState2(args) {
21213
+ const currentState = await getPersistedThreadState(
21214
+ args.sessionRecord.conversationId
21215
+ );
21216
+ const conversation = coerceThreadConversationState(currentState);
21217
+ const artifacts = coerceThreadArtifactsState(currentState);
21218
+ const userMessage2 = getTurnUserMessage(
21219
+ conversation,
21220
+ args.sessionRecord.sessionId
21221
+ );
21222
+ const statePatch = buildDeliveredTurnStatePatch({
21223
+ artifacts,
21224
+ conversation,
21225
+ reply: args.reply,
21226
+ sessionId: args.sessionRecord.sessionId,
21227
+ userMessageId: userMessage2?.id
21228
+ });
21229
+ await persistThreadStateById(args.sessionRecord.conversationId, {
21230
+ ...statePatch
21231
+ });
21232
+ }
21233
+ async function failSessionRecordBestEffort3(args) {
21234
+ try {
21235
+ await failAgentTurnSessionRecord({
21236
+ conversationId: args.sessionRecord.conversationId,
21237
+ expectedVersion: args.sessionRecord.version,
21238
+ sessionId: args.sessionRecord.sessionId,
21239
+ errorMessage: args.errorMessage
21240
+ });
21241
+ } catch (error) {
21242
+ logException(
21243
+ error,
21244
+ "agent_continue_session_record_fail_persist_failed",
21245
+ {},
21246
+ {
21247
+ "app.ai.conversation_id": args.sessionRecord.conversationId,
21248
+ "app.ai.session_id": args.sessionRecord.sessionId
21249
+ },
21250
+ "Failed to mark paused agent run session record failed"
21251
+ );
21252
+ }
21253
+ }
21254
+ async function persistFailedReplyState2(sessionRecord) {
21255
+ const currentState = await getPersistedThreadState(
21256
+ sessionRecord.conversationId
21257
+ );
21258
+ const conversation = coerceThreadConversationState(currentState);
21259
+ clearPendingAuth(conversation, sessionRecord.sessionId);
21260
+ markTurnFailed({
21261
+ conversation,
21262
+ nowMs: Date.now(),
21263
+ sessionId: sessionRecord.sessionId,
21264
+ userMessageId: getTurnUserMessage(conversation, sessionRecord.sessionId)?.id,
21265
+ markConversationMessage,
21266
+ updateConversationStats
21267
+ });
21268
+ await failSessionRecordBestEffort3({
21269
+ sessionRecord,
21270
+ errorMessage: "Paused agent run failed while continuing"
21271
+ });
21272
+ await persistThreadStateById(sessionRecord.conversationId, {
21273
+ conversation
21274
+ });
21275
+ }
21276
+ async function failContinuationStartup(args) {
21277
+ try {
21278
+ await persistFailedReplyState2(args.sessionRecord);
21279
+ } catch (persistError) {
21280
+ await failSessionRecordBestEffort3({
21281
+ sessionRecord: args.sessionRecord,
21282
+ errorMessage: "Paused agent run failed while preparing continuation"
21283
+ });
21284
+ logException(
21285
+ persistError,
21286
+ "agent_continue_startup_failure_persist_failed",
21287
+ {},
21288
+ {
21289
+ "app.ai.conversation_id": args.sessionRecord.conversationId,
21290
+ "app.ai.session_id": args.sessionRecord.sessionId
21291
+ },
21292
+ "Failed to persist paused agent run startup failure"
21293
+ );
21294
+ }
21295
+ }
21296
+ function isContinuationResume(summary) {
21297
+ return summary.state === "awaiting_resume" && (summary.resumeReason === "timeout" || summary.resumeReason === "yield");
21298
+ }
21299
+ async function failUnresumableContinuation(args) {
21300
+ await failAgentTurnSessionRecord({
21301
+ conversationId: args.conversationId,
21302
+ expectedVersion: args.expectedVersion ?? args.summary.version,
21303
+ sessionId: args.summary.sessionId,
21304
+ errorMessage: args.errorMessage
21305
+ });
21306
+ }
21307
+ async function continueSlackAgentRun(payload, options = {}) {
21308
+ const thread = parseSlackThreadId(payload.conversationId);
21309
+ if (!thread) {
21310
+ throw new Error(
21311
+ `Agent continuation requires a Slack thread conversation id, got "${payload.conversationId}"`
21312
+ );
21313
+ }
21314
+ const scheduleAgentContinue2 = options.scheduleAgentContinue ?? scheduleAgentContinue;
21315
+ const resumeTurn = options.resumeTurn ?? resumeSlackTurn;
21316
+ return await resumeTurn({
21317
+ messageText: "",
21318
+ channelId: thread.channelId,
21319
+ threadTs: thread.threadTs,
21320
+ lockKey: payload.conversationId,
21321
+ generateReply: options.generateReply,
21322
+ beforeStart: async () => {
21323
+ let sessionRecord;
21324
+ try {
21325
+ sessionRecord = await getAgentTurnSessionRecord(
21326
+ payload.conversationId,
21327
+ payload.sessionId
21328
+ );
21329
+ if (!sessionRecord || sessionRecord.state !== "awaiting_resume" || sessionRecord.resumeReason !== "timeout" && sessionRecord.resumeReason !== "yield" || sessionRecord.version !== payload.expectedVersion) {
21330
+ return false;
21331
+ }
21332
+ const activeSessionRecord = sessionRecord;
21333
+ const currentState = await getPersistedThreadState(
21334
+ payload.conversationId
21335
+ );
21336
+ const conversation = coerceThreadConversationState(currentState);
21337
+ const artifacts = coerceThreadArtifactsState(currentState);
21338
+ const userMessage2 = getTurnUserMessage(conversation, payload.sessionId);
21339
+ if (!userMessage2?.author?.userId) {
21340
+ throw new Error(
21341
+ `Unable to locate the persisted user message for agent continuation session "${payload.sessionId}"`
21342
+ );
21343
+ }
21344
+ if (conversation.processing.activeTurnId !== payload.sessionId) {
21345
+ return false;
21346
+ }
21347
+ const channelConfiguration = getChannelConfigurationServiceById(
21348
+ thread.channelId
21349
+ );
21350
+ const conversationContext = buildConversationContext(conversation, {
21351
+ excludeMessageId: userMessage2.id
21352
+ });
21353
+ const sandbox = getPersistedSandboxState(currentState);
21354
+ const requester = createRequesterFromStoredSlackRequester({
21355
+ requester: activeSessionRecord.requester,
21356
+ teamId: payload.destination.teamId,
21357
+ userId: userMessage2.author.userId
21358
+ });
21359
+ return {
21360
+ messageText: userMessage2.text,
21361
+ messageTs: getTurnUserSlackMessageTs(userMessage2),
21362
+ replyContext: {
21363
+ credentialContext: {
21364
+ actor: {
21365
+ type: "user",
21366
+ userId: requester.userId
21367
+ }
21368
+ },
21369
+ requester,
21370
+ destination: payload.destination,
21371
+ correlation: {
21372
+ conversationId: payload.conversationId,
21373
+ turnId: payload.sessionId,
21374
+ channelId: thread.channelId,
21375
+ threadTs: thread.threadTs,
21376
+ requesterId: requester.userId
21377
+ },
21378
+ toolChannelId: artifacts.assistantContextChannelId ?? thread.channelId,
21379
+ artifactState: artifacts,
21380
+ pendingAuth: conversation.processing.pendingAuth,
21381
+ conversationContext,
21382
+ channelConfiguration,
21383
+ piMessages: conversation.piMessages,
21384
+ sandbox,
21385
+ onAuthPending: async (nextPendingAuth) => {
21386
+ await applyPendingAuthUpdate({
21387
+ conversation,
21388
+ conversationId: payload.conversationId,
21389
+ nextPendingAuth
21390
+ });
21391
+ await persistThreadStateById(payload.conversationId, {
21392
+ conversation
21393
+ });
21394
+ },
21395
+ ...getTurnUserReplyAttachmentContext(userMessage2)
21396
+ },
21397
+ onSuccess: async (reply) => {
21398
+ await persistCompletedReplyState2({
21399
+ sessionRecord: activeSessionRecord,
21400
+ reply
21401
+ });
21402
+ },
21403
+ onFailure: async () => {
21404
+ await persistFailedReplyState2(activeSessionRecord);
21405
+ },
21406
+ onPostDeliveryCommitFailure: async () => {
21407
+ await failAgentTurnSessionRecord({
21408
+ conversationId: activeSessionRecord.conversationId,
21409
+ expectedVersion: activeSessionRecord.version,
21410
+ sessionId: activeSessionRecord.sessionId,
21411
+ errorMessage: "Continued agent reply was delivered but completion state did not persist"
21412
+ });
21413
+ },
21414
+ onAuthPause: async () => {
21415
+ await persistAuthPauseTurnState({
21416
+ sessionId: payload.sessionId,
21417
+ threadStateId: payload.conversationId
21418
+ });
21419
+ logWarn(
21420
+ "agent_continue_reparked_for_auth",
21421
+ {},
21422
+ {
21423
+ "app.ai.conversation_id": payload.conversationId,
21424
+ "app.ai.session_id": payload.sessionId
21425
+ },
21426
+ "Continued agent run parked for auth"
21427
+ );
21428
+ },
21429
+ onTimeoutPause: async (error) => {
21430
+ if (!isRetryableTurnError(error, "agent_continue")) {
21431
+ throw error;
21432
+ }
21433
+ const version = error.metadata?.version;
21434
+ if (typeof version !== "number") {
21435
+ throw new Error(
21436
+ "Agent continuation did not include a session record version"
21437
+ );
21438
+ }
21439
+ await scheduleAgentContinue2({
21440
+ conversationId: payload.conversationId,
21441
+ destination: payload.destination,
21442
+ sessionId: payload.sessionId,
21443
+ expectedVersion: version
22311
21444
  });
22312
- return;
22313
21445
  }
22314
- await options.runtime.handleSubscribedMessage(thread, latestMessage, {
22315
- destination: context.destination,
22316
- messageContext,
22317
- drainSteeringMessages,
22318
- onInputCommitted,
22319
- shouldYield: context.shouldYield
21446
+ };
21447
+ } catch (error) {
21448
+ if (sessionRecord) {
21449
+ await failContinuationStartup({
21450
+ sessionRecord
22320
21451
  });
22321
- } catch (error) {
22322
- if (isCooperativeTurnYieldError(error)) {
22323
- return { status: "yielded" };
22324
- }
22325
- if (isTurnInputCommitLostError(error)) {
22326
- return { status: "lost_lease" };
22327
- }
22328
- throw error;
22329
21452
  }
21453
+ throw error;
22330
21454
  }
22331
- });
22332
- if (turnResult?.status === "yielded" || turnResult?.status === "lost_lease") {
22333
- return turnResult;
22334
21455
  }
22335
- return { status: "completed" };
22336
- };
22337
- }
22338
- function buildSlackInboundMessage(args) {
22339
- const authorId = requireSlackAuthorId(args.message);
22340
- const destination = createSlackDestination({
22341
- channelId: args.thread.channelId,
22342
- teamId: args.installation?.teamId
22343
21456
  });
22344
- if (!destination) {
22345
- throw new Error("Slack inbound message requires destination context");
21457
+ }
21458
+ async function resumeAwaitingSlackContinuation(conversationId) {
21459
+ const summaries = await listAgentTurnSessionSummariesForConversation(conversationId);
21460
+ for (const summary of summaries) {
21461
+ if (!isContinuationResume(summary)) {
21462
+ continue;
21463
+ }
21464
+ const request = await getAwaitingAgentContinueRequest({
21465
+ conversationId,
21466
+ sessionId: summary.sessionId
21467
+ });
21468
+ if (!request) {
21469
+ await failUnresumableContinuation({
21470
+ conversationId,
21471
+ summary,
21472
+ errorMessage: "Awaiting agent continuation metadata could not be materialized"
21473
+ });
21474
+ continue;
21475
+ }
21476
+ if (await continueSlackAgentRunWithLockRetry(request)) {
21477
+ return true;
21478
+ }
21479
+ await failUnresumableContinuation({
21480
+ conversationId,
21481
+ expectedVersion: request.expectedVersion,
21482
+ summary,
21483
+ errorMessage: "Awaiting agent continuation was stale before it could run"
21484
+ });
22346
21485
  }
22347
- return {
22348
- conversationId: args.conversationId,
22349
- destination,
22350
- inboundMessageId: [
22351
- "slack",
22352
- args.installation?.teamId ?? args.installation?.enterpriseId ?? "unknown",
22353
- args.conversationId,
22354
- args.message.id
22355
- ].join(":"),
22356
- source: "slack",
22357
- createdAtMs: args.message.metadata.dateSent.getTime(),
22358
- receivedAtMs: args.receivedAtMs,
22359
- input: {
22360
- text: args.message.text || " ",
22361
- authorId,
22362
- attachments: args.message.attachments,
22363
- metadata: {
22364
- platform: "slack",
22365
- route: args.route,
22366
- installation: args.installation,
22367
- thread: args.thread.toJSON(),
22368
- message: args.message.toJSON()
21486
+ return false;
21487
+ }
21488
+ async function continueSlackAgentRunWithLockRetry(payload, options = {}) {
21489
+ const scheduleAgentContinue2 = options.scheduleAgentContinue ?? scheduleAgentContinue;
21490
+ for (const [attempt, delayMs] of [
21491
+ ...AGENT_CONTINUE_LOCK_RETRY_DELAYS_MS,
21492
+ void 0
21493
+ ].entries()) {
21494
+ try {
21495
+ return await continueSlackAgentRun(payload, options);
21496
+ } catch (error) {
21497
+ if (!(error instanceof ResumeTurnBusyError)) {
21498
+ throw error;
21499
+ }
21500
+ if (typeof delayMs !== "number") {
21501
+ logWarn(
21502
+ "agent_continue_lock_busy",
21503
+ {},
21504
+ {
21505
+ "app.ai.conversation_id": payload.conversationId,
21506
+ "app.ai.session_id": payload.sessionId,
21507
+ "app.ai.resume_lock_retry_count": attempt
21508
+ },
21509
+ "Rescheduling agent continuation because another run still owns the thread lock"
21510
+ );
21511
+ await scheduleAgentContinue2(payload);
21512
+ return true;
22369
21513
  }
21514
+ logWarn(
21515
+ "agent_continue_lock_busy_retrying",
21516
+ {},
21517
+ {
21518
+ "app.ai.conversation_id": payload.conversationId,
21519
+ "app.ai.session_id": payload.sessionId,
21520
+ "app.ai.resume_lock_retry_attempt": attempt + 1,
21521
+ "app.ai.resume_lock_retry_delay_ms": delayMs
21522
+ },
21523
+ "Agent continuation lock was busy; retrying"
21524
+ );
21525
+ await sleep3(delayMs);
22370
21526
  }
22371
- };
21527
+ }
21528
+ return true;
22372
21529
  }
22373
21530
 
22374
21531
  // src/chat/app/production.ts
@@ -22414,6 +21571,7 @@ function getProductionConversationWorkOptions() {
22414
21571
  queue: getVercelConversationWorkQueue(),
22415
21572
  run: createSlackConversationWorker({
22416
21573
  getSlackAdapter: getProductionSlackAdapter,
21574
+ resumeAwaitingContinuation: resumeAwaitingSlackContinuation,
22417
21575
  runtime
22418
21576
  })
22419
21577
  };
@@ -22883,13 +22041,16 @@ async function handleSlashCommandForm(args) {
22883
22041
  args.params.get("user_id"),
22884
22042
  "Slack slash command payload"
22885
22043
  );
22886
- const userIdentity = buildActorIdentity(
22044
+ const teamId = args.params.get("team_id") ?? void 0;
22045
+ const userIdentity = createRequester(
22887
22046
  {
22047
+ platform: "slack",
22048
+ teamId,
22888
22049
  userId,
22889
22050
  userName: args.params.get("user_name") ?? void 0,
22890
22051
  fullName: args.params.get("user_name") ?? void 0
22891
22052
  },
22892
- userId
22053
+ { teamId, userId }
22893
22054
  );
22894
22055
  if (!userIdentity?.userId) {
22895
22056
  throw new Error("Slack slash command payload actor identity is invalid");
@@ -23248,7 +22409,7 @@ async function handlePlatformWebhook(request, platform, waitUntil, legacyBot) {
23248
22409
  }
23249
22410
  });
23250
22411
  }
23251
- async function POST3(request, platform, waitUntil) {
22412
+ async function POST2(request, platform, waitUntil) {
23252
22413
  return handlePlatformWebhook(request, platform, waitUntil);
23253
22414
  }
23254
22415
 
@@ -23277,7 +22438,7 @@ function isConversationQueueMessageRejectedError(error) {
23277
22438
  // src/chat/task-execution/worker.ts
23278
22439
  var CONVERSATION_WORK_DEFER_DELAY_MS = 15e3;
23279
22440
  var CONVERSATION_WORK_SOFT_YIELD_AFTER_MS = 24e4;
23280
- function now2(options) {
22441
+ function now(options) {
23281
22442
  return options.nowMs?.() ?? Date.now();
23282
22443
  }
23283
22444
  function nudgeIdempotencyKey(reason, conversationId, nowMs) {
@@ -23294,21 +22455,11 @@ async function sendWakeNudge(args) {
23294
22455
  idempotencyKey: args.idempotencyKey
23295
22456
  }
23296
22457
  );
23297
- try {
23298
- await markConversationWorkEnqueued({
23299
- conversationId: args.conversationId,
23300
- nowMs: args.nowMs,
23301
- state: args.options.state
23302
- });
23303
- } catch (error) {
23304
- logException(
23305
- error,
23306
- "conversation_work_enqueue_marker_failed",
23307
- { conversationId: args.conversationId },
23308
- {},
23309
- "Conversation work enqueue marker failed after queue acceptance"
23310
- );
23311
- }
22458
+ await markConversationWorkEnqueued({
22459
+ conversationId: args.conversationId,
22460
+ nowMs: args.nowMs,
22461
+ state: args.options.state
22462
+ });
23312
22463
  }
23313
22464
  async function requestLostLeaseRecovery(args) {
23314
22465
  const continuationMarked = await requestConversationContinuation({
@@ -23344,7 +22495,7 @@ async function requestLostLeaseRecovery(args) {
23344
22495
  }
23345
22496
  function startLeaseCheckIn(args) {
23346
22497
  const timer = setInterval(() => {
23347
- const nowMs = now2(args.options);
22498
+ const nowMs = now(args.options);
23348
22499
  void checkInConversationWork({
23349
22500
  conversationId: args.conversationId,
23350
22501
  leaseToken: args.leaseToken,
@@ -23382,10 +22533,10 @@ async function processConversationWork(message, options) {
23382
22533
  conversationId,
23383
22534
  state: options.state
23384
22535
  });
23385
- if (!initial || countPendingConversationMessages(initial) === 0 && !initial.needsRun && !initial.lease) {
22536
+ if (!initial || countPendingConversationMessages(initial) === 0 && initial.execution.status === "idle" && !initial.execution.lease) {
23386
22537
  return { status: "no_work" };
23387
22538
  }
23388
- if (!sameDestination(initial.destination, message.destination)) {
22539
+ if (!initial.destination || !sameDestination(initial.destination, message.destination)) {
23389
22540
  throw new ConversationQueueMessageRejectedError(
23390
22541
  "destination_mismatch",
23391
22542
  `Conversation work queue destination changed for ${conversationId}`,
@@ -23393,50 +22544,24 @@ async function processConversationWork(message, options) {
23393
22544
  );
23394
22545
  }
23395
22546
  const destination = initial.destination;
23396
- let lease;
23397
- try {
23398
- lease = await startConversationWork({
23399
- conversationId,
23400
- nowMs: now2(options),
23401
- state: options.state
23402
- });
23403
- } catch (error) {
23404
- logException(
23405
- error,
23406
- "conversation_work_lease_acquire_failed",
23407
- { conversationId },
23408
- {},
23409
- "Conversation work lease acquisition failed; heartbeat will recover"
23410
- );
23411
- return { status: "no_work" };
23412
- }
22547
+ const lease = await startConversationWork({
22548
+ conversationId,
22549
+ nowMs: now(options),
22550
+ state: options.state
22551
+ });
23413
22552
  if (lease.status === "no_work") {
23414
22553
  return { status: "no_work" };
23415
22554
  }
23416
22555
  if (lease.status === "active") {
23417
- const nudgeNowMs = now2(options);
23418
- try {
23419
- await sendWakeNudge({
23420
- conversationId,
23421
- destination,
23422
- delayMs: CONVERSATION_WORK_DEFER_DELAY_MS,
23423
- idempotencyKey: nudgeIdempotencyKey(
23424
- "active",
23425
- conversationId,
23426
- nudgeNowMs
23427
- ),
23428
- nowMs: nudgeNowMs,
23429
- options
23430
- });
23431
- } catch (error) {
23432
- logException(
23433
- error,
23434
- "conversation_work_active_nudge_failed",
23435
- { conversationId },
23436
- {},
23437
- "Conversation work active-lease nudge failed; heartbeat will recover"
23438
- );
23439
- }
22556
+ const nudgeNowMs = now(options);
22557
+ await sendWakeNudge({
22558
+ conversationId,
22559
+ destination,
22560
+ delayMs: CONVERSATION_WORK_DEFER_DELAY_MS,
22561
+ idempotencyKey: nudgeIdempotencyKey("active", conversationId, nudgeNowMs),
22562
+ nowMs: nudgeNowMs,
22563
+ options
22564
+ });
23440
22565
  logInfo(
23441
22566
  "conversation_work_nudge_deferred_for_active_lease",
23442
22567
  { conversationId },
@@ -23447,7 +22572,7 @@ async function processConversationWork(message, options) {
23447
22572
  );
23448
22573
  return { status: "active" };
23449
22574
  }
23450
- const startedAtMs = now2(options);
22575
+ const startedAtMs = now(options);
23451
22576
  const softYieldDeadlineMs = startedAtMs + (options.softYieldAfterMs ?? CONVERSATION_WORK_SOFT_YIELD_AFTER_MS);
23452
22577
  let leaseLost = false;
23453
22578
  const markLeaseLost = () => {
@@ -23472,12 +22597,12 @@ async function processConversationWork(message, options) {
23472
22597
  conversationId,
23473
22598
  destination,
23474
22599
  leaseToken: lease.leaseToken,
23475
- shouldYield: () => leaseLost || now2(options) >= softYieldDeadlineMs,
22600
+ shouldYield: () => leaseLost || now(options) >= softYieldDeadlineMs,
23476
22601
  checkIn: async () => {
23477
22602
  const checkedIn = await checkInConversationWork({
23478
22603
  conversationId,
23479
22604
  leaseToken: lease.leaseToken,
23480
- nowMs: now2(options),
22605
+ nowMs: now(options),
23481
22606
  state: options.state
23482
22607
  });
23483
22608
  if (!checkedIn) {
@@ -23489,7 +22614,7 @@ async function processConversationWork(message, options) {
23489
22614
  conversationId,
23490
22615
  leaseToken: lease.leaseToken,
23491
22616
  inject,
23492
- nowMs: now2(options),
22617
+ nowMs: now(options),
23493
22618
  state: options.state
23494
22619
  })
23495
22620
  };
@@ -23500,7 +22625,7 @@ async function processConversationWork(message, options) {
23500
22625
  conversationId,
23501
22626
  destination,
23502
22627
  leaseToken: lease.leaseToken,
23503
- nowMs: now2(options),
22628
+ nowMs: now(options),
23504
22629
  options
23505
22630
  });
23506
22631
  return { status: "lost_lease" };
@@ -23510,13 +22635,13 @@ async function processConversationWork(message, options) {
23510
22635
  conversationId,
23511
22636
  destination,
23512
22637
  leaseToken: lease.leaseToken,
23513
- nowMs: now2(options),
22638
+ nowMs: now(options),
23514
22639
  options
23515
22640
  });
23516
22641
  return { status: "lost_lease" };
23517
22642
  }
23518
22643
  if (result.status === "yielded") {
23519
- const yieldNowMs = now2(options);
22644
+ const yieldNowMs = now(options);
23520
22645
  const continuationMarked = await requestConversationContinuation({
23521
22646
  conversationId,
23522
22647
  destination,
@@ -23548,7 +22673,7 @@ async function processConversationWork(message, options) {
23548
22673
  "conversation_work_cooperative_yield",
23549
22674
  { conversationId },
23550
22675
  {
23551
- "app.worker.elapsed_ms": now2(options) - startedAtMs,
22676
+ "app.worker.elapsed_ms": now(options) - startedAtMs,
23552
22677
  "app.worker.soft_yield_deadline_ms": softYieldDeadlineMs
23553
22678
  },
23554
22679
  "Conversation work yielded cooperatively"
@@ -23558,14 +22683,14 @@ async function processConversationWork(message, options) {
23558
22683
  const completion = await completeConversationWork({
23559
22684
  conversationId,
23560
22685
  leaseToken: lease.leaseToken,
23561
- nowMs: now2(options),
22686
+ nowMs: now(options),
23562
22687
  state: options.state
23563
22688
  });
23564
22689
  if (completion === "lost_lease") {
23565
22690
  return { status: "lost_lease" };
23566
22691
  }
23567
22692
  if (completion === "pending") {
23568
- const nudgeNowMs = now2(options);
22693
+ const nudgeNowMs = now(options);
23569
22694
  await sendWakeNudge({
23570
22695
  conversationId,
23571
22696
  destination,
@@ -23583,104 +22708,42 @@ async function processConversationWork(message, options) {
23583
22708
  "conversation_work_completed",
23584
22709
  { conversationId },
23585
22710
  {
23586
- "app.worker.elapsed_ms": now2(options) - startedAtMs
22711
+ "app.worker.elapsed_ms": now(options) - startedAtMs
23587
22712
  },
23588
22713
  "Conversation work completed"
23589
22714
  );
23590
22715
  return { status: "completed" };
23591
22716
  } catch (error) {
23592
- const errorNowMs = now2(options);
23593
- let failure2;
22717
+ const errorNowMs = now(options);
23594
22718
  try {
23595
- failure2 = await recordConversationWorkFailure({
22719
+ const continuationMarked = await requestConversationContinuation({
23596
22720
  conversationId,
22721
+ destination,
22722
+ leaseToken: lease.leaseToken,
23597
22723
  nowMs: errorNowMs,
23598
22724
  state: options.state
23599
22725
  });
23600
- } catch (recordError) {
23601
- logException(
23602
- recordError,
23603
- "conversation_work_failure_record_failed",
23604
- { conversationId },
23605
- {},
23606
- "Conversation work failure counter update failed"
23607
- );
23608
- }
23609
- if (!isProviderRetryError(error)) {
23610
- logException(
23611
- error,
23612
- "conversation_work_failed",
23613
- { conversationId },
23614
- {
23615
- "app.worker.consecutive_failure_count": failure2?.consecutiveFailureCount ?? null,
23616
- "app.worker.elapsed_ms": now2(options) - startedAtMs
23617
- },
23618
- "Conversation work failed"
23619
- );
23620
- }
23621
- if (failure2?.abandoned) {
23622
- logWarn(
23623
- "conversation_work_abandoned",
23624
- { conversationId },
23625
- {
23626
- "app.worker.consecutive_failure_count": failure2.consecutiveFailureCount,
23627
- "app.worker.max_consecutive_failures": CONVERSATION_WORK_MAX_CONSECUTIVE_FAILURES
23628
- },
23629
- "Conversation work abandoned after repeated failures; stopping retries"
23630
- );
23631
- if (!failure2.releasedLease) {
23632
- try {
23633
- await releaseConversationWork({
23634
- conversationId,
23635
- leaseToken: lease.leaseToken,
23636
- nowMs: errorNowMs,
23637
- state: options.state
23638
- });
23639
- } catch (releaseError) {
23640
- logException(
23641
- releaseError,
23642
- "conversation_work_release_failed",
23643
- { conversationId },
23644
- {},
23645
- "Conversation work release failed after abandoning"
23646
- );
23647
- }
23648
- }
23649
- return { status: "abandoned" };
23650
- }
23651
- let requeueSucceeded = false;
23652
- if (failure2) {
23653
- try {
23654
- const continuationMarked = await requestConversationContinuation({
22726
+ if (continuationMarked) {
22727
+ await sendWakeNudge({
23655
22728
  conversationId,
23656
22729
  destination,
23657
- leaseToken: lease.leaseToken,
22730
+ idempotencyKey: nudgeIdempotencyKey(
22731
+ "error",
22732
+ conversationId,
22733
+ errorNowMs
22734
+ ),
23658
22735
  nowMs: errorNowMs,
23659
- state: options.state
22736
+ options
23660
22737
  });
23661
- if (continuationMarked) {
23662
- await sendWakeNudge({
23663
- conversationId,
23664
- destination,
23665
- idempotencyKey: nudgeIdempotencyKey(
23666
- "error",
23667
- conversationId,
23668
- errorNowMs
23669
- ),
23670
- nowMs: errorNowMs,
23671
- options
23672
- });
23673
- requeueSucceeded = true;
23674
- }
23675
- } catch (requeueError) {
23676
- logException(
23677
- requeueError,
23678
- "conversation_work_requeue_failed",
23679
- { conversationId },
23680
- {},
23681
- "Conversation work requeue failed after runner error"
23682
- );
23683
22738
  }
22739
+ } catch (requeueError) {
22740
+ logException(
22741
+ requeueError,
22742
+ "conversation_work_requeue_failed",
22743
+ { conversationId },
22744
+ {},
22745
+ "Conversation work requeue failed after runner error"
22746
+ );
23684
22747
  }
23685
22748
  try {
23686
22749
  await releaseConversationWork({
@@ -23698,8 +22761,16 @@ async function processConversationWork(message, options) {
23698
22761
  "Conversation work release failed after runner error"
23699
22762
  );
23700
22763
  }
23701
- if (requeueSucceeded) {
23702
- return { status: "pending_requeued" };
22764
+ if (!isProviderRetryError(error)) {
22765
+ logException(
22766
+ error,
22767
+ "conversation_work_failed",
22768
+ { conversationId },
22769
+ {
22770
+ "app.worker.elapsed_ms": now(options) - startedAtMs
22771
+ },
22772
+ "Conversation work failed"
22773
+ );
23703
22774
  }
23704
22775
  throw error;
23705
22776
  } finally {
@@ -24044,9 +23115,6 @@ async function createApp(options) {
24044
23115
  app.get("/api/oauth/callback/:provider", (c) => {
24045
23116
  return GET4(c.req.raw, c.req.param("provider"), waitUntil);
24046
23117
  });
24047
- app.post("/api/internal/turn-resume", (c) => {
24048
- return POST2(c.req.raw, waitUntil);
24049
- });
24050
23118
  app.post("/api/internal/agent-dispatch", (c) => {
24051
23119
  return POST(c.req.raw, waitUntil);
24052
23120
  });
@@ -24069,7 +23137,7 @@ async function createApp(options) {
24069
23137
  return GET2(c.req.raw, waitUntil);
24070
23138
  });
24071
23139
  app.post("/api/webhooks/:platform", (c) => {
24072
- return POST3(c.req.raw, c.req.param("platform"), waitUntil);
23140
+ return POST2(c.req.raw, c.req.param("platform"), waitUntil);
24073
23141
  });
24074
23142
  return app;
24075
23143
  }