@cursor/july 0.1.95 → 0.1.97

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 (424) hide show
  1. package/AGENTS.md +3 -4
  2. package/README.md +3 -2
  3. package/dist/bin/agent-serve.js +14 -3
  4. package/dist/channels/deployments/types.d.ts +1 -1
  5. package/dist/channels/deployments/types.d.ts.map +1 -1
  6. package/dist/channels/deployments/types.js +1 -0
  7. package/dist/channels/origin/api.d.ts +72 -15
  8. package/dist/channels/origin/api.d.ts.map +1 -1
  9. package/dist/channels/origin/api.js +185 -28
  10. package/dist/channels/origin/binding.d.ts +13 -2
  11. package/dist/channels/origin/binding.d.ts.map +1 -1
  12. package/dist/channels/origin/binding.js +41 -25
  13. package/dist/channels/origin/checks.d.ts +6 -6
  14. package/dist/channels/origin/checks.js +6 -6
  15. package/dist/channels/origin/cursor-account.d.ts +5 -3
  16. package/dist/channels/origin/cursor-account.d.ts.map +1 -1
  17. package/dist/channels/origin/cursor-account.js +5 -3
  18. package/dist/channels/origin/index.d.ts +1 -0
  19. package/dist/channels/origin/index.d.ts.map +1 -1
  20. package/dist/channels/origin/index.js +1 -0
  21. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  22. package/dist/channels/origin/origin-channel.js +3 -0
  23. package/dist/channels/origin/reactions.d.ts +56 -0
  24. package/dist/channels/origin/reactions.d.ts.map +1 -0
  25. package/dist/channels/origin/reactions.js +67 -0
  26. package/dist/channels/origin/types.d.ts +11 -0
  27. package/dist/channels/origin/types.d.ts.map +1 -1
  28. package/dist/channels/slack/api.d.ts +9 -0
  29. package/dist/channels/slack/api.d.ts.map +1 -1
  30. package/dist/channels/slack/api.js +36 -5
  31. package/dist/channels/slack/approvals.d.ts +16 -0
  32. package/dist/channels/slack/approvals.d.ts.map +1 -1
  33. package/dist/channels/slack/approvals.js +86 -0
  34. package/dist/channels/slack/cli-project.d.ts +19 -0
  35. package/dist/channels/slack/cli-project.d.ts.map +1 -0
  36. package/dist/channels/slack/cli-project.js +234 -0
  37. package/dist/channels/slack/defaults.d.ts.map +1 -1
  38. package/dist/channels/slack/defaults.js +110 -81
  39. package/dist/channels/slack/dispatch.d.ts +7 -1
  40. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  41. package/dist/channels/slack/dispatch.js +47 -23
  42. package/dist/channels/slack/inbound.d.ts +8 -0
  43. package/dist/channels/slack/inbound.d.ts.map +1 -1
  44. package/dist/channels/slack/inbound.js +32 -0
  45. package/dist/channels/slack/init.d.ts +3 -2
  46. package/dist/channels/slack/init.d.ts.map +1 -1
  47. package/dist/channels/slack/init.js +5 -5
  48. package/dist/channels/slack/interactive.d.ts +2 -1
  49. package/dist/channels/slack/interactive.d.ts.map +1 -1
  50. package/dist/channels/slack/interactive.js +15 -4
  51. package/dist/channels/slack/live-delivery.d.ts +26 -29
  52. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  53. package/dist/channels/slack/live-delivery.js +172 -73
  54. package/dist/channels/slack/manifest.d.ts +1 -1
  55. package/dist/channels/slack/manifest.d.ts.map +1 -1
  56. package/dist/channels/slack/manifest.js +6 -2
  57. package/dist/channels/slack/nudge-carrier.d.ts +2 -0
  58. package/dist/channels/slack/nudge-carrier.d.ts.map +1 -1
  59. package/dist/channels/slack/nudge-carrier.js +48 -1
  60. package/dist/channels/slack/setup.d.ts +1 -1
  61. package/dist/channels/slack/setup.d.ts.map +1 -1
  62. package/dist/channels/slack/setup.js +10 -6
  63. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  64. package/dist/channels/slack/slack-channel.js +3 -2
  65. package/dist/channels/slack/tool-approval-events.d.ts.map +1 -1
  66. package/dist/channels/slack/tool-approval-events.js +91 -1
  67. package/dist/docs/404.html +2 -2
  68. package/dist/docs/ab.html +4 -4
  69. package/dist/docs/assets/{app.BBj0klBO.js → app.B3rWNYE1.js} +1 -1
  70. package/dist/docs/assets/chunks/@localSearchIndexroot.BU9nHdnQ.js +1 -0
  71. package/dist/docs/assets/chunks/{VPLocalSearchBox.CUEXpL78.js → VPLocalSearchBox.C1lhqJJR.js} +1 -1
  72. package/dist/docs/assets/chunks/{theme.DabmQnia.js → theme.B40_SXuv.js} +2 -2
  73. package/dist/docs/assets/{deployment.md.D9msOFOW.js → deployment.md.MFPKqsqI.js} +2 -2
  74. package/dist/docs/assets/{guides_mcp-oauth.md.DPYmBCbV.js → guides_mcp-oauth.md.CN-6YmTJ.js} +4 -4
  75. package/dist/docs/assets/{guides_mcp-oauth.md.DPYmBCbV.lean.js → guides_mcp-oauth.md.CN-6YmTJ.lean.js} +1 -1
  76. package/dist/docs/assets/guides_slack.md.VDXQV3ja.js +22 -0
  77. package/dist/docs/assets/guides_slack.md.VDXQV3ja.lean.js +1 -0
  78. package/dist/docs/assets/index.md.CVeRUOeZ.js +5 -0
  79. package/dist/docs/assets/{index.md.BoHaMdeZ.lean.js → index.md.CVeRUOeZ.lean.js} +1 -1
  80. package/dist/docs/assets/{reference_agent-config.md.Cp_x38Nl.js → reference_agent-config.md.BHeYMgme.js} +1 -1
  81. package/dist/docs/assets/{reference_cli.md.BsMOxDvh.js → reference_cli.md.RyZf5OTE.js} +10 -6
  82. package/dist/docs/assets/{reference_cli.md.BsMOxDvh.lean.js → reference_cli.md.RyZf5OTE.lean.js} +1 -1
  83. package/dist/docs/assets/{reference_connections.md.BH8Oc0D0.js → reference_connections.md.DxldvyIB.js} +4 -3
  84. package/dist/docs/assets/{reference_http-api.md.D89k1mdm.js → reference_http-api.md.5zOAbV86.js} +1 -1
  85. package/dist/docs/assets/reference_sessions.md.NxFP_CAC.js +1 -0
  86. package/dist/docs/assets/{reference_tools.md.BHeXn2id.js → reference_tools.md.B84gw9Ii.js} +1 -1
  87. package/dist/docs/assets/templates_agents-md.md.CXSMZljE.js +2 -0
  88. package/dist/docs/assets/templates_agents-md.md.CXSMZljE.lean.js +1 -0
  89. package/dist/docs/assets/templates_code-wiki.md.CDYZIBFN.js +2 -0
  90. package/dist/docs/assets/templates_code-wiki.md.CDYZIBFN.lean.js +1 -0
  91. package/dist/docs/assets/troubleshooting.md.DCiPBhYs.js +1 -0
  92. package/dist/docs/building-with-agents.html +4 -4
  93. package/dist/docs/concepts.html +4 -4
  94. package/dist/docs/deployment.html +7 -7
  95. package/dist/docs/deployment.md +5 -4
  96. package/dist/docs/evals.html +4 -4
  97. package/dist/docs/guides/agent-to-agent.html +4 -4
  98. package/dist/docs/guides/cloud-runtime.html +4 -4
  99. package/dist/docs/guides/convert-automation.html +4 -4
  100. package/dist/docs/guides/github.html +4 -4
  101. package/dist/docs/guides/human-in-the-loop.html +4 -4
  102. package/dist/docs/guides/mcp-oauth.html +8 -8
  103. package/dist/docs/guides/mcp-oauth.md +26 -8
  104. package/dist/docs/guides/opentelemetry.html +4 -4
  105. package/dist/docs/guides/slack.html +17 -33
  106. package/dist/docs/guides/slack.md +92 -201
  107. package/dist/docs/guides/webhooks.html +4 -4
  108. package/dist/docs/hashmap.json +1 -1
  109. package/dist/docs/hillclimbing.html +4 -4
  110. package/dist/docs/index.html +6 -6
  111. package/dist/docs/index.md +7 -3
  112. package/dist/docs/llms-full.txt +398 -653
  113. package/dist/docs/llms.txt +3 -2
  114. package/dist/docs/quickstart.html +4 -4
  115. package/dist/docs/reference/agent-config.html +6 -6
  116. package/dist/docs/reference/agent-config.md +2 -2
  117. package/dist/docs/reference/artifacts.html +4 -4
  118. package/dist/docs/reference/channels.html +4 -4
  119. package/dist/docs/reference/cli.html +14 -10
  120. package/dist/docs/reference/cli.md +39 -16
  121. package/dist/docs/reference/connections.html +9 -8
  122. package/dist/docs/reference/connections.md +6 -3
  123. package/dist/docs/reference/hooks.html +4 -4
  124. package/dist/docs/reference/http-api.html +6 -6
  125. package/dist/docs/reference/http-api.md +2 -1
  126. package/dist/docs/reference/instructions.html +4 -4
  127. package/dist/docs/reference/playground.html +4 -4
  128. package/dist/docs/reference/project-layout.html +4 -4
  129. package/dist/docs/reference/prompt.html +4 -4
  130. package/dist/docs/reference/schedules.html +4 -4
  131. package/dist/docs/reference/sessions.html +5 -5
  132. package/dist/docs/reference/sessions.md +4 -2
  133. package/dist/docs/reference/skills.html +4 -4
  134. package/dist/docs/reference/subagents.html +4 -4
  135. package/dist/docs/reference/tools.html +6 -6
  136. package/dist/docs/reference/tools.md +9 -2
  137. package/dist/docs/scaffolding-agents.html +4 -4
  138. package/dist/docs/storage.html +4 -4
  139. package/dist/docs/templates/agentic-owners.html +4 -4
  140. package/dist/docs/templates/agents-md.html +28 -0
  141. package/dist/docs/templates/agents-md.md +105 -0
  142. package/dist/docs/templates/code-wiki.html +28 -0
  143. package/dist/docs/templates/code-wiki.md +91 -0
  144. package/dist/docs/templates/demo.html +5 -5
  145. package/dist/docs/templates/pr-autofixer.html +4 -4
  146. package/dist/docs/templates/security-help.html +4 -4
  147. package/dist/docs/templates/security-reviewer.html +5 -5
  148. package/dist/docs/templates/triage.html +4 -4
  149. package/dist/docs/troubleshooting.html +5 -5
  150. package/dist/docs/troubleshooting.md +2 -2
  151. package/dist/evals/run-facts.d.ts.map +1 -1
  152. package/dist/evals/run-facts.js +27 -0
  153. package/dist/files-backends/cursor-hosted.d.ts +26 -4
  154. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  155. package/dist/files-backends/cursor-hosted.js +32 -4
  156. package/dist/internal/advertise-tools.d.ts +2 -0
  157. package/dist/internal/advertise-tools.d.ts.map +1 -1
  158. package/dist/internal/advertise-tools.js +30 -10
  159. package/dist/internal/approval-gate.d.ts +12 -13
  160. package/dist/internal/approval-gate.d.ts.map +1 -1
  161. package/dist/internal/approval-gate.js +16 -21
  162. package/dist/internal/bounded.d.ts +7 -0
  163. package/dist/internal/bounded.d.ts.map +1 -0
  164. package/dist/internal/bounded.js +32 -0
  165. package/dist/internal/chat-client.js +10 -0
  166. package/dist/internal/cli-deploy.d.ts +5 -0
  167. package/dist/internal/cli-deploy.d.ts.map +1 -1
  168. package/dist/internal/cli-deploy.js +57 -18
  169. package/dist/internal/cli-mcp-oauth.js +8 -18
  170. package/dist/internal/cli-slack-manual.d.ts +24 -2
  171. package/dist/internal/cli-slack-manual.d.ts.map +1 -1
  172. package/dist/internal/cli-slack-manual.js +251 -20
  173. package/dist/internal/cli-slack.d.ts +9 -2
  174. package/dist/internal/cli-slack.d.ts.map +1 -1
  175. package/dist/internal/cli-slack.js +29 -82
  176. package/dist/internal/conversation-mirror.d.ts +11 -8
  177. package/dist/internal/conversation-mirror.d.ts.map +1 -1
  178. package/dist/internal/conversation-mirror.js +116 -55
  179. package/dist/internal/cursor/account-mcp.d.ts +1 -0
  180. package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
  181. package/dist/internal/cursor/account-mcp.js +17 -0
  182. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  183. package/dist/internal/cursor/backend-client.js +22 -48
  184. package/dist/internal/cursor/session-token.d.ts +45 -0
  185. package/dist/internal/cursor/session-token.d.ts.map +1 -0
  186. package/dist/internal/cursor/session-token.js +99 -0
  187. package/dist/internal/cursor-agent-template.d.ts +1 -1
  188. package/dist/internal/cursor-agent-template.d.ts.map +1 -1
  189. package/dist/internal/cursor-agent-template.js +2 -0
  190. package/dist/internal/deploy-client.d.ts +17 -0
  191. package/dist/internal/deploy-client.d.ts.map +1 -1
  192. package/dist/internal/deploy-client.js +31 -10
  193. package/dist/internal/env-file.d.ts +19 -0
  194. package/dist/internal/env-file.d.ts.map +1 -0
  195. package/dist/internal/env-file.js +90 -0
  196. package/dist/internal/framework-file-storage.d.ts +61 -0
  197. package/dist/internal/framework-file-storage.d.ts.map +1 -0
  198. package/dist/internal/framework-file-storage.js +210 -0
  199. package/dist/internal/framework-storage-selection.d.ts +11 -5
  200. package/dist/internal/framework-storage-selection.d.ts.map +1 -1
  201. package/dist/internal/framework-storage-selection.js +38 -11
  202. package/dist/internal/hosted-delivery-protocol.d.ts +7 -0
  203. package/dist/internal/hosted-delivery-protocol.d.ts.map +1 -1
  204. package/dist/internal/hosted-delivery-protocol.js +7 -0
  205. package/dist/internal/hosted-delivery.d.ts +13 -0
  206. package/dist/internal/hosted-delivery.d.ts.map +1 -1
  207. package/dist/internal/hosted-delivery.js +64 -19
  208. package/dist/internal/hosted-execution-diag.d.ts +48 -0
  209. package/dist/internal/hosted-execution-diag.d.ts.map +1 -0
  210. package/dist/internal/hosted-execution-diag.js +150 -0
  211. package/dist/internal/http-channel.d.ts.map +1 -1
  212. package/dist/internal/http-channel.js +81 -1
  213. package/dist/internal/init-project.d.ts.map +1 -1
  214. package/dist/internal/init-project.js +4 -0
  215. package/dist/internal/install-cursor-skills.d.ts.map +1 -1
  216. package/dist/internal/install-cursor-skills.js +1 -3
  217. package/dist/internal/logs-client.d.ts +5 -0
  218. package/dist/internal/logs-client.d.ts.map +1 -1
  219. package/dist/internal/logs-client.js +5 -0
  220. package/dist/internal/managed-delivery.d.ts +80 -0
  221. package/dist/internal/managed-delivery.d.ts.map +1 -0
  222. package/dist/internal/managed-delivery.js +86 -0
  223. package/dist/internal/mcp-endpoint.js +2 -3
  224. package/dist/internal/mcp-host.d.ts +10 -0
  225. package/dist/internal/mcp-host.d.ts.map +1 -1
  226. package/dist/internal/mcp-host.js +57 -8
  227. package/dist/internal/mcp-oauth-on-demand.d.ts +68 -0
  228. package/dist/internal/mcp-oauth-on-demand.d.ts.map +1 -0
  229. package/dist/internal/mcp-oauth-on-demand.js +287 -0
  230. package/dist/internal/mcp-oauth-persist.d.ts +42 -0
  231. package/dist/internal/mcp-oauth-persist.d.ts.map +1 -0
  232. package/dist/internal/mcp-oauth-persist.js +92 -0
  233. package/dist/internal/mcp-oauth.d.ts +1 -0
  234. package/dist/internal/mcp-oauth.d.ts.map +1 -1
  235. package/dist/internal/mcp-oauth.js +10 -2
  236. package/dist/internal/otel/collector.d.ts.map +1 -1
  237. package/dist/internal/otel/collector.js +22 -9
  238. package/dist/internal/runtime-dispatch-runner.d.ts +5 -1
  239. package/dist/internal/runtime-dispatch-runner.d.ts.map +1 -1
  240. package/dist/internal/runtime-dispatch-runner.js +7 -0
  241. package/dist/internal/sdk-runner.d.ts +11 -0
  242. package/dist/internal/sdk-runner.d.ts.map +1 -1
  243. package/dist/internal/sdk-runner.js +39 -8
  244. package/dist/internal/server.d.ts +3 -0
  245. package/dist/internal/server.d.ts.map +1 -1
  246. package/dist/internal/server.js +49 -21
  247. package/dist/internal/session-engine.d.ts +101 -3
  248. package/dist/internal/session-engine.d.ts.map +1 -1
  249. package/dist/internal/session-engine.js +577 -46
  250. package/dist/internal/sessions-client.d.ts +9 -7
  251. package/dist/internal/sessions-client.d.ts.map +1 -1
  252. package/dist/internal/sessions-client.js +3 -1
  253. package/dist/internal/steering.d.ts +15 -0
  254. package/dist/internal/steering.d.ts.map +1 -0
  255. package/dist/internal/steering.js +18 -0
  256. package/dist/internal/storage-coordinator.d.ts +7 -0
  257. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  258. package/dist/internal/storage-coordinator.js +28 -5
  259. package/dist/internal/storage-roles.d.ts +5 -0
  260. package/dist/internal/storage-roles.d.ts.map +1 -1
  261. package/dist/internal/stream-progress.d.ts.map +1 -1
  262. package/dist/internal/stream-progress.js +10 -0
  263. package/dist/internal/tool-policy.d.ts +9 -1
  264. package/dist/internal/tool-policy.d.ts.map +1 -1
  265. package/dist/internal/tool-policy.js +13 -0
  266. package/dist/internal/trajectory.d.ts.map +1 -1
  267. package/dist/internal/trajectory.js +10 -0
  268. package/dist/internal/turn-outbox.d.ts +24 -0
  269. package/dist/internal/turn-outbox.d.ts.map +1 -0
  270. package/dist/internal/turn-outbox.js +80 -0
  271. package/dist/playground/assets/{index-CZA1uEWA.js → index-DrkI6y5O.js} +51 -51
  272. package/dist/playground/assets/index-DzNGwm7q.css +1 -0
  273. package/dist/playground/index.html +2 -2
  274. package/dist/storage-backends/cursor-hosted.d.ts +9 -6
  275. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  276. package/dist/storage-backends/cursor-hosted.js +16 -3
  277. package/dist/storage.d.ts +8 -0
  278. package/dist/storage.d.ts.map +1 -1
  279. package/dist/types.d.ts +65 -8
  280. package/dist/types.d.ts.map +1 -1
  281. package/docs/README.md +7 -3
  282. package/docs/deployment.md +5 -4
  283. package/docs/guides/mcp-oauth.md +26 -8
  284. package/docs/guides/slack.md +93 -202
  285. package/docs/reference/agent-config.md +2 -2
  286. package/docs/reference/cli.md +39 -16
  287. package/docs/reference/connections.md +6 -3
  288. package/docs/reference/http-api.md +2 -1
  289. package/docs/reference/sessions.md +4 -2
  290. package/docs/reference/tools.md +9 -2
  291. package/docs/templates/agents-md.md +110 -0
  292. package/docs/templates/code-wiki.md +96 -0
  293. package/docs/troubleshooting.md +2 -2
  294. package/package.json +2 -2
  295. package/skills/ab/SKILL.md +13 -97
  296. package/skills/create-agent/SKILL.md +124 -223
  297. package/skills/debug/SKILL.md +34 -68
  298. package/skills/evals/SKILL.md +55 -180
  299. package/skills/framework-map/SKILL.md +83 -145
  300. package/skills/github/SKILL.md +53 -111
  301. package/skills/hillclimb/SKILL.md +40 -163
  302. package/skills/mcp-auth/SKILL.md +51 -107
  303. package/skills/otel/SKILL.md +22 -68
  304. package/skills/setup-slack/SKILL.md +91 -212
  305. package/src/bin/agent-serve.ts +13 -1
  306. package/src/channels/deployments/types.ts +1 -0
  307. package/src/channels/origin/api.ts +264 -33
  308. package/src/channels/origin/binding.ts +57 -28
  309. package/src/channels/origin/checks.ts +6 -6
  310. package/src/channels/origin/cursor-account.ts +5 -3
  311. package/src/channels/origin/index.ts +1 -0
  312. package/src/channels/origin/origin-channel.ts +4 -0
  313. package/src/channels/origin/reactions.ts +82 -0
  314. package/src/channels/origin/types.ts +11 -0
  315. package/src/channels/slack/api.ts +41 -5
  316. package/src/channels/slack/approvals.ts +105 -0
  317. package/src/channels/slack/cli-project.ts +267 -0
  318. package/src/channels/slack/defaults.ts +122 -93
  319. package/src/channels/slack/dispatch.ts +56 -22
  320. package/src/channels/slack/inbound.ts +43 -0
  321. package/src/channels/slack/init.ts +10 -5
  322. package/src/channels/slack/interactive.ts +35 -9
  323. package/src/channels/slack/live-delivery.ts +235 -114
  324. package/src/channels/slack/manifest.ts +6 -2
  325. package/src/channels/slack/nudge-carrier.ts +48 -1
  326. package/src/channels/slack/setup.ts +10 -6
  327. package/src/channels/slack/slack-channel.ts +3 -0
  328. package/src/channels/slack/tool-approval-events.ts +93 -0
  329. package/src/evals/run-facts.ts +32 -0
  330. package/src/files-backends/cursor-hosted.ts +40 -7
  331. package/src/internal/advertise-tools.ts +49 -12
  332. package/src/internal/approval-gate.ts +20 -29
  333. package/src/internal/bounded.ts +28 -0
  334. package/src/internal/chat-client.ts +19 -0
  335. package/src/internal/cli-deploy.ts +91 -9
  336. package/src/internal/cli-mcp-oauth.ts +7 -21
  337. package/src/internal/cli-slack-manual.ts +350 -18
  338. package/src/internal/cli-slack.ts +40 -84
  339. package/src/internal/conversation-mirror.ts +145 -69
  340. package/src/internal/cursor/account-mcp.ts +21 -0
  341. package/src/internal/cursor/backend-client.ts +34 -58
  342. package/src/internal/cursor/session-token.ts +115 -0
  343. package/src/internal/cursor-agent-template.ts +2 -0
  344. package/src/internal/deploy-client.ts +43 -0
  345. package/src/internal/env-file.ts +84 -0
  346. package/src/internal/framework-file-storage.ts +228 -0
  347. package/src/internal/framework-storage-selection.ts +51 -11
  348. package/src/internal/hosted-delivery-protocol.ts +7 -0
  349. package/src/internal/hosted-delivery.ts +107 -24
  350. package/src/internal/hosted-execution-diag.ts +207 -0
  351. package/src/internal/http-channel.ts +99 -0
  352. package/src/internal/init-project.ts +4 -0
  353. package/src/internal/install-cursor-skills.ts +1 -4
  354. package/src/internal/logs-client.ts +5 -0
  355. package/src/internal/managed-delivery.ts +125 -0
  356. package/src/internal/mcp-endpoint.ts +13 -4
  357. package/src/internal/mcp-host.ts +77 -11
  358. package/src/internal/mcp-oauth-on-demand.ts +352 -0
  359. package/src/internal/mcp-oauth-persist.ts +127 -0
  360. package/src/internal/mcp-oauth.ts +17 -1
  361. package/src/internal/otel/collector.ts +30 -0
  362. package/src/internal/runtime-dispatch-runner.ts +10 -0
  363. package/src/internal/sdk-runner.ts +56 -8
  364. package/src/internal/server.ts +61 -9
  365. package/src/internal/session-engine.ts +723 -60
  366. package/src/internal/sessions-client.ts +17 -8
  367. package/src/internal/steering.ts +17 -0
  368. package/src/internal/storage-coordinator.ts +38 -8
  369. package/src/internal/storage-roles.ts +7 -0
  370. package/src/internal/stream-progress.ts +19 -0
  371. package/src/internal/tool-policy.ts +18 -0
  372. package/src/internal/trajectory.ts +10 -0
  373. package/src/internal/turn-outbox.ts +85 -0
  374. package/src/storage-backends/cursor-hosted.ts +22 -8
  375. package/src/storage.ts +12 -0
  376. package/src/types.ts +70 -9
  377. package/templates/agents-md/README.md +39 -0
  378. package/templates/agents-md/agent/agent.ts +8 -0
  379. package/templates/agents-md/agent/channels/github.ts +10 -0
  380. package/templates/agents-md/agent/channels/queue.ts +28 -0
  381. package/templates/agents-md/agent/channels/slack.ts +9 -0
  382. package/templates/agents-md/agent/instructions.md +39 -0
  383. package/templates/agents-md/agent/lib/collect.ts +439 -0
  384. package/templates/agents-md/agent/lib/config.ts +8 -0
  385. package/templates/agents-md/agent/lib/paths.ts +51 -0
  386. package/templates/agents-md/agent/schedules/weekly.ts +17 -0
  387. package/templates/agents-md/agent/tools/apply_agents.ts +226 -0
  388. package/templates/agents-md/evals/agents.eval.ts +135 -0
  389. package/templates/agents-md/evals/evals.config.ts +5 -0
  390. package/templates/agents-md/init.json +38 -0
  391. package/templates/agents-md/package.json +18 -0
  392. package/templates/agents-md/tsconfig.json +12 -0
  393. package/templates/code-wiki/README.md +34 -0
  394. package/templates/code-wiki/agent/agent.ts +8 -0
  395. package/templates/code-wiki/agent/channels/github.ts +144 -0
  396. package/templates/code-wiki/agent/instructions.md +39 -0
  397. package/templates/code-wiki/agent/lib/repos.ts +5 -0
  398. package/templates/code-wiki/agent/lib/wiki.ts +22 -0
  399. package/templates/code-wiki/agent/tools/apply_wiki.ts +219 -0
  400. package/templates/code-wiki/evals/evals.config.ts +5 -0
  401. package/templates/code-wiki/evals/wiki.eval.ts +132 -0
  402. package/templates/code-wiki/init.json +25 -0
  403. package/templates/code-wiki/package.json +18 -0
  404. package/templates/code-wiki/tsconfig.json +12 -0
  405. package/dist/docs/assets/chunks/@localSearchIndexroot.oqPawjiD.js +0 -1
  406. package/dist/docs/assets/guides_slack.md.C32HsdKk.js +0 -38
  407. package/dist/docs/assets/guides_slack.md.C32HsdKk.lean.js +0 -1
  408. package/dist/docs/assets/index.md.BoHaMdeZ.js +0 -5
  409. package/dist/docs/assets/reference_sessions.md.D0mIh4KK.js +0 -1
  410. package/dist/docs/assets/troubleshooting.md.Ctv3T8C2.js +0 -1
  411. package/dist/docs/design/agsh.md +0 -406
  412. package/dist/internal/deployment-scm-event-stream.d.ts +0 -116
  413. package/dist/internal/deployment-scm-event-stream.d.ts.map +0 -1
  414. package/dist/internal/deployment-scm-event-stream.js +0 -474
  415. package/dist/playground/assets/index-CF7hUDnQ.css +0 -1
  416. package/docs/design/agsh.md +0 -406
  417. package/src/internal/deployment-scm-event-stream.ts +0 -593
  418. /package/dist/docs/assets/{deployment.md.D9msOFOW.lean.js → deployment.md.MFPKqsqI.lean.js} +0 -0
  419. /package/dist/docs/assets/{reference_agent-config.md.Cp_x38Nl.lean.js → reference_agent-config.md.BHeYMgme.lean.js} +0 -0
  420. /package/dist/docs/assets/{reference_connections.md.BH8Oc0D0.lean.js → reference_connections.md.DxldvyIB.lean.js} +0 -0
  421. /package/dist/docs/assets/{reference_http-api.md.D89k1mdm.lean.js → reference_http-api.md.5zOAbV86.lean.js} +0 -0
  422. /package/dist/docs/assets/{reference_sessions.md.D0mIh4KK.lean.js → reference_sessions.md.NxFP_CAC.lean.js} +0 -0
  423. /package/dist/docs/assets/{reference_tools.md.BHeXn2id.lean.js → reference_tools.md.B84gw9Ii.lean.js} +0 -0
  424. /package/dist/docs/assets/{troubleshooting.md.Ctv3T8C2.lean.js → troubleshooting.md.DCiPBhYs.lean.js} +0 -0
