@koda-sl/baker-bridge 0.109.0-dev.f28c07bd2 → 0.109.0-dev.f7ef4c7e1

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 (324) 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 +19 -2
  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/avatar-accent.d.ts +23 -5
  119. package/dist/hono/avatar-accent.d.ts.map +1 -1
  120. package/dist/hono/avatar-accent.js +54 -63
  121. package/dist/hono/avatar-accent.js.map +1 -1
  122. package/dist/hono/avatar-accent.test.js +64 -35
  123. package/dist/hono/avatar-accent.test.js.map +1 -1
  124. package/dist/hono/avatar-casting.d.ts +1 -0
  125. package/dist/hono/avatar-casting.d.ts.map +1 -1
  126. package/dist/hono/avatar-casting.js +18 -4
  127. package/dist/hono/avatar-casting.js.map +1 -1
  128. package/dist/hono/avatar-casting.test.js +5 -0
  129. package/dist/hono/avatar-casting.test.js.map +1 -1
  130. package/dist/hono/avatar-face-question.d.ts +23 -6
  131. package/dist/hono/avatar-face-question.d.ts.map +1 -1
  132. package/dist/hono/avatar-face-question.js +48 -59
  133. package/dist/hono/avatar-face-question.js.map +1 -1
  134. package/dist/hono/avatar-face-question.test.js +46 -101
  135. package/dist/hono/avatar-face-question.test.js.map +1 -1
  136. package/dist/hono/baker-ui-tool-schemas.test.js +30 -4
  137. package/dist/hono/baker-ui-tool-schemas.test.js.map +1 -1
  138. package/dist/hono/blocking-card.d.ts +12 -0
  139. package/dist/hono/blocking-card.d.ts.map +1 -0
  140. package/dist/hono/blocking-card.js +12 -0
  141. package/dist/hono/blocking-card.js.map +1 -0
  142. package/dist/hono/bridge-job.d.ts +68 -2
  143. package/dist/hono/bridge-job.d.ts.map +1 -1
  144. package/dist/hono/bridge-job.js +301 -64
  145. package/dist/hono/bridge-job.js.map +1 -1
  146. package/dist/hono/bridge-job.test.js +489 -38
  147. package/dist/hono/bridge-job.test.js.map +1 -1
  148. package/dist/hono/check-content.d.ts +49 -0
  149. package/dist/hono/check-content.d.ts.map +1 -0
  150. package/dist/hono/check-content.js +182 -0
  151. package/dist/hono/check-content.js.map +1 -0
  152. package/dist/hono/check-content.test.d.ts +2 -0
  153. package/dist/hono/check-content.test.d.ts.map +1 -0
  154. package/dist/hono/check-content.test.js +87 -0
  155. package/dist/hono/check-content.test.js.map +1 -0
  156. package/dist/hono/connection-input.d.ts +170 -12
  157. package/dist/hono/connection-input.d.ts.map +1 -1
  158. package/dist/hono/connection-input.js +166 -14
  159. package/dist/hono/connection-input.js.map +1 -1
  160. package/dist/hono/connection-input.test.js +112 -9
  161. package/dist/hono/connection-input.test.js.map +1 -1
  162. package/dist/hono/convex.d.ts +8 -0
  163. package/dist/hono/convex.d.ts.map +1 -1
  164. package/dist/hono/convex.js +8 -0
  165. package/dist/hono/convex.js.map +1 -1
  166. package/dist/hono/convex.test.js +4 -1
  167. package/dist/hono/convex.test.js.map +1 -1
  168. package/dist/hono/credential-input.d.ts +39 -0
  169. package/dist/hono/credential-input.d.ts.map +1 -0
  170. package/dist/hono/credential-input.js +78 -0
  171. package/dist/hono/credential-input.js.map +1 -0
  172. package/dist/hono/credential-scrub.d.ts +42 -0
  173. package/dist/hono/credential-scrub.d.ts.map +1 -0
  174. package/dist/hono/credential-scrub.js +173 -0
  175. package/dist/hono/credential-scrub.js.map +1 -0
  176. package/dist/hono/credential-scrub.test.d.ts +2 -0
  177. package/dist/hono/credential-scrub.test.d.ts.map +1 -0
  178. package/dist/hono/credential-scrub.test.js +130 -0
  179. package/dist/hono/credential-scrub.test.js.map +1 -0
  180. package/dist/hono/env.d.ts +1 -0
  181. package/dist/hono/env.d.ts.map +1 -1
  182. package/dist/hono/env.js +2 -0
  183. package/dist/hono/env.js.map +1 -1
  184. package/dist/hono/flow-crypto.d.ts +2 -1
  185. package/dist/hono/flow-crypto.d.ts.map +1 -1
  186. package/dist/hono/flow-crypto.js +50 -26
  187. package/dist/hono/flow-crypto.js.map +1 -1
  188. package/dist/hono/flow-crypto.test.js +32 -0
  189. package/dist/hono/flow-crypto.test.js.map +1 -1
  190. package/dist/hono/flow-input-card-fields.test.d.ts +10 -0
  191. package/dist/hono/flow-input-card-fields.test.d.ts.map +1 -0
  192. package/dist/hono/flow-input-card-fields.test.js +26 -0
  193. package/dist/hono/flow-input-card-fields.test.js.map +1 -0
  194. package/dist/hono/flow-input-fields.test.js +1 -1
  195. package/dist/hono/flow-input-fields.test.js.map +1 -1
  196. package/dist/hono/flow-input.d.ts +23 -2
  197. package/dist/hono/flow-input.d.ts.map +1 -1
  198. package/dist/hono/flow-input.js +63 -14
  199. package/dist/hono/flow-input.js.map +1 -1
  200. package/dist/hono/flow-input.test.js +67 -11
  201. package/dist/hono/flow-input.test.js.map +1 -1
  202. package/dist/hono/jev.d.ts +16 -0
  203. package/dist/hono/jev.d.ts.map +1 -0
  204. package/dist/hono/jev.js +60 -0
  205. package/dist/hono/jev.js.map +1 -0
  206. package/dist/hono/jev.test.d.ts +2 -0
  207. package/dist/hono/jev.test.d.ts.map +1 -0
  208. package/dist/hono/jev.test.js +15 -0
  209. package/dist/hono/jev.test.js.map +1 -0
  210. package/dist/hono/knowledge-content.d.ts.map +1 -1
  211. package/dist/hono/knowledge-content.js +4 -2
  212. package/dist/hono/knowledge-content.js.map +1 -1
  213. package/dist/hono/knowledge-content.test.js +6 -5
  214. package/dist/hono/knowledge-content.test.js.map +1 -1
  215. package/dist/hono/mcp-servers.d.ts +8 -1
  216. package/dist/hono/mcp-servers.d.ts.map +1 -1
  217. package/dist/hono/mcp-servers.js +17 -6
  218. package/dist/hono/mcp-servers.js.map +1 -1
  219. package/dist/hono/mcp-servers.test.js +25 -0
  220. package/dist/hono/mcp-servers.test.js.map +1 -1
  221. package/dist/hono/model-capabilities.d.ts.map +1 -1
  222. package/dist/hono/model-capabilities.js +20 -9
  223. package/dist/hono/model-capabilities.js.map +1 -1
  224. package/dist/hono/model-capabilities.test.js +16 -8
  225. package/dist/hono/model-capabilities.test.js.map +1 -1
  226. package/dist/hono/oom-priority.agent.test.d.ts +2 -0
  227. package/dist/hono/oom-priority.agent.test.d.ts.map +1 -0
  228. package/dist/hono/oom-priority.agent.test.js +138 -0
  229. package/dist/hono/oom-priority.agent.test.js.map +1 -0
  230. package/dist/hono/oom-priority.d.ts +39 -0
  231. package/dist/hono/oom-priority.d.ts.map +1 -0
  232. package/dist/hono/oom-priority.js +138 -0
  233. package/dist/hono/oom-priority.js.map +1 -0
  234. package/dist/hono/oom-priority.test.d.ts +2 -0
  235. package/dist/hono/oom-priority.test.d.ts.map +1 -0
  236. package/dist/hono/oom-priority.test.js +52 -0
  237. package/dist/hono/oom-priority.test.js.map +1 -0
  238. package/dist/hono/openrouter-session.d.ts.map +1 -1
  239. package/dist/hono/openrouter-session.js +12 -1
  240. package/dist/hono/openrouter-session.js.map +1 -1
  241. package/dist/hono/openrouter-session.test.js +9 -3
  242. package/dist/hono/openrouter-session.test.js.map +1 -1
  243. package/dist/hono/prompt-context.d.ts +8 -1
  244. package/dist/hono/prompt-context.d.ts.map +1 -1
  245. package/dist/hono/prompt-context.js +45 -7
  246. package/dist/hono/prompt-context.js.map +1 -1
  247. package/dist/hono/prompt-context.test.js +32 -1
  248. package/dist/hono/prompt-context.test.js.map +1 -1
  249. package/dist/hono/publish-recovery.d.ts +1 -1
  250. package/dist/hono/publish-recovery.d.ts.map +1 -1
  251. package/dist/hono/publish-recovery.js +9 -6
  252. package/dist/hono/publish-recovery.js.map +1 -1
  253. package/dist/hono/publish-recovery.test.js +11 -2
  254. package/dist/hono/publish-recovery.test.js.map +1 -1
  255. package/dist/hono/relay.d.ts +10 -1
  256. package/dist/hono/relay.d.ts.map +1 -1
  257. package/dist/hono/relay.js +6 -0
  258. package/dist/hono/relay.js.map +1 -1
  259. package/dist/hono/sandbox-content.d.ts +2 -0
  260. package/dist/hono/sandbox-content.d.ts.map +1 -1
  261. package/dist/hono/sandbox-content.js +8 -4
  262. package/dist/hono/sandbox-content.js.map +1 -1
  263. package/dist/hono/sandbox-content.test.js +20 -1
  264. package/dist/hono/sandbox-content.test.js.map +1 -1
  265. package/dist/hono/server.d.ts.map +1 -1
  266. package/dist/hono/server.js +32 -0
  267. package/dist/hono/server.js.map +1 -1
  268. package/dist/hono/server.test.js +21 -1
  269. package/dist/hono/server.test.js.map +1 -1
  270. package/dist/hono/session-continuity.integration.test.js +10 -1
  271. package/dist/hono/session-continuity.integration.test.js.map +1 -1
  272. package/dist/hono/tag-fields.d.ts.map +1 -1
  273. package/dist/hono/tag-fields.js +2 -0
  274. package/dist/hono/tag-fields.js.map +1 -1
  275. package/dist/hono/tag-input.d.ts +124 -3
  276. package/dist/hono/tag-input.d.ts.map +1 -1
  277. package/dist/hono/tag-input.js +27 -21
  278. package/dist/hono/tag-input.js.map +1 -1
  279. package/dist/hono/tag-input.test.js +19 -1
  280. package/dist/hono/tag-input.test.js.map +1 -1
  281. package/dist/hono/tool-mentions.d.ts.map +1 -1
  282. package/dist/hono/tool-mentions.js +1 -0
  283. package/dist/hono/tool-mentions.js.map +1 -1
  284. package/dist/hono/tool-mentions.test.js +5 -0
  285. package/dist/hono/tool-mentions.test.js.map +1 -1
  286. package/dist/hono/types.d.ts +8 -0
  287. package/dist/hono/types.d.ts.map +1 -1
  288. package/dist/hono/workspace-script.d.ts +10 -0
  289. package/dist/hono/workspace-script.d.ts.map +1 -0
  290. package/dist/hono/workspace-script.js +19 -0
  291. package/dist/hono/workspace-script.js.map +1 -0
  292. package/dist/hono/workspace-settle.d.ts +20 -0
  293. package/dist/hono/workspace-settle.d.ts.map +1 -0
  294. package/dist/hono/workspace-settle.js +50 -0
  295. package/dist/hono/workspace-settle.js.map +1 -0
  296. package/dist/review/digest.d.ts +8 -1
  297. package/dist/review/digest.d.ts.map +1 -1
  298. package/dist/review/digest.js +10 -43
  299. package/dist/review/digest.js.map +1 -1
  300. package/dist/review/digest.test.js +11 -6
  301. package/dist/review/digest.test.js.map +1 -1
  302. package/dist/review/evidence-block.js +1 -1
  303. package/dist/review/evidence-block.js.map +1 -1
  304. package/dist/review/evidence-block.test.js +0 -10
  305. package/dist/review/evidence-block.test.js.map +1 -1
  306. package/dist/review/shell-policy.d.ts +6 -0
  307. package/dist/review/shell-policy.d.ts.map +1 -1
  308. package/dist/review/shell-policy.js +240 -72
  309. package/dist/review/shell-policy.js.map +1 -1
  310. package/dist/review/shell-policy.test.js +114 -0
  311. package/dist/review/shell-policy.test.js.map +1 -1
  312. package/package.json +9 -5
  313. package/dist/hono/baker-ui-server.test.d.ts +0 -2
  314. package/dist/hono/baker-ui-server.test.d.ts.map +0 -1
  315. package/dist/hono/baker-ui-server.test.js +0 -46
  316. package/dist/hono/baker-ui-server.test.js.map +0 -1
  317. package/dist/hono/latitude-telemetry.d.ts +0 -17
  318. package/dist/hono/latitude-telemetry.d.ts.map +0 -1
  319. package/dist/hono/latitude-telemetry.js +0 -52
  320. package/dist/hono/latitude-telemetry.js.map +0 -1
  321. package/dist/hono/latitude-telemetry.test.d.ts +0 -2
  322. package/dist/hono/latitude-telemetry.test.d.ts.map +0 -1
  323. package/dist/hono/latitude-telemetry.test.js +0 -18
  324. 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()) {
798
+ return;
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
+ });
710
825
  return;
