@qodo/sdk 0.13.3 → 2.0.0-next.0

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