@sentry/junior 0.52.0 → 0.54.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 (222) hide show
  1. package/README.md +3 -2
  2. package/dist/api-reference.d.ts +7 -0
  3. package/dist/app.d.ts +5 -10
  4. package/dist/app.js +1671 -1219
  5. package/dist/build/copy-build-content.d.ts +4 -0
  6. package/dist/build/glob-to-regex.d.ts +2 -0
  7. package/dist/build/rolldown-workarounds.d.ts +14 -0
  8. package/dist/build/virtual-config.d.ts +4 -0
  9. package/dist/chat/app/factory.d.ts +10 -0
  10. package/dist/chat/app/production.d.ts +6 -0
  11. package/dist/chat/app/services.d.ts +17 -0
  12. package/dist/chat/capabilities/catalog.d.ts +16 -0
  13. package/dist/chat/capabilities/factory.d.ts +10 -0
  14. package/dist/chat/capabilities/jr-rpc-command.d.ts +26 -0
  15. package/dist/chat/capabilities/router.d.ts +19 -0
  16. package/dist/chat/coerce.d.ts +6 -0
  17. package/dist/chat/config.d.ts +46 -0
  18. package/dist/chat/configuration/defaults.d.ts +4 -0
  19. package/dist/chat/configuration/service.d.ts +2 -0
  20. package/dist/chat/configuration/types.d.ts +37 -0
  21. package/dist/chat/configuration/validation.d.ts +2 -0
  22. package/dist/chat/credentials/broker.d.ts +22 -0
  23. package/dist/chat/credentials/header-transforms.d.ts +3 -0
  24. package/dist/chat/credentials/oauth-scope.d.ts +4 -0
  25. package/dist/chat/credentials/state-adapter-token-store.d.ts +9 -0
  26. package/dist/chat/credentials/test-broker.d.ts +19 -0
  27. package/dist/chat/credentials/unlink-provider.d.ts +2 -0
  28. package/dist/chat/credentials/user-token-store.d.ts +11 -0
  29. package/dist/chat/discovery.d.ts +47 -0
  30. package/dist/chat/ingress/junior-chat.d.ts +26 -0
  31. package/dist/chat/ingress/message-changed.d.ts +50 -0
  32. package/dist/chat/ingress/message-router.d.ts +9 -0
  33. package/dist/chat/ingress/slash-command.d.ts +3 -0
  34. package/dist/chat/ingress/workspace-membership.d.ts +10 -0
  35. package/dist/chat/interruption-marker.d.ts +2 -0
  36. package/dist/chat/logging.d.ts +88 -0
  37. package/dist/chat/mcp/auth-store.d.ts +41 -0
  38. package/dist/chat/mcp/client.d.ts +34 -0
  39. package/dist/chat/mcp/errors.d.ts +8 -0
  40. package/dist/chat/mcp/oauth-provider.d.ts +27 -0
  41. package/dist/chat/mcp/oauth.d.ts +17 -0
  42. package/dist/chat/mcp/tool-manager.d.ts +60 -0
  43. package/dist/chat/oauth-flow.d.ts +45 -0
  44. package/dist/chat/optional-string.d.ts +5 -0
  45. package/dist/chat/pi/client.d.ts +49 -0
  46. package/dist/chat/pi/messages.d.ts +3 -0
  47. package/dist/chat/pi/traced-stream.d.ts +9 -0
  48. package/dist/chat/plugins/auth/api-headers-broker.d.ts +6 -0
  49. package/dist/chat/plugins/auth/auth-token-placeholder.d.ts +3 -0
  50. package/dist/chat/plugins/auth/github-app-broker.d.ts +4 -0
  51. package/dist/chat/plugins/auth/oauth-bearer-broker.d.ts +6 -0
  52. package/dist/chat/plugins/auth/oauth-request.d.ts +18 -0
  53. package/dist/chat/plugins/command-env.d.ts +3 -0
  54. package/dist/chat/plugins/manifest.d.ts +3 -0
  55. package/dist/chat/plugins/package-discovery.d.ts +14 -0
  56. package/dist/chat/plugins/registry.d.ts +23 -0
  57. package/dist/chat/plugins/types.d.ts +146 -0
  58. package/dist/chat/prompt.d.ts +39 -0
  59. package/dist/chat/queue/thread-message-dispatcher.d.ts +33 -0
  60. package/dist/chat/respond-helpers.d.ts +77 -0
  61. package/dist/chat/respond.d.ts +64 -0
  62. package/dist/chat/runtime/auth-pause-state.d.ts +11 -0
  63. package/dist/chat/runtime/delivered-turn-state.d.ts +15 -0
  64. package/dist/chat/runtime/dev-agent-trace.d.ts +1 -0
  65. package/dist/chat/runtime/processing-reaction.d.ts +25 -0
  66. package/dist/chat/runtime/reply-executor.d.ts +56 -0
  67. package/dist/chat/runtime/report-progress.d.ts +3 -0
  68. package/dist/chat/runtime/slack-resume.d.ts +50 -0
  69. package/dist/chat/runtime/slack-runtime.d.ts +100 -0
  70. package/dist/chat/runtime/thread-context.d.ts +22 -0
  71. package/dist/chat/runtime/thread-state.d.ts +28 -0
  72. package/dist/chat/runtime/turn-preparation.d.ts +43 -0
  73. package/dist/chat/runtime/turn-user-message.d.ts +12 -0
  74. package/dist/chat/runtime/turn.d.ts +69 -0
  75. package/dist/chat/sandbox/credentials.d.ts +11 -0
  76. package/dist/chat/sandbox/egress-oidc.d.ts +3 -0
  77. package/dist/chat/sandbox/egress-policy.d.ts +11 -0
  78. package/dist/chat/sandbox/egress-proxy.d.ts +10 -0
  79. package/dist/chat/sandbox/egress-session.d.ts +27 -0
  80. package/dist/chat/sandbox/errors.d.ts +12 -0
  81. package/dist/chat/sandbox/eval-gh-stub.d.ts +2 -0
  82. package/dist/chat/sandbox/eval-oauth-stub.d.ts +2 -0
  83. package/dist/chat/sandbox/eval-sentry-stub.d.ts +2 -0
  84. package/dist/chat/sandbox/fault-injection.d.ts +2 -0
  85. package/dist/chat/sandbox/http-error-details.d.ts +18 -0
  86. package/dist/chat/sandbox/noninteractive-command.d.ts +17 -0
  87. package/dist/chat/sandbox/paths.d.ts +5 -0
  88. package/dist/chat/sandbox/runtime-dependency-snapshots.d.ts +20 -0
  89. package/dist/chat/sandbox/sandbox.d.ts +52 -0
  90. package/dist/chat/sandbox/session.d.ts +53 -0
  91. package/dist/chat/sandbox/skill-sandbox.d.ts +42 -0
  92. package/dist/chat/sandbox/skill-sync.d.ts +17 -0
  93. package/dist/chat/sandbox/workspace.d.ts +55 -0
  94. package/dist/chat/sentry.d.ts +2 -0
  95. package/dist/chat/services/attachment-claims.d.ts +2 -0
  96. package/dist/chat/services/auth-pause-response.d.ts +2 -0
  97. package/dist/chat/services/auth-pause.d.ts +12 -0
  98. package/dist/chat/services/channel-intent.d.ts +2 -0
  99. package/dist/chat/services/conversation-memory.d.ts +33 -0
  100. package/dist/chat/services/mcp-auth-orchestration.d.ts +29 -0
  101. package/dist/chat/services/pending-auth.d.ts +27 -0
  102. package/dist/chat/services/plugin-auth-orchestration.d.ts +36 -0
  103. package/dist/chat/services/provider-default-config.d.ts +9 -0
  104. package/dist/chat/services/provider-retry.d.ts +6 -0
  105. package/dist/chat/services/reply-delivery-plan.d.ts +17 -0
  106. package/dist/chat/services/subscribed-decision.d.ts +63 -0
  107. package/dist/chat/services/subscribed-reply-policy.d.ts +23 -0
  108. package/dist/chat/services/timeout-resume.d.ts +17 -0
  109. package/dist/chat/services/turn-checkpoint.d.ts +74 -0
  110. package/dist/chat/services/turn-continuation-response.d.ts +2 -0
  111. package/dist/chat/services/turn-failure-response.d.ts +15 -0
  112. package/dist/chat/services/turn-result.d.ts +55 -0
  113. package/dist/chat/services/turn-thinking-level.d.ts +49 -0
  114. package/dist/chat/services/vision-context.d.ts +61 -0
  115. package/dist/chat/skills.d.ts +48 -0
  116. package/dist/chat/slack/adapter.d.ts +9 -0
  117. package/dist/chat/slack/app-home.d.ts +11 -0
  118. package/dist/chat/slack/assistant-thread/lifecycle.d.ts +13 -0
  119. package/dist/chat/slack/assistant-thread/status-render.d.ts +28 -0
  120. package/dist/chat/slack/assistant-thread/status-scheduler.d.ts +23 -0
  121. package/dist/chat/slack/assistant-thread/status-send.d.ts +31 -0
  122. package/dist/chat/slack/assistant-thread/status.d.ts +36 -0
  123. package/dist/chat/slack/assistant-thread/title.d.ts +28 -0
  124. package/dist/chat/slack/canvas-references.d.ts +2 -0
  125. package/dist/chat/slack/channel.d.ts +48 -0
  126. package/dist/chat/slack/client.d.ts +52 -0
  127. package/dist/chat/slack/context.d.ts +9 -0
  128. package/dist/chat/slack/emoji.d.ts +1 -0
  129. package/dist/chat/slack/errors.d.ts +6 -0
  130. package/dist/chat/slack/footer.d.ts +42 -0
  131. package/dist/chat/slack/legacy-attachments.d.ts +4 -0
  132. package/dist/chat/slack/message.d.ts +6 -0
  133. package/dist/chat/slack/mrkdwn.d.ts +12 -0
  134. package/dist/chat/slack/outbound.d.ts +57 -0
  135. package/dist/chat/slack/output.d.ts +54 -0
  136. package/dist/chat/slack/reply.d.ts +33 -0
  137. package/dist/chat/slack/status-format.d.ts +2 -0
  138. package/dist/chat/slack/turn-continuation-notice.d.ts +8 -0
  139. package/dist/chat/slack/user.d.ts +7 -0
  140. package/dist/chat/slack/users.d.ts +39 -0
  141. package/dist/chat/state/adapter.d.ts +9 -0
  142. package/dist/chat/state/artifacts.d.ts +29 -0
  143. package/dist/chat/state/conversation.d.ts +81 -0
  144. package/dist/chat/state/pi-session-message-store.d.ts +15 -0
  145. package/dist/chat/state/turn-id.d.ts +2 -0
  146. package/dist/chat/state/turn-session-store.d.ts +49 -0
  147. package/dist/chat/tools/advisor/session-store.d.ts +9 -0
  148. package/dist/chat/tools/advisor/tool.d.ts +33 -0
  149. package/dist/chat/tools/agent-tools.d.ts +9 -0
  150. package/dist/chat/tools/channel-capabilities.d.ts +11 -0
  151. package/dist/chat/tools/definition.d.ts +17 -0
  152. package/dist/chat/tools/execution/build-sandbox-input.d.ts +2 -0
  153. package/dist/chat/tools/execution/normalize-result.d.ts +6 -0
  154. package/dist/chat/tools/execution/tool-error-handler.d.ts +3 -0
  155. package/dist/chat/tools/execution/tool-input-error.d.ts +6 -0
  156. package/dist/chat/tools/idempotency.d.ts +1 -0
  157. package/dist/chat/tools/index.d.ts +5 -0
  158. package/dist/chat/tools/runtime/report-progress.d.ts +4 -0
  159. package/dist/chat/tools/sandbox/attach-file.d.ts +7 -0
  160. package/dist/chat/tools/sandbox/bash.d.ts +5 -0
  161. package/dist/chat/tools/sandbox/edit-file.d.ts +37 -0
  162. package/dist/chat/tools/sandbox/file-utils.d.ts +52 -0
  163. package/dist/chat/tools/sandbox/find-files.d.ts +24 -0
  164. package/dist/chat/tools/sandbox/grep.d.ts +33 -0
  165. package/dist/chat/tools/sandbox/list-dir.d.ts +22 -0
  166. package/dist/chat/tools/sandbox/read-file.d.ts +32 -0
  167. package/dist/chat/tools/sandbox/text-edits.d.ts +28 -0
  168. package/dist/chat/tools/sandbox/write-file.d.ts +5 -0
  169. package/dist/chat/tools/skill/call-mcp-tool.d.ts +7 -0
  170. package/dist/chat/tools/skill/load-skill.d.ts +22 -0
  171. package/dist/chat/tools/skill/mcp-tool-summary.d.ts +31 -0
  172. package/dist/chat/tools/skill/search-mcp-tools.d.ts +8 -0
  173. package/dist/chat/tools/slack/canvas-tools.d.ts +29 -0
  174. package/dist/chat/tools/slack/canvases.d.ts +45 -0
  175. package/dist/chat/tools/slack/channel-list-messages.d.ts +9 -0
  176. package/dist/chat/tools/slack/channel-post-message.d.ts +4 -0
  177. package/dist/chat/tools/slack/list-tools.d.ts +21 -0
  178. package/dist/chat/tools/slack/lists.d.ts +42 -0
  179. package/dist/chat/tools/slack/message-add-reaction.d.ts +4 -0
  180. package/dist/chat/tools/slack/slack-message-url.d.ts +18 -0
  181. package/dist/chat/tools/slack/thread-read.d.ts +9 -0
  182. package/dist/chat/tools/slack/user-lookup.d.ts +8 -0
  183. package/dist/chat/tools/system-time.d.ts +1 -0
  184. package/dist/chat/tools/types.d.ts +55 -0
  185. package/dist/chat/tools/web/constants.d.ts +6 -0
  186. package/dist/chat/tools/web/fetch-content.d.ts +23 -0
  187. package/dist/chat/tools/web/fetch-tool.d.ts +5 -0
  188. package/dist/chat/tools/web/image-generate.d.ts +4 -0
  189. package/dist/chat/tools/web/network.d.ts +6 -0
  190. package/dist/chat/tools/web/search.d.ts +5 -0
  191. package/dist/chat/turn-context-tag.d.ts +6 -0
  192. package/dist/chat/usage.d.ts +24 -0
  193. package/dist/chat/xml.d.ts +1 -0
  194. package/dist/{chunk-XPXD3FCE.js → chunk-5LUISFEY.js} +189 -35
  195. package/dist/{chunk-KCOKQLBF.js → chunk-7WTXNEPF.js} +120 -15
  196. package/dist/{chunk-ZNFNY53B.js → chunk-QCHPJ4FD.js} +2 -2
  197. package/dist/{chunk-Q3FDONU7.js → chunk-YITDDLS3.js} +34 -28
  198. package/dist/cli/check.js +3 -3
  199. package/dist/cli/init.js +1 -0
  200. package/dist/cli/snapshot-warmup.js +3 -3
  201. package/dist/handlers/diagnostics-dashboard.d.ts +2 -0
  202. package/dist/handlers/diagnostics.d.ts +2 -0
  203. package/dist/handlers/health.d.ts +4 -0
  204. package/dist/handlers/mcp-oauth-callback.d.ts +2 -0
  205. package/dist/handlers/oauth-callback.d.ts +2 -0
  206. package/dist/handlers/oauth-html.d.ts +2 -0
  207. package/dist/handlers/sandbox-egress-proxy.d.ts +4 -0
  208. package/dist/handlers/turn-resume.d.ts +3 -0
  209. package/dist/handlers/types.d.ts +2 -0
  210. package/dist/handlers/webhooks.d.ts +15 -0
  211. package/dist/instrumentation.d.ts +1 -3
  212. package/dist/nitro.d.ts +4 -7
  213. package/dist/nitro.js +112 -54
  214. package/dist/package-resolution.d.ts +13 -0
  215. package/dist/vercel.d.ts +2 -4
  216. package/package.json +25 -25
  217. package/dist/cli/check.d.ts +0 -8
  218. package/dist/cli/env.d.ts +0 -7
  219. package/dist/cli/init.d.ts +0 -3
  220. package/dist/cli/run.d.ts +0 -12
  221. package/dist/cli/snapshot-warmup.d.ts +0 -3
  222. package/dist/types-X_iCClPb.d.ts +0 -75
package/dist/app.js CHANGED
@@ -3,8 +3,9 @@ import {
3
3
  findSkillByName,
4
4
  loadSkillsByName,
5
5
  parseSkillInvocation
6
- } from "./chunk-ZNFNY53B.js";
6
+ } from "./chunk-QCHPJ4FD.js";
7
7
  import {
8
+ ACTIVE_LOCK_TTL_MS,
8
9
  GEN_AI_PROVIDER_NAME,
9
10
  MISSING_GATEWAY_CREDENTIALS_ERROR,
10
11
  SANDBOX_DATA_ROOT,
@@ -31,7 +32,7 @@ import {
31
32
  runNonInteractiveCommand,
32
33
  sandboxSkillDir,
33
34
  sandboxSkillFile
34
- } from "./chunk-KCOKQLBF.js";
35
+ } from "./chunk-7WTXNEPF.js";
35
36
  import {
36
37
  CredentialUnavailableError,
37
38
  buildOAuthTokenRequest,
@@ -48,6 +49,7 @@ import {
48
49
  getPluginDefinition,
49
50
  getPluginMcpProviders,
50
51
  getPluginOAuthConfig,
52
+ getPluginPackageContent,
51
53
  getPluginProviders,
52
54
  hasRequiredOAuthScope,
53
55
  isPluginConfigKey,
@@ -70,18 +72,16 @@ import {
70
72
  toOptionalString,
71
73
  withContext,
72
74
  withSpan
73
- } from "./chunk-Q3FDONU7.js";
75
+ } from "./chunk-YITDDLS3.js";
74
76
  import {
75
77
  sentry_exports
76
78
  } from "./chunk-Z3YD6NHK.js";
77
79
  import {
78
- discoverInstalledPluginPackageContent,
79
80
  homeDir,
80
81
  listReferenceFiles,
81
- setPluginPackages,
82
82
  soulPathCandidates,
83
83
  worldPathCandidates
84
- } from "./chunk-XPXD3FCE.js";
84
+ } from "./chunk-5LUISFEY.js";
85
85
  import "./chunk-2KG3PWR4.js";
86
86
 
87
87
  // src/app.ts
@@ -89,11 +89,22 @@ import { Hono } from "hono";
89
89
 
90
90
  // src/chat/configuration/defaults.ts
91
91
  var installDefaults = {};