711
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();
@@ -990,7 +1120,11 @@ export class BridgeJobExecutor {
990
1120
  const verbatim = verbatimSlashCommand(job.prompt) ??
991
1121
  leadingSkillCommand(job.prompt, (await readSessionSkills(bridgeRuntimeFiles.projectRoot())) ?? []);
992
1122
  let requestPrompt = verbatim ?? applyCurrentSenderAttribution(job.prompt, job.currentSender);
993
- 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;
994
1128
  if (job.attachments && job.attachments.length > 0) {
995
1129
  requestPrompt += await this.prepareAttachments(job.attachments, "message");
996
1130
  }
@@ -1247,6 +1381,39 @@ export class BridgeJobExecutor {
1247
1381
  return undefined;
1248
1382
  }
1249
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
+ }
1250
1417
  reportMcpFailures(job, msg) {
1251
1418
  for (const scope of composioScopesToInvalidate(msg)) {
1252
1419
  void reportMcpInvalid(job.threadId, { scope }, job.currentSender?.userId);
@@ -1266,7 +1433,7 @@ export class BridgeJobExecutor {
1266
1433
  * handlers — except on an unattended turn, where there is nobody to answer and
1267
1434
  * every one of them is refused up front (see `UNATTENDED_PROMPT_MESSAGE`).
1268
1435
  */
1269
- routeToolUse(job, controls, toolName, input, opts) {
1436
+ async routeToolUse(job, controls, toolName, input, opts) {
1270
1437
  const refusedShell = reviewShellRefusal(job, toolName, input);
1271
1438
  if (refusedShell) {
1272
1439
  return Promise.resolve({ behavior: "deny", message: refusedShell });
@@ -1282,9 +1449,9 @@ export class BridgeJobExecutor {
1282
1449
  });
1283
1450
  return Promise.resolve({ behavior: "deny", message: UNATTENDED_PROMPT_MESSAGE });
1284
1451
  }
1285
- const malformed = refuseMalformedCall(toolName, input, job.prompt, userTurnsBefore(job));
1452
+ const malformed = await refuseMalformedCall(toolName, input, this.judge, job.prompt, userTurnsBefore(job));
1286
1453
  if (malformed)
1287
- return Promise.resolve({ behavior: "deny", message: malformed });
1454
+ return { behavior: "deny", message: malformed };
1288
1455
  // Cast what is owed FIRST, whatever comes next — including another create. Checked
1289
1456
  // the other way round, a second create (the agent retrying after a rejected flag,
1290
1457
  // which is what happens in practice) would overwrite the handle and the first
@@ -1337,6 +1504,15 @@ export class BridgeJobExecutor {
1337
1504
  if (toolName === CONNECTION_INPUT_TOOL_NAME) {
1338
1505
  return this.handleConnectionInputRequest(job, controls, input, opts.toolUseID, opts.signal);
1339
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
+ }
1340
1516
  if (toolName === AVATAR_CASTING_TOOL_NAME) {
1341
1517
  return this.handleAvatarCastingRequest(job, controls, input, opts.toolUseID, opts.signal);
1342
1518
  }
@@ -1375,6 +1551,10 @@ export class BridgeJobExecutor {
1375
1551
  // warning to the user. Pinning it takes that threshold out of remote control.
1376
1552
  // See `autoCompactWindowForModel` for the full chain and its ceiling.
1377
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",
1378
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.
1379
1559
  ...(process.env.BAKER_CHAT_ID
1380
1560
  ? {
@@ -1407,11 +1587,8 @@ export class BridgeJobExecutor {
1407
1587
  settingSources: ["project"],
1408
1588
  // `baker_ui` hosts in-process interactive dashboard tools (secure tag
1409
1589
  // form). Always present; HTTP (Composio) servers merge alongside it.
1410
- mcpServers: { ...(mcpServers ?? {}), baker_ui: createBakerUiServer() },
1411
- // Only send the key when there is something in it: the SDK forwards
1412
- // `settings` to the CLI whenever it is present, so an empty object still
1413
- // becomes a `--settings {}` flag tier for the CLI to parse.
1414
- ...(Object.keys(settings).length > 0 ? { settings } : {}),
1590
+ mcpServers: { ...(mcpServers ?? {}), baker_ui: createBakerUiServer({ credentials: credentialsEnabled() }) },
1591
+ settings,
1415
1592
  includePartialMessages: true,
1416
1593
  // Only the workspace's own skills. Omitting the option is NOT "skills
1417
1594
  // off" — the CLI's defaults still load its built-in developer skills
@@ -1420,8 +1597,11 @@ export class BridgeJobExecutor {
1420
1597
  // the workspace could not be read; omit rather than allowlist nothing.
1421
1598
  ...(skills ? { skills } : {}),
1422
1599
  disallowedTools: disallowedToolsFor(job),
1423
- ...reviewQueryOptions(job),
1424
- ...(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",
1425
1605
  canUseTool: (toolName, input, opts) => this.routeToolUse(job, controls, toolName, input, opts),
1426
1606
  // No `fallbackModel`, and no server-side refusal `fallbacks` in the extra
1427
1607
  // body: the CLI's refusal fallback is gated on a first-party base URL (see
@@ -1445,14 +1625,13 @@ export class BridgeJobExecutor {
1445
1625
  // system-reminder repeatedly pushed the agent toward that ephemeral
1446
1626
  // backlog instead of the product's own Tasks (`baker actions`).
1447
1627
  CLAUDE_CODE_ENABLE_TASKS: "0",
1448
- // Defer MCP tool schemas to their names until `ToolSearch` loads one.
1449
- // Unset, the CLI loads every schema up front whenever ANTHROPIC_BASE_URL
1450
- // is not first-party (the OpenRouter passthrough): a connected Make
1451
- // toolkit put ~180 schemas, ~115k tokens, into every request of a Session
1452
- // that called two. Measured through OpenRouter on Bedrock: `tool_reference`
1453
- // round-trips, and a 150-tool server's prefix fell from 134k to 17k.
1454
- // `baker_ui` opts out with `alwaysLoad` (see `createBakerUiServer`).
1455
- ENABLE_TOOL_SEARCH: "true",
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),
1456
1635
  // `citty` writes ANSI escapes into `baker --help` unconditionally —
1457
1636
  // they are never TTY-gated, so every help read the agent does inside
1458
1637
  // the sandbox pays ~5-10% of its tokens on colour codes it can't see.
@@ -1474,6 +1653,7 @@ export class BridgeJobExecutor {
1474
1653
  ...attempt.resumeOptions,
1475
1654
  };
1476
1655
  const query = this.sdk.query({ prompt: attempt.prompt, reminder: attempt.reminder, options });
1656
+ controls.reloadMcpServers = () => this.reloadMcpServers(job, query);
1477
1657
  let queryClosed = false;
1478
1658
  const closeQuery = () => {
1479
1659
  if (queryClosed) {
@@ -1495,6 +1675,7 @@ export class BridgeJobExecutor {
1495
1675
  throw error;
1496
1676
  }
1497
1677
  finally {
1678
+ controls.reloadMcpServers = undefined;
1498
1679
  controls.abortSignal.removeEventListener("abort", closeQuery);
1499
1680
  // Release the held-open input if the turn-end policy never fired (the
1500
1681
  // stream ended on its own, or an error path closed the query). Idempotent.
@@ -1518,7 +1699,19 @@ export class BridgeJobExecutor {
1518
1699
  // in-progress session.
1519
1700
  async drainQuery(job, controls, state, query, closeQuery) {
1520
1701
  const stallDetector = this.createStallDetector(job, controls, closeQuery);
1521
- 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
+ });
1522
1715
  let costUsd = 0;
1523
1716
  // Errors thrown while HANDLING a message (control-channel breaker, error
1524
1717
  // results) are real verdicts and must surface as-is; only errors from the
@@ -1528,6 +1721,7 @@ export class BridgeJobExecutor {
1528
1721
  for await (const msg of query) {
1529
1722
  stallDetector.observeMessage(msg);
1530
1723
  turnEnd.observeMessage(msg);
1724
+ controls.setWorkInFlight?.(stallDetector.hasPendingToolUses() || turnEnd.hasInFlightWork());
1531
1725
  try {
1532
1726
  costUsd = await this.consumeSdkMessage(job, controls, state, msg, costUsd);
1533
1727
  }
@@ -1550,6 +1744,8 @@ export class BridgeJobExecutor {
1550
1744
  throw error;
1551
1745
  }
1552
1746
  finally {
1747
+ drained = true;
1748
+ controls.setWorkInFlight?.(false);
1553
1749
  stallDetector.dispose();
1554
1750
  turnEnd.dispose();
1555
1751
  await this.openPendingCastingAtTurnEnd(job, controls);
@@ -1557,7 +1753,11 @@ export class BridgeJobExecutor {
1557
1753
  if (stallDetector.stalled() && controls.isActive()) {
1558
1754
  throw new StreamStallError();
1559
1755
  }
1560
- 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);
1561
1761
  }
1562
1762
  /**
1563
1763
  * Open a casting the turn recorded but never reached.
@@ -1614,6 +1814,7 @@ export class BridgeJobExecutor {
1614
1814
  controls.pendingTagInputs.size > 0 ||
1615
1815
  controls.pendingFlowInputs.size > 0 ||
1616
1816
  controls.pendingConnectionInputs.size > 0 ||
1817
+ controls.pendingCredentialInputs.size > 0 ||
1617
1818
  controls.pendingAvatarCastings.size > 0,
1618
1819
  onStall: () => {
1619
1820
  console.error(`[BridgeJob] no stream data for ${this.stallTimeoutMs}ms while awaiting the model — closing query for retry (thread ${job.threadId})`);
@@ -1650,17 +1851,7 @@ export class BridgeJobExecutor {
1650
1851
  }
1651
1852
  trackMainRequestUsage(state, msg);
1652
1853
  if (msg.type === "result") {
1653
- const own = ownQueryUsage({ totalCostUsd: msg.total_cost_usd, modelUsage: msg.modelUsage }, state.queryUsageBaseline);
1654
- state.observedCostUsd = own.costUsd;
1655
- // A query's result `modelUsage` is cumulative (like `total_cost_usd`, which
1656
- // we overwrite above), so keep the latest snapshot rather than summing —
1657
- // it is folded into the turn totals when the query settles (see
1658
- // `runQueryOnce`). Guard against a spuriously empty later result wiping a
1659
- // real earlier one.
1660
- const snapshot = queryModelUsage(own.modelUsage);
1661
- if (snapshot.size > 0) {
1662
- state.queryModelUsage = snapshot;
1663
- }
1854
+ const ownCostUsd = recordCumulativeUsage(state, { totalCostUsd: msg.total_cost_usd, modelUsage: msg.modelUsage });
1664
1855
  await this.publishContextUsage(job, state, msg.modelUsage);
1665
1856
  // What a Session review returns. The SDK enforces the JSON schema, so this
1666
1857
  // either parses against the findings contract or the review failed to
@@ -1677,7 +1868,7 @@ export class BridgeJobExecutor {
1677
1868
  await this.clearIfDirty(job, controls, state, "result_error");
1678
1869
  throw resultError;
1679
1870
  }
1680
- return own.costUsd;
1871
+ return ownCostUsd;
1681
1872
  }
1682
1873
  if (isUserVisibleSdkMessage(msg)) {
1683
1874
  state.sawUserVisibleSdkMessage = true;
@@ -2052,6 +2243,16 @@ export class BridgeJobExecutor {
2052
2243
  // Strip any secret values the model tried to prefill; their names still reach
2053
2244
  // the user as requested fields. The backend strips again.
2054
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
+ }
2055
2256
  const posted = await this.relay.flowInputRequest(target, { toolUseId, requests: sanitizedRequests });
2056
2257
  if (!posted) {
2057
2258
  console.error(`[request_flow_input] failed to post flow-input-request for ${toolUseId} — denying to avoid a hang`);
@@ -2102,7 +2303,7 @@ export class BridgeJobExecutor {
2102
2303
  if (connections.length === 0) {
2103
2304
  return {
2104
2305
  behavior: "deny",
2105
- 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.`,
2106
2307
  };
2107
2308
  }
2108
2309
  const posted = await this.relay.connectionInputRequest(target, { toolUseId, connections });
@@ -2124,7 +2325,43 @@ export class BridgeJobExecutor {
2124
2325
  return { behavior: "deny", message: PROMPT_UNANSWERED_MESSAGE };
2125
2326
  }
2126
2327
  void this.relay.connectionInputResolved(target);
2127
- 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 } };
2128
2365
  }
2129
2366
  async handlePlanReview(job, controls, input, toolUseId, signal) {
2130
2367
  const target = { threadId: job.threadId, turnId: job.turnId };