@koda-sl/baker-bridge 0.108.0-dev.fe3d33fa3 → 0.109.0-dev.557795bef

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 (321) hide show
  1. package/README.md +107 -51
  2. package/dist/broker/baker-api.d.ts +23 -0
  3. package/dist/broker/baker-api.d.ts.map +1 -0
  4. package/dist/broker/baker-api.js +55 -0
  5. package/dist/broker/baker-api.js.map +1 -0
  6. package/dist/broker/broker-client.d.ts +10 -0
  7. package/dist/broker/broker-client.d.ts.map +1 -0
  8. package/dist/broker/broker-client.js +60 -0
  9. package/dist/broker/broker-client.js.map +1 -0
  10. package/dist/broker/broker-process.d.ts +10 -0
  11. package/dist/broker/broker-process.d.ts.map +1 -0
  12. package/dist/broker/broker-process.js +20 -0
  13. package/dist/broker/broker-process.js.map +1 -0
  14. package/dist/broker/broker-process.test.d.ts +2 -0
  15. package/dist/broker/broker-process.test.d.ts.map +1 -0
  16. package/dist/broker/broker-process.test.js +13 -0
  17. package/dist/broker/broker-process.test.js.map +1 -0
  18. package/dist/broker/ca.d.ts +38 -0
  19. package/dist/broker/ca.d.ts.map +1 -0
  20. package/dist/broker/ca.js +80 -0
  21. package/dist/broker/ca.js.map +1 -0
  22. package/dist/broker/ca.test.d.ts +2 -0
  23. package/dist/broker/ca.test.d.ts.map +1 -0
  24. package/dist/broker/ca.test.js +58 -0
  25. package/dist/broker/ca.test.js.map +1 -0
  26. package/dist/broker/credential-cache.d.ts +24 -0
  27. package/dist/broker/credential-cache.d.ts.map +1 -0
  28. package/dist/broker/credential-cache.js +76 -0
  29. package/dist/broker/credential-cache.js.map +1 -0
  30. package/dist/broker/credential-cache.test.d.ts +2 -0
  31. package/dist/broker/credential-cache.test.d.ts.map +1 -0
  32. package/dist/broker/credential-cache.test.js +136 -0
  33. package/dist/broker/credential-cache.test.js.map +1 -0
  34. package/dist/broker/credential-text.d.ts +45 -0
  35. package/dist/broker/credential-text.d.ts.map +1 -0
  36. package/dist/broker/credential-text.js +103 -0
  37. package/dist/broker/credential-text.js.map +1 -0
  38. package/dist/broker/credential-text.test.d.ts +2 -0
  39. package/dist/broker/credential-text.test.d.ts.map +1 -0
  40. package/dist/broker/credential-text.test.js +54 -0
  41. package/dist/broker/credential-text.test.js.map +1 -0
  42. package/dist/broker/env.d.ts +21 -0
  43. package/dist/broker/env.d.ts.map +1 -0
  44. package/dist/broker/env.js +20 -0
  45. package/dist/broker/env.js.map +1 -0
  46. package/dist/broker/main.d.ts +3 -0
  47. package/dist/broker/main.d.ts.map +1 -0
  48. package/dist/broker/main.js +57 -0
  49. package/dist/broker/main.js.map +1 -0
  50. package/dist/broker/proxy.d.ts +24 -0
  51. package/dist/broker/proxy.d.ts.map +1 -0
  52. package/dist/broker/proxy.integration.test.d.ts +2 -0
  53. package/dist/broker/proxy.integration.test.d.ts.map +1 -0
  54. package/dist/broker/proxy.integration.test.js +197 -0
  55. package/dist/broker/proxy.integration.test.js.map +1 -0
  56. package/dist/broker/proxy.js +147 -0
  57. package/dist/broker/proxy.js.map +1 -0
  58. package/dist/broker/rewrite.d.ts +44 -0
  59. package/dist/broker/rewrite.d.ts.map +1 -0
  60. package/dist/broker/rewrite.js +291 -0
  61. package/dist/broker/rewrite.js.map +1 -0
  62. package/dist/broker/rewrite.test.d.ts +2 -0
  63. package/dist/broker/rewrite.test.d.ts.map +1 -0
  64. package/dist/broker/rewrite.test.js +171 -0
  65. package/dist/broker/rewrite.test.js.map +1 -0
  66. package/dist/broker/socket-service.d.ts +18 -0
  67. package/dist/broker/socket-service.d.ts.map +1 -0
  68. package/dist/broker/socket-service.js +113 -0
  69. package/dist/broker/socket-service.js.map +1 -0
  70. package/dist/broker/socket-service.test.d.ts +2 -0
  71. package/dist/broker/socket-service.test.d.ts.map +1 -0
  72. package/dist/broker/socket-service.test.js +80 -0
  73. package/dist/broker/socket-service.test.js.map +1 -0
  74. package/dist/broker/supervisor-main.d.ts +3 -0
  75. package/dist/broker/supervisor-main.d.ts.map +1 -0
  76. package/dist/broker/supervisor-main.js +70 -0
  77. package/dist/broker/supervisor-main.js.map +1 -0
  78. package/dist/broker/supervisor.d.ts +25 -0
  79. package/dist/broker/supervisor.d.ts.map +1 -0
  80. package/dist/broker/supervisor.js +88 -0
  81. package/dist/broker/supervisor.js.map +1 -0
  82. package/dist/broker/supervisor.test.d.ts +2 -0
  83. package/dist/broker/supervisor.test.d.ts.map +1 -0
  84. package/dist/broker/supervisor.test.js +161 -0
  85. package/dist/broker/supervisor.test.js.map +1 -0
  86. package/dist/broker/token-bucket.d.ts +9 -0
  87. package/dist/broker/token-bucket.d.ts.map +1 -0
  88. package/dist/broker/token-bucket.js +18 -0
  89. package/dist/broker/token-bucket.js.map +1 -0
  90. package/dist/broker/usage-reporter.d.ts +18 -0
  91. package/dist/broker/usage-reporter.d.ts.map +1 -0
  92. package/dist/broker/usage-reporter.js +31 -0
  93. package/dist/broker/usage-reporter.js.map +1 -0
  94. package/dist/broker/usage-reporter.test.d.ts +2 -0
  95. package/dist/broker/usage-reporter.test.d.ts.map +1 -0
  96. package/dist/broker/usage-reporter.test.js +20 -0
  97. package/dist/broker/usage-reporter.test.js.map +1 -0
  98. package/dist/cli.d.ts.map +1 -1
  99. package/dist/cli.js +2 -0
  100. package/dist/cli.js.map +1 -1
  101. package/dist/hono/agent-session.d.ts +4 -0
  102. package/dist/hono/agent-session.d.ts.map +1 -1
  103. package/dist/hono/agent-session.js +28 -2
  104. package/dist/hono/agent-session.js.map +1 -1
  105. package/dist/hono/agent-session.test.js +37 -0
  106. package/dist/hono/agent-session.test.js.map +1 -1
  107. package/dist/hono/agent-tasks.d.ts.map +1 -1
  108. package/dist/hono/agent-tasks.js +11 -7
  109. package/dist/hono/agent-tasks.js.map +1 -1
  110. package/dist/hono/agent-tasks.test.js +15 -0
  111. package/dist/hono/agent-tasks.test.js.map +1 -1
  112. package/dist/hono/agent.d.ts +2 -0
  113. package/dist/hono/agent.d.ts.map +1 -1
  114. package/dist/hono/agent.js +22 -4
  115. package/dist/hono/agent.js.map +1 -1
  116. package/dist/hono/agent.test.js +8 -2
  117. package/dist/hono/agent.test.js.map +1 -1
  118. package/dist/hono/attachments.d.ts +6 -1
  119. package/dist/hono/attachments.d.ts.map +1 -1
  120. package/dist/hono/attachments.js +9 -4
  121. package/dist/hono/attachments.js.map +1 -1
  122. package/dist/hono/attachments.test.js +5 -0
  123. package/dist/hono/attachments.test.js.map +1 -1
  124. package/dist/hono/avatar-accent.d.ts +23 -5
  125. package/dist/hono/avatar-accent.d.ts.map +1 -1
  126. package/dist/hono/avatar-accent.js +54 -63
  127. package/dist/hono/avatar-accent.js.map +1 -1
  128. package/dist/hono/avatar-accent.test.js +64 -35
  129. package/dist/hono/avatar-accent.test.js.map +1 -1
  130. package/dist/hono/avatar-casting.d.ts +1 -0
  131. package/dist/hono/avatar-casting.d.ts.map +1 -1
  132. package/dist/hono/avatar-casting.js +18 -4
  133. package/dist/hono/avatar-casting.js.map +1 -1
  134. package/dist/hono/avatar-casting.test.js +5 -0
  135. package/dist/hono/avatar-casting.test.js.map +1 -1
  136. package/dist/hono/avatar-face-question.d.ts +23 -6
  137. package/dist/hono/avatar-face-question.d.ts.map +1 -1
  138. package/dist/hono/avatar-face-question.js +48 -59
  139. package/dist/hono/avatar-face-question.js.map +1 -1
  140. package/dist/hono/avatar-face-question.test.js +46 -101
  141. package/dist/hono/avatar-face-question.test.js.map +1 -1
  142. package/dist/hono/baker-ui-tool-schemas.test.js +30 -4
  143. package/dist/hono/baker-ui-tool-schemas.test.js.map +1 -1
  144. package/dist/hono/blocking-card.d.ts +12 -0
  145. package/dist/hono/blocking-card.d.ts.map +1 -0
  146. package/dist/hono/blocking-card.js +12 -0
  147. package/dist/hono/blocking-card.js.map +1 -0
  148. package/dist/hono/bridge-job.d.ts +78 -3
  149. package/dist/hono/bridge-job.d.ts.map +1 -1
  150. package/dist/hono/bridge-job.js +322 -61
  151. package/dist/hono/bridge-job.js.map +1 -1
  152. package/dist/hono/bridge-job.test.js +551 -26
  153. package/dist/hono/bridge-job.test.js.map +1 -1
  154. package/dist/hono/check-content.d.ts +49 -0
  155. package/dist/hono/check-content.d.ts.map +1 -0
  156. package/dist/hono/check-content.js +182 -0
  157. package/dist/hono/check-content.js.map +1 -0
  158. package/dist/hono/check-content.test.d.ts +2 -0
  159. package/dist/hono/check-content.test.d.ts.map +1 -0
  160. package/dist/hono/check-content.test.js +87 -0
  161. package/dist/hono/check-content.test.js.map +1 -0
  162. package/dist/hono/connection-input.d.ts +163 -12
  163. package/dist/hono/connection-input.d.ts.map +1 -1
  164. package/dist/hono/connection-input.js +162 -12
  165. package/dist/hono/connection-input.js.map +1 -1
  166. package/dist/hono/connection-input.test.js +112 -9
  167. package/dist/hono/connection-input.test.js.map +1 -1
  168. package/dist/hono/convex.d.ts +8 -0
  169. package/dist/hono/convex.d.ts.map +1 -1
  170. package/dist/hono/convex.js +8 -0
  171. package/dist/hono/convex.js.map +1 -1
  172. package/dist/hono/convex.test.js +4 -1
  173. package/dist/hono/convex.test.js.map +1 -1
  174. package/dist/hono/credential-input.d.ts +39 -0
  175. package/dist/hono/credential-input.d.ts.map +1 -0
  176. package/dist/hono/credential-input.js +78 -0
  177. package/dist/hono/credential-input.js.map +1 -0
  178. package/dist/hono/credential-scrub.d.ts +42 -0
  179. package/dist/hono/credential-scrub.d.ts.map +1 -0
  180. package/dist/hono/credential-scrub.js +173 -0
  181. package/dist/hono/credential-scrub.js.map +1 -0
  182. package/dist/hono/credential-scrub.test.d.ts +2 -0
  183. package/dist/hono/credential-scrub.test.d.ts.map +1 -0
  184. package/dist/hono/credential-scrub.test.js +130 -0
  185. package/dist/hono/credential-scrub.test.js.map +1 -0
  186. package/dist/hono/env.d.ts +1 -0
  187. package/dist/hono/env.d.ts.map +1 -1
  188. package/dist/hono/env.js +2 -0
  189. package/dist/hono/env.js.map +1 -1
  190. package/dist/hono/flow-crypto.d.ts +2 -1
  191. package/dist/hono/flow-crypto.d.ts.map +1 -1
  192. package/dist/hono/flow-crypto.js +50 -26
  193. package/dist/hono/flow-crypto.js.map +1 -1
  194. package/dist/hono/flow-crypto.test.js +32 -0
  195. package/dist/hono/flow-crypto.test.js.map +1 -1
  196. package/dist/hono/flow-input-card-fields.test.d.ts +10 -0
  197. package/dist/hono/flow-input-card-fields.test.d.ts.map +1 -0
  198. package/dist/hono/flow-input-card-fields.test.js +26 -0
  199. package/dist/hono/flow-input-card-fields.test.js.map +1 -0
  200. package/dist/hono/flow-input-fields.test.js +1 -1
  201. package/dist/hono/flow-input-fields.test.js.map +1 -1
  202. package/dist/hono/flow-input.d.ts +23 -2
  203. package/dist/hono/flow-input.d.ts.map +1 -1
  204. package/dist/hono/flow-input.js +63 -14
  205. package/dist/hono/flow-input.js.map +1 -1
  206. package/dist/hono/flow-input.test.js +67 -11
  207. package/dist/hono/flow-input.test.js.map +1 -1
  208. package/dist/hono/jev.d.ts +16 -0
  209. package/dist/hono/jev.d.ts.map +1 -0
  210. package/dist/hono/jev.js +60 -0
  211. package/dist/hono/jev.js.map +1 -0
  212. package/dist/hono/jev.test.d.ts +2 -0
  213. package/dist/hono/jev.test.d.ts.map +1 -0
  214. package/dist/hono/jev.test.js +15 -0
  215. package/dist/hono/jev.test.js.map +1 -0
  216. package/dist/hono/knowledge-content.d.ts.map +1 -1
  217. package/dist/hono/knowledge-content.js +4 -2
  218. package/dist/hono/knowledge-content.js.map +1 -1
  219. package/dist/hono/knowledge-content.test.js +6 -5
  220. package/dist/hono/knowledge-content.test.js.map +1 -1
  221. package/dist/hono/mcp-servers.d.ts +8 -1
  222. package/dist/hono/mcp-servers.d.ts.map +1 -1
  223. package/dist/hono/mcp-servers.js +17 -6
  224. package/dist/hono/mcp-servers.js.map +1 -1
  225. package/dist/hono/mcp-servers.test.js +25 -0
  226. package/dist/hono/mcp-servers.test.js.map +1 -1
  227. package/dist/hono/model-capabilities.d.ts.map +1 -1
  228. package/dist/hono/model-capabilities.js +20 -9
  229. package/dist/hono/model-capabilities.js.map +1 -1
  230. package/dist/hono/model-capabilities.test.js +16 -8
  231. package/dist/hono/model-capabilities.test.js.map +1 -1
  232. package/dist/hono/oom-priority.agent.test.d.ts +2 -0
  233. package/dist/hono/oom-priority.agent.test.d.ts.map +1 -0
  234. package/dist/hono/oom-priority.agent.test.js +138 -0
  235. package/dist/hono/oom-priority.agent.test.js.map +1 -0
  236. package/dist/hono/oom-priority.d.ts +39 -0
  237. package/dist/hono/oom-priority.d.ts.map +1 -0
  238. package/dist/hono/oom-priority.js +138 -0
  239. package/dist/hono/oom-priority.js.map +1 -0
  240. package/dist/hono/oom-priority.test.d.ts +2 -0
  241. package/dist/hono/oom-priority.test.d.ts.map +1 -0
  242. package/dist/hono/oom-priority.test.js +52 -0
  243. package/dist/hono/oom-priority.test.js.map +1 -0
  244. package/dist/hono/openrouter-session.d.ts.map +1 -1
  245. package/dist/hono/openrouter-session.js +12 -1
  246. package/dist/hono/openrouter-session.js.map +1 -1
  247. package/dist/hono/openrouter-session.test.js +9 -3
  248. package/dist/hono/openrouter-session.test.js.map +1 -1
  249. package/dist/hono/prompt-context.d.ts +8 -1
  250. package/dist/hono/prompt-context.d.ts.map +1 -1
  251. package/dist/hono/prompt-context.js +45 -7
  252. package/dist/hono/prompt-context.js.map +1 -1
  253. package/dist/hono/prompt-context.test.js +32 -1
  254. package/dist/hono/prompt-context.test.js.map +1 -1
  255. package/dist/hono/publish-recovery.d.ts +1 -1
  256. package/dist/hono/publish-recovery.d.ts.map +1 -1
  257. package/dist/hono/publish-recovery.js +9 -6
  258. package/dist/hono/publish-recovery.js.map +1 -1
  259. package/dist/hono/publish-recovery.test.js +11 -2
  260. package/dist/hono/publish-recovery.test.js.map +1 -1
  261. package/dist/hono/relay.d.ts +10 -1
  262. package/dist/hono/relay.d.ts.map +1 -1
  263. package/dist/hono/relay.js +6 -0
  264. package/dist/hono/relay.js.map +1 -1
  265. package/dist/hono/sandbox-content.d.ts +2 -0
  266. package/dist/hono/sandbox-content.d.ts.map +1 -1
  267. package/dist/hono/sandbox-content.js +8 -4
  268. package/dist/hono/sandbox-content.js.map +1 -1
  269. package/dist/hono/sandbox-content.test.js +20 -1
  270. package/dist/hono/sandbox-content.test.js.map +1 -1
  271. package/dist/hono/server.d.ts.map +1 -1
  272. package/dist/hono/server.js +32 -0
  273. package/dist/hono/server.js.map +1 -1
  274. package/dist/hono/server.test.js +21 -1
  275. package/dist/hono/server.test.js.map +1 -1
  276. package/dist/hono/session-continuity.integration.test.js +10 -1
  277. package/dist/hono/session-continuity.integration.test.js.map +1 -1
  278. package/dist/hono/tag-fields.d.ts.map +1 -1
  279. package/dist/hono/tag-fields.js +2 -0
  280. package/dist/hono/tag-fields.js.map +1 -1
  281. package/dist/hono/tag-input.d.ts +124 -3
  282. package/dist/hono/tag-input.d.ts.map +1 -1
  283. package/dist/hono/tag-input.js +27 -17
  284. package/dist/hono/tag-input.js.map +1 -1
  285. package/dist/hono/tag-input.test.js +19 -1
  286. package/dist/hono/tag-input.test.js.map +1 -1
  287. package/dist/hono/types.d.ts +15 -0
  288. package/dist/hono/types.d.ts.map +1 -1
  289. package/dist/hono/workspace-script.d.ts +10 -0
  290. package/dist/hono/workspace-script.d.ts.map +1 -0
  291. package/dist/hono/workspace-script.js +19 -0
  292. package/dist/hono/workspace-script.js.map +1 -0
  293. package/dist/hono/workspace-settle.d.ts +20 -0
  294. package/dist/hono/workspace-settle.d.ts.map +1 -0
  295. package/dist/hono/workspace-settle.js +50 -0
  296. package/dist/hono/workspace-settle.js.map +1 -0
  297. package/dist/review/digest.d.ts +15 -2
  298. package/dist/review/digest.d.ts.map +1 -1
  299. package/dist/review/digest.js +90 -59
  300. package/dist/review/digest.js.map +1 -1
  301. package/dist/review/digest.test.js +94 -6
  302. package/dist/review/digest.test.js.map +1 -1
  303. package/dist/review/evidence-block.js +1 -1
  304. package/dist/review/evidence-block.js.map +1 -1
  305. package/dist/review/evidence-block.test.js +0 -10
  306. package/dist/review/evidence-block.test.js.map +1 -1
  307. package/dist/review/shell-policy.d.ts +6 -0
  308. package/dist/review/shell-policy.d.ts.map +1 -1
  309. package/dist/review/shell-policy.js +240 -72
  310. package/dist/review/shell-policy.js.map +1 -1
  311. package/dist/review/shell-policy.test.js +114 -0
  312. package/dist/review/shell-policy.test.js.map +1 -1
  313. package/package.json +10 -5
  314. package/dist/hono/latitude-telemetry.d.ts +0 -17
  315. package/dist/hono/latitude-telemetry.d.ts.map +0 -1
  316. package/dist/hono/latitude-telemetry.js +0 -52
  317. package/dist/hono/latitude-telemetry.js.map +0 -1
  318. package/dist/hono/latitude-telemetry.test.d.ts +0 -2
  319. package/dist/hono/latitude-telemetry.test.d.ts.map +0 -1
  320. package/dist/hono/latitude-telemetry.test.js +0 -18
  321. package/dist/hono/latitude-telemetry.test.js.map +0 -1