92
+ function cloneDefaults(defaults) {
93
+ return structuredClone(defaults);
94
+ }
95
+ function isConfigDefaultsRecord(defaults) {
96
+ return typeof defaults === "object" && defaults !== null && !Array.isArray(defaults);
97
+ }
92
98
  function setConfigDefaults(defaults) {
93
- if (!defaults) {
99
+ if (defaults === void 0) {
94
100
  installDefaults = {};
95
101
  return;
96
102
  }
103
+ if (!isConfigDefaultsRecord(defaults)) {
104
+ throw new Error(
105
+ "configDefaults must be an object keyed by plugin config key"
106
+ );
107
+ }
97
108
  for (const key of Object.keys(defaults)) {
98
109
  if (!isPluginConfigKey(key)) {
99
110
  throw new Error(
@@ -101,10 +112,10 @@ function setConfigDefaults(defaults) {
101
112
  );
102
113
  }
103
114
  }
104
- installDefaults = { ...defaults };
115
+ installDefaults = cloneDefaults(defaults);
105
116
  }
106
117
  function getConfigDefaults() {
107
- return installDefaults;
118
+ return cloneDefaults(installDefaults);
108
119
  }
109
120
 
110
121
  // src/handlers/diagnostics.ts
@@ -122,7 +133,7 @@ function readDescriptionText() {
122
133
  }
123
134
  }
124
135
  async function GET() {
125
- const packagedContent = discoverInstalledPluginPackageContent();
136
+ const packagedContent = getPluginPackageContent();
126
137
  const skills = await discoverSkills();
127
138
  return Response.json({
128
139
  cwd: process.cwd(),
@@ -494,6 +505,7 @@ function coerceThreadConversationState(value) {
494
505
  const processing = {
495
506
  activeTurnId: toOptionalString(rawProcessing.activeTurnId),
496
507
  lastCompletedAtMs: toOptionalNumber(rawProcessing.lastCompletedAtMs),
508
+ lastSessionId: toOptionalString(rawProcessing.lastSessionId),
497
509
  pendingAuth: coercePendingAuthState(rawProcessing.pendingAuth)
498
510
  };
499
511
  const rawStats = isRecord(rawConversation.stats) ? rawConversation.stats : {};
@@ -1478,6 +1490,9 @@ var StateBackedMcpOAuthClientProvider = class {
1478
1490
  this.sessionContext = sessionContext;
1479
1491
  this.clientMetadata = createClientMetadata(callbackUrl);
1480
1492
  }
1493
+ authSessionId;
1494
+ callbackUrl;
1495
+ sessionContext;
1481
1496
  clientMetadata;
1482
1497
  get redirectUrl() {
1483
1498
  return this.callbackUrl;
@@ -2101,17 +2116,24 @@ function startActiveTurn(args) {
2101
2116
  args.conversation.processing.activeTurnId = args.nextTurnId;
2102
2117
  args.updateConversationStats(args.conversation);
2103
2118
  }
2104
- function markTurnCompleted(args) {
2105
- if (!args.sessionId || args.conversation.processing.activeTurnId === args.sessionId) {
2106
- args.conversation.processing.activeTurnId = void 0;
2119
+ function clearActiveTurn(conversation, sessionId) {
2120
+ if (!sessionId || conversation.processing.activeTurnId === sessionId) {
2121
+ conversation.processing.activeTurnId = void 0;
2107
2122
  }
2123
+ }
2124
+ function markTurnClosed(args) {
2125
+ clearActiveTurn(args.conversation, args.sessionId);
2126
+ args.conversation.processing.lastCompletedAtMs = args.nowMs;
2127
+ args.updateConversationStats(args.conversation);
2128
+ }
2129
+ function markTurnCompleted(args) {
2130
+ clearActiveTurn(args.conversation, args.sessionId);
2131
+ args.conversation.processing.lastSessionId = args.sessionId;
2108
2132
  args.conversation.processing.lastCompletedAtMs = args.nowMs;
2109
2133
  args.updateConversationStats(args.conversation);
2110
2134
  }
2111
2135
  function markTurnFailed(args) {
2112
- if (!args.sessionId || args.conversation.processing.activeTurnId === args.sessionId) {
2113
- args.conversation.processing.activeTurnId = void 0;
2114
- }
2136
+ clearActiveTurn(args.conversation, args.sessionId);
2115
2137
  args.conversation.processing.lastCompletedAtMs = args.nowMs;
2116
2138
  args.markConversationMessage(args.conversation, args.userMessageId, {
2117
2139
  replied: false,
@@ -2120,39 +2142,6 @@ function markTurnFailed(args) {
2120
2142
  args.updateConversationStats(args.conversation);
2121
2143
  }
2122
2144
 
2123
- // src/chat/runtime/turn-user-message.ts
2124
- function normalizeSlackMessageTs(value) {
2125
- const trimmed = value?.trim();
2126
- return trimmed && /^\d+(?:\.\d+)?$/.test(trimmed) ? trimmed : void 0;
2127
- }
2128
- function getTurnUserMessage(conversation, sessionId) {
2129
- for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
2130
- const message = conversation.messages[index];
2131
- if (message?.role !== "user") {
2132
- continue;
2133
- }
2134
- if (buildDeterministicTurnId(message.id) === sessionId) {
2135
- return message;
2136
- }
2137
- }
2138
- return void 0;
2139
- }
2140
- function getTurnUserMessageId(conversation, sessionId) {
2141
- return getTurnUserMessage(conversation, sessionId)?.id;
2142
- }
2143
- function getTurnUserSlackMessageTs(message) {
2144
- return normalizeSlackMessageTs(message?.meta?.slackTs) ?? normalizeSlackMessageTs(message?.id);
2145
- }
2146
- function getTurnUserReplyAttachmentContext(message) {
2147
- const inboundAttachmentCount = message?.meta?.attachmentCount ?? 0;
2148
- const imageAttachmentCount = message?.meta?.imageAttachmentCount ?? 0;
2149
- const imagesHydrated = message?.meta?.imagesHydrated === true;
2150
- return {
2151
- ...inboundAttachmentCount > 0 ? { inboundAttachmentCount } : {},
2152
- ...!imagesHydrated && imageAttachmentCount > 0 ? { omittedImageAttachmentCount: imageAttachmentCount } : {}
2153
- };
2154
- }
2155
-
2156
2145
  // src/chat/services/conversation-memory.ts
2157
2146
  var CONTEXT_COMPACTION_TRIGGER_TOKENS = 9e3;
2158
2147
  var CONTEXT_COMPACTION_TARGET_TOKENS = 7e3;
@@ -2443,141 +2432,578 @@ function getConversationMessageSlackTs(message) {
2443
2432
  return message.meta?.slackTs ?? toOptionalString(message.id);
2444
2433
  }
2445
2434
 
2446
- // src/chat/respond.ts
2447
- import { Agent as Agent2 } from "@mariozechner/pi-agent-core";
2448
-
2449
- // src/chat/prompt.ts
2450
- import fs from "fs";
2451
- import path2 from "path";
2435
+ // src/chat/state/turn-session-store.ts
2436
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS2 } from "chat";
2452
2437
 
2453
- // src/chat/interruption-marker.ts
2454
- var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
2455
- function getInterruptionMarker() {
2456
- return INTERRUPTED_MARKER;
2438
+ // src/chat/state/pi-session-message-store.ts
2439
+ import { isDeepStrictEqual } from "util";
2440
+ var PI_SESSION_MESSAGE_PREFIX = "junior:pi_session_message";
2441
+ function piSessionMessageKey(scope, index) {
2442
+ return `${PI_SESSION_MESSAGE_PREFIX}:${scope.conversationId}:${scope.sessionId}:${index}`;
2443
+ }
2444
+ function parsePiMessage(value) {
2445
+ return isRecord(value) ? value : void 0;
2446
+ }
2447
+ function normalizeMessageCount(value) {
2448
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
2449
+ }
2450
+ function countMatchingPrefix(left, right) {
2451
+ const limit = Math.min(left.length, right.length);
2452
+ for (let index = 0; index < limit; index += 1) {
2453
+ if (!isDeepStrictEqual(left[index], right[index])) {
2454
+ return index;
2455
+ }
2456
+ }
2457
+ return limit;
2457
2458
  }
2458
-
2459
- // src/chat/slack/status-format.ts
2460
- var SLACK_STATUS_MAX_LENGTH = 50;
2461
- function truncateStatusText(text) {
2462
- const trimmed = text.trim();
2463
- if (!trimmed) {
2464
- return "";
2459
+ async function loadPiSessionMessages(args) {
2460
+ const stateAdapter = getStateAdapter();
2461
+ await stateAdapter.connect();
2462
+ const messageCount = normalizeMessageCount(args.messageCount);
2463
+ if (messageCount === 0) {
2464
+ return [];
2465
2465
  }
2466
- if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
2467
- return trimmed;
2466
+ const values = await Promise.all(
2467
+ Array.from(
2468
+ { length: messageCount },
2469
+ (_, index) => stateAdapter.get(piSessionMessageKey(args, index))
2470
+ )
2471
+ );
2472
+ const messages = [];
2473
+ for (const value of values) {
2474
+ const message = parsePiMessage(value);
2475
+ if (!message) {
2476
+ break;
2477
+ }
2478
+ messages.push(message);
2468
2479
  }
2469
- return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
2480
+ return messages.length === messageCount ? messages : void 0;
2470
2481
  }
2471
-
2472
- // src/chat/slack/mrkdwn.ts
2473
- function ensureBlockSpacing(text) {
2474
- const codeBlockPattern = /^```/;
2475
- const listItemPattern = /^[-*•]\s|^\d+\.\s/;
2476
- const lines = text.split("\n");
2477
- const result = [];
2478
- let inCodeBlock = false;
2479
- for (let i = 0; i < lines.length; i++) {
2480
- const line = lines[i];
2481
- const isCodeFence = codeBlockPattern.test(line.trimStart());
2482
- if (isCodeFence) {
2483
- if (!inCodeBlock) {
2484
- const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
2485
- if (prev2 !== void 0 && prev2.trim() !== "") {
2486
- result.push("");
2487
- }
2488
- }
2489
- inCodeBlock = !inCodeBlock;
2490
- result.push(line);
2491
- continue;
2492
- }
2493
- if (inCodeBlock) {
2494
- result.push(line);
2495
- continue;
2496
- }
2497
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
2498
- if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
2499
- result.push("");
2482
+ async function loadExistingPiSessionMessages(scope, maxCount) {
2483
+ const count = normalizeMessageCount(maxCount);
2484
+ if (count === 0) {
2485
+ return [];
2486
+ }
2487
+ const stateAdapter = getStateAdapter();
2488
+ await stateAdapter.connect();
2489
+ const values = await Promise.all(
2490
+ Array.from(
2491
+ { length: count },
2492
+ (_, index) => stateAdapter.get(piSessionMessageKey(scope, index))
2493
+ )
2494
+ );
2495
+ const messages = [];
2496
+ for (const value of values) {
2497
+ const message = parsePiMessage(value);
2498
+ if (!message) {
2499
+ break;
2500
2500
  }
2501
- result.push(line);
2501
+ messages.push(message);
2502
2502
  }
2503
- return result.join("\n");
2504
- }
2505
- function renderSlackMrkdwn(text) {
2506
- let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
2507
- normalized = ensureBlockSpacing(normalized);
2508
- return normalized.replace(/\n{3,}/g, "\n\n").trim();
2503
+ return messages;
2509
2504
  }
2510
- function normalizeSlackStatusText(text) {
2511
- const trimmed = text.trim();
2512
- if (!trimmed) {
2513
- return "";
2514
- }
2515
- return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
2505
+ async function commitPiSessionMessages(args) {
2506
+ const stateAdapter = getStateAdapter();
2507
+ await stateAdapter.connect();
2508
+ const existingMessages = await loadExistingPiSessionMessages(
2509
+ { conversationId: args.conversationId, sessionId: args.sessionId },
2510
+ args.messages.length
2511
+ );
2512
+ const writeFromIndex = countMatchingPrefix(existingMessages, args.messages);
2513
+ await Promise.all(
2514
+ args.messages.slice(writeFromIndex).map(
2515
+ (message, offset) => stateAdapter.set(
2516
+ piSessionMessageKey(args, writeFromIndex + offset),
2517
+ message,
2518
+ args.ttlMs
2519
+ )
2520
+ )
2521
+ );
2516
2522
  }
2517
2523
 
2518
- // src/chat/slack/output.ts
2519
- var MAX_INLINE_CHARS = 2200;
2520
- var MAX_INLINE_LINES = 45;
2521
- var CONTINUED_MARKER = "\n\n[Continued below]";
2522
- function countSlackLines(text) {
2523
- if (!text) {
2524
- return 0;
2525
- }
2526
- return text.split("\n").length;
2524
+ // src/chat/state/turn-session-store.ts
2525
+ var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
2526
+ var AGENT_TURN_SESSION_TTL_MS = THREAD_STATE_TTL_MS2;
2527
+ function agentTurnSessionKey(conversationId, sessionId) {
2528
+ return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
2527
2529
  }
2528
- function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
2529
- return text.length <= maxChars && countSlackLines(text) <= maxLines;
2530
+ function toFiniteNonNegativeNumber(value) {
2531
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
2530
2532
  }
2531
- function findSplitIndex(text, maxChars) {
2532
- if (text.length <= maxChars) {
2533
- return text.length;
2534
- }
2535
- const bounded = text.slice(0, maxChars);
2536
- const newlineIndex = bounded.lastIndexOf("\n");
2537
- if (newlineIndex > 0) {
2538
- return newlineIndex;
2533
+ function parseAgentTurnUsage(value) {
2534
+ if (!isRecord(value)) {
2535
+ return void 0;
2539
2536
  }
2540
- const spaceIndex = bounded.lastIndexOf(" ");
2541
- if (spaceIndex > 0) {
2542
- return spaceIndex;
2537
+ const usage = {};
2538
+ for (const field of [
2539
+ "inputTokens",
2540
+ "outputTokens",
2541
+ "cachedInputTokens",
2542
+ "cacheCreationTokens",
2543
+ "totalTokens"
2544
+ ]) {
2545
+ const count = toFiniteNonNegativeNumber(value[field]);
2546
+ if (count !== void 0) {
2547
+ usage[field] = count;
2548
+ }
2543
2549
  }
2544
- return maxChars;
2550
+ return Object.keys(usage).length > 0 ? usage : void 0;
2545
2551
  }
2546
- function splitByLineBudget(text, maxLines) {
2547
- if (maxLines <= 0) {
2548
- return "";
2552
+ function parseStoredRecord(value) {
2553
+ if (isRecord(value)) {
2554
+ return value;
2549
2555
  }
2550
- const lines = text.split("\n");
2551
- if (lines.length <= maxLines) {
2552
- return text;
2556
+ if (typeof value !== "string") {
2557
+ return void 0;
2558
+ }
2559
+ try {
2560
+ const parsed = JSON.parse(value);
2561
+ return isRecord(parsed) ? parsed : void 0;
2562
+ } catch {
2563
+ return void 0;
2553
2564
  }
2554
- return lines.slice(0, maxLines).join("\n");
2555
2565
  }
2556
- function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
2566
+ function parseAgentTurnSessionRecord(value) {
2567
+ const parsed = parseStoredRecord(value);
2568
+ if (!parsed) {
2569
+ return void 0;
2570
+ }
2571
+ const status = parsed.state;
2572
+ if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed" && status !== "superseded") {
2573
+ return void 0;
2574
+ }
2575
+ const conversationId = parsed.conversationId;
2576
+ const sessionId = parsed.sessionId;
2577
+ const sliceId = parsed.sliceId;
2578
+ const checkpointVersion = parsed.checkpointVersion;
2579
+ const updatedAtMs = parsed.updatedAtMs;
2580
+ const cumulativeDurationMs = toFiniteNonNegativeNumber(
2581
+ parsed.cumulativeDurationMs
2582
+ );
2583
+ const cumulativeUsage = parseAgentTurnUsage(parsed.cumulativeUsage);
2584
+ if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
2585
+ return void 0;
2586
+ }
2587
+ const legacyPiMessages = Array.isArray(parsed.piMessages) ? parsed.piMessages : [];
2588
+ const messageCount = toFiniteNonNegativeNumber(parsed.messageCount) ?? legacyPiMessages.length;
2557
2589
  return {
2558
- maxChars: Math.max(1, maxChars - suffix.length),
2559
- maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
2590
+ legacyPiMessages,
2591
+ record: {
2592
+ checkpointVersion,
2593
+ conversationId,
2594
+ sessionId,
2595
+ sliceId,
2596
+ state: status,
2597
+ updatedAtMs,
2598
+ messageCount,
2599
+ ...cumulativeDurationMs !== void 0 ? { cumulativeDurationMs } : {},
2600
+ ...cumulativeUsage ? { cumulativeUsage } : {},
2601
+ ...Array.isArray(parsed.loadedSkillNames) ? {
2602
+ loadedSkillNames: parsed.loadedSkillNames.filter(
2603
+ (value2) => typeof value2 === "string"
2604
+ )
2605
+ } : {},
2606
+ ...parsed.resumeReason === "timeout" || parsed.resumeReason === "auth" ? { resumeReason: parsed.resumeReason } : {},
2607
+ ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
2608
+ ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
2609
+ }
2560
2610
  };
2561
2611
  }
2562
- function forceSplitBudget(text, budget) {
2563
- const lineCount = countSlackLines(text);
2612
+ function materializePiMessages(legacyPiMessages, messageCount, sessionMessages) {
2613
+ if (messageCount === 0) {
2614
+ return [];
2615
+ }
2616
+ if (sessionMessages) {
2617
+ return sessionMessages;
2618
+ }
2619
+ if (legacyPiMessages.length >= messageCount) {
2620
+ return legacyPiMessages.slice(0, messageCount);
2621
+ }
2622
+ return void 0;
2623
+ }
2624
+ async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
2625
+ const stateAdapter = getStateAdapter();
2626
+ await stateAdapter.connect();
2627
+ const value = await stateAdapter.get(
2628
+ agentTurnSessionKey(conversationId, sessionId)
2629
+ );
2630
+ const parsed = parseAgentTurnSessionRecord(value);
2631
+ if (!parsed) {
2632
+ return void 0;
2633
+ }
2634
+ const sessionMessages = await loadPiSessionMessages({
2635
+ conversationId,
2636
+ sessionId,
2637
+ messageCount: parsed.record.messageCount
2638
+ });
2639
+ const piMessages = materializePiMessages(
2640
+ parsed.legacyPiMessages,
2641
+ parsed.record.messageCount,
2642
+ sessionMessages
2643
+ );
2644
+ if (!piMessages) {
2645
+ return void 0;
2646
+ }
2564
2647
  return {
2565
- maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
2566
- maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
2648
+ ...parsed.record,
2649
+ piMessages
2567
2650
  };
2568
2651
  }
2569
- function getFenceContinuation(text) {
2570
- let open;
2571
- for (const line of text.split("\n")) {
2572
- const trimmed = line.trimStart();
2573
- const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
2574
- if (!openerMatch) {
2575
- continue;
2576
- }
2577
- if (!open) {
2578
- open = {
2579
- fence: openerMatch[1],
2580
- openerLine: trimmed
2652
+ async function upsertAgentTurnSessionCheckpoint(args) {
2653
+ const stateAdapter = getStateAdapter();
2654
+ await stateAdapter.connect();
2655
+ const existingValue = await stateAdapter.get(
2656
+ agentTurnSessionKey(args.conversationId, args.sessionId)
2657
+ );
2658
+ const existingRecord = parseAgentTurnSessionRecord(existingValue);
2659
+ const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
2660
+ await commitPiSessionMessages({
2661
+ conversationId: args.conversationId,
2662
+ sessionId: args.sessionId,
2663
+ messages: args.piMessages,
2664
+ ttlMs
2665
+ });
2666
+ const storedMessageCount = args.piMessages.length;
2667
+ const checkpoint = {
2668
+ checkpointVersion: (existingRecord?.record.checkpointVersion ?? 0) + 1,
2669
+ conversationId: args.conversationId,
2670
+ sessionId: args.sessionId,
2671
+ sliceId: args.sliceId,
2672
+ state: args.state,
2673
+ updatedAtMs: Date.now(),
2674
+ messageCount: storedMessageCount,
2675
+ ...typeof args.cumulativeDurationMs === "number" && Number.isFinite(args.cumulativeDurationMs) ? {
2676
+ cumulativeDurationMs: Math.max(
2677
+ 0,
2678
+ Math.floor(args.cumulativeDurationMs)
2679
+ )
2680
+ } : {},
2681
+ ...args.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage } : {},
2682
+ ...Array.isArray(args.loadedSkillNames) ? {
2683
+ loadedSkillNames: args.loadedSkillNames.filter(
2684
+ (value) => typeof value === "string"
2685
+ )
2686
+ } : {},
2687
+ ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
2688
+ ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
2689
+ ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
2690
+ };
2691
+ await stateAdapter.set(
2692
+ agentTurnSessionKey(args.conversationId, args.sessionId),
2693
+ checkpoint,
2694
+ ttlMs
2695
+ );
2696
+ return {
2697
+ ...checkpoint,
2698
+ piMessages: [...args.piMessages]
2699
+ };
2700
+ }
2701
+ async function supersedeAgentTurnSessionCheckpoint(args) {
2702
+ const existing = await getAgentTurnSessionCheckpoint(
2703
+ args.conversationId,
2704
+ args.sessionId
2705
+ );
2706
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "superseded") {
2707
+ return void 0;
2708
+ }
2709
+ return await upsertAgentTurnSessionCheckpoint({
2710
+ conversationId: existing.conversationId,
2711
+ sessionId: existing.sessionId,
2712
+ sliceId: existing.sliceId,
2713
+ state: "superseded",
2714
+ piMessages: existing.piMessages,
2715
+ cumulativeDurationMs: existing.cumulativeDurationMs,
2716
+ cumulativeUsage: existing.cumulativeUsage,
2717
+ loadedSkillNames: existing.loadedSkillNames,
2718
+ resumeReason: existing.resumeReason,
2719
+ resumedFromSliceId: existing.resumedFromSliceId,
2720
+ errorMessage: args.errorMessage ?? existing.errorMessage
2721
+ });
2722
+ }
2723
+ async function failAgentTurnSessionCheckpoint(args) {
2724
+ const existing = await getAgentTurnSessionCheckpoint(
2725
+ args.conversationId,
2726
+ args.sessionId
2727
+ );
2728
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "superseded" || typeof args.expectedCheckpointVersion === "number" && existing.checkpointVersion !== args.expectedCheckpointVersion) {
2729
+ return void 0;
2730
+ }
2731
+ return await upsertAgentTurnSessionCheckpoint({
2732
+ conversationId: existing.conversationId,
2733
+ sessionId: existing.sessionId,
2734
+ sliceId: existing.sliceId,
2735
+ state: "failed",
2736
+ piMessages: existing.piMessages,
2737
+ cumulativeDurationMs: existing.cumulativeDurationMs,
2738
+ cumulativeUsage: existing.cumulativeUsage,
2739
+ loadedSkillNames: existing.loadedSkillNames,
2740
+ resumeReason: existing.resumeReason,
2741
+ resumedFromSliceId: existing.resumedFromSliceId,
2742
+ errorMessage: args.errorMessage ?? existing.errorMessage
2743
+ });
2744
+ }
2745
+
2746
+ // src/chat/services/pending-auth.ts
2747
+ var AUTH_LINK_REUSE_WINDOW_MS = 10 * 60 * 1e3;
2748
+ function canReusePendingAuthLink(args) {
2749
+ const { pendingAuth } = args;
2750
+ if (!pendingAuth) {
2751
+ return false;
2752
+ }
2753
+ return pendingAuth.kind === args.kind && pendingAuth.provider === args.provider && pendingAuth.requesterId === args.requesterId && pendingAuth.linkSentAtMs + AUTH_LINK_REUSE_WINDOW_MS > (args.nowMs ?? Date.now());
2754
+ }
2755
+ function getConversationPendingAuth(args) {
2756
+ const pendingAuth = args.conversation.processing.pendingAuth;
2757
+ if (!pendingAuth) {
2758
+ return void 0;
2759
+ }
2760
+ if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId) {
2761
+ return void 0;
2762
+ }
2763
+ return pendingAuth;
2764
+ }
2765
+ function clearPendingAuth(conversation, sessionId) {
2766
+ if (!conversation.processing.pendingAuth) {
2767
+ return;
2768
+ }
2769
+ if (sessionId && conversation.processing.pendingAuth.sessionId !== sessionId) {
2770
+ return;
2771
+ }
2772
+ conversation.processing.pendingAuth = void 0;
2773
+ }
2774
+ async function applyPendingAuthUpdate(args) {
2775
+ const previousPendingAuth = args.conversation.processing.pendingAuth;
2776
+ args.conversation.processing.pendingAuth = args.nextPendingAuth;
2777
+ if (previousPendingAuth && previousPendingAuth.sessionId !== args.nextPendingAuth.sessionId && args.conversationId) {
2778
+ await supersedeAgentTurnSessionCheckpoint({
2779
+ conversationId: args.conversationId,
2780
+ sessionId: previousPendingAuth.sessionId,
2781
+ errorMessage: "Superseded by a newer auth-blocked request in the same conversation."
2782
+ });
2783
+ }
2784
+ }
2785
+ function isPendingAuthLatestRequest(conversation, pendingAuth) {
2786
+ for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
2787
+ const message = conversation.messages[index];
2788
+ if (message?.role !== "user") {
2789
+ continue;
2790
+ }
2791
+ return buildDeterministicTurnId(message.id) === pendingAuth.sessionId;
2792
+ }
2793
+ return false;
2794
+ }
2795
+
2796
+ // src/chat/runtime/delivered-turn-state.ts
2797
+ function buildDeliveredTurnStatePatch(args) {
2798
+ const conversation = structuredClone(args.conversation);
2799
+ const artifactStatePatch = {
2800
+ ...args.reply.artifactStatePatch ?? {},
2801
+ ...args.artifactStatePatch ?? {}
2802
+ };
2803
+ const artifacts = Object.keys(artifactStatePatch).length > 0 ? mergeArtifactsState(args.artifacts, artifactStatePatch) : void 0;
2804
+ clearPendingAuth(conversation, args.sessionId);
2805
+ markConversationMessage(conversation, args.userMessageId, {
2806
+ replied: true,
2807
+ skippedReason: void 0
2808
+ });
2809
+ upsertConversationMessage(conversation, {
2810
+ id: generateConversationId("assistant"),
2811
+ role: "assistant",
2812
+ text: normalizeConversationText(args.reply.text) || "[empty response]",
2813
+ createdAtMs: Date.now(),
2814
+ author: {
2815
+ userName: botConfig.userName,
2816
+ isBot: true
2817
+ },
2818
+ meta: {
2819
+ replied: true
2820
+ }
2821
+ });
2822
+ markTurnCompleted({
2823
+ conversation,
2824
+ nowMs: Date.now(),
2825
+ sessionId: args.sessionId,
2826
+ updateConversationStats
2827
+ });
2828
+ return {
2829
+ artifacts,
2830
+ conversation,
2831
+ sandboxId: args.reply.sandboxId,
2832
+ sandboxDependencyProfileHash: args.reply.sandboxDependencyProfileHash
2833
+ };
2834
+ }
2835
+
2836
+ // src/chat/runtime/turn-user-message.ts
2837
+ function normalizeSlackMessageTs(value) {
2838
+ const trimmed = value?.trim();
2839
+ return trimmed && /^\d+(?:\.\d+)?$/.test(trimmed) ? trimmed : void 0;
2840
+ }
2841
+ function getTurnUserMessage(conversation, sessionId) {
2842
+ for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
2843
+ const message = conversation.messages[index];
2844
+ if (message?.role !== "user") {
2845
+ continue;
2846
+ }
2847
+ if (buildDeterministicTurnId(message.id) === sessionId) {
2848
+ return message;
2849
+ }
2850
+ }
2851
+ return void 0;
2852
+ }
2853
+ function getTurnUserMessageId(conversation, sessionId) {
2854
+ return getTurnUserMessage(conversation, sessionId)?.id;
2855
+ }
2856
+ function getTurnUserSlackMessageTs(message) {
2857
+ return normalizeSlackMessageTs(message?.meta?.slackTs) ?? normalizeSlackMessageTs(message?.id);
2858
+ }
2859
+ function getTurnUserReplyAttachmentContext(message) {
2860
+ const inboundAttachmentCount = message?.meta?.attachmentCount ?? 0;
2861
+ const imageAttachmentCount = message?.meta?.imageAttachmentCount ?? 0;
2862
+ const imagesHydrated = message?.meta?.imagesHydrated === true;
2863
+ return {
2864
+ ...inboundAttachmentCount > 0 ? { inboundAttachmentCount } : {},
2865
+ ...!imagesHydrated && imageAttachmentCount > 0 ? { omittedImageAttachmentCount: imageAttachmentCount } : {}
2866
+ };
2867
+ }
2868
+
2869
+ // src/chat/respond.ts
2870
+ import { Agent as Agent2 } from "@earendil-works/pi-agent-core";
2871
+
2872
+ // src/chat/prompt.ts
2873
+ import fs from "fs";
2874
+ import path2 from "path";
2875
+
2876
+ // src/chat/turn-context-tag.ts
2877
+ var TURN_CONTEXT_TAG = "runtime-turn-context";
2878
+
2879
+ // src/chat/interruption-marker.ts
2880
+ var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
2881
+ function getInterruptionMarker() {
2882
+ return INTERRUPTED_MARKER;
2883
+ }
2884
+
2885
+ // src/chat/slack/status-format.ts
2886
+ var SLACK_STATUS_MAX_LENGTH = 50;
2887
+ function truncateStatusText(text) {
2888
+ const trimmed = text.trim();
2889
+ if (!trimmed) {
2890
+ return "";
2891
+ }
2892
+ if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
2893
+ return trimmed;
2894
+ }
2895
+ return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
2896
+ }
2897
+
2898
+ // src/chat/slack/mrkdwn.ts
2899
+ function ensureBlockSpacing(text) {
2900
+ const codeBlockPattern = /^```/;
2901
+ const listItemPattern = /^[-*•]\s|^\d+\.\s/;
2902
+ const lines = text.split("\n");
2903
+ const result = [];
2904
+ let inCodeBlock = false;
2905
+ for (let i = 0; i < lines.length; i++) {
2906
+ const line = lines[i];
2907
+ const isCodeFence = codeBlockPattern.test(line.trimStart());
2908
+ if (isCodeFence) {
2909
+ if (!inCodeBlock) {
2910
+ const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
2911
+ if (prev2 !== void 0 && prev2.trim() !== "") {
2912
+ result.push("");
2913
+ }
2914
+ }
2915
+ inCodeBlock = !inCodeBlock;
2916
+ result.push(line);
2917
+ continue;
2918
+ }
2919
+ if (inCodeBlock) {
2920
+ result.push(line);
2921
+ continue;
2922
+ }
2923
+ const prev = result.length > 0 ? result[result.length - 1] : void 0;
2924
+ if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
2925
+ result.push("");
2926
+ }
2927
+ result.push(line);
2928
+ }
2929
+ return result.join("\n");
2930
+ }
2931
+ function renderSlackMrkdwn(text) {
2932
+ let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
2933
+ normalized = ensureBlockSpacing(normalized);
2934
+ return normalized.replace(/\n{3,}/g, "\n\n").trim();
2935
+ }
2936
+ function normalizeSlackStatusText(text) {
2937
+ const trimmed = text.trim();
2938
+ if (!trimmed) {
2939
+ return "";
2940
+ }
2941
+ return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
2942
+ }
2943
+
2944
+ // src/chat/slack/output.ts
2945
+ var MAX_INLINE_CHARS = 2200;
2946
+ var MAX_INLINE_LINES = 45;
2947
+ var CONTINUED_MARKER = "\n\n[Continued below]";
2948
+ function countSlackLines(text) {
2949
+ if (!text) {
2950
+ return 0;
2951
+ }
2952
+ return text.split("\n").length;
2953
+ }
2954
+ function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
2955
+ return text.length <= maxChars && countSlackLines(text) <= maxLines;
2956
+ }
2957
+ function findSplitIndex(text, maxChars) {
2958
+ if (text.length <= maxChars) {
2959
+ return text.length;
2960
+ }
2961
+ const bounded = text.slice(0, maxChars);
2962
+ const newlineIndex = bounded.lastIndexOf("\n");
2963
+ if (newlineIndex > 0) {
2964
+ return newlineIndex;
2965
+ }
2966
+ const spaceIndex = bounded.lastIndexOf(" ");
2967
+ if (spaceIndex > 0) {
2968
+ return spaceIndex;
2969
+ }
2970
+ return maxChars;
2971
+ }
2972
+ function splitByLineBudget(text, maxLines) {
2973
+ if (maxLines <= 0) {
2974
+ return "";
2975
+ }
2976
+ const lines = text.split("\n");
2977
+ if (lines.length <= maxLines) {
2978
+ return text;
2979
+ }
2980
+ return lines.slice(0, maxLines).join("\n");
2981
+ }
2982
+ function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
2983
+ return {
2984
+ maxChars: Math.max(1, maxChars - suffix.length),
2985
+ maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
2986
+ };
2987
+ }
2988
+ function forceSplitBudget(text, budget) {
2989
+ const lineCount = countSlackLines(text);
2990
+ return {
2991
+ maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
2992
+ maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
2993
+ };
2994
+ }
2995
+ function getFenceContinuation(text) {
2996
+ let open;
2997
+ for (const line of text.split("\n")) {
2998
+ const trimmed = line.trimStart();
2999
+ const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
3000
+ if (!openerMatch) {
3001
+ continue;
3002
+ }
3003
+ if (!open) {
3004
+ open = {
3005
+ fence: openerMatch[1],
3006
+ openerLine: trimmed
2581
3007
  };
2582
3008
  continue;
2583
3009
  }
@@ -2859,17 +3285,17 @@ function formatAvailableSkillsForPrompt(skills, invocation) {
2859
3285
  (s) => s.disableModelInvocation === true && s.name === invocation.skillName
2860
3286
  ) : [];
2861
3287
  const sections = [];
2862
- const available = [
2863
- "<available-skills>",
2864
- ...autoSelectable.length > 0 ? [
3288
+ if (autoSelectable.length > 0) {
3289
+ const available = [
3290
+ "<available-skills>",
2865
3291
  "Scan before answering. Load the most specific matching skill; do not answer from memory when a skill fits. If none fits, do not load a skill."
2866
- ] : []
2867
- ];
2868
- for (const skill of autoSelectable) {
2869
- available.push(...formatSkillEntry(skill));
3292
+ ];
3293
+ for (const skill of autoSelectable) {
3294
+ available.push(...formatSkillEntry(skill));
3295
+ }
3296
+ available.push("</available-skills>");
3297
+ sections.push(available.join("\n"));
2870
3298
  }
2871
- available.push("</available-skills>");
2872
- sections.push(available.join("\n"));
2873
3299
  if (invokedExplicitOnly.length > 0) {
2874
3300
  const userCallable = [
2875
3301
  "<user-callable-skills>",
@@ -2881,11 +3307,11 @@ function formatAvailableSkillsForPrompt(skills, invocation) {
2881
3307
  userCallable.push("</user-callable-skills>");
2882
3308
  sections.push(userCallable.join("\n"));
2883
3309
  }
2884
- return sections.join("\n");
3310
+ return sections.length > 0 ? sections.join("\n") : null;
2885
3311
  }
2886
3312
  function formatLoadedSkillsForPrompt(skills) {
2887
3313
  if (skills.length === 0) {
2888
- return "<loaded-skills>\n</loaded-skills>";
3314
+ return null;
2889
3315
  }
2890
3316
  const lines = ["<loaded-skills>"];
2891
3317
  for (const skill of skills) {
@@ -3010,17 +3436,8 @@ function formatConfigurationLines(configuration) {
3010
3436
  (key) => `- ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`
3011
3437
  );
3012
3438
  }
3013
- function formatSlackCapabilityNames(capabilities) {
3014
- const names = [
3015
- capabilities?.canCreateCanvas ? "canvas_create" : "",
3016
- capabilities?.canPostToChannel ? "channel_post" : "",
3017
- capabilities?.canAddReactions ? "reaction_add" : ""
3018
- ].filter(Boolean);
3019
- return names.length > 0 ? names.join(", ") : "none";
3020
- }
3021
3439
  var HEADER = "You are a Slack-based helper assistant. Follow the personality block for voice and tone in every reply. The behavior and output blocks define platform mechanics and override personality only when those mechanics conflict.";
3022
3440
  var TURN_CONTEXT_HEADER = "Per-turn runtime context for this request. Treat these blocks as trusted runtime facts and skill/provider instructions for the current turn; the static system prompt remains authoritative.";
3023
- var TURN_CONTEXT_TAG = "runtime-turn-context";
3024
3441
  var TOOL_POLICY_RULES = [
3025
3442
  "- Tool schemas are the source of truth for parameters; tool names are case-sensitive, so call tools exactly by their exposed names and do not invent arguments.",
3026
3443
  "- Use tools for actionable work and for facts that are mutable, external, repository-backed, provider-backed, or requested as verified/current. Stable general knowledge and already-provided context may be answered directly.",
@@ -3108,16 +3525,12 @@ function buildIdentitySection() {
3108
3525
  }
3109
3526
  function buildRuntimeSection(params) {
3110
3527
  const lines = [
3111
- `- version: ${escapeXml(getRuntimeMetadata().version ?? "unknown")}`,
3112
- params.modelId ? `- model: ${escapeXml(params.modelId)}` : "",
3113
- params.fastModelId ? `- fast_model: ${escapeXml(params.fastModelId)}` : "",
3114
- params.thinkingLevel ? `- thinking: ${escapeXml(params.thinkingLevel)}` : "",
3115
- params.channelId ? "- channel: slack" : "",
3116
- params.channelId ? `- slack_capabilities: ${escapeXml(
3117
- formatSlackCapabilityNames(params.slackCapabilities)
3118
- )}` : "",
3119
- `- sandbox_workspace: ${escapeXml(SANDBOX_WORKSPACE_ROOT)}`
3528
+ params.conversationId ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` : "",
3529
+ params.traceId ? `- trace_id: ${escapeXml(params.traceId)}` : ""
3120
3530
  ].filter(Boolean);
3531
+ if (lines.length === 0) {
3532
+ return null;
3533
+ }
3121
3534
  return renderTagBlock("runtime", lines.join("\n"));
3122
3535
  }
3123
3536
  function buildContextSection(params) {
@@ -3170,14 +3583,24 @@ function buildContextSection(params) {
3170
3583
  );
3171
3584
  }
3172
3585
  const body = blocks.map((block) => block.join("\n")).join("\n\n");
3586
+ if (!body) {
3587
+ return null;
3588
+ }
3173
3589
  return renderTagBlock("context", body);
3174
3590
  }
3175
3591
  function buildCapabilitiesSection(params) {
3176
3592
  const blocks = [];
3177
- blocks.push(
3178
- formatAvailableSkillsForPrompt(params.availableSkills, params.invocation)
3593
+ const availableSkills = formatAvailableSkillsForPrompt(
3594
+ params.availableSkills,
3595
+ params.invocation
3179
3596
  );
3180
- blocks.push(formatLoadedSkillsForPrompt(params.activeSkills));
3597
+ if (availableSkills) {
3598
+ blocks.push(availableSkills);
3599
+ }
3600
+ const loadedSkills = formatLoadedSkillsForPrompt(params.activeSkills);
3601
+ if (loadedSkills) {
3602
+ blocks.push(loadedSkills);
3603
+ }
3181
3604
  const activeCatalogs = formatActiveMcpCatalogsForPrompt(
3182
3605
  params.activeMcpCatalogs
3183
3606
  );
@@ -3192,6 +3615,9 @@ function buildCapabilitiesSection(params) {
3192
3615
  if (providerCatalog) {
3193
3616
  blocks.push(renderTagBlock("providers", providerCatalog));
3194
3617
  }
3618
+ if (blocks.length === 0) {
3619
+ return null;
3620
+ }
3195
3621
  return renderTagBlock("capabilities", blocks.join("\n\n"));
3196
3622
  }
3197
3623
  var STATIC_SYSTEM_PROMPT = [
@@ -3225,7 +3651,7 @@ function buildTurnContextPrompt(params) {
3225
3651
  }),
3226
3652
  buildRuntimeSection(params.runtime ?? {}),
3227
3653
  `</${TURN_CONTEXT_TAG}>`
3228
- ];
3654
+ ].filter((section) => Boolean(section));
3229
3655
  return sections.join("\n\n");
3230
3656
  }
3231
3657
 
@@ -3939,6 +4365,8 @@ var PluginMcpClient = class {
3939
4365
  this.plugin = plugin;
3940
4366
  this.options = options;
3941
4367
  }
4368
+ plugin;
4369
+ options;
3942
4370
  client;
3943
4371
  lastAttemptedTransportSessionId;
3944
4372
  transport;
@@ -4220,6 +4648,7 @@ var McpToolManager = class {
4220
4648
  }
4221
4649
  }
4222
4650
  }
4651
+ options;
4223
4652
  pluginsByProvider = /* @__PURE__ */ new Map();
4224
4653
  activeProviders = /* @__PURE__ */ new Set();
4225
4654
  authorizationPendingProviders = /* @__PURE__ */ new Set();
@@ -7877,11 +8306,12 @@ function createSystemTimeTool() {
7877
8306
  // src/chat/tools/advisor/tool.ts
7878
8307
  import {
7879
8308
  Agent
7880
- } from "@mariozechner/pi-agent-core";
8309
+ } from "@earendil-works/pi-agent-core";
7881
8310
  import { Type as Type21 } from "@sinclair/typebox";
7882
8311
 
7883
8312
  // src/chat/respond-helpers.ts
7884
8313
  var MAX_INLINE_ATTACHMENT_BASE64_CHARS = 12e4;
8314
+ var RUNTIME_TURN_CONTEXT_START = `<${TURN_CONTEXT_TAG}>`;
7885
8315
  function getSessionIdentifiers(context) {
7886
8316
  return {
7887
8317
  conversationId: context.correlation?.conversationId ?? context.correlation?.threadId ?? context.correlation?.runId,
@@ -7961,44 +8391,20 @@ function summarizeMessageText(text) {
7961
8391
  }
7962
8392
  return normalized.length > 1200 ? `${normalized.slice(0, 1200)}...` : normalized;
7963
8393
  }
7964
- function buildUserTurnText(userInput, conversationContext, metadata) {
8394
+ function buildUserTurnText(userInput, conversationContext) {
7965
8395
  const trimmedContext = conversationContext?.trim();
7966
- const conversationId = metadata?.sessionContext?.conversationId;
7967
- const traceId = metadata?.turnContext?.traceId;
7968
- if (!trimmedContext && !conversationId && !traceId) {
8396
+ if (!trimmedContext) {
7969
8397
  return userInput;
7970
8398
  }
7971
- const sections = [];
7972
- if (trimmedContext) {
7973
- sections.push(
7974
- "<thread-background>",
7975
- trimmedContext,
7976
- "</thread-background>",
7977
- ""
7978
- );
7979
- }
7980
- if (conversationId) {
7981
- sections.push(
7982
- "<session-context>",
7983
- `- gen_ai.conversation.id: ${conversationId}`,
7984
- "</session-context>",
7985
- ""
7986
- );
7987
- }
7988
- if (traceId) {
7989
- sections.push(
7990
- "<turn-context>",
7991
- `- trace_id: ${traceId}`,
7992
- "</turn-context>",
7993
- ""
7994
- );
7995
- }
7996
- sections.push(
7997
- '<current-instruction priority="highest">',
8399
+ return [
8400
+ "<thread-background>",
8401
+ trimmedContext,
8402
+ "</thread-background>",
8403
+ "",
8404
+ "<current-instruction>",
7998
8405
  userInput,
7999
8406
  "</current-instruction>"
8000
- );
8001
- return sections.join("\n");
8407
+ ].join("\n");
8002
8408
  }
8003
8409
  function encodeNonImageAttachmentForPrompt(attachment) {
8004
8410
  const base64 = attachment.data.toString("base64");
@@ -8037,12 +8443,81 @@ function isToolResultError(result) {
8037
8443
  function isAssistantMessage(value) {
8038
8444
  return typeof value === "object" && value !== null && value.role === "assistant";
8039
8445
  }
8040
- function getPiMessageRole(value) {
8041
- if (!value || typeof value !== "object") {
8042
- return void 0;
8043
- }
8044
- const role = value.role;
8045
- return typeof role === "string" ? role : void 0;
8446
+ function getPiMessageRole(value) {
8447
+ if (!value || typeof value !== "object") {
8448
+ return void 0;
8449
+ }
8450
+ const role = value.role;
8451
+ return typeof role === "string" ? role : void 0;
8452
+ }
8453
+ function getUserMessageContent(message) {
8454
+ const record = message;
8455
+ return record.role === "user" && Array.isArray(record.content) ? record.content : void 0;
8456
+ }
8457
+ function isRuntimeTurnContextPart(part, marker) {
8458
+ return part !== null && typeof part === "object" && part.type === "text" && typeof part.text === "string" && part.text.startsWith(marker);
8459
+ }
8460
+ function replaceRuntimeTurnContext(message, turnContextPrompt) {
8461
+ const content = getUserMessageContent(message);
8462
+ if (!content) {
8463
+ return void 0;
8464
+ }
8465
+ const marker = turnContextPrompt.split("\n", 1)[0];
8466
+ const contextIndex = content.findIndex(
8467
+ (part) => isRuntimeTurnContextPart(part, marker)
8468
+ );
8469
+ if (contextIndex < 0) {
8470
+ return void 0;
8471
+ }
8472
+ const nextContent = [...content];
8473
+ nextContent[contextIndex] = {
8474
+ ...nextContent[contextIndex],
8475
+ text: turnContextPrompt
8476
+ };
8477
+ return {
8478
+ ...message,
8479
+ content: nextContent
8480
+ };
8481
+ }
8482
+ function refreshRuntimeTurnContext(messages, turnContextPrompt) {
8483
+ for (let index = 0; index < messages.length; index += 1) {
8484
+ const updated = replaceRuntimeTurnContext(
8485
+ messages[index],
8486
+ turnContextPrompt
8487
+ );
8488
+ if (!updated) {
8489
+ continue;
8490
+ }
8491
+ const nextMessages = [...messages];
8492
+ nextMessages[index] = updated;
8493
+ return nextMessages;
8494
+ }
8495
+ return [
8496
+ ...messages,
8497
+ {
8498
+ role: "user",
8499
+ content: [{ type: "text", text: turnContextPrompt }],
8500
+ timestamp: Date.now()
8501
+ }
8502
+ ];
8503
+ }
8504
+ function stripRuntimeTurnContext(messages) {
8505
+ return messages.flatMap((message) => {
8506
+ const content = getUserMessageContent(message);
8507
+ if (!content) {
8508
+ return [message];
8509
+ }
8510
+ const nextContent = content.filter(
8511
+ (part) => !isRuntimeTurnContextPart(part, RUNTIME_TURN_CONTEXT_START)
8512
+ );
8513
+ if (nextContent.length === content.length) {
8514
+ return [message];
8515
+ }
8516
+ if (nextContent.length === 0) {
8517
+ return [];
8518
+ }
8519
+ return [{ ...message, content: nextContent }];
8520
+ });
8046
8521
  }
8047
8522
  function extractAssistantText(message) {
8048
8523
  const content = message.content ?? [];
@@ -8081,8 +8556,8 @@ function trimTrailingAssistantMessages(messages) {
8081
8556
  }
8082
8557
 
8083
8558
  // src/chat/tools/advisor/session-store.ts
8084
- import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS2 } from "chat";
8085
- var ADVISOR_SESSION_TTL_MS = THREAD_STATE_TTL_MS2;
8559
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS3 } from "chat";
8560
+ var ADVISOR_SESSION_TTL_MS = THREAD_STATE_TTL_MS3;
8086
8561
  function cloneMessages(messages) {
8087
8562
  return structuredClone(messages);
8088
8563
  }
@@ -9057,7 +9532,7 @@ function resolveChannelCapabilities(channelId) {
9057
9532
  // src/chat/pi/traced-stream.ts
9058
9533
  import {
9059
9534
  streamSimple
9060
- } from "@mariozechner/pi-ai";
9535
+ } from "@earendil-works/pi-ai";
9061
9536
  function buildChatStartAttributes(model, context) {
9062
9537
  const attributes = {
9063
9538
  "gen_ai.operation.name": "chat",
@@ -9142,23 +9617,18 @@ import fs4 from "fs/promises";
9142
9617
  import { createHmac, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
9143
9618
  var SANDBOX_EGRESS_PROXY_PATH = "/api/internal/sandbox-egress";
9144
9619
  var SANDBOX_EGRESS_TOKEN_VERSION = "v1";
9620
+ var SANDBOX_EGRESS_HMAC_CONTEXT = "junior.sandbox_egress.v1";
9145
9621
  var SANDBOX_EGRESS_LEASE_PREFIX = "sandbox-egress-lease";
9146
9622
  var DEFAULT_SESSION_TTL_MS = 30 * 60 * 1e3;
9147
9623
  function leaseKey(provider, context) {
9148
9624
  return `${SANDBOX_EGRESS_LEASE_PREFIX}:${provider}:${context.requesterId}:${context.egressId}:${context.contextId}`;
9149
9625
  }
9150
9626
  function getSandboxEgressSecret() {
9151
- const explicit = process.env.JUNIOR_SANDBOX_EGRESS_SECRET?.trim();
9152
- if (explicit) {
9153
- return explicit;
9627
+ const secret = process.env.JUNIOR_SECRET?.trim();
9628
+ if (secret) {
9629
+ return secret;
9154
9630
  }
9155
- const sharedInternal = process.env.JUNIOR_INTERNAL_RESUME_SECRET?.trim();
9156
- if (sharedInternal) {
9157
- return sharedInternal;
9158
- }
9159
- throw new Error(
9160
- "Cannot determine sandbox egress secret (set JUNIOR_SANDBOX_EGRESS_SECRET or JUNIOR_INTERNAL_RESUME_SECRET)"
9161
- );
9631
+ throw new Error("Cannot determine sandbox egress secret (set JUNIOR_SECRET)");
9162
9632
  }
9163
9633
  function base64Url(input) {
9164
9634
  return Buffer.from(input, "utf8").toString("base64url");
@@ -9167,7 +9637,7 @@ function fromBase64Url(input) {
9167
9637
  return Buffer.from(input, "base64url").toString("utf8");
9168
9638
  }
9169
9639
  function signPayload(payload) {
9170
- return createHmac("sha256", getSandboxEgressSecret()).update(payload).digest("base64url");
9640
+ return createHmac("sha256", getSandboxEgressSecret()).update(`${SANDBOX_EGRESS_HMAC_CONTEXT}:${payload}`).digest("base64url");
9171
9641
  }
9172
9642
  function timingSafeMatch(expected, actual) {
9173
9643
  const expectedBuffer = Buffer.from(expected);
@@ -11207,319 +11677,123 @@ var AuthorizationPauseError = class extends Error {
11207
11677
  this.kind = kind;
11208
11678
  this.provider = provider;
11209
11679
  }
11210
- };
11211
-
11212
- // src/chat/runtime/report-progress.ts
11213
- function buildReportedProgressStatus(input) {
11214
- if (!input || typeof input !== "object") {
11215
- return void 0;
11216
- }
11217
- const message = input.message;
11218
- if (typeof message !== "string") {
11219
- return void 0;
11220
- }
11221
- const text = message.trim();
11222
- if (!text) {
11223
- return void 0;
11224
- }
11225
- return { text };
11226
- }
11227
-
11228
- // src/chat/tools/execution/build-sandbox-input.ts
11229
- function buildSandboxInput(toolName, params) {
11230
- const optionalNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
11231
- if (toolName === "bash") {
11232
- return {
11233
- command: String(params.command ?? ""),
11234
- ...optionalNumber(params.timeoutMs) ? { timeoutMs: optionalNumber(params.timeoutMs) } : {}
11235
- };
11236
- }
11237
- if (toolName === "readFile") {
11238
- return {
11239
- path: String(params.path ?? ""),
11240
- ...optionalNumber(params.offset) ? { offset: optionalNumber(params.offset) } : {},
11241
- ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
11242
- };
11243
- }
11244
- if (toolName === "editFile") {
11245
- return {
11246
- path: String(params.path ?? ""),
11247
- edits: Array.isArray(params.edits) ? params.edits : []
11248
- };
11249
- }
11250
- if (toolName === "grep") {
11251
- return {
11252
- pattern: String(params.pattern ?? ""),
11253
- ...typeof params.path === "string" ? { path: params.path } : {},
11254
- ...typeof params.glob === "string" ? { glob: params.glob } : {},
11255
- ...typeof params.ignoreCase === "boolean" ? { ignoreCase: params.ignoreCase } : {},
11256
- ...typeof params.literal === "boolean" ? { literal: params.literal } : {},
11257
- ...optionalNumber(params.context) ? { context: optionalNumber(params.context) } : {},
11258
- ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
11259
- };
11260
- }
11261
- if (toolName === "findFiles") {
11262
- return {
11263
- pattern: String(params.pattern ?? ""),
11264
- ...typeof params.path === "string" ? { path: params.path } : {},
11265
- ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
11266
- };
11267
- }
11268
- if (toolName === "listDir") {
11269
- return {
11270
- ...typeof params.path === "string" ? { path: params.path } : {},
11271
- ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
11272
- };
11273
- }
11274
- if (toolName === "writeFile") {
11275
- return {
11276
- path: String(params.path ?? ""),
11277
- content: String(params.content ?? "")
11278
- };
11279
- }
11280
- return params;
11281
- }
11282
-
11283
- // src/chat/tools/execution/normalize-result.ts
11284
- function isStructuredToolExecutionResult(value) {
11285
- const content = value?.content;
11286
- return typeof value === "object" && value !== null && Array.isArray(content) && content.every((part) => {
11287
- if (!part || typeof part !== "object") {
11288
- return false;
11289
- }
11290
- const record = part;
11291
- if (record.type === "text") {
11292
- return typeof record.text === "string";
11293
- }
11294
- if (record.type === "image") {
11295
- return typeof record.data === "string" && typeof record.mimeType === "string";
11296
- }
11297
- return false;
11298
- }) && "details" in value;
11299
- }
11300
- function toToolContentText(value) {
11301
- if (typeof value === "string") return value;
11302
- try {
11303
- return JSON.stringify(value);
11304
- } catch {
11305
- return String(value);
11306
- }
11307
- }
11308
- function normalizeToolResult(result, isSandboxResult) {
11309
- const unwrapped = isSandboxResult && result && typeof result === "object" && "result" in result ? result.result : result;
11310
- if (isStructuredToolExecutionResult(unwrapped)) {
11311
- return unwrapped;
11312
- }
11313
- return {
11314
- content: [{ type: "text", text: toToolContentText(unwrapped) }],
11315
- details: unwrapped
11316
- };
11317
- }
11318
-
11319
- // src/chat/credentials/unlink-provider.ts
11320
- async function unlinkProvider(userId, provider, userTokenStore) {
11321
- await Promise.all([
11322
- userTokenStore.delete(userId, provider),
11323
- deleteMcpStoredOAuthCredentials(userId, provider),
11324
- deleteMcpServerSessionId(userId, provider),
11325
- deleteMcpAuthSessionsForUserProvider(userId, provider)
11326
- ]);
11327
- }
11328
-
11329
- // src/chat/state/turn-session-store.ts
11330
- var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
11331
- var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
11332
- function agentTurnSessionKey(conversationId, sessionId) {
11333
- return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
11334
- }
11335
- function toFiniteNonNegativeNumber(value) {
11336
- return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
11337
- }
11338
- function parseAgentTurnUsage(value) {
11339
- if (!isRecord(value)) {
11340
- return void 0;
11341
- }
11342
- const usage = {};
11343
- for (const field of [
11344
- "inputTokens",
11345
- "outputTokens",
11346
- "cachedInputTokens",
11347
- "cacheCreationTokens",
11348
- "totalTokens"
11349
- ]) {
11350
- const count = toFiniteNonNegativeNumber(value[field]);
11351
- if (count !== void 0) {
11352
- usage[field] = count;
11353
- }
11354
- }
11355
- return Object.keys(usage).length > 0 ? usage : void 0;
11356
- }
11357
- function parseAgentTurnSessionCheckpoint(value) {
11358
- if (typeof value !== "string") {
11359
- return void 0;
11360
- }
11361
- try {
11362
- const parsed = JSON.parse(value);
11363
- if (!isRecord(parsed)) {
11364
- return void 0;
11365
- }
11366
- const status = parsed.state;
11367
- if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed" && status !== "superseded") {
11368
- return void 0;
11369
- }
11370
- const conversationId = parsed.conversationId;
11371
- const sessionId = parsed.sessionId;
11372
- const sliceId = parsed.sliceId;
11373
- const checkpointVersion = parsed.checkpointVersion;
11374
- const updatedAtMs = parsed.updatedAtMs;
11375
- const cumulativeDurationMs = toFiniteNonNegativeNumber(
11376
- parsed.cumulativeDurationMs
11377
- );
11378
- const cumulativeUsage = parseAgentTurnUsage(parsed.cumulativeUsage);
11379
- if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
11380
- return void 0;
11381
- }
11382
- return {
11383
- checkpointVersion,
11384
- conversationId,
11385
- sessionId,
11386
- sliceId,
11387
- state: status,
11388
- updatedAtMs,
11389
- ...cumulativeDurationMs !== void 0 ? { cumulativeDurationMs } : {},
11390
- ...cumulativeUsage ? { cumulativeUsage } : {},
11391
- piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
11392
- ...Array.isArray(parsed.loadedSkillNames) ? {
11393
- loadedSkillNames: parsed.loadedSkillNames.filter(
11394
- (value2) => typeof value2 === "string"
11395
- )
11396
- } : {},
11397
- ...parsed.resumeReason === "timeout" || parsed.resumeReason === "auth" ? { resumeReason: parsed.resumeReason } : {},
11398
- ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
11399
- ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
11400
- };
11401
- } catch {
11402
- return void 0;
11403
- }
11404
- }
11405
- async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
11406
- const stateAdapter = getStateAdapter();
11407
- await stateAdapter.connect();
11408
- const value = await stateAdapter.get(
11409
- agentTurnSessionKey(conversationId, sessionId)
11410
- );
11411
- return parseAgentTurnSessionCheckpoint(value);
11412
- }
11413
- async function upsertAgentTurnSessionCheckpoint(args) {
11414
- const stateAdapter = getStateAdapter();
11415
- await stateAdapter.connect();
11416
- const existing = await getAgentTurnSessionCheckpoint(
11417
- args.conversationId,
11418
- args.sessionId
11419
- );
11420
- const checkpoint = {
11421
- checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
11422
- conversationId: args.conversationId,
11423
- sessionId: args.sessionId,
11424
- sliceId: args.sliceId,
11425
- state: args.state,
11426
- updatedAtMs: Date.now(),
11427
- piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
11428
- ...typeof args.cumulativeDurationMs === "number" && Number.isFinite(args.cumulativeDurationMs) ? {
11429
- cumulativeDurationMs: Math.max(
11430
- 0,
11431
- Math.floor(args.cumulativeDurationMs)
11432
- )
11433
- } : {},
11434
- ...args.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage } : {},
11435
- ...Array.isArray(args.loadedSkillNames) ? {
11436
- loadedSkillNames: args.loadedSkillNames.filter(
11437
- (value) => typeof value === "string"
11438
- )
11439
- } : {},
11440
- ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
11441
- ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
11442
- ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
11443
- };
11444
- const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
11445
- await stateAdapter.set(
11446
- agentTurnSessionKey(args.conversationId, args.sessionId),
11447
- JSON.stringify(checkpoint),
11448
- ttlMs
11449
- );
11450
- return checkpoint;
11451
- }
11452
- async function supersedeAgentTurnSessionCheckpoint(args) {
11453
- const existing = await getAgentTurnSessionCheckpoint(
11454
- args.conversationId,
11455
- args.sessionId
11456
- );
11457
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "superseded") {
11458
- return void 0;
11459
- }
11460
- return await upsertAgentTurnSessionCheckpoint({
11461
- conversationId: existing.conversationId,
11462
- sessionId: existing.sessionId,
11463
- sliceId: existing.sliceId,
11464
- state: "superseded",
11465
- piMessages: existing.piMessages,
11466
- cumulativeDurationMs: existing.cumulativeDurationMs,
11467
- cumulativeUsage: existing.cumulativeUsage,
11468
- loadedSkillNames: existing.loadedSkillNames,
11469
- resumeReason: existing.resumeReason,
11470
- resumedFromSliceId: existing.resumedFromSliceId,
11471
- errorMessage: args.errorMessage ?? existing.errorMessage
11472
- });
11473
- }
11680
+ };
11474
11681
 
11475
- // src/chat/services/pending-auth.ts
11476
- var AUTH_LINK_REUSE_WINDOW_MS = 10 * 60 * 1e3;
11477
- function canReusePendingAuthLink(args) {
11478
- const { pendingAuth } = args;
11479
- if (!pendingAuth) {
11480
- return false;
11682
+ // src/chat/runtime/report-progress.ts
11683
+ function buildReportedProgressStatus(input) {
11684
+ if (!input || typeof input !== "object") {
11685
+ return void 0;
11481
11686
  }
11482
- return pendingAuth.kind === args.kind && pendingAuth.provider === args.provider && pendingAuth.requesterId === args.requesterId && pendingAuth.linkSentAtMs + AUTH_LINK_REUSE_WINDOW_MS > (args.nowMs ?? Date.now());
11483
- }
11484
- function getConversationPendingAuth(args) {
11485
- const pendingAuth = args.conversation.processing.pendingAuth;
11486
- if (!pendingAuth) {
11687
+ const message = input.message;
11688
+ if (typeof message !== "string") {
11487
11689
  return void 0;
11488
11690
  }
11489
- if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId) {
11691
+ const text = message.trim();
11692
+ if (!text) {
11490
11693
  return void 0;
11491
11694
  }
11492
- return pendingAuth;
11695
+ return { text };
11493
11696
  }
11494
- function clearPendingAuth(conversation, sessionId) {
11495
- if (!conversation.processing.pendingAuth) {
11496
- return;
11697
+
11698
+ // src/chat/tools/execution/build-sandbox-input.ts
11699
+ function buildSandboxInput(toolName, params) {
11700
+ const optionalNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
11701
+ if (toolName === "bash") {
11702
+ return {
11703
+ command: String(params.command ?? ""),
11704
+ ...optionalNumber(params.timeoutMs) ? { timeoutMs: optionalNumber(params.timeoutMs) } : {}
11705
+ };
11497
11706
  }
11498
- if (sessionId && conversation.processing.pendingAuth.sessionId !== sessionId) {
11499
- return;
11707
+ if (toolName === "readFile") {
11708
+ return {
11709
+ path: String(params.path ?? ""),
11710
+ ...optionalNumber(params.offset) ? { offset: optionalNumber(params.offset) } : {},
11711
+ ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
11712
+ };
11500
11713
  }
11501
- conversation.processing.pendingAuth = void 0;
11502
- }
11503
- async function applyPendingAuthUpdate(args) {
11504
- const previousPendingAuth = args.conversation.processing.pendingAuth;
11505
- args.conversation.processing.pendingAuth = args.nextPendingAuth;
11506
- if (previousPendingAuth && previousPendingAuth.sessionId !== args.nextPendingAuth.sessionId && args.conversationId) {
11507
- await supersedeAgentTurnSessionCheckpoint({
11508
- conversationId: args.conversationId,
11509
- sessionId: previousPendingAuth.sessionId,
11510
- errorMessage: "Superseded by a newer auth-blocked request in the same conversation."
11511
- });
11714
+ if (toolName === "editFile") {
11715
+ return {
11716
+ path: String(params.path ?? ""),
11717
+ edits: Array.isArray(params.edits) ? params.edits : []
11718
+ };
11719
+ }
11720
+ if (toolName === "grep") {
11721
+ return {
11722
+ pattern: String(params.pattern ?? ""),
11723
+ ...typeof params.path === "string" ? { path: params.path } : {},
11724
+ ...typeof params.glob === "string" ? { glob: params.glob } : {},
11725
+ ...typeof params.ignoreCase === "boolean" ? { ignoreCase: params.ignoreCase } : {},
11726
+ ...typeof params.literal === "boolean" ? { literal: params.literal } : {},
11727
+ ...optionalNumber(params.context) ? { context: optionalNumber(params.context) } : {},
11728
+ ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
11729
+ };
11730
+ }
11731
+ if (toolName === "findFiles") {
11732
+ return {
11733
+ pattern: String(params.pattern ?? ""),
11734
+ ...typeof params.path === "string" ? { path: params.path } : {},
11735
+ ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
11736
+ };
11737
+ }
11738
+ if (toolName === "listDir") {
11739
+ return {
11740
+ ...typeof params.path === "string" ? { path: params.path } : {},
11741
+ ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
11742
+ };
11743
+ }
11744
+ if (toolName === "writeFile") {
11745
+ return {
11746
+ path: String(params.path ?? ""),
11747
+ content: String(params.content ?? "")
11748
+ };
11512
11749
  }
11750
+ return params;
11513
11751
  }
11514
- function isPendingAuthLatestRequest(conversation, pendingAuth) {
11515
- for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
11516
- const message = conversation.messages[index];
11517
- if (message?.role !== "user") {
11518
- continue;
11752
+
11753
+ // src/chat/tools/execution/normalize-result.ts
11754
+ function isStructuredToolExecutionResult(value) {
11755
+ const content = value?.content;
11756
+ return typeof value === "object" && value !== null && Array.isArray(content) && content.every((part) => {
11757
+ if (!part || typeof part !== "object") {
11758
+ return false;
11519
11759
  }
11520
- return buildDeterministicTurnId(message.id) === pendingAuth.sessionId;
11760
+ const record = part;
11761
+ if (record.type === "text") {
11762
+ return typeof record.text === "string";
11763
+ }
11764
+ if (record.type === "image") {
11765
+ return typeof record.data === "string" && typeof record.mimeType === "string";
11766
+ }
11767
+ return false;
11768
+ }) && "details" in value;
11769
+ }
11770
+ function toToolContentText(value) {
11771
+ if (typeof value === "string") return value;
11772
+ try {
11773
+ return JSON.stringify(value);
11774
+ } catch {
11775
+ return String(value);
11521
11776
  }
11522
- return false;
11777
+ }
11778
+ function normalizeToolResult(result, isSandboxResult) {
11779
+ const unwrapped = isSandboxResult && result && typeof result === "object" && "result" in result ? result.result : result;
11780
+ if (isStructuredToolExecutionResult(unwrapped)) {
11781
+ return unwrapped;
11782
+ }
11783
+ return {
11784
+ content: [{ type: "text", text: toToolContentText(unwrapped) }],
11785
+ details: unwrapped
11786
+ };
11787
+ }
11788
+
11789
+ // src/chat/credentials/unlink-provider.ts
11790
+ async function unlinkProvider(userId, provider, userTokenStore) {
11791
+ await Promise.all([
11792
+ userTokenStore.delete(userId, provider),
11793
+ deleteMcpStoredOAuthCredentials(userId, provider),
11794
+ deleteMcpServerSessionId(userId, provider),
11795
+ deleteMcpAuthSessionsForUserProvider(userId, provider)
11796
+ ]);
11523
11797
  }
11524
11798
 
11525
11799
  // src/chat/services/plugin-auth-orchestration.ts
@@ -12000,7 +12274,6 @@ function buildBriefPostCanvasReply(artifactStatePatch) {
12000
12274
  function buildTurnResult(input) {
12001
12275
  const {
12002
12276
  newMessages,
12003
- piMessages,
12004
12277
  userInput,
12005
12278
  replyFiles,
12006
12279
  artifactStatePatch,
@@ -12035,7 +12308,11 @@ function buildTurnResult(input) {
12035
12308
  hasFiles: replyFiles.length > 0
12036
12309
  });
12037
12310
  const sideEffectOnlySuccess = !primaryText && toolErrorCount === 0 && (reactionPerformed || channelPostPerformed || replyFiles.length > 0);
12038
- if (!primaryText && !sideEffectOnlySuccess) {
12311
+ const lastAssistant = terminalAssistantMessages.at(-1);
12312
+ const stopReason = typeof lastAssistant?.stopReason === "string" ? lastAssistant.stopReason : void 0;
12313
+ const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
12314
+ const isProviderError = stopReason === "error";
12315
+ if (!primaryText && !sideEffectOnlySuccess && !isProviderError) {
12039
12316
  logWarn(
12040
12317
  "ai_model_response_empty",
12041
12318
  {
@@ -12054,11 +12331,15 @@ function buildTurnResult(input) {
12054
12331
  "Model returned empty text response"
12055
12332
  );
12056
12333
  }
12057
- const lastAssistant = terminalAssistantMessages.at(-1);
12058
- const stopReason = typeof lastAssistant?.stopReason === "string" ? lastAssistant.stopReason : void 0;
12059
- const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
12060
12334
  const usedPrimaryText = Boolean(primaryText);
12061
- const outcome = primaryText ? stopReason === "error" ? "provider_error" : "success" : sideEffectOnlySuccess ? "success" : "execution_failure";
12335
+ let outcome;
12336
+ if (isProviderError) {
12337
+ outcome = "provider_error";
12338
+ } else if (primaryText || sideEffectOnlySuccess) {
12339
+ outcome = "success";
12340
+ } else {
12341
+ outcome = "execution_failure";
12342
+ }
12062
12343
  const suppressReactionOnlyText = reactionPerformed && !channelPostPerformed && replyFiles.length === 0 && Boolean(primaryText) && isReactionOnlyIntent(userInput);
12063
12344
  const rawResponseText = suppressReactionOnlyText ? "" : primaryText;
12064
12345
  const responseText = canvasCreated && isVerbosePostCanvasReply(rawResponseText) ? buildBriefPostCanvasReply(artifactStatePatch) : rawResponseText;
@@ -12104,7 +12385,6 @@ function buildTurnResult(input) {
12104
12385
  text: resolvedText,
12105
12386
  files: replyFiles.length > 0 ? replyFiles : void 0,
12106
12387
  artifactStatePatch: Object.keys(artifactStatePatch).length > 0 ? artifactStatePatch : void 0,
12107
- piMessages,
12108
12388
  deliveryPlan,
12109
12389
  deliveryMode,
12110
12390
  sandboxId,
@@ -12113,6 +12393,27 @@ function buildTurnResult(input) {
12113
12393
  };
12114
12394
  }
12115
12395
 
12396
+ // src/chat/services/provider-retry.ts
12397
+ var RETRYABLE_PROVIDER_ERROR_PATTERN = /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i;
12398
+ var NON_RETRYABLE_PROVIDER_ERROR_PATTERN = /invalid.?api.?key|authentication|authorization|permission|forbidden|context.?length|context.?window|content.?policy|validation|bad request|400|401|403/i;
12399
+ function isRetryableProviderError(message) {
12400
+ if (message?.stopReason !== "error" || !message.errorMessage) {
12401
+ return false;
12402
+ }
12403
+ if (NON_RETRYABLE_PROVIDER_ERROR_PATTERN.test(message.errorMessage)) {
12404
+ return false;
12405
+ }
12406
+ return RETRYABLE_PROVIDER_ERROR_PATTERN.test(message.errorMessage);
12407
+ }
12408
+ function trimRetryableProviderErrorTail(messages) {
12409
+ const trimmed = trimTrailingAssistantMessages(messages);
12410
+ if (trimmed.length === messages.length) {
12411
+ return void 0;
12412
+ }
12413
+ const tailRole = getPiMessageRole(trimmed.at(-1));
12414
+ return tailRole === "user" || tailRole === "toolResult" ? trimmed : void 0;
12415
+ }
12416
+
12116
12417
  // src/chat/services/turn-thinking-level.ts
12117
12418
  import { z } from "zod";
12118
12419
  var CLASSIFIER_CONFIDENCE_THRESHOLD = 0.75;
@@ -12516,7 +12817,7 @@ async function persistAuthPauseCheckpoint(args) {
12516
12817
  const piMessages = trimTrailingAssistantMessages(
12517
12818
  args.messages.length > 0 ? args.messages : latestCheckpoint?.piMessages ?? []
12518
12819
  );
12519
- await upsertAgentTurnSessionCheckpoint({
12820
+ return await upsertAgentTurnSessionCheckpoint({
12520
12821
  conversationId: args.conversationId,
12521
12822
  cumulativeDurationMs: addDurationMs(
12522
12823
  latestCheckpoint?.cumulativeDurationMs,
@@ -12547,7 +12848,7 @@ async function persistAuthPauseCheckpoint(args) {
12547
12848
  "Failed to persist auth checkpoint before retry"
12548
12849
  );
12549
12850
  }
12550
- return nextSliceId;
12851
+ return void 0;
12551
12852
  }
12552
12853
  async function persistTimeoutCheckpoint(args) {
12553
12854
  const nextSliceId = args.currentSliceId + 1;
@@ -12686,6 +12987,10 @@ function createMcpAuthOrchestration(deps, abortAgent) {
12686
12987
  }
12687
12988
 
12688
12989
  // src/chat/respond.ts
12990
+ var PROVIDER_RETRY_DELAYS_MS = [1e3, 2e3];
12991
+ function sleep3(ms) {
12992
+ return new Promise((resolve) => setTimeout(resolve, ms));
12993
+ }
12689
12994
  var startupDiscoveryLogged = false;
12690
12995
  var MAX_ROUTER_ATTACHMENT_PREVIEW_CHARS = 2e3;
12691
12996
  function buildOmittedImageAttachmentNotice(count) {
@@ -12787,69 +13092,6 @@ function buildUserTurnInput(args) {
12787
13092
  }
12788
13093
  return { routerBlocks, userContentParts };
12789
13094
  }
12790
- function refreshCheckpointTurnContext(messages, turnContextPrompt) {
12791
- const marker = getTurnContextMarker(turnContextPrompt);
12792
- for (let index = 0; index < messages.length; index += 1) {
12793
- const content = getUserMessageContent(messages[index]);
12794
- if (!content) {
12795
- continue;
12796
- }
12797
- const contextIndex = content.findIndex(
12798
- (part) => isTurnContextPart(part, marker)
12799
- );
12800
- if (contextIndex < 0) {
12801
- continue;
12802
- }
12803
- const updatedMessages = [...messages];
12804
- const updatedContent = [...content];
12805
- updatedContent[contextIndex] = {
12806
- ...updatedContent[contextIndex],
12807
- text: turnContextPrompt
12808
- };
12809
- updatedMessages[index] = {
12810
- ...messages[index],
12811
- content: updatedContent
12812
- };
12813
- return updatedMessages;
12814
- }
12815
- return [
12816
- ...messages,
12817
- {
12818
- role: "user",
12819
- content: [{ type: "text", text: turnContextPrompt }],
12820
- timestamp: Date.now()
12821
- }
12822
- ];
12823
- }
12824
- function stripTurnContextFromMessages(messages, turnContextPrompt) {
12825
- const marker = getTurnContextMarker(turnContextPrompt);
12826
- return messages.flatMap((message) => {
12827
- const content = getUserMessageContent(message);
12828
- if (!content) {
12829
- return [message];
12830
- }
12831
- const strippedContent = content.filter(
12832
- (part) => !isTurnContextPart(part, marker)
12833
- );
12834
- if (strippedContent.length === content.length) {
12835
- return [message];
12836
- }
12837
- if (strippedContent.length === 0) {
12838
- return [];
12839
- }
12840
- return [{ ...message, content: strippedContent }];
12841
- });
12842
- }
12843
- function getTurnContextMarker(turnContextPrompt) {
12844
- return turnContextPrompt.split("\n", 1)[0];
12845
- }
12846
- function getUserMessageContent(message) {
12847
- const record = message;
12848
- return record.role === "user" && Array.isArray(record.content) ? record.content : void 0;
12849
- }
12850
- function isTurnContextPart(part, marker) {
12851
- return part !== null && typeof part === "object" && part.type === "text" && typeof part.text === "string" && part.text.startsWith(marker);
12852
- }
12853
13095
  async function generateAssistantReply(messageText, context = {}) {
12854
13096
  const replyStartedAtMs = Date.now();
12855
13097
  let timeoutResumeConversationId;
@@ -13046,11 +13288,7 @@ async function generateAssistantReply(messageText, context = {}) {
13046
13288
  const promptConversationContext = context.piMessages && context.piMessages.length > 0 ? void 0 : context.conversationContext;
13047
13289
  const userTurnText = buildUserTurnText(
13048
13290
  userInput,
13049
- promptConversationContext,
13050
- {
13051
- sessionContext: { conversationId: sessionConversationId },
13052
- turnContext: { traceId: getActiveTraceId() }
13053
- }
13291
+ promptConversationContext
13054
13292
  );
13055
13293
  const { routerBlocks, userContentParts } = buildUserTurnInput({
13056
13294
  omittedImageAttachmentCount: context.omittedImageAttachmentCount ?? 0,
@@ -13235,11 +13473,8 @@ async function generateAssistantReply(messageText, context = {}) {
13235
13473
  activeMcpCatalogs,
13236
13474
  toolGuidance,
13237
13475
  runtime: {
13238
- channelId: toolChannelId,
13239
- fastModelId: botConfig.fastModelId,
13240
- modelId: botConfig.modelId,
13241
- slackCapabilities: channelCapabilities,
13242
- thinkingLevel: thinkingSelection.thinkingLevel
13476
+ conversationId: spanContext.conversationId,
13477
+ traceId: getActiveTraceId()
13243
13478
  },
13244
13479
  invocation: skillInvocation,
13245
13480
  requester: context.requester,
@@ -13362,7 +13597,7 @@ async function generateAssistantReply(messageText, context = {}) {
13362
13597
  beforeMessageCount = agent.state.messages.length;
13363
13598
  try {
13364
13599
  if (resumedFromCheckpoint) {
13365
- agent.state.messages = refreshCheckpointTurnContext(
13600
+ agent.state.messages = refreshRuntimeTurnContext(
13366
13601
  existingCheckpoint.piMessages,
13367
13602
  turnContextPrompt
13368
13603
  );
@@ -13387,67 +13622,96 @@ async function generateAssistantReply(messageText, context = {}) {
13387
13622
  freshPromptMessage
13388
13623
  ]);
13389
13624
  }
13390
- const promptPromise = resumedFromCheckpoint ? agent.continue() : agent.prompt(freshPromptMessage);
13391
- let timeoutId;
13392
- const timeoutPromise = new Promise((_, reject) => {
13393
- timeoutId = setTimeout(() => {
13394
- timedOut = true;
13395
- agent.abort();
13396
- reject(
13397
- new Error(
13398
- `Agent turn timed out after ${botConfig.turnTimeoutMs}ms`
13399
- )
13400
- );
13401
- }, botConfig.turnTimeoutMs);
13402
- });
13403
- try {
13404
- promptResult = await Promise.race([promptPromise, timeoutPromise]);
13405
- } catch (error) {
13406
- if (timedOut) {
13407
- logWarn(
13408
- "agent_turn_timeout",
13409
- {},
13410
- {
13411
- "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
13412
- "gen_ai.operation.name": "invoke_agent",
13413
- "gen_ai.request.model": botConfig.modelId,
13414
- ...thinkingSelection ? {
13415
- "app.ai.reasoning_effort": thinkingSelection.thinkingLevel
13416
- } : {},
13417
- "app.ai.turn_timeout_ms": botConfig.turnTimeoutMs
13418
- },
13419
- "Agent turn timed out and was aborted"
13420
- );
13421
- await promptPromise.catch(() => {
13422
- });
13423
- timeoutResumeMessages = [...agent.state.messages];
13625
+ const runAgentStep = async (run2) => {
13626
+ let timeoutId;
13627
+ const timeoutPromise = new Promise((_, reject) => {
13628
+ timeoutId = setTimeout(() => {
13629
+ timedOut = true;
13630
+ agent.abort();
13631
+ reject(
13632
+ new Error(
13633
+ `Agent turn timed out after ${botConfig.turnTimeoutMs}ms`
13634
+ )
13635
+ );
13636
+ }, botConfig.turnTimeoutMs);
13637
+ });
13638
+ try {
13639
+ return await Promise.race([run2, timeoutPromise]);
13640
+ } catch (error) {
13641
+ if (timedOut) {
13642
+ logWarn(
13643
+ "agent_turn_timeout",
13644
+ {},
13645
+ {
13646
+ "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
13647
+ "gen_ai.operation.name": "invoke_agent",
13648
+ "gen_ai.request.model": botConfig.modelId,
13649
+ ...thinkingSelection ? {
13650
+ "app.ai.reasoning_effort": thinkingSelection.thinkingLevel
13651
+ } : {},
13652
+ "app.ai.turn_timeout_ms": botConfig.turnTimeoutMs
13653
+ },
13654
+ "Agent turn timed out and was aborted"
13655
+ );
13656
+ await run2.catch(() => {
13657
+ });
13658
+ timeoutResumeMessages = [...agent.state.messages];
13659
+ }
13660
+ if (getPendingAuthPause()) {
13661
+ timeoutResumeMessages = [...agent.state.messages];
13662
+ throw getPendingAuthPause();
13663
+ }
13664
+ throw error;
13665
+ } finally {
13666
+ if (timeoutId) {
13667
+ clearTimeout(timeoutId);
13668
+ }
13424
13669
  }
13670
+ };
13671
+ let run = resumedFromCheckpoint ? agent.continue() : agent.prompt(freshPromptMessage);
13672
+ let retryUsage;
13673
+ for (let attempt = 0; ; attempt += 1) {
13674
+ promptResult = await runAgentStep(run);
13675
+ newMessages = agent.state.messages.slice(beforeMessageCount);
13676
+ const outputMessages = newMessages.filter(isAssistantMessage);
13677
+ const outputMessagesAttribute = serializeGenAiAttribute(outputMessages);
13678
+ const usageSummary = extractGenAiUsageSummary(
13679
+ promptResult,
13680
+ agent.state,
13681
+ ...outputMessages
13682
+ );
13683
+ const currentUsage = hasAgentTurnUsage(usageSummary) ? usageSummary : void 0;
13684
+ turnUsage = addAgentTurnUsage(retryUsage, currentUsage);
13685
+ setSpanAttributes({
13686
+ ...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : {},
13687
+ ...extractGenAiUsageAttributes(usageSummary)
13688
+ });
13425
13689
  if (getPendingAuthPause()) {
13426
13690
  timeoutResumeMessages = [...agent.state.messages];
13427
13691
  throw getPendingAuthPause();
13428
13692
  }
13429
- throw error;
13430
- } finally {
13431
- if (timeoutId) {
13432
- clearTimeout(timeoutId);
13693
+ const lastAssistant = outputMessages.at(-1);
13694
+ const retryDelayMs = PROVIDER_RETRY_DELAYS_MS[attempt];
13695
+ if (retryDelayMs === void 0 || !isRetryableProviderError(lastAssistant)) {
13696
+ break;
13433
13697
  }
13434
- }
13435
- newMessages = agent.state.messages.slice(beforeMessageCount);
13436
- const outputMessages = newMessages.filter(isAssistantMessage);
13437
- const outputMessagesAttribute = serializeGenAiAttribute(outputMessages);
13438
- const usageSummary = extractGenAiUsageSummary(
13439
- promptResult,
13440
- agent.state,
13441
- ...outputMessages
13442
- );
13443
- turnUsage = hasAgentTurnUsage(usageSummary) ? usageSummary : void 0;
13444
- setSpanAttributes({
13445
- ...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : {},
13446
- ...extractGenAiUsageAttributes(usageSummary)
13447
- });
13448
- if (getPendingAuthPause()) {
13449
- timeoutResumeMessages = [...agent.state.messages];
13450
- throw getPendingAuthPause();
13698
+ retryUsage = turnUsage;
13699
+ const retryMessages = trimRetryableProviderErrorTail(
13700
+ agent.state.messages
13701
+ );
13702
+ if (!retryMessages) {
13703
+ break;
13704
+ }
13705
+ agent.state.messages = retryMessages;
13706
+ await persistSafeBoundary(retryMessages);
13707
+ logWarn(
13708
+ "agent_turn_provider_retry",
13709
+ spanContext,
13710
+ {},
13711
+ "Retrying transient provider failure"
13712
+ );
13713
+ await sleep3(retryDelayMs);
13714
+ run = agent.continue();
13451
13715
  }
13452
13716
  },
13453
13717
  {
@@ -13475,10 +13739,6 @@ async function generateAssistantReply(messageText, context = {}) {
13475
13739
  }
13476
13740
  return buildTurnResult({
13477
13741
  newMessages,
13478
- piMessages: stripTurnContextFromMessages(
13479
- agent.state.messages,
13480
- turnContextPrompt
13481
- ),
13482
13742
  userInput,
13483
13743
  replyFiles,
13484
13744
  artifactStatePatch,
@@ -13528,7 +13788,7 @@ async function generateAssistantReply(messageText, context = {}) {
13528
13788
  beforeMessageCount
13529
13789
  );
13530
13790
  }
13531
- const nextSliceId = await persistAuthPauseCheckpoint({
13791
+ const checkpoint = await persistAuthPauseCheckpoint({
13532
13792
  conversationId: timeoutResumeConversationId,
13533
13793
  sessionId: timeoutResumeSessionId,
13534
13794
  currentSliceId: timeoutResumeSliceId,
@@ -13539,21 +13799,23 @@ async function generateAssistantReply(messageText, context = {}) {
13539
13799
  errorMessage: error.message,
13540
13800
  logContext: checkpointLogContext
13541
13801
  });
13542
- throw new RetryableTurnError(
13543
- error.kind === "plugin" ? "plugin_auth_resume" : "mcp_auth_resume",
13544
- `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`,
13545
- {
13546
- authDisposition: error.disposition,
13547
- authDurationMs: Date.now() - replyStartedAtMs,
13548
- authKind: error.kind,
13549
- authProvider: error.provider,
13550
- authThinkingLevel: thinkingSelection?.thinkingLevel,
13551
- authUsage: turnUsage,
13552
- conversationId: timeoutResumeConversationId,
13553
- sessionId: timeoutResumeSessionId,
13554
- sliceId: nextSliceId
13555
- }
13556
- );
13802
+ if (checkpoint) {
13803
+ throw new RetryableTurnError(
13804
+ error.kind === "plugin" ? "plugin_auth_resume" : "mcp_auth_resume",
13805
+ `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${checkpoint.sliceId}`,
13806
+ {
13807
+ authDisposition: error.disposition,
13808
+ authDurationMs: Date.now() - replyStartedAtMs,
13809
+ authKind: error.kind,
13810
+ authProvider: error.provider,
13811
+ authThinkingLevel: thinkingSelection?.thinkingLevel,
13812
+ authUsage: turnUsage,
13813
+ conversationId: timeoutResumeConversationId,
13814
+ sessionId: timeoutResumeSessionId,
13815
+ sliceId: checkpoint.sliceId
13816
+ }
13817
+ );
13818
+ }
13557
13819
  }
13558
13820
  if (isRetryableTurnError(error)) {
13559
13821
  throw error;
@@ -14769,16 +15031,10 @@ function createResumeReplyContext(args, statusSession) {
14769
15031
  };
14770
15032
  }
14771
15033
  async function resumeSlackTurn(args) {
14772
- if (!args.replyContext?.requester?.userId) {
14773
- throw new Error("Resumed turn requires replyContext.requester.userId");
14774
- }
14775
15034
  const stateAdapter = getStateAdapter();
14776
15035
  await stateAdapter.connect();
14777
15036
  const lockKey = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
14778
- const lock = await stateAdapter.acquireLock(
14779
- lockKey,
14780
- botConfig.turnTimeoutMs + 6e4
14781
- );
15037
+ const lock = await stateAdapter.acquireLock(lockKey, ACTIVE_LOCK_TTL_MS);
14782
15038
  if (!lock) {
14783
15039
  throw new ResumeTurnBusyError(lockKey);
14784
15040
  }
@@ -14790,31 +15046,44 @@ async function resumeSlackTurn(args) {
14790
15046
  let deferredPauseKind;
14791
15047
  let deferredPauseHandler;
14792
15048
  let deferredFailureHandler;
15049
+ let finalReplyDelivered = false;
15050
+ let postDeliveryCommitError;
15051
+ let runArgs = args;
14793
15052
  try {
14794
- if (args.messageTs) {
15053
+ const preparedArgs = await args.beforeStart?.();
15054
+ if (preparedArgs === false) {
15055
+ return;
15056
+ }
15057
+ if (preparedArgs) {
15058
+ runArgs = { ...args, ...preparedArgs };
15059
+ }
15060
+ if (!runArgs.replyContext?.requester?.userId) {
15061
+ throw new Error("Resumed turn requires replyContext.requester.userId");
15062
+ }
15063
+ if (runArgs.messageTs) {
14795
15064
  processingReaction = await startSlackProcessingReactionForMessage({
14796
- channelId: args.channelId,
14797
- timestamp: args.messageTs,
15065
+ channelId: runArgs.channelId,
15066
+ timestamp: runArgs.messageTs,
14798
15067
  logException,
14799
- logContext: { ...getResumeLogContext(args, lockKey) }
15068
+ logContext: { ...getResumeLogContext(runArgs, lockKey) }
14800
15069
  });
14801
15070
  }
14802
- if (args.initialText) {
15071
+ if (runArgs.initialText) {
14803
15072
  await postSlackMessageBestEffort(
14804
- args.channelId,
14805
- args.threadTs,
14806
- args.initialText
15073
+ runArgs.channelId,
15074
+ runArgs.threadTs,
15075
+ runArgs.initialText
14807
15076
  );
14808
15077
  }
14809
15078
  status.start();
14810
- const generateReply = args.generateReply ?? generateAssistantReply;
14811
- const replyContext = createResumeReplyContext(args, status);
15079
+ const generateReply = runArgs.generateReply ?? generateAssistantReply;
15080
+ const replyContext = createResumeReplyContext(runArgs, status);
14812
15081
  const priorCheckpoint = replyContext.correlation?.conversationId && replyContext.correlation?.turnId ? await getAgentTurnSessionCheckpoint(
14813
15082
  replyContext.correlation.conversationId,
14814
15083
  replyContext.correlation.turnId
14815
15084
  ) : void 0;
14816
- const replyPromise = generateReply(args.messageText, replyContext);
14817
- const replyTimeoutMs = resolveReplyTimeoutMs(args.replyTimeoutMs);
15085
+ const replyPromise = generateReply(runArgs.messageText, replyContext);
15086
+ const replyTimeoutMs = resolveReplyTimeoutMs(runArgs.replyTimeoutMs);
14818
15087
  let reply = typeof replyTimeoutMs === "number" ? await Promise.race([
14819
15088
  replyPromise,
14820
15089
  new Promise(
@@ -14831,11 +15100,11 @@ async function resumeSlackTurn(args) {
14831
15100
  reply = finalizeFailedTurnReply({
14832
15101
  reply,
14833
15102
  logException,
14834
- context: getResumeLogContext(args, lockKey)
15103
+ context: getResumeLogContext(runArgs, lockKey)
14835
15104
  });
14836
15105
  await status.stop();
14837
15106
  const footer = buildSlackReplyFooter({
14838
- conversationId: args.replyContext?.correlation?.conversationId ?? lockKey,
15107
+ conversationId: runArgs.replyContext?.correlation?.conversationId ?? lockKey,
14839
15108
  durationMs: typeof priorCheckpoint?.cumulativeDurationMs === "number" || typeof reply.diagnostics.durationMs === "number" ? (priorCheckpoint?.cumulativeDurationMs ?? 0) + (reply.diagnostics.durationMs ?? 0) : void 0,
14840
15109
  thinkingLevel: reply.diagnostics.thinkingLevel,
14841
15110
  usage: addAgentTurnUsage(
@@ -14844,18 +15113,32 @@ async function resumeSlackTurn(args) {
14844
15113
  ) ?? reply.diagnostics.usage
14845
15114
  });
14846
15115
  await postSlackApiReplyPosts({
14847
- channelId: args.channelId,
14848
- threadTs: args.threadTs,
15116
+ channelId: runArgs.channelId,
15117
+ threadTs: runArgs.threadTs,
14849
15118
  posts: planSlackReplyPosts({ reply }),
14850
15119
  fileUploadFailureMode: "best_effort",
14851
15120
  footer
14852
15121
  });
14853
- await args.onSuccess?.(reply);
15122
+ finalReplyDelivered = true;
15123
+ await runArgs.onSuccess?.(reply);
14854
15124
  } catch (error) {
14855
15125
  await status.stop();
14856
- const onAuthPause = args.onAuthPause;
14857
- const onTimeoutPause = args.onTimeoutPause;
14858
- if ((isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) && onAuthPause) {
15126
+ const onAuthPause = runArgs.onAuthPause;
15127
+ const onTimeoutPause = runArgs.onTimeoutPause;
15128
+ if (finalReplyDelivered) {
15129
+ postDeliveryCommitError = error;
15130
+ try {
15131
+ await runArgs.onPostDeliveryCommitFailure?.(error);
15132
+ } catch (terminalizeError) {
15133
+ logException(
15134
+ terminalizeError,
15135
+ "slack_resume_post_delivery_terminalize_failed",
15136
+ getResumeLogContext(runArgs, lockKey),
15137
+ {},
15138
+ "Failed to terminalize resumed turn after post-delivery commit failure"
15139
+ );
15140
+ }
15141
+ } else if ((isRetryableTurnError(error, "mcp_auth_resume") || isRetryableTurnError(error, "plugin_auth_resume")) && onAuthPause) {
14859
15142
  deferredPauseKind = "auth";
14860
15143
  deferredPauseHandler = async () => {
14861
15144
  await onAuthPause(error);
@@ -14872,7 +15155,7 @@ async function resumeSlackTurn(args) {
14872
15155
  error,
14873
15156
  eventName: "slack_resume_turn_failed",
14874
15157
  lockKey,
14875
- resumeArgs: args
15158
+ resumeArgs: runArgs
14876
15159
  });
14877
15160
  };
14878
15161
  }
@@ -14880,20 +15163,30 @@ async function resumeSlackTurn(args) {
14880
15163
  await processingReaction?.stop();
14881
15164
  await stateAdapter.releaseLock(lock);
14882
15165
  }
15166
+ if (postDeliveryCommitError) {
15167
+ logException(
15168
+ postDeliveryCommitError,
15169
+ "slack_resume_success_handler_failed",
15170
+ getResumeLogContext(runArgs, lockKey),
15171
+ {},
15172
+ "Failed to persist resumed turn state after final reply delivery"
15173
+ );
15174
+ throw postDeliveryCommitError;
15175
+ }
14883
15176
  if (deferredPauseHandler) {
14884
15177
  try {
14885
15178
  await deferredPauseHandler();
14886
15179
  if (deferredPauseKind === "auth") {
14887
15180
  await postSlackMessageBestEffort(
14888
- args.channelId,
14889
- args.threadTs,
15181
+ runArgs.channelId,
15182
+ runArgs.threadTs,
14890
15183
  buildAuthPauseResponse()
14891
15184
  );
14892
15185
  }
14893
15186
  if (deferredPauseKind === "timeout") {
14894
15187
  await postTurnContinuationNoticeBestEffort({
14895
15188
  lockKey,
14896
- resumeArgs: args
15189
+ resumeArgs: runArgs
14897
15190
  });
14898
15191
  }
14899
15192
  return;
@@ -14903,7 +15196,7 @@ async function resumeSlackTurn(args) {
14903
15196
  error: pauseError,
14904
15197
  eventName: "slack_resume_pause_handler_failed",
14905
15198
  lockKey,
14906
- resumeArgs: args
15199
+ resumeArgs: runArgs
14907
15200
  });
14908
15201
  return;
14909
15202
  }
@@ -14926,6 +15219,8 @@ async function resumeAuthorizedRequest(args) {
14926
15219
  onFailure: args.onFailure,
14927
15220
  onAuthPause: args.onAuthPause,
14928
15221
  onTimeoutPause: args.onTimeoutPause,
15222
+ onPostDeliveryCommitFailure: args.onPostDeliveryCommitFailure,
15223
+ beforeStart: args.beforeStart,
14929
15224
  replyTimeoutMs: args.replyTimeoutMs
14930
15225
  });
14931
15226
  }
@@ -14940,7 +15235,7 @@ function completeAuthPauseTurn(args) {
14940
15235
  skippedReason: void 0
14941
15236
  }
14942
15237
  );
14943
- markTurnCompleted({
15238
+ markTurnClosed({
14944
15239
  conversation: args.conversation,
14945
15240
  nowMs: Date.now(),
14946
15241
  sessionId: args.sessionId,
@@ -14960,6 +15255,7 @@ async function persistAuthPauseTurnState(args) {
14960
15255
  // src/chat/services/timeout-resume.ts
14961
15256
  import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
14962
15257
  var TURN_TIMEOUT_RESUME_PATH = "/api/internal/turn-resume";
15258
+ var TURN_TIMEOUT_RESUME_HMAC_CONTEXT = "junior.turn_timeout_resume.v1";
14963
15259
  var TURN_TIMEOUT_RESUME_SIGNATURE_VERSION = "v1";
14964
15260
  var TURN_TIMEOUT_RESUME_MAX_SKEW_MS = 5 * 60 * 1e3;
14965
15261
  var TURN_TIMEOUT_RESUME_TIMESTAMP_HEADER = "x-junior-resume-timestamp";
@@ -14983,14 +15279,10 @@ async function getAwaitingTurnContinuationRequest(args) {
14983
15279
  };
14984
15280
  }
14985
15281
  function getTurnTimeoutResumeSecret() {
14986
- const explicit = process.env.JUNIOR_INTERNAL_RESUME_SECRET?.trim();
14987
- if (explicit) {
14988
- return explicit;
14989
- }
14990
- return getSlackSigningSecret();
15282
+ return process.env.JUNIOR_SECRET?.trim() || void 0;
14991
15283
  }
14992
15284
  function buildSignedPayload(timestamp, body) {
14993
- return `${timestamp}:${body}`;
15285
+ return `${TURN_TIMEOUT_RESUME_HMAC_CONTEXT}:${timestamp}:${body}`;
14994
15286
  }
14995
15287
  function signTurnTimeoutResumeBody(secret, timestamp, body) {
14996
15288
  const digest = createHmac2("sha256", secret).update(buildSignedPayload(timestamp, body)).digest("hex");
@@ -15028,7 +15320,7 @@ async function scheduleTurnTimeoutResume(request) {
15028
15320
  const secret = getTurnTimeoutResumeSecret();
15029
15321
  if (!secret) {
15030
15322
  throw new Error(
15031
- "Cannot determine timeout resume secret (set JUNIOR_INTERNAL_RESUME_SECRET or SLACK_SIGNING_SECRET)"
15323
+ "Cannot determine timeout resume secret (set JUNIOR_SECRET)"
15032
15324
  );
15033
15325
  }
15034
15326
  const body = JSON.stringify(request);
@@ -15126,57 +15418,43 @@ function htmlResponse(kind) {
15126
15418
  const page = CALLBACK_PAGES[kind];
15127
15419
  return htmlCallbackResponse(page.title, page.message, page.status);
15128
15420
  }
15129
- async function buildResumeConversationContext(channelId, threadTs, sessionId) {
15130
- const threadId = `slack:${channelId}:${threadTs}`;
15131
- const conversation = coerceThreadConversationState(
15132
- await getPersistedThreadState(threadId)
15133
- );
15134
- const userMessageId = getTurnUserMessageId(conversation, sessionId);
15135
- return buildConversationContext(conversation, {
15136
- excludeMessageId: userMessageId
15137
- });
15138
- }
15139
15421
  async function persistCompletedReplyState(channelId, threadTs, sessionId, reply) {
15140
15422
  const threadId = `slack:${channelId}:${threadTs}`;
15141
15423
  const currentState = await getPersistedThreadState(threadId);
15142
15424
  const conversation = coerceThreadConversationState(currentState);
15143
15425
  const artifacts = coerceThreadArtifactsState(currentState);
15144
- const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : void 0;
15145
- const userMessageId = getTurnUserMessageId(conversation, sessionId);
15146
- clearPendingAuth(conversation, sessionId);
15147
- markConversationMessage(conversation, userMessageId, {
15148
- replied: true,
15149
- skippedReason: void 0
15150
- });
15151
- upsertConversationMessage(conversation, {
15152
- id: generateConversationId("assistant"),
15153
- role: "assistant",
15154
- text: normalizeConversationText(reply.text) || "[empty response]",
15155
- createdAtMs: Date.now(),
15156
- author: {
15157
- userName: botConfig.userName,
15158
- isBot: true
15159
- },
15160
- meta: {
15161
- replied: true
15162
- }
15163
- });
15164
- if (reply.piMessages) {
15165
- conversation.piMessages = reply.piMessages;
15166
- }
15167
- markTurnCompleted({
15426
+ const userMessage = getTurnUserMessage(conversation, sessionId);
15427
+ const statePatch = buildDeliveredTurnStatePatch({
15428
+ artifacts,
15168
15429
  conversation,
15169
- nowMs: Date.now(),
15430
+ reply,
15170
15431
  sessionId,
15171
- updateConversationStats
15432
+ userMessageId: userMessage?.id
15172
15433
  });
15173
15434
  await persistThreadStateById(threadId, {
15174
- artifacts: nextArtifacts,
15175
- conversation,
15176
- sandboxId: reply.sandboxId,
15177
- sandboxDependencyProfileHash: reply.sandboxDependencyProfileHash
15435
+ ...statePatch
15178
15436
  });
15179
15437
  }
15438
+ async function failCheckpointBestEffort(args) {
15439
+ try {
15440
+ await failAgentTurnSessionCheckpoint({
15441
+ conversationId: args.conversationId,
15442
+ sessionId: args.sessionId,
15443
+ errorMessage: args.errorMessage
15444
+ });
15445
+ } catch (error) {
15446
+ logException(
15447
+ error,
15448
+ "mcp_oauth_callback_checkpoint_fail_persist_failed",
15449
+ {},
15450
+ {
15451
+ "app.ai.conversation_id": args.conversationId,
15452
+ "app.ai.session_id": args.sessionId
15453
+ },
15454
+ "Failed to mark MCP OAuth-resumed turn checkpoint failed"
15455
+ );
15456
+ }
15457
+ }
15180
15458
  async function persistFailedReplyState(channelId, threadTs, sessionId) {
15181
15459
  const threadId = `slack:${channelId}:${threadTs}`;
15182
15460
  const currentState = await getPersistedThreadState(threadId);
@@ -15190,6 +15468,11 @@ async function persistFailedReplyState(channelId, threadTs, sessionId) {
15190
15468
  markConversationMessage,
15191
15469
  updateConversationStats
15192
15470
  });
15471
+ await failCheckpointBestEffort({
15472
+ conversationId: threadId,
15473
+ sessionId,
15474
+ errorMessage: "OAuth-resumed MCP turn failed"
15475
+ });
15193
15476
  await persistThreadStateById(threadId, {
15194
15477
  conversation
15195
15478
  });
@@ -15202,7 +15485,6 @@ async function resumeAuthorizedMcpTurn(args) {
15202
15485
  const threadId = `slack:${authSession.channelId}:${authSession.threadTs}`;
15203
15486
  const currentState = await getPersistedThreadState(threadId);
15204
15487
  const conversation = coerceThreadConversationState(currentState);
15205
- const artifacts = coerceThreadArtifactsState(currentState);
15206
15488
  const pendingAuth = getConversationPendingAuth({
15207
15489
  conversation,
15208
15490
  kind: "mcp",
@@ -15228,132 +15510,174 @@ async function resumeAuthorizedMcpTurn(args) {
15228
15510
  if (!userMessage) {
15229
15511
  return;
15230
15512
  }
15231
- const channelConfiguration = getChannelConfigurationServiceById(
15232
- authSession.channelId
15233
- );
15234
- const conversationContext = await buildResumeConversationContext(
15235
- authSession.channelId,
15236
- authSession.threadTs,
15237
- resolvedSessionId
15238
- );
15239
15513
  await resumeAuthorizedRequest({
15240
15514
  messageText: userMessage.text,
15241
15515
  channelId: authSession.channelId,
15242
15516
  threadTs: authSession.threadTs,
15243
15517
  messageTs: getTurnUserSlackMessageTs(userMessage),
15244
- lockKey: authSession.conversationId,
15518
+ lockKey: threadId,
15245
15519
  connectedText: "",
15246
- replyContext: {
15247
- requester: {
15248
- userId: authSession.userId,
15249
- userName: userMessage?.author?.userName,
15250
- fullName: userMessage?.author?.fullName
15251
- },
15252
- correlation: {
15253
- conversationId: authSession.conversationId,
15254
- turnId: resolvedSessionId,
15255
- channelId: authSession.channelId,
15256
- threadTs: authSession.threadTs,
15520
+ beforeStart: async () => {
15521
+ const lockedState = await getPersistedThreadState(threadId);
15522
+ const lockedConversation = coerceThreadConversationState(lockedState);
15523
+ const lockedArtifacts = coerceThreadArtifactsState(lockedState);
15524
+ const lockedPendingAuth = getConversationPendingAuth({
15525
+ conversation: lockedConversation,
15526
+ kind: "mcp",
15527
+ provider,
15257
15528
  requesterId: authSession.userId
15258
- },
15259
- toolChannelId: authSession.toolChannelId ?? artifacts.assistantContextChannelId ?? authSession.channelId,
15260
- conversationContext,
15261
- artifactState: artifacts,
15262
- piMessages: conversation.piMessages,
15263
- configuration: authSession.configuration,
15264
- pendingAuth,
15265
- channelConfiguration,
15266
- sandbox: getPersistedSandboxState(currentState),
15267
- onAuthPending: async (nextPendingAuth) => {
15268
- await applyPendingAuthUpdate({
15269
- conversation,
15270
- conversationId: authSession.conversationId,
15271
- nextPendingAuth
15272
- });
15273
- await persistThreadStateById(threadId, { conversation });
15274
- },
15275
- ...getTurnUserReplyAttachmentContext(userMessage)
15276
- },
15277
- onSuccess: async (reply) => {
15278
- try {
15279
- await persistCompletedReplyState(
15280
- authSession.channelId,
15281
- authSession.threadTs,
15282
- resolvedSessionId,
15283
- reply
15284
- );
15285
- } catch (persistError) {
15286
- logException(
15287
- persistError,
15288
- "mcp_oauth_callback_resume_persist_failed",
15289
- {},
15290
- { "app.credential.provider": provider },
15291
- "Failed to persist resumed MCP turn state"
15292
- );
15529
+ });
15530
+ const lockedSessionId = lockedPendingAuth?.sessionId ?? authSession.sessionId;
15531
+ if (lockedSessionId !== resolvedSessionId) {
15532
+ return false;
15293
15533
  }
15294
- },
15295
- onFailure: async () => {
15296
- try {
15297
- await persistFailedReplyState(
15298
- authSession.channelId,
15299
- authSession.threadTs,
15300
- resolvedSessionId
15301
- );
15302
- } catch (persistError) {
15303
- logException(
15304
- persistError,
15305
- "mcp_oauth_callback_resume_failure_persist_failed",
15306
- {},
15307
- { "app.credential.provider": provider },
15308
- "Failed to persist failed MCP resume state"
15309
- );
15534
+ if (lockedPendingAuth) {
15535
+ if (!isPendingAuthLatestRequest(lockedConversation, lockedPendingAuth)) {
15536
+ clearPendingAuth(lockedConversation, lockedPendingAuth.sessionId);
15537
+ await persistThreadStateById(threadId, {
15538
+ conversation: lockedConversation
15539
+ });
15540
+ await supersedeAgentTurnSessionCheckpoint({
15541
+ conversationId: authSession.conversationId,
15542
+ sessionId: lockedPendingAuth.sessionId,
15543
+ errorMessage: "Auth completed after a newer thread message superseded this blocked request."
15544
+ });
15545
+ return false;
15546
+ }
15547
+ } else if (lockedConversation.processing.activeTurnId !== authSession.sessionId) {
15548
+ return false;
15310
15549
  }
15311
- },
15312
- onAuthPause: async (error) => {
15313
- await persistAuthPauseTurnState({
15314
- sessionId: resolvedSessionId,
15315
- threadStateId: `slack:${authSession.channelId}:${authSession.threadTs}`
15316
- });
15317
- logWarn(
15318
- "mcp_oauth_callback_resume_reparked_for_auth",
15319
- {},
15320
- {
15321
- "app.credential.provider": provider,
15322
- ...isRetryableTurnError(error) ? { "app.ai.retryable_reason": error.reason } : {}
15323
- },
15324
- "Resumed MCP turn requested another authorization flow"
15550
+ const lockedUserMessage = getTurnUserMessage(
15551
+ lockedConversation,
15552
+ lockedSessionId
15325
15553
  );
15326
- },
15327
- onTimeoutPause: async (error) => {
15328
- if (!isRetryableTurnError(error, "turn_timeout_resume")) {
15329
- throw error;
15330
- }
15331
- const checkpointVersion = error.metadata?.checkpointVersion;
15332
- const nextSliceId = error.metadata?.sliceId;
15333
- if (typeof checkpointVersion !== "number") {
15334
- throw new Error(
15335
- "Timed-out MCP resume did not include a checkpoint version"
15336
- );
15554
+ if (!lockedUserMessage) {
15555
+ return false;
15337
15556
  }
15338
- if (!canScheduleTurnTimeoutResume(nextSliceId)) {
15339
- logWarn(
15340
- "mcp_oauth_callback_resume_slice_limit_reached",
15341
- {},
15342
- {
15343
- "app.credential.provider": provider,
15344
- ...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
15557
+ const lockedConversationContext = buildConversationContext(
15558
+ lockedConversation,
15559
+ {
15560
+ excludeMessageId: lockedUserMessage.id
15561
+ }
15562
+ );
15563
+ const lockedChannelConfiguration = getChannelConfigurationServiceById(
15564
+ authSession.channelId
15565
+ );
15566
+ return {
15567
+ messageText: lockedUserMessage.text,
15568
+ messageTs: getTurnUserSlackMessageTs(lockedUserMessage),
15569
+ replyContext: {
15570
+ requester: {
15571
+ userId: authSession.userId,
15572
+ userName: lockedUserMessage.author?.userName,
15573
+ fullName: lockedUserMessage.author?.fullName
15345
15574
  },
15346
- "Skipped automatic timeout resume because the turn exceeded the slice limit"
15347
- );
15348
- throw new Error(
15349
- "Timed-out turn exceeded the automatic resume slice limit"
15350
- );
15351
- }
15352
- await scheduleTurnTimeoutResume({
15353
- conversationId: authSession.conversationId,
15354
- sessionId: resolvedSessionId,
15355
- expectedCheckpointVersion: checkpointVersion
15356
- });
15575
+ correlation: {
15576
+ conversationId: authSession.conversationId,
15577
+ turnId: lockedSessionId,
15578
+ channelId: authSession.channelId,
15579
+ threadTs: authSession.threadTs,
15580
+ requesterId: authSession.userId
15581
+ },
15582
+ toolChannelId: authSession.toolChannelId ?? lockedArtifacts.assistantContextChannelId ?? authSession.channelId,
15583
+ conversationContext: lockedConversationContext,
15584
+ artifactState: lockedArtifacts,
15585
+ piMessages: lockedConversation.piMessages,
15586
+ configuration: authSession.configuration,
15587
+ pendingAuth: lockedPendingAuth,
15588
+ channelConfiguration: lockedChannelConfiguration,
15589
+ sandbox: getPersistedSandboxState(lockedState),
15590
+ onAuthPending: async (nextPendingAuth) => {
15591
+ await applyPendingAuthUpdate({
15592
+ conversation: lockedConversation,
15593
+ conversationId: authSession.conversationId,
15594
+ nextPendingAuth
15595
+ });
15596
+ await persistThreadStateById(threadId, {
15597
+ conversation: lockedConversation
15598
+ });
15599
+ },
15600
+ ...getTurnUserReplyAttachmentContext(lockedUserMessage)
15601
+ },
15602
+ onSuccess: async (reply) => {
15603
+ await persistCompletedReplyState(
15604
+ authSession.channelId,
15605
+ authSession.threadTs,
15606
+ lockedSessionId,
15607
+ reply
15608
+ );
15609
+ },
15610
+ onPostDeliveryCommitFailure: async () => {
15611
+ await failAgentTurnSessionCheckpoint({
15612
+ conversationId: authSession.conversationId,
15613
+ sessionId: lockedSessionId,
15614
+ errorMessage: "OAuth-resumed MCP reply was delivered but completion state did not persist"
15615
+ });
15616
+ },
15617
+ onFailure: async () => {
15618
+ try {
15619
+ await persistFailedReplyState(
15620
+ authSession.channelId,
15621
+ authSession.threadTs,
15622
+ lockedSessionId
15623
+ );
15624
+ } catch (persistError) {
15625
+ logException(
15626
+ persistError,
15627
+ "mcp_oauth_callback_resume_failure_persist_failed",
15628
+ {},
15629
+ { "app.credential.provider": provider },
15630
+ "Failed to persist failed MCP resume state"
15631
+ );
15632
+ }
15633
+ },
15634
+ onAuthPause: async (error) => {
15635
+ await persistAuthPauseTurnState({
15636
+ sessionId: lockedSessionId,
15637
+ threadStateId: threadId
15638
+ });
15639
+ logWarn(
15640
+ "mcp_oauth_callback_resume_reparked_for_auth",
15641
+ {},
15642
+ {
15643
+ "app.credential.provider": provider,
15644
+ ...isRetryableTurnError(error) ? { "app.ai.retryable_reason": error.reason } : {}
15645
+ },
15646
+ "Resumed MCP turn requested another authorization flow"
15647
+ );
15648
+ },
15649
+ onTimeoutPause: async (error) => {
15650
+ if (!isRetryableTurnError(error, "turn_timeout_resume")) {
15651
+ throw error;
15652
+ }
15653
+ const checkpointVersion = error.metadata?.checkpointVersion;
15654
+ const nextSliceId = error.metadata?.sliceId;
15655
+ if (typeof checkpointVersion !== "number") {
15656
+ throw new Error(
15657
+ "Timed-out MCP resume did not include a checkpoint version"
15658
+ );
15659
+ }
15660
+ if (!canScheduleTurnTimeoutResume(nextSliceId)) {
15661
+ logWarn(
15662
+ "mcp_oauth_callback_resume_slice_limit_reached",
15663
+ {},
15664
+ {
15665
+ "app.credential.provider": provider,
15666
+ ...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
15667
+ },
15668
+ "Skipped automatic timeout resume because the turn exceeded the slice limit"
15669
+ );
15670
+ throw new Error(
15671
+ "Timed-out turn exceeded the automatic resume slice limit"
15672
+ );
15673
+ }
15674
+ await scheduleTurnTimeoutResume({
15675
+ conversationId: authSession.conversationId,
15676
+ sessionId: lockedSessionId,
15677
+ expectedCheckpointVersion: checkpointVersion
15678
+ });
15679
+ }
15680
+ };
15357
15681
  }
15358
15682
  });
15359
15683
  }
@@ -15552,55 +15876,42 @@ async function publishAppHomeView(slackClient, userId, userTokenStore) {
15552
15876
  function htmlErrorResponse(title, message, status) {
15553
15877
  return htmlCallbackResponse(escapeXml(title), escapeXml(message), status);
15554
15878
  }
15555
- async function buildCheckpointConversationContext(conversationId, sessionId) {
15556
- const conversation = coerceThreadConversationState(
15557
- await getPersistedThreadState(conversationId)
15558
- );
15559
- const userMessage = getTurnUserMessage(conversation, sessionId);
15560
- return buildConversationContext(conversation, {
15561
- excludeMessageId: userMessage?.id
15562
- });
15563
- }
15564
15879
  async function persistCompletedOAuthReplyState(args) {
15565
15880
  const currentState = await getPersistedThreadState(args.conversationId);
15566
15881
  const conversation = coerceThreadConversationState(currentState);
15567
15882
  const artifacts = coerceThreadArtifactsState(currentState);
15568
- const nextArtifacts = args.reply.artifactStatePatch ? mergeArtifactsState(artifacts, args.reply.artifactStatePatch) : void 0;
15569
15883
  const userMessage = getTurnUserMessage(conversation, args.sessionId);
15570
- clearPendingAuth(conversation, args.sessionId);
15571
- markConversationMessage(conversation, userMessage?.id, {
15572
- replied: true,
15573
- skippedReason: void 0
15574
- });
15575
- upsertConversationMessage(conversation, {
15576
- id: generateConversationId("assistant"),
15577
- role: "assistant",
15578
- text: normalizeConversationText(args.reply.text) || "[empty response]",
15579
- createdAtMs: Date.now(),
15580
- author: {
15581
- userName: botConfig.userName,
15582
- isBot: true
15583
- },
15584
- meta: {
15585
- replied: true
15586
- }
15587
- });
15588
- if (args.reply.piMessages) {
15589
- conversation.piMessages = args.reply.piMessages;
15590
- }
15591
- markTurnCompleted({
15884
+ const statePatch = buildDeliveredTurnStatePatch({
15885
+ artifacts,
15592
15886
  conversation,
15593
- nowMs: Date.now(),
15887
+ reply: args.reply,
15594
15888
  sessionId: args.sessionId,
15595
- updateConversationStats
15889
+ userMessageId: userMessage?.id
15596
15890
  });
15597
15891
  await persistThreadStateById(args.conversationId, {
15598
- artifacts: nextArtifacts,
15599
- conversation,
15600
- sandboxId: args.reply.sandboxId,
15601
- sandboxDependencyProfileHash: args.reply.sandboxDependencyProfileHash
15892
+ ...statePatch
15602
15893
  });
15603
15894
  }
15895
+ async function failCheckpointBestEffort2(args) {
15896
+ try {
15897
+ await failAgentTurnSessionCheckpoint({
15898
+ conversationId: args.conversationId,
15899
+ sessionId: args.sessionId,
15900
+ errorMessage: args.errorMessage
15901
+ });
15902
+ } catch (error) {
15903
+ logException(
15904
+ error,
15905
+ "oauth_callback_checkpoint_fail_persist_failed",
15906
+ {},
15907
+ {
15908
+ "app.ai.conversation_id": args.conversationId,
15909
+ "app.ai.session_id": args.sessionId
15910
+ },
15911
+ "Failed to mark OAuth-resumed turn checkpoint failed"
15912
+ );
15913
+ }
15914
+ }
15604
15915
  async function persistFailedOAuthReplyState(args) {
15605
15916
  const currentState = await getPersistedThreadState(args.conversationId);
15606
15917
  const conversation = coerceThreadConversationState(currentState);
@@ -15613,6 +15924,11 @@ async function persistFailedOAuthReplyState(args) {
15613
15924
  markConversationMessage,
15614
15925
  updateConversationStats
15615
15926
  });
15927
+ await failCheckpointBestEffort2({
15928
+ conversationId: args.conversationId,
15929
+ sessionId: args.sessionId,
15930
+ errorMessage: "OAuth-resumed turn failed"
15931
+ });
15616
15932
  await persistThreadStateById(args.conversationId, {
15617
15933
  conversation
15618
15934
  });
@@ -15638,7 +15954,6 @@ async function resumeCheckpointedOAuthTurn(stored) {
15638
15954
  stored.resumeConversationId
15639
15955
  );
15640
15956
  const conversation = coerceThreadConversationState(currentState);
15641
- const artifacts = coerceThreadArtifactsState(currentState);
15642
15957
  const pendingAuth = getConversationPendingAuth({
15643
15958
  conversation,
15644
15959
  kind: "plugin",
@@ -15671,102 +15986,163 @@ async function resumeCheckpointedOAuthTurn(stored) {
15671
15986
  if (!userMessage?.author?.userId || !resolvedSessionId) {
15672
15987
  return false;
15673
15988
  }
15674
- const conversationContext = await buildCheckpointConversationContext(
15675
- stored.resumeConversationId,
15676
- resolvedSessionId
15677
- );
15678
- const channelConfiguration = getChannelConfigurationServiceById(
15679
- stored.channelId
15680
- );
15681
15989
  await resumeSlackTurn({
15682
15990
  messageText: stored.pendingMessage ?? userMessage.text,
15683
15991
  channelId: stored.channelId,
15684
15992
  threadTs: stored.threadTs,
15685
15993
  messageTs: getTurnUserSlackMessageTs(userMessage),
15686
15994
  lockKey: stored.resumeConversationId,
15687
- initialText: "",
15688
- replyContext: {
15689
- requester: {
15690
- userId: userMessage.author.userId,
15691
- userName: userMessage.author.userName,
15692
- fullName: userMessage.author.fullName
15693
- },
15694
- correlation: {
15695
- conversationId: stored.resumeConversationId,
15696
- turnId: resolvedSessionId,
15697
- channelId: stored.channelId,
15698
- threadTs: stored.threadTs,
15699
- requesterId: userMessage.author.userId
15700
- },
15701
- toolChannelId: artifacts.assistantContextChannelId ?? stored.channelId,
15702
- artifactState: artifacts,
15703
- pendingAuth,
15704
- conversationContext,
15705
- channelConfiguration,
15706
- piMessages: conversation.piMessages,
15707
- sandbox: getPersistedSandboxState(currentState),
15708
- onAuthPending: async (nextPendingAuth) => {
15709
- await applyPendingAuthUpdate({
15710
- conversation,
15711
- conversationId: stored.resumeConversationId,
15712
- nextPendingAuth
15713
- });
15714
- await persistThreadStateById(stored.resumeConversationId, {
15715
- conversation
15716
- });
15717
- },
15718
- ...getTurnUserReplyAttachmentContext(userMessage)
15719
- },
15720
- onSuccess: async (reply) => {
15721
- logInfo(
15722
- "oauth_callback_resume_complete",
15723
- {},
15724
- {
15725
- "app.credential.provider": stored.provider,
15726
- "app.ai.outcome": reply.diagnostics.outcome,
15727
- "app.ai.tool_calls": reply.diagnostics.toolCalls.length
15728
- },
15729
- "OAuth callback auto-resumed checkpoint finished replying"
15730
- );
15731
- await persistCompletedOAuthReplyState({
15732
- conversationId: stored.resumeConversationId,
15733
- sessionId: resolvedSessionId,
15734
- reply
15735
- });
15736
- },
15737
- onFailure: async () => {
15738
- await persistFailedOAuthReplyState({
15739
- conversationId: stored.resumeConversationId,
15740
- sessionId: resolvedSessionId
15741
- });
15742
- },
15743
- onAuthPause: async () => {
15744
- await persistAuthPauseTurnState({
15745
- sessionId: resolvedSessionId,
15746
- threadStateId: stored.resumeConversationId
15995
+ initialText: "",
15996
+ beforeStart: async () => {
15997
+ const lockedCheckpoint = await getAgentTurnSessionCheckpoint(
15998
+ stored.resumeConversationId,
15999
+ stored.resumeSessionId
16000
+ );
16001
+ if (!lockedCheckpoint || lockedCheckpoint.state !== "awaiting_resume" || lockedCheckpoint.resumeReason !== "auth") {
16002
+ return false;
16003
+ }
16004
+ const lockedState = await getPersistedThreadState(
16005
+ stored.resumeConversationId
16006
+ );
16007
+ const lockedConversation = coerceThreadConversationState(lockedState);
16008
+ const lockedArtifacts = coerceThreadArtifactsState(lockedState);
16009
+ const lockedPendingAuth = getConversationPendingAuth({
16010
+ conversation: lockedConversation,
16011
+ kind: "plugin",
16012
+ provider: stored.provider,
16013
+ requesterId: stored.userId
15747
16014
  });
15748
- },
15749
- onTimeoutPause: async (error) => {
15750
- if (!isRetryableTurnError(error, "turn_timeout_resume")) {
15751
- throw error;
16015
+ const lockedSessionId = lockedPendingAuth?.sessionId ?? stored.resumeSessionId;
16016
+ if (lockedSessionId !== resolvedSessionId) {
16017
+ return false;
15752
16018
  }
15753
- const checkpointVersion = error.metadata?.checkpointVersion;
15754
- const nextSliceId = error.metadata?.sliceId;
15755
- if (typeof checkpointVersion !== "number") {
15756
- throw new Error(
15757
- "Timed-out OAuth resume did not include a checkpoint version"
15758
- );
16019
+ if (lockedPendingAuth) {
16020
+ if (!isPendingAuthLatestRequest(lockedConversation, lockedPendingAuth)) {
16021
+ clearPendingAuth(lockedConversation, lockedPendingAuth.sessionId);
16022
+ await persistThreadStateById(stored.resumeConversationId, {
16023
+ conversation: lockedConversation
16024
+ });
16025
+ await supersedeAgentTurnSessionCheckpoint({
16026
+ conversationId: stored.resumeConversationId,
16027
+ sessionId: lockedPendingAuth.sessionId,
16028
+ errorMessage: "Auth completed after a newer thread message superseded this blocked request."
16029
+ });
16030
+ return false;
16031
+ }
16032
+ } else if (lockedConversation.processing.activeTurnId !== stored.resumeSessionId) {
16033
+ return false;
15759
16034
  }
15760
- if (!canScheduleTurnTimeoutResume(nextSliceId)) {
15761
- throw new Error(
15762
- "Timed-out turn exceeded the automatic resume slice limit"
15763
- );
16035
+ const lockedUserMessage = getTurnUserMessage(
16036
+ lockedConversation,
16037
+ lockedSessionId
16038
+ );
16039
+ if (!lockedUserMessage?.author?.userId) {
16040
+ return false;
15764
16041
  }
15765
- await scheduleTurnTimeoutResume({
15766
- conversationId: stored.resumeConversationId,
15767
- sessionId: resolvedSessionId,
15768
- expectedCheckpointVersion: checkpointVersion
15769
- });
16042
+ const lockedConversationContext = buildConversationContext(
16043
+ lockedConversation,
16044
+ {
16045
+ excludeMessageId: lockedUserMessage.id
16046
+ }
16047
+ );
16048
+ const lockedChannelConfiguration = getChannelConfigurationServiceById(
16049
+ stored.channelId
16050
+ );
16051
+ return {
16052
+ messageText: stored.pendingMessage ?? lockedUserMessage.text,
16053
+ messageTs: getTurnUserSlackMessageTs(lockedUserMessage),
16054
+ replyContext: {
16055
+ requester: {
16056
+ userId: lockedUserMessage.author.userId,
16057
+ userName: lockedUserMessage.author.userName,
16058
+ fullName: lockedUserMessage.author.fullName
16059
+ },
16060
+ correlation: {
16061
+ conversationId: stored.resumeConversationId,
16062
+ turnId: lockedSessionId,
16063
+ channelId: stored.channelId,
16064
+ threadTs: stored.threadTs,
16065
+ requesterId: lockedUserMessage.author.userId
16066
+ },
16067
+ toolChannelId: lockedArtifacts.assistantContextChannelId ?? stored.channelId,
16068
+ artifactState: lockedArtifacts,
16069
+ pendingAuth: lockedPendingAuth,
16070
+ conversationContext: lockedConversationContext,
16071
+ channelConfiguration: lockedChannelConfiguration,
16072
+ piMessages: lockedConversation.piMessages,
16073
+ sandbox: getPersistedSandboxState(lockedState),
16074
+ onAuthPending: async (nextPendingAuth) => {
16075
+ await applyPendingAuthUpdate({
16076
+ conversation: lockedConversation,
16077
+ conversationId: stored.resumeConversationId,
16078
+ nextPendingAuth
16079
+ });
16080
+ await persistThreadStateById(stored.resumeConversationId, {
16081
+ conversation: lockedConversation
16082
+ });
16083
+ },
16084
+ ...getTurnUserReplyAttachmentContext(lockedUserMessage)
16085
+ },
16086
+ onSuccess: async (reply) => {
16087
+ logInfo(
16088
+ "oauth_callback_resume_complete",
16089
+ {},
16090
+ {
16091
+ "app.credential.provider": stored.provider,
16092
+ "app.ai.outcome": reply.diagnostics.outcome,
16093
+ "app.ai.tool_calls": reply.diagnostics.toolCalls.length
16094
+ },
16095
+ "OAuth callback auto-resumed checkpoint finished replying"
16096
+ );
16097
+ await persistCompletedOAuthReplyState({
16098
+ conversationId: stored.resumeConversationId,
16099
+ sessionId: lockedSessionId,
16100
+ reply
16101
+ });
16102
+ },
16103
+ onPostDeliveryCommitFailure: async () => {
16104
+ await failAgentTurnSessionCheckpoint({
16105
+ conversationId: stored.resumeConversationId,
16106
+ expectedCheckpointVersion: lockedCheckpoint.checkpointVersion,
16107
+ sessionId: lockedSessionId,
16108
+ errorMessage: "OAuth-resumed reply was delivered but completion state did not persist"
16109
+ });
16110
+ },
16111
+ onFailure: async () => {
16112
+ await persistFailedOAuthReplyState({
16113
+ conversationId: stored.resumeConversationId,
16114
+ sessionId: lockedSessionId
16115
+ });
16116
+ },
16117
+ onAuthPause: async () => {
16118
+ await persistAuthPauseTurnState({
16119
+ sessionId: lockedSessionId,
16120
+ threadStateId: stored.resumeConversationId
16121
+ });
16122
+ },
16123
+ onTimeoutPause: async (error) => {
16124
+ if (!isRetryableTurnError(error, "turn_timeout_resume")) {
16125
+ throw error;
16126
+ }
16127
+ const checkpointVersion = error.metadata?.checkpointVersion;
16128
+ const nextSliceId = error.metadata?.sliceId;
16129
+ if (typeof checkpointVersion !== "number") {
16130
+ throw new Error(
16131
+ "Timed-out OAuth resume did not include a checkpoint version"
16132
+ );
16133
+ }
16134
+ if (!canScheduleTurnTimeoutResume(nextSliceId)) {
16135
+ throw new Error(
16136
+ "Timed-out turn exceeded the automatic resume slice limit"
16137
+ );
16138
+ }
16139
+ await scheduleTurnTimeoutResume({
16140
+ conversationId: stored.resumeConversationId,
16141
+ sessionId: lockedSessionId,
16142
+ expectedCheckpointVersion: checkpointVersion
16143
+ });
16144
+ }
16145
+ };
15770
16146
  }
15771
16147
  });
15772
16148
  return true;
@@ -16522,7 +16898,7 @@ function isSandboxEgressRequest(request) {
16522
16898
 
16523
16899
  // src/handlers/turn-resume.ts
16524
16900
  var TIMEOUT_RESUME_LOCK_RETRY_DELAYS_MS = [250, 1e3, 2e3];
16525
- function sleep3(ms) {
16901
+ function sleep4(ms) {
16526
16902
  return new Promise((resolve) => setTimeout(resolve, ms));
16527
16903
  }
16528
16904
  async function persistCompletedReplyState2(args) {
@@ -16531,45 +16907,42 @@ async function persistCompletedReplyState2(args) {
16531
16907
  );
16532
16908
  const conversation = coerceThreadConversationState(currentState);
16533
16909
  const artifacts = coerceThreadArtifactsState(currentState);
16534
- const nextArtifacts = args.reply.artifactStatePatch ? mergeArtifactsState(artifacts, args.reply.artifactStatePatch) : void 0;
16535
16910
  const userMessage = getTurnUserMessage(
16536
16911
  conversation,
16537
16912
  args.checkpoint.sessionId
16538
16913
  );
16539
- clearPendingAuth(conversation, args.checkpoint.sessionId);
16540
- markConversationMessage(conversation, userMessage?.id, {
16541
- replied: true,
16542
- skippedReason: void 0
16543
- });
16544
- upsertConversationMessage(conversation, {
16545
- id: generateConversationId("assistant"),
16546
- role: "assistant",
16547
- text: normalizeConversationText(args.reply.text) || "[empty response]",
16548
- createdAtMs: Date.now(),
16549
- author: {
16550
- userName: botConfig.userName,
16551
- isBot: true
16552
- },
16553
- meta: {
16554
- replied: true
16555
- }
16556
- });
16557
- if (args.reply.piMessages) {
16558
- conversation.piMessages = args.reply.piMessages;
16559
- }
16560
- markTurnCompleted({
16914
+ const statePatch = buildDeliveredTurnStatePatch({
16915
+ artifacts,
16561
16916
  conversation,
16562
- nowMs: Date.now(),
16917
+ reply: args.reply,
16563
16918
  sessionId: args.checkpoint.sessionId,
16564
- updateConversationStats
16919
+ userMessageId: userMessage?.id
16565
16920
  });
16566
16921
  await persistThreadStateById(args.checkpoint.conversationId, {
16567
- artifacts: nextArtifacts,
16568
- conversation,
16569
- sandboxId: args.reply.sandboxId,
16570
- sandboxDependencyProfileHash: args.reply.sandboxDependencyProfileHash
16922
+ ...statePatch
16571
16923
  });
16572
16924
  }
16925
+ async function failCheckpointBestEffort3(args) {
16926
+ try {
16927
+ await failAgentTurnSessionCheckpoint({
16928
+ conversationId: args.checkpoint.conversationId,
16929
+ expectedCheckpointVersion: args.checkpoint.checkpointVersion,
16930
+ sessionId: args.checkpoint.sessionId,
16931
+ errorMessage: args.errorMessage
16932
+ });
16933
+ } catch (error) {
16934
+ logException(
16935
+ error,
16936
+ "timeout_resume_checkpoint_fail_persist_failed",
16937
+ {},
16938
+ {
16939
+ "app.ai.conversation_id": args.checkpoint.conversationId,
16940
+ "app.ai.session_id": args.checkpoint.sessionId
16941
+ },
16942
+ "Failed to mark timed-out turn checkpoint failed"
16943
+ );
16944
+ }
16945
+ }
16573
16946
  async function persistFailedReplyState2(checkpoint) {
16574
16947
  const currentState = await getPersistedThreadState(checkpoint.conversationId);
16575
16948
  const conversation = coerceThreadConversationState(currentState);
@@ -16582,145 +16955,152 @@ async function persistFailedReplyState2(checkpoint) {
16582
16955
  markConversationMessage,
16583
16956
  updateConversationStats
16584
16957
  });
16958
+ await failCheckpointBestEffort3({
16959
+ checkpoint,
16960
+ errorMessage: "Timed-out turn failed while resuming"
16961
+ });
16585
16962
  await persistThreadStateById(checkpoint.conversationId, {
16586
16963
  conversation
16587
16964
  });
16588
16965
  }
16589
16966
  async function resumeTimedOutTurn(payload) {
16590
- const checkpoint = await getAgentTurnSessionCheckpoint(
16591
- payload.conversationId,
16592
- payload.sessionId
16593
- );
16594
- if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "timeout" || checkpoint.checkpointVersion !== payload.expectedCheckpointVersion) {
16595
- return;
16596
- }
16597
16967
  const thread = parseSlackThreadId(payload.conversationId);
16598
16968
  if (!thread) {
16599
16969
  throw new Error(
16600
16970
  `Timeout resume requires a Slack thread conversation id, got "${payload.conversationId}"`
16601
16971
  );
16602
16972
  }
16603
- const currentState = await getPersistedThreadState(payload.conversationId);
16604
- const conversation = coerceThreadConversationState(currentState);
16605
- const artifacts = coerceThreadArtifactsState(currentState);
16606
- const userMessage = getTurnUserMessage(conversation, payload.sessionId);
16607
- if (!userMessage?.author?.userId) {
16608
- throw new Error(
16609
- `Unable to locate the persisted user message for timeout resume session "${payload.sessionId}"`
16610
- );
16611
- }
16612
- if (conversation.processing.activeTurnId !== payload.sessionId) {
16613
- return;
16614
- }
16615
- const channelConfiguration = getChannelConfigurationServiceById(
16616
- thread.channelId
16617
- );
16618
- const conversationContext = buildConversationContext(conversation, {
16619
- excludeMessageId: userMessage.id
16620
- });
16621
- const sandbox = getPersistedSandboxState(currentState);
16622
16973
  await resumeSlackTurn({
16623
- messageText: userMessage.text,
16974
+ messageText: "",
16624
16975
  channelId: thread.channelId,
16625
16976
  threadTs: thread.threadTs,
16626
16977
  lockKey: payload.conversationId,
16627
- replyContext: {
16628
- requester: {
16629
- userId: userMessage.author.userId,
16630
- userName: userMessage.author.userName,
16631
- fullName: userMessage.author.fullName
16632
- },
16633
- correlation: {
16634
- conversationId: payload.conversationId,
16635
- turnId: payload.sessionId,
16636
- channelId: thread.channelId,
16637
- threadTs: thread.threadTs,
16638
- requesterId: userMessage.author.userId
16639
- },
16640
- toolChannelId: artifacts.assistantContextChannelId ?? thread.channelId,
16641
- artifactState: artifacts,
16642
- pendingAuth: conversation.processing.pendingAuth,
16643
- conversationContext,
16644
- channelConfiguration,
16645
- piMessages: conversation.piMessages,
16646
- sandbox,
16647
- onAuthPending: async (nextPendingAuth) => {
16648
- await applyPendingAuthUpdate({
16649
- conversation,
16650
- conversationId: payload.conversationId,
16651
- nextPendingAuth
16652
- });
16653
- await persistThreadStateById(payload.conversationId, {
16654
- conversation
16655
- });
16656
- },
16657
- ...getTurnUserReplyAttachmentContext(userMessage)
16658
- },
16659
- onSuccess: async (reply) => {
16660
- try {
16661
- await persistCompletedReplyState2({ checkpoint, reply });
16662
- } catch (persistError) {
16663
- logException(
16664
- persistError,
16665
- "timeout_resume_complete_persist_failed",
16666
- {},
16667
- {
16668
- "app.ai.conversation_id": payload.conversationId,
16669
- "app.ai.session_id": payload.sessionId
16670
- },
16671
- "Failed to persist completed timeout-resume state after reply delivery"
16672
- );
16673
- }
16674
- },
16675
- onFailure: async () => {
16676
- await persistFailedReplyState2(checkpoint);
16677
- },
16678
- onAuthPause: async () => {
16679
- await persistAuthPauseTurnState({
16680
- sessionId: payload.sessionId,
16681
- threadStateId: payload.conversationId
16682
- });
16683
- logWarn(
16684
- "timeout_resume_reparked_for_auth",
16685
- {},
16686
- {
16687
- "app.ai.conversation_id": payload.conversationId,
16688
- "app.ai.session_id": payload.sessionId
16689
- },
16690
- "Resumed timed-out turn parked for auth"
16978
+ beforeStart: async () => {
16979
+ const checkpoint = await getAgentTurnSessionCheckpoint(
16980
+ payload.conversationId,
16981
+ payload.sessionId
16691
16982
  );
16692
- },
16693
- onTimeoutPause: async (error) => {
16694
- if (!isRetryableTurnError(error, "turn_timeout_resume")) {
16695
- throw error;
16983
+ if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "timeout" || checkpoint.checkpointVersion !== payload.expectedCheckpointVersion) {
16984
+ return false;
16696
16985
  }
16697
- const checkpointVersion = error.metadata?.checkpointVersion;
16698
- const nextSliceId = error.metadata?.sliceId;
16699
- if (typeof checkpointVersion !== "number") {
16986
+ const currentState = await getPersistedThreadState(
16987
+ payload.conversationId
16988
+ );
16989
+ const conversation = coerceThreadConversationState(currentState);
16990
+ const artifacts = coerceThreadArtifactsState(currentState);
16991
+ const userMessage = getTurnUserMessage(conversation, payload.sessionId);
16992
+ if (!userMessage?.author?.userId) {
16700
16993
  throw new Error(
16701
- "Timed-out resume turn did not include a checkpoint version"
16994
+ `Unable to locate the persisted user message for timeout resume session "${payload.sessionId}"`
16702
16995
  );
16703
16996
  }
16704
- if (!canScheduleTurnTimeoutResume(nextSliceId)) {
16705
- logWarn(
16706
- "timeout_resume_slice_limit_reached",
16707
- {},
16708
- {
16709
- "app.ai.conversation_id": payload.conversationId,
16710
- "app.ai.session_id": payload.sessionId,
16711
- ...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
16712
- },
16713
- "Skipped automatic timeout resume because the turn exceeded the slice limit"
16714
- );
16715
- throw new Error(
16716
- "Timed-out turn exceeded the automatic resume slice limit"
16717
- );
16997
+ if (conversation.processing.activeTurnId !== payload.sessionId) {
16998
+ return false;
16718
16999
  }
16719
- await scheduleTurnTimeoutResume({
16720
- conversationId: payload.conversationId,
16721
- sessionId: payload.sessionId,
16722
- expectedCheckpointVersion: checkpointVersion
17000
+ const channelConfiguration = getChannelConfigurationServiceById(
17001
+ thread.channelId
17002
+ );
17003
+ const conversationContext = buildConversationContext(conversation, {
17004
+ excludeMessageId: userMessage.id
16723
17005
  });
17006
+ const sandbox = getPersistedSandboxState(currentState);
17007
+ return {
17008
+ messageText: userMessage.text,
17009
+ messageTs: getTurnUserSlackMessageTs(userMessage),
17010
+ replyContext: {
17011
+ requester: {
17012
+ userId: userMessage.author.userId,
17013
+ userName: userMessage.author.userName,
17014
+ fullName: userMessage.author.fullName
17015
+ },
17016
+ correlation: {
17017
+ conversationId: payload.conversationId,
17018
+ turnId: payload.sessionId,
17019
+ channelId: thread.channelId,
17020
+ threadTs: thread.threadTs,
17021
+ requesterId: userMessage.author.userId
17022
+ },
17023
+ toolChannelId: artifacts.assistantContextChannelId ?? thread.channelId,
17024
+ artifactState: artifacts,
17025
+ pendingAuth: conversation.processing.pendingAuth,
17026
+ conversationContext,
17027
+ channelConfiguration,
17028
+ piMessages: conversation.piMessages,
17029
+ sandbox,
17030
+ onAuthPending: async (nextPendingAuth) => {
17031
+ await applyPendingAuthUpdate({
17032
+ conversation,
17033
+ conversationId: payload.conversationId,
17034
+ nextPendingAuth
17035
+ });
17036
+ await persistThreadStateById(payload.conversationId, {
17037
+ conversation
17038
+ });
17039
+ },
17040
+ ...getTurnUserReplyAttachmentContext(userMessage)
17041
+ },
17042
+ onSuccess: async (reply) => {
17043
+ await persistCompletedReplyState2({ checkpoint, reply });
17044
+ },
17045
+ onFailure: async () => {
17046
+ await persistFailedReplyState2(checkpoint);
17047
+ },
17048
+ onPostDeliveryCommitFailure: async () => {
17049
+ await failAgentTurnSessionCheckpoint({
17050
+ conversationId: checkpoint.conversationId,
17051
+ expectedCheckpointVersion: checkpoint.checkpointVersion,
17052
+ sessionId: checkpoint.sessionId,
17053
+ errorMessage: "Timed-out turn reply was delivered but completion state did not persist"
17054
+ });
17055
+ },
17056
+ onAuthPause: async () => {
17057
+ await persistAuthPauseTurnState({
17058
+ sessionId: payload.sessionId,
17059
+ threadStateId: payload.conversationId
17060
+ });
17061
+ logWarn(
17062
+ "timeout_resume_reparked_for_auth",
17063
+ {},
17064
+ {
17065
+ "app.ai.conversation_id": payload.conversationId,
17066
+ "app.ai.session_id": payload.sessionId
17067
+ },
17068
+ "Resumed timed-out turn parked for auth"
17069
+ );
17070
+ },
17071
+ onTimeoutPause: async (error) => {
17072
+ if (!isRetryableTurnError(error, "turn_timeout_resume")) {
17073
+ throw error;
17074
+ }
17075
+ const checkpointVersion = error.metadata?.checkpointVersion;
17076
+ const nextSliceId = error.metadata?.sliceId;
17077
+ if (typeof checkpointVersion !== "number") {
17078
+ throw new Error(
17079
+ "Timed-out resume turn did not include a checkpoint version"
17080
+ );
17081
+ }
17082
+ if (!canScheduleTurnTimeoutResume(nextSliceId)) {
17083
+ logWarn(
17084
+ "timeout_resume_slice_limit_reached",
17085
+ {},
17086
+ {
17087
+ "app.ai.conversation_id": payload.conversationId,
17088
+ "app.ai.session_id": payload.sessionId,
17089
+ ...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
17090
+ },
17091
+ "Skipped automatic timeout resume because the turn exceeded the slice limit"
17092
+ );
17093
+ throw new Error(
17094
+ "Timed-out turn exceeded the automatic resume slice limit"
17095
+ );
17096
+ }
17097
+ await scheduleTurnTimeoutResume({
17098
+ conversationId: payload.conversationId,
17099
+ sessionId: payload.sessionId,
17100
+ expectedCheckpointVersion: checkpointVersion
17101
+ });
17102
+ }
17103
+ };
16724
17104
  }
16725
17105
  });
16726
17106
  }
@@ -16745,8 +17125,9 @@ async function resumeTimedOutTurnWithLockRetry(payload) {
16745
17125
  "app.ai.session_id": payload.sessionId,
16746
17126
  "app.ai.resume_lock_retry_count": attempt
16747
17127
  },
16748
- "Skipped timeout resume because another turn still owns the thread lock"
17128
+ "Rescheduling timeout resume because another turn still owns the thread lock"
16749
17129
  );
17130
+ await scheduleTurnTimeoutResume(payload);
16750
17131
  return;
16751
17132
  }
16752
17133
  logWarn(
@@ -16760,7 +17141,7 @@ async function resumeTimedOutTurnWithLockRetry(payload) {
16760
17141
  },
16761
17142
  "Timeout resume lock was busy; retrying"
16762
17143
  );
16763
- await sleep3(delayMs);
17144
+ await sleep4(delayMs);
16764
17145
  }
16765
17146
  }
16766
17147
  }
@@ -18260,6 +18641,31 @@ function getCurrentTurnCanvasUrl(args) {
18260
18641
  function buildCanvasRecoveryReply(canvasUrl) {
18261
18642
  return `I created the canvas, but the turn was interrupted before I could finish the thread reply: ${canvasUrl}`;
18262
18643
  }
18644
+ async function loadPiMessagesForTurn(args) {
18645
+ const fallback = args.fallback.length > 0 ? [...args.fallback] : void 0;
18646
+ if (!args.conversationId) {
18647
+ return fallback;
18648
+ }
18649
+ if (args.activeTurnId) {
18650
+ const checkpoint2 = await getAgentTurnSessionCheckpoint(
18651
+ args.conversationId,
18652
+ args.activeTurnId
18653
+ );
18654
+ if (checkpoint2?.piMessages.length) {
18655
+ return stripRuntimeTurnContext(
18656
+ trimTrailingAssistantMessages(checkpoint2.piMessages)
18657
+ );
18658
+ }
18659
+ }
18660
+ if (!args.lastSessionId) {
18661
+ return fallback;
18662
+ }
18663
+ const checkpoint = await getAgentTurnSessionCheckpoint(
18664
+ args.conversationId,
18665
+ args.lastSessionId
18666
+ );
18667
+ return checkpoint?.state === "completed" && checkpoint.piMessages.length > 0 ? stripRuntimeTurnContext(checkpoint.piMessages) : fallback;
18668
+ }
18263
18669
  function createReplyToThread(deps) {
18264
18670
  return async function replyToThread(thread, message, options = {}) {
18265
18671
  if (message.author.isMe) {
@@ -18412,6 +18818,7 @@ function createReplyToThread(deps) {
18412
18818
  return;
18413
18819
  }
18414
18820
  }
18821
+ const lastSessionIdForHistory = preparedState.conversation.processing.lastSessionId;
18415
18822
  const configReply = await maybeApplyProviderDefaultConfigRequest({
18416
18823
  channelConfiguration: preparedState.channelConfiguration,
18417
18824
  requesterId: message.author.userId,
@@ -18487,6 +18894,12 @@ function createReplyToThread(deps) {
18487
18894
  }
18488
18895
  );
18489
18896
  const omittedImageAttachmentCount = !isVisionEnabled() && hasPotentialImageAttachment(message.attachments) ? countPotentialImageAttachments(message.attachments) : 0;
18897
+ const piMessages = await loadPiMessagesForTurn({
18898
+ conversationId,
18899
+ activeTurnId,
18900
+ lastSessionId: lastSessionIdForHistory,
18901
+ fallback: preparedState.conversation.piMessages
18902
+ });
18490
18903
  const status = createSlackAdapterAssistantStatusSession({
18491
18904
  channelId: assistantThreadContext?.channelId,
18492
18905
  threadTs: assistantThreadContext?.threadTs,
@@ -18538,7 +18951,7 @@ function createReplyToThread(deps) {
18538
18951
  },
18539
18952
  conversationContext: preparedState.routingContext ?? preparedState.conversationContext,
18540
18953
  artifactState: preparedState.artifacts,
18541
- piMessages: preparedState.conversation.piMessages,
18954
+ piMessages,
18542
18955
  pendingAuth: preparedState.conversation.processing.pendingAuth,
18543
18956
  configuration: preparedState.configuration,
18544
18957
  channelConfiguration: preparedState.channelConfiguration,
@@ -18600,30 +19013,6 @@ function createReplyToThread(deps) {
18600
19013
  context: diagnosticsContext
18601
19014
  });
18602
19015
  }
18603
- markConversationMessage(
18604
- preparedState.conversation,
18605
- preparedState.userMessageId,
18606
- {
18607
- replied: true,
18608
- skippedReason: void 0
18609
- }
18610
- );
18611
- upsertConversationMessage(preparedState.conversation, {
18612
- id: generateConversationId("assistant"),
18613
- role: "assistant",
18614
- text: normalizeConversationText(reply.text) || "[empty response]",
18615
- createdAtMs: Date.now(),
18616
- author: {
18617
- userName: botConfig.userName,
18618
- isBot: true
18619
- },
18620
- meta: {
18621
- replied: true
18622
- }
18623
- });
18624
- if (reply.piMessages) {
18625
- preparedState.conversation.piMessages = reply.piMessages;
18626
- }
18627
19016
  const artifactStatePatch = reply.artifactStatePatch ? { ...reply.artifactStatePatch } : {};
18628
19017
  const reactionPerformed = reply.diagnostics.toolCalls.includes(
18629
19018
  "slackMessageAddReaction"
@@ -18696,19 +19085,18 @@ function createReplyToThread(deps) {
18696
19085
  if (titleUpdateResult) {
18697
19086
  artifactStatePatch.assistantTitleSourceMessageId = titleUpdateResult;
18698
19087
  }
18699
- const shouldPersistArtifacts = Object.keys(artifactStatePatch).length > 0;
18700
- const nextArtifacts = shouldPersistArtifacts ? mergeArtifactsState(preparedState.artifacts, artifactStatePatch) : void 0;
18701
- markTurnCompleted({
19088
+ const completedState = buildDeliveredTurnStatePatch({
19089
+ artifactStatePatch,
19090
+ artifacts: preparedState.artifacts,
18702
19091
  conversation: preparedState.conversation,
18703
- nowMs: Date.now(),
18704
- updateConversationStats
19092
+ reply,
19093
+ sessionId: turnId,
19094
+ userMessageId: preparedState.userMessageId
18705
19095
  });
18706
19096
  await persistThreadState(thread, {
18707
- artifacts: nextArtifacts,
18708
- conversation: preparedState.conversation,
18709
- sandboxId: reply.sandboxId,
18710
- sandboxDependencyProfileHash: reply.sandboxDependencyProfileHash
19097
+ ...completedState
18711
19098
  });
19099
+ preparedState.conversation = completedState.conversation;
18712
19100
  persistedAtLeastOnce = true;
18713
19101
  if (shouldEmitDevAgentTrace()) {
18714
19102
  logInfo(
@@ -18826,9 +19214,10 @@ function createReplyToThread(deps) {
18826
19214
  replied: true
18827
19215
  }
18828
19216
  });
18829
- markTurnCompleted({
19217
+ markTurnClosed({
18830
19218
  conversation: preparedState.conversation,
18831
19219
  nowMs: Date.now(),
19220
+ sessionId: turnId,
18832
19221
  updateConversationStats
18833
19222
  });
18834
19223
  await persistThreadState(thread, {
@@ -18845,12 +19234,30 @@ function createReplyToThread(deps) {
18845
19234
  markTurnFailed({
18846
19235
  conversation: preparedState.conversation,
18847
19236
  nowMs: Date.now(),
19237
+ sessionId: turnId,
18848
19238
  userMessageId: preparedState.userMessageId,
18849
19239
  markConversationMessage: (conversation, messageId, patch) => {
18850
19240
  markConversationMessage(conversation, messageId, patch);
18851
19241
  },
18852
19242
  updateConversationStats
18853
19243
  });
19244
+ if (conversationId) {
19245
+ try {
19246
+ await failAgentTurnSessionCheckpoint({
19247
+ conversationId,
19248
+ sessionId: turnId,
19249
+ errorMessage: "Agent turn failed before final reply delivery completed"
19250
+ });
19251
+ } catch (checkpointError) {
19252
+ logException(
19253
+ checkpointError,
19254
+ "agent_turn_failed_checkpoint_persist_failed",
19255
+ turnTraceContext,
19256
+ {},
19257
+ "Failed to mark failed turn checkpoint"
19258
+ );
19259
+ }
19260
+ }
18854
19261
  await persistThreadState(thread, {
18855
19262
  conversation: preparedState.conversation
18856
19263
  });
@@ -19983,22 +20390,67 @@ async function resolveBuildPluginConfig() {
19983
20390
  try {
19984
20391
  const mod = await import("#junior/config");
19985
20392
  return mod.plugins;
19986
- } catch {
19987
- const env = process.env.JUNIOR_PLUGIN_PACKAGES;
19988
- if (env) {
19989
- try {
19990
- return { packages: JSON.parse(env) };
19991
- } catch {
19992
- }
20393
+ } catch (error) {
20394
+ if (!isMissingVirtualConfig(error)) {
20395
+ throw error;
20396
+ }
20397
+ const packages = readEnvPluginPackages();
20398
+ if (packages) {
20399
+ return { packages };
19993
20400
  }
19994
20401
  return void 0;
19995
20402
  }
19996
20403
  }
20404
+ function isMissingVirtualConfig(error) {
20405
+ if (!(error instanceof Error)) {
20406
+ return false;
20407
+ }
20408
+ const code = error.code;
20409
+ return (code === "ERR_PACKAGE_IMPORT_NOT_DEFINED" || code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") && error.message.includes("#junior/config");
20410
+ }
20411
+ function readEnvPluginPackages() {
20412
+ const env = process.env.JUNIOR_PLUGIN_PACKAGES;
20413
+ if (!env) {
20414
+ return void 0;
20415
+ }
20416
+ let parsed;
20417
+ try {
20418
+ parsed = JSON.parse(env);
20419
+ } catch (error) {
20420
+ throw new Error("JUNIOR_PLUGIN_PACKAGES must be valid JSON", {
20421
+ cause: error
20422
+ });
20423
+ }
20424
+ if (!Array.isArray(parsed) || parsed.some((value) => typeof value !== "string" || !value.trim())) {
20425
+ throw new Error(
20426
+ "JUNIOR_PLUGIN_PACKAGES must be a JSON array of package names"
20427
+ );
20428
+ }
20429
+ return parsed;
20430
+ }
20431
+ function hasConfiguredPluginCatalog(config) {
20432
+ if (!config) {
20433
+ return false;
20434
+ }
20435
+ return Boolean(
20436
+ config.packages?.length || Object.keys(config.manifests ?? {}).length
20437
+ );
20438
+ }
19997
20439
  async function createApp(options) {
19998
20440
  const pluginConfig = options?.plugins ?? await resolveBuildPluginConfig();
19999
- setPluginPackages(pluginConfig?.packages);
20000
- setPluginConfig(pluginConfig);
20001
- setConfigDefaults(options?.configDefaults);
20441
+ const shouldValidatePluginCatalog = hasConfiguredPluginCatalog(pluginConfig) || Boolean(Object.keys(options?.configDefaults ?? {}).length);
20442
+ const previousPluginConfig = setPluginConfig(pluginConfig);
20443
+ const previousConfigDefaults = getConfigDefaults();
20444
+ try {
20445
+ setConfigDefaults(options?.configDefaults);
20446
+ if (shouldValidatePluginCatalog) {
20447
+ getPluginCatalogSignature();
20448
+ }
20449
+ } catch (error) {
20450
+ setPluginConfig(previousPluginConfig);
20451
+ setConfigDefaults(previousConfigDefaults);
20452
+ throw error;
20453
+ }
20002
20454
  const waitUntil = options?.waitUntil ?? await defaultWaitUntil();
20003
20455
  const app = new Hono();
20004
20456
  app.onError((err, c) => {