@sentry/junior 0.53.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 +1934 -1658
  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(),
@@ -1479,6 +1490,9 @@ var StateBackedMcpOAuthClientProvider = class {
1479
1490
  this.sessionContext = sessionContext;
1480
1491
  this.clientMetadata = createClientMetadata(callbackUrl);
1481
1492
  }
1493
+ authSessionId;
1494
+ callbackUrl;
1495
+ sessionContext;
1482
1496
  clientMetadata;
1483
1497
  get redirectUrl() {
1484
1498
  return this.callbackUrl;
@@ -2128,39 +2142,6 @@ function markTurnFailed(args) {
2128
2142
  args.updateConversationStats(args.conversation);
2129
2143
  }
2130
2144
 
2131
- // src/chat/runtime/turn-user-message.ts
2132
- function normalizeSlackMessageTs(value) {
2133
- const trimmed = value?.trim();
2134
- return trimmed && /^\d+(?:\.\d+)?$/.test(trimmed) ? trimmed : void 0;
2135
- }
2136
- function getTurnUserMessage(conversation, sessionId) {
2137
- for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
2138
- const message = conversation.messages[index];
2139
- if (message?.role !== "user") {
2140
- continue;
2141
- }
2142
- if (buildDeterministicTurnId(message.id) === sessionId) {
2143
- return message;
2144
- }
2145
- }
2146
- return void 0;
2147
- }
2148
- function getTurnUserMessageId(conversation, sessionId) {
2149
- return getTurnUserMessage(conversation, sessionId)?.id;
2150
- }
2151
- function getTurnUserSlackMessageTs(message) {
2152
- return normalizeSlackMessageTs(message?.meta?.slackTs) ?? normalizeSlackMessageTs(message?.id);
2153
- }
2154
- function getTurnUserReplyAttachmentContext(message) {
2155
- const inboundAttachmentCount = message?.meta?.attachmentCount ?? 0;
2156
- const imageAttachmentCount = message?.meta?.imageAttachmentCount ?? 0;
2157
- const imagesHydrated = message?.meta?.imagesHydrated === true;
2158
- return {
2159
- ...inboundAttachmentCount > 0 ? { inboundAttachmentCount } : {},
2160
- ...!imagesHydrated && imageAttachmentCount > 0 ? { omittedImageAttachmentCount: imageAttachmentCount } : {}
2161
- };
2162
- }
2163
-
2164
2145
  // src/chat/services/conversation-memory.ts
2165
2146
  var CONTEXT_COMPACTION_TRIGGER_TOKENS = 9e3;
2166
2147
  var CONTEXT_COMPACTION_TARGET_TOKENS = 7e3;
@@ -2451,589 +2432,1015 @@ function getConversationMessageSlackTs(message) {
2451
2432
  return message.meta?.slackTs ?? toOptionalString(message.id);
2452
2433
  }
2453
2434
 
2454
- // src/chat/respond.ts
2455
- import { Agent as Agent2 } from "@mariozechner/pi-agent-core";
2456
-
2457
- // src/chat/prompt.ts
2458
- import fs from "fs";
2459
- import path2 from "path";
2460
-
2461
- // src/chat/turn-context-tag.ts
2462
- var TURN_CONTEXT_TAG = "runtime-turn-context";
2435
+ // src/chat/state/turn-session-store.ts
2436
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS2 } from "chat";
2463
2437
 
2464
- // src/chat/interruption-marker.ts
2465
- var INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
2466
- function getInterruptionMarker() {
2467
- 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}`;
2468
2443
  }
2469
-
2470
- // src/chat/slack/status-format.ts
2471
- var SLACK_STATUS_MAX_LENGTH = 50;
2472
- function truncateStatusText(text) {
2473
- const trimmed = text.trim();
2474
- if (!trimmed) {
2475
- return "";
2476
- }
2477
- if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
2478
- return trimmed;
2479
- }
2480
- return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
2444
+ function parsePiMessage(value) {
2445
+ return isRecord(value) ? value : void 0;
2481
2446
  }
2482
-
2483
- // src/chat/slack/mrkdwn.ts
2484
- function ensureBlockSpacing(text) {
2485
- const codeBlockPattern = /^```/;
2486
- const listItemPattern = /^[-*•]\s|^\d+\.\s/;
2487
- const lines = text.split("\n");
2488
- const result = [];
2489
- let inCodeBlock = false;
2490
- for (let i = 0; i < lines.length; i++) {
2491
- const line = lines[i];
2492
- const isCodeFence = codeBlockPattern.test(line.trimStart());
2493
- if (isCodeFence) {
2494
- if (!inCodeBlock) {
2495
- const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
2496
- if (prev2 !== void 0 && prev2.trim() !== "") {
2497
- result.push("");
2498
- }
2499
- }
2500
- inCodeBlock = !inCodeBlock;
2501
- result.push(line);
2502
- continue;
2503
- }
2504
- if (inCodeBlock) {
2505
- result.push(line);
2506
- continue;
2507
- }
2508
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
2509
- if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
2510
- result.push("");
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;
2511
2455
  }
2512
- result.push(line);
2513
2456
  }
2514
- return result.join("\n");
2457
+ return limit;
2515
2458
  }
2516
- function renderSlackMrkdwn(text) {
2517
- let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
2518
- normalized = ensureBlockSpacing(normalized);
2519
- return normalized.replace(/\n{3,}/g, "\n\n").trim();
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
+ }
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);
2479
+ }
2480
+ return messages.length === messageCount ? messages : void 0;
2520
2481
  }
2521
- function normalizeSlackStatusText(text) {
2522
- const trimmed = text.trim();
2523
- if (!trimmed) {
2524
- return "";
2482
+ async function loadExistingPiSessionMessages(scope, maxCount) {
2483
+ const count = normalizeMessageCount(maxCount);
2484
+ if (count === 0) {
2485
+ return [];
2525
2486
  }
2526
- return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
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
+ }
2501
+ messages.push(message);
2502
+ }
2503
+ return messages;
2504
+ }
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
+ );
2527
2522
  }
2528
2523
 
2529
- // src/chat/slack/output.ts
2530
- var MAX_INLINE_CHARS = 2200;
2531
- var MAX_INLINE_LINES = 45;
2532
- var CONTINUED_MARKER = "\n\n[Continued below]";
2533
- function countSlackLines(text) {
2534
- if (!text) {
2535
- return 0;
2536
- }
2537
- 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}`;
2538
2529
  }
2539
- function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
2540
- 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;
2541
2532
  }
2542
- function findSplitIndex(text, maxChars) {
2543
- if (text.length <= maxChars) {
2544
- return text.length;
2545
- }
2546
- const bounded = text.slice(0, maxChars);
2547
- const newlineIndex = bounded.lastIndexOf("\n");
2548
- if (newlineIndex > 0) {
2549
- return newlineIndex;
2533
+ function parseAgentTurnUsage(value) {
2534
+ if (!isRecord(value)) {
2535
+ return void 0;
2550
2536
  }
2551
- const spaceIndex = bounded.lastIndexOf(" ");
2552
- if (spaceIndex > 0) {
2553
- 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
+ }
2554
2549
  }
2555
- return maxChars;
2550
+ return Object.keys(usage).length > 0 ? usage : void 0;
2556
2551
  }
2557
- function splitByLineBudget(text, maxLines) {
2558
- if (maxLines <= 0) {
2559
- return "";
2552
+ function parseStoredRecord(value) {
2553
+ if (isRecord(value)) {
2554
+ return value;
2560
2555
  }
2561
- const lines = text.split("\n");
2562
- if (lines.length <= maxLines) {
2563
- 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;
2564
2564
  }
2565
- return lines.slice(0, maxLines).join("\n");
2566
- }
2567
- function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
2568
- return {
2569
- maxChars: Math.max(1, maxChars - suffix.length),
2570
- maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
2571
- };
2572
2565
  }
2573
- function forceSplitBudget(text, budget) {
2574
- const lineCount = countSlackLines(text);
2575
- return {
2576
- maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
2577
- maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
2578
- };
2579
- }
2580
- function getFenceContinuation(text) {
2581
- let open;
2582
- for (const line of text.split("\n")) {
2583
- const trimmed = line.trimStart();
2584
- const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
2585
- if (!openerMatch) {
2586
- continue;
2587
- }
2588
- if (!open) {
2589
- open = {
2590
- fence: openerMatch[1],
2591
- openerLine: trimmed
2592
- };
2593
- continue;
2594
- }
2595
- if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
2596
- open = void 0;
2597
- }
2566
+ function parseAgentTurnSessionRecord(value) {
2567
+ const parsed = parseStoredRecord(value);
2568
+ if (!parsed) {
2569
+ return void 0;
2598
2570
  }
2599
- if (!open) {
2600
- return null;
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;
2601
2586
  }
2587
+ const legacyPiMessages = Array.isArray(parsed.piMessages) ? parsed.piMessages : [];
2588
+ const messageCount = toFiniteNonNegativeNumber(parsed.messageCount) ?? legacyPiMessages.length;
2602
2589
  return {
2603
- closeSuffix: text.endsWith("\n") ? open.fence : `
2604
- ${open.fence}`,
2605
- reopenPrefix: `${open.openerLine}
2606
- `
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
+ }
2607
2610
  };
2608
2611
  }
2609
- function appendSlackSuffix(text, marker) {
2610
- const carryover = getFenceContinuation(text);
2611
- return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
2612
- }
2613
- function stripTrailingContinuationMarker(text) {
2614
- return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
2615
- }
2616
- function takeSlackContinuationChunk(text, budget) {
2617
- let { prefix, rest } = takeSlackInlinePrefix(text, budget);
2618
- if (!rest) {
2619
- ({ prefix, rest } = takeSlackInlinePrefix(
2620
- text,
2621
- forceSplitBudget(text, budget)
2622
- ));
2612
+ function materializePiMessages(legacyPiMessages, messageCount, sessionMessages) {
2613
+ if (messageCount === 0) {
2614
+ return [];
2623
2615
  }
2624
- let carryover = rest ? getFenceContinuation(prefix) : null;
2625
- if (!carryover) {
2626
- return { prefix, rest };
2616
+ if (sessionMessages) {
2617
+ return sessionMessages;
2627
2618
  }
2628
- const carryoverBudget = reserveInlineBudgetForSuffix(
2629
- `${carryover.closeSuffix}${CONTINUED_MARKER}`
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)
2630
2629
  );
2631
- ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
2632
- if (!rest) {
2633
- ({ prefix, rest } = takeSlackInlinePrefix(
2634
- text,
2635
- forceSplitBudget(text, carryoverBudget)
2636
- ));
2630
+ const parsed = parseAgentTurnSessionRecord(value);
2631
+ if (!parsed) {
2632
+ return void 0;
2637
2633
  }
2638
- carryover = rest ? getFenceContinuation(prefix) : null;
2639
- if (!carryover) {
2640
- return { prefix, rest };
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;
2641
2646
  }
2642
2647
  return {
2643
- prefix,
2644
- rest: `${carryover.reopenPrefix}${rest}`
2648
+ ...parsed.record,
2649
+ piMessages
2645
2650
  };
2646
2651
  }
2647
- function takeSlackContinuationPrefix(text, options) {
2648
- const budget = {
2649
- maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
2650
- maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
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 } : {}
2651
2690
  };
2652
- const { prefix, rest } = (() => {
2653
- if (options?.forceSplit) {
2654
- return takeSlackContinuationChunk(text, budget);
2655
- }
2656
- const initial = takeSlackInlinePrefix(text, budget);
2657
- return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
2658
- })();
2691
+ await stateAdapter.set(
2692
+ agentTurnSessionKey(args.conversationId, args.sessionId),
2693
+ checkpoint,
2694
+ ttlMs
2695
+ );
2659
2696
  return {
2660
- prefix,
2661
- renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
2662
- rest
2697
+ ...checkpoint,
2698
+ piMessages: [...args.piMessages]
2663
2699
  };
2664
2700
  }
2665
- function takeSlackInlinePrefix(text, options) {
2666
- const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
2667
- const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
2668
- const normalized = text.replace(/\r\n?/g, "\n");
2669
- if (!normalized) {
2670
- return { prefix: "", rest: "" };
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;
2671
2708
  }
2672
- if (fitsInlineBudget(normalized, maxChars, maxLines)) {
2673
- return { prefix: normalized, rest: "" };
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;
2674
2730
  }
2675
- const lineBounded = splitByLineBudget(normalized, maxLines);
2676
- const cutIndex = findSplitIndex(lineBounded, maxChars);
2677
- const prefix = lineBounded.slice(0, cutIndex).trimEnd();
2678
- if (prefix) {
2679
- return {
2680
- prefix,
2681
- rest: normalized.slice(prefix.length).trimStart()
2682
- };
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;
2683
2752
  }
2684
- const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
2685
- return {
2686
- prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
2687
- rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
2688
- };
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());
2689
2754
  }
2690
- function splitSlackReplyText(text, options) {
2691
- const normalized = renderSlackMrkdwn(text);
2692
- if (!normalized) {
2693
- return [];
2755
+ function getConversationPendingAuth(args) {
2756
+ const pendingAuth = args.conversation.processing.pendingAuth;
2757
+ if (!pendingAuth) {
2758
+ return void 0;
2694
2759
  }
2695
- const chunks = [];
2696
- const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
2697
- let remaining = normalized;
2698
- while (remaining) {
2699
- const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
2700
- if (fitsFinalChunk) {
2701
- chunks.push(
2702
- options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
2703
- );
2704
- break;
2705
- }
2706
- const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
2707
- ...continuationBudget,
2708
- forceSplit: true
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."
2709
2782
  });
2710
- chunks.push(renderedPrefix);
2711
- remaining = rest;
2712
2783
  }
2713
- if (chunks.length === 2) {
2714
- chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
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;
2715
2792
  }
2716
- return chunks;
2793
+ return false;
2717
2794
  }
2718
- function getSlackContinuationBudget() {
2719
- return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
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
+ };
2720
2834
  }
2721
- function buildSlackOutputMessage(text, files) {
2722
- const normalized = renderSlackMrkdwn(text);
2723
- const fileCount = files?.length ?? 0;
2724
- if (!normalized) {
2725
- if (fileCount > 0) {
2726
- return {
2727
- raw: "",
2728
- files
2729
- };
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;
2730
2849
  }
2731
- throw new Error(
2732
- `Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
2733
- );
2734
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;
2735
2863
  return {
2736
- markdown: normalized,
2737
- files
2864
+ ...inboundAttachmentCount > 0 ? { inboundAttachmentCount } : {},
2865
+ ...!imagesHydrated && imageAttachmentCount > 0 ? { omittedImageAttachmentCount: imageAttachmentCount } : {}
2738
2866
  };
2739
2867
  }
2740
- var slackOutputPolicy = {
2741
- maxInlineChars: MAX_INLINE_CHARS,
2742
- maxInlineLines: MAX_INLINE_LINES
2743
- };
2868
+
2869
+ // src/chat/respond.ts
2870
+ import { Agent as Agent2 } from "@earendil-works/pi-agent-core";
2744
2871
 
2745
2872
  // src/chat/prompt.ts
2746
- var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
2747
- function getLoggedMarkdownFiles() {
2748
- const globalState = globalThis;
2749
- globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
2750
- return globalState.__juniorLoggedMarkdownFiles;
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;
2751
2883
  }
2752
- function loadOptionalMarkdownFile(candidates, fileName) {
2753
- for (const resolved of candidates) {
2754
- try {
2755
- const raw = fs.readFileSync(resolved, "utf8").trim();
2756
- if (raw.length > 0) {
2757
- const loggedMarkdownFiles = getLoggedMarkdownFiles();
2758
- const logKey = `${fileName}:${resolved}`;
2759
- if (!loggedMarkdownFiles.has(logKey)) {
2760
- loggedMarkdownFiles.add(logKey);
2761
- logInfo(
2762
- `${fileName.toLowerCase()}_loaded`,
2763
- {},
2764
- {
2765
- "file.path": resolved
2766
- },
2767
- `Loaded ${fileName}`
2768
- );
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("");
2769
2913
  }
2770
- return raw;
2771
2914
  }
2772
- } catch {
2915
+ inCodeBlock = !inCodeBlock;
2916
+ result.push(line);
2917
+ continue;
2918
+ }
2919
+ if (inCodeBlock) {
2920
+ result.push(line);
2773
2921
  continue;
2774
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);
2775
2928
  }
2776
- return null;
2929
+ return result.join("\n");
2777
2930
  }
