@qodo/sdk 0.13.4 → 2.0.0-next.1

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 (764) hide show
  1. package/LICENSE +31 -118
  2. package/README.md +133 -121
  3. package/bin/qodo-skills.mjs +13 -0
  4. package/bundled-skills/code-review/SKILL.md +41 -0
  5. package/bundled-skills/pr-summary/SKILL.md +59 -0
  6. package/bundled-skills/test-gen/SKILL.md +47 -0
  7. package/dist/auth/index.browser.d.ts +38 -0
  8. package/dist/auth/index.browser.d.ts.map +1 -0
  9. package/dist/auth/index.browser.js +62 -0
  10. package/dist/auth/index.browser.js.map +1 -0
  11. package/dist/auth/index.d.ts +44 -30
  12. package/dist/auth/index.d.ts.map +1 -1
  13. package/dist/auth/index.js +57 -110
  14. package/dist/auth/index.js.map +1 -1
  15. package/dist/client/AgentsClient.d.ts +33 -0
  16. package/dist/client/AgentsClient.d.ts.map +1 -0
  17. package/dist/client/AgentsClient.js +40 -0
  18. package/dist/client/AgentsClient.js.map +1 -0
  19. package/dist/client/ArtifactsClient.d.ts +43 -0
  20. package/dist/client/ArtifactsClient.d.ts.map +1 -0
  21. package/dist/client/ArtifactsClient.js +54 -0
  22. package/dist/client/ArtifactsClient.js.map +1 -0
  23. package/dist/client/BulletinClient.d.ts +45 -0
  24. package/dist/client/BulletinClient.d.ts.map +1 -0
  25. package/dist/client/BulletinClient.js +51 -0
  26. package/dist/client/BulletinClient.js.map +1 -0
  27. package/dist/client/InfoClient.d.ts +58 -0
  28. package/dist/client/InfoClient.d.ts.map +1 -0
  29. package/dist/client/InfoClient.js +135 -0
  30. package/dist/client/InfoClient.js.map +1 -0
  31. package/dist/client/PipelineClient.d.ts +162 -0
  32. package/dist/client/PipelineClient.d.ts.map +1 -0
  33. package/dist/client/PipelineClient.js +340 -0
  34. package/dist/client/PipelineClient.js.map +1 -0
  35. package/dist/client/QarRegistryClient.d.ts +396 -0
  36. package/dist/client/QarRegistryClient.d.ts.map +1 -0
  37. package/dist/client/QarRegistryClient.js +536 -0
  38. package/dist/client/QarRegistryClient.js.map +1 -0
  39. package/dist/client/QodoClient.d.ts +296 -0
  40. package/dist/client/QodoClient.d.ts.map +1 -0
  41. package/dist/client/QodoClient.js +803 -0
  42. package/dist/client/QodoClient.js.map +1 -0
  43. package/dist/client/SpecsClient.d.ts +121 -0
  44. package/dist/client/SpecsClient.d.ts.map +1 -0
  45. package/dist/client/SpecsClient.js +252 -0
  46. package/dist/client/SpecsClient.js.map +1 -0
  47. package/dist/client/StateClient.d.ts +35 -0
  48. package/dist/client/StateClient.d.ts.map +1 -0
  49. package/dist/client/StateClient.js +36 -0
  50. package/dist/client/StateClient.js.map +1 -0
  51. package/dist/client/TaskClient.d.ts +706 -0
  52. package/dist/client/TaskClient.d.ts.map +1 -0
  53. package/dist/client/TaskClient.js +2522 -0
  54. package/dist/client/TaskClient.js.map +1 -0
  55. package/dist/client/ToolClient.d.ts +278 -0
  56. package/dist/client/ToolClient.d.ts.map +1 -0
  57. package/dist/client/ToolClient.js +1139 -0
  58. package/dist/client/ToolClient.js.map +1 -0
  59. package/dist/client/a2a/index.d.ts +10 -0
  60. package/dist/client/a2a/index.d.ts.map +1 -0
  61. package/dist/client/a2a/index.js +9 -0
  62. package/dist/client/a2a/index.js.map +1 -0
  63. package/dist/client/a2a/registerA2A.d.ts +170 -0
  64. package/dist/client/a2a/registerA2A.d.ts.map +1 -0
  65. package/dist/client/a2a/registerA2A.js +85 -0
  66. package/dist/client/a2a/registerA2A.js.map +1 -0
  67. package/dist/client/connection.d.ts +893 -0
  68. package/dist/client/connection.d.ts.map +1 -0
  69. package/dist/client/connection.js +2189 -0
  70. package/dist/client/connection.js.map +1 -0
  71. package/dist/client/errors.d.ts +735 -0
  72. package/dist/client/errors.d.ts.map +1 -0
  73. package/dist/client/errors.js +921 -0
  74. package/dist/client/errors.js.map +1 -0
  75. package/dist/client/index.d.ts +26 -0
  76. package/dist/client/index.d.ts.map +1 -0
  77. package/dist/client/index.js +20 -0
  78. package/dist/client/index.js.map +1 -0
  79. package/dist/client/inlineGraph.d.ts +66 -0
  80. package/dist/client/inlineGraph.d.ts.map +1 -0
  81. package/dist/client/inlineGraph.js +500 -0
  82. package/dist/client/inlineGraph.js.map +1 -0
  83. package/dist/client/internal/thenable.d.ts +27 -0
  84. package/dist/client/internal/thenable.d.ts.map +1 -0
  85. package/dist/client/internal/thenable.js +31 -0
  86. package/dist/client/internal/thenable.js.map +1 -0
  87. package/dist/client/iterator.d.ts +32 -0
  88. package/dist/client/iterator.d.ts.map +1 -0
  89. package/dist/client/iterator.js +73 -0
  90. package/dist/client/iterator.js.map +1 -0
  91. package/dist/client/mcp/McpClientPool.browser.d.ts +76 -0
  92. package/dist/client/mcp/McpClientPool.browser.d.ts.map +1 -0
  93. package/dist/client/mcp/McpClientPool.browser.js +78 -0
  94. package/dist/client/mcp/McpClientPool.browser.js.map +1 -0
  95. package/dist/client/mcp/McpClientPool.d.ts +236 -0
  96. package/dist/client/mcp/McpClientPool.d.ts.map +1 -0
  97. package/dist/client/mcp/McpClientPool.js +585 -0
  98. package/dist/client/mcp/McpClientPool.js.map +1 -0
  99. package/dist/client/mcp/projection.d.ts +109 -0
  100. package/dist/client/mcp/projection.d.ts.map +1 -0
  101. package/dist/client/mcp/projection.js +446 -0
  102. package/dist/client/mcp/projection.js.map +1 -0
  103. package/dist/client/mcp/substituteEnv.browser.d.ts +18 -0
  104. package/dist/client/mcp/substituteEnv.browser.d.ts.map +1 -0
  105. package/dist/client/mcp/substituteEnv.browser.js +20 -0
  106. package/dist/client/mcp/substituteEnv.browser.js.map +1 -0
  107. package/dist/client/mcp/substituteEnv.d.ts +45 -0
  108. package/dist/client/mcp/substituteEnv.d.ts.map +1 -0
  109. package/dist/client/mcp/substituteEnv.js +63 -0
  110. package/dist/client/mcp/substituteEnv.js.map +1 -0
  111. package/dist/client/observers.d.ts +57 -0
  112. package/dist/client/observers.d.ts.map +1 -0
  113. package/dist/client/observers.js +203 -0
  114. package/dist/client/observers.js.map +1 -0
  115. package/dist/client/options.d.ts +269 -0
  116. package/dist/client/options.d.ts.map +1 -0
  117. package/dist/client/options.js +9 -0
  118. package/dist/client/options.js.map +1 -0
  119. package/dist/client/tools/_readlineApprovalPrompt.browser.d.ts +17 -0
  120. package/dist/client/tools/_readlineApprovalPrompt.browser.d.ts.map +1 -0
  121. package/dist/client/tools/_readlineApprovalPrompt.browser.js +24 -0
  122. package/dist/client/tools/_readlineApprovalPrompt.browser.js.map +1 -0
  123. package/dist/client/tools/_readlineApprovalPrompt.d.ts +33 -0
  124. package/dist/client/tools/_readlineApprovalPrompt.d.ts.map +1 -0
  125. package/dist/client/tools/_readlineApprovalPrompt.js +90 -0
  126. package/dist/client/tools/_readlineApprovalPrompt.js.map +1 -0
  127. package/dist/client/tools/approval.d.ts +280 -0
  128. package/dist/client/tools/approval.d.ts.map +1 -0
  129. package/dist/client/tools/approval.js +229 -0
  130. package/dist/client/tools/approval.js.map +1 -0
  131. package/dist/client/tools/bindFunctionToolDefs.d.ts +156 -0
  132. package/dist/client/tools/bindFunctionToolDefs.d.ts.map +1 -0
  133. package/dist/client/tools/bindFunctionToolDefs.js +360 -0
  134. package/dist/client/tools/bindFunctionToolDefs.js.map +1 -0
  135. package/dist/client/tools/defineFunctionTool.d.ts +277 -0
  136. package/dist/client/tools/defineFunctionTool.d.ts.map +1 -0
  137. package/dist/client/tools/defineFunctionTool.js +190 -0
  138. package/dist/client/tools/defineFunctionTool.js.map +1 -0
  139. package/dist/client/transport.browser.d.ts +20 -0
  140. package/dist/client/transport.browser.d.ts.map +1 -0
  141. package/dist/client/transport.browser.js +29 -0
  142. package/dist/client/transport.browser.js.map +1 -0
  143. package/dist/client/transport.d.ts +47 -0
  144. package/dist/client/transport.d.ts.map +1 -0
  145. package/dist/client/transport.js +102 -0
  146. package/dist/client/transport.js.map +1 -0
  147. package/dist/client/transport.shared.d.ts +30 -0
  148. package/dist/client/transport.shared.d.ts.map +1 -0
  149. package/dist/client/transport.shared.js +40 -0
  150. package/dist/client/transport.shared.js.map +1 -0
  151. package/dist/client/uuid.d.ts +32 -0
  152. package/dist/client/uuid.d.ts.map +1 -0
  153. package/dist/client/uuid.js +65 -0
  154. package/dist/client/uuid.js.map +1 -0
  155. package/dist/index.d.ts +88 -39
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +166 -43
  158. package/dist/index.js.map +1 -1
  159. package/dist/observability/attributes.d.ts +136 -0
  160. package/dist/observability/attributes.d.ts.map +1 -0
  161. package/dist/observability/attributes.js +184 -0
  162. package/dist/observability/attributes.js.map +1 -0
  163. package/dist/observability/index.d.ts +14 -0
  164. package/dist/observability/index.d.ts.map +1 -0
  165. package/dist/observability/index.js +11 -0
  166. package/dist/observability/index.js.map +1 -0
  167. package/dist/observability/resolveOTel.browser.d.ts +13 -0
  168. package/dist/observability/resolveOTel.browser.d.ts.map +1 -0
  169. package/dist/observability/resolveOTel.browser.js +14 -0
  170. package/dist/observability/resolveOTel.browser.js.map +1 -0
  171. package/dist/observability/resolveOTel.d.ts +28 -0
  172. package/dist/observability/resolveOTel.d.ts.map +1 -0
  173. package/dist/observability/resolveOTel.js +74 -0
  174. package/dist/observability/resolveOTel.js.map +1 -0
  175. package/dist/observability/spans.d.ts +198 -0
  176. package/dist/observability/spans.d.ts.map +1 -0
  177. package/dist/observability/spans.js +300 -0
  178. package/dist/observability/spans.js.map +1 -0
  179. package/dist/observability/traceContext.d.ts +51 -0
  180. package/dist/observability/traceContext.d.ts.map +1 -0
  181. package/dist/observability/traceContext.js +151 -0
  182. package/dist/observability/traceContext.js.map +1 -0
  183. package/dist/observability/transportMetrics.d.ts +58 -0
  184. package/dist/observability/transportMetrics.d.ts.map +1 -0
  185. package/dist/observability/transportMetrics.js +55 -0
  186. package/dist/observability/transportMetrics.js.map +1 -0
  187. package/dist/qar/agentSpec.d.ts +93 -0
  188. package/dist/qar/agentSpec.d.ts.map +1 -0
  189. package/dist/qar/agentSpec.js +184 -0
  190. package/dist/qar/agentSpec.js.map +1 -0
  191. package/dist/qar/clientEvents.d.ts +86 -0
  192. package/dist/qar/clientEvents.d.ts.map +1 -0
  193. package/dist/qar/clientEvents.js +36 -0
  194. package/dist/qar/clientEvents.js.map +1 -0
  195. package/dist/qar/envelopes.d.ts +227 -0
  196. package/dist/qar/envelopes.d.ts.map +1 -0
  197. package/dist/qar/envelopes.js +67 -0
  198. package/dist/qar/envelopes.js.map +1 -0
  199. package/dist/qar/generated/envelope.d.ts +332 -0
  200. package/dist/qar/generated/envelope.d.ts.map +1 -0
  201. package/dist/qar/generated/envelope.js +15 -0
  202. package/dist/qar/generated/envelope.js.map +1 -0
  203. package/dist/qar/generated/qar-info.d.ts +76 -0
  204. package/dist/qar/generated/qar-info.d.ts.map +1 -0
  205. package/dist/qar/generated/qar-info.js +15 -0
  206. package/dist/qar/generated/qar-info.js.map +1 -0
  207. package/dist/qar/generated/qodo-task-start-payload.d.ts +54 -0
  208. package/dist/qar/generated/qodo-task-start-payload.d.ts.map +1 -0
  209. package/dist/qar/generated/qodo-task-start-payload.js +15 -0
  210. package/dist/qar/generated/qodo-task-start-payload.js.map +1 -0
  211. package/dist/qar/ids.d.ts +19 -0
  212. package/dist/qar/ids.d.ts.map +1 -0
  213. package/dist/qar/ids.js +11 -0
  214. package/dist/qar/ids.js.map +1 -0
  215. package/dist/qar/index.d.ts +24 -0
  216. package/dist/qar/index.d.ts.map +1 -0
  217. package/dist/qar/index.js +16 -0
  218. package/dist/qar/index.js.map +1 -0
  219. package/dist/qar/info.d.ts +37 -0
  220. package/dist/qar/info.d.ts.map +1 -0
  221. package/dist/qar/info.js +17 -0
  222. package/dist/qar/info.js.map +1 -0
  223. package/dist/qar/json.d.ts +14 -0
  224. package/dist/qar/json.d.ts.map +1 -0
  225. package/dist/qar/json.js +9 -0
  226. package/dist/qar/json.js.map +1 -0
  227. package/dist/qar/payloads.d.ts +480 -0
  228. package/dist/qar/payloads.d.ts.map +1 -0
  229. package/dist/qar/payloads.js +37 -0
  230. package/dist/qar/payloads.js.map +1 -0
  231. package/dist/qar/specs.d.ts +604 -0
  232. package/dist/qar/specs.d.ts.map +1 -0
  233. package/dist/qar/specs.js +29 -0
  234. package/dist/qar/specs.js.map +1 -0
  235. package/dist/qar/taskEvents.d.ts +25 -0
  236. package/dist/qar/taskEvents.d.ts.map +1 -0
  237. package/dist/qar/taskEvents.js +22 -0
  238. package/dist/qar/taskEvents.js.map +1 -0
  239. package/dist/qar/trace.d.ts +12 -0
  240. package/dist/qar/trace.d.ts.map +1 -0
  241. package/dist/qar/trace.js +12 -0
  242. package/dist/qar/trace.js.map +1 -0
  243. package/dist/skills/activation.d.ts +177 -0
  244. package/dist/skills/activation.d.ts.map +1 -0
  245. package/dist/skills/activation.js +428 -0
  246. package/dist/skills/activation.js.map +1 -0
  247. package/dist/skills/cli/index.browser.d.ts +18 -0
  248. package/dist/skills/cli/index.browser.d.ts.map +1 -0
  249. package/dist/skills/cli/index.browser.js +27 -0
  250. package/dist/skills/cli/index.browser.js.map +1 -0
  251. package/dist/skills/cli/index.d.ts +37 -0
  252. package/dist/skills/cli/index.d.ts.map +1 -0
  253. package/dist/skills/cli/index.js +494 -0
  254. package/dist/skills/cli/index.js.map +1 -0
  255. package/dist/skills/events.d.ts +255 -0
  256. package/dist/skills/events.d.ts.map +1 -0
  257. package/dist/skills/events.js +224 -0
  258. package/dist/skills/events.js.map +1 -0
  259. package/dist/skills/index.d.ts +45 -0
  260. package/dist/skills/index.d.ts.map +1 -0
  261. package/dist/skills/index.js +34 -0
  262. package/dist/skills/index.js.map +1 -0
  263. package/dist/skills/inject.d.ts +57 -0
  264. package/dist/skills/inject.d.ts.map +1 -0
  265. package/dist/skills/inject.js +162 -0
  266. package/dist/skills/inject.js.map +1 -0
  267. package/dist/skills/lockfile.browser.d.ts +56 -0
  268. package/dist/skills/lockfile.browser.d.ts.map +1 -0
  269. package/dist/skills/lockfile.browser.js +55 -0
  270. package/dist/skills/lockfile.browser.js.map +1 -0
  271. package/dist/skills/lockfile.d.ts +137 -0
  272. package/dist/skills/lockfile.d.ts.map +1 -0
  273. package/dist/skills/lockfile.js +423 -0
  274. package/dist/skills/lockfile.js.map +1 -0
  275. package/dist/skills/manager.browser.d.ts +94 -0
  276. package/dist/skills/manager.browser.d.ts.map +1 -0
  277. package/dist/skills/manager.browser.js +159 -0
  278. package/dist/skills/manager.browser.js.map +1 -0
  279. package/dist/skills/manager.d.ts +362 -0
  280. package/dist/skills/manager.d.ts.map +1 -0
  281. package/dist/skills/manager.js +1386 -0
  282. package/dist/skills/manager.js.map +1 -0
  283. package/dist/skills/mcp/index.d.ts +15 -0
  284. package/dist/skills/mcp/index.d.ts.map +1 -0
  285. package/dist/skills/mcp/index.js +12 -0
  286. package/dist/skills/mcp/index.js.map +1 -0
  287. package/dist/skills/mcp/path.browser.d.ts +27 -0
  288. package/dist/skills/mcp/path.browser.d.ts.map +1 -0
  289. package/dist/skills/mcp/path.browser.js +33 -0
  290. package/dist/skills/mcp/path.browser.js.map +1 -0
  291. package/dist/skills/mcp/path.d.ts +57 -0
  292. package/dist/skills/mcp/path.d.ts.map +1 -0
  293. package/dist/skills/mcp/path.js +150 -0
  294. package/dist/skills/mcp/path.js.map +1 -0
  295. package/dist/skills/mcp/server.browser.d.ts +32 -0
  296. package/dist/skills/mcp/server.browser.d.ts.map +1 -0
  297. package/dist/skills/mcp/server.browser.js +53 -0
  298. package/dist/skills/mcp/server.browser.js.map +1 -0
  299. package/dist/skills/mcp/server.d.ts +144 -0
  300. package/dist/skills/mcp/server.d.ts.map +1 -0
  301. package/dist/skills/mcp/server.js +841 -0
  302. package/dist/skills/mcp/server.js.map +1 -0
  303. package/dist/skills/mcp/types.d.ts +72 -0
  304. package/dist/skills/mcp/types.d.ts.map +1 -0
  305. package/dist/skills/mcp/types.js +20 -0
  306. package/dist/skills/mcp/types.js.map +1 -0
  307. package/dist/skills/mcp/wireDefs.d.ts +58 -0
  308. package/dist/skills/mcp/wireDefs.d.ts.map +1 -0
  309. package/dist/skills/mcp/wireDefs.js +141 -0
  310. package/dist/skills/mcp/wireDefs.js.map +1 -0
  311. package/dist/skills/parser.d.ts +63 -0
  312. package/dist/skills/parser.d.ts.map +1 -0
  313. package/dist/skills/parser.js +755 -0
  314. package/dist/skills/parser.js.map +1 -0
  315. package/dist/skills/prefilter.d.ts +104 -0
  316. package/dist/skills/prefilter.d.ts.map +1 -0
  317. package/dist/skills/prefilter.js +398 -0
  318. package/dist/skills/prefilter.js.map +1 -0
  319. package/dist/skills/preprocess.d.ts +169 -0
  320. package/dist/skills/preprocess.d.ts.map +1 -0
  321. package/dist/skills/preprocess.js +535 -0
  322. package/dist/skills/preprocess.js.map +1 -0
  323. package/dist/skills/render.d.ts +83 -0
  324. package/dist/skills/render.d.ts.map +1 -0
  325. package/dist/skills/render.js +397 -0
  326. package/dist/skills/render.js.map +1 -0
  327. package/dist/skills/sources/index.browser.d.ts +29 -0
  328. package/dist/skills/sources/index.browser.d.ts.map +1 -0
  329. package/dist/skills/sources/index.browser.js +16 -0
  330. package/dist/skills/sources/index.browser.js.map +1 -0
  331. package/dist/skills/sources/index.d.ts +59 -0
  332. package/dist/skills/sources/index.d.ts.map +1 -0
  333. package/dist/skills/sources/index.js +471 -0
  334. package/dist/skills/sources/index.js.map +1 -0
  335. package/dist/skills/sources/walk.browser.d.ts +17 -0
  336. package/dist/skills/sources/walk.browser.d.ts.map +1 -0
  337. package/dist/skills/sources/walk.browser.js +19 -0
  338. package/dist/skills/sources/walk.browser.js.map +1 -0
  339. package/dist/skills/sources/walk.d.ts +68 -0
  340. package/dist/skills/sources/walk.d.ts.map +1 -0
  341. package/dist/skills/sources/walk.js +264 -0
  342. package/dist/skills/sources/walk.js.map +1 -0
  343. package/dist/skills/substitute.d.ts +87 -0
  344. package/dist/skills/substitute.d.ts.map +1 -0
  345. package/dist/skills/substitute.js +322 -0
  346. package/dist/skills/substitute.js.map +1 -0
  347. package/dist/skills/testing/SkillKit.browser.d.ts +62 -0
  348. package/dist/skills/testing/SkillKit.browser.d.ts.map +1 -0
  349. package/dist/skills/testing/SkillKit.browser.js +41 -0
  350. package/dist/skills/testing/SkillKit.browser.js.map +1 -0
  351. package/dist/skills/testing/SkillKit.d.ts +130 -0
  352. package/dist/skills/testing/SkillKit.d.ts.map +1 -0
  353. package/dist/skills/testing/SkillKit.js +316 -0
  354. package/dist/skills/testing/SkillKit.js.map +1 -0
  355. package/dist/skills/testing/index.d.ts +9 -0
  356. package/dist/skills/testing/index.d.ts.map +1 -0
  357. package/dist/skills/testing/index.js +8 -0
  358. package/dist/skills/testing/index.js.map +1 -0
  359. package/dist/skills/trust.d.ts +72 -0
  360. package/dist/skills/trust.d.ts.map +1 -0
  361. package/dist/skills/trust.js +183 -0
  362. package/dist/skills/trust.js.map +1 -0
  363. package/dist/skills/types.d.ts +627 -0
  364. package/dist/skills/types.d.ts.map +1 -0
  365. package/dist/skills/types.js +85 -0
  366. package/dist/skills/types.js.map +1 -0
  367. package/dist/skills/validator.d.ts +95 -0
  368. package/dist/skills/validator.d.ts.map +1 -0
  369. package/dist/skills/validator.js +486 -0
  370. package/dist/skills/validator.js.map +1 -0
  371. package/dist/tracing/PipelineTracer.d.ts +35 -22
  372. package/dist/tracing/PipelineTracer.d.ts.map +1 -1
  373. package/dist/tracing/PipelineTracer.js +106 -61
  374. package/dist/tracing/PipelineTracer.js.map +1 -1
  375. package/dist/tracing/SdkTracer.d.ts +63 -61
  376. package/dist/tracing/SdkTracer.d.ts.map +1 -1
  377. package/dist/tracing/SdkTracer.js +185 -177
  378. package/dist/tracing/SdkTracer.js.map +1 -1
  379. package/dist/tracing/index.d.ts +10 -1
  380. package/dist/tracing/index.d.ts.map +1 -1
  381. package/dist/tracing/index.js +9 -0
  382. package/dist/tracing/index.js.map +1 -1
  383. package/dist/tracing/types.d.ts +89 -16
  384. package/dist/tracing/types.d.ts.map +1 -1
  385. package/dist/tracing/types.js +17 -4
  386. package/dist/tracing/types.js.map +1 -1
  387. package/dist/types.d.ts +6 -1
  388. package/dist/types.d.ts.map +1 -1
  389. package/dist/types.js +4 -0
  390. package/dist/types.js.map +1 -1
  391. package/dist/version.d.ts.map +1 -1
  392. package/dist/version.js +10 -20
  393. package/dist/version.js.map +1 -1
  394. package/package.json +53 -39
  395. package/.claude/skills/qodo-agent/SKILL.md +0 -974
  396. package/.claude/skills/qodo-agent/assets/programmatic-agent.ts +0 -407
  397. package/.claude/skills/qodo-agent/references/builtin-tools.md +0 -342
  398. package/.claude/skills/qodo-agent/references/common-issues.md +0 -537
  399. package/bin/rg +0 -0
  400. package/dist/api/agent.d.ts +0 -105
  401. package/dist/api/agent.d.ts.map +0 -1
  402. package/dist/api/agent.js +0 -963
  403. package/dist/api/agent.js.map +0 -1
  404. package/dist/api/analytics.d.ts +0 -43
  405. package/dist/api/analytics.d.ts.map +0 -1
  406. package/dist/api/analytics.js +0 -163
  407. package/dist/api/analytics.js.map +0 -1
  408. package/dist/api/http.d.ts +0 -5
  409. package/dist/api/http.d.ts.map +0 -1
  410. package/dist/api/http.js +0 -62
  411. package/dist/api/http.js.map +0 -1
  412. package/dist/api/index.d.ts +0 -12
  413. package/dist/api/index.d.ts.map +0 -1
  414. package/dist/api/index.js +0 -17
  415. package/dist/api/index.js.map +0 -1
  416. package/dist/api/taskTracking.d.ts +0 -54
  417. package/dist/api/taskTracking.d.ts.map +0 -1
  418. package/dist/api/taskTracking.js +0 -208
  419. package/dist/api/taskTracking.js.map +0 -1
  420. package/dist/api/types.d.ts +0 -93
  421. package/dist/api/types.d.ts.map +0 -1
  422. package/dist/api/types.js +0 -2
  423. package/dist/api/types.js.map +0 -1
  424. package/dist/api/utils.d.ts +0 -8
  425. package/dist/api/utils.d.ts.map +0 -1
  426. package/dist/api/utils.js +0 -63
  427. package/dist/api/utils.js.map +0 -1
  428. package/dist/api/websocket.d.ts +0 -203
  429. package/dist/api/websocket.d.ts.map +0 -1
  430. package/dist/api/websocket.js +0 -1166
  431. package/dist/api/websocket.js.map +0 -1
  432. package/dist/bin/install-skill.d.ts +0 -14
  433. package/dist/bin/install-skill.d.ts.map +0 -1
  434. package/dist/bin/install-skill.js +0 -125
  435. package/dist/bin/install-skill.js.map +0 -1
  436. package/dist/bin/run-helpers.d.ts +0 -34
  437. package/dist/bin/run-helpers.d.ts.map +0 -1
  438. package/dist/bin/run-helpers.js +0 -186
  439. package/dist/bin/run-helpers.js.map +0 -1
  440. package/dist/bin/run.d.ts +0 -13
  441. package/dist/bin/run.d.ts.map +0 -1
  442. package/dist/bin/run.js +0 -57
  443. package/dist/bin/run.js.map +0 -1
  444. package/dist/clients/index.d.ts +0 -10
  445. package/dist/clients/index.d.ts.map +0 -1
  446. package/dist/clients/index.js +0 -8
  447. package/dist/clients/index.js.map +0 -1
  448. package/dist/clients/info/InfoClient.d.ts +0 -37
  449. package/dist/clients/info/InfoClient.d.ts.map +0 -1
  450. package/dist/clients/info/InfoClient.js +0 -69
  451. package/dist/clients/info/InfoClient.js.map +0 -1
  452. package/dist/clients/info/index.d.ts +0 -4
  453. package/dist/clients/info/index.d.ts.map +0 -1
  454. package/dist/clients/info/index.js +0 -2
  455. package/dist/clients/info/index.js.map +0 -1
  456. package/dist/clients/info/types.d.ts +0 -21
  457. package/dist/clients/info/types.d.ts.map +0 -1
  458. package/dist/clients/info/types.js +0 -2
  459. package/dist/clients/info/types.js.map +0 -1
  460. package/dist/clients/sessions/SessionsClient.d.ts +0 -34
  461. package/dist/clients/sessions/SessionsClient.d.ts.map +0 -1
  462. package/dist/clients/sessions/SessionsClient.js +0 -71
  463. package/dist/clients/sessions/SessionsClient.js.map +0 -1
  464. package/dist/clients/sessions/index.d.ts +0 -4
  465. package/dist/clients/sessions/index.d.ts.map +0 -1
  466. package/dist/clients/sessions/index.js +0 -2
  467. package/dist/clients/sessions/index.js.map +0 -1
  468. package/dist/clients/sessions/types.d.ts +0 -20
  469. package/dist/clients/sessions/types.d.ts.map +0 -1
  470. package/dist/clients/sessions/types.js +0 -2
  471. package/dist/clients/sessions/types.js.map +0 -1
  472. package/dist/clients/tools/ToolsClient.d.ts +0 -39
  473. package/dist/clients/tools/ToolsClient.d.ts.map +0 -1
  474. package/dist/clients/tools/ToolsClient.js +0 -95
  475. package/dist/clients/tools/ToolsClient.js.map +0 -1
  476. package/dist/clients/tools/index.d.ts +0 -4
  477. package/dist/clients/tools/index.d.ts.map +0 -1
  478. package/dist/clients/tools/index.js +0 -2
  479. package/dist/clients/tools/index.js.map +0 -1
  480. package/dist/clients/tools/types.d.ts +0 -14
  481. package/dist/clients/tools/types.d.ts.map +0 -1
  482. package/dist/clients/tools/types.js +0 -2
  483. package/dist/clients/tools/types.js.map +0 -1
  484. package/dist/config/ConfigManager.d.ts +0 -43
  485. package/dist/config/ConfigManager.d.ts.map +0 -1
  486. package/dist/config/ConfigManager.js +0 -472
  487. package/dist/config/ConfigManager.js.map +0 -1
  488. package/dist/config/index.d.ts +0 -6
  489. package/dist/config/index.d.ts.map +0 -1
  490. package/dist/config/index.js +0 -7
  491. package/dist/config/index.js.map +0 -1
  492. package/dist/config/urlConfig.d.ts +0 -15
  493. package/dist/config/urlConfig.d.ts.map +0 -1
  494. package/dist/config/urlConfig.js +0 -75
  495. package/dist/config/urlConfig.js.map +0 -1
  496. package/dist/constants/errors.d.ts +0 -2
  497. package/dist/constants/errors.d.ts.map +0 -1
  498. package/dist/constants/errors.js +0 -2
  499. package/dist/constants/errors.js.map +0 -1
  500. package/dist/constants/index.d.ts +0 -7
  501. package/dist/constants/index.d.ts.map +0 -1
  502. package/dist/constants/index.js +0 -11
  503. package/dist/constants/index.js.map +0 -1
  504. package/dist/constants/tools.d.ts +0 -4
  505. package/dist/constants/tools.d.ts.map +0 -1
  506. package/dist/constants/tools.js +0 -4
  507. package/dist/constants/tools.js.map +0 -1
  508. package/dist/constants/versions.d.ts +0 -2
  509. package/dist/constants/versions.d.ts.map +0 -1
  510. package/dist/constants/versions.js +0 -2
  511. package/dist/constants/versions.js.map +0 -1
  512. package/dist/context/buildUserContext.d.ts +0 -18
  513. package/dist/context/buildUserContext.d.ts.map +0 -1
  514. package/dist/context/buildUserContext.js +0 -34
  515. package/dist/context/buildUserContext.js.map +0 -1
  516. package/dist/context/index.d.ts +0 -9
  517. package/dist/context/index.d.ts.map +0 -1
  518. package/dist/context/index.js +0 -9
  519. package/dist/context/index.js.map +0 -1
  520. package/dist/context/messageManager.d.ts +0 -42
  521. package/dist/context/messageManager.d.ts.map +0 -1
  522. package/dist/context/messageManager.js +0 -322
  523. package/dist/context/messageManager.js.map +0 -1
  524. package/dist/context/taskFocus.d.ts +0 -2
  525. package/dist/context/taskFocus.d.ts.map +0 -1
  526. package/dist/context/taskFocus.js +0 -26
  527. package/dist/context/taskFocus.js.map +0 -1
  528. package/dist/context/userInput.d.ts +0 -3
  529. package/dist/context/userInput.d.ts.map +0 -1
  530. package/dist/context/userInput.js +0 -20
  531. package/dist/context/userInput.js.map +0 -1
  532. package/dist/mcp/MCPManager.d.ts +0 -109
  533. package/dist/mcp/MCPManager.d.ts.map +0 -1
  534. package/dist/mcp/MCPManager.js +0 -592
  535. package/dist/mcp/MCPManager.js.map +0 -1
  536. package/dist/mcp/approvedTools.d.ts +0 -4
  537. package/dist/mcp/approvedTools.d.ts.map +0 -1
  538. package/dist/mcp/approvedTools.js +0 -19
  539. package/dist/mcp/approvedTools.js.map +0 -1
  540. package/dist/mcp/baseServer.d.ts +0 -75
  541. package/dist/mcp/baseServer.d.ts.map +0 -1
  542. package/dist/mcp/baseServer.js +0 -107
  543. package/dist/mcp/baseServer.js.map +0 -1
  544. package/dist/mcp/builtinServers.d.ts +0 -15
  545. package/dist/mcp/builtinServers.d.ts.map +0 -1
  546. package/dist/mcp/builtinServers.js +0 -141
  547. package/dist/mcp/builtinServers.js.map +0 -1
  548. package/dist/mcp/dynamicBEServer.d.ts +0 -20
  549. package/dist/mcp/dynamicBEServer.d.ts.map +0 -1
  550. package/dist/mcp/dynamicBEServer.js +0 -52
  551. package/dist/mcp/dynamicBEServer.js.map +0 -1
  552. package/dist/mcp/index.d.ts +0 -18
  553. package/dist/mcp/index.d.ts.map +0 -1
  554. package/dist/mcp/index.js +0 -23
  555. package/dist/mcp/index.js.map +0 -1
  556. package/dist/mcp/mcpInitialization.d.ts +0 -2
  557. package/dist/mcp/mcpInitialization.d.ts.map +0 -1
  558. package/dist/mcp/mcpInitialization.js +0 -56
  559. package/dist/mcp/mcpInitialization.js.map +0 -1
  560. package/dist/mcp/servers/filesystem.d.ts +0 -44
  561. package/dist/mcp/servers/filesystem.d.ts.map +0 -1
  562. package/dist/mcp/servers/filesystem.js +0 -776
  563. package/dist/mcp/servers/filesystem.js.map +0 -1
  564. package/dist/mcp/servers/git.d.ts +0 -18
  565. package/dist/mcp/servers/git.d.ts.map +0 -1
  566. package/dist/mcp/servers/git.js +0 -441
  567. package/dist/mcp/servers/git.js.map +0 -1
  568. package/dist/mcp/servers/ripgrep.d.ts +0 -39
  569. package/dist/mcp/servers/ripgrep.d.ts.map +0 -1
  570. package/dist/mcp/servers/ripgrep.js +0 -550
  571. package/dist/mcp/servers/ripgrep.js.map +0 -1
  572. package/dist/mcp/servers/shell.d.ts +0 -20
  573. package/dist/mcp/servers/shell.d.ts.map +0 -1
  574. package/dist/mcp/servers/shell.js +0 -519
  575. package/dist/mcp/servers/shell.js.map +0 -1
  576. package/dist/mcp/serversRegistry.d.ts +0 -55
  577. package/dist/mcp/serversRegistry.d.ts.map +0 -1
  578. package/dist/mcp/serversRegistry.js +0 -416
  579. package/dist/mcp/serversRegistry.js.map +0 -1
  580. package/dist/mcp/toolProcessor.d.ts +0 -82
  581. package/dist/mcp/toolProcessor.d.ts.map +0 -1
  582. package/dist/mcp/toolProcessor.js +0 -392
  583. package/dist/mcp/toolProcessor.js.map +0 -1
  584. package/dist/mcp/types.d.ts +0 -29
  585. package/dist/mcp/types.d.ts.map +0 -1
  586. package/dist/mcp/types.js +0 -2
  587. package/dist/mcp/types.js.map +0 -1
  588. package/dist/messages/index.d.ts +0 -8
  589. package/dist/messages/index.d.ts.map +0 -1
  590. package/dist/messages/index.js +0 -7
  591. package/dist/messages/index.js.map +0 -1
  592. package/dist/messages/openai.d.ts +0 -26
  593. package/dist/messages/openai.d.ts.map +0 -1
  594. package/dist/messages/openai.js +0 -55
  595. package/dist/messages/openai.js.map +0 -1
  596. package/dist/messages/types.d.ts +0 -73
  597. package/dist/messages/types.d.ts.map +0 -1
  598. package/dist/messages/types.js +0 -78
  599. package/dist/messages/types.js.map +0 -1
  600. package/dist/parser/index.d.ts +0 -72
  601. package/dist/parser/index.d.ts.map +0 -1
  602. package/dist/parser/index.js +0 -967
  603. package/dist/parser/index.js.map +0 -1
  604. package/dist/parser/types.d.ts +0 -153
  605. package/dist/parser/types.d.ts.map +0 -1
  606. package/dist/parser/types.js +0 -6
  607. package/dist/parser/types.js.map +0 -1
  608. package/dist/parser/utils.d.ts +0 -18
  609. package/dist/parser/utils.d.ts.map +0 -1
  610. package/dist/parser/utils.js +0 -64
  611. package/dist/parser/utils.js.map +0 -1
  612. package/dist/sdk/QodoSDK.d.ts +0 -218
  613. package/dist/sdk/QodoSDK.d.ts.map +0 -1
  614. package/dist/sdk/QodoSDK.js +0 -1115
  615. package/dist/sdk/QodoSDK.js.map +0 -1
  616. package/dist/sdk/artifacts.d.ts +0 -156
  617. package/dist/sdk/artifacts.d.ts.map +0 -1
  618. package/dist/sdk/artifacts.js +0 -166
  619. package/dist/sdk/artifacts.js.map +0 -1
  620. package/dist/sdk/bootstrap.d.ts +0 -16
  621. package/dist/sdk/bootstrap.d.ts.map +0 -1
  622. package/dist/sdk/bootstrap.js +0 -28
  623. package/dist/sdk/bootstrap.js.map +0 -1
  624. package/dist/sdk/builders.d.ts +0 -54
  625. package/dist/sdk/builders.d.ts.map +0 -1
  626. package/dist/sdk/builders.js +0 -117
  627. package/dist/sdk/builders.js.map +0 -1
  628. package/dist/sdk/defaults.d.ts +0 -11
  629. package/dist/sdk/defaults.d.ts.map +0 -1
  630. package/dist/sdk/defaults.js +0 -39
  631. package/dist/sdk/defaults.js.map +0 -1
  632. package/dist/sdk/discovery.d.ts +0 -2
  633. package/dist/sdk/discovery.d.ts.map +0 -1
  634. package/dist/sdk/discovery.js +0 -25
  635. package/dist/sdk/discovery.js.map +0 -1
  636. package/dist/sdk/events.d.ts +0 -269
  637. package/dist/sdk/events.d.ts.map +0 -1
  638. package/dist/sdk/events.js +0 -69
  639. package/dist/sdk/events.js.map +0 -1
  640. package/dist/sdk/exit-expression.d.ts +0 -13
  641. package/dist/sdk/exit-expression.d.ts.map +0 -1
  642. package/dist/sdk/exit-expression.js +0 -35
  643. package/dist/sdk/exit-expression.js.map +0 -1
  644. package/dist/sdk/index.d.ts +0 -17
  645. package/dist/sdk/index.d.ts.map +0 -1
  646. package/dist/sdk/index.js +0 -17
  647. package/dist/sdk/index.js.map +0 -1
  648. package/dist/sdk/middleware.d.ts +0 -59
  649. package/dist/sdk/middleware.d.ts.map +0 -1
  650. package/dist/sdk/middleware.js +0 -69
  651. package/dist/sdk/middleware.js.map +0 -1
  652. package/dist/sdk/pipeline/PipelineBuilder.d.ts +0 -79
  653. package/dist/sdk/pipeline/PipelineBuilder.d.ts.map +0 -1
  654. package/dist/sdk/pipeline/PipelineBuilder.js +0 -129
  655. package/dist/sdk/pipeline/PipelineBuilder.js.map +0 -1
  656. package/dist/sdk/pipeline/PipelineRunner.d.ts +0 -28
  657. package/dist/sdk/pipeline/PipelineRunner.d.ts.map +0 -1
  658. package/dist/sdk/pipeline/PipelineRunner.js +0 -326
  659. package/dist/sdk/pipeline/PipelineRunner.js.map +0 -1
  660. package/dist/sdk/pipeline/compiler.d.ts +0 -24
  661. package/dist/sdk/pipeline/compiler.d.ts.map +0 -1
  662. package/dist/sdk/pipeline/compiler.js +0 -199
  663. package/dist/sdk/pipeline/compiler.js.map +0 -1
  664. package/dist/sdk/pipeline/declarative.d.ts +0 -34
  665. package/dist/sdk/pipeline/declarative.d.ts.map +0 -1
  666. package/dist/sdk/pipeline/declarative.js +0 -9
  667. package/dist/sdk/pipeline/declarative.js.map +0 -1
  668. package/dist/sdk/pipeline/index.d.ts +0 -20
  669. package/dist/sdk/pipeline/index.d.ts.map +0 -1
  670. package/dist/sdk/pipeline/index.js +0 -19
  671. package/dist/sdk/pipeline/index.js.map +0 -1
  672. package/dist/sdk/pipeline/types.d.ts +0 -93
  673. package/dist/sdk/pipeline/types.d.ts.map +0 -1
  674. package/dist/sdk/pipeline/types.js +0 -10
  675. package/dist/sdk/pipeline/types.js.map +0 -1
  676. package/dist/sdk/policies.d.ts +0 -163
  677. package/dist/sdk/policies.d.ts.map +0 -1
  678. package/dist/sdk/policies.js +0 -243
  679. package/dist/sdk/policies.js.map +0 -1
  680. package/dist/sdk/runner/AgentRunner.d.ts +0 -22
  681. package/dist/sdk/runner/AgentRunner.d.ts.map +0 -1
  682. package/dist/sdk/runner/AgentRunner.js +0 -222
  683. package/dist/sdk/runner/AgentRunner.js.map +0 -1
  684. package/dist/sdk/runner/finalize.d.ts +0 -56
  685. package/dist/sdk/runner/finalize.d.ts.map +0 -1
  686. package/dist/sdk/runner/finalize.js +0 -155
  687. package/dist/sdk/runner/finalize.js.map +0 -1
  688. package/dist/sdk/runner/formats.d.ts +0 -7
  689. package/dist/sdk/runner/formats.d.ts.map +0 -1
  690. package/dist/sdk/runner/formats.js +0 -76
  691. package/dist/sdk/runner/formats.js.map +0 -1
  692. package/dist/sdk/runner/index.d.ts +0 -9
  693. package/dist/sdk/runner/index.d.ts.map +0 -1
  694. package/dist/sdk/runner/index.js +0 -9
  695. package/dist/sdk/runner/index.js.map +0 -1
  696. package/dist/sdk/runner/progress.d.ts +0 -3
  697. package/dist/sdk/runner/progress.d.ts.map +0 -1
  698. package/dist/sdk/runner/progress.js +0 -16
  699. package/dist/sdk/runner/progress.js.map +0 -1
  700. package/dist/sdk/schemas.d.ts +0 -72
  701. package/dist/sdk/schemas.d.ts.map +0 -1
  702. package/dist/sdk/schemas.js +0 -282
  703. package/dist/sdk/schemas.js.map +0 -1
  704. package/dist/sdk/trigger-context.d.ts +0 -24
  705. package/dist/sdk/trigger-context.d.ts.map +0 -1
  706. package/dist/sdk/trigger-context.js +0 -136
  707. package/dist/sdk/trigger-context.js.map +0 -1
  708. package/dist/session/SessionContext.d.ts +0 -89
  709. package/dist/session/SessionContext.d.ts.map +0 -1
  710. package/dist/session/SessionContext.js +0 -410
  711. package/dist/session/SessionContext.js.map +0 -1
  712. package/dist/session/environment.d.ts +0 -52
  713. package/dist/session/environment.d.ts.map +0 -1
  714. package/dist/session/environment.js +0 -27
  715. package/dist/session/environment.js.map +0 -1
  716. package/dist/session/history.d.ts +0 -18
  717. package/dist/session/history.d.ts.map +0 -1
  718. package/dist/session/history.js +0 -68
  719. package/dist/session/history.js.map +0 -1
  720. package/dist/session/index.d.ts +0 -10
  721. package/dist/session/index.d.ts.map +0 -1
  722. package/dist/session/index.js +0 -9
  723. package/dist/session/index.js.map +0 -1
  724. package/dist/session/serverData.d.ts +0 -38
  725. package/dist/session/serverData.d.ts.map +0 -1
  726. package/dist/session/serverData.js +0 -261
  727. package/dist/session/serverData.js.map +0 -1
  728. package/dist/tracing/pipelineHelpers.d.ts +0 -29
  729. package/dist/tracing/pipelineHelpers.d.ts.map +0 -1
  730. package/dist/tracing/pipelineHelpers.js +0 -224
  731. package/dist/tracing/pipelineHelpers.js.map +0 -1
  732. package/dist/tracking/Tracker.d.ts +0 -55
  733. package/dist/tracking/Tracker.d.ts.map +0 -1
  734. package/dist/tracking/Tracker.js +0 -217
  735. package/dist/tracking/Tracker.js.map +0 -1
  736. package/dist/tracking/index.d.ts +0 -8
  737. package/dist/tracking/index.d.ts.map +0 -1
  738. package/dist/tracking/index.js +0 -8
  739. package/dist/tracking/index.js.map +0 -1
  740. package/dist/tracking/schemas.d.ts +0 -292
  741. package/dist/tracking/schemas.d.ts.map +0 -1
  742. package/dist/tracking/schemas.js +0 -91
  743. package/dist/tracking/schemas.js.map +0 -1
  744. package/dist/utils/extractSetFlags.d.ts +0 -6
  745. package/dist/utils/extractSetFlags.d.ts.map +0 -1
  746. package/dist/utils/extractSetFlags.js +0 -16
  747. package/dist/utils/extractSetFlags.js.map +0 -1
  748. package/dist/utils/formatTimeAgo.d.ts +0 -2
  749. package/dist/utils/formatTimeAgo.d.ts.map +0 -1
  750. package/dist/utils/formatTimeAgo.js +0 -20
  751. package/dist/utils/formatTimeAgo.js.map +0 -1
  752. package/dist/utils/index.d.ts +0 -12
  753. package/dist/utils/index.d.ts.map +0 -1
  754. package/dist/utils/index.js +0 -12
  755. package/dist/utils/index.js.map +0 -1
  756. package/dist/utils/machineId.d.ts +0 -14
  757. package/dist/utils/machineId.d.ts.map +0 -1
  758. package/dist/utils/machineId.js +0 -66
  759. package/dist/utils/machineId.js.map +0 -1
  760. package/dist/utils/pathUtils.d.ts +0 -22
  761. package/dist/utils/pathUtils.d.ts.map +0 -1
  762. package/dist/utils/pathUtils.js +0 -54
  763. package/dist/utils/pathUtils.js.map +0 -1
  764. package/scripts/download-ripgrep.js +0 -269
