@cursor/july 0.1.96 → 0.1.99

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 (381) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +3 -2
  3. package/dist/bin/agent-serve.js +13 -3
  4. package/dist/channels/github/github-channel.d.ts.map +1 -1
  5. package/dist/channels/github/github-channel.js +16 -0
  6. package/dist/channels/origin/api.d.ts +15 -4
  7. package/dist/channels/origin/api.d.ts.map +1 -1
  8. package/dist/channels/origin/api.js +19 -3
  9. package/dist/channels/origin/binding.d.ts +11 -0
  10. package/dist/channels/origin/binding.d.ts.map +1 -1
  11. package/dist/channels/origin/binding.js +41 -25
  12. package/dist/channels/origin/index.d.ts +1 -0
  13. package/dist/channels/origin/index.d.ts.map +1 -1
  14. package/dist/channels/origin/index.js +1 -0
  15. package/dist/channels/origin/origin-channel.d.ts +2 -0
  16. package/dist/channels/origin/origin-channel.d.ts.map +1 -1
  17. package/dist/channels/origin/origin-channel.js +40 -4
  18. package/dist/channels/origin/reactions.d.ts +56 -0
  19. package/dist/channels/origin/reactions.d.ts.map +1 -0
  20. package/dist/channels/origin/reactions.js +67 -0
  21. package/dist/channels/origin/types.d.ts +11 -0
  22. package/dist/channels/origin/types.d.ts.map +1 -1
  23. package/dist/channels/slack/approvals.d.ts +16 -0
  24. package/dist/channels/slack/approvals.d.ts.map +1 -1
  25. package/dist/channels/slack/approvals.js +86 -0
  26. package/dist/channels/slack/cli-project.d.ts +19 -0
  27. package/dist/channels/slack/cli-project.d.ts.map +1 -0
  28. package/dist/channels/slack/cli-project.js +234 -0
  29. package/dist/channels/slack/defaults.d.ts.map +1 -1
  30. package/dist/channels/slack/defaults.js +142 -63
  31. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  32. package/dist/channels/slack/dispatch.js +1 -1
  33. package/dist/channels/slack/init.d.ts +3 -2
  34. package/dist/channels/slack/init.d.ts.map +1 -1
  35. package/dist/channels/slack/init.js +5 -5
  36. package/dist/channels/slack/interactive.d.ts +2 -1
  37. package/dist/channels/slack/interactive.d.ts.map +1 -1
  38. package/dist/channels/slack/interactive.js +15 -4
  39. package/dist/channels/slack/live-delivery.d.ts +1 -1
  40. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  41. package/dist/channels/slack/live-delivery.js +4 -2
  42. package/dist/channels/slack/manifest.d.ts +1 -1
  43. package/dist/channels/slack/manifest.js +1 -1
  44. package/dist/channels/slack/progress-delivery.d.ts +28 -0
  45. package/dist/channels/slack/progress-delivery.d.ts.map +1 -0
  46. package/dist/channels/slack/progress-delivery.js +115 -0
  47. package/dist/channels/slack/setup.d.ts +1 -1
  48. package/dist/channels/slack/setup.d.ts.map +1 -1
  49. package/dist/channels/slack/setup.js +10 -6
  50. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  51. package/dist/channels/slack/slack-channel.js +1 -0
  52. package/dist/channels/slack/tool-approval-events.d.ts.map +1 -1
  53. package/dist/channels/slack/tool-approval-events.js +91 -1
  54. package/dist/channels/slack/types.d.ts +9 -1
  55. package/dist/channels/slack/types.d.ts.map +1 -1
  56. package/dist/docs/404.html +2 -2
  57. package/dist/docs/ab.html +4 -4
  58. package/dist/docs/assets/{app.jXleCmOc.js → app.BplEAZ3S.js} +1 -1
  59. package/dist/docs/assets/chunks/@localSearchIndexroot.DpuI4i7Z.js +1 -0
  60. package/dist/docs/assets/chunks/{VPLocalSearchBox.8LhyglIT.js → VPLocalSearchBox.CHXyv4wP.js} +1 -1
  61. package/dist/docs/assets/chunks/{theme.9ged-pr2.js → theme.BTcsZmjQ.js} +2 -2
  62. package/dist/docs/assets/{deployment.md.D9msOFOW.js → deployment.md.MFPKqsqI.js} +2 -2
  63. package/dist/docs/assets/{guides_mcp-oauth.md.DPYmBCbV.js → guides_mcp-oauth.md.CN-6YmTJ.js} +4 -4
  64. package/dist/docs/assets/{guides_mcp-oauth.md.DPYmBCbV.lean.js → guides_mcp-oauth.md.CN-6YmTJ.lean.js} +1 -1
  65. package/dist/docs/assets/guides_slack.md.VDXQV3ja.js +22 -0
  66. package/dist/docs/assets/guides_slack.md.VDXQV3ja.lean.js +1 -0
  67. package/dist/docs/assets/index.md.CVeRUOeZ.js +5 -0
  68. package/dist/docs/assets/{reference_agent-config.md.Cp_x38Nl.js → reference_agent-config.md.BHeYMgme.js} +1 -1
  69. package/dist/docs/assets/{reference_cli.md.CCnG-nJx.js → reference_cli.md.DfoeyvL0.js} +9 -6
  70. package/dist/docs/assets/{reference_cli.md.CCnG-nJx.lean.js → reference_cli.md.DfoeyvL0.lean.js} +1 -1
  71. package/dist/docs/assets/{reference_connections.md.BH8Oc0D0.js → reference_connections.md.DxldvyIB.js} +4 -3
  72. package/dist/docs/assets/{reference_http-api.md.D89k1mdm.js → reference_http-api.md.BEJx9XVj.js} +2 -2
  73. package/dist/docs/assets/reference_sessions.md.NxFP_CAC.js +1 -0
  74. package/dist/docs/assets/{reference_tools.md.BHeXn2id.js → reference_tools.md.CvAHsdSp.js} +10 -2
  75. package/dist/docs/assets/{reference_tools.md.BHeXn2id.lean.js → reference_tools.md.CvAHsdSp.lean.js} +1 -1
  76. package/dist/docs/assets/templates_agents-md.md.CXSMZljE.js +2 -0
  77. package/dist/docs/assets/templates_agents-md.md.CXSMZljE.lean.js +1 -0
  78. package/dist/docs/assets/troubleshooting.md.DCiPBhYs.js +1 -0
  79. package/dist/docs/building-with-agents.html +4 -4
  80. package/dist/docs/concepts.html +4 -4
  81. package/dist/docs/deployment.html +7 -7
  82. package/dist/docs/deployment.md +5 -4
  83. package/dist/docs/design/runtime-abstraction.md +1757 -0
  84. package/dist/docs/evals.html +4 -4
  85. package/dist/docs/guides/agent-to-agent.html +4 -4
  86. package/dist/docs/guides/cloud-runtime.html +4 -4
  87. package/dist/docs/guides/convert-automation.html +4 -4
  88. package/dist/docs/guides/github.html +4 -4
  89. package/dist/docs/guides/human-in-the-loop.html +4 -4
  90. package/dist/docs/guides/mcp-oauth.html +8 -8
  91. package/dist/docs/guides/mcp-oauth.md +26 -8
  92. package/dist/docs/guides/opentelemetry.html +4 -4
  93. package/dist/docs/guides/slack.html +17 -33
  94. package/dist/docs/guides/slack.md +92 -201
  95. package/dist/docs/guides/webhooks.html +4 -4
  96. package/dist/docs/hashmap.json +1 -1
  97. package/dist/docs/hillclimbing.html +4 -4
  98. package/dist/docs/index.html +6 -6
  99. package/dist/docs/index.md +5 -3
  100. package/dist/docs/llms-full.txt +2065 -637
  101. package/dist/docs/llms.txt +3 -2
  102. package/dist/docs/quickstart.html +4 -4
  103. package/dist/docs/reference/agent-config.html +6 -6
  104. package/dist/docs/reference/agent-config.md +2 -2
  105. package/dist/docs/reference/artifacts.html +4 -4
  106. package/dist/docs/reference/channels.html +4 -4
  107. package/dist/docs/reference/cli.html +13 -10
  108. package/dist/docs/reference/cli.md +32 -16
  109. package/dist/docs/reference/connections.html +9 -8
  110. package/dist/docs/reference/connections.md +6 -3
  111. package/dist/docs/reference/hooks.html +4 -4
  112. package/dist/docs/reference/http-api.html +7 -7
  113. package/dist/docs/reference/http-api.md +3 -1
  114. package/dist/docs/reference/instructions.html +4 -4
  115. package/dist/docs/reference/playground.html +4 -4
  116. package/dist/docs/reference/project-layout.html +4 -4
  117. package/dist/docs/reference/prompt.html +4 -4
  118. package/dist/docs/reference/schedules.html +4 -4
  119. package/dist/docs/reference/sessions.html +5 -5
  120. package/dist/docs/reference/sessions.md +4 -2
  121. package/dist/docs/reference/skills.html +4 -4
  122. package/dist/docs/reference/subagents.html +4 -4
  123. package/dist/docs/reference/tools.html +15 -7
  124. package/dist/docs/reference/tools.md +34 -2
  125. package/dist/docs/scaffolding-agents.html +4 -4
  126. package/dist/docs/storage.html +4 -4
  127. package/dist/docs/templates/agentic-owners.html +4 -4
  128. package/dist/docs/templates/agents-md.html +28 -0
  129. package/dist/docs/templates/agents-md.md +105 -0
  130. package/dist/docs/templates/code-wiki.html +5 -5
  131. package/dist/docs/templates/demo.html +4 -4
  132. package/dist/docs/templates/pr-autofixer.html +4 -4
  133. package/dist/docs/templates/security-help.html +4 -4
  134. package/dist/docs/templates/security-reviewer.html +5 -5
  135. package/dist/docs/templates/triage.html +4 -4
  136. package/dist/docs/troubleshooting.html +5 -5
  137. package/dist/docs/troubleshooting.md +2 -2
  138. package/dist/evals/run-facts.d.ts.map +1 -1
  139. package/dist/evals/run-facts.js +27 -0
  140. package/dist/files-backends/cursor-hosted.d.ts +3 -2
  141. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  142. package/dist/files-backends/cursor-hosted.js +3 -2
  143. package/dist/index.d.ts +1 -0
  144. package/dist/index.d.ts.map +1 -1
  145. package/dist/index.js +1 -0
  146. package/dist/internal/advertise-tools.d.ts.map +1 -1
  147. package/dist/internal/advertise-tools.js +15 -9
  148. package/dist/internal/approval-gate.d.ts +12 -13
  149. package/dist/internal/approval-gate.d.ts.map +1 -1
  150. package/dist/internal/approval-gate.js +16 -21
  151. package/dist/internal/as-of.d.ts +19 -0
  152. package/dist/internal/as-of.d.ts.map +1 -0
  153. package/dist/internal/as-of.js +82 -0
  154. package/dist/internal/chat-client.js +10 -0
  155. package/dist/internal/cli-deploy.d.ts +6 -0
  156. package/dist/internal/cli-deploy.d.ts.map +1 -1
  157. package/dist/internal/cli-deploy.js +149 -21
  158. package/dist/internal/cli-mcp-oauth.js +8 -18
  159. package/dist/internal/cli-slack-manual.d.ts +24 -2
  160. package/dist/internal/cli-slack-manual.d.ts.map +1 -1
  161. package/dist/internal/cli-slack-manual.js +251 -20
  162. package/dist/internal/cli-slack.d.ts +9 -2
  163. package/dist/internal/cli-slack.d.ts.map +1 -1
  164. package/dist/internal/cli-slack.js +29 -82
  165. package/dist/internal/cursor/account-mcp.d.ts +1 -0
  166. package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
  167. package/dist/internal/cursor/account-mcp.js +17 -0
  168. package/dist/internal/cursor-agent-template.d.ts +1 -1
  169. package/dist/internal/cursor-agent-template.d.ts.map +1 -1
  170. package/dist/internal/cursor-agent-template.js +1 -0
  171. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  172. package/dist/internal/cursor-event-relay.js +5 -6
  173. package/dist/internal/deploy-client.d.ts +17 -0
  174. package/dist/internal/deploy-client.d.ts.map +1 -1
  175. package/dist/internal/deploy-client.js +31 -10
  176. package/dist/internal/env-file.d.ts +19 -0
  177. package/dist/internal/env-file.d.ts.map +1 -0
  178. package/dist/internal/env-file.js +90 -0
  179. package/dist/internal/framework-file-storage.d.ts +11 -17
  180. package/dist/internal/framework-file-storage.d.ts.map +1 -1
  181. package/dist/internal/framework-file-storage.js +17 -40
  182. package/dist/internal/framework-storage-selection.d.ts +13 -11
  183. package/dist/internal/framework-storage-selection.d.ts.map +1 -1
  184. package/dist/internal/framework-storage-selection.js +37 -26
  185. package/dist/internal/hosted-admission-adapter.d.ts +28 -0
  186. package/dist/internal/hosted-admission-adapter.d.ts.map +1 -0
  187. package/dist/internal/hosted-admission-adapter.js +7 -0
  188. package/dist/internal/hosted-delivery-protocol.d.ts +13 -1
  189. package/dist/internal/hosted-delivery-protocol.d.ts.map +1 -1
  190. package/dist/internal/hosted-delivery-protocol.js +8 -1
  191. package/dist/internal/hosted-delivery.d.ts +21 -1
  192. package/dist/internal/hosted-delivery.d.ts.map +1 -1
  193. package/dist/internal/hosted-delivery.js +101 -34
  194. package/dist/internal/hosted-execution-diag.d.ts +59 -0
  195. package/dist/internal/hosted-execution-diag.d.ts.map +1 -0
  196. package/dist/internal/hosted-execution-diag.js +187 -0
  197. package/dist/internal/http-channel.d.ts.map +1 -1
  198. package/dist/internal/http-channel.js +85 -2
  199. package/dist/internal/init-project.d.ts.map +1 -1
  200. package/dist/internal/init-project.js +2 -0
  201. package/dist/internal/logs-client.d.ts +5 -0
  202. package/dist/internal/logs-client.d.ts.map +1 -1
  203. package/dist/internal/logs-client.js +5 -0
  204. package/dist/internal/mcp-endpoint.js +2 -3
  205. package/dist/internal/mcp-host.d.ts +10 -0
  206. package/dist/internal/mcp-host.d.ts.map +1 -1
  207. package/dist/internal/mcp-host.js +57 -8
  208. package/dist/internal/mcp-oauth-on-demand.d.ts +68 -0
  209. package/dist/internal/mcp-oauth-on-demand.d.ts.map +1 -0
  210. package/dist/internal/mcp-oauth-on-demand.js +287 -0
  211. package/dist/internal/mcp-oauth-persist.d.ts +42 -0
  212. package/dist/internal/mcp-oauth-persist.d.ts.map +1 -0
  213. package/dist/internal/mcp-oauth-persist.js +92 -0
  214. package/dist/internal/mcp-oauth.d.ts +1 -0
  215. package/dist/internal/mcp-oauth.d.ts.map +1 -1
  216. package/dist/internal/mcp-oauth.js +10 -2
  217. package/dist/internal/null-kv-storage.d.ts +48 -0
  218. package/dist/internal/null-kv-storage.d.ts.map +1 -0
  219. package/dist/internal/null-kv-storage.js +58 -0
  220. package/dist/internal/otel/collector.d.ts.map +1 -1
  221. package/dist/internal/otel/collector.js +22 -9
  222. package/dist/internal/server.d.ts +6 -0
  223. package/dist/internal/server.d.ts.map +1 -1
  224. package/dist/internal/server.js +123 -33
  225. package/dist/internal/session-engine.d.ts +88 -3
  226. package/dist/internal/session-engine.d.ts.map +1 -1
  227. package/dist/internal/session-engine.js +538 -46
  228. package/dist/internal/session-run-log.d.ts +120 -0
  229. package/dist/internal/session-run-log.d.ts.map +1 -0
  230. package/dist/internal/session-run-log.js +359 -0
  231. package/dist/internal/sessions-client.d.ts +9 -7
  232. package/dist/internal/sessions-client.d.ts.map +1 -1
  233. package/dist/internal/sessions-client.js +3 -1
  234. package/dist/internal/storage-coordinator.d.ts +13 -0
  235. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  236. package/dist/internal/storage-coordinator.js +37 -6
  237. package/dist/internal/storage-roles.d.ts +5 -0
  238. package/dist/internal/storage-roles.d.ts.map +1 -1
  239. package/dist/internal/stream-progress.d.ts.map +1 -1
  240. package/dist/internal/stream-progress.js +10 -0
  241. package/dist/internal/tool-policy.d.ts +29 -9
  242. package/dist/internal/tool-policy.d.ts.map +1 -1
  243. package/dist/internal/tool-policy.js +24 -0
  244. package/dist/internal/trajectory.d.ts.map +1 -1
  245. package/dist/internal/trajectory.js +10 -0
  246. package/dist/internal/turn-outbox.d.ts +24 -0
  247. package/dist/internal/turn-outbox.d.ts.map +1 -0
  248. package/dist/internal/turn-outbox.js +80 -0
  249. package/dist/playground/assets/index-D7rGZNjy.css +1 -0
  250. package/dist/playground/assets/index-jzNbJENa.js +69 -0
  251. package/dist/playground/index.html +2 -2
  252. package/dist/storage-backends/cursor-hosted.d.ts +1 -1
  253. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  254. package/dist/storage-backends/cursor-hosted.js +12 -0
  255. package/dist/storage-protocol.d.ts +8 -0
  256. package/dist/storage-protocol.d.ts.map +1 -1
  257. package/dist/storage-protocol.js +8 -0
  258. package/dist/storage.d.ts +8 -0
  259. package/dist/storage.d.ts.map +1 -1
  260. package/dist/tools.d.ts +12 -2
  261. package/dist/tools.d.ts.map +1 -1
  262. package/dist/types.d.ts +88 -7
  263. package/dist/types.d.ts.map +1 -1
  264. package/docs/README.md +5 -3
  265. package/docs/deployment.md +5 -4
  266. package/docs/design/runtime-abstraction.md +1757 -0
  267. package/docs/guides/mcp-oauth.md +26 -8
  268. package/docs/guides/slack.md +93 -202
  269. package/docs/reference/agent-config.md +2 -2
  270. package/docs/reference/cli.md +32 -16
  271. package/docs/reference/connections.md +6 -3
  272. package/docs/reference/http-api.md +3 -1
  273. package/docs/reference/sessions.md +4 -2
  274. package/docs/reference/tools.md +34 -2
  275. package/docs/templates/agents-md.md +110 -0
  276. package/docs/troubleshooting.md +2 -2
  277. package/package.json +1 -1
  278. package/skills/create-agent/SKILL.md +2 -1
  279. package/skills/mcp-auth/SKILL.md +6 -3
  280. package/skills/setup-slack/SKILL.md +23 -13
  281. package/src/bin/agent-serve.ts +12 -1
  282. package/src/channels/github/github-channel.ts +17 -0
  283. package/src/channels/origin/api.ts +24 -5
  284. package/src/channels/origin/binding.ts +55 -26
  285. package/src/channels/origin/index.ts +1 -0
  286. package/src/channels/origin/origin-channel.ts +78 -10
  287. package/src/channels/origin/reactions.ts +82 -0
  288. package/src/channels/origin/types.ts +11 -0
  289. package/src/channels/slack/approvals.ts +105 -0
  290. package/src/channels/slack/cli-project.ts +267 -0
  291. package/src/channels/slack/defaults.ts +165 -74
  292. package/src/channels/slack/dispatch.ts +2 -0
  293. package/src/channels/slack/init.ts +10 -5
  294. package/src/channels/slack/interactive.ts +35 -9
  295. package/src/channels/slack/live-delivery.ts +5 -3
  296. package/src/channels/slack/manifest.ts +1 -1
  297. package/src/channels/slack/progress-delivery.ts +133 -0
  298. package/src/channels/slack/setup.ts +10 -6
  299. package/src/channels/slack/slack-channel.ts +1 -0
  300. package/src/channels/slack/tool-approval-events.ts +93 -0
  301. package/src/channels/slack/types.ts +9 -1
  302. package/src/evals/run-facts.ts +32 -0
  303. package/src/files-backends/cursor-hosted.ts +3 -2
  304. package/src/index.ts +4 -0
  305. package/src/internal/advertise-tools.ts +31 -10
  306. package/src/internal/approval-gate.ts +20 -29
  307. package/src/internal/as-of.ts +107 -0
  308. package/src/internal/chat-client.ts +19 -0
  309. package/src/internal/cli-deploy.ts +203 -13
  310. package/src/internal/cli-mcp-oauth.ts +7 -21
  311. package/src/internal/cli-slack-manual.ts +350 -18
  312. package/src/internal/cli-slack.ts +40 -84
  313. package/src/internal/cursor/account-mcp.ts +21 -0
  314. package/src/internal/cursor-agent-template.ts +1 -0
  315. package/src/internal/cursor-event-relay.ts +5 -6
  316. package/src/internal/deploy-client.ts +43 -0
  317. package/src/internal/env-file.ts +84 -0
  318. package/src/internal/framework-file-storage.ts +18 -46
  319. package/src/internal/framework-storage-selection.ts +41 -25
  320. package/src/internal/hosted-admission-adapter.ts +42 -0
  321. package/src/internal/hosted-delivery-protocol.ts +14 -2
  322. package/src/internal/hosted-delivery.ts +169 -44
  323. package/src/internal/hosted-execution-diag.ts +252 -0
  324. package/src/internal/http-channel.ts +107 -0
  325. package/src/internal/init-project.ts +2 -0
  326. package/src/internal/logs-client.ts +5 -0
  327. package/src/internal/mcp-endpoint.ts +13 -4
  328. package/src/internal/mcp-host.ts +77 -11
  329. package/src/internal/mcp-oauth-on-demand.ts +352 -0
  330. package/src/internal/mcp-oauth-persist.ts +127 -0
  331. package/src/internal/mcp-oauth.ts +17 -1
  332. package/src/internal/null-kv-storage.ts +74 -0
  333. package/src/internal/otel/collector.ts +30 -0
  334. package/src/internal/server.ts +159 -11
  335. package/src/internal/session-engine.ts +691 -57
  336. package/src/internal/session-run-log.ts +584 -0
  337. package/src/internal/sessions-client.ts +17 -8
  338. package/src/internal/storage-coordinator.ts +49 -9
  339. package/src/internal/storage-roles.ts +7 -0
  340. package/src/internal/stream-progress.ts +19 -0
  341. package/src/internal/tool-policy.ts +49 -9
  342. package/src/internal/trajectory.ts +10 -0
  343. package/src/internal/turn-outbox.ts +85 -0
  344. package/src/storage-backends/cursor-hosted.ts +13 -1
  345. package/src/storage-protocol.ts +10 -0
  346. package/src/storage.ts +12 -0
  347. package/src/tools.ts +12 -2
  348. package/src/types.ts +93 -8
  349. package/templates/agents-md/README.md +39 -0
  350. package/templates/agents-md/agent/agent.ts +8 -0
  351. package/templates/agents-md/agent/channels/github.ts +10 -0
  352. package/templates/agents-md/agent/channels/queue.ts +28 -0
  353. package/templates/agents-md/agent/channels/slack.ts +9 -0
  354. package/templates/agents-md/agent/instructions.md +39 -0
  355. package/templates/agents-md/agent/lib/collect.ts +439 -0
  356. package/templates/agents-md/agent/lib/config.ts +8 -0
  357. package/templates/agents-md/agent/lib/paths.ts +51 -0
  358. package/templates/agents-md/agent/schedules/weekly.ts +17 -0
  359. package/templates/agents-md/agent/tools/apply_agents.ts +226 -0
  360. package/templates/agents-md/evals/agents.eval.ts +135 -0
  361. package/templates/agents-md/evals/evals.config.ts +5 -0
  362. package/templates/agents-md/init.json +38 -0
  363. package/templates/agents-md/package.json +18 -0
  364. package/templates/agents-md/tsconfig.json +12 -0
  365. package/dist/docs/assets/chunks/@localSearchIndexroot.DFoGuETX.js +0 -1
  366. package/dist/docs/assets/guides_slack.md.C32HsdKk.js +0 -38
  367. package/dist/docs/assets/guides_slack.md.C32HsdKk.lean.js +0 -1
  368. package/dist/docs/assets/index.md.CI93Ltlb.js +0 -5
  369. package/dist/docs/assets/reference_sessions.md.D0mIh4KK.js +0 -1
  370. package/dist/docs/assets/troubleshooting.md.Ctv3T8C2.js +0 -1
  371. package/dist/docs/design/agsh.md +0 -406
  372. package/dist/playground/assets/index-B1DDvBeC.css +0 -1
  373. package/dist/playground/assets/index-Cx9CclK_.js +0 -88
  374. package/docs/design/agsh.md +0 -406
  375. /package/dist/docs/assets/{deployment.md.D9msOFOW.lean.js → deployment.md.MFPKqsqI.lean.js} +0 -0
  376. /package/dist/docs/assets/{index.md.CI93Ltlb.lean.js → index.md.CVeRUOeZ.lean.js} +0 -0
  377. /package/dist/docs/assets/{reference_agent-config.md.Cp_x38Nl.lean.js → reference_agent-config.md.BHeYMgme.lean.js} +0 -0
  378. /package/dist/docs/assets/{reference_connections.md.BH8Oc0D0.lean.js → reference_connections.md.DxldvyIB.lean.js} +0 -0
  379. /package/dist/docs/assets/{reference_http-api.md.D89k1mdm.lean.js → reference_http-api.md.BEJx9XVj.lean.js} +0 -0
  380. /package/dist/docs/assets/{reference_sessions.md.D0mIh4KK.lean.js → reference_sessions.md.NxFP_CAC.lean.js} +0 -0
  381. /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,9 +83,13 @@ 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";