@@ -11,15 +11,18 @@ import { avatarAwaitingItsFace, avatarCreatedInAssistantMessage, avatarHandleFro
11
11
  import { accentContradictsUser } from "./avatar-accent.js";
12
12
  import { AVATAR_CASTING_TOOL_NAME, normalizeAvatarCastingToolInput, } from "./avatar-casting.js";
13
13
  import { asksForAvatarFace, FACE_QUESTION_MESSAGE } from "./avatar-face-question.js";
14
- import { CONNECTION_INPUT_TOOL_NAME, CONNECTION_PLATFORMS, normalizeConnectionInputToolInput, } from "./connection-input.js";
14
+ import { CONNECTION_INPUT_TOOL_NAME, CONNECTION_PLATFORMS, connectedButNotLoaded, normalizeConnectionInputToolInput, toolsConnected, } from "./connection-input.js";
15
15
  import { messageContextTokens, pickContextWindow, streamEventContextUsage, turnContextUsage, } from "./context-usage.js";
16
- import { FLOW_INPUT_TOOL_NAME, nodeTargetRefusals, normalizeFlowInputToolInput, readRawFlowTree, sanitizeFlowRequests, } from "./flow-input.js";
16
+ import { CREDENTIAL_INPUT_TOOL_NAME, normalizeCredentialInputToolInput, } from "./credential-input.js";
17
+ import { credentialScrubHooks } from "./credential-scrub.js";
18
+ import { agentStatedFlowMessage, FLOW_INPUT_TOOL_NAME, nodeTargetRefusals, normalizeFlowInputToolInput, readRawFlowTree, sanitizeFlowRequests, unknownFlowPrefillMessage, } from "./flow-input.js";
17
19
  import { writesInventedLogo } from "./invented-logo.js";
18
- import { installedLatitudeHook, latitudeTelemetryFor } from "./latitude-telemetry.js";
20
+ import { jevJudge } from "./jev.js";
19
21
  import { composioScopesToInvalidate, failedCustomServers, reportMcpInvalid, serversConnectedWithNoTools, } from "./mcp-servers.js";
20
22
  import { agentModelOverride, autoCompactWindowForModel, betasForModel, contextWindowForModel, } from "./model-capabilities.js";
23
+ import { agentShellPrefixEnv, killFirstWrapper } from "./oom-priority.js";
21
24
  import { BEDROCK_SERVABLE_CAPABILITIES, buildAgentExtraBody, EXTRA_BODY_ENV, MODEL_CAPABILITIES_ENV, } from "./openrouter-session.js";
22
- import { currentReferenceReminder } from "./prompt-context.js";
25
+ import { credentialReminder, currentReferenceReminder } from "./prompt-context.js";
23
26
  import { bridgeRuntimeFiles } from "./runtime-files.js";
24
27
  import { applyCurrentSenderAttribution, buildCleanBoundary, buildRehydratedPrompt, compactionRetryAttempt, emptyInProgressSession, isContextOverflowError, isResumeSessionError, observeInProgressSession, prepareSessionAttempt, stallContinuationAttempt, } from "./session-continuity.js";
25
28
  import { readSessionSkills } from "./session-skills.js";
@@ -29,6 +32,7 @@ import { createBakerUiServer, normalizeTagInputToolInput, sanitizeTagChanges, TA
29
32
  import { blockingToolDelegated, toolRunAsCommand } from "./tool-in-shell.js";
30
33
  import { recordTurnEvent } from "./turn-events.js";
31
34
  import { addQueryModelUsage, queryModelUsage, turnModelUsageList } from "./turn-model-usage.js";
35
+ import { settleWorkspaceAfterTasks } from "./workspace-settle.js";
32
36
  export function parsePlanDecision(value) {
33
37
  if (!value || typeof value !== "object") {
34
38
  return null;
@@ -43,6 +47,35 @@ export function parsePlanDecision(value) {
43
47
  }
44
48
  return null;
45
49
  }
50
+ /**
51
+ * How long a mid-turn MCP reload may hold the card's answer. Connecting a
52
+ * handful of HTTP servers takes seconds; past this the tools arrive next
53
+ * message instead, and the turn goes on.
54
+ */
55
+ const MCP_RELOAD_TIMEOUT_MS = 30_000;
56
+ /** The card's resolution, with what the reload did: ready now, or which servers would not start. */
57
+ function withReload(resolution, reload) {
58
+ const missing = reload?.ready ? connectedButNotLoaded(resolution, reload.loaded) : [];
59
+ const failed = [...(reload?.failed ?? []), ...missing];
60
+ return {
61
+ ...resolution,
62
+ toolsReady: reload?.ready === true && missing.length === 0,
63
+ ...(failed.length > 0 ? { failedToLoad: failed } : {}),
64
+ };
65
+ }
66
+ /** The promise's value, or `undefined` once `ms` pass without one. */
67
+ async function settleWithin(promise, ms) {
68
+ let timer;
69
+ const timedOut = new Promise((resolve) => {
70
+ timer = setTimeout(() => resolve(undefined), ms);
71
+ });
72
+ try {
73
+ return await Promise.race([promise, timedOut]);
74
+ }
75
+ finally {
76
+ clearTimeout(timer);
77
+ }
78
+ }
46
79
  /**
47
80
  * The Agent SDK matches AskUserQuestion answers to their questions by the
48
81
  * question TEXT (see `AskUserQuestionInput.answers`), but the dashboard submits
@@ -117,11 +150,10 @@ const BASE_DISALLOWED_TOOLS = [
117
150
  //
118
151
  // Since CLI 2.1.233 the CLI additionally withholds these from
119
152
  // Sonnet-5-and-newer models — so on Opus 5.5 this list is now
120
- // belt-and-braces. It is NOT redundant, because development runs on
121
- // Haiku 4.5, which predates that gate: read off the session's own
122
- // `init` tool list on 2.1.239, Haiku is offered all four and Opus 5
123
- // none. Removing them would hand the dev agent the backlog the
124
- // production agent cannot see.
153
+ // belt-and-braces. It is NOT redundant: Haiku 4.5 predates that gate
154
+ // (read off the session's own `init` tool list on 2.1.239, it is offered
155
+ // all four and Opus 5 none), and any override naming it would hand that
156
+ // agent the backlog the production agent cannot see.
125
157
  //
126
158
  // `TodoWrite` is gone from both — it is absent from an SDK session's
127
159
  // tool list on either model, so in-turn planning no longer has a tool
@@ -151,10 +183,11 @@ const BASE_DISALLOWED_TOOLS = [
151
183
  // not asked to fund a research fleet. Ordinary fan-out is unaffected:
152
184
  // `Task` stays, so the scaffold's subagents still run.
153
185
  "Workflow",
154
- // Cross-session messaging. A Runtime holds exactly one session, so
155
- // there is never a peer to reach; every call is a dead end that costs a
156
- // round trip to discover.
157
- "SendMessage",
186
+ // NOT listed: `SendMessage`. It is how the model follows up with a
187
+ // subagent it already ran, and the CLI tells it to on every Agent result.
188
+ // The continued subagent runs in the background, but it announces itself
189
+ // with `task_started` / `background_tasks_changed`, so the turn stays open
190
+ // until it finishes (`createTurnEndController`).
158
191
  // A Session review may not change anything. Removing the definitions is
159
192
  // the second of three layers — the key it carries is read-scoped and its
160
193
  // Bash calls go through `reviewBashPolicy` — because each fails
@@ -189,6 +222,16 @@ export function reviewQueryOptions(job) {
189
222
  ? { outputFormat: { type: "json_schema", schema: job.review.outputSchema }, hooks: reviewOutputHooks() }
190
223
  : {};
191
224
  }
225
+ /**
226
+ * Every job's tool results pass through the credential scrub, appended after
227
+ * any other PostToolUse hook so its rewrite is the one the SDK keeps.
228
+ */
229
+ function withCredentialScrub(options) {
230
+ const scrub = credentialScrubHooks();
231
+ const hooks = { ...(options.hooks ?? {}) };
232
+ hooks.PostToolUse = [...(hooks.PostToolUse ?? []), ...(scrub.PostToolUse ?? [])];
233
+ return { ...options, hooks };
234
+ }
192
235
  /**
193
236
  * The review runs on Baker's own OpenRouter credential and its own read-only
194
237
  * key, both replacing whatever the Runtime was started with — the client never
@@ -215,6 +258,12 @@ export const REVIEW_DEBUG_LOG = "/tmp/baker-review-cli.jsonl";
215
258
  // briefly; model activity cancels it (the CLI clearly isn't idle). Turns that
216
259
  // never ran a task skip the delay entirely.
217
260
  const DEFAULT_TURN_END_SETTLE_MS = 1_000;
261
+ // When the last task settles after the clean result, the CLI wakes the model on
262
+ // it and streams nothing until the woken turn's first token — seconds, and 30+
263
+ // on a slow route. Ending input inside that silence strands the woken turn on a
264
+ // closed channel, and the CLI's exit at its result kills anything it backgrounds
265
+ // (dev, Oct 2026: a render). A settle that wakes nobody waits this long once.
266
+ const DEFAULT_TURN_END_WAKE_MS = 45_000;
218
267
  // After input ends, a healthy idle CLI exits within seconds. If the stream is
219
268
  // still open past this window with no messages arriving, the CLI is wedged —
220
269
  // close the query so the turn can finish instead of hanging to the watchdog.
@@ -242,10 +291,10 @@ const UNATTENDED_PROMPT_MESSAGE = "This run is unattended — it was started by
242
291
  * asked for in a text prompt renders a question nobody can answer. In each case the user
243
292
  * is left waiting for something that was never going to come.
244
293
  */
245
- function refuseMalformedCall(toolName, input, userText, priorUserText) {
294
+ async function refuseMalformedCall(toolName, input, judge, userText, priorUserText) {
246
295
  if (toolName === "Bash") {
247
296
  const command = typeof input?.command === "string" ? input.command : undefined;
248
- return toolRunAsCommand(command) ?? accentContradictsUser(command, userText, priorUserText);
297
+ return toolRunAsCommand(command) ?? (await accentContradictsUser(command, userText, priorUserText, judge));
249
298
  }
250
299
  if (toolName === "Agent" || toolName === "Task") {
251
300
  return blockingToolDelegated(typeof input?.prompt === "string" ? input.prompt : undefined);
@@ -253,11 +302,18 @@ function refuseMalformedCall(toolName, input, userText, priorUserText) {
253
302
  if (toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
254
303
  return writesInventedLogo(typeof input?.file_path === "string" ? input.file_path : undefined);
255
304
  }
256
- if (toolName === "AskUserQuestion" && asksForAvatarFace(input)) {
305
+ if (toolName === "AskUserQuestion" && (await asksForAvatarFace(input, judge))) {
257
306
  return FACE_QUESTION_MESSAGE;
258
307
  }
259
308
  return null;
260
309
  }
310
+ /**
311
+ * Whether this Runtime has Credentials: the backend sets `BAKER_CREDENTIALS=1`
312
+ * alongside the credential broker's proxy env. Read per turn, not at import.
313
+ */
314
+ function credentialsEnabled() {
315
+ return process.env.BAKER_CREDENTIALS === "1";
316
+ }
261
317
  /** Blocking prompts: every tool that would leave a card the backend parks on. */
262
318
  function deniedUnattendedPrompt(toolName) {
263
319
  return (toolName === "AskUserQuestion" ||
@@ -265,6 +321,7 @@ function deniedUnattendedPrompt(toolName) {
265
321
  toolName === TAG_INPUT_TOOL_NAME ||
266
322
  toolName === FLOW_INPUT_TOOL_NAME ||
267
323
  toolName === CONNECTION_INPUT_TOOL_NAME ||
324
+ toolName === CREDENTIAL_INPUT_TOOL_NAME ||
268
325
  toolName === AVATAR_CASTING_TOOL_NAME);
269
326
  }
270
327
  export class StreamStallError extends Error {
@@ -341,8 +398,7 @@ function enrichImageError(message) {
341
398
  return `${message} — likely a file whose extension lies about its format (e.g. PNG renamed .jpg) or a corrupted download. Run the image validator and check magic bytes with \`file <path>\`.`;
342
399
  }
343
400
  /**
344
- * Which hooks a job runs. The client's agent runs the workspace's own and is
345
- * traced to Latitude. A Session review runs none: every workspace hook is
401
+ * Which hooks a job runs. The client's agent runs the workspace's own. A Session review runs none: every workspace hook is
346
402
  * written for the client's agent, and in the reviewer they speak about "this
347
403
  * Session" with the review's read-scoped key. The SessionStart probes are
348
404
  * refused on full-scope routes, and the reviewer filed its own "status unknown"
@@ -359,12 +415,11 @@ function hookSettingsForJob(job) {
359
415
  if (job.review) {
360
416
  return { disableAllHooks: true, permissions: { ask: ["Bash"] } };
361
417
  }
362
- const hooks = latitudeTelemetryFor(process.env, installedLatitudeHook());
363
- return hooks ? { hooks } : {};
418
+ return {};
364
419
  }
365
420
  // Carry a Runtime-wide model override down to the fan-out workers.
366
421
  //
367
- // The shared subagents pin themselves to Sonnet 5 in their own frontmatter (see
422
+ // The shared subagents pin themselves to Sonnet 5.5 in their own frontmatter (see
368
423
  // `DEFAULT_MODEL`). Since CLI 2.1.251 `CLAUDE_CODE_SUBAGENT_MODEL` is only the
369
424
  // default beneath that pin; `CLAUDE_CODE_SUBAGENT_MODEL_FORCE` (2.1.257) makes it
370
425
  // win over the frontmatter and any per-spawn model again. Absent (production),
@@ -498,9 +553,9 @@ function sdkResultError(msg) {
498
553
  // the whole turn; the deny THRESHOLD below still guards a one-off end-of-turn race.
499
554
  // The trailing colon matters: the CLI always emits the envelope followed by the
500
555
  // error that caused it ("…failed: Error: Stream closed", "…failed: AbortError: …").
501
- // Prose that merely NAMES the failure quotes it without one, which is how the
502
- // scaffold's guard-background-commands hook describes what it prevents — and its
503
- // block is an is_error tool_result, so a colon-less match counted a healthy
556
+ // Prose that merely NAMES the failure quotes it without one, which is how a
557
+ // hook explaining a block quotes it (the scaffold's former background-subagent
558
+ // block did) — and a hook block is an is_error tool_result, so a colon-less match counted a healthy
504
559
  // blocked spawn as a deny and could trip the breaker on a healthy turn.
505
560
  const CONTROL_CHANNEL_DENY_MARKER = "Tool permission request failed:";
506
561
  // Hook failures are reported through the same is_error tool_result channel but
@@ -658,14 +713,37 @@ export function createStallDetector(input) {
658
713
  timer.unref?.();
659
714
  return {
660
715
  observeMessage: (msg) => {
716
+ // A tool's progress says nothing about the model: a background task's
717
+ // heartbeat arrives while the model's stream is stalled.
718
+ if (msg.type === "tool_progress") {
719
+ return;
720
+ }
661
721
  lastMessageAt = input.now();
662
722
  trackPendingToolUses(pendingToolUses, msg);
663
723
  awaitingModel = (msg.type === "user" || msg.type === "stream_event") && pendingToolUses.size === 0;
664
724
  },
665
725
  stalled: () => stalled,
726
+ hasPendingToolUses: () => pendingToolUses.size > 0,
666
727
  dispose: () => clearInterval(timer),
667
728
  };
668
729
  }
730
+ // A query's cost and `modelUsage` are cumulative — on every result message and
731
+ // on the CLI's usage reply alike — so the latest reading replaces the last one
732
+ // rather than adding to it. It is folded into the turn totals when the query
733
+ // settles (see `runQueryOnce`). A spuriously empty later reading never wipes a
734
+ // real earlier one.
735
+ function recordCumulativeUsage(state, reading) {
736
+ const own = ownQueryUsage(reading, state.queryUsageBaseline);
737
+ state.observedCostUsd = own.costUsd;
738
+ const snapshot = queryModelUsage(own.modelUsage);
739
+ if (snapshot.size > 0) {
740
+ state.queryModelUsage = snapshot;
741
+ }
742
+ return own.costUsd;
743
+ }
744
+ // How long ending a turn waits on the CLI's usage reply before going ahead
745
+ // without it. Billing a turn late is recoverable; hanging one is not.
746
+ const USAGE_READING_TIMEOUT_MS = 5_000;
669
747
  // Decides when the turn is really over and the CLI may be told to exit.
670
748
  //
671
749
  // The prompt rides a held-open input stream (see `createHeldOpenQuery` in
@@ -687,7 +765,10 @@ export function createStallDetector(input) {
687
765
  // events must not, or the end would never re-arm. After input ends, an exit
688
766
  // guard closes the query if the CLI neither speaks nor exits, so a wedged
689
767
  // process cannot hang the turn until the session watchdog.
690
- function createTurnEndController(job, query, closeQuery, timings, onTurnEvent) {
768
+ function createTurnEndController(job, query, closeQuery, timings, onTurnEvent,
769
+ // Runs once, before input ends on a turn that ran tasks: a task can settle
770
+ // without waking the model, so the last result may not include what it spent.
771
+ beforeEndInput) {
691
772
  const inFlightTasks = new Set();
692
773
  // The background_tasks_changed LEVEL signal, tracked separately from the
693
774
  // edge-paired set above: a foreground command the CLI auto-backgrounds emits
@@ -700,15 +781,52 @@ function createTurnEndController(job, query, closeQuery, timings, onTurnEvent) {
700
781
  let cleanResultSeen = false;
701
782
  let lastWasTaskLifecycle = false;
702
783
  let inputEnded = false;
784
+ // Between deciding to end and ending, while the usage reading is in flight.
785
+ // Anything that would have cancelled a pending end — model activity, a task
786
+ // starting — is recorded here and abandons the end instead.
787
+ let ending;
788
+ // The query stopped being drained (a Stop, a crash): a reading answering
789
+ // after that has no turn left to end.
790
+ let disposed = false;
703
791
  let settleTimer;
704
792
  let exitGuard;
705
793
  const endInput = () => {
706
794
  settleTimer = undefined;
707
795
  // Fire-time guard: a task that started during the settle window keeps the
708
796
  // channel open; a later settle re-arms the end.
709
- if (inputEnded || hasInFlightWork()) {
797
+ if (inputEnded || ending || hasInFlightWork()) {
710
798
  return;
711
799
  }
800
+ if (sawAnyTask && beforeEndInput) {
801
+ const attempt = { interrupted: false };
802
+ ending = attempt;
803
+ let timeout;
804
+ const timedOut = new Promise((resolveTimeout) => {
805
+ timeout = setTimeout(resolveTimeout, USAGE_READING_TIMEOUT_MS);
806
+ timeout.unref?.();
807
+ });
808
+ void Promise.race([beforeEndInput().catch(() => undefined), timedOut]).finally(() => {
809
+ clearTimeout(timeout);
810
+ ending = undefined;
811
+ if (disposed) {
812
+ return;
813
+ }
814
+ // Re-check at the moment input would end, as the fire-time guard above
815
+ // does: a wake or a task that landed during the reading keeps the
816
+ // channel open, and the woken turn's own result or settle ends it.
817
+ if (attempt.interrupted || hasInFlightWork()) {
818
+ if (cleanResultSeen && !hasInFlightWork()) {
819
+ scheduleEnd();
820
+ }
821
+ return;
822
+ }
823
+ finishEndInput();
824
+ });
825
+ return;
826
+ }
827
+ finishEndInput();
828
+ };
829
+ const finishEndInput = () => {
712
830
  inputEnded = true;
713
831
  query.endInput?.();
714
832
  onTurnEvent({
@@ -728,7 +846,7 @@ function createTurnEndController(job, query, closeQuery, timings, onTurnEvent) {
728
846
  }, timings.exitGuardMs);
729
847
  exitGuard.unref?.();
730
848
  };
731
- const scheduleEnd = () => {
849
+ const scheduleEnd = (delayMs = timings.settleMs) => {
732
850
  if (settleTimer) {
733
851
  return;
734
852
  }
@@ -740,7 +858,7 @@ function createTurnEndController(job, query, closeQuery, timings, onTurnEvent) {
740
858
  endInput();
741
859
  return;
742
860
  }
743
- settleTimer = setTimeout(endInput, timings.settleMs);
861
+ settleTimer = setTimeout(endInput, delayMs);
744
862
  settleTimer.unref?.();
745
863
  };
746
864
  return {
@@ -778,6 +896,9 @@ function createTurnEndController(job, query, closeQuery, timings, onTurnEvent) {
778
896
  clearTimeout(settleTimer);
779
897
  settleTimer = undefined;
780
898
  }
899
+ if (ending && (isModelActivity || hasInFlightWork())) {
900
+ ending.interrupted = true;
901
+ }
781
902
  if (exitGuard) {
782
903
  exitGuard.refresh();
783
904
  }
@@ -803,10 +924,13 @@ function createTurnEndController(job, query, closeQuery, timings, onTurnEvent) {
803
924
  // second result) would strand the turn until the watchdog. An emptied
804
925
  // background_tasks_changed level counts as a settle the same way.
805
926
  if (lastWasTaskLifecycle && cleanResultSeen && !hasInFlightWork()) {
806
- scheduleEnd();
927
+ scheduleEnd(timings.wakeMs);
807
928
  }
808
929
  },
930
+ hasInFlightWork,
931
+ ranTasks: () => sawAnyTask,
809
932
  dispose: () => {
933
+ disposed = true;
810
934
  if (settleTimer) {
811
935
  clearTimeout(settleTimer);
812
936
  settleTimer = undefined;
@@ -893,11 +1017,14 @@ export class BridgeJobExecutor {
893
1017
  stallCheckIntervalMs;
894
1018
  recordTurnEvent;
895
1019
  turnEndSettleMs;
1020
+ turnEndWakeMs;
896
1021
  turnEndExitGuardMs;
897
1022
  now;
898
1023
  readUsageBaseline;
899
1024
  stalledCliExitMs;
900
1025
  readFlowTree;
1026
+ settleWorkspace;
1027
+ judge;
901
1028
  constructor(dependencies) {
902
1029
  this.sdk = dependencies.sdk;
903
1030
  this.relay = dependencies.relay;
@@ -910,14 +1037,17 @@ export class BridgeJobExecutor {
910
1037
  this.stallCheckIntervalMs = dependencies.stallCheckIntervalMs ?? DEFAULT_STALL_CHECK_INTERVAL_MS;
911
1038
  this.recordTurnEvent = dependencies.recordTurnEvent ?? recordTurnEvent;
912
1039
  this.turnEndSettleMs = dependencies.turnEndSettleMs ?? DEFAULT_TURN_END_SETTLE_MS;
1040
+ this.turnEndWakeMs = dependencies.turnEndWakeMs ?? DEFAULT_TURN_END_WAKE_MS;
913
1041
  this.turnEndExitGuardMs = dependencies.turnEndExitGuardMs ?? DEFAULT_TURN_END_EXIT_GUARD_MS;
914
1042
  this.now = dependencies.now ?? Date.now;
915
1043
  this.readUsageBaseline =
916
1044
  dependencies.readUsageBaseline ??
917
1045
  ((sessionId) => readUsageBaseline(sessionId, process.env.CLAUDE_CONFIG_DIR || resolve(process.env.HOME || homedir(), ".claude")));
918
1046
  this.stalledCliExitMs = dependencies.stalledCliExitMs ?? STALLED_CLI_EXIT_MS;
1047
+ this.settleWorkspace = dependencies.settleWorkspace ?? (() => settleWorkspaceAfterTasks());
919
1048
  this.readFlowTree =
920
1049
  dependencies.readFlowTree ?? ((flowSlug) => readRawFlowTree(bridgeRuntimeFiles.flowsDir(), flowSlug));
1050
+ this.judge = dependencies.judge ?? jevJudge(process.env.OPENROUTER_API_KEY);
921
1051
  }
922
1052
  async execute(job, controls) {
923
1053
  const state = createBridgeJobState();
@@ -930,6 +1060,9 @@ export class BridgeJobExecutor {
930
1060
  return { kind: "cancelled", costUsd: 0, model: state.model };
931
1061
  }
932
1062
  const loaded = await this.loadTrustedBoundary(job, controls);
1063
+ if (loaded?.kind !== "clean") {
1064
+ request = await this.withContinuedAttachments(job, request);
1065
+ }
933
1066
  attempt = prepareSessionAttempt({
934
1067
  requestPrompt: request.prompt,
935
1068
  reminder: request.reminder,
@@ -987,9 +1120,13 @@ export class BridgeJobExecutor {
987
1120
  const verbatim = verbatimSlashCommand(job.prompt) ??
988
1121
  leadingSkillCommand(job.prompt, (await readSessionSkills(bridgeRuntimeFiles.projectRoot())) ?? []);
989
1122
  let requestPrompt = verbatim ?? applyCurrentSenderAttribution(job.prompt, job.currentSender);
990
- const reminder = verbatim === null ? currentReferenceReminder(job.currentReference) : undefined;
1123
+ const reminders = [
1124
+ verbatim === null ? currentReferenceReminder(job.currentReference) : undefined,
1125
+ credentialReminder(job.credentials ?? [], credentialsEnabled()),
1126
+ ].filter((text) => text !== undefined);
1127
+ const reminder = reminders.length > 0 ? reminders.join("\n\n") : undefined;
991
1128
  if (job.attachments && job.attachments.length > 0) {
992
- requestPrompt += await this.prepareAttachments(job.attachments);
1129
+ requestPrompt += await this.prepareAttachments(job.attachments, "message");
993
1130
  }
994
1131
  if (!controls.isActive()) {
995
1132
  return null;
@@ -1193,6 +1330,18 @@ export class BridgeJobExecutor {
1193
1330
  });
1194
1331
  }
1195
1332
  }
1333
+ /**
1334
+ * The files of the request a late reply continues, for a turn that does not
1335
+ * resume the Session that received them with the request. Its history names
1336
+ * them but cannot open them.
1337
+ */
1338
+ async withContinuedAttachments(job, request) {
1339
+ if (!job.continuedAttachments?.length || request.leadingCommand) {
1340
+ return request;
1341
+ }
1342
+ const suffix = await this.prepareAttachments(job.continuedAttachments, "continued_request");
1343
+ return { ...request, prompt: request.prompt + suffix };
1344
+ }
1196
1345
  async invalidResumeFallbackAttempt(job, controls, request, attempt, state, error) {
1197
1346
  if (!attempt.usedBoundary ||
1198
1347
  state.sawUserVisibleSdkMessage ||
@@ -1201,6 +1350,7 @@ export class BridgeJobExecutor {
1201
1350
  return null;
1202
1351
  }
1203
1352
  await this.sessionStore.clear(job.threadId, { runToken: controls.runToken, reason: "sdk_error" });
1353
+ const rebuilt = await this.withContinuedAttachments(job, request);
1204
1354
  void this.relay.contextSource({ threadId: job.threadId, turnId: job.turnId }, "rehydrated_invalid_resume", {
1205
1355
  historyEntryCount: job.history?.entries.length ?? 0,
1206
1356
  omittedEntryCount: job.history?.omittedEntryCount ?? 0,
@@ -1209,12 +1359,12 @@ export class BridgeJobExecutor {
1209
1359
  });
1210
1360
  // A leading command stays bare here too: a history preamble in front of it
1211
1361
  // would turn it into plain text (see `prepareSessionAttempt`).
1212
- if (request.leadingCommand) {
1213
- return { prompt: request.prompt, attemptedRehydration: false, resumeOptions: {} };
1362
+ if (rebuilt.leadingCommand) {
1363
+ return { prompt: rebuilt.prompt, attemptedRehydration: false, resumeOptions: {} };
1214
1364
  }
1215
1365
  return {
1216
- prompt: buildRehydratedPrompt(request.prompt, job.history),
1217
- ...(request.reminder ? { reminder: request.reminder } : {}),
1366
+ prompt: buildRehydratedPrompt(rebuilt.prompt, job.history),
1367
+ ...(rebuilt.reminder ? { reminder: rebuilt.reminder } : {}),
1218
1368
  attemptedRehydration: true,
1219
1369
  resumeOptions: {},
1220
1370
  };
@@ -1231,6 +1381,39 @@ export class BridgeJobExecutor {
1231
1381
  return undefined;
1232
1382
  }
1233
1383
  }
1384
+ /**
1385
+ * Load what the user just connected into the running query, so its tools
1386
+ * answer in this turn instead of the next. Asks Convex for the whole set
1387
+ * afresh (the same set the next turn starts from, so the tool list changes
1388
+ * once, not twice) and hands it to the CLI with `baker_ui` kept — the SDK
1389
+ * drops any in-process server missing from the call, and `baker_ui` is the
1390
+ * server whose tool call is resolving right now. Never throws. A server the
1391
+ * CLI could not start is named, because it will not start next message
1392
+ * either; anything else that goes wrong here loads next message.
1393
+ */
1394
+ async reloadMcpServers(job, query) {
1395
+ const notReady = { ready: false, loaded: [], failed: [] };
1396
+ if (!this.mcpServersProvider || !query.setMcpServers) {
1397
+ return notReady;
1398
+ }
1399
+ try {
1400
+ const servers = await this.mcpServersProvider(job.threadId, job.currentSender?.userId, { fresh: true });
1401
+ if (!servers) {
1402
+ return notReady;
1403
+ }
1404
+ const result = await settleWithin(query.setMcpServers({ ...servers, baker_ui: createBakerUiServer({ credentials: credentialsEnabled() }) }), MCP_RELOAD_TIMEOUT_MS);
1405
+ const failed = Object.keys(result?.errors ?? {});
1406
+ if (failed.length > 0) {
1407
+ console.error(`[mcp] reload after a connection could not start ${failed.join(", ")}`);
1408
+ }
1409
+ const loaded = Object.keys(servers).filter((name) => !failed.includes(name));
1410
+ return { ready: result !== undefined && failed.length === 0, loaded, failed };
1411
+ }
1412
+ catch (error) {
1413
+ console.error(`[mcp] reload after a connection failed: ${error instanceof Error ? error.message : String(error)}`);
1414
+ return notReady;
1415
+ }
1416
+ }
1234
1417
  reportMcpFailures(job, msg) {
1235
1418
  for (const scope of composioScopesToInvalidate(msg)) {
1236
1419
  void reportMcpInvalid(job.threadId, { scope }, job.currentSender?.userId);
@@ -1250,7 +1433,7 @@ export class BridgeJobExecutor {
1250
1433
  * handlers — except on an unattended turn, where there is nobody to answer and
1251
1434
  * every one of them is refused up front (see `UNATTENDED_PROMPT_MESSAGE`).
1252
1435
  */
1253
- routeToolUse(job, controls, toolName, input, opts) {
1436
+ async routeToolUse(job, controls, toolName, input, opts) {
1254
1437
  const refusedShell = reviewShellRefusal(job, toolName, input);
1255
1438
  if (refusedShell) {
1256
1439
  return Promise.resolve({ behavior: "deny", message: refusedShell });
@@ -1266,9 +1449,9 @@ export class BridgeJobExecutor {
1266
1449
  });
1267
1450
  return Promise.resolve({ behavior: "deny", message: UNATTENDED_PROMPT_MESSAGE });
1268
1451
  }
1269
- const malformed = refuseMalformedCall(toolName, input, job.prompt, userTurnsBefore(job));
1452
+ const malformed = await refuseMalformedCall(toolName, input, this.judge, job.prompt, userTurnsBefore(job));
1270
1453
  if (malformed)
1271
- return Promise.resolve({ behavior: "deny", message: malformed });
1454
+ return { behavior: "deny", message: malformed };
1272
1455
  // Cast what is owed FIRST, whatever comes next — including another create. Checked
1273
1456
  // the other way round, a second create (the agent retrying after a rejected flag,
1274
1457
  // which is what happens in practice) would overwrite the handle and the first
@@ -1321,6 +1504,15 @@ export class BridgeJobExecutor {
1321
1504
  if (toolName === CONNECTION_INPUT_TOOL_NAME) {
1322
1505
  return this.handleConnectionInputRequest(job, controls, input, opts.toolUseID, opts.signal);
1323
1506
  }
1507
+ if (toolName === CREDENTIAL_INPUT_TOOL_NAME) {
1508
+ if (!credentialsEnabled()) {
1509
+ return Promise.resolve({
1510
+ behavior: "deny",
1511
+ message: "Saved credentials are not available in this workspace. Ask the user to share what you need another way, and never ask them to paste a password or key into the chat.",
1512
+ });
1513
+ }
1514
+ return this.handleCredentialInputRequest(job, controls, input, opts.toolUseID, opts.signal);
1515
+ }
1324
1516
  if (toolName === AVATAR_CASTING_TOOL_NAME) {
1325
1517
  return this.handleAvatarCastingRequest(job, controls, input, opts.toolUseID, opts.signal);
1326
1518
  }
@@ -1359,6 +1551,10 @@ export class BridgeJobExecutor {
1359
1551
  // warning to the user. Pinning it takes that threshold out of remote control.
1360
1552
  // See `autoCompactWindowForModel` for the full chain and its ceiling.
1361
1553
  ...(autoCompactWindow !== undefined ? { autoCompactWindow } : {}),
1554
+ // Subagents, workflows and helper requests default to a 5-minute prompt
1555
+ // cache outside a Claude subscription, so a subagent waiting on a render
1556
+ // longer than that rewrites its whole prefix on the next call.
1557
+ subagentPromptCacheTtl: "1h",
1362
1558
  // SDK rejects autoMemoryDirectory from project settings for security; must use settings tier. Gate on BAKER_CHAT_ID so local dev never writes into the monorepo.
1363
1559
  ...(process.env.BAKER_CHAT_ID
1364
1560
  ? {
@@ -1391,11 +1587,8 @@ export class BridgeJobExecutor {
1391
1587
  settingSources: ["project"],
1392
1588
  // `baker_ui` hosts in-process interactive dashboard tools (secure tag
1393
1589
  // form). Always present; HTTP (Composio) servers merge alongside it.
1394
- mcpServers: { ...(mcpServers ?? {}), baker_ui: createBakerUiServer() },
1395
- // Only send the key when there is something in it: the SDK forwards
1396
- // `settings` to the CLI whenever it is present, so an empty object still
1397
- // becomes a `--settings {}` flag tier for the CLI to parse.
1398
- ...(Object.keys(settings).length > 0 ? { settings } : {}),
1590
+ mcpServers: { ...(mcpServers ?? {}), baker_ui: createBakerUiServer({ credentials: credentialsEnabled() }) },
1591
+ settings,
1399
1592
  includePartialMessages: true,
1400
1593
  // Only the workspace's own skills. Omitting the option is NOT "skills
1401
1594
  // off" — the CLI's defaults still load its built-in developer skills
@@ -1404,8 +1597,11 @@ export class BridgeJobExecutor {
1404
1597
  // the workspace could not be read; omit rather than allowlist nothing.
1405
1598
  ...(skills ? { skills } : {}),
1406
1599
  disallowedTools: disallowedToolsFor(job),
1407
- ...reviewQueryOptions(job),
1408
- ...(job.planMode ? { permissionMode: "plan" } : {}),
1600
+ ...withCredentialScrub(reviewQueryOptions(job)),
1601
+ // Never omitted: since SDK 0.3.286 an omitted mode starts a third-party-provider
1602
+ // session (OpenRouter) in auto mode, where a classifier rather than `canUseTool`
1603
+ // decides tool calls.
1604
+ permissionMode: job.planMode ? "plan" : "default",
1409
1605
  canUseTool: (toolName, input, opts) => this.routeToolUse(job, controls, toolName, input, opts),
1410
1606
  // No `fallbackModel`, and no server-side refusal `fallbacks` in the extra
1411
1607
  // body: the CLI's refusal fallback is gated on a first-party base URL (see
@@ -1429,6 +1625,13 @@ export class BridgeJobExecutor {
1429
1625
  // system-reminder repeatedly pushed the agent toward that ephemeral
1430
1626
  // backlog instead of the product's own Tasks (`baker actions`).
1431
1627
  CLAUDE_CODE_ENABLE_TASKS: "0",
1628
+ // Every Bash command runs through a wrapper that makes it, not the agent
1629
+ // or the credential broker, what the OOM killer takes first (oom-priority.ts).
1630
+ ...agentShellPrefixEnv(killFirstWrapper(), process.env),
1631
+ // The CLI clamps any Bash timeout to 10 minutes unless told otherwise;
1632
+ // the workspace's `inject-timeouts` hook gives a render up to 30, and at
1633
+ // the clamp the render was moved to the background mid-repair.
1634
+ BASH_MAX_TIMEOUT_MS: String(30 * 60_000),
1432
1635
  // `citty` writes ANSI escapes into `baker --help` unconditionally —
1433
1636
  // they are never TTY-gated, so every help read the agent does inside
1434
1637
  // the sandbox pays ~5-10% of its tokens on colour codes it can't see.
@@ -1450,6 +1653,7 @@ export class BridgeJobExecutor {
1450
1653
  ...attempt.resumeOptions,
1451
1654
  };
1452
1655
  const query = this.sdk.query({ prompt: attempt.prompt, reminder: attempt.reminder, options });
1656
+ controls.reloadMcpServers = () => this.reloadMcpServers(job, query);
1453
1657
  let queryClosed = false;
1454
1658
  const closeQuery = () => {
1455
1659
  if (queryClosed) {
@@ -1471,6 +1675,7 @@ export class BridgeJobExecutor {
1471
1675
  throw error;
1472
1676
  }
1473
1677
  finally {
1678
+ controls.reloadMcpServers = undefined;
1474
1679
  controls.abortSignal.removeEventListener("abort", closeQuery);
1475
1680
  // Release the held-open input if the turn-end policy never fired (the
1476
1681
  // stream ended on its own, or an error path closed the query). Idempotent.
@@ -1494,7 +1699,19 @@ export class BridgeJobExecutor {
1494
1699
  // in-progress session.
1495
1700
  async drainQuery(job, controls, state, query, closeQuery) {
1496
1701
  const stallDetector = this.createStallDetector(job, controls, closeQuery);
1497
- const turnEnd = createTurnEndController(job, query, closeQuery, { settleMs: this.turnEndSettleMs, exitGuardMs: this.turnEndExitGuardMs }, this.recordTurnEvent);
1702
+ // What the query had spent when its turn ended, read from the CLI — set
1703
+ // only when a task ran, and higher than the last result's figure when a
1704
+ // task kept working after it.
1705
+ let settledCostUsd = 0;
1706
+ // A reading that answers after the query drained (past its timeout) belongs
1707
+ // to a query already folded into the turn — and a retry reuses `state`.
1708
+ let drained = false;
1709
+ const turnEnd = createTurnEndController(job, query, closeQuery, { settleMs: this.turnEndSettleMs, wakeMs: this.turnEndWakeMs, exitGuardMs: this.turnEndExitGuardMs }, this.recordTurnEvent, async () => {
1710
+ const reading = await query.usage?.();
1711
+ if (reading && !drained) {
1712
+ settledCostUsd = recordCumulativeUsage(state, reading);
1713
+ }
1714
+ });
1498
1715
  let costUsd = 0;
1499
1716
  // Errors thrown while HANDLING a message (control-channel breaker, error
1500
1717
  // results) are real verdicts and must surface as-is; only errors from the
@@ -1504,6 +1721,7 @@ export class BridgeJobExecutor {
1504
1721
  for await (const msg of query) {
1505
1722
  stallDetector.observeMessage(msg);
1506
1723
  turnEnd.observeMessage(msg);
1724
+ controls.setWorkInFlight?.(stallDetector.hasPendingToolUses() || turnEnd.hasInFlightWork());
1507
1725
  try {
1508
1726
  costUsd = await this.consumeSdkMessage(job, controls, state, msg, costUsd);
1509
1727
  }
@@ -1526,6 +1744,8 @@ export class BridgeJobExecutor {
1526
1744
  throw error;
1527
1745
  }
1528
1746
  finally {
1747
+ drained = true;
1748
+ controls.setWorkInFlight?.(false);
1529
1749
  stallDetector.dispose();
1530
1750
  turnEnd.dispose();
1531
1751
  await this.openPendingCastingAtTurnEnd(job, controls);
@@ -1533,7 +1753,11 @@ export class BridgeJobExecutor {
1533
1753
  if (stallDetector.stalled() && controls.isActive()) {
1534
1754
  throw new StreamStallError();
1535
1755
  }
1536
- return costUsd;
1756
+ // Not for a Session review: it runs with every hook off and may not change anything.
1757
+ if (turnEnd.ranTasks() && controls.isActive() && !job.review) {
1758
+ await this.settleWorkspace();
1759
+ }
1760
+ return Math.max(costUsd, settledCostUsd);
1537
1761
  }
1538
1762
  /**
1539
1763
  * Open a casting the turn recorded but never reached.
@@ -1590,6 +1814,7 @@ export class BridgeJobExecutor {
1590
1814
  controls.pendingTagInputs.size > 0 ||
1591
1815
  controls.pendingFlowInputs.size > 0 ||
1592
1816
  controls.pendingConnectionInputs.size > 0 ||
1817
+ controls.pendingCredentialInputs.size > 0 ||
1593
1818
  controls.pendingAvatarCastings.size > 0,
1594
1819
  onStall: () => {
1595
1820
  console.error(`[BridgeJob] no stream data for ${this.stallTimeoutMs}ms while awaiting the model — closing query for retry (thread ${job.threadId})`);
@@ -1626,17 +1851,7 @@ export class BridgeJobExecutor {
1626
1851
  }
1627
1852
  trackMainRequestUsage(state, msg);
1628
1853
  if (msg.type === "result") {
1629
- const own = ownQueryUsage({ totalCostUsd: msg.total_cost_usd, modelUsage: msg.modelUsage }, state.queryUsageBaseline);
1630
- state.observedCostUsd = own.costUsd;
1631
- // A query's result `modelUsage` is cumulative (like `total_cost_usd`, which
1632
- // we overwrite above), so keep the latest snapshot rather than summing —
1633
- // it is folded into the turn totals when the query settles (see
1634
- // `runQueryOnce`). Guard against a spuriously empty later result wiping a
1635
- // real earlier one.
1636
- const snapshot = queryModelUsage(own.modelUsage);
1637
- if (snapshot.size > 0) {
1638
- state.queryModelUsage = snapshot;
1639
- }
1854
+ const ownCostUsd = recordCumulativeUsage(state, { totalCostUsd: msg.total_cost_usd, modelUsage: msg.modelUsage });
1640
1855
  await this.publishContextUsage(job, state, msg.modelUsage);
1641
1856
  // What a Session review returns. The SDK enforces the JSON schema, so this
1642
1857
  // either parses against the findings contract or the review failed to
@@ -1653,7 +1868,7 @@ export class BridgeJobExecutor {
1653
1868
  await this.clearIfDirty(job, controls, state, "result_error");
1654
1869
  throw resultError;
1655
1870
  }
1656
- return own.costUsd;
1871
+ return ownCostUsd;
1657
1872
  }
1658
1873
  if (isUserVisibleSdkMessage(msg)) {
1659
1874
  state.sawUserVisibleSdkMessage = true;
@@ -2028,6 +2243,16 @@ export class BridgeJobExecutor {
2028
2243
  // Strip any secret values the model tried to prefill; their names still reach
2029
2244
  // the user as requested fields. The backend strips again.
2030
2245
  const sanitizedRequests = sanitizeFlowRequests(requests);
2246
+ // A key the card does not render is dropped there without a word, and the
2247
+ // person retypes what the agent already knew — refuse it before the card opens.
2248
+ const stated = agentStatedFlowMessage(sanitizedRequests);
2249
+ if (stated) {
2250
+ return { behavior: "deny", message: stated };
2251
+ }
2252
+ const unknownPrefill = unknownFlowPrefillMessage(sanitizedRequests);
2253
+ if (unknownPrefill) {
2254
+ return { behavior: "deny", message: unknownPrefill };
2255
+ }
2031
2256
  const posted = await this.relay.flowInputRequest(target, { toolUseId, requests: sanitizedRequests });
2032
2257
  if (!posted) {
2033
2258
  console.error(`[request_flow_input] failed to post flow-input-request for ${toolUseId} — denying to avoid a hang`);
@@ -2078,7 +2303,7 @@ export class BridgeJobExecutor {
2078
2303
  if (connections.length === 0) {
2079
2304
  return {
2080
2305
  behavior: "deny",
2081
- message: `request_connection requires \`connections: [...]\` with at least one entry { platform, reason }. Platform must be one of ${CONNECTION_PLATFORMS.join(", ")}. Retry with that shape.`,
2306
+ message: `request_connection requires \`connections: [...]\` with at least one entry: { platform, reason } where platform is one of ${CONNECTION_PLATFORMS.join(", ")}; { server: { name, url, auth?, keyHeader?, scope? }, reason } for a remote MCP server; or { app, reason } for a managed app from \`baker mcp catalog\`. Retry with that shape.`,
2082
2307
  };
2083
2308
  }
2084
2309
  const posted = await this.relay.connectionInputRequest(target, { toolUseId, connections });
@@ -2100,7 +2325,43 @@ export class BridgeJobExecutor {
2100
2325
  return { behavior: "deny", message: PROMPT_UNANSWERED_MESSAGE };
2101
2326
  }
2102
2327
  void this.relay.connectionInputResolved(target);
2103
- return { behavior: "allow", updatedInput: { connections, resolution: decision.value } };
2328
+ // Loaded before the tool answers, so the model's next request already
2329
+ // carries the new tools. Only when an app or server came through: a
2330
+ // platform adds no tool, and reloading for nothing rewrites the tool list.
2331
+ const resolution = toolsConnected(decision.value)
2332
+ ? withReload(decision.value, await controls.reloadMcpServers?.())
2333
+ : decision.value;
2334
+ return { behavior: "allow", updatedInput: { connections, resolution } };
2335
+ }
2336
+ async handleCredentialInputRequest(job, controls, input, toolUseId, signal) {
2337
+ const target = { threadId: job.threadId, turnId: job.turnId };
2338
+ const request = normalizeCredentialInputToolInput(input);
2339
+ if (!request) {
2340
+ return {
2341
+ behavior: "deny",
2342
+ message: "request_credential requires { label, reason, allowedHosts: [...] } (and optionally a lowercase `name`). Retry with that shape.",
2343
+ };
2344
+ }
2345
+ const posted = await this.relay.credentialInputRequest(target, { toolUseId, ...request });
2346
+ if (!posted) {
2347
+ console.error(`[request_credential] failed to post credential-input-request for ${toolUseId} — denying`);
2348
+ return {
2349
+ behavior: "deny",
2350
+ message: "The credential request could not be delivered to the dashboard — retry request_credential in a moment.",
2351
+ };
2352
+ }
2353
+ const decision = await this.awaitPromptDecision({
2354
+ label: "request_credential",
2355
+ signal,
2356
+ controls,
2357
+ pending: controls.pendingCredentialInputs,
2358
+ toolUseId,
2359
+ });
2360
+ if (decision.kind === "unanswered") {
2361
+ return { behavior: "deny", message: PROMPT_UNANSWERED_MESSAGE };
2362
+ }
2363
+ void this.relay.credentialInputResolved(target);
2364
+ return { behavior: "allow", updatedInput: { ...request, resolution: decision.value } };
2104
2365
  }
2105
2366
  async handlePlanReview(job, controls, input, toolUseId, signal) {
2106
2367
  const target = { threadId: job.threadId, turnId: job.turnId };