@fastagent-sh/fastagent 0.21.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +9 -12
  2. package/dist/agent.d.ts +24 -37
  3. package/dist/agent.js +7 -15
  4. package/dist/atomic-write.d.ts +13 -17
  5. package/dist/atomic-write.js +18 -29
  6. package/dist/bind.d.ts +12 -22
  7. package/dist/bind.js +18 -27
  8. package/dist/channel.d.ts +11 -33
  9. package/dist/channels/agentcore-limits.d.ts +2 -13
  10. package/dist/channels/agentcore-limits.js +2 -13
  11. package/dist/channels/agentcore-protocol.d.ts +67 -58
  12. package/dist/channels/agentcore-protocol.js +23 -15
  13. package/dist/channels/agentcore-service.d.ts +19 -33
  14. package/dist/channels/agentcore-service.js +176 -59
  15. package/dist/channels/agentcore.d.ts +39 -33
  16. package/dist/channels/agentcore.js +90 -224
  17. package/dist/channels/body.d.ts +15 -2
  18. package/dist/channels/body.js +24 -2
  19. package/dist/channels/busy.d.ts +1 -21
  20. package/dist/channels/busy.js +2 -39
  21. package/dist/channels/control.d.ts +17 -62
  22. package/dist/channels/control.js +74 -182
  23. package/dist/channels/define-channel.d.ts +39 -0
  24. package/dist/channels/define-channel.js +9 -0
  25. package/dist/channels/discover.d.ts +10 -31
  26. package/dist/channels/discover.js +37 -31
  27. package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
  28. package/dist/channels/feishu/bootstrap-token.js +7 -23
  29. package/dist/channels/feishu/card.d.ts +14 -28
  30. package/dist/channels/feishu/card.js +16 -30
  31. package/dist/channels/feishu/cloud.d.ts +1 -5
  32. package/dist/channels/feishu/context-buffer.d.ts +4 -23
  33. package/dist/channels/feishu/context-buffer.js +8 -31
  34. package/dist/channels/feishu/crypto.d.ts +10 -5
  35. package/dist/channels/feishu/crypto.js +12 -19
  36. package/dist/channels/feishu/feishu-api.d.ts +2 -1
  37. package/dist/channels/feishu/feishu-api.js +5 -4
  38. package/dist/channels/feishu/feishu.d.ts +4 -10
  39. package/dist/channels/feishu/feishu.js +113 -227
  40. package/dist/channels/feishu/invoke-turn.d.ts +5 -4
  41. package/dist/channels/feishu/invoke-turn.js +36 -54
  42. package/dist/channels/feishu/model.d.ts +4 -21
  43. package/dist/channels/feishu/model.js +1 -8
  44. package/dist/channels/feishu/normalize.d.ts +2 -10
  45. package/dist/channels/feishu/normalize.js +7 -42
  46. package/dist/channels/feishu/parse.d.ts +9 -29
  47. package/dist/channels/feishu/parse.js +8 -26
  48. package/dist/channels/feishu/preview.d.ts +12 -14
  49. package/dist/channels/feishu/preview.js +108 -207
  50. package/dist/channels/feishu/register-app.d.ts +8 -23
  51. package/dist/channels/feishu/register-app.js +7 -20
  52. package/dist/channels/feishu/register-webhook.d.ts +2 -9
  53. package/dist/channels/feishu/register-webhook.js +9 -33
  54. package/dist/channels/feishu/scaffold/channel.ts +27 -19
  55. package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
  56. package/dist/channels/feishu/setup-mode.d.ts +11 -36
  57. package/dist/channels/feishu/setup-mode.js +7 -26
  58. package/dist/channels/feishu/ws-ingress.d.ts +1 -2
  59. package/dist/channels/feishu/ws-ingress.js +6 -8
  60. package/dist/channels/http.d.ts +15 -15
  61. package/dist/channels/http.js +19 -9
  62. package/dist/channels/kit/attachment-path.d.ts +4 -5
  63. package/dist/channels/kit/attachment-path.js +10 -34
  64. package/dist/channels/kit/context-buffer.d.ts +14 -21
  65. package/dist/channels/kit/context-buffer.js +25 -38
  66. package/dist/channels/kit/delivery.d.ts +35 -0
  67. package/dist/channels/kit/delivery.js +129 -0
  68. package/dist/channels/kit/event-stream.d.ts +5 -0
  69. package/dist/channels/kit/event-stream.js +29 -0
  70. package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
  71. package/dist/channels/kit/invoke-turn-kit.js +89 -32
  72. package/dist/channels/kit/preview-kit.d.ts +18 -72
  73. package/dist/channels/kit/preview-kit.js +22 -88
  74. package/dist/channels/kit/signature.d.ts +3 -9
  75. package/dist/channels/kit/signature.js +3 -9
  76. package/dist/channels/kit/state.d.ts +1 -2
  77. package/dist/channels/kit/state.js +5 -14
  78. package/dist/channels/kit/stop-command.d.ts +1 -2
  79. package/dist/channels/kit/stop-command.js +6 -9
  80. package/dist/channels/kit/tasks.d.ts +1 -1
  81. package/dist/channels/kit/tasks.js +8 -21
  82. package/dist/channels/kit/text.d.ts +2 -16
  83. package/dist/channels/kit/text.js +23 -31
  84. package/dist/channels/kit/thread-participants.d.ts +4 -17
  85. package/dist/channels/kit/thread-participants.js +8 -55
  86. package/dist/channels/kit/transport.d.ts +26 -0
  87. package/dist/channels/kit/transport.js +20 -0
  88. package/dist/channels/kit/turn-queue.d.ts +7 -11
  89. package/dist/channels/kit/turn-queue.js +21 -39
  90. package/dist/channels/kit/turn-runner.d.ts +32 -18
  91. package/dist/channels/kit/turn-runner.js +98 -53
  92. package/dist/channels/kit/turn-store.d.ts +32 -32
  93. package/dist/channels/kit/turn-store.js +45 -88
  94. package/dist/channels/lark/lark.d.ts +1 -5
  95. package/dist/channels/lark/onboard.d.ts +3 -12
  96. package/dist/channels/lark/onboard.js +3 -13
  97. package/dist/channels/lark/scaffold/channel.ts +27 -19
  98. package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
  99. package/dist/channels/registration.d.ts +9 -35
  100. package/dist/channels/registration.js +6 -37
  101. package/dist/channels/secret.js +2 -5
  102. package/dist/channels/serve.d.ts +47 -44
  103. package/dist/channels/serve.js +165 -101
  104. package/dist/channels/slack/config-api.d.ts +4 -27
  105. package/dist/channels/slack/config-api.js +5 -34
  106. package/dist/channels/slack/context-buffer.d.ts +2 -3
  107. package/dist/channels/slack/context-buffer.js +5 -5
  108. package/dist/channels/slack/invoke-turn.d.ts +3 -1
  109. package/dist/channels/slack/invoke-turn.js +25 -34
  110. package/dist/channels/slack/manifest.js +1 -3
  111. package/dist/channels/slack/model.d.ts +1 -2
  112. package/dist/channels/slack/model.js +1 -1
  113. package/dist/channels/slack/onboard.d.ts +4 -3
  114. package/dist/channels/slack/onboard.js +8 -11
  115. package/dist/channels/slack/onboarding-state.d.ts +3 -5
  116. package/dist/channels/slack/onboarding-state.js +9 -6
  117. package/dist/channels/slack/parse.d.ts +4 -13
  118. package/dist/channels/slack/parse.js +7 -28
  119. package/dist/channels/slack/preview.d.ts +12 -2
  120. package/dist/channels/slack/preview.js +292 -269
  121. package/dist/channels/slack/reaction.d.ts +11 -3
  122. package/dist/channels/slack/reaction.js +19 -8
  123. package/dist/channels/slack/register-webhook.js +4 -7
  124. package/dist/channels/slack/scaffold/channel.ts +20 -13
  125. package/dist/channels/slack/scaffold/slack-send.ts +2 -2
  126. package/dist/channels/slack/setup-server.d.ts +1 -5
  127. package/dist/channels/slack/setup-server.js +1 -5
  128. package/dist/channels/slack/shared-api.d.ts +1 -4
  129. package/dist/channels/slack/shared-api.js +2 -13
  130. package/dist/channels/slack/slack-api.d.ts +2 -1
  131. package/dist/channels/slack/slack-api.js +6 -4
  132. package/dist/channels/slack/slack.d.ts +11 -14
  133. package/dist/channels/slack/slack.js +121 -122
  134. package/dist/channels/sse.d.ts +1 -1
  135. package/dist/channels/sse.js +1 -1
  136. package/dist/channels/telegram/context-buffer.d.ts +7 -22
  137. package/dist/channels/telegram/context-buffer.js +6 -23
  138. package/dist/channels/telegram/invoke-turn.d.ts +7 -7
  139. package/dist/channels/telegram/invoke-turn.js +24 -43
  140. package/dist/channels/telegram/parse.d.ts +24 -51
  141. package/dist/channels/telegram/parse.js +32 -84
  142. package/dist/channels/telegram/preview.d.ts +10 -1
  143. package/dist/channels/telegram/preview.js +79 -100
  144. package/dist/channels/telegram/register-webhook.js +5 -23
  145. package/dist/channels/telegram/scaffold/channel.ts +25 -18
  146. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
  147. package/dist/channels/telegram/telegram-api.d.ts +4 -3
  148. package/dist/channels/telegram/telegram-api.js +9 -7
  149. package/dist/channels/telegram/telegram.d.ts +4 -18
  150. package/dist/channels/telegram/telegram.js +38 -94
  151. package/dist/channels/telegram/turn-store.d.ts +11 -14
  152. package/dist/channels/telegram/turn-store.js +8 -6
  153. package/dist/channels/wait-health.d.ts +4 -6
  154. package/dist/channels/wait-health.js +10 -14
  155. package/dist/cli/add-feishu.d.ts +2 -12
  156. package/dist/cli/add-feishu.js +24 -71
  157. package/dist/cli/add-slack.d.ts +5 -3
  158. package/dist/cli/add-slack.js +53 -76
  159. package/dist/cli/auth-view.d.ts +2 -16
  160. package/dist/cli/auth-view.js +2 -16
  161. package/dist/cli/commands/add.js +25 -40
  162. package/dist/cli/commands/chat.d.ts +0 -1
  163. package/dist/cli/commands/chat.js +5 -6
  164. package/dist/cli/commands/deploy/agentcore.js +60 -65
  165. package/dist/cli/commands/deploy/docker.js +50 -23
  166. package/dist/cli/commands/deploy/fly.js +23 -61
  167. package/dist/cli/commands/deploy/railway.js +23 -18
  168. package/dist/cli/commands/deploy/shared.d.ts +38 -57
  169. package/dist/cli/commands/deploy/shared.js +40 -59
  170. package/dist/cli/commands/deploy.d.ts +10 -27
  171. package/dist/cli/commands/deploy.js +38 -60
  172. package/dist/cli/commands/destroy.d.ts +4 -0
  173. package/dist/cli/commands/destroy.js +56 -0
  174. package/dist/cli/commands/dev.d.ts +2 -1
  175. package/dist/cli/commands/dev.js +20 -23
  176. package/dist/cli/commands/info.d.ts +0 -2
  177. package/dist/cli/commands/info.js +83 -49
  178. package/dist/cli/commands/init.d.ts +1 -2
  179. package/dist/cli/commands/init.js +16 -55
  180. package/dist/cli/commands/invoke.d.ts +0 -1
  181. package/dist/cli/commands/invoke.js +5 -7
  182. package/dist/cli/commands/login.d.ts +2 -1
  183. package/dist/cli/commands/login.js +33 -38
  184. package/dist/cli/commands/logs.js +3 -4
  185. package/dist/cli/commands/models.js +1 -5
  186. package/dist/cli/commands/routine-run.d.ts +6 -0
  187. package/dist/cli/commands/routine-run.js +47 -0
  188. package/dist/cli/commands/routine.d.ts +25 -0
  189. package/dist/cli/commands/routine.js +122 -0
  190. package/dist/cli/commands/start.d.ts +31 -3
  191. package/dist/cli/commands/start.js +131 -81
  192. package/dist/cli/commands/tool.js +26 -12
  193. package/dist/cli/fail.d.ts +13 -23
  194. package/dist/cli/fail.js +20 -23
  195. package/dist/cli/invoke-stream.d.ts +1 -8
  196. package/dist/cli/kernel.d.ts +6 -38
  197. package/dist/cli/kernel.js +14 -77
  198. package/dist/cli/models-view.d.ts +1 -9
  199. package/dist/cli/models-view.js +11 -22
  200. package/dist/cli/program.d.ts +5 -6
  201. package/dist/cli/program.js +120 -157
  202. package/dist/cli/serve.d.ts +57 -41
  203. package/dist/cli/serve.js +94 -121
  204. package/dist/cli/shared.d.ts +15 -61
  205. package/dist/cli/shared.js +55 -181
  206. package/dist/cli.js +1 -8
  207. package/dist/collect.d.ts +6 -0
  208. package/dist/collect.js +6 -0
  209. package/dist/core.d.ts +4 -1
  210. package/dist/core.js +9 -7
  211. package/dist/declared-secrets.d.ts +55 -0
  212. package/dist/declared-secrets.js +88 -0
  213. package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
  214. package/dist/deploy/agentcore/aws-cli.js +64 -0
  215. package/dist/deploy/agentcore/destroy.d.ts +51 -0
  216. package/dist/deploy/agentcore/destroy.js +273 -0
  217. package/dist/deploy/agentcore/forwarder.js +28 -79
  218. package/dist/deploy/agentcore/logs.d.ts +2 -18
  219. package/dist/deploy/agentcore/logs.js +27 -51
  220. package/dist/deploy/agentcore/plan.d.ts +85 -105
  221. package/dist/deploy/agentcore/plan.js +152 -314
  222. package/dist/deploy/agentcore/run.d.ts +28 -28
  223. package/dist/deploy/agentcore/run.js +227 -306
  224. package/dist/deploy/agentcore/zip.d.ts +2 -15
  225. package/dist/deploy/agentcore/zip.js +3 -17
  226. package/dist/deploy/build-context.d.ts +21 -0
  227. package/dist/deploy/build-context.js +137 -0
  228. package/dist/deploy/channel-ingress.d.ts +26 -39
  229. package/dist/deploy/channel-ingress.js +57 -28
  230. package/dist/deploy/container.d.ts +20 -34
  231. package/dist/deploy/container.js +49 -100
  232. package/dist/deploy/docker/plan.d.ts +12 -11
  233. package/dist/deploy/docker/plan.js +43 -24
  234. package/dist/deploy/docker/run.d.ts +22 -25
  235. package/dist/deploy/docker/run.js +39 -37
  236. package/dist/deploy/fly/plan.d.ts +15 -44
  237. package/dist/deploy/fly/plan.js +41 -73
  238. package/dist/deploy/fly/run.d.ts +16 -51
  239. package/dist/deploy/fly/run.js +42 -95
  240. package/dist/deploy/hosts.d.ts +24 -3
  241. package/dist/deploy/hosts.js +24 -3
  242. package/dist/deploy/preflight.d.ts +47 -41
  243. package/dist/deploy/preflight.js +282 -383
  244. package/dist/deploy/railway/plan.d.ts +16 -42
  245. package/dist/deploy/railway/plan.js +42 -60
  246. package/dist/deploy/railway/run.d.ts +30 -68
  247. package/dist/deploy/railway/run.js +86 -100
  248. package/dist/deploy/registration-gate.d.ts +6 -13
  249. package/dist/deploy/residency.d.ts +37 -0
  250. package/dist/deploy/residency.js +24 -0
  251. package/dist/deploy/runner.d.ts +10 -9
  252. package/dist/deploy/runner.js +7 -18
  253. package/dist/deploy/secrets.d.ts +40 -41
  254. package/dist/deploy/secrets.js +117 -103
  255. package/dist/deploy/workspace.d.ts +50 -0
  256. package/dist/deploy/workspace.js +197 -0
  257. package/dist/dev-supervisor.d.ts +1 -9
  258. package/dist/dev-supervisor.js +21 -64
  259. package/dist/effect-port.d.ts +50 -0
  260. package/dist/effect-port.js +77 -0
  261. package/dist/engines/pi/agent-session-factory.d.ts +50 -69
  262. package/dist/engines/pi/agent-session-factory.js +249 -146
  263. package/dist/engines/pi/auth.d.ts +15 -11
  264. package/dist/engines/pi/auth.js +96 -99
  265. package/dist/engines/pi/chat.d.ts +1 -5
  266. package/dist/engines/pi/chat.js +2 -14
  267. package/dist/engines/pi/config.d.ts +53 -51
  268. package/dist/engines/pi/config.js +83 -92
  269. package/dist/engines/pi/create.d.ts +60 -75
  270. package/dist/engines/pi/create.js +108 -141
  271. package/dist/engines/pi/definition.d.ts +14 -58
  272. package/dist/engines/pi/definition.js +21 -85
  273. package/dist/engines/pi/invoke-session.d.ts +5 -25
  274. package/dist/engines/pi/invoke-session.js +209 -244
  275. package/dist/engines/pi/login.d.ts +95 -35
  276. package/dist/engines/pi/login.js +259 -107
  277. package/dist/engines/pi/machine.d.ts +31 -0
  278. package/dist/engines/pi/machine.js +106 -0
  279. package/dist/engines/pi/models.d.ts +70 -89
  280. package/dist/engines/pi/models.js +188 -81
  281. package/dist/engines/pi/open.d.ts +65 -53
  282. package/dist/engines/pi/open.js +129 -69
  283. package/dist/engines/pi/report.d.ts +3 -12
  284. package/dist/engines/pi/report.js +2 -16
  285. package/dist/engines/pi/search-tools.d.ts +1 -9
  286. package/dist/engines/pi/search-tools.js +26 -57
  287. package/dist/engines/pi/service.d.ts +2 -16
  288. package/dist/engines/pi/service.js +2 -16
  289. package/dist/engines/pi/session-builder.d.ts +5 -9
  290. package/dist/engines/pi/session-builder.js +44 -82
  291. package/dist/engines/pi/session-control.js +286 -269
  292. package/dist/engines/pi/session-effects.d.ts +14 -0
  293. package/dist/engines/pi/session-effects.js +30 -0
  294. package/dist/engines/pi/session-inheritance.d.ts +9 -18
  295. package/dist/engines/pi/session-inheritance.js +103 -160
  296. package/dist/engines/pi/session-markers.d.ts +31 -30
  297. package/dist/engines/pi/session-markers.js +26 -25
  298. package/dist/engines/pi/session-settings.d.ts +6 -22
  299. package/dist/engines/pi/session-settings.js +10 -21
  300. package/dist/engines/pi/session-store.d.ts +26 -92
  301. package/dist/engines/pi/session-store.js +96 -310
  302. package/dist/engines/pi/tool-context.d.ts +17 -45
  303. package/dist/engines/pi/tool-context.js +7 -26
  304. package/dist/engines/pi/tool.d.ts +45 -36
  305. package/dist/engines/pi/tool.js +43 -57
  306. package/dist/engines/pi/turn-kit.d.ts +14 -52
  307. package/dist/engines/pi/turn-kit.js +18 -67
  308. package/dist/engines/pi/wake-tool.d.ts +2 -10
  309. package/dist/engines/pi/wake-tool.js +7 -27
  310. package/dist/env.d.ts +27 -28
  311. package/dist/env.js +45 -50
  312. package/dist/index.js +0 -2
  313. package/dist/lark.d.ts +1 -2
  314. package/dist/lark.js +1 -2
  315. package/dist/loader.d.ts +14 -55
  316. package/dist/loader.js +24 -43
  317. package/dist/log.d.ts +2 -12
  318. package/dist/log.js +3 -24
  319. package/dist/node.d.ts +2 -15
  320. package/dist/node.js +2 -18
  321. package/dist/observe.d.ts +2 -8
  322. package/dist/open-url.d.ts +1 -1
  323. package/dist/open-url.js +1 -1
  324. package/dist/paths.d.ts +70 -134
  325. package/dist/paths.js +165 -232
  326. package/dist/pi.d.ts +4 -6
  327. package/dist/pi.js +3 -3
  328. package/dist/proxy.d.ts +16 -6
  329. package/dist/proxy.js +52 -16
  330. package/dist/runtime.d.ts +18 -12
  331. package/dist/runtime.js +18 -11
  332. package/dist/scaffold/add-channel.d.ts +14 -31
  333. package/dist/scaffold/add-channel.js +48 -112
  334. package/dist/scaffold/init.d.ts +9 -31
  335. package/dist/scaffold/init.js +45 -138
  336. package/dist/scaffold/templates/env.example +6 -4
  337. package/dist/scaffold/templates/fastagent.config.ts +36 -0
  338. package/dist/scaffold/templates.d.ts +3 -3
  339. package/dist/scaffold/templates.js +5 -8
  340. package/dist/scaffold/vendor-skill.d.ts +2 -5
  341. package/dist/scaffold/vendor-skill.js +19 -29
  342. package/dist/schedule/cron.d.ts +9 -5
  343. package/dist/schedule/cron.js +9 -11
  344. package/dist/schedule/discover.d.ts +12 -12
  345. package/dist/schedule/discover.js +43 -34
  346. package/dist/schedule/routine.d.ts +43 -0
  347. package/dist/schedule/routine.js +32 -0
  348. package/dist/schedule/run.d.ts +55 -0
  349. package/dist/schedule/run.js +146 -0
  350. package/dist/schedule/scheduler.d.ts +44 -55
  351. package/dist/schedule/scheduler.js +291 -265
  352. package/dist/schedule/state.d.ts +105 -10
  353. package/dist/schedule/state.js +293 -19
  354. package/dist/schedule/wake-alarm.d.ts +10 -23
  355. package/dist/schedule/wake-alarm.js +91 -121
  356. package/dist/schedule/wakeups.d.ts +23 -39
  357. package/dist/schedule/wakeups.js +33 -75
  358. package/dist/secrets-gate.d.ts +32 -0
  359. package/dist/secrets-gate.js +33 -0
  360. package/dist/service.d.ts +137 -98
  361. package/dist/service.js +250 -252
  362. package/dist/session-remote.d.ts +33 -44
  363. package/dist/session-remote.js +203 -192
  364. package/dist/session.d.ts +153 -228
  365. package/dist/session.js +33 -55
  366. package/dist/tunnel.d.ts +8 -36
  367. package/dist/tunnel.js +24 -65
  368. package/dist/version.d.ts +2 -2
  369. package/dist/version.js +2 -2
  370. package/package.json +10 -16
  371. package/dist/channels/agentcore-state.d.ts +0 -77
  372. package/dist/channels/agentcore-state.js +0 -276
  373. package/dist/channels/github/github.d.ts +0 -31
  374. package/dist/channels/github/github.js +0 -89
  375. package/dist/channels/github/scaffold/channel.ts +0 -25
  376. package/dist/cli/commands/attach.d.ts +0 -108
  377. package/dist/cli/commands/attach.js +0 -637
  378. package/dist/cli/commands/fire.d.ts +0 -7
  379. package/dist/cli/commands/fire.js +0 -38
  380. package/dist/cli/commands/schedule.d.ts +0 -12
  381. package/dist/cli/commands/schedule.js +0 -87
  382. package/dist/github.d.ts +0 -3
  383. package/dist/github.js +0 -2
  384. package/dist/scaffold/templates/fastagent.config.mjs +0 -23
  385. package/dist/schedule/audit.d.ts +0 -18
  386. package/dist/schedule/audit.js +0 -57
  387. package/dist/schedule/schedule.d.ts +0 -35
  388. package/dist/schedule/schedule.js +0 -23