92
+ import { assertTimeArgs, normalizeAsOf, sessionClock } from "./as-of.js";
85
93
  import { SessionAuthError, samePrincipal } from "./auth.js";
86
94
  import { mergeCloudOptions, resolveSessionRuntime } from "./cloud-merge.js";
87
95
  import {
@@ -125,7 +133,8 @@ import {
125
133
  parseContinuationToken,
126
134
  } from "./ids.js";
127
135
  import { validateToolInput } from "./json-schema.js";
128
- import { McpHost } from "./mcp-host.js";
136
+ import { isMcpNeedsAuthError, McpHost } from "./mcp-host.js";
137
+ import { McpOauthOnDemandHub } from "./mcp-oauth-on-demand.js";
129
138
  import type { OtelCollector } from "./otel/collector.js";
130
139
  import {
131
140
  shouldStartProcessMemoryTelemetry,
@@ -143,7 +152,7 @@ import { syncSkillsToStore } from "./skills-sync.js";
143
152
  import { steeringEnabled } from "./steering.js";
144
153
  import { StorageCoordinator } from "./storage-coordinator.js";
145
154
  import type { FrameworkSessionStore } from "./storage-roles.js";
146
- import { decideToolCall, resolveEffect } from "./tool-policy.js";
155
+ import { decideToolCall, directCallEffect } from "./tool-policy.js";
147
156
  import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
148
157
  import {
149
158
  type EstimateTurnCostFn,
@@ -152,6 +161,7 @@ import {
152
161
  } from "./turn-cost.js";
153
162
  import { TurnGovernor } from "./turn-governor.js";
154
163
  import { TurnLeaseStore } from "./turn-lease-store.js";
164
+ import { TurnOutbox } from "./turn-outbox.js";
155
165
  import {
156
166
  buildAgentsMdContent,
157
167
  buildCloudMemorySection,
@@ -258,6 +268,22 @@ export class ApprovalInterruptedError extends Error {
258
268
  }
259
269
  }
260
270
 
271
+ export class AuthNotFoundError extends Error {
272
+ constructor(sessionId: string, callId: string) {
273
+ super(`No pending MCP auth for session ${sessionId} call ${callId}`);
274
+ this.name = "AuthNotFoundError";
275
+ }
276
+ }
277
+
278
+ export class AuthInterruptedError extends Error {
279
+ constructor(sessionId: string, callId: string) {
280
+ super(
281
+ `MCP auth for session ${sessionId} call ${callId} is no longer live (host may have restarted while the tool was parked)`
282
+ );
283
+ this.name = "AuthInterruptedError";
284
+ }
285
+ }
286
+
261
287
  export interface SessionEngineOptions {
262
288
  project: AgentProject;
263
289
  stateRoot: string;
@@ -312,6 +338,16 @@ interface ActiveToolContext {
312
338
  emit: (payload: SessionEventPayload) => void;
313
339
  }
314
340
 
341
+ type McpAuthParkCtx = {
342
+ sessionId: string;
343
+ turnId: string;
344
+ callId: string;
345
+ toolName: string;
346
+ emit: (payload: SessionEventPayload) => void;
347
+ };
348
+
349
+ const mcpAuthAls = new AsyncLocalStorage<McpAuthParkCtx>();
350
+
315
351
  export interface EngineSendOptions extends SendMessageOptions {
316
352
  mode?: "chat" | "task";
317
353
  /**
@@ -346,6 +382,8 @@ export class SessionEngine {
346
382
  readonly askDedupe: SlackAskDedupeStore;
347
383
  /** Cross-process turn ownership under `<stateRoot>/turn-leases`. */
348
384
  readonly turnLeases: TurnLeaseStore;
385
+ /** Messages for turns another server owns, under `<stateRoot>/outbox`. */
386
+ readonly outbox: TurnOutbox<AdmitTurnOptions>;
349
387
  /**
350
388
  * Runtime for `agent/storage.ts` (`defineStorage`), when
351
389
  * authored. All durable state changes funnel through it (session
@@ -370,6 +408,13 @@ export class SessionEngine {
370
408
  /** Sessions with a turn being admitted (before the run promise exists). */
371
409
  private readonly pendingTurns = new Set<string>();
372
410
  private readonly activeTurns = new Map<string, Promise<TurnOutcome>>();
411
+ /**
412
+ * Sessions whose workspace a turn in this process has materialized. A
413
+ * session is busy from admit — before the governor slot and the turn's
414
+ * own materialize — so a read running alongside a turn consults this,
415
+ * not busy, to know whether the workspace is ready to be left alone.
416
+ */
417
+ private readonly materializedWorkspaces = new Set<string>();
373
418
  /** Context used by the cloud turn's HTTP MCP callbacks. */
374
419
  private readonly activeToolContexts = new Map<string, ActiveToolContext>();
375
420
  /**
@@ -410,6 +455,11 @@ export class SessionEngine {
410
455
  private readonly mcpHost: McpHost;
411
456
  private readonly platforms: HostPlatforms;
412
457
  private readonly approvals = new ApprovalGate();
458
+ private readonly auths = new ApprovalGate<PendingAuth>();
459
+ private readonly oauthHub = new McpOauthOnDemandHub();
460
+ private probeCursorAccount:
461
+ | ((connection: string) => Promise<string | undefined>)
462
+ | undefined;
413
463
  private readonly abCollector: ABCollector;
414
464
  private readonly otel: OtelCollector | undefined;
415
465
  private readonly otelHost: HostOtelSessionApi;
@@ -458,6 +508,7 @@ export class SessionEngine {
458
508
  this.turnLeases = new TurnLeaseStore(
459
509
  join(options.stateRoot, "turn-leases")
460
510
  );
511
+ this.outbox = new TurnOutbox(join(options.stateRoot, "outbox"));
461
512
  this.logs = new EventLogStore(
462
513
  (sessionId) => this.sessions.eventFilePath(sessionId),
463
514
  (sessionId, error) =>
@@ -565,10 +616,25 @@ export class SessionEngine {
565
616
  this.cloudServerToolsConnection = connection;
566
617
  }
567
618
 
619
+ setMcpOauthCallbackUrl(url: string): void {
620
+ this.oauthHub.callbackUrl = url;
621
+ this.mcpHost.setOAuthRedirectUrl(url);
622
+ }
623
+
624
+ setCursorAccountAuthProbe(
625
+ probe: (connection: string) => Promise<string | undefined>
626
+ ): void {
627
+ this.probeCursorAccount = probe;
628
+ }
629
+
630
+ get oauthCallbackHub(): McpOauthOnDemandHub {
631
+ return this.oauthHub;
632
+ }
633
+
568
634
  /** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / files / otel / Slack nudges). */
569
635
  get host(): HostContext {
570
636
  return {
571
- mcp: this.mcpHost,
637
+ mcp: this.wrapMcp(this.mcpHost),
572
638
  github: this.platforms.github,
573
639
  slack: this.platforms.slack,
574
640
  reminders: this.reminderApi,
@@ -958,12 +1024,15 @@ export class SessionEngine {
958
1024
  const now = new Date().toISOString();
959
1025
  const channelState = this.initialChannelState(channelId, sendOptions.state);
960
1026
  const purpose = sendOptions.purpose === "eval" ? "eval" : "live";
1027
+ const frozenAsOf = normalizeAsOf(sendOptions.asOf);
1028
+ assertTimeArgs(this.project.agent.tools, frozenAsOf);
961
1029
  let record: SessionRecord = {
962
1030
  sessionId: sendOptions.hostedSessionId ?? newSessionId(),
963
1031
  channelId,
964
1032
  mode: sendOptions.mode ?? "chat",
965
1033
  purpose,
966
- ...(sendOptions.dryRun === true ? { dryRun: true } : {}),
1034
+ dryRun: sendOptions.dryRun === true ? true : undefined,
1035
+ asOf: frozenAsOf,
967
1036
  continuationKey: sendOptions.continuationToken ?? newContinuationKey(),
968
1037
  auth,
969
1038
  title: sendOptions.title,
@@ -981,7 +1050,10 @@ export class SessionEngine {
981
1050
  this.storage?.sessionRecord(record);
982
1051
  await this.appendEvent(record.sessionId, {
983
1052
  type: "session.started",
984
- data: { channelId },
1053
+ data: {
1054
+ channelId,
1055
+ dryRun: record.dryRun === true ? true : undefined,
1056
+ },
985
1057
  });
986
1058
  record = await this.enrollABs(record);
987
1059
  const since = (await this.logs.get(record.sessionId)).count;
@@ -1231,7 +1303,14 @@ export class SessionEngine {
1231
1303
  const prior = this.admissionChains.get(sessionId) ?? Promise.resolve();
1232
1304
  const admitted = prior
1233
1305
  .catch(() => {})
1234
- .then(() => this.admitTurn(sessionId, message, options));
1306
+ .then(async () => {
1307
+ // Another server holds the turn: hand the message to it and coalesce.
1308
+ if (await this.turnLeases.runningElsewhere(sessionId)) {
1309
+ await this.outbox.post(sessionId, { message, options });
1310
+ return true;
1311
+ }
1312
+ return this.admitTurn(sessionId, message, options);
1313
+ });
1235
1314
  this.admissionChains.set(
1236
1315
  sessionId,
1237
1316
  admitted.catch(() => {})
@@ -1239,6 +1318,20 @@ export class SessionEngine {
1239
1318
  return admitted;
1240
1319
  }
1241
1320
 
1321
+ /** Admit what other servers handed to this turn's holder. Never awaited by the lifecycle. */
1322
+ private drainOutbox(sessionId: string): void {
1323
+ void this.outbox
1324
+ .takeAll(sessionId)
1325
+ .then((items) => {
1326
+ for (const item of items) {
1327
+ void this.startTurn(sessionId, item.message, item.options).catch(
1328
+ () => {}
1329
+ );
1330
+ }
1331
+ })
1332
+ .catch(() => {});
1333
+ }
1334
+
1242
1335
  private enqueueCoalesced(sessionId: string, item: CoalescedQueuedTurn): void {
1243
1336
  const queue = this.coalesceQueues.get(sessionId) ?? [];
1244
1337
  if (
@@ -1476,6 +1569,8 @@ export class SessionEngine {
1476
1569
  let lifecycle!: Promise<TurnOutcome>;
1477
1570
  lifecycle = (async (): Promise<TurnOutcome> => {
1478
1571
  let outcome: TurnOutcome;
1572
+ const poll = setInterval(() => this.drainOutbox(sessionId), 1_000);
1573
+ poll.unref();
1479
1574
  try {
1480
1575
  outcome = await this.executeTurn(
1481
1576
  record,
@@ -1489,6 +1584,8 @@ export class SessionEngine {
1489
1584
  status: "error",
1490
1585
  errorMessage: describeError(error),
1491
1586
  };
1587
+ } finally {
1588
+ clearInterval(poll);
1492
1589
  }
1493
1590
  this.lastOutcomes.set(sessionId, outcome);
1494
1591
  const wasCurrent = this.activeTurns.get(sessionId) === lifecycle;
@@ -1518,6 +1615,10 @@ export class SessionEngine {
1518
1615
  // The drain may have nested a successor turn, which took its own
1519
1616
  // reference; the lease outlives this release until that one ends too.
1520
1617
  await lease.release();
1618
+ // Handed off between the last poll and the release: nobody else will.
1619
+ if (!abort.signal.aborted) {
1620
+ this.drainOutbox(sessionId);
1621
+ }
1521
1622
  return outcome;
1522
1623
  })();
1523
1624
  this.activeTurns.set(sessionId, lifecycle);
@@ -1562,6 +1663,7 @@ export class SessionEngine {
1562
1663
  this.coalesceDrains.has(sessionId) ||
1563
1664
  (this.coalesceQueues.get(sessionId)?.length ?? 0) > 0;
1564
1665
  this.clearCoalesced(sessionId);
1666
+ await this.outbox.clear(sessionId);
1565
1667
  const record = await this.sessions.get(sessionId);
1566
1668
  const continuationKey = record?.continuationKey;
1567
1669
  if (continuationKey !== undefined && continuationKey !== null) {
@@ -1795,6 +1897,7 @@ export class SessionEngine {
1795
1897
  seed: isFirstTurn,
1796
1898
  stateRoot: this.stateRoot,
1797
1899
  });
1900
+ this.materializedWorkspaces.add(sessionId);
1798
1901
  // Must precede the carrier: the harness resolves rules, skills,
1799
1902
  // `mcp.json` and hooks from its git root, so a workspace nested in an
1800
1903
  // outer repo would load that repo's hooks instead of the ones written
@@ -1879,11 +1982,14 @@ export class SessionEngine {
1879
1982
  }
1880
1983
  } catch (error) {
1881
1984
  this.approvals.cancelSession(sessionId, "deny");
1985
+ this.auths.cancelSession(sessionId, "deny");
1882
1986
  outcome = { status: "error", errorMessage: describeError(error) };
1883
1987
  } finally {
1884
1988
  permit.release();
1885
1989
  this.approvals.cancelSession(sessionId, "deny");
1990
+ this.auths.cancelSession(sessionId, "deny");
1886
1991
  await this.clearPendingApprovals(sessionId);
1992
+ await this.clearPendingAuths(sessionId);
1887
1993
  // Anything still queued belongs to the next turn's boundary drain, not
1888
1994
  // to this turn's edge detection.
1889
1995
  this.deliveredNudges.delete(sessionId);
@@ -2105,6 +2211,8 @@ export class SessionEngine {
2105
2211
  channelId: record.channelId,
2106
2212
  channelKind: this.channelsById.get(record.channelId)?.meta?.kind,
2107
2213
  environment: isDevMode() ? "local dev server" : "hosted deployment",
2214
+ now:
2215
+ record.asOf === undefined ? undefined : sessionClock(record.asOf)(),
2108
2216
  });
2109
2217
  turnPrompt = `${contextBlock}\n\n${prompt}`;
2110
2218
  }
@@ -2518,11 +2626,21 @@ export class SessionEngine {
2518
2626
  // named passthroughs resolved fresh on every turn — reminder fires and
2519
2627
  // post-restart follow-ups included — and a listing failure throws,
2520
2628
  // failing the turn closed.
2521
- for (const tool of await resolveAdvertisedTools({
2522
- agent,
2523
- session: this.sessionInfo(record),
2524
- hostMcp: this.mcpHost,
2525
- })) {
2629
+ for (const tool of await mcpAuthAls.run(
2630
+ {
2631
+ sessionId: record.sessionId,
2632
+ turnId,
2633
+ callId: newToolCallId("mcp"),
2634
+ toolName: "mcp",
2635
+ emit,
2636
+ },
2637
+ () =>
2638
+ resolveAdvertisedTools({
2639
+ agent,
2640
+ session: this.sessionInfo(record),
2641
+ hostMcp: this.advertiseMcp(),
2642
+ })
2643
+ )) {
2526
2644
  register(tool);
2527
2645
  }
2528
2646
  return tools;
@@ -2615,6 +2733,7 @@ export class SessionEngine {
2615
2733
  getSession: (channelId, sessionId) =>
2616
2734
  this.getChannelSession(channelId, sessionId),
2617
2735
  emit: args.emit,
2736
+ now: sessionClock(args.record.asOf),
2618
2737
  };
2619
2738
  try {
2620
2739
  // Before the approval gate: a dry run is unattended, so parking a write
@@ -2632,6 +2751,8 @@ export class SessionEngine {
2632
2751
  : {}),
2633
2752
  dryRunResult: args.tool.definition.dryRunResult,
2634
2753
  input: validation.value,
2754
+ asOf: args.record.asOf,
2755
+ timeArgs: args.tool.definition.timeArgs,
2635
2756
  });
2636
2757
  if (decision.kind === "answer") {
2637
2758
  this.stubbedCallIds.get(args.record.sessionId)?.add(toolCallId);
@@ -2640,6 +2761,12 @@ export class SessionEngine {
2640
2761
  );
2641
2762
  return normalizeToolResult(decision.result);
2642
2763
  }
2764
+ if (decision.kind === "unbounded") {
2765
+ this.logger(
2766
+ `[session] tool ${args.tool.name} refused, unbounded_time (${toolCallId})`
2767
+ );
2768
+ return toolErrorResult(decision.message);
2769
+ }
2643
2770
  if (decision.kind === "refuse") {
2644
2771
  this.logger(
2645
2772
  `[session] tool ${args.tool.name} refused, effect unclassified (${toolCallId})`
@@ -2672,7 +2799,16 @@ export class SessionEngine {
2672
2799
  }
2673
2800
  const started = Date.now();
2674
2801
  this.logger(`[session] tool ${args.tool.name} start`);
2675
- const result = await execute(validation.value as never, toolCtx);
2802
+ const result = await mcpAuthAls.run(
2803
+ {
2804
+ sessionId: args.record.sessionId,
2805
+ turnId: args.turnId,
2806
+ callId: toolCallId,
2807
+ toolName: args.tool.name,
2808
+ emit: args.emit,
2809
+ },
2810
+ () => execute(validation.value as never, toolCtx)
2811
+ );
2676
2812
  this.logger(
2677
2813
  `[session] tool ${args.tool.name} done (${Date.now() - started}ms)`
2678
2814
  );
@@ -2708,10 +2844,10 @@ export class SessionEngine {
2708
2844
  args: args.args,
2709
2845
  requestedAt,
2710
2846
  };
2711
- const resolutionPromise = this.approvals.wait({
2712
- sessionId: args.sessionId,
2713
- ...pendingRecord,
2714
- });
2847
+ const resolutionPromise = this.approvals.wait(
2848
+ args.sessionId,
2849
+ pendingRecord
2850
+ );
2715
2851
  await this.updateRecord(args.sessionId, (current) => ({
2716
2852
  ...current,
2717
2853
  pendingApprovals: [
@@ -2831,6 +2967,318 @@ export class SessionEngine {
2831
2967
  }));
2832
2968
  }
2833
2969
 
2970
+ private wrapMcp(inner: McpHost): HostMcpRegistry {
2971
+ return {
2972
+ names: () => inner.names(),
2973
+ listTools: (name) => this.withMcpAuth(name, () => inner.listTools(name)),
2974
+ callTool: (name, tool, args) =>
2975
+ this.withMcpAuth(name, () => inner.callTool(name, tool, args)),
2976
+ };
2977
+ }
2978
+
2979
+ private advertiseMcp(): AdvertiseHostMcp {
2980
+ return {
2981
+ listTools: (name) =>
2982
+ this.withMcpListAuth(name, () => this.mcpHost.listTools(name)),
2983
+ callTool: (name, tool, args) =>
2984
+ this.withMcpAuth(name, () => this.mcpHost.callTool(name, tool, args)),
2985
+ oneOff: (connection) => {
2986
+ const inner = this.mcpHost.oneOff(connection);
2987
+ return {
2988
+ listTools: () =>
2989
+ this.withMcpListAuth(connection.name, () => inner.listTools()),
2990
+ callTool: (tool, args) =>
2991
+ this.withMcpAuth(connection.name, () => inner.callTool(tool, args)),
2992
+ };
2993
+ },
2994
+ };
2995
+ }
2996
+
2997
+ private withMcpListAuth<T>(
2998
+ connection: string,
2999
+ run: () => Promise<T>
3000
+ ): Promise<T> {
3001
+ const ctx = mcpAuthAls.getStore();
3002
+ if (ctx === undefined) {
3003
+ return this.withMcpAuth(connection, run);
3004
+ }
3005
+ const toolName = `mcp:${connection}`;
3006
+ return mcpAuthAls.run(
3007
+ {
3008
+ ...ctx,
3009
+ callId: newToolCallId(toolName),
3010
+ toolName,
3011
+ },
3012
+ () => this.withMcpAuth(connection, run)
3013
+ );
3014
+ }
3015
+
3016
+ private async withMcpAuth<T>(
3017
+ connection: string,
3018
+ run: () => Promise<T>
3019
+ ): Promise<T> {
3020
+ try {
3021
+ return await run();
3022
+ } catch (error) {
3023
+ if (!isMcpNeedsAuthError(error)) {
3024
+ throw error;
3025
+ }
3026
+ return await this.parkAndRetryMcp(connection, run, error);
3027
+ }
3028
+ }
3029
+
3030
+ private async parkAndRetryMcp<T>(
3031
+ connection: string,
3032
+ run: () => Promise<T>,
3033
+ error: unknown
3034
+ ): Promise<T> {
3035
+ const ctx = mcpAuthAls.getStore();
3036
+ if (ctx === undefined) {
3037
+ throw error;
3038
+ }
3039
+ const minted = await this.mintAuthUrl(connection, ctx.sessionId);
3040
+ if (minted === undefined) {
3041
+ throw error;
3042
+ }
3043
+ if ("authorized" in minted) {
3044
+ await this.persistHostedMcpOauth(connection);
3045
+ await this.mcpHost.dropClient(connection);
3046
+ return await this.withMcpAuth(connection, run);
3047
+ }
3048
+ if (minted.state !== "") {
3049
+ const bind = this.oauthHub.bind(minted.state, ctx.sessionId, ctx.callId);
3050
+ if (bind === "already_done") {
3051
+ await this.persistHostedMcpOauth(connection);
3052
+ await this.mcpHost.dropClient(connection);
3053
+ return await this.withMcpAuth(connection, run);
3054
+ }
3055
+ }
3056
+ const decision = await this.parkForAuth({
3057
+ sessionId: ctx.sessionId,
3058
+ turnId: ctx.turnId,
3059
+ callId: ctx.callId,
3060
+ toolName: ctx.toolName,
3061
+ connection,
3062
+ authUrl: minted.authUrl,
3063
+ emit: ctx.emit,
3064
+ });
3065
+ if (decision === "deny") {
3066
+ throw new Error(
3067
+ `MCP connection "${connection}" authorization was denied.`
3068
+ );
3069
+ }
3070
+ await this.persistHostedMcpOauth(connection);
3071
+ await this.mcpHost.dropClient(connection);
3072
+ return await this.withMcpAuth(connection, run);
3073
+ }
3074
+
3075
+ private async persistHostedMcpOauth(connection: string): Promise<void> {
3076
+ const authored = this.authoredConnection(connection);
3077
+ if (
3078
+ authored === undefined ||
3079
+ !isHostOauthConnectionTransport(authored.transport)
3080
+ ) {
3081
+ return;
3082
+ }
3083
+ await this.oauthHub.persistFor(connection, authored.transport.url);
3084
+ }
3085
+
3086
+ private authoredConnection(name: string) {
3087
+ return (
3088
+ this.project.agent.connections.find((entry) => entry.name === name) ??
3089
+ this.project.agent.hostConnections?.find((entry) => entry.name === name)
3090
+ );
3091
+ }
3092
+
3093
+ private async mintAuthUrl(
3094
+ connection: string,
3095
+ sessionId: string
3096
+ ): Promise<
3097
+ { authorized: true } | { authUrl: string; state: string } | undefined
3098
+ > {
3099
+ const authored = this.authoredConnection(connection);
3100
+ if (authored === undefined) {
3101
+ return undefined;
3102
+ }
3103
+ if (isCursorAccountTransport(authored.transport)) {
3104
+ const probe = this.probeCursorAccount;
3105
+ if (probe === undefined) {
3106
+ return undefined;
3107
+ }
3108
+ const authUrl = await probe(connection);
3109
+ if (authUrl === undefined || authUrl === "") {
3110
+ return undefined;
3111
+ }
3112
+ return { authUrl, state: "" };
3113
+ }
3114
+ if (!isHostOauthConnectionTransport(authored.transport)) {
3115
+ return undefined;
3116
+ }
3117
+ return await this.oauthHub.mint({
3118
+ connection,
3119
+ url: authored.transport.url,
3120
+ type: authored.transport.type === "sse" ? "sse" : "http",
3121
+ headers: authored.transport.headers,
3122
+ sessionId,
3123
+ });
3124
+ }
3125
+
3126
+ private async parkForAuth(args: {
3127
+ sessionId: string;
3128
+ turnId: string;
3129
+ callId: string;
3130
+ toolName: string;
3131
+ connection: string;
3132
+ authUrl: string;
3133
+ emit: (payload: SessionEventPayload) => void;
3134
+ }): Promise<ApprovalDecision> {
3135
+ const requestedAt = new Date().toISOString();
3136
+ const pendingRecord: PendingAuth = {
3137
+ callId: args.callId,
3138
+ turnId: args.turnId,
3139
+ toolName: args.toolName,
3140
+ connection: args.connection,
3141
+ authUrl: args.authUrl,
3142
+ requestedAt,
3143
+ };
3144
+ const resolutionPromise = this.auths.wait(args.sessionId, pendingRecord);
3145
+ await this.updateRecord(args.sessionId, (current) => ({
3146
+ ...current,
3147
+ pendingAuths: [
3148
+ ...(current.pendingAuths ?? []).filter(
3149
+ (item) => item.callId !== args.callId
3150
+ ),
3151
+ pendingRecord,
3152
+ ],
3153
+ }));
3154
+ args.emit({
3155
+ type: "action.auth_requested",
3156
+ data: {
3157
+ callId: args.callId,
3158
+ toolName: args.toolName,
3159
+ connection: args.connection,
3160
+ authUrl: args.authUrl,
3161
+ },
3162
+ });
3163
+ this.logger(
3164
+ `[session] tool ${args.toolName} awaiting MCP auth ${args.connection} (${args.callId})`
3165
+ );
3166
+ const { decision, by } = await resolutionPromise;
3167
+ await this.updateRecord(args.sessionId, (current) => ({
3168
+ ...current,
3169
+ pendingAuths: (current.pendingAuths ?? []).filter(
3170
+ (item) => item.callId !== args.callId
3171
+ ),
3172
+ }));
3173
+ args.emit({
3174
+ type: "action.auth_resolved",
3175
+ data: {
3176
+ callId: args.callId,
3177
+ toolName: args.toolName,
3178
+ connection: args.connection,
3179
+ decision,
3180
+ by,
3181
+ },
3182
+ });
3183
+ this.logger(
3184
+ `[session] tool ${args.toolName} MCP auth ${decision} (${args.callId})`
3185
+ );
3186
+ return decision;
3187
+ }
3188
+
3189
+ private async clearPendingAuths(sessionId: string): Promise<void> {
3190
+ await this.oauthHub.abortSession(sessionId);
3191
+ const current = await this.sessions.get(sessionId);
3192
+ if (
3193
+ current === undefined ||
3194
+ current.pendingAuths === undefined ||
3195
+ current.pendingAuths.length === 0
3196
+ ) {
3197
+ return;
3198
+ }
3199
+ await this.updateRecord(sessionId, (record) => ({
3200
+ ...record,
3201
+ pendingAuths: [],
3202
+ }));
3203
+ }
3204
+
3205
+ async listPendingAuths(sessionId: string): Promise<PendingAuth[]> {
3206
+ const record = await this.sessions.get(sessionId);
3207
+ if (record === undefined) {
3208
+ return [];
3209
+ }
3210
+ const live = this.auths.list(sessionId);
3211
+ if (live.length > 0) {
3212
+ return live;
3213
+ }
3214
+ return record.pendingAuths ?? [];
3215
+ }
3216
+
3217
+ async resolveAuth(
3218
+ sessionId: string,
3219
+ callId: string,
3220
+ decision: ApprovalDecision,
3221
+ auth: AuthContext | null,
3222
+ options?: { asSessionOwner?: boolean }
3223
+ ): Promise<PendingAuth> {
3224
+ const record = await this.sessions.get(sessionId);
3225
+ if (record === undefined) {
3226
+ throw new AuthNotFoundError(sessionId, callId);
3227
+ }
3228
+ const ownershipAuth = options?.asSessionOwner === true ? record.auth : auth;
3229
+ if (!samePrincipal(record.auth, ownershipAuth)) {
3230
+ throw new SessionAuthError(sessionId);
3231
+ }
3232
+ const pending =
3233
+ (record.pendingAuths ?? []).find((item) => item.callId === callId) ??
3234
+ this.auths.list(sessionId).find((item) => item.callId === callId);
3235
+ if (pending === undefined) {
3236
+ throw new AuthNotFoundError(sessionId, callId);
3237
+ }
3238
+ const resolvedBy = approvalResolverLabel(auth);
3239
+ if (decision === "deny") {
3240
+ await this.oauthHub.abortCall(sessionId, callId);
3241
+ }
3242
+ if (!this.auths.resolve(sessionId, callId, decision, resolvedBy)) {
3243
+ await this.removePendingAuth(sessionId, callId);
3244
+ throw new AuthInterruptedError(sessionId, callId);
3245
+ }
3246
+ return pending;
3247
+ }
3248
+
3249
+ private async removePendingAuth(
3250
+ sessionId: string,
3251
+ callId: string
3252
+ ): Promise<void> {
3253
+ await this.updateRecord(sessionId, (current) => ({
3254
+ ...current,
3255
+ pendingAuths: (current.pendingAuths ?? []).filter(
3256
+ (item) => item.callId !== callId
3257
+ ),
3258
+ }));
3259
+ }
3260
+
3261
+ async runWithMcpPark<T>(
3262
+ sessionId: string,
3263
+ toolName: string,
3264
+ fn: () => Promise<T>
3265
+ ): Promise<T> {
3266
+ const active = this.activeToolContexts.get(sessionId);
3267
+ if (active === undefined) {
3268
+ return fn();
3269
+ }
3270
+ return mcpAuthAls.run(
3271
+ {
3272
+ sessionId,
3273
+ turnId: active.turnId,
3274
+ callId: newToolCallId(toolName),
3275
+ toolName,
3276
+ emit: active.emit,
3277
+ },
3278
+ fn
3279
+ );
3280
+ }
3281
+
2834
3282
  // ==========================================================================
2835
3283
  // Direct tool calls (deterministic invocation, no model turn)
2836
3284
  // ==========================================================================
@@ -2846,8 +3294,10 @@ export class SessionEngine {
2846
3294
  * and materialized workspace) and the call is recorded on the session's
2847
3295
  * event stream as `actions.requested` / `action.result` under a
2848
3296
  * per-call `turnId` (the call id), so it shows up in trajectories.
2849
- * Rejected with {@link SessionBusyError} while a model turn is running.
2850
- * Without a session, the tool runs against an **ephemeral** scratch
3297
+ * While a model turn is running, a read-effect call runs alongside it
3298
+ * (the turn's workspace is read as-is) and a write-effect call is
3299
+ * rejected with {@link SessionBusyError}. Without a session, the tool
3300
+ * runs against an **ephemeral** scratch
2851
3301
  * workspace under `<stateRoot>/tool-calls/<callId>` (materialized with
2852
3302
  * seed files, removed after the call returns).
2853
3303
  *
@@ -2873,6 +3323,12 @@ export class SessionEngine {
2873
3323
  session: SessionInfo;
2874
3324
  workspaceDir: string;
2875
3325
  scratchDir: string | undefined;
3326
+ /**
3327
+ * A model turn is running or queued on the session. Reported, not
3328
+ * enforced: what it means depends on the call — a write waits, a read
3329
+ * (including the catalog) runs alongside the turn.
3330
+ */
3331
+ busy: boolean;
2876
3332
  }> {
2877
3333
  if (
2878
3334
  options.sessionId !== undefined &&
@@ -2898,9 +3354,9 @@ export class SessionEngine {
2898
3354
  );
2899
3355
  }
2900
3356
  // A token that maps to a live (or restorable) session takes the
2901
- // session path: same owner check, same turn serialization, same
2902
- // event recording as an explicit sessionId. Skipping those just
2903
- // because the caller addressed the session by token would fail open.
3357
+ // session path: same owner check, same busy semantics, same event
3358
+ // recording as an explicit sessionId. Skipping those just because
3359
+ // the caller addressed the session by token would fail open.
2904
3360
  record =
2905
3361
  (await this.sessions.findByContinuation(
2906
3362
  continuation.channelId,
@@ -2915,19 +3371,14 @@ export class SessionEngine {
2915
3371
  }
2916
3372
  }
2917
3373
  if (record !== undefined) {
2918
- // Direct calls respect the same serialization as follow-up sends: a
2919
- // running model turn owns the session workspace and event stream.
2920
- if (
2921
- this.activeTurns.has(record.sessionId) ||
2922
- this.pendingTurns.has(record.sessionId)
2923
- ) {
2924
- throw new SessionBusyError(record.sessionId);
2925
- }
2926
3374
  return {
2927
3375
  record,
2928
3376
  session: this.sessionInfo(record),
2929
3377
  workspaceDir: this.harnessCwd(record),
2930
3378
  scratchDir: undefined,
3379
+ busy:
3380
+ this.activeTurns.has(record.sessionId) ||
3381
+ this.pendingTurns.has(record.sessionId),
2931
3382
  };
2932
3383
  }
2933
3384
  // No session behind the token (or no token at all): a synthetic
@@ -2947,6 +3398,7 @@ export class SessionEngine {
2947
3398
  },
2948
3399
  workspaceDir: scratchDir,
2949
3400
  scratchDir,
3401
+ busy: false,
2950
3402
  };
2951
3403
  }
2952
3404
 
@@ -2989,6 +3441,43 @@ export class SessionEngine {
2989
3441
  };
2990
3442
  }
2991
3443
 
3444
+ /**
3445
+ * One catalog tool by name. Authored tools are local — their schema does
3446
+ * not depend on session identity or connection listings — so a detail
3447
+ * fetch for `ls` must not wait on every advertised MCP server.
3448
+ */
3449
+ async describeCatalogTool(
3450
+ toolName: string,
3451
+ options: Pick<
3452
+ CallToolOptions,
3453
+ "auth" | "sessionId" | "continuationToken"
3454
+ > = {}
3455
+ ): Promise<
3456
+ | { tool: DiscoveredTool }
3457
+ | {
3458
+ tool: undefined;
3459
+ availableTools: string[];
3460
+ connectionErrors: Array<{ connection: string; message: string }>;
3461
+ }
3462
+ > {
3463
+ const authored = this.project.agent.tools.find(
3464
+ (tool) => tool.name === toolName
3465
+ );
3466
+ if (authored !== undefined) {
3467
+ return { tool: authored };
3468
+ }
3469
+ const catalog = await this.listCatalogTools(options);
3470
+ const tool = catalog.tools.find((entry) => entry.name === toolName);
3471
+ if (tool === undefined) {
3472
+ return {
3473
+ tool: undefined,
3474
+ availableTools: catalog.tools.map((entry) => entry.name),
3475
+ connectionErrors: catalog.connectionErrors,
3476
+ };
3477
+ }
3478
+ return { tool };
3479
+ }
3480
+
2992
3481
  async callTool(
2993
3482
  toolName: string,
2994
3483
  input: unknown = {},
@@ -3015,7 +3504,7 @@ export class SessionEngine {
3015
3504
  const advertised = await resolveAdvertisedTools({
3016
3505
  agent: this.project.agent,
3017
3506
  session,
3018
- hostMcp: this.mcpHost,
3507
+ hostMcp: this.advertiseMcp(),
3019
3508
  });
3020
3509
  tool = advertised.find((t) => t.name === toolName);
3021
3510
  }
@@ -3035,6 +3524,19 @@ export class SessionEngine {
3035
3524
  if (!validation.ok) {
3036
3525
  throw new ToolInputError({ toolName, detail: validation.message });
3037
3526
  }
3527
+ // A running model turn owns the session workspace: a direct write would
3528
+ // race the harness's own edits, so it waits (409). A read runs alongside
3529
+ // the turn — the operator inspecting a live session is the point — and
3530
+ // its events stay separable by their per-call turnId. The effect can
3531
+ // depend on the input, hence deciding after validation; an MCP tool
3532
+ // nothing classifies is a write here, fail-closed.
3533
+ if (
3534
+ identity.busy &&
3535
+ record !== undefined &&
3536
+ directCallEffect(tool, validation.value) !== "read"
3537
+ ) {
3538
+ throw new SessionBusyError(record.sessionId);
3539
+ }
3038
3540
  let workspaceUnavailable = false;
3039
3541
  try {
3040
3542
  await mkdir(workspaceDir, { recursive: true });
@@ -3053,35 +3555,59 @@ export class SessionEngine {
3053
3555
  // to write) authored tool may rely on ctx.workspaceDir being the
3054
3556
  // real session workspace, so silently relocating it is not safe —
3055
3557
  // fail typed instead.
3558
+ //
3559
+ // A shared `local.workspaceDir` (change-monitor's phantom /repo) is
3560
+ // that path by design. Relocating it to scratch would flag a false
3561
+ // session-state warning; keep the borrowed cwd and skip the flag.
3056
3562
  const relocatable =
3057
3563
  tool.advertised === true ||
3058
- resolveEffect(tool.definition.effect, validation.value) === "read";
3564
+ directCallEffect(tool, validation.value) === "read";
3059
3565
  if (!relocatable) {
3060
3566
  throw new WorkspaceUnavailableError(toolName, error);
3061
3567
  }
3062
- this.logger(
3063
- `[session] tool ${toolName} workspace unavailable (direct ${callId}); using scratch: ${
3064
- error instanceof Error ? error.message : String(error)
3065
- }`
3066
- );
3067
- scratchDir = join(this.stateRoot, "tool-calls", callId);
3068
- workspaceDir = scratchDir;
3069
- workspaceUnavailable = true;
3070
- await mkdir(workspaceDir, { recursive: true });
3568
+ if (this.project.agent.local?.workspaceDir !== undefined) {
3569
+ this.logger(
3570
+ `[session] tool ${toolName} harness cwd unavailable (direct ${callId}); keeping borrowed workspace`
3571
+ );
3572
+ } else {
3573
+ this.logger(
3574
+ `[session] tool ${toolName} workspace unavailable (direct ${callId}); using scratch: ${
3575
+ error instanceof Error ? error.message : String(error)
3576
+ }`
3577
+ );
3578
+ scratchDir = join(this.stateRoot, "tool-calls", callId);
3579
+ workspaceDir = scratchDir;
3580
+ workspaceUnavailable = true;
3581
+ await mkdir(workspaceDir, { recursive: true });
3582
+ }
3071
3583
  }
3072
3584
  // Match what a model turn would see: session workspaces get the same
3073
3585
  // scaffold as executeTurn (seeds only before the first turn); scratch
3074
- // workspaces are materialized fresh.
3075
- await materializeWorkspace({
3076
- agent: this.project.agent,
3077
- workspaceDir:
3078
- record === undefined || workspaceUnavailable
3079
- ? workspaceDir
3080
- : this.scaffoldDir(record),
3081
- seed:
3082
- record === undefined || workspaceUnavailable || record.turnCount === 0,
3083
- stateRoot: this.stateRoot,
3084
- });
3586
+ // workspaces are materialized fresh. A read running alongside a turn
3587
+ // that has materialized its workspace leaves it alone — re-seeding
3588
+ // would restore files the agent deleted mid-turn. A session is busy
3589
+ // from admit, though, so a read that lands while the first turn is
3590
+ // still queued or scaffolding materializes the workspace itself, seeds
3591
+ // included (turnCount is already 1 for that pending first turn).
3592
+ const turnMaterialized =
3593
+ record !== undefined &&
3594
+ identity.busy &&
3595
+ this.materializedWorkspaces.has(record.sessionId);
3596
+ if (record === undefined || workspaceUnavailable || !turnMaterialized) {
3597
+ await materializeWorkspace({
3598
+ agent: this.project.agent,
3599
+ workspaceDir:
3600
+ record === undefined || workspaceUnavailable
3601
+ ? workspaceDir
3602
+ : this.scaffoldDir(record),
3603
+ seed:
3604
+ record === undefined ||
3605
+ workspaceUnavailable ||
3606
+ record.turnCount === 0 ||
3607
+ (identity.busy && record.turnCount === 1),
3608
+ stateRoot: this.stateRoot,
3609
+ });
3610
+ }
3085
3611
 
3086
3612
  // A per-call turnId keeps these events visible in trajectories (which
3087
3613
  // drop turn-less action events) without attributing them to a model turn.
@@ -3104,6 +3630,8 @@ export class SessionEngine {
3104
3630
  : {}),
3105
3631
  dryRunResult: tool.definition.dryRunResult,
3106
3632
  input: validation.value,
3633
+ asOf: record?.asOf,
3634
+ timeArgs: tool.definition.timeArgs,
3107
3635
  });
3108
3636
 
3109
3637
  const started = Date.now();
@@ -3122,7 +3650,13 @@ export class SessionEngine {
3122
3650
  }).catch(() => {});
3123
3651
  };
3124
3652
  try {
3125
- if (decision.kind === "refuse") {
3653
+ if (decision.kind === "unbounded") {
3654
+ this.logger(
3655
+ `[session] tool ${toolName} refused, unbounded_time (direct ${callId})`
3656
+ );
3657
+ result = normalizeToolResult(toolErrorResult(decision.message));
3658
+ isError = true;
3659
+ } else if (decision.kind === "refuse") {
3126
3660
  this.logger(
3127
3661
  `[session] tool ${toolName} refused, effect unclassified (direct ${callId})`
3128
3662
  );
@@ -3139,8 +3673,8 @@ export class SessionEngine {
3139
3673
  result = normalizeToolResult(decision.result);
3140
3674
  isError = false;
3141
3675
  } else {
3142
- result = normalizeToolResult(
3143
- await execute(validation.value as never, {
3676
+ const run = () =>
3677
+ execute(validation.value as never, {
3144
3678
  toolCallId: callId,
3145
3679
  session,
3146
3680
  workspaceDir,
@@ -3165,7 +3699,21 @@ export class SessionEngine {
3165
3699
  getSession: (channelId, sessionId) =>
3166
3700
  this.getChannelSession(channelId, sessionId),
3167
3701
  emit,
3168
- })
3702
+ now: sessionClock(record?.asOf),
3703
+ });
3704
+ result = normalizeToolResult(
3705
+ record !== undefined && emit !== undefined
3706
+ ? await mcpAuthAls.run(
3707
+ {
3708
+ sessionId: record.sessionId,
3709
+ turnId: callId,
3710
+ callId,
3711
+ toolName,
3712
+ emit,
3713
+ },
3714
+ run
3715
+ )
3716
+ : await run()
3169
3717
  );
3170
3718
  isError = isErrorEnvelope(result);
3171
3719
  }
@@ -3263,6 +3811,12 @@ export class SessionEngine {
3263
3811
  if (decision.kind === "run") {
3264
3812
  return { answered: false };
3265
3813
  }
3814
+ if (decision.kind === "unbounded") {
3815
+ this.logger(
3816
+ `[session] bridged tool ${args.toolName} refused, unbounded_time (${args.sessionId})`
3817
+ );
3818
+ return { answered: true, result: toolErrorResult(decision.message) };
3819
+ }
3266
3820
  if (decision.kind === "refuse") {
3267
3821
  this.logger(
3268
3822
  `[session] bridged tool ${args.toolName} refused, effect unclassified (${args.sessionId})`
@@ -3585,6 +4139,75 @@ export class SessionEngine {
3585
4139
  };
3586
4140
  }
3587
4141
 
4142
+ /**
4143
+ * The single newest session visible to `caller` (`GET /v1/sessions?latest=1`).
4144
+ *
4145
+ * Short-circuits the full listing: the local store already holds every
4146
+ * session this host created plus the startup bulk restore, so the common
4147
+ * case answers from memory without a sink round trip. Only when the local
4148
+ * store has nothing for the caller (fresh host, `restore: "off"`) does it
4149
+ * ask the sink — for a bounded newest-written window when the backend
4150
+ * can order by write time, else the full list. A session written by
4151
+ * another replica moments ago can lose to a local one; callers that need
4152
+ * the exact newest row should use the full listing.
4153
+ */
4154
+ async latestSessionWithSource(
4155
+ caller: AuthContext | null,
4156
+ options?: { includeAll?: boolean }
4157
+ ): Promise<{ sessions: SessionSummary[]; source: SessionListSource }> {
4158
+ const includeAll = options?.includeAll ?? false;
4159
+ const newestVisible = (
4160
+ records: readonly SessionRecord[]
4161
+ ): SessionRecord | undefined => {
4162
+ let newest: SessionRecord | undefined;
4163
+ for (const record of records) {
4164
+ if (!(includeAll || samePrincipal(record.auth, caller))) {
4165
+ continue;
4166
+ }
4167
+ if (
4168
+ newest === undefined ||
4169
+ record.updatedAt.localeCompare(newest.updatedAt) > 0
4170
+ ) {
4171
+ newest = record;
4172
+ }
4173
+ }
4174
+ return newest;
4175
+ };
4176
+ const local = newestVisible(await this.sessions.list());
4177
+ if (local !== undefined) {
4178
+ return {
4179
+ sessions: this.summarizeSessionRecords([local], caller, includeAll),
4180
+ source: "local",
4181
+ };
4182
+ }
4183
+ if (this.storage?.isCursorHosted === true) {
4184
+ try {
4185
+ const window = await this.storage.listNewestSessionsFromSink(
4186
+ LATEST_SESSION_SINK_WINDOW
4187
+ );
4188
+ if (window !== undefined) {
4189
+ const stored = newestVisible(window);
4190
+ if (stored !== undefined) {
4191
+ return {
4192
+ sessions: this.summarizeSessionRecords(
4193
+ [stored],
4194
+ caller,
4195
+ includeAll
4196
+ ),
4197
+ source: "cursor-hosted",
4198
+ };
4199
+ }
4200
+ }
4201
+ } catch (error) {
4202
+ this.logger(
4203
+ `[agent-sdk] cursor-hosted newest-session lookup failed; falling back to the full list: ${describeError(error)}`
4204
+ );
4205
+ }
4206
+ }
4207
+ const listed = await this.listSessionsWithSource(caller, options);
4208
+ return { sessions: listed.sessions.slice(0, 1), source: listed.source };
4209
+ }
4210
+
3588
4211
  /**
3589
4212
  * Durable sink rows plus local records (local wins on the same id when
3590
4213
  * newer — covers debounce windows that have not flushed yet). Newest
@@ -3638,6 +4261,7 @@ export class SessionEngine {
3638
4261
  : undefined,
3639
4262
  createdAt: record.createdAt,
3640
4263
  updatedAt: record.updatedAt,
4264
+ dryRun: record.dryRun === true ? true : undefined,
3641
4265
  continuationToken:
3642
4266
  record.httpContinuationKey !== undefined
3643
4267
  ? formatContinuationToken(
@@ -3967,7 +4591,8 @@ export class SessionEngine {
3967
4591
  channelId: record.channelId,
3968
4592
  mode: record.mode,
3969
4593
  purpose: record.purpose === "eval" ? "eval" : "live",
3970
- ...(record.dryRun === true ? { dryRun: true } : {}),
4594
+ dryRun: record.dryRun === true ? true : undefined,
4595
+ asOf: record.asOf,
3971
4596
  auth: record.auth,
3972
4597
  continuationKey: record.continuationKey,
3973
4598
  ...(record.abs === undefined ? {} : { abs: record.abs }),
@@ -4340,6 +4965,8 @@ export class SessionEngine {
4340
4965
  async close(): Promise<void> {
4341
4966
  this.stopMemoryTelemetry?.();
4342
4967
  this.approvals.cancelAll("deny");
4968
+ this.auths.cancelAll("deny");
4969
+ await this.oauthHub.close();
4343
4970
  await Promise.allSettled([...this.activeTurns.values()]);
4344
4971
  await Promise.allSettled([...this.backgroundWork]);
4345
4972
  await Promise.allSettled([...this.dispatchChains.values()]);
@@ -4357,6 +4984,13 @@ export class SessionEngine {
4357
4984
  /** Parallel event-stream loads during the startup bulk restore. */
4358
4985
  const RESTORE_FETCH_CONCURRENCY = 8;
4359
4986
 
4987
+ /**
4988
+ * Newest-written sink rows fetched for `GET /v1/sessions?latest=1` when the
4989
+ * local store has nothing for the caller. Wide enough that the caller's own
4990
+ * session is normally inside it even when other principals are busier.
4991
+ */
4992
+ const LATEST_SESSION_SINK_WINDOW = 50;
4993
+
4360
4994
  /** NDJSON payload for a restored event stream; undefined when empty. */
4361
4995
  function mergePendingImages(
4362
4996
  current: Array<{ data: string; mimeType: string }> | undefined,