@@ -7,6 +7,7 @@
7
7
  * event handlers and hooks.
8
8
  */
9
9
 
10
+ import { AsyncLocalStorage } from "node:async_hooks";
10
11
  import { mkdir, rm, writeFile } from "node:fs/promises";
11
12
  import { dirname, join } from "node:path";
12
13
  import type { SDKCustomTool, SDKCustomToolResult } from "@cursor/sdk";
@@ -52,12 +53,15 @@ import {
52
53
  type DiscoveredTool,
53
54
  type HookHandler,
54
55
  type HostContext,
56
+ type HostMcpRegistry,
55
57
  type HttpChannelDefinition,
58
+ isCursorAccountTransport,
56
59
  isHostOauthConnectionTransport,
57
60
  isSymbolicConnectionTransport,
58
61
  type JsonValue,
59
62
  type McpToolAnnotations,
60
63
  type PendingApproval,
64
+ type PendingAuth,
61
65
  type ReceiveInput,
62
66
  type ResolvedAgent,
63
67
  type RunnerSubagent,
@@ -79,7 +83,10 @@ import {
79
83
  import { ABCollector } from "./ab-collector.js";
80
84
  import { foldABStream } from "./ab-fold.js";
81
85
  import { type ABSnapshot, buildABSnapshot } from "./ab-snapshot.js";
82
- import { resolveAdvertisedTools } from "./advertise-tools.js";
86
+ import {
87
+ type AdvertiseHostMcp,
88
+ resolveAdvertisedTools,
89
+ } from "./advertise-tools.js";
83
90
  import { ApprovalGate } from "./approval-gate.js";
84
91
  import { ArtifactsStore } from "./artifacts-store.js";
85
92
  import { SessionAuthError, samePrincipal } from "./auth.js";
@@ -125,7 +132,8 @@ import {
125
132
  parseContinuationToken,
126
133
  } from "./ids.js";
127
134
  import { validateToolInput } from "./json-schema.js";
128
- import { McpHost } from "./mcp-host.js";
135
+ import { isMcpNeedsAuthError, McpHost } from "./mcp-host.js";
136
+ import { McpOauthOnDemandHub } from "./mcp-oauth-on-demand.js";
129
137
  import type { OtelCollector } from "./otel/collector.js";
130
138
  import {
131
139
  shouldStartProcessMemoryTelemetry,
@@ -136,13 +144,14 @@ import {
136
144
  SCHEDULE_CHANNEL_PREFIX,
137
145
  } from "./prompt-context.js";
138
146
  import { assertCloudCanHonorAccountServersFilters } from "./resolved-connections.js";
139
- import type { AgentRunner } from "./sdk-runner.js";
147
+ import type { AgentRunner, SteerAckOutcome } from "./sdk-runner.js";
140
148
  import { sessionExternalLinks } from "./session-links.js";
141
149
  import { SessionStore } from "./session-store.js";
142
150
  import { syncSkillsToStore } from "./skills-sync.js";
151
+ import { steeringEnabled } from "./steering.js";
143
152
  import { StorageCoordinator } from "./storage-coordinator.js";
144
153
  import type { FrameworkSessionStore } from "./storage-roles.js";
145
- import { decideToolCall, resolveEffect } from "./tool-policy.js";
154
+ import { decideToolCall, directCallEffect } from "./tool-policy.js";
146
155
  import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
147
156
  import {
148
157
  type EstimateTurnCostFn,
@@ -151,6 +160,7 @@ import {
151
160
  } from "./turn-cost.js";
152
161
  import { TurnGovernor } from "./turn-governor.js";
153
162
  import { TurnLeaseStore } from "./turn-lease-store.js";
163
+ import { TurnOutbox } from "./turn-outbox.js";
154
164
  import {
155
165
  buildAgentsMdContent,
156
166
  buildCloudMemorySection,
@@ -257,6 +267,22 @@ export class ApprovalInterruptedError extends Error {
257
267
  }
258
268
  }
259
269
 
270
+ export class AuthNotFoundError extends Error {
271
+ constructor(sessionId: string, callId: string) {
272
+ super(`No pending MCP auth for session ${sessionId} call ${callId}`);
273
+ this.name = "AuthNotFoundError";
274
+ }
275
+ }
276
+
277
+ export class AuthInterruptedError extends Error {
278
+ constructor(sessionId: string, callId: string) {
279
+ super(
280
+ `MCP auth for session ${sessionId} call ${callId} is no longer live (host may have restarted while the tool was parked)`
281
+ );
282
+ this.name = "AuthInterruptedError";
283
+ }
284
+ }
285
+
260
286
  export interface SessionEngineOptions {
261
287
  project: AgentProject;
262
288
  stateRoot: string;
@@ -311,6 +337,16 @@ interface ActiveToolContext {
311
337
  emit: (payload: SessionEventPayload) => void;
312
338
  }
313
339
 
340
+ type McpAuthParkCtx = {
341
+ sessionId: string;
342
+ turnId: string;
343
+ callId: string;
344
+ toolName: string;
345
+ emit: (payload: SessionEventPayload) => void;
346
+ };
347
+
348
+ const mcpAuthAls = new AsyncLocalStorage<McpAuthParkCtx>();
349
+
314
350
  export interface EngineSendOptions extends SendMessageOptions {
315
351
  mode?: "chat" | "task";
316
352
  /**
@@ -345,6 +381,8 @@ export class SessionEngine {
345
381
  readonly askDedupe: SlackAskDedupeStore;
346
382
  /** Cross-process turn ownership under `<stateRoot>/turn-leases`. */
347
383
  readonly turnLeases: TurnLeaseStore;
384
+ /** Messages for turns another server owns, under `<stateRoot>/outbox`. */
385
+ readonly outbox: TurnOutbox<AdmitTurnOptions>;
348
386
  /**
349
387
  * Runtime for `agent/storage.ts` (`defineStorage`), when
350
388
  * authored. All durable state changes funnel through it (session
@@ -369,6 +407,13 @@ export class SessionEngine {
369
407
  /** Sessions with a turn being admitted (before the run promise exists). */
370
408
  private readonly pendingTurns = new Set<string>();
371
409
  private readonly activeTurns = new Map<string, Promise<TurnOutcome>>();
410
+ /**
411
+ * Sessions whose workspace a turn in this process has materialized. A
412
+ * session is busy from admit — before the governor slot and the turn's
413
+ * own materialize — so a read running alongside a turn consults this,
414
+ * not busy, to know whether the workspace is ready to be left alone.
415
+ */
416
+ private readonly materializedWorkspaces = new Set<string>();
372
417
  /** Context used by the cloud turn's HTTP MCP callbacks. */
373
418
  private readonly activeToolContexts = new Map<string, ActiveToolContext>();
374
419
  /**
@@ -409,6 +454,11 @@ export class SessionEngine {
409
454
  private readonly mcpHost: McpHost;
410
455
  private readonly platforms: HostPlatforms;
411
456
  private readonly approvals = new ApprovalGate();
457
+ private readonly auths = new ApprovalGate<PendingAuth>();
458
+ private readonly oauthHub = new McpOauthOnDemandHub();
459
+ private probeCursorAccount:
460
+ | ((connection: string) => Promise<string | undefined>)
461
+ | undefined;
412
462
  private readonly abCollector: ABCollector;
413
463
  private readonly otel: OtelCollector | undefined;
414
464
  private readonly otelHost: HostOtelSessionApi;
@@ -457,6 +507,7 @@ export class SessionEngine {
457
507
  this.turnLeases = new TurnLeaseStore(
458
508
  join(options.stateRoot, "turn-leases")
459
509
  );
510
+ this.outbox = new TurnOutbox(join(options.stateRoot, "outbox"));
460
511
  this.logs = new EventLogStore(
461
512
  (sessionId) => this.sessions.eventFilePath(sessionId),
462
513
  (sessionId, error) =>
@@ -564,10 +615,25 @@ export class SessionEngine {
564
615
  this.cloudServerToolsConnection = connection;
565
616
  }
566
617
 
618
+ setMcpOauthCallbackUrl(url: string): void {
619
+ this.oauthHub.callbackUrl = url;
620
+ this.mcpHost.setOAuthRedirectUrl(url);
621
+ }
622
+
623
+ setCursorAccountAuthProbe(
624
+ probe: (connection: string) => Promise<string | undefined>
625
+ ): void {
626
+ this.probeCursorAccount = probe;
627
+ }
628
+
629
+ get oauthCallbackHub(): McpOauthOnDemandHub {
630
+ return this.oauthHub;
631
+ }
632
+
567
633
  /** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / files / otel / Slack nudges). */
568
634
  get host(): HostContext {
569
635
  return {
570
- mcp: this.mcpHost,
636
+ mcp: this.wrapMcp(this.mcpHost),
571
637
  github: this.platforms.github,
572
638
  slack: this.platforms.slack,
573
639
  reminders: this.reminderApi,
@@ -698,6 +764,9 @@ export class SessionEngine {
698
764
  ) {
699
765
  return;
700
766
  }
767
+ if (steeringEnabled()) {
768
+ return;
769
+ }
701
770
  try {
702
771
  await materializeSlackNudgeCarrier({
703
772
  harnessCwd,
@@ -959,7 +1028,7 @@ export class SessionEngine {
959
1028
  channelId,
960
1029
  mode: sendOptions.mode ?? "chat",
961
1030
  purpose,
962
- ...(sendOptions.dryRun === true ? { dryRun: true } : {}),
1031
+ dryRun: sendOptions.dryRun === true ? true : undefined,
963
1032
  continuationKey: sendOptions.continuationToken ?? newContinuationKey(),
964
1033
  auth,
965
1034
  title: sendOptions.title,
@@ -977,7 +1046,10 @@ export class SessionEngine {
977
1046
  this.storage?.sessionRecord(record);
978
1047
  await this.appendEvent(record.sessionId, {
979
1048
  type: "session.started",
980
- data: { channelId },
1049
+ data: {
1050
+ channelId,
1051
+ dryRun: record.dryRun === true ? true : undefined,
1052
+ },
981
1053
  });
982
1054
  record = await this.enrollABs(record);
983
1055
  const since = (await this.logs.get(record.sessionId)).count;
@@ -1227,7 +1299,14 @@ export class SessionEngine {
1227
1299
  const prior = this.admissionChains.get(sessionId) ?? Promise.resolve();
1228
1300
  const admitted = prior
1229
1301
  .catch(() => {})
1230
- .then(() => this.admitTurn(sessionId, message, options));
1302
+ .then(async () => {
1303
+ // Another server holds the turn: hand the message to it and coalesce.
1304
+ if (await this.turnLeases.runningElsewhere(sessionId)) {
1305
+ await this.outbox.post(sessionId, { message, options });
1306
+ return true;
1307
+ }
1308
+ return this.admitTurn(sessionId, message, options);
1309
+ });
1231
1310
  this.admissionChains.set(
1232
1311
  sessionId,
1233
1312
  admitted.catch(() => {})
@@ -1235,6 +1314,20 @@ export class SessionEngine {
1235
1314
  return admitted;
1236
1315
  }
1237
1316
 
1317
+ /** Admit what other servers handed to this turn's holder. Never awaited by the lifecycle. */
1318
+ private drainOutbox(sessionId: string): void {
1319
+ void this.outbox
1320
+ .takeAll(sessionId)
1321
+ .then((items) => {
1322
+ for (const item of items) {
1323
+ void this.startTurn(sessionId, item.message, item.options).catch(
1324
+ () => {}
1325
+ );
1326
+ }
1327
+ })
1328
+ .catch(() => {});
1329
+ }
1330
+
1238
1331
  private enqueueCoalesced(sessionId: string, item: CoalescedQueuedTurn): void {
1239
1332
  const queue = this.coalesceQueues.get(sessionId) ?? [];
1240
1333
  if (
@@ -1359,18 +1452,52 @@ export class SessionEngine {
1359
1452
  });
1360
1453
  }
1361
1454
 
1455
+ private async steerIntoRunningTurn(
1456
+ sessionId: string,
1457
+ message: string
1458
+ ): Promise<SteerAckOutcome> {
1459
+ if (this.runner.steer === undefined) {
1460
+ return "revert_to_followup";
1461
+ }
1462
+ try {
1463
+ return await this.runner.steer({ sessionId, text: message });
1464
+ } catch (error) {
1465
+ this.logger(
1466
+ `[session] steer failed for ${sessionId}: ${describeError(error)}`
1467
+ );
1468
+ return "revert_to_followup";
1469
+ }
1470
+ }
1471
+
1362
1472
  private async admitTurn(
1363
1473
  sessionId: string,
1364
1474
  message: string,
1365
1475
  options: AdmitTurnOptions = {}
1366
1476
  ): Promise<boolean> {
1367
1477
  const admission = options.admission ?? "preempt";
1478
+ if (
1479
+ admission === "coalesce" &&
1480
+ steeringEnabled() &&
1481
+ this.isTurnRunning(sessionId)
1482
+ ) {
1483
+ const toolCtx = this.activeToolContexts.get(sessionId);
1484
+ const steerOutcome = await this.steerIntoRunningTurn(sessionId, message);
1485
+ if (steerOutcome === "complete_delivered") {
1486
+ this.logger(`[session] steered into running turn ${sessionId}`);
1487
+ if (toolCtx !== undefined) {
1488
+ toolCtx.emit({ type: "message.received", data: { text: message } });
1489
+ toolCtx.emit({ type: "nudge.delivered", data: {} });
1490
+ }
1491
+ return true;
1492
+ }
1493
+ }
1494
+ // Re-checked after the steer: a turn that ended during it falls through
1495
+ // and this message starts the next turn itself. Enqueueing here instead
1496
+ // would orphan it — the completion drain already ran.
1368
1497
  if (admission === "coalesce" && this.isTurnRunning(sessionId)) {
1369
1498
  this.enqueueCoalesced(sessionId, {
1370
1499
  message,
1371
- ...(options.sourceTs === undefined
1372
- ? {}
1373
- : { sourceTs: options.sourceTs }),
1500
+ sourceTs: options.sourceTs,
1374
1501
  });
1375
1502
  this.logger(`[session] coalesce enqueue ${sessionId}`);
1376
1503
  return true;
@@ -1438,6 +1565,8 @@ export class SessionEngine {
1438
1565
  let lifecycle!: Promise<TurnOutcome>;
1439
1566
  lifecycle = (async (): Promise<TurnOutcome> => {
1440
1567
  let outcome: TurnOutcome;
1568
+ const poll = setInterval(() => this.drainOutbox(sessionId), 1_000);
1569
+ poll.unref();
1441
1570
  try {
1442
1571
  outcome = await this.executeTurn(
1443
1572
  record,
@@ -1451,6 +1580,8 @@ export class SessionEngine {
1451
1580
  status: "error",
1452
1581
  errorMessage: describeError(error),
1453
1582
  };
1583
+ } finally {
1584
+ clearInterval(poll);
1454
1585
  }
1455
1586
  this.lastOutcomes.set(sessionId, outcome);
1456
1587
  const wasCurrent = this.activeTurns.get(sessionId) === lifecycle;
@@ -1480,6 +1611,10 @@ export class SessionEngine {
1480
1611
  // The drain may have nested a successor turn, which took its own
1481
1612
  // reference; the lease outlives this release until that one ends too.
1482
1613
  await lease.release();
1614
+ // Handed off between the last poll and the release: nobody else will.
1615
+ if (!abort.signal.aborted) {
1616
+ this.drainOutbox(sessionId);
1617
+ }
1483
1618
  return outcome;
1484
1619
  })();
1485
1620
  this.activeTurns.set(sessionId, lifecycle);
@@ -1524,6 +1659,7 @@ export class SessionEngine {
1524
1659
  this.coalesceDrains.has(sessionId) ||
1525
1660
  (this.coalesceQueues.get(sessionId)?.length ?? 0) > 0;
1526
1661
  this.clearCoalesced(sessionId);
1662
+ await this.outbox.clear(sessionId);
1527
1663
  const record = await this.sessions.get(sessionId);
1528
1664
  const continuationKey = record?.continuationKey;
1529
1665
  if (continuationKey !== undefined && continuationKey !== null) {
@@ -1757,6 +1893,7 @@ export class SessionEngine {
1757
1893
  seed: isFirstTurn,
1758
1894
  stateRoot: this.stateRoot,
1759
1895
  });
1896
+ this.materializedWorkspaces.add(sessionId);
1760
1897
  // Must precede the carrier: the harness resolves rules, skills,
1761
1898
  // `mcp.json` and hooks from its git root, so a workspace nested in an
1762
1899
  // outer repo would load that repo's hooks instead of the ones written
@@ -1841,11 +1978,14 @@ export class SessionEngine {
1841
1978
  }
1842
1979
  } catch (error) {
1843
1980
  this.approvals.cancelSession(sessionId, "deny");
1981
+ this.auths.cancelSession(sessionId, "deny");
1844
1982
  outcome = { status: "error", errorMessage: describeError(error) };
1845
1983
  } finally {
1846
1984
  permit.release();
1847
1985
  this.approvals.cancelSession(sessionId, "deny");
1986
+ this.auths.cancelSession(sessionId, "deny");
1848
1987
  await this.clearPendingApprovals(sessionId);
1988
+ await this.clearPendingAuths(sessionId);
1849
1989
  // Anything still queued belongs to the next turn's boundary drain, not
1850
1990
  // to this turn's edge detection.
1851
1991
  this.deliveredNudges.delete(sessionId);
@@ -2480,11 +2620,21 @@ export class SessionEngine {
2480
2620
  // named passthroughs resolved fresh on every turn — reminder fires and
2481
2621
  // post-restart follow-ups included — and a listing failure throws,
2482
2622
  // failing the turn closed.
2483
- for (const tool of await resolveAdvertisedTools({
2484
- agent,
2485
- session: this.sessionInfo(record),
2486
- hostMcp: this.mcpHost,
2487
- })) {
2623
+ for (const tool of await mcpAuthAls.run(
2624
+ {
2625
+ sessionId: record.sessionId,
2626
+ turnId,
2627
+ callId: newToolCallId("mcp"),
2628
+ toolName: "mcp",
2629
+ emit,
2630
+ },
2631
+ () =>
2632
+ resolveAdvertisedTools({
2633
+ agent,
2634
+ session: this.sessionInfo(record),
2635
+ hostMcp: this.advertiseMcp(),
2636
+ })
2637
+ )) {
2488
2638
  register(tool);
2489
2639
  }
2490
2640
  return tools;
@@ -2634,7 +2784,16 @@ export class SessionEngine {
2634
2784
  }
2635
2785
  const started = Date.now();
2636
2786
  this.logger(`[session] tool ${args.tool.name} start`);
2637
- const result = await execute(validation.value as never, toolCtx);
2787
+ const result = await mcpAuthAls.run(
2788
+ {
2789
+ sessionId: args.record.sessionId,
2790
+ turnId: args.turnId,
2791
+ callId: toolCallId,
2792
+ toolName: args.tool.name,
2793
+ emit: args.emit,
2794
+ },
2795
+ () => execute(validation.value as never, toolCtx)
2796
+ );
2638
2797
  this.logger(
2639
2798
  `[session] tool ${args.tool.name} done (${Date.now() - started}ms)`
2640
2799
  );
@@ -2670,10 +2829,10 @@ export class SessionEngine {
2670
2829
  args: args.args,
2671
2830
  requestedAt,
2672
2831
  };
2673
- const resolutionPromise = this.approvals.wait({
2674
- sessionId: args.sessionId,
2675
- ...pendingRecord,
2676
- });
2832
+ const resolutionPromise = this.approvals.wait(
2833
+ args.sessionId,
2834
+ pendingRecord
2835
+ );
2677
2836
  await this.updateRecord(args.sessionId, (current) => ({
2678
2837
  ...current,
2679
2838
  pendingApprovals: [
@@ -2793,6 +2952,318 @@ export class SessionEngine {
2793
2952
  }));
2794
2953
  }
2795
2954
 
2955
+ private wrapMcp(inner: McpHost): HostMcpRegistry {
2956
+ return {
2957
+ names: () => inner.names(),
2958
+ listTools: (name) => this.withMcpAuth(name, () => inner.listTools(name)),
2959
+ callTool: (name, tool, args) =>
2960
+ this.withMcpAuth(name, () => inner.callTool(name, tool, args)),
2961
+ };
2962
+ }
2963
+
2964
+ private advertiseMcp(): AdvertiseHostMcp {
2965
+ return {
2966
+ listTools: (name) =>
2967
+ this.withMcpListAuth(name, () => this.mcpHost.listTools(name)),
2968
+ callTool: (name, tool, args) =>
2969
+ this.withMcpAuth(name, () => this.mcpHost.callTool(name, tool, args)),
2970
+ oneOff: (connection) => {
2971
+ const inner = this.mcpHost.oneOff(connection);
2972
+ return {
2973
+ listTools: () =>
2974
+ this.withMcpListAuth(connection.name, () => inner.listTools()),
2975
+ callTool: (tool, args) =>
2976
+ this.withMcpAuth(connection.name, () => inner.callTool(tool, args)),
2977
+ };
2978
+ },
2979
+ };
2980
+ }
2981
+
2982
+ private withMcpListAuth<T>(
2983
+ connection: string,
2984
+ run: () => Promise<T>
2985
+ ): Promise<T> {
2986
+ const ctx = mcpAuthAls.getStore();
2987
+ if (ctx === undefined) {
2988
+ return this.withMcpAuth(connection, run);
2989
+ }
2990
+ const toolName = `mcp:${connection}`;
2991
+ return mcpAuthAls.run(
2992
+ {
2993
+ ...ctx,
2994
+ callId: newToolCallId(toolName),
2995
+ toolName,
2996
+ },
2997
+ () => this.withMcpAuth(connection, run)
2998
+ );
2999
+ }
3000
+
3001
+ private async withMcpAuth<T>(
3002
+ connection: string,
3003
+ run: () => Promise<T>
3004
+ ): Promise<T> {
3005
+ try {
3006
+ return await run();
3007
+ } catch (error) {
3008
+ if (!isMcpNeedsAuthError(error)) {
3009
+ throw error;
3010
+ }
3011
+ return await this.parkAndRetryMcp(connection, run, error);
3012
+ }
3013
+ }
3014
+
3015
+ private async parkAndRetryMcp<T>(
3016
+ connection: string,
3017
+ run: () => Promise<T>,
3018
+ error: unknown
3019
+ ): Promise<T> {
3020
+ const ctx = mcpAuthAls.getStore();
3021
+ if (ctx === undefined) {
3022
+ throw error;
3023
+ }
3024
+ const minted = await this.mintAuthUrl(connection, ctx.sessionId);
3025
+ if (minted === undefined) {
3026
+ throw error;
3027
+ }
3028
+ if ("authorized" in minted) {
3029
+ await this.persistHostedMcpOauth(connection);
3030
+ await this.mcpHost.dropClient(connection);
3031
+ return await this.withMcpAuth(connection, run);
3032
+ }
3033
+ if (minted.state !== "") {
3034
+ const bind = this.oauthHub.bind(minted.state, ctx.sessionId, ctx.callId);
3035
+ if (bind === "already_done") {
3036
+ await this.persistHostedMcpOauth(connection);
3037
+ await this.mcpHost.dropClient(connection);
3038
+ return await this.withMcpAuth(connection, run);
3039
+ }
3040
+ }
3041
+ const decision = await this.parkForAuth({
3042
+ sessionId: ctx.sessionId,
3043
+ turnId: ctx.turnId,
3044
+ callId: ctx.callId,
3045
+ toolName: ctx.toolName,
3046
+ connection,
3047
+ authUrl: minted.authUrl,
3048
+ emit: ctx.emit,
3049
+ });
3050
+ if (decision === "deny") {
3051
+ throw new Error(
3052
+ `MCP connection "${connection}" authorization was denied.`
3053
+ );
3054
+ }
3055
+ await this.persistHostedMcpOauth(connection);
3056
+ await this.mcpHost.dropClient(connection);
3057
+ return await this.withMcpAuth(connection, run);
3058
+ }
3059
+
3060
+ private async persistHostedMcpOauth(connection: string): Promise<void> {
3061
+ const authored = this.authoredConnection(connection);
3062
+ if (
3063
+ authored === undefined ||
3064
+ !isHostOauthConnectionTransport(authored.transport)
3065
+ ) {
3066
+ return;
3067
+ }
3068
+ await this.oauthHub.persistFor(connection, authored.transport.url);
3069
+ }
3070
+
3071
+ private authoredConnection(name: string) {
3072
+ return (
3073
+ this.project.agent.connections.find((entry) => entry.name === name) ??
3074
+ this.project.agent.hostConnections?.find((entry) => entry.name === name)
3075
+ );
3076
+ }
3077
+
3078
+ private async mintAuthUrl(
3079
+ connection: string,
3080
+ sessionId: string
3081
+ ): Promise<
3082
+ { authorized: true } | { authUrl: string; state: string } | undefined
3083
+ > {
3084
+ const authored = this.authoredConnection(connection);
3085
+ if (authored === undefined) {
3086
+ return undefined;
3087
+ }
3088
+ if (isCursorAccountTransport(authored.transport)) {
3089
+ const probe = this.probeCursorAccount;
3090
+ if (probe === undefined) {
3091
+ return undefined;
3092
+ }
3093
+ const authUrl = await probe(connection);
3094
+ if (authUrl === undefined || authUrl === "") {
3095
+ return undefined;
3096
+ }
3097
+ return { authUrl, state: "" };
3098
+ }
3099
+ if (!isHostOauthConnectionTransport(authored.transport)) {
3100
+ return undefined;
3101
+ }
3102
+ return await this.oauthHub.mint({
3103
+ connection,
3104
+ url: authored.transport.url,
3105
+ type: authored.transport.type === "sse" ? "sse" : "http",
3106
+ headers: authored.transport.headers,
3107
+ sessionId,
3108
+ });
3109
+ }
3110
+
3111
+ private async parkForAuth(args: {
3112
+ sessionId: string;
3113
+ turnId: string;
3114
+ callId: string;
3115
+ toolName: string;
3116
+ connection: string;
3117
+ authUrl: string;
3118
+ emit: (payload: SessionEventPayload) => void;
3119
+ }): Promise<ApprovalDecision> {
3120
+ const requestedAt = new Date().toISOString();
3121
+ const pendingRecord: PendingAuth = {
3122
+ callId: args.callId,
3123
+ turnId: args.turnId,
3124
+ toolName: args.toolName,
3125
+ connection: args.connection,
3126
+ authUrl: args.authUrl,
3127
+ requestedAt,
3128
+ };
3129
+ const resolutionPromise = this.auths.wait(args.sessionId, pendingRecord);
3130
+ await this.updateRecord(args.sessionId, (current) => ({
3131
+ ...current,
3132
+ pendingAuths: [
3133
+ ...(current.pendingAuths ?? []).filter(
3134
+ (item) => item.callId !== args.callId
3135
+ ),
3136
+ pendingRecord,
3137
+ ],
3138
+ }));
3139
+ args.emit({
3140
+ type: "action.auth_requested",
3141
+ data: {
3142
+ callId: args.callId,
3143
+ toolName: args.toolName,
3144
+ connection: args.connection,
3145
+ authUrl: args.authUrl,
3146
+ },
3147
+ });
3148
+ this.logger(
3149
+ `[session] tool ${args.toolName} awaiting MCP auth ${args.connection} (${args.callId})`
3150
+ );
3151
+ const { decision, by } = await resolutionPromise;
3152
+ await this.updateRecord(args.sessionId, (current) => ({
3153
+ ...current,
3154
+ pendingAuths: (current.pendingAuths ?? []).filter(
3155
+ (item) => item.callId !== args.callId
3156
+ ),
3157
+ }));
3158
+ args.emit({
3159
+ type: "action.auth_resolved",
3160
+ data: {
3161
+ callId: args.callId,
3162
+ toolName: args.toolName,
3163
+ connection: args.connection,
3164
+ decision,
3165
+ by,
3166
+ },
3167
+ });
3168
+ this.logger(
3169
+ `[session] tool ${args.toolName} MCP auth ${decision} (${args.callId})`
3170
+ );
3171
+ return decision;
3172
+ }
3173
+
3174
+ private async clearPendingAuths(sessionId: string): Promise<void> {
3175
+ await this.oauthHub.abortSession(sessionId);
3176
+ const current = await this.sessions.get(sessionId);
3177
+ if (
3178
+ current === undefined ||
3179
+ current.pendingAuths === undefined ||
3180
+ current.pendingAuths.length === 0
3181
+ ) {
3182
+ return;
3183
+ }
3184
+ await this.updateRecord(sessionId, (record) => ({
3185
+ ...record,
3186
+ pendingAuths: [],
3187
+ }));
3188
+ }
3189
+
3190
+ async listPendingAuths(sessionId: string): Promise<PendingAuth[]> {
3191
+ const record = await this.sessions.get(sessionId);
3192
+ if (record === undefined) {
3193
+ return [];
3194
+ }
3195
+ const live = this.auths.list(sessionId);
3196
+ if (live.length > 0) {
3197
+ return live;
3198
+ }
3199
+ return record.pendingAuths ?? [];
3200
+ }
3201
+
3202
+ async resolveAuth(
3203
+ sessionId: string,
3204
+ callId: string,
3205
+ decision: ApprovalDecision,
3206
+ auth: AuthContext | null,
3207
+ options?: { asSessionOwner?: boolean }
3208
+ ): Promise<PendingAuth> {
3209
+ const record = await this.sessions.get(sessionId);
3210
+ if (record === undefined) {
3211
+ throw new AuthNotFoundError(sessionId, callId);
3212
+ }
3213
+ const ownershipAuth = options?.asSessionOwner === true ? record.auth : auth;
3214
+ if (!samePrincipal(record.auth, ownershipAuth)) {
3215
+ throw new SessionAuthError(sessionId);
3216
+ }
3217
+ const pending =
3218
+ (record.pendingAuths ?? []).find((item) => item.callId === callId) ??
3219
+ this.auths.list(sessionId).find((item) => item.callId === callId);
3220
+ if (pending === undefined) {
3221
+ throw new AuthNotFoundError(sessionId, callId);
3222
+ }
3223
+ const resolvedBy = approvalResolverLabel(auth);
3224
+ if (decision === "deny") {
3225
+ await this.oauthHub.abortCall(sessionId, callId);
3226
+ }
3227
+ if (!this.auths.resolve(sessionId, callId, decision, resolvedBy)) {
3228
+ await this.removePendingAuth(sessionId, callId);
3229
+ throw new AuthInterruptedError(sessionId, callId);
3230
+ }
3231
+ return pending;
3232
+ }
3233
+
3234
+ private async removePendingAuth(
3235
+ sessionId: string,
3236
+ callId: string
3237
+ ): Promise<void> {
3238
+ await this.updateRecord(sessionId, (current) => ({
3239
+ ...current,
3240
+ pendingAuths: (current.pendingAuths ?? []).filter(
3241
+ (item) => item.callId !== callId
3242
+ ),
3243
+ }));
3244
+ }
3245
+
3246
+ async runWithMcpPark<T>(
3247
+ sessionId: string,
3248
+ toolName: string,
3249
+ fn: () => Promise<T>
3250
+ ): Promise<T> {
3251
+ const active = this.activeToolContexts.get(sessionId);
3252
+ if (active === undefined) {
3253
+ return fn();
3254
+ }
3255
+ return mcpAuthAls.run(
3256
+ {
3257
+ sessionId,
3258
+ turnId: active.turnId,
3259
+ callId: newToolCallId(toolName),
3260
+ toolName,
3261
+ emit: active.emit,
3262
+ },
3263
+ fn
3264
+ );
3265
+ }
3266
+
2796
3267
  // ==========================================================================
2797
3268
  // Direct tool calls (deterministic invocation, no model turn)
2798
3269
  // ==========================================================================
@@ -2808,8 +3279,10 @@ export class SessionEngine {
2808
3279
  * and materialized workspace) and the call is recorded on the session's
2809
3280
  * event stream as `actions.requested` / `action.result` under a
2810
3281
  * per-call `turnId` (the call id), so it shows up in trajectories.
2811
- * Rejected with {@link SessionBusyError} while a model turn is running.
2812
- * Without a session, the tool runs against an **ephemeral** scratch
3282
+ * While a model turn is running, a read-effect call runs alongside it
3283
+ * (the turn's workspace is read as-is) and a write-effect call is
3284
+ * rejected with {@link SessionBusyError}. Without a session, the tool
3285
+ * runs against an **ephemeral** scratch
2813
3286
  * workspace under `<stateRoot>/tool-calls/<callId>` (materialized with
2814
3287
  * seed files, removed after the call returns).
2815
3288
  *
@@ -2835,6 +3308,12 @@ export class SessionEngine {
2835
3308
  session: SessionInfo;
2836
3309
  workspaceDir: string;
2837
3310
  scratchDir: string | undefined;
3311
+ /**
3312
+ * A model turn is running or queued on the session. Reported, not
3313
+ * enforced: what it means depends on the call — a write waits, a read
3314
+ * (including the catalog) runs alongside the turn.
3315
+ */
3316
+ busy: boolean;
2838
3317
  }> {
2839
3318
  if (
2840
3319
  options.sessionId !== undefined &&
@@ -2860,9 +3339,9 @@ export class SessionEngine {
2860
3339
  );
2861
3340
  }
2862
3341
  // A token that maps to a live (or restorable) session takes the
2863
- // session path: same owner check, same turn serialization, same
2864
- // event recording as an explicit sessionId. Skipping those just
2865
- // because the caller addressed the session by token would fail open.
3342
+ // session path: same owner check, same busy semantics, same event
3343
+ // recording as an explicit sessionId. Skipping those just because
3344
+ // the caller addressed the session by token would fail open.
2866
3345
  record =
2867
3346
  (await this.sessions.findByContinuation(
2868
3347
  continuation.channelId,
@@ -2877,19 +3356,14 @@ export class SessionEngine {
2877
3356
  }
2878
3357
  }
2879
3358
  if (record !== undefined) {
2880
- // Direct calls respect the same serialization as follow-up sends: a
2881
- // running model turn owns the session workspace and event stream.
2882
- if (
2883
- this.activeTurns.has(record.sessionId) ||
2884
- this.pendingTurns.has(record.sessionId)
2885
- ) {
2886
- throw new SessionBusyError(record.sessionId);
2887
- }
2888
3359
  return {
2889
3360
  record,
2890
3361
  session: this.sessionInfo(record),
2891
3362
  workspaceDir: this.harnessCwd(record),
2892
3363
  scratchDir: undefined,
3364
+ busy:
3365
+ this.activeTurns.has(record.sessionId) ||
3366
+ this.pendingTurns.has(record.sessionId),
2893
3367
  };
2894
3368
  }
2895
3369
  // No session behind the token (or no token at all): a synthetic
@@ -2909,6 +3383,7 @@ export class SessionEngine {
2909
3383
  },
2910
3384
  workspaceDir: scratchDir,
2911
3385
  scratchDir,
3386
+ busy: false,
2912
3387
  };
2913
3388
  }
2914
3389
 
@@ -2951,6 +3426,43 @@ export class SessionEngine {
2951
3426
  };
2952
3427
  }
2953
3428
 
3429
+ /**
3430
+ * One catalog tool by name. Authored tools are local — their schema does
3431
+ * not depend on session identity or connection listings — so a detail
3432
+ * fetch for `ls` must not wait on every advertised MCP server.
3433
+ */
3434
+ async describeCatalogTool(
3435
+ toolName: string,
3436
+ options: Pick<
3437
+ CallToolOptions,
3438
+ "auth" | "sessionId" | "continuationToken"
3439
+ > = {}
3440
+ ): Promise<
3441
+ | { tool: DiscoveredTool }
3442
+ | {
3443
+ tool: undefined;
3444
+ availableTools: string[];
3445
+ connectionErrors: Array<{ connection: string; message: string }>;
3446
+ }
3447
+ > {
3448
+ const authored = this.project.agent.tools.find(
3449
+ (tool) => tool.name === toolName
3450
+ );
3451
+ if (authored !== undefined) {
3452
+ return { tool: authored };
3453
+ }
3454
+ const catalog = await this.listCatalogTools(options);
3455
+ const tool = catalog.tools.find((entry) => entry.name === toolName);
3456
+ if (tool === undefined) {
3457
+ return {
3458
+ tool: undefined,
3459
+ availableTools: catalog.tools.map((entry) => entry.name),
3460
+ connectionErrors: catalog.connectionErrors,
3461
+ };
3462
+ }
3463
+ return { tool };
3464
+ }
3465
+
2954
3466
  async callTool(
2955
3467
  toolName: string,
2956
3468
  input: unknown = {},
@@ -2977,7 +3489,7 @@ export class SessionEngine {
2977
3489
  const advertised = await resolveAdvertisedTools({
2978
3490
  agent: this.project.agent,
2979
3491
  session,
2980
- hostMcp: this.mcpHost,
3492
+ hostMcp: this.advertiseMcp(),
2981
3493
  });
2982
3494
  tool = advertised.find((t) => t.name === toolName);
2983
3495
  }
@@ -2997,6 +3509,19 @@ export class SessionEngine {
2997
3509
  if (!validation.ok) {
2998
3510
  throw new ToolInputError({ toolName, detail: validation.message });
2999
3511
  }
3512
+ // A running model turn owns the session workspace: a direct write would
3513
+ // race the harness's own edits, so it waits (409). A read runs alongside
3514
+ // the turn — the operator inspecting a live session is the point — and
3515
+ // its events stay separable by their per-call turnId. The effect can
3516
+ // depend on the input, hence deciding after validation; an MCP tool
3517
+ // nothing classifies is a write here, fail-closed.
3518
+ if (
3519
+ identity.busy &&
3520
+ record !== undefined &&
3521
+ directCallEffect(tool, validation.value) !== "read"
3522
+ ) {
3523
+ throw new SessionBusyError(record.sessionId);
3524
+ }
3000
3525
  let workspaceUnavailable = false;
3001
3526
  try {
3002
3527
  await mkdir(workspaceDir, { recursive: true });
@@ -3015,35 +3540,59 @@ export class SessionEngine {
3015
3540
  // to write) authored tool may rely on ctx.workspaceDir being the
3016
3541
  // real session workspace, so silently relocating it is not safe —
3017
3542
  // fail typed instead.
3543
+ //
3544
+ // A shared `local.workspaceDir` (change-monitor's phantom /repo) is
3545
+ // that path by design. Relocating it to scratch would flag a false
3546
+ // session-state warning; keep the borrowed cwd and skip the flag.
3018
3547
  const relocatable =
3019
3548
  tool.advertised === true ||
3020
- resolveEffect(tool.definition.effect, validation.value) === "read";
3549
+ directCallEffect(tool, validation.value) === "read";
3021
3550
  if (!relocatable) {
3022
3551
  throw new WorkspaceUnavailableError(toolName, error);
3023
3552
  }
3024
- this.logger(
3025
- `[session] tool ${toolName} workspace unavailable (direct ${callId}); using scratch: ${
3026
- error instanceof Error ? error.message : String(error)
3027
- }`
3028
- );
3029
- scratchDir = join(this.stateRoot, "tool-calls", callId);
3030
- workspaceDir = scratchDir;
3031
- workspaceUnavailable = true;
3032
- await mkdir(workspaceDir, { recursive: true });
3553
+ if (this.project.agent.local?.workspaceDir !== undefined) {
3554
+ this.logger(
3555
+ `[session] tool ${toolName} harness cwd unavailable (direct ${callId}); keeping borrowed workspace`
3556
+ );
3557
+ } else {
3558
+ this.logger(
3559
+ `[session] tool ${toolName} workspace unavailable (direct ${callId}); using scratch: ${
3560
+ error instanceof Error ? error.message : String(error)
3561
+ }`
3562
+ );
3563
+ scratchDir = join(this.stateRoot, "tool-calls", callId);
3564
+ workspaceDir = scratchDir;
3565
+ workspaceUnavailable = true;
3566
+ await mkdir(workspaceDir, { recursive: true });
3567
+ }
3033
3568
  }
3034
3569
  // Match what a model turn would see: session workspaces get the same
3035
3570
  // scaffold as executeTurn (seeds only before the first turn); scratch
3036
- // workspaces are materialized fresh.
3037
- await materializeWorkspace({
3038
- agent: this.project.agent,
3039
- workspaceDir:
3040
- record === undefined || workspaceUnavailable
3041
- ? workspaceDir
3042
- : this.scaffoldDir(record),
3043
- seed:
3044
- record === undefined || workspaceUnavailable || record.turnCount === 0,
3045
- stateRoot: this.stateRoot,
3046
- });
3571
+ // workspaces are materialized fresh. A read running alongside a turn
3572
+ // that has materialized its workspace leaves it alone — re-seeding
3573
+ // would restore files the agent deleted mid-turn. A session is busy
3574
+ // from admit, though, so a read that lands while the first turn is
3575
+ // still queued or scaffolding materializes the workspace itself, seeds
3576
+ // included (turnCount is already 1 for that pending first turn).
3577
+ const turnMaterialized =
3578
+ record !== undefined &&
3579
+ identity.busy &&
3580
+ this.materializedWorkspaces.has(record.sessionId);
3581
+ if (record === undefined || workspaceUnavailable || !turnMaterialized) {
3582
+ await materializeWorkspace({
3583
+ agent: this.project.agent,
3584
+ workspaceDir:
3585
+ record === undefined || workspaceUnavailable
3586
+ ? workspaceDir
3587
+ : this.scaffoldDir(record),
3588
+ seed:
3589
+ record === undefined ||
3590
+ workspaceUnavailable ||
3591
+ record.turnCount === 0 ||
3592
+ (identity.busy && record.turnCount === 1),
3593
+ stateRoot: this.stateRoot,
3594
+ });
3595
+ }
3047
3596
 
3048
3597
  // A per-call turnId keeps these events visible in trajectories (which
3049
3598
  // drop turn-less action events) without attributing them to a model turn.
@@ -3101,8 +3650,8 @@ export class SessionEngine {
3101
3650
  result = normalizeToolResult(decision.result);
3102
3651
  isError = false;
3103
3652
  } else {
3104
- result = normalizeToolResult(
3105
- await execute(validation.value as never, {
3653
+ const run = () =>
3654
+ execute(validation.value as never, {
3106
3655
  toolCallId: callId,
3107
3656
  session,
3108
3657
  workspaceDir,
@@ -3127,7 +3676,20 @@ export class SessionEngine {
3127
3676
  getSession: (channelId, sessionId) =>
3128
3677
  this.getChannelSession(channelId, sessionId),
3129
3678
  emit,
3130
- })
3679
+ });
3680
+ result = normalizeToolResult(
3681
+ record !== undefined && emit !== undefined
3682
+ ? await mcpAuthAls.run(
3683
+ {
3684
+ sessionId: record.sessionId,
3685
+ turnId: callId,
3686
+ callId,
3687
+ toolName,
3688
+ emit,
3689
+ },
3690
+ run
3691
+ )
3692
+ : await run()
3131
3693
  );
3132
3694
  isError = isErrorEnvelope(result);
3133
3695
  }
@@ -3394,6 +3956,28 @@ export class SessionEngine {
3394
3956
  // Session handles
3395
3957
  // ==========================================================================
3396
3958
 
3959
+ /**
3960
+ * Wait until everything this session has already emitted is durably
3961
+ * mirrored: its dispatch chain first (the chain's tail runs the turn-end
3962
+ * storage flush, so the record carries the boundary event's handler
3963
+ * mutations), then the storage delivery queue. Hosted v2 awaits this
3964
+ * before the execution reply — the pod can be parked the moment the
3965
+ * reply lands, and an unflushed session record would strand a
3966
+ * conversation whose blobs persisted but whose record (which selects the
3967
+ * conversation to hydrate) never reached the sink. No-op without a
3968
+ * storage mirror.
3969
+ */
3970
+ async drainSessionDurability(sessionId: string): Promise<void> {
3971
+ if (this.storage === undefined) {
3972
+ return;
3973
+ }
3974
+ const chain = this.dispatchChains.get(sessionId);
3975
+ if (chain !== undefined) {
3976
+ await chain.catch(() => {});
3977
+ }
3978
+ await this.storage.whenIdle();
3979
+ }
3980
+
3397
3981
  async waitForCompletion(sessionId: string): Promise<TurnOutcome> {
3398
3982
  // Drain admission first so pendingTurns → activeTurns is visible before we
3399
3983
  // decide the session is idle (reminders use this for queueBehindTurn).
@@ -3525,6 +4109,75 @@ export class SessionEngine {
3525
4109
  };
3526
4110
  }
3527
4111
 
4112
+ /**
4113
+ * The single newest session visible to `caller` (`GET /v1/sessions?latest=1`).
4114
+ *
4115
+ * Short-circuits the full listing: the local store already holds every
4116
+ * session this host created plus the startup bulk restore, so the common
4117
+ * case answers from memory without a sink round trip. Only when the local
4118
+ * store has nothing for the caller (fresh host, `restore: "off"`) does it
4119
+ * ask the sink — for a bounded newest-written window when the backend
4120
+ * can order by write time, else the full list. A session written by
4121
+ * another replica moments ago can lose to a local one; callers that need
4122
+ * the exact newest row should use the full listing.
4123
+ */
4124
+ async latestSessionWithSource(
4125
+ caller: AuthContext | null,
4126
+ options?: { includeAll?: boolean }
4127
+ ): Promise<{ sessions: SessionSummary[]; source: SessionListSource }> {
4128
+ const includeAll = options?.includeAll ?? false;
4129
+ const newestVisible = (
4130
+ records: readonly SessionRecord[]
4131
+ ): SessionRecord | undefined => {
4132
+ let newest: SessionRecord | undefined;
4133
+ for (const record of records) {
4134
+ if (!(includeAll || samePrincipal(record.auth, caller))) {
4135
+ continue;
4136
+ }
4137
+ if (
4138
+ newest === undefined ||
4139
+ record.updatedAt.localeCompare(newest.updatedAt) > 0
4140
+ ) {
4141
+ newest = record;
4142
+ }
4143
+ }
4144
+ return newest;
4145
+ };
4146
+ const local = newestVisible(await this.sessions.list());
4147
+ if (local !== undefined) {
4148
+ return {
4149
+ sessions: this.summarizeSessionRecords([local], caller, includeAll),
4150
+ source: "local",
4151
+ };
4152
+ }
4153
+ if (this.storage?.isCursorHosted === true) {
4154
+ try {
4155
+ const window = await this.storage.listNewestSessionsFromSink(
4156
+ LATEST_SESSION_SINK_WINDOW
4157
+ );
4158
+ if (window !== undefined) {
4159
+ const stored = newestVisible(window);
4160
+ if (stored !== undefined) {
4161
+ return {
4162
+ sessions: this.summarizeSessionRecords(
4163
+ [stored],
4164
+ caller,
4165
+ includeAll
4166
+ ),
4167
+ source: "cursor-hosted",
4168
+ };
4169
+ }
4170
+ }
4171
+ } catch (error) {
4172
+ this.logger(
4173
+ `[agent-sdk] cursor-hosted newest-session lookup failed; falling back to the full list: ${describeError(error)}`
4174
+ );
4175
+ }
4176
+ }
4177
+ const listed = await this.listSessionsWithSource(caller, options);
4178
+ return { sessions: listed.sessions.slice(0, 1), source: listed.source };
4179
+ }
4180
+
3528
4181
  /**
3529
4182
  * Durable sink rows plus local records (local wins on the same id when
3530
4183
  * newer — covers debounce windows that have not flushed yet). Newest
@@ -3578,6 +4231,7 @@ export class SessionEngine {
3578
4231
  : undefined,
3579
4232
  createdAt: record.createdAt,
3580
4233
  updatedAt: record.updatedAt,
4234
+ dryRun: record.dryRun === true ? true : undefined,
3581
4235
  continuationToken:
3582
4236
  record.httpContinuationKey !== undefined
3583
4237
  ? formatContinuationToken(
@@ -3907,7 +4561,7 @@ export class SessionEngine {
3907
4561
  channelId: record.channelId,
3908
4562
  mode: record.mode,
3909
4563
  purpose: record.purpose === "eval" ? "eval" : "live",
3910
- ...(record.dryRun === true ? { dryRun: true } : {}),
4564
+ dryRun: record.dryRun === true ? true : undefined,
3911
4565
  auth: record.auth,
3912
4566
  continuationKey: record.continuationKey,
3913
4567
  ...(record.abs === undefined ? {} : { abs: record.abs }),
@@ -4280,6 +4934,8 @@ export class SessionEngine {
4280
4934
  async close(): Promise<void> {
4281
4935
  this.stopMemoryTelemetry?.();
4282
4936
  this.approvals.cancelAll("deny");
4937
+ this.auths.cancelAll("deny");
4938
+ await this.oauthHub.close();
4283
4939
  await Promise.allSettled([...this.activeTurns.values()]);
4284
4940
  await Promise.allSettled([...this.backgroundWork]);
4285
4941
  await Promise.allSettled([...this.dispatchChains.values()]);
@@ -4297,6 +4953,13 @@ export class SessionEngine {
4297
4953
  /** Parallel event-stream loads during the startup bulk restore. */
4298
4954
  const RESTORE_FETCH_CONCURRENCY = 8;
4299
4955
 
4956
+ /**
4957
+ * Newest-written sink rows fetched for `GET /v1/sessions?latest=1` when the
4958
+ * local store has nothing for the caller. Wide enough that the caller's own
4959
+ * session is normally inside it even when other principals are busier.
4960
+ */
4961
+ const LATEST_SESSION_SINK_WINDOW = 50;
4962
+
4300
4963
  /** NDJSON payload for a restored event stream; undefined when empty. */
4301
4964
  function mergePendingImages(
4302
4965
  current: Array<{ data: string; mimeType: string }> | undefined,