@@ -1,50 +1,94 @@
1
1
  import { SSE_HEARTBEAT_MS } from "./channels/sse.js";
2
2
  import { abortFirstIterator } from "./collect.js";
3
3
  import { isAddressableSession, } from "./session.js";
4
- /** Dead-connection watchdog for SSE reads: the server heartbeats every SSE_HEARTBEAT_MS, so a
5
- * PENDING READ seeing no bytes (of ANY kind — comments included) for this many missed beats
6
- * means the connection is a black hole. The stream is aborted and surfaced as an error, so a
7
- * consumer's failure budget ticks instead of hanging forever. Quiet-but-alive streams (a long
8
- * tool call) keep heartbeating and never trip this. */
4
+ /**
5
+ * Dead-connection watchdog for SSE reads: the server heartbeats every SSE_HEARTBEAT_MS, so a PENDING READ seeing no
6
+ * bytes (of ANY kind — comments included) for this many missed beats means the connection is a black hole.
7
+ */
9
8
  const SSE_IDLE_LIMIT_MS = 3 * SSE_HEARTBEAT_MS;
10
- function idleWatchdog(abort) {
11
- let armedAt;
12
- let stale = false;
13
- const timer = setInterval(() => {
14
- if (armedAt !== undefined && Date.now() - armedAt > SSE_IDLE_LIMIT_MS) {
15
- stale = true;
16
- abort.abort();
17
- }
18
- }, SSE_HEARTBEAT_MS);
9
+ /**
10
+ * Every request carries a TIMEOUT: a client's reliability model counts failed rounds against a budget ("unreachable
11
+ * for ~Ns"), which a black-hole endpoint (firewall drop, half-dead tunnel) would silently defeat — a hung
12
+ * state()/entries(), or a stream that never finishes connecting, ticks nothing.
13
+ */
14
+ const REQUEST_TIMEOUT_MS = 10_000;
15
+ /** The PAYLOAD-bearing calls get a longer budget than the black-hole detector's 10s. */
16
+ const PAYLOAD_TIMEOUT_MS = 60_000;
17
+ class StreamEnded extends Error {
18
+ kind;
19
+ constructor(kind, message) {
20
+ super(message);
21
+ this.name = "StreamEnded";
22
+ this.kind = kind;
23
+ }
24
+ }
25
+ /** The reason this connection was ended, when one was given — `undefined` for any other failure. */
26
+ function endedBecause(signal) {
27
+ return signal.reason instanceof StreamEnded ? signal.reason : undefined;
28
+ }
29
+ function readBudget(abort, what, connectMs) {
30
+ let timer;
31
+ let connected = false;
32
+ const expire = () => abort.abort(connected
33
+ ? new StreamEnded("idle", `${what}: no bytes for ${SSE_IDLE_LIMIT_MS / 1000}s (heartbeats absent) — dead connection`)
34
+ : new StreamEnded("connect-timeout", `${what}: no usable response in ${connectMs / 1000}s — the endpoint accepted the connection and never completed one`));
35
+ const disarm = () => {
36
+ clearTimeout(timer);
37
+ timer = undefined;
38
+ };
19
39
  return {
40
+ // `??=`: a nested arm does not restart the clock, so one long read cannot be extended by re-arming inside it.
20
41
  arm: () => {
21
- armedAt ??= Date.now();
42
+ timer ??= setTimeout(expire, connected ? SSE_IDLE_LIMIT_MS : connectMs);
22
43
  },
23
- disarm: () => {
24
- armedAt = undefined;
44
+ disarm,
45
+ connected: () => {
46
+ disarm();
47
+ connected = true;
25
48
  },
26
- stale: () => stale,
27
- stop: () => clearInterval(timer),
28
49
  };
29
50
  }
30
- /** A control request the server answered with a non-2xx status. Carries the STRUCTURED status so a
31
- * consumer distinguishing auth failure (401 — stale token, unrecoverable) from transient transport
32
- * trouble branches on `status`, never on message prose. */
51
+ /**
52
+ * Open a streaming response — the one connect sequence both wire planes perform, under one budget: the headers, the
53
+ * error body of a non-2xx (a half-dead tunnel answering 4xx and then black-holing the body is the case that makes
54
+ * this one phase rather than two), and the check that a body exists at all. It returns only once the connection is
55
+ * something that can carry events, which is the moment the budget switches to the idle limit.
56
+ */
57
+ async function openStreamBody(options) {
58
+ const { fetchFn, url, init, abort, budget, what } = options;
59
+ budget.arm(); // the connect await is a pending read
60
+ const res = await fetchFn(url, { ...init, signal: abort.signal });
61
+ if (!res.ok)
62
+ throw new ControlRequestError(res.status, await res.text());
63
+ if (!res.body)
64
+ throw new Error(`${what}: response has no body`);
65
+ budget.connected();
66
+ return res.body;
67
+ }
68
+ /** A control request the server answered with a non-2xx status. */
33
69
  export class ControlRequestError extends Error {
34
70
  status;
35
- /** The plane's own error code, when the reply carried one (`sessions()` is the only read that
36
- * does today — design §13). Absent for a plain-text rejection (401) or a proxy's page: a caller
37
- * distinguishing "this deployment cannot list sessions" from "the endpoint is unreachable" reads
38
- * THIS, not the status. */
71
+ /**
72
+ * The plane's own error code, when the reply carried one (`sessions()` is the only read that does today — design
73
+ * §13).
74
+ */
39
75
  code;
40
- constructor(status, body, code) {
76
+ /**
77
+ * Whether re-sending is worth it, as the plane itself answered — the same question `SessionResult.retryable`
78
+ * answers for a write, carried here because a throwing read has no result to put it in. Absent when the reply
79
+ * declared no opinion; a caller then has only the status.
80
+ */
81
+ retryable;
82
+ constructor(status, body, declared) {
41
83
  super(`control request failed: ${status} ${body}`);
42
84
  this.status = status;
43
- if (code !== undefined)
44
- this.code = code;
85
+ if (declared?.code !== undefined)
86
+ this.code = declared.code;
87
+ if (declared?.retryable !== undefined)
88
+ this.retryable = declared.retryable;
45
89
  }
46
90
  }
47
- /** A non-2xx reply as an error, carrying the plane's code when the reply declared one. */
91
+ /** A non-2xx reply as an error, carrying what the plane declared about it. */
48
92
  async function controlError(res) {
49
93
  const body = await res.text();
50
94
  if (!res.headers.get("content-type")?.includes("application/json"))
@@ -54,77 +98,73 @@ async function controlError(res) {
54
98
  parsed = JSON.parse(body);
55
99
  }
56
100
  catch {
57
- // The reply declared JSON and is not — a protocol fault worth seeing, but not worth losing the
58
- // status over: both travel in one error rather than a bare SyntaxError from a rejection path.
101
+ // The reply declared JSON and is not — a protocol fault worth seeing, but not worth losing the status over.
59
102
  return new ControlRequestError(res.status, `${body} (declared application/json but did not parse)`);
60
103
  }
61
- return new ControlRequestError(res.status, body, typeof parsed?.code === "string" ? parsed.code : undefined);
104
+ return new ControlRequestError(res.status, body, {
105
+ ...(typeof parsed?.code === "string" ? { code: parsed.code } : {}),
106
+ ...(typeof parsed?.retryable === "boolean" ? { retryable: parsed.retryable } : {}),
107
+ });
62
108
  }
63
- /**
64
- * Connect and return a remote `SessionControl`. Async because `capabilities()` is synchronous in
65
- * the contract: the static declaration is fetched ONCE here and served from memory — which also
66
- * makes a wrong URL/token fail at connect time, not on first use.
67
- */
68
109
  export async function connectSessionControl(options) {
69
- const { url, token, fetchFn = fetch } = options;
110
+ const { url, fetchFn = fetch } = options;
70
111
  const base = url.replace(/\/$/, "");
71
- const headers = { authorization: `Bearer ${token}` };
72
- // Non-streaming requests carry a TIMEOUT: attach's whole reliability model counts failed rounds
73
- // against a budget ("unreachable for ~Ns"), which a black-hole endpoint (firewall drop, half-dead
74
- // tunnel) would silently defeat — a hung state()/entries() ticks nothing. The SSE stream stays
75
- // timeout-free (quiet is normal there; heartbeats cover proxy idling).
76
- const REQUEST_TIMEOUT_MS = 10_000;
77
- // The PAYLOAD-bearing calls get a longer budget than the black-hole detector's 10s — in both
78
- // directions: dispatch may UPLOAD up to the 1 MiB body cap (base64 images in steer/follow_up),
79
- // and entries may DOWNLOAD a long session's full record (a cursor-less first backfill) — a slow
80
- // link legitimately needs longer, and cutting a healthy transfer would be indistinguishable from
81
- // a dead endpoint. capabilities/state stay on the short detector: they are small by contract.
82
- const PAYLOAD_TIMEOUT_MS = 60_000;
83
112
  const get = async (path, timeoutMs = REQUEST_TIMEOUT_MS) => {
84
- const res = await fetchFn(`${base}${path}`, { headers, signal: AbortSignal.timeout(timeoutMs) });
113
+ const res = await fetchFn(`${base}${path}`, { signal: AbortSignal.timeout(timeoutMs) });
85
114
  if (!res.ok)
86
115
  throw await controlError(res);
87
116
  return (await res.json());
88
117
  };
89
118
  const capabilities = await get("/control/capabilities");
90
119
  const eventsOf = (session) => {
91
- // Each ITERATION opens its own connection (gen/abort created inside asyncIterator), matching
92
- // the local hub's "every iteration is a fresh subscription" — a shared single-use generator
93
- // would make the second for-await silently empty, breaking local/remote isomorphism.
94
- // The abort controller lives OUTSIDE the generator: a consumer's `return()`/`break` while the
95
- // generator is suspended on a quiet SSE read must abort the fetch FIRST — an async generator's
96
- // own finally only runs after the pending await settles, which a silent stream never does.
120
+ // The server subscribes BEFORE it writes the response headers (channels/sse.ts pulls the source once first), so
121
+ // "headers arrived, 2xx" IS "this session's subscription exists" — the boundary a reconnecting client awaits
122
+ // before reading history (session.ts, SessionEventStream).
123
+ let subscribed;
124
+ let unreachable;
125
+ const ready = new Promise((resolve, reject) => {
126
+ subscribed = resolve;
127
+ unreachable = reject;
128
+ });
129
+ // A consumer that never awaits `ready` (it can iterate and let the stream report the same failure) would
130
+ // otherwise take an unhandled rejection for a connection that legitimately failed. Awaiters still get it.
131
+ ready.catch(() => { });
132
+ // ONE CALL IS ONE SUBSCRIPTION — one connection, one readiness. A second iteration would open a second
133
+ // connection and inherit this `ready`, reporting a boundary the first one crossed; it is refused instead, the
134
+ // same way the local hub refuses it.
97
135
  const openStream = (abort) => (async function* iterate() {
98
- // Armed BEFORE the fetch: the connect phase (headers never arriving from a black-holed
99
- // endpoint) is otherwise a window no timeout covers — the same watchdog terminates it,
100
- // with headers-arrival counting as the first sign of life.
101
- const watchdog = idleWatchdog(abort);
102
- watchdog.arm(); // the connect await is a pending read
136
+ const budget = readBudget(abort, "control events", REQUEST_TIMEOUT_MS);
103
137
  try {
104
- const res = await fetchFn(`${base}/control/sessions/${encodeURIComponent(session)}/events`, {
105
- headers,
106
- signal: abort.signal,
107
- });
108
- watchdog.disarm(); // headers arrived
109
- if (!res.ok) {
110
- // The error body is a pending read too — a half-dead tunnel serving 4xx headers then
111
- // black-holing the body must not hang the round outside every budget. Re-armed: the
112
- // watchdog aborts the read and the round fails with the dead-connection diagnosis.
113
- watchdog.arm();
114
- throw new ControlRequestError(res.status, await res.text());
138
+ let body;
139
+ try {
140
+ body = await openStreamBody({
141
+ fetchFn,
142
+ url: `${base}/control/sessions/${encodeURIComponent(session)}/events`,
143
+ init: {},
144
+ abort,
145
+ budget,
146
+ what: "control events",
147
+ });
115
148
  }
116
- if (!res.body)
117
- throw new Error("control events: response has no body");
149
+ catch (error) {
150
+ // The subscription was never established: the endpoint is unreachable or never completed a response, a
151
+ // gateway in front refused it, the consumer cancelled first. Whoever ended it said why, and `fetch` rejected with
152
+ // that very object. A waiter on `ready` must learn it instead of waiting out a stream that will never
153
+ // carry anything — including the cancellation, which the ITERATION reports as a clean end (walking away
154
+ // is not an error) while `ready` still has a promise it cannot keep.
155
+ const failure = endedBecause(abort.signal) ?? error;
156
+ unreachable(failure);
157
+ throw failure;
158
+ }
159
+ subscribed();
118
160
  let nextSeq = 0;
119
- for await (const data of sseData(res.body, watchdog)) {
120
- // Parse discipline, same as the other two wire planes (dispatch parses, invoke
121
- // classifies drift): a non-JSON or non-envelope payload is PROTOCOL MISMATCH —
122
- // thrown, so a consumer's failure budget applies — never misdiagnosed as an
123
- // in-transit gap whose remedy (reconnect) can never fix it.
161
+ for await (const data of sseData(body, budget)) {
162
+ // Parse discipline, same as the other two wire planes (dispatch parses, invoke classifies drift): a
163
+ // non-JSON or non-envelope payload is PROTOCOL MISMATCH.
124
164
  let wire;
125
165
  try {
126
- // The ONE envelope type (control.ts's WireEvent) — an inline shape would let the
127
- // envelope drift server-side while this cast silently kept the old fields.
166
+ // The ONE envelope type (control.ts's WireEvent) — an inline shape would let the envelope drift
167
+ // server-side while this cast silently kept the old fields.
128
168
  wire = JSON.parse(data);
129
169
  }
130
170
  catch (parseError) {
@@ -138,11 +178,7 @@ export async function connectSessionControl(options) {
138
178
  typeof wire.event.type !== "string") {
139
179
  throw new Error("control events: malformed envelope — the endpoint does not speak this protocol version");
140
180
  }
141
- // Envelope checks — consumed HERE. (epoch is not compared: it cannot change within
142
- // one connection — see the header note.) A gap THROWS like a protocol mismatch: the
143
- // consumer's failure path (budget, its own io) owns the diagnostic — a library-level
144
- // log would bypass consumer output discipline, and a silent clean end would be
145
- // indistinguishable from the server closing normally.
181
+ // Envelope checks — consumed HERE.
146
182
  if (wire.seq !== nextSeq) {
147
183
  throw new Error(`control events: sequence gap (expected ${nextSeq}, got ${wire.seq}) — events were lost in transit; resync via entries()`);
148
184
  }
@@ -151,34 +187,48 @@ export async function connectSessionControl(options) {
151
187
  }
152
188
  }
153
189
  catch (error) {
154
- if (abort.signal.aborted) {
155
- if (watchdog.stale()) {
156
- throw new Error(`control events: no bytes for ${SSE_IDLE_LIMIT_MS / 1000}s (heartbeats absent) — dead connection; resync via entries()`);
157
- }
190
+ const ended = endedBecause(abort.signal);
191
+ if (ended?.kind === "cancelled")
158
192
  return; // the consumer walked away — clean end, not an error
159
- }
160
- throw error;
193
+ throw ended ?? error;
161
194
  }
162
195
  finally {
163
- watchdog.stop();
196
+ budget.disarm();
164
197
  abort.abort();
165
198
  }
166
199
  })();
200
+ let iterated = false;
167
201
  return {
202
+ ready,
168
203
  [Symbol.asyncIterator]() {
204
+ if (iterated) {
205
+ throw new Error("control events: this stream is one subscription — call events() again for another");
206
+ }
207
+ iterated = true;
169
208
  const abort = new AbortController();
170
- // Abort-first cancellation (see abortFirstIterator): aborting the connection unblocks a
171
- // generator suspended on a quiet stream read.
172
- return abortFirstIterator(openStream(abort), () => abort.abort());
209
+ // Abort-first cancellation (see abortFirstIterator): aborting the connection unblocks a generator suspended
210
+ // on a quiet stream read.
211
+ return abortFirstIterator(openStream(abort), () => {
212
+ const cancelled = new StreamEnded("cancelled", "control events: cancelled by the consumer");
213
+ abort.abort(cancelled);
214
+ // Said HERE as well as in the generator's catch, because a generator that was never pulled does not run its
215
+ // body on `return()` — and then nothing else would ever settle `ready`. Rejecting an already-settled
216
+ // promise is a no-op, so the connected case still reports whatever ended it.
217
+ unreachable(cancelled);
218
+ });
173
219
  },
174
220
  };
175
221
  };
176
- /** A write that answers a `SessionResult`: the result rides HTTP 200 either way (`ok: false` is a
177
- * protocol answer, not a transport failure), so a non-2xx here is a REAL transport/auth fault. */
222
+ /**
223
+ * A write that answers a `SessionResult`: the result rides HTTP 200 either way (`ok: false` is a protocol answer,
224
+ * not a transport failure), so a non-2xx here is a REAL transport fault.
225
+ */
178
226
  const write = async (path, method, body) => {
227
+ // Always declared, body or not: the plane refuses a write it was not told is JSON (channels/body.ts), and an
228
+ // empty `PATCH` is a legal no-op that still has to get through.
179
229
  const res = await fetchFn(`${base}${path}`, {
180
230
  method,
181
- headers: body === undefined ? headers : { ...headers, "content-type": "application/json" },
231
+ headers: { "content-type": "application/json" },
182
232
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
183
233
  signal: AbortSignal.timeout(PAYLOAD_TIMEOUT_MS),
184
234
  });
@@ -189,12 +239,8 @@ export async function connectSessionControl(options) {
189
239
  const id = (session) => encodeURIComponent(session);
190
240
  return {
191
241
  capabilities: () => capabilities,
192
- // NOT prefetched like capabilities: a live definition can grow a skill between calls, so the
193
- // list is fetched per call. The endpoint is UNCACHED server-side (it re-reads the definition's
194
- // skills/ per request), which is what keeps it honest about a directory that changes underneath
195
- // it. A 404 is SKEW, not a fault in the definition: without this the two read identically
196
- // (uncoded non-2xx), and a client would report "this agent's skills are unreadable" about a
197
- // serve that simply predates the route.
242
+ // NOT prefetched like capabilities: a live definition can grow a skill between calls, so the list is fetched per
243
+ // call.
198
244
  async commands() {
199
245
  try {
200
246
  return await get("/control/commands");
@@ -207,16 +253,9 @@ export async function connectSessionControl(options) {
207
253
  }
208
254
  },
209
255
  sessions: {
210
- // Rejects when the deployment cannot enumerate its store — the coded 503 arrives as a
211
- // ControlRequestError carrying `sessions_unavailable`, so a client can tell it from an
212
- // unreachable endpoint instead of retrying forever.
256
+ // Rejects when the deployment cannot enumerate its store.
213
257
  list: () => get("/control/sessions", PAYLOAD_TIMEOUT_MS),
214
- // PUT: the fork is idempotent, and so is the request that carries it. `into` becomes a path
215
- // segment exactly like `get`'s id, so it is refused on the same rule — without this the local
216
- // plane answers `invalid_command` while the wire answers 404 from a URL that normalised away.
217
- // ASYNC, so the guard REJECTS rather than throwing out of a method typed `Promise`: a caller
218
- // that wrote `.catch(…)` — or handed this to `Promise.all` — must not be surprised by a
219
- // synchronous throw. (`get` may throw: it is synchronous by signature.)
258
+ // PUT: the fork is idempotent, and so is the request that carries it.
220
259
  fork: async ({ from, at, into }) => {
221
260
  if (!isAddressableSession(into)) {
222
261
  throw new Error(`session id ${JSON.stringify(into)} cannot travel as a URL path segment — this transport cannot address it`);
@@ -224,12 +263,6 @@ export async function connectSessionControl(options) {
224
263
  return write(`/control/sessions/${id(into)}`, "PUT", { from, at });
225
264
  },
226
265
  // The local hub's handle is a pure binding; so is this one — an id and the transport above it.
227
- // Nothing is FETCHED here, which is what keeps the two isomorphic. What is checked is the one
228
- // thing the wire cannot express: `.` and `..` survive `encodeURIComponent` and are then
229
- // normalised away by URL parsing, so every call on such a handle would arrive at a DIFFERENT
230
- // route — `.` reads as the collection (200 JSON, which the SSE reader ends as a silently empty
231
- // stream) and `..` as a 404 the local plane answers normally. Refused at the binding, where a
232
- // caller can see it, rather than once per call in a place it looks like a server answer.
233
266
  get: (session) => {
234
267
  if (!isAddressableSession(session)) {
235
268
  throw new Error(`session id ${JSON.stringify(session)} cannot travel as a URL path segment — this transport cannot address it`);
@@ -254,18 +287,12 @@ export async function connectSessionControl(options) {
254
287
  };
255
288
  }
256
289
  /**
257
- * The remote DATA plane: an `Agent` whose `invoke` drives `POST /control/invoke` on a serving
258
- * process — paired with {@link connectSessionControl}, a client holds a full remote fastagent
259
- * instance through the same two contracts local code uses. A REAL Agent, failure discipline
260
- * included: SPEC MUST 2 forbids iteration throws, so every failure — transport (401/refused/
261
- * dropped mid-stream), protocol, and the images precheck — becomes a terminal `failed` event
262
- * (`retryable` from the HTTP status where one exists; network trouble is retryable). Breaking out
263
- * of iteration disconnects the request, which cancels the run (SPEC cancellation semantics travel
264
- * the wire). The invoke wire is text-only for now: a prompt with images fails visibly instead of
265
- * silently dropping them (steer/follow_up on the control plane carry full Prompts).
290
+ * The remote DATA plane: an `Agent` whose `invoke` drives `POST /invoke` on a serving process. A real Agent,
291
+ * failure discipline included — SPEC MUST 2 forbids iteration throws, so transport, protocol and precheck failures
292
+ * all become `failed` events.
266
293
  */
267
294
  export function connectAgent(options) {
268
- const { url, token, fetchFn = fetch } = options;
295
+ const { url, fetchFn = fetch } = options;
269
296
  const base = url.replace(/\/$/, "");
270
297
  const toFailed = (error) => {
271
298
  if (error instanceof ControlRequestError) {
@@ -273,13 +300,12 @@ export function connectAgent(options) {
273
300
  }
274
301
  return { type: "failed", details: String(error), retryable: true }; // network-class: worth re-sending
275
302
  };
276
- // COMPILE-TIME drift guard (dispatch-wire parity): the invoke body carries exactly text (and
277
- // rejects images visibly) — a new Prompt field must break THIS line and force a decision
278
- // (carry it or reject it), never vanish on the wire while the client believes it was sent.
303
+ // COMPILE-TIME drift guard (dispatch-wire parity): the invoke body carries exactly text (and rejects images
304
+ // visibly).
279
305
  const _invokeDriftGuard = {};
280
306
  void _invokeDriftGuard;
281
- // Same guard for Scope: the body carries session + the lineage extension — a new Scope field must
282
- // force a decision (carry it or reject it), never vanish on the wire.
307
+ // Same guard for Scope: the body carries session + the lineage extension — a new Scope field must force a decision
308
+ // (carry it or reject it), never vanish on the wire.
283
309
  const _scopeDriftGuard = {};
284
310
  void _scopeDriftGuard;
285
311
  return {
@@ -296,37 +322,33 @@ export function connectAgent(options) {
296
322
  }
297
323
  // A terminal closes the stream. Cleanup errors must not append a second terminal.
298
324
  let terminalSeen = false;
299
- // Armed BEFORE the fetch — the run's driver must not hang on a black-holed connect
300
- // either (the connect await is a pending read; headers arriving disarm it).
301
- const watchdog = idleWatchdog(abort);
302
- watchdog.arm();
325
+ // The run's driver rides the same mechanism as the events plane, with the connect limit its own callers
326
+ // need: a black-holed POST must not hang it, but a scale-to-zero host (fly `auto_start_machines`, a cold
327
+ // AgentCore container) can legitimately hold this open for tens of seconds before the first header, and
328
+ // that is a slow success, not a dead endpoint. A connect failure lands in the catch below, which is where
329
+ // every non-terminal failure becomes the one `failed` event this stream owes its caller.
330
+ const budget = readBudget(abort, "remote invoke", PAYLOAD_TIMEOUT_MS);
303
331
  try {
304
- const res = await fetchFn(`${base}/control/invoke`, {
305
- method: "POST",
306
- headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
307
- body: JSON.stringify({
308
- session: scope.session,
309
- text: prompt.text,
310
- // Lineage rides the wire so a remote thread scope inherits server-side; the server
311
- // reads it on the session-create path only, same as in-process.
312
- ...(scope.parentSession !== undefined ? { parentSession: scope.parentSession } : {}),
313
- ...(scope.branchHints !== undefined ? { branchHints: scope.branchHints } : {}),
314
- }),
315
- signal: abort.signal,
332
+ const body = await openStreamBody({
333
+ fetchFn,
334
+ url: `${base}/invoke`,
335
+ init: {
336
+ method: "POST",
337
+ headers: { "content-type": "application/json" },
338
+ body: JSON.stringify({
339
+ session: scope.session,
340
+ text: prompt.text,
341
+ // Lineage rides the wire so a remote thread scope inherits server-side; the server reads it on the
342
+ // session-create path only, same as in-process.
343
+ ...(scope.parentSession !== undefined ? { parentSession: scope.parentSession } : {}),
344
+ ...(scope.branchHints !== undefined ? { branchHints: scope.branchHints } : {}),
345
+ }),
346
+ },
347
+ abort,
348
+ budget,
349
+ what: "remote invoke",
316
350
  });
317
- watchdog.disarm(); // headers arrived
318
- if (!res.ok) {
319
- watchdog.arm(); // the error body is a pending read too — see the events() twin
320
- const failure = toFailed(new ControlRequestError(res.status, await res.text()));
321
- watchdog.disarm();
322
- yield failure;
323
- return;
324
- }
325
- if (!res.body) {
326
- yield { type: "failed", details: "remote invoke: response has no body", retryable: true };
327
- return;
328
- }
329
- for await (const data of sseData(res.body, watchdog)) {
351
+ for await (const data of sseData(body, budget)) {
330
352
  let event;
331
353
  try {
332
354
  event = JSON.parse(data);
@@ -339,9 +361,8 @@ export function connectAgent(options) {
339
361
  };
340
362
  return;
341
363
  }
342
- // Shape check, same discipline as the events plane: `data: null` / `data: 42` is
343
- // valid JSON but protocol drift — it must not TypeError into the catch below and be
344
- // misclassified as retryable network trouble.
364
+ // Shape check, same discipline as the events plane: `data: null` / `data: 42` is valid JSON but
365
+ // protocol drift.
345
366
  if (typeof event !== "object" || event === null || typeof event.type !== "string") {
346
367
  yield {
347
368
  type: "failed",
@@ -358,29 +379,23 @@ export function connectAgent(options) {
358
379
  yield { type: "failed", details: "remote invoke: stream ended without a terminal", retryable: true };
359
380
  }
360
381
  catch (error) {
361
- if (abort.signal.aborted) {
362
- if (watchdog.stale() && !terminalSeen) {
363
- yield {
364
- type: "failed",
365
- details: `remote invoke: no bytes for ${SSE_IDLE_LIMIT_MS / 1000}s (heartbeats absent) — dead connection`,
366
- retryable: true,
367
- };
368
- }
369
- return; // the consumer walked away — cancellation, not an error
370
- }
382
+ const ended = endedBecause(abort.signal);
383
+ // Cancellation is the consumer's own doing — reporting it back would be news to nobody. Any other stated
384
+ // reason is a connection that died under the run, and this stream still owes its caller a terminal —
385
+ // unless one was already sent, since a second would describe the run rather than the connection.
386
+ if (ended?.kind === "cancelled")
387
+ return;
371
388
  if (!terminalSeen)
372
- yield toFailed(error);
389
+ yield ended ? { type: "failed", details: ended.message, retryable: true } : toFailed(error);
373
390
  }
374
391
  finally {
375
- watchdog.stop();
392
+ budget.disarm();
376
393
  abort.abort();
377
394
  }
378
395
  })();
379
- // ONE stream per invoke, like a local async generator (which is its own iterator): a second
380
- // iteration must never re-POST — that would silently start a second run with the same prompt.
381
- // Abort-first cancellation (see abortFirstIterator): disconnect = cancel the run, even
382
- // while suspended on a quiet read.
383
- const iterator = abortFirstIterator(openStream(), () => abort.abort());
396
+ // ONE stream per invoke, like a local async generator (which is its own iterator): a second iteration must
397
+ // never re-POST.
398
+ const iterator = abortFirstIterator(openStream(), () => abort.abort(new StreamEnded("cancelled", "remote invoke: cancelled by the consumer")));
384
399
  return {
385
400
  [Symbol.asyncIterator]() {
386
401
  return iterator;
@@ -389,10 +404,7 @@ export function connectAgent(options) {
389
404
  },
390
405
  };
391
406
  }
392
- /** Minimal SSE reader: yields each `data:` payload; ignores comments (heartbeats) and other
393
- * fields. The explicit reader loop (not for-await) exists for the watchdog: armed strictly
394
- * around each pending read, so only "we are listening and nothing arrives" counts as idle — a
395
- * consumer pausing at a yield leaves the watch disarmed (see {@link ReadWatch}). */
407
+ /** Minimal SSE reader: yields each `data:` payload; ignores comments (heartbeats) and other fields. */
396
408
  async function* sseData(body, watch) {
397
409
  const decoder = new TextDecoder();
398
410
  let buffer = "";
@@ -404,8 +416,7 @@ async function* sseData(body, watch) {
404
416
  watch?.disarm(); // bytes (ANY bytes — heartbeats included) or a clean end arrived
405
417
  if (done)
406
418
  return;
407
- // SSE permits CRLF line endings (proxies/other servers may produce them); normalize AFTER
408
- // appending so a \r\n split across chunks still collapses once its second half arrives.
419
+ // SSE permits CRLF line endings (proxies/other servers may produce them).
409
420
  buffer = (buffer + decoder.decode(value, { stream: true })).replace(/\r\n/g, "\n");
410
421
  let sep = buffer.indexOf("\n\n");
411
422
  while (sep !== -1) {