2778
- function loadSoul() {
2779
- const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
2780
- if (soul) {
2781
- return soul;
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 "";
2782
2940
  }
2783
- logWarn(
2784
- "soul_load_fallback",
2785
- {},
2786
- {
2787
- "app.file.candidates": soulPathCandidates()
2788
- },
2789
- "SOUL.md not found; using built-in default personality"
2790
- );
2791
- return DEFAULT_SOUL;
2941
+ return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
2792
2942
  }
2793
- function loadWorld() {
2794
- return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
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;
2795
2953
  }
2796
- var JUNIOR_PERSONALITY = (() => {
2797
- try {
2798
- return loadSoul();
2799
- } catch (error) {
2800
- logWarn(
2801
- "soul_load_failed",
2802
- {},
2803
- {
2804
- "exception.message": error instanceof Error ? error.message : String(error)
2805
- },
2806
- "Failed to load SOUL.md; using built-in default personality"
2807
- );
2808
- return DEFAULT_SOUL;
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;
2809
2960
  }
2810
- })();
2811
- var JUNIOR_WORLD = (() => {
2812
- try {
2813
- return loadWorld();
2814
- } catch (error) {
2815
- logWarn(
2816
- "world_load_failed",
2817
- {},
2818
- {
2819
- "exception.message": error instanceof Error ? error.message : String(error)
2820
- },
2821
- "Failed to load WORLD.md; omitting world prompt context"
2822
- );
2823
- return null;
2961
+ const bounded = text.slice(0, maxChars);
2962
+ const newlineIndex = bounded.lastIndexOf("\n");
2963
+ if (newlineIndex > 0) {
2964
+ return newlineIndex;
2824
2965
  }
2825
- })();
2826
- function workspaceSkillDir(skillName) {
2827
- return sandboxSkillDir(skillName);
2966
+ const spaceIndex = bounded.lastIndexOf(" ");
2967
+ if (spaceIndex > 0) {
2968
+ return spaceIndex;
2969
+ }
2970
+ return maxChars;
2828
2971
  }
2829
- function formatConfigurationValue(value) {
2830
- if (typeof value === "string") {
2831
- return escapeXml(value);
2972
+ function splitByLineBudget(text, maxLines) {
2973
+ if (maxLines <= 0) {
2974
+ return "";
2832
2975
  }
2833
- try {
2834
- return escapeXml(JSON.stringify(value));
2835
- } catch {
2836
- return escapeXml(String(value));
2976
+ const lines = text.split("\n");
2977
+ if (lines.length <= maxLines) {
2978
+ return text;
2837
2979
  }
2980
+ return lines.slice(0, maxLines).join("\n");
2838
2981
  }
2839
- function renderRequesterBlock(fields) {
2840
- const lines = Object.entries(fields).filter(([, value]) => Boolean(value)).map(([key, value]) => `- ${key}: ${escapeXml(value)}`);
2841
- if (lines.length === 0) {
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
3007
+ };
3008
+ continue;
3009
+ }
3010
+ if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
3011
+ open = void 0;
3012
+ }
3013
+ }
3014
+ if (!open) {
2842
3015
  return null;
2843
3016
  }
2844
- return ["<requester>", ...lines, "</requester>"];
3017
+ return {
3018
+ closeSuffix: text.endsWith("\n") ? open.fence : `
3019
+ ${open.fence}`,
3020
+ reopenPrefix: `${open.openerLine}
3021
+ `
3022
+ };
2845
3023
  }
2846
- function renderTag(tag, lines) {
2847
- return [`<${tag}>`, ...lines, `</${tag}>`];
3024
+ function appendSlackSuffix(text, marker) {
3025
+ const carryover = getFenceContinuation(text);
3026
+ return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
2848
3027
  }
2849
- function renderTagBlock(tag, content) {
2850
- return [`<${tag}>`, content, `</${tag}>`].join("\n");
3028
+ function stripTrailingContinuationMarker(text) {
3029
+ return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
2851
3030
  }
2852
- function formatSkillEntry(skill) {
2853
- const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
2854
- const lines = [];
2855
- lines.push(" <skill>");
2856
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
2857
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
2858
- lines.push(` <location>${escapeXml(skillLocation)}</location>`);
2859
- if (skill.pluginProvider) {
2860
- lines.push(` <provider>${escapeXml(skill.pluginProvider)}</provider>`);
3031
+ function takeSlackContinuationChunk(text, budget) {
3032
+ let { prefix, rest } = takeSlackInlinePrefix(text, budget);
3033
+ if (!rest) {
3034
+ ({ prefix, rest } = takeSlackInlinePrefix(
3035
+ text,
3036
+ forceSplitBudget(text, budget)
3037
+ ));
2861
3038
  }
2862
- lines.push(" </skill>");
2863
- return lines;
2864
- }
2865
- function formatAvailableSkillsForPrompt(skills, invocation) {
2866
- const autoSelectable = skills.filter(
2867
- (s) => s.disableModelInvocation !== true
3039
+ let carryover = rest ? getFenceContinuation(prefix) : null;
3040
+ if (!carryover) {
3041
+ return { prefix, rest };
3042
+ }
3043
+ const carryoverBudget = reserveInlineBudgetForSuffix(
3044
+ `${carryover.closeSuffix}${CONTINUED_MARKER}`
2868
3045
  );
2869
- const invokedExplicitOnly = invocation ? skills.filter(
2870
- (s) => s.disableModelInvocation === true && s.name === invocation.skillName
2871
- ) : [];
2872
- const sections = [];
2873
- const available = [
2874
- "<available-skills>",
2875
- ...autoSelectable.length > 0 ? [
2876
- "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."
2877
- ] : []
2878
- ];
2879
- for (const skill of autoSelectable) {
2880
- available.push(...formatSkillEntry(skill));
3046
+ ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
3047
+ if (!rest) {
3048
+ ({ prefix, rest } = takeSlackInlinePrefix(
3049
+ text,
3050
+ forceSplitBudget(text, carryoverBudget)
3051
+ ));
2881
3052
  }
2882
- available.push("</available-skills>");
2883
- sections.push(available.join("\n"));
2884
- if (invokedExplicitOnly.length > 0) {
2885
- const userCallable = [
2886
- "<user-callable-skills>",
2887
- "The user's current message explicitly references this skill by name. Load it when relevant to the request."
2888
- ];
2889
- for (const skill of invokedExplicitOnly) {
2890
- userCallable.push(...formatSkillEntry(skill));
2891
- }
2892
- userCallable.push("</user-callable-skills>");
2893
- sections.push(userCallable.join("\n"));
3053
+ carryover = rest ? getFenceContinuation(prefix) : null;
3054
+ if (!carryover) {
3055
+ return { prefix, rest };
2894
3056
  }
2895
- return sections.join("\n");
3057
+ return {
3058
+ prefix,
3059
+ rest: `${carryover.reopenPrefix}${rest}`
3060
+ };
2896
3061
  }
2897
- function formatLoadedSkillsForPrompt(skills) {
2898
- if (skills.length === 0) {
2899
- return "<loaded-skills>\n</loaded-skills>";
2900
- }
2901
- const lines = ["<loaded-skills>"];
2902
- for (const skill of skills) {
2903
- const skillDir = workspaceSkillDir(skill.name);
2904
- lines.push(
2905
- ` <skill name="${escapeXml(skill.name)}" location="${escapeXml(`${skillDir}/SKILL.md`)}">`
2906
- );
2907
- lines.push(
2908
- `Skill directory: ${escapeXml(skillDir)}. Resolve relative paths there; for skill-owned bash commands, cd there first or use absolute paths.`
2909
- );
2910
- lines.push("");
2911
- lines.push(skill.body);
2912
- lines.push(" </skill>");
2913
- }
2914
- lines.push("</loaded-skills>");
2915
- return lines.join("\n");
3062
+ function takeSlackContinuationPrefix(text, options) {
3063
+ const budget = {
3064
+ maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
3065
+ maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
3066
+ };
3067
+ const { prefix, rest } = (() => {
3068
+ if (options?.forceSplit) {
3069
+ return takeSlackContinuationChunk(text, budget);
3070
+ }
3071
+ const initial = takeSlackInlinePrefix(text, budget);
3072
+ return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
3073
+ })();
3074
+ return {
3075
+ prefix,
3076
+ renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
3077
+ rest
3078
+ };
2916
3079
  }
2917
- function formatProviderCatalogForPrompt() {
2918
- const providers = getPluginProviders().map((plugin) => plugin.manifest);
2919
- if (providers.length === 0) {
2920
- return null;
2921
- }
2922
- const lines = [
2923
- "Config keys and default targets per provider; use after a skill is loaded. Run authenticated provider commands directly after resolving target defaults; let the runtime handle auth pauses/resumes."
2924
- ];
2925
- for (const provider of providers) {
2926
- lines.push(`- provider: ${escapeXml(provider.name)}`);
2927
- lines.push(
2928
- ` - config_keys: ${provider.configKeys.length > 0 ? escapeXml(provider.configKeys.join(", ")) : "none"}`
2929
- );
2930
- lines.push(
2931
- ` - default_context: ${provider.target ? escapeXml(
2932
- `${provider.target.type} via ${provider.target.configKey}`
2933
- ) : "none"}`
2934
- );
3080
+ function takeSlackInlinePrefix(text, options) {
3081
+ const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
3082
+ const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
3083
+ const normalized = text.replace(/\r\n?/g, "\n");
3084
+ if (!normalized) {
3085
+ return { prefix: "", rest: "" };
2935
3086
  }
2936
- return lines.join("\n");
2937
- }
2938
- function formatActiveMcpCatalogsForPrompt(catalogs) {
2939
- if (catalogs.length === 0) {
2940
- return null;
3087
+ if (fitsInlineBudget(normalized, maxChars, maxLines)) {
3088
+ return { prefix: normalized, rest: "" };
2941
3089
  }
2942
- const lines = [
2943
- "Active MCP provider catalogs are available through `searchMcpTools`. Call it with provider to list descriptors or with query to narrow results, then pass the exact returned `tool_name` to `callMcpTool`. Put provider fields inside `arguments`."
2944
- ];
2945
- for (const catalog of catalogs) {
2946
- lines.push(" <catalog>");
2947
- lines.push(` <provider>${escapeXml(catalog.provider)}</provider>`);
2948
- lines.push(
2949
- ` <available_tool_count>${catalog.available_tool_count}</available_tool_count>`
2950
- );
2951
- lines.push(" </catalog>");
3090
+ const lineBounded = splitByLineBudget(normalized, maxLines);
3091
+ const cutIndex = findSplitIndex(lineBounded, maxChars);
3092
+ const prefix = lineBounded.slice(0, cutIndex).trimEnd();
3093
+ if (prefix) {
3094
+ return {
3095
+ prefix,
3096
+ rest: normalized.slice(prefix.length).trimStart()
3097
+ };
2952
3098
  }
2953
- return lines.join("\n");
3099
+ const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
3100
+ return {
3101
+ prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
3102
+ rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
3103
+ };
2954
3104
  }
2955
- function formatToolGuidanceForPrompt(tools) {
2956
- const guidedTools = tools.filter(
2957
- (tool2) => Boolean(tool2.promptSnippet?.trim()) || (tool2.promptGuidelines?.length ?? 0) > 0
2958
- );
2959
- if (guidedTools.length === 0) {
2960
- return null;
3105
+ function splitSlackReplyText(text, options) {
3106
+ const normalized = renderSlackMrkdwn(text);
3107
+ if (!normalized) {
3108
+ return [];
2961
3109
  }
2962
- const lines = [];
2963
- for (const tool2 of guidedTools) {
2964
- lines.push(` <tool name="${escapeXml(tool2.name)}">`);
2965
- if (tool2.promptSnippet?.trim()) {
2966
- lines.push(` - ${escapeXml(tool2.promptSnippet.trim())}`);
3110
+ const chunks = [];
3111
+ const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
3112
+ let remaining = normalized;
3113
+ while (remaining) {
3114
+ const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
3115
+ if (fitsFinalChunk) {
3116
+ chunks.push(
3117
+ options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
3118
+ );
3119
+ break;
2967
3120
  }
2968
- if (tool2.promptGuidelines && tool2.promptGuidelines.length > 0) {
2969
- for (const guideline of tool2.promptGuidelines) {
2970
- lines.push(` - ${escapeXml(guideline)}`);
3121
+ const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
3122
+ ...continuationBudget,
3123
+ forceSplit: true
3124
+ });
3125
+ chunks.push(renderedPrefix);
3126
+ remaining = rest;
3127
+ }
3128
+ if (chunks.length === 2) {
3129
+ chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
3130
+ }
3131
+ return chunks;
3132
+ }
3133
+ function getSlackContinuationBudget() {
3134
+ return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
3135
+ }
3136
+ function buildSlackOutputMessage(text, files) {
3137
+ const normalized = renderSlackMrkdwn(text);
3138
+ const fileCount = files?.length ?? 0;
3139
+ if (!normalized) {
3140
+ if (fileCount > 0) {
3141
+ return {
3142
+ raw: "",
3143
+ files
3144
+ };
3145
+ }
3146
+ throw new Error(
3147
+ `Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
3148
+ );
3149
+ }
3150
+ return {
3151
+ markdown: normalized,
3152
+ files
3153
+ };
3154
+ }
3155
+ var slackOutputPolicy = {
3156
+ maxInlineChars: MAX_INLINE_CHARS,
3157
+ maxInlineLines: MAX_INLINE_LINES
3158
+ };
3159
+
3160
+ // src/chat/prompt.ts
3161
+ var DEFAULT_SOUL = "You are Junior, a practical and concise assistant.";
3162
+ function getLoggedMarkdownFiles() {
3163
+ const globalState = globalThis;
3164
+ globalState.__juniorLoggedMarkdownFiles ??= /* @__PURE__ */ new Set();
3165
+ return globalState.__juniorLoggedMarkdownFiles;
3166
+ }
3167
+ function loadOptionalMarkdownFile(candidates, fileName) {
3168
+ for (const resolved of candidates) {
3169
+ try {
3170
+ const raw = fs.readFileSync(resolved, "utf8").trim();
3171
+ if (raw.length > 0) {
3172
+ const loggedMarkdownFiles = getLoggedMarkdownFiles();
3173
+ const logKey = `${fileName}:${resolved}`;
3174
+ if (!loggedMarkdownFiles.has(logKey)) {
3175
+ loggedMarkdownFiles.add(logKey);
3176
+ logInfo(
3177
+ `${fileName.toLowerCase()}_loaded`,
3178
+ {},
3179
+ {
3180
+ "file.path": resolved
3181
+ },
3182
+ `Loaded ${fileName}`
3183
+ );
3184
+ }
3185
+ return raw;
2971
3186
  }
3187
+ } catch {
3188
+ continue;
2972
3189
  }
2973
- lines.push(" </tool>");
2974
3190
  }
2975
- return lines.join("\n");
3191
+ return null;
2976
3192
  }
2977
- function formatReferenceFilesLines() {
2978
- const files = listReferenceFiles();
2979
- if (files.length === 0) {
2980
- return null;
3193
+ function loadSoul() {
3194
+ const soul = loadOptionalMarkdownFile(soulPathCandidates(), "SOUL.md");
3195
+ if (soul) {
3196
+ return soul;
2981
3197
  }
2982
- return files.map((filePath) => {
2983
- const name = path2.basename(filePath);
2984
- return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
2985
- });
3198
+ logWarn(
3199
+ "soul_load_fallback",
3200
+ {},
3201
+ {
3202
+ "app.file.candidates": soulPathCandidates()
3203
+ },
3204
+ "SOUL.md not found; using built-in default personality"
3205
+ );
3206
+ return DEFAULT_SOUL;
2986
3207
  }
2987
- function formatArtifactsLines(artifactState) {
2988
- if (!artifactState) return null;
2989
- const lines = [];
2990
- if (artifactState.lastCanvasId) {
2991
- lines.push(`- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}`);
3208
+ function loadWorld() {
3209
+ return loadOptionalMarkdownFile(worldPathCandidates(), "WORLD.md");
3210
+ }
3211
+ var JUNIOR_PERSONALITY = (() => {
3212
+ try {
3213
+ return loadSoul();
3214
+ } catch (error) {
3215
+ logWarn(
3216
+ "soul_load_failed",
3217
+ {},
3218
+ {
3219
+ "exception.message": error instanceof Error ? error.message : String(error)
3220
+ },
3221
+ "Failed to load SOUL.md; using built-in default personality"
3222
+ );
3223
+ return DEFAULT_SOUL;
2992
3224
  }
2993
- if (artifactState.lastCanvasUrl) {
2994
- lines.push(`- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}`);
3225
+ })();
3226
+ var JUNIOR_WORLD = (() => {
3227
+ try {
3228
+ return loadWorld();
3229
+ } catch (error) {
3230
+ logWarn(
3231
+ "world_load_failed",
3232
+ {},
3233
+ {
3234
+ "exception.message": error instanceof Error ? error.message : String(error)
3235
+ },
3236
+ "Failed to load WORLD.md; omitting world prompt context"
3237
+ );
3238
+ return null;
2995
3239
  }
2996
- if (artifactState.recentCanvases && artifactState.recentCanvases.length > 0) {
2997
- lines.push("- recent_canvases:");
2998
- for (const canvas of artifactState.recentCanvases) {
2999
- lines.push(` - id: ${escapeXml(canvas.id)}`);
3000
- if (canvas.title) lines.push(` title: ${escapeXml(canvas.title)}`);
3001
- if (canvas.url) lines.push(` url: ${escapeXml(canvas.url)}`);
3002
- if (canvas.createdAt) {
3003
- lines.push(` created_at: ${escapeXml(canvas.createdAt)}`);
3004
- }
3005
- }
3240
+ })();
3241
+ function workspaceSkillDir(skillName) {
3242
+ return sandboxSkillDir(skillName);
3243
+ }
3244
+ function formatConfigurationValue(value) {
3245
+ if (typeof value === "string") {
3246
+ return escapeXml(value);
3006
3247
  }
3007
- if (artifactState.lastListId) {
3008
- lines.push(`- last_list_id: ${escapeXml(artifactState.lastListId)}`);
3248
+ try {
3249
+ return escapeXml(JSON.stringify(value));
3250
+ } catch {
3251
+ return escapeXml(String(value));
3009
3252
  }
3010
- if (artifactState.lastListUrl) {
3011
- lines.push(`- last_list_url: ${escapeXml(artifactState.lastListUrl)}`);
3253
+ }
3254
+ function renderRequesterBlock(fields) {
3255
+ const lines = Object.entries(fields).filter(([, value]) => Boolean(value)).map(([key, value]) => `- ${key}: ${escapeXml(value)}`);
3256
+ if (lines.length === 0) {
3257
+ return null;
3012
3258
  }
3013
- return lines.length > 0 ? lines : null;
3259
+ return ["<requester>", ...lines, "</requester>"];
3014
3260
  }
3015
- function formatConfigurationLines(configuration) {
3016
- const keys = Object.keys(configuration ?? {}).sort(
3017
- (a, b) => a.localeCompare(b)
3018
- );
3019
- if (keys.length === 0) return null;
3020
- return keys.map(
3021
- (key) => `- ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`
3261
+ function renderTag(tag, lines) {
3262
+ return [`<${tag}>`, ...lines, `</${tag}>`];
3263
+ }
3264
+ function renderTagBlock(tag, content) {
3265
+ return [`<${tag}>`, content, `</${tag}>`].join("\n");
3266
+ }
3267
+ function formatSkillEntry(skill) {
3268
+ const skillLocation = `${workspaceSkillDir(skill.name)}/SKILL.md`;
3269
+ const lines = [];
3270
+ lines.push(" <skill>");
3271
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
3272
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
3273
+ lines.push(` <location>${escapeXml(skillLocation)}</location>`);
3274
+ if (skill.pluginProvider) {
3275
+ lines.push(` <provider>${escapeXml(skill.pluginProvider)}</provider>`);
3276
+ }
3277
+ lines.push(" </skill>");
3278
+ return lines;
3279
+ }
3280
+ function formatAvailableSkillsForPrompt(skills, invocation) {
3281
+ const autoSelectable = skills.filter(
3282
+ (s) => s.disableModelInvocation !== true
3022
3283
  );
3284
+ const invokedExplicitOnly = invocation ? skills.filter(
3285
+ (s) => s.disableModelInvocation === true && s.name === invocation.skillName
3286
+ ) : [];
3287
+ const sections = [];
3288
+ if (autoSelectable.length > 0) {
3289
+ const available = [
3290
+ "<available-skills>",
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."
3292
+ ];
3293
+ for (const skill of autoSelectable) {
3294
+ available.push(...formatSkillEntry(skill));
3295
+ }
3296
+ available.push("</available-skills>");
3297
+ sections.push(available.join("\n"));
3298
+ }
3299
+ if (invokedExplicitOnly.length > 0) {
3300
+ const userCallable = [
3301
+ "<user-callable-skills>",
3302
+ "The user's current message explicitly references this skill by name. Load it when relevant to the request."
3303
+ ];
3304
+ for (const skill of invokedExplicitOnly) {
3305
+ userCallable.push(...formatSkillEntry(skill));
3306
+ }
3307
+ userCallable.push("</user-callable-skills>");
3308
+ sections.push(userCallable.join("\n"));
3309
+ }
3310
+ return sections.length > 0 ? sections.join("\n") : null;
3023
3311
  }
3024
- function formatSlackCapabilityNames(capabilities) {
3025
- const names = [
3026
- capabilities?.canCreateCanvas ? "canvas_create" : "",
3027
- capabilities?.canPostToChannel ? "channel_post" : "",
3028
- capabilities?.canAddReactions ? "reaction_add" : ""
3029
- ].filter(Boolean);
3030
- return names.length > 0 ? names.join(", ") : "none";
3312
+ function formatLoadedSkillsForPrompt(skills) {
3313
+ if (skills.length === 0) {
3314
+ return null;
3315
+ }
3316
+ const lines = ["<loaded-skills>"];
3317
+ for (const skill of skills) {
3318
+ const skillDir = workspaceSkillDir(skill.name);
3319
+ lines.push(
3320
+ ` <skill name="${escapeXml(skill.name)}" location="${escapeXml(`${skillDir}/SKILL.md`)}">`
3321
+ );
3322
+ lines.push(
3323
+ `Skill directory: ${escapeXml(skillDir)}. Resolve relative paths there; for skill-owned bash commands, cd there first or use absolute paths.`
3324
+ );
3325
+ lines.push("");
3326
+ lines.push(skill.body);
3327
+ lines.push(" </skill>");
3328
+ }
3329
+ lines.push("</loaded-skills>");
3330
+ return lines.join("\n");
3031
3331
  }
3032
- 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.";
3033
- 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.";
3034
- var TOOL_POLICY_RULES = [
3035
- "- 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.",
3036
- "- 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.",
3332
+ function formatProviderCatalogForPrompt() {
3333
+ const providers = getPluginProviders().map((plugin) => plugin.manifest);
3334
+ if (providers.length === 0) {
3335
+ return null;
3336
+ }
3337
+ const lines = [
3338
+ "Config keys and default targets per provider; use after a skill is loaded. Run authenticated provider commands directly after resolving target defaults; let the runtime handle auth pauses/resumes."
3339
+ ];
3340
+ for (const provider of providers) {
3341
+ lines.push(`- provider: ${escapeXml(provider.name)}`);
3342
+ lines.push(
3343
+ ` - config_keys: ${provider.configKeys.length > 0 ? escapeXml(provider.configKeys.join(", ")) : "none"}`
3344
+ );
3345
+ lines.push(
3346
+ ` - default_context: ${provider.target ? escapeXml(
3347
+ `${provider.target.type} via ${provider.target.configKey}`
3348
+ ) : "none"}`
3349
+ );
3350
+ }
3351
+ return lines.join("\n");
3352
+ }
3353
+ function formatActiveMcpCatalogsForPrompt(catalogs) {
3354
+ if (catalogs.length === 0) {
3355
+ return null;
3356
+ }
3357
+ const lines = [
3358
+ "Active MCP provider catalogs are available through `searchMcpTools`. Call it with provider to list descriptors or with query to narrow results, then pass the exact returned `tool_name` to `callMcpTool`. Put provider fields inside `arguments`."
3359
+ ];
3360
+ for (const catalog of catalogs) {
3361
+ lines.push(" <catalog>");
3362
+ lines.push(` <provider>${escapeXml(catalog.provider)}</provider>`);
3363
+ lines.push(
3364
+ ` <available_tool_count>${catalog.available_tool_count}</available_tool_count>`
3365
+ );
3366
+ lines.push(" </catalog>");
3367
+ }
3368
+ return lines.join("\n");
3369
+ }
3370
+ function formatToolGuidanceForPrompt(tools) {
3371
+ const guidedTools = tools.filter(
3372
+ (tool2) => Boolean(tool2.promptSnippet?.trim()) || (tool2.promptGuidelines?.length ?? 0) > 0
3373
+ );
3374
+ if (guidedTools.length === 0) {
3375
+ return null;
3376
+ }
3377
+ const lines = [];
3378
+ for (const tool2 of guidedTools) {
3379
+ lines.push(` <tool name="${escapeXml(tool2.name)}">`);
3380
+ if (tool2.promptSnippet?.trim()) {
3381
+ lines.push(` - ${escapeXml(tool2.promptSnippet.trim())}`);
3382
+ }
3383
+ if (tool2.promptGuidelines && tool2.promptGuidelines.length > 0) {
3384
+ for (const guideline of tool2.promptGuidelines) {
3385
+ lines.push(` - ${escapeXml(guideline)}`);
3386
+ }
3387
+ }
3388
+ lines.push(" </tool>");
3389
+ }
3390
+ return lines.join("\n");
3391
+ }
3392
+ function formatReferenceFilesLines() {
3393
+ const files = listReferenceFiles();
3394
+ if (files.length === 0) {
3395
+ return null;
3396
+ }
3397
+ return files.map((filePath) => {
3398
+ const name = path2.basename(filePath);
3399
+ return `- ${escapeXml(name)} (${escapeXml(`${SANDBOX_DATA_ROOT}/${name}`)})`;
3400
+ });
3401
+ }
3402
+ function formatArtifactsLines(artifactState) {
3403
+ if (!artifactState) return null;
3404
+ const lines = [];
3405
+ if (artifactState.lastCanvasId) {
3406
+ lines.push(`- last_canvas_id: ${escapeXml(artifactState.lastCanvasId)}`);
3407
+ }
3408
+ if (artifactState.lastCanvasUrl) {
3409
+ lines.push(`- last_canvas_url: ${escapeXml(artifactState.lastCanvasUrl)}`);
3410
+ }
3411
+ if (artifactState.recentCanvases && artifactState.recentCanvases.length > 0) {
3412
+ lines.push("- recent_canvases:");
3413
+ for (const canvas of artifactState.recentCanvases) {
3414
+ lines.push(` - id: ${escapeXml(canvas.id)}`);
3415
+ if (canvas.title) lines.push(` title: ${escapeXml(canvas.title)}`);
3416
+ if (canvas.url) lines.push(` url: ${escapeXml(canvas.url)}`);
3417
+ if (canvas.createdAt) {
3418
+ lines.push(` created_at: ${escapeXml(canvas.createdAt)}`);
3419
+ }
3420
+ }
3421
+ }
3422
+ if (artifactState.lastListId) {
3423
+ lines.push(`- last_list_id: ${escapeXml(artifactState.lastListId)}`);
3424
+ }
3425
+ if (artifactState.lastListUrl) {
3426
+ lines.push(`- last_list_url: ${escapeXml(artifactState.lastListUrl)}`);
3427
+ }
3428
+ return lines.length > 0 ? lines : null;
3429
+ }
3430
+ function formatConfigurationLines(configuration) {
3431
+ const keys = Object.keys(configuration ?? {}).sort(
3432
+ (a, b) => a.localeCompare(b)
3433
+ );
3434
+ if (keys.length === 0) return null;
3435
+ return keys.map(
3436
+ (key) => `- ${escapeXml(key)}: ${formatConfigurationValue(configuration?.[key])}`
3437
+ );
3438
+ }
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.";
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.";
3441
+ var TOOL_POLICY_RULES = [
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.",
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.",
3037
3444
  "- Resolve provider action targets before calls: explicit target wins; ambient `<configuration>` fills omitted targets. Treat non-target links/references as context.",
3038
3445
  "- Verification source order: conversation/thread context; user-provided attachments, links, and reference files; local/sandbox files when present; loaded skill references; repository/provider tools; public web. Use the nearest authoritative available source before weaker sources.",
3039
3446
  "- For repository or implementation questions, inspect the target repository first: local checkout when present, otherwise the configured GitHub/source provider. Do not treat loaded skill files as repo source unless the user asks about the skill. Cite file paths, symbols, PRs/issues, commits, or URLs that support the answer.",
@@ -3118,16 +3525,12 @@ function buildIdentitySection() {
3118
3525
  }
3119
3526
  function buildRuntimeSection(params) {
3120
3527
  const lines = [
3121
- `- version: ${escapeXml(getRuntimeMetadata().version ?? "unknown")}`,
3122
- params.modelId ? `- model: ${escapeXml(params.modelId)}` : "",
3123
- params.fastModelId ? `- fast_model: ${escapeXml(params.fastModelId)}` : "",
3124
- params.thinkingLevel ? `- thinking: ${escapeXml(params.thinkingLevel)}` : "",
3125
- params.channelId ? "- channel: slack" : "",
3126
- params.channelId ? `- slack_capabilities: ${escapeXml(
3127
- formatSlackCapabilityNames(params.slackCapabilities)
3128
- )}` : "",
3129
- `- sandbox_workspace: ${escapeXml(SANDBOX_WORKSPACE_ROOT)}`
3528
+ params.conversationId ? `- gen_ai.conversation.id: ${escapeXml(params.conversationId)}` : "",
3529
+ params.traceId ? `- trace_id: ${escapeXml(params.traceId)}` : ""
3130
3530
  ].filter(Boolean);
3531
+ if (lines.length === 0) {
3532
+ return null;
3533
+ }
3131
3534
  return renderTagBlock("runtime", lines.join("\n"));
3132
3535
  }
3133
3536
  function buildContextSection(params) {
@@ -3180,14 +3583,24 @@ function buildContextSection(params) {
3180
3583
  );
3181
3584
  }
3182
3585
  const body = blocks.map((block) => block.join("\n")).join("\n\n");
3586
+ if (!body) {
3587
+ return null;
3588
+ }
3183
3589
  return renderTagBlock("context", body);
3184
3590
  }
3185
3591
  function buildCapabilitiesSection(params) {
3186
3592
  const blocks = [];
3187
- blocks.push(
3188
- formatAvailableSkillsForPrompt(params.availableSkills, params.invocation)
3593
+ const availableSkills = formatAvailableSkillsForPrompt(
3594
+ params.availableSkills,
3595
+ params.invocation
3189
3596
  );
3190
- 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
+ }
3191
3604
  const activeCatalogs = formatActiveMcpCatalogsForPrompt(
3192
3605
  params.activeMcpCatalogs
3193
3606
  );
@@ -3202,6 +3615,9 @@ function buildCapabilitiesSection(params) {
3202
3615
  if (providerCatalog) {
3203
3616
  blocks.push(renderTagBlock("providers", providerCatalog));
3204
3617
  }
3618
+ if (blocks.length === 0) {
3619
+ return null;
3620
+ }
3205
3621
  return renderTagBlock("capabilities", blocks.join("\n\n"));
3206
3622
  }
3207
3623
  var STATIC_SYSTEM_PROMPT = [
@@ -3235,7 +3651,7 @@ function buildTurnContextPrompt(params) {
3235
3651
  }),
3236
3652
  buildRuntimeSection(params.runtime ?? {}),
3237
3653
  `</${TURN_CONTEXT_TAG}>`
3238
- ];
3654
+ ].filter((section) => Boolean(section));
3239
3655
  return sections.join("\n\n");
3240
3656
  }
3241
3657
 
@@ -3949,6 +4365,8 @@ var PluginMcpClient = class {
3949
4365
  this.plugin = plugin;
3950
4366
  this.options = options;
3951
4367
  }
4368
+ plugin;
4369
+ options;
3952
4370
  client;
3953
4371
  lastAttemptedTransportSessionId;
3954
4372
  transport;
@@ -4230,6 +4648,7 @@ var McpToolManager = class {
4230
4648
  }
4231
4649
  }
4232
4650
  }
4651
+ options;
4233
4652
  pluginsByProvider = /* @__PURE__ */ new Map();
4234
4653
  activeProviders = /* @__PURE__ */ new Set();
4235
4654
  authorizationPendingProviders = /* @__PURE__ */ new Set();
@@ -7887,7 +8306,7 @@ function createSystemTimeTool() {
7887
8306
  // src/chat/tools/advisor/tool.ts
7888
8307
  import {
7889
8308
  Agent
7890
- } from "@mariozechner/pi-agent-core";
8309
+ } from "@earendil-works/pi-agent-core";
7891
8310
  import { Type as Type21 } from "@sinclair/typebox";
7892
8311
 
7893
8312
  // src/chat/respond-helpers.ts
@@ -7972,44 +8391,20 @@ function summarizeMessageText(text) {
7972
8391
  }
7973
8392
  return normalized.length > 1200 ? `${normalized.slice(0, 1200)}...` : normalized;
7974
8393
  }
7975
- function buildUserTurnText(userInput, conversationContext, metadata) {
8394
+ function buildUserTurnText(userInput, conversationContext) {
7976
8395
  const trimmedContext = conversationContext?.trim();
7977
- const conversationId = metadata?.sessionContext?.conversationId;
7978
- const traceId = metadata?.turnContext?.traceId;
7979
- if (!trimmedContext && !conversationId && !traceId) {
8396
+ if (!trimmedContext) {
7980
8397
  return userInput;
7981
8398
  }
7982
- const sections = [];
7983
- if (trimmedContext) {
7984
- sections.push(
7985
- "<thread-background>",
7986
- trimmedContext,
7987
- "</thread-background>",
7988
- ""
7989
- );
7990
- }
7991
- if (conversationId) {
7992
- sections.push(
7993
- "<session-context>",
7994
- `- gen_ai.conversation.id: ${conversationId}`,
7995
- "</session-context>",
7996
- ""
7997
- );
7998
- }
7999
- if (traceId) {
8000
- sections.push(
8001
- "<turn-context>",
8002
- `- trace_id: ${traceId}`,
8003
- "</turn-context>",
8004
- ""
8005
- );
8006
- }
8007
- sections.push(
8008
- '<current-instruction priority="highest">',
8399
+ return [
8400
+ "<thread-background>",
8401
+ trimmedContext,
8402
+ "</thread-background>",
8403
+ "",
8404
+ "<current-instruction>",
8009
8405
  userInput,
8010
8406
  "</current-instruction>"
8011
- );
8012
- return sections.join("\n");
8407
+ ].join("\n");
8013
8408
  }
8014
8409
  function encodeNonImageAttachmentForPrompt(attachment) {
8015
8410
  const base64 = attachment.data.toString("base64");
@@ -8161,8 +8556,8 @@ function trimTrailingAssistantMessages(messages) {
8161
8556
  }
8162
8557
 
8163
8558
  // src/chat/tools/advisor/session-store.ts
8164
- import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS2 } from "chat";
8165
- 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;
8166
8561
  function cloneMessages(messages) {
8167
8562
  return structuredClone(messages);
8168
8563
  }
@@ -9137,7 +9532,7 @@ function resolveChannelCapabilities(channelId) {
9137
9532
  // src/chat/pi/traced-stream.ts
9138
9533
  import {
9139
9534
  streamSimple
9140
- } from "@mariozechner/pi-ai";
9535
+ } from "@earendil-works/pi-ai";
9141
9536
  function buildChatStartAttributes(model, context) {
9142
9537
  const attributes = {
9143
9538
  "gen_ai.operation.name": "chat",
@@ -9222,23 +9617,18 @@ import fs4 from "fs/promises";
9222
9617
  import { createHmac, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
9223
9618
  var SANDBOX_EGRESS_PROXY_PATH = "/api/internal/sandbox-egress";
9224
9619
  var SANDBOX_EGRESS_TOKEN_VERSION = "v1";
9620
+ var SANDBOX_EGRESS_HMAC_CONTEXT = "junior.sandbox_egress.v1";
9225
9621
  var SANDBOX_EGRESS_LEASE_PREFIX = "sandbox-egress-lease";
9226
9622
  var DEFAULT_SESSION_TTL_MS = 30 * 60 * 1e3;
9227
9623
  function leaseKey(provider, context) {
9228
9624
  return `${SANDBOX_EGRESS_LEASE_PREFIX}:${provider}:${context.requesterId}:${context.egressId}:${context.contextId}`;
9229
9625
  }
9230
9626
  function getSandboxEgressSecret() {
9231
- const explicit = process.env.JUNIOR_SANDBOX_EGRESS_SECRET?.trim();
9232
- if (explicit) {
9233
- return explicit;
9234
- }
9235
- const sharedInternal = process.env.JUNIOR_INTERNAL_RESUME_SECRET?.trim();
9236
- if (sharedInternal) {
9237
- return sharedInternal;
9627
+ const secret = process.env.JUNIOR_SECRET?.trim();
9628
+ if (secret) {
9629
+ return secret;
9238
9630
  }
9239
- throw new Error(
9240
- "Cannot determine sandbox egress secret (set JUNIOR_SANDBOX_EGRESS_SECRET or JUNIOR_INTERNAL_RESUME_SECRET)"
9241
- );
9631
+ throw new Error("Cannot determine sandbox egress secret (set JUNIOR_SECRET)");
9242
9632
  }
9243
9633
  function base64Url(input) {
9244
9634
  return Buffer.from(input, "utf8").toString("base64url");
@@ -9247,7 +9637,7 @@ function fromBase64Url(input) {
9247
9637
  return Buffer.from(input, "base64url").toString("utf8");
9248
9638
  }
9249
9639
  function signPayload(payload) {
9250
- return createHmac("sha256", getSandboxEgressSecret()).update(payload).digest("base64url");
9640
+ return createHmac("sha256", getSandboxEgressSecret()).update(`${SANDBOX_EGRESS_HMAC_CONTEXT}:${payload}`).digest("base64url");
9251
9641
  }
9252
9642
  function timingSafeMatch(expected, actual) {
9253
9643
  const expectedBuffer = Buffer.from(expected);
@@ -11249,500 +11639,161 @@ function createSandboxExecutor(options) {
11249
11639
  referenceFiles = [...files];
11250
11640
  sessionManager.configureReferenceFiles(files);
11251
11641
  },
11252
- getSandboxId() {
11253
- return sessionManager.getSandboxId();
11254
- },
11255
- getDependencyProfileHash() {
11256
- return sessionManager.getDependencyProfileHash();
11257
- },
11258
- canExecute(toolName) {
11259
- return SANDBOX_TOOL_NAMES.has(toolName);
11260
- },
11261
- async createSandbox() {
11262
- return await sessionManager.createSandbox();
11263
- },
11264
- execute,
11265
- async dispose() {
11266
- await sessionManager.dispose();
11267
- }
11268
- };
11269
- }
11270
-
11271
- // src/chat/runtime/dev-agent-trace.ts
11272
- function shouldEmitDevAgentTrace() {
11273
- return process.env.NODE_ENV === "development";
11274
- }
11275
-
11276
- // src/chat/services/auth-pause.ts
11277
- var AuthorizationPauseError = class extends Error {
11278
- disposition;
11279
- kind;
11280
- provider;
11281
- constructor(kind, provider, disposition) {
11282
- super(
11283
- kind === "mcp" ? `MCP authorization started for ${provider}` : `Plugin authorization started for ${provider}`
11284
- );
11285
- this.name = kind === "mcp" ? "McpAuthorizationPauseError" : "PluginAuthorizationPauseError";
11286
- this.disposition = disposition;
11287
- this.kind = kind;
11288
- this.provider = provider;
11289
- }
11290
- };
11291
-
11292
- // src/chat/runtime/report-progress.ts
11293
- function buildReportedProgressStatus(input) {
11294
- if (!input || typeof input !== "object") {
11295
- return void 0;
11296
- }
11297
- const message = input.message;
11298
- if (typeof message !== "string") {
11299
- return void 0;
11300
- }
11301
- const text = message.trim();
11302
- if (!text) {
11303
- return void 0;
11304
- }
11305
- return { text };
11306
- }
11307
-
11308
- // src/chat/tools/execution/build-sandbox-input.ts
11309
- function buildSandboxInput(toolName, params) {
11310
- const optionalNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
11311
- if (toolName === "bash") {
11312
- return {
11313
- command: String(params.command ?? ""),
11314
- ...optionalNumber(params.timeoutMs) ? { timeoutMs: optionalNumber(params.timeoutMs) } : {}
11315
- };
11316
- }
11317
- if (toolName === "readFile") {
11318
- return {
11319
- path: String(params.path ?? ""),
11320
- ...optionalNumber(params.offset) ? { offset: optionalNumber(params.offset) } : {},
11321
- ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
11322
- };
11323
- }
11324
- if (toolName === "editFile") {
11325
- return {
11326
- path: String(params.path ?? ""),
11327
- edits: Array.isArray(params.edits) ? params.edits : []
11328
- };
11329
- }
11330
- if (toolName === "grep") {
11331
- return {
11332
- pattern: String(params.pattern ?? ""),
11333
- ...typeof params.path === "string" ? { path: params.path } : {},
11334
- ...typeof params.glob === "string" ? { glob: params.glob } : {},
11335
- ...typeof params.ignoreCase === "boolean" ? { ignoreCase: params.ignoreCase } : {},
11336
- ...typeof params.literal === "boolean" ? { literal: params.literal } : {},
11337
- ...optionalNumber(params.context) ? { context: optionalNumber(params.context) } : {},
11338
- ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
11339
- };
11340
- }
11341
- if (toolName === "findFiles") {
11342
- return {
11343
- pattern: String(params.pattern ?? ""),
11344
- ...typeof params.path === "string" ? { path: params.path } : {},
11345
- ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
11346
- };
11347
- }
11348
- if (toolName === "listDir") {
11349
- return {
11350
- ...typeof params.path === "string" ? { path: params.path } : {},
11351
- ...optionalNumber(params.limit) ? { limit: optionalNumber(params.limit) } : {}
11352
- };
11353
- }
11354
- if (toolName === "writeFile") {
11355
- return {
11356
- path: String(params.path ?? ""),
11357
- content: String(params.content ?? "")
11358
- };
11359
- }
11360
- return params;
11361
- }
11362
-
11363
- // src/chat/tools/execution/normalize-result.ts
11364
- function isStructuredToolExecutionResult(value) {
11365
- const content = value?.content;
11366
- return typeof value === "object" && value !== null && Array.isArray(content) && content.every((part) => {
11367
- if (!part || typeof part !== "object") {
11368
- return false;
11369
- }
11370
- const record = part;
11371
- if (record.type === "text") {
11372
- return typeof record.text === "string";
11373
- }
11374
- if (record.type === "image") {
11375
- return typeof record.data === "string" && typeof record.mimeType === "string";
11376
- }
11377
- return false;
11378
- }) && "details" in value;
11379
- }
11380
- function toToolContentText(value) {
11381
- if (typeof value === "string") return value;
11382
- try {
11383
- return JSON.stringify(value);
11384
- } catch {
11385
- return String(value);
11386
- }
11387
- }
11388
- function normalizeToolResult(result, isSandboxResult) {
11389
- const unwrapped = isSandboxResult && result && typeof result === "object" && "result" in result ? result.result : result;
11390
- if (isStructuredToolExecutionResult(unwrapped)) {
11391
- return unwrapped;
11392
- }
11393
- return {
11394
- content: [{ type: "text", text: toToolContentText(unwrapped) }],
11395
- details: unwrapped
11396
- };
11397
- }
11398
-
11399
- // src/chat/credentials/unlink-provider.ts
11400
- async function unlinkProvider(userId, provider, userTokenStore) {
11401
- await Promise.all([
11402
- userTokenStore.delete(userId, provider),
11403
- deleteMcpStoredOAuthCredentials(userId, provider),
11404
- deleteMcpServerSessionId(userId, provider),
11405
- deleteMcpAuthSessionsForUserProvider(userId, provider)
11406
- ]);
11407
- }
11408
-
11409
- // src/chat/state/turn-session-store.ts
11410
- import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS3 } from "chat";
11411
-
11412
- // src/chat/state/pi-session-message-store.ts
11413
- import { isDeepStrictEqual } from "util";
11414
- var PI_SESSION_MESSAGE_PREFIX = "junior:pi_session_message";
11415
- function piSessionMessageKey(scope, index) {
11416
- return `${PI_SESSION_MESSAGE_PREFIX}:${scope.conversationId}:${scope.sessionId}:${index}`;
11417
- }
11418
- function parsePiMessage(value) {
11419
- return isRecord(value) ? value : void 0;
11420
- }
11421
- function normalizeMessageCount(value) {
11422
- return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
11423
- }
11424
- function countMatchingPrefix(left, right) {
11425
- const limit = Math.min(left.length, right.length);
11426
- for (let index = 0; index < limit; index += 1) {
11427
- if (!isDeepStrictEqual(left[index], right[index])) {
11428
- return index;
11429
- }
11430
- }
11431
- return limit;
11432
- }
11433
- async function loadPiSessionMessages(args) {
11434
- const stateAdapter = getStateAdapter();
11435
- await stateAdapter.connect();
11436
- const messageCount = normalizeMessageCount(args.messageCount);
11437
- if (messageCount === 0) {
11438
- return [];
11439
- }
11440
- const values = await Promise.all(
11441
- Array.from(
11442
- { length: messageCount },
11443
- (_, index) => stateAdapter.get(piSessionMessageKey(args, index))
11444
- )
11445
- );
11446
- const messages = [];
11447
- for (const value of values) {
11448
- const message = parsePiMessage(value);
11449
- if (!message) {
11450
- break;
11451
- }
11452
- messages.push(message);
11453
- }
11454
- return messages.length === messageCount ? messages : void 0;
11455
- }
11456
- async function loadExistingPiSessionMessages(scope, maxCount) {
11457
- const count = normalizeMessageCount(maxCount);
11458
- if (count === 0) {
11459
- return [];
11460
- }
11461
- const stateAdapter = getStateAdapter();
11462
- await stateAdapter.connect();
11463
- const values = await Promise.all(
11464
- Array.from(
11465
- { length: count },
11466
- (_, index) => stateAdapter.get(piSessionMessageKey(scope, index))
11467
- )
11468
- );
11469
- const messages = [];
11470
- for (const value of values) {
11471
- const message = parsePiMessage(value);
11472
- if (!message) {
11473
- break;
11474
- }
11475
- messages.push(message);
11476
- }
11477
- return messages;
11478
- }
11479
- async function commitPiSessionMessages(args) {
11480
- const stateAdapter = getStateAdapter();
11481
- await stateAdapter.connect();
11482
- const existingMessages = await loadExistingPiSessionMessages(
11483
- { conversationId: args.conversationId, sessionId: args.sessionId },
11484
- args.messages.length
11485
- );
11486
- const writeFromIndex = countMatchingPrefix(existingMessages, args.messages);
11487
- await Promise.all(
11488
- args.messages.slice(writeFromIndex).map(
11489
- (message, offset) => stateAdapter.set(
11490
- piSessionMessageKey(args, writeFromIndex + offset),
11491
- message,
11492
- args.ttlMs
11493
- )
11494
- )
11495
- );
11496
- }
11497
-
11498
- // src/chat/state/turn-session-store.ts
11499
- var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
11500
- var AGENT_TURN_SESSION_TTL_MS = THREAD_STATE_TTL_MS3;
11501
- function agentTurnSessionKey(conversationId, sessionId) {
11502
- return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
11503
- }
11504
- function toFiniteNonNegativeNumber(value) {
11505
- return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
11506
- }
11507
- function parseAgentTurnUsage(value) {
11508
- if (!isRecord(value)) {
11509
- return void 0;
11510
- }
11511
- const usage = {};
11512
- for (const field of [
11513
- "inputTokens",
11514
- "outputTokens",
11515
- "cachedInputTokens",
11516
- "cacheCreationTokens",
11517
- "totalTokens"
11518
- ]) {
11519
- const count = toFiniteNonNegativeNumber(value[field]);
11520
- if (count !== void 0) {
11521
- usage[field] = count;
11522
- }
11523
- }
11524
- return Object.keys(usage).length > 0 ? usage : void 0;
11525
- }
11526
- function parseStoredRecord(value) {
11527
- if (isRecord(value)) {
11528
- return value;
11529
- }
11530
- if (typeof value !== "string") {
11531
- return void 0;
11532
- }
11533
- try {
11534
- const parsed = JSON.parse(value);
11535
- return isRecord(parsed) ? parsed : void 0;
11536
- } catch {
11537
- return void 0;
11538
- }
11539
- }
11540
- function parseAgentTurnSessionRecord(value) {
11541
- const parsed = parseStoredRecord(value);
11542
- if (!parsed) {
11543
- return void 0;
11544
- }
11545
- const status = parsed.state;
11546
- if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed" && status !== "superseded") {
11547
- return void 0;
11548
- }
11549
- const conversationId = parsed.conversationId;
11550
- const sessionId = parsed.sessionId;
11551
- const sliceId = parsed.sliceId;
11552
- const checkpointVersion = parsed.checkpointVersion;
11553
- const updatedAtMs = parsed.updatedAtMs;
11554
- const cumulativeDurationMs = toFiniteNonNegativeNumber(
11555
- parsed.cumulativeDurationMs
11556
- );
11557
- const cumulativeUsage = parseAgentTurnUsage(parsed.cumulativeUsage);
11558
- if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
11559
- return void 0;
11560
- }
11561
- const legacyPiMessages = Array.isArray(parsed.piMessages) ? parsed.piMessages : [];
11562
- const messageCount = toFiniteNonNegativeNumber(parsed.messageCount) ?? legacyPiMessages.length;
11563
- return {
11564
- legacyPiMessages,
11565
- record: {
11566
- checkpointVersion,
11567
- conversationId,
11568
- sessionId,
11569
- sliceId,
11570
- state: status,
11571
- updatedAtMs,
11572
- messageCount,
11573
- ...cumulativeDurationMs !== void 0 ? { cumulativeDurationMs } : {},
11574
- ...cumulativeUsage ? { cumulativeUsage } : {},
11575
- ...Array.isArray(parsed.loadedSkillNames) ? {
11576
- loadedSkillNames: parsed.loadedSkillNames.filter(
11577
- (value2) => typeof value2 === "string"
11578
- )
11579
- } : {},
11580
- ...parsed.resumeReason === "timeout" || parsed.resumeReason === "auth" ? { resumeReason: parsed.resumeReason } : {},
11581
- ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
11582
- ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
11583
- }
11584
- };
11585
- }
11586
- function materializePiMessages(legacyPiMessages, messageCount, sessionMessages) {
11587
- if (messageCount === 0) {
11588
- return [];
11589
- }
11590
- if (sessionMessages) {
11591
- return sessionMessages;
11592
- }
11593
- if (legacyPiMessages.length >= messageCount) {
11594
- return legacyPiMessages.slice(0, messageCount);
11595
- }
11596
- return void 0;
11597
- }
11598
- async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
11599
- const stateAdapter = getStateAdapter();
11600
- await stateAdapter.connect();
11601
- const value = await stateAdapter.get(
11602
- agentTurnSessionKey(conversationId, sessionId)
11603
- );
11604
- const parsed = parseAgentTurnSessionRecord(value);
11605
- if (!parsed) {
11606
- return void 0;
11607
- }
11608
- const sessionMessages = await loadPiSessionMessages({
11609
- conversationId,
11610
- sessionId,
11611
- messageCount: parsed.record.messageCount
11612
- });
11613
- const piMessages = materializePiMessages(
11614
- parsed.legacyPiMessages,
11615
- parsed.record.messageCount,
11616
- sessionMessages
11617
- );
11618
- if (!piMessages) {
11619
- return void 0;
11620
- }
11621
- return {
11622
- ...parsed.record,
11623
- piMessages
11624
- };
11625
- }
11626
- async function upsertAgentTurnSessionCheckpoint(args) {
11627
- const stateAdapter = getStateAdapter();
11628
- await stateAdapter.connect();
11629
- const existingValue = await stateAdapter.get(
11630
- agentTurnSessionKey(args.conversationId, args.sessionId)
11631
- );
11632
- const existingRecord = parseAgentTurnSessionRecord(existingValue);
11633
- const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
11634
- await commitPiSessionMessages({
11635
- conversationId: args.conversationId,
11636
- sessionId: args.sessionId,
11637
- messages: args.piMessages,
11638
- ttlMs
11639
- });
11640
- const storedMessageCount = args.piMessages.length;
11641
- const checkpoint = {
11642
- checkpointVersion: (existingRecord?.record.checkpointVersion ?? 0) + 1,
11643
- conversationId: args.conversationId,
11644
- sessionId: args.sessionId,
11645
- sliceId: args.sliceId,
11646
- state: args.state,
11647
- updatedAtMs: Date.now(),
11648
- messageCount: storedMessageCount,
11649
- ...typeof args.cumulativeDurationMs === "number" && Number.isFinite(args.cumulativeDurationMs) ? {
11650
- cumulativeDurationMs: Math.max(
11651
- 0,
11652
- Math.floor(args.cumulativeDurationMs)
11653
- )
11654
- } : {},
11655
- ...args.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage } : {},
11656
- ...Array.isArray(args.loadedSkillNames) ? {
11657
- loadedSkillNames: args.loadedSkillNames.filter(
11658
- (value) => typeof value === "string"
11659
- )
11660
- } : {},
11661
- ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
11662
- ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
11663
- ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
11664
- };
11665
- await stateAdapter.set(
11666
- agentTurnSessionKey(args.conversationId, args.sessionId),
11667
- checkpoint,
11668
- ttlMs
11669
- );
11670
- return {
11671
- ...checkpoint,
11672
- piMessages: [...args.piMessages]
11642
+ getSandboxId() {
11643
+ return sessionManager.getSandboxId();
11644
+ },
11645
+ getDependencyProfileHash() {
11646
+ return sessionManager.getDependencyProfileHash();
11647
+ },
11648
+ canExecute(toolName) {
11649
+ return SANDBOX_TOOL_NAMES.has(toolName);
11650
+ },
11651
+ async createSandbox() {
11652
+ return await sessionManager.createSandbox();
11653
+ },
11654
+ execute,
11655
+ async dispose() {
11656
+ await sessionManager.dispose();
11657
+ }
11673
11658
  };
11674
11659
  }
11675
- async function supersedeAgentTurnSessionCheckpoint(args) {
11676
- const existing = await getAgentTurnSessionCheckpoint(
11677
- args.conversationId,
11678
- args.sessionId
11679
- );
11680
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "superseded") {
11681
- return void 0;
11682
- }
11683
- return await upsertAgentTurnSessionCheckpoint({
11684
- conversationId: existing.conversationId,
11685
- sessionId: existing.sessionId,
11686
- sliceId: existing.sliceId,
11687
- state: "superseded",
11688
- piMessages: existing.piMessages,
11689
- cumulativeDurationMs: existing.cumulativeDurationMs,
11690
- cumulativeUsage: existing.cumulativeUsage,
11691
- loadedSkillNames: existing.loadedSkillNames,
11692
- resumeReason: existing.resumeReason,
11693
- resumedFromSliceId: existing.resumedFromSliceId,
11694
- errorMessage: args.errorMessage ?? existing.errorMessage
11695
- });
11660
+
11661
+ // src/chat/runtime/dev-agent-trace.ts
11662
+ function shouldEmitDevAgentTrace() {
11663
+ return process.env.NODE_ENV === "development";
11696
11664
  }
11697
11665
 
11698
- // src/chat/services/pending-auth.ts
11699
- var AUTH_LINK_REUSE_WINDOW_MS = 10 * 60 * 1e3;
11700
- function canReusePendingAuthLink(args) {
11701
- const { pendingAuth } = args;
11702
- if (!pendingAuth) {
11703
- return false;
11666
+ // src/chat/services/auth-pause.ts
11667
+ var AuthorizationPauseError = class extends Error {
11668
+ disposition;
11669
+ kind;
11670
+ provider;
11671
+ constructor(kind, provider, disposition) {
11672
+ super(
11673
+ kind === "mcp" ? `MCP authorization started for ${provider}` : `Plugin authorization started for ${provider}`
11674
+ );
11675
+ this.name = kind === "mcp" ? "McpAuthorizationPauseError" : "PluginAuthorizationPauseError";
11676
+ this.disposition = disposition;
11677
+ this.kind = kind;
11678
+ this.provider = provider;
11704
11679
  }
11705
- return pendingAuth.kind === args.kind && pendingAuth.provider === args.provider && pendingAuth.requesterId === args.requesterId && pendingAuth.linkSentAtMs + AUTH_LINK_REUSE_WINDOW_MS > (args.nowMs ?? Date.now());
11706
- }
11707
- function getConversationPendingAuth(args) {
11708
- const pendingAuth = args.conversation.processing.pendingAuth;
11709
- if (!pendingAuth) {
11680
+ };
11681
+
11682
+ // src/chat/runtime/report-progress.ts
11683
+ function buildReportedProgressStatus(input) {
11684
+ if (!input || typeof input !== "object") {
11710
11685
  return void 0;
11711
11686
  }
11712
- if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.requesterId !== args.requesterId) {
11687
+ const message = input.message;
11688
+ if (typeof message !== "string") {
11713
11689
  return void 0;
11714
11690
  }
11715
- return pendingAuth;
11691
+ const text = message.trim();
11692
+ if (!text) {
11693
+ return void 0;
11694
+ }
11695
+ return { text };
11716
11696
  }
11717
- function clearPendingAuth(conversation, sessionId) {
11718
- if (!conversation.processing.pendingAuth) {
11719
- 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
+ };
11720
11706
  }
11721
- if (sessionId && conversation.processing.pendingAuth.sessionId !== sessionId) {
11722
- 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
+ };
11723
11713
  }
11724
- conversation.processing.pendingAuth = void 0;
11725
- }
11726
- async function applyPendingAuthUpdate(args) {
11727
- const previousPendingAuth = args.conversation.processing.pendingAuth;
11728
- args.conversation.processing.pendingAuth = args.nextPendingAuth;
11729
- if (previousPendingAuth && previousPendingAuth.sessionId !== args.nextPendingAuth.sessionId && args.conversationId) {
11730
- await supersedeAgentTurnSessionCheckpoint({
11731
- conversationId: args.conversationId,
11732
- sessionId: previousPendingAuth.sessionId,
11733
- errorMessage: "Superseded by a newer auth-blocked request in the same conversation."
11734
- });
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
+ };
11735
11749
  }
11750
+ return params;
11736
11751
  }
11737
- function isPendingAuthLatestRequest(conversation, pendingAuth) {
11738
- for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
11739
- const message = conversation.messages[index];
11740
- if (message?.role !== "user") {
11741
- 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;
11742
11759
  }
11743
- 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);
11744
11776
  }
11745
- 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
+ ]);
11746
11797
  }
11747
11798
 
11748
11799
  // src/chat/services/plugin-auth-orchestration.ts
@@ -12257,7 +12308,11 @@ function buildTurnResult(input) {
12257
12308
  hasFiles: replyFiles.length > 0
12258
12309
  });
12259
12310
  const sideEffectOnlySuccess = !primaryText && toolErrorCount === 0 && (reactionPerformed || channelPostPerformed || replyFiles.length > 0);
12260
- 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) {
12261
12316
  logWarn(
12262
12317
  "ai_model_response_empty",
12263
12318
  {
@@ -12276,11 +12331,15 @@ function buildTurnResult(input) {
12276
12331
  "Model returned empty text response"
12277
12332
  );
12278
12333
  }
12279
- const lastAssistant = terminalAssistantMessages.at(-1);
12280
- const stopReason = typeof lastAssistant?.stopReason === "string" ? lastAssistant.stopReason : void 0;
12281
- const errorMessage = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
12282
12334
  const usedPrimaryText = Boolean(primaryText);
12283
- 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
+ }
12284
12343
  const suppressReactionOnlyText = reactionPerformed && !channelPostPerformed && replyFiles.length === 0 && Boolean(primaryText) && isReactionOnlyIntent(userInput);
12285
12344
  const rawResponseText = suppressReactionOnlyText ? "" : primaryText;
12286
12345
  const responseText = canvasCreated && isVerbosePostCanvasReply(rawResponseText) ? buildBriefPostCanvasReply(artifactStatePatch) : rawResponseText;
@@ -12334,6 +12393,27 @@ function buildTurnResult(input) {
12334
12393
  };
12335
12394
  }
12336
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
+
12337
12417
  // src/chat/services/turn-thinking-level.ts
12338
12418
  import { z } from "zod";
12339
12419
  var CLASSIFIER_CONFIDENCE_THRESHOLD = 0.75;
@@ -12737,7 +12817,7 @@ async function persistAuthPauseCheckpoint(args) {
12737
12817
  const piMessages = trimTrailingAssistantMessages(
12738
12818
  args.messages.length > 0 ? args.messages : latestCheckpoint?.piMessages ?? []
12739
12819
  );
12740
- await upsertAgentTurnSessionCheckpoint({
12820
+ return await upsertAgentTurnSessionCheckpoint({
12741
12821
  conversationId: args.conversationId,
12742
12822
  cumulativeDurationMs: addDurationMs(
12743
12823
  latestCheckpoint?.cumulativeDurationMs,
@@ -12768,7 +12848,7 @@ async function persistAuthPauseCheckpoint(args) {
12768
12848
  "Failed to persist auth checkpoint before retry"
12769
12849
  );
12770
12850
  }
12771
- return nextSliceId;
12851
+ return void 0;
12772
12852
  }
12773
12853
  async function persistTimeoutCheckpoint(args) {
12774
12854
  const nextSliceId = args.currentSliceId + 1;
@@ -12907,6 +12987,10 @@ function createMcpAuthOrchestration(deps, abortAgent) {
12907
12987
  }
12908
12988
 
12909
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
+ }
12910
12994
  var startupDiscoveryLogged = false;
12911
12995
  var MAX_ROUTER_ATTACHMENT_PREVIEW_CHARS = 2e3;
12912
12996
  function buildOmittedImageAttachmentNotice(count) {
@@ -13204,11 +13288,7 @@ async function generateAssistantReply(messageText, context = {}) {
13204
13288
  const promptConversationContext = context.piMessages && context.piMessages.length > 0 ? void 0 : context.conversationContext;
13205
13289
  const userTurnText = buildUserTurnText(
13206
13290
  userInput,
13207
- promptConversationContext,
13208
- {
13209
- sessionContext: { conversationId: sessionConversationId },
13210
- turnContext: { traceId: getActiveTraceId() }
13211
- }
13291
+ promptConversationContext
13212
13292
  );
13213
13293
  const { routerBlocks, userContentParts } = buildUserTurnInput({
13214
13294
  omittedImageAttachmentCount: context.omittedImageAttachmentCount ?? 0,
@@ -13393,11 +13473,8 @@ async function generateAssistantReply(messageText, context = {}) {
13393
13473
  activeMcpCatalogs,
13394
13474
  toolGuidance,
13395
13475
  runtime: {
13396
- channelId: toolChannelId,
13397
- fastModelId: botConfig.fastModelId,
13398
- modelId: botConfig.modelId,
13399
- slackCapabilities: channelCapabilities,
13400
- thinkingLevel: thinkingSelection.thinkingLevel
13476
+ conversationId: spanContext.conversationId,
13477
+ traceId: getActiveTraceId()
13401
13478
  },
13402
13479
  invocation: skillInvocation,
13403
13480
  requester: context.requester,
@@ -13545,67 +13622,96 @@ async function generateAssistantReply(messageText, context = {}) {
13545
13622
  freshPromptMessage
13546
13623
  ]);
13547
13624
  }
13548
- const promptPromise = resumedFromCheckpoint ? agent.continue() : agent.prompt(freshPromptMessage);
13549
- let timeoutId;
13550
- const timeoutPromise = new Promise((_, reject) => {
13551
- timeoutId = setTimeout(() => {
13552
- timedOut = true;
13553
- agent.abort();
13554
- reject(
13555
- new Error(
13556
- `Agent turn timed out after ${botConfig.turnTimeoutMs}ms`
13557
- )
13558
- );
13559
- }, botConfig.turnTimeoutMs);
13560
- });
13561
- try {
13562
- promptResult = await Promise.race([promptPromise, timeoutPromise]);
13563
- } catch (error) {
13564
- if (timedOut) {
13565
- logWarn(
13566
- "agent_turn_timeout",
13567
- {},
13568
- {
13569
- "gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
13570
- "gen_ai.operation.name": "invoke_agent",
13571
- "gen_ai.request.model": botConfig.modelId,
13572
- ...thinkingSelection ? {
13573
- "app.ai.reasoning_effort": thinkingSelection.thinkingLevel
13574
- } : {},
13575
- "app.ai.turn_timeout_ms": botConfig.turnTimeoutMs
13576
- },
13577
- "Agent turn timed out and was aborted"
13578
- );
13579
- await promptPromise.catch(() => {
13580
- });
13581
- 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
+ }
13582
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
+ });
13583
13689
  if (getPendingAuthPause()) {
13584
13690
  timeoutResumeMessages = [...agent.state.messages];
13585
13691
  throw getPendingAuthPause();
13586
13692
  }
13587
- throw error;
13588
- } finally {
13589
- if (timeoutId) {
13590
- 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;
13591
13697
  }
13592
- }
13593
- newMessages = agent.state.messages.slice(beforeMessageCount);
13594
- const outputMessages = newMessages.filter(isAssistantMessage);
13595
- const outputMessagesAttribute = serializeGenAiAttribute(outputMessages);
13596
- const usageSummary = extractGenAiUsageSummary(
13597
- promptResult,
13598
- agent.state,
13599
- ...outputMessages
13600
- );
13601
- turnUsage = hasAgentTurnUsage(usageSummary) ? usageSummary : void 0;
13602
- setSpanAttributes({
13603
- ...outputMessagesAttribute ? { "gen_ai.output.messages": outputMessagesAttribute } : {},
13604
- ...extractGenAiUsageAttributes(usageSummary)
13605
- });
13606
- if (getPendingAuthPause()) {
13607
- timeoutResumeMessages = [...agent.state.messages];
13608
- 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();
13609
13715
  }
13610
13716
  },
13611
13717
  {
@@ -13682,7 +13788,7 @@ async function generateAssistantReply(messageText, context = {}) {
13682
13788
  beforeMessageCount
13683
13789
  );
13684
13790
  }
13685
- const nextSliceId = await persistAuthPauseCheckpoint({
13791
+ const checkpoint = await persistAuthPauseCheckpoint({
13686
13792
  conversationId: timeoutResumeConversationId,
13687
13793
  sessionId: timeoutResumeSessionId,
13688
13794
  currentSliceId: timeoutResumeSliceId,
@@ -13693,21 +13799,23 @@ async function generateAssistantReply(messageText, context = {}) {
13693
13799
  errorMessage: error.message,
13694
13800
  logContext: checkpointLogContext
13695
13801
  });
13696
- throw new RetryableTurnError(
13697
- error.kind === "plugin" ? "plugin_auth_resume" : "mcp_auth_resume",
13698
- `conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`,
13699
- {
13700
- authDisposition: error.disposition,
13701
- authDurationMs: Date.now() - replyStartedAtMs,
13702
- authKind: error.kind,
13703
- authProvider: error.provider,
13704
- authThinkingLevel: thinkingSelection?.thinkingLevel,
13705
- authUsage: turnUsage,
13706
- conversationId: timeoutResumeConversationId,
13707
- sessionId: timeoutResumeSessionId,
13708
- sliceId: nextSliceId
13709
- }
13710
- );
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
+ }
13711
13819
  }
13712
13820
  if (isRetryableTurnError(error)) {
13713
13821
  throw error;
@@ -14923,16 +15031,10 @@ function createResumeReplyContext(args, statusSession) {
14923
15031
  };
14924
15032
  }
14925
15033
  async function resumeSlackTurn(args) {
14926
- if (!args.replyContext?.requester?.userId) {
14927
- throw new Error("Resumed turn requires replyContext.requester.userId");
14928
- }
14929
15034
  const stateAdapter = getStateAdapter();
14930
15035
  await stateAdapter.connect();
14931
15036
  const lockKey = args.lockKey ?? getDefaultLockKey(args.channelId, args.threadTs);
14932
- const lock = await stateAdapter.acquireLock(
14933
- lockKey,
14934
- botConfig.turnTimeoutMs + 6e4
14935
- );
15037
+ const lock = await stateAdapter.acquireLock(lockKey, ACTIVE_LOCK_TTL_MS);
14936
15038
  if (!lock) {
14937
15039
  throw new ResumeTurnBusyError(lockKey);
14938
15040
  }
@@ -14944,31 +15046,44 @@ async function resumeSlackTurn(args) {
14944
15046
  let deferredPauseKind;
14945
15047
  let deferredPauseHandler;
14946
15048
  let deferredFailureHandler;
15049
+ let finalReplyDelivered = false;
15050
+ let postDeliveryCommitError;
15051
+ let runArgs = args;
14947
15052
  try {
14948
- 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) {
14949
15064
  processingReaction = await startSlackProcessingReactionForMessage({
14950
- channelId: args.channelId,
14951
- timestamp: args.messageTs,
15065
+ channelId: runArgs.channelId,
15066
+ timestamp: runArgs.messageTs,
14952
15067
  logException,
14953
- logContext: { ...getResumeLogContext(args, lockKey) }
15068
+ logContext: { ...getResumeLogContext(runArgs, lockKey) }
14954
15069
  });
14955
15070
  }
14956
- if (args.initialText) {
15071
+ if (runArgs.initialText) {
14957
15072
  await postSlackMessageBestEffort(
14958
- args.channelId,
14959
- args.threadTs,
14960
- args.initialText
15073
+ runArgs.channelId,
15074
+ runArgs.threadTs,
15075
+ runArgs.initialText
14961
15076
  );
14962
15077
  }
14963
15078
  status.start();
14964
- const generateReply = args.generateReply ?? generateAssistantReply;
14965
- const replyContext = createResumeReplyContext(args, status);
15079
+ const generateReply = runArgs.generateReply ?? generateAssistantReply;
15080
+ const replyContext = createResumeReplyContext(runArgs, status);
14966
15081
  const priorCheckpoint = replyContext.correlation?.conversationId && replyContext.correlation?.turnId ? await getAgentTurnSessionCheckpoint(
14967
15082
  replyContext.correlation.conversationId,
14968
15083
  replyContext.correlation.turnId
14969
15084
  ) : void 0;
14970
- const replyPromise = generateReply(args.messageText, replyContext);
14971
- const replyTimeoutMs = resolveReplyTimeoutMs(args.replyTimeoutMs);
15085
+ const replyPromise = generateReply(runArgs.messageText, replyContext);
15086
+ const replyTimeoutMs = resolveReplyTimeoutMs(runArgs.replyTimeoutMs);
14972
15087
  let reply = typeof replyTimeoutMs === "number" ? await Promise.race([
14973
15088
  replyPromise,
14974
15089
  new Promise(
@@ -14985,11 +15100,11 @@ async function resumeSlackTurn(args) {
14985
15100
  reply = finalizeFailedTurnReply({
14986
15101
  reply,
14987
15102
  logException,
14988
- context: getResumeLogContext(args, lockKey)
15103
+ context: getResumeLogContext(runArgs, lockKey)
14989
15104
  });
14990
15105
  await status.stop();
14991
15106
  const footer = buildSlackReplyFooter({
14992
- conversationId: args.replyContext?.correlation?.conversationId ?? lockKey,
15107
+ conversationId: runArgs.replyContext?.correlation?.conversationId ?? lockKey,
14993
15108
  durationMs: typeof priorCheckpoint?.cumulativeDurationMs === "number" || typeof reply.diagnostics.durationMs === "number" ? (priorCheckpoint?.cumulativeDurationMs ?? 0) + (reply.diagnostics.durationMs ?? 0) : void 0,
14994
15109
  thinkingLevel: reply.diagnostics.thinkingLevel,
14995
15110
  usage: addAgentTurnUsage(
@@ -14998,18 +15113,32 @@ async function resumeSlackTurn(args) {
14998
15113
  ) ?? reply.diagnostics.usage
14999
15114
  });
15000
15115
  await postSlackApiReplyPosts({
15001
- channelId: args.channelId,
15002
- threadTs: args.threadTs,
15116
+ channelId: runArgs.channelId,
15117
+ threadTs: runArgs.threadTs,
15003
15118
  posts: planSlackReplyPosts({ reply }),
15004
15119
  fileUploadFailureMode: "best_effort",
15005
15120
  footer
15006
15121
  });
15007
- await args.onSuccess?.(reply);
15122
+ finalReplyDelivered = true;
15123
+ await runArgs.onSuccess?.(reply);
15008
15124
  } catch (error) {
15009
15125
  await status.stop();
15010
- const onAuthPause = args.onAuthPause;
15011
- const onTimeoutPause = args.onTimeoutPause;
15012
- 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) {
15013
15142
  deferredPauseKind = "auth";
15014
15143
  deferredPauseHandler = async () => {
15015
15144
  await onAuthPause(error);
@@ -15026,7 +15155,7 @@ async function resumeSlackTurn(args) {
15026
15155
  error,
15027
15156
  eventName: "slack_resume_turn_failed",
15028
15157
  lockKey,
15029
- resumeArgs: args
15158
+ resumeArgs: runArgs
15030
15159
  });
15031
15160
  };
15032
15161
  }
@@ -15034,20 +15163,30 @@ async function resumeSlackTurn(args) {
15034
15163
  await processingReaction?.stop();
15035
15164
  await stateAdapter.releaseLock(lock);
15036
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
+ }
15037
15176
  if (deferredPauseHandler) {
15038
15177
  try {
15039
15178
  await deferredPauseHandler();
15040
15179
  if (deferredPauseKind === "auth") {
15041
15180
  await postSlackMessageBestEffort(
15042
- args.channelId,
15043
- args.threadTs,
15181
+ runArgs.channelId,
15182
+ runArgs.threadTs,
15044
15183
  buildAuthPauseResponse()
15045
15184
  );
15046
15185
  }
15047
15186
  if (deferredPauseKind === "timeout") {
15048
15187
  await postTurnContinuationNoticeBestEffort({
15049
15188
  lockKey,
15050
- resumeArgs: args
15189
+ resumeArgs: runArgs
15051
15190
  });
15052
15191
  }
15053
15192
  return;
@@ -15057,7 +15196,7 @@ async function resumeSlackTurn(args) {
15057
15196
  error: pauseError,
15058
15197
  eventName: "slack_resume_pause_handler_failed",
15059
15198
  lockKey,
15060
- resumeArgs: args
15199
+ resumeArgs: runArgs
15061
15200
  });
15062
15201
  return;
15063
15202
  }
@@ -15080,6 +15219,8 @@ async function resumeAuthorizedRequest(args) {
15080
15219
  onFailure: args.onFailure,
15081
15220
  onAuthPause: args.onAuthPause,
15082
15221
  onTimeoutPause: args.onTimeoutPause,
15222
+ onPostDeliveryCommitFailure: args.onPostDeliveryCommitFailure,
15223
+ beforeStart: args.beforeStart,
15083
15224
  replyTimeoutMs: args.replyTimeoutMs
15084
15225
  });
15085
15226
  }
@@ -15114,6 +15255,7 @@ async function persistAuthPauseTurnState(args) {
15114
15255
  // src/chat/services/timeout-resume.ts
15115
15256
  import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
15116
15257
  var TURN_TIMEOUT_RESUME_PATH = "/api/internal/turn-resume";
15258
+ var TURN_TIMEOUT_RESUME_HMAC_CONTEXT = "junior.turn_timeout_resume.v1";
15117
15259
  var TURN_TIMEOUT_RESUME_SIGNATURE_VERSION = "v1";
15118
15260
  var TURN_TIMEOUT_RESUME_MAX_SKEW_MS = 5 * 60 * 1e3;
15119
15261
  var TURN_TIMEOUT_RESUME_TIMESTAMP_HEADER = "x-junior-resume-timestamp";
@@ -15137,14 +15279,10 @@ async function getAwaitingTurnContinuationRequest(args) {
15137
15279
  };
15138
15280
  }
15139
15281
  function getTurnTimeoutResumeSecret() {
15140
- const explicit = process.env.JUNIOR_INTERNAL_RESUME_SECRET?.trim();
15141
- if (explicit) {
15142
- return explicit;
15143
- }
15144
- return getSlackSigningSecret();
15282
+ return process.env.JUNIOR_SECRET?.trim() || void 0;
15145
15283
  }
15146
15284
  function buildSignedPayload(timestamp, body) {
15147
- return `${timestamp}:${body}`;
15285
+ return `${TURN_TIMEOUT_RESUME_HMAC_CONTEXT}:${timestamp}:${body}`;
15148
15286
  }
15149
15287
  function signTurnTimeoutResumeBody(secret, timestamp, body) {
15150
15288
  const digest = createHmac2("sha256", secret).update(buildSignedPayload(timestamp, body)).digest("hex");
@@ -15182,7 +15320,7 @@ async function scheduleTurnTimeoutResume(request) {
15182
15320
  const secret = getTurnTimeoutResumeSecret();
15183
15321
  if (!secret) {
15184
15322
  throw new Error(
15185
- "Cannot determine timeout resume secret (set JUNIOR_INTERNAL_RESUME_SECRET or SLACK_SIGNING_SECRET)"
15323
+ "Cannot determine timeout resume secret (set JUNIOR_SECRET)"
15186
15324
  );
15187
15325
  }
15188
15326
  const body = JSON.stringify(request);
@@ -15280,54 +15418,43 @@ function htmlResponse(kind) {
15280
15418
  const page = CALLBACK_PAGES[kind];
15281
15419
  return htmlCallbackResponse(page.title, page.message, page.status);
15282
15420
  }
15283
- async function buildResumeConversationContext(channelId, threadTs, sessionId) {
15284
- const threadId = `slack:${channelId}:${threadTs}`;
15285
- const conversation = coerceThreadConversationState(
15286
- await getPersistedThreadState(threadId)
15287
- );
15288
- const userMessageId = getTurnUserMessageId(conversation, sessionId);
15289
- return buildConversationContext(conversation, {
15290
- excludeMessageId: userMessageId
15291
- });
15292
- }
15293
15421
  async function persistCompletedReplyState(channelId, threadTs, sessionId, reply) {
15294
15422
  const threadId = `slack:${channelId}:${threadTs}`;
15295
15423
  const currentState = await getPersistedThreadState(threadId);
15296
15424
  const conversation = coerceThreadConversationState(currentState);
15297
15425
  const artifacts = coerceThreadArtifactsState(currentState);
15298
- const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : void 0;
15299
15426
  const userMessage = getTurnUserMessage(conversation, sessionId);
15300
- clearPendingAuth(conversation, sessionId);
15301
- markConversationMessage(conversation, userMessage?.id, {
15302
- replied: true,
15303
- skippedReason: void 0
15304
- });
15305
- upsertConversationMessage(conversation, {
15306
- id: generateConversationId("assistant"),
15307
- role: "assistant",
15308
- text: normalizeConversationText(reply.text) || "[empty response]",
15309
- createdAtMs: Date.now(),
15310
- author: {
15311
- userName: botConfig.userName,
15312
- isBot: true
15313
- },
15314
- meta: {
15315
- replied: true
15316
- }
15317
- });
15318
- markTurnCompleted({
15427
+ const statePatch = buildDeliveredTurnStatePatch({
15428
+ artifacts,
15319
15429
  conversation,
15320
- nowMs: Date.now(),
15430
+ reply,
15321
15431
  sessionId,
15322
- updateConversationStats
15432
+ userMessageId: userMessage?.id
15323
15433
  });
15324
15434
  await persistThreadStateById(threadId, {
15325
- artifacts: nextArtifacts,
15326
- conversation,
15327
- sandboxId: reply.sandboxId,
15328
- sandboxDependencyProfileHash: reply.sandboxDependencyProfileHash
15435
+ ...statePatch
15329
15436
  });
15330
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
+ }
15331
15458
  async function persistFailedReplyState(channelId, threadTs, sessionId) {
15332
15459
  const threadId = `slack:${channelId}:${threadTs}`;
15333
15460
  const currentState = await getPersistedThreadState(threadId);
@@ -15341,6 +15468,11 @@ async function persistFailedReplyState(channelId, threadTs, sessionId) {
15341
15468
  markConversationMessage,
15342
15469
  updateConversationStats
15343
15470
  });
15471
+ await failCheckpointBestEffort({
15472
+ conversationId: threadId,
15473
+ sessionId,
15474
+ errorMessage: "OAuth-resumed MCP turn failed"
15475
+ });
15344
15476
  await persistThreadStateById(threadId, {
15345
15477
  conversation
15346
15478
  });
@@ -15353,7 +15485,6 @@ async function resumeAuthorizedMcpTurn(args) {
15353
15485
  const threadId = `slack:${authSession.channelId}:${authSession.threadTs}`;
15354
15486
  const currentState = await getPersistedThreadState(threadId);
15355
15487
  const conversation = coerceThreadConversationState(currentState);
15356
- const artifacts = coerceThreadArtifactsState(currentState);
15357
15488
  const pendingAuth = getConversationPendingAuth({
15358
15489
  conversation,
15359
15490
  kind: "mcp",
@@ -15379,132 +15510,174 @@ async function resumeAuthorizedMcpTurn(args) {
15379
15510
  if (!userMessage) {
15380
15511
  return;
15381
15512
  }
15382
- const channelConfiguration = getChannelConfigurationServiceById(
15383
- authSession.channelId
15384
- );
15385
- const conversationContext = await buildResumeConversationContext(
15386
- authSession.channelId,
15387
- authSession.threadTs,
15388
- resolvedSessionId
15389
- );
15390
15513
  await resumeAuthorizedRequest({
15391
15514
  messageText: userMessage.text,
15392
15515
  channelId: authSession.channelId,
15393
15516
  threadTs: authSession.threadTs,
15394
15517
  messageTs: getTurnUserSlackMessageTs(userMessage),
15395
- lockKey: authSession.conversationId,
15518
+ lockKey: threadId,
15396
15519
  connectedText: "",
15397
- replyContext: {
15398
- requester: {
15399
- userId: authSession.userId,
15400
- userName: userMessage?.author?.userName,
15401
- fullName: userMessage?.author?.fullName
15402
- },
15403
- correlation: {
15404
- conversationId: authSession.conversationId,
15405
- turnId: resolvedSessionId,
15406
- channelId: authSession.channelId,
15407
- 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,
15408
15528
  requesterId: authSession.userId
15409
- },
15410
- toolChannelId: authSession.toolChannelId ?? artifacts.assistantContextChannelId ?? authSession.channelId,
15411
- conversationContext,
15412
- artifactState: artifacts,
15413
- piMessages: conversation.piMessages,
15414
- configuration: authSession.configuration,
15415
- pendingAuth,
15416
- channelConfiguration,
15417
- sandbox: getPersistedSandboxState(currentState),
15418
- onAuthPending: async (nextPendingAuth) => {
15419
- await applyPendingAuthUpdate({
15420
- conversation,
15421
- conversationId: authSession.conversationId,
15422
- nextPendingAuth
15423
- });
15424
- await persistThreadStateById(threadId, { conversation });
15425
- },
15426
- ...getTurnUserReplyAttachmentContext(userMessage)
15427
- },
15428
- onSuccess: async (reply) => {
15429
- try {
15430
- await persistCompletedReplyState(
15431
- authSession.channelId,
15432
- authSession.threadTs,
15433
- resolvedSessionId,
15434
- reply
15435
- );
15436
- } catch (persistError) {
15437
- logException(
15438
- persistError,
15439
- "mcp_oauth_callback_resume_persist_failed",
15440
- {},
15441
- { "app.credential.provider": provider },
15442
- "Failed to persist resumed MCP turn state"
15443
- );
15529
+ });
15530
+ const lockedSessionId = lockedPendingAuth?.sessionId ?? authSession.sessionId;
15531
+ if (lockedSessionId !== resolvedSessionId) {
15532
+ return false;
15444
15533
  }
15445
- },
15446
- onFailure: async () => {
15447
- try {
15448
- await persistFailedReplyState(
15449
- authSession.channelId,
15450
- authSession.threadTs,
15451
- resolvedSessionId
15452
- );
15453
- } catch (persistError) {
15454
- logException(
15455
- persistError,
15456
- "mcp_oauth_callback_resume_failure_persist_failed",
15457
- {},
15458
- { "app.credential.provider": provider },
15459
- "Failed to persist failed MCP resume state"
15460
- );
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;
15461
15549
  }
15462
- },
15463
- onAuthPause: async (error) => {
15464
- await persistAuthPauseTurnState({
15465
- sessionId: resolvedSessionId,
15466
- threadStateId: `slack:${authSession.channelId}:${authSession.threadTs}`
15467
- });
15468
- logWarn(
15469
- "mcp_oauth_callback_resume_reparked_for_auth",
15470
- {},
15471
- {
15472
- "app.credential.provider": provider,
15473
- ...isRetryableTurnError(error) ? { "app.ai.retryable_reason": error.reason } : {}
15474
- },
15475
- "Resumed MCP turn requested another authorization flow"
15550
+ const lockedUserMessage = getTurnUserMessage(
15551
+ lockedConversation,
15552
+ lockedSessionId
15476
15553
  );
15477
- },
15478
- onTimeoutPause: async (error) => {
15479
- if (!isRetryableTurnError(error, "turn_timeout_resume")) {
15480
- throw error;
15481
- }
15482
- const checkpointVersion = error.metadata?.checkpointVersion;
15483
- const nextSliceId = error.metadata?.sliceId;
15484
- if (typeof checkpointVersion !== "number") {
15485
- throw new Error(
15486
- "Timed-out MCP resume did not include a checkpoint version"
15487
- );
15554
+ if (!lockedUserMessage) {
15555
+ return false;
15488
15556
  }
15489
- if (!canScheduleTurnTimeoutResume(nextSliceId)) {
15490
- logWarn(
15491
- "mcp_oauth_callback_resume_slice_limit_reached",
15492
- {},
15493
- {
15494
- "app.credential.provider": provider,
15495
- ...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
15496
15574
  },
15497
- "Skipped automatic timeout resume because the turn exceeded the slice limit"
15498
- );
15499
- throw new Error(
15500
- "Timed-out turn exceeded the automatic resume slice limit"
15501
- );
15502
- }
15503
- await scheduleTurnTimeoutResume({
15504
- conversationId: authSession.conversationId,
15505
- sessionId: resolvedSessionId,
15506
- expectedCheckpointVersion: checkpointVersion
15507
- });
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
+ };
15508
15681
  }
15509
15682
  });
15510
15683
  }
@@ -15703,52 +15876,42 @@ async function publishAppHomeView(slackClient, userId, userTokenStore) {
15703
15876
  function htmlErrorResponse(title, message, status) {
15704
15877
  return htmlCallbackResponse(escapeXml(title), escapeXml(message), status);
15705
15878
  }
15706
- async function buildCheckpointConversationContext(conversationId, sessionId) {
15707
- const conversation = coerceThreadConversationState(
15708
- await getPersistedThreadState(conversationId)
15709
- );
15710
- const userMessage = getTurnUserMessage(conversation, sessionId);
15711
- return buildConversationContext(conversation, {
15712
- excludeMessageId: userMessage?.id
15713
- });
15714
- }
15715
15879
  async function persistCompletedOAuthReplyState(args) {
15716
15880
  const currentState = await getPersistedThreadState(args.conversationId);
15717
15881
  const conversation = coerceThreadConversationState(currentState);
15718
15882
  const artifacts = coerceThreadArtifactsState(currentState);
15719
- const nextArtifacts = args.reply.artifactStatePatch ? mergeArtifactsState(artifacts, args.reply.artifactStatePatch) : void 0;
15720
15883
  const userMessage = getTurnUserMessage(conversation, args.sessionId);
15721
- clearPendingAuth(conversation, args.sessionId);
15722
- markConversationMessage(conversation, userMessage?.id, {
15723
- replied: true,
15724
- skippedReason: void 0
15725
- });
15726
- upsertConversationMessage(conversation, {
15727
- id: generateConversationId("assistant"),
15728
- role: "assistant",
15729
- text: normalizeConversationText(args.reply.text) || "[empty response]",
15730
- createdAtMs: Date.now(),
15731
- author: {
15732
- userName: botConfig.userName,
15733
- isBot: true
15734
- },
15735
- meta: {
15736
- replied: true
15737
- }
15738
- });
15739
- markTurnCompleted({
15884
+ const statePatch = buildDeliveredTurnStatePatch({
15885
+ artifacts,
15740
15886
  conversation,
15741
- nowMs: Date.now(),
15887
+ reply: args.reply,
15742
15888
  sessionId: args.sessionId,
15743
- updateConversationStats
15889
+ userMessageId: userMessage?.id
15744
15890
  });
15745
15891
  await persistThreadStateById(args.conversationId, {
15746
- artifacts: nextArtifacts,
15747
- conversation,
15748
- sandboxId: args.reply.sandboxId,
15749
- sandboxDependencyProfileHash: args.reply.sandboxDependencyProfileHash
15892
+ ...statePatch
15750
15893
  });
15751
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
+ }
15752
15915
  async function persistFailedOAuthReplyState(args) {
15753
15916
  const currentState = await getPersistedThreadState(args.conversationId);
15754
15917
  const conversation = coerceThreadConversationState(currentState);
@@ -15761,6 +15924,11 @@ async function persistFailedOAuthReplyState(args) {
15761
15924
  markConversationMessage,
15762
15925
  updateConversationStats
15763
15926
  });
15927
+ await failCheckpointBestEffort2({
15928
+ conversationId: args.conversationId,
15929
+ sessionId: args.sessionId,
15930
+ errorMessage: "OAuth-resumed turn failed"
15931
+ });
15764
15932
  await persistThreadStateById(args.conversationId, {
15765
15933
  conversation
15766
15934
  });
@@ -15786,7 +15954,6 @@ async function resumeCheckpointedOAuthTurn(stored) {
15786
15954
  stored.resumeConversationId
15787
15955
  );
15788
15956
  const conversation = coerceThreadConversationState(currentState);
15789
- const artifacts = coerceThreadArtifactsState(currentState);
15790
15957
  const pendingAuth = getConversationPendingAuth({
15791
15958
  conversation,
15792
15959
  kind: "plugin",
@@ -15817,104 +15984,165 @@ async function resumeCheckpointedOAuthTurn(stored) {
15817
15984
  }
15818
15985
  }
15819
15986
  if (!userMessage?.author?.userId || !resolvedSessionId) {
15820
- return false;
15821
- }
15822
- const conversationContext = await buildCheckpointConversationContext(
15823
- stored.resumeConversationId,
15824
- resolvedSessionId
15825
- );
15826
- const channelConfiguration = getChannelConfigurationServiceById(
15827
- stored.channelId
15828
- );
15829
- await resumeSlackTurn({
15830
- messageText: stored.pendingMessage ?? userMessage.text,
15831
- channelId: stored.channelId,
15832
- threadTs: stored.threadTs,
15833
- messageTs: getTurnUserSlackMessageTs(userMessage),
15834
- lockKey: stored.resumeConversationId,
15835
- initialText: "",
15836
- replyContext: {
15837
- requester: {
15838
- userId: userMessage.author.userId,
15839
- userName: userMessage.author.userName,
15840
- fullName: userMessage.author.fullName
15841
- },
15842
- correlation: {
15843
- conversationId: stored.resumeConversationId,
15844
- turnId: resolvedSessionId,
15845
- channelId: stored.channelId,
15846
- threadTs: stored.threadTs,
15847
- requesterId: userMessage.author.userId
15848
- },
15849
- toolChannelId: artifacts.assistantContextChannelId ?? stored.channelId,
15850
- artifactState: artifacts,
15851
- pendingAuth,
15852
- conversationContext,
15853
- channelConfiguration,
15854
- piMessages: conversation.piMessages,
15855
- sandbox: getPersistedSandboxState(currentState),
15856
- onAuthPending: async (nextPendingAuth) => {
15857
- await applyPendingAuthUpdate({
15858
- conversation,
15859
- conversationId: stored.resumeConversationId,
15860
- nextPendingAuth
15861
- });
15862
- await persistThreadStateById(stored.resumeConversationId, {
15863
- conversation
15864
- });
15865
- },
15866
- ...getTurnUserReplyAttachmentContext(userMessage)
15867
- },
15868
- onSuccess: async (reply) => {
15869
- logInfo(
15870
- "oauth_callback_resume_complete",
15871
- {},
15872
- {
15873
- "app.credential.provider": stored.provider,
15874
- "app.ai.outcome": reply.diagnostics.outcome,
15875
- "app.ai.tool_calls": reply.diagnostics.toolCalls.length
15876
- },
15877
- "OAuth callback auto-resumed checkpoint finished replying"
15987
+ return false;
15988
+ }
15989
+ await resumeSlackTurn({
15990
+ messageText: stored.pendingMessage ?? userMessage.text,
15991
+ channelId: stored.channelId,
15992
+ threadTs: stored.threadTs,
15993
+ messageTs: getTurnUserSlackMessageTs(userMessage),
15994
+ lockKey: stored.resumeConversationId,
15995
+ initialText: "",
15996
+ beforeStart: async () => {
15997
+ const lockedCheckpoint = await getAgentTurnSessionCheckpoint(
15998
+ stored.resumeConversationId,
15999
+ stored.resumeSessionId
15878
16000
  );
15879
- await persistCompletedOAuthReplyState({
15880
- conversationId: stored.resumeConversationId,
15881
- sessionId: resolvedSessionId,
15882
- reply
15883
- });
15884
- },
15885
- onFailure: async () => {
15886
- await persistFailedOAuthReplyState({
15887
- conversationId: stored.resumeConversationId,
15888
- sessionId: resolvedSessionId
15889
- });
15890
- },
15891
- onAuthPause: async () => {
15892
- await persistAuthPauseTurnState({
15893
- sessionId: resolvedSessionId,
15894
- threadStateId: stored.resumeConversationId
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
15895
16014
  });
15896
- },
15897
- onTimeoutPause: async (error) => {
15898
- if (!isRetryableTurnError(error, "turn_timeout_resume")) {
15899
- throw error;
16015
+ const lockedSessionId = lockedPendingAuth?.sessionId ?? stored.resumeSessionId;
16016
+ if (lockedSessionId !== resolvedSessionId) {
16017
+ return false;
15900
16018
  }
15901
- const checkpointVersion = error.metadata?.checkpointVersion;
15902
- const nextSliceId = error.metadata?.sliceId;
15903
- if (typeof checkpointVersion !== "number") {
15904
- throw new Error(
15905
- "Timed-out OAuth resume did not include a checkpoint version"
15906
- );
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;
15907
16034
  }
15908
- if (!canScheduleTurnTimeoutResume(nextSliceId)) {
15909
- throw new Error(
15910
- "Timed-out turn exceeded the automatic resume slice limit"
15911
- );
16035
+ const lockedUserMessage = getTurnUserMessage(
16036
+ lockedConversation,
16037
+ lockedSessionId
16038
+ );
16039
+ if (!lockedUserMessage?.author?.userId) {
16040
+ return false;
15912
16041
  }
15913
- await scheduleTurnTimeoutResume({
15914
- conversationId: stored.resumeConversationId,
15915
- sessionId: resolvedSessionId,
15916
- expectedCheckpointVersion: checkpointVersion
15917
- });
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
+ };
15918
16146
  }
15919
16147
  });
15920
16148
  return true;
@@ -16670,7 +16898,7 @@ function isSandboxEgressRequest(request) {
16670
16898
 
16671
16899
  // src/handlers/turn-resume.ts
16672
16900
  var TIMEOUT_RESUME_LOCK_RETRY_DELAYS_MS = [250, 1e3, 2e3];
16673
- function sleep3(ms) {
16901
+ function sleep4(ms) {
16674
16902
  return new Promise((resolve) => setTimeout(resolve, ms));
16675
16903
  }
16676
16904
  async function persistCompletedReplyState2(args) {
@@ -16679,42 +16907,42 @@ async function persistCompletedReplyState2(args) {
16679
16907
  );
16680
16908
  const conversation = coerceThreadConversationState(currentState);
16681
16909
  const artifacts = coerceThreadArtifactsState(currentState);
16682
- const nextArtifacts = args.reply.artifactStatePatch ? mergeArtifactsState(artifacts, args.reply.artifactStatePatch) : void 0;
16683
16910
  const userMessage = getTurnUserMessage(
16684
16911
  conversation,
16685
16912
  args.checkpoint.sessionId
16686
16913
  );
16687
- clearPendingAuth(conversation, args.checkpoint.sessionId);
16688
- markConversationMessage(conversation, userMessage?.id, {
16689
- replied: true,
16690
- skippedReason: void 0
16691
- });
16692
- upsertConversationMessage(conversation, {
16693
- id: generateConversationId("assistant"),
16694
- role: "assistant",
16695
- text: normalizeConversationText(args.reply.text) || "[empty response]",
16696
- createdAtMs: Date.now(),
16697
- author: {
16698
- userName: botConfig.userName,
16699
- isBot: true
16700
- },
16701
- meta: {
16702
- replied: true
16703
- }
16704
- });
16705
- markTurnCompleted({
16914
+ const statePatch = buildDeliveredTurnStatePatch({
16915
+ artifacts,
16706
16916
  conversation,
16707
- nowMs: Date.now(),
16917
+ reply: args.reply,
16708
16918
  sessionId: args.checkpoint.sessionId,
16709
- updateConversationStats
16919
+ userMessageId: userMessage?.id
16710
16920
  });
16711
16921
  await persistThreadStateById(args.checkpoint.conversationId, {
16712
- artifacts: nextArtifacts,
16713
- conversation,
16714
- sandboxId: args.reply.sandboxId,
16715
- sandboxDependencyProfileHash: args.reply.sandboxDependencyProfileHash
16922
+ ...statePatch
16716
16923
  });
16717
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
+ }
16718
16946
  async function persistFailedReplyState2(checkpoint) {
16719
16947
  const currentState = await getPersistedThreadState(checkpoint.conversationId);
16720
16948
  const conversation = coerceThreadConversationState(currentState);
@@ -16727,145 +16955,152 @@ async function persistFailedReplyState2(checkpoint) {
16727
16955
  markConversationMessage,
16728
16956
  updateConversationStats
16729
16957
  });
16958
+ await failCheckpointBestEffort3({
16959
+ checkpoint,
16960
+ errorMessage: "Timed-out turn failed while resuming"
16961
+ });
16730
16962
  await persistThreadStateById(checkpoint.conversationId, {
16731
16963
  conversation
16732
16964
  });
16733
16965
  }
16734
16966
  async function resumeTimedOutTurn(payload) {
16735
- const checkpoint = await getAgentTurnSessionCheckpoint(
16736
- payload.conversationId,
16737
- payload.sessionId
16738
- );
16739
- if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "timeout" || checkpoint.checkpointVersion !== payload.expectedCheckpointVersion) {
16740
- return;
16741
- }
16742
16967
  const thread = parseSlackThreadId(payload.conversationId);
16743
16968
  if (!thread) {
16744
16969
  throw new Error(
16745
16970
  `Timeout resume requires a Slack thread conversation id, got "${payload.conversationId}"`
16746
16971
  );
16747
16972
  }
16748
- const currentState = await getPersistedThreadState(payload.conversationId);
16749
- const conversation = coerceThreadConversationState(currentState);
16750
- const artifacts = coerceThreadArtifactsState(currentState);
16751
- const userMessage = getTurnUserMessage(conversation, payload.sessionId);
16752
- if (!userMessage?.author?.userId) {
16753
- throw new Error(
16754
- `Unable to locate the persisted user message for timeout resume session "${payload.sessionId}"`
16755
- );
16756
- }
16757
- if (conversation.processing.activeTurnId !== payload.sessionId) {
16758
- return;
16759
- }
16760
- const channelConfiguration = getChannelConfigurationServiceById(
16761
- thread.channelId
16762
- );
16763
- const conversationContext = buildConversationContext(conversation, {
16764
- excludeMessageId: userMessage.id
16765
- });
16766
- const sandbox = getPersistedSandboxState(currentState);
16767
16973
  await resumeSlackTurn({
16768
- messageText: userMessage.text,
16974
+ messageText: "",
16769
16975
  channelId: thread.channelId,
16770
16976
  threadTs: thread.threadTs,
16771
16977
  lockKey: payload.conversationId,
16772
- replyContext: {
16773
- requester: {
16774
- userId: userMessage.author.userId,
16775
- userName: userMessage.author.userName,
16776
- fullName: userMessage.author.fullName
16777
- },
16778
- correlation: {
16779
- conversationId: payload.conversationId,
16780
- turnId: payload.sessionId,
16781
- channelId: thread.channelId,
16782
- threadTs: thread.threadTs,
16783
- requesterId: userMessage.author.userId
16784
- },
16785
- toolChannelId: artifacts.assistantContextChannelId ?? thread.channelId,
16786
- artifactState: artifacts,
16787
- pendingAuth: conversation.processing.pendingAuth,
16788
- conversationContext,
16789
- channelConfiguration,
16790
- piMessages: conversation.piMessages,
16791
- sandbox,
16792
- onAuthPending: async (nextPendingAuth) => {
16793
- await applyPendingAuthUpdate({
16794
- conversation,
16795
- conversationId: payload.conversationId,
16796
- nextPendingAuth
16797
- });
16798
- await persistThreadStateById(payload.conversationId, {
16799
- conversation
16800
- });
16801
- },
16802
- ...getTurnUserReplyAttachmentContext(userMessage)
16803
- },
16804
- onSuccess: async (reply) => {
16805
- try {
16806
- await persistCompletedReplyState2({ checkpoint, reply });
16807
- } catch (persistError) {
16808
- logException(
16809
- persistError,
16810
- "timeout_resume_complete_persist_failed",
16811
- {},
16812
- {
16813
- "app.ai.conversation_id": payload.conversationId,
16814
- "app.ai.session_id": payload.sessionId
16815
- },
16816
- "Failed to persist completed timeout-resume state after reply delivery"
16817
- );
16818
- }
16819
- },
16820
- onFailure: async () => {
16821
- await persistFailedReplyState2(checkpoint);
16822
- },
16823
- onAuthPause: async () => {
16824
- await persistAuthPauseTurnState({
16825
- sessionId: payload.sessionId,
16826
- threadStateId: payload.conversationId
16827
- });
16828
- logWarn(
16829
- "timeout_resume_reparked_for_auth",
16830
- {},
16831
- {
16832
- "app.ai.conversation_id": payload.conversationId,
16833
- "app.ai.session_id": payload.sessionId
16834
- },
16835
- "Resumed timed-out turn parked for auth"
16978
+ beforeStart: async () => {
16979
+ const checkpoint = await getAgentTurnSessionCheckpoint(
16980
+ payload.conversationId,
16981
+ payload.sessionId
16836
16982
  );
16837
- },
16838
- onTimeoutPause: async (error) => {
16839
- if (!isRetryableTurnError(error, "turn_timeout_resume")) {
16840
- throw error;
16983
+ if (!checkpoint || checkpoint.state !== "awaiting_resume" || checkpoint.resumeReason !== "timeout" || checkpoint.checkpointVersion !== payload.expectedCheckpointVersion) {
16984
+ return false;
16841
16985
  }
16842
- const checkpointVersion = error.metadata?.checkpointVersion;
16843
- const nextSliceId = error.metadata?.sliceId;
16844
- 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) {
16845
16993
  throw new Error(
16846
- "Timed-out resume turn did not include a checkpoint version"
16994
+ `Unable to locate the persisted user message for timeout resume session "${payload.sessionId}"`
16847
16995
  );
16848
16996
  }
16849
- if (!canScheduleTurnTimeoutResume(nextSliceId)) {
16850
- logWarn(
16851
- "timeout_resume_slice_limit_reached",
16852
- {},
16853
- {
16854
- "app.ai.conversation_id": payload.conversationId,
16855
- "app.ai.session_id": payload.sessionId,
16856
- ...typeof nextSliceId === "number" ? { "app.ai.resume_slice_id": nextSliceId } : {}
16857
- },
16858
- "Skipped automatic timeout resume because the turn exceeded the slice limit"
16859
- );
16860
- throw new Error(
16861
- "Timed-out turn exceeded the automatic resume slice limit"
16862
- );
16997
+ if (conversation.processing.activeTurnId !== payload.sessionId) {
16998
+ return false;
16863
16999
  }
16864
- await scheduleTurnTimeoutResume({
16865
- conversationId: payload.conversationId,
16866
- sessionId: payload.sessionId,
16867
- expectedCheckpointVersion: checkpointVersion
17000
+ const channelConfiguration = getChannelConfigurationServiceById(
17001
+ thread.channelId
17002
+ );
17003
+ const conversationContext = buildConversationContext(conversation, {
17004
+ excludeMessageId: userMessage.id
16868
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
+ };
16869
17104
  }
16870
17105
  });
16871
17106
  }
@@ -16890,8 +17125,9 @@ async function resumeTimedOutTurnWithLockRetry(payload) {
16890
17125
  "app.ai.session_id": payload.sessionId,
16891
17126
  "app.ai.resume_lock_retry_count": attempt
16892
17127
  },
16893
- "Skipped timeout resume because another turn still owns the thread lock"
17128
+ "Rescheduling timeout resume because another turn still owns the thread lock"
16894
17129
  );
17130
+ await scheduleTurnTimeoutResume(payload);
16895
17131
  return;
16896
17132
  }
16897
17133
  logWarn(
@@ -16905,7 +17141,7 @@ async function resumeTimedOutTurnWithLockRetry(payload) {
16905
17141
  },
16906
17142
  "Timeout resume lock was busy; retrying"
16907
17143
  );
16908
- await sleep3(delayMs);
17144
+ await sleep4(delayMs);
16909
17145
  }
16910
17146
  }
16911
17147
  }
@@ -18777,27 +19013,6 @@ function createReplyToThread(deps) {
18777
19013
  context: diagnosticsContext
18778
19014
  });
18779
19015
  }
18780
- markConversationMessage(
18781
- preparedState.conversation,
18782
- preparedState.userMessageId,
18783
- {
18784
- replied: true,
18785
- skippedReason: void 0
18786
- }
18787
- );
18788
- upsertConversationMessage(preparedState.conversation, {
18789
- id: generateConversationId("assistant"),
18790
- role: "assistant",
18791
- text: normalizeConversationText(reply.text) || "[empty response]",
18792
- createdAtMs: Date.now(),
18793
- author: {
18794
- userName: botConfig.userName,
18795
- isBot: true
18796
- },
18797
- meta: {
18798
- replied: true
18799
- }
18800
- });
18801
19016
  const artifactStatePatch = reply.artifactStatePatch ? { ...reply.artifactStatePatch } : {};
18802
19017
  const reactionPerformed = reply.diagnostics.toolCalls.includes(
18803
19018
  "slackMessageAddReaction"
@@ -18870,20 +19085,18 @@ function createReplyToThread(deps) {
18870
19085
  if (titleUpdateResult) {
18871
19086
  artifactStatePatch.assistantTitleSourceMessageId = titleUpdateResult;
18872
19087
  }
18873
- const shouldPersistArtifacts = Object.keys(artifactStatePatch).length > 0;
18874
- const nextArtifacts = shouldPersistArtifacts ? mergeArtifactsState(preparedState.artifacts, artifactStatePatch) : void 0;
18875
- markTurnCompleted({
19088
+ const completedState = buildDeliveredTurnStatePatch({
19089
+ artifactStatePatch,
19090
+ artifacts: preparedState.artifacts,
18876
19091
  conversation: preparedState.conversation,
18877
- nowMs: Date.now(),
19092
+ reply,
18878
19093
  sessionId: turnId,
18879
- updateConversationStats
19094
+ userMessageId: preparedState.userMessageId
18880
19095
  });
18881
19096
  await persistThreadState(thread, {
18882
- artifacts: nextArtifacts,
18883
- conversation: preparedState.conversation,
18884
- sandboxId: reply.sandboxId,
18885
- sandboxDependencyProfileHash: reply.sandboxDependencyProfileHash
19097
+ ...completedState
18886
19098
  });
19099
+ preparedState.conversation = completedState.conversation;
18887
19100
  persistedAtLeastOnce = true;
18888
19101
  if (shouldEmitDevAgentTrace()) {
18889
19102
  logInfo(
@@ -19021,12 +19234,30 @@ function createReplyToThread(deps) {
19021
19234
  markTurnFailed({
19022
19235
  conversation: preparedState.conversation,
19023
19236
  nowMs: Date.now(),
19237
+ sessionId: turnId,
19024
19238
  userMessageId: preparedState.userMessageId,
19025
19239
  markConversationMessage: (conversation, messageId, patch) => {
19026
19240
  markConversationMessage(conversation, messageId, patch);
19027
19241
  },
19028
19242
  updateConversationStats
19029
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
+ }
19030
19261
  await persistThreadState(thread, {
19031
19262
  conversation: preparedState.conversation
19032
19263
  });
@@ -20159,22 +20390,67 @@ async function resolveBuildPluginConfig() {
20159
20390
  try {
20160
20391
  const mod = await import("#junior/config");
20161
20392
  return mod.plugins;
20162
- } catch {
20163
- const env = process.env.JUNIOR_PLUGIN_PACKAGES;
20164
- if (env) {
20165
- try {
20166
- return { packages: JSON.parse(env) };
20167
- } catch {
20168
- }
20393
+ } catch (error) {
20394
+ if (!isMissingVirtualConfig(error)) {
20395
+ throw error;
20396
+ }
20397
+ const packages = readEnvPluginPackages();
20398
+ if (packages) {
20399
+ return { packages };
20169
20400
  }
20170
20401
  return void 0;
20171
20402
  }
20172
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
+ }
20173
20439
  async function createApp(options) {
20174
20440
  const pluginConfig = options?.plugins ?? await resolveBuildPluginConfig();
20175
- setPluginPackages(pluginConfig?.packages);
20176
- setPluginConfig(pluginConfig);
20177
- 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
+ }
20178
20454
  const waitUntil = options?.waitUntil ?? await defaultWaitUntil();
20179
20455
  const app = new Hono();
20180
20456
  app.onError((err, c) => {