@@ -0,0 +1,2189 @@
1
+ /**
2
+ * `Connection` — owns the WebSocket, multiplexes inbound envelopes onto
3
+ * subscriptions, encodes outbound envelopes.
4
+ *
5
+ * One `Connection` per `QodoClient.connect()` call. `tasks.start` /
6
+ * `tasks.continue` register a `TaskSubscription`; `client.receive()` registers
7
+ * a raw subscription that taps every inbound envelope. The connection routes
8
+ * inbound envelopes to subscriptions whose `parent_message_id` chain matches.
9
+ *
10
+ * **Reconnect + replay.** The Connection survives transport drops:
11
+ * subscriptions stay alive while the underlying `WSTransport` is replaced via
12
+ * the same factory. On a fresh transport, every active task subscription's
13
+ * `task_id` + `lastSeenMessageId` is sent back to the server as a
14
+ * `task.resubscribe` envelope; the server replays anything after that anchor
15
+ * from its per-session ring buffer (cap 1000 envelopes per session). No
16
+ * client-side outbox — durability is server-side. The 1.x `OutboxTurn` /
17
+ * `Resume` / `ResumeAck` machinery does not exist here.
18
+ */
19
+ import { AsyncQueue } from './iterator.js';
20
+ import { QodoBackpressureError, QodoColdAddressError } from './errors.js';
21
+ import { GEN_AI_CONVERSATION_ID, QAR_SESSION_ID, QAR_TASK_ID, } from '../observability/attributes.js';
22
+ import { asMessageId, asSessionId, uuidv7 } from './uuid.js';
23
+ /**
24
+ * A single task's inbound stream. Routes by parent-message chain: every
25
+ * envelope whose `parent_message_id` is in the chain becomes part of the chain
26
+ * (its `message_id` joins). Terminal envelopes (`task.done`, `error`) close the
27
+ * iterator.
28
+ *
29
+ * On `Connection`-level reconnect, the matching subscription stays alive and
30
+ * the `Connection` stitches the resubscribe envelope's id into `chain` so the
31
+ * server's replayed envelopes route back here transparently.
32
+ */
33
+ export class TaskSubscription {
34
+ rootMessageId;
35
+ onEarlyReturn;
36
+ onClose;
37
+ metrics;
38
+ queue = new AsyncQueue();
39
+ chain = new Set();
40
+ /**
41
+ * Lineage-ownership set — message_ids this subscription OWNS, used to
42
+ * attribute an inbound `tool.request` to the turn that produced it (so the
43
+ * SDK's `qar.client.tool.handler` span runs in the right task context).
44
+ *
45
+ * Distinct from {@link chain}, which is the broader ROUTING set: `chain`
46
+ * also accepts envelopes via the `task_id` fallback (a passive same-task
47
+ * subscription — e.g. an auto-paused prior `tasks.start` sub — absorbs
48
+ * another turn's stream that way for observation + replay continuity).
49
+ * `ownedMessages` grows ONLY through real `parent_message_id` lineage: this
50
+ * subscription's own outbound root(s), plus every inbound envelope whose
51
+ * parent it ALREADY owns. It never grows via the `task_id` fallback.
52
+ *
53
+ * Invariant (by construction): every message is owned by EXACTLY ONE
54
+ * subscription. Roots are unique per outbound request; a child is owned by
55
+ * its parent's unique owner. So a `tool.request` — which carries no
56
+ * `task_id` and links only by `parent_message_id` — has a unique owner: the
57
+ * subscription whose `ownedMessages` contains its parent. This is why a
58
+ * passive same-task absorber, which holds the other turn's deltas in `chain`
59
+ * but never in `ownedMessages` (their parent is a root it doesn't own),
60
+ * never wins ownership of that turn's tool calls — at any depth.
61
+ */
62
+ ownedMessages = new Set();
63
+ taskId;
64
+ /**
65
+ * `message_id` of the most recent inbound envelope that matched this
66
+ * subscription's chain or `task_id`. Anchor for `task.resubscribe` on
67
+ * reconnect — the server replays everything after this point. Stays
68
+ * `undefined` until the first inbound envelope arrives. Connection-scoped
69
+ * envelopes (`flow.pause` / `flow.resume`) don't update it: they're
70
+ * session-scoped, not task-scoped, so they can't anchor a per-task replay
71
+ * window.
72
+ */
73
+ lastSeen;
74
+ /**
75
+ * Outbound `task.resubscribe` message ids whose direct descendant marks
76
+ * the start of replay accounting for this subscription. Seeded by
77
+ * {@link attachOutboundMessageId} (auto-reconnect) and the constructor's
78
+ * `rootIsReplayAnchor` flag (manual `tasks.resubscribe`). Stays small —
79
+ * one entry per resubscribe envelope this subscription owns, not one
80
+ * entry per replay envelope received.
81
+ */
82
+ replayAnchors = new Set();
83
+ /**
84
+ * Once an envelope absorbed by this subscription has been identified as
85
+ * a replay descendant (its `parent_message_id` was in `replayAnchors`),
86
+ * every subsequently-absorbed envelope is also counted as a replay
87
+ * envelope: QAR's per-session ring buffer replays a contiguous chain
88
+ * after the anchor, so the post-anchor continuation is logically the
89
+ * same wire window as the replay itself.
90
+ *
91
+ * Sticky boolean rather than a growing chain Set — cheaper memory
92
+ * profile for long-running subscriptions that survive many reconnects.
93
+ */
94
+ replayCountingActive = false;
95
+ /**
96
+ * @param rootMessageId message_id of the outbound envelope that started the
97
+ * task (the `task.start` or `task.continue`).
98
+ * @param knownTaskId task_id known up front (only for `tasks.continue` /
99
+ * `tasks.cancel` / `tasks.resubscribe` — `tasks.start`
100
+ * doesn't know the id until the first inbound envelope
101
+ * reveals it). Pre-seeding the matcher closes a
102
+ * routing gap where envelopes that carry
103
+ * `payload.task_id` but no chain link to our outbound
104
+ * message would otherwise be missed.
105
+ * @param onEarlyReturn called once if the consumer breaks the iterator
106
+ * before terminal — best-effort `task.cancel`. Pass a
107
+ * no-op for resubscribe-style subscriptions where
108
+ * breaking the loop must NOT cancel the underlying
109
+ * task (the consumer is observing, not driving).
110
+ * @param onClose called once when the subscription is no longer
111
+ * routable (terminal received OR consumer broke).
112
+ */
113
+ /** Span lifecycle for the task this subscription represents. Closed on terminal/fail/early-return. */
114
+ span;
115
+ /**
116
+ * Resolver for the `task.started` admission ack. Receives both the
117
+ * inherited `session_id` (server-derived UUID) AND the canonical
118
+ * `task_id` from the envelope's payload — the two ids are distinct on
119
+ * the admission-retry path, where a retried `task.start` carries a
120
+ * fresh `message_id` while the server's canonical task_id stays bound
121
+ * to the FIRST winning attempt.
122
+ *
123
+ * Provided by `TaskClient.subscribeAndSend` for `task.start`
124
+ * subscriptions only — `task.continue` / `task.cancel` /
125
+ * `task.resubscribe` already know the session_id + task_id from a
126
+ * prior admission and don't wire a resolver. `null` once the resolver
127
+ * has fired (resolved OR rejected) — prevents a second resolution
128
+ * from a spec-violating duplicate ack.
129
+ */
130
+ taskStartedResolver = null;
131
+ /**
132
+ * Resolver for the `task.force_resumed` ack — fires when the SDK's
133
+ * outbound `task.forceResume` envelope's response arrives. Wired by
134
+ * `TaskClient.forceResume`. Mirrors {@link taskStartedResolver} in
135
+ * shape and lifecycle (null after fire; rejected if the subscription
136
+ * closes before the ack lands).
137
+ */
138
+ taskForceResumedResolver = null;
139
+ constructor(rootMessageId, knownTaskId, onEarlyReturn, onClose, span,
140
+ /**
141
+ * Optional transport-counter store. When provided, `consider` increments
142
+ * `replay_envelopes_received_total` for every absorbed envelope that
143
+ * descends from a replay anchor, and `replay_anchor_missing_total` for
144
+ * every `error { code: 'replay_anchor_missing' }` envelope that routes
145
+ * here.
146
+ */
147
+ metrics,
148
+ /**
149
+ * `true` when `rootMessageId` is itself a `task.resubscribe` envelope —
150
+ * its descendants count as replay envelopes for
151
+ * `replay_envelopes_received_total`. Set by the manual
152
+ * `client.tasks.resubscribe(...)` seam; the `tasks.start` /
153
+ * `tasks.continue` paths leave it `false` so live envelopes don't
154
+ * masquerade as replay until an auto-replay anchor is stitched in via
155
+ * {@link attachOutboundMessageId}.
156
+ */
157
+ rootIsReplayAnchor = false,
158
+ /**
159
+ * Optional resolver fired with the server-derived `session_id` when
160
+ * the `task.started` admission ack arrives on this subscription's
161
+ * chain. Wired by `task.start` paths so the caller's `TaskStartIterable`
162
+ * can expose the derived id via its `sessionId` Promise.
163
+ *
164
+ * Resolver lifecycle:
165
+ * - Resolved on inbound `task.started` whose `parent_message_id`
166
+ * matches the outbound `task.start.message_id`.
167
+ * - Rejected if the subscription terminates (clean close, transport
168
+ * fail, terminal `task.done` / `error`) before any `task.started`
169
+ * arrives — keeps the consumer's `await stream.sessionId` from
170
+ * hanging forever.
171
+ */
172
+ taskStartedResolver,
173
+ /**
174
+ * Optional resolver fired with the recovered task's id +
175
+ * post-recovery state when an inbound `task.force_resumed` ack
176
+ * arrives on this subscription's chain. Wired by
177
+ * `TaskClient.forceResume`. Same lifecycle as
178
+ * {@link taskStartedResolver}: rejected on close/error before
179
+ * the ack lands.
180
+ */
181
+ taskForceResumedResolver) {
182
+ this.rootMessageId = rootMessageId;
183
+ this.onEarlyReturn = onEarlyReturn;
184
+ this.onClose = onClose;
185
+ this.metrics = metrics;
186
+ this.chain.add(rootMessageId);
187
+ // The outbound root is the origin of this subscription's lineage — it
188
+ // owns it. Every inbound envelope whose parent chain leads back here
189
+ // (via real `parent_message_id` links) is owned by this subscription.
190
+ this.ownedMessages.add(rootMessageId);
191
+ if (rootIsReplayAnchor) {
192
+ this.replayAnchors.add(rootMessageId);
193
+ }
194
+ this.taskId = knownTaskId;
195
+ this.span = span;
196
+ if (taskStartedResolver !== undefined) {
197
+ this.taskStartedResolver = taskStartedResolver;
198
+ }
199
+ if (taskForceResumedResolver !== undefined) {
200
+ this.taskForceResumedResolver = taskForceResumedResolver;
201
+ }
202
+ // Stamp `qar.task_id` eagerly when we know the id at construction.
203
+ // For `task.start` subscriptions the SDK derives `task_id` from the
204
+ // outbound `task.start.message_id`; for `task.continue` /
205
+ // `task.cancel` / `task.resubscribe` the caller supplies it. The
206
+ // lazy stamp inside `consider` stays as defense-in-depth for any
207
+ // callsite that constructs without an id.
208
+ if (knownTaskId !== undefined && span !== undefined) {
209
+ span.setAttribute(QAR_TASK_ID, knownTaskId);
210
+ }
211
+ }
212
+ /** The `task_id` once we've seen the first envelope that carries it. */
213
+ get currentTaskId() {
214
+ return this.taskId;
215
+ }
216
+ /**
217
+ * Most-recent inbound envelope's `message_id`, or `undefined` if no inbound
218
+ * matched yet. Public so `Connection.replayActiveTasks` can read it without
219
+ * pinning to a specific subscription class.
220
+ */
221
+ get lastSeenMessageId() {
222
+ return this.lastSeen;
223
+ }
224
+ /** Whether this subscription has terminated and stopped accepting envelopes. */
225
+ get isTerminated() {
226
+ return this.queue.isClosed;
227
+ }
228
+ /**
229
+ * Stitch a new outbound `message_id` (typically a `task.resubscribe`) into
230
+ * this subscription's chain so the server's replies routed via
231
+ * `parent_message_id` link back here. Idempotent — Set.add no-ops on dups.
232
+ *
233
+ * Called by `Connection` after sending a resubscribe on behalf of an
234
+ * already-live subscription (the auto-replay path on reconnect).
235
+ */
236
+ attachOutboundMessageId(messageId) {
237
+ this.chain.add(messageId);
238
+ // A `task.resubscribe` we sent is also a real lineage origin for the
239
+ // envelopes the server replays in response — own it so those descendants
240
+ // resolve their tool-call context to this subscription.
241
+ this.ownedMessages.add(messageId);
242
+ // The only caller is `Connection.replayActiveTasks`, which stitches a
243
+ // `task.resubscribe` envelope's id in. Seed `replayAnchors` so the
244
+ // next absorbed descendant flips `replayCountingActive` on and starts
245
+ // counting toward `replay_envelopes_received_total`.
246
+ this.replayAnchors.add(messageId);
247
+ }
248
+ consider(env) {
249
+ // Connection-scoped flow events: broadcast to every active task
250
+ // iterator regardless of message-id chain — QAR emits these once per
251
+ // connection state change, and consumers expect them on the same iterator
252
+ // they're already reading. They never claim the envelope, so raw taps and
253
+ // peer subscriptions still see them. They DON'T update `lastSeen` —
254
+ // they're session-scoped, not task-scoped, so they can't anchor a
255
+ // per-task replay window.
256
+ if (env.kind === 'flow.pause' || env.kind === 'flow.resume') {
257
+ this.queue.push(env);
258
+ return false;
259
+ }
260
+ if (!this.matches(env))
261
+ return false;
262
+ // Did this envelope reach us as a direct reply to one of our own
263
+ // outbound messages (chain), or only because its payload `task_id`
264
+ // equals ours (fallback)? Captured BEFORE `chain.add` below stitches
265
+ // this envelope's own `message_id` in. The admission / recovery ack
266
+ // branches gate their self-close on this: an ack that matched only via
267
+ // the `task_id` fallback belongs to a different request and must not
268
+ // close THIS subscription.
269
+ const matchedByChain = this.matchesByChain(env);
270
+ // Replay-counter accounting. Two states:
271
+ // - `replayCountingActive` already true → count this envelope (the
272
+ // post-anchor continuation is part of the same wire window).
273
+ // - `replayCountingActive` false but env's `parent_message_id` is in
274
+ // `replayAnchors` → first replay descendant; flip the flag and
275
+ // count this envelope.
276
+ // Sticky-boolean replaces the prior growing replay-chain Set so memory
277
+ // stays bounded over long-lived subscriptions.
278
+ if (this.metrics !== undefined) {
279
+ if (this.replayCountingActive) {
280
+ this.metrics.recordReplayEnvelopeReceived();
281
+ }
282
+ else {
283
+ const parent = env.parent_message_id;
284
+ if (parent !== undefined &&
285
+ parent !== null &&
286
+ this.replayAnchors.has(parent)) {
287
+ this.replayCountingActive = true;
288
+ this.metrics.recordReplayEnvelopeReceived();
289
+ }
290
+ }
291
+ }
292
+ this.chain.add(env.message_id);
293
+ // Lineage ownership: this envelope belongs to THIS subscription's turn iff
294
+ // we already own its parent (a real `parent_message_id` link back to our
295
+ // outbound root). A `task_id`-fallback match (e.g. a passive same-task sub
296
+ // absorbing another turn's stream) is routed into `chain` above but is
297
+ // NEVER an ownership link — its parent is a root we don't own — so the
298
+ // ownership set stays exact and the unique-owner invariant holds at any
299
+ // depth. See {@link ownedMessages}.
300
+ const parentId = env.parent_message_id;
301
+ if (parentId !== undefined && parentId !== null && this.ownedMessages.has(parentId)) {
302
+ this.ownedMessages.add(env.message_id);
303
+ }
304
+ this.lastSeen = env.message_id;
305
+ if (this.taskId === undefined && envelopeHasTaskId(env)) {
306
+ this.taskId = env.payload.task_id;
307
+ // Stamp `qar.task_id` on the span lazily — `tasks.start`'s span doesn't
308
+ // know the id at open time (the server allocates it server-side and
309
+ // reports it on the first inbound envelope).
310
+ if (this.span !== undefined) {
311
+ this.span.setAttribute(QAR_TASK_ID, this.taskId);
312
+ }
313
+ }
314
+ // Capture the server-derived `session_id` from the `task.started`
315
+ // admission ack and resolve the caller-facing `sessionId` Promise.
316
+ // The envelope is NOT yielded on the public `TaskEvent` iterator —
317
+ // it's the start ack, not a content event. Per the wire contract
318
+ // the kind is server-emitted exactly once per successful admission,
319
+ // before any `task.delta`; routing only matters when this
320
+ // subscription owns the matching outbound `task.start` (so
321
+ // `taskStartedResolver` was wired).
322
+ if (env.kind === 'task.started') {
323
+ if (!matchedByChain) {
324
+ // This `task.started` reached us only via the `task_id` fallback —
325
+ // it is NOT the reply to this subscription's own outbound
326
+ // `task.start`. Two ways that happens, both relying on the chain
327
+ // seeding above:
328
+ // - a sibling's idempotent re-admission ack
329
+ // (`task.started { is_new: false }`) on an already-dispatched
330
+ // session, which carries this task's canonical id; and
331
+ // - a cold `task.resubscribe` replaying the ORIGINAL `task.started`
332
+ // (foreign parent) from the per-session ring buffer.
333
+ // Absorb it — the `chain.add` above already seeded the chain so the
334
+ // chain-only replayed events that follow (tool.request /
335
+ // state.update / agent.spawn / bulletin.post / artifact.add) still
336
+ // route here — but do NOT resolve admission or self-close. Resolving
337
+ // or closing on a foreign ack is exactly what silently collapsed an
338
+ // unrelated in-flight consumer stream.
339
+ return true;
340
+ }
341
+ // The per-Task `session_id` is captured by
342
+ // {@link Connection.dispatch} into `taskSessions[task_id]` (the
343
+ // authoritative lookup map for outbound encoding). Holding a
344
+ // per-subscription copy would be dead state — encoding paths
345
+ // address by `task_id`, not by subscription identity.
346
+ //
347
+ // Admission_in_progress retry path: the SDK opens each
348
+ // `task.start` attempt with `knownTaskId = asTaskId(rootMessageId)`
349
+ // (the ATTEMPT id), but the server's canonical `task_id` on
350
+ // `task.started.payload.task_id` may differ — on a deterministic-key
351
+ // retry the SDK rotates `message_id` per attempt while the
352
+ // server-canonical task_id stays bound to the winning admission.
353
+ // Overwrite `this.taskId` with the CANONICAL value so that
354
+ // subsequent outbound envelopes (`task.cancel` from iterator
355
+ // early-return, `task.resubscribe` on auto-reconnect) address the
356
+ // task by the id `Connection.taskSessions` is keyed on. Without
357
+ // overwrite, post-ack ongoing emits would miss the session map
358
+ // and raise `QodoColdAddressError` despite admission being fully
359
+ // complete.
360
+ //
361
+ // Idempotent-admission fields: forward-compat — old QAR builds
362
+ // omit them; the SDK treats the absence as `is_new: true` (the
363
+ // pre-idempotent-admission default) and leaves `state` /
364
+ // `previousTaskId` / `previousState` undefined so the typed
365
+ // admission result's `isNew === true` branch is selected.
366
+ const payload = env.payload;
367
+ const canonicalTaskId = payload.task_id;
368
+ this.taskId = canonicalTaskId;
369
+ if (this.span !== undefined) {
370
+ // Stamp the server-derived `session_id` AND re-stamp the canonical
371
+ // `task_id` onto the open span. Pre-admission the span carries the
372
+ // attempt id (or none); post-admission both attributes reflect the
373
+ // canonical values QAR will report on every downstream envelope.
374
+ this.span.setAttribute(QAR_SESSION_ID, env.session_id);
375
+ this.span.setAttribute(GEN_AI_CONVERSATION_ID, env.session_id);
376
+ this.span.setAttribute(QAR_TASK_ID, canonicalTaskId);
377
+ }
378
+ // Default `is_new` to `true` when the field is absent (forward-compat
379
+ // with older QAR builds). `is_new === false` indicates an idempotent
380
+ // existing-session return — no further envelopes will flow on this
381
+ // subscription's chain (the existing session's events route on the
382
+ // chain that opened it). Auto-close after the ack so iterators don't
383
+ // hang waiting for events that will never arrive.
384
+ const isNew = payload.is_new ?? true;
385
+ if (this.taskStartedResolver !== null) {
386
+ const resolver = this.taskStartedResolver;
387
+ this.taskStartedResolver = null;
388
+ resolver.resolve({
389
+ sessionId: env.session_id,
390
+ taskId: canonicalTaskId,
391
+ isNew,
392
+ ...(payload.state !== undefined ? { state: payload.state } : {}),
393
+ ...(payload.previous_task_id !== undefined
394
+ ? { previousTaskId: payload.previous_task_id }
395
+ : {}),
396
+ ...(payload.previous_state !== undefined
397
+ ? { previousState: payload.previous_state }
398
+ : {}),
399
+ });
400
+ }
401
+ if (!isNew) {
402
+ // Idempotent return — close the subscription without firing
403
+ // `onEarlyReturn` (which would emit a spurious `task.cancel`
404
+ // against the existing session). Use `close()` rather than
405
+ // manual `queue.close()` + `onClose(this)` so the span's
406
+ // `succeed()` fires too — `onClose` is wired to
407
+ // `connection.unsubscribe` and DOES NOT end spans, so a
408
+ // manual teardown leaks open spans on every idempotent
409
+ // admission. Same safe-shutdown shape as `disconnect()` and
410
+ // consumer-return paths.
411
+ this.close();
412
+ }
413
+ return true;
414
+ }
415
+ if (env.kind === 'task.force_resumed') {
416
+ if (!matchedByChain) {
417
+ // Same rationale as `task.started`: a `task.force_resumed` that
418
+ // matched only via the `task_id` fallback is NOT this subscription's
419
+ // own recovery ack (the forceResume one-shot matches its ack by chain
420
+ // through the outbound `task.forceResume.message_id`). Absorb for
421
+ // chain continuity; never one-shot-close on a foreign ack — that
422
+ // would collapse an unrelated in-flight stream bound to the same
423
+ // task.
424
+ return true;
425
+ }
426
+ // `task.forceResume` ack — surface the recovered task's id +
427
+ // post-recovery state to `TaskClient.forceResume`. This is the
428
+ // one-shot terminal for the forceResume subscription; close the
429
+ // queue so the iterator (consumed only by SDK-internal wait
430
+ // logic) drains.
431
+ const payload = env.payload;
432
+ const recoveredTaskId = payload.task_id;
433
+ this.taskId = recoveredTaskId;
434
+ if (this.span !== undefined) {
435
+ this.span.setAttribute(QAR_SESSION_ID, env.session_id);
436
+ this.span.setAttribute(GEN_AI_CONVERSATION_ID, env.session_id);
437
+ this.span.setAttribute(QAR_TASK_ID, recoveredTaskId);
438
+ }
439
+ if (this.taskForceResumedResolver !== null) {
440
+ const resolver = this.taskForceResumedResolver;
441
+ this.taskForceResumedResolver = null;
442
+ resolver.resolve({
443
+ sessionId: env.session_id,
444
+ taskId: recoveredTaskId,
445
+ state: payload.state,
446
+ });
447
+ }
448
+ // forceResume is one-shot — close the subscription after the
449
+ // ack. Use `close()` (not manual `queue.close()` + `onClose`) so
450
+ // the span's `succeed()` fires; `onClose` only unregisters and
451
+ // does not end spans.
452
+ this.close();
453
+ return true;
454
+ }
455
+ if (isTaskEvent(env)) {
456
+ this.queue.push(env);
457
+ }
458
+ if (env.kind === 'task.done') {
459
+ const status = env.payload.status;
460
+ const error = env.payload.error;
461
+ this.endSpanForDone(status, error);
462
+ // Terminal without admission ack ⇒ reject the sessionId promise
463
+ // so `await stream.sessionId` doesn't hang. The wire contract says
464
+ // `task.done` cannot land before `task.started` on a successfully
465
+ // admitted task; reaching here with a live resolver means the run
466
+ // ended without admission (test fixtures, transport-injected
467
+ // terminals) and the caller deserves a typed rejection.
468
+ this.rejectTaskStartedIfPending(new Error(`task ended before task.started ack arrived (status=${status})`));
469
+ this.rejectTaskForceResumedIfPending(new Error(`task ended before task.force_resumed ack arrived (status=${status})`));
470
+ this.queue.close();
471
+ this.onClose(this);
472
+ }
473
+ else if (env.kind === 'error') {
474
+ const code = env.payload.code;
475
+ const message = env.payload.message;
476
+ if (code === 'replay_anchor_missing') {
477
+ this.metrics?.recordReplayAnchorMissing();
478
+ }
479
+ // `span.fail()` records the exception + sets ERROR status; the wire
480
+ // `error.code` is captured in the exception message. We deliberately
481
+ // don't `setAttribute(QAR_*, code)` here — error envelopes are wire
482
+ // transport-level, not tool-level, and their span semantics belong
483
+ // to the recorded exception, not a custom attribute key.
484
+ this.span?.fail(new Error(`server error: ${code}${message !== undefined ? `: ${message}` : ''}`));
485
+ // Pre-admission failure (e.g. `admission_stalled`, invalid
486
+ // `idempotency_key`, deps build) closes the subscription before
487
+ // any `task.started` lands — reject the sessionId promise so the
488
+ // caller's `await stream.sessionId` surfaces the failure rather
489
+ // than hanging. The same error envelope still flows through the
490
+ // event iterator (or the wrap-server-errors layer in TaskClient
491
+ // for typed-code routes) so the rejection here is additive, not a
492
+ // replacement.
493
+ //
494
+ // EXCEPTION: `admission_in_progress` is retryable. The retry
495
+ // wrapper at the TaskClient layer dispatches a fresh subscription
496
+ // that may still resolve the deferred — keep the promise pending
497
+ // so the eventual `task.started` (on a successful retry) lands
498
+ // cleanly. The current per-attempt subscription is still closed
499
+ // (the error envelope is its terminal); only the
500
+ // promise-rejection step is skipped.
501
+ if (code !== 'admission_in_progress') {
502
+ this.rejectTaskStartedIfPending(new Error(`server error before task.started: ${code}${message !== undefined ? `: ${message}` : ''}`));
503
+ this.rejectTaskForceResumedIfPending(new Error(`server error before task.force_resumed: ${code}${message !== undefined ? `: ${message}` : ''}`));
504
+ }
505
+ this.queue.close();
506
+ this.onClose(this);
507
+ }
508
+ return true;
509
+ }
510
+ /**
511
+ * Reject the pending `task.started` resolver, if any. No-op when the
512
+ * resolver has already fired (resolved earlier on the ack, or rejected
513
+ * by a prior terminal). Idempotent — every terminal path can call
514
+ * this without guarding.
515
+ */
516
+ rejectTaskStartedIfPending(err) {
517
+ if (this.taskStartedResolver === null)
518
+ return;
519
+ const resolver = this.taskStartedResolver;
520
+ this.taskStartedResolver = null;
521
+ resolver.reject(err);
522
+ }
523
+ /**
524
+ * Reject the pending `task.force_resumed` resolver, if any. Mirrors
525
+ * {@link rejectTaskStartedIfPending}: idempotent; called from every
526
+ * terminal path so `tasks.forceResume()` Promises don't hang on
527
+ * close / transport failure.
528
+ */
529
+ rejectTaskForceResumedIfPending(err) {
530
+ if (this.taskForceResumedResolver === null)
531
+ return;
532
+ const resolver = this.taskForceResumedResolver;
533
+ this.taskForceResumedResolver = null;
534
+ resolver.reject(err);
535
+ }
536
+ /**
537
+ * End the task span for a `task.done` envelope. `completed` and `canceled`
538
+ * are both clean terminal states (the consumer asked for the cancel, or the
539
+ * task ran to completion); `failed` records an error.
540
+ */
541
+ endSpanForDone(status, error) {
542
+ if (this.span === undefined)
543
+ return;
544
+ if (status === 'failed') {
545
+ this.span.fail(new Error(error ?? 'task failed'));
546
+ return;
547
+ }
548
+ this.span.succeed();
549
+ }
550
+ considerClient(ev) {
551
+ if (this.queue.isClosed)
552
+ return;
553
+ this.queue.push(ev);
554
+ }
555
+ fail(err) {
556
+ this.span?.fail(err);
557
+ // Transport failure before admission ack ⇒ surface to anyone
558
+ // awaiting `TaskStartIterable.sessionId` rather than letting them hang.
559
+ this.rejectTaskStartedIfPending(err);
560
+ this.rejectTaskForceResumedIfPending(err);
561
+ this.queue.fail(err);
562
+ this.onClose(this);
563
+ }
564
+ close() {
565
+ // Clean-close path used by `disconnect()` and consumer `.return()` —
566
+ // neither is an error from the SDK's perspective (the consumer asked or
567
+ // the connection is shutting down). `succeed()` is idempotent if the
568
+ // span already ended via `task.done` arriving first.
569
+ this.span?.succeed();
570
+ // Same lifecycle rejection as `fail()` — a clean close before
571
+ // `task.started` means the consumer never got the admission ack.
572
+ this.rejectTaskStartedIfPending(new Error('subscription closed before task.started ack arrived'));
573
+ this.rejectTaskForceResumedIfPending(new Error('subscription closed before task.force_resumed ack arrived'));
574
+ this.queue.close();
575
+ this.onClose(this);
576
+ }
577
+ next() {
578
+ // Run the queue read under the SDK span's OTel context so the consumer's
579
+ // `for await` continuation resumes with the SDK span active. This is
580
+ // what lets a consumer call `client.tools.respond(...)` from inside the
581
+ // loop body and have *that* span parent under our task span (and have
582
+ // its outbound `traceparent` reference our task span's id). Without
583
+ // this wrap, `getActiveSpan()` at await-resume time falls back to
584
+ // whatever was active when the consumer originally `for await`-ed,
585
+ // breaking the cross-call trace tree.
586
+ if (this.span !== undefined) {
587
+ return this.span.withContext(() => this.queue.next());
588
+ }
589
+ return this.queue.next();
590
+ }
591
+ async return() {
592
+ if (!this.queue.isClosed) {
593
+ this.onEarlyReturn(this.taskId);
594
+ // Iterator early-termination is consumer intent, not a failure. The
595
+ // SDK best-effort sends `task.cancel` (via `onEarlyReturn`); the span
596
+ // ends with `OK` so traces don't surface canceled tasks as errors.
597
+ this.span?.succeed();
598
+ this.queue.close();
599
+ this.onClose(this);
600
+ }
601
+ return { value: undefined, done: true };
602
+ }
603
+ async throw(err) {
604
+ if (!this.queue.isClosed) {
605
+ this.span?.fail(err);
606
+ this.queue.close();
607
+ this.onClose(this);
608
+ }
609
+ throw err instanceof Error ? err : new Error(String(err));
610
+ }
611
+ [Symbol.asyncIterator]() {
612
+ return this;
613
+ }
614
+ matches(env) {
615
+ if (this.matchesByChain(env)) {
616
+ return true;
617
+ }
618
+ if (this.taskId !== undefined && envelopeHasTaskId(env)) {
619
+ const payloadTaskId = env.payload.task_id;
620
+ if (payloadTaskId === this.taskId)
621
+ return true;
622
+ }
623
+ return false;
624
+ }
625
+ /**
626
+ * Whether `env`'s `parent_message_id` links into this subscription's chain
627
+ * — i.e. the envelope is a direct reply to an outbound message this
628
+ * subscription owns (`task.start` / `task.continue` / `task.resubscribe`,
629
+ * or a prior inbound that already joined the chain).
630
+ *
631
+ * Distinct from the broader {@link matches}, which also accepts an envelope
632
+ * solely because its payload `task_id` equals ours. The admission /
633
+ * recovery acks (`task.started`, `task.force_resumed`) use THIS predicate —
634
+ * not the `task_id` fallback — to decide whether to resolve admission or
635
+ * self-close: those acks carry the canonical `task_id`, which on an
636
+ * idempotent re-admission (a second `task.start` on an already-dispatched
637
+ * session) or a buffer replay equals the id of a DIFFERENT subscription
638
+ * whose task is still streaming. A `task_id`-only match must still absorb
639
+ * the ack (so a cold resubscribe's replayed `task.started` seeds the chain
640
+ * for the chain-only events that follow), but must NOT drive that other
641
+ * subscription's admission/close — see {@link consider}.
642
+ */
643
+ matchesByChain(env) {
644
+ return (env.parent_message_id !== undefined &&
645
+ env.parent_message_id !== null &&
646
+ this.chain.has(env.parent_message_id));
647
+ }
648
+ /**
649
+ * Whether this subscription OWNS `env`'s turn — i.e. its `parent_message_id`
650
+ * is in {@link ownedMessages} (a real lineage link back to our outbound
651
+ * root, never a `task_id`-fallback absorb). Used by
652
+ * {@link Connection.runInOwningTaskContext} to attribute an inbound
653
+ * `tool.request` to the turn that produced it: the owner is unique (each
654
+ * message is owned by exactly one subscription), so this is unambiguous even
655
+ * when multiple same-task subscriptions overlap (e.g. a live auto-paused
656
+ * prior `tasks.start` sub alongside an active `tasks.continue`). A passive
657
+ * absorber holds the other turn's stream in {@link chain} but never in
658
+ * {@link ownedMessages}, so it never wins ownership — at any depth.
659
+ */
660
+ ownsLineage(env) {
661
+ return (env.parent_message_id !== undefined &&
662
+ env.parent_message_id !== null &&
663
+ this.ownedMessages.has(env.parent_message_id));
664
+ }
665
+ /**
666
+ * Public alias for `matches` — exposed so `Connection.dispatchCancelAcked`
667
+ * can route a `task.canceling` envelope to the correct subscription
668
+ * before broadcasting the synthetic `qar.client.cancel_acked` client
669
+ * event. Same chain + `task_id` matcher used for normal envelope
670
+ * routing.
671
+ */
672
+ matchesEnvelope(env) {
673
+ return this.matches(env);
674
+ }
675
+ /**
676
+ * Run `fn` inside this subscription's task span context. When the
677
+ * subscription has no span (no OTel provider configured, or a call site
678
+ * that didn't supply a span builder), runs `fn` directly — a strict
679
+ * pass-through.
680
+ *
681
+ * Used by {@link Connection.runInOwningTaskContext} to re-attach the
682
+ * originating task's context around an inbound `tool.request` handler
683
+ * dispatch, so the handler's `qar.client.tool.handler` span (and any
684
+ * outbound work it triggers) parents under the task span rather than the
685
+ * bare WS-receive context.
686
+ */
687
+ runInTaskContext(fn) {
688
+ return this.span !== undefined ? this.span.withContext(fn) : fn();
689
+ }
690
+ }
691
+ /**
692
+ * Raw subscription from `client.receive()` — taps every inbound envelope plus
693
+ * the synthetic `qar.client.*` lifecycle events. The yielded type is
694
+ * `Envelope | ClientEvent`; consumers narrow on `kind` (the QAR `kind`
695
+ * discriminator stays disjoint from `qar.client.*`).
696
+ */
697
+ export class RawSubscription {
698
+ onClose;
699
+ queue = new AsyncQueue();
700
+ constructor(onClose) {
701
+ this.onClose = onClose;
702
+ }
703
+ consider(env) {
704
+ this.queue.push(env);
705
+ // Raw taps don't claim envelopes — they always return false so task
706
+ // subscriptions still see them.
707
+ return false;
708
+ }
709
+ considerClient(ev) {
710
+ if (this.queue.isClosed)
711
+ return;
712
+ this.queue.push(ev);
713
+ }
714
+ fail(err) {
715
+ this.queue.fail(err);
716
+ this.onClose(this);
717
+ }
718
+ close() {
719
+ this.queue.close();
720
+ this.onClose(this);
721
+ }
722
+ next() {
723
+ return this.queue.next();
724
+ }
725
+ async return() {
726
+ if (!this.queue.isClosed) {
727
+ this.queue.close();
728
+ this.onClose(this);
729
+ }
730
+ return { value: undefined, done: true };
731
+ }
732
+ async throw(err) {
733
+ this.queue.close();
734
+ this.onClose(this);
735
+ throw err instanceof Error ? err : new Error(String(err));
736
+ }
737
+ [Symbol.asyncIterator]() {
738
+ return this;
739
+ }
740
+ }
741
+ /** Default cap on the paused-queue size. */
742
+ const DEFAULT_PAUSED_QUEUE_MAX = 100;
743
+ /**
744
+ * Marker `session_id` stamped on synthetic `envelope_parse_error` events
745
+ * the SDK generates when an inbound frame fails JSON-schema parsing.
746
+ * Never crosses the wire — consumers seeing it via `client.receive()`
747
+ * should recognize the zero pattern as "synthetic, not server-derived".
748
+ *
749
+ * The zero-UUID is not used as a wire-side cold-address fallback (cold
750
+ * cancel / continue / resubscribe / respond throw
751
+ * {@link QodoColdAddressError} locally before encoding). This constant
752
+ * is dedicated to the in-memory synthetic-event path.
753
+ */
754
+ const SYNTHETIC_PARSE_ERROR_SESSION_ID = asSessionId('00000000-0000-0000-0000-000000000000');
755
+ /** Default reconnect policy — three attempts at 1s/2s/4s, then give up. */
756
+ const DEFAULT_RECONNECT_MAX_ATTEMPTS = 3;
757
+ const DEFAULT_RECONNECT_INITIAL_BACKOFF_MS = 1000;
758
+ const DEFAULT_RECONNECT_BACKOFF_MULTIPLIER = 2;
759
+ /**
760
+ * Outbound kinds the SDK throttles when the connection is in `flow.pause`
761
+ * state. `tool.response` is the server's blocked party — never throttled.
762
+ * `task.cancel` is user intent to bail — never throttled.
763
+ * `task.resubscribe` is a recovery path and stays unthrottled too: holding a
764
+ * resubscribe behind backpressure would defeat the point of replay.
765
+ */
766
+ const THROTTLED_OUTBOUND_KINDS = new Set([
767
+ 'task.start',
768
+ 'task.continue',
769
+ ]);
770
+ /**
771
+ * Live connection state. Owns the (replaceable) transport and the set of
772
+ * subscriptions; routes inbound envelopes to subscriptions via `consider`.
773
+ *
774
+ * **No connection-level `sessionId`.** Every `task.start` creates a NEW
775
+ * server-derived session, so an envelope's `session_id` is the TASK's
776
+ * session — not a property of the WebSocket. The SDK tracks
777
+ * `task_id → session_id` and `tool_call_id → session_id` here so that
778
+ * outbound ongoing envelopes can be stamped with the right session at
779
+ * encode time without coupling consumer code to per-task session
780
+ * bookkeeping.
781
+ */
782
+ export class Connection {
783
+ factory;
784
+ url;
785
+ headers;
786
+ traceContext;
787
+ metrics;
788
+ subscriptions = new Set();
789
+ /**
790
+ * Map of `task_id → session_id` populated from inbound `task.started`
791
+ * envelopes. Read by {@link sendEnvelope} when encoding `task.continue`
792
+ * / `task.cancel` / `task.resubscribe` so each ongoing envelope
793
+ * carries the same server-derived `session_id` the server bound during
794
+ * admission — preventing multi-task `session_mismatch` without
795
+ * exposing per-task session bookkeeping on the public API.
796
+ *
797
+ * Lifetime: entries are added when `task.started` lands and PRUNED on
798
+ * `task.done` (the wire's terminal ack — task is fully finished). Any
799
+ * later `task.resubscribe` against the same `task_id` would be a
800
+ * cross-process recovery path that has no in-memory session anyway, so
801
+ * pruning doesn't regress that scenario. Cleared on transport close.
802
+ * The auto-reconnect/replay path uses LIVE (non-terminated)
803
+ * subscriptions and adds the session back via the server's replayed
804
+ * `task.started`, so it's unaffected.
805
+ *
806
+ * **Pre-admission invariant.** A live `TaskSubscription` MAY lack an
807
+ * entry in this map: between `tasks.start` writing its outbound
808
+ * `task.start` and the inbound `task.started` ack landing, the
809
+ * subscription is registered in {@link subscriptions} but the
810
+ * `session_id` is not yet pinned here. A WS drop during this window
811
+ * leaves the subscription in an unrecoverable state per the
812
+ * session-identity contract. {@link replayActiveTasks} pre-checks this
813
+ * map before attempting `task.resubscribe` and short-circuits to
814
+ * `sub.fail(QodoColdAddressError)` for un-pinned subs — see that
815
+ * method's JSDoc for the state-machine narrative.
816
+ */
817
+ taskSessions = new Map();
818
+ /**
819
+ * Map of `tool_call_id → session_id` populated from inbound `tool.request`
820
+ * envelopes. Read by {@link sendEnvelope} when encoding `tool.response`,
821
+ * which carries `responses: ToolResponseItem[]` and inherits the parent
822
+ * `tool.request`'s session via the first response item's `tool_call_id`.
823
+ *
824
+ * Lifetime: entries are added when `tool.request` lands and PRUNED on
825
+ * EITHER (a) the outbound `tool.response` hits the wire (single-use
826
+ * — one request, one response), OR (b) the owning task terminates
827
+ * via `task.done` without the consumer ever responding (manual handler
828
+ * returned `undefined` to indicate async-respond-later, then the task
829
+ * timed out / canceled / completed without the response). Without (b)
830
+ * the map would accumulate orphan `tool_call_id` entries on long-lived
831
+ * connections. Cleared on transport close.
832
+ */
833
+ toolCallSessions = new Map();
834
+ /**
835
+ * Reverse index of `session_id → Set<ToolCallId>`. Populated on
836
+ * inbound `tool.request`
837
+ * (one entry per call). Used to sweep {@link toolCallSessions} when
838
+ * the owning task terminates without a `tool.response` ever being
839
+ * sent for some of its outstanding calls — the manual-handler
840
+ * respond-later path explicitly allows this, and without the sweep
841
+ * the per-call map entries would survive forever on long-lived
842
+ * connections.
843
+ *
844
+ * Indexed by `session_id` (not `task_id`) because that's the
845
+ * dimension `tool.request` envelopes carry on the wire — the SDK
846
+ * derives the owning session via the envelope's `session_id` field,
847
+ * not via any task_id field on the payload.
848
+ */
849
+ outstandingToolCallsBySession = new Map();
850
+ /**
851
+ * Reverse index of outbound `task.continue` message_id →
852
+ * `{ taskId, sessionId }` for envelopes
853
+ * that carried a caller-supplied cold-address `sessionId` override.
854
+ * Populated AFTER the wire write (or queued-drain) succeeds; used to
855
+ * identify which `taskSessions` entry to prune when the server rejects
856
+ * the override with `error { code: 'session_mismatch' }`.
857
+ *
858
+ * Without this index, a wrong cold-address override would pollute
859
+ * {@link taskSessions} (because `sendEnvelope`'s post-send write
860
+ * persists the consumer-supplied value as authoritative) and a
861
+ * subsequent no-override call for the same task would silently reuse
862
+ * the already-rejected session instead of raising
863
+ * {@link QodoColdAddressError}.
864
+ *
865
+ * **Generation safety:** the entry stores the sessionId of the
866
+ * override the offending message carried. Pruning is
867
+ * conditional: we delete `taskSessions[taskId]` ONLY when the
868
+ * authoritative entry still matches the rejected sessionId. If a
869
+ * later send for the same task superseded the override with a fresh
870
+ * (valid) sessionId, the rejection for the stale message must NOT
871
+ * blow away the newer authoritative entry. The reverse-map entry for
872
+ * the offending message_id is deleted unconditionally (it's a per-
873
+ * message tombstone; the message is finished one way or the other).
874
+ *
875
+ * Entries are pruned (a) on inbound `error { code: 'session_mismatch' }`
876
+ * for a known offending_message_id (with generation check), and (b)
877
+ * on inbound `task.done` for the matching task_id (terminal sweep).
878
+ * Cleared on transport-terminal cleanup.
879
+ */
880
+ outboundOverrideRefs = new Map();
881
+ state = 'connected';
882
+ /** App-level backpressure flag. Flipped by inbound `flow.pause` / `flow.resume`. */
883
+ paused = false;
884
+ /**
885
+ * FIFO of pre-encoded JSON frames that arrived while paused. Drained in
886
+ * insertion order on `flow.resume`. Pre-encoding at queue-time keeps the
887
+ * drain path a thin pass-through to the transport.
888
+ *
889
+ * Each entry is tagged with the envelope's `messageId` so the owning
890
+ * `TaskSubscription` can retract its frame on early termination (abort or
891
+ * iterator break) — without that link, a queued `task.continue` would
892
+ * still go to the wire on `flow.resume` after the consumer has already
893
+ * canceled, producing `cancel → continue` reordering on the server.
894
+ */
895
+ pausedQueue = [];
896
+ pausedQueueMax;
897
+ /** Reconnect policy. */
898
+ reconnectMaxAttempts;
899
+ reconnectInitialBackoffMs;
900
+ reconnectBackoffMultiplier;
901
+ /**
902
+ * Pending backoff sleeps. Each entry pairs the timer handle with the
903
+ * Promise's resolve callback so `disconnect()` mid-reconnect can both
904
+ * cancel the timer AND wake the loop up so it observes the new state on
905
+ * the next iteration — without the resolver, clearing the timer would
906
+ * leave the await pending forever.
907
+ */
908
+ pendingSleeps = new Set();
909
+ /** The active transport. Mutable — replaced on every successful reconnect. */
910
+ transport;
911
+ /** Bound handler instance reused across every transport (initial + reconnects). */
912
+ handlers;
913
+ constructor(factory, url, headers, initialTransport, handlers, maxPausedQueueSize, reconnect, traceContext, metrics) {
914
+ this.factory = factory;
915
+ this.url = url;
916
+ this.headers = headers;
917
+ this.traceContext = traceContext;
918
+ this.metrics = metrics;
919
+ this.transport = initialTransport;
920
+ this.handlers = handlers;
921
+ // Treat <=0 as "fall back to default" — a zero/negative cap would lock out
922
+ // every throttled send the moment pause arrives, which is never what a
923
+ // consumer wanted. They should opt in explicitly with a positive integer.
924
+ this.pausedQueueMax =
925
+ maxPausedQueueSize !== undefined && maxPausedQueueSize > 0
926
+ ? maxPausedQueueSize
927
+ : DEFAULT_PAUSED_QUEUE_MAX;
928
+ this.reconnectMaxAttempts = pickNonNegativeInt(reconnect?.maxAttempts, DEFAULT_RECONNECT_MAX_ATTEMPTS);
929
+ this.reconnectInitialBackoffMs = pickPositiveInt(reconnect?.initialBackoffMs, DEFAULT_RECONNECT_INITIAL_BACKOFF_MS);
930
+ this.reconnectBackoffMultiplier = pickPositiveNumber(reconnect?.backoffMultiplier, DEFAULT_RECONNECT_BACKOFF_MULTIPLIER);
931
+ }
932
+ /**
933
+ * Construct + open a fresh connection through the factory. The connection
934
+ * captures the factory + url + headers so it can re-open on transport drops
935
+ * without the caller threading them through again.
936
+ */
937
+ static async open(args) {
938
+ // No connection-level `sessionId` — every `task.start` creates a
939
+ // NEW server-derived session, so the connection holds no session at
940
+ // all. Outbound encodes stamp the right per-Task session_id from
941
+ // {@link Connection.taskSessions} / {@link Connection.toolCallSessions}.
942
+ //
943
+ // Handlers are constructed before the `Connection` instance because
944
+ // the factory needs them. The default `ws` transport can't fire
945
+ // `onMessage` until 'open' resolves, so the original `connection?.…`
946
+ // closure was safe with the bundled transport. Custom transports —
947
+ // an in-memory mock, a non-`ws` browser adapter, a synchronous-replay
948
+ // test double — can fire callbacks from inside the factory call,
949
+ // *before* the `Connection` is assigned. With the original code, those
950
+ // early frames silently no-op'd through the `?.` chain. We instead
951
+ // queue inbound events until the `Connection` is wired and drain them
952
+ // synchronously on the next tick, preserving FIFO order.
953
+ let connection = null;
954
+ const pending = [];
955
+ const handlers = {
956
+ onMessage: (data) => {
957
+ if (connection !== null)
958
+ connection.dispatch(data);
959
+ else
960
+ pending.push({ kind: 'message', data });
961
+ },
962
+ onError: (err) => {
963
+ if (connection !== null)
964
+ connection.transportFailed(err);
965
+ else
966
+ pending.push({ kind: 'error', err });
967
+ },
968
+ onClose: (code, reason) => {
969
+ if (connection !== null)
970
+ connection.transportClosed(code, reason);
971
+ else
972
+ pending.push({ kind: 'close', code, reason });
973
+ },
974
+ };
975
+ const transport = await args.factory({
976
+ url: args.url,
977
+ headers: args.headers,
978
+ handlers,
979
+ });
980
+ connection = new Connection(args.factory, args.url, args.headers, transport, handlers, args.maxPausedQueueSize, args.reconnect, args.traceContext, args.metrics);
981
+ // Drain anything the transport fired synchronously inside the factory
982
+ // call. Order matters: messages, errors, closes must replay in the
983
+ // exact sequence they arrived. Track the first terminal event so that
984
+ // if the replay moves the connection out of `connected`, we surface
985
+ // that as a rejection from `open()` rather than handing back an
986
+ // already-broken connection to `QodoClient.connect()` — which would
987
+ // otherwise resolve, wire sub-clients, and let the failure go
988
+ // unobserved.
989
+ let terminalReason;
990
+ for (const ev of pending) {
991
+ if (ev.kind === 'message') {
992
+ connection.dispatch(ev.data);
993
+ }
994
+ else if (ev.kind === 'error') {
995
+ connection.transportFailed(ev.err);
996
+ terminalReason ??= ev.err;
997
+ }
998
+ else {
999
+ connection.transportClosed(ev.code, ev.reason);
1000
+ terminalReason ??= new Error(`Transport closed before connect() returned (code=${ev.code}, reason=${ev.reason || '<none>'})`);
1001
+ }
1002
+ }
1003
+ if (!connection.isOpen) {
1004
+ throw (terminalReason ??
1005
+ new Error('Transport closed before connect() returned (no reason captured)'));
1006
+ }
1007
+ return connection;
1008
+ }
1009
+ /**
1010
+ * Encode and send an outbound envelope. Returns the assigned `message_id` so
1011
+ * the caller can track the parent_message_id chain.
1012
+ *
1013
+ * Optionally accepts a pre-allocated `messageId` so subscriptions can be
1014
+ * registered against a known root before the envelope hits the wire — avoids
1015
+ * a race where an inbound reply could be dispatched before the subscription
1016
+ * is in place.
1017
+ *
1018
+ * If the connection is in `flow.pause` and `out.kind` is throttled
1019
+ * (`task.start` / `task.continue`), the encoded frame is enqueued and drained
1020
+ * once `flow.resume` arrives. The returned `messageId` is still allocated
1021
+ * synchronously so callers can register subscriptions against it before the
1022
+ * envelope hits the wire — same invariant as the unpaused path.
1023
+ *
1024
+ * Throws `QodoBackpressureError` when the queue is at its cap.
1025
+ */
1026
+ sendEnvelope(out, opts) {
1027
+ if (!this.canSend()) {
1028
+ throw new Error('Connection is not open');
1029
+ }
1030
+ const messageId = opts?.messageId ?? asMessageId(uuidv7());
1031
+ const parentMessageId = opts?.parentMessageId;
1032
+ // Wire-shape split: `task.start` (a `_StartEnvelopeBase`)
1033
+ // does NOT carry `session_id` on the wire — the server derives it from
1034
+ // `(tenant_id, payload.idempotency_key)` during the ingress
1035
+ // bind-and-derive phase. QAR's Pydantic model has `extra="forbid"`, so
1036
+ // a stray `session_id` field would fail server-side parsing. Build the
1037
+ // wire view WITHOUT `session_id` for `task.start`; every other kind
1038
+ // (`_OngoingEnvelopeBase`) MUST carry the per-Task session — resolved
1039
+ // below via taskSessions / toolCallSessions / explicit override.
1040
+ const baseCommon = {
1041
+ envelope_version: 1,
1042
+ message_id: messageId,
1043
+ ...(parentMessageId !== undefined ? { parent_message_id: parentMessageId } : {}),
1044
+ // Read the live trace context at emit-time so the active OTel span
1045
+ // (typically the per-public-API span the client is currently inside)
1046
+ // becomes the parent of QAR's server-side WS-upgrade span. Without
1047
+ // OTel configured, this is a fresh random 55-char traceparent — the
1048
+ // server's schema enforces a non-empty match, so an actually-empty
1049
+ // string would fail validation rather than continue the trace.
1050
+ trace_context: this.traceContext.current(),
1051
+ ts: new Date().toISOString(),
1052
+ };
1053
+ let json;
1054
+ if (out.kind === 'task.start') {
1055
+ json = JSON.stringify({
1056
+ ...baseCommon,
1057
+ kind: 'task.start',
1058
+ payload: out.payload,
1059
+ });
1060
+ }
1061
+ else if (out.kind === 'task.forceResume') {
1062
+ // `task.forceResume` is a `_StartEnvelopeBase` shape: it operates
1063
+ // on the consumer's `idempotency_key` rather than a server-bound
1064
+ // `session_id`. QAR derives the session UUID server-side using the
1065
+ // same `uuidv5(QAR_NS_V1, tenant_id + ":" + idempotency_key)`
1066
+ // derivation as `task.start` admission, so a stray outbound
1067
+ // `session_id` would (a) be redundant and (b) fail Pydantic
1068
+ // `extra="forbid"` validation on the QAR side. Strip the field at
1069
+ // emit-time, matching the `task.start` path.
1070
+ json = JSON.stringify({
1071
+ ...baseCommon,
1072
+ kind: 'task.forceResume',
1073
+ payload: out.payload,
1074
+ });
1075
+ }
1076
+ else {
1077
+ const sessionId = this.resolveOutboundSessionId(out, opts?.sessionId);
1078
+ const base = { ...baseCommon, session_id: sessionId };
1079
+ let envelope;
1080
+ switch (out.kind) {
1081
+ case 'task.continue':
1082
+ envelope = { ...base, kind: 'task.continue', payload: out.payload };
1083
+ break;
1084
+ case 'task.cancel':
1085
+ envelope = { ...base, kind: 'task.cancel', payload: out.payload };
1086
+ break;
1087
+ case 'task.resubscribe':
1088
+ envelope = { ...base, kind: 'task.resubscribe', payload: out.payload };
1089
+ break;
1090
+ case 'tool.response':
1091
+ envelope = { ...base, kind: 'tool.response', payload: out.payload };
1092
+ break;
1093
+ }
1094
+ json = JSON.stringify(envelope);
1095
+ }
1096
+ // Capture the cold-address override info BEFORE the queue/send
1097
+ // branches, but DON'T mutate
1098
+ // {@link taskSessions} yet — that write must wait for send-success
1099
+ // (or for drain-success on the queued path). Stamping the
1100
+ // authoritative map at this point would leave a stale entry behind
1101
+ // if `transport.send` throws, the paused-queue overflows, or the
1102
+ // connection drops mid-pause before the queued frame drains.
1103
+ //
1104
+ // Only `task.continue` actually NEEDS the post-send save: a
1105
+ // subscription started from `tasks.continue(..., { sessionId })`
1106
+ // can trigger an internal `task.cancel` through TWO paths that
1107
+ // both go through `Connection.sendEnvelope` WITHOUT re-threading
1108
+ // the option:
1109
+ // (a) TaskSubscription.onEarlyReturn — iterator break sends a
1110
+ // best-effort `task.cancel` (the consumer's iterator break
1111
+ // implies cancel intent).
1112
+ // (b) `TaskClient.bindAbort` — the consumer's AbortSignal fires
1113
+ // a `task.cancel` with reason `'abort signal'`.
1114
+ // Without the post-send save, BOTH paths would raise
1115
+ // `QodoColdAddressError` (the connection has no in-memory
1116
+ // session for this task_id). The save unblocks (a) directly;
1117
+ // (b) is double-protected by `bindAbort` forwarding the
1118
+ // original `opts.sessionId` to its own `sendEnvelope` call.
1119
+ // `task.cancel` and `task.resubscribe` don't spawn follow-up
1120
+ // internal sends (the former is terminal; the latter passes
1121
+ // `suppressEarlyReturnCancel: true`), so they don't need to
1122
+ // leave behind a sticky entry — keeping the map tighter means
1123
+ // a future no-override call for the same task_id correctly
1124
+ // raises `QodoColdAddressError`.
1125
+ const overrideTaskSave = opts?.sessionId !== undefined && out.kind === 'task.continue'
1126
+ ? { taskId: out.payload.task_id, sessionId: opts.sessionId }
1127
+ : undefined;
1128
+ if (this.paused && THROTTLED_OUTBOUND_KINDS.has(out.kind)) {
1129
+ if (this.pausedQueue.length >= this.pausedQueueMax) {
1130
+ // Throw BEFORE mutating any authoritative state (taskSessions,
1131
+ // pausedQueue itself) so a queue-overflow leaves zero side
1132
+ // effects from this call.
1133
+ throw new QodoBackpressureError(this.pausedQueue.length, this.pausedQueueMax);
1134
+ }
1135
+ this.pausedQueue.push({
1136
+ messageId,
1137
+ json,
1138
+ ...(overrideTaskSave !== undefined ? { overrideToSave: overrideTaskSave } : {}),
1139
+ });
1140
+ return messageId;
1141
+ }
1142
+ this.transport.send(json);
1143
+ // Persist the cold-address override
1144
+ // ONLY AFTER the wire write returned without throwing. A
1145
+ // transport-mid-call failure now leaves no stale entry in
1146
+ // {@link taskSessions}, so a subsequent no-override call for the
1147
+ // same task_id correctly raises `QodoColdAddressError` instead of
1148
+ // silently reusing the never-sent override.
1149
+ if (overrideTaskSave !== undefined) {
1150
+ this.taskSessions.set(overrideTaskSave.taskId, overrideTaskSave.sessionId);
1151
+ // Note the outbound
1152
+ // message_id with the sessionId it carried so an inbound
1153
+ // `session_mismatch` for this frame can prune the polluted
1154
+ // authoritative entry above — but ONLY when the entry has not
1155
+ // been superseded by a fresh override for the same task.
1156
+ this.outboundOverrideRefs.set(messageId, {
1157
+ taskId: overrideTaskSave.taskId,
1158
+ sessionId: overrideTaskSave.sessionId,
1159
+ });
1160
+ }
1161
+ // Stamp `resubscribes_sent_total` only after the wire write returned
1162
+ // without throwing — counting before send would over-count when the
1163
+ // transport closes mid-call and `send` raises. `task.resubscribe`
1164
+ // isn't in `THROTTLED_OUTBOUND_KINDS`, so it
1165
+ // never enters the paused-queue branch above; the only success path is
1166
+ // the direct send.
1167
+ if (out.kind === 'task.resubscribe') {
1168
+ this.metrics?.recordResubscribeSent();
1169
+ }
1170
+ // Prune both forward and reverse session-index maps after the
1171
+ // outbound `tool.response` lands on the wire — each `tool_call_id`
1172
+ // is single-use (one request → one response per call). Bounds the
1173
+ // maps on long-lived connections running many tool calls.
1174
+ if (out.kind === 'tool.response') {
1175
+ for (const r of out.payload.responses) {
1176
+ const tcid = r.tool_call_id;
1177
+ const sid = this.toolCallSessions.get(tcid);
1178
+ this.toolCallSessions.delete(tcid);
1179
+ if (sid !== undefined) {
1180
+ const outstanding = this.outstandingToolCallsBySession.get(sid);
1181
+ if (outstanding !== undefined) {
1182
+ outstanding.delete(tcid);
1183
+ if (outstanding.size === 0) {
1184
+ this.outstandingToolCallsBySession.delete(sid);
1185
+ }
1186
+ }
1187
+ }
1188
+ }
1189
+ }
1190
+ return messageId;
1191
+ }
1192
+ /**
1193
+ * Resolve the per-Task `session_id` for an outbound ongoing envelope.
1194
+ * Caller-supplied `override` wins (the dispatcher path passes the
1195
+ * captured inbound `session_id` directly, and the cold-address
1196
+ * public-API path passes the consumer-supplied option); otherwise look
1197
+ * up by the payload's task_id (`task.continue` / `task.cancel` /
1198
+ * `task.resubscribe`) or by the first response item's tool_call_id
1199
+ * (`tool.response`).
1200
+ *
1201
+ * When neither an override NOR an in-memory entry is available, throw
1202
+ * {@link QodoColdAddressError} synchronously. Throwing locally:
1203
+ *
1204
+ * - puts the cold-address remediation right in the error message
1205
+ * (consumer should pass `sessionId` from durable storage),
1206
+ * - saves a wire round-trip + the misleading `session_mismatch`
1207
+ * error message ("envelope session_id differs from connection
1208
+ * session" — true but unactionable),
1209
+ * - matches the doctrinal correct shape (make it right, not make
1210
+ * it work).
1211
+ *
1212
+ * The primary cross-task session-leak guard is the lookup itself when
1213
+ * admission HAS completed.
1214
+ */
1215
+ resolveOutboundSessionId(out, override) {
1216
+ if (override !== undefined)
1217
+ return override;
1218
+ switch (out.kind) {
1219
+ case 'task.continue':
1220
+ case 'task.cancel':
1221
+ case 'task.resubscribe': {
1222
+ const taskId = out.payload.task_id;
1223
+ const sessionId = this.taskSessions.get(taskId);
1224
+ if (sessionId === undefined) {
1225
+ throw new QodoColdAddressError('task', taskId);
1226
+ }
1227
+ return sessionId;
1228
+ }
1229
+ case 'tool.response': {
1230
+ const first = out.payload.responses[0];
1231
+ // `responses` is validated non-empty by `ToolClient.respond` /
1232
+ // the auto-dispatch path; reach here only via direct internal
1233
+ // mis-use, so the throw is a hard invariant. Tag the empty case
1234
+ // explicitly to make the violated invariant visible.
1235
+ if (first === undefined) {
1236
+ throw new QodoColdAddressError('tool_call', '<empty-responses-array>');
1237
+ }
1238
+ const toolCallId = first.tool_call_id;
1239
+ const sessionId = this.toolCallSessions.get(toolCallId);
1240
+ if (sessionId === undefined) {
1241
+ throw new QodoColdAddressError('tool_call', toolCallId);
1242
+ }
1243
+ return sessionId;
1244
+ }
1245
+ }
1246
+ }
1247
+ /**
1248
+ * Retract a queued throttled envelope by its `message_id`. Called by
1249
+ * `TaskSubscription` when it terminates early (abort signal, iterator
1250
+ * break, transport failure) so the consumer's cancel intent doesn't race
1251
+ * with their own queued frame on the next `flow.resume`.
1252
+ *
1253
+ * No-op if the messageId isn't queued (already drained, never queued, or
1254
+ * a different envelope kind that doesn't enter the throttle path).
1255
+ * Returns whether an entry was removed — informational only.
1256
+ */
1257
+ dropQueued(messageId) {
1258
+ if (this.pausedQueue.length === 0)
1259
+ return false;
1260
+ const idx = this.pausedQueue.findIndex((entry) => entry.messageId === messageId);
1261
+ if (idx === -1)
1262
+ return false;
1263
+ this.pausedQueue.splice(idx, 1);
1264
+ return true;
1265
+ }
1266
+ /** Send a raw envelope verbatim — escape hatch. */
1267
+ sendRawEnvelope(env) {
1268
+ if (!this.canSend()) {
1269
+ throw new Error('Connection is not open');
1270
+ }
1271
+ this.transport.send(JSON.stringify(env));
1272
+ }
1273
+ /**
1274
+ * Look up the server-derived `session_id` for a known `task_id`.
1275
+ * Used by span recorders that want to stamp `qar.session_id`
1276
+ * on per-Task spans without coupling caller code to the per-Task lookup
1277
+ * map. Returns `undefined` when no `task.started` ack has arrived for
1278
+ * this `task_id` yet (the start path's span is opened pre-admission and
1279
+ * must tolerate this).
1280
+ */
1281
+ getSessionForTask(taskId) {
1282
+ return this.taskSessions.get(taskId);
1283
+ }
1284
+ /**
1285
+ * Look up the `session_id` of the inbound `tool.request` a given
1286
+ * `tool_call_id` came from. Used by `ToolClient.respond` to stamp
1287
+ * its outbound `tool.response` span with the right session without
1288
+ * threading the value through every call site. Returns `undefined` when
1289
+ * no matching `tool.request` arrived (test-only path, or a malformed
1290
+ * consumer call).
1291
+ */
1292
+ getSessionForToolCall(toolCallId) {
1293
+ return this.toolCallSessions.get(toolCallId);
1294
+ }
1295
+ /**
1296
+ * Run `fn` inside the span context of the live `TaskSubscription` that
1297
+ * OWNS `env` by lineage, so an inbound `tool.request` dispatched to the
1298
+ * consumer's handler inherits the originating turn's trace context (its
1299
+ * `qar.client.task.*` span). Without this, the fanout invokes the handler
1300
+ * under the bare WS-receive context and the SDK's `qar.client.tool.handler`
1301
+ * span — plus the consumer's outbound MCP/fetch traceparent — are orphaned
1302
+ * from the task trace.
1303
+ *
1304
+ * Ownership is the unique subscription whose {@link TaskSubscription.ownsLineage}
1305
+ * holds for `env` — its `parent_message_id` traces (through real
1306
+ * `parent_message_id` links) back to that subscription's own outbound root.
1307
+ * This is unambiguous even when same-task subscriptions overlap (a live
1308
+ * auto-paused `tasks.start` sub alongside an active `tasks.continue`): the
1309
+ * passive sub absorbs the active turn's stream into its routing chain via
1310
+ * the `task_id` fallback, but never into its ownership lineage, so it never
1311
+ * wins — at any tool-call depth (multi-round T1→D2→T2).
1312
+ *
1313
+ * Terminated subscriptions are skipped (a live task owns an in-flight tool
1314
+ * call). Falls through to `fn()` unchanged when no owner is found — a strict
1315
+ * no-op for non-OTel consumers, for a `tool.request` whose lineage root was
1316
+ * never sent by this process (cold `task.resubscribe` replay), and for an
1317
+ * owner with no span.
1318
+ */
1319
+ runInOwningTaskContext(env, fn) {
1320
+ for (const sub of this.subscriptions) {
1321
+ if (sub instanceof TaskSubscription && !sub.isTerminated && sub.ownsLineage(env)) {
1322
+ return sub.runInTaskContext(fn);
1323
+ }
1324
+ }
1325
+ return fn();
1326
+ }
1327
+ /** Register a subscription. Caller is responsible for unregistering on close. */
1328
+ subscribe(sub) {
1329
+ this.subscriptions.add(sub);
1330
+ }
1331
+ unsubscribe(sub) {
1332
+ this.subscriptions.delete(sub);
1333
+ }
1334
+ /**
1335
+ * Tear down: close transport, fail all subscriptions cleanly. Idempotent.
1336
+ * Called by `QodoClient.disconnect()` and by the `onClose` transport handler
1337
+ * for clean closes. Mid-reconnect calls switch state to `disconnecting` so
1338
+ * the in-flight loop bails on the next tick.
1339
+ */
1340
+ close() {
1341
+ if (this.state === 'disconnected' || this.state === 'failed')
1342
+ return;
1343
+ const wasReconnecting = this.state === 'reconnecting';
1344
+ this.state = wasReconnecting ? 'disconnecting' : 'disconnected';
1345
+ this.clearBackpressureState();
1346
+ this.clearTerminalState();
1347
+ this.clearPendingTimers();
1348
+ try {
1349
+ this.transport.close(1000, 'client disconnect');
1350
+ }
1351
+ catch {
1352
+ // Best-effort — transport may be wedged after a failure.
1353
+ }
1354
+ if (!wasReconnecting) {
1355
+ // No outstanding async work — close subs synchronously.
1356
+ for (const sub of [...this.subscriptions]) {
1357
+ sub.close();
1358
+ }
1359
+ this.subscriptions.clear();
1360
+ }
1361
+ // For wasReconnecting=true: the reconnect loop sees `disconnecting`
1362
+ // on its next iteration and walks the close path itself, including
1363
+ // sub cleanup. This avoids a race where two paths both try to close
1364
+ // the same subscriptions.
1365
+ }
1366
+ /**
1367
+ * Drop any queued envelopes and clear the paused flag. Called on every
1368
+ * tear-down path (clean close, transport error, transport close) — leaving
1369
+ * frames in the queue past disconnect would either leak memory or surface
1370
+ * stale outbound on a future connection that reused this object.
1371
+ *
1372
+ * Does NOT clear the per-Task / per-Tool-call session-id lookup maps —
1373
+ * those must survive the reconnect window so `replayActiveTasks` can
1374
+ * stamp the right `session_id` on each `task.resubscribe` envelope it
1375
+ * emits. Final cleanup of the session maps lives in
1376
+ * {@link clearTerminalState}, invoked from the actual tear-down paths.
1377
+ */
1378
+ clearBackpressureState() {
1379
+ this.paused = false;
1380
+ this.pausedQueue.length = 0;
1381
+ }
1382
+ /**
1383
+ * Drop the per-Task and per-Tool-call session-id lookup maps. Called
1384
+ * from terminal cleanup paths (`close`,
1385
+ * `cleanCloseAndCleanup`, `failHardAndCleanup`, `finalizeDisconnect`) so
1386
+ * a future `connect()` reusing the same `Connection` object starts with
1387
+ * an empty session-id table. Distinct from {@link clearBackpressureState}
1388
+ * because the reconnect path MUST keep these populated for replay.
1389
+ */
1390
+ clearTerminalState() {
1391
+ this.taskSessions.clear();
1392
+ this.toolCallSessions.clear();
1393
+ this.outstandingToolCallsBySession.clear();
1394
+ this.outboundOverrideRefs.clear();
1395
+ }
1396
+ /** Whether the connection accepts outbound sends right now. */
1397
+ get isOpen() {
1398
+ return this.state === 'connected' && this.transport.isOpen;
1399
+ }
1400
+ /**
1401
+ * Whether the connection is mid-reconnect. `isOpen` is false during this
1402
+ * window; outbound sends throw. Tests + observability surfaces read this
1403
+ * to render a "reconnecting" indicator.
1404
+ */
1405
+ get isReconnecting() {
1406
+ return this.state === 'reconnecting';
1407
+ }
1408
+ /**
1409
+ * Whether the server has signalled `flow.pause` and we have not yet seen
1410
+ * `flow.resume`. While `true`, throttled outbound kinds are queued.
1411
+ */
1412
+ get isPaused() {
1413
+ return this.paused;
1414
+ }
1415
+ /**
1416
+ * Snapshot of the paused-queue depth. Useful for tests and observability;
1417
+ * not a live counter (no subscription, no event).
1418
+ */
1419
+ get pausedQueueDepth() {
1420
+ return this.pausedQueue.length;
1421
+ }
1422
+ /** Raw outbound predicate — connected AND transport is in OPEN state. */
1423
+ canSend() {
1424
+ return this.state === 'connected' && this.transport.isOpen;
1425
+ }
1426
+ dispatch(text) {
1427
+ if (this.state === 'disconnected' || this.state === 'failed')
1428
+ return;
1429
+ let env;
1430
+ try {
1431
+ env = parseEnvelope(text);
1432
+ }
1433
+ catch (err) {
1434
+ // Malformed inbound — synthesize an `error` envelope so consumers see it
1435
+ // rather than silently dropping the frame. The synthetic carries no
1436
+ // parent_message_id, so it won't claim any task subscription; raw taps
1437
+ // see it, task iterators don't.
1438
+ //
1439
+ // The synthetic never reaches the wire, but the `Envelope` shape
1440
+ // requires a `session_id`. Use the zero UUID as a
1441
+ // marker — the value is opaque to consumers (the only ones that
1442
+ // see this are raw taps via `client.receive()`) and the zero pattern
1443
+ // makes "parse-error synthetic" cheap to recognize in logs.
1444
+ const synthetic = {
1445
+ envelope_version: 1,
1446
+ message_id: asMessageId(uuidv7()),
1447
+ session_id: SYNTHETIC_PARSE_ERROR_SESSION_ID,
1448
+ trace_context: this.traceContext.current(),
1449
+ ts: new Date().toISOString(),
1450
+ kind: 'error',
1451
+ payload: {
1452
+ code: 'envelope_parse_error',
1453
+ message: err instanceof Error ? err.message : String(err),
1454
+ },
1455
+ };
1456
+ this.fanout(synthetic);
1457
+ return;
1458
+ }
1459
+ // Capture per-Task `session_id` from the admission ack BEFORE
1460
+ // fanout so any subscription that synchronously re-enters
1461
+ // `sendEnvelope` (e.g. a tool-handler completing inside the same tick
1462
+ // that received `task.started`) sees the right session for outbound
1463
+ // ongoing envelopes. Same pattern for `tool.request` → `tool_call_id`
1464
+ // mapping, populated for every call in the batched payload.
1465
+ //
1466
+ // Pruning: a long-lived connection running
1467
+ // many sequential tasks / tool calls would accumulate entries
1468
+ // unboundedly without active eviction. Prune `taskSessions` on
1469
+ // `task.done` (the wire's terminal ack — task is fully finished;
1470
+ // post-done `task.resubscribe` for cross-pod recovery doesn't have
1471
+ // the session in-memory either way) and let `tool.response` send
1472
+ // prune `toolCallSessions` (each response item's `tool_call_id` is
1473
+ // the last reference). Reconnect/replay isn't affected because the
1474
+ // replay path uses `task.resubscribe` against a still-LIVE
1475
+ // subscription's `task_id`, which is added back to the map on the
1476
+ // server's replayed `task.started`.
1477
+ if (env.kind === 'task.started') {
1478
+ const payload = env.payload;
1479
+ this.taskSessions.set(payload.task_id, env.session_id);
1480
+ }
1481
+ else if (env.kind === 'task.force_resumed') {
1482
+ // `task.force_resumed` is the ack for `task.forceResume` — the
1483
+ // recovered session's existing `task_id` is on the payload, and
1484
+ // the inherited `session_id` is the derived UUID. Bind the
1485
+ // per-Task session map so a subsequent
1486
+ // `tasks.continue(taskId)` resolves the right session at
1487
+ // outbound encode time without forcing the consumer to thread
1488
+ // `sessionId` through every call.
1489
+ const payload = env.payload;
1490
+ this.taskSessions.set(payload.task_id, env.session_id);
1491
+ }
1492
+ else if (env.kind === 'task.done') {
1493
+ // Cast through `unknown` because the codegen payload's `task_id` is
1494
+ // the unbranded `string` shape; the SDK overlay brands it as
1495
+ // `TaskId` everywhere else but the inbound parse path doesn't
1496
+ // re-brand. Safe — the wire value IS a TaskId by invariant.
1497
+ const payload = env.payload;
1498
+ const taskId = payload.task_id;
1499
+ const sessionForTask = this.taskSessions.get(taskId);
1500
+ this.taskSessions.delete(taskId);
1501
+ // Sweep any outbound-override
1502
+ // refs pointing at this task — the task is terminal, no further
1503
+ // sends can need the message_id → {taskId, sessionId} lookup.
1504
+ for (const [mid, ref] of this.outboundOverrideRefs) {
1505
+ if (ref.taskId === taskId)
1506
+ this.outboundOverrideRefs.delete(mid);
1507
+ }
1508
+ // Sweep any outstanding tool_call_ids that were never responded
1509
+ // to before
1510
+ // the task terminated (manual handler returned `undefined` then
1511
+ // the task timed out / canceled / completed without a response).
1512
+ if (sessionForTask !== undefined) {
1513
+ const outstanding = this.outstandingToolCallsBySession.get(sessionForTask);
1514
+ if (outstanding !== undefined) {
1515
+ for (const tcid of outstanding) {
1516
+ this.toolCallSessions.delete(tcid);
1517
+ }
1518
+ this.outstandingToolCallsBySession.delete(sessionForTask);
1519
+ }
1520
+ }
1521
+ }
1522
+ else if (env.kind === 'error') {
1523
+ // A `session_mismatch` error against an outbound that carried a
1524
+ // caller-supplied cold-address override means THAT override was
1525
+ // wrong. Prune the polluted entry in `taskSessions` so a
1526
+ // subsequent no-override call raises `QodoColdAddressError` —
1527
+ // BUT only when the authoritative entry still matches the
1528
+ // rejected session. If a later send superseded this override
1529
+ // with a newer (valid) sessionId for the same task, the stale
1530
+ // mismatch must NOT blow away the newer entry (generation
1531
+ // safety).
1532
+ //
1533
+ // The reverse-map tombstone for the offending message_id is
1534
+ // always deleted: that message is finished one way or the other.
1535
+ const errorPayload = env.payload;
1536
+ if (errorPayload.code === 'session_mismatch') {
1537
+ const offendingId = typeof errorPayload.offending_message_id === 'string'
1538
+ ? errorPayload.offending_message_id
1539
+ : undefined;
1540
+ if (offendingId !== undefined) {
1541
+ const ref = this.outboundOverrideRefs.get(offendingId);
1542
+ if (ref !== undefined) {
1543
+ const authoritative = this.taskSessions.get(ref.taskId);
1544
+ if (authoritative === ref.sessionId) {
1545
+ // The override is STILL the authoritative entry — it
1546
+ // hasn't been superseded by a fresher send. Prune.
1547
+ this.taskSessions.delete(ref.taskId);
1548
+ }
1549
+ // The reverse-map entry is per-message; remove
1550
+ // unconditionally now that this message has resolved
1551
+ // (server rejection counts as resolution).
1552
+ this.outboundOverrideRefs.delete(offendingId);
1553
+ }
1554
+ // `tool.response` cleanup: even though the per-send prune
1555
+ // already removes the tool_call_id mapping post-send, a
1556
+ // wrong override on `tool.response` would have polluted
1557
+ // `toolCallSessions` before the prune ran (the wire send
1558
+ // succeeds before the prune; only the server's rejection
1559
+ // arrives later). The post-send prune already deletes the
1560
+ // entry on success, so by the time the session_mismatch
1561
+ // lands the entry is gone — nothing to clean. This branch
1562
+ // is here for symmetry with the task-id path; documented
1563
+ // so future changes don't reintroduce a leak.
1564
+ }
1565
+ }
1566
+ }
1567
+ else if (env.kind === 'tool.request') {
1568
+ const calls = env.payload.calls;
1569
+ let outstanding = this.outstandingToolCallsBySession.get(env.session_id);
1570
+ if (outstanding === undefined) {
1571
+ outstanding = new Set();
1572
+ this.outstandingToolCallsBySession.set(env.session_id, outstanding);
1573
+ }
1574
+ for (const c of calls) {
1575
+ const tcid = c.tool_call_id;
1576
+ this.toolCallSessions.set(tcid, env.session_id);
1577
+ outstanding.add(tcid);
1578
+ }
1579
+ }
1580
+ // App-level backpressure. Update internal state BEFORE fanout so
1581
+ // consumers reading `isPaused` from a TaskEvent handler see the new value.
1582
+ // For `flow.resume`, drain the queue first too — any throttled send the
1583
+ // consumer issues from the same handler then takes the unpaused fast path.
1584
+ if (env.kind === 'flow.pause') {
1585
+ this.paused = true;
1586
+ }
1587
+ else if (env.kind === 'flow.resume') {
1588
+ this.paused = false;
1589
+ this.drainPausedQueue();
1590
+ }
1591
+ this.fanout(env);
1592
+ // Surface `task.canceling` to task iterators as a
1593
+ // `qar.client.cancel_acked` synthetic client event. The envelope
1594
+ // itself still went through `fanout` so raw `client.receive()` taps
1595
+ // observe the wire frame; the synthetic event is what reaches
1596
+ // consumers iterating via `for await (const event of
1597
+ // client.tasks.start(...))` without expanding the `TaskEvent`
1598
+ // switch surface.
1599
+ if (env.kind === 'task.canceling') {
1600
+ this.dispatchCancelAcked(env);
1601
+ }
1602
+ }
1603
+ /**
1604
+ * Translate an inbound `task.canceling` envelope into a typed
1605
+ * `ClientCancelAckedEvent` and route it to the subscription whose chain
1606
+ * (or `task_id`) matches. Scoped — not connection-wide — so a consumer
1607
+ * running multiple tasks only sees the ack for the task they're
1608
+ * iterating.
1609
+ *
1610
+ * Skips taps (`RawSubscription` already received the envelope through
1611
+ * `fanout`); fans into `TaskSubscription` (matching by `task_id` /
1612
+ * chain) and observer ports (no-op `considerClient`).
1613
+ */
1614
+ dispatchCancelAcked(env) {
1615
+ const payload = env.payload;
1616
+ const event = {
1617
+ kind: 'qar.client.cancel_acked',
1618
+ task_id: payload.task_id,
1619
+ ...(typeof payload.received_by_pod === 'string'
1620
+ ? { receivedByPod: payload.received_by_pod }
1621
+ : {}),
1622
+ ...(typeof payload.owning_pod === 'string'
1623
+ ? { owningPod: payload.owning_pod }
1624
+ : {}),
1625
+ ...(typeof payload.cancel_reason === 'string'
1626
+ ? { cancelReason: payload.cancel_reason }
1627
+ : {}),
1628
+ ...(typeof payload.expected_terminal_within_s === 'number'
1629
+ ? { expectedTerminalWithinS: payload.expected_terminal_within_s }
1630
+ : {}),
1631
+ };
1632
+ const snapshot = [...this.subscriptions];
1633
+ for (const sub of snapshot) {
1634
+ if (sub instanceof TaskSubscription) {
1635
+ // Only route to the subscription that owns this task. Other
1636
+ // task subscriptions on the same connection should not see a
1637
+ // cancel-ack for a peer task they don't drive.
1638
+ if (sub.matchesEnvelope(env)) {
1639
+ sub.considerClient(event);
1640
+ }
1641
+ }
1642
+ else {
1643
+ // Observer ports + raw taps: pass through. `KindObserverPort.considerClient`
1644
+ // is a no-op; `RawSubscription` queues the event for the raw tap so
1645
+ // consumers tapping `client.receive()` see both the wire envelope
1646
+ // (via fanout) AND the typed client event on one iterator.
1647
+ sub.considerClient(event);
1648
+ }
1649
+ }
1650
+ }
1651
+ /**
1652
+ * Flush the paused queue in FIFO order. Stops if the transport closes
1653
+ * mid-drain (a `transport.send` after close would throw and lose the
1654
+ * remaining frames silently). Caller is responsible for ensuring `paused`
1655
+ * is `false` before invoking — this method does not reset the flag.
1656
+ *
1657
+ * Drain failure preserves queue state. The earlier shape ("slice +
1658
+ * clear + iterate") dropped every
1659
+ * REMAINING entry (including their `overrideToSave` metadata) when
1660
+ * `transport.send` threw mid-drain. The post-send-success invariant
1661
+ * says the override write is conditional on wire-success; an entry
1662
+ * that
1663
+ * never reaches the wire MUST stay in the queue so the next drain
1664
+ * (after the transport recovers and `flow.resume` arrives again,
1665
+ * or via the reconnect-loop's eventual retry) can replay it with
1666
+ * the same metadata intact.
1667
+ *
1668
+ * New shape: walk by INDEX without clearing the queue first.
1669
+ * - For each entry, send + commit override. If both succeed,
1670
+ * mark the index as drained.
1671
+ * - If `transport.send` throws OR `canSend()` flips mid-drain,
1672
+ * stop immediately, splice off only the successfully drained
1673
+ * prefix. Everything from the failing index onward stays in
1674
+ * `pausedQueue` for the next drain attempt.
1675
+ *
1676
+ * The naive `while (length > 0) shift()` form is O(n²) because each
1677
+ * `shift()` reindexes the whole array; with a user-configurable cap
1678
+ * the blocking time on a large drain shows up in the inbound-message
1679
+ * handler. Index-walk + single splice at the end keeps the drain O(n).
1680
+ */
1681
+ drainPausedQueue() {
1682
+ if (this.pausedQueue.length === 0)
1683
+ return;
1684
+ let drainedCount = 0;
1685
+ try {
1686
+ for (let i = 0; i < this.pausedQueue.length; i += 1) {
1687
+ if (!this.canSend())
1688
+ break;
1689
+ const entry = this.pausedQueue[i];
1690
+ this.transport.send(entry.json);
1691
+ // Commit the cold-address override to {@link taskSessions} now
1692
+ // that the queued frame
1693
+ // actually landed on the wire. Doing this at queue-push time
1694
+ // would pollute the map with sessions that never reached the
1695
+ // server if the transport dropped mid-pause.
1696
+ if (entry.overrideToSave !== undefined) {
1697
+ this.taskSessions.set(entry.overrideToSave.taskId, entry.overrideToSave.sessionId);
1698
+ // Track the drained message_id → task_id (with sessionId for
1699
+ // generation-safety) so a subsequent `session_mismatch` can
1700
+ // prune the polluted entry only when it hasn't been
1701
+ // superseded.
1702
+ this.outboundOverrideRefs.set(entry.messageId, {
1703
+ taskId: entry.overrideToSave.taskId,
1704
+ sessionId: entry.overrideToSave.sessionId,
1705
+ });
1706
+ }
1707
+ drainedCount += 1;
1708
+ }
1709
+ }
1710
+ finally {
1711
+ // Splice off ONLY the successfully drained prefix. Any entry
1712
+ // from `drainedCount` onward stays in the queue — its
1713
+ // `overrideToSave` metadata is preserved for the next drain.
1714
+ if (drainedCount > 0) {
1715
+ this.pausedQueue.splice(0, drainedCount);
1716
+ }
1717
+ }
1718
+ }
1719
+ fanout(env) {
1720
+ // Snapshot — `consider` may unregister via `close`/`onClose`.
1721
+ const snapshot = [...this.subscriptions];
1722
+ for (const sub of snapshot) {
1723
+ sub.consider(env);
1724
+ }
1725
+ }
1726
+ broadcastClient(ev) {
1727
+ const snapshot = [...this.subscriptions];
1728
+ for (const sub of snapshot) {
1729
+ sub.considerClient(ev);
1730
+ }
1731
+ }
1732
+ /**
1733
+ * Wire-level error from the transport. If the user hasn't disconnected and
1734
+ * we have something worth replaying, kick off the reconnect loop. Otherwise
1735
+ * fail the subscriptions and shut down.
1736
+ *
1737
+ * Re-entrancy: a custom `WSTransport.close()` may synchronously invoke
1738
+ * `handlers.onClose` (the `WSTransport` interface places no async-vs-sync
1739
+ * contract on close-event timing — only that one fires). If we called
1740
+ * `disposeTransportSafely()` while `state` was still `'connected'`, the
1741
+ * synchronous `onClose` would re-enter `transportClosed`, see state still
1742
+ * `'connected'`, and start its own reconnect loop. Then this method would
1743
+ * return and start ANOTHER one. To prevent this, we transition state OUT
1744
+ * of `'connected'` before disposing — any re-entrant `transportClosed` /
1745
+ * `transportFailed` then sees the guard and returns early.
1746
+ */
1747
+ transportFailed(err) {
1748
+ if (this.state !== 'connected')
1749
+ return;
1750
+ // Hold a guard state so any synchronous re-entry from `transport.close()`
1751
+ // sees a non-connected state and bails. We pick `'reconnecting'` directly
1752
+ // — `runReconnectLoop` would set it anyway, and the no-reconnect branch
1753
+ // overrides it via `failHardAndCleanup`.
1754
+ this.state = 'reconnecting';
1755
+ this.disposeTransportSafely();
1756
+ if (this.shouldAttemptReconnect()) {
1757
+ void this.runReconnectLoop(err.message);
1758
+ return;
1759
+ }
1760
+ this.failHardAndCleanup(err);
1761
+ }
1762
+ /**
1763
+ * Transport closed. Clean codes (1000/1001/1005) end the subs cleanly even
1764
+ * if reconnect would otherwise be possible — the server told us to go.
1765
+ * Unexpected codes drop into the same reconnect loop as `transportFailed`.
1766
+ *
1767
+ * Re-entrancy: same guard as `transportFailed` — state transitions before
1768
+ * disposal so a synchronous `onClose` from `transport.close()` can't fire
1769
+ * a second reconnect loop. Clean closes use a separate guard state
1770
+ * (`'disconnecting'` → `'disconnected'`) for the same reason.
1771
+ */
1772
+ transportClosed(code, reason) {
1773
+ if (this.state !== 'connected')
1774
+ return;
1775
+ const cleanClose = code === 1000 || code === 1001 || code === 1005;
1776
+ // Set guard state BEFORE the dispose. Pick the state we'll end up in:
1777
+ // `'disconnected'` for a clean close, `'reconnecting'` otherwise (or
1778
+ // overridden to `'failed'` when no reconnect is warranted).
1779
+ this.state = cleanClose ? 'disconnected' : 'reconnecting';
1780
+ this.disposeTransportSafely();
1781
+ if (cleanClose) {
1782
+ // We already set `state = 'disconnected'`; cleanCloseAndCleanup is
1783
+ // idempotent on that and just runs the cleanup half.
1784
+ this.cleanCloseAndCleanup();
1785
+ return;
1786
+ }
1787
+ if (this.shouldAttemptReconnect()) {
1788
+ void this.runReconnectLoop(`unexpected close (code=${code}, reason=${reason})`);
1789
+ return;
1790
+ }
1791
+ const err = new Error(`WebSocket closed unexpectedly (code=${code}, reason=${reason})`);
1792
+ this.failHardAndCleanup(err);
1793
+ }
1794
+ /**
1795
+ * Reconnect is worth attempting only when there's at least one in-flight
1796
+ * task subscription that can be replayed. A connection with no live tasks
1797
+ * and only raw taps gains nothing from a reconnect — the user can call
1798
+ * `connect()` fresh next time they need it.
1799
+ */
1800
+ shouldAttemptReconnect() {
1801
+ if (this.reconnectMaxAttempts <= 0)
1802
+ return false;
1803
+ for (const sub of this.subscriptions) {
1804
+ if (sub instanceof TaskSubscription && !sub.isTerminated)
1805
+ return true;
1806
+ }
1807
+ return false;
1808
+ }
1809
+ /**
1810
+ * Reconnect loop. Backs off exponentially, retries up to `maxAttempts`. On
1811
+ * success: swap the transport, broadcast `qar.client.reconnected`, fire a
1812
+ * `task.resubscribe` per active task. On exhaustion: broadcast
1813
+ * `qar.client.reconnect_failed`, fail every subscription.
1814
+ *
1815
+ * Mid-loop user disconnect is honored — we check `state` after every async
1816
+ * boundary and bail with a clean close.
1817
+ */
1818
+ async runReconnectLoop(initialCause) {
1819
+ this.state = 'reconnecting';
1820
+ this.clearBackpressureState();
1821
+ let cause = initialCause;
1822
+ let lastError;
1823
+ for (let attempt = 1; attempt <= this.reconnectMaxAttempts; attempt++) {
1824
+ const delayMs = this.reconnectInitialBackoffMs *
1825
+ Math.pow(this.reconnectBackoffMultiplier, attempt - 1);
1826
+ this.broadcastClient({
1827
+ kind: 'qar.client.reconnecting',
1828
+ attempt,
1829
+ delayMs,
1830
+ cause,
1831
+ });
1832
+ await this.sleepCancellable(delayMs);
1833
+ if (this.state !== 'reconnecting') {
1834
+ // User called `disconnect()` (state -> 'disconnecting') or something
1835
+ // else tore us down. Walk the cleanup path; subs are still alive
1836
+ // because we deferred their close in `Connection.close()` while
1837
+ // reconnecting was true.
1838
+ this.finalizeDisconnect();
1839
+ return;
1840
+ }
1841
+ try {
1842
+ const newTransport = await this.factory({
1843
+ url: this.url,
1844
+ headers: this.headers,
1845
+ handlers: this.handlers,
1846
+ });
1847
+ if (this.state !== 'reconnecting') {
1848
+ // Disconnected while the upgrade was in flight — close the new
1849
+ // transport so we don't leak it, then walk cleanup.
1850
+ try {
1851
+ newTransport.close(1000, 'client disconnect');
1852
+ }
1853
+ catch {
1854
+ // best-effort
1855
+ }
1856
+ this.finalizeDisconnect();
1857
+ return;
1858
+ }
1859
+ this.transport = newTransport;
1860
+ this.state = 'connected';
1861
+ this.broadcastClient({ kind: 'qar.client.reconnected', attempt });
1862
+ this.replayActiveTasks();
1863
+ return;
1864
+ }
1865
+ catch (err) {
1866
+ lastError = err instanceof Error ? err : new Error(String(err));
1867
+ cause = lastError.message;
1868
+ }
1869
+ }
1870
+ // Max attempts exhausted — give up.
1871
+ this.broadcastClient({
1872
+ kind: 'qar.client.reconnect_failed',
1873
+ attempts: this.reconnectMaxAttempts,
1874
+ ...(lastError !== undefined ? { lastError: lastError.message } : {}),
1875
+ });
1876
+ this.state = 'failed';
1877
+ const failure = new Error(`Reconnect failed after ${this.reconnectMaxAttempts} attempts` +
1878
+ (lastError !== undefined ? `: ${lastError.message}` : ''));
1879
+ for (const sub of [...this.subscriptions]) {
1880
+ sub.fail(failure);
1881
+ }
1882
+ this.subscriptions.clear();
1883
+ }
1884
+ /**
1885
+ * For each active task subscription, send a `task.resubscribe` envelope
1886
+ * using its `lastSeenMessageId` as the anchor. The new envelope's id is
1887
+ * stitched into the subscription's chain so the server's replayed
1888
+ * envelopes route back transparently.
1889
+ *
1890
+ * **Active-subscription state machine.** A `TaskSubscription`
1891
+ * registered in `this.subscriptions` can be in one of three states at
1892
+ * any point in a connection's lifetime:
1893
+ *
1894
+ * 1. **pre-admission** — `tasks.start` sent its outbound envelope on
1895
+ * the wire and the SDK eagerly registered the subscription so
1896
+ * inbound routing can find it. The server-derived `session_id`
1897
+ * is NOT yet in {@link taskSessions} because no `task.started`
1898
+ * ack has been observed. `currentTaskId` IS set (derived from
1899
+ * `task.start.message_id` per the wire contract `task_id ==
1900
+ * task.start.message_id`).
1901
+ * 2. **admitted** — the `task.started` ack landed; the canonical
1902
+ * `task_id` is in `currentTaskId` and the matching `session_id`
1903
+ * is pinned in {@link taskSessions}. Resubscribe is fully
1904
+ * addressable.
1905
+ * 3. **terminated** — `task.done`, server-error, transport-fail,
1906
+ * or consumer-driven close has flipped `isTerminated` true.
1907
+ * `onClose` has already unsubscribed; subs in this state should
1908
+ * not be in the iterating snapshot.
1909
+ *
1910
+ * `replayActiveTasks` must handle states (1) and (2) explicitly.
1911
+ * State (1) is **unrecoverable** per the session-identity contract:
1912
+ * without an in-memory `session_id`, the SDK cannot construct a
1913
+ * wire-valid `task.resubscribe` for the server's pre-merge
1914
+ * bind-and-derive ingress (the resubscribe envelope MUST carry the
1915
+ * correct `session_id` field per the QAR wire schema).
1916
+ *
1917
+ * **Implementation note.** The implementation pre-checks the per-Task
1918
+ * session map BEFORE attempting the wire send. A state-(1) sub is
1919
+ * failed with a freshly-constructed `QodoColdAddressError` whose
1920
+ * stack starts cleanly inside `replayActiveTasks` — no wire
1921
+ * round-trip is attempted; the failure is local, typed, and
1922
+ * origin-traceable. An earlier implementation called
1923
+ * {@link sendEnvelope} first and relied on the synchronous throw
1924
+ * from {@link resolveOutboundSessionId} to route the failure into
1925
+ * `sub.fail(err)` — functionally correct but the thrown error
1926
+ * carried a misleading stack (the wire-send frames). The current
1927
+ * pre-check approach surfaces a typed local failure instead.
1928
+ * The consumer-facing API surface is unchanged: state-(1) subs still
1929
+ * surface `QodoColdAddressError` on their iterator. Lost-ack
1930
+ * idempotent retry (re-issuing the original `task.start` on the new
1931
+ * transport with the same idempotency-key) lives at the layer
1932
+ * above; the raw `Connection` ships the typed failure.
1933
+ *
1934
+ * Every `TaskSubscription` carries a known `task_id` from construction:
1935
+ * `tasks.continue` / `tasks.cancel` / `tasks.resubscribe` receive it from
1936
+ * the caller, and `tasks.start` derives it from the outbound
1937
+ * `task.start.message_id`. The `currentTaskId === undefined` case is
1938
+ * therefore unreachable here — we keep an invariant assertion rather
1939
+ * than the prior "in-flight task is unrecoverable" `sub.fail` path.
1940
+ */
1941
+ replayActiveTasks() {
1942
+ for (const sub of [...this.subscriptions]) {
1943
+ if (!(sub instanceof TaskSubscription))
1944
+ continue;
1945
+ if (sub.isTerminated)
1946
+ continue;
1947
+ const taskId = sub.currentTaskId;
1948
+ if (taskId === undefined) {
1949
+ // Invariant violation — see method comment. Fail the subscription so
1950
+ // the consumer's iterator doesn't hang silently, but this path should
1951
+ // never execute in practice.
1952
+ const dead = sub;
1953
+ queueMicrotask(() => {
1954
+ if (!dead.isTerminated) {
1955
+ dead.fail(new Error('Invariant: TaskSubscription has no task_id at reconnect — task.start should derive task_id from the outbound message_id.'));
1956
+ }
1957
+ });
1958
+ continue;
1959
+ }
1960
+ // Pre-admission shortcut: a sub with a `task_id` (derived from
1961
+ // `task.start.message_id`) but NO pinned `session_id` in
1962
+ // {@link taskSessions} is in state (1) — `task.started` never
1963
+ // landed before the drop. Fail it directly with a typed
1964
+ // `QodoColdAddressError` whose stack originates here rather than
1965
+ // routing through the wire-encoder's throw. See the JSDoc above
1966
+ // for the rationale.
1967
+ if (this.taskSessions.get(taskId) === undefined) {
1968
+ sub.fail(new QodoColdAddressError('task', taskId));
1969
+ continue;
1970
+ }
1971
+ const sinceMessageId = sub.lastSeenMessageId;
1972
+ try {
1973
+ const messageId = this.sendEnvelope({
1974
+ kind: 'task.resubscribe',
1975
+ payload: {
1976
+ task_id: taskId,
1977
+ since_message_id: sinceMessageId ?? null,
1978
+ },
1979
+ });
1980
+ sub.attachOutboundMessageId(messageId);
1981
+ }
1982
+ catch (err) {
1983
+ // Defensive backstop. The pre-check above eliminates the
1984
+ // QodoColdAddressError case, but `sendEnvelope` can still raise
1985
+ // on (a) transport mid-call disconnects (rare — the upgrade
1986
+ // succeeded one tick ago) or (b) a {@link canSend} race where
1987
+ // the transport closed between upgrade and send. Surface the
1988
+ // failure to the one subscription and let the others continue.
1989
+ sub.fail(err instanceof Error ? err : new Error(String(err)));
1990
+ }
1991
+ }
1992
+ }
1993
+ cleanCloseAndCleanup() {
1994
+ this.state = 'disconnected';
1995
+ this.clearBackpressureState();
1996
+ this.clearTerminalState();
1997
+ this.clearPendingTimers();
1998
+ for (const sub of [...this.subscriptions]) {
1999
+ sub.close();
2000
+ }
2001
+ this.subscriptions.clear();
2002
+ }
2003
+ failHardAndCleanup(err) {
2004
+ this.state = 'failed';
2005
+ this.clearBackpressureState();
2006
+ this.clearTerminalState();
2007
+ this.clearPendingTimers();
2008
+ for (const sub of [...this.subscriptions]) {
2009
+ sub.fail(err);
2010
+ }
2011
+ this.subscriptions.clear();
2012
+ }
2013
+ /**
2014
+ * Walk the user-initiated disconnect path from inside the reconnect loop.
2015
+ * The subs are still registered (we deferred their close); end them
2016
+ * cleanly so `for await` consumers see `done: true` rather than an error.
2017
+ */
2018
+ finalizeDisconnect() {
2019
+ this.state = 'disconnected';
2020
+ this.clearBackpressureState();
2021
+ this.clearTerminalState();
2022
+ this.clearPendingTimers();
2023
+ for (const sub of [...this.subscriptions]) {
2024
+ sub.close();
2025
+ }
2026
+ this.subscriptions.clear();
2027
+ }
2028
+ disposeTransportSafely() {
2029
+ try {
2030
+ this.transport.close();
2031
+ }
2032
+ catch {
2033
+ // see comments above — best-effort.
2034
+ }
2035
+ }
2036
+ clearPendingTimers() {
2037
+ for (const entry of this.pendingSleeps) {
2038
+ clearTimeout(entry.timer);
2039
+ entry.resolve();
2040
+ }
2041
+ this.pendingSleeps.clear();
2042
+ }
2043
+ /**
2044
+ * Sleep for `ms` milliseconds, cancellable by `clearPendingTimers`. Returns
2045
+ * even if the timer is cleared early — the caller checks `state` after the
2046
+ * await to decide whether to proceed. Cancelling resolves the Promise
2047
+ * rather than rejecting it: cancellation is a flow-control signal, not an
2048
+ * error, and the reconnect loop's post-await state check handles it.
2049
+ */
2050
+ sleepCancellable(ms) {
2051
+ return new Promise((resolve) => {
2052
+ const entry = {
2053
+ timer: setTimeout(() => {
2054
+ this.pendingSleeps.delete(entry);
2055
+ resolve();
2056
+ }, ms),
2057
+ resolve,
2058
+ };
2059
+ this.pendingSleeps.add(entry);
2060
+ });
2061
+ }
2062
+ }
2063
+ const ENVELOPE_KINDS = new Set([
2064
+ 'task.start',
2065
+ 'task.started',
2066
+ 'task.continue',
2067
+ 'task.cancel',
2068
+ 'task.canceling',
2069
+ 'task.resubscribe',
2070
+ 'task.delta',
2071
+ 'task.done',
2072
+ // Cold-start primitive admit/ack pair. Inbound parser must accept
2073
+ // `task.force_resumed` (the ack) or it falls through to the synthetic
2074
+ // `envelope_parse_error` path and the `tasks.forceResume` Promise
2075
+ // hangs. `task.forceResume` is included for symmetry — it's a
2076
+ // client→server kind but server-side echo / loopback tests + raw
2077
+ // taps (`client.receive()`) still parse it.
2078
+ 'task.forceResume',
2079
+ 'task.force_resumed',
2080
+ 'tool.request',
2081
+ 'tool.response',
2082
+ 'state.update',
2083
+ 'agent.spawn',
2084
+ 'bulletin.post',
2085
+ 'artifact.add',
2086
+ 'flow.pause',
2087
+ 'flow.resume',
2088
+ 'error',
2089
+ ]);
2090
+ /**
2091
+ * Decode a wire frame into an `Envelope`. Throws if the JSON doesn't shape-match
2092
+ * the discriminated union — the connection turns these into a synthetic `error`
2093
+ * envelope so the frame isn't silently dropped.
2094
+ */
2095
+ function parseEnvelope(text) {
2096
+ const raw = JSON.parse(text);
2097
+ if (typeof raw !== 'object' || raw === null) {
2098
+ throw new Error('envelope: expected JSON object');
2099
+ }
2100
+ const obj = raw;
2101
+ if (typeof obj.kind !== 'string' || !ENVELOPE_KINDS.has(obj.kind)) {
2102
+ throw new Error(`envelope: unknown kind "${String(obj.kind)}"`);
2103
+ }
2104
+ if (typeof obj.message_id !== 'string') {
2105
+ throw new Error('envelope: missing message_id');
2106
+ }
2107
+ if (typeof obj.session_id !== 'string') {
2108
+ throw new Error('envelope: missing session_id');
2109
+ }
2110
+ if (typeof obj.ts !== 'string') {
2111
+ throw new Error('envelope: missing ts');
2112
+ }
2113
+ if (obj.envelope_version !== 1) {
2114
+ throw new Error(`envelope: unsupported envelope_version ${String(obj.envelope_version)}`);
2115
+ }
2116
+ if (typeof obj.payload !== 'object' || obj.payload === null) {
2117
+ throw new Error('envelope: missing payload');
2118
+ }
2119
+ // The shape passes the discriminator gate — we trust the rest. QAR's
2120
+ // server-side validators are the source of truth; any over-strict checks
2121
+ // here would surface as false-positive `envelope_parse_error` events for
2122
+ // forward-compatible additions.
2123
+ return raw;
2124
+ }
2125
+ const KINDS_WITH_TASK_ID = new Set([
2126
+ 'task.started',
2127
+ 'task.continue',
2128
+ 'task.cancel',
2129
+ 'task.canceling',
2130
+ 'task.resubscribe',
2131
+ 'task.delta',
2132
+ 'task.done',
2133
+ // `task.force_resumed` carries `task_id` on its payload (the recovered
2134
+ // task's id). Membership here lets the `task_id` fallback in
2135
+ // {@link TaskSubscription.matches} ABSORB the ack — e.g. so a cold
2136
+ // `task.resubscribe` that replays a buffered `task.force_resumed` (foreign
2137
+ // parent) still seeds the subscription chain for the events that follow.
2138
+ // Absorption is all the fallback grants: whether the ack drives
2139
+ // admission-resolution / one-shot close is gated separately on a chain
2140
+ // match in {@link TaskSubscription.consider} (`matchedByChain`), so a
2141
+ // fallback-only match never collapses an unrelated in-flight stream. Same
2142
+ // contract applies to `task.started` above.
2143
+ 'task.force_resumed',
2144
+ ]);
2145
+ function envelopeHasTaskId(env) {
2146
+ return KINDS_WITH_TASK_ID.has(env.kind);
2147
+ }
2148
+ const TASK_EVENT_KINDS = new Set([
2149
+ 'task.delta',
2150
+ 'task.done',
2151
+ 'tool.request',
2152
+ 'state.update',
2153
+ 'agent.spawn',
2154
+ 'bulletin.post',
2155
+ 'artifact.add',
2156
+ 'flow.pause',
2157
+ 'flow.resume',
2158
+ 'error',
2159
+ ]);
2160
+ function isTaskEvent(env) {
2161
+ return TASK_EVENT_KINDS.has(env.kind);
2162
+ }
2163
+ function pickPositiveInt(value, fallback) {
2164
+ if (value === undefined)
2165
+ return fallback;
2166
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 1)
2167
+ return fallback;
2168
+ return value;
2169
+ }
2170
+ /**
2171
+ * Like {@link pickPositiveInt} but allows `0` — used for `reconnect.maxAttempts`
2172
+ * where `0` is a meaningful configuration ("don't reconnect, fail immediately
2173
+ * on transport drop"). Negatives, fractions, and non-integers still fall back.
2174
+ */
2175
+ function pickNonNegativeInt(value, fallback) {
2176
+ if (value === undefined)
2177
+ return fallback;
2178
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0)
2179
+ return fallback;
2180
+ return value;
2181
+ }
2182
+ function pickPositiveNumber(value, fallback) {
2183
+ if (value === undefined)
2184
+ return fallback;
2185
+ if (!Number.isFinite(value) || value <= 0)
2186
+ return fallback;
2187
+ return value;
2188
+ }
2189
+ //# sourceMappingURL=connection